# F3 — Layout & UX (design system · slots · a11y gate)

> **Épico:** `F3` · **Depende de:** F2 · **Habilita:** F4 · **Planos:** 09, 10, 11

## Objetivo

A camada de apresentação: tokens e componentes acessíveis, engine de layout com **slots** e tema
como **dado**, e o gate de acessibilidade que passa a bloquear merges.

## Issues e tasks

### `F3.I1` — Design system (plano 09)
- **T1** Tailwind 4 + daisyUI 5 + Lucide **vendorizados** (zero CDN, gate `vendor-check`).
- **T2** Tokens em `@layer` (a11y por último), `@property`, `oklch`, `clamp`; sem `!important`.
- **T3** Tipografia **Atkinson Hyperlegible Next** (WOFF2 vendorizado, `--font-content`).
- **T4** `wiki/DESIGN.md` como fonte única visual.

### `F3.I2` — Estrutura de layout: contrato mínimo + tema + a11y (plano 10 · ADR-016)

> **Contrato (estrutura ≠ conteúdo ≠ dados).** O módulo define **somente o `<main>`** e os **campos
> padrão** esperados pelo componente do shell (`title`, `description`, `breadcrumbs`, `canonical`,
> `robots`, `image`, `schemas`). Header, rodapé, áreas laterais, acessibilidade, SEO de base e grid
> são **da estrutura**. Vale igualmente para **portal** (`<x-layout::portal>`) e **CMS**
> (`<x-layout::cms>`).

- **T1** ✅ Slots/regiões e presets (Enum tipado `Layout\Engine\LayoutPreset` + `PresetConfig`);
  resolução determinística (`SlotRegistry`).
- **T2** ✅ Tema **como dado** (`jsonb`): precedência `default→global→preset→URL→instância`.
- **T3** ✅ **Shells como componentes** (`Components/{portal,cms}.blade.php`): `@props` com defaults
  determinísticos + **slot único de conteúdo**; `Engine/` na raiz (`LayoutPreset`+`PresetConfig`,
  `SlotRegistry`/`SlotProvider`/`SlotConfig`, `ThemeResolver`/`ThemeTokens`); grid **em CSS**
  (mobile-first, `fr`+`gap`; `.align-rows` via subgrid com fallback `@supports`); regiões vazias
  **colapsam** (`SlotRegistry::has()`); `@extends` legado substituído por componente (ADR-009).
- **T4** ✅ **Camada a11y universal** (`@layer a11y` por último — ADR-010/016): alto contraste (P&B),
  dislexia, espaçamento ampliável, imagens em P&B, movimento reduzido. Estado vem do **servidor**
  (cookie → `data-a11y-*` no `<html>`, sem FOUC); baseline sem JS (`prefers-contrast`/
  `forced-colors`/`prefers-reduced-motion`); barra nativa (atalhos eMAG + A-/A+/contraste/
  grayscale/dislexia) que **sobrepõe só tokens**, nunca a marcação. **VLibras** sob demanda
  (zero CDN por padrão; ADR-002 com exceção justificada).
- **T5** ✅ **Overrides limitados por schema** (`config/design.php` + `Engine/ThemeSchema`): o CMS só
  aceita tokens em **escalas/enums** ou **cor CSS validada**; valor fora do schema é **ignorado**
  (a camada anterior vence) — nunca quebra o layout. Entrega: `GET /theme.css` (same-origin,
  `ETag`/`304`, sem `<style>` inline — ADR-022); sem overrides, **zero** requisição extra.
- **T6** ✅ **Legado removido**: zero CDN (Google Fonts/GA/`api.qrserver.com`), Alpine/Livewire,
  motor “shadow print DOM”, `<style>`/`style="…"`/`!important` inline; um só caminho de impressão
  (`@media print`). `app_version()` inexistente → `SystemVersion`.
- **T7** ✅ **Contrato aplicado aos módulos** (`Noticias`, `Demo`, `Blueprint`) + **teste estrutural**
  (`LayoutContractTest`): view de página usa **um** shell e não contém
  `<html>/<head>/<style>/style="…"`/`x-app-layout`/slots de região.
- **T8** ✅ **Home como módulo próprio** (`src/modules/Portal/Home`, token `home`), mesma filosofia.

### `F3.I3` — Gate de acessibilidade (plano 11)
- **T1** ✅ `src/tests-e2e` (Playwright 1.64.1 + `@axe-core/playwright` + sniffCSS) com **6 histórias
      1:1** (`histories/*.md` × `tests/*.spec.ts`), 3 projetos (desktop, mobile `Pixel 7`, **sem JS**)
      e runner `tools/e2e.sh` (host/Docker com imagem Playwright **pinada por digest** — ADR-031).
      Base mantida: `tools/sniff.sh` + `tools/sniff-gate.sh` (CLI; sem alvos make) e baseline
      versionado em `src/tests-e2e/sniff/baseline/`.
      **O gate deixou de aceitar página de erro**: exige HTTP 200 antes de medir (antes dava verde
      sobre um 404).
- **T2** ✅ `SNIFF_A11Y_STRICT=1` no CI **sem `allow_failure`**; alvos **≥24 px** sempre (SC 2.5.8) e
      **≥44 px** em `pointer: coarse` (`@layer components`, `@media (pointer: coarse)`), foco visível
      (`:focus-visible` ≥0,1875rem) e teclado/skip links cobertos por spec.
- **T3** ✅ `wiki/specs/04-acessibilidade-ux.md`: 3 camadas de prova, checklist eMAG/WCAG 2.2 AA,
      divisão de trabalho (axe arbitra contraste; sniff mede alvo/uniformidade) e **ledger de
      exceções** contado pelo gate.

> **Bugs reais revelados pelo gate (todos corrigidos):** 404 renderizava página “pelada” com o nome
> da exceção; `clean()` (legado v5) quebrava o detalhe com **500**; cookie de a11y era descartado pela
> criptografia (preferência não sobrevivia à navegação); tokens em `:root` **fora de camada** anulavam
> o `@layer a11y` (alto contraste não aplicava); botões `A-`/`A+` com nome acessível divergente do texto
> visível; contraste do texto secundário em 4,47:1; alvos de rodapé/breadcrumb < 24 px.

### `F3.I4` — i18n no módulo (ADR-012) — ✅ fechado
- **T1** ✅ Core carrega `Lang/{locale}/*.php` **por convenção** (módulos e services; namespace = token)
  + `ResolveLocaleFromUrl` (locale **pela URL**, sem sessão/cookie) + rotas do portal localizadas
  (`Core\Localization\Locales`, `URL::defaults`, `hreflang`/`x-default`, `LocalizedDate` com `intl`).
  Strings do **shell** seguem no namespace `layout`.
- **T2** ✅ `make:module`/stubs geram `Lang/{locale}/{index,menu,cms}.php` nos **4 locales** e as views
  já nascem com `__('{token}::…')`; os stubs de view foram reescritos para o contrato F3
  (`<x-layout::portal>`/`<x-layout::cms>`; sem `x-app-layout`, `wire:navigate`, `clean()` nem `onclick`).
- **T3** ✅ Views (módulos **e** services Media/Security) usam o **próprio** namespace; o manifesto
  guarda **chaves** (`token::menu.title`); `ModuleI18nTest` prova 4 locales, **paridade de chaves**,
  ownership e **ausência de texto fixo**; datas via `intl`. Fechamento do contrato de URL:
  `localized_url()` no Core + **gate** contra caminho literal (`url('/x')`, `href="/x"`) — o prefixo do
  locale é aplicado **uma vez** pelo Core e nenhum módulo o conhece. Garantia provada na **saída**
  (`LocaleUrlInvariantTest`): invariante de roteamento (toda rota do portal passa pelo resolvedor e
  permanece no locale) + invariante de link (nenhum link interno escapa do locale no HTML) — cobrem
  módulo futuro sem o autor saber da regra; a busca de config (`/busca`) foi achada por ela.
  _(Conteúdo por locale — `LocaleCast` + aba no CMS — fica na F4.I1.)_

## DoD (binário)

- [x] Página pública montada por **preset + slots**; layout/tema trocáveis **sem deploy**.
- [x] **Contrato de shell:** toda view de módulo (portal/CMS) usa **um** componente de shell e define
  **apenas** o conteúdo central + campos padrão — verificado por **teste estrutural** (`LayoutContractTest`).
- [x] **A11y universal:** P&B, dislexia, espaçamento e imagem P&B sobrepõem tokens/filtros **sem
  alterar a marcação** (provado no E2E); baseline funciona **sem JS**.
- [x] **Zero CDN** na página por padrão (VLibras sob demanda); gate `vendor-check` verde — e o E2E
  prova que **nenhuma requisição sai da origem**.
- [x] Gate axe/eMAG/WCAG AA **sem violações**; `SNIFF_A11Y_STRICT=1` sem `fail`/`warn` não isento;
  **bloqueia merge** (CI sem `allow_failure`).
- [x] Conteúdo público navega **com JS desabilitado** (projeto `portal-no-js`).
- [x] `make check` 2× verde (**212 testes / 779 asserções**) + `make check E2E=1` verde (**50 testes**).
- [x] Cada módulo resolve `__('{token}::…')` do **próprio** `Lang/`; nenhuma string fixa de layout hardcoded na view.

## Evidência

- `make check E2E=1` → **50 passou** (desktop, mobile e sem-JS) com axe sem violações.
- `SNIFF_A11Y_STRICT=1 make check` → desktop 1366×768 e mobile 390×844, `fail=0`, `ignored=55/55`.
- `make check` → EXIT 0; PHPStan `max` 0 erros; baseline 143; `check-gates` OK.
- i18n: `I18nTest` (7) + `ModuleI18nTest` (4, 88 asserções) + `LocalizedDateTest` (3) verdes —
  **218 testes / 886 asserções** no total.
- Baseline versionado: `src/tests-e2e/sniff/baseline/home.jsonl`.

`--note "i18n: locale pela URL, Lang por convencao (4 locales, paridade, sem string fixa), make:module com Lang/ + stubs F3"`.
