  /* ===== Fontes =====
     Self-hosted (sem dependência de CDN externo): Chakra Petch é a fonte-base
     da interface (legível em tamanho pequeno pro HUD), Cinzel é a fonte dos
     títulos (h1/h2 de painéis, modais e fim de jogo) e Metal Mania é
     exclusiva do nome do jogo (#menuTitle) — ela só é legível em texto
     grande, então não deve aparecer em mais nenhum lugar. Arquivos em
     public/assets/fonts/, dois subsets cada (latin + latin-ext, pro acento
     do português) via unicode-range. */
  @font-face {
    font-family: 'Metal Mania';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../assets/fonts/metal-mania-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
  }
  @font-face {
    font-family: 'Metal Mania';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../assets/fonts/metal-mania-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-024F, U+1E00-1EFF, U+2020, U+2113;
  }
  @font-face {
    font-family: 'Cinzel';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../assets/fonts/cinzel-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
  }
  @font-face {
    font-family: 'Cinzel';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../assets/fonts/cinzel-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-024F, U+1E00-1EFF, U+2020, U+2113;
  }
  @font-face {
    font-family: 'Chakra Petch';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../assets/fonts/chakra-petch-400-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
  }
  @font-face {
    font-family: 'Chakra Petch';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../assets/fonts/chakra-petch-400-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-024F, U+1E00-1EFF, U+2020, U+2113;
  }
  @font-face {
    font-family: 'Chakra Petch';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../assets/fonts/chakra-petch-600-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122;
  }
  @font-face {
    font-family: 'Chakra Petch';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../assets/fonts/chakra-petch-600-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-024F, U+1E00-1EFF, U+2020, U+2113;
  }

  /* ===== Paleta =====
     Fonte única das cores da interface: nenhuma regra abaixo deve escrever um
     hex literal — use uma variável daqui (ou crie uma, se a cor for realmente
     nova). Cores iguais ou quase iguais foram unificadas na mesma variável de
     propósito: antes existiam vários cinzas levemente diferentes (#2b2b3d,
     #2e2e2e, #2f2f40…) que apareciam misturados na mesma tela.

     Toda a interface é escala de cinza + o vermelho da identidade. Os cinzas
     eram azulados/arroxeados (#2b2b3d, #9a9ab0, #52527a…) e foram
     neutralizados; azul só sobrou onde a cor é *informação* e não decoração
     (`--cor-azul-*`, `--class-color` de cada classe em shared/classes.js). */
  :root {
    /* ----- Eixos da tela -----
       Largura e altura *do conteúdo*, que não são necessariamente as da tela
       física: em modo touch com o aparelho em retrato o app inteiro é girado
       90° por CSS (`body.tela-girada` mais abaixo, ver public/js/orientation.js
       e public/js/screenRotation.js) e os dois eixos trocam de papel. Nenhuma
       regra deste arquivo deve usar `100vh`/`100dvh`/`100vw` direto por causa
       disso — use estas duas variáveis, que já sabem se a tela está girada.
       `dvh` (e não `vh`) porque a barra de endereço do navegador móvel entra e
       sai da viewport; a declaração dupla é o fallback pra quem não suporta
       `dvh`, mesmo padrão que as regras usavam antes. */
    --largura-tela: 100vw;
    --altura-tela: 100vh;
    /* Altura da barra superior da tela inicial (#menuTopbar). É variável e não
       valor fixo porque duas regras diferentes precisam do *mesmo* número: a
       altura da própria barra e o `padding-top` de #menu, que é o que impede
       qualquer painel do menu de passar por baixo dela. São 40px de botão mais
       os 8px de `padding` de cima e de baixo. É uma fileira só: Talentos,
       Monstruário e Relicário já foram uma segunda faixa com altura própria
       (--altura-subnav), que sumiu quando eles viraram abas irmãs das outras,
       e as abas saíram depois com o acampamento. */
    --altura-topbar: 56px;
    /* Medidas do container padrão das telas do menu (.menu-painel). São
       variáveis, e não números escritos em cada painel, porque o ponto delas é
       serem *as mesmas* nas cinco telas: mudar aqui move todas juntas, que é o
       que impede a moldura de mudar de tamanho quando o jogador troca de aba.
       A largura é a da tela mais exigente (o Relicário, que abre a lista em
       três colunas de 280px + os vãos + o padding); as outras sobram nela de
       propósito. A altura é só um teto: o padrão é o painel ocupar a altura
       inteira da visão (ver `height: 100%` em .menu-painel), e o teto é o que
       impede a moldura de virar uma faixa esticada numa tela muito alta. */
    --largura-painel-menu: 1040px;
    --altura-painel-menu: 620px;
    /* Largura em px da arena já redimensionada, escrita por gameScale.js no
       `#game-wrap`. Os textos desenhados *dentro* da arena (overlays de
       espera/contagem regressiva/fim de jogo) escalam por ela, não por
       `--largura-tela`: a arena mantém 1000x600 de proporção e quase nunca
       ocupa a tela inteira — num celular deitado a tela é bem mais larga que
       ela, e o texto saía enorme dentro de uma arena pequena. O valor aqui é
       só o fallback de antes de a primeira partida abrir. */
    --largura-arena: var(--largura-tela);
    /* Véu do portão de tela cheia (modo touch): escuro o bastante para o texto
       e o botão lerem por cima de qualquer tela, translúcido o bastante para o
       jogo continuar visível atrás. */
    --cor-veu-tela-cheia: rgba(0, 0, 0, 0.80);
    /* Véu por cima da textura de arena usada como fundo de botão (quadrado de
       nível e linha de sala, ver "Textura de arena" no fim do arquivo): a
       textura é o que identifica o nível, mas o texto por cima dela é branco
       miúdo — sem escurecer, "Fenda de Gelo" some no chão de gelo. O tom
       fraco é o do hover, onde a textura ganha um pouco mais de presença. */
    --cor-veu-textura-arena: rgba(0, 0, 0, 0.74);
    --cor-veu-textura-arena-fraco: rgba(0, 0, 0, 0.58);
    /* Véu da tela de fim de partida, por cima do tijolo de masmorra
       (`#gameOverOverlay`). É bem mais escuro que o mesmo tijolo no fundo do
       menu (0.6): aqui a parede é só clima atrás do resultado — o que precisa
       ser lido é "Você ganhou"/"Você perdeu" e o relatório, e um tijolo claro
       demais disputava com os dois. Como o tijolo agora fica parado (ver
       `#gameOverOverlay`), ele é só textura de fundo: escurecer mais ainda
       (0.88) tira o pouco de disputa que sobrava com o título. */
    --cor-veu-endgame: rgba(0, 0, 0, 0.88);
    /* Véu do fim de partida **na vitória** (`#gameOverOverlay.vitoria-sobre-arena`):
       sem o tijolo, direto sobre a última cena da arena, que o render continua
       desenhando por baixo. Mais leve que o do tijolo (0.88) porque o que está
       atrás é a arena da partida que acabou de ser vencida — ela tem de continuar
       reconhecível —, e escuro o bastante para o relatório, a barra e a medalha
       seguirem legíveis por cima dos fogos. O tom puxa um fio de azul, o mesmo do
       fundo do mural (`#12121a`), para não ler como cinza apagado. */
    --cor-veu-vitoria: rgba(10, 10, 16, 0.72);
    /* Sala de espera da Defesa da base: o chão do nível a 50% de brilho
       (ver `#waitingOverlay.horde`). */
    --cor-veu-sala-espera: rgba(0, 0, 0, 0.5);

    /* Neutros: superfícies, da mais funda para a mais alta */
    --cor-fundo: #1d1d1d;
    --cor-fundo-arena: #202020;
    /* Véu translúcido da contagem regressiva sobre a arena (deixa os
       jogadores visíveis atrás do número) — mesmo tom de --cor-fundo-arena. */
    --cor-fundo-arena-veu: rgba(32, 32, 32, 0.85);
    --cor-superficie-baixa: #1c1c1c;
    --cor-superficie-icone: #242424;
    --cor-superficie: #2a2a2a;
    --cor-superficie-alta: #2e2e2e;
    --cor-superficie-forte: #333;
    --cor-superficie-elevada: #3a3a3a;
    --cor-superficie-hover: #3f3f3f;
    --cor-superficie-hover-alta: #4a4a4a;
    /* Botões secundários sobre painel de pedra ("Patch"/"Créditos" na tela
       inicial, os atalhos de leitura da Defesa da base). Um cinza claro o
       bastante para a silhueta do botão se separar do painel escuro atrás:
       nos tons de superfície (#2a2a2a–#333) o botão sumia no fundo e só o
       rótulo aparecia. */
    --cor-botao-secundario: #565656;
    --cor-botao-secundario-hover: #676767;
    --cor-modal-fundo: #100f0e;
    /* Barra de navegação da tela inicial (#menuTopbar): fundo quase preto e
       botões um degrau acima dele. São tons próprios, e não os
       `--cor-botao-flutuante-*` de um botão solto sobre a cena, porque aqui a
       peça não flutua — é uma faixa sólida, e os botões precisam se destacar
       *dentro* dela em vez de sobre o fundo do jogo. */
    --cor-barra-fundo: #050505;
    --cor-barra-botao: #121010;
    /* Fundo do palco do acampamento, na sobra de menos de uma escala que o
       canvas ampliado por fator inteiro deixa nas bordas. */
    --cor-acampamento-fundo: #0c0b0a;
    --cor-barra-botao-hover: #1e1b1b;
    --cor-painel-fundo: rgba(38, 35, 34, 0.78);
    /* Painéis "de pedra" (menu principal e ranking): cinza bem escuro e opaco,
       sem textura, pra lerem como um bloco de pedra sólido. */
    --cor-painel-pedra-veu: #100f0e;
    /* Contorno da laje do container do menu (.menu-painel). A pedra do painel
       é quase preta, então o contorno tem de ser mais **claro** que ela, e não
       mais escuro como nos botões coloridos: escurecer o quase-preto não
       desenha borda nenhuma sobre a parede de pedra, que também é escura. */
    --cor-painel-pedra-contorno: #35302c;
    --cor-pedra-borda: #100f0e;

    /* Prata dos pregos dos cantos do container padrão do menu (.menu-painel).
       Três tons e não um: o prego é um pixel de luz, um corpo e um pixel de
       sombra — é o degrau de cor que faz uma peça de 8px ler como cabeça de
       prego redonda em vez de quadradinho cinza. */
    --cor-prego: #8b8d94;
    --cor-prego-luz: #dfe2e8;
    --cor-prego-sombra: #4a4c52;

    /* Neutros: bordas e cinzas médios */
    --cor-borda: #444;
    --cor-borda-media: #555;
    --cor-borda-clara: #666;
    --cor-borda-hover: #777;
    --cor-cinza-medio: #6b6b6b;
    --cor-cinza-claro: #8a8a8a;

    /* Texto */
    --cor-branco: #fff;
    --cor-texto: #eee;
    --cor-texto-suave: #ccc;
    --cor-texto-medio: #bbb;
    --cor-texto-fraco: #9a9a9a;

    /* Identidade vermelha (botões, bordas de modal, seleção, foco) */
    --cor-vermelho: #8b0000;
    --cor-vermelho-hover: #6e0000;
    --cor-vermelho-selecao: #6e2020;
    --cor-vermelho-destaque: #ff4d4d;

    /* Feedback: erro, sucesso, tempo/ouro, empate */
    --cor-erro: #e63946;
    --cor-erro-forte: #c0392b;
    --cor-erro-forte-hover: #a5322a;
    --cor-erro-fundo: #a12a35;
    --cor-erro-claro: #ff8b93;
    --cor-erro-texto: #ff6b6b;
    --cor-sucesso: #4ade80;
    --cor-sucesso-claro: #1cff7a;
    --cor-ouro: #facc15;
    /* A silhueta do bicho ainda não encontrado no Monstruário: um cinza de
       pedra do painel, e **não** a cor do bicho com menos brilho — a cor é
       metade da identidade dele na arena (ver drawInimigos em hordeRender.js),
       e escurecê-la entregaria o que a silhueta existe para esconder. É o mesmo
       valor que `OCULTO_COR` em monstruario.js usa no canvas do que não tem
       sprite. */
    --cor-silhueta-monstruario: #3a3a46;
    --cor-ouro-suave: #f4c542;
    /* O logo da tela inicial (#menuTitle) e a cena atrás dele (#cenaInicial):
       o contorno marrom-escuro e o brilho quente vêm da ilustração, onde o
       logo dourado fica sobre um céu vermelho. */
    --cor-logo-contorno: #2a0d04;
    --cor-logo-brilho: rgba(255, 140, 20, 0.35);
    --cor-logo-brilho-forte: rgba(255, 160, 30, 0.7);
    --cor-cena-inicial-fundo: #3a1208;
    --cor-cena-inicial-veu: rgba(0, 0, 0, 0.6);
    --cor-cena-inicial-veu-meio: rgba(0, 0, 0, 0.35);
    /* Os botões da capa (ver #menuViewHome, perto de #menuTitleExtras). */
    --cor-capa-jogar: #b3120f;
    --cor-capa-jogar-hover: #cc1d18;
    --cor-capa-jogar-subtitulo: #f3c9b8;
    --cor-capa-secundario: #2e2723;
    --cor-capa-secundario-hover: #40362f;
    --cor-cena-brilho-cajado: rgba(170, 255, 60, 0.8);
    --cor-cena-brilho-olhos: rgba(255, 220, 60, 0.95);
    --cor-empate: #d0c07f;

    /* Tiers de conquista (aba "Conquistas" do perfil): cada tier é um
       identificador visual próprio, igual --class-color já é o identificador
       de classe — por isso fogem da regra de "cinza + vermelho" do resto da
       interface. */
    --cor-tier-bronze: #b0763f;
    --cor-tier-prata: #d5dbe1;
    --cor-tier-ouro: #d9b544;
    --cor-tier-diamante: #7fd8e8;
    /* Lendário: o teto da escala, e o único tier fora da família de metais —
       roxo, porque nenhuma variação de dourado/prateado se distingue do ouro
       e do diamante a 40px de distância na grade. Ver a tabela de bandas em
       shared/achievements.js. */
    --cor-tier-lendario: #c46bff;
    --gradiente-tier-ouro: linear-gradient(135deg, #f5da7a, var(--cor-tier-ouro) 55%, #a9791f);
    --gradiente-tier-diamante: linear-gradient(135deg, #d6f6fb, var(--cor-tier-diamante) 55%, #2f9cb5);
    --gradiente-tier-lendario: linear-gradient(135deg, #eab6ff, var(--cor-tier-lendario) 55%, #6a1fa9);
    /* Texto por cima de uma cor de tier: as cinco são claras, então aqui o
       texto inverte e fica escuro (ver .achievement-badge.desbloqueada). */
    --cor-texto-sobre-tier: #1a1a1a;

    /* Raridade dos itens da Defesa da base (Relicário, tela de escolha e inventário
       do HUD). Foge da regra de "cinza + vermelho" pelo mesmo motivo dos tiers
       acima: a cor é o único jeito de dizer "isto é raro" sem escrever a
       palavra em três telas. Comum é cinza de propósito — é o que faz as outras
       três lerem como exceção.

       Os mesmos hexes estão em `CORES_RARIDADE` (public/js/raridades.js), que é
       o lado do canvas: CSS não alcança o canvas (ver "Paleta de
       cores" em public/css/CLAUDE.md). Mudar um lado sem o outro deixa o
       o HUD e o Relicário discordando sobre o mesmo item.

       O `-veu` é a versão translúcida que vira **fundo** do item. É translúcido
       e não um cinza tingido pronto porque o mesmo tom entra sobre duas
       superfícies diferentes — o painel do Relicário e o pedra do HUD sobre a
       arena — e um hex fixo só funcionaria numa delas. */
    --cor-raridade-comum: #c8c8c8;
    --cor-raridade-raro: #6cb6ff;
    --cor-raridade-quebra-jogo: #facc15;
    --cor-raridade-maldito: #dc2626;
    --cor-raridade-comum-veu: rgba(200, 200, 200, 0.10);
    --cor-raridade-raro-veu: rgba(108, 182, 255, 0.18);
    --cor-raridade-quebra-jogo-veu: rgba(250, 204, 21, 0.18);
    --cor-raridade-maldito-veu: rgba(220, 38, 38, 0.20);

    /* A moeda da Loja do Relicário. É o **mesmo** dourado de
       `--cor-raridade-quebra-jogo`, e a repetição tem nome próprio de propósito:
       as duas nunca aparecem juntas (o saldo mora na barra da Loja, a raridade
       mora na carta), e uma variável própria é o que permite mexer numa sem a
       outra. Ver "Paleta de cores" em public/css/CLAUDE.md. */
    --cor-moeda: #facc15;
    /* Tom de hover da moeda, para o botão de compra preenchido. */
    --cor-moeda-hover: #fde047;

    /* Cor do selo de família exclusiva (o aviso "só um por partida" no cartão da
       tela de escolha e na entrada do Relicário — ver avisoDeFamilia em
       raridades.js). **Uma por família**, e não uma cor de "aviso": as três
       usavam o mesmo `--cor-hud-tiro-pronto`, que é o mesmo hex de
       `--cor-raridade-quebra-jogo` — então a mesma cor dizia duas coisas
       diferentes no mesmo cartão (raridade duas linhas acima, família na última)
       e as três famílias diziam a mesma.

       Os três hues são escolhidos por **separação**, e não por semântica: eles
       ficam fora das quatro cores de raridade (cinza, azul, amarelo, vermelho),
       que é o que eles precisam não parecer, e a ~90° um do outro, que é o que
       eles precisam não parecer entre si. O par mais próximo de tudo isso é
       cano (verde-ciano) contra raro (azul) — e é aceitável porque o selo
       **escreve** o nome da família: a cor é pista redundante, não a informação.

       Verde-ciano para o `cano` (o mecanismo, metal), verde-limão para a
       `municao` (o que queima, congela e envenena), violeta para o `bocal` (a
       direção) e rosa para o `leque` (a trajetória). Família nova precisa de uma
       variável aqui e de uma classe `.familia-*` abaixo — o teste do catálogo
       confere as duas.

       O rosa do `leque` é o quarto hue e o mais apertado dos quatro: ele fica a
       ~75° do violeta do bocal (a família vizinha, e a que mais aparece no mesmo
       cartão) e a ~30° do vermelho de `maldito`. A separação contra o maldito não
       é de hue e sim de **luminosidade e saturação** — `#dc2626` é vermelho
       escuro e cheio, `#f472b6` é rosa claro —, e é aceitável pelo mesmo motivo
       que o par cano/raro já era: o selo **escreve** o nome da família, então a
       cor é pista redundante e não a informação. */
    --cor-familia-cano: #2dd4bf;
    --cor-familia-municao: #a3e635;
    --cor-familia-bocal: #a78bfa;
    --cor-familia-leque: #f472b6;
    /* Família que este cliente não conhece (servidor mais novo que o front): o
       selo continua aparecendo, em neutro. O aviso é a informação; a cor é o
       acabamento dela. */
    --cor-familia-outra: #bbb;

    /* Azuis: só onde a cor carrega informação (classe no HUD, escudo, links) */
    --cor-azul-classe: #7dd3fc;
    --cor-azul-escudo: #4aa8ff;
    --cor-azul-link: #7fb2d0;
    --cor-azul-ranking: #9ecbff;

    /* Cores do HUD (vida, escudo e barra de tiro). Ficam separadas das
       variáveis genéricas (--cor-erro, --cor-azul-escudo, --cor-sucesso)
       porque o HUD é lido de relance no meio da partida e precisa de tons
       vivos, enquanto as mesmas cores na interface (erro de formulário,
       vitória no ranking) não seguem esse ajuste. */
    --cor-hud-vida-texto: #d4212c;
    --cor-hud-escudo: #196fb5;
    /* Mesmas cores da prévia de mira (AIM_PREVIEW_COLOR/_COOLDOWN em
       render.js): vermelho carregando, amarelo pronto — a barra de cooldown
       e a mira precisam concordar sobre o que "pronto para atirar" parece. */
    --cor-hud-tiro: #ef4444;
    --cor-hud-tiro-pronto: #facc15;
    /* Barra do cano de cadência (.horde-cano-fill). Não reusa o par
       vermelho/amarelo acima de propósito: as duas barras ficam coladas no
       mesmo cartão, e com a mesma paleta o jogador não distinguia de relance
       qual estava lendo. Verde/vermelho é o par mais separável do amarelo da
       recarga, e mantém a leitura "verde = pode atirar". */
    --cor-hud-cano-pronto: #22c55e;
    --cor-hud-cano-vazio: #ef4444;
    /* Barra do rolamento (.horde-rolamento-fill) pronta. O trilho enchendo usa
       o azul do escudo (--cor-hud-escudo); pronto ele acende no ciano, que é o
       terceiro par do cartão — os outros dois já são vermelho/amarelo (recarga)
       e verde/vermelho (cano). */
    --cor-hud-rolamento-pronto: #38bdf8;
    /* Selo da "Lança do vazio" pronta (.horde-lanca-selo). É o violeta do ícone
       e do FX do item (public/js/itemIcones.js) — a mesma cor que o jogador
       acabou de ver acender no personagem —, e é o quarto hue do cartão local:
       os três trilhos já ocupam vermelho/amarelo, azul/ciano e verde/vermelho. */
    --cor-lanca-pronta: #a78bfa;
    --cor-lanca-pronta-brilho: #f5f3ff;
    /* Casca do talento "Alicerce" sobre a barra da base (.horde-base-casca). É
       o azul do Bastião (ARVORES em shared/talentTree.js), a árvore que a
       concede: a barra nasce de um talento, e a cor é o que a liga a ele. */
    --cor-casca-base: #4a9be8;
    /* A muralha da barra de vida da base (.horde-base-track/-fill). A barra
       era um retângulo de cor chapada — a leitura era de barra de carregamento,
       e o que ela mede é uma muralha de pedra sendo derrubada. Três tons e não
       um, porque uma fiada de blocos é feita de três coisas: a junta entre um
       bloco e outro, o topo iluminado de cada bloco e a face quebrada na ponta
       de onde a muralha já caiu.

       São `rgba` e não hex: os três entram por cima de **duas** cores
       diferentes (o verde da vida e o vermelho do estado crítico) e por cima do
       trilho vazio, e um tom fixo só serviria para uma delas. Pelo mesmo motivo
       eles não têm par claro/escuro por estado — é a mesma pedra em toda a
       barra, só muda o que está por baixo. */
    --cor-muralha-junta: rgba(0, 0, 0, 0.38);
    --cor-muralha-luz: rgba(255, 255, 255, 0.16);
    --cor-muralha-quebra: rgba(0, 0, 0, 0.55);
    /* Contorno dos ícones das fileiras de vida/escudo (ver
       .resource-icon-outline) — separa visualmente ícones concatenados/
       sobrepostos (empilhamento por largura máxima). Desenhado como pixels
       extras por baixo do preenchimento (hud.js), não `filter`, então é preto
       puro, sem tom. */
    --cor-contorno-icone-hud: #000;

    /* Fundo dos botões flutuantes de canto (volume, música): mais claro que
       as superfícies normais, de propósito, pra ter contraste sobre
       qualquer fundo (arena escura ou parede clara do menu). Opaco: era
       translúcido para o `backdrop-filter: blur()` que esses botões
       carregavam, e o desfoque saiu junto com o canto arredondado (ver a
       moldura dos botões de ícone, mais abaixo) — sem ele a transparência só
       deixava o fundo do jogo vazar por dentro da peça. */
    --cor-botao-flutuante-fundo: var(--cor-superficie-elevada);
    --cor-botao-flutuante-fundo-hover: var(--cor-superficie-hover-alta);

    /* ===== Pixel art =====
       A arte do jogo é pixel art (sprites das classes, arenas e tudo o que
       public/js/pixel.js desenha no canvas), então painéis e modais não usam
       canto arredondado nem sombra borrada: sombra aqui é um bloco sólido
       deslocado, no mesmo múltiplo de 4px do "pixel de arte" do canvas.

       Botões, campos e modais também são pixel art (moldura em escadinha, no
       fim do arquivo) e por isso não têm mais sombra externa nenhuma — a
       silhueta recortada deixaria a sombra aparecendo como um dente sólido
       nos cantos. Quem continua usando sombra desfocada são os painéis
       flutuantes e os botões de ícone, por isso --sombra-flutuante segue
       existindo. */
    --sombra-flutuante: 0 8px 20px rgba(0, 0, 0, 0.35);
    /* Espessura da moldura dos botões. O degrau do canto continua sendo o
       "pixel de arte" cheio (4px, PX em public/js/pixel.js) — é ele que dá a
       escadinha; o contorno é metade disso, senão a moldura pesa mais que o
       próprio botão nos botões menores (confirmar das modais, overlays). */
    --espessura-borda-botao: 2px;
    /* Quanto a moldura escurece o fundo do próprio botão. É um fator de
       brilho, não uma cor: a moldura tem que ser o tom escuro *daquele*
       botão, e o vermelho, o cinza e o desabilitado saem todos daqui sem
       precisar de uma variável por cor. */
    --escurecimento-borda-botao: 0.5;
    /* Silhueta do botão: retângulo com os quatro cantos em escadinha de dois
       degraus de 4px. Usada nas duas camadas da moldura — a de dentro é a
       mesma escadinha, só nasce recuada pela espessura da borda (ver o
       `inset` do ::after). */
    --recorte-botao-pixel: polygon(
      8px 0, calc(100% - 8px) 0,
      calc(100% - 8px) 4px, calc(100% - 4px) 4px,
      calc(100% - 4px) 8px, 100% 8px,
      100% calc(100% - 8px),
      calc(100% - 4px) calc(100% - 8px), calc(100% - 4px) calc(100% - 4px),
      calc(100% - 8px) calc(100% - 4px), calc(100% - 8px) 100%,
      8px 100%,
      8px calc(100% - 4px), 4px calc(100% - 4px),
      4px calc(100% - 8px), 0 calc(100% - 8px),
      0 8px,
      4px 8px, 4px 4px, 8px 4px
    );
    /* Silhueta dos botões **de ícone** (abas da barra, configurações, mudo,
       fechar): a mesma escadinha, com **um** degrau de 4px em cada canto em
       vez de dois. Num quadrado de 40px — e mais ainda nos de 26 a 30 — a
       escadinha de dois degraus come o botão inteiro, e era por isso que esses
       botões ficaram de fora da moldura de fliperama e mantiveram
       `border-radius` (ver docs/pixel-art.md). Um degrau resolve as duas
       coisas: dá a silhueta em pixel na escala certa sem a moldura pesada. */
    --recorte-icone-pixel: polygon(
      4px 0, calc(100% - 4px) 0,
      calc(100% - 4px) 4px, 100% 4px,
      100% calc(100% - 4px), calc(100% - 4px) calc(100% - 4px),
      calc(100% - 4px) 100%, 4px 100%,
      4px calc(100% - 4px), 0 calc(100% - 4px),
      0 4px, 4px 4px
    );
    /* Foco/seleção: anel sólido de 4px em vez de brilho difuso. */
    --sombra-foco-vermelho: 0 0 0 4px rgba(255, 77, 77, 0.55);
    /* ----- Realce de foco (navegação sem ponteiro) -----
       As três medidas do realce que marca o elemento em foco enquanto o
       jogador navega por controle ou por seta (ver "Realce de foco" no fim
       deste arquivo). São variáveis porque nenhuma das três é gosto — as duas
       primeiras foram **medidas em pixel** depois de o realce ter nascido
       vermelho e a 4px de recuo, e ter passado despercebido:

       - **A cor não pode ser o vermelho da identidade.** Vermelho é a cor que
         as peças já vestem: o fundo de `#btnHorde` é `--cor-vermelho` (#8b0000) e
         a moldura dele é esse mesmo vermelho escurecido, então o anel entrava
         vermelho sobre vermelho — medido, 3,0:1 de contraste, e a olho nu ele
         lia como a borda do botão ter engordado. No `#lojaComprarBtn` sem
         saldo é pior: a peça leva `filter: grayscale(1)` + `opacity: .45`, e o
         filtro come o **anel junto**, porque ele é pintado dentro do elemento
         filtrado — o vermelho saía como rgb(60,60,59) sobre rgb(21,21,20),
         1,7:1, invisível. Branco atravessa os dois: 10:1 sobre o botão
         vermelho e 4,9:1 mesmo depois do grayscale e da opacidade, porque o
         que sobrevive a um filtro que apaga matiz é **luminância**.
         O anel é de duas cores, e a segunda não é inventada: é a **moldura da
         própria peça**, sempre um tom mais escuro que o miolo dela e já
         desenhada para separá-la do que está atrás. Claro por dentro, escuro
         por fora — é por isso que ele não depende de contraste com um fundo só.
       - **O recuo é -6px, e não -4px.** `outline-offset` empurra a *borda* do
         anel, e o anel é pintado para **fora** dela: a -4px ele ocupava de 2 a
         4px para dentro, encostado na moldura de 2px da peça (as duas viravam
         um traço só de 4px) e, pior, dentro do que o `clip-path` come nos
         cantos — medido, o anel do `#btnHorde` sumia em três linhas de cada
         quina, o mesmo "quatro riscos soltos com um vão em cada canto" de uma
         `border`. A -6px ele ocupa de 4 a 6px: passa por dentro dos dois
         recortes (`--recorte-icone-pixel` come o quadrado de 4x4 de cada canto
         e `--recorte-botao-pixel` a escadinha de 4x8/8x4) e deixa 2px do
         material da peça entre a moldura dela e ele, que é o vão que faz os
         dois se lerem como duas linhas.
       - **A espessura é a mesma da moldura dos botões**: o realce é uma
         **segunda moldura** dentro da que a peça já tem, e não um traço de
         outro peso.

       O recuo continua negativo (o anel é desenhado **para dentro** da caixa)
       porque é a única posição que sobrevive ao `clip-path`: quase toda peça
       deste jogo é recortada em escadinha, e o recorte apaga tudo o que é
       pintado fora da silhueta — `outline`, `box-shadow` e até
       `filter: drop-shadow` (medido no navegador: o recorte é aplicado *depois*
       do filtro, então nem ele escapa). */
    --cor-realce-foco: var(--cor-branco);
    --espessura-realce-foco: var(--espessura-borda-botao);
    --recuo-realce-foco: -6px;
    /* A quarta medida, e ela existe porque as três de cima foram calibradas
       numa peça só de material: `#btnHorde` e `#lojaComprarBtn` são **pedra
       chapada** a 4–6px de dentro, e ali a segunda cor do anel vem de graça
       (a moldura da própria peça, um degrau para fora). Onde o material
       daquela faixa não é chapado, o par de duas cores some junto com ela e o
       anel branco fica sozinho sobre o que houver:

       - o **quadrado de nível** tem a textura da arena repetida no miolo
         (`.horde-level-item::before`) — medido a 1440x900, a faixa vizinha do
         anel e o próprio anel deram rgb(14,13,12) contra rgb(13,12,11),
         **1,01:1**, porque a textura não só disputa com o anel: ela é um
         `::before` posicionado e o Chromium a pinta **por cima** do `outline`,
         que sobrevive só nas quinas que a escadinha do recorte dela abre;
       - a **carta** (escolha de item e mesa da Loja) tem o mesmo problema por
         outro caminho: ela é `position: relative` dentro do botão que recebe o
         foco, e descendente posicionado também é pintado por cima do
         `outline` — sobravam 17% do anel, nos pedaços em que a arte da carta é
         transparente;
       - a **medalha desbloqueada** tem fundo cheio na cor do tier, e nos tiers
         claros o branco encosta no branco: rgb(255,255,255) sobre
         rgb(213,219,225) na prata é **1,40:1**, 1,57:1 no diamante e 1,91:1 no
         ouro — o mesmo modo de falha do anel vermelho sobre o botão vermelho,
         com a cor trocada.

       A resposta é dar o par de duas cores **explicitamente**: uma faixa
       escura de 6px atrás do anel (2..8px da borda), com os 2px brancos no
       meio dela. Escuro/claro/escuro, e aí o realce para de depender do que a
       peça tem por baixo. A cor é a pedra quase preta que o jogo já usa como
       contorno de peça (`--cor-pedra-borda`), e não um preto novo.

       Ela é desenhada por `box-shadow: inset` (ou por camadas de
       `background-image` num `::after`), nunca por `box-shadow` externo:
       inset é pintado **dentro** da caixa e por isso sobrevive ao `clip-path`,
       que é a mesma razão de o recuo do anel ser negativo. */
    --cor-realce-foco-fundo: var(--cor-pedra-borda);
    /* Painel "de pedra" (menu principal e ranking): bisel interno de 4px (luz
       em cima/à esquerda, sombra embaixo/à direita) + sombra dura deslocada. */
    --sombra-pedra: 8px 8px 0 rgba(0, 0, 0, 0.4),
      inset 4px 4px 0 rgba(255, 255, 255, 0.05),
      inset -4px -4px 0 rgba(0, 0, 0, 0.55);
    /* Só o bisel de dentro, sem a sombra deslocada. Existe para quem recorta a
       silhueta com `clip-path` (ver .menu-painel): recortada, a sombra externa
       seria pintada fora da forma e sumiria inteira — lá ela volta como
       `filter: drop-shadow`, que acompanha o recorte. */
    --sombra-pedra-interna: inset 4px 4px 0 rgba(255, 255, 255, 0.05),
      inset -4px -4px 0 rgba(0, 0, 0, 0.55);
    /* Mesma pedra em peça pequena (cronômetro, ícone de classe): bisel de 2px e
       sombra dura de 4px — o de 4px/8px pesaria mais que o próprio elemento. */
    --sombra-pedra-pequena: 4px 4px 0 rgba(0, 0, 0, 0.4),
      inset 2px 2px 0 rgba(255, 255, 255, 0.05),
      inset -2px -2px 0 rgba(0, 0, 0, 0.55);
    /* Só o bisel da peça pequena, sem a sombra deslocada — o irmão de
       --sombra-pedra-interna para quem recorta a silhueta com `clip-path` na
       escala de 2px (a linha da lista do Monstruário). */
    --sombra-pedra-pequena-interna: inset 2px 2px 0 rgba(255, 255, 255, 0.05),
      inset -2px -2px 0 rgba(0, 0, 0, 0.55);
    /* Bisel invertido (sombra em cima/à esquerda): o que usa isso é buraco
       cavado na pedra, não bloco em relevo — trilho da barra de cooldown. */
    --sombra-pedra-afundada: inset 2px 2px 0 rgba(0, 0, 0, 0.6),
      inset -2px -2px 0 rgba(255, 255, 255, 0.04);
  }
  @supports (height: 100dvh) {
    :root {
      --altura-tela: 100dvh;
    }
  }
  /* ===== Linha de lista com contorno (material de apoio) =====
     As linhas de lista que se **marcam** com uma cor: a selecionada do
     Monstruário (`.monstruario-row`, vermelho de seleção) e a do próprio
     jogador no placar do Sem fim (`#hordeEndlessList li`, ouro de "você").

     O contorno não é `border` nem anel `box-shadow: inset`: recortadas por
     `--recorte-icone-pixel`, as duas somem na diagonal dos cantos e a marcação
     sai como quatro riscos soltos com um vão em cada quina (o mesmo motivo de
     `.campo-pixel` e de `.tecla`). Ele é `background-image`, três camadas: o
     miolo em duas faixas cruzadas e, no fundo, a cor do contorno na caixa
     inteira. O contorno é o que **sobra** depois do miolo — 2px em volta, mais
     o degrau de 4px de cada canto, acompanhando o recorte.

     Estas duas linhas são as únicas peças do menu **sem** o bisel de pedra
     (`--sombra-pedra-pequena-interna`), que elas tiveram: `box-shadow` é
     pintado por cima de todo o `background`, então o bisel atravessava o
     contorno e escurecia justamente os 2px da direita e de baixo — a borda
     saía de duas cores. Empurrado para dentro do contorno ele ficava irregular
     em volta do degrau dos cantos, que é o que uma faixa reta não sabe
     acompanhar. Numa linha que já tem contorno de identidade, o relevo estava
     cobrando mais do que entregava.

     Quem usa isso declara duas variáveis e nada mais: `--cor-fundo-linha` (o
     miolo, que o hover mexe) e `--cor-contorno-linha` (o contorno, que é a cor
     do miolo quando a linha não está marcada). Assim "marcada" é uma troca de
     cor só, sem nada mudar de lugar. Peça de 12px para cima nas duas medidas;
     abaixo disso as faixas se cruzam.

     A receita é uma regra e não um token de `:root` porque um `var()` aninhado
     dentro de um token é resolvido **no elemento que declara o token** — em
     `:root` as duas cores da linha não existem, e a lista inteira sairia
     inválida. */
  .monstruario-row,
  #hordeEndlessList li:not(.skeleton-row),
  #hordeCampanhaList li:not(.skeleton-row),
  #nivelRankingList li:not(.skeleton-row) {
    background-image:
      linear-gradient(var(--cor-fundo-linha), var(--cor-fundo-linha)),
      linear-gradient(var(--cor-fundo-linha), var(--cor-fundo-linha)),
      linear-gradient(var(--cor-contorno-linha), var(--cor-contorno-linha));
    background-size:
      calc(100% - 4px) calc(100% - 12px),
      calc(100% - 12px) calc(100% - 4px),
      100% 100%;
    background-position: 2px 6px, 6px 2px, 0 0;
    background-repeat: no-repeat;
  }
  /* ===== Raridade de item (classe de apoio) =====
     Uma classe por raridade, aplicada pelo JS (`classeDeRaridade` em
     public/js/raridades.js) nos três lugares que mostram um item: o cartão da
     tela de escolha do fim da onda, a linha do Relicário e o `<li>` do
     inventário do HUD. Ela não pinta nada sozinha — só traz as duas variáveis
     daquela raridade para o elemento, e quem decide como usá-las é cada
     componente (o Relicário e o cartão da escolha têm espaço para um fundo
     cheio, o HUD é uma caixa do tamanho do ícone sobre a arena).

     É classe e não `style="--cor-raridade: ..."` inline pelo mesmo motivo de
     `--class-color` ser o contrário: a cor de classe vem de shared/classes.js e
     só o JS a conhece, enquanto a raridade é um conjunto fechado de quatro
     valores que o CSS pode declarar de uma vez. */
  .raridade-comum {
    --cor-raridade: var(--cor-raridade-comum);
    --cor-raridade-veu: var(--cor-raridade-comum-veu);
  }
  .raridade-raro {
    --cor-raridade: var(--cor-raridade-raro);
    --cor-raridade-veu: var(--cor-raridade-raro-veu);
  }
  .raridade-quebra-jogo {
    --cor-raridade: var(--cor-raridade-quebra-jogo);
    --cor-raridade-veu: var(--cor-raridade-quebra-jogo-veu);
  }
  .raridade-maldito {
    --cor-raridade: var(--cor-raridade-maldito);
    --cor-raridade-veu: var(--cor-raridade-maldito-veu);
  }
  /* A classe que leva a cor da família ao selo, no mesmo molde de `.raridade-*`
     acima (ver `classeDeFamilia` em raridades.js). Vale nas duas telas que
     mostram o selo, e é por isso que ela não é aninhada em nenhuma das duas. */
  .familia-cano { --cor-familia: var(--cor-familia-cano); }
  .familia-municao { --cor-familia: var(--cor-familia-municao); }
  .familia-bocal { --cor-familia: var(--cor-familia-bocal); }
  .familia-leque { --cor-familia: var(--cor-familia-leque); }
  /* ===== Tela girada (modo touch em retrato) =====
     Gira o app inteiro 90° no sentido do relógio em vez de pedir para o
     jogador girar o aparelho: com a tela física em retrato, o conteúdo é
     desenhado deitado, ocupando a tela toda. A classe é aplicada por
     public/js/orientation.js, que também é quem decide quando isso vale (só em
     modo touch, e só enquanto a viewport estiver em retrato — se a trava de
     orientação do sistema funcionar, a viewport já vem deitada e nada disto
     entra).

     Os eixos do conteúdo trocam junto com a rotação, então as duas variáveis de
     tela trocam de valor aqui — é isso que faz todo o resto do arquivo
     (alturas de painel, `max-height` de modal, tamanhos de fonte relativos)
     continuar valendo sem uma segunda cópia de cada regra.

     `translateY(-100%)` depois do `rotate` é o que traz o conteúdo de volta pra
     dentro da tela: girando em volta do canto superior esquerdo, o conteúdo
     sairia todo para a esquerda da viewport. A ordem importa (a lista é
     aplicada da direita para a esquerda). E o `transform` no `<body>` faz dele
     o bloco contentor dos descendentes `position: fixed` — o que é justamente o
     desejado, porque assim overlays, modais e botões flutuantes giram junto com
     o resto (ver o posicionamento em coordenada de viewport de talentTreeUi.js
     e dicaDeMedalha.js, que depende disso). */
  body.tela-girada {
    --largura-tela: 100vh;
    --altura-tela: 100vw;
    position: fixed;
    top: 0;
    left: 0;
    width: var(--largura-tela);
    height: var(--altura-tela);
    transform-origin: 0 0;
    transform: rotate(90deg) translateY(-100%);
    /* Desliga o gesto de rolagem/zoom nativo enquanto girado, porque ele não
       funciona aqui: testado em Chrome (com janela real, não só headless), um
       contêiner rolável dentro de um elemento girado 90° por `transform` não
       rola por toque em nenhuma direção física, embora role normalmente sem a
       rotação. Quem move o scroll nesse modo é public/js/touchScroll.js — e
       desligar o nativo aqui garante que o comportamento seja o mesmo em
       qualquer navegador, sem rolar duas vezes onde o nativo funcionar. */
    touch-action: none;
  }
  @supports (height: 100dvh) {
    body.tela-girada {
      --largura-tela: 100dvh;
    }
  }
  /* O flex centralizado é só do body. O html fica um bloco comum de altura
     100% — é o overflow dele que a viewport usa para rolar, então ele não pode
     centralizar nem prender o conteúdo (era o que impedia o menu mais alto que
     a tela de ser rolado). */
  html {
    height: 100%;
    background: var(--cor-fundo);
  }
  body {
    margin: 0;
    height: 100%;
    background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), var(--cor-fundo) url('../assets/backgrounds/dungeon_brick_wall_grey.png') repeat;
    /* Paralaxe da tela inicial (public/js/parallax.js): desloca só a camada
       da textura (segunda posição) pelo mouse via variáveis CSS; a camada do
       gradiente escuro (primeira posição) fica parada em 0 0. Sem transição
       de background-position durante a partida, porque `body.game-active`
       nunca tem essas variáveis atualizadas — o fundo já fica parado. */
    background-position: 0 0, var(--parallax-x, 0px) var(--parallax-y, 0px);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    font-family: 'Chakra Petch', sans-serif;
    color: var(--cor-texto);
    overflow: hidden;
    user-select: none;
    -webkit-user-select: none;
  }
  input, textarea {
    user-select: text;
    -webkit-user-select: text;
  }
  /* A porta de entrada (public/js/splash.js): a tela que cobre tudo enquanto o
     jogo carrega, sobre um fundo liso mais escuro que o do menu
     (--cor-barra-fundo, o mais escuro da paleta e praticamente preto) — sem
     textura/paralaxe, ela vem antes até do menu carregar. `position: fixed`
     porque nasce como primeiro filho do body, antes do `display: flex` dele
     centralizar qualquer coisa.

     **Tudo o que ela desenha é CSS**, e nenhum pedaço depende de JS para
     aparecer: sob rede lenta o módulo que decide a saída só é avaliado lá pelo
     décimo segundo, e um herói montado por JS ficaria ausente justamente
     durante a espera que ele existe para cobrir. O JS só acende as casas da
     barra e manda a tela sair. */
  #splashScreen {
    position: fixed;
    inset: 0;
    z-index: 30;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* A escala do herói mora aqui, no pai, porque ela é a **composição** da
       tela e não propriedade de uma peça só — e o respiro sai dela
       (`2px * escala`), em vez de ser um terceiro número a manter em dia:
       herói maior, respiro maior, na mesma proporção. */
    --splash-escala: 6;
    gap: calc(2px * var(--splash-escala));
    overflow: hidden;
    background: var(--cor-barra-fundo);
  }
  /* Os degraus da escala dependem da **altura** tanto quanto da largura: numa
     faixa curta e larga o herói na escala grande empurraria a barra para fora
     do quadro. Cada degrau só entra quando herói + barra + respiro cabem na
     janela.

     Celular **em pé** fica no degrau de baixo de propósito, mesmo sobrando
     altura: em modo touch o jogo inteiro é girado 90 graus por CSS
     (`body.tela-girada`, ver orientation.js), e quando essa classe entra — o
     que pode acontecer com a porta ainda na tela, porque quem a põe é o boot —
     a altura útil passa a ser a **largura** da janela. Um degrau escolhido pela
     altura em pé estouraria o quadro deitado. */
  @media (min-width: 900px) and (min-height: 400px) {
    #splashScreen { --splash-escala: 8; }
  }
  @media (min-width: 1200px) and (min-height: 700px) {
    #splashScreen { --splash-escala: 12; }
  }
  /* Os **dois atos**. Enquanto carrega (a classe já nasce no HTML, porque o JS
     que a governa só é avaliado segundos depois numa rede lenta) só existem o
     herói e a barra; quando o jogo fica pronto, splash.js tira a classe e o que
     fica na tela é a assinatura do estúdio, sozinha. `display: none` e não
     opacidade: o que sai do ato não pode ocupar espaço, senão a peça que ficou
     deixa de estar centralizada. */
  #splashScreen.carregando #splashLogo,
  #splashScreen:not(.carregando) #splashCorredor,
  #splashScreen:not(.carregando) #splashBarra {
    display: none;
  }
  #splashLogo {
    max-width: min(46vw, 320px);
    max-height: min(30vh, 200px);
    image-rendering: pixelated;
    animation: splash-entra 0.3s steps(3, end) 1;
  }
  /* O atirador correndo: a tira `walk.png` de 6 quadros de 100x100 andando por
     `background-position` em `steps(6)` — os mesmos 90ms por quadro que
     characterSprites.js usa na arena (6 x 90 = 540ms de ciclo), para o herói da
     abertura correr na cadência do herói do jogo.

     **A janela é o corpo, não o quadro.** O desenho do Little Mage ocupa só
     x 44..56 / y 44..59 dentro do quadro de 100x100 — 13 por 16 pixels, medidos
     no PNG, com o resto transparente. Dimensionar pela caixa do quadro é o erro
     que deixou o herói lendo como detalhe numa tela de 1080p: a caixa era
     grande e o corpo dentro dela, pequeno. A janela abaixo é o corpo com 2px de
     folga de cada lado (x 42..58, y 42..61 — 16 por 20), então a escala
     multiplica **o que se vê**. Mexer nas medidas do corpo é mexer nos quatro
     números de uma vez: o recorte, a caixa e os dois extremos do keyframe.

     A escala é **inteira** (regra de pixel art), em três degraus — 6, 8 e 12 —.
     O degrau de baixo não desce a 4 porque numa tela pequena o herói **é** a
     tela: metade do tamanho num celular deitado de 390px de altura vira um
     detalhe, e não o assunto. A conta é a proporção da janela, não o número
     absoluto —
     e tudo é `calc()` sobre ela, então trocar o tamanho é trocar um número. */
  #splashCorredor {
    width: calc(16px * var(--splash-escala));
    height: calc(20px * var(--splash-escala));
    background-image: url('../assets/sprites/atirador/walk.png');
    background-repeat: no-repeat;
    background-size: calc(600px * var(--splash-escala)) calc(100px * var(--splash-escala));
    background-position: calc(-42px * var(--splash-escala)) calc(-42px * var(--splash-escala));
    image-rendering: pixelated;
    animation: splash-corre 0.54s steps(6) infinite;
  }
  @keyframes splash-corre {
    from { background-position-x: calc(-42px * var(--splash-escala)); }
    to { background-position-x: calc(-642px * var(--splash-escala)); }
  }
  /* A barra de progresso: uma casa por frente de carregamento (a capa, o jogo,
     as tiras — ver public/js/carregamentoDoBoot.js), acesa quando aquela frente
     fecha. São casas discretas, e não uma barra que enche: as frentes são três
     coisas que acontecem ou não, e uma barra contínua teria de inventar um
     progresso que ninguém mede. Sem texto de propósito — a tela aparece antes
     de o dicionário do idioma carregar, e três casas dizem o mesmo nos dois
     idiomas. As casas são criadas por splash.js. */
  #splashBarra {
    display: flex;
    gap: 8px;
    min-height: 16px;
  }
  .splash-casa {
    width: 16px;
    height: 16px;
    box-sizing: border-box;
    border: 2px solid var(--cor-borda);
    background: transparent;
  }
  .splash-casa.acesa {
    border-color: var(--cor-ouro-suave);
    background: var(--cor-ouro-suave);
  }
  /* Entrada em três tamanhos inteiros, mesma receita de gameover-impacto/
     conquista-surge: nada de escala interpolada. */
  @keyframes splash-entra {
    0% { opacity: 0; transform: scale(0.7); }
    50% { opacity: 1; transform: scale(1.08); }
    100% { opacity: 1; transform: scale(1); }
  }
  /* Saída seca em degrau (splash.js poe .saindo e remove o elemento ao fim
     da animação) — sem esmaecer contínuo, mesma regra do resto da interface. */
  #splashScreen.saindo {
    animation: splash-sai 0.26s steps(2, end) 1 forwards;
  }
  @keyframes splash-sai {
    0% { opacity: 1; }
    100% { opacity: 0; }
  }
  /* Movimento reduzido: sai a entrada da assinatura, que é **deslocamento**
     (ela cresce de 0,7 a 1). O herói correndo fica: ele não sai do lugar — é um
     ciclo de pose no mesmo ponto —, e é a única coisa na tela que diz que o
     jogo está vindo e não travado. É a mesma régua do brilho do botão da Loja
     mais abaixo neste arquivo: fica o que é informação, sai o que é
     deslocamento. */
  @media (prefers-reduced-motion: reduce) {
    #splashLogo { animation: none; }
  }
  /* Enquanto a assinatura está na tela, a barra superior não aparece (e com
     ela os controles de som e de música, que moram lá dentro): a abertura é
     uma tela só, sem interface por cima. Seletor de irmão em vez de classe no
     body porque splash.js remove o elemento ao fim da saída — a regra para de
     valer sozinha, sem estado novo pra sincronizar. */
  #splashScreen ~ #menuTopbar {
    display: none;
  }
  /* Botões não herdam a fonte do body por padrão (fonte de UI do navegador) —
     precisa declarar explicitamente. */
  button {
    font-family: 'Chakra Petch', sans-serif;
    transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
  }
  /* `[aria-disabled="true"]` sai junto de `:disabled`: o jogo usa de propósito
     o botão *apagado mas clicável* onde o clique é o que explica a recusa (o nó
     de talento bloqueado, que precisa continuar alcançável pelo controle), e um
     botão que não faz nada não pode se levantar da mesa ao passar o cursor.

     **Esta é a SÉTIMA lista de `:not()` do arquivo**, e a única que não começa
     em `#menu` — as outras seis são as de `#menu button:not(...)`. Uma classe
     nova que precise ficar fora do "levantar" tem de entrar aqui **também**, e
     é fácil não perceber: `.config-aba` e `.config-opcao` entraram nas seis e
     faltavam nesta, então as abas da tela de configurações levantavam 2px e
     escalavam 1,03 dentro de uma modal com `overflow: hidden` — saíam cortadas
     em cima e embaixo a cada passagem do cursor. É o mesmo defeito de
     `.menu-voltar`, que está aqui pelo mesmo motivo. Aba, opção e interruptor
     fazem hover de **cor**, nunca de geometria. */
  button:not(:disabled):not([aria-disabled="true"]):not(.link-button):not(.ranking-nome):not(.resumo-item-atalho):not(.password-toggle-btn):not(.horde-level-item):not(.monstruario-row):not(.item-choice-botao):not(.menu-voltar):not(.sem-moedas):not(.config-aba):not(.config-opcao):hover {
    transform: translateY(-2px) scale(1.03);
    filter: brightness(1.08);
    box-shadow: var(--sombra-flutuante);
  }
  button:not(:disabled):not([aria-disabled="true"]):not(.link-button):not(.ranking-nome):not(.resumo-item-atalho):not(.password-toggle-btn):not(.horde-level-item):not(.monstruario-row):not(.item-choice-botao):not(.menu-voltar):not(.sem-moedas):not(.config-aba):not(.config-opcao):active {
    transform: translateY(0) scale(0.98);
    filter: brightness(0.97);
    box-shadow: none;
  }
  /* .password-toggle-btn fica de fora da regra genérica acima de propósito:
     ela sobrescreve `transform` inteiro, e ele já usa
     `transform: translateY(-50%)` só para ficar centralizado verticalmente na
     posição absoluta — combinar os dois fazia o botão "pular" da posição toda
     vez que o mouse passava por cima ou clicava. Hover/active nele só mudam
     brilho/sombra/cor, sem mexer em posição. */
  /* Cinzel é a fonte dos títulos (painéis, modais, fim de jogo) — Metal
     Mania é do nome do jogo (#menuTitle) e do resultado da partida
     (#gameOverMessage, ver o comentário lá), e de mais nada: ela vira
     ilegível em texto corrido ou números pequenos (HUD, cronômetro). */
  h1, h2 {
    font-family: 'Cinzel', serif;
    letter-spacing: 0.5px;
  }
  #menuTitle {
    font-family: 'Metal Mania', cursive;
  }
  /* Canvas do rastro de fogo do cursor (public/js/fireCursor.js): fixo por
     cima de tudo e sem capturar clique/hover, porque é puramente decorativo
     e fica em cima de botões e modais da tela inicial. Some sozinho durante a
     partida (o próprio módulo checa `body.game-active` a cada frame). */
  #fireCursorCanvas {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    background: transparent; /* sobrescreve a regra genérica `canvas { background: var(--cor-superficie-elevada) }` mais abaixo */
    pointer-events: none;
    z-index: 999;
  }
  * {
    scrollbar-width: thin;
    scrollbar-color: var(--cor-cinza-medio) transparent;
  }
  *::-webkit-scrollbar {
    width: 10px;
    height: 10px;
  }
  *::-webkit-scrollbar-track {
    background: transparent;
  }
  *::-webkit-scrollbar-thumb {
    background-color: var(--cor-cinza-medio);
    border-radius: 0;
    border: 2px solid transparent;
    background-clip: padding-box;
  }
  *::-webkit-scrollbar-thumb:hover {
    background-color: var(--cor-cinza-claro);
  }
  *::-webkit-scrollbar-corner {
    background: transparent;
  }
  /* A tela inicial nunca rola verticalmente (ver .menu-view): #menu ocupa a
     altura inteira da viewport e cada painel interno (ranking, partidas ao
     vivo) tem seu próprio scroll quando o conteúdo não cabe. A partida também
     não precisa de nada aqui: gameScale.js já ajusta o #game-wrap para caber
     na tela. */
  body.game-active {
    overflow: hidden;
  }
  /* ----- Skeleton de loading (tela inicial) -----
     Padrão: elemento carrega dado assíncrono (sessão, contagem online,
     ranking) e mostra esse esqueleto animado no lugar do conteúdo real até a
     primeira resposta chegar, sem alterar a estrutura da página — o JS só
     alterna a classe/conteúdo, os elementos e o layout continuam os mesmos. */
  /* Exceção deliberada à regra de `steps()` do resto da interface (ver
     "Pixel art no CSS"): aqui o brilho varre a linha de forma contínua, com
     gradiente suave e timing `linear`. Em passos, o skeleton parecia um
     fliperama travado — a impressão era de jogo congelado, não de conteúdo
     carregando. Movimento fluido é o que comunica "está vindo". */
  @keyframes skeleton-shimmer {
    0% { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
  }
  .skeleton-loading {
    position: relative;
    color: transparent !important;
    pointer-events: none;
  }
  .skeleton-loading::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 0;
    background: linear-gradient(90deg,
      var(--cor-superficie-alta) 0%,
      var(--cor-superficie-hover) 50%,
      var(--cor-superficie-alta) 100%);
    background-size: 300% 100%;
    animation: skeleton-shimmer 1.4s linear infinite;
  }
  .skeleton-row {
    height: 14px;
    border-radius: 0;
    background: linear-gradient(90deg,
      var(--cor-superficie-alta) 0%,
      var(--cor-superficie-hover) 50%,
      var(--cor-superficie-alta) 100%);
    background-size: 300% 100%;
    animation: skeleton-shimmer 1.4s linear infinite;
  }
  /* ----- Animações de entrada -----
     Padrão para qualquer elemento que "aparece" na tela (menu, modais,
     overlays de partida, HUD, listas): fade leve + pequeno movimento/escala
     em vez de surgir seco. Curto o bastante (150–400ms) pra não atrasar a
     resposta ao clique. Como são `animation` (não `transition`), reaparecem
     sozinhos toda vez que o elemento volta de `display: none` pra visível —
     não precisa de nenhum gatilho extra em JS. */
  @keyframes entrada-fade {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
  }
  @keyframes entrada-pop {
    from { opacity: 0; transform: scale(0.92); }
    to { opacity: 1; transform: scale(1); }
  }
  @keyframes overlay-fade {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  /* Reverso de entrada-pop/overlay-fade, para o fechar da modal não ser um
     corte seco (display: none na hora do clique). Quem fecha (JS) adiciona
     `.closing`, espera o `animationend` do overlay e só então esconde o
     elemento — ver closeBotClassSelect/closeOnlineClassSelect/fechar() em
     authScreens.js e profile.js. */
  @keyframes saida-pop {
    from { opacity: 1; transform: scale(1); }
    to { opacity: 0; transform: scale(0.92); }
  }
  @keyframes overlay-fade-out {
    from { opacity: 1; }
    to { opacity: 0; }
  }
  .modal.closing {
    animation: saida-pop 0.15s ease forwards;
  }
  #authOverlay.visible.closing,
  #profileOverlay.visible.closing,
  #pauseOverlay.visible.closing,
  #configuracoesOverlay.visible.closing,
  #creditsOverlay.visible.closing,
  #changelogOverlay.visible.closing {
    animation: overlay-fade-out 0.15s ease forwards;
  }
  /* Botão de menu da partida (engrenagem): flutua sobre a página inteira, não
     sobre a arena (`position: fixed`, espelhado no canto esquerdo do botão de
     configurações da barra). Abre a modal de pausa, ver pauseMenu.js. */
  #pauseMenuBtn {
    position: fixed;
    top: 16px;
    left: 16px;
    z-index: 5;
  }
  /* ----- Botão de configurações -----
     Um botão só, no canto direito da barra superior (#menuTopbarDir), junto do
     botão de conta: ele abre a **tela de configurações** (#configuracoesOverlay,
     ver telaDeConfiguracoes.js), que é a porta única de toda preferência do
     jogador. No lugar dele já houve um widget de som (#soundSettings, só para os
     efeitos) e depois um player de música com os dois volumes dentro
     (#musicPlayer/#musicPanel); os dois saíram pelo mesmo motivo, um degrau
     acima do outro — eram portas a mais para uma ideia que hoje tem tela
     própria, e cobriam só áudio. Ele não existe com a partida na tela, porque a
     barra superior inteira some ali (`body.game-active #menuTopbar`); durante a
     partida a mesma tela é montada dentro do menu da partida. */
  /* ----- A moldura dos botões de ícone -----
     Botões quadrados com fundo mais claro que as superfícies normais, pra
     destacar sobre qualquer fundo. #pauseMenuBtn entra na mesma regra: é o
     próprio botão flutuante, e não um botão dentro de um wrapper, mas tem
     exatamente o mesmo formato. O botão de voltar da tela inicial
     (#btnMenuViewHome, ver menuViews.js) e o de modo debug também: são a mesma
     peça, na mesma barra, e é de propósito que se pareçam — só a largura fixa é
     sobrescrita lá embaixo, porque eles levam rótulo ao lado do ícone.

     Eram a última peça da interface com `border-radius` e
     `backdrop-filter: blur()`, que é exatamente o que o resto da tela deixou
     de fazer. Ficarem de fora da moldura de fliperama de dois degraus (ver o
     bloco "Botões em pixel art" no fim do arquivo) nunca implicou canto
     redondo: implicava só que dois degraus de 4px num quadrado de 40px comem
     o botão inteiro. Um degrau (--recorte-icone-pixel) dá a mesma silhueta em
     escadinha na escala certa.

     O contorno é `background-image`, e não `border`: recortada pelo
     `clip-path`, uma borda some justamente na diagonal dos cantos — é o mesmo
     motivo pelo qual os campos de texto desenham a moldura assim
     (ver `.campo-pixel`). São três camadas: a de baixo pinta a cor do
     contorno na caixa inteira, e as duas de cima repintam o miolo recuado por
     2px — a faixa larga e a faixa alta, que juntas deixam o degrau de 4px em
     cada canto de dentro também. Assim "aceso" continua sendo uma troca de
     cor só (--cor-borda-campo), como era com `border-color`.

     O nó da árvore de talentos (.talent-no) é esta mesma peça na mesma escala,
     mas **não** entra nesta regra: lá a silhueta é desenhada por duas camadas
     a mais em vez de recortada por `clip-path`, porque o tracejado do fecho e
     o anel da seleção são pintados fora dela e o recorte os apagaria (ver
     ".talent-no").

     As alternativas de desenho de cada ícone destes botões, e o porquê de
     cada escolha, estão em docs/sugestoes-icones-botoes.html. */
  #btnIdioma,
  #btnConfiguracoes,
  #menuTopbarLinha .menu-voltar,
  #pauseMenuBtn,
  .tela-config .volume-control button,
  #accountFabBtn,
  .modal-close,
  .modal-close-flutuante {
    --cor-borda-campo: var(--cor-borda);
    --cor-fundo-campo: var(--cor-botao-flutuante-fundo);
    border: none;
    border-radius: 0;
    clip-path: var(--recorte-icone-pixel);
    background-color: transparent;
    background-image:
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo));
    background-size:
      calc(100% - 4px) calc(100% - 12px),
      calc(100% - 12px) calc(100% - 4px),
      100% 100%;
    background-position: 2px 6px, 6px 2px, 0 0;
    background-repeat: no-repeat;
    color: var(--cor-texto);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
  }
  #btnIdioma,
  #btnConfiguracoes,
  #menuTopbarLinha .menu-voltar,
  #pauseMenuBtn,
  .tela-config .volume-control button {
    width: 40px;
    height: 40px;
  }
  #btnIdioma:hover,
  #btnConfiguracoes:hover,
  #menuTopbarLinha .menu-voltar:hover,
  #pauseMenuBtn:hover,
  .tela-config .volume-control button:hover {
    --cor-fundo-campo: var(--cor-botao-flutuante-fundo-hover);
  }
  /* O botão que mora **na barra** usa os tons da barra, e não os do botão
     flutuante: os `--cor-botao-flutuante-*` da regra compartilhada valem para
     peça solta sobre a cena, e aqui ele está lado a lado com a conta. */
  #btnConfiguracoes,
  #btnIdioma {
    --cor-fundo-campo: var(--cor-barra-botao);
  }
  #btnConfiguracoes:hover,
  #btnIdioma:hover {
    --cor-fundo-campo: var(--cor-barra-botao-hover);
  }
  /* A sigla do atalho de idioma (ver public/js/atalhoDeIdioma.js). Ela é
     **texto de interface**, e não desenho no canvas: são duas letras, e um
     glifo de bandeira (ou um SVG por idioma) seria arte nova para dizer o que
     "PT"/"EN" já diz. A fonte é a dos títulos, como a do botão de voltar ao
     lado: os dois são a mesma peça de barra com texto dentro. Canto reto e
     sombra sólida deslocada vêm da moldura compartilhada — nenhuma curva nova
     entrou. `letter-spacing` de 1px porque em 13px as duas maiúsculas coladas
     lêem como uma sílaba só. */
  #btnIdiomaSigla {
    font-family: 'Cinzel', serif;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 1px;
    line-height: 1;
    text-shadow: 1px 1px 0 var(--cor-pedra-borda);
  }
  /* `display: flex` da moldura compartilhada ganha do `display: none` que o
     navegador dá ao atributo `hidden`: sem esta linha o atalho ficaria na
     barra durante a partida e na sala de espera, onde a recarga que ele pede
     derrubaria a conexão. É a mesma armadilha do `[hidden]` do botão de
     voltar. */
  #btnIdioma[hidden] {
    display: none;
  }
  /* Só existe durante uma partida — `body.game-active` é a mesma classe que
     showGame()/showMenu() (menu.js) já alternam para esconder o menu, não
     precisa de estado novo. Depois do `display: flex` da regra compartilhada
     acima (mesma especificidade, decidido pela ordem no arquivo). */
  #pauseMenuBtn {
    display: none;
  }
  body.game-active #pauseMenuBtn {
    display: flex;
  }
  .sound-settings-row {
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 13px;
  }
  /* A linha tem `display: flex` próprio, então o `hidden` sozinho não bastaria
     para escondê-la. Hoje quem decide se ela existe é a montagem da tela
     (configuracoesVisiveis.js) e não este atributo, mas a regra fica: ela é o
     que faz `hidden` significar "some" em qualquer linha da família. */
  .sound-settings-row[hidden] {
    display: none;
  }
  .sound-settings-row label {
    color: var(--cor-texto-suave);
  }
  .sound-settings-row input[type="range"] {
    width: 100%;
    accent-color: var(--cor-vermelho);
  }
  /* Botão de mudo sempre à esquerda do slider de volume — vale tanto para
     efeitos sonoros quanto para música. */
  .volume-control {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  /* Um seletor **por hospedeiro** seria o erro aqui: a tela de configurações é
     montada em dois (a modal do menu inicial e o menu da partida), e um terceiro
     id nesta lista viria junto com o quarto hospedeiro. O escopo é a classe da
     própria tela, e ela aparece nas duas listas de propósito: é lá em cima que
     este botão ganha borda, fundo e centralização (é a mesma peça dos botões de
     ícone), e aqui que ele encolhe para 28px. */
  .tela-config .volume-control button {
    flex: none;
    width: 28px;
    height: 28px;
  }
  /* No dedo o botão de mudo é o **único** alvo da tela abaixo do mínimo de
     44px: os 28px são a medida de sempre desta peça e servem ao ponteiro, mas no
     celular ela fica encostada no slider do mesmo volume e errar o toque é
     arrastar a barra sem querer. Mesmo desvio que `.modal-close` já faz em
     `touch-mode`. Ele continua com o desenho de 28px — o que cresce é a caixa de
     toque, e a moldura acompanha porque é `background-size` em porcentagem. */
  body.touch-mode .tela-config .volume-control button {
    width: 44px;
    height: 44px;
  }
  /* O "x" de mudo é por classe, e não um id por botão: os mesmos dois
     controles aparecem também na modal de pausa (ver pauseMenu.js). */
  .icone-mudo {
    display: none;
  }
  button.muted .icone-mudo {
    display: block;
  }
  /* A palavra ao lado do botão de vibração (soundSettings.js). Os dois mudos
     não precisam dela — o slider ao lado deles já diz o que o botão silencia —,
     e a vibração não tem slider nenhum: sozinho, o ícone seria um desenho
     pedindo para ser adivinhado. Ela é legenda, não controle: mesma cor do
     rótulo da linha, e nada de fundo, borda ou alvo de clique. */
  .opcao-estado {
    color: var(--cor-texto-suave);
    font-size: 12px;
  }
  /* Tela inicial: sete visões que ocupam a tela inteira, uma por vez
     (início = logo + a porta do modo; Defesa da base = navegador de salas;
     Talentos = a árvore; Conquistas = as medalhas; Monstruário = ficha dos
     inimigos da horda; Relicário = catálogo dos itens dela; ranking = Sem
     fim), trocadas pelas estações do acampamento — nunca por scroll (ver
     public/js/menuViews.js e a nota em `body:not(.game-active)` acima). */
  #menu {
    position: relative;
    z-index: 1;
    width: 100%;
    height: var(--altura-tela);
    overflow: hidden;
    box-sizing: border-box;
    /* A barra superior é `position: fixed` e não empurraria nada sozinha: é
       este `padding-top` que reserva a faixa dela e mantém todas as visões do
       menu inteiras abaixo da barra, em vez de passarem por baixo dela. */
    padding-top: var(--altura-topbar);
  }
  /* A visão inativa sai do fluxo com `display: none`, e não com opacidade ou
     posição fora da tela: é isso que faz o poll do ranking parar sozinho
     (IntersectionObserver no próprio painel, ver ranking.js) sem nenhuma
     checagem a mais aqui. */
  .menu-view {
    display: none;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    /* Simétrico: os 64px que sobravam à direita eram o corredor dos controles
       de áudio, que flutuavam naquele canto — hoje eles moram dentro da barra
       superior (ver #menuTopbarDir). */
    padding: 16px 24px;
    align-items: center;
    justify-content: center;
    /* `safe` desliga a centralização quando o conteúdo é maior que a visão:
       centralizado, o excesso sai pelos *dois* lados e o pedaço de cima fica
       inalcançável mesmo com o `overflow` abaixo (o scroll não anda para trás
       do início). Navegador que não conhece a palavra descarta esta linha e
       fica com o `center` acima. */
    justify-content: safe center;
    gap: 24px;
    /* Rede de segurança: numa janela baixa o suficiente para o conteúdo não
       caber em pé, a visão rola por dentro dela mesma em vez de cortar o
       conteúdo — a página nunca rola. Em qualquer tamanho normal de tela isso
       nunca ativa. */
    overflow-y: auto;
  }
  .menu-view.menu-view-ativa {
    display: flex;
  }
  /* A tela inicial é uma coluna centrada: logo em cima, os dois modos lado a
     lado embaixo dele e as duas ações secundárias fechando. Lado a lado (logo
     à esquerda, botões à direita) o logo dividia a largura com o índice e
     ficava pequeno — sendo ele a primeira coisa que a tela tem a dizer, a
     largura inteira é dele. As outras visões seguem lado a lado, que é o
     padrão de .menu-view. */
  #menuViewHome {
    flex-direction: column;
    gap: clamp(16px, calc(0.03 * var(--altura-tela)), 40px);
    /* **A régua da capa.** O logo e a linha de botões são *uma* peça, e por
       isso saem do mesmo número: `--capa-logo` é o tamanho do logo (cada faixa
       de tela o redefine logo abaixo, e é só isso que elas redefinem), e todo
       o resto — a largura das duas linhas de botão, a fonte e o respiro
       interno deles — é uma fração dele.

       Antes as duas linhas tinham **460px fixos** enquanto o logo escalava com
       a tela, e a proporção entre os dois virava qualquer coisa: num celular
       deitado (780x360) o logo dava 162px de largura e a linha continuava com
       460 — o botão "Jogar" era quase três vezes o nome do jogo. Numa tela de
       420px a mesma linha simplesmente **saía pelos dois lados** da tela.

       O teto de 460px e a fonte de 20px são os de sempre: numa tela grande o
       logo bate no teto de 200px, `2.6 * 200` passa de 460 e tudo fica onde
       estava. O piso de 200px é o menor em que "Patch beta 0.4.1" cabe numa
       linha só — ele era 300 enquanto "Patch" e "Créditos" dividiam a linha
       lado a lado, e caiu quando os três botões passaram a ser empilhados (ver
       #menuTitleExtras). */
    --capa-logo: clamp(44px, min(calc(0.13 * var(--largura-tela)), calc(0.21 * var(--altura-tela))), 200px);
    --capa-linha: min(
      calc(var(--largura-tela) - 40px),
      clamp(200px, calc(2.6 * var(--capa-logo)), 460px));
  }
  /* Fora da faixa estreita a tela inicial segue o enquadramento da ilustração
     (#cenaInicial): logo e botões numa coluna encostada à esquerda, por cima do
     céu, e o centro e a direita da tela ficam para o mago verde e o demônio.
     Centralizada, a coluna cairia bem em cima do rosto do mago. E encostada
     no topo, e não no meio da altura: a metade de baixo da esquerda é dos três
     magos, e no meio os botões caíam em cima deles.

     A coluna encosta à esquerda, mas logo e botões ficam centrados **entre
     si**: é uma grade de uma coluna só, da largura da peça mais larga (o logo
     numa tela grande, a linha de botões numa baixa), e cada peça centrada
     nela. Em flex, cada uma encostava à esquerda na própria largura, e o eixo
     dos botões ficava deslocado do eixo do logo. `#menuBody` encolhe até o
     conteúdo para não esticar a coluna com a largura de 640px dele. */
  body:not(.tela-estreita) #menuViewHome.menu-view-ativa {
    display: grid;
    grid-template-columns: max-content;
    align-content: start;
    justify-content: start;
    justify-items: center;
    padding-top: clamp(16px, calc(0.05 * var(--altura-tela)), 56px);
    padding-left: clamp(24px, calc(0.05 * var(--largura-tela)), 110px);
  }
  body:not(.tela-estreita) #menuBody {
    width: auto;
  }
  /* ===== O acampamento (#menuViewAcampamento) =====
     A visão entre a capa e as telas (ver public/js/acampamento.js). O palco é a
     cena em pixel art: um canvas **lógico** de 180 px de altura, ampliado por
     um fator inteiro (o maior que cabe) e centrado; as oito estações são
     `<button>` transparentes postos pelo JS sobre o desenho de cada objeto.

     **O palco ocupa a janela inteira, por baixo da barra superior.** A escala é
     medida na área do palco, e a spec a fixa pela janela: 1920×1080 é 6×, e
     740×360 é 2× com 370 lógicos de largura. Descontada a barra (56px), a
     primeira cairia para 5× e a segunda para 1×. Por isso a visão devolve a
     faixa que #menu reserva à barra (`margin-top` negativo) e a barra fica
     transparente aqui, sobre a muralha — que é cenário, e não estação. */
  #menuViewAcampamento {
    padding: 0;
    margin-top: calc(-1 * var(--altura-topbar));
    height: calc(100% + var(--altura-topbar));
  }
  /* **As telas do menu abrem *sobre* o acampamento, e não no lugar dele.**
     A visão continua sendo trocada como sempre (só uma tem .menu-view-ativa),
     mas a do acampamento ganha uma segunda forma: com uma tela aberta ela sai
     do fluxo e vira o **fundo** dela (.menu-view-fundo, posta por
     `acampamentoComoFundo` em public/js/acampamento.js). Antes a tela trocava
     o acampamento pelo tijolo do body, e o jogador saía de um lugar para uma
     parede; assim a moldura de pedra lê como um painel aberto *no* lugar onde
     ele estava.

     `z-index: -1` e não uma camada positiva: #menu já é um contexto de
     empilhamento próprio (position/z-index), então o -1 cai atrás do conteúdo
     dele — os painéis das telas, que são de fluxo — sem sair para trás do
     fundo da página. É também o que dispensa mexer no empilhamento de todas
     as outras visões.

     O fundo não anima e não pega gesto nenhum: é um retrato, pintado uma vez
     ao entrar neste modo (e de novo quando a janela muda de tamanho ou um dado
     da cena muda). `inert` tira as oito estações do Tab, do direcional e do
     clique — elas estão atrás de um painel, e um alvo de toque invisível ali
     abriria outra tela por baixo da que está aberta. */
  #menuViewAcampamento.menu-view-fundo {
    display: flex;
    position: absolute;
    inset: 0;
    z-index: -1;
    margin-top: 0;
    height: auto;
    padding: 0;
    pointer-events: none;
  }
  /* Véu sobre o fundo: a cena é clara em algumas regiões (a areia, o gelo) e a
     moldura de pedra dos painéis tem sombra escura em volta. Escurecer o
     acampamento inteiro devolve a separação entre o painel e o que está atrás
     dele sem apagar a cena.

     Ele **entra em degraus**, e não de uma vez: a classe é posta no mesmo
     quadro em que a tela abre, e o corte seco lia como a cena trocando de cor
     num piscar. Quatro degraus em 240ms (`steps(4, end)`, nunca uma rampa
     contínua — ver a regra de pixel art em public/css/CLAUDE.md), que é o
     tempo que a tela leva para se montar por cima. A animação recomeça sozinha
     a cada abertura, porque a classe é reposta. */
  #menuViewAcampamento.menu-view-fundo::after {
    content: '';
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.62);
    animation: acampamento-fundo-escurece 0.24s steps(4, end) 1;
  }
  @keyframes acampamento-fundo-escurece {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  @media (prefers-reduced-motion: reduce) {
    #menuViewAcampamento.menu-view-fundo::after { animation: none; }
  }
  .acampamento-palco {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
    /* O arrasto do dedo no palco é o joystick do passeio (ver "Joystick de
       toque" em acampamento.js), e não rolagem nem zoom do navegador. */
    touch-action: none;
    /* A sobra de menos de uma escala nas bordas (a largura lógica é inteira, a
       janela nem sempre é múltipla dela). */
    background: var(--cor-acampamento-fundo);
  }
  /* Tamanho e posição saem do JS, em pixel inteiro (largura lógica × escala).
     `pixelated` é o que faz a ampliação inteira sair nítida; sem o fundo da
     regra genérica de `canvas`, que pintaria um retângulo cinza antes do
     primeiro quadro. */
  #acampamentoCanvas {
    position: absolute;
    left: 0;
    top: 0;
    display: block;
    background: none;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
  }
  .acampamento-estacoes {
    position: absolute;
    inset: 0;
    pointer-events: none;
  }
  /* A estação é um botão **sem material nenhum**: o que se vê é o objeto
     desenhado no canvas, e o botão só dá a ele alvo de toque, foco, rótulo e
     clique. Dois ids para vencer as duas regras genéricas que todo botão do
     menu recebe — `#menu button:not(...)` (fundo vermelho, largura cheia) e a
     de `button:hover` (levantar e escalar), cuja lista de `:not()` não precisa
     crescer por causa disto. */
  #menu #acampamentoEstacoes .acampamento-estacao {
    position: absolute;
    pointer-events: auto;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    transform: none;
    filter: none;
    transition: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  #menu #acampamentoEstacoes .acampamento-estacao:hover,
  #menu #acampamentoEstacoes .acampamento-estacao:active {
    background: none;
    box-shadow: none;
    transform: none;
    filter: none;
  }
  /* **O realce de foco da estação é desenhado no canvas**, e não pelo anel
     branco da regra geral (ver "Realce de foco" em public/css/CLAUDE.md). O
     anel do CSS contornaria a **área de toque**, que é maior que o objeto (24
     lógicos, 44px no toque) e não tem forma nenhuma na tela; o canvas contorna
     a **silhueta** do próprio desenho, na mesma cor do sistema — um brilho
     branco em degraus por fora do contorno escuro do recorte, que o mantém
     visível no chão claro de gelo e de areia — e em pixel da cena (ver
     `brilhoDaEstacao` em acampamentoCena.js). O foco
     continua visível sem ponteiro; só muda quem o pinta. */
  body.foco-sem-ponteiro #acampamentoEstacoes .acampamento-estacao:focus,
  #acampamentoEstacoes .acampamento-estacao:focus-visible {
    outline: none;
  }
  /* Conquista nova ainda não vista (ver achievementNotify.js): a mesma
     bolinha dourada de #changelogDot, no canto de cima da estante. Não é o "!"
     da chegada — medalha não está entre as novidades que chamam atenção na
     volta da partida (a tela de fim de partida já a mostrou inteira) —, e por
     isso fica aqui, fora do canvas, acesa até a tela de Conquistas ser vista,
     como ficava na aba. */
  #acampamentoEstacoes .acampamento-estacao.tem-conquista-nova::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    width: 10px;
    height: 10px;
    background: var(--cor-ouro);
    box-shadow: 2px 2px 0 var(--cor-pedra-borda);
    animation: changelog-dot-pulso 1s steps(1, end) infinite;
  }
  /* A placa: nome da estação · tela, e o dado resumido embaixo. Mesmo material
     do balão de dica de talentos (fundo da barra, borda dourada de 2px, sombra
     sólida deslocada, canto reto) — as duas são texto sobre o acampamento. A
     posição é do JS, em pixel inteiro e presa dentro do palco. */
  .acampamento-placa {
    position: absolute;
    z-index: 2;
    pointer-events: none;
    padding: 4px 10px;
    background: var(--cor-barra-fundo);
    border: 2px solid var(--cor-ouro);
    box-shadow: 4px 4px 0 var(--cor-pedra-borda);
    color: var(--cor-texto);
    text-align: center;
    white-space: nowrap;
    line-height: 1.3;
    animation: acampamento-placa-entra 0.12s steps(2, end) 1;
  }
  .acampamento-placa[hidden] {
    display: none;
  }
  .acampamento-placa-nome,
  .acampamento-placa-resumo {
    display: block;
  }
  .acampamento-placa-nome {
    font-size: 13px;
    font-weight: 700;
  }
  .acampamento-placa-resumo {
    font-size: 12px;
    color: var(--cor-ouro-suave);
  }
  @keyframes acampamento-placa-entra {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  /* O marco do portão (ver `atualizarMarcoDoPortao` em acampamento.js): "Jogar"
     com a seta para o portão, fixo em toda visita. O material é o da placa com
     o fundo **dourado** e o texto escuro — a placa é o que o ponteiro revela, o
     marco é o que a cena anuncia sozinha, e os dois não podem se confundir. A
     seta é um triângulo recortado em `clip-path` (como a do nó parado do mapa)
     e cutuca o portão em dois degraus. Abaixo da placa e da dica (z-index 1):
     quando se encostam, a frase que responde ao jogador é a que importa. */
  .acampamento-marco-portao {
    position: absolute;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 0.45em;
    padding: 0.25em 0.6em;
    background: var(--cor-ouro);
    border: 2px solid var(--cor-barra-fundo);
    box-shadow: 3px 3px 0 var(--cor-pedra-borda);
    color: var(--cor-barra-fundo);
    /* Acompanha a escala da cena (`--escala`, posta pelo JS): 13px a 2×, 14 a
       4×, 21 a 6× — num texto de tamanho fixo o marco some no 1080p. */
    font-size: max(13px, calc(var(--escala, 1) * 3.5px));
    font-weight: 700;
    line-height: 1.3;
    white-space: nowrap;
    cursor: pointer;
    animation: acampamento-placa-entra 0.24s steps(2, end) 1;
  }
  .acampamento-marco-portao[hidden] {
    display: none;
  }
  .acampamento-marco-portao[data-lado='direita'] {
    flex-direction: row-reverse;
  }
  .acampamento-marco-portao[data-lado='acima'] {
    flex-direction: row-reverse;
  }
  .acampamento-marco-portao-seta {
    width: 0.5em;
    height: 0.8em;
    background: currentColor;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
    animation: acampamento-marco-cutuca 0.8s steps(1, end) infinite;
    --cutucada: 2px 0;
  }
  .acampamento-marco-portao[data-lado='direita'] .acampamento-marco-portao-seta {
    clip-path: polygon(100% 0, 0 50%, 100% 100%);
    --cutucada: -2px 0;
  }
  .acampamento-marco-portao[data-lado='acima'] .acampamento-marco-portao-seta {
    width: 0.8em;
    height: 0.5em;
    clip-path: polygon(0 100%, 50% 0, 100% 100%);
    --cutucada: 0 -2px;
  }
  @keyframes acampamento-marco-cutuca {
    0% { translate: 0 0; }
    50% { translate: var(--cutucada); }
  }
  @media (prefers-reduced-motion: reduce) {
    .acampamento-marco-portao-seta {
      animation: none;
    }
  }
  /* A dica de andar (ver public/js/dicaDoPasseio.js): o mesmo material da
     placa, embaixo dos pés do herói, na primeira visita. `pointer-events: none`
     porque ela não pode roubar o dedo de quem vai arrastar para andar — e é por
     isso que não tem botão de fechar: ela sai quando o jogador anda. A posição
     é do JS, em pixel inteiro e presa dentro do palco. Acima da placa (z-index
     3) porque, quando as duas se encostam, a frase que ensina é a que importa. */
  .acampamento-dica-passeio {
    position: absolute;
    z-index: 3;
    pointer-events: none;
    width: max-content;
    max-width: min(320px, calc(100% - 12px));
    box-sizing: border-box;
    padding: 6px 10px;
    background: var(--cor-barra-fundo);
    border: 2px solid var(--cor-ouro);
    box-shadow: 4px 4px 0 var(--cor-pedra-borda);
    color: var(--cor-texto-fraco);
    font-size: 12px;
    line-height: 1.5;
    text-align: center;
    animation: acampamento-placa-entra 0.24s steps(2, end) 1;
  }
  .acampamento-dica-passeio[hidden] {
    display: none;
  }
  .dica-passeio-titulo,
  .dica-passeio-linha {
    display: block;
  }
  .dica-passeio-titulo {
    margin-bottom: 2px;
    color: var(--cor-ouro);
    font-size: 13px;
  }
  /* A capinha no tamanho de dentro de frase — as mesmas medidas da dica do
     tiro automático e de `body.touch-mode` (20px de caixa, SVG de 12px), e não
     um terceiro tamanho. */
  .acampamento-dica-passeio .tecla,
  .acampamento-dica-passeio .glifo {
    min-width: 20px;
    height: 20px;
    padding: 0 5px;
    font-size: 12px;
    vertical-align: middle;
  }
  .acampamento-dica-passeio .tecla + .tecla {
    margin-left: 2px;
  }
  .acampamento-dica-passeio .glifo svg {
    width: 12px;
    height: 12px;
  }
  /* A barra superior sobre a cena: sem fundo nem borda, e sem pegar o clique
     da faixa que ela cobre — a muralha e o alto do portão estão ali embaixo.
     Os botões dela continuam com o material e o clique de sempre. */
  body:has(#menuViewAcampamento.menu-view-ativa) #menuTopbar {
    background: transparent;
    border-bottom-color: transparent;
    pointer-events: none;
  }
  body:has(#menuViewAcampamento.menu-view-ativa) #menuTopbarLinha > *,
  body:has(#menuViewAcampamento.menu-view-ativa) #menuTopbarDir > * {
    pointer-events: auto;
  }
  /* ===== O mapa da campanha (#menuViewHorde) =====
     A tela "Jogar" (ver public/js/mapaDaCampanha.js e o change
     `mapa-da-campanha`). É o palco do acampamento com outro conteúdo: um canvas
     **lógico** de 180 px de altura ampliado por um fator inteiro (`--px`,
     escrito pelo JS junto com o tamanho e a posição do canvas), e os nós, as
     fogueiras e a ficha como elementos por cima dele.

     Todas as medidas desta seção são múltiplos de `--px` (o pixel de arte da
     escala atual), e é isso que mantém texto, moldura e sombra na mesma grade
     do desenho em qualquer tamanho de tela. As do estudo
     (docs/sugestoes-mapa-da-campanha.html) foram mantidas.

     Como o acampamento, **o palco passa por baixo da barra superior**, que fica
     transparente aqui: a escala é medida pela janela inteira. */
  #menuViewHorde {
    padding: 0;
    margin-top: calc(-1 * var(--altura-topbar));
    height: calc(100% + var(--altura-topbar));
    overflow: hidden;
  }
  body:has(#menuViewHorde.menu-view-ativa) #menuTopbar {
    background: transparent;
    border-bottom-color: transparent;
    pointer-events: none;
  }
  body:has(#menuViewHorde.menu-view-ativa) #menuTopbarLinha > *,
  body:has(#menuViewHorde.menu-view-ativa) #menuTopbarDir > * {
    pointer-events: auto;
  }
  .mapa-palco {
    --px: 4px;
    /* Piso de legibilidade do texto da ficha e do canto: em telas baixas
       (844x390, 740x360) `--px` cai para 2, e `calc(var(--px) * 4.5..5)` vira
       9-10px. Nesse tamanho o "Chakra Petch" (a fonte do corpo) arredonda o
       glifo do espaço para 0 em algumas palavras e não em outras — medido:
       "Sentinela de Cascalho" e "Criar sala" colavam as duas palavras,
       "Sala pública" não, todos no mesmo --px. Não é `word-spacing` (o
       computado é `normal`/0px nos dois): é o hinting do navegador
       arredondando o avanço de cada glifo a pixel inteiro, que em fonte
       estreita como esta come o espaço inteiro por sorte de largura da
       string. Todo texto da ficha/canto usa `max(calc(var(--px) * n), var(--mapa-fonte-min))`
       por isso — o piso só entra quando `--px` está pequeno, e a escala
       normal (--px >= 3) continua saindo de `--px` puro. */
    --mapa-fonte-min: 11px;
    /* A mesma silhueta de `--recorte-botao-pixel`, mas com o degrau em
       `--px` em vez de 4px/8px fixos: "Criar sala" e os botões do canto
       encolhem no touch/tela baixa (min-height 44px numa fonte de 10px), e o
       degrau fixo do botão genérico comia a caixa inteira — o botão saía como
       um losango em vez de um retângulo com canto cortado. Como os quatro
       cantos precisam da mesma silhueta em duas escalas de recuo (`--px` e
       `--px * 2`), ela é redeclarada aqui e usada só pelos botões da ficha e
       do canto (ver `--recorte-botao-mapa` mais abaixo). */
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
    /* A subida rola pelo arrasto do próprio mapa (mapaDaCampanha.js), e não
       pelo gesto nativo. */
    touch-action: none;
    background: #1a1918;
    font-size: calc(var(--px) * 5);
    line-height: 1.25;
    color: var(--cor-texto);
  }
  .mapa-palco {
    --recorte-botao-mapa: polygon(
      calc(var(--px) * 2) 0, calc(100% - var(--px) * 2) 0,
      calc(100% - var(--px) * 2) var(--px), calc(100% - var(--px)) var(--px),
      calc(100% - var(--px)) calc(var(--px) * 2), 100% calc(var(--px) * 2),
      100% calc(100% - var(--px) * 2),
      calc(100% - var(--px)) calc(100% - var(--px) * 2), calc(100% - var(--px)) calc(100% - var(--px)),
      calc(100% - var(--px) * 2) calc(100% - var(--px)), calc(100% - var(--px) * 2) 100%,
      calc(var(--px) * 2) 100%,
      calc(var(--px) * 2) calc(100% - var(--px)), var(--px) calc(100% - var(--px)),
      var(--px) calc(100% - var(--px) * 2), 0 calc(100% - var(--px) * 2),
      0 calc(var(--px) * 2),
      var(--px) calc(var(--px) * 2), var(--px) var(--px), calc(var(--px) * 2) var(--px)
    );
  }
  #mapaCanvas {
    position: absolute;
    left: 0;
    top: 0;
    display: block;
    background: none;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
  }
  /* `display` explícito vence o `hidden` do HTML: a ficha e a fogueira
     escondidas continuariam na tela (e a ficha vazia, pegando o clique). */
  .mapa-ficha[hidden],
  .mapa-fogueira[hidden] {
    display: none;
  }
  .mapa-regioes,
  .mapa-nos {
    position: absolute;
    inset: 0;
    pointer-events: none;
  }
  .mapa-regiao {
    position: absolute;
    white-space: nowrap;
    font-size: calc(var(--px) * 5);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--cor-texto-fraco);
    text-shadow: var(--px) var(--px) 0 #000;
  }

  /* O nó: um quadrado na cor do tema da região, com o número (ou "?"/"∞").
     Dois ids para vencer as regras genéricas de botão do menu
     (`#menu button:not(...)`, o fundo vermelho com moldura recortada, e o
     `button:hover` que levanta e escala) sem crescer a lista de `:not()`. */
  #menu #mapaNos .mapa-no {
    --tema: #8a7f72;
    /* O anel de foco vai **por fora** (2 px de arte): o nó é pequeno e o
       número ocupa o miolo, e o recuo para dentro cortaria o algarismo (ver
       "Peça sem material nenhum" em public/css/CLAUDE.md). O nó não tem
       `clip-path`, então o anel externo é pintado. */
    --recuo-realce-foco: calc(var(--px) * 2);
    position: absolute;
    display: grid;
    place-items: center;
    width: calc(var(--px) * 18);
    height: calc(var(--px) * 18);
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    clip-path: none;
    isolation: auto;
    pointer-events: auto;
    cursor: pointer;
    /* A borda padrão dos quadrados de nível (ver "Campos e quadrados de nível
       em pixel art"): contorno de 1 px de arte e a escadinha de dois degraus de
       `--recorte-botao-mapa`, trocando de estado só por
       `--cor-borda-campo`/`--cor-fundo-campo`. Desenhada em seis camadas, e não
       recortada por `clip-path`, porque o nó pinta fora de si (a seta do nó
       atual, o rótulo, as medalhas e as fogueiras) — como `.talent-no`. As três
       de cima são o miolo recuado pelo contorno; as três de baixo, a silhueta. */
    --cor-fundo-campo: var(--tema);
    --cor-borda-campo: #000;
    --b: var(--px);
    background-color: transparent;
    background-image:
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo));
    background-size:
      calc(100% - var(--px) * 4 - var(--b) * 2) calc(100% - var(--b) * 2),
      calc(100% - var(--px) * 2 - var(--b) * 2) calc(100% - var(--px) * 2 - var(--b) * 2),
      calc(100% - var(--b) * 2) calc(100% - var(--px) * 4 - var(--b) * 2),
      calc(100% - var(--px) * 4) 100%,
      calc(100% - var(--px) * 2) calc(100% - var(--px) * 2),
      100% calc(100% - var(--px) * 4);
    background-position:
      calc(var(--px) * 2 + var(--b)) var(--b),
      calc(var(--px) + var(--b)) calc(var(--px) + var(--b)),
      var(--b) calc(var(--px) * 2 + var(--b)),
      calc(var(--px) * 2) 0,
      var(--px) var(--px),
      0 calc(var(--px) * 2);
    background-repeat: no-repeat;
    color: #100f0e;
    font-family: inherit;
    font-weight: bold;
    font-size: calc(var(--px) * 9);
    line-height: 1;
    box-shadow: none;
    transform: none;
    filter: none;
    transition: none;
    -webkit-tap-highlight-color: transparent;
  }
  #menu #mapaNos .mapa-no::before,
  #menu #mapaNos .mapa-no::after {
    content: none;
  }
  #menu #mapaNos .mapa-no:hover,
  #menu #mapaNos .mapa-no:active {
    background-color: transparent;
    box-shadow: none;
    transform: none;
    filter: none;
  }
  #menu #mapaNos .mapa-no:hover {
    filter: brightness(1.12);
  }
  #menu #mapaNos .mapa-no[data-regiao="pedra"] { --tema: #8a7f72; }
  #menu #mapaNos .mapa-no[data-regiao="gelo"] { --tema: #7cc4e8; }
  #menu #mapaNos .mapa-no[data-regiao="dunas"] { --tema: #d9b25f; }
  #menu #mapaNos .mapa-no[data-regiao="chamas"] { --tema: #e2572b; }
  #menu #mapaNos .mapa-no[data-regiao="semfim"] { --tema: #a06cf0; }
  #menu #mapaNos .mapa-no.trancado {
    --tema: #2e2d2c;
    color: var(--cor-texto-fraco);
  }
  #menu #mapaNos .mapa-no.vortice {
    color: #fff;
  }
  /* Fase completa: a moldura verde, por fora da borda preta. */
  #menu #mapaNos .mapa-no.completo {
    --cor-borda-campo: var(--cor-sucesso);
  }
  /* O nó atual pula em dois degraus. É o único que pula, e é o que diz onde o
     jogador está na campanha — não há boneco nem marcador. `translate`, e não
     `transform`, para não disputar com nada. Um pixel de arte a cada 0,8 s: com
     2 px a cada 0,5 s ele somava ao resto e o mapa parecia tremer. */
  #menu #mapaNos .mapa-no.atual {
    animation: mapa-no-pula 1.6s steps(2, jump-none) infinite;
  }
  @keyframes mapa-no-pula {
    from { translate: 0 0; }
    to { translate: 0 calc(var(--px) * -1); }
  }
  /* Com movimento reduzido o nó atual não pula, e continua distinguível: uma
     seta dourada fixa em cima dele. */
  #menu #mapaNos .mapa-no.atual-parado::before,
  #menu #mapaNos .mapa-no.atual::before {
    content: '';
    position: absolute;
    left: 50%;
    top: calc(var(--px) * -7);
    width: calc(var(--px) * 4);
    height: calc(var(--px) * 3);
    margin-left: calc(var(--px) * -2);
    background: var(--cor-ouro);
    clip-path: polygon(0 0, 100% 0, 50% 100%);
    pointer-events: none;
  }
  #menu #mapaNos .mapa-no.atual::before {
    display: none;
  }
  /* O nó com o ponteiro em cima ou com a ficha aberta: o contorno preto vira
     dourado, no mesmo lugar — o nó não cresce nem ganha moldura afastada. */
  #menu #mapaNos .mapa-no:hover,
  #menu #mapaNos .mapa-no.selecionado {
    --cor-borda-campo: var(--cor-ouro);
  }
  #menu #mapaNos .mapa-no.destrancando {
    animation: mapa-no-brilha 0.2s steps(1, end) infinite;
  }
  @keyframes mapa-no-brilha {
    0% { --cor-fundo-campo: #fff; }
    50% { --cor-fundo-campo: var(--tema); }
  }
  .mapa-no-numero {
    pointer-events: none;
  }
  /* O rótulo do nó ("Fase 3"): só com o ponteiro em cima ou o foco nele. */
  .mapa-no-rotulo {
    position: absolute;
    left: 50%;
    bottom: calc(100% + var(--px) * 3);
    transform: translateX(-50%);
    display: none;
    white-space: nowrap;
    font-size: max(calc(var(--px) * 5), var(--mapa-fonte-min));
    font-weight: normal;
    color: var(--cor-texto);
    text-shadow: var(--px) var(--px) 0 #000;
    pointer-events: none;
  }
  #mapaNos .mapa-no:hover .mapa-no-rotulo,
  #mapaNos .mapa-no:focus .mapa-no-rotulo {
    display: block;
  }
  /* Acima da seta do nó parado e da moldura da ficha aberta, e não em cima
     delas. */
  #mapaNos .mapa-no.atual-parado .mapa-no-rotulo {
    bottom: calc(100% + var(--px) * 9);
  }
  /* As quatro bolinhas de medalha, embaixo do nó aberto. */
  .mapa-no-pips {
    position: absolute;
    left: 50%;
    top: calc(100% + var(--px) * 3);
    transform: translateX(-50%);
    display: flex;
    gap: calc(var(--px) * 3);
    pointer-events: none;
  }
  /* Borda preta de 1 px de arte: sem ela o bronze e o gelo somem no chão. */
  .mapa-no-pips i {
    display: block;
    width: calc(var(--px) * 3);
    height: calc(var(--px) * 3);
    background: #2a2928;
    box-shadow: 0 0 0 var(--px) #000;
  }
  .mapa-no-pips i.tier-bronze { background: var(--cor-tier-bronze); }
  .mapa-no-pips i.tier-prata { background: var(--cor-tier-prata); }
  .mapa-no-pips i.tier-ouro { background: var(--cor-tier-ouro); }
  .mapa-no-pips i.tier-diamante { background: var(--cor-tier-diamante); }
  .mapa-no-pips i.tier-lendario { background: var(--cor-tier-lendario); }
  /* A medalha nova acendendo, no começo do momento de destrancar. */
  #mapaNos .mapa-no.medalha-acendendo .mapa-no-pips i.tem {
    animation: mapa-pip-acende 0.12s steps(1, end) infinite;
  }
  @keyframes mapa-pip-acende {
    0% { background: #fff; }
  }
  /* A fogueira: as salas públicas abertas daquela fase, com a contagem. */
  .mapa-fogueira {
    position: absolute;
    display: flex;
    align-items: flex-end;
    gap: var(--px);
    color: #ffcf4d;
    font-size: calc(var(--px) * 5);
    line-height: 1;
    text-shadow: var(--px) var(--px) 0 #000;
    pointer-events: none;
  }
  .mapa-fogueira i {
    position: relative;
    display: block;
    width: calc(var(--px) * 4);
    height: calc(var(--px) * 5);
    background: #ff7a1a;
    animation: mapa-fogueira-chama 0.9s steps(2, jump-none) infinite;
  }
  .mapa-fogueira i::after {
    content: '';
    position: absolute;
    left: var(--px);
    top: calc(var(--px) * -2);
    width: calc(var(--px) * 2);
    height: calc(var(--px) * 3);
    background: #ffcf4d;
  }
  .mapa-fogueira i::before {
    content: '';
    position: absolute;
    left: calc(var(--px) * -1);
    bottom: calc(var(--px) * -1);
    width: calc(var(--px) * 6);
    height: var(--px);
    background: #5a3b24;
  }
  .mapa-fogueira b {
    font-weight: normal;
  }
  @keyframes mapa-fogueira-chama {
    to { height: calc(var(--px) * 4); }
  }

  /* A ficha da fase: **colada à direita do nó escolhido**, 132 px de arte de
     largura, entre a barra superior e o canto. A borda em volta é a cor do tema da
     região.

     Ela é uma **coluna flex de dois andares**, e não um único bloco que rola
     por dentro: em tela baixa (844x390 touch, 740x360) a ficha inteira não
     cabia em pé, e a ação principal ("Criar sala", em `.mapa-ficha-rodape`)
     ficava abaixo da dobra, escondida pela rolagem interna — a única forma de
     alcançá-la era rolar a ficha inteira até o fim, e um jogador que não
     soubesse disso via a ficha e achava que não dava para criar sala. Quem
     rola agora é só `.mapa-ficha-corpo` (cabeçalho, chefões, medalhas, marca,
     aviso e a lista de salas — que pode crescer bastante com 1 a 3 salas
     listadas); o rodapé fica **fora** da área que rola, sempre visível no fim
     da ficha, em qualquer tamanho de tela. Fase trancada não tem rodapé (não
     oferece "Criar sala") e o corpo sozinho ocupa a ficha inteira, rolando
     como sempre rolou. */
  .mapa-ficha {
    --tema: #8a7f72;
    position: absolute;
    z-index: 5;
    /* `left`, `top` e `max-height` são escritos por `posicionarFicha`
       (mapaDaCampanha.js): a ficha abre colada à direita do nó escolhido, na
       altura dele, e tem a altura do conteúdo — o corpo só rola quando passa
       do espaço entre a barra de cima e os botões do canto. */
    width: calc(var(--px) * 132);
    max-width: calc(100% - var(--px) * 8);
    box-sizing: border-box;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    /* A borda padrão (a mesma dos quadrados de nível): contorno de 1 px de
       arte na cor do tema da região e a escadinha de `--recorte-botao-mapa`. */
    clip-path: var(--recorte-botao-mapa);
    background-color: transparent;
    background-image:
      linear-gradient(var(--cor-superficie-baixa), var(--cor-superficie-baixa)),
      linear-gradient(var(--cor-superficie-baixa), var(--cor-superficie-baixa)),
      linear-gradient(var(--cor-superficie-baixa), var(--cor-superficie-baixa)),
      linear-gradient(var(--tema), var(--tema));
    background-size:
      calc(100% - var(--px) * 2) calc(100% - var(--px) * 6),
      calc(100% - var(--px) * 4) calc(100% - var(--px) * 4),
      calc(100% - var(--px) * 6) calc(100% - var(--px) * 2),
      100% 100%;
    background-position: center;
    background-repeat: no-repeat;
    animation: mapa-ficha-surge 0.18s steps(3, end) both;
  }
  .mapa-ficha[data-regiao="gelo"] { --tema: #7cc4e8; }
  .mapa-ficha[data-regiao="dunas"] { --tema: #d9b25f; }
  .mapa-ficha[data-regiao="chamas"] { --tema: #e2572b; }
  .mapa-ficha[data-regiao="semfim"] { --tema: #a06cf0; }
  .mapa-ficha[data-tipo="trancada"] { --tema: #555; }
  @keyframes mapa-ficha-surge {
    from { opacity: 0; transform: scale(0.8); }
    to { opacity: 1; transform: none; }
  }
  .mapa-ficha-corpo {
    /* `1 1 auto` e não `1`: com a base em 0% o corpo não sabe encolher abaixo
       do conteúdo dele, e o rodapé seria empurrado para fora da ficha em vez
       de o corpo rolar — mesmo defeito e mesma correção de `#relicarioList`
       (ver "Rodapé de ação da Loja" mais abaixo). */
    flex: 1 1 auto;
    min-height: 0;
    box-sizing: border-box;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Só rola em tela que não cabe a ficha, e mesmo assim sem barra visível. */
    scrollbar-width: none;
    display: grid;
    align-content: start;
    gap: calc(var(--px) * 4);
    padding: calc(var(--px) * 5) calc(var(--px) * 6);
  }
  /* O rodapé: o interruptor de sala pública e "Criar sala". Item de flex
     comum, **fora** do corpo rolável — não `position: sticky` (que exigiria
     compensar o padding do corpo com margem negativa): como ele é um irmão
     depois de `.mapa-ficha-corpo` no fluxo normal, o próprio `flex-shrink: 0`
     implícito já garante que ele nunca é empurrado para fora da ficha nem
     comido pela rolagem. Sem sombra nem linha: a ficha é chapada. */
  .mapa-ficha-corpo::-webkit-scrollbar { display: none; }
  .mapa-ficha-rodape {
    flex: none;
    display: grid;
    gap: calc(var(--px) * 4);
    padding: calc(var(--px) * 3) calc(var(--px) * 6) calc(var(--px) * 5);
  }
  .mapa-ficha-cabeca {
    display: grid;
    gap: var(--px);
  }
  .mapa-ficha-regiao,
  .mapa-ficha-rotulo {
    margin: 0;
    font-size: max(calc(var(--px) * 4.5), var(--mapa-fonte-min));
    font-weight: normal;
    color: var(--cor-texto-fraco);
    letter-spacing: 0.1em;
    text-transform: uppercase;
  }
  .mapa-ficha-nome {
    margin: 0;
    font-size: calc(var(--px) * 8);
    line-height: 1.1;
    color: var(--cor-texto);
  }
  .mapa-ficha-texto {
    margin: 0;
    font-size: max(calc(var(--px) * 5), var(--mapa-fonte-min));
    color: var(--cor-texto-medio);
  }
  .mapa-ficha-aviso {
    margin: 0;
    font-size: max(calc(var(--px) * 4.5), var(--mapa-fonte-min));
    color: #ffb3b3;
  }
  .mapa-medalhas {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--px) calc(var(--px) * 4);
  }
  .mapa-medalha {
    display: flex;
    align-items: center;
    gap: calc(var(--px) * 2);
    min-width: 0;
    font-size: max(calc(var(--px) * 4.5), var(--mapa-fonte-min));
    color: var(--cor-texto-fraco);
  }
  .mapa-medalha i {
    flex: none;
    width: calc(var(--px) * 4);
    height: calc(var(--px) * 4);
    background: #2a2928;
  }
  .mapa-medalha.tem { color: var(--cor-texto); }
  .mapa-medalha.tem.tier-bronze i { background: var(--cor-tier-bronze); }
  .mapa-medalha.tem.tier-prata i { background: var(--cor-tier-prata); }
  .mapa-medalha.tem.tier-ouro i { background: var(--cor-tier-ouro); }
  .mapa-medalha.tem.tier-diamante i { background: var(--cor-tier-diamante); }
  .mapa-medalha.tem.tier-lendario i { background: var(--cor-tier-lendario); }
  /* "Sua marca" e "Sala pública" são pares rótulo → valor com o valor na
     borda oposta. Soltos na ficha, o olho não ligava um ao outro; a faixa
     escura, chapada e sem bisel, é o que diz que os dois lados são uma linha
     só. */
  .mapa-ficha-marca,
  .mapa-ficha-publica {
    padding: calc(var(--px) * 2) calc(var(--px) * 3);
    background: rgba(0, 0, 0, 0.3);
  }
  .mapa-ficha-marca {
    margin: 0;
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: calc(var(--px) * 3);
    font-size: max(calc(var(--px) * 5), var(--mapa-fonte-min));
  }
  .mapa-ficha-marca b {
    font-weight: normal;
    color: var(--cor-ouro);
  }
  .mapa-ficha-marca.marca-nunca b {
    color: var(--cor-texto-fraco);
  }
  .mapa-ficha-publica {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: calc(var(--px) * 3);
    font-size: max(calc(var(--px) * 5), var(--mapa-fonte-min));
  }
  /* O interruptor é o `.toggle-switch` de sempre, na grade do mapa. */
  #mapaFicha .toggle-switch {
    --recuo-realce-foco: 2px;
  }
  #mapaFicha .toggle-switch-track {
    width: calc(var(--px) * 16);
    height: calc(var(--px) * 8);
    padding: var(--px);
    border-width: var(--px);
  }
  #mapaFicha .toggle-switch-thumb {
    width: calc(var(--px) * 5);
  }
  #mapaFicha .toggle-switch.ativo .toggle-switch-thumb {
    margin-left: calc(100% - var(--px) * 5);
  }
  /* "Criar sala" e "Entrar" são os botões de ação do menu (o vermelho com a
     moldura recortada da regra genérica), na grade do mapa. Dois ids para
     vencer o tamanho da regra genérica (`width: 100%`, 20px, 14px 32px). */
  #menu #mapaFicha .mapa-ficha-acao {
    width: auto;
    min-width: 0;
    justify-self: start;
    padding: calc(var(--px) * 2) calc(var(--px) * 5);
    font-size: max(calc(var(--px) * 5), var(--mapa-fonte-min));
    line-height: 1.25;
  }
  /* "Jogar" (cria a sala na fase): a ação principal ocupa a largura inteira
     do rodapé, mais alta que o "Entrar" das salas listadas. */
  #menu #mapaFicha .mapa-ficha-criar {
    width: 100%;
    justify-self: stretch;
    padding: calc(var(--px) * 4) calc(var(--px) * 6);
    font-size: max(calc(var(--px) * 6), var(--mapa-fonte-min));
  }
  #menu #mapaFicha .mapa-ficha-acao:not(:disabled):hover {
    transform: none;
  }
  #menu #mapaFicha .mapa-ficha-acao:disabled {
    background: var(--cor-borda-media);
    color: var(--cor-texto-fraco);
    cursor: default;
  }
  /* Em `--px` baixo (2, nas telas de 844x390 e 740x360) o botão encolhe a
     ponto da escadinha de dois degraus **fixos** (4px/8px, ver
     `--recorte-botao-pixel`) comer a caixa inteira e virar um losango — o
     mesmo defeito que os botões de ícone já resolvem com um degrau só, aqui
     resolvido trocando o recorte fixo por um escrito em `--px`
     (`--recorte-botao-mapa`, definido em `.mapa-palco`). Dois ids para vencer
     o `clip-path` da regra genérica de botão do menu, e as três camadas
     (botão + os dois pseudo-elementos da moldura) para vencer as três dela. */
  #menu #mapaFicha .mapa-ficha-acao,
  #menu #mapaFicha .mapa-ficha-acao::before,
  #menu #mapaFicha .mapa-ficha-acao::after {
    clip-path: var(--recorte-botao-mapa);
  }
  .mapa-salas {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: calc(var(--px) * 2);
  }
  .mapa-sala {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: calc(var(--px) * 3);
    padding: calc(var(--px) * 2) calc(var(--px) * 3);
    background: var(--cor-superficie);
    font-size: max(calc(var(--px) * 5), var(--mapa-fonte-min));
  }
  .mapa-sala-texto {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .mapa-sala-texto b {
    font-weight: normal;
    color: #ffcf4d;
  }
  #menu #mapaFicha .mapa-sala-entrar {
    flex: none;
    padding: var(--px) calc(var(--px) * 3);
  }

  /* O canto: "Entrar com código" e "Tutorial", e o aviso de fora do ar acima
     deles. Fica sempre à mão, fora da ficha. */
  .mapa-canto {
    position: absolute;
    z-index: 6;
    right: calc(var(--px) * 4);
    bottom: calc(var(--px) * 4);
    display: grid;
    justify-items: end;
    gap: calc(var(--px) * 2);
    max-width: calc(100% - var(--px) * 8);
  }
  .mapa-canto-botoes {
    display: flex;
    gap: calc(var(--px) * 3);
  }
  .mapa-aviso {
    margin: 0;
    max-width: calc(var(--px) * 132);
    padding: var(--px) calc(var(--px) * 3);
    font-size: max(calc(var(--px) * 4.5), var(--mapa-fonte-min));
    color: #ffb3b3;
    background: rgba(0, 0, 0, 0.8);
    text-align: right;
  }
  /* Cinza, e não vermelho: levam a partidas sem fase (a sala de outra pessoa,
     o tutorial), não ao que a ficha oferece. */
  #menu #mapaCanto .mapa-canto-btn {
    width: auto;
    min-width: 0;
    padding: calc(var(--px) * 2) calc(var(--px) * 4);
    font-size: max(calc(var(--px) * 5), var(--mapa-fonte-min));
    line-height: 1.25;
    background: var(--cor-botao-secundario);
    color: var(--cor-texto);
  }
  #menu #mapaCanto .mapa-canto-btn:not(:disabled):hover {
    background: var(--cor-botao-secundario-hover);
    transform: none;
  }
  #menu #mapaCanto .mapa-canto-btn:disabled {
    background: var(--cor-borda-media);
    color: var(--cor-texto-fraco);
    cursor: default;
  }
  /* Mesmo defeito do botão vermelho da ficha, e mesma correção: o degrau fixo
     de `--recorte-botao-pixel` come a caixa dos botões do canto quando
     `--px` está baixo. */
  #menu #mapaCanto .mapa-canto-btn,
  #menu #mapaCanto .mapa-canto-btn::before,
  #menu #mapaCanto .mapa-canto-btn::after {
    clip-path: var(--recorte-botao-mapa);
  }
  /* Toque: o alvo mínimo de 44×44px (o tamanho do nó é garantido pelo JS). A
     ficha termina acima do canto, que cresceu. */
  body.touch-mode #menu #mapaCanto .mapa-canto-btn,
  body.touch-mode #menu #mapaFicha .mapa-ficha-acao {
    min-height: 44px;
  }
  @media (prefers-reduced-motion: reduce) {
    #menu #mapaNos .mapa-no.atual,
    #menu #mapaNos .mapa-no.destrancando,
    #mapaNos .mapa-no.medalha-acendendo .mapa-no-pips i.tem,
    .mapa-fogueira i,
    .mapa-ficha { animation: none; }
  }
  /* ===== Cena da tela inicial (#cenaInicial) =====
     A ilustração de "Demon Arena" em camadas, atrás de #menu e só na visão
     "Início" (o `:has` abaixo) — as outras visões são painéis de pedra e
     continuam sobre o tijolo do body. É a exceção deliberada à regra de pixel
     art (ver docs/pixel-art.md): arte pintada, com deslocamento contínuo.

     O palco cobre a tela como `background-size: cover` cobriria, mais uma
     sobra de `--cena-sobra` em cada borda: é o que a camada deslocada pelo
     paralaxe (public/js/cenaDaTelaInicial.js) pode andar sem mostrar a borda da
     arte — o mago verde e o chão terminam exatamente na borda de baixo. */
  #cenaInicial {
    --cena-sobra: 64px;
    --cena-proporcao: 2679 / 1507;
    display: none;
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    background: var(--cor-cena-inicial-fundo);
  }
  body:not(.game-active):has(#menuViewHome.menu-view-ativa) #cenaInicial {
    display: block;
  }
  /* Véu da esquerda: a coluna de logo e botões fica sobre o céu vermelho, e o
     botão "Jogar" é vermelho também. Escurecer só a faixa dela devolve o
     contraste sem apagar a ilustração inteira. */
  #cenaInicial::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, var(--cor-cena-inicial-veu) 0%, var(--cor-cena-inicial-veu-meio) 30%, transparent 50%);
  }
  #cenaInicialPalco {
    position: absolute;
    left: 50%;
    top: 50%;
    width: max(calc(var(--largura-tela) + 2 * var(--cena-sobra)), calc((var(--altura-tela) + 2 * var(--cena-sobra)) * 2679 / 1507));
    aspect-ratio: var(--cena-proporcao);
    transform: translate(-50%, -50%);
  }
  /* ===== A travessia entre a capa e o acampamento =====
     Ver public/js/transicaoDaCapa.js. O avanço e o recuo da câmera **não estão
     aqui**: cada camada da ilustração cresce e se afasta no próprio ritmo, e
     quem escreve o `transform` de cada uma é o JS (`definirMergulho` em
     cenaDaTelaInicial.js), que já é o dono dessa propriedade por causa do
     paralaxe. Em CSS ficam só as peças de interface da travessia — a cortina,
     a coluna do logo e o acampamento —, todas em `steps()`. */
  /* A coluna do logo e dos botões é interface, e não a ilustração: ela esmaece
     em degraus, como o resto (ver public/css/CLAUDE.md). Sai antes de a
     cortina fechar — o preto pega a ilustração sozinha — e entra junto com a
     cortina que abre, quando a câmera já está recuando. */
  #menuViewHome.capa-saindo {
    animation: capa-sai 0.36s steps(4, end) forwards;
  }
  @keyframes capa-sai {
    from { opacity: 1; }
    to { opacity: 0; }
  }
  #menuViewHome.capa-surgindo {
    animation: capa-entra 0.42s steps(5, end) backwards;
  }
  @keyframes capa-entra {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  /* A abertura da capa, uma vez por carga (`entrarNaCapa` em
     transicaoDaCapa.js põe `.capa-abertura` no body e a tira em 3,2s). Lenta
     de propósito: é a primeira cena do jogo. A ilustração acende do escuro
     enquanto a câmera recua — contínua, porque é a arte pintada (a exceção de
     docs/pixel-art.md) — e a interface chega peça por peça, cada uma em
     degraus: a primeira linha do logo, a segunda, o "Jogar" e por fim as duas
     ações secundárias. Os atrasos são a coreografia; a soma do último atraso
     com a duração dele tem de caber em `ABERTURA_CAPA_MS`. */
  body.capa-abertura #cenaInicialPalco {
    animation: capa-cena-acende 2.2s ease-out both;
  }
  @keyframes capa-cena-acende {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  body.capa-abertura #menuTitle .title-line {
    animation: capa-logo-crava 0.6s steps(6, end) both;
  }
  body.capa-abertura #menuTitle .title-line:nth-child(1) { animation-delay: 0.9s; }
  body.capa-abertura #menuTitle .title-line:nth-child(2) { animation-delay: 1.4s; }
  @keyframes capa-logo-crava {
    from { opacity: 0; transform: scale(1.5); }
    60% { opacity: 1; transform: scale(0.96); }
    to { opacity: 1; transform: scale(1); }
  }
  body.capa-abertura #menuModos {
    animation: capa-botao-sobe 0.5s steps(5, end) 2.1s both;
  }
  body.capa-abertura #menuTitleExtras {
    animation: capa-botao-sobe 0.5s steps(5, end) 2.6s both;
  }
  @keyframes capa-botao-sobe {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: translateY(0); }
  }
  /* O acampamento surgindo atrás da cortina que abre: mesma duração da
     abertura dela, para a cena terminar de aparecer junto com o último degrau
     do preto. */
  #menuViewAcampamento.acampamento-surgindo {
    animation: acampamento-surge 0.42s steps(5, end) forwards;
  }
  @keyframes acampamento-surge {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  /* A cortina (#cortinaDaTela): o retângulo preto da travessia. Acima da barra
     superior (15) e de tudo o que a tela inicial mostra; abaixo do cursor de
     fogo, que é decoração do ponteiro. Sem classe nenhuma ela é invisível e
     não recebe clique — é o estado em que passa a vida inteira. */
  #cortinaDaTela {
    position: fixed;
    inset: 0;
    z-index: 61;
    background: var(--cor-barra-fundo);
    opacity: 0;
    pointer-events: none;
  }
  /* Fechando, ela **bloqueia o clique**: sem isso um segundo toque no meio do
     escuro chegaria à estação que já está montada atrás. */
  #cortinaDaTela.cortina-fechando {
    pointer-events: auto;
    animation: cortina-fecha 0.58s steps(6, end) forwards;
  }
  #cortinaDaTela.cortina-abrindo {
    animation: cortina-abre 0.42s steps(6, end) forwards;
  }
  @keyframes cortina-fecha {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  @keyframes cortina-abre {
    from { opacity: 1; }
    to { opacity: 0; }
  }
  /* Com movimento reduzido não há travessia (transicaoDaCapa.js troca de visão
     no mesmo quadro e nem chega a pôr as classes); as regras abaixo são a
     rede de segurança de uma classe que tenha ficado pendurada. */
  @media (prefers-reduced-motion: reduce) {
    #menuViewHome.capa-saindo,
    #menuViewHome.capa-surgindo,
    #menuViewAcampamento.acampamento-surgindo,
    body.capa-abertura #cenaInicialPalco,
    body.capa-abertura #menuTitle .title-line,
    body.capa-abertura #menuModos,
    body.capa-abertura #menuTitleExtras,
    #cortinaDaTela.cortina-fechando,
    #cortinaDaTela.cortina-abrindo { animation: none; }
  }
  /* Posição e largura de cada camada em porcentagem do palco, a partir de onde
     ela está na arte de 2679×1507 (a altura segue a proporção da imagem). */
  .cena-camada {
    position: absolute;
    display: block;
    height: auto;
    will-change: transform;
    user-select: none;
    -webkit-user-drag: none;
  }
  #cenaFundo { left: 0; top: 0; width: 100%; }
  #cenaDemonio1 { left: 49.496%; top: 23.159%; width: 5.300%; }
  #cenaDemonio2 { left: 52.744%; top: 31.984%; width: 3.882%; }
  #cenaDemonio3 { left: 57.447%; top: 40.544%; width: 5.562%; }
  #cenaDemonio4 { left: 64.166%; top: 39.217%; width: 5.375%; }
  #cenaDemonio5 { left: 76.596%; top: 31.121%; width: 3.770%; }
  #cenaDemonioGrande { left: 60.918%; top: 4.048%; width: 22.658%; }
  #cenaMagosDupla { left: 3.621%; top: 61.314%; width: 13.960%; }
  #cenaMagoRoxo { left: 17.021%; top: 45.455%; width: 13.699%; }
  #cenaMagoVerde { left: 31.131%; top: 21.898%; width: 66.704%; }
  /* As bolhas verdes do cajado do mago verde, recortadas por cor da própria
     imagem dele e pousadas exatamente em cima dela, na mesma profundidade. Quem
     brilha é esta cópia: ela clareia e ganha um halo verde, e embaixo o
     desenho original continua igual — apagada, ela some dentro dele. */
  #cenaCajadoBolhas {
    left: 87.271%;
    top: 21.898%;
    width: 10.004%;
    animation: cena-cajado-brilha 3.2s ease-in-out infinite;
  }
  /* Os olhos do mago verde, pelo mesmo truque das bolhas: a cópia dos dois
     riscos amarelos por cima do rosto. O halo é maior que o da peça (ela tem
     poucos pixels) e o ritmo é outro, para rosto e cajado não pulsarem juntos. */
  #cenaOlhosMagoVerde {
    left: 40.761%;
    top: 31.453%;
    width: 2.538%;
    animation: cena-olhos-brilham 2.4s ease-in-out infinite;
  }
  @keyframes cena-olhos-brilham {
    0%, 100% { filter: brightness(1) drop-shadow(0 0 0 transparent); }
    50% { filter: brightness(1.4) drop-shadow(0 0 6px var(--cor-cena-brilho-olhos)) drop-shadow(0 0 12px var(--cor-cena-brilho-olhos)); }
  }
  @keyframes cena-cajado-brilha {
    0%, 100% { filter: brightness(1) drop-shadow(0 0 0 transparent); }
    50% { filter: brightness(1.3) drop-shadow(0 0 14px var(--cor-cena-brilho-cajado)); }
  }
  #cenaFaiscas1 { left: 9.668%; top: 32.382%; width: 90.332%; }
  #cenaFaiscas2 { left: 17.618%; top: 38.308%; width: 79.769%; }
  #cenaFaiscas3 { left: 3.210%; top: 38.753%; width: 96.080%; }
  /* As faíscas piscam devagar, em três grupos defasados de um terço do ciclo:
     o grupo inteiro apagando junto leria como a luz da cena oscilando, e três
     fases espalhadas pela tela leem como brasas. Nunca somem de todo — o piso
     de opacidade mantém o pontinho no lugar enquanto ele esfria. */
  .cena-faiscas {
    animation: cena-faisca-pisca 4.5s ease-in-out infinite;
  }
  #cenaFaiscas2 { animation-delay: -1.5s; }
  #cenaFaiscas3 { animation-delay: -3s; }
  @keyframes cena-faisca-pisca {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.15; }
  }
  @media (prefers-reduced-motion: reduce) {
    .cena-faiscas,
    #cenaCajadoBolhas,
    #cenaOlhosMagoVerde { animation: none; }
  }
  .menu-view > * {
    max-width: 100%;
  }
  /* ===== Container padrão das telas do menu (.menu-painel) =====
     A moldura de pedra de todas as visões do menu que não são a tela inicial:
     Árvore de Talentos (#talentTreePanel), Monstruário (#monstruarioPainel),
     Relicário (#relicarioListPanel) e Ranking (#hordeEndlessPanel). Antes cada
     uma trazia a própria cópia do material (fundo, borda, sombra, padding) com
     uma largura e um teto de
     altura diferentes — e como as abas trocam a visão sem nada mais mudar na
     tela, a moldura *pulava* de tamanho a cada clique. Aqui ela é uma só.

     Um retângulo por visão, e nunca um por coluna: a visão que tem duas
     colunas (Monstruário) as coloca **dentro** deste
     container, separadas por uma divisória — dois retângulos lado a lado leem
     como duas telas, e deixam o título da visão solto sobre a textura.

     `height: 100%` e não uma altura fixa: a visão já mede a tela menos a barra
     superior e o padding dela, então isto é o que faz as cinco telas terem a
     mesma altura sem repetir a conta. O `max-height` é só o teto.

     `overflow: hidden` porque quem rola é a lista de dentro (a corrente de
     `flex: 1` + `min-height: 0` até ela), nunca o painel: o título é cabeçalho
     e fica sempre à vista. */
  .menu-painel {
    /* Medidas do prego dos cantos, aqui e não no `:root` porque só esta regra
       e a camada de cima usam. A margem é contada a partir do **miolo**
       (o ::after nasce recuado pela espessura da borda), e 10px é o primeiro
       valor que deixa o prego inteiro dentro da silhueta: a escadinha dos
       botões tem degraus de 4px, e mais perto que isso a quina do prego passa
       para fora do recorte e a parede de pedra aparece por baixo dela. */
    --prego-lado: 8px;
    --prego-margem: 10px;
    /* As três medidas da laje, aqui e não fixas nas camadas: as mesmas duas
       regras de `::before`/`::after` desenham também a pedra das peças do HUD
       da partida (`.hud-pedra`, no fim do arquivo), que é a mesma laje numa
       escala menor — escadinha de um degrau, bisel de 2px e sombra de 4px. Uma
       segunda cópia das 16 camadas de prego seria a mesma coisa escrita duas
       vezes, e o prego é justamente o que faz as duas lerem como o mesmo
       material. */
    --moldura-recorte: var(--recorte-botao-pixel);
    --moldura-bisel: var(--sombra-pedra-interna);
    --moldura-sombra: 8px 8px 0 rgba(0, 0, 0, 0.4);
    position: relative;
    /* Contexto de empilhamento próprio: sem ele as duas camadas em `z-index:
       -1` (a pedra e os pregos) cairiam atrás do fundo da página em vez de
       ficarem entre ele e o conteúdo do painel. Mesmo truque da moldura de
       fliperama dos botões. */
    isolation: isolate;
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: var(--largura-painel-menu);
    max-width: 100%;
    height: 100%;
    max-height: var(--altura-painel-menu);
    box-sizing: border-box;
    padding: 24px;
    /* A pedra, o contorno e a sombra moram nas duas camadas (::before/::after),
       não aqui: são elas que carregam o `clip-path`, e recortar o painel
       inteiro recortaria junto o conteúdo e as listas que rolam por dentro. */
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    overflow: hidden;
    animation: entrada-fade 0.5s ease 0.12s both;
  }
  /* Camada de baixo: a silhueta inteira na cor do contorno. Junto com o
     ::after, que repinta o miolo recuado pela espessura da borda, ela é o que
     sobra como contorno de 2px — `clip-path` não sabe recortar buraco, então a
     moldura são duas camadas e não uma `border` (recortada pelo clip-path, a
     borda sumiria justamente na diagonal dos cantos). É a mesma construção da
     moldura dos botões, com duas diferenças: as camadas ficam aqui e não no
     próprio painel (recortar o painel recortaria junto o conteúdo e as listas
     que rolam por dentro), e a cor do contorno é fixa em vez de sair do fundo
     do elemento — a pedra do painel é quase preta e não tem tom mais escuro
     que se veja sobre a parede.

     A sombra deslocada é `filter: drop-shadow` e não `box-shadow`: o
     `clip-path` apaga tudo que é pintado fora da silhueta, então uma sombra de
     caixa sumiria inteira — a de `filter` acompanha os cantos recortados. */
  .menu-painel::before,
  .hud-pedra::before,
  #hordeLobby::before,
  .talent-arvore::before,
  #talentFicha::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -2;
    /* A cor do contorno tem um padrão, e não um valor fixo: a caixa da árvore
       de talentos usa a mesma laje com o contorno **escuro** (ver
       .talent-arvore). Quem não declara nada continua com o contorno claro do
       painel, que é o de todo mundo. */
    background: var(--moldura-contorno, var(--cor-painel-pedra-contorno));
    clip-path: var(--moldura-recorte);
    filter: drop-shadow(var(--moldura-sombra));
  }
  /* Camada de cima: o miolo de pedra, a mesma escadinha nascendo recuada pela
     espessura da borda — e, por cima dela, os quatro pregos de prata.

     Os pregos vêm junto com o miolo em vez de numa terceira camada porque só
     há dois pseudo-elementos: `background-color` pinta a pedra e as camadas de
     `background-image` pintam os pregos por cima dela.

     Um prego por canto — a laje é uma placa pregada na parede de pedra, e o
     prego é o que diz isso. Cada um são quatro camadas (a técnica das modais e
     dos campos de texto, ver "Pixel art no CSS"):

       corpo deitado (8x6) + corpo em pé (6x8): a união dos dois é um octógono
         de 8px (o quadrado sem os quatro cantos), que é como se desenha uma
         bolinha nesse tamanho sem `border-radius`;
       luz (2x2) em cima e à esquerda e sombra (2x2) embaixo e à direita: o
         degrau de cor que faz a cabeça do prego ler como redonda. A luz vem do
         mesmo canto que o bisel da pedra (`--sombra-pedra-interna`) — duas
         fontes de luz no mesmo painel leem como erro.

     São 16 camadas porque são quatro pregos. Dentro de cada quarteto a ordem é
     luz, sombra, corpo em pé, corpo deitado: a **primeira** camada de
     `background-image` é a de cima, então os dois pixels de volume têm de vir
     antes do corpo.

     A camada fica atrás do conteúdo (`z-index: -1`), e não por cima: numa tela
     apertada, onde a lista chega perto da borda, o prego passa por trás da
     linha em vez de riscá-la. */
  .menu-painel::after,
  .hud-pedra::after,
  #hordeLobby::after,
  .talent-arvore::after,
  #talentFicha::after {
    content: "";
    position: absolute;
    inset: var(--espessura-borda-botao);
    z-index: -1;
    /* Mesmo padrão do contorno acima: a pedra do miolo é a do painel, a menos
       que a peça peça outra — a caixa da árvore é um degrau **acima** da laje
       que a contém, e é isso que a separa do fundo. */
    background-color: var(--moldura-pedra, var(--cor-painel-pedra-veu));
    box-shadow: var(--moldura-bisel);
    clip-path: var(--moldura-recorte);
    background-image:
      /* superior esquerdo */
      linear-gradient(var(--cor-prego-luz), var(--cor-prego-luz)),
      linear-gradient(var(--cor-prego-sombra), var(--cor-prego-sombra)),
      linear-gradient(var(--cor-prego), var(--cor-prego)),
      linear-gradient(var(--cor-prego), var(--cor-prego)),
      /* superior direito */
      linear-gradient(var(--cor-prego-luz), var(--cor-prego-luz)),
      linear-gradient(var(--cor-prego-sombra), var(--cor-prego-sombra)),
      linear-gradient(var(--cor-prego), var(--cor-prego)),
      linear-gradient(var(--cor-prego), var(--cor-prego)),
      /* inferior esquerdo */
      linear-gradient(var(--cor-prego-luz), var(--cor-prego-luz)),
      linear-gradient(var(--cor-prego-sombra), var(--cor-prego-sombra)),
      linear-gradient(var(--cor-prego), var(--cor-prego)),
      linear-gradient(var(--cor-prego), var(--cor-prego)),
      /* inferior direito */
      linear-gradient(var(--cor-prego-luz), var(--cor-prego-luz)),
      linear-gradient(var(--cor-prego-sombra), var(--cor-prego-sombra)),
      linear-gradient(var(--cor-prego), var(--cor-prego)),
      linear-gradient(var(--cor-prego), var(--cor-prego));
    background-size:
      /* superior esquerdo */
      2px 2px,
      2px 2px,
      6px 8px,
      8px 6px,
      /* superior direito */
      2px 2px,
      2px 2px,
      6px 8px,
      8px 6px,
      /* inferior esquerdo */
      2px 2px,
      2px 2px,
      6px 8px,
      8px 6px,
      /* inferior direito */
      2px 2px,
      2px 2px,
      6px 8px,
      8px 6px;
    background-position:
      /* superior esquerdo */
      left calc(var(--prego-margem) + 2px) top calc(var(--prego-margem) + 2px),
      left calc(var(--prego-margem) + 4px) top calc(var(--prego-margem) + 4px),
      left calc(var(--prego-margem) + 1px) top var(--prego-margem),
      left var(--prego-margem) top calc(var(--prego-margem) + 1px),
      /* superior direito */
      right calc(var(--prego-margem) + var(--prego-lado) - 4px) top calc(var(--prego-margem) + 2px),
      right calc(var(--prego-margem) + var(--prego-lado) - 6px) top calc(var(--prego-margem) + 4px),
      right calc(var(--prego-margem) + var(--prego-lado) - 7px) top var(--prego-margem),
      right var(--prego-margem) top calc(var(--prego-margem) + 1px),
      /* inferior esquerdo */
      left calc(var(--prego-margem) + 2px) bottom calc(var(--prego-margem) + var(--prego-lado) - 4px),
      left calc(var(--prego-margem) + 4px) bottom calc(var(--prego-margem) + var(--prego-lado) - 6px),
      left calc(var(--prego-margem) + 1px) bottom var(--prego-margem),
      left var(--prego-margem) bottom calc(var(--prego-margem) + var(--prego-lado) - 7px),
      /* inferior direito */
      right calc(var(--prego-margem) + var(--prego-lado) - 4px) bottom calc(var(--prego-margem) + var(--prego-lado) - 4px),
      right calc(var(--prego-margem) + var(--prego-lado) - 6px) bottom calc(var(--prego-margem) + var(--prego-lado) - 6px),
      right calc(var(--prego-margem) + var(--prego-lado) - 7px) bottom var(--prego-margem),
      right var(--prego-margem) bottom calc(var(--prego-margem) + var(--prego-lado) - 7px);
    background-repeat: no-repeat;
  }
  /* As duas colunas de dentro de um .menu-painel (a fileira abaixo do título).
     Usada pelo Monstruário (#monstruarioCols). `min-height: 0` para a corrente de altura chegar às
     colunas: sem ele o `height: 100%` delas mede a fileira já esticada pelo
     conteúdo, e a lista de dentro deixa de rolar por dentro. */
  .menu-painel-cols {
    display: flex;
    align-items: stretch;
    justify-content: center;
    max-width: 100%;
    flex: 1;
    min-height: 0;
  }
  /* O título é da visão inteira, não das colunas: fica acima delas, com a
     largura das duas, e não encolhe quando a fileira pede espaço. */
  .menu-painel > .horde-rooms-view-title,
  .menu-painel > h2 {
    flex: none;
  }
  /* ===== Barra superior da tela inicial (#menuTopbar) =====
     A faixa fixa no topo com o botão de voltar do menu (e as capinhas de
     LB/RB ao lado dele), a configuração e o botão de conta. Ela **não passa por cima do conteúdo**: #menu reserva a altura dela
     em `padding-top` (ver --altura-topbar). Só existe fora de partida e fora
     da capa — a visibilidade depende só do modo (menu × partida) e da visão
     aberta, nunca de estado assíncrono.

     Foi uma barra lateral de 184px na borda esquerda até aqui. O que a tela
     inicial tem de sobra é largura (o Relicário em três colunas e o
     Monstruário em dois painéis eram os que mais sofriam com a faixa lateral);
     o que falta é altura — daí a faixa de 56px em cima em vez da coluna. */
  #menuTopbar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 15;
    width: 100%;
    box-sizing: border-box;
    background: var(--cor-barra-fundo);
    border-bottom: 1px solid var(--cor-borda);
    /* Grade de duas colunas: a 1 leva o botão de voltar, a 2 leva
       configuração+conta grudado na borda direita.

       `minmax(0, 1fr)` na primeira coluna (e não `1fr`) porque o mínimo
       automático de uma faixa de grade é o conteúdo dela: sem o zero, uma
       linha maior que a tela empurraria o canto direito para fora. */
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
  }
  body.game-active #menuTopbar {
    display: none;
  }
  /* **A capa tem barra, e ela é transparente.** Por um tempo a barra sumia
     aqui: a capa era "uma tela só" — logo, "Jogar" e as duas ações
     secundárias sobre a ilustração —, e configurações e conta ficavam a uma
     tela de distância, no acampamento. Isso deixava três botões escondidos
     justamente de quem acabou de abrir o jogo, e o de **idioma** (que entrou
     depois, ver public/js/atalhoDeIdioma.js) é o caso que fechou a decisão:
     quem abriu o jogo num idioma que não lê é quem menos tem como achar o
     caminho até o acampamento. Hoje o canto direito da barra aparece na capa
     como aparece no acampamento.

     Transparente, e não a faixa escura de sempre, pelo mesmo motivo do
     acampamento e do mapa: a capa é uma ilustração de borda a borda, e uma
     faixa opaca cortaria o céu dela. Sem fundo, sem borda e sem pegar o
     clique da faixa que ela cobre — só os botões dela clicam.

     A capa continua devolvendo a faixa que #menu reserva para a barra em
     `padding-top` (do mesmo jeito que o acampamento faz com `margin-top`
     negativo): a barra passa **por cima** da ilustração, e o enquadramento da
     coluna do logo não muda. Ela não cobre nada, e isso é medido: o voltar
     some na capa (não há para onde voltar) e o título fica vazio, então o
     lado esquerdo da barra é ar; do lado direito, fora da faixa estreita a
     coluna do logo encosta à esquerda, e nela (`tela-estreita`, ≤700px) a
     coluna é centrada e começa abaixo da barra. */
  body:has(#menuViewHome.menu-view-ativa) #menuTopbar {
    background: transparent;
    border-bottom-color: transparent;
    pointer-events: none;
  }
  body:has(#menuViewHome.menu-view-ativa) #menuTopbarLinha > *,
  body:has(#menuViewHome.menu-view-ativa) #menuTopbarDir > * {
    pointer-events: auto;
  }
  body:has(#menuViewHome.menu-view-ativa) #menu {
    padding-top: 0;
  }
  /* Na faixa estreita a capa é uma coluna **centrada**, e o logo começa no
     topo da visão: ali ele passaria por baixo dos botões do canto direito.
     Nessa faixa, e só nela, a capa reserva a altura da barra. */
  body.tela-estreita:has(#menuViewHome.menu-view-ativa) #menu {
    padding-top: var(--altura-topbar);
  }
  /* A primeira linha da barra é a de altura fixa (--altura-topbar): o voltar à
     esquerda, configuração e conta empurrados para a direita. Os
     8px de `padding` vertical mais os 40px do botão fecham os 56px — nenhuma
     conta de meio pixel, que reamostraria a pixel art dos ícones. */
  #menuTopbarLinha {
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
    height: var(--altura-topbar);
    box-sizing: border-box;
    padding: 8px 12px;
    display: flex;
    align-items: center;
    gap: 12px;
  }
  /* O botão contextual de voltar (#btnMenuViewHome, ver `voltarNoMenu` em
     menuViews.js): some na capa, é "Tela inicial" (a casa) no acampamento e
     "Acampamento" (a tenda) numa tela. Ele tomou o lugar da marca do jogo e
     da fileira de abas, e é do mesmo material dos botões de ícone da barra (a
     regra compartilhada de #btnConfiguracoes): ícone + rótulo, crescendo com o
     texto, e só o ícone quando falta largura (ver "Barra sem largura"). */
  #menuTopbarLinha .menu-voltar {
    --cor-fundo-campo: var(--cor-barra-botao);
    width: auto;
    justify-content: flex-start;
    /* O vão entre ícone e rótulo é `padding` **do rótulo**, e não `gap` do
       botão: `gap` vale mesmo com o rótulo em largura zero, e sem rótulo o
       botão ficaria 8px mais largo que o quadrado de 40px que ele deve ser. */
    gap: 0;
    padding: 0 9px;
    box-sizing: border-box;
    flex-shrink: 0;
  }
  #menuTopbarLinha .menu-voltar:hover {
    --cor-fundo-campo: var(--cor-barra-botao-hover);
  }
  /* `display: flex` da regra compartilhada ganha do `display: none` que o
     navegador dá ao atributo `hidden`: sem esta linha o botão ficaria na capa,
     que é justamente onde não há para onde voltar. */
  #menuTopbarLinha .menu-voltar[hidden] {
    display: none;
  }
  /* Um ícone por destino, escolhido pelo `data-destino` que o JS escreve. */
  .menu-voltar-icone {
    flex-shrink: 0;
    display: none;
  }
  .menu-voltar[data-destino="capa"] .menu-voltar-icone-capa,
  .menu-voltar[data-destino="acampamento"] .menu-voltar-icone-acampamento {
    display: block;
  }
  /* O nome da tela aberta, ao lado do voltar (ver `atualizarTituloDaTela` em
     menuViews.js). Na fonte dos títulos, porque é o título que os painéis não
     têm; encolhe com reticências antes de empurrar a conta para fora. */
  .menu-titulo-da-tela {
    min-width: 0;
    overflow: hidden;
    font-family: 'Cinzel', serif;
    font-size: 18px;
    font-weight: 700;
    color: var(--cor-texto);
    white-space: nowrap;
    text-overflow: ellipsis;
    text-shadow: 2px 2px 0 var(--cor-pedra-borda);
  }
  .menu-titulo-da-tela[hidden] {
    display: none;
  }
  body.tela-estreita .menu-titulo-da-tela,
  body.tela-baixa-deitada .menu-titulo-da-tela {
    font-size: 15px;
  }
  /* Canto direito: o controle de áudio e a conta. Quem o gruda na borda
     é a coluna `auto` da grade — nada aqui tem largura fixa, então não há
     número para manter em sincronia com o resto da barra. Ele nunca encolhe
     nem rola: numa barra apertada quem cede é o rótulo do voltar, não o botão
     de entrar na conta. */
  #menuTopbarDir {
    grid-column: 2;
    grid-row: 1;
    display: flex;
    align-items: center;
    gap: 8px;
    padding-right: 12px;
    flex-shrink: 0;
  }
  /* A capinha de atalho do controle colada a cada botão do canto direito da
     barra: Start abre as Configurações e Select abre o Perfil (ver
     public/js/atalhosNoControle.js). Ela é **peça miúda**, pela mesma regra de
     contexto do rodapé da dica do tiro automático (ver `.dica-tiro-auto-sub
     .glifo`, mais abaixo): na caixa cheia de 40px ela teria a altura exata do
     botão ao lado dela e leria como mais um botão — e ela não é clicável, é
     rótulo. */
  .menu-glifo-atalho {
    display: flex;
    align-items: center;
    flex-shrink: 0;
    /* Mais perto do botão dela que do botão vizinho: o `gap` da barra é 8px. */
    margin-right: -4px;
  }
  /* `display: flex` ganha do `display: none` que o navegador dá ao atributo
     `hidden`, então o interruptor do JS precisa desta linha para funcionar —
     sem ela a dica ficaria na barra com o miolo vazio para quem nunca conectou
     um controle. */
  .menu-glifo-atalho[hidden] {
    display: none;
  }
  .menu-glifo-atalho .glifo {
    min-width: 20px;
    height: 20px;
    padding: 0 5px;
    font-size: 12px;
  }
  .menu-glifo-atalho .glifo svg {
    width: 12px;
    height: 12px;
  }
  /* Numa barra sem largura a dica sai primeiro: disputando espaço com a conta,
     ela seria a primeira coisa a cortar. Em touch some pelo motivo de sempre:
     não há controle na mão de quem joga com o dedo, e quando há, a tela é
     larga. */
  body.tela-estreita .menu-glifo-atalho,
  body.touch-mode .menu-glifo-atalho {
    display: none;
  }
  /* Selo de pendência num botão: pontos de talento por gastar no "Talentos" da
     sala de espera (ver public/js/talentPointsAviso.js). Ele marcava também as
     abas "Talentos", "Itens" e "Metas" da barra superior, que saíram com o
     acampamento — lá quem diz isso são os objetos da cena. O desenho é um só
     de propósito (ver public/js/seloDeAba.js).
     O `data-selo` no botão é o interruptor — ele só existe com pendência,
     então "nada a fazer" não tem regra a desligar.

     O número vem num `<span class="aba-selo">` que o JS põe dentro do
     botão, e **não** num `::after` com `attr()`: a moldura em pixel art dos
     botões de ação é feita justamente com `::before`/`::after` (ver o bloco no
     fim do arquivo), e num botão que tem as duas coisas — como
     "Árvore de Talentos" da visão Defesa da base — o selo e a moldura
     brigavam pelo mesmo pseudo-elemento. A animação do selo ganha de qualquer
     declaração normal, então a camada interna da moldura era pintada de ouro
     de ponta a ponta: uma tarja piscando atrás do rótulo, no lugar do número.

     O selo é absoluto, então continua fora do fluxo e o rótulo "Talentos" não
     anda um pixel quando ele aparece — inclusive no modo só-ícone do touch, em
     que o botão vira um quadrado e o selo continua no mesmo canto. Quadrado de
     canto reto e sombra sólida deslocada, como o resto da interface (ver
     public/css/CLAUDE.md), mesmo dentro de um botão que é exceção de canto
     arredondado. */
  /* Desenho do selo, compartilhado por todos os botões que o usam; cada um
     ajusta só o canto (e o tamanho, na sala de espera) na regra dele. */
  .aba-selo {
    position: absolute;
    top: 2px;
    right: 2px;
    min-width: 16px;
    height: 16px;
    line-height: 16px;
    box-sizing: border-box;
    padding: 0 3px;
    font-size: 11px;
    text-align: center;
    color: var(--cor-texto-sobre-tier);
    background: var(--cor-ouro);
    /* Pisca entre duas cores, nunca entre a cor e o nada — mesma regra do selo
       de nível da escolha de item: sumir metade do tempo faria o lembrete
       parecer um defeito de desenho. */
    animation: aba-selo-pisca 0.9s steps(1, end) infinite alternate;
  }
  @keyframes aba-selo-pisca {
    0% { background: var(--cor-ouro); }
    100% { background: var(--cor-branco); }
  }
  /* O rótulo ao lado do ícone do voltar: ícone sozinho não diz para onde o
     botão leva. O teto de largura é um corte, não uma largura: o rótulo
     continua do tamanho do texto. */
  .menu-voltar-rotulo {
    display: inline-block;
    max-width: 112px;
    overflow: hidden;
    /* O vão até o ícone — ver `gap: 0` em .menu-voltar. */
    padding-left: 8px;
    font-size: 13px;
    white-space: nowrap;
  }
  /* ===== Barra em tela pequena =====
     A barra tem dois recursos escassos, e eles são escassos em telas
     diferentes — misturar os dois numa regra só foi o erro do desenho
     anterior, em que `body.touch-mode` escondia os rótulos "porque falta
     altura". Rótulo não custa altura nenhuma: custa **largura**. O resultado
     era um tablet de 1180px de largura com oito ícones anônimos, e um celular
     deitado com duas faixas empilhadas comendo 82 dos 390px de altura dele.

     Aqui as duas escassez são tratadas separadamente:

     - **falta altura** (`tela-baixa-deitada`, ver orientation.js) → o rótulo
       da conta sai;
     - **falta largura** (`tela-estreita`, ≤700px) → saem o rótulo do voltar e
       o da conta, e os dois viram quadrados de 40px.

     `touch-mode` continua valendo só para o que é de fato do dedo: faixas um
     pouco mais baixas e vãos menores. */
  body.touch-mode {
    --altura-topbar: 48px;
  }
  body.touch-mode #menuTopbarLinha {
    padding: 4px 8px;
    gap: 8px;
  }
  body.touch-mode #menuTopbarDir {
    padding-right: 8px;
  }
  /* ----- Barra de uma tela baixa (falta altura) -----
     Num celular deitado — que é como o jogo roda em qualquer telefone, seja
     porque a orientação travou, seja porque o `<body>` foi girado por CSS (ver
     orientation.js) — a tela tem ~390px de altura e mais de 800 de largura, e
     cada pixel de faixa fixa sai do conteúdo. A barra já é uma linha só desde
     que Talentos, Monstruário e Relicário viraram abas irmãs (antes eles eram
     uma segunda faixa, e as duas empilhadas cobravam 82px, 21% da tela).

     O que sobra a fazer aqui é a largura da conta, que é o rótulo mais
     comprido da linha. */
  body.tela-baixa-deitada #accountFabLabel {
    max-width: 0;
    padding: 0;
    overflow: hidden;
  }
  /* ----- Barra sem largura -----
     Abaixo de 700px (`tela-estreita`) o voltar fica **só com o ícone**, e a
     conta também. O nome não se perde: o botão leva `aria-label` e `title`.

     Zero e não `display: none` porque o elemento continua no fluxo, e assim a
     linha não muda de altura quando o rótulo aparece ou some. */
  body.tela-estreita .menu-voltar-rotulo {
    max-width: 0;
    padding: 0;
    overflow: hidden;
  }
  body.tela-estreita #accountFabLabel {
    max-width: 0;
    padding: 0;
    overflow: hidden;
  }
  /* Sem rótulo, o botão de conta volta a ser o quadrado de 40px que os outros
     da barra são: os 12px de `padding` à direita existem só para separar o
     anel do apelido, e sem apelido eles seriam um vão morto dentro de um
     alvo de toque. */
  body.tela-baixa-deitada #accountFabBtn,
  body.tela-estreita #accountFabBtn {
    width: 40px;
    padding-right: 0;
    justify-content: center;
  }
  /* Abaixo de 420px (`tela-muito-estreita`, orientation.js) os vãos encolhem.
     É o último ajuste da barra. */
  body.tela-muito-estreita #menuTopbarLinha {
    padding: 8px 6px;
  }
  body.tela-muito-estreita #menuTopbarDir {
    padding-right: 6px;
  }
  body.tela-muito-estreita #menuTopbarLinha,
  body.tela-muito-estreita #menuTopbarDir {
    gap: 6px;
  }
  #menuTitle {
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    /* O logo tem a largura inteira da visão para ele (a tela inicial é uma
       coluna, ver #menuViewHome), então cresce bem mais do que crescia quando
       dividia a tela com o índice dos modos. Escala pelos *dois* eixos e fica
       com o menor: só por largura, uma janela larga e baixa dava um logo que
       comia a tela toda e empurrava os botões para fora dela.

       O número mora em `--capa-logo` (ver #menuViewHome): a linha de botões
       sai dele, então logo e botões nunca mais saem de proporção. */
    font-size: var(--capa-logo);
    line-height: 0.92;
    text-align: center;
    /* Dourado com contorno marrom-escuro, como o logo da ilustração da tela
       inicial (#cenaInicial): vermelho, ele sumia no céu vermelho dela. */
    color: var(--cor-ouro-suave);
    -webkit-text-stroke: 2px var(--cor-logo-contorno);
    text-stroke: 2px var(--cor-logo-contorno);
    animation: logoBrilhoPulsante 4s ease-in-out infinite;
    perspective: 400px;
  }
  .title-line {
    display: flex;
    justify-content: center;
  }
  .title-letter {
    display: inline-block;
    animation: tituloFlutuar 3s ease-in-out infinite;
  }
  .flip-letter {
    animation-name: tituloFlutuarInvertido;
  }
  #menuTitle .title-letter[data-i="1"] { animation-duration: 4.6s; animation-delay: -0.1s; }
  #menuTitle .title-letter[data-i="2"] { animation-duration: 5.4s; animation-delay: -2.4s; }
  #menuTitle .title-letter[data-i="3"] { animation-duration: 4.9s; animation-delay: -1.1s; }
  #menuTitle .title-letter[data-i="4"] { animation-duration: 5.8s; animation-delay: -3.5s; }
  #menuTitle .title-letter[data-i="5"] { animation-duration: 5.1s; animation-delay: -1.9s; }
  #menuTitle .title-letter[data-i="6"] { animation-duration: 5.6s; animation-delay: -0.7s; }
  #menuTitle .title-letter[data-i="7"] { animation-duration: 4.7s; animation-delay: -3s; }
  #menuTitle .title-letter[data-i="8"] { animation-duration: 6s; animation-delay: -1.6s; }
  #menuTitle .title-letter[data-i="9"] { animation-duration: 5.1s; animation-delay: -4.1s; }
  #menuTitle .title-letter[data-i="10"] { animation-duration: 5.4s; animation-delay: -0.4s; }
  @keyframes tituloFlutuar {
    0%, 100% {
      transform: translateY(0);
    }
    50% {
      transform: translateY(-4px);
    }
  }
  #menuTitle .title-letter.title-letter-hover {
    animation: tituloGirar 1s linear;
  }
  @keyframes tituloGirar {
    0% {
      transform: rotateY(0deg);
    }
    70% {
      transform: rotateY(420deg);
    }
    90% {
      transform: rotateY(340deg);
    }
    100% {
      transform: rotateY(360deg);
    }
  }
  #menuTitle .title-letter.flip-letter.title-letter-hover {
    animation-name: tituloGirarInvertido;
  }
  @keyframes tituloGirarInvertido {
    0% {
      transform: scaleX(-1) rotateY(0deg);
    }
    70% {
      transform: scaleX(-1) rotateY(420deg);
    }
    90% {
      transform: scaleX(-1) rotateY(340deg);
    }
    100% {
      transform: scaleX(-1) rotateY(360deg);
    }
  }
  @keyframes tituloFlutuarInvertido {
    0%, 100% {
      transform: scaleX(-1) translateY(0);
    }
    50% {
      transform: scaleX(-1) translateY(-4px);
    }
  }
  @keyframes logoBrilhoPulsante {
    0%, 100% {
      color: var(--cor-ouro-suave);
      text-shadow: 4px 4px 0 var(--cor-logo-contorno), 0 0 10px var(--cor-logo-brilho);
    }
    50% {
      color: var(--cor-ouro);
      text-shadow: 4px 4px 0 var(--cor-logo-contorno), 0 0 24px var(--cor-logo-brilho-forte);
    }
  }

  /* O nome é um <button> (abre o perfil do jogador), mas tem que parecer só um
     item da lista: nada de fundo, borda ou padding de botão. O placar fica
     dentro de #menu, então precisa do prefixo #menu para vencer em
     especificidade a regra genérica `#menu button:not(...)` — mesmo truque
     usado em `#menu button.link-button`. */
  #menu #hordeEndlessList .ranking-nome,
  #menu #hordeCampanhaList .ranking-nome,
  #menu #nivelRankingList .ranking-nome {
    min-width: 0;
    width: auto;
    padding: 0;
    border: none;
    border-radius: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  #menu #hordeEndlessList .ranking-nome:hover,
  #menu #hordeCampanhaList .ranking-nome:hover,
  #menu #nivelRankingList .ranking-nome:hover {
    background: none;
    text-decoration: underline;
  }
  #menu #hordeEndlessList .ranking-nome-proprio,
  #menu #hordeCampanhaList .ranking-nome-proprio,
  #menu #nivelRankingList .ranking-nome-proprio {
    color: var(--cor-ouro);
  }
  body.touch-mode #menu #hordeEndlessList .ranking-nome,
  body.touch-mode #menu #hordeCampanhaList .ranking-nome,
  body.touch-mode #menu #nivelRankingList .ranking-nome {
    display: flex;
    align-items: center;
  }
  /* Placar do Sem fim: o conteúdo da visão "Ranking" (ver
     hordeEndlessRanking.js), com a alternância solo/grupo em cima da lista.
     A moldura é a padrão (.menu-painel) — aqui só o que é do placar.

     O placar usa a largura inteira da moldura. Já foi uma coluna de 320px
     centrada — o argumento era não esticar o nome do jogador por mil pixels —,
     mas na moldura padrão (1040px) aquilo deixava dois terços da tela vazios de
     cada lado, e a tela lia como um erro de layout. O que segura a leitura na
     largura cheia é a linha ter fundo próprio (ver #hordeEndlessList li): o
     olho segue a faixa do nome até os números, em vez de atravessar um vão. */
  /* A visão "Ranking" tem **três** placares e mostra **um por vez** (ver
     #rankingAbas no index.html e public/js/rankingAbas.js). Eles já
     conviveram lado a lado, em colunas, como a Defesa da base e o Monstruário
     fazem; com três, as colunas de marca (que são de largura fixa) comiam a do
     nome do jogador, e o nome é o que a linha existe para mostrar. */
  #rankingCols {
    display: flex;
    flex: 1;
    min-height: 0;
  }
  /* O placar da aba apagada some de verdade: `display: none` e não só
     `visibility`, porque é o que tira o painel do alcance do foco por controle
     **e** o que faz o `IntersectionObserver` de cada placar parar o poll dele
     (ver public/js/rankingAbas.js). A regra existe porque `.ranking-coluna` é
     `display: flex`, e um `display` explícito vence o `hidden` do HTML. */
  .ranking-coluna[hidden] {
    display: none;
  }
  /* O placar visível, centrado numa coluna de leitura.

     Ele **não** usa a largura inteira do painel: sozinho na tela, o nome ficava
     numa borda e os números na outra, com meio metro de linha vazia no meio — o
     olho perde a linha no caminho, que é o mesmo motivo de a linha ter fundo
     próprio. 760px é a largura em que o nome mais longo cabe inteiro e a marca
     ainda está no campo de visão dele. Não é a coluna de 320px centrada que o
     placar já teve e perdeu: aquela deixava dois terços da moldura vazios de
     cada lado e lia como erro de layout. */
  .ranking-coluna {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    flex: 1 1 auto;
    width: 100%;
    max-width: 760px;
    margin: 0 auto;
    min-width: 0;
    height: 100%;
    box-sizing: border-box;
  }
  /* A fileira de abas, que é o cabeçalho da tela: a aba acesa nomeia o placar
     de baixo, e por isso nenhum dos três tem <h2> próprio.

     O material é o das linhas de lista e dos botões de ícone — silhueta
     recortada em escadinha, contorno na cor da borda —, e a acesa é vermelha
     como as abas que o Sem fim teve antes: é a cor de "esta é a que está na
     tela" no menu inteiro. */
  #rankingAbas {
    display: flex;
    justify-content: center;
    gap: 8px;
    flex: none;
    margin-bottom: 4px;
  }
  #menu .ranking-aba {
    --cor-fundo-aba: var(--cor-superficie);
    --cor-contorno-aba: var(--cor-pedra-borda);
    width: auto;
    min-width: 180px;
    padding: 10px 24px;
    border: none;
    border-radius: 0;
    background-color: transparent;
    clip-path: var(--recorte-icone-pixel);
    background-image:
      linear-gradient(var(--cor-fundo-aba), var(--cor-fundo-aba)),
      linear-gradient(var(--cor-fundo-aba), var(--cor-fundo-aba)),
      linear-gradient(var(--cor-contorno-aba), var(--cor-contorno-aba));
    background-size:
      calc(100% - 4px) calc(100% - 12px),
      calc(100% - 12px) calc(100% - 4px),
      100% 100%;
    background-position: 2px 6px, 6px 2px, 0 0;
    background-repeat: no-repeat;
    color: var(--cor-texto-medio);
    font-family: inherit;
    font-size: 15px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    cursor: pointer;
  }
  #menu .ranking-aba:hover {
    --cor-contorno-aba: var(--cor-borda-clara);
    color: var(--cor-texto);
  }
  #menu .ranking-aba-ativa,
  #menu .ranking-aba-ativa:hover {
    --cor-fundo-aba: var(--cor-vermelho-destaque);
    --cor-contorno-aba: var(--cor-vermelho-destaque);
    color: var(--cor-texto);
  }
  /* Em tela estreita o painel do menu inteiro deixa de esticar (ver
     `body.tela-estreita .menu-painel`), e a coluna acompanha: altura do
     conteúdo, com a lista rolando dentro do painel e não dentro dela. */
  body.tela-estreita .ranking-coluna {
    height: auto;
  }
  /* As abas empilham quando três não cabem em fileira — três rótulos curtos
     cabem até ~420px, e abaixo disso a fileira quebra em vez de espremer o
     rótulo até a reticência. */
  body.tela-estreita #rankingAbas {
    flex-wrap: wrap;
  }
  body.tela-estreita #menu .ranking-aba {
    min-width: 0;
    flex: 1 1 auto;
    padding: 8px 12px;
    font-size: 13px;
  }
  /* A fileira de controles da campanha: só a fase (um <select>, o único do
     jogo — são oito fases, e oito abas não cabem na coluna). Ela dividiu esta
     fileira com duas abas de marca (melhor onda / menor tempo) até a marca
     virar uma só. */
  #hordeCampanhaControles {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  /* A largura e a centralização moram na **fileira**, e não no campo: é ela que
     ancora as setas de foco (ver o bloco "As setas do seletor de fase" no fim
     deste arquivo), e para isso ela precisa ter exatamente a caixa do campo. */
  #hordeCampanhaFaseLinha {
    position: relative;
    width: 320px;
    /* Os 28px que faltam para 100% são o **lugar das setas** (12px de cada lado,
       mais 2px de folga): a coluna do placar chega a ter a largura exata do
       campo no celular, e ali as setas cairiam fora do painel — que tem
       `overflow: hidden` e as recortaria inteiras. No desktop a largura fixa
       ganha, então isto não muda nada lá. */
    max-width: calc(100% - 28px);
    align-self: center;
  }
  #hordeCampanhaControles .campo-pixel {
    width: 100%;
    box-sizing: border-box;
  }
  /* O **único** `<select>` do jogo (são oito fases, e oito abas não caberiam na
     coluna). Ele é o caso em que "deixar o navegador desenhar" não serve: a
     lista suspensa é do sistema, não da página, e não herda o tema.

     Duas armadilhas, e as duas custaram uma lista ilegível:
     - `background: transparent` no `<select>` deixa a lista suspensa cair no
       branco do sistema, com o `color` claro que a página pediu por cima —
       texto branco em fundo branco. O fundo tem de ser **opaco** e escrito
       aqui;
     - o `<option>` não herda do `<select>` no Chromium: sem uma regra própria,
       cada linha da lista volta ao branco mesmo com o campo escuro. */
  #hordeCampanhaFase {
    width: 100%;
    box-sizing: border-box;
    border: none;
    background-color: var(--cor-superficie);
    color: var(--cor-texto);
    font: inherit;
    font-size: 13px;
    padding: 6px 8px;
    /* A seta nativa sai (ela é desenhada pelo sistema, antisserrilhada, e é a
       única forma vetorial que sobraria nesta tela) e no lugar dela vai uma
       escadinha de três degraus pintada com gradiente — a mesma técnica das
       molduras de pixel art do resto da interface, e não um SVG. `padding-right`
       abre o espaço dela, senão o nome da fase passa por baixo. */
    appearance: none;
    cursor: pointer;
    padding-right: 22px;
    background-image:
      linear-gradient(var(--cor-texto-medio) 0 0),
      linear-gradient(var(--cor-texto-medio) 0 0),
      linear-gradient(var(--cor-texto-medio) 0 0);
    background-repeat: no-repeat;
    background-size: 8px 2px, 4px 2px, 2px 2px;
    background-position:
      right 8px top calc(50% - 2px),
      right 10px top 50%,
      right 11px top calc(50% + 2px);
  }
  #hordeCampanhaFase option {
    background-color: var(--cor-superficie);
    color: var(--cor-texto);
  }
  /* Três colunas, e não as cinco do Sem fim: aqui a marca é uma só (o tempo de
     vitória) e não há categoria a distinguir. */
  #hordeCampanhaHeader {
    display: grid;
    grid-template-columns: 28px 1fr 90px;
    gap: 12px;
    padding: 0 12px;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  #hordeCampanhaHeader span:nth-child(3) {
    text-align: right;
  }
  #hordeCampanhaList,
  #nivelRankingList {
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  #hordeCampanhaList li {
    display: grid;
    grid-template-columns: 28px 1fr 90px;
    align-items: center;
    gap: 12px;
    padding: 6px 12px;
    font-size: 14px;
  }
  /* Placar de nível: quatro colunas, com a XP mais estreita que o nível — ela
     é o desempate, não a marca (ver public/js/nivelRanking.js). O cabeçalho
     repete a grade da linha, com o mesmo padding lateral, para os títulos
     caírem em cima das colunas que nomeiam. */
  #nivelRankingHeader,
  #nivelRankingList li {
    display: grid;
    grid-template-columns: 28px 1fr 90px 90px;
    gap: 12px;
    padding: 0 12px;
  }
  #nivelRankingHeader {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  #nivelRankingHeader span:nth-child(3),
  #nivelRankingHeader span:nth-child(4) {
    text-align: right;
  }
  #nivelRankingList li {
    align-items: center;
    padding: 6px 12px;
    font-size: 14px;
  }
  /* A **própria colocação**, embaixo da lista dos dois placares (ver
     `escreverColocacao` em rankingLinhas.js). Ela não é "mais uma linha do
     top" — é a resposta a outra pergunta, e por isso fica fora da <ol>, com
     uma borda separando as duas. Dourada, o mesmo "você" da linha destacada na
     lista e do cartão do jogador local no HUD.

     `flex: none` porque a lista acima é quem estica: sem isso, numa coluna
     baixa a linha do jogador seria a primeira a ser espremida — justamente a
     que ele veio ler. */
  .ranking-minha-colocacao {
    flex: none;
    margin: 0;
    padding: 8px 12px 0;
    border-top: 2px solid var(--cor-borda);
    font-size: 13px;
    color: var(--cor-ouro);
  }
  /* "Você ainda não tem marca aqui" é ausência, não resultado: um tom abaixo,
     e sem o dourado — dourado é a marca de "você **está** no placar". */
  .ranking-minha-colocacao--sem-marca {
    color: var(--cor-texto-fraco);
  }
  #hordeEndlessPanel > * {
    width: auto;
    max-width: 100%;
  }
  /* Mesma grade das linhas da lista abaixo (incluindo o padding lateral), para
     os títulos caírem em cima das colunas que nomeiam. */
  /* Cinco colunas: a estreita entre o nome e as ondas é o ícone de categoria
     (solo ou grupo), a coluna que a alternância solo/grupo virou. Ela não tem
     título — dois desenhos de 12px não cabem embaixo de um rótulo, e o balão do
     próprio ícone é quem diz o que ele é. */
  #hordeEndlessHeader {
    display: grid;
    grid-template-columns: 28px 1fr 24px 90px 90px;
    gap: 12px;
    padding: 0 12px;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  #hordeEndlessHeader span:nth-child(4),
  #hordeEndlessHeader span:nth-child(5) {
    text-align: right;
  }
  #hordeEndlessList {
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  /* A linha tem fundo próprio: na largura cheia da moldura o nome fica longe
     dos números, e sem a faixa o olho perde a linha no meio do caminho.

     E é a **mesma peça** da linha do Monstruário e da do Relicário — é uma
     linha de lista nas três telas: silhueta recortada em escadinha de um degrau
     (a dos botões de ícone, a escala certa para uma linha desta altura). Era um
     retângulo reto de fundo chapado, e depois que aquelas duas ganharam
     material esta ficou sendo a única superfície do menu que não parecia do
     jogo. Nada de `border` junto: recortada pelo `clip-path`, ela sumiria
     justamente na diagonal dos cantos (o mesmo motivo de .menu-painel e de
     .monstruario-row). O bisel de pedra que ela teve saiu junto com o do
     Monstruário — ver "Linha de lista com contorno". */
  #hordeEndlessList li {
    display: grid;
    grid-template-columns: 28px 1fr 24px 90px 90px;
    align-items: center;
    gap: 12px;
    padding: 6px 12px;
    font-size: 14px;
  }
  /* `:not(.skeleton-row)` porque este bloco vence `.skeleton-row` por
     especificidade (id + elemento contra uma classe): sem ele o fundo chapado
     e a animação de entrada apagavam o shimmer, e as três linhas de espera
     apareciam paradas. */
  #hordeEndlessList li:not(.skeleton-row),
  #hordeCampanhaList li:not(.skeleton-row),
  #nivelRankingList li:not(.skeleton-row) {
    /* Miolo e contorno vêm da regra compartilhada com `.monstruario-row`, lá
       no alto do arquivo ("Linha de lista com contorno"), que é também onde
       está o porquê de esta linha não ter o bisel de pedra. Daqui saem só as
       duas cores. */
    --cor-fundo-linha: var(--cor-superficie);
    --cor-contorno-linha: var(--cor-fundo-linha);
    background-color: transparent;
    clip-path: var(--recorte-icone-pixel);
    animation: overlay-fade 0.35s ease both;
  }
  /* A linha do próprio jogador é marcada no **dourado** de "você", como o
     cartão do jogador local no HUD — o nome já vinha dourado, mas sozinho ele
     se perdia numa lista de dez nomes claros. Contorno em `background-image`,
     que acompanha o recorte dos cantos (o mesmo desenho da linha selecionada
     do Monstruário, trocando o vermelho de seleção pelo ouro de identidade —
     ver .monstruario-row-ativa para o porquê de não ser um anel `inset`). */
  #hordeEndlessList li.ranking-linha-propria,
  #hordeCampanhaList li.ranking-linha-propria,
  #nivelRankingList li.ranking-linha-propria {
    --cor-contorno-linha: var(--cor-ouro);
  }
  /* A linha de espera tem a altura de uma linha de verdade, para a lista não
     encolher quando o placar chega. */
  #hordeEndlessList li.skeleton-row,
  #hordeCampanhaList li.skeleton-row,
  #nivelRankingList li.skeleton-row {
    box-sizing: border-box;
    height: 30px;
  }
  #hordeEndlessList .ranking-posicao,
  #hordeCampanhaList .ranking-posicao,
  #nivelRankingList .ranking-posicao {
    color: var(--cor-texto-fraco);
    font-size: 12px;
    text-align: right;
  }
  #menu #hordeEndlessList .ranking-nome,
  #menu #hordeCampanhaList .ranking-nome,
  #menu #nivelRankingList .ranking-nome {
    min-width: 0;
    width: auto;
    padding: 0;
    border: none;
    border-radius: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  #menu #hordeEndlessList .ranking-nome:hover,
  #menu #hordeCampanhaList .ranking-nome:hover,
  #menu #nivelRankingList .ranking-nome:hover {
    background: none;
    text-decoration: underline;
  }
  #menu #hordeEndlessList .ranking-nome-proprio,
  #menu #hordeCampanhaList .ranking-nome-proprio,
  #menu #nivelRankingList .ranking-nome-proprio {
    color: var(--cor-ouro);
  }
  #hordeEndlessList .horde-endless-ondas,
  #hordeCampanhaList .horde-endless-ondas,
  #nivelRankingList .horde-endless-ondas {
    color: var(--cor-azul-ranking);
    font-weight: bold;
    text-align: right;
  }
  #hordeEndlessList .horde-endless-abates,
  #nivelRankingList .horde-endless-abates {
    color: var(--cor-texto-fraco);
    text-align: right;
  }
  /* O ícone de solo/grupo. Cinza como os abates: ele é o rótulo da linha, não a
     marca dela — quem a linha destaca continua sendo o nome (dourado quando é
     você) e as ondas (azul). O `currentColor` do <svg> é quem pega esta cor. */
  #hordeEndlessList .ranking-categoria {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--cor-texto-fraco);
  }
  #hordeEndlessList li.ranking-linha-propria .ranking-categoria {
    color: var(--cor-ouro);
  }
  .ranking-vazio, .ranking-erro {
    margin: 0;
    font-size: 14px;
    text-align: center;
    color: var(--cor-texto-medio);
  }
  /* As exclusões de componente (`.horde-level-item`, `.toggle-switch`,
     `.monstruario-row`, `.talent-no`, `.talent-aba`, `.ranking-aba`)
     existem porque
     #menuViewHorde e o placar do Sem fim trouxeram para dentro de #menu
     componentes que antes viviam numa modal fora dele
     (#hordeRoomsOverlay) ou têm estilo de aba próprio —
     sem elas esta regra genérica vencia a deles por especificidade (id
     vence classe) e virava um botão vermelho de ação por cima do cartão/
     interruptor/linha/aba original. */
  #menu button:not(.link-button):not(.horde-level-item):not(.toggle-switch):not(.monstruario-row):not(.talent-no):not(.talent-aba):not(.ranking-aba):not(.config-aba):not(.config-opcao):not(.tela-fechar-botao) {
    font-size: 20px;
    padding: 14px 32px;
    border-radius: 8px;
    border: none;
    cursor: pointer;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
    width: 100%;
    box-sizing: border-box;
  }
  #menu button:not(.link-button):not(.horde-level-item):not(.toggle-switch):not(.monstruario-row):not(.talent-no):not(.talent-aba):not(.ranking-aba):not(.config-aba):not(.config-opcao):not(.tela-fechar-botao):hover {
    background: var(--cor-vermelho-hover);
  }
  #nicknameField {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    width: 100%;
  }
  #nicknameLabel {
    font-size: 13px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    text-align: center;
  }
  /* Campo + "OK" na mesma linha: o botão só fecha a modal (ver
     profile.js), então não pode roubar largura do campo — ele encolhe até o
     texto e o input fica com o resto. */
  #nicknameRow {
    display: flex;
    align-items: stretch;
    gap: 8px;
    width: 100%;
  }
  #btnNicknameOk {
    flex: 0 0 auto;
    padding: 0 18px;
    font-family: inherit;
    font-size: 16px;
    color: var(--cor-branco);
    background: var(--cor-vermelho);
    cursor: pointer;
  }
  #btnNicknameOk:hover {
    background: var(--cor-vermelho-hover);
  }
  /* Cores da moldura em pixel art — ver o bloco no fim do arquivo. */
  #nicknameField .campo-pixel {
    --cor-fundo-campo: var(--cor-superficie-elevada);
    --cor-borda-campo: var(--cor-branco);
  }
  #nicknameField .campo-pixel:focus-within {
    --cor-borda-campo: var(--cor-vermelho-destaque);
  }
  #nicknameInput {
    font-size: 18px;
    text-align: center;
  }
  #nicknameError {
    visibility: hidden;
    color: var(--cor-erro);
    font-size: 13px;
    line-height: 16px;
    text-align: center;
  }
  #nicknameError.visible {
    visibility: visible;
  }
  /* A largura é definida por gameScale.js (é a largura da arena depois de
     caber na tela); 800px é só o valor natural antes dele rodar. */
  #game-wrap {
    display: none;
    position: relative;
    flex-direction: column;
    align-items: center;
    width: 800px;
    max-width: 100%;
    box-sizing: border-box;
  }
  @keyframes desempatePulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
  }
  /* Caixa da fileira de vida (`createResourceBar` em hud.js): é ela que dá
     cor e tamanho de ícone ao recurso (ver "Fileiras de ícones" mais
     abaixo), e não uma classe por tipo no JS. Houve uma irmã `.shields`, a
     fileira de escudo do duelo 1x1 — o modo saiu na beta 0.1.8 e nada mais
     cria essa caixa. */
  .hearts {
    display: flex;
    width: 100%;
  }
  /* Trilho cavado na pedra: o padding é o que deixa o bisel aparecer em volta
     do preenchimento (sombra `inset` é pintada abaixo dos filhos, então sem a
     folga a .cooldown-fill cobriria o bisel inteiro). */
  .cooldown-bar {
    width: 100%;
    height: 10px;
    box-sizing: border-box;
    padding: 2px;
    border-radius: 0;
    background: var(--cor-superficie-baixa);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-afundada);
    overflow: hidden;
  }
  /* A barra enche em blocos, não num preenchimento liso: o gradiente repetido
     desenha um bloco de 6px e um vão de 2px, então o que cresce são as
     divisões — a mesma leitura "em degraus" da arte do canvas. Precisa ser
     `repeating-`: um `linear-gradient` comum estica a última parada até o fim,
     e a barra inteira ficava pintada com a cor do vão (a mesma do trilho), ou
     seja, enchia sem aparecer. */
  .cooldown-fill {
    height: 100%;
    width: 0%;
    /* Vermelho enquanto recarrega, amarelo quando o tiro está disponível. */
    background-color: var(--cor-hud-tiro);
    background-image: repeating-linear-gradient(
      90deg,
      transparent 0 6px,
      var(--cor-superficie-icone) 6px 8px
    );
    transition: background-color 0.15s;
  }
  /* Só a cor muda: `background` no atalho apagaria os blocos do gradiente. */
  .cooldown-fill.ready {
    background-color: var(--cor-hud-tiro-pronto);
  }
  .resource-bar {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    min-width: 0;
  }
  /* ===== Fileira de ícones (vida) =====
     Um ícone por unidade *atual* do recurso (ver updateIconsRow em hud.js),
     sem ícone vazio pra unidade já perdida — diferente da barra-trilho
     segmentada que existia antes. A largura fica limitada pela caixa que
     contém (`.hearts`); quando os ícones não cabem lado a lado,
     `--icon-overlap` (escrita por layoutIconsRow) sobrepõe uns aos outros em
     vez de encolher ou quebrar linha. Cor e tamanho do pixel saem da caixa
     (HEART_PIXEL_VMIN em hordeHud.js), não de uma classe por tipo. */
  .resource-icons-row {
    display: flex;
    align-items: center;
    /* `flex: 1`, não `width: 100%`: o contador ao lado (ver .resource-count
       abaixo) também mora dentro de `.resource-bar`, e é essa fileira que
       precisa ceder espaço pra ele, não o contrário. */
    flex: 1;
    min-width: 0;
  }
  /* `.resource-icon.resource-unit` (duas classes), não só `.resource-unit`:
     `.resource-icon` reserva 4px à direita pra separar do número no contador,
     e como as duas regras têm um seletor de classe cada, quem ganhava era a
     que vem depois no arquivo (`.resource-icon`, mais abaixo) — os 4px
     sobreviviam em cada ícone da fileira. Isso é o que fazia a fileira vazar
     por cima do contador: layoutIconsRow (hud.js) calcula a sobreposição
     contando só a largura dos ícones e o vão de `--icon-overlap`, então esses
     4px por ícone eram 4px × contagem de largura que ninguém previa (com 14
     corações, mais de 50px passando por fora da caixa). Aqui quem dá *todo* o
     espaçamento entre ícones é a margem de `--icon-overlap` abaixo. */
  .resource-icon.resource-unit {
    margin-right: 0;
  }
  .hearts .resource-unit {
    color: var(--cor-hud-vida-texto);
  }
  /* Contorno do ícone (separa visualmente ícones concatenados/sobrepostos,
     ver layoutIconsRow em hud.js): pixels extras desenhados por baixo do
     preenchimento, uma dilatação de 1 célula da silhueta (contornoDaGrade em
     hud.js) — não `filter: drop-shadow`, que força o navegador a rasterizar o
     ícone inteiro (dezenas de divs) a cada repaint e, com até 14 ícones por
     jogador *e* a animação de piscar (`.resource-unit.blink`, que já muda
     opacity a cada frame), travava o jogo inteiro enquanto o HUD existia na
     tela. Cor fixa (não `currentColor`): o contorno é sempre preto,
     independente do recurso. */
  /* `.resource-icon-pixel.resource-icon-outline` (duas classes), não só
     `.resource-icon-outline`: precisa vencer `.resource-icon-pixel {
     background: currentColor }` (mesma especificidade de seletor único, e
     esse elemento tem as duas classes) sem depender da ordem das regras no
     arquivo. */
  .resource-icon-pixel.resource-icon-outline {
    background: var(--cor-contorno-icone-hud);
  }
  .resource-unit:not(:first-child) {
    margin-left: var(--icon-overlap, 2px);
  }
  .resource-unit.bonus {
    color: var(--cor-ouro);
  }
  /* Meio coração (dano fracionário): corte seco na metade, sem transição —
     mesmo corte reto que o resto da pixel art usa. */
  .resource-unit.half {
    clip-path: inset(0 50% 0 0);
  }
  /* Pisca em degraus (era o piscar do coração/carga perdida): a animação
     marca só os ícones que saíram da fileira nesse hit, antes de serem
     removidos (ver ICON_REMOVE_DELAY em hud.js). */
  .resource-unit.blink {
    animation: segmentBlink 0.4s steps(1, start) 3;
  }
  @keyframes segmentBlink {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.15; }
  }
  /* Coração perdido explode em vez de piscar (ver triggerSegmentRemove em
     hud.js). `steps(1, end)` entre os keyframes, não easing contínuo — mesma
     regra de pixel art de nunca ter transição suave (ver "Nada de transição
     contínua" no CLAUDE.md): o ícone salta de estágio em estágio, cresce e
     clareia num flash antes de sumir. Dura o mesmo tanto que o blink
     (ICON_REMOVE_DELAY/HIT_FLASH_DURATION, 0.4s), pra continuar sumindo no
     mesmo instante em que o ícone é removido do DOM. `z-index` durante a
     explosão pra não ficar por baixo do ícone vizinho (`--icon-overlap`
     sobrepõe ícones com margem negativa). */
  .resource-unit.explode {
    position: relative;
    z-index: 1;
    animation: segmentExplode 0.4s steps(1, end) 1;
  }
  @keyframes segmentExplode {
    0% { transform: scale(1); opacity: 1; filter: brightness(1); }
    25% { transform: scale(1.3); opacity: 1; filter: brightness(2.4); }
    50% { transform: scale(1.7); opacity: 0.75; filter: brightness(1.6); }
    75% { transform: scale(2); opacity: 0.35; filter: brightness(1); }
    100% { transform: scale(2.2); opacity: 0; }
  }
  /* Alarme de vida baixa (marcarVidaBaixa em hud.js, limiar
     FRACAO_VIDA_BAIXA): a barra de vida inteira — ícones e contador — pisca
     entre o vermelho escuro do HUD e o vermelho claro da interface. Pisca
     entre *duas cores*, e não entre a cor e o nada, pela mesma razão do aviso
     de onda desenhado no canvas (ver public/js/CLAUDE.md): alternando com
     invisível, metade do tempo não haveria coração nenhum na tela e isso lê
     como falha de desenho em vez de urgência — ainda mais aqui, onde o estado
     dura o resto da partida em vez de alguns segundos. `steps(1, end)`, sem
     esmaecer entre uma cor e outra.
     Os dois seletores têm uma classe a mais que as regras de cor de `.hearts`
     (`.hearts .resource-unit`/`.hearts .resource-count`), que é o que os faz
     vencer sem depender da ordem no arquivo. */
  @keyframes vidaBaixaPulso {
    0%, 100% { color: var(--cor-hud-vida-texto); }
    50% { color: var(--cor-vermelho-destaque); }
  }
  .resource-count {
    flex: none;
    display: flex;
    align-items: center;
    font-weight: 600;
    /* Número de largura fixa: sem isso a barra muda de tamanho a cada hit,
       porque o contador encolhe junto com o valor. */
    font-variant-numeric: tabular-nums;
  }
  /* Largura fixa (não só `tabular-nums`, que só garante dígito a dígito): o
     valor varia de "1" a "22,5", e sem uma largura reservada pro pior caso o
     container inteiro (`.hearts`) muda de tamanho a cada hit — a fileira de
     ícones ao lado se reacomoda junto. `white-space: nowrap` porque um valor
     maior que a reserva deve vazar por cima do vizinho, não quebrar linha.
     A largura é `vmin` e não `ch`: `ch` resolve pelo `font-size` do próprio
     `.resource-count`, que muda com a tela — a fileira de ícones passaria a
     começar numa coluna diferente conforme o número encolhe. */
  .resource-int {
    display: inline-block;
    white-space: nowrap;
    text-align: right;
    min-width: clamp(26px, 10vmin, 40px);
  }
  /* `clamp` com `vmin`, não `px` fixo: mesmo motivo do ícone
     (HEART_PIXEL_VMIN em hordeHud.js) — um tamanho fixo em px lê pequeno
     demais na tela reduzida de um celular. O teto em px mantém o valor de
     sempre em telas grandes, e o piso evita o número sumir num vmin bem
     pequeno. */
  .hearts .resource-count {
    font-size: clamp(9px, 3.2vmin, 14px);
    color: var(--cor-hud-vida-texto);
    font-weight: 700;
    -webkit-text-stroke: 0.6px currentColor;
  }
  /* Ícone pixel-art do contador (coração), montado em DOM por
     createResourceIcon: um div por pixel, cor herdada do contador. */
  .resource-icon {
    position: relative;
    flex: none;
    /* O espaço fica só entre o ícone e o número: "14/14" é um valor só, e um
       gap no contador inteiro abriria um vão antes da barra também. */
    margin-right: 4px;
  }
  .resource-icon-pixel {
    position: absolute;
    background: currentColor;
  }
  canvas {
    background: var(--cor-superficie-elevada);
    border: none;
    display: block;
    /* O buffer do canvas é sempre 800x600 e o CSS o reduz para caber na tela
       (gameScale.js), quase nunca por um fator inteiro. Sem isto o navegador
       interpola essa redução e borra a arena inteira — sprites, bolhas,
       explosões, tudo — desfazendo o trabalho de desenhar em grade
       (public/js/pixel.js). Com `pixelated` a redução é por vizinho mais
       próximo: perde-se uma coluna aqui e ali, mas a arte continua nítida,
       que é o que a estética pede. */
    image-rendering: pixelated;
  }
  /* Só o fundo inicial, antes do primeiro frame: o fundo e a borda da arena
     são desenhados dentro do canvas (render.js), porque o canvas é reduzido por
     CSS para caber na tela (gameScale.js) e uma borda CSS fina desapareceria
     nessa redução. */
  #game {
    background: var(--cor-superficie-elevada);
  }
  /* Defesa da base, jogador local caído (hordeHud.js): a visão dele fica em
     escala de cinza. A cena continua sendo desenhada normalmente — a câmera é
     que passa a seguir um companheiro vivo (camera.js) —, e a falta de cor é o
     que diz, num relance, que ele está assistindo e não jogando. Só o canvas
     do mundo: o HUD precisa continuar legível (a vida da base é vermelha por
     design) e o mini-mapa é onde ele acompanha o grupo. Sem transição: a troca
     é seca, como o resto da interface (ver docs/pixel-art.md). */
  body.horde-morto #game {
    filter: grayscale(1);
  }
  /* A **volta** da cor ao renascer (`horde-voltando`, posta por iniciarVoltaDaCor
     em hordeHud.js). Tirar `horde-morto` devolvia a cor num corte seco de um
     quadro; aqui ela volta em **três degraus** — `steps(3)`, a mesma regra de
     toda animação do projeto, e nunca uma transição contínua. A classe é
     retirada por timer no fim da janela: mantida, a próxima queda começaria com
     a animação já consumida. */
  @keyframes horde-volta-da-cor {
    from { filter: grayscale(1); }
    to { filter: grayscale(0); }
  }
  body.horde-voltando #game {
    animation: horde-volta-da-cor 390ms steps(3, end) 1;
  }
  #canvas-wrap {
    position: relative;
    width: 100%;
  }
  /* Fica na faixa de **baixo** da arena, e isso é a decisão inteira: o jogador
     está sempre no centro (a câmera o segue), a base é o centro do mundo, o topo
     é do HUD de vida da base e do mural de avisos (desenhado no canvas em
     `MURAL_TOPO`, hordeRender.js), e os inimigos chegam por todos os lados. O
     que sobra é a faixa de baixo — a única parte da tela que nada mais disputa.
     `z-index` acima da tela de escolha de item (12) e do painel de builds (13):
     o passo de escolher item precisa ser lido **com a tela de escolha aberta**,
     e é `body.tutorial-ativo` (abaixo) que empurra aquele painel para cima para
     os dois não se cobrirem.

     O material é `.hud-pedra` (classe no HTML), a mesma laje da faixa da base e
     dos cartões de jogador que dividem a arena com ela — era a última caixa
     preta reta sobre o chão de pedra, e o passo do tutorial é justamente o que
     o jogador mais novo do jogo lê primeiro. **Com** pregos, ao contrário de
     quase todo o resto do HUD: a faixa é alta (~170px no desktop) e tem 16px de
     respiro em cima e embaixo, então as duas cabeças de cada lado não se
     encostam nem passam por trás do texto — a mesma exceção da moldura do
     mini-mapa. */
  #matchTutorialBanner {
    position: absolute;
    /* Na faixa entre o jogador (que a câmera mantém no centro) e os cartões de
       jogador do HUD, presos na borda de baixo — não colada na borda, senão ela
       cobre os cartões. */
    bottom: 14%;
    left: 50%;
    z-index: 14;
    transform: translateX(-50%);
    max-width: 72%;
    /* A escadinha come 4px de cada canto: o padding lateral é o do resto das
       peças que ganharam a laje, com folga de sobra para o texto não começar
       dentro dela. */
    padding: 16px 28px;
    color: var(--cor-texto);
    font-size: clamp(18px, calc(0.046 * var(--largura-tela)), 24px);
    line-height: 1.35;
    text-align: center;
    pointer-events: none;
    opacity: 0;
    /* Em degraus, como o resto da interface: a escala contínua do `ease` era a
       transição suave que sobrava aqui. */
    transition: opacity 0.24s steps(3, end);
  }
  #matchTutorialBanner.visible {
    opacity: 1;
  }
  /* Flash de passo cumprido. A laje inteira acende: o contorno e os pregos vão
     para o verde de sucesso e o miolo para uma pedra esverdeada — não é uma
     caixa verde chapada por cima, é a mesma placa com outra cor de material,
     que é o que mantém o retorno legível sem trocar a gramática no meio do
     tutorial. Sem `transition`: a troca é seca, como todo estado da
     interface. */
  #matchTutorialBanner.sucesso {
    --cor-prego: var(--cor-sucesso);
    --cor-prego-luz: var(--cor-sucesso-claro);
    --cor-prego-sombra: #1f6b45;
  }
  #matchTutorialBanner.sucesso::before {
    background: var(--cor-sucesso);
  }
  #matchTutorialBanner.sucesso::after {
    background-color: #122b1d;
  }
  /* Dica do tiro automático (public/js/tiroAutomatico.js). Mesma caixa e o
     mesmo lugar da faixa do tutorial — é a mesma ideia (uma frase do jogo para
     o jogador, sobre a arena) e as duas nunca aparecem juntas: a dica é
     suprimida durante o tutorial. Fonte um degrau menor porque ela é um
     recado de passagem, e não a instrução que segura o passo. Ela é
     `.hud-pedra` pelo mesmo motivo que a faixa: "mesma caixa" só continua
     verdade se as duas forem do mesmo material — e o **T** dela é a mesma tecla
     desenhada (`.tecla`) dos passos do tutorial, porque as duas pedem a mesma
     coisa ("aperte isto").

     O prego é decidido **por mensagem**, no JS (`mostrarFaixa`,
     `tiroAutomatico.js`), e não aqui: a dica de entrada tem 90px e sobra folga
     para as quatro cabeças, o aviso de ligar/desligar tem 50px e ali as duas do
     mesmo lado quase se encostam. Este é o caso que mostra que `sem-pregos` é
     do **tamanho** da peça, e não do elemento. */
  #dicaTiroAutomatico {
    position: absolute;
    bottom: 14%;
    left: 50%;
    z-index: 14;
    transform: translateX(-50%);
    max-width: 72%;
    padding: 12px 22px;
    color: var(--cor-texto);
    font-size: clamp(15px, calc(0.036 * var(--largura-tela)), 19px);
    line-height: 1.4;
    text-align: center;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.24s steps(3, end);
  }
  #dicaTiroAutomatico.visible {
    opacity: 1;
  }
  #dicaTiroAutomatico strong {
    color: var(--cor-ouro-suave);
  }
  .dica-tiro-auto-sub {
    color: var(--cor-texto-fraco);
    font-size: 0.8em;
  }
  /* Com a faixa do tutorial na tela, a tela de escolha de item sobe: os dois
     ocupariam o mesmo lugar, e o passo que a faixa está explicando é justamente
     a escolha. Vale só durante o tutorial — fora dele o painel continua
     centrado. */
  body.tutorial-ativo #itemChoiceOverlay {
    align-items: flex-start;
  }
  /* Teto de partida, e só isso: quem decide o teto de verdade é o JS, medindo
     a faixa do passo já montada (`acomodarFaixaDoTutorial` em escolhaDeItens.js)
     e escrevendo um `max-height` em px no painel. A fração aqui era o teto
     **final**, e ela errava nos dois sentidos: em desktop sobravam ~75px de
     arena vazia entre o painel e a faixa (com a lista rolando por dentro do
     painel, escondendo pedaço das cartas), e em arena baixa a faixa cobria o
     painel e o cartão do jogador. Ela fica como piso do caso em que a modal é
     montada antes de haver layout para medir. */
  body.tutorial-ativo #itemChoicePainel {
    max-height: 58%;
    /* Nesta etapa o painel divide a arena com a faixa do passo, e a folga entre
       título, lista, barra de tempo e rodapé é o que ele tem de mais barato a
       ceder: os 60px que as cinco folgas somam no desktop valem um degrau
       inteiro de escala da carta (a carta é o que se lê aqui, o respiro não). */
    gap: 4px;
  }
  /* Arena curta: a instrução do passo entra **dentro** do painel e a faixa sai
     da arena (ver o arranjo embutido em acomodarFaixaDoTutorial). Empilhar as
     duas caixas numa arena de ~390px de altura só faz uma cobrir a outra, e o
     que não pode ser coberto é a oferta. */
  body.escolha-com-faixa-embutida #matchTutorialBanner {
    display: none;
  }
  /* Ela é a instrução, não o subtítulo: mesma cor de destaque do título e uma
     faixa de pedra atrás, para não ser lida como a linha de "Escolha um item"
     que fica logo acima. Mesma gramática de peça pequena da faixa do passo que
     ela substitui — escadinha de um degrau e bisel no lugar da `border`, que o
     `clip-path` comeria na diagonal dos cantos. */
  #itemChoiceTutorial {
    margin: 0;
    padding: 4px 8px;
    background: var(--cor-barra-fundo);
    box-shadow: var(--sombra-pedra-pequena-interna);
    clip-path: var(--recorte-icone-pixel);
    text-align: center;
    font-size: clamp(10px, calc(0.02 * var(--largura-arena)), 16px);
    line-height: 1.3;
    color: var(--cor-texto);
  }
  /* Tecla desenhada (`.tecla`), a peça que o jogo usa toda vez que precisa
     dizer "aperte isto": as teclas de cada passo do tutorial e o **T** da dica
     do tiro automático. Cinza = ainda não apertada, vermelho (`.done`) = já
     apertada — o `.done` só é usado pelo tutorial, que acompanha o progresso do
     passo, mas mora aqui porque o desenho é o mesmo.

     A classe não se chama `tutorial-tecla` desde que a dica passou a usá-la: a
     dica não é do tutorial, e um nome que mente sobre o dono é o que faz a
     segunda tela copiar o CSS em vez de reusá-lo. Só o **arranjo** continua
     sendo do tutorial (`.tutorial-teclas`/`-linha`, a fileira das quatro
     direções) — na dica a tecla é inline no meio da frase, como o `ESPAÇO` do
     passo de rolamento.

     Elas são peças pequenas da mesma gramática das linhas de catálogo e dos
     botões de ícone: silhueta em escadinha de **um** degrau
     (`--recorte-icone-pixel` — dois degraus de 4px comem uma tecla de 40px) e
     bisel de pedra no lugar da `border`. Nada de contorno junto, nem como
     `border` nem como anel `inset`: o `clip-path` come os dois na diagonal dos
     cantos, e o anel vermelho da tecla apertada saía como quatro riscos soltos
     com um vão em cada quina. O que separa apertada de não apertada é o
     **bisel**, que é o que a peça já tem: em relevo antes, cavado depois. */
  .tutorial-teclas {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    margin-top: 12px;
  }
  .tutorial-teclas-linha {
    display: flex;
    justify-content: center;
    gap: 6px;
  }
  /* `.glifo` entra **na mesma regra**, e não numa cópia dela: ele é o irmão de
     controle da `.tecla` (ver public/js/glifosDeControle.js), e a razão de o
     nome `tutorial-tecla` ter sido abandonado vale igual aqui — uma segunda
     lista de propriedades com os mesmos valores é o que faz as duas peças
     divergirem na primeira mudança de material. Material, medida e recorte são
     os mesmos; o que o glifo acrescenta são três linhas próprias, logo abaixo. */
  .tecla,
  .glifo {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    height: 40px;
    padding: 0 10px;
    border: none;
    border-radius: 0;
    background: var(--cor-superficie);
    box-shadow: var(--sombra-pedra-pequena-interna);
    clip-path: var(--recorte-icone-pixel);
    color: var(--cor-texto-fraco);
    font-size: 18px;
    font-weight: 700;
    /* Troca seca, como todo estado da interface: a tecla acende no quadro em
       que é apertada. */
  }
  .tecla.larga {
    min-width: 180px;
    font-size: 16px;
  }
  /* Tecla já apertada: a mesma peça com o bisel **invertido**
     (`--sombra-pedra-afundada`, o buraco cavado na pedra que o trilho da barra
     de cooldown usa), a pedra um degrau mais escura — `--cor-superficie-baixa`
     e não a da laje, senão a tecla apertada some dentro dela —, o texto no
     vermelho de
     destaque e a cabeça descendo 2px. É a leitura de tecla pressionada dita
     pelo relevo, que é como o resto da interface diz "afundado". */
  .tecla.done {
    color: var(--cor-vermelho-destaque);
    background: var(--cor-superficie-baixa);
    box-shadow: var(--sombra-pedra-afundada);
    transform: translateY(2px);
  }
  /* O glifo **não** declara caixa própria — nem `padding`, nem `font-size`,
     nem cor. Ele já teve `padding: 0 8px` e `font-size: 16px`, para caber os
     rótulos de dois caracteres (`LB`, `ZR`, `R2`), e a medida mostrou que a
     preocupação não existia: como não há reset de `box-sizing` neste arquivo, a
     largura da peça é `min-width` mais o `padding`, e o maior rótulo (`ZR` em
     18px) mede ~24px dentro de um miolo de 40. O que aquele override fazia era
     só deixar o glifo 4px mais estreito que a `.tecla` ao lado dele
     (medido: 56px contra 60px), que é exatamente a diferença que "irmão da
     `.tecla`" existe para não ter.

     A cor vem herdada (`--cor-texto-fraco` da regra compartilhada) e o desenho
     de dentro é `currentColor`. É o que mantém a família inteira dentro do
     "cinza + vermelho" — inclusive o Xbox, cujos quatro botões o aparelho pinta
     de verde/vermelho/azul/amarelo e que aqui se distinguem pela **letra**, que
     é a informação (o porquê completo está no cabeçalho de
     `public/js/glifosDeControle.js`). De quebra, herdar a cor é o que faz o
     glifo apagar junto com o cartão de um jogador caído, que apaga por cor.

     Sobra uma coisa a dizer, e é sobre o miolo desenhado: o SVG ocupa 24px dos
     40 da capinha, e **24 é escolhido por ser 12 x 2** — uma unidade de arte
     vira exatamente 2px. Ele nasceu em 20px (a mesma sobra do ícone de 18px
     dentro do nó de 38px da árvore de talentos), e a medida mostrou por que não
     servia: a 20px cada unidade vale 1,67px, então o anel de 2 unidades do
     analógico saía com 3,3px e os desenhos redondos — os dois manches e o
     direcional — viravam um borrão escuro na fileira, enquanto as letras e as
     formas retas ao lado deles continuavam nítidas. Escala inteira é a mesma
     regra do retrato do jogador no console do HUD (48 = 16 x 3): fração de
     escala põe pixels de dois tamanhos no mesmo desenho.

     O que o desenho não pode ter é junta ou ponta arredondada, e é isso que as
     duas últimas linhas garantem (mesmo motivo de `.talent-no-icone svg`). */
  .glifo svg {
    display: block;
    width: 24px;
    height: 24px;
    stroke-linejoin: miter;
    stroke-linecap: butt;
  }
  /* A **peça miúda**, e ela é uma regra de contexto e não uma classe nova.

     Duas frases do jogo pedem um botão dentro de um texto pequeno: o rodapé da
     dica do tiro automático ("também no menu da partida, em Esc", a 0.8em) e a
     segunda linha da dica do inventário ("Aperte Tab para ver mais", que chega
     a 7px em celular). A capinha de 40px daria três vezes a altura da linha em
     que ela mora — e as duas frases são rodapé, não instrução: uma peça maior
     que o texto que a explica inverteria a hierarquia da caixa.

     Ela vive aqui, no contexto de quem a mostra, em vez de virar um modificador
     que o JS escolhe: quem sabe que aquela linha é pequena é a folha de estilo,
     e `dicasDoDispositivo.js` emite **uma** peça — é o que faz a mesma frase
     servir ao rodapé e a um título sem o módulo saber onde ela caiu.

     As medidas são **as mesmas** que `body.touch-mode` já dá à peça (20px de
     caixa, SVG de 12px, logo abaixo), e isso não é coincidência: o problema é o
     mesmo — a capinha de 40px é grande demais para o texto em volta dela — e
     inventar um terceiro tamanho faria a mesma peça ter duas miniaturas. O SVG
     em 12px é o lado do `viewBox` dos glifos, ou seja **uma unidade de arte por
     pixel** — escala inteira, a mesma regra dos 24px acima e do retrato do
     jogador no HUD. `vertical-align: middle` porque a peça é `inline-flex` no
     meio de uma linha de texto, e pela linha de base ela desceria abaixo dela. */
  .dica-tiro-auto-sub .tecla,
  .dica-tiro-auto-sub .glifo,
  .item-choice-dica-tab .tecla,
  .item-choice-dica-tab .glifo {
    min-width: 20px;
    height: 20px;
    padding: 0 5px;
    font-size: 12px;
    vertical-align: middle;
  }
  .dica-tiro-auto-sub .glifo svg,
  .item-choice-dica-tab .glifo svg {
    width: 12px;
    height: 12px;
  }
  /* Dá evidência ao nome da tecla dentro do texto do passo. */
  .tecla-nome {
    color: var(--cor-vermelho-destaque);
    font-weight: 700;
  }
  /* O `clamp(18px, calc(0.046 * var(--largura-tela)), 24px)` acima foi calibrado pra desktop: como a
     arena em modo touch já é bem reduzida pra caber na viewport (ver
     gameScale.js), o mesmo tamanho lia como texto gigante colado na tela
     pequena. Em touch o banner usa um teto bem menor, e as teclas/setas
     desenhadas dentro dele encolhem junto — sem isso "arraste a área
     destacada abaixo em todas as direções" com setas de 40px não cabia numa
     tela de celular sem quebrar em várias linhas apertadas. */
  /* Em touch a arena é baixa (um celular deitado tem ~390px de altura), e a
     faixa disputa a mesma faixa vertical com a base e os cartões de jogador:
     ela encolhe o quanto dá — fonte menor, menos respiro e a fileira de teclas
     colada ao texto. */
  body.touch-mode #matchTutorialBanner {
    max-width: 78%;
    padding: 5px 12px;
    font-size: clamp(10px, 2.6vmin, 13px);
    line-height: 1.2;
    /* E **sem** pregos aqui, ao contrário do desktop: com 5px de respiro em
       cima e 12px na lateral, a cabeça de 8px a 6px do canto cai dentro da
       primeira linha de texto — foi medido, o prego da direita passava por trás
       do fim da frase. É a mesma regra de `sem-pregos` do resto do HUD (apagar
       a cor, e não uma segunda lista de camadas), aplicada ao caso em que a
       mesma peça é alta no desktop e baixa no celular. */
    --cor-prego: transparent;
    --cor-prego-luz: transparent;
    --cor-prego-sombra: transparent;
  }
  body.touch-mode .tutorial-teclas {
    gap: 2px;
    margin-top: 3px;
  }
  body.touch-mode .tutorial-teclas-linha {
    gap: 4px;
  }
  body.touch-mode .tecla,
  body.touch-mode .glifo {
    min-width: 20px;
    height: 20px;
    padding: 0 5px;
    font-size: 12px;
  }
  /* O desenho encolhe junto com a capinha: numa capinha de 20px um SVG de 24px
     não caberia. 12px é a mesma escala inteira um degrau abaixo (1 unidade de
     arte = 1px), pelo motivo da regra acima. */
  body.touch-mode .glifo svg {
    width: 12px;
    height: 12px;
  }
  body.touch-mode .tecla.larga {
    min-width: 90px;
    font-size: 11px;
  }
  /* Destaque da área de toque dos joysticks no tutorial (modo touch, ver
     matchTutorial.js): mesma marcação para o passo de movimento
     (#tutorialMoveZoneHint, metade inferior esquerda) e o passo de mira/tiro
     (#tutorialAimZoneHint, metade inferior direita — espelhada), cobrindo
     exatamente as mesmas áreas que touchControls.js usa como zona de toque de
     cada joystick. Borda amarela tracejada pulsando e setas nos quatro lados
     — sem isso a área não tem marcação nenhuma até o jogador tocar (o
     joystick nasce onde o dedo toca, não tem posição fixa pra destacar com um
     recorte permanente). */
  .tutorial-zone-hint {
    display: none;
  }
  .tutorial-zone-hint.visible {
    display: block;
    position: absolute;
    top: 50%;
    width: 50%;
    height: 50%;
    box-sizing: border-box;
    border: 2px dashed var(--cor-ouro);
    background: rgba(250, 204, 21, 0.12);
    pointer-events: none;
    z-index: 6;
    animation: tutorial-zone-pulse 1.6s ease-in-out infinite;
  }
  #tutorialMoveZoneHint.visible { left: 0; }
  #tutorialAimZoneHint.visible { right: 0; }
  /* Com canhoto ligado os dois joysticks trocam de metade (ver
     `ladoDoJoystick` em public/js/touchControls.js), e o destaque tem de
     acompanhar: ele é a única marcação da área do joystick — que nasce onde o
     dedo toca e não tem posição fixa —, então apontando a metade errada ele
     ensina o gesto errado justamente no passo que existe para ensiná-lo. A
     classe é escrita por `aplicarLadoNoBody`, nos mesmos pontos que atualizam a
     preferência. As duas âncoras precisam ser zeradas com `auto`: `left` e
     `right` coexistem, e sem isso a caixa esticaria de ponta a ponta. */
  body.canhoto #tutorialMoveZoneHint.visible { left: auto; right: 0; }
  body.canhoto #tutorialAimZoneHint.visible { right: auto; left: 0; }
  @keyframes tutorial-zone-pulse {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
  }
  .tutorial-zone-arrow {
    position: absolute;
    color: var(--cor-ouro-suave);
    font-size: clamp(20px, 5vmin, 32px);
    font-weight: 700;
    text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
  }
  .tutorial-zone-arrow.up { top: 6px; left: 50%; transform: translateX(-50%); }
  .tutorial-zone-arrow.down { bottom: 6px; left: 50%; transform: translateX(-50%); }
  .tutorial-zone-arrow.left { left: 6px; top: 50%; transform: translateY(-50%); }
  .tutorial-zone-arrow.right { right: 6px; top: 50%; transform: translateY(-50%); }
  /* `z-index` acima do `#hordeHud` (10): o HUD cobre a arena inteira
     (`inset: 0`) e é irmão destes overlays no mesmo contexto de empilhamento —
     sem isso os corações, a barra da base e o nome apareceriam por cima da tela
     de fim de partida, que é opaca. Pelo mesmo motivo as modais (perfil,
     novidades…) usam 20. */
  #gameOverOverlay, #waitingOverlay, #countdownOverlay {
    display: none;
    position: absolute;
    inset: 2px;
    z-index: 11;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: var(--cor-fundo-arena);
    animation: entrada-pop 0.22s ease;
  }
  /* `#waitingOverlay`/`#countdownOverlay` continuam com o conteúdo direto
     (pouco texto, cabe sempre); só `#gameOverOverlay` ganha o wrapper de
     rolagem abaixo — é o único cujo conteúdo varia o bastante (relatório do
     grupo, medalha, resumo) para não caber numa arena baixa. */
  #waitingOverlay, #countdownOverlay {
    gap: clamp(8px, calc(0.02 * var(--largura-arena)), 20px);
  }
  /* A tela de fim de partida troca o cinza chapado pela parede de tijolo da
     masmorra (`dungeon_brick_wall_grey.png`, a mesma do fundo do menu), **parada**.
     Ela já rondou as duas diagonais da arena em passo inteiro (`steps`), mas o
     movimento competia com o único elemento que devia se mexer nessa tela — a
     brasa (`#gameOverEmbers`) — e puxava o olho para longe do resultado. A arte
     anterior (`endgame.png`) era vermelha e cobria a tela inteira: vitória e
     derrota chegavam com a mesma moldura de sangue e o jogador lia derrota nas
     duas. O tijolo é neutro e vem bem escurecido (`--cor-veu-endgame`, 0.88)
     justamente para que a **cor do título** seja o que diz o resultado. A cor de
     fundo continua atrás dela porque o overlay é opaco. */
  #gameOverOverlay {
    background:
      linear-gradient(var(--cor-veu-endgame), var(--cor-veu-endgame)),
      var(--cor-fundo-arena) url('../assets/backgrounds/dungeon_brick_wall_grey.png') repeat;
    animation: entrada-pop 0.22s ease;
  }
  /* Na **vitória** o overlay entra por cima da última cena da arena, e não por
     cima de um tijolo opaco: o render continua desenhando a arena (e os fogos
     que ainda estão no ar) por baixo, e o que separa o relatório dela é só um
     véu escuro translúcido (`--cor-veu-vitoria`). Sem o tijolo de propósito: a
     parede de masmorra existe para dar clima a uma tela que apagou a arena, e
     aqui a arena é o clima — a que o grupo acabou de vencer. Tijolo translúcido
     por cima dela seriam duas texturas brigando atrás do relatório. A entrada
     (`entrada-pop`) é a mesma de sempre; a classe é ligada por
     `showGameOverOverlay` só quando houve janela de vitória, e a derrota continua
     com o fundo de cima. */
  #gameOverOverlay.vitoria-sobre-arena {
    background: linear-gradient(var(--cor-veu-vitoria), var(--cor-veu-vitoria));
  }
  /* A sala de espera da Defesa da base troca o cinza chapado pelo chão do
     nível que o grupo vai jogar (a mesma textura ladrilhada da partida —
     `--textura-arena`, ver "Textura de arena como fundo de botão"), a 50% de
     brilho: o jogador reconhece o mapa antes da contagem acabar, sem que o
     painel de nível/grupo/código perca contraste. O ladrilho fica no tamanho
     nativo (320px, sem reamostrar a pixel art), que é também mais ou menos a
     escala com que ele aparece na partida (300 de 1000 unidades de mundo).
     `background-color` embaixo é a base de sempre: sem classe de textura
     (nível desconhecido) a tela continua sendo o cinza chapado de antes. */
  #waitingOverlay.horde {
    background-color: var(--cor-fundo-arena);
    background-image:
      linear-gradient(var(--cor-veu-sala-espera), var(--cor-veu-sala-espera)),
      var(--textura-arena, none);
    background-size: auto, 320px 320px;
    background-repeat: repeat;
  }
  #countdownOverlay {
    background: var(--cor-fundo-arena-veu);
  }
  #countdownOverlay h2 {
    margin: 0;
    font-size: clamp(11px, calc(0.022 * var(--largura-arena)), 18px);
    color: var(--cor-ouro-suave);
  }
  /* Sem a linha do confronto (modo defesa), o título passa a ser o texto
     principal da tela — o tamanho de "legenda acima dos nomes" ficaria
     perdido no meio do vazio. */
  #countdownOverlay.sem-confronto h2 {
    max-width: 92%;
    font-size: clamp(15px, calc(0.05 * var(--largura-arena)), 46px);
    text-align: center;
    color: var(--cor-branco);
    overflow-wrap: anywhere;
  }
  #countdownNumber {
    font-variant-numeric: tabular-nums;
    font-size: clamp(32px, calc(0.13 * var(--largura-arena)), 120px);
    font-weight: bold;
    color: var(--cor-vermelho-destaque);
    text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 0 24px var(--cor-vermelho-destaque);
  }
  /* --- Tela de escolha de item (Defesa da base) -------------------------
     Abre no fim de cada onda vencida e a partida fica **pausada** enquanto ela
     está de pé (ver public/js/escolhaDeItens.js). É a primeira tela do jogo que
     pausa de verdade — o menu do meio da partida se chama "Configurações"
     justamente porque não pausava —, e é por isso que ela cobre a arena inteira
     com um véu em vez de ser um painel de canto: o que está por baixo está
     congelado, e uma tela translúcida sobre um mundo parado lê como travamento.

     Não entra na regra de `#gameOverOverlay, #waitingOverlay, #countdownOverlay`
     acima porque aquelas três são opacas e centralizam um texto; esta é um véu
     com um painel de pedra dentro. O `z-index` é o mesmo motivo: acima do
     `#hordeHud` (10), abaixo das modais de menu (20). */
  #itemChoiceOverlay {
    position: absolute;
    inset: 2px;
    z-index: 12;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2%;
    background: var(--cor-fundo-arena-veu);
    /* `hidden` no HTML não basta num elemento com `display: flex` — o atributo
       perde da regra de classe. */
    animation: item-choice-veu 0.18s steps(3, end);
  }
  #itemChoiceOverlay[hidden] { display: none; }
  /* O véu entra em três degraus, não num fade: é a mesma regra de "nada de
     transição contínua" que vale para o resto da arte (ver public/css/CLAUDE.md). */
  @keyframes item-choice-veu {
    0% { opacity: 0; }
    100% { opacity: 1; }
  }
  /* O painel é a mesma pedra dos painéis da tela inicial e do HUD
     (`--cor-painel-pedra-veu` + `--sombra-pedra`): a tela é do jogo, não uma
     caixa de diálogo do navegador. */
  #itemChoicePainel {
    /* Respiro do halo, medido pelo maior brilho da tela (os 26px de borrão do
       hover; o da escolhida é mais largo e perde só o rabo, onde ele já é
       quase transparente). Mora aqui porque quem abre a folga é a lista e quem
       para de recortar é o painel — os dois precisam do mesmo número. */
    --folga-do-halo: 28px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: clamp(4px, calc(0.012 * var(--largura-arena)), 12px);
    /* Largo o bastante para as três cartas lado a lado. Ele já foi **estreito**,
       de propósito, enquanto a oferta era uma pilha de linhas de texto; com a
       carta a decisão passou a ser entre três desenhos, e três desenhos altos
       lado a lado é o oposto daquele layout. Quem cabe na largura é decidido em
       JS pela escala da carta (ver escalaDaCartaNaEscolha), então aqui o painel
       só não pode ser o gargalo. */
    width: min(96%, calc(0.86 * var(--largura-arena)));
    max-height: 100%;
    padding: clamp(8px, calc(0.02 * var(--largura-arena)), 22px);
    background: var(--cor-painel-pedra-veu);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
    /* A lista rola por dentro em arena baixa; o painel inteiro nunca rola, para
       o título e o cronômetro ficarem sempre à vista.

       `clip` e não `hidden` por causa do halo: o brilho do hover e o da carta
       escolhida são `box-shadow` **borrado**, que vaza para fora da lista, e
       qualquer recorte no caminho cortava esse vazamento num retângulo reto em
       volta da carta — o oposto de luz. `overflow-clip-margin` recorta como
       antes, só que uma folga depois da borda, e é essa folga que a lista
       (`#itemChoiceOpcoes`) usa como respiro do halo. */
    overflow: clip;
    overflow-clip-margin: var(--folga-do-halo);
  }
  /* Entrada em quatro degraus de escala, do jeito que o retrato de conquista
     surge (`conquista-surge`): a modal se monta, não desliza. `steps(4)` sobre
     uma escala que passa de 1 e volta é o "pop" da interface sem nenhum quadro
     em posição fracionária — o painel é centralizado por flex, então escalar não
     desalinha a grade de nada que esteja dentro dele. */
  #itemChoicePainel.abrindo {
    animation: item-choice-surge 0.28s steps(4, end);
  }
  @keyframes item-choice-surge {
    0% { transform: scale(0.4); }
    60% { transform: scale(1.08); }
    100% { transform: scale(1); }
  }
  #itemChoiceTitulo {
    margin: 0;
    text-align: center;
    font-size: clamp(13px, calc(0.032 * var(--largura-arena)), 30px);
    color: var(--cor-ouro-suave);
  }
  #itemChoiceSubtitulo {
    margin: 0;
    text-align: center;
    font-size: clamp(9px, calc(0.017 * var(--largura-arena)), 15px);
    color: var(--cor-texto-suave);
  }
  #itemChoiceOpcoes {
    display: flex;
    /* Lado a lado, e **sempre no máximo três** (OPCOES_POR_ESCOLHA em
       shared/horde.js): a decisão é entre três cartas que se comparam de uma
       olhada, e uma quarta já obriga o olho a varrer em vez de comparar. Sem
       `wrap` de propósito — uma carta que descesse para a segunda linha seria a
       opção que o jogador não vê. */
    flex-direction: row;
    align-items: center;
    justify-content: center;
    flex-wrap: nowrap;
    /* Folga larga entre as três: sem o halo (ver `.item-choice-opcao .carta`
       abaixo) o que separa uma carta da outra é o espaço vazio, e com 12px elas
       encostavam. Quem mede essa folga na conta da escala é o JS, lendo daqui
       (`folgaEntreCartas` em escolhaDeItens.js) — não há um segundo 28 escrito
       lá. */
    gap: 28px;
    /* Padding para o halo caber dentro do recorte da rolagem, e a margem
       negativa do mesmo tamanho para ele não custar nada no layout: a lista
       continua esticada na largura do painel (`align-items: stretch`), então o
       espaço das cartas é exatamente o de antes — o que cresceu é só a área
       pintável. Sem isso o `overflow` daqui corta o brilho rente à carta. */
    margin: calc(-1 * var(--folga-do-halo));
    padding: var(--folga-do-halo);
    list-style: none;
    overflow-y: auto;
    /* Explícito, e não herdado: `overflow-y: auto` faz o `overflow-x` computar
       para `auto` também, então qualquer coisa que passe um pixel para fora do
       cartão (uma sombra deslocada, um `transform`) faz nascer uma barra de
       rolagem horizontal — e ela **consome altura**, o que muda o tamanho do
       painel inteiro. Foi exatamente o que o `translate` de hover fazia. */
    overflow-x: hidden;
  }
  /* Uma carta por opção, lado a lado. A carta em si é `.carta` (ver o bloco
     "Carta de item"); o que este bloco faz é a **chegada** dela.

     A carta **sobe de fora e vira ao chegar na altura dela**: `item-choice-sobe`
     traz o `<li>` de baixo em degraus, e no fim dessa subida `item-choice-vira`
     encolhe a largura até o fio e devolve — é a virada. As duas rodam na mesma
     carta porque estão em elementos diferentes (`<li>` sobe, `.carta` vira): uma
     propriedade `transform` só não empilha as duas.

     Ela chega **de costas** (a classe `verso` de cartaDeItem.js, mais o dorso do
     `::after` daqui) e a face aparece no meio da virada, que é o instante em que
     uma carta de verdade passa do verso para a frente.

     `--atraso` sai da **ordem de revelação**, não do índice (ver
     ordemDeRevelacao em escolhaDeItens.js): o quebra-jogo é sempre o último. */
  .item-choice-opcao {
    position: relative;
    flex: none;
    animation: item-choice-sobe 0.34s steps(6, jump-none) backwards;
    animation-delay: var(--atraso, 0ms);
  }
  /* `jump-none` pelo mesmo motivo da entrada das cartas da Loja: com `steps(6)`
     (jump-end) a carta pararia a um sexto do caminho e ficaria mais baixa que as
     irmãs para sempre. */
  @keyframes item-choice-sobe {
    from { transform: translateY(60%); }
    to { transform: translateY(0); }
  }
  /* A virada, na própria carta e **depois** da subida (o atraso soma a duração
     dela): quatro degraus de `scaleX` até o fio e quatro de volta. É o mesmo
     movimento da meia-volta da Loja (`QUADROS` em cartaDeItem.js), aqui escrito
     em CSS porque não há nada a decidir no meio — a face já está montada. */
  .item-choice-opcao .carta {
    animation: item-choice-vira 0.32s steps(4, end) backwards;
    animation-delay: calc(var(--atraso, 0ms) + 280ms);
  }
  @keyframes item-choice-vira {
    0% { transform: scaleX(1); }
    50% { transform: scaleX(0.06); }
    100% { transform: scaleX(1); }
  }
  /* O **dorso**: a camada hachurada que cobre a face enquanto a carta está de
     costas. Ela não encolhe — ela **troca** pela face na metade da virada, num
     degrau só, que é o instante em que a carta passa do verso para a frente.
     `pointer-events: none` porque ela cobre o botão enquanto existe. */
  .item-choice-opcao::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background-color: var(--cor-superficie-forte);
    background-image: repeating-linear-gradient(
      45deg,
      var(--cor-pedra-borda) 0 4px,
      transparent 4px 8px
    );
    border: 1px solid var(--cor-pedra-borda);
    animation: item-choice-dorso 0.32s steps(1, end) both;
    animation-delay: calc(var(--atraso, 0ms) + 280ms);
  }
  @keyframes item-choice-dorso {
    0% { opacity: 1; }
    50% { opacity: 0; }
    100% { opacity: 0; }
  }
  /* Clarão da carta que **fecha** a revelação sendo quebra-jogo: dois degraus de
     brilho na cor da raridade, no instante em que a face aparece — metade da
     virada, não o começo dela, senão ele pisca por trás do dorso. Piscada seca,
     em `steps`, como todo o resto. */
  /* Ele mora numa camada própria (`::before` do `<li>`), e não na carta nem no
     botão: a carta já tem a **virada** e o botão já tem o pulso da janela morta,
     e `animation` não empilha entre regras — a última a casar apaga a outra. Foi
     exatamente assim que a virada sumiu quando o pulso da trava morava na carta. */
  .item-choice-opcao.destaque::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    background: var(--cor-raridade);
    /* Opaco **só** durante a piscada: a camada cobre a carta inteira, então o
       estado parado dela tem que ser invisível. Sem este `opacity: 0` de base,
       qualquer coisa que tire a animação deixa um bloco cheio na cor da
       raridade por cima da carta — foi o que aconteceu com
       `#itemChoicePainel.revelada .item-choice-opcao::before { animation: none }`
       (a regra que conclui a revelação ao clicar): escolher uma carta pintava o
       quebra-jogo de amarelo sólido e o jogador não lia mais o que ele fazia.
       A camada do dorso (`::after`) já se protege com `display: none` lá. */
    opacity: 0;
    animation: item-choice-clarao 0.18s steps(1, end) 2 both;
    animation-delay: calc(var(--atraso, 0ms) + 440ms);
  }
  @keyframes item-choice-clarao {
    0% { opacity: 0; }
    50% { opacity: 0.45; }
    100% { opacity: 0; }
  }
  /* Tremor curto quando a carta que fecha a revelação é quebra-jogo, agendado
     pelo CSS a partir de `--atraso-destaque` (escrito pelo JS): um `setTimeout`
     sobreviveria a fechar a modal no meio da revelação. Deslocamentos inteiros,
     em degraus — nunca uma senoide.

     Ele fica na **lista de opções**, e não em `#itemChoicePainel`: o painel já
     tem `animation` em duas classes suas (`abrindo` e `saindo`), e uma terceira
     regra de mesma especificidade sobre a mesma propriedade mataria a entrada da
     modal — a mais abaixo no arquivo ganha. */
  #itemChoiceOpcoes.com-destaque {
    animation: item-choice-tremor 0.24s steps(1, end) 1 both;
    animation-delay: var(--atraso-destaque, 0ms);
  }
  @keyframes item-choice-tremor {
    0% { transform: translate(3px, 0); }
    25% { transform: translate(-3px, 2px); }
    50% { transform: translate(2px, -2px); }
    75% { transform: translate(-2px, 0); }
    100% { transform: translate(0, 0); }
  }
  /* Clique durante a revelação a **conclui** (classe posta por `escolher`): as
     cartas passam ao estado final no mesmo quadro, em vez de o dorso engolir o
     toque.

     A regra desliga só as animações **da revelação** — a subida do `<li>`, a
     virada da carta, o dorso e o tremor da lista. Ela **não** pode alcançar a
     carta **escolhida** (daí o `:not(.escolhido)`): o comprovante da escolha
     (`item-choice-confirma`, a carta escolhida piscando duas vezes) mora em
     `.item-choice-opcao.escolhido .carta`, e com um seletor de id na frente esta
     regra ganhava dele por especificidade e apagava o comprovante — o clique
     fechava a modal sem resposta nenhuma, que é justamente o bug que aquela
     piscada existe para não deixar voltar. Ele já voltou uma vez, quando a
     piscada saiu do botão para a carta e caiu debaixo desta regra. */
  #itemChoicePainel.revelada .item-choice-opcao,
  #itemChoicePainel.revelada .item-choice-opcao:not(.escolhido) .carta,
  #itemChoicePainel.revelada .item-choice-opcao::before,
  #itemChoicePainel.revelada #itemChoiceOpcoes.com-destaque {
    animation: none;
  }
  #itemChoicePainel.revelada .item-choice-opcao::after {
    animation: none;
    display: none;
  }
  /* O botão é só a área de clique em volta da carta: o desenho é todo de
     `.carta` (ver o bloco "Carta de item"), e a moldura, o véu de raridade e o
     texto que moravam aqui saíram com a linha de texto que este cartão era. O
     que sobrou é o alvo de clique e o foco de teclado. */
  .item-choice-botao {
    display: block;
    padding: 0;
    background: none;
    border: none;
    box-shadow: none;
    cursor: pointer;
    line-height: 0;
  }
  /* Sem halo nesta tela. O brilho da raridade (`.carta.frente`) é o que
     distingue uma carta sozinha na mesa da Loja; com **três** lado a lado no
     meio da partida ele vira um borrão contínuo que encosta uma na outra e
     apaga a borda da moldura, que é justamente o desenho que diz a raridade.
     Aqui quem separa as cartas é a folga da lista. O halo continua valendo para
     o hover, para a escolhida e para a Loja. */
  .item-choice-opcao .carta.frente {
    box-shadow: none;
  }
  /* A carta sob o cursor acende: um halo branco por cima do halo da raridade que
     ela já tem. Nada de `transform` nem de sombra maior — hover que mexe em
     geometria faz a carta passar para fora da lista, e a lista rola: nasce uma
     barra horizontal, ela consome altura e o painel inteiro muda de tamanho a
     cada vez que o cursor cruza uma carta. Cor não tem esse efeito. */
  .item-choice-botao:hover:not(:disabled) .carta,
  .item-choice-botao:focus-visible .carta {
    box-shadow: 0 0 0 3px var(--cor-branco), 0 0 26px rgba(255, 255, 255, 0.35);
  }
  .item-choice-botao:focus-visible { outline: none; }
  /* **O mesmo halo para quem chegou de controle.** O `:focus-visible` acima é
     a heurística do navegador sobre *teclado*, e o foco que `focarElemento`
     aplica ao abrir esta tela chega sem evento de tecla nenhum (ver
     public/js/entradaDeFoco.js e "Realce de foco" no fim deste arquivo): ele
     **nunca** casa no controle. Sem esta regra o mouse ganhava o destaque
     forte e o controle ficava com um anel que quase não existe: `.carta` é
     `position: relative`, e o Chromium pinta descendente posicionado **por
     cima** do `outline` do elemento — medido a 1440x900, só **17%** da faixa
     de 4..6px saía branca, e só onde a arte da carta é transparente. É o mesmo
     modo de falha da textura do quadrado de nível (ver `--cor-realce-foco-fundo`
     no `:root`), e nenhum ajuste de cor traria um anel coberto de volta.
     Aqui a saída é a mais simples do arquivo: `.carta` é peça focável **sem**
     `clip-path`, então um anel por **fora** dela sobrevive (13,6:1 contra o
     véu da tela) e ainda por cima não encosta na arte. Por isso o `outline`
     sai (abaixo) — dois realces na mesma carta seriam duas linhas brancas
     concêntricas, e a de dentro é a que não se vê.
     O `:not(.escolhido)` não é zelo: o comprovante da escolha
     (`.item-choice-opcao.escolhido .carta`, a piscada na cor da raridade) tem
     especificidade menor que este seletor e sairia apagado justamente na carta
     que o jogador acabou de escolher — que é onde o foco está. */
  body.foco-sem-ponteiro .item-choice-opcao:not(.escolhido) .item-choice-botao:focus .carta {
    box-shadow: 0 0 0 3px var(--cor-realce-foco), 0 0 26px rgba(255, 255, 255, 0.35);
  }
  body.foco-sem-ponteiro .item-choice-botao:focus { outline: none; }
  /* Depois de escolher: a escolhida fica acesa e as outras apagam, em vez de a
     tela simplesmente fechar. Numa sala de 2 a 4 a pausa continua até o último
     decidir, e é esse estado que mostra o que ficou decidido enquanto se espera. */
  .item-choice-opcao.escolhido .carta {
    box-shadow: 0 0 0 4px var(--cor-raridade), 0 0 48px var(--cor-raridade);
    /* Duas piscadas na cor cheia da raridade: é o comprovante de **qual** das
       três foi escolhida, e ele precisa acontecer antes de a tela sair, senão a
       escolha mais importante da partida termina com a modal desaparecendo sem
       resposta. `steps(1)` em cada metade — degrau seco, como o resto da
       interface. A soma (2 × 0,36s + a saída) é o que `CONFIRMA_MS` em
       escolhaDeItens.js reserva: mexer num sem o outro corta a piscada no meio
       ou deixa o painel parado esperando. */
    animation: item-choice-confirma 0.36s steps(1, end) 2;
  }
  @keyframes item-choice-confirma {
    0% { filter: none; }
    50% { filter: brightness(1.8); }
    100% { filter: none; }
  }
  /* Janela morta dos primeiros segundos (ver TRAVA_INICIAL_MS em
     escolhaDeItens.js): as três cartas estão na tela, mas ainda não valem
     clique. O estado precisa se ler **antes** do dedo descer, então não é só o
     `disabled` do botão: a carta perde peso e o halo dela pisca entre a pedra e
     a raridade, em degrau seco. Duas cores, nunca cor e nada — mesma regra do
     aviso de onda e da barra de vida baixa. */
  .item-choice-opcao.travado .carta {
    /* Só a opacidade fica na carta: é propriedade estática e não disputa nada
       com a virada. O **pulso** vai para o botão, porque `animation` não empilha
       entre regras — com ele aqui, a regra da trava apagava a virada e a carta
       nunca virava (a janela morta dura 3s e cobre a revelação inteira). */
    opacity: 0.55;
  }
  .item-choice-opcao.travado .item-choice-botao {
    cursor: not-allowed;
    animation: item-choice-travado 0.6s steps(1, end) infinite alternate;
  }
  @keyframes item-choice-travado {
    0% { box-shadow: 0 0 0 3px var(--cor-pedra-borda); }
    100% { box-shadow: 0 0 0 3px var(--cor-raridade); }
  }
  /* A recusada perde a cor **e** o tamanho, como na mesa da Loja: é a diferença
     entre as duas que diz qual é a sua. Encolhe a partir da base, para as três
     continuarem apoiadas na mesma linha. */
  .item-choice-opcao.descartado .carta {
    transform-origin: center bottom;
    transform: scale(0.88);
    filter: grayscale(1) brightness(0.55);
    box-shadow: none;
  }
  .item-choice-botao:disabled { cursor: default; }
  /* Indicativo de onde o item escolhido vai parar, só na **primeira** escolha
     da partida (ver dicaDoInventario em public/js/escolhaDeItens.js). A coluna
     do inventário (`#hordeItens`) nasce vazia: na primeira onda vencida não há
     um único ícone no canto, então o item escolhido aparecia num lugar da tela
     que o jogador nunca tinha visto ocupado. A moldura marca esse canto
     enquanto a tela de escolha está de pé.

     Ela entra **depois** de a tela de escolha sair (por isso o elemento é irmão
     do `#itemChoiceOverlay`, e não filho: ele volta a `display: none` ao fim da
     pausa e levaria a dica junto), e sai sozinha em alguns segundos.

     `fixed` e posicionada pelo JS: a coluna fica no canto do `#hordeHud`, que é
     outro elemento — copiar aqui o `left/top` dele daria duas medidas para
     manter em sincronia (e três, com a variante de touch). Quem mede é o JS, no
     `#hordeItens` de verdade. `z-index` acima do `#hordeHud` (10), como o resto
     do que se sobrepõe à arena. */
  #itemChoiceDica {
    position: fixed;
    z-index: 13;
    display: flex;
    align-items: center;
    gap: 6px;
    pointer-events: none;
    /* Pisca **entre duas cores**, e não entre a cor e o quase-nada: a dica fica
       sobre o chão ladrilhado da arena, com a partida correndo, e uma piscada
       por opacidade deixava metade do tempo com a moldura e o texto ilegíveis —
       a mesma razão de o aviso de onda e a barra de vida baixa alternarem duas
       cores (ver public/js/CLAUDE.md). Quem alterna é a moldura, no elemento de
       dentro; o texto fica opaco o tempo todo. */
  }
  #itemChoiceDica[hidden] { display: none; }
  /* Do tamanho de uma caixa de item do HUD (`.horde-item`), tracejada como os
     destaques do tutorial: é um lugar sendo apontado, e uma moldura cheia
     esconderia o ícone que acabou de entrar ali. */
  .item-choice-dica-alvo {
    box-sizing: border-box;
    /* Medida de reserva: quem manda são o `width`/`height` que o JS escreve
       aqui, medidos na caixa de item de verdade (ver posicionarDica em
       public/js/escolhaDeItens.js). Um tamanho só de CSS não batia com a caixa
       — o ícone do item aparecia meio fora da moldura que o aponta. */
    width: clamp(24px, calc(0.05 * var(--largura-arena)), 42px);
    height: clamp(16px, calc(0.032 * var(--largura-arena)), 26px);
    flex: none;
    border: 2px dashed var(--cor-ouro);
    animation: item-choice-dica-pisca 0.6s steps(1, end) infinite alternate;
  }
  /* O texto tem a pedra do HUD por baixo (mesma peça pequena do cronômetro e do
     cartão de jogador): sobre o chão texturizado, letra de 13px sem fundo some
     no ladrilho — e é justamente esta a frase que precisa ser lida. */
  .item-choice-dica-texto {
    /* Duas linhas na mesma pedra: onde os itens ficam e como abrir a leitura
       completa deles. Uma segunda placa ao lado disputaria com a moldura
       tracejada que aponta o canto. */
    display: flex;
    flex-direction: column;
    padding: 2px 5px;
    font-size: clamp(9px, calc(0.019 * var(--largura-arena)), 14px);
    color: var(--cor-ouro);
    white-space: nowrap;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
  }
  /* A linha do Tab é subordinada à de cima: menor. Dourada e do mesmo tamanho,
     as duas frases pareceriam dois avisos — mas em texto suave ela sumia no
     véu de pedra a 11px, então fica **branca**: é o contraste que a torna
     legível sem competir com o ouro da linha de cima.

     Recuada a largura da seta: a linha de cima começa com um `←` que aponta o
     canto, e a de baixo, colada na borda esquerda da pedra, ficava um degrau à
     frente da frase que ela continua. Com o recuo, as duas frases começam na
     mesma coluna e a seta fica sozinha à esquerda, como um marcador. */
  .item-choice-dica-tab {
    color: var(--cor-branco);
    font-size: clamp(7px, calc(0.015 * var(--largura-arena)), 11px);
    padding-left: 1.55em;
  }
  /* Em modo touch não existe Tab: o painel de builds é do teclado (ver
     public/js/hordeBuilds.js), e prometer uma tecla que o aparelho não tem é
     pior que não dizer nada. */
  body.touch-mode .item-choice-dica-tab {
    display: none;
  }
  @keyframes item-choice-dica-pisca {
    0% { border-color: var(--cor-ouro); }
    100% { border-color: var(--cor-branco); }
  }
  /* A saída, depois das piscadas: o painel encolhe em degraus e sai, em vez de
     ser removido entre dois quadros. É o fecho da mesma cerimônia da entrada
     (`item-choice-surge`), na ordem inversa. */
  #itemChoicePainel.saindo {
    animation: item-choice-sai 0.24s steps(3, end) forwards;
  }
  @keyframes item-choice-sai {
    0% { transform: scale(1); opacity: 1; }
    100% { transform: scale(0.5); opacity: 0; }
  }
  /* Cronômetro da pausa. Trilho afundado como as barras do HUD
     (`--sombra-pedra-afundada`), e o preenchimento **anda em blocos**: o JS
     escreve a largura em passos de 1/20 (ver TEMPO_BLOCOS), então não há
     transição nenhuma aqui de propósito. */
  #itemChoiceTempo {
    height: 8px;
    padding: 2px;
    background: var(--cor-fundo-arena);
    box-shadow: var(--sombra-pedra-afundada);
  }
  #itemChoiceTempoFill {
    height: 100%;
    width: 100%;
    background: var(--cor-ouro-suave);
  }
  /* Último quinto: vermelho, porque é quando o automático (a primeira opção)
     passa a ser o resultado provável. */
  #itemChoiceTempoFill.urgente {
    background: var(--cor-vermelho-destaque);
    animation: item-choice-urgente 0.5s steps(1, end) infinite alternate;
  }
  @keyframes item-choice-urgente {
    0% { background: var(--cor-vermelho-destaque); }
    100% { background: var(--cor-ouro-suave); }
  }
  #itemChoiceRodape {
    margin: 0;
    text-align: center;
    font-variant-numeric: tabular-nums;
    font-size: clamp(8px, calc(0.015 * var(--largura-arena)), 13px);
    color: var(--cor-texto-fraco);
  }
  /* Arena curta (janela estreita ou tela deitada e baixa): o painel encolhe e a
     folga entre as cartas cai, para as **três** caberem lado a lado sem rolagem.
     A opção que o jogador não vê é uma opção que não existe, e uma carta atrás de
     uma barra de rolagem é exatamente isso.

     Quem encolhe a **carta** não está aqui, e sim no JS
     (`escalaDaCartaNaEscolha`, escolhaDeItens.js): a escala é um passo inteiro
     medido do painel já montado, porque a moldura é pixel art e uma escala
     fracionária a reamostraria. Estas regras só dão o espaço que ela mede. */
  body.tela-estreita #itemChoicePainel,
  body.tela-baixa-deitada #itemChoicePainel {
    width: min(98%, calc(0.96 * var(--largura-arena)));
    padding: 6px;
    gap: 3px;
  }
  body.tela-estreita #itemChoiceOpcoes,
  body.tela-baixa-deitada #itemChoiceOpcoes {
    gap: 6px;
  }
  /* Tela muito estreita (janela de desktop em pé — no touch a tela é girada e
     esta faixa não vale): a arena vira uma faixa baixa no meio de uma janela
     alta, e o painel da escolha herdava essa faixa. Com três cartas que somam
     396px na menor escala, ela não cabia: a lista rolava e a terceira opção
     saía da vista, que é a coisa que esta tela não pode fazer.

     Aqui a modal passa a cobrir a **janela** e não a arena — é onde está o
     espaço, e ela já é uma tela que pausa tudo, não um painel de canto. O
     `planoDasCartas` mede esse espaço novo e, se ainda assim não couber uma
     fileira, quebra em duas linhas (`.duas-linhas`). */
  body.tela-muito-estreita #itemChoiceOverlay {
    position: fixed;
    inset: 0;
  }
  body.tela-muito-estreita #itemChoicePainel {
    width: 100%;
    padding: 4px;
  }
  /* A quebra em 2+1: a largura vem do JS (as colunas que couberam), e é ela que
     diz onde o `wrap` quebra. `align-content` é o que mantém as duas linhas
     juntas no meio em vez de espalhadas pela altura da lista. */
  #itemChoiceOpcoes.duas-linhas {
    flex-wrap: wrap;
    align-content: center;
    /* Centraliza pelo `align-self`, e não por `margin-inline: auto`: a margem
       automática apagaria a margem negativa do respiro do halo e a lista
       andaria meia folga para o lado. */
    align-self: center;
  }
  body.tela-estreita #itemChoiceTitulo,
  body.tela-baixa-deitada #itemChoiceTitulo {
    font-size: clamp(11px, calc(0.026 * var(--largura-arena)), 22px);
  }

  #gameOverOverlay h2, #waitingOverlay h2 {
    margin: 0;
    font-size: clamp(15px, calc(0.045 * var(--largura-arena)), 36px);
    text-align: center;
    overflow-wrap: anywhere;
  }
  #waitingOverlay h2 {
    color: var(--cor-vermelho-destaque);
    text-shadow: 0 0 16px var(--cor-vermelho-destaque);
    animation: waiting-pulse-texto 1.6s ease-in-out infinite;
  }
  /* 5 bolinhas orbitando um pentagrama no centro, no lugar do spinner verde
     antigo — ecoa o brilho vermelho do número da contagem regressiva e o
     clima oculto do jogo, em vez de um loader genérico. As bolinhas giram
     no sentido contrário ao do pentagrama, senão os dois girando junto
     pareceriam um objeto só em vez de duas camadas independentes. */
  /* Com a sala na tela o painel já diz tudo (nível, grupo, código, tempo
     restante) e a espera tem fim anunciado: o pentagrama girando só empurrava o
     painel para baixo e dava a impressão de que algo ainda estava carregando.
     Ele fica enquanto a sala não chegou — ver showWaitingOverlay({ carregando:
     true }) em overlays.js. */
  #waitingOverlay.horde .loader {
    display: none;
  }
  #waitingOverlay .loader {
    --size: 1px;

    width: calc(48 * var(--size));
    height: calc(48 * var(--size));
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
  }
  #waitingOverlay .loader-orbit {
    position: absolute;
    inset: 0;
    animation: rotation 3s linear infinite;
  }
  #waitingOverlay .loader-dot {
    position: absolute;
    top: 50%;
    left: 50%;
    width: calc(6 * var(--size));
    height: calc(6 * var(--size));
    margin: calc(-3 * var(--size));
    border-radius: 50%;
    background: var(--cor-vermelho-destaque);
    box-shadow: 0 0 4px var(--cor-vermelho-destaque);
  }
  #waitingOverlay .loader-dot:nth-child(1) { transform: rotate(0deg) translateX(calc(21 * var(--size))); }
  #waitingOverlay .loader-dot:nth-child(2) { transform: rotate(72deg) translateX(calc(21 * var(--size))); }
  #waitingOverlay .loader-dot:nth-child(3) { transform: rotate(144deg) translateX(calc(21 * var(--size))); }
  #waitingOverlay .loader-dot:nth-child(4) { transform: rotate(216deg) translateX(calc(21 * var(--size))); }
  #waitingOverlay .loader-dot:nth-child(5) { transform: rotate(288deg) translateX(calc(21 * var(--size))); }
  #waitingOverlay .loader-pentagrama {
    width: calc(30 * var(--size));
    height: calc(30 * var(--size));
    fill: none;
    stroke: var(--cor-vermelho-destaque);
    stroke-width: 4;
    stroke-linejoin: round;
    filter: drop-shadow(0 0 4px var(--cor-vermelho-destaque));
    animation: rotationBack 6s linear infinite;
  }
  @keyframes rotation {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
  @keyframes rotationBack {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(-360deg); }
  }
  @keyframes waiting-pulse-texto {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
  }
  /* O resultado é o texto principal da tela — e é ele, não o fundo, que
     diferencia vitória de derrota (ver `#gameOverOverlay`). Por isso quebra
     duas convenções de propósito:

     1. Usa 'Metal Mania', que o resto do projeto reserva ao nome do jogo
        (#menuTitle) porque vira ilegível em texto corrido e número pequeno.
        Aqui não é nem um nem outro: são duas ou três palavras num tamanho
        grande, o mesmo uso que a fonte tem no logo.
     2. É bem maior que o h2 genérico dos overlays (0.065 da arena contra
        0.045) e ganha contorno preto sólido + brilho na cor do resultado,
        para se destacar da parede de tijolo atrás. O teto (56px) não sobe
        mais porque as frases da Defesa da base são inteiras ("As dunas
        engoliram a base") e não só "Você ganhou": num tamanho de logo elas
        virariam quatro linhas.

     O contorno é sombra sólida deslocada nos quatro cantos (nada de `blur`
     como borda), na regra da pixel art da interface. */
  #gameOverOverlay h2#gameOverMessage {
    font-family: 'Metal Mania', cursive;
    font-size: clamp(22px, calc(0.065 * var(--largura-arena)), 56px);
    line-height: 1.05;
    letter-spacing: 1px;
  }
  #gameOverOverlay.win h2 {
    color: var(--cor-sucesso);
    text-shadow:
      2px 0 0 #000, -2px 0 0 #000, 0 2px 0 #000, 0 -2px 0 #000,
      0 0 18px var(--cor-sucesso);
  }
  #gameOverOverlay.lose h2 {
    color: var(--cor-erro);
    text-shadow:
      2px 0 0 #000, -2px 0 0 #000, 0 2px 0 #000, 0 -2px 0 #000,
      0 0 18px var(--cor-erro);
  }
  /* Empate e "partida encerrada" não põem classe nenhuma no overlay: o
     contorno vem daqui para o título não ficar sem leitura sobre o tijolo. */
  #gameOverOverlay:not(.win):not(.lose) h2 {
    color: var(--cor-branco);
    text-shadow:
      2px 0 0 #000, -2px 0 0 #000, 0 2px 0 #000, 0 -2px 0 #000,
      0 0 18px rgba(0, 0, 0, 0.9);
  }
  /* ===== Fim de partida: brasas, resumo e medalha ===== */

  /* Brasa e cinza têm canvas próprio porque o overlay é opaco: qualquer coisa
     desenhada no canvas do jogo ficaria escondida atrás dele (ver
     public/js/embers.js). Fica no fundo do overlay, com o conteúdo por
     cima — a partícula passa atrás do texto, não na frente dele. */
  #gameOverEmbers {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    image-rendering: pixelated;
    /* Sobrescreve a regra genérica `canvas { background:
       var(--cor-superficie-elevada) }`: como este canvas cobre o overlay
       inteiro (`inset: 0`), aquele cinza opaco apagava o fundo do
       #gameOverOverlay — era ele o cinza chapado da tela de fim de partida,
       no lugar da parede de tijolo do fim de partida. Mesmo motivo do `background:
       transparent` do canvas de fundo do menu. */
    background: transparent;
  }
  /* O overlay em si fica com o tamanho fixo de sempre (`inset: 2px`), pra
     brasa e cinza (`#gameOverEmbers`, que cobre `inset: 0` dele) continuarem
     cobrindo a arena inteira mesmo se o conteúdo rolar.

     **Três faixas, e não uma coluna rolante**: resultado em cima, placar no
     meio, saídas embaixo. O `overflow-y` estava aqui, no conjunto todo — e
     como o que sai da vista numa coluna é o fim dela, faltando altura quem
     sumia eram os botões: a tela cortava exatamente onde o jogador precisa
     clicar. Agora quem rola é só `.gameover-placar`, e as duas faixas de fora
     ficam onde estão em qualquer altura de arena.

     `minmax(0, 1fr)` na linha do meio, e não `1fr`: o mínimo automático de uma
     faixa de grid é o tamanho do conteúdo dela, então com `1fr` ela nunca
     encolhe abaixo do que carrega e a rolagem simplesmente não acontece — o
     conteúdo empurra as ações para fora do overlay de novo.

     `max-width`/`max-height` existem pelo mesmo motivo de sempre: nada aqui
     deve depender de a arena ser larga (ou alta) o bastante. */
  .gameover-conteudo {
    display: grid;
    /* A quarta linha é o título "E agora?" do fim do corredor, que só existe
       nesse passo: escondido, ele não ocupa célula e a faixa das ações cai na
       terceira. */
    grid-template-rows: auto minmax(0, 1fr) auto auto;
    justify-items: center;
    row-gap: clamp(8px, calc(0.02 * var(--largura-arena)), 20px);
    /* Largura da arena, não do conteúdo: o overlay é um flex column com
       `align-items: center`, então sem isto o wrapper encolhe até a peça mais
       larga que carrega — e o relatório lá dentro tentava medir uma
       porcentagem contra uma largura que ele mesmo estava definindo. Quem é
       centrado continua centrado pelo `justify-items` acima. */
    width: 100%;
    max-width: 100%;
    max-height: 100%;
    overflow: hidden;
    padding: 4px;
    box-sizing: border-box;
    position: relative;
    z-index: 1;
  }
  /* A faixa do meio: relatório (ou resumo em lista), medalha e avisos. A soma
     deles varia com o resultado — medalha desbloqueada soma ~150px fixos que
     nenhum ajuste de proporção absorve (ver public/js/CLAUDE.md) — e uma arena
     baixa pode não ter altura para tudo de uma vez. É a única coisa da tela
     que pode sair da vista.

     `min-height: 0` pelo mesmo motivo do `minmax(0, …)` acima: sem ele o item
     de grid não encolhe abaixo do conteúdo e o `overflow-y` fica decorativo. */
  .gameover-placar {
    min-height: 0;
    width: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    /* A faixa rola, mas a **barra** não aparece (a mesma receita que a fileira de abas do menu usava):
       durante a revelação cada peça entra com um `scale(1.3)` de um quadro
       (`gameover-revelar-pop`), e o que sobra dele empurra o `scrollHeight`
       por alguns quadros — medido: 286 contra 282 de altura útil. O resultado
       era uma barra de rolagem piscando no meio do placar, num overlay que na
       maioria das telas nem precisa rolar. Rolar continua funcionando (roda do
       mouse e toque) para o caso que motivou o `overflow-y`: medalha
       desbloqueada em cima do relatório numa arena baixa. */
    scrollbar-width: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: min(10px, calc(0.016 * var(--largura-arena)));
  }
  .gameover-placar::-webkit-scrollbar {
    display: none;
  }
  /* A faixa de baixo, numa fileira só e em **duas formas** que nunca aparecem
     juntas (ver o corredor em gameOver.js): a do passo (a ação do passo,
     Continuar/Depois e Pular tudo) e a das saídas (Jogar de novo, Mapa e
     Acampamento). Três botões cabem numa fileira em toda faixa de tela; o
     `wrap` fica só como rede para a arena mais estreita, onde quebrar é melhor
     que cortar um botão. */
  .gameover-acoes {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: min(16px, calc(0.022 * var(--largura-arena)));
  }
  .gameover-placar[hidden],
  .gameover-acoes[hidden],
  .gameover-acoes-titulo[hidden],
  .gameover-passo[hidden],
  #gameOverOverlay .gameover-acoes button[hidden] {
    display: none;
  }
  /* O título curto do E agora?, logo acima das saídas. É legenda, não
     manchete: a manchete da tela continua sendo o resultado. */
  .gameover-acoes-titulo {
    margin: 0;
    font-size: 14px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--cor-texto-fraco);
  }
  /* ===== Passos do corredor depois da partida =====
     Talentos, Carta e Metas: um cartão de pedra no lugar do placar, na faixa
     do meio. Mesmo material dos painéis do menu (fundo de véu de pedra e a
     sombra sólida deslocada), canto reto, e a entrada em degraus
     (`steps()`) a cada troca de passo — nunca um deslizar contínuo. */
  .gameover-passo {
    align-self: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    width: min(520px, 92%);
    max-height: 100%;
    overflow-y: auto;
    scrollbar-width: none;
    box-sizing: border-box;
    padding: clamp(12px, calc(0.03 * var(--largura-arena)), 24px);
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra);
    text-align: center;
  }
  .gameover-passo::-webkit-scrollbar {
    display: none;
  }
  .gameover-passo.entrando {
    animation: gameover-passo-entra 0.24s steps(1, end) 1;
  }
  @keyframes gameover-passo-entra {
    0% { opacity: 0; transform: translateY(12px); }
    34% { opacity: 1; transform: translateY(6px); }
    67% { transform: translateY(-2px); }
    100% { transform: translateY(0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .gameover-passo.entrando {
      animation: none;
    }
  }
  .gameover-passo-titulo {
    margin: 0;
    font-size: clamp(16px, calc(0.028 * var(--largura-arena)), 22px);
    color: var(--cor-moeda);
    text-transform: uppercase;
    letter-spacing: 1px;
  }
  .gameover-passo-texto {
    margin: 0;
    font-size: 15px;
    line-height: 1.4;
    color: var(--cor-texto);
  }
  .gameover-passo-lista {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 6px;
    width: 100%;
  }
  .gameover-passo-lista li {
    padding: 6px 10px;
    font-size: 15px;
    color: var(--cor-branco);
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-afundada);
  }
  /* Entrada em degraus (três tamanhos inteiros), não uma escala interpolada:
     é a mesma regra de animação do resto da interface (ver
     public/css/CLAUDE.md). `steps(1, end)` entre keyframes mantém cada
     tamanho parado até o próximo, em vez de crescer continuamente. */
  #gameOverOverlay h2.impacto {
    animation: gameover-impacto 0.36s steps(1, end) 1;
  }
  @keyframes gameover-impacto {
    0% { transform: scale(1.6); }
    40% { transform: scale(0.85); }
    70% { transform: scale(1.12); }
    100% { transform: scale(1); }
  }
  #gameOverStats[hidden] {
    display: none;
  }
  #gameOverStats {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 0;
    padding: 8px 14px;
    list-style: none;
    min-width: min(280px, 80%);
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
  }
  /* Uma linha por vez, com atraso fixo (`--atraso`, escrito por gameOver.js):
     a linha aparece inteira de uma vez no seu instante, sem esmaecer — o
     `steps(1, end)` é o que troca `hidden` por visível de um quadro para o
     outro. `backwards` segura o estado inicial durante o atraso. */
  #gameOverStats li {
    display: flex;
    justify-content: space-between;
    gap: 18px;
    font-size: 14px;
    visibility: hidden;
    animation: gameover-stat-linha 0.01s steps(1, end) var(--atraso, 0ms) forwards;
  }
  @keyframes gameover-stat-linha {
    from { visibility: hidden; }
    to { visibility: visible; }
  }
  .gameover-stat-rotulo {
    color: var(--cor-texto-fraco);
  }
  .gameover-stat-valor {
    color: var(--cor-branco);
    font-weight: bold;
    font-variant-numeric: tabular-nums;
  }
  /* ===== Relatório do grupo (#gameOverRelatorio) =====
     Um bloco por defensor da Defesa da base, lado a lado, cada um com o sprite
     da classe em cima e os números embaixo, mais o rodapé da partida (montado
     por `renderRelatorioDaPartida` em gameOver.js). É o caminho normal do fim
     de partida: quando ele desenha, o resumo em lista do #gameOverStats acima
     fica escondido — a lista só aparece no fallback, quando não há snapshot
     final para montar o relatório. */
  #gameOverRelatorio {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    /* Largura **definida**, e não o encolhe-até-o-conteúdo que um item de
       `.gameover-placar` (flex column com `align-items: center`) ganha por
       padrão. Sem isto o relatório media o mais largo do que carrega — e o
       mais largo é o **rodapé**, que muda de tamanho com o resultado (ondas e
       tempo sempre; XP e moedas só quando a partida rendeu). Aí a porcentagem
       das colunas de `.gameover-lutadores` resolvia contra essa medida
       acidental e o mesmo cartão saía com largura diferente a cada partida:
       medido em arena de 884px, um defensor dava 93px com uma linha de rodapé,
       142px com duas e 244px com quatro — e no cartão estreito "Maior
       sequência" quebrava em duas linhas. Com a largura da faixa, quem manda
       no cartão são as colunas abaixo, e só elas. */
    width: 100%;
  }
  #gameOverRelatorio[hidden] {
    display: none;
  }
  .gameover-lutadores {
    /* Grid com colunas **explícitas** por número de defensores (o
       `data-defensores` que `renderRelatorioDaPartida` escreve), e não
       `flex-wrap`: o wrap decidia sozinho onde a fileira quebra, e com três
       defensores numa arena média saía 2+1 desalinhado. As colunas ficam logo
       abaixo, junto do atributo. */
    display: grid;
    justify-content: center;
    align-items: start;
    width: 100%;
    /* Sem piso fixo (`min()`, não `clamp()`): a arena mantém proporção 5:3
       (ARENA = 1000x600, shared/constants.js), então a altura disponível
       encolhe junto com `--largura-arena` sempre na mesma razão. Um piso em
       px continua constante enquanto a arena encolhe, e é isso que empurrava
       o relatório para fora dela numa arena estreita (medido: 380px de
       largura, 228px de altura — o piso de 72px do sprite sozinho já
       estourava a altura disponível). */
    gap: min(28px, calc(0.03 * var(--largura-arena)));
  }
  /* Um defensor sozinho fica numa coluna larga e centrada, e não numa faixa
     estreita perdida no meio da tela. Dois dividem ao meio; três e quatro
     usam uma coluna cada.
     Cada coluna tem **teto em px** e não `1fr`: o `1fr` reparte a faixa
     inteira, então o cartão de um grupo de dois ficava quase o dobro do de um
     grupo de quatro na mesma arena — e o de um defensor sozinho, numa arena
     larga, virava um cartão de 440px com três números dentro. O teto é a
     largura em que o cartão lê bem ("Maior sequência" numa linha, o sprite
     folgado); a porcentagem ao lado é o que faz tudo encolher junto em arena
     estreita, na mesma receita de `min()` sem piso do resto do relatório. */
  .gameover-lutadores[data-defensores="1"] { grid-template-columns: min(300px, 62%); }
  .gameover-lutadores[data-defensores="2"] { grid-template-columns: repeat(2, minmax(0, min(260px, 42%))); }
  .gameover-lutadores[data-defensores="3"] { grid-template-columns: repeat(3, minmax(0, min(215px, 30%))); }
  .gameover-lutadores[data-defensores="4"] { grid-template-columns: repeat(4, minmax(0, min(195px, 23%))); }
  /* Em tela muito estreita o grupo grande cai para duas colunas: quatro
     cartões espremidos deixam o nome ilegível antes de faltar altura. */
  body.tela-muito-estreita .gameover-lutadores[data-defensores="3"],
  body.tela-muito-estreita .gameover-lutadores[data-defensores="4"] {
    grid-template-columns: repeat(2, minmax(0, min(215px, 46%)));
  }
  .gameover-lutador {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    /* Item de grid encolhe até o conteúdo e não menos, a não ser que se diga.
       Sem isto os quatro cartões estouram a fileira em vez de dividir a
       largura da arena entre si. */
    min-width: 0;
    padding: min(8px, calc(0.014 * var(--largura-arena))) min(16px, calc(0.02 * var(--largura-arena)));
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
    /* A cor da classe marca o bloco por cima (borda sólida, sem gradiente nem
       canto arredondado — ver public/css/CLAUDE.md). */
    border-top: 3px solid var(--class-color, var(--cor-borda-media));
  }
  /* Quem venceu ganha a moldura de ouro; quem perdeu escurece um degrau — não
     é uma opacidade animada, é o estado final do bloco. */
  .gameover-lutador.vencedor {
    outline: 2px solid var(--cor-ouro);
  }
  .gameover-lutador.perdedor .gameover-lutador-sprite,
  .gameover-lutador.perdedor .gameover-lutador-nome {
    opacity: 0.7;
  }
  /* O `--sprite-anchor` padrão foi calibrado para um card pequeno; aqui o
     quadro é grande e o personagem ocupa pouco do quadro de 100x100 (o corpo
     mora entre y=45 e y=60, centro em ~0,52), então é esse centro que vai no
     meio da caixa — vale para o `idle` de quem venceu e para o último quadro
     da morte de quem perdeu, que sobra um pouco mais baixo. */
  .gameover-lutador-sprite.class-sprite {
    /* Mesma razão do bloco acima: `min()`, não `clamp()` — sem piso, o
       personagem encolhe até caber em qualquer arena, no lugar de estourar a
       altura dela abaixo de ~450px de largura. */
    --sprite-box: min(112px, calc(0.16 * var(--largura-arena)));
    --sprite-zoom: 3.6;
    --sprite-anchor: 0.53;
  }
  /* Em tela muito estreita ou baixa-deitada (celular, ver "Faixas de tamanho
     de tela" mais abaixo) o sprite some — é decorativo, e nessas faixas altura
     e largura já são o recurso escasso do próprio relatório: nome, classe e os
     três números continuam, só o personagem sai. `.gameover-lutador-quadrado`
     entra na mesma regra por ser a mesma coisa (o fallback de classe sem
     sprite próprio). */
  body.tela-muito-estreita .gameover-lutador-sprite,
  body.tela-baixa-deitada .gameover-lutador-sprite,
  body.tela-muito-estreita .gameover-lutador-quadrado,
  body.tela-baixa-deitada .gameover-lutador-quadrado {
    display: none;
  }
  /* Fallback de classe sem sprite próprio: o mesmo quadrado colorido do
     preview da modal de seleção. */
  .gameover-lutador-quadrado {
    flex: none;
    width: min(22px, calc(0.05 * var(--largura-arena)));
    height: min(22px, calc(0.05 * var(--largura-arena)));
    margin: min(12px, calc(0.02 * var(--largura-arena))) 0;
    border-radius: 0;
    border: 2px solid var(--cor-ouro);
    box-sizing: border-box;
    background: var(--class-color, var(--cor-vermelho));
  }
  .gameover-lutador-nome {
    font-size: min(15px, calc(0.024 * var(--largura-arena)));
    font-weight: bold;
    color: var(--cor-branco);
    /* Nome longo corta em vez de alargar a coluna: quem manda na largura do
       cartão é a fileira (ver `[data-defensores]` acima), não o apelido. */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .gameover-lutador-classe {
    font-size: min(11px, calc(0.016 * var(--largura-arena)));
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--class-color, var(--cor-texto-fraco));
  }
  .gameover-lutador-stats {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin: 4px 0 0;
    padding: 0;
    list-style: none;
    /* A largura é a da coluna do grid, e não um piso próprio: o piso antigo
       (190px) multiplicado por quatro não cabia em arena nenhuma. */
    width: 100%;
    min-width: 0;
  }
  .gameover-lutador-stats li {
    display: flex;
    justify-content: space-between;
    gap: min(14px, calc(0.02 * var(--largura-arena)));
    font-size: min(13px, calc(0.02 * var(--largura-arena)));
  }
  /* Grupo de 3 ou 4 defensores (Defesa da base, ver `renderRelatorioDaPartida`
     em gameOver.js): são os mesmos blocos de 1 ou 2 defensores, apertados. A
     largura folgada de um bloco, quatro vezes, não cabe na arena — e sem isso a fileira
     quebra em 2x2 e o rodapé sai pela borda de baixo numa arena curta. Mesma
     regra de `min()` sem piso do resto do relatório: tudo encolhe junto com
     `--largura-arena`. */
  .gameover-lutadores.grupo {
    gap: min(14px, calc(0.016 * var(--largura-arena)));
  }
  .gameover-lutadores.grupo .gameover-lutador {
    padding: min(6px, calc(0.01 * var(--largura-arena))) min(9px, calc(0.012 * var(--largura-arena)));
  }
  .gameover-lutadores.grupo .gameover-lutador-sprite.class-sprite {
    --sprite-box: min(76px, calc(0.11 * var(--largura-arena)));
  }
  .gameover-lutadores.grupo .gameover-lutador-nome {
    font-size: min(13px, calc(0.02 * var(--largura-arena)));
  }
  .gameover-lutadores.grupo .gameover-lutador-stats li {
    gap: min(8px, calc(0.012 * var(--largura-arena)));
    font-size: min(11px, calc(0.017 * var(--largura-arena)));
  }
  /* Ondas, XP, moedas e tempo são da **partida**, não de um dos lados, então
     ficam embaixo dos blocos e não dentro deles.

     Deitados numa faixa, e não empilhados numa coluna: quatro linhas custavam
     ~60px de altura — que é justamente o que falta numa arena baixa — e o
     formato diferente é o que separa "número da partida" de "número do
     jogador" sem precisar de mais texto. Cada selo carrega a própria pedra;
     antes o fundo ficava no `<ul>` inteiro. */
  .gameover-relatorio-rodape {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: min(6px, calc(0.009 * var(--largura-arena)));
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .gameover-relatorio-rodape li {
    display: flex;
    align-items: baseline;
    gap: min(6px, calc(0.009 * var(--largura-arena)));
    padding: min(4px, calc(0.006 * var(--largura-arena))) min(10px, calc(0.015 * var(--largura-arena)));
    font-size: min(13px, calc(0.02 * var(--largura-arena)));
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
  }
  /* Cada peça do relatório entra no instante dela, escondida até lá — o atraso
     e o som vêm do JS (`revelarRelatorio` em gameOver.js), porque som agendado
     por CSS não existe. A entrada é em três tamanhos inteiros (`steps(1,
     end)`), mesma receita do texto de resultado: nada de escala interpolada. */
  .gameover-revelar {
    visibility: hidden;
  }
  .gameover-revelar.revelado {
    visibility: visible;
    animation: gameover-revelar-pop 0.24s steps(1, end) 1;
  }
  /* O cartão do defensor é a exceção: ele entra **sem** o pop. É o único
     `.gameover-revelar` grande o bastante para o `scale(1.3)` do primeiro
     quadro dar na vista como "o bloco mudou de tamanho enquanto os números
     carregavam" — e é dele que vinha a maior parte da sobra que fazia a barra
     de rolagem de `.gameover-placar` piscar. Sem transform, o cartão tem o
     tamanho final desde a montagem: nome, classe e as três linhas já estão no
     DOM, só escondidos por `visibility`, então a caixa não muda mais depois de
     montada — o que entra em degraus é só a visibilidade dela. Os valores
     dentro do cartão continuam com o pop: são pequenos, e é o blip deles que
     dá o ritmo de placar à revelação. */
  .gameover-lutador.gameover-revelar.revelado {
    animation: none;
  }
  @keyframes gameover-revelar-pop {
    0% { transform: scale(1.3); }
    50% { transform: scale(0.92); }
    100% { transform: scale(1); }
  }

  /* ===== Progresso de conta no fim de partida =====
     Anel de nível à esquerda, barra de XP e legenda à direita. Uma fileira só,
     centralizada como o resto do placar.

     A **barra é pixel art**: canto reto, trilho afundado (o mesmo bisel
     invertido dos trilhos do HUD e da contagem da Loja) e preenchimento sólido.
     Ela **não** herda a exceção do arco liso do anel (ver docs/pixel-art.md) —
     o anel é um arco fino de leitura que já era exceção declarada, e a barra é
     uma peça nova de interface, que segue a regra.

     Nada de `transition` na largura: quem a move é a agenda de revelação de
     gameOver.js, um passo de cada vez, e é isso que a mantém em degraus. Uma
     transição de CSS por cima deslizaria entre os passos e desfaria os
     degraus — inclusive o do nível cheio, que é o instante que a tela marca. */
  /* Grade de duas colunas e duas linhas, e não uma fileira com o texto
     empilhado numa coluna ao lado: o anel e a barra mostram o **mesmo** número
     e têm de ficar na mesma linha, centrados um contra o outro. Com a barra e
     a legenda dentro de um bloco só, o `align-items: center` da fileira
     centrava o anel contra a **soma** dos dois (a legenda reserva 2,4em de
     altura) — o anel descia meia legenda e não se alinhava nem com a barra nem
     com o texto. Na grade a legenda mora na linha de baixo, na coluna da
     barra: ela continua começando onde a barra começa, e não entra na conta do
     anel. */
  #gameOverProgresso {
    display: grid;
    grid-template-columns: auto auto;
    align-items: center;
    justify-content: center;
    column-gap: 10px;
    row-gap: 4px;
  }
  #gameOverProgresso[hidden] {
    display: none;
  }
  #gameOverLevelRing {
    grid-row: 1;
    grid-column: 1;
  }
  .gameover-progresso-trilho {
    grid-row: 1;
    grid-column: 2;
    /* Largura de leitura, não da faixa inteira: esticada, a barra fica larga
       demais para o passo de um nível ser visível como degrau. */
    width: min(260px, calc(0.34 * var(--largura-arena)));
    box-sizing: border-box;
    height: 10px;
    background: var(--cor-superficie-baixa);
    box-shadow: var(--sombra-pedra-afundada);
    /* O padding é o que deixa o bisel `inset` do trilho aparecer por baixo do
       preenchimento — mesma razão dos trilhos do HUD. */
    padding: 2px;
  }
  .gameover-progresso-trilho > i {
    display: block;
    height: 100%;
    width: 0;
    background: var(--cor-vermelho-destaque);
    /* A sombra do mesmo material, como o preenchimento da contagem da Loja faz
       com o ouro da moeda. */
    box-shadow: inset 0 -2px 0 #7f1d1d;
  }
  /* O quadro em que a barra cruza um degrau de nível: ela fica **cheia e
     dourada** por um instante, com o anel piscando junto. É o tratamento
     distinto do avanço comum que o degrau pede, e ele é uma classe trocada pela
     agenda — não uma animação contínua. */
  #gameOverProgresso.degrau .gameover-progresso-trilho > i {
    background: var(--cor-moeda);
    box-shadow: inset 0 -2px 0 #a16207;
  }
  #gameOverProgresso.degrau .anel-nivel-arco {
    stroke: var(--cor-moeda);
  }
  #gameOverProgresso.degrau .gameover-progresso-legenda {
    color: var(--cor-moeda);
  }
  /* O anel do overlay não anima o arco: a transição contínua de
     `.anel-nivel-arco` serve às telas de menu, onde o valor muda de uma vez; aqui
     quem dá o ritmo é a agenda, e a transição atrasaria o arco em relação à
     barra ao lado dele — as duas peças mostram o mesmo número. */
  #gameOverProgresso .anel-nivel-arco {
    transition: none;
  }
  .gameover-progresso-legenda {
    grid-row: 2;
    grid-column: 2;
    font-size: min(11px, calc(0.019 * var(--largura-arena)));
    color: var(--cor-texto-medio);
    text-align: left;
    line-height: 1.2;
    /* A legenda troca de texto durante a revelação (o que falta muda a cada
       degrau) e ela é a última linha da fileira: sem altura reservada, cada
       troca de uma para duas linhas empurraria a medalha abaixo dela. */
    min-height: 2.4em;
  }

  /* A linha de contexto da moeda: o preço da próxima carta e o que falta (ver
     `avisoDaProximaCarta` em gameOver.js). Ela sai inteira no dourado da moeda —
     o mesmo `--cor-moeda` do saldo da Loja e do botão de compra ao lado —,
     porque a frase toda é sobre a moeda; destacar só o número exigiria marcação
     montada em JS para um ganho de leitura nenhum. */
  #gameOverLojaAviso {
    margin: 0;
    font-size: min(12px, calc(0.02 * var(--largura-arena)));
    color: var(--cor-moeda);
    text-align: center;
  }
  #gameOverLojaAviso[hidden] {
    display: none;
  }

  /* O selo de "marca nova" na linha da melhor marca (ver `linhaDeMelhorMarca` em
     matchStats.js). Dourado como o resto do que a tela comemora, e em versalete
     como os outros selos da interface. Ele é o **único** conteúdo condicional
     de uma linha do rodapé, então não reserva espaço: a linha sem ele fica
     exatamente como sempre foi. */
  .gameover-stat-selo {
    margin-left: 8px;
    font-size: min(10px, calc(0.017 * var(--largura-arena)));
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--cor-moeda);
    /* Pisca entre **duas cores**, não entre a cor e o nada: um selo que
       desaparece metade do tempo lê como falha de desenho (mesma regra do aviso
       de onda e da barra de vida baixa). Em `steps()`, como todo movimento da
       interface. */
    animation: gameover-selo-pisca 0.7s steps(1, end) infinite alternate;
  }
  @keyframes gameover-selo-pisca {
    from { color: var(--cor-moeda); }
    to { color: var(--cor-ouro-suave); }
  }

  #gameOverMedal {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
  }
  #gameOverMedal[hidden] {
    display: none;
  }
  .gameover-medalha-titulo {
    font-size: min(12px, calc(0.02 * var(--largura-arena)));
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--cor-ouro-suave);
  }
  /* No perfil as medalhas vivem numa grade de 4 colunas, que dá a largura a
     elas; soltas aqui elas encolheriam até o texto, e duas medalhas lado a
     lado sairiam de tamanhos diferentes. */
  .gameover-medalha-lista {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
  }
  .gameover-medalha-lista .achievement-badge {
    /* A medalha é um retângulo deitado (retrato à esquerda, texto à direita),
       então a largura mínima acompanha: com os 96px de quando ela era quadrada,
       o critério quebrava em quatro linhas dentro do cartão. */
    min-width: min(200px, calc(0.34 * var(--largura-arena)));
    /* Sem piso fixo aqui: na tela de Conquistas `.achievement-badge` está numa
       grade que lhe dá a largura (ver comentário acima), mas dentro do overlay
       ela precisa encolher junto com a arena — é o retrato de 40x40px fixo
       (herdado de lá) que sobrava altura demais numa arena baixa quando a
       medalha entra empilhada em cima do relatório da partida. `padding`/`gap`
       também encolhem para não sobrar folga fixa em volta de um retrato que já
       ficou menor. */
    padding: min(8px, calc(0.014 * var(--largura-arena))) min(10px, calc(0.017 * var(--largura-arena)));
    column-gap: min(10px, calc(0.017 * var(--largura-arena)));
  }
  .gameover-medalha-lista .achievement-portrait {
    width: min(40px, calc(0.07 * var(--largura-arena)));
    height: min(40px, calc(0.07 * var(--largura-arena)));
  }
  .gameover-medalha-lista .achievement-portrait svg {
    width: min(20px, calc(0.035 * var(--largura-arena)));
    height: min(20px, calc(0.035 * var(--largura-arena)));
  }
  .gameover-medalha-lista .achievement-tier {
    font-size: min(11px, calc(0.019 * var(--largura-arena)));
  }
  .gameover-medalha-lista .achievement-progresso {
    font-size: min(10px, calc(0.017 * var(--largura-arena)));
  }

  #waitingOverlay .overlay-buttons {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 16px;
  }
  /* Nem todo `<button>` do overlay de espera é botão de ação: na sala
     cooperativa o cartão do próprio jogador (troca de classe) e o código da
     sala (copiar) são clicáveis mas continuam sendo cartão e texto. Os dois
     levam `.botao-nu` e ficam fora tanto desta base vermelha quanto da
     moldura de fliperama no fim do arquivo. */
  #gameOverOverlay button, #waitingOverlay button:not(.botao-nu) {
    font-size: 16px;
    padding: 10px 22px;
    border-radius: 8px;
    border: none;
    cursor: pointer;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #gameOverOverlay button:hover, #waitingOverlay button:not(.botao-nu):hover {
    background: var(--cor-vermelho-hover);
  }
  #waitingOverlay button#btnLeaveQueue {
    background: var(--cor-borda-media);
  }
  #waitingOverlay button#btnLeaveQueue:hover {
    background: var(--cor-borda);
  }
  /* A ação do passo do corredor (abrir a árvore, abrir a carta, ver as metas)
     sai no **dourado** que era o da compra da Loja no fim de partida: é a
     oferta de gastar o que a partida rendeu, e o vermelho a leria como mais uma
     saída. O fundo é preenchido, e não transparente com contorno: a moldura de
     fliperama do fim do arquivo desenha o contorno escurecendo o próprio fundo
     do botão (`::before` com `background: inherit`), e fundo transparente
     deixava o tom escuro vazar para o miolo. */
  #gameOverOverlay button#gameOverAcaoDoPasso {
    background: var(--cor-moeda);
    /* Texto escuro: sobre o dourado o branco fica com contraste pior. */
    color: var(--cor-fundo);
  }
  #gameOverOverlay button#gameOverAcaoDoPasso:hover {
    background: var(--cor-moeda-hover);
  }
  /* "Depois" e "Pular tudo" são a saída sem obrigação, e por isso neutros. O
     mesmo botão vira "Continuar" no Resultado e depois de a ação ser feita, e
     aí ele é a ação principal (`data-foco-inicial`, escrito pelo JS) e ganha o
     vermelho das saídas. */
  #gameOverOverlay button#gameOverSeguirBtn,
  #gameOverOverlay button#gameOverPularBtn,
  #gameOverOverlay button#gameOverMapaBtn {
    background: var(--cor-borda-media);
  }
  #gameOverOverlay button#gameOverSeguirBtn:hover,
  #gameOverOverlay button#gameOverPularBtn:hover,
  #gameOverOverlay button#gameOverMapaBtn:hover {
    background: var(--cor-borda);
  }
  #gameOverOverlay button#gameOverSeguirBtn[data-foco-inicial] {
    background: var(--cor-vermelho);
  }
  #gameOverOverlay button#gameOverSeguirBtn[data-foco-inicial]:hover {
    background: var(--cor-vermelho-hover);
  }
  /* No toque, todo botão da faixa tem alvo de 44x44px. */
  body.touch-mode #gameOverOverlay .gameover-acoes button {
    min-height: 44px;
    min-width: 44px;
  }
  /* Sem `display: none` de partida: ele nasceu escondido, ligado pelo JS só
     onde fazia sentido, e hoje é a **única** saída da tela — esconder por
     omissão deixaria o fim do tutorial sem botão nenhum. */
  #gameOverOverlay button#btnTrocarSala {
    background: var(--cor-borda-media);
  }
  #gameOverOverlay button#btnTrocarSala:hover {
    background: var(--cor-borda);
  }
  /* Sprite de personagem fora do canvas (ver public/js/classSprite.js): a tira
     de quadros 100x100 é o `background-image` e a animação anda os quadros em
     `steps()`. O JS só escreve `--sprite-frames`/`--sprite-duration` e a
     imagem; o enquadramento é aqui.
     `--sprite-zoom` aproxima a arte (o personagem ocupa menos da metade do
     quadro) e `--sprite-anchor` é a altura do centro do corpo dentro do
     quadro (0 = topo, 1 = base) — sem isso o personagem fica pequeno e caído
     para baixo na miniatura. Nas miniaturas (0.4) é só estética; no preview
     da modal (`--sprite-anchor: 0.5` mais abaixo) precisa bater exatamente
     com a altura em que o tiro sai, então usa o centro geométrico do quadro —
     é o mesmo ponto usado por render.js (`cy`) para centralizar o sprite no
     canvas, e `--sprite-shot-offset` (escrito por classPreview.js a partir de
     `getSpriteOffsetY`) corrige a mesma folga vertical que assassino/sniper
     já recebem no canvas por terem uma pose mais agachada/alongada. */
  .class-sprite {
    --sprite-box: 48px;
    --sprite-zoom: 2.5;
    --sprite-anchor: 0.43;
    --sprite-shot-offset: 0px;
    --sprite-frames: 6;
    --sprite-duration: 900ms;
    --sprite-frame: calc(var(--sprite-box) * var(--sprite-zoom));
    --sprite-x0: calc((var(--sprite-box) - var(--sprite-frame)) / 2);
    flex: none;
    width: var(--sprite-box);
    height: var(--sprite-box);
    background-repeat: no-repeat;
    background-size: calc(var(--sprite-frames) * var(--sprite-frame)) var(--sprite-frame);
    background-position: var(--sprite-x0)
      calc(var(--sprite-box) / 2 - var(--sprite-frame) * var(--sprite-anchor) + var(--sprite-shot-offset));
    image-rendering: pixelated;
    animation: spriteStrip var(--sprite-duration) steps(var(--sprite-frames)) infinite;
  }
  /* Ataque/dano/morte: travam no último quadro em vez de repetir, igual ao
     canvas (`loop: false` em characterSprites.js). Precisa de uma tira
     **própria**, e não de `iteration-count: 1` sobre a de cima: aquela anda até
     o quadro `frames` (um passado o fim da tira, o ponto em que voltaria ao
     início), e travar ali com `forwards` deixava o elemento **vazio** — foi
     assim que o lutador derrotado do fim de partida apareceu sem personagem.
     Aqui o último quadro é `frames - 1` e o `jump-none` é o que faz os `frames`
     valores caberem entre os dois extremos, inclusive. */
  .class-sprite.class-sprite-once {
    animation: spriteStripUnica var(--sprite-duration)
      steps(var(--sprite-frames), jump-none) 1 forwards;
  }
  @keyframes spriteStrip {
    from { background-position-x: var(--sprite-x0); }
    to { background-position-x: calc(var(--sprite-x0) - var(--sprite-frames) * var(--sprite-frame)); }
  }
  @keyframes spriteStripUnica {
    from { background-position-x: var(--sprite-x0); }
    to {
      background-position-x:
        calc(var(--sprite-x0) - (var(--sprite-frames) - 1) * var(--sprite-frame));
    }
  }
  @keyframes classPreviewShot {
    0% { transform: translate(0, -50%); opacity: 0; }
    8% { opacity: 1; }
    88% { opacity: 1; }
    100% { transform: translate(var(--shot-dx), calc(-50% + var(--shot-dy))); opacity: 0; }
  }
  @keyframes classPreviewDummyHit {
    0% { background: var(--cor-branco); border-color: var(--cor-branco); transform: scale(1.18); }
    100% { background: var(--cor-superficie-hover-alta); border-color: var(--cor-cinza-claro); transform: scale(1); }
  }
  @keyframes classPreviewDamageFloat {
    0% { opacity: 0; transform: translate(-50%, 0); }
    15% { opacity: 1; }
    100% { opacity: 0; transform: translate(-50%, -16px); }
  }
  #menuTitleWrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    animation: entrada-fade 0.5s ease both;
  }
  /* Índice dos modos: duas linhas de dois botões, centradas embaixo do logo.
     Não tem mais fundo de pedra nem moldura — as molduras de fliperama dos
     próprios botões já separam o índice do tijolo do fundo, e o painel em
     volta deles só engordava o bloco e brigava com o logo pela atenção. */
  #menuBody {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    /* A largura é a da linha dos modos: 640px é onde os dois botões cabem
       folgados com rótulo e subtítulo cada um em uma linha só, e abaixo disso
       a linha encolhe junto com a tela até virar coluna na faixa
       `tela-estreita`. */
    width: clamp(320px, calc(0.72 * var(--largura-tela)), 640px);
    max-width: 100%;
    box-sizing: border-box;
    animation: entrada-fade 0.5s ease both;
  }
  /* A porta do jogo, hoje um botão só (o jogo tem um modo — ver a capacidade
     `modo-unico`): ele toma a linha inteira por `flex: 1 1 0`, e não por
     conteúdo (`1 1 auto`), então a largura é sempre a dos 460px da linha e não
     a do rótulo que estiver escrito nele. A regra é a mesma de quando havia
     dois botões irmãos aqui, em que o `1 1 0` os mantinha do mesmo tamanho. */
  #menuModos {
    display: flex;
    gap: 12px;
    width: var(--capa-linha);
  }
  #menuModos > button {
    flex: 1 1 0;
    min-width: 0;
  }
  /* "Patch" e "Créditos" fecham o índice, embaixo dos botões de modo, em vez de
     terem visão própria — são ações secundárias, não merecem um botão na
     navegação principal ao lado de "jogar" e "ranking". São botões
     de ação normais (mesma moldura de fliperama dos de modo, ver o bloco de
     pixel art no fim do arquivo); a hierarquia está na cor (cinza para quem
     abre modal, vermelho para quem leva a uma visão) e na linha mais estreita
     que a dos modos. */
  #menuTitleExtras {
    display: flex;
    gap: 12px;
    /* Mesma largura da linha dos modos (ver `--capa-linha` em #menuViewHome):
       as duas são a mesma coluna, e a hierarquia entre elas está na cor e na
       altura do botão, não em larguras diferentes.

       **E os dois botões empilham**, cada um com a largura cheia da linha, em
       vez de dividirem-na ao meio: com meia linha cada um, "Patch" e "Créditos"
       mediam 144px contra os 300 do "Jogar" (224 contra 460 no desktop), e a
       capa lia como um botão de verdade em cima de dois pedaços. Empilhados, os
       três têm a mesma largura e a hierarquia fica onde ela já estava — a cor e
       a altura do botão. O preço é altura: os três mais os dois vãos custam
       151px fixos, e é essa conta que o `--capa-logo` da faixa deitada tem de
       respeitar (ver lá). */
    flex-direction: column;
    width: var(--capa-linha);
  }
  #menuTitleExtras > button {
    flex: 1 1 0;
    min-width: 0;
  }
  /* Os dois ids no seletor são de propósito: a cor precisa vencer o vermelho da
     regra genérica de botão do menu, que soma um id e as seis classes dos
     `:not()` dela (`:not()` conta a especificidade do argumento). */
  #menu #menuTitleExtras button.title-action-btn {
    background: var(--cor-capa-secundario);
  }
  #menu #menuTitleExtras button.title-action-btn:hover {
    background: var(--cor-capa-secundario-hover);
  }
  /* Os botões da capa ficam sobre a ilustração — céu vermelho e laranja —, e
     não sobre a pedra escura das outras telas: com a moldura padrão (2px, o
     fundo a 50%) o "Jogar" vermelho e os cinza se dissolviam nela. Aqui a
     moldura é mais grossa e quase preta, que é o que recorta a peça de
     qualquer ponto da arte; o "Jogar" ganha um vermelho mais vivo e as duas
     secundárias viram pedra escura em vez de cinza médio. O realce de foco
     acompanha: fino e mais recuado, para cair dentro da moldura nova e não em
     cima dela (as medidas estão em `--recuo-realce-foco`). */
  #menuViewHome {
    --espessura-borda-botao: 4px;
    --escurecimento-borda-botao: 0.18;
    --espessura-realce-foco: 2px;
    --recuo-realce-foco: -8px;
  }
  /* A tipografia dos botões da capa sai da mesma régua da linha que os segura
     (`--capa-logo`), e não dos 20px/14px/32px fixos da regra genérica de botão
     do menu: uma linha que encolheu com a tela e um rótulo que não encolheu
     junto é exatamente o que fazia "Patch beta 0.4.1" quebrar em duas — foi
     por causa disso que a linha das ações secundárias tinha um teto em px
     próprio. Numa tela grande o logo bate no teto de 200px e os três clamps
     batem nos valores de sempre, então o desktop não muda.

     `white-space: nowrap` é a garantia do contrato: se um rótulo não couber,
     ele estoura a caixa em vez de quebrar em silêncio — e o arnês da capa
     mede a largura, então o estouro aparece no teste em vez de aparecer no
     celular de quem joga. */
  #menu #menuViewHome #menuModos > button,
  #menu #menuViewHome #menuTitleExtras > button {
    font-size: clamp(15px, calc(0.1 * var(--capa-logo)), 20px);
    padding:
      clamp(9px, calc(0.07 * var(--capa-logo)), 14px)
      clamp(14px, calc(0.16 * var(--capa-logo)), 32px);
    white-space: nowrap;
  }
  #menu #menuViewHome #hordeSubtitle {
    font-size: clamp(10px, calc(0.06 * var(--capa-logo)), 12px);
  }
  #menu #menuModos #btnHorde {
    background: var(--cor-capa-jogar);
    text-shadow: 2px 2px 0 var(--cor-pedra-borda);
  }
  #menu #menuModos #btnHorde:hover {
    background: var(--cor-capa-jogar-hover);
  }
  #menu #menuModos #btnHorde #hordeSubtitle {
    color: var(--cor-capa-jogar-subtitulo);
  }
  #menu #menuTitleExtras button.title-action-btn {
    color: var(--cor-branco);
    text-shadow: 2px 2px 0 var(--cor-pedra-borda);
  }
  #menuVersion {
    color: var(--cor-vermelho-destaque);
  }
  /* Bolinha de "tem novidade que você não viu". Fica dentro do botão, não
     grudada num canto de fora: o botão é recortado por clip-path e um badge
     fora da silhueta seria cortado. Como "bolinha" aqui é pixel art, o
     círculo é um quadrado de 10px com os cantos em degrau de 2px — mesma
     ideia da moldura dos botões, na escala do ponto. Sem `border-radius`, e
     o pisca-pisca alterna entre duas cores em dois passos — escalar o ponto
     reamostraria os degraus dos cantos em meio pixel e borraria a bolinha. */
  #changelogDot {
    display: none;
    width: 10px;
    height: 10px;
    margin-left: 2px;
    background: var(--cor-ouro);
    clip-path: polygon(
      2px 0, 8px 0, 8px 2px, 10px 2px,
      10px 8px, 8px 8px, 8px 10px, 2px 10px,
      2px 8px, 0 8px, 0 2px, 2px 2px);
    animation: changelog-dot-pulso 1s steps(1, end) infinite;
  }
  #btnChangelog.tem-novidade #changelogDot {
    display: inline-block;
    vertical-align: 1px;
  }
  @keyframes changelog-dot-pulso {
    0%, 50% { background: var(--cor-ouro); }
    50.01%, 100% { background: var(--cor-ouro-suave); }
  }
  /* Mesma bolinha de #changelogDot, reusada em qualquer botão marcado com
     .tem-conquista-nova (accountFabBtn e a aba "Conquistas" do perfil) — ver
     achievementNotify.js. Um só desenho em vez de duplicar a regra por
     elemento. */
  .conquista-nova-dot {
    display: none;
    width: 10px;
    height: 10px;
    margin-left: 4px;
    background: var(--cor-ouro);
    clip-path: polygon(
      2px 0, 8px 0, 8px 2px, 10px 2px,
      10px 8px, 8px 8px, 8px 10px, 2px 10px,
      2px 8px, 0 8px, 0 2px, 2px 2px);
    animation: changelog-dot-pulso 1s steps(1, end) infinite;
  }
  .tem-conquista-nova .conquista-nova-dot {
    display: inline-block;
    vertical-align: 1px;
  }
  /* Rota para a árvore de talentos depois do tutorial (ver
     public/js/dicaDeTalentos.js). Fixa na página e posicionada por JS abaixo
     do altar do acampamento (#estacao-altar).

     `z-index` 16: um a mais que a barra (15), que é o que ela precisa cobrir, e
     muito abaixo do toast de conquista (950) — um desbloqueio no meio do
     caminho continua sendo a notícia mais urgente da tela. Canto reto, borda
     de 2px e sombra sólida deslocada, como o resto da interface. */
  .dica-talentos {
    position: fixed;
    z-index: 16;
    width: 260px;
    max-width: calc(var(--largura-tela) - 24px);
    box-sizing: border-box;
    padding: 12px 14px;
    background: var(--cor-barra-fundo);
    border: 2px solid var(--cor-ouro);
    box-shadow: 4px 4px 0 var(--cor-pedra-borda);
    color: var(--cor-texto);
    font-size: 12px;
    line-height: 1.4;
    /* Entrada em degraus inteiros, como o toast de conquista. A saída é seca:
       tirar o balão é `hidden`, de um quadro para o outro. */
    animation: dica-talentos-entra 0.24s steps(1, end) 1;
  }
  .dica-talentos[hidden] {
    display: none;
  }
  /* Durante a partida a barra some, e a âncora do balão junto — ele não pode
     ficar flutuando sobre a arena esperando a volta ao menu. */
  body.game-active .dica-talentos {
    display: none;
  }
  @keyframes dica-talentos-entra {
    0% { opacity: 0; transform: translateY(-6px); }
    50% { opacity: 1; transform: translateY(-3px); }
    100% { transform: translateY(0); }
  }
  /* A seta é de blocos, não de glifo nem de borda diagonal: três degraus
     inteiros (4, 12 e 20px de largura) apontando para a aba. O `left` vem do
     JS — é o centro da aba, que continua sendo o alvo mesmo quando o balão
     encosta na borda da janela e sai de baixo dela. */
  .dica-talentos-seta {
    position: absolute;
    top: -6px;
    width: 20px;
    height: 4px;
    background: var(--cor-ouro);
    transform: translateX(-50%);
  }
  .dica-talentos-seta::before,
  .dica-talentos-seta::after {
    content: '';
    position: absolute;
    left: 50%;
    height: 4px;
    background: var(--cor-ouro);
    transform: translateX(-50%);
  }
  .dica-talentos-seta::before {
    top: -4px;
    width: 12px;
  }
  .dica-talentos-seta::after {
    top: -8px;
    width: 4px;
  }
  .dica-talentos strong {
    display: block;
    margin-bottom: 5px;
    color: var(--cor-ouro);
    font-size: 13px;
  }
  .dica-talentos p {
    margin: 0;
    color: var(--cor-texto-fraco);
  }
  .dica-talentos-acoes {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 10px;
  }
  /* O botão que **é** a rota: ele clica na aba (ver dicaDeTalentos.js), então
     tem o peso de destino, e o "Agora não" ao lado é `.link-button`. */
  #btnDicaTalentosAbrir {
    padding: 6px 10px;
    border: none;
    border-radius: 0;
    background: var(--cor-ouro);
    box-shadow: 2px 2px 0 var(--cor-pedra-borda);
    color: var(--cor-texto-sobre-tier);
    font-size: 12px;
    cursor: pointer;
  }
  /* Toast de conquista desbloqueada: fixo na página inteira (não na arena,
     como #matchTutorialBanner) porque uma conquista pode
     desbloquear tanto durante uma partida quanto ao voltar pro menu. Acima de
     tudo (`z-index` maior que o HUD e as modais). Some sozinho, mas dá pra
     clicar nele para abrir o perfil na aba de conquistas — por isso é um
     `<button>`, e por isso o `pointer-events` volta a valer quando ele está
     visível (escondido, não pode roubar clique de quem está por baixo). */
  #achievementToast {
    position: fixed;
    top: 12px;
    left: 50%;
    transform: translate(-50%, -12px);
    z-index: 950;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 20px;
    background: rgba(20, 20, 20, 0.94);
    border: 1px solid var(--cor-ouro);
    color: var(--cor-texto);
    font-family: inherit;
    font-size: 14px;
    text-align: left;
    width: auto;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    pointer-events: none;
    cursor: pointer;
    opacity: 0;
  }
  /* A entrada é em degraus (três posições inteiras), e não a interpolação de
     antes: é o mesmo passo-a-passo do resto da interface. A saída continua
     seca — sem `transition`, tirar `.visible` apaga o aviso de um quadro para
     o outro, que é o que a estética pede. */
  #achievementToast.visible {
    opacity: 1;
    transform: translate(-50%, 0);
    pointer-events: auto;
    animation: conquista-toast-entra 0.24s steps(1, end) 1;
  }
  @keyframes conquista-toast-entra {
    0% { opacity: 0; transform: translate(-50%, -12px); }
    33% { opacity: 1; transform: translate(-50%, -8px); }
    66% { transform: translate(-50%, -3px); }
    100% { transform: translate(-50%, 0); }
  }
  #achievementToast:hover {
    background: rgba(40, 40, 40, 0.96);
  }
  .achievement-toast-texto {
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  #achievementToast strong {
    color: var(--cor-ouro);
    white-space: nowrap;
  }
  /* Retrato da medalha dentro do toast: o desbloqueio deixou de ser só texto.
     A medalha é a mesma do perfil e do fim de partida (achievementPortrait.js);
     aqui o anel dela ganha a cor do tier, que é o que dá contraparte visível ao
     som diferenciado por tier (audio.js). */
  #achievementToastMedals {
    display: flex;
    flex-shrink: 0;
    gap: 6px;
  }
  #achievementToastMedals .achievement-portrait {
    animation: conquista-surge 0.32s steps(1, end) 1;
  }
  #achievementToastMedals .tier-bronze { border-color: var(--cor-tier-bronze); }
  #achievementToastMedals .tier-prata { border-color: var(--cor-tier-prata); }
  #achievementToastMedals .tier-ouro { border-color: var(--cor-tier-ouro); }
  #achievementToastMedals .tier-diamante { border-color: var(--cor-tier-diamante); }
  #achievementToastMedals .tier-lendario { border-color: var(--cor-tier-lendario); }
  /* A medalha "surge" em quatro tamanhos inteiros, sem interpolar — mesma
     receita do texto de resultado do fim de partida (`gameover-impacto`).
     Começa em zero: a medalha não estava ali um quadro antes. */
  @keyframes conquista-surge {
    0% { transform: scale(0); }
    25% { transform: scale(1.5); }
    50% { transform: scale(0.8); }
    75% { transform: scale(1.2); }
    100% { transform: scale(1); }
  }
  /* ----- Regra comum a todas as modais -----
     Cada modal define a própria largura, mas a altura é sempre limitada à
     viewport: numa tela baixa uma modal mais alta que a tela era cortada em
     cima e embaixo, escondendo o título e o botão de fechar (a única saída
     virava o Esc). Passando do limite, o conteúdo rola dentro da modal. */
  .modal {
    max-height: calc(var(--altura-tela) - 32px);
    overflow-y: auto;
    animation: entrada-pop 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
    /* Não existe reset global de `box-sizing` neste arquivo (o `*` acima só
       mexe em scrollbar), e toda modal tem padding — sem isto, o teto de
       altura acima passa a valer para o **conteúdo** e o padding vai por
       fora, deixando a modal maior que a tela justamente no aparelho em que
       o teto existe para ajudar. Era o caso do navegador de salas, a única
       modal que não declarava `box-sizing` na regra própria; declarado aqui,
       nenhuma modal nova pode esquecer. */
    box-sizing: border-box;
  }
  /* Alvo de toque dos botões de fechar de modal (26x26px no desktop, abaixo
     do alvo recomendado de ~44px): em modo touch só a área clicável cresce,
     via `min-width`/`min-height` — o ícone (✕, 15px) continua centralizado
     pelo próprio `<button>` e do mesmo tamanho de sempre, sem redesenhar
     nada. Desktop (sem `body.touch-mode`) fica exatamente como era.
     `.modal-close-flutuante` fica de fora: já é um botão flutuante com espaço
     de sobra ao redor (ver a regra própria dele), sem precisar do reforço —
     e o extra de 44px empurraria o teto reservado pra ele numa modal que já
     não pode rolar. */
  /* Em modo touch nenhuma modal pode passar da altura da tela: a saída (botão
     de fechar, botão de ação) fica fora da viewport e, girado, o gesto de
     rolagem nem existe nativamente (ver `body.tela-girada`, `touch-action:
     none`). Este é o piso — as modais que sabem se organizar melhor (a de
     classe e a de salas) fixam a própria altura e distribuem o espaço por
     dentro em vez de rolar; esta regra é a rede de segurança para as
     demais, inclusive a do modo treino, que no desktop abre mão do teto.
     Vem com `#botClassOverlay` no seletor porque `max-height: none` de lá
     está num seletor com id e venceria uma regra só de classe. */
  body.touch-mode .modal {
    max-height: calc(var(--altura-tela) - 16px);
    overflow-y: auto;
  }
  body.touch-mode .modal-close {
    min-width: 44px;
    min-height: 44px;
  }
  /* O ✕ do cabeçalho de modal (.modal-close, no HTML de cada uma). Cinco
     modais declaravam esta mesma caixa de 26px separadamente; aqui é uma vez
     só. Ele nasce **sem** moldura — as duas cores da moldura de botão de
     ícone (ver a regra compartilhada de #btnConfiguracoes) começam transparentes e
     só acendem no hover: com o contorno sempre visível, cinco cabeçalhos
     ganhariam um quadrado disputando atenção com o próprio título.
     O ✕ era o glifo U+2715 desenhado pela fonte da interface — espessura,
     inclinação e peso ficavam por conta do navegador. Hoje é o mesmo desenho
     do X de mudo: duas diagonais de traço 2 em ponta de esquadro. */
  .modal-close {
    width: 26px;
    height: 26px;
    min-width: 0;
    padding: 0;
    --cor-borda-campo: transparent;
    --cor-fundo-campo: transparent;
    color: var(--cor-texto-fraco);
  }
  .modal-close:hover {
    --cor-borda-campo: var(--cor-borda);
    --cor-fundo-campo: var(--cor-superficie-hover);
    color: var(--cor-branco);
  }
  /* Botão de fechar flutuante: fica sobre a modal (`position: absolute`, e a
     modal é o `position: relative` acima), não empurra nem disputa espaço com
     o título nem com o conteúdo. Vermelho como os outros botões de ação em vez
     da cor neutra dos flutuantes de ícone — esse aqui fecha a modal, então
     precisa continuar chamando atenção mesmo sem um cabeçalho ao redor para dar
     contexto. */
  .modal-close-flutuante {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 2;
    width: 30px;
    height: 30px;
    min-width: 0;
    padding: 0;
    /* A moldura vem da regra compartilhada dos botões de ícone (ver
       #btnConfiguracoes); aqui só as duas cores. Ao contrário do ✕ de cabeçalho, o
       contorno deste aparece sempre: sem cabeçalho em volta, é ele que separa
       o botão do conteúdo da modal por baixo. */
    --cor-borda-campo: var(--cor-vermelho-hover);
    --cor-fundo-campo: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  .modal-close-flutuante:hover {
    --cor-fundo-campo: var(--cor-vermelho-hover);
    --cor-borda-campo: var(--cor-vermelho-selecao);
  }

  /* O ✕ das telas abertas pelas estações do acampamento (ver
     public/js/fecharDaTela.js). É o mesmo botão flutuante vermelho das modais
     sem cabeçalho, mas **saindo** da laje: o canto do botão passa da quina de
     cima e da direita do painel. Ele mora na `.menu-view`, irmão do painel (o
     painel é `overflow: hidden` e cortaria o que passa da borda), e o JS
     escreve `top`/`left` a partir das medidas do painel. O `left` é a quina
     direita, e o `translateX(-100%)` alinha a **direita** da caixa nela — com o
     glifo do controle aparecendo à esquerda do ✕, a caixa cresce para dentro do
     painel, e não para fora da tela.

     A entrada é a mesma do painel (`entrada-fade`, com o mesmo atraso), senão o
     botão chegaria antes da laje em que ele está pregado. O `transform` do
     deslocamento fica na caixa e a animação no botão e no glifo: as duas no
     mesmo elemento brigariam pela mesma propriedade. */
  .menu-view:has(> .tela-fechar) {
    position: relative;
  }
  .tela-fechar {
    position: absolute;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 6px;
    transform: translateX(-100%);
  }
  .tela-fechar[hidden],
  .tela-fechar-glifo[hidden] {
    display: none;
  }
  .tela-fechar > * {
    animation: entrada-fade 0.5s ease 0.12s both;
  }
  .modal-close-flutuante.tela-fechar-botao {
    position: static;
    width: 36px;
    height: 36px;
  }
  body.touch-mode .modal-close-flutuante.tela-fechar-botao {
    width: 44px;
    height: 44px;
  }
  /* A capinha do controle é rótulo, não botão: peça miúda, como a do atalho da
     barra (`.menu-glifo-atalho`), mas um degrau maior e com o texto claro:
     ela fica sobre a pedra escura da laje, e no tom fraco de sempre sumia. */
  .tela-fechar-glifo {
    display: flex;
  }
  .tela-fechar-glifo .glifo {
    min-width: 28px;
    height: 28px;
    padding: 0 7px;
    font-size: 14px;
    color: var(--cor-texto);
  }
  .tela-fechar-glifo .glifo svg {
    width: 14px;
    height: 14px;
  }

  /* ----- Anel de nível (.anel-nivel, ver public/js/levelRing.js) -----
     Componente único de nível + progresso de XP da conta, usado em três
     lugares (botão fixo de conta, perfil e árvore de talentos) no lugar das
     duas barras horizontais que existiam antes.

     O círculo é liso — arco de traço fino, com antialiasing e transição
     contínua — e é uma exceção deliberada à regra de pixel art da interface,
     na mesma família dos ícones de classe e do círculo da zona de power-up
     (ver docs/pixel-art.md). O desenho vem do SVG montado em levelRing.js; aqui
     ficam só tamanho, cores e espessura. Todo o tamanho sai de --anel-lado,
     então a versão pequena do botão de conta troca uma medida só. */
  .anel-nivel {
    --anel-lado: 56px;
    position: relative;
    width: var(--anel-lado);
    height: var(--anel-lado);
    flex-shrink: 0;
  }
  .anel-nivel--pequeno {
    --anel-lado: 30px;
  }
  .anel-nivel svg {
    display: block;
    width: 100%;
    height: 100%;
    /* O arco começa no topo e cresce no sentido horário — sem isso o zero fica
       às 3 horas, onde ninguém procura o início de um medidor. */
    transform: rotate(-90deg);
  }
  .anel-nivel-trilho, .anel-nivel-arco {
    fill: none;
    stroke-width: 4;
  }
  .anel-nivel-trilho {
    stroke: var(--cor-borda);
  }
  .anel-nivel-arco {
    stroke: var(--cor-vermelho-destaque);
    stroke-linecap: round;
    transition: stroke-dasharray 300ms ease-out;
  }
  /* Em 0% (convidado, ou nível que acabou de virar) o arco sai de cena: a ponta
     arredondada desenha uma bolinha no topo mesmo com comprimento zero, e ela
     leria como um progresso que não existe. */
  .anel-nivel--vazio .anel-nivel-arco {
    display: none;
  }
  /* O número fica no miolo do anel, centralizado sobre o SVG. */
  .anel-nivel-num {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    font-weight: 600;
    line-height: 1;
    color: var(--cor-texto);
  }
  .anel-nivel--pequeno .anel-nivel-num {
    font-size: 11px;
  }
  /* Anel esperando a resposta do servidor (marcarAnelCarregando em
     levelRing.js): o loading skeleton padrão da tela inicial, só que redondo —
     um retângulo brilhando por cima de um medidor circular leria como falha de
     layout. O conteúdo fica escondido, e não apagado, para o anel não mudar de
     tamanho entre carregando e carregado. */
  .anel-nivel.skeleton-loading > * {
    visibility: hidden;
  }
  .anel-nivel.skeleton-loading::after {
    border-radius: 50%;
  }
  /* Os dois números que acompanham o anel (XP exata no perfil, pontos livres na
     árvore de talentos). Carregando eles estão vazios ou com o valor da abertura
     anterior, então o skeleton precisa de um tamanho próprio — sem ele a linha
     some em vez de mostrar que está vindo. */
  #profileLevelXp.skeleton-loading,
  #talentTreePoints.skeleton-loading {
    display: inline-block;
    min-width: 180px;
    min-height: 1em;
  }

  /* ----- Conta: botão da barra superior, modal de conta e modal de entrar/criar
     conta -----
     O botão fica fora de #menu, no canto direito de #menuTopbar (quem
     posiciona a linha é a barra), mas só existe fora de partida:
     `body.game-active` some com ele em vez de um `display` controlado por JS,
     já que a visibilidade só depende do modo (menu × partida), nunca de estado
     assíncrono. */
  #accountFabBtn {
    display: flex;
    align-items: center;
    /* Em touch é um quadrado de 40px com só o anel de nível dentro; com o
       rótulo (desktop) leva o apelido ao lado — ver a seção "Barra em touch"
       junto das regras de #menuTopbar. O alinhamento e o `padding` são os
       mesmos do botão de voltar da barra e valem nos dois desenhos, pelo mesmo
       motivo: o anel fica no mesmo lugar nos dois. Os 2px de `padding` (e não
       os 9px dos outros botões) são o que sobra dos 40px depois do anel de
       30px, da margem de 2px dele e das bordas: com isso o anel fica com o
       **mesmo centro** dos ícones de 20px da barra, que é o alinhamento que
       se vê na barra. */
    justify-content: flex-start;
    gap: 0;
    padding: 0px 12px 0px 4px;
    height: 40px;
    /* A moldura é a dos botões de ícone (ver a regra compartilhada de
       #btnConfiguracoes): o botão mora na mesma fileira que ele, e com canto
       arredondado ele era o único degrau fora de esquadro da barra. Aqui só as
       duas cores — o contorno continua sendo o ouro que separa a conta das
       abas vermelhas. */
    --cor-borda-campo: var(--cor-ouro-suave);
    --cor-fundo-campo: var(--cor-barra-botao);
    color: var(--cor-branco);
    font-size: 13px;
    cursor: pointer;
    box-sizing: border-box;
    flex-shrink: 0;
  }
  #accountFabBtn:hover {
    --cor-fundo-campo: var(--cor-barra-botao-hover);
  }
  body.game-active #accountFabBtn {
    display: none;
  }
  /* No lugar do antigo quadrado cinza de placeholder de avatar (não há upload
     de foto no jogo), o anel de nível da conta — o mesmo componente do perfil
     e da árvore de talentos, aqui na versão pequena (ver .anel-nivel e
     public/js/levelRing.js). Convidado fica com o anel vazio. */
  #accountFabAvatar {
    margin: 2px;
  }
  /* Como .menu-voltar-rotulo: na barra só-ícone é largura zero (e
     não `display: none`), para não mexer na altura da linha — ver a seção
     "Barra em touch". O teto com `ellipsis` é o que faz apelido comprido
     cortar em vez de empurrar a barra. */
  #accountFabLabel {
    display: inline-block;
    max-width: 112px;
    /* Mesmo vão dos rótulos das abas, e pelo mesmo motivo (ver
       .menu-voltar-rotulo). */
    padding-left: 8px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Barra de conta dentro do perfil (#profileOverlay): saudação + Sair (logado)
     ou campo de nickname + Entrar/criar conta (convidado). Escondida
     (`hidden`) quando o perfil aberto é o de outro jogador, ver profile.js. */
  #profileAccountBar {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--cor-superficie-hover);
  }
  #profileAccountBar[hidden] {
    display: none;
  }
  /* Nível/XP da conta (ver shared/progressao.js), só no perfil próprio
     logado — o anel (.anel-nivel) é o mesmo componente da tela "Árvore de
     Talentos" e do botão fixo de conta (ver public/js/levelRing.js); o texto
     embaixo da saudação é o que o anel não dá: a XP exata. `display: contents`
     porque anel e XP são células da grade de #accountLoggedIn — este wrapper
     existe só para o profile.js esconder os dois de uma vez com `hidden`. */
  #profileLevelBar {
    display: contents;
  }
  #profileLevelBar[hidden] {
    display: none;
  }
  #profileLevelXp {
    color: var(--cor-texto-fraco);
    font-size: 12px;
  }
  /* Aviso de convidado: histórico/conquistas abaixo vêm do navegador, não do
     servidor. Escondido por padrão (ver #profileGuestNotice[hidden] e
     profile.js, que só desconde para o próprio perfil sem conta). */
  #profileGuestNotice {
    padding-bottom: 14px;
    text-align: center;
    border-bottom: 1px solid var(--cor-superficie-hover);
    font-size: 12px;
    color: var(--cor-ouro);
  }
  #profileGuestNotice[hidden] {
    display: none;
  }
  /* Identidade da conta em grade: o anel de nível na coluna 1 ocupa as duas
     linhas; "Jogando como X" e o Sair dividem a primeira linha; a XP exata
     fica embaixo da saudação. Sem nível (fetch falhou), #profileLevelBar fica
     `hidden` e a grade colapsa para a linha da saudação. */
  #accountLoggedIn {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    column-gap: 10px;
    row-gap: 2px;
    margin-right: 12px;
    font-size: 14px;
    color: var(--cor-texto-fraco);
  }
  #profileLevelRing {
    grid-column: 1;
    grid-row: 1 / span 2;
  }
  #accountGreeting {
    grid-column: 2;
    grid-row: 1;
  }
  #accountLoggedIn #btnLogout {
    grid-column: 3;
    grid-row: 1;
  }
  #profileLevelXp {
    grid-column: 2 / span 2;
    grid-row: 2;
  }
  #accountLoggedOut {
    display: none;
    flex-direction: column;
    align-items: center;
    gap: 0px;
    margin-top: -16px;
    font-size: 14px;
    color: var(--cor-texto-fraco);
  }
  #accountName {
    color: var(--cor-texto);
  }
  #accountOrSep {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--cor-cinza-medio);
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  #accountOrSep::before, #accountOrSep::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--cor-cinza-medio);
  }
  #accountAuthLinks {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 8px;
  }
  .account-sep {
    color: var(--cor-cinza-medio);
  }
  #menu button.link-button, .link-button {
    background: none;
    border: none;
    padding: 0;
    width: auto;
    font-size: 14px;
    color: var(--cor-texto-medio);
    cursor: pointer;
    text-decoration: underline;
  }
  #menu button.link-button:hover, .link-button:hover {
    background: none;
    color: var(--cor-texto);
  }
  /* Alvo de toque de `.link-button` (ações da barra de conta, links da tela
     de autenticação): no desktop a hit area
     é só a altura do texto sublinhado. Em modo touch cresce para ~44px sem
     esticar o botão pro tamanho do container pai (`inline-flex`, não
     `flex`), já que ele convive tanto em linha (`#accountAuthLinks`) quanto
     em coluna (`#authForm`). */
  body.touch-mode .link-button {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 4px;
  }
  #authOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 20;
  }
  #authOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #authOverlay .modal {
    padding: 14px 24px;
    width: 360px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* Quem rola é o corpo, não a modal inteira: o cabeçalho com o botão de
       fechar precisa ficar visível o tempo todo (mesmo padrão da modal de
       novidades). */
    overflow: hidden;
  }
  /* Só os campos rolam (`#authForm`, dentro daqui); botão de enviar, aviso
     e os links ficam fora, em `#authFooter`, sempre visíveis — um rodapé
     escondido atrás de scroll não é descoberto sozinho. */
  #authBody {
    display: flex;
    flex-direction: column;
    gap: 10px;
    overflow-y: auto;
    /* Filho flex com `overflow-y: auto` só rola de verdade com
       `min-height: 0` — sem isso o `min-height: auto` padrão do item flex
       impede que ele encolha, e o conteúdo estoura a altura do `.modal`
       (que tem `overflow: hidden`) em vez de rolar por dentro. */
    min-height: 0;
  }
  #authFooter {
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* Nunca encolhe nem rola — fica sempre visível abaixo dos campos,
       igual ao cabeçalho acima deles. */
    flex-shrink: 0;
    padding-top: 10px;
    border-top: 1px solid var(--cor-borda-media);
  }
  body.touch-mode #authFooter {
    gap: 4px;
    padding-top: 4px;
  }
  body.touch-mode #authLinks {
    gap: 6px;
  }
  body.touch-mode #authHeader h2 {
    font-size: 18px;
  }
  /* Cadastro (mais um campo que login) é o que estoura a altura disponível
     em celular retrato primeiro (ver proposal.md). Só o respiro vertical
     encolhe — os `<input>` (alvo de toque principal, `font-size: 16px` pra
     não disparar zoom do iOS) e o widget do Turnstile (fora do controle do
     projeto) não mudam de altura. `.modal` continua com teto de altura +
     rolagem interna como rede de segurança para o caso extremo em que isso
     ainda não bastar (ver design.md). */
  body.touch-mode #authOverlay .modal {
    padding: 8px 18px;
    gap: 4px;
  }
  body.touch-mode #authForm {
    gap: 6px;
  }
  body.touch-mode .auth-field {
    gap: 2px;
  }
  body.touch-mode .auth-field label {
    font-size: 12px;
    letter-spacing: 0.02em;
  }
  #authHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  #authHeader h2 {
    margin: 0;
    font-size: 22px;
  }
  /* O desenho de todos eles é `.modal-close`, uma regra só perto do fim do
     arquivo — eram cinco cópias idênticas desta mesma declaração, uma por
     modal. */
  #authForm {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  .auth-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .auth-field label {
    font-size: 13px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  /* Cores da moldura em pixel art — ver o bloco no fim do arquivo. */
  .auth-field .campo-pixel {
    --cor-fundo-campo: var(--cor-superficie-elevada);
    --cor-borda-campo: var(--cor-branco);
  }
  .auth-field .campo-pixel:focus-within {
    --cor-borda-campo: var(--cor-vermelho-destaque);
  }
  .auth-field input {
    font-size: 16px;
  }
  .password-field-wrap {
    position: relative;
  }
  /* Precisa das duas classes: `.campo-pixel input`, no fim do arquivo, tem a
     mesma especificidade e ganharia por vir depois — o campo de senha ficaria
     com o texto passando por baixo do olho. */
  .password-field-wrap.campo-pixel input {
    padding-right: 40px;
  }
  .password-toggle-btn {
    position: absolute;
    top: 50%;
    right: 6px;
    transform: translateY(-50%);
    width: 32px;
    height: 32px;
    border: none;
    background: transparent;
    color: var(--cor-texto-fraco);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    /* Canto reto como o resto da interface (ver docs/pixel-art.md). Ele foi o
       último `border-radius: 6px` sobrevivente por não ter fundo próprio, e o
       que o denunciou foi o realce de foco: o `outline` acompanha o raio da
       peça, e o anel do controle saía **arredondado** nesta e em nenhuma
       outra. */
    border-radius: 0;
  }
  .password-toggle-btn:hover {
    color: var(--cor-texto);
    background: var(--cor-superficie-alta);
  }
  .password-toggle-btn:disabled {
    opacity: 0.5;
    cursor: default;
  }
  #authTurnstile {
    display: flex;
    justify-content: center;
  }
  #authFeedback, #resetFeedback {
    display: none;
    font-size: 13px;
    line-height: 1.5;
    padding: 10px 12px;
    border-radius: 0;
  }
  #authFeedback.visible, #resetFeedback.visible {
    display: block;
  }
  #authFeedback.erro, #resetFeedback.erro {
    background: rgba(230, 57, 70, 0.15);
    color: var(--cor-erro-claro);
  }
  #authFeedback.sucesso, #resetFeedback.sucesso {
    background: rgba(42, 157, 92, 0.15);
    color: var(--cor-sucesso-claro);
  }
  #btnAuthSubmit, #btnResetSubmit {
    font-size: 18px;
    padding: 12px 24px;
    border-radius: 8px;
    border: none;
    cursor: pointer;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
    width: 100%;
    box-sizing: border-box;
  }
  #btnAuthSubmit:hover, #btnResetSubmit:hover {
    background: var(--cor-vermelho-hover);
  }
  #btnAuthSubmit:disabled, #btnResetSubmit:disabled {
    background: var(--cor-borda-media);
    color: var(--cor-texto-fraco);
    cursor: default;
  }
  #authLinks {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
  }

  /* ----- Perfil (conta, histórico de partidas e conquistas) ----- */
  #profileOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 20;
  }
  #profileOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #profileOverlay .modal {
    padding: 20px 24px;
    width: 460px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  #profileHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  #profileHeader h2 {
    margin: 0;
    font-size: 22px;
  }
  /* O desenho de todos eles é `.modal-close`, uma regra só perto do fim do
     arquivo — eram cinco cópias idênticas desta mesma declaração, uma por
     modal. */
  /* Uma lista por fase (título + as 3 medalhas de tamanho de grupo dela), não
     um grid solto de 24 cartões — sem o título, "Solo"/"Esquadrão" repetido
     oito vezes não diz de qual fase é cada um. */
  /* `flex-basis: auto` (e não `0`): a grade vive dentro de contêineres de
     altura **de conteúdo** (a modal do perfil de outro jogador), e ali uma base
     `0` some com a lista inteira — ela fica com 0px de altura e as 24 medalhas
     existem no DOM sem aparecer na tela. Com base automática a lista mede o
     próprio conteúdo, e é o teto de altura de quem a contém (o `max-height` da
     `.modal`, o `.menu-painel` da visão "Conquistas") que a faz encolher e
     rolar quando não cabe. */
  /* `display: flex` abaixo vence o `display: none` que o navegador dá ao
     atributo `hidden`, então a grade escondida do perfil **próprio**
     (profile.js) continuava na tela — e ela só não aparecia por estar vazia:
     bastava visitar o perfil de outro jogador antes para o conteúdo dele
     reaparecer embaixo do resumo do próprio. Mesmo par de regras de
     #profileAccountBar e #profileLevelBar. */
  .achievements-grid[hidden] {
    display: none;
  }
  .achievements-grid {
    display: flex;
    flex-direction: column;
    gap: 14px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }
  /* As 3 medalhas de uma fase (solo/esquadrão/grupo completo) ficam lado a
     lado, uma por categoria de tamanho de grupo. A grade é por **largura
     mínima** e não um número fixo de colunas: a medalha é um retângulo largo
     (retrato à esquerda, texto à direita), e o mesmo bloco precisa caber em
     três colunas no painel do menu e numa só na modal estreita do perfil de
     outro jogador, sem faixa de `@media` própria — mesma receita da lista do
     Relicário. */
  .achievement-classe-medalhas {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 8px;
  }
  /* Exceção da modal do perfil de outro jogador: ali a largura (460px) fica
     logo abaixo dos 220px x 2 da regra acima, então a grade caía numa coluna
     só e as 4 medalhas de cada fase viravam uma tira vertical de 8 telas de
     scroll. Duas colunas fixas: a legenda ("Vença com 2 a 3 jogadores") quebra
     em duas linhas nessa largura, o que custa bem menos que dobrar o scroll.
     Em tela muito estreita (≤420px, ver "Faixas de tamanho de tela") não há
     largura para duas, e a medalha volta a ocupar a fileira inteira. */
  #profileAchievementsGrid .achievement-classe-medalhas {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  body.tela-muito-estreita #profileAchievementsGrid .achievement-classe-medalhas {
    grid-template-columns: minmax(0, 1fr);
  }
  /* Retrato à esquerda, texto à direita: em coluna, o cartão quadrado espremia
     a legenda ("Vença com 2 a 3 jogadores") em três linhas de texto minúsculo
     debaixo do retrato. O retrato ocupa as duas linhas da grade; o texto fica
     alinhado à esquerda, encostado nele. */
  .achievement-badge {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto;
    align-items: center;
    column-gap: 10px;
    row-gap: 1px;
    background: var(--cor-superficie-forte);
    padding: 8px 10px;
    /* Mesma gramática da linha do Relicário e do Monstruário — a medalha é a
       terceira lista de catálogo do menu, e era a única das três que ainda
       tinha canto arredondado e fundo chapado: silhueta recortada em escadinha
       de um degrau (a escala certa para uma peça desta altura) e bisel de pedra
       no lugar do `border-radius`. Nada de `border` junto: recortada pelo
       `clip-path`, ela sumiria justamente na diagonal dos cantos.

       A faixa de 3px à esquerda é a cor do **tier**, do mesmo jeito que a da
       linha do Relicário é a cor da raridade: é o que diz "bronze"/"lendário"
       na medalha ainda trancada, que fora dela é cinza igual às outras 35. Ela
       vem antes do bisel na lista porque `box-shadow` pinta na ordem, e a luz
       do bisel por cima comeria 2px dos 3. */
    box-shadow: inset 3px 0 0 var(--cor-tier, var(--cor-borda)),
      var(--sombra-pedra-pequena-interna);
    clip-path: var(--recorte-icone-pixel);
    /* Entrada em degraus inteiros, como o resto da interface — a escala
       contínua do `ease` era a única transição suave desta tela. */
    animation: entrada-pop 0.24s steps(3, end) both;
    /* Âncora da tarja "New" no canto — ver .conquista-nova-badge abaixo.
       Vale para toda medalha (não só a nova) para o tier diamante, que já é
       `relative` por causa do brilho, não precisar de exceção. */
    position: relative;
  }
  .achievement-badge .achievement-portrait {
    grid-row: 1 / span 2;
  }
  /* As duas linhas de texto se encostam no meio da caixa: a de cima ancorada
     embaixo, a de baixo ancorada em cima. Sem isso elas se afastariam para as
     bordas das duas faixas da grade, e o par lia como dois textos soltos em vez
     de um bloco. */
  .achievement-badge .achievement-tier {
    align-self: end;
  }
  .achievement-badge .achievement-progresso {
    align-self: start;
  }

  /* Tarja de medalha recém-desbloqueada, no canto do cartão: aqui o rótulo
     diz *qual* medalha é a nova no meio de 25 iguais, papel que uma bolinha
     não cumpriria. Fora do fluxo porque o cartão é uma coluna centralizada
     (retrato, tier, legenda) e qualquer coisa no meio dela empurraria o
     layout. Vermelho de destaque (a cor de "olhe aqui" do jogo), canto reto e
     sem sombra desfocada, como o resto da UI. */
  .conquista-nova-badge {
    display: none;
    position: absolute;
    /* Dentro do cartão, não pra fora dele: o tier diamante tem
       `overflow: hidden` (por causa do brilho) e cortaria uma tarja que
       vazasse o canto. */
    /* 4px e não 2: o cartão agora é recortado em escadinha (`clip-path`), e um
       canto de 4px encostado no topo direito ficaria mordido pelo degrau. */
    top: 4px;
    right: 4px;
    padding: 1px 5px;
    background: var(--cor-vermelho-destaque);
    color: var(--cor-branco);
    font-size: 9px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    line-height: 1.4;
    /* Acima do brilho varrendo o tier diamante (`::after`, inset: 0), que sem
       isso passaria por cima da tarja. */
    z-index: 1;
  }
  .achievement-badge.tem-conquista-nova .conquista-nova-badge {
    display: block;
  }
  /* O retrato mora numa **cavidade cavada na pedra** do cartão (bisel
     invertido), como a miniatura do Monstruário e o ícone do Relicário: é o que
     faz a medalha ler como entrada de catálogo, e não como texto com um desenho
     redondo ao lado. O anel circular de 3px que havia aqui era herança das
     medalhas de classe, que mostravam o sprite do personagem recortado em
     círculo — hoje o retrato é a **textura do chão da fase**, e um ladrilho
     quadrado não pede recorte redondo. */
  .achievement-portrait {
    width: 40px;
    height: 40px;
    overflow: hidden;
    background-color: var(--cor-painel-pedra-veu);
    image-rendering: pixelated;
    box-shadow: var(--sombra-pedra-afundada);
    clip-path: var(--recorte-icone-pixel);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--class-color, var(--cor-texto-fraco));
  }
  /* O retrato da família **Sequência** não tem arena por trás: o combo não
     pertence a fase nenhuma (ver `renderMarcasDeCombo` em achievementsGrid.js),
     e sem a textura de chão o nicho ficava um buraco preto ao lado dos outros
     trinta e seis.

     O que ele mostra é a **cor do degrau** — a mesma que o número pisca no meio
     da tela ao cruzar aquela contagem. Ela chega por `--cor-degrau`, escrita
     por CSSOM a partir de COMBO_ESCADA: um hexadecimal aqui seria a segunda
     cópia da escada. As duas paradas do gradiente são o mesmo degrau seco das
     outras superfícies (nada de rampa contínua), e o `color-mix` escurece o pé
     em vez de um segundo valor a manter em par. */
  .achievement-portrait--sequencia {
    background-image: linear-gradient(
      var(--cor-degrau, var(--cor-texto-fraco)) 0 50%,
      color-mix(in srgb, var(--cor-degrau, var(--cor-texto-fraco)) 55%, #000) 50% 100%
    );
  }
  .achievement-portrait svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
  }
  /* Trancada (marco não alcançado ainda): cartão cinza padrão, retrato em
     escala de cinza. Destrancada: o cartão inteiro vira a cor do tier (não só
     o anel do retrato) — é o que faz a medalha ler como "conquistada" de
     relance, sem precisar comparar 24 anéis finos. */
  .achievement-badge.bloqueada {
    opacity: 0.55;
  }
  .achievement-badge.bloqueada .achievement-portrait {
    filter: grayscale(1);
  }
  /* A cor do tier como variável do cartão: a faixa da borda esquerda a usa em
     qualquer estado (é ela que dá o tier da medalha trancada), e o fundo cheio
     abaixo só quando destrancada. Ouro, diamante e lendário têm gradiente de
     fundo, mas a faixa é sólida — `box-shadow` não pinta gradiente. */
  .achievement-badge.tier-bronze { --cor-tier: var(--cor-tier-bronze); }
  .achievement-badge.tier-prata { --cor-tier: var(--cor-tier-prata); }
  .achievement-badge.tier-ouro { --cor-tier: var(--cor-tier-ouro); }
  .achievement-badge.tier-diamante { --cor-tier: var(--cor-tier-diamante); }
  .achievement-badge.tier-lendario { --cor-tier: var(--cor-tier-lendario); }
  .achievement-badge.desbloqueada.tier-bronze { background: var(--cor-tier-bronze); }
  .achievement-badge.desbloqueada.tier-prata { background: var(--cor-tier-prata); }
  .achievement-badge.desbloqueada.tier-ouro { background: var(--gradiente-tier-ouro); }
  .achievement-badge.desbloqueada.tier-diamante {
    background: var(--gradiente-tier-diamante);
    position: relative;
    overflow: hidden;
  }
  .achievement-badge.desbloqueada.tier-lendario {
    background: var(--gradiente-tier-lendario);
    position: relative;
    overflow: hidden;
  }
  /* Brilho do diamante (e do lendário, o tier acima dele): uma faixa clara
     varrendo a medalha continuamente — exceção deliberada ao passo-a-passo do
     resto da UI, porque aqui o efeito é luz/reflexo, não um desenho pixelado.
     Os dois tiers do topo o compartilham: um lendário sem o brilho leria como
     menos raro que o diamante que ele supera. */
  .achievement-badge.desbloqueada.tier-diamante::after,
  .achievement-badge.desbloqueada.tier-lendario::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(115deg,
      transparent 0 40%,
      rgba(255, 255, 255, 0.85) 40% 50%,
      transparent 50% 100%);
    background-size: 300% 300%;
    background-repeat: no-repeat;
    animation: diamante-brilho 4.5s linear infinite;
    pointer-events: none;
  }
  /* A varredura em si dura pouco (~1/3 da animação); o resto é pausa parada
     numa posição sem brilho visível, pra o efeito não ficar repetindo sem parar. */
  @keyframes diamante-brilho {
    0%, 60% { background-position: 120% 120%; }
    90% { background-position: -20% -20%; }
    100% { background-position: -20% -20%; }
  }
  /* As cores de tier são claras (pra ler bem como "metal precioso"), então o
     texto delas vira escuro em vez do texto claro padrão — só nesse estado. */
  .achievement-badge.desbloqueada .achievement-tier,
  .achievement-badge.desbloqueada .achievement-progresso {
    color: var(--cor-texto-sobre-tier);
  }
  /* Retrato das medalhas da "Defesa da base": em vez do ícone de pessoas,
     o próprio círculo é preenchido com a textura de chão do nível
     (`nivel.fundo`, shared/horde.js — mesma imagem do fundo ladrilhado da
     partida, `ARENA_BG_SRC_HORDE` em arenaVisuals.js), do mesmo jeito que o
     retrato das medalhas de classe é preenchido com o sprite do personagem.
     Uma classe por tipo de fundo, não `style` inline com a URL: a CSP do
     jogo não libera `style-src` inline (`src/server/httpServer.js`/
     `vercel.json`), então a imagem tem que vir de uma regra desta folha. */
  .achievement-portrait-horda-terra {
    background-image: url("../assets/arenas/defesa_da_base_1.png");
    background-size: cover;
    background-position: center;
  }
  .achievement-portrait-horda-gelo {
    background-image: url("../assets/arenas/defesa_da_base_2.png");
    background-size: cover;
    background-position: center;
  }
  .achievement-portrait-horda-areia {
    background-image: url("../assets/arenas/defesa_da_base_3.png");
    background-size: cover;
    background-position: center;
  }
  .achievement-portrait-horda-fogo {
    background-image: url("../assets/arenas/defesa_da_base_4.png");
    background-size: cover;
    background-position: center;
  }
  .achievement-portrait-horda-vortice {
    background-image: url("../assets/arenas/defesa_da_base_sem_fim.png");
    background-size: cover;
    background-position: center;
  }
  /* Categoria em cima ("Solo", "Esquadrão"), critério embaixo. Os dois
     alinhados à esquerda: no cartão deitado o texto nasce colado no retrato, e
     centrar deixava as duas linhas balançando uma sobre a outra. */
  .achievement-tier {
    font-size: 11px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto);
  }
  .achievement-progresso {
    font-size: 10px;
    color: var(--cor-texto-fraco);
    line-height: 1.25;
  }
  /* Visão "Conquistas" (#menuViewConquistas): o container padrão do menu, com
     o título como cabeçalho fixo e a grade rolando por dentro — mesma corrente
     de `flex: 1` + `min-height: 0` das outras telas de leitura. */
  #conquistasPainel {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  /* Mesmo papel (e mesma cor) do aviso de convidado da árvore de talentos: o
     progresso mostrado abaixo mora neste navegador e não segue para outro
     aparelho. */
  .conquistas-aviso {
    margin: 0;
    font-size: 12px;
    color: var(--cor-ouro-suave);
  }
  #profileSummary {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    flex-wrap: wrap;
  }
  #profileSummary:empty {
    display: none;
  }
  .resumo-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    flex: 1;
    min-width: 60px;
  }
  /* O contador de conquistas do perfil **próprio** é um atalho para a visão
     "Conquistas" (ver profile.js): as medalhas não moram mais nesta modal, e
     sem um caminho daqui até elas o número seria um beco. Como <button> ele
     precisa perder o desenho de botão e ficar idêntico ao irmão <div> ao lado;
     o que diz que ele é clicável é o hover — fundo sólido de canto reto, o
     mesmo destaque de fileira do resto da interface, sem transição contínua
     (ele está fora da regra genérica de hover de `button`, lá em cima). */
  .resumo-item-atalho {
    border: none;
    background: none;
    font: inherit;
    color: inherit;
    padding: 4px 6px;
    margin: -4px -6px;
    cursor: pointer;
  }
  /* O `body.foco-sem-ponteiro …:focus` acompanha o `:focus-visible`: este botão
     não tem desenho de botão, então o fundo é a **única** coisa que diz que ele
     é clicável, e o `:focus-visible` sozinho a entrega ao teclado e nega ao
     controle (ver "Realce de foco"). O anel do sistema continua vindo por cima
     — aqui ele soma, não substitui. */
  .resumo-item-atalho:hover,
  .resumo-item-atalho:focus-visible,
  body.foco-sem-ponteiro .resumo-item-atalho:focus {
    background: var(--cor-superficie-hover);
  }
  .resumo-item strong {
    font-size: 20px;
  }
  .resumo-item span {
    font-size: 11px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  /* === A ficha do jogador (ver public/js/fichaDoJogador.js) ===
     Os contadores vitalícios e o recorde por fase, no perfil próprio.

     Canto reto e sem transição, como o resto da interface: a moldura é a linha
     de cima separando a ficha do resumo, e não uma caixa em volta — a modal já
     é uma caixa, e uma segunda dentro dela dobraria a borda no mesmo pixel. */
  #profileFicha {
    border-top: 2px solid var(--cor-borda);
    padding-top: 12px;
    margin-top: 4px;
  }
  .ficha-titulo {
    margin: 0 0 10px;
    font-family: 'Cinzel', serif;
    font-size: 14px;
    letter-spacing: 0.5px;
  }
  .ficha-subtitulo {
    margin: 14px 0 6px;
    font-size: 11px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  /* Grade de 3 e não fileira que se espreme: são seis números de larguras
     bem diferentes ("2" e "3 h 42 min"), e num `flex` o mais largo achataria os
     outros. Em três colunas cada um tem o mesmo espaço, e a quebra para duas
     linhas é a mesma em qualquer largura de modal. */
  .ficha-numeros {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px 8px;
    list-style: none;
    margin: 0;
    padding: 0;
  }
  .ficha-numeros li {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    text-align: center;
  }
  .ficha-numeros strong {
    font-size: 17px;
  }
  .ficha-numeros span {
    font-size: 10px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  /* "Contando desde X." — a linha que impede o número de mentir para quem
     jogava antes de os contadores existirem. Discreta de propósito: ela é uma
     ressalva, não um dado. */
  .ficha-desde {
    margin: 10px 0 0;
    font-size: 11px;
    color: var(--cor-texto-fraco);
  }
  .ficha-vazia {
    margin: 0;
    font-size: 12px;
    color: var(--cor-texto-medio);
    line-height: 1.5;
  }
  .ficha-fases {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
  }
  /* Nome à esquerda, marca à direita, e a marca **não encolhe**: numa fase
     vencida ela é a informação, e deixá-la quebrar em duas linhas enquanto o
     nome fica inteiro inverteria a prioridade. Quem encolhe é o nome, com
     reticências. */
  .ficha-fases li {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 10px;
    padding: 4px 0;
    font-size: 12px;
  }
  .ficha-fases li + li {
    border-top: 1px solid var(--cor-superficie-forte);
  }
  .ficha-fase-nome {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .ficha-fase-marca {
    flex: none;
    color: var(--cor-texto-suave);
  }
  /* "nunca jogada" e o traço de desconhecido: o mesmo tom fraco, porque os dois
     são ausência de marca — e nenhum deles pode se parecer com um resultado. */
  .ficha-fase-nada {
    color: var(--cor-texto-fraco);
  }

  .profile-subtitulo {
    margin: 0 0 8px;
    font-size: 11px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .partida {
    display: grid;
    grid-template-columns: auto 1fr auto;
    grid-template-areas:
      "resultado oponente data"
      "resultado classes  data";
    column-gap: 10px;
    align-items: center;
    /* Mais claro que o fundo da modal (`--cor-modal-fundo`), não mais escuro:
       com um tom parecido os cartões desapareciam dentro da modal. */
    background: var(--cor-superficie-forte);
    border-radius: 0;
    padding: 8px 12px;
    animation: entrada-fade 0.25s ease both;
  }
  .profile-vazio {
    margin: 0;
    font-size: 14px;
    line-height: 1.6;
    color: var(--cor-texto-fraco);
    text-align: center;
  }

  /* ----- Árvore de talentos -----
     Painel único da visão #menuViewTalentos — a tela aberta pelo altar do
     acampamento, irmã do Monstruário e do Relicário (ver #rankingPanel para o
     mesmo padrão de painel de pedra na tela inicial),
     não mais uma modal nem a coluna da direita de #menuViewHorde. Sozinho na
     visão, ele é largo o bastante para as **três** árvores lado a lado — cada
     uma uma grade de 5 colunas por 4 linhas, com as arestas desenhadas.
     Quem rola é **só** a faixa das árvores (#talentTreeArvores): título, nível
     e pontos são cabeçalho e ficam sempre à vista. O painel em si não rola
     (`overflow: hidden`) — daí a corrente de `flex: 1` + `min-height: 0` daqui
     até as árvores, que é o que permite ao filho encolher abaixo do conteúdo
     dele e virar a área de rolagem. O detalhe de um nó **não** mora aqui: é a
     tooltip flutuante #talentTooltip, irmã do painel, justamente porque esta
     faixa recorta (ver a regra dela abaixo). */
  /* A moldura é a padrão (.menu-painel), inclusive a largura: as três caixas
     de árvore (grade de 5 colunas de 52px + padding e borda, ~278px cada, mais
     os vãos de #talentTreeArvores) cabiam nos 920px que este painel tinha para
     si e cabem de sobra na largura padrão. */
  #talentTreePanel {
    gap: 10px;
  }
  #talentTreePanel h2 {
    margin: 0;
    font-size: 22px;
    text-align: center;
  }
  /* Legenda das árvores: fica **embaixo** delas, fora da faixa que rola
     (#talentTreeArvores), lida depois de ver os nós tracejados e não antes. */
  #talentTreeHint {
    margin: 0;
    font-size: 12px;
    line-height: 1.35;
    text-align: center;
    color: var(--cor-texto-fraco);
  }
  /* A palavra "fecho" é o termo que a tela usa no rodapé de cada caixa e na
     tooltip da folha; destacá-la na dica é o que liga as três aparições. */
  #talentTreeHint strong {
    color: var(--cor-texto-suave);
    font-weight: bold;
  }
  #talentTreeGuestNotice {
    display: flex;
    align-items: center;
    gap: 10px;
    justify-content: space-between;
  }
  #talentTreeGuestNotice p {

    margin: 0;
    font-size: 13px;
    color: var(--cor-ouro);
  }
  #talentTreeContent {
    display: flex;
    flex-direction: column;
    gap: 12px;
    /* Ocupa a sobra do painel e pode encolher: sem o `min-height: 0` um filho
       flex nunca fica menor que o conteúdo dele, e a rolagem apareceria no
       painel inteiro em vez de na lista de tiers. */
    flex: 1;
    min-height: 0;
  }
  #talentTreeContent[hidden], #talentTreeGuestNotice[hidden] {
    display: none;
  }
  /* As duas caixas que só existem para o celular (ver #talentTreeCabecalho e
     #talentTreeCorpo em index.html): no desktop elas somem do layout e os
     filhos continuam sendo filhos diretos de #talentTreeContent, com a mesma
     corrente de `flex: 1` + `min-height: 0` que a faixa das árvores depende. */
  #talentTreeCabecalho, #talentTreeCorpo {
    display: contents;
  }
  #talentTreeSummary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    width: 100%;
    justify-content: space-between;
  }
  /* O anel de nível (.anel-nivel) tem tamanho fixo; os pontos disponíveis
     ocupam a sobra da linha e empurram o "Resetar" para a direita. */
  #talentTreePoints {
    flex: 1 1 0;
    color: var(--cor-texto-fraco);
    white-space: nowrap;
    font-size: 18px;
  }
  /* Tem ponto para gastar: o contador sai do cinza de texto secundário e vira
     ouro (ver public/js/talentPointsAviso.js). Dentro do painel o número já
     está na tela — o que faltava era ele pesar mais que a dica ao lado.
     Vem **antes** da regra de `.sem-pontos` de propósito: as duas têm a mesma
     especificidade, e a recusa por falta de ponto precisa ganhar da cor
     normal. */
  #talentTreePoints.com-pontos {
    color: var(--cor-ouro);
  }
  /* Clicou num talento sem ter ponto: o contador pisca em vermelho (junto do
     som de erro, ver talentTreeUi.js) porque é ele que carrega o motivo da
     recusa. Piscada em degraus (steps), não fade — mesma regra de pixel art do
     resto da interface. */
  #talentTreePoints.sem-pontos {
    animation: talent-sem-pontos 0.16s steps(1) 4;
  }
  @keyframes talent-sem-pontos {
    0% { color: var(--cor-vermelho-destaque); }
    50% { color: var(--cor-texto-fraco); }
  }
  /* Falha ao carregar a árvore (ver carregarERenderizar em talentTreeUi.js).
     Ocupa o lugar da lista de tiers, centralizado, porque é isso que ele
     substitui: sem ele o painel ficava só vazio. */
  #talentTreeErro {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 20px 0;
    text-align: center;
  }
  #talentTreeErro p {
    margin: 0;
    font-size: 13px;
    color: var(--cor-texto-fraco);
  }
  /* Sem isto o botão herda a largura dos botões de ação do menu e ocupa a
     linha inteira, com mais peso na tela que a árvore que ele recupera. Os
     **dois** ids são necessários: a regra genérica `#menu button:not(...)`
     também tem id, e id empata antes de classe na conta de especificidade —
     é o mesmo motivo de `#talentTreeSummary #btnTalentTreeReset`. */
  #talentTreeErro #btnTalentTreeRetry {
    width: auto;
    padding: 6px 16px;
    font-size: 13px;
  }
  /* `display: flex` vence o `hidden` do HTML, então os dois blocos que o JS
     mostra/esconde por atributo precisam desfazê-lo à mão. */
  #talentTreeErro[hidden], #talentTreeArvores[hidden], #talentTreeAbas[hidden] {
    display: none;
  }
  /* A única área rolável do painel (ver #talentTreePanel). O padding da direita
     é a folga da barra de rolagem, para ela não encostar nas árvores.
     Ela **não** é a linha das três caixas: quem faz o flex é o trilho de
     dentro (.talent-arvores-trilho). O motivo é a tela baixa, em que a faixa
     rola: um filho esticado (`stretch`) de um contêiner de altura definida
     para na altura **visível**, não na do conteúdo, e ao rolar as caixas
     terminavam no meio da árvore — nós e arestas ficavam fora do fundo
     cinza. Com o trilho, a linha esticada mede o conteúdo e a faixa só
     recorta. */
  #talentTreeArvores {
    display: block;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding-right: 4px;
  }
  /* A linha das três caixas. `min-height: 100%` é o que mantém o desenho de
     tela alta: sobrando espaço, as caixas descem até o fim da faixa; faltando,
     o trilho cresce com o conteúdo e as três continuam da mesma altura (ver
     .talent-arvore). */
  .talent-arvores-trilho {
    display: flex;
    /* `stretch`, e não `flex-start`: é o que dá às três caixas a mesma altura
       (ver .talent-arvore). */
    align-items: stretch;
    justify-content: center;
    gap: 14px;
    min-height: 100%;
  }
  /* As abas só existem no touch-mode (três colunas não cabem em tela de
     celular). No desktop a faixa some, e as três árvores ficam lado a lado. */
  #talentTreeAbas {
    display: none;
  }
  /* Cada árvore vive dentro de uma **caixa**, e não solta na faixa: as três
     silhuetas têm alturas diferentes de propósito (ver shared/talentTree.js), e
     sem o quadro cada uma começava numa altura, com o painel parecendo três
     desenhos jogados lado a lado. O `align-items: stretch` de
     #talentTreeArvores é o que faz as três caixas terminarem na mesma linha,
     independentemente de quantas linhas de nós cada árvore usa. */
  .talent-arvore {
    display: flex;
    flex-direction: column;
    /* As três dividem a faixa em partes iguais em vez de terem a largura da
       própria grade de nós: com `0 0 auto` as caixas somavam ~860px dentro de
       um painel de ~990px e a moldura sobrava dos dois lados. A grade de nós
       em si não muda de tamanho (a geometria dela é do JS, ver "Geometria da
       grade" em talentTreeUi.js) — ela fica centrada na caixa, que é o que o
       `justify-content: center` de .talent-arvore-corpo já fazia.
       Cresce mas **não encolhe** (`1 0 auto`): numa janela em que as três não
       cabem lado a lado, encolher cortaria a grade de nós, que tem largura
       fixa em px e não sabe se reorganizar — quem resolve essa largura é a
       rolagem da faixa, como antes. */
    flex: 1 0 auto;
    /* A caixa é **a mesma laje do painel** que a contém, um degrau acima: as
       duas camadas de .menu-painel::before/::after, com a mesma escadinha de
       dois degraus. Era um retângulo reto de `border: 1px` com um filete claro
       por dentro, e sobre a pedra do painel isso lia como uma caixa de
       aplicativo dentro de uma tela do jogo.

       Antes de chegar aqui ela passou por um meio-termo que não funcionou: só
       o bisel de peça pequena com a silhueta recortada, sem contorno. Sobre
       este fundo escuro o bisel claro aparece em cima e à esquerda e o escuro
       **não aparece em lugar nenhum** — a caixa ficava com dois cantos
       desenhados e dois cantos invisíveis, cada um lendo de um jeito. Contorno
       é o que faz os quatro serem o mesmo canto.

       Três medidas próprias, e as duas primeiras é que mantêm a decisão D1
       (ver docs/estudo-arvore-de-talentos.html) de pé — nada de moldura clara
       dentro de moldura, nada de placa pregada dentro de placa:

       - o contorno é o preto da pedra (--cor-pedra-borda), o mesmo da `border`
         que ela tinha, e não o contorno claro do painel;
       - sem pregos (apagar as três cores, como .hud-pedra.sem-pregos faz);
       - o miolo é --cor-superficie-baixa, um degrau acima do véu do painel: é
         essa diferença de tom, e não o contorno, que diz que a caixa está
         sobre a laje.

       O bisel e a sombra são os da peça pequena (2px/4px), e não os do painel
       (4px/8px): a caixa é uma fração dele. */
    position: relative;
    /* Contexto de empilhamento próprio, mesmo motivo de .menu-painel: sem ele
       as duas camadas em `z-index` negativo cairiam atrás do fundo da visão em
       vez de ficarem entre ele e o conteúdo da caixa. */
    isolation: isolate;
    --moldura-recorte: var(--recorte-botao-pixel);
    --moldura-bisel: var(--sombra-pedra-pequena-interna);
    --moldura-sombra: 4px 4px 0 rgba(0, 0, 0, 0.4);
    --moldura-contorno: var(--cor-pedra-borda);
    --moldura-pedra: var(--cor-superficie-baixa);
    --prego-lado: 8px;
    --prego-margem: 6px;
    --cor-prego: transparent;
    --cor-prego-luz: transparent;
    --cor-prego-sombra: transparent;
    /* O material mora nas camadas, nunca no elemento (a mesma regra de quem
       usa .hud-pedra). */
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
  }
  .talent-arvore-cabecalho {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    /* Apertado de propósito: a dica do painel passou a ter duas linhas (ver
       #talentTreeHint), e sem estes poucos pixels os rodapés das caixas
       ficavam fora da área visível da faixa. */
    padding: 6px 8px 5px;
    /* A cor da árvore aparece aqui, e só aqui, como faixa: é o que separa as
       três à distância sem tingir a caixa inteira. */
    border-bottom: 2px solid var(--talent-arvore-cor);
  }
  /* Cada árvore é nomeada na cor dela (--talent-arvore-cor, escrita pelo JS a
     partir de ARVORES): a cor é a única coisa que separa as três colunas à
     distância, e ela se repete na moldura do nó comprável e na aresta acesa. */
  .talent-arvore h3 {
    margin: 0;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--talent-arvore-cor);
  }
  /* A passiva da árvore (PASSIVA_DA_ARVORE em shared/talentTree.js) vale a
     partir do primeiro ponto investido — apagada enquanto não vale, acesa
     depois. É a promessa que a caixa faz antes do primeiro nó. */
  .talent-arvore-passiva {
    margin: 0;
    font-size: 10px;
    color: var(--cor-cinza-medio);
  }
  .talent-arvore-passiva-ativa {
    color: var(--cor-texto-suave);
  }
  /* Árvore trancada (hoje só o Ímpeto, ver arvoreLiberada em
     shared/talentTree.js). Ouro como o resto dos avisos de progressão — o selo
     de pontos por gastar, o aviso de convidado —, e no topo da caixa, onde a
     pergunta "por que isto está tudo apagado?" nasce. */
  /* Árvore trancada (hoje só o Ímpeto). Ocupa a linha da passiva no cabeçalho
     e a do fecho no rodapé — as duas não valem nada enquanto a árvore não abre,
     e reaproveitá-las é o que mantém as três caixas da mesma altura. Ouro como
     o resto dos avisos de progressão: o selo de pontos por gastar, o aviso de
     convidado. */
  .talent-arvore-tranca, .talent-arvore-tranca-faltam {
    margin: 0;
    font-size: 10px;
    line-height: 1.3;
    color: var(--cor-ouro);
  }
  /* O corpo cresce e centra a grade: é ele que absorve a diferença de altura
     entre a muralha do Bastião (4 linhas) e o raio do Ímpeto (5). */
  .talent-arvore-corpo {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 4px 8px 6px;
  }
  .talent-arvore-rodape {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 5px 8px 6px;
    border-top: 1px solid var(--cor-superficie);
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  /* Enquanto nenhum fecho foi tomado o rodapé conta quantos existem; depois
     ele nomeia o escolhido, na cor da árvore — o mesmo par de estados da
     passiva no cabeçalho. */
  .talent-arvore-fecho-escolhido {
    color: var(--talent-arvore-cor);
  }
  /* A grade em que os nós são posicionados. Largura, altura e o tamanho da
     célula vêm do JS (ver "Geometria da grade" em talentTreeUi.js): o ângulo
     das arestas sai dessas mesmas medidas, e um segundo valor aqui apontaria
     as linhas para o lugar errado. */
  .talent-grade {
    position: relative;
    flex: 0 0 auto;
  }
  /* A aresta pai→filho: um retângulo fino girado no ângulo que o catálogo já
     conhece (nada de SVG nem canvas — ver design.md, Decisão 7). Nasce no
     centro do pai, daí o transform-origin na ponta esquerda e o
     `translateY(-50%)` para o traço ficar centrado na linha. */
  .talent-aresta {
    position: absolute;
    height: 2px;
    /* Cinza da superfície, e não `--cor-pedra-borda`: essa é quase preta
       (#100f0e) e sobre o painel escuro a ligação simplesmente não aparecia —
       as três árvores viravam nuvens de quadrados soltos. */
    background: var(--cor-superficie-forte);
    transform-origin: 0 50%;
    margin-top: -1px;
    pointer-events: none;
  }
  /* Pai no rank máximo: a ligação acende na cor da árvore. É a mesma
     informação do nó filho, dita pela ligação — e a troca é seca, sem
     transição, como o resto da interface. */
  .talent-aresta-acesa {
    background: var(--talent-arvore-cor);
  }
  /* O nó: alvo pequeno, sem texto corrido — só o ícone e o rank. `left`/`top`
     saem de --talent-linha/--talent-coluna, que o JS escreve a partir do
     catálogo. */
  .talent-no {
    position: absolute;
    left: calc((var(--talent-coluna) - 1) * var(--talent-celula-w)
      + (var(--talent-celula-w) - var(--talent-no-tam)) / 2);
    top: calc((var(--talent-linha) - 1) * var(--talent-celula-h)
      + (var(--talent-celula-h) - var(--talent-no-tam)) / 2);
    width: var(--talent-no-tam);
    height: var(--talent-no-tam);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1px;
    box-sizing: border-box;
    padding: 0;
    /* O nó é a peça dos botões de ícone (ver "A moldura dos botões de ícone"):
       quadrado de 38px, silhueta em escadinha de **um** degrau e contorno de
       2px pintado em camadas — e não uma `border`, que sumiria na diagonal dos
       cantos. Era um retângulo reto de fundo chapado com um anel `inset`, a
       última superfície desta tela fora do material do jogo.

       A diferença para aquela regra são as **quatro** camadas no lugar de
       três: lá a silhueta é recortada por `clip-path`, e aqui ela é desenhada.
       O recorte apagaria as duas marcas que o nó pinta **fora** de si — o
       tracejado dourado do fecho (`outline`) e o anel branco da seleção
       (`box-shadow`) —, então as duas camadas de baixo desenham a mesma
       escadinha que o recorte faria: uma faixa alta sem os 4px de cada lado e
       uma faixa larga sem os 4px de cima e de baixo, cuja união é o quadrado
       menos os quatro cantos. As duas de cima repintam o miolo recuado por
       2px, como na regra compartilhada.

       Com isso os cinco estados abaixo são uma troca de cor
       (--cor-borda-campo/--cor-fundo-campo), e não cinco anéis diferentes. */
    --cor-borda-campo: var(--cor-superficie-hover);
    --cor-fundo-campo: var(--cor-superficie-forte);
    border: none;
    border-radius: 0;
    background-color: transparent;
    background-image:
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo));
    background-size:
      calc(100% - 4px) calc(100% - 12px),
      calc(100% - 12px) calc(100% - 4px),
      calc(100% - 8px) 100%,
      100% calc(100% - 8px);
    background-position: 2px 6px, 6px 2px, 4px 0, 0 4px;
    background-repeat: no-repeat;
    color: var(--cor-branco);
    font-family: inherit;
    cursor: pointer;
  }
  /* Nó que não compra nada: sem cursor de mão. `[aria-disabled]` e não
     `:disabled` porque o nó apagado deixou de ser `disabled` — ele precisa
     continuar recebendo foco e abrindo a tooltip que explica o bloqueio (ver
     criarNo, em talentTreeUi.js). */
  .talent-no[aria-disabled="true"], .talent-no-sem-pontos {
    cursor: default;
  }
  .talent-no-icone {
    display: block;
    width: 18px;
    height: 18px;
    line-height: 0;
  }
  .talent-no-icone svg {
    width: 18px;
    height: 18px;
    display: block;
    /* Traço reto, sem junta arredondada: o ícone é pixel art de linhas, e
       `miter` é o que mantém os cantos vivos (ver public/css/CLAUDE.md). */
    stroke-linejoin: miter;
    stroke-linecap: butt;
  }
  /* O contador de rank é o número que decide a compra ("estou em 2 de 3?"), e
     nasceu em 8px no cinza de texto secundário — dois erros somados, tamanho e
     contraste, num alvo de 38px onde o ícone já ocupa o meio. 10px no texto
     claro é o menor par que se lê sem esforço sobre a superfície do nó. */
  .talent-no-rank {
    font-size: 10px;
    font-weight: bold;
    line-height: 1;
    color: var(--cor-texto);
  }
  /* Os quatro estados que o requisito exige distinguir à vista.
     1) Comprável agora: moldura grossa na cor da árvore.
     Sem ponto livre o nó continua clicável (para avisar a recusa, ver
     talentTreeUi.js), mas não pode se anunciar como comprável — por isso
     `sem-pontos` é um estado próprio, e não um modificador. */
  .talent-no-disponivel {
    --cor-borda-campo: var(--talent-arvore-cor);
  }
  /* Mesmo motivo do `.resumo-item-atalho`: o `:focus-visible` não casa no
     controle (ver "Realce de foco"), e o nó comprável em foco ficava sem o
     aceso que o mouse recebe. */
  .talent-no-disponivel:hover, .talent-no-disponivel:focus-visible,
  body.foco-sem-ponteiro .talent-no-disponivel:focus {
    --cor-fundo-campo: var(--cor-superficie-hover);
  }
  /* 2) Bloqueado (pai incompleto, árvore trancada ou fecho já tomado): cinza
     apagado, mas com **fundo opaco** — nada de `opacity` no nó inteiro. Com a
     opacidade, a parede de pedra da tela passava por trás do ícone e o nó
     virava um retângulo sujo, cada um com uma textura diferente conforme o que
     estivesse atrás dele. O apagado sai de cor própria: superfície mais baixa
     e ícone/contador no cinza médio. */
  .talent-no-bloqueado-pai {
    --cor-borda-campo: var(--cor-superficie);
    --cor-fundo-campo: var(--cor-superficie-icone);
    color: var(--cor-cinza-medio);
  }
  .talent-no-bloqueado-pai .talent-no-rank {
    color: var(--cor-cinza-medio);
  }
  /* 3) Comprável mas sem ponto: aceso o bastante para se ver que o caminho
     abriu, apagado o bastante para não convidar ao clique. Pelo mesmo motivo do
     estado acima, o apagado é cor, não opacidade. */
  /* O contorno é o **mesmo** do estado acima (a cor da árvore, e não um anel
     mais fino): com o contorno desenhado em camadas ele tem uma espessura só,
     e quem carrega o "apagado" passou a ser o miolo — pedra mais baixa e texto
     médio, o mesmo par de cores do nó bloqueado. O caminho aberto continua
     legível pela borda; o convite ao clique, que é o que falta, sai do fundo. */
  .talent-no-sem-pontos {
    --cor-borda-campo: var(--talent-arvore-cor);
    --cor-fundo-campo: var(--cor-superficie-icone);
    color: var(--cor-texto-medio);
  }
  .talent-no-sem-pontos .talent-no-rank {
    color: var(--cor-texto-medio);
  }
  /* O nó que a ficha do celular está descrevendo (ver "Ficha do talento no
     celular"). Anel branco **por fora** do nó, em `box-shadow`: o `outline` já
     é do fecho (tracejado dourado) e a silhueta com o contorno de cor é
     `background-image` — a seleção fica com a terceira propriedade, e não
     disputa nenhuma das duas. Era uma `border`, que com a silhueta em camadas
     encolheria o desenho do nó a cada seleção (as camadas nascem na caixa de
     padding) além de contornar em ângulo reto uma peça que tem escadinha. */
  .talent-no-selecionado {
    box-shadow: 0 0 0 2px var(--cor-branco);
  }
  /* Nó de fecho (folha): a exclusividade entre eles precisa aparecer **antes**
     do clique, senão a regra só se revela na recusa. Tracejado no ouro das
     conquistas, `outline` e não `border` para não empurrar o layout do nó nem
     brigar com o `box-shadow` que carrega os quatro estados abaixo. */
  .talent-no-fecho {
    outline: 1px dashed var(--cor-ouro);
    outline-offset: 2px;
  }
  /* 4) Rank máximo: fundo cheio na cor da árvore, com o texto invertido para
     escuro — igual .achievement-badge.desbloqueada faz com as cores de tier. */
  .talent-no-maxado {
    --cor-borda-campo: var(--talent-arvore-cor);
    --cor-fundo-campo: var(--talent-arvore-cor);
    color: var(--cor-texto-sobre-tier);
  }
  .talent-no-maxado .talent-no-rank {
    color: var(--cor-texto-sobre-tier);
  }

  /* ----- Tooltip do nó -----
     Camada `position: fixed` **fora** de #talentTreeArvores, que rola: um balão
     dentro dele desaparecia nas linhas de baixo e era cortado nas de cima — foi
     por isso que a dica de antes era uma linha fixa embaixo da lista. Quem
     posiciona (com flip nas bordas) é posicionarTooltip, em talentTreeUi.js. */
  #talentTooltip {
    position: fixed;
    z-index: 30;
    display: flex;
    flex-direction: column;
    gap: 3px;
    max-width: 260px;
    box-sizing: border-box;
    /* Padding maior que o de um painel simples porque a moldura de modal come
       8px de escadinha em cada lado (ver "Moldura de modal na tooltip"). */
    padding: 11px 13px;
    /* Fundo opaco, e não o véu translúcido dos painéis: a tooltip flutua
       **sobre** as árvores, e com o fundo semitransparente as arestas e os nós
       de trás atravessavam o texto. Quem o pinta é a própria moldura abaixo. */
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    font-size: 11px;
    line-height: 1.3;
    color: var(--cor-texto-fraco);
    pointer-events: none;
  }
  #talentTooltip[hidden] {
    display: none;
  }
  /* ----- Moldura de modal na tooltip -----
     A mesma silhueta em escadinha e a mesma borda vermelha das modais (ver
     "Modais em pixel art"), pelo mesmo truque de seis camadas de
     `background-image`: três pintam a silhueta na cor da borda e três repintam
     o miolo, recuadas pela espessura. É a moldura que o jogador já associa a
     "isto é uma janela do jogo", e ela cabe aqui porque a tooltip é a única
     coisa do painel que flutua por cima do resto.

     Nada de `clip-path` nem de pseudo-elemento, pelas mesmas razões da regra
     das modais — e nada de `box-shadow` externo, que é retangular e apareceria
     como dente sólido justamente nos cantos recortados. */
  #talentTooltip,
  .dica-medalha,
  .dica-barra {
    background-color: transparent;
    background-image:
      /* miolo: recuado por 8px de canto + 2px de borda */
      linear-gradient(var(--cor-modal-fundo), var(--cor-modal-fundo)),
      linear-gradient(var(--cor-modal-fundo), var(--cor-modal-fundo)),
      linear-gradient(var(--cor-modal-fundo), var(--cor-modal-fundo)),
      /* silhueta na cor da borda: mesma escadinha, sem recuo */
      linear-gradient(var(--cor-vermelho), var(--cor-vermelho)),
      linear-gradient(var(--cor-vermelho), var(--cor-vermelho)),
      linear-gradient(var(--cor-vermelho), var(--cor-vermelho));
    background-size:
      calc(100% - 4px) calc(100% - 20px),
      calc(100% - 12px) calc(100% - 12px),
      calc(100% - 20px) calc(100% - 4px),
      100% calc(100% - 16px),
      calc(100% - 8px) calc(100% - 8px),
      calc(100% - 16px) 100%;
    background-position: center;
    background-repeat: no-repeat;
  }
  /* Sem animação de entrada. O balão já teve uma (`scale` em três degraus) e
     ela saiu: percorrendo a grade, o cursor troca de nó a todo momento, e
     reanimar a cada troca lia como interface travada. O `data-lado` continua
     escrito pelo JS — ele diz de que lado o balão nasceu, e é o gancho que
     sobra caso um dia haja um bico apontando para o nó. */
  .talent-tooltip-nome {
    font-size: 12px;
    color: var(--cor-branco);
  }
  .talent-tooltip-rank {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  /* O que o talento faz, em frase inteira: é a leitura principal do balão. */
  .talent-tooltip-descricao {
    color: var(--cor-branco);
  }
  /* A progressão por rank. O rank atual vai aceso no vermelho da identidade —
     é o número que diz onde o jogador está na escada. */
  .talent-tooltip-ranks strong {
    color: var(--cor-vermelho-destaque);
  }
  /* O motivo do bloqueio (qual pai falta, rank máximo, sem ponto). Em ouro, a
     mesma cor dos avisos do menu. */
  .talent-tooltip-bloqueio {
    color: var(--cor-ouro);
  }

  /* Fica na mesma linha do nível/XP, no topo do painel (ver #talentTreeSummary
     em index.html) — daí o tamanho reduzido, para não competir com a barra. Os
     dois ids são para vencer a regra genérica de botão do #menu (que impõe
     `width: 100%` e fonte de 20px); a moldura de fliperama dela continua
     valendo, só as medidas mudam. */
  #talentTreeSummary #btnTalentTreeReset {
    flex: 0 0 auto;
    width: auto;
    padding: 10px 20px;
    font-size: 14px;
    font-weight: bold;
    margin-left: auto;
  }

  /* ----- Árvore de talentos no celular -----
     Em modo touch o painel inteiro cabe em pouca altura, e o cabeçalho dele
     (título + aviso de convidado + nível/pontos/Resetar + a linha de detalhe)
     é fixo: ele não rola junto com os tiers (ver #talentTreePanel), então cada
     pixel que gasta some da árvore. Sem isto sobrava uma faixa de menos de uma
     linha de nós — o cabeçalho ocupava quase o painel todo.

     `body.touch-mode` sozinho, sem faixa de largura: o critério é o tipo de
     aparelho, não o tamanho da tela — em retrato o app é girado por CSS e
     reporta uma viewport larga (ver `body.touch-mode` junto de #menuTopbar). */
  body.touch-mode #talentTreePanel {
    gap: 6px;
  }
  body.touch-mode #talentTreePanel h2 {
    font-size: 15px;
  }
  /* O aviso é informativo e o "Criar conta" ao lado dele continua clicável;
     só o texto encolhe, para não gastar duas linhas de cabeçalho. */
  body.touch-mode #talentTreeGuestNotice p {
    font-size: 10px;
    line-height: 1.25;
  }
  body.touch-mode #talentTreeContent {
    gap: 6px;
  }
  /* Título fora: a aba do menu que trouxe o jogador até aqui já diz "Talentos",
     e no celular cada faixa fixa de cabeçalho sai da árvore. Mesma conta da
     dica dos fechos, que passou para a ficha do nó de fecho — ela é lida uma
     vez na vida e ocupava uma linha para sempre. */
  body.touch-mode #talentTreePanel h2,
  body.touch-mode #talentTreeHint {
    display: none;
  }
  /* Nível, abas, pontos e "Resetar" numa linha só (ver #talentTreeCabecalho em
     index.html). Eram duas faixas empilhadas, e a segunda — as abas — cabia
     folgada na sobra da primeira. */
  body.touch-mode #talentTreeCabecalho {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  body.touch-mode #talentTreeCabecalho #talentTreeAbas {
    flex: 1 1 auto;
    min-width: 0;
  }
  body.touch-mode #talentTreeSummary {
    gap: 6px;
    flex-wrap: nowrap;
    /* Dentro da linha ele mede o próprio conteúdo: quem fica com a sobra são as
       abas. */
    width: auto;
    flex: 0 0 auto;
  }
  body.touch-mode #talentTreeSummary .anel-nivel {
    --anel-lado: 34px;
  }
  body.touch-mode #talentTreePoints {
    font-size: 10px;
    flex: 0 0 auto;
    white-space: nowrap;
  }
  /* O rótulo é curto ("Resetar"): dá para encolher sem perder a área de toque
     confortável, que aqui vem do padding. */
  body.touch-mode #talentTreeSummary #btnTalentTreeReset {
    padding: 7px 12px;
    font-size: 10px;
  }
  /* A tooltip é flutuante e não ocupa espaço no painel (ver #talentTooltip),
     então no touch o que ela precisa é caber na tela estreita. */
  body.touch-mode #talentTooltip {
    max-width: 220px;
    padding: 5px 7px;
    font-size: 10px;
  }
  body.touch-mode .talent-arvores-trilho {
    gap: 8px;
  }
  /* Uma árvore por vez: as três colunas somam ~800px, e em tela de celular
     (deitado ou girado por CSS) elas não cabem lado a lado. As abas usam a
     moldura dos botões de ícone da barra do menu (contorno de 2px em três
     camadas de `background-image` e silhueta recortada de um degrau, ver "A
     moldura dos botões de ícone"), e "aceso" é troca de cor só. Este bloco já
     foi descrito como reuso do padrão de `.horde-endless-tab`, as abas
     solo/grupo do placar do Sem fim; elas saíram quando o placar virou uma
     lista só, e o desenho ficou morando aqui. */
  body.touch-mode #talentTreeAbas {
    display: flex;
    gap: 2px;
  }
  body.touch-mode #menu .talent-aba,
  body.touch-mode #hordeLobbyTalentsBox .talent-aba {
    flex: 1 1 0;
    width: auto;
    padding: 4px 0;
    --cor-borda-campo: var(--cor-pedra-borda);
    --cor-fundo-campo: var(--cor-superficie);
    border: none;
    border-radius: 0;
    clip-path: var(--recorte-icone-pixel);
    background-color: transparent;
    background-image:
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo));
    background-size:
      calc(100% - 4px) calc(100% - 12px),
      calc(100% - 12px) calc(100% - 4px),
      100% 100%;
    background-position: 2px 6px, 6px 2px, 0 0;
    background-repeat: no-repeat;
    color: var(--cor-texto-fraco);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    cursor: pointer;
  }
  /* A aba acesa usa a cor da própria árvore, e não o vermelho de destaque das
     abas do Sem fim: é a mesma cor que nomeia a coluna e acende as arestas
     dela, então a aba diz qual das três está na tela sem depender do rótulo. */
  body.touch-mode #menu .talent-aba-ativa,
  body.touch-mode #hordeLobbyTalentsBox .talent-aba-ativa {
    --cor-borda-campo: var(--talent-arvore-cor);
    --cor-fundo-campo: var(--talent-arvore-cor);
    color: var(--cor-texto-sobre-tier);
  }
  body.touch-mode .talent-arvore:not(.talent-arvore-ativa) {
    display: none;
  }
  /* O título da coluna sai: a aba acesa já o diz, e no touch cada linha de
     cabeçalho some da árvore (ver "Árvore de talentos no celular"). */
  body.touch-mode .talent-arvore-cabecalho {
    /* No celular a aba já diz o nome da árvore; o cabeçalho da caixa fica só
       com a passiva, que a aba não tem onde escrever. */
    border-bottom-width: 1px;
    padding-top: 5px;
  }
  body.touch-mode .talent-arvore h3 {
    display: none;
  }
  /* No celular o nó é o mesmo alvo de 38px (só uma árvore por vez cabe na
     tela), então o contador **não** encolhe: 7px era o tamanho de quando o
     painel tinha as três colunas espremidas, e ali ele simplesmente não era
     lido. */
  body.touch-mode .talent-no-rank {
    font-size: 10px;
  }

  /* ----- Ficha do talento no celular -----
     A tooltip flutuante não serve no toque: `mouseover` não existe, `focusin`
     num botão não é garantido, o balão cai debaixo do dedo e some na primeira
     rolagem — e o toque que a abriria já **comprava** o talento. Aqui o toque
     seleciona e o detalhe vem para uma caixa fixa ao lado da árvore (ver
     #talentFicha em index.html e docs/sugestoes-arvore-talentos-mobile.html).

     Ao **lado**, e não embaixo, porque no celular a tela é sempre deitada (em
     retrato o `body` é girado por CSS): a altura é o que falta e a largura é o
     que sobra — uma árvore ocupa ~280px dos ~720px do painel. */
  body.touch-mode #talentTreeCorpo {
    display: flex;
    gap: 8px;
    /* Assume o lugar que a faixa das árvores tinha na coluna do painel: é ele
       que cresce e é dentro dele que a rolagem acontece. */
    flex: 1;
    min-height: 0;
  }
  /* A faixa passa a medir a árvore, e não a linha inteira: o resto é da ficha.
     A grade tem largura fixa em px (ver "Geometria da grade" em
     talentTreeUi.js), então ela não encolhe — quem cede é a ficha. */
  body.touch-mode #talentTreeCorpo #talentTreeArvores {
    flex: 0 0 auto;
  }
  body.touch-mode #talentFicha {
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex: 1 1 0;
    min-width: 0;
    padding: 10px 14px;
    /* A ficha é a **mesma caixa** da árvore ao lado, com as mesmas medidas de
       laje (ver .talent-arvore): as duas são o mesmo nível de superfície e
       ficam lado a lado na mesma tela — duas gramáticas ali fariam o jogador
       ler duas vezes. O padding subiu junto, pelo motivo de sempre: a escadinha
       de dois degraus come 8px de cada canto.
       Quem rola é o **texto** (#talentFichaTexto), e não a ficha: as camadas da
       laje são absolutas dentro dela, então numa caixa que rola a moldura
       subiria junto com o conteúdo. De quebra o botão de comprar deixa de
       depender do `margin-top: auto` para ficar à vista num talento de texto
       longo. */
    position: relative;
    isolation: isolate;
    --moldura-recorte: var(--recorte-botao-pixel);
    --moldura-bisel: var(--sombra-pedra-pequena-interna);
    --moldura-sombra: 4px 4px 0 rgba(0, 0, 0, 0.4);
    --moldura-contorno: var(--cor-pedra-borda);
    --moldura-pedra: var(--cor-superficie-baixa);
    --prego-lado: 8px;
    --prego-margem: 6px;
    --cor-prego: transparent;
    --cor-prego-luz: transparent;
    --cor-prego-sombra: transparent;
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    overflow: hidden;
  }
  #talentFicha[hidden] {
    display: none;
  }
  /* O texto é o mesmo `conteudoDaTooltip` do desktop, então ele chega com as
     classes da tooltip — aqui elas ganham o tamanho de quem tem espaço: 11px
     contra os 10px do balão. */
  #talentFichaTexto {
    display: flex;
    flex-direction: column;
    gap: 3px;
    /* A área que rola dentro da ficha (ver #talentFicha): `min-height: 0` é o
       de sempre — sem ele um filho flex nunca fica menor que o conteúdo. */
    overflow-y: auto;
    min-height: 0;
    font-size: 11px;
    line-height: 1.35;
    color: var(--cor-texto-fraco);
  }
  #talentFichaTexto .talent-tooltip-nome {
    font-size: 13px;
    color: var(--cor-branco);
  }
  .talent-ficha-vazio {
    color: var(--cor-cinza-medio);
  }
  /* Quantos ranks o nó tem e quantos já foram pagos. O "2/3" continua dentro do
     nó, mas ali ele é de 10px num alvo de 38px que já carrega o ícone. */
  #talentFichaRanks {
    display: flex;
    gap: 3px;
  }
  .talent-ficha-rank {
    width: 12px;
    height: 12px;
    background: var(--cor-superficie-icone);
    box-shadow: inset 0 0 0 1px var(--cor-superficie);
  }
  .talent-ficha-rank-cheio {
    background: var(--talent-arvore-cor);
    box-shadow: none;
  }
  /* O botão que compra — no celular ele é o **único** caminho de compra. Colado
     no fim da ficha (`margin-top: auto`) para ficar sempre no mesmo lugar,
     independentemente do tamanho do texto do talento: o polegar aprende a
     posição e não persegue o botão. Os dois ids são necessários pelo mesmo
     motivo de `#talentTreeSummary #btnTalentTreeReset`: a regra genérica
     `#menu button:not(...)` também tem id. */
  #talentFicha #btnTalentFichaComprar {
    margin-top: auto;
    width: 100%;
    padding: 10px 8px;
    font-size: 11px;
    /* Ouro do resto da progressão (o mesmo de #btnDicaTalentosAbrir): a moldura
       em pixel art do fim do arquivo se pinta com `background: inherit`, então
       basta a cor de fundo — borda é a moldura que dá. */
    background: var(--cor-ouro);
    color: var(--cor-texto-sobre-tier);
  }
  /* Bloqueado ou maxado. "Sem ponto" **não** cai aqui: ali o botão continua
     ativo, para que o clique dispare o aviso da recusa (ver renderFicha).
     `[aria-disabled]` e não `:disabled` porque o botão precisa continuar na
     coleta do motor de foco — no celular ele é o vizinho do nó em foco, e um
     botão que some a cada nó bloqueado mudaria o caminho do direcional a cada
     passo. Quem recusa a compra é `onComprarDaFicha`. */
  #talentFicha #btnTalentFichaComprar[aria-disabled="true"] {
    background: var(--cor-superficie);
    color: var(--cor-cinza-medio);
    cursor: default;
  }
  #btnTalentFichaComprar[hidden] {
    display: none;
  }
  /* Abaixo de 700px de largura o painel do menu perde o teto de altura e cresce
     com o conteúdo — quem rola é a visão inteira (ver `body.tela-estreita
     .menu-painel`). Nessa faixa a ficha esticada até o fim da árvore levava o
     botão de comprar para o pé de uma tela rolável, longe do texto que ele
     compra. Colada no topo (`sticky`) e do tamanho do próprio conteúdo, ela
     acompanha a rolagem: o talento e o botão dele ficam juntos, e quem passa é
     a árvore. */
  body.touch-mode.tela-estreita #talentFicha {
    align-self: flex-start;
    position: sticky;
    top: 0;
  }
  body.touch-mode.tela-estreita #talentFicha #btnTalentFichaComprar {
    margin-top: 4px;
  }

  /* z-index 22 (um acima das outras modais, que ficam em 21): na sala de
     espera da Defesa da base o painel de talentos é movido para dentro de
     #hordeLobbyTalentsOverlay, que também é 21 e vem depois no HTML — com
     empate, a confirmação abria **atrás** da árvore e não dava para clicar. */
  #talentResetConfirmOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 22;
  }
  #talentResetConfirmOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #talentResetConfirmOverlay .modal {
    padding: 20px 24px;
    width: 360px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  #talentResetConfirmOverlay h2 {
    margin: 0;
    font-size: 18px;
  }
  #talentResetConfirmOverlay p {
    margin: 0;
    font-size: 13px;
    color: var(--cor-texto-fraco);
  }
  #talentResetConfirmActions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
  }
  #talentResetConfirmActions button {
    padding: 10px 20px;
    font-size: 14px;
  }
  #btnTalentResetCancel {
    background: var(--cor-superficie);
    color: var(--cor-texto);
  }
  #btnTalentResetCancel:hover {
    background: var(--cor-superficie-hover);
  }
  #btnTalentResetConfirm {
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #btnTalentResetConfirm:hover {
    background: var(--cor-vermelho-hover);
  }

  /* ----- Confirmação do treino (ver public/js/confirmacaoDeTreino.js) -----
     Mesma casca de #talentResetConfirmOverlay, no z-index das modais do menu
     (20): ela abre por cima do acampamento e nunca cruza com outra. */
  #treinoConfirmOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 20;
  }
  #treinoConfirmOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #treinoConfirmOverlay .modal {
    padding: 20px 24px;
    width: 360px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  /* Título e ✕ na mesma linha, como o cabeçalho das outras modais
     (#creditsHeader e os irmãos dele). O desenho do ✕ é `.modal-close`, uma
     regra só perto do fim do arquivo. */
  #treinoConfirmHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
  }
  #treinoConfirmOverlay h2 {
    margin: 0;
    font-size: 18px;
  }
  #treinoConfirmOverlay p {
    margin: 0;
    font-size: 13px;
    color: var(--cor-texto-fraco);
  }
  /* Uma ação só, na largura inteira: ela não divide a linha com ninguém, e um
     botão curto encostado à direita leria como o menor dos dois caminhos. Quem
     desiste usa o ✕ do cabeçalho. */
  #treinoConfirmActions {
    display: flex;
  }
  #treinoConfirmActions button {
    flex: 1 1 0;
    min-width: 0;
    padding: 10px 12px;
    font-size: 14px;
    white-space: nowrap;
  }
  /* A cor é a dos botões de ação do jogo. A moldura de fliperama vem do bloco
     de `--recorte-botao-pixel` no fim do arquivo, onde os botões desta modal
     estão listados — sem entrar lá, um botão do jogo sai com a caixa do
     navegador. */
  #btnTreinoConfirm {
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #btnTreinoConfirm:hover {
    background: var(--cor-vermelho-hover);
  }
  /* O encaixe do modo debug: separado do par acima por um filete, porque não é
     uma terceira opção da mesma pergunta — é a porta de quem faz o jogo, e só
     existe no DOM com o sinal (ver initHordeDebugTool). */
  #treinoConfirmDebug {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding-top: 12px;
    border-top: 2px solid var(--cor-borda);
  }
  .treino-debug-nota {
    margin: 0;
    font-size: 12px;
    color: var(--cor-texto-fraco);
  }
  .treino-debug-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 12px;
    font-size: 14px;
    background: var(--cor-superficie);
    color: var(--cor-texto);
  }
  .treino-debug-btn:hover {
    background: var(--cor-superficie-hover);
  }

  /* ----- Confirmação de abandono (ver public/js/pauseMenu.js) -----
     Mesma casca de #talentResetConfirmOverlay. Ela **substitui** o menu da
     partida (o JS fecha um antes de abrir a outra), então o z-index só precisa
     ser o das modais do jogo — mas fica acima do #pauseOverlay mesmo assim,
     para o cruzamento das duas nunca aparecer invertido. */
  #leaveConfirmOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 41;
  }
  #leaveConfirmOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #leaveConfirmOverlay .modal {
    padding: 20px 24px;
    width: 360px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  #leaveConfirmOverlay h2 {
    margin: 0;
    font-size: 18px;
  }
  #leaveConfirmOverlay p {
    margin: 0;
    font-size: 13px;
    color: var(--cor-texto-fraco);
  }
  /* O que se perde é a informação da janela: o resto do texto é contexto. */
  #leaveConfirmOverlay p strong {
    color: var(--cor-moeda);
  }
  /* Os dois botões ficam lado a lado — a escolha é entre eles, e empilhados
     viravam uma lista de duas ações soltas. Sem `flex-wrap`: o par nunca quebra
     em duas linhas, e é a largura de cada botão que cede (`flex: 1` + o padding
     lateral menor) quando a tela é estreita. */
  #leaveConfirmActions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
  }
  #leaveConfirmActions button {
    flex: 1 1 0;
    min-width: 0;
    padding: 10px 12px;
    font-size: 14px;
    white-space: nowrap;
  }
  #btnLeaveConfirmCancel {
    background: var(--cor-superficie);
    color: var(--cor-texto);
  }
  #btnLeaveConfirmCancel:hover {
    background: var(--cor-superficie-hover);
  }
  #btnLeaveConfirmOk {
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #btnLeaveConfirmOk:hover {
    background: var(--cor-vermelho-hover);
  }

  /* ----- Menu da partida (pausa), ver public/js/pauseMenu.js -----
     Mesma casca das outras modais do jogo. Fica **fora** de #menu (irmão dele,
     como #game-wrap): é a única modal que abre com o menu inicial escondido,
     e dentro dele o `display: none` de showGame a levaria junto. */
  #pauseOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    /* Acima do HUD e dos overlays de fim de jogo/espera (≤10), como as outras
       modais, e abaixo do portão de tela cheia (900). */
    z-index: 20;
  }
  #pauseOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #pauseOverlay .modal {
    padding: 20px 24px;
    /* A mesma largura da modal de configurações do menu inicial: as duas
       hospedam o **mesmo** corpo, e a fileira de cinco abas não cabe em 340px
       (era essa a largura de quando esta modal tinha três linhas de volume) nem
       em 520 — ver a medição no comentário de `#configuracoesOverlay .modal`. */
    width: 560px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
    /* Quem rola é o painel da seção aberta, não a modal: o "✕" do cabeçalho, a
       fileira de abas e o "Sair da partida" do pé precisam ficar à vista. */
    overflow: hidden;
  }
  #pauseHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  #pauseHeader h2 {
    margin: 0;
    font-size: 22px;
  }
  /* Hospedeiro da tela de configurações (ver pauseMenu.js), mais o "Sair da
     partida" que continua sendo HTML estático e o **último** elemento daqui. A
     corrente de `flex: 1` + `min-height: 0` vai da modal até o painel da seção,
     que é quem rola — sem o `min-height: 0` o mínimo automático do item flex é o
     conteúdo dele, e o botão de sair seria empurrado para fora da modal. */
  #pauseBody {
    display: flex;
    flex-direction: column;
    gap: 16px;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
  }
  #pauseBody .tela-config {
    flex: 1 1 auto;
  }
  #btnPauseLeave {
    flex: none;
  }
  /* Vermelho como os botões de ação principais do jogo: sair no meio de uma
     partida é a ação de peso desta modal, não uma opção neutra ao lado dos
     volumes — por isso também fica separada deles, no fim. */
  #btnPauseLeave {
    margin-top: 4px;
    padding: 10px 16px;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
    font-size: 15px;
    font-weight: 700;
    cursor: pointer;
  }
  #btnPauseLeave:hover {
    background: var(--cor-vermelho-hover);
  }

  /* ----- Tela de configurações, ver public/js/telaDeConfiguracoes.js -----
     Duas partes: a **modal do menu inicial** (#configuracoesOverlay, a casca —
     mesma casca das outras modais do jogo) e o **corpo** (`.tela-config`, a
     fileira de abas mais o painel da seção aberta), que é montado por JS e
     aparece em **dois** hospedeiros: esta modal e o #pauseBody do menu da
     partida.

     É por isso que tudo abaixo é escopado na classe da tela, e não num id de
     hospedeiro: um seletor por hospedeiro viria junto com o terceiro. É a mesma
     razão pela qual o botão de mudo de 28px trocou `#musicPanel`/`#pauseBody`
     por `.tela-config` lá em cima. */
  #configuracoesOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 20;
  }
  #configuracoesOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #configuracoesOverlay .modal {
    padding: 20px 24px;
    /* Mais larga que a de créditos (420) porque ela carrega a fileira de cinco
       abas, e a largura saiu de **medição**: em 420px elas quebram em duas
       fileiras já no desktop, e em 520 também — a soma dos cinco rótulos com os
       vãos dava 508px contra os 472 de área útil (520 menos o padding), e
       "Acessibilidade" caía sozinha numa segunda linha, esticada pelo
       `flex: 1 1 auto`. Numa fileira de abas isso não é só feio: quem navega no
       controle anda para a **direita** e não alcança a última sem um passo
       vertical que a fileira não anuncia. */
    width: 560px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
    /* Quem rola é o painel da seção, não a modal inteira: o cabeçalho com o "✕"
       e a fileira de abas precisam ficar à vista o tempo todo — mesmo padrão da
       modal de créditos e da de novidades. */
    overflow: hidden;
  }
  #configuracoesHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  #configuracoesHeader h2 {
    margin: 0;
    font-size: 22px;
  }
  #configuracoesBody {
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
  }

  /* --- O corpo (o componente) --- */
  .tela-config {
    display: flex;
    flex-direction: column;
    gap: 12px;
    min-height: 0;
  }
  /* A fileira de abas. O material é o da fileira do Ranking (`.ranking-aba`) e
     dos botões de ícone: silhueta recortada em escadinha e contorno na cor da
     borda, com a acesa em vermelho — é a cor de "esta é a que está na tela" no
     menu inteiro. Ela **quebra** em vez de espremer: são cinco rótulos, e no
     celular deitado eles não cabem numa linha. */
  .config-abas {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    flex: none;
  }
  .config-aba {
    --cor-fundo-aba: var(--cor-superficie);
    --cor-contorno-aba: var(--cor-pedra-borda);
    flex: 1 1 auto;
    /* Alvo de toque: 44px é o mínimo do projeto, e as abas são a peça mais
       clicada desta tela. */
    min-height: 44px;
    min-width: 0;
    /* 8px de recuo lateral, e não os 12 de um botão de aba comum: são cinco
       numa fileira, então cada pixel de padding é cobrado cinco vezes — os 4px
       que saíram de cada lado devolvem 40px à linha. O alvo de toque continua
       de pé pelo `min-height` e pela largura do rótulo. */
    padding: 8px 8px;
    border: none;
    border-radius: 0;
    background-color: transparent;
    clip-path: var(--recorte-icone-pixel);
    background-image:
      linear-gradient(var(--cor-fundo-aba), var(--cor-fundo-aba)),
      linear-gradient(var(--cor-fundo-aba), var(--cor-fundo-aba)),
      linear-gradient(var(--cor-contorno-aba), var(--cor-contorno-aba));
    background-size:
      calc(100% - 4px) calc(100% - 12px),
      calc(100% - 12px) calc(100% - 4px),
      100% 100%;
    background-position: 2px 6px, 6px 2px, 0 0;
    background-repeat: no-repeat;
    color: var(--cor-texto-medio);
    font-family: inherit;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    cursor: pointer;
  }
  .config-aba:hover {
    --cor-contorno-aba: var(--cor-borda-clara);
    color: var(--cor-texto);
  }
  .config-aba-ativa,
  .config-aba-ativa:hover {
    --cor-fundo-aba: var(--cor-vermelho-destaque);
    --cor-contorno-aba: var(--cor-vermelho-destaque);
    color: var(--cor-texto);
  }
  /* O painel da seção aberta. É ele que rola — ver o `overflow: hidden` da
     modal acima. */
  .config-painel {
    display: flex;
    flex-direction: column;
    gap: 14px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* `overflow-y: auto` faz o `overflow-x` **computar** para `auto`, então
       qualquer coisa que passe 1px da borda direita abre uma barra de rolagem
       horizontal no pé do painel — e ela consome altura, o que muda o tamanho
       de tudo. É o cinto de segurança que a regra "hover de botão dentro de
       área rolável" já pede; aqui ele foi cobrado de verdade (ver a margem do
       slider abaixo). */
    overflow-x: hidden;
  }
  /* **O slider zera a margem.** O Chrome dá `margin: 2px` de folha própria a
     `input[type="range"]`, e num slider de largura cheia isso soma 4px a uma
     caixa que já mede 100% do painel: medido, ele ia a 978px numa área útil de
     976 e abria a barra de rolagem horizontal nas seções Vídeo e Controles.
     A folga vertical que a margem dava volta pelo `gap` do painel, que é quem
     espaça as linhas aqui. */
  .tela-config input[type="range"] {
    margin: 0;
  }
  /* A linha de opção que não é volume: rótulo à esquerda, controle à direita.
     Ela reusa `.horde-toggle-row` (a mesma linha da sala de espera e do picker
     de criação de sala), porque é a mesma pergunta com a mesma peça. */
  .config-linha {
    align-items: center;
    min-height: 44px;
  }
  .config-rotulo {
    display: block;
    font-size: 13px;
    color: var(--cor-texto-suave);
  }
  /* A confirmação escrita de uma ação ("A próxima partida abre com o
     tutorial."): ela é legenda, não controle — mesma cor e mesmo peso do
     `.opcao-estado` que legendava o interruptor de vibração. */
  .config-acao-feito:not(:empty) {
    display: block;
    margin-top: 2px;
    font-size: 11px;
    color: var(--cor-sucesso);
  }
  /* Rótulo e valor de um slider numérico na mesma linha: o número existe porque,
     ao contrário do volume, não há como ouvir o resultado sem sair da tela. */
  .config-cabeca {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
  }
  .config-valor {
    font-size: 13px;
    color: var(--cor-texto);
    font-variant-numeric: tabular-nums;
  }
  /* A escolha entre valores nomeados é uma **fileira de botões**, e não um
     `<select>`: o motor de navegação por foco não opera `<select>` (só
     `<input type="range">` consome a direção, ver passoDeValor.js), então no
     controle a lista abriria e o direcional sairia dela em vez de trocar a
     opção. Material: o dos botões de ícone, um degrau abaixo das abas. */
  .config-opcoes {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }
  .config-opcao {
    --cor-fundo-campo: var(--cor-superficie);
    --cor-borda-campo: var(--cor-pedra-borda);
    flex: 1 1 auto;
    min-height: 44px;
    min-width: 0;
    padding: 6px 10px;
    border: none;
    border-radius: 0;
    background-color: transparent;
    clip-path: var(--recorte-icone-pixel);
    background-image:
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo));
    background-size:
      calc(100% - 4px) calc(100% - 12px),
      calc(100% - 12px) calc(100% - 4px),
      100% 100%;
    background-position: 2px 6px, 6px 2px, 0 0;
    background-repeat: no-repeat;
    color: var(--cor-texto-medio);
    font-family: inherit;
    font-size: 13px;
    cursor: pointer;
  }
  .config-opcao:hover {
    --cor-borda-campo: var(--cor-borda-clara);
    color: var(--cor-texto);
  }
  /* Escolhida é uma troca de cor só, nada muda de lugar — o mesmo princípio dos
     botões de ícone e da linha de lista com contorno. */
  .config-opcao-ativa,
  .config-opcao-ativa:hover {
    --cor-borda-campo: var(--cor-vermelho-destaque);
    color: var(--cor-texto);
  }
  /* O interruptor é o `.toggle-switch` de sempre — trilho de 52x26. No dedo a
     caixa de toque cresce para 44px de altura sem mexer no trilho: o desenho é
     um `<span>` dentro do botão, então o que estica é a área clicável em volta
     dele. O trilho fica centrado nela pelo `display: flex` da regra base do
     `.toggle-switch`… que não existe, então ele é declarado aqui. */
  body.touch-mode .tela-config .toggle-switch {
    min-height: 44px;
    display: flex;
    align-items: center;
  }
  /* O slider de valor (efeitos, música, escala do HUD, zona morta, curva do
     analógico — todo `<input type="range">` da tela, `.sound-settings-row`)
     ficou de fora da rodada de 44px: a trilha nativa mede ~16px de altura e a
     caixa de clique é exatamente a trilha, então no dedo ele fica abaixo do
     mínimo do projeto — medido em 844x390 com Playwright
     (`getBoundingClientRect`, não CSS). Mesmo truque do `.toggle-switch`
     acima: o que cresce é só a caixa de toque, a trilha visual continua fina
     (o navegador desenha ela centralizada verticalmente). */
  body.touch-mode .tela-config .sound-settings-row input[type="range"] {
    min-height: 44px;
  }
  /* O botão de uma ação (refazer o tutorial, rever as dicas) não estica: ele
     mora numa linha de rótulo + controle, e esticado ele comeria o rótulo. */
  .config-acao {
    flex: none;
  }
  /* "Restaurar padrões" desta seção. Ele é a última linha do painel e é
     **secundário** de propósito: o vermelho de ação principal ali leria como o
     botão que a seção existe para ter, quando ela existe para ajustar opções. */
  .config-restaurar {
    flex: none;
    align-self: flex-start;
    margin-top: 2px;
    min-height: 44px;
    padding: 8px 14px;
    border: none;
    border-radius: 0;
    background: var(--cor-botao-secundario);
    color: var(--cor-texto);
    font-family: inherit;
    font-size: 13px;
    cursor: pointer;
  }
  .config-restaurar:hover {
    background: var(--cor-botao-secundario-hover);
  }
  /* Celular deitado (ver "Faixas de tamanho de tela"): a altura é o recurso
     escasso, então os rótulos das abas encolhem e o painel rola. Nada de alvo
     abaixo de 44px — o que cede é fonte e recuo, nunca a caixa de toque. */
  body.tela-baixa-deitada #configuracoesOverlay .modal {
    padding: 10px 14px;
    gap: 8px;
  }
  body.tela-baixa-deitada #configuracoesHeader h2 {
    font-size: 16px;
  }
  body.tela-baixa-deitada .tela-config {
    gap: 8px;
  }
  body.tela-baixa-deitada .config-aba,
  body.tela-baixa-deitada .config-opcao {
    font-size: 11px;
    padding: 6px 8px;
  }
  body.tela-baixa-deitada .config-painel {
    gap: 10px;
  }

  #creditsOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 20;
  }
  #creditsOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #creditsOverlay .modal {
    padding: 20px 24px;
    width: 420px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
    /* Quem rola é o corpo, não a modal inteira: o cabeçalho com o botão de
       fechar precisa ficar visível o tempo todo (mesmo padrão da modal de
       novidades). */
    overflow: hidden;
  }
  #creditsHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  #creditsHeader h2 {
    margin: 0;
    font-size: 22px;
  }
  /* O desenho de todos eles é `.modal-close`, uma regra só perto do fim do
     arquivo — eram cinco cópias idênticas desta mesma declaração, uma por
     modal. */
  #creditsBody {
    display: flex;
    flex-direction: column;
    gap: 10px;
    overflow-y: auto;
  }
  .creditos-secao {
    background: var(--cor-superficie-baixa);
    border-radius: 0;
    padding: 10px 14px 14px;
  }
  .creditos-secao p {
    margin: 0 0 4px;
    font-size: 14px;
    line-height: 1.5;
  }
  .creditos-secao a {
    color: var(--cor-azul-link);
    font-size: 13px;
    word-break: break-all;
  }
  /* A linha de licença é obrigação legal (CC BY 4.0 na trilha), não recado
     de rodapé: fica menor e mais apagada que o crédito em si, mas legível —
     esconder ou clarear demais a descumpriria. */
  .creditos-licenca {
    margin-top: 8px;
    font-size: 12px;
    color: var(--cor-texto-suave);
  }
  .creditos-divisor {
    border: none;
    border-top: 1px solid var(--cor-superficie-hover);
    margin: 10px 0;
  }

  /* ----- Página isolada (redefinição de senha) ----- */
  body.standalone-page {
    justify-content: flex-start;
    padding: 48px 16px;
    box-sizing: border-box;
  }
  .standalone-card {
    width: 360px;
    max-width: 100%;
    display: flex;
    flex-direction: column;
    gap: 16px;
    background: var(--cor-superficie-alta);
    border: 2px solid var(--cor-borda-media);
    border-radius: 0;
    padding: 24px;
    box-sizing: border-box;
  }
  .standalone-card h1 {
    margin: 0;
    text-align: center;
  }
  .standalone-card h1 img {
    width: 120px;
    max-width: 100%;
    height: auto;
  }
  .standalone-card h2 {
    margin: 0;
    font-size: 18px;
    color: var(--cor-texto-fraco);
    text-align: center;
    font-weight: normal;
  }
  .standalone-card form {
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  /* Página isolada (reset-password.html não passa por mobileBlock.js/
     main.js, então não tem `body.touch-mode` disponível para escopar isto):
     a folga vertical incondicional já deixa a área de toque perto de 44px em
     qualquer dispositivo, sem depender de detecção de touch. */
  #resetBackLink {
    display: block;
    text-align: center;
    font-size: 14px;
    padding: 12px 4px;
    color: var(--cor-azul-link);
  }

  /* ----- Modal de novidades (histórico de versões) -----
     Mesma carcaça da modal de créditos, só mais larga e com o corpo rolável:
     o conteúdo é texto corrido e cresce a cada versão publicada. */
  #changelogOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 20;
  }
  #changelogOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #changelogOverlay .modal {
    padding: 20px 24px;
    width: 740px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
    /* Quem rola é o corpo, não a modal inteira: o cabeçalho com o botão de
       fechar precisa ficar visível o tempo todo. */
    overflow: hidden;
  }
  #changelogHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  #changelogHeader h2 {
    margin: 0;
    font-size: 22px;
  }
  /* A faixa "as notas só existem em português", fora do português (ver
     `data-fora-do-portugues` em public/js/idioma.js). Sem `display` aqui: é o
     `hidden` do HTML que a esconde em português. */
  #changelogAvisoDeIdioma {
    margin: 0;
    padding: 8px 12px;
    font-size: 13px;
    background: var(--cor-superficie-baixa);
    border-left: 4px solid var(--cor-ouro);
  }
  /* O desenho de todos eles é `.modal-close`, uma regra só perto do fim do
     arquivo — eram cinco cópias idênticas desta mesma declaração, uma por
     modal. */
  #changelogBody {
    display: flex;
    flex-direction: column;
    gap: 12px;
    overflow-y: auto;
    padding-right: 4px;
  }
  .changelog-versao {
    display: flex;
    flex-direction: column;
    gap: 6px;
    background: var(--cor-superficie-baixa);
    border-radius: 0;
    padding: 14px 18px 18px;
  }
  .changelog-versao-cabecalho {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 10px;
    border-bottom: 1px solid var(--cor-borda);
    padding-bottom: 8px;
    margin-bottom: 4px;
  }
  .changelog-versao-cabecalho h3 {
    font-family: 'Cinzel', serif;
    font-size: 20px;
    margin: 0;
    color: var(--cor-texto);
  }
  .changelog-tag {
    font-size: 12px;
    color: var(--cor-sucesso);
    border: 1px solid var(--cor-sucesso);
    border-radius: 0;
    padding: 1px 8px;
  }
  .changelog-data {
    margin-left: auto;
    font-size: 13px;
    color: var(--cor-texto-fraco);
  }
  .changelog-versao h4 {
    margin: 12px 0 0;
    font-size: 15px;
    color: var(--cor-vermelho-destaque);
  }
  .changelog-versao p {
    margin: 0;
    font-size: 14px;
    line-height: 1.5;
    color: var(--cor-texto-suave);
  }
  .changelog-resumo {
    color: var(--cor-texto-medio);
  }
  /* Tabela de mudanças de atributos por classe (tópico "Equilíbrio das
     classes"). O invólucro rola no eixo X porque a coluna de mudanças é longa
     e não pode empurrar a modal inteira para os lados numa tela estreita. */
  .changelog-tabela-rolagem {
    margin-top: 10px;
    overflow-x: auto;
  }
  .changelog-tabela {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
    line-height: 1.45;
    color: var(--cor-texto-suave);
  }
  .changelog-tabela th,
  .changelog-tabela td {
    padding: 6px 8px;
    text-align: left;
    vertical-align: top;
    border: 1px solid var(--cor-borda);
  }
  .changelog-tabela thead th {
    background: var(--cor-superficie-icone);
    color: var(--cor-texto-medio);
  }
  .changelog-tabela tbody th {
    white-space: nowrap;
    color: var(--cor-texto);
  }
  .changelog-tabela strong {
    color: var(--cor-texto);
  }
  /* Screenshots do jogo ilustrando um tópico, como a partida real em
     "Partida online 1x1" e a faixa do tutorial em "Modo treino e tutorial". */
  .changelog-topico-imagem {
    display: block;
    width: 100%;
    margin-top: 10px;
    border-radius: 0;
  }

  /* Os quatro personagens da sala, um por cor de slot, no tópico da cor da
     túnica (ver changelog.js). Cada um é uma `.class-sprite` — a mesma tira
     animada por `background-position` do lobby e do fim de partida — com a
     janela maior e o zoom alto do lobby, porque o corpo do Little Mage ocupa
     16px de um quadro de 100x100. O fundo é o mesmo véu de pedra do cartão da
     sala de espera, com a mesma sombra dura: é o lugar onde o jogador vê esses
     quatro personagens de verdade, e sobre o fundo liso da modal eles ficavam
     boiando. */
  /* A galeria percorre CORES_DE_JOGADOR inteira, e a paleta cresceu de quatro
     para oito quando a cor virou escolha do jogador: oito bonecos de 88px não
     cabem na largura da modal, então ela quebra em duas fileiras em vez de
     empurrar os das pontas para fora. */
  .changelog-magos {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    margin-top: 10px;
    padding: 8px;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
  }
  .changelog-mago.class-sprite {
    --sprite-box: 88px;
    --sprite-zoom: 5.2;
    --sprite-anchor: 0.52;
  }

  /* ===== Faixas de tamanho de tela =====
     Estas quatro faixas (`tela-larga`, `tela-estreita`, `tela-muito-estreita`,
     `tela-baixa-deitada`) eram `@media` por largura/altura/orientação, e não
     podem mais ser: em modo touch com o aparelho em retrato o conteúdo é girado
     90° (ver `body.tela-girada`), então o navegador reporta uma tela estreita e
     em pé enquanto o layout é largo e deitado — as regras de "celular em
     retrato" cairiam justo no layout deitado, e as de "paisagem curta", que são
     as certas, nunca valeriam. Quem mede é public/js/orientation.js, com os
     eixos já trocados quando é o caso; os limites (1300/700/420/430px) são os
     mesmos dos `@media` que estas classes substituem, e numa janela de desktop
     o resultado é idêntico ao de antes.

     A ordem entre as faixas continua importando como importava entre os
     `@media`: `tela-estreita` e `tela-baixa-deitada` podem valer as duas ao
     mesmo tempo e definem algumas propriedades em comum (padding de
     `.menu-view`, tamanho do título), então a de baixo é a que ganha. */

  /* Tela estreita: o título tem a largura toda pra ele, então pode crescer
     mais do que cresce quando divide a tela com outra coisa. A faixa mexe só
     na régua da capa (`--capa-logo`); a linha de botões sai dela sozinha. */
  body.tela-estreita #menuViewHome {
    --capa-logo: clamp(56px, calc(0.18 * var(--largura-tela)), 92px);
  }
  body.tela-estreita #menu button:not(.ranking-nome):not(.link-button):not(.horde-level-item):not(.toggle-switch):not(.monstruario-row):not(.talent-no):not(.talent-aba):not(.ranking-aba):not(.config-aba):not(.config-opcao) {
    min-width: 0;
    width: 100%;
    box-sizing: border-box;
  }
  /* Nessa largura nada cabe lado a lado: as duas visões empilham o conteúdo e
     rolam por dentro delas mesmas quando não couber em pé (ver `overflow-y`
     de .menu-view). Alinhadas pelo topo, e não centralizadas: com conteúdo
     maior que a tela, centralizar esconderia o começo atrás do início do
     scroll. */
  body.tela-estreita .menu-view {
    padding: 12px 16px;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
  }
  body.tela-estreita #menuViewHome {
    gap: 24px;
  }
  /* Nessa largura os dois modos não cabem lado a lado: as duas linhas do
     índice viram coluna, e cada botão pega a largura toda (o `width: 100%` da
     regra genérica de botão desta faixa). A linha das ações secundárias perde
     o estreitamento junto — 72% de uma coluna estreita viraria um botão
     visivelmente torto no meio da pilha. */
  body.tela-estreita #menuBody {
    width: var(--capa-linha);
  }
  body.tela-estreita #menuModos,
  body.tela-estreita #menuTitleExtras {
    flex-direction: column;
  }
  /* Nessa largura o container padrão deixa de ter tamanho padrão: ele pega a
     largura toda e cresce com o conteúdo, e quem rola para alcançá-lo é a
     visão (`overflow-y` de .menu-view). Espremido pelo flex ele comeria a
     área rolável de dentro (a faixa das árvores, a lista de inimigos) até ela
     sumir; com o teto de altura de sempre, as colunas empilhadas passariam
     dele e a moldura cortaria a de baixo no meio. */
  body.tela-estreita .menu-painel {
    flex-shrink: 0;
    width: 100%;
    height: auto;
    max-height: none;
    overflow: visible;
  }
  /* As duas colunas empilham nessa largura: no Monstruário a lista de inimigos
     em cima e a ficha embaixo. */
  body.tela-estreita .menu-painel-cols {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    gap: 16px;
  }
  body.tela-estreita #monstruarioListPanel,
  body.tela-estreita #monstruarioDetailPanel {
    /* `flex: none` e não só `flex-shrink: 0`: empilhadas, as colunas trazem o
       `flex` que as faz dividir a *largura* lado a lado — numa coluna flex
       esses mesmos números passariam a dividir a **altura** da fileira, e as duas se esticariam sobre o pouco
       que a janela estreita tem. */
    flex: none;
    width: 100%;
    /* `height: auto` é obrigatório aqui: empilhadas numa coluna flex, as duas
       colunas resolveriam o `height: 100%` delas contra a altura da **fileira**
       e cada uma pediria a coluna inteira — as duas se sobrepondo, com a lista
       saindo pela borda de cima. */
    height: auto;
  }
  /* Nesta faixa a coluna de detalhe tem a largura da tela, e a janela de arte
     (306px) comeria quase tudo: a coluna de números ao lado ficaria com uma
     tira de texto quebrando em uma palavra por linha. As duas caixas do topo
     empilham, e a ficha volta a ser uma coluna — aqui quem rola é a visão
     inteira (ver `body.tela-estreita .menu-painel` acima), então altura deixa
     de ser o recurso escasso que a disposição de duas fileiras existe para
     poupar. O rodapé de "Onde aparece" continua igual: ele já era de largura
     inteira. */
  body.tela-estreita .monstruario-carta-e .monstruario-fileira-topo {
    grid-template-columns: minmax(0, 1fr);
  }
  body.tela-estreita .monstruario-carta-e .monstruario-carta-arte {
    width: 100%;
  }
  /* Empilhadas, a divisória vertical entre as colunas não é mais divisória
     nenhuma: ela viraria um risco na lateral da coluna de baixo. O que separa
     as duas passa a ser a borda de cima. */
  body.tela-estreita #monstruarioListPanel {
    padding-right: 0;
  }
  body.tela-estreita #monstruarioDetailPanel {
    padding-left: 0;
    padding-top: 16px;
    border-left: none;
    border-top: 2px solid var(--cor-borda);
  }
  /* Tela deitada e curta (ex.: 812x375, e todo celular em modo touch, girado ou
     não): altura é o recurso escasso, não largura — as duas faixas acima
     (700/420px) não cobrem esse caso, porque miram largura. Altura baixa
     sozinha pegaria também um desktop com janela baixa, então a faixa exige
     largura maior que altura, para valer só onde a tela é de fato deitada.
     Reduz só o que é decorativo (título, respiro entre painéis) e o padding
     interno dos painéis/modais — nenhum botão de ação ou conteúdo interativo é
     escondido aqui. */
  body.tela-baixa-deitada .menu-view {
    padding: 8px 44px;
    gap: 16px;
  }
  /* Aqui a altura é o recurso escasso, então o vão entre logo e opções encolhe
     — eles já ficam lado a lado por padrão. */
  /* Nesta faixa a carta do Monstruário inteira (janela de arte de 260px mais o
     nome, os atributos e as fases) é mais alta que a coluna, e a ficha abria
     pedindo rolagem para ver o bicho — que é a primeira coisa que ela existe
     para mostrar. O retrato encolhe junto: os três números descem na mesma
     proporção (0,62), que é o que mantém a garantia de nada ficar cortado — a
     janela é dimensionada em cima do corpo (ver monstruario.js). */
  body.tela-baixa-deitada #monstruarioPainel {
    --retrato-corpo: 82;
    --retrato-janela-l: 224;
    --retrato-janela-a: 162;
  }
  /* E aqui as duas caixas do topo empilham, como em `tela-estreita` e pelo
     mesmo motivo medido: a coluna de detalhe tem ~346px de largura, então ao
     lado do retrato sobram ~150px para o texto — cada valor de atributo quebra
     em três linhas, a coluna de números sozinha passa de 380px e o rótulo fica
     mais largo que o valor. Encolher o retrato não salva a fileira: com a
     janela em 224, 168, 134 e 112px a carta mede os **mesmos** 558px, porque o
     que manda na altura aqui é a quebra do texto, não o desenho. Empilhada ela
     mede 536px e o texto volta a ser legível.

     Ou seja: nesta faixa a disposição de duas fileiras não paga, e a ficha
     continua rolando como já rolava antes dela. A saída medida para 844x390 é
     a coluna da lista virar uma tira compacta de miniaturas — ela devolveria
     ~300px ao detalhe e faria a fileira de cima caber —, e ela não existe hoje:
     ver docs/sugestoes-ficha-do-monstruario.html, aba "No celular". */
  body.tela-baixa-deitada .monstruario-carta-e .monstruario-fileira-topo {
    grid-template-columns: minmax(0, 1fr);
  }
  body.tela-baixa-deitada .monstruario-carta-e .monstruario-carta-arte {
    width: 100%;
  }

  /* O vão entre o logo e os botões também é fração da altura nesta faixa: em
     375px de altura os 24px fixos eram os que punham a última linha 5px para
     fora da tela e faziam a capa pedir rolagem. */
  body.tela-baixa-deitada #menuViewHome {
    gap: clamp(12px, calc(0.045 * var(--altura-tela)), 24px);
  }
  body.tela-baixa-deitada #menuViewHome {
    /* Aqui, sim, pela altura: a tela inicial é uma coluna (logo em cima, a
       porta do modo e as ações secundárias embaixo), e nesta faixa a altura é
       justamente o recurso escasso — escalado pela largura, o logo sozinho
       comia dois terços da tela e empurrava a última linha de botões para
       fora dela.

       A fração subiu de 0,17 para 0,25 da altura quando a linha de botões
       passou a sair do logo (`--capa-linha`, ver #menuViewHome). Com ela em
       0,17 o logo dava 61px num celular deitado de 780x360 — 162px de largura
       contra os 460 fixos da linha, que era o desequilíbrio a consertar.

       O segundo termo é um **orçamento de altura**, e ele entrou quando os três
       botões passaram a empilhar (ver #menuTitleExtras): empilhados eles custam
       151px fixos, mais o vão e o respiro da visão, e nesta faixa a altura vai
       até 320px (um 568x320 deitado). Só a fração de 0,25 estouraria ali — a
       coluna pediria rolagem na capa. `(altura - 195px) / 1.9` é o maior logo
       que ainda deixa o "Créditos" dentro da tela (1,9 é a altura do logo em
       função da fonte dele: duas linhas a `line-height` 0,92 mais o vão).

       Quem vence é o menor dos dois, então a fração continua mandando onde há
       altura (a 360px o logo dá 87px, ~230 de largura contra os 226 da linha) e
       o orçamento só morde nas telas mais baixas da faixa. */
    --capa-logo: clamp(44px, min(
      calc(0.25 * var(--altura-tela)),
      calc((var(--altura-tela) - 195px) / 1.9)), 100px);
  }
  /* Um celular deitado **estreito** (667x375, 568x320) cai nas duas faixas, e
     ali a capa é a única que reserva a altura da barra superior (ver
     `body.tela-estreita:has(#menuViewHome...) #menu`): a coluna tem
     `--altura-topbar` a menos para caber. Sem descontá-la do orçamento acima, a
     capa passava 41px da tela e pedia rolagem. */
  body.tela-estreita.tela-baixa-deitada #menuViewHome {
    --capa-logo: clamp(44px, min(
      calc(0.25 * var(--altura-tela)),
      calc((var(--altura-tela) - 195px - var(--altura-topbar)) / 1.9)), 100px);
  }

  body.tela-baixa-deitada #menuTitleWrap {
    gap: 10px;
  }
  /* Só o respiro interno da moldura encolhe. O teto de altura não precisa de
     desconto nenhum aqui: o container padrão é `height: 100%` da visão, que já
     é a tela menos a barra superior e o padding dela (ver .menu-painel). */
  body.tela-baixa-deitada .menu-painel {
    padding: 10px 14px 14px;
  }
  body.tela-baixa-deitada #talentTreePanel h2, body.tela-baixa-deitada .horde-rooms-view-title {
    font-size: 18px;
  }
  body.tela-baixa-deitada .modal {
    max-height: calc(var(--altura-tela) - 16px);
  }
  /* ===== Portão de tela cheia (modo touch) =====
     Ver public/js/orientation.js. Em modo touch a tela cheia é requisito, não
     conforto: a rotação por CSS gira o conteúdo do app, mas não o teclado
     virtual nem os menus do sistema — em tela cheia é o aparelho que gira os
     dois junto com o jogo. Então, sempre que o jogo devia estar em tela cheia e
     não está, este portão cobre o app inteiro com um botão que pede a tela
     cheia (o clique é o gesto do usuário que a API exige).

     Fica dentro do `<body>`, como qualquer outro `position: fixed` daqui, para
     girar junto com o resto quando `body.tela-girada` está ativo. `z-index`
     acima de todo o resto do app (overlays e HUD ficam em 10): enquanto ele
     está na tela, nada atrás deve ser tocável. */
  #fullscreenGate {
    display: none;
  }
  #fullscreenGate.visible {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 900;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 24px;
    box-sizing: border-box;
    padding: 32px 24px;
    text-align: center;
    background: var(--cor-veu-tela-cheia);
    animation: overlay-fade 0.15s ease;
  }
  #fullscreenGate p {
    max-width: 26ch;
    margin: 0;
    font-size: clamp(16px, calc(0.028 * var(--largura-tela)), 22px);
    line-height: 1.35;
    color: var(--cor-texto);
  }
  #btnEnterFullscreen {
    padding: 14px 28px;
    /* Mesmo alvo de toque mínimo dos outros botões em modo touch. */
    min-height: 48px;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
    font-size: 18px;
    font-weight: 700;
    cursor: pointer;
  }
  #btnEnterFullscreen:hover {
    background: var(--cor-vermelho-hover);
  }


  /* ===== Controles touch (joysticks virtuais + botão de escudo) =====
     Ver public/js/touchControls.js. Elementos DOM sobrepostos ao canvas (não
     desenhados nele — são elementos de interação, precisam de hit-testing de
     touchstart/touchmove/touchend e de área de toque maior que o desenho, ver
     design.md decisão 1 do change add-mobile-touch-controls). Dimensionados em
     vmin (não em coordenada de arena) para manterem tamanho de toque
     consistente independente da escala aplicada por gameScale.js.

     Só existem no DOM quando state.touchMode é verdadeiro (initTouchControls
     só roda nesse caso, ver input.js) — não precisam de seletor de modo aqui. */
  /* O tamanho é uma **escala só** (`--escala-joystick`, escrita por
     initTouchControls a partir da preferência `tamanhoDoJoystick`), e ela
     multiplica o desenho aqui **e** a área de arrasto no JS — inclusive a zona
     morta do gesto (ver `raioDoManipulo`/`zonaMortaDoToque` em
     touchControls.js). Duas medidas independentes fariam o joystick crescer na
     tela e continuar respondendo na distância antiga.

     O fallback `, 1` é obrigatório: a variável só é escrita em
     `state.touchMode`, e sem ele o `calc()` inteiro ficaria inválido nas
     telas em que ela não existe. O teto em `px` escala junto do tamanho em
     `vmin`, senão em janela grande o teto clampa e a proporção entre base e
     manípulo muda com o tamanho escolhido. */
  .touch-joystick {
    position: absolute;
    width: calc(24vmin * var(--escala-joystick, 1));
    height: calc(24vmin * var(--escala-joystick, 1));
    max-width: calc(128px * var(--escala-joystick, 1));
    max-height: calc(128px * var(--escala-joystick, 1));
    margin-left: calc(-12vmin * var(--escala-joystick, 1));
    margin-top: calc(-12vmin * var(--escala-joystick, 1));
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.08);
    border: 2px solid rgba(255, 255, 255, 0.35);
    opacity: 0;
    pointer-events: none;
    z-index: 5;
  }
  .touch-joystick.active {
    opacity: 1;
  }
  .touch-joystick-handle {
    position: absolute;
    left: 50%;
    top: 50%;
    width: calc(10vmin * var(--escala-joystick, 1));
    height: calc(10vmin * var(--escala-joystick, 1));
    max-width: calc(54px * var(--escala-joystick, 1));
    max-height: calc(54px * var(--escala-joystick, 1));
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.45);
    transform: translate(-50%, -50%);
  }
  .touch-joystick-aim .touch-joystick-handle {
    background: rgba(255, 77, 77, 0.55);
  }
  /* ===== Botões em pixel art =====
     Moldura de botão de fliperama: contorno grosso e cantos em escadinha de
     4px (--recorte-botao-pixel), no mesmo múltiplo do pixel de arte do
     canvas.

     `clip-path` não sabe recortar buraco, então a moldura é feita em duas
     camadas em vez de uma borda: as duas usam o fundo do próprio botão
     (`background: inherit`), e a diferença é que a de baixo (`::before`,
     silhueta inteira) escurece esse fundo e a de cima (`::after`) o repinta
     no tom original, recuada pela espessura da borda. Sobra o contorno — um
     tom mais escuro dele. Assim cada botão continua definindo a própria cor
     onde já definia (hover e `:disabled` inclusive) e a moldura acompanha,
     sem precisar de uma cor de borda por botão. Uma `border` normal não
     serviria: recortada pelo clip-path, ela some justamente na diagonal dos
     cantos.

     As duas camadas ficam em z-index negativo (acima do fundo do botão,
     abaixo do texto), e é `isolation: isolate` que faz isso funcionar —
     sem o contexto de empilhamento próprio elas cairiam atrás do fundo do
     painel.

     Vale só para os botões de ação. Botão-link, nome do placar, aba do placar
     do Sem fim e os botões de ícone (configurações, mudo, fechar ✕) ficam como estão:
     moldura de 4px em cima de um ✕ de 26px vira só ruído.

     Este bloco fica no fim do arquivo de propósito — precisa vir depois das
     regras que dão `border`/`border-radius` a esses mesmos botões. */
  #menu button:not(.link-button):not(.ranking-nome):not(.horde-level-item):not(.toggle-switch):not(.monstruario-row):not(.talent-no):not(.talent-aba):not(.ranking-aba):not(.config-aba):not(.config-opcao):not(.tela-fechar-botao),
  #gameOverOverlay button,
  #waitingOverlay button:not(.botao-nu),
  #btnAuthSubmit,
  #btnNicknameOk,
  #btnResetSubmit,
  #btnEnterFullscreen,
  #btnHordeRoomsJoinBack,
  #btnHordeRoomsJoinConfirm,
  #btnHordeRoomsListBack,
  #hordeRoomsListOverlay .sala-lista-entrar,
  #talentResetConfirmActions button,
  #leaveConfirmActions button,
  #treinoConfirmActions button,
  .treino-debug-btn,
  #hordeLobbyTalentsBox #btnTalentTreeReset,
  #hordeLobbyTalentsBox #btnTalentTreeRetry,
  #hordeLobbyTalentsBox #btnTalentFichaComprar,
  #lojaMesaFechar,
  #btnPauseLeave {
    position: relative;
    isolation: isolate;
    border: none;
    border-radius: 0;
    clip-path: var(--recorte-botao-pixel);
  }
  #menu button:not(.link-button):not(.ranking-nome):not(.horde-level-item):not(.toggle-switch):not(.monstruario-row):not(.talent-no):not(.talent-aba):not(.ranking-aba):not(.config-aba):not(.config-opcao):not(.tela-fechar-botao)::before,
  #gameOverOverlay button::before,
  #waitingOverlay button:not(.botao-nu)::before,
  #btnAuthSubmit::before,
  #btnNicknameOk::before,
  #btnResetSubmit::before,
  #btnEnterFullscreen::before,
  #btnHordeRoomsJoinBack::before,
  #btnHordeRoomsJoinConfirm::before,
  #btnHordeRoomsListBack::before,
  #hordeRoomsListOverlay .sala-lista-entrar::before,
  #talentResetConfirmActions button::before,
  #leaveConfirmActions button::before,
  #treinoConfirmActions button::before,
  .treino-debug-btn::before,
  #hordeLobbyTalentsBox #btnTalentTreeReset::before,
  #hordeLobbyTalentsBox #btnTalentTreeRetry::before,
  #hordeLobbyTalentsBox #btnTalentFichaComprar::before,
  #lojaMesaFechar::before,
  #btnPauseLeave::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: inherit;
    filter: brightness(var(--escurecimento-borda-botao));
    clip-path: var(--recorte-botao-pixel);
  }
  #menu button:not(.link-button):not(.ranking-nome):not(.horde-level-item):not(.toggle-switch):not(.monstruario-row):not(.talent-no):not(.talent-aba):not(.ranking-aba):not(.config-aba):not(.config-opcao):not(.tela-fechar-botao)::after,
  #gameOverOverlay button::after,
  #waitingOverlay button:not(.botao-nu)::after,
  #btnAuthSubmit::after,
  #btnNicknameOk::after,
  #btnResetSubmit::after,
  #btnEnterFullscreen::after,
  #btnHordeRoomsJoinBack::after,
  #btnHordeRoomsJoinConfirm::after,
  #btnHordeRoomsListBack::after,
  #hordeRoomsListOverlay .sala-lista-entrar::after,
  #talentResetConfirmActions button::after,
  #leaveConfirmActions button::after,
  #treinoConfirmActions button::after,
  .treino-debug-btn::after,
  #hordeLobbyTalentsBox #btnTalentTreeReset::after,
  #hordeLobbyTalentsBox #btnTalentTreeRetry::after,
  #hordeLobbyTalentsBox #btnTalentFichaComprar::after,
  #lojaMesaFechar::after,
  #btnPauseLeave::after {
    content: '';
    position: absolute;
    inset: var(--espessura-borda-botao);
    z-index: -1;
    background: inherit;
    clip-path: var(--recorte-botao-pixel);
  }
  /* ===== Campos e quadrados de nível em pixel art =====
     Mesma silhueta dos botões (--recorte-botao-pixel), mas aqui a borda tem
     cor própria — branco, cinza ou o vermelho de foco/seleção — em vez de ser
     um tom do fundo. Cada componente continua decidindo as duas cores nas
     regras dele, agora por `--cor-borda-campo`/`--cor-fundo-campo`; este bloco
     só desenha.

     O truque das duas camadas em pseudo-elemento não serve: `<input>` não tem
     ::before/::after. Então a moldura é feita em `background-image` —
     a camada de baixo pinta a cor da borda na caixa inteira e as três de cima
     repintam o miolo, recuado pela espessura. São três porque a escadinha,
     recortada em retângulos, é exatamente isto: a faixa larga do meio, a
     faixa alta e o quadrado que liga as duas nos cantos.

     A cor da borda é `background-image` (e não `background-color`) de
     propósito: o autofill do Chrome sobrescreve `background-color` do campo,
     e por baixo das camadas de imagem ele fica invisível. É o que permitiu
     tirar o `box-shadow` gigante que existia só para escondê-lo.

     Efeito colateral aceito: `box-shadow` externo não existe mais nesses
     componentes — o clip-path recorta tudo o que é pintado fora da silhueta.
     O anel de foco/seleção virou a própria cor da borda. */
  .campo-pixel,
  .horde-level-item {
    border: none;
    border-radius: 0;
    clip-path: var(--recorte-botao-pixel);
    background-color: transparent;
    background-image:
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-fundo-campo), var(--cor-fundo-campo)),
      linear-gradient(var(--cor-borda-campo), var(--cor-borda-campo));
    /* Miolo = faixa larga (sem os cantos), faixa alta (sem os cantos) e o
       quadrado do meio, todos centrados e recuados pela espessura da borda:
       8px de canto + 2px de borda = 10px de recuo em cada ponta. */
    background-size:
      calc(100% - 4px) calc(100% - 20px),
      calc(100% - 12px) calc(100% - 12px),
      calc(100% - 20px) calc(100% - 4px),
      100% 100%;
    background-position: center;
    background-repeat: no-repeat;
  }
  /* Campo de texto: a moldura fica no invólucro, não no `<input>`. Quando o
     Chrome autopreenche, ele força `background-image: none !important` no
     campo — regra de folha do navegador, que a cascata do autor não vence —
     e a moldura inteira sumiria junto, deixando o retângulo claro do
     autofill no lugar dela.

     O recuo horizontal de 10px é o que mantém o campo dentro da escadinha:
     8px de canto + 2px de borda. Assim o retângulo do input coincide com a
     faixa alta do miolo, e o único fundo que o navegador ainda controla —
     o do próprio campo — não tem como encostar na moldura. O que sobra do
     autofill é a cor, resolvida logo abaixo com um box-shadow interno, que
     é recortado pela caixa do input e por isso também não a alcança. */
  .campo-pixel {
    display: flex;
    align-items: center;
    box-sizing: border-box;
    width: 100%;
    padding: 2px 10px;
  }
  .campo-pixel input {
    flex: 1;
    min-width: 0;
    border: none;
    outline: none;
    background: transparent;
    padding: 8px 4px;
    color: var(--cor-texto);
    font-family: inherit;
  }
  /* Autofill do Chrome: o fundo claro é imposto por `!important` e não dá
     para trocar por `background`. O jeito de cobri-lo é este box-shadow
     interno, que é pintado por cima do fundo do campo — e o texto, que o
     navegador também recolore, só cede a `-webkit-text-fill-color`. */
  .campo-pixel input:-webkit-autofill,
  .campo-pixel input:-webkit-autofill:hover,
  .campo-pixel input:-webkit-autofill:focus {
    -webkit-box-shadow: 0 0 0 1000px var(--cor-fundo-campo) inset;
    box-shadow: 0 0 0 1000px var(--cor-fundo-campo) inset;
    -webkit-text-fill-color: var(--cor-texto);
    caret-color: var(--cor-texto);
  }
  .campo-pixel:has(input:disabled) {
    opacity: 0.5;
  }

  /* ===== Modais em pixel art =====
     Mesma silhueta dos botões e dos campos: cantos em escadinha de dois
     degraus de 4px, com a borda vermelha de sempre acompanhando o recorte.

     Aqui a moldura NÃO pode ser `clip-path`: além de recortar, ele torna a
     modal o bloco contentor dos descendentes `position: fixed` — e os balões
     do jogo são posicionados em coordenada de viewport por JS (#talentTooltip,
     em talentTreeUi.js, e a dica de medalha de dicaDeMedalha.js), então dentro
     de uma modal recortada abririam no lugar errado e ainda seriam cortados na
     borda. É por isso que os dois nascem soltos, fora do bloco que rola. Também não serve pseudo-elemento: a modal
     rola por dentro (`overflow-y: auto`), e uma camada `inset: 0` cresceria
     junto com o conteúdo rolado.

     Sobra o mesmo truque de `background-image` dos campos, só que com a
     escadinha dos dois lados: três camadas desenham a silhueta na cor da
     borda e três repintam o miolo, recuadas pela espessura. São três porque
     a escadinha, recortada em retângulos, é isto — a faixa larga, a faixa
     alta e o quadrado que liga as duas nos cantos.

     Sem clip-path o `box-shadow` externo até funcionaria, mas ele é
     retangular e apareceria como um dente sólido justamente nos cantos que
     acabaram de ser cortados — então as modais perdem a sombra deslocada,
     como já aconteceu com botões e campos. */
  .modal {
    border: none;
    border-radius: 0;
    box-shadow: none;
    background-color: transparent;
    background-image:
      /* miolo: recuado por 8px de canto + 2px de borda */
      linear-gradient(var(--cor-modal-fundo), var(--cor-modal-fundo)),
      linear-gradient(var(--cor-modal-fundo), var(--cor-modal-fundo)),
      linear-gradient(var(--cor-modal-fundo), var(--cor-modal-fundo)),
      /* silhueta na cor da borda: mesma escadinha, sem recuo */
      linear-gradient(var(--cor-vermelho), var(--cor-vermelho)),
      linear-gradient(var(--cor-vermelho), var(--cor-vermelho)),
      linear-gradient(var(--cor-vermelho), var(--cor-vermelho));
    background-size:
      calc(100% - 4px) calc(100% - 20px),
      calc(100% - 12px) calc(100% - 12px),
      calc(100% - 20px) calc(100% - 4px),
      100% calc(100% - 16px),
      calc(100% - 8px) calc(100% - 8px),
      calc(100% - 16px) 100%;
    background-position: center;
    background-repeat: no-repeat;
  }

  /* ===== Modo "Defesa da base" =====
     Ver public/js/horde.js, hordeHud.js e shared/horde.js. Dois blocos: o
     botão do menu e o HUD da partida (faixa do grupo em cima, console do
     jogador local embaixo). O botão só troca a tela inicial para a visão
     "Defesa da base" (#menuViewHorde) — não abre modal nenhuma. */

  #hordeSubtitle {
    font-size: 12px;
    font-weight: 400;
    letter-spacing: 0.4px;
    color: var(--cor-texto-fraco);
  }

  /* ===== Entrar com código =====
     A modal #hordeRoomsJoinOverlay, aberta pelo canto do mapa da campanha, no
     mesmo esquema de #talentResetConfirmOverlay — classe `.visible`, alternada
     via JS em hordeRoomBrowser.js. */
  #hordeRoomsJoinOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 21;
  }
  #hordeRoomsJoinOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #hordeRoomsJoinOverlay .modal {
    /* `.modal` genérica não declara `position`, então sem isto um filho
       `position: absolute` sobe até o ancestral posicionado seguinte —
       `#hordeRoomsJoinOverlay`, que é `fixed; inset: 0` — e cola no canto da
       **janela**, atrás do botão de som. */
    position: relative;
    padding: 20px 24px;
    width: 380px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  .horde-rooms-view-title {
    margin: 0;
    font-size: 22px;
    text-align: center;
  }

  /* ===== Salas abertas (a lista única) =====
     A modal #hordeRoomsListOverlay, aberta pelo "Salas abertas" do canto do
     mapa: todas as salas públicas de todas as fases, no mesmo esquema
     `.visible` da modal de código. As linhas reusam `.mapa-sala`/
     `.mapa-sala-texto` da ficha do mapa — é a mesma linha, com a fase a mais —,
     e por isso a modal **redeclara** `--px` e `--mapa-fonte-min`: elas moram em
     `.mapa-palco`, e a modal vive fora dele (as calc() sairiam inválidas). */
  #hordeRoomsListOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 21;
  }
  #hordeRoomsListOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #hordeRoomsListOverlay .modal {
    --px: 4px;
    --mapa-fonte-min: 11px;
    /* A modal reproduz a **ficha do mapa**: o mesmo material de fundo dela
       (`--cor-superficie-baixa`) por baixo das linhas em `--cor-superficie`.
       Sobre o preto de modal (`--cor-modal-fundo`) a linha saltava como se
       estivesse selecionada; aqui ela é o degrau suave que a ficha já tem. */
    --cor-modal-fundo: var(--cor-superficie-baixa);
    position: relative;
    padding: 20px 24px;
    width: 420px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  /* A lista rola dentro da modal, e não a modal dentro da tela: o cabeçalho, o
     aviso e o "Voltar" ficam sempre à vista, por mais salas que existam. */
  #hordeRoomsListOverlay .mapa-salas {
    max-height: min(46vh, 320px);
    overflow-y: auto;
    /* A coluna precisa poder **encolher**: `grid` dá ao item um mínimo de
       `min-content`, e a linha (nome da sala + fase + "Entrar") passava da
       largura da modal, jogando o "Entrar" para fora da moldura. */
    grid-template-columns: minmax(0, 1fr);
  }
  /* A linha traz o **chão da fase** ladrilhado, com o véu que a classe de
     textura já carrega — a mesma pista visual do quadrado da grade de níveis e
     do retrato da medalha (ver "Textura de arena como fundo de botão"). Sem
     classe de textura (fase desconhecida) sobra o fundo chapado de
     `.mapa-sala`. O ladrilho de 80px é o mesmo do quadrado da grade: numa
     linha desta altura, o de 320px nativo mostraria um borrão só. */
  #hordeRoomsListOverlay .mapa-sala {
    min-width: 0;
    background-image:
      linear-gradient(var(--veu-arena, transparent) 0 0),
      var(--textura-arena, none);
    background-size: auto, 80px 80px;
    background-repeat: repeat;
    background-position: center;
  }
  /* A linha da lista tem duas alturas: quem é a sala em cima, a fase embaixo. */
  #hordeRoomsListOverlay .mapa-sala-texto {
    display: flex;
    flex-direction: column;
    gap: calc(var(--px) * 0.5);
    white-space: normal;
  }
  #hordeRoomsListOverlay .sala-lista-quem {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  #hordeRoomsListOverlay .sala-lista-fase {
    color: var(--cor-texto-fraco);
    font-size: max(calc(var(--px) * 4), var(--mapa-fonte-min));
  }
  #hordeRoomsListOverlay #hordeRoomsListVazio {
    margin: 0;
    color: var(--cor-texto-fraco);
    text-align: center;
  }
  /* O aviso de fase trancada é informação, não erro: cinza, e não o vermelho
     que `.mapa-aviso` usa no canto do mapa para o servidor fora do ar. */
  #hordeRoomsListOverlay .mapa-aviso {
    max-width: none;
    color: var(--cor-texto-fraco);
    text-align: left;
  }
  /* O "Entrar" é o mesmo botão vermelho do "Entrar" da ficha do mapa: mesma
     cor, mesma moldura em pixel art (a modal vive fora de #menu, então os dois
     botões dela entram por id/classe na lista de seletores da moldura, junto
     dos da modal de código). */
  #hordeRoomsListOverlay .sala-lista-entrar {
    flex: none;
    width: auto;
    min-width: 0;
    padding: calc(var(--px) * 1.5) calc(var(--px) * 3);
    font-size: max(calc(var(--px) * 4.5), var(--mapa-fonte-min));
    line-height: 1.25;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #hordeRoomsListOverlay .sala-lista-entrar:hover {
    background: var(--cor-vermelho-hover);
  }
  body.touch-mode #hordeRoomsListOverlay .sala-lista-entrar {
    min-height: 44px;
  }
  /* O "Voltar" é o mesmo botão escuro do "Voltar" da modal de código: o botão
     genérico do jogo é claro, e numa modal escura ele vira uma barra branca
     que puxa o olho para a saída em vez de para as salas. */
  #btnHordeRoomsListBack {
    background: var(--cor-superficie);
    color: var(--cor-texto);
  }
  #btnHordeRoomsListBack:hover {
    background: var(--cor-superficie-hover);
  }
  /* ===== Monstruário (ficha dos inimigos da Defesa da base) =====
     Duas colunas dentro de #menuViewMonstruario, mesmo material do painel de
     #talentTreePanel: a lista vertical de
     inimigos à esquerda e a ficha do selecionado à direita. Montados por
     public/js/monstruario.js — aqui só há o material, nenhum inimigo. */
  #monstruarioPainel {
    /* As medidas do retrato do inimigo, em pixels **sem unidade**: o JS as lê
       daqui (ver `medidas` em monstruario.js) para dimensionar o sprite, e a
       janela de arte da carta as usa como `calc(... * 1px)`. Ficam no CSS
       porque quem decide o tamanho do retrato é a faixa de tela — e faixa de
       tela, neste projeto, é classe no `<body>` posta pelo CSS, não
       `matchMedia` no JS.

       `corpo` é a referência da comparação de tamanho entre os bichos; a
       janela é o quanto se vê ao redor dele, e é maior que o corpo de
       propósito (ver .monstruario-sprite).

       Os três números do retrato são 85% dos medidos (132/360/260): na ficha
       de duas fileiras (ver .monstruario-carta-e) a janela divide a largura
       com a coluna de números, e 306px é o que sobra para os dois caberem numa
       coluna de detalhe de 610px sem apertar o texto. É a maior escala entre as
       disposições que cabem sem rolagem — encolher o retrato é a última coisa a
       se fazer nesta ficha, não a primeira. A proporção entre os três é a
       mesma, que é o que mantém a garantia de nenhum sprite aparecer cortado:
       a janela é dimensionada em cima do corpo (ver monstruario.js). */
    --retrato-corpo: 112;
    --retrato-janela-l: 306;
    --retrato-janela-a: 221;
    --mini-corpo: 36;
    --mini-janela-l: 60;
    --mini-janela-a: 36;
  }
  #monstruarioListPanel,
  #monstruarioDetailPanel {
    display: flex;
    flex-direction: column;
    gap: 12px;
    height: 100%;
    max-width: 100%;
    box-sizing: border-box;
  }
  #monstruarioListPanel {
    width: 380px;
    flex: none;
    padding-right: 24px;
    /* Só a lista rola (o contador é cabeçalho e fica sempre à vista) — mesma
       corrente de `flex: 1` + `min-height: 0` de #talentTreePanel. */
    overflow: hidden;
  }
  /* A divisória entre as colunas é borda da coluna da direita, e não um
     elemento no meio: assim ela acompanha a altura da fileira sozinha, com a
     coluna rolando por dentro. */
  #monstruarioDetailPanel {
    flex: 1;
    align-items: center;
    /* A carta é centralizada pela **margem automática** dela, e não por um
       `justify-content: center` aqui: numa coluna que rola, o conteúdo
       centralizado por `justify-content` vaza pelos dois lados e o topo dele
       fica inalcançável — a carta abria com o retrato já cortado acima da
       borda numa tela deitada e curta. */
    text-align: center;
    min-height: 0;
    overflow-y: auto;
    padding-left: 24px;
    border-left: 2px solid var(--cor-borda);
  }
  #monstruarioList {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }
  /* Linha da lista: a miniatura numa cavidade de pedra + o nome do inimigo.
     Fora da regra genérica de botão de #menu (ver as exclusões lá em cima)
     porque é uma linha de lista, não um botão de ação vermelho — mas com o
     mesmo vocabulário de pixel art do resto da tela: silhueta recortada em
     escadinha de um degrau (a dos botões de ícone, que é a escala certa para
     uma linha desta altura), no lugar do retângulo reto com borda de 1px que
     era a única peça da tela que não parecia do jogo.

     Nada de `border` aqui, e nada de anel `inset`: recortados pelo `clip-path`,
     os dois sumiriam justamente na diagonal dos cantos (o mesmo motivo de
     .menu-painel). Hover é troca de cor do miolo, seleção é troca de cor do
     contorno — ver "Linha de lista com contorno". */
  .monstruario-row {
    /* Miolo e contorno vêm da regra compartilhada com a linha do placar, lá
       no alto do arquivo ("Linha de lista com contorno"), que é também onde
       está o porquê de esta linha não ter o bisel de pedra. Daqui saem só as
       duas cores: o hover troca a do miolo, a seleção troca a do contorno, e a
       geometria é a mesma nos três estados — nada se mexe de lugar ao
       selecionar. */
    --cor-fundo-linha: var(--cor-superficie);
    --cor-contorno-linha: var(--cor-fundo-linha);
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    box-sizing: border-box;
    padding: 4px 12px 4px 6px;
    border: none;
    background-color: transparent;
    clip-path: var(--recorte-icone-pixel);
    font-family: inherit;
    font-size: inherit;
    text-align: left;
    color: var(--cor-texto);
    cursor: pointer;
  }
  .monstruario-row:hover {
    --cor-fundo-linha: var(--cor-superficie-hover);
  }
  /* O selecionado é marcado pela cor da identidade, como o botão da visão
     ativa na antiga fileira de abas do menu — não por um fundo cheio, que competiria com a cor do
     próprio bicho na miniatura ao lado. É uma troca de cor só: o contorno já
     está desenhado o tempo todo, na cor do miolo, e aqui ele acende. */
  .monstruario-row-ativa,
  .monstruario-row-ativa:hover {
    --cor-contorno-linha: var(--cor-vermelho-destaque);
  }
  /* A miniatura mora numa cavidade cavada na pedra da linha (bisel invertido),
     como a janela de arte da carta ao lado: é o que faz a linha ler como
     entrada de catálogo, e não como texto com um desenho ao lado.

     60x36 e não um quadrado de 36: a janela precisa caber o bicho **inteiro**,
     e as asas abertas do Abutre Rei medem 57px de ponta a ponta na escala em
     que o corpo dele é desenhado aqui (as medidas estão em monstruario.js). O
     corpo não mudou de tamanho — o que mudou é o quanto se vê em volta dele. */
  .monstruario-mini {
    flex-shrink: 0;
    width: 60px;
    height: 36px;
    background-color: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-afundada);
  }
  .monstruario-row-nome {
    min-width: 0;
  }
  /* Contador de descobertas, no cabeçalho do painel (ver
     public/js/monstruarioDescobertas.js). Mesmo tratamento de rótulo
     secundário do subtítulo da ficha: versalete pequeno em cinza. */
  .monstruario-progresso {
    margin: 0;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  /* Linha do inimigo ainda não encontrado: o "???" fica no mesmo cinza dos
     rótulos secundários e a silhueta (desenhada no canvas por
     `desenharQuadrado`, ver monstruario.js) vem sem cor e sem olhos. A linha
     continua clicável — a ficha trancada diz o que falta fazer para abri-la. */
  .monstruario-row-oculta .monstruario-row-nome {
    color: var(--cor-texto-fraco);
  }
  /* ===== A carta da ficha =====
     A ficha do inimigo é uma **carta**, com a mesma leitura das cartas de item
     do jogo (ver .carta): moldura recortada, a janela de arte em cima e o texto
     em slots embaixo. O material é o do container do menu (.menu-painel) numa
     peça menor — as mesmas duas camadas, porque o `clip-path` continua sem
     saber recortar buraco —, e não a arte de 64x96 das cartas de item: aquela
     tem dois slots de texto de tamanho fixo, e a ficha traz de cinco a oito
     linhas de atributos mais a lista de fases.

     A carta é centralizada na coluna, e não esticada nela: a coluna agora é
     larga (o painel não tem mais teto de largura) e uma ficha de ponta a ponta
     deixaria cada rótulo a meia tela do seu valor. */
  .monstruario-carta {
    position: relative;
    /* Contexto de empilhamento próprio, como em .menu-painel: sem ele as duas
       camadas em `z-index` negativo cairiam atrás do painel. */
    isolation: isolate;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    /* A largura é a da janela de arte mais o padding dos dois lados: é a janela
       que manda no tamanho da carta, e ela muda de tamanho por faixa de tela. */
    width: calc(var(--retrato-janela-l) * 1px + 24px);
    max-width: 100%;
    box-sizing: border-box;
    padding: 12px;
    /* `flex: none` para a carta não ser esticada nem espremida pela coluna, e a
       margem automática para ela ficar no meio da altura que sobra. */
    flex: none;
    margin: auto 0;
  }
  .monstruario-carta::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -2;
    background: var(--cor-painel-pedra-contorno);
    clip-path: var(--recorte-botao-pixel);
    filter: drop-shadow(6px 6px 0 rgba(0, 0, 0, 0.4));
  }
  .monstruario-carta::after {
    content: "";
    position: absolute;
    inset: var(--espessura-borda-botao);
    z-index: -1;
    background-color: var(--cor-superficie-baixa);
    box-shadow: var(--sombra-pedra-interna);
    clip-path: var(--recorte-botao-pixel);
  }
  /* Entrada da ficha: a cada inimigo selecionado a carta é remontada do zero
     (`montarCarta`, em monstruario.js), e sem nada marcando essa troca a ficha
     nova aparecia no lugar da velha sem aviso — descendo a lista com o
     direcional, o painel parecia estático mesmo trocando de bicho. Três
     degraus de escala e opacidade, do jeito que o painel de item surge
     (`item-choice-surge`): a carta se **monta**, não desliza.

     Duas escolhas que não são gosto:

     - a escala nunca passa de 1. A carta destrancada ocupa a largura inteira
       da coluna de detalhe, e um overshoot (o 1,08 do painel de item) faria
       essa coluna rolar na horizontal pelo quadro em que a carta está maior
       que ela.
     - `steps(3, jump-none)` e um começo já visível (0,2 de opacidade), não
       `steps(3, end)` a partir de zero: com `end` o último degrau nunca é
       desenhado e o primeiro é invisível — sobrariam dois quadros de animação
       para gastar 180ms. A duração é curta de propósito, porque na navegação
       por direcional a ficha troca a cada toque. */
  .monstruario-carta-entra {
    animation: monstruario-carta-entra 0.18s steps(3, jump-none);
  }
  @keyframes monstruario-carta-entra {
    0% { opacity: 0.2; transform: scale(0.88); }
    100% { opacity: 1; transform: scale(1); }
  }
  /* Quem pediu menos movimento fica com a troca de opacidade, que é o aviso de
     que a ficha é outra; a escala, que é o movimento, sai. */
  @media (prefers-reduced-motion: reduce) {
    .monstruario-carta-entra {
      animation: monstruario-carta-aparece 0.18s steps(3, jump-none);
    }
    @keyframes monstruario-carta-aparece {
      0% { opacity: 0.2; }
      100% { opacity: 1; }
    }
  }
  /* A janela de arte: uma cavidade na pedra da carta, com o bicho no meio. A
     altura é fixa (e não a do sprite) para a carta não mudar de tamanho a cada
     inimigo selecionado — o retrato do Cupim e o do Arauto Vulcânico têm
     alturas bem diferentes, e uma carta que pula a cada clique é o mesmo
     defeito que a moldura do menu pulando a cada aba.

     `overflow: hidden` é a rede de segurança de tela estreita: a janela tem a
     largura do maior sprite (360px, ver monstruario.js) e numa coluna menor que
     isso ela encolhe, aparando o desenho pelos dois lados em vez de deixá-lo
     vazar a carta por um. */
  .monstruario-carta-arte {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: calc(var(--retrato-janela-a) * 1px);
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-afundada);
    overflow: hidden;
  }
  /* ===== A ficha em duas fileiras (a disposição "E") =====
     Em cima, a janela de arte à esquerda e o nome + os números à direita; em
     baixo, "Onde aparece" na largura inteira. Montada por `montarDetalhe`
     (monstruario.js), e só na ficha **destrancada**: a trancada é uma linha de
     texto e continua sendo a coluna centrada de `.monstruario-carta`.

     A carta ocupa a largura da coluna, e não a da janela de arte: aqui o texto
     mora ao lado do retrato, e é essa largura que a ficha existe para gastar —
     empilhada, ela pedia até 659px de altura numa coluna de 526 (os dezoito
     inimigos rolavam) com 226px de largura sobrando ao lado.

     Duas fileiras de verdade, e não um grid só de duas colunas: a de baixo não
     acompanha a divisão de cima, e é isso que dá à lista de fases largura para
     caber em duas linhas em vez de cinco. */
  .monstruario-carta-e {
    /* `stretch`, e não o `center` de `.monstruario-carta`: com o rodapé
       centralizado, um "Onde aparece" curto ("Todas as arenas") virava um
       retângulo pequeno flutuando no meio da fileira. */
    align-items: stretch;
    width: 100%;
    text-align: left;
  }
  .monstruario-carta-e .monstruario-fileira-topo {
    display: grid;
    /* `minmax(0, auto)` e não `auto` na coluna da arte: numa coluna de detalhe
       mais estreita que a janela (o celular deitado de 740px, onde ela mede
       242px) uma trilha `auto` fica na largura pedida e **vaza** a carta pela
       direita. Com o mínimo em zero ela cede, e o que apara o desenho é o
       `overflow: hidden` que a janela já tem. */
    grid-template-columns: minmax(0, auto) minmax(0, 1fr);
    /* As duas caixas se resolvem contra a mais alta, e nenhuma sobra de altura
       abaixo da outra. Era daí que vinha o desnível: a janela tinha altura fixa
       e a coluna de números crescia por baixo dela. */
    align-items: stretch;
    gap: 12px;
    width: 100%;
  }
  /* A janela de arte aqui tem a largura do retrato e a altura do **conteúdo da
     fileira**: o `min-height` é a medida de sempre (a que garante o sprite
     inteiro) e o que passar disso vem da coluna de números ao lado. A
     contrapartida conhecida é que a janela muda de altura de um bicho para o
     outro, porque a coluna tem de cinco a oito linhas conforme o inimigo. */
  .monstruario-carta-e .monstruario-carta-arte {
    position: relative;
    flex-direction: column;
    width: calc(var(--retrato-janela-l) * 1px);
    max-width: 100%;
    height: auto;
    min-height: calc(var(--retrato-janela-a) * 1px);
  }
  /* A contagem de abates mora **dentro** da janela, embaixo do bicho: é o único
     número da ficha que fala do jogador, e não do inimigo. No fim da caixa para
     não brigar com o sprite, que é centralizado nela. */
  .monstruario-carta-e .monstruario-abatidos {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 10px;
    margin: 0;
    text-align: center;
    font-size: 13px;
    color: var(--cor-texto-fraco);
  }
  /* A coluna da direita é **uma** cavidade: o nome no topo, separado por um
     risco, e os números embaixo. Duas caixas empilhadas ali dariam duas
     molduras na mesma coluna. */
  .monstruario-carta-e .monstruario-coluna-texto {
    display: flex;
    flex-direction: column;
    min-width: 0;
    padding: 10px 12px;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-afundada);
  }
  .monstruario-carta-e .monstruario-detalhe-nome {
    margin: 0 0 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--cor-borda);
  }
  /* Dentro da cavidade da coluna, a lista de atributos perde a própria: ela já
     está num slot. */
  .monstruario-carta-e .monstruario-coluna-texto .monstruario-atributos {
    padding: 0;
    background: transparent;
    box-shadow: none;
  }
  .monstruario-carta-e .monstruario-bloco-onde {
    padding: 8px 12px;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-afundada);
  }
  .monstruario-carta-e .monstruario-detalhe-subtitulo {
    margin: 0 0 4px;
  }
  /* Linha da ficha trancada. Nada de itálico nem de opacidade: é o único
     texto da carta, e apagá-lo faria a ficha inteira ler como algo que não
     carregou — que é justamente o que ela não é. */
  .monstruario-detalhe-oculto {
    margin: 0;
    max-width: 30ch;
    font-size: 13px;
    line-height: 1.5;
    color: var(--cor-texto-fraco);
  }
  /* Sem medida aqui: quem dimensiona o retrato é o JS, pelas custom properties
     do sprite abaixo (e pelos atributos do canvas, na silhueta trancada). */
  .monstruario-retrato {
    flex-shrink: 0;
    background: transparent;
  }
  /* Retrato do bicho já encontrado (ver inimigoSprites.js): ele **se
     mexendo**, na mesma pose de idle da arena — e, na carta, batendo assim que
     ela abre e a cada cinco segundos (ver GOLPE_INTERVALO_MS em
     monstruario.js). É a mesma máquina de `.class-sprite` — tira horizontal
     andada por `background-position` em `steps()`, sem loop de JS —, e o que as
     duas de fato compartilham são os keyframes `spriteStrip`/`spriteStripUnica`.
     Não é `.class-sprite` reaproveitada porque "class" ali quer dizer classe
     jogável, e esta lista é de inimigos.

     Sem laço de JS não é detalhe de implementação, e virou o caso normal: a
     lista tem um retrato por bicho do bestiário, todos na tela ao mesmo tempo,
     e desde que os dezoito ganharam arte são **todos** eles que se mexem — um
     `requestAnimationFrame` por linha aberto o tempo todo no menu seria caro
     por um retrato que ninguém fica olhando. O canvas pintado à mão que sobrou
     em `criarRetrato` é a silhueta do bicho ainda trancado.

     `--sprite-w`/`--sprite-h` (a janela), `--sprite-frame` (o lado do quadro
     desenhado) e `--sprite-anchor` são escritos pelo JS (`criarRetrato` em
     monstruario.js) a partir das medidas do corpo daquele bicho: o quadro
     cresce até o corpo medir o lado pedido — é ele que carrega a comparação de
     tamanho entre os inimigos — e a âncora põe o **corpo** no centro do
     retrato, e não o quadro, que é quase todo vazio.

     A janela é **maior que o corpo**, e é isso que conserta o sprite cortado:
     enquanto ela era um quadrado do tamanho do corpo, tudo que o bicho tem além
     do corpo — as asas do Abutre Rei, a lança do Arauto de Granito, a arma de
     qualquer um no meio do golpe — caía fora dela. As medidas saem medidas
     quadro a quadro, ver monstruario.js. */
  .monstruario-sprite {
    --sprite-x0: calc((var(--sprite-w) - var(--sprite-frame)) / 2);
    flex-shrink: 0;
    width: var(--sprite-w);
    height: var(--sprite-h);
    /* A janela mostra **um** quadro, e este recorte é o que garante isso. A
       tira é desenhada inteira (`background-size` é `quadros x quadro`) e o
       que se enquadra é uma fatia dela; quando o quadro desenhado é mais
       estreito que a janela — o caso de todo bicho pequeno, porque a janela é
       o teto do bestiário —, sobrava largura dos dois lados e o que aparecia
       ali era o **quadro vizinho da tira**: um pedaço de outro instante da
       animação flutuando ao lado do bicho. Recorta só na horizontal porque a
       tira tem a altura de um quadro e vazamento vertical não existe. */
    clip-path: inset(0 max(0px, var(--sprite-x0)));
    background-repeat: no-repeat;
    background-size: calc(var(--sprite-frames) * var(--sprite-frame)) var(--sprite-frame);
    background-position: var(--sprite-x0)
      calc(var(--sprite-h) / 2 - var(--sprite-frame) * var(--sprite-anchor));
    image-rendering: pixelated;
    animation: spriteStrip var(--sprite-duration) steps(var(--sprite-frames)) infinite;
  }
  /* A **silhueta** do bicho ainda não encontrado (ver `criarSilhueta` em
     monstruario.js): a forma do próprio sprite, recortando uma cor chapada.

     O sprite entra como **máscara** e não como fundo — é o que garante que
     nenhum pixel de cor dele chegue à tela. A cor é o cinza de pedra do painel,
     e não a cor do bicho escurecida: a cor é metade da identidade dele na arena,
     e escurecê-la entregaria o que a silhueta existe para esconder.

     As duas propriedades de máscara espelham exatamente as de fundo de
     `.monstruario-sprite`, e o `mask-position` fixa o **primeiro** quadro: o
     trancado não anda, e é essa diferença que diz de relance quem já foi
     encontrado. Sem `animation`, pelo mesmo motivo. */
  .monstruario-silhueta {
    background-image: none;
    background-color: var(--cor-silhueta-monstruario);
    animation: none;
    mask-image: var(--silhueta-src);
    mask-repeat: no-repeat;
    mask-size: calc(var(--sprite-frames) * var(--sprite-frame)) var(--sprite-frame);
    mask-position: var(--sprite-x0)
      calc(var(--sprite-h) / 2 - var(--sprite-frame) * var(--sprite-anchor));
    -webkit-mask-image: var(--silhueta-src);
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-size: calc(var(--sprite-frames) * var(--sprite-frame)) var(--sprite-frame);
    -webkit-mask-position: var(--sprite-x0)
      calc(var(--sprite-h) / 2 - var(--sprite-frame) * var(--sprite-anchor));
  }
  /* O golpe: tira de um tiro só, que trava no último quadro em vez de repetir
     (`loop: false` em inimigoSprites.js). Mesma dupla de keyframes de
     `.class-sprite-once`, e pelo mesmo motivo — a de cima anda até um quadro
     **passado** o fim da tira, e travar ali deixaria o retrato vazio. */
  .monstruario-sprite.monstruario-sprite-uma-vez {
    animation: spriteStripUnica var(--sprite-duration)
      steps(var(--sprite-frames), jump-none) 1 forwards;
  }
  .monstruario-detalhe-nome {
    margin: 0;
    font-size: 22px;
  }
  /* Ficha de atributos em duas colunas (rótulo à esquerda, valor à direita):
     é uma tabela de números, e ler "Vida 2" em linhas corridas obriga a
     procurar onde um par termina e o outro começa. O fundo cavado é o mesmo
     tratamento da janela de arte — na carta, texto mora em slot. */
  /* Ficha de atributos em **duas colunas de pares**, cada par com o rótulo em
     cima e o valor embaixo (`.monstruario-atributo`, escrito por
     `montarDetalhe`). Ela já foi rótulo à esquerda e valor à direita, numa
     coluna só: ali a coluna do rótulo tinha a largura do maior deles ("Dano em
     você") e o valor começava longe do rótulo dele, com metade da caixa vazia
     entre os dois. Empilhado, o par se lê de cima para baixo e cabem dois por
     linha — que é o que dá espaço para o atributo **de frase** ocupar a largura
     inteira sem a ficha crescer.

     O fundo cavado é o mesmo tratamento da janela de arte — na carta, texto
     mora em slot. */
  .monstruario-atributos {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px 16px;
    width: 100%;
    box-sizing: border-box;
    margin: 0;
    padding: 10px 12px;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-afundada);
    font-size: 13px;
    text-align: left;
  }
  /* O atributo cujo valor é uma frase (a cadência do ataque, o alvo, o que
     acontece na morte — ver `atributos` em monstruario.js) ocupa as duas
     colunas: numa metade ele quebraria em três ou quatro linhas e a fileira
     ficaria com um buraco do lado. */
  .monstruario-atributo-largo {
    grid-column: 1 / -1;
  }
  .monstruario-atributos dt {
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    font-size: 11px;
    letter-spacing: 0.05em;
  }
  .monstruario-atributos dd {
    margin: 2px 0 0;
    line-height: 1.35;
  }
  .monstruario-detalhe-subtitulo {
    margin: 4px 0 0;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  .monstruario-detalhe-niveis {
    margin: 0;
    font-size: 12px;
    line-height: 1.5;
  }
  /* ===== Relicário (catálogo dos itens da Defesa da base) =====
     Mesmo material dos painéis do Monstruário acima, mas **um painel só** e
     não duas colunas: item não tem ficha de números para um painel de detalhe
     (ver o comentário no topo de public/js/relicario.js), então a largura
     inteira fica para o catálogo. Montado por public/js/relicario.js — aqui só
     há o material, nenhum item.

     A lista se abre em **três colunas**: cada linha carrega ícone, nome e a
     descrição inteira do item, e numa coluna só o catálogo cheio vira uma
     rolagem longa onde o jogador não vê duas raridades ao mesmo tempo. */
  /* A moldura é a padrão (.menu-painel) — foi desta tela que saiu a largura
     dela: três colunas de 280px com os vãos e o padding pedem 1040px, e é a
     mais larga das cinco. */
  #relicarioList {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    /* Três colunas na largura cheia do painel (1040px cabem 3 de 322px, e
       nunca uma quarta). Espremido — janela estreita, barra lateral de ícone,
       tela deitada —, cai para 2 e depois para 1 sozinho, em vez de manter
       três colunas ilegíveis por uma faixa de `@media` a mais. */
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    align-content: start;
    /* Folga maior que a de antes (6px/12px): desde que a linha virou peça de
       pedra recortada, colar uma na outra devolvia a malha de planilha que o
       recorte existe para desfazer — a escadinha dos cantos precisa de vão
       para ser vista. */
    gap: 8px 12px;
    /* `1 1 auto` e não `1`: com a base em 0% a lista não sabe encolher abaixo do
       conteúdo dela, e o rodapé de ação (`.loja-rodape`) era empurrado para
       fora do painel em vez de a lista rolar. */
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }
  /* Cabeçalho de raridade. Não é um botão nem um filtro: o catálogo inteiro
     fica na tela, e o grupo só diz em que ordem os itens entram na corrida. */
  .relicario-grupo {
    /* A raridade é o cabeçalho das linhas abaixo dela, então atravessa as três
       colunas — numa célula só ela leria como o primeiro item do grupo. */
    grid-column: 1 / -1;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 14px 2px 4px;
    /* A régua embaixo do cabeçalho é a **cor da raridade** em 2px, e não a
       linha de 1px cinza que era: 1px é a espessura que não existe em lugar
       nenhum desta tela (ver "Pixel art na interface" em public/css/CLAUDE.md),
       e em cinza ela ainda separava os grupos sem dizer qual é qual. Aqui ela
       repete a cor que as linhas abaixo carregam na faixa da esquerda. */
    box-shadow: inset 0 -2px 0 var(--cor-raridade, var(--cor-borda));
  }
  .relicario-grupo:first-child {
    padding-top: 0;
  }
  /* O nome da raridade é a legenda das cores das linhas abaixo dele: se o
     cabeçalho ficasse cinza, o jogador veria quatro fundos diferentes sem
     nada dizendo qual é qual. */
  .relicario-grupo-nome {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-raridade, var(--cor-texto-suave));
  }
  .relicario-grupo-resumo {
    font-size: 12px;
    color: var(--cor-texto-fraco);
  }
  /* Linha de item: ícone + nome + descrição. Sem hover e sem cursor de clique —
     ela não é clicável, porque não há painel de detalhe para abrir (ver o
     comentário no topo de public/js/relicario.js). */
  .relicario-item {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 8px 10px;
    /* Fundo tingido pela raridade: a superfície de sempre por baixo e o véu da
       raridade por cima como camada de imagem (ver `.raridade-*` no topo do
       arquivo). É camada e não um hex por raridade porque o mesmo véu entra
       também sobre o pedra do HUD, que é bem mais escuro — dois fundos, um
       tom. */
    background-color: var(--cor-superficie-elevada);
    background-image: linear-gradient(var(--cor-raridade-veu), var(--cor-raridade-veu));
    /* Faixa sólida na borda esquerda, na cor cheia. O véu sozinho é fraco de
       propósito (o texto tem que continuar legível por cima dele), e é esta
       faixa que se lê varrendo a grade de três colunas sem parar em nenhuma
       linha. Inset em vez de `border-left` para não empurrar o conteúdo. */
    /* Faixa sólida na borda esquerda, na cor cheia. O véu sozinho é fraco de
       propósito (o texto tem que continuar legível por cima dele), e é esta
       faixa que se lê varrendo a grade de três colunas sem parar em nenhuma
       linha. Inset em vez de `border-left` para não empurrar o conteúdo — e,
       inset, ela acompanha o recorte dos cantos.

       Depois dela vem o **bisel de pedra na escala pequena**, o mesmo da linha
       do Monstruário: a linha do catálogo é a mesma peça de interface nas duas
       telas, e aqui ela era um retângulo reto com fundo chapado — a única
       superfície do menu que não parecia do jogo. A faixa vem primeiro na
       lista porque `box-shadow` pinta na ordem, e a luz do bisel por cima dela
       comeria 2px dos 3. */
    box-shadow: inset 3px 0 0 var(--cor-raridade),
      var(--sombra-pedra-pequena-interna);
    /* Silhueta recortada em escadinha de um degrau (a dos botões de ícone, que
       é a escala certa para uma peça desta altura). Nada de `border` junto:
       recortada, ela sumiria justamente na diagonal dos cantos — o mesmo
       motivo de .menu-painel e de .monstruario-row. */
    clip-path: var(--recorte-icone-pixel);
  }
  .relicario-icone {
    flex-shrink: 0;
    /* Exatamente o tamanho em que o canvas é criado (ICONE_LADO_PX em
       relicario.js): canvas esticado por CSS reamostra o desenho, que é o meio
       pixel proibido em "Pixel art no canvas" (public/js/CLAUDE.md). */
    width: 42px;
    height: 42px;
    /* O ícone mora numa **cavidade cavada na pedra** da linha (bisel invertido),
       como a miniatura do Monstruário e a janela de arte da carta: é o que faz a
       linha ler como entrada de catálogo, e não como texto com um desenho solto
       ao lado.

       `content-box` com padding, e não uma caixa maior: o canvas continua
       desenhado em 42x42 (ICONE_LADO_PX em relicario.js) e o padding só abre a
       borda da cavidade em volta dele. Esticar o canvas por CSS é o meio pixel
       proibido em "Pixel art no canvas" (public/js/CLAUDE.md). */
    box-sizing: content-box;
    padding: 4px;
    background-color: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-afundada);
    clip-path: var(--recorte-icone-pixel);
  }
  .relicario-item-texto {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }
  .relicario-item-nome {
    font-size: 14px;
  }
  .relicario-item-descricao {
    font-size: 12px;
    line-height: 1.4;
    color: var(--cor-texto-fraco);
  }
  /* O mesmo aviso de família do cartão da tela de escolha, na mesma gramática
     (faixa à esquerda, caixa de canto reto, versalete): é o mesmo dado, e um
     segundo desenho para ele faria o jogador aprender duas formas de ler a
     mesma regra. */
  .relicario-item-familia {
    margin-top: 4px;
    padding: 2px 6px;
    align-self: flex-start;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--cor-familia, var(--cor-familia-outra));
    background: var(--cor-superficie-baixa);
    border-left: 3px solid var(--cor-familia, var(--cor-familia-outra));
    border-radius: 0;
  }
  /* ===== Carta de item =====
     A carta que a Loja vende e que a tela de escolha entre ondas apresenta —
     uma só, montada por public/js/cartaDeItem.js. A moldura é **arte**
     (assets/ui/cartas-raridade.png, gerada de "assets-fonte/Card Template/"
     pelo gerar-cores-de-raridade.py ao lado dela), e as duas caixas marrons do
     desenho são texto, não decoração.

     Medidas da arte (64x96 por quadro), e é delas que sai todo o
     posicionamento aqui:
       janela da arte  x 12..51   y 17..50
       slot de cima    x  4..60   y 53..63   -> nome + raridade (+ família)
       slot de baixo   x  8..55   y 70..86   -> descrição
     Tudo escrito como `<pixel da arte> * var(--escala-carta)`, e nunca em
     pixel de tela solto: mudar a escala move a carta inteira junto, e é a
     escala que responde pelo tamanho de tela (as `@media` no fim deste bloco).

     Nada aqui tem transição contínua nem canto arredondado — as animações da
     carta são todas em `steps()`, como o resto da interface (ver
     public/css/CLAUDE.md). */
  :root {
    --escala-carta: 4;
  }
  .carta {
    position: relative;
    width: calc(64px * var(--escala-carta));
    height: calc(96px * var(--escala-carta));
    flex: none;
    background-image: url('../assets/ui/cartas-raridade.png');
    /* A folha inteira tem cinco quadros de 64px; a posição escolhe a coluna. */
    background-size: calc(320px * var(--escala-carta)) calc(96px * var(--escala-carta));
    background-repeat: no-repeat;
    image-rendering: pixelated;
    transition: none;
  }
  /* Uma coluna por raridade, na ordem de RARIDADES (shared/itens.js), e o
     verso no fim. A ordem é contrato com o gerador do asset. */
  .carta.f-comum { background-position: 0 0; }
  .carta.f-raro { background-position: calc(-64px * var(--escala-carta)) 0; }
  .carta.f-quebra-jogo { background-position: calc(-128px * var(--escala-carta)) 0; }
  .carta.f-maldito { background-position: calc(-192px * var(--escala-carta)) 0; }
  .carta.verso { background-position: calc(-256px * var(--escala-carta)) 0; }
  /* O halo da raridade só existe na frente: no verso não há raridade a
     anunciar, e um verso brilhando na cor certa entregaria o sorteio. */
  .carta.frente {
    box-shadow: 0 0 0 3px var(--cor-raridade), 0 0 30px var(--cor-raridade);
  }
  .carta-arte {
    position: absolute;
    left: 50%;
    top: calc(20px * var(--escala-carta));
    margin-left: calc(-14px * var(--escala-carta));
    image-rendering: pixelated;
    background: transparent;
  }
  .carta-slot-nome {
    position: absolute;
    left: calc(4px * var(--escala-carta));
    top: calc(53px * var(--escala-carta));
    width: calc(57px * var(--escala-carta));
    height: calc(11px * var(--escala-carta));
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 0 4px;
    overflow: hidden;
    text-align: center;
  }
  .carta-nome {
    font-weight: 700;
    line-height: 1.05;
    color: var(--cor-texto);
    text-shadow: 2px 2px 0 #000;
  }
  /* Em passo da escala da carta, e não em pixel solto: a linha cresce junto
     com a moldura. É ela que carrega a família, e a 9px fixos era a coisa
     menos legível da carta. */
  .carta-raridade {
    letter-spacing: 0.1em;
    line-height: 1.1;
    white-space: nowrap;
    color: var(--cor-raridade);
    text-shadow: 1px 1px 0 #000;
  }
  /* O aviso de família, última linha da caixa de baixo (ver porFrente em
     cartaDeItem.js). Ele leva um fundo escuro próprio — e não a janela de texto
     da moldura como fundo — porque a cor da família é o dado: sobre o marrom do
     slot, o ciano do cano e o rosa do leque perdem contraste e a linha vira a
     coisa menos legível da carta. O retângulo é só o tamanho do texto, de canto
     reto e medido em passo de `--escala-carta`, para encostar na arte sem
     virar um segundo quadro por cima dela. */
  .carta-familia {
    color: var(--cor-familia, var(--cor-familia-outra));
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 0.85em;
    margin-top: 0.35em;
    /* `inline-block` para o fundo parar no fim do texto; a caixa de baixo já
       centraliza (`align-items: center`). */
    display: inline-block;
    max-width: 100%;
    padding: calc(1px * var(--escala-carta)) calc(2px * var(--escala-carta));
    background: rgba(0, 0, 0, 0.72);
    border-radius: 0;
    /* Sobre o fundo escuro a sombra de 1px do slot só engorda a letra. */
    text-shadow: none;
  }
  .carta-slot-desc {
    position: absolute;
    left: calc(8px * var(--escala-carta));
    top: calc(70px * var(--escala-carta));
    width: calc(48px * var(--escala-carta));
    height: calc(17px * var(--escala-carta));
    display: flex;
    /* Coluna: a descrição e, embaixo dela, o aviso de família. Com um filho só
       (a Loja, que não pede o aviso) o centro continua sendo o centro. */
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 0 3px;
    overflow: hidden;
    text-align: center;
    line-height: 1.12;
    color: var(--cor-texto);
    text-shadow: 1px 1px 0 #000;
  }
  /* Selo de nível da cópia (o "NV 5" da tela de escolha). No canto da janela da
     arte, que é a única área da moldura sem texto. */
  .carta-nivel {
    position: absolute;
    left: calc(13px * var(--escala-carta));
    top: calc(18px * var(--escala-carta));
    padding: 1px 4px;
    font-size: 10px;
    font-weight: 700;
    line-height: 1.2;
    color: var(--cor-fundo);
    background: var(--cor-raridade);
    text-shadow: none;
  }

  /* --- Brilhos da revelação --- */
  .carta-clarao {
    position: absolute;
    inset: 0;
    background: #fff;
    pointer-events: none;
    animation: carta-clarao 0.3s steps(4) forwards;
  }
  @keyframes carta-clarao {
    from { opacity: 0.85; }
    to { opacity: 0; }
  }
  .carta-anel {
    position: absolute;
    inset: calc(-4px * var(--escala-carta));
    border: 5px solid var(--cor-raridade);
    pointer-events: none;
    animation: carta-anel 0.5s steps(6) forwards;
  }
  @keyframes carta-anel {
    from { transform: scale(0.7); opacity: 0.95; }
    to { transform: scale(1.6); opacity: 0; }
  }
  .carta-faisca {
    position: absolute;
    left: 50%;
    top: 50%;
    background: var(--cor-raridade);
    pointer-events: none;
    animation: carta-faisca var(--vida) steps(7) forwards;
  }
  @keyframes carta-faisca {
    from { transform: translate(0, 0); opacity: 1; }
    to { transform: translate(var(--dx), var(--dy)); opacity: 0; }
  }

  /* ===== Mesa da compra (Loja) =====
     O overlay em que as três cartas entram, flutuam e viram. Três camadas
     aninhadas porque três transformações rodam ao mesmo tempo e uma
     propriedade `transform` só não empilha: o **slot** entra na tela, a
     camada `.carta-flutua` flutua, e a `.carta` gira. */
  #lojaMesaOverlay {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 20px;
    padding: 20px;
    overflow: hidden;
    background: rgba(0, 0, 0, 0.9);
  }
  #lojaMesaOverlay.aberto { display: flex; }
  #lojaMesaTitulo {
    font-size: 24px;
    text-align: center;
  }
  #lojaMesaSubtitulo {
    min-height: 20px;
    margin-top: -12px;
    font-size: 14px;
    color: var(--cor-texto-fraco);
  }
  /* O "Continuar" da mesa. Ele carrega `.menu-btn`, que **não existe** no CSS —
     o que dá estilo aos botões da Loja é a regra genérica `#menu button:not(...)`,
     e a mesa mora fora de #menu de propósito (é aberta também do fim de
     partida). Então o botão precisa das cores e da medida aqui, senão sai o
     botão cru do navegador. */
  #lojaMesaFechar {
    font-size: 18px;
    padding: 12px 34px;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #lojaMesaFechar:hover {
    background: var(--cor-vermelho-hover);
  }
  /* Escondido por `visibility` e não por `display`: aparecendo por display ele
     empurrava a mesa inteira para cima no meio da revelação. */
  #lojaMesaFechar.oculto { visibility: hidden; }
  /* A folga em cima e embaixo é o espaço que a carta escolhida ocupa ao crescer
     (1,14x): sem ela a carta do meio cobre o botão. */
  .loja-mesa {
    display: flex;
    gap: 34px;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: center;
    margin: 30px 0 26px;
  }
  /* Duas linhas (2+1) é a saída da tela em que nem a menor escala cabe numa
     fileira só — quem decide é `medirMesa` (relicario.js), que também escreve o
     `max-width` de onde o `wrap` quebra. Três cartas empilhadas em duas linhas
     continuam todas na tela; cortadas pelo `overflow: hidden` do overlay, não. */
  .loja-mesa.duas-linhas {
    flex-wrap: wrap;
    margin-inline: auto;
  }
  .carta-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
  }
  /* Entrada: as cartas sobem de fora da tela, uma depois da outra.
     `jump-none` porque `steps(9)` (jump-end) nunca chega ao último valor — a
     carta parava a um nono do caminho e ficava mais baixa que as outras para
     sempre. */
  @keyframes carta-entrar {
    from { transform: translateY(90vh); }
    to { transform: translateY(0); }
  }
  .carta-slot.entrando {
    animation: carta-entrar 0.55s steps(9, jump-none) both;
    animation-delay: var(--atraso);
  }
  /* Flutuação: as três partem da mesma linha de repouso (é uma mesa, não um
     arco), mas com amplitude, ritmo e fase próprios — é isso que faz elas não
     parecerem um bloco só. */
  @keyframes carta-flutuar {
    0% { transform: translateY(calc(-1 * var(--amp))); }
    25% { transform: translateY(0); }
    50% { transform: translateY(var(--amp)); }
    75% { transform: translateY(0); }
    100% { transform: translateY(calc(-1 * var(--amp))); }
  }
  .carta-flutua {
    position: relative;
    animation: carta-flutuar var(--dur) steps(6) infinite;
    animation-delay: var(--fase);
  }
  .carta-flutua.parada {
    animation: none;
    transform: translateY(0);
  }
  .carta-flutua.escolhivel { cursor: pointer; }
  .carta-flutua.escolhivel:hover .carta {
    box-shadow: 0 0 0 3px var(--cor-texto), 0 0 26px rgba(255, 255, 255, 0.35);
  }
  /* Quem escolhe pelo controle recebe o mesmo halo — a carta da mesa é
     `role="button"` + `tabindex` (ver relicario.js), e sem isto a decisão mais
     cara do Relicário se apontava com um anel de 2px por dentro do desenho da
     carta. Mesmo raciocínio da carta da escolha de item (ver
     `.item-choice-botao`), inclusive o `outline: none`: aqui o branco é o do
     sistema (`--cor-realce-foco`) e não o `--cor-texto` do hover, porque este
     anel é o realce de foco e não a afordância de ponteiro. */
  body.foco-sem-ponteiro .carta-flutua:focus .carta {
    box-shadow: 0 0 0 3px var(--cor-realce-foco), 0 0 26px rgba(255, 255, 255, 0.35);
  }
  body.foco-sem-ponteiro .carta-flutua:focus { outline: none; }
  /* Cresce e encolhe a partir da **base**, e não do centro: as três continuam
     apoiadas na mesma linha da mesa, e o que a escolhida ganha de tamanho ela
     ganha para cima, longe do botão. A recusada perde a cor **e** o tamanho; é
     a diferença entre as duas que diz qual é a sua. */
  .carta-flutua.recusada,
  .carta-flutua.escolhida { transform-origin: center bottom; }
  .carta-flutua.recusada { transform: translateY(0) scale(0.84); }
  .carta-flutua.escolhida {
    transform: translateY(0) scale(1.14);
    z-index: 2;
  }
  .carta.recusada {
    filter: grayscale(1) brightness(0.55);
    box-shadow: none;
  }
  .carta.escolhida {
    box-shadow: 0 0 0 4px var(--cor-raridade), 0 0 48px var(--cor-raridade);
  }

  /* ===== Tela das metas do ciclo (a visão "Metas") =====
     As três metas sorteadas para o ciclo, cada uma numa linha com a descrição,
     o selo do estado, a barra de progresso e a moeda que ela paga. Só o
     **interior** do painel mora aqui: a moldura, os pregos e a rolagem são de
     `.menu-painel`, como em toda tela do menu.

     A tela inteira é pixel art pelas regras de public/css/CLAUDE.md: canto reto,
     relevo sólido deslocado e nenhuma transição — a barra não desliza de um
     valor para o outro, ela é redesenhada na largura nova a cada pintura (o
     mesmo que `.loja-progresso` faz).

     **Os três estados se distinguem por três coisas ao mesmo tempo**, e não por
     uma cor só: o selo escrito (`Em andamento` / `Cumprida` / `Recebida`), a
     faixa da esquerda e o preenchimento do trilho. Cor sozinha não serve — uma
     linha cumprida e uma recebida a 60% de opacidade seriam a mesma linha para
     quem não distingue verde de ouro. E a cumprida tem uma quarta marca, que é
     a única acionável da tela: o botão "Receber recompensa". */
  .metas-barra {
    display: flex;
    flex-wrap: nowrap;
    align-items: baseline;
    gap: 10px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--cor-borda);
  }
  .metas-barra h2 { text-align: left; }
  /* Quanto falta para as três trocarem. Empurrada para a borda direita, e a
     primeira a encolher numa tela estreita: o texto corta, a tela não quebra. */
  .metas-ciclo {
    flex: 0 1 auto;
    margin-left: auto;
    overflow: hidden;
    white-space: nowrap;
    font-size: 12px;
    color: var(--cor-texto-fraco);
  }
  .metas-ciclo strong { color: var(--cor-texto-suave); }
  .metas-lista {
    list-style: none;
    margin: 12px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  /* A linha é a mesma peça de pedra da linha do catálogo do Relicário e da do
     Monstruário: fundo afundado, contorno de um pixel e o relevo sólido
     deslocado, sem raio nenhum. A faixa da esquerda é o que muda por estado. */
  .meta-linha {
    padding: 10px 12px;
    background: var(--cor-superficie-baixa);
    border: 1px solid var(--cor-borda);
    border-left: 4px solid var(--cor-cinza-medio);
    box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.5);
  }
  .meta-cabeca {
    display: flex;
    align-items: baseline;
    gap: 10px;
  }
  .meta-descricao {
    flex: 1 1 auto;
    font-size: 14px;
    color: var(--cor-texto);
  }
  /* O estado escrito. Maiúscula pequena, porque ele é rótulo e não frase — e
     porque assim ele não briga com a descrição pela leitura da linha. */
  .meta-selo {
    flex: 0 0 auto;
    font-size: 10px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--cor-texto-fraco);
  }
  /* O trilho é a mesma gramática de `.loja-progresso` e da vida da base no HUD:
     trilha afundada, preenchimento cheio, nenhuma borda arredondada. */
  .meta-trilho {
    margin: 8px 0 6px;
    height: 8px;
    background: var(--cor-fundo);
    box-shadow: inset 2px 2px 0 rgba(0, 0, 0, 0.55);
  }
  .meta-trilho-cheio {
    display: block;
    height: 100%;
    width: 0;
    background: var(--cor-cinza-medio);
    box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.35);
  }
  /* Três peças na mesma linha: o progresso, a moeda e a XP. `wrap` porque em
     tela estreita elas não cabem, e o que se quer nesse caso é a **XP inteira
     descer** para a linha de baixo — não a frase do progresso picada no meio.
     Medido em 360px com os 15 textos do catálogo: sem o `nowrap` das peças,
     "30min de 30min" saía em três linhas ("30min" / "de" / "30min"), porque
     `.meta-progresso` era o único a ceder e quebrava por dentro. Nunca houve
     estouro horizontal — o defeito era de leitura, não de corte. */
  .meta-pe {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
    font-size: 12px;
    color: var(--cor-texto-fraco);
  }
  .meta-pe > * { white-space: nowrap; }
  .meta-progresso { flex: 1 1 auto; }
  /* A moeda reusa `.loja-moeda` (o quadrado com relevo): é a mesma moeda do
     Relicário, e um segundo desenho dela seria uma segunda moeda. */
  .meta-moedas {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--cor-moeda);
  }
  .meta-moedas .loja-moeda {
    width: 12px;
    height: 12px;
    box-shadow: inset -2px -2px 0 #a16207, inset 2px 2px 0 #fde68a;
  }
  /* A XP fica ao lado da moeda, e **sem** ícone: a moeda tem um desenho próprio
     (o quadrado com relevo do Relicário) e a XP nunca teve — inventar um só para
     emparelhar criaria um símbolo que o resto do jogo não usa. O que separa as
     duas é a cor, a mesma que a barra de XP do fim de partida já usa. */
  .meta-xp {
    flex: 0 0 auto;
    color: var(--cor-destaque);
  }
  /* --- Em andamento: o estado neutro, e o único em que a barra não está cheia.
     Não existe estado de "falhou" (a spec o proíbe com cenário): uma meta que
     o ciclo levou sem ser cumprida simplesmente sai. */
  .meta-andando .meta-trilho-cheio { background: var(--cor-ouro-suave); }
  /* --- Cumprida e à espera do clique: o verde de sucesso na faixa, no selo e
     no trilho cheio. É o **único** estado com botão, e vale para as duas
     naturezas de meta: desde que o fechamento passou a só registrar, a de
     partida também espera aqui. */
  .meta-cumprida { border-left-color: var(--cor-sucesso); }
  .meta-cumprida .meta-selo { color: var(--cor-sucesso); }
  .meta-cumprida .meta-trilho-cheio { background: var(--cor-sucesso); }
  /* --- Recebida: a moeda e a XP já entraram. A linha inteira **recua** — é
     história, não ação —, e a faixa e o trilho vão para o ouro da moeda. O
     recuo é opacidade e não uma cor cinza escrita à mão para a linha continuar
     legível sobre a pedra do painel. */
  .meta-recebida {
    opacity: 0.62;
    border-left-color: var(--cor-moeda);
  }
  .meta-recebida .meta-selo { color: var(--cor-moeda); }
  .meta-recebida .meta-trilho-cheio { background: var(--cor-moeda); }
  /* O ponto de talento que o recebimento concedeu, quando concedeu: ele só
     aparece na linha repintada depois do clique, e some na pintura seguinte.
     Na cor do destaque, ao lado da XP, porque é consequência dela. */
  .meta-ponto { color: var(--cor-destaque); }
  /* A nota da acumulada que alcançou o alvo antes de um fechamento a
     reconhecer. Fora do `.meta-pe` porque ela é frase, e ali cada peça é
     `nowrap` — dentro, ela estouraria a linha em 320px. */
  .meta-nota {
    margin: 6px 0 0;
    font-size: 11px;
    line-height: 1.4;
    color: var(--cor-texto-fraco);
  }
  /* ===== O botão de receber, e o bloco das pendentes =====
     O botão é a única ação desta tela, e é ele que credita a moeda e a XP (o
     fechamento da partida só registra). Fica no pé da linha cumprida, na
     própria fileira, para o alvo de toque não disputar espaço com o texto.

     A **caixa** é a mesma dos botões de ação do menu (moldura de fliperama por
     `clip-path`, canto reto, sombra sólida deslocada, herdadas do bloco de
     pixel art no fim do arquivo). O que muda aqui é o tamanho: a regra genérica
     `#menu button:not(...)` daria 20px de fonte e largura cheia, e vencê-la
     exige **dois ids** (ela vale 1 id + 10 classes), o mesmo truque de
     `#relicarioListPanel #lojaComprarBtn`. A medida é a do alvo de toque, 44px
     de altura, e não a do botão de menu. */
  .meta-acao {
    display: flex;
    margin-top: 10px;
  }
  #menuViewMetas #metasPainel .meta-receber {
    width: auto;
    flex: 0 0 auto;
    min-height: 44px;
    font-size: 15px;
    padding: 10px 20px;
  }
  /* Ocupado: o pedido está no ar. `aria-busy`/`aria-disabled` e **nunca**
     `disabled` — o navegador tira o foco de um botão desabilitado, e quem está
     no controle ficaria sem foco na tela no meio do gesto (ver
     docs/controles-de-video-game.md). Como ele continua focável, o apagado é
     só visual, na mesma gramática de `#lojaComprarBtn.sem-moedas`. */
  #menuViewMetas #metasPainel .meta-receber[aria-busy="true"] {
    opacity: 0.55;
    filter: grayscale(1);
    cursor: progress;
  }
  /* Chamar atenção é o mesmo pisca de `#lojaComprarBtn.pode-comprar`, e pelo
     mesmo motivo de ser `steps(2, jump-none)`: com um passo só o valor é o do
     início durante todo o intervalo e nada pisca (medido no navegador, ver o
     bloco da Loja). Sem o pulo: aqui pode haver **quatro** botões na tela ao
     mesmo tempo (as três de hoje mais as pendentes), e quatro caixas subindo e
     descendo viram ruído em vez de notícia. */
  #menuViewMetas #metasPainel .meta-receber:not([aria-busy="true"]) {
    animation: meta-receber-brilho 1.1s steps(2, jump-none) infinite;
  }
  @keyframes meta-receber-brilho {
    0% { background: var(--cor-vermelho); }
    100% { background: var(--cor-vermelho-destaque); }
  }
  /* O recado de uma falha recuperável, na própria linha: a meta continua à
     espera e o botão voltou. Vermelho porque é recusa, e pequeno porque a ação
     de novo é o botão logo acima. */
  .meta-recado {
    margin: 6px 0 0;
    font-size: 12px;
    line-height: 1.4;
    color: var(--cor-vermelho-destaque);
  }
  /* As cumpridas de ciclos que já viraram. Título próprio porque elas não são
     as metas de hoje — o que as agrupa é justamente não serem —, e a lista
     reusa `.metas-lista` inteira: o desenho da linha é o mesmo, só sem a barra
     (o progresso delas já é história). */
  .metas-pendentes-titulo {
    margin: 18px 0 0;
    font-size: 13px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--cor-texto-fraco);
  }
  .metas-pendentes { margin-top: 8px; }
  /* O rodapé explica o ciclo uma vez, embaixo das três: a regra de "vira e
     troca, sem penalidade" não cabe em cada linha e não pode faltar. */
  .metas-rodape {
    margin: 14px 0 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--cor-texto-fraco);
  }
  /* Celular: a linha continua a mesma peça, só mais apertada — o que sai é
     folga, nunca informação. A descrição é o que ganha a largura de volta. */
  body.tela-estreita .meta-linha { padding: 8px 10px; }
  body.tela-estreita .meta-descricao { font-size: 13px; }
  body.tela-estreita .metas-ciclo { font-size: 11px; }
  /* Deitado no celular (844x390) o que encolhe é folga e fonte, e o botão de
     receber **não** entra nisso: 44px é alvo de toque, não estilo, e é a única
     ação da tela. O que ele perde é o padding lateral e um ponto de fonte. */
  body.tela-baixa-deitada .meta-linha { padding: 8px 10px; }
  body.tela-baixa-deitada .metas-pendentes-titulo { margin-top: 12px; }
  body.tela-baixa-deitada #menuViewMetas #metasPainel .meta-receber {
    min-height: 44px;
    font-size: 14px;
    padding: 8px 16px;
  }

  /* ===== Barra da Loja =====
     O cabeçalho da visão "Itens": título, o progresso do catálogo e o saldo.
     **Uma fileira só, e sem o botão** — ele mora em `.loja-rodape`, abaixo da
     lista.

     A barra já teve tudo junto, e no celular ela comia a tela: 154px de altura
     em retrato (390x844) e 135px deitado (844x390), sobrando 141px de lista —
     o cabeçalho ocupava quase tanto quanto o catálogo de 47 itens. O motivo era
     o botão: ele herda `width: 100%`, `font-size: 20px` e `padding: 14px 32px`
     da regra genérica `#menu button:not(...)`, escrita para os botões do menu
     principal, onde ser o único alvo da tela faz sentido.

     `nowrap` porque nada aqui pode cair para uma segunda linha: o que não cabe
     encolhe (a barra de progresso) ou é cortado (a contagem em texto, que a
     barra repete desenhada). */
  .loja-barra {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 10px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--cor-borda);
  }
  .loja-barra h2 { text-align: left; }
  /* A contagem desenhada: trilha afundada e preenchimento na cor da moeda, a
     mesma gramática da vida da base no HUD. É ela que absorve a folga da
     fileira (`flex: 1 1`), então numa tela estreita ela encolhe em vez de
     empurrar o saldo para fora. Preenchimento em passos inteiros de 5% — quem
     escreve a largura é relicario.js, arredondando, porque uma barra de pixel
     art não desliza. */
  .loja-progresso {
    flex: 1 1 60px;
    min-width: 24px;
    height: 8px;
    background: var(--cor-superficie-baixa);
    box-shadow: inset 2px 2px 0 rgba(0, 0, 0, 0.55);
  }
  .loja-progresso i {
    display: block;
    height: 100%;
    width: 0;
    background: var(--cor-moeda);
    /* O mesmo ouro escuro do relevo da moeda (`.loja-moeda`), escrito do mesmo
       jeito: é a sombra do mesmo material. */
    box-shadow: inset 0 -2px 0 #a16207;
  }
  /* ===== Rodapé de ação da Loja =====
     A explicação e o botão de comprar, abaixo da lista.

     `sticky` e não só o fim do painel: em `body.tela-estreita` o painel deixa
     de ter altura própria (`height: auto`, `overflow: visible`) e quem rola é a
     visão inteira — no fim do conteúdo o botão ficaria a uma rolagem de 4000px
     do jogador, que é pior do que estava. Grudado no fundo, ele acompanha a
     leitura do catálogo, que é justamente quando dá vontade de comprar.

     O fundo opaco é obrigatório: transparente, as linhas do catálogo passariam
     por baixo do botão. É a pedra do painel (`--cor-painel-pedra-veu`), a mesma
     que o ::after dele pinta. */
  .loja-rodape {
    position: sticky;
    bottom: 0;
    z-index: 1;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 12px;
    padding-top: 10px;
    background: var(--cor-painel-pedra-veu);
    border-top: 1px solid var(--cor-borda);
  }
  /* O botão do rodapé não é mais largura cheia. Vencer a regra genérica exige
     **dois ids**: ela vale 1 id + 8 classes, e `#lojaComprarBtn` sozinho perde.
     A medida é a do alvo de toque (44px de altura), não a do botão de menu. */
  #relicarioListPanel #lojaComprarBtn {
    width: auto;
    flex: 0 0 auto;
    font-size: 16px;
    padding: 11px 22px;
  }
  /* O texto de apoio da tela: de onde vem a moeda e o que ela compra. Fica ao
     **lado** do botão e absorve a folga do rodapé (era `flex: 0 0 100%`, uma
     linha inteira só para ela dentro do cabeçalho). O preço é escrito por
     relicario.js, na cor da moeda, porque é o número que liga a frase ao saldo. */
  .loja-explicacao {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    font-size: 12px;
    color: var(--cor-texto-fraco);
  }
  .loja-explicacao #lojaPreco,
  .loja-explicacao #lojaPasso {
    color: var(--cor-moeda);
    font-variant-numeric: tabular-nums;
  }
  /* O selo de coleção completa, que ocupa o lugar do botão de comprar quando
     não há mais carta para abrir (ver atualizarBarra em relicario.js). Tem a
     mesma caixa do botão — mesma altura de alvo, mesma borda reta, sombra
     sólida deslocada — para o rodapé não mudar de altura no momento da troca,
     e a cor do ouro porque ele é o carimbo do fim da coleção, não um aviso. */
  .loja-completo {
    flex: 0 0 auto;
    margin: 0;
    padding: 11px 22px;
    font-size: 16px;
    color: var(--cor-ouro);
    background: var(--cor-superficie-baixa);
    border: 1px solid var(--cor-ouro-suave);
    box-shadow: 3px 3px 0 var(--cor-pedra-borda);
  }
  .loja-saldo {
    display: flex;
    /* Não encolhe: quem absorve a folga da fileira é a barra de progresso. Sem
       isto o `nowrap` da barra espremia o número do saldo contra a borda. */
    flex: 0 0 auto;
    align-items: center;
    gap: 8px;
    font-size: 20px;
    color: var(--cor-moeda);
    font-variant-numeric: tabular-nums;
  }
  /* A moeda é desenhada em CSS, e não é um ícone de fonte nem um emoji: um
     quadrado com relevo sólido deslocado é a mesma gramática do resto da
     interface (ver public/css/CLAUDE.md), e um emoji muda de desenho a cada
     sistema operacional. */
  .loja-moeda {
    width: 18px;
    height: 18px;
    background: var(--cor-moeda);
    box-shadow: inset -3px -3px 0 #a16207, inset 3px 3px 0 #fde68a;
  }
  /* A contagem em texto é a primeira a ir embora quando a fileira aperta: a
     barra de progresso ao lado diz o mesmo sem cobrar largura. */
  .loja-restam {
    flex: 0 1 auto;
    overflow: hidden;
    white-space: nowrap;
    font-size: 12px;
    color: var(--cor-texto-fraco);
  }
  /* Saldo insuficiente: o botão **parece** desabilitado mas continua clicável
     (sem `disabled`), pelo mesmo motivo do nó de talento sem ponto
     (`.talent-no-sem-pontos`) — botão desabilitado não dispara clique nenhum, e
     é esse clique que explica a recusa. */
  #lojaComprarBtn.sem-moedas {
    opacity: 0.45;
    filter: grayscale(1);
    cursor: not-allowed;
  }
  /* ===== Dá para abrir carta: o botão avisa (`.pode-comprar`) =====
     Saldo suficiente (ou uma carta paga esperando escolha) é a única notícia
     desta tela, e antes dela o botão só **deixava** de estar cinza — nenhum
     movimento, no canto que o jogador já aprendeu a ignorar. Aqui ele pisca e
     pula. Só nesse estado: um botão que se mexe sempre não é notícia nenhuma, e
     é o botão parado de `.sem-moedas` que faz o aceso ser visto.

     O brilho é a **cor de fundo** e não uma borda ou um halo: o botão tem
     `clip-path` (a moldura de fliperama), e `box-shadow` externo é apagado por
     ele — foi o que já obrigou a sombra do `.menu-painel` a virar
     `filter: drop-shadow`. Trocar o fundo tem um bônus: as duas camadas da
     moldura pintam com `background: inherit`, então a borda escura acompanha o
     pisca sozinha, sem uma segunda animação.

     O pisca é `steps(2, jump-none)` num ciclo de 1,1s — meio segundo em cada
     cor, discreto, sem fade. **Não** é `steps(1, end) infinite alternate`, que é
     como o resto do arquivo escreve pisca (`.aba-selo`,
     `#itemChoiceTempoFill.urgente`, `.item-choice-travado`): essa forma não
     pisca. Medido no navegador — o computed do `background` fica no keyframe de
     0% o ciclo inteiro e as cores dos outros quatro seletores nunca aparecem.
     Com um passo só, o valor é o do início durante todo o intervalo e o salto
     cai exatamente no fim; `alternate` inverte o tempo, mas o easing invertido
     devolve o mesmo 0. `jump-none` com dois passos é o que dá dois valores
     discretos dentro de um ciclo.

     O pulo tem pausa longa dentro do próprio ciclo: pular sem parar vira ruído
     em três segundos, e são 6px em degraus de 3 — em pixels inteiros, como manda
     "Pixel art no CSS". */
  #lojaComprarBtn.pode-comprar {
    animation:
      loja-pode-comprar-brilho 1.1s steps(2, jump-none) infinite,
      loja-pode-comprar-pulo 1.4s steps(1, end) infinite;
  }
  @keyframes loja-pode-comprar-brilho {
    0% { background: var(--cor-vermelho); }
    100% { background: var(--cor-vermelho-destaque); }
  }
  @keyframes loja-pode-comprar-pulo {
    0%, 60% { transform: translateY(0); }
    70% { transform: translateY(-3px); }
    80% { transform: translateY(-6px); }
    90% { transform: translateY(-3px); }
    100% { transform: translateY(0); }
  }
  /* Com a mesa da compra aberta o botão está atrás dela: o jogador já **está**
     na compra, e não há notícia nenhuma a dar. */
  body:has(#lojaMesaOverlay.aberto) #lojaComprarBtn.pode-comprar {
    animation: none;
  }
  /* Quem pediu menos movimento fica com o pisca, que é a informação; o pulo,
     que é o deslocamento, sai. É a primeira regra de `prefers-reduced-motion`
     do arquivo, e ela entra aqui porque este é o primeiro movimento **de
     posição** e **sem fim** da interface do menu: as outras animações infinitas
     ou trocam cor no lugar (o selo da aba, o cronômetro) ou pertencem à arena. */
  @media (prefers-reduced-motion: reduce) {
    #lojaComprarBtn.pode-comprar {
      animation: loja-pode-comprar-brilho 1.1s steps(2, jump-none) infinite;
    }
  }
  /* Clicou em comprar sem saldo: o saldo pisca em vermelho — o número e a moeda
     juntos, porque é o par que carrega o motivo. Piscada em degraus (steps),
     não fade: mesma regra de pixel art do resto da interface. O som de erro que
     acompanha está em `avisarSemMoedas` (relicario.js). */
  .loja-saldo.sem-moedas #lojaSaldo {
    animation: loja-sem-moedas 0.16s steps(1) 4;
  }
  .loja-saldo.sem-moedas .loja-moeda {
    animation: loja-sem-moedas-moeda 0.16s steps(1) 4;
  }
  @keyframes loja-sem-moedas {
    0% { color: var(--cor-vermelho-destaque); }
    50% { color: var(--cor-moeda); }
  }
  @keyframes loja-sem-moedas-moeda {
    0% { background: var(--cor-vermelho-destaque); }
    50% { background: var(--cor-moeda); }
  }
  /* ===== A Loja nas telas apertadas =====
     Duas correções, e as duas atacam a mesma coisa: o rodapé de ação é a
     segunda faixa fixa da tela, e faixa fixa cobra altura da lista.

     1) Paisagem curta e larga (o celular em modo touch, que joga deitado): não
     há altura para duas faixas — 54px de rodapé em 390px de tela é a lista
     perdendo um terço do que acabou de ganhar. Aqui o painel vira **grade**: a
     barra e o rodapé dividem a primeira linha, e a lista fica com as duas
     colunas da segunda. Sem duplicar o botão no HTML, que é o preço da
     alternativa óbvia.

     Não vale em `.tela-estreita` (a paisagem **estreita**, 568x320): lá o
     painel não tem altura própria — quem rola é a visão inteira — e o botão no
     topo sairia da tela junto com a barra. Nessa faixa o `sticky` do rodapé é
     justamente o que salva. */
  body.tela-baixa-deitada:not(.tela-estreita) #relicarioListPanel {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto minmax(0, 1fr);
  }
  /* `align-self` item a item, e não `align-items: center` no painel: centrado
     no painel inteiro, o item da segunda linha (a lista) perde o `stretch` e
     passa a ter a altura do conteúdo — 2166px de lista centralizados numa linha
     de 200px, metade dela fora do painel. */
  body.tela-baixa-deitada:not(.tela-estreita) .loja-barra {
    grid-area: 1 / 1;
    align-self: center;
    padding-bottom: 0;
    border-bottom: none;
  }
  body.tela-baixa-deitada:not(.tela-estreita) .loja-rodape {
    grid-area: 1 / 2;
    align-self: center;
    position: static;
    padding-top: 0;
    background: transparent;
    border-top: none;
  }
  body.tela-baixa-deitada:not(.tela-estreita) #relicarioList {
    grid-area: 2 / 1 / 3 / 3;
    padding-top: 10px;
    border-top: 1px solid var(--cor-borda);
  }
  /* 2) A frase de apoio some das duas faixas mais apertadas. Deitado ela não
     tem onde caber; em retrato de 390px ela quebra em **cinco** linhas ao lado
     do botão e devolve o rodapé a 91px — quase o cabeçalho de 154px que esta
     mudança foi desfazer. O que ela ensina sobrevive sem ela: o preço está
     escrito no próprio botão ("Abrir carta — 30") e o saldo, na barra do topo.
     O que se perde é a explicação de que o preço **sobe** a cada carta — e ela
     se explica sozinha no botão, que é justamente onde o número novo aparece
     depois de cada compra. Nessa largura o botão fica com a faixa inteira, que
     é também o maior alvo de toque possível. */
  body.tela-baixa-deitada .loja-explicacao,
  body.tela-muito-estreita .loja-explicacao {
    display: none;
  }
  /* Nessa largura a contagem em texto não cabe na fileira (ela era cortada no
     meio de "faltam 22"), e quem conta é a barra desenhada ao lado — que ganha
     a folga inteira e passa a ser legível de verdade. Quem lê a tela não perde
     nada: o rótulo acessível da contagem é o da barra. */
  body.tela-muito-estreita .loja-restam {
    display: none;
  }
  body.tela-muito-estreita #relicarioListPanel #lojaComprarBtn,
  body.tela-muito-estreita .loja-completo {
    flex: 1 1 auto;
    width: 100%;
    text-align: center;
  }
  /* Item que a conta ainda não possui: ícone apagado, nome esmaecido e, no
     lugar da descrição, a marca de conteúdo oculto. O que ele **faz** é o que a
     Loja vende — o nome e o ícone ficam, porque é preciso saber que ele existe
     para querer comprá-lo. */
  .relicario-item.bloqueado {
    background-image: none;
    background-color: var(--cor-superficie-baixa);
    /* A pedra é a mesma da linha possuída (a peça não muda de material por
       estar trancada): o que sai é a cor da raridade na faixa, que vira o
       cinza da borda. */
    box-shadow: inset 3px 0 0 var(--cor-borda),
      var(--sombra-pedra-pequena-interna);
  }
  .relicario-item.bloqueado .relicario-icone { opacity: 0.35; }
  .relicario-item.bloqueado .relicario-item-nome { color: var(--cor-texto-fraco); }
  .relicario-item-oculto {
    font-size: 12px;
    letter-spacing: 0.2em;
    color: var(--cor-texto-fraco);
    opacity: 0.7;
  }
  /* O cadeado é pixel art desenhada por `desenharIcone`, como qualquer ícone do
     catálogo — o 🔒 que ele substituiu era a única coisa da tela que não era
     pixel art, e mudava de desenho a cada sistema operacional. */
  .relicario-cadeado {
    flex-shrink: 0;
    margin-left: auto;
    align-self: center;
    opacity: 0.75;
    background: transparent;
  }
  /* O item recém-comprado pisca ao voltar para a lista, para o jogador achar o
     que acabou de ganhar no meio do catálogo. Em `steps()`, como todo o resto. */
  .relicario-item.novo { animation: relicario-novo 1.6s steps(2) 4; }
  @keyframes relicario-novo {
    0% { background-color: var(--cor-superficie-elevada); }
    50% { background-color: var(--cor-raridade-veu); }
    100% { background-color: var(--cor-superficie-elevada); }
  }

  /* A escala da carta **não** sai daqui. Ela já saiu: `body.tela-estreita`
     valia 3 e `body.tela-muito-estreita`/`body.tela-baixa-deitada` valiam 2, e
     os degraus dessas classes (700px e 420px) não são os degraus em que a carta
     cabe — três cartas em escala 3 pedem 652px de janela, então entre 652 e 700
     as das pontas eram cortadas em silêncio, e o mesmo acontecia abaixo de
     448px com a escala 2. Nenhuma das duas olhava a altura.

     Hoje quem escolhe é o JS das duas telas que mostram carta, cada uma medindo
     o espaço **dela** (`medirMesa` em relicario.js, `planoDasCartas` em
     escolhaDeItens.js), e escreve `--escala-carta` no container. O `:root`
     continua com o padrão de 4, que é o que vale antes de qualquer medida.

     O que sobra por classe de tela é a **folga** entre as cartas, que é medida
     por esse mesmo JS ao decidir a escala. */
  body.tela-estreita .loja-mesa { gap: 18px; }
  body.tela-muito-estreita .loja-mesa,
  body.tela-baixa-deitada .loja-mesa { gap: 12px; }
  /* Tela baixa (celular deitado é a faixa em que isso vale): o que custa altura
     na mesa é o cerco dela — título, subtítulo, folgas e o botão —, não a carta.
     Sem este bloco a conta de `medirMesa` sobra menos altura e a escala cai um
     degrau à toa; com ele, um aparelho de 390px ainda comporta a escala 2. */
  body.tela-baixa-deitada #lojaMesaOverlay {
    gap: 10px;
    padding: 10px;
  }
  body.tela-baixa-deitada #lojaMesaTitulo { font-size: 17px; }
  body.tela-baixa-deitada #lojaMesaSubtitulo {
    min-height: 0;
    margin-top: -6px;
    font-size: 12px;
  }
  body.tela-baixa-deitada .loja-mesa { margin: 12px 0 10px; }
  body.tela-baixa-deitada #lojaMesaFechar {
    font-size: 15px;
    padding: 8px 24px;
  }

  /* ===== Textura de arena como fundo de botão =====
     A textura de chão do nível (`nivel.fundo`, shared/horde.js — a mesma
     imagem ladrilhada da partida, `ARENA_BG_SRC_HORDE` em arenaVisuals.js, e
     a mesma que preenche o retrato das medalhas de nível) entra como fundo de
     dois botões do navegador de salas: o quadrado da grade de nível e a linha
     de sala da lista. Assim o jogador reconhece o nível pelo chão antes de
     ler o nome — é a mesma pista visual que o retrato da medalha já usa.

     Uma classe por tipo de fundo, não `style` inline com a URL: a CSP do jogo
     não libera `style-src` inline (`src/server/httpServer.js`/`vercel.json`),
     então a imagem tem que sair de uma regra desta folha. A classe só declara
     `--textura-arena`; quem pinta (com o véu por cima) é cada componente,
     porque o enquadramento é diferente nos dois. O ladrilho é 320x320 e vai
     para 80px — divisão inteira, sem meio pixel de textura.

     `--veu-arena` sai daqui junto com a imagem (e não do componente) porque é
     o par dela: sem textura ele fica `transparent` e o componente segue com o
     fundo chapado de antes, sem ganhar um escurecimento do nada. */
  .textura-arena-terra,
  .textura-arena-gelo,
  .textura-arena-areia,
  .textura-arena-fogo,
  .textura-arena-vortice {
    --veu-arena: var(--cor-veu-textura-arena);
  }
  .textura-arena-terra {
    --textura-arena: url("../assets/arenas/defesa_da_base_1.png");
  }
  .textura-arena-gelo {
    --textura-arena: url("../assets/arenas/defesa_da_base_2.png");
  }
  .textura-arena-areia {
    --textura-arena: url("../assets/arenas/defesa_da_base_3.png");
  }
  .textura-arena-fogo {
    --textura-arena: url("../assets/arenas/defesa_da_base_4.png");
  }
  .textura-arena-vortice {
    --textura-arena: url("../assets/arenas/defesa_da_base_sem_fim.png");
  }
  .horde-rooms-section-label {
    margin: 0 0 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--cor-texto-fraco);
  }
  /* Linha rótulo + interruptor, reusada na sala de espera (#hordeLobbyPublicToggle,
     hordeLobbyUi.js) — mesmo componente nos dois lugares para "sala pública"
     ler igual em quem cria e em quem já está dentro. */
  .horde-toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
  }
  /* Sala de espera (hordeLobbyUi.js): as quatro caixas numa fileira só — o
     que descreve a sala (interruptor de visibilidade e código do convite) e
     as duas janelas que ela abre ("Trocar nível", "Talentos"). `stretch`
     iguala a altura das quatro pela mais alta; `center` mantém a fileira
     centrada no painel quando alguma delas não aparece (só o dono vê o
     interruptor e o "Trocar nível"). Nada de rótulo/interruptor esticado a
     largura toda como no picker de criação de sala (`.horde-toggle-row`).
     `nowrap` é regra, não detalhe: as quatro ficam **sempre** lado a lado.
     Com `wrap`, qualquer pixel a mais (o selo de talentos, o "Convidar" da
     build de desktop, a fonte que escala com `--largura-arena`) mandava
     "Talentos" sozinho para uma segunda linha. Quem cede quando falta espaço
     é o recuo das caixas (`flex-shrink` com `min-width: 0`), nunca a fileira. */
  #hordeLobbyInfoRow {
    display: flex;
    flex-wrap: nowrap;
    align-items: stretch;
    justify-content: center;
    gap: 8px;
    width: 100%;
  }
  /* Moldura de pedra pequena comum às quatro, para a fileira ler como um
     conjunto só e não como quatro coisas de famílias diferentes. */
  #hordeLobbyInfoRow > * {
    box-sizing: border-box;
    flex: 0 1 auto;
    min-width: 0;
    white-space: nowrap;
    min-height: 44px;
    padding: 6px 12px;
    background: var(--cor-superficie-baixa);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-pequena);
  }
  /* Em touch a fileira cabe numa linha só por pouco: a largura da caixa
     (limitada pelo `max-width` do `.modal`, igual no desktop) não sobra o
     bastante para o recuo de 12px de cada lado das quatro caixas, e
     "Talentos" — a última, e a mais estreita — é quem quebra para uma
     segunda linha. Aparelho touch nunca é o dono que só vê três caixas (o
     interruptor de sala pública e "Trocar nível" são só do dono, mas
     "Talentos" está em todo mundo), então esse aperto vale sempre em mobile,
     não só num caso raro. Reduzir o respiro interno (e o vão entre elas)
     devolve as quatro para a mesma linha sem cortar texto nenhum; o desktop
     (sem `body.touch-mode`) mantém o respiro de sempre. */
  body.touch-mode #hordeLobbyInfoRow {
    gap: 6px;
  }
  body.touch-mode #hordeLobbyInfoRow > * {
    padding: 6px 8px;
  }
  /* A caixa toda é o botão do interruptor (ver index.html): o alvo de clique
     é a moldura inteira, do mesmo tamanho das outras três da fileira, e não
     só o retângulo do interruptor. Por ser `<button>` precisa desfazer a
     fonte/cor do agente de usuário — a moldura e o recuo vêm de
     `#hordeLobbyInfoRow > *`. */
  #hordeLobbyPublicToggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    font-family: inherit;
    color: var(--cor-texto);
    cursor: pointer;
    text-align: left;
  }
  #hordeLobbyPublicToggle .horde-rooms-section-label {
    margin: 0;
  }
  #hordeLobbyPublicToggle:hover .horde-rooms-section-label {
    color: var(--cor-ouro-suave);
  }
  .horde-toggle-text {
    min-width: 0;
  }
  .horde-toggle-desc {
    margin: 2px 0 0;
    font-size: 12px;
    color: var(--cor-texto-fraco);
  }
  /* Interruptor: trilho retangular (nunca arredondado — pixel art, ver
     CLAUDE.md) com o bloco preenchendo metade dele e deslizando pro outro
     lado no estado ativo, em vez de um "slider" com curva. */
  .toggle-switch {
    flex: none;
    padding: 0;
    background: none;
    border: none;
  }
  .toggle-switch-track {
    display: block;
    width: 52px;
    height: 26px;
    box-sizing: border-box;
    padding: 3px;
    background: var(--cor-superficie-baixa);
    border: 2px solid var(--cor-borda);
  }
  .toggle-switch-thumb {
    display: block;
    width: 18px;
    height: 100%;
    background: var(--cor-texto-fraco);
  }
  .toggle-switch.ativo .toggle-switch-track {
    border-color: var(--cor-vermelho);
  }
  .toggle-switch.ativo .toggle-switch-thumb {
    background: var(--cor-vermelho);
    margin-left: calc(100% - 18px);
  }
  /* Grade de quadrados em duas linhas (3 + 2, a campanha inteira + o Sem
     fim), não uma fileira só: com o clique já criando a sala (ver
     `selecionarNivel` em hordeRoomBrowser.js) os quadrados cresceram para
     virar o único alvo de toque da modal, e 5 numa linha só forçava a modal
     inteira a esticar de largura para caber. */
  #hordeLobbyLevelList {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
    overflow: visible;
  }
  /* Maiores que antes: sem botão de confirmar embaixo, o quadrado passou a
     ser o único alvo de toque da modal (o clique já cria a sala, ver
     `selecionarNivel` em hordeRoomBrowser.js) — precisa ler bem e caber o
     dedo com folga. */
  .horde-level-item {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    box-sizing: border-box;
    width: 100%;
    height: 112px;
    /* Item de grade com `1fr` tem mínimo automático = tamanho do conteúdo
       (`min-width: auto`), que em tela estreita vence o `1fr` e faz a grade
       inteira vazar para fora da modal — a moldura chegou a ficar cortada na
       borda direita. Zerado, o quadrado aceita encolher abaixo do texto (que
       já quebra em qualquer lugar via `overflow-wrap: anywhere` no nome). */
    min-width: 0;
    aspect-ratio: 1;
    padding: 10px 8px;
    font-size: 15px;
    text-align: center;
    color: var(--cor-texto);
    font-family: inherit;
    --cor-fundo-campo: var(--cor-superficie-alta);
    --cor-borda-campo: var(--cor-borda);
  }
  /* A textura do nível preenche o **miolo** do quadrado, não a caixa inteira:
     a moldura em escadinha continua sendo as quatro camadas de gradiente da
     regra `.campo-pixel/.horde-level-item` (ver "Campos e quadrados de nível
     em pixel art"), e uma imagem no lugar delas viraria três
     recortes esticados de tamanhos diferentes, com emenda visível. Então a
     textura vem num ::before recuado pela espessura da borda (2px) e com o
     mesmo recorte — o degrau de 8px contado a partir da borda dele cai
     exatamente onde o miolo dos gradientes já terminava (2px + 8px = os 10px
     de recuo daquelas camadas). O quadrado bloqueado não recebe classe de
     textura (ver hordeRoomBrowser.js): ele é o negativo cinza de propósito, e
     mostrar o chão de um nível que ainda não abriu entregaria o que ele é. */
  .horde-level-item::before {
    content: '';
    position: absolute;
    inset: var(--espessura-borda-botao);
    clip-path: var(--recorte-botao-pixel);
    background-image:
      linear-gradient(var(--veu-arena, transparent) 0 0),
      var(--textura-arena, none);
    background-size: 80px 80px;
    background-repeat: repeat;
    background-position: center;
    pointer-events: none;
  }
  /* Quadrado de esqueleto, enquanto o progresso da conta não chega (ver
     `desenharEsqueletoDaGrade` em hordeRoomBrowser.js). O brilho padrão de
     `.skeleton-loading::after` cobre a caixa inteira, e aqui isso apagaria a
     moldura em escadinha — o quadrado leria como um retângulo cinza, e não como
     "um nível vindo". Recuado e recortado como a textura do `::before`, ele
     preenche só o miolo e a moldura continua de pé. */
  .horde-level-item.skeleton-loading::after {
    inset: var(--espessura-borda-botao);
    clip-path: var(--recorte-botao-pixel);
  }
  /* Número, nome e cadeado ficam por cima do ::before da textura (que é
     posicionado, e sem isto pintaria em cima deles). */
  /* Todo conteúdo do quadrado sobe acima da textura do `::before`, que é um
     posicionado com fundo e por isso pinta **por cima** de irmão estático. A
     marca pessoal entrou nesta lista depois de sumir por não estar nela — e
     sumiu só nos dois estados que não têm `opacity` própria, porque a
     `opacity` de `.horde-level-marca--nada` criava o contexto de empilhamento
     por acidente e salvava aquele caso sozinho. */
  .horde-level-numero,
  .horde-level-nome,
  .horde-level-lock,
  .horde-level-marca,
  .horde-level-medalhas {
    position: relative;
    z-index: 1;
  }
  .horde-level-numero {
    font-size: 30px;
    line-height: 1;
    font-weight: 700;
  }
  .horde-level-nome {
    font-size: 12px;
    line-height: 1.2;
    color: var(--cor-texto-fraco);
    overflow-wrap: anywhere;
  }
  /* A **marca pessoal** do jogador naquela fase (ver `preencherMarcaPessoal` em
     hordeRoomBrowser.js). Mais miúda que o nome do nível: ela é a última linha
     do quadrado e não pode competir com o número, que é o que o jogador procura
     na grade.

     `nowrap` com reticências, e não quebra: o quadrado é quadrado
     (`aspect-ratio: 1`) e uma segunda linha aqui empurraria a fileira de
     bolinhas para fora dele — em celular, para cima do cadeado do vizinho. O
     texto mais longo ("nunca jogada") cabe inteiro nas larguras de celular
     medidas; acima disso, a reticência é o degrau seguro. */
  .horde-level-marca {
    max-width: 100%;
    font-size: 9px;
    line-height: 1;
    color: var(--cor-texto-fraco);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Vencida é a única que ganha cor: é a marca que o jogador quer achar de
     relance na grade. A mesma verde de conquista completa
     (`.horde-level-item--completo`), e não uma cor nova. */
  .horde-level-marca--vencida {
    color: var(--cor-sucesso);
  }
  /* "nunca jogada" é ausência de marca, e tem de se ler como tal — um tom
     abaixo do resto, nunca como resultado. */
  .horde-level-marca--nada {
    color: var(--cor-texto-fraco);
    opacity: 0.7;
  }
  /* O Sem fim não é "mais um nível": número em dourado (a mesma cor
     de conquista/tier alto, não uma cor nova) o separa da fileira numerada
     mesmo parado — só assim ele lê como "o que vem depois da campanha" e não
     como um quinto degrau dela. */
  .horde-level-item--infinito .horde-level-numero {
    color: var(--cor-ouro);
  }
  /* As quatro bolinhas de conquista da fase (ver `bolinhasDeConquista` em
     hordeRoomBrowser.js): quadradinhos, não círculos — a regra de pixel art
     vale na interface também (ver public/css/CLAUDE.md), e um `border-radius`
     aqui destoaria da moldura em escadinha do próprio quadrado. Apagada é só o
     contorno sobre o fundo do nível; conquistada é o preenchimento cheio na
     cor do tier, que é a mesma régua de cor da grade de conquistas. */
  .horde-level-medalhas {
    display: flex;
    gap: 5px;
    /* Encosta no nome em vez de dividir o `gap: 6px` do quadrado com ele: a
       fileira pertence à fase, e afastada lia como um terceiro elemento
       solto. */
    margin-top: -2px;
  }
  .horde-level-medalha {
    /* 12px, e não os 8px que ela nasceu com: a bolinha é o alvo do balão de
       dica (dicaDeMedalha.js) e a cor do tier precisa ser reconhecível ao lado
       da grade de conquistas — em 8px o bronze e o ouro liam como a mesma
       mancha escura. Quatro delas com 5px de vão dão 63px, que cabem com folga
       no quadrado mais estreito (97px de largura em tela de celular). */
    width: 12px;
    height: 12px;
    box-sizing: border-box;
    border: 2px solid var(--cor-borda-clara);
    background: transparent;
  }
  .horde-level-medalha:hover {
    border-color: var(--cor-branco);
  }
  .horde-level-medalha.conquistada {
    border-color: transparent;
  }
  .horde-level-medalha.tier-bronze { background: var(--cor-tier-bronze); }
  .horde-level-medalha.tier-prata { background: var(--cor-tier-prata); }
  .horde-level-medalha.tier-ouro { background: var(--cor-tier-ouro); }
  .horde-level-medalha.tier-diamante { background: var(--cor-tier-diamante); }
  .horde-level-medalha.tier-lendario { background: var(--cor-tier-lendario); }
  /* ----- Balão de dica da bolinha (dicaDeMedalha.js) -----
     Vive no `<body>` e é `position: fixed`: a grade aparece na coluna de criar
     sala e dentro do overlay "Trocar nível" da sala de espera, e um balão preso
     ao quadrado seria cortado pelo próprio quadrado. A moldura em escadinha é a
     mesma de `#talentTooltip` (as duas dividem a regra de seis camadas mais
     abaixo, em "Moldura de modal na tooltip") — é o balão que o jogador já
     conhece da árvore de talentos, e um segundo desenho de balão só para quatro
     linhas de texto seria um dialeto a mais. z-index acima do 22 das modais da
     sala de espera, senão ele nasce atrás da janela que o abriu. */
  .dica-medalha {
    position: fixed;
    z-index: 40;
    display: flex;
    flex-direction: column;
    gap: 3px;
    max-width: 230px;
    box-sizing: border-box;
    /* Mesma folga de #talentTooltip: a moldura come 8px de escadinha por
       lado. */
    padding: 11px 13px;
    font-size: 11px;
    line-height: 1.3;
    color: var(--cor-texto-fraco);
    /* O balão nasce colado na bolinha; sem isto ele roubaria o `mouseover` de
       quem o abriu e piscaria sem parar. */
    pointer-events: none;
  }
  .dica-medalha[hidden] {
    display: none;
  }
  .dica-medalha-nome {
    font-size: 12px;
    color: var(--cor-branco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .dica-medalha-texto {
    color: var(--cor-texto-suave);
  }
  /* Última linha: já conquistada (verde, a mesma cor da moldura da fase
     completa) ou ainda não (cinza). O tier vem junto porque é ele que diz o
     valor da medalha, e a cor da bolinha sozinha não se traduz em nome. */
  .dica-medalha-estado {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .dica-medalha-estado.conquistada {
    color: var(--cor-sucesso);
  }
  /* ----- Balão dos ícones das barras do console (dicaDeBarra.js) -----
     Mesma moldura de modal do balão de talento e do de medalha (a regra de seis
     camadas acima): é a janela que o jogador já conhece, e um segundo desenho de
     balão para duas linhas de texto seria mais um dialeto.

     Vive no `<body>` em `position: fixed` porque nasce **à esquerda** do console
     — que é filho de `#hordeHud`, um overlay com `overflow` e `z-index`
     próprios. `z-index` 40 o põe acima do HUD (10) e da tela de escolha de item
     (13); `pointer-events: none` para ele não roubar o `hover` do ícone que o
     abriu e piscar sem parar. */
  .dica-barra {
    position: fixed;
    z-index: 40;
    display: flex;
    flex-direction: column;
    gap: 3px;
    max-width: 240px;
    box-sizing: border-box;
    /* Mesma folga de #talentTooltip: a moldura come 8px de escadinha por lado. */
    padding: 11px 13px;
    font-size: 11px;
    line-height: 1.3;
    color: var(--cor-texto-fraco);
    pointer-events: none;
  }
  .dica-barra[hidden] {
    display: none;
  }
  .dica-barra-nome {
    font-size: 12px;
    color: var(--cor-branco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .dica-barra-texto {
    color: var(--cor-texto-suave);
  }
  /* Excluído da regra genérica de button:hover (ver acima): sem "levantar" no
     hover, só a cor da borda muda. */
  .horde-level-item:not(:disabled):not(.horde-level-item--completo):not(.horde-level-item--atual):hover {
    --cor-borda-campo: var(--cor-borda-clara);
  }
  /* Servidor fora do ar (ver hordeRoomBrowser.js): a grade é o botão "criar
     sala" desta tela, então ela sai de serviço junto com o "Entrar com código".
     O quadrado continua visível — o jogador segue vendo o que existe e o que já
     desbloqueou —, só não responde. */
  .horde-level-item:disabled:not(.locked) {
    color: var(--cor-texto-fraco);
    cursor: default;
    --cor-fundo-campo: var(--cor-borda-media);
  }
  /* O bloqueado é o negativo do jogável: fundo cinza claro e tipografia
     escura, em vez de tipografia clara sobre cinza escuro. Invertido ele lê
     como "casa vazia" à primeira vista, sem precisar comparar com os
     vizinhos. */
  /* Fase com as **quatro** medalhas: moldura verde (`--cor-sucesso`, a mesma
     cor de "deu certo" do resto da interface), a única leitura da grade que
     funciona de longe — as bolinhas dizem *quais* faltam, a moldura diz *se*
     falta alguma. Vem antes da regra do nível atual de propósito: na janela
     "Trocar nível" um nível pode ser completo **e** o que está valendo, e ali
     o dourado do "é este o seu" é a informação que decide o clique.

     A cor é a da **moldura** (`--cor-borda-campo`), não um `outline`: o
     quadrado é recortado em escadinha pelo `clip-path` (ver "Campos e
     quadrados de nível em pixel art"), e um anel retangular por cima disso
     desenhava um retângulo de cantos vivos por fora dos degraus. Pintar a
     própria borda faz o realce seguir a silhueta — e é por isso que o hover
     acima se exclui de quem está realçado: mudar `--cor-borda-campo` no
     hover apagaria o verde e o dourado. */
  .horde-level-item--completo {
    --cor-borda-campo: var(--cor-sucesso);
  }
  /* Quadrado do nível que a sala de espera já está usando (só a janela de
     trocar nível marca algum): moldura dourada, o mesmo "este é o seu" dos
     cartões de jogador, e sem cursor de clique porque escolher o nível que já
     está valendo não é uma troca. */
  .horde-level-item--atual {
    --cor-borda-campo: var(--cor-ouro-suave);
    cursor: default;
  }
  .horde-level-item.locked {
    color: var(--cor-fundo);
    cursor: not-allowed;
    --cor-fundo-campo: var(--cor-cinza-claro);
  }
  .horde-level-item.locked .horde-level-nome,
  .horde-level-item.locked .horde-level-lock {
    color: inherit;
  }
  .horde-level-item.locked:hover {
    --cor-borda-campo: var(--cor-borda);
  }
  .horde-level-lock {
    flex: none;
    width: 16px;
    height: 16px;
    color: var(--cor-texto-fraco);
  }
  /* No bloqueado o rótulo é uma frase ("Em desenvolvimento"), não um nome
     curto: quebra em duas linhas e precisa caber junto com número e cadeado
     dentro do quadrado. */
  .horde-level-item.locked .horde-level-nome {
    font-size: 9px;
  }
  .horde-level-lock svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  #hordeRoomsJoinOverlay .campo-pixel {
    --cor-fundo-campo: var(--cor-superficie-elevada);
    --cor-borda-campo: var(--cor-branco);
  }
  #hordeRoomsJoinOverlay .campo-pixel:focus-within {
    --cor-borda-campo: var(--cor-vermelho-destaque);
  }
  #hordeRoomsCodeInput {
    text-transform: uppercase;
    letter-spacing: 3px;
    text-align: center;
  }
  #hordeRoomsJoinFeedback {
    margin: 0;
    min-height: 16px;
    font-size: 12px;
    color: var(--cor-erro);
  }
  .overlay-actions {
    display: flex;
    gap: 12px;
  }
  .overlay-actions button {
    flex: 1;
    padding: 12px 24px;
    font-size: 16px;
  }
  #btnHordeRoomsJoinConfirm {
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #btnHordeRoomsJoinConfirm:hover {
    background: var(--cor-vermelho-hover);
  }
  #btnHordeRoomsJoinBack {
    background: var(--cor-superficie);
    color: var(--cor-texto);
  }
  #btnHordeRoomsJoinBack:hover {
    background: var(--cor-superficie-hover);
  }

  /* ----- Grade de níveis em modo touch -----
     A grade da janela "Trocar nível" da sala de espera, mais compacta. */
  body.touch-mode .horde-level-item {
    aspect-ratio: auto;
    min-height: 64px;
    padding: 8px 6px;
  }
  body.touch-mode .horde-level-numero {
    font-size: 22px;
  }
  /* Em telas pequenas as células ficam menores: número + cadeado +
     "Em desenvolvimento" não cabem juntos. O texto diz a mesma coisa que o
     cadeado e mais, então quem sai é o ícone. */
  body.touch-mode .horde-level-lock {
    display: none;
  }

  /* Sala de espera cooperativa, dentro do overlay de espera (#waitingOverlay,
     que antes dela mostra só o spinner de conexão). O bloco
     inteiro é um painel de pedra (mesmo material dos painéis da tela inicial):
     solto no meio do overlay, as cinco informações (nível, código, grupo,
     cartões, contagem) apareciam como cinco textos empilhados sem relação
     entre si. */
  #hordeLobby {
    /* A laje é a mesma das telas do menu (`.menu-painel`), e não uma cópia do
       material: as duas camadas que a desenham são compartilhadas acima, e
       daqui saem só as três medidas da escala grande (escadinha de dois
       degraus, bisel e sombra de 8px) mais o prego dos cantos. Antes daqui a
       sala de espera era um retângulo chapado com sombra dura — a única
       superfície de tamanho de painel fora do material do resto do jogo. */
    --prego-lado: 8px;
    --prego-margem: 10px;
    --moldura-recorte: var(--recorte-botao-pixel);
    --moldura-bisel: var(--sombra-pedra-interna);
    --moldura-sombra: 8px 8px 0 rgba(0, 0, 0, 0.4);
    position: relative;
    /* Contexto de empilhamento próprio, mesmo motivo de `.menu-painel`: sem
       ele as camadas em `z-index` negativo cairiam atrás do véu do overlay em
       vez de ficarem entre ele e o conteúdo da sala. */
    isolation: isolate;
    display: none;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    width: min(92%, 620px);
    box-sizing: border-box;
    /* Piso de 14px e não os 10px de antes: o prego nasce a 10px do canto e tem
       8px de lado, então abaixo disso a primeira linha da sala passa por cima
       dele. */
    padding: clamp(14px, calc(0.025 * var(--largura-arena)), 24px);
    /* O material mora nas camadas, nunca no elemento. */
    background: transparent;
    box-shadow: none;
    /* Ampliada na proporção da arena (ver `escalaDaSalaDeEspera` em
       gameScale.js). `zoom` e não `transform: scale`: ele ocupa o espaço
       ampliado no fluxo, então a fileira de botões embaixo desce junto. */
    zoom: var(--escala-sala, 1);
  }
  #waitingOverlay.horde .overlay-buttons {
    zoom: var(--escala-sala, 1);
  }
  #hordeLobbyNivel {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    text-align: center;
  }
  #hordeLobbyNivelNome {
    font-size: clamp(12px, calc(0.032 * var(--largura-arena)), 22px);
    font-weight: 600;
    color: var(--cor-branco);
  }
  #hordeLobbyNivelInfo,
  #hordeLobbyConviteLabel {
    font-size: clamp(9px, calc(0.019 * var(--largura-arena)), 13px);
    letter-spacing: 0.5px;
    color: var(--cor-texto-fraco);
  }
  /* Rótulo + código na mesma caixa: o código sozinho não dizia para que
     serve, e "copiar" ao lado dele é a única pista de que é clicável. */
  #hordeLobbyConvite {
    display: flex;
    align-items: center;
    text-align: center;
  }
  #hordeLobbyPlayers {
    list-style: none;
    margin: 0;
    padding: 0;
    width: 100%;
    display: flex;
    justify-content: center;
    gap: 8px;
  }
  /* Os quatro slots dividem a largura em partes iguais (`flex: 1 1 0`): com
     largura de conteúdo, o cartão de um jogador de nome longo ficava do dobro
     do tamanho do vizinho. */
  #hordeLobbyPlayers li {
    display: flex;
    flex: 1 1 0;
    min-width: 0;
  }
  /* Cartão em coluna (sprite em cima, nome embaixo), não mais uma linha com
     ícone: com o personagem de verdade no lugar do ícone de 16px, o retrato é
     a informação principal do cartão. Quadrado (`aspect-ratio: 1`), e não um
     retângulo alto: todos os slots têm o mesmo formato, ocupados e vagos, para
     a fileira ler como uma grade de posições e não como cartões de tamanhos
     avulsos. */
  .horde-lobby-slot {
    /* Contêiner de consulta: o retrato lá embaixo se mede em `cqmin`, ou seja
       no tamanho que o cartão **de fato** tem. O cartão é 1/4 da fileira, que
       por sua vez sai do painel, que sai da arena — reconstruir isso a partir
       de `--largura-arena` era um palpite, e o palpite errava para menos: o
       mago ficava pequeno no meio de um quadrado quase vazio. Assim ele
       acompanha qualquer largura, inclusive a paisagem curta de celular, sem
       uma segunda tabela de medidas. */
    container-type: size;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    width: 100%;
    aspect-ratio: 1;
    min-width: 84px;
    box-sizing: border-box;
    padding: 6px 10px;
    border: none;
    font-family: inherit;
    font-size: clamp(10px, calc(0.022 * var(--largura-arena)), 16px);
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
  }
  .horde-lobby-slot.ocupado {
    /* `relative` por causa do `x` de remover, que é absoluto no canto. */
    position: relative;
    color: var(--class-color, var(--cor-texto));
  }
  /* Em paisagem curta de aparelho touch (a sala de espera girada por CSS, ver
     `body.tela-girada` — `--altura-tela` vira a largura física do aparelho, o
     recurso escasso ali), o quadrado que cabe folgado numa arena alta de
     desktop consome espaço vertical demais e esmaga o resto do painel
     (título, fileira de informação, contagem, botões). Encolhe só aqui —
     `min-width: 0` destrava o piso de 84px de cima, e o `max-width` (que o
     `aspect-ratio: 1` espelha em altura) é relativo a `--altura-tela`, não a
     um pixel fixo, para acompanhar aparelhos de tamanhos diferentes. O
     desktop (sem `body.touch-mode`) fica exatamente como estava. */
  body.touch-mode.tela-baixa-deitada #hordeLobbyPlayers li {
    justify-content: center;
  }
  /* Largura **e** altura explícitas, e não só `max-width` com o
     `aspect-ratio` de cima fazendo a conta: `li` estica os filhos pela cross
     axis por padrão (`align-items: stretch`), e isso vence o `aspect-ratio`
     antes dele ter a chance de igualar as duas medidas — o quadrado saía
     achatado (mais largo que alto) em vez de encolher junto. */
  body.touch-mode.tela-baixa-deitada .horde-lobby-slot {
    width: clamp(48px, calc(0.16 * var(--altura-tela)), 84px);
    height: clamp(48px, calc(0.16 * var(--altura-tela)), 84px);
    min-width: 0;
    gap: 0;
    padding: 3px 4px;
    font-size: clamp(7px, calc(0.02 * var(--altura-tela)), 11px);
  }
  /* O ícone e o `+` de vaga livre têm tamanho próprio (não herdam o
     `font-size` do cartão, ver as regras deles mais abaixo) — sem encolher os
     três juntos, eles continuavam do tamanho de desktop dentro de um cartão que
     ficou bem menor, e transbordavam por cima do nome. O **retrato** não
     precisa de regra aqui: ele se mede no cartão (`cqmin`), então encolher o
     cartão já o encolhe junto. */
  body.touch-mode.tela-baixa-deitada .horde-lobby-icon svg {
    width: 18px;
    height: 18px;
  }
  body.touch-mode.tela-baixa-deitada .horde-lobby-vazio-marca {
    font-size: clamp(11px, calc(0.045 * var(--altura-tela)), 20px);
  }
  /* Vaga livre lê como "cabe mais gente aqui": moldura tracejada e um `+` no
     lugar do retrato, em vez de um cartão sólido igual aos ocupados. */
  .horde-lobby-slot.vazio {
    color: var(--cor-texto-fraco);
    background: none;
    box-shadow: none;
    border: 2px dashed var(--cor-borda-media);
  }
  .horde-lobby-vazio-marca {
    font-size: clamp(18px, calc(0.05 * var(--largura-arena)), 34px);
    line-height: 1;
    color: var(--cor-pedra-borda);
  }
  /* Dourado marcando "você", a mesma marca que ele tem na tela inteira: aqui
     os quatro cartões são iguais em tudo o mais, então sem esse destaque não
     dá para saber qual é o seu. */
  .horde-lobby-slot.voce {
    color: var(--cor-ouro-suave);
    outline: 2px solid var(--cor-ouro-suave);
    outline-offset: -2px;
  }
  .horde-lobby-slot.voce .horde-lobby-nome {
    color: var(--cor-ouro-suave);
  }
  /* `x` de remover da sala, só nos cartões dos outros e só para o dono (ver
     hordeLobbyUi.js). Fica no canto do cartão, fora da coluna de conteúdo, para
     não empurrar sprite/nome/classe — daí o cartão ocupado ser `relative`.
     Discreto por padrão e vermelho no hover: é uma ação destrutiva num painel
     que o dono fica olhando enquanto espera, não um botão para clicar sem
     querer. */
  .horde-lobby-expulsar {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 16px;
    height: 16px;
    padding: 0;
    border: none;
    line-height: 16px;
    text-align: center;
    font-family: inherit;
    font-size: 11px;
    color: var(--cor-texto-fraco);
    background: none;
    cursor: pointer;
  }
  .horde-lobby-expulsar:hover {
    color: var(--cor-erro-texto);
    background: var(--cor-painel-pedra-veu);
  }
  /* Check de "já está pronto", no canto de cima à esquerda do cartão — o de
     cima à direita é do `x` de expulsar, e os dois aparecem juntos no cartão de
     um companheiro pronto na tela do dono. Absoluto pelo mesmo motivo do `x`:
     fora da coluna de conteúdo, para não empurrar sprite/nome/classe. */
  .horde-lobby-check {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 16px;
    height: 16px;
    line-height: 16px;
    text-align: center;
    font-size: 14px;
    font-weight: 600;
    color: var(--cor-sucesso);
  }
  /* Além do check, o cartão do companheiro pronto ganha uma moldura verde: é o
     que dá a resposta "quem falta?" de longe, sem procurar um caractere de 12px
     em quatro cartões. O próprio cartão fica de fora (`:not(.voce)`): o dourado
     ali é a identidade fixa de "este sou eu" — a mesma razão de a moldura da
     câmera na partida ser branca e não dourada — e o próprio estado já está
     escrito no botão, além do check. */
  .horde-lobby-slot.ocupado.pronto:not(.voce) {
    outline: 2px solid var(--cor-sucesso);
    outline-offset: -2px;
  }

  .horde-lobby-nome {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* O retrato ocupa o espaço que sobra no cartão: `cqmin` é o lado menor do
     cartão (ver `container-type` lá em cima), e os 66% deixam a folga do nome
     embaixo e do respiro das bordas. O zoom sobe junto porque `--sprite-box` é
     só a **janela**: o corpo do personagem tem 16px dentro de um quadro de
     100x100, então ele mede `0.16 * zoom` da janela — em 2.9 sobrava um
     bonequinho no meio de um quadrado vazio. O ponto de ancoragem sai de 0.45
     para 0.52 pelo mesmo motivo: 0.52 é o **meio do corpo** no quadro (ele vai
     de y=44 a y=60), e com o zoom alto qualquer desvio disso joga a cabeça ou
     os pés para fora da janela. */
  .horde-lobby-arte.class-sprite {
    --sprite-box: 66cqmin;
    --sprite-zoom: 5.2;
    --sprite-anchor: 0.52;
  }
  /* O retrato do **próprio** cartão é um botão: cada acionamento pede a
     próxima cor do personagem (ver criarSlotOcupado em hordeLobbyUi.js). Ele
     precisa se anunciar como acionável sem virar um botão de interface no meio
     do cartão — o desenho continua sendo o personagem, o que entra é a moldura
     tracejada em volta dele.
     Nada de transição contínua e nada de escala fracionária: a resposta ao
     toque é a moldura mudando de tracejada para sólida na cor do jogador (que
     é o que a troca vai produzir) e um degrau seco de 2px de subida. Canto
     reto, como o resto da interface. */
  .horde-lobby-arte-troca {
    border: 2px dashed var(--cor-borda-clara);
    border-radius: 0;
    /* `background-color`, e **nunca** o atalho `background`: ele zera junto o
       `background-size` e o `background-position` que `.class-sprite` usa para
       enquadrar o quadro do sprite dentro da tira. Com o atalho, a tira era
       desenhada no tamanho natural e a partir do canto — o retrato ficava
       vazio, sem erro nenhum no console. A imagem em si vem inline, de
       `applyClassSprite`. */
    background-color: transparent;
    padding: 0;
    cursor: pointer;
    color: inherit;
  }
  .horde-lobby-arte-troca:hover,
  .horde-lobby-arte-troca:focus-visible {
    border-style: solid;
    border-color: var(--class-color);
    transform: translateY(-2px);
  }
  /* O realce de foco tem de aparecer **por fora** da moldura tracejada: o
     `outline` fica embaixo de descendente posicionado (ver
     docs/controles-de-video-game.md), e o sprite do personagem é um deles. */
  .horde-lobby-arte-troca:focus-visible {
    outline: 2px solid var(--cor-texto);
    outline-offset: 2px;
  }
  /* No celular não há foco nem ponteiro pairando, então a moldura fica sempre
     visível — ela é a única pista de que o retrato responde.
     E **só** a moldura: um `min-width`/`min-height` de 44px aqui, pela folga
     de toque, estica a caixa do retrato acima do `--sprite-box` (medido: 44px
     contra os ~40 naturais), e o enquadramento do quadro é calculado a partir
     dessa variável — o personagem sai menor que a janela e fora do centro.
     Quem é o alvo grande do dedo é o cartão inteiro, que o retrato quase
     preenche. */
  body.touch-mode .horde-lobby-arte-troca {
    border-color: var(--class-color);
  }
  .horde-lobby-icon svg {
    width: 32px;
    height: 32px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
  }
  #hordeLobbyCountdown {
    font-size: clamp(11px, calc(0.026 * var(--largura-arena)), 18px);
    color: var(--cor-texto-fraco);
  }
  /* Aviso de ação que não chegou a quem manda na sala (ver hordeLobbyUi.js).
     Nasce vazio e sem ocupar altura: ele é a exceção, não uma linha fixa do
     painel — reservar espaço para ele deixaria um buraco na tela de toda sala
     que funciona. Vermelho claro da interface, o mesmo dos avisos de
     formulário. */
  #hordeLobbyAviso {
    font-size: clamp(10px, calc(0.022 * var(--largura-arena)), 14px);
    color: var(--cor-erro-claro);
    text-align: center;
  }
  #hordeLobbyAviso:empty {
    display: none;
  }
  /* Barra da espera: o mesmo trilho afundado das barras da partida. Drena em
     blocos de 5% (ver PASSOS_BARRA em hordeLobbyUi.js) — o número em m:ss
     sozinho não dá a noção de "quase acabando". */
  #hordeLobbyBar {
    width: 100%;
    height: 10px;
    box-sizing: border-box;
    padding: 2px;
    background: var(--cor-superficie-baixa);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-afundada);
    overflow: hidden;
  }
  #hordeLobbyBarFill {
    height: 100%;
    width: 100%;
    background: var(--cor-vermelho-destaque);
  }
  /* "Estou pronto" mora na fileira de botões do overlay de espera, que começa
     como só o spinner de conexão (`carregando`, ver overlays.js) — então o
     padrão é escondido, e quem o mostra é a sala cooperativa, quando ela chega
     e põe `.horde` no overlay. Ao contrário do antigo "Começar agora", ele é de todo mundo na
     sala: não há classe de permissão nenhuma. */
  #btnHordePronto {
    display: none;
  }
  #waitingOverlay.horde #btnHordePronto {
    display: block;
  }
  /* Confirmado: o botão passa a ser o "voltar atrás" (o texto vira "Cancelar
     pronto" em hordeLobbyUi.js), então sai do vermelho de ação e vai para o
     verde do estado que ele representa — o mesmo verde do check nos cartões,
     com o texto escuro por cima (igual `.talent-no-maxado`, que usa este
     mesmo verde de fundo). O seletor repete `#waitingOverlay button` porque a
     base vermelha dos botões do overlay tem um elemento a mais na
     especificidade (mesmo motivo de `#waitingOverlay button#btnLeaveQueue`). */
  #waitingOverlay button#btnHordePronto.ativo {
    background: var(--cor-sucesso);
    color: var(--cor-texto-sobre-tier);
  }
  #waitingOverlay button#btnHordePronto.ativo:hover {
    background: var(--cor-sucesso-claro);
  }
  /* Os dois botões que abrem janela por cima da sala de espera ("Trocar
     nível", só do dono, e "Talentos"). São `.botao-nu` de propósito: a moldura
     de fliperama vermelha dos botões do overlay é da ação da tela (ficar
     pronto, sair da fila), e três botões vermelhos empilhados na mesma tela
     não diriam qual é a principal. Aqui é a pedra pequena do resto do painel. */
  #btnHordeLobbyTrocarNivel,
  #btnHordeLobbyTalentos {
    font-family: inherit;
    font-size: clamp(10px, calc(0.022 * var(--largura-arena)), 14px);
    color: var(--cor-texto);
    cursor: pointer;
  }
  #btnHordeLobbyTrocarNivel:hover,
  #btnHordeLobbyTalentos:hover {
    color: var(--cor-ouro-suave);
  }
  /* O mesmo selo de pontos por gastar do botão da barra lateral, aqui no
     atalho de quem já está numa sala (ver public/js/talentPointsAviso.js).
     Absoluto no canto da própria moldura, e **não** no fluxo depois do
     rótulo: as quatro caixas de #hordeLobbyInfoRow cabem em uma linha por
     pouco, e os ~20px de um selo em linha empurravam "Talentos" para uma
     segunda fileira — o aviso quebraria o layout da sala de espera toda vez
     que houvesse ponto a gastar. Fica dentro do recuo da caixa (nada de
     deslocamento negativo), senão encostaria no botão vizinho da fileira. */
  #btnHordeLobbyTalentos[data-selo] {
    position: relative;
  }
  #btnHordeLobbyTalentos > .aba-selo {
    min-width: 14px;
    height: 14px;
    line-height: 14px;
    padding: 0 2px;
    font-size: 10px;
  }
  /* As duas janelas da sala de espera. A de nível é a mesma modal (e a mesma
     grade) da criação de sala; a de talentos não tem `.modal` nenhuma, porque
     o #talentTreePanel movido para dentro dela já traz a moldura de pedra
     dele — ver hordeLobbyUi.js. */
  #hordeLobbyLevelOverlay,
  #hordeLobbyTalentsOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 21;
  }
  #hordeLobbyLevelOverlay.visible,
  #hordeLobbyTalentsOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #hordeLobbyLevelOverlay .modal {
    /* Mesmos motivos da modal de criar sala: `relative` para o `✕` flutuante
       não colar no canto da janela, e 560px para a grade de 5 níveis caber
       sem espremer o texto dos quadrados. */
    position: relative;
    width: 560px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    padding: 20px 24px;
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  #hordeLobbyTalentsBox {
    position: relative;
    display: flex;
    max-width: calc(var(--largura-tela) - 32px);
    max-height: calc(var(--altura-tela) - 32px);
  }
  /* Dentro do overlay o painel é uma modal, não um painel do menu: troca o
     `entrada-fade` com atraso (que é a entrada em cascata das colunas do
     menu, ver #talentTreePanel) pelo `entrada-pop` curto de `.modal`. Com o
     original, a janela abria devagar e subindo, fora de compasso com o véu. */
  #hordeLobbyTalentsBox #talentTreePanel {
    animation: entrada-pop 0.15s ease;
    /* O teto de altura é **repetido** aqui, e não só na caixa: a caixa só tem
       `max-height`, então a altura da linha flex dela é a do conteúdo — o
       `stretch` do painel se estica nessa altura e a caixa é que fica com a
       barriga cortada. Em celular deitado o painel passava de 460px numa janela
       de 358px, e o que sobrava de fora era justamente o fim dele: o botão de
       comprar da ficha. Com o teto no próprio painel, o `overflow: hidden` dele
       e a corrente de `min-height: 0` de dentro (ver #talentTreeContent) levam
       a rolagem para a faixa das árvores, que é de quem ela é. */
    max-height: calc(var(--altura-tela) - 32px);
    min-height: 0;
  }
  /* O painel de talentos é movido para dentro desta caixa (hordeLobbyUi.js) e
     sai do #menu — com isso os botões dele perdem a regra genérica de botão de
     ação e voltavam ao branco do navegador. As medidas continuam vindo de
     `#talentTreeSummary #btnTalentTreeReset` / `#talentTreeErro
     #btnTalentTreeRetry`; aqui só a cor (a moldura de fliperama vem do bloco
     de `--recorte-botao-pixel` no fim do arquivo). */
  #hordeLobbyTalentsBox #btnTalentTreeReset,
  #hordeLobbyTalentsBox #btnTalentTreeRetry {
    border: none;
    cursor: pointer;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #hordeLobbyTalentsBox #btnTalentTreeReset:hover,
  #hordeLobbyTalentsBox #btnTalentTreeRetry:hover {
    background: var(--cor-vermelho-hover);
  }
  /* Clicável: copia o código para a área de transferência (é o que se manda
     para os amigos entrarem na sala). Continua com cara de texto, não de
     botão — a moldura de fliperama e o fundo vermelho dos botões de ação do
     overlay ficam de fora por `.botao-nu`. */
  #hordeLobbyCode {
    display: flex;
    align-items: center;
    text-align: center;
    gap: 8px;
    /* Sem recuo próprio: quem desenha a caixa agora é #hordeLobbyConvite, e o
       recuo dobrado empurrava o código para fora do miolo da moldura. */
    padding: 0;
    width: 100%;
    background: none;
    border: none;
    font-family: inherit;
    /* Menor que o nome do nível no topo do painel: o código é informação de
       apoio da caixa, não o título da sala de espera. */
    font-size: clamp(10px, calc(0.022 * var(--largura-arena)), 14px);
    letter-spacing: 1px;
    font-weight: 600;
    color: var(--cor-texto);
    cursor: pointer;
  }
  #hordeLobbyCode:hover {
    color: var(--cor-ouro-suave);
  }
  #hordeLobbyCode.copiado {
    color: var(--cor-sucesso);
  }
  .horde-lobby-copiar {
    font-size: clamp(8px, calc(0.016 * var(--largura-arena)), 11px);
    letter-spacing: 0.5px;
    font-weight: 400;
    color: var(--cor-texto-fraco);
    text-align: center;
    width: 60px;
  }
  #hordeLobbyCode:hover .horde-lobby-copiar,
  #hordeLobbyCode.copiado .horde-lobby-copiar {
    color: inherit;
  }

  /* ----- HUD da partida ----- */
  /* ===== Laje de pedra das peças do HUD (.hud-pedra) =====
     O HUD da partida era a única superfície do jogo que ficava de fora do
     material do resto: caixa retangular, fundo chapado e uma sombra dura —
     enquanto toda tela de menu é uma laje de pedra pregada na parede, com
     canto em escadinha, contorno e bisel. Sobre a arena (que é pedra
     desenhada) a caixa preta lia como interface de aplicativo por cima do
     jogo, e não como parte dele.

     Esta classe é **a mesma laje do menu**, e não uma imitação: as duas camadas
     que a desenham são as de `.menu-painel::before/::after`, compartilhadas
     acima. O que muda são as três medidas — escadinha de **um** degrau
     (`--recorte-icone-pixel`, o mesmo motivo dos botões de ícone: dois degraus
     de 4px comem uma peça de 30px de altura), bisel de 2px e sombra deslocada
     de 4px, que é a escala de peça pequena que o `:root` já nomeia.

     Quem usa: a faixa da base, o cartão de jogador (console local e fichas dos
     aliados) e a moldura do mini-mapa. O inventário do Relicário ficou de
     fora de propósito — ele é uma coluna de itens, e cada item já traz a
     própria cor de raridade. */
  .hud-pedra {
    --prego-lado: 8px;
    /* Margem menor que a do menu (10px): a peça é uma fração da altura de um
       painel, e o prego a 10px da borda cairia perto do meio dela. */
    --prego-margem: 6px;
    --moldura-recorte: var(--recorte-icone-pixel);
    --moldura-bisel: var(--sombra-pedra-pequena-interna);
    --moldura-sombra: 4px 4px 0 rgba(0, 0, 0, 0.4);
    position: relative;
    /* Contexto de empilhamento próprio, mesmo motivo de `.menu-painel`: sem
       ele as duas camadas em `z-index` negativo cairiam atrás do canvas da
       arena em vez de ficarem entre ele e o conteúdo da peça. */
    isolation: isolate;
    /* O material mora nas camadas, nunca no elemento — quem usa esta classe
       não declara `background` nem `box-shadow` de novo. */
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
  }
  /* Peça sem espaço para prego, que no HUD é quase toda. A cabeça tem 8px e a
     laje quer duas por lado (uma em cima, uma embaixo), a 6px do canto: numa
     faixa de 30px elas se encostam no meio, e mesmo no cartão local (que tem o
     dobro disso) o conteúdo vai de borda a borda — o prego saía por trás do
     nome do jogador. Quem tem altura e margem para prego no HUD é só a moldura
     do mini-mapa. Desligar é apagar a cor, e não uma segunda lista de camadas:
     as 16 continuam lá, pintando transparente. */
  .hud-pedra.sem-pregos {
    --cor-prego: transparent;
    --cor-prego-luz: transparent;
    --cor-prego-sombra: transparent;
  }
  /* Moldura do mini-mapa. Ela existe porque `<canvas>` é elemento substituído
     e não desenha `::before`/`::after` — a laje precisa de um elemento em
     volta. O posicionamento (canto, largura, opacidade) mudou-se para cá, e o
     canvas de dentro virou só o conteúdo da laje. */
  #minimapMoldura {
    display: none;
    position: absolute;
    top: 10px;
    right: 10px;
    /* A largura era do canvas; aqui ela é da moldura, com o padding somando
       por fora para o mapa continuar do mesmo tamanho de leitura. */
    width: 20%;
    box-sizing: content-box;
    padding: 6px;
    z-index: 11;
    pointer-events: none;
    /* Translúcida, como o mapa inteiro era: ela cobre um canto da arena o
       tempo todo, e nesse modo a câmera anda — o que está por baixo continua
       sendo jogo. Mas a pedra é mais opaca que o mapa, e não o mesmo 0,5 de
       antes: o mini-mapa perdeu a borda vermelha que era desenhada no buffer
       (ver hordeMinimap.js) e é a moldura que virou o contorno dele. A 0,5 ela
       sumia contra o chão de pedra da arena e o mapa ficava sem enquadramento
       nenhum — que era pior que a caixa preta de antes. O mapa em si continua
       na transparência de sempre: 0,8 da moldura x 0,6 do canvas dão os ~0,5
       que ele tinha. */
    opacity: 0.8;
  }
  body.horde-mode #minimapMoldura {
    display: block;
  }
  /* Mini-mapa: só existe no modo defesa, onde o mapa é 3x a tela (ver
     public/js/hordeMinimap.js). Dimensionado em porcentagem da arena, e não
     em px, para acompanhar a redução que gameScale.js aplica ao canvas —
     em px ele viraria metade da tela num celular. `pixelated` pelo mesmo
     motivo do canvas do jogo: o buffer é pequeno e a arte é pixel art. */
  #minimap {
    display: block;
    /* Largura e canto agora são da moldura (#minimapMoldura); o canvas só
       preenche o miolo dela. A silhueta em escadinha é a mesma da laje, um
       degrau menor de cada lado, para o mapa não sobrar por cima do canto
       recortado da pedra. */
    width: 100%;
    height: auto;
    image-rendering: pixelated;
    clip-path: var(--recorte-icone-pixel);
    /* Ver a moldura acima: o produto das duas opacidades é a de antes. */
    opacity: 0.6;
  }
  /* O HUD é um overlay `inset: 0` sobre a arena, e não altura de fluxo —
     gameScale.js dá a viewport quase inteira para a arena e
     não reserva espaço para HUD nenhum. Faixa do grupo em cima (vida da base,
     onda e as fichas dos aliados) e o console do jogador local embaixo, no
     centro. */
  /* ===== Escala do HUD =====
     Preferência `escalaHud`, escrita por `aplicarEscalaHud` (public/js/gameScale.js)
     como `--escala-hud`. Ela vale só para as peças de interface **sobre** a
     arena: o enquadramento do mundo é do canvas, e nada aqui toca nele — é o
     que garante que a arena continue mostrando exatamente a mesma área do mundo
     nos dois extremos da faixa.

     É `zoom` e não `transform: scale()` de propósito. `transform` não afeta
     layout: escalando a faixa do topo por 1,25 a coluna dos aliados sairia pela
     borda esquerda, e o console cresceria por cima da borda de baixo. `zoom`
     escala o layout, então cada peça continua ancorada onde estava e cresce por
     **dentro** da própria caixa — a largura em porcentagem resolve contra o
     bloco contentor já dividido pelo fator, e por isso nada transborda.

     Navegador sem `zoom` ignora a linha e o HUD fica no tamanho de sempre: a
     opção não faz nada, e nada quebra.

     O retrato do jogador é a exceção e desfaz este zoom — ver
     `.horde-rosto-nicho`, que é a única arte do HUD com escala inteira. */
  .horde-topo,
  #hordePlayersLocal,
  #hordeItens,
  #hordeProgresso,
  #minimapMoldura {
    zoom: var(--escala-hud, 1);
  }
  /* `#minimapMoldura` tem a largura em **porcentagem**, e porcentagem se anula
     sob `zoom` (o contentor encolhe na mesma medida): sem este `calc` o mapa
     seria a única peça que não acompanha a escala. */
  #minimapMoldura {
    width: calc(20% * var(--escala-hud, 1));
  }
  #hordeHud {
    display: none;
    position: absolute;
    inset: 0;
    z-index: 10;
    pointer-events: none;
    flex-direction: column;
    justify-content: space-between;
    align-items: center;
    box-sizing: border-box;
    /* Voltou a ter padding embaixo: quem encosta na borda inferior agora é o
       console do jogador local, não a fileira dos aliados. A fileira podia
       colar na borda porque era a informação menos urgente do HUD; o console é
       o oposto — é o que se lê no meio da onda —, e grudado no limite da arena
       ele fica na altura em que o navegador/telefone desenha as próprias
       barras (e, em touch, na aresta que o polegar cobre). */
    padding: 16px 16px 12px;
    animation: entrada-fade 0.4s ease;
  }
  /* Vida da base e contador de onda na **mesma** faixa, no topo: são as duas
     respostas de "como a partida está indo", e separá-las (a onda já ficou
     numa caixa própria no meio da tela) espalhava
     pela tela o que se lê de uma olhada só. Sem margem — é o primeiro filho do
     flex column de `#hordeHud`, então o padding do próprio `#hordeHud` (16px)
     já a afasta do topo da arena. */
  #hordeBase {
    display: flex;
    align-items: center;
    /* Centrada na coluna do meio da `.horde-topo`, e não esticada nela: a
       largura abaixo é a que ela sempre teve. */
    justify-self: center;
    gap: 8px;
    /* Um pouco mais larga que antes porque agora divide a faixa com o contador
       de onda: na largura anterior o trilho encolhia até virar um risco em
       tela estreita, justamente onde a vida da base é o que se lê de relance. */
    /* A porcentagem passou a medir a **coluna do meio** da `.horde-topo` (metade
       da tela), e não o HUD inteiro: por isso 100% aqui, com o mesmo teto de
       520px de sempre. */
    width: min(100%, 520px);
    /* Um pouco mais de folga lateral que os 10px de antes: a escadinha da laje
       come 4px de cada canto, e o "BASE" começava dentro dela. */
    padding: 6px 14px;
    /* O material vem de `.hud-pedra` (classe no elemento, ver o HTML) — a
       faixa é uma placa pregada na parede como as do menu, não uma caixa
       preta sobre a arena. `sem-pregos`: ela tem ~30px de altura, e nessa
       altura os quatro pregos se encostam no meio e passam por trás do
       "BASE". */
  }
  /* Contador de onda: mesma família tipográfica do rótulo/valor da base ao
     lado, porque é a mesma classe de informação. `tabular-nums` para a largura
     não dançar enquanto os segundos correm. */
  .horde-wave {
    font-size: clamp(9px, calc(0.02 * var(--largura-arena)), 14px);
    letter-spacing: 1px;
    color: var(--cor-texto-fraco);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
  }
  /* Última onda do nível: o mesmo vermelho de alerta que o indicador antigo
     usava, agora no contador. */
  .horde-wave.warning {
    color: var(--cor-vermelho-destaque);
  }
  body.touch-mode.horde-mode .horde-wave {
    font-size: clamp(7px, calc(0.014 * var(--largura-arena)), 10px);
  }
  .horde-base-label,
  .horde-base-value {
    font-size: clamp(9px, calc(0.02 * var(--largura-arena)), 14px);
    letter-spacing: 1px;
    color: var(--cor-texto-fraco);
    white-space: nowrap;
  }
  /* Trilho afundado, igual às barras de vida/escudo/cooldown dos cartões de
     jogador (ver hud.js): é um
     buraco cavado na pedra, não um bloco em relevo. */
  .horde-base-track {
    flex: 1;
    height: 12px;
    box-sizing: border-box;
    padding: 2px;
    background-color: var(--cor-superficie-baixa);
    /* O vão da muralha continua sendo muralha: as juntas seguem na parte
       vazia do trilho, mais fracas, como as fiadas de pedra que sobraram
       depois de o bloco cair. Sem elas a barra voltava a ler como
       carregamento assim que a vida descia da metade — que é justamente
       quando ela é mais olhada.

       `background-origin: content-box` para as juntas do vazio nascerem no
       mesmo x das do preenchimento: medidas da borda, elas sairiam 2px (o
       padding do trilho) fora de fase, e duas fiadas desalinhadas na mesma
       barra leem como erro de desenho. */
    background-image: repeating-linear-gradient(
      90deg,
      transparent 0 10px,
      var(--cor-muralha-junta) 10px 12px
    );
    background-origin: content-box;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-afundada);
    overflow: hidden;
    position: relative;
  }
  /* Em touch a arena é a viewport quase inteira (celular em paisagem é bem
     mais baixo que um monitor), e a barra no tamanho de desktop tomava uma
     fatia grande do topo — encolhida junto com o indicador de onda, que
     precisa subir de volta para acompanhar a barra menor. */
  body.touch-mode.horde-mode #hordeBase {
    width: min(100%, 265px);
    padding: 3px 6px;
    gap: 5px;
  }
  body.touch-mode.horde-mode .horde-base-label,
  body.touch-mode.horde-mode .horde-base-value {
    font-size: clamp(7px, calc(0.014 * var(--largura-arena)), 10px);
  }
  body.touch-mode.horde-mode .horde-base-track {
    height: 7px;
  }
  /* A vida da base desenhada como **muralha**, e não como barra: blocos de
     10px separados por uma junta de 2px, com o topo de cada fiada iluminado e
     a ponta direita em face quebrada — é dali que a muralha vai caindo. A cor
     continua sendo a informação (verde de pé, vermelho em estado crítico); o
     que a pedra acrescenta é a leitura de *o que* está caindo, que o retângulo
     chapado não dava.

     As três camadas são `background-image` sobre `background-color`, e por
     isso a regra do estado crítico abaixo troca só a cor: um `background`
     encurtado ali apagaria a muralha inteira e o estado crítico voltaria a ser
     um retângulo liso — justamente o momento em que essa barra é mais olhada.

     Ordem das camadas: a luz do topo vem antes da junta, senão a junta some
     nos 2px de cima de cada bloco. As duas são translúcidas de propósito (ver
     --cor-muralha-* no `:root`) — elas entram por cima do verde e do vermelho
     sem precisar de um par de tons por estado. */
  .horde-base-fill {
    height: 100%;
    width: 100%;
    background-color: var(--cor-sucesso);
    background-image:
      linear-gradient(var(--cor-muralha-luz) 0 2px, transparent 2px),
      repeating-linear-gradient(
        90deg,
        transparent 0 10px,
        var(--cor-muralha-junta) 10px 12px
      );
    /* A face quebrada: a coluna de 2px na ponta de onde a muralha já caiu. Ela
       anda com a largura da barra, então é o único pedaço do desenho que se
       move — e é para ele que o olho vai quando a base leva um golpe. */
    box-shadow: inset -2px 0 0 var(--cor-muralha-quebra);
    transition: width 0.12s linear;
  }
  .horde-base-fill.critica {
    background-color: var(--cor-vermelho-destaque);
  }
  /* Casca do "Alicerce": camada por cima do trilho, começando da mesma borda
     esquerda que a vida — ela é o que come o golpe antes da vida, então lê como
     um revestimento sobre ela e não como um segundo recurso à parte. Azul do
     Bastião, a cor da árvore que a concede (shared/talentTree.js), para o
     jogador ligar a barra ao talento que comprou. Listrada, e não chapada: sobre
     o verde da vida uma cor sólida esconderia quanto de vida existe embaixo. */
  .horde-base-casca {
    position: absolute;
    left: 2px;
    top: 2px;
    bottom: 2px;
    /* A fração vem por variável, e a largura desconta os 4px de padding do
       trilho: sendo absoluta, uma porcentagem aqui mediria a caixa **com** o
       padding e a casca ficaria mais longa que a vida na mesma fração. */
    width: calc((100% - 4px) * var(--fracao-casca, 0));
    /* Listras **verticais**, como as da barra de recarga logo abaixo no HUD: a
       diagonal seria a única borda antisserrilhada da faixa (ver
       public/css/CLAUDE.md). O vão é transparente de propósito — é por ele que
       a vida embaixo continua sendo lida. */
    background-image: repeating-linear-gradient(
      90deg,
      var(--cor-casca-base) 0 3px,
      transparent 3px 6px
    );
  }
  .horde-base-fill.piscando {
    animation: horde-base-hit 0.28s steps(2) 2;
  }
  /* Piscada em degraus (steps), não um fade: mesma regra de pixel art do
     resto do jogo — o que muda de estado perde etapas visíveis. */
  @keyframes horde-base-hit {
    0% { filter: brightness(3); }
    100% { filter: none; }
  }

  /* Inventário de itens do jogador local (Relicário, ver atualizarInventario
     em hordeHud.js). Coluna no canto esquerdo, e não uma linha dentro do
     cartão do jogador: no Sem fim a lista cresce a partida inteira, e no
     cartão ela empurraria nome, vida e cooldown — os três dados que decidem se
     dá para sair da base agora.

     Posicionado em `absolute` dentro de `#hordeHud` porque ele é um flex column
     com `space-between` (base no topo, cartões embaixo): como terceiro filho de
     fluxo, o inventário desmontaria essa distribuição. Alinhado ao topo, abaixo
     da faixa da base, e nunca à direita — lá fica o mini-mapa. */
  /* ===== Progresso de conta durante a partida (ver
     public/js/progressoNaPartida.js) =====

     Encostado na borda direita, **abaixo do mini-mapa**. A altura do topo é
     derivada da largura da arena e não escrita em px nem em %: o mini-mapa é
     `top: 10px; right: 10px; width: 20%` com 6px de padding, e o canvas dele
     tem a proporção do mundo (200x120 = 5:3) — então o rodapé dele cai em
     `10 + 0,12 x largura + 12`. Um `top` em porcentagem mediria a **altura** do
     bloco e passaria por cima do mapa em arena larga; `--largura-arena` é a
     medida que `gameScale.js` já escreve, e é a mesma que a coluna de itens
     usa.

     `sem-pregos`: mesmo depois de crescer (valor maior e rótulo escrito) a
     caixa tem duas linhas de ~18px, e nessa altura os quatro pregos da laje
     pesam mais que o conteúdo — mesma decisão das outras peças
     pequenas do HUD. */
  #hordeProgresso {
    position: absolute;
    right: 16px;
    top: calc(22px + 0.12 * var(--largura-arena));
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 5px 8px;
    /* O `#hordeHud` inteiro é `pointer-events: none` e esta peça **não** abre
       exceção: ela é leitura, não controle, e em celular os joysticks nascem
       onde o dedo toca — um alvo de ponteiro aqui roubaria o toque de quem
       mirasse neste canto. */
  }
  #hordeProgresso[hidden] { display: none; }
  /* O mural de avisos ganha o lugar. A faixa de "ONDA N" (e a chegada do chefe,
     e o fecho da onda) é desenhada no **canvas**, de ponta a ponta da arena, e
     passa exatamente pela altura desta caixa; como o HUD é DOM por cima do
     canvas, não há z-index que ponha o indicador atrás dela — ele se apaga
     enquanto ela está de pé (ver `muralOcupado` em progressoNaPartida.js), como
     o número do combo e a dica do inventário já fazem. */
  #hordeProgresso.mural-na-frente { display: none; }
  /* Passados alguns segundos sem ganho nenhum, a caixa se apaga (ver
     `ESPERA_PARA_ESMAECER_MS` em progressoNaPartida.js). Ela é um recibo, não
     um mostrador: entre uma onda e outra o jogador não está ganhando nada, e
     um número parado num canto da arena é ocupação de tela sem informação. O
     ganho seguinte a traz de volta inteira, no mesmo quadro do pulo.

     Apaga em **degraus**, e não num fade contínuo: é a mesma regra de pixel art
     do resto da interface (nada de transição interpolada). Quatro passos de
     opacidade em `steps(1, end)`, com `forwards` para ficar apagada até que o
     JS tire a classe. */
  @keyframes horde-progresso-esmaecer {
    0% { opacity: 1; }
    25% { opacity: 0.6; }
    50% { opacity: 0.35; }
    75% { opacity: 0.15; }
    100% { opacity: 0; }
  }
  #hordeProgresso.esmaecido {
    animation: horde-progresso-esmaecer 0.8s steps(1, end) forwards;
  }
  .horde-progresso-linha {
    display: flex;
    align-items: center;
    gap: 4px;
  }
  .horde-progresso-linha[hidden] { display: none; }
  /* O pulinho de quem acabou de ganhar (ver `pularLinha` em
     progressoNaPartida.js). Só a linha que subiu pula — a moeda e a XP ticam em
     ritmos diferentes (a cada quatro e a cada dez pontos de abate), e a caixa
     inteira saltando diria "ganhei" sem dizer **o quê**.

     Deslocamento em pixel inteiro e em `steps(1, end)`, como todo movimento
     desta interface: quatro degraus (sobe 4, sobe 6, desce 2, pousa), a mesma
     gramática do pulo do botão de comprar da Loja. */
  @keyframes horde-progresso-pulo {
    0% { transform: translateY(0); }
    25% { transform: translateY(-4px); }
    50% { transform: translateY(-6px); }
    75% { transform: translateY(-2px); }
    100% { transform: translateY(0); }
  }
  .horde-progresso-linha.pulou {
    animation: horde-progresso-pulo 0.24s steps(1, end) 1;
  }
  /* Quem pediu menos movimento não recebe o pulo: o número que muda (e o som)
     já é a notícia inteira, e aqui o deslocamento é só ênfase. Mesma decisão do
     botão de comprar da Loja. */
  @media (prefers-reduced-motion: reduce) {
    .horde-progresso-linha.pulou { animation: none; }
  }
  /* Os dois glifos são desenhados em CSS, como o `.loja-moeda`: quadrado com
     relevo sólido deslocado, a gramática do resto da interface (ver
     public/css/CLAUDE.md). Um emoji muda de desenho a cada sistema
     operacional, e um canvas aqui seria um buffer por partida para 8px de
     desenho. */
  /* 12px, e não 8px: com o rótulo escrito do lado o glifo passou a ser a
     marca de cor da linha, e a 8px ele desaparecia contra o texto. */
  .horde-progresso-glifo {
    width: 12px;
    height: 12px;
    flex: none;
  }
  /* A moeda é a **mesma** do saldo da Loja, no mesmo ouro e com o mesmo bisel:
     é a mesma moeda, e um segundo desenho faria o jogador aprender duas peças
     para a mesma coisa. */
  .horde-progresso-linha .glifo-moeda {
    background: var(--cor-moeda);
    box-shadow: inset -2px -2px 0 #a16207, inset 2px 2px 0 #fde68a;
  }
  /* A XP sai no vermelho de destaque do anel de nível
     (`.anel-nivel-arco`), que é a cor com que a conta já fala de nível e XP nas
     três telas que o mostram. */
  .horde-progresso-linha .glifo-xp {
    background: var(--cor-vermelho-destaque);
    box-shadow: inset -2px -2px 0 #7f1d1d, inset 2px 2px 0 #fca5a5;
  }
  /* O valor cresceu (era clamp(8px, 0,017 x largura, 12px)): a 8px o número
     ficava do tamanho da legenda de um gráfico, e é o dado que o jogador lê de
     canto de olho no meio da luta. */
  .horde-progresso-valor {
    font-size: clamp(12px, calc(0.024 * var(--largura-arena)), 18px);
    color: var(--cor-texto);
    font-variant-numeric: tabular-nums;
    line-height: 1;
  }
  /* O rótulo é legenda, não dado: menor que o valor e num cinza recuado, para
     que a leitura rápida caia no número e a palavra só resolva a dúvida de
     quem não sabe o que a cor quer dizer. */
  .horde-progresso-rotulo {
    font-size: clamp(8px, calc(0.015 * var(--largura-arena)), 11px);
    color: var(--cor-texto-fraco);
    letter-spacing: 1px;
    line-height: 1;
  }
  /* Em touch a arena é quase a viewport inteira e a caixa encolhe junto, como a
     coluna de itens e o cartão do jogador. */
  body.touch-mode.horde-mode #hordeProgresso {
    right: 8px;
    gap: 2px;
    padding: 2px 4px;
  }
  body.touch-mode.horde-mode .horde-progresso-glifo {
    width: 9px;
    height: 9px;
  }
  body.touch-mode.horde-mode .horde-progresso-valor {
    font-size: clamp(10px, calc(0.019 * var(--largura-arena)), 14px);
  }
  body.touch-mode.horde-mode .horde-progresso-rotulo {
    font-size: clamp(7px, calc(0.012 * var(--largura-arena)), 9px);
    letter-spacing: 0;
  }
  #hordeItens {
    list-style: none;
    margin: 0;
    padding: 0;
    position: absolute;
    left: 16px;
    /* Começa mais abaixo do que começava: a coluna de fichas dos aliados
       (`#hordePlayers`) passou a ocupar o topo esquerdo, e com três aliados ela
       desce até perto de um terço da altura. */
    top: 36%;
    /* A coluna tem fim: no Sem fim a lista cresce a partida inteira e, presa
       só pelo topo, ela passava da borda de baixo da arena — os itens novos,
       que entram sempre no fim, ficavam fora da tela justamente quando são a
       informação nova. Ancorada nas duas pontas (e parando acima dos cartões
       de jogador), a caixa tem altura definida e o `wrap` abaixo pode virar
       uma coluna nova em vez de vazar. O fim desceu junto (era 20%): a fileira
       de cartões que ocupava a borda de baixo virou um console centralizado, e
       o canto inferior esquerdo ficou livre — a coluna termina com mais espaço
       do que tinha antes, apesar de começar mais embaixo. */
    bottom: 6%;
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
    /* Cada coluna com a altura do seu conteúdo, colada à esquerda: sem isto o
       flex distribui as colunas pela largura toda e os ícones se espalham
       pela arena. */
    align-content: flex-start;
    gap: 4px;
  }
  /* Cada item é ícone + contagem. Fundo de pedra translúcido como o resto do
     HUD deste modo, e caixa do tamanho do ícone: a lista fica sobre a arena o
     tempo todo, então ela não pode virar um painel. */
  .horde-item {
    display: flex;
    align-items: center;
    gap: 3px;
    padding: 2px 4px;
    /* Mesma dupla de camadas da linha do Relicário (véu da raridade sobre a
       superfície de sempre), aqui sobre o pedra do HUD: um item colhido tem a
       mesma cor na coluna do canto que tinha no cartão da escolha e tem no
       catálogo. */
    background-color: var(--cor-painel-pedra-veu);
    background-image: linear-gradient(var(--cor-raridade-veu), var(--cor-raridade-veu));
    /* A faixa da raridade entra **junto** com a pedra, e não no lugar dela: a
       caixa tem o tamanho do ícone e sem o bisel ela deixaria de ler como peça
       de HUD sobre a arena. */
    box-shadow: var(--sombra-pedra-pequena), inset 3px 0 0 var(--cor-raridade);
    /* `#hordeHud` inteiro é `pointer-events: none` para não roubar o clique da
       arena; o item volta a receber ponteiro porque o nome e a descrição dele
       aparecem no hover (ver criarEntradaDeItem em hordeHud.js). Mira e tiro
       escutam no `window`, então o evento continua chegando lá por bubbling —
       a exceção não tira nenhum clique de jogo. */
    pointer-events: auto;
    position: relative;
  }
  /* Nome + descrição do item, revelados só sob o cursor. Sai para a **direita**
     porque a coluna mora na borda esquerda da arena, e é ancorado no topo do
     próprio item: um balão centralizado verticalmente sairia da tela nas
     entradas do fim da lista. Largura fixa em `ch` para a descrição quebrar em
     duas ou três linhas em vez de atravessar a arena inteira. */
  .horde-item-dica {
    display: none;
    position: absolute;
    left: calc(100% + 6px);
    top: 0;
    z-index: 2;
    width: 26ch;
    padding: 6px 8px;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
    color: var(--cor-texto);
    font-size: clamp(9px, calc(0.016 * var(--largura-arena)), 13px);
    line-height: 1.35;
    text-align: left;
  }
  .horde-item-dica strong {
    display: block;
    color: var(--cor-ouro);
  }
  /* Nome da raridade no balão: a faixa colorida diz que *há* uma diferença, e
     esta linha é a única que a nomeia dentro da partida — o jogador não vai
     abrir o Relicário no meio de uma onda para descobrir se roxo é bom. */
  .horde-item-raridade {
    display: block;
    color: var(--cor-raridade);
    font-size: 0.85em;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .horde-item:hover .horde-item-dica {
    display: block;
  }
  /* O buffer do canvas tem o tamanho do ícone em pixels de arte (ver
     ITEM_CANVAS_LADO em hordeHud.js) e é o CSS que o amplia — `pixelated` é o
     que mantém o bloco quadrado em vez de interpolá-lo, mesma regra do
     mini-mapa e do canvas do jogo. */
  .horde-item canvas {
    display: block;
    width: clamp(14px, calc(0.028 * var(--largura-arena)), 22px);
    height: auto;
    image-rendering: pixelated;
  }
  /* Item cuja carga a partida já gastou (hoje só o Coração sobressalente, ver
     itemSemCarga em itemFx.js): a miniatura fica em preto e branco e a linha
     apaga um pouco. É a leitura de "isto já foi usado" sem tirar o item da
     lista — ele continua na build, e a build é o histórico da partida.

     `grayscale` num canvas de pixel art não interpola nada (não é um blur nem
     uma transição): cada bloco continua do mesmo tamanho, só perde a cor. */
  .horde-item.gasto canvas {
    filter: grayscale(1);
  }
  .horde-item.gasto {
    opacity: 0.65;
  }
  /* Só aparece no item empilhado (o JS deixa o texto vazio em x1). Dourado
     como o excedente de power-up e o combo: é a mesma família de "acima do
     normal". */
  .horde-item-contagem {
    font-size: clamp(8px, calc(0.017 * var(--largura-arena)), 12px);
    color: var(--cor-ouro);
    font-variant-numeric: tabular-nums;
  }
  /* Contador vivo do item que acumula (pilha da Colheita de almas, abates até o
     coração da Sanguessuga, Espelho guardado). Cor diferente da contagem de
     cópias — os dois números ficam lado a lado na mesma caixa, e se fossem da
     mesma cor "x2 14" leria como um número só. */
  .horde-item-pilha {
    font-size: clamp(8px, calc(0.017 * var(--largura-arena)), 12px);
    color: var(--cor-texto-medio);
    font-variant-numeric: tabular-nums;
    min-width: 1ch;
  }
  /* Em touch a arena é a viewport quase inteira e a coluna disputa espaço com
     o joystick esquerdo: mesma redução do cartão de jogador. */
  body.touch-mode.horde-mode #hordeItens {
    left: 8px;
    gap: 2px;
    /* Em touch a arena é quase a viewport inteira e os cartões ficam mais
       baixos: a coluna pode descer mais antes de encostar neles. */
    bottom: 16%;
  }
  body.touch-mode.horde-mode .horde-item {
    padding: 1px 2px;
    /* A caixa encolhe, a faixa da raridade acompanha: 3px sobre 2px de padding
       passaria por baixo do ícone em vez de ficar ao lado dele. */
    box-shadow: var(--sombra-pedra-pequena), inset 2px 0 0 var(--cor-raridade);
    /* Em touch não existe hover, então o balão nunca abriria — e o ponteiro
       ligado só serviria para roubar um toque do joystick esquerdo, que fica
       nesse mesmo canto. Quem conta o que cada item faz ali é o Relicário do
       menu. */
    pointer-events: none;
  }
  body.touch-mode.horde-mode .horde-item canvas {
    width: clamp(10px, calc(0.02 * var(--largura-arena)), 16px);
  }
  body.touch-mode.horde-mode .horde-item-contagem,
  body.touch-mode.horde-mode .horde-item-pilha {
    font-size: clamp(6px, calc(0.013 * var(--largura-arena)), 9px);
  }

  /* ----- A fita de builds (Tab segurado, ou o ombro direito do controle — ver
     public/js/hordeBuilds.js) -----

     Até a `beta 0.4.0` isto era um painel centrado com véu, cobrindo **99% da
     arena**: quatro colunas de nome escrito e três de árvore de talento, 74
     pedaços de texto de uma vez no pior caso medido (quarteto na campanha), com
     recorte silencioso em tela baixa. O estudo com as seis alternativas medidas
     está em `docs/sugestoes-painel-do-tab.html`; esta é a proposta **F**.

     A fita encosta na borda de baixo e cresce **para cima**: ela nunca cobre o
     console do jogador local (`#hordePlayersLocal`), que é o que se lê no meio
     da onda. O `--fita-piso` é escrito pelo JS a partir do console de verdade, e
     não um número fixo aqui: a altura dele depende da escala de HUD que o
     jogador escolheu (`--escala-hud` é um `zoom`) e do conteúdo do cartão.

     `z-index` 13: acima do `#hordeHud` (10) e também da tela de escolha de item
     (12), porque a pausa do fim da onda é justamente quando saber o que os
     companheiros carregam decide a própria escolha. */
  #hordeBuilds {
    position: absolute;
    left: 2px;
    right: 2px;
    bottom: var(--fita-piso, 78px);
    z-index: 13;
    display: flex;
    justify-content: center;
    /* A partida continua correndo por baixo: a fita é leitura, e engolir o
       clique do tiro faria a onda passar por cima de quem a abriu. É por isso
       que o item apontado é descoberto por **coordenada** (`mousemove` contra
       os retângulos em cache, ver hordeBuilds.js) e não por `hover`. */
    pointer-events: none;
  }
  #hordeBuilds[hidden] { display: none; }
  /* Enquanto a fita está aberta, o inventário do canto sai de cena: o primeiro
     bloco dela **é** o inventário do jogador local, com os mesmos ícones e a
     mesma ordem de colheita. `visibility` e não `display` para a coluna não
     mudar de tamanho por baixo e voltar reposicionada. */
  body.fita-de-builds-aberta #hordeItens {
    visibility: hidden;
  }
  /* As duas faixas de aviso da partida — a dica do tiro automático e a faixa do
     tutorial — moram no mesmo `bottom: 14%` em que a fita passou a nascer, e com
     `z-index` maior (14 contra 13): sem isto elas são desenhadas **por cima**
     dela, tapando a ficha do rodapé e metade dos blocos. Enquanto a fita está
     aberta as duas sobem para cima dela. `--fita-topo` é a distância da borda de
     baixo da arena até o teto da fita, medida e escrita por hordeBuilds.js — a
     altura da fita é do conteúdo (a build do Sem fim tem o dobro da altura da
     build da campanha), então não há como calculá-la aqui. */
  body.fita-de-builds-aberta #dicaTiroAutomatico,
  body.fita-de-builds-aberta #matchTutorialBanner {
    bottom: calc(var(--fita-topo, 14%) + 8px);
  }
  /* A laje: material de `.hud-pedra` (classe posta em `atualizarBuildsDoGrupo`),
     a mesma pedra dos cartões do HUD e das telas do menu — e opaca, porque aqui
     ela fica sobre a arena **viva**, e texto pequeno sobre horda em movimento
     não se lê. */
  .fita-builds {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: clamp(3px, calc(0.005 * var(--largura-arena)), 6px);
    padding: clamp(4px, calc(0.008 * var(--largura-arena)), 9px)
      clamp(7px, calc(0.012 * var(--largura-arena)), 13px);
    box-sizing: border-box;
  }
  /* Um bloco por jogador, na ordem dos slots — a mesma dos cartões do HUD, do
     mini-mapa e das cores. */
  .fita-builds-linha {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: clamp(5px, calc(0.014 * var(--largura-arena)), 14px);
    width: 100%;
  }
  /* Cada bloco leva no máximo um quarto da largura, esteja o grupo com 1 ou com
     4 jogadores: assim o bloco de quem joga sozinho não se espalha pela fita, e
     a build de ninguém muda de largura quando um companheiro cai da partida. */
  .fita-bloco {
    width: calc((100% - 3 * clamp(5px, calc(0.014 * var(--largura-arena)), 14px)) / 4);
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* A faixa da cor do slot à esquerda, no lugar de uma laje por bloco: quatro
       lajes dentro de uma laje eram metade do "tudo jogado" do painel antigo. */
    border-left: 2px solid var(--slot-color, var(--cor-borda));
    padding-left: clamp(4px, calc(0.006 * var(--largura-arena)), 7px);
  }
  .fita-bloco.local {
    --slot-color: var(--cor-ouro);
  }
  .fita-cabeca {
    display: flex;
    align-items: baseline;
    gap: 5px;
  }
  /* Nome na cor do slot, como no cartão do HUD; o jogador local fica dourado,
     que é a marca de "você" na tela inteira (ver public/js/CLAUDE.md). */
  .fita-nome {
    color: var(--slot-color, var(--cor-texto));
    font-size: clamp(8px, calc(0.015 * var(--largura-arena)), 13px);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* A contagem de itens **distintos**, ao lado do nome: numa fita de ícones ela
     é o número que responde "quem está com a build maior" sem contar
     quadradinho. Em cinza, e não no dourado da contagem de cópias: ali o
     dourado quer dizer "acima do normal", aqui é só um tamanho. */
  .fita-contagem {
    margin-left: auto;
    flex: none;
    color: var(--cor-texto-fraco);
    font-size: clamp(7px, calc(0.013 * var(--largura-arena)), 11px);
    font-variant-numeric: tabular-nums;
  }
  /* A grade de chaves. `auto-fill` com um mínimo, e não um número fixo de
     colunas: o bloco muda de largura com a arena, e um número fixo cortaria a
     última chave nas telas estreitas. Sem `overflow: hidden` de propósito — a
     fita é ancorada embaixo e cresce para cima, então build grande ganha mais
     uma fileira em vez de ser recortada em silêncio, que era o defeito medido
     do painel antigo. */
  .fita-grade {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(clamp(15px, calc(0.021 * var(--largura-arena)), 24px), 1fr));
    gap: 2px;
    align-content: start;
  }
  /* A chave é a peça do inventário do canto: ícone com a cor da raridade na
     aresta de baixo e a contagem de cópias. */
  .fita-chave {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1px;
    background-image: linear-gradient(var(--cor-raridade-veu), var(--cor-raridade-veu));
    box-shadow: inset 0 -2px 0 var(--cor-raridade);
  }
  .fita-chave canvas {
    display: block;
    flex: none;
    width: clamp(10px, calc(0.015 * var(--largura-arena)), 18px);
    height: auto;
    image-rendering: pixelated;
  }
  /* O item que o ponteiro está apontando, e cuja ficha está no rodapé. Moldura
     dourada por `box-shadow` e não por `outline`: a de baixo continua sendo a
     cor da raridade, e as duas precisam conviver na mesma peça. */
  .fita-chave.apontada {
    box-shadow: inset 0 -2px 0 var(--cor-raridade), 0 0 0 2px var(--cor-ouro);
  }
  /* A mesma chave na build dos outros jogadores. A marca é mais fraca (1px) que
     a do apontado porque é consequência, e não intenção — mas ela existe porque
     é a pergunta da fita: apontar o Espantalho mostra na hora que dois
     companheiros também o carregam. Ver `marcarApontado` em hordeBuilds.js. */
  .fita-chave.apontada-irma {
    box-shadow: inset 0 -2px 0 var(--cor-raridade), 0 0 0 1px var(--cor-ouro);
  }
  .fita-chave-qtd {
    position: absolute;
    right: 0;
    bottom: 1px;
    color: var(--cor-ouro);
    font-size: clamp(6px, calc(0.011 * var(--largura-arena)), 10px);
    font-variant-numeric: tabular-nums;
    text-shadow: 1px 1px 0 #000;
  }
  /* Bloco sem item nenhum é estado escrito: vazio, ele leria como falha de
     carregamento — e na primeira onda ninguém tem item. */
  .fita-vazio {
    color: var(--cor-texto-medio);
    font-size: clamp(7px, calc(0.013 * var(--largura-arena)), 11px);
    font-style: italic;
  }

  /* ----- O rodapé: a ficha de um item -----
     É o que a fita **compra** ao trocar nome escrito por ícone: a descrição do
     catálogo, que não existia em lugar nenhum durante a partida — até aqui, quem
     não lembrava o que a Peçonha fazia tinha de sair para o Relicário do menu.
     O item é o apontado pelo ponteiro, ou o último colhido (ver `itemDaFicha` em
     fitaDeBuilds.js). */
  .fita-ficha {
    display: flex;
    align-items: flex-start;
    gap: clamp(4px, calc(0.008 * var(--largura-arena)), 8px);
    border-top: 1px solid var(--cor-borda);
    padding-top: clamp(2px, calc(0.004 * var(--largura-arena)), 5px);
  }
  .fita-ficha[hidden] { display: none; }
  .fita-ficha-arte {
    display: block;
    flex: none;
    width: clamp(14px, calc(0.022 * var(--largura-arena)), 26px);
    height: auto;
    image-rendering: pixelated;
  }
  .fita-ficha-texto {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
  }
  .fita-ficha-linha {
    display: flex;
    align-items: baseline;
    gap: 6px;
    flex-wrap: wrap;
  }
  .fita-ficha-nome {
    color: var(--cor-texto);
    font-size: clamp(8px, calc(0.015 * var(--largura-arena)), 13px);
  }
  .fita-ficha-raridade {
    color: var(--cor-raridade);
    font-size: clamp(6px, calc(0.011 * var(--largura-arena)), 10px);
    letter-spacing: 0.06em;
  }
  /* A cor é a da família (as quatro de `--cor-familia-*`), e não uma cor de
     "aviso": é o mesmo texto e a mesma cor do cartão da escolha e da entrada do
     Relicário (`avisoDeFamilia`/`classeDeFamilia`, raridades.js). */
  .fita-ficha-familia {
    color: var(--cor-familia, var(--cor-familia-outra));
    font-size: clamp(6px, calc(0.011 * var(--largura-arena)), 10px);
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  .fita-ficha-familia[hidden] { display: none; }
  /* "último colhido": de onde a ficha veio, quando não foi o jogador que
     apontou. Sem isto, quem joga de controle veria uma ficha aparecer sozinha e
     não saberia por que é aquele item. */
  .fita-ficha-marca {
    margin-left: auto;
    color: var(--cor-texto-fraco);
    font-size: clamp(6px, calc(0.011 * var(--largura-arena)), 10px);
    font-style: italic;
  }
  .fita-ficha-marca[hidden] { display: none; }
  .fita-ficha-desc {
    color: var(--cor-texto-medio);
    font-size: clamp(7px, calc(0.013 * var(--largura-arena)), 11px);
    line-height: 1.35;
  }

  /* Faixa do topo do HUD: as fichas dos aliados à esquerda e a vida da base no
     meio. Grade de três colunas, e não um flex com a base centrada por
     `margin: auto`: a coluna da esquerda tem largura própria (a mesma fatia de
     1/4 que um cartão sempre ocupou), a base fica na coluna do meio e a coluna
     vazia da direita é o que a mantém no **centro da tela**, e não no centro do
     que sobrou ao lado dos aliados. É a linha do grupo: base, onda e como estão
     os outros. */
  .horde-topo {
    width: 100%;
    display: grid;
    grid-template-columns: 25% 1fr 25%;
    align-items: start;
    gap: 8px;
  }
  #hordePlayers,
  #hordePlayersLocal {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    gap: 8px;
  }
  /* Os aliados viraram uma **coluna** na primeira célula da grade do topo, no
     lugar onde o cartão local ficava. Empilhados, e não em fileira: a célula
     tem 1/4 da largura da tela, e três cartões lado a lado ali sairiam com um
     dezesseis avos cada.
     A 80% de opacidade: o cartão do companheiro é leitura de canto de olho, e
     opaco ele disputava atenção com o próprio console do jogador. A opacidade
     fica no cartão, e não na lista, para o `.morto`/`.desconectado` (que apagam
     partes do cartão) continuarem compondo com um valor só por elemento. */
  #hordePlayers {
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
  }
  /* `flex: 0 0 auto` e largura cheia: numa lista em coluna a `flex-basis: 25%`
     de `.horde-player` mediria a **altura** da célula, não a largura, e o
     cartão nasceria com uma altura arbitrária em cima do conteúdo dele. */
  #hordePlayers .horde-player {
    flex: 0 0 auto;
    width: 100%;
    opacity: 0.8;
    /* A ficha é o mesmo cartão, encolhido: caixa mais rasa e nome menor que o
       do console do rodapé (o coração menor vem do JS, ver HEART_FATOR_ALIADO
       em hordeHud.js). Empilhadas, três fichas no tamanho do console pesavam
       mais no canto do que a peça que o jogador realmente lê durante a onda. */
    padding: 3px 6px;
  }
  #hordePlayers .horde-player-name {
    font-size: clamp(8px, calc(0.016 * var(--largura-arena)), 12px);
  }
  /* Em touch a regra de cima venceria por especificidade (id + classe) a do
     bloco `body.touch-mode.horde-mode` lá embaixo, e o nome do aliado sairia
     maior no celular do que no desktop proporcionalmente. */
  body.touch-mode.horde-mode #hordePlayers .horde-player-name {
    font-size: clamp(6px, calc(0.013 * var(--largura-arena)), 10px);
  }
  /* Console do jogador local: centralizado na borda de baixo, sempre — a lista
     ocupa a largura toda do HUD e centra o único cartão que tem dentro, então a
     posição dele não muda com 0, 1, 2 ou 3 aliados na partida. É a razão de ele
     ter saído do topo: o personagem passa a partida perto da base, no meio da
     tela, e o canto superior esquerdo obrigava a tirar o olho do combate para
     ler a própria vida.
     Mais largo que 1/4 da tela porque carrega três trilhos de prazo (tiro,
     rolamento e cano) além da vida — nessa largura cada bloco do gradiente das
     barras continua legível de relance. Teto em px para a peça não crescer sem
     fim em monitor largo. */
  #hordePlayersLocal {
    width: 100%;
  }
  #hordePlayersLocal .horde-player {
    flex: 0 0 auto;
    /* Mais largo que os 34% de antes: o retrato do jogador (`.horde-rosto-nicho`)
       entrou como primeira coluna e come 48px + 8 de vão. Sem a folga, os três
       trilhos perderiam justamente essa largura — e o console cresce em
       **largura** de propósito, que é o eixo barato aqui (o HUD é um overlay
       sobre a arena; altura nova é pixel de jogo que some). */
    width: min(40%, 520px);
  }
  /* ----- Linha do console local: retrato + a pilha de sempre -----
     Só o cartão local tem esta linha (ver criarCartao em hordeHud.js): a ficha
     do aliado continua sendo a pilha crua. O retrato é do jogador, não do
     grupo — três rostos empilhados na coluna dos aliados seriam maiores que o
     próprio console. */
  .horde-player-linha {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-width: 0;
  }
  .horde-player-pilha {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
  }

  /* ===== O retrato do jogador (.horde-rosto-nicho / .horde-rosto) =====
     O rosto do personagem à esquerda do console, reagindo ao que acontece com
     ele. Quem escolhe o estado é `public/js/rostoDoJogador.js`; aqui está só o
     material e o movimento.

     **A arte é toda do pacote, nenhuma foi desenhada:** os quatro retratos de
     16x16 de `_Faces` (publicados na tira `rostos.png`, quatro quadros), a
     caveira do quadro 4 de `decompose.png` e a tira de rolamento `roll.png`.
     Nas duas últimas o recorte de 16x16 em (42,44) devolve exatamente o quadro
     original do pacote: é onde `scripts/sprites-little-mage.py` cola o corpo
     dentro do quadro de 100x100 (a constante `BODY`).

     **48px, e não mais.** O console media 55 a 65px de altura antes disto
     (medido em 900x500, 1280x800 e 1920x1080), e o nicho de 48 é o maior
     tamanho que cabe nessa faixa sem virar o que decide a altura da laje. Ele
     ainda custa alguma coisa nas telas mais baixas — medido depois: +0px em
     1920x1080 (o console já tinha 65), +1px em 1280x800 e +3px em 900x500,
     onde a pilha era mais curta que o nicho. Três pixels de arena numa tela de
     500 é o preço, e ele está escrito aqui porque a regra deste HUD é que
     altura se paga e largura não.

     48 é `16 x 3`: a escala do pixel é sempre um inteiro, nunca fracionária —
     senão a mesma imagem sai com pixels de dois tamanhos.

     **É por isso que o retrato não entra no `zoom` da escala do HUD.** Ele é a
     única arte do HUD com escala inteira declarada, e `0,75 x 48 = 36` e
     `1,25 x 48 = 60` não são múltiplos de 16. Ele desfaz o `zoom` da peça que o
     contém e cresce em **degrau inteiro** (`--degrau-rosto`, -1/0/+1, escrito
     por `aplicarEscalaHud` em public/js/gameScale.js). */
  .horde-rosto-nicho {
    position: relative;
    flex: 0 0 auto;
    zoom: calc(1 / var(--escala-hud, 1));
    width: calc(16px * (3 + var(--degrau-rosto, 0)));
    height: calc(16px * (3 + var(--degrau-rosto, 0)));
    display: flex;
    align-items: center;
    justify-content: center;
    /* Buraco cavado na pedra, o mesmo relevo dos trilhos das barras: o retrato
       está **dentro** da laje, não pousado em cima dela. */
    background: #07070a;
    box-shadow: var(--sombra-pedra-afundada);
    /* O rolamento gira o retrato e o sono o desloca: sem o corte, os dois
       vazariam por cima dos corações. */
    overflow: hidden;
  }
  .horde-rosto {
    /* Escala do pixel — inteira, sempre. Trocada em touch e pelo degrau da
       escala do HUD (`--degrau-rosto`), e em lugar nenhum mais. */
    --e: calc(3 + var(--degrau-rosto, 0));
    /* Índice do quadro em `rostos.png`, escrito pelo JS. A ordem é contrato com
       `ROSTOS` em rostoDoJogador.js e no script de arte. */
    --rosto: 0;
    display: block;
    width: calc(16px * var(--e));
    height: calc(16px * var(--e));
    image-rendering: pixelated;
    background-repeat: no-repeat;
    background-image: url("../assets/sprites/atirador/rostos.png");
    background-size: calc(64px * var(--e)) calc(16px * var(--e));
    background-position: calc(var(--rosto) * -16px * var(--e)) 0;
  }
  /* Rolando: a mesma tira que a arena roda, recortada na cabeça. Ela já é a
     cambalhota — girar o nicho por cima seria uma cambalhota dentro de outra. */
  .horde-rosto-nicho[data-estado="rolando"] .horde-rosto {
    background-image: url("../assets/sprites/atirador/roll.png");
    background-size: calc(500px * var(--e)) calc(100px * var(--e));
    /* 200ms e 5 quadros: exatamente `ROLAMENTO_DURACAO_MS` e a tira inteira, os
       mesmos números de `characterSprites.js`. O retrato e o corpo na arena
       andam no mesmo passo. */
    animation: horde-rosto-rolando 200ms steps(5) infinite;
  }
  @keyframes horde-rosto-rolando {
    from { background-position: calc(-42px * var(--e)) calc(-44px * var(--e)); }
    to { background-position: calc(-542px * var(--e)) calc(-44px * var(--e)); }
  }
  /* Morto: a caveira. Quadro 4 de `decompose.png`, parada — é o instante em que
     a carne já se foi e ainda não viraram restos. Quanto falta para voltar
     continua sendo dito pelo texto do cartão ("Volta em 4s"), e não repetido
     aqui.

     Este é o único estado que **não** usa a janela (42,44) do quadro do pacote:
     a caveira mede 10x7 e fica deitada no rodapé daquele quadro (bbox medido:
     (42,53)-(52,60)), então enquadrá-la como o corpo a jogaria no canto de
     baixo do nicho, pequena. A janela (39,49) é a mesma medida de 16x16
     recentrada nela, e a escala sobe um degrau — a caveira é a menor das cinco
     imagens, e no mesmo tamanho dos rostos ela seria a única que não preenche o
     nicho. Escala 4, e não 3,5: inteira sempre. */
  .horde-rosto-nicho[data-estado="morto"] .horde-rosto {
    --e: 4;
    background-image: url("../assets/sprites/atirador/decompose.png");
    background-size: calc(600px * var(--e)) calc(100px * var(--e));
    background-position: calc(-439px * var(--e)) calc(-49px * var(--e));
  }
  /* **O fundo do nicho é o mesmo em todos os estados** (`#07070a`, escrito uma
     vez na regra do nicho lá em cima). Cada estado já teve o seu bloco de cor
     — vermelho no dano, azul do trilho na cambalhota, escuro na morte — e eles
     saíram: como cada imagem preenche o quadro de um jeito diferente (o rosto
     é uma cabeça de 16x16, a cambalhota é o corpo inteiro recortado, a caveira
     é menor e sobe de escala), a cor aparecia em quantidades diferentes em cada
     um e o buraco na pedra lia como cinco buracos distintos, trocando de cor a
     cada cambalhota. O que o estado muda é a **imagem e o movimento**; a pedra
     em volta fica quieta. */
  /* Movimento, todo em `steps()` e em pixel inteiro: nada de transição contínua
     nem de deslocamento fracionário (ver docs/pixel-art.md). */
  /* Bravo é o estado de quem acabou de levar dano, e o tremor é a raiva. A
     piscada que este estado já teve saiu: ela era o piscar do golpe, e o coração
     apagando na fileira ao lado já é esse piscar — duas peças fazendo o mesmo
     gesto ao mesmo tempo leem como uma falha de desenho. O que o rosto acrescenta
     é o **humor**, e humor não pisca. */
  .horde-rosto-nicho[data-estado="bravo"] .horde-rosto {
    animation: horde-rosto-tremor 200ms steps(2) infinite;
  }
  @keyframes horde-rosto-tremor {
    from { transform: translateX(0); }
    to { transform: translateX(2px); }
  }
  /* O capuz sem olhos balançando devagar é o que faz `face_empty` ler como
     sono, e não como morte — é o movimento que separa os dois usos do mesmo
     arquivo. */
  .horde-rosto-nicho[data-estado="dormindo"] .horde-rosto {
    animation: horde-rosto-cochilo 1800ms steps(3) infinite alternate;
  }
  @keyframes horde-rosto-cochilo {
    from { transform: translateY(0); }
    to { transform: translateY(3px); }
  }
  /* Morto: o corpo caiu. Deslocamento em pixel inteiro e opacidade em degrau
     único — o retrato apaga junto com o resto do cartão (`.horde-player.morto`),
     e não por conta própria. */
  .horde-rosto-nicho[data-estado="morto"] .horde-rosto {
    transform: translateY(4px);
  }
  /* Cada cartão ocupa exatamente 1/4 da largura, independente de quantos
     jogadores estão na partida. Dimensionado pelo conteúdo (como era antes) o
     cartão mudava de largura no meio da luta: as barras de vida/escudo são
     fileiras de ícones, então perder corações encurtava a fileira, e a caixa
     inteira encolhia junto. Largura fixa também mantém o cartão de cada
     jogador no mesmo lugar de uma onda para a outra. */
  .horde-player {
    display: flex;
    flex-direction: column;
    gap: 3px;
    /* Âncora do combo, que fica fora do fluxo no canto superior direito
       (`.horde-player-combo`). */
    position: relative;
    flex: 0 0 25%;
    width: 25%;
    min-width: 0;
    box-sizing: border-box;
    /* Mais folga lateral que os 8px de antes pelo mesmo motivo da faixa da
       base: a escadinha da laje come 4px de cada canto. */
    padding: 5px 12px;
    /* Fundo e sombra vêm de `.hud-pedra` (classe posta em criarCartao,
       hordeHud.js) — a mesma laje de pedra das telas do menu. */
  }
  /* O cartão do jogador local **não** tem destaque próprio — nem o fundo de
     pedra "aquecido", nem a moldura dourada de 2px, nem o nome em ouro. Os três
     existiam para achar o próprio cartão entre quatro iguais numa fileira só; o
     console local agora mora sozinho no centro da borda de baixo, do outro lado
     da tela em relação às fichas dos aliados, e não há com o que confundi-lo. O nome dele fica na cor do slot como o de
     todo mundo — a mesma cor da túnica do personagem na arena e do ponto dele
     no mini-mapa, o que liga o cartão ao corpo em vez de só dizer "você". */
  /* ----- Fileira de trilho + ícone (iconesDeBarra.js) -----
     Cada um dos três trilhos do console local mora numa fileira com o ícone
     dele à esquerda. O ícone existe porque as três barras são o mesmo desenho
     empilhado (mesmo bisel, mesmo bloco de 6px) e só a cor e a ordem as
     separavam — o que ninguém decora na primeira partida.

     **A fileira não é mais alta que o trilho.** A altura sai do próprio trilho
     (10, 6 e 8px) e o ícone é desenhado exatamente nessa altura, célula a
     célula, em `iconesDeBarra.js`; `align-items: center` nunca tem o que
     centrar. É essa a regra que sustenta o pedido de origem: o HUD é um overlay
     sobre a arena, e cada pixel que o console cresce é pixel de jogo que some.
     O que o ícone toma é **largura**, que não custa nada — o cartão tem largura
     fixa e o trilho encolhe 14px.

     O `margin-top` que era de cada trilho passou para cá: com ele no trilho,
     dentro de uma fileira flex, a margem entraria na altura da linha e o ícone
     ficaria centrado numa caixa maior que o desenho. */
  .horde-barra-linha {
    display: flex;
    align-items: center;
    gap: 4px;
    width: 100%;
    min-width: 0;
  }
  /* `display: flex` acima vence o `hidden` do HTML: sem esta regra a fileira do
     cano apareceria vazia no cartão de quem não tem cano nenhum. */
  .horde-barra-linha[hidden] {
    display: none;
  }
  .horde-barra-linha + .horde-barra-linha {
    margin-top: 2px;
  }
  /* O trilho é quem estica; o ícone tem largura fixa (a da grade dele). */
  .horde-barra-linha > :not(.horde-barra-icone) {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
  }
  /* Caixa do desenho: os pixels são `position: absolute` (`.resource-icon-pixel`,
     `currentColor`), como nos ícones de vida e escudo. Cor única para os três —
     e não a cor de cada trilho — porque nas barras a cor **é** estado ("pronto"
     ou "enchendo"), e um ícone verde ao lado de um trilho vermelho seria lido
     como um segundo estado, contradizendo o primeiro.

     `pointer-events: auto` devolve o `hover` só aqui: `#hordeHud` inteiro é
     `pointer-events: none` para não roubar o toque dos joysticks, e o balão
     (dicaDeBarra.js) precisa do evento. Nada de `cursor: help`: o cursor da
     partida é o desenhado pelo jogo (fireCursor.js). */
  .horde-barra-icone {
    position: relative;
    flex: 0 0 auto;
    color: var(--cor-texto-suave);
    pointer-events: auto;
  }
  /* No celular não há `hover`, e o balão nunca abre: um alvo de 10px no rodapé
     só serviria para engolir o toque de quem estava mirando. O ícone continua
     desenhado — ele é rótulo, não botão. */
  body.touch-mode .horde-barra-icone {
    pointer-events: none;
  }
  /* Barra da recarga do rolamento (a habilidade base de esquiva, ver as
     constantes ROLAMENTO_* em shared/constants.js), entre a de recarga do tiro e
     a do cano. Mesmo trilho de `.cooldown-bar` — mesmo bisel cavado, mesmo bloco
     de 6px no gradiente —, porque é a mesma pergunta que a de cima faz ("já
     posso de novo?"), e um segundo estilo de trilho no mesmo cartão só faria o
     jogador aprender mais uma gramática.

     Ela é a mais fina das três (6px) e vem no meio: o rolamento é a habilidade
     que se usa menos vezes por onda, e a hierarquia do cartão é vida > tiro >
     rolamento. Ao contrário da do cano, ela **não** nasce escondida — o
     rolamento é de todo jogador, não de uma build. Sem `transition`: a barra é
     reescrita a cada quadro, e transição contínua é o que a regra de pixel art
     proíbe. */
  .horde-rolamento-bar {
    width: 100%;
    height: 6px;
    box-sizing: border-box;
    padding: 1px;
    border-radius: 0;
    background: var(--cor-superficie-baixa);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-afundada);
    overflow: hidden;
  }
  /* Duas cores, como as outras duas barras, e o par é o **terceiro**: azul
     enchendo e ciano pronto. Vermelho/amarelo é da recarga do tiro e
     verde/vermelho é do cano — repetir qualquer um dos dois num trilho colado
     nos outros faria as três serem lidas como a mesma coisa. O azul é o mesmo
     tom do escudo no HUD: as duas coisas que este jogo tem para não levar dano. */
  .horde-rolamento-fill {
    height: 100%;
    width: 0%;
    background-color: var(--cor-hud-escudo);
    background-image: repeating-linear-gradient(
      90deg,
      transparent 0 6px,
      var(--cor-superficie-icone) 6px 8px
    );
  }
  .horde-rolamento-fill.pronto {
    background-color: var(--cor-hud-rolamento-pronto);
  }
  /* Barra do cano de cadência (Tambor/Ferrolho), abaixo da de recarga no cartão
     do jogador. Ela existe porque a barra de recarga sozinha mente sobre os dois
     canos — ver atualizarBarraDoCano em hordeHud.js. Reusa o trilho de
     `.cooldown-bar` (mesmo bisel cavado, mesmo bloco de 6px no gradiente): é o
     mesmo tipo de leitura, e um segundo estilo de trilho no mesmo cartão só
     faria o jogador aprender duas gramáticas.

     Chegou a ter metade da altura da de recarga, como informação secundária —
     mas nessa espessura ela sumia contra o cartão e ninguém a lia no meio da
     luta. Agora tem quase a altura da de recarga (8px contra 10px): continua
     subordinada pela ordem e pelo tamanho, sem depender de o jogador reparar
     numa faixa de 5px. Sem `transition` em `width` nem em `background-color` —
     a barra é reescrita a cada quadro e transição contínua é o que a regra de
     pixel art proíbe. */
  .horde-cano-bar {
    width: 100%;
    height: 8px;
    box-sizing: border-box;
    padding: 2px;
    border-radius: 0;
    background: var(--cor-superficie-baixa);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-afundada);
    overflow: hidden;
  }
  .horde-cano-fill {
    height: 100%;
    width: 0%;
    background-color: var(--cor-hud-cano-vazio);
    background-image: repeating-linear-gradient(
      90deg,
      transparent 0 6px,
      var(--cor-superficie-icone) 6px 8px
    );
  }
  /* Quatro estados, duas cores, e a cor é o que diz qual leitura vale: verde
     quando o cano entrega tiro agora (`pente` esvazia, tem bala; `carregado` é
     o instante em que o tiro cheio está disponível) e vermelho quando ele está
     enchendo e o tiro não sai como devia (`recarregando`, `carregando`). O par
     verde/vermelho é o mesmo par de todos os medidores do gênero, e não se
     confunde com o vermelho/amarelo da barra de recarga logo acima. */
  .horde-cano-fill.pente,
  .horde-cano-fill.carregado {
    background-color: var(--cor-hud-cano-pronto);
  }
  .horde-cano-fill.recarregando,
  .horde-cano-fill.carregando {
    background-color: var(--cor-hud-cano-vazio);
  }
  /* Selo do item "Lança do vazio" (quebra-jogo): a carga fechou e o próximo
     tiro atravessa a arena. Fica no cartão do jogador local, abaixo do trilho do
     cano, e só existe enquanto a carga está de pé — ver atualizarSeloDaLanca em
     hordeHud.js, onde está registrado por que ele é um elemento próprio e não um
     estado da barra do cano.

     **Não** é um quarto trilho, e a diferença é proposital: sem o bisel cavado
     (`--sombra-pedra-afundada`) e sem sub-elemento de preenchimento, porque as
     três barras de cima são prazos e esta peça é binária — ou está lá, ou não
     está. Uma faixa sempre cheia dentro de um trilho ensinaria a lê-la como
     prazo cheio.

     Violeta porque é a cor do ícone e do FX do item (#a78bfa em
     public/js/itemIcones.js): quem viu o clarão do carregamento reencontra a
     mesma cor no cartão. Ela também é o quarto hue do cartão — os três trilhos
     já são vermelho/amarelo (recarga), azul/ciano (rolamento) e verde/vermelho
     (cano) —, então o selo não se confunde com nenhum deles de relance.

     A piscada é em `steps(2)`: dois degraus de cor, sem meio-tom, que é o que a
     regra de pixel art da interface pede. Ela existe porque o selo aparece no
     meio da luta, longe do olho — e "apareceu agora" é justamente a informação. */
  .horde-lanca-selo {
    width: 100%;
    height: 5px;
    margin-top: 2px;
    border-radius: 0;
    background-color: var(--cor-lanca-pronta);
    box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
    animation: lanca-selo-pisca 0.8s steps(2, jump-none) infinite;
  }
  @keyframes lanca-selo-pisca {
    from { background-color: var(--cor-lanca-pronta); }
    to { background-color: var(--cor-lanca-pronta-brilho); }
  }
  /* Cartão de quem caiu (ou saiu): a caixa inteira **não** leva `opacity`. O
     texto de status (`.horde-player-respawn`: "Volta em 4s", "Caído", "Saiu")
     é justamente o que o grupo precisa ler nesse cartão, e opacidade no pai
     compõe com tudo o que está dentro — não há como devolver o contraste cheio
     ao texto depois. Então quem apaga são só as partes que representam o
     jogador em campo: nome, vida e as barras de prazo (que só o cartão
     local tem). (O combo não entra na
     lista porque quem caiu não tem combo aceso — ver atualizarCombo em
     hordeHud.js.) */
  .horde-player.morto .horde-player-name,
  .horde-player.morto .hearts,
  .horde-player.morto .horde-barra-linha,
  .horde-player.morto .horde-lanca-selo {
    opacity: 0.55;
  }
  .horde-player.desconectado .horde-player-name,
  .horde-player.desconectado .hearts,
  .horde-player.desconectado .horde-barra-linha,
  .horde-player.desconectado .horde-lanca-selo {
    opacity: 0.35;
  }
  /* Jogador local caído (`body.horde-morto`, ver atualizarEspectador em
     hordeHud.js): ele vira espectador e a câmera passa a seguir um
     companheiro vivo. O cartão de quem pode ser seguido volta a receber
     clique — `#hordeHud` inteiro é `pointer-events: none` para não roubar o
     toque dos joysticks, e só estes cartões, só enquanto ele está no chão,
     abrem exceção. */
  .horde-player.camera-clicavel {
    pointer-events: auto;
    cursor: pointer;
  }
  .horde-player.camera-clicavel:hover {
    background: var(--cor-superficie-elevada);
  }
  /* Quem a câmera está seguindo agora. Moldura de 2px para dentro (sem mudar
     o tamanho do cartão, que é fixo em 1/4 da largura), em branco e **não** em
     dourado: dourado é a marca fixa do cartão do jogador local
     (`.horde-player.local`), e enquanto ele está caído os dois apareceriam ao
     mesmo tempo na tela — a mesma moldura em dois cartões diferentes leria
     como "sou eu" duas vezes. Branco é neutro e some quando a câmera volta. */
  .horde-player.camera-alvo {
    outline: 2px solid var(--cor-branco);
    outline-offset: -2px;
  }
  .horde-player-top {
    display: flex;
    align-items: center;
    gap: 5px;
  }
  /* O nome existe **só na ficha do aliado**: o console local não escreve nome
     nenhum (o elemento nem é criado, ver criarCartao em hordeHud.js) — quem
     está jogando não precisa que a tela lhe diga como ele se chama, e a
     pergunta que o nome responde ("qual destes é o Fulano") só a coluna de
     aliados faz.

     Ele vai na cor do slot daquele jogador (`--slot-color`, posto pelo JS a
     partir de shared/coresDeJogador.js): é a mesma cor do nome acima da cabeça
     dele na arena e do ponto dele no mini-mapa. Não é `--class-color` porque na
     Defesa da base a classe é única — a cor de classe sairia igual nos quatro
     cartões e não identificaria ninguém. */
  .horde-player-name {
    color: var(--slot-color, var(--cor-texto));
    font-size: clamp(9px, calc(0.02 * var(--largura-arena)), 14px);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
  }
  /* Combo de abates do jogador daquele cartão. Fica no cartão — e não como um
     número grande no meio da tela de quem o fez — para o grupo inteiro ver a
     sequência de cada um; é isso que a torna uma disputa. Some quando não há
     combo (o JS esvazia o texto), então não reserva espaço em cartão parado.
     Dourado como o excedente de power-up: é a mesma família de "acima do
     normal". */
  /* Combo de abates do slot, em letras de pixel desenhadas por
     `desenharCombo` (hordeHud.js) — não é texto de CSS. Fica no canto superior
     direito, **fora do fluxo**: assim ele pode crescer de tamanho e tremer a
     cada abate sem empurrar o nome do jogador, que era o que prendia a versão
     anterior (um `<span>` na linha do nome) a um tamanho fixo e discreto.
     A cor vem do próprio desenho (a mesma do número grande no meio da tela,
     ver hordeRender.js), então aqui não há `color`.

     O tamanho na tela é a altura do buffer em pixels de arte
     (`--combo-altura`, escrita pelo JS a cada troca de escala) vezes o tamanho
     de um pixel de arte (`--combo-unidade`) — e não uma altura fixa, que
     desfaria justamente o crescimento em degraus. `width: auto` deixa o
     navegador seguir a proporção do buffer, como no ícone de item. */
  .horde-player-combo {
    --combo-unidade: clamp(1.2px, calc(0.0022 * var(--largura-arena)), 2px);
    position: absolute;
    top: 2px;
    right: 3px;
    display: block;
    height: calc(var(--combo-altura, 16) * var(--combo-unidade));
    width: auto;
    image-rendering: pixelated;
    /* Todo `canvas` deste CSS nasce com o fundo da superfície elevada (regra
       genérica lá em cima, feita para o canvas da arena). Aqui ele viraria uma
       caixa cinza em volta do número, no meio do cartão de pedra. */
    background: transparent;
    /* O cartão volta a receber clique quando o jogador local está caído (ver
       `.camera-clicavel`): o combo por cima dele não pode roubar esse toque. */
    pointer-events: none;
  }
  /* `display: block` acima venceria o `hidden` do agente de usuário. */
  .horde-player-combo[hidden] {
    display: none;
  }
  body.touch-mode.horde-mode .horde-player-combo {
    --combo-unidade: clamp(0.9px, calc(0.0016 * var(--largura-arena)), 1.4px);
  }
  /* Fica ao lado do nome, na mesma linha: como o estado ("Caído", "Volta em
     6s") só existe parte da partida, uma linha própria reservaria altura vazia
     no cartão o tempo todo. O nome é quem encolhe (já tem reticências); o
     estado nunca quebra. */
  /* Status do jogador caído. Fica em contraste cheio (ver `.horde-player.morto`
     acima) e ganha contorno sólido de 1px: o cartão fica por cima da arena, que
     em pleno combate é clara e cheia de sprite, e vermelho em cima de vermelho
     de sangue/fogo era o que apagava o texto. Sombra sólida deslocada, sem
     blur, como o resto da interface (ver docs/pixel-art.md). */
  .horde-player-respawn {
    font-size: clamp(9px, calc(0.02 * var(--largura-arena)), 14px);
    color: var(--cor-vermelho-destaque);
    text-shadow: 1px 1px 0 var(--cor-contorno-icone-hud);
    flex: 0 0 auto;
    margin-left: auto;
    white-space: nowrap;
  }
  /* Em touch a arena é a viewport quase inteira, e o cartão do jogador
     (`.horde-player`) lia grande demais nesse espaço reduzido — encolhido
     junto com o pixel do ícone (ver HEART_PIXEL_VMIN_TOUCH em hordeHud.js).
     Escopado a `body.horde-mode` porque `.hearts` é uma caixa genérica de
     recurso (ver createResourceBar em hud.js), e não uma classe do cartão:
     sem o escopo, a medida de touch valeria para qualquer outra fileira de
     ícones que venha a usá-la. */
  body.touch-mode.horde-mode .horde-player {
    gap: 1px;
    padding: 3px 5px;
  }
  /* Em touch a arena ocupa a viewport quase inteira e o console já encolhe
     (regra acima). O retrato desce um degrau **inteiro** de escala — 16 x 2 =
     32px —, e não uma fração de 48: escala fracionária põe pixels de tamanhos
     diferentes na mesma imagem, que é o único jeito de a pixel art sair
     visivelmente errada. O vão da linha encolhe junto. */
  body.touch-mode.horde-mode .horde-player-linha {
    gap: 5px;
  }
  body.touch-mode.horde-mode .horde-player-pilha {
    gap: 1px;
  }
  body.touch-mode.horde-mode .horde-rosto-nicho {
    width: calc(16px * (2 + var(--degrau-rosto, 0)));
    height: calc(16px * (2 + var(--degrau-rosto, 0)));
  }
  body.touch-mode.horde-mode .horde-rosto {
    --e: calc(2 + var(--degrau-rosto, 0));
  }
  body.touch-mode.horde-mode .horde-rosto-nicho[data-estado="morto"] .horde-rosto {
    --e: calc(3 + var(--degrau-rosto, 0));
  }
  body.touch-mode.horde-mode .horde-player-name {
    font-size: clamp(7px, calc(0.015 * var(--largura-arena)), 11px);
  }
  body.touch-mode.horde-mode .horde-player-respawn {
    font-size: clamp(7px, calc(0.015 * var(--largura-arena)), 11px);
  }
  body.touch-mode.horde-mode .horde-player .resource-count {
    font-size: clamp(6px, 1.8vmin, 9px);
  }
  body.touch-mode.horde-mode .horde-player .resource-int {
    min-width: clamp(14px, 5vmin, 22px);
  }
  /* O trilho do cooldown fica na altura padrão (10px) também aqui (inclusive
     em touch): com 6px, a borda de 1px e o padding de 2px do trilho consomem
     os 6px inteiros e sobra **zero** de altura para .cooldown-fill — a barra
     nunca enchia. */

  /* ===== Painel da ferramenta de debug (Defesa da base) ==================
     Só existe em host local (ver public/js/hordeDebugTool.js): nenhum jogador
     final vê esta peça. Mesmo assim segue a estética do resto — canto reto,
     borda sólida, nada de gradiente nem transição contínua —, porque um painel
     que destoa acaba lido como parte do jogo em captura de tela.

     Fixo à direita e por cima da arena (`z-index` acima do #hordeHud, abaixo
     da tela de escolha de item, que precisa continuar clicável). Rola por
     dentro: o catálogo de itens mais a árvore inteira não cabem em tela
     nenhuma. */
  /* A arena não fica **atrás** do painel: `body.horde-debug-ativo` reserva a
     faixa da direita como padding, o que desloca para a esquerda o centro de
     onde o `#game-wrap` se alinha, e `updateGameScale` desconta a mesma largura
     (`state.larguraReservadaDireita`) para a arena encolher e caber no que
     sobrou. Sem as duas metades, o painel cobriria o canto direito do campo e o
     HUD — justamente o que se está tentando olhar. A largura é escrita pelo JS
     na variável (hordeDebugTool.js é a fonte única do número); o valor aqui é o
     fallback. */
  body.horde-debug-ativo {
    padding-right: var(--horde-debug-largura, 268px);
    box-sizing: border-box;
  }
  #hordeDebugPanel {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: var(--horde-debug-largura, 268px);
    /* `border-box` não é detalhe: a largura da variável é a **mesma** que
       `updateGameScale` desconta da arena, e sem isto a borda de 2px ficava
       fora dela — o painel media 270px, a reserva 268px, e a arena passava
       exatamente esses 2px por baixo dele. */
    box-sizing: border-box;
    z-index: 12;
    display: flex;
    flex-direction: column;
    background: var(--cor-modal-fundo);
    border-left: 2px solid var(--cor-borda);
    color: var(--cor-texto);
    font-size: 11px;
  }
  /* Recolhido = só o cabeçalho. Serve para olhar a arena inteira sem sair da
     sala e perder a build montada. */
  #hordeDebugPanel.recolhido {
    bottom: auto;
  }
  #hordeDebugPanel.recolhido .horde-debug-corpo {
    display: none;
  }
  .horde-debug-cabecalho {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    padding: 6px 8px;
    border-bottom: 2px solid var(--cor-borda);
    background: var(--cor-barra-fundo);
  }
  .horde-debug-cabecalho strong {
    font-size: 12px;
    letter-spacing: 1px;
    text-transform: uppercase;
  }
  /* Fase e onda em curso: é o que diz se "Disparar onda" vai fazer alguma
     coisa, e evita ter de abrir o devtools para saber onde a partida está. */
  /* Linha própria (`flex-basis: 100%` + `order`), abaixo do título e dos dois
     botões: em 268px de painel a fase disputava espaço com "Recolher"/"Sair" e
     quebrava no meio da palavra. */
  .horde-debug-fase {
    flex: 1 0 100%;
    order: 3;
    color: var(--cor-texto-fraco);
    font-size: 10px;
  }
  /* Fica fora de `.horde-debug-corpo` (a parte que rola): é o recado que explica
     por que nada desta sala aparece no perfil, e rolar para fora da vista é
     justamente o que ele não pode fazer. */
  .horde-debug-aviso {
    margin: 0;
    padding: 5px 8px;
    background: var(--cor-vermelho-selecao);
    border-bottom: 2px solid var(--cor-borda);
    color: var(--cor-texto-suave);
    font-size: 10px;
    line-height: 1.3;
  }
  #hordeDebugPanel.recolhido .horde-debug-aviso {
    display: none;
  }
  .horde-debug-corpo {
    flex: 1;
    overflow-y: auto;
    padding: 8px;
  }
  .horde-debug-secao + .horde-debug-secao {
    margin-top: 12px;
    border-top: 1px solid var(--cor-borda);
    padding-top: 10px;
  }
  .horde-debug-secao h3 {
    margin: 0 0 4px;
    font-size: 11px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--cor-texto-suave);
  }
  .horde-debug-dica {
    margin: 0 0 6px;
    color: var(--cor-texto-fraco);
    font-size: 10px;
    line-height: 1.3;
  }
  .horde-debug-grade {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
  }
  .horde-debug-chip,
  .horde-debug-acao,
  .horde-debug-mini {
    background: var(--cor-superficie);
    border: 1px solid var(--cor-borda);
    color: var(--cor-texto);
    font-family: inherit;
    font-size: 10px;
    padding: 3px 6px;
    cursor: pointer;
  }
  .horde-debug-chip:hover,
  .horde-debug-acao:hover,
  .horde-debug-mini:hover {
    background: var(--cor-superficie-hover);
  }
  .horde-debug-chip.ativo,
  .horde-debug-interruptor.ativo {
    background: var(--cor-vermelho);
    border-color: var(--cor-vermelho-destaque);
  }
  /* Interruptor empilha em vez de dividir a linha com o vizinho: o rótulo cresce
     quando ele está ligado ("Imortal" -> "Imortal: ligado"), e em duas colunas
     de 130px a segunda palavra quebraria só num dos estados. */
  .horde-debug-interruptor {
    margin-top: 4px;
  }
  .horde-debug-acao {
    display: block;
    width: 100%;
    margin-top: 6px;
    padding: 5px;
    font-size: 11px;
  }
  .horde-debug-acao:disabled,
  .horde-debug-mini:disabled {
    opacity: 0.4;
    cursor: default;
  }
  .horde-debug-mini {
    min-width: 20px;
    text-align: center;
  }
  .horde-debug-lista {
    list-style: none;
    margin: 0;
    padding: 0;
  }
  .horde-debug-grupo {
    margin: 8px 0 3px;
    color: var(--cor-texto-fraco);
    font-size: 10px;
    letter-spacing: 1px;
    text-transform: uppercase;
  }
  .horde-debug-item,
  .horde-debug-talento {
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 2px 3px;
    border-left: 3px solid transparent;
  }
  /* O que está na build ganha peso, para achar de relance o que já foi ligado
     numa lista de dezenas de linhas. */
  .horde-debug-item.ativo,
  .horde-debug-talento.ativo {
    background: var(--cor-superficie-baixa);
    border-left-color: var(--cor-vermelho-destaque);
  }
  /* Nó cujo pai ainda não está no rank máximo: o + dele não responde, e sem a
     marca isso lê como painel quebrado. Aqui a `opacity` pode: a linha fica
     sobre o fundo sólido do painel de debug, e não sobre a parede de pedra da
     tela — que é o motivo de o nó da árvore de verdade
     (.talent-no-bloqueado-pai) apagar por cor, e não por opacidade. */
  .horde-debug-talento.bloqueado,
  /* Item cuja família uma irmã já ocupou: o painel respeita a exclusividade de
     família da partida normal, então o + dele não responde — e a linha apaga
     pelo mesmo motivo (e com a mesma ressalva sobre `opacity`) do nó de
     talento acima. */
  .horde-debug-item.bloqueado {
    opacity: 0.45;
  }
  /* `image-rendering: pixelated` no canvas do ícone: mesmo cuidado do
     Relicário — o canvas tem exatamente o tamanho que o CSS reserva, então não
     há reescala, mas a regra é a rede de segurança se o painel encolher. */
  .horde-debug-item-icone {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    image-rendering: pixelated;
  }
  .horde-debug-item-nome,
  .horde-debug-talento-nome {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .horde-debug-contador {
    display: flex;
    align-items: center;
    gap: 3px;
    flex-shrink: 0;
  }
  .horde-debug-conta {
    min-width: 24px;
    text-align: center;
    color: var(--cor-texto-suave);
    font-size: 10px;
  }
  /* Medição do boneco de treino: rótulo à esquerda, número à direita, em grade
     de duas colunas. O número é tabular e alinhado à direita porque ele muda
     quatro vezes por segundo — com dígito de largura variável, a coluna
     inteira dança a cada atualização e fica ilegível justamente enquanto se
     está atirando. */
  .horde-debug-medicao {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 1px 6px;
    margin: 6px 0 0;
    font-size: 10px;
  }
  .horde-debug-medicao dt {
    color: var(--cor-texto-fraco);
  }
  .horde-debug-medicao dd {
    margin: 0;
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--cor-texto-suave);
  }

  /* ===== Realce de foco (navegação sem ponteiro) =====================
     O elemento em foco ganha **uma** moldura branca, igual em toda peça —
     botão de pedra, carta de item, linha de lista, nó da árvore de talentos,
     aba da barra superior, campo de texto. Antes disto o foco era pontual:
     quatro componentes tinham um `:focus-visible` próprio (`.item-choice-botao`,
     `.talent-no-disponivel`, `.resumo-item-atalho` e o `:focus-within` dos
     campos) e todo o resto da interface não dizia nada — quem navega de
     controle não tinha como saber onde estava.

     Três decisões, e as três são o que faz o realce ser um sistema em vez de
     mais um caso:

     - **É `:focus`, e não `:focus-visible`.** O `:focus-visible` é uma
       heurística do navegador sobre *teclado*, e o controle não é teclado: o
       foco que `focarElemento` aplica ao abrir uma camada (public/js/navegacaoPorFoco.js)
       chega sem nenhum evento de tecla antes dele, e o Chromium não o marca
       como visível. O realce sairia justamente para quem ele existe. Quem
       responde "isto é navegação sem ponteiro?" é o jogo, e a resposta chega
       aqui como a classe `foco-sem-ponteiro` no `<body>` (escrita por
       public/js/entradaDeFoco.js a partir de `navegandoSemPonteiro()`) — que é
       o mesmo sinal que decide se abrir uma camada move o foco sozinho. Com
       ponteiro em uso a classe cai e o realce some inteiro, sem roubar nada de
       quem está no mouse.

     - **É `outline` recuado para dentro, e não um anel por fora.** Anel por
       fora não sobrevive ao `clip-path` da escadinha (ver o comentário de
       `--recuo-realce-foco` no `:root`); `border` empurraria o layout; e
       `background-image` — a receita que as peças usam para desenhar a
       moldura delas — teria de conhecer a cor de cada uma. `outline` não ocupa
       espaço, funciona em `<input>` (que não tem pseudo-elemento) e some
       inteiro quando a classe sai. Recuado por 6px, ele nasce como uma segunda
       moldura logo dentro da primeira, com 2px do material da peça entre as
       duas — o vão é o que impede que elas se leiam como um traço só.

     - **Branco sólido, sem pisca e sem transição.** Ele **era** vermelho, e
       foi por isso que passou despercebido: vermelho é a cor que as peças já
       vestem (o fundo e a moldura de `#btnHorde`, a borda do campo em foco, a
       linha marcada do Monstruário), e um anel vermelho sobre elas some. Medido
       em pixel: 3,0:1 no `#btnHorde` e 1,7:1 no `#lojaComprarBtn` sem saldo,
       que ainda leva `filter: grayscale(1)` — um filtro que apaga matiz apaga
       um anel que só tem matiz. Branco é a única cor que nenhuma peça usa como
       moldura **em foco** (a do campo vira vermelha justamente aí) e a única
       que atravessa o grayscale, porque o que sobra de um filtro desses é
       luminância. O par de duas cores existe: a metade escura é a moldura da
       própria peça, um degrau para fora. Sem pisca porque foco é um estado que
       dura enquanto o jogador estiver ali — um anel piscando na tela inteira
       seria ruído, e o precedente do arquivo é o contrário ("o contorno existe
       o tempo todo, e marcar é uma troca de cor só"). Quem um dia quiser o
       pisca precisa de `steps(2, jump-none)`, nunca de `steps(1, end)
       alternate` (ver a armadilha em public/css/CLAUDE.md).

     **Como ele se distingue do hover:** o hover desta interface nunca é um
     anel — ele é *fundo* (troca de `--cor-fundo-campo`/`--cor-fundo-linha`, o
     `--cor-superficie-hover` das linhas e dos nós, a troca de fundo das abas)
     ou *movimento* (o `translateY`/`scale` da regra genérica de `button:hover`).
     O realce de foco não toca em nenhum dos dois: ele acrescenta uma linha
     branca onde antes não havia linha nenhuma — e branco não é cor de hover em
     peça nenhuma deste arquivo. Os dois podem estar de pé ao
     mesmo tempo no mesmo elemento e continuam legíveis como duas coisas
     diferentes.

     `[tabindex="-1"]` fica de fora: é a marca de "focável por programa, mas
     fora da navegação", e é o que uma camada (a modal inteira) usa em si mesma
     para receber o foco ao abrir — emoldurar a camada inteira diria "você está
     na modal" onde a pergunta é "você está em qual botão dela". */
  body.foco-sem-ponteiro :focus:not([tabindex="-1"]) {
    outline: var(--espessura-realce-foco) solid var(--cor-realce-foco);
    outline-offset: var(--recuo-realce-foco);
  }
  /* Campo de texto: quem veste o realce é o **invólucro** (`.campo-pixel`), e
     não o `<input>`. É o mesmo motivo de a moldura em pixel art morar ali (o
     autofill do Chrome apaga o `background-image` do campo) e do
     `:focus-within` que os campos já usavam: o `<input>` fica encostado na
     faixa alta do miolo, e um anel nele sairia colado na moldura em vez de
     dentro dela. Sem o `outline: none`, o campo ganharia dois anéis.

     **O `<select>` entra na mesma regra, e ele é o caso em que os dois anéis de
     fato apareceram**: o do invólucro (a 6..8px da borda dele) e o do próprio
     campo, dois traços concêntricos a 4px um do outro, medidos em captura. O
     `input:focus` daqui cobria só `<input>`, e o seletor de fase é o único campo
     do jogo que não é um. */
  body.foco-sem-ponteiro .campo-pixel:focus-within {
    outline: var(--espessura-realce-foco) solid var(--cor-realce-foco);
    outline-offset: var(--recuo-realce-foco);
  }
  body.foco-sem-ponteiro .campo-pixel input:focus,
  body.foco-sem-ponteiro .campo-pixel select:focus {
    outline: none;
  }
  /* ----- Peça com arte no miolo: o anel precisa levar o próprio fundo -----
     O `-6px` do anel foi calibrado em `#btnHorde` e `#lojaComprarBtn`, e as
     duas são **pedra chapada** a 4–6px de dentro: ali o realce ganha o par de
     duas cores de graça (branco por dentro, a moldura escura da própria peça
     um degrau para fora). Nas peças abaixo esse par não existe, e o jogador
     relatou exatamente o sintoma — "não dá para ver a borda do seletor na
     seleção de telas e na seleção de cartas". Medido a 1440x900, amostrando o
     pixel do anel contra o **vizinho imediato** dele (a faixa 2..4px, para
     fora, e a 6..8px, para dentro), o número explica o relato: veja
     `--cor-realce-foco-fundo` no `:root`.

     A receita é a mesma nas duas: uma faixa escura de 6px atrás do anel
     (2..8px da borda), com os 2px brancos no meio. O que muda é **em que
     canal** ela cabe, e o motivo de cada escolha está em cada regra. */

  /* **Quadrado de nível** (a grade de fases do navegador de salas e da sala de
     espera). Aqui não é só contraste: a textura da arena é um `::before`
     posicionado (`inset: 2px`), e o Chromium pinta pseudo-elemento posicionado
     **por cima** do `outline` do elemento. Medido, o anel só sobrevivia nas
     quatro quinas — nos entalhes que a escadinha do recorte do `::before` abre
     —, e o resto dele ficava embaixo da textura. Um anel invisível por estar
     coberto é pior que um anel de pouco contraste: nenhum ajuste de cor o
     traria de volta.

     Então o realce inteiro (faixa escura + anel) é desenhado num `::after`,
     que vem **depois** do `::before` e por isso fica por cima dele. De brinde
     ele ganha o que o `outline` nunca teve: as três camadas de `box-shadow`
     são recortadas pelo mesmo `clip-path` do miolo, então o anel **acompanha a
     escadinha** da peça em vez de desenhar um retângulo de cantos vivos por
     dentro dela. As camadas vão de dentro para fora porque `box-shadow` pinta
     a primeira por cima: 2px escuros, 2px brancos, 2px escuros.
     O `outline` do sistema sai (ele estaria embaixo da textura de qualquer
     jeito, e nas quinas apareceria como um segundo desenho fora de fase). */
  body.foco-sem-ponteiro .horde-level-item:focus {
    outline: none;
  }
  body.foco-sem-ponteiro .horde-level-item:focus::after {
    content: '';
    position: absolute;
    /* A mesma caixa do ::before da textura: 2px para dentro da borda da peça,
       com o mesmo recorte. */
    inset: var(--espessura-borda-botao);
    clip-path: var(--recorte-botao-pixel);
    box-shadow:
      inset 0 0 0 2px var(--cor-realce-foco-fundo),
      inset 0 0 0 4px var(--cor-realce-foco),
      inset 0 0 0 6px var(--cor-realce-foco-fundo);
    pointer-events: none;
  }

  /* **Medalha da grade de conquistas** (`achievementsGrid.js`, na visão
     Conquistas e na modal de perfil de outro jogador). Ela é focável desde que
     a aba inteira passou a ser alcançável sem ponteiro, e o problema dela é o
     outro: aqui o `outline` **aparece** (não há pseudo-elemento posicionado
     sobre o anel), mas a medalha desbloqueada tem fundo cheio na cor do tier e
     nos claros o branco encosta no branco — 1,40:1 na prata.

     A faixa escura vai no `box-shadow` do próprio elemento, e não num
     pseudo-elemento. O `::after` já é o brilho que varre o diamante e o
     lendário, e sobraria o `::before` — mas um pseudo aqui trocaria uma peça
     que hoje pinta certo por uma que depende de ordem de pintura, que é
     justamente o modo de falha que o quadrado de nível logo acima teve. Isso
     obriga a **repetir** as
     duas sombras que a medalha já tem, e a ordem é a informação: a faixa de
     3px do tier fica na frente para não ser engolida — ela é o que diz o tier
     da medalha ainda trancada —, e o bisel de pedra vai para o fim, onde a
     faixa escura o cobre enquanto o foco durar.

     A tarja "New" não corre risco: ela é `position: absolute` com `z-index`, e
     `box-shadow: inset` é pintado no fundo da caixa, embaixo de qualquer
     descendente posicionado. Conferido na captura. */
  body.foco-sem-ponteiro .achievement-badge:focus {
    box-shadow:
      inset 3px 0 0 var(--cor-tier, var(--cor-borda)),
      inset 0 0 0 8px var(--cor-realce-foco-fundo),
      var(--sombra-pedra-pequena-interna);
  }
  /* O esqueleto de carregamento não é medalha nenhuma: ele não recebe
     `tabindex` (ver `renderSkeleton` em achievementsGrid.js) e não pode
     **parecer** focável nem por acidente — se um dia um `focus()` de programa
     cair nele, ele não acende. */
  body.foco-sem-ponteiro .achievement-badge.skeleton-loading:focus {
    outline: none;
    box-shadow: inset 3px 0 0 var(--cor-tier, var(--cor-borda)),
      var(--sombra-pedra-pequena-interna);
  }

  /* ----- Peça pequena: o anel não cabe, então quem acende é a moldura -----
     `outline-offset: -6px` num quadrado de lado L desenha um anel de (L-12)px.
     No `x` de expulsar da sala de espera (16x16) isso é um ponto de 4x4; no ✕
     de fechar (26x26), um quadrado de 14x14 bem em cima do glifo. E baixar o
     recuo não resolve nos que têm `clip-path`: qualquer faixa que comece antes
     de 4px cai dentro do quadrado de 4x4 que `--recorte-icone-pixel` come de
     cada canto, e o anel volta a ser "quatro riscos soltos com um vão em cada
     quina" — que é exatamente o defeito medido que aposentou o `-4px`.

     Então a peça pequena se divide em duas famílias, e cada uma usa o canal
     que ela já tem:

     1. **Quem tem a moldura de botão de ícone** (`--cor-borda-campo`, ver a
        regra compartilhada de `#btnConfiguracoes`) acende a **própria moldura** em
        branco, e o anel sai. É o precedente do arquivo — "o contorno existe o
        tempo todo, e marcar é uma troca de cor só" —, ele acompanha a
        escadinha por construção (as camadas de `background-image` são
        desenhadas com ela) e não disputa espaço com o ícone. O ✕ de cabeçalho
        nasce com a moldura transparente e a acende no hover: em foco ela
        acende branca, que é o mesmo gesto um degrau acima.
     2. **Quem não tem moldura nenhuma** (o `x` de expulsar e o olho de mostrar
        senha são fundo transparente e um glifo) redeclara
        `--recuo-realce-foco`, que é para isso que ele é variável. `-2px` só é
        seguro porque nenhuma das duas tem `clip-path`: o anel encosta na borda
        da caixa e não há escadinha para comê-lo.

     Peças de 40px (as abas da barra superior, o de configurações, o `#pauseMenuBtn`)
     ficam de fora: lá o anel de 28x28 é o realce que o resto da interface tem,
     e trocá-lo custaria a uniformidade sem ganhar nada.

     **Duas exceções entram na família 1 por medida, e não por tamanho**, e as
     duas são "quem tem arte encostada na faixa do anel":

     - o **botão de conta** (`#accountFabBtn`) tem 40px de altura e ainda assim
       não sobrevive ao anel: o anel de nível é desenhado a **5px** da borda
       (levelRing.js), e a faixa branca de 6..8px atravessava a
       circunferência dele — medido a 1440x900, o anel de foco cortava o de
       nível ao meio. Ele é a mesma peça de moldura em camadas dos botões de
       ícone, então acender a própria moldura é o realce que já existia ali;
     - o **botão de mudo da tela de configurações** encolhe para 28px
       (`.tela-config .volume-control button`) e cai na mesma medida do ✕ de
       cabeçalho, com o glifo a 6px da borda — a faixa nascia encostada nele. */
  body.foco-sem-ponteiro .modal-close:focus,
  body.foco-sem-ponteiro .modal-close-flutuante:focus,
  body.foco-sem-ponteiro #accountFabBtn:focus,
  body.foco-sem-ponteiro .tela-config .volume-control button:focus {
    outline: none;
    --cor-borda-campo: var(--cor-realce-foco);
  }
  /* O ✕ de cabeçalho precisa do **miolo** junto, e isso foi medido do jeito
     ruim: com só a borda branca ele saiu um quadrado branco sólido de 26px na
     captura. A moldura desses botões é a camada de **baixo** das três
     (`background-image` na regra compartilhada de `#btnConfiguracoes`), e o miolo
     dele é `transparent` até o hover — sem miolo opaco, a cor da moldura
     aparece na caixa inteira. O fundo é o mesmo do hover dele, então o foco é
     o hover com a moldura acesa um degrau acima. O ✕ flutuante não precisa:
     o miolo dele já é o vermelho de ação. */
  body.foco-sem-ponteiro .modal-close:focus {
    --cor-fundo-campo: var(--cor-superficie-hover);
  }
  body.foco-sem-ponteiro .horde-lobby-expulsar:focus,
  body.foco-sem-ponteiro .password-toggle-btn:focus {
    --recuo-realce-foco: -2px;
  }

  /* ----- Peça sem material: o anel vai por FORA -----
     O recuo negativo do sistema pressupõe **material da peça** entre a borda e
     o conteúdo: em `#btnHorde` (70px de altura, 14px de folga) a faixa de 4..6px
     cai em pedra chapada. As peças abaixo não têm material nenhum — elas são
     texto, ou um campo com 16px de altura —, e ali o mesmo recuo deixa de ser
     moldura e passa a ser **um risco por cima do conteúdo**. Foi o defeito que
     o jogador relatou ("uma borda branca cortando o elemento na metade") e ele
     está medido, peça por peça, por
     `.claude/skills/test-gameplay/scripts/levantar-realce-de-foco.js` — a
     distância do conteúdo à borda ("folga") contra a faixa do anel:

     | peça | caixa | folga | o que a faixa de 4..6px fazia |
     | --- | --- | --- | --- |
     | `.ranking-nome` | 456x18 | 0px | as duas faixas se encontravam: um retângulo branco fechado por cima do nome |
     | `input[type=range]` da tela de configurações | 476x16 | — | idem, **anel colapsado**: uma barra branca sólida no lugar do slider |
          | ponto de leitura das Novidades (`h3`/`h4`) | 652x20 | 0px | risco atravessando o título |
     | `.link-button` ("Entrar", "criar conta") | 71x18 | 0px | idem |
     | `<a>` dos Créditos | 134x17 | 0px | idem |
     | `#hordeLobbyCode` (o código do convite, na sala de espera) | 174x18 | 0px | risco atravessando "Código: XXXXX copiar" |

     Nenhuma delas tem `clip-path` — e é só por isso que o anel por fora é
     possível aqui. O recorte em escadinha é o motivo de o recuo do sistema ser
     negativo (ver `--recuo-realce-foco` no `:root`), e onde ele existe o anel
     externo simplesmente não é pintado. Confirmado no levantamento: as sete
     peças acima respondem `clip-path: none`.

     `2px` e não `0`: encostado na caixa, o anel de uma linha de texto se lê
     como sublinhado de três lados em vez de moldura. 2px é o mesmo vão que o
     recuo de dentro deixa entre a moldura da peça e o anel — o realce continua
     sendo "uma segunda linha a 2px da primeira", só que agora medida para o
     outro lado. */
  body.foco-sem-ponteiro .ranking-nome:focus,
  body.foco-sem-ponteiro .link-button:focus,
  body.foco-sem-ponteiro #hordeLobbyCode:focus,
  body.foco-sem-ponteiro #creditsOverlay a:focus,
  body.foco-sem-ponteiro #changelogBody [data-focavel]:focus,
  body.foco-sem-ponteiro input[type="range"]:focus {
    --recuo-realce-foco: 2px;
  }
  /* ----- As setas do seletor de fase -----
     Em foco sem ponteiro, o único `<select>` do jogo mostra **uma seta de cada
     lado**, para fora, dizendo que o direcional troca a fase. Elas existem
     porque o gesto daquele campo é o único da interface que não se adivinha: em
     todo o resto o direcional **move o foco**, e aqui ele opera o valor — a
     escadinha que o campo já desenha à direita promete outra coisa (abrir a
     lista), e é justamente a coisa que o controle **não** consegue fazer, porque
     nenhum navegador deixa um script abrir aquele menu.

     Quatro decisões:

     - **Elas moram na fileira (`#hordeCampanhaFaseLinha`), e não no campo.**
       `.campo-pixel` é recortado por `clip-path`, e o recorte apaga tudo o que
       é pintado fora da silhueta — pseudo-elemento incluído. Dentro da
       silhueta também não dava: o vão de 10px do invólucro já é onde o anel de
       foco cai (6..8px da borda), e uma seta de 6px ali encostaria nele.
     - **A fileira tem a caixa do campo**, então as setas se ancoram por
       `right: 100%` / `left: 100%` em vez de um `calc()` com a largura escrita
       de novo — o campo cai para `max-width: 100%` no celular, e um número
       mágico deixaria as setas soltas longe dele exatamente ali.
     - **Elas são desenhadas por gradiente, em três colunas de 2px** (8, 4 e 2px
       de altura), que é a **mesma** escadinha que o campo desenha na seta de
       abrir — espelhada para o lado. Nada de SVG e nada de `border` diagonal:
       triângulo de borda é antisserrilhado, e aqui a peça tem 6px.
     - **Na ponta da lista a seta apaga**, em vez de sumir: ela é a resposta a
       "dá para andar para este lado?", e some do lugar seria pior que apagada —
       o precedente do arquivo é "o contorno existe o tempo todo, e marcar é uma
       troca de cor só". Quem responde é o CSS, sem uma linha de JS:
       `:has(option:first-child:checked)` / `:has(option:last-child:checked)` — o
       `<option>` selecionado casa com `:checked`, então a marcação continua
       verdadeira seja quem for que mudou o valor (direcional, teclado ou a lista
       nativa do mouse). Navegador sem `:has()` deixa as duas acesas, que é a
       degradação certa: a seta aparece prometendo um passo que a ponta recusa,
       e a recusa já é o campo grudando. */
  body.foco-sem-ponteiro #hordeCampanhaFaseLinha:focus-within {
    --cor-seta-anterior: var(--cor-realce-foco);
    --cor-seta-seguinte: var(--cor-realce-foco);
  }
  body.foco-sem-ponteiro #hordeCampanhaFaseLinha:focus-within:has(option:first-child:checked) {
    --cor-seta-anterior: var(--cor-superficie-hover);
  }
  body.foco-sem-ponteiro #hordeCampanhaFaseLinha:focus-within:has(option:last-child:checked) {
    --cor-seta-seguinte: var(--cor-superficie-hover);
  }
  body.foco-sem-ponteiro #hordeCampanhaFaseLinha:focus-within::before,
  body.foco-sem-ponteiro #hordeCampanhaFaseLinha:focus-within::after {
    content: '';
    position: absolute;
    top: 50%;
    /* 6x8 é a caixa da seta; o `-4px` a centra na altura do campo sem depender
       da altura dele. */
    width: 6px;
    height: 8px;
    margin-top: -4px;
    /* As três colunas de 2px caem em `left`/`center`/`right` da própria caixa,
       então a escadinha não tem número mágico nenhum. */
    background-position: left center, center center, right center;
    background-repeat: no-repeat;
    /* Ela é decoração: não pode roubar o clique do campo que ela explica. */
    pointer-events: none;
  }
  /* A de trás aponta para a esquerda: a ponta (2px) fica na coluna de fora. */
  body.foco-sem-ponteiro #hordeCampanhaFaseLinha:focus-within::before {
    right: 100%;
    margin-right: 6px;
    background-image:
      linear-gradient(var(--cor-seta-anterior) 0 0),
      linear-gradient(var(--cor-seta-anterior) 0 0),
      linear-gradient(var(--cor-seta-anterior) 0 0);
    background-size: 2px 2px, 2px 4px, 2px 8px;
  }
  body.foco-sem-ponteiro #hordeCampanhaFaseLinha:focus-within::after {
    left: 100%;
    margin-left: 6px;
    background-image:
      linear-gradient(var(--cor-seta-seguinte) 0 0),
      linear-gradient(var(--cor-seta-seguinte) 0 0),
      linear-gradient(var(--cor-seta-seguinte) 0 0);
    background-size: 2px 8px, 2px 4px, 2px 2px;
  }

  /* ----- Nó da árvore de talentos: o realce muda de propriedade -----
     O nó tem 38px e o miolo dele é ícone de 18px **mais** o contador de rank —
     medido, o "0/3" fica a 4px da borda de baixo e a faixa de 4..6px passava
     por cima dele. Ele também não pode receber o anel por fora pelo `outline`:
     essa propriedade já é do **fecho** (o tracejado dourado de `.talent-no-fecho`,
     que marca a exclusividade entre as folhas), e o realce do sistema a
     sobrescrevia — focar um nó de fecho apagava a marca que explica por que ele
     é exclusivo.

     Então aqui o realce é `box-shadow`, que é a terceira propriedade do nó — a
     mesma que `.talent-no-selecionado` usa, e pela mesma razão: a silhueta em
     escadinha é **desenhada** em camadas de `background-image` em vez de
     recortada por `clip-path` (ver `.talent-no`), e sem recorte o que é pintado
     fora da caixa sobrevive. Os dois anéis convivem aninhados: o branco do foco
     a 0..2px para fora, o tracejado do fecho a 2..3px. */
  body.foco-sem-ponteiro .talent-no:focus {
    outline: none;
    box-shadow: 0 0 0 var(--espessura-realce-foco) var(--cor-realce-foco);
  }
  /* O `outline: none` acima é do realce, não do fecho: o tracejado volta a ser
     declarado aqui para o nó de fecho em foco continuar dizendo que é fecho. */
  body.foco-sem-ponteiro .talent-no-fecho:focus {
    outline: 1px dashed var(--cor-ouro);
    outline-offset: 2px;
  }
