diff --git a/script.d5454706.js b/script.d5454706.js
deleted file mode 100644
index 8e41f46..0000000
--- a/script.d5454706.js
+++ /dev/null
@@ -1,468 +0,0 @@
-const STORAGE_KEY = 'impostorGameStateV2';
-const MAX_PLAYERS = 10;
-const MIN_PLAYERS = 3;
-const POOLS_CACHE_KEY = 'impostorWordPoolsV1';
-const POOLS_MANIFEST_URL = 'word-pools/manifest.json';
-
-const EMBEDDED_POOLS = [
- { id: 'animales_naturaleza', name: 'Animales y naturaleza', emoji: 'πΏ', words: ['Perro','Gato','Lobo','Zorro','Oso','Tigre','LeΓ³n','Pantera','Jaguar','Puma','Guepardo','Elefante','Rinoceronte','HipopΓ³tamo','Jirafa','Cebra','Camello','Dromedario','Canguro','Koala','Panda','Mapache','Nutria','Castor','Foca','Morsa','DelfΓn','Ballena','TiburΓ³n','Orca','Pulpo','Calamar','Medusa','Tortuga','Lagarto','Cocodrilo','Serpiente','Anaconda','Iguana','Rana','Sapo','BΓΊho','HalcΓ³n','Γguila','CΓ³ndor','Gaviota','Loro','Flamenco','PingΓΌino','Avestruz','Gallina','Pato','Ganso','Cisne','Abeja','Hormiga','Mariquita','LibΓ©lula','Mariposa','Escarabajo','Grillo','Saltamontes','AraΓ±a','EscorpiΓ³n','Lombriz','Caracol','Estrella de mar','Coral','Musgo','Helecho','Pino','Roble','Encina','Palmera','Cactus','BambΓΊ','Rosa','TulipΓ‘n','Girasol','Lavanda','MontaΓ±a','RΓo','Lago','Mar','Playa','Desierto','Selva','Bosque','Pradera','Glaciar','VolcΓ‘n'] },
- { id: 'vida_cotidiana', name: 'Vida cotidiana', emoji: 'π ', words: ['Pan','Leche','CafΓ©','TΓ©','Agua','Jugo','Refresco','Cerveza','Vino','Pizza','Hamburguesa','SΓ‘ndwich','Taco','Burrito','Pasta','Arroz','Paella','Sushi','Ramen','Ensalada','Sopa','Croqueta','Tortilla','Empanada','Arepa','Queso','JamΓ³n','Chorizo','Pollo','Carne','Cerdo','Pescado','Marisco','Patata','Tomate','Cebolla','Ajo','Pimiento','Zanahoria','Lechuga','BrΓ³coli','Coliflor','Manzana','PlΓ‘tano','Naranja','Pera','Uva','Fresa','Mango','PiΓ±a','MelΓ³n','SandΓa','Yogur','Galletas','Chocolate','Helado','Cereales','Mantequilla','Aceite','Sal','Pimienta','AzΓΊcar','Harina','Huevo','Cuchara','Tenedor','Cuchillo','Plato','Vaso','Taza','Olla','SartΓ©n','Microondas','Horno','Nevera','Mesa','Silla','SofΓ‘','Cama','Almohada','SΓ‘bana','Toalla','Ducha','JabΓ³n','ChampΓΊ','Cepillo','Pasta de dientes'] },
- { id: 'deportes', name: 'Deportes', emoji: 'π
', words: ['FΓΊtbol','Baloncesto','Tenis','PΓ‘del','BΓ‘dminton','Voleibol','BΓ©isbol','Rugby','Hockey hielo','Hockey cΓ©sped','Golf','Boxeo','MMA','Judo','Karate','Taekwondo','Esgrima','Tiro con arco','Halterofilia','Crossfit','Atletismo','MaratΓ³n','TriatlΓ³n','Ciclismo ruta','Ciclismo montaΓ±a','BMX','NataciΓ³n','Waterpolo','Surf','Vela','Remo','PiragΓΌismo','EsquΓ','Snowboard','Patinaje artΓstico','Patinaje velocidad','Curling','Escalada','Senderismo','Trail running','Parkour','Gimnasia artΓstica','Gimnasia rΓtmica','TrampolΓn','Skate','Breakdance','Carreras coches','FΓ³rmula 1','Rally','Karting','Motociclismo','Enduro','Motocross','EquitaciΓ³n','Polo','CrΓquet','Billar','Dardos','Petanca','Pickleball','Ultimate frisbee','Paintball','Airsoft','eSports'] },
- { id: 'marcas', name: 'Marcas', emoji: 'ποΈ', words: ['Apple','Samsung','Google','Microsoft','Amazon','Meta','Tesla','Toyota','Honda','Ford','BMW','Mercedes','Audi','Volkswagen','Porsche','Ferrari','Lamborghini','Maserati','McLaren','Chevrolet','Nissan','Kia','Hyundai','Peugeot','Renault','Volvo','Jaguar','Land Rover','Fiat','Alfa Romeo','Ducati','Yamaha','Canon','Nikon','Sony','Panasonic','LG','Philips','Siemens','Bosch','Whirlpool','Ikea','Zara','H&M','Uniqlo','Nike','Adidas','Puma','Reebok','New Balance','Under Armour','Converse','Vans','Patagonia','The North Face','Columbia','Leviβs','Calvin Klein','Gucci','Prada','Louis Vuitton','Chanel','HermΓ¨s','Dior','Rolex','Omega','Casio','Pepsi','Coca-Cola','Fanta','Red Bull','Monster','Starbucks','Nespresso','NestlΓ©','Danone','Kelloggβs','Oreo','Intel','AMD','Nvidia','Qualcomm','TikTok','Netflix','Disney','Warner Bros','HBO','Spotify','Airbnb','Uber','Booking'] },
- { id: 'musica', name: 'MΓΊsica', emoji: 'π΅', words: ['Guitarra','Piano','ViolΓn','BaterΓa','Bajo','SaxofΓ³n','Trompeta','Flauta','Clarinete','AcordeΓ³n','Ukelele','Arpa','Sintetizador','DJ','MicrΓ³fono','Altavoz','Concierto','Festival','Vinilo','Rock','Pop','Punk','Metal','Heavy','Thrash','Death metal','Jazz','Blues','Soul','Funk','R&B','Rap','Hip hop','Trap','ReggaetΓ³n','Salsa','Bachata','Merengue','Cumbia','Vallenato','Flamenco','Rumba','Bossa nova','Samba','Tango','Country','EDM','Techno','House','Trance','Dubstep','Drum and bass','Lo-fi','Reggae','Ska','K-pop','J-pop','Indie','Gospel','Γpera','SinfonΓa','Orquesta','Coro','Cantautor','Balada','Bolero','Ranchera','Corrido','Mariachi'] },
- { id: 'personajes', name: 'Personajes', emoji: 'π§', words: ['Sherlock Holmes','Harry Potter','Hermione Granger','Ron Weasley','Albus Dumbledore','Voldemort','Frodo BolsΓ³n','Sam Gamyi','Gandalf','Aragorn','Legolas','Gimli','Gollum','Bilbo BolsΓ³n','Katniss Everdeen','Peeta Mellark','Batman','Bruce Wayne','Joker','Harley Quinn','Superman','Clark Kent','Lois Lane','Wonder Woman','Diana Prince','Flash','Barry Allen','Aquaman','Arthur Curry','Spider-Man','Peter Parker','Iron Man','Tony Stark','CapitΓ‘n AmΓ©rica','Steve Rogers','Black Widow','Natasha Romanoff','Hulk','Bruce Banner','Thor','Loki','Thanos','Doctor Strange','Wanda Maximoff','Vision','Star-Lord','Gamora','Groot','Rocket','Drax','Deadpool','Wolverine','Magneto','Professor X','Storm','Cyclops','Jean Grey','Mystique','Darth Vader','Luke Skywalker','Leia Organa','Han Solo','Chewbacca','Yoda','Obi-Wan Kenobi','Anakin Skywalker','Rey','Kylo Ren','R2-D2','C-3PO','Indiana Jones','Lara Croft','James Bond','Mario','Luigi','Princesa Peach','Bowser','Link','Zelda','Geralt de Rivia','Ciri','Yennefer','Kratos','Atreus','Ellie','Joel Miller','Nathan Drake','Master Chief','Cortana','Sonic','Tails','Ash Ketchum','Pikachu','Goku','Vegeta','Naruto','Sasuke','Luffy','Zoro','Nami','Tanjiro','Nezuko','Saitama','Light Yagami','L Lawliet'] }
-];
-
-let availablePools = [];
-let poolsCache = {};
-
-let state = {
- phase: 'setup',
- numPlayers: 6,
- numImpostors: 1,
- gameTime: 180,
- deliberationTime: 60,
- playerNames: [],
- roles: [],
- civilianWord: '',
- impostorWord: '',
- currentReveal: 0,
- startPlayer: 0,
- turnDirection: 'horario',
- revealOrder: [],
- timerEndAt: null,
- timerPhase: null,
- votes: {},
- votingPlayer: 0,
- selections: [],
- executed: [],
- selectedPool: 'animales_naturaleza',
- votingPool: null,
- isTiebreak: false,
- tiebreakCandidates: []
-};
-
-const saveState = () => localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
-const loadState = () => {
- const raw = localStorage.getItem(STORAGE_KEY);
- if (!raw) return false;
- try { state = JSON.parse(raw); return true; } catch { return false; }
-};
-const clearState = () => localStorage.removeItem(STORAGE_KEY);
-
-const loadPoolsCache = () => {
- try { poolsCache = JSON.parse(localStorage.getItem(POOLS_CACHE_KEY) || '{}'); } catch { poolsCache = {}; }
-};
-const savePoolsCache = () => localStorage.setItem(POOLS_CACHE_KEY, JSON.stringify(poolsCache));
-
-// ---------- Defaults ----------
-function defaultImpostors(nPlayers) {
- const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
- let impostors = 1;
- if (capped > 7) impostors = 3;
- else if (capped > 5) impostors = 2;
- const halfCap = Math.max(1, Math.floor(capped / 2));
- return Math.min(impostors, halfCap);
-}
-
-function defaultGameTime(nPlayers) {
- const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
- if (capped <= 4) return 300;
- if (capped >= 10) return 900;
- const extraPlayers = capped - 4;
- const seconds = 300 + extraPlayers * 100;
- return Math.round(seconds / 30) * 30;
-}
-
-function defaultDeliberation(gameSeconds) {
- return Math.max(30, Math.round(gameSeconds / 3));
-}
-
-// ---------- Pools ----------
-async function loadPoolsList() {
- loadPoolsCache();
- let list = [];
- try {
- const res = await fetch(POOLS_MANIFEST_URL);
- if (res.ok) list = await res.json();
- } catch (_) {}
- if (!Array.isArray(list) || list.length === 0) {
- list = EMBEDDED_POOLS.map(p => ({ id: p.id, name: p.name, emoji: p.emoji, count: p.words.length }));
- }
- availablePools = list;
- renderPoolButtons();
-}
-
-function parseWordsFile(text) {
- const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
- if (!lines.length) return [];
- if (lines[0].startsWith('#')) return lines.slice(1);
- return lines;
-}
-
-async function pickWords() {
- const poolId = state.selectedPool || 'default';
- let words = [];
- if (poolsCache[poolId]?.words) {
- words = poolsCache[poolId].words;
- } else if (poolId !== 'default') {
- const res = await fetch(`word-pools/${poolId}.txt`);
- if (!res.ok) throw new Error('No se pudo cargar el pool');
- const text = await res.text();
- words = parseWordsFile(text);
- poolsCache[poolId] = { words, ts: Date.now() }; savePoolsCache();
- } else {
- words = EMBEDDED_POOLS[0].words;
- }
- const shuffled = [...words].sort(() => Math.random() - 0.5);
- return { civilian: shuffled[0], impostor: shuffled[1] };
-}
-
-function renderPoolButtons() {
- const container = document.getElementById('pool-buttons');
- if (!container) return;
- container.innerHTML = '';
- availablePools.forEach(pool => {
- const btn = document.createElement('button');
- btn.type = 'button';
- btn.className = 'pool-btn';
- btn.textContent = `${pool.emoji || 'π²'} ${pool.name || pool.id}`;
- if (state.selectedPool === pool.id) btn.classList.add('selected');
- btn.onclick = () => { state.selectedPool = pool.id; saveState(); renderPoolButtons(); };
- container.appendChild(btn);
- });
-}
-
-// ---------- ConfiguraciΓ³n y nombres ----------
-function goToNames() {
- let nPlayers = parseInt(document.getElementById('num-players').value) || MIN_PLAYERS;
- nPlayers = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
- const maxImpostors = Math.max(1, Math.floor(nPlayers / 2));
- let nImpostors = parseInt(document.getElementById('num-impostors').value) || defaultImpostors(nPlayers);
- nImpostors = Math.min(Math.max(1, nImpostors), maxImpostors);
- let gTime = parseInt(document.getElementById('game-time').value) || defaultGameTime(nPlayers);
- gTime = Math.min(Math.max(gTime, 60), 900);
- let dTime = parseInt(document.getElementById('deliberation-time').value) || defaultDeliberation(gTime);
- dTime = Math.min(Math.max(dTime, 30), Math.round(900 / 3));
- if (nImpostors >= nPlayers) { alert('Impostores debe ser menor que jugadores'); return; }
- state.numPlayers = nPlayers; state.numImpostors = nImpostors; state.gameTime = gTime; state.deliberationTime = dTime;
- buildNameInputs();
- showScreen('names-screen');
-}
-
-function buildNameInputs() {
- const list = document.getElementById('player-names-list');
- list.innerHTML = '';
- for (let i = 0; i < state.numPlayers; i++) {
- const div = document.createElement('div');
- div.className = 'player-name-item';
- div.innerHTML = `Jugador ${i+1}:`;
- list.appendChild(div);
- }
-}
-
-// ---------- Inicio de partida ----------
-function startGame() {
- state.playerNames = [];
- for (let i = 0; i < state.numPlayers; i++) {
- const val = document.getElementById(`player-name-${i}`).value.trim();
- state.playerNames.push(val || `Jugador ${i+1}`);
- }
- pickWords().then(({civilian, impostor}) => {
- state.civilianWord = civilian;
- state.impostorWord = impostor;
- finalizeStart();
- }).catch(() => {
- const fallback = EMBEDDED_POOLS[0].words;
- const shuffled = [...fallback].sort(() => Math.random() - 0.5);
- state.civilianWord = shuffled[0];
- state.impostorWord = shuffled[1];
- finalizeStart();
- });
-}
-
-function finalizeStart() {
- state.roles = Array(state.numPlayers - state.numImpostors).fill('CIVIL').concat(Array(state.numImpostors).fill('IMPOSTOR')).sort(() => Math.random()-0.5);
- state.startPlayer = Math.floor(Math.random() * state.numPlayers);
- state.turnDirection = Math.random() < 0.5 ? 'horario' : 'antihorario';
- const step = state.turnDirection === 'horario' ? 1 : -1;
- state.revealOrder = Array.from({length: state.numPlayers}, (_, k) => (state.startPlayer + step * k + state.numPlayers) % state.numPlayers);
- state.currentReveal = 0; state.phase = 'pre-reveal'; state.votes = {}; state.votingPlayer = 0; state.selections = []; state.executed = []; state.timerEndAt = null; state.timerPhase = null;
- state.votingPool = null; state.isTiebreak = false; state.tiebreakCandidates = [];
- saveState();
- renderSummary();
- showScreen('pre-reveal-screen');
-}
-
-// Ajustar defaults cuando se edita el nΒΊ de jugadores
-document.getElementById('num-players').addEventListener('change', () => {
- let nPlayers = parseInt(document.getElementById('num-players').value) || MIN_PLAYERS;
- nPlayers = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
- document.getElementById('num-players').value = nPlayers;
- const imp = defaultImpostors(nPlayers);
- const gTime = defaultGameTime(nPlayers);
- const dTime = defaultDeliberation(gTime);
- document.getElementById('num-impostors').max = Math.max(1, Math.floor(nPlayers / 2));
- document.getElementById('num-impostors').value = imp;
- document.getElementById('game-time').value = gTime;
- document.getElementById('deliberation-time').value = dTime;
-});
-
-function renderSummary() {
- const el = document.getElementById('config-summary');
- const fmt = secs => `${Math.floor(secs/60)}:${(secs%60).toString().padStart(2,'0')}`;
- const startName = state.playerNames[state.startPlayer] || `Jugador ${state.startPlayer+1}`;
- const poolMeta = availablePools.find(p => p.id === state.selectedPool) || EMBEDDED_POOLS[0];
- el.innerHTML = `
-
Jugadores: ${state.numPlayers}
- Impostores: ${state.numImpostors}
- Tiempo de partida: ${fmt(state.gameTime)}
- Tiempo de deliberaciΓ³n: ${fmt(state.deliberationTime)}
- Pool: ${poolMeta.emoji || 'π²'} ${poolMeta.name || poolMeta.id}
- Empieza: ${startName} Β· Orden: ${state.turnDirection === 'horario' ? 'Horario' : 'Antihorario'}
- `;
-}
-
-// ---------- RevelaciΓ³n ----------
-function loadCurrentReveal() {
- state.phase = 'reveal'; saveState();
- if (!state.revealOrder || state.revealOrder.length !== state.numPlayers) {
- const step = state.turnDirection === 'horario' ? 1 : -1;
- state.revealOrder = Array.from({length: state.numPlayers}, (_, k) => (state.startPlayer + step * k + state.numPlayers) % state.numPlayers);
- }
- const idx = state.revealOrder[state.currentReveal];
- const name = state.playerNames[idx];
- document.getElementById('current-player-name').textContent = name;
- document.getElementById('curtain-cover').classList.remove('lifted');
- document.getElementById('next-player-btn').style.display = 'none';
- document.getElementById('start-game-btn').style.display = 'none';
-}
-
-function liftCurtain() {
- const cover = document.getElementById('curtain-cover');
- if (cover.classList.contains('lifted')) return;
- cover.classList.add('lifted');
- const idx = state.revealOrder[state.currentReveal];
- const role = state.roles[idx];
- const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord;
- document.getElementById('role-text').textContent = role;
- document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor');
- document.getElementById('word-text').textContent = word;
- setTimeout(() => {
- if (state.currentReveal + 1 < state.numPlayers) document.getElementById('next-player-btn').style.display = 'block';
- else document.getElementById('start-game-btn').style.display = 'block';
- }, 700);
-}
-
-function nextReveal() { state.currentReveal++; saveState(); loadCurrentReveal(); }
-
-// swipe support
-(() => {
- const curtain = document.getElementById('curtain');
- let startY = null;
- curtain.addEventListener('touchstart', e => { startY = e.touches[0].clientY; }, {passive:true});
- curtain.addEventListener('touchmove', e => { if (startY === null) return; const dy = e.touches[0].clientY - startY; if (dy < -40) { liftCurtain(); startY = null; } }, {passive:true});
- curtain.addEventListener('click', liftCurtain);
-})();
-
-// ---------- Timers ----------
-let timerInterval = null;
-function startPhaseTimer(phase, seconds, elementId, onEnd) {
- if (timerInterval) clearInterval(timerInterval);
- const now = Date.now();
- state.timerPhase = phase;
- state.timerEndAt = now + seconds*1000;
- saveState();
- const el = document.getElementById(elementId);
- const tick = () => {
- const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000));
- updateTimerDisplay(el, remaining);
- if (remaining <= 0) { clearInterval(timerInterval); playBeep(); onEnd(); }
- };
- tick();
- timerInterval = setInterval(tick, 1000);
-}
-
-function resumeTimerIfNeeded() {
- if (!state.timerEndAt || !state.timerPhase) return;
- const remaining = Math.round((state.timerEndAt - Date.now())/1000);
- if (remaining <= 0) { state.timerEndAt = null; saveState(); return; }
- if (state.timerPhase === 'game') { showScreen('game-screen'); startPhaseTimer('game', remaining, 'game-timer', startDeliberationPhase); }
- else if (state.timerPhase === 'deliberation') { showScreen('deliberation-screen'); startPhaseTimer('deliberation', remaining, 'deliberation-timer', startVotingPhase); }
-}
-
-function updateTimerDisplay(el, remaining) {
- const minutes = Math.floor(remaining/60); const secs = remaining%60;
- el.textContent = `${minutes}:${secs.toString().padStart(2,'0')}`;
- el.className = 'timer';
- if (remaining <= 10) el.classList.add('danger'); else if (remaining <= 30) el.classList.add('warning');
-}
-
-function playBeep() {
- const ctx = new (window.AudioContext || window.webkitAudioContext)();
- const osc = ctx.createOscillator(); const gain = ctx.createGain();
- osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 820; osc.type = 'sine';
- gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.45);
- osc.start(); osc.stop(ctx.currentTime + 0.45);
-}
-
-// ---------- Fases ----------
-function startGamePhase() { state.phase = 'game'; saveState(); showScreen('game-screen'); startPhaseTimer('game', state.gameTime, 'game-timer', startDeliberationPhase); }
-function startDeliberationPhase() { state.phase = 'deliberation'; saveState(); showScreen('deliberation-screen'); startPhaseTimer('deliberation', state.deliberationTime, 'deliberation-timer', startVotingPhase); }
-function startVotingPhase(candidates = null, isTiebreak = false) {
- state.phase = 'voting';
- state.votingPlayer = 0;
- state.votes = {};
- state.selections = [];
- state.votingPool = candidates;
- state.isTiebreak = isTiebreak;
- saveState();
- renderVoting();
- showScreen('voting-screen');
-}
-function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); startDeliberationPhase(); }
-function skipToVoting() { if (timerInterval) clearInterval(timerInterval); startVotingPhase(); }
-function startTiebreakDeliberation(candidates) {
- state.phase = 'deliberation';
- state.tiebreakCandidates = candidates;
- saveState();
- showScreen('deliberation-screen');
- startPhaseTimer('deliberation', 60, 'deliberation-timer', () => startVotingPhase(candidates, true));
-}
-
-// ---------- VotaciΓ³n secreta ----------
-function renderVoting() {
- const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i);
- const voter = state.playerNames[state.votingPlayer];
- document.getElementById('voter-name').textContent = voter;
- document.getElementById('votes-needed').textContent = state.numImpostors;
- state.selections = state.selections || [];
- const list = document.getElementById('vote-list'); list.innerHTML = '';
- pool.forEach(i => {
- const item = document.createElement('div');
- item.className = 'player-item';
- item.textContent = state.playerNames[i];
- if (state.votes[i]) item.innerHTML += `Votos: ${state.votes[i]}`;
- if (state.selections.includes(i)) item.classList.add('selected');
- if (i === state.votingPlayer) {
- item.classList.add('disabled');
- item.style.opacity = '0.5';
- item.style.pointerEvents = 'none';
- } else {
- item.onclick = () => toggleSelection(i, item);
- }
- list.appendChild(item);
- });
- updateConfirmButton();
-}
-
-function toggleSelection(idx, el) {
- if (idx === state.votingPlayer) return;
- if (state.selections.includes(idx)) state.selections = state.selections.filter(x => x !== idx);
- else {
- if (state.selections.length >= state.numImpostors) return;
- state.selections.push(idx);
- }
- saveState();
- renderVoting();
-}
-
-function updateConfirmButton() {
- const btn = document.getElementById('confirm-vote-btn');
- btn.disabled = state.selections.length !== state.numImpostors;
-}
-
-function confirmCurrentVote() {
- state.selections.forEach(t => { state.votes[t] = (state.votes[t] || 0) + 1; });
- state.votingPlayer++;
- state.selections = [];
- saveState();
- if (state.votingPlayer >= state.numPlayers) { handleVoteOutcome(); return; }
- renderVoting();
-}
-
-// ---------- ResoluciΓ³n de voto ----------
-function handleVoteOutcome() {
- const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i);
- const counts = pool.map(idx => ({ idx, votes: state.votes[idx] || 0 }));
- counts.sort((a, b) => b.votes - a.votes);
-
- let slots = state.numImpostors;
- const executed = [];
- for (let i = 0; i < counts.length && slots > 0; ) {
- const currentVotes = counts[i].votes;
- const group = [];
- let j = i;
- while (j < counts.length && counts[j].votes === currentVotes) { group.push(counts[j].idx); j++; }
- if (group.length <= slots) {
- executed.push(...group);
- slots -= group.length;
- i = j;
- } else {
- // Tie for remaining slots
- if (state.isTiebreak) {
- // segunda vez empatados: ganan impostores
- state.executed = [];
- showResults(true);
- return;
- }
- startTiebreakDeliberation(group);
- return;
- }
- }
-
- state.executed = executed;
- showResults();
-}
-
-// ---------- Resultados ----------
-function showResults(isTiebreak = false) {
- state.phase = 'results'; saveState();
- const executed = state.executed || [];
- let impostorsAlive = 0;
- state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; });
- const winner = impostorsAlive > 0 ? 'IMPOSTORES' : 'CIVILES';
- const results = document.getElementById('results-content');
- results.innerHTML = `
- ${winner === 'CIVILES' ? 'β
Β‘GANAN LOS CIVILES!' : 'β Β‘GANAN LOS IMPOSTORES!'}
- Ejecutados: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : 'Nadie'}
- Votos: ${Object.keys(state.votes).length ? '' : 'Sin votos'}
- Roles revelados
- ${state.roles.map((role,i) => {
- const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord;
- const killed = executed.includes(i) ? 'executed' : '';
- return `${state.playerNames[i]}: ${role} β "${word}" ${killed ? 'β οΈ' : ''}
`;
- }).join('')}
- `;
- showScreen('results-screen');
-}
-
-// ---------- Utilidades ----------
-function showScreen(id) {
- document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
- document.getElementById(id).classList.add('active');
- state.phase = id.replace('-screen','');
- saveState();
-}
-
-function newMatch() { clearState(); state = { ...state, phase:'setup', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; location.reload(); }
-
-// ---------- RehidrataciΓ³n ----------
-(function init() {
- const restored = loadState();
- showScreen('setup-screen');
- loadPoolsList();
- if (!state.turnDirection) state.turnDirection = 'horario';
- if (typeof state.startPlayer !== 'number') state.startPlayer = 0;
- switch (state.phase) {
- case 'setup': showScreen('setup-screen'); break;
- case 'names': buildNameInputs(); showScreen('names-screen'); break;
- case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break;
- case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break;
- case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break;
- case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break;
- case 'voting': showScreen('voting-screen'); renderVoting(); break;
- case 'results': showResults(); break;
- default: showScreen('setup-screen');
- }
-})();
diff --git a/script.f88d8968.js b/script.f88d8968.js
new file mode 100644
index 0000000..05c0b74
--- /dev/null
+++ b/script.f88d8968.js
@@ -0,0 +1,1347 @@
+const STORAGE_KEY = 'impostorGameStateV2';
+const MAX_PLAYERS = 10;
+const MIN_PLAYERS = 3;
+const POOLS_CACHE_KEY = 'impostorWordPoolsV1';
+const POOLS_MANIFEST_URL = 'word-pools/manifest.json';
+const THEME_STORAGE_KEY = 'impostorGameTheme';
+const LANGUAGE_STORAGE_KEY = 'impostorGameLanguage';
+const SCREEN_LOCK_STORAGE_KEY = 'impostorGameScreenLock';
+
+// ---------- Internationalization system ----------
+const TRANSLATIONS = {
+ es: {
+ gameTitle: 'Juego del Impostor',
+ gameSubtitle: 'ΒΏPodrΓ‘s descubrir quiΓ©n es el impostor?',
+ play: 'Jugar',
+ rules: 'Reglas',
+ createdBy: 'Creado por DarΓo Sevilla',
+ rulesTitle: 'Reglas del Juego',
+ objective: 'Objetivo',
+ objectiveText: 'Los civiles deben identificar a los impostores antes de que termine el tiempo.',
+ preparation: 'PreparaciΓ³n',
+ preparationSteps: ['Cada jugador recibe una palabra secreta', 'Los civiles reciben la misma palabra', 'Los impostores reciben una palabra diferente pero relacionada'],
+ gameplay: 'Partida',
+ gameplaySteps: ['Por turnos, cada jugador da un sinΓ³nimo o descripciΓ³n de su palabra', 'Intenta ser especΓfico pero no revelar tu palabra exacta', 'Los impostores deben intentar pasar desapercibidos'],
+ voting: 'VotaciΓ³n',
+ votingSteps: ['Tras el tiempo de juego y deliberaciΓ³n, vota en secreto', 'Los mΓ‘s votados son eliminados', 'Si todos los impostores son eliminados, ganan los civiles', 'Si queda algΓΊn impostor, ellos ganan'],
+ back: 'Volver',
+ configuration: 'ConfiguraciΓ³n',
+ players: 'Jugadores',
+ impostors: 'Impostores',
+ gameTime: 'Tiempo de partida (seg)',
+ deliberationTime: 'DeliberaciΓ³n (seg)',
+ pools: 'Pools (toca para seleccionar)',
+ next: 'Siguiente',
+ playerNames: 'Nombres de jugadores',
+ startGame: 'Comenzar partida',
+ player: 'Jugador',
+ readyToReveal: 'Listo para revelar',
+ eachPlayerSecret: 'Cada jugador debe ver su rol en secreto. Desliza la cortina hacia arriba para revelar.',
+ startReveal: 'Empezar revelaciΓ³n',
+ revelation: 'RevelaciΓ³n',
+ turnOf: 'Turno de',
+ othersLookAway: 'Los demΓ‘s, no mirΓ©is. MantΓ©n levantada la cortina para ver tu rol.',
+ liftCurtain: 'LEVANTA LA CORTINA',
+ nextPlayer: 'Siguiente jugador',
+ startMatch: 'Β‘Iniciar partida!',
+ gameInProgress: 'Partida en curso',
+ giveSynonyms: 'A decir sinΓ³nimos!',
+ skipToDeliberation: 'Saltar a deliberaciΓ³n',
+ deliberation: 'DeliberaciΓ³n',
+ lastArguments: 'Γltimos argumentos antes de votar.',
+ goToVoting: 'Ir a votaciΓ³n',
+ secretVoting: 'VotaciΓ³n secreta',
+ passMobileTo: 'Pasa el mΓ³vil a',
+ chooseSuspects: 'Elige',
+ suspect: 'sospechoso(s)',
+ confirmVote: 'Confirmar voto',
+ votes: 'Votos',
+ results: 'Resultados',
+ civiliansWin: 'Β‘GANAN LOS CIVILES!',
+ impostorsWin: 'Β‘GANAN LOS IMPOSTORES!',
+ executed: 'Ejecutados',
+ nobody: 'Nadie',
+ noVotes: 'Sin votos',
+ revealedRoles: 'Roles revelados',
+ newMatch: 'Nueva partida',
+ civil: 'CIVIL',
+ impostor: 'IMPOSTOR',
+ civilians: 'civiles',
+ poolsLabel: 'Pools',
+ starts: 'Empieza',
+ order: 'Orden',
+ clockwise: 'Horario',
+ counterclockwise: 'Antihorario',
+ impostorsMustBeLess: 'Impostores debe ser menor que jugadores',
+ animalsNature: 'Animales y Naturaleza',
+ everydayObjects: 'Objetos Cotidianos',
+ exitGame: 'Salir de la partida',
+ poolsSelection: 'SelecciΓ³n de Pools',
+ poolsSelectionText: 'Toca para seleccionar las categorΓas de palabras que quieres usar en la partida.'
+ },
+ en: {
+ gameTitle: 'The Impostor Game',
+ gameSubtitle: 'Can you figure out who the impostor is?',
+ play: 'Play',
+ rules: 'Rules',
+ createdBy: 'Created by DarΓo Sevilla',
+ rulesTitle: 'Game Rules',
+ objective: 'Objective',
+ objectiveText: 'Civilians must identify the impostors before time runs out.',
+ preparation: 'Setup',
+ preparationSteps: ['Each player receives a secret word', 'Civilians receive the same word', 'Impostors receive a different but related word'],
+ gameplay: 'Gameplay',
+ gameplaySteps: ['Taking turns, each player gives a synonym or description of their word', 'Try to be specific but don\'t reveal your exact word', 'Impostors must try to blend in'],
+ voting: 'Voting',
+ votingSteps: ['After game time and deliberation, vote in secret', 'The most voted players are eliminated', 'If all impostors are eliminated, civilians win', 'If any impostor remains, they win'],
+ back: 'Back',
+ configuration: 'Setup',
+ players: 'Players',
+ impostors: 'Impostors',
+ gameTime: 'Game time (sec)',
+ deliberationTime: 'Deliberation (sec)',
+ pools: 'Pools (tap to select)',
+ next: 'Next',
+ playerNames: 'Player names',
+ startGame: 'Start game',
+ player: 'Player',
+ readyToReveal: 'Ready to reveal',
+ eachPlayerSecret: 'Each player must see their role in secret. Swipe the curtain up to reveal.',
+ startReveal: 'Start reveal',
+ revelation: 'Revelation',
+ turnOf: 'Turn of',
+ othersLookAway: 'Others, look away. Keep the curtain lifted to see your role.',
+ liftCurtain: 'LIFT THE CURTAIN',
+ nextPlayer: 'Next player',
+ startMatch: 'Start match!',
+ gameInProgress: 'Game in progress',
+ giveSynonyms: 'Give synonyms!',
+ skipToDeliberation: 'Skip to deliberation',
+ deliberation: 'Deliberation',
+ lastArguments: 'Last arguments before voting.',
+ goToVoting: 'Go to voting',
+ secretVoting: 'Secret voting',
+ passMobileTo: 'Pass the phone to',
+ chooseSuspects: 'Choose',
+ suspect: 'suspect(s)',
+ confirmVote: 'Confirm vote',
+ votes: 'Votes',
+ results: 'Results',
+ civiliansWin: 'CIVILIANS WIN!',
+ impostorsWin: 'IMPOSTORS WIN!',
+ executed: 'Executed',
+ nobody: 'Nobody',
+ noVotes: 'No votes',
+ revealedRoles: 'Revealed roles',
+ newMatch: 'New match',
+ civil: 'CIVILIAN',
+ impostor: 'IMPOSTOR',
+ civilians: 'civilians',
+ poolsLabel: 'Pools',
+ starts: 'Starts',
+ order: 'Order',
+ clockwise: 'Clockwise',
+ counterclockwise: 'Counterclockwise',
+ impostorsMustBeLess: 'Impostors must be less than players',
+ animalsNature: 'Animals and Nature',
+ everydayObjects: 'Everyday Objects',
+ exitGame: 'Exit Game',
+ poolsSelection: 'Pool Selection',
+ poolsSelectionText: 'Tap to select the word categories you want to use in the game.'
+ }
+};
+
+let currentLanguage = 'es';
+
+function getBrowserLanguage() {
+ const lang = navigator.language || navigator.userLanguage;
+ return lang.startsWith('es') ? 'es' : 'en';
+}
+
+function loadLanguage() {
+ const saved = localStorage.getItem(LANGUAGE_STORAGE_KEY);
+ return saved || getBrowserLanguage();
+}
+
+function saveLanguage(lang) {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, lang);
+}
+
+function t(key) {
+ return TRANSLATIONS[currentLanguage][key] || key;
+}
+
+function setLanguage(lang) {
+ currentLanguage = lang;
+ saveLanguage(lang);
+ document.documentElement.setAttribute('lang', lang);
+ updateUI();
+}
+
+function toggleLanguage() {
+ const newLang = currentLanguage === 'es' ? 'en' : 'es';
+ setLanguage(newLang);
+}
+
+async function updateUI() {
+ // Update language button
+ const langText = document.querySelector('.language-text');
+ if (langText) {
+ langText.textContent = currentLanguage.toUpperCase();
+ }
+
+ // Update all static text elements
+ updateStaticTexts();
+
+ // Reload pools for the new language (wait for it to complete)
+ await loadPoolsList();
+
+ // Re-render dynamic content if in specific phases
+ if (state.phase === 'names') {
+ buildNameInputs();
+ } else if (state.phase === 'pre-reveal') {
+ renderSummary();
+ } else if (state.phase === 'voting') {
+ renderVoting();
+ } else if (state.phase === 'results') {
+ showResults();
+ }
+}
+
+function updateStaticTexts() {
+ // Welcome screen
+ const welcomeTitle = document.querySelector('.welcome-title');
+ if (welcomeTitle) welcomeTitle.textContent = t('gameTitle');
+
+ const welcomeSubtitle = document.querySelector('.welcome-subtitle');
+ if (welcomeSubtitle) welcomeSubtitle.textContent = t('gameSubtitle');
+
+ const playBtn = document.querySelector('.btn-primary');
+ if (playBtn) playBtn.textContent = t('play');
+
+ const rulesBtn = document.querySelector('.btn-secondary');
+ if (rulesBtn) rulesBtn.textContent = t('rules');
+
+ const credits = document.querySelector('.welcome-credits');
+ if (credits) credits.textContent = t('createdBy');
+
+ // Rules screen
+ const rulesTitle = document.querySelector('#rules-screen h1');
+ if (rulesTitle) rulesTitle.textContent = t('rulesTitle');
+
+ const ruleSections = document.querySelectorAll('.rule-section');
+ if (ruleSections.length >= 4) {
+ ruleSections[0].querySelector('h3').textContent = t('objective');
+ ruleSections[0].querySelector('p').innerHTML = t('objectiveText');
+
+ ruleSections[1].querySelector('h3').textContent = t('preparation');
+ const prepSteps = t('preparationSteps');
+ ruleSections[1].querySelectorAll('p').forEach((p, i) => {
+ if (prepSteps[i]) p.textContent = `${i + 1}. ${prepSteps[i]}`;
+ });
+
+ ruleSections[2].querySelector('h3').textContent = t('gameplay');
+ const gameSteps = t('gameplaySteps');
+ ruleSections[2].querySelectorAll('p').forEach((p, i) => {
+ if (gameSteps[i]) p.textContent = `${i + 1}. ${gameSteps[i]}`;
+ });
+
+ ruleSections[3].querySelector('h3').textContent = t('voting');
+ const voteSteps = t('votingSteps');
+ ruleSections[3].querySelectorAll('p').forEach((p, i) => {
+ if (voteSteps[i]) p.textContent = `${i + 1}. ${voteSteps[i]}`;
+ });
+ }
+
+ // Setup screen
+ const setupTitle = document.querySelector('#setup-screen h1');
+ if (setupTitle) setupTitle.textContent = t('configuration');
+
+ const labels = {
+ 'num-players': t('players'),
+ 'num-impostors': t('impostors'),
+ 'game-time': t('gameTime'),
+ 'deliberation-time': t('deliberationTime')
+ };
+
+ Object.entries(labels).forEach(([id, text]) => {
+ const label = document.querySelector(`label[for="${id}"]`);
+ if (label) label.textContent = text + ':';
+ });
+
+ // Pools screen
+ const poolsTitle = document.querySelector('#pools-screen h1');
+ if (poolsTitle) poolsTitle.textContent = t('poolsSelection');
+
+ const poolsText = document.querySelector('#pools-screen .info-text');
+ if (poolsText) poolsText.textContent = t('poolsSelectionText');
+
+ // Names screen
+ const namesTitle = document.querySelector('#names-screen h1');
+ if (namesTitle) namesTitle.textContent = t('playerNames');
+
+ // Pre-reveal screen
+ const preRevealTitle = document.querySelector('#pre-reveal-screen h1');
+ if (preRevealTitle) preRevealTitle.textContent = t('readyToReveal');
+
+ const preRevealText = document.querySelector('#pre-reveal-screen .info-text:not(#config-summary)');
+ if (preRevealText) preRevealText.textContent = t('eachPlayerSecret');
+
+ // Reveal screen
+ const revealTitle = document.querySelector('#reveal-screen h1');
+ if (revealTitle) revealTitle.textContent = t('revelation');
+
+ // Game screen
+ const gameTitle = document.querySelector('#game-screen h1');
+ if (gameTitle) gameTitle.textContent = t('gameInProgress');
+
+ const gameText = document.querySelector('#game-screen .info-text');
+ if (gameText) gameText.textContent = t('giveSynonyms');
+
+ // Deliberation screen
+ const delibTitle = document.querySelector('#deliberation-screen h1');
+ if (delibTitle) delibTitle.textContent = t('deliberation');
+
+ const delibText = document.querySelector('#deliberation-screen .info-text');
+ if (delibText) delibText.textContent = t('lastArguments');
+
+ // Voting screen
+ const votingTitle = document.querySelector('#voting-screen h1');
+ if (votingTitle) votingTitle.textContent = t('secretVoting');
+
+ // Results screen
+ const resultsTitle = document.querySelector('#results-screen h1');
+ if (resultsTitle) resultsTitle.textContent = t('results');
+
+ // Buttons
+ const backButtons = document.querySelectorAll('button.ghost');
+ backButtons.forEach(btn => {
+ if (btn.textContent.includes('Volver') || btn.textContent.includes('Back')) {
+ btn.textContent = `β ${t('back')}`;
+ }
+ });
+
+ // Update all other buttons based on their onclick or content
+ document.querySelectorAll('button').forEach(btn => {
+ if (btn.getAttribute('onclick') === 'goToPools()') btn.textContent = t('next');
+ else if (btn.getAttribute('onclick') === 'goToNames()') btn.textContent = t('next');
+ else if (btn.getAttribute('onclick') === 'startGame()') btn.textContent = t('startGame');
+ else if (btn.getAttribute('onclick') === "showScreen('reveal-screen'); loadCurrentReveal();") btn.textContent = t('startReveal');
+ else if (btn.id === 'next-player-btn') btn.textContent = t('nextPlayer') + ' β';
+ else if (btn.id === 'start-game-btn') btn.textContent = t('startMatch');
+ else if (btn.getAttribute('onclick') === 'skipToDeliberation()') btn.textContent = t('skipToDeliberation') + ' β';
+ else if (btn.getAttribute('onclick') === 'skipToVoting()') btn.textContent = t('goToVoting') + ' β';
+ else if (btn.id === 'confirm-vote-btn') btn.textContent = t('confirmVote');
+ else if (btn.getAttribute('onclick') === 'newMatch()') btn.textContent = t('newMatch');
+ });
+
+ // Exit game button
+ const exitText = document.querySelector('.exit-text');
+ if (exitText) exitText.textContent = t('exitGame');
+}
+
+// Embedded pools with impostor words [civilian_word, impostor_word]
+const EMBEDDED_POOLS = [
+ // Spanish pools
+ { id: 'animales_naturaleza', name: 'Animales y Naturaleza', emoji: 'πΏ', lang: 'es', words: [['Oso','Pez'],['Pavo real','Abanico'],['Camello','Arena'],['Lirio','Rana'],['Lobo','Luna'],['Represa','Castor'],['Elefante','Safari'],['Flamenco','CamarΓ³n'],['BΓΊho','Nieve'],['Canguro','Koala'],['Jungla','Serpiente'],['Muerte','Cuervo'],['DelfΓn','Orca'],['Zorro','Gallina'],['Tortuga','GalΓ‘pagos'],['LeΓ³n','Sabana'],['Polo Sur','PingΓΌino'],['Hormiga','Trabajo'],['Abeja','Verano'],['Ballena','Dory'],['MandΓbula','TiburΓ³n'],['RΓo de Janeiro','Loro'],['Caballo','Libertad'],['Gorila','Plata'],['MurciΓ©lago','Fruta'],['Venado','Tambor'],['Misisipi','Γguila'],['Cisne','Lago'],['Grillo','Campo'],['Leopardo','Manchas'],['Mascarilla','Mapache'],['Chita','Velocidad'],['AraΓ±a','Nueva York'],['Playa','Medusa'],['Glaciar','Oso polar'],['Jirafa','Madagascar'],['Maine','Langosta'],['Pulpo','Pluma'],['Cuervo','Pantera'],['Foca','Rosa'],['Mariposa','Algodoncillo'],['Burro','Santorini'],['Lluvia','Caracol'],['Cangrejo','AraΓ±a'],['Rana','Grillo'],['Siberia','Tigre'],['Gaviota','Playa'],['Cocodrilo','Nilo'],['PingΓΌino','Nueva Zelanda'],['Loro','Gato'],['Cuervo','Bandada'],['Conejo','Agujero'],['TiburΓ³n','Paleozoico'],['Trueno','JΓΊpiter'],['Sol','Playa'],['OcΓ©ano AtlΓ‘ntico','HuracΓ‘n'],['Tsunami','Derrumbe'],['Ola','HawΓ‘i'],['Papel','Γrbol'],['Universo','EnergΓa'],['Vida','Tiempo'],['OcΓ©ano','Tormenta'],['Lago','Sal'],['OxΓgeno','Fuego'],['BiologΓa','CΓ©lula'],['Tiza','Hielo'],['Clima','Invierno'],['Planeta','Gas'],['Era de hielo','Bellota'],['Avalancha','MontaΓ±a'],['Bisonte','Llanuras'],['FloraciΓ³n','NΓ©ctar'],['CaΓ±Γ³n','Γguila'],['Ardilla listada','Nueces'],['Coral','Arrecife'],['Desierto','Espejismo'],['Ecosistema','Equilibrio'],['HalcΓ³n','Picado'],['LuciΓ©rnaga','Brillo'],['Gecko','Hoja'],['ColibrΓ','AzΓΊcar'],['Koala','Eucalipto'],['Meteoro','CrΓ‘ter'],['Nutria','RΓo'],['Selva tropical','Dosel'],['Rinoceronte','Cuerno'],['VolcΓ‘n','Ceniza'],['Naturaleza salvaje','Huellas']] },
+ { id: 'objetos_cotidianos', name: 'Objetos Cotidianos', emoji: 'π ', lang: 'es', words: [['Martillo','TiburΓ³n'],['Silla','Espalda'],['Mesa','CafΓ©'],['Cuchara','Crema'],['Tenedor','PosidΓ³n'],['Cuchillo','Mantequilla'],['Plata','Plato'],['Copa','Campeonato'],['Vidrio','Arena'],['Botella','Aerosol'],['Lata','Boda'],['TelΓ©fono','Radio'],['Laptop','Tarjeta'],['Teclado','Piano'],['RatΓ³n','Laboratorio'],['Marco','Pantalla'],['Control','SatΓ©lite'],['LΓ‘mpara','Aceite'],['Horno','Bombilla'],['Vela','Corona de flores'],['Carro','Espejo'],['Ventana','Caja'],['Puerta','Armario'],['Llave','Auto'],['Candado','Sello'],['Monedero','Piel'],['Cartera','Etiqueta'],['Mochila','AviΓ³n'],['Maleta','Toalla'],['Sombrero','Paja'],['Zapatos','Vela'],['Calcetas','Medida'],['Playera','AlgodΓ³n'],['Cierre','PantalΓ³n'],['Abrigo','Pelo'],['Paraguas','Ala'],['Vacaciones','Gafas de sol'],['Reloj de pulsera','Monitor'],['Rueda','Anillo'],['Collar','Tiara'],['Manga','Tatuaje'],['Cama','Monstruo'],['Funda','Media'],['Manta','Cuna'],['ColchΓ³n','Aire'],['Libro','Pop-up'],['Revista','Diario'],['PeriΓ³dico','Columna'],['Pluma','Bola'],['LΓ‘piz','Delineador'],['Borrador','Goma'],['Dibujo','Cuaderno'],['Tijeras','Cabello'],['Regla','Parrilla'],['Pegamento','Tubo'],['Cinta adhesiva','Clip'],['Pincel','Escoba'],['Cesto','Arco'],['Caja','Zapato'],['Sobre','Carta'],['Sello','Fecha'],['Calendario','Luna'],['Reloj','Campana'],['Radio','Onda'],['Bocina','Pared'],['DJ','AudΓfonos'],['MicrΓ³fono','TelevisiΓ³n'],['TelevisiΓ³n','Imagen'],['CΓ‘mara','LΓ‘ser'],['TrΓpode','Pierna'],['Ventilador','OxΓgeno'],['Calefactor','Secadora'],['Estufa','CarbΓ³n'],['Refrigerador','Leche'],['Congelador','Helado'],['Microondas','Radar'],['Tostadora','Horno'],['Licuadora','EspΓ‘tula'],['Olla','Sopa'],['Acero','SartΓ©n'],['Tetera','Cobre'],['Esponja','Gelatina'],['JabΓ³n','Barra'],['Toalla','Ducha'],['Cepillo de dientes','Lengua'],['Pasta de dientes','Gel'],['Marfil','Peine'],['Cepillo','Rastrillo'],['Navaja','JabΓ³n'],['ChampΓΊ','SΓ‘bila'],['Acondicionador','Espuma'],['LociΓ³n','Seda'],['Balde','Tierra'],['Trapeador','Piso'],['Escoba','AviΓ³n'],['Recogedor','Nube'],['Basurero','CamiΓ³n'],['Reciclaje','Papel'],['Escalera','Cuerda']] },
+
+ // English pools
+ { id: 'animals_nature_en', name: 'Animals and Nature', emoji: 'πΏ', lang: 'en', words: [['Bear','Fish'],['Peacock','Fan'],['Camel','Sand'],['Lily','Frog'],['Wolf','Moon'],['Dam','Beaver'],['Elephant','Safari'],['Flamingo','Shrimp'],['Owl','Snow'],['Kangaroo','Koala'],['Jungle','Snake'],['Death','Crow'],['Dolphin','Orca'],['Fox','Chicken'],['Turtle','Galapagos'],['Lion','Savanna'],['South Pole','Penguin'],['Ant','Work'],['Bee','Summer'],['Whale','Dory'],['Jaw','Shark'],['Rio','Parrot'],['Horse','Freedom'],['Gorilla','Silver'],['Bat','Fruit'],['Deer','Drum'],['Mississippi','Eagle'],['Swan','Lake'],['Cricket','Field'],['Leopard','Spots'],['Mask','Raccoon'],['Cheetah','Speed'],['Spider','New York'],['Beach','Jellyfish'],['Glacier','Polar bear'],['Giraffe','Madagascar'],['Maine','Lobster'],['Octopus','Feather'],['Raven','Panther'],['Seal','Rose'],['Butterfly','Milkweed'],['Donkey','Santorini'],['Rain','Snail'],['Crab','Spider'],['Frog','Cricket'],['Siberia','Tiger'],['Seagull','Beach'],['Crocodile','Nile'],['Penguin','New Zealand'],['Parrot','Cat'],['Crow','Flock'],['Rabbit','Hole'],['Shark','Paleozoic'],['Thunder','Jupiter'],['Sun','Beach'],['Atlantic','Hurricane'],['Tsunami','Landslide'],['Wave','Hawaii'],['Paper','Tree'],['Universe','Energy'],['Life','Time'],['Ocean','Storm'],['Lake','Salt'],['Oxygen','Fire'],['Biology','Cell'],['Chalk','Ice'],['Climate','Winter'],['Planet','Gas'],['Ice age','Acorn'],['Avalanche','Mountain'],['Bison','Plains'],['Bloom','Nectar'],['Canyon','Eagle'],['Chipmunk','Nuts'],['Coral','Reef'],['Desert','Mirage'],['Ecosystem','Balance'],['Falcon','Dive'],['Firefly','Glow'],['Gecko','Leaf'],['Hummingbird','Sugar'],['Koala','Eucalyptus'],['Meteor','Crater'],['Otter','River'],['Rainforest','Canopy'],['Rhino','Horn'],['Volcano','Ash'],['Wilderness','Tracks']] },
+ { id: 'everyday_objects_en', name: 'Everyday Objects', emoji: 'π ', lang: 'en', words: [['Hammer','Shark'],['Chair','Back'],['Table','Coffee'],['Spoon','Cream'],['Fork','Poseidon'],['Knife','Butter'],['Silver','Plate'],['Cup','Championship'],['Glass','Sand'],['Bottle','Spray'],['Can','Wedding'],['Phone','Radio'],['Laptop','Card'],['Keyboard','Piano'],['Mouse','Lab'],['Frame','Screen'],['Remote','Satellite'],['Lamp','Oil'],['Oven','Bulb'],['Candle','Wreath'],['Car','Mirror'],['Window','Box'],['Door','Closet'],['Key','Car'],['Lock','Seal'],['Wallet','Leather'],['Purse','Tag'],['Backpack','Airplane'],['Suitcase','Towel'],['Hat','Straw'],['Shoes','Sail'],['Socks','Measure'],['Shirt','Cotton'],['Zipper','Pants'],['Coat','Hair'],['Umbrella','Wing'],['Vacation','Sunglasses'],['Watch','Monitor'],['Wheel','Ring'],['Necklace','Tiara'],['Sleeve','Tattoo'],['Bed','Monster'],['Pillowcase','Stocking'],['Blanket','Cradle'],['Mattress','Air'],['Book','Pop-up'],['Magazine','Journal'],['Newspaper','Column'],['Pen','Ball'],['Pencil','Eyeliner'],['Eraser','Rubber'],['Notebook','Drawing'],['Scissors','Hair'],['Ruler','Grill'],['Glue','Tube'],['Tape','Clip'],['Brush','Broom'],['Basket','Arc'],['Box','Shoe'],['Envelope','Letter'],['Stamp','Date'],['Calendar','Moon'],['Clock','Bell'],['Radio','Wave'],['Speaker','Wall'],['DJ','Headphones'],['Microphone','TV'],['Television','Picture'],['Camera','Laser'],['Tripod','Leg'],['Fan','Oxygen'],['Heater','Dryer'],['Stove','Coal'],['Fridge','Milk'],['Freezer','Ice cream'],['Microwave','Radar'],['Toaster','Oven'],['Blender','Spatula'],['Pot','Soup'],['Pan','Steel'],['Kettle','Copper'],['Sponge','Jelly'],['Soap','Bar'],['Towel','Shower'],['Toothbrush','Tongue'],['Toothpaste','Gel'],['Comb','Ivory'],['Brush','Rake'],['Razor','Soap'],['Shampoo','Aloe'],['Conditioner','Foam'],['Lotion','Silk'],['Bucket','Earth'],['Mop','Floor'],['Broom','Airplane'],['Dustpan','Cloud'],['Trash can','Truck'],['Recycling','Paper'],['Ladder','Rope']] }
+];
+
+let availablePools = [];
+let poolsCache = {};
+
+let state = {
+ phase: 'setup',
+ numPlayers: 6,
+ numImpostors: 1,
+ gameTime: 180,
+ deliberationTime: 60,
+ playerNames: [],
+ roles: [],
+ civilianWord: '',
+ impostorWord: '',
+ currentReveal: 0,
+ startPlayer: 0,
+ turnDirection: 'horario',
+ revealOrder: [],
+ timerEndAt: null,
+ timerPhase: null,
+ votes: {},
+ votingPlayer: 0,
+ selections: [],
+ executed: [],
+ selectedPools: [], // Now it's an array for multiple pools, will be populated based on language
+ votingPool: null,
+ isTiebreak: false,
+ tiebreakCandidates: []
+};
+
+const saveState = () => localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+const loadState = () => {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return false;
+ try { state = JSON.parse(raw); return true; } catch { return false; }
+};
+const clearState = () => localStorage.removeItem(STORAGE_KEY);
+
+const loadPoolsCache = () => {
+ try { poolsCache = JSON.parse(localStorage.getItem(POOLS_CACHE_KEY) || '{}'); } catch { poolsCache = {}; }
+};
+const savePoolsCache = () => localStorage.setItem(POOLS_CACHE_KEY, JSON.stringify(poolsCache));
+
+// ---------- Default values ----------
+function defaultImpostors(nPlayers) {
+ const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
+ let impostors = 1;
+ if (capped > 7) impostors = 3;
+ else if (capped > 5) impostors = 2;
+ const halfCap = Math.max(1, Math.floor(capped / 2));
+ return Math.min(impostors, halfCap);
+}
+
+function defaultGameTime(nPlayers) {
+ const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
+ if (capped <= 4) return 300;
+ if (capped >= 10) return 900;
+ const extraPlayers = capped - 4;
+ const seconds = 300 + extraPlayers * 100;
+ return Math.round(seconds / 30) * 30;
+}
+
+function defaultDeliberation(gameSeconds) {
+ return Math.max(30, Math.round(gameSeconds / 3));
+}
+
+// ---------- Word Pools ----------
+async function loadPoolsList() {
+ loadPoolsCache();
+
+ // Start with embedded pools (always available)
+ let embeddedList = EMBEDDED_POOLS.map(p => ({
+ id: p.id,
+ name: p.name,
+ emoji: p.emoji,
+ count: p.words.length,
+ lang: p.lang
+ }));
+
+ // Try to load external pools from manifest
+ let externalList = [];
+ try {
+ const res = await fetch(POOLS_MANIFEST_URL);
+ if (res.ok) {
+ const manifest = await res.json();
+ if (Array.isArray(manifest)) {
+ externalList = manifest;
+ }
+ }
+ } catch (e) {
+ console.log('Failed to load manifest:', e);
+ }
+
+ // Combine pools, avoiding duplicates (prefer embedded version over manifest)
+ const embeddedIds = new Set(embeddedList.map(p => p.id));
+ const uniqueExternal = externalList.filter(p => !embeddedIds.has(p.id));
+ const allPools = [...embeddedList, ...uniqueExternal];
+
+ // Filter pools by current language (only show pools matching current language)
+ availablePools = allPools.filter(p => p.lang === currentLanguage);
+
+ // Check if selected pools are valid for current language
+ const validSelectedPools = (state.selectedPools || []).filter(id =>
+ availablePools.some(p => p.id === id)
+ );
+
+ // If no valid pools or pools don't match current language, reset to defaults
+ if (validSelectedPools.length === 0) {
+ const defaultPools = availablePools.slice(0, 2).map(p => p.id);
+ state.selectedPools = defaultPools.length > 0 ? defaultPools : [];
+ saveState();
+ } else {
+ state.selectedPools = validSelectedPools;
+ saveState();
+ }
+
+ renderPoolButtons();
+}
+
+function parseWordsFile(text) {
+ const lines = text.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#'));
+ return lines.map(line => {
+ // Format: civilian_word|impostor_word
+ if (line.includes('|')) {
+ const [civil, impostor] = line.split('|').map(s => s.trim());
+ return [civil, impostor];
+ }
+ // Fallback: if no pipe, use the same word for both
+ return [line, line];
+ });
+}
+
+async function pickWords() {
+ const selectedIds = state.selectedPools && state.selectedPools.length > 0 ? state.selectedPools : ['animales_naturaleza'];
+ let allWords = [];
+
+ // Collect words from all selected pools
+ for (const poolId of selectedIds) {
+ let words = [];
+
+ // Search embedded pools first
+ const embeddedPool = EMBEDDED_POOLS.find(p => p.id === poolId);
+ if (embeddedPool) {
+ words = embeddedPool.words;
+ } else if (poolsCache[poolId]?.words) {
+ words = poolsCache[poolId].words;
+ } else {
+ try {
+ const res = await fetch(`word-pools/${poolId}.txt`);
+ if (res.ok) {
+ const text = await res.text();
+ words = parseWordsFile(text);
+ poolsCache[poolId] = { words, ts: Date.now() };
+ savePoolsCache();
+ }
+ } catch (_) {}
+ }
+
+ allWords = allWords.concat(words);
+ }
+
+ if (allWords.length === 0) {
+ // Fallback to embedded pool
+ allWords = EMBEDDED_POOLS[0].words;
+ }
+
+ const shuffled = [...allWords].sort(() => Math.random() - 0.5);
+ const wordPair = shuffled[0];
+
+ // wordPair is [civilian_word, impostor_word]
+ return { civilian: wordPair[0], impostor: wordPair[1] };
+}
+
+function renderPoolButtons() {
+ const container = document.getElementById('pool-buttons');
+ if (!container) return;
+ container.innerHTML = '';
+
+ // Ensure selectedPools is an array and contains valid pools for current language
+ if (!Array.isArray(state.selectedPools)) {
+ state.selectedPools = [state.selectedPools || 'animales_naturaleza'];
+ }
+
+ // Filter out pools that don't exist in current language
+ const validSelectedPools = state.selectedPools.filter(id =>
+ availablePools.some(p => p.id === id)
+ );
+
+ // If no valid pools selected, select first 2 available
+ if (validSelectedPools.length === 0 && availablePools.length > 0) {
+ state.selectedPools = availablePools.slice(0, 2).map(p => p.id);
+ saveState();
+ } else {
+ state.selectedPools = validSelectedPools;
+ }
+
+ availablePools.forEach(pool => {
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'pool-btn';
+ btn.textContent = `${pool.emoji || 'π²'} ${pool.name || pool.id}`;
+ if (state.selectedPools.includes(pool.id)) btn.classList.add('selected');
+ btn.onclick = () => {
+ // Toggle multiple selection
+ if (state.selectedPools.includes(pool.id)) {
+ state.selectedPools = state.selectedPools.filter(id => id !== pool.id);
+ // Ensure at least one is selected
+ if (state.selectedPools.length === 0) {
+ state.selectedPools = [pool.id];
+ }
+ } else {
+ state.selectedPools.push(pool.id);
+ }
+ saveState();
+ renderPoolButtons();
+ };
+ container.appendChild(btn);
+ });
+}
+
+// ---------- Setup and player names ----------
+function goToPools() {
+ let nPlayers = parseInt(document.getElementById('num-players').value) || MIN_PLAYERS;
+ nPlayers = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
+ const maxImpostors = Math.max(1, Math.floor(nPlayers / 2));
+ let nImpostors = parseInt(document.getElementById('num-impostors').value) || defaultImpostors(nPlayers);
+ nImpostors = Math.min(Math.max(1, nImpostors), maxImpostors);
+ let gTime = parseInt(document.getElementById('game-time').value) || defaultGameTime(nPlayers);
+ gTime = Math.min(Math.max(gTime, 60), 900);
+ let dTime = parseInt(document.getElementById('deliberation-time').value) || defaultDeliberation(gTime);
+ dTime = Math.min(Math.max(dTime, 30), Math.round(900 / 3));
+ if (nImpostors >= nPlayers) { alert(t('impostorsMustBeLess')); return; }
+ state.numPlayers = nPlayers;
+ state.numImpostors = nImpostors;
+ state.gameTime = gTime;
+ state.deliberationTime = dTime;
+ showScreen('pools-screen');
+}
+
+function goToNames() {
+ buildNameInputs();
+ showScreen('names-screen');
+}
+
+function buildNameInputs() {
+ const list = document.getElementById('player-names-list');
+ list.innerHTML = '';
+ for (let i = 0; i < state.numPlayers; i++) {
+ const div = document.createElement('div');
+ div.className = 'player-name-item';
+ const playerLabel = `${t('player')} ${i+1}`;
+ div.innerHTML = `${playerLabel}:`;
+ list.appendChild(div);
+ }
+}
+
+// ---------- Game start ----------
+function startGame() {
+ state.playerNames = [];
+ for (let i = 0; i < state.numPlayers; i++) {
+ const val = document.getElementById(`player-name-${i}`).value.trim();
+ state.playerNames.push(val || `Jugador ${i+1}`);
+ }
+ pickWords().then(({civilian, impostor}) => {
+ state.civilianWord = civilian;
+ state.impostorWord = impostor;
+ finalizeStart();
+ }).catch(() => {
+ const fallback = EMBEDDED_POOLS[0].words;
+ const shuffled = [...fallback].sort(() => Math.random() - 0.5);
+ const wordPair = shuffled[0];
+ state.civilianWord = wordPair[0];
+ state.impostorWord = wordPair[1];
+ finalizeStart();
+ });
+}
+
+function finalizeStart() {
+ state.roles = Array(state.numPlayers - state.numImpostors).fill('CIVIL').concat(Array(state.numImpostors).fill('IMPOSTOR')).sort(() => Math.random()-0.5);
+ state.startPlayer = Math.floor(Math.random() * state.numPlayers);
+ state.turnDirection = Math.random() < 0.5 ? 'horario' : 'antihorario';
+ const step = state.turnDirection === 'horario' ? 1 : -1;
+ state.revealOrder = Array.from({length: state.numPlayers}, (_, k) => (state.startPlayer + step * k + state.numPlayers) % state.numPlayers);
+ state.currentReveal = 0; state.phase = 'pre-reveal'; state.votes = {}; state.votingPlayer = 0; state.selections = []; state.executed = []; state.timerEndAt = null; state.timerPhase = null;
+ state.votingPool = null; state.isTiebreak = false; state.tiebreakCandidates = [];
+ saveState();
+ renderSummary();
+ showScreen('pre-reveal-screen');
+}
+
+// Adjust defaults when player count is edited
+document.getElementById('num-players').addEventListener('change', () => {
+ let nPlayers = parseInt(document.getElementById('num-players').value) || MIN_PLAYERS;
+ nPlayers = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
+ document.getElementById('num-players').value = nPlayers;
+ const imp = defaultImpostors(nPlayers);
+ const gTime = defaultGameTime(nPlayers);
+ const dTime = defaultDeliberation(gTime);
+ document.getElementById('num-impostors').max = Math.max(1, Math.floor(nPlayers / 2));
+ document.getElementById('num-impostors').value = imp;
+ document.getElementById('game-time').value = gTime;
+ document.getElementById('deliberation-time').value = dTime;
+});
+
+function renderSummary() {
+ const el = document.getElementById('config-summary');
+ const fmt = secs => `${Math.floor(secs/60)}:${(secs%60).toString().padStart(2,'0')}`;
+ const startName = state.playerNames[state.startPlayer] || `${t('player')} ${state.startPlayer+1}`;
+
+ // Generate list of selected pools
+ const selectedIds = Array.isArray(state.selectedPools) ? state.selectedPools : [state.selectedPools || 'animales_naturaleza'];
+ const poolsText = selectedIds.map(id => {
+ const pool = availablePools.find(p => p.id === id) || EMBEDDED_POOLS.find(p => p.id === id);
+ return pool ? `${pool.emoji || 'π²'} ${pool.name || pool.id}` : id;
+ }).join(', ');
+
+ el.innerHTML = `
+ ${t('players')}: ${state.numPlayers}
+ ${t('impostors')}: ${state.numImpostors}
+ ${t('gameTime')}: ${fmt(state.gameTime)}
+ ${t('deliberationTime')}: ${fmt(state.deliberationTime)}
+ ${t('poolsLabel')}: ${poolsText}
+ ${t('starts')}: ${startName} Β· ${t('order')}: ${state.turnDirection === 'horario' ? t('clockwise') : t('counterclockwise')}
+ `;
+}
+
+// ---------- Role revelation ----------
+function loadCurrentReveal() {
+ state.phase = 'reveal'; saveState();
+
+ // Activar Wake Lock para mantener pantalla encendida durante el juego
+ requestWakeLock();
+
+ if (!state.revealOrder || state.revealOrder.length !== state.numPlayers) {
+ const step = state.turnDirection === 'horario' ? 1 : -1;
+ state.revealOrder = Array.from({length: state.numPlayers}, (_, k) => (state.startPlayer + step * k + state.numPlayers) % state.numPlayers);
+ }
+ const idx = state.revealOrder[state.currentReveal];
+ const name = state.playerNames[idx];
+ document.getElementById('current-player-name').textContent = name;
+
+ // Update curtain text
+ const revealText = document.querySelector('#reveal-screen .info-text');
+ if (revealText) {
+ revealText.innerHTML = `${t('turnOf')}: ${name}
${t('othersLookAway')}`;
+ }
+
+ const curtainText = document.querySelector('.curtain-cover div:last-child');
+ if (curtainText) {
+ curtainText.textContent = t('liftCurtain');
+ }
+
+ // Reset curtain state
+ curtainState.isRevealed = false;
+ const coverEl = document.getElementById('curtain-cover');
+ coverEl.style.transform = 'translateY(0)';
+ coverEl.style.transition = '';
+
+ document.getElementById('next-player-btn').style.display = 'none';
+ document.getElementById('start-game-btn').style.display = 'none';
+}
+
+function liftCurtain() {
+ const cover = document.getElementById('curtain-cover');
+ if (cover.classList.contains('lifted')) return;
+
+ // Restore CSS transition and use the class
+ cover.style.transition = '';
+ cover.style.transform = '';
+ cover.classList.add('lifted');
+
+ const idx = state.revealOrder[state.currentReveal];
+ const role = state.roles[idx];
+ const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord;
+ document.getElementById('role-text').textContent = role;
+ document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor');
+ document.getElementById('word-text').textContent = word;
+ setTimeout(() => {
+ if (state.currentReveal + 1 < state.numPlayers) document.getElementById('next-player-btn').style.display = 'block';
+ else document.getElementById('start-game-btn').style.display = 'block';
+ }, 700);
+}
+
+function nextReveal() { state.currentReveal++; saveState(); loadCurrentReveal(); }
+
+// Curtain system with GRAVITY - The curtain always tends to fall
+// Supports both touch (mobile) and mouse (desktop)
+// On desktop: curtain stays up while mouse button is held, even if cursor leaves the area
+let curtainState = { isRevealed: false };
+let curtainDragState = {
+ startY: null,
+ isDragging: false,
+ currentTranslateY: 0
+};
+
+function initCurtainHandlers() {
+ const curtain = document.getElementById('curtain');
+ if (!curtain) return;
+
+ // Function to get Y position from event (touch or mouse)
+ const getY = (e) => {
+ return e.touches ? e.touches[0].clientY : e.clientY;
+ };
+
+ // Start function (touch and mouse)
+ const handleStart = (e) => {
+ curtainDragState.startY = getY(e);
+ curtainDragState.isDragging = true;
+ curtainDragState.currentTranslateY = 0;
+ if (e.type === 'mousedown') {
+ e.preventDefault(); // Prevent text selection on desktop
+ }
+ };
+
+ // Move function (touch and mouse)
+ const handleMove = (e) => {
+ if (curtainDragState.startY === null || !curtainDragState.isDragging) return;
+ const currentY = getY(e);
+ const dy = currentY - curtainDragState.startY;
+ const coverEl = document.getElementById('curtain-cover');
+ if (!coverEl) return;
+
+ // Calculate displacement: negative = up, positive = down
+ // Allow going further up than the curtain height (user can keep dragging up)
+ // but don't allow going below initial position (0)
+ curtainDragState.currentTranslateY = Math.min(dy, 0);
+
+ coverEl.style.transform = `translateY(${curtainDragState.currentTranslateY}px)`;
+ coverEl.style.transition = 'none';
+
+ // If lifted enough, show content
+ if (curtainDragState.currentTranslateY < -80 && !curtainState.isRevealed) {
+ curtainState.isRevealed = true;
+ const idx = state.revealOrder[state.currentReveal];
+ const role = state.roles[idx];
+ const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord;
+ const roleText = t(role.toLowerCase());
+ document.getElementById('role-text').textContent = roleText;
+ document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor');
+ document.getElementById('word-text').textContent = word;
+ }
+
+ e.preventDefault(); // Prevent selection
+ };
+
+ // End function (touch and mouse)
+ const handleEnd = (e) => {
+ if (!curtainDragState.isDragging || curtainDragState.startY === null) return;
+ const coverEl = document.getElementById('curtain-cover');
+ if (!coverEl) return;
+
+ // ALWAYS bring the curtain down when released (GRAVITY)
+ coverEl.style.transition = 'transform 0.4s ease';
+ coverEl.style.transform = 'translateY(0)';
+
+ // If content was revealed, show button after it falls
+ if (curtainState.isRevealed) {
+ setTimeout(() => {
+ if (state.currentReveal + 1 < state.numPlayers) {
+ document.getElementById('next-player-btn').style.display = 'block';
+ } else {
+ document.getElementById('start-game-btn').style.display = 'block';
+ }
+ }, 400);
+ }
+
+ curtainDragState.startY = null;
+ curtainDragState.isDragging = false;
+ curtainDragState.currentTranslateY = 0;
+ };
+
+ // Touch events (mobile)
+ curtain.addEventListener('touchstart', handleStart, {passive: false});
+ curtain.addEventListener('touchmove', handleMove, {passive: false});
+ curtain.addEventListener('touchend', handleEnd, {passive: true});
+ curtain.addEventListener('touchcancel', handleEnd, {passive: true});
+
+ // Mouse events (desktop) - start on curtain only
+ curtain.addEventListener('mousedown', handleStart);
+
+ // Mouse move and up events on WINDOW so we can track even when cursor leaves everything
+ window.addEventListener('mousemove', handleMove);
+ window.addEventListener('mouseup', handleEnd);
+}
+
+// Initialize curtain handlers when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initCurtainHandlers);
+} else {
+ initCurtainHandlers();
+}
+
+// ---------- Screen Wake Lock (prevent screen from sleeping during timers) ----------
+let wakeLock = null;
+let wakeLockVideo = null; // For iOS workaround
+
+// Detect if device is iOS
+function isIOS() {
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
+}
+
+// Check if screen lock is enabled in settings
+function isScreenLockEnabled() {
+ const saved = localStorage.getItem(SCREEN_LOCK_STORAGE_KEY);
+ return saved === null ? true : saved === 'true'; // Default enabled
+}
+
+// Save screen lock preference
+function setScreenLockEnabled(enabled) {
+ localStorage.setItem(SCREEN_LOCK_STORAGE_KEY, enabled.toString());
+ updateScreenLockButton();
+}
+
+async function requestWakeLock() {
+ if (!isScreenLockEnabled()) return;
+
+ // Try native Wake Lock API first (works on Android Chrome, etc.)
+ if ('wakeLock' in navigator) {
+ try {
+ wakeLock = await navigator.wakeLock.request('screen');
+ wakeLock.addEventListener('release', () => {
+ wakeLock = null;
+ });
+ console.log('Wake Lock activated (native API)');
+ return;
+ } catch (err) {
+ console.log('Wake lock request failed:', err);
+ }
+ }
+
+ // Fallback for iOS - use hidden video loop
+ if (isIOS() && !wakeLockVideo) {
+ try {
+ wakeLockVideo = document.createElement('video');
+ wakeLockVideo.setAttribute('playsinline', '');
+ wakeLockVideo.setAttribute('muted', '');
+ wakeLockVideo.style.position = 'fixed';
+ wakeLockVideo.style.opacity = '0';
+ wakeLockVideo.style.pointerEvents = 'none';
+ wakeLockVideo.style.width = '1px';
+ wakeLockVideo.style.height = '1px';
+
+ // Minimal base64 encoded video (1 frame, silent)
+ wakeLockVideo.src = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAu1tZGF0AAACrQYF//+p3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE1NSByMjkwMSA3ZDBmZjIyIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxOCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTMgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAwWWIhAAz//727L4FNf2f0JcRLMXaSnA+KqSAgHc0wAAAAwAAAwAAJuKiZ0WFMeJsgAAAHGAFBCwCPCVC';
+ wakeLockVideo.loop = true;
+
+ document.body.appendChild(wakeLockVideo);
+ await wakeLockVideo.play();
+ console.log('Wake Lock activated (iOS video workaround)');
+ } catch (err) {
+ console.log('iOS wake lock workaround failed:', err);
+ }
+ }
+}
+
+function releaseWakeLock() {
+ // Release native Wake Lock
+ if (wakeLock) {
+ wakeLock.release();
+ wakeLock = null;
+ }
+
+ // Stop iOS video workaround
+ if (wakeLockVideo) {
+ wakeLockVideo.pause();
+ wakeLockVideo.remove();
+ wakeLockVideo = null;
+ }
+}
+
+// Re-request wake lock when page becomes visible again
+document.addEventListener('visibilitychange', async () => {
+ if (document.visibilityState === 'visible' && (wakeLock !== null || wakeLockVideo !== null)) {
+ await requestWakeLock();
+ }
+});
+
+// ---------- Timers ----------
+let timerInterval = null;
+async function startPhaseTimer(phase, seconds, elementId, onEnd) {
+ if (timerInterval) clearInterval(timerInterval);
+
+ // Request wake lock to keep screen on during timer
+ await requestWakeLock();
+
+ const now = Date.now();
+ state.timerPhase = phase;
+ state.timerEndAt = now + seconds*1000;
+ saveState();
+ const el = document.getElementById(elementId);
+ const tick = () => {
+ const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000));
+ updateTimerDisplay(el, remaining);
+ if (remaining <= 0) {
+ clearInterval(timerInterval);
+ releaseWakeLock(); // Release wake lock when timer ends
+ playBeep();
+ onEnd();
+ }
+ };
+ tick();
+ timerInterval = setInterval(tick, 1000);
+}
+
+function resumeTimerIfNeeded() {
+ if (!state.timerEndAt || !state.timerPhase) return;
+ const remaining = Math.round((state.timerEndAt - Date.now())/1000);
+ if (remaining <= 0) { state.timerEndAt = null; saveState(); return; }
+ if (state.timerPhase === 'game') { showScreen('game-screen'); startPhaseTimer('game', remaining, 'game-timer', startDeliberationPhase); }
+ else if (state.timerPhase === 'deliberation') { showScreen('deliberation-screen'); startPhaseTimer('deliberation', remaining, 'deliberation-timer', startVotingPhase); }
+}
+
+function updateTimerDisplay(el, remaining) {
+ const minutes = Math.floor(remaining/60); const secs = remaining%60;
+ el.textContent = `${minutes}:${secs.toString().padStart(2,'0')}`;
+ el.className = 'timer';
+ if (remaining <= 10) el.classList.add('danger'); else if (remaining <= 30) el.classList.add('warning');
+}
+
+function playBeep() {
+ // Play alarm sound - 3 ascending beeps pattern repeated twice
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const now = ctx.currentTime;
+
+ // Frequencies for alarm pattern (ascending)
+ const frequencies = [523, 659, 784]; // C5, E5, G5
+ const beepDuration = 0.15;
+ const gapDuration = 0.08;
+ const patternGap = 0.3;
+
+ let time = now;
+
+ // Play pattern twice
+ for (let pattern = 0; pattern < 2; pattern++) {
+ for (let i = 0; i < frequencies.length; i++) {
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+
+ osc.connect(gain);
+ gain.connect(ctx.destination);
+ osc.frequency.value = frequencies[i];
+ osc.type = 'square'; // More alarm-like sound
+
+ gain.gain.setValueAtTime(0, time);
+ gain.gain.linearRampToValueAtTime(0.25, time + 0.02);
+ gain.gain.setValueAtTime(0.25, time + beepDuration - 0.02);
+ gain.gain.linearRampToValueAtTime(0, time + beepDuration);
+
+ osc.start(time);
+ osc.stop(time + beepDuration);
+
+ time += beepDuration + gapDuration;
+ }
+ time += patternGap;
+ }
+}
+
+// ---------- Game phases ----------
+function startGamePhase() { state.phase = 'game'; saveState(); showScreen('game-screen'); startPhaseTimer('game', state.gameTime, 'game-timer', startDeliberationPhase); }
+function startDeliberationPhase() { state.phase = 'deliberation'; saveState(); showScreen('deliberation-screen'); startPhaseTimer('deliberation', state.deliberationTime, 'deliberation-timer', startVotingPhase); }
+function startVotingPhase(candidates = null, isTiebreak = false) {
+ releaseWakeLock(); // Release wake lock when voting starts (no timer)
+ state.phase = 'voting';
+ state.votingPlayer = 0;
+ state.votes = {};
+ state.selections = [];
+ state.votingPool = candidates;
+ state.isTiebreak = isTiebreak;
+ saveState();
+ renderVoting();
+ showScreen('voting-screen');
+}
+function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startDeliberationPhase(); }
+function skipToVoting() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startVotingPhase(); }
+function startTiebreakDeliberation(candidates) {
+ state.phase = 'deliberation';
+ state.tiebreakCandidates = candidates;
+ saveState();
+ showScreen('deliberation-screen');
+ startPhaseTimer('deliberation', 60, 'deliberation-timer', () => startVotingPhase(candidates, true));
+}
+
+// ---------- Secret voting ----------
+function renderVoting() {
+ const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i);
+ const voter = state.playerNames[state.votingPlayer];
+ document.getElementById('voter-name').textContent = voter;
+ document.getElementById('votes-needed').textContent = state.numImpostors;
+
+ // Update voting instruction text
+ const votingInfo = document.querySelector('#voting-screen .info-text');
+ if (votingInfo) {
+ votingInfo.innerHTML = `${t('passMobileTo')} ${voter}. ${t('chooseSuspects')} ${state.numImpostors} ${t('suspect')}.`;
+ }
+
+ state.selections = state.selections || [];
+ const list = document.getElementById('vote-list'); list.innerHTML = '';
+ pool.forEach(i => {
+ const item = document.createElement('div');
+ item.className = 'player-item';
+
+ // Marcar como disabled ANTES de aΓ±adir al DOM para que la animaciΓ³n correcta se aplique
+ if (i === state.votingPlayer) {
+ item.classList.add('disabled');
+ // NO aplicar opacity inline - dejamos que CSS lo maneje con la animaciΓ³n
+ item.style.pointerEvents = 'none';
+ }
+
+ item.textContent = state.playerNames[i];
+ if (state.votes[i]) item.innerHTML += `${t('votes')}: ${state.votes[i]}`;
+ if (state.selections.includes(i)) item.classList.add('selected');
+
+ if (i !== state.votingPlayer) {
+ item.onclick = () => toggleSelection(i, item);
+ }
+
+ list.appendChild(item);
+ });
+ updateConfirmButton();
+}
+
+function toggleSelection(idx, el) {
+ if (idx === state.votingPlayer) return;
+ if (state.selections.includes(idx)) state.selections = state.selections.filter(x => x !== idx);
+ else {
+ if (state.selections.length >= state.numImpostors) return;
+ state.selections.push(idx);
+ }
+ saveState();
+ renderVoting();
+}
+
+function updateConfirmButton() {
+ const btn = document.getElementById('confirm-vote-btn');
+ btn.disabled = state.selections.length !== state.numImpostors;
+}
+
+function confirmCurrentVote() {
+ state.selections.forEach(t => { state.votes[t] = (state.votes[t] || 0) + 1; });
+ state.votingPlayer++;
+ state.selections = [];
+ saveState();
+ if (state.votingPlayer >= state.numPlayers) { handleVoteOutcome(); return; }
+ renderVoting();
+}
+
+// ---------- Vote resolution ----------
+function handleVoteOutcome() {
+ const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i);
+ const counts = pool.map(idx => ({ idx, votes: state.votes[idx] || 0 }));
+ counts.sort((a, b) => b.votes - a.votes);
+
+ let slots = state.numImpostors;
+ const executed = [];
+ for (let i = 0; i < counts.length && slots > 0; ) {
+ const currentVotes = counts[i].votes;
+ const group = [];
+ let j = i;
+ while (j < counts.length && counts[j].votes === currentVotes) { group.push(counts[j].idx); j++; }
+ if (group.length <= slots) {
+ executed.push(...group);
+ slots -= group.length;
+ i = j;
+ } else {
+ // Tie for remaining slots
+ if (state.isTiebreak) {
+ // Second tie: impostors win
+ state.executed = [];
+ showResults(true);
+ return;
+ }
+ startTiebreakDeliberation(group);
+ return;
+ }
+ }
+
+ state.executed = executed;
+ showResults();
+}
+
+// ---------- Results ----------
+function showResults(isTiebreak = false) {
+ state.phase = 'results'; saveState();
+
+ // Liberar Wake Lock cuando termina la partida
+ releaseWakeLock();
+
+ const executed = state.executed || [];
+ let impostorsAlive = 0;
+ state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; });
+ const winner = impostorsAlive > 0 ? 'IMPOSTORES' : 'CIVILES';
+ const results = document.getElementById('results-content');
+ const winText = winner === 'CIVILES' ? `β
${t('civiliansWin')}` : `β ${t('impostorsWin')}`;
+ results.innerHTML = `
+ ${winText}
+ ${t('executed')}: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : t('nobody')}
+ ${t('votes')}: ${Object.keys(state.votes).length ? '' : t('noVotes')}
+ ${t('revealedRoles')}
+ ${state.roles.map((role,i) => {
+ const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord;
+ const killed = executed.includes(i) ? 'executed' : '';
+ const roleText = t(role.toLowerCase());
+ return `${state.playerNames[i]}: ${roleText} β "${word}" ${killed ? 'β οΈ' : ''}
`;
+ }).join('')}
+ `;
+ showScreen('results-screen');
+}
+
+// ---------- Utilities ----------
+function showScreen(id) {
+ document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
+ document.getElementById(id).classList.add('active');
+ state.phase = id.replace('-screen','');
+ saveState();
+ updateExitButtonVisibility();
+}
+
+function newMatch() {
+ clearState();
+ releaseWakeLock(); // Make sure wake lock is released when exiting game
+ if (timerInterval) clearInterval(timerInterval);
+ state = { ...state, phase:'welcome', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] };
+ saveState();
+ showScreen('welcome-screen');
+}
+
+function confirmExitGame() {
+ const confirmMessage = currentLanguage === 'es'
+ ? 'ΒΏEstΓ‘s seguro de que quieres salir de la partida? Se perderΓ‘ todo el progreso actual.'
+ : 'Are you sure you want to exit the game? All current progress will be lost.';
+
+ if (confirm(confirmMessage)) {
+ newMatch();
+ }
+}
+
+function updateExitButtonVisibility() {
+ const exitBtn = document.getElementById('exit-game');
+ const langBtn = document.getElementById('language-toggle');
+ const screenLockBtn = document.getElementById('screen-lock-toggle');
+
+ // Show exit button and hide language/screen-lock toggles in all phases except welcome and setup
+ if (state.phase !== 'welcome' && state.phase !== 'setup') {
+ exitBtn.classList.add('visible');
+ if (langBtn) langBtn.style.display = 'none';
+ if (screenLockBtn) screenLockBtn.classList.remove('visible');
+ } else {
+ exitBtn.classList.remove('visible');
+ if (langBtn) langBtn.style.display = 'inline-flex';
+ // Only show screen lock button on iOS
+ if (screenLockBtn && isIOS()) {
+ screenLockBtn.classList.add('visible');
+ }
+ }
+}
+
+// ---------- Theme system ----------
+function getSystemTheme() {
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+}
+
+function loadTheme() {
+ const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
+ return savedTheme || getSystemTheme();
+}
+
+function saveTheme(theme) {
+ localStorage.setItem(THEME_STORAGE_KEY, theme);
+}
+
+function applyTheme(theme) {
+ document.documentElement.setAttribute('data-theme', theme);
+ const themeIcon = document.querySelector('.theme-icon');
+ if (themeIcon) {
+ themeIcon.textContent = theme === 'dark' ? 'βοΈ' : 'π';
+ }
+}
+
+function toggleTheme() {
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
+ applyTheme(newTheme);
+ saveTheme(newTheme);
+}
+
+// Initialize theme
+const initialTheme = loadTheme();
+applyTheme(initialTheme);
+
+// ---------- Screen Lock Button ----------
+function updateScreenLockButton() {
+ const btn = document.getElementById('screen-lock-toggle');
+ if (!btn) return;
+
+ const enabled = isScreenLockEnabled();
+ const icon = btn.querySelector('.screen-lock-icon');
+
+ if (enabled) {
+ btn.classList.add('active');
+ btn.setAttribute('title', 'Bloqueo de pantalla activado');
+ if (icon) icon.textContent = 'π';
+ } else {
+ btn.classList.remove('active');
+ btn.setAttribute('title', 'Bloqueo de pantalla desactivado');
+ if (icon) icon.textContent = 'π';
+ }
+}
+
+function toggleScreenLock() {
+ const currentState = isScreenLockEnabled();
+ setScreenLockEnabled(!currentState);
+
+ // If disabling, release any active wake lock
+ if (currentState) {
+ releaseWakeLock();
+ }
+}
+
+// Event listener for theme and language buttons
+document.addEventListener('DOMContentLoaded', () => {
+ const themeToggle = document.getElementById('theme-toggle');
+ if (themeToggle) {
+ themeToggle.addEventListener('click', toggleTheme);
+ }
+
+ const languageToggle = document.getElementById('language-toggle');
+ if (languageToggle) {
+ languageToggle.addEventListener('click', toggleLanguage);
+ }
+
+ const screenLockToggle = document.getElementById('screen-lock-toggle');
+ if (screenLockToggle) {
+ screenLockToggle.addEventListener('click', toggleScreenLock);
+ updateScreenLockButton();
+ }
+
+ // Initialize language
+ currentLanguage = loadLanguage();
+ setLanguage(currentLanguage);
+
+ // Detect system theme changes
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
+ // Only apply automatically if user hasn't manually selected a theme
+ if (!localStorage.getItem(THEME_STORAGE_KEY)) {
+ applyTheme(e.matches ? 'dark' : 'light');
+ }
+ });
+});
+
+// ---------- State rehydration ----------
+(function init() {
+ const restored = loadState();
+ loadPoolsList();
+ if (!state.turnDirection) state.turnDirection = 'horario';
+ if (typeof state.startPlayer !== 'number') state.startPlayer = 0;
+
+ // Set default values in inputs if we're in setup
+ if (state.phase === 'setup' || !restored) {
+ const defaultPlayers = 6;
+ const defaultImp = defaultImpostors(defaultPlayers);
+ const defaultGTime = defaultGameTime(defaultPlayers);
+ const defaultDTime = defaultDeliberation(defaultGTime);
+
+ document.getElementById('num-players').value = defaultPlayers;
+ document.getElementById('num-impostors').value = defaultImp;
+ document.getElementById('num-impostors').max = Math.max(1, Math.floor(defaultPlayers / 2));
+ document.getElementById('game-time').value = defaultGTime;
+ document.getElementById('deliberation-time').value = defaultDTime;
+ }
+
+ // Determine initial screen
+ if (!restored || state.phase === 'setup' || state.phase === 'welcome') {
+ // If no saved state or we're in setup/welcome, show welcome
+ showScreen('welcome-screen');
+ } else {
+ // If there's a game in progress, restore it
+ switch (state.phase) {
+ case 'names': buildNameInputs(); showScreen('names-screen'); break;
+ case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break;
+ case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break;
+ case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break;
+ case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break;
+ case 'voting': showScreen('voting-screen'); renderVoting(); break;
+ case 'results': showResults(); break;
+ default: showScreen('welcome-screen');
+ }
+ }
+
+ // Initialize exit button visibility
+ updateExitButtonVisibility();
+
+ // Initialize screen lock button for iOS
+ initScreenLockButton();
+})();
diff --git a/styles.1a37b506.css b/styles.1a37b506.css
new file mode 100644
index 0000000..8e4ebaf
--- /dev/null
+++ b/styles.1a37b506.css
@@ -0,0 +1,1829 @@
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ EXPEDIENTE CLASIFICADO - IMPOSTOR GAME
+ Noir Cyberpunk Interrogation Aesthetic
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Special+Elite&display=swap');
+
+:root {
+ /* LIGHT THEME: Interrogation Room */
+ --bg-primary: #dcd9d2;
+ --bg-secondary: #c8c3b8;
+ --bg-overlay: rgba(0, 0, 0, 0.05);
+
+ --surface-glass: rgba(255, 255, 255, 0.85);
+ --surface-card: rgba(255, 255, 255, 0.95);
+ --surface-hover: rgba(255, 255, 255, 1);
+
+ --text-primary: #0a0a0a;
+ --text-secondary: #2a2a2a;
+ --text-tertiary: #5a5a5a;
+ --text-inverted: #ffffff;
+
+ --accent-warning: #e6a73c;
+ --accent-danger: #d93626;
+ --accent-success: #2d8b3d;
+ --accent-info: #2e4e7a;
+
+ --border-light: rgba(0, 0, 0, 0.18);
+ --border-medium: rgba(0, 0, 0, 0.35);
+ --border-heavy: rgba(0, 0, 0, 0.55);
+
+ --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.15);
+ --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.22);
+ --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.28);
+ --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.25);
+
+ --grain-opacity: 0.05;
+ --scanline-opacity: 0.025;
+
+ /* Spotlight effect */
+ --spotlight-color: rgba(255, 235, 180, 0.08);
+ --vignette-intensity: 0.4;
+}
+
+[data-theme="dark"] {
+ /* DARK THEME: Night Investigation */
+ --bg-primary: #050505;
+ --bg-secondary: #0f0f0f;
+ --bg-overlay: rgba(255, 255, 255, 0.03);
+
+ --surface-glass: rgba(25, 25, 25, 0.9);
+ --surface-card: rgba(35, 35, 35, 0.95);
+ --surface-hover: rgba(45, 45, 45, 1);
+
+ --text-primary: #f5f5f5;
+ --text-secondary: #d0d0d0;
+ --text-tertiary: #909090;
+ --text-inverted: #0a0a0a;
+
+ --accent-warning: #ffb84d;
+ --accent-danger: #ff3d2e;
+ --accent-success: #3dd46b;
+ --accent-info: #4d8ce0;
+
+ --border-light: rgba(255, 255, 255, 0.12);
+ --border-medium: rgba(255, 255, 255, 0.22);
+ --border-heavy: rgba(255, 255, 255, 0.35);
+
+ --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.6);
+ --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.8);
+ --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.95);
+ --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.7);
+
+ --grain-opacity: 0.07;
+ --scanline-opacity: 0.035;
+
+ /* Spotlight effect */
+ --spotlight-color: rgba(255, 200, 100, 0.04);
+ --vignette-intensity: 0.6;
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ BASE STYLES & TYPOGRAPHY
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+}
+
+body {
+ font-family: 'JetBrains Mono', 'Courier Prime', 'Courier New', monospace;
+ background:
+ radial-gradient(ellipse 80% 50% at 50% 20%, var(--spotlight-color) 0%, transparent 50%),
+ radial-gradient(circle at 20% 30%, rgba(230, 167, 60, 0.08) 0%, transparent 40%),
+ radial-gradient(circle at 80% 70%, rgba(217, 54, 38, 0.06) 0%, transparent 40%),
+ var(--bg-primary);
+ min-height: 100vh;
+ min-height: 100dvh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 70px 16px 16px;
+ color: var(--text-primary);
+ position: relative;
+ overflow: hidden;
+ font-size: 14px;
+ letter-spacing: 0px;
+ transition: background 0.5s ease, color 0.3s ease;
+}
+
+/* Film grain texture overlay */
+body::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.5'/%3E%3C/svg%3E");
+ opacity: var(--grain-opacity);
+ pointer-events: none;
+ z-index: 9999;
+ mix-blend-mode: overlay;
+ animation: grain 8s steps(10) infinite;
+}
+
+@keyframes grain {
+ 0%, 100% { transform: translate(0, 0); }
+ 10% { transform: translate(-5%, -10%); }
+ 20% { transform: translate(-15%, 5%); }
+ 30% { transform: translate(7%, -25%); }
+ 40% { transform: translate(-5%, 25%); }
+ 50% { transform: translate(-15%, 10%); }
+ 60% { transform: translate(15%, 0%); }
+ 70% { transform: translate(0%, 15%); }
+ 80% { transform: translate(3%, 35%); }
+ 90% { transform: translate(-10%, 10%); }
+}
+
+/* Scanlines */
+body::after {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: repeating-linear-gradient(
+ 0deg,
+ transparent,
+ transparent 2px,
+ rgba(0, 0, 0, 0.1) 2px,
+ rgba(0, 0, 0, 0.1) 4px
+ );
+ opacity: var(--scanline-opacity);
+ pointer-events: none;
+ z-index: 9998;
+}
+
+/* Dramatic vignette overlay */
+.vignette-overlay {
+ position: fixed;
+ inset: 0;
+ background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,var(--vignette-intensity)) 100%);
+ pointer-events: none;
+ z-index: 9997;
+}
+
+/* VHS interference effect */
+@keyframes vhsInterference {
+ 0%, 100% { opacity: 0; }
+ 5% { opacity: 0.03; transform: translateX(-2px); }
+ 10% { opacity: 0; }
+ 15% { opacity: 0.02; transform: translateX(1px); }
+ 20% { opacity: 0; }
+}
+
+.vhs-line {
+ position: fixed;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
+ pointer-events: none;
+ z-index: 9996;
+ animation: vhsScan 8s linear infinite;
+ opacity: 0.04;
+}
+
+@keyframes vhsScan {
+ 0% { top: -10px; }
+ 100% { top: 110%; }
+}
+
+h1 {
+ font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif;
+ text-align: center;
+ margin-bottom: 20px;
+ font-size: 2.6em;
+ font-weight: 400;
+ letter-spacing: 4px;
+ text-transform: uppercase;
+ position: relative;
+ text-shadow: 3px 3px 0px var(--bg-secondary), 0 0 30px rgba(230, 167, 60, 0.2);
+ line-height: 1.1;
+ animation: titleReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+@keyframes titleReveal {
+ from {
+ opacity: 0;
+ letter-spacing: 20px;
+ filter: blur(8px);
+ }
+ to {
+ opacity: 1;
+ letter-spacing: 4px;
+ filter: blur(0);
+ }
+}
+
+h1::after {
+ content: '';
+ display: block;
+ width: 80px;
+ height: 4px;
+ background: linear-gradient(90deg, var(--accent-danger) 0%, var(--accent-warning) 50%, var(--accent-danger) 100%);
+ background-size: 200% 100%;
+ margin: 14px auto 0;
+ box-shadow: 0 0 15px rgba(230, 167, 60, 0.5);
+ animation: shimmer 3s ease-in-out infinite;
+}
+
+@keyframes shimmer {
+ 0%, 100% { background-position: 0% 50%; }
+ 50% { background-position: 100% 50%; }
+}
+
+h2 {
+ font-family: 'Crimson Text', Georgia, serif;
+ text-align: center;
+ margin: 16px 0;
+ font-size: 1.4em;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+}
+
+h3 {
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 1em;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 1.5px;
+ margin-bottom: 12px;
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ CONTAINER & SCREENS
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.container {
+ width: 100%;
+ max-width: 480px;
+ background: var(--surface-glass);
+ backdrop-filter: blur(20px) saturate(150%);
+ border-radius: 0;
+ padding: 28px 22px;
+ box-shadow: var(--shadow-harsh), var(--shadow-lg);
+ border: 4px solid var(--border-heavy);
+ display: flex;
+ flex-direction: column;
+ transition: all 0.4s ease;
+ margin-bottom: 20px;
+ position: relative;
+ overflow: hidden;
+ clip-path: polygon(
+ 0 20px,
+ 20px 0,
+ 100% 0,
+ 100% calc(100% - 20px),
+ calc(100% - 20px) 100%,
+ 0 100%
+ );
+}
+
+.container::before {
+ content: 'β¬’ CLASSIFIED β¬’';
+ position: absolute;
+ top: 8px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 0.65em;
+ letter-spacing: 3px;
+ opacity: 0.4;
+ font-weight: 800;
+ color: var(--accent-danger);
+ text-shadow: 0 0 10px rgba(217, 54, 38, 0.3);
+ animation: classifiedPulse 4s ease-in-out infinite;
+}
+
+@keyframes classifiedPulse {
+ 0%, 100% { opacity: 0.4; text-shadow: 0 0 10px rgba(217, 54, 38, 0.3); }
+ 50% { opacity: 0.6; text-shadow: 0 0 20px rgba(217, 54, 38, 0.6); }
+}
+
+/* Diagonal classified stamp */
+.container::after {
+ content: 'EXPEDIENTE';
+ position: absolute;
+ bottom: 15px;
+ right: -30px;
+ font-family: 'Special Elite', 'Courier Prime', monospace;
+ font-size: 0.7em;
+ letter-spacing: 3px;
+ color: var(--accent-danger);
+ opacity: 0.12;
+ transform: rotate(-45deg);
+ font-weight: 400;
+ white-space: nowrap;
+ pointer-events: none;
+}
+
+.screen {
+ display: none;
+ animation: screenEnter 0.35s cubic-bezier(0.22, 1, 0.36, 1);
+ flex: 1;
+ overflow: hidden;
+ min-height: 0;
+}
+
+.screen.active {
+ display: flex;
+ flex-direction: column;
+}
+
+@keyframes screenEnter {
+ 0% {
+ opacity: 0;
+ transform: translateY(30px) scale(0.95);
+ filter: blur(4px);
+ }
+ 60% {
+ opacity: 1;
+ filter: blur(0);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ filter: blur(0);
+ }
+}
+
+/* Staggered children animation */
+.screen.active > * {
+ animation: fadeSlideUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
+}
+
+.screen.active > *:nth-child(1) { animation-delay: 0.05s; }
+.screen.active > *:nth-child(2) { animation-delay: 0.1s; }
+.screen.active > *:nth-child(3) { animation-delay: 0.15s; }
+.screen.active > *:nth-child(4) { animation-delay: 0.2s; }
+.screen.active > *:nth-child(5) { animation-delay: 0.25s; }
+.screen.active > *:nth-child(6) { animation-delay: 0.3s; }
+.screen.active > *:nth-child(7) { animation-delay: 0.35s; }
+.screen.active > *:nth-child(8) { animation-delay: 0.4s; }
+
+@keyframes fadeSlideUp {
+ from {
+ opacity: 0;
+ transform: translateY(15px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ FORM CONTROLS
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.form-group {
+ margin-bottom: 16px;
+}
+
+.form-group.compact {
+ margin-bottom: 12px;
+}
+
+label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 700;
+ font-size: 0.8em;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 1.2px;
+}
+
+input {
+ width: 100%;
+ padding: 12px 14px;
+ border: 2px solid var(--border-medium);
+ border-radius: 0;
+ font-size: 0.95em;
+ font-family: 'JetBrains Mono', monospace;
+ background: var(--surface-card);
+ color: var(--text-primary);
+ transition: all 0.2s ease;
+ box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+input:focus {
+ outline: none;
+ border-color: var(--accent-warning);
+ box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(212, 165, 116, 0.2);
+ transform: translateY(-1px);
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ BUTTONS
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+button {
+ width: 100%;
+ padding: 16px 20px;
+ border: 3px solid var(--text-primary);
+ border-radius: 0;
+ font-size: 0.9em;
+ font-weight: 800;
+ font-family: 'JetBrains Mono', monospace;
+ cursor: pointer;
+ background: var(--text-primary);
+ color: var(--text-inverted);
+ box-shadow: var(--shadow-harsh);
+ transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
+ margin-top: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ position: relative;
+ overflow: hidden;
+ clip-path: polygon(
+ 0 0,
+ calc(100% - 12px) 0,
+ 100% 12px,
+ 100% 100%,
+ 12px 100%,
+ 0 calc(100% - 12px)
+ );
+}
+
+button::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 0;
+ height: 0;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.2);
+ transform: translate(-50%, -50%);
+ transition: width 0.5s ease, height 0.5s ease;
+}
+
+button:hover::before {
+ width: 300px;
+ height: 300px;
+}
+
+button:hover {
+ box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.3);
+ filter: brightness(1.1);
+}
+
+button:active {
+ box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15);
+ filter: brightness(0.95);
+}
+
+button.secondary {
+ background: linear-gradient(135deg, var(--accent-warning) 0%, #c48a2e 100%);
+ border-color: var(--accent-warning);
+ color: var(--text-inverted);
+ box-shadow: var(--shadow-harsh), 0 0 15px rgba(230, 167, 60, 0.25);
+}
+
+button.ghost {
+ background: transparent;
+ color: var(--text-primary);
+ border-color: var(--border-medium);
+ box-shadow: none;
+}
+
+button.ghost:hover {
+ background: var(--surface-hover);
+ box-shadow: var(--shadow-harsh);
+}
+
+button:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, var(--accent-danger) 0%, #b8301e 100%);
+ border-color: var(--accent-danger);
+ box-shadow: var(--shadow-harsh), 0 0 20px rgba(217, 54, 38, 0.3);
+}
+
+.btn-secondary {
+ background: linear-gradient(135deg, var(--accent-info) 0%, #1e3a5f 100%);
+ border-color: var(--accent-info);
+ box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 78, 122, 0.3);
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ WELCOME SCREEN
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.welcome-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ height: 100%;
+ gap: 24px;
+ padding: 20px 0;
+}
+
+.welcome-logo {
+ width: 140px;
+ height: 140px;
+ object-fit: contain;
+ filter: drop-shadow(5px 5px 0px var(--bg-secondary))
+ drop-shadow(0 0 30px rgba(230, 167, 60, 0.3))
+ grayscale(0.2) contrast(1.15);
+ animation: logoFloat 4s ease-in-out infinite, logoGlitch 8s step-end infinite;
+ position: relative;
+}
+
+@keyframes logoFloat {
+ 0%, 100% { transform: translateY(0) rotate(0deg); }
+ 25% { transform: translateY(-8px) rotate(-2deg); }
+ 75% { transform: translateY(-8px) rotate(2deg); }
+}
+
+@keyframes logoGlitch {
+ 0%, 90%, 100% {
+ filter: drop-shadow(4px 4px 0px var(--bg-secondary))
+ drop-shadow(0 0 20px var(--border-heavy))
+ grayscale(0.3) contrast(1.1);
+ }
+ 91% {
+ filter: drop-shadow(6px 4px 0px var(--accent-danger))
+ drop-shadow(0 0 20px var(--accent-danger))
+ grayscale(0) contrast(1.3);
+ }
+ 92% {
+ filter: drop-shadow(4px 6px 0px var(--accent-info))
+ drop-shadow(0 0 20px var(--accent-info))
+ grayscale(0) contrast(1.3);
+ }
+ 93% {
+ filter: drop-shadow(4px 4px 0px var(--bg-secondary))
+ drop-shadow(0 0 20px var(--border-heavy))
+ grayscale(0.3) contrast(1.1);
+ }
+}
+
+.welcome-title {
+ font-size: 2.8em;
+ margin: 0;
+ font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif;
+ font-weight: 400;
+ text-shadow: 4px 4px 0px var(--bg-secondary), 0 0 40px rgba(230, 167, 60, 0.25);
+ letter-spacing: 6px;
+ line-height: 1;
+ position: relative;
+ animation: welcomeTitleReveal 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+@keyframes welcomeTitleReveal {
+ 0% {
+ opacity: 0;
+ letter-spacing: 30px;
+ filter: blur(10px);
+ transform: scale(0.9);
+ }
+ 100% {
+ opacity: 1;
+ letter-spacing: 6px;
+ filter: blur(0);
+ transform: scale(1);
+ }
+}
+
+.welcome-subtitle {
+ font-size: 0.95em;
+ color: var(--text-secondary);
+ margin: -10px 0 0 0;
+ font-weight: 400;
+ letter-spacing: 0.5px;
+ font-family: 'JetBrains Mono', monospace;
+}
+
+.welcome-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+ max-width: 320px;
+ margin-top: 10px;
+}
+
+.welcome-credits {
+ color: var(--text-tertiary);
+ font-size: 0.75em;
+ margin-top: auto;
+ font-weight: 400;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+}
+
+.welcome-credits::before {
+ content: 'βββββ ';
+}
+
+.welcome-credits::after {
+ content: ' βββββ';
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ RULES SCREEN
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.rules-content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 10px 0;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+
+.rules-content::-webkit-scrollbar {
+ display: none;
+}
+
+.rule-section {
+ background: var(--surface-card);
+ border: 3px solid var(--border-medium);
+ border-left: 8px solid var(--accent-warning);
+ border-radius: 0;
+ padding: 18px;
+ margin-bottom: 16px;
+ transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1);
+ position: relative;
+ box-shadow: var(--shadow-md);
+ clip-path: polygon(
+ 0 0,
+ 100% 0,
+ 100% calc(100% - 10px),
+ calc(100% - 10px) 100%,
+ 0 100%
+ );
+ animation: ruleSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards;
+}
+
+.rule-section:nth-child(1) { animation-delay: 0.1s; }
+.rule-section:nth-child(2) { animation-delay: 0.2s; }
+.rule-section:nth-child(3) { animation-delay: 0.3s; }
+.rule-section:nth-child(4) { animation-delay: 0.4s; }
+
+@keyframes ruleSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.rule-section::before {
+ content: 'βΈ';
+ position: absolute;
+ left: -3px;
+ top: 18px;
+ font-size: 1.5em;
+ color: var(--accent-warning);
+ animation: blink 2s ease-in-out infinite;
+}
+
+@keyframes blink {
+ 0%, 49%, 100% { opacity: 1; }
+ 50%, 99% { opacity: 0; }
+}
+
+.rule-section:hover {
+ background: var(--surface-hover);
+ border-left-color: var(--accent-danger);
+ box-shadow: var(--shadow-lg);
+}
+
+.rule-section h3 {
+ margin: 0 0 14px 0;
+ color: var(--text-primary);
+ font-size: 0.95em;
+}
+
+.rule-section p {
+ margin: 8px 0;
+ color: var(--text-secondary);
+ line-height: 1.7;
+ font-size: 0.85em;
+ letter-spacing: 0.3px;
+}
+
+.rule-section strong {
+ color: var(--accent-danger);
+ font-weight: 800;
+ text-transform: uppercase;
+ font-size: 0.9em;
+ letter-spacing: 0.5px;
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ PLAYER MANAGEMENT
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.player-names-list {
+ flex: 1 1 auto;
+ min-height: 0;
+ max-height: 360px; /* Altura mΓ‘xima para activar scroll y mostrar fila parcial - efecto peek */
+ overflow-y: scroll;
+ overflow-x: hidden;
+ margin-bottom: 12px;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE y Edge */
+ /* Visual frame to indicate scrollable area */
+ background: var(--surface-card);
+ border: 4px solid var(--border-heavy);
+ border-radius: 0;
+ padding: 12px;
+ box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.15),
+ inset 0 -4px 12px rgba(0, 0, 0, 0.15),
+ var(--shadow-md);
+ /* Gradiente para crear efecto peek - texto cortado visible */
+ -webkit-mask-image: linear-gradient(to bottom,
+ black 0%,
+ black calc(100% - 40px),
+ transparent 100%);
+ mask-image: linear-gradient(to bottom,
+ black 0%,
+ black calc(100% - 40px),
+ transparent 100%);
+}
+
+.player-names-list::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
+
+.player-name-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+ background: var(--bg-secondary);
+ padding: 12px;
+ border-radius: 0;
+ border: 2px solid var(--border-light);
+ border-left: 4px solid var(--accent-info);
+ transition: all 0.2s ease;
+ box-shadow: var(--shadow-sm);
+}
+
+.player-name-item:last-child {
+ margin-bottom: 0;
+}
+
+.player-name-item:hover {
+ background: var(--surface-hover);
+ border-left-color: var(--accent-warning);
+ transform: translateX(2px);
+}
+
+.player-name-item span {
+ font-weight: 800;
+ min-width: 80px;
+ font-size: 0.8em;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.player-name-item input {
+ flex: 1;
+ padding: 10px;
+ margin: 0;
+ font-size: 0.85em;
+ border-width: 2px;
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ CURTAIN REVEAL MECHANISM
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.curtain {
+ position: relative;
+ width: 100%;
+ height: 280px;
+ background: var(--bg-secondary);
+ border-radius: 0;
+ overflow: hidden;
+ margin: 12px 0;
+ box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.3), var(--shadow-harsh);
+ cursor: grab;
+ user-select: none;
+ border: 3px solid var(--border-heavy);
+ flex-shrink: 0;
+}
+
+.curtain:active {
+ cursor: grabbing;
+}
+
+.curtain-cover {
+ position: absolute;
+ inset: 0;
+ background:
+ repeating-linear-gradient(
+ 0deg,
+ #2a2a2a 0px,
+ #2a2a2a 8px,
+ #1a1a1a 8px,
+ #1a1a1a 12px
+ ),
+ linear-gradient(180deg, rgba(255,200,100,0.03) 0%, transparent 50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ font-size: 1.1em;
+ font-weight: 800;
+ color: #888;
+ transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
+ z-index: 10;
+ user-select: none;
+ box-shadow: inset 0 -20px 40px rgba(0, 0, 0, 0.5), inset 0 0 60px rgba(0,0,0,0.3);
+ letter-spacing: 2px;
+ font-family: 'Bebas Neue', 'JetBrains Mono', monospace;
+}
+
+.curtain-cover::after {
+ content: '';
+ position: absolute;
+ bottom: -8px;
+ left: 0;
+ right: 0;
+ height: 8px;
+ background: linear-gradient(90deg,
+ transparent 0%,
+ rgba(0,0,0,0.3) 25%,
+ rgba(0,0,0,0.5) 50%,
+ rgba(0,0,0,0.3) 75%,
+ transparent 100%);
+}
+
+.curtain-cover.lifted {
+ transform: translateY(-100%);
+}
+
+.curtain-icon {
+ font-size: 2.5em;
+ animation: bounce 2s ease-in-out infinite;
+}
+
+@keyframes bounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-12px); }
+}
+
+.curtain-content {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 20px;
+ padding: 20px;
+ text-align: center;
+}
+
+.role {
+ font-size: 2.4em;
+ font-weight: 400;
+ padding: 16px 32px;
+ border-radius: 0;
+ text-transform: uppercase;
+ border: 4px solid;
+ font-family: 'Bebas Neue', 'JetBrains Mono', monospace;
+ letter-spacing: 6px;
+ box-shadow: var(--shadow-harsh);
+ position: relative;
+ animation: roleReveal 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+@keyframes roleReveal {
+ 0% {
+ opacity: 0;
+ transform: scale(0.5) rotate(-5deg);
+ filter: blur(10px);
+ }
+ 50% {
+ transform: scale(1.1) rotate(2deg);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1) rotate(0);
+ filter: blur(0);
+ }
+}
+
+.role.civil {
+ background: var(--accent-success);
+ color: var(--text-inverted);
+ border-color: #3d5a40;
+ animation: civilPulse 2s ease-in-out infinite;
+}
+
+.role.impostor {
+ background: var(--accent-danger);
+ color: var(--text-inverted);
+ border-color: #8a2e26;
+ animation: impostorPulse 1.5s ease-in-out infinite;
+}
+
+@keyframes civilPulse {
+ 0%, 100% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 0px rgba(90, 125, 95, 0.5); }
+ 50% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 25px rgba(90, 125, 95, 0.8); }
+}
+
+@keyframes impostorPulse {
+ 0%, 100% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 0px rgba(196, 69, 54, 0.5); }
+ 50% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 30px rgba(196, 69, 54, 0.9); }
+}
+
+.word {
+ font-size: 2em;
+ font-weight: 400;
+ background: var(--surface-card);
+ padding: 20px 36px;
+ border-radius: 0;
+ border: 3px solid var(--border-heavy);
+ font-family: 'Special Elite', 'Crimson Text', serif;
+ letter-spacing: 2px;
+ box-shadow: var(--shadow-harsh);
+ color: var(--text-primary);
+ text-transform: uppercase;
+ animation: wordReveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.2s forwards;
+ opacity: 0;
+}
+
+@keyframes wordReveal {
+ 0% {
+ opacity: 0;
+ transform: translateY(20px);
+ filter: blur(5px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ filter: blur(0);
+ }
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ TIMER
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.timer {
+ font-size: 4em;
+ font-weight: 800;
+ text-align: center;
+ margin: 20px 0;
+ padding: 24px;
+ background: var(--surface-card);
+ border-radius: 0;
+ border: 5px solid var(--border-heavy);
+ font-family: 'Bebas Neue', 'JetBrains Mono', monospace;
+ letter-spacing: 8px;
+ box-shadow: var(--shadow-harsh), inset 0 0 30px rgba(0, 0, 0, 0.2);
+ position: relative;
+ clip-path: polygon(
+ 16px 0,
+ calc(100% - 16px) 0,
+ 100% 16px,
+ 100% calc(100% - 16px),
+ calc(100% - 16px) 100%,
+ 16px 100%,
+ 0 calc(100% - 16px),
+ 0 16px
+ );
+ animation: timerAppear 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+@keyframes timerAppear {
+ from {
+ opacity: 0;
+ transform: scale(0.8);
+ filter: blur(5px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ filter: blur(0);
+ }
+}
+
+.timer::before {
+ content: '';
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 12px;
+ height: 12px;
+ background: var(--accent-success);
+ border-radius: 50%;
+ box-shadow: 0 0 10px var(--accent-success);
+ animation: statusBlink 2s ease-in-out infinite;
+}
+
+@keyframes statusBlink {
+ 0%, 49%, 100% { opacity: 1; }
+ 50%, 99% { opacity: 0.3; }
+}
+
+.timer.warning {
+ color: var(--accent-warning);
+ border-color: var(--accent-warning);
+ animation: timerShake 0.5s ease-in-out infinite;
+}
+
+.timer.warning::before {
+ background: var(--accent-warning);
+ box-shadow: 0 0 10px var(--accent-warning);
+}
+
+.timer.danger {
+ color: var(--accent-danger);
+ border-color: var(--accent-danger);
+ animation: timerShake 0.25s ease-in-out infinite, dangerFlash 1s ease-in-out infinite;
+}
+
+.timer.danger::before {
+ background: var(--accent-danger);
+ box-shadow: 0 0 15px var(--accent-danger);
+ animation: statusBlink 0.5s ease-in-out infinite;
+}
+
+@keyframes timerShake {
+ 0%, 100% { transform: translateX(0); }
+ 25% { transform: translateX(-4px); }
+ 75% { transform: translateX(4px); }
+}
+
+@keyframes dangerFlash {
+ 0%, 100% { background: var(--surface-card); }
+ 50% { background: rgba(196, 69, 54, 0.15); }
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ INFO BOXES & CONTENT
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.info-text {
+ text-align: center;
+ margin: 14px 0;
+ font-size: 0.85em;
+ line-height: 1.7;
+ background: var(--surface-card);
+ padding: 14px 16px;
+ border-radius: 0;
+ color: var(--text-secondary);
+ border: 2px solid var(--border-light);
+ border-left: 5px solid var(--accent-info);
+ box-shadow: var(--shadow-sm), inset 4px 0 8px rgba(46, 78, 122, 0.1);
+ letter-spacing: 0.3px;
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ PLAYER SELECTION GRID
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.player-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 10px;
+ margin: 12px 0;
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ padding: 4px;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+
+.player-list::-webkit-scrollbar {
+ display: none;
+}
+
+.player-item {
+ padding: 18px 14px;
+ min-height: 80px; /* Altura fija para evitar cambios de tamaΓ±o con vote-count */
+ background: var(--surface-card);
+ border-radius: 0;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
+ font-weight: 800;
+ font-size: 0.85em;
+ border: 3px solid var(--border-medium);
+ box-shadow: var(--shadow-sm);
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ clip-path: polygon(
+ 8px 0,
+ 100% 0,
+ 100% calc(100% - 8px),
+ calc(100% - 8px) 100%,
+ 0 100%,
+ 0 8px
+ );
+ animation: playerItemAppear 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards;
+}
+
+.player-item:nth-child(1) { animation-delay: 0.05s; }
+.player-item:nth-child(2) { animation-delay: 0.1s; }
+.player-item:nth-child(3) { animation-delay: 0.15s; }
+.player-item:nth-child(4) { animation-delay: 0.2s; }
+.player-item:nth-child(5) { animation-delay: 0.25s; }
+.player-item:nth-child(6) { animation-delay: 0.3s; }
+.player-item:nth-child(7) { animation-delay: 0.35s; }
+.player-item:nth-child(8) { animation-delay: 0.4s; }
+.player-item:nth-child(9) { animation-delay: 0.45s; }
+.player-item:nth-child(10) { animation-delay: 0.5s; }
+
+@keyframes playerItemAppear {
+ from {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.player-item::before {
+ content: 'β‘';
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ font-size: 1.2em;
+ transition: all 0.2s ease;
+}
+
+.player-item:hover {
+ background: var(--surface-hover);
+ box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.25);
+ filter: brightness(1.05);
+}
+
+.player-item:active {
+ box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15);
+ filter: brightness(0.95);
+}
+
+.player-item.selected {
+ background: var(--accent-danger);
+ border-color: var(--text-primary);
+ color: var(--text-inverted);
+ box-shadow: 0 0 0 4px rgba(217, 54, 38, 0.5), 6px 6px 0px rgba(0, 0, 0, 0.4);
+ animation: selectPulse 0.3s ease-out;
+}
+
+@keyframes selectPulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.08); }
+ 100% { transform: scale(1); }
+}
+
+.player-item.selected::before {
+ content: 'β';
+ animation: checkAppear 0.2s ease-out;
+}
+
+@keyframes checkAppear {
+ from { transform: scale(0) rotate(-180deg); }
+ to { transform: scale(1) rotate(0); }
+}
+
+.player-item.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+ background: var(--bg-secondary);
+ border-color: var(--border-light);
+ filter: grayscale(0.6);
+ animation: playerItemAppearDisabled 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards !important;
+}
+
+.player-item.disabled::before {
+ content: 'β';
+ color: var(--text-tertiary);
+}
+
+.player-item .vote-count {
+ display: block;
+ font-size: 0.7em;
+ margin-top: 4px;
+ opacity: 0.75;
+ font-weight: 600;
+ letter-spacing: 0.3px;
+ min-height: 1em; /* Reservar espacio siempre */
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ RESULTS
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.results {
+ background: var(--surface-card);
+ border-radius: 0;
+ padding: 14px;
+ margin: 8px 0;
+ flex: 1;
+ overflow: visible;
+ border: 2px solid var(--border-medium);
+ box-shadow: var(--shadow-md);
+ animation: resultsReveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+@keyframes resultsReveal {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.results h2 {
+ font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif;
+ font-size: 1.6em;
+ letter-spacing: 3px;
+ margin-bottom: 10px;
+ animation: winnerReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+@keyframes winnerReveal {
+ 0% {
+ opacity: 0;
+ transform: scale(0.5);
+ filter: blur(10px);
+ }
+ 60% {
+ transform: scale(1.1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ filter: blur(0);
+ }
+}
+
+.role-reveal {
+ background: var(--bg-secondary);
+ padding: 8px 10px;
+ border-radius: 0;
+ margin: 5px 0;
+ border-left: 4px solid;
+ font-size: 0.8em;
+ letter-spacing: 0.2px;
+ box-shadow: var(--shadow-sm);
+ transition: all 0.2s ease;
+ animation: roleRevealSlide 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards;
+}
+
+.role-reveal:nth-child(1) { animation-delay: 0.3s; }
+.role-reveal:nth-child(2) { animation-delay: 0.4s; }
+.role-reveal:nth-child(3) { animation-delay: 0.5s; }
+.role-reveal:nth-child(4) { animation-delay: 0.6s; }
+.role-reveal:nth-child(5) { animation-delay: 0.7s; }
+.role-reveal:nth-child(6) { animation-delay: 0.8s; }
+.role-reveal:nth-child(7) { animation-delay: 0.9s; }
+.role-reveal:nth-child(8) { animation-delay: 1s; }
+.role-reveal:nth-child(9) { animation-delay: 1.1s; }
+.role-reveal:nth-child(10) { animation-delay: 1.2s; }
+
+@keyframes roleRevealSlide {
+ from {
+ opacity: 0;
+ transform: translateX(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.role-reveal:hover {
+ transform: translateX(3px);
+}
+
+.role-reveal.civil-reveal {
+ border-left-color: var(--accent-success);
+}
+
+.role-reveal.impostor-reveal {
+ border-left-color: var(--accent-danger);
+}
+
+.role-reveal.executed {
+ opacity: 0.5;
+ background: rgba(0, 0, 0, 0.2);
+ text-decoration: line-through;
+}
+
+.tag {
+ display: inline-block;
+ padding: 6px 10px;
+ border-radius: 0;
+ background: var(--surface-hover);
+ margin: 4px 0;
+ font-weight: 800;
+ font-size: 0.75em;
+ border: 2px solid var(--border-medium);
+ letter-spacing: 1px;
+ text-transform: uppercase;
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ POOL SELECTION
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.pool-buttons {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px;
+ padding: 0;
+}
+
+.pool-buttons-wrapper {
+ position: relative;
+ flex: 1 1 auto;
+ min-height: 0;
+ max-height: 320px; /* Ajustado para mostrar fila parcial - efecto peek */
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE y Edge */
+ /* Visual frame to indicate scrollable area */
+ background: var(--surface-card);
+ border: 4px solid var(--border-heavy);
+ border-radius: 0;
+ padding: 12px;
+ margin: 12px 0;
+ box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.15),
+ inset 0 -4px 12px rgba(0, 0, 0, 0.15),
+ var(--shadow-md);
+ /* Gradiente para crear efecto peek - texto cortado visible */
+ -webkit-mask-image: linear-gradient(to bottom,
+ black 0%,
+ black calc(100% - 50px),
+ transparent 100%);
+ mask-image: linear-gradient(to bottom,
+ black 0%,
+ black calc(100% - 50px),
+ transparent 100%);
+}
+
+.pool-buttons-wrapper::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
+
+.pool-btn {
+ padding: 12px 10px;
+ border-radius: 0;
+ border: 2px solid var(--border-medium);
+ background: var(--surface-card);
+ color: var(--text-primary);
+ font-weight: 700;
+ font-size: 0.8em;
+ cursor: pointer;
+ transition: all 0.18s ease;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ box-shadow: var(--shadow-sm);
+}
+
+.pool-btn:hover {
+ background: var(--surface-hover);
+ box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.15);
+ filter: brightness(1.05);
+}
+
+.pool-btn.selected {
+ border-color: var(--text-primary);
+ background: var(--accent-warning);
+ color: var(--text-inverted);
+ box-shadow: 0 0 0 3px rgba(212, 165, 116, 0.3), 3px 3px 0px rgba(0, 0, 0, 0.2);
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ FIXED UI CONTROLS (Theme, Language, Exit)
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.theme-toggle {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 56px;
+ height: 56px;
+ border-radius: 0;
+ border: 3px solid var(--border-heavy);
+ background: var(--surface-glass);
+ backdrop-filter: blur(20px);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.6em;
+ box-shadow: var(--shadow-harsh);
+ transition: all 0.2s ease;
+ z-index: 1000;
+ margin: 0;
+ padding: 0;
+}
+
+.theme-toggle:hover {
+ box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2);
+ filter: brightness(1.1);
+}
+
+.theme-toggle:active {
+ box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15);
+ filter: brightness(0.95);
+}
+
+.theme-icon {
+ transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ display: inline-block;
+}
+
+.theme-toggle:hover .theme-icon {
+ transform: rotate(180deg) scale(1.1);
+}
+
+.language-toggle {
+ position: fixed;
+ top: 86px;
+ right: 20px;
+ width: auto;
+ min-width: 56px;
+ height: 56px;
+ padding: 0 16px;
+ border-radius: 0;
+ border: 3px solid var(--border-heavy);
+ background: var(--surface-glass);
+ backdrop-filter: blur(20px);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ font-size: 1em;
+ font-weight: 800;
+ box-shadow: var(--shadow-harsh);
+ transition: all 0.2s ease;
+ z-index: 1000;
+ color: var(--text-primary);
+ margin: 0;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.language-toggle:hover {
+ box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2);
+ filter: brightness(1.1);
+}
+
+.language-toggle:active {
+ box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15);
+ filter: brightness(0.95);
+}
+
+.language-icon {
+ font-size: 1.3em;
+ transition: transform 0.3s ease;
+ display: inline-block;
+}
+
+.language-text {
+ font-size: 0.85em;
+ letter-spacing: 1.5px;
+ font-family: 'JetBrains Mono', monospace;
+}
+
+.exit-game {
+ position: fixed;
+ top: 20px;
+ left: 20px;
+ width: auto;
+ height: 56px;
+ padding: 0 16px;
+ border-radius: 0;
+ border: 3px solid var(--border-heavy);
+ background: var(--surface-glass);
+ backdrop-filter: blur(20px);
+ cursor: pointer;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ font-size: 0.85em;
+ font-weight: 800;
+ box-shadow: var(--shadow-harsh);
+ transition: all 0.2s ease;
+ z-index: 1000;
+ color: var(--text-primary);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.exit-game.visible {
+ display: inline-flex;
+}
+
+.exit-game:hover {
+ box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2);
+ background: var(--accent-danger);
+ color: var(--text-inverted);
+}
+
+.exit-game:active {
+ box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15);
+}
+
+.exit-icon {
+ font-size: 1.3em;
+}
+
+.exit-text {
+ font-size: 0.9em;
+ font-family: 'JetBrains Mono', monospace;
+}
+
+.screen-lock-toggle {
+ position: fixed;
+ top: 152px;
+ right: 20px;
+ width: 56px;
+ height: 56px;
+ border-radius: 0;
+ border: 3px solid var(--border-heavy);
+ background: var(--surface-glass);
+ backdrop-filter: blur(20px);
+ cursor: pointer;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.6em;
+ box-shadow: var(--shadow-harsh);
+ transition: all 0.2s ease;
+ z-index: 1000;
+ margin: 0;
+ padding: 0;
+}
+
+.screen-lock-toggle.visible {
+ display: inline-flex;
+}
+
+.screen-lock-toggle:hover {
+ box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2);
+ filter: brightness(1.1);
+}
+
+.screen-lock-toggle:active {
+ box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15);
+ filter: brightness(0.95);
+}
+
+.screen-lock-toggle.active {
+ background: var(--accent-success);
+ color: var(--text-inverted);
+ border-color: var(--accent-success);
+}
+
+.screen-lock-icon {
+ transition: transform 0.3s ease;
+ display: inline-block;
+}
+
+.screen-lock-toggle:hover .screen-lock-icon {
+ transform: scale(1.1);
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ RESPONSIVE DESIGN
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+@media (max-width: 600px) {
+ body {
+ padding: 60px 10px 10px 10px;
+ font-size: 13px;
+ }
+
+ h1 {
+ font-size: 1.7em;
+ margin-bottom: 14px;
+ }
+
+ .container {
+ padding: 20px 16px;
+ }
+
+ .theme-toggle,
+ .language-toggle,
+ .exit-game,
+ .screen-lock-toggle {
+ top: 8px;
+ width: 44px;
+ height: 44px;
+ min-width: 44px;
+ }
+
+ .language-toggle {
+ top: 58px;
+ }
+
+ .screen-lock-toggle {
+ top: 108px;
+ }
+
+ .exit-game {
+ padding: 0 12px;
+ font-size: 0.75em;
+ height: 44px;
+ }
+
+ /* Ocultar textos en mΓ³vil, solo emojis */
+ .exit-text,
+ .language-text {
+ display: none;
+ }
+
+ .exit-game {
+ padding: 0;
+ width: 44px;
+ min-width: 44px;
+ }
+
+ .language-toggle {
+ padding: 0;
+ width: 44px;
+ min-width: 44px;
+ }
+
+ .exit-icon,
+ .language-icon {
+ font-size: 1.4em;
+ }
+
+ .timer {
+ font-size: 2.5em;
+ padding: 16px;
+ }
+
+ .welcome-title {
+ font-size: 1.8em;
+ }
+
+ .role {
+ font-size: 1.6em;
+ padding: 10px 18px;
+ }
+
+ .word {
+ font-size: 1.3em;
+ padding: 12px 20px;
+ }
+
+ .form-group {
+ margin-bottom: 10px;
+ }
+
+ .form-group.compact {
+ margin-bottom: 8px;
+ }
+
+ button {
+ padding: 12px 16px;
+ margin-top: 8px;
+ }
+
+ .rule-section {
+ padding: 12px;
+ margin-bottom: 12px;
+ }
+
+ .rule-section h3 {
+ font-size: 0.85em;
+ margin-bottom: 10px;
+ }
+
+ .rule-section p {
+ font-size: 0.8em;
+ margin: 6px 0;
+ }
+
+ .player-name-item {
+ padding: 10px;
+ margin-bottom: 6px;
+ }
+
+ .player-name-item span {
+ font-size: 0.75em;
+ min-width: 70px;
+ }
+
+ .player-item {
+ padding: 14px 10px;
+ min-height: 72px; /* Altura fija tambiΓ©n en mΓ³vil */
+ font-size: 0.8em;
+ }
+
+ .pool-btn {
+ padding: 10px 8px;
+ font-size: 0.75em;
+ }
+
+ .pool-buttons-wrapper {
+ max-height: 240px; /* Ajustado para mostrar fila parcial en mΓ³vil - efecto peek */
+ padding: 10px;
+ margin: 10px 0;
+ }
+
+ .player-names-list {
+ max-height: 280px; /* Ajustado para mostrar fila parcial en mΓ³vil - efecto peek */
+ padding: 10px;
+ }
+
+ .info-text {
+ padding: 12px 14px;
+ font-size: 0.8em;
+ margin: 10px 0;
+ }
+
+ .curtain {
+ height: 240px;
+ margin: 10px 0;
+ }
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ UTILITY ANIMATIONS
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+@keyframes typewriter {
+ from { width: 0; }
+ to { width: 100%; }
+}
+
+@keyframes glitch {
+ 0% { transform: translate(0); }
+ 20% { transform: translate(-2px, 2px); }
+ 40% { transform: translate(-2px, -2px); }
+ 60% { transform: translate(2px, 2px); }
+ 80% { transform: translate(2px, -2px); }
+ 100% { transform: translate(0); }
+}
+
+/* Smooth scrolling */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-medium) transparent;
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-medium);
+ border-radius: 0;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--border-heavy);
+}
diff --git a/styles.26a5b74f.css b/styles.26a5b74f.css
deleted file mode 100644
index 5b64e14..0000000
--- a/styles.26a5b74f.css
+++ /dev/null
@@ -1,455 +0,0 @@
-/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- EXPEDIENTE CLASIFICADO - IMPOSTOR GAME
- Noir Cyberpunk Interrogation Aesthetic
- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
-
-@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Special+Elite&display=swap');
-
-:root {
- /* LIGHT THEME: Interrogation Room */
- --bg-primary: #dcd9d2;
- --bg-secondary: #c8c3b8;
- --bg-overlay: rgba(0, 0, 0, 0.05);
-
- --surface-glass: rgba(255, 255, 255, 0.85);
- --surface-card: rgba(255, 255, 255, 0.95);
- --surface-hover: rgba(255, 255, 255, 1);
-
- --text-primary: #0a0a0a;
- --text-secondary: #2a2a2a;
- --text-tertiary: #5a5a5a;
- --text-inverted: #ffffff;
-
- --accent-warning: #e6a73c;
- --accent-danger: #d93626;
- --accent-success: #2d8b3d;
- --accent-info: #2e4e7a;
-
- --border-light: rgba(0, 0, 0, 0.18);
- --border-medium: rgba(0, 0, 0, 0.35);
- --border-heavy: rgba(0, 0, 0, 0.55);
-
- --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.15);
- --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.22);
- --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.28);
- --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.25);
-
- --grain-opacity: 0.05;
- --scanline-opacity: 0.025;
-
- /* Spotlight effect */
- --spotlight-color: rgba(255, 235, 180, 0.08);
- --vignette-intensity: 0.4;
-}
-
-[data-theme="dark"] {
- /* DARK THEME: Night Investigation */
- --bg-primary: #050505;
- --bg-secondary: #0f0f0f;
- --bg-overlay: rgba(255, 255, 255, 0.03);
-
- --surface-glass: rgba(25, 25, 25, 0.9);
- --surface-card: rgba(35, 35, 35, 0.95);
- --surface-hover: rgba(45, 45, 45, 1);
-
- --text-primary: #f5f5f5;
- --text-secondary: #d0d0d0;
- --text-tertiary: #909090;
- --text-inverted: #0a0a0a;
-
- --accent-warning: #ffb84d;
- --accent-danger: #ff3d2e;
- --accent-success: #3dd46b;
- --accent-info: #4d8ce0;
-
- --border-light: rgba(255, 255, 255, 0.12);
- --border-medium: rgba(255, 255, 255, 0.22);
- --border-heavy: rgba(255, 255, 255, 0.35);
-
- --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.6);
- --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.8);
- --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.95);
- --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.7);
-
- --grain-opacity: 0.07;
- --scanline-opacity: 0.035;
-
- /* Spotlight effect */
- --spotlight-color: rgba(255, 200, 100, 0.04);
- --vignette-intensity: 0.6;
-}
-
-/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- BASE STYLES & TYPOGRAPHY
- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
-
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- -webkit-tap-highlight-color: transparent;
-}
-
-body {
- font-family: 'JetBrains Mono', 'Courier Prime', 'Courier New', monospace;
- background:
- radial-gradient(ellipse 80% 50% at 50% 20%, var(--spotlight-color) 0%, transparent 50%),
- radial-gradient(circle at 20% 30%, rgba(230, 167, 60, 0.08) 0%, transparent 40%),
- radial-gradient(circle at 80% 70%, rgba(217, 54, 38, 0.06) 0%, transparent 40%),
- var(--bg-primary);
- min-height: 100vh;
- min-height: 100dvh;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 70px 16px 16px;
- color: var(--text-primary);
- position: relative;
- overflow: hidden;
- font-size: 14px;
- letter-spacing: 0px;
- transition: background 0.5s ease, color 0.3s ease;
-}
-
-/* Film grain texture overlay */
-body::before {
- content: '';
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.5'/%3E%3C/svg%3E");
- opacity: var(--grain-opacity);
- pointer-events: none;
- z-index: 9999;
- mix-blend-mode: overlay;
- animation: grain 8s steps(10) infinite;
-}
-
-@keyframes grain {
- 0%, 100% { transform: translate(0, 0); }
- 10% { transform: translate(-5%, -10%); }
- 20% { transform: translate(-15%, 5%); }
- 30% { transform: translate(7%, -25%); }
- 40% { transform: translate(-5%, 25%); }
- 50% { transform: translate(-15%, 10%); }
- 60% { transform: translate(15%, 0%); }
- 70% { transform: translate(0%, 15%); }
- 80% { transform: translate(3%, 35%); }
- 90% { transform: translate(-10%, 10%); }
-}
-
-/* Scanlines */
-body::after {
- content: '';
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: repeating-linear-gradient(
- 0deg,
- transparent,
- transparent 2px,
- rgba(0, 0, 0, 0.1) 2px,
- rgba(0, 0, 0, 0.1) 4px
- );
- opacity: var(--scanline-opacity);
- pointer-events: none;
- z-index: 9998;
-}
-
-/* Dramatic vignette overlay */
-.vignette-overlay {
- position: fixed;
- inset: 0;
- background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,var(--vignette-intensity)) 100%);
- pointer-events: none;
- z-index: 9997;
-}
-
-/* VHS interference effect */
-@keyframes vhsInterference {
- 0%, 100% { opacity: 0; }
- 5% { opacity: 0.03; transform: translateX(-2px); }
- 10% { opacity: 0; }
- 15% { opacity: 0.02; transform: translateX(1px); }
- 20% { opacity: 0; }
-}
-
-.vhs-line {
- position: fixed;
- left: 0;
- width: 100%;
- height: 3px;
- background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
- pointer-events: none;
- z-index: 9996;
- animation: vhsScan 8s linear infinite;
- opacity: 0.04;
-}
-
-@keyframes vhsScan {
- 0% { top: -10px; }
- 100% { top: 110%; }
-}
-
-h1 {
- font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif;
- text-align: center;
- margin-bottom: 20px;
- font-size: 2.6em;
- font-weight: 400;
- letter-spacing: 4px;
- text-transform: uppercase;
- position: relative;
- text-shadow: 3px 3px 0px var(--bg-secondary), 0 0 30px rgba(230, 167, 60, 0.2);
- line-height: 1.1;
- animation: titleReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
-}
-
-@keyframes titleReveal {
- from {
- opacity: 0;
- letter-spacing: 20px;
- filter: blur(8px);
- }
- to {
- opacity: 1;
- letter-spacing: 4px;
- filter: blur(0);
- }
-}
-
-h1::after {
- content: '';
- display: block;
- width: 80px;
- height: 4px;
- background: linear-gradient(90deg, var(--accent-danger) 0%, var(--accent-warning) 50%, var(--accent-danger) 100%);
- background-size: 200% 100%;
- margin: 14px auto 0;
- box-shadow: 0 0 15px rgba(230, 167, 60, 0.5);
- animation: shimmer 3s ease-in-out infinite;
-}
-
-@keyframes shimmer {
- 0%, 100% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
-}
-
-h2 {
- font-family: 'Crimson Text', Georgia, serif;
- text-align: center;
- margin: 16px 0;
- font-size: 1.4em;
- font-weight: 700;
- letter-spacing: 0.5px;
-}
-
-h3 {
- font-family: 'JetBrains Mono', monospace;
- font-size: 1em;
- font-weight: 800;
- text-transform: uppercase;
- letter-spacing: 1.5px;
- margin-bottom: 12px;
-}
-
-/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- CONTAINER & SCREENS
- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
-
-.container {
- width: 100%;
- max-width: 480px;
- background: var(--surface-glass);
- backdrop-filter: blur(20px) saturate(150%);
- border-radius: 0;
- padding: 28px 22px;
- box-shadow: var(--shadow-harsh), var(--shadow-lg);
- border: 4px solid var(--border-heavy);
- display: flex;
- flex-direction: column;
- transition: all 0.4s ease;
- margin-bottom: 20px;
- position: relative;
- overflow: hidden;
- clip-path: polygon(
- 0 20px,
- 20px 0,
- 100% 0,
- 100% calc(100% - 20px),
- calc(100% - 20px) 100%,
- 0 100%
- );
-}
-
-.container::before {
- content: 'β¬’ CLASSIFIED β¬’';
- position: absolute;
- top: 8px;
- left: 50%;
- transform: translateX(-50%);
- font-size: 0.65em;
- letter-spacing: 3px;
- opacity: 0.4;
- font-weight: 800;
- color: var(--accent-danger);
- text-shadow: 0 0 10px rgba(217, 54, 38, 0.3);
- animation: classifiedPulse 4s ease-in-out infinite;
-}
-
-@keyframes classifiedPulse {
- 0%, 100% { opacity: 0.4; text-shadow: 0 0 10px rgba(217, 54, 38, 0.3); }
- 50% { opacity: 0.6; text-shadow: 0 0 20px rgba(217, 54, 38, 0.6); }
-}
-
-/* Diagonal classified stamp */
-.container::after {
- content: 'EXPEDIENTE';
- position: absolute;
- bottom: 15px;
- right: -30px;
- font-family: 'Special Elite', 'Courier Prime', monospace;
- font-size: 0.7em;
- letter-spacing: 3px;
- color: var(--accent-danger);
- opacity: 0.12;
- transform: rotate(-45deg);
- font-weight: 400;
- white-space: nowrap;
- pointer-events: none;
-}
-
-.screen {
- display: none;
- animation: screenEnter 0.35s cubic-bezier(0.22, 1, 0.36, 1);
- flex: 1;
- overflow: hidden;
- min-height: 0;
-}
-
-.screen.active {
- display: flex;
- flex-direction: column;
-}
-
-@keyframes screenEnter {
- 0% {
- opacity: 0;
- transform: translateY(30px) scale(0.95);
- filter: blur(4px);
- }
- 60% {
- opacity: 1;
- filter: blur(0);
- }
- 100% {
- opacity: 1;
- transform: translateY(0) scale(1);
- filter: blur(0);
- }
-}
-
-/* Staggered children animation */
-.screen.active > * {
- animation: fadeSlideUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
-}
-
-.screen.active > *:nth-child(1) { animation-delay: 0.05s; }
-.screen.active > *:nth-child(2) { animation-delay: 0.1s; }
-.screen.active > *:nth-child(3) { animation-delay: 0.15s; }
-.screen.active > *:nth-child(4) { animation-delay: 0.2s; }
-.screen.active > *:nth-child(5) { animation-delay: 0.25s; }
-.screen.active > *:nth-child(6) { animation-delay: 0.3s; }
-.screen.active > *:nth-child(7) { animation-delay: 0.35s; }
-.screen.active > *:nth-child(8) { animation-delay: 0.4s; }
-
-@keyframes fadeSlideUp {
- from {
- opacity: 0;
- transform: translateY(15px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- FORM CONTROLS
- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
-
-.form-group {
- margin-bottom: 16px;
-}
-
-.form-group.compact {
- margin-bottom: 12px;
-}
-
-label {
- display: block;
- margin-bottom: 8px;
- font-weight: 700;
- font-size: 0.8em;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 1.2px;
-}
-
-input {
- width: 100%;
- padding: 12px 14px;
- border: 2px solid var(--border-medium);
- border-radius: 0;
- font-size: 0.95em;
- font-family: 'JetBrains Mono', monospace;
- background: var(--surface-card);
- color: var(--text-primary);
- transition: all 0.2s ease;
- box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-input:focus {
- outline: none;
- border-color: var(--accent-warning);
- box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(212, 165, 116, 0.2);
- transform: translateY(-1px);
-}
-
-/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- BUTTONS
- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
-
-button {
- width: 100%;
- padding: 16px 20px;
- border: 3px solid var(--text-primary);
- border-radius: 0;
- font-size: 0.9em;
- font-weight: 800;
- font-family: 'JetBrains Mono', monospace;
- cursor: pointer;
- background: var(--text-primary);
- color: var(--text-inverted);
- box-shadow: var(--shadow-harsh);
- transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
- margin-top: 12px;
- text-transform: uppercase;
- letter-spacing: 0.8px;
- position: relative;
- overflow: hidden;
- clip-path: polygon(
- 0 0,
- calc(100% - 12px) 0,
- 100% 12px,
- 100% 100%,
- 12px 100%,
- 0 calc(100% - 12px)
- );
-}
-
-