feat(site): hero da campanha responsivo aos boards do Penpot

A primeira dobra da home pública agora acompanha as referências do Penpot
em 393, 768, 1024, 1280, 1366 e 1920 px. No desktop, a escala e a posição
horizontal das fotos dependem somente da largura: Solla, Lula e Wagner
ficam ancorados ao rodapé real do hero mesmo quando o conteúdo aumenta a
seção, e um palco centralizado de 1920 px preserva a proximidade entre
retrato e texto em monitores ultrawide. E2E fixa a geometria das seis
referências, transições de proporção, desktops baixos, ultrawide e os
overrides da WCAG 1.4.12; o caso de regressão também roda no Firefox.
This commit is contained in:
Francisco Solla 2026-08-16 20:33:00 -03:00
parent 21b3c00d96
commit 665157a0c0
6 changed files with 861 additions and 64 deletions

View file

@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
---
**Recently resolved (2026-08-16):** **CAMPAIGN-HOME-RESPONSIVE**: a primeira dobra da home pública agora acompanha as referências do Penpot em 393, 768, 1024, 1280, 1366 e 1920 px. Retrato, aliados, marca, texto, CTAs e prova parlamentar interpolam entre os tamanhos com `clamp()`, `vw` e `dvh`, sem saltos por proporção de tela nem overflow horizontal; texto, ações e condecorações permanecem no fluxo e crescem com os ajustes de espaçamento da WCAG 1.4.12. No desktop, a escala e a posição horizontal das fotos dependem somente da largura: Solla, Lula e Wagner ficam ancorados ao rodapé real do hero até quando o conteúdo aumenta a seção, e um palco centralizado de 1920 px preserva a proximidade entre retrato e texto em monitores ultrawide. O E2E fixa a geometria das seis referências, larguras intermediárias, transições de proporção, desktops baixos, ultrawide e os overrides de acessibilidade; o caso de regressão também foi executado no Firefox. Sem migration, schema ou mudança de conteúdo.
**Recently resolved (2026-08-15):** **IMG-RESIZE** — otimizador local de imagens para o draft do site de campanha (fotos pesadas travavam o PenPot). `pnpm images:resize` (`scripts/resize-images.mjs`) aceita múltiplos paths (arquivos ou pastas, varridas recursivamente), redimensiona para no máximo 3840px — 2× a tela desktop de 1920 — sem upscaling (`fit: 'inside'`, `withoutEnlargement`), aplica EXIF-rotation, remove metadados e converte para **AVIF** por padrão (formato leve moderno; WebP/JPEG via `--format`, qualidade por formato via `--quality`, `--out` para outra pasta — default `public/`). Saída mantém o basename (`foto.jpg` → `foto.avif`) e recusa colidir com a fonte. Derivação pura em `scripts/lib/imageResize.mjs` (parse fail-closed, alias jpg→jpeg, default de qualidade por formato), unit-testada em `tests/unit/imageResize.unit.spec.ts`. Sem migration/access/Consent/UI/schema.
**Recently resolved (2026-08-15):** **IMG-RESIZE** — otimizador local de imagens para o draft do site de campanha (fotos pesadas travavam o PenPot). `pnpm images:resize` (`scripts/resize-images.mjs`) aceita múltiplos paths (arquivos ou pastas, varridas recursivamente), redimensiona para no máximo 3840px — 2× a tela desktop de 1920 — sem upscaling (`fit: 'inside'`, `withoutEnlargement`), aplica EXIF-rotation, remove metadados e gera **DUAS versões por padrão**: `.avif` (formato leve moderno, para o site) e `.webp` (aceito em mais lugares, incluindo PenPot e navegadores antigos); o conjunto muda via `--format avif,webp` (lista separada por vírgula), qualidade por formato via `--quality`, pasta via `--out` (default `public/`). Saída mantém o basename (`foto.jpg` → `foto.avif` + `foto.webp`) e recusa colidir com a fonte. Derivação pura em `scripts/lib/imageResize.mjs` (parse fail-closed, alias jpg→jpeg, default de qualidade por formato), unit-testada em `tests/unit/imageResize.unit.spec.ts`. Sem migration/access/Consent/UI/schema.

View file

@ -0,0 +1 @@
**Recently resolved (2026-08-16):** **CAMPAIGN-HOME-RESPONSIVE**: a primeira dobra da home pública agora acompanha as referências do Penpot em 393, 768, 1024, 1280, 1366 e 1920 px. Retrato, aliados, marca, texto, CTAs e prova parlamentar interpolam entre os tamanhos com `clamp()`, `vw` e `dvh`, sem saltos por proporção de tela nem overflow horizontal; texto, ações e condecorações permanecem no fluxo e crescem com os ajustes de espaçamento da WCAG 1.4.12. No desktop, a escala e a posição horizontal das fotos dependem somente da largura: Solla, Lula e Wagner ficam ancorados ao rodapé real do hero até quando o conteúdo aumenta a seção, e um palco centralizado de 1920 px preserva a proximidade entre retrato e texto em monitores ultrawide. O E2E fixa a geometria das seis referências, larguras intermediárias, transições de proporção, desktops baixos, ultrawide e os overrides de acessibilidade; o caso de regressão também foi executado no Firefox. Sem migration, schema ou mudança de conteúdo.

View file

@ -85,15 +85,15 @@ export default async function HomePage() {
<section
aria-label="Experiência e atuação parlamentar"
data-home-section="proof"
className="flex h-[88px] justify-center bg-white lg:h-[148px]"
className="campaign-proof flex justify-center bg-white"
>
<ul className="m-0 grid h-full w-[calc(100%_-_22px)] max-w-[712px] list-none grid-cols-3 items-center gap-[23px] p-0 lg:grid-cols-[147px_178px_183px] lg:items-start lg:justify-between lg:gap-0 lg:pt-[23px]">
<ul className="campaign-proof-list m-0 grid h-full w-[calc(100%_-_22px)] max-w-[712px] list-none grid-cols-3 p-0">
{proofItems.map((item) => (
<li key={item.value} className="m-0 text-center">
<strong className="block font-[family-name:var(--font-exo2)] text-[24px] leading-none font-black text-(--pt-red) lg:text-[36px] lg:leading-[1.1]">
<strong className="campaign-proof-value block font-[family-name:var(--font-exo2)] leading-none font-black text-(--pt-red)">
{item.value}
</strong>
<span className="mt-1 block text-[10px] leading-[1.08] font-medium text-[#242124] lg:text-[18px] lg:leading-[1.25]">
<span className="campaign-proof-label mt-1 block font-medium text-[#242124]">
{item.label}
</span>
</li>
@ -104,30 +104,30 @@ export default async function HomePage() {
<section
aria-labelledby="problem-title"
data-home-section="problem"
className="relative h-[730px] bg-(--pt-red-dark) text-white lg:h-[659px]"
className="campaign-problem relative bg-(--pt-red-dark) text-white"
>
<div className="relative h-full w-full">
<p className="absolute top-[35px] left-5 m-0 font-[family-name:var(--font-exo2)] text-[10px] leading-3 font-black tracking-[0.1em] text-(--pt-yellow) uppercase lg:left-[var(--campaign-content-inset)] lg:text-[12px] lg:leading-[15px]">
<p className="campaign-section-eyebrow campaign-problem-eyebrow absolute m-0 font-[family-name:var(--font-exo2)] font-black tracking-[0.1em] text-(--pt-yellow) uppercase">
Por que essa eleição importa
</p>
<h2
id="problem-title"
aria-label="Eleger deputado é coisa séria. O Congresso tem nas mãos vidas reais e o SUS"
className="absolute top-[59px] left-5 m-0 w-[331px] border-0 p-0 text-[22px] leading-[1.08] font-black tracking-[-0.02em] text-balance lg:top-[66px] lg:left-[var(--campaign-content-inset)] lg:w-[616px] lg:text-[24px] lg:leading-[1.06]"
className="campaign-section-title campaign-problem-title absolute m-0 border-0 p-0 font-black tracking-[-0.02em] text-balance"
>
<span className="block">Eleger deputado é coisa séria</span>
<span className="block text-(--pt-yellow)">
O Congresso tem nas mãos vidas reais e o SUS
</span>
</h2>
<p className="absolute top-[146px] left-5 m-0 w-[327px] font-[family-name:var(--font-exo2)] text-[14px] leading-[1.2] text-white/85 lg:top-[127px] lg:left-[var(--campaign-content-inset)] lg:w-[716px] lg:text-[16px]">
<p className="campaign-section-copy campaign-problem-copy absolute m-0 font-[family-name:var(--font-exo2)] leading-[1.2] text-white/85">
É no Congresso que se define o orçamento do SUS, o piso da enfermagem, a recompra de
Mataripe e o fim da escala 6×1.{' '}
<strong className="font-extrabold text-white">
Reeleger Lula não basta: é preciso eleger uma bancada aliada.
</strong>
</p>
<div className="absolute top-[245px] right-0 left-0 lg:top-[191px]">
<div className="campaign-problem-carousel absolute right-0 left-0">
<CampaignCarousel
ariaLabel="Bandeiras que tornam esta eleição decisiva"
items={problemItems}
@ -141,25 +141,25 @@ export default async function HomePage() {
id="bandeiras"
aria-labelledby="flags-title"
data-home-section="flags"
className="relative h-[400px] scroll-mt-4 bg-(--campaign-band) text-black lg:h-[368px]"
className="campaign-flags relative scroll-mt-4 bg-(--campaign-band) text-black"
>
<div className="relative h-full w-full">
<p className="absolute top-[37px] left-5 m-0 font-[family-name:var(--font-exo2)] text-[10px] leading-3 font-black tracking-[0.1em] text-(--pt-red) uppercase lg:top-[33px] lg:left-[var(--campaign-content-inset)] lg:text-[12px] lg:leading-[15px]">
<p className="campaign-section-eyebrow campaign-flags-eyebrow absolute m-0 font-[family-name:var(--font-exo2)] font-black tracking-[0.1em] text-(--pt-red) uppercase">
Nossa caminhada
</p>
<h2
id="flags-title"
aria-label="Junto com o trabalhador e do lado de quem mais precisa, sempre."
className="absolute top-[66px] left-5 m-0 w-[331px] border-0 p-0 text-[22px] leading-[1.08] font-black tracking-[-0.02em] text-balance lg:top-[62px] lg:left-[var(--campaign-content-inset)] lg:w-[664px] lg:text-[24px] lg:leading-[1.06]"
className="campaign-section-title campaign-flags-title absolute m-0 border-0 p-0 font-black tracking-[-0.02em] text-balance"
>
<span className="block">Junto com o trabalhador e</span>
<span className="block text-(--pt-red)">do lado de quem mais precisa, sempre.</span>
</h2>
<p className="absolute top-[149px] left-5 m-0 w-[327px] font-sans text-[14px] leading-[1.2] text-black/80 lg:top-[130px] lg:left-[var(--campaign-content-inset)] lg:w-[730px] lg:text-[16px]">
<p className="campaign-section-copy campaign-flags-copy absolute m-0 font-sans leading-[1.2] text-black/80">
Não é promessa de palanque, é a experiência de quem já fez na gestão do SUS, do
Ministério da Saúde e da saúde da Bahia.
</p>
<div className="absolute top-[215px] right-0 left-0 lg:top-[184px]">
<div className="campaign-flags-carousel absolute right-0 left-0">
<CampaignCarousel
ariaLabel="Bandeiras da campanha"
items={flagItems}

View file

@ -178,33 +178,561 @@ body,
--campaign-surface: #f9faff;
--campaign-band: #ebe9e9;
--campaign-line: rgb(0 0 0 / 12%);
--campaign-content-inset: 20px;
--campaign-content-inset: clamp(20px, calc(25.36vw - 79.64px), 180px);
}
/* Penpot uses two lockups from the official campaign identity: the wide mark
on phones and the stacked mark on desktop. The PNG canvases are square and
mostly transparent, so keep their exact board geometry instead of sizing by
visible ink bounds. */
/* Campaign home geometry is interpolated between the approved Penpot boards.
Copy stays in normal flow so browser text-spacing preferences can expand the
hero; only the photographic composition is positioned absolutely. */
.campaign-hero {
--campaign-hero-height: max(
clamp(745px, calc(933.42px - 47.95vw), 780px),
clamp(745px, calc(41.6vw + 581.51px), 901px)
);
min-height: var(--campaign-hero-height);
height: auto;
}
.campaign-hero-stage {
max-width: 1920px;
min-height: var(--campaign-hero-height);
height: auto;
}
/* Penpot uses the wide official lockup on phones/tablets. Its PNG canvas is
square and mostly transparent, so the element follows the board geometry. */
.campaign-home-logo {
top: -123px;
left: 19px;
width: 356px;
height: 356px;
--campaign-logo-size: clamp(356px, calc(38.93vw + 203px), 502px);
top: clamp(-166px, calc(-11.47vw - 77.92px), -123px);
left: clamp(0px, calc(33.6vw - 113.05px), 145px);
width: var(--campaign-logo-size);
height: var(--campaign-logo-size);
background-image: url('/Prancheta%206%403x.png');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
.campaign-hero-portrait {
top: clamp(53px, calc(9.33vw + 16.32px), 88px);
left: calc(50% - 211px + clamp(8.5px, calc(3.07vw - 3.55px), 20px));
width: 422px;
height: auto;
aspect-ratio: 422 / 469;
}
.campaign-hero-portrait-image {
top: -3.63%;
left: 0.24%;
width: 106.4%;
}
.campaign-hero-ally {
z-index: 0;
width: 177px;
height: auto;
}
.campaign-hero-ally--rui {
top: clamp(144px, calc(9.33vw + 107.32px), 179px);
left: calc(50% - 218.5px + clamp(0px, calc(3.07vw - 12.05px), 11.5px));
}
.campaign-hero-ally--wagner {
top: clamp(148px, calc(9.33vw + 111.32px), 183px);
left: calc(50% - 265.5px + clamp(0px, calc(3.07vw - 12.05px), 11.5px));
}
.campaign-hero-ally--jeronimo {
top: clamp(140px, calc(9.33vw + 103.32px), 175px);
left: calc(50% + 27.5px + clamp(0px, calc(3.07vw - 12.05px), 11.5px));
}
.campaign-hero-ally--lula {
top: clamp(179px, calc(9.33vw + 142.32px), 214px);
left: calc(50% + 65.5px + clamp(0px, calc(3.07vw - 12.05px), 11.5px));
z-index: 2;
width: 216px;
}
.campaign-hero-overlay {
top: clamp(300px, calc(9.33vw + 263.32px), 335px);
left: 50%;
width: 100vw;
height: 245px;
transform: translateX(-50%);
}
.campaign-hero-content {
min-height: var(--campaign-hero-height);
padding-top: clamp(462px, calc(9.33vw + 425.32px), 497px);
padding-bottom: clamp(14px, calc(28.34px - 1.87vw), 21px);
}
.campaign-hero-title {
width: min(100%, clamp(392px, calc(38.67vw + 240.04px), 537px));
margin-right: auto;
margin-left: auto;
text-align: center;
font-size: clamp(32px, calc(4.27vw + 15.23px), 48px);
}
.campaign-hero-copy {
width: min(calc(100% - 40px), clamp(327px, calc(24.8vw + 229.54px), 420px));
margin-top: clamp(13px, calc(4.59vw - 4.73px), 30.5px);
margin-left: clamp(20px, calc(31.2vw - 98.62px), 141px);
font-size: clamp(14px, calc(1.07vw + 9.81px), 18px);
}
.campaign-hero-actions {
width: max-content;
margin-top: clamp(16px, calc(6.85vw - 5.92px), 31px);
margin-right: clamp(18px, calc(52.27vw - 187.43px), 214px);
margin-left: auto;
gap: clamp(10px, calc(3.73vw - 4.67px), 24px);
}
.campaign-hero-action {
min-height: clamp(44px, calc(4.27vw + 27.23px), 60px);
height: auto;
font-size: clamp(17px, calc(0.27vw + 15.95px), 18px);
}
.campaign-hero-action--secondary {
width: clamp(145px, calc(3.47vw + 131.38px), 158px);
}
.campaign-hero-action--primary {
width: clamp(112px, calc(12.27vw + 63.79px), 158px);
}
.campaign-hero-action-detail,
.campaign-hero-action-long {
display: none;
}
.campaign-hero-accolades {
width: max-content;
margin-top: clamp(16px, calc(6.13vw - 8.09px), 39px);
margin-right: clamp(18px, calc(5.07vw - 1.91px), 37px);
margin-left: auto;
align-items: flex-end;
gap: 2px;
text-align: right;
font-size: clamp(10px, calc(0.53vw + 7.9px), 12px);
line-height: 1.2;
}
.campaign-hero-accolade-secondary {
display: none;
}
.campaign-proof {
height: clamp(88px, calc(9.33vw + 51.32px), 123px);
}
.campaign-proof-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: center;
gap: 23px;
}
.campaign-proof-value {
font-size: clamp(24px, calc(3.2vw + 11.42px), 36px);
}
.campaign-proof-label {
font-size: clamp(10px, calc(2.13vw + 1.62px), 18px);
line-height: 1.15;
}
.campaign-problem {
height: clamp(659px, calc(774.22px - 11.25vw), 730px);
}
.campaign-flags {
height: clamp(368px, calc(419.93px - 5.08vw), 400px);
}
.campaign-section-eyebrow,
.campaign-section-title,
.campaign-section-copy {
left: var(--campaign-content-inset);
}
.campaign-section-eyebrow {
font-size: clamp(10px, calc(0.32vw + 8.75px), 12px);
line-height: 1.25;
}
.campaign-section-title {
font-size: clamp(22px, calc(0.32vw + 20.75px), 24px);
line-height: 1.07;
}
.campaign-section-copy {
font-size: clamp(14px, calc(0.32vw + 12.75px), 16px);
}
.campaign-problem-eyebrow {
top: 35px;
}
.campaign-problem-title {
top: clamp(59px, calc(1.11vw + 54.64px), 66px);
width: min(
calc(100% - var(--campaign-content-inset) - var(--campaign-content-inset)),
clamp(331px, calc(45.17vw + 153.49px), 616px)
);
}
.campaign-problem-copy {
top: clamp(127px, calc(157.83px - 3.01vw), 146px);
width: min(
calc(100% - var(--campaign-content-inset) - 20px),
clamp(327px, calc(61.65vw + 84.72px), 716px)
);
}
.campaign-problem-carousel {
top: clamp(191px, calc(278.63px - 8.56vw), 245px);
}
.campaign-flags-eyebrow {
top: clamp(33px, calc(39.49px - 0.63vw), 37px);
}
.campaign-flags-title {
top: clamp(62px, calc(68.49px - 0.63vw), 66px);
width: min(
calc(100% - var(--campaign-content-inset) - var(--campaign-content-inset)),
clamp(331px, calc(52.77vw + 123.6px), 664px)
);
}
.campaign-flags-copy {
top: clamp(130px, calc(160.83px - 3.01vw), 149px);
width: min(
calc(100% - var(--campaign-content-inset) - 20px),
clamp(327px, calc(63.87vw + 76px), 730px)
);
}
.campaign-flags-carousel {
top: clamp(215px, calc(285px - 17.81vw), 228px);
}
[data-theme='campaign-site'] [data-carousel='problem'] [data-carousel-track] {
gap: clamp(10px, calc(1.74vw + 3.16px), 21px);
}
[data-theme='campaign-site'] [data-carousel='flags'] [data-carousel-track] {
gap: clamp(10px, calc(0.63vw + 7.51px), 14px);
}
[data-theme='campaign-site'] [data-carousel-chips] {
gap: clamp(6px, calc(0.16vw + 5.37px), 7px);
}
@media (min-width: 24.5625rem) {
.campaign-flags-carousel {
top: clamp(184px, calc(234.31px - 4.91vw), 215px);
}
}
@media (min-width: 40rem) {
.campaign-hero-action-detail,
.campaign-hero-action-long {
display: block;
}
.campaign-hero-action-short {
display: none;
}
}
@media (min-width: 64rem) {
[data-theme='campaign-site'] {
--campaign-desktop-portrait-width: clamp(465px, calc(26.45vw + 194.15px), 702px);
--campaign-desktop-portrait-height: clamp(517px, calc(29.41vw + 215.84px), 781px);
--campaign-desktop-hero-height: max(
var(--campaign-desktop-portrait-height),
clamp(477px, min(clamp(543px, 52.8vw, 1014px), calc(-16.79vw + 108.01dvh + 26.8px)), 871px)
);
--campaign-proof-min-height: clamp(124px, calc(244px - 9.38vw), 148px);
--campaign-proof-max-height: clamp(148px, calc(6.81vw + 78.3px), 209px);
}
.campaign-hero {
--campaign-hero-height: var(--campaign-desktop-hero-height);
}
.campaign-home-logo {
top: 55px;
left: 47px;
width: 197px;
height: 150px;
top: clamp(55px, calc(15.23vw - 101px), 94px);
left: clamp(47px, calc(6.64vw - 21px), 64px);
width: clamp(197px, calc(23.44vw - 43px), 257px);
height: clamp(150px, calc(17.97vw - 34px), 196px);
transform: none;
background-image: url('/Prancheta%203%403x.png');
background-position: -43px -67px;
background-size: 283px 284px;
background-position: clamp(-56.75px, calc(-5.37vw + 12px), -43px)
clamp(-87.55px, calc(-8.03vw + 15.2px), -67px);
background-size: 143.65% 189.3%;
}
.campaign-hero-portrait {
top: auto;
bottom: 0;
left: clamp(153px, calc(33.59vw - 191px), 239px);
width: var(--campaign-desktop-portrait-width);
aspect-ratio: 465 / 517;
}
.campaign-hero-portrait-image {
top: -3.67%;
left: 0.25%;
width: 106.45%;
}
.campaign-hero-ally {
top: auto;
bottom: var(--campaign-ally-bottom, 0px);
width: clamp(177px, calc(6.14vw + 93.16px), 211px);
}
.campaign-hero-ally--rui {
left: clamp(-27px, calc(37.89vw - 415px), 70px);
--campaign-ally-bottom: clamp(67px, calc(102px - 2.73vw), 74px);
}
.campaign-hero-ally--wagner {
left: clamp(-75px, calc(37.89vw - 463px), 22px);
--campaign-ally-bottom: 0px;
z-index: 1;
}
.campaign-hero-ally--jeronimo {
left: clamp(63px, calc(31.25vw - 257px), 143px);
--campaign-ally-bottom: clamp(49px, calc(1.56vw + 33px), 53px);
z-index: 1;
}
.campaign-hero-ally--lula {
left: clamp(-8px, calc(37.89vw - 396px), 89px);
width: clamp(238px, calc(8.3vw + 124.6px), 284px);
--campaign-ally-bottom: 0px;
}
.campaign-hero-overlay {
display: none;
}
.campaign-hero-content {
--campaign-hero-content-right: clamp(47px, calc(40.23vw - 365px), 150px);
padding-top: clamp(106px, calc(8.2vw + 22px), 127px);
padding-bottom: 11px;
}
.campaign-hero-title {
width: 392px;
margin-right: var(--campaign-hero-content-right);
margin-left: auto;
text-align: right;
font-size: 36px;
line-height: 1.2;
}
.campaign-hero-copy {
width: 418px;
margin-top: 19.6px;
margin-right: var(--campaign-hero-content-right);
margin-left: auto;
text-align: right;
font-size: 18px;
}
.campaign-hero-actions {
margin-top: 29px;
margin-right: var(--campaign-hero-content-right);
margin-left: auto;
gap: 24px;
}
.campaign-hero-action {
width: 158px;
min-height: 60px;
height: auto;
font-size: 18px;
}
.campaign-hero-action-detail,
.campaign-hero-action-long,
.campaign-hero-accolade-secondary {
display: block;
}
.campaign-hero-action-short {
display: none;
}
.campaign-hero-accolades {
margin-top: clamp(64px, calc(169px - 8.2vw), 85px);
margin-right: 0;
margin-left: 559px;
align-items: flex-start;
gap: 8px;
text-align: left;
font-size: 12px;
line-height: 15px;
}
.campaign-proof {
height: clamp(
var(--campaign-proof-min-height),
calc(100dvh - var(--campaign-desktop-hero-height)),
var(--campaign-proof-max-height)
);
}
.campaign-proof-list {
grid-template-columns: 147px 178px 183px;
align-items: start;
justify-content: space-between;
gap: 0;
padding-top: clamp(17px, calc(47px - 2.34vw), 23px);
}
.campaign-proof-value {
font-size: 36px;
line-height: 1.1;
}
.campaign-proof-label {
font-size: 18px;
line-height: 1.25;
}
}
@media (min-width: 80rem) {
.campaign-hero-portrait {
left: clamp(239px, calc(100vw - 1041px), 325px);
}
.campaign-home-logo {
top: clamp(45px, calc(823.3px - 56.98vw), 94px);
left: clamp(64px, calc(8.14vw - 40.19px), 71px);
width: clamp(257px, calc(43.02vw - 293.7px), 294px);
height: clamp(196px, calc(32.56vw - 220.74px), 224px);
background-position: clamp(-64.92px, calc(64.85px - 9.5vw), -56.75px)
clamp(-100.05px, calc(98.5px - 14.54vw), -87.55px);
}
.campaign-hero-ally--rui {
left: clamp(70px, calc(83.72vw - 1001.63px), 142px);
--campaign-ally-bottom: clamp(64px, calc(111.65px - 3.49vw), 67px);
}
.campaign-hero-ally--wagner {
left: clamp(22px, calc(87.21vw - 1094.28px), 97px);
--campaign-ally-bottom: 0px;
}
.campaign-hero-ally--jeronimo {
left: clamp(143px, calc(93.02vw - 1047.7px), 223px);
--campaign-ally-bottom: clamp(49px, calc(112.53px - 4.65vw), 53px);
}
.campaign-hero-ally--lula {
left: clamp(89px, calc(87.21vw - 1027.28px), 164px);
--campaign-ally-bottom: 0px;
}
.campaign-hero-content {
--campaign-hero-content-right: clamp(150px, calc(72.09vw - 772.8px), 212px);
padding-top: clamp(106px, calc(439.56px - 24.42vw), 127px);
}
.campaign-hero-accolades {
margin-top: clamp(64px, calc(122.09vw - 1498.8px), 169px);
margin-left: clamp(559px, calc(186.05vw - 1822.4px), 719px);
}
.campaign-proof-list {
padding-top: clamp(17px, calc(19.77vw - 236.02px), 34px);
}
}
@media (min-width: 85.375rem) {
.campaign-hero-portrait {
left: clamp(325px, calc(35.92vw - 165.7px), 524px);
}
.campaign-home-logo {
top: clamp(45px, calc(7.4vw - 56.1px), 86px);
left: clamp(71px, calc(22.38vw - 234.8px), 195px);
width: clamp(294px, calc(16.43vw + 69.6px), 385px);
height: clamp(224px, calc(12.64vw + 51.4px), 294px);
background-position: clamp(-85px, calc(-3.63vw - 15.35px), -64.92px)
clamp(-131px, calc(-5.6vw - 23.6px), -100.05px);
}
.campaign-hero-ally--rui {
left: clamp(142px, calc(30.51vw - 274.7px), 311px);
--campaign-ally-bottom: clamp(64px, calc(2.17vw + 34.4px), 76px);
}
.campaign-hero-ally--wagner {
left: clamp(97px, calc(28.88vw - 297.5px), 257px);
--campaign-ally-bottom: 0px;
}
.campaign-hero-ally--jeronimo {
left: clamp(223px, calc(35.56vw - 262.8px), 420px);
--campaign-ally-bottom: clamp(49px, calc(1.44vw + 29.27px), 57px);
}
.campaign-hero-ally--lula {
left: clamp(164px, calc(32.31vw - 277.4px), 343px);
--campaign-ally-bottom: 0px;
}
.campaign-hero-content {
--campaign-hero-content-right: clamp(212px, calc(1.44vw + 192.27px), 220px);
padding-top: clamp(106px, calc(15.16vw - 101.1px), 190px);
padding-bottom: clamp(7px, calc(24.32px - 0.9vw), 12px);
}
.campaign-hero-title {
width: clamp(392px, calc(38.81vw - 138.1px), 607px);
font-size: clamp(36px, calc(2.17vw + 6.41px), 48px);
}
.campaign-hero-copy {
width: clamp(418px, calc(26.71vw + 53.05px), 566px);
margin-top: clamp(19.6px, calc(3.83vw - 32.68px), 40.8px);
font-size: clamp(18px, calc(1.08vw + 3.2px), 24px);
}
.campaign-hero-actions {
margin-top: clamp(29px, calc(6.68vw - 62.23px), 66px);
gap: clamp(24px, calc(0.36vw + 19.07px), 26px);
}
.campaign-hero-action {
width: clamp(158px, calc(2.89vw + 118.55px), 174px);
min-height: clamp(60px, calc(1.08vw + 45.2px), 66px);
height: auto;
font-size: clamp(18px, calc(0.36vw + 13.07px), 20px);
}
.campaign-hero-accolades {
margin-top: clamp(169px, calc(5.87vw + 88.76px), 201.5px);
margin-left: clamp(719px, calc(73.83vw - 289.5px), 1128px);
font-size: clamp(11px, calc(0.36vw + 7.07px), 14px);
line-height: 1.2;
}
.campaign-proof-list {
padding-top: clamp(30px, calc(1.08vw + 19.2px), 40px);
}
}

View file

@ -4,31 +4,32 @@ const SUPPORT_URL = 'https://apoiar.me/jorgesolla'
const allyImages = [
{
id: 'rui',
src: '/RUI%20-%202-2.avif',
width: 177,
height: 265,
className: 'left-[-22px] top-[144px] h-[265px] w-[177px] lg:left-[-35px] lg:top-[238px] lg:z-0',
sizes: '(min-width: 1366px) calc(6.14vw + 93px), 177px',
},
{
id: 'wagner',
src: '/WAGNER%20-%202-9%20final.avif',
width: 177,
height: 265,
className:
'left-[-69px] top-[148px] h-[265px] w-[177px] lg:left-[-75px] lg:top-[278px] lg:z-[1]',
sizes: '(min-width: 1366px) calc(6.14vw + 93px), 177px',
},
{
id: 'jeronimo',
src: '/Jeronimo.avif',
width: 177,
height: 265,
className:
'left-[224px] top-[140px] h-[265px] w-[177px] lg:left-[84px] lg:top-[249px] lg:z-[1]',
sizes: '(min-width: 1366px) calc(6.14vw + 93px), 177px',
},
{
id: 'lula',
src: '/Lula.avif',
width: 216,
height: 213,
className:
'left-[262px] top-[179px] h-[213px] w-[216px] lg:left-[-8px] lg:top-[308px] lg:z-[2] lg:h-[235px] lg:w-[238px]',
sizes: '(min-width: 1366px) calc(8.3vw + 125px), (min-width: 1024px) 238px, 216px',
},
]
@ -36,9 +37,9 @@ export const CampaignHero = () => (
<section
aria-labelledby="campaign-title"
data-home-section="hero"
className="relative h-[745px] overflow-hidden bg-(--pt-red) text-white max-[383px]:h-[780px] lg:h-[543px]"
className="campaign-hero relative overflow-hidden bg-(--pt-red) text-white"
>
<div className="relative mx-auto h-full w-full max-w-[393px] lg:max-w-[1024px]">
<div className="campaign-hero-stage relative mx-auto h-full w-full">
<div
role="img"
aria-label="Jorge Solla 1313 — candidato a deputado federal"
@ -57,43 +58,41 @@ export const CampaignHero = () => (
alt=""
width={image.width}
height={image.height}
sizes="(min-width: 1024px) 238px, 216px"
sizes={image.sizes}
loading="eager"
className={`absolute max-w-none object-contain ${image.className}`}
className={`campaign-hero-ally campaign-hero-ally--${image.id} absolute max-w-none object-contain`}
/>
))}
</div>
<div className="absolute top-[53px] left-1/2 z-20 h-[469px] w-[422px] -translate-x-1/2 overflow-hidden lg:top-[26px] lg:left-[153px] lg:h-[517px] lg:w-[465px] lg:translate-x-0">
<div className="campaign-hero-portrait absolute z-20 overflow-hidden">
<Image
src="/JOA00162.avif"
alt="Jorge Solla, candidato a deputado federal pela Bahia"
width={495}
height={742}
sizes="(min-width: 1024px) 495px, 449px"
sizes="(min-width: 1720px) 747px, (min-width: 1280px) 595px, (min-width: 1024px) 495px, 449px"
priority
className="absolute -top-[17px] left-px h-[673px] w-[449px] max-w-none object-contain lg:-top-[19px] lg:h-[742px] lg:w-[495px]"
className="campaign-hero-portrait-image absolute h-auto max-w-none object-contain"
/>
</div>
<div
data-mobile-hero-overlay
className="absolute top-[300px] left-1/2 z-20 h-[245px] w-screen -translate-x-1/2 bg-[linear-gradient(to_bottom,transparent_0%,rgba(162,28,28,0.5)_38%,#a21c1c_78%)] lg:hidden"
className="campaign-hero-overlay absolute z-20 bg-[linear-gradient(to_bottom,transparent_0%,rgba(162,28,28,0.5)_38%,#a21c1c_78%)]"
/>
<div className="absolute top-[462px] left-0 z-30 w-full lg:top-[106px] lg:right-[47px] lg:left-auto lg:w-[392px]">
<div className="campaign-hero-content relative z-30">
<h1
id="campaign-title"
aria-label="UM MANDATO DO TAMANHO DA BAHIA"
className="m-0 border-0 p-0 text-center font-[family-name:var(--font-exo2)] text-[32px] leading-[1.12] tracking-[-0.02em] text-balance uppercase lg:text-right lg:text-[36px] lg:leading-[1.2]"
className="campaign-hero-title m-0 border-0 p-0 font-[family-name:var(--font-exo2)] leading-[1.12] tracking-[-0.02em] text-balance uppercase"
>
<span className="block font-medium">Um mandato do</span>
<span className="block font-black">tamanho da Bahia</span>
</h1>
</div>
<div className="absolute top-[547px] left-0 z-30 w-full lg:static">
<p className="m-0 ml-6 w-[calc(100%_-_66px)] text-left font-sans text-[14px] leading-[1.2] uppercase lg:absolute lg:top-[212px] lg:right-[47px] lg:z-30 lg:ml-0 lg:w-[418px] lg:text-right lg:text-[18px]">
<p className="campaign-hero-copy m-0 text-left font-sans leading-[1.2] uppercase">
Médico sanitarista.{' '}
<strong className="font-extrabold text-(--pt-yellow)">Criou o SAMU 192</strong> e o{' '}
<strong className="font-extrabold">Brasil Sorridente</strong>. Já comandou a saúde em
@ -101,15 +100,15 @@ export const CampaignHero = () => (
à educação.
</p>
<div className="mt-5 mr-[18px] flex items-stretch justify-end gap-2.5 lg:absolute lg:top-[349px] lg:right-[47px] lg:z-30 lg:mt-0 lg:mr-0 lg:justify-start lg:gap-6">
<div className="campaign-hero-actions flex items-stretch justify-start">
<a
href="#bandeiras"
aria-label="Conhecer bandeiras"
data-cta="secondary"
className="campaign-action-feedback inline-flex min-h-11 w-[145px] flex-col items-center justify-center rounded-[10px] border-2 border-white/55 px-3 text-center font-[family-name:var(--font-exo2)] text-[17px] leading-[1.05] font-extrabold text-white no-underline hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-(--pt-red) focus-visible:outline-none lg:h-[60px] lg:w-[158px] lg:text-[18px]"
className="campaign-action-feedback campaign-hero-action campaign-hero-action--secondary inline-flex min-h-11 flex-col items-center justify-center rounded-[10px] border-2 border-white/55 px-3 text-center font-[family-name:var(--font-exo2)] leading-[1.05] font-extrabold text-white no-underline hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-(--pt-red) focus-visible:outline-none"
>
Conhecer
<small className="hidden text-[12px] leading-none font-semibold lg:block">
<small className="campaign-hero-action-detail text-[12px] leading-none font-semibold">
bandeiras
</small>
</a>
@ -119,24 +118,28 @@ export const CampaignHero = () => (
target="_blank"
rel="noopener"
data-cta="primary"
className="campaign-action-feedback inline-flex min-h-11 w-[112px] flex-col items-center justify-center rounded-[10px] bg-(--pt-yellow) px-2 text-center font-[family-name:var(--font-exo2)] text-[17px] leading-[1.05] font-black text-(--pt-yellow-ink) no-underline hover:brightness-95 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-(--pt-red) focus-visible:outline-none lg:h-[60px] lg:w-[158px] lg:text-[18px]"
className="campaign-action-feedback campaign-hero-action campaign-hero-action--primary inline-flex min-h-11 flex-col items-center justify-center rounded-[10px] bg-(--pt-yellow) px-2 text-center font-[family-name:var(--font-exo2)] leading-[1.05] font-black text-(--pt-yellow-ink) no-underline hover:brightness-95 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-(--pt-red) focus-visible:outline-none"
>
<span className="lg:hidden">Apoiar</span>
<span className="hidden lg:inline">Quero apoiar</span>
<small className="hidden text-[12px] leading-none font-semibold lg:block">
<span className="campaign-hero-action-short">Apoiar</span>
<span className="campaign-hero-action-long">Quero apoiar</span>
<small className="campaign-hero-action-detail text-[12px] leading-none font-semibold">
é rápido, 30 segundos
</small>
</a>
</div>
</div>
<div className="absolute right-[18px] bottom-3 z-30 flex flex-col items-end gap-0.5 text-right font-sans text-[10px] leading-3 text-white/80 lg:top-[494px] lg:right-auto lg:bottom-auto lg:left-[559px] lg:items-start lg:gap-2 lg:text-left lg:text-[12px] lg:leading-[15px]">
<span aria-label="DIAP entre os 40 melhores da Câmara">
<strong className="font-extrabold text-white">DIAP</strong> entre os 40 melhores da Câmara
</span>
<span aria-label="Mais votado do PT-BA em 2022">
<strong className="font-extrabold text-white">Mais votado</strong> do PT-BA em 2022
</span>
<div className="campaign-hero-accolades flex flex-col font-sans text-white/80">
<span aria-label="DIAP entre os 40 melhores da Câmara">
<strong className="font-extrabold text-white">DIAP</strong> entre os 40 melhores da
Câmara
</span>
<span
aria-label="Mais votado do PT-BA em 2022"
className="campaign-hero-accolade-secondary"
>
<strong className="font-extrabold text-white">Mais votado</strong> do PT-BA em 2022
</span>
</div>
</div>
</div>
</section>

View file

@ -86,6 +86,269 @@ test.describe('Frontend', () => {
expect(actionsBox.y - (subtitleBox.y + subtitleBox.height)).toBeGreaterThanOrEqual(16)
})
test('adapts the campaign first fold fluidly across the reference viewport sizes', async ({
page,
}) => {
await page.goto('/')
await expect(page).toHaveURL(/\/$/)
const measureFirstFold = async (width: number, height: number) => {
await page.setViewportSize({ width, height })
return page.evaluate(() => {
const hero = document.querySelector<HTMLElement>('[data-home-section="hero"]')
const proof = document.querySelector<HTMLElement>('[data-home-section="proof"]')
const scrollContainer = document.querySelector<HTMLElement>('[data-theme="campaign-site"]')
if (!hero || !proof || !scrollContainer)
throw new Error('A primeira dobra não foi montada.')
const box = (selector: string) => {
const element = document.querySelector<HTMLElement>(selector)
if (!element) throw new Error(`Elemento responsivo ausente: ${selector}`)
const bounds = element.getBoundingClientRect()
return { x: bounds.x, y: bounds.y, width: bounds.width }
}
const heroBounds = hero.getBoundingClientRect()
return {
heroHeight: heroBounds.height,
firstFoldHeight: heroBounds.height + proof.getBoundingClientRect().height,
horizontalOverflow: scrollContainer.scrollWidth - scrollContainer.clientWidth,
title: box('.campaign-hero-title'),
portrait: box('.campaign-hero-portrait'),
secondaryAction: box('[data-cta="secondary"]'),
}
})
}
const penpotBoards = [
{
width: 393,
height: 852,
heroHeight: 745,
firstFoldHeight: 833,
title: [-5, 462, 392],
portrait: [-6, 53, 422],
secondaryAction: [108, 652, 145],
},
{
width: 768,
height: 1024,
heroHeight: 901,
firstFoldHeight: 1024,
title: [121, 497, 537],
portrait: [193, 88, 422],
secondaryAction: [214, 774, 158],
},
{
width: 1024,
height: 900,
heroHeight: 543,
firstFoldHeight: 691,
title: [585, 106, 392],
portrait: [153, 26, 465],
secondaryAction: [637, 349, 158],
},
{
width: 1280,
height: 800,
heroHeight: 676,
firstFoldHeight: 800,
title: [738, 127, 392],
portrait: [239, 84, 533],
secondaryAction: [790, 370, 158],
},
{
width: 1366,
height: 768,
heroHeight: 627,
firstFoldHeight: 768,
title: [762, 106, 392],
portrait: [325, 9, 555],
secondaryAction: [814, 349, 158],
},
{
width: 1920,
height: 1080,
heroHeight: 871,
firstFoldHeight: 1080,
title: [1093, 190, 607],
portrait: [524, 91, 702],
secondaryAction: [1325, 556, 174],
},
]
for (const board of penpotBoards) {
const measured = await measureFirstFold(board.width, board.height)
expect(Math.abs(measured.heroHeight - board.heroHeight)).toBeLessThanOrEqual(2)
expect(Math.abs(measured.firstFoldHeight - board.firstFoldHeight)).toBeLessThanOrEqual(2)
expect(measured.horizontalOverflow).toBeLessThanOrEqual(1)
for (const key of ['title', 'portrait', 'secondaryAction'] as const) {
const [x, y, width] = board[key]
expect(Math.abs(measured[key].x - x)).toBeLessThanOrEqual(8)
expect(Math.abs(measured[key].y - y)).toBeLessThanOrEqual(2)
expect(Math.abs(measured[key].width - width)).toBeLessThanOrEqual(2)
}
}
const compact = [
await measureFirstFold(393, 852),
await measureFirstFold(580, 900),
await measureFirstFold(768, 1024),
]
expect(compact[1].heroHeight).toBeGreaterThan(compact[0].heroHeight)
expect(compact[1].heroHeight).toBeLessThan(compact[2].heroHeight)
const standardDesktop = [
await measureFirstFold(1024, 900),
await measureFirstFold(1152, 900),
await measureFirstFold(1280, 800),
]
expect(standardDesktop[1].heroHeight).toBeGreaterThan(standardDesktop[0].heroHeight)
expect(standardDesktop[1].heroHeight).toBeLessThan(standardDesktop[2].heroHeight)
const wideDesktop = [
await measureFirstFold(1366, 768),
await measureFirstFold(1600, 900),
await measureFirstFold(1920, 1080),
]
expect(wideDesktop[1].heroHeight).toBeGreaterThan(wideDesktop[0].heroHeight)
expect(wideDesktop[1].heroHeight).toBeLessThan(wideDesktop[2].heroHeight)
const aspectBoundary = [
await measureFirstFold(1280, 747),
await measureFirstFold(1280, 746),
await measureFirstFold(1920, 1121),
await measureFirstFold(1920, 1120),
]
expect(Math.abs(aspectBoundary[0].heroHeight - aspectBoundary[1].heroHeight)).toBeLessThan(2)
expect(Math.abs(aspectBoundary[2].heroHeight - aspectBoundary[3].heroHeight)).toBeLessThan(2)
})
test('anchors the desktop portraits to the hero floor without height-dependent scaling', async ({
page,
}) => {
await page.goto('/')
await expect(page).toHaveURL(/\/$/)
const measureComposition = async (width: number, height: number) => {
await page.setViewportSize({ width, height })
return page.evaluate(() => {
const bounds = (selector: string) => {
const element = document.querySelector<HTMLElement>(selector)
if (!element) throw new Error(`Elemento da composição ausente: ${selector}`)
return element.getBoundingClientRect()
}
const hero = bounds('[data-home-section="hero"]')
const stage = bounds('.campaign-hero-stage')
const portrait = bounds('.campaign-hero-portrait')
const lula = bounds('.campaign-hero-ally--lula')
const wagner = bounds('.campaign-hero-ally--wagner')
const title = bounds('.campaign-hero-title')
return {
portrait: { left: portrait.left, width: portrait.width },
portraitBottomGap: hero.bottom - portrait.bottom,
lulaBottomGap: hero.bottom - lula.bottom,
wagnerBottomGap: hero.bottom - wagner.bottom,
portraitTitleGap: title.left - portrait.right,
stageWidth: stage.width,
}
})
}
const short = await measureComposition(1340, 70)
const regular = await measureComposition(1340, 700)
expect(Math.abs(short.portrait.width - regular.portrait.width)).toBeLessThanOrEqual(1)
expect(Math.abs(short.portrait.left - regular.portrait.left)).toBeLessThanOrEqual(1)
for (const composition of [short, regular]) {
expect(Math.abs(composition.portraitBottomGap)).toBeLessThanOrEqual(1)
expect(Math.abs(composition.lulaBottomGap)).toBeLessThanOrEqual(1)
expect(Math.abs(composition.wagnerBottomGap)).toBeLessThanOrEqual(1)
}
const ultraWide = await measureComposition(2358, 1030)
expect(Math.abs(ultraWide.portraitBottomGap)).toBeLessThanOrEqual(1)
expect(Math.abs(ultraWide.lulaBottomGap)).toBeLessThanOrEqual(1)
expect(Math.abs(ultraWide.wagnerBottomGap)).toBeLessThanOrEqual(1)
expect(ultraWide.portraitTitleGap).toBeLessThanOrEqual(64)
expect(ultraWide.stageWidth).toBeLessThanOrEqual(1920)
})
test('keeps the hero readable with WCAG text spacing overrides', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveURL(/\/$/)
await page.addStyleTag({
content: `
[data-home-section="hero"] :is(h1, p, a, small, span, strong) {
line-height: 1.5 !important;
letter-spacing: 0.12em !important;
word-spacing: 0.16em !important;
}
[data-home-section="hero"] p { margin-bottom: 2em !important; }
`,
})
for (const viewport of [
{ width: 320, height: 568 },
{ width: 393, height: 852 },
{ width: 768, height: 1024 },
{ width: 1024, height: 597 },
{ width: 1366, height: 768 },
{ width: 1600, height: 600 },
{ width: 1920, height: 840 },
]) {
await page.setViewportSize(viewport)
const spacing = await page.evaluate(() => {
const element = (selector: string) => {
const match = document.querySelector<HTMLElement>(selector)
if (!match) throw new Error(`Elemento ausente no teste de espaçamento: ${selector}`)
return match
}
const bounds = (selector: string) => {
return element(selector).getBoundingClientRect()
}
const hero = bounds('[data-home-section="hero"]')
const title = bounds('.campaign-hero-title')
const copy = bounds('.campaign-hero-copy')
const actions = bounds('.campaign-hero-actions')
const accolades = bounds('.campaign-hero-accolades')
const secondaryAction = element('[data-cta="secondary"]')
const primaryAction = element('[data-cta="primary"]')
const scrollContainer = document.querySelector<HTMLElement>('[data-theme="campaign-site"]')
if (!scrollContainer) throw new Error('Container da campanha ausente.')
const overlapArea = (first: DOMRect, second: DOMRect) =>
Math.max(0, Math.min(first.right, second.right) - Math.max(first.left, second.left)) *
Math.max(0, Math.min(first.bottom, second.bottom) - Math.max(first.top, second.top))
return {
titleCopyGap: copy.top - title.bottom,
copyActionsGap: actions.top - copy.bottom,
actionsBottomGap: hero.bottom - actions.bottom,
accoladesBottomGap: hero.bottom - accolades.bottom,
actionsAccoladesOverlap: overlapArea(actions, accolades),
primaryActionOverflow: primaryAction.scrollHeight - primaryAction.clientHeight,
secondaryActionOverflow: secondaryAction.scrollHeight - secondaryAction.clientHeight,
horizontalOverflow: scrollContainer.scrollWidth - scrollContainer.clientWidth,
}
})
expect(spacing.titleCopyGap).toBeGreaterThanOrEqual(0)
expect(spacing.copyActionsGap).toBeGreaterThanOrEqual(0)
expect(spacing.actionsBottomGap).toBeGreaterThanOrEqual(44)
expect(spacing.accoladesBottomGap).toBeGreaterThanOrEqual(0)
expect(spacing.actionsAccoladesOverlap).toBe(0)
expect(spacing.primaryActionOverflow).toBeLessThanOrEqual(0)
expect(spacing.secondaryActionOverflow).toBeLessThanOrEqual(0)
expect(spacing.horizontalOverflow).toBeLessThanOrEqual(1)
}
})
test('auto-advances and keeps carousel controls synchronized', async ({ page }) => {
await page.goto('/')