Inclusão dos temas sazonais

This commit is contained in:
Gabriel 2026-09-15 17:54:20 -03:00
parent 7d885b8a03
commit 444d76a159
36 changed files with 1726 additions and 16 deletions

View File

@ -40,7 +40,37 @@
"Bash(\"/c/Program Files/Google/Chrome/Application/chrome.exe\" --headless --disable-gpu --screenshot=\"/c/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/dc-header5.png\" --window-size=1680,300 \"file:///C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/test-dc-header5.html\")",
"Bash(\"/c/Program Files/Google/Chrome/Application/chrome.exe\" --headless --disable-gpu --screenshot=\"/c/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/dc-header6.png\" --window-size=1680,300 \"file:///C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/test-dc-header6.html\")",
"Bash(sed 's/justify-content: space-between;/justify-content: space-between;\\\\n flex-wrap: wrap;/' test-dc-header2.html)",
"Bash(\"/c/Program Files/Google/Chrome/Application/chrome.exe\" --headless --disable-gpu --screenshot=\"/c/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/dc-header7.png\" --window-size=1200,300 \"file:///C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/test-dc-header7.html\")"
"Bash(\"/c/Program Files/Google/Chrome/Application/chrome.exe\" --headless --disable-gpu --screenshot=\"/c/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/dc-header7.png\" --window-size=1200,300 \"file:///C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/test-dc-header7.html\")",
"Bash(python3 -c ' *)",
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" --version)",
"Bash(/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe -c ' *)",
"Bash(cp \"C:\\\\Users\\\\Depaula\\\\Documents\\\\Logos\\\\P.I.D. Logo Design Halloween\\\\pid-marca-halloween\\\\pid-halloween-favicon.svg\" \"static/img/pid-halloween-favicon.svg\")",
"Bash(cp \"C:\\\\Users\\\\Depaula\\\\Documents\\\\Logos\\\\P.I.D. Logo Design Halloween\\\\pid-marca-halloween\\\\pid-halloween-icone-escuro.svg\" \"static/img/pid-halloween-icone-escuro.svg\")",
"Bash(cp \"C:\\\\Users\\\\Depaula\\\\Documents\\\\Logos\\\\P.I.D. Logo Design Halloween\\\\pid-marca-halloween\\\\pid-halloween-logo-horizontal-escuro.svg\" \"static/img/pid-halloween-logo-horizontal-escuro.svg\")",
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" _scratch_insert_seasonal.py)",
"Bash(rm _scratch_insert_seasonal.py)",
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" manage.py check)",
"Bash(grep -n '`' static/js/seasonal-theme.js)",
"Bash(netstat -ano)",
"Bash(tasklist)",
"Bash(curl -s -o /dev/null -w \"index.html: %{http_code}\\\\n\" http://127.0.0.1:8000/)",
"Bash(curl -s -o /dev/null -w \"seasonal-theme.js: %{http_code}\\\\n\" http://127.0.0.1:8000/static/js/seasonal-theme.js)",
"Bash(curl -s -o /dev/null -w \"login-intro-halloween.js: %{http_code}\\\\n\" http://127.0.0.1:8000/static/js/login-intro-halloween.js)",
"Bash(curl -s -o /dev/null -w \"login-intro-halloween.css: %{http_code}\\\\n\" http://127.0.0.1:8000/static/css/login-intro-halloween.css)",
"Bash(curl -s -o /dev/null -w \"pid-halloween-favicon.svg: %{http_code}\\\\n\" http://127.0.0.1:8000/static/img/pid-halloween-favicon.svg)",
"Bash(curl -s http://127.0.0.1:8000/)",
"Bash(curl -s http://127.0.0.1:8000/portal.html)",
"Bash(curl -sI http://127.0.0.1:8000/)",
"Bash(curl -sI http://127.0.0.1:8000/static/js/seasonal-theme.js)",
"Bash(curl -sI http://127.0.0.1:8000/static/img/pid-icone-escuro.svg)",
"Bash(curl -s http://127.0.0.1:8000/static/js/seasonal-theme.js)",
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" -)",
"Bash(curl -s -o /dev/null -w \"halloween-cobweb.css: %{http_code}\\\\n\" http://127.0.0.1:8000/static/css/halloween-cobweb.css)",
"Bash(curl -s http://127.0.0.1:8000/static/css/login.css)",
"Bash(curl -s http://127.0.0.1:8000/static/css/layout.css)",
"Bash(curl -s http://127.0.0.1:8000/static/css/halloween-cobweb.css)",
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" _scratch_insert_seasonal_toggle.py)",
"Bash(rm _scratch_insert_seasonal_toggle.py)"
]
}
}

View File

@ -36,6 +36,7 @@ Aplicações sem pacote Python dedicado (código ainda em `portal_api/models.py`
| Favoritos (grade de `portal.html`) | `docs/favoritos/` — `favoritos.md` (técnico), `README.md`, `CHANGELOG.md` |
| Perfis de Acesso / Usuários (telas administrativas, Liderança, inativação) | `docs/perfis-usuarios/` — `perfis-usuarios.md` (técnico), `README.md`, `CHANGELOG.md` |
| Solicitações | `docs/solicitacoes/` — `solicitacoes.md` (técnico), `README.md`, `CHANGELOG.md` |
| Temas Sazonais (marca P.I.D. sazonal — hoje só Halloween) | `docs/temas-sazonais/` — `temas-sazonais.md` (técnico), `README.md`, `CHANGELOG.md` |
## Como rodar / testar localmente
@ -232,7 +233,7 @@ Padrão de guarda por página: `profiles.js`, `users-admin.js` e `widgets.js` ve
| Onde | O quê |
|---|---|
| `localStorage` (`pid_theme`, `pid_color_theme`) | Só preferência de tema — ver `theme.js`. |
| `localStorage` (`pid_theme`, `pid_color_theme`, `pid_seasonal_theme_opt_out`, `pid_seasonal_dead_spiders`) | Preferência de tema (`theme.js`) + preferências de Temas Sazonais (`seasonal-theme.js`, ver `docs/temas-sazonais/`). |
| Postgres, via API | Tudo o mais: sessão (cookie do Django), usuários, perfis de acesso, favoritos, widgets, compromissos do Calendário Individual, notificações dispensadas. |
`notifications.js` usa uma lista mockada em memória (`PID_NEW_TOOLS_NOTIFICATIONS`) para os anúncios de "nova ferramenta" — os itens de compromisso vêm de `/api/compromissos/` de verdade. O conteúdo das notificações continua mockado/derivado a cada carregamento, mas **quais delas o usuário já dispensou** (X individual ou "Limpar tudo") persiste por usuário via `NotificacaoDispensada`/`/api/notificacoes-dispensadas/` — por isso um item dispensado não reaparece depois de um reload.
@ -437,3 +438,9 @@ Ao apertar "Entrar" com sucesso, `index.html` toca uma animação de marca em te
**"A barra lateral surgindo da esquerda para a direita, e em seguida o resto da tela"** (pedido explícito do usuário sobre como a tela Principal deveria surgir depois da animação, refinado duas vezes: a primeira versão só escondia `.main-content` e deixava a sidebar sempre visível desde o início, sem nenhuma entrada própria; a segunda versão deu à sidebar um fade + deslize sutil de 16px, considerado "ainda não satisfatório" — pequeno demais pra ler como "surgindo da esquerda pra direita"): `static/js/portal-reveal.js` (só `portal.html`, incluído `defer` logo depois de `theme.js` — mesmo padrão de "script que roda como IIFE de topo antes da primeira pintura pra evitar flash", ver "Ordem de `<script>`" acima) checa a `sessionStorage` marcada por `auth.js`; se presente, remove a marca (não sobrevive a um F5) e aplica **duas** classes em `<html>` antes do `DOMContentLoaded`: `pid-entering-sidebar` (esconde só `.sidebar`, `opacity:0` + `transform:translateX(-100%)` — a largura inteira dela, não um deslize de poucos pixels, pra realmente ler como um slide de fora da tela) e `pid-entering` (esconde `.main-content`, o `<div>` que envolve topbar **e** conteúdo da página). As duas são removidas **em sequência**, não juntas: `pid-entering-sidebar` sai primeiro (120ms depois do `DOMContentLoaded`), a sidebar desliza da esquerda pra direita ao longo de 420ms (`.sidebar` em `layout.css` tem uma `transition` própria — `opacity 420ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1)`, mais longa e com uma curva de "chegada" suave, não `var(--transition-base)` — genérica demais pra um movimento desse tamanho); só depois de esperar essa mesma duração (420ms) é que `pid-entering` sai, revelando o resto do app num fade, garantindo que as duas entradas não se sobreponham. Acessar `portal.html` direto (sem passar pelo login) nunca aciona nada disso, já que a `sessionStorage` só é setada no caminho de login bem-sucedido.
Testado pelo usuário em três rodadas (funcional) — ajustes até agora: a cor do overlay (era `#241c33`, virou `#121017`), o fade de entrada antes do ícone começar a se desenhar (não existia, overlay aparecia de repente; a duração foi ajustada depois de 350ms pra 500ms, "muito rápido... só pra ficar mais fluído"), a compressão da cena Portal (era 2,3s com pausa parada, foi pra 0,95s corrida numa rodada e depois ~0,9s), a aceleração de Build/Face (de 2,6s/2s cada pra 1s cada, Wordmark mantida em 2,8s de propósito) e a entrada da sidebar em `portal.html` (de um fade sutil de 16px pra um slide de fora da tela inteiro, `translateX(-100%)`, 420ms). Ainda falta conferir o posicionamento do "voo" final em diferentes tamanhos de tela/com a sidebar colapsada.
## Temas Sazonais
Troca sazonal da marca P.I.D. (logo, ícone da sidebar/login, favicon, intro de abertura pós-login) por uma variante temática durante uma janela de datas — hoje só o Halloween (mascote "vampiro", outubro), mecanismo pensado pra outro tema sazonal futuro reaproveitar. Inclui também, só durante o Halloween: uma animação de "susto" ao clicar na logo (substitui a piscada normal) e teias de aranha decorativas nos cantos do login/tela Principal (com uma aranha que pode ser "morta" ao clicar). Um toggle "Temas sazonais" no menu da conta deixa o próprio usuário desligar isso (preferência pessoal ou acessibilidade, ex.: aracnofobia). 100% runtime — fora da janela ativa (ou com o opt-out ligado), tudo volta sozinho ao P.I.D. de sempre, sem nenhuma ação manual.
Ver `docs/temas-sazonais/temas-sazonais.md`.

View File

@ -41,6 +41,7 @@ Não há suíte de testes, lint ou build configurados neste projeto.
| Indicador de Desempenho | Geradoc | [portal_api/indicadores/](portal_api/indicadores/README.md) |
| Não Conformidades | Relatórios > Qualidade | [portal_api/nao_conformidades/](portal_api/nao_conformidades/README.md) |
| Relatório Contábil | Relatórios > Contabilidade | [portal_api/dashboard_contabil/](portal_api/dashboard_contabil/README.md) |
| Temas Sazonais (marca P.I.D. sazonal, hoje só Halloween) | — (recurso visual, não é item de menu) | [docs/temas-sazonais/](docs/temas-sazonais/README.md) |
Cada pasta acima tem um `README.md` (o que a aplicação faz, estado atual) e um `CHANGELOG.md` (histórico rodada a rodada, extraído de `plano.md`). As 5 aplicações com pacote Python próprio também têm um `CLAUDE.md` com o detalhamento técnico completo, carregado automaticamente pelo Claude Code; as demais têm esse detalhe no arquivo dentro da própria pasta em `docs/`.

View File

@ -0,0 +1,49 @@
# Changelog — Temas Sazonais
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo continuam em `plano.md` — a numeração de rodada é a mesma usada lá, para referência cruzada.
### Rodada 94 — Tema sazonal Halloween: logo/mascote vampiro, intro de abertura e "susto" ao clicar
Pedido (2026-09-15): deixar uma logo e uma introdução de abertura com tema de Halloween, aplicadas só durante outubro, revertendo pro P.I.D. normal depois — sem descartar nada do que já existe. Mascote (a marca P.I.D. fantasiada de vampiro) e a composição da intro foram desenhados fora do repositório, em `C:\Users\Depaula\Documents\Logos\P.I.D. Logo Design Halloween\` (`pid-marca-halloween/` + `Logo intro animation Halloween/export-halloween/pid-intro-halloween.html`, mesmo formato de bundle "Design Canvas" da intro original) e portados fielmente pra vanilla JS/CSS, mesma técnica já usada na intro normal (ver `CLAUDE.md` raiz → "Animação de intro do login" → "Origem do arquivo"). Implementado como mecanismo **100% runtime**, sem editar nenhum arquivo original: `static/js/seasonal-theme.js` (novo, incluído em toda página) decide se o tema está ativo (`pidIsHalloweenActive()`, janela de outubro) e, se sim, troca em memória o favicon, o ícone/assinatura da sidebar e o ícone do login pelo vampiro, e troca a piscada de olhos ao clicar por uma animação de "susto" (capa se abre, rosto fica raivoso, tremida + brilho vermelho — `.is-scaring` em `base.css`); `static/css/login-intro-halloween.css` + `static/js/login-intro-halloween.js` são uma intro de abertura completa alternativa (cenas Rise/Scare/Settle/Wordmark + uma cena "Fly" acrescentada pra aterrissar na sidebar, mesmo espírito da intro normal), escolhida por `auth.js` no lugar da intro normal quando o tema está ativo. Detalhe técnico completo em `temas-sazonais.md`.
**Ainda em preview, não confirmado**: `PID_HALLOWEEN_PREVIEW_FORCE = true` (em `seasonal-theme.js`) força o tema ligado agora só pra essa revisão visual — o usuário ainda precisa (a) rodar o `runserver` e conferir a logo/intro/susto nos dois temas, (b) confirmar se a janela é outubro inteiro (já implementado) ou um recorte diferente, e (c) desligar essa constante depois de validado (daí em diante só a data decide, sem mais nenhuma mudança de código). Depois de outubro, tudo volta ao P.I.D. normal sozinho, sem nenhuma ação manual — nenhum arquivo original (`login-intro.js`/`.css`, ícones `pid-icone*`/`pid-logo-horizontal*`, `pid-icon-eye`/`is-blinking`) foi alterado ou removido.
**Ajuste na mesma rodada**: o usuário reportou o favicon aparecendo na cor errada (branco, destoando do resto da marca) — `pidApplySeasonalFavicon()` trocava pra `pid-halloween-favicon.svg` (variante simplificada, sem capa/gola/cores completas); corrigido pra usar `pid-halloween-icone-escuro.svg` (o mesmo ícone/cores do resto da UI), mesmo padrão que o P.I.D. normal já seguia.
### Rodada 95 — Teias de aranha nos cantos do login e da tela Principal
Pedido (2026-09-15, mesma rodada do tema Halloween acima): teias de aranha com aranha animada nos cantos do login e da tela Principal, marcados pelo usuário em prints da tela real. Antes de aplicar, foi publicado um protótipo isolado (Artifact "Cantos Assombrados") com 4 vs 2 cantos e aranha andando vs parada, pra decidir o desenho sem mexer no Portal de verdade — o usuário confirmou **2 cantos** (superior-esquerdo/inferior-direito, os mesmos dos prints) e pediu a teia da tela Principal **menor** que a do login, "evitando cobrir muito as informações que constam na página" (a Principal tem cards/widgets perto das bordas; o login só tem o card centralizado).
Aplicado só em `index.html` e `portal.html` (nenhum outro shell), mesmo mecanismo 100% runtime do resto do tema: `static/css/halloween-cobweb.css` (novo) define a teia (6 fios + 3 anéis, coordenadas por trigonometria) e a aranha (corpo+8 pernas, anda e volta ao longo de um fio via `@keyframes` puro CSS, pernas mexendo continuamente); o markup (`PID_COBWEB_SVG` em `seasonal-theme.js`) só é injetado num container já presente e `hidden` no template (`#pw-cobwebs-login`/`#pw-cobwebs-portal`) quando `pidIsHalloweenActive()`, chamado por `login-logo-blink.js`/`sidebar-brand.js`. Na Principal, a teia é ancorada em `.main-content`, não no viewport — a sidebar já é sólida e cobriria o canto esquerdo de qualquer jeito. Detalhe técnico completo em `temas-sazonais.md` → "Teias de aranha de canto".
**Ajuste na mesma rodada**: a teia do login não aparecia de jeito nenhum, e a da Principal ficou "muito pequena". Causa raiz do primeiro: `z-index:-1` só funciona dentro de um contexto de empilhamento isolado — sem isso, o próprio fundo em tela cheia de `.login-page` (conteúdo estático normal) pintava por cima da teia, escondendo-a por completo; corrigido com `isolation:isolate` em `.login-page` (`login.css`) e em `.main-content` (`layout.css`), que tinha o mesmo problema de raiz (só não percebido porque a Principal não tem um fundo cobrindo tudo). Tamanho aumentado nas duas telas (`clamp(120px,14vw,190px)` → `clamp(160px,17vw,240px)` no login; `clamp(80px,8vw,120px)` → `clamp(110px,11vw,170px)` na Principal), mantendo a Principal um pouco menor que o login.
**Mais um ajuste, ainda na mesma rodada**: (a) a topbar da Principal (`position:sticky`, fundo opaco) cobria a metade de cima da teia do canto superior-esquerdo — `.pw-corner--tl` na Principal ganhou `top:var(--topbar-height)` pra começar logo abaixo dela; (b) tamanho aumentado de novo nas duas telas (login → `clamp(220px,22vw,320px)`, Principal → `clamp(160px,15vw,230px)`); (c) contraste por tema: no escuro, a aranha (quase a mesma cor do fundo) ganhou um `drop-shadow` branco pra não sumir, sobrando só o olho vermelho visível como antes; no claro, a teia (creme, pensada pro fundo escuro) viraria invisível no fundo branco — agora fica cinza-chumbo escuro nesse tema, e o brilho branco da aranha é desligado (já contrasta sozinha, escura sobre claro).
### Rodada 96 — Opt-out de temas sazonais (menu da conta)
Pedido (2026-09-15, mesma rodada do tema Halloween): um jeito do próprio usuário desligar temas sazonais (hoje só o Halloween), "caso o usuário tenha alguma preferência com relação aos temas, ou no caso do halloween, se tiver aracnofobia". Botão-toggle "Temas sazonais" (`.seasonal-theme-toggle`, `components.css`) no dropdown da conta, logo abaixo de "Tema de cores", com dois estados "Ativo"/"Inativo" — duplicado nos 13 shells, mesmo padrão dos demais elementos replicados por template. Preferência salva em `localStorage` (`pid_seasonal_theme_opt_out`), mesmo padrão do tema claro/escuro (nunca migrada pro banco — é preferência de navegador). `pidIsHalloweenActive()` (`seasonal-theme.js`) checa essa preferência **antes** de qualquer outra coisa, inclusive antes do `PID_HALLOWEEN_PREVIEW_FORCE` (prioridade máxima: se o usuário desligou, fica desligado). Alternar o toggle recarrega a página (`account.js`) — o tema sazonal já foi aplicado via substituição de markup no carregamento atual, não dá pra desfazer in-place, e pra quem desligou por aracnofobia a aranha precisa sumir na hora. Detalhe técnico completo em `temas-sazonais.md` → "Opt-out do usuário".
### Rodada 97 — Clicar na aranha da teia mata ela
Pedido (2026-09-15, mesma rodada): "ao clicar nas aranhas, deve sair uma pequena caveirinha dela (como se tivesse morrido) e ela cair, deixando a teia vazia. Ao atualizar a página, deve voltar como estava." A aranha (única parte da teia com `pointer-events:auto`) ganhou um listener de clique `{once:true}` que dispara duas animações CSS em paralelo: a própria aranha cai girando e some (`pwSpiderDie`, pernas encolhendo numa animação própria, `pwLegsDie`) enquanto uma caveirinha (`<g class="pw-skull">`, novo, sempre presente mas invisível) sobe e desaparece (`pwSkullRise`), como se a alma saísse do corpo. Sem nenhum estado salvo nesta rodada — a teia ficava vazia só até a página ser recarregada, já que `pidApplyHalloweenCobwebs()` reconstruía tudo do zero a cada carregamento (persistência veio na rodada 99). Detalhe técnico completo em `temas-sazonais.md` → "Teias de aranha de canto" → "Clicar mata a aranha".
### Rodada 98 — Teia mais profissional + correção da aranha/caveira invertidas no canto inferior-direito
Pedido (2026-09-15, mesma rodada): (a) o desenho da teia (6 fios retos + 3 anéis poligonais) estava "muito bruto", o usuário anexou uma referência visual e pediu algo "nesse sentido", mais profissional, "para que não fique parecendo que só foi colado um PNG bruto"; (b) no canto inferior-direito, a aranha caía **pra cima** ao morrer e a caveira aparecia **de cabeça pra baixo**.
**Teia redesenhada**: agora 8 fios (ângulos irregulares, não uniformes — mais parecido com uma teia real) + 5 anéis em espiral **levemente arqueados** (`<path class="pw-ring">` com curvas `Q`, controle empurrado ~8% mais longe do centro que o ponto médio da corda — simula a curva natural de uma espiral de captura, em vez de segmentos retos ligando os pontos) + 1 fio "perdido" bem mais comprido que os demais, se estendendo pra fora do canto (detalhe da referência do usuário) e cortado naturalmente pela borda da página (`.pw-corners{overflow:hidden}`).
**Causa raiz do bug do canto inferior-direito**: esse canto reaproveitava o mesmo desenho do superior-esquerdo com `transform:scale(-1,-1)` no `<div>` inteiro — um jeito simples de não duplicar o markup, mas que vira **tudo** de cabeça pra baixo, inclusive a orientação da aranha/caveira e a direção "pra baixo"/"pra cima" das animações de queda/subida. Corrigido separando as duas coisas: só a teia (`.pw-strands`, que não tem um "lado certo pra cima") continua espelhada, com `transform-box:view-box;transform-origin:100px 100px` (o centro do viewBox autorado — não a caixa delimitadora dos fios, que o fio "perdido" deslocaria); a aranha e a caveira passaram a usar variáveis CSS (`--rx`/`--ry`/`--rr`/`--ox`/`--oy`/`--sx`/`--sy`) redefinidas com os valores espelhados só para `.pw-corner--br`, sem nenhum `scale`/`rotate` extra nelas — ficam sempre em pé e caem/sobem na direção real da tela nos dois cantos. Detalhe técnico completo em `temas-sazonais.md` → "Teias de aranha de canto".
### Rodada 99 — Aranha morta persiste entre carregamentos + atalho Tab+B pra resetar
Pedido (2026-09-15, mesma rodada): "guardar que a aranha foi morta na memória... até executar o comando tab+b que reseta a aranha". Cada aranha ganhou um id próprio (`${containerId}__tl`/`__br`, 4 possíveis — 2 cantos × 2 páginas) guardado num array em `localStorage` (`pid_seasonal_dead_spiders`, `pidMarkSpiderDead()`/`pidGetDeadSpiders()`/`pidResetDeadSpiders()` em `seasonal-theme.js`) — mesmo padrão de armazenamento das outras preferências sazonais, nunca no banco. `pidApplyHalloweenCobwebs()` agora checa essa lista a cada carregamento: aranha já morta nasce direto com `.is-dead` (invisível, sem tocar a animação de queda nem a caveira de novo), em vez de ligar o clique. Segurar Tab e apertar B (sem interceptar o Tab em si — a navegação por foco continua normal) limpa a lista e recarrega a página, trazendo todas de volta. Detalhe técnico completo em `temas-sazonais.md` → "Teias de aranha de canto".
### Rodada 100 — Documentação separada em `docs/temas-sazonais/`
Pedido (2026-09-15): "gerar um changelog específico para esta questão dos temas sazonais... estruturar suas informações como se fossem aplicação específica". Seguindo o mesmo padrão das demais aplicações sem pacote Python dedicado (`docs/<app>/<app>.md`+`README.md`+`CHANGELOG.md`), o conteúdo técnico detalhado que vivia na seção "Tema sazonal (Halloween)" do `CLAUDE.md` raiz foi movido pra este arquivo/`temas-sazonais.md`/`README.md`; o histórico rodada a rodada (94-99) foi extraído de `plano.md` pra este `CHANGELOG.md` (mesma numeração). `CLAUDE.md` e `plano.md` ficaram só com a parte estrutural (a existência do mecanismo + onde encontrar o resto), sem duplicar o detalhe aqui.
### Rodada 101 — Preview confirmado, tema programado pra outubro de verdade
Pedido (2026-09-15): "pode parar de forçar o tema e deixar somente programado para aplicar a partir de outubro". `PID_HALLOWEEN_PREVIEW_FORCE` (`seasonal-theme.js`) virou `false` — mantida na constante (não removida), documentando a decisão em vez de apagar o rastro. A partir daqui, `pidIsHalloweenActive()` decide só pela data (`PID_HALLOWEEN_MES = 9`, outubro inteiro, todo ano), sem nenhuma ação manual pra ligar/desligar. Encerra o período de preview aberto na rodada 94.

View File

@ -0,0 +1,22 @@
# Temas Sazonais
> Ver `docs/temas-sazonais/temas-sazonais.md` para o detalhamento técnico. 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 pós-login) por uma variante temática durante uma janela de datas — hoje só o **Halloween** (mascote "vampiro", ativo em outubro), mas pensado desde já pra outro tema sazonal futuro reaproveitar o mesmo mecanismo. Fora da janela ativa, tudo volta sozinho ao P.I.D. de sempre, sem nenhuma ação manual.
**Status**: confirmado e programado — ativa sozinho todo mês de outubro, sem nenhuma ação manual (ver `temas-sazonais.md`, seção "Status atual").
Durante o Halloween, o tema também inclui:
- Uma animação de "susto" ao clicar na logo (substitui a piscada de olhos normal).
- Teias de aranha decorativas nos cantos do login e da tela Principal, com uma aranha animada que pode ser "morta" ao clicar (persiste entre carregamentos; atalho Tab+B reseta todas de volta).
- Um toggle "Temas sazonais" no menu da conta, pra o próprio usuário desligar (preferência pessoal, ou acessibilidade — ex.: aracnofobia).
## Onde mexer
- `static/js/seasonal-theme.js` — toda a lógica: checagem de data/opt-out, troca de ícone/favicon, teias de aranha, óbito persistido, atalho Tab+B.
- `static/css/halloween-cobweb.css` — desenho/animação da teia de aranha, aranha e caveira.
- `static/css/login-intro-halloween.css` + `static/js/login-intro-halloween.js` — intro de abertura pós-login alternativa.
- `static/js/sidebar-brand.js` / `static/js/login-logo-blink.js` — aplicam a troca de ícone e o susto ao clicar.
- `static/js/account.js` + `components.css` (`.seasonal-theme-toggle`) — toggle de opt-out no menu da conta.
- `static/js/auth.js` — escolhe a intro normal ou a sazonal no sucesso do login.
- `static/img/pid-halloween-*.svg` — cópias de referência da marca sazonal (a UI injeta o markup inline, não referencia esses arquivos diretamente).

View File

@ -0,0 +1,63 @@
# 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 só o Halloween (mascote "vampiro", outubro), mas o mecanismo (`pidIsHalloweenActive()`/`seasonal-theme.js`) foi pensado pra ser genérico o bastante pra outro tema sazonal futuro reaproveitar o mesmo esqueleto sem reescrever nada.
## Status atual
**Confirmado e programado** — revisão visual concluída pelo usuário. `PID_HALLOWEEN_PREVIEW_FORCE = false` em `static/js/seasonal-theme.js` (deixado de propósito na constante, documentando a decisão, em vez de removido): daqui pra frente só a janela de outubro (`PID_HALLOWEEN_MES = 9`, `Date#getMonth()` 0-indexed, mês inteiro, todo ano) decide se o tema está ativo, sem nenhuma ação manual — nem pra ligar em outubro, nem pra desligar depois. Nada mais precisa mudar de código pra isso valer.
## 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/`" no `CLAUDE.md` raiz) 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()` — a checagem de data (mais o preview force e o opt-out do usuário, ver "Opt-out do usuário" abaixo — checado **antes** de tudo, prioridade máxima).
- `pidApplySeasonalFavicon()` — troca `<link rel="icon">` de `pid-icone-escuro.svg` pra `pid-halloween-icone-escuro.svg` via substring no `href` (funciona com qualquer prefixo de `STATIC_URL`). **Não** usa `pid-halloween-favicon.svg` — essa é uma variante simplificada (sem capa/gola, cores diferentes) que destoava do resto da marca sazonal na aba do navegador (bug relatado pelo usuário logo na revisão visual); 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`.
## 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`/`pidApplySeasonalFavicon` no próprio `DOMContentLoaded`, só se `pidIsHalloweenActive()`. Isso também é o que torna o "retorno" depois de outubro automático: o innerHTML original nunca é tocado no arquivo, só sobrescrito em memória a cada carregamento de página enquanto a 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.
## 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()` checa `pidSeasonalThemesOptedOut()` **antes** de qualquer outra coisa, inclusive `PID_HALLOWEEN_PREVIEW_FORCE` — se o usuário desligou, fica desligado mesmo com o preview forçado ligado.
- **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.

View File

@ -389,6 +389,10 @@ Nova aplicação em Relatórios > Contabilidade (nasce restrita ao perfil "Inova
Pedido (2026-09-14): remover o item "Relatório Setorial" (`href="#"`, sem tela própria) do menu Relatórios, já que o tópico agora tem itens de verdade ("Qualidade" → Não Conformidades, "Contabilidade" → Relatório Contábil). Removido de `MODULE_APPS["relatorios"]` em `catalogo.py` e do `<li data-app="relatorio-setorial">` hardcoded nas 13 páginas-shell (o menu é replicado por template, não um componente único — ver "Ordem de `<script>`" no `CLAUDE.md` raiz pro mesmo padrão de duplicação entre shells). O item "Indicadores" (outro placeholder `href="#"` dentro de Relatórios) não foi tocado, não fazia parte do pedido. Perfis já existentes no banco podem manter uma chave órfã `"relatorio-setorial"` dentro de `permissoes["relatorios"]["apps"]` — inofensiva, mesmo padrão já visto quando "Gerar Contrato"/"Gerar Procuração" foram removidos de Geradoc (ver comentário em `catalogo.py`).
### 94. Temas sazonais (Halloween) — logo/mascote vampiro, intro de abertura, teias de aranha e opt-out
Pedido (2026-09-15): deixar uma logo e uma introdução de abertura com tema de Halloween, aplicadas só durante outubro, revertendo pro P.I.D. normal depois — sem descartar nada do que já existe. Implementado como mecanismo **100% runtime** (nenhum arquivo original alterado ou removido — fora da janela ativa, tudo volta sozinho): troca de ícone/favicon/assinatura pelo mascote vampiro, uma intro de abertura pós-login alternativa, uma animação de "susto" ao clicar na logo, teias de aranha decorativas nos cantos do login/tela Principal (com uma aranha "matável" ao clicar, óbito persistido) e um toggle no menu da conta pro usuário desligar temas sazonais por preferência própria (ou acessibilidade — aracnofobia). Ainda em preview (`PID_HALLOWEEN_PREVIEW_FORCE` força ligado pra revisão visual, antes de confirmar a janela de outubro definitivamente). Histórico técnico completo (rodadas 94-100) e detalhamento em `docs/temas-sazonais/CHANGELOG.md`/`temas-sazonais.md` — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).
## Limitações conhecidas / decisões assumidas
- Calendário De Paula (empresa) não tem mais versão interna — depende

View File

@ -136,6 +136,119 @@ svg {
}
}
/* Variante "susto" do clique na logo, só durante o tema sazonal Halloween
(`seasonal-theme.js`/`pidIsHalloweenActive()`) — substitui a piscada acima
enquanto o ícone P.I.D. normal é trocado pelo vampiro
(`pidApplyVampireIcon`/`pidApplyVampireFull`). `.pid-vamp-icon-rig` é o
grupo raiz injetado dentro do <svg> (sidebar colapsada/expandida e botão
do login); `.pid-vamp-face--friendly`/`--scare` são dois grupos sempre
presentes, cruzando opacidade entre si. Mesmo cuidado de `.pid-icon-eye`
com `transform-box:fill-box` pra girar/escalar em torno do próprio ícone,
não da origem do SVG. */
.pid-vamp-icon-rig,
.pid-vamp-cape-group {
transform-box: fill-box;
transform-origin: center;
}
.pid-vamp-face--friendly {
opacity: 1;
}
.pid-vamp-face--scare {
opacity: 0;
}
.pid-vamp-icon-rig.is-scaring {
animation: pidVampShake 900ms ease;
}
.pid-vamp-icon-rig.is-scaring .pid-vamp-cape-group {
animation: pidVampCapeBurst 900ms ease;
}
.pid-vamp-icon-rig.is-scaring .pid-vamp-face--friendly {
animation: pidVampFaceOut 900ms ease;
}
.pid-vamp-icon-rig.is-scaring .pid-vamp-face--scare {
animation: pidVampFaceIn 900ms ease;
}
@keyframes pidVampFaceOut {
0% {
opacity: 1;
}
14% {
opacity: 0.15;
}
22%, 70% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes pidVampFaceIn {
0% {
opacity: 0;
}
14% {
opacity: 0.85;
}
22%, 70% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes pidVampCapeBurst {
0%, 100% {
transform: scale(1) rotate(0deg);
}
16% {
transform: scale(1.12) rotate(-3deg);
}
30% {
transform: scale(1.05) rotate(1.5deg);
}
55% {
transform: scale(1.07) rotate(-1deg);
}
}
@keyframes pidVampShake {
0% {
transform: translateX(0) scale(1);
filter: drop-shadow(0 0 0 rgba(158, 50, 65, 0));
}
10% {
transform: translateX(-2.4px) scale(1.05);
}
18% {
transform: translateX(2.6px) scale(1.08);
filter: drop-shadow(0 0 10px rgba(158, 50, 65, 0.6));
}
28% {
transform: translateX(-1.8px) scale(1.04);
}
38% {
transform: translateX(1.4px) scale(1.02);
filter: drop-shadow(0 0 12px rgba(158, 50, 65, 0.65));
}
55% {
transform: translateX(0) scale(1.01);
filter: drop-shadow(0 0 8px rgba(158, 50, 65, 0.4));
}
100% {
transform: translateX(0) scale(1);
filter: drop-shadow(0 0 0 rgba(158, 50, 65, 0));
}
}
/* scrollbar discreta */
* {
scrollbar-width: thin;

View File

@ -783,6 +783,51 @@
color: #ffffff;
}
/* Botão "Temas sazonais" (menu da conta) — liga/desliga temas sazonais
(Halloween etc.) por preferência do usuário (motivos: gosto pessoal ou,
no caso do Halloween, aracnofobia). Estado "Ativo" (padrão) usa a cor de
destaque, igual a um toggle ligado; "Inativo" fica neutro/apagado, igual
a um toggle desligado — mesmo padrão visual de outros toggles booleanos
do Portal (ex.: `.theme-swatch.is-active`, mas aqui como texto+ponto em
vez de amostra de cor, já que não há uma cor própria pra representar). */
.seasonal-theme-toggle {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: 6px 12px;
border-radius: var(--radius-pill);
border: 1px solid var(--border-subtle);
background: var(--bg-surface-raised);
color: var(--text-secondary);
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
.seasonal-theme-toggle:hover {
border-color: var(--accent);
}
.seasonal-theme-toggle__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
opacity: 0.5;
transition: opacity var(--transition-fast);
}
.seasonal-theme-toggle[aria-pressed="true"] {
background: var(--accent);
border-color: var(--accent);
color: #ffffff;
}
.seasonal-theme-toggle[aria-pressed="true"] .seasonal-theme-toggle__dot {
opacity: 1;
}
/* Notificações */
.notifications {
position: relative;

View File

@ -0,0 +1,321 @@
/* Teias de aranha decorativas de canto, tema sazonal Halloween — só no
canto superior-esquerdo e no inferior-direito (pedido explícito do
usuário, mesmos 2 cantos marcados nos prints que ele mandou), só no login
(`index.html`) e na tela Principal (`portal.html`) — nenhuma outra
página. Puramente decorativa (`pointer-events:none`, `z-index:-1` pra
ficar atrás de todo o conteúdo real, exceto a própria aranha — ver
"Clicar mata a aranha" abaixo), escondida por padrão (`hidden` no HTML) e
só exibida via `pidApplyHalloweenCobwebs()` (seasonal-theme.js) quando
`pidIsHalloweenActive()` — ver `docs/temas-sazonais/temas-sazonais.md`.
Desenho/cores validados no protótipo (artifact "Cantos Assombrados")
antes de aplicar aqui; o desenho da teia em si (fios/anéis) foi refeito
numa rodada seguinte a partir de uma referência visual do usuário, pra
parecer uma teia real (espiral levemente arqueada, mais fios, um fio
"perdido" se estendendo bem mais longe que o resto) em vez de um desenho
geométrico cru.
No login, ancorada em `.login-page` (`position:absolute`, ela já cobre
`min-height:100vh` e ganhou `isolation:isolate` em `login.css` — sem
isolar, o próprio fundo de `.login-page` pintava por cima da teia,
escondendo-a por completo). Na tela Principal, ancorada em
`.main-content` (mesma ideia, `isolation:isolate` em `layout.css`) — não
no viewport, porque a sidebar já é sólida e cobriria a teia do canto
esquerdo de qualquer jeito; assim a teia nunca fica escondida atrás dela.
A teia da Principal também é um pouco menor que a do 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 as duas foram aumentadas numa rodada
seguinte, "ficaram muito pequenas". */
.pw-corners {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: -1;
}
.pw-corner {
position: absolute;
pointer-events: none;
}
.pw-corner--tl {
top: 0;
left: 0;
}
.pw-corner--br {
bottom: 0;
right: 0;
}
/* O canto inferior-direito reaproveita o MESMO desenho/markup do
superior-esquerdo (a teia é autorada assumindo o canto no (0,0) do seu
próprio viewBox), só espelhando a TEIA em si (`.pw-strands`) em torno do
centro do viewBox — não a aranha nem a caveira. Espelhar o `<div
class="pw-corner--br">` inteiro (como era antes) virava a aranha de
cabeça pra baixo e fazia ela "cair" pra cima ao morrer (bug relatado pelo
usuário): `scale(-1,-1)` em qualquer ancestral inverte literalmente tudo
dentro, inclusive a direção "pra baixo" das animações de queda/subida.
Por isso a aranha e a caveira têm sua própria posição "espelhada" abaixo
(variáveis `--rx`/`--ry`/`--rr`/`--ox`/`--oy` e `--sx`/`--sy`,
recalculadas pra esse canto), sem nenhum `scale`/`rotate` adicional nelas
— assim ficam sempre "em pé" e caem/sobem na direção real da tela nos
dois cantos. `transform-box:view-box` garante que o centro (100,100) do
espelhamento é sempre o centro do viewBox autorado, não a caixa
delimitadora visual dos fios (que dispararia pro lado errado por causa
do fio "perdido", bem mais comprido que o resto). */
.pw-corner--br .pw-strands {
transform-box: view-box;
transform-origin: 100px 100px;
transform: scale(-1, -1);
}
/* Na Principal, a topbar (`position:sticky`, `z-index:20`, fundo opaco —
layout.css) pintava por cima da metade de cima da teia do canto
superior-esquerdo, escondendo a aranha; empurra a teia pra começar
logo abaixo dela (`var(--topbar-height)`, mesmo token que a topbar já
usa). Não se aplica ao canto inferior-direito (nada cobre o rodapé) nem
ao login (sem topbar). */
.pw-corners--portal .pw-corner--tl {
top: var(--topbar-height);
}
.pw-corners--login .pw-corner {
width: clamp(220px, 22vw, 320px);
}
.pw-corners--portal .pw-corner {
width: clamp(160px, 15vw, 230px);
}
.pw-corner svg {
display: block;
width: 100%;
height: auto;
overflow: visible;
}
.pw-strands {
fill: none;
stroke: rgba(242, 237, 227, 0.24);
stroke-width: 0.7;
stroke-linecap: round;
animation: pwShimmer 6.5s ease-in-out infinite;
}
.pw-strand-stray {
stroke: rgba(242, 237, 227, 0.16);
}
.pw-ring {
stroke: rgba(242, 237, 227, 0.16);
stroke-width: 0.6;
}
.pw-legs {
stroke: #17111d;
stroke-width: 1.6;
stroke-linecap: round;
fill: none;
transform-box: fill-box;
transform-origin: center;
animation: pwLegsWiggle 0.85s ease-in-out infinite;
}
.pw-abdomen,
.pw-head {
fill: #17111d;
}
.pw-eye {
fill: #9e3241;
}
.pw-spider {
/* Posição/ângulo de descanso (--r*) e do "passeio" pra fora (--o*) — o
canto inferior-direito redefine essas 4 variáveis abaixo com os valores
espelhados (200-x, 200-y, +180deg), em vez de espelhar a aranha via
`transform` (ver comentário grande em `.pw-corner--br .pw-strands`). */
--rx: 85px;
--ry: 113px;
--rr: 51deg;
--ox: 138px;
--oy: 178px;
transform: translate(var(--rx), var(--ry)) rotate(var(--rr));
animation: pwWalk 8s ease-in-out infinite;
/* Corpo/pernas são quase a mesma cor do fundo escuro padrão (#17111d vs
--bg-canvas #121017) — sem esse brilho claro a aranha praticamente some,
sobrando só o olho vermelho visível. Removido no tema claro abaixo
(lá o contraste já é natural: aranha escura sobre fundo claro). */
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.6)) drop-shadow(0 0 5px rgba(255, 255, 255, 0.3));
/* Única parte "viva" com `pointer-events:auto` — o resto da teia
(`.pw-corners`/`.pw-corner`) é `pointer-events:none`, decorativo. */
pointer-events: auto;
cursor: pointer;
}
.pw-corner--br .pw-spider {
--rx: 115px;
--ry: 87px;
--rr: 231deg;
--ox: 62px;
--oy: 22px;
}
/* Clicar mata a aranha (pedido explícito do usuário) — some caindo/girando,
pernas encolhem, e uma caveirinha sobe da teia (ver `.pw-skull` abaixo);
a teia fica vazia pelo resto desta visita à página (só volta com um F5
de verdade — `pidApplyHalloweenCobwebs()`, seasonal-theme.js, reconstrói
tudo do zero a cada carregamento). `forwards` mantém o estado final
(invisível) depois que a animação termina, sem precisar de mais JS. Os
deslocamentos em `calc()` são sempre "queda pra baixo" (`--ry` + valor
positivo) em relação ao próprio ponto de descanso do canto, então caem
do jeito certo nos dois cantos sem precisar de uma segunda animação. */
.pw-spider.is-dying {
animation: pwSpiderDie 950ms ease-in forwards;
pointer-events: none;
}
/* Aranha já morta numa visita anterior (persistido em `localStorage`, ver
`pidGetDeadSpiders()`/`pidApplyHalloweenCobwebs()` em seasonal-theme.js)
— nasce direto no estado final, sem tocar `pwSpiderDie`/`pwWalk` de novo;
a caveira nem chega a aparecer (já subiu e sumiu na visita em que a
aranha morreu, não faz sentido reprisar isso a cada carregamento). Só
volta com o atalho Tab+B. */
.pw-spider.is-dead {
animation: none;
opacity: 0;
pointer-events: none;
}
.pw-spider.is-dying .pw-legs {
animation: pwLegsDie 450ms ease-out forwards;
}
.pw-skull {
--sx: 85px;
--sy: 105px;
opacity: 0;
transform: translate(var(--sx), var(--sy)) scale(0.4);
transform-box: fill-box;
transform-origin: center;
pointer-events: none;
}
.pw-corner--br .pw-skull {
--sx: 115px;
--sy: 95px;
}
.pw-skull circle {
fill: #f2ede3;
}
.pw-skull-socket {
fill: #17111d;
}
.pw-skull path {
stroke: #17111d;
stroke-width: 0.9;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.pw-skull.is-rising {
animation: pwSkullRise 1300ms ease-out forwards;
}
/* Tema claro: a teia (cor creme, pensada pra contrastar com fundo escuro)
ficaria invisível sobre o fundo branco do tema claro — vira escura. A
aranha já é escura por natureza (#17111d), então já contrasta sozinha
contra um fundo claro; o brilho branco de cima (pensado pro fundo escuro)
só seria ruído aqui, por isso desligado. */
:root[data-theme="light"] .pw-strands {
stroke: rgba(20, 16, 28, 0.32);
}
:root[data-theme="light"] .pw-strand-stray,
:root[data-theme="light"] .pw-ring {
stroke: rgba(20, 16, 28, 0.2);
}
:root[data-theme="light"] .pw-spider {
filter: none;
}
@keyframes pwShimmer {
0%, 100% {
opacity: 0.85;
}
50% {
opacity: 1;
}
}
@keyframes pwLegsWiggle {
0%, 100% {
transform: rotate(0deg) scaleY(1);
}
50% {
transform: rotate(1.4deg) scaleY(0.95);
}
}
@keyframes pwWalk {
0%, 14% {
transform: translate(var(--rx), var(--ry)) rotate(var(--rr));
}
32%, 55% {
transform: translate(var(--ox), var(--oy)) rotate(var(--rr));
}
74%, 100% {
transform: translate(var(--rx), var(--ry)) rotate(var(--rr));
}
}
@keyframes pwSpiderDie {
0% {
transform: translate(var(--rx), var(--ry)) rotate(var(--rr));
opacity: 1;
}
45% {
transform: translate(calc(var(--rx) + 8px), calc(var(--ry) + 52px)) rotate(calc(var(--rr) + 99deg));
opacity: 1;
}
100% {
transform: translate(calc(var(--rx) + 13px), calc(var(--ry) + 117px)) rotate(calc(var(--rr) + 209deg));
opacity: 0;
}
}
@keyframes pwLegsDie {
to {
transform: rotate(22deg) scaleY(0.72);
}
}
@keyframes pwSkullRise {
0% {
opacity: 0;
transform: translate(var(--sx), var(--sy)) scale(0.35);
}
20% {
opacity: 1;
transform: translate(var(--sx), calc(var(--sy) - 15px)) scale(1.15);
}
32% {
transform: translate(var(--sx), calc(var(--sy) - 11px)) scale(1);
}
78% {
opacity: 1;
transform: translate(var(--sx), calc(var(--sy) - 37px)) scale(1);
}
100% {
opacity: 0;
transform: translate(var(--sx), calc(var(--sy) - 47px)) scale(0.9);
}
}

View File

@ -360,6 +360,13 @@
}
.main-content {
/* `isolation:isolate` garante um contexto de empilhamento próprio — sem
isso, `position:relative` sozinho (z-index auto) não isola nada, e a
teia de aranha sazonal (z-index:-1, ver halloween-cobweb.css) escaparia
pro contexto raiz da página em vez de ficar só atrás do conteúdo desta
área. Mesmo raciocínio de `isolation` em `.login-page` (login.css). */
position: relative;
isolation: isolate;
min-width: 0;
transition: opacity var(--transition-base);
}

View File

@ -0,0 +1,259 @@
/* Intro pós-login sazonal "Halloween" (vampiro) — variante de
login-intro.css, ativa só quando `pidIsHalloweenActive()`
(seasonal-theme.js) é verdadeiro; fora da janela de outubro,
login-intro.css/login-intro.js (arquivo original, intocado) continuam
sendo os únicos usados. Porta a composição de cenas Rise/Scare/Settle/
Wordmark autorada em "Logo intro animation Halloween/export-halloween/
pid-intro-halloween.html" (pasta de logos, fora do repositório) — ver
`login-intro-halloween.js` e `docs/temas-sazonais/temas-sazonais.md`. Mesma
paleta da marca P.I.D., acrescida do vinho da capa (#7A2733/#9E3241),
hardcoded aqui como no arquivo de origem (não são tokens de tema). Só
usado em index.html. */
.login-intro-h {
position: fixed;
inset: 0;
z-index: 200;
background: radial-gradient(120% 80% at 50% 38%, rgba(123, 91, 168, 0.16), transparent 60%), #151022;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
opacity: 0;
transition: opacity 500ms ease;
}
.login-intro-h.is-visible {
opacity: 1;
}
.login-intro-h__flash {
position: absolute;
inset: 0;
background: #9e3241;
opacity: 0;
mix-blend-mode: screen;
pointer-events: none;
}
.login-intro-h__stage {
display: flex;
align-items: center;
gap: 36px;
}
.login-intro-h__icon {
width: 200px;
height: 210px;
flex: none;
overflow: visible;
opacity: 0;
transform: translate(0, 0) scale(1);
}
.login-intro-h__cape path {
fill: #2e2740;
}
.login-intro-h__cape-line {
stroke: #3d3454;
stroke-width: 1;
fill: none;
}
.login-intro-h__collar {
fill: #7a2733;
}
.login-intro-h__body {
fill: #7b5ba8;
}
.login-intro-h__tab {
fill: #c6a24a;
}
.login-intro-h__tab-shadow {
fill: #7b5ba8;
}
.login-intro-h__screen {
fill: #241c33;
}
.login-intro-h__face {
opacity: 0;
}
.login-intro-h__face--scare circle:not(.login-intro-h__pupil-red) {
fill: #f2ede3;
}
.login-intro-h__pupil-red {
fill: #9e3241;
}
.login-intro-h__brow-angry {
stroke: #7a2733;
stroke-width: 1.9;
fill: none;
stroke-linecap: round;
}
.login-intro-h__mouth-fill {
fill: #0c0814;
}
.login-intro-h__mouth {
stroke: #c6a24a;
stroke-width: 3;
fill: none;
stroke-linecap: round;
}
.login-intro-h__fang {
fill: #f2ede3;
}
.login-intro-h__face--friendly circle:not(.login-intro-h__pupil) {
fill: #f2ede3;
}
.login-intro-h__pupil {
fill: #241c33;
}
.login-intro-h__brow {
stroke: #7a2733;
stroke-width: 1.3;
fill: none;
stroke-linecap: round;
}
.login-intro-h__mouth-friendly {
stroke: #c6a24a;
stroke-width: 3;
fill: none;
stroke-linecap: round;
}
.login-intro-h__fang-friendly {
fill: #f2ede3;
}
/* A capa fica "levantada" cobrindo o rosto até a cena Scare abrir — por
isso, ao contrário de `.login-intro-h__face` (que começa escondido),
começa visível (T=0 já é capa cobrindo, ver `flapGoneFn` em
login-intro-halloween.js). */
.login-intro-h__flap-wrap {
opacity: 1;
}
.login-intro-h__flap-rotate path:first-child {
fill: #2e2740;
}
/* Olhos semicerrados espiando por cima da capa levantada — também visíveis
desde o início (T=0), mesmo raciocínio do comentário acima. */
.login-intro-h__squint {
opacity: 1;
}
.login-intro-h__squint path {
stroke: #f2ede3;
stroke-width: 2.6;
fill: none;
stroke-linecap: round;
}
.login-intro-h__squint circle {
fill: #9e3241;
}
.login-intro-h__wordmark {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
opacity: 0;
}
.login-intro-h__letters {
display: flex;
align-items: baseline;
gap: 2px;
}
.login-intro-h__letter {
display: inline-block;
font-family: "Creepster", "Space Grotesk", sans-serif;
font-weight: 400;
font-size: 3.4rem;
letter-spacing: 0.02em;
color: #f2ede3;
opacity: 0;
text-shadow: 0 0 16px rgba(122, 39, 51, 0.75), 0 3px 0 rgba(0, 0, 0, 0.4);
}
.login-intro-h__dot {
color: #c6a24a;
}
.login-intro-h__rule {
width: 240px;
height: 3px;
background: linear-gradient(90deg, #c6a24a, #7a2733);
transform-origin: left center;
transform: scaleX(0);
}
.login-intro-h__tagline {
font-family: "DM Mono", ui-monospace, monospace;
font-size: 0.85rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #c6a24a;
opacity: 0;
}
/* Tema claro: mesmo raciocínio de login-intro.css — fundo escuro fixo
destoava do resto do Portal nesse tema, então vira `--bg-canvas` claro; o
texto do wordmark (pensado pra contrastar com fundo escuro) precisa virar
escuro. O ícone não precisa de override (mesmo motivo de login-intro.css:
o roxo/vinho tem contraste de sobra nos dois fundos). */
:root[data-theme="light"] .login-intro-h {
background: radial-gradient(120% 80% at 50% 38%, rgba(123, 91, 168, 0.1), transparent 60%), var(--bg-canvas);
}
:root[data-theme="light"] .login-intro-h__letter {
color: #3a1620;
text-shadow: 0 0 10px rgba(122, 39, 51, 0.35), 0 2px 0 rgba(0, 0, 0, 0.12);
}
:root[data-theme="light"] .login-intro-h__tagline {
color: #7a2733;
}
@media (max-width: 600px) {
.login-intro-h__stage {
gap: 20px;
}
.login-intro-h__icon {
width: 130px;
height: 137px;
}
.login-intro-h__letter {
font-size: 2.2rem;
}
.login-intro-h__rule {
width: 160px;
}
.login-intro-h__tagline {
font-size: 0.65rem;
letter-spacing: 0.16em;
}
}

View File

@ -1,4 +1,12 @@
.login-page {
/* Isola um contexto de empilhamento próprio — sem isso, o fundo desta
regra (abaixo) é conteúdo estático normal e pinta por CIMA de qualquer
filho com z-index negativo (caso da teia de aranha sazonal, ver
halloween-cobweb.css), já que `position:relative` sozinho (sem z-index)
não cria um novo contexto. Com `isolation:isolate`, a teia (z-index:-1,
filha direta daqui) fica corretamente atrás deste fundo e na frente
do `.login-card`. */
isolation: isolate;
min-height: 100vh;
display: flex;
align-items: center;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -47,6 +47,32 @@ document.addEventListener("DOMContentLoaded", async () => {
});
}
/* Toggle "Temas sazonais" — liga/desliga temas sazonais (Halloween etc.),
ver seasonal-theme.js. Recarrega a página ao alternar: o tema sazonal já
foi aplicado (ou não) via troca de innerHTML/markup no carregamento
desta mesma página, então não dá pra simplesmente desfazer isso in-place
— o reload garante que tudo (ícone, teias, favicon) reflita a nova
preferência de forma correta e imediata (importante sobretudo pra quem
desligou por aracnofobia — a aranha precisa sumir na hora, não só na
próxima navegação). */
const seasonalToggleBtn = document.getElementById("seasonal-theme-toggle");
const seasonalToggleLabel = document.getElementById("seasonal-theme-toggle-label");
if (seasonalToggleBtn && seasonalToggleLabel && typeof pidSeasonalThemesOptedOut === "function") {
const syncSeasonalToggle = () => {
const optedOut = pidSeasonalThemesOptedOut();
seasonalToggleBtn.setAttribute("aria-pressed", String(!optedOut));
seasonalToggleLabel.textContent = optedOut ? "Inativo" : "Ativo";
};
syncSeasonalToggle();
seasonalToggleBtn.addEventListener("click", () => {
const optedOut = pidSeasonalThemesOptedOut();
pidSetSeasonalThemesOptedOut(!optedOut);
window.location.reload();
});
}
chipBtn.addEventListener("click", (event) => {
event.stopPropagation();
dropdown.hidden = !dropdown.hidden;

View File

@ -64,7 +64,11 @@ document.addEventListener("DOMContentLoaded", async () => {
redirectOn401: false,
body: { username, password },
});
pidPlayLoginIntro(() => {
const tocaIntro =
typeof pidIsHalloweenActive === "function" && pidIsHalloweenActive() && typeof pidPlayLoginIntroHalloween === "function"
? pidPlayLoginIntroHalloween
: pidPlayLoginIntro;
tocaIntro(() => {
sessionStorage.setItem("pid_reveal_portal", "1");
window.location.href = "portal.html";
});

View File

@ -0,0 +1,174 @@
/*
* Intro pós-login sazonal "Halloween" — variante vampiro de
* pidPlayLoginIntro (login-intro.js), portada da composição autorada em
* "Logo intro animation Halloween/export-halloween/pid-intro-halloween.html"
* (pasta de logos, fora do repositório; cenas Rise/Scare/Settle/Wordmark,
* 1.3s/1.5s/1.4s/1.8s). Reaproveita PID_INTRO_EASING/pidIntroAnimate/
* pidIntroClamp/pidIntroCornerTarget já definidos em login-intro.js (mesma
* página, index.html, sempre presente) em vez de duplicá-los — só a
* coreografia (cues/timing/elementos) é própria daqui. Só é chamada quando
* `pidIsHalloweenActive()` (seasonal-theme.js); fora da janela de outubro,
* pidPlayLoginIntro() (arquivo original, intocado) continua sendo a única
* usada.
*
* Diferença em relação à composição original: ela termina com a wordmark e
* o ícone sumindo juntos (sem "pouso" nenhum, pensada como vídeo solto); aqui
* foi acrescentada uma cena extra "Fly" (mesma ideia/matemática do
* `pidIntroCornerTarget` de login-intro.js) pra o ícone voar até o canto da
* sidebar antes de tudo desaparecer, mesmo espírito de adaptação já usado no
* intro normal ("só o destino final do voo foi recalibrado").
*/
const PID_INTRO_H_CUES = { Rise: 0, Scare: 1.3, Settle: 2.8, Wordmark: 4.2, Fly: 6.0 };
const PID_INTRO_H_TOTAL = 6.9;
const PID_INTRO_H_ICON_K = 200 / 460; // coreografia original autorada pra um ícone de 460px; reescala pros nossos 200px
function pidPlayLoginIntroHalloween(onDone) {
const overlay = document.getElementById("login-intro-halloween");
const icon = document.getElementById("login-intro-h-icon");
const stage = document.getElementById("login-intro-h-stage");
const wordmark = document.getElementById("login-intro-h-wordmark");
const flash = document.getElementById("login-intro-h-flash");
if (!overlay || !icon || !stage || !wordmark || !flash) {
onDone();
return;
}
const cape = icon.querySelector(".login-intro-h__cape");
const collar = icon.querySelector(".login-intro-h__collar");
const faceScare = icon.querySelector(".login-intro-h__face--scare");
const faceFriendly = icon.querySelector(".login-intro-h__face--friendly");
const flapWrap = icon.querySelector(".login-intro-h__flap-wrap");
const flapRotate = document.getElementById("login-intro-h-flap-rotate");
const squint = icon.querySelector(".login-intro-h__squint");
const letters = Array.from(wordmark.querySelectorAll(".login-intro-h__letter"));
const rule = wordmark.querySelector(".login-intro-h__rule");
const tagline = wordmark.querySelector(".login-intro-h__tagline");
const { easeOutCubic, easeInOutQuad, easeOutBack } = PID_INTRO_EASING;
const easeOutExpo = (t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
const CUES = PID_INTRO_H_CUES;
const K = PID_INTRO_H_ICON_K;
const riseFn = pidIntroAnimate({ from: 46, to: 0, start: CUES.Rise + 0.1, end: CUES.Rise + 1.1, ease: easeOutCubic });
const appearFn = pidIntroAnimate({ start: CUES.Rise + 0.1, end: CUES.Rise + 0.75, ease: easeOutCubic });
const openFn = pidIntroAnimate({ start: CUES.Scare + 0.02, end: CUES.Scare + 0.42, ease: easeOutExpo });
const settleSwayFn = pidIntroAnimate({ start: CUES.Settle + 0.3, end: CUES.Settle + 1.0, ease: easeInOutQuad });
const flapGoneFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Scare + 0.42, end: CUES.Scare + 0.95, ease: easeInOutQuad });
const lungeFn = pidIntroAnimate({ start: CUES.Scare + 0.05, end: CUES.Scare + 0.32, ease: easeOutExpo });
const relaxFn = pidIntroAnimate({ start: CUES.Scare + 0.75, end: CUES.Settle + 0.55, ease: easeInOutQuad });
const toFriendlyFn = pidIntroAnimate({ start: CUES.Settle + 0.2, end: CUES.Settle + 0.85, ease: easeInOutQuad });
const shiftFn = pidIntroAnimate({ start: CUES.Wordmark - 0.3, end: CUES.Wordmark + 0.55, ease: easeOutCubic });
const ruleFn = pidIntroAnimate({ start: CUES.Wordmark + 0.55, end: CUES.Wordmark + 1.1, ease: easeInOutQuad });
const tagFn = pidIntroAnimate({ start: CUES.Wordmark + 0.7, end: CUES.Wordmark + 1.25, ease: easeOutCubic });
const letterFns = letters.map((_, i) => {
const s = CUES.Wordmark + 0.05 + i * 0.14;
return {
o: pidIntroAnimate({ start: s, end: s + 0.45, ease: easeOutCubic }),
y: pidIntroAnimate({ from: 30, to: 0, start: s, end: s + 0.6, ease: easeOutBack }),
};
});
const flyFn = pidIntroAnimate({ start: CUES.Fly + 0.05, end: CUES.Fly + 0.55, ease: easeOutCubic });
const wordmarkOutFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Fly, end: CUES.Fly + 0.3, ease: easeOutCubic });
const stageFadeFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Fly + 0.5, end: CUES.Fly + 0.9, ease: easeOutCubic });
const SHIFT_X = -110;
const ICON_SIZE = 200;
const FINAL_ICON_SIZE = 40;
const FINAL_SCALE = FINAL_ICON_SIZE / ICON_SIZE;
const CORNER_MARGIN = 40;
const loginCard = document.querySelector(".login-card");
if (loginCard) loginCard.classList.add("is-leaving");
overlay.hidden = false;
void overlay.offsetWidth;
overlay.classList.add("is-visible");
let start;
function frame(now) {
const T = Math.min((now - start) / 1000, PID_INTRO_H_TOTAL);
const rise = riseFn(T);
const appear = appearFn(T);
const open = openFn(T);
const settleSway = settleSwayFn(T);
const flapGone = flapGoneFn(T);
const flapAngle = -64 * open + settleSway * 6;
const lunge = lungeFn(T);
const relax = relaxFn(T);
const push = pidIntroClamp(lunge - relax, 0, 1);
const shakeWin = pidIntroClamp(1 - Math.abs(T - (CUES.Scare + 0.34)) / 0.4, 0, 1);
const shake = Math.sin(T * 78) * 4.2 * shakeWin;
const toFriendly = toFriendlyFn(T);
const scare = pidIntroClamp(open * 1.6, 0, 1) * (1 - toFriendly);
const friendly = toFriendly;
const squintOpacity = pidIntroClamp(1 - open * 2.4, 0, 1);
const glow = pidIntroClamp(push * 1.1, 0, 1);
icon.style.opacity = String(appear);
const shift = shiftFn(T);
const fly = flyFn(T);
const lockupX = SHIFT_X * shift;
const lockupScale = 1 - 0.15 * shift;
const target = pidIntroCornerTarget(ICON_SIZE, FINAL_ICON_SIZE, CORNER_MARGIN);
const flyX = lockupX + (target.x - lockupX) * fly;
const flyY = target.y * fly;
const flyScale = lockupScale + (FINAL_SCALE - lockupScale) * fly;
const baseX = shake * K;
const baseY = (rise - push * 26) * K;
const baseScale = 1 + push * 0.14;
icon.style.transform = `translate(${baseX + flyX}px, ${baseY + flyY}px) scale(${baseScale * flyScale})`;
icon.style.filter = `drop-shadow(0 ${(10 * K).toFixed(1)}px ${(24 * K).toFixed(1)}px rgba(0,0,0,0.5)) drop-shadow(0 0 ${((18 + glow * 46) * K).toFixed(1)}px rgba(158,50,65,${(0.18 + glow * 0.5).toFixed(2)}))`;
cape.setAttribute("transform", `translate(38 44) scale(${1 + push * 0.2} ${1 + push * 0.06}) translate(-38 -44)`);
collar.style.fill = glow > 0.05 ? "#9e3241" : "#7a2733";
faceScare.style.opacity = String(scare);
faceFriendly.style.opacity = String(friendly);
squint.style.opacity = String(squintOpacity);
flapWrap.style.opacity = String(flapGone);
flapRotate.setAttribute("transform", `rotate(${flapAngle} 62 46)`);
wordmark.style.opacity = String(pidIntroClamp(shift * 1.4, 0, 1) * wordmarkOutFn(T));
letters.forEach((el, i) => {
el.style.opacity = String(letterFns[i].o(T));
el.style.transform = `translateY(${letterFns[i].y(T)}px)`;
});
rule.style.transform = `scaleX(${ruleFn(T)})`;
const tag = tagFn(T);
tagline.style.opacity = String(tag);
tagline.style.transform = `translateY(${(1 - tag) * 12}px)`;
const flashWin = pidIntroClamp(1 - Math.abs(T - (CUES.Scare + 0.16)) / 0.26, 0, 1);
flash.style.opacity = String(flashWin * 0.34);
stage.style.opacity = String(stageFadeFn(T));
if (T < PID_INTRO_H_TOTAL) {
requestAnimationFrame(frame);
} else {
onDone();
}
}
setTimeout(() => {
start = performance.now();
requestAnimationFrame(frame);
}, PID_INTRO_FADE_MS);
}

View File

@ -1,15 +1,32 @@
/*
* Ícone "P.I.D." do card de login (`#login-logo-btn`) — clicar faz os olhos
* do disquete piscarem (`.pid-icon-eye`/`pidIconBlink` em base.css, mesmo
* mecanismo da logo da sidebar em sidebar-brand.js). Sem navegação nenhuma
* aqui — já estamos na tela de login, é só um toque de graça ao clicar no
* mascote, não um atalho pra lugar nenhum.
* Ícone "P.I.D." do card de login (`#login-logo-btn`). Fora do tema sazonal
* Halloween: clicar faz os olhos do disquete piscarem
* (`.pid-icon-eye`/`pidIconBlink` em base.css, mesmo mecanismo da logo da
* sidebar em sidebar-brand.js). Durante o Halloween
* (`pidIsHalloweenActive()`, seasonal-theme.js): o ícone vira o vampiro
* (`pidApplyVampireIcon`) e o clique dispara a animação de "susto"
* (`pidTriggerVampireScare`, `.is-scaring` em base.css) em vez da piscada.
* Sem navegação nenhuma aqui — já estamos na tela de login, é só um toque de
* graça ao clicar no mascote.
*/
document.addEventListener("DOMContentLoaded", () => {
const btn = document.getElementById("login-logo-btn");
if (!btn) return;
const halloween = typeof pidIsHalloweenActive === "function" && pidIsHalloweenActive();
if (halloween) {
pidApplyVampireIcon(btn.querySelector(".login-card__logo"));
if (typeof pidApplySeasonalFavicon === "function") pidApplySeasonalFavicon();
if (typeof pidApplyHalloweenCobwebs === "function") pidApplyHalloweenCobwebs("pw-cobwebs-login");
}
btn.addEventListener("click", () => {
if (halloween) {
pidTriggerVampireScare(btn);
return;
}
btn.querySelectorAll(".pid-icon-eye").forEach((eye) => {
eye.classList.remove("is-blinking");
void eye.getBoundingClientRect();

304
static/js/seasonal-theme.js Normal file
View File

@ -0,0 +1,304 @@
/*
* Tema sazonal "Halloween" do Portal — troca a marca P.I.D. (favicon, ícone
* do login, ícone/assinatura da sidebar) pela variante "vampiro" durante uma
* janela de datas (outubro) e troca a piscada de olhos ao clicar na logo por
* uma animação de "susto" (capa se abre, olhos ficam vermelhos e raivosos,
* presas de fora, tremida + brilho vermelho) só nesse período — fora da
* janela, tudo volta ao P.I.D. normal (`pid-icon-eye`/`is-blinking`). Ver
* `docs/temas-sazonais/temas-sazonais.md`. Consumido por `sidebar-brand.js`,
* `login-logo-blink.js` e `auth.js` (troca de intro); incluído em toda
* página (mesmo padrão de `confirm-modal.js`) por isso mora aqui, não num
* arquivo específico de página.
*
* PID_HALLOWEEN_PREVIEW_FORCE forçava o tema ligado o ano todo pra revisão
* visual; validado e desligado pelo usuário — a partir daqui só a checagem
* de mês abaixo decide (outubro, todo ano).
*/
const PID_HALLOWEEN_PREVIEW_FORCE = false;
const PID_HALLOWEEN_MES = 9; // outubro, 0-indexed (Date#getMonth())
const PID_HALLOWEEN_SCARE_MS = 900;
/* Preferência do usuário (menu da conta → "Temas sazonais") pra desligar
qualquer tema sazonal (hoje só o Halloween, mas vale pra futuros também)
e manter sempre o P.I.D. padrão — motivos: gosto pessoal ou, no caso do
Halloween, aracnofobia. Mesmo padrão de armazenamento do tema claro/escuro
(`theme.js`, só `localStorage`, nunca migrado pro banco — é preferência
de navegador, não dado de negócio). Tem prioridade sobre tudo, inclusive
`PID_HALLOWEEN_PREVIEW_FORCE`: se o usuário desligou, fica desligado. */
const PID_SEASONAL_OPT_OUT_KEY = "pid_seasonal_theme_opt_out";
function pidSeasonalThemesOptedOut() {
try {
return localStorage.getItem(PID_SEASONAL_OPT_OUT_KEY) === "1";
} catch (e) {
return false;
}
}
function pidSetSeasonalThemesOptedOut(optedOut) {
try {
if (optedOut) {
localStorage.setItem(PID_SEASONAL_OPT_OUT_KEY, "1");
} else {
localStorage.removeItem(PID_SEASONAL_OPT_OUT_KEY);
}
} catch (e) {
// localStorage indisponível (modo privado, storage bloqueado etc.) — a
// preferência simplesmente não persiste, mas a página continua funcionando.
}
}
function pidIsHalloweenActive(data) {
if (pidSeasonalThemesOptedOut()) return false;
if (PID_HALLOWEEN_PREVIEW_FORCE) return true;
const hoje = data || new Date();
return hoje.getMonth() === PID_HALLOWEEN_MES;
}
/* Usa `pid-halloween-icone-escuro.svg` (o mesmo ícone/cores do vampiro
usado na sidebar e no login), não `pid-halloween-favicon.svg` — esse
último é uma variante simplificada (sem capa/gola, cores diferentes,
pensada só pra tamanhos bem pequenos) e destoava do resto da marca sazonal
na aba do navegador. Mesmo padrão do P.I.D. normal, cujo favicon também
usa `pid-icone-escuro.svg` em vez do `pid-favicon.svg` dedicado — ver
"Logos em `static/img/`" no CLAUDE.md. */
function pidApplySeasonalFavicon() {
const link = document.querySelector('link[rel="icon"]');
if (link && link.href.indexOf("pid-icone-escuro.svg") !== -1) {
link.href = link.href.replace("pid-icone-escuro.svg", "pid-halloween-icone-escuro.svg");
}
}
const PID_VAMP_FACE_FRIENDLY = `
<circle cx="30" cy="58" r="5" fill="#F2EDE3"></circle>
<circle cx="46" cy="58" r="5" fill="#F2EDE3"></circle>
<circle cx="32" cy="56.2" r="1.6" fill="#241C33"></circle>
<circle cx="48" cy="56.2" r="1.6" fill="#241C33"></circle>
<path d="M26 63.2 q4 2.6 8 -0.5" stroke="#7A2733" stroke-width="1.3" fill="none" stroke-linecap="round"></path>
<path d="M42 63.2 q4 2.6 8 -0.5" stroke="#7A2733" stroke-width="1.3" fill="none" stroke-linecap="round"></path>
<path d="M31 67 q7 6 14 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
<path d="M33.6 68.6 L36.4 68.9 L35 72 Z" fill="#F2EDE3"></path>
<path d="M40.2 68.9 L43 68.6 L41.6 72 Z" fill="#F2EDE3"></path>`;
const PID_VAMP_FACE_SCARE = `
<circle cx="30" cy="57.4" r="5.6" fill="#F2EDE3"></circle>
<circle cx="46" cy="57.4" r="5.6" fill="#F2EDE3"></circle>
<circle cx="30" cy="57.4" r="2.1" fill="#9E3241"></circle>
<circle cx="46" cy="57.4" r="2.1" fill="#9E3241"></circle>
<path d="M24.6 51.4 L34.6 54.2" stroke="#7A2733" stroke-width="1.9" fill="none" stroke-linecap="round"></path>
<path d="M51.4 51.4 L41.4 54.2" stroke="#7A2733" stroke-width="1.9" fill="none" stroke-linecap="round"></path>
<path d="M29 66.4 q9 7.4 18 0 q-9 3 -18 0 Z" fill="#0C0814"></path>
<path d="M29 66.4 q9 7.4 18 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
<path d="M31.4 67.4 L34.6 67.9 L32.9 73 Z" fill="#F2EDE3"></path>
<path d="M41.4 67.9 L44.6 67.4 L43.1 73 Z" fill="#F2EDE3"></path>`;
function pidVampireIconRigMarkup() {
return `
<g class="pid-vamp-cape-group">
<path d="M4 9 L20 19 Q38 24 56 19 L72 9 C74 32 74 56 71 78 L5 78 C2 56 2 32 4 9 Z" fill="#2E2740"></path>
<path class="pid-vamp-collar" d="M9 16 L21 22 Q38 26 55 22 L67 16 C68 26 68 31 66 36 L10 36 C8 31 8 26 9 16 Z" fill="#7A2733"></path>
</g>
<path d="M16 22 h34 l14 14 v40 a2 2 0 0 1 -2 2 H14 a2 2 0 0 1 -2 -2 V24 a2 2 0 0 1 2 -2 z" fill="#7B5BA8"></path>
<rect x="28" y="22" width="20" height="16" fill="#C6A24A"></rect>
<rect x="40" y="25" width="5" height="10" fill="#7B5BA8"></rect>
<rect x="18" y="46" width="40" height="28" rx="2" fill="#241C33"></rect>
<g class="pid-vamp-face pid-vamp-face--friendly">${PID_VAMP_FACE_FRIENDLY}</g>
<g class="pid-vamp-face pid-vamp-face--scare">${PID_VAMP_FACE_SCARE}</g>`;
}
/* Substitui o conteúdo de um <svg> de ícone (viewBox 0 0 64 64, o P.I.D. de
sempre) pelo vampiro (viewBox 0 0 76 80) — usado tanto no ícone da sidebar
colapsada quanto no botão da logo do login. `dataset.vampApplied` evita
reaplicar se a função rodar de novo (não deveria, mas por segurança). */
function pidApplyVampireIcon(svg) {
if (!svg || svg.dataset.vampApplied) return;
svg.setAttribute("viewBox", "0 0 76 80");
svg.innerHTML = `<g class="pid-vamp-icon-rig">${pidVampireIconRigMarkup()}</g>`;
svg.dataset.vampApplied = "1";
}
/* Idem, pra assinatura horizontal (ícone + "P.I.D." + tagline) da sidebar
expandida — viewBox 0 0 320 96 (a versão vampiro é um pouco mais larga que
os 300x80 do P.I.D. normal, por causa da capa). */
function pidApplyVampireFull(svg) {
if (!svg || svg.dataset.vampApplied) return;
svg.setAttribute("viewBox", "0 0 320 96");
svg.innerHTML = `
<g transform="translate(4,8)"><g class="pid-vamp-icon-rig">${pidVampireIconRigMarkup()}</g></g>
<text x="96" y="50" font-family="'Space Grotesk',Helvetica,Arial,sans-serif" font-size="34" font-weight="700" letter-spacing="1.4" fill="#F2EDE3">P.I.D.</text>
<text x="98" y="69" font-family="'DM Mono',ui-monospace,monospace" font-size="9.5" letter-spacing="2.6" fill="#C6A24A">PORTAL INTERNO DA DE PAULA</text>`;
svg.dataset.vampApplied = "1";
}
/* Dispara a animação de "susto" (`.is-scaring`/`pidVampShake` etc. em
base.css) em todo `.pid-vamp-icon-rig` dentro de `root` — substitui a
piscada de olhos (`.pid-icon-eye`/`is-blinking`) enquanto o tema Halloween
está ativo. Remove e reaplica a classe (em vez de só adicionar) pra
permitir cliques repetidos re-tocarem a animação do zero. */
function pidTriggerVampireScare(root) {
root.querySelectorAll(".pid-vamp-icon-rig").forEach((rig) => {
rig.classList.remove("is-scaring");
void rig.getBoundingClientRect();
rig.classList.add("is-scaring");
});
}
/* Teia de aranha decorativa de canto (`static/css/halloween-cobweb.css`) —
markup só existe em memória (nunca hardcoded no template) pra não sobrar
nada no HTML fora da janela do Halloween. `.pw-spider`/`.pw-legs` (mesmas
classes usadas pela animação em CSS) andam/mexem sozinhas via
`@keyframes` contínuo, sem precisar de mais JS depois de injetadas. */
const PID_COBWEB_SVG = `<svg viewBox="0 0 200 200" role="img" aria-hidden="true">
<g class="pw-strands">
<line x1="0" y1="0" x2="200.0" y2="10.5"></line>
<line x1="0" y1="0" x2="200.0" y2="53.6"></line>
<line x1="0" y1="0" x2="200.0" y2="106.3"></line>
<line x1="0" y1="0" x2="200.0" y2="180.1"></line>
<line x1="0" y1="0" x2="134.9" y2="200.0"></line>
<line x1="0" y1="0" x2="76.8" y2="200.0"></line>
<line x1="0" y1="0" x2="35.3" y2="200.0"></line>
<line x1="0" y1="0" x2="7.0" y2="200.0"></line>
<line class="pw-strand-stray" x1="0" y1="0" x2="296.3" y2="46.9"></line>
<path class="pw-ring" d="M 30.0,1.6 Q 32.4,5.2 30.0,8.0 Q 32.4,13.0 30.0,16.0 Q 32.4,23.2 30.0,27.0 Q 27.1,30.8 20.2,30.0 Q 17.1,32.4 11.5,30.0 Q 9.1,32.4 5.3,30.0 Q 3.4,32.4 1.0,30.0"></path>
<path class="pw-ring" d="M 58.0,3.0 Q 62.6,10.0 58.0,15.5 Q 62.6,25.0 58.0,30.8 Q 62.6,44.9 58.0,52.2 Q 52.4,59.5 39.1,58.0 Q 33.1,62.6 22.3,58.0 Q 17.5,62.6 10.2,58.0 Q 6.6,62.6 2.0,58.0"></path>
<path class="pw-ring" d="M 90.0,4.7 Q 97.2,15.6 90.0,24.1 Q 97.2,38.9 90.0,47.9 Q 97.2,69.6 90.0,81.0 Q 81.4,92.4 60.7,90.0 Q 51.4,97.2 34.5,90.0 Q 27.2,97.2 15.9,90.0 Q 10.3,97.2 3.1,90.0"></path>
<path class="pw-ring" d="M 126.0,6.6 Q 136.1,21.8 126.0,33.8 Q 136.1,54.4 126.0,67.0 Q 136.1,97.4 126.0,113.5 Q 113.9,129.3 85.0,126.0 Q 72.0,136.1 48.4,126.0 Q 38.1,136.1 22.2,126.0 Q 14.4,136.1 4.4,126.0"></path>
<path class="pw-ring" d="M 166.0,8.7 Q 179.3,28.7 166.0,44.5 Q 179.3,71.7 166.0,88.3 Q 179.3,128.4 166.0,149.5 Q 150.1,170.4 112.0,166.0 Q 94.9,179.3 63.7,166.0 Q 50.2,179.3 29.3,166.0 Q 18.9,179.3 5.8,166.0"></path>
</g>
<g class="pw-spider">
<g class="pw-legs">
<path d="M-3,-2 L-14,-8 L-22,-6"></path>
<path d="M-2,-1 L-13,2 L-21,5"></path>
<path d="M-2,1 L-13,7 L-20,13"></path>
<path d="M-3,2 L-12,11 L-18,18"></path>
<path d="M3,-2 L14,-8 L22,-6"></path>
<path d="M2,-1 L13,2 L21,5"></path>
<path d="M2,1 L13,7 L20,13"></path>
<path d="M3,2 L12,11 L18,18"></path>
</g>
<ellipse class="pw-abdomen" cx="0" cy="6" rx="7" ry="9"></ellipse>
<circle class="pw-head" cx="0" cy="-6" r="4.5"></circle>
<circle class="pw-eye" cx="-1.6" cy="-7" r="0.9"></circle>
<circle class="pw-eye" cx="1.6" cy="-7" r="0.9"></circle>
</g>
<g class="pw-skull">
<circle cx="0" cy="0" r="6.5"></circle>
<ellipse class="pw-skull-socket" cx="-2.4" cy="-0.6" rx="1.5" ry="1.9"></ellipse>
<ellipse class="pw-skull-socket" cx="2.4" cy="-0.6" rx="1.5" ry="1.9"></ellipse>
<path d="M-1.2 3.4 L-0.4 2.2 L0.4 3.4 L1.2 2.2"></path>
</g>
</svg>`;
/* Quais aranhas já "morreram" (clicadas numa visita anterior) — persistido
pra sobreviver a um F5/nova navegação, pedido explícito do usuário
("guardar que a aranha foi morta... até executar o comando tab+b que
reseta a aranha"). Cada aranha tem um id próprio (`${containerId}__tl`/
`__br` — os dois cantos de cada uma das 2 páginas, 4 ids possíveis no
total) guardado como array JSON num único `localStorage`, mesmo padrão
de armazenamento das outras preferências sazonais (`pid_theme`,
`pid_seasonal_theme_opt_out`) — nunca migrado pro banco. */
const PID_DEAD_SPIDERS_KEY = "pid_seasonal_dead_spiders";
function pidGetDeadSpiders() {
try {
const raw = JSON.parse(localStorage.getItem(PID_DEAD_SPIDERS_KEY) || "[]");
return Array.isArray(raw) ? raw : [];
} catch (e) {
return [];
}
}
function pidMarkSpiderDead(spiderId) {
try {
const dead = pidGetDeadSpiders();
if (!dead.includes(spiderId)) {
dead.push(spiderId);
localStorage.setItem(PID_DEAD_SPIDERS_KEY, JSON.stringify(dead));
}
} catch (e) {
// localStorage indisponível — a aranha "morre" só nesta visita, sem persistir
}
}
function pidResetDeadSpiders() {
try {
localStorage.removeItem(PID_DEAD_SPIDERS_KEY);
} catch (e) {
// ignorado
}
}
/* Preenche o container (já presente e `hidden` no template, `#pw-cobwebs-login`
em index.html ou `#pw-cobwebs-portal` em portal.html) com os 2 cantos
(superior-esquerdo/inferior-direito, pedido explícito do usuário) e revela.
Se a aranha daquele canto já morreu numa visita anterior
(`pidGetDeadSpiders()`), nasce direto com `.is-dead` (sem tocar a
animação de novo — só um "estado final" instantâneo). Senão, liga o
clique (`.pw-spider`, `pointer-events:auto` só nela — o resto da teia
continua `pointer-events:none`): "morre" (cai girando e some, pernas
encolhem — `.is-dying`/`pwSpiderDie` em halloween-cobweb.css) e uma
caveirinha sobe da teia (`.pw-skull`, `.is-rising`/`pwSkullRise`),
deixando a teia vazia — e grava o óbito (`pidMarkSpiderDead()`), que só
é desfeito pelo atalho de teclado Tab+B (ver `pidBindSeasonalResetShortcut()`
abaixo). É um no-op seguro em qualquer outra página (o id não existe ali). */
function pidApplyHalloweenCobwebs(containerId) {
const el = document.getElementById(containerId);
if (!el || el.dataset.cobwebApplied) return;
el.innerHTML =
`<div class="pw-corner pw-corner--tl">${PID_COBWEB_SVG}</div>` +
`<div class="pw-corner pw-corner--br">${PID_COBWEB_SVG}</div>`;
el.hidden = false;
el.dataset.cobwebApplied = "1";
const deadSpiders = pidGetDeadSpiders();
el.querySelectorAll(".pw-corner").forEach((cornerEl) => {
const corner = cornerEl.classList.contains("pw-corner--tl") ? "tl" : "br";
const spiderId = `${containerId}__${corner}`;
const spider = cornerEl.querySelector(".pw-spider");
if (!spider) return;
if (deadSpiders.includes(spiderId)) {
spider.classList.add("is-dead");
return;
}
spider.addEventListener(
"click",
() => {
const skull = spider.parentElement.querySelector(".pw-skull");
spider.classList.add("is-dying");
if (skull) skull.classList.add("is-rising");
pidMarkSpiderDead(spiderId);
},
{ once: true }
);
});
}
/* Atalho de teclado Tab+B (segurar Tab e apertar B) — reseta todas as
aranhas "mortas" salvas (`pidResetDeadSpiders()`) e recarrega a página,
mesmo raciocínio do toggle de opt-out (`account.js`): o estado já foi
aplicado no carregamento atual, não dá pra desfazer in-place. Não chama
`preventDefault()` no Tab (continuaria navegando o foco normalmente,
acessibilidade intacta) — só rastreia se ele está pressionado no momento
em que o B chega. Incluído em toda página (mesmo padrão de
`confirm-modal.js`), mas só tem efeito onde há aranha salva como morta. */
document.addEventListener("DOMContentLoaded", () => {
let tabHeld = false;
document.addEventListener("keydown", (event) => {
if (event.key === "Tab") {
tabHeld = true;
return;
}
if (tabHeld && (event.key === "b" || event.key === "B") && pidGetDeadSpiders().length) {
pidResetDeadSpiders();
window.location.reload();
}
});
document.addEventListener("keyup", (event) => {
if (event.key === "Tab") tabHeld = false;
});
});

View File

@ -1,20 +1,42 @@
/*
* Logo da sidebar (`#sidebar-brand-link`, presente nos 10 shells) — link de
* verdade pra `portal.html` (tela Principal), com os olhos do ícone
* piscando antes de navegar. A navegação é adiada (`setTimeout`) só o
* suficiente pra a piscada (`.pid-icon-eye`/`pidIconBlink` em base.css,
* 200ms) terminar de tocar antes da página trocar; ctrl/cmd/shift/clique do
* meio continuam abrindo em nova aba normalmente (não intercepta esses
* casos).
* Logo da sidebar (`#sidebar-brand-link`, presente em todo shell) — link de
* verdade pra `portal.html` (tela Principal). Fora do tema sazonal
* Halloween: os olhos do ícone piscam antes de navegar
* (`.pid-icon-eye`/`pidIconBlink` em base.css, 200ms), navegação adiada só o
* suficiente pra a piscada terminar de tocar. Durante o Halloween
* (`pidIsHalloweenActive()`, seasonal-theme.js): o ícone/assinatura viram o
* vampiro (`pidApplyVampireIcon`/`pidApplyVampireFull`) e o clique dispara a
* animação de "susto" (`pidTriggerVampireScare`, `.is-scaring` em base.css)
* em vez da piscada, com a navegação adiada o suficiente pra ela ser vista.
* ctrl/cmd/shift/clique do meio continuam abrindo em nova aba normalmente
* (não intercepta esses casos).
*/
document.addEventListener("DOMContentLoaded", () => {
const brand = document.getElementById("sidebar-brand-link");
if (!brand) return;
const halloween = typeof pidIsHalloweenActive === "function" && pidIsHalloweenActive();
if (halloween) {
pidApplyVampireIcon(brand.querySelector(".sidebar__logo--icon"));
pidApplyVampireFull(brand.querySelector(".sidebar__logo--full"));
if (typeof pidApplySeasonalFavicon === "function") pidApplySeasonalFavicon();
// Container só existe em portal.html (#pw-cobwebs-portal) — no-op nos demais shells.
if (typeof pidApplyHalloweenCobwebs === "function") pidApplyHalloweenCobwebs("pw-cobwebs-portal");
}
brand.addEventListener("click", (event) => {
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
if (halloween) {
pidTriggerVampireScare(brand);
setTimeout(() => {
window.location.href = brand.getAttribute("href");
}, PID_HALLOWEEN_SCARE_MS - 100);
return;
}
brand.querySelectorAll(".pid-icon-eye").forEach((eye) => {
eye.classList.remove("is-blinking");
void eye.getBoundingClientRect();

View File

@ -308,6 +308,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -559,6 +566,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -314,6 +314,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -695,6 +702,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -308,6 +308,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -681,6 +688,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -309,6 +309,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -858,6 +865,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -309,6 +309,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -876,6 +883,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -309,6 +309,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -876,6 +883,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -8,15 +8,18 @@
<link rel="icon" type="image/svg+xml" href="{% static 'img/pid-icone-escuro.svg' %}" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bree+Serif&family=Pinyon+Script&family=Space+Grotesk:wght@700&family=DM+Mono&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Bree+Serif&family=Pinyon+Script&family=Space+Grotesk:wght@700&family=DM+Mono&family=Creepster&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="{% static 'css/tokens.css' %}" />
<link rel="stylesheet" href="{% static 'css/base.css' %}" />
<link rel="stylesheet" href="{% static 'css/login.css' %}" />
<link rel="stylesheet" href="{% static 'css/login-intro.css' %}" />
<link rel="stylesheet" href="{% static 'css/login-intro-halloween.css' %}" />
<link rel="stylesheet" href="{% static 'css/halloween-cobweb.css' %}" />
<script src="{% static 'js/theme.js' %}" defer></script>
</head>
<body>
<div class="login-page">
<div class="pw-corners pw-corners--login" id="pw-cobwebs-login" hidden aria-hidden="true"></div>
<div class="login-card">
<button type="button" class="login-card__logo-btn" id="login-logo-btn" aria-label="P.I.D.">
<svg class="login-card__logo" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
@ -117,10 +120,88 @@
</div>
</div>
<!-- Variante sazonal (Halloween/vampiro) da intro acima — ver
login-intro-halloween.css/.js e `docs/temas-sazonais/temas-sazonais.md`.
Só é exibida quando `pidIsHalloweenActive()` (seasonal-theme.js); fora
da janela de outubro, o bloco #login-intro acima continua sendo o
único usado. -->
<div class="login-intro-h" id="login-intro-halloween" hidden aria-hidden="true">
<div class="login-intro-h__flash" id="login-intro-h-flash"></div>
<div class="login-intro-h__stage" id="login-intro-h-stage">
<svg class="login-intro-h__icon" id="login-intro-h-icon" viewBox="0 0 76 80">
<defs>
<clipPath id="pidHIntroBodyClip">
<path d="M16 22 h34 l14 14 v40 a2 2 0 0 1 -2 2 H14 a2 2 0 0 1 -2 -2 V24 a2 2 0 0 1 2 -2 z"></path>
</clipPath>
</defs>
<g class="login-intro-h__cape">
<path d="M4 9 L20 19 Q38 24 56 19 L72 9 C74 32 74 56 71 78 L5 78 C2 56 2 32 4 9 Z"></path>
<path class="login-intro-h__cape-line" d="M4 9 L20 19 Q38 24 56 19 L72 9"></path>
</g>
<path class="login-intro-h__collar" d="M9 16 L21 22 Q38 26 55 22 L67 16 C68 26 68 31 66 36 L10 36 C8 31 8 26 9 16 Z"></path>
<path class="login-intro-h__body" d="M16 22 h34 l14 14 v40 a2 2 0 0 1 -2 2 H14 a2 2 0 0 1 -2 -2 V24 a2 2 0 0 1 2 -2 z"></path>
<rect class="login-intro-h__tab" x="28" y="22" width="20" height="16"></rect>
<rect class="login-intro-h__tab-shadow" x="40" y="25" width="5" height="10"></rect>
<rect class="login-intro-h__screen" x="18" y="46" width="40" height="28" rx="2"></rect>
<g class="login-intro-h__face login-intro-h__face--scare">
<circle cx="30" cy="57.4" r="5"></circle>
<circle cx="46" cy="57.4" r="5"></circle>
<circle class="login-intro-h__pupil-red" cx="30" cy="57.4" r="2.1"></circle>
<circle class="login-intro-h__pupil-red" cx="46" cy="57.4" r="2.1"></circle>
<path class="login-intro-h__brow-angry" d="M24.6 51.4 L34.6 54.2"></path>
<path class="login-intro-h__brow-angry" d="M51.4 51.4 L41.4 54.2"></path>
<path class="login-intro-h__mouth-fill" d="M29 66.4 q9 7.4 18 0 q-9 3 -18 0 Z"></path>
<path class="login-intro-h__mouth" d="M29 66.4 q9 7.4 18 0"></path>
<path class="login-intro-h__fang" d="M31.4 67.4 L34.6 67.9 L32.9 73 Z"></path>
<path class="login-intro-h__fang" d="M41.4 67.9 L44.6 67.4 L43.1 73 Z"></path>
</g>
<g class="login-intro-h__face login-intro-h__face--friendly">
<circle cx="30" cy="58" r="5"></circle>
<circle cx="46" cy="58" r="5"></circle>
<circle class="login-intro-h__pupil" cx="32" cy="56.2" r="1.6"></circle>
<circle class="login-intro-h__pupil" cx="48" cy="56.2" r="1.6"></circle>
<path class="login-intro-h__brow" d="M26 63.2 q4 2.6 8 -0.5"></path>
<path class="login-intro-h__brow" d="M42 63.2 q4 2.6 8 -0.5"></path>
<path class="login-intro-h__mouth-friendly" d="M31 67 q7 6 14 0"></path>
<path class="login-intro-h__fang-friendly" d="M33.6 68.6 L36.4 68.9 L35 72 Z"></path>
<path class="login-intro-h__fang-friendly" d="M40.2 68.9 L43 68.6 L41.6 72 Z"></path>
</g>
<g class="login-intro-h__flap-wrap" clip-path="url(#pidHIntroBodyClip)">
<g class="login-intro-h__flap-rotate" id="login-intro-h-flap-rotate">
<path d="M66 58 C50 52 26 52 13 58 C10 64 10 72 11 78 C30 80 52 80 67 76 C68 68 68 63 66 58 Z"></path>
<path class="login-intro-h__cape-line" d="M66 58 C50 52 26 52 13 58"></path>
</g>
</g>
<g class="login-intro-h__squint">
<path d="M25.5 53.4 q4.5 -3.4 9 0"></path>
<path d="M41.5 53.4 q4.5 -3.4 9 0"></path>
<circle cx="30" cy="52.4" r="1.1"></circle>
<circle cx="46" cy="52.4" r="1.1"></circle>
</g>
</svg>
<div class="login-intro-h__wordmark" id="login-intro-h-wordmark">
<div class="login-intro-h__letters">
<span class="login-intro-h__letter">P<span class="login-intro-h__dot">.</span></span>
<span class="login-intro-h__letter">I<span class="login-intro-h__dot">.</span></span>
<span class="login-intro-h__letter">D<span class="login-intro-h__dot">.</span></span>
</div>
<div class="login-intro-h__rule"></div>
<div class="login-intro-h__tagline">Portal Interno da De Paula</div>
</div>
</div>
</div>
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/login-logo-blink.js' %}"></script>
<script src="{% static 'js/login-intro.js' %}"></script>
<script src="{% static 'js/login-intro-halloween.js' %}"></script>
<script src="{% static 'js/auth.js' %}"></script>
</body>
</html>

View File

@ -309,6 +309,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -935,6 +942,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -314,6 +314,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -545,6 +552,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -309,6 +309,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -749,6 +756,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -308,6 +308,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -610,6 +617,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -12,6 +12,7 @@
<link rel="stylesheet" href="{% static 'css/components.css' %}" />
<link rel="stylesheet" href="{% static 'css/widgets.css' %}" />
<link rel="stylesheet" href="{% static 'css/ramais-lookup.css' %}" />
<link rel="stylesheet" href="{% static 'css/halloween-cobweb.css' %}" />
<script src="{% static 'js/theme.js' %}" defer></script>
<script src="{% static 'js/portal-reveal.js' %}" defer></script>
</head>
@ -264,6 +265,7 @@
</aside>
<div class="main-content">
<div class="pw-corners pw-corners--portal" id="pw-cobwebs-portal" hidden aria-hidden="true"></div>
<header class="topbar">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Alternar menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" stroke-linecap="round"/></svg>
@ -313,6 +315,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -538,6 +547,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -309,6 +309,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -760,6 +767,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>

View File

@ -308,6 +308,13 @@
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
@ -661,6 +668,7 @@
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>