diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md
index 856a6460..1cebc11b 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
---
+**S7** — polimento dos cards da seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br), look editorial e direto no lugar do "card de app": cantos retos (**sem `rounded-xl` no card e `rounded-lg` na capa**), capa de **borda a borda** (o `p-3` saiu do contêiner do card e foi para o bloco de texto — título/subtítulo/meta continuam respirando `p-3` das bordas; `mt-auto` da meta e `overflow-hidden` do zoom hover intactos) e, por decisão do humano posterior ao gate, **sem borda (`border --campaign-line`) e sem sombra** (`shadow-sm`/`hover:shadow-md`) — o card branco lê contra a banda do S5 só pela forma. **Chips de badge removidos de todos os cards** (decisão B do gate): nem os tipos de post do site (`POST_TYPE_BADGE_LABELS` deletado de `src/utilities/posts.ts`, junto com o describe no unit test) nem as origens externas YouTube/Instagram (sinalização externa segue por `target="_blank"`/`rel="noopener"`); `badgeLabel` saiu do tipo `CampaignContentCardData`. Superfície única no componente dono (`CampaignContentCard.tsx`) — bento e carrossel herdam; `ContentShareButton` (S4), banda do S5, carrossel e fail-closed da seção intocados. Zero migration/schema/Consent. e2e do describe da seção atualizados para o novo estado: loop de badges → ausência (`toHaveCount(0)` para os 4 tipos + YouTube/Instagram), pins CSS do card (`border-radius`/`border-width`/`box-shadow` + raio da capa) e pin geométrico da capa de borda a borda (boundingBox do card × capa). Verificação: gate:fast verde, unit 2415 + int 791 verdes, e2e frontend 19/19 local, format/knip/cycles limpos. Plano de implementação: `docs/plans/cards-acompanhe-de-perto-polimento-impl.md`.
+
**OPS69** — o admin do Payload abria **em branco em produção** (P0, 2026-08-19): o `importMap` commitado (`src/app/(payload)/admin/importMap.js`) ficou sem a entrada `@payloadcms/storage-s3/client#S3ClientUploadHandler` — o commit OPS52 (`dabc00f4`) removeu o handler do Vercel Blob e **não** adicionou o do S3, porque o `pnpm generate:importmap` rodou **sem** as envs `S3_*`: com o plugin `s3Storage` condicional, sem as envs o importMap sai consistente (dev/test, storage local) e com elas (produção, Garage) o admin precisa do handler e o montava → `getFromImportMap: PayloadComponent not found in importMap` → login nunca renderizava. Fix mínimo (opção A do plano, aprovado no gate): regenerar o importMap com as 4 `S3_*` dummy inline (a mecânica do CLI: `iterateConfig` sem DB; `loadEnvConfig` do `@next/env` não sobrescreve envs já no processo) → diff de **exatamente +2 linhas** (import + entrada do `S3ClientUploadHandler`), nenhum outro drift. Verificação: smoke headless com S3 ativo no dev local — `/admin/login` renderiza (6 inputs, formulário visível, **zero erros de console**) vs. o branco reproduzido no diagnóstico; gates completos verdes (tsc/lint/format/knip/cycles, unit+int 791, build). Sem migration/access/Consent/UI. Docs: linha no AGENTS.md (checklist local: `generate:importmap` precisa das `S3_*` setadas — valores dummy ok — enquanto o plugin for condicional; sem elas o importMap fica órfão, classe OPS69) + item sucessor registrado (guard de CI que regenera com envs dummy e falha o gate se o arquivo commitado divergir — `depends` #77). Verificação pós-deploy: `/admin` abre o login e o log do container não re-emite o `getFromImportMap` para o handler (runbook `docs/ops/teqo-1313-deploy.md`).
**OPS62-FOLLOWUP** — flake do `municipalitiesWithoutUpdateTool.unit.spec.ts` (2 ocorrências 2026-08-18: gate local 19:22 e CI run 779 23:53) eliminado pela raiz, com a hipótese original da Issue refutada por reprodução. Causa raiz: o fixture do teste (não o ambiente nem locale/ICU): `daysAgo()` chamava `Date.now()` **por chamada**; as duas construções `daysAgo(40)` de `Beta`/`Alpha` podiam cair em milissegundos diferentes sob carga (GC/preempção do fork do vitest na máquina compartilhada), deixando o timestamp de `Alpha` estritamente mais novo — em `sortStale` (`getMunicipalitiesWithoutUpdate.ts`) o sort por timestamp vencia o tie-break por nome e o teste recebia `[Beta, Alpha]` no lugar de `[Alpha, Beta]` (só o par trocado, resto correto — "sort não atuou" estava errado; o sort atuou sobre timestamps acidentalmente desiguais). Repro mecânica (sort de produção verbatim + fixture): fixture atual 0.08% de falhas em 200k runs sem pausa e **100%** com pausa de 1 ms injetada; fixture ancorado 0 em 2000 runs. Fix (test-only, nada em produção — o sort real é determinístico sobre timestamps fixos do DB): âncora `fixtureNowMs = Date.now()` no load do módulo (padrão do `prioritiesTool.unit.spec.ts`) + hoist do timestamp empatado (`const stale40 = daysAgo(40)` compartilhado por `Beta`/`Alpha` — igualdade estrutural, imune até a regressão da âncora) + comentário explicando a classe de flake. Scan da mesma classe: `prioritiesTool` (âncora no load) e `suggestionCatalog` (now fixo) já seguros; nenhum outro spec combina `Date.now()` por chamada com assunção de igualdade. Verificação: 30/30 runs do spec, unit full (249 arquivos/2405 testes), int (89/791), tsc, lint, format, check:cycles verdes (knip só com o P3 ledgered de sempre). /simplify: 3 reviewers aprovaram; débito absorvido no próprio teste (comentário do porquê). Impl plan: `docs/plans/ops62-followup-ci-flake-fixture-timestamps-impl.md`.
diff --git a/docs/changelog/2026-08-19-s7.md b/docs/changelog/2026-08-19-s7.md
new file mode 100644
index 00000000..dc72862b
--- /dev/null
+++ b/docs/changelog/2026-08-19-s7.md
@@ -0,0 +1 @@
+**S7** — polimento dos cards da seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br), look editorial e direto no lugar do "card de app": cantos retos (**sem `rounded-xl` no card e `rounded-lg` na capa**), capa de **borda a borda** (o `p-3` saiu do contêiner do card e foi para o bloco de texto — título/subtítulo/meta continuam respirando `p-3` das bordas; `mt-auto` da meta e `overflow-hidden` do zoom hover intactos) e, por decisão do humano posterior ao gate, **sem borda (`border --campaign-line`) e sem sombra** (`shadow-sm`/`hover:shadow-md`) — o card branco lê contra a banda do S5 só pela forma. **Chips de badge removidos de todos os cards** (decisão B do gate): nem os tipos de post do site (`POST_TYPE_BADGE_LABELS` deletado de `src/utilities/posts.ts`, junto com o describe no unit test) nem as origens externas YouTube/Instagram (sinalização externa segue por `target="_blank"`/`rel="noopener"`); `badgeLabel` saiu do tipo `CampaignContentCardData`. Superfície única no componente dono (`CampaignContentCard.tsx`) — bento e carrossel herdam; `ContentShareButton` (S4), banda do S5, carrossel e fail-closed da seção intocados. Zero migration/schema/Consent. e2e do describe da seção atualizados para o novo estado: loop de badges → ausência (`toHaveCount(0)` para os 4 tipos + YouTube/Instagram), pins CSS do card (`border-radius`/`border-width`/`box-shadow` + raio da capa) e pin geométrico da capa de borda a borda (boundingBox do card × capa). Verificação: gate:fast verde, unit 2415 + int 791 verdes, e2e frontend 19/19 local, format/knip/cycles limpos. Plano de implementação: `docs/plans/cards-acompanhe-de-perto-polimento-impl.md`.
diff --git a/docs/plans/cards-acompanhe-de-perto-polimento-impl.md b/docs/plans/cards-acompanhe-de-perto-polimento-impl.md
new file mode 100644
index 00000000..30d15bef
--- /dev/null
+++ b/docs/plans/cards-acompanhe-de-perto-polimento-impl.md
@@ -0,0 +1,68 @@
+# Impl: Cards da seção "Acompanhe de perto": sem cantos arredondados, imagem de borda a borda, chip removido
+
+Status: aprovado
+Atualizado em: 2026-08-19
+Issue: #90
+Intenção: docs/plans/cards-acompanhe-de-perto-polimento.md
+Appetite restante: herdado (~0,5–1 dia; na prática, horas)
+
+## Leitura da intenção
+
+- **Outcome:** os cards da seção "Acompanhe de perto" (bento 1+4 no desktop, carrossel no mobile) passam a ter cantos retos (card e capa), capa de borda a borda, **sem borda e sem sombra no card** (decisão do humano em 2026-08-19, após o gate — estende o rascunho, que mantinha `border`/`shadow`) e nenhum chip de badge — nem tipos de post do site, nem origens externas (decisão B do gate). O texto interno (título, subtítulo, meta) continua com o respiro atual (`p-3`) em relação às bordas.
+- **O que NÃO negociar:** nada além da superfície muda — compartilhamento (S4) usável no canto, grade do bento, carrossel, links, bandas do S5 (borda/background da seção) e o fail-closed da seção (some inteira quando nada é visível). Nada de tokens de cor, capas reais, proporções ou outras seções.
+- **O que reavaliar:** a hipótese da intenção ("mover o padding para o bloco de texto e remover o badge; dados de badge tendem a ficar órfãos — o executor decide o que descartar"). Confirmado no código: `badgeLabel` vive só no card + nas três funções de mapeamento da seção + `POST_TYPE_BADGE_LABELS` (com um unit test dedicado). Decisão de engenharia: remover os órfãos por completo (tipo do card, constante e teste unitário) — manter morto seria dead code flagrável pelo knip (CI-blocking).
+
+## Abordagem recomendada
+
+```mermaid
+flowchart LR
+ A[CampaignContentCard.tsx superfície do card] --> B["card: -rounded-xl -p-3 -gap capa: -rounded-lg texto: +p-3 badge span: removido"]
+ A --> C["CampaignContentCardData -badgeLabel"]
+ D[CampaignContentSection.tsx] --> E["-badgeLabel nas 3 funções -import POST_TYPE_BADGE_LABELS"]
+ F[posts.ts] --> G["-POST_TYPE_BADGE_LABELS"]
+ H[postsDateFormats.unit.spec.ts] --> I["-describe POST_TYPE_BADGE_LABELS"]
+ J[frontend.e2e.spec.ts] --> K["badge loop: 812-814, YouTube: 952, Instagram: 1153 → ausência + raio 0"]
+```
+
+**Opções consideradas:** A (superfície no card único + remoção dos órfãos) | B (variante/flag de card, ex. `variant="flat"`) | C (manter `POST_TYPE_BADGE_LABELS` e o badge no DOM com CSS oculto)
+**Recomendação:** A — o card é o único componente da seção (bento e carrossel reusam `CampaignContentCard`); não há outro consumidor com cantos arredondados para preservar, então uma variante seria abstração sem dono. A remoção dos dados de badge é limpa: `badgeLabel` não é usado em mais nenhum lugar do repo.
+**Rejeitadas:** B (over-engineering: nenhum outro card usa o componente; "edit the owner, don't twin"); C (dead code + dead data path; knip derruba o CI e o teste unitário continuaria pinando um contrato morto).
+
+### Componentes / mudanças
+
+- **`CampaignContentCard.tsx`** (`src/components/`): `cardClassName` vira constante sem `featured` (o único uso do param era o `gap-3`/`gap-2`; `featured` continua dirigindo `sizes` da imagem e o `h3`): `flex h-full flex-col bg-white transition-shadow ...` — sem `rounded-xl`, sem `p-3`, sem `gap`, **sem `border border-(--campaign-line)` e sem `shadow-sm`/`hover:shadow-md`/`transition-shadow`**. Capa: remove `rounded-lg` (mantém `overflow-hidden` — o zoom hover continua contido na própria capa). Bloco de texto: ganha `p-3` (`flex flex-1 flex-col gap-1 p-3`), igual ao draft. Badge `` removido; `badgeLabel` sai de `CampaignContentCardData`. O anel de foco (`focus-visible:ring-2 ring-(--pt-red)`) do link permanece — a11y.
+- **`CampaignContentSection.tsx`** (`src/components/`): `badgeLabel` removido de `toArticleCardData`/`toVideoCardData`/`toInstagramCardData`; import `POST_TYPE_BADGE_LABELS` removido. Nada mais muda (limites, mesclagem, capas, metas).
+- **`src/utilities/posts.ts`**: deleta `POST_TYPE_BADGE_LABELS` (único consumidor era a seção; `POST_TYPE_LABELS` plural fica — usado nas listagens).
+- **`tests/unit/postsDateFormats.unit.spec.ts`**: import e describe de `POST_TYPE_BADGE_LABELS` removidos.
+- **Migration:** sem migration (nenhuma mudança de schema/coleção).
+- **Access / Consent:** nenhum.
+- **UI:** Impeccable B — encaixe em um componente existente (shape→craft→critique→polish). O botão `ContentShareButton` (S4) segue `absolute top-2 right-2` sobre a capa, agora sem moldura; o draft mantém o botão no canto — sem mudança nele.
+
+### Dados → forma (se aplicável)
+
+- N/A — nenhum dado novo; apenas remoção de um campo de superfície (`badgeLabel`) do tipo de dados do card.
+
+## Fases verificáveis
+
+1. **Superfície + órfãos** — `CampaignContentCard.tsx`, `CampaignContentSection.tsx`, `posts.ts`, unit test do badge; verificação: `pnpm typecheck` + `pnpm test:unit` (o describe removido some, o resto passa).
+2. **e2e** — `tests/e2e/frontend.e2e.spec.ts`: loop de badges (812–814) e as asserções `YouTube`/`Instagram` (952, 1153) → asserções de ausência (`toHaveCount(0)` para os quatro tipos + YouTube/Instagram nos testes de feed) e pin do novo visual no teste do bento (`border-radius: 0px` no card e na capa, `border-top-width: 0px` e `box-shadow: none` no card, no mesmo espírito dos pins CSS do S5). `toHaveCSS` com `first()` para não quebrar com o carrossel escondido no mobile.
+3. **Gates** — `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test` (unit+int). E2e roda no CI (OPS59) — local opcional com `pnpm test:e2e:affected`.
+
+## Rabbit holes / Não escopo (engenharia)
+
+- Não tocar `ContentShareButton`, `CampaignContentCarousel`, banda/bordas da seção (S5), `POST_TYPE_LABELS` (plural), `getVisiblePosts`/fail-closed ou capas. O rascunho HTML do gate fica intocado como artefato — a remoção de borda/sombra é decisão do humano posterior ao gate e não altera o arquivo do rascunho.
+- Não criar variante de card, não esconder badge via CSS, não renomear `h3`/metas.
+- Cuidado com o `gap` removido: o `mt-auto` da meta continua pinando a data ao rodapé do bloco; o `p-3` do bloco de texto é quem dá o respiro (draft).
+
+## Riscos e mitigação
+
+- **e2e que assere badge que não mapeei** — verificado por grep: só 812–814 (loop), 952 (YouTube) e 1153 (Instagram) no `frontend.e2e.spec.ts`; `waitForHomeHTML` só usa títulos/links de cabeçalho. Grep final antes do push.
+- **Raio 0 pinado pelo e2e pode flake por arredondamento** — `toHaveCSS('border-radius', '0px')` é determinístico (nenhum token de raio no card/capa).
+- **Dead code residual** — `POST_TYPE_BADGE_LABELS` e `badgeLabel` removidos do mesmo PR; knip no gate confirma.
+- **Capa sem cover (placeholder `--campaign-band`)** — continua com borda a borda e cantos retos, igual ao draft; nada quebra.
+
+## Aceite de engenharia
+
+- [ ] Aceite de produto da intenção ainda coberto (raio 0 card+capa, capa borda a borda, texto `p-3`, nenhum chip em nenhum card, **sem borda e sem sombra no card**)
+- [ ] Invariantes AGENTS/engineering-standards (card único como dono da superfície; S5/bandas intocadas; fail-closed da seção intacto)
+- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam — N/A (nenhum write path muda); e2e atualizados para o novo estado visual
diff --git a/src/components/CampaignContentCard.tsx b/src/components/CampaignContentCard.tsx
index b51ce76b..8571b793 100644
--- a/src/components/CampaignContentCard.tsx
+++ b/src/components/CampaignContentCard.tsx
@@ -7,7 +7,6 @@ export type CampaignContentCardData = {
id: string | number
href: string
title: string
- badgeLabel: string
meta: string
/** Source of the content — drives the WhatsApp share message template. */
shareKind: ContentShareKind
@@ -25,10 +24,8 @@ type CampaignContentCardProps = {
featured?: boolean
}
-const cardClassName = (featured: boolean) =>
- `flex h-full flex-col rounded-xl border border-(--campaign-line) bg-white p-3 shadow-sm transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--pt-red) ${
- featured ? 'gap-3' : 'gap-2'
- }`
+const cardClassName =
+ 'flex h-full flex-col bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--pt-red)'
/**
* Presentational content card for the campaign home content bento/carousel —
@@ -43,7 +40,7 @@ export const CampaignContentCard = ({ card, featured = false }: CampaignContentC
const inner = (
<>