  /* ===== 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;
    /* Largura da barra lateral da tela inicial (#menuSidebar). É variável e
       não valor fixo porque duas regras diferentes precisam do *mesmo* número:
       a largura da própria barra e o `padding-left` de #menu, que é o que
       impede qualquer painel do menu de passar por baixo dela. Em tela pequena
       a barra vira só-ícone e o valor encolhe junto (ver body.touch-mode/
       body.tela-estreita mais abaixo) — as duas regras acompanham sozinhas. */
    --largura-sidebar: 188px;
    /* 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);
    --cor-veu-endgame: rgba(0, 0, 0, 0.2);
    /* 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-forte-hover: #222;
    --cor-superficie-elevada: #3a3a3a;
    --cor-superficie-hover: #3f3f3f;
    --cor-superficie-hover-alta: #4a4a4a;
    --cor-modal-fundo: #100f0e;
    /* Barra lateral da tela inicial: fundo quase preto e botões um degrau
       acima dele. São tons próprios, e não os `--cor-botao-flutuante-*` dos
       controles do canto direito, porque aqui a peça não flutua sobre a cena —
       é uma coluna sólida, e os botões precisam se destacar *dentro* dela em
       vez de sobre o fundo do jogo. */
    --cor-sidebar-fundo: #050505;
    --cor-sidebar-botao: #121010;
    --cor-sidebar-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;
    /* Mesma pedra puxada para o quente: fundo do cartão do jogador local no
       HUD da Defesa da base, para ele se separar dos companheiros. */
    --cor-painel-pedra-veu-local: #1d1710;
    --cor-pedra-borda: #100f0e;

    /* 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;
    --cor-ouro-suave: #f4c542;
    --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;
    --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);
    /* Texto por cima de uma cor de tier: as quatro 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);

    /* 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;
    --cor-hud-escudo-texto: #2885c7;
    /* 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;
    /* 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). */
    --cor-botao-flutuante-fundo: rgba(58, 58, 58, 0.85);
    --cor-botao-flutuante-fundo-hover: rgba(78, 78, 78, 0.92);

    /* ===== 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
    );
    /* 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);
    /* 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);
    /* 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);
    /* 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;
    }
  }
  /* ===== 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);
  }
  /* ===== 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 a conversão de coordenada em dropdownPosition.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;
  }
  /* Assinatura de abertura (public/js/splash.js): cobre a tela por 2s antes
     de qualquer outra coisa, sobre um fundo liso mais escuro que o do menu
     (--cor-sidebar-fundo, o mais escuro da paleta) — sem textura/paralaxe, a
     assinatura vem antes até do menu carregar. `position: fixed` porque
     nasce como primeiro filho do body, antes do `display: flex` dele
     centralizar qualquer coisa. */
  #splashScreen {
    position: fixed;
    inset: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--cor-sidebar-fundo);
  }
  #splashLogo {
    max-width: min(60vw, 420px);
    max-height: min(60vh, 420px);
    image-rendering: pixelated;
    animation: splash-entra 0.3s steps(3, end) 1;
  }
  /* 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; }
  }
  /* Enquanto a assinatura está na tela, os botões flutuantes de som e de
     música não aparecem: 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 ~ #soundSettings,
  #splashScreen ~ #musicPlayer {
    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;
  }
  /* Hover "levanta" o botão com leve escala/brilho/sombra — exceto os que já
     têm semântica própria de hover (link de texto sublinhado, item de lista
     de classe/ranking/sala/inimigo/item, cujo destaque é a borda/fundo da linha
     inteira).

     A lista de exceções não é preferência estética: um `scale` num elemento
     largo o faz **transbordar** o contêiner (1,03 de 794px são 23px a mais, 11
     para cada lado). Se esse contêiner rola — como a lista da tela de escolha de
     item —, nasce uma barra de rolagem, ela consome altura e o painel inteiro
     muda de tamanho a cada vez que o cursor cruza uma linha. Toda linha larga
     dentro de área rolável precisa entrar aqui.

     O *movimento* do hover continua interpolado mesmo nos botões de ação, que
     têm moldura em pixel art (ver o fim do arquivo): a versão em "bloco de
     pedra", com o estado inteiro em degrau e sombra sólida em cada elemento
     clicável, carregava demais a tela. A sombra desfocada daqui não aparece
     nesses botões — o clip-path da moldura recorta o que é pintado fora da
     silhueta —, mas segue valendo para os botões de ícone e flutuantes. */
  button:not(:disabled):not(.link-button):not(.class-card):not(.ranking-nome):not(.password-toggle-btn):not(.horde-level-item):not(.horde-room-row):not(.monstruario-row):not(.item-choice-botao):hover {
    transform: translateY(-2px) scale(1.03);
    filter: brightness(1.08);
    box-shadow: var(--sombra-flutuante);
  }
  button:not(:disabled):not(.link-button):not(.class-card):not(.ranking-nome):not(.password-toggle-btn):not(.horde-level-item):not(.horde-room-row):not(.monstruario-row):not(.item-choice-botao):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 é exclusiva do nome do jogo (#menuTitle), 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;
  }
  #botClassOverlay.closing,
  .class-picker-overlay.closing,
  #authOverlay.visible.closing,
  #profileOverlay.visible.closing,
  #pauseOverlay.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 (mesmo `position: fixed` de #soundSettings/#musicPlayer,
     espelhado no canto esquerdo). Abre a modal de pausa, ver pauseMenu.js. */
  #pauseMenuBtn {
    position: fixed;
    top: 16px;
    left: 16px;
    z-index: 5;
  }
  /* ----- Controle de volume -----
     Fixo no canto da tela porque vale tanto para o menu quanto para uma
     partida em andamento — o jogador não deveria ter que voltar ao menu para
     mudar o volume no meio de um jogo. z-index alto (acima de HUD, overlays
     de fim de jogo/espera e modais, todos ≤20) porque esses dois botões
     precisam continuar clicáveis por cima de qualquer tela do jogo; abaixo
     só de #fullscreenGate (900), que bloqueia a tela de propósito. */
  #soundSettings {
    position: fixed;
    top: 16px;
    right: 16px;
    z-index: 800;
  }
  /* Fica por cima do player de música (abaixo dele na pilha) enquanto o
     painel está aberto, senão o botão de música apareceria por cima do
     painel de efeitos, que abre para baixo bem onde ele está. */
  #soundSettings.open {
    z-index: 801;
  }
  /* Botões quadrados de canto arredondados (não circulares) e 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 (não
     um botão dentro de um wrapper como #soundSettings/#musicPlayer), mas
     tem exatamente o mesmo formato. Os botões de navegação da tela inicial
     (#menuNav, ver menuViews.js) também: são a mesma peça no mesmo canto da
     janela, e é de propósito que se pareçam — só a largura fixa é
     sobrescrita lá embaixo, porque eles levam rótulo ao lado do ícone. */
  #soundSettings button,
  #musicPlayer button,
  #menuNav button,
  #pauseMenuBtn,
  #pauseBody .volume-control button {
    width: 40px;
    height: 40px;
    border-radius: 10px;
    border: 1px solid var(--cor-borda);
    background: var(--cor-botao-flutuante-fundo);
    color: var(--cor-texto);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    backdrop-filter: blur(6px);
  }
  #soundSettings button:hover,
  #musicPlayer button:hover,
  #menuNav button:hover,
  #pauseMenuBtn:hover,
  #pauseBody .volume-control button:hover {
    background: var(--cor-botao-flutuante-fundo-hover);
  }
  /* 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;
  }
  /* O contrário dele: os dois controles de áudio só existem **fora** de
     partida. Dentro dela eles ficavam por cima da arena, e o que eles fazem
     mudou de lugar — os mesmos dois volumes estão na modal de pausa, que é
     onde o jogador já vai parar para mexer em qualquer coisa (ver
     pauseMenu.js). */
  body.game-active #soundSettings,
  body.game-active #musicPlayer {
    display: none;
  }
  #soundSettingsMute {
    display: none;
  }
  #soundSettings.muted #soundSettingsWave1,
  #soundSettings.muted #soundSettingsWave2 {
    display: none;
  }
  #soundSettings.muted #soundSettingsMute {
    display: block;
  }
  #soundSettingsPanel {
    display: none;
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    min-width: 200px;
    padding: 14px 16px;
    box-sizing: border-box;
    background: rgba(29, 29, 29, 0.92);
    border: 1px solid var(--cor-borda);
    border-radius: 0;
    box-shadow: var(--sombra-flutuante);
    backdrop-filter: blur(6px);
  }
  #soundSettings.open #soundSettingsPanel {
    display: block;
    animation: entrada-pop 0.15s ease;
    transform-origin: top right;
  }
  /* Precisa ser tão específico quanto ".open ... " acima (2 ids), senão a
     animação de entrada continua ganhando da de saída na cascata. */
  #soundSettings.open #soundSettingsPanel.closing {
    animation: saida-pop 0.15s ease forwards;
  }
  .sound-settings-row {
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 13px;
  }
  .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;
  }
  /* Precisa ser tão específico quanto "#soundSettings button"/"#musicPlayer
     button" acima (1 id), senão o tamanho de 40px continua ganhando na
     cascata mesmo com este seletor vindo depois. O da modal de pausa aparece
     nas duas listas de propósito: é lá em cima que ele ganha borda, fundo e
     centralização (é a mesma peça), e aqui que encolhe. */
  #soundSettingsPanel .volume-control button,
  #musicPanel .volume-control button,
  #pauseBody .volume-control button {
    flex: none;
    width: 28px;
    height: 28px;
    border-radius: 8px;
  }
  /* 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;
  }
  /* ----- Player de música -----
     Empilhado embaixo de #soundSettings, no canto da tela: os dois
     controlam áudio e fazem sentido agrupados na mesma coluna. */
  #musicPlayer {
    position: fixed;
    top: 64px;
    right: 16px;
    z-index: 800;
  }
  #btnMusicToggle.playing {
    color: var(--cor-vermelho-destaque);
    border-color: var(--cor-vermelho-destaque);
  }
  #musicPanel {
    display: none;
    position: absolute;
    /* âncora para #musicCopiedMsg, que é posicionado relativo ao painel */
    top: calc(100% + 8px);
    right: 0;
    min-width: 240px;
    padding: 14px 16px;
    box-sizing: border-box;
    background: rgba(29, 29, 29, 0.92);
    border: 1px solid var(--cor-borda);
    border-radius: 0;
    box-shadow: var(--sombra-flutuante);
    backdrop-filter: blur(6px);
  }
  #musicPlayer.open #musicPanel {
    display: block;
    animation: entrada-pop 0.15s ease;
    transform-origin: top right;
  }
  #musicPlayer.open #musicPanel.closing {
    animation: saida-pop 0.15s ease forwards;
  }
  #musicTrackName {
    font-size: 13px;
    color: var(--cor-texto-suave);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-bottom: 10px;
    padding: 4px 6px;
    margin-left: -6px;
    margin-right: -6px;
    border-radius: 0;
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
  }
  #musicTrackName:hover {
    background: var(--cor-superficie-hover);
    color: var(--cor-texto);
  }
  #musicCopiedMsg {
    position: absolute;
    top: -28px;
    left: 50%;
    transform: translateX(-50%) translateY(4px);
    font-size: 12px;
    color: var(--cor-sucesso);
    background: rgba(29, 29, 29, 0.92);
    border: 1px solid var(--cor-borda);
    border-radius: 0;
    padding: 4px 10px;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease, transform 0.15s ease;
  }
  #musicCopiedMsg.visible {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
  #musicControls {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-bottom: 12px;
  }
  #musicControls button {
    width: 34px;
    height: 34px;
  }
  #btnMusicPlayPause {
    width: 40px;
    height: 40px;
  }
  #musicPlayIcon {
    display: none;
  }
  #btnMusicPlayPause.is-paused #musicPauseIcon {
    display: none;
  }
  #btnMusicPlayPause.is-paused #musicPlayIcon {
    display: block;
  }
  /* Tela inicial: seis visões que ocupam a tela inteira, uma por vez
     (início = logo + índice dos modos; Arena PVP = opções do 1x1; Defesa da
     base = salas + talentos; Monstruário = ficha dos inimigos da horda;
     Relicário = catálogo dos itens dela; ranking = Top 10 + partidas ao vivo),
     trocadas pelos botões de #menuNav — 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 lateral é `position: fixed` e não empurraria nada sozinha: é
       este `padding-left` que reserva a faixa dela e mantém todas as visões
       do menu inteiras à direita da barra, em vez de passarem por baixo. */
    padding-left: var(--largura-sidebar);
  }
  /* A visão inativa sai do fluxo com `display: none`, e não com opacidade ou
     posição fora da tela: é isso que faz os polls do ranking e das partidas ao
     vivo pararem sozinhos (IntersectionObserver nos próprios painéis, ver
     ranking.js/liveMatches.js) sem nenhuma checagem a mais aqui. */
  .menu-view {
    display: none;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    padding: 16px 64px 16px 12px;
    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;
  }
  /* Nas duas visões o conteúdo fica lado a lado, como quando cada metade da
     tela era um item do carrossel: logo à esquerda e opções de jogo à direita
     no início; Top 10 e partidas ao vivo na de ranking. Cada um centralizado
     na vertical (o `align-items: center` de .menu-view). Em tela estreita não
     há largura para dois, e aí os dois empilham — ver a faixa
     `tela-estreita` abaixo. */
  #menuViewHome {
    gap: clamp(12vw, calc(0.035 * var(--largura-tela)), 110px);
  }
  .menu-view > * {
    max-width: 100%;
  }
  /* Barra lateral da tela inicial: coluna fixa na borda esquerda com o botão
     de conta no topo e, embaixo dele, os botões que trocam a visão de #menu.
     Ao contrário dos controles flutuantes do canto direito (som/música), esta
     barra **não** fica por cima do conteúdo: #menu reserva `--largura-sidebar`
     em `padding-left`, então nenhum painel do menu entra embaixo dela. Só
     existe fora de partida — a visibilidade depende só do modo (menu ×
     partida), nunca de estado assíncrono. */
  #menuSidebar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 15;
    width: var(--largura-sidebar);
    height: var(--altura-tela);
    box-sizing: border-box;
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    background: var(--cor-sidebar-fundo);
    border-right: 1px solid var(--cor-borda);
    /* Rede de segurança das telas baixas (paisagem de celular): com sete
       botões de 40px a coluna pode não caber em pé, e aí ela rola por dentro
       de si mesma em vez de cortar o último botão. */
    overflow-y: auto;
  }
  body.game-active #menuSidebar {
    display: none;
  }
  /* Os botões ficam empilhados dentro da barra, do mesmo material dos botões
     de som/música (ver a regra compartilhada de #soundSettings/#musicPlayer);
     quem posiciona a coluna é #menuSidebar. */
  #menuNav {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  /* Ícone + nome da visão, em vez do quadrado só-ícone da regra compartilhada
     acima: três escudos/barras no canto não dizem para onde levam. A altura
     continua a mesma (40px) — a largura é a da barra inteira, para os botões
     lerem como uma lista alinhada e não como sete pílulas de tamanhos
     diferentes. */
  #menuNav button.menu-view-btn {
    background: var(--cor-sidebar-botao);
    width: 100%;
    justify-content: flex-start;
    gap: 8px;
    padding: 0 12px;
    box-sizing: border-box;
    flex-shrink: 0;
  }
  #menuNav button.menu-view-btn:hover {
    background: var(--cor-sidebar-botao-hover);
  }
  /* "Monstruário" e "Relicário" não são modos: são as duas leituras *sobre* a
     Defesa da base (os inimigos dela e os itens dela). Aparecem como sub-itens
     do botão daquele modo — recuados e ligados a ele por um fio vertical, o
     mesmo desenho de uma árvore de pastas. Sem isso a barra lê como seis
     destinos irmãos, e "Relicário" fica tão no mesmo nível de "Arena PVP"
     quanto do modo a que ele de fato pertence. */
  .menu-nav-sub {
    display: flex;
    flex-direction: column;
    /* O mesmo vão de #menuNav: o recuo é o que separa o grupo, não um respiro
       diferente, que faria os dois lerem como outra seção em vez de filhos do
       botão acima. */
    gap: 8px;
    position: relative;
    margin-left: 8px;
    padding-left: 14px;
    /* O fio sobe pelo vão que separa o grupo do botão do modo (o `padding` de
       cima é anulado pela margem negativa, então nada se move: o que muda é só
       até onde o fio desenha), senão ele começaria solto no ar em vez de sair
       de "Defesa da base". */
    margin-top: -8px;
    padding-top: 8px;
  }
  /* O tronco vertical é um pseudo-elemento, e não o `border-left` do grupo:
     borda vai obrigatoriamente até a última linha do elemento, e sobrava um
     toco descendo abaixo do ramo do último sub-item — um fio apontando para
     um item que não existe. Aqui ele para exatamente onde o último ramo sai
     (`bottom`: os 19px que vão do fim do botão até a base do ramo dele). */
  .menu-nav-sub::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 19px;
    width: 2px;
    background: var(--cor-borda);
  }
  /* O ramo horizontal que sai do fio vertical e encosta em cada sub-item. É o
     que fecha o desenho de árvore: só a linha vertical ao lado já bastava com
     rótulo e recuo, mas na barra só-ícone (onde os dois somem) ela sozinha
     lia como uma borda decorativa da barra. Valores inteiros e fundo sólido —
     nada de `transform` para centralizar: 2px no meio de 40px é 19px exatos. */
  .menu-view-btn-sub {
    position: relative;
  }
  .menu-view-btn-sub::before {
    content: '';
    position: absolute;
    left: -12px;
    width: 12px;
    top: 19px;
    height: 2px;
    background: var(--cor-borda);
  }
  /* Um degrau abaixo do rótulo do pai (13px), pelo mesmo motivo do recuo. */
  .menu-view-btn-sub .menu-view-btn-label {
    font-size: 12px;
    color: var(--cor-texto-fraco);
  }
  /* O ativo continua vermelho como qualquer outra visão: a cor diz "você está
     aqui" e não pode ser comida pelo cinza de sub-item. */
  #menuNav button.menu-view-btn-sub.menu-view-btn-ativo .menu-view-btn-label {
    color: inherit;
  }
  #menuNav button.menu-view-btn svg {
    flex-shrink: 0;
  }
  .menu-view-btn-label {
    font-size: 13px;
    white-space: nowrap;
  }
  /* Selo de pontos de talento por gastar, no botão que leva à árvore (ver
     public/js/talentPointsAviso.js). Só existe com `data-pontos-livres`: o
     seletor é o próprio atributo, então zero ponto não tem regra a desligar —
     o selo simplesmente não é desenhado.

     O número vem de `attr()` num `::after` absoluto, e não de um elemento
     dentro do botão: assim ele não entra no fluxo e o rótulo "Defesa da base"
     não anda um pixel quando o selo aparece — inclusive no modo só-ícone, em
     que o botão vira um quadrado de 40px 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. */
  #menuNav button.menu-view-btn[data-pontos-livres] {
    position: relative;
    overflow: visible;
  }
  #menuNav button.menu-view-btn[data-pontos-livres]::after {
    content: attr(data-pontos-livres);
    position: absolute;
    top: -4px;
    right: -4px;
    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);
    box-shadow: 2px 2px 0 var(--cor-pedra-borda);
    /* 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: talento-selo-pisca 0.9s steps(1, end) infinite alternate;
  }
  @keyframes talento-selo-pisca {
    0% { background: var(--cor-ouro); }
    100% { background: var(--cor-branco); }
  }
  /* Só-ícone em aparelho touch (qualquer um, de celular a tablet), em janela de
     desktop estreita e em tela deitada e curta: nesses tamanhos a coluna de
     rótulos come a largura do conteúdo do menu, que aí já ocupa a tela toda —
     e numa tela baixa os sete botões com rótulo ainda ocupariam a altura
     inteira da barra. `body.touch-mode` sozinho, sem faixa de tamanho junto,
     porque o critério é o aparelho e não a viewport — girado por CSS um celular
     reporta largura folgada (ver orientation.js) e nenhuma faixa de "tela
     pequena" o pegaria.

     Os dois botões voltam ao quadrado de 40px da regra compartilhada de
     #soundSettings/#musicPlayer — a mesma peça do canto direito, aqui num
     #menuSidebar que encolhe junto. O nome não se perde: cada botão já leva
     `aria-label` e `title`. */
  body.touch-mode .menu-view-btn-label,
  body.touch-mode #accountFabLabel,
  body.tela-estreita .menu-view-btn-label,
  body.tela-estreita #accountFabLabel,
  body.tela-baixa-deitada .menu-view-btn-label,
  body.tela-baixa-deitada #accountFabLabel {
    display: none;
  }
  body.touch-mode,
  body.tela-estreita,
  body.tela-baixa-deitada {
    /* 40px de botão + os 12px de `padding` de cada lado da barra: a coluna
       encolhe para caber exatamente o quadrado do ícone, e o `padding-left`
       de #menu devolve ao conteúdo a largura que os rótulos ocupavam. */
    --largura-sidebar: 64px;
  }
  body.touch-mode #menuNav button.menu-view-btn,
  body.tela-estreita #menuNav button.menu-view-btn,
  body.tela-baixa-deitada #menuNav button.menu-view-btn {
    width: 100%;
    justify-content: center;
    gap: 0;
    padding: 0;
  }
  /* Só-ícone: recuar os dois sub-itens deixaria eles mais estreitos que os
     botões de cima, e uma coluna de quadrados com dois retângulos no meio lê
     como defeito, não como hierarquia. O fio vertical continua — o que muda é
     que ele corre pela beirada esquerda da *coluna dos botões* (e não fora
     dela): sem o recuo, o tronco cai exatamente sob a borda esquerda do botão
     do modo acima, que é o que faz a árvore ler como descendo dele. Os 8px de
     `padding` são o degrau que o quadrado menor (32px dentro de 40px) já
     deixava à esquerda, então nada muda de tamanho. */
  body.touch-mode .menu-nav-sub,
  body.tela-estreita .menu-nav-sub,
  body.tela-baixa-deitada .menu-nav-sub {
    margin-left: 0;
    padding-left: 8px;
  }
  body.touch-mode .menu-nav-sub::before,
  body.tela-estreita .menu-nav-sub::before,
  body.tela-baixa-deitada .menu-nav-sub::before {
    /* O último sub-item é 32px em vez de 40px, então o fio para 15px acima do
       fim do grupo — de novo, na base do último ramo. */
    bottom: 15px;
    /* Sem rótulo nem recuo, o fio é boa parte do que diz "estes dois são do
       modo acima" — e um cinza de 2px encostado na beirada da barra quase não
       aparece sobre o fundo quase preto dela. */
    background: var(--cor-borda-clara);
  }
  /* O que sobrou de hierarquia sem rótulo e sem recuo era um fio de 2px na
     beirada — perto de nada. Aqui os dois sub-itens viram um quadrado *menor*
     (32px), encostado na borda direita da barra: as bordas direitas alinham
     com as do pai e sobra um degrau à esquerda, que é o recuo do modo com
     rótulo traduzido para uma coluna estreita. Continuam quadrados, então a
     coluna não ganha retângulo torto no meio. */
  body.touch-mode #menuNav button.menu-view-btn-sub,
  body.tela-estreita #menuNav button.menu-view-btn-sub,
  body.tela-baixa-deitada #menuNav button.menu-view-btn-sub {
    width: 32px;
    height: 32px;
    margin-left: auto;
  }
  body.touch-mode .menu-view-btn-sub::before,
  body.tela-estreita .menu-view-btn-sub::before,
  body.tela-baixa-deitada .menu-view-btn-sub::before {
    /* Os 8px que o quadrado menor (32px) deixou à esquerda dentro da coluna
       de 40px; e o meio de 32px são 15px exatos. */
    left: -8px;
    width: 8px;
    top: 15px;
    background: var(--cor-borda-clara);
  }
  body.touch-mode #menuNav button.menu-view-btn-sub svg,
  body.tela-estreita #menuNav button.menu-view-btn-sub svg,
  body.tela-baixa-deitada #menuNav button.menu-view-btn-sub svg {
    width: 16px;
    height: 16px;
  }
  body.touch-mode #accountFabBtn,
  body.tela-estreita #accountFabBtn,
  body.tela-baixa-deitada #accountFabBtn {
    width: 100%;
    height: 40px;
    justify-content: center;
    gap: 0;
    padding: 0;
  }
  /* A visão em que o jogador está é marcada pela cor da identidade, não por
     um fundo diferente: são dois ícones no canto, e um bloco preenchido ali
     competiria com o logo. */
  #menuNav button.menu-view-btn-ativo {
    border-color: var(--cor-vermelho-destaque);
    color: var(--cor-vermelho-destaque);
  }
  #menuTitle {
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    /* Cresce com a viewport até o teto de 116px — o item do título tem só
       50% da largura da tela (100% no mobile, que sobrescreve isso mais
       abaixo), então um valor fixo ou estoura em telas estreitas ou fica
       pequeno demais nas largas. */
    font-size: clamp(44px, calc(0.08 * var(--largura-tela)), 168px);
    line-height: 0.92;
    text-align: center;
    color: var(--cor-vermelho);
    -webkit-text-stroke: 1px #000;
    text-stroke: 1px #000;
    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-vermelho);
      text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 0 8px var(--cor-vermelho);
    }
    50% {
      color: var(--cor-vermelho-destaque);
      text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 0 22px var(--cor-vermelho-destaque);
    }
  }
  #rankingPanel {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 300px;
    height: 100%;
    max-width: 100%;
    max-height: min(500px, calc(var(--altura-tela) - 96px));
    box-sizing: border-box;
    padding: 16px 24px 24px 16px;
    background: var(--cor-painel-pedra-veu);
    border-radius: 0;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
    /* Entra um instante depois de #menuBody, pra guiar o olho da esquerda pra
       direita em vez de tudo aparecer junto. */
    animation: entrada-fade 0.5s ease 0.12s both;
  }
  #rankingPanel h2 {
    margin: 0;
    font-size: 26px;
    text-align: center;
  }
  #rankingHeader {
    display: flex;
    justify-content: space-between;
    /* Alinha "Jogador" com o nome (e não com a posição) na linha de baixo. */
    padding-left: 26px;
    gap: 8px;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  #rankingList {
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  #rankingList li {
    display: grid;
    grid-template-columns: 18px 1fr auto;
    /* Centro, não baseline: o nome é um <button> (inline-block) com
       `overflow: hidden` para a reticência, e nesse caso a baseline do
       elemento passa a ser a borda de baixo da caixa — a posição e as
       vitórias alinhavam com o pé do nome, deixando as três colunas em
       alturas diferentes. */
    align-items: center;
    gap: 8px;
    font-size: 14px;
    /* Fade puro (sem translateY): o deslocamento de entrada-fade empurrava a
       última linha para fora da área visível por um instante, fazendo a
       lista (overflow-y: auto) mostrar uma barra de rolagem passageira. */
    animation: overlay-fade 0.35s ease both;
  }
  /* Stagger: cada linha entra um pouco depois da anterior. A lista é
     reescrita inteira a cada poll (ranking.js), então isso também dá uma
     pista visual sutil de "atualizou" a cada 30s. */
  #rankingList li:nth-child(1) { animation-delay: 0s; }
  #rankingList li:nth-child(2) { animation-delay: 0.04s; }
  #rankingList li:nth-child(3) { animation-delay: 0.08s; }
  #rankingList li:nth-child(4) { animation-delay: 0.12s; }
  #rankingList li:nth-child(5) { animation-delay: 0.16s; }
  #rankingList li:nth-child(6) { animation-delay: 0.2s; }
  #rankingList li:nth-child(7) { animation-delay: 0.24s; }
  #rankingList li:nth-child(8) { animation-delay: 0.28s; }
  #rankingList li:nth-child(n+9) { animation-delay: 0.32s; }
  #rankingList .ranking-posicao {
    color: var(--cor-texto-fraco);
    font-size: 12px;
    text-align: right;
  }
  /* 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 painel de
     ranking fica dentro de #menu, então precisa do prefixo #menu para vencer
     em especificidade a regra `#menu button:not(.class-card)` — mesmo truque
     usado em `#menu button.link-button`. */
  #menu #rankingList .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 #rankingList .ranking-nome:hover {
    background: none;
    text-decoration: underline;
  }
  #menu #rankingList .ranking-nome-proprio {
    color: var(--cor-ouro);
  }
  #rankingList .ranking-vitorias {
    color: var(--cor-azul-ranking);
    font-weight: bold;
    flex-shrink: 0;
  }
  /* Alvo de toque das linhas do ranking: no desktop a linha tem hit area de
     altura de texto (`align-items: baseline`); em modo touch a linha inteira
     vira o alvo (`align-items: stretch` + `min-height`/`display: flex` no
     nome, que é quem realmente responde ao clique) e o espaçamento entre
     linhas cresce, para não tocar a vizinha por engano. */
  body.touch-mode #rankingList {
    gap: 14px;
  }
  body.touch-mode #rankingList li {
    align-items: stretch;
    min-height: 22px;
  }
  body.touch-mode #menu #rankingList .ranking-nome {
    display: flex;
    align-items: center;
  }
  body.touch-mode #rankingList .ranking-posicao {
    display: flex;
    align-items: center;
    justify-content: flex-end;
  }
  body.touch-mode #rankingList .ranking-vitorias {
    display: flex;
    align-items: center;
  }
  /* Placar do Sem fim: mesma caixa de #rankingPanel (ver ranking.js), com a
     alternância solo/grupo no lugar do cabeçalho fixo de vitórias. */
  #hordeEndlessPanel {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 300px;
    max-width: 100%;
    height: 100%;
    max-height: min(500px, calc(var(--altura-tela) - 96px));
    box-sizing: border-box;
    padding: 16px 24px 24px 16px;
    background: var(--cor-painel-pedra-veu);
    border-radius: 0;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
    animation: entrada-fade 0.5s ease 0.16s both;
  }
  #hordeEndlessPanel h2 {
    margin: 0;
    font-size: 26px;
    text-align: center;
  }
  .horde-endless-tabs {
    display: flex;
    gap: 2px;
  }
  #menu .horde-endless-tabs button {
    flex: 1 1 0;
    padding: 3px 0;
    border: 1px solid var(--cor-pedra-borda);
    border-radius: 0;
    background: var(--cor-superficie);
    color: var(--cor-texto-fraco);
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    cursor: pointer;
  }
  #menu .horde-endless-tabs button.ativo {
    background: var(--cor-vermelho-destaque);
    color: var(--cor-texto);
    border-color: var(--cor-vermelho-destaque);
  }
  #hordeEndlessHeader {
    display: grid;
    grid-template-columns: 18px 1fr 48px 54px;
    gap: 8px;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  #hordeEndlessHeader span:nth-child(3),
  #hordeEndlessHeader span:nth-child(4) {
    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;
  }
  #hordeEndlessList li {
    display: grid;
    grid-template-columns: 18px 1fr 48px 54px;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    animation: overlay-fade 0.35s ease both;
  }
  #hordeEndlessList .ranking-posicao {
    color: var(--cor-texto-fraco);
    font-size: 12px;
    text-align: right;
  }
  #menu #hordeEndlessList .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 {
    background: none;
    text-decoration: underline;
  }
  #menu #hordeEndlessList .ranking-nome-proprio {
    color: var(--cor-ouro);
  }
  #hordeEndlessList .horde-endless-ondas {
    color: var(--cor-azul-ranking);
    font-weight: bold;
    text-align: right;
  }
  #hordeEndlessList .horde-endless-abates {
    color: var(--cor-texto-fraco);
    text-align: right;
  }
  /* "Partidas ao vivo" fica em #menuViewPvp. Continua por classe, e não por
     id, por uniformidade com o resto do painel (mesmo padrão de
     `#rankingPanel`/`#hordeEndlessPanel`). */
  .spectator-panel {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 360px;
    max-width: 100%;
    max-height: min(500px, calc(var(--altura-tela) - 96px));
    box-sizing: border-box;
    padding: 16px 24px 24px;
    background: var(--cor-painel-pedra-veu);
    border-radius: 0;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
    animation: entrada-fade 0.5s ease 0.2s both;
  }
  .spectator-panel h2 {
    margin: 0;
    font-size: 22px;
    text-align: center;
  }
  .live-matches-list {
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .live-matches-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    font-size: 14px;
    padding: 6px 0;
    animation: overlay-fade 0.35s ease both;
  }
  .live-matches-list li + li {
    border-top: 1px solid var(--cor-pedra-borda);
  }
  /* Nickname não tem limite de tamanho (convidado aceita qualquer caractere),
     então o par de nomes é cortado com reticência em vez de quebrar linha e
     empurrar o "Assistir" para baixo — o `min-width: 0` é o que permite o
     item flex encolher abaixo do conteúdo. */
  .live-match-players {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .live-match-classe {
    color: var(--cor-texto-fraco);
    font-size: 12px;
  }
  #menu button.link-button.live-match-watch {
    font-size: 14px;
    flex-shrink: 0;
    color: var(--cor-sucesso);
  }
  #menu button.link-button.live-match-watch:hover {
    color: var(--cor-sucesso-claro);
  }
  .ranking-vazio, .ranking-erro {
    margin: 0;
    font-size: 14px;
    text-align: center;
    color: var(--cor-texto-medio);
  }
  /* #menuBody (índice dos modos) e #pvpPanel (opções do 1x1) são a mesma peça:
     a coluna de botões no meio da visão. O painel da Arena PVP herda daqui em
     vez de ter cópia das mesmas medidas — a diferença entre os dois é só o
     conteúdo. */
  #menuBody,
  #pvpPanel {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* Mesmo motivo de .menu-view: numa janela baixa o painel rola por dentro,
       e centralizado o primeiro botão fica atrás do início do scroll, fora de
       alcance. */
    justify-content: safe center;
    gap: 4px;
    width: clamp(340px, calc(0.2 * var(--largura-tela)), 460px);
    max-width: 100%;
    max-height: min(500px, calc(var(--altura-tela) - 32px));
    overflow-y: auto;
    box-sizing: border-box;
    padding: 12px 24px;
    background: var(--cor-painel-pedra-veu);
    border-radius: 0;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
    animation: entrada-fade 0.5s ease both;
  }
  /* As exclusões de componente (`.horde-level-item`, `.toggle-switch`,
     `.horde-room-row`, `.monstruario-row`, `.talent-node`,
     `.horde-endless-tab`) existem porque
     #menuViewHorde e o placar do Sem fim trouxeram para dentro de #menu
     componentes que antes viviam numa modal fora dele
     (#hordeRoomsOverlay/#talentTreeOverlay) 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(.class-card):not(.link-button):not(.horde-level-item):not(.toggle-switch):not(.horde-room-row):not(.monstruario-row):not(.talent-node):not(.horde-endless-tab) {
    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(.class-card):not(.link-button):not(.horde-level-item):not(.toggle-switch):not(.horde-room-row):not(.monstruario-row):not(.talent-node):not(.horde-endless-tab):hover {
    background: var(--cor-vermelho-hover);
  }
  #pvpPanel h2 {
    margin: 0 0 4px;
    font-size: 26px;
    text-align: center;
  }
  #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;
  }
  /* 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;
  }
  #menu button#btnHowToPlay,
  #menu button#btnBot {
    background: var(--cor-erro-forte);
  }
  #menu button#btnHowToPlay:hover,
  #menu button#btnBot:hover {
    background: var(--cor-erro-forte-hover);
  }
  /* 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;
  }
  /* ===== HUD dividida nos 4 cantos da arena =====
     A tela de partida é a mesma em desktop e em modo touch: reservar altura de
     fluxo para o HUD acima da arena (como era antes só no desktop) sobrava
     menos espaço pra arena em si, e as duas telas liam como jogos diferentes.
     O HUD sai do fluxo (gameScale.js não reserva espaço nenhum pra ele: a
     arena usa a viewport quase inteira) e vira um grid cobrindo a arena
     inteira (`inset: 0`), com cada pedaço ancorado no canto correspondente:
     vida/escudo/cooldown no topo, nome+classe embaixo do mesmo lado (vida no
     topo é o que o jogador precisa checar num relance durante a troca de
     tiros; nome é só identificação, pode ficar mais longe do centro de
     atenção).

     `.hud-row`/`.hud-row-right` viram `display: contents`: a caixa deles some
     e os filhos (nome, vida, escudo, cooldown) passam a ser itens diretos do
     grid de `#hud`, cada um com seu próprio `grid-area` — sem duplicar
     HTML/JS por modo de input, só reposiciona os mesmos elementos que hud.js
     já preenche por id.

     `pointer-events: none` no `#hud` inteiro é essencial: em modo touch os
     grupos de baixo (`bl`/`br`) caem na metade inferior da arena, a mesma área
     onde os joysticks nascem dinamicamente (ver touchControls.js) — sem isso,
     tocar perto de um canto tocaria a HUD (que não tem nada clicável) em vez
     de iniciar o joystick por baixo dela. */
  #hud {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto auto auto 1fr auto;
    grid-template-areas:
      "tl-vida tr-vida"
      "tl-escudo tr-escudo"
      "tl-cd tr-cd"
      ".  ."
      "bl br";
    gap: 2px 0;
    width: auto;
    /* Afastado o bastante da borda vermelha da arena (8px) pra não parecer
       colado nela. */
    padding: 16px;
    /* Quatro manchas radiais translúcidas, uma por quina, em vez de uma
       faixa cobrindo a lateral inteira da arena — só dão contraste onde a
       HUD de fato está. `background` (não pseudo-elemento) porque camadas
       de background já pintam atrás de qualquer filho, sem precisar de
       `z-index` pra ganhar do conteúdo real. As de cima (`tl-vida`/`tr-vida`,
       onde ficam vida/escudo/cooldown) são a prioridade de legibilidade e
       por isso maiores; as de baixo (só o nome) ficam menores. */
    background:
      radial-gradient(ellipse at top left, rgba(0, 0, 0, 0.55), transparent 70%) top left / 24% 13% no-repeat,
      radial-gradient(ellipse at top right, rgba(0, 0, 0, 0.55), transparent 70%) top right / 24% 13% no-repeat,
      radial-gradient(ellipse at bottom left, rgba(0, 0, 0, 0.45), transparent 70%) bottom left / 16% 8% no-repeat,
      radial-gradient(ellipse at bottom right, rgba(0, 0, 0, 0.45), transparent 70%) bottom right / 16% 8% no-repeat;
    border: none;
    box-shadow: none;
    pointer-events: none;
    z-index: 10;
    font-size: 12px;
    /* #game-wrap (pai) alterna display none/flex a cada partida (menu.js,
       showGame), então essa animação replaya sozinha em toda partida nova. */
    animation: entrada-fade 0.4s ease;
  }
  #hud .hud-row {
    display: contents;
  }
  #hud .hud-row:not(.hud-row-right) .hearts { grid-area: tl-vida; justify-self: start; }
  #hud .hud-row:not(.hud-row-right) .shields { grid-area: tl-escudo; justify-self: start; }
  #hud .hud-row:not(.hud-row-right) .cooldown-bar { grid-area: tl-cd; justify-self: start; }
  #hud .hud-row:not(.hud-row-right) .hud-name-row { grid-area: bl; justify-self: start; }
  #hud .hud-row.hud-row-right .hearts { grid-area: tr-vida; justify-self: end; }
  #hud .hud-row.hud-row-right .shields { grid-area: tr-escudo; justify-self: end; }
  #hud .hud-row.hud-row-right .cooldown-bar { grid-area: tr-cd; justify-self: end; }
  #hud .hud-row.hud-row-right .hud-name-row { grid-area: br; justify-self: end; }
  /* As barras eram `width: 100%` do `.hud-row` — sem esse pai (`display:
     contents`), 100% resolveria pra largura da coluna do grid (metade da
     arena), larga demais pra um canto compacto. */
  #hud .hearts,
  #hud .shields,
  #hud .cooldown-bar {
    width: 34vmin;
    max-width: 210px;
  }
  /* Cronômetro centralizado sobre a arena. Posicionado absoluto porque as duas
     colunas do HUD têm larguras diferentes (nome/classe de cada jogador) — no
     fluxo normal ele ficaria fora do centro da arena. */
  #matchTimer {
    position: absolute;
    top: 14px;
    left: 50%;
    transform: translateX(-50%);
    padding: 4px 14px;
    border-radius: 0;
    background: var(--cor-painel-pedra-veu);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-pequena);
    color: var(--cor-texto);
    font-size: 22px;
    font-variant-numeric: tabular-nums;
    letter-spacing: 1px;
  }
  #matchTimer.warning {
    color: var(--cor-ouro-suave);
    border-color: var(--cor-ouro-suave);
  }
  #matchTimer.desempate {
    color: var(--cor-branco);
    background: var(--cor-erro-fundo);
    border-color: var(--cor-erro);
    font-size: 16px;
    letter-spacing: 0;
    animation: desempatePulse 0.8s ease-in-out infinite;
  }
  /* Só aparece no modo espectador (ver showSpectatorBanner em hud.js): deixa
     claro que ninguém dos dois lados é "você", já que o resto do HUD é
     idêntico ao de quem está jogando. Posicionado abaixo do cronômetro pelo
     mesmo motivo dele ser `absolute` — #hud é `flex` com só dois filhos no
     fluxo (as colunas dos jogadores), e um terceiro item no fluxo quebraria
     o `space-between`. */
  #spectatorBanner {
    display: none;
    position: absolute;
    top: 80px;
    left: 50%;
    transform: translateX(-50%);
    padding: 2px 12px;
    border-radius: 0;
    background: var(--cor-painel-pedra-veu);
    border: 1px solid var(--cor-ouro-suave);
    box-shadow: var(--sombra-pedra-pequena);
    color: var(--cor-ouro-suave);
    font-size: 13px;
    white-space: nowrap;
  }
  #spectatorBanner.visible {
    display: block;
  }
  @keyframes desempatePulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
  }
  .hud-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    /* Metade exata do HUD para cada jogador. Precisa ser uma largura
       *definida*, e não o tamanho do conteúdo: as barras de vida/escudo e a de
       cooldown têm largura relativa à coluna, então com a coluna dimensionada
       pelo conteúdo elas encolheriam até a largura do nome. */
    flex: 1 1 0;
    max-width: 50%;
    min-width: 0;
  }
  .hud-row-right {
    align-items: flex-end;
  }
  /* Ícone da classe, nome e classe numa única linha. O DOM das duas linhas do
     HUD tem a mesma ordem (ícone, nome, classe) e a do oponente é espelhada
     aqui, para não existirem duas marcações diferentes da mesma informação. */
  .hud-name-row {
    display: flex;
    align-items: center;
    gap: 6px;
    max-width: 100%;
    min-width: 0;
  }
  .hud-row-right .hud-name-row {
    flex-direction: row-reverse;
  }
  .hud-name {
    font-size: 14px;
    color: var(--cor-texto-suave);
    /* Numa linha só, um nickname longo empurraria a classe para fora da
       metade do HUD — então quem encurta é o nome, não a classe. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  #nameP0 {
    color: var(--cor-ouro);
  }
  /* Modo espectador: o slot 0 não é "você", então não leva o dourado que
     marca o próprio nome nos outros modos (ver showSpectatorBanner em
     hud.js). */
  body.spectating #nameP0 {
    color: var(--cor-texto-suave);
  }
  /* Na mesma linha do nome, então vem entre parênteses. Eles ficam no CSS e
     não no texto que `updateClassIcon` (hud.js) escreve, senão o elemento
     vazio (antes de a partida definir a classe) mostraria um "()" solto. */
  .hud-class-name {
    flex: none;
    font-size: 11px;
    color: var(--cor-azul-classe);
    opacity: 0.8;
  }
  .hud-class-name:not(:empty)::before {
    content: '(';
  }
  .hud-class-name:not(:empty)::after {
    content: ')';
  }
  /* Menor do que o quadro de 32px de antes: agora o ícone divide a linha com
     o nome e a classe, então ele acompanha a altura do texto em vez de ser o
     elemento mais alto da informação do jogador. */
  .hud-class-icon {
    flex: none;
    width: 20px;
    height: 20px;
    border-radius: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--cor-painel-pedra-veu);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-pequena);
    color: var(--cor-azul-classe);
  }
  .hud-class-icon svg {
    width: 14px;
    height: 14px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  /* Caixas das duas fileiras de recurso (`createResourceBar` em hud.js):
     mesma largura máxima nos dois casos, e é essa caixa que diferencia vida
     de escudo — cor e tamanho do ícone (ver "Fileiras de ícones" mais
     abaixo). O preview de classe da modal de seleção usa as mesmas duas
     classes, então herda o mesmo visual em miniatura. */
  .hearts,
  .shields {
    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;
  }
  /* No HUD o contador fica do lado *de fora* (embaixo do ícone de classe) e a
     fileira cresce para o meio da arena, como nos placares de luta: os
     números longe do centro deixam o cronômetro livre, e as duas fileiras
     esvaziam se afastando do meio. Por isso a linha direita inverte a ordem
     do par fileira/contador. */
  .hud-row .resource-bar {
    flex-direction: row-reverse;
  }
  .hud-row-right .resource-bar {
    flex-direction: row;
  }
  /* ===== Fileiras de ícones (vida e escudo) =====
     As duas são a mesma peça: 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`/`.shields`, com max-width em
     CSS); 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. Quem diferencia vida de escudo é só a caixa (cor, tamanho do
     pixel via HEART_PIXEL_VMIN/SHIELD_PIXEL_VMIN em hud.js). */
  .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;
  }
  .hud-row-right .resource-icons-row {
    /* Fileira encolhe a partir de baixa contagem de ícones — sem isso ela
       fica grudada no canto interno (perto do centro da arena) em vez de no
       canto de fora, onde o resto da coluna do jogador está ancorado. */
    justify-content: flex-end;
  }
  /* `.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);
  }
  .shields .resource-unit {
    color: var(--cor-hud-escudo-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 do duelista): corte seco na metade, sem
     transição — mesmo corte reto que o resto da pixel art usa. Não existe
     meia carga de escudo, então essa classe nunca é aplicada em `.shields`. */
  .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. */
  /* `:not(.explode):not(.blink)`: os dois são animações de `animation` no
     mesmo elemento, e esta regra tem especificidade maior — sem a exclusão, o
     coração perdido durante a vida baixa deixaria de explodir justamente nos
     últimos hits da partida. */
  .resource-bar.vida-baixa .resource-unit:not(.explode):not(.blink),
  .resource-bar.vida-baixa .resource-count {
    animation: vidaBaixaPulso 0.6s steps(1, end) infinite;
  }
  @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" (vida) ou dois dígitos (escudo com bônus de
     power-up), e sem uma largura reservada pro pior caso o container inteiro
     (`.hearts`/`.shields`) 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.
     Vida e escudo usam a **mesma** largura (em `vmin`, não `ch`): `ch`
     resolve pelo próprio `font-size` de cada `.resource-count`, que é
     diferente entre os dois (ver abaixo) — larguras diferentes deslocavam a
     fileira de ícones de cada um pra um ponto de partida diferente, os dois
     não começavam alinhados na mesma coluna. */
  .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/
     SHIELD_PIXEL_VMIN em hud.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 (mesmo padrão do `max-width: 210px` de `.hearts`/`.shields`
     acima), 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;
  }
  .shields .resource-count {
    font-size: clamp(7px, 2.5vmin, 11px);
    color: var(--cor-hud-escudo-texto);
    font-weight: 700;
    -webkit-text-stroke: 0.6px currentColor;
  }
  /* Ícone pixel-art do contador (coração/escudo), 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);
  }
  #canvas-wrap {
    position: relative;
    width: 100%;
  }
  /* Fora do fluxo (senão #game-wrap ficaria mais alto que a arena, e o `#hud`
     — `inset: 0` nele — desalinharia dos cantos de verdade da arena). Some em
     modo touch (ver `body.touch-mode #escHint` mais abaixo, sem teclado). */
  #escHint {
    position: absolute;
    left: 50%;
    bottom: 12px;
    transform: translateX(-50%);
    font-size: 12px;
    color: var(--cor-borda-hover);
    pointer-events: none;
    z-index: 10;
  }
  /* Fica na faixa de cima da arena, mas bem afastado da borda e perto do
     centro: é o texto mais importante da primeira partida e precisa ser lido
     sem tirar os olhos da ação. Não vai no centro exato porque os dois
     jogadores nascem na metade vertical da arena e ficariam cobertos.
     O `top` começa abaixo de #matchTimer nos dois modos: desde que o HUD
     passou a cobrir a arena (`#hud`, `inset: 0`, z-index acima do canvas), um
     valor colado no topo põe o balão atrás do cronômetro. */
  #matchTutorialBanner {
    position: absolute;
    top: 13%;
    left: 50%;
    transform: translateX(-50%);
    max-width: 82%;
    padding: 16px 28px;
    background: rgba(20, 20, 20, 0.9);
    border: 1px solid var(--cor-borda-media);
    border-radius: 0;
    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;
    transition: opacity 0.25s ease, background-color 0.2s ease, border-color 0.2s ease;
  }
  #matchTutorialBanner.visible {
    opacity: 1;
  }
  #matchTutorialBanner.success {
    background: rgba(45, 138, 86, 0.9);
    border-color: var(--cor-sucesso);
  }
  /* Teclas desenhadas no balão do tutorial: cada passo mostra exatamente as
     teclas que o jogador precisa apertar. Cinza = ainda não apertada,
     vermelho (`.done`) = já apertada. */
  .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;
  }
  .tutorial-tecla {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    height: 40px;
    padding: 0 10px;
    border: 2px solid var(--cor-borda-media);
    border-radius: 0;
    background: var(--cor-superficie);
    color: var(--cor-texto-fraco);
    font-size: 18px;
    font-weight: 700;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
  }
  .tutorial-tecla.larga {
    min-width: 180px;
    font-size: 16px;
  }
  .tutorial-tecla.done {
    border-color: var(--cor-vermelho-destaque);
    color: var(--cor-vermelho-destaque);
    background: var(--cor-superficie-alta);
    transform: translateY(2px);
  }
  /* Tecla de segurar (Espaço, no passo de escudo): a barra preenche por
     dentro conforme o jogador segura, em vez de só acender ao apertar. */
  .tutorial-tecla-hold {
    position: relative;
    overflow: hidden;
  }
  .tutorial-tecla-hold-fill {
    position: absolute;
    inset: 0;
    width: 0%;
    background: var(--cor-vermelho-destaque);
    opacity: 0.45;
    transition: width 0.05s linear;
  }
  .tutorial-tecla-hold-label {
    position: relative;
  }
  /* Dá evidência ao nome da tecla dentro do texto do passo. */
  .tutorial-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. */
  body.touch-mode #matchTutorialBanner {
    max-width: 92%;
    padding: 8px 14px;
    font-size: clamp(11px, 3vmin, 14px);
    line-height: 1.25;
  }
  body.touch-mode .tutorial-teclas {
    gap: 4px;
    margin-top: 6px;
  }
  body.touch-mode .tutorial-teclas-linha {
    gap: 4px;
  }
  body.touch-mode .tutorial-tecla {
    min-width: 24px;
    height: 24px;
    padding: 0 6px;
    border-width: 1px;
    font-size: 13px;
  }
  body.touch-mode .tutorial-tecla.larga {
    min-width: 90px;
    font-size: 11px;
  }
  /* Anel pulsando em volta do cursor no passo de atirar: induz o clique sem
     esconder a arena (pointer-events: none e centralizado no ponteiro). */
  #tutorialClickHint {
    position: absolute;
    left: 50%;
    top: 55%;
    width: 44px;
    height: 44px;
    margin: -22px 0 0 -22px;
    border-radius: 50%;
    pointer-events: none;
    display: none;
  }
  #tutorialClickHint.visible {
    display: block;
  }
  #tutorialClickHint::before, #tutorialClickHint::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 2px solid var(--cor-vermelho-destaque);
    border-radius: 50%;
    animation: tutorial-click-pulse 1.4s ease-out infinite;
  }
  #tutorialClickHint::after {
    animation-delay: 0.7s;
  }
  @keyframes tutorial-click-pulse {
    0% { transform: scale(0.35); opacity: 0.9; }
    100% { transform: scale(1); opacity: 0; }
  }
  /* 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; }
  @keyframes tutorial-zone-pulse {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
  }
  /* Destaque do botão de escudo: mesma geometria de .touch-shield-btn
     (touchControls.js), só um pouco maior para formar um halo em volta dele,
     em vez de cobrir metade da arena como os dois destaques acima. */
  .tutorial-shield-zone-hint.visible {
    left: auto;
    top: auto;
    right: calc(4vmin - 6px);
    bottom: calc(22vmin - 6px);
    width: calc(14vmin + 12px);
    height: calc(14vmin + 12px);
    max-width: 84px;
    max-height: 84px;
    border-radius: 50%;
    border-style: dashed;
  }
  .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 `#hud` (10): o HUD cobre a arena inteira (`inset: 0`)
     e é irmão destes overlays no mesmo contexto de empilhamento — sem isso
     vida/escudo/nome apareceriam por cima da tela de fim de partida, que é
     opaca. Pelo mesmo motivo as modais (classe, 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
     duelo, 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 arte de
     `endgame.png`, que fica em ronda lenta pelas duas diagonais da arena
     (↘ e depois ↗). O passo é inteiro (`steps`, 8px por quadro sobre um
     tile de 320px) como o resto da interface: um pan interpolado deixaria
     a textura em posição fracionária e leria como rolagem travada. O
     brilho em 50% vem do véu (`--cor-veu-endgame`) por cima da imagem, e
     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/endgame.png') repeat;
    background-size: auto, 320px 320px;
    animation: entrada-pop 0.22s ease, endgame-diagonais 24s steps(8, end) infinite;
  }
  @keyframes endgame-diagonais {
    0% { background-position: 0 0, 0 0; }
    25% { background-position: 0 0, 64px 64px; }
    50% { background-position: 0 0, 0 0; }
    75% { background-position: 0 0, 64px -64px; }
    100% { background-position: 0 0, 0 0; }
  }
  /* 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;
  }
  /* Os textos dos overlays da partida têm tamanho próprio, proporcional à
     largura da **arena** (`--largura-arena`, escrita por gameScale.js) e não à
     da tela: a arena é redimensionada por CSS e não reduz mais o texto junto
     com ela, então cada um precisa caber sozinho dentro dela — e ela quase
     nunca é a tela inteira (num celular deitado sobra tela dos dois lados). O
     mínimo do clamp é baixo de propósito: numa arena pequena um piso alto
     voltaria a estourar o texto, que é justamente o que se quer evitar. */
  #countdownMatchup {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.3em;
    max-width: 92%;
    font-size: clamp(16px, calc(0.07 * var(--largura-arena)), 72px);
    font-weight: bold;
    color: var(--cor-branco);
    text-align: center;
    overflow-wrap: anywhere;
  }
  #countdownVs {
    font-size: clamp(12px, calc(0.035 * var(--largura-arena)), 32px);
    font-weight: normal;
    color: var(--cor-texto-fraco);
  }
  #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 `#hud`
     (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 {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: clamp(4px, calc(0.012 * var(--largura-arena)), 12px);
    /* Estreito de propósito: os cartões são linhas de texto curto, e um painel
       largo espalha nome, selo e descrição por uma linha que o olho precisa
       varrer inteira — a decisão é entre três itens, não uma leitura de tabela. */
    width: min(92%, calc(0.44 * 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. */
    overflow: hidden;
  }
  /* 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;
    flex-direction: column;
    gap: clamp(3px, calc(0.008 * var(--largura-arena)), 8px);
    margin: 0;
    padding: 0;
    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;
  }
  /* Um cartão por opção, empilhados na vertical como na tela do Vampire
     Survivors — e não em três colunas: cada linha carrega nome, raridade e
     descrição, e em coluna estreita a descrição (a única coisa que diz o que o
     item faz) quebraria em cinco linhas. */
  .item-choice-opcao {
    /* Cada cartão entra depois do anterior (`--atraso`, escrito pelo JS): a
       lista se monta de cima para baixo em vez de os três aparecerem juntos. */
    animation: item-choice-cartao 0.2s steps(2, end) backwards;
    animation-delay: var(--atraso, 0ms);
  }
  @keyframes item-choice-cartao {
    0% { opacity: 0; transform: translateX(-8px); }
    100% { opacity: 1; transform: translateX(0); }
  }
  .item-choice-botao {
    display: flex;
    align-items: center;
    gap: clamp(5px, calc(0.014 * var(--largura-arena)), 14px);
    width: 100%;
    padding: clamp(4px, calc(0.011 * var(--largura-arena)), 12px);
    text-align: left;
    /* Mesma dupla de camadas do inventário do HUD e da linha do Relicário: véu
       da raridade sobre a pedra, mais a faixa sólida na borda esquerda. O mesmo
       item tem a mesma cor nas quatro telas em que ele aparece. */
    background-color: var(--cor-painel-pedra-veu);
    background-image: linear-gradient(var(--cor-raridade-veu), var(--cor-raridade-veu));
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-pequena), inset 4px 0 0 var(--cor-raridade);
    cursor: pointer;
  }
  /* O cartão sob o cursor acende: a borda vira a cor da raridade e o véu dela
     entra numa camada a mais, escurecendo o fundo. Nada de `transform` nem de
     sombra maior — hover que mexe em geometria (mesmo em `transform`, que não
     entra no layout) faz o cartão 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 um item. Cor não tem esse efeito. */
  .item-choice-botao:hover:not(:disabled),
  .item-choice-botao:focus-visible {
    border-color: var(--cor-raridade);
    /* O mesmo véu duas vezes, em vez de um token novo: a raridade só tem duas
       variáveis, e empilhá-las é o que dá o segundo passo de intensidade. */
    background-image:
      linear-gradient(var(--cor-raridade-veu), var(--cor-raridade-veu)),
      linear-gradient(var(--cor-raridade-veu), var(--cor-raridade-veu));
    outline: none;
  }
  /* O **tamanho** deste canvas não está aqui de propósito: quem o escreve é o JS
     (`criarCartao` em escolhaDeItens.js), no mesmo lugar que cria o buffer, e em
     duas medidas conforme a faixa de tela. Canvas esticado por CSS reamostra o
     desenho — o meio pixel proibido —, e com o número em dois arquivos era
     questão de tempo até um mudar sem o outro. */
  .item-choice-icone {
    flex: none;
    image-rendering: pixelated;
  }
  .item-choice-texto {
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* Ocupa o resto do cartão, e não só a largura do texto: é isso que permite
       ao selo de nível encostar na borda direita (`margin-left: auto`). */
    flex: 1;
    min-width: 0;
  }
  .item-choice-linha {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.5em;
  }
  .item-choice-nome {
    font-size: clamp(10px, calc(0.021 * var(--largura-arena)), 19px);
    color: var(--cor-branco);
  }
  /* O selo de nível: "NOVO" na primeira cópia, "nível N" a partir da segunda
     (ver nivelDoItemNaBuild em shared/horde.js). O de subida é o dourado do
     jogo, porque é a informação que muda a decisão — a cópia rende metade da
     anterior, então "isso é reforço" precisa se ler antes de clicar. */
  .item-choice-nivel {
    /* Encostado na borda direita do cartão, como na tela do Vampire Survivors:
       ao lado do nome ele lê como parte do nome, e é uma informação de outra
       natureza — o nome diz o que é, o selo diz o que ele vira. */
    margin-left: auto;
    padding: 1px 5px;
    font-size: clamp(7px, calc(0.013 * var(--largura-arena)), 12px);
    font-weight: bold;
    letter-spacing: 0.04em;
    border: 1px solid var(--cor-pedra-borda);
  }
  .item-choice-nivel.novo {
    color: var(--cor-texto-fraco);
  }
  .item-choice-nivel.subindo {
    color: var(--cor-ouro-suave);
    box-shadow: inset 0 0 0 1px var(--cor-ouro-suave);
    /* Pisca entre duas cores, nunca entre a cor e o nada: alternando com
       invisível, metade do tempo o selo não estaria lá — mesma regra do aviso de
       onda e da barra de vida baixa. */
    animation: item-choice-nivel-pisca 0.9s steps(1, end) infinite alternate;
  }
  @keyframes item-choice-nivel-pisca {
    0% { color: var(--cor-ouro-suave); }
    100% { color: var(--cor-branco); }
  }
  .item-choice-raridade {
    font-size: clamp(7px, calc(0.013 * var(--largura-arena)), 12px);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--cor-raridade);
  }
  .item-choice-descricao {
    font-size: clamp(8px, calc(0.016 * var(--largura-arena)), 14px);
    color: var(--cor-texto-suave);
  }
  /* Depois de escolher: o escolhido fica aceso e os outros dois 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. A moldura é `inset` de propósito — sombra para fora aqui faria
     nascer a barra de rolagem horizontal (ver #itemChoiceOpcoes acima). */
  .item-choice-opcao.escolhido .item-choice-botao {
    box-shadow: var(--sombra-pedra-pequena), inset 0 0 0 2px var(--cor-raridade);
    border-color: var(--cor-raridade);
    /* Duas piscadas na cor cheia da raridade: é o comprovante de **qual** dos três
       foi escolhido, 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% { background-color: var(--cor-painel-pedra-veu); }
    50% { background-color: var(--cor-raridade); }
    100% { background-color: var(--cor-painel-pedra-veu); }
  }
  /* Janela morta dos primeiros segundos (ver TRAVA_INICIAL_MS em
     escolhaDeItens.js): os três cartões 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 faixa de raridade e a borda saem para a cor apagada
     da pedra, o cartão perde peso e o cursor diz que ali não se clica. O texto
     continua na intensidade normal — é justamente o que se quer que o jogador
     leia nesses segundos. */
  .item-choice-opcao.travado .item-choice-botao {
    opacity: 0.55;
    border-color: var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-pequena), inset 4px 0 0 var(--cor-texto-fraco);
    background-image: none;
    cursor: not-allowed;
    /* A faixa da borda esquerda pisca entre a pedra e a raridade em degrau seco,
       como o selo de nível: é o que separa "ainda não dá para clicar" de "este
       cartão está desligado". Duas cores, nunca cor e nada — mesma regra do
       aviso de onda e da barra de vida baixa. */
    animation: item-choice-travado 0.6s steps(1, end) infinite alternate;
  }
  @keyframes item-choice-travado {
    0% { box-shadow: var(--sombra-pedra-pequena), inset 4px 0 0 var(--cor-texto-fraco); }
    100% { box-shadow: var(--sombra-pedra-pequena), inset 4px 0 0 var(--cor-raridade); }
  }
  .item-choice-opcao.descartado .item-choice-botao {
    opacity: 0.3;
  }
  .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;
    width: clamp(24px, calc(0.05 * var(--largura-arena)), 42px);
    height: clamp(16px, calc(0.032 * var(--largura-arena)), 26px);
    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 {
    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);
  }
  @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);
  }
  /* Em modo touch não há hover nem teclado: o cartão é o alvo de toque, então
     ele cresce e a lista ganha folga entre os itens (um toque errado aqui custa
     o item da onda). */
  body.touch-mode .item-choice-botao {
    padding: 10px;
    gap: 10px;
  }
  /* Arena curta (janela estreita ou tela deitada e baixa): o painel encolhe para
     os **três** cartões caberem sem rolagem. A opção que o jogador não vê é uma
     opção que não existe, e o cartão de baixo atrás de uma barra de rolagem é
     exatamente isso. O ícone encolhe junto, mas pelo JS (ver
     `.item-choice-icone` acima). O `touch-mode` acima perde desta regra de
     propósito quando as duas valem: numa arena de 200px de altura o alvo de
     toque generoso é o que empurra o terceiro cartão para fora. */
  body.tela-estreita #itemChoicePainel,
  body.tela-baixa-deitada #itemChoicePainel {
    width: min(98%, calc(0.9 * var(--largura-arena)));
    padding: 6px;
    gap: 3px;
  }
  body.tela-estreita .item-choice-botao,
  body.tela-baixa-deitada .item-choice-botao {
    padding: 4px;
    gap: 6px;
  }
  body.tela-estreita .item-choice-descricao,
  body.tela-baixa-deitada .item-choice-descricao {
    font-size: clamp(7px, calc(0.014 * var(--largura-arena)), 12px);
  }
  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;
    max-width: 92%;
    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;
  }
  #noOpponentsMessage {
    display: none;
    margin: 0;
    max-width: 320px;
    text-align: center;
    color: var(--cor-texto-suave);
    font-size: 15px;
  }
  /* 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. */
  /* Na sala de espera do modo defesa o painel já diz tudo (nível, grupo,
     código, tempo restante) e a espera tem fim anunciado: o pentagrama
     girando e o "Montando o grupo de defesa..." só empurravam o painel para
     baixo e davam a impressão de que algo ainda estava carregando. No 1x1
     ficam, porque lá não há mais nada na tela e a espera é indefinida. */
  #waitingOverlay.horde .loader,
  #waitingOverlay.horde #waitingMessage {
    display: none;
  }
  /* Enquanto os dados da sala não chegaram, só o pentagrama girando: ver
     showWaitingOverlay({ carregando: true }) em overlays.js. */
  #waitingOverlay.carregando #waitingMessage {
    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; }
  }
  #gameOverOverlay.win h2 {
    color: var(--cor-sucesso);
  }
  #gameOverOverlay.lose h2 {
    color: var(--cor-erro);
  }
  /* ===== 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 arte de endgame.png. 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. Quem rola é só o
     `.gameover-conteudo`: título, relatório, resumo, medalha, aviso e
     botões — 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. Sem isso o conteúdo vazava pra fora da arena inteira em vez de
     ficar contido nela. `max-width: 100%` existe pelo mesmo motivo do
     `max-height`: nada aqui deve depender de a arena ser larga o bastante. */
  .gameover-conteudo {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(8px, calc(0.02 * var(--largura-arena)), 20px);
    max-width: 100%;
    max-height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 4px;
    box-sizing: border-box;
    position: relative;
    z-index: 1;
  }
  /* 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 duelo (#gameOverRelatorio) =====
     Os dois lutadores lado a lado, cada um com o sprite da classe em cima e os
     números embaixo (montado por gameOver.js). Substitui o resumo em lista no
     1x1 — a Defesa da base continua no #gameOverStats acima. */
  #gameOverRelatorio {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
  }
  #gameOverRelatorio[hidden] {
    display: none;
  }
  .gameover-lutadores {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    /* 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)));
  }
  .gameover-lutador {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    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` das miniaturas (0,4) foi calibrado para um card
     pequeno; aqui o quadro é grande e o personagem ocupa pouco do quadro de
     100x100 (o corpo mora entre y=33 e y=60, centro em ~0,48), 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: 2.7;
    --sprite-anchor: 0.5;
  }
  /* 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);
  }
  .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,
  .gameover-relatorio-rodape {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin: 4px 0 0;
    padding: 0;
    list-style: none;
    /* A largura mínima vem da **arena**, não de `vw`: em modo touch a tela é
       girada por CSS (ver screenRotation.js) e ali `vw` é o lado errado. */
    min-width: min(190px, calc(0.34 * var(--largura-arena)));
  }
  .gameover-lutador-stats li,
  .gameover-relatorio-rodape 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 do duelo, apertados. A largura mínima
     de dois lutadores, 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-stats {
    min-width: min(122px, calc(0.2 * 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)));
  }
  /* O tempo é da partida, não de um dos lados, então fica embaixo dos dois
     blocos e não dentro deles. */
  .gameover-relatorio-rodape {
    margin: 0;
    padding: min(4px, calc(0.008 * var(--largura-arena))) min(14px, 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;
  }
  @keyframes gameover-revelar-pop {
    0% { transform: scale(1.3); }
    50% { transform: scale(0.92); }
    100% { transform: scale(1); }
  }

  #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 {
    min-width: min(96px, calc(0.17 * var(--largura-arena)));
    /* Sem piso fixo aqui: o perfil usa `.achievement-badge` numa grade de 4
       colunas (onde a largura vem da grade, 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 do duelo.
       `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(4px, calc(0.007 * var(--largura-arena)));
    gap: min(2px, calc(0.004 * 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(10px, calc(0.017 * var(--largura-arena)));
  }
  .gameover-medalha-lista .achievement-progresso {
    font-size: min(9px, calc(0.015 * var(--largura-arena)));
  }

  #gameOverOverlay .overlay-buttons, #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);
  }
  #gameOverOverlay button#btnBackToMenu {
    background: transparent;
    border: 2px solid var(--cor-borda-media);
    font-size: 14px;
    padding: 8px 18px;
  }
  #gameOverOverlay button#btnBackToMenu:hover {
    background: var(--cor-superficie-hover);
    border-color: var(--cor-borda-hover);
  }
  #waitingOverlay button#btnTryTrainingMode {
    display: none;
  }
  #gameOverOverlay button#btnSwapClasses {
    display: none;
    background: var(--cor-borda-media);
  }
  #gameOverOverlay button#btnSwapClasses:hover {
    background: var(--cor-borda);
  }
  #onlineCount {
    margin-top: -2px;
    font-size: 14px;
    color: var(--cor-sucesso-claro);
  }
  /* ===== Modal de escolha de classe (`.class-picker`) =====
     Componente do 1x1 online (#onlineClassOverlay) e do modo bot
     (#botClassOverlay). Lista vertical das 6 classes à esquerda, preview
     animado + painel de detalhes empilhados à direita. A defesa da base não
     tem essa modal: o modo tem uma única classe (Atirador).

     As regras aqui são todas por classe (`class-picker-*`), e não por id, de
     propósito — é o que dá o modo touch de graça às duas telas que a usam.
     Id continua valendo só para o que é mesmo de uma tela só (a mensagem
     de classe bloqueada, por exemplo). */
  .class-picker-area {
    display: flex;
    align-items: stretch;
    gap: 14px;
  }
  .class-picker-list {
    display: flex;
    flex-direction: column;
    flex: 0 0 150px;
    gap: 8px;
  }
  /* Modo touch: a coluna vertical de cards vira uma faixa horizontal (ver
     design.md) — libera a largura inteira da modal para o preview/detalhes
     empilhados abaixo dela, em vez de dividir a tela entre lista e
     detalhes. `body.touch-mode` sozinho (sem media query de largura)
     porque a troca vale em qualquer viewport em modo touch, não só nos
     estreitos. `.class-card-compact` é o modificador que classSelect.js
     aplica a cada card quando `state.touchMode`.
     A modal não pode rolar na vertical em hipótese alguma em modo touch
     (ver a regra de `.modal` mais abaixo), então a faixa também não pode
     rolar na horizontal: os 6 cards encolhem (`flex: 1 1 0`, sem largura
     fixa) até caber lado a lado na largura da modal, em vez de vazar para
     um scroll. */
  body.touch-mode .class-picker-area {
    flex-direction: column;
    flex: 1;
    min-height: 0;
  }
  body.touch-mode .class-picker-list {
    flex: none;
    flex-direction: row;
    flex-wrap: nowrap;
    overflow: visible;
    gap: 6px;
  }
  body.touch-mode .class-picker-list .class-card-compact {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 4px;
    padding: 8px 2px;
  }
  body.touch-mode .class-picker-list .class-card-compact .class-name {
    align-items: center;
  }
  body.touch-mode .class-picker-list .class-card-compact .class-demon-name {
    font-size: 11px;
    white-space: normal;
    text-align: center;
  }
  /* Sem descrição de subclasse no card compacto — só ícone + nome. */
  body.touch-mode .class-picker-list .class-card-compact .class-name-secondary {
    display: none;
  }
  .class-picker-right {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    gap: 10px;
  }
  /* Em modo touch não cabe preview + tabela de stats empilhados sem rolar a
     modal, então só um dos dois aparece por vez — o botão "Detalhes"
     (`.class-picker-toggle`) intercala via `.show-details` no painel da
     direita (ver classDetailsToggle.js). Some `body.touch-mode` sozinho de
     propósito: em desktop os dois sempre ficam visíveis lado a lado, como já
     era. */
  body.touch-mode .class-picker-right {
    flex: 1;
    min-height: 0;
  }
  body.touch-mode .class-picker-preview {
    display: flex;
    flex: 1;
    min-height: 0;
  }
  body.touch-mode .class-picker-details {
    display: none;
  }
  body.touch-mode .class-picker-right.show-details .class-picker-preview {
    display: none;
  }
  body.touch-mode .class-picker-right.show-details .class-picker-details {
    display: flex;
    flex: 1;
    min-height: 0;
    overflow: hidden;
  }
  .class-card {
    display: flex;
    align-items: center;
    gap: 16px;
    width: 100%;
    box-sizing: border-box;
    text-align: left;
    /* Cores da moldura em pixel art — ver o bloco no fim do arquivo. */
    --cor-fundo-campo: var(--cor-superficie-alta);
    --cor-borda-campo: var(--cor-borda);
    padding: 8px 10px;
    cursor: pointer;
    color: var(--cor-texto);
    font: inherit;
  }
  .class-card:hover {
    --cor-borda-campo: var(--cor-borda-clara);
  }
  /* O cartão selecionado tinha um anel de 1px além da borda vermelha; com a
     moldura recortada não daria para vê-lo (nada é pintado fora da
     silhueta), então quem marca a seleção é a cor da borda. */
  .class-card.selected {
    --cor-borda-campo: var(--cor-vermelho-destaque);
  }
  /* Cartão bloqueado (classe exige conta): só um cadeado no canto, continua
     clicável — o jogador pode ver as características de qualquer classe. Quem
     realmente impede de jogar é o botão "Jogar" (#btnOnlineClassConfirm). */
  .class-card.locked {
    position: relative;
  }
  .class-card-lock {
    position: absolute;
    top: 6px;
    right: 8px;
    width: 18px;
    height: 18px;
    color: var(--cor-texto-fraco);
  }
  .class-card-lock svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  /* Botão de confirmar bloqueado: o próprio texto vira a mensagem de bloqueio,
     sem tooltip. Usa cor sólida (não `opacity`) para não desbotar o cadeado.
     Vale pela classe (`.class-picker-confirm`, no 1x1 e na defesa da base), não
     por id: o 1x1 bloqueia a classe exclusiva de conta e a defesa bloqueia a
     classe que outro jogador da sala já escolheu — sem a regra, o cadeado
     inline nasce do tamanho do botão. */
  .class-picker-confirm.locked {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    cursor: not-allowed;
    background: var(--cor-superficie-alta);
    color: var(--cor-texto-fraco);
    font-size: 14px;
  }
  .class-picker-confirm.locked:hover {
    background: var(--cor-superficie-alta);
  }
  .btn-lock-icon {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
  }
  .btn-lock-icon svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .class-card .class-sprite {
    --sprite-box: 36px;
    --sprite-zoom: 3;
    margin: -6px 0px 6px 0px;
  }
  /* A margem negativa acima foi calibrada para o card horizontal (sprite ao
     lado do texto); no card compacto (sprite em cima do nome, ver acima) ela
     desalinharia o sprite para fora do card. */
  .class-card-compact .class-sprite {
    margin: 0;
  }
  .class-name {
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow: hidden;
  }
  .class-demon-name {
    font-size: 14px;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .class-name-secondary {
    font-size: 12px;
    color: var(--cor-texto-fraco);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .class-dropdown-toggle {
    gap: 10px;
  }
  .class-dropdown-toggle .class-name {
    flex: 1;
    text-align: left;
  }
  .class-dropdown-toggle .class-demon-name {
    font-size: 15px;
  }
  .class-dropdown-item {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .class-dropdown-item.selected {
    box-shadow: inset 0 0 0 1px var(--cor-vermelho-destaque);
  }
  /* 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: 1.9;
    --sprite-anchor: 0.4;
    --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));
    }
  }
  .class-icon {
    flex: none;
    width: 32px;
    height: 32px;
    border-radius: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--cor-superficie-icone);
    border: 1px solid var(--class-color, var(--cor-borda));
    color: var(--class-color, var(--cor-azul-classe));
  }
  .class-icon svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .class-picker-details {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    width: 100%;
    box-sizing: border-box;
    background: var(--cor-superficie-baixa);
    border: 2px solid var(--cor-borda);
    border-radius: 0;
    padding: 12px 14px;
    min-height: 118px;
  }
  /* Crossfade curto ao trocar de classe na modal online (classSelect.js
     retrigger via reflow) — só o painel de stats, o preview animado ao lado
     já tem sua própria troca via classPreview.js. */
  .class-picker-details.class-details-fade {
    animation: overlay-fade 0.18s ease;
  }
  .class-picker-details .class-icon {
    width: 44px;
    height: 44px;
  }
  .class-picker-details .class-icon svg {
    width: 26px;
    height: 26px;
  }
  .class-picker-details .class-info {
    flex: 1;
    min-width: 0;
  }
  /* Nome e ícones de stat ficam neutros: a cor da classe aparece no
     `.class-icon` ao lado (`--class-color`), que já é o identificador visual. */
  .class-picker-details h3 {
    margin: 0 0 6px 0;
    font-size: 16px;
    color: var(--cor-texto);
  }
  .class-picker-details .class-stats {
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: 12px;
    color: var(--cor-texto-suave);
  }
  .class-picker-details .stat-row {
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .class-picker-details .stat-icon {
    flex: none;
    width: 15px;
    height: 15px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--cor-texto-fraco);
  }
  .class-picker-details .stat-icon svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .class-picker-details .stat-label {
    color: var(--cor-texto-fraco);
    white-space: nowrap;
  }
  .class-picker-details .stat-value {
    margin-left: auto;
    font-weight: 600;
    color: var(--cor-texto);
    white-space: nowrap;
  }
  .class-picker-preview {
    position: relative;
    flex: 1;
    min-height: 170px;
    box-sizing: border-box;
    overflow: hidden;
    background: var(--cor-superficie-icone);
    border: 2px solid var(--cor-borda);
    border-radius: 0;
    padding: 8px 0 0;
    display: flex;
    flex-direction: column;
  }
  /* `box-sizing` porque a caixa tem padding lateral e a barra dentro dela ocupa
     100% da largura — sem isso as duas somam e a barra vaza do preview. */
  .class-preview-hearts,
  .class-preview-shields {
    box-sizing: border-box;
    flex: none;
    padding: 0 10px;
  }
  .class-preview-shields {
    padding-top: 3px;
  }
  /* Fileiras em miniatura: a caixa do preview é bem mais estreita que a
     metade do HUD de partida, então tudo encolhe junto (ver
     PREVIEW_CONTADOR_PIXEL) — os ícones já saem pequenos, só o contador
     precisa de ajuste de fonte aqui. */
  .class-preview-hearts .resource-count {
    font-size: 11px;
  }
  .class-preview-shields .resource-count {
    font-size: 10px;
  }
  .class-preview-character {
    position: relative;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    margin-left: 12px;
  }
  .class-preview-fighter.class-sprite {
    --sprite-box: 130px;
    --sprite-anchor: 0.5;
  }
  /* Fallback de classe sem sprite próprio: o quadrado colorido antigo. */
  .class-preview-square {
    flex: none;
    width: 22px;
    height: 22px;
    border-radius: 0;
    border: 2px solid var(--cor-ouro);
    box-sizing: border-box;
  }
  .class-preview-spacer {
    flex: none;
  }
  .class-preview-shot {
    --shot-dx: 0px;
    --shot-dy: 0px;
    position: absolute;
    top: 50%;
    left: 0;
    border-radius: 50%;
    transform: translate(0, -50%);
    opacity: 0;
  }
  .class-preview-shot.shooting {
    animation-name: classPreviewShot;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
  }
  @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; }
  }
  .class-preview-dummy {
    flex: none;
    width: 34px;
    height: 34px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--cor-superficie-hover-alta);
    border: 2px solid var(--cor-cinza-claro);
    border-radius: 0;
    color: var(--cor-texto-suave);
  }
  .class-preview-dummy svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
  }
  .class-preview-dummy.hit {
    animation: classPreviewDummyHit 0.3s ease-out;
  }
  @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); }
  }
  .class-preview-damage {
    position: absolute;
    font-size: 15px;
    font-weight: 700;
    color: var(--cor-erro-texto);
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
    pointer-events: none;
    white-space: nowrap;
    animation: classPreviewDamageFloat 0.9s ease-out forwards;
  }
  @keyframes classPreviewDamageFloat {
    0% { opacity: 0; transform: translate(-50%, 0); }
    15% { opacity: 1; }
    100% { opacity: 0; transform: translate(-50%, -16px); }
  }
  /* "Patch" e "Créditos" ficam no fim de #menuBody, embaixo dos botões de modo
     de jogo, em vez de terem visão própria — são ações secundárias, não merecem
     um botão na navegação principal (#menuNav) ao lado de "jogar" e "ranking".
     São botões de ação normais (mesma medida e mesma moldura de fliperama dos
     de modo, ver o bloco de pixel art no fim do arquivo); a hierarquia está só
     na cor: cinza para quem abre modal, vermelho para quem leva a uma visão. */
  #menuTitleWrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    animation: entrada-fade 0.5s ease both;
  }
  #menuTitleExtras {
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Mesmo vão dos botões de modo (#menuBody), com um respiro a mais em cima
       pra marcar que aqui começam as ações secundárias. */
    gap: 4px;
    margin-top: 8px;
    width: 100%;
  }
  /* 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-superficie-forte);
  }
  #menu #menuTitleExtras button.title-action-btn:hover {
    background: var(--cor-superficie-forte-hover);
  }
  #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;
  }
  /* 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); }
  /* 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 (ver `body.touch-mode #onlineClassOverlay .modal`). */
  /* 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,
  body.touch-mode #botClassOverlay .modal {
    max-height: calc(var(--altura-tela) - 16px);
    overflow-y: auto;
  }
  body.touch-mode #btnBotClassClose,
  body.touch-mode #btnAuthClose,
  body.touch-mode #btnProfileClose,
  body.touch-mode #btnCreditsClose,
  body.touch-mode #btnChangelogClose {
    min-width: 44px;
    min-height: 44px;
  }
  #botClassOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    animation: overlay-fade 0.15s ease;
    align-items: center;
    justify-content: center;
    z-index: 20;
  }
  /* Modal do modo treino: fundo opaco (o menu atrás competia com o conteúdo) e
     sem scroll próprio — o conteúdo é curto e os dropdowns são `position: fixed`
     (ver dropdownPosition.js), então nada aqui precisa rolar.
     "Curto" vale no desktop: `max-height: none` é o **único** lugar do arquivo
     que abre mão do teto de altura da regra comum de `.modal`, e num celular
     deitado (~400px de altura de conteúdo) as duas colunas de classes mais os
     dois dropdowns passam da tela — a modal crescia para fora dela e o botão
     de confirmar ficava inalcançável. O teto volta em modo touch, logo
     abaixo. */
  #botClassOverlay .modal {
    max-height: none;
    overflow: visible;
    padding: 20px 24px;
    /* 468 = os 420 de conteúdo de antes + os 24px de padding de cada lado:
       a regra comum de `.modal` passou a declarar `box-sizing: border-box`
       (ver lá o porquê), e sem somar o padding aqui a modal encolheria no
       desktop. `max-width` porque ela era a única modal sem um — na largura
       de um celular ela vazava para fora da tela. */
    width: 468px;
    max-width: calc(var(--largura-tela) - 32px);
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  #botClassHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  #botClassHeader h2 {
    margin: 0;
    font-size: 22px;
  }
  #btnBotClassClose {
    width: 26px;
    height: 26px;
    min-width: 0;
    padding: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--cor-texto-fraco);
    font-size: 15px;
    line-height: 1;
    border: none;
    cursor: pointer;
  }
  #btnBotClassClose:hover {
    background: var(--cor-superficie-hover);
    color: var(--cor-branco);
  }
  #botClassColumns {
    display: flex;
    gap: 14px;
  }
  .class-select-col {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1;
    min-width: 0;
  }
  .class-select-col h3 {
    margin: 0;
    font-size: 13px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .class-mini-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  #botOptionsRow {
    display: flex;
    gap: 14px;
  }
  #botDifficultyCol,
  #botArenaCol {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1;
    min-width: 0;
  }
  #botDifficultyCol h3,
  #botArenaCol h3 {
    margin: 0;
    font-size: 13px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .dropdown {
    position: relative;
  }
  .dropdown-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 10px 14px;
    /* Cores da moldura em pixel art — ver o bloco no fim do arquivo. */
    --cor-fundo-campo: var(--cor-superficie-elevada);
    --cor-borda-campo: var(--cor-branco);
    color: var(--cor-texto);
    font-size: 14px;
    font-family: inherit;
    cursor: pointer;
  }
  .dropdown-toggle:hover {
    --cor-fundo-campo: var(--cor-superficie-hover-alta);
  }
  .dropdown-toggle:focus-visible {
    outline: none;
    --cor-borda-campo: var(--cor-vermelho-destaque);
  }
  .dropdown-arrow {
    color: var(--cor-texto-fraco);
    flex: none;
    margin-left: 10px;
    transition: transform 0.15s;
  }
  .dropdown.open .dropdown-toggle {
    --cor-borda-campo: var(--cor-vermelho-destaque);
    --cor-fundo-campo: var(--cor-superficie-hover-alta);
  }
  .dropdown.open .dropdown-arrow {
    transform: rotate(180deg);
  }
  .dropdown-menu {
    list-style: none;
    margin: 0;
    padding: 6px;
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    right: 0;
    background: var(--cor-superficie-alta);
    border: 2px solid var(--cor-borda);
    border-radius: 0;
    box-shadow: var(--sombra-flutuante);
    z-index: 20;
    display: flex;
    flex-direction: column;
    gap: 2px;
    opacity: 0;
    transform: translateY(-6px);
    pointer-events: none;
    transition: opacity 0.12s ease, transform 0.12s ease;
    max-height: 280px;
    overflow-y: auto;
  }
  .dropdown.open .dropdown-menu {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }
  .dropdown-item {
    padding: 9px 10px;
    border-radius: 0;
    color: var(--cor-texto-suave);
    font-size: 14px;
    cursor: pointer;
  }
  .dropdown-item:hover {
    background: var(--cor-superficie-hover);
    color: var(--cor-branco);
  }
  .dropdown-item.selected {
    background: var(--cor-vermelho-selecao);
    color: var(--cor-branco);
  }
  #btnBotClassConfirm {
    font-size: 16px;
    padding: 12px 24px;
    border-radius: 8px;
    border: none;
    cursor: pointer;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #btnBotClassConfirm:hover {
    background: var(--cor-vermelho-hover);
  }
  /* Véu e caixa da modal de classe. Os overlays que usam o componente
     carregam `.class-picker-overlay`, então abrir/fechar e dimensionar é a
     mesma regra para todos. */
  .class-picker-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 20;
    animation: overlay-fade 0.15s ease;
  }
  .class-picker {
    position: relative;
    padding: 20px 24px;
    width: 540px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  .class-picker-header h2 {
    margin: 0;
    font-size: 22px;
  }
  /* Linha de apoio do cabeçalho (só a defesa da base tem uma). */
  .class-picker-header p {
    margin: 0;
    font-size: 13px;
    color: var(--cor-texto-fraco);
  }
  #btnOnline {
    padding: 6px !important;
  }
  /* 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 ou com a lista de classes — fica na mesma linha da lista, só que
     por cima dela (ver `body.touch-mode .class-picker-list`, que encolhe a
     própria largura pra sobrar um vão livre sem card nenhum atrás do botão).
     Vermelho como os outros botões de ação (Jogar, Detalhes quando ativo) em
     vez da cor neutra dos flutuantes de som/música — esse aqui fecha a modal,
     então precisa continuar chamando atenção mesmo sem um cabeçalho ao redor
     para dar contexto. Existe mesmo com o cabeçalho escondido em modo touch
     (ver `body.touch-mode .class-picker-header` mais abaixo) — é justamente o
     caso que motivou a mudança: sem cabeçalho, não sobrava lugar nenhum pra
     ele. */
  .modal-close-flutuante {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 2;
    width: 30px;
    height: 30px;
    min-width: 0;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 10px;
    border: none;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
  }
  .modal-close-flutuante:hover {
    background: var(--cor-vermelho-hover);
  }
  .class-picker-confirm {
    font-size: 16px;
    padding: 12px 24px;
    border-radius: 8px;
    border: none;
    cursor: pointer;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  .class-picker-confirm:hover {
    background: var(--cor-vermelho-hover);
  }
  /* Linha de ação (Detalhes + Jogar). Em desktop é só um invólucro
     transparente (`display: contents`) para o botão "Jogar" continuar
     exatamente como antes — largura cheia, filho direto de `.modal` — e o
     botão "Detalhes" (só faz sentido em modo touch, ver abaixo) fica
     escondido. */
  .class-picker-actions {
    display: contents;
  }
  .class-picker-toggle {
    display: none;
  }
  /* Modo touch: sem cabeçalho de título (só o "✕" flutuante continua, ver
     acima) — sobra altura pra lista de classes e o preview. A modal nunca
     rola na vertical, e a barra de ação (Detalhes/Jogar) precisa ficar
     sempre visível: era o bug original, numa tela baixa a modal cortava a
     barra de ação para fora da viewport atrás de um scroll. */
  body.touch-mode .class-picker-header {
    display: none;
  }
  /* `height` (não só `max-height`): sem uma altura definida os filhos
     flex:1 (área → painel da direita → preview/detalhes) só encolhem/crescem
     com o próprio conteúdo, e como o preview animado e a tabela de stats têm
     alturas naturais diferentes, a modal mudava de tamanho (e de posição, por
     estar centralizada verticalmente no véu) ao intercalar entre os dois. Com
     altura fixa, os dois lados sempre preenchem o mesmo espaço. */
  body.touch-mode .class-picker {
    width: min(calc(0.96 * var(--largura-tela)), 720px);
    height: calc(var(--altura-tela) - 16px);
    max-height: calc(var(--altura-tela) - 16px);
    overflow: hidden;
  }
  /* O "✕" flutuante fica na mesma linha da lista de classes (não reserva
     uma faixa própria acima dela, que rouba altura de uma modal que já não
     pode rolar) — a lista só encolhe a própria largura o suficiente pra
     sobrar um vão livre à direita, sem card nenhum atrás do botão. */
  body.touch-mode .class-picker-list {
    width: calc(100% - 46px);
  }
  body.touch-mode .class-picker-actions {
    display: flex;
    flex: none;
    gap: 10px;
  }
  body.touch-mode .class-picker-actions .class-picker-confirm {
    flex: 1;
  }
  /* Mesma cor de "botão secundário" de #btnSwapClasses (fim de jogo): o
     par certo para ficar ao lado do vermelho de "Jogar" sem competir com
     ele. Herda a moldura em pixel art dos outros botões de ação (ver o
     bloco `--recorte-botao-pixel` no fim do arquivo) por estar na lista de
     seletores de lá — aqui só a cor/tamanho. */
  body.touch-mode .class-picker-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    font-size: 16px;
    padding: 12px 24px;
    cursor: pointer;
    background: var(--cor-borda-media);
    color: var(--cor-texto);
  }
  body.touch-mode .class-picker-toggle:hover {
    background: var(--cor-borda);
  }
  #botClassOverlay .modal {
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
  }

  /* ----- 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 lateral, modal de conta e modal de entrar/criar
     conta -----
     O botão fica fora de #menu, no topo de #menuSidebar (quem posiciona a
     coluna é 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;
    gap: 8px;
    padding: 6px 14px 6px 6px;
    border: 1px solid var(--cor-ouro-suave);
    border-radius: 8px;
    background: var(--cor-sidebar-botao);
    color: var(--cor-branco);
    font-size: 13px;
    cursor: pointer;
    width: 100%;
    box-sizing: border-box;
    flex-shrink: 0;
  }
  #accountFabBtn:hover {
    background: var(--cor-sidebar-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;
  }
  #accountFabLabel {
    max-width: 140px;
    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, "Assistir" em
     partidas ao vivo, 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;
  }
  #btnAuthClose {
    width: 26px;
    height: 26px;
    min-width: 0;
    padding: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--cor-texto-fraco);
    font-size: 15px;
    line-height: 1;
    border: none;
    cursor: pointer;
  }
  #btnAuthClose:hover {
    background: var(--cor-superficie-hover);
    color: var(--cor-branco);
  }
  #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;
    border-radius: 6px;
  }
  .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;
  }
  #btnProfileClose {
    width: 26px;
    height: 26px;
    min-width: 0;
    padding: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--cor-texto-fraco);
    font-size: 15px;
    line-height: 1;
    border: none;
    cursor: pointer;
  }
  #btnProfileClose:hover {
    background: var(--cor-superficie-hover);
    color: var(--cor-branco);
  }
  #profileTabs {
    display: flex;
    gap: 4px;
    border-bottom: 1px solid var(--cor-superficie-hover);
  }
  .profile-tab {
    flex: 1;
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--cor-texto-fraco);
    font-family: inherit;
    font-size: 13px;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 8px 4px;
    cursor: pointer;
  }
  .profile-tab:hover {
    color: var(--cor-branco);
  }
  .profile-tab.active {
    color: var(--cor-vermelho-destaque);
    border-bottom-color: var(--cor-vermelho-destaque);
  }
  #profileTabs[hidden], #profileTabPartidas[hidden], #profileTabConquistas[hidden] {
    display: none;
  }
  /* Altura **fixa** (não só um teto), igual nas duas abas: trocar de aba não
     pode mudar o tamanho da modal. #profileSummary (só existe na aba
     Partidas) tem altura própria fora do flex; quem preenche o resto é a
     lista/grid rolável (`flex: 1 1 0; min-height: 0`, ver #profileBody e
     .achievements-grid abaixo) — assim a aba Conquistas, sem resumo, usa
     esse espaço extra pro próprio grid em vez de a modal encolher.
     `flex-shrink: 0` é necessário porque este container é filho do `.modal`
     (também flex-column): em modo touch, com o cabeçalho + aviso de
     convidado + abas somados, o conteúdo da modal passa do teto de altura
     dela — sem isto o flexbox encolhia este container bem abaixo dos
     `min(320px, 42%)` (chegou a 35px num celular em retrato), deixando a
     lista de conquistas/partidas praticamente invisível mesmo rolando por
     dentro. Com o valor travado, é a modal que ganha scroll próprio
     (`overflow-y: auto` na regra comum de `.modal`), não o conteúdo que
     encolhe. */
  #profileTabPartidas, #profileTabConquistas {
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    gap: 14px;
    height: min(320px, calc(0.42 * var(--altura-tela)));
  }
  /* Uma lista por classe (título + as 4 medalhas dela), não um grid solto de
     24 cartões — sem o título, "Bronze"/"Prata" repetido 6 vezes não diz de
     qual classe é cada um. */
  .achievements-grid {
    display: flex;
    flex-direction: column;
    gap: 14px;
    flex: 1 1 0;
    min-height: 0;
    overflow-y: auto;
  }
  /* As 4 medalhas de uma classe (bronze/prata/ouro/diamante) ficam lado a
     lado, uma por marco — nenhuma delas "substitui" a anterior. */
  .achievement-classe-medalhas {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
  }
  .achievement-badge {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    background: var(--cor-superficie-forte);
    padding: 8px 4px;
    border-radius: 8px;
    animation: entrada-pop 0.25s ease 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;
  }
  /* 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. */
    top: 2px;
    right: 2px;
    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;
  }
  .achievement-portrait {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    overflow: hidden;
    background-color: var(--cor-superficie-baixa);
    image-rendering: pixelated;
    border: 3px solid var(--cor-texto-fraco);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--class-color, var(--cor-texto-fraco));
  }
  .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);
  }
  .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;
  }
  /* Brilho do diamante: 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. */
  .achievement-badge.desbloqueada.tier-diamante::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);
  }
  .achievement-badge.desbloqueada .achievement-portrait {
    border-color: rgba(0, 0, 0, 0.35);
  }
  /* 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;
  }
  .achievement-tier {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  .achievement-progresso {
    font-size: 9px;
    color: var(--cor-texto-fraco);
    text-align: center;
  }
  /* Progresso até a medalha trancada, em blocos — só nas de classe, que são
     as únicas com marco contável (as da Defesa da base são "venceu ou não
     venceu", ver renderMedalhaHorda em profile.js). Três planos: as ranhuras
     por cima, o preenchimento no meio (`--blocos-cheios`, escrito por CSSOM em
     profile.js — a CSP bloqueia atributo `style`) e o trilho vazio no fundo.
     A ranhura é o que faz a barra encher em degraus de 10% em vez de deslizar
     como uma faixa contínua. */
  .achievement-barra {
    width: 100%;
    height: 5px;
    margin-top: 2px;
    background-color: var(--cor-superficie-baixa);
    background-image:
      repeating-linear-gradient(90deg, transparent 0 8%, var(--cor-superficie-forte) 8% 10%),
      linear-gradient(90deg,
        var(--cor-ouro-suave) 0 calc(var(--blocos-cheios, 0) * 10%),
        transparent 0);
    background-repeat: no-repeat;
  }
  #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;
  }
  .resumo-item strong {
    font-size: 20px;
  }
  .resumo-item span {
    font-size: 11px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .resultado-vitoria { color: var(--cor-sucesso-claro); }
  .resultado-derrota { color: var(--cor-erro-claro); }
  .resultado-empate  { color: var(--cor-empate); }
  .profile-subtitulo {
    margin: 0 0 8px;
    font-size: 11px;
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  /* A lista tem scroll próprio (é o que dispara o infinite scroll). Preenche
     o que sobra de #profileTabPartidas depois de #profileSummary
     (`flex: 1 1 0; min-height: 0`, mesmo padrão de .achievements-grid) — a
     altura de quem contém já é fixa, então não precisa de teto próprio aqui. */
  #profileBody {
    flex: 1 1 0;
    min-height: 0;
    overflow-y: auto;
  }
  .lista-partidas {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .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;
  }
  .partida-resultado {
    grid-area: resultado;
    font-size: 13px;
    font-weight: bold;
  }
  .partida-oponente {
    grid-area: oponente;
    font-size: 14px;
    overflow-wrap: anywhere;
  }
  .partida-classes {
    grid-area: classes;
    font-size: 11px;
    color: var(--cor-texto-fraco);
  }
  .partida-data {
    grid-area: data;
    font-size: 11px;
    color: var(--cor-texto-fraco);
    white-space: nowrap;
  }
  .profile-vazio {
    margin: 0;
    font-size: 14px;
    line-height: 1.6;
    color: var(--cor-texto-fraco);
    text-align: center;
  }

  /* ----- Árvore de talentos -----
     Painel dentro de #menuViewHorde (ver #rankingPanel/.spectator-panel para
     o mesmo padrão de painel de pedra na tela inicial), não mais uma modal.
     Quem rola é **só** a lista de tiers (#talentTreeTiers): título, nível,
     pontos e a linha de detalhe 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é os tiers, que é o que permite ao filho encolher
     abaixo do conteúdo dele e virar a área de rolagem. */
  #talentTreePanel {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 460px;
    height: 100%;
    max-width: 100%;
    max-height: min(560px, calc(var(--altura-tela) - 96px));
    box-sizing: border-box;
    padding: 16px 24px 24px;
    background: var(--cor-painel-pedra-veu);
    border-radius: 0;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
    overflow: hidden;
    animation: entrada-fade 0.5s ease 0.2s both;
  }
  #talentTreePanel h2 {
    margin: 0;
    font-size: 22px;
    text-align: center;
  }
  #talentTreeHint {
    margin: 0;
    font-size: 12px;
    color: var(--cor-texto-fraco);
    text-align: center;
  }
  #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;
  }
  #talentTreeSummary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    width: 100%;
    justify-content: space-between;
  }
  /* O bloco de texto (dica + pontos) fica encostado no anel mesmo quando o
     "Resetar" some — é o que acontece enquanto a árvore não carregou. Sem
     isto o `space-between` do container joga o texto para a direita e o
     cabeçalho parece outro layout só porque um botão saiu. */
  #talentTreeSummary > div {
    margin-right: auto;
  }
  /* 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;
  }
  /* 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], #talentTreeTiers[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 nos cartões. */
  #talentTreeTiers {
    display: flex;
    flex-direction: column;
    gap: 12px;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding-right: 4px;
  }
  .talent-tier h3 {
    margin: 0 0 6px;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--cor-texto-fraco);
  }
  /* Uma linha por tier: os nós ficam lado a lado, em colunas de largura igual
     (`grid-auto-flow: column`), sem depender de quantos nós o tier tem — tier 5
     (capstone) tem 3 e os outros 4. */
  .talent-tier-grid {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 6px;
  }
  /* Invólucro do cartão (ver criarCartaoDoNo em talentTreeUi.js): existe para
     ancorar a dica de hover, que precisa aparecer mesmo com o botão
     `disabled`. É ele o item do grid, então o botão preenche a célula. */
  .talent-node-wrap {
    position: relative;
    display: flex;
    min-width: 0;
  }
  .talent-node-wrap > .talent-node {
    flex: 1;
    min-width: 0;
  }
  .talent-node {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* Altura fixa igual em todos os tiers: dentro de um tier o grid já estica
       os nós juntos, mas entre tiers a altura seguia o rótulo mais longo
       daquela linha (1, 2 ou 3 linhas de texto), e a árvore inteira ficava
       irregular. A medida comporta o rótulo de três linhas, o ganho em duas e
       o rank. */
    min-height: 92px;
    gap: 2px;
    background: var(--cor-superficie-forte);
    color: var(--cor-branco);
    border: none;
    border-radius: 0;
    padding: 8px 3px;
    font-family: inherit;
    /* Quatro nós por linha num painel de 460px: o rótulo quebra em duas ou três
       linhas, então a fonte é um passo menor que a dos outros cartões. */
    font-size: 10px;
    line-height: 1.2;
    min-width: 0;
    overflow-wrap: anywhere;
    cursor: pointer;
  }
  .talent-node:disabled, .talent-node-sem-pontos {
    cursor: default;
  }
  /* Negrito nos dois: a fonte do nó é um passo menor que a do resto dos cartões
     (ver .talent-node), então o traço mais grosso é o que mantém o rótulo
     legível — inclusive no verde do estado maxado. */
  .talent-node-titulo {
    text-align: center;
    font-weight: bold;
  }
  /* Quanto o rank ajuda ("+1 coração", "-10% recarga"). Fica entre o nome e o
     rank, no vermelho da identidade, porque é o número que decide a compra —
     o nome sozinho não diz se o nó vale o ponto. */
  .talent-node-ganho {
    text-align: center;
    font-size: 9px;
    font-weight: bold;
    color: var(--cor-vermelho-destaque);
  }
  .talent-node-rank {
    font-size: 9px;
    font-weight: bold;
    color: var(--cor-texto-fraco);
  }
  /* Detalhe do talento sob o cursor/foco (ver #talentTreeDica em index.html).
     Não é um balão flutuante: um balão em cima do nó desaparecia nas linhas de
     baixo da lista rolável (#talentTreeTiers), e embaixo dele nas de cima. Como
     linha do cabeçalho — fora da área que rola, então sempre à vista, seja qual
     for o tier sob o cursor — também funciona no toque (o cartão recebe foco).
     A altura mínima é de duas linhas de texto para a árvore inteira não
     pular de posição quando o detalhe entra e sai. */
  #talentTreeDica {
    margin: 0;
    /* Duas linhas de texto **mais** o padding vertical: em `border-box` um
       `min-height: 2.6em` seco descontaria o padding do texto e a barra
       caberia só uma linha e meia — a árvore inteira pulava de posição ao
       trocar um detalhe de uma linha por um de duas. */
    min-height: calc(2.6em + 10px);
    box-sizing: border-box;
    padding: 5px 7px;
    background: var(--cor-superficie-forte);
    box-shadow: inset 0 0 0 1px var(--cor-pedra-borda);
    font-size: 11px;
    line-height: 1.3;
    color: var(--cor-texto-fraco);
  }
  /* Disponível para comprar agora: mesmo destaque de foco/seleção do resto
     da interface.
     Sem ponto livre o nó continua clicável (para avisar a recusa, ver
     talentTreeUi.js), mas não pode se anunciar como comprável — daí o
     `:not(.talent-node-sem-pontos)` nas duas regras. */
  .talent-node-disponivel:not(:disabled):not(.talent-node-sem-pontos) {
    box-shadow: inset 0 0 0 2px var(--cor-vermelho-destaque);
  }
  .talent-node-disponivel:not(:disabled):not(.talent-node-sem-pontos):hover {
    background: var(--cor-superficie-hover);
  }
  /* Bloqueado (tier ou grupo exclusivo) e maxado usam o mesmo cinza apagado
     das conquistas trancadas (.achievement-badge.bloqueada). */
  .talent-node-bloqueado-tier, .talent-node-bloqueado-grupo {
    opacity: 0.55;
  }
  /* Verde claro de fundo: o texto inverte para escuro, igual .achievement-badge
     .desbloqueada faz com as cores de tier. O rank também — senão continuava no
     cinza fraco padrão, que sobre o verde fica ilegível. */
  .talent-node-maxado {
    background: var(--cor-sucesso);
    color: var(--cor-texto-sobre-tier);
  }
  .talent-node-maxado .talent-node-rank,
  .talent-node-maxado .talent-node-ganho {
    color: var(--cor-texto-sobre-tier);
  }
  /* 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: 8px 16px;
    font-size: 11px;
    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 #menuNav button`). */
  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;
  }
  body.touch-mode #talentTreeSummary {
    gap: 6px;
    flex-wrap: nowrap;
  }
  body.touch-mode #talentTreeSummary .anel-nivel {
    --anel-lado: 34px;
  }
  body.touch-mode #talentTreeHint,
  body.touch-mode #talentTreePoints {
    font-size: 10px;
  }
  /* 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;
  }
  /* Uma linha de texto em vez de duas: o detalhe do talento sob o dedo é curto,
     e no toque ele aparece por foco, um de cada vez. O `min-height` continua
     existindo (com o padding somado, ver a regra base) só para a árvore não
     pular de posição quando o detalhe entra e sai. */
  body.touch-mode #talentTreeDica {
    min-height: calc(1.3em + 8px);
    padding: 4px 6px;
    font-size: 10px;
  }
  body.touch-mode #talentTreeTiers {
    gap: 8px;
  }
  body.touch-mode .talent-tier h3 {
    margin: 0 0 3px;
    font-size: 9px;
  }
  /* Altura mínima menor que a do desktop: o rótulo de três linhas ainda cabe
     porque a fonte também caiu. Continua igual em todos os tiers, senão a
     árvore fica irregular (ver .talent-node). */
  body.touch-mode .talent-node {
    min-height: 70px;
    padding: 6px 2px;
    font-size: 9px;
  }
  body.touch-mode .talent-node-ganho,
  body.touch-mode .talent-node-rank {
    font-size: 8px;
  }

  /* 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);
  }

  /* ----- 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;
    width: 340px;
    max-width: calc(var(--largura-tela) - 32px);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  #pauseHeader {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  #pauseHeader h2 {
    margin: 0;
    font-size: 22px;
  }
  #pauseBody {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }
  /* 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);
  }

  #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;
  }
  #btnPauseClose,
  #btnCreditsClose {
    width: 26px;
    height: 26px;
    min-width: 0;
    padding: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--cor-texto-fraco);
    font-size: 15px;
    line-height: 1;
    border: none;
    cursor: pointer;
  }
  #btnPauseClose:hover,
  #btnCreditsClose:hover {
    background: var(--cor-superficie-hover);
    color: var(--cor-branco);
  }
  #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;
  }
  .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;
  }
  #btnChangelogClose {
    width: 26px;
    height: 26px;
    min-width: 0;
    padding: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--cor-texto-fraco);
    font-size: 15px;
    line-height: 1;
    border: none;
    cursor: pointer;
  }
  #btnChangelogClose:hover {
    background: var(--cor-superficie-hover);
    color: var(--cor-branco);
  }
  #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);
  }
  /* Galeria dos personagens no tópico "Sprites dos personagens": os 6
     andando (animação `walk`), com nome do demônio e classe embaixo. O
     enquadramento reusa `.class-sprite` (ver classSprite.js), só com o quadro
     maior que o das miniaturas da lista de classes. */
  .changelog-sprites {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
    gap: 10px;
    margin-top: 10px;
  }
  .changelog-sprite-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    text-align: center;
    padding: 10px 6px 8px;
    border-radius: 0;
    background: var(--cor-superficie-icone);
    border: 1px solid var(--class-color, var(--cor-borda));
  }
  .changelog-sprite-item .class-demon-name {
    color: var(--class-color, var(--cor-texto));
  }
  .changelog-sprite.class-sprite {
    --sprite-box: 72px;
    --sprite-zoom: 2.4;
    --sprite-anchor: 0.45;
    margin-bottom: 4px;
  }
  /* 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;
  }

  /* ===== 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. */
  body.tela-estreita #menuTitle {
    font-size: clamp(56px, calc(0.18 * var(--largura-tela)), 92px);
  }
  body.tela-estreita #menu button:not(.class-card):not(.ranking-nome):not(.link-button):not(.horde-level-item):not(.toggle-switch):not(.horde-room-row):not(.monstruario-row):not(.talent-node):not(.horde-endless-tab) {
    min-width: 0;
    width: 100%;
    box-sizing: border-box;
  }
  body.tela-estreita #botClassColumns,
  body.tela-estreita #botOptionsRow {
    flex-direction: column;
  }
  /* 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;
  }
  /* Empilhado, o painel da árvore não pode ser espremido pelo flex: como quem
     rola por dentro dele é só a lista de tiers (ver #talentTreePanel), encolher
     o painel para caber junto do de salas comia a lista até ela sumir — em vez
     disso o painel guarda a altura dele (limitada pelo `max-height` de sempre)
     e quem rola para alcançar os dois é a visão (`overflow-y` de .menu-view). */
  body.tela-estreita #talentTreePanel {
    flex-shrink: 0;
  }
  /* Mesmo caso do painel da árvore acima: empilhados, os dois painéis do
     Monstruário guardam a altura deles (quem rola por dentro da lista é só
     #monstruarioList) e quem rola para alcançar os dois é a visão. */
  body.tela-estreita #monstruarioListPanel,
  body.tela-estreita #monstruarioDetailPanel,
  body.tela-estreita #relicarioListPanel {
    flex-shrink: 0;
  }
  /* O HUD da partida não é ajustado aqui: quem decide o quanto ele encolhe é
     gameScale.js, que conhece a largura que sobrou depois da arena. */
  body.tela-muito-estreita .class-picker-area {
    flex-direction: column;
  }
  body.tela-muito-estreita .class-picker-list {
    flex: none;
    flex-direction: row;
    flex-wrap: wrap;
  }
  body.tela-muito-estreita .class-picker-list .class-card {
    flex: 1 1 45%;
  }
  body.tela-muito-estreita .class-preview-fighter.class-sprite {
    --sprite-box: 90px;
  }
  /* 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. */
  body.tela-baixa-deitada #menuViewHome {
    gap: 24px;
  }
  body.tela-baixa-deitada #menuTitle {
    /* Pela largura, não pela altura: numa tela deitada a largura é o recurso
       generoso (a altura é justamente o que define esta faixa), então escalar
       por altura mantinha o título quase tão pequeno quanto o valor fixo
       antigo. */
    font-size: clamp(56px, calc(0.11 * var(--largura-tela)), 100px);
  }
  body.tela-baixa-deitada #menuTitleWrap {
    gap: 10px;
  }
  body.tela-baixa-deitada #rankingPanel,
  body.tela-baixa-deitada .spectator-panel,
  body.tela-baixa-deitada #hordeRoomsPanel,
  body.tela-baixa-deitada #talentTreePanel,
  body.tela-baixa-deitada #monstruarioListPanel,
  body.tela-baixa-deitada #monstruarioDetailPanel,
  body.tela-baixa-deitada #relicarioListPanel,
  body.tela-baixa-deitada #menuBody,
  body.tela-baixa-deitada #pvpPanel {
    padding: 10px 14px 14px;
    max-height: calc(var(--altura-tela) - 24px);
  }
  body.tela-baixa-deitada #rankingPanel h2,
  body.tela-baixa-deitada .spectator-panel h2,
  body.tela-baixa-deitada #talentTreePanel h2,
  body.tela-baixa-deitada #pvpPanel 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);
  }

  /* O HUD nos 4 cantos (ver `#hud` mais acima) já é o mesmo em desktop e em
     modo touch. As únicas diferenças em touch são: o cronômetro um pouco
     menor, porque a viewport de um celular em paisagem é baixa, e o
     `#escHint` (aviso do ESC) escondido, porque não existe teclado — quem
     não tem tecla abre a mesma modal pela engrenagem do canto
     (`#pauseMenuBtn`). */
  body.touch-mode #matchTimer {
    font-size: 15px;
    padding: 2px 10px;
  }
  body.touch-mode #escHint {
    display: none;
  }

  /* ===== 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. */
  .touch-joystick {
    position: absolute;
    width: 24vmin;
    height: 24vmin;
    max-width: 128px;
    max-height: 128px;
    margin-left: -12vmin;
    margin-top: -12vmin;
    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: 10vmin;
    height: 10vmin;
    max-width: 54px;
    max-height: 54px;
    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);
  }
  /* Mesmo azul do escudo em qualquer outro lugar do jogo (HUD, preview de
     classe): --cor-hud-escudo. O ícone em si é o mesmo pixel-art
     (SHIELD_PIXELS/createResourceIcon, hud.js) usado nas barras de recurso —
     antes este botão desenhava um pentágono em CSS (clip-path) numa cor
     diferente, a única silhueta de escudo do jogo que não vinha da grade de
     pixels. */
  .touch-shield-btn {
    position: absolute;
    right: 4vmin;
    bottom: 22vmin;
    width: 14vmin;
    height: 14vmin;
    max-width: 72px;
    max-height: 72px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    border: 2px solid var(--cor-hud-escudo-texto);
    background: rgba(25, 111, 181, 0.22);
    color: var(--cor-hud-escudo-texto);
    z-index: 5;
  }
  .touch-shield-btn.hidden {
    display: none;
  }
  .touch-shield-btn.active {
    background: var(--cor-hud-escudo);
  }
  .touch-shield-btn .resource-icon {
    margin-right: 0;
  }

  /* ===== 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, cartão de classe, nome do
     ranking, aba do placar do Sem fim, dropdown e os botões de ícone
     (som/música, 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(.class-card):not(.link-button):not(.ranking-nome):not(.horde-level-item):not(.toggle-switch):not(.horde-room-row):not(.monstruario-row):not(.talent-node):not(.horde-endless-tab),
  #gameOverOverlay button,
  #waitingOverlay button:not(.botao-nu),
  #btnBotClassConfirm,
  .class-picker-confirm,
  .class-picker-toggle,
  #btnAuthSubmit,
  #btnResetSubmit,
  #btnEnterFullscreen,
  #hordeRoomsListActions button,
  #btnHordeRoomsJoinBack,
  #btnHordeRoomsJoinConfirm,
  #talentResetConfirmActions button,
  #hordeLobbyTalentsBox #btnTalentTreeReset,
  #hordeLobbyTalentsBox #btnTalentTreeRetry,
  #btnPauseLeave {
    position: relative;
    isolation: isolate;
    border: none;
    border-radius: 0;
    clip-path: var(--recorte-botao-pixel);
  }
  #menu button:not(.class-card):not(.link-button):not(.ranking-nome):not(.horde-level-item):not(.toggle-switch):not(.horde-room-row):not(.monstruario-row):not(.talent-node):not(.horde-endless-tab)::before,
  #gameOverOverlay button::before,
  #waitingOverlay button:not(.botao-nu)::before,
  #btnBotClassConfirm::before,
  .class-picker-confirm::before,
  .class-picker-toggle::before,
  #btnAuthSubmit::before,
  #btnResetSubmit::before,
  #btnEnterFullscreen::before,
  #hordeRoomsListActions button::before,
  #btnHordeRoomsJoinBack::before,
  #btnHordeRoomsJoinConfirm::before,
  #talentResetConfirmActions button::before,
  #hordeLobbyTalentsBox #btnTalentTreeReset::before,
  #hordeLobbyTalentsBox #btnTalentTreeRetry::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(.class-card):not(.link-button):not(.ranking-nome):not(.horde-level-item):not(.toggle-switch):not(.horde-room-row):not(.monstruario-row):not(.talent-node):not(.horde-endless-tab)::after,
  #gameOverOverlay button::after,
  #waitingOverlay button:not(.botao-nu)::after,
  #btnBotClassConfirm::after,
  .class-picker-confirm::after,
  .class-picker-toggle::after,
  #btnAuthSubmit::after,
  #btnResetSubmit::after,
  #btnEnterFullscreen::after,
  #hordeRoomsListActions button::after,
  #btnHordeRoomsJoinBack::after,
  #btnHordeRoomsJoinConfirm::after,
  #talentResetConfirmActions button::after,
  #hordeLobbyTalentsBox #btnTalentTreeReset::after,
  #hordeLobbyTalentsBox #btnTalentTreeRetry::after,
  #btnPauseLeave::after {
    content: '';
    position: absolute;
    inset: var(--espessura-borda-botao);
    z-index: -1;
    background: inherit;
    clip-path: var(--recorte-botao-pixel);
  }
  /* "Menu inicial" era o único botão de ação sem fundo próprio (transparente,
     com borda de 2px marcando a hierarquia). Com a moldura, fundo
     transparente deixaria o tom escuro do ::before aparecer no miolo e o
     botão viraria um bloco escuro — então ele ganha uma superfície, e a
     diferença para os botões principais passa a ser a cor. */
  #gameOverOverlay button#btnBackToMenu {
    background: var(--cor-superficie);
    border: none;
  }
  #gameOverOverlay button#btnBackToMenu:hover {
    background: var(--cor-superficie-hover);
  }

  /* ===== Campos, dropdowns e cartões de classe 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,
  .dropdown-toggle,
  .class-card,
  .horde-level-item,
  #hordeRoomsVisibilityGroup {
    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
     dropdowns da modal de treino são posicionados em coordenada de viewport
     (dropdownPosition.js), então abririam no lugar errado e ainda seriam
     cortados na borda da modal. 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, campos e cartões de classe. */
  .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. Três blocos: o
     botão do menu, a modal de escolha (classe + solo/online) e o HUD da
     partida, que substitui o HUD de duas colunas do duelo. O botão só troca a
     tela inicial para a visão "Defesa da base" (#menuViewHorde) — não abre
     mais modal nenhuma. */

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

  /* A modal de escolha de classe da defesa é a **mesma** peça do 1x1
     (`.class-picker-overlay` + `.class-picker` no index.html): tamanho,
     cabeçalho, botão de fechar, divisão lista/preview e todo o
     comportamento em modo touch vêm de lá. Aqui sobra só o que é próprio
     desta tela. */

  /* ===== Navegador de salas (lista pública + criar sala + entrar com código) =====
     A lista pública é um painel dentro de #menuViewHorde, mesmo padrão de
     #rankingPanel/.spectator-panel/#talentTreePanel. Criar sala e entrar com
     código abrem modais próprias por cima dela
     (#hordeRoomsCreateOverlay/#hordeRoomsJoinOverlay), mesmo esquema de
     #talentResetConfirmOverlay (classe `.visible`, alternada via JS em
     hordeRoomBrowser.js). */
  #hordeRoomsPanel {
    display: flex;
    flex-direction: column;
    gap: 16px;
    width: 420px;
    height: 100%;
    max-width: 100%;
    max-height: min(560px, calc(var(--altura-tela) - 96px));
    box-sizing: border-box;
    padding: 16px 24px 24px;
    background: var(--cor-painel-pedra-veu);
    border-radius: 0;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
    overflow-y: auto;
    animation: entrada-fade 0.5s ease 0.12s both;
  }
  #hordeRoomsListView {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 14px;
    height: 100%;
  }
  #hordeRoomsCreateOverlay,
  #hordeRoomsJoinOverlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    z-index: 21;
  }
  #hordeRoomsCreateOverlay.visible,
  #hordeRoomsJoinOverlay.visible {
    display: flex;
    animation: overlay-fade 0.15s ease;
  }
  #hordeRoomsCreateOverlay .modal,
  #hordeRoomsJoinOverlay .modal {
    /* `.modal` genérica não declara `position`, então sem isto o `✕`
       flutuante (`position: absolute`) sobe até o ancestral posicionado
       seguinte — `#hordeRoomsCreateOverlay`, que é `fixed; inset: 0` — e
       cola no canto da **janela**, atrás do botão de som. Mesma regra que
       `.class-picker` já declara para o botão equivalente da modal de
       classe. */
    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;
  }
  /* Mais larga que as outras modais do navegador de salas: com 5 colunas
     (campanha + Sem fim), 380px força "Vença o nível N" e "Em
     desenvolvimento" a espremer o quadrado abaixo do que o texto precisa —
     a grade extrapolava a largura do miolo e ficava cortada na borda direita
     da modal (`#hordeRoomsLevelList` tem `overflow: visible` de propósito,
     ver o comentário dela, então não rolava: só vazava). 560px (e não mais
     480px) porque o clique no quadrado agora cria a sala na hora — sem botão
     de confirmar embaixo, o quadrado é o único alvo de toque da modal e
     precisa ser grande o bastante para isso. */
  #hordeRoomsCreateOverlay .modal {
    width: 560px;
  }
  .horde-rooms-view-title {
    margin: 0;
    font-size: 22px;
    text-align: center;
  }
  /* Piso e teto de altura: a lista é a única parte da modal que muda de
     tamanho com a resposta da API, então sem o piso a modal encolhia até
     virar uma faixa fina quando não havia sala aberta (só a linha de "nenhuma
     sala") e pulava de altura a cada poll. O piso é a altura de 3 salas
     (3 x 40px + 2 de vão), a mesma do skeleton — assim a modal já abre no
     tamanho que vai ter depois, sem salto. */
  #hordeRoomsList {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 8px;
    height: 100%;
    min-height: 136px;
    overflow-y: auto;
  }
  /* Com salas na lista o alinhamento é do topo; centralizado é só para a
     mensagem de lista vazia não ficar colada no topo do vão reservado. */
  #hordeRoomsList:has(.horde-room-row) {
    justify-content: flex-start;
  }

  /* ===== Monstruário (ficha dos inimigos da Defesa da base) =====
     Duas colunas dentro de #menuViewMonstruario, mesmo material dos painéis de
     #menuViewHorde (#hordeRoomsPanel/#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. */
  #monstruarioListPanel,
  #monstruarioDetailPanel {
    display: flex;
    flex-direction: column;
    gap: 12px;
    height: 100%;
    max-width: 100%;
    max-height: min(560px, calc(var(--altura-tela) - 96px));
    box-sizing: border-box;
    padding: 16px 24px 24px;
    background: var(--cor-painel-pedra-veu);
    border-radius: 0;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
  }
  #monstruarioListPanel {
    width: 360px;
    /* Só a lista rola (o título é cabeçalho e fica sempre à vista) — mesma
       corrente de `flex: 1` + `min-height: 0` de #talentTreePanel. */
    overflow: hidden;
    animation: entrada-fade 0.5s ease 0.12s both;
  }
  #monstruarioDetailPanel {
    width: 420px;
    align-items: center;
    text-align: center;
    overflow-y: auto;
    animation: entrada-fade 0.5s ease 0.2s both;
  }
  #monstruarioList {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }
  /* Linha da lista: miniatura + 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. */
  .monstruario-row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    box-sizing: border-box;
    padding: 8px 10px;
    border: 1px solid transparent;
    background: var(--cor-superficie-elevada);
    font-family: inherit;
    font-size: inherit;
    text-align: left;
    color: var(--cor-texto);
    cursor: pointer;
  }
  .monstruario-row:hover {
    border-color: var(--cor-texto-fraco);
  }
  /* O selecionado é marcado pela cor da identidade, como o botão da visão
     ativa em #menuNav — não por um fundo cheio, que competiria com a cor do
     próprio bicho na miniatura ao lado. */
  .monstruario-row-ativa,
  .monstruario-row-ativa:hover {
    border-color: var(--cor-vermelho-destaque);
  }
  .monstruario-mini {
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    /* Sem o fundo da regra genérica de canvas: a miniatura é o bicho recortado
       sobre o painel, não um quadro com moldura. */
    background: transparent;
  }
  .monstruario-row-nome {
    min-width: 0;
  }
  .monstruario-retrato {
    width: 132px;
    height: 132px;
    flex-shrink: 0;
    background: transparent;
  }
  .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. */
  .monstruario-atributos {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 4px 12px;
    width: 100%;
    margin: 0;
    font-size: 13px;
    text-align: left;
  }
  .monstruario-atributos dt {
    color: var(--cor-texto-fraco);
    text-transform: uppercase;
    font-size: 11px;
    letter-spacing: 0.05em;
    align-self: center;
  }
  .monstruario-atributos dd {
    margin: 0;
  }
  .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. */
  #relicarioListPanel {
    display: flex;
    flex-direction: column;
    gap: 12px;
    height: 100%;
    width: 1040px;
    max-width: 100%;
    max-height: min(620px, calc(var(--altura-tela) - 96px));
    box-sizing: border-box;
    padding: 16px 24px 24px;
    background: var(--cor-painel-pedra-veu);
    border-radius: 0;
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra);
    /* Só a lista rola (o título é cabeçalho e fica sempre à vista) — mesma
       corrente de `flex: 1` + `min-height: 0` de #monstruarioListPanel. */
    overflow: hidden;
    animation: entrada-fade 0.5s ease 0.12s both;
  }
  #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;
    gap: 6px 12px;
    flex: 1;
    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: 10px 2px 2px;
    border-bottom: 1px solid 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. */
    box-shadow: inset 3px 0 0 var(--cor-raridade);
  }
  .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;
    /* Sem o fundo da regra genérica de canvas: o ícone é o item recortado sobre
       o painel, não um quadro com moldura. */
    background: transparent;
  }
  .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);
  }
  /* ===== 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");
  }
  /* A linha inteira é o botão de entrar (não um "Entrar" pequeno no canto):
     no celular o alvo de toque passa a ser a sala toda, e no teclado ela
     entra na ordem de tabulação de graça. */
  .horde-room-row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    box-sizing: border-box;
    padding: 16px 12px;
    border: none;
    font-family: inherit;
    font-size: inherit;
    text-align: left;
    color: var(--cor-texto);
  }
  /* `:not(.skeleton-row)` evita que este fundo sólido vença o gradiente
     animado de `.skeleton-row` (mesma especificidade, mas este bloco vem
     depois no arquivo — sem o `:not`, o shimmer nunca apareceria). */
  /* A textura do nível é o fundo da linha (ver "Textura de arena" acima), com
     o véu por cima para o nome e o dono continuarem legíveis; a cor chapada
     fica embaixo como base — é ela que aparece sozinha se a linha vier sem
     classe de textura. Por isso `background-color`/`background-image`
     separados em vez do atalho `background`: o atalho no hover apagaria a
     textura junto. */
  .horde-room-row:not(.skeleton-row) {
    background-color: var(--cor-painel-pedra-veu);
    background-image:
      linear-gradient(var(--veu-arena, transparent) 0 0),
      var(--textura-arena, none);
    background-size: 80px 80px;
    background-repeat: repeat;
    box-shadow: var(--sombra-pedra-pequena);
    cursor: pointer;
  }
  .horde-room-row:not(.skeleton-row):hover {
    background-color: var(--cor-superficie-hover);
    --veu-arena: var(--cor-veu-textura-arena-fraco);
  }
  .horde-room-row.skeleton-row {
    height: 40px;
  }
  .horde-room-info {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    gap: 2px;
  }
  .horde-room-nivel {
    font-size: 13px;
    color: var(--cor-texto);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .horde-room-dono {
    font-size: 11px;
    color: var(--cor-texto-fraco);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .horde-room-count {
    font-size: 12px;
    color: var(--cor-texto-fraco);
    white-space: nowrap;
  }
  /* Só a pista de que a linha é clicável — o botão é a linha inteira, então
     aqui não pode haver outro `<button>` dentro. */
  .horde-room-entrar {
    font-size: 12px;
    letter-spacing: 0.5px;
    color: var(--cor-azul-link);
    white-space: nowrap;
  }
  #hordeRoomsListActions {
    display: flex;
    gap: 10px;
  }
  #hordeRoomsListActions button {
    flex: 1 1 0;
    padding: 12px 24px;
    font-size: 16px;
    background: var(--cor-vermelho);
    color: var(--cor-branco);
  }
  #hordeRoomsListActions button:hover {
    background: var(--cor-vermelho-hover);
  }
  .horde-rooms-section-label {
    margin: 0 0 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--cor-texto-fraco);
  }
  /* A moldura em volta do bloco "Sala pública" existe pra amarrar rótulo +
     descrição + interruptor num objeto só: sem ela o interruptor lia como
     solto na modal, sem deixar claro o que ele liga/desliga. É a mesma
     escadinha dos quadrados de nível logo abaixo (o bloco `.campo-pixel/
     .horde-level-item` desenha; aqui só entram as duas cores), pra os dois
     objetos da modal lerem como a mesma família. O recuo horizontal de 12px
     é o mínimo que mantém o conteúdo dentro do miolo: 8px de canto + 2px de
     borda. */
  #hordeRoomsVisibilityGroup {
    margin-bottom: 10px;
    padding: 10px 12px;
    box-sizing: border-box;
    --cor-fundo-campo: var(--cor-superficie-alta);
    --cor-borda-campo: var(--cor-borda);
  }
  #hordeRoomsVisibilityGroup .horde-rooms-section-label {
    margin: 0;
  }
  /* 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`). */
  #hordeLobbyInfoRow {
    display: flex;
    flex-wrap: wrap;
    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;
    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. */
  #hordeRoomsLevelList,
  #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, dropdowns e cartões
     de classe 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;
  }
  /* Número, nome e cadeado ficam por cima do ::before da textura (que é
     posicionado, e sem isto pintaria em cima deles). */
  .horde-level-numero,
  .horde-level-nome,
  .horde-level-lock {
    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;
  }
  /* 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);
  }
  /* Excluído da regra genérica de button:hover (ver acima, mesmo tratamento
     de .class-card): sem "levantar" no hover, só a cor da borda muda. */
  .horde-level-item:hover {
    --cor-borda-campo: var(--cor-borda-clara);
  }
  /* 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. */
  /* 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 {
    outline: 2px solid var(--cor-ouro-suave);
    outline-offset: -2px;
    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);
  }

  /* ----- Navegador de salas em modo touch -----
     O painel (#hordeRoomsPanel) não é mais modal: rola como o resto de
     #menuViewHorde/#rankingPanel/.spectator-panel, sem precisar de altura
     fixa nem cabeçalho escondido. Sobra só o que é alvo de toque maior e a
     grade de níveis 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;
  }
  /* Alvo de toque: a linha inteira da sala é o botão de entrar (ver
     `renderRooms` em hordeRoomBrowser.js), então ela precisa dos ~44px
     recomendados. */
  body.touch-mode .horde-room-row {
    min-height: 44px;
  }

  /* Sala de espera cooperativa, dentro do overlay de espera do 1x1. 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 {
    display: none;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    width: min(92%, 620px);
    box-sizing: border-box;
    padding: clamp(10px, calc(0.025 * var(--largura-arena)), 20px);
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra);
  }
  #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;
    min-width: 200px;
    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`), como
     os cartões de classe em modo touch: 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 {
    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 sprite/í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, o retrato e a marca continuavam do tamanho de desktop dentro
     de um cartão que ficou bem menor, e transbordavam por cima do nome. */
  body.touch-mode.tela-baixa-deitada .horde-lobby-arte.class-sprite {
    --sprite-box: clamp(22px, calc(0.09 * var(--altura-tela)), 36px);
  }
  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ê", igual ao slot 0 do HUD do duelo (#nameP0): 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;
  }
  .horde-lobby-arte.class-sprite {
    --sprite-box: clamp(40px, calc(0.09 * var(--largura-arena)), 60px);
    --sprite-zoom: 2.2;
    --sprite-anchor: 0.42;
  }
  .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);
  }
  /* 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 também
     serve o 1x1 — então o padrão é escondido, e quem o mostra é a sala
     cooperativa. 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-node-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-pontos-livres] {
    position: relative;
  }
  #btnHordeLobbyTalentos[data-pontos-livres]::after {
    content: attr(data-pontos-livres);
    position: absolute;
    top: 2px;
    right: 2px;
    min-width: 14px;
    height: 14px;
    line-height: 14px;
    box-sizing: border-box;
    padding: 0 2px;
    font-size: 10px;
    text-align: center;
    color: var(--cor-texto-sobre-tier);
    background: var(--cor-ouro);
    animation: talento-selo-pisca 0.9s steps(1, end) infinite alternate;
  }
  /* 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 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 ----- */
  /* 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: none;
    position: absolute;
    top: 10px;
    right: 10px;
    width: 20%;
    height: auto;
    z-index: 11;
    pointer-events: none;
    image-rendering: pixelated;
    box-shadow: var(--sombra-pedra-pequena);
    /* Translúcido: ele cobre um canto da arena o tempo todo, e nesse modo a
       câmera anda — o que está por baixo dele continua sendo jogo. */
    opacity: 0.5;
  }
  body.horde-mode #minimap {
    display: block;
  }
  /* No modo defesa não existe "você × oponente": o HUD de duas colunas sai e
     entra o painel de baixo (vida da base + um cartão por jogador). */
  body.horde-mode #hud .hud-row {
    display: none;
  }
  /* Mesma natureza do `#hud` do duelo: overlay `inset: 0` sobre a arena, não
     altura de fluxo — gameScale.js dá a viewport quase inteira para a arena e
     não reserva espaço para HUD nenhum. Vida da base em cima (logo abaixo do
     indicador de onda, que ocupa o lugar do cronômetro) e os cartões dos
     jogadores embaixo. */
  #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;
    padding: 16px;
    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 ficava numa
     caixa própria no meio da tela, herdada do cronômetro do duelo) 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;
    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. */
    width: min(66%, 520px);
    padding: 6px 10px;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
  }
  /* O modo defesa não tem cronômetro: a onda virou um contador pequeno dentro
     da própria barra da base (`.horde-wave`), então a caixa central do duelo
     não tem mais o que mostrar aqui. Antes ela era reaproveitada para o texto
     da onda, ocupando o meio da tela com o tamanho de um relógio de partida. */
  body.horde-mode #matchTimer {
    display: none;
  }
  /* 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 do duelo: é 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: var(--cor-superficie-baixa);
    border: 1px solid var(--cor-pedra-borda);
    box-shadow: var(--sombra-pedra-afundada);
    overflow: hidden;
  }
  /* 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(50%, 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;
  }
  .horde-base-fill {
    height: 100%;
    width: 100%;
    background: var(--cor-sucesso);
    transition: width 0.12s linear;
  }
  .horde-base-fill.critica {
    background: var(--cor-vermelho-destaque);
  }
  .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. */
  #hordeItens {
    list-style: none;
    margin: 0;
    padding: 0;
    position: absolute;
    left: 16px;
    top: 25%;
    /* 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. */
    bottom: 20%;
    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;
  }
  /* 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);
  }

  /* Painel "Builds do grupo" (Tab segurado, ou o botão GRUPO em touch — ver
     public/js/hordeBuilds.js). O inventário do canto é só o do jogador local de
     propósito; este é a leitura do grupo inteiro, e por isso ele **não** fica na
     tela durante a luta: aparece enquanto a tecla está pressionada e some.

     `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 — soltar a tecla devolve os
     três cartões. */
  #hordeBuilds {
    position: absolute;
    inset: 2px;
    z-index: 13;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: clamp(4px, calc(0.012 * var(--largura-arena)), 12px);
    padding: 2%;
    box-sizing: border-box;
    background: var(--cor-fundo-arena-veu);
    /* A partida continua correndo por baixo: o painel é leitura, e engolir o
       clique do tiro faria a onda passar por cima de quem o abriu. */
    pointer-events: none;
  }
  #hordeBuilds[hidden] { display: none; }
  .horde-build-titulo {
    margin: 0;
    color: var(--cor-ouro);
    font-size: clamp(11px, calc(0.024 * var(--largura-arena)), 20px);
    text-transform: uppercase;
    letter-spacing: 0.08em;
  }
  /* Uma coluna por jogador, na ordem dos slots — a mesma dos cartões do HUD, do
     mini-mapa e das cores: procurar o companheiro numa ordem diferente da que o
     resto da tela usa custa mais que a leitura em si. */
  .horde-build-colunas {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: clamp(4px, calc(0.014 * var(--largura-arena)), 14px);
    /* Largura cheia, e não o tamanho do conteúdo: é ela que faz o quarto de
       largura de cada coluna (abaixo) ser um quarto **da arena**. Dimensionada
       pelo conteúdo, as porcentagens dos filhos se resolveriam contra uma
       largura que os próprios filhos decidem, e as colunas saíam estreitas
       demais para o nome do jogador caber. */
    width: 100%;
    max-height: 100%;
  }
  .horde-build-col {
    display: flex;
    flex-direction: column;
    gap: 4px;
    max-height: 100%;
    overflow: hidden;
    /* Cada coluna leva no máximo um quarto da largura, esteja o grupo com 1 ou
       com 4 jogadores: assim a coluna de quem joga sozinho não se espalha pela
       arena inteira, e a build de ninguém muda de largura quando um companheiro
       cai da partida. */
    width: calc((100% - 3 * clamp(4px, calc(0.014 * var(--largura-arena)), 14px)) / 4);
    padding: clamp(4px, calc(0.01 * var(--largura-arena)), 10px);
    box-sizing: border-box;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
  }
  /* 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). */
  .horde-build-nome {
    color: var(--slot-color, var(--cor-texto));
    font-size: clamp(9px, calc(0.018 * var(--largura-arena)), 15px);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .horde-build-col.local .horde-build-nome {
    color: var(--cor-ouro);
  }
  .horde-build-itens {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* A build cresce a partida inteira no Sem fim: o que não couber na altura
       da arena é recortado em vez de vazar pela borda de baixo. `min-height: 0`
       porque item de flex não encolhe abaixo do conteúdo sozinho — sem ele a
       coluna cresceria para fora do painel em vez de recortar. */
    flex: 1;
    min-height: 0;
    overflow: hidden;
  }
  /* Ícone + nome + quantidade. O nome vem escrito, e não num balão de hover
     como no inventário do canto: aqui há largura para ele, e em touch (onde o
     painel também abre) não existe hover nenhum. */
  .horde-build-item {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 1px 3px;
    background-image: linear-gradient(var(--cor-raridade-veu), var(--cor-raridade-veu));
    box-shadow: inset 3px 0 0 var(--cor-raridade);
  }
  .horde-build-item canvas {
    display: block;
    flex: none;
    width: clamp(10px, calc(0.02 * var(--largura-arena)), 16px);
    height: auto;
    image-rendering: pixelated;
  }
  .horde-build-item-nome {
    flex: 1;
    min-width: 0;
    color: var(--cor-texto);
    font-size: clamp(7px, calc(0.015 * var(--largura-arena)), 12px);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Dourado como a contagem do inventário e o excedente de power-up: é a mesma
     família de "acima do normal". */
  .horde-build-item-contagem {
    flex: none;
    color: var(--cor-ouro);
    font-size: clamp(7px, calc(0.015 * var(--largura-arena)), 12px);
    font-variant-numeric: tabular-nums;
  }
  /* Coluna sem item nenhum é estado escrito: vazia, ela leria como falha de
     carregamento — e na primeira onda ninguém tem item. */
  .horde-build-vazio {
    color: var(--cor-texto-medio);
    font-size: clamp(7px, calc(0.015 * var(--largura-arena)), 12px);
    font-style: italic;
  }

  #hordePlayers {
    list-style: none;
    margin: 0;
    padding: 0;
    width: 100%;
    display: flex;
    justify-content: center;
    gap: 8px;
  }
  /* 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;
    padding: 5px 8px;
    background: var(--cor-painel-pedra-veu);
    box-shadow: var(--sombra-pedra-pequena);
  }
  /* O cartão do jogador local é o único destacado — é o equivalente do nome
     dourado do slot 0 no HUD do duelo. Numa partida de 4 jogadores só o nome
     dourado não bastava para achar o próprio cartão no meio da luta, então o
     cartão inteiro é marcado: fundo de pedra "aquecido" e moldura dourada de
     2px por dentro (box-shadow inset, e não `border`, porque a largura do
     cartão é fixa em 1/4 da tela e uma borda de verdade empurraria o
     conteúdo). A moldura vem antes da sombra de pedra para o bisel continuar
     por cima dela. */
  .horde-player.local {
    background: var(--cor-painel-pedra-veu-local);
    box-shadow: inset 0 0 0 2px var(--cor-ouro), var(--sombra-pedra-pequena);
  }
  .horde-player.local .horde-player-name {
    color: var(--cor-ouro);
  }
  /* 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: ícone, nome, vida e cooldown. (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-icon,
  .horde-player.morto .horde-player-name,
  .horde-player.morto .hearts,
  .horde-player.morto .cooldown-bar {
    opacity: 0.55;
  }
  .horde-player.desconectado .horde-player-icon,
  .horde-player.desconectado .horde-player-name,
  .horde-player.desconectado .hearts,
  .horde-player.desconectado .cooldown-bar {
    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;
  }
  .horde-player-icon {
    display: inline-flex;
    color: var(--class-color, var(--cor-texto));
  }
  .horde-player-icon svg {
    width: 15px;
    height: 15px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
  }
  /* O nome 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, e é o que faz o cartão
     responder "quem é aquele ali" no meio da onda. Não é `--class-color`
     porque na Defesa da base a classe é única — a cor de classe é a mesma nos
     quatro cartões e não identifica ninguém. O cartão do jogador local ignora
     isso e continua dourado (`.horde-player.local` acima, especificidade
     maior): dourado é a marca de "você" na tela inteira, e o próprio jogador
     não precisa de cor para se achar. */
  .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 `.horde-player`/`.hearts` não têm
     classe própria fora desse modo — não pode vazar pro HUD do 1x1
     (`#hud .hearts`), que usa a mesma classe de caixa. */
  body.touch-mode.horde-mode .horde-player {
    gap: 1px;
    padding: 3px 5px;
  }
  body.touch-mode.horde-mode .horde-player-icon svg {
    width: 11px;
    height: 11px;
  }
  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. */
