/* css/main.css — Bloco E: design system + secções visuais.
   Paleta e tipografia fechadas no PLAN.md ("Design System (Bloco E)").
   CSP: style-src 'self' — nenhum estilo inline em lado nenhum; estilos
   dinâmicos só via CSSOM no JS. */

/* ------------------------------------------------------------------ */
/* Fontes (self-hosted — font-src 'self').
   Newsreader (display) e Barlow Condensed (corpo), ambas Open Font
   License, em assets/fonts/. Os .woff2 são gerados a partir dos ficheiros
   do Google Fonts: a Newsreader vem como variável (wght 200–800, opsz
   6–72) e foi instanciada nos pesos que estas regras usam, com opsz
   fixado em 18 — não há aqui nenhum eixo variável em runtime. */

@font-face {
  font-family: Newsreader;
  src: url("../assets/fonts/Newsreader-ExtraLight.woff2") format("woff2");
  font-weight: 200;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: Newsreader;
  src: url("../assets/fonts/Newsreader-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: Newsreader;
  src: url("../assets/fonts/Newsreader-Italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Barlow Condensed";
  src: url("../assets/fonts/BarlowCondensed-Light.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Barlow Condensed";
  src: url("../assets/fonts/BarlowCondensed-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ------------------------------------------------------------------ */
/* Tokens */

:root {
  --bg: #0f0e0d;
  --bg-raised: #161413;
  --ink: #f0efed;
  --ink-dim: rgba(240, 239, 237, 0.75);
  --ink-faint: rgba(240, 239, 237, 0.60);
  --hairline: rgba(240, 239, 237, 0.14);
  --wine: #4a1f28;
  --wine-hover: #5e2833;
  /* Tom claro derivado do acento — só para texto/traços pequenos em cima
     do fundo escuro, onde #4A1F28 puro não teria contraste legível. */
  --wine-bright: #b06a78;
  --lamp: rgba(201, 138, 74, 0.14); /* eco da lâmpada quente do hero */
  /* Os canais de --bg, para os véus e overlays que precisam do fundo com
     alfa. Sem isto a cor ficava escrita à mão em cada rgba() e uma mudança
     de paleta deixava-os todos para trás, sem dar sinal. */
  --bg-rgb: 15, 14, 13;
  --font-display: Newsreader, "Times New Roman", georgia, serif;
  --font-body: "Barlow Condensed", "Avenir Next Condensed", "Arial Narrow",
    "Helvetica Neue", arial, sans-serif;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* Preenchimento de "sem capa", partilhado pela porta vazia, pela
     miniatura vazia da faixa e pelo palco vazio — três sítios, um valor. */
  --empty-fill:
    radial-gradient(circle at 50% 42%, var(--lamp), transparent 62%),
    linear-gradient(180deg, #16171c 0%, var(--bg-raised) 100%);
  --pad-inline: clamp(1.25rem, 5vw, 4rem);
  --section-gap: clamp(3rem, 7vh, 4.5rem);
  /* Deixa transições animarem até uma altura intrínseca (auto) — usado
     no colapso suave do <details> dos stats. Herdado; onde não há
     suporte (Firefox 2026) a altura salta em vez de deslizar. */
  interpolate-size: allow-keywords;
}

html {
  background: var(--bg);
  color-scheme: dark;
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 1.125rem;
  letter-spacing: 0.015em;
}

::selection {
  background: var(--wine);
  color: var(--ink);
}

:focus-visible {
  outline: 1px solid var(--wine-bright);
  outline-offset: 3px;
}

/* ------------------------------------------------------------------ */
/* Utilitários */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Reveal-on-scroll: a classe .reveal é aplicada pelo JS (progressive
   enhancement — sem JS, tudo fica visível). */
.reveal {
  opacity: 0;
  transform: translateY(1.5rem);
  transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* ------------------------------------------------------------------ */
/* Navegação */

.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.25rem var(--pad-inline);
  background: linear-gradient(180deg, rgba(var(--bg-rgb), 0.55), transparent);
  transition: background 0.4s var(--ease);
}

.nav--scrolled {
  background: rgba(var(--bg-rgb), 0.88);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--hairline);
}

.nav__mark {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.25rem;
  text-decoration: none;
  letter-spacing: 0.02em;
}

.nav__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem clamp(1rem, 3vw, 2.5rem);
}

.nav__links a {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  text-decoration: none;
  color: var(--ink-dim);
  padding-bottom: 0.2rem;
  background-image: linear-gradient(var(--wine-bright), var(--wine-bright));
  background-repeat: no-repeat;
  background-size: 0% 1px;
  background-position: left bottom;
  transition: color 0.35s var(--ease), background-size 0.35s var(--ease);
}

.nav__links a:hover,
.nav__links a:focus-visible {
  color: var(--ink);
  background-size: 100% 1px;
}

/* Book é a acção de conversão, não navegação — lê como "a acção":
   cor de acento e sublinhado sempre presente (os outros três só o
   ganham em hover). */
.nav__links a.nav__links-cta {
  color: var(--wine-bright);
  background-size: 100% 1px;
}

.nav__links a.nav__links-cta:hover,
.nav__links a.nav__links-cta:focus-visible {
  color: var(--ink);
  background-size: 100% 1px;
}

/* Cursor de selecção do overlay mobile — criado e posicionado por JS.
   Só existe no menu fullscreen; na barra de desktop fica fora do fluxo. */
.nav__cursor {
  display: none;
}

/* Hamburger — display:none aqui, só aparece abaixo de 48rem. Fica acima
   do overlay (z-index) para continuar a ser o botão de fechar. */
.nav__toggle {
  display: none;
  position: relative;
  z-index: 2;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: 0.4rem;
  width: 2.75rem;
  height: 2.75rem;
  /* Alvo de toque de 44px sem alterar a métrica da barra. */
  margin: -0.75rem -0.6rem;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.nav__toggle-bar {
  display: block;
  width: 1.6rem;
  height: 1px;
  background: var(--ink);
  transition: transform 0.4s var(--ease);
}

/* Aberto: as duas barras juntam-se e cruzam num X. */
.nav[data-menu="open"] .nav__toggle-bar:first-child {
  transform: translateY(0.2rem) rotate(45deg);
}

.nav[data-menu="open"] .nav__toggle-bar:last-child {
  transform: translateY(-0.2rem) rotate(-45deg);
}

/* Overlay fullscreen — o mesmo .nav__links, reformatado. */
@media (max-width: 48rem) {
  .nav__toggle {
    display: flex;
  }

  .nav__mark {
    position: relative;
    z-index: 2;
  }

  .nav__links {
    position: fixed;
    inset: 0;
    z-index: 1;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: clamp(1.1rem, 3.5vh, 2rem);
    padding: 0 var(--pad-inline);
    /* Sólido, sem transparência a lutar com o conteúdo por baixo. */
    background: var(--bg);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.45s var(--ease), visibility 0s linear 0.45s;
  }

  /* O cookie banner é criado por JS com z-index: 9999 inline. Subir o
     z-index do .nav__links não chegaria: o .nav tem z-index 50 e cria um
     contexto de empilhamento, por isso o overlay nunca passa dos 50 face
     ao resto da página, faça o que fizer lá dentro. Quem tem de subir é
     o contexto — e só com o menu aberto, para não mexer no empilhamento
     da barra em mais nenhum estado. O banner mantém os 9999. */
  .nav[data-menu="open"] {
    z-index: 10000;
    /* Com o menu aberto o overlay .nav__links é position:fixed; inset:0 e
       conta com a viewport como bloco de contenção. Mas .nav--scrolled
       (adicionada por JS após 40px de scroll) põe backdrop-filter na .nav,
       e backdrop-filter — tal como transform/filter — faz a .nav virar o
       bloco de contenção dos descendentes fixos. O overlay passaria então a
       ancorar na barra (~75px) em vez do ecrã: abria colapsado no topo, com
       o conteúdo a vazar por trás (o "quebra ao reabrir depois de rolar").
       Desligar o filtro enquanto aberto devolve a viewport ao overlay — e o
       blur seria invisível na mesma, tapado pelo fundo sólido do overlay. */
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  .nav[data-menu="open"] .nav__links {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.45s var(--ease), visibility 0s;
  }

  .nav__links a {
    font-family: var(--font-display);
    font-style: italic;
    font-size: clamp(2.25rem, 11vw, 3.5rem);
    line-height: 1;
    text-transform: none;
    letter-spacing: 0;
    color: var(--ink);
    opacity: 0;
    transform: translateY(1.5rem);
    transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
  }

  .nav[data-menu="open"] .nav__links a {
    opacity: 1;
    transform: none;
  }

  /* Stagger: cada link entra logo a seguir ao anterior. */
  .nav[data-menu="open"] .nav__links a:nth-child(1) { transition-delay: 0.10s; }
  .nav[data-menu="open"] .nav__links a:nth-child(2) { transition-delay: 0.17s; }
  .nav[data-menu="open"] .nav__links a:nth-child(3) { transition-delay: 0.24s; }
  .nav[data-menu="open"] .nav__links a:nth-child(4) { transition-delay: 0.31s; }

  /* O Book mantém o tratamento do desktop, não inventa um novo. */
  .nav__links a.nav__links-cta {
    color: var(--wine-bright);
    background-size: 100% 1px;
  }

  /* Cursor de selecção: uma hairline em --wine-bright, na goteira à
     esquerda, que desliza para junto do link com foco (Tab/Shift+Tab ou
     toque). Altura e Y vêm do JS, que mede o link com foco; aqui só o
     aspecto e o glide. O caret vive à esquerda de propósito — o sublinhado
     de foco já usa --wine-bright, um segundo sublinhado repetir-se-ia. Sob
     prefers-reduced-motion o kill-switch global do reset zera a duração:
     salta em vez de deslizar, e continua a aparecer. */
  .nav__cursor {
    display: block;
    position: absolute;
    left: calc(var(--pad-inline) - 0.85rem);
    top: 0;
    width: 1px;
    height: 2rem;
    background: var(--wine-bright);
    opacity: 0;
    transform: translateY(0);
    transition: transform 0.4s var(--ease), opacity 0.3s var(--ease);
    pointer-events: none;
  }

  /* Só visível com o overlay aberto — fechar devolve a opacidade a 0
     sem tocar no JS. */
  .nav[data-menu="open"] .nav__cursor.is-active {
    opacity: 1;
  }

  body.menu-open {
    overflow: hidden;
  }
}

/* Sem stagger para quem pediu menos movimento: os links aparecem de uma
   vez. O !important é preciso porque o transition-delay do stagger vive
   em selectores mais específicos (e o kill-switch do reset.css zera a
   duração, mas não o delay). O overlay fechado continua escondido pelo
   visibility/opacity do pai. */
@media (prefers-reduced-motion: reduce) {
  .nav__links a {
    opacity: 1 !important;
    transform: none !important;
    transition-delay: 0s !important;
  }
}

/* ------------------------------------------------------------------ */
/* Hero */

.hero {
  position: relative;
  height: 100svh;
  min-height: 34rem;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  /* Fundo de reserva com os tons da foto (oceano frio + brilho da
     lâmpada) — segura a cena se a imagem ainda não existir. */
  background:
    radial-gradient(circle at 72% 68%, var(--lamp), transparent 42%),
    linear-gradient(180deg, #131820 0%, #10131a 45%, var(--bg) 100%);
}

.hero__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: right center;
  /* O parallax (main.js) escreve a custom property --parallax; o
     transform é composto aqui. Sem JS, ou sob prefers-reduced-motion,
     --parallax nunca é definida e o fallback 0px deixa a foto estática. */
  transform: translate3d(0, var(--parallax, 0px), 0);
  will-change: transform;
}

.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(var(--bg-rgb), 0.25) 0%,
    transparent 35%,
    transparent 55%,
    rgba(var(--bg-rgb), 0.82) 100%
  );
}

.hero__content {
  position: relative;
  z-index: 1;
  width: 100%;
  padding: 0 var(--pad-inline) clamp(3rem, 9vh, 6rem);
}

/* Seguro de contraste: um véu escuro subtil só atrás do bloco de texto
   (não sobre a foto toda), para o nome se manter legível seja qual for
   a foto de hero usada no futuro. Fica por baixo do texto (paint order),
   por cima da imagem. */
.hero__content::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 190%;
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(var(--bg-rgb), 0.62) 0%,
    rgba(var(--bg-rgb), 0.34) 42%,
    transparent 80%
  );
}

.hero__name {
  font-family: var(--font-display);
  font-weight: 200;
  font-size: clamp(3rem, 11vw, 9rem);
  line-height: 0.98;
  letter-spacing: -0.01em;
}

.hero__name em {
  font-style: italic;
  font-weight: 400;
}

.hero__meta {
  margin-top: 1.25rem;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-dim);
}

.hero__scroll-cue {
  position: absolute;
  z-index: 1;
  bottom: clamp(3rem, 9vh, 6rem);
  right: var(--pad-inline);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.28em;
  color: var(--ink-dim);
}

.hero__scroll-cue::after {
  content: "";
  width: 1px;
  height: 4.5rem;
  background: linear-gradient(180deg, var(--ink-dim), transparent);
}

@media (prefers-reduced-motion: no-preference) {
  .hero__content > * {
    animation: hero-rise 1.2s var(--ease) both;
  }

  .hero__meta {
    animation-delay: 0.25s;
  }

  .hero__scroll-cue {
    animation: hero-rise 1.2s var(--ease) 0.5s both;
  }
}

@keyframes hero-rise {
  from {
    opacity: 0;
    transform: translateY(1.75rem);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@media (max-width: 40rem) {
  .hero__scroll-cue {
    display: none;
  }

  /* Em mobile o recorte fica ancorado um pouco à esquerda de "right", o
     que empurra a cena (modelo + lâmpada) ligeiramente para a direita e
     tira o nome de cima do corpo dela. A lâmpada continua inteira. */
  .hero__img {
    object-position: 95% center;
  }

  /* Nome e subtítulo centrados sobre a foto. O .hero__meta é flex, por
     isso precisa do justify-content além do text-align. */
  .hero__content {
    text-align: center;
  }

  .hero__meta {
    justify-content: center;
  }
}

/* ------------------------------------------------------------------ */
/* Base das secções */

.section {
  padding: var(--section-gap) var(--pad-inline);
  /* Igual ao offset sticky do .gallery__pin/.switcher — a barra fixa
     tem ~4–4.5rem de altura real, por isso 3.5rem deixava o label da
     secção meio tapado ao saltar para uma âncora. */
  scroll-margin-top: 5.5rem;
}

.section__label {
  display: flex;
  align-items: center;
  gap: 1rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-dim);
  /* Gap padrão "label/título → corpo" (o mesmo triplo em #work, .portals
     e .booking-intro), para o ritmo interno das secções bater certo. */
  margin-bottom: clamp(2.5rem, 6vh, 4rem);
}

.section__label::before {
  content: "";
  width: 2.5rem;
  height: 1px;
  background: var(--wine-bright);
}

.section__title {
  font-family: var(--font-display);
  font-weight: 200;
  font-size: clamp(2.2rem, 5.5vw, 4.25rem);
  line-height: 1.05;
  max-width: 24ch;
}

.section__title em {
  font-style: italic;
  font-weight: 400;
}

/* ------------------------------------------------------------------ */
/* Portfolio — portais de categoria */

#portfolio {
  padding-inline: 0;
}

#portfolio .section__label,
#portfolio .portfolio-head {
  padding-inline: var(--pad-inline);
}

/* Desktop: o cabeçalho do portfolio centra-se e adopta a voz tipográfica
   do nome do hero (mesmo regular+itálico da Newsreader, mesmo tracking
   e line-height). Mobile fica como está — alinhado à esquerda. */
@media (min-width: 48rem) {
  #portfolio .section__label {
    justify-content: center;
  }

  /* Um traço de cada lado, para o label centrado ficar simétrico. */
  #portfolio .section__label::after {
    content: "";
    width: 2.5rem;
    height: 1px;
    background: var(--wine-bright);
  }

  #portfolio .portfolio-head {
    display: flex;
    justify-content: center;
  }

  #portfolio .section__title {
    text-align: center;
    line-height: 0.98;
    letter-spacing: -0.01em;
    /* Frase curta, pensada para uma linha só — sem max-width em ch a
       forçar quebra, e o clamp fecha mais baixo do que o das secções
       alinhadas à esquerda para garantir que cabe até em 1280px. */
    max-width: none;
    font-size: clamp(2.4rem, 4.4vw, 3.75rem);
    text-wrap: balance;
  }
}

.portfolio-status {
  padding: 2rem var(--pad-inline);
  color: var(--ink-dim);
  font-size: 0.95rem;
}

/* Quatro portas lado a lado a partir de 48rem (~768px); abaixo disso
   empilham a full width — não há espaço para quatro colunas sem encolher
   as fotos até thumbnail, que é precisamente o que não queremos.

   O display vive numa regra própria com :not([hidden]) — mesmo padrão do
   .collabs__arrow. Um `display` declarado aqui em cima venceria o
   [hidden] { display: none } da folha do agente (regra de autor ganha por
   origem, não por especificidade) e o atributo hidden do #portals passava
   a não fazer nada: ficava a grelha vazia, com o margin-top inteiro, por
   baixo do "Loading the work…" e de qualquer erro. */
.portals {
  grid-template-columns: 1fr;
  gap: 2px;
  margin-top: clamp(2.5rem, 6vh, 4rem);
}

.portals:not([hidden]) {
  display: grid;
}

.portal {
  position: relative;
  display: block;
  width: 100%;
  height: clamp(24rem, 74vh, 46rem);
  overflow: hidden;
  text-align: left;
  background: var(--bg-raised);
  /* Direcção da inclinação de entrada. 0 por defeito (sem rotação): só as
     quatro colunas do desktop lhe dão um valor mais abaixo. */
  --side: 0;
}

/* Entrada por scroll: cada porta cresce para o lugar à medida que se
   rola do hero para o portfólio (escala 0.90 → 1) com uma inclinação de
   2° que se endireita. O intervalo longo (até contain 38%) mantém o
   crescimento atado ao scroll em vez de resolver de imediato — a porta
   está a chegar, não a saltar. Gesto contido, não um floreado a competir
   com a fotografia. Progressive enhancement puro — o @supports isola a
   animation-timeline, por isso o Firefox (ainda sem view() em 2026)
   mostra as portas já assentes: sem JS, sem biblioteca, sem layout
   partido. Respeita prefers-reduced-motion. */
@keyframes portal-reveal {
  from {
    scale: 0.9;
    rotate: calc(var(--side, 0) * 2deg);
  }
}

/* Variante mobile: a lista é uma coluna, por isso não há inclinação nem
   foto full-bleed a preservar — troca-se por uma entrada clássica (um
   ligeiro subir + fade + assentar de escala). Como cada linha está atada
   à SUA posição de scroll pela view(), revelam-se uma de cada vez (1ª,
   2ª, 3ª) ao rolar, sem stagger manual nem JS. Transição pequena e
   discreta, não um efeito. */
@keyframes portal-reveal-row {
  from {
    opacity: 0;
    translate: 0 1.25rem;
    scale: 0.97;
  }
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .portal {
      animation: portal-reveal linear both;
      animation-timeline: view();
      animation-range: cover 0% contain 38%;
    }
  }
}

/* Duas camadas de propósito: o parallax desloca .portal__media (sem
   transition, tem de seguir o scroll instantaneamente) e o hover escala
   .portal__img (com transition lenta). No mesmo transform, um travaria
   o outro. A folga de 8% em cima e em baixo dá espaço ao deslocamento
   sem descobrir as margens. */
.portal__media {
  position: absolute;
  inset: -8% 0;
  display: block;
  transform: translate3d(0, var(--parallax, 0px), 0);
  will-change: transform;
}

.portal__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 1.1s var(--ease), filter 1.1s var(--ease);
}

/* Categoria ainda sem cover (a API devolve cover: null) — estado
   declarado, não uma imagem partida. */
.portal--empty {
  background: var(--empty-fill);
}

.portal__placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-faint);
}

.portal::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    transparent 40%,
    rgba(var(--bg-rgb), 0.72) 100%
  );
  transition: background 0.6s var(--ease);
}

/* Empilhado: índice → nome → hint. Repete o par tipográfico do hero
   (meta tracked-out + nome em serifa), para a porta ler como categoria
   e não como um título gigante a competir com o hero. */
.portal__body {
  position: absolute;
  z-index: 1;
  left: var(--pad-inline);
  right: var(--pad-inline);
  bottom: clamp(1.75rem, 5vh, 3rem);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.55rem;
}

/* 0.22em é o mesmo tracking do .hero__meta — a ligação ao hero é
   literal, não aproximada. */
.portal__hint {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-faint);
  transition: color 0.4s var(--ease);
}

.portal__name {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(2rem, 8vw, 2.75rem);
  line-height: 1;
  transition: transform 0.6s var(--ease);
}

/* Layout de quatro colunas — decidido por largura, independente de haver
   hover. Um tablet largo recebe as quatro portas E os nomes sempre
   visíveis (ver bloco hover abaixo). */
@media (min-width: 48rem) {
  .portals {
    grid-template-columns: repeat(4, 1fr);
  }

  .portal {
    height: clamp(30rem, 86vh, 54rem);
  }

  /* Quatro colunas lado a lado: leque simétrico, as duas portas exteriores
     inclinam-se mais para fora e as duas interiores ficam quase direitas,
     com a mesma amplitude máxima (±2°, rotate = --side * 2deg) que o
     leque de três tinha. O padrão 4n repõe-se por linha, para qualquer
     número de categorias. */
  .portal:nth-child(4n + 1) {
    --side: -1;
  }

  .portal:nth-child(4n + 2) {
    --side: -0.34;
  }

  .portal:nth-child(4n + 3) {
    --side: 0.34;
  }

  .portal:nth-child(4n) {
    --side: 1;
  }

  /* Numa coluna estreita o --pad-inline do full-width é demasiado. */
  .portal__body {
    left: clamp(1.25rem, 2vw, 2rem);
    right: clamp(1.25rem, 2vw, 2rem);
  }

  .portal__name {
    font-size: clamp(1.75rem, 2.6vw, 2.5rem);
  }
}

/* Abaixo das quatro colunas as portas deixam de ser portas: viram uma
   lista compacta de linhas (thumbnail + nome), separadas pela
   mesma hairline do resto do site. Mesmo DOM e mesmo handler de
   clique — só o layout muda. 47.9375rem e não 48rem para não haver
   nenhum ponto em que este bloco e o de min-width: 48rem se apliquem
   os dois. */
@media (max-width: 47.9375rem) {
  .portals {
    gap: 0;
    margin-top: clamp(2rem, 5vh, 3rem);
  }

  /* Coluna única: a inclinação alternada do desktop não tem aqui um
     "fora" para onde apontar — cada linha a tombar num sentido diferente
     leria como ruído, não como calma editorial. Mantém-se o --side: 0 do
     base (sem rotação); a entrada é a variante clássica abaixo. */
  .portal {
    display: flex;
    align-items: center;
    gap: 1.1rem;
    height: auto;
    /* As portas full-bleed encostavam às margens do ecrã de propósito;
       uma lista não — alinha com o título da secção. */
    padding: 0.9rem var(--pad-inline);
    background: none;
  }

  /* Uma de cada vez: troca a escala+rotação do desktop pela entrada
     clássica (subir+fade+assentar) e atada ao entry de cada linha, por
     isso cada linha revela-se em sequência ao rolar — o stagger
     é a própria posição de scroll, não delays manuais. Herda a view() e
     o both/linear do bloco base. */
  @media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: view()) {
      .portal {
        animation-name: portal-reveal-row;
        animation-range: entry 0% entry 100%;
      }
    }
  }

  .portal + .portal {
    border-top: 1px solid var(--hairline);
  }

  /* O véu escuro (gradiente do base) existia para o texto assentar sobre
     a foto; aqui o texto está ao lado dela, por isso reaproveita-se o
     ::after como pista de navegação. Sem hover nem "Enter" no mobile, a
     linha não anuncia que é tocável — um chevron fino (o mesmo traço de
     1px do ícone da câmara no cursor e das ondas divisórias, não um ícone de
     biblioteca) marca que a linha leva a algum lado. Função real de
     navegação, não decoração. Desenhado por máscara para a cor vir do
     token em vez de ser cozida no SVG. */
  .portal::after {
    content: "";
    position: static;
    inset: auto;
    flex: 0 0 auto;
    align-self: center;
    width: 0.5rem;
    height: 0.9rem;
    margin-left: 0.4rem;
    background: var(--ink-faint);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 16'%3E%3Cpath d='M1 1 7 8 1 15' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 16'%3E%3Cpath d='M1 1 7 8 1 15' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
  }

  .portal__media {
    position: static;
    inset: auto;
    flex: 0 0 auto;
    width: 6rem;
    height: 6rem;
    overflow: hidden;
    /* Neutraliza o --parallax que o JS continua a escrever: nesta
       largura a thumbnail não deriva. */
    transform: none;
    will-change: auto;
    background: var(--bg-raised);
  }

  .portal--empty {
    background: none;
  }

  .portal__placeholder {
    position: static;
    flex: 0 0 auto;
    width: 6rem;
    height: 6rem;
    padding: 0 0.4rem;
    font-size: 0.58rem;
    letter-spacing: 0.16em;
    text-align: center;
    background: var(--empty-fill);
  }

  .portal__body {
    position: static;
    left: auto;
    right: auto;
    bottom: auto;
    flex: 1 1 auto;
    gap: 0.35rem;
  }

  .portal__name {
    font-size: 1.45rem;
  }

  /* "Enter" era a affordance da porta full-height; numa linha de lista
     o toque é óbvio e a palavra só faz ruído. */
  .portal__hint {
    display: none;
  }
}

/* Interacção de hover — só onde existe rato. Em toque, nada aqui se
   aplica: o nome fica permanentemente visível (mesmo princípio do
   cursor personalizado), porque o hover nunca chega. */
@media (hover: hover) and (pointer: fine) {
  /* O nome entra por clip-path (wipe da esquerda). O clip vive dentro
     desta media query de propósito: sem hover, nunca é aplicado, logo
     não há forma de o rótulo ficar preso escondido num ecrã de toque. */
  .portal__name {
    clip-path: inset(0 100% 0 0);
    transition: clip-path 0.75s var(--ease), transform 0.6s var(--ease);
  }

  .portal:hover .portal__name,
  .portal:focus-visible .portal__name {
    clip-path: inset(0 0 0 0);
    transform: translateX(0.5rem);
  }

  .portal:hover .portal__img,
  .portal:focus-visible .portal__img {
    transform: scale(1.03);
  }

  .portal:hover::after,
  .portal:focus-visible::after {
    background: linear-gradient(
      180deg,
      rgba(74, 31, 40, 0.18) 0%,
      rgba(var(--bg-rgb), 0.8) 100%
    );
  }

  .portal:hover .portal__hint,
  .portal:focus-visible .portal__hint {
    color: var(--wine-bright);
  }
}

/* O clip-path é decoração: quem pediu menos movimento vê o nome
   simplesmente lá, sem wipe. */
@media (prefers-reduced-motion: reduce) {
  .portal__name {
    clip-path: none;
    transition: none;
  }
}

/* Cabeçalho do álbum (Nível 3) — o título do ensaio e o link de volta.
   O nome segue o bloco que o envolve (.album-detail). */

.album-detail__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0 var(--pad-inline);
  margin-bottom: 2.5rem;
}

.album-detail__title {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(2rem, 4.5vw, 3.25rem);
}

/* O traço curto faz de seta, como em .browse__back — nem a Newsreader nem
   a Barlow Condensed trazem "←", e o fallback destoava. Os dois níveis
   ficam assim com o mesmo gesto de voltar. */
.link-back {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
  text-decoration: none;
  transition: color 0.35s var(--ease);
}

.link-back::before {
  content: "";
  width: 1.6rem;
  height: 1px;
  background: currentColor;
  transition: width 0.35s var(--ease);
}

.link-back:hover::before,
.link-back:focus-visible::before {
  width: 2.4rem;
}

.link-back:hover,
.link-back:focus-visible {
  color: var(--wine-bright);
}

/* Nível 2 — selector de ensaios de uma categoria.

   Um palco mostra o ensaio seleccionado e as miniaturas dos restantes
   vivem numa FAIXA horizontal por baixo dele — nas duas larguras. A
   órbita elíptica que aqui esteve estava bem construída, mas punha
   miniaturas a mudar de sítio dos dois lados da foto ao mesmo tempo, e
   é isso que lê como agitação para quem é sensível a movimento. A faixa
   diz o mesmo com o vocabulário do .switcher de album.html: miniaturas
   finas em linha, hairline, realce por opacidade e um fio vinho.

   O que se move: quase nada. O realce caminha pela faixa parada; só ao
   chegar à margem é que a janela avança um passo (180ms, uma posição).
   O palco faz cross-fade de 200ms. Nada roda, nada varre.

   Nove slots com janela virtualizada: ver js/strip.js. Aqui só interessa
   que --pos posiciona cada slot na faixa, que --item-op é 0 nas duas
   PONTAS (a costura, onde a reciclagem acontece invisível) e que
   .is-seam desliga o alvo aí; .is-jump suprime a transição do slot que
   atravessa de uma ponta à outra, senão ele cruzava a faixa à vista.

   Mobile (<48rem): a mesma faixa, mas em fluxo e com scroll horizontal
   em vez de slots posicionados — inalterada.

   prefers-reduced-motion: tudo aqui são transitions CSS — o kill-switch
   global do reset.css zera-lhes a duração e o mecanismo colapsa em
   cortes secos.

   Layout da página: #shoots-panel é uma coluna flex fechada a 100svh — a
   página NÃO faz scroll. Só o label e o título da categoria ocupam fluxo
   no topo; "All work", o contador e a dica vivem nos cantos, fora do
   fluxo, e o nome do ensaio está sobreposto à foto. Todo o resto do
   espaço é do palco e da faixa. */

#shoots-panel {
  box-sizing: border-box;
  position: relative;
  height: 100svh;
  /* Topo: folga para a nav fixa (~4.5rem de altura real). Em baixo, a
     altura do aviso de cookies enquanto ele existir: a faixa vive no
     baixo-centro, exactamente onde o aviso está, e sem
     isto a miniatura seleccionada ficava cortada por ele. */
  padding: 5rem var(--pad-inline)
    calc(var(--cookie-notice-h, 0px) + clamp(0.75rem, 2svh, 1.5rem));
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Cabeçalho — o mesmo par label+título das secções da index, centrado.
   Classes próprias (não .album-detail__head/.link-back) porque essas são
   do album.html, que tem uma tarefa em curso. */

.browse__head {
  flex: 0 0 auto;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.browse__label {
  justify-content: center;
  margin-bottom: clamp(0.6rem, 1.6svh, 1.2rem);
}

/* Um traço de cada lado, como no label centrado do #portfolio. */
.browse__label::after {
  content: "";
  width: 2.5rem;
  height: 1px;
  background: var(--wine-bright);
}

.browse__title {
  font-family: var(--font-display);
  font-weight: 200;
  font-size: clamp(1.9rem, 1.1rem + 3.6svh, 3rem);
  line-height: 1;
}

/* Cantos inferiores. Em mobile ficam numa linha própria abaixo da faixa:
   a foto ocupa a largura toda do ecrã e não sobra canto onde flutuar. No
   desktop saem do fluxo (ver a media query): a faixa é estreita e
   centrada, por isso os cantos continuam vazios e pô-los lá devolve ao
   palco a altura que uma linha própria lhe roubaria. */
.browse__corners {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-top: clamp(0.5rem, 1.4svh, 0.9rem);
}

.browse__corner {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-faint);
}

/* Voltar — a mesma voz tracked-out dos links pequenos; o traço curto faz
   de seta (a Newsreader/Barlow não têm o glifo "←") e cresce no hover. */
.browse__back {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  text-decoration: none;
  transition: color 0.35s var(--ease);
}

.browse__back::before {
  content: "";
  width: 1.6rem;
  height: 1px;
  background: currentColor;
  transition: width 0.35s var(--ease);
}

.browse__back:hover,
.browse__back:focus-visible {
  color: var(--wine-bright);
}

.browse__back:hover::before,
.browse__back:focus-visible::before {
  width: 2.4rem;
}

/* Contador — a única informação que o arco de progresso não consegue
   dar: a cardinalidade do conjunto. Só aparece quando a faixa esconde
   alguma coisa (o JS tira-lhe o hidden); com a faixa completa seria ruído
   a dizer o que já se vê. */
.browse__count {
  font-variant-numeric: tabular-nums;
}

.browse__count-sep {
  margin: 0 0.35rem;
  color: var(--hairline);
}

.browse {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(0.6rem, 1.6svh, 1.1rem);
}

/* Faixa — mesma gramática do .switcher de album.html, mas em fluxo (não
   fixed): a página do Nível 2 não tem scroll longo que justifique uma
   barra colada ao ecrã. Fica sempre abaixo do palco. Esta é a base
   (mobile): fila com scroll horizontal. */
.strip {
  order: 2;
  flex: 0 0 auto;
  display: flex;
  justify-content: safe center;
  gap: 0.4rem;
  overflow-x: auto;
  scrollbar-width: none;
}

.strip::-webkit-scrollbar {
  display: none;
}

.strip__slot {
  flex: 0 0 auto;
}

.strip__item {
  display: block;
  position: relative;
  width: 3.25rem;
  height: 4.1rem;
  overflow: hidden;
  background: var(--bg-raised);
  /* Base (mobile): opacidade plana. Não há pontas a esconder aqui — a
     faixa faz scroll —, por isso --item-op (que vai a zero) não entra. */
  opacity: 0.5;
  transition: opacity 0.25s var(--ease);
}

.strip__thumb {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Ensaio sem capa — o mesmo preenchimento dos portais vazios, agora pelo
   token partilhado (--empty-fill, no :root). */
.strip__item--empty,
.stage__empty {
  background: var(--empty-fill);
}

.strip__item--current {
  opacity: 1;
  outline: 1px solid var(--wine-bright);
  outline-offset: -1px;
}

.strip__item:focus-visible {
  opacity: 1;
  outline: 1px solid var(--wine-bright);
  outline-offset: 2px;
}

/* Palco — a imagem É a fronteira: o JS dá ao .stage__view width/height
   a partir da moldura livre e da razão da foto, por isso não há fundo
   visível à volta e a transition anima a mudança de forma entre fotos
   de razões diferentes. */
.stage {
  flex: 1;
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.stage__frame {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.stage__view {
  position: relative;
  overflow: hidden;
  max-width: 100%;
  max-height: 100%;
  cursor: pointer;
  transition: width 0.35s var(--ease), height 0.35s var(--ease);
}

/* Cross-fade de duas camadas: a que entra começa transparente com um
   scale subtil e o JS dá-lhe .is-shown; a que sai é removida no fim.
   cover, não contain: o palco já tem a razão da foto — cover só evita
   réstias de 1px durante a animação de forma. */
.stage__img,
.stage__empty {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.02);
  /* 0.2s e não 0.3s: com as miniaturas a assentar directamente, um cross-fade
     de 0.3s no meio do ecrã deixava de ser o elemento discreto e passava a ser
     o lento — 0.2s põe o palco ao ritmo da faixa. */
  transition: opacity 0.2s var(--ease), transform 0.2s var(--ease);
}

.stage__img.is-shown,
.stage__empty.is-shown {
  opacity: 1;
  transform: none;
}

/* Véu de legibilidade do nome sobreposto — o mesmo tratamento e o mesmo
   rgba do .portal::after do hero/portais, não uma superfície nova. */
.stage__view::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    transparent 55%,
    rgba(var(--bg-rgb), 0.72) 100%
  );
}

/* Nome do ensaio — sobre a foto, não numa linha própria por baixo: é o
   segundo dos dois elementos de texto permanentes da página. */
.stage__caption {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 1rem 1rem 0.8rem;
  pointer-events: none;
  text-align: center;
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.2rem, 0.9rem + 1.8svh, 1.75rem);
  line-height: 1.05;
  color: var(--ink);
}

/* Dica de orientação — a voz do .offers-hint ("Tap a category to open
   it"). Transitória: o JS marca .is-gone à primeira interacção e ela sai,
   porque deixou de servir. Ancorada por baixo do título e fora do fluxo,
   para não roubar altura ao palco nem a devolver com um salto quando
   desaparece. Em baixo não podia ficar: é lá que está a miniatura
   frontal. */
.browse__hint {
  position: absolute;
  z-index: 2;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: clamp(0.4rem, 1.2svh, 0.9rem);
  width: max-content;
  max-width: 22rem;
  text-align: center;
  font-size: 0.8rem;
  color: var(--ink-faint);
  transition: opacity 0.5s var(--ease);
}

.browse__hint.is-gone {
  opacity: 0;
  pointer-events: none;
}

.browse__hint-keys {
  display: none;
}

/* Zoom (mobile) — a foto em ecrã inteiro, com a entrada no ensaio
   explícita. O toque na foto do palco abre isto em vez de entrar
   directamente: sem cursor não há nada que anuncie "Open", e um toque
   com dois significados seria adivinhação. */
.zoom {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  padding: 4rem var(--pad-inline);
  background: rgba(var(--bg-rgb), 0.96);
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}

.zoom.is-shown {
  opacity: 1;
}

.zoom__img {
  min-height: 0;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.zoom__actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 2rem;
}

/* CTA de entrada — a mesma voz tracked-out do .link-back, com a marca
   micro-onda + bico (função real de navegação, não seta decorativa).
   Só existe dentro do zoom: é lá que a acção precisa de nome. */
.stage__enter {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink);
  transition: color 0.35s var(--ease);
}

.stage__enter:hover,
.stage__enter:focus-visible {
  color: var(--wine-bright);
}

.stage__mark {
  flex: 0 0 auto;
  width: 1.9rem;
  height: 0.66rem;
  color: var(--ink-faint);
  transition: transform 0.5s var(--ease), color 0.4s var(--ease);
}

.stage__enter:hover .stage__mark,
.stage__enter:focus-visible .stage__mark {
  transform: translateX(0.35rem);
  color: currentColor;
}

.zoom__close {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-faint);
  transition: color 0.35s var(--ease);
}

.zoom__close:hover,
.zoom__close:focus-visible {
  color: var(--wine-bright);
}

/* --pos registado como <number> e não como texto opaco: dá-lhe um
   initial-value tipado, e é o que garante que o calc() do transform do
   .strip__slot recebe um número válido mesmo antes do primeiro render do
   JS. Também é o que permite ao browser interpolar a posição no único
   momento em que ela muda — o avanço da janela. */
@property --pos {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* A partir de 48rem a faixa deixa de fazer scroll e passa a ser uma
   janela de posições fixas: cada slot é colocado pelo seu --pos, e o que
   não cabe é reciclado pelas pontas (js/strip.js). A ordem da coluna é a
   mesma da base — palco, faixa por baixo. */
@media (min-width: 48rem) {
  .browse__hint-keys {
    display: inline;
  }

  .browse__hint-touch {
    display: none;
  }

  /* Aqui há canto livre: a faixa é estreita e centrada, e as laterais
     ficam vazias. display: contents dissolve o contentor para que cada
     canto se posicione contra o painel, e a linha que ocupavam em mobile
     volta para o palco. */
  .browse__corners {
    display: contents;
  }

  .browse__corner {
    position: absolute;
    bottom: calc(var(--cookie-notice-h, 0px) + clamp(0.75rem, 2svh, 1.5rem));
    z-index: 2;
  }

  .browse__back {
    left: var(--pad-inline);
  }

  .browse__count {
    right: var(--pad-inline);
  }

  .strip {
    /* Passo = miniatura + goteira. É a única medida da faixa: o JS
       escreve só --pos (inteiro), a geometria fica toda aqui. */
    --strip-item-w: clamp(3.4rem, 7svh, 4.6rem);
    --strip-gap: 0.5rem;
    --strip-pitch: calc(var(--strip-item-w) + var(--strip-gap));
    display: block;
    position: relative;
    /* A razão 42/53 é a das miniaturas; a altura da faixa é a de uma. */
    height: calc(var(--strip-item-w) * 53 / 42);
    overflow: visible;
    /* Só as miniaturas são alvo: a faixa em si não recebe cliques. */
    pointer-events: none;
  }

  /* Hairline sobre a faixa, com a largura dos slots visíveis (--strip-span,
     escrito pelo JS: exclui as duas pontas apagadas quando há reciclagem).
     É a mesma linha fina que separa no resto do sistema. */
  .strip::before {
    content: "";
    position: absolute;
    left: 50%;
    top: -0.55rem;
    transform: translateX(-50%);
    width: calc(var(--strip-span, 7) * var(--strip-pitch) - var(--strip-gap));
    height: 1px;
    background: var(--hairline);
  }

  .strip__slot {
    position: absolute;
    left: 50%;
    top: 0;
    bottom: 0;
    pointer-events: auto;
    transform: translateX(calc(var(--pos, 0) * var(--strip-pitch) - 50%));
    /* O único movimento de posição que existe: uma nudge de um passo
       quando o realce chega à margem e a janela avança. Nos outros
       passos este transform não muda e nada aqui anima. */
    transition: transform 0.18s var(--ease);
  }

  /* O slot que atravessou a costura reaparece na outra ponta. Sem isto
     deslizava a faixa toda à vista em vez de saltar (apagado). */
  .strip__slot.is-jump {
    transition: none;
  }

  /* Ponta apagada: existe para reciclar, não para ser clicada. */
  .strip__slot.is-seam {
    pointer-events: none;
  }

  .strip__item {
    width: var(--strip-item-w);
    height: 100%;
    /* Aqui sim: as pontas vão a zero. */
    opacity: var(--item-op, 0.45);
    transition: opacity 0.18s var(--ease);
  }

  /* Vencem o --item-op acima por virem depois: sem isto o realce e o foco
     herdavam a opacidade da janela. */
  .strip__item--current,
  .strip__item:focus-visible {
    opacity: 1;
  }
}

@media (min-width: 48rem) and (hover: hover) and (pointer: fine) {
  .strip__slot:not(.is-seam) .strip__item:hover {
    opacity: 1;
  }
}

/* ------------------------------------------------------------------ */
/* Galeria de ensaio — overlay com o momento assinatura de scroll
   híbrido: o scroll vertical do overlay traduz-se em movimento
   horizontal da fila de fotos (transform aplicado por JS via CSSOM). */

.gallery {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--bg);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.gallery__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
}

.gallery__track {
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 3vw, 3rem);
  padding-inline: var(--pad-inline);
  will-change: transform;
}

.gallery__item {
  flex: 0 0 auto;
  height: 70svh;
}

.gallery__item img {
  height: 100%;
  width: auto;
  max-width: 88vw;
  object-fit: contain;
}

.gallery__head {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.25rem var(--pad-inline);
  background: linear-gradient(180deg, rgba(var(--bg-rgb), 0.7), transparent);
}

.gallery__title {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.4rem;
}

.gallery__counter {
  font-size: 0.82rem;
  letter-spacing: 0.2em;
  color: var(--ink-dim);
}

.gallery__close {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
  transition: color 0.35s var(--ease);
}

.gallery__close:hover,
.gallery__close:focus-visible {
  color: var(--wine-bright);
}

.gallery__bar {
  position: fixed;
  bottom: 0;
  left: 0;
  z-index: 2;
  width: 100%;
  height: 2px;
  background: var(--wine-bright);
  transform-origin: left;
  transform: scaleX(0);
}

/* Fallback vertical: ecrãs estreitos ou prefers-reduced-motion —
   a classe .gallery--stack é decidida no JS. */
.gallery--stack .gallery__spacer {
  display: none;
}

.gallery--stack .gallery__pin {
  position: static;
  height: auto;
  overflow: visible;
  padding: 5.5rem 0 3rem;
}

.gallery--stack .gallery__track {
  flex-direction: column;
  width: 100%;
}

.gallery--stack .gallery__item {
  height: auto;
  width: 100%;
  max-width: 44rem;
}

.gallery--stack .gallery__item img {
  height: auto;
  width: 100%;
  max-width: none;
}

.gallery--stack .gallery__bar {
  display: none;
}

/* Sem scroll híbrido não há progresso a mostrar — contador escondido */
.gallery--stack .gallery__counter {
  display: none;
}

/* ------------------------------------------------------------------ */
/* Nível 3 — detalhe do álbum: masonry + comutador lateral de álbuns.

   Masonry calculado em JS (shortest-column, proporções reais da API):
   colunas de altura equilibrada, sem as sobras do column-count. O JS põe
   left/top/width e a altura do contentor; o transform fica reservado ao
   reveal escalonado (opacity + translateY por item, via
   IntersectionObserver). Clicar numa foto entra na galeria imersiva já
   existente, a começar NESSA foto.

   O comutador lateral dá um salto directo para outro ensaio da mesma
   categoria sem recuar. Desktop: um trilho vertical fino, sticky, ao lado
   do masonry (persiste enquanto se rola). Mobile: um trilho vertical não
   cabe num ecrã de retrato sem comer a área de leitura das fotos, por
   isso adapta-se a uma tira horizontal fixada em baixo. */

.album-detail__grid {
  display: block;
  padding: 0 var(--pad-inline) 6rem;
}

.masonry {
  position: relative;
}

/* Estado escondido do reveal; o JS dá is-revealed (com transition-delay
   crescente por lote visível) quando o item entra no viewport. Sob
   prefers-reduced-motion o JS marca tudo revelado à nascença e o
   kill-switch do reset zera a duração — nada fica escondido. */
.masonry__item {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  overflow: hidden;
  background: var(--bg-raised);
  opacity: 0;
  transform: translateY(1rem);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}

.masonry__item.is-revealed {
  opacity: 1;
  transform: none;
}

.masonry__item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Comutador — base = tira horizontal fixada em baixo (mobile). */
.switcher {
  position: fixed;
  left: 0;
  right: 0;
  /* Encosta-se acima do aviso de cookies enquanto ele existir — os dois
     são fixed ao fundo, e z-index só escolheria qual deles desaparece.
     cookie-banner.js publica --cookie-notice-h e remove-a ao ser aceite;
     sem banner o fallback devolve a tira ao fundo do ecrã. */
  bottom: var(--cookie-notice-h, 0px);
  z-index: 40;
  display: flex;
  gap: 0.4rem;
  padding: 0.55rem var(--pad-inline);
  overflow-x: auto;
  background: linear-gradient(0deg, var(--bg) 62%, transparent);
  scrollbar-width: none;
}

.switcher::-webkit-scrollbar {
  display: none;
}

.switcher__item {
  flex: 0 0 auto;
  position: relative;
  width: 3.25rem;
  height: 4.1rem;
  overflow: hidden;
  background: var(--bg-raised);
  opacity: 0.5;
  transition: opacity 0.4s var(--ease);
}

.switcher__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Ensaio actual marcado distintamente — hairline vinho e opacidade cheia. */
.switcher__item--current {
  opacity: 1;
  outline: 1px solid var(--wine-bright);
  outline-offset: -1px;
}

@media (hover: hover) and (pointer: fine) {
  .switcher__item:hover {
    opacity: 1;
  }

  .masonry__item img {
    transition: transform 0.9s var(--ease);
  }

  .masonry__item:hover img {
    transform: scale(1.03);
  }
}

/* Desktop: o trilho passa a coluna vertical sticky ao lado do masonry, e
   a tira deixa de estar fixada em baixo. */
@media (min-width: 60rem) {
  .album-detail__grid {
    display: grid;
    grid-template-columns: 4.5rem 1fr;
    gap: clamp(1.5rem, 3vw, 3rem);
    align-items: start;
    padding-bottom: 0;
  }

  /* A contagem de colunas (2 / 3 aqui / 4 a 90rem) vive agora no JS do
     masonry (matchMedia com os mesmos breakpoints) — o CSS já não
     precisa de regra própria. */

  .switcher {
    position: sticky;
    top: 5.5rem;
    left: auto;
    right: auto;
    bottom: auto;
    z-index: 1;
    flex-direction: column;
    padding: 0;
    background: none;
    overflow: visible;
    max-height: calc(100vh - 7rem);
    overflow-y: auto;
  }

  .switcher__item {
    width: 100%;
    height: 3.75rem;
  }
}

/* ------------------------------------------------------------------ */
/* About */

/* Composição assimétrica: o retrato é a peça grande e ancora a secção
   (coluna larga, a sangrar para a goteira esquerda); o texto vive numa
   coluna estreita ao lado, centrado contra a altura da foto. A foto é
   landscape (4:3 real) e nunca é cortada — a proporção sai da imagem,
   não de uma moldura imposta. */
.about {
  display: grid;
  /* 1.55/1 ≈ 60/40. Com a foto landscape, uma coluna mais larga que isto
     esmagaria o texto abaixo de uma medida legível; mais estreita e a
     foto deixava de ancorar. */
  grid-template-columns: 1.55fr minmax(16rem, 1fr);
  /* O label "About" é filho directo da grelha; aqui volta ao topo da coluna
     de texto (figura à esquerda a sangrar as duas linhas). No mobile este
     template é anulado e a ordem do DOM manda: label → figura → texto. */
  grid-template-areas:
    "figure label"
    "figure text";
  column-gap: clamp(2rem, 4.5vw, 4rem);
  align-items: center;
  max-width: 82rem;
  margin-inline: auto;
}

.about__label {
  grid-area: label;
}

.about__figure {
  grid-area: figure;
  margin: 0;
  /* Sangra para a goteira esquerda — a composição não é um par de colunas
     centrado e arrumado; o retrato puxa para fora da medida da página. */
  margin-left: clamp(-1.5rem, -4vw, -5rem);
  /* Proporção real do ficheiro (2048×1536). Fica na moldura, não na
     imagem, para o fundo de reserva segurar o espaço se o main.js
     esconder um about.webp em falta. */
  aspect-ratio: 2048 / 1536;
  background:
    radial-gradient(circle at 60% 30%, var(--lamp), transparent 55%),
    var(--bg-raised);
}

/* contain, não cover: com a moldura na proporção exacta do ficheiro não
   há corte nem barras — e um recorte futuro degrada para barras, nunca
   para uma imagem cortada. */
.about__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* A foto assenta enquanto a secção atravessa o ecrã: entra ligeiramente
   maior e fixa-se a 1 ao chegar ao centro. Scroll-driven nativo (sem JS,
   sem listener novo) — a animação está ligada à posição da figura, não ao
   tempo, por isso acompanha o scroll nos dois sentidos e nunca "toca"
   sozinha. Só 1.05: passa como respiração da imagem, não como zoom.
   Fora do @supports (Safari mais antigo) não acontece nada — a foto fica
   simplesmente estática, que é o estado final de qualquer forma. */
@keyframes about-settle {
  from {
    transform: scale(1.05);
  }
  to {
    transform: scale(1);
  }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* A escala sangraria para fora da moldura sem isto. Corta-se com
       clip-path, não com overflow:hidden: overflow faz da figura um
       contentor de scroll, e o view() da foto passava a medir-se contra
       essa caixa — que nunca faz scroll. Resultado: a animação ficava
       presa a meio e a foto nunca assentava. clip-path corta na mesma
       sem criar contentor de scroll. */
    .about__figure {
      clip-path: inset(0);
    }

    .about__photo {
      animation: about-settle linear both;
      animation-timeline: view();
      animation-range: entry 15% cover 55%;
    }
  }
}

/* Ancorada ao topo da linha da grelha, não centrada: com align-items:center
   no .about, abrir o dropdown dos stats fazia o bloco inteiro re-centrar
   contra a altura da foto e empurrava o heading/nota para cima. Fixo ao
   topo, o dropdown só cresce para baixo. (No mobile o .about já é
   align-items:start, por isso isto não muda nada lá.) */
.about__text {
  grid-area: text;
  align-self: start;
}

/* As duas frases são uma só voz: heading e nota partilham a aresta
   esquerda e a mesma coluna, separadas só por hierarquia tipográfica. */
.about__line {
  font-family: var(--font-display);
  font-weight: 200;
  font-size: clamp(1.9rem, 3vw, 2.5rem);
  line-height: 1.2;
  margin-top: 0.35rem;
}

.about__note {
  margin-top: 1.15rem;
  color: var(--ink-dim);
  font-size: 0.98rem;
  line-height: 1.55;
}

.about__line em {
  font-style: italic;
  font-weight: 400;
}

/* Mobile: o retrato rompe as goteiras de borda a borda (full-bleed) — no
   telemóvel o retrato É a peça, à altura da força da fotografia — e o
   bloco de texto fecha por baixo, alinhado à esquerda como o resto do
   site. Continua sem corte: a imagem impõe a altura. */
@media (max-width: 48rem) {
  .about {
    grid-template-columns: 1fr;
    /* Anula as áreas do desktop — a ordem do DOM manda: label → figura →
       texto (título por cima da foto, textos e dropdown por baixo). */
    grid-template-areas: none;
    row-gap: clamp(1.75rem, 5vh, 2.5rem);
    align-items: start;
  }

  /* Sem as áreas nomeadas, os grid-area do desktop apontavam para linhas
     inexistentes e empilhavam label e texto na mesma célula (sobreposição).
     Auto devolve o empilhamento normal pela ordem do DOM. */
  .about__label,
  .about__figure,
  .about__text {
    grid-area: auto;
  }

  /* Só no mobile o "About" ganha a voz do cabeçalho do Work with me:
     centrado, com um traço de cada lado. No desktop mantém-se alinhado à
     esquerda no topo da coluna de texto (um único traço, à esquerda). */
  .about__label {
    justify-content: center;
    margin-bottom: 0;
  }

  .about__label::after {
    content: "";
    width: 2.5rem;
    height: 1px;
    background: var(--wine-bright);
  }

  .about__figure {
    /* Rompe o padding da secção até às bordas do ecrã. */
    margin-inline: calc(var(--pad-inline) * -1);
  }

  .about__line {
    /* Abre maior no mobile — a declaração ganha presença de título. */
    font-size: clamp(1.9rem, 7.5vw, 2.5rem);
    line-height: 1.15;
    margin-top: 0.25rem;
  }
}

/* ------------------------------------------------------------------ */
/* About — entrada atada ao scroll (momento-assinatura).
   A foto entra com o reveal-base subtil do site (data-reveal, main.js);
   o gesto próprio desta secção é o TEXTO a subir atado ao scroll. Sobe
   como um bloco só, porque é uma voz só — não duas frases a entrar em
   tempos diferentes. Mesmo padrão validado nos portais:
   animation-timeline: view() isolado por @supports (Firefox 2026 ainda
   sem view() → texto assente), sob prefers-reduced-motion. Sem JS, sem
   biblioteca. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .about__text {
      animation: about-rise linear both;
      animation-timeline: view();
      animation-range: cover 12% contain 40%;
    }
  }
}

@keyframes about-rise {
  from {
    opacity: 0;
    translate: 0 1.5rem;
  }
  to {
    opacity: 1;
    translate: 0 0;
  }
}

/* ------------------------------------------------------------------ */
/* About — quick facts (stats). Curadoria deliberada do PurplePort: só o
   que ajuda quem vai reservar a planear guarda-roupa/conceito (altura,
   medidas, tamanhos, olhos/cabelo, tatuagens) — não a lista toda.
   Semântica de <dl>: é literalmente uma lista termo/valor. Cada par vive
   num wrapper .about__stat para o ícone poder ficar ao lado (HTML5
   permite <div> a agrupar dt/dd dentro de <dl>). Mesma linguagem visual
   do resto do site: hairline a separar linhas (como .offer__name,
   .album + .album), rótulo tracked-out em maiúsculas (como
   .section__label / .portal__hint), ícones de traço fino em
   --wine-bright (o mesmo tratamento do ícone da câmara no cursor, a peça
   de referência da família) — nunca preenchidos, nunca ícones de
   biblioteca. */
.about__stat {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--hairline);
}

.about__stat:last-child {
  border-bottom: none;
}

.about__stat-icon {
  flex: 0 0 auto;
  width: 1.1rem;
  height: 1.1rem;
  margin-top: 0.2rem;
  color: var(--wine-bright);
}

.about__stat-text {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.about__stat-text dt {
  margin: 0;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ink-faint);
}

.about__stat-text dd {
  margin: 0;
  font-size: 0.98rem;
  color: var(--ink);
  line-height: 1.4;
}

/* Segundo idioma dentro do valor (unidade, conversão, legenda) — um
   degrau mais discreto que o valor principal, mesmo princípio do
   .offer__line-label a distinguir-se do .offer__line. */
.about__stat-alt {
  color: var(--ink-dim);
  font-size: 0.9em;
}

/* About stats como <details> dropdown. Card com fundo e hairline, como os
   offers. O padding do card vive numa variável para o summary (que sangra
   até às bordas para dar alvo de clique) e o mobile ficarem sempre em sync.
   Fechado por defeito em desktop e mobile. */
.about__stats {
  display: block;
  --stats-pad: clamp(1.1rem, 2.4vw, 1.5rem);
  margin: clamp(1.75rem, 4vh, 2.5rem) 0 0;
  padding: var(--stats-pad);
  background: var(--bg-raised);
  border: 1px solid var(--hairline);
  transition: border-color 0.35s var(--ease);
}

.about__stats:hover {
  border-color: var(--wine-bright);
}

.about__stats summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  cursor: pointer;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-dim);
  user-select: none;
  transition: color 0.35s var(--ease);
  /* Sangra até às bordas do card para o alvo de clique ser o topo inteiro. */
  margin: calc(-1 * var(--stats-pad));
  margin-bottom: 0;
  padding: var(--stats-pad);
}

.about__stats summary:hover {
  color: var(--ink);
}

.about__stats summary::marker {
  display: none;
}

/* Ícone customizado que rotaciona ao abrir. Traço fino desenhado à mão,
   mesma família do ícone da câmara e dos ícones de navegação. Lê-se como "abrir"
   ao ser um sinal de adição (+) em estado fechado. */
.about__stats-toggle {
  flex: 0 0 auto;
  width: 0.85rem;
  height: 0.85rem;
  color: var(--wine-bright);
  transition: rotate 0.35s var(--ease);
  display: flex;
  align-items: center;
  justify-content: center;
}

.about__stats[open] .about__stats-toggle {
  rotate: 45deg;
}

.about__stats-toggle svg {
  width: 100%;
  height: 100%;
  stroke: currentColor;
  stroke-width: 1.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Colapso suave PARA BAIXO: anima o ::details-content (o slot nativo do
   <details>) em vez de grid-rows. O UA põe o conteúdo do <details> fechado
   em display:none, por isso a antiga animação de grelha nunca corria a
   partir do estado fechado — abria "duro", de repente. Animar block-size
   até auto (graças ao interpolate-size no :root) desliza a altura de 0 até
   ao conteúdo, para baixo. Firefox 2026 sem ::details-content anima nada
   e salta aberto — progressive enhancement, continua funcional. */
.about__stats::details-content {
  block-size: 0;
  overflow: hidden;
  opacity: 0;
  transition: block-size 0.55s var(--ease), opacity 0.4s var(--ease),
    content-visibility 0.55s allow-discrete;
}

.about__stats[open]::details-content {
  block-size: auto;
  opacity: 1;
}

/* Hairline a separar o resumo da lista, com a mesma folga do padding do
   card. Fica sempre presente; escondido pela altura 0 enquanto fechado. */
.about__stats-panel > div {
  padding-top: var(--stats-pad);
  border-top: 1px solid var(--hairline);
}

/* Mobile: só o sizing muda — o padding do card encolhe via --stats-pad
   (o summary segue-o automaticamente). A lista respira um pouco mais
   (1rem, alinhado com os alvos de toque do resto do mobile). */
@media (max-width: 48rem) {
  .about__stats {
    --stats-pad: 1rem;
    margin-top: clamp(2rem, 6vh, 2.75rem);
  }

  .about__stat {
    padding: 1rem 0;
  }

  .about__stat-text dd {
    font-size: 1rem;
  }
}

/* ------------------------------------------------------------------ */
/* Work with me */

/* O cabeçalho centra-se com as ofertas — desalinhado ficava a puxar para a
   esquerda contra colunas centradas. O rótulo mantém o traço vinho,
   agora com o par simétrico do outro lado, para o eixo bater certo. */
#work .section__label {
  justify-content: center;
}

#work .section__label::after {
  content: "";
  width: 2.5rem;
  height: 1px;
  background: var(--wine-bright);
}

#work .section__title {
  max-width: 20ch;
  margin-inline: auto;
  margin-bottom: clamp(2.5rem, 6vh, 4rem);
  text-align: center;
}

/* Quatro contentores lado a lado, texto centrado. O contentor é feito com a
   linguagem do site — hairline + --bg-raised, sem sombra e sem cantos
   arredondados — para ler como caixa editorial e não como card SaaS. */
.offers {
  max-width: 80rem;
  margin-inline: auto;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1rem, 2vw, 1.75rem);
}

.offer {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: clamp(2rem, 4vw, 3rem) clamp(1.25rem, 2.5vw, 2.25rem);
  background: var(--bg-raised);
  border: 1px solid var(--hairline);
  transition: border-color 0.35s var(--ease);
}

.offer:hover {
  border-color: var(--wine-bright);
}

/* A dica só faz sentido onde o acordeão existe (telemóvel). */
.offers-hint {
  display: none;
}

/* O botão só se comporta como botão no acordeão: no desktop é o título,
   sem cursor de clique nem sublinhado. */
.offer__toggle {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  width: 100%;
  cursor: default;
  text-align: inherit;
}

.offer__toggle-icon {
  display: none;
}

/* display:contents no desktop: o painel existe no DOM para o acordeão do
   telemóvel, mas aqui não cria caixa nenhuma — as linhas e o CTA
   continuam a ser filhos directos do flex de .offer__body, e por isso o
   margin-top:auto do CTA continua a alinhar os botões. */
.offer__panel {
  display: contents;
}

/* flex:1 + o margin-top:auto do CTA alinham os botões na mesma
   linha, mesmo com blocos de texto de alturas diferentes. */
.offer__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1;
  width: 100%;
}

/* Hairline a fechar o nome: separa-o do corpo da caixa sem precisar de
   peso nem de cor — mesma linguagem do resto do site. */
.offer__name {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.9rem, 3.5vw, 2.75rem);
  line-height: 1.1;
  color: var(--ink);
  width: 100%;
  padding-bottom: clamp(1rem, 2vh, 1.4rem);
  margin-bottom: clamp(1.4rem, 3vh, 2rem);
  border-bottom: 1px solid var(--hairline);
}

/* Duas linhas por bloco ("o que inclui" / "como funciona"). O rótulo é
   inline e não um cabeçalho: mantém-se prosa, não vira feature list. */
.offer__line {
  max-width: 34ch;
  color: var(--ink-dim);
}

.offer__line + .offer__line {
  margin-top: 1.25rem;
}

/* O subtítulo precisa de voz própria: na condensada e em corpo parecido
   com o do texto, lia-se como mais uma linha do parágrafo. Fica na
   Newsreader romana, caixa alta e baixa — três registos distintos
   dentro da caixa: nome em itálico, subtítulo em romana, texto na
   condensada. --ink cheio contra o --ink-dim do corpo. */
.offer__line-label {
  display: block;
  margin-bottom: 0.5rem;
  font-family: var(--font-display);
  font-size: 1.15rem;
  line-height: 1.2;
  letter-spacing: 0.01em;
  color: var(--ink);
}

.offer__cta {
  display: inline-block;
  margin-top: auto;
  padding-top: clamp(2rem, 4vh, 2.75rem);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  text-decoration: none;
  color: var(--ink);
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--wine-bright);
  transition: color 0.35s var(--ease), border-color 0.35s var(--ease),
    letter-spacing 0.35s var(--ease);
}

.offer__cta:hover,
.offer__cta:focus-visible {
  color: var(--wine-bright);
  letter-spacing: 0.22em;
}

/* Bloco de logística do "Work with me" — informação transversal às
   caixas de oferta, por isso vive fora da grelha delas e sem caixa
   própria: não é mais uma categoria. A hairline em cima é a mesma do resto do site e
   faz o trabalho que o espaço branco sozinho não fazia — lê-se como
   secção nova mas ligada, não como sobra. 56rem e não as 80rem da grelha
   das ofertas: à largura toda sobrava uma goteira morta no meio e os dois
   blocos deixavam de se ler como par. */
/* Banda-epílogo do "Work with me". Mesmo material das caixas de oferta
   acima (--bg-raised + hairline, sem sombra nem raio) e a mesma largura
   (80rem), para alinhar aresta a aresta com o trio e ler como parte do
   mesmo sistema — não como texto solto no fim da secção. Duas zonas
   divididas por uma hairline: logística à esquerda, página de membros à
   direita. Alinhado à esquerda de propósito: o trio acima é simétrico
   (menu), esta banda é a nota de rodapé — o contraste é a hierarquia. */
.beyond {
  max-width: 80rem;
  margin: clamp(3rem, 8vh, 5rem) auto 0;
  display: grid;
  grid-template-columns: 1.25fr 1fr;
  background: var(--bg-raised);
  border: 1px solid var(--hairline);
}

.beyond__zone {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: clamp(2rem, 4vw, 3.25rem);
}

/* A hairline entre as zonas é a mesma do resto do site — divide sem peso.
   Vertical no desktop; passa a horizontal ao empilhar (bloco @media). */
.beyond__zone--members {
  border-left: 1px solid var(--hairline);
}

/* Ícone + heading na mesma linha (ícone à esquerda), como o .about__stat. */
.beyond__head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: clamp(1rem, 2.5vh, 1.5rem);
}

/* Traço fino em --wine-bright, mesma família do ícone da câmara no cursor
   — nunca preenchido, nunca ícone de biblioteca. */
.beyond__icon {
  flex: 0 0 auto;
  width: 1.3rem;
  height: 1.3rem;
  color: var(--wine-bright);
}

/* Newsreader itálico, romano 400 — mesmo registo do nome das caixas de
   oferta, um degrau abaixo na escala: subordinado ao trio, não par dele. */
.beyond__heading {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.5rem, 2.4vw, 2rem);
  line-height: 1.1;
  color: var(--ink);
}

.beyond__body {
  color: var(--ink-dim);
  max-width: 46ch;
}

/* Empurra o badge do Patreon para a base da zona, para assentar numa linha
   estável independente da altura do parágrafo. */
.beyond__zone--members .patreon-link {
  margin-top: auto;
}

/* Único sítio do site onde uma marca externa aparece — contida neste badge,
   sem sangrar para a paleta à volta. A identidade actual do Patreon
   (rebrand de Out/2023) é preto #070707 + branco, não o coral #FF424D
   legado: cor e path confirmados no favicon.svg servido pelo próprio
   Patreon. O hover reproduz o lockup deles (marca preta sobre branco). */
.patreon-link {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  margin-top: 1.75rem;
  padding: 0.75rem 1.15rem;
  border: 1px solid var(--hairline);
  color: var(--ink);
  text-decoration: none;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  transition: background-color 0.35s var(--ease), color 0.35s var(--ease),
    border-color 0.35s var(--ease);
}

.patreon-link__mark {
  width: 1.05rem;
  height: 1.12rem;
  flex: none;
  fill: currentColor;
}

.patreon-link:hover,
.patreon-link:focus-visible {
  background: #fff;
  border-color: #fff;
  color: #070707;
}

/* Abaixo de 62rem quatro colunas espremem as linhas de texto a 2–3 palavras;
   duas colunas primeiro, coluna única só no telemóvel. */
@media (max-width: 62rem) {
  .offers {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Telemóvel: acordeão. Quatro caixas abertas davam ~1000pt de scroll só
   nesta secção; fechadas, a secção cabe quase toda no ecrã e a pessoa
   abre a que lhe interessa. Exclusivo — abrir uma fecha a outra (o JS
   trata disso).
   O texto continua todo lá: fechar é esconder, não cortar. Por isso a
   caixa também é comprimida (padding e entrelinha menores) quando aberta.
   A altura anima por grid-template-rows 0fr→1fr, que funciona sem saber
   a altura do conteúdo; sob prefers-reduced-motion o kill-switch do
   reset.css desliga a transição e o painel salta, aberto na mesma. */
@media (max-width: 48rem) {
  .offers {
    grid-template-columns: 1fr;
    gap: 0.85rem;
  }

  .offers-hint {
    display: block;
    margin: -1.5rem 0 1.75rem;
    text-align: center;
    font-size: 0.9rem;
    color: var(--ink-faint);
  }

  .offer {
    padding: 1.35rem;
  }

  /* Alvo de toque: a linha inteira do nome, não só as letras. */
  .offer__toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    cursor: pointer;
    text-align: left;
  }

  /* Dois traços cruzados: "+" fechado, "×" aberto. A rotação de 45° dá as
     duas leituras sem trocar de glifo nem carregar um ícone. */
  .offer__toggle-icon {
    display: block;
    position: relative;
    flex: none;
    width: 0.85rem;
    height: 0.85rem;
    transition: rotate 0.35s var(--ease);
  }

  .offer__toggle-icon::before,
  .offer__toggle-icon::after {
    content: "";
    position: absolute;
    inset: 50% 0 auto;
    height: 1px;
    background: var(--wine-bright);
  }

  .offer__toggle-icon::after {
    rotate: 90deg;
  }

  .offer--open .offer__toggle-icon {
    rotate: 45deg;
  }

  .offer__name {
    font-size: 1.75rem;
    text-align: left;
    padding-bottom: 0;
    margin-bottom: 0;
    border-bottom: 0;
  }

  /* A hairline passa a marcar a abertura: só existe com o painel aberto,
     a separar o nome do texto — fechada, a caixa é uma linha limpa. */
  .offer--open .offer__name {
    padding-bottom: 0.75rem;
    margin-bottom: 1rem;
    border-bottom: 1px solid var(--hairline);
  }

  /* align-items:stretch é obrigatório aqui, não é cosmético: com o
     `center` herdado do flex do desktop, o painel mantinha a altura do
     conteúdo em vez de encolher para a linha de 0fr, e transbordava
     centrado para fora da caixa — o texto fechado aparecia por cima dos
     blocos vizinhos. Esticado, a linha de 0fr esmaga-o e o overflow:hidden
     do painel corta-o. flex:none pelo mesmo motivo: nada deve dar altura
     a este elemento além da linha da grelha. */
  .offer__body {
    display: grid;
    grid-template-rows: 0fr;
    align-items: stretch;
    flex: none;
    /* Colapso suave para baixo, alinhado com o timing do dropdown do About. */
    transition: grid-template-rows 0.5s var(--ease);
  }

  .offer--open .offer__body {
    grid-template-rows: 1fr;
  }

  .offer__panel {
    display: block;
    min-height: 0;
    overflow: hidden;
    /* Com o nome à esquerda (o "+" pede a linha toda), centrar o texto por
       baixo ficava desalinhado — o painel inteiro alinha à esquerda. */
    text-align: left;
  }

  .offer__line {
    max-width: none;
    font-size: 0.95rem;
    line-height: 1.35;
  }

  .offer__line + .offer__line {
    margin-top: 0.9rem;
  }

  .offer__line-label {
    margin-bottom: 0.3rem;
    font-size: 1.05rem;
  }

  /* Empilhado, o CTA já não precisa de ser empurrado para alinhar com
     os vizinhos — encosta ao texto do próprio bloco. */
  .offer__cta {
    margin-top: 0;
    padding-top: 1.35rem;
  }

  /* A banda-epílogo empilha: as duas zonas viram uma coluna e a hairline
     que as dividia passa de vertical a horizontal. */
  .beyond {
    grid-template-columns: 1fr;
  }

  .beyond__zone--members {
    border-left: 0;
    border-top: 1px solid var(--hairline);
  }

  .beyond__heading {
    font-size: 1.5rem;
  }

  .beyond__body {
    max-width: none;
    font-size: 0.95rem;
    line-height: 1.35;
  }

  /* margin-top:auto só empurra o badge para a base quando sobra espaço
     livre no eixo do flex — com as zonas empilhadas cada uma tem
     exactamente a altura do seu conteúdo, não sobra nada, e o badge
     colava-se ao texto. Volta a uma distância fixa, como no botão base. */
  .beyond__zone--members .patreon-link {
    margin-top: 1.75rem;
  }
}

/* ------------------------------------------------------------------ */
/* Booking — só apresentação; estrutura funcional do form intocada
   (nomes/ids dos campos são contrato com js/main.js e functions/booking.js) */

/* Cabeçalho centrado — mesmo eixo simétrico de #work e #portfolio, agora
   em desktop e mobile, para o site inteiro partilhar um só padrão de
   cabeçalho por secção. */
#booking .section__label {
  justify-content: center;
}

#booking .section__label::after {
  content: "";
  width: 2.5rem;
  height: 1px;
  background: var(--wine-bright);
}

#booking .section__title {
  max-width: 20ch;
  margin-inline: auto;
  text-align: center;
}

.booking-intro {
  max-width: 46ch;
  margin-inline: auto;
  text-align: center;
  color: var(--ink-dim);
  margin-top: 1.25rem;
  margin-bottom: clamp(2.75rem, 7vh, 4.5rem);
  text-wrap: pretty;
}

#booking-form {
  max-width: 80rem;
  margin: 0 auto;
}

/* Caixa de agrupamento do formulário. Mesmo material das .offer do
   Work with me — --bg-raised, hairline, zero border-radius, sem sombra —
   para o formulário não introduzir um segundo padrão de card no site.
   Sem :hover: aqui a caixa é estrutura de leitura, não alvo de clique. */
.form-group {
  border: 1px solid var(--hairline);
  background: var(--bg-raised);
  padding: clamp(1.75rem, 3.5vw, 2.75rem);
  margin: 0;
  min-inline-size: 0; /* fieldset traz min-width:min-content por defeito */
}

.form-group + .form-group {
  margin-top: clamp(1rem, 2vw, 1.75rem);
}

/* float+width:100% impede o <legend> de abrir o entalhe no topo da borda:
   assim a caixa fecha-se por completo e o rótulo lê-se como primeira
   linha do bloco, na mesma voz do .section__label. */
.form-group__legend {
  float: left;
  width: 100%;
  padding: 0;
  margin-bottom: clamp(1.75rem, 3vw, 2.25rem);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-dim);
}

/* O clear é obrigatório, não cosmético: sem ele a grelha (contexto de
   formatação independente) coloca-se AO LADO do legend flutuante de
   width:100% e colapsa para uma coluna de poucos pixéis. */
.form-group .form-grid {
  clear: both;
}

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2.75rem clamp(2rem, 5vw, 4rem);
}

.form-field--full {
  grid-column: 1 / -1;
}

/* Marca de campo obrigatório — o asterisco herda o acento vinho para o
   par obrigatório/opcional se ler de relance, sem legenda extra. */
.lbl-req {
  color: var(--wine-bright);
  margin-left: 0.15em;
}

/* Hairline a separar o envio dos consentimentos dentro da mesma caixa —
   o botão é o fim do gesto, não mais uma linha da lista. */
.form-field--group-start {
  margin-top: 1rem;
  padding-top: 2.75rem;
  border-top: 1px solid var(--hairline);
}

/* O botão ganha ar antes de si — o envio é o fim do gesto, não mais uma
   linha da grelha. */
.form-field--submit {
  margin-top: 0.5rem;
}

.form-field label {
  display: block;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
  margin-bottom: 0.6rem;
}

.form-field input[type="text"],
.form-field input[type="date"],
.form-field input[type="email"],
.form-field input[type="tel"],
.form-field select,
.form-field textarea {
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--hairline);
  padding: 0.55rem 0;
  color: var(--ink);
  transition: border-color 0.35s var(--ease);
}

.form-field input::placeholder,
.form-field textarea::placeholder {
  color: var(--ink-faint);
}

.form-field input:hover,
.form-field select:hover,
.form-field textarea:hover {
  border-bottom-color: var(--ink-faint);
}

.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
  outline: none;
  border-bottom-color: var(--wine-bright);
}

/* Marca visual de campo obrigatório inválido depois de interacção —
   puro CSS, sem tocar na lógica de validação do main.js. */
.form-field input:user-invalid,
.form-field select:user-invalid,
.form-field textarea:user-invalid {
  border-bottom-color: var(--wine-bright);
}

.form-field select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 1.5rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%23F0EFED' stroke-opacity='.55' stroke-width='1'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.25rem center;
}

.form-field select option {
  background: var(--bg-raised);
  color: var(--ink);
}

.form-field textarea {
  min-height: 7.5rem;
  resize: vertical;
}

/* Nota de antecedência sob o campo de data internacional — mesma voz
   discreta do .about__note, em escala de campo de formulário. */
.form-field__note {
  font-size: 0.8rem;
  color: var(--ink-dim);
  margin-top: 0.5rem;
}

.form-field--checkbox {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
}

.form-field--checkbox input {
  width: 1.05rem;
  height: 1.05rem;
  margin-top: 0.2rem;
  accent-color: var(--wine);
  flex-shrink: 0;
}

.form-field--checkbox label {
  margin-bottom: 0;
  text-transform: none;
  letter-spacing: 0.02em;
  font-size: 0.95rem;
  color: var(--ink-dim);
}

#form-error {
  grid-column: 1 / -1;
  color: var(--wine-bright);
  border-left: 2px solid var(--wine-bright);
  padding-left: 1rem;
  font-size: 0.95rem;
}

.btn {
  display: inline-block;
  background: var(--wine);
  color: var(--ink);
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  padding: 1.05rem 3rem;
  transition: background 0.35s var(--ease), letter-spacing 0.35s var(--ease);
}

.btn:hover,
.btn:focus-visible {
  background: var(--wine-hover);
  letter-spacing: 0.26em;
}

.btn:disabled {
  opacity: 0.5;
  cursor: wait;
}

/* Telemóvel: a grelha continua de 2 colunas, mas por defeito cada campo
   ocupa a linha inteira. Só os pares explicitamente curtos (.form-field--pair)
   ficam lado a lado — assim o formulário não vira uma coluna infinita nem
   espreme selects de opções longas em meia coluna. */
@media (max-width: 48rem) {
  .form-grid {
    gap: 2rem 1.25rem;
  }

  .form-field {
    grid-column: 1 / -1;
  }

  .form-field--pair {
    grid-column: span 1;
  }
}

/* ------------------------------------------------------------------ */
/* Footer */

.footer {
  margin-top: 0;
  border-top: 1px solid var(--hairline);
  padding: var(--section-gap) var(--pad-inline) 2.5rem;
  text-align: center;
}

.footer__name {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.6rem, 3vw, 2.2rem);
}

/* Mesmo sinal do Book da nav: acento + sublinhado sempre presente, para
   ler como "a acção" e não como mais um link da lista abaixo. Fica na
   linha do nome (o bloco-assinatura) por isso mesmo — em .footer__links
   seria o quinto de cinco itens iguais, e os outros quatro são canais de
   contacto, não acções. */
.footer__cta {
  display: inline-block;
  margin-top: 1.4rem;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  text-decoration: none;
  padding-bottom: 0.2rem;
  color: var(--wine-bright);
  background-image: linear-gradient(var(--wine-bright), var(--wine-bright));
  background-repeat: no-repeat;
  background-size: 100% 1px;
  background-position: left bottom;
  transition: color 0.35s var(--ease);
}

.footer__cta:hover,
.footer__cta:focus-visible {
  color: var(--ink);
}

.footer__links {
  margin-top: 3rem;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem 2.25rem;
}

.footer__links a {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  text-decoration: none;
  color: var(--ink-dim);
  transition: color 0.35s var(--ease);
}

.footer__links a:hover,
.footer__links a:focus-visible {
  color: var(--wine-bright);
}

.footer__legal {
  margin-top: 1.5rem;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem 1.75rem;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
}

.footer__legal a {
  color: inherit;
  text-decoration: none;
  transition: color 0.35s var(--ease);
}

.footer__legal a:hover,
.footer__legal a:focus-visible {
  color: var(--ink);
}

/* Em mobile o registo passa para linha própria, centrado sob Privacy e
   Terms: são coisas diferentes — dois links e uma nota de copyright — e
   lado a lado na largura estreita lêem-se como três itens da mesma lista.
   flex-basis: 100% força a quebra sem markup extra. */
@media (max-width: 48rem) {
  .footer__legal span {
    flex-basis: 100%;
  }
}

/* ------------------------------------------------------------------ */
/* Cursor contextual (só rato de precisão; activado por JS com
   html.has-cursor — em touch nada disto existe) */

#cursor {
  display: none;
}

html.has-cursor #cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 10px;
  height: 10px;
  margin: -5px 0 0 -5px;
  border-radius: 50%;
  background: var(--ink);
  mix-blend-mode: difference;
  pointer-events: none;
  transition: width 0.3s var(--ease), height 0.3s var(--ease),
    margin 0.3s var(--ease), background 0.3s var(--ease),
    opacity 0.3s var(--ease);
}

html.has-cursor #cursor span {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}

/* Mesmo par de estados do <span>: escondido por omissão, revelado
   quando o alvo pede ícone. Cabe dentro do círculo de 4.5rem com a
   mesma folga que o rótulo tinha. */
html.has-cursor #cursor .cursor__icon {
  position: absolute;
  width: 1.6rem;
  height: 1.6rem;
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}

html.has-cursor #cursor.cursor--icon .cursor__icon {
  opacity: 1;
}

html.has-cursor #cursor.cursor--label {
  width: 4.5rem;
  height: 4.5rem;
  margin: -2.25rem 0 0 -2.25rem;
  background: var(--wine);
  mix-blend-mode: normal;
}

html.has-cursor #cursor.cursor--label span {
  opacity: 1;
}

html.has-cursor #cursor.cursor--hidden {
  opacity: 0;
}

html.has-cursor [data-cursor] {
  cursor: none;
}

/* ------------------------------------------------------------------ */
/* Alvos de toque — em ecrã táctil os links pequenos de UI ganham área
   de toque decente (~44px) sem mudar o desenho em desktop. */

@media (pointer: coarse) {
  .nav__links a,
  .footer__links a,
  .footer__legal a,
  .link-back,
  .gallery__close,
  .offer__cta {
    padding-top: 0.7rem;
    padding-bottom: 0.7rem;
  }

  .form-field--checkbox input {
    width: 1.35rem;
    height: 1.35rem;
  }

  .form-field input[type="text"],
  .form-field input[type="date"],
  .form-field input[type="email"],
  .form-field input[type="tel"],
  .form-field select {
    padding-top: 0.8rem;
    padding-bottom: 0.8rem;
  }
}

/* ------------------------------------------------------------------ */
/* Páginas de documento (privacy, terms, booking-confirmation) —
   <main> sem classe; nenhum markup dessas páginas precisou de mudar. */

main:not([class]) {
  max-width: 44rem;
  margin-inline: auto;
  padding: clamp(5rem, 12vh, 8rem) 1.5rem 5rem;
}

main:not([class]) h1 {
  font-family: var(--font-display);
  font-weight: 200;
  font-size: clamp(2.4rem, 6vw, 3.5rem);
  line-height: 1.05;
  margin-bottom: 0.75rem;
}

main:not([class]) h2 {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: 1.5rem;
  margin: 2.75rem 0 0.75rem;
}

main:not([class]) p {
  margin-top: 0.75rem;
  color: var(--ink-dim);
  max-width: 62ch;
}

main:not([class]) ul {
  margin-top: 0.75rem;
  display: grid;
  gap: 0.5rem;
}

main:not([class]) li {
  position: relative;
  padding-left: 1.4rem;
  color: var(--ink-dim);
}

main:not([class]) li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.75em;
  width: 0.85rem;
  height: 1px;
  background: var(--wine-bright);
}

main:not([class]) a {
  color: var(--ink);
  text-decoration-color: var(--wine-bright);
  text-underline-offset: 0.25em;
  transition: color 0.35s var(--ease);
}

main:not([class]) a:hover,
main:not([class]) a:focus-visible {
  color: var(--wine-bright);
}
/* ------------------------------------------------------------------ */
/* "On set" (#together) — testemunhos aprovados + fita de colaboradores.
   Uma só secção com duas sub-partes de origens diferentes (API e lista
   fixa em js/index.js). Ambas se escondem sozinhas quando vazias; a
   secção arranca com [hidden] e é o JS que a revela. */

/* Cabeçalho centrado, como o de #work e #booking — a secção fica entre
   os dois e não tem razão nenhuma para mudar de eixo a meio. */
#together .section__label {
  justify-content: center;
}

#together .section__label::after {
  content: "";
  width: 2.5rem;
  height: 1px;
  background: var(--wine-bright);
}

/* Dois degraus abaixo do .section__title padrão: esta secção é sobre
   outras pessoas, e o título é curto de propósito — abre a secção com
   calor, não com o peso do "Book a shoot." ou do "Four ways in.". */
#together .section__title {
  font-size: clamp(1.6rem, 2.6vw, 2.1rem);
  max-width: 20ch;
  margin-inline: auto;
  text-align: center;
}

/* Testemunhos: texto e mais nada. Sem aspas decorativas, sem card, sem
   sombra — a citação assenta na página com o mesmo tratamento
   tipográfico do .about__line, que é a voz "frase grande" do site. */
.voices {
  max-width: 46rem;
  /* Mesmo intervalo "título → corpo" das outras secções (o triplo de
     #work, .portals e .booking-intro) — sem ele a primeira citação
     encostava ao título, que é da mesma família tipográfica. */
  margin: clamp(2.5rem, 6vh, 4rem) auto 0;
}

.voice + .voice {
  margin-top: clamp(2.5rem, 6vh, 3.75rem);
  padding-top: clamp(2.5rem, 6vh, 3.75rem);
  border-top: 1px solid var(--hairline);
}

.voice__quote {
  font-family: var(--font-display);
  font-weight: 200;
  font-style: italic;
  /* Um degrau abaixo do .section__title em todas as larguras: a citação
     é a voz de outra pessoa dentro da secção, não o título dela. */
  font-size: clamp(1.35rem, 2.6vw, 2.1rem);
  line-height: 1.25;
  text-wrap: pretty;
}

/* Atribuição na condensada tracked-out — o mesmo par de vozes do resto
   do site (display para a frase, condensada para a etiqueta). */
.voice__by {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 1rem;
  margin-top: 1.25rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
}

.voice__handle {
  color: var(--wine-bright);
  text-decoration: none;
  text-transform: none;
  letter-spacing: 0.08em;
  transition: color 0.35s var(--ease);
}

.voice__handle:hover,
.voice__handle:focus-visible {
  color: var(--ink);
}

/* Colaboradores. Se houver testemunhos por cima, a fita ganha a mesma
   folga que separa dois testemunhos — sem hairline, porque aqui a
   mudança de material (texto → cartões) já divide. */
.collabs {
  margin-top: clamp(3rem, 8vh, 5rem);
}

/* A hairline entre as duas sub-partes alinha-se com a coluna das
   citações (46rem), não com a largura da secção: é a mesma divisória que
   separa dois testemunhos, e a fita de cartões é que é full-bleed. */
.voices:not([hidden]) + .collabs {
  position: relative;
  padding-top: clamp(3rem, 8vh, 5rem);
}

.voices:not([hidden]) + .collabs::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(46rem, 100%);
  height: 1px;
  background: var(--hairline);
}

.collabs__heading {
  font-family: var(--font-display);
  font-weight: 400;
  font-style: italic;
  font-size: clamp(1.25rem, 2.2vw, 1.6rem);
  text-align: center;
  margin-bottom: clamp(1.75rem, 4vh, 2.5rem);
}

/* Ao toque a linha é só a fita, que sangra até às bordas — daí display
   block, para as margens negativas dela continuarem a funcionar como
   antes. No desktop passa a três colunas: seta · fita · seta. */
.collabs__row {
  display: block;
}

@media (min-width: 48rem) {
  .collabs__row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: clamp(0.5rem, 1.5vw, 1.25rem);
  }
}

@media (min-width: 48rem) and (hover: hover) and (pointer: fine) {
  .collabs__arrow:not([hidden]) {
    display: grid;
  }
}

/* Mesmo material do resto: sem caixa, sem círculo, sem sombra — só o
   traço, que acende ao passar.
   Escondidas por omissão: sem JS não há navegação para conduzir, e ao
   toque a fita arrasta-se com o dedo. Só aparecem onde há rato (o
   [hidden] deixa o JS escondê-las se a lista couber inteira no ecrã). */
.collabs__arrow {
  display: none;
  flex: 0 0 auto;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color 0.35s var(--ease);
}

.collabs__arrow svg {
  width: 1.4rem;
  height: 1.4rem;
}

.collabs__arrow:hover,
.collabs__arrow:focus-visible {
  color: var(--ink);
}

/* Fita horizontal: scroll nativo em todas as larguras — as setas do
   desktop conduzem este mesmo scroll, não o substituem.
   Rompe as goteiras da secção para a fita começar e acabar na margem, e
   --collab-per-view manda no número de cartões visíveis: 2 ao toque, 5
   no desktop. O JS lê esta mesma variável para saber quanto avançar, por
   isso o número vive num sítio só. */
.collabs__track {
  --collab-gap: clamp(0.75rem, 2vw, 1.25rem);
  --collab-per-view: 2;
  /* Nesga do cartão seguinte deixada a descoberto na aresta direita. Ao
     toque fica a 0: lá a fita sangra para fora do ecrã e a nesga já
     acontece sozinha. É o desktop, com a fita fechada em caixa, que
     precisa dela — sem isto os cinco retratos acabavam certos na borda e
     a fita passava por ser a lista inteira. */
  --collab-peek: 0rem;
  display: flex;
  gap: var(--collab-gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  margin-inline: calc(-1 * var(--pad-inline));
  padding-inline: var(--pad-inline);
  /* O snap mede-se pela padding box, que ignora o padding-inline: sem
     isto o cartão encostava à borda do ecrã em vez de assentar na goteira
     da página, e a fita deixava de alinhar com o título por cima. */
  scroll-padding-inline: var(--pad-inline);
  padding-bottom: 0.5rem;
}

.collabs__track::-webkit-scrollbar {
  display: none;
}

/* Os cartões dividem exactamente a medida da página: com padding-inline
   igual à goteira da secção, uma "página" da fita começa e acaba
   alinhada com o texto acima — nunca meio cartão cortado na margem. */
.collabs__track > li {
  flex: 0 0 calc(
    (
        100% - var(--collab-peek) - (var(--collab-per-view) - 1) *
          var(--collab-gap)
      ) / var(--collab-per-view)
  );
  scroll-snap-align: start;
}

/* Desktop: 5 cartões, mas com a medida antiga (14rem) — maiores do que
   isto e cinco retratos passavam a dominar a secção, que é sobre outras
   pessoas. Como a fita deixa de precisar da largura toda, larga-se o
   full-bleed e centra-se a caixa: as setas passam a ter onde viver, uma
   de cada lado, dentro da goteira da secção. */
@media (min-width: 48rem) {
  .collabs__track {
    --collab-per-view: 5;
    --collab-peek: 2.75rem;
    flex: 0 1 auto;
    min-width: 0;
    width: calc(5 * 14rem + 4 * var(--collab-gap) + var(--collab-peek));
    max-width: 100%;
    margin-inline: 0;
    padding-inline: 0;
    scroll-padding-inline: 0;
  }
}

/* Mesmo material das caixas do Work with me e do formulário: hairline
   sobre --bg-raised, zero border-radius, sem sombra. */
.collab {
  position: relative;
  display: block;
  height: 100%;
  border: 1px solid var(--hairline);
  background: var(--bg-raised);
  text-decoration: none;
  transition: border-color 0.35s var(--ease);
}

.collab:hover,
.collab:focus-visible {
  border-color: var(--ink-faint);
}

.collab__frame {
  display: block;
  aspect-ratio: 3 / 4;
  overflow: hidden;
}

.collab__photo {
  /* Sem isto a img fica inline e assenta na baseline: sobravam sempre
     uns pixels de --bg-raised na aresta de baixo da moldura. */
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease), opacity 0.6s var(--ease);
}

/* Sem foto (ficheiro ainda por entregar): o cartão perde a moldura e
   fica só com o nome — nada de imagem partida nem caixa vazia. */
.collab--nophoto .collab__frame {
  display: none;
}

.collab__body {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.9rem 1rem 1.05rem;
}

.collab__name {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.05rem;
  line-height: 1.2;
}

.collab__handle {
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  color: var(--wine-bright);
}

/* Onde há rato, o nome e o handle passam para trás da foto e a foto
   sobe e esbate-se a revelá-los — sem desaparecer: fica em fundo bem
   opaco, a dar textura por trás do nome. Em toque nada disto se aplica — o
   nome fica visível de origem, o mesmo princípio já usado nos portais
   do portfolio. */
@media (hover: hover) and (pointer: fine) {
  .collab__body {
    position: absolute;
    inset: 0;
    z-index: 0;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 0.5rem;
  }

  .collab__frame {
    position: relative;
    z-index: 1;
  }

  /* Sem foto, a moldura fica como espaçador transparente: mantém a
     altura do cartão e deixa ver o nome que está por trás. */
  .collab--nophoto .collab__frame {
    display: block;
  }

  /* O gesto é só luz, não deslocação: a foto esbate-se e aproxima-se de
     leve. Deslocá-la (o antigo translateY) abria uma faixa de fundo na
     aresta de baixo assim que ela deixou de desaparecer por completo —
     uma escala nunca descobre a moldura. */
  .collab:hover .collab__photo,
  .collab:focus-visible .collab__photo {
    transform: scale(1.04);
    opacity: 0.16;
  }

  /* A foto continua lá, por isso o nome tem de passar à frente dela —
     senão liam-se um por cima do outro. */
  .collab:hover .collab__body,
  .collab:focus-within .collab__body {
    z-index: 2;
  }
}

/* Convite discreto a quem já trabalhou com ela — uma linha, no fim, sem
   caixa nem botão: não é uma conversão, é uma porta aberta. */
.together__outro {
  margin-top: clamp(2.5rem, 6vh, 3.5rem);
  text-align: center;
  font-size: 0.9rem;
  color: var(--ink-faint);
}

.together__outro a {
  color: var(--ink-dim);
  text-decoration: none;
  border-bottom: 1px solid var(--wine-bright);
  padding-bottom: 0.15rem;
  transition: color 0.35s var(--ease);
}

.together__outro a:hover,
.together__outro a:focus-visible {
  color: var(--ink);
}

/* ------------------------------------------------------------------ */
/* testimonials.html — a página de submissão. Reutiliza inteiramente o
   material do formulário de booking; só o cabeçalho centrado, o
   indicador de envio e o estado de confirmação são novos. */

/* A barra de navegação é fixa (~4.5rem): sem esta folga o label da
   secção nasceria por baixo dela. As páginas de documento resolvem o
   mesmo com o padding-top do main:not([class]), que aqui não se aplica. */
#testimonial-submit {
  padding-top: clamp(6rem, 14vh, 9rem);
}

#testimonial-submit .section__label {
  justify-content: center;
}

#testimonial-submit .section__label::after {
  content: "";
  width: 2.5rem;
  height: 1px;
  background: var(--wine-bright);
}

#testimonial-submit .section__title {
  max-width: 20ch;
  margin-inline: auto;
  text-align: center;
}

#testimonial-form {
  max-width: 46rem;
  margin: 0 auto;
}

/* O indicador vive na linha do botão: enquanto está a enviar, o botão
   fica trancado (.btn:disabled) e esta linha diz porquê. */
#testimonial-form .form-field--submit {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.25rem;
}

.form-sending {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-faint);
}

/* Confirmação: substitui o formulário. Diz "recebido", não "publicado". */
.submitted {
  max-width: 46rem;
  /* A intro desaparece com o formulário; sem esta folga a confirmação
     encostava ao título. Mesmo intervalo "título → corpo" das secções. */
  margin: clamp(2.5rem, 6vh, 4rem) auto 0;
  border: 1px solid var(--hairline);
  background: var(--bg-raised);
  padding: clamp(1.75rem, 3.5vw, 2.75rem);
}

.submitted:focus {
  outline: none;
}

.submitted__line {
  font-family: var(--font-display);
  font-weight: 200;
  font-style: italic;
  font-size: clamp(1.6rem, 3vw, 2.2rem);
  line-height: 1.15;
}

.submitted__note {
  margin-top: 1.15rem;
  color: var(--ink-dim);
  font-size: 0.98rem;
  line-height: 1.55;
  max-width: 58ch;
}

.submitted__back {
  margin-top: 1.75rem;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
}

.submitted__back a {
  color: var(--wine-bright);
  text-decoration: none;
  transition: color 0.35s var(--ease);
}

.submitted__back a:hover,
.submitted__back a:focus-visible {
  color: var(--ink);
}
