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

23 KiB
Raw Blame History

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 do Relatório Contábil (via _logo_relatorio_data_uri() em views.py): o original, com 6250 px, pesava mais que o PDF impresso inteiro. Se logo-branco.png mudar, regerar esta a partir dele. Foi criada para o relatório da Conciliação de Fornecedores, que desde 2026-10-02 (rodada 72 em portal_api/conciliacao_fornecedores/CHANGELOG.md) é uma aba do Portal, sem logo do escritório, por ser de uso interno do contador.
  • 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.