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

35 KiB
Raw Permalink Blame History

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.