diff --git a/script.js b/script.js
index 05c0b74..8e41f46 100644
--- a/script.js
+++ b/script.js
@@ -3,352 +3,14 @@ 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']] }
+ { 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 = [];
@@ -374,7 +36,7 @@ let state = {
votingPlayer: 0,
selections: [],
executed: [],
- selectedPools: [], // Now it's an array for multiple pools, will be populated based on language
+ selectedPool: 'animales_naturaleza',
votingPool: null,
isTiebreak: false,
tiebreakCandidates: []
@@ -393,7 +55,7 @@ const loadPoolsCache = () => {
};
const savePoolsCache = () => localStorage.setItem(POOLS_CACHE_KEY, JSON.stringify(poolsCache));
-// ---------- Default values ----------
+// ---------- Defaults ----------
function defaultImpostors(nPlayers) {
const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
let impostors = 1;
@@ -416,162 +78,63 @@ function defaultDeliberation(gameSeconds) {
return Math.max(30, Math.round(gameSeconds / 3));
}
-// ---------- Word Pools ----------
+// ---------- 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 = [];
+ let list = [];
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);
+ 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 }));
}
-
- // 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();
- }
-
+ availablePools = list;
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];
- });
+ 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 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);
+ 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;
}
-
- 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] };
+ 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 = '';
-
- // 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();
- };
+ if (state.selectedPool === pool.id) btn.classList.add('selected');
+ btn.onclick = () => { state.selectedPool = pool.id; saveState(); renderPoolButtons(); };
container.appendChild(btn);
});
}
-// ---------- Setup and player names ----------
-function goToPools() {
+// ---------- 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));
@@ -581,15 +144,8 @@ function goToPools() {
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() {
+ 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');
}
@@ -600,13 +156,12 @@ function buildNameInputs() {
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}:`;
+ div.innerHTML = `Jugador ${i+1}:`;
list.appendChild(div);
}
}
-// ---------- Game start ----------
+// ---------- Inicio de partida ----------
function startGame() {
state.playerNames = [];
for (let i = 0; i < state.numPlayers; i++) {
@@ -620,9 +175,8 @@ function startGame() {
}).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];
+ state.civilianWord = shuffled[0];
+ state.impostorWord = shuffled[1];
finalizeStart();
});
}
@@ -640,7 +194,7 @@ function finalizeStart() {
showScreen('pre-reveal-screen');
}
-// Adjust defaults when player count is edited
+// 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);
@@ -657,32 +211,21 @@ document.getElementById('num-players').addEventListener('change', () => {
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(', ');
-
+ const startName = state.playerNames[state.startPlayer] || `Jugador ${state.startPlayer+1}`;
+ const poolMeta = availablePools.find(p => p.id === state.selectedPool) || EMBEDDED_POOLS[0];
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')}
+ 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'}
`;
}
-// ---------- Role revelation ----------
+// ---------- Revelación ----------
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);
@@ -690,24 +233,7 @@ function loadCurrentReveal() {
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('curtain-cover').classList.remove('lifted');
document.getElementById('next-player-btn').style.display = 'none';
document.getElementById('start-game-btn').style.display = 'none';
}
@@ -715,12 +241,7 @@ function loadCurrentReveal() {
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;
@@ -735,207 +256,19 @@ function liftCurtain() {
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() {
+// swipe support
+(() => {
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();
- }
-});
+ 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;
-async function startPhaseTimer(phase, seconds, elementId, onEnd) {
+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;
@@ -944,12 +277,7 @@ async function startPhaseTimer(phase, seconds, elementId, onEnd) {
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();
- }
+ if (remaining <= 0) { clearInterval(timerInterval); playBeep(); onEnd(); }
};
tick();
timerInterval = setInterval(tick, 1000);
@@ -971,48 +299,17 @@ function updateTimerDisplay(el, remaining) {
}
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;
- }
+ 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);
}
-// ---------- Game phases ----------
+// ---------- 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) {
- releaseWakeLock(); // Release wake lock when voting starts (no timer)
state.phase = 'voting';
state.votingPlayer = 0;
state.votes = {};
@@ -1023,8 +320,8 @@ function startVotingPhase(candidates = null, isTiebreak = false) {
renderVoting();
showScreen('voting-screen');
}
-function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startDeliberationPhase(); }
-function skipToVoting() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startVotingPhase(); }
+function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); startDeliberationPhase(); }
+function skipToVoting() { if (timerInterval) clearInterval(timerInterval); startVotingPhase(); }
function startTiebreakDeliberation(candidates) {
state.phase = 'deliberation';
state.tiebreakCandidates = candidates;
@@ -1033,40 +330,27 @@ function startTiebreakDeliberation(candidates) {
startPhaseTimer('deliberation', 60, 'deliberation-timer', () => startVotingPhase(candidates, true));
}
-// ---------- Secret voting ----------
+// ---------- 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;
-
- // 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
+ 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');
- // NO aplicar opacity inline - dejamos que CSS lo maneje con la animación
+ item.style.opacity = '0.5';
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);
- }
-
+ } else {
+ item.onclick = () => toggleSelection(i, item);
+ }
list.appendChild(item);
});
updateConfirmButton();
@@ -1097,7 +381,7 @@ function confirmCurrentVote() {
renderVoting();
}
-// ---------- Vote resolution ----------
+// ---------- 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 }));
@@ -1117,7 +401,7 @@ function handleVoteOutcome() {
} else {
// Tie for remaining slots
if (state.isTiebreak) {
- // Second tie: impostors win
+ // segunda vez empatados: ganan impostores
state.executed = [];
showResults(true);
return;
@@ -1131,217 +415,54 @@ function handleVoteOutcome() {
showResults();
}
-// ---------- Results ----------
+// ---------- Resultados ----------
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')}
+ ${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' : '';
- const roleText = t(role.toLowerCase());
- return `${state.playerNames[i]}: ${roleText} — "${word}" ${killed ? '☠️' : ''}
`;
+ return `${state.playerNames[i]}: ${role} — "${word}" ${killed ? '☠️' : ''}
`;
}).join('')}
`;
showScreen('results-screen');
}
-// ---------- Utilities ----------
+// ---------- 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();
- 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 newMatch() { clearState(); state = { ...state, phase:'setup', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; location.reload(); }
-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 ----------
+// ---------- 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;
-
- // 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;
+ 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');
}
-
- // 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.css b/styles.css
index 8e4ebaf..5b64e14 100644
--- a/styles.css
+++ b/styles.css
@@ -452,1378 +452,4 @@ button {
);
}
-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);
-}