# Identidade visual — a paleta e a tipografia do produto

## Conteúdo

- 1. A virada: marca da empresa vs. identidade do produto
- 2. A fonte de verdade, e a armadilha da branch
- 3. A paleta (11 séries)
- 4. Contraste medido contra o card branco
- 5. Os três azuis, e o que isso implica
- 6. Deuteranopia
- 7. As regras de uso que saem da medição
- 8. Tipografia: Poppins
- 9. O chrome do gráfico: eixo, grid, legenda, data label
- 10. Como o tema é aplicado
- 11. Onde a marca da Horus continua valendo

Este arquivo é o dono da cor e da tipografia de qualquer render desta skill.
Leia ANTES de escolher qualquer cor. [`horus-theme.json`](horus-theme.json) é o
asset que o pipeline injeta em cada spec Vega-Lite; este documento é a origem e
a medição de cada valor daquele arquivo.

## 1. A virada: marca da empresa vs. identidade do produto

Existe uma tentação forte aqui, e ela é errada: derivar uma paleta a partir do
manual de marca da Horus-**empresa** (laranja `#F39339`, roxo `#1C0A3B`, fundo
bege `#EFEDE6`, Montserrat), escurecendo as cores até baterem contraste. Isso
produz um trabalho tecnicamente correto no alvo errado.

O mockup existe para mostrar ao cliente **o dashboard que ele vai receber**, e o
que ele vai receber é o Horus BI-**produto**: card branco, Poppins, e uma paleta
de 11 cores que já está no código. Paleta de marca no mockup promete uma peça
que a plataforma não entrega.

Por isso a paleta aqui não é derivada nem otimizada: ela é **lida do código e
usada como está**. O trabalho deste arquivo é medir essa paleta com honestidade
e derivar regras de uso a partir do que a medição mostra.

## 2. A fonte de verdade, e a armadilha da branch

`bi-monorepo-front`, branch **`main`**, arquivo
`apps/horus.dataviz.frontend/src/components/Widgets/Configs.ts`.

**A armadilha é real e já pegou:** uma branch de trabalho pode reescrever
`Configs.ts` com outra paleta e adicionar um bloco `fontFamily` ao
`tailwind.config.cjs` que não existe em `main`. Ler o working tree em vez da
`main` produz uma paleta e uma fonte plausíveis e erradas.

Antes de confiar em qualquer hex daqui:

```bash
cd ~/git/bi-monorepo-front
git show main:apps/horus.dataviz.frontend/src/components/Widgets/Configs.ts
```

Leia via `git show main:<caminho>`, nunca o working tree.

**Os prints de dashboard também não servem como fonte de cor.** Os screenshots
internos são de 2021 (coral `#E84838`, azul `#78B0E8`, verde `#20B868`) e mostram
um tema já substituído. Eles valem para layout e arquétipo, o que é assunto de
[`chassi-horus-bi.md`](chassi-horus-bi.md).

## 3. A paleta (11 séries)

`defaultColors`, na ordem exata em que o Highcharts atribui às séries:

| # | Hex | Nome | Consumida por |
|---|---|---|---|
| S1 | `#6567ea` | indigo | `Barline`, `Pie`, `Gauge`, `Forecast` |
| S2 | `#30b1ec` | sky | idem |
| S3 | `#10b981` | emerald | idem |
| S4 | `#ff9800` | orange | idem |
| S5 | `#f000b9` | magenta | idem |
| S6 | `#2ab7f9` | light blue | idem |
| S7 | `#64e572` | light green | idem |
| S8 | `#EF5350` | red | idem |
| S9 | `#29B6F6` | light blue 2 | idem |
| S10 | `#FF5722` | deep orange | idem |
| S11 | `#AB47BC` | purple | idem |

Não existe override por tenant nem por app: `defaultColors` é a única paleta do
sistema. O usuário pode escolher cor manual por série no configurador, mas o
default que o cliente vê ao criar um widget é esta lista, nesta ordem.

`#ff9800` e `#f000b9` também são os tokens `warning` e `secondary` do
`tailwind.config.cjs`. As quatro últimas vêm da paleta Material.

## 4. Contraste medido contra o card branco

Fundo de referência `#FFFFFF` (o card de `Widget.svelte`). Piso WCAG 1.4.11 para
elemento gráfico não-textual: 3:1. Fórmula oficial de luminância relativa
(`L = 0.2126R + 0.7152G + 0.0722B` em espaço linear).

| Série | vs branco | Veredito |
|---|---|---|
| S1 indigo | **4.49:1** | passa |
| S2 sky | 2.43:1 | falha |
| S3 emerald | 2.54:1 | falha |
| S4 orange | 2.16:1 | falha |
| S5 magenta | **3.88:1** | passa |
| S6 light blue | 2.27:1 | falha |
| S7 light green | 1.62:1 | falha |
| S8 red | **3.49:1** | passa |
| S9 light blue 2 | 2.30:1 | falha |
| S10 deep orange | **3.16:1** | passa |
| S11 purple | **4.82:1** | passa |

**Seis das onze falham.** A pior é S7 (`#64e572`) a 1.62:1, um verde claro que
quase some contra branco.

O chrome também foi medido:

| Token | vs branco | Piso aplicável | Veredito |
|---|---|---|---|
| rótulo de eixo `#949DAC` | 2.74:1 | 4.5:1 (texto) | falha |
| gridline `#efefef` | 1.15:1 | nenhum (decorativo) | n/a |

**Nada disso vira motivo para corrigir a paleta nesta skill.** O objetivo
declarado é fidelidade. Um mockup com cores "consertadas" volta a prometer uma
peça que o cliente não recebe, que é exatamente o defeito que esta reescrita
existe para resolver. A medição serve para gerar regras de uso (seção 7) e para
ser levada ao time de produto por fora daqui.

## 5. Os três azuis, e o que isso implica

Medindo todos os 55 pares, três deles são a mesma cor na prática:

| Par | Δmatiz | Contraste |
|---|---|---|
| S2 `#30b1ec` × S9 `#29B6F6` | 0.1° | 1.06:1 |
| S2 `#30b1ec` × S6 `#2ab7f9` | 0.3° | 1.07:1 |
| S6 `#2ab7f9` × S9 `#29B6F6` | 0.4° | 1.01:1 |

Mais um par quente quase colado: S8 `#EF5350` × S10 `#FF5722`, 13.3° e 1.10:1.

Consequência direta: **um gráfico com 9 séries tem três séries indistinguíveis**,
porque as posições 2, 6 e 9 recebem o mesmo azul. Isso acontece no produto hoje,
independentemente do mockup. É o achado mais concreto desta medição.

## 6. Deuteranopia

Simulação pela matriz de aproximação Brettel–Viénot–Mollon aplicada sobre sRGB
gamma-codificado, a mesma linhagem usada por ferramentas web de simulação. É uma
aproximação declarada, não um laudo clínico.

| Par | Normal | Simulado |
|---|---|---|
| S2 × S6 | 1.07:1 | 1.00:1 |
| S2 × S9 | 1.06:1 | 1.01:1 |
| S6 × S9 | 1.01:1 | 1.02:1 |
| S7 × S11 | 2.98:1 | 1.02:1 |
| **S1 × S2** | 1.84:1 | **1.10:1** |
| S1 × S9 | 1.95:1 | 1.11:1 |
| S8 × S10 | 1.10:1 | 1.13:1 |

O item que importa é o quinto: **S1 × S2 colapsa**. Indigo e sky são as duas
primeiras posições da paleta, ou seja, as duas primeiras séries de qualquer
gráfico. Sob deuteranopia elas ficam a 1.10:1 (`#6666C3` vs `#6057DA`).

Isso desmonta a suposição intuitiva de que "poucas séries, sem problema". O
problema começa com **duas**.

Pares que sobrevivem bem, para quando houver liberdade de escolha:

| Par | Normal | Simulado |
|---|---|---|
| S3 × S4 | 1.18:1 | **5.73:1** |
| S2 × S4 | 1.13:1 | **3.78:1** |
| S1 × S4 | 2.08:1 | **3.44:1** |

S4 (`#ff9800`) é a cor que mais se separa das outras sob deuteranopia, porque
vira um amarelo-oliva claro (`#D8E02E`) enquanto o resto escurece.

## 7. As regras de uso que saem da medição

1. **Use `range.category` na ordem do produto, sempre.** Fidelidade vem antes de
   otimização. Não reordene para "melhorar" o contraste; o cliente vai receber a
   ordem do produto.
2. **Máximo de 5 séries simultâneas num widget de mockup.** A partir da sexta a
   paleta começa a repetir matiz (S6 e S9 são o azul de S2). Se a ficha pede
   mais, ela precisa mudar de arquétipo: small multiples, ou a dimensão vira
   filtro. Isso conversa com o limite de 4 categorias do donut em
   [`ficha-para-vega.md`](ficha-para-vega.md).
3. **A partir de 2 séries, some um discriminador não cromático.** A seção 6 prova
   que cor sozinha não separa nem as duas primeiras. O produto já resolve isso em
   linha: o Highcharts atribui um símbolo diferente por série (círculo, losango,
   quadrado, triângulo), visível nos prints de série múltipla. O tema desta skill
   replica com `"line": {"point": true}` mais `shape` por série no encoding.
4. **Em barra com 2+ séries, use rótulo direto ou legenda adjacente.** Barra não
   tem símbolo para carregar a distinção, então a legenda no topo (padrão do
   produto) é o único apoio, e ela depende de cor. Rótulo direto na série resolve
   sem depender de cor nenhuma.
5. **Nunca use cor sozinha para codificar sinal semântico** (bom/ruim,
   acima/abaixo da meta). Some sentido de seta, posição ou rótulo.

## 8. Tipografia: Poppins

O produto usa **Poppins**, servida de arquivo local
(`apps/horus.dataviz.frontend/src/css/poppins.css`), aplicada explicitamente em
todo estilo de chart:

```js
fontFamily: '"Poppins", sans-serif'   // Barline.svelte:148, 246, 494, 508
```

Montserrat é a fonte do manual de marca da empresa e **não** é a fonte do
produto. Confundir as duas é o mesmo erro da paleta (§1).

Poppins é licenciada sob SIL Open Font License, então embeda em HTML, SVG e PDF
gerado sem custo nem atribuição obrigatória no artefato. `config.font` no tema
define Poppins como fonte única de eixo, legenda e texto.

Pesos observados no produto: 400 para eixo e legenda (o Highcharts declara
`fontWeight: "normal"` explicitamente), 500 para título de widget.

## 9. O chrome do gráfico: eixo, grid, legenda, data label

Todos lidos de `Barline.svelte` em `main`.

| Elemento | Valor | Linha |
|---|---|---|
| rótulo de eixo | `#949DAC` (dark `#aeb8c8`) | 246 |
| título de eixo | `#949DAC` | 250 |
| gridline | `#efefef` (dark `#192132`) | 255 |
| linha do eixo X | `lineWidth: 0` | 524 |
| legenda | `verticalAlign` default **`"top"`**, texto preto, peso normal | 491 |
| data label | preto sobre `#f6f6f6aa`, `borderRadius: 5`, `padding: 2`, `textOutline: "none"` | 145 |
| tooltip | fundo transparente, `borderWidth: 0`, `padding: 0`, `useHTML` | 480 |

Dois detalhes que quem reproduz de memória erra:

- **A legenda fica no topo por padrão**, não no rodapé. Isso aparece nos prints
  (o widget "Estoque de Protocolos" tem `● TOTAL` acima do gráfico) e é o default
  do código.
- **O data label já é uma caixinha cinza arredondada com o número dentro.** Esse
  detalhe é o que torna a supressão de magnitude quase invisível como artifício:
  o slab de [`forma-sem-magnitude.md`](forma-sem-magnitude.md) é essa mesma
  caixinha, sem o número.

Quanto ao grid, o produto expõe `chart.gridLineX` e `chart.gridLineY` como
configuráveis. O padrão observado nos prints é grid no eixo de **magnitude** e
nenhum no eixo **categórico**. O tema resolve isso por seletor em vez de por
orientação:

```json
"axisQuantitative": { "grid": true },
"axisDiscrete":     { "grid": false },
"axisTemporal":     { "grid": false }
```

Assim uma barra vertical ganha grid horizontal e uma barra ranqueada horizontal
ganha grid vertical, sem o spec precisar saber qual eixo é qual.

## 10. Como o tema é aplicado

`horus-theme.json` é consumido como o objeto `config` de um spec Vega-Lite. Cada
chave de topo é uma chave válida de `Config` no schema v5/v6, e `vl-convert`
recebe o objeto ao renderizar:

```python
theme = json.load(open(f"{SKILL_DIR}/references/horus-theme.json"))
svg = vlc.vegalite_to_svg(spec, config=theme, vl_version="6.4")
```

A chave `_fonte` do arquivo é metadado e foi verificada: o Vega-Lite a tolera sem
erro, então o pipeline não precisa removê-la.

**O que o tema decide:** paleta, fonte, fundo, estilo de eixo, legenda no topo,
ausência de moldura no canvas, marks (barra sem raio nem contorno, linha com
ponto, arco sem stroke).

**O que fica por spec:** qual eixo perde o rótulo e qual recebe o layer de
placeholder, qual série
recebe qual `shape`, e o `scale.domain` explícito quando importa qual categoria
pega qual cor. Vega-Lite atribui cor por posição no domínio ordenado, não pela
ordem do tema, então fixar isso é responsabilidade de cada spec. Detalhes em
[`ficha-para-vega.md`](ficha-para-vega.md).

**O que o tema não decide:** o card, o título do widget e as ações. O título do
widget vive no header do card e o spec Vega **não deve emitir `title`**. Ver
[`chassi-horus-bi.md`](chassi-horus-bi.md).

## 11. Onde a marca da Horus continua valendo

A paleta institucional (laranja `#F39339`, roxo `#1C0A3B`, bege `#EFEDE6`) e a
Humnst continuam sendo a identidade da **empresa**, e continuam certas na peça
que embrulha o render: capa da proposta, cabeçalho do documento, rodapé do termo
de abertura. Isso é território da `lumo-scope`.

Elas simplesmente não entram no canvas do gráfico nem no card do widget, porque
ali o assunto é o produto que o cliente vai operar, não a marca de quem vende.
