# Dashboard Luz Componentização — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Disponibilizar 8 novos componentes do "padrão luz" (7 visuais + 1 helper de tokens) sob `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/`, prontos para adoção futura, sem alterar `dashboard.php` nem `dashboard.js`.

**Architecture:** Cada componente é um arquivo PHP autocontido em `html/luz/<categoria>/`, recebe um array nomeado e emite HTML via `<?= ?>`. Paleta de tons centralizada em `internos/luz_tokens.php` (retorna array PHP). Composição: `luz_destaque_pessoa` e `luz_tabela_paginada` reusam `luz_card_painel`; `luz_card_painel` reusa `luz_chip_titulo`. Estilos permanecem inline (sem CSS externo). O único arquivo existente alterado é `luz_card_kpi.php`, que passa a consumir `luz_tokens` e `luz_chip_titulo` mantendo o HTML de saída idêntico.

**Tech Stack:** PHP 7+ (sem dependências), Bootstrap Icons (já em uso), padrão Genesis (`require __DIR__ . '...'`), helper `show()` para i18n.

**Validação:** Cada task inclui um "smoke render" — script PHP CLI que inclui o componente com dados de exemplo, captura o output via `ob_start()`, e verifica substrings esperadas. Não há framework de teste de view no Genesis; isso é o equivalente pragmático.

**Convenção de commits:** Cada task termina com commit local. Push só após confirmação humana — não está incluído no plano.

---

### Task 1: Criar diretórios e baseline

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/.gitkeep`
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/listas/.gitkeep`
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/.gitkeep`
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/.gitkeep`

- [ ] **Step 1: Criar diretórios**

```bash
cd /home/goldie/www/genesis
mkdir -p componentes/Missoes/Logistica/sinais/Dashboard/html/luz/{cards,listas,layout,internos}
touch componentes/Missoes/Logistica/sinais/Dashboard/html/luz/{cards,listas,layout,internos}/.gitkeep
```

- [ ] **Step 2: Verificar estrutura**

Run:
```bash
ls -la componentes/Missoes/Logistica/sinais/Dashboard/html/luz/
```
Expected: lista contendo `cards/`, `listas/`, `layout/`, `internos/`, além dos 3 arquivos `luz_*.php` existentes.

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/
git commit -m "chore(dashboard-luz): scaffold subdirs cards/listas/layout/internos"
```

---

### Task 2: `luz_tokens.php` (helper de paleta)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_tokens.php`

- [ ] **Step 1: Smoke test (rodar antes — deve falhar)**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
$tons = require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_tokens.php";
foreach (["neutral","info","success","warning","danger"] as $t) {
    if (!isset($tons[$t]["borda"])) { echo "FAIL: tom $t sem borda\n"; exit(1); }
    if (!isset($tons[$t]["valor"])) { echo "FAIL: tom $t sem valor\n"; exit(1); }
    if (!isset($tons[$t]["chipFundo"])) { echo "FAIL: tom $t sem chipFundo\n"; exit(1); }
    if (!isset($tons[$t]["chipIcone"])) { echo "FAIL: tom $t sem chipIcone\n"; exit(1); }
    if (!isset($tons[$t]["chipRing"])) { echo "FAIL: tom $t sem chipRing\n"; exit(1); }
    if (!isset($tons[$t]["spark"])) { echo "FAIL: tom $t sem spark\n"; exit(1); }
}
echo "PASS\n";
'
```
Expected: erro "Failed opening required" — o arquivo ainda não existe.

- [ ] **Step 2: Implementar**

Write `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_tokens.php`:

```php
<?php

/**
 * Luz Tokens
 * Paleta de tons reusada por todos os componentes luz_* do Dashboard.
 * Valores extraídos 1:1 da $paletaTons original em luz_card_kpi.php.
 *
 * Uso:
 *   $tons  = require __DIR__ . '/luz_tokens.php';
 *   $cores = $tons[$tomEscolhido] ?? $tons['neutral'];
 */

return [
    'neutral' => ['valor' => '#e2e8f0', 'borda' => 'rgba(148, 163, 184, 0.25)', 'chipFundo' => 'rgba(148, 163, 184, 0.08)', 'chipIcone' => '#94a3b8', 'chipRing' => 'rgba(148, 163, 184, 0.30)', 'spark' => '#94a3b8'],
    'success' => ['valor' => '#4ade80', 'borda' => 'rgba(34, 197, 94, 0.40)',  'chipFundo' => 'rgba(34, 197, 94, 0.10)',  'chipIcone' => '#4ade80', 'chipRing' => 'rgba(34, 197, 94, 0.35)',  'spark' => '#4ade80'],
    'warning' => ['valor' => '#fbbf24', 'borda' => 'rgba(245, 158, 11, 0.40)', 'chipFundo' => 'rgba(245, 158, 11, 0.10)', 'chipIcone' => '#fbbf24', 'chipRing' => 'rgba(245, 158, 11, 0.35)', 'spark' => '#fbbf24'],
    'danger'  => ['valor' => '#f87171', 'borda' => 'rgba(239, 68, 68, 0.40)',  'chipFundo' => 'rgba(239, 68, 68, 0.10)',  'chipIcone' => '#f87171', 'chipRing' => 'rgba(239, 68, 68, 0.35)',  'spark' => '#f87171'],
    'info'    => ['valor' => '#60a5fa', 'borda' => 'rgba(59, 130, 246, 0.40)', 'chipFundo' => 'rgba(59, 130, 246, 0.10)', 'chipIcone' => '#60a5fa', 'chipRing' => 'rgba(59, 130, 246, 0.35)', 'spark' => '#22d3ee'],
];
```

- [ ] **Step 3: Rerodar smoke test**

Mesmo comando do Step 1.
Expected: `PASS`.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_tokens.php
git commit -m "feat(dashboard-luz): add luz_tokens.php with shared palette"
```

---

### Task 3: `luz_chip_titulo.php` (chip de ícone + título)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_chip_titulo.php`

- [ ] **Step 1: Smoke test (deve falhar)**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
$chip = ["titulo" => "Fluxo Geral", "icone" => "bi-graph-up", "tom" => "info", "tamanho" => "md"];
ob_start();
require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_chip_titulo.php";
$html = ob_get_clean();
$checks = ["Fluxo Geral", "bi-graph-up", "rgba(59, 130, 246", "2.25rem"];
foreach ($checks as $c) {
    if (strpos($html, $c) === false) { echo "FAIL: missing $c\n"; exit(1); }
}
echo "PASS\n";
'
```
Expected: `Failed opening required`.

- [ ] **Step 2: Implementar**

Write `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_chip_titulo.php`:

```php
<?php

/**
 * Luz Component: ChipTitulo
 * Building block: chip circular (ícone tonalizado) + título uppercase ao lado.
 *
 * Recebe: $chip (array)
 *   - titulo       string  (obrigatório)
 *   - icone        string  classe bootstrap-icons (default 'bi-bar-chart')
 *   - tom          string  info|success|warning|danger|neutral (default 'neutral')
 *   - tamanho      string  'sm' (chip 2rem) | 'md' (chip 2.25rem) (default 'md')
 *   - tituloEscala float   multiplicador no font-size do título (default 1.0)
 */
$chip          = $chip ?? [];
$tituloChip    = $chip['titulo']       ?? '';
$iconeChip     = $chip['icone']        ?? 'bi-bar-chart';
$tomChip       = $chip['tom']          ?? 'neutral';
$tamanhoChip   = $chip['tamanho']      ?? 'md';
$escalaChip    = (float) ($chip['tituloEscala'] ?? 1.0);

$tons          = require __DIR__ . '/luz_tokens.php';
$coresChip     = $tons[$tomChip] ?? $tons['neutral'];

$dimensaoChip  = $tamanhoChip === 'sm' ? '2rem'    : '2.25rem';
$iconeFontChip = $tamanhoChip === 'sm' ? '0.9rem'  : '1.1rem';
$tituloFontChip= round((($tamanhoChip === 'sm' ? 0.75 : 0.98)) * $escalaChip, 3) . 'rem';
?>
<div style="display: flex; align-items: center; gap: 0.6rem;">
    <div style="display: inline-flex; align-items: center; justify-content: center;
                width: <?= $dimensaoChip ?>; height: <?= $dimensaoChip ?>;
                border-radius: 9999px;
                background: <?= $coresChip['chipFundo'] ?>;
                box-shadow: inset 0 0 0 1px <?= $coresChip['chipRing'] ?>;">
        <i class="<?= htmlspecialchars($iconeChip) ?>"
           style="font-size: <?= $iconeFontChip ?>; color: <?= $coresChip['chipIcone'] ?>;"></i>
    </div>
    <div style="font-size: <?= $tituloFontChip ?>; font-weight: 700; color: #e2e8f0;
                text-transform: uppercase; letter-spacing: 0.08em;">
        <?= htmlspecialchars($tituloChip) ?>
    </div>
</div>
```

- [ ] **Step 3: Rerodar smoke test**

Mesmo comando do Step 1.
Expected: `PASS`.

- [ ] **Step 4: Smoke test extra — variante `sm`**

Run:
```bash
php -r '
$chip = ["titulo" => "X", "tom" => "danger", "tamanho" => "sm"];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_chip_titulo.php"; $html = ob_get_clean();
if (strpos($html, "2rem") === false) { echo "FAIL: tamanho sm\n"; exit(1); }
if (strpos($html, "rgba(239, 68, 68") === false) { echo "FAIL: tom danger\n"; exit(1); }
echo "PASS\n";'
```
Expected: `PASS`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_chip_titulo.php
git commit -m "feat(dashboard-luz): add luz_chip_titulo internal building block"
```

---

### Task 4: `luz_skeleton.php` (placeholder de loading)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_skeleton.php`

- [ ] **Step 1: Smoke test (deve falhar)**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
$skeleton = ["altura" => "120px"];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_skeleton.php"; $html = ob_get_clean();
if (strpos($html, "dash-skeleton") === false) { echo "FAIL: classe\n"; exit(1); }
if (strpos($html, "height:120px") === false && strpos($html, "height: 120px") === false) { echo "FAIL: altura\n"; exit(1); }
echo "PASS\n";'
```
Expected: `Failed opening required`.

- [ ] **Step 2: Implementar**

Write `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_skeleton.php`:

```php
<?php

/**
 * Luz Component: Skeleton
 * Placeholder de loading (depende da classe .dash-skeleton já definida no
 * <style> do dashboard.php).
 *
 * Recebe: $skeleton (array)
 *   - altura   string CSS (obrigatório, ex: '80px', '12rem')
 *   - largura  string CSS (default '100%')
 *   - classe   string classes adicionais (default '')
 */
$skeleton        = $skeleton ?? [];
$alturaSkeleton  = $skeleton['altura']  ?? '60px';
$larguraSkeleton = $skeleton['largura'] ?? '100%';
$classeSkeleton  = $skeleton['classe']  ?? '';

$classesSkeleton = trim('dash-skeleton ' . $classeSkeleton);
?>
<div class="<?= htmlspecialchars($classesSkeleton) ?>"
     style="height: <?= htmlspecialchars($alturaSkeleton) ?>;
            width: <?= htmlspecialchars($larguraSkeleton) ?>;"></div>
```

- [ ] **Step 3: Rerodar smoke test**

Mesmo comando do Step 1.
Expected: `PASS`.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/internos/luz_skeleton.php
git commit -m "feat(dashboard-luz): add luz_skeleton internal placeholder"
```

---

### Task 5: `luz_card_painel.php` (wrapper genérico — o grande)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_card_painel.php`

- [ ] **Step 1: Smoke test (deve falhar)**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
$painel = [
    "titulo" => "Fluxo: Separações vs Carregamentos",
    "icone"  => "bi-graph-up-arrow",
    "tom"    => "info",
    "corpo"  => "<div id=\"chart-fluxo\" style=\"min-height:280px;\"></div>",
];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_card_painel.php"; $html = ob_get_clean();
$checks = [
    "Fluxo: Separações vs Carregamentos",
    "bi-graph-up-arrow",
    "rgba(59, 130, 246",
    "linear-gradient(145deg, rgba(15,12,41",
    "chart-fluxo",
    "data-tom=\"info\"",
];
foreach ($checks as $c) {
    if (strpos($html, $c) === false) { echo "FAIL: missing $c\n"; exit(1); }
}
echo "PASS\n";'
```
Expected: `Failed opening required`.

- [ ] **Step 2: Implementar**

Write `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_card_painel.php`:

```php
<?php

/**
 * Luz Component: CardPainel
 * Wrapper genérico de painel — gradient navy + borda colorida por tom +
 * cabeçalho (chip de ícone + título + slot opcional à direita) + corpo
 * arbitrário (HTML cru).
 *
 * Recebe: $painel (array)
 *   - titulo        string (obrigatório)
 *   - icone         string classe bootstrap-icons (default 'bi-bar-chart')
 *   - tom           string info|success|warning|danger|neutral (default 'info')
 *   - corpo         string HTML cru renderizado dentro do painel
 *   - headerExtra   string HTML cru à direita do título (paginação, badge etc)
 *   - gridColumn    string ex.: 'span 2', '1 / 7'
 *   - gridRow       string ex.: '2 / 3'
 *   - paddingY      string padding vertical (default '0.875rem')
 *   - gap           string flex gap (default '0.6rem')
 *   - marginBottom  string ex.: '1rem' (default '')
 *   - classe        string classes extras no root
 *   - id            string id no root
 */
$painel             = $painel ?? [];
$tituloPainel       = $painel['titulo']       ?? '';
$iconePainel        = $painel['icone']        ?? 'bi-bar-chart';
$tomPainel          = $painel['tom']          ?? 'info';
$corpoPainel        = $painel['corpo']        ?? '';
$headerExtraPainel  = $painel['headerExtra']  ?? '';
$gridColumnPainel   = $painel['gridColumn']   ?? '';
$gridRowPainel      = $painel['gridRow']      ?? '';
$paddingYPainel     = $painel['paddingY']     ?? '0.875rem';
$gapPainel          = $painel['gap']          ?? '0.6rem';
$marginBottomPainel = $painel['marginBottom'] ?? '';
$classePainel       = $painel['classe']       ?? '';
$idPainel           = $painel['id']           ?? '';

$tons               = require __DIR__ . '/../internos/luz_tokens.php';
$coresPainel        = $tons[$tomPainel] ?? $tons['info'];

$classesRootPainel  = trim('g-card-kpi ' . $classePainel);

$estiloRootPainel   = 'border-radius: 0.75rem;'
                    . ' padding: ' . $paddingYPainel . ' 1rem;'
                    . ' display: flex; flex-direction: column;'
                    . ' gap: ' . $gapPainel . ';'
                    . ' background: linear-gradient(145deg, rgba(15,12,41,0.95) 0%, rgba(20,18,50,0.9) 100%);'
                    . ' border: 1px solid ' . $coresPainel['borda'] . ';'
                    . ' transition: box-shadow 0.3s, transform 0.2s;'
                    . ' box-shadow: 0 4px 15px rgba(0,0,0,0.3);';
if ($gridColumnPainel   !== '') $estiloRootPainel .= ' grid-column: ' . $gridColumnPainel . ';';
if ($gridRowPainel      !== '') $estiloRootPainel .= ' grid-row: ' . $gridRowPainel . ';';
if ($marginBottomPainel !== '') $estiloRootPainel .= ' margin-bottom: ' . $marginBottomPainel . ';';
?>
<div class="<?= htmlspecialchars($classesRootPainel) ?>"
     <?= $idPainel !== '' ? 'id="' . htmlspecialchars($idPainel) . '"' : '' ?>
     data-tom="<?= htmlspecialchars($tomPainel) ?>"
     style="<?= $estiloRootPainel ?>"
     onmouseover="this.style.boxShadow='0 22px 45px -12px rgba(0,0,0,0.55), 0 10px 20px -6px rgba(0,0,0,0.35)'"
     onmouseout="this.style.boxShadow='0 4px 15px rgba(0,0,0,0.3)'">

    <div style="display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;">
        <?php
        $chip = [
            'titulo'  => $tituloPainel,
            'icone'   => $iconePainel,
            'tom'     => $tomPainel,
            'tamanho' => 'md',
        ];
        require __DIR__ . '/../internos/luz_chip_titulo.php';
        ?>
        <?php if ($headerExtraPainel !== ''): ?>
            <div><?= $headerExtraPainel ?></div>
        <?php endif; ?>
    </div>

    <?= $corpoPainel ?>
</div>
```

- [ ] **Step 3: Rerodar smoke test principal**

Mesmo comando do Step 1.
Expected: `PASS`.

- [ ] **Step 4: Smoke test variantes (tom danger + headerExtra + gridColumn)**

Run:
```bash
php -r '
$painel = [
    "titulo"      => "Erros",
    "icone"       => "bi-bug",
    "tom"         => "danger",
    "corpo"       => "<p>conteudo</p>",
    "headerExtra" => "<button id=\"pag\">Próx</button>",
    "gridColumn"  => "1 / 7",
    "gridRow"     => "2 / 3",
];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_card_painel.php"; $html = ob_get_clean();
foreach (["rgba(239, 68, 68", "id=\"pag\"", "grid-column: 1 / 7", "grid-row: 2 / 3", "<p>conteudo</p>"] as $c) {
    if (strpos($html, $c) === false) { echo "FAIL: $c\n"; exit(1); }
}
echo "PASS\n";'
```
Expected: `PASS`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_card_painel.php
git commit -m "feat(dashboard-luz): add luz_card_painel generic wrapper"
```

---

### Task 6: `luz_aba_section.php` (wrapper de aba)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_aba_section.php`

- [ ] **Step 1: Smoke test (deve falhar)**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
$aba = ["id" => "aba-visao-geral", "chave" => "visao-geral", "ativa" => true, "corpo" => "<p>X</p>"];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_aba_section.php"; $html = ob_get_clean();
foreach (["id=\"aba-visao-geral\"", "data-aba=\"visao-geral\"", "data-loaded=\"false\"", "ativa", "<p>X</p>"] as $c) {
    if (strpos($html, $c) === false) { echo "FAIL: $c\n"; exit(1); }
}
echo "PASS\n";'
```
Expected: `Failed opening required`.

- [ ] **Step 2: Implementar**

Write `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_aba_section.php`:

```php
<?php

/**
 * Luz Component: AbaSection
 * Wrapper <section data-aba> com classe condicional 'ativa' e data-loaded.
 *
 * Recebe: $aba (array)
 *   - id     string (obrigatório) — vira id="..."
 *   - chave  string (obrigatório) — vira data-aba="..."
 *   - ativa  bool   adiciona ' ativa' à classe (default false)
 *   - corpo  string HTML cru de toda a aba (default '')
 */
$aba         = $aba ?? [];
$idAba       = $aba['id']    ?? '';
$chaveAba    = $aba['chave'] ?? '';
$ativaAba    = !empty($aba['ativa']);
$corpoAba    = $aba['corpo'] ?? '';

$classeAba   = 'dash-aba-section' . ($ativaAba ? ' ativa' : '');
?>
<section class="<?= $classeAba ?>"
         id="<?= htmlspecialchars($idAba) ?>"
         data-aba="<?= htmlspecialchars($chaveAba) ?>"
         data-loaded="false">
    <?= $corpoAba ?>
</section>
```

- [ ] **Step 3: Rerodar smoke test**

Mesmo comando do Step 1.
Expected: `PASS`.

- [ ] **Step 4: Smoke test variante inativa**

Run:
```bash
php -r '
$aba = ["id" => "aba-x", "chave" => "x", "corpo" => ""];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_aba_section.php"; $html = ob_get_clean();
if (strpos($html, "ativa") !== false) { echo "FAIL: ativa indevido\n"; exit(1); }
if (strpos($html, "data-loaded=\"false\"") === false) { echo "FAIL: data-loaded\n"; exit(1); }
echo "PASS\n";'
```
Expected: `PASS`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_aba_section.php
git commit -m "feat(dashboard-luz): add luz_aba_section layout wrapper"
```

---

### Task 7: `luz_grade_kpis.php` (grade dos 6 KPIs)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_grade_kpis.php`

- [ ] **Step 1: Smoke test (deve falhar)**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
$grade = [
    "kpis" => [
        ["titulo" => "Separações", "icone" => "bi-box-seam", "tom" => "info", "chave" => "separacoes", "hint" => "rotas"],
        ["titulo" => "Carregamentos", "icone" => "bi-truck", "tom" => "info", "chave" => "carregamentos", "hint" => "rotas"],
    ],
    "escalaPadrao" => 1.3,
    "tituloEscalaPadrao" => 1.15,
];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_grade_kpis.php"; $html = ob_get_clean();
if (strpos($html, "dash-kpi-grade") === false) { echo "FAIL: grade wrapper\n"; exit(1); }
if (strpos($html, "data-kpi=\"separacoes\"") === false) { echo "FAIL: kpi 1\n"; exit(1); }
if (strpos($html, "data-kpi=\"carregamentos\"") === false) { echo "FAIL: kpi 2\n"; exit(1); }
echo "PASS\n";'
```
Expected: `Failed opening required`.

- [ ] **Step 2: Implementar**

Write `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_grade_kpis.php`:

```php
<?php

/**
 * Luz Component: GradeKpis
 * Renderiza a grade <div class="dash-kpi-grade"> contendo N cards
 * (delega cada card para luz_card_kpi.php).
 *
 * Recebe: $grade (array)
 *   - kpis                array de arrays no formato do $kpi do luz_card_kpi
 *   - escalaPadrao        float aplicado em cada kpi se não vier explicitado
 *                         (default 1.0)
 *   - tituloEscalaPadrao  float idem para tituloEscala (default 1.0)
 */
$grade               = $grade ?? [];
$kpisGrade           = $grade['kpis']               ?? [];
$escalaPadraoGrade   = (float) ($grade['escalaPadrao']       ?? 1.0);
$tituloEscalaPadraoGrade = (float) ($grade['tituloEscalaPadrao'] ?? 1.0);
?>
<div class="dash-kpi-grade">
    <?php foreach ($kpisGrade as $kpiGrade): ?>
        <?php
        $kpi = $kpiGrade;
        if (!isset($kpi['escala']))       $kpi['escala']       = $escalaPadraoGrade;
        if (!isset($kpi['tituloEscala'])) $kpi['tituloEscala'] = $tituloEscalaPadraoGrade;
        require __DIR__ . '/../luz_card_kpi.php';
        ?>
    <?php endforeach; ?>
</div>
```

- [ ] **Step 3: Rerodar smoke test**

Mesmo comando do Step 1.
Expected: `PASS`.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/layout/luz_grade_kpis.php
git commit -m "feat(dashboard-luz): add luz_grade_kpis layout"
```

---

### Task 8: `luz_tabela_paginada.php`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/listas/luz_tabela_paginada.php`

- [ ] **Step 1: Smoke test (deve falhar)**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
function show($s) { return $s; }
$tabela = [
    "titulo"        => "Ranking Geral de Separadores",
    "icone"         => "bi-list-ol",
    "tom"           => "info",
    "idTabela"      => "ranking-separadores-tabela",
    "idCorpo"       => "ranking-separadores-body",
    "idPaginacao"   => "ranking-sep-paginacao",
    "colunas"       => ["Pos.", "Nome", "Score"],
    "alturaSkeleton"=> "120px",
    "gridColumn"    => "1 / 7",
    "gridRow"       => "2 / 3",
];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/listas/luz_tabela_paginada.php"; $html = ob_get_clean();
$checks = [
    "Ranking Geral de Separadores",
    "bi-list-ol",
    "id=\"ranking-separadores-tabela\"",
    "id=\"ranking-separadores-body\"",
    "id=\"ranking-sep-paginacao\"",
    "<th>Pos.</th>",
    "<th>Nome</th>",
    "<th>Score</th>",
    "colspan=\"3\"",
    "dash-skeleton",
    "grid-column: 1 / 7",
];
foreach ($checks as $c) { if (strpos($html, $c) === false) { echo "FAIL: $c\n"; exit(1); } }
echo "PASS\n";'
```
Expected: `Failed opening required`.

- [ ] **Step 2: Implementar**

Write `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/listas/luz_tabela_paginada.php`:

```php
<?php

/**
 * Luz Component: TabelaPaginada
 * Painel com header (chip+título à esquerda, paginação à direita) + tabela
 * <table class="dash-table"> com tbody vazio (skeleton inicial; JS popula).
 *
 * Compõe luz_card_painel (passa o cabeçalho via headerExtra e o conteúdo
 * via corpo).
 *
 * Recebe: $tabela (array)
 *   - titulo          string (obrigatório)
 *   - icone           string classe bootstrap-icons (default 'bi-list-ol')
 *   - tom             string info|success|warning|danger (default 'info')
 *   - idTabela        string id da <table>
 *   - idCorpo         string id do <tbody>
 *   - idPaginacao     string id do <div class="dash-paginacao">
 *   - colunas         array de strings (passadas por show())
 *   - alturaSkeleton  string CSS, default '120px'
 *   - gridColumn      string opcional
 *   - gridRow         string opcional
 *
 * Requer que a função show() (i18n do Genesis) esteja disponível no escopo.
 */
$tabela             = $tabela ?? [];
$tituloTabela       = $tabela['titulo']         ?? '';
$iconeTabela        = $tabela['icone']          ?? 'bi-list-ol';
$tomTabela          = $tabela['tom']            ?? 'info';
$idTabelaTabela     = $tabela['idTabela']       ?? '';
$idCorpoTabela      = $tabela['idCorpo']        ?? '';
$idPaginacaoTabela  = $tabela['idPaginacao']    ?? '';
$colunasTabela      = $tabela['colunas']        ?? [];
$alturaSkelTabela   = $tabela['alturaSkeleton'] ?? '120px';
$gridColumnTabela   = $tabela['gridColumn']     ?? '';
$gridRowTabela      = $tabela['gridRow']        ?? '';

$totalColsTabela    = max(count($colunasTabela), 1);

// Header extra (paginação à direita)
ob_start();
?>
<div class="dash-paginacao" id="<?= htmlspecialchars($idPaginacaoTabela) ?>"
     style="margin: 0; font-size: 0.9rem;"></div>
<?php
$headerExtraTabela = ob_get_clean();

// Corpo (tabela com skeleton)
ob_start();
?>
<table class="dash-table" id="<?= htmlspecialchars($idTabelaTabela) ?>">
    <thead>
        <tr>
            <?php foreach ($colunasTabela as $colunaTabela): ?>
                <th><?= show($colunaTabela) ?></th>
            <?php endforeach; ?>
        </tr>
    </thead>
    <tbody id="<?= htmlspecialchars($idCorpoTabela) ?>">
        <tr>
            <td colspan="<?= $totalColsTabela ?>">
                <?php
                $skeleton = ['altura' => $alturaSkelTabela];
                require __DIR__ . '/../internos/luz_skeleton.php';
                ?>
            </td>
        </tr>
    </tbody>
</table>
<?php
$corpoTabela = ob_get_clean();

// Delega para o painel
$painel = [
    'titulo'      => $tituloTabela,
    'icone'       => $iconeTabela,
    'tom'         => $tomTabela,
    'corpo'       => $corpoTabela,
    'headerExtra' => $headerExtraTabela,
    'gridColumn'  => $gridColumnTabela,
    'gridRow'     => $gridRowTabela,
];
require __DIR__ . '/../cards/luz_card_painel.php';
```

- [ ] **Step 3: Rerodar smoke test**

Mesmo comando do Step 1.
Expected: `PASS`.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/listas/luz_tabela_paginada.php
git commit -m "feat(dashboard-luz): add luz_tabela_paginada list component"
```

---

### Task 9: `luz_destaque_pessoa.php`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_destaque_pessoa.php`

- [ ] **Step 1: Smoke test (deve falhar)**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
function show($s) { return $s; }
$destaque = [
    "titulo"     => "Melhor Separador",
    "tom"        => "warning",
    "icone"      => "bi-trophy",
    "badgeRank"  => "#1",
    "idAvatar"   => "melhor-sep-avatar",
    "idNome"     => "melhor-sep-nome",
    "stats"      => [
        ["idValor" => "melhor-sep-score",  "sufixo" => "score",  "corValor" => "#fbbf24", "destaque" => true],
        ["idValor" => "melhor-sep-itens",  "sufixo" => "itens"],
    ],
    "gridColumn" => "span 1",
];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_destaque_pessoa.php"; $html = ob_get_clean();
$checks = [
    "Melhor Separador",
    "bi-trophy",
    "rgba(245, 158, 11",
    ">#1<",
    "id=\"melhor-sep-avatar\"",
    "id=\"melhor-sep-nome\"",
    "id=\"melhor-sep-score\"",
    "id=\"melhor-sep-itens\"",
    "grid-column: span 1",
];
foreach ($checks as $c) { if (strpos($html, $c) === false) { echo "FAIL: $c\n"; exit(1); } }
echo "PASS\n";'
```
Expected: `Failed opening required`.

- [ ] **Step 2: Implementar**

Write `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_destaque_pessoa.php`:

```php
<?php

/**
 * Luz Component: DestaquePessoa
 * Card "Melhor X" — avatar circular gradiente + nome + linhas de stats +
 * badge #1 no canto superior direito.
 *
 * Compõe luz_card_painel (passa badgeRank via headerExtra e o conteúdo
 * via corpo).
 *
 * Recebe: $destaque (array)
 *   - titulo      string (obrigatório)
 *   - tom         string info|success|warning|danger (default 'warning')
 *   - icone       string classe bootstrap-icons (default 'bi-trophy')
 *   - badgeRank   string ex.: '#1' — se vazio, sem badge
 *   - idAvatar    string id do <div> do avatar (JS popula iniciais)
 *   - idNome      string id do <div> do nome
 *   - stats       array de linhas, cada uma:
 *                   - idValor   string (obrigatório)
 *                   - sufixo    string texto após o valor (default '')
 *                   - corValor  string cor CSS do valor (default '#94a3b8')
 *                   - destaque  bool   <strong> se true, <span> caso contrário
 *   - gridColumn  string opcional
 */
$destaque              = $destaque ?? [];
$tituloDestaque        = $destaque['titulo']     ?? '';
$tomDestaque           = $destaque['tom']        ?? 'warning';
$iconeDestaque         = $destaque['icone']      ?? 'bi-trophy';
$badgeRankDestaque     = $destaque['badgeRank']  ?? '';
$idAvatarDestaque      = $destaque['idAvatar']   ?? '';
$idNomeDestaque        = $destaque['idNome']     ?? '';
$statsDestaque         = $destaque['stats']      ?? [];
$gridColumnDestaque    = $destaque['gridColumn'] ?? '';

// headerExtra: badge #1 (gradiente laranja)
$headerExtraDestaque = '';
if ($badgeRankDestaque !== '') {
    $headerExtraDestaque = '<div style="background: linear-gradient(135deg, #f59e0b, #ea580c);'
                         . ' color: #1e1b4b; font-weight: 800; font-size: 0.78rem;'
                         . ' padding: 0.25rem 0.6rem; border-radius: 9999px;'
                         . ' box-shadow: 0 4px 10px rgba(245,158,11,0.35);">'
                         . htmlspecialchars($badgeRankDestaque) . '</div>';
}

// Corpo: avatar + bloco de nome/stats
ob_start();
?>
<div style="display: flex; align-items: center; gap: 0.85rem; padding-top: 0.15rem;">
    <div <?= $idAvatarDestaque !== '' ? 'id="' . htmlspecialchars($idAvatarDestaque) . '"' : '' ?>
         style="width: 3rem; height: 3rem; border-radius: 50%;
                background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
                display: flex; align-items: center; justify-content: center;
                color: #fff; font-weight: 800; font-size: 0.95rem; flex-shrink: 0;
                box-shadow: 0 6px 14px rgba(124,58,237,0.35);">—</div>
    <div style="flex: 1; min-width: 0;">
        <div <?= $idNomeDestaque !== '' ? 'id="' . htmlspecialchars($idNomeDestaque) . '"' : '' ?>
             style="font-size: 1.1rem; font-weight: 700; color: #f1f5f9; line-height: 1.2;
                    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">—</div>
        <?php if (!empty($statsDestaque)): ?>
            <div style="font-size: 0.78rem; color: #94a3b8; margin-top: 0.2rem;
                        display: flex; flex-wrap: wrap; gap: 0.35rem;">
                <?php foreach ($statsDestaque as $indiceStat => $statDestaque):
                    $idValorStat   = $statDestaque['idValor']  ?? '';
                    $sufixoStat    = $statDestaque['sufixo']   ?? '';
                    $corValorStat  = $statDestaque['corValor'] ?? '#94a3b8';
                    $destaqueStat  = !empty($statDestaque['destaque']);
                    $tagStat       = $destaqueStat ? 'strong' : 'span';
                ?>
                    <?php if ($indiceStat > 0): ?>
                        <span style="opacity: 0.4;">·</span>
                    <?php endif; ?>
                    <<?= $tagStat ?> <?= $idValorStat !== '' ? 'id="' . htmlspecialchars($idValorStat) . '"' : '' ?>
                        style="color: <?= $corValorStat ?>;">—</<?= $tagStat ?>>
                    <?php if ($sufixoStat !== ''): ?>
                        <span style="opacity: 0.7;"><?= htmlspecialchars($sufixoStat) ?></span>
                    <?php endif; ?>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>
    </div>
</div>
<?php
$corpoDestaque = ob_get_clean();

// Delega para o painel
$painel = [
    'titulo'      => $tituloDestaque,
    'icone'       => $iconeDestaque,
    'tom'         => $tomDestaque,
    'corpo'       => $corpoDestaque,
    'headerExtra' => $headerExtraDestaque,
    'gridColumn'  => $gridColumnDestaque,
];
require __DIR__ . '/luz_card_painel.php';
```

- [ ] **Step 3: Rerodar smoke test**

Mesmo comando do Step 1.
Expected: `PASS`.

- [ ] **Step 4: Smoke test variante sem badge / sem stats**

Run:
```bash
php -r '
$destaque = ["titulo" => "Empty", "idAvatar" => "av", "idNome" => "nm", "stats" => []];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_destaque_pessoa.php"; $html = ob_get_clean();
if (strpos($html, "linear-gradient(135deg, #f59e0b") !== false) { echo "FAIL: badge indevido\n"; exit(1); }
if (strpos($html, "id=\"av\"") === false) { echo "FAIL: avatar id\n"; exit(1); }
echo "PASS\n";'
```
Expected: `PASS`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/cards/luz_destaque_pessoa.php
git commit -m "feat(dashboard-luz): add luz_destaque_pessoa card"
```

---

### Task 10: Refatorar `luz_card_kpi.php` para consumir `luz_tokens` + `luz_chip_titulo`

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php`

> **Atenção:** este é o único arquivo existente alterado pelo plano. A refatoração é **interna** — o HTML de saída deve ficar idêntico (mesma sequência de tags, atributos e estilos).

- [ ] **Step 1: Snapshot do output atual (golden file)**

Run:
```bash
cd /home/goldie/www/genesis
mkdir -p /tmp/luz-golden
php -r '
$kpi = ["titulo" => "Separações", "icone" => "bi-box-seam", "tom" => "info", "chave" => "separacoes", "hint" => "rotas no período", "escala" => 1.3];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php"; echo ob_get_clean();
' > /tmp/luz-golden/kpi_basico.html

php -r '
$kpi = ["titulo" => "Eficiência", "icone" => "bi-check-circle", "tom" => "success", "chave" => "ef", "hint" => "X / Y", "semSparkline" => true, "escala" => 1.3, "centralizar" => true];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php"; echo ob_get_clean();
' > /tmp/luz-golden/kpi_centralizado.html

php -r '
$kpi = ["titulo" => "Top", "icone" => "bi-trophy", "tom" => "warning", "spark" => [1,3,2,5,4,6], "valor" => "12"];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php"; echo ob_get_clean();
' > /tmp/luz-golden/kpi_sparkline.html

wc -c /tmp/luz-golden/*.html
```
Expected: 3 arquivos não-vazios. Anote os tamanhos — vão servir de baseline.

- [ ] **Step 2: Refatorar `luz_card_kpi.php` extraindo a paleta**

Editar `componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php`:

Substituir o bloco `// Paleta por tom ...` (linhas ~68-77 do arquivo atual, que define `$paletaTons` localmente) por:

```php
// Paleta por tom (tokens compartilhados)
$paletaTons = require __DIR__ . '/internos/luz_tokens.php';
$coresValor = $paletaTons[$tomKpi]      ?? $paletaTons['neutral'];
$coresBorda = $paletaTons[$tomBordaKpi] ?? $paletaTons['neutral'];
```

Não alterar mais nada (deixar `<div class="g-card-kpi">` e o restante do markup intactos por enquanto — o `luz_chip_titulo` será introduzido no Step 4).

- [ ] **Step 3: Verificar diff binário do output**

Run:
```bash
cd /home/goldie/www/genesis
php -r '
$kpi = ["titulo" => "Separações", "icone" => "bi-box-seam", "tom" => "info", "chave" => "separacoes", "hint" => "rotas no período", "escala" => 1.3];
ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php"; echo ob_get_clean();
' > /tmp/luz-golden/kpi_basico_NEW.html

diff /tmp/luz-golden/kpi_basico.html /tmp/luz-golden/kpi_basico_NEW.html && echo "IDENTICAL"
```
Expected: `IDENTICAL` (sem diff).

Repetir para os outros 2 cenários:
```bash
php -r '$kpi = ["titulo" => "Eficiência", "icone" => "bi-check-circle", "tom" => "success", "chave" => "ef", "hint" => "X / Y", "semSparkline" => true, "escala" => 1.3, "centralizar" => true]; ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php"; echo ob_get_clean();' > /tmp/luz-golden/kpi_centralizado_NEW.html
diff /tmp/luz-golden/kpi_centralizado.html /tmp/luz-golden/kpi_centralizado_NEW.html && echo "IDENTICAL"

php -r '$kpi = ["titulo" => "Top", "icone" => "bi-trophy", "tom" => "warning", "spark" => [1,3,2,5,4,6], "valor" => "12"]; ob_start(); require "componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php"; echo ob_get_clean();' > /tmp/luz-golden/kpi_sparkline_NEW.html
diff /tmp/luz-golden/kpi_sparkline.html /tmp/luz-golden/kpi_sparkline_NEW.html && echo "IDENTICAL"
```
Expected: 3× `IDENTICAL`.

- [ ] **Step 4: Substituir o cabeçalho inline pelo `luz_chip_titulo`**

No `luz_card_kpi.php`, localizar o bloco do cabeçalho (linhas ~121-131 do arquivo atual, do `<!-- Cabeçalho: título + chip de ícone -->` até o `</div>` que fecha esse cabeçalho).

> **Cuidado:** o cabeçalho atual no `luz_card_kpi.php` tem o **chip à direita e o título à esquerda** (ordem invertida em relação ao `luz_chip_titulo`, que tem chip à esquerda + título à direita). Para preservar o HTML idêntico, **não** trocar pelo `luz_chip_titulo` agora — esse refator estrutural fica fora do escopo.

Em vez disso, deixar o cabeçalho inline como está. **Pular o Step 4** — o consumo de `luz_chip_titulo` pelo `luz_card_kpi` fica para um trabalho futuro (a refatoração visual exigiria mudar a ordem chip/título e foge do "render idêntico").

> ATUALIZAÇÃO DE ESCOPO: a spec previa que o `luz_card_kpi` consumisse `luz_chip_titulo`, mas a inspeção da estrutura mostrou que o cabeçalho do KPI tem layout invertido (título à esquerda, chip à direita) por design. Forçar o reuso quebraria o visual. **Apenas a paleta** é centralizada nesta task. Documentar isso na spec após esta task (Task 11).

- [ ] **Step 5: Rerodar diff golden (deve continuar IDENTICAL)**

Repetir os 3 diffs do Step 3.
Expected: 3× `IDENTICAL`.

- [ ] **Step 6: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Dashboard/html/luz/luz_card_kpi.php
git commit -m "refactor(dashboard-luz): luz_card_kpi consumes shared luz_tokens"
```

---

### Task 11: Sanidade visual + ajuste da spec

**Files:**
- Modify: `docs/superpowers/specs/2026-05-06-dashboard-luz-componentizacao-design.md` (nota sobre escopo do Task 10)

- [ ] **Step 1: Smoke render do dashboard inteiro**

Abrir o dashboard no navegador (URL local do Genesis, dev_gx_goldie). Confirmar:

- [ ] As 3 abas (Visão Geral, Separação, Carregamento) carregam sem erro PHP.
- [ ] Os 18 painéis hard-coded renderizam visualmente idênticos ao estado anterior.
- [ ] Nenhum console error novo.

> Como o único arquivo de runtime alterado é o `luz_card_kpi.php` (paleta extraída pra `luz_tokens.php`), e o golden diff confirmou bytes idênticos, não devem haver regressões. Este passo é o teste de fumaça final.

- [ ] **Step 2: Atualizar a spec**

Editar `docs/superpowers/specs/2026-05-06-dashboard-luz-componentizacao-design.md`:

Na seção 4.5 ("Composição"), substituir o parágrafo:

> "`luz_card_painel` e `luz_card_kpi` (existente) usam `luz_chip_titulo` internamente — quando o `luz_card_kpi` for ajustado para consumir `luz_tokens.php`, oportunisticamente também passa a consumir `luz_chip_titulo`."

por:

> "`luz_card_painel` usa `luz_chip_titulo` internamente. O `luz_card_kpi` mantém seu cabeçalho inline (layout invertido: título à esquerda, chip à direita) — refatorá-lo para `luz_chip_titulo` exigiria mudança visual e está fora do escopo. O `luz_card_kpi` apenas passa a consumir `luz_tokens.php`."

Na seção 6 ("Plano de implementação"), atualizar o item 8:

> "8. Refatorar `luz_card_kpi.php` para consumir `internos/luz_tokens.php` (apenas a paleta — o cabeçalho inline permanece). Único arquivo existente alterado, output HTML idêntico ao baseline."

- [ ] **Step 3: Commit**

```bash
git add docs/superpowers/specs/2026-05-06-dashboard-luz-componentizacao-design.md
git commit -m "docs(dashboard-luz): align spec with executed scope (luz_card_kpi keeps inline chip)"
```

- [ ] **Step 4: Resumo final no terminal**

Run:
```bash
git log --oneline -12
ls -la componentes/Missoes/Logistica/sinais/Dashboard/html/luz/{,cards,listas,layout,internos}/ | grep -v '\.gitkeep' | grep -E '\.php$|^/'
```
Expected: 11 commits relacionados ao refator + listagem dos 8 novos arquivos PHP.

---

## Notas de execução

- **Sem alteração em `dashboard.php` nem `dashboard.js`.** Confirmado em todas as tasks.
- **Único arquivo existente alterado:** `luz_card_kpi.php` (paleta para `luz_tokens.php`, output HTML inalterado — verificado via diff golden em Task 10 Step 3 e Step 5).
- **Sem `luz.css`** — todos os estilos permanecem inline, conforme decisão da spec.
- **Smoke renders** rodam via `php -r` em CLI, sem dependência de framework de teste. Onde o componente usa `show()` (i18n), o smoke test stuba a função (`function show($s) { return $s; }`).
- **Cuidado com `htmlspecialchars`:** todos os campos vindos do array são escapados quando viram atributo/texto, exceto os campos explicitamente marcados como "HTML cru" (`corpo`, `headerExtra`) — esses são responsabilidade do chamador.
