loadMunicipalityListPageBundle"] -->|"+ lastUpdate por página"| vm["MunicipalityListViewModel
+ lastUpdate: MunicipalityUpdateViewModel | null"] + vm --> cards["MunicipalityListMobileCards (novo)"] + cards --> card["MunicipalityMobileCard (client, estado expandido)"] + card --> bar["ExpectedVotesControl + trigger barra (VoteEstimateScenarioStrip)"] + card --> chips["Trend/Level controls + trigger chip label-esquerda"] + card --> avatars["RelationEditor + trigger pilha wrap (3 grupos)"] + card --> footer["Rodapé: expande | CTA direto (UpdateControl)"] +``` + +**Opções consideradas:** + +- **A — Controles ganham prop `trigger` opcional (recomendada).** Cada controle de edição (`MunicipalityListExpectedVotesControl`, `MunicipalityListLevelControl`, `MunicipalityListTrendControl`, `MunicipalityRelationEditor` + os 3 wrappers de relação) passa a aceitar um `trigger` customizado que substitui o interno; o `CampaignCellEditOverlay` já tem o `aria-label` que cobre a11y de qualquer conteúdo. O card compõe label+valor/barra/pilha dentro do botão, então o toque no **label** abre a sheet (aceite), e o trigger continua `relative` acima do overlay `after:inset-0` (invariante B42 documentado no próprio `CampaignCellEditOverlay`). +- **B — Card posiciona label e trigger como irmãos, e o label abre a sheet via handler próprio.** Duplica o estado de abertura do sheet (dois triggers para um overlay) e fura a maquinaria `openSheet/dismissSheet` do host compartilhado — o host guarda `activeOnOpenChangeRef` e o segundo trigger entraria em conflito. Rejeitada: é a C109 que se descartou; quebraria a invariante "um control = um onOpenChange". +- **C — Recriar o editor no card (componentes novos de sheet).** Rejeitada: rabbit hole "recriar editors" nomeado na intenção; anti-goal explícito. +- **D — Barra/pilhas como affordances estáticas + navegação para a página de edição.** Rejeitada: viola edit-where-you-see (editar "sem sair do card", aceite). + +**Decisões de engenharia:** + +1. **Dado da expansão — última atualização no view model** (custo de reverter: médio — toca loader e tipo). + - Opções: (a) `lastUpdate: MunicipalityUpdateViewModel | null` no `MunicipalityListViewModel`, carregado em batch por página; (b) fetch per-card no client (N+1, e o read precisa de access server-side); (c) `lastUpdateAt` sozinho (não dá o corpo/autoria do card). + - Recomendação: (a) — um `payload.find` em `municipalityUpdate` com `where: { municipality: { in: idsDaPágina } }`, `sort: '-createdAt'`, select enxuto, `overrideAccess: false` (o `canReadMunicipalityUpdate` escopa por portfólio), primeiro doc por município; nomes de autor via `loadCampaignUserNamesByIds` (mesmo padrão de `loadMunicipalityUpdatesFeed`). Alternativas rejeitadas: (b) porque explode o número de reads por página e expõe o access no client; (c) porque o card expandido precisa de corpo/polaridade/autor (aceite). + - Gatilho de revisitação (barato, não agora): se um município tiver centenas de updates, o batch puxa tudo — `DISTINCT ON` via drizzle quando houver medição (mesma política de `supporterListOverviewAggregate`). +2. **Cobertura (conta da cadeira) sai do card mobile.** O wireframe não a inclui; o card atual mostra "Cobertura" (goal coverage). Fica na tabela desktop e no detalhe. Sinalizo no gate como remoção de dado da superfície (não é "dado novo", é composição nova do wireframe). +3. **Rodapé passa a medir a idade da ATUALIZAÇÃO, não o frescor (update∨pledge).** + - O wireframe é explícito ("Última atualização há X dias", "sem nenhuma atualização registrada → CTA direto") e o estado vazio é chaveado na existência de update, não de pledge. O `SignalAgeReadout`/frescor continua na coluna desktop (`lastSignal`), intacto. A cor de "sinal frio" (`estimate-pending`) é mantida no rodapé quando a **atualização** está fria (≥21 dias), preservando o alerta da fila. + - Sinalizo como divergência pequena de produto no gate (a intenção herda o vocabulário frescor; troco para update-only no card). +4. **Borda de prioridade** — `border-r-[6px] border-primary` no `