diff --git a/docs/plans/bug-salvar-tendencia-fecha-wizard.md b/docs/plans/bug-salvar-tendencia-fecha-wizard.md new file mode 100644 index 00000000..1b3b08b7 --- /dev/null +++ b/docs/plans/bug-salvar-tendencia-fecha-wizard.md @@ -0,0 +1,120 @@ +# Bug — Salvar tendência fecha o wizard (não volta ao começo) + +Status: rascunho +Atualizado em: 2026-08-01 +Issue: — +Priority: P0 +Model: composer-2.5 +Impeccable: B — pós-submit de `WizardTrendNoteStep` +Appetite: ~0,5d eng; race revalidate×redirect; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Feel the action; commit óbvio) · tema `campaign`. + +Na implementação: craft compacto → critique → polish (toast + navegação). + +Brief: + +- **Persona:** CG salvou a justificativa de tendência. +- **Job principal:** sair do ritual com feedback de sucesso — **não** reabrir a grade de tendências. +- **Anti-goals:** loop escolha↔nota; sumir sem toast. + +### Wireframe (texto) + +```text +Antes (bug): + Nota → [Salvar] → (revalidate) página vê status==atual → redirect escolha + → “volta ao começo” + +Depois: + Nota → [Salvar] → toast “Tendência política registrada.” → Início + (ou, com B98: próximo encadeado / tela de sucesso) +``` + +## Dados → decisão → apresentação + +Dados: N/A — navegação pós-write; mensagem de toast já existe (`WIZARD_TREND_SAVED_MESSAGE`). + +## Contexto + +`WizardTrendNoteStep` grava via `setMunicipalityPoliticalTrendFormAction` e, no sucesso client, `useCampaignFormSuccessToast` → `router.push(CAMPAIGN_HOME)`. + +A action também chama `revalidateMunicipalityListPaths`. A página RSC (`acoes/[slug]/page.tsx`) para `change-trend`, quando `trendStatus` query **igual** ao `currentStatus` do município (já persistido), faz `redirect(...)` **de volta à etapa de escolha** (sem `tendencia=`). + +Ordem típica do bug: + +1. Usuário salva nota com `?tendencia=favoravel` (atual era outra). +2. Write OK + revalidate; RSC refresha com `politicalTrend.status === favoravel`. +3. Branch `trendStatus === currentStatus` → **redirect para a escolha**. +4. Client `router.push(HOME)` perde a corrida ou é sobrescrito → sensação de “Salvar volta ao começo”. + +Pedido (2026-08-01): Salvar **fecha** o wizard. + +## Objetivos + +- Após Salvar bem-sucedido na nota de tendência, o usuário **não** permanece/reentra na grade de escolha do mesmo município. +- Destino v1: **Início** (toast intacto). Quando **B98** existir, o destino pode ser o próximo da cadeia — este item deixa um único ponto de navegação pós-sucesso. +- Eliminar a corrida revalidate → `redirect` de “status já atual” enquanto o client ainda está no passo nota **ou** tornar o redirect inofensivo (não mandar de volta à escolha após save). +- Unit/int ou e2e smoke: save → URL final ≠ escolha do mesmo fluxo. + +## Decisões travadas + +- **Fechar = navegar para fora do passo nota com sucesso explícito (Início v1).** Fonte: produto 2026-08-01. **Rejeitado:** ficar na nota com banner; voltar à escolha “para confirmar”. +- **Corrigir a race no server redirect e/ou no success path — não só `router.push` mais agressivo.** O `redirect` quando `trendStatus === currentStatus` é legítimo para deep-link stale **antes** do save; pós-save ele vira armadilha. **Rejeitado:** só `router.replace(HOME)` sem mudar o redirect (continua flash/loop em rede lenta). +- **Preferência de implementação (barata):** success path da form action (ou wrapper do wizard) usa `redirect(CAMPAIGN_HOME)` server-side **ou** a página deixa de redirecionar à escolha quando o doc já reflete o query (tratar como “já aplicado” → home / success). **Recomendação no craft:** `runCampaignRedirectFormAction` / redirect no success do wizard trend (precedente outros forms) **ou** client navigation + não revalidar a rota do wizard antes do push. Pin a opção no as-built. +- **i18n:** `WIZARD_TREND_SAVED_MESSAGE` intacto. + +## Questões em aberto + +- **Redirect server na action vs client toast+push?** **Opções:** A `redirect` na action (fecha cedo; toast via query `?saved=1`) | B manter toast client mas impedir redirect RSC à escolha (ex. não revalidate path do wizard; ou branch “same status” → home). **Recomendação:** **B com revalidate seletivo + push**, ou A se B ainda correr — Feel the action prefere toast; redirect puro sem toast é pior. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart TD + Save["POST trend note"] --> Write["setMunicipalityPoliticalTrend"] + Write --> Reval["revalidate listas"] + Write --> Success["status success"] + Success --> Nav["navigate HOME (sem passar pela escolha)"] + Page["RSC change-trend"] -->|"stale deep-link only"| Choice["redirect escolha"] + Page -->|"post-save same status"| NotChoice["não empurrar escolha"] +``` + +Componentes: + +- **`WizardTrendNoteStep.tsx`** + **`municipalityStaffFormActions.ts`** / página `acoes/[slug]`: quebrar a corrida (detalhe no craft). +- **`acoes/[slug]/page.tsx`**: revisitar branch `trendStatus === currentStatus` — distinguir deep-link inválido vs “já salvo”. +- **Teste:** unit do helper de resolução de destino; e2e smoke se barato. +- **Migration:** Sem migration. + +## Dependências + +- Nenhuma dura. Soft: B64 ✓. Compatível com **B96** (X no standalone). Destino encadeado futuro: **B98**. + +## Não escopo + +- Encadear tendência após votos → **B98**. +- Mudar tiles/nota UI. +- Autosave da lista (B24) — outro path. + +## Rabbit holes + +- **Optimistic UI da tendência no wizard.** **Mitigação:** fix de navegação só. +- **Remover o guard `trendStatus === currentStatus` por completo.** Pode reabrir deep-links lixo; mitigar com destino home ou mensagem, não loop. + +## Adiado com gatilho + +- **Pós-sucesso → próximo da cadeia.** **B98**. + +## Referências + +- `src/components/campaign/municipality/WizardTrendNoteStep.tsx` +- `src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx` (branch change-trend) +- `src/app/(campaign)/campanha/(app)/municipios/municipalityStaffFormActions.ts` +- `src/components/campaign/shared/useCampaignFormSuccessToast.ts` +- [wizard-mudar-tendencia.md](wizard-mudar-tendencia.md) +- AGENTS.md — Feel the action / form actions + +Qualidade de decisão: 5/5 diff --git a/docs/plans/ci-deploy-cooldown-vercel.md b/docs/plans/ci-deploy-cooldown-vercel.md new file mode 100644 index 00000000..228d7e8b --- /dev/null +++ b/docs/plans/ci-deploy-cooldown-vercel.md @@ -0,0 +1,99 @@ +# CI — cooldown mínimo de 30 min entre deploys de produção (Vercel) + +Status: rascunho +Atualizado em: 2026-08-01 +Issue: — +Priority: P0 +Model: composer-2.5 +Impeccable: A — N/A (sem superfície UI) +Appetite: ~0,5–1d eng; 1 job step em `ci.yml` + retry/requeue; sem migration +Responsável: — + +## Dados → decisão → apresentação + +Dados: N/A — gate de infra/CI; sem KPI de produto. + +## Contexto + +O verificador `ci.yml` (push em `main`) sobe produção com `vercel deploy --prebuilt --prod`. No plano Hobby/Free da Vercel o upload da CLI estoura o teto diário (`api-upload-free`, “Too many requests — try again in 24 hours / more than 5000”). Em 2026-08-01 vários merges em sequência (incl. **B92–B94**) passaram nos checks e **falharam no job `deploy`** — o código ficou em `main` sem ir a produção. + +Já existe um “stale HEAD” guard (só o tip de `main` faz deploy) + `requeue` se `main` avançou. **Não** há intervalo mínimo entre deploys bem-sucedidos: cada merge verde dispara upload completo. + +Pedido de produto (2026-08-01): **mínimo 30 minutos** entre um deploy e o próximo; antes de continuar, o job verifica a idade do último deploy. + +**Consequência imediata (não é bug de UI):** o empty/suggest do wizard de município (**B92–B94**, Issues #92–#94, merged) **não está em produção** porque o deploy falhou por rate limit — não porque o idle esteja “quebrado” no código de `main`. + +## Objetivos + +- Antes de `vercel pull`/`build`/`deploy --prod`, consultar o **último deployment de produção** do projeto e, se a idade for **< 30 min**, **não** fazer upload agora. +- Em skip por cooldown: sair com sucesso “adiado” (não vermelho falso) e **reagendar** o workflow (mesmo espírito do `requeue` atual) para rodar depois do restante do intervalo — assim o tip de `main` ainda chega a produção sem depender de humano. +- Opcional barato no mesmo PR: passar `--archive=tgz` no `vercel deploy` (a própria CLI sugere) para reduzir pressão de upload — **não** substitui o cooldown. +- Guardrails: sem migration; secrets `VERCEL_*` já usados; não tocar `ci-pr.yml` (sem deploy); Vercel preview Git continua ignorado (`vercel-ignore-build.sh`). + +## Decisões travadas + +- **Fonte da idade = último production deployment do projeto Vercel (API/`vercel ls`/`vercel inspect`), não o last successful `ci.yml` run.** Motivo: o teto é da conta Vercel; um deploy manual/CLI fora do Actions também conta. **Rejeitado:** só olhar `gh run list --workflow=ci.yml` (ignora deploys fora do GHA); sleep fixo de 30 min em todo run (desperdício quando já passou). +- **Cooldown = 30 min (constante nomeada no workflow/script).** Fonte: pedido 2026-08-01. **Rejeitado:** 1 h (atrasa demais o tip); só backoff após 429 (já estouramos o dia — tarde demais). +- **Skip por cooldown ≠ failure.** Job `deploy` marca “adiado”, `requeue`/dispatch agenda novo `ci.yml` com delay (ou workflow auxiliar `workflow_dispatch` + `wait`). **Rejeitado:** `exit 1` (alarme falso; agentes/CI “dono do PR” interpretam vermelho); skip silencioso sem requeue (main fica sem deploy até o próximo merge). +- **Stale HEAD continua primeiro.** Ordem: tip check → cooldown check → credentials → pull/build/deploy. **Rejeitado:** cooldown antes do tip (adiaria deploy que já seria skipado). +- **i18n:** ids em inglês (`DEPLOY_COOLDOWN_MS`, `lastProductionDeployAt`); logs pt-BR ou EN ok no Actions. + +## Questões em aberto + +- **Como obter o timestamp do último prod deploy?** **Opções:** A) `vercel ls --prod` / API REST deployments filtrando `target=production` + `ready` | B) gravar timestamp em Actions cache/artifact após deploy OK | C) GitHub Deployment API. **Recomendação:** **A** — fonte de verdade Vercel; B desincroniza se houver deploy fora do CI. _(assumido)_ +- **Delay do requeue: `gh workflow run` imediato + step `sleep` residual, ou cron/`repository_dispatch`?** **Opções:** A sleep no mesmo job até completar 30 min (bloqueia runner) | B dispatch com `sleep` curto só do residual num job leve | C Actions `schedule` a cada 15 min checando tip≠prod. **Recomendação:** **B** — job leve espera o residual (cap ~30 min) e re-dispara `ci.yml`; A gasta minutos caros no job de build. _(assumido — validar custo de runner)_ + +## Abordagem proposta + +```mermaid +flowchart TD + Tip["Guard stale HEAD"] -->|tip| Age["Idade último prod deploy"] + Age -->|"< 30 min"| Wait["Skip + schedule requeue"] + Age -->|">= 30 min"| Creds["Require VERCEL_*"] + Creds --> Build["pull + build + deploy --prod --archive=tgz"] + Build --> Ok["Record / log deploy ok"] + Wait --> Later["ci.yml again after residual"] +``` + +Componentes: + +- **`.github/workflows/ci.yml` (job `deploy`)**: step `Check deploy cooldown` após tip; output `cooldown=true|false` + `wait_seconds`; condicionar steps de build/deploy; estender `requeue` (ou step irmão) para “main tip sem deploy recente / cooldown”. +- **Script pequeno** (`scripts/vercel-deploy-cooldown.mjs` ou shell no step): chama API Vercel com `VERCEL_TOKEN` + `VERCEL_PROJECT_ID`; devolve idade; constante `DEPLOY_COOLDOWN_MS = 30 * 60 * 1000`. +- **`vercel deploy --prebuilt --prod --archive=tgz`**: redução de arquivos no upload (hint da CLI no erro 2026-08-01). +- **Verificação pós-entrega:** após um deploy OK, smoke do idle do wizard (`mode: wizard-municipality-suggest`) em produção — confirma que B92–B94 saíram do limbo. +- **Migration:** Sem migration, sem collection, sem server action de app. + +## Dependências + +- Nenhuma Issue de produto. Soft: entendimento do `requeue` atual em `ci.yml`; secrets Vercel já no repo. +- Desbloqueia visualização em prod de tudo que está em `main` sem deploy (incl. B92–B94). + +## Não escopo + +- Upgrade de plano Vercel / mudança de limites da conta. +- Deploy de preview por branch (já skipado). +- Mudar gate de merge (`checks` / `migration-lock`). +- Refatorar o orquestrador do agent-pool para “batch merges” (adiado). +- Bugfix de UI do empty state — causa raiz = undeployed; se após deploy OK o idle ainda falhar, abrir Issue B nova com evidência. + +## Rabbit holes + +- **“Já que toco deploy, migro para OIDC / Vercel Git / multi-env.”** Explode appetite. **Mitigação:** só cooldown + archive + requeue. +- **State machine de fila de deploys no Postgres.** **Mitigação:** workflow + API Vercel bastam. +- **Deduplicar builds com cache Vercel remoto exótico.** **Mitigação:** fora; `--prebuilt` já isola build→upload. + +## Adiado com gatilho + +- **Batch de merges → um deploy.** Revisitar se, com cooldown de 30 min, o tip ainda atrasar >1 h sob carga do pool **e** produto aceitar “deploy só a cada N merges”. +- **`--archive=tgz` sozinho sem cooldown.** Se o teto 5000/dia continuar após archive, o cooldown permanece necessário. + +## Referências + +- `.github/workflows/ci.yml` — jobs `deploy` / `requeue`, tip guard +- Erro observado 2026-08-01: `Too many requests … code: "api-upload-free"` no merge de B94 (#100) +- Issues B92–B94 (#92–#94) — código em `main`, deploy falhou +- `scripts/vercel-ignore-build.sh` — previews Git off +- `docs/AGENT-OPS.md` — entrega = merge em `main`; deploy gated por `ci.yml` +- AGENTS.md — região `gru1`; deploy via Actions + +Qualidade de decisão: 5/5 diff --git a/docs/plans/encadear-ajustes-wizard.md b/docs/plans/encadear-ajustes-wizard.md new file mode 100644 index 00000000..4819fe5f --- /dev/null +++ b/docs/plans/encadear-ajustes-wizard.md @@ -0,0 +1,134 @@ +# Encadear ajustes após a ação principal do wizard + +Status: rascunho +Atualizado em: 2026-08-01 +Issue: — +Priority: P1 +Model: cursor-grok-4.5-medium +Impeccable: C — orquestração multi-fluxo no chassis `/campanha/acoes` +Appetite: ~1–2d eng; matriz de cadeia + navegação pós-sucesso; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Ação → Local → Quem; Continuity) / `DESIGN.md` · `fluxos-acao-primeiro-inicio.md` · B59/B75 chassis · tema `campaign`. + +Na implementação: shape → craft → critique → polish. + +Brief compacto: + +- **Persona / contexto:** CG no polegar; acabou o ajuste principal (ex. votos) e ainda tem contexto do município na cabeça. +- **Job principal:** oferecer **na sequência** os ajustes que costumam vir juntos, sem voltar ao Início e reescolher município; poder **Pular** cada um. +- **Estratégia de cor:** Restrained; reusa tiles/passos já existentes. +- **Edit where you see:** writes reusam actions existentes (não segundo sistema). +- **Anti-goals:** state machine XState global; forçar todos os ajustes; reescolher município entre elos; resumo mega-commit (v1 pode gravar por elo como hoje). + +### Wireframe (texto) + +```text +Ajustar votos (entry) + 1. Busca município + 2. Config principal: votos → Salvar + 3. Encadeada: tendência? [Pular] … Salvar + 4. Encadeada: sinal? [Pular] … Salvar + 5. (opcional) liderança? [Pular] + → Início / “Atualização registrada em {município}” + +Mudar tendência (entry) + 1. Município → 2. tendência (X até salvar) → 3. encadeadas leves (sinal?) → fim +``` + +## Dados → decisão → apresentação + +Dados: N/A como métrica nova — encadeia writes já existentes (votos, tendência, sinal, liderança). Decisão = “registrar agora o próximo ajuste neste município ou pular”. + +## Contexto + +O rascunho UX-1 ([fluxos-acao-primeiro-inicio.md](fluxos-acao-primeiro-inicio.md)) pediu continuidade no fim de A1 (“Quer também…?: sinal / tendência / liderança”). Os wizards standalone existem (votos, sinal, tendência, liderança), com `entryAction` query **já** usada para skip (sinal/liderança) e prefill de nota de tendência — mas **ninguém navega** de um fluxo ao próximo após o save. + +`WizardExpectedVotesStep` tem fase `saved` com placeholder: _“Próximo passo deste fluxo (sinal, tendência ou resumo) em breve.”_ — gancho explícito. + +Pedido (2026-08-01): ao iniciar uma ação → município → **ajuste principal** → **encadear** outros ajustes que façam sentido (ex. votos → tendência). + +## Objetivos + +- Matriz v1 (travada abaixo) de `entryAction` → fila ordenada de subfluxos após sucesso da principal. +- Após Salvar da principal: ir ao **primeiro** elo da fila **no mesmo município** (URL do wizard alvo + `entryAction` + `municipio=`), não ao Início. +- Cada elo encadeado: chrome **Pular** (B96) → próximo elo ou fim; Salvar → próximo / fim. +- Fim: Início (ou tela curta de sucesso reusando copy de votos) **sem** re-pedir município. +- Guardrails: mesmas actions/access; sem migration; leader fora (layout `acoes` já gateia staff). + +## Decisões travadas + +- **Cadeia = navegação entre rotas/passos existentes + `entryAction`, não um mega-form único.** Reusa B61/B63/B64/B70. **Rejeitado:** um só POST transacional de tudo (caro; quebra skip parcial); checkboxes “Quer também?” sem subfluxo (raso demais vs tiles já feitos). +- **Matriz v1 (assumida — validar com produto):** + + | Entry (botão Início) | Principal | Encadeadas (ordem) | + | -------------------- | --------- | ----------------------------- | + | `update-votes` | votos | tendência → sinal → liderança | + | `register-signal` | sinal | tendência | + | `change-trend` | tendência | sinal | + | `update-leadership` | liderança | — (sem cadeia v1) | + + **Rejeitado:** incluir nível de envolvimento (E14) / assessor na v1 (A7/A8 do UX-1 — raros no ritual 7h); cadeia simétrica completa em todo entry. + +- **Pular / Salvar avançam a fila; X só na principal (B96).** Encadeada nunca usa X como “pular um elo” — X abortaria o ritual inteiro só se quisermos dismiss global; **recomendação:** nas encadeadas dismiss/skip = próximo ou fim (Pular), sem X que descarte a principal já salva sem aviso. **Rejeitado:** X nas encadeadas = Início (ok como dismiss explícito se B75 mantiver — produto pediu Pular no lugar do X). +- **Gravação por elo (já existente), não commit único no fim.** **Rejeitado:** rascunho multi-collection até o CTA final (appetite + U4). +- **i18n:** `wizardChain`, `entryAction`, `nextChainHref`; copy pt-BR nos títulos de fluxo já do catálogo. + +## Questões em aberto + +- **Sinal antes ou depois de tendência na cadeia de votos?** **Opções:** A tendência→sinal (tabela acima) | B sinal→tendência (UX-1 A1 passos 5–6). **Recomendação:** **A** — tendência é leitura política rápida; sinal é texto. _(assumido — validar)_ +- **Tela “Votos atualizados” intermediária some?** **Opções:** A pular direto à 1ª encadeada | B 1 toque “Continuar” na fase saved. **Recomendação:** **A** — menos atrito; Feel the action. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart LR + Entry["entryAction"] --> Main["passo principal"] + Main -->|save OK| Queue["wizardChainQueue(entry)"] + Queue --> Next["wizardHref(next, municipio, entryAction)"] + Next -->|Pular/Salvar| Queue + Queue -->|vazia| Home["Início / success"] +``` + +Componentes: + +- **`src/lib/wizardActionChain.ts`** (puro): `wizardChainAfter(entry): CampaignWizardActionId[]`; `nextWizardChainStep(...)`; unit-pinned. +- **Pontos de sucesso:** `WizardExpectedVotesStep` (fase saved → replace por redirect ao próximo); `WizardTrendNoteStep` / signal body / leadership continue — após save, consultar fila restantes. +- **URLs:** reusar `wizardTrendHref` / `wizardSignalHref` / `wizardActionHref` com `entryAction` = dono da sessão de cadeia (o botão original). +- **B97** deve estar verde antes de encadear a partir de tendência (senão o save da tendência loopa e nunca avança). +- **Migration:** Sem migration. + +## Dependências + +- Duras: **B96** (Pular nas encadeadas), **B97** (Salvar tendência não loopa). Soft: B61/B63/B64/B70 ✓, placeholder de votos. +- Soft: OPS11 só para ver em prod. + +## Não escopo + +- Resumo multi-campo + CTA único “Registrar atualização” (UX-1 passo 8) — adiado. +- E14 / troca de assessor na cadeia. +- Demanda (A5) na matriz v1. +- Persistência de rascunho mid-Zap (B59 adiado). + +## Rabbit holes + +- **XState / orquestrador multi-route genérico.** **Mitigação:** array puro + hrefs. +- **Transação única multi-collection.** **Mitigação:** grava por elo. +- **Reordenar a strip do Início “porque a cadeia mudou o ritual”.** **Mitigação:** fora (B99 à parte). + +## Adiado com gatilho + +- **Tela de resumo antes do fim.** Revisitar se CG reportar “não sei o que gravei”. +- **Cadeia a partir de sugestão E11.** Quando E11 deep-linkar wizards com município pré-escolhido. + +## Referências + +- [fluxos-acao-primeiro-inicio.md](fluxos-acao-primeiro-inicio.md) +- `src/components/campaign/shared/WizardExpectedVotesStep.tsx` (fase `saved` + placeholder) +- `src/lib/campaignActionRoutes.ts` · `campaignWizardCopy.ts` +- [wizard-header-x-vs-pular.md](wizard-header-x-vs-pular.md) (B96) · [bug-salvar-tendencia-fecha-wizard.md](bug-salvar-tendencia-fecha-wizard.md) (B97) +- `PRODUCT.md` / `DESIGN.md` + +Qualidade de decisão: 4/5 (matriz v1 assumida — validar no gate) diff --git a/docs/plans/polimento-busca-municipio-wizard.md b/docs/plans/polimento-busca-municipio-wizard.md new file mode 100644 index 00000000..f63b6cb8 --- /dev/null +++ b/docs/plans/polimento-busca-municipio-wizard.md @@ -0,0 +1,118 @@ +# Polimento — busca de município no wizard (bullets, gaps, título mobile) + +Status: rascunho +Atualizado em: 2026-08-01 +Issue: — +Priority: P1 +Model: composer-2.5 +Impeccable: B — encaixe em `WizardMunicipalitySearchStep` + padding do `CampaignWizardShell` +Appetite: ~0,25–0,5d eng; CSS/markup; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Clarity under pressure) / `DESIGN.md` · tema `data-theme='campaign'` · B60/B75 shells. + +Na implementação (`work-issue`): craft compacto → critique → polish. + +Brief compacto: + +- **Persona / contexto:** CG/assessor no celular no passo 1 do ritual ação→município. +- **Job principal:** escanear hits sob a busca sem ruído visual; título “Em qual município?” colado ao chrome vermelho. +- **Estratégia de cor:** inalterada. +- **Edit where you see:** não — só layout do passo. +- **Anti-goals:** redesign da row (`HomeSearchHitRow`); mudar ranking B92–B94; cards. + +### Wireframe (texto) + +```text +┌─ header wizard (Mandate Red) ───────────────────────┐ +│ Ajustar votos [X] │ +└─────────────────────────────────────────────────────┘ +┌─ conteúdo (pt menor no mobile) ─────────────────────┐ +│ Em qual município? │ ← perto do header +│ ┌─ Buscar município ─────────────────────────────┐ │ +│ │ Nome do município… │ │ +│ └────────────────────────────────────────────────┘ │ +│ ← gap curto (não gap-4) │ +│ Cairu · Baixo Sul │ ← sem • bullet +│ Salinas da Margarida · … │ +└─────────────────────────────────────────────────────┘ + Fora do frame: chrome B75; suggest idle B92–B94. +``` + +## Dados → decisão → apresentação + +Dados: N/A — apresentação de hits já existentes; sem métrica nova. + +## Contexto + +`WizardMunicipalitySearchStep` renderiza resultados em `