diff --git a/docs/changelog/2026-09-12-c148.md b/docs/changelog/2026-09-12-c148.md
new file mode 100644
index 00000000..736ea1ea
--- /dev/null
+++ b/docs/changelog/2026-09-12-c148.md
@@ -0,0 +1 @@
+**C148 — Agenda: modais usáveis no desktop (2026-09-12):** os três modais da agenda (`/campanha/atividades` e `/campanha/agenda`) param de brigar com a janela no desktop. O overlay de criação/edição, o dialog de sync Google e o dialog do feed ICS passam a espelhar as variantes `sm:` da base `ui/dialog.tsx` (`sm:max-w-3xl`/`sm:max-w-xl` + `sm:p-0`) — antes os overrides sem variante perdiam para `sm:max-w-md`/`sm:p-8`, e o modal abria com 28rem de largura e padding dobrado (as "bordas gigantes"). Os três ganham corpo rolável (`min-h-0 flex-1 overflow-y-auto`, `data-slot="dialog-scroll-body"`) com header e rodapé `shrink-0` (`data-slot="dialog-footer"`): o conteúdo nunca corta e as ações ficam sempre visíveis — o sync/feed no desktop ganhou rodapé fixo com as ações (o erro da ação fica junto delas no desktop; no mobile a composição antiga foi preservada). O par Início/Término do overlay deixa de empilhar: data+hora inline na mesma linha e selects de hora/minuto com `w-16` (o grid `sm:grid-cols-2` já existia). `ui/dialog.tsx` e os outros 7 consumidores ficam intocados; o contrato mobile (Drawer/`DrawerFooter`) não muda. Testes: asserções estruturais nos units dos três dialogs (conteúdo no corpo, ações no rodapé) e nos e2e (`campaignActivity` mede largura > 600 e o par na mesma linha; `campaignAgendaGoogleSync` encolhe a janela para 560px e prova scroll interno + rodapé visível). O `E2E_AFFECTED_MANIFEST` passa a acordar `campaignAgendaGoogleSync` e `campaignAgendaFeed` para mudanças em `src/components/campaign/activity`. Sem migration, sem access/Consent.
diff --git a/docs/plans/c148-modais-agenda-desktop-impl.md b/docs/plans/c148-modais-agenda-desktop-impl.md
new file mode 100644
index 00000000..079c6562
--- /dev/null
+++ b/docs/plans/c148-modais-agenda-desktop-impl.md
@@ -0,0 +1,117 @@
+# Impl: Agenda: modais usáveis no desktop (criação de atividade, sync e feed)
+
+Status: rascunho
+Atualizado em: 2026-09-12
+Issue: #933
+Intenção: docs/plans/c148-modais-agenda-desktop.md
+Appetite restante: herdado — ~0,5–1 dia eng. Correção local de encaixe em 3 componentes + testes; sem migration, sem schema, sem access/Consent.
+
+## Leitura da intenção
+
+- **Outcome:** no desktop, os três modais da agenda (criação/edição, sync Google, feed ICS) abrem com largura/padding coerentes, nunca cortam conteúdo (corpo rola internamente com o rodapé de ações sempre visível) e o par Início/Término do modal de criação fica lado a lado com data+hora na mesma linha. Mobile intocado.
+- **O que NÃO negociar:** (1) mobile dos três modais não regride — o DOM do Drawer/folha atual (overlay `h-[calc(100dvh-1rem)]` + corpo `min-h-0 flex-1 overflow-y-auto`; sync/feed `max-h-[85vh]` + corpo rolável + `DrawerFooter`) permanece exatamente o de hoje; (2) lógica de validação/salvamento e dados intocados; (3) nenhum outro modal do app muda — `ui/dialog.tsx` e seus 10 consumidores intactos; (4) sem migration/Consent (dado interno de staff, `activity` não tem Consent).
+- **O que reavaliar:** a hipótese central da intenção está **confirmada**, com mecânica precisa: `cn` = clsx + tailwind-merge resolve conflito de mesma variante (`grid`→`flex`, `p-6`→`p-0`, `gap-4`→`gap-0`, `max-w-sm`→`max-w-2xl`), mas **não** derruba `sm:max-w-md`/`sm:p-8` da base quando o override não tem `sm:`; no CSS do Tailwind v4 a utility com variante é emitida depois e vence em ≥640px. Correção = espelhar a variante (`sm:max-w-3xl sm:p-0`), deixando o twMerge apagar as da base — sem guerra de ordem. Dois defeitos vizinhos no mesmo caminho: `w-full` no overlay anula o gutter de 2rem da base (`w-[calc(100vw-2rem)]`), e `max-h-[calc(100dvh-14rem)]` é número mágico acoplado às alturas de header/footer. O "empilhamento" do par data/hora é sintoma da largura de 28rem (o grid já é `sm:grid-cols-2`; o componente já renderiza data+hora inline), não falta de estrutura — não há layout novo a inventar.
+- **Fora de escopo reafirmado:** a divergência de breakpoint (overlay 640 via `useNarrowMeasured(640)` em `ActivityCreateOverlayHost.tsx:35`/`ActivityEditOverlayHost.tsx:34` vs sync/feed 768 via `useIsMobile()` em `src/hooks/use-mobile.ts:3`) **não** será unificada nesta entrega; as fronteiras atuais são preservadas.
+
+## Abordagem recomendada
+
+```mermaid
+flowchart LR
+ A[ActivityOverlay desktop] --> A1["DialogContent: sm:max-w-3xl sm:p-0
remove w-full e max-w-2xl"]
+ A1 --> A2["form min-h-0 flex-1 + corpo min-h-0 flex-1 overflow-y-auto
(morre max-h-[calc(100dvh-14rem)])"]
+ A2 --> A3["Início/Término em sm:grid-cols-2
data+hora inline por campo"]
+ B[Sync Google / Feed ICS desktop] --> B1["DialogContent: flex max-h-[calc(100dvh-2rem)]
flex-col p-0 sm:max-w-xl sm:p-0"]
+ B1 --> B2["header shrink-0 / corpo rolável /
footer shrink-0 com as ações"]
+ B2 --> B3["mobile: composição atual intacta
(body com ações + DrawerFooter)"]
+ C["ui/dialog.tsx"] -. "não tocar" .-> A
+ C -. "não tocar" .-> B
+```
+
+**Opções consideradas:** A) override local com variantes `sm:` explícitas nos 3 modais | B) mudar a base `ui/dialog.tsx` (largura/padding/scroll default) | C) extrair um shell compartilhado "modal desktop com corpo rolável" para os 3 call sites.
+**Recomendação:** A — cada modal já tem conteúdo, padding e largura próprios; a correção é de classe, e o padrão a replicar já existe no repo (QuickActions: `flex max-h-[min(85dvh,40rem)] … overflow-hidden` + filho `overflow-y-auto`, `CampaignQuickActionsOverlay.tsx:166-186`; Advisor: `max-h-[calc(100dvh-2rem)] overflow-y-auto`, `AdvisorPermissionBadge.tsx:86`). Zero superfície de regressão fora da agenda.
+**Rejeitadas:**
+
+- **B — mexer em `ui/dialog.tsx`:** base de 10 imports (`Command`, `PetitionSuccessDialog`, `CampaignNotificationBell`, `CampaignQuickActionsOverlay`, `MunicipalityV2StatusReasonDialog`, `CampaignUpdatesCreateModal`, `AdvisorPermissionBadge` + os 3 da agenda). Mudar `sm:max-w-md`/`sm:p-8`/scroll default é regressão silenciosa em telas não auditadas; a própria intenção marca como rabbit hole.
+- **C — shell compartilhado:** seria um pass-through raso (classes + 2 slots) para 3 modais heterogêneos (overlay `p-0` + cards; sync/feed padding próprio e `max-w-xl`), sem conhecimento encapsulado — depth check "pass-through raso → não criar". O repo já resolve isso por receita inline repetida (QuickActions, Advisor, sheet mobile). **Gatilho de revisitação:** 4º modal da agenda com o mesmo corpo rolável, ou adoção do padrão por outro domínio.
+
+### Decisões de engenharia
+
+1. **Onde a rolagem do overlay desktop acontece.** Opções: A) `form` `min-h-0 flex-1` + corpo `min-h-0 flex-1 overflow-y-auto`, herdando o `max-h` do `DialogContent` (padrão do sheet mobile e do QuickActions) | B) manter `max-h-[calc(100dvh-14rem)]` e só corrigir largura/padding. **Recomendação: A** — o magic number é a causa do corte quando header/footer mudam de altura; A torna a geometria derivada (`100dvh - 2rem` no content + header/footer `shrink-0`). **Rejeitada:** B porque preserva o acoplamento que gerou o defeito.
+2. **Sync/feed: rodapé fixo vs conteúdo inteiro rolável.** Opções: A) header/body/footer em flex, ações extraídas para footer `shrink-0` (draft cena 2; aceite exige "rodapé de ações permanece visível") | B) `overflow-y-auto` no `DialogContent` inteiro (padrão Advisor). **Recomendação: A** — só A satisfaz o aceite; B rola as ações para fora. **Rejeitada:** B (mais simples, porém falha o aceite explícito).
+3. **Disposição Início/Término (questão aberta de produto).** Opções: A) dois campos lado a lado, cada um data+hora inline (draft cena 1) | B) um campo por linha com data+hora inline. **Recomendação: A** — o grid `sm:grid-cols-2` (`ActivityOverlay.tsx:355`) já existe; com a largura recuperada cada coluna ganha ~20rem e o gatilho de data (~10rem) mostra `07/08/2026` sem truncar. **Rejeitada:** B (perde a largura recuperada; vira fallback). **Gatilho de revisitação:** se no gate a faixa 640–767px ficar ilegível, cair para B nessa faixa (não antes).
+4. **Larguras.** Overlay: `sm:max-w-3xl` (48rem, cena 1) vs `sm:max-w-2xl` (42rem, hipótese da intenção). Sync/feed: `sm:max-w-xl` (36rem, cena 2) vs manter `sm:max-w-lg` (32rem). **Recomendação: 48rem/36rem** — o rascunho UI é o gate aprovado; "largura generosa" e "ampliar pouco" estão materializados nas cenas. **Rejeitadas:** manter 42rem/32rem — a diferença é uma classe e o alvo está no gate.
+5. **Como preservar o mobile exatamente.** Opções: A) dividir o `body` em `content` + `actionRow` e recompor o ramo mobile na mesma ordem de hoje | B) footer desktop com `position: sticky` dentro do corpo atual. **Recomendação: A** — DOM mobile idêntico (os unit mobile e os e2e C103/C94/C114 são a prova), sem sticky frágil com padding negativo. **Rejeitada:** B (quebra com `space-y`/padding do content; menos explícito que o flex).
+
+### Componentes / mudanças
+
+- **`ActivityOverlay.tsx`** (só caminho desktop):
+ - `DialogContent` (`:601`): `flex max-h-[calc(100dvh-2rem)] w-full max-w-2xl flex-col gap-0 overflow-hidden p-0` → `flex max-h-[calc(100dvh-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-3xl sm:p-0` (remove `w-full` para voltar o gutter `w-[calc(100vw-2rem)]` da base; `sm:` espelhado derruba `sm:max-w-md`/`sm:p-8` via twMerge).
+ - `DialogHeader` desktop (`:602`): + `shrink-0` (mantém `border-b px-6 py-4`).
+ - `form` (`:442`): ramo desktop `flex flex-col` → `flex min-h-0 flex-1 flex-col` (igual ao sheet).
+ - corpo (`:451-456`): ramo desktop `max-h-[calc(100dvh-14rem)] flex-1 overflow-y-auto overscroll-contain p-6` → `min-h-0 flex-1 overflow-y-auto overscroll-contain p-6`; + `data-slot="dialog-scroll-body"`.
+ - rodapé (`:520-525`): classes inalteradas (`shrink-0` já existe); + `data-slot="dialog-footer"`.
+- **`GoogleCalendarSyncDialog.tsx`:**
+ - separar o `body` (`:186-286`) em `content` (sem as ações) + `actions` por estado (`not-configured` → nenhuma; `disabled` → Reativar; `synced`/`paused` → Abrir Google Calendar quando `paused` + Sincronizar agora + Desativar). `actionError` vai para o rodapé desktop (junto da ação que falhou) e **fica na posição atual no mobile** (entre content e actions).
+ - mobile (`:296-310`): composição idêntica à de hoje — `DrawerContent max-h-[85vh]` + `