/* Componentes reutilizáveis - Elevate Solutions */

/* Botões */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-300);
  padding: 0.85em 1.6em;
  border-radius: var(--radius-sm);
  border: 1.5px solid transparent;
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
  text-decoration: none;
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: scale(0.97); }

.btn-primary {
  background: var(--sky-500);
  color: var(--navy-900);
}
/* Gesto "elevar" mais expressivo, reservado só ao CTA de conversão principal
   (Pedir orçamento e equivalentes) - deliberadamente não aplicado a
   .btn-secondary/.btn-on-dark: a intensidade do gesto é o que sinaliza qual
   é o botão mais importante da secção; espalhar por todos cancelava o
   sinal (Épico 14.6). */
.btn-primary:hover {
  background: var(--sky-600);
  /* Mantém a cor de texto de .btn-primary (--navy-900): navy sobre sky-600
     dá 5.94:1. Um "color: white" aqui dava só 2.69:1 e falhava AA (Épico 12.1). */
  transform: translateY(-3px);
  box-shadow: 0 10px 24px -6px rgba(47, 169, 217, 0.45), var(--shadow-md);
}
.btn-primary:active { transform: scale(0.97); box-shadow: none; }

.btn-secondary {
  background: var(--white);
  color: var(--navy-800);
  border-color: var(--neutral-200);
}
.btn-secondary:hover { border-color: var(--navy-700); }

.btn-on-dark {
  background: transparent;
  color: var(--text-on-dark);
  border-color: rgba(245, 247, 250, 0.35);
}
.btn-on-dark:hover { border-color: var(--sky-500); color: var(--sky-500); }

.btn-block { width: 100%; justify-content: center; }

.btn-arrow svg {
  width: var(--icon-sm);
  height: var(--icon-sm);
  transition: transform var(--duration-fast) var(--ease-out);
}
.btn-arrow:hover svg { transform: translateX(3px); }

/* Eyebrow (rótulo curto acima de um título - usar só quando classifica algo real) */
.eyebrow {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--fs-100);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sky-700);
  margin-bottom: var(--space-3);
}
.eyebrow.on-dark { color: var(--sky-500); }

/* Cards */
.card {
  position: relative;
  background: var(--white);
  border: 1px solid var(--neutral-200);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  transition: border-color var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-out);
}
.card:hover {
  border-color: var(--sky-500);
  box-shadow: var(--shadow-md);
  transform: translateY(-3px);
}

/* Chip de ícone (Épico 16): fundo em gradiente sky + sombra colorida em vez
   do quadrado liso original - a cor de ícone passa de navy-800 para
   sky-700, alinhado com o brand.md ("Cor Principal: acento vivo... ícones,
   detalhes"), que já indicava o sky como a cor certa para isto. */
.card-icon {
  width: var(--icon-xl);
  height: var(--icon-xl);
  border-radius: var(--radius-md);
  background: linear-gradient(155deg, var(--sky-100), #DCEFFB);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--sky-700);
  margin-bottom: var(--space-4);
  box-shadow: 0 10px 20px -10px rgba(93, 195, 240, 0.55);
  transition: transform var(--duration-base) var(--ease-out);
}
.card-icon svg { width: var(--icon-md); height: var(--icon-md); }
.card:hover .card-icon { transform: translateY(-2px) scale(1.05); }

.card h3 { font-size: var(--fs-500); margin-bottom: var(--space-2); }
.card p { color: var(--text-muted); font-size: var(--fs-300); }

.card-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-200);
  color: var(--navy-800);
  text-decoration: none;
}
/* "Stretched link": o card inteiro fica clicável através deste único link,
   sem duplicar <a> nem partir a leitura por leitores de ecrã. */
.card-link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
.card:hover .card-link { color: var(--sky-700); }
.card:hover { cursor: pointer; }
.card-link svg { width: var(--icon-sm); height: var(--icon-sm); transition: transform var(--duration-fast) var(--ease-out); }
.card:hover .card-link svg { transform: translateX(3px); }

/* Card placeholder - usado só até existir conteúdo real (ex: casos de estudo
   ainda por publicar). Nunca inventar nomes de clientes, números ou resultados
   aqui - o estilo "tracejado" + badge "Em breve" torna a natureza temporária
   óbvia para quem visita o site. */
.card-placeholder {
  border-style: dashed;
  border-color: var(--neutral-400);
  background: var(--neutral-100);
}
.card-placeholder:hover {
  border-color: var(--neutral-400);
  box-shadow: none;
  transform: none;
  cursor: default;
}
.card-placeholder .card-icon { background: var(--neutral-200); color: var(--text-muted); box-shadow: none; }
.card-placeholder p { color: var(--text-muted); }

/* Métrica real animada (Épico 14.4, componente do Épico 11.1) - o
   contabilizador "7 Projetos entregues" no teaser de portfólio da homepage.
   Removido no Épico 20 a pedido do utilizador ("tira este contabilizador").
   O mecanismo genérico (`[data-count-to]`/`animateCount` em
   assets/js/main.js) fica intacto - continua reutilizável para uma métrica
   futura, real e verificável, se um dia fizer sentido voltar a usar. */

/* Grelha de logótipos de clientes (Épico 11.2). Componente pronto, por
   popular. A autorização para publicar um caso de estudo (Épico 5,
   `client_authorized`) NÃO inclui autorização para usar o logótipo do
   cliente - são consentimentos diferentes (um é sobre texto/testemunho,
   o outro é sobre um ativo de marca de terceiros). Só adicionar um <img>
   aqui quando o cliente tiver dado autorização explícita para o logótipo
   especificamente, com `alt` = nome real do cliente. Nunca um logótipo
   genérico/placeholder a fingir ser de um cliente real. */
.logo-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-7) var(--space-6);
  align-items: center;
  justify-items: center;
}
@media (min-width: 700px) {
  .logo-grid { grid-template-columns: repeat(4, 1fr); }
}
.logo-grid img {
  max-height: 40px;
  width: auto;
  filter: grayscale(100%) opacity(0.55);
  transition: filter var(--duration-base) var(--ease-out);
}
.logo-grid img:hover,
.logo-grid img:focus-visible {
  filter: grayscale(0%) opacity(1);
}

/* Carrossel de testemunhos (Épico 14.5, componente do Épico 11.3). O
   scroll-snap nativo é a base - funciona por completo ao arrastar/deslizar
   mesmo sem JS nenhum; assets/js/main.js só acrescenta setas e bolinhas de
   navegação por cima disso (progressive enhancement, nunca dependência). */
.testimonial-carousel { position: relative; }
.testimonial-track {
  display: flex;
  /* Sem isto, o flex esticava todos os cartões à altura do testemunho mais
     comprido (comportamento default `stretch`) - um testemunho curto ficava
     com uma faixa de espaço vazio por baixo do texto. Cada cartão passa a
     ter só a altura do seu próprio conteúdo. */
  align-items: flex-start;
  overflow-x: auto;
  /* Por especificação, quando overflow-x não é "visible", overflow-y
     assume "auto" por omissão se não for definido - tornava este track
     também scrollável na VERTICAL, por dentro de si mesmo. Ao arrastar
     para cima com o dedo em cima do cartão no telemóvel, isso fazia
     scroll DENTRO do cartão (revelando/escondendo parte da citação) em
     vez de rolar a página - o conteúdo visível do cartão ficava
     dessincronizado da posição real da página. hidden bloqueia esse
     scroll interno; o scroll horizontal do carrossel (overflow-x)
     continua a funcionar normalmente. */
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  gap: var(--space-6);
  scrollbar-width: none;
  padding-bottom: var(--space-1);
  /* Altura fixada via JS (main.js: syncTestimonialHeight) ao tamanho real do
     testemunho ativo, em vez da altura natural do mais alto dos três
     partilhada por toda a linha flex - sem isto sobrava um vazio grande
     antes das setas/bolinhas quando o testemunho à vista era mais curto. A
     transição só suaviza a MUDANÇA entre testemunhos de tamanhos diferentes. */
  transition: height var(--duration-base) var(--ease-out);
}
.testimonial-track::-webkit-scrollbar { display: none; }

.testimonial-card {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: center;
  background: var(--white);
  border: 1px solid var(--neutral-200);
  border-radius: var(--radius-lg);
  padding: var(--space-7);
}
@media (min-width: 700px) {
  .testimonial-card { padding: var(--space-8); }
}
/* Pedido do utilizador: menos padding em mobile para sobrar mais espaço
   para a citação em si (mostrada sempre por inteiro - ver .testimonial-quote
   abaixo, já não trunca). */
@media (max-width: 699px) {
  .testimonial-card { padding: var(--space-4); }
}

.testimonial-quote {
  font-family: var(--font-display);
  font-size: var(--fs-500);
  font-weight: 600;
  line-height: var(--lh-tight);
  color: var(--navy-800);
  margin-bottom: var(--space-6);
}
/* Em mobile, 24px em negrito para uma citação longa dava muitas linhas.
   Reduz para 16px (--fs-300, o mínimo de leitura confortável) só até
   699px (mesmo breakpoint já usado para o padding do .testimonial-card,
   acima) - continua a ler-se como citação em destaque. Pedido explícito do
   utilizador: SEM truncar/reticências, a citação mostra-se sempre por
   inteiro (já se tentou um "Ler mais" com -webkit-line-clamp, removido a
   pedido). */
@media (max-width: 699px) {
  .testimonial-quote { font-size: var(--fs-300); }
}
.testimonial-quote::before { content: "\201C"; color: var(--sky-500); }
.testimonial-quote::after { content: "\201D"; color: var(--sky-500); }

/* Simplificado a pedido do utilizador: sem ícone de iniciais nem
   setor/cargo por baixo - só o nome do cliente. */
.testimonial-name { font-family: var(--font-display); font-weight: 700; color: var(--navy-800); }

.testimonial-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
  margin-top: var(--space-7);
}
.testimonial-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1.5px solid var(--neutral-200);
  background: var(--white);
  cursor: pointer;
  padding: 0;
  transition: border-color var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.testimonial-btn:hover { border-color: var(--sky-500); transform: translateY(-1px); }
.testimonial-btn svg { width: var(--icon-md); height: var(--icon-md); color: var(--navy-800); }

.testimonial-dots { display: flex; align-items: center; }
/* Alvo de toque real de 24×24px (WCAG 2.5.8), com o ponto visível a 8px lá
   dentro - encontrado numa auditoria Lighthouse durante o Épico 14.7: a
   versão original tinha o próprio ponto de 8px como alvo clicável. */
.testimonial-dot {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
}
.testimonial-dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--neutral-200);
  transition: background var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.testimonial-dot.is-active::before { background: var(--sky-700); transform: scale(1.3); }

/* Cards de caso de estudo com tratamento tipográfico/gradiente - enquanto
   não há fotografia real dos projetos (Épico 5.4). O gradiente é calculado
   de forma determinística a partir do slug (scripts/build-portfolio.mjs):
   o mesmo caso produz sempre o mesmo gradiente entre builds. O matiz fica
   sempre dentro do azul da marca (--case-hue entre 195-230, de navy a sky -
   ver tokens.css) - nunca uma cor nova, só variação de tom (Épico 14.3). */
.case-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 1px solid var(--neutral-200);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: border-color var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-out);
}
.case-card:hover {
  border-color: var(--sky-500);
  box-shadow: var(--shadow-md);
  cursor: pointer;
  transform: translateY(-3px);
}
.case-card:hover .card-link { color: var(--white); }
.case-card:hover .card-link svg { transform: translateX(3px); }

/* Sem link de saída (Épico 25) - quando um caso não tem client_website_url,
   o card não pode fingir ser clicável (não há para onde ir). Anula por
   completo o hover "interativo" de .case-card acima, mesmo princípio já
   usado em .card-placeholder para os cartões "Em breve". */
.case-card--static:hover {
  border-color: var(--neutral-200);
  box-shadow: none;
  cursor: default;
  transform: none;
}

.case-card-cover {
  position: relative;
  aspect-ratio: 16 / 10;
  display: flex;
  align-items: flex-end;
  padding: var(--space-5);
  background: linear-gradient(
    var(--case-angle, 135deg),
    hsl(var(--case-hue, 205) 55% 20%),
    hsl(calc(var(--case-hue, 205) + 15) 68% 46%)
  );
}
/* Épico 5.4: quando o caso tem cover_image_url real (Supabase Storage), o
   gradiente deixa de aparecer (a imagem cobre o cartão todo) - o
   ::after (scrim escuro, abaixo) mantém-se em ambos os casos, para o
   nome/badge continuarem legíveis seja qual for o brilho da foto. */
.case-card-cover--photo {
  background-size: cover;
  background-position: center;
}
.case-card-cover::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Scrim escuro no fundo do cover: garante contraste do nome do cliente
     seja qual for a luminosidade exata do gradiente gerado. */
  background: linear-gradient(to top, rgba(22, 33, 58, 0.7), transparent 65%);
}
.case-card-name {
  position: relative;
  z-index: 1;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-500);
  line-height: var(--lh-tight);
  color: var(--white);
}

/* Corpo do cartão em navy (Épico 16) - antes branco; o tom escuro dá ao
   cartão de portfólio uma leitura mais "produto/dashboard", consistente com
   o resto da paleta da marca. Pares de contraste já validados em
   docs/brand.md (Épico 12.1): text-on-dark/text-on-dark-muted sobre
   navy-800, sky-500 sobre navy-800 - nenhum tom novo introduzido. */
.case-card-body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: var(--space-6);
  background: var(--navy-800);
}
.case-card-body p { color: var(--text-on-dark-muted); font-size: var(--fs-300); margin-bottom: var(--space-4); }
.case-card-body .card-link { margin-top: auto; color: var(--sky-500); }

.placeholder-badge {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--fs-100);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  background: var(--white);
  border: 1px solid var(--neutral-200);
  padding: 0.25em 0.75em;
  border-radius: 999px;
  margin-bottom: var(--space-4);
}

/* Avatar de iniciais - fallback consistente quando não há foto real
   (nunca usar avatar genérico/stock como substituto de uma pessoa real) */
.avatar-initials {
  width: 88px;
  height: 88px;
  border-radius: 50%;
  background: var(--navy-800);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-500);
  flex-shrink: 0;
}
/* Pedido do utilizador: o círculo de iniciais (testemunhos e equipa em
   /sobre/) sentia-se grande demais em mobile - reduz proporcionalmente
   (círculo e letra lá dentro, para a letra continuar bem centrada e no
   mesmo rácio face ao círculo). */
@media (max-width: 699px) {
  .avatar-initials { width: 64px; height: 64px; font-size: var(--fs-400); }
}

/* Avatar de foto real - mesmo tamanho/comportamento de .avatar-initials
   (substitui o fallback de iniciais quando há fotografia real da pessoa,
   ver /sobre/). object-fit:cover porque os ficheiros de origem já vêm
   recortados em círculo com fundo transparente à volta - cover garante que
   se algum dia entrar uma foto retangular, continua bem enquadrada. */
.avatar-photo {
  width: 88px;
  height: 88px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}
@media (max-width: 699px) {
  .avatar-photo { width: 64px; height: 64px; }
}

/* Grid utilitário */
.grid {
  display: grid;
  gap: var(--space-6);
}
@media (min-width: 700px) {
  .grid-2 { grid-template-columns: repeat(2, 1fr); }
  .grid-3 { grid-template-columns: repeat(3, 1fr); }
}

/* Secções - espaçamento e cabeçalhos, usados em qualquer página. `position:
   relative` é a base para o `.section-chevron-divider` (abaixo) poder viver
   dentro de QUALQUER secção, não só `.section-alt` (que já a definia) -
   necessário a partir do Épico 15.2, que estende o divisor para além da
   homepage. Não afeta o layout de secções sem filhos posicionados. */
section { padding-block: var(--space-9); position: relative; }
/* Em mobile, os 6rem (96px) de cima E de baixo de CADA secção somavam-se ao
   padding-bottom próprio do .page-header/margin-bottom do que vem antes,
   dando vazios de 140px+ entre um título e o conteúdo logo a seguir (ex:
   /portfolio/ entre o <h1> e o 1º caso de estudo) - proporção que só faz
   sentido a uma largura de ecrã de desktop, não nos ~380-430px de um
   telemóvel. Reduz para 4rem (64px), continua a marcar separação entre
   secções sem o vazio desproporcional. */
@media (max-width: 899px) {
  section { padding-block: var(--space-8); }
}
.section-head { max-width: 640px; margin-bottom: var(--space-7); }
.section-head.center { margin-inline: auto; text-align: center; }

.section-alt { background: var(--neutral-100); position: relative; }

/* Divisor de secção em forma de chevron - eco do motivo "ascensão" nas
   transições que marcam progressão real de conteúdo (mudança de fundo
   genuína entre secções). Mesmo mecanismo do .hero-divider (home.css): vive
   na secção que "sai", pintado com a cor da secção que "entra" - nunca
   aplicado a todas as transições, só onde há uma mudança de fundo real. */
.section-chevron-divider {
  display: block;
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  width: 100%;
  height: 40px;
  pointer-events: none;
}
.section-chevron-divider path { fill: var(--neutral-100); }
.section-chevron-divider--light path { fill: var(--white); }

/* Revelação suave ao scroll (ver assets/js/main.js). Os elementos "sobem"
   ao aparecer - o mesmo gesto do motivo "ascensão" da marca, aplicado como
   princípio de interação, não só de imagem (Épico 14). */
[data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--duration-slow) var(--ease-out),
              transform var(--duration-slow) var(--ease-out);
}
[data-reveal].is-visible { opacity: 1; transform: none; }

/* Atraso escalonado em grelhas: os cards sobem em sequência, não todos ao
   mesmo tempo. Só a partir do breakpoint em que a grelha é multi-coluna -
   em ecrã estreito (coluna única) os cards já aparecem um a um naturalmente
   ao scroll, e um atraso artificial aqui só tornaria a revelação mais lenta
   sem propósito. Cobre até 8 items; grelhas maiores repetem o último atraso. */
@media (min-width: 700px) {
  .grid > [data-reveal]:nth-child(2) { transition-delay: 90ms; }
  .grid > [data-reveal]:nth-child(3) { transition-delay: 180ms; }
  .grid > [data-reveal]:nth-child(4) { transition-delay: 270ms; }
  .grid > [data-reveal]:nth-child(5) { transition-delay: 360ms; }
  .grid > [data-reveal]:nth-child(6) { transition-delay: 450ms; }
  .grid > [data-reveal]:nth-child(7) { transition-delay: 540ms; }
  .grid > [data-reveal]:nth-child(n+8) { transition-delay: 630ms; }
}

/* Keyframe partilhada do gesto "ascensão" - usada no hero da homepage
   (home.css) e na entrada mais discreta dos .page-header abaixo (Épico 14).
   Mesmo movimento em toda a parte: os elementos sobem, nunca deslizam
   lateralmente nem fazem zoom - um único vocabulário de motion. */
@keyframes ascend {
  to { opacity: 1; transform: translateY(0); }
}

/* Cabeçalho de página interior (Serviços, Sobre, Portfólio, Blog, Contacto...) */
.page-header {
  background: var(--navy-900);
  padding-block: var(--space-8) var(--space-7);
}
@media (max-width: 899px) {
  .page-header { padding-block: var(--space-7) var(--space-6); }
}
.page-header .eyebrow { color: var(--sky-500); }
.page-header h1 { color: var(--white); margin-bottom: var(--space-4); }
.page-header .lead { color: var(--text-on-dark-muted); max-width: 60ch; margin-bottom: 0; }

/* Breadcrumbs (SEO/GEO) - navegação estrutural no topo do .page-header,
   acima do eyebrow. Sempre sobre fundo escuro (--navy-900), por isso usa o
   mesmo tom claro do resto do cabeçalho em vez do link global. */
.breadcrumbs { margin-bottom: var(--space-4); }
.breadcrumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--fs-200);
  color: var(--text-on-dark-muted);
}
.breadcrumbs li:not(:last-child)::after {
  content: "/";
  margin-left: var(--space-2);
  color: var(--text-on-dark-muted);
}
.breadcrumbs a { color: var(--text-on-dark-muted); text-decoration: underline; text-underline-offset: 2px; }
.breadcrumbs a:hover { color: var(--sky-500); }
.breadcrumbs li[aria-current="page"] { color: var(--text-on-dark); }
/* Épico 12.1: o link `a` global usa --sky-700 (AA sobre fundo claro), mas
   este cabeçalho tem fundo escuro (--navy-900) - sky-700 dava só 2.9:1 aí.
   Reutiliza o mesmo tom claro do eyebrow acima, já validado sobre este fundo. */
.page-header a { color: var(--sky-500); }
.page-header a:hover { color: var(--white); }

/* Coreografia intermédia (Épico 15.1): mais percetível do que o "quase-
   simultâneo" original do Épico 14 (80/160ms - mais apertado até que o
   stagger de 90ms já usado nas grelhas [data-reveal] logo acima), mas
   continua claramente mais curta e mais discreta do que a sequência
   completa do hero (eyebrow→h1→lead→CTA, 100/220/360/500ms, --duration-slow,
   24px, sincronizada com os chevrons de fundo - home.css). Sem CTA aqui
   porque nenhum .page-header do site (estático ou gerado) tem um dentro.
   translateY alinhado aos 18px do [data-reveal] geral, para o gesto de
   "ascensão" ter a mesma distância em toda a parte. Aplica-se a QUALQUER
   .page-header no site, incluindo os gerados a partir do Supabase
   (Épico 3/5), sem precisar de tocar em HTML/scripts - só a presença das
   classes já existentes. */
.page-header .eyebrow,
.page-header h1,
.page-header .lead {
  opacity: 0;
  transform: translateY(18px);
  animation: ascend var(--duration-base) var(--ease-out) forwards;
}
.page-header h1 { animation-delay: 160ms; }
.page-header .lead { animation-delay: 320ms; }

@media (prefers-reduced-motion: reduce) {
  .page-header .eyebrow,
  .page-header h1,
  .page-header .lead { opacity: 1; transform: none; animation: none; }
}

/* Secção CTA genérica - usada em TODAS as páginas (homepage e as geradas em
   /servicos/ e /portfolio/), não só na homepage; qualquer tratamento aqui
   herda-se automaticamente para o site inteiro. */
.section-cta {
  position: relative;
  overflow: hidden; /* contém o glow decorativo abaixo dentro dos cantos arredondados */
  background: var(--navy-800);
  color: var(--text-on-dark);
  border-radius: var(--radius-lg);
  padding: var(--space-8) var(--space-6);
  text-align: center;
}
/* Mais respiração horizontal em ecrãs largos (Épico 20 - feedback do
   utilizador: o texto sentia-se demasiado perto da borda depois de remover
   a decoração de blocos empilhados abaixo, que antes preenchia visualmente
   o lado direito). Mesmo padrão incremental já usado em `.container`
   (base.css: --space-5 → --space-7 a partir de 900px). */
@media (min-width: 900px) {
  .section-cta { padding-inline: var(--space-8); }
}
/* Épico 23: o reset global (`* { margin: 0; }`, base.css) tinha deixado o
   h2/p/btn-group deste componente sem nenhum espaço vertical entre si -
   texto e botões coladas. Reintroduzido explicitamente aqui, em vez de
   confiar num margin por omissão que já não existe no resto do site. */
.section-cta h2 { color: var(--white); margin-bottom: var(--space-4); }
.section-cta p { color: var(--text-on-dark-muted); margin-inline: auto; margin-bottom: var(--space-6); }
.section-cta .btn-group { justify-content: center; }

/* Glow ambiente navy→sky (Épico 15.4) - o gradiente de impacto que o
   Épico 14 já tinha previsto para hero/CTA na "Direção criativa" mas nunca
   chegou a aplicar; implementado agora como camada decorativa atrás do
   conteúdo, nunca sobre texto corrido. `.section-cta > *` sobe o conteúdo
   real para cima do glow - sem isto, o glow (que vem depois no DOM)
   tapava-o pela ordem natural de stacking. */
.section-cta::before {
  content: "";
  position: absolute;
  inset: -20% -10%;
  background: radial-gradient(circle at 30% 20%, rgba(93, 195, 240, 0.28), transparent 60%);
  pointer-events: none;
  z-index: 0;
}
.section-cta > * { position: relative; z-index: 1; }

/* Composição decorativa "blocos empilhados" (Épico 16) removida no Épico 20
   - feedback direto do utilizador ("tira esse quadrado, não gostei"). O CTA
   fica só com o glow ambiente (acima) por trás do texto. */

/* Parallax subtil do glow ao scroll - mesma técnica e as mesmas restrições
   do hero (home.css): scroll-driven animation nativa via `view()`, sem JS,
   atrás de @supports, restrita a ≥700px e a prefers-reduced-motion:
   no-preference explícito (a regra global de reduced-motion não cobre com
   confiança animações guiadas por timeline em vez de tempo). */
@supports (animation-timeline: view()) {
  @media (min-width: 700px) and (prefers-reduced-motion: no-preference) {
    .section-cta::before {
      animation: cta-parallax linear both;
      animation-timeline: view();
    }
  }
}
@keyframes cta-parallax {
  to { transform: translateY(-6%); }
}

.btn-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: center;
}
.btn-group.center { justify-content: center; }
/* Pedido do utilizador: em ecrãs ≤557px os botões devem ocupar toda a
   largura disponível, alinhados com os cartões acima (não uma largura
   fixa pequena - 240px, tentado antes, ficava muito mais estreito do que
   os cartões). 100% da largura do próprio .btn-group, que por sua vez já
   ocupa a largura toda do .container - mesma largura dos cartões. */
@media (max-width: 557px) {
  .btn-group .btn { width: 100%; justify-content: center; }
}

/* Utilitário pontual de espaçamento - usar tokens, nunca valores soltos */
.mb-7 { margin-bottom: var(--space-7); }

/* Formulário */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
  /* Quando .field vive dentro de um grid/flex (ex: .contact-form-row em
     contact.css, nome+email lado a lado), o valor por omissão de
     min-width para itens de grid/flex é "auto" - resolve para a largura
     NATURAL do conteúdo, aqui o próprio <input> (que tem um "size" HTML
     implícito de 20 caracteres). Isso impedia o campo de encolher abaixo
     dessa largura, empurrando o cartão inteiro para fora do ecrã à
     direita em mobile - mesma causa raiz já corrigida para o email em
     .contact-info-text (contact.css). */
  min-width: 0;
}
.field label {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-200);
  color: var(--navy-800);
}
.field input,
.field textarea,
.field select {
  border: 1.5px solid var(--neutral-200);
  border-radius: var(--radius-sm);
  padding: 0.75em 1em;
  background: var(--white);
  transition: border-color var(--duration-fast) var(--ease-out);
}
.field input:focus,
.field textarea:focus,
.field select:focus {
  border-color: var(--sky-700);
  outline: none;
}
.field .hint { font-size: var(--fs-100); color: var(--text-muted); }
.field.has-error input,
.field.has-error textarea { border-color: var(--error); }
.field .error-message { font-size: var(--fs-100); color: var(--error); }

.form-status {
  border-radius: var(--radius-sm);
  padding: var(--space-4);
  font-weight: 600;
  display: none;
}
.form-status.is-success { display: block; background: #E9F7EF; color: var(--success); }

/* Botão flutuante de WhatsApp (Épico 19) - presente em todas as páginas,
   sempre com o próprio fundo verde da marca do WhatsApp (a única exceção
   deliberada à paleta navy/sky do site: é a cor universalmente
   reconhecível do WhatsApp, não teria a mesma função de contacto imediato
   com um tom azul). `position: fixed` funciona da mesma forma em fundo
   claro ou escuro, por isso vive em components.css (partilhado), não em
   theme-dark.css nem home.css. */
.whatsapp-float {
  position: fixed;
  right: var(--space-5);
  bottom: var(--space-5);
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: #25D366;
  box-shadow: 0 10px 24px -6px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.25);
  transition: transform var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out), opacity var(--duration-base) var(--ease-out);
}
.whatsapp-float:hover {
  transform: translateY(-3px) scale(1.05);
  box-shadow: 0 14px 30px -6px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
}
/* Escondido enquanto o rodapé está em vista (main.js, IntersectionObserver)
   - sobrepunha-se aos links de email/telefone do rodapé, exatamente no
   mesmo canto. opacity+scale (não display:none) para desaparecer/
   reaparecer com uma transição suave em vez de um corte abrupto. */
.whatsapp-float.is-hidden {
  opacity: 0;
  transform: scale(0.8);
  pointer-events: none;
}
.whatsapp-float svg { width: 30px; height: 30px; fill: var(--white); }
@media (max-width: 559px) {
  .whatsapp-float { right: var(--space-4); bottom: var(--space-4); width: 52px; height: 52px; }
  .whatsapp-float svg { width: 26px; height: 26px; }
}
/* Com o menu mobile aberto, o círculo do WhatsApp (z-index:200, sempre por
   cima) tapava o canto do botão "Pedir orçamento" no fundo do .mobile-nav
   (z-index:99) - os dois são fixed e ambos vivem no fundo do ecrã. Já não
   faz sentido nenhum flutuante extra por cima de um menu que ocupa o ecrã
   todo, por isso esconde-o enquanto o menu está aberto (~ funciona porque
   .mobile-nav vem antes de .whatsapp-float na ordem do DOM). */
.mobile-nav.is-open ~ .whatsapp-float { display: none; }
.form-status.is-error { display: block; background: #FBEAEA; color: var(--error); }

/* Textura de grão (Épico 20) - camada subtilíssima de ruído sobre todo o
   site, o acabamento "premium" que interfaces escuras de referência (Linear,
   Vercel, Stripe) usam para tirar o fundo de um azul-marinho plano demais.
   SVG feTurbulence embutido como data URI: zero pedido de rede, zero JS,
   zero movimento (só textura estática) - não interage com prefers-reduced-
   motion porque não há nada a animar. position:fixed cobre o viewport sem
   entrar no fluxo do documento (não adiciona altura de scroll); pointer-
   events:none garante que nunca intercepta cliques, incluindo o WhatsApp
   flutuante (z-index 200) e o skip-link (z-index 1000, continua acima). */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 999;
  opacity: 0.035;
  mix-blend-mode: overlay;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Spotlight de cursor nos cards (Épico 20) - brilho radial subtil que segue
   o rato dentro de .card/.case-card/.testimonial-card, reforçando a
   sensação de "vidro"/profundidade do tema escuro com uma resposta direta
   ao gesto do utilizador, em vez de só o hover estático (lift + sombra) já
   existente. Coordenadas atualizadas via --spot-x/--spot-y em
   assets/js/main.js. Só desktop com rato real (hover:hover e pointer:fine)
   - em touch não há posição de cursor a seguir, por isso nem o listener de
   JS é registado (mesmo guard, ver main.js). Pseudo-elemento com
   pointer-events:none: nunca interfere com o "stretched link" de
   .card-link::after nem com o clique em .case-card/.testimonial-btn.
   Opacidade máxima 0.12 - decorativo, não reduz de forma percetível o
   contraste do texto por cima (WCAG: estado transitório de hover, não a
   aparência de repouso auditada em docs/brand.md). Desligado por completo
   sob prefers-reduced-motion: é uma camada de movimento contínuo ligado à
   posição do rato, a mesma categoria de cautela já aplicada ao parallax do
   Épico 15.4 (guard explícito, não confiar só na regra global). */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .card,
  .case-card,
  .testimonial-card {
    --spot-x: 50%;
    --spot-y: 50%;
  }
  .card::before,
  .case-card::before,
  .testimonial-card::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(280px circle at var(--spot-x) var(--spot-y), rgba(93, 195, 240, 0.12), transparent 70%);
    opacity: 0;
    transition: opacity var(--duration-base) var(--ease-out);
    pointer-events: none;
  }
  .card:hover::before,
  .case-card:hover::before,
  .testimonial-card:hover::before { opacity: 1; }
}
