diff --git a/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md b/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md new file mode 100644 index 00000000..a9ecdf6e --- /dev/null +++ b/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md @@ -0,0 +1,91 @@ +--- +target: /campanha/login +total_score: 26 +p0_count: 0 +p1_count: 2 +timestamp: 2026-07-19T21-47-04Z +slug: src-app-campaign-campanha-login +--- +# Critique: /campanha/login + +Method: dual-agent (A: 8a8b1ae0-9a8a-45f7-907f-54afcdf62db8 · B: 3899acad-4f36-4817-91d6-4135d72d2e96) + +## Design Health Score + +| # | Heuristic | Score | Key Issue | +|---|-----------|-------|-----------| +| 1 | Visibility of System Status | 3 | Pending state clear; no other status chrome | +| 2 | Match System / Real World | 3 | PT-BR + e-mail/celular fit; megaphone mark generic | +| 3 | User Control and Freedom | 2 | Only “Esqueceu a senha?”; no exit to public site | +| 4 | Consistency and Standards | 2 | h1 left vs centered muted copy; leading clash | +| 5 | Error Prevention | 2 | required + inputMode help; dual identifier still easy to mistype | +| 6 | Recognition Rather Than Recall | 3 | Labels/placeholder clear | +| 7 | Flexibility and Efficiency | 3 | E-mail or phone + adaptive inputMode | +| 8 | Aesthetic and Minimalist Design | 2 | Header rhythm/alignment + always-on help prose | +| 9 | Error Recovery | 3 | FieldError + leadership recovery hint | +| 10 | Help and Documentation | 3 | Help exists; always-visible first-access competes | +| **Total** | | **26/40** | **Acceptable** | + +## Anti-Patterns Verdict + +**LLM:** Low–moderate slop. Not purple-SaaS; stock shadcn auth shell. Dual muted blurbs feel like copy padding. Typesetting bugs read unfinished. + +**Detector CLI:** exit 0, 0 source findings on login + CampaignAuthPageShell. + +**Runtime (Playwright inject):** `flat-type-hierarchy` (body; sizes 14/16/20px); `nested-cards` on CardHeader — likely false positive (shadcn Card > CardHeader). + +## Overall Impression + +Competent Field Desk palette discipline, phone-first form craft — undermined by a campaign-theme `h1 { text-align: left }` leak and article-style `p { leading-7 }` on auth copy. Biggest opportunity: fix header typesetting; do **not** add split-screen public posts. + +## What's Working + +1. Phone-first: min-h-11, enterKeyHint, identifierInputMode +2. Red as signal ≤10% +3. Auth-error recovery hint for liderança phone-only accounts + +## Priority Issues + +### P1 — Title misaligned with subtitle +Campaign `[data-theme='campaign'] h1 { text-align: left }` overrides CardHeader `text-center`. Title glyph start ~28px left of centered subtitle. +**Fix:** Scope left-align to app chrome, or `text-center` on auth heading. +**Command:** `/impeccable typeset` or `/impeccable layout` + +### P1 — Subtitle vertical rhythm inverted +CardHeader `gap-1` (4px) between title and description; orphan `
` inherits base `leading-7` (28px) while CardDescription stays ~20px. Title glued to howto; second muted block breathes like article body. +**Fix:** One muted block with explicit leading-snug; bump title→desc gap; never bare `
` under campaign theme without leading override. +**Command:** `/impeccable layout` + `/impeccable typeset` + +### P2 — Split-screen + public publications: recommend No +Login is ops gate (noindex); posts are public register. Phone users get scroll soup. Motivation ≠ news feed on unlock. +**Soft maybe:** one quiet purpose line under mark — not a publication panel. +**Command:** `/impeccable shape` only if product insists on panel (internal voice, not posts) + +### P2 — Generic Megaphone mark +Not Solla / Field Desk identity. +**Command:** `/impeccable polish` (reuse campaign-logo) + +### P3 — Always-on first-access paragraph +Progressive disclosure for returning users. +**Command:** `/impeccable distill` or `/impeccable clarify` + +## Persona Red Flags + +**Casey:** Misalignment reads broken; two gray blurbs delay thumb-to-input; split-screen would be fatal. +**Jordan:** Copy helps; visual hierarchy undercuts trust; no public-site escape. +**Lia:** Input path strong; header bugs tax one-handed use; keep error recovery; reject post panel. +**Sam:** Muted contrast scrapes AA (~4.83:1); labels/ARIA look solid. + +## Minor Observations + +- CardHeader grid-rows assumes 2 rows; third orphan `
` is API misuse
+- Desktop narrow column is correct for this task
+- Placeholder dual-format is heavy
+
+## Questions to Consider
+
+1. If the desk is the product, why does the door still look like every shadcn login?
+2. Is “inspiring” one quiet line under the mark — or only after auth?
+3. Would merging the two muted sentences fix more trust than any split-screen?
+4. Who unlocks at 7am in a núcleo — why would they want a publication?
+5. Why does auth inherit dashboard `h1` left-align rules?
diff --git a/docs/plans/escala-dry-pos-reset-senha-perfil.md b/docs/plans/escala-dry-pos-reset-senha-perfil.md
index 439d77be..c796c690 100644
--- a/docs/plans/escala-dry-pos-reset-senha-perfil.md
+++ b/docs/plans/escala-dry-pos-reset-senha-perfil.md
@@ -1,7 +1,7 @@
# Escala e DRY pós-reset de senha + foto de perfil
Status: registrado no roadmap (fases pendentes)
-Atualizado em: 2026-07-19 (Fase 3 parcial entregue em `main` @ `246ca85`; absorção `capture-review-debts`)
+Atualizado em: 2026-07-19 (Fase 3 parcial: polish `/campanha/login` + simplify; absorção `capture-review-debts` 2026-07-19)
Item do roadmap: [docs/roadmap.md](../roadmap.md) (RS+, fill-in de engenharia pós-MVP)
Responsável: —
@@ -11,13 +11,15 @@ O fill-in **Reset de senha self-service + foto de perfil** ([reset-senha-foto-pe
**Já resolvido no simplify (não reabrir):** `CampaignFormActionState` compartilhado; constantes de copy (`CAMPAIGN_PASSWORD_RESET_*`, `CAMPAIGN_LEADERSHIP_FORGOT_PASSWORD_MESSAGE`, `CAMPAIGN_SESSION_EXPIRED_MESSAGE`); `campaignUserShellView` / `CampaignUserShellView`; `canUpdateCampaignUserAvatar` delegando para `canUpdateCampaignUser`; `fieldError` nos forms; `FormActionStatus` no perfil; remoção de wrappers `passwordFormError`/`profileFormError`; exports internos de `resetCampaignPassword`/`changeCampaignPassword`; teste int renomeado para login com senha errada.
-**Já resolvido na Fase 3 parcial (merge `main` 2026-07-19, `246ca85` — não reabrir):** `CampaignAuthPageShell` em `login` / `esqueci-senha` / `redefinir-senha` (incl. card de token inválido); `campaignAuthHeadingClassName` + copy compartilhada (`CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT`, `CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT`); login com `inputMode` dinâmico tel/email, `aria-invalid`/`aria-describedby` nos dois campos, copy de primeiro acesso, `w-full` no submit — fecha P1–P3 do critique Impeccable `/campanha/login` da sessão.
+**Já resolvido na Fase 3 parcial (merge `main` 2026-07-19, `246ca85` — não reabrir):** `CampaignAuthPageShell` em `login` / `esqueci-senha` / `redefinir-senha` (incl. card de token inválido); `campaignAuthHeadingClassName` + copy compartilhada (`CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT`, `CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT`); login com `inputMode` dinâmico tel/email, `aria-invalid`/`aria-describedby` nos dois campos, `w-full` no submit.
+
+**Já resolvido no polish + `/simplify` de `/campanha/login` (2026-07-19 — não reabrir):** critique Impeccable P1–P3 fechados (alinhamento título/subtítulo `!text-center`, ritmo `gap-2` + `leading-snug`, `CampaignLogo` no shell, `
+
- Primeiro acesso? Peça um convite no WhatsApp ao coordenador do seu núcleo.
-
depth 0 + getCampaignUserWithAvatar
sessão leve em rotas públicas"]
F1 --> F2["Fase 2 — Avatar write perf
blob fora da TX + revalidate estreito"]
- F2 --> F3["Fase 3 — DRY auth UI/senha
shell ✓ parcial · password fields + login state"]
+ F2 --> F3["Fase 3 — DRY auth UI/senha
shell/card ✓ parcial · password fields + login state"]
F3 --> F4["Fase 4 — DRY copy/role + actions
campaignRoleOptions + getCampaignActionContext"]
F4 --> F5["Fase 5 — Opcional
avatar intent único + int change password"]
```
@@ -64,7 +66,7 @@ flowchart TD
### Fase 3 — DRY UI e senha
-**Entregue parcialmente (2026-07-19):** `CampaignAuthPageShell` + headings/copy unificados nas três rotas públicas e no card de token inválido; polish de login do critique Impeccable (ver Contexto).
+**Entregue parcialmente (2026-07-19):** `CampaignAuthPageShell`, `CampaignAuthCardHeader`, `CampaignAuthBackToLoginLink`, tokens de copy/classes em `campaignAuthCopy.ts`, `CampaignLogo` no shell, polish Impeccable `/campanha/login` (ver Contexto).
**Pendente nesta fase:**
@@ -74,6 +76,7 @@ flowchart TD
### Fase 4 — DRY copy, role e action context
+- Consolidar strings de liderança/convite (`CAMPAIGN_LEADERSHIP_*`, `CAMPAIGN_FIRST_ACCESS_HINT`, `CAMPAIGN_LOGIN_SUBTITLE` + mensagem server em `campaignPasswordReset.ts`) numa fonte única com variantes por contexto.
- **`campaignRoleLabels`** ↔ opções do campo `role` em `CampaignUser.ts` (fonte única `campaignRoleOptions`).
- Propagar para **`NucleusUpdateFeed.tsx`** e outros `roleLabels` locais quando esses arquivos forem tocados.
- **`password.ts` / `profile.ts`:** adotar `getCampaignActionContext()` (`src/utilities/campaignActionContext.ts`) como `supporter.ts` / `actionPlan.ts`.
@@ -98,13 +101,21 @@ flowchart TD
- RBAC em `users` (admin Payload) → [roadmap](../roadmap.md) Admin Payload.
- Unificar **todo** o ciclo 1 de auth (convite + login + admin) num único mega-refactor — RS+ cobre só o fill-in entregue e vizinhos diretos.
+## Explicitamente fora (critique/simplify desta sessão — não reabrir)
+
+- Split-screen login com publicações ou painel motivacional (decisão produto 2026-07-19; critique recomendou não).
+- Link de saída para o site público na tela de login (heurística 3, score 2).
+- Placeholder longo dual-format no identificador; submit helper global (`Spinner` + `aria-live`); constantes de rota auth; `flat-type-hierarchy` do detector runtime no body.
+- Comentário em `styles.css` para auth — workaround com tokens/componentes já suficiente.
+
## Referências
- [reset-senha-foto-perfil.md](reset-senha-foto-perfil.md) — MVP entregue
- [escala-dry-pos-visitados-recentemente.md](escala-dry-pos-visitados-recentemente.md) — precedente RS+/VR+
- `src/utilities/campaignAuth.ts`, `src/app/(campaign)/campanha/actions/password.ts`, `profile.ts`
- `src/components/campaign/CampaignProfileSettings.tsx`, `CampaignSidebar.tsx`, `CampaignUserAvatar.tsx`
-- `src/components/campaign/CampaignAuthPageShell.tsx` — shell entregue; ver Fase 3 pendente
+- `src/components/campaign/CampaignAuthPageShell.tsx`, `CampaignAuthCardHeader.tsx`, `CampaignAuthBackToLoginLink.tsx`
+- `src/lib/campaignAuthCopy.ts`
- `src/app/(campaign)/campanha/login/`, `esqueci-senha/`, `redefinir-senha/`
- `src/lib/schemas/campaignPassword.ts`, `src/lib/schemas/invite.ts`
- `src/collections/CampaignUser.ts`
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 74ce8402..24356a31 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -110,7 +110,7 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig
| A3/A4 Baseline TSE 2022 + Gap vs 2022 | `Baseline-Eleitoral-2022` (+ bloco "Baseline 2022" em `Lista-Nucleos-Overview`) | [baseline-eleitoral-tse.md](plans/baseline-eleitoral-tse.md) |
| A5 Insights (5 planos) | `Baseline-Eleitoral-2022` (card "Insights do território", uma linha por insight) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) (Fase 1 conversão da base + Fase 2 virada: vencedor dir. **ou** share dir. alto) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) |
| A7 Escala e DRY pós-A4 | — (sem design novo; reusa baseline A4 + Alert/Progress) | [escala-dry-pos-a4.md](plans/escala-dry-pos-a4.md) |
-| B6 Escala e DRY pós-B3 | — (sem design novo; reusa mapa B3) | [escala-dry-pos-b3.md](plans/escala-dry-pos-b3.md) |
+| B6 Escala e DRY pós-B3 | — (sem design novo; reusa mapa B3) | [escala-dry-pos-b3.md](plans/escala-dry-pos-b3.md) |
| A8 Perfis do eleitorado (IBGE + manuais) | — (sem design nesta leva; reusa aba Eleitorado + `NucleusIntelligenceDialog`) | [perfil-eleitorado-ibge.md](plans/perfil-eleitorado-ibge.md) |
| C2 Cadastro nominal de apoiadores | `Apoiadores-Lista`, `Apoiador-Ficha`, `Importar-CSV-Apoiadores` | [cadastro-nominal-apoiadores.md](plans/cadastro-nominal-apoiadores.md) |
| C6 Escala e DRY pós-C2 | — (sem design novo; reusa os de C2 + padrões de núcleos) | [escala-dry-pos-c2.md](plans/escala-dry-pos-c2.md) |
@@ -122,12 +122,12 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig
| C11 Escala e DRY pós-C7 | — (sem design novo; reusa os de C3 + padrões de núcleos/C6/C9) | [escala-dry-pos-c7.md](plans/escala-dry-pos-c7.md) |
| O0+ Escala e DRY pós-Onda 0 | — (sem design novo; reusa `/privacidade` + padrão revalidate `posts`) | [escala-dry-pos-onda0.md](plans/escala-dry-pos-onda0.md) |
| VR+ Escala e DRY pós-visitados | — (sem design novo; reusa dashboard + padrões de lista de núcleos) | [escala-dry-pos-visitados-recentemente.md](plans/escala-dry-pos-visitados-recentemente.md) |
-| RS+ Escala e DRY pós-reset senha/perfil | — (sem design novo; reusa login + perfil + padrões de form de campanha) | [escala-dry-pos-reset-senha-perfil.md](plans/escala-dry-pos-reset-senha-perfil.md) |
-| FD+ Escala e DRY pós-Field Desk | — (sem design novo; reusa shells/strips Field Desk) | [escala-dry-pos-field-desk.md](plans/escala-dry-pos-field-desk.md) |
-| FD2 UX Field Desk pós-critique | — (critique Impeccable; reusa dashboard + badges) | [field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.md) |
+| RS+ Escala e DRY pós-reset senha/perfil | — (sem design novo; reusa login + perfil + padrões de form de campanha) | [escala-dry-pos-reset-senha-perfil.md](plans/escala-dry-pos-reset-senha-perfil.md) |
+| FD+ Escala e DRY pós-Field Desk | — (sem design novo; reusa shells/strips Field Desk) | [escala-dry-pos-field-desk.md](plans/escala-dry-pos-field-desk.md) |
+| FD2 UX Field Desk pós-critique | — (critique Impeccable; reusa dashboard + badges) | [field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.md) |
| B5 Escala e DRY pós-B2 | — (sem design novo; reusa geometrias B2 + padrões de scripts CLI) | [escala-dry-pos-b2.md](plans/escala-dry-pos-b2.md) |
| E6 Escala e DRY pós-E1+E3 | — (sem design novo; reusa Metas 2026 + overview B1) | [escala-dry-pos-e1.md](plans/escala-dry-pos-e1.md) |
-| E7 Escala e DRY pós-E2 | — (sem design novo; reusa Baseline + Insights + overview tendência/conversão) | [escala-dry-pos-e2.md](plans/escala-dry-pos-e2.md) (Fase 2 ✓; Fases 1, 3, 4 pendentes) |
+| E7 Escala e DRY pós-E2 | — (sem design novo; reusa Baseline + Insights + overview tendência/conversão) | [escala-dry-pos-e2.md](plans/escala-dry-pos-e2.md) (Fase 2 ✓; Fases 1, 3, 4 pendentes) |
| D2 Notificações (sino) | `Notificacoes-PWA` (central de notificações) | [notifications.md](plans/notifications.md) |
| D1 PWA `/campanha` | `Notificacoes-PWA` (banner "Instale o app da campanha") | [pwa-campanha.md](plans/pwa-campanha.md) |
| C5 Operação dia D / GOTV | `Dia-D-GOTV` | sem plano ainda — o design é a primeira materialização do item (check-off de apoiadores por seção/zona, progresso "62% da base", filtro "Ainda não confirmados", estado "Sincronizado" offline-tolerante) |
@@ -266,36 +266,36 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): **O0+** (ap
**Janela 2 — 05/08 → 16/08 (pré-propaganda): base nominal + inteligência + agenda prontas para o arranque.**
-| Ordem | Item | Plano | Depende de | Paralelizável com |
-| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------------- | ------------------------------------- |
-| 7 | C2 Cadastro nominal de apoiadores _(engenharia pronta e mesclada em `main` 2026-07-18 — deploy + Consent keys ainda pendentes)_ | [detalhes](plans/cadastro-nominal-apoiadores.md) | produção: lote jurídico (Onda 0) | A4, C3, C6, C7 |
-| 8 | C6 Escala e DRY pós-C2 (import em massa, KPI, shells compartilhados com núcleos) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–5 + simplify)** | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7, C8 |
-| 9 | A4 Baseline no produto + insight Gap vs 2022 **(implementado e mesclado em `main` 2026-07-18 — Fases 2–4 + simplify)** | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7, C8 |
-| 10 | C3 Eventos / agenda de mobilização (`actionPlan`) **(implementado e mesclado em `main` 2026-07-18)** | [detalhes](plans/eventos-agenda-mobilizacao.md) | A1 | C2, A4, C6, C7, C8 |
-| 11 | C7 Escala e DRY pós-C3 **(entregue 2026-07-19 — Fases 1–5; feed O(n) condicional fora de escopo)** | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6, C8, E1+E3, E2 |
-| 12 | C8 Escala e DRY pós-C6 (locks bulk em 1 RT, leituras/`pg_trgm`, helpers drizzle, forms DRY) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c6.md) | C6 (merge) | A4, C3, C7, E1+E3, E2, C9 |
-| 13 | C9 Escala e DRY pós-C8 (filtro unificado Payload↔SQL, `contactSearchQuery` no aggregate, loader único `/apoiadores`, forms restantes) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c8.md) | C8 (merge) | A4, C3, C7, E1+E3, E2, C10 |
-| 14 | C10 Escala e DRY pós-C9 (dedup access lista, scope único de núcleos do coordenador, `errorProps` nos forms) _(cortável se a base nominal permanecer pequena; Fase 3 DRY pode entrar isolada)_ | [detalhes](plans/escala-dry-pos-c9.md) | C9 (merge) | A4, C3, C7, E1+E3, E2, C11 |
-| 15 | C11 Escala e DRY pós-C7 (feed O(n) condicional, loaders do detalhe, selects por aba, hot paths mutação/form, utilities de busca) _(cortável se a agenda permanecer pequena; Fase 1 migration só com volume medido)_ | [detalhes](plans/escala-dry-pos-c7.md) | C7 (merge) | A4, C3, C10, E1+E3, E2 |
-| 16 | E1 Metas em cenários + prioridade + E3 dobradinhas/encaminhamentos manuais **(implementado e mesclado em `main` 2026-07-19 — migration `20260719_054522`; `/simplify` → E6)** | [detalhes](plans/mapa-projecao-municipios.md) | — | tudo da janela |
-| 17 | E6 Escala e DRY pós-E1+E3 (aggregate SQL metas/prioridade, geografia baseline, DRY formatação) _(cortável se poucos núcleos ativos)_ | [detalhes](plans/escala-dry-pos-e1.md) | E1+E3 ✓ | E2, C10, A7, E4 |
-| 18 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) **(implementado 2026-07-19; `/simplify` → E7)** | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, E6, E7, C7, C8, C9, C10, C11 |
-| 19 | E7 Escala e DRY pós-E2 (trend VM único no detalhe, int `loadNucleusListElectionOverview`, tipo `VoteTrendDistribution`, helpers wipe eleitoral nos int tests) _(Fase 2 int entregue 2026-07-19; Fases 1, 3 e 4 pendentes; F4 cortável — só DX de teste)_ | [detalhes](plans/escala-dry-pos-e2.md) | E2 (merge) | E6, A7, E4, C10 |
+| Ordem | Item | Plano | Depende de | Paralelizável com |
+| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------------- | ----------------------------------- |
+| 7 | C2 Cadastro nominal de apoiadores _(engenharia pronta e mesclada em `main` 2026-07-18 — deploy + Consent keys ainda pendentes)_ | [detalhes](plans/cadastro-nominal-apoiadores.md) | produção: lote jurídico (Onda 0) | A4, C3, C6, C7 |
+| 8 | C6 Escala e DRY pós-C2 (import em massa, KPI, shells compartilhados com núcleos) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–5 + simplify)** | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7, C8 |
+| 9 | A4 Baseline no produto + insight Gap vs 2022 **(implementado e mesclado em `main` 2026-07-18 — Fases 2–4 + simplify)** | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7, C8 |
+| 10 | C3 Eventos / agenda de mobilização (`actionPlan`) **(implementado e mesclado em `main` 2026-07-18)** | [detalhes](plans/eventos-agenda-mobilizacao.md) | A1 | C2, A4, C6, C7, C8 |
+| 11 | C7 Escala e DRY pós-C3 **(entregue 2026-07-19 — Fases 1–5; feed O(n) condicional fora de escopo)** | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6, C8, E1+E3, E2 |
+| 12 | C8 Escala e DRY pós-C6 (locks bulk em 1 RT, leituras/`pg_trgm`, helpers drizzle, forms DRY) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c6.md) | C6 (merge) | A4, C3, C7, E1+E3, E2, C9 |
+| 13 | C9 Escala e DRY pós-C8 (filtro unificado Payload↔SQL, `contactSearchQuery` no aggregate, loader único `/apoiadores`, forms restantes) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c8.md) | C8 (merge) | A4, C3, C7, E1+E3, E2, C10 |
+| 14 | C10 Escala e DRY pós-C9 (dedup access lista, scope único de núcleos do coordenador, `errorProps` nos forms) _(cortável se a base nominal permanecer pequena; Fase 3 DRY pode entrar isolada)_ | [detalhes](plans/escala-dry-pos-c9.md) | C9 (merge) | A4, C3, C7, E1+E3, E2, C11 |
+| 15 | C11 Escala e DRY pós-C7 (feed O(n) condicional, loaders do detalhe, selects por aba, hot paths mutação/form, utilities de busca) _(cortável se a agenda permanecer pequena; Fase 1 migration só com volume medido)_ | [detalhes](plans/escala-dry-pos-c7.md) | C7 (merge) | A4, C3, C10, E1+E3, E2 |
+| 16 | E1 Metas em cenários + prioridade + E3 dobradinhas/encaminhamentos manuais **(implementado e mesclado em `main` 2026-07-19 — migration `20260719_054522`; `/simplify` → E6)** | [detalhes](plans/mapa-projecao-municipios.md) | — | tudo da janela |
+| 17 | E6 Escala e DRY pós-E1+E3 (aggregate SQL metas/prioridade, geografia baseline, DRY formatação) _(cortável se poucos núcleos ativos)_ | [detalhes](plans/escala-dry-pos-e1.md) | E1+E3 ✓ | E2, C10, A7, E4 |
+| 18 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) **(implementado 2026-07-19; `/simplify` → E7)** | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, E6, E7, C7, C8, C9, C10, C11 |
+| 19 | E7 Escala e DRY pós-E2 (trend VM único no detalhe, int `loadNucleusListElectionOverview`, tipo `VoteTrendDistribution`, helpers wipe eleitoral nos int tests) _(Fase 2 int entregue 2026-07-19; Fases 1, 3 e 4 pendentes; F4 cortável — só DX de teste)_ | [detalhes](plans/escala-dry-pos-e2.md) | E2 (merge) | E6, A7, E4, C10 |
**Janela 3 — 16/08 → set (campanha de rua): inteligência ampliada, visualização e engajamento.**
-| Ordem | Item | Plano | Depende de | Paralelizável com |
-| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------- |
-| 18 | E4 Import único da planilha (`pnpm db:seed:mapa`; sem criar `Contact`/`leadership`) | [detalhes](plans/mapa-projecao-municipios.md) | E1 ✓ + E3 ✓ (suave: E2 ✓ — tendência visível na revisão; E6 F1 — perf da lista) | A5, A7, A8, B3, B5, C4, D2 |
-| 19 | A5 Insights derivados do baseline (5 itens, paralelizáveis entre si; conversão e classificação exigem limiares de produto; alavancagem inclui Fase 2 virada por vencedor **ou** share da direita) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | A4 | A7, A8, B3, B5, C4, E4, D1 |
-| 20 | A8 Perfis do eleitorado — perfil médio IBGE (Censo) + manuais na aba Eleitorado _(cortável: manuais já existem)_ | [detalhes](plans/perfil-eleitorado-ibge.md) | A1 ✓ (suave: B2 ✓ — `codarea`) | A5, A7, B3, B5, C4, E4, D1 |
-| 21 | A7 Escala e DRY pós-A4 (`cityCode` TSE, Alert `confirmed` + helpers gap + `Progress`, fetch único 2022 lista+coroplético, batch federal flip/leverage na lista) _(Fase 1 agregar federal entregue 2026-07-19; Fases 2–5 pendentes; F4 pós-B3; F5 pós-A5 via `capture-review-debts`)_ | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, A8, B3, B5, B6, C4, E4, D1 |
-| 22 | B2 Mapa Fase 1 (geometrias) **(entregue 2026-07-18)** + B3 Leaflet nas superfícies | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1 ✓ + B2 ✓ (suave: A4 coroplético; E1/E2 métricas; B5 F1 lazy geometrias) | A5, A7, A8, B5, B6, C4, E4, D1 |
-| 23 | B5 Escala e DRY pós-B2 (cache CLI + factory mun/TI; F1 lazy ✓) | [detalhes](plans/escala-dry-pos-b2.md) | B2 ✓ (F1 entregue com B3) | A5, A7, A8, B3, B6, C4, E4, D1 |
-| 24 | B6 Escala e DRY pós-B3 (`BahiaMap` setStyle incremental) _(cortável se troca de métrica for rara)_ | [detalhes](plans/escala-dry-pos-b3.md) | B3 (suave: A7 F4 na lista) | A5, A7, A8, B5, C4, E4, D1 |
-| 25 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, A7, A8, B3, B5, B6, E4, D1 |
-| 26 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo |
-| 27 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, A7, A8, B3, B5, B6, C4, E4 |
+| Ordem | Item | Plano | Depende de | Paralelizável com |
+| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------ |
+| 18 | E4 Import único da planilha (`pnpm db:seed:mapa`; sem criar `Contact`/`leadership`) | [detalhes](plans/mapa-projecao-municipios.md) | E1 ✓ + E3 ✓ (suave: E2 ✓ — tendência visível na revisão; E6 F1 — perf da lista) | A5, A7, A8, B3, B5, C4, D2 |
+| 19 | A5 Insights derivados do baseline (5 itens, paralelizáveis entre si; conversão e classificação exigem limiares de produto; alavancagem inclui Fase 2 virada por vencedor **ou** share da direita) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | A4 | A7, A8, B3, B5, C4, E4, D1 |
+| 20 | A8 Perfis do eleitorado — perfil médio IBGE (Censo) + manuais na aba Eleitorado _(cortável: manuais já existem)_ | [detalhes](plans/perfil-eleitorado-ibge.md) | A1 ✓ (suave: B2 ✓ — `codarea`) | A5, A7, B3, B5, C4, E4, D1 |
+| 21 | A7 Escala e DRY pós-A4 (`cityCode` TSE, Alert `confirmed` + helpers gap + `Progress`, fetch único 2022 lista+coroplético, batch federal flip/leverage na lista) _(Fase 1 agregar federal entregue 2026-07-19; Fases 2–5 pendentes; F4 pós-B3; F5 pós-A5 via `capture-review-debts`)_ | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, A8, B3, B5, B6, C4, E4, D1 |
+| 22 | B2 Mapa Fase 1 (geometrias) **(entregue 2026-07-18)** + B3 Leaflet nas superfícies | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1 ✓ + B2 ✓ (suave: A4 coroplético; E1/E2 métricas; B5 F1 lazy geometrias) | A5, A7, A8, B5, B6, C4, E4, D1 |
+| 23 | B5 Escala e DRY pós-B2 (cache CLI + factory mun/TI; F1 lazy ✓) | [detalhes](plans/escala-dry-pos-b2.md) | B2 ✓ (F1 entregue com B3) | A5, A7, A8, B3, B6, C4, E4, D1 |
+| 24 | B6 Escala e DRY pós-B3 (`BahiaMap` setStyle incremental) _(cortável se troca de métrica for rara)_ | [detalhes](plans/escala-dry-pos-b3.md) | B3 (suave: A7 F4 na lista) | A5, A7, A8, B5, C4, E4, D1 |
+| 25 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, A7, A8, B3, B5, B6, E4, D1 |
+| 26 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo |
+| 27 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, A7, A8, B3, B5, B6, C4, E4 |
**Janela 4 — set → 04/10 (reta final): dobradinha, dia D e estabilização.**
@@ -312,7 +312,7 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): **O0+** (ap
- **Visitados recentemente** ✓ (client-side, `localStorage`; MVP entregue 2026-07-19). → [detalhes](plans/visitados-recentemente.md)
- **VR+ Escala e DRY pós-visitados recentemente** (refresh bfcache/storage, DRY tempo relativo, `CampaignLinkListRow`, shell do dashboard) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-visitados-recentemente.md)
- **Reset de senha self-service + foto de perfil** ✓ (Resend + `/campanha/perfil`; MVP entregue 2026-07-19). → [detalhes](plans/reset-senha-foto-perfil.md)
-- **RS+ Escala e DRY pós-reset senha/perfil** (auth read `depth: 0`, avatar fora da TX, **`CampaignAuthPageShell` ✓ parcial**, schemas de senha unificados, login em `CampaignFormActionState`) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-reset-senha-perfil.md)
+- **RS+ Escala e DRY pós-reset senha/perfil** (auth read `depth: 0`, avatar fora da TX, **auth UI parcial ✓** — shell/card/logo + critique login fechados; pendem `CampaignPasswordFields` + login em `CampaignFormActionState`) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-reset-senha-perfil.md)
- **Field Desk polish** (Impeccable quieter→polish; critique 28→32) — shells/strips/freshness/filas-first/Mais filtros/empties; em branch local 2026-07-19. → `PRODUCT.md` / `DESIGN.md`
- **FD+ Escala e DRY pós-Field Desk** (`CampaignPageShell` size, heading class, VoteGoals→MetricStrip, datas canônicas, NucleusFilters bag) — engenharia pós-polish; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-field-desk.md)
- **FD2 UX Field Desk pós-critique** (bulk na fila Sem coordenador, glossário inline, outline h2 Filas, empty coordenador+CTA, Sidebar motion cortável) — produto/UX pós-critique; _(validar prioridades com produto)_. → [detalhes](plans/field-desk-ux-pos-critique.md)
diff --git a/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx b/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx
index f33b31e0..e9197fa6 100644
--- a/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx
+++ b/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx
@@ -1,17 +1,18 @@
'use client'
-import Link from 'next/link'
import { useActionState } from 'react'
import { requestCampaignPasswordResetFormAction } from '@/app/(campaign)/campanha/actions/password'
+import { CampaignAuthBackToLoginLink } from '@/components/campaign/CampaignAuthBackToLoginLink'
+import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader'
import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card'
+import { Card, CardContent } from '@/components/ui/card'
import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Spinner } from '@/components/ui/Spinner'
import {
CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT,
- campaignAuthHeadingClassName,
+ campaignAuthMutedTextClassName,
} from '@/lib/campaignAuthCopy'
import type { CampaignFormActionState } from '@/utilities/campaignFormActionError'
import { fieldError } from '@/utilities/campaignFormFields'
@@ -21,20 +22,21 @@ export const ForgotPasswordForm = () => {
requestCampaignPasswordResetFormAction,
{} satisfies CampaignFormActionState,
)
+ const emailError = fieldError(state.fieldErrors, 'email')
return (
Esqueceu a senha?
- Entrar na campanha
- Redefinir senha
- Link inválido
-