diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md
index 8ba0d7f8..3fc1a2ba 100644
--- a/docs/CHANGELOG-AGENTS.md
+++ b/docs/CHANGELOG-AGENTS.md
@@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
---
+**Recently resolved (2026-08-05):** **B158** (Issue #364) — `/campanha/municipios` troca cards↔tabela e revela colunas pela largura do painel de conteúdo com named container queries (48–84rem), sem rolagem horizontal; Território virou sublinha de Município, Tendência/Sinal preservam um único editor na tabela quando ocultos manualmente, e `campaign_columns=municipios:__none__` diferencia “todas visíveis” dos defaults. E2E mede cada limiar em Chromium e mantém um caso iPad-sized no projeto WebKit focado. Sem migration ou mudança de RBAC. Intenção [`colunas-responsivas-municipios.md`](docs/plans/colunas-responsivas-municipios.md); impl [`colunas-responsivas-municipios-impl.md`](docs/plans/colunas-responsivas-municipios-impl.md).
+
**Recently resolved (2026-08-05):** **B159** (Issue #374) — as colunas Assessores, Lideranças e Dobradinhas de `/campanha/municipios` agora compartilham `MunicipalityRelationEditor` (`CampaignCellEditOverlay` + Command + chips + optimistic reconcile) e `MunicipalityRelationAvatarStack`; Dobradinhas deixa `RelationChipCell` só nesta superfície, preserva busca por partido e links para a ficha, e Lideranças cria inline somente com nome (`Contact.phone = null`, sem stub; telefone real depois na ficha). Sem migration, rota nova ou mudança de access. Intenção [`generalizar-colunas-relacao-municipios.md`](docs/plans/generalizar-colunas-relacao-municipios.md); impl [`generalizar-colunas-relacao-municipios-impl.md`](docs/plans/generalizar-colunas-relacao-municipios-impl.md).
**Recently resolved (2026-08-04):** **B155** (Issue #359) — coluna de lideranças na lista de municípios (espelho de Assessores): coluna "Lideranças" após "Assessores" (staff; leader fora), chips com nome do contato, popover/sheet com Command + delta otimista (seq/pending/rollback) e criação inline (mini-form Nome + Celular). **Sem migration**: leitura por query reversa de `leadership.municipalities` (batch no bundle, `loadMunicipalityLeadershipSummaries`) e escrita reusando B34 (`setLeadershipMunicipalitiesMembershipRecord`) + wizard create (`createLeadershipWizardRecord`) — fonte única de verdade, consistência automática com `/campanha/liderancas`. Rota nova `POST /campanha/municipios/leaderships` (`campaignJsonMutationRoute`, union toggle|create, safe-messages allowlist); provider bridge espelho do B154. Regra travada com o humano: **célula → rota, form → server action** (débito de migração das 3 anomalias — sinal da lista e colunas B31/B34 de `/campanha/liderancas`). Intenção [`liderancas-coluna-municipios.md`](docs/plans/liderancas-coluna-municipios.md); impl [`liderancas-coluna-municipios-impl.md`](docs/plans/liderancas-coluna-municipios-impl.md).
diff --git a/docs/plans/colunas-responsivas-municipios-impl.md b/docs/plans/colunas-responsivas-municipios-impl.md
new file mode 100644
index 00000000..191c72f4
--- /dev/null
+++ b/docs/plans/colunas-responsivas-municipios-impl.md
@@ -0,0 +1,190 @@
+# Impl: B158 — Colunas adaptativas na tabela de Municípios
+
+Status: aprovado
+Atualizado em: 2026-08-05
+Issue: #364
+Intenção: docs/plans/colunas-responsivas-municipios.md
+Appetite restante: ~1,5–2 dias eng; preservar o corte de um único outcome verificável
+
+## Leitura da intenção
+
+- **Outcome:** `/campanha/municipios` responde à largura real do painel de conteúdo: cards abaixo de 48rem; acima disso, uma tabela sem scroll horizontal que acrescenta contexto progressivamente, preserva preferência manual e mantém os quick edits acessíveis.
+- **O que NÃO negociar:** container query em vez de viewport/JavaScript; ordem C0–C6; `leader` continua fora da rota; Dobradinha só para coordinator/candidate; território continua filtrável e `sort=region` continua válido; nenhum editor duplicado entre coluna adaptativa e `actions`; nenhuma migration, collection, Consent, server action ou mudança de URL pública.
+- **O que reavaliar:** a intenção sugere um seam em `CampaignTable`, mas o código atual já aceita `className` em cada head e `cellClassName` em cada célula; a política responsiva pode e deve permanecer no dono local. A frase “uma instância por linha” será verificada dentro da superfície de tabela, porque cards e tabela já são árvores irmãs simultâneas no DOM, ocultadas por CSS; desmontar a superfície inativa exigiria estado/JavaScript e reabriria B42 fora do appetite.
+
+## Abordagem recomendada
+
+```mermaid
+flowchart TD
+ Cookie[Cookie campaign_columns] --> Parse[Parser preserva ausente vs vazio explícito]
+ Parse --> Defaults[Defaults só quando municipios está ausente]
+ Defaults --> Manual[Visibilidade manual no servidor]
+ Manual --> Columns[Definições locais de colunas]
+ Width[Named container municipality-list] --> Capacity[Classes C0 a C6]
+ Capacity --> Columns
+ Columns --> Cards[Cards abaixo de 48rem]
+ Columns --> Table[Tabela a partir de 48rem]
+ Manual --> Actions[actions somente para trend ou lastSignal ocultos]
+```
+
+### Decisão 1 — onde vive a política responsiva
+
+**Opções consideradas:**
+
+- A. Classes de container query locais nas definições de `MunicipalityList`, aplicadas simetricamente a head/cell.
+- B. Generalizar `CampaignTableColumn` com prioridade/breakpoint responsivo.
+- C. Medir largura com `ResizeObserver` e recalcular colunas no cliente.
+
+**Recomendação:** A — `CampaignTable` continua um módulo profundo para renderização e preferência manual; a matriz C0–C6 é política exclusiva de Municípios e todos os heads já aceitam `className`. Um record local de classes literais e tipadas evita divergência entre `
` e `
` sem criar API compartilhada prematura.
+
+**Rejeitadas:** B vazaria uma política de uma lista para todas as tabelas e transformaria breakpoints de produto em configuração genérica; C adicionaria hidratação, estado e risco de flicker para um problema resolvido nativamente por CSS.
+
+### Decisão 2 — ausência vs vazio explícito no cookie B17
+
+**Opções consideradas:**
+
+- A. Sentinel reservado `__none__` no formato atual (`municipios:__none__`).
+- B. Cookie novo em JSON/versionado, migrando todos os consumidores.
+- C. Manter o cookie atual e guardar um segundo cookie de “configurado”.
+
+**Recomendação:** A — é retrocompatível, cabe no parser/serializer puro existente e não cria uma segunda fonte de verdade. O parser trata o sentinel somente como vazio explícito; ele nunca emerge como column ID. O serializer usa presença da chave para distinguir “ausente” de `[]`.
+
+**Rejeitadas:** B amplia a migração e o risco de decoding entre `document.cookie` e `cookies()` sem benefício; C duplica estado e permite combinações contraditórias.
+
+### Decisão 3 — montagem única de Tendência/Sinal
+
+**Opções consideradas:**
+
+- A. Definir uma função local por controle e chamá-la ou na coluna real ou em `actions`, conforme a ocultação manual resolvida no servidor.
+- B. Renderizar coluna e `actions` sempre, escondendo uma cópia com CSS.
+- C. Mover quick edits para um menu genérico de ações por linha.
+
+**Recomendação:** A — apenas uma célula chama cada factory por linha. A coluna adaptativa permanece montada ao variar capacidade e troca compacto ↔ completo apenas por CSS; `actions` só é incluída na definição quando `trend` e/ou `lastSignal` está manualmente oculta.
+
+**Rejeitadas:** B viola o contrato de uma instância e duplica estado/autosave; C remove edição “onde se vê”, muda o fluxo aprovado e cria uma abstração rasa.
+
+### Decisão 4 — prova de responsividade e compatibilidade
+
+**Opções consideradas:**
+
+- A. Apenas snapshots/static markup e inspeção manual.
+- B. Unit/static markup + Playwright medindo o named container em cada corte + interação real com sidebar/chat + projeto WebKit focado.
+- C. Expandir toda a suíte campaign para uma matriz permanente Chromium/WebKit.
+
+**Recomendação:** B — prova o mecanismo pelo tamanho do container, não por proxy de viewport, e cobre sticky/container queries em WebKit sem duplicar a suíte inteira. O spec focado roda no projeto Chromium já existente e em um projeto WebKit dedicado; CI instala somente os dois engines necessários.
+
+**Rejeitadas:** A não detecta overflow nem erros de corte; C aumenta muito o tempo/custo de CI para uma necessidade localizada. WebKit é a automação disponível no Linux para compatibilidade Safari; uma checagem em Safari/iPad físico continua sendo validação de lançamento, não gate reproduzível deste agente.
+
+### Componentes / mudanças
+
+- **`parseCampaignHiddenColumns` / `serializeCampaignHiddenColumns` / resolver de defaults** (`src/lib/campaignColumnVisibility.ts`): preservar chave ausente versus vazio explícito, declarar o sentinel reservado e o preset de Municípios (`goalCoverage`, `lastSignal`) sem alterar defaults das outras listas.
+- **`readCampaignColumnVisibility`** (`src/utilities/campaignColumnVisibilityCookie.ts`): aplicar preset somente quando a entrada da lista está ausente; devolver `[]` quando o sentinel registra “todas visíveis”.
+- **`CampaignColumnPicker`** (`src/components/campaign/shared/CampaignColumnPicker.tsx`): manter o fluxo atual; `Restaurar todas` já chama `apply([])` e passará a persistir vazio explícito por efeito do serializer. Ajustar comentários/testes, não criar estado novo.
+- **`MunicipalityList`** (`src/components/campaign/municipality/MunicipalityList.tsx`):
+ - envolver cards+tabela em `@container/municipality-list` com seletor estável de teste;
+ - reordenar colunas para Nome, 2022, 2026, Nível, Classe, Assessor, Tendência, Liderança, Dobradinha, Cobertura, Sinal, Ações;
+ - mover `TerritoryLink` para a segunda linha do nome e retirar `region` da tabela/picker;
+ - declarar `MunicipalityTableColumnId = MunicipalityListColumnId | 'actions'` localmente;
+ - declarar classes C0–C6 como strings literais, reutilizadas em head/cell;
+ - calcular ocultação manual uma vez e incluir `actions` somente quando necessário;
+ - remover `overflow-x-auto` local e manter o wrapper sem scroller horizontal;
+ - preservar legenda/caption, inclusive `sort=region` ativo.
+- **`MunicipalityListMobileCards`** (`src/components/campaign/municipality/MunicipalityListMobileCards.tsx`): trocar somente `md:hidden` pela variante do named container; conteúdo/interação ficam intactos.
+- **`MunicipalityListTrendControl`** (`src/components/campaign/municipality/MunicipalityListTrendControl.tsx`): seam tipado de trigger adaptativo para a tabela; uma instância contém ícone compacto e badge/readout completo alternados por CSS. O botão continua com nome acessível textual e sem depender apenas de cor.
+- **`MunicipalityListSignalControl`** (`src/components/campaign/municipality/MunicipalityListSignalControl.tsx`): manter o slot `children`; o chamador fornece, na mesma instância, trigger compacto e `SignalAgeReadout` completo. Em `actions`, fornecer somente o trigger compacto apropriado.
+- **`MunicipalityListLevelControl` / `MunicipalityLevelBadge`** (`src/components/campaign/municipality/`): a face vazia na célula de tabela passa a `—`, preservando “Sem nível” no `aria-label`, filtro e formulários/cards.
+- **Labels e URL** (`src/utilities/municipality/municipalityLabels.ts`, `src/utilities/municipality/municipalityListUrl.ts`): retirar `region` apenas de `MunicipalityListColumnId`/records de coluna; manter `region` em sort/parser/serializer/resumo. Separar label de picker (`Estimativa 2026`) do head telegráfico (`2026`) e usar singular nos demais heads aprovados.
+- **Filtros** (`MunicipalityFilters.tsx`, `municipalityOmnibox.ts`): sem mudança comportamental; regressões garantem `regionFilterOptions`, chips e `?region=`.
+- **Migration:** sem migration.
+- **Access / Consent:** nenhuma mudança; a composição por papel continua usando `isStaffView` e `isCampaignUnrestricted` existentes.
+- **UI:** Impeccable Adapt, modo Operate. Shape já está fixado pela matriz; execução segue craft → critique → polish em duas rodadas visuais no máximo. Reusar tokens e shells existentes; sem redesign.
+- **Documentação:** entrada curta em `docs/CHANGELOG-AGENTS.md`; não alterar `docs/ARCHITECTURE.md`, pois nenhuma fronteira muda.
+
+### Dados → forma
+
+- **Forma escolhida:** tabela essencial + disclosure progressivo por coluna, com cards no container estreito.
+- **Por quê:** nome/2022/2026/nível/classe sustentam decisão imediata; responsabilidade e rede entram à medida que o painel comporta; trend/sinal preservam ação por trigger compacto.
+- **Rejeitadas:** scroll horizontal mantém informação mas esconde contexto fora da dobra; tabela fixa reduz legibilidade; cards em toda largura desperdiçam densidade operacional.
+
+## Fases verificáveis
+
+1. **Tracer: preferência + estrutura local (~30%)**
+ - Evoluir parser/serializer/defaults com unit tests de round-trip e legado.
+ - Reordenar/remover `region`, criar named container, trocar cards↔tabela em 48rem e aplicar C0 essencial.
+ - Rodar testes unitários focados e `pnpm gate:fast`.
+2. **Controles adaptativos + matriz completa (~35%)**
+ - Implementar `actions`, factories únicas e estados compacto/completo.
+ - Aplicar C1–C6 simetricamente em heads/cells, headers curtos, nome+território e nível vazio.
+ - Craft com conteúdo real/nomes longos; calibrar cortes uma única vez por evidência de `scrollWidth`.
+3. **Browser + compatibilidade (~25%)**
+ - Spec Playwright focado com larguras 1px antes/depois de 48/54/60/66/72/78/84rem.
+ - Medir `scrollWidth <= clientWidth`, visibilidade cards/tabela, ordem/entrada, defaults, `actions` e uma instância por controle dentro da tabela.
+ - Remover largura forçada e validar que sidebar e chat alteram a etapa sem mudar a viewport.
+ - Rodar o mesmo spec em Chromium e WebKit; incluir cenário de viewport iPad para sticky/touch targets.
+4. **Polish e gates (~10%)**
+ - Critique/polish em lote, teclado/foco/nomes acessíveis e alvo mínimo de 44px.
+ - Atualizar changelog.
+ - `pnpm gate:fast`, testes E2E focados, `pnpm exec knip`, `pnpm check:cycles`; entrega via `pnpm push`.
+
+## Verificação prevista
+
+### Unit/static markup
+
+- `tests/unit/campaignColumnVisibility.unit.spec.ts`: ausente → sem chave; sentinel → chave com `[]`; round-trip explícito; entradas legadas e outras listas intactas.
+- `tests/unit/campaignColumnPicker.unit.spec.ts`: “Restaurar todas” escreve `municipios:__none__`; toggles partem do preset recebido e persistem uma única sessão.
+- `tests/unit/campaignComponents.unit.spec.ts`: ordem de headers por papel, `region` fora da tabela/picker, território sob nome, `actions` fora do picker, nível nulo `—`, classes simétricas e quantidade de triggers por cenário manual dentro do `
`.
+- `tests/unit/municipalityList.unit.spec.ts` e `municipalityOmnibox.unit.spec.ts`: `sort=region`, resumo, filtros/chips/serialização continuam válidos.
+
+### Browser/E2E
+
+- Novo spec focado `tests/e2e/campaignMunicipalityResponsiveColumns.e2e.spec.ts`.
+- Named container selecionável por atributo estável; largura controlada diretamente para provar os cortes.
+- Defaults sem cookie: Cobertura/Sinal ocultos e ação de Sinal disponível; sentinel explícito: todas visíveis após reload.
+- Cada faixa prova headers esperados, mudança compacta/completa e ausência de overflow.
+- Sidebar/chat são acionados pelos controles reais; o teste compara a largura medida e a etapa resultante.
+- Chromium desktop + WebKit focado; viewport iPad no cenário sticky/touch.
+
+### Fonte oficial
+
+- Tailwind CSS 4 container queries: `@container/{name}`, variantes nomeadas e valores arbitrários `@min-[…]`: https://tailwindcss.com/docs/responsive-design#container-queries
+
+## Rabbit holes / Não escopo (engenharia)
+
+- Não criar estado React, `ResizeObserver`, `matchMedia` ou hook de largura.
+- Não adicionar política responsiva genérica a `CampaignTable` nem alterar `ui/Table` globalmente.
+- Não desmontar a árvore cards/tabela com JavaScript; a montagem única é garantida entre coluna e `actions` na tabela.
+- Não otimizar loaders por coluna visível.
+- Não alterar conteúdo dos cards, o omnibox ou contratos de URL.
+- Não converter toda a suíte E2E para uma matriz cross-browser.
+- Não criar novos ícones/linguagem visual fora dos triggers adaptativos necessários.
+
+## Riscos e mitigação
+
+- **Tailwind não emitir classes dinâmicas:** manter todas as variantes C0–C6 como strings literais completas no arquivo; typecheck e build comprovam geração.
+- **Head/cell divergirem:** usar um record local único por coluna e testes estáticos que verificam a mesma variante nos dois lados.
+- **Overflow por conteúdo real, especialmente relações:** testar nomes longos e várias relações; compactar somente trend/sinal; calibrar cortes em uma rodada medida, sem loop de microajustes.
+- **Sticky left em container sem scroller próprio:** testar Chromium/WebKit; manter fundo/z-index existentes e medir overflow do container.
+- **Sentinel vazar como coluna:** parser reconhece o valor antes da validação de IDs; serializer nunca o aceita como entrada de usuário; tests pinam que `hiddenColumnIds` recebe `[]`.
+- **Default novo parecer escolha explícita:** ausência aplica preset, mas a primeira interação escreve uma entrada explícita; “Restaurar todas” escreve sentinel e sobrevive a reload.
+- **Custo CI WebKit:** projeto restrito ao spec B158, não à suíte campaign inteira.
+- **Safari/iPad físico indisponível no ambiente Linux:** WebKit automatizado é gate técnico; inspeção física permanece checklist de lançamento, explicitamente não alegada como executada.
+
+## Aceite de engenharia
+
+- [ ] Aceite de produto da intenção ainda coberto.
+- [ ] Invariantes AGENTS/engineering-standards preservados; sem schema, DB, access ou Consent.
+- [ ] Preferência manual e capacidade do container permanecem dimensões independentes.
+- [ ] Cada controle Trend/Sinal é montado no máximo uma vez por linha dentro da tabela.
+- [ ] Testes unit/static cobrem cookie, papel, DOM e contratos URL/filtro.
+- [ ] E2E mede container, overflow, shell real e compatibilidade Chromium/WebKit.
+- [ ] Gate rápido, dead-code/cycles e push canônico previstos.
+
+## Self-score decision-quality
+
+1. Decisões caras têm alternativas rejeitadas: **1/1**.
+2. Abordagem cabe no appetite: **1/1**.
+3. Rabbit holes nomeados: **1/1**.
+4. Depth check reusa owners/shells/helpers: **1/1**.
+5. Outcome e lockdowns da intenção preservados: **1/1**.
+
+**Total: 5/5.**
diff --git a/src/components/campaign/municipality/MunicipalityList.tsx b/src/components/campaign/municipality/MunicipalityList.tsx
index 023ebabb..32c0b66a 100644
--- a/src/components/campaign/municipality/MunicipalityList.tsx
+++ b/src/components/campaign/municipality/MunicipalityList.tsx
@@ -1,4 +1,4 @@
-import { SearchXIcon } from 'lucide-react'
+import { ActivityIcon, CircleAlertIcon, SearchXIcon } from 'lucide-react'
import Link from 'next/link'
import {
@@ -69,6 +69,10 @@ import {
resolveMunicipalityListSort,
type MunicipalityListState,
} from '@/utilities/municipality/municipalityListUrl'
+import {
+ isMunicipalitySignalCold,
+ municipalitySignalAgeInDays,
+} from '@/utilities/municipality/municipalitySignal'
import type {
EligibleAdvisorOption,
EligibleLeadershipOption,
@@ -155,8 +159,48 @@ const advisorEntries = (
* Desktop table columns (Pass 2 W1). `id` is the picker key, cookie key, and B22
* description key — typed instead of widening to `string`.
*/
+type MunicipalityTableColumnId = MunicipalityListColumnId | 'actions'
+
type MunicipalityColumn = CampaignTableColumn & {
- id: MunicipalityListColumnId
+ id: MunicipalityTableColumnId
+}
+
+const responsiveColumnClassName = {
+ advisors: 'hidden @min-[54rem]/municipality-list:table-cell',
+ leaderships: 'hidden @min-[66rem]/municipality-list:table-cell',
+ stateDeputies: 'hidden @min-[72rem]/municipality-list:table-cell',
+ goalCoverage: 'hidden @min-[78rem]/municipality-list:table-cell',
+} as const satisfies Partial>
+
+const MunicipalityListSignalTrigger = ({
+ lastSignalAt,
+ presentation,
+}: {
+ lastSignalAt: string | null
+ presentation: 'compact' | 'adaptive'
+}) => {
+ const isCold = isMunicipalitySignalCold(municipalitySignalAgeInDays(lastSignalAt))
+ const Icon = isCold ? CircleAlertIcon : ActivityIcon
+ const compact = (
+
+ )
+
+ if (presentation === 'compact') return compact
+
+ return (
+ <>
+ {compact}
+
+
+
+ >
+ )
}
const municipalityListColumns = ({
@@ -174,326 +218,373 @@ const municipalityListColumns = ({
stateDeputyCommitAction,
stateDeputyCreateAction,
signalFormAction,
-}: MunicipalityListProps): Array => [
- {
- id: 'name',
- label: municipalityColumnLabels.name,
- mandatory: true,
- head: (
- => {
+ const manuallyHidden = isStaffView ? new Set(columnVisibility.hiddenColumnIds) : new Set()
+ const trendIsHidden = manuallyHidden.has('trend')
+ const signalIsHidden = manuallyHidden.has('lastSignal')
+
+ const renderTrendControl = (
+ municipality: MunicipalityListViewModel,
+ triggerPresentation: 'compact' | 'adaptive',
+ ) => (
+
+ )
+
+ const renderSignalControl = (
+ municipality: MunicipalityListViewModel,
+ triggerPresentation: 'compact' | 'adaptive',
+ ) => (
+
+
- ),
- cellClassName: 'sticky left-0 z-[5] min-w-56 whitespace-normal bg-background',
- cell: (municipality) => (
-