diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc
index 1183ddc0..861eba8e 100644
--- a/.cursor/rules/projects/nucleos-eleitorais.mdc
+++ b/.cursor/rules/projects/nucleos-eleitorais.mdc
@@ -7,6 +7,7 @@ alwaysApply: false
## Status
+- **B44 entregue em código (2026-07-29):** primitivo UX-1 — `CampaignHomeActionButton` (círculo `bg-muted` + ícone + label; um só controle focável) e `CampaignHomeActionStrip` (`overflow-x-auto`, `touch-pan-x`, snap). Descrição: Tooltip shadcn em ponteiro fino; long-press (~450ms) abre Drawer em ponteiro grosso (`useSyncExternalStore` + `matchMedia`, um alvo no DOM) sem disparar click (`src/lib/campaignLongPress.ts`). **Sem** montar catálogo em `/campanha` — B45. Specs: `campaignLongPress.unit.spec.tsx`, `campaignHomeActionButton.unit.spec.tsx`. Plano: [`botao-acao-inicio-strip.md`](../../../docs/plans/botao-acao-inicio-strip.md). Desbloqueia **B45**.
- **B43 entregue em código (2026-07-29):** chassis UX-1 — `/campanha` fica só shell (`CampaignPageShell` vazio); dashboard staff move para `/campanha/quadro` (corpo idêntico ao antigo Início); `LeaderContactsPanel` em `/campanha/contatos`; nav staff ganha **Quadro** (sidebar; fora do bottom nav mobile); liderança **Início** + **Meus contatos**; `requireCampaignPageActor` redireciona leader bloqueado para `/campanha/contatos` (não para o Início blank). Sem migration. Plano: [`inicio-em-branco-quadro.md`](../../../docs/plans/inicio-em-branco-quadro.md). Desbloqueia **B44** / **B45**.
- **Sessão observada CG 2026-07-29 (Bloco B):** 1º hands-on ~54 min. Achados U1–U8 / O9–O11 — não rola; mental model **Ação→Local→Quem**; confirmações invisíveis; forms precisam ser contínuos; hit = lista sem assessor × votos 2022; mapa ≠ mesa de trabalho. Pivot: tabelas=bulk; operação=fluxos lineares tipo Nubank. Snapshot [`sessao-observada-coordenador-2026-07-29-snapshot.md`](../../../docs/plans/sessao-observada-coordenador-2026-07-29-snapshot.md); CUSTOMER.md/OST atualizados. Prazo soft: manejável até reunião assessoria **03/08**. **Chassis:** ~~B43~~ ✓ → **B44** botão/strip → **B45** catálogo por persona (wizards depois).
- **E12+ entregue em código (2026-07-29):** escala/DRY pós-E12 — F1 forçada: `src/lib/metropolitanoTerritoryPeers.ts` unifica constantes, filtros Salvador vs demais RMS e `catalogPeersForSlug` (rollup E17/E12, `slugsForMetropolitanoSubRow`, benchmark T4). F2 não entrou — `benchmarkBySlug` já memoiza por slug. Spec `metropolitanoTerritoryPeers.unit.spec.ts`. Sem migration/UI. Plano: [`escala-dry-pos-e12.md`](../../../docs/plans/escala-dry-pos-e12.md).
diff --git a/docs/plans/botao-acao-inicio-strip.md b/docs/plans/botao-acao-inicio-strip.md
index 214e700a..fb656723 100644
--- a/docs/plans/botao-acao-inicio-strip.md
+++ b/docs/plans/botao-acao-inicio-strip.md
@@ -1,7 +1,7 @@
# Botão de ação do Início + lista horizontal
-Status: rascunho
-Atualizado em: 2026-07-29
+Status: entregue (2026-07-29)
+Atualizado em: 2026-07-29 — as-built: `CampaignHomeActionButton` + `CampaignHomeActionStrip` + `useCampaignLongPress`; Tooltip (fine) / long-press Drawer (coarse via `matchMedia`); sem mount no Início (B45).
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B44 — chassis UX-1)
Impeccable: C — primitivo UI novo (botão circular + strip rolável) no Início blank
Appetite: ~1 dia eng; 2 componentes client + long-press + Tooltip/Drawer; sem migration
@@ -94,6 +94,16 @@ Componentes:
- **Drag-to-scroll no desktop.** Revisitar se CG não descobrir overflow na strip.
- **Fade edges** na strip. Revisitar no critique Impeccable se o corte visual mentir “acabou”.
+- **Lazy mount do Drawer** (só no primeiro long-press). Trade-off B34/B42: Drawer coarse montado por ação com `description`; revisitar se o Início (B45) com 6 ações perfilar DOM pesado.
+- **Shell read-only “info drawer”** compartilhado com `CampaignCellEditOverlay` / `InstallPwaToast`. Gatilho: **2º** call site idêntico (título + descrição + Fechar).
+- **`children` da strip sem `
`.** Documentar ou validar quando B45 montar ações custom.
+- **Mover `useCampaignLongPress` para fora de `lib/`.** Gatilho: 2º call site idêntico (plano já prevê extrair no 2º).
+
+## Pós-simplify (2026-07-29, não reabrir)
+
+**Já resolvido na entrega:** timer long-press com cleanup no unmount; `aria-describedby` só com drawer aberto; `useCoarsePointer` com um listener `matchMedia` compartilhado; `href` sem `onClick` no hook (navegação = tap curto); Drawer omitido quando `disabled`; keys `href ?? label`; specs duplicadas `.ts` removidas; testes coarse long-press + unmount.
+
+**Descartado (score baixo):** constante de classe Tooltip compartilhada com `CampaignHoverTooltip`; e2e de scroll do `body` além de smoke de classes na strip.
## Referências
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 57627a5f..da5984aa 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
# Roadmap — Teqo
-Atualizado em: 2026-07-29 — B43 entregue (chassis UX-1); B44–B45 pendentes.
+Atualizado em: 2026-07-29 — B44 entregue (botão/strip UX-1); B45 pendente.
Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc).
## Âncoras do calendário eleitoral 2026 (Res. TSE 23.760/2026)
@@ -83,9 +83,9 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re
### Demais itens abertos
-- **UX-1 fluxos ação-primeiro (prioridade pós-sessão 29/07)** — evidência: [snapshot](plans/sessao-observada-coordenador-2026-07-29-snapshot.md) · O9–O11 em CUSTOMER.md · **rascunho:** [fluxos-acao-primeiro-inicio.md](plans/fluxos-acao-primeiro-inicio.md). Soft deadline: **03/08**. **Chassis:** ~~**B43**~~ ✓ → **B44** → **B45** (botão/strip + catálogo; wizards com escrita = fatias seguintes).
+- **UX-1 fluxos ação-primeiro (prioridade pós-sessão 29/07)** — evidência: [snapshot](plans/sessao-observada-coordenador-2026-07-29-snapshot.md) · O9–O11 em CUSTOMER.md · **rascunho:** [fluxos-acao-primeiro-inicio.md](plans/fluxos-acao-primeiro-inicio.md). Soft deadline: **03/08**. **Chassis:** ~~**B43**~~ ✓ → ~~**B44**~~ ✓ → **B45** (catálogo por persona; wizards com escrita = fatias seguintes).
- ~~**B43** Início em branco + Quadro~~ — **entregue 2026-07-29** (`/campanha` blank; staff dashboard em `/campanha/quadro`; liderança em `/campanha/contatos`; nav Início+Quadro / Início+Meus contatos; `requireCampaignPageActor` manda leader bloqueado → `/campanha/contatos`; sem migration) · [plano](plans/inicio-em-branco-quadro.md)
-- **B44** Botão de ação do Início + lista horizontal — círculo `muted` + ícone + label; strip com scroll interno; descrição opcional Tooltip (fine) / Drawer via long-press (coarse; long-press ≠ click). Depende de B43 · ~1d · Janela 1 · Impeccable C · [plano](plans/botao-acao-inicio-strip.md)
+- ~~**B44** Botão de ação do Início + lista horizontal~~ — **entregue 2026-07-29** (`CampaignHomeActionButton` + `CampaignHomeActionStrip`; Tooltip fine / long-press Drawer coarse; sem mount no Início — B45) · ~1d · Janela 1 · Impeccable C · [plano](plans/botao-acao-inicio-strip.md)
- **B45** Catálogo de ações por persona — CG/candidato e assessor (6 ações; `uncovered-municipalities` já navega); liderança (2; `my-contacts` → `/campanha/contatos`); resto inerte até wizards. Depende de B44 · ~0,5d · Janela 1 · Impeccable B · [plano](plans/catalogo-acoes-inicio-por-persona.md)
- ~~**E4R** import único da planilha de projeção~~ — **entregue 2026-07-24** (`pnpm db:seed:projecao`; overwrite-always; [plano](plans/import-planilha-projecao.md))
- ~~**A11** posição em votos do município (rank absoluto + % da própria votação) + ordenação da lista por votação~~ — **entregue 2026-07-24** (helper `municipalityVoteRank`; baseline + coluna lista; default `?sort=votos` desc; header=`2022`; consome contrato B15) · [plano](plans/ranking-votos-municipio.md)
@@ -188,7 +188,7 @@ flowchart TD
B41n["B41 ✓ Scroll horizontal + sticky Município"]
B42n["B42 ✓ Polimento mobile lista municípios"]
B43n["B43 ✓ Início em branco + Quadro / Contatos"]
- B44n["B44 Botão ação + strip horizontal"]
+ B44n["B44 ✓ Botão ação + strip horizontal"]
B45n["B45 Catálogo ações por persona"]
B15n["B15 ✓ Ordenar lista por coluna"]
B16n["B16 ✓ Filtros no header da lista"]
diff --git a/src/components/campaign/dashboard/CampaignHomeActionButton.tsx b/src/components/campaign/dashboard/CampaignHomeActionButton.tsx
new file mode 100644
index 00000000..accc28b5
--- /dev/null
+++ b/src/components/campaign/dashboard/CampaignHomeActionButton.tsx
@@ -0,0 +1,164 @@
+'use client'
+
+import type { LucideIcon } from 'lucide-react'
+import Link from 'next/link'
+import { useCallback, useId, useRef, useState, type MouseEvent, type ReactNode } from 'react'
+
+import { Button } from '@/components/ui/button'
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+} from '@/components/ui/Drawer'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
+import { useCoarsePointer } from '@/lib/campaignCoarsePointer'
+import { useCampaignLongPress } from '@/lib/campaignLongPress'
+
+export type CampaignHomeActionButtonProps = {
+ label: string
+ icon: LucideIcon
+ description?: string
+ href?: string
+ onClick?: () => void
+ disabled?: boolean
+}
+
+const actionControlClassName =
+ 'group flex min-h-11 w-[4.75rem] shrink-0 snap-start flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50'
+
+const ActionVisual = ({ label, icon: Icon }: { label: string; icon: LucideIcon }) => (
+ <>
+
+
+
+
+ {label}
+
+ >
+)
+
+export const CampaignHomeActionButton = ({
+ label,
+ icon,
+ description,
+ href,
+ onClick,
+ disabled = false,
+}: CampaignHomeActionButtonProps) => {
+ const isCoarsePointer = useCoarsePointer()
+ const [drawerOpen, setDrawerOpen] = useState(false)
+ const descriptionId = useId()
+ const titleRef = useRef(null)
+
+ const openDescription = useCallback(() => {
+ if (description) setDrawerOpen(true)
+ }, [description])
+
+ const fireClick = useCallback(
+ (_event: MouseEvent) => {
+ onClick?.()
+ },
+ [onClick],
+ )
+
+ const useLongPress = Boolean(description) && isCoarsePointer && !disabled
+ const longPress = useCampaignLongPress({
+ enabled: useLongPress,
+ onLongPress: openDescription,
+ onClick: href ? undefined : onClick ? fireClick : undefined,
+ })
+
+ const pointerHandlers = useLongPress ? longPress : undefined
+ const className = actionControlClassName
+ const aria = {
+ 'aria-label': label,
+ 'aria-describedby': drawerOpen && useLongPress ? descriptionId : undefined,
+ }
+
+ const pointerProps = pointerHandlers
+ ? {
+ onPointerDown: pointerHandlers.onPointerDown,
+ onPointerMove: pointerHandlers.onPointerMove,
+ onPointerUp: pointerHandlers.onPointerUp,
+ onPointerCancel: pointerHandlers.onPointerCancel,
+ }
+ : undefined
+
+ let control: ReactNode
+
+ if (disabled) {
+ control = (
+
+
+
+ )
+ } else if (href) {
+ control = (
+
+
+
+ )
+ } else {
+ control = (
+
+ )
+ }
+
+ const withTooltip =
+ description && !isCoarsePointer && !disabled ? (
+
+ {control}
+
+ {description}
+
+
+ ) : (
+ control
+ )
+
+ return (
+ <>
+ {withTooltip}
+ {description && isCoarsePointer && !disabled ? (
+
+
+
+
+ {label}
+
+ {description}
+
+
+ }
+ >
+ Fechar
+
+
+
+
+ ) : null}
+ >
+ )
+}
diff --git a/src/components/campaign/dashboard/CampaignHomeActionStrip.tsx b/src/components/campaign/dashboard/CampaignHomeActionStrip.tsx
new file mode 100644
index 00000000..b8441b32
--- /dev/null
+++ b/src/components/campaign/dashboard/CampaignHomeActionStrip.tsx
@@ -0,0 +1,41 @@
+'use client'
+
+import type { ReactNode } from 'react'
+
+import {
+ CampaignHomeActionButton,
+ type CampaignHomeActionButtonProps,
+} from '@/components/campaign/dashboard/CampaignHomeActionButton'
+import { TooltipProvider } from '@/components/ui/tooltip'
+import { cn } from '@/lib/utils'
+
+export const CampaignHomeActionStrip = ({
+ children,
+ actions,
+ ariaLabel = 'Ações rápidas',
+ className,
+}: {
+ children?: ReactNode
+ actions?: readonly CampaignHomeActionButtonProps[]
+ ariaLabel?: string
+ className?: string
+}) => (
+
+