portal_publico/docs/identidade-visual/identidade-visual.md

64 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Identidade visual do Portal
> Detalhamento técnico da identidade visual transversal: os arquivos de logo, a marca "P.I.D." na sidebar/login/favicon, as animações genéricas e a animação de intro pós-login. Movido do `CLAUDE.md` da raiz em 2026-09-21, pelo mesmo motivo que levou a documentação das aplicações a sair de lá: era detalhe de uma área específica ocupando ~21 KB de um arquivo carregado em toda sessão.
>
> Este arquivo **não é carregado automaticamente** pelo Claude Code (não há pacote Python correspondente) — ler manualmente ao mexer em logo, favicon, `sidebar__brand`, animação de entrada ou na intro do login. A variação sazonal dessa mesma marca (mascote de Halloween, intro alternativa, teias) fica em `docs/temas-sazonais/temas-sazonais.md`.
## Logos em `static/img/`
Duas identidades visuais coexistem **de propósito** hoje: o logo cursivo "D De Paula Contadores" (usado só nos documentos/PDFs que a aplicação gera, ver abaixo) e a marca nova "P.I.D." (`.svg`, ver `pid-marca-leiame.md` em `C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`), adotada no favicon, no login e na sidebar do Portal. **Essa separação é deliberada, não uma migração incompleta**: um documento gerado pela aplicação (contrato, procuração, recibo do Indicador de Desempenho, PDF da Simulação de Custo de Contratação) é emitido como se o próprio escritório o tivesse gerado — carrega a identidade do escritório perante o cliente, não a identidade do Portal como ferramenta interna. A marca "P.I.D." é só pra UI do Portal em si. Não migrar o logo de um gerador de documento pra "P.I.D." (nem vice-versa numa tela do Portal) sem confirmar de novo com o usuário.
**Logo cursivo "D De Paula Contadores"** (D em degradê dourado/marrom + texto, PNG com fundo transparente) — não aparece em nenhum template HTML hoje, só nos PDFs gerados pela aplicação:
- `logo.png` — original, texto **preto**. Serve como fonte pra gerar as outras variantes e é usada diretamente no recibo do Indicador de Desempenho (`indicadores/recibo.py`, `LOGO_PATH`, redimensionada/recomprimida em memória pra impressão — ver `portal_api/indicadores/CLAUDE.md`).
- `logo-branco.png` — usada no cabeçalho do PDF de Simulação de Custo de Contratação (`custo_contratacao/pdf.py`, banner marrom escuro, ver `portal_api/custo_contratacao/CLAUDE.md`) — até uma rodada anterior também era usada no `sidebar__brand` dos 13 shells, migrada pra marca "P.I.D." (ver abaixo; a UI do Portal e os documentos gerados usam fontes de logo independentes agora). Mesmo D colorido de `logo.png`, mas com o texto recolorido pra branco; gerada programaticamente a partir de `logo.png` (script Python com Pillow: qualquer pixel opaco quase-neutro/escuro — `max(r,g,b) < 70` e `spread(r,g,b) < 12` — virou branco; o D nunca entra nesse filtro porque mesmo na sombra mais escura do degradê ele mantém um matiz quente nitidamente não-neutro). Se o logo oficial mudar, regerar `logo-branco.png` a partir do novo `logo.png` com o mesmo filtro, não editar à mão.
- `logo-branco-relatorio.png` — cópia de `logo-branco.png` só redimensionada (480 px de largura, 40 KB, Pillow LANCZOS), usada no cabeçalho do relatório da Conciliação de Fornecedores (`conciliacao-fornecedores-relatorio.html`): o original, com 6250 px, pesava mais que o PDF impresso inteiro. Se `logo-branco.png` mudar, regerar esta a partir dele.
- `logo-mono.png` — versão totalmente monocromática (D **e** texto em branco/cinza claro). Não usada em nenhum consumidor hoje; existe como variante alternativa (útil se algum dia precisar de um logo "chapado" sem o dourado do D).
**Marca nova "P.I.D."**:
- `pid-icone.svg` — ícone quadrado, variante pra **fundo claro** (corpo roxo escuro `#4B2E75`); chegou a ser usada no login quando `data-theme="light"`, mas o usuário pediu pra usar sempre a mesma logo nos dois temas — **não tem mais nenhum consumidor** hoje, é o único arquivo sem uso que ficou no repositório.
- `pid-icone-escuro.svg` — ícone quadrado, variante pra **fundo escuro** (corpo roxo mais claro `#7B5BA8`, pra manter contraste); usada (a) no favicon (`<link rel="icon" type="image/svg+xml">` no `<head>` das 14 páginas, tudo menos o template de relatório do Relatório Contábil) e (b) no login, **sempre**, nos dois temas — o card do login já é congelado escuro nos dois temas (ver "Card de login" abaixo), e agora o ícone também, pedido explícito do usuário ("deixe no tema claro a mesma logo usada no tema escuro"; antes alternava com `pid-icone.svg` conforme `data-theme`, mecanismo removido — ver "Ícone do login é clicável" abaixo). O ícone da sidebar (expandida e colapsada, 13 shells) e o do card de login usam o **mesmo desenho/cores** desse arquivo, mas como markup `<svg>` inline copiado direto no HTML, não uma referência a este arquivo — ver "Ícone do login/sidebar são clicáveis" abaixo pro motivo (precisa expor os olhos pro CSS/JS animar o piscar ao clicar).
- `pid-logo-horizontal-escuro.svg` — assinatura horizontal (ícone + "P.I.D." + "PORTAL INTERNO DA DE PAULA" em texto, viewBox `300×80`, texto claro `#F2EDE3`/dourado `#C6A24A` — variante pra fundo escuro). Mesmo caso do ícone acima: a sidebar expandida (13 shells) usa o mesmo desenho como `<svg>` inline, não uma referência a este arquivo.
- `pid-halloween-icone-escuro.svg`, `pid-halloween-logo-horizontal-escuro.svg` e `pid-halloween-favicon.svg` — variantes sazonais do mascote vampiro, trocadas em runtime durante a janela do Halloween. Ver `docs/temas-sazonais/temas-sazonais.md`.
> `pid-favicon.svg` e `pid-logo-horizontal.svg` existem na pasta de marca de origem (`C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`), mas **nunca foram copiados pra `static/img/`** — a documentação anterior os descrevia como se estivessem aqui. Se algum dia forem necessários, copiar de lá.
**`sidebar__brand` com duas marcas sobrepostas (crossfade), não uma redimensionada** (`layout.css`): a sidebar expandida mostra a assinatura com texto (tem texto, ilegível se só encolhida) e a colapsada mostra só o símbolo — são dois `<svg class="sidebar__logo sidebar__logo--full">`/`<svg class="sidebar__logo sidebar__logo--icon">` **sempre presentes no DOM**, sobrepostos via `position:absolute` dentro de `.sidebar__brand` (`position:relative; height:92px`, alto o bastante pra caber as duas sem depender da altura natural de nenhuma, já que filhos absolutos não contribuem pra altura do pai). `.sidebar__logo--full` é `width:250px; max-width:96%` (quase toda a largura útil da sidebar, `264px` menos o padding horizontal de `.sidebar`) — aumentado em duas rodadas a partir do tamanho inicial (`176px`/`70%` → `220px`/`92%` → `250px`/`96%`) porque o subtítulo "PORTAL INTERNO DA DE PAULA" (fonte pequena dentro do SVG, viewBox `300×80`) ficava ilegível menor; `height:92px` acompanhou cada aumento pra sobrar espaço vertical (proporção do SVG é `300:80`, então a altura renderizada escala junto com a largura). A transição entre elas é `opacity`+`transform:scale()` (`var(--transition-base)`, mesma duração das outras animações do sidebar) — pedido explícito do usuário pra não ser uma troca brusca; `.app-shell.is-collapsed` (toggle desktop) e o breakpoint mobile (`@media (max-width:1024px)`, onde o colapsado é o estado *default* e `.is-expanded-mobile` o inverte, mesmo padrão já usado pelos demais elementos do menu nesse breakpoint) alternam qual das duas fica com `opacity:1`.
**Ícone do login e da sidebar são clicáveis, com os olhos piscando** (pedido explícito do usuário, em duas rodadas — primeiro a sidebar, depois o ícone do login): tanto `.sidebar__brand` (13 shells) quanto `#login-logo-btn` (`index.html`) tiveram o ícone convertido de `<img src="...svg">` pra `<svg>` **inline**, com markup idêntico ao de `pid-icone-escuro.svg` copiado direto no HTML — um `<img>` não expõe seu conteúdo interno pro CSS/JS da página (é uma imagem opaca), então não dava pra animar só os olhos sem inlinear. Dentro de cada SVG, os dois olhos (círculo creme + glint escuro) ficam num `<g class="pid-icon-eye">` próprio, sem nenhum `transform` no XML (a posição já vem dos `cx`/`cy` dos círculos) — importante porque um `transform` de CSS aplicado num elemento que já tem um `transform` de atributo **substitui** o atributo inteiro (perderia a posição); mantendo os dois olhos "limpos" desse jeito, a única transformação deles é a que a animação de piscar aplica. `.pid-icon-eye`/`.is-blinking`/`@keyframes pidIconBlink` moram em `base.css` (`transform-box:fill-box; transform-origin:center` faz o `scaleY()` girar em torno do próprio olho, não da origem do SVG; `scaleY(1)→0.05→1`, 200ms, os dois olhos piscam juntos) — em `base.css`, não em `layout.css`, porque é carregado por **todas** as páginas, inclusive `index.html` (que não carrega `layout.css`).
- **Sidebar** (`static/js/sidebar-brand.js`, incluído logo depois de `api.js` nos 13 shells): `.sidebar__brand` deixou de ser `<div>` e virou `<a href="portal.html" id="sidebar-brand-link" aria-label="Ir para a tela Principal">` — a mudança de tag não afeta o CSS existente (todo seletor é por classe), então o crossfade descrito acima continua igual. O script escuta o clique: ignora cliques modificados (`ctrl`/`cmd`/`shift`/botão do meio — deixa abrir em nova aba normalmente), senão faz `preventDefault()`, adiciona `.is-blinking` em todo `.pid-icon-eye` dentro do link (pega os olhos das duas marcas — a visível e a escondida pelo crossfade, inofensivo já que a escondida tem `opacity:0`) e só navega pra `portal.html` depois de 260ms, tempo suficiente pra piscada terminar de tocar antes da página trocar — mesmo espírito de "deixar a transição ser vista antes de navegar" já usado na animação de intro do login.
- **Login** (`static/js/login-logo-blink.js`, novo): `#login-logo-btn` é um `<button type="button">` (não um link — não há pra onde navegar a partir do próprio login), sem `preventDefault`/delay nenhum, só dispara a piscada ao clicar; é puramente decorativo, sem efeito colateral. Também aqui foi a oportunidade de simplificar um mecanismo que existia só por causa do `<img>`: a logo do login **alternava** entre `pid-icone.svg`(claro)/`pid-icone-escuro.svg`(escuro) conforme `data-theme`, via `pidSyncLoginLogo()` em `theme.js` — removida junto com essa mudança, a pedido do usuário ("deixe no tema claro a mesma logo usada no tema escuro"), já que o card do login já era congelado escuro nos dois temas mesmo antes disso (ver "Card de login" abaixo) — a lógica de alternância nunca fazia muito sentido nesse contexto. Agora `#login-logo-btn` sempre usa as cores/desenho de `pid-icone-escuro.svg`, sem nenhuma checagem de tema.
## Animações genéricas
Três `@keyframes` genéricos em `base.css` (`pidFadeIn`, `pidFadeSlideUp`, `pidScaleIn`) — reutilizados via `animation` (não `transition`) em elementos que entram/saem do layout via `hidden`/`display:none` (modal, dropdown, `.page-content` a cada navegação, `.login-card`), porque só `animation` reinicia sozinho quando um elemento passa de `display:none` para visível; `transition` não anima essa troca (não há frame intermediário). Duração sempre curta (120–200ms) — pedido explícito do usuário: "fluidas, porém rápidas, otimizando o tempo". Botões (`.btn-solid`/`.btn-outline`/`.btn-ghost`/`.btn-danger-outline`/`.icon-btn`) ganharam `transform: scale()` no `:active` como feedback de clique.
**Nenhuma dessas animações respeita `prefers-reduced-motion`** — decisão deliberada do usuário ("as animações devem ignorar a preferência de não mostrar animações ou de acessibilidade do computador do usuário"), não um descuido. Não adicionar um bloco `@media (prefers-reduced-motion: reduce)` desativando isso sem confirmar de novo com o usuário, já que contraria um pedido explícito.
### Animação de intro do login
Ao apertar "Entrar" com sucesso, `index.html` toca uma animação de marca em tela cheia (~6,2s: 500ms de fade + ~5,7s de animação) antes de navegar pra `portal.html` — pedido explícito do usuário, inspirado num arquivo `pid-intro-escuro.html` que ele forneceu (mesma pasta de origem dos SVGs da marca "P.I.D.", `C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`).
**Origem do arquivo — por que não foi só copiado**: `pid-intro-escuro.html` não é HTML/CSS simples, é um bundle auto-contido de um editor de "Design Canvas" (Anthropic) — um `<script type="__bundler/manifest">` com um JSON mapeando uuid → recurso (`{mime, compressed, data}`, `data` sendo base64 de um gzip), incluindo o JSX fonte de verdade (`pid-intro.jsx`, a composição em si) e a biblioteca de motion (`animations-v3.jsx`, easing/interpolação), montados em tempo real por um runtime React + motor de composição por "tempo autorado" (`useComposition`/`CompositionStage`) que não faz sentido carregar em produção (pesado, e pensado pra edição/export de vídeo, não pra rodar dentro de uma página de login). A coreografia foi **extraída** (decodificado gzip+base64 por uuid, script Python ad-hoc) e **portada fielmente** pra vanilla JS/CSS — mesmas durações de cena, mesmas curvas de easing (hand-rolled, estilo Popmotion) e mesmo timing de piscada dos olhos do original; só o destino final do "voo" do ícone foi recalibrado (ver "Cena Portal" abaixo).
**As 4 cenas** (`PID_INTRO_CUES`/`PID_INTRO_TOTAL` em `login-intro.js` — `Build:0, Face:1, Wordmark:2, Portal:4.8`, total `5.7`): durações **aceleradas** em relação ao arquivo original (`Build`/`Face` de 2,6s/2s pra 1s cada, `Portal` de 2,3s pra ~0,9s — pedido explícito do usuário, "acelere um pouco a velocidade na qual a logo é construída"), exceto `Wordmark` (2,8s, igual ao original) — "a parte onde aparece o nome do portal deve permanecer a mesma", pedido explícito também. Os deslocamentos internos de cada cena (quando cada elemento começa/termina de animar) foram reproporcionados pra caber na duração nova, não são mais os valores originais do arquivo — só a cena Wordmark manteve os originais, já que a duração dela não mudou.
- **Build** (1s): o contorno do corpo do ícone se desenha (`stroke-dashoffset`, `pathLength="100"` normaliza o path pra unidades 0–100 independente da geometria real), preenche a cor, a aba dourada "cai" (`easeOutBack`, dá um leve overshoot) e a "tela"/rosto do ícone abre (scale+opacity).
- **Face** (1s): os dois olhos aparecem com "pop" (`easeOutBack`) e piscam uma vez (`pidIntroBlinkAt()` — uma janela de 220ms em que a escala vertical do olho vai de 1 a 0 e volta a 1, formando o fecha-e-abre; a mesma função é reaproveitada em 2 momentos diferentes agora, ver abaixo — o terceiro blink do original, na cena Portal, foi removido: a cena ficou curta demais pra caber um blink visível com o ícone já encolhendo/voando), o sorriso dourado se desenha (mesma técnica de `stroke-dashoffset` do corpo).
- **Wordmark** (2,8s, **inalterada**): o ícone desliza pra esquerda (`SHIFT_X=-100px`) enquanto "P.I.D." (fonte "Space Grotesk" 700, cada letra com um "." dourado à parte) + uma régua dourada (`scaleX`) + a tagline "Portal Interno da De Paula" (fonte "DM Mono", uppercase, letter-spacing largo) aparecem — cada letra com seu próprio atraso escalonado (`CUES.Wordmark + 0.25 + i*0.13`).
- **Portal** (~0,9s): a wordmark esmaece quase na hora (`Portal` a `Portal+0.3`), o ícone encolhe (de 160px pra 40px — o mesmo tamanho de `.sidebar__logo--icon`, ver "Logos em `static/img/`" acima) e "voa" até o canto superior esquerdo em 0,5s (`Portal+0.05` a `Portal+0.55`), e o stage inteiro (ícone+wordmark) esmaece logo depois, sobrepondo o fim do voo (`Portal+0.5` a `Portal+0.9`) — sem pausa parada entre o ícone assentar e o fade começar (ajuste de uma rodada anterior, que já tinha comprimido essa cena de 2,3s pra ~0,95s; esta rodada só encurtou mais um pouco, até ~0,9s). **Diferença deliberada em relação ao original** (além do tempo): lá o destino é um pixel fixo dentro de um frame de vídeo de exportação 1920×1080 (`logoX:-892, logoY:-496`, coordenadas que não existem em página nenhuma); aqui, `pidIntroCornerTarget()` calcula o alvo em tempo real a partir de `window.innerWidth/innerHeight`, mirando um ponto perto do canto real da janela — a ideia de "o ícone termina indo pro cabeçalho/sidebar do app" só faz sentido revisitada assim, já que o original nunca foi pensado pra rodar dentro de uma página de verdade.
- As duas piscadas do olho (`Face+0.8`, `Wordmark+1.1`) e as três funções de easing usadas (`easeOutCubic`/`easeInOutQuad`/`easeOutBack`) seguem as fórmulas exatas extraídas de `animations-v3.jsx` — ver o código de `login-intro.js` se precisar ajustar timing, não redesenhar do zero.
**Fade de entrada, antes do ícone começar a se desenhar** (pedido explícito do usuário — a primeira versão fazia o overlay aparecer de repente por cima do card ainda visível, "de um modo bruto"; a duração foi ajustada de 350ms pra 500ms numa rodada seguinte, "está muito rápido... só pra ficar mais fluído"): `pidPlayLoginIntro()` aplica `.is-leaving` no `.login-card` (`login.css`, `opacity:0; transform:scale(0.98)`, transição de 500ms) e `.is-visible` no `#login-intro` (`opacity:0→1`, mesmos 500ms, `PID_INTRO_FADE_MS`) ao mesmo tempo — o card se dissolve enquanto o overlay (já na cor final sólida) sobe por cima, um cross-fade real, não uma troca instantânea. Só depois desses 500ms (`setTimeout`) é que o loop de `requestAnimationFrame` do ícone começa (`start = performance.now()` é atribuído só nesse momento, não antes).
**Arquivos**: `static/css/login-intro.css` (só `index.html`) — formas idênticas às já usadas em `pid-icone-escuro.svg`/`pid-logo-horizontal-escuro.svg` (hex hardcoded — `#7b5ba8`/`#c6a24a`/`#f2ede3`, não são tokens de tema, são a paleta fixa da marca, mesmo espírito de `.login-slogan` já hardcodar `#c6a24a`); a **exceção** é o fundo do overlay, que reage ao tema (ver "Tema claro" logo abaixo) em vez de ser um hex fixo da marca. `static/js/login-intro.js` expõe `pidPlayLoginIntro(onDone)` (global, chamada só por `auth.js`) — monta o loop de `requestAnimationFrame`, calcula cada valor (`bodyDraw`, `eyeL`, `shift`, `fly`, ...) a partir de `T` (segundos decorridos desde o início do loop, via `performance.now()`) e escreve direto nos atributos/estilo dos elementos do overlay (`#login-intro`, já presente e `hidden` no HTML de `index.html`); chama `onDone()` quando `T` atinge `PID_INTRO_TOTAL`. Fontes "Space Grotesk" (peso 700) e "DM Mono" adicionadas ao mesmo `<link>` do Google Fonts de `index.html`, junto de "Bree Serif"/"Pinyon Script" já usadas ali.
**Tema claro**: o fundo do overlay (`#121017`, o valor fixo de `--bg-canvas` no tema escuro) e o texto do wordmark (`.login-intro__letter`/`.login-intro__tagline`, cor clara — pensados pra contrastar com um fundo escuro) só faziam sentido enquanto o overlay era sempre escuro (decisão original, pra login → animação → portal ler como uma coisa só). O usuário pediu que, no tema claro, o fundo da animação também acompanhasse o `--bg-canvas` claro (mesmo raciocínio já aplicado a `.login-page` em `login.css`) — o que por sua vez tornou o texto claro do wordmark ilegível contra um fundo claro. `:root[data-theme="light"]` overrides em `login-intro.css` cobrem os dois: `.login-intro` vira `background: var(--bg-canvas)` e `.login-intro__letter`/`.login-intro__tagline` viram cores escuras (`#241c33`/`rgba(36, 28, 51, 0.62)`, os mesmos tons só invertidos). O **ícone não precisou de override** — o corpo roxo (`#7b5ba8`) tem contraste de sobra contra um fundo claro, e a "tela" do disquete (`#241c33`) já é escura por si só, então os olhos/sorriso continuam legíveis nos dois temas sem mudar nada. Nada disso afeta o tema escuro (padrão), que continua com os valores fixos originais.
**Fluxo de navegação**: `auth.js`, no sucesso do `POST /api/auth/login/`, chama `pidPlayLoginIntro(() => { sessionStorage.setItem("pid_reveal_portal", "1"); window.location.href = "portal.html"; })` em vez de navegar direto — a navegação só acontece depois da animação inteira (não há como "pular" a animação hoje, nem foi pedido).
**"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.