# F3.I2.T4 — Camada a11y universal (P&B, dislexia, espaçamento, grayscale; sem JS; VLibras sob demanda)

| Campo | Valor |
|---|---|
| **Fase** | F3 — Layout & UX |
| **Issue** | F3.I2 — Layout engine com slots (plano 10 · ADR-016) |
| **kd task** | `task_01u125ey` (`closed`) |
| **Âncora** | `wiki/implementation/03-layout-ux.md` |
| **Status da revisão** | 🔧 com correções — A (bloqueador) corrigido; B/C baixos registrados |

## O que foi implementado

- `@layer a11y` (último) sobrepõe cores/tipografia/contraste/imagens **sem tocar a marcação**.
- 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) e VLibras sob demanda (zero CDN por padrão).

## Arquivos para revisar

- `src/layout/Assets/css/a11y.css`
- `src/layout/Engine/A11yPreferences.php`
- `src/layout/Engine/A11yFontScale.php`
- `src/layout/Portal/partials/a11y-bar.blade.php`
- `src/layout/Assets/js/a11y.js`
- `src/layout/Lang/{pt_BR,en,es,fr}/a11y.php`
- `src/tests-e2e/tests/05-preferencias-a11y.spec.ts`
- `src/tests-e2e/tests/03-no-js.spec.ts`

## Pontos de revisão

### PHP 8.4
- [ ] `declare(strict_types=1)`; tipos nativos (union/enum/`readonly`); sem `mixed` desnecessário.
- [ ] Dependências **injetadas** (nenhum `new`/`app()`/`resolve()` no corpo); sem traits/abstract/generics.
- [ ] `match` em vez de cadeias de `if`; `?->`/`??`; sem código morto; PHPStan `max` limpo.

### Laravel 12
- [ ] Nativo primeiro (Eloquent casts/scopes, `FormRequest`, `Gate`, scheduler, events) — sem reimplementar.
- [ ] Sem closure em rota; resources/rotas nomeadas; `config()` só lê, `env()` só em `config/`.
- [ ] Migração **idempotente**; `down()` → `backup_*` (nunca `DROP`); sem `orderBy` cru quando `latest/oldest` serve.

### HTML semântico + a11y (eMAG/WCAG 2.2 AA)
- [ ] Um `<main>`, headings em nível correto, landmarks; só ARIA quando necessário.
- [ ] Alvos ≥ 24 px (≥ 44 px em ponteiro grosso); foco visível; rótulos em todos os campos; contraste (axe).
- [ ] Funciona **sem JS**; `sniffCSS` sem `fail`/`warn` fora do ledger.

### CSS moderno
- [ ] `oklch`, `clamp`, `rem`/`fr`/`dvh`/`%`; `@layer` (a11y por último); `@property`; subgrid com fallback.
- [ ] Sem `!important`, sem `style="…"` inline, sem hex/rgb (exceto justificado); sem CDN.

### Decisões do projeto (ADRs)
- [ ] Respeita a ADR do escopo (ver cabeçalho) e não escapa das demais (001, 002, 004, 013, 015…).
- [ ] Determinismo: relógio/id injetados, ordem explícita, dados idempotentes (ADR-001).

### i18n (ADR-012)
- [ ] Nenhuma string visível fixa; 4 locales com paridade de chaves; `route()`/`localized_url()` (nunca path literal).

### Observabilidade/segurança/testes
- [ ] Span + `log_event('{module}.{action}')` com atributos `scalar|null`, **sem PII** (ADR-024).
- [ ] Teste que falha sem a mudança (vermelho→verde) + evidência no `kd`; `make quality` 2×.

### Específicos desta task

- [ ] Preferência sobrevive à navegação (cookie **não** criptografado; `encryptCookies(except:)`).
- [ ] A- / A+ com nome acessível = texto visível; contraste do texto secundário ≥ 4,5:1.
- [ ] VLibras só carrega por ação do usuário (allowlist).

## Comandos úteis

```bash
make quality            # lint + PHPStan max + gates + testes
make e2e                # Playwright + axe (se a task toca UI)
make check-boundaries   # fronteiras core/services/layout/modules
```

---

> Revisar contra `AGENTS.md`, `wiki/adr/` e o plano citado. Registrar achados no `kd` (`kd write --anchor`) e, se houver correção, teste de regressão.

## Notas da revisão

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-17 | agente (protocolo v2.0) | 🔧 | 1 bloqueador corrigido (A) com teste vermelho→verde (axe no modo alto contraste); 2 baixos no `kd` |

### Evidência
- **Gates (§10):** `make quality-2x` EXIT=0 (**407×2**) · PHPStan max `No errors` · `make e2e` **81/81** (+2) · `SNIFF_A11Y_STRICT=1 make a11y` OK (`fail=0`).
- **D7 — 3 camadas de prova:** estrutural (`LayoutShellTest` a11y/`A11yPreferences`), jornada (e2e `05` agora com **axe no modo alto contraste**), estilo computado (sniff).
- **D8 — i18n:** `layout::a11y.*` com paridade de chaves nos 4 locales (pt_BR/en/es/fr) — conferido.

### Achados

| ID | Sev. | Dimensão | Descrição | Disposição |
|---|---|---|---|---|
| **A** | Bloqueador | D7/D6 | O `@layer a11y` remapeava só os tokens **semânticos** (`--surface`/`--ink`/`--tenant-*`/`--status-*`), mas os módulos consomem a paleta **`--civic-*`** (`text-civic-ink`/`text-civic-outline`…) e o daisyUI usa `--color-*-content`. No alto contraste (fundo preto/amarelo) isso produziu **4 violações AA** medidas pelo axe — `h1` `#000511` sobre preto (**1,02:1**), `.text-civic-outline` (4,3:1), `btn-primary` branco sobre amarelo (**1,07:1**) e o botão “Contraste” ativo (1,07:1) — **sem nenhum gate** rodando axe nesse modo (ADR-016/ADR-025). | **Corrigido**: `a11y.css` remapeia a paleta civic + os pares `--color-*-content` + o botão ativo da barra; e2e `05` roda **axe em 4 páginas × 2 projetos** (vermelho→verde). Resolve o risco `risk_00b2hslm`. `kd`: `fact_00phcs0b`. |
| **B** | Baixo | D1/D14 | O *opt-in* de movimento reduzido é **inalcançável**: `A11yPreferences` lê `a11y_motion` e o CSS tem `html[data-a11y-motion='reduced']`, mas o `TOGGLES` do `a11y.js` não inclui `motion` e a barra não tem botão. O baseline `prefers-reduced-motion` cobre o sistema. | Nota: `risk_00lbc67g`. |
| **C** | Baixo | D4/D6 | O CMS importa `a11y.css` mas `cms.blade.php` **não** emite `data-a11y-*` nem tem a barra → camada a11y **inerte** no painel. | Nota: `risk_00j25aw6`. |

> Resultado: `✅ revisado` · `🔧 com correções` · `⏭️ delegado` (com justificativa).
