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
+
+
@@ -318,6 +321,9 @@
Leaderboard
Auto-Weiter
+
+
",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.
Clear Filters
- `;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}`),`
+ data-provider-count="${y}"
+ ${T}>
${A.escapeHtml(o.name)}
${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+=`
-
+ `}}).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"))}}),d?document.getElementById("start-game")?.classList.remove("hidden"):document.getElementById("start-game")?.classList.add("hidden"),a.selectedPlaylists.length===0)try{let o=localStorage.getItem(I),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 f=parseInt(u.dataset.providerCount,10)||0;f>0&&!a.selectedPlaylists.some(v=>v.path===m)&&(a.selectedPlaylists.push({path:m,songCount:f}),u.closest(".playlist-item")?.classList.add("is-selected"))}})}catch(o){console.warn("[Beatify] restore saved playlists failed:",o)}Xe(),D()}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(),D()}function ht(e){let t=document.getElementById("playlist-filter-bar");if(!t)return;let n=new Set;if(e.forEach(d=>{(d.tags||[]).forEach(o=>n.add(o))}),n.size===0){t.classList.add("hidden");return}let i=d=>d.charAt(0).toUpperCase()+d.slice(1),s='';Object.entries($e).forEach(([d,o])=>{let r=o.tags.filter(m=>n.has(m));if(r.length===0)return;let c=a.activeFilters[d]||"";s+=`
+
${o.label}
${r.map(m=>{let u=c===m?"selected":"";return`${i(m)} `}).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 ")}
Clear
- `),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?'\u{1F451} ':"",f=m?'TOUR ':"",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='\u23F3 '+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?'\xD7 ':"";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?'\u{1F451} ':"",y=m?'TOUR ':"",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='\u23F3 '+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?'\xD7 ':"";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 + ' ' +
'' + streakIndicator + changeIndicator + ' ' +
'' + 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 + ' ' +
'
' +
streakIndicator +
changeIndicator +
@@ -886,6 +899,11 @@
// Render leaderboard with position changes
renderRevealLeaderboard(data.leaderboard || []);
+ // Issue #827: Sudden-Death — full-bleed "OUT" takeover for this round's
+ // eliminations + FINAL banner (2 players left).
+ renderSuddenDeathOut(data);
+ renderSuddenDeathFinalBanner(data, 'sd-final-banner-reveal');
+
// Render motivational message (Story 14.4)
renderMotivationalMessage(data.game_performance);
@@ -1278,6 +1296,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 ? '💀 ' : '';
+
// Position change indicator (AC 10.4.4 - with arrows)
var changeHtml = '';
if (entry.rank_change > 0) {
@@ -1293,9 +1316,9 @@
streakIndicator = '🔥' + entry.streak + ' ';
}
- html += '' +
+ html += '
' +
'
#' + entry.rank + ' ' +
- '
' + utils.escapeHtml(entry.name) + awayBadge + ' ' +
+ '
' + skullPrefix + utils.escapeHtml(entry.name) + awayBadge + ' ' +
'
' +
streakIndicator +
changeHtml +
@@ -1318,6 +1341,9 @@
function renderEndView(data) {
var leaderboard = data.leaderboard || [];
+ // Issue #827: Sudden-Death "Last One Standing" hero above the podium.
+ renderSuddenDeathLastStanding(data);
+
// Update podium (AC 10.4.5) — name, score, and a colour-keyed avatar.
[1, 2, 3].forEach(function(place) {
var player = leaderboard.find(function(p) { return p.rank === place; });
@@ -1374,9 +1400,14 @@
var disconnectedClass = entry.connected === false ? 'leaderboard-entry--disconnected' : '';
var awayBadge = entry.connected === false ? '(away) ' : '';
- html += '' +
+ // 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 ? '💀 ' : '';
+
+ html += '
' +
'#' + entry.rank + ' ' +
- '' + utils.escapeHtml(entry.name) + awayBadge + ' ' +
+ '' + skullPrefix + utils.escapeHtml(entry.name) + awayBadge + ' ' +
'' + entry.score + ' ' +
'
';
});
@@ -1536,6 +1567,111 @@
container.classList.remove('hidden');
}
+ // ============================================
+ // Sudden Death (Issue #827)
+ // ============================================
+
+ /**
+ * Issue #827: full-bleed "OUT" takeover (design S4-C). Called on REVEAL.
+ * When sudden_death_mode is on and the state carries a non-empty
+ * eliminated_this_round, flash the overlay with the eliminated name(s) for
+ * ~2.5s. Deduped per (round, names) so re-renders/re-broadcasts of the same
+ * REVEAL don't re-trigger it. This is a TV display — the overlay auto-hides
+ * so it never permanently blocks the underlying reveal.
+ * @param {Object} data - REVEAL state data
+ */
+ function renderSuddenDeathOut(data) {
+ var overlay = document.getElementById('sd-out-overlay');
+ if (!overlay) return;
+
+ var names = data.sudden_death_mode ? (data.eliminated_this_round || []) : [];
+ if (!names.length) {
+ // No elimination this round — make sure the key resets so a future
+ // elimination with the same names (different round) still fires.
+ return;
+ }
+
+ var key = (data.round || 0) + ':' + names.join(',');
+ if (key === sdLastOutKey) return; // already shown for this elimination
+ sdLastOutKey = key;
+
+ // Big word uses the localized game.out (uppercased).
+ var wordEl = overlay.querySelector('.sd-out__word');
+ if (wordEl) wordEl.textContent = (utils.t('game.out', 'OUT') || 'OUT').toUpperCase();
+
+ var nameEl = document.getElementById('sd-out-name');
+ if (nameEl) nameEl.textContent = names.join(', ');
+
+ overlay.classList.add('show');
+ if (sdOutTimer) clearTimeout(sdOutTimer);
+ sdOutTimer = setTimeout(function() {
+ overlay.classList.remove('show');
+ sdOutTimer = null;
+ }, 2500);
+ }
+
+ /**
+ * Issue #827: Sudden-Death FINAL banner (design S5-C). Shown in the
+ * PLAYING and REVEAL chip strips when exactly 2 players are still in the
+ * game (non-eliminated) AND sudden_death_mode is on. Hidden otherwise.
+ * @param {Object} data - State data
+ * @param {string} bannerId - id of the banner element for this phase
+ */
+ function renderSuddenDeathFinalBanner(data, bannerId) {
+ var banner = document.getElementById(bannerId);
+ if (!banner) return;
+
+ var players = data.players || [];
+ var alive = players.filter(function(p) { return !p.eliminated; });
+
+ if (data.sudden_death_mode && alive.length === 2) {
+ banner.textContent = '💀 ' + utils.t('game.finalShowdown', 'FINAL — SUDDEN DEATH');
+ banner.classList.remove('hidden');
+ } else {
+ banner.classList.add('hidden');
+ }
+ }
+
+ /**
+ * Issue #827: END "Last One Standing" hero (design S6-C). Shown above the
+ * podium/superlatives only in Sudden Death mode. The survivor is the single
+ * non-eliminated entry in the final leaderboard, or — failing that — the
+ * player_name from the last_one_standing superlative.
+ * @param {Object} data - END state data
+ */
+ function renderSuddenDeathLastStanding(data) {
+ var hero = document.getElementById('sd-last-standing');
+ if (!hero) return;
+
+ if (!data.sudden_death_mode) {
+ hero.classList.add('hidden');
+ return;
+ }
+
+ var leaderboard = data.leaderboard || [];
+ var survivors = leaderboard.filter(function(e) { return !e.eliminated; });
+ var winner = survivors.length ? survivors[0].name : null;
+
+ // Fallback to the last_one_standing superlative's player_name.
+ if (!winner && data.superlatives) {
+ var award = data.superlatives.find(function(a) { return a.id === 'last_one_standing'; });
+ if (award) winner = award.player_name;
+ }
+
+ if (!winner) {
+ hero.classList.add('hidden');
+ return;
+ }
+
+ var headlineEl = hero.querySelector('.sd-last-standing__headline');
+ if (headlineEl) headlineEl.textContent = utils.t('game.lastOneStanding', 'Last One Standing');
+
+ var winnerEl = document.getElementById('sd-last-standing-winner');
+ if (winnerEl) winnerEl.textContent = winner;
+
+ hero.classList.remove('hidden');
+ }
+
// ============================================
// Confetti System (Story 14.5 - AC7)
// ============================================
diff --git a/custom_components/beatify/www/js/dashboard.min.js b/custom_components/beatify/www/js/dashboard.min.js
index 8374d163..e331e7b4 100644
--- a/custom_components/beatify/www/js/dashboard.min.js
+++ b/custom_components/beatify/www/js/dashboard.min.js
@@ -1 +1 @@
-(function(){"use strict";var o=window.BeatifyUtils||{},m=o.debug||function(){},P=document.getElementById("dashboard-loading"),U=document.getElementById("dashboard-starting"),z=document.getElementById("dashboard-no-game"),q=document.getElementById("dashboard-lobby"),K=document.getElementById("dashboard-playing"),$=document.getElementById("dashboard-reveal"),AA=document.getElementById("dashboard-end"),eA=document.getElementById("dashboard-paused"),aA=[P,U,z,q,K,$,AA,eA],g=null,I=0,x=o.createReconnectGuard&&o.createReconnectGuard()||(function(){var A=null;return{schedule:function(e,n){A!==null&&clearTimeout(A),A=setTimeout(function(){A=null,e()},n)},cancel:function(){A!==null&&(clearTimeout(A),A=null)},isPending:function(){return A!==null}}})(),F=3e4,W=[],C=null,T=null;function b(A){o.showView(aA,A)}function tA(){return o.reconnectBackoffDelay?o.reconnectBackoffDelay(I,{maxDelay:F}):Math.min(1e3*Math.pow(2,I-1),F)}function D(){if(x.cancel(),g){g.onopen=g.onmessage=g.onclose=g.onerror=null;try{g.close()}catch{}}var A=window.location.protocol==="https:"?"wss:":"ws:",e=A+"//"+window.location.host+"/beatify/ws";g=new WebSocket(e),g.onopen=function(){m("[Dashboard] WebSocket connected"),I=0,g.send(JSON.stringify({type:"get_state"}))},g.onmessage=function(n){try{var a=JSON.parse(n.data);cA(a)}catch(r){console.error("[Dashboard] Failed to parse message:",r)}},g.onclose=function(){m("[Dashboard] WebSocket closed"),I++,b("dashboard-no-game");var n=tA();m("[Dashboard] Reconnecting in "+n+"ms (attempt "+I+")"),x.schedule(D,n)},g.onerror=function(n){console.error("[Dashboard] WebSocket error:",n)}}var M=null,L=null,_=!1;function nA(){if(L)return L;if(typeof window<"u"&&typeof window.NoSleep=="function")try{L=new window.NoSleep}catch(A){console.debug("[BeatifyWakeLock] NoSleep instantiation failed:",A)}return L}var Q=null,rA="data:video/mp4;base64,AAAAHGZ0eXBNNFYgAAACAGlzb21pc28yYXZjMQAAAAhmcmVlAAAGF21kYXTeBAAAbGliZmFhYyAxLjI4AABCAJMgBDIARwAAArEGBf//rdxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNDIgcjIgOTU2YzhkOCAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMTQgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDE6MHgxMTEgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02IGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MCB3ZWlnaHRwPTAga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCB2YnZfbWF4cmF0ZT03NjggdmJ2X2J1ZnNpemU9MzAwMCBjcmZfbWF4PTAuMCBuYWxfaHJkPW5vbmUgZmlsbGVyPTAgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAFZliIQL8mKAAKvMnJycnJycnJycnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXiEASZACGQAjgCEASZACGQAjgAAAAAdBmjgX4GSAIQBJkAIZACOAAAAAB0GaVAX4GSAhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGagC/AySEASZACGQAjgAAAAAZBmqAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZrAL8DJIQBJkAIZACOAAAAABkGa4C/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmwAvwMkhAEmQAhkAI4AAAAAGQZsgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGbQC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm2AvwMkhAEmQAhkAI4AAAAAGQZuAL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGboC/AySEASZACGQAjgAAAAAZBm8AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZvgL8DJIQBJkAIZACOAAAAABkGaAC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmiAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpAL8DJIQBJkAIZACOAAAAABkGaYC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmoAvwMkhAEmQAhkAI4AAAAAGQZqgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGawC/AySEASZACGQAjgAAAAAZBmuAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZsAL8DJIQBJkAIZACOAAAAABkGbIC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm0AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZtgL8DJIQBJkAIZACOAAAAABkGbgCvAySEASZACGQAjgCEASZACGQAjgAAAAAZBm6AnwMkhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AAAAhubW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAABDcAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAzB0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAA+kAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAALAAAACQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAPpAAAAAAABAAAAAAKobWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAB1MAAAdU5VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAACU21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAhNzdGJsAAAAr3N0c2QAAAAAAAAAAQAAAJ9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAALAAkABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAALWF2Y0MBQsAN/+EAFWdCwA3ZAsTsBEAAAPpAADqYA8UKkgEABWjLg8sgAAAAHHV1aWRraEDyXyRPxbo5pRvPAyPzAAAAAAAAABhzdHRzAAAAAAAAAAEAAAAeAAAD6QAAABRzdHNzAAAAAAAAAAEAAAABAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAAIxzdHN6AAAAAAAAAAAAAAAeAAADDwAAAAsAAAALAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAAiHN0Y28AAAAAAAAAHgAAAEYAAANnAAADewAAA5gAAAO0AAADxwAAA+MAAAP2AAAEEgAABCUAAARBAAAEXQAABHAAAASMAAAEnwAABLsAAATOAAAE6gAABQYAAAUZAAAFNQAABUgAAAVkAAAFdwAABZMAAAWmAAAFwgAABd4AAAXxAAAGDQAABGh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAABDcAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAQkAAADcAABAAAAAAPgbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAC7gAAAykBVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAADi21pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAADT3N0YmwAAABnc3RzZAAAAAAAAAABAAAAV21wNGEAAAAAAAAAAQAAAAAAAAAAAAIAEAAAAAC7gAAAAAAAM2VzZHMAAAAAA4CAgCIAAgAEgICAFEAVBbjYAAu4AAAADcoFgICAAhGQBoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAADIAAAQAAAAAAQAAAkAAAAFUc3RzYwAAAAAAAAAbAAAAAQAAAAEAAAABAAAAAgAAAAIAAAABAAAAAwAAAAEAAAABAAAABAAAAAIAAAABAAAABgAAAAEAAAABAAAABwAAAAIAAAABAAAACAAAAAEAAAABAAAACQAAAAIAAAABAAAACgAAAAEAAAABAAAACwAAAAIAAAABAAAADQAAAAEAAAABAAAADgAAAAIAAAABAAAADwAAAAEAAAABAAAAEAAAAAIAAAABAAAAEQAAAAEAAAABAAAAEgAAAAIAAAABAAAAFAAAAAEAAAABAAAAFQAAAAIAAAABAAAAFgAAAAEAAAABAAAAFwAAAAIAAAABAAAAGAAAAAEAAAABAAAAGQAAAAIAAAABAAAAGgAAAAEAAAABAAAAGwAAAAIAAAABAAAAHQAAAAEAAAABAAAAHgAAAAIAAAABAAAAHwAAAAQAAAABAAAA4HN0c3oAAAAAAAAAAAAAADMAAAAaAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAACMc3RjbwAAAAAAAAAfAAAALAAAA1UAAANyAAADhgAAA6IAAAO+AAAD0QAAA+0AAAQAAAAEHAAABC8AAARLAAAEZwAABHoAAASWAAAEqQAABMUAAATYAAAE9AAABRAAAAUjAAAFPwAABVIAAAVuAAAFgQAABZ0AAAWwAAAFzAAABegAAAX7AAAGFwAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTUuMzMuMTAw";function sA(){if(Q)return Q;try{var A=document.createElement("video");A.muted=!0,A.setAttribute("muted",""),A.setAttribute("autoplay",""),A.setAttribute("playsinline",""),A.setAttribute("loop",""),A.setAttribute("title","Beatify keep awake"),A.style.cssText="position:fixed;left:-1px;top:-1px;width:1px;height:1px;opacity:0;pointer-events:none;";var e=document.createElement("source");e.src=rA,e.type="video/mp4",A.appendChild(e),(document.body||document.documentElement).appendChild(A),Q=A}catch(n){console.debug("[BeatifyWakeLock] Layer 2a video create failed:",n)}return Q}async function H(){if("wakeLock"in navigator)try{return M=await navigator.wakeLock.request("screen"),M.addEventListener("release",function(){console.debug("[BeatifyWakeLock] Layer 1 released by browser"),M=null}),console.debug("[BeatifyWakeLock] Layer 1 (native wakeLock) acquired"),!0}catch(r){console.debug("[BeatifyWakeLock] Layer 1 request failed:",r,"\u2014 trying Layer 2")}else console.debug("[BeatifyWakeLock] Layer 1 unavailable \u2014 using Layer 2");var A=sA();if(A)try{var e=A.play();e&&typeof e.catch=="function"&&e.catch(function(r){console.debug("[BeatifyWakeLock] Layer 2a muted-video play rejected:",r)})}catch(r){console.debug("[BeatifyWakeLock] Layer 2a play threw:",r)}var n=nA();if(!n)return console.debug("[BeatifyWakeLock] Layer 2 unavailable (NoSleep vendor not loaded)"),!1;if(_)return!1;try{var a=n.enable();_=!0,a&&typeof a.catch=="function"&&a.catch(function(r){_=!1,console.debug("[BeatifyWakeLock] Layer 2 enable promise rejected:",r)}),console.debug("[BeatifyWakeLock] Layer 2 (NoSleep video) enabled")}catch(r){console.debug("[BeatifyWakeLock] Layer 2 enable failed:",r),_=!1}return!1}var J="beatify_wakelock_banner_dismissed";function iA(){try{return localStorage.getItem(J)==="1"}catch{return!1}}function R(){try{localStorage.setItem(J,"1")}catch{}}function N(){var A=document.getElementById("dashboard-wakelock-banner");A&&A.classList.add("hidden")}function oA(){if(!iA()){var A=document.getElementById("dashboard-wakelock-banner");A&&A.classList.remove("hidden")}}function dA(){var A=document.getElementById("dashboard-wakelock-banner");if(A){var e=document.getElementById("dashboard-wakelock-activate"),n=document.getElementById("dashboard-wakelock-dismiss");e&&e.addEventListener("click",function(){H(),R(),N()}),n&&n.addEventListener("click",function(){R(),N()})}}document.addEventListener("visibilitychange",function(){document.visibilityState==="visible"&&(H(),(!g||g.readyState===WebSocket.CLOSING||g.readyState===WebSocket.CLOSED)&&(m("[Dashboard] Page visible, WebSocket dead \u2014 reconnecting immediately."),I=0,D()))});function cA(A){A.type==="state"?(A.game_performance&&m("[Dashboard] game_performance:",A.game_performance),j(A)):A.type==="error"?m("[Dashboard] Server error:",A.message):A.type==="player_reaction"?SA(A.player_name,A.emoji):A.type==="metadata_update"?lA(A.song):A.type==="game_starting"&&(p(),b("dashboard-starting"))}function lA(A){if(A){var e=document.getElementById("dashboard-album-art");if(e&&A.album_art){var n=A.album_art;if(e.src===n)return;e.style.transition="opacity 0.3s ease-in-out",e.style.opacity="0.5";var a=new Image;a.onload=function(){e.src=n,e.style.opacity="1"},a.onerror=function(){e.src="/beatify/static/img/no-artwork.svg",e.style.opacity="1"},a.src=n}m("[Dashboard] Metadata updated:",A.artist,"-",A.title)}}function j(A){var e=A.phase;if(typeof BeatifyI18n<"u"&&A.language&&A.language!==BeatifyI18n.getLanguage()){BeatifyI18n.setLanguage(A.language).then(function(n){BeatifyI18n.initPageTranslations(),n===BeatifyI18n.getLanguage()&&(A.language!==n&&(A=Object.assign({},A,{language:n})),j(A))});return}if(!e||e==="END"&&!A.game_id){b("dashboard-no-game"),p();return}switch(e){case"LOBBY":p(),b("dashboard-lobby"),uA(A);break;case"PLAYING":b("dashboard-playing"),gA(A);break;case"REVEAL":p(),b("dashboard-reveal"),yA(A);break;case"END":p(),b("dashboard-end"),XA(A);break;case"PAUSED":p(),b("dashboard-paused");break;default:m("[Dashboard] Unknown phase:",e)}}function uA(A){var e=A.players||[];A.join_url&&mA(A.join_url),fA(A);var n=document.getElementById("dashboard-player-count");if(n){var a=e.length,r=a===1?"dashboard.playersJoinedOne":"dashboard.playersJoined",s=a+" player"+(a!==1?"s":"")+" joined";n.textContent=o.t(r,s).replace(/\{n\}/g,a)}vA(e)}function fA(A){var e=document.getElementById("dashboard-game-settings");if(e){var n=A.total_rounds||10,a=A.difficulty||"normal",r=t("admin.difficulty"+a.charAt(0).toUpperCase()+a.slice(1),a);e.textContent=n+" "+o.t("dashboard.rounds","rounds")+" \u2022 "+r}}function mA(A){var e=document.getElementById("dashboard-qr-code");e&&A!==T&&(T=A,e.innerHTML="",typeof QRCode<"u"?new QRCode(e,{text:A,width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):e.innerHTML="
QR code unavailable
")}function vA(A){var e=document.getElementById("dashboard-player-list");if(e){var n=A.slice().sort(function(s,d){return s.connected!==d.connected?s.connected?-1:1:0}),a=W.map(function(s){return s.name}),r=n.filter(function(s){return a.indexOf(s.name)===-1}).map(function(s){return s.name});e.innerHTML=n.map(function(s){var d=r.indexOf(s.name)!==-1,u=s.connected===!1,c=["dashboard-player-card"];d&&c.push("is-new"),u&&c.push("dashboard-player-card--disconnected");var i=u?'
(away) ':"";return'
'+o.escapeHtml(s.name)+i+"
"}).join(""),setTimeout(function(){for(var s=e.querySelectorAll(".is-new"),d=0;d
0?v="leaderboard-entry--climbing":i.rank_change<0&&(v="leaderboard-entry--falling");var y=i.connected===!1?"leaderboard-entry--disconnected":"",B=i.connected===!1?'(away) ':"",f="";i.rank_change>0?f='\u25B2'+i.rank_change+" ":i.rank_change<0&&(f='\u25BC'+Math.abs(i.rank_change)+" ");var h="";if(i.streak>=2){var S=i.streak>=5?"streak-indicator--hot":"";h='\u{1F525}'+i.streak+" "}var V="";r&&u[i.name]&&(V='BET ');var O="";if(a){var xA=d[i.name]===!0;O='
'}c+='#'+i.rank+' '+o.escapeHtml(i.name)+B+V+' '+h+f+' '+i.score+" "+O+"
"}),s.innerHTML=c}}function yA(A){var e=A.song||{},n=A.players||[],a=document.getElementById("reveal-album-art");a&&(a.src=e.album_art||"/beatify/static/img/no-artwork.svg",a.onerror=function(){this.src="/beatify/static/img/no-artwork.svg"});var r=document.getElementById("reveal-artist"),s=document.getElementById("reveal-title"),d=document.getElementById("reveal-year");r&&(r.textContent=e.artist||"Unknown Artist"),s&&(s.textContent=e.title||"Unknown Song"),d&&(d.textContent=e.year||"????");var u=!!A.title_artist_mode,c=document.getElementById("reveal-year-row");if(c&&c.classList.toggle("hidden",u),BA(A),kA(u?null:A.artist_challenge),wA(e),_A(n),QA(A.leaderboard||[]),CA(A.game_performance),IA(A),LA(A.song_difficulty),A.game_performance&&A.game_performance.is_new_record)G("record");else{var i=n.some(function(l){return l.years_off===0&&!l.missed_round});i&&G("exact")}}function BA(A){var e=document.getElementById("dashboard-ta-banner");if(e){var n=A.title_artist_challenge||null;if(!A.title_artist_mode||!n||!n.correct_title){e.classList.add("hidden"),Z();return}e.classList.remove("hidden");var a=document.getElementById("dashboard-ta-title"),r=document.getElementById("dashboard-ta-artist");a&&(a.textContent=n.correct_title||""),r&&(r.textContent=n.correct_artist||"");var s=!!n.voting_open,d=n.near_misses||[],u=n.near_miss_outcomes||[],c=document.getElementById("dashboard-ta-voting"),i=document.getElementById("dashboard-ta-live"),l=document.getElementById("dashboard-ta-outcomes"),v=function(f){return f==="artist"?o.t("titleArtist.artistLabel","Artist"):o.t("titleArtist.titleLabel","Song title")};if(s&&d.length>0&&i){c&&c.classList.add("hidden"),l&&(l.innerHTML="",l.classList.add("hidden"));var y=d.map(function(f){var h=o.taTallyPercents(f.votes_yes,f.votes_no);return''+o.escapeHtml(f.player)+' '+o.escapeHtml(v(f.field))+'
\u201C'+o.escapeHtml(f.guess||"\u2014")+'\u201D
\u{1F44D} '+(f.votes_yes||0)+' \u{1F44E} '+(f.votes_no||0)+"
"}).join("");i.innerHTML=''+o.escapeHtml(o.t("titleArtist.dashboardDeciding","The room is deciding\u2026"))+'
'+y+"
",i.classList.remove("hidden"),EA(n.vote_seconds_remaining);return}if(Z(),i&&(i.innerHTML="",i.classList.add("hidden")),c&&c.classList.add("hidden"),l)if(!s&&u.length>0){var B=u.map(function(f){var h=!!f.accepted;return''+o.escapeHtml(f.player)+' '+o.escapeHtml(v(f.field))+' '+o.escapeHtml(o.taVerdictLabel(h,f.points))+"
"}).join("");l.innerHTML=''+o.escapeHtml(o.t("titleArtist.closeCallsDecided","Close calls \u2014 decided"))+'
'+B+"
",l.classList.remove("hidden")}else l.innerHTML="",l.classList.add("hidden")}}function kA(A){var e=document.getElementById("reveal-artist-challenge");if(e){if(!A||!A.correct_artist){e.classList.add("hidden"),e.innerHTML="";return}var n=o.t("artistChallenge.theArtistWas","The artist"),a;if(A.winner){var r=A.bonus_points||5;a=''+o.escapeHtml(A.winner)+" +"+r+" "}else a=''+o.escapeHtml(o.t("artistChallenge.noWinner","Nobody guessed it"))+" ";e.innerHTML='\u{1F3A4} '+o.escapeHtml(n)+' '+o.escapeHtml(A.correct_artist)+" "+a,e.classList.remove("hidden")}}var X=null;function EA(A){Z();var e=document.getElementById("dashboard-ta-live-cd");if(!e)return;var n=typeof A=="number"?A:0,a=Date.now()+n*1e3;function r(){var s=Math.max(0,Math.ceil((a-Date.now())/1e3));e.textContent=s+"s",s<=0&&Z()}r(),X=setInterval(r,500)}function Z(){X&&(clearInterval(X),X=null)}function wA(A){var e=document.getElementById("dashboard-fun-fact"),n=document.getElementById("dashboard-fun-fact-text"),a=o.getLocalizedSongField(A,"fun_fact");if(m("[Dashboard] renderFunFact called with song:",A),m("[Dashboard] fun_fact value:",a||"no fun fact"),!e||!n){console.warn("[Dashboard] Fun fact elements not found");return}if(!a||a.trim()===""){e.classList.add("hidden"),m("[Dashboard] No fun_fact, hiding container");return}n.textContent=a,e.classList.remove("hidden"),m("[Dashboard] Fun fact shown:",a)}var k=null;function IA(A){var e=document.getElementById("reveal-countdown"),n=document.getElementById("reveal-countdown-num"),a=e?e.querySelector(".chip-countdown-fg"):null;if(!e||!n||!a)return;k!==null&&(clearInterval(k),k=null);var r=A.reveal_auto_advance||0,s=A.reveal_started_at||0,d=!!A.idle_halt;if(r<=0||!s||d){e.classList.add("hidden");return}e.classList.remove("hidden");var u=157.08;a.style.strokeDasharray=u;function c(){var i=Math.max(0,s+r*1e3-Date.now()),l=Math.ceil(i/1e3);n.textContent=l;var v=i/(r*1e3);a.style.strokeDashoffset=String(u*(1-v)),i<=0&&k!==null&&(clearInterval(k),k=null)}c(),k=setInterval(c,500)}function CA(A){var e=document.getElementById("reveal-motivational");if(e){if(!A||!A.message){e.classList.add("hidden");return}var n=A.message,a=e.querySelector(".motivational-icon"),r=e.querySelector(".motivational-text");e.className="motivational-message motivational-message--"+n.type;var s={first:"\u{1F31F}",record:"\u{1F3C6}",strong:"\u{1F525}",above:"\u{1F4C8}",close:"\u{1F4AA}"};a&&(a.textContent=s[n.type]||""),r&&(r.textContent=n.message||"")}}function LA(A){var e=document.getElementById("song-difficulty");if(e){if(!A){e.classList.add("hidden");return}for(var n="",a=0;a★';e.innerHTML=''+n+'
'+o.t("difficulty."+A.label)+' '+A.accuracy+"% "+o.t("difficulty.accuracy")+" ",e.classList.remove("hidden")}}function _A(A){var e=document.getElementById("reveal-top-guesses-list");if(e){var n=A.filter(function(r){return!r.missed_round}).sort(function(r,s){return(s.round_score||0)-(r.round_score||0)}).slice(0,3),a="";n.forEach(function(r,s){var d=r.guess?'('+r.guess+") ":"",u="";if(r.bet){var c="bet-badge";r.bet_outcome==="won"?c+=" bet-badge--won":r.bet_outcome==="lost"&&(c+=" bet-badge--lost"),u='BET '}a+='#'+(s+1)+' '+o.escapeHtml(r.name)+d+' +'+(r.round_score||0)+u+"
"}),e.innerHTML=a}}function QA(A){var e=document.getElementById("reveal-leaderboard");if(e){var n="";A.forEach(function(a){var r=a.rank<=3?"is-top-"+a.rank:"",s="";a.rank_change>0?s="leaderboard-entry--climbing":a.rank_change<0&&(s="leaderboard-entry--falling");var d=a.connected===!1?"leaderboard-entry--disconnected":"",u=a.connected===!1?'(away) ':"",c="";a.rank_change>0?c='\u25B2'+a.rank_change+" ":a.rank_change<0&&(c='\u25BC'+Math.abs(a.rank_change)+" ");var i="";if(a.streak>=2){var l=a.streak>=5?"streak-indicator--hot":"";i='\u{1F525}'+a.streak+" "}n+='#'+a.rank+' '+o.escapeHtml(a.name)+u+' '+i+c+' '+a.score+"
"}),e.innerHTML=n}}function XA(A){var e=A.leaderboard||[];[1,2,3].forEach(function(l){var v=e.find(function(S){return S.rank===l}),y=document.getElementById("end-podium-"+l+"-name"),B=document.getElementById("end-podium-"+l+"-score"),f=document.getElementById("end-podium-"+l+"-avatar");if(y&&(y.textContent=v?v.name:"---"),B&&(B.textContent=v?v.score:"0"),f){var h=v?v.name:"";f.textContent=h?h.trim().charAt(0).toUpperCase():"",f.style.background=h?ZA(h):"transparent"}});var n=A.game_stats||{},a=document.getElementById("end-meta-rounds"),r=document.getElementById("end-meta-players");a&&(a.textContent="\u{1F3B5} "+(n.total_rounds!=null?n.total_rounds:e.length&&A.round||0)),r&&(r.textContent="\u{1F465} "+(n.total_players!=null?n.total_players:e.length)),MA(A.game_performance),HA(A.superlatives),DA(A.highlights);var s=e.find(function(l){return l.rank===1});s&&s.score>0&&G("winner");var d=document.getElementById("end-leaderboard");if(d){var u=e.filter(function(l){return l.rank>3}),c=u.length?u:e,i="";c.forEach(function(l){var v=l.rank<=3?"is-top-"+l.rank:"",y=l.connected===!1?"leaderboard-entry--disconnected":"",B=l.connected===!1?'(away) ':"";i+='#'+l.rank+' '+o.escapeHtml(l.name)+B+' '+l.score+"
"}),d.innerHTML=i}}function ZA(A){for(var e=[["#ff2d6a","#ff6600"],["#00f5ff","#7a5cff"],["#39ff14","#00f5ff"],["#ff6600","#ff0040"],["#7a5cff","#b3b3c2"],["#ff2d6a","#7a5cff"]],n=0,a=0;a>>0;var r=e[n%e.length];return"linear-gradient(140deg,"+r[0]+","+r[1]+")"}function DA(A){var e=document.getElementById("end-highlights");if(e){if(!A||A.length===0){e.innerHTML="",e.classList.add("hidden");return}var n={exact_match:"#00f5ff",streak:"#ff6600",bet_win:"#39ff14",heartbreaker:"#ff2d6a",speed_record:"#00f5ff",comeback:"#39ff14",photo_finish:"#ffd34d"},a="";A.forEach(function(r,s){var d=n[r.type]||"#ff2d6a",u=o.t("highlights."+r.description,r.description_params||{})||"",c=r.round?''+(o.t("game.roundLabel")||"Round")+" "+r.round+"
":"";a+='"}),e.innerHTML=a,e.classList.remove("hidden")}}function MA(A){var e=document.getElementById("end-stats-comparison");if(e){if(!A){e.classList.add("hidden");return}var n=e.querySelector(".stats-comparison-icon"),a=e.querySelector(".stats-comparison-text"),r="",s="",d="stats-comparison";A.is_first_game?(r="\u{1F31F}",s="First game recorded! Avg: "+A.current_avg.toFixed(1)+" pts/round",d+=" stats-comparison--first"):A.is_new_record?(r="\u{1F3C6}",s="NEW RECORD! "+A.current_avg.toFixed(1)+" pts/round (prev: "+A.all_time_avg.toFixed(1)+")",d+=" stats-comparison--record"):A.is_above_average?(r="\u{1F4C8}",s=A.current_avg.toFixed(1)+" pts/round (+"+A.difference.toFixed(1)+" vs all-time avg)",d+=" stats-comparison--above"):(r="\u{1F4CA}",s=A.current_avg.toFixed(1)+" pts/round ("+A.difference.toFixed(1)+" vs all-time avg)",d+=" stats-comparison--below"),e.className=d,n&&(n.textContent=r),a&&(a.textContent=s)}}function HA(A){var e=document.getElementById("superlatives-container");if(e){if(!A||A.length===0){e.classList.add("hidden");return}var n="";A.forEach(function(a,r){var s="";switch(a.value_label){case"avg_time":s=a.value+"s "+o.t("superlatives.avgTime");break;case"streak":s=a.value+" "+o.t("superlatives.streak");break;case"bets":s=a.value+" "+o.t("superlatives.bets");break;case"points":s=a.value+" "+o.t("superlatives.points");break;case"close_guesses":s=a.value+" "+o.t("superlatives.closeGuesses");break;default:s=a.value}n+=''+a.emoji+'
'+o.t("superlatives."+a.title)+'
'+o.escapeHtml(a.player_name)+'
'+s+"
"}),e.innerHTML=n,e.classList.remove("hidden")}}var E=null,w=null;function G(A){if(!window.matchMedia("(prefers-reduced-motion: reduce)").matches){if(typeof confetti>"u"){console.warn("[Dashboard Confetti] Library not loaded");return}switch(GA(),A=A||"exact",A){case"exact":var e=2*1e3,n=Date.now()+e;(function i(){confetti({particleCount:15,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now()QR code unavailable")}function pA(A){var e=document.getElementById("dashboard-player-list");if(e){var n=A.slice().sort(function(s,d){return s.connected!==d.connected?s.connected?-1:1:0}),a=T.map(function(s){return s.name}),r=n.filter(function(s){return a.indexOf(s.name)===-1}).map(function(s){return s.name});e.innerHTML=n.map(function(s){var d=r.indexOf(s.name)!==-1,u=s.connected===!1,c=["dashboard-player-card"];d&&c.push("is-new"),u&&c.push("dashboard-player-card--disconnected");var i=u?'(away) ':"";return''+o.escapeHtml(s.name)+i+"
"}).join(""),setTimeout(function(){for(var s=e.querySelectorAll(".is-new"),d=0;d0?m="leaderboard-entry--climbing":i.rank_change<0&&(m="leaderboard-entry--falling");var b=i.connected===!1?"leaderboard-entry--disconnected":"",B=i.connected===!1?'(away) ':"",f=i.eliminated?"leaderboard-entry--disconnected":"",h=i.eliminated?"\u{1F480} ":"",C="";i.rank_change>0?C='\u25B2'+i.rank_change+" ":i.rank_change<0&&(C='\u25BC'+Math.abs(i.rank_change)+" ");var U="";if(i.streak>=2){var RA=i.streak>=5?"streak-indicator--hot":"";U='\u{1F525}'+i.streak+" "}var z="";r&&u[i.name]&&(z='BET ');var q="";if(a){var jA=d[i.name]===!0;q='
'}c+='#'+i.rank+' '+h+o.escapeHtml(i.name)+B+z+' '+U+C+' '+i.score+" "+q+"
"}),s.innerHTML=c}}function wA(A){var e=A.song||{},n=A.players||[],a=document.getElementById("reveal-album-art");a&&(a.src=e.album_art||"/beatify/static/img/no-artwork.svg",a.onerror=function(){this.src="/beatify/static/img/no-artwork.svg"});var r=document.getElementById("reveal-artist"),s=document.getElementById("reveal-title"),d=document.getElementById("reveal-year");r&&(r.textContent=e.artist||"Unknown Artist"),s&&(s.textContent=e.title||"Unknown Song"),d&&(d.textContent=e.year||"????");var u=!!A.title_artist_mode,c=document.getElementById("reveal-year-row");if(c&&c.classList.toggle("hidden",u),IA(A),CA(u?null:A.artist_challenge),_A(e),ZA(n),MA(A.leaderboard||[]),WA(A),V(A,"sd-final-banner-reveal"),XA(A.game_performance),QA(A),DA(A.song_difficulty),A.game_performance&&A.game_performance.is_new_record)G("record");else{var i=n.some(function(l){return l.years_off===0&&!l.missed_round});i&&G("exact")}}function IA(A){var e=document.getElementById("dashboard-ta-banner");if(e){var n=A.title_artist_challenge||null;if(!A.title_artist_mode||!n||!n.correct_title){e.classList.add("hidden"),M();return}e.classList.remove("hidden");var a=document.getElementById("dashboard-ta-title"),r=document.getElementById("dashboard-ta-artist");a&&(a.textContent=n.correct_title||""),r&&(r.textContent=n.correct_artist||"");var s=!!n.voting_open,d=n.near_misses||[],u=n.near_miss_outcomes||[],c=document.getElementById("dashboard-ta-voting"),i=document.getElementById("dashboard-ta-live"),l=document.getElementById("dashboard-ta-outcomes"),m=function(f){return f==="artist"?o.t("titleArtist.artistLabel","Artist"):o.t("titleArtist.titleLabel","Song title")};if(s&&d.length>0&&i){c&&c.classList.add("hidden"),l&&(l.innerHTML="",l.classList.add("hidden"));var b=d.map(function(f){var h=o.taTallyPercents(f.votes_yes,f.votes_no);return''+o.escapeHtml(f.player)+' '+o.escapeHtml(m(f.field))+'
\u201C'+o.escapeHtml(f.guess||"\u2014")+'\u201D
\u{1F44D} '+(f.votes_yes||0)+' \u{1F44E} '+(f.votes_no||0)+"
"}).join("");i.innerHTML=''+o.escapeHtml(o.t("titleArtist.dashboardDeciding","The room is deciding\u2026"))+'
'+b+"
",i.classList.remove("hidden"),LA(n.vote_seconds_remaining);return}if(M(),i&&(i.innerHTML="",i.classList.add("hidden")),c&&c.classList.add("hidden"),l)if(!s&&u.length>0){var B=u.map(function(f){var h=!!f.accepted;return''+o.escapeHtml(f.player)+' '+o.escapeHtml(m(f.field))+' '+o.escapeHtml(o.taVerdictLabel(h,f.points))+"
"}).join("");l.innerHTML=''+o.escapeHtml(o.t("titleArtist.closeCallsDecided","Close calls \u2014 decided"))+'
'+B+"
",l.classList.remove("hidden")}else l.innerHTML="",l.classList.add("hidden")}}function CA(A){var e=document.getElementById("reveal-artist-challenge");if(e){if(!A||!A.correct_artist){e.classList.add("hidden"),e.innerHTML="";return}var n=o.t("artistChallenge.theArtistWas","The artist"),a;if(A.winner){var r=A.bonus_points||5;a=''+o.escapeHtml(A.winner)+" +"+r+" "}else a=''+o.escapeHtml(o.t("artistChallenge.noWinner","Nobody guessed it"))+" ";e.innerHTML='\u{1F3A4} '+o.escapeHtml(n)+' '+o.escapeHtml(A.correct_artist)+" "+a,e.classList.remove("hidden")}}var Z=null;function LA(A){M();var e=document.getElementById("dashboard-ta-live-cd");if(!e)return;var n=typeof A=="number"?A:0,a=Date.now()+n*1e3;function r(){var s=Math.max(0,Math.ceil((a-Date.now())/1e3));e.textContent=s+"s",s<=0&&M()}r(),Z=setInterval(r,500)}function M(){Z&&(clearInterval(Z),Z=null)}function _A(A){var e=document.getElementById("dashboard-fun-fact"),n=document.getElementById("dashboard-fun-fact-text"),a=o.getLocalizedSongField(A,"fun_fact");if(v("[Dashboard] renderFunFact called with song:",A),v("[Dashboard] fun_fact value:",a||"no fun fact"),!e||!n){console.warn("[Dashboard] Fun fact elements not found");return}if(!a||a.trim()===""){e.classList.add("hidden"),v("[Dashboard] No fun_fact, hiding container");return}n.textContent=a,e.classList.remove("hidden"),v("[Dashboard] Fun fact shown:",a)}var k=null;function QA(A){var e=document.getElementById("reveal-countdown"),n=document.getElementById("reveal-countdown-num"),a=e?e.querySelector(".chip-countdown-fg"):null;if(!e||!n||!a)return;k!==null&&(clearInterval(k),k=null);var r=A.reveal_auto_advance||0,s=A.reveal_started_at||0,d=!!A.idle_halt;if(r<=0||!s||d){e.classList.add("hidden");return}e.classList.remove("hidden");var u=157.08;a.style.strokeDasharray=u;function c(){var i=Math.max(0,s+r*1e3-Date.now()),l=Math.ceil(i/1e3);n.textContent=l;var m=i/(r*1e3);a.style.strokeDashoffset=String(u*(1-m)),i<=0&&k!==null&&(clearInterval(k),k=null)}c(),k=setInterval(c,500)}function XA(A){var e=document.getElementById("reveal-motivational");if(e){if(!A||!A.message){e.classList.add("hidden");return}var n=A.message,a=e.querySelector(".motivational-icon"),r=e.querySelector(".motivational-text");e.className="motivational-message motivational-message--"+n.type;var s={first:"\u{1F31F}",record:"\u{1F3C6}",strong:"\u{1F525}",above:"\u{1F4C8}",close:"\u{1F4AA}"};a&&(a.textContent=s[n.type]||""),r&&(r.textContent=n.message||"")}}function DA(A){var e=document.getElementById("song-difficulty");if(e){if(!A){e.classList.add("hidden");return}for(var n="",a=0;a★';e.innerHTML=''+n+'
'+o.t("difficulty."+A.label)+' '+A.accuracy+"% "+o.t("difficulty.accuracy")+" ",e.classList.remove("hidden")}}function ZA(A){var e=document.getElementById("reveal-top-guesses-list");if(e){var n=A.filter(function(r){return!r.missed_round}).sort(function(r,s){return(s.round_score||0)-(r.round_score||0)}).slice(0,3),a="";n.forEach(function(r,s){var d=r.guess?'('+r.guess+") ":"",u="";if(r.bet){var c="bet-badge";r.bet_outcome==="won"?c+=" bet-badge--won":r.bet_outcome==="lost"&&(c+=" bet-badge--lost"),u='BET '}a+='#'+(s+1)+' '+o.escapeHtml(r.name)+d+' +'+(r.round_score||0)+u+"
"}),e.innerHTML=a}}function MA(A){var e=document.getElementById("reveal-leaderboard");if(e){var n="";A.forEach(function(a){var r=a.rank<=3?"is-top-"+a.rank:"",s="";a.rank_change>0?s="leaderboard-entry--climbing":a.rank_change<0&&(s="leaderboard-entry--falling");var d=a.connected===!1?"leaderboard-entry--disconnected":"",u=a.connected===!1?'(away) ':"",c=a.eliminated?"leaderboard-entry--disconnected":"",i=a.eliminated?"\u{1F480} ":"",l="";a.rank_change>0?l='\u25B2'+a.rank_change+" ":a.rank_change<0&&(l='\u25BC'+Math.abs(a.rank_change)+" ");var m="";if(a.streak>=2){var b=a.streak>=5?"streak-indicator--hot":"";m='\u{1F525}'+a.streak+" "}n+='#'+a.rank+' '+i+o.escapeHtml(a.name)+u+' '+m+l+' '+a.score+"
"}),e.innerHTML=n}}function SA(A){var e=A.leaderboard||[];TA(A),[1,2,3].forEach(function(l){var m=e.find(function(C){return C.rank===l}),b=document.getElementById("end-podium-"+l+"-name"),B=document.getElementById("end-podium-"+l+"-score"),f=document.getElementById("end-podium-"+l+"-avatar");if(b&&(b.textContent=m?m.name:"---"),B&&(B.textContent=m?m.score:"0"),f){var h=m?m.name:"";f.textContent=h?h.trim().charAt(0).toUpperCase():"",f.style.background=h?xA(h):"transparent"}});var n=A.game_stats||{},a=document.getElementById("end-meta-rounds"),r=document.getElementById("end-meta-players");a&&(a.textContent="\u{1F3B5} "+(n.total_rounds!=null?n.total_rounds:e.length&&A.round||0)),r&&(r.textContent="\u{1F465} "+(n.total_players!=null?n.total_players:e.length)),GA(A.game_performance),FA(A.superlatives),HA(A.highlights);var s=e.find(function(l){return l.rank===1});s&&s.score>0&&G("winner");var d=document.getElementById("end-leaderboard");if(d){var u=e.filter(function(l){return l.rank>3}),c=u.length?u:e,i="";c.forEach(function(l){var m=l.rank<=3?"is-top-"+l.rank:"",b=l.connected===!1?"leaderboard-entry--disconnected":"",B=l.connected===!1?'(away) ':"",f=l.eliminated?"leaderboard-entry--disconnected":"",h=l.eliminated?"\u{1F480} ":"";i+='#'+l.rank+' '+h+o.escapeHtml(l.name)+B+' '+l.score+"
"}),d.innerHTML=i}}function xA(A){for(var e=[["#ff2d6a","#ff6600"],["#00f5ff","#7a5cff"],["#39ff14","#00f5ff"],["#ff6600","#ff0040"],["#7a5cff","#b3b3c2"],["#ff2d6a","#7a5cff"]],n=0,a=0;a>>0;var r=e[n%e.length];return"linear-gradient(140deg,"+r[0]+","+r[1]+")"}function HA(A){var e=document.getElementById("end-highlights");if(e){if(!A||A.length===0){e.innerHTML="",e.classList.add("hidden");return}var n={exact_match:"#00f5ff",streak:"#ff6600",bet_win:"#39ff14",heartbreaker:"#ff2d6a",speed_record:"#00f5ff",comeback:"#39ff14",photo_finish:"#ffd34d"},a="";A.forEach(function(r,s){var d=n[r.type]||"#ff2d6a",u=o.t("highlights."+r.description,r.description_params||{})||"",c=r.round?''+(o.t("game.roundLabel")||"Round")+" "+r.round+"
":"";a+='"}),e.innerHTML=a,e.classList.remove("hidden")}}function GA(A){var e=document.getElementById("end-stats-comparison");if(e){if(!A){e.classList.add("hidden");return}var n=e.querySelector(".stats-comparison-icon"),a=e.querySelector(".stats-comparison-text"),r="",s="",d="stats-comparison";A.is_first_game?(r="\u{1F31F}",s="First game recorded! Avg: "+A.current_avg.toFixed(1)+" pts/round",d+=" stats-comparison--first"):A.is_new_record?(r="\u{1F3C6}",s="NEW RECORD! "+A.current_avg.toFixed(1)+" pts/round (prev: "+A.all_time_avg.toFixed(1)+")",d+=" stats-comparison--record"):A.is_above_average?(r="\u{1F4C8}",s=A.current_avg.toFixed(1)+" pts/round (+"+A.difference.toFixed(1)+" vs all-time avg)",d+=" stats-comparison--above"):(r="\u{1F4CA}",s=A.current_avg.toFixed(1)+" pts/round ("+A.difference.toFixed(1)+" vs all-time avg)",d+=" stats-comparison--below"),e.className=d,n&&(n.textContent=r),a&&(a.textContent=s)}}function FA(A){var e=document.getElementById("superlatives-container");if(e){if(!A||A.length===0){e.classList.add("hidden");return}var n="";A.forEach(function(a,r){var s="";switch(a.value_label){case"avg_time":s=a.value+"s "+o.t("superlatives.avgTime");break;case"streak":s=a.value+" "+o.t("superlatives.streak");break;case"bets":s=a.value+" "+o.t("superlatives.bets");break;case"points":s=a.value+" "+o.t("superlatives.points");break;case"close_guesses":s=a.value+" "+o.t("superlatives.closeGuesses");break;default:s=a.value}n+=''+a.emoji+'
'+o.t("superlatives."+a.title)+'
'+o.escapeHtml(a.player_name)+'
'+s+"
"}),e.innerHTML=n,e.classList.remove("hidden")}}function WA(A){var e=document.getElementById("sd-out-overlay");if(e){var n=A.sudden_death_mode?A.eliminated_this_round||[]:[];if(n.length){var a=(A.round||0)+":"+n.join(",");if(a!==N){N=a;var r=e.querySelector(".sd-out__word");r&&(r.textContent=(o.t("game.out","OUT")||"OUT").toUpperCase());var s=document.getElementById("sd-out-name");s&&(s.textContent=n.join(", ")),e.classList.add("show"),_&&clearTimeout(_),_=setTimeout(function(){e.classList.remove("show"),_=null},2500)}}}}function V(A,e){var n=document.getElementById(e);if(n){var a=A.players||[],r=a.filter(function(s){return!s.eliminated});A.sudden_death_mode&&r.length===2?(n.textContent="\u{1F480} "+o.t("game.finalShowdown","FINAL \u2014 SUDDEN DEATH"),n.classList.remove("hidden")):n.classList.add("hidden")}}function TA(A){var e=document.getElementById("sd-last-standing");if(e){if(!A.sudden_death_mode){e.classList.add("hidden");return}var n=A.leaderboard||[],a=n.filter(function(c){return!c.eliminated}),r=a.length?a[0].name:null;if(!r&&A.superlatives){var s=A.superlatives.find(function(c){return c.id==="last_one_standing"});s&&(r=s.player_name)}if(!r){e.classList.add("hidden");return}var d=e.querySelector(".sd-last-standing__headline");d&&(d.textContent=o.t("game.lastOneStanding","Last One Standing"));var u=document.getElementById("sd-last-standing-winner");u&&(u.textContent=r),e.classList.remove("hidden")}}var E=null,w=null;function G(A){if(!window.matchMedia("(prefers-reduced-motion: reduce)").matches){if(typeof confetti>"u"){console.warn("[Dashboard Confetti] Library not loaded");return}switch(JA(),A=A||"exact",A){case"exact":var e=2*1e3,n=Date.now()+e;(function i(){confetti({particleCount:15,spread:70,origin:{y:.6},colors:["#FFD700","#FFA500","#FFEC8B"]}),Date.now() 0;
tracker.classList.toggle('all-submitted', allSubmitted);
@@ -303,25 +402,41 @@ function renderSubmissionTracker(players) {
var initials = getInitials(player.name);
var isCurrentPlayer = player.name === state.playerName;
var isDisconnected = player.connected === false;
+ var isEliminated = !!player.eliminated; // Issue #827
var classes = [
'player-indicator',
- player.submitted ? 'is-submitted' : '',
+ // Issue #827: eliminated chips never read as "submitted".
+ (player.submitted && !isEliminated) ? 'is-submitted' : '',
isCurrentPlayer ? 'is-current-player' : '',
- isDisconnected ? 'player-indicator--disconnected' : ''
+ isDisconnected ? 'player-indicator--disconnected' : '',
+ isEliminated ? 'is-eliminated' : ''
].filter(Boolean).join(' ');
var badges = '';
- if (player.steal_used) {
- badges += '🥷 ';
- }
- if (player.bet) {
- badges += '🎲 ';
+ // Issue #827: eliminated players show only the "Out · R{round}" badge,
+ // not steal/bet badges (they're no longer playing this round).
+ if (isEliminated) {
+ var round = (player.eliminated_round != null) ? player.eliminated_round : '';
+ var outText = utils.t('game.outRound', { round: round }) || ('Out · R' + round);
+ badges += '' + escapeHtml(outText) + ' ';
+ } else {
+ if (player.steal_used) {
+ badges += '🥷 ';
+ }
+ if (player.bet) {
+ badges += '🎲 ';
+ }
}
+ // Issue #827: skull replaces the avatar initials for eliminated players.
+ var avatarInner = isEliminated
+ ? '💀 '
+ : '' + escapeHtml(initials) + ' ';
+
return '' +
badges +
'
' +
- '' + escapeHtml(initials) + ' ' +
+ avatarInner +
'
' +
'
' + escapeHtml(player.name) + ' ' +
'
';
@@ -573,6 +688,7 @@ export function initYearSelector() {
yearSelectorInitialized = true; // #854 — set only after DOM was found
slider.addEventListener('input', function() {
+ if (meEliminated) return; // Issue #827: eliminated players can't change the year
yearDisplay.textContent = this.value;
});
@@ -597,7 +713,7 @@ export function initYearSelector() {
var longPressTimeoutId = null;
btn.addEventListener('pointerdown', function(e) {
- if (hasSubmitted) return;
+ if (hasSubmitted || meEliminated) return; // Issue #827
e.preventDefault();
adjustYear(delta);
longPressTimeoutId = setTimeout(function() {
@@ -615,7 +731,7 @@ export function initYearSelector() {
// Keyboard fallback (Space / Enter when the button has focus)
btn.addEventListener('keydown', function(e) {
- if (hasSubmitted) return;
+ if (hasSubmitted || meEliminated) return; // Issue #827
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
adjustYear(delta);
@@ -694,6 +810,7 @@ export function initYearSelector() {
*/
export function handleSubmitGuess() {
if (hasSubmitted) return;
+ if (meEliminated) return; // Issue #827: eliminated players can't submit
var slider = document.getElementById('year-slider');
var submitBtn = document.getElementById('submit-btn');
@@ -902,6 +1019,7 @@ export function renderTitleArtistInput(data) {
*/
export function handleTitleArtistSubmit() {
if (hasSubmitted) return;
+ if (meEliminated) return; // Issue #827: eliminated players can't submit
var titleInput = document.getElementById('ta-title-input');
var artistInput = document.getElementById('ta-artist-input');
diff --git a/custom_components/beatify/www/js/player.bundle.min.js b/custom_components/beatify/www/js/player.bundle.min.js
index 83b6fdde..5f89c76a 100644
--- a/custom_components/beatify/www/js/player.bundle.min.js
+++ b/custom_components/beatify/www/js/player.bundle.min.js
@@ -1,2 +1,2 @@
-var ht=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},Da=document.getElementById("loading-view"),Ga=document.getElementById("starting-view"),Va=document.getElementById("not-found-view"),Wa=document.getElementById("ended-view"),Fa=document.getElementById("in-progress-view"),qa=document.getElementById("join-view"),Ua=document.getElementById("tour-view"),ja=document.getElementById("ready-view"),za=document.getElementById("lobby-view"),Ya=document.getElementById("game-view"),Ja=document.getElementById("reveal-view"),Qa=document.getElementById("paused-view"),Xa=document.getElementById("end-view"),Ka=document.getElementById("connection-lost-view"),Za=[Da,Ga,Va,Wa,Fa,qa,Ua,ja,za,Ya,Ja,Qa,Xa,Ka];function _(e){ht.showView(Za,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"),u=document.getElementById("confirm-modal-yes"),d=document.getElementById("confirm-modal-no");if(!r||!o||!l||!u||!d){i(confirm(t||e));return}o.textContent=e,l.textContent=t,u.textContent=n||ht.t("common.confirm")||"Confirm",d.textContent=a||ht.t("common.cancel")||"Cancel",r.classList.remove("hidden");function f(){r.classList.add("hidden"),u.removeEventListener("click",c),d.removeEventListener("click",v),g.removeEventListener("click",v)}function c(){f(),i(!0)}function v(){f(),i(!1)}var g=r.querySelector(".modal-backdrop");u.addEventListener("click",c),d.addEventListener("click",v),g&&g.addEventListener("click",v)})}function p(e){var t=document.createElement("div");return t.textContent=e,t.innerHTML}function $a(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}function er(e){return 1-Math.pow(1-e,4)}function wt(e,t,n,a,i){if($a()||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||er;var l=null,u=null,d=!1,f=n;function c(v){if(!d){l||(l=v);var g=v-l,y=Math.min(g/o,1),h=i(y),b=Math.round(t+(f-t)*h);e.textContent=b,y<1&&(u=requestAnimationFrame(c))}}return u=requestAnimationFrame(c),{cancel:function(){d=!0,u&&cancelAnimationFrame(u)},skipToEnd:function(){d=!0,u&&cancelAnimationFrame(u),e.textContent=f}}}var Q={players:{},leaderboard:[],initialized:!1};function un(){return Q.initialized}function fn(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 Ze(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},S={observer:null,fullData:[],visibleRange:{start:0,end:10},listEl:null,isLazyEnabled:!1};function vn(e){e&&(S.observer&&S.listEl!==e&&(S.observer.disconnect(),S.observer=null),!S.observer&&(S.listEl=e,S.observer=new IntersectionObserver(function(t){t.forEach(function(n){if(!(!n.isIntersecting||!S.isLazyEnabled)){var a=S.fullData,i=S.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);S.visibleRange.start=o,Pe()}}else if(n.target.classList.contains("leaderboard-sentinel--bottom")&&i.end0&&(l+='
'),l+='
';for(var u=n.start;u ',r>0&&(l+='
'),e.innerHTML=l,e.scrollTop=o,S.observer){var d=e.querySelectorAll(".leaderboard-sentinel");d.forEach(function(f){S.observer.observe(f)})}}}function Lt(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 u="";if(e.streak>=2){var d=e.streak>=5?"streak-indicator--hot":"";u='\u{1F525}'+e.streak+" "}var f=e.connected===!1?"leaderboard-entry--disconnected":"",c=e.connected===!1?'(away) ':"",v=e._displayScore!==void 0?e._displayScore:a;return'#'+n+' '+p(t)+c+' '+u+l+' '+v+"
"}function It(e,t){for(var n=Le,a=S.listEl&&S.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?(u=Math.max(0,e.length-i-r),d=e.length):(u=Math.max(0,o-Math.floor(i/2)-r),d=Math.min(e.length,o+Math.ceil(i/2)+r)),{start:u,end:d}}function mn(){S.observer&&(S.observer.disconnect(),S.observer=null),S.isLazyEnabled=!1,S.fullData=[]}function pn(){var e;function t(){clearTimeout(e),e=setTimeout(function(){S.isLazyEnabled&&S.fullData.length>0&&(S.visibleRange=It(S.fullData,s.playerName),Pe())},150)}window.addEventListener("resize",t),window.addEventListener("orientationchange",t)}function gn(){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 yn(){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 bn={ITEM_HEIGHT:60,OVERSCAN:3,THRESHOLD:15,CONTAINER_HEIGHT:320},w={container:null,items:[],scrollTop:0,isVirtual:!1,topSpacer:null,bottomSpacer:null,contentWrapper:null,scrollHandler:null,resizeHandler:null};function hn(e){if(e){w.container=e;var t=!1;w.scrollHandler=function(){w.scrollTop=e.scrollTop,t||(requestAnimationFrame(function(){Et(),t=!1}),t=!0)};var n;w.resizeHandler=function(){clearTimeout(n),n=setTimeout(function(){w.isVirtual&&Et()},100)},e.addEventListener("scroll",w.scrollHandler,{passive:!0}),window.addEventListener("resize",w.resizeHandler)}}function En(e,t){w.items=e,w.renderItem=t;var n=w.container;if(n){var a=n.scrollTop,i=w.isVirtual;e.length0&&(n.scrollTop=a,w.scrollTop=a)}}function tr(){var e=w.container;if(e){e.innerHTML="";var t=document.createElement("div");t.className="virtual-spacer-top",w.topSpacer=t;var n=document.createElement("div");n.className="virtual-content-wrapper",w.contentWrapper=n;var a=document.createElement("div");a.className="virtual-spacer-bottom",w.bottomSpacer=a,e.appendChild(t),e.appendChild(n),e.appendChild(a)}}function Et(){var e=bn,t=w.items,n=w.container,a=w.contentWrapper;if(!(!n||!a||!t.length)){var i=n.clientHeight||e.CONTAINER_HEIGHT,r=w.scrollTop,o=e.ITEM_HEIGHT,l=e.OVERSCAN,u=Math.max(0,Math.floor(r/o)-l),d=Math.min(t.length,Math.ceil((r+i)/o)+l);w.topSpacer&&(w.topSpacer.style.height=u*o+"px"),w.bottomSpacer&&(w.bottomSpacer.style.height=(t.length-d)*o+"px");for(var f="",c=u;c"u"){console.warn("[Confetti] Library not loaded");return}X();var t=Ee.getQualitySettings(),n=t.confettiParticles;if(n===0){cn();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,h){return y.connected!==h.connected?y.connected?-1:1:0}),u=In.map(function(y){return y.name}),d=l.filter(function(y){return u.indexOf(y.name)===-1}).map(function(y){return y.name});w.container||hn(t);var f=["c1","c2","c3","c4"],c={},v=0;l.forEach(function(y){y.is_admin?c[y.name]="host":c[y.name]=f[v++%f.length]});var g=function(y){var h=d.indexOf(y.name)!==-1,b=y.name===s.playerName,E=y.is_admin===!0,I=y.connected===!1,C=c[y.name]||"c1",x=["player-tile","player-tile--"+C];h&&x.push("is-new"),I&&x.push("player-tile--disconnected");var B=(y.name||"?").trim(),D=(B.charAt(0)||"?").toUpperCase(),A="";E?A='\u{1F451} ':b&&(A='YOU ');var M=I?''+$.t("lobby.away")+" ":"";return''+p(D)+' '+p(B)+" "+A+M+"
"};En(l,g),setTimeout(function(){var y=w.isVirtual?w.contentWrapper:t;if(y)for(var h=y.querySelectorAll(".is-new"),b=0;bQR code library not loaded',t.onclick=Sn,t.onkeydown=function(n){(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),Sn())})}}function Sn(){if(ee){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:ee,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function St(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function _n(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",St),n&&n.addEventListener("click",St),document.addEventListener("keydown",function(a){a.key==="Escape"&&e&&!e.classList.contains("hidden")&&St()})}function rr(){if(ee){var e=document.getElementById("invite-modal"),t=document.getElementById("invite-modal-code"),n=document.getElementById("invite-modal-url");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:ee,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',n&&(n.value=ee),e.classList.remove("hidden"),document.body.style.overflow="hidden";var a=document.getElementById("invite-modal-close");a&&a.focus()}}}function He(){var e=document.getElementById("invite-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="");var t=document.getElementById("invite-copy-feedback");t&&t.classList.add("hidden")}function ir(){var e=document.getElementById("invite-modal-url"),t=document.getElementById("invite-copy-feedback");!e||!ee||(navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(ee).then(function(){kn(t)}).catch(function(){Bn(e,t)}):Bn(e,t))}function Bn(e,t){e.select(),e.setSelectionRange(0,99999);try{document.execCommand("copy"),kn(t)}catch(n){console.warn("[Beatify] Copy failed:",n)}}function kn(e){e&&(e.classList.remove("hidden"),setTimeout(function(){e.classList.add("hidden")},2e3))}function Cn(){var e=document.getElementById("invite-modal"),t=e?e.querySelector(".invite-modal-backdrop"):null,n=document.getElementById("invite-modal-close"),a=document.getElementById("invite-players-btn"),i=document.getElementById("invite-copy-btn");t&&t.addEventListener("click",He),n&&n.addEventListener("click",He),a&&a.addEventListener("click",rr),i&&i.addEventListener("click",ir),document.addEventListener("keydown",function(r){r.key==="Escape"&&e&&!e.classList.contains("hidden")&&He()})}function An(e){var t=document.getElementById("admin-controls"),n=document.getElementById("lobby-status");if(t){(!e||!Array.isArray(e))&&(e=[]);var a=e.find(function(r){return r.name===s.playerName}),i=a?.is_admin===!0;i?(t.classList.remove("hidden"),n&&n.classList.add("hidden")):(t.classList.add("hidden"),n&&n.classList.remove("hidden"))}}function Tn(){var e=document.getElementById("start-game-btn");e?.addEventListener("click",function(){!s.ws||s.ws.readyState!==WebSocket.OPEN||(e.disabled=!0,e.textContent=$.t("game.starting"),s.ws.send(JSON.stringify({type:"admin",action:"start_game"})))})}function Nn(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=$.t("player.welcomeBack",{name:e}),t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},2e3))}function Mn(){var e=document.getElementById("volume-indicator");e&&(e.textContent=$.t("earlyReveal.message")||"All guesses in!",e.classList.remove("hidden"),e.classList.add("is-visible"),setTimeout(function(){e.classList.remove("is-visible"),setTimeout(function(){e.classList.add("hidden")},300)},1500))}var De=null,K=null,sr=250,or=400,xe=null;function On(){xe!==null&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(xe),xe=null}function lr(e){var t=1-e;return 1-t*t*t}function _t(e){if(De!==null&&K!==null&&typeof requestAnimationFrame=="function"){var t=Math.abs(e-K);if(t<=sr){K=e;return}if(dr(e))return}fe();var n=document.getElementById("timer");if(!n)return;var a=document.getElementById("timer-neon"),i=document.getElementById("timer-float"),r=document.getElementById("timer-float-num");n.classList.remove("timer--warning","timer--critical"),a&&a.classList.remove("timer-neon--warn"),i&&i.classList.remove("timer-float--warn"),cr(a,i),K=e;var o=0;function l(){var u=Date.now(),d=Math.max(0,Math.ceil((K-u)/1e3));n.textContent=d,r&&(r.textContent=d),d<=5?(n.classList.remove("timer--warning"),n.classList.add("timer--critical")):d<=10?(n.classList.remove("timer--critical"),n.classList.add("timer--warning")):n.classList.remove("timer--warning","timer--critical"),a&&a.classList.toggle("timer-neon--warn",d<=10),i&&i.classList.toggle("timer-float--warn",d<=10),d===10?n.setAttribute("aria-label","10 seconds remaining"):d===5?n.setAttribute("aria-label","5 seconds!"):d===0?n.setAttribute("aria-label","Time is up!"):n.setAttribute("aria-label","Time remaining: "+d+" seconds"),d<=0&&(o+=1,(o===1||o%3===0)&&s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"round_timeout"})))}l(),De=setInterval(l,1e3)}function Rn(e){var t=document.getElementById("timer");if(!t)return!1;var n=document.getElementById("timer-neon"),a=document.getElementById("timer-float"),i=document.getElementById("timer-float-num");return t.textContent=e,i&&(i.textContent=e),e<=5?(t.classList.remove("timer--warning"),t.classList.add("timer--critical")):e<=10?(t.classList.remove("timer--critical"),t.classList.add("timer--warning")):t.classList.remove("timer--warning","timer--critical"),n&&n.classList.toggle("timer-neon--warn",e<=10),a&&a.classList.toggle("timer-float--warn",e<=10),!0}function dr(e){var t=document.getElementById("timer-neon");if(!document.getElementById("timer"))return!1;var n=K,a=Date.now();On(),t&&t.classList.add("timer-neon--catchup");function i(){var r=Date.now()-a,o=r/or;if(o>=1){K=e,Rn(Math.max(0,Math.ceil((e-Date.now())/1e3))),t&&t.classList.remove("timer-neon--catchup"),xe=null;return}var l=n+(e-n)*lr(o);K=l,Rn(Math.max(0,Math.ceil((l-Date.now())/1e3))),xe=requestAnimationFrame(i)}return xe=requestAnimationFrame(i),!0}var te=null,xt=null;function cr(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(){De&&(clearInterval(De),De=null),On(),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||{},ve=!1,ne=null,tt=null,ur=300,Pn=0;function At(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(f){return f.dataset.artist}),o=e.options;if(JSON.stringify(r)!==JSON.stringify(o)&&(a.innerHTML="",o.forEach(function(f,c){var v=document.createElement("button");v.className="artist-option-btn",v.dataset.artist=f,v.dataset.index=c,v.textContent=f,v.addEventListener("click",function(){fr(f)}),a.appendChild(v)})),e.winner){var l=a.querySelectorAll(".artist-option-btn");if(l.forEach(function(f){f.classList.add("is-disabled"),f.classList.remove("is-loading","is-wrong");var c=e.correct_artist||tt;c&&f.dataset.artist===c&&f.classList.add("is-winner")}),e.winner===s.playerName){var u=e.bonus_points||5;i.textContent=(ae.t("artistChallenge.youGotIt")||"You got it! +{points} points").replace("{points}",u),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"),ve=!0}else ve||i.classList.add("hidden")}}function fr(e){var t=Date.now();if(!(t-Pn0)){var n=document.createElement("span");n.className="movie-rank-badge",n.textContent="+"+e.bonus,t.appendChild(n)}Rt();var a=(Ge.t("movieChallenge.youGotIt")||"Correct! #{rank} \u2014 +{bonus} points").replace("{rank}",e.rank||1).replace("{bonus}",e.bonus||0);Ot(a,!0),_e=!0}else t&&(t.classList.remove("is-loading"),t.classList.add("is-wrong","is-selected")),Rt(),Ot(Ge.t("movieChallenge.wrongGuess")||"Not quite...",!1),_e=!0;ke=null}function Rt(){document.querySelectorAll(".movie-option-btn").forEach(function(e){e.classList.add("is-disabled"),e.classList.remove("is-loading")})}function Ot(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 Dt(){_e=!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 Gt(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=Ge.t("movieChallenge.winnersTitle")||"Movie Quiz Winners",i.appendChild(o),r.forEach(function(u){var d=document.createElement("div");d.className="movie-reveal-winner-entry",u.name===t?d.classList.add("is-you"):d.classList.add("is-other"),d.textContent=u.name+" \u2014 +"+u.bonus+" ("+u.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=Ge.t("movieChallenge.noWinner")||"No one guessed the movie",i.appendChild(l)}}}}var L=window.BeatifyUtils||{},pr=L.debug||function(){};function Fn(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=L.t("game.introStopped")||"Intro complete!")):(r.classList.remove("intro-badge--stopped"),l&&(l.setAttribute("data-i18n","game.introRound"),l.textContent=L.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 u=document.getElementById("album-cover"),d=document.getElementById("album-loading");if(u&&e.song){d&&d.classList.remove("hidden");var f=e.song.album_art||"/beatify/static/img/no-artwork.svg";u.onload=function(){d&&d.classList.add("hidden")},u.onerror=function(){u.src="/beatify/static/img/no-artwork.svg",d&&d.classList.add("hidden")},u.src=f}Ft(),yr(e),br(e.players),e.leaderboard&&hr(e,"leaderboard-list"),_r(e.players),e.artist_challenge!==void 0&&At(e.artist_challenge,"PLAYING"),e.movie_challenge!==void 0&&Pt(e.movie_challenge,"PLAYING"),Br(e)}function qn(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}pr("[Metadata] Updated:",e.artist,"-",e.title)}}function gr(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 Ft(){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 yr(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)}}function br(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(c){return c.submitted}).length,o=i.length,l=r===o&&o>0;t.classList.toggle("all-submitted",l),a&&(o===0?a.textContent="":l?a.textContent=L.t("game.allSubmitted")||"All in":a.textContent=L.t("game.submittedCount",{count:r,total:o})||r+" of "+o+" submitted");var u=document.getElementById("submitted-banner"),d=document.getElementById("submitted-banner-text");if(u&&d&&!u.classList.contains("hidden")){var f=Math.max(0,o-r);f===0?d.textContent=L.t("game.lockedInAllSubmitted")||"Locked in \xB7 everyone submitted":d.textContent=L.t("game.lockedInWaitingCount",{count:f})||"Locked in \xB7 waiting for "+f+" more"}n.innerHTML=i.map(function(c){var v=gr(c.name),g=c.name===s.playerName,y=c.connected===!1,h=["player-indicator",c.submitted?"is-submitted":"",g?"is-current-player":"",y?"player-indicator--disconnected":""].filter(Boolean).join(" "),b="";return c.steal_used&&(b+='\u{1F977} '),c.bet&&(b+='\u{1F3B2} '),''+b+'
'+p(v)+'
'+p(c.name)+" "}).join("")}}function hr(e,t,n){var a=e.leaderboard||[],i=document.getElementById(t||"leaderboard-list");if(i){var r=n&&un(),o=r?fn(a):{};a.forEach(function(c){c.is_current=c.name===s.playerName;var v=o[c.name];v&&(c._rankChange=v);var g=Q.players[c.name],y=g?g.score:c.score;c._prevScore=y,c._displayScore=n?y:c.score});var l=Er(a,s.playerName),u=a.length>=Le.MIN_PLAYERS_FOR_LAZY;if(u)S.observer||vn(i),S.fullData=l,S.isLazyEnabled=!0,S.listEl=i,S.visibleRange=It(l,s.playerName),Pe();else{S.isLazyEnabled=!1;var d="";l.forEach(function(c){d+=Lt(c)}),i.innerHTML=d}var f=[];r&&l.forEach(function(c){!c.separator&&c._prevScore!==c.score&&f.push({name:c.name,prevScore:c._prevScore,newScore:c.score})}),r&&f.length>0&&requestAnimationFrame(function(){for(var c={},v=i.querySelectorAll(".leaderboard-entry[data-name]"),g=0;g8&&wr(i),Lr(a),Ir(a),Ze(e.players||[],a)}}function Er(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 wr(e){var t=e.querySelector(".leaderboard-entry.is-current");t&&t.scrollIntoView({behavior:"smooth",block:"center"})}function Lr(e){var t=document.getElementById("leaderboard-you"),n=e.find(function(a){return a.is_current});t&&n&&(t.textContent=L.t("leaderboard.you")+" #"+n.rank,t.classList.remove("hidden"))}function Un(){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 Ir(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 jn(){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 W=!1,Ve=!1,We=!1,Vt=!1,Dn=!1,Gn=!1;function zn(){if(Gn)return;var e=document.getElementById("year-slider"),t=document.getElementById("selected-year");if(!e||!t)return;Gn=!0,e.addEventListener("input",function(){t.textContent=this.value});function n(v){var g=parseInt(e.value,10)+v;g=Math.max(parseInt(e.min,10),Math.min(parseInt(e.max,10),g)),e.value=g,t.textContent=g}function a(v,g){if(!v)return;var y=null,h=null;v.addEventListener("pointerdown",function(E){W||(E.preventDefault(),n(g),h=setTimeout(function(){y=setInterval(function(){n(g)},150)},500))});function b(){h&&(clearTimeout(h),h=null),y&&(clearInterval(y),y=null)}["pointerup","pointerleave","pointercancel"].forEach(function(E){v.addEventListener(E,b)}),v.addEventListener("keydown",function(E){W||(E.key==="Enter"||E.key===" ")&&(E.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(){W||(Ve=!Ve,i.classList.toggle("is-active",Ve))});var r=document.getElementById("submit-btn");if(r&&r.addEventListener("click",function(){Vt?Vn():Sr()}),!Dn){var o=document.getElementById("ta-title-input"),l=document.getElementById("ta-artist-input");o&&o.addEventListener("keydown",function(v){v.key==="Enter"&&(v.preventDefault(),l&&l.focus())}),l&&l.addEventListener("keydown",function(v){v.key==="Enter"&&(v.preventDefault(),Vt&&Vn())}),Dn=!0}var u=document.getElementById("steal-btn");u&&u.addEventListener("click",kr);var d=document.getElementById("steal-modal-close");d&&d.addEventListener("click",Wt);var f=document.getElementById("steal-modal");if(f){var c=f.querySelector(".steal-modal-backdrop");c&&c.addEventListener("click",Wt)}}function Sr(){if(!W){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:Ve})):(at(L.t("errors.connectionLost")),t.disabled=!1,t.classList.remove("is-loading"))}}}function rt(){W=!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(L.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 qt(e){var t=document.getElementById("submit-btn");t&&(t.disabled=!1,t.classList.remove("is-loading")),e.code==="ROUND_EXPIRED"?(at(L.t("errors.timesUp")),W=!0,t&&(t.disabled=!0)):e.code==="ALREADY_SUBMITTED"?rt():at(e.message||"Submission failed")}function at(e){var t=document.getElementById("submit-btn");t&&(t.textContent=e,t.classList.add("is-error"),setTimeout(function(){t.textContent=L.t("game.submitGuess"),t.classList.remove("is-error")},2e3))}function Yn(){W=!1,Ve=!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=L.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 u=document.getElementById(l);u&&(u.disabled=!1)}),We=!1,Ut(),Nt(),Dt(),xr()}function Br(e){var t=!!(e&&e.title_artist_mode);Vt=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&&!W&&(o.textContent=L.t("titleArtist.submitGuess")||"Submit")}}function Vn(){if(!W){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})):(at(L.t("errors.connectionLost")),n.disabled=!1,n.classList.remove("is-loading"))}}}function Jn(e){rt();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=L.t("titleArtist.submitted")||"Submitted \u2014 see how you did at the reveal!",a.classList.remove("hidden"))}function xr(){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 _r(e){if(!(!s.playerName||!e)){var t=e.find(function(i){return i.name===s.playerName});if(t){We=t.steal_available&&!W;var n=document.getElementById("steal-indicator"),a=document.getElementById("steal-btn");We?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden")):Ut(),Ft()}}}function Ut(){var e=document.getElementById("steal-indicator"),t=document.getElementById("steal-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden"),Ft()}function kr(){!We||W||s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"get_steal_targets"}))}function Cr(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=L.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(){Ar(i)}),n.appendChild(r)});t.classList.remove("hidden")}}function Wt(){var e=document.getElementById("steal-modal");e&&e.classList.add("hidden")}async function Ar(e){var t=L.t("steal.confirm").replace("{name}",e),n=await Ie(L.t("steal.confirmTitle")||"Steal Answer?",t,L.t("steal.confirmButton")||"Steal",L.t("common.cancel"));n&&(s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"steal",target:e})),Wt())}function Qn(e){if(e.success){We=!1,W=!0,Ut();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"),Tr(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 Xn(e){Cr(e.targets||[])}function Tr(e,t){var n=document.getElementById("steal-confirmation"),a=document.getElementById("steal-confirmation-text");if(!(!n||!a)){var i=L.t("steal.success").replace("{name}",e).replace("{year}",t);a.textContent=i,n.classList.remove("hidden"),setTimeout(function(){n.classList.add("hidden")},3e3)}}var Wn=0,Nr=500,Fe=!1,jt=.5;function it(){var e=Date.now();return e-Wn=1){ea("max");return}it()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"up"})))}function Pr(){if(jt<=0){ea("min");return}it()&&(!s.ws||s.ws.readyState!==WebSocket.OPEN||s.ws.send(JSON.stringify({type:"admin",action:"set_volume",direction:"down"})))}function ea(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 Hr(){var e=await Ie(L.t("admin.endGameConfirm")||"End Game?",L.t("admin.endGameWarning")||"All players will be disconnected.",L.t("admin.endGame")||"End Game",L.t("common.cancel"));if(e&&it()){if(!s.ws||s.ws.readyState!==WebSocket.OPEN){alert(L.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=L.t("game.ending"))}s.ws.send(JSON.stringify({type:"admin",action:"end_game"}))}}var nt=!1;function ta(){if(!nt&&s.ws&&s.ws.readyState===WebSocket.OPEN){nt=!0;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!0,e.textContent=L.t("game.loading")),t){t.disabled=!0;var n=t.querySelector(".control-label");n&&(n.textContent=L.t("game.wait"))}s.ws.send(JSON.stringify({type:"admin",action:"next_round"})),setTimeout(function(){nt&&Yt()},1e4)}}function Yt(){nt=!1;var e=document.getElementById("next-round-btn"),t=document.getElementById("next-round-admin-btn");if(e&&(e.disabled=!1,e.textContent=L.t("admin.nextRound")),t){t.disabled=!1;var n=t.querySelector(".control-label");n&&(n.textContent=L.t("admin.nextRound"))}}function Dr(){ta()}function na(){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",Rr),t&&t.addEventListener("click",Or),n&&n.addEventListener("click",Pr),a&&a.addEventListener("click",Dr),i&&i.addEventListener("click",Hr)}function aa(){Fe=!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=L.t("game.stopped"))}}function Jt(){Fe=!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=L.t("game.stop"))}}function ra(e){jt=e,Gr(e),Vr(e)}function Gr(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 Vr(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 ia(){var e=document.getElementById("next-round-btn");e&&e.addEventListener("click",ta);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 sa(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 oa(){var e=document.getElementById("intro-splash-modal");e&&e.classList.add("hidden")}var m=window.BeatifyUtils||{},la=30,ot=null;function Xt(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),Wr(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 u=l.querySelector("[data-i18n]");u&&(u.setAttribute("data-i18n","game.introStopped"),u.textContent=m.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 f=document.getElementById("reveal-backdrop");if(f){var c=t.album_art;if(c){var v=new Image;v.onload=function(){f.style.backgroundImage='url("'+c+'")',f.classList.remove("reveal-backdrop--synthetic")},v.onerror=function(){f.style.backgroundImage="",f.classList.add("reveal-backdrop--synthetic")},v.src=c}else f.style.backgroundImage="",f.classList.add("reveal-backdrop--synthetic")}var g=document.getElementById("correct-year");g&&(g.textContent=t.year||"????");var y=document.getElementById("song-title"),h=document.getElementById("song-artist");y&&(y.textContent=t.title||"Unknown Song"),h&&(h.textContent=t.artist||"Unknown Artist");var b=document.getElementById("fun-fact-container"),E=document.getElementById("fun-fact"),I=b?b.querySelector(".fun-fact-header"):null,C=m.getLocalizedSongField(t,"fun_fact");if(E&&(E.textContent=C||""),I&&(I.style.display=C?"flex":"none"),Ur(t),qr(e.song_difficulty),b){var x=document.getElementById("song-rich-info"),B=x&&x.innerHTML.trim()!=="",D=C&&C.trim()!=="";b.classList.toggle("hidden",!D&&!B)}for(var A=null,M=0;M'+m.t("analytics.noGuesses")+" ";var a=e.map(function(G){return G.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,u=r+o,d=Math.max(1,u-l);function f(G){return(G-l)/d*100}for(var c=f(t),v='
",g="",y=0;y<=4;y++){var h=Math.round(l+d*y/4),b=y*25;g+='
'+h+"
"}function E(G){for(var T=0,O=0;O
>>0;return"c"+(T%4+1)}for(var I="",C=0;C0?"dotaxis-score--pos":"dotaxis-score--zero",R=H>0?"+"+H:"+0";I+=''+D+'
'+R+"
"}for(var ie=e.slice().sort(function(G,T){return(G.years_off||0)-(T.years_off||0)}),Ae=["\u{1F3C6}","\u{1F948}","\u{1F949}"],Te="",se=0;se'+Ae[se]+"":"",ye=n&&J.name===n?' '+m.t("analytics.youMarker")+" ":"";Te+=''+oe+' '+p(J.name||"?")+ye+" "}return''+Te+"
"}function qr(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+'
'+m.t("difficulty."+e.label)+' '+e.accuracy+"% "+m.t("difficulty.accuracy")+" ",t.classList.remove("hidden")}}function Ur(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=zr(e.certifications||[]);i.length>0&&(n=n.concat(i));var r=m.getLocalizedSongField(e,"awards")||[],o=Qr(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+" "+m.t("reveal.weeksShort")+" ":"";t.push('\u{1F4CA} #'+e.billboard_peak+" "+m.t("reveal.chartBillboard")+n+" ")}return e.german_peak&&e.german_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA} #'+e.german_peak+" "+m.t("reveal.chartGerman")+" "),e.uk_peak&&e.uk_peak>0&&!e.billboard_peak&&t.push('\u{1F4CA} #'+e.uk_peak+" "+m.t("reveal.chartUK")+" "),t}function zr(e){if(!e||e.length===0)return[];for(var t=[],n=0;n'+r+" "+p(a)+"")}return t}function Yr(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 Jr(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 Qr(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 Xr(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 Kr(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 Zr(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=m.t("reveal.emotions");function l(y){return y[Math.floor(Math.random()*y.length)]}function u(y){return y===1?m.t("reveal.offByYear"):m.t("reveal.offByYears",{years:y})}var d="missed",f=l(o.missed),c=l(o.missedSub);if(e&&!e.missed_round){var v=e.years_off||0;v===0?(d="exact",f=l(o.exact),c=l(o.exactSub)):v<=2?(d="close",f=l(o.close),c=l(o.closeSub)+" "+u(v)):v<=5?(d="close",f=l(o.close),c=u(v)):(d="wrong",f=l(o.wrong),c=l(o.wrongSub)+" "+u(v))}else e&&e.missed_round&&(d="missed",f=l(o.missed),c=l(o.missedSub));if(i)n.textContent=f,n.classList.add("duel-emotion--"+d);else{var g=''+f+" ";c&&(g+=''+c+"
"),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 $r(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=m.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?m.t("reveal.duel.yearUnit")||"year":m.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 ei(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(m.t("reveal.chip.betWon")||"Bet won \xB7 \xD72")+" "):e.bet_outcome==="lost"&&a.push('\u{1F3B2} '+p(m.t("reveal.chip.betLost")||"Bet lost")+" ");var i=e.streak_bonus||0;if(i>0&&e.streak){var r=m.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 Kt(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 ti(e){var t=document.getElementById("reveal-total-pts"),n=document.getElementById("score-row-subtitle");if(t){var a=Kt(e);if(t.textContent=(a>=0?"+":"")+a,n)if(!e||e.missed_round)n.textContent=m.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=m.t(r,{years:i})||i+" years off"}}}function ni(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 ai(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=(m.t("analytics.youMarker")||"YOU").replace(/[()]/g,""),o="";n.forEach(function(l,u){var d=i[l.name]||{},f=l.name===s.playerName,c=((l.name||"?").trim().charAt(0)||"?").toUpperCase(),v=l.rank_change||0,g=v>0?"up":v<0?"down":"flat",y=v>0?"\u25B2"+v:v<0?"\u25BC"+Math.abs(v):"\u2013",h="";if(!d.missed_round&&d.years_off!=null&&d.guess!=null&&d.guess!==""){var b=d.years_off,E=b===0?''+p(m.t("reveal.exact")||"Exact!")+" ":p(m.t("reveal.shortOff",{years:b})||b+" off");h=''+p(String(d.guess))+" \xB7 "+E+"
"}var I=[];d.streak&&d.streak>=2&&I.push('\u{1F525} '+d.streak+" "),d.stole_from?I.push('\u{1F977} '+p(m.t("steal.stolenFrom",{name:d.stole_from})||"stole "+d.stole_from)+" "):d.was_stolen_by&&d.was_stolen_by.length&&I.push('\u{1F3AF} '+p(m.t("steal.stolenBy",{name:d.was_stolen_by.join(", ")})||"stolen")+" "),d.bet_outcome==="won"?I.push('\u{1F3B2} '+p(m.t("reveal.chip.betWon")||"Bet won")+" "):d.bet_outcome==="lost"&&I.push('\u{1F3B2} '+p(m.t("reveal.chip.betLost")||"Bet lost")+" ");var C=I.length?''+I.join("")+"
":"",x=Kt(d),B=x>0?"":" rstand-delta--zero",D=(x>=0?"+":"")+x,A=u>=4?" rstand-row--taper2":u>=3?" rstand-row--taper1":"",M=f?" rstand-row--you":"",P=f?' '+p(r)+" ":"";o+=''+l.rank+' '+y+'
'+p(c)+'
'+p(l.name||"?")+P+"
"+h+C+'
'+(l.score||0)+' '+D+"
"}),t.innerHTML=o,Ze(a,n)}}function ri(){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='\u23F0
'+p(m.t("reveal.breakdown.noSubmission")||m.t("reveal.noSubmission")||"No guess submitted")+'
'+p(m.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,u=Math.floor(r*l)-r;a.push({emoji:"\u{1F3AF}",label:m.t("reveal.breakdown.baseScore",{years:i})||"Base score",value:String(r),kind:"neutral"}),l>1&&u>0&&a.push({emoji:"\u26A1",label:(m.t("reveal.breakdown.speedBonus")||"Speed bonus")+" ("+l.toFixed(2)+"\xD7)",value:"+"+u,kind:"positive"}),n.streak_bonus&&n.streak_bonus>0&&a.push({emoji:"\u{1F525}",label:m.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:m.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:m.t("reveal.breakdown.movieBonus")||"Movie challenge",value:"+"+n.movie_bonus,kind:"positive"}),n.intro_bonus&&n.intro_bonus>0&&a.push({emoji:"\u26A1",label:m.t("reveal.breakdown.introBonus")||"Intro speed bonus",value:"+"+n.intro_bonus,kind:"positive"}),n.bet_outcome==="won"?a.push({emoji:"\u{1F3B2}",label:m.t("reveal.breakdown.betMultiplier")||"Double or Nothing",value:"\xD72",kind:"multiplier"}):n.bet_outcome==="lost"&&a.push({emoji:"\u{1F3B2}",label:m.t("reveal.breakdown.betLost")||"Bet lost",value:"\xD70",kind:"multiplier"});var d=Kt(n),f='';a.forEach(function(c){f+='
'+c.emoji+" "+p(c.label)+' '+p(c.value)+"
"}),f+="
",f+=''+p(m.t("reveal.breakdown.total")||"Total this round")+' '+(d>=0?"+":"")+d+"
",e.innerHTML=f}}function ii(){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="",u=5,d=0;d\u2605';var f=m.t("difficulty."+a.label)||a.label||"",c=a.accuracy!=null?m.t("reveal.stats.onlyPercent",{percent:a.accuracy})||"Only "+a.accuracy+"% of all players guess it right.":"";o.push(''+p(f)+(c?'
'+p(c)+"
":"")+'
'+l+"
")}if(n){var v=[];if(n.average_guess!=null){var g=r?Math.round(n.average_guess-r):null,y=g!=null?g===0?m.t("analytics.onTarget")||"On target":Math.abs(g)+" "+(m.t("reveal.duel.yearsUnit")||"years")+" "+(g>0?"late":"early"):"";v.push(''+p(m.t("reveal.stats.avgGuess")||"Avg guess")+'
'+Math.round(n.average_guess)+"
"+(y?'
'+p(y)+"
":"")+"
")}if(n.all_guesses&&n.all_guesses.length>0){var h=n.all_guesses[0],b=h.name+" \xB7 "+(h.years_off===0?m.t("reveal.exact")||"Exact!":h.years_off+" "+(h.years_off===1?m.t("reveal.duel.yearUnit")||"year":m.t("reveal.duel.yearsUnit")||"years")+" off");v.push(''+p(m.t("reveal.stats.closest")||"Closest")+'
'+p(String(h.guess))+'
'+p(b)+"
")}if(n.speed_champion&&n.speed_champion.time!=null&&v.push(''+p(m.t("reveal.stats.fastest")||"Fastest")+'
'+n.speed_champion.time+'s
'+p((n.speed_champion.names||[]).join(", "))+"
"),a&&a.times_played!=null){var E=m.t("reveal.stats.playedBeforeSub")||"across all Beatify games";v.push(''+p(m.t("reveal.stats.playedBefore")||"Played before")+'
'+a.times_played+'\xD7
'+p(E)+"
")}v.length>0&&o.push(' '+v.join("")+"
")}if(n&&n.all_guesses&&n.all_guesses.length>0&&o.push('"+Fr(n.all_guesses,r,s.playerName)+"
"),n&&n.furthest_players&&n.furthest_players.length>0&&n.all_guesses&&n.all_guesses.length>0){var I=n.all_guesses[n.all_guesses.length-1];if(I&&I.years_off>0){var C=n.furthest_players.map(function(x){return''+p(x)+' '+I.years_off+" "+(I.years_off===1?m.t("reveal.duel.yearUnit")||"yr":m.t("reveal.duel.yearsUnit")||"yrs")+" off
"}).join("");o.push('")}}o.length===0&&o.push(''+p(m.t("reveal.stats.empty")||"No stats for this round yet.")+"
"),e.innerHTML=o.join("")}}function da(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 Qt(e){var t=document.getElementById(e);t&&t.classList.add("hidden")}function ua(){var e=document.getElementById("points-breakdown-btn");e&&e.addEventListener("click",function(){da("points-breakdown-sheet",ri)});var t=document.getElementById("round-stats-btn");t&&t.addEventListener("click",function(){da("round-stats-sheet",ii)}),document.querySelectorAll("[data-sheet-close]").forEach(function(n){n.addEventListener("click",function(a){Qt(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(){Qt(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")&&Qt(a)})})}function si(){var e=document.getElementById("reveal-report-btn");e&&(e.textContent=m.t("reveal.reportBtn")||"\u{1F6A9} Wrong year?",e.disabled=!1)}function fa(){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=m.t("reveal.reportBtnDone")||"\u2713 Reported \u2014 thanks!",e.disabled=!0)})}function ca(e){var t="ta-pill ta-pill--"+(e||"skipped").replace(/_/g,"-"),n;switch(e){case"exact":n=m.t("titleArtist.statusExact")||"Correct";break;case"fuzzy":n=m.t("titleArtist.statusFuzzy")||"Close enough";break;case"near_miss_accepted":n=m.t("titleArtist.statusAccepted")||"Accepted";break;case"near_miss":n=m.t("titleArtist.statusNearMiss")||"Near miss";break;case"wrong":n=m.t("titleArtist.statusWrong")||"Wrong";break;default:n=m.t("titleArtist.statusSkipped")||"Skipped"}return''+p(n)+" "}function oi(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:m.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:m.t("titleArtist.verdictWin")||"Nailed it!"}:r===1?{tier:"partial",text:m.t("titleArtist.verdictPartial")||"Got one!"}:{tier:"miss",text:m.t("titleArtist.verdictMiss")||"Not this time"}}function li(e,t){var n=!!e.accepted,a=((e.player||"?").trim().charAt(0)||"?").toUpperCase(),i=n?"\u2713 +"+(e.points||0):"\u2717";return''+p(a)+' '+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 di(e,t){var n=document.getElementById("ta-reveal-section");if(n){if(!e||!e.correct_title){n.classList.add("hidden"),me();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)+' \u2014 '+p(e.correct_artist||"")+" ");for(var i=document.getElementById("ta-reveal-own"),r=e.results||[],o=null,l=0;l'+p(u.text)+" ":"";i.innerHTML=d+''+p(m.t("titleArtist.yourTitle")||"Your title")+' '+p(o.title||"\u2014")+" "+ca(o.title_status)+'
'+p(m.t("titleArtist.yourArtist")||"Your artist")+' '+p(o.artist||"\u2014")+" "+ca(o.artist_status)+"
"}else i.innerHTML=''+p(m.t("titleArtist.noGuess")||"No guess this round")+"
";var f=document.getElementById("ta-voting"),c=document.getElementById("ta-voting-cards"),v=document.getElementById("ta-voting-title"),g=document.getElementById("ta-voting-countdown"),y=e.near_misses||[],h=e.near_miss_outcomes||[],b=!!e.voting_open,E=!!(t&&t.is_admin);if(!f||!c){me();return}var I=function(B){return B==="artist"?m.t("titleArtist.artistLabel")||"Artist":m.t("titleArtist.titleLabel")||"Song title"};if(!b&&h.length>0){me(),f.classList.remove("hidden"),v&&(v.textContent=m.t("titleArtist.closeCallsDecided")||"Close calls \u2014 decided"),g&&(g.textContent="",g.classList.add("hidden")),c.innerHTML=h.map(function(B){return li(B,I)}).join("");return}if(y.length===0){f.classList.add("hidden"),c.innerHTML="",me();return}f.classList.remove("hidden"),v&&(v.textContent=m.t("titleArtist.voteHeader")||"Close calls \u2014 vote \u{1F44D}/\u{1F44E}"),g&&g.classList.remove("hidden");var C="";if(y.forEach(function(B){var D=B.player===s.playerName,A=B.votes_yes||0,M=B.votes_no||0,P=A+M,F=P?Math.round(A/P*100):0,H=P?100-F:0,q=((B.player||"?").trim().charAt(0)||"?").toUpperCase();if(C+=''+p(q)+' '+p(B.player)+' '+p(I(B.field))+'
\u201C'+p(B.guess||"\u2014")+'\u201D
\u{1F44D} '+A+'
\u{1F44E} '+M+'
',D)C+='
'+p(m.t("titleArtist.yourCloseCall")||"Your close call \u2014 others decide")+"
";else if(b){var R=s.taMyVotes[B.id],ie=R===!0||R===!1;C+='
\u{1F44D} \u{1F44E}
',ie&&(C+='
'+p(m.t("titleArtist.youVoted")||"You voted")+" "+(R?"\u{1F44D}":"\u{1F44E}")+" \xB7 "+p(m.t("titleArtist.tapToChange")||"tap to change")+"
")}E&&b&&(C+='
'+p(m.t("titleArtist.hostOverride")||"Host decides")+' Accept Reject
'),C+="
"}),c.innerHTML=C,b)ci(e);else{me();var x=document.getElementById("ta-voting-countdown");x&&(x.textContent=m.t("titleArtist.voteClosed")||"Voting closed",x.removeAttribute("aria-label"),x.classList.add("ta-voting-countdown--closed"))}}}function ci(e){me();var t=document.getElementById("ta-voting-countdown");if(!t)return;var n=e&&typeof e.vote_seconds_remaining=="number"?e.vote_seconds_remaining:la,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",m.t("titleArtist.voteCountdown",{seconds:r})||r+"s"),t.style.setProperty("--ta-vote-progress",r/la*360+"deg"),t.classList.remove("ta-voting-countdown--closed"),r<=0&&me()}i(),ot=setInterval(i,500)}function me(){ot&&(clearInterval(ot),ot=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(c){c.classList.remove("is-chosen")}),n.classList.add("is-chosen")}return}if(a){var u=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:u,accept:d}));var f=a.closest(".ta-vote-card");f&&(f.querySelectorAll(".ta-override-btn").forEach(function(c){c.classList.remove("is-chosen")}),a.classList.add("is-chosen"))}})}var N=window.BeatifyUtils||{};function pa(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 E=t.find(function(B){return B.rank===b}),I=document.querySelector(".podium-place.podium-"+b);I&&I.classList.toggle("hidden",!E);var C=document.getElementById("podium-"+b+"-name"),x=document.getElementById("podium-"+b+"-score");C&&(C.textContent=E?p(E.name):"---"),x&&(x.textContent=E?E.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 u=document.getElementById("final-leaderboard-list");u&&(u.innerHTML=t.map(function(b){var E=b.is_current?"is-current":"",I=b.connected===!1?"final-entry--disconnected":"",C=b.connected===!1?'(away) ':"";return'#'+b.rank+' '+p(b.name)+C+' '+b.score+"
"}).join("")),ui(e.superlatives),fi(e.highlights),vi(e.share_data);var d=document.getElementById("end-admin-controls"),f=document.getElementById("end-player-message");if(n&&n.is_admin){d&&d.classList.remove("hidden"),f&&f.classList.add("hidden");var c=document.getElementById("new-game-btn");c&&(c.onclick=gi);var v=document.getElementById("player-rematch-btn");v&&(v.onclick=function(){v.disabled=!0;var b=v.textContent;if(v.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(E){if(!E.ok)return E.json().then(function(I){throw new Error(I.message||"Rematch failed")});v.textContent="\u23F3"}).catch(function(E){console.error("[Player] Rematch failed:",E),alert(E.message||"Failed to start rematch"),v.disabled=!1,v.textContent=b})})}else d&&d.classList.add("hidden"),f&&f.classList.remove("hidden");if(n){var g=e.total_rounds||10,y=n.best_streak||0,h=y===g&&g>0;h?Be("perfect"):n.rank===1&&Be("winner")}}function ui(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 fi(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 vi(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"),mi(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(){pi(i)})})}}function mi(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="",u="",d="",f="",c="",v=0;v0&&(Re+=dn),V.type==="url"?(r.font="800 15px Outfit, system-ui, sans-serif",Re+=r.measureText(V.text).width):(r.font="900 18px Outfit, system-ui, sans-serif",Re+=r.measureText(V.num).width,r.font="600 15px Inter, system-ui, sans-serif",Re+=r.measureText(V.label).width)});var Z=T-Re/2;r.textAlign="left",Me.forEach(function(V,bt){bt>0&&(r.font="600 15px Inter, system-ui, sans-serif",r.fillStyle="#6b6b7a",r.fillText(" \xB7 ",Z,Ke),Z+=dn),V.type==="url"?(r.font="800 15px Outfit, system-ui, sans-serif",r.fillStyle="#00f5ff",r.fillText(V.text,Z,Ke),Z+=r.measureText(V.text).width):(r.font="900 18px Outfit, system-ui, sans-serif",r.fillStyle="#00f5ff",r.fillText(V.num,Z,Ke),Z+=r.measureText(V.num).width,r.font="600 15px Inter, system-ui, sans-serif",r.fillStyle="#b3b3c2",r.fillText(V.label,Z,Ke),Z+=r.measureText(V.label).width)})}}function pi(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(){ma(t)});return}}ma(t)}},"image/png")}function ma(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 ga(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 gi(){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||{},ba="beatify_onboarded_v2",yi=4e3,bi=1400;function Zt(){var e=document.querySelectorAll(".tour-card").length;return e>0?e:4}function hi(){try{return localStorage.getItem(ba)==="1"}catch{return!1}}function Ei(){try{localStorage.setItem(ba,"1")}catch{}}var k={active:!1,replay:!1,currentIdx:0,autoAdvanceTimer:null,readyTimer:null};function pe(){k.autoAdvanceTimer&&(clearTimeout(k.autoAdvanceTimer),k.autoAdvanceTimer=null)}function $t(){pe(),!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches)&&(k.autoAdvanceTimer=setTimeout(function(){ha()},yi))}function en(){for(var e=document.querySelectorAll(".tour-wiz-seg"),t=0;t'+o+' \u2192 '}}function ha(){pe(),k.currentIdx
xa){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='\u{1F389} '+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+' '+c+l+' '+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='\u{1F451} ':b&&(A='YOU ');var M=w?''+$.t("lobby.away")+" ":"";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;bQR code library not loaded',t.onclick=Bn,t.onkeydown=function(n){(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),Bn())})}}function Bn(){if(ee){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:ee,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Bt(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function kn(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Bt),n&&n.addEventListener("click",Bt),document.addEventListener("keydown",function(a){a.key==="Escape"&&e&&!e.classList.contains("hidden")&&Bt()})}function ir(){if(ee){var e=document.getElementById("invite-modal"),t=document.getElementById("invite-modal-code"),n=document.getElementById("invite-modal-url");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:ee,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',n&&(n.value=ee),e.classList.remove("hidden"),document.body.style.overflow="hidden";var a=document.getElementById("invite-modal-close");a&&a.focus()}}}function De(){var e=document.getElementById("invite-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="");var t=document.getElementById("invite-copy-feedback");t&&t.classList.add("hidden")}function sr(){var e=document.getElementById("invite-modal-url"),t=document.getElementById("invite-copy-feedback");!e||!ee||(navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(ee).then(function(){Cn(t)}).catch(function(){_n(e,t)}):_n(e,t))}function _n(e,t){e.select(),e.setSelectionRange(0,99999);try{document.execCommand("copy"),Cn(t)}catch(n){console.warn("[Beatify] Copy failed:",n)}}function Cn(e){e&&(e.classList.remove("hidden"),setTimeout(function(){e.classList.add("hidden")},2e3))}function An(){var e=document.getElementById("invite-modal"),t=e?e.querySelector(".invite-modal-backdrop"):null,n=document.getElementById("invite-modal-close"),a=document.getElementById("invite-players-btn"),i=document.getElementById("invite-copy-btn");t&&t.addEventListener("click",De),n&&n.addEventListener("click",De),a&&a.addEventListener("click",ir),i&&i.addEventListener("click",sr),document.addEventListener("keydown",function(r){r.key==="Escape"&&e&&!e.classList.contains("hidden")&&De()})}function Tn(e){var t=document.getElementById("admin-controls"),n=document.getElementById("lobby-status");if(t){(!e||!Array.isArray(e))&&(e=[]);var a=e.find(function(r){return r.name===s.playerName}),i=a?.is_admin===!0;i?(t.classList.remove("hidden"),n&&n.classList.add("hidden")):(t.classList.add("hidden"),n&&n.classList.remove("hidden"))}}function Nn(){var e=document.getElementById("start-game-btn");e?.addEventListener("click",function(){!s.ws||s.ws.readyState!==WebSocket.OPEN||(e.disabled=!0,e.textContent=$.t("game.starting"),s.ws.send(JSON.stringify({type:"admin",action:"start_game"})))})}function Mn(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=$.t("player.welcomeBack",{name:e}),t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},2e3))}function Rn(){var e=document.getElementById("volume-indicator");e&&(e.textContent=$.t("earlyReveal.message")||"All guesses in!",e.classList.remove("hidden"),e.classList.add("is-visible"),setTimeout(function(){e.classList.remove("is-visible"),setTimeout(function(){e.classList.add("hidden")},300)},1500))}var Ge=null,K=null,or=250,lr=400,_e=null;function Pn(){_e!==null&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(_e),_e=null}function dr(e){var t=1-e;return 1-t*t*t}function kt(e){if(Ge!==null&&K!==null&&typeof requestAnimationFrame=="function"){var t=Math.abs(e-K);if(t<=or){K=e;return}if(cr(e))return}fe();var n=document.getElementById("timer");if(!n)return;var a=document.getElementById("timer-neon"),i=document.getElementById("timer-float"),r=document.getElementById("timer-float-num");n.classList.remove("timer--warning","timer--critical"),a&&a.classList.remove("timer-neon--warn"),i&&i.classList.remove("timer-float--warn"),ur(a,i),K=e;var o=0;function l(){var c=Date.now(),d=Math.max(0,Math.ceil((K-c)/1e3));n.textContent=d,r&&(r.textContent=d),d<=5?(n.classList.remove("timer--warning"),n.classList.add("timer--critical")):d<=10?(n.classList.remove("timer--critical"),n.classList.add("timer--warning")):n.classList.remove("timer--warning","timer--critical"),a&&a.classList.toggle("timer-neon--warn",d<=10),i&&i.classList.toggle("timer-float--warn",d<=10),d===10?n.setAttribute("aria-label","10 seconds remaining"):d===5?n.setAttribute("aria-label","5 seconds!"):d===0?n.setAttribute("aria-label","Time is up!"):n.setAttribute("aria-label","Time remaining: "+d+" seconds"),d<=0&&(o+=1,(o===1||o%3===0)&&s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"round_timeout"})))}l(),Ge=setInterval(l,1e3)}function On(e){var t=document.getElementById("timer");if(!t)return!1;var n=document.getElementById("timer-neon"),a=document.getElementById("timer-float"),i=document.getElementById("timer-float-num");return t.textContent=e,i&&(i.textContent=e),e<=5?(t.classList.remove("timer--warning"),t.classList.add("timer--critical")):e<=10?(t.classList.remove("timer--critical"),t.classList.add("timer--warning")):t.classList.remove("timer--warning","timer--critical"),n&&n.classList.toggle("timer-neon--warn",e<=10),a&&a.classList.toggle("timer-float--warn",e<=10),!0}function cr(e){var t=document.getElementById("timer-neon");if(!document.getElementById("timer"))return!1;var n=K,a=Date.now();Pn(),t&&t.classList.add("timer-neon--catchup");function i(){var r=Date.now()-a,o=r/lr;if(o>=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?'\u{1F480} ':''+p(g)+" ";return'"}).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='",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''+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='\u23F0
'+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+='
'+f.emoji+" "+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('"+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('")}}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(a)+' '+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)+' \u2014 '+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(q)+' '+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+='
\u{1F44D} \u{1F44E}
',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")+' Accept Reject
'),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+'\u2192 '}}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='\u{1F389} '+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 `
${m.icon}
${_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 @@
+
+
+
+
+
💀
+
+
You're out
+
+
Watching from the sidelines
+
+
diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py
index 3ff8edf8..200b252d 100644
--- a/tests/unit/test_state.py
+++ b/tests/unit/test_state.py
@@ -2230,3 +2230,239 @@ async def start(self, *args, **kwargs):
await state.configure_party_lights(["light.a"], "medium")
assert captured["inherited"] is None
+
+
+# ---------------------------------------------------------------------------
+# Sudden Death mode (Issue #827)
+# ---------------------------------------------------------------------------
+
+
+def _add_live_player(state: GameState, name: str) -> None:
+ """Add a player whose WebSocket reads as genuinely connected (is_active)."""
+ ws = AsyncMock()
+ ws.closed = False
+ state.add_player(name, ws)
+ state.players[name].connected = True
+
+
+class TestSuddenDeathElimination:
+ """Core elimination logic for Sudden Death (#827)."""
+
+ def setup_method(self):
+ self.state = make_game_state()
+ _create_fresh_game(self.state, sudden_death_mode=True)
+ for n in ("Alice", "Bob", "Carol", "Dave"):
+ _add_live_player(self.state, n)
+
+ def _set_round_scores(self, scores: dict[str, int]) -> None:
+ for name, sc in scores.items():
+ self.state.players[name].round_score = sc
+
+ def test_sudden_death_skips_round_1(self):
+ """Round 1 never eliminates anyone."""
+ self.state.round = 1
+ self._set_round_scores({"Alice": 0, "Bob": 5, "Carol": 9, "Dave": 3})
+ eliminated = self.state._apply_sudden_death_elimination()
+ assert eliminated == []
+ assert all(not p.eliminated for p in self.state.players.values())
+
+ def test_sudden_death_eliminates_lowest_round_score(self):
+ """From round 2 on, the lowest *round* score is eliminated."""
+ self.state.round = 2
+ self._set_round_scores({"Alice": 8, "Bob": 2, "Carol": 9, "Dave": 5})
+ eliminated = self.state._apply_sudden_death_elimination()
+ assert eliminated == ["Bob"]
+ assert self.state.players["Bob"].eliminated is True
+ assert self.state.players["Bob"].eliminated_round == 2
+ # Everyone else survives.
+ assert not self.state.players["Alice"].eliminated
+ assert not self.state.players["Carol"].eliminated
+ assert not self.state.players["Dave"].eliminated
+
+ def test_sudden_death_uses_round_delta_not_cumulative(self):
+ """A high cumulative leader with the worst *round* is the one cut."""
+ self.state.round = 3
+ self.state.players["Alice"].score = 100 # cumulative leader
+ self.state.players["Bob"].score = 5
+ self._set_round_scores({"Alice": 0, "Bob": 7, "Carol": 4, "Dave": 6})
+ eliminated = self.state._apply_sudden_death_elimination()
+ assert eliminated == ["Alice"]
+
+ def test_sudden_death_tie_break_uses_last_submission(self):
+ """Tie for last → the slowest (latest) submitter is eliminated."""
+ self.state.round = 2
+ self._set_round_scores({"Alice": 1, "Bob": 1, "Carol": 9, "Dave": 9})
+ self.state.players["Alice"].submission_time = 10.0 # earlier = faster
+ self.state.players["Bob"].submission_time = 25.0 # later = slower → out
+ eliminated = self.state._apply_sudden_death_elimination()
+ assert eliminated == ["Bob"]
+ assert self.state.players["Alice"].eliminated is False
+
+ def test_sudden_death_non_submitter_is_slowest(self):
+ """A non-submitter (submission_time None) loses a last-place tie."""
+ self.state.round = 2
+ self._set_round_scores({"Alice": 0, "Bob": 0, "Carol": 5, "Dave": 5})
+ self.state.players["Alice"].submission_time = 12.0
+ self.state.players["Bob"].submission_time = None # never submitted → out
+ eliminated = self.state._apply_sudden_death_elimination()
+ assert eliminated == ["Bob"]
+
+ def test_sudden_death_no_elimination_with_one_survivor(self):
+ """Never eliminate the last player standing."""
+ self.state.round = 5
+ for n in ("Bob", "Carol", "Dave"):
+ self.state.players[n].eliminated = True
+ self._set_round_scores({"Alice": 0})
+ eliminated = self.state._apply_sudden_death_elimination()
+ assert eliminated == []
+ assert self.state.players["Alice"].eliminated is False
+
+ def test_sudden_death_disabled_no_elimination(self):
+ """With the mode off, scoring never eliminates anyone."""
+ self.state.sudden_death_mode = False
+ self.state.round = 4
+ self._set_round_scores({"Alice": 0, "Bob": 1, "Carol": 2, "Dave": 3})
+ eliminated = self.state._apply_sudden_death_elimination()
+ assert eliminated == []
+
+ def test_already_eliminated_excluded_from_next_cut(self):
+ """An eliminated player is not re-eliminated and not considered."""
+ self.state.round = 3
+ self.state.players["Bob"].eliminated = True
+ self.state.players["Bob"].eliminated_round = 2
+ self._set_round_scores({"Alice": 9, "Bob": -99, "Carol": 1, "Dave": 8})
+ eliminated = self.state._apply_sudden_death_elimination()
+ # Bob already out and ignored; Carol is the lowest live score.
+ assert eliminated == ["Carol"]
+ assert self.state.players["Bob"].eliminated_round == 2
+
+
+class TestSuddenDeathSubmissionTracker:
+ """all_submitted ignores eliminated players (#827)."""
+
+ def test_all_submitted_ignores_eliminated(self):
+ state = make_game_state()
+ _create_fresh_game(state, sudden_death_mode=True)
+ for n in ("Alice", "Bob", "Carol"):
+ _add_live_player(state, n)
+ # Carol is out and will never submit again.
+ state.players["Carol"].eliminated = True
+ state.players["Alice"].submitted = True
+ state.players["Bob"].submitted = True
+ state.players["Carol"].submitted = False
+ assert state.all_submitted() is True
+
+
+class TestSuddenDeathAutoEnd:
+ """start_round ends the game when one player remains (#827)."""
+
+ @pytest.mark.asyncio
+ async def test_sudden_death_auto_ends_at_one_remaining(self):
+ state = make_game_state()
+ _create_fresh_game(state, sudden_death_mode=True)
+ for n in ("Alice", "Bob", "Carol"):
+ _add_live_player(state, n)
+ state.players["Bob"].eliminated = True
+ state.players["Carol"].eliminated = True
+ state.round = 4
+ state.phase = GamePhase.PLAYING
+ started = await state.start_round()
+ assert started is False
+ assert state.phase == GamePhase.END
+
+ @pytest.mark.asyncio
+ async def test_no_auto_end_when_two_remain(self):
+ """Two survivors → start_round must NOT end the game on the guard."""
+ state = make_game_state()
+ _create_fresh_game(state, sudden_death_mode=True)
+ for n in ("Alice", "Bob", "Carol"):
+ _add_live_player(state, n)
+ state.players["Carol"].eliminated = True
+ state.round = 3
+ state.phase = GamePhase.PLAYING
+ # Stub playback so start_round can proceed past the guard without media.
+ with patch.object(state, "_ensure_media_player_service"):
+ state._media_player_service = None
+ await state.start_round()
+ # The auto-end guard did not fire (phase is not END from the guard).
+ assert state.phase != GamePhase.END
+
+
+class TestSuddenDeathPersistence:
+ """eliminated state survives a round but resets for a new game (#827)."""
+
+ def test_reset_round_preserves_eliminated(self):
+ state = make_game_state()
+ _create_fresh_game(state, sudden_death_mode=True)
+ _add_live_player(state, "Alice")
+ p = state.players["Alice"]
+ p.eliminated = True
+ p.eliminated_round = 2
+ p.reset_round()
+ assert p.eliminated is True
+ assert p.eliminated_round == 2
+
+ def test_reset_for_new_game_clears_eliminated(self):
+ state = make_game_state()
+ _create_fresh_game(state, sudden_death_mode=True)
+ _add_live_player(state, "Alice")
+ p = state.players["Alice"]
+ p.eliminated = True
+ p.eliminated_round = 2
+ p.reset_for_new_game()
+ assert p.eliminated is False
+ assert p.eliminated_round is None
+
+ @pytest.mark.asyncio
+ async def test_pause_resume_preserves_eliminated(self):
+ state = make_game_state()
+ _create_fresh_game(state, sudden_death_mode=True)
+ for n in ("Alice", "Bob", "Carol"):
+ _add_live_player(state, n)
+ state.phase = GamePhase.PLAYING
+ state.players["Carol"].eliminated = True
+ state.players["Carol"].eliminated_round = 2
+ await state.pause_game("test_pause")
+ await state.resume_game()
+ assert state.players["Carol"].eliminated is True
+ assert state.players["Carol"].eliminated_round == 2
+
+
+class TestSuddenDeathLiveToggle:
+ """The reveal-screen live toggle (#827)."""
+
+ def test_set_sudden_death_toggles_flag(self):
+ state = make_game_state()
+ _create_fresh_game(state) # default off
+ assert state.sudden_death_mode is False
+ assert state.set_sudden_death(True) is True
+ assert state.sudden_death_mode is True
+ assert state.set_sudden_death(False) is False
+ assert state.sudden_death_mode is False
+
+
+class TestSuddenDeathSuperlative:
+ """Last One Standing superlative (#827)."""
+
+ def test_last_one_standing_awarded_to_survivor(self):
+ state = make_game_state()
+ _create_fresh_game(state, sudden_death_mode=True)
+ for n in ("Alice", "Bob", "Carol"):
+ _add_live_player(state, n)
+ state.players["Bob"].eliminated = True
+ state.players["Carol"].eliminated = True
+ state.round = 4
+ awards = state.calculate_superlatives()
+ last = [a for a in awards if a["id"] == "last_one_standing"]
+ assert len(last) == 1
+ assert last[0]["player_name"] == "Alice"
+ assert last[0]["value"] == 2 # two eliminated
+
+ def test_no_last_one_standing_when_mode_off(self):
+ state = make_game_state()
+ _create_fresh_game(state) # sudden death off
+ for n in ("Alice", "Bob"):
+ _add_live_player(state, n)
+ state.round = 4
+ awards = state.calculate_superlatives()
+ assert not any(a["id"] == "last_one_standing" for a in awards)
From 73d9c8d613cce308b920d1e94332f710e7667c69 Mon Sep 17 00:00:00 2001
From: Claude Code
Date: Sun, 14 Jun 2026 14:28:31 +0200
Subject: [PATCH 2/3] =?UTF-8?q?fix(game):=20address=20Sudden=20Death=20fol?=
=?UTF-8?q?low-ups=20=E2=80=94=20winner,=20<3=20guard,=20cheers=20(#827)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Survivor is the winner: compute_winners() returns the last player standing
(regardless of cumulative score) once the game resolves; get_final_leaderboard
orders by survival (survivor 1st, then reverse elimination order), so the
podium and the "Last One Standing" hero agree.
- Server-side <3-player floor: start-gameplay auto-disables Sudden Death (with a
warning) when fewer than 3 players are connected — players join the lobby after
create_game, so this is the correct enforcement point.
- Eliminated players can cheer: surface the existing reaction bar during PLAYING
for eliminated spectators (piggybacks the live-reaction system; no new UI, keeps
the minimal Stark Blackout view).
- 4 new tests (survivor wins, score fallback, survival-order leaderboard).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../beatify/game/state_leaderboard.py | 31 ++++++++--
.../beatify/game/state_serialization.py | 11 ++++
.../beatify/server/game_views.py | 24 +++++++-
.../beatify/www/js/player-game.js | 5 ++
.../beatify/www/js/player.bundle.min.js | 4 +-
tests/unit/test_state.py | 60 +++++++++++++++++++
6 files changed, 127 insertions(+), 8 deletions(-)
diff --git a/custom_components/beatify/game/state_leaderboard.py b/custom_components/beatify/game/state_leaderboard.py
index e5ee66f5..08d0e84f 100644
--- a/custom_components/beatify/game/state_leaderboard.py
+++ b/custom_components/beatify/game/state_leaderboard.py
@@ -103,18 +103,39 @@ def get_final_leaderboard(self) -> list[dict[str, Any]]:
Note: is_current is set client-side based on playerName.
"""
- # Sort by score descending, then by name for tie-breaking display order
- sorted_players = sorted(
- self.players.values(),
- key=lambda p: (-p.score, p.name),
+ # Issue #827: in a Sudden Death game the finish order IS the survival
+ # order — the last one standing is 1st, then players in reverse
+ # elimination order (eliminated latest = higher), score breaking ties.
+ # Ranks are sequential (no score-tie grouping) because survival is a
+ # total order. Falls back to the score sort for normal games.
+ sudden_death = self.sudden_death_mode and any(
+ p.eliminated for p in self.players.values()
)
+ if sudden_death:
+ sorted_players = sorted(
+ self.players.values(),
+ key=lambda p: (
+ p.eliminated,
+ -(p.eliminated_round or 0),
+ -p.score,
+ p.name,
+ ),
+ )
+ else:
+ # Sort by score descending, then by name for tie-breaking display order
+ sorted_players = sorted(
+ self.players.values(),
+ key=lambda p: (-p.score, p.name),
+ )
leaderboard = []
current_rank = 0
previous_score = None
for i, player in enumerate(sorted_players):
- if player.score != previous_score:
+ if sudden_death:
+ current_rank = i + 1
+ elif player.score != previous_score:
current_rank = i + 1
previous_score = player.score
diff --git a/custom_components/beatify/game/state_serialization.py b/custom_components/beatify/game/state_serialization.py
index 2768a618..0606ed9b 100644
--- a/custom_components/beatify/game/state_serialization.py
+++ b/custom_components/beatify/game/state_serialization.py
@@ -141,6 +141,17 @@ def compute_winners(self) -> tuple[list[PlayerSession], int]:
"""
if not self.players:
return [], 0
+ # Issue #827: in Sudden Death the winner is the last player standing,
+ # regardless of cumulative score — provided the game actually ran to its
+ # conclusion (at least one elimination and exactly one survivor). Falls
+ # through to the score-based winner when Sudden Death is off or the game
+ # 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()
+ ):
+ 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]
return winners, top_score
diff --git a/custom_components/beatify/server/game_views.py b/custom_components/beatify/server/game_views.py
index f5c61c36..c6f4e202 100644
--- a/custom_components/beatify/server/game_views.py
+++ b/custom_components/beatify/server/game_views.py
@@ -591,6 +591,24 @@ async def post(self, request: web.Request) -> web.Response:
if game_state.phase != GamePhase.LOBBY:
return _json_error("Game already started", 409, code="INVALID_PHASE")
+ # Issue #827: Sudden Death requires >=3 players. Players join the LOBBY
+ # *after* create_game (which clears sessions), so the floor can only be
+ # enforced here, at the LOBBY->PLAYING transition. The wizard also
+ # disables the toggle client-side; this is the server-side backstop for
+ # direct API callers. Auto-disable rather than block the start so the
+ # 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
+ )
+ if connected_count < 3:
+ game_state.set_sudden_death(False)
+ sudden_death_warning = (
+ "Sudden Death needs at least 3 players — "
+ "starting without it."
+ )
+
# Set round end callback for broadcasting
ws_handler = data.get("ws_handler")
if ws_handler:
@@ -609,7 +627,11 @@ async def post(self, request: web.Request) -> web.Response:
if ws_handler:
await ws_handler.broadcast_state()
- return web.json_response({"success": True, "phase": game_state.phase.value})
+ response: dict[str, Any] = {"success": True, "phase": game_state.phase.value}
+ if sudden_death_warning: # Issue #827
+ response["warnings"] = [sudden_death_warning]
+ response["sudden_death_disabled"] = True
+ return web.json_response(response)
class SetSuddenDeathView(BeatifyAdminView):
diff --git a/custom_components/beatify/www/js/player-game.js b/custom_components/beatify/www/js/player-game.js
index ee6f65ab..691a4ca0 100644
--- a/custom_components/beatify/www/js/player-game.js
+++ b/custom_components/beatify/www/js/player-game.js
@@ -331,6 +331,11 @@ function applySuddenDeathState(data) {
subEl.textContent = utils.t('game.eliminatedRound', { round: round })
|| ('Eliminated · Round ' + round);
}
+
+ // Issue #827: eliminated players are spectators — surface the existing
+ // reaction bar during PLAYING (it normally only shows in REVEAL) so they
+ // can still cheer the active players. Piggybacks the live-reaction system.
+ showReactionBar();
} else {
// Restore the normal UI. Only un-hide the year-based play controls when
// NOT in Title & Artist mode (renderTitleArtistInput owns that toggle);
diff --git a/custom_components/beatify/www/js/player.bundle.min.js b/custom_components/beatify/www/js/player.bundle.min.js
index 5f89c76a..9db8c50a 100644
--- a/custom_components/beatify/www/js/player.bundle.min.js
+++ b/custom_components/beatify/www/js/player.bundle.min.js
@@ -1,2 +1,2 @@
-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+' '+c+l+' '+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='\u{1F451} ':b&&(A='YOU ');var M=w?''+$.t("lobby.away")+" ":"";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;bQR code library not loaded',t.onclick=Bn,t.onkeydown=function(n){(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),Bn())})}}function Bn(){if(ee){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:ee,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Bt(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function kn(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Bt),n&&n.addEventListener("click",Bt),document.addEventListener("keydown",function(a){a.key==="Escape"&&e&&!e.classList.contains("hidden")&&Bt()})}function ir(){if(ee){var e=document.getElementById("invite-modal"),t=document.getElementById("invite-modal-code"),n=document.getElementById("invite-modal-url");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:ee,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',n&&(n.value=ee),e.classList.remove("hidden"),document.body.style.overflow="hidden";var a=document.getElementById("invite-modal-close");a&&a.focus()}}}function De(){var e=document.getElementById("invite-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="");var t=document.getElementById("invite-copy-feedback");t&&t.classList.add("hidden")}function sr(){var e=document.getElementById("invite-modal-url"),t=document.getElementById("invite-copy-feedback");!e||!ee||(navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(ee).then(function(){Cn(t)}).catch(function(){_n(e,t)}):_n(e,t))}function _n(e,t){e.select(),e.setSelectionRange(0,99999);try{document.execCommand("copy"),Cn(t)}catch(n){console.warn("[Beatify] Copy failed:",n)}}function Cn(e){e&&(e.classList.remove("hidden"),setTimeout(function(){e.classList.add("hidden")},2e3))}function An(){var e=document.getElementById("invite-modal"),t=e?e.querySelector(".invite-modal-backdrop"):null,n=document.getElementById("invite-modal-close"),a=document.getElementById("invite-players-btn"),i=document.getElementById("invite-copy-btn");t&&t.addEventListener("click",De),n&&n.addEventListener("click",De),a&&a.addEventListener("click",ir),i&&i.addEventListener("click",sr),document.addEventListener("keydown",function(r){r.key==="Escape"&&e&&!e.classList.contains("hidden")&&De()})}function Tn(e){var t=document.getElementById("admin-controls"),n=document.getElementById("lobby-status");if(t){(!e||!Array.isArray(e))&&(e=[]);var a=e.find(function(r){return r.name===s.playerName}),i=a?.is_admin===!0;i?(t.classList.remove("hidden"),n&&n.classList.add("hidden")):(t.classList.add("hidden"),n&&n.classList.remove("hidden"))}}function Nn(){var e=document.getElementById("start-game-btn");e?.addEventListener("click",function(){!s.ws||s.ws.readyState!==WebSocket.OPEN||(e.disabled=!0,e.textContent=$.t("game.starting"),s.ws.send(JSON.stringify({type:"admin",action:"start_game"})))})}function Mn(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=$.t("player.welcomeBack",{name:e}),t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},2e3))}function Rn(){var e=document.getElementById("volume-indicator");e&&(e.textContent=$.t("earlyReveal.message")||"All guesses in!",e.classList.remove("hidden"),e.classList.add("is-visible"),setTimeout(function(){e.classList.remove("is-visible"),setTimeout(function(){e.classList.add("hidden")},300)},1500))}var Ge=null,K=null,or=250,lr=400,_e=null;function Pn(){_e!==null&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(_e),_e=null}function dr(e){var t=1-e;return 1-t*t*t}function kt(e){if(Ge!==null&&K!==null&&typeof requestAnimationFrame=="function"){var t=Math.abs(e-K);if(t<=or){K=e;return}if(cr(e))return}fe();var n=document.getElementById("timer");if(!n)return;var a=document.getElementById("timer-neon"),i=document.getElementById("timer-float"),r=document.getElementById("timer-float-num");n.classList.remove("timer--warning","timer--critical"),a&&a.classList.remove("timer-neon--warn"),i&&i.classList.remove("timer-float--warn"),ur(a,i),K=e;var o=0;function l(){var c=Date.now(),d=Math.max(0,Math.ceil((K-c)/1e3));n.textContent=d,r&&(r.textContent=d),d<=5?(n.classList.remove("timer--warning"),n.classList.add("timer--critical")):d<=10?(n.classList.remove("timer--critical"),n.classList.add("timer--warning")):n.classList.remove("timer--warning","timer--critical"),a&&a.classList.toggle("timer-neon--warn",d<=10),i&&i.classList.toggle("timer-float--warn",d<=10),d===10?n.setAttribute("aria-label","10 seconds remaining"):d===5?n.setAttribute("aria-label","5 seconds!"):d===0?n.setAttribute("aria-label","Time is up!"):n.setAttribute("aria-label","Time remaining: "+d+" seconds"),d<=0&&(o+=1,(o===1||o%3===0)&&s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"round_timeout"})))}l(),Ge=setInterval(l,1e3)}function On(e){var t=document.getElementById("timer");if(!t)return!1;var n=document.getElementById("timer-neon"),a=document.getElementById("timer-float"),i=document.getElementById("timer-float-num");return t.textContent=e,i&&(i.textContent=e),e<=5?(t.classList.remove("timer--warning"),t.classList.add("timer--critical")):e<=10?(t.classList.remove("timer--critical"),t.classList.add("timer--warning")):t.classList.remove("timer--warning","timer--critical"),n&&n.classList.toggle("timer-neon--warn",e<=10),a&&a.classList.toggle("timer-float--warn",e<=10),!0}function cr(e){var t=document.getElementById("timer-neon");if(!document.getElementById("timer"))return!1;var n=K,a=Date.now();Pn(),t&&t.classList.add("timer-neon--catchup");function i(){var r=Date.now()-a,o=r/lr;if(o>=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?'\u{1F480} ':''+p(g)+" ";return'"}).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='
",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''+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='\u23F0
'+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+='
'+f.emoji+" "+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('"+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('")}}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(a)+' '+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)+' \u2014 '+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(q)+' '+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+='
\u{1F44D} \u{1F44E}
',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")+' Accept Reject
'),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+'\u2192 '}}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='\u{1F389} '+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+' '+c+l+' '+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='\u{1F451} ':b&&(A='YOU ');var M=w?''+$.t("lobby.away")+" ":"";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;bQR code library not loaded ',t.onclick=_n,t.onkeydown=function(n){(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),_n())})}}function _n(){if(ee){var e=document.getElementById("qr-modal"),t=document.getElementById("qr-modal-code");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:ee,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',e.classList.remove("hidden"),document.body.style.overflow="hidden";var n=document.getElementById("qr-modal-close");n&&n.focus()}}}function Bt(){var e=document.getElementById("qr-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="")}function Cn(){var e=document.getElementById("qr-modal"),t=e?e.querySelector(".qr-modal-backdrop"):null,n=document.getElementById("qr-modal-close");t&&t.addEventListener("click",Bt),n&&n.addEventListener("click",Bt),document.addEventListener("keydown",function(a){a.key==="Escape"&&e&&!e.classList.contains("hidden")&&Bt()})}function ir(){if(ee){var e=document.getElementById("invite-modal"),t=document.getElementById("invite-modal-code"),n=document.getElementById("invite-modal-url");if(!(!e||!t)){t.innerHTML="",typeof QRCode<"u"?new QRCode(t,{text:ee,width:256,height:256,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.M}):t.innerHTML='QR code library not loaded
',n&&(n.value=ee),e.classList.remove("hidden"),document.body.style.overflow="hidden";var a=document.getElementById("invite-modal-close");a&&a.focus()}}}function De(){var e=document.getElementById("invite-modal");e&&(e.classList.add("hidden"),document.body.style.overflow="");var t=document.getElementById("invite-copy-feedback");t&&t.classList.add("hidden")}function sr(){var e=document.getElementById("invite-modal-url"),t=document.getElementById("invite-copy-feedback");!e||!ee||(navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(ee).then(function(){An(t)}).catch(function(){xn(e,t)}):xn(e,t))}function xn(e,t){e.select(),e.setSelectionRange(0,99999);try{document.execCommand("copy"),An(t)}catch(n){console.warn("[Beatify] Copy failed:",n)}}function An(e){e&&(e.classList.remove("hidden"),setTimeout(function(){e.classList.add("hidden")},2e3))}function Tn(){var e=document.getElementById("invite-modal"),t=e?e.querySelector(".invite-modal-backdrop"):null,n=document.getElementById("invite-modal-close"),a=document.getElementById("invite-players-btn"),i=document.getElementById("invite-copy-btn");t&&t.addEventListener("click",De),n&&n.addEventListener("click",De),a&&a.addEventListener("click",ir),i&&i.addEventListener("click",sr),document.addEventListener("keydown",function(r){r.key==="Escape"&&e&&!e.classList.contains("hidden")&&De()})}function Nn(e){var t=document.getElementById("admin-controls"),n=document.getElementById("lobby-status");if(t){(!e||!Array.isArray(e))&&(e=[]);var a=e.find(function(r){return r.name===s.playerName}),i=a?.is_admin===!0;i?(t.classList.remove("hidden"),n&&n.classList.add("hidden")):(t.classList.add("hidden"),n&&n.classList.remove("hidden"))}}function Mn(){var e=document.getElementById("start-game-btn");e?.addEventListener("click",function(){!s.ws||s.ws.readyState!==WebSocket.OPEN||(e.disabled=!0,e.textContent=$.t("game.starting"),s.ws.send(JSON.stringify({type:"admin",action:"start_game"})))})}function Rn(e){var t=document.getElementById("volume-indicator");t&&(t.textContent=$.t("player.welcomeBack",{name:e}),t.classList.remove("hidden"),t.classList.add("is-visible"),setTimeout(function(){t.classList.remove("is-visible"),setTimeout(function(){t.classList.add("hidden")},300)},2e3))}function On(){var e=document.getElementById("volume-indicator");e&&(e.textContent=$.t("earlyReveal.message")||"All guesses in!",e.classList.remove("hidden"),e.classList.add("is-visible"),setTimeout(function(){e.classList.remove("is-visible"),setTimeout(function(){e.classList.add("hidden")},300)},1500))}var Ge=null,K=null,or=250,lr=400,_e=null;function Hn(){_e!==null&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(_e),_e=null}function dr(e){var t=1-e;return 1-t*t*t}function kt(e){if(Ge!==null&&K!==null&&typeof requestAnimationFrame=="function"){var t=Math.abs(e-K);if(t<=or){K=e;return}if(cr(e))return}fe();var n=document.getElementById("timer");if(!n)return;var a=document.getElementById("timer-neon"),i=document.getElementById("timer-float"),r=document.getElementById("timer-float-num");n.classList.remove("timer--warning","timer--critical"),a&&a.classList.remove("timer-neon--warn"),i&&i.classList.remove("timer-float--warn"),ur(a,i),K=e;var o=0;function l(){var c=Date.now(),d=Math.max(0,Math.ceil((K-c)/1e3));n.textContent=d,r&&(r.textContent=d),d<=5?(n.classList.remove("timer--warning"),n.classList.add("timer--critical")):d<=10?(n.classList.remove("timer--critical"),n.classList.add("timer--warning")):n.classList.remove("timer--warning","timer--critical"),a&&a.classList.toggle("timer-neon--warn",d<=10),i&&i.classList.toggle("timer-float--warn",d<=10),d===10?n.setAttribute("aria-label","10 seconds remaining"):d===5?n.setAttribute("aria-label","5 seconds!"):d===0?n.setAttribute("aria-label","Time is up!"):n.setAttribute("aria-label","Time remaining: "+d+" seconds"),d<=0&&(o+=1,(o===1||o%3===0)&&s.ws&&s.ws.readyState===WebSocket.OPEN&&s.ws.send(JSON.stringify({type:"round_timeout"})))}l(),Ge=setInterval(l,1e3)}function Pn(e){var t=document.getElementById("timer");if(!t)return!1;var n=document.getElementById("timer-neon"),a=document.getElementById("timer-float"),i=document.getElementById("timer-float-num");return t.textContent=e,i&&(i.textContent=e),e<=5?(t.classList.remove("timer--warning"),t.classList.add("timer--critical")):e<=10?(t.classList.remove("timer--critical"),t.classList.add("timer--warning")):t.classList.remove("timer--warning","timer--critical"),n&&n.classList.toggle("timer-neon--warn",e<=10),a&&a.classList.toggle("timer-float--warn",e<=10),!0}function cr(e){var t=document.getElementById("timer-neon");if(!document.getElementById("timer"))return!1;var n=K,a=Date.now();Hn(),t&&t.classList.add("timer-neon--catchup");function i(){var r=Date.now()-a,o=r/lr;if(o>=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?'\u{1F480} ':''+p(g)+" ";return'"}).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='",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''+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='\u23F0
'+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+='
'+f.emoji+" "+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('"+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('")}}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(a)+' '+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)+' \u2014 '+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(q)+' '+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+='
\u{1F44D} \u{1F44E}
',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")+' Accept Reject
'),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+'\u2192 '}}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='\u{1F389} '+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