# Kit de mockup — ficha de widget, ícones e render Vega-Lite

## Conteúdo

- O que o mockup captura vs o que NÃO captura
- A ficha vem antes do gráfico
- Ícones de tipo (`relacao` → SVG) — marcador opcional, não o gráfico
  - Camada de confiança no widget
- Regras de integridade (herdadas de lumo-design/Few — valem como trava de escopo)
- Kit HTML
  - Como o corpo do widget é gerado
  - Prova — dois widgets do exemplo `vendas-executivo`, renderizados de verdade
  - Esqueleto da página (chrome ao redor do gráfico)
  - Convenções obrigatórias

O mockup existe para o cliente aprovar **conteúdo, hierarquia e escopo** —
mas aprovar isso não exige que a peça pareça inacabada. A ideia de que uma
estética feia-de-propósito ("efeito Balsamiq": quem vê algo polido comenta
cor e fonte, quem vê caixa cinza discute se a informação certa está no
lugar certo) produz crítica melhor foi avaliada e **rejeitada** neste kit,
por dois motivos:

1. **A própria fonte do argumento não o sustenta.** Landay & Myers (SILK,
   CHI '95), a origem citada da tese, chamam a evidência de
   *"anecdotal evidence"* e de *"commonly held belief"* — nunca foi provada,
   é folclore de prática relatado por quem construiu a ferramenta, não um
   achado replicado.
2. **O contexto aqui é diferente do que motivou a tese.** SILK era ferramenta
   de conversa de produto, iterada dentro da própria equipe. O artefato que
   este kit produz alimenta uma proposta comercial e, adiante, um **termo de
   abertura que o cliente assina**. Um documento que vai ser assinado e
   parece provisório é defeito, não virtude — o cliente desconta seriedade
   de uma peça rascunhada, o oposto do efeito que a tese promete.

O registro deste kit é o **do próprio Horus BI**: card branco com borda,
Poppins, a paleta de 11 cores de `Configs.ts`, gráfico Vega-Lite de verdade,
nunca caixa cinza com ícone esquemático fazendo as vezes de gráfico. A
honestidade sobre o que ainda não se sabe não vem de a peça parecer feia; vem
da fronteira explícita entre forma e magnitude e do banner que declara o que é
ilustrativo. O argumento completo (perspectiva artística vs. foto,
divergência deliberada da IDEO e de Sauer & Sonderegger) mora em
`references/forma-sem-magnitude.md` §6 — este arquivo não o duplica, só o
aplica na prática ao kit.

## O que o mockup captura vs o que NÃO captura

| Captura (decisão de escopo) | Não captura (decisão da lumo-design, depois) |
|---|---|
| posição/tamanho no grid, hierarquia de leitura | paleta final de tema claro/escuro do dashboard entregue |
| nomes de negócio reais de métrica/dimensão | visualType exato do widget Horus na plataforma |
| relação-nos-dados de cada bloco (tendência, ranking…) | cor de série do dashboard entregue (aqui a cor já vem do tema Horus do mockup, mas é fixa, não escolhida por widget) |
| encoding: datalabels, legenda, formato de valor | elemento de assinatura visual do dashboard final |
| filtros e interações (cross-filter, drill) | como a semântica de sinal vira exatamente qual token de cor |
| semântica (positivo/negativo/neutro) | tema claro/escuro que o cliente vai navegar depois do go-live |

Quando a ficha identificar relação `fluxo` (sankey) ou `correlação contínua`
(scatter com dado denso, densidade/hexbin): marque `custom: true` na ficha —
fora do catálogo Vega-Lite nativo, provável Svelte, sempre tier XL no
sizing (`references/ficha-para-vega.md` §1.10 fecha o porquê técnico).
Sinalizar cedo gerencia expectativa de custo.

## A ficha vem antes do gráfico

Cada widget nasce como ficha numerada em `dashboards/<dash>.md` — escrita
pela `lumo-scope`, nunca por esta skill. O gráfico deriva da ficha, nunca o
contrário; esta skill lê o campo `relacao` (e o `forma`, se presente) e
devolve o render:

```yaml
- widget: 3
  titulo: "Receita Líquida por Loja — mês corrente"   # descritivo: unidade + métrica + período (IBCS)
  mensagem: "Loja Sul puxa a queda do mês"   # a conclusão que o widget sustenta (IBCS/SAY) —
                       # OBRIGATÓRIO quando publico: executivo; recomendado nos demais;
                       # "resumo de vendas" é vago — vira `a-perguntar` ou peça a mensagem real
  relacao: ranking     # magnitude|tendencia|tendencia-inline|parte-todo|ranking|
                       # distribuicao|correlacao|desvio|espacial|fluxo|metrica-unica|lookup
                       # tendencia-inline = sparkline DENTRO do card do KPI (≤80px),
                       # não widget separado — reduz contagem sem perder informação
  forma: pareto        # sazonal|pareto|tendencia-alta|platô|cauda-longa — OPCIONAL;
                       # a forma matemática que a série do mockup desenha (decaimento,
                       # rampa, onda...), nunca ruído aleatório; ausente = default neutro
                       # por `relacao` (ver ficha-para-vega.md §3)
  confianca: confirmado  # confirmado|hipótese|a-perguntar — herda de incognitas.md;
                       # métrica/mensagem ainda não validada = hipótese/a-perguntar,
                       # widget nunca some, vira placeholder visível (ver seção abaixo)
  metricas: ["metricas/receita-liquida.md"]
  dimensoes: ["dim_loja.nome"]
  filtros: ["responde ao cross-filter de período (widget 1)"]
  interacoes: "clique na barra filtra os widgets 4-6"
  encoding: { datalabels: sim, legenda: nao, formato: "R$ mil" }
                       # formato vira placeholder via do_formato() do
                       # scripts/placeholder.py: "R$ mil" -> R$ 123,5 mil,
                       # "R$" -> R$ 123.456,78, "%" -> 12,3%, "n/a" -> vazio
  posicao: { y: 1, x: 0, w: 6, h: 6 }    # ver "O schema de posicao" abaixo:
                       # w = colunas do grid de 12; h = LINHAS de 40px, e há
                       # mínimo por tipo de widget (h:1 e h:2 não cabem gráfico)
  custom: false
```

### O schema de `posicao`

| Campo | Significa |
|---|---|
| `x` | coluna inicial, 0 a 11 |
| `y` | linha inicial |
| `w` | largura em colunas do grid de **12** (produção configura 12/24/36/60; declare `grid_columns` no `dashboards/*.md` se o dashboard pedir densidade diferente) |
| `h` | altura em **linhas de 40px** (`cellHeight` do GridStack) |

**`h` tem mínimo, e ele não é 1.** A altura útil de um card é
`h × 40 + (h−1) × 4 − 32` (padding), menos 30 se o widget tem título. Isso
significa que valores baixos produzem áreas impossíveis:

| `h` | útil com título | cabe? |
|---|---|---|
| 1 | **−22px** | nada: valor negativo |
| 2 | 22px | nada |
| 3 | 66px | só KPI (que não usa título; sem ele são 96px) |
| 6 | 198px | gráfico |
| 8 | 286px | gráfico confortável, tabela curta |
| 12 | 446px | tabela, mapa, matriz |

Regra prática: **KPI `h:3`, gráfico `h:6` a `h:8`, tabela e mapa `h:8` a `h:12`.**
Uma ficha que declara `h:1` ou `h:2` num gráfico não é renderizável, e o erro só
aparece na hora do render, quando a fórmula devolve altura negativa.

`encoding` é obrigatório — datalabels/legenda/formato nunca especificados =
ciclo de correção pós-entrega garantido (incidente real). `relacao` é o
vocabulário que a `lumo-design` reusa depois para escolher o widget Horus
definitivo — o mockup não escolhe o visualType final, só a forma que vende a
decisão de escopo.

## Ícones de tipo (`relacao` → SVG) — marcador opcional, não o gráfico

No kit antigo o ícone SVG era o **conteúdo inteiro** da caixa do widget —
placeholder esquemático do gráfico que ainda não existia. Isso mudou: o
corpo do widget agora é o **gráfico Vega-Lite real**, renderizado a partir
da ficha (mapeamento completo por `relacao`: `references/ficha-para-vega.md`).
O ícone sobrevive como **vocabulário compartilhado**, mas rebaixado a
marcador de tipo opcional — no máximo um selo pequeno perto do número do
widget, útil para escanear a tela rápido sem interpretar cada gráfico; ele
nunca mais substitui o gráfico nem aparece sozinho quando o `relacao` mapeia
pra um mark Vega-Lite. Mapeamento canônico (12 valores, inalterado):

| `relacao` | classe | glifo |
|---|---|---|
| magnitude | `wi-bars` | barras verticais |
| ranking | `wi-bars-h` | barras horizontais |
| tendencia | `wi-line` | linha |
| tendencia-inline | `wi-spark` | sparkline (dentro do KPI) |
| parte-todo | `wi-donut` | donut |
| distribuicao | `wi-dist` | histograma/boxplot |
| correlacao | `wi-scatter` | pontos dispersos |
| desvio | `wi-bullet` | bullet |
| espacial | `wi-map` | pino de mapa |
| fluxo | `wi-sankey` | sankey |
| metrica-unica | `wi-kpi` | número grande |
| lookup | `wi-table` | grade de tabela |

Snippets verbatim (`viewBox 0 0 32 20`, exceto `wi-spark`) — mesmos 12 SVG do
kit original, sem alteração de forma:

```html
<!-- magnitude -->
<svg class="wi-icon wi-bars" viewBox="0 0 32 20"><rect x="3" y="10" width="4" height="8"/><rect x="10" y="6" width="4" height="12"/><rect x="17" y="12" width="4" height="6"/><rect x="24" y="3" width="4" height="15"/></svg>
<!-- ranking -->
<svg class="wi-icon wi-bars-h" viewBox="0 0 32 20"><rect x="3" y="3" width="14" height="3"/><rect x="3" y="8.5" width="22" height="3"/><rect x="3" y="14" width="10" height="3"/></svg>
<!-- tendencia -->
<svg class="wi-icon wi-line" viewBox="0 0 32 20"><polyline points="3,15 11,9 18,12 29,4"/></svg>
<!-- parte-todo -->
<svg class="wi-icon wi-donut" viewBox="0 0 32 20"><circle cx="16" cy="10" r="7"/><circle cx="16" cy="10" r="3" fill="#fff"/><path d="M16 3 A7 7 0 0 1 23 10" stroke-width="3"/></svg>
<!-- distribuicao -->
<svg class="wi-icon wi-dist" viewBox="0 0 32 20"><rect x="4" y="12" width="4" height="6"/><rect x="10" y="7" width="4" height="11"/><rect x="16" y="4" width="4" height="14"/><rect x="22" y="9" width="4" height="9"/></svg>
<!-- correlacao -->
<svg class="wi-icon wi-scatter" viewBox="0 0 32 20"><circle cx="7" cy="14" r="1.5"/><circle cx="13" cy="9" r="1.5"/><circle cx="18" cy="12" r="1.5"/><circle cx="23" cy="6" r="1.5"/><circle cx="27" cy="10" r="1.5"/></svg>
<!-- desvio (bullet) -->
<svg class="wi-icon wi-bullet" viewBox="0 0 32 20"><rect x="4" y="8" width="24" height="4"/><rect x="4" y="8" width="15" height="4" fill="#999"/><line x1="22" y1="6" x2="22" y2="14" stroke-width="2"/></svg>
<!-- espacial (pino) -->
<svg class="wi-icon wi-map" viewBox="0 0 32 20"><path d="M16 3 C12 3 10 6 10 9 C10 13 16 17 16 17 C16 17 22 13 22 9 C22 6 20 3 16 3 Z"/><circle cx="16" cy="9" r="2"/></svg>
<!-- fluxo (sankey) -->
<svg class="wi-icon wi-sankey" viewBox="0 0 32 20"><path d="M4 5 C14 5 18 8 28 8"/><path d="M4 12 C14 12 18 9 28 9"/><path d="M4 16 C14 16 20 13 28 13"/></svg>
<!-- metrica-unica (KPI) -->
<svg class="wi-icon wi-kpi" viewBox="0 0 32 20"><text x="16" y="15" text-anchor="middle" font-size="13" fill="#999" stroke="none">42</text></svg>
<!-- lookup (tabela) -->
<svg class="wi-icon wi-table" viewBox="0 0 32 20"><rect x="4" y="4" width="24" height="12"/><line x1="4" y1="9" x2="28" y2="9"/><line x1="12" y1="4" x2="12" y2="16"/><line x1="20" y1="4" x2="20" y2="16"/></svg>
<!-- tendencia-inline (sparkline dentro do card) -->
<svg class="wi-icon wi-spark" viewBox="0 0 32 12"><polyline points="2,9 8,5 14,7 20,3 26,6 30,2"/></svg>
```

CSS — redimensionado pro novo papel de selo, não de conteúdo:
`.wi-icon{width:16px;height:12px;opacity:.7;fill:none;stroke:#A19E91;stroke-width:1.5}`.
O selo, quando usado, fica no cabeçalho do widget ao lado do número
(`.wf-n`) — nunca mais centralizado no corpo, que agora é do gráfico. Widget
que cruza a relação com o gráfico finalmente escolhido (ex.: `parte-todo`
desenhado como barra empilhada por causa do limite de fatias) mantém o selo
da relação abstrata — o ícone documenta o vocabulário da ficha, não a
decisão final de visualType (isso é da `lumo-design`).

### Camada de confiança no widget

A ficha de widget carrega `confianca:` — herda os 3 níveis de
`incognitas.md` (`confirmado`/`hipótese`/`a-perguntar`, ver `lumo-scope`).
Widget cuja métrica ainda não foi validada nunca desaparece do mockup nem
vira caixa vazia: o corpo do widget continua sendo o gráfico Vega-Lite
(com dados sintéticos de forma, `ficha-para-vega.md` §3), e abaixo dele
renderiza o marcador `.wf-confidence` — texto vestido em linguagem de
negócio, nunca a etiqueta técnica crua (`hipótese`/`a-perguntar`) nem a
fórmula interna da métrica. O vocabulário exato — por nível de confiança,
por vista comercial/termo, por modo rascunho/termo — e o markup/CSS do
marcador são definidos em `references/marcadores-confianca.md` §4/§8; este
arquivo não redefine essa mecânica, só garante que o widget nunca fica mudo
sobre o próprio estado de confiança. Exemplo de ficha com métrica em aberto:

```yaml
- widget: 6
  titulo: "Churn de Clientes — mês"
  mensagem: "Churn sobe no trimestre"
  relacao: metrica-unica
  confianca: hipótese     # confirmado|hipótese|a-perguntar — ver incognitas.md
  metricas: []             # fórmula ainda não validada com o cliente — detalhe
                       # interno, nunca aparece no render (marcadores-confianca.md §2)
  dimensoes: []
  filtros: []
  interacoes: "nenhuma — KPI puro"
  encoding: { datalabels: sim, legenda: nao, formato: "%" }
  posicao: { y: 2, x: 0, w: 4, h: 3 }
  custom: false
```

Renderizado (`vista: termo` / `modo: termo`, texto de
`marcadores-confianca.md` §4):

```html
<div class="hb-card" style="grid-column:span 3; grid-row:span 3">
  <span class="wf-n">6</span>
  <div class="hb-kpi">
    <div class="hb-kpi__icon">📉</div>
    <div>
      <div class="hb-kpi__over">Churn de Clientes — mês</div>
      <div class="hb-kpi__val">12,3%</div>
      <div class="hb-kpi__help">Premissa de trabalho para este indicador, pendente de validação com o cliente.</div>
    </div>
  </div>
</div>
```

Nunca `[hipótese: churn = cancelamentos/base ativa]` — o formato antigo
expunha, ao mesmo tempo, a etiqueta técnica crua e a fórmula interna da
métrica; os dois são proibidos no que o cliente lê
(`marcadores-confianca.md` §2). O CSS de `.wf-confidence` e a tabela
completa de wording por nível/vista/modo moram só em
`marcadores-confianca.md` §4/§8 — um dono por regra.

## Regras de integridade (herdadas de lumo-design/Few — valem como trava de escopo)

Se o analista/cliente pedir algo desta lista, reconduza ANTES de ir para
aprovação — aprovar escopo tecnicamente inválido é overdelivery de
retrabalho. Estas regras não são estética: são trava de escopo, e
sobrevivem intactas à migração deste kit para a `lumo-mockup`.

- **≤ 9 widgets por tela.** Mais que isso = segunda aba/dashboard (nova linha de
  escopo, novo tier).
- **Sem gauge/velocímetro** → KPI com meta + variação, ou bullet. **Bullet exige
  as `faixas_qualitativas` do KPI card** (2-5 faixas ruim/ok/bom) — sem faixas
  declaradas é barra de progresso, não bullet; não aprove como bullet sem elas.
- **Precisão no nível da decisão, não da fonte** — KPI executivo arredonda ao
  que muda a decisão (R$ mil/mi, 0-1 casa decimal); centavos só em tabela de
  conferência/auditoria. Pedido de valor exato em resumo executivo é recondução.
- **Escala idêntica entre widgets comparáveis** — mesma métrica em cortes
  diferentes (mesma tela) ou repetida entre dashboards da rodada usa o mesmo
  range de eixo, mesma unidade (R$ mil OU R$, nunca misturado) e mesmas casas
  decimais (IBCS/CHECK).
- **Parte-todo: donut com até 4 categorias, barra horizontal a partir de 5.**
  Sempre donut (`innerSize: "60%"`), pizza sólida nunca, mesmo quando o
  dashboard de referência do cliente usa pizza (`forma-sem-magnitude.md` §5).
- **>5 séries simultâneas** em linha ou barra agrupada → troque por pequenos
  múltiplos, Top-N + "outros", ou 1 série em destaque com o resto em cinza. O
  teto de 5 vem da medição da paleta: a partir da 6ª posição ela repete matiz
  (`identidade-visual.md` §5).
- **Agregação segue a aditividade da métrica.** Coluna de razão (ticket médio,
  margem, %) não recebe linha `Total` somada, e widget que agrega razão precisa
  ser sinalizado como recomposição. Classe no campo `aditividade:` da ficha;
  regra operacional em `lumo-design:references/aditividade.md`.
- **Distribuição de variável contínua não-binada** → histograma/boxplot
  (`custom: true`); já binada em faixas → barra ordenada, nunca pizza.
- **Contraste**: elementos de gráfico ≥3:1, texto ≥4.5:1 — o tema Horus
  (`identidade-visual.md`) já entrega isso medido; se o widget usar cor fora
  do tema, meça de novo antes de aprovar.
- **Widget que cruza métricas de 2 fatos**: só dimensões estritamente comuns
  aos dois + nota de drill-across na ficha (`limites-horus.md`, `lumo-scope`).
- **Tabela de detalhe do mockup nunca usa dado real** (LGPD): cada coluna
  numérica repete o mesmo placeholder (e a linha `Total` usa o de uma escala
  acima), coluna de texto vira placeholder semântico
  (`forma-sem-magnitude.md` §2/§4), nunca lorem ipsum nem nome plausível.
- **Série temporal = linha**, nunca barra (barra só para poucos períodos discretos).
- **Barra parte do zero** (baseline zero inegociável).
- **Dashboard = uma tela** (sem scroll para público executivo/monitoramento).
- **KPI principal no topo-esquerda**; KPI-row no topo (y:0) com o cross-filter;
  gráficos no meio; tabelas de detalhe embaixo.
- Layout por público: executivo = 3-6 KPIs + 2-4 gráficos (subdivisão em 3 col);
  operacional denso = subdivisão em 6 col; filtros ricos = sidebar 15-20%.

**Semântica sem depender só de cor**: real = preenchido, plano/meta =
contorno, forecast = hachurado, período anterior = tom mais claro da mesma
série. Variação favorável/desfavorável = ▲/▼ + sinal, nunca só cor — mesmo
com a paleta Horus aplicada, esta redundância continua obrigatória: é a
mesma disciplina que `identidade-visual.md` §5 documenta para deuteranopia
(a partir da 4ª série simultânea, cor sozinha não basta).

## Kit HTML

Um arquivo `wireframes/<dash>.html` autocontido por dashboard — mesma
estrutura de sempre (banner, grid de 12 colunas, trilho de anotações), mas
o corpo de cada widget (`.wf-body`) agora recebe o **SVG do Vega-Lite**
renderizado para aquele widget, não mais uma caixa cinza com ícone.

### Como o corpo do widget é gerado

1. Ler `relacao` (e `forma`, se presente) da ficha → montar o spec Vega-Lite
   correspondente (mapeamento completo: `references/ficha-para-vega.md`).
2. Renderizar o spec a SVG com o tema Horus injetado:
   ```python
   import vl_convert as vlc
   svg = vlc.vegalite_to_svg(spec, config=horus_theme)  # horus-theme.json
   ```
3. Inserir a string SVG resultante direto dentro do `.wf-body` do widget —
   sem `<img>`, sem base64, o markup `<svg>` inline (mesmo padrão que os
   ícones de tipo já usavam, só que agora é o gráfico inteiro, não um selo).

### Prova — dois widgets do exemplo `vendas-executivo`, renderizados de verdade

Os dois specs abaixo foram compilados com `vl-convert-python` 1.9.0 nesta
task (não teorizados) — mesmo mecanismo e mesmo tema de
`ficha-para-vega.md` §2, aplicado ao widget 2 do exemplo canônico da
`lumo-scope` (`exemplo-canonico/escopo/dashboards/vendas-executivo.md`) e a
uma segunda demonstração de `ranking` que reaproveita o título de outro
widget do mesmo gabarito (nota de precisão abaixo).

**"Widget 5" (demonstração de `ranking`) — "Receita Líquida por Loja — mês
corrente"** (mesmo título/dado do exemplo em `ficha-para-vega.md` §3.2,
`forma: pareto`. Nota de precisão: no gabarito real
(`exemplo-canonico/escopo/dashboards/vendas-executivo.md`) este título é o
widget 5, mas com `relacao: parte-todo` — reconduzido para barra empilhada,
não `ranking` puro, ver `ficha-para-vega.md` §1.5. O exemplo abaixo reaproveita
o mesmo título/dado só pra provar o render `ranking`; não reproduz o chart
type real do widget 5):

```json
{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "width": 340, "height": 140,
  "title": "Receita Líquida por Loja — mês corrente",
  "data": { "values": [
    {"loja": "Loja Sul", "valor": 1.00}, {"loja": "Loja Centro", "valor": 0.62},
    {"loja": "Loja Norte", "valor": 0.38}, {"loja": "Loja Leste", "valor": 0.24},
    {"loja": "Loja Oeste", "valor": 0.15}
  ]},
  "mark": {"type": "bar", "aria": false},
  "encoding": {
    "y": { "field": "loja", "type": "nominal", "sort": "-x", "title": null,
           "axis": {"domain": true, "ticks": true} },
    "x": { "field": "valor", "type": "quantitative", "axis": null },
    "color": {
      "field": "loja", "type": "nominal", "legend": null,
      "scale": { "domain": ["Loja Sul", "Loja Centro", "Loja Norte", "Loja Leste", "Loja Oeste"] }
    }
  }
}
```

**Widget 2 — `tendencia-inline`, sparkline dentro do card de "Receita
Líquida — mês"**:

```json
{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "width": 80, "height": 24,
  "data": { "values": [
    {"periodo": "P1", "valor": 0.25}, {"periodo": "P4", "valor": 0.415},
    {"periodo": "P8", "valor": 0.635}, {"periodo": "P12", "valor": 0.855}
  ]},
  "mark": {"type": "line", "aria": false},
  "encoding": {
    "x": { "field": "periodo", "type": "ordinal", "sort": null, "axis": null },
    "y": { "field": "valor", "type": "quantitative", "axis": null }
  }
}
```

Renderizados com `vlc.vegalite_to_svg(spec, config=theme)` (`horus-theme.json`
completo, os 12 pontos de dado no widget 2, aqui truncados na citação para
brevidade — o comportamento não muda com mais pontos):

Saída real de `exemplo/render-prova.py`, que renderiza quatro arquétipos e roda
o assert anti-vazamento em cada um:

```
=== escalas desta tela ===
  receita  R$ 123.456,78   total R$ 1.234.567,89
  ticket   R$ 123,45       qtde  123

=== widgets ===
  [ok ] magnitude/Barline
  [ok ] ranking/Barline
  [ok ] parte-todo/Pie
  [ok ] tendencia/Barline
```

Todo texto com dígito no SVG é ou um placeholder da tabela de escalas ou um
nome de categoria declarado na ficha. Nada mais passa.

### Esqueleto da página (chrome ao redor do gráfico)

O slot do overlay narrativo (obrigatório em toda tela,
`references/overlay-narrativo.md`) mora aqui: classe `.wf-overlay`, logo
abaixo do `.wf-banner` e acima do `.wf-layout`, sempre **UM por tela** —
nunca um por widget, nunca duplicando o `.wf-annot` lateral (que documenta
decisão de encoding, não de negócio). Este arquivo só define classe e
posição; o texto é assunto exclusivo de `overlay-narrativo.md`.

A área do dashboard (`.hb-*`) reproduz o chassi do produto token a token, e o
que sobra (`.wf-*`) é ferramenta de aprovação que só existe no mockup: banner,
overlay, numeração de widget e trilho de anotações. Tokens em
`horus-chassi.json`, origem de cada valor em `chassi-horus-bi.md`.

```html
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mockup — [Dashboard]</title>
<style>
  /* Poppins local: o artefato circula por e-mail e precisa abrir sem rede. */
  @font-face { font-family:Poppins; font-weight:400;
               src:url('<SKILL_DIR>/assets/fonts/Poppins-Regular.ttf') format('truetype') }
  @font-face { font-family:Poppins; font-weight:500;
               src:url('<SKILL_DIR>/assets/fonts/Poppins-Medium.ttf') format('truetype') }

  :root {
    --hb-page:#F8FAFC; --hb-card:#fff; --hb-card-border:rgba(241,245,249,.5);
    --hb-title:#334155; --hb-action:#CBD5E1; --hb-axis:#949DAC;
    --hb-slab:#E2E8F0; --hb-cell-border:#F1F5F9;
  }
  * { box-sizing:border-box; margin:0 }
  body { font:14px/1.45 Poppins,-apple-system,system-ui,sans-serif;
         color:var(--hb-title); background:var(--hb-page); padding:20px;
         max-width:1440px; margin:0 auto }

  /* --- chassi do produto --- */
  .hb-filters { display:flex; gap:6px; margin-bottom:8px }
  .hb-filter { background:#fff; border:1px solid #E2E8F0; border-radius:4px;
         padding:4px 8px; font-size:12px }
  .hb-filter b { display:block; font-size:10px; font-weight:400; color:var(--hb-axis) }
  .hb-grid { display:grid; grid-template-columns:repeat(12,1fr);
         grid-auto-rows:40px; gap:4px }            /* GridStack: 12 col, margin 2, cellHeight 40 */
  .hb-card { position:relative; background:var(--hb-card);
         border:2px solid var(--hb-card-border); border-radius:8px;
         box-shadow:0 1px 2px 0 rgb(0 0 0/.05); padding:16px;
         display:flex; flex-direction:column; overflow:hidden }
  .hb-card h3 { font-size:14px; font-weight:500; letter-spacing:.025em;
         margin-bottom:8px; padding-right:56px }
  .hb-actions { position:absolute; top:16px; right:16px; display:flex; gap:8px;
         color:var(--hb-action); font-size:13px }
  .hb-body { flex:1; display:flex; align-items:center; justify-content:center; min-height:0 }
  /* slab: fallback, só quando não há escala honesta (forma-sem-magnitude §2b) */
  .hb-slab { display:inline-block; height:12px; border-radius:5px;
         background:var(--hb-slab); vertical-align:middle }

  /* KPI — slots do KpiV2 */
  .hb-kpi { display:grid; grid-template-columns:auto 1fr; align-items:center; gap:12px }
  .hb-kpi__over { font-size:12px; text-transform:uppercase; letter-spacing:.025em; opacity:.7 }
  .hb-kpi__val  { font-weight:500; font-size:28px; display:flex; align-items:baseline; gap:6px }
  .hb-kpi__sub  { font-size:12px; opacity:.8 }
  .hb-kpi__help { font-size:10px; opacity:.6 }

  /* Tabela */
  .hb-table { width:100%; border-collapse:collapse; font-size:12px }
  .hb-table th { font-weight:500; text-align:left; border-bottom:1px solid #E2E8F0; padding:4px 6px }
  .hb-table td { border-bottom:1px solid var(--hb-cell-border); padding:4px 6px }
  .hb-table .num { text-align:right }
  .hb-table .ord { width:28px; color:var(--hb-axis) }
  .hb-table tfoot td { font-weight:600; border-top:1px solid #E2E8F0; border-bottom:0 }

  /* --- chrome só do mockup, não existe no produto --- */
  .wf-banner { border:1px solid #E2E8F0; background:#fff; padding:10px 14px;
         text-align:center; font-weight:500; letter-spacing:.02em; margin-bottom:16px }
  .wf-overlay { border-left:3px solid #6567ea; background:#fff; padding:10px 16px;
         margin-bottom:16px; font-size:13px; line-height:1.4 }
  .wf-layout { display:grid; grid-template-columns:1fr 300px; gap:20px }
  .wf-n { position:absolute; top:-1px; left:-1px; width:18px; height:18px;
         background:#6567ea; color:#fff; border-radius:8px 0 8px 0; font-size:10px;
         font-weight:500; display:flex; align-items:center; justify-content:center }
  .wf-annot { border-left:1px solid #E2E8F0; padding-left:16px; font-size:12px }
  .wf-annot li { margin-bottom:10px }
</style></head><body>
<div class="wf-banner">DADOS ILUSTRATIVOS — A FORMA REFLETE A PROPOSTA, OS VALORES NÃO REFLETEM O CLIENTE</div>
<div class="wf-overlay">[overlay narrativo desta tela — UMA decisão de negócio, nunca uma por widget; texto vem de overlay-narrativo.md, nunca inventado aqui]</div>
<div class="wf-layout">
  <div>
    <!-- Filtros: dimensões REAIS da ficha. A DATA é filtro como os outros e vem
         SEMPRE primeiro (chassi-horus-bi.md §8). Não existe pill de período
         separado no canto direito. -->
    <div class="hb-filters">
      <div class="hb-filter"><b>Período</b>🗓 Abril 1, 2026 - Abril 30, 2026 ▾</div>
      <div class="hb-filter"><b>Filial</b>Todas selecionadas</div>
      <div class="hb-filter"><b>Vendedor</b>Todos selecionados</div>
    </div>

    <div class="hb-grid">
      <div class="hb-card" style="grid-column:span 3; grid-row:span 2"><span class="wf-n">2</span>
        <div class="hb-kpi">
          <div class="hb-kpi__icon">💲</div>
          <div>
            <div class="hb-kpi__over">Receita Líquida — mês</div>
            <div class="hb-kpi__val">R$ 123.456,78</div>
            <div class="hb-kpi__sub">▲ vs mês anterior</div>
          </div>
        </div>
      </div>
      <div class="hb-card" style="grid-column:span 6; grid-row:span 6"><span class="wf-n">5</span>
        <h3>Receita Líquida por Loja — mês corrente</h3>
        <div class="hb-actions">🔍 ↗</div>
        <div class="hb-body">
          <!-- SVG do vl-convert injetado aqui, inline -->
        </div>
      </div>
      <!-- …um .hb-card por ficha; posição = ficha.posicao; corpo = SVG Vega-Lite do relacao -->
    </div>
  </div>
  <aside class="wf-annot"><h3>Anotações</h3><ol>
    <li><b>Receita Líquida:</b> ver KPI card, tendência diária embutida como sparkline.</li>
    <li><b>Ranking por loja:</b> datalabels SIM, sem legenda, R$ mil; clique filtra widgets seguintes.</li>
  </ol></aside>
</div></body></html>
```

`grid-auto-rows: 40px` e `gap: 4px` não são escolha estética: são o `cellHeight`
e o `margin` do GridStack do produto. Manter esses dois valores é o que faz a
proporção dos widgets no mockup bater com a do dashboard entregue, e é por isso
que um KPI ocupa `span 2` de altura e um gráfico ocupa `span 6`.

O banner acima é o mínimo que `forma-sem-magnitude.md` §6 exige (analogia
do "perspectiva artística ilustrada" de render de incorporação) — o texto
dele é fixo, não varia por modo (rascunho vs. termo). Quem varia por modo
(e por nível de confiança) é o marcador `.wf-confidence` de cada widget —
vocabulário exato em `references/marcadores-confianca.md` §4/§8.

### Convenções obrigatórias

- Banner de dados ilustrativos sempre no topo — nunca "BAIXA FIDELIDADE":
  a peça é sóbria e projetada, o que é ilustrativo é o **valor**, não o
  acabamento.
- Overlay narrativo (`.wf-overlay`) sempre presente, sempre **UM por
  tela** — nunca por widget, nunca confundido com o banner acima (esse é
  aviso de conformidade; o overlay é conteúdo comercial). Texto e critério
  de qualidade (filtro anti-blandness) em `references/overlay-narrativo.md`.
- Paleta e tipografia do **produto** (`defaultColors` + Poppins), nunca as cores
  de marca da Horus-empresa e nunca cor ad hoc por widget
  (`identidade-visual.md`, `horus-theme.json`). O card, a borda e a sombra vêm
  do `Widget.svelte` e não são decoração: são o que faz o cliente reconhecer a
  tela dele (`chassi-horus-bi.md`).
- Registrar a fonte antes de todo render
  (`vlc.register_font_directory(f"{SKILL_DIR}/assets/fonts")`). Sem isso o PNG
  sai sem nenhum rótulo, em silêncio (`pipeline.md` §6.4).
- Nomes de negócio REAIS em todo título (nunca "Gráfico 1"; o rótulo É o
  conteúdo sendo validado). Todo número suprimido vira slab (`.hb-slab`) na
  Todo número exibido é o **placeholder da escala escolhida**, idêntico em todo
  o widget (`R$ 123.456,78`), gerado por `scripts/placeholder.py`. Jamais
  `R$ —` (lê como quebrado), jamais `R$ ——.———` nem `9.999`, e jamais um valor
  que varia entre elementos. O gráfico usa `labels: false` no eixo de
  magnitude, mantendo a gridline, mais um layer de `text` com `value` constante
  no data label (`ficha-para-vega.md` §2). Texto de célula é placeholder semântico
  (`Fornecedor A`), nunca nome plausível (`forma-sem-magnitude.md` §2/§4).
- Corpo do widget = SVG Vega-Lite real (ou slab/tabela HTML para
  `metrica-unica`/`lookup`, que não são Vega-Lite —
  `ficha-para-vega.md` §1.11/§1.12); nunca caixa vazia, nunca ícone sozinho
  fazendo as vezes de gráfico.
- Anotações numeradas ①② no trilho lateral, escritas JUNTO com o desenho —
  nunca setas cruzando o canvas. Densidade de anotação não cai com a
  estética sóbria — o trilho continua carregando o porquê de cada decisão.
- Aprovação por dashboard ("aprova este antes de eu desenhar o próximo?"),
  não em bloco no final.
