# Changelog — Otimização de Performance, CDN Vendorization e CSP (Julho/2026)

Registro completo das melhorias de performance, segurança e arquitetura aplicadas ao **Portal V2** (`layouts/portal/`), incluindo vendorização de CDN, correção de CSP, e otimização de carregamento.

```
Data:    24-26 Julho 2026
Escopo:  V2/layouts/portal/ (~45 blades)
         V2/Modules/*/Views/portal/*.blade.php (33 arquivos)
         public/assets/css/portal.css (novo)
         public/assets/js/portal-init.js (novo)
         public/assets/vendor/* (7 novos diretórios)
         docker/nginx/lotep.conf.template
         V2/Seeds/SleConfigsSeeder.php
         V2/Services/Gallery/Views/components/
         V2/Services/Media/Views/components/
```

---

## 1. Arquitetura CSS — portal.css

### 1.1. Design System Base

Criado `public/assets/css/portal.css` (17KB) com:

- **6 Cascade Layers**: `@layer reset, base, layout, components, utilities, print`
- **Componentes**: header navigation (`.lotep-nav`, `.lotep-dropdown`, `.lotep-submenu`), search button (`.header-search-btn`), accessibility widget (`.accessibility-widget`, `.accessibility-trigger`, `.accessibility-panel`), footer grid (`.footer-grid`), pagination, back-to-top, global banner
- **Utilitários**: `.accessibility-link`, `.icon-sm`, `.icon-xs`, `.fs-xs`, `.fs-smaller`, `.fw-800`, `.tracking-wider`, `.cursor-pointer`, `.hover-white`, `.animate-padding`, `.focus-none`
- **Print Layer**: completo com classes `.print-header`, `.print-footer`, `.print-header-inner`, `.print-header-logo-cell`, `.print-header-title`, `.print-header-subtitle`, `.print-footer-inner-row`, `.print-footer-info`, `.print-footer-brand`, `.print-footer-url`, `.print-footer-timestamp`, `.print-footer-qr-cell`

### 1.2. Inline Styles Migrados

~80 ocorrências de `style="..."` migradas para classes CSS em 28 blades do layout:

| Componente | Antes | Depois |
|------------|-------|--------|
| Header nav | 6 `font-size: 0.75rem` | `.icon-sm` |
| Header logo | 8 styles (flex, border-radius, font-weight, letter-spacing) | Classes BS5 + portal.css |
| Header search | 5 styles (position, border-radius, padding) | Classes BS5 |
| Header accessibility | 6 styles (font-size, letter-spacing, background) | `.accessibility-link` + `.fs-smaller` |
| Footer logos | 8 styles (flex, gap, margin) | `d-flex flex-wrap gap-3 mb-4` etc |
| Footer services | 4 styles (margin-bottom, padding, font-size) | `mb-1 p-0 icon-xs` |
| Footer public contact | 8 styles (margin-bottom, icons) | `mb-2 me-1` |
| Footer social network | 2 styles (flex, font-size) | `d-flex flex-wrap gap-3 fs-4` |
| Footer copyright | 1 style (color, margin) | `text-secondary` |
| Footer LAI | 2 styles (margin) | `mb-5` |
| Main sections | 8 styles (breadcrumb, headings) | `bg-transparent p-0 fw-semibold` etc |
| Error pages | 4 styles (margin-inline) | `mx-auto` |
| Search index | 3 styles (font-weight, padding) | `fw-800 fw-700 px-3 py-1` |
| Print header | 8 styles (flex, dimensions, fonts) | Classes `.print-header-*` |
| Print footer | 9 styles (flex, dimensions, fonts) | Classes `.print-footer-*` |

---

## 2. JavaScript — portal-init.js

### 2.1. Unificação de DOMContentLoaded

Criado `public/assets/js/portal-init.js` (25KB) consolidando **7 listeners DOMContentLoaded** em **1 único listener**:

| Origem | Função |
|--------|--------|
| `base.blade.php` | Print engine (beforeprint/afterprint) |
| `app-layout.blade.php` | Flatpickr initialization |
| `app-layout.blade.php` | Back-to-top button |
| `header-nav.blade.php` | Dropdown navigation toggle |
| `header-search-area.blade.php` | Search toggle |
| `global-banner.blade.php` | Banner close button |
| `main-footer.blade.php` | Back/print buttons |
| `accessibility/scripts.blade.php` | All accessibility features (11 funcionalidades) |

**Scripts removidos** dos blades individuais:
- `header-nav.blade.php` — removido `@push('scripts')` (175-251)
- `header-search-area.blade.php` — removido `@push('scripts')` (88-162)
- `app-layout.blade.php` — removidos 2 scripts inline DOMContentLoaded
- `global-banner.blade.php` — removido `@push('scripts')` (88-106)
- `main-footer.blade.php` — removido `@push('scripts')` (21-47)
- `base.blade.php` — removido script inline do print engine
- `header/accessibility/scripts.blade.php` — include removido do base.blade.php

### 2.2. Carregamento com `defer`

| Script | Antes | Depois |
|--------|-------|--------|
| jQuery 3.6.0 | Sync (bloqueante) | Sync (← voltou por causa dos plugins SLE) |
| Bootstrap 5 JS | Sync | `defer` |
| portal-init.js | — | `defer` (novo) |
| masks.js | Sync | `defer` |
| flatpickr.min.js | Sync | `defer` |
| flatpickr pt.js | Sync | `defer` |
| VLibras | Sync + 2 requests | Dinâmico via `window.load` + `defer` + SRI |

**Nota**: jQuery voltou a ser síncrono porque 9 plugins SLE (nice-select, meanmenu, counterup, waypoints, magnific-popup, viewport, swiper, wow, main.min.js) dependem de jQuery disponível no momento do parse do HTML. Bootstrap JS manteve `defer` por ser independente.

---

## 3. CDN Vendorization

### 3.1. Fontes Vendorizadas

| Fonte | Antes | Depois | Tamanho |
|-------|-------|--------|---------|
| **Nunito Sans** (portal) | `fonts.googleapis.com` + `fonts.gstatic.com` (2 reqs) | `assets/vendor/nunito-sans/` (16 woff2) | 1.7MB |
| **Inter** (CMS login/maintenance) | `fonts.googleapis.com` + `fonts.gstatic.com` (2 reqs) | `assets/vendor/inter/` (4 woff2) | 1.3MB |
| **DM Sans** (homepage) | `fonts.googleapis.com` (via @import) | `assets/vendor/dm-sans/` (19 ttf) | 1.7MB |
| **Prata** (homepage) | `fonts.googleapis.com` (via @import) | incluso no dm-sans | — |
| **Manrope** (homepage) | `fonts.googleapis.com` (via @import) | `assets/vendor/manrope/` (7 ttf) | 1.3MB |

**Total**: 5 famílias de fontes, ~6MB, **0 requisições externas**. Todas servidas com cache `public, immutable` e `expires 1y` pelo nginx.

### 3.2. Bibliotecas JS/CSS Vendorizadas

| Biblioteca | CDN | Local |
|-----------|-----|-------|
| **SortableJS 1.15.0** | `cdn.jsdelivr.net` | `assets/vendor/sortablejs/Sortable.min.js` (44KB) |
| **jQuery 3.7.1** (SLE) | `code.jquery.com` | `assets/vendor/jquery/jquery-3.7.1.min.js` (86KB) |
| **CropperJS 1.5.13** CSS | `cdnjs.cloudflare.com` | `assets/vendor/cropperjs/cropper.min.css` (3.8KB) |
| **CropperJS 1.5.13** JS | `cdnjs.cloudflare.com` | `assets/vendor/cropperjs/cropper.min.js` (37KB) |

### 3.3. Views Atualizadas

| Arquivo | Recurso CDN → Local |
|---------|---------------------|
| `V2/layouts/portal/base.blade.php` | Google Fonts Nunito Sans → local |
| `V2/layouts/cms/staticPages/login.blade.php` | Google Fonts Inter → local |
| `V2/layouts/cms/staticPages/maintenance.blade.php` | Google Fonts Inter → local |
| `V2/Modules/Support/SLE/Views/cms/components/asset-sidebar.blade.php` | jQuery CDN → local |
| `V2/Modules/Layouts/Homepage/Views/cms/index.blade.php` | SortableJS CDN → local |
| `V2/Services/Gallery/Views/components/logic/styles.blade.php` | CropperCSS CDN → local |
| `V2/Services/Gallery/Views/components/gallery-picker.blade.php` | CropperJS CDN → local |
| `V2/Services/Media/Views/components/media-group-input.blade.php` | CropperCSS + CropperJS CDN → local |
| `public/assets/sucesu/css/main.css` | 3× `@import` Google Fonts → local |
| `public/assets/sucesu/css/main.min.css` | 3× `@import` Google Fonts → local |

### 3.4. CDN Remanescentes (não vendorizáveis)

| Recurso | Motivo |
|---------|--------|
| `googletagmanager.com` (GTAG) | Analytics dinâmico por cliente |
| `youtube.com/embed/` + `img.youtube.com` | Conteúdo dinâmico de terceiros |
| `www.vlibras.gov.br` | Tradutor Libras oficial (já com SRI + defer) |
| Redes sociais (`facebook.com/sharer/`, `twitter.com/intent/`, `wa.me/`) | Links de compartilhamento, não resources |

---

## 4. Imagens — Lazy Loading

### 4.1. Layout Base

9 imagens corrigidas em `layouts/portal/`:

| Arquivo | Imagens | Correção |
|---------|---------|----------|
| `header-logo.blade.php` | 2 | `loading="eager"` (above-fold) + `loading="lazy"` (contraste) |
| `footer-logo.blade.php` | 5 | `loading="lazy"` + `width: auto` |
| `global-banner.blade.php` | 1 | `loading="lazy"` |
| `print-header.blade.php` | 1 | `loading="eager"` (print-only) |

### 4.2. Módulos Portal Views

33 imagens corrigidas em 32 arquivos de módulos:

| Grupo | Módulos | Imagens |
|-------|---------|---------|
| Institucional | Associados, AssocieSe, Diretoria, FaleConosco, História, UnidadeRegional | 10 |
| Layouts | BasicPage (homepage, acessibilidade), Homepage (sections) | 16 |
| Publicações | Agenda, Notícias, Parceiros, TrabalheConosco | 7 |

**Atributos adicionados**: `loading="lazy"/"eager"`, `decoding="async"`, `width`, `height`, `alt`

---

## 5. Acessibilidade

### 5.1. Navegação

- `aria-current="page"` adicionado ao link "Início" no `header-nav.blade.php`
- Breadcrumbs já possuíam `aria-current="page"`

### 5.2. ARIA Roles Corrigidos

| Arquivo | Antes | Depois |
|---------|-------|--------|
| `bar-left.blade.php` | `role="menubar"` + `role="menuitem"` (incorreto — sem implementação de teclado) | `<nav>` com links simples |
| `widget.blade.php` | `role="dialog" aria-modal="false"` (contraditório) | `role="region"` |

### 5.3. Tabelas

2 `<th>` sem `scope` corrigidos em `BasicPage/Views/portal/acessibilidade.blade.php`.

---

## 6. Segurança

### 6.1. Content Security Policy (CSP)

**Problema**: O placeholder `'nonce-{NONCE}'` era enviado literalmente ao browser, quebrando todo o CSP e bloqueando scripts.

**Solução** (`docker/nginx/lotep.conf.template`):

```
Antes:
  script-src 'self' 'nonce-{NONCE}' https://cdnjs.cloudflare.com;

Depois:
  script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com
            https://www.googletagmanager.com https://www.vlibras.gov.br;
  style-src  'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com;
  img-src    'self' data: blob: https://img.youtube.com;
  font-src   'self' data: https://fonts.gstatic.com;
  connect-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
  frame-src  'self' https://www.youtube.com;
```

**Nota**: `'unsafe-inline'` em `script-src` é necessário porque os blocos SLE são armazenados no banco de dados e renderizados como HTML inline — não há como usar nonce nesse fluxo.

### 6.2. SRI (Subresource Integrity)

- **VLibras**: hash SHA-384 real gerado para `vlibras-plugin.js`, carregamento dinâmico com `crossorigin="anonymous"`
- **GTAG**: adicionado `crossorigin="anonymous"` no script tag

### 6.3. Debug Leak

`V2/Modules/Layouts/BasicPage/Views/portal/runtime.blade.php` — função `log()` que criava div visível com debug info foi removida. Substituída por comentários HTML.

### 6.4. magnific-popup.css

Arquivo continha 5 variáveis SCSS não compiladas (`$white`) que causavam erros de parsing. Substituídas pelo valor literal `#fff`.

---

## 7. SleConfigsSeeder — Scripts SLE com `defer`

Os 9 scripts JS do tema SUCESU carregados via SLE (viewport, nice-select, waypoints, counterup, swiper, meanmenu, magnific-popup, wow, main.min.js) agora carregam com `defer`, já que jQuery está disponível sincronamente antes deles no HTML. Isso elimina bloqueio de renderização por esses scripts.

---

## 8. Resumo de Performance

| Métrica | Antes | Depois |
|---------|-------|--------|
| Requisições externas (fonts + CDN) | ~12 | ~4 (apenas GTAG + VLibras + YouTube) |
| Scripts bloqueantes | 7+ | 1 (jQuery — necessário) |
| CSS blocking render | 3+ | 3 (bootstrap, icons, portal.css) |
| Tamanho de CSS inline em blades | ~80 ocorrências | ~0 no layout (apenas módulos) |
| Listeners DOMContentLoaded | 7 | 1 (portal-init.js) |
| Imagens sem lazy loading (portal) | 9 | 0 |
| Imagens sem lazy loading (módulos) | ~36 | 0 |
| `<th>` sem `scope` | 2 | 0 |
| Debug leak | 1 (runtime.blade.php) | 0 |

---

## Arquivos Criados

```
public/assets/css/portal.css                    — Design system base (412 linhas, 17KB)
public/assets/js/portal-init.js                 — JS global consolidado (8 seções, 25KB)
public/assets/vendor/nunito-sans/               — Nunito Sans (16 woff2 + CSS)
public/assets/vendor/inter/                     — Inter (4 woff2 + CSS)
public/assets/vendor/dm-sans/                   — DM Sans (19 ttf + CSS)
public/assets/vendor/manrope/                   — Manrope (7 ttf + CSS)
public/assets/vendor/sortablejs/                — SortableJS (44KB)
public/assets/vendor/cropperjs/                 — CropperJS (41KB)
public/assets/vendor/jquery/jquery-3.7.1.min.js — jQuery 3.7.1 (86KB)
tests/Unit/V2/Layouts/Portal/PortalPerformanceTest.php  — 2 testes
```

## Arquivos Modificados (~60)

```
V2/layouts/portal/base.blade.php               — defer, jQuery sync, portal.css, portal-init.js, fontes locais
V2/layouts/portal/app-layout.blade.php          — defer scripts, CSS sync flatpickr
V2/layouts/portal/header/*.blade.php (6)        — inline → classes
V2/layouts/portal/header/accessibility/*.blade.php (7) — acessibilidade + inline → classes
V2/layouts/portal/footer/*.blade.php (8)        — inline → classes
V2/layouts/portal/main-section/*.blade.php (5)  — inline → classes
V2/layouts/portal/partials/*.blade.php (2)      — inline → classes
V2/layouts/portal/print/*.blade.php (3)         — inline → classes CSS
V2/layouts/portal/errors/*.blade.php (5)        — inline → classes
V2/layouts/portal/search/index.blade.php        — inline → classes
V2/layouts/portal/base-struct/vlibras.blade.php — SRI + defer dinâmico
V2/layouts/portal/base-struct/global-banner.blade.php — loading
V2/layouts/portal/metadata/gtag.blade.php       — crossorigin
V2/layouts/cms/staticPages/login.blade.php      — Inter local
V2/layouts/cms/staticPages/maintenance.blade.php — Inter local
V2/Modules/*/Views/portal/*.blade.php (33)      — loading, decoding, alt, width, height, scope
V2/Modules/Support/SLE/Views/cms/components/asset-sidebar.blade.php — jQuery local
V2/Modules/Layouts/Homepage/Views/cms/index.blade.php — SortableJS local
V2/Modules/Layouts/BasicPage/Views/portal/runtime.blade.php — debug leak fix
V2/Services/Gallery/Views/components/*.blade.php (2) — CropperJS local
V2/Services/Media/Views/components/media-group-input.blade.php — CropperJS local
docker/nginx/lotep.conf.template                — CSP fix + reload + cache homepage
docker/nginx/nginx.conf                         — gzip_types completos, FORCE_HTTPS removido
V2/Seeds/SleConfigsSeeder.php                   — defer nos scripts SLE + WOW.init fix
V2/Core/Middleware/HtmlMinify.php               — middleware de minificação HTML (novo)
app/Http/Kernel.php                             — HtmlMinify registrado globalmente
V2/layouts/portal/base-struct/fonts.blade.php   — {!! !!} para CSS válido no font-family
V2/layouts/cms/layouts/app.blade.php            — defer removido do jQuery ($ is not defined); + Inter font; JS applyFontScale fix; defer removido do Bootstrap JS (bootstrap is not defined)
public/assets/vendor/inter/inter.css            — corrigidos hashes dos arquivos .woff2 (404 nas fontes)
public/assets/vendor/nunito-sans/nunito-sans.css — corrigidos hashes dos 16 arquivos .woff2 (404 nas fontes)
V2/layouts/cms/layouts/footer.blade.php         — inline font-size removido
V2/layouts/cms/dashboard.blade.php              — 2 inline font-size removidos
V2/layouts/cms/partials/table.blade.php         — sort icon font-size tokenizado
V2/layouts/cms/search/index.blade.php           — inline font-size removido
public/assets/css/cms-layout.css                — design tokens, element mapping, densidade notebook; sidebar redesign: group headers sutis, ícones 0.875rem, chevron opacity, submenu 1.5rem, active 3px, current-group
V2/Modules/Publicacoes/Noticias/Views/cms/includes/scripts.blade.php — qs('input[name="cover_image_id"]') → qs('cover_image_id') (seletor inválido)
V2/Modules/Infraestrutura/CertificadoDigital/Views/cms/sign/partials/scripts.blade.php — guard contra form null (appendChild error)
V2/layouts/cms/layouts/app.blade.php            — handler duplicado de group header removido (delegado ao sidebar)
V2/layouts/cms/layouts/sidebar.blade.php        — redesign: "NAVEGAÇÃO PRINCIPAL" removido, "Visão Geral" removido, "Ver Todos" removido; + sidebar search; accordion mode; current-group; title nos links; JS substituído
tests/Unit/V2/Layouts/Cms/CmsSidebarTest.php    — novo (16 testes, 25 asserções)
public/assets/sucesu/css/main.css               — @import local
public/assets/sucesu/css/main.min.css           — @import local
public/assets/sucesu/css/magnific-popup.css     — SCSS → CSS
```

---

## 9. Middleware de Minificação HTML

### 9.1. HtmlMinify

Criado `V2/Core/Middleware/HtmlMinify.php` e registrado como middleware global em `app/Http/Kernel.php`.

**Funcionamento**:
1. Extrai conteúdo sensível (`<pre>`, `<code>`, `<textarea>`, `<script>`, `<style>`, `<svg>`) para placeholders seguros (`###HTMLMINIFY_PH_N###`)
2. Remove linhas em branco
3. Remove indentação (`trim()` por linha)
4. Colapsa múltiplos espaços para um único espaço
5. Remove espaços entre tags (`> <` → `><`)
6. Restaura o conteúdo original dos placeholders

**Segurança**: Só processa responses `Content-Type: text/html`. Preserva conteúdo de tags sensíveis intacto.

**Resultado**:

| Métrica | Antes | Depois |
|---------|-------|--------|
| HTML bruto (homepage) | 70.876 bytes | 49.597 bytes |
| Redução | — | **30%** |
| Linhas | 1.585 | 824 |
| Linhas em branco | 194 | **0** |

---

## 10. Cache FastCGI para Homepage

### 10.1. Configuração

Adicionado cache FastCGI no `lotep.conf.template` para a homepage (`GET /`):

```nginx
map $http_cookie $no_cache {
    default 0;
    ~*laravel_session 1;
    ~*XSRF-TOKEN 1;
}

location ~ \.php$ {
    fastcgi_cache SELO_CACHE;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    fastcgi_cache_valid 200 60s;
    fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
    fastcgi_cache_bypass $no_cache;
    fastcgi_no_cache $no_cache;
    add_header X-Cache-Status $upstream_cache_status;
}
```

### 10.2. Comportamento

| Cenário | Cache | Motivo |
|---------|-------|--------|
| Primeiro acesso (sem cookie) | `MISS` | Cacheia a resposta por 60s |
| Segundo acesso (sem cookie) | `HIT` | Servido do cache |
| Acesso com `laravel_session` | `BYPASS` | Usuário precisa de CSRF fresco |
| Após 60s | `MISS` | Cache expirou, refaz |

**Zona**: `SELO_CACHE` (10MB, reutilizada do endpoint SELO)

---

## 11. Correções de Bugs

### 11.1. Placeholder no HtmlMinify

`\x00` (null byte) era removido por `trim()` durante o processamento, fazendo os placeholders vazarem para o HTML final. Corrigido substituindo `\x00...\x00` por `###...###`.

### 11.2. CSS font-family com HTML entities

`V2/layouts/portal/base-struct/fonts.blade.php` usava `{{ }}` (Blade escaped) para renderizar `font-family`. Dentro de `<style>`, HTML entities como `&#039;` NÃO são interpretados como aspas, gerando erro de parsing CSS:

```
Expected declaration but found 'Sans'. Skipped to next declaration.
```

Corrigido: `{{ }}` → `{!! !!}` (Blade unescaped).

### 11.3. nginx $FORCE_HTTPS

Variável `$FORCE_HTTPS` era usada no `fastcgi_param` mas nunca definida como variável nginx — causava erro de config. Removida (a informação está em `HTTP_X_FORWARDED_PROTO $scheme`).

### 11.4. nginx gzip_types duplicado

`text/html` listado em `gzip_types` mas o nginx já o inclui por padrão — causava warning. Removido.

### 11.5. CMS: `$ is not defined` no Structural Editor

`V2/layouts/cms/layouts/app.blade.php` carregava jQuery com `defer` (linha 103). Os scripts inline injetados via `@stack('scripts')` (galeria, `media-input`, `code-editor`) usam `$()` e executam **durante o parse do HTML** — antes do jQuery `defer` ser executado.

| Contexto | Ocorrência |
|----------|-----------|
| `defer` | Baixa em paralelo, executa **após** o parse completo |
| Script inline (`@stack`) | Executa **imediatamente** durante o parse |

**Sintomas**: `Uncaught ReferenceError: $ is not defined` em todas as páginas CMS que usam `@stack('scripts')` com jQuery (Structural Editor, Editor de Páginas, etc.).

**Corrigido**: Removido `defer` do `<script src="jquery-3.6.0.min.js">`. jQuery (87KB minificado) já está no final do `<body>`, então o impacto no parse é desprezível.

---

## 12. nginx Config

### Arquivos Modificados

| Arquivo | Mudanças |
|---------|----------|
| `docker/nginx/lotep.conf.template` | + `map $no_cache`, + `fastcgi_cache` no PHP location, + `fastcgi_ignore_headers`, CSP fix, `$FORCE_HTTPS` removido |
| `docker/nginx/nginx.conf` | + `gzip_types` completos (text/html, woff2, webp, avif, etc), `$FORCE_HTTPS` removido |

### Testes

```
Nginx: syntax OK
Cache: MISS → HIT → HIT → BYPASS (com cookie)
```

## Métricas Consolidadas

| Métrica | Antes | Depois |
|---------|-------|--------|
| Requisições externas (fonts + CDN) | ~12 | ~4 |
| Scripts bloqueantes | 7+ | 1 (jQuery) |
| CSS blocking render | 3+ | 3 |
| Listeners DOMContentLoaded | 7 | 1 |
| Imagens sem lazy loading (portal) | 9 | 0 |
| Imagens sem lazy loading (módulos) | ~36 | 0 |
| `<th>` sem `scope` | 2 | 0 |
| Debug leak | 1 | 0 |
| HTML bruto (homepage) | 70.876 bytes | 49.597 bytes (-30%) |
| Gzip (homepage) | 12.557 bytes | 11.353 bytes (-10%) |
| Linhas em branco (homepage) | 194 | 0 |
| Cache homepage | nenhum | HIT em 60s |
| Erros de console funcionais (portal) | vários | **0** |
| Erros de console funcionais (CMS) | `$ is not defined` ×N | **0** |

## Testes

```
Tests: 253 (+2 novos), Assertions: 591 (+7)
Errors: 2 (pré-existentes, GestaoPermissoes)
Nginx: syntax OK
```

---

## 13. CMS Typography System

### Design Tokens Tipográficos

Implementado sistema de tokens CSS tipográficos no `cms-layout.css` com escala modular Minor Third (1.200):

| Token | Valor | Uso |
|-------|-------|-----|
| `--text-xs` | 0.785rem | th, badges, breadcrumbs |
| `--text-sm` | 0.875rem | small, sidebar, footer |
| `--text-base` | 1rem (14px) | Corpo, inputs, botões |
| `--text-md` | 1.15rem | h6 |
| `--text-lg` | 1.35rem | h5 (card titles) |
| `--text-xl` | 1.6rem | h4 (section titles) |
| `--text-2xl` | 1.9rem | h3 (page header) |
| `--text-3xl` | 2.3rem | h1, h2 (KPIs) |

### Fonte Inter

- Fonte **Inter** (400/500/600/700) carregada localmente via WOFF2 com `font-display: swap`
- Preload das 4 variações no `<head>` para evitar FOIT
- `inter.css` adicionado ao layout CMS

### Acessibilidade

- `applyFontScale` corrigido: agora usa `(14 * scale) + 'px'` em vez de `(scale * 100) + '%'`
- Range 0.8×–1.4×, persistente via `localStorage`

### Arquivos Modificados

| Arquivo | Mudança |
|---------|---------|
| `public/assets/css/cms-layout.css` | 8 tokens, element mapping, ~12 hardcoded font-size → tokens |
| `V2/layouts/cms/layouts/app.blade.php` | Inter preload + CSS, JS fix |
| `V2/layouts/cms/layouts/footer.blade.php` | inline style removido |
| `V2/layouts/cms/dashboard.blade.php` | 2 inline font-size removidos |
| `V2/layouts/cms/partials/table.blade.php` | sort icon font-size tokenizado |
| `V2/layouts/cms/search/index.blade.php` | inline font-size removido |
| `tests/Unit/V2/Layouts/Cms/CmsTypographyTest.php` | **Novo** — 11 testes |

### Métricas

| Métrica | Antes | Depois |
|---------|-------|--------|
| Hardcoded `font-size` no CSS CMS | ~14 | **0** |
| Inline `font-size` nas views CMS | 6 | 3 (ícones decorativos intencionais) |
| Fonte carregada | system-ui (fallback) | **Inter WOFF2 local** |
| `applyFontScale` base | 16px (%) | **14px (px)** |
| Testes de tipografia | 0 | **11** |

### Documentação

`wiki/v2/cms-typography-system.md` — guia completo do sistema tipográfico do CMS.## Testes

```
Tests: 264 (+11 novos), Assertions: 636 (+45)
Errors: 2 (pré-existentes, GestaoPermissoes)
CmsTypography: 14 tests, 57 assertions — OK
```
