portal_publico/docs/temas-sazonais/temas-sazonais.md

118 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Temas Sazonais
> Criado em 2026-09-15, seguindo o mesmo padrão das demais aplicações sem pacote Python dedicado (ver `CLAUDE.md` na raiz, "Documentação dividida por aplicação") — como o Halloween foi só o primeiro de possivelmente vários temas sazonais, faz mais sentido documentar isso como uma "aplicação" própria desde já do que deixar acumulando dentro do `CLAUDE.md` raiz. Este arquivo não é auto-carregado pelo Claude Code (não há pacote Python — o mecanismo é só frontend, `static/js`/`static/css`/templates) — leia manualmente ao mexer nisto. Ver `CLAUDE.md` na raiz para arquitetura geral/transversal do Portal.
Troca sazonal da marca P.I.D. (logo, ícone da sidebar/login, favicon, intro de abertura) por uma variante temática durante uma janela de datas — hoje dois: Halloween (mascote "vampiro", 16 a 31/10) e Aniversário (56 anos da De Paula Contadores, mascote com balões, só 15/10). O mecanismo (`pidIsHalloweenActive()`/`pidIsAniversarioActive()`/`seasonal-theme.js`) foi pensado pra ser genérico o bastante pra outro tema sazonal futuro reaproveitar o mesmo esqueleto sem reescrever nada — as duas janelas atuais nunca coincidem por desenho (15 vs 16-31/10), não por checagem explícita de conflito.
## Status atual
**Confirmado e programado** — revisão visual concluída pelo usuário. `PID_HALLOWEEN_PREVIEW_FORCE`/`PID_ANIVERSARIO_PREVIEW_FORCE = false` em `static/js/seasonal-theme.js` (deixados de propósito nas constantes, documentando a decisão, em vez de removidos): daqui pra frente só a data decide se cada tema está ativo, sem nenhuma ação manual — nem pra ligar, nem pra desligar depois. Nada mais precisa mudar de código pra isso valer.
O Halloween só começa em 16/10 (antes: outubro inteiro) desde que o tema de Aniversário foi criado — 15/10 é o aniversário da empresa e passou a ter tema próprio, então os dois janelas foram desenhadas pra nunca disputar o mesmo dia.
**"56 ANOS" é específico deste ano** — a pílula do lockup horizontal, o texto "56" da intro e os 3 arquivos `static/img/pid-aniversario-*.svg` precisam ser atualizados manualmente todo ano (novo número, nova pasta `pid-marca-<N>` de referência) — ver `leiame.md` original: "é uma marca temporária". Isso não é automatizado de propósito (não vale a complexidade pra um ajuste anual de um texto).
## Origem dos assets (Halloween)
Mascote "vampiro" (a mesma marca P.I.D. fantasiada de Drácula — capa com gola erguida e forro vinho, lápis de olho, dois dentinhos) desenhada em `C:\Users\Depaula\Documents\Logos\P.I.D. Logo Design Halloween\pid-marca-halloween\` (SVGs de referência + `leiame.md`, fora do repositório, mesmo papel de `pid-marca/` pra marca "P.I.D." normal — ver "Logos em `static/img/`" em `docs/identidade-visual/identidade-visual.md`) e a intro de abertura em `...\P.I.D. Logo Design Halloween\Logo intro animation Halloween\export-halloween\pid-intro-halloween.html` (mesmo formato de bundle "Design Canvas" do `pid-intro-escuro.html` original — ver `CLAUDE.md` raiz, "Animação de intro do login" → "Origem do arquivo"; a composição (`PidHalloween`, cenas `Rise`/`Scare`/`Settle`/`Wordmark`, 1,3s/1,5s/1,4s/1,8s) foi extraída do bundle com o mesmo processo ad-hoc (gzip+base64 por uuid) e portada fielmente pra vanilla JS/CSS). Cópias dos SVGs de referência (não usadas diretamente pela UI, que injeta o markup inline — ver abaixo) vivem em `static/img/pid-halloween-favicon.svg`/`pid-halloween-icone-escuro.svg`/`pid-halloween-logo-horizontal-escuro.svg`.
## `static/js/seasonal-theme.js`
Incluído em toda página (mesmo padrão de `confirm-modal.js`, logo depois dele) — concentra tudo que é compartilhado entre login e sidebar:
- `pidIsHalloweenActive()`/`pidIsAniversarioActive()` — a checagem de data de cada tema (mais o respectivo preview force e o opt-out do usuário, ver "Opt-out do usuário" abaixo — checado **antes** de tudo, prioridade máxima).
- `pidApplySeasonalFavicon(iconFile)` — troca `<link rel="icon">` de `pid-icone-escuro.svg` pro arquivo passado (`pid-halloween-icone-escuro.svg` ou `pid-aniversario-icone-escuro.svg`) via substring no `href` (funciona com qualquer prefixo de `STATIC_URL`). **Não** usa o favicon dedicado de cada tema (`pid-halloween-favicon.svg`/`pid-aniversario-favicon.svg`) — são variantes simplificadas (sem capa/gola ou sem balões, cores diferentes) que destoam do resto da marca sazonal na aba do navegador (bug relatado pelo usuário na revisão visual do Halloween, aplicado por precedente ao Aniversário); mesmo padrão do P.I.D. normal, cujo favicon também usa `pid-icone-escuro.svg` em vez do `pid-favicon.svg` dedicado.
- `pidApplyVampireIcon(svg)`/`pidApplyVampireFull(svg)` — substituem o `viewBox`+`innerHTML` de um `<svg>` existente (ícone 64×64 → vampiro 76×80; assinatura horizontal 300×80 → 320×96) pelo markup do vampiro, com dois grupos de rosto sempre presentes (`.pid-vamp-face--friendly`/`--scare`, cruzando opacidade) dentro de um `<g class="pid-vamp-icon-rig">` — mesma ideia de "dois estados sempre no DOM, alternando opacidade" já usada pelo crossfade da sidebar (`sidebar__logo--full`/`--icon`).
- `pidTriggerVampireScare(root)` — dispara a animação de susto (`.is-scaring`, ver `base.css`) em todo `.pid-vamp-icon-rig` dentro de `root`.
- `pidApplyAniversarioIcon(svg)`/`pidApplyAniversarioFull(svg)` — substituem só o `innerHTML` (viewBox não muda, ao contrário do vampiro) pelo markup com balões; preservam os dois `.pid-icon-eye` do ícone normal (a cara não muda nesse tema), então a piscada ao clicar continua funcionando sem nenhum código de interação dedicado.
## Troca do ícone/assinatura (sidebar + login)
Como o markup do ícone P.I.D. normal já é idêntico (copiado inline) nas 13 páginas-shell + login, a troca é feita **em runtime**, não editando cada template — `sidebar-brand.js`/`login-logo-blink.js` chamam `pidApplyVampireIcon`/`pidApplyVampireFull`/`pidApplyAniversarioIcon`/`pidApplyAniversarioFull`/`pidApplySeasonalFavicon` no próprio `DOMContentLoaded`, escolhendo o tema (`pidIsHalloweenActive()` checado primeiro, `pidIsAniversarioActive()` senão — nunca os dois juntos, mas Halloween ganha prioridade de código por clareza). Isso também é o que torna o "retorno" ao P.I.D. normal automático fora de qualquer janela: o innerHTML original nunca é tocado no arquivo, só sobrescrito em memória a cada carregamento de página enquanto alguma janela estiver ativa.
## Susto ao clicar (substitui a piscada de olhos)
Fora do Halloween, clicar na logo pisca os olhos (`.pid-icon-eye`/`is-blinking`, comportamento original, intocado). Durante o Halloween, o mesmo clique dispara `pidTriggerVampireScare()` — `.pid-vamp-icon-rig.is-scaring` (`base.css`, logo depois do bloco de piscada) toca 4 animações de 900ms em paralelo: `pidVampFaceOut`/`pidVampFaceIn` (crossfade rosto amigável → raivoso e de volta), `pidVampCapeBurst` (a capa "estufa" com scale+rotate) e `pidVampShake` (o ícone inteiro treme e ganha um brilho vermelho via `filter:drop-shadow`) — tudo CSS puro (sem `requestAnimationFrame`), disparado só pela troca de classe, mesmo padrão da piscada original. Na sidebar, a navegação pra `portal.html` é adiada `PID_HALLOWEEN_SCARE_MS - 100` (800ms, contra 260ms da piscada normal) pra dar tempo de ver o susto antes de trocar de página; no login não há navegação, é só o toque decorativo.
## Intro de abertura pós-login (`static/css/login-intro-halloween.css` + `static/js/login-intro-halloween.js`)
Variante completa de `login-intro.css`/`.js`, com seu próprio overlay (`#login-intro-halloween`, mesma estrutura de `#login-intro` em `index.html`, hidden por padrão) — reaproveita as funções de easing/animate/clamp/corner-target já globais de `login-intro.js` (`PID_INTRO_EASING`, `pidIntroAnimate`, `pidIntroClamp`, `pidIntroCornerTarget`) em vez de duplicá-las, só a coreografia é própria. `auth.js` escolhe qual intro chamar no sucesso do login (`pidPlayLoginIntroHalloween` vs `pidPlayLoginIntro`) com base em `pidIsHalloweenActive()`.
- **Cenas** (`PID_INTRO_H_CUES`/`PID_INTRO_H_TOTAL` — `Rise:0, Scare:1.3, Settle:2.8, Wordmark:4.2, Fly:6.0`, total `6.9`): `Rise` (a figura surge do escuro com a capa erguida cobrindo o rosto, só os olhos semicerrados espiando — `squint`), `Scare` (a capa se abre numa investida — `lunge`/`push`/tremida —, revelando o rosto raivoso com sobrancelhas e presas, mais um flash vermelho de tela inteira via `#login-intro-h-flash`), `Settle` (a capa volta a fechar, o rosto cruza pro amigável de sempre), `Wordmark` (a figura desliza, "P.I.D." aparece na fonte "Creepster" com régua dourado→vinho e a tagline). As primeiras 4 cenas (`Rise`/`Scare`/`Settle`/`Wordmark`, somando 6,0s) são a composição original **praticamente 1:1** — mesmas fórmulas/janelas de tempo de `pid-intro-halloween.html`, sem reproporcionar nada (ao contrário da intro normal, que teve que comprimir as cenas — aqui não foi pedido acelerar).
- **`Fly` (~0,9s, acrescentada, não existe no arquivo original)**: a composição-fonte foi pensada como vídeo solto (tudo esmaece junto no final); pra se integrar ao fluxo do Portal (a mesma razão documentada no `CLAUDE.md` raiz sobre o "voo" da intro normal), foi adicionada uma cena final equivalente à cena "Portal" da intro normal — a wordmark esmaece, o ícone encolhe e voa até o canto real da janela (mesmo `pidIntroCornerTarget()`, reaproveitado) e o stage inteiro esmaece por cima do fim do voo.
- **Reescala de tamanho**: a composição original foi autorada pra um ícone de 460px (uso como hero de vídeo); aqui o ícone roda a 200px (`PID_INTRO_H_ICON_K = 200/460`), então todo deslocamento em "px" (a subida inicial, a tremida, o desvio vertical da investida, o blur do brilho) é multiplicado por esse fator — as transformações internas do SVG (a capa abrindo, o ângulo da aba) continuam em unidades do próprio `viewBox` (76×80, igual ao das duas variantes vampiro em `static/img/`) e não precisam de reescala.
- **Tema claro**: mesmo raciocínio da intro normal — fundo acompanha `--bg-canvas`, texto do wordmark vira escuro (`#3a1620`) pra continuar legível; o ícone não precisa de override (roxo/vinho com contraste de sobra nos dois fundos).
## Teias de aranha de canto (`static/css/halloween-cobweb.css`)
Decoração de canto pedida pelo usuário depois de revisar um protótipo isolado publicado como Artifact ("Cantos Assombrados", fora do repositório — comparava 2 vs 4 cantos e aranha andando vs parada antes de aplicar de verdade). Só em **2 cantos** (superior-esquerdo/inferior-direito, os mesmos marcados por ele num print da tela real) e só em **duas páginas**: `index.html` (login) e `portal.html` (tela Principal) — nenhum outro shell. Cada teia é um quarto de círculo (8 fios retos + 5 anéis em espiral levemente arqueados — `<path class="pw-ring">` com curvas `Q` em vez de polilinhas retas, pra não parecer um desenho geométrico cru — mais 1 fio "perdido" bem mais comprido que os outros, se estendendo pra fora do canto; todas as coordenadas calculadas por trigonometria, não à mão) com uma aranha (`<g class="pw-spider">`, corpo+cabeça+8 pernas) que anda pra fora ao longo de um dos fios e volta (`@keyframes pwWalk`, 8s, com pausas nas pontas) enquanto as pernas mexem continuamente (`pwLegsWiggle`) — tudo CSS puro, sem `requestAnimationFrame`. **Desenho da teia refeito numa rodada seguinte** a partir de uma referência visual que o usuário anexou (queria "algo menos bruto", mais profissional, "sem parecer que só foi colado um PNG"): a versão original tinha só 6 fios retos + 3 anéis poligonais (linhas retas entre pontos, sem curva nenhuma) — a v2 acima é mais densa perto do canto e mais esparsa longe dele, com as curvas arqueadas simulando a espiral de captura de uma teia real.
- **Markup só existe em memória**: igual ao ícone vampiro, o HTML da teia não fica hardcoded no template — `PID_COBWEB_SVG` (string, `seasonal-theme.js`) é injetado dentro de um container já presente e `hidden` (`#pw-cobwebs-login`/`#pw-cobwebs-portal`) por `pidApplyHalloweenCobwebs(containerId)`, chamada por `login-logo-blink.js`/`sidebar-brand.js` no mesmo `if (halloween)` que já troca o ícone — fora de outubro, o container continua vazio e `hidden`.
- **Ancoragem diferente em cada página**: no login, a teia é filha direta de `.login-page` (ela já cobre `min-height:100vh`); na Principal, filha de `.main-content` (não o viewport, porque a sidebar já é sólida e cobriria o canto esquerdo de qualquer jeito) — ancorar ali garante que a teia nunca fica escondida atrás dela, com ou sem a sidebar colapsada. As duas usam a mesma regra base `.pw-corners{position:absolute;inset:0}`.
- **Tamanho**: menor na Principal que no login (pedido explícito do usuário, "evitando cobrir muito as informações que constam na página" — a tela Principal tem cards/widgets perto das bordas, o login só tem o card centralizado), mas aumentado em duas rodadas seguintes ("ficaram muito pequenas", nas duas telas): `clamp(220px,22vw,320px)` no login vs `clamp(160px,15vw,230px)` na Principal (valores atuais).
- `z-index:-1` (não `pointer-events` sozinho) garante que a teia fica atrás do conteúdo real, não só sem receber clique.
- **Bug real — a teia do login não aparecia de jeito nenhum**: `z-index:-1` só funciona como esperado dentro de um contexto de empilhamento isolado; sem isso, um elemento negativo escapa pro contexto raiz da página e qualquer irmão estático com fundo opaco pinta por cima dele (é exatamente o caso de `.login-page`, que tem fundo cobrindo a tela inteira) — a teia ficava sempre atrás desse fundo, nunca visível. Na Principal o mesmo problema existia, só que passava despercebido porque `.main-content` não tem fundo opaco cobrindo tudo (a teia só aparecia nos vãos sem conteúdo por cima, "sumindo" só parcialmente/nos cantos exatos). Corrigido com `isolation:isolate` em `.login-page` (`login.css`) e em `.main-content` (`layout.css`, junto do `position:relative` já existente) — isola um contexto de empilhamento próprio pra cada uma, e a teia (filha direta, `z-index:-1`) passa a ficar corretamente atrás do fundo e na frente do conteúdo real nas duas.
- **Canto superior-esquerdo da Principal escondido atrás da topbar**: a `.topbar` (`position:sticky`, `z-index:20`, fundo opaco — `layout.css`) cobria a metade de cima da teia desse canto. `.pw-corners--portal .pw-corner--tl` ganhou `top: var(--topbar-height)` (mesmo token que a topbar já usa) — só esse canto, o inferior-direito e os dois do login não têm nada cobrindo por cima e continuam em `top:0`/`bottom:0`.
- **Contraste por tema**: a aranha (`#17111d`) é quase a mesma cor do fundo escuro padrão (`--bg-canvas:#121017`) e sumia, sobrando só o olho vermelho visível — `.pw-spider` ganhou `filter:drop-shadow(...)` branco (dois drop-shadows, um mais fechado e um mais espalhado) só pra dar contraste no tema escuro. No tema claro é o oposto: a teia (creme, pensada pro fundo escuro) sumiria no fundo branco — `:root[data-theme="light"] .pw-strands`/`.pw-strand-stray`/`.pw-ring` viram um cinza-chumbo escuro semitransparente, e o filtro branco da aranha é desligado (`filter:none`, já que aranha escura sobre fundo claro já contrasta sozinha e o brilho branco só seria ruído ali).
- **Clicar mata a aranha** (pedido explícito do usuário): a aranha (única parte da teia com `pointer-events:auto`, o resto continua `pointer-events:none`, decorativo) tem um listener `{once:true}` (`pidApplyHalloweenCobwebs()`) que, ao clique, adiciona `.is-dying` (`pwSpiderDie`, 950ms — cai girando, opacity até 0, `animation-fill-mode:forwards` mantém o estado final) e `.is-rising` num `<g class="pw-skull">` irmão (`pwSkullRise`, 1300ms — sobe, aparece e some, como a "alma saindo do corpo" enquanto o corpo cai); as pernas ganham sua própria `pwLegsDie` (encolhem/giram) via `.pw-spider.is-dying .pw-legs`, sobrescrevendo o `pwLegsWiggle` contínuo por especificidade.
- **O óbito é persistido** (pedido explícito do usuário, numa rodada seguinte — "guardar que a aranha foi morta na memória... até executar o comando tab+b que reseta a aranha"): cada aranha tem um id próprio (`${containerId}__tl`/`__br` — 4 ids possíveis, 2 cantos × 2 páginas) guardado num array JSON em `localStorage` (`pid_seasonal_dead_spiders`, `pidMarkSpiderDead()`/`pidGetDeadSpiders()`), mesmo padrão de armazenamento das outras preferências sazonais (nunca migrado pro banco). No carregamento seguinte, `pidApplyHalloweenCobwebs()` checa essa lista e, pra aranha já morta, aplica `.is-dead` direto (`animation:none;opacity:0`, sem tocar `pwSpiderDie`/a caveira de novo — não faz sentido reprisar a queda a cada F5) em vez de ligar o clique.
- **Atalho Tab+B reseta todas as aranhas** (`pidResetDeadSpiders()`, limpa o `localStorage` e recarrega a página): segurar Tab (sem `preventDefault`, a navegação por foco continua normal) e apertar B enquanto ele está pressionado. Listener global (`seasonal-theme.js`, incluído em toda página), só tem efeito de verdade nas duas páginas com teia e só quando há alguma aranha morta salva.
- **Bug real — aranha caindo pra cima e caveira de cabeça pra baixo no canto inferior-direito**: o canto `--br` originalmente era só o mesmo desenho do `--tl` com `transform:scale(-1,-1)` no `<div class="pw-corner">` inteiro (jeito mais simples de reaproveitar o mesmo markup pro canto oposto). Só que `scale(-1,-1)` num ancestral vira **tudo** de cabeça pra baixo, inclusive a "aranha"/"caveira" (que têm uma orientação certa — pernas, olhos, queixo) e a direção das animações de queda/subida (`pwSpiderDie`/`pwSkullRise`, que "sobem" em vez de "descer" depois de espelhadas). Corrigido separando as duas coisas: só a teia em si (`.pw-strands`, sem opinião de "lado certo pra cima") continua sendo espelhada, agora com `transform-box:view-box; transform-origin:100px 100px` (o centro do viewBox autorado, não a caixa delimitadora dos fios — importante porque o fio "perdido" mais comprido deslocaria esse centro se fosse `fill-box`); a aranha e a caveira passaram a usar variáveis CSS (`--rx`/`--ry`/`--rr`/`--ox`/`--oy` na aranha, `--sx`/`--sy` na caveira) pra descanso/passeio/posição, redefinidas com os valores espelhados (`200-x`, `200-y`, `+180deg`) só pra `.pw-corner--br`, **sem nenhum `scale`/`rotate` extra nelas** — ficam sempre em pé, e as `@keyframes` de queda/subida (agora usando `calc(var(--r*) + valor)`) sempre significam "pra baixo"/"pra cima" de verdade na tela, nos dois cantos.
## Tema "Aniversário" (56 anos da De Paula Contadores, só 15/10)
Segundo tema sazonal, criado numa rodada seguinte à do Halloween — mesmo personagem P.I.D., "o acento é o número" (ver `leiame.md` original abaixo): ganha três balões (azul/âmbar/rosa) segurados por um braço saindo da lateral direita do corpo. A cara de **repouso** não muda (sem susto como o vampiro) — mas o clique na logo ganhou uma animação própria numa rodada seguinte (ver "Clique de festa" abaixo), e o tema também ganhou um bolo com velinhas na tela Principal (ver "Bolo de aniversário" abaixo).
### Origem dos assets
Desenhado em `C:\Users\Depaula\Documents\Logos\P.I.D. Logo Design Aniversário\pid-marca-56\` (SVGs de referência + `leiame.md`, fora do repositório, mesmo papel de `pid-marca-halloween/` pro Halloween) e a intro de abertura em `...\P.I.D. Logo Design Aniversário\Logo intro animation\export-56\pid-56-anos.html` (mesmo formato de bundle "Design Canvas", desta vez um componente React/JSX — `pid-56.jsx` — em vez de JS puro; extraído do bundle com o mesmo processo ad-hoc de gzip+base64 por uuid já usado no Halloween/intro normal e portado fielmente pra vanilla JS/CSS em `login-intro-aniversario.js`). Cópias dos SVGs de referência (não usadas diretamente pela UI, que injeta o markup inline) vivem em `static/img/pid-aniversario-favicon.svg`/`pid-aniversario-icone-escuro.svg`/`pid-aniversario-logo-horizontal-escuro.svg`; a logo da De Paula Contadores usada na intro (imagem `<img>`, não SVG) vive em `static/img/pid-aniversario-depaula-wordmark.png`.
**Exceção deliberada à separação logo cursivo vs "P.I.D."** (ver "Logos em `static/img/`" no `CLAUDE.md` raiz): a intro de abertura deste tema mostra a logo cursiva oficial da De Paula Contadores (não a marca "P.I.D.") ao lado do texto "Parabéns" — normalmente essa logo é reservada só a documentos/PDFs gerados pela aplicação, nunca à UI do Portal. Aqui a exceção é intencional: a composição celebra o aniversário da **empresa**, não do sistema, e o próprio usuário forneceu essa imagem como parte do design de referência (não é uma migração de logo decidida unilateralmente).
**"56 ANOS" é específico deste ano** — ver "Status atual" no topo deste arquivo.
### Ícone/assinatura (sidebar + login)
`pidApplyAniversarioIcon(svg)`/`pidApplyAniversarioFull(svg)` (`seasonal-theme.js`) substituem só o `innerHTML` — ao contrário do vampiro, a caixa (`viewBox`) não muda (64×64/300×80, iguais ao P.I.D. normal), já que os balões cabem na mesma composição sem precisar de mais espaço. Os dois `.pid-icon-eye` do ícone normal são preservados dentro do markup novo, então a piscada ao clicar (`sidebar-brand.js`/`login-logo-blink.js`) continua funcionando — inclusive **junto** com o clique de festa (ver abaixo), não em vez dele.
Estrutura em dois `<g>` aninhados — `<g class="pid-aniv-hop">` (recebe o "pulo" via CSS, sem nenhum `transform` de atributo XML) por fora, `<g class="pid-aniv-icon-rig" transform="...">` (o posicionamento estático dentro da caixa) por dentro — pelo mesmo motivo já documentado pra `.pid-icon-eye`/`.pid-vamp-icon-rig`: um `transform` de CSS aplicado ao MESMO elemento que já tem um `transform` de atributo substitui o atributo inteiro, perdendo a posição.
### Clique de festa (pulo + confete + língua-de-sogra), pedido numa rodada seguinte
Pedido explícito do usuário depois de ver o tema pela primeira vez ("aplique a mesma animação do mascote de pular com os confetes e a língua de sogra ao clicar no botão da logo"). Ao contrário do susto do vampiro (que **substitui** a piscada), o clique de festa **convive** com ela — a cara de repouso do mascote de aniversário não muda, então a piscada de olhos continua fazendo sentido; o clique dispara os dois efeitos juntos.
- **Markup**: `pidAniversarioIconRigMarkup()` (`seasonal-theme.js`) ganhou, além dos balões já existentes, um braço/balões agrupados (`.pid-aniv-balloons`/`.pid-aniv-arm`, antes soltos), um grupo de confete escondido (`.pid-aniv-confetti`, 8 formas pequenas perto do braço) e, dentro do corpo, um sorriso/bico-de-pato que se alternam (`.pid-aniv-smile`/`.pid-aniv-pucker-outer`/`-inner`) mais uma língua-de-sogra (`.pid-aniv-horn`, um tubo + traço pontilhado + 2 "bolhas" de sopro) — todos escondidos por padrão (`opacity:0`/`scaleX(0)`), revelados só durante o clique.
- **Trigger**: `pidTriggerAniversarioParty(root)` (mesmo padrão de `pidTriggerVampireScare` — remove e reaplica `.is-partying` em todo `.pid-aniv-hop` dentro de `root`, permitindo cliques repetidos re-tocarem do zero). Chamado por `sidebar-brand.js`/`login-logo-blink.js` quando `pidIsAniversarioActive()`, sempre antes de também disparar a piscada normal.
- **Animação** (`base.css`, `PID_ANIVERSARIO_PARTY_MS = 650ms` pra tudo, mesmo truque do susto do vampiro de escalonar o efeito só via percentuais internos de cada `@keyframes`): o personagem pula (`pidAnivHop`, translateY+squash), o braço joga os balões pra cima (`pidAnivArmThrow`, rotate), os balões balançam mais forte (`pidAnivBalloonSwing`), as 8 peças de confete explodem em direções fixas (`pidAnivConfettiPiece`, uma por `:nth-child` com `--tx`/`--ty` próprios e um pequeno atraso escalonado) e a boca faz bico enquanto a língua-de-sogra desenrola e recolhe (`pidAnivPuckerIn`/`pidAnivSmileOut`/`pidAnivHornUnroll`).
- **Pivôs de transform**: `.pid-aniv-hop`/`.pid-aniv-balloons`/`.pid-aniv-arm` usam `transform-box:fill-box; transform-origin:center bottom` (pivô aproximado na base do próprio desenho, suficiente pra um ícone pequeno — não vale a complexidade de calcular o pivô exato em `view-box`); `.pid-aniv-horn` usa `transform-origin:left center` (a ponta esquerda da própria caixa delimitadora é exatamente onde o tubo nasce, perto da boca — `scaleX` a partir dali "desenrola" corretamente sem precisar de coordenada manual).
- **Bug real — a língua-de-sogra saía do lugar errado e não chegava a sair do corpo**: ela tinha nascido DENTRO do grupo do corpo (`<g transform="translate(2,18) scale(0.72)">`), com as coordenadas da ponta esquerda do sorriso (`M25 47`, o começo do traço da boca, não o centro dela) — resultado: o tubo atravessava o rosto na diagonal e terminava ainda dentro do disquete, sem nunca aparecer pra fora. Corrigida movendo o grupo pra FORA do corpo (irmão dele, depois dele no DOM pra continuar desenhando por cima) e recalculando o ponto de origem no espaço do rig: o bico da boca está em `(32, 48.5)` no espaço local do corpo, que vira `(2 + 32×0,72, 18 + 48,5×0,72) = (25, 53)` no espaço do rig. Daí o tubo segue `q12.6 -5 30 -11` (as mesmas proporções da composição original, onde o efeito também cruza o corpo de propósito) e termina 11 unidades além da borda direita do disquete, em espaço livre.
### Intro de abertura pós-login (`static/css/login-intro-aniversario.css` + `static/js/login-intro-aniversario.js`)
Variante de `login-intro.css`/`.js`, com seu próprio overlay (`#login-intro-aniversario` em `index.html`) — reaproveita `PID_INTRO_EASING`/`pidIntroAnimate`/`pidIntroClamp`/`pidIntroCornerTarget` já globais de `login-intro.js`. `auth.js` escolhe a intro certa (Halloween > Aniversário > normal, nessa ordem de prioridade de código — nunca duas ativas ao mesmo tempo de verdade).
- **Cenas originais** (`PID_INTRO_A_CUES` — `Chegada:0, Confete:1.1, Texto:2.5, Serpentina:4.1`, somando 5,9s, extraídas de `pid-56.jsx`): `Chegada` (o personagem cai com os balões e quica ao pousar), `Confete` (dá um pulo, joga o braço com os balões pra cima e arremessa uma explosão de confete — 58 peças com física determinística, mesma semente pseudo-aleatória do original pra toda visita produzir a mesma explosão), `Texto` (o ícone desliza pra esquerda enquanto "Parabéns" + a logo da De Paula + "56 Anos" entram), `Serpentina` (a boca faz bico e uma língua-de-sogra desenrola da boca, depois recolhe totalmente, enquanto o slogan pousa e a cena esmaece).
- **Diferenças em relação à composição original**: (1) a composição original foi autorada num canvas de vídeo 1920×1080 com elementos posicionados por coordenadas absolutas — aqui virou uma única linha flexível (ícone + mensagem lado a lado), com o "deslizar pra esquerda" reproduzido pela mesma técnica de `SHIFT_X` já usada nas outras duas intros (o ícone se desloca por `transform`, a mensagem só aparece fixa ao lado); (2) o "zoom" de respiro do fundo (1.05→1.0→1.02) e as vinhetas radiais do canvas original foram descartados — floreios de vídeo sem efeito perceptível numa janela de overlay pequena; (3) `Fly` (~0,9s, acrescentada, não existe no arquivo original, mesma ideia já usada nas outras duas intros) — a composição original termina com tudo esmaecendo junto (pensada como vídeo solto); aqui o ícone encolhe e voa até o canto real da sidebar antes do stage inteiro esmaecer.
- **Reescala de tamanho**: a composição original foi autorada pra um ícone de 420px (`Party` component, `size=420` default); aqui o ícone roda a `PID_INTRO_A_ICON_SIZE` (260px — maior que os 200px das outras duas intros, pedido explícito do usuário numa rodada seguinte, "aumente um pouco o tamanho do mascote... para que fique mais fácil sua visualização"), com `PID_INTRO_A_ICON_K = PID_INTRO_A_ICON_SIZE / 420` calculado **a partir** dessa constante (não fixo) — só os deslocamentos aplicados via CSS `transform` em px reais no elemento `<svg>` (a queda `drop`, o "lift" do pulo, o blur da sombra) são multiplicados por esse fator; as transformações internas em unidades do `viewBox` (balanço dos balões, ângulo do braço, comprimento da língua-de-sogra) escalam junto automaticamente, e a física do confete (px reais fora do SVG) é multiplicada pelo mesmo fator.
- **Tema claro**: mesmo raciocínio das outras duas intros — fundo acompanha `--bg-canvas`, texto claro (pensado pra contrastar com fundo escuro) vira escuro; o ícone não precisa de override (roxo/dourado com contraste de sobra nos dois fundos).
### Bolo de aniversário (`static/css/aniversario-decor.css`), pedido numa rodada seguinte
Pedido explícito do usuário pra "deixar o tema mais completo". A mesma rodada também trouxe bandeirinhas de festa nos cantos (login + Principal, nos mesmos 2 cantos das teias do Halloween), **removidas a pedido do usuário logo depois** de vê-las na tela — não tentar ressuscitá-las sem pedido explícito. O bolo ficou.
- **Bolo de aniversário** (`pid-aniv-cake`, `pid-cake-*`): só na Principal, `position:fixed` no canto inferior-direito da **janela** (não da página — continua visível mesmo com scroll, ao contrário das teias de aranha, que são `position:absolute` dentro do conteúdo). Duas camadas com gotejamento de cobertura (`pid-cake-drip1`/`drip2`, um path ondulado) e 3 velas com chama tremulando continuamente (`pidCakeFlicker`, atrasos diferentes por vela pra não tremerem em sincronia). Cada gotejamento usa a cor da camada DE CIMA (creme sobre o dourado, dourado sobre o roxo) — pintá-lo da cor da própria camada o deixaria invisível, que foi como nasceu.
- **Iluminação das velas** (pedida numa rodada seguinte), em duas camadas: um `drop-shadow` duplo no próprio `.pid-cake-flame` (o brilho colado na chama, que pulsa junto com `pidCakeFlicker`) e o `.pid-cake-halo` — uma elipse com `<radialGradient>` âmbar, posicionada **depois das camadas do bolo e antes das velas**, pra a luz lavar o topo do bolo sem desbotar as chamas. Os dois pulsam em ciclos de durações diferentes (1,4s e 2,6s) de propósito, pra não parecerem sincronizados. O `filter` está também na regra base de `.pid-cake-flame`, não só nos keyframes, pra o brilho sobreviver enquanto `pidCakeFlameOut` toca; e o halo tem seu próprio `pidCakeHaloOut`, senão sobraria uma mancha quente boiando no canto depois das velas apagadas. No tema claro a intensidade cai pela metade (`--halo-max`) — brilho quente sobre fundo claro lê como névoa suja, não como luz.
- **Clicar apaga as velinhas** (pedido explícito do usuário): `pidApplyAniversarioCake(containerId)` liga o clique num `<button>` (não numa `<div>`, pra ganhar foco por teclado/`aria-label` de graça) — adiciona `.is-blowing` (toca `pidCakeFlameOut`/`pidCakeSmokeRise` uma vez, a fumaça sobe e some enquanto a chama encolhe) e, depois de `PID_CAKE_BLOW_MS` (1400ms), troca pra `.is-blown` (estado final em repouso, sem `animation` — mesma ideia de `.pw-spider.is-dead`).
- **O estado apagado NÃO é persistido, de propósito** — as velinhas reacendem a cada carregamento da página, e o atalho Tab+B continua sendo só da aranha do Halloween. A persistência chegou a existir aqui (`pid_aniversario_cake_blown` no `localStorage`, copiada de `pidMarkSpiderDead`) e foi **removida depois que o usuário relatou como bug**: "não aparece o fogo das velinhas para funcionar a animação de apagar elas ao clickar". O que acontecia: quem clicasse no bolo uma vez nunca mais via as velas acesas (nem o listener de clique era ligado, já que o bolo nascia direto em `.is-blown`), sem nenhuma pista disso na tela, e só voltava com um atalho de teclado obscuro. A aranha persiste porque o usuário **pediu** isso explicitamente; aqui o pedido foi só "ao clicar nele as velas se apagam" — a persistência era invenção minha por analogia. **Lição pra decoração futura**: só persistir estado de easter egg quando pedido, senão o efeito desaparece de vez no primeiro uso.
- **Bug real — o bolo nasceu ocupando a tela inteira, fora do canto**: a `<div>` em `portal.html` tinha só o `id` (`#pid-aniv-cake`), sem a classe `.pid-aniv-cake` que carrega TODO o dimensionamento/posicionamento (`position:fixed`, `right`/`bottom`, `width:clamp(...)`) — sem ela a div virou um bloco normal do fluxo, com a largura inteira do conteúdo, e o `<svg>` (`width:100%`) esticou junto. As classes `.is-blowing`/`.is-blown` também não teriam efeito, pelo mesmo motivo. **Ao adicionar um container decorativo novo, conferir que ele leva classe E id** — o id é só o gancho do JS, quem estiliza é a classe (mesmo padrão dos containers de teia, que já nasceram com as duas).
- **Bug real — as chamas flutuavam acima das velas**: nasciam alguns pontos acima do topo de cada vela, soltas no ar; agora começam exatamente no `y` do `<rect>` da vela correspondente. A fumaça também precisou de `transform-box:fill-box` — sem isso, o `scale` da subida cresce a partir da origem do viewBox e joga o traço pra longe do pavio.
- **Outros dois ajustes visuais da mesma rodada**: as chamas nasciam alguns pontos acima do topo das velas (flutuando soltas) — agora começam exatamente no `y` do `<rect>` de cada vela; e o gotejamento de cobertura de cada camada estava pintado da mesma cor da própria camada (portanto invisível) — agora cada gotejamento usa a cor da camada de cima (creme escorrendo sobre o dourado, dourado sobre o roxo).
## Opt-out do usuário ("Temas sazonais", menu da conta)
Pedido explícito do usuário — "essa medida serve para caso o usuário tenha alguma preferência com relação aos temas, ou no caso do halloween, se tiver aracnofobia". Botão-toggle (`.seasonal-theme-toggle`, `components.css`) no dropdown da conta (`#account-dropdown`, duplicado nos 13 shells — mesmo padrão de duplicação de `sidebar__brand` já documentado no `CLAUDE.md` raiz, "Ordem de `<script>`"), logo abaixo de "Tema de cores", com dois estados "Ativo"/"Inativo" (`aria-pressed`, cor de destaque quando ativo). Wiring em `account.js`, junto do bloco de `theme-swatches` — não em `seasonal-theme.js`, que só guarda a preferência.
- **Armazenamento**: `pid_seasonal_theme_opt_out` no `localStorage` (`"1"` = desligado), mesmo padrão de `pid_theme`/`pid_color_theme` (`theme.js`) — preferência de navegador, nunca migrada pro banco.
- **Prioridade máxima**: `pidIsHalloweenActive()`/`pidIsAniversarioActive()` checam `pidSeasonalThemesOptedOut()` **antes** de qualquer outra coisa, inclusive o respectivo preview force — se o usuário desligou, fica desligado mesmo com o preview forçado ligado. O toggle vale pros dois temas de uma vez (não há opt-out por tema).
- **Recarrega a página ao alternar**: o tema sazonal (ícone, teias, favicon) já foi aplicado via substituição de `innerHTML` no carregamento da página atual — não dá pra desfazer isso in-place de forma confiável, então o clique já salva a preferência e chama `window.location.reload()`. Importante sobretudo pra quem desligou por aracnofobia: a aranha precisa sumir na hora, não só na próxima navegação.