Rework dos bónus diários + códigos resgatáveis — ganha fragmentos todos os dias e usa códigos da comunidade
Duas mudanças para a comunidade chegam juntas. (1) O login diário já não distribui pacotes grátis num esquema plano — agora concede fragmentos de pacote todos os dias num ciclo [3,3,3,5,5,5,8], e completar uma semana consecutiva dá um bónus de pacotes crescente (1 → 2 → 3 → 4, teto em 4). Falhas um dia, a sequência volta a zero — termina a semana para levares essa recompensa de ancoragem. (2) Nova secção no separador Perfil para resgatar códigos (p. ex. WELCOME-2026, códigos de parceiros, sorteios de criadores). Os códigos podem entregar qualquer combinação de pacotes, fragmentos e peças por raridade; o admin cria-os pela nova aba Códigos do painel, com tetos de uso e datas de validade opcionais.
Bónus diários
- Ciclo diário de fragmentos: [3, 3, 3, 5, 5, 5, 8] conforme a tua posição na sequência. Os fragmentos acumulam-se para a forja e conversão em pacotes.
- Bónus de pacotes ao completar a semana no dia 7 / 14 / 21 / 28 / …: +1 pacote na primeira semana, +2 na segunda, +3 na terceira, +4 a partir da semana 4 (teto).
- Falhas um dia → a sequência volta a 0; o próximo bónus de 'semana completa' recomeça em +1. Os antigos bónus de marcos +3 / +5 / +10 nos dias 7 / 14 / 28 são retirados.
- Interface do calendário atualizada: faixa de 7 dias com a semana atual e um selo 'Sem N' para as semanas concluídas acumuladas.
- As sequências existentes foram zeradas no deploy para todos começarem o novo esquema em pé de igualdade — sem crédito de meia semana arrastado.
Códigos resgatáveis
- Nova secção 'Resgatar' no separador Perfil — insere um código, vê a recompensa, consulta os teus últimos 20 resgates.
- Cada utilizador pode resgatar o mesmo código no máximo uma vez (garantido por um índice único por (código, utilizador) — sem resgates duplicados).
- Os códigos podem entregar pacotes, fragmentos e peças por raridade em qualquer combinação; o admin define a recompensa ao criar.
- Teto total de utilizações opcional (p. ex. 'só os primeiros 100 resgates') e data de validade por código (p. ex. 'válido até sexta').
- Limitado a 5 tentativas por minuto por utilizador para impedir brute-force por digitação. Toda criação / edição / eliminação de código pelo admin fica no audit log.
Sweep mais amplo de traduções no jogo
- As etiquetas de carta (ATK / DEF / HP / PM / Passiva), os badges (NOVA!, ✦ Nova, Em rotação em breve, Legado) e as fases (Cria / Fase 1-3 / MAX) aparecem agora no teu idioma na coleção, na revelação de gacha e nos ecrãs de batalha.
- Sino de notificações + toasts de guerra de guilda: cada linha, sufixo de 'há X', badge de vitória/derrota/empate e resumo de recompensas passa pelo idioma, com plurais corretos em polaco e russo.
- O cromo da sala de batalha (copy do lobby, overlay 'roda o dispositivo', etiqueta da mão, prefixo do adversário, pista de alvo, link Cancelar) e a overlay do anúncio recompensado (carregamento, motivos de erro, botão de resgate, aviso de manter visível) estão agora totalmente localizados.
- Fluxo de confirmação da galeria de cópias (progresso de nível, confirmações de dissolução, avisos de baralho bloqueado e troca pendente, contagem decrescente do rate limit, previsão de fragmentos / peças) — cada string que estava só em inglês agora está por idioma.
- O fallback do ErrorBoundary (botão Recarregar + copy seguro) e os tooltips da WalletBar (pacotes / fragmentos / pity / bónus diário disponível) aparecem agora no idioma ativo nas 12 locales suportadas.
- Ecrãs pré-login traduzidos de ponta a ponta: login / registo / 2FA / esqueci a palavra-passe / redefinir palavra-passe (títulos, subtítulos, placeholders, botões, mensagens de erro e sucesso, copy do gate de idade + Termos, aviso de email não verificado com timer de cooldown).
- Overlay de onboarding do primeiro arranque (3 passos ativos + indicadores em ponto, botões saltar e seguinte, todas as descrições de tipo de carta / raridade / holo / fase / habilidade) agora no teu idioma para o fluxo do primeiro pacote aterrar nativo.
- Frases flavor durante a abertura de pacotes: 58 frases únicas × 12 locales (~700 escritas criativas) para o texto que roda enquanto o pacote abre ser no teu idioma em pacotes padrão, ouro e diamante.
Acabamento de idiomas em todo o site
- O banner de cookies muda agora de idioma corretamente sempre que mudas de idioma — antes ficava preso no primeiro idioma visitado a partir da segunda mudança (p. ex. /es → /en mudava, mas /en → /it deixava o banner em inglês sobre a página em italiano).
- Cartas em Destaque (a página de showcase) traduzida de ponta a ponta para os 12 idiomas suportados — cada nome de carta e cada texto de marketing de 100-150 palavras é agora nativo em vez de cair para o espanhol para visitantes que não falam espanhol nem inglês.
- Entradas da página de Novidades: esta entrada e as duas anteriores (2026-04-28 — tradução do texto das cartas + esclarecimento do teto do Dissolver em massa) estão agora também totalmente nativas nos 12 idiomas. Entradas mais antigas caem elegantemente para o inglês enquanto as vamos completando em próximas releases.
- Modal de detalhes da carta: a etiqueta 'X cópias' e o teto do Dissolver em massa refletem agora a carta que estás realmente a ver — antes, abrir uma carta e navegar para outra na mesma sessão do modal mostrava a contagem da PRIMEIRA carta para todas as cartas seguintes, dando números errados como '4 cópias' numa pilha de que tinhas, na realidade, 19.
Texto das cartas agora no teu idioma — nomes, flavor e descrições de habilidades traduzidos para as 113 cartas
Até hoje a interface era localizada mas o conteúdo das cartas ficava em inglês: nomes, linhas de flavor, nomes e descrições de habilidades apareciam em inglês independentemente do idioma escolhido. Traduzimos as 113 cartas para o espanhol (Espanha + fallback LATAM), francês, italiano, alemão, português (BR + fallback PT), polaco, neerlandês, russo e turco — o nome canónico mais o flavor, mais os nomes e descrições de habilidades ativas e passivas quando existem. O payload do servidor em inglês continua a ser a fonte autoritativa para a base de dados; o que vês no ecrã é sobreposto no cliente conforme o teu idioma, por isso a identidade da carta mantém-se em trocas de idioma e em trades. Se uma tradução faltar em algum campo de alguma carta, cai no texto em inglês — nunca num vazio.
O que mudou
- Todos os 113 nomes de carta traduzidos para 11 idiomas-alvo (es, es-419, fr, it, de, pt-BR, pt-PT, pl, nl, ru, tr) — pt-PT e es-419 estão agora totalmente preenchidos como variantes nativas, não fallback.
- PT-PT (português europeu): grafias regionais (Fênix→Fénix, Tectônico→Tectónico, Gêiser→Géiser, Fumaça→Fumo), vocabulário distinto (mordida→dentada, filhote→cria, pulinho→saltinho, quica→salta, terremoto→terramoto, demais→restantes, libera→liberta, bravo→zangado), colocação enclítica (se alimenta→alimenta-se, a se tornar→a tornar-se) e imperativos com tu (Você não passará→Tu não passarás).
- es-419 (espanhol latino-americano): expressões naturais LATAM (Picado en Bomba→Bombardeo en Picada), evita 'Concha' (vulgar no Cone Sul) substituído por 'Caparazón' por segurança, e nota clara de que o voseo (AR/UY) não é aplicado intencionalmente para o catálogo se manter universal pela LATAM.
- Linhas de flavor traduzidas para cada carta — são 113 strings únicas × 9 idiomas.
- Nomes e descrições de habilidades ativas traduzidos para as 60 cartas de criatura que as têm.
- Nomes e descrições de habilidades passivas traduzidos para as 50 cartas de criatura que as têm.
- Ligado em todos os locais onde uma carta é renderizada: grelha da coleção, modal de carta, comparador, deck builder, modal de forja, separador Forja, revelação de gacha, sala de draft, mão e zonas em batalha, tooltips ao passar o cursor, pré-visualizações em drag, dropdowns de proposta de troca e pré-visualização da troca.
Notas
- Linhas de stats de objetos como '+5 ATK.' ou 'Fogo +3 ATK.' ficam como estão — é texto mecânico onde traduzir não acrescentaria nada.
- A identidade da carta (card_id, instance_id, semente holo, ATK/DEF/HP/PM) preserva-se no overlay de idioma — só mudam as strings mostradas. Trocas, baralhos e replays de batalha são 100 % compatíveis entre idiomas.
O teto do Dissolver em massa agora explica-se sozinho — vês porque é que o teu máximo é o que é
Feedback dos jogadores depois do lançamento do Dissolver em massa: o changelog dizia "até 50 por lote" mas um jogador com só 21 cópias de uma carta batia num teto de 20 e achava que algo estava partido. Nada estava — o servidor garante "manter pelo menos 1 cópia de cada carta", por isso uma pilha de 21 cópias teta em 20 de uma vez. O teto sempre foi mín(teto servidor 50, cópias elegíveis, cópias totais − 1); só era invisível. Agora o rótulo do botão de entrada diz exatamente qual é o teu teto de lote no formato "máx N de M elegíveis" e um tooltip ao passar o cursor explica a restrição vinculante (teto do servidor, elegibilidade ou manter-1). O cabeçalho do painel de seleção repete a restrição para não teres de adivinhar.
O que mudou
- O botão de entrada do Dissolver em massa agora diz "💠💠 Dissolver em massa... (máx N de M elegíveis)" em vez de só "(M elegíveis)" — N é o teto real de seleção honrando teto do servidor + manter-1.
- O tooltip ao passar o cursor sobre o botão mostra a razão vinculante: "máx servidor 50", "M elegíveis" ou "a manter 1 de T totais".
- O cabeçalho do painel de seleção agora coloca a restrição vinculante depois do número do teto, por isso a razão fica visível sem passar o cursor.
- Comportamento sem mudanças: a regra manter-1 sempre foi aplicada no servidor. Esta mudança é pura clareza UX — mesmas proteções, rótulos mais transparentes.
Dissolver em massa — destrói até 50 cópias de uma carta numa única ação
Acumuladores, esta é para vocês. Dissolver cópias comuns uma de cada vez queimava a janela de rate-limit por utilizador — um jogador com 80 comuns de uma única carta não conseguia limpar numa sessão. A vista de detalhe da carta mostra agora um botão "Dissolver em massa..." por baixo do Dissolver por cópia que já existia, que aparece sempre que tiveres pelo menos 2 cópias elegíveis da carta que estás a ver. Clica, escolhe quais números de impressão destruir numa grelha de mosaicos (as cópias bloqueadas, em baralho ou em troca ativa aparecem atenuadas e desativadas), e o lote inteiro passa numa única confirmação + uma única vaga de rate-limit. O teto do servidor é 50 por chamada, por isso a resposta continua rápida. A pré-visualização de recompensa mostra-te os fragmentos exatos + as peças por raridade que vais receber antes de carregar em Destruir tudo.
O que mudou
- Vista de detalhe da carta → novo botão "💠💠 Dissolver em massa... (N elegíveis)" por baixo do Dissolver por cópia. Só aparece quando tens ≥2 cópias elegíveis (elegíveis = desbloqueadas + não-no-baralho + não-em-troca-ativa).
- A grelha de mosaicos mostra cada cópia com o seu número de impressão + marcadores holo / bloqueada / em-baralho / em-troca. As cópias não elegíveis aparecem atenuadas e desativadas. A seleção é limitada a mín(50, elegíveis, cópias − 1) para nunca poderes zerar a pilha completa.
- Controlos "Selecionar tudo" + "Limpar" + uma pré-visualização ao vivo "+X fragmentos / +Y peças R" que espelha a UI de recompensa por cópia. O modal de confirmação pergunta uma vez antes de qualquer chamada de rede: "Dissolver N cópias de \"Nome da Carta\"? +X frags +Y peças R. [Cancelar] [Destruir tudo]".
- Servidor: uma única vaga de rate-limit é consumida independentemente do tamanho do lote — é precisamente o ponto da funcionalidade. A transação inteira é atómica (todas as 50 cópias dissolvem juntas ou nenhuma).
- Segurança em trocas pendentes: se alguma cópia selecionada estiver numa troca ativa, o servidor avisa e oferece um prompt "Cancelar e Dissolver" que reenvia a chamada com confirmação explícita. Sem sobrescrita silenciosa de uma troca em curso.
- Segurança da última cópia: por carta, o servidor garante que mantens pelo menos uma cópia. Um lote que zeraria uma pilha é rejeitado com um erro claro e nenhuma linha é apagada.
Coleção: filtro "A rodar em breve" + classificações públicas de torneios de guilda
Duas pequenas mas visíveis melhorias de qualidade de vida. No separador Coleção, o menu de disponibilidade ganhou a opção "A rodar em breve" que filtra a grelha pelas cartas que expiram nos próximos 30 dias — mesmo limite do emblema que já vinha aparecendo nessas cartas, por isso filtro e emblema concordam sempre. No separador Torneio, os torneios de guilda mostram agora o painel de classificações a qualquer visitante — não apenas a participantes inscritos. Antes abrias um torneio de guilda e vias participantes mas nenhum quadro a menos que a tua guilda estivesse registada. Agora o painel público é renderizado para todos, a ler os mesmos dados que o servidor já devolvia, pelo que podes ver quem vai à frente sem precisares de te registar primeiro.
O que mudou
- Coleção → menu de disponibilidade → nova opção "A rodar em breve" filtra cartas cujo `available_until` está nos próximos 30 dias. A linha de chips ativos mostra um chip "A rodar em breve" removível enquanto o filtro está ativo.
- A janela de 30 dias é agora uma única constante partilhada, pelo que o filtro e o emblema "A rodar em breve" existente não podem divergir.
- Separador Torneio → torneios de guilda → o painel de classificações é renderizado também para visitantes não inscritos. Mesmos dados da vista de participante; o painel mostra quem vai à frente antes mesmo de a tua guilda se registar.
- Os torneios fora de guilda permanecem inalterados — o painel público só é renderizado quando `tournament_type === "guild"`.
Endurecimento de build + CI — gates de qualidade movidos para o CI, dispensas para o audit de dependências, refresco diário da IAB GVL — varrimento de revisão hostil de 14 áreas concluído
A última área do varrimento de revisão hostil de 14 áreas — pipelines de build e gates de CI. A maioria das mudanças é invisível para os jogadores (vivem em `.github/workflows/` e `scripts/`) mas movem a qualidade de «só local com bypass de --no-verify» para «a bloquear o merge em cada PR». A suite vitest completa (~2700 testes), type-check TypeScript, drift gate do Drizzle, paridade de chaves i18n, pin do BullMQ, validação de battle-config e baseline de bare-db-execute correm agora todos em CI em cada PR. O scan CVE de produção ganhou um gate consciente de dispensas que expõe vulnerabilidades moderadas (o limite anterior silenciava seis). A IAB Global Vendor List usada pelo banner de consentimento atualiza-se agora diariamente através de um workflow agendado que abre um PR quando a lista muda. Dois testes skipped da suite battle levam referências rastreadas (T-201, T-202) e um novo gate invariante faz o build falhar em qualquer skip futuro sem tag para que código morto não se acumule.
O que mudou para os jogadores
- Nada visualmente. Todo o lote é tooling interno — workflows de CI, gates de audit de dependências, higiene de build. A nota de fecho importa porque a revisão de 14 áreas trouxe à luz uns ~600 achados em todo o código (auth, motor de batalha, schema, gacha, API admin, API pública, Game.tsx + separadores, UI de cartas/baralhos/batalha, internals de perfil/guilda/torneio, anticheat, i18n, legal/consentimento, observabilidade, infra de build/test) — cada correção visível ao jogador das treze áreas anteriores já saiu em entradas anteriores do changelog. Esta entrada apenas fecha o varrimento.
- Lado operador: a IAB Global Vendor List que o banner de consentimento usa para a divulgação por fornecedor atualiza-se agora diariamente através de uma GitHub Action agendada que abre um PR quando a lista muda. Uma GVL desatualizada era um risco de deriva de âmbito de fornecedor (pior caso: Limited Ads do AdSense na UE). Uma pessoa revê cada PR de refresco antes do merge.
- Duas dependências de produção com vulnerabilidades conhecidas (next-intl + postcss-via-next) estão agora formalmente documentadas em `context/refs/cve-waivers.md` com datas de revisão explícitas (2026-07-25). O gate de CVE anterior escondia-as a subir o limite de severidade; o novo gate expõe todas as moderadas-e-acima e faz o CI falhar a menos que uma linha de dispensa as cubra. Visibilidade honesta em vez de risco silencioso.
Estabilidade do servidor + privacidade nos logs — handlers de erros do processo, retenção de audit aplicada, nomes de jogadores redatados dos logs
Uma passagem pela pipeline de observabilidade e audit fechou a lacuna entre o que a política de privacidade diz e o que o servidor faz na realidade. Nada mudou visualmente para os jogadores, mas algumas coisas melhoraram por baixo: o processo Node.js regista e conta agora erros não capturados em vez de cair em silêncio e deixar a plataforma reiniciá-lo sem explicação; a tabela audit_log é agora purgada diariamente por um cron que aplica a janela de retenção de 90 dias prometida pela política; os nomes de visualização dos jogadores são agora redatados das linhas de log do servidor (o Considerando 30 do RGPD trata os nomes de visualização como dados pessoais); e a pipeline de audit de eventos de segurança ganhou verificações de durabilidade e proveniência por hash de IP por ação, pelo que uma inspeção regulatória pode reconstruir qualquer ação de conta com timestamp, ator e origem.
O que mudou
- O processo Node.js regista agora um erro não capturado antes de a plataforma o reiniciar. Antes o servidor morria em silêncio, o container reiniciava sozinho, e debugar depois exigia adivinhar a partir das marcas de tempo de morte do container. Agora cada erro fatal aterra no log estruturado com o stack trace completo + um contador para graficar as taxas de crash ao longo do tempo.
- A política de privacidade diz que os logs de audit são mantidos durante 90 dias. Antes nada aplicava realmente isso — as linhas antigas acumulavam-se para sempre. Um novo cron diário (`/api/cron/audit-retention`) poda tudo o que tem mais de 90 dias em lotes, pelo que a tabela fica limitada e a promessa da política é honesta.
- Os nomes de visualização dos jogadores são agora redatados das linhas de log do servidor. O Considerando 30 do RGPD trata os nomes de visualização como dados pessoais; a forma anterior dos logs incluía os campos winnerName / challengerName / opponentName tal e qual em cada linha de fim de batalha. O Pino redact mascara-os como [Redacted] antes de chegarem ao agregador.
- Lado operador: os endpoints /api/metrics + /api/health suportam agora um gate por token Bearer em produção para que os dados de observabilidade não fiquem expostos na internet aberta (definir METRICS_BEARER_TOKEN / HEALTH_TOKEN; dev tolera ausência). O endpoint de relatório de violação CSP (/api/csp-report) limita agora por IP para que um spammer não consiga afogar os alertas CSP reais.
Endurecimento legal e de consentimento — string TCF v2.3 oficial, auditoria durável do consentimento, idade + Termos aplicados no servidor, lista real de subcontratantes, eliminação + exportação de dados ampliadas
Uma passagem pela superfície legal e de consentimento fechou uma longa lista de lacunas de conformidade. Manchetes: a string de consentimento IAB TCF v2.3 é agora o formato bit-packed oficial produzido pelas bibliotecas @iabtechlabtcf/* em vez de um shim base64(JSON) — o parser do Google AdSense rejeitava o nosso shim desde o prazo de 2026-03-01, o que silenciosamente colocava visitantes da UE em Limited Ads. Cada escolha do banner de consentimento (Aceitar tudo / Rejeitar tudo / Personalizar / retirar / aviso de version bump / reabrir) escreve agora uma linha durável em audit_log para podermos demonstrar o consentimento perante uma inspeção do Art.7(1) do RGPD. A confirmação de idade 16+ e a aceitação dos Termos no registo são agora aplicadas do lado do servidor através dos additional fields do Better Auth — a checkbox só-cliente anterior podia ser contornada via curl. A política de privacidade nomeia agora os terceiros reais (Arsys para alojamento + Google AdSense para publicidade) em vez de texto genérico — Postgres e Redis correm sobre infraestrutura Arsys gerida pelo operador e não são subcontratantes distintos. A eliminação de conta limpa agora um conjunto bem maior de tabelas com dados pessoais (amizades, pertença a guilda, inscrições em torneios, tickets, histórico de ad-watch, seeds provably-fair, tokens de verificação e várias outras), usa um sufixo pseudónimo cripto-aleatório que quebra a ligação anterior com audit_log, recusa recriar um pedido de eliminação para uma conta já totalmente eliminada, e limita os retries do cron para que uma falha transitória não fique em loop infinito. O artefacto de exportação de dados ganhou seis novas secções (pertença a guilda, torneios, amizades, tickets de suporte, histórico de ad-watch, histórico de seeds provably-fair) e tetos de linhas por secção para que um utilizador avançado não derrube o build da exportação por OOM. O banner de consentimento é agora um diálogo modal correto com focus trap e aria-modal, o seletor de idioma é visível em todos os breakpoints (estava oculto no mobile), e os bans de admin entregam agora uma Declaração de Motivos do Art.17 do DSA através de notificação na app com a regra violada e uma via de recurso. Adicionalmente: uma IAB Global Vendor List fresca incluída e um script de refresh diário para que mudanças no âmbito dos fornecedores não desatualizem, cookies first-party do AdSense agora realmente apagados ao retirar o consentimento (antes apenas se desmontava a tag de script), e um novo gate CSRF com token nos endpoints de eliminação de conta e exportação de dados para que uma página atacante com formulário forjado não consiga disparar a contagem decrescente da eliminação para um visitante autenticado. Versão de consentimento subiu de 3.1 para 3.2 — cada visitante verá o banner uma vez na próxima visita para reconfirmar sob a lista atualizada de subcontratantes.
String IAB TCF v2.3 real + auditoria durável do consentimento
- A string de consentimento TCF v2.3 é agora produzida pelo encoder oficial @iabtechlabtcf/core e a API de gestão do consentimento é a biblioteca oficial @iabtechlabtcf/cmpapi — substituindo um shim base64(JSON) que o parser do Google AdSense rejeitava desde 2026-03-01, colocando visitantes da UE em Limited Ads. A CMP instala-se como `__tcfapi` na página exatamente conforme a especificação IAB CMP v2.
- Cada resolução do banner de consentimento (Aceitar tudo / Rejeitar tudo / Personalizar / retirar / aviso de version bump / reabrir) escreve uma linha durável em audit_log marcada scope='legal' com o payload completo de consentimento por finalidade, opt-ins de funcionalidades especiais, string addtl_consent, versão anterior e uma flag para indicar se a curadoria Google ATP faltava no momento do consentimento. Agora conseguimos responder 'prova que este utilizador consentiu neste momento sob esta versão' apenas a partir do audit_log — antes do fix esse rasto não existia.
- A IAB Global Vendor List incluída foi atualizada e um script `npm run refresh:gvl` permite que um cron diário deposite um snapshot fresco. Uma GVL desatualizada era um risco de drift do âmbito do fornecedor — cada refresh corre uma verificação de schema antes do rename atómico no disco, para que um fetch interrompido nunca deixe um ficheiro a meio.
Idade + Termos + CSRF + lista de subcontratantes aplicada no servidor
- O registo persiste agora `age_confirmed_at` e `tos_version_accepted` na linha de utilizador através dos additional fields do Better Auth, e um hook no servidor rejeita de forma dura qualquer registo a que falte um dos valores. Antes do fix ambos eram checkboxes só-cliente que qualquer pedido curl conseguia saltar.
- A lista de subcontratantes na política de privacidade nomeia agora os terceiros reais: Arsys (Espanha) para alojamento + Google AdSense / AdMob para publicidade. Postgres e Redis correm em infraestrutura Arsys gerida pelo operador e não são subcontratantes distintos. A linha hipotética de 'processador de pagamento' sai até a loja cosmética ser lançada sob uma flag futura.
- Os endpoints de eliminação de conta e exportação de dados exigem agora um token CSRF (padrão double-submit cookie), para que uma página atacante com formulário forjado não consiga disparar a contagem decrescente de eliminação de 30 dias nem roubar o artefacto de exportação de uma vítima autenticada. O cookie de sessão do Better Auth é sameSite=Lax, o que de outra forma teria permitido POSTs cross-origin a partir de navegação top-level.
Eliminação de conta e exportação de dados ampliadas
- A eliminação de conta apaga ou anonimiza agora um conjunto bem maior de tabelas com dados pessoais: amizades e pedidos de amizade, pertença e chat de guilda, pedidos de entrada e wishlist de guilda, participantes e rondas de torneio, tickets de suporte e respostas, tokens e histórico de ad-watch, histórico de seeds provably-fair, tokens de verificação de e-mail e reposição de palavra-passe — e aplica a anonimização sentinela a match-telemetry, doações ao fundo de guilda e várias outras tabelas com retenção para auditoria. Antes do fix mais de dez tabelas guardavam silenciosamente os dados do utilizador mesmo após a eliminação ter sido marcada como 'concluída'.
- O pseudónimo usado para sobrescrever nome e e-mail de uma conta eliminada usa agora um sufixo cripto-aleatório em vez de um corte determinístico de 8 caracteres do id original do utilizador. O corte anterior carregava 32 bits de entropia do id original, pelo que um regulador conseguia correlacionar o nome sentinela de volta ao utilizador eliminado via audit_log.target_id. O novo sufixo quebra a ligação por completo.
- Dois pedidos de eliminação simultâneos do mesmo utilizador colapsam agora numa única linha ao nível da base de dados (índice único parcial em `(user_id) WHERE status IN ('pending','processing')`); o POST do pedido devolve `already_pending` em vez de rebentar com um 500. Um utilizador cuja eliminação já foi concluída não pode criar um novo pedido — a nova verificação `wasUserEverDeleted` recusa logo, defendendo contra um un-ban futuro que de outra forma ressuscitaria uma conta meio-anonimizada.
- Se uma transação de eliminação falhar a meio (violação de FK, lock timeout, blip de rede) o worker do cron incrementa agora um contador de retries e guarda a última mensagem de erro em vez de ficar em loop em `pending`. Após cinco tentativas falhadas a linha passa a `failed` para atenção do admin.
- A exportação de dados cobre agora seis novas secções: pertença a guilda, inscrições em torneios, amizades, tickets de suporte e respostas, histórico de ad-watch e histórico de seeds provably-fair. A auto-documentação `gaps[]` anterior listava-as como 'temos mas não entregamos' — o que violava a portabilidade do Art.20 do RGPD. Cada secção leva também um teto de 10.000 linhas para que um utilizador avançado não derrube o build da exportação por OOM, e um campo `truncations[]` reporta honestamente as exportações parciais.
Acessibilidade do banner + higiene de cookies AdSense + entrega do Art.17 do DSA
- O banner de cookies é agora um diálogo modal correto: role='dialog', aria-modal='true', aria-labelledby a apontar para um cabeçalho apenas para leitores de ecrã, e um focus trap que captura o foco ao abrir e o restaura ao fechar. O Tab não escapa para a página por baixo. O Esc propositadamente NÃO fecha porque seria uma rejeição implícita — dark pattern.
- O seletor de idioma (ES | EN) está agora visível em todos os breakpoints, incluindo mobile — antes do fix estava escondido sob o breakpoint sm:, o que combinava mal com o bug do middleware F-A11-009 que já silenciosamente colocava visitantes fora da locale padrão no idioma errado.
- Retirar o consentimento publicitário apaga agora realmente os cookies first-party do AdSense (`__gads`, `__gpi`, `__eoi`, `FCNEC`, `FCCDCF`) do navegador, não apenas a tag de script injetada. Antes do fix uma retirada deixava esses cookies no sítio, o que violava ePrivacy + Art.7(3) do RGPD (a retirada tem de equivaler à eliminação dos cookies).
- Quando um admin bane um utilizador, o utilizador recebe agora uma notificação na app com a Declaração de Motivos do Art.17 do DSA com a regra violada, a razão e uma via de recurso — e a linha de audit_log carrega o mesmo payload sob scope='legal'. Antes do fix os bans eram silenciosos: o utilizador não conseguia entrar, não sabia porquê, e não tinha via de recurso documentada. O endpoint recusa agora qualquer ban que não forneça a regra e a razão.
i18n hardening — Spanish chrome everywhere, locale-aware dates, mobile language switcher, unified contact email
A pass through the bilingual surface closed every visible English-only chrome remnant on Spanish pages and tightened a few middleware + SEO gaps. Headline items: the marketing nav (About / How to Play / Cards / FAQ / Contact / Updates / Log in / Play for Free), every legal-page header link, the global cookie-consent banner, the offline banner, and the language-switcher screen-reader label all read from the active-locale message bundle now — Spanish visitors no longer see English wrappers around Spanish copy. The language switcher is also visible on phones (was hidden under the sm: breakpoint), so a visitor landing on the wrong locale on mobile can finally jump in-page. Changelog dates are stored as ISO YYYY-MM-DD and rendered through Intl.DateTimeFormat per locale — Spanish visitors see "26 de abril de 2026" / English visitors see "April 26, 2026". Email addresses across the legal + contact surface have been unified to a single canonical contact@drawntcg.com so visitors don't have to guess between info / privacy / support inboxes. A new /robots.txt now points crawlers at the per-locale sitemap, and link-preview crawlers (Slack, Discord, iMessage, Twitter) get an Open Graph card with the right locale tag. A subtle middleware bug that made the <html lang="…"> attribute always say "es" on /en/* pages — silently breaking screen-reader locale routing and Google's hreflang verification — has been fixed.
Spanish chrome on every public surface
- MarketingHeader (every public page) reads its nav labels and CTAs from the marketing_header.* namespace — Spanish visitors see Sobre nosotros / Cómo jugar / Cartas / FAQ / Contacto / Novedades / Iniciar sesión / Juega Gratis instead of the English equivalents.
- Legal page chrome (the back button, the four short links in the header — Terms / Privacy / Cookies / Legal Notice — and the long versions in the footer plus the all-rights-reserved line) all translate. Pre-fix /es/legal/* pages had a Spanish body with English chrome wrapping it.
- Global cookie-consent banner (the EU-mandated TCF v2.3 surface) translates intro copy, the Accept All / Reject All / Customize / Save Preferences buttons, every section heading, and every screen-reader aria-label so Spanish visitors don't see an English banner when they first land.
- The "You appear to be offline" banner (mounted on every page) and the language-switcher's accessibility label both read from the active locale.
Locale-aware dates + visible language switcher on mobile
- Changelog entry dates are now stored as ISO YYYY-MM-DD and rendered with Intl.DateTimeFormat — "26 de abril de 2026" in Spanish, "April 26, 2026" in English, with a proper <time dateTime="…"> wrapper for SEO.
- The Spanish changelog page heading is now "Registro de cambios" instead of the English word "Changelog".
- The language switcher (ES | EN) is now visible at every breakpoint, including phones. Pre-fix it was hidden under the sm: breakpoint, so a phone visitor landing on the wrong locale had to manually edit the URL to switch.
Single canonical contact email + softer Discord copy
- Every contact channel across the marketing, legal, and privacy surface points at a single canonical address: contact@drawntcg.com. Pre-fix the same visitor could see info@ on the contact page, privacy@ in the privacy policy, and support@ in the terms — confusing for anyone trying to figure out where to write.
- The Discord callout was softened — instead of "the invite link is being set up — when it lands it will show up here" it now says the invite is coming soon and points readers at the changelog page for updates in the meantime. No false promise of imminence.
Crawler + middleware fixes (mostly invisible but real)
- A new /robots.txt enumerates the public surface, blocks the authenticated game shell + admin + API + auth-only paths from crawl, and points search engines at /sitemap.xml so the per-locale sitemap is no longer discovered organically.
- Link-preview crawlers (Slack, Discord, iMessage, Twitter / X) now receive an Open Graph card with the active locale tag (es_ES or en_US) plus the alternate-locale list and a Twitter summary_large_image hint — bilingual link shares no longer surface the wrong-locale variant by accident.
- A subtle middleware bug that made <html lang="…"> always say "es" on /en/* pages is fixed. Pre-fix the request-header forwarding ran before next-intl's locale-injection, so the root layout never saw the active locale and fell back to the default — silently breaking screen-reader locale routing and Google's hreflang verification.
- Set-Cookie headers are now appended (not set) when merging next-intl's response — multiple cookies on a single response are no longer collapsed into one comma-joined value that browsers misparse.
Anticheat + provably-fair hardening — atomic seed rotation, replay scrubbing, spectator strategy strip
A pass through the anticheat surface (provably-fair seeds, replay sanitization, spectator state, collusion-detection telemetry) closed a small list of subtle but real defects. Headline items: pack-opening seed rotation is now atomic against in-flight pack opens, so a verifier replay can never reproduce N−1 of N openings while the Nth orphan dangles unverifiable; replay history scrubs raw player ids out of turn-log strings before persistence so the publishable replay file truly cannot be cross-correlated by id; spectator state strips two alpha-strategy hints (adrenaline counters and per-turn summon counts) that pre-fix flowed through unmasked; provably-fair seed rotations and client-seed changes are now durably audited so a player dispute has a recoverable trail; the history endpoint no longer 500s on garbage page params and no longer leaks raw Postgres errors to clients.
Provably-fair integrity
- Pack-opening seed rotation is now atomic against any in-flight pack open. Pre-fix a rotate landing between a pack-open's seed read and nonce update could publish a stale total_nonces value, leaving the in-flight opening unverifiable by the verifier. The race is closed via a transactional row lock plus a database-level unique-active-seed constraint.
- Both seed rotations and player-supplied client_seed changes now write a durable audit row, so a player who later disputes "I rotated at 12:34 and my opening is broken" has a recoverable trail in audit_log alone instead of relying on log-line forensics.
- PATCH client_seed now validates the format (8-64 hex chars) so unicode, control characters, or `:` separators that would ambiguate the verifier's HMAC message string can no longer be saved.
- Provably-fair history page now degrades gracefully on garbage page parameters (?page=NaN, ?page=abc) and no longer leaks raw Postgres error messages to authenticated clients.
Spectator + replay privacy
- Spectator state no longer surfaces adrenaline counters or per-turn summon counts — these are alpha-strategy hints that competitive metagame analysis tooling could be built off of. Combatants still see them in their own action-planning UI.
- Persisted replays now scrub raw player ids out of turn-log strings before storage. Pre-fix the JSON-keyed identifier fields were already pseudonymized to player_1 / player_2 but the free-text log lines could still embed raw ids — a non-admin reader of the replay blob could correlate replays by matching id substrings.
Profile, guild, tournament, and battle UI hardening — public profile clarity, achievement toasts, action timeouts
A pass through tournaments, guilds, the profile sections, and the in-battle internals fixed a long list of small bugs and added the visible affordances that were missing. Headline items: viewing a stranger's profile now tells you why bio / display case / achievements are hidden ("add as friend to see more") instead of rendering blank sections; achievement toasts auto-dismiss after 6 seconds (and cap at 5 visible at once) instead of stacking forever; every in-battle action — summon, attack, ability, mulligan, end turn, forfeit — now has a 5-second "no response from server" safety net so a flaky socket can't leave the UI stuck pending; the forfeit button uses an in-app modal instead of the browser's native confirm() popup that iOS PWAs sometimes silently suppress; the tournament bracket marks forfeit / disconnect / bye matches with a small badge so admins and spectators can tell them apart from real wins; deck-builder save validates the 3-copies-per-card limit client-side before letting you press Save; and a list of guild-management quality-of-life polish items (kick / promote confirmation, donation amount cap, war-end refresh filtering, identical-message dedupe).
Public profile + achievement toasts
- Viewing another player's profile now shows an inline notice ("Add as friend to see this player's bio, display case, achievements, and recent activity") when those sections are gated by the friendship check. Pre-fix the sections just rendered blank and players thought their account was broken.
- Public profile now shows the player's division badge (Bronze / Silver / Gold / etc.) alongside ELO — was missing on other-player profiles even though it was on your own.
- Sharing a profile link now shows "Link copied!" / "Could not share" feedback instead of being silent.
- Achievement toasts now auto-dismiss after 6 seconds and cap at 5 visible at once. Pre-fix they stacked forever and players who unlocked 5 achievements in a battle had to manually click each ×.
In-battle robustness
- Every player action — summon, evolve, attack, ability, equip, set trap, place field, use object, mulligan, end turn, go to battle, forfeit — now races against a 5-second server-ack timeout. If the server doesn't respond (network drop, server hiccup) you get a "No response from server" toast instead of the action sitting visually pending forever.
- Forfeit button uses an in-app confirmation modal showing the consequences (loss for record + rating). Replaces the browser's native confirm() popup that iOS PWA installs sometimes silently suppress, leading to "I clicked forfeit and nothing happened" reports.
- Mulligan decision can no longer multi-fire: a rapid double-tap on Keep/Mulligan now sends only one decision to the server.
- Spectator orientation hardening: the in-battle action handlers now no-op for spectators as a defense-in-depth layer over the existing UI gating.
- Battle tooltips no longer flicker shut on Android because of the touch+click race; status / zone tooltips now stay open on tap until you tap somewhere else.
- Mulligan timer screen and the in-app confirm modals now have proper aria-modal labelling for screen-reader users; previously they were unannounced.
Tournaments + guilds
- Tournament bracket now shows a small FF / DC / BYE badge on matches that ended by forfeit, disconnect, or auto-advance — pre-fix those rendered identical to real wins.
- Guild kick / promote / demote actions now require a second click to confirm (button changes to "Click again" with a 4-second auto-cancel). Pre-fix a single accidental click on Kick removed the member instantly.
- Guild chat: Enter spam can no longer post identical messages within 5 seconds; emoji and RTL names render their first character correctly in avatar bubbles.
- Guild pool donation amount is now bounded (1 to 99,999 fragments) and rejects garbage values like NaN that the form could previously generate.
- Guild war end-of-war notification now only refetches our own guild's wars instead of every guild_war_ended event in the world.
Profile sections
- Display Case selection: ordering is now user-controllable (←→ buttons on selected cards); cards traded away or dissolved no longer linger as zombie selections; the per-page collection cap was raised to 1000 cards with a clear notice if you have more than that.
- Profile name / avatar / bio now save independently — pre-fix clicking Save next to your avatar (after also typing a new name) submitted both, accidentally burning the 30-day name change cooldown.
- Avatar URL field now shows an inline preview as you type, with a fallback when the URL is invalid.
- Password change client validation now mirrors the server policy (min 8 chars + at least one digit + at least one letter + must differ from current). Pre-fix the only check was 8-char minimum.
- 2FA setup secrets (TOTP URI, backup codes, password fields) now wipe from React state on unmount so browser extensions can't scrape them from a stale snapshot.
- Provably-fair Rotate Seed: failures now surface a clear error (was silent), and the revealed seed gets a "Copy seed" button + a warning that the next rotation will reveal a new one and this one cannot be re-displayed.
- Account deletion request: surfaces "Deletion service is temporarily unavailable" upfront if the backend isn't configured, and double-clicks are rejected synchronously so two deletion rows can't be scheduled.
Battle UI fixes — spectator view, mulligan timer, deck builder hardening
A pass through the in-battle screen, the deck builder, and the card / trade modals fixed a long list of small but visible bugs. The headline items: spectators were seeing the battle mirrored to the wrong side (their view labelled the opponent as "you"), the mulligan timer reset to 30 every time anything happened on the board, the queue size showed blank when joining casual matchmaking, and trade history's wishlist 🎯 markers showed the wrong player's wishlist. The card modal now cancels in-flight fetches when you flip through a gallery, the deck builder caps how many pages it loads (so a server hiccup can't lock the screen), and copying a deck export now actually tells you whether it worked.
In-battle correctness
- Spectators now see the battle with a fixed orientation — challenger always at the bottom, opponent always at the top — instead of having the view randomly mirror to the wrong side and label the wrong player as you.
- Mulligan timer no longer resets to 30 every time the opponent acts. It now starts at 30 once when the mulligan phase begins and counts down honestly from there.
- Card tooltips no longer get wiped mid-read every time the opponent's HP ticks. They only close on real turn / phase changes now.
- If you tab out of a battle and come back, the in-battle screen requests a fresh state from the server instead of rendering whatever stale snapshot it had.
- Leaving a battle from the lobby (before the opponent joins) now actually frees your slot on the server — pre-fix you could get stuck in "already in a battle" trying to requeue.
- Casual queue size now displays correctly on join (was showing blank / NaN due to a payload mismatch).
- Battle history (the resolved list under your decks) now updates live when a battle ends — no more refresh required to see your latest match.
- Rematch banner now only shows after a battle that resolved naturally (win / loss). It used to show after forfeits and opponent disconnects too, which was confusing.
- Tournament report-result now uses an in-app modal that shows the opponent's name. Native browser confirm() popups are gone (one less spot the design guidelines didn't apply).
Deck builder + card modal
- Deck builder now validates the 3-copies-per-card limit before letting you press Save (was checked at add time only — corrupt or imported decks could slip through with 4+ copies and silently fail server-side).
- Loading a giant collection in the deck builder is now bounded — there's a hard cap on how many pages it'll fetch (50, well above any realistic collection) so a server pagination bug can't lock the screen.
- Card modal cancels in-flight network requests when you flip to another card. Pre-fix the previous card's wishlist / instances / evolutions could resolve last and overwrite the new card's data.
- Locking / unlocking a card copy now surfaces an error if the server rejects it. Used to silently swallow the failure.
- Trade history shows a Retry button if the load fails, instead of a fake "No completed trades yet" forever.
- Copying a deck export to clipboard now shows a Copied! / Copy failed toast. Used to be silent.
- Live battles list (the spectate panel) no longer shows a Watch button on your own battles, and pauses its 15-second poll when the tab is in the background.
Battle action feedback — no more frozen UI when the server hiccups
When you summon a creature, attack, or end your turn, the client now waits up to 5 seconds for the server to confirm. If the confirmation never arrives — server hiccup, network drop, dropped packet — you get a clear "No response from server — check your connection" message instead of a button that just stops responding. If the server rejects the action (out of phase, illegal target, rate-limited, etc.) the rejection reason now reaches you immediately even on flaky connections, instead of riding the next state push and arriving seconds late. The other (less critical) actions are unchanged. Spanish and English locales also got `not-found` and `loading` pages so a stale link or a slow load lands on a localised page instead of the framework default.
Battle action feedback
- Summon, attack, and end-turn now show an error toast if the server doesn't acknowledge within 5 seconds, instead of leaving the action visually pending.
- Server-side rejections (out of phase, illegal target, rate-limited) now reach you immediately even when state push is delayed.
- Localised `not-found` and `loading` pages — stale links and slow loads now land on a Spanish or English page instead of the framework default.
Privacy + safety tightening
- Profile bio, display case, recent activity, and achievements are now visible only to friends and to yourself. Strangers see your name, rating, and division but not your collection wall — closes a long-standing complaint that anyone with your user URL could browse your private profile content.
- Blocking a player is now a hard hide — your profile returns 404 to anyone you've blocked (and theirs to you), instead of just hiding tabs while still leaking the basics.
- Friend activity feed no longer surfaces fields that the original event didn't intend to share. Each event type has a strict allowlist (rare pull, achievement, battle result, trade, guild join, tournament win) so nothing extra leaks even if a future event payload changes shape.
- Leaderboard now requires sign-in. Anonymous bots can no longer scrape the rating table.
Notifications + import
- You can now mark individual notifications as read instead of "all or nothing" — the inbox now passes the specific ids you tap.
- Deck import now caps the pasted text at 4 KB so a giant accidental paste returns a clear error instead of stalling. The cap is large enough for any legitimate deck list.
Real-time push fixes
- Friend requests, accepts, and rejects now actually update the badge counters and friend list without a manual refresh. The wire vocabulary between client and server had drifted apart and every notification was being silently dropped at the server allowlist; the canonical verbs are now shared and the relay forwards them end-to-end.
- Trade offers, accepts, declines, and cancellations also push to the other player live now — the badge ticks up the moment the offer arrives, instead of waiting for the next page load.
- Trade UI: the 🎯 markers now correctly show cards the OTHER player wants (used to silently show your own wishlist on both columns).
Tab UX + battery savings
- Hidden tabs no longer drain bandwidth in the background — the leaderboard and challenges tabs now pause their live subscriptions when you're on another tab and resume when you come back.
- Leaderboard now shows a clear error + retry button if the load fails, instead of the perpetual "Loading leaderboard…" spinner.
- Challenge / Accept buttons in Battles no longer lock forever when the socket hiccups. After 15 seconds without a server ack you get a "Network timeout — try again" toast and the button releases.
- Tournament "I Won" confirmation is now a styled in-app dialog showing the opponent's name, instead of the browser's native confirm() popup. Same for visual polish + accessibility (aria-modal).
- Support ticket replies are now a multi-line textbox (Shift+Enter for newline, Enter to send) instead of a single-line input that silently truncated paragraph replies.
SEO sweep — sitemap, hreflang, canonical URLs across all locales — Phase 1 i18n fully closed
The bilingual public surface is now wired up for search engines. Every Spanish and English page emits a `<link rel="canonical">` pointing at its own locale URL plus an `<link rel="alternate" hreflang>` map covering both locales and an `x-default` fallback, so Google can pair the /es and /en variants of the same page instead of treating them as duplicates. A new `/sitemap.xml` enumerates every Phase 1 public page in every locale with the same hreflang alternates per entry. With this release Phase 1 of the i18n project is fully closed: 4 legal pages bilingual, 7 marketing pages bilingual, controlling-version clauses live in both languages, locale switcher in both headers, hreflang plumbing in metadata and sitemap, and a written workflow template for any future page.
SEO + future-page template
- New `/sitemap.xml` enumerates every Phase 1 public page (landing, about, contact, FAQ, how-to-play, showcase, changelog, legal/cookies, legal/notice, legal/privacy, legal/terms) in both Spanish and English, with per-entry hreflang alternates so search engines pair the locale variants properly.
- Every translated page now emits a per-page canonical URL pointing at its own /es or /en path and an hreflang `languages` map covering both locales plus `x-default` (defaulting to Spanish), so duplicate-content penalties are off the table.
- Future-page workflow template added to `context/refs/marketing-hero-draft.md` — every new public page added after Phase 1 must ship in BOTH Spanish and English from the first commit. The template covers the dual-locale draft workflow, TCG vocabulary, register rules, and the required SEO wiring.
- Phase 1 i18n closes here: 11 public pages × 2 locales fully bilingual end-to-end, plus the SEO infrastructure, controlling-version clauses, locale switcher, and the written contract for future bilingual pages.
Marketing pages, mastery, tournaments, and a sweep of UX gaps
A big batch of player-facing work landed across the spring. The headline items: a public marketing surface for new visitors, a card mastery system that rewards loyalty to specific cards, a tournament bracket viewer that finally lets you see how a tournament is unfolding, and a long list of small UX fixes that close gaps players have been pointing out for months. Below is the rough shape of what changed, organised by area. Patch notes for individual cards and balance tweaks live in the in-game patch feed; this changelog covers the structural and surface-level stuff.
Marketing pages (public)
- New landing page that actually explains the game, instead of dropping you straight onto a login form. Returning players get a one-click "Log in" in the sticky header.
- About, How to Play, FAQ, Contact, Featured Cards, and this Changelog — all reachable from the same header on every public page.
- Cookie consent banner upgraded to the IAB TCF v2.3 standard so EU players get the proper opt-in flow with all the per-purpose toggles.
Crafting and collection
- New Forge tab dedicated to crafting cards from the fragments you earn by dissolving duplicates.
- Mastery system: as you play with the same card across battles, it levels up. Once a card hits level 5, you get a 10% crafting discount on copies of it. At level 10 it gets a "Mastered" badge.
- Crafting cost now shows the discounted price inline at the craft button, so you can see the saving without diving into card detail.
- Wallet bar now shows your fragment stash split per rarity, instead of one aggregate number. Easier to see at a glance whether you can afford a craft.
- Card sets browser: see every set, what cards belong to it, and your progress through it. Helps you target which sets to chase.
- Persistent "Verify past pulls" link in the Gacha tab — tap it any time to audit any pack you have ever opened against the provably-fair RNG record.
Battles and competitive
- Tournament bracket viewer: when you click into a single-elimination tournament, you now see the full bracket tree with every match, who is playing whom, and who has won so far.
- Public guild tournament standings: non-participants browsing a guild tournament can see the standings panel without being enrolled.
- Battle replays now have their own shareable URL. Open the replay in a new tab, send the link to a friend, point at the moment you want to talk about. Works for any battle either of you played in.
- Element field-spell weather effects now show up properly in damage calculations during battle — fire fields boost fire creatures, water fields boost water, etc.
- Daily and weekly challenges have a streak system: completing them on consecutive days builds a streak with milestone rewards at 7, 14, and 28 days.
- Battle stats panel shows per-element breakdowns + period filtering, so you can see how your fire decks have been doing this week vs last month.
- AI battle spam fix: a race condition that allowed double-starting AI battles is closed. No more orphan match windows.
Account and trust
- Two-factor authentication available from your profile security section. Standard authenticator apps (Authy, Google Authenticator, 1Password, Bitwarden) all work.
- Self-serve account deletion lives in the profile section — no support ticket, no won't-let-you-leave dance.
- Self-serve data export: download a copy of everything we hold about your account.
- Session security tightened: changing your password or enabling 2FA now invalidates all your other active sessions automatically. If someone was logged in as you on a different device, they get kicked.
- Email verification flow improvements: clearer messaging when verification is pending, retry-with-cooldown on the resend button.
Performance and reliability
- Battle reconnection improvements: if you drop mid-match for under a minute, you reconnect to the same game state with no penalty.
- Server-side deck-out handling rewrites — the slow control decks that win by exhausting your opponent's library now work cleanly across reconnects.
- Tournament forfeit timeouts moved to a durable job queue, so a server restart no longer eats a forfeit-pending tournament.
- Offline detection banner: if your connection drops, you see a clear banner explaining what happened, instead of the UI silently failing.
Notes for next time
- We are still adding cards, sets, and modes. The world has plenty of room to grow.
- Discord invite link is being set up — when it lands the contact page will surface it.
- If you spot something here that does not match what you see in-game, tell us — most likely we shipped it differently than the note suggests.
All marketing pages now available in Spanish — Phase 1 complete
Six more marketing pages are now bilingual, closing out Phase 1 of the marketing translation effort. /es/about renders the team / vision / game-world write-up in Spanish. /es/contact translates every channel. /es/faq covers all 12 Q&A items. /es/how-to-play translates the 10-minute rundown. /es/showcase renders the curated 7-card showcase with bilingual card names + flavor blurbs. /es/changelog (this page) is now bilingual too — every ChangelogEntry carries both Spanish and English copy together in the source so future updates land in both locales atomically. The four legal pages shipped bilingual earlier this month, so the public surface is now fully bilingual end-to-end.
Marketing pages translated
- About page (Sobre Nosotros) translated to Spanish — hero 'Hecho entre amigos, por gusto', team / vision / game-world sections all mirror the English source 1:1.
- Contact page translated — section headings 'Correo / Discord / Tickets de soporte / Una nota sobre los tiempos de respuesta', back / FAQ navigation buttons.
- FAQ translated — all 12 Q&A items across three sections (Sobre el juego / Cómo jugar / Cuenta), including questions about rewarded ads, account deletion, and where data is stored.
- How to Play translated — three sections (Mecánicas, Tipos de cartas y elementos, Flujo de batalla — turno a turno). TCG glossary mazo / sobre / carta / PV / zona de monstruo / mulligan kept as-is.
- Featured Cards (showcase) translated — every card name + 50-150 word marketing blurb. Element + monster-class labels also translated (Fuego/Agua/Viento/Tierra/Metal/Rayo/Neutral; Dragón/Bestia/Constructo/Leviatán/Alado; slime preserved as TCG term).
- Changelog page itself bilingual — every ChangelogEntry now carries both Spanish and English copy together in the source. MAINTAINER NOTE in the page source updated: from this release on, every new ChangelogEntry MUST land in BOTH Spanish AND English in the same commit. Single-locale entries are forbidden.
- All Spanish versions use the indie/playful voice from the landing page — informal tú throughout, TCG vocabulary preserved (sobres, mazos, cartas, tablero, dorsos, gremios).
- Locale switcher in the marketing header lets you flip between /es and /en versions of any page without losing your place. Phase 1 marketing translations now complete.
Landing page now available in Spanish — first marketing page translated
The homepage hero is now bilingual. Spanish-speaking visitors landing on /es see the full marketing pitch in Spanish — anti-pay-to-win positioning, daily-free-packs promise, multi-pace modes (casual, ranked, AI, just collecting), element chips, and the 'built among friends' footer — all in informal tú voice. English visitors keep the original wording at /en. This is the first marketing page to land in two languages; the rest are queued up next.
Marketing pages
- Landing page (homepage hero) translated to Spanish — every paragraph mirrors the English source 1:1: hero badge ('Gratis · Sin pay-to-win'), headline, subhead, three 'why play' tiles, 'Cómo funciona' section with element chips (Fuego, Agua, Viento, Tierra, Metal, Rayo, Neutral), and the 'Hecho entre amigos por gusto' footer.
- Spanish version uses the same playful indie register as the English source — informal tú throughout, TCG vocabulary players already know (sobres, mazos, cartas), no marketing-speak.
- Marketing translations rolling out next: about, how-to-play, FAQ, contact, showcase, changelog. Each will follow the same per-page review gate.
- Locale switcher in the marketing header lets you flip between /es and /en versions of the landing page without losing your place — same flow already in use on the legal pages.
Privacy Policy now available in Spanish — Phase 1 legal coverage complete
The Privacy Policy is now bilingual, closing out Phase 1 of the legal-page localization effort. Spanish-speaking visitors land on a fully translated /es/legal/privacy, English visitors keep the original wording at /en/legal/privacy. With this release, all four legal pages — Terms of Service, Cookie Policy, Legal Notice, and Privacy Policy — ship in both English and Spanish, each carrying the controlling-version notice that names the English text as the legally authoritative version. Phase 1 legal coverage is now fully complete.
Legal pages
- Privacy Policy translated to Spanish — every section, every data table, every right enumeration mirrors the English source 1:1 (data controller, categories of data collected, purposes and legal basis, data sharing with processors, international transfers, retention periods, GDPR data-subject rights including access/rectification/erasure/portability/objection/restriction/withdrawal of consent, GDPR Article 21 objection box, AEPD complaint route, GDPR Article 33/34 breach notification, CCPA/CPRA rights for California users, cookie disclosure, children-under-16 protections, automated-decision-making disclosure).
- Spanish version uses canonical RGPD terminology throughout: 'responsable del tratamiento', 'interesado', 'encargado del tratamiento', 'base legal', 'consentimiento', 'interés legítimo', 'Delegado de Protección de Datos (DPD)', plus the full Spanish names for each GDPR right ('derecho de acceso', 'derecho de supresión', 'derecho de oposición', 'derecho a la portabilidad', 'derecho a la limitación').
- Phase 1 legal coverage now fully complete: Terms, Cookies, Legal Notice, and Privacy all available in both Spanish and English with controlling-version notices in place.
- Locale switcher in the legal header lets you flip between /es/legal/privacy and /en/legal/privacy without losing your place — same flow already in use on the other three legal pages.
Terms of Service now available in Spanish — three legal pages bilingual
The Terms of Service page is now bilingual. Spanish-speaking visitors land on a fully translated /es/legal/terms, English visitors keep the original wording at /en/legal/terms. Both versions carry the same controlling-version notice at the top, making clear that the English text remains the legally authoritative version and the Spanish translation is provided for convenience. Three of four legal pages — Terms, Cookies, and Legal Notice — now ship in both languages; Privacy is the next page in line.
Legal pages
- Terms of Service translated to Spanish — every clause, every list item, every legal definition mirrors the English source 1:1 (eligibility, gacha mechanics, ad disclosures, IP ownership, DSA Article 17 appeal flow, Belgian gacha note, EU consumer right of withdrawal).
- Phase 1 legal-page coverage is now three-quarters complete: Terms, Cookies, and Legal Notice all available in Spanish and English. Privacy Policy translation is the remaining page.
- Controlling-version banner present at the top of every translated legal page in both locales: the English text remains legally binding and translations are explicitly marked as non-authoritative for legal interpretation.
- Locale switcher in the legal header lets you flip between /es and /en versions of any legal page without losing your place.
Cookie Policy and Legal Notice now available in Spanish
The Cookie Policy and Legal Notice pages are now bilingual. Spanish-speaking visitors land on fully translated /es/legal/cookies and /es/legal/notice, English visitors keep the original wording at /en/legal/cookies and /en/legal/notice. All four pages carry a controlling-version notice at the top making clear that the English text is the legally authoritative version and the Spanish translation is provided for convenience. Terms and privacy will follow the same pattern in upcoming releases.
Legal pages
- Cookie Policy translated to Spanish — every section, every cookie table, every browser-settings note is mirrored 1:1 with the English source.
- Legal Notice (Aviso Legal) translated to Spanish — operator identity, NIF/CIF cell, Spain-based address, applicable LSSI-CE/RGPD/LOPDGDD/DSA legislation list, intellectual property, liability and dispute-resolution sections all mirrored 1:1.
- Controlling-version banner added at the top of both legal pages in both locales: the English text remains the legally binding version and translations are explicitly marked as non-authoritative for legal interpretation.
- Locale switcher in the legal header lets you flip between /es/legal/* and /en/legal/* without losing your place.
Queres sugerir o próximo passo? A página de contacto tem email e Discord. Queres acompanhar as atualizações? Marca esta página — atualizamos quando uma mudança visível ao jogador chega.