/* =========================================================================
   VARIÁVEIS GLOBAIS
   ========================================================================= */

:root{
  /* tamanho único dos títulos de seção (MY PROJECTS / STACK / CONTACT).
     Antes eram três escalas bem diferentes — 48px, 220px e 200px — e as
     seções não pareciam do mesmo site. */
  --titulo-secao: clamp(46px, 7.5vw, 132px);

  --bg: #0E0F0E;
  --white: #f1ece1;
  --accent: #ffb454;
  --muted: #5b6270;
  --darkgreen: #121412;
  --lightgreen: #d9ff00;
}

/* =========================================================================
   RESET
   ========================================================================= */

*{
  box-sizing: border-box;
}

html, body{
  margin: 0;
  padding: 0;
  width: 100%;
  background: var(--bg);
  scroll-behavior: smooth;
}

/* Trava o vazamento lateral: várias seções usam 100vw / full-bleed e, com a
   barra de rolagem vertical presente, 100vw fica MAIOR que a área visível.
   ATENÇÃO ao trocar `clip` por `hidden` aqui: `overflow: hidden` transforma o
   elemento num CONTAINER DE ROLAGEM, e isso mata o `position: sticky` de todos
   os descendentes — ou seja, o pin do scroll horizontal do "Sobre" para de
   grudar e a seção inteira desanda. `clip` corta igual, mas não cria container
   de rolagem, então o sticky continua funcionando. */
body{
  overflow-x: clip;
}

/* quem pediu menos movimento no sistema não deve levar animação na cara */
@media (prefers-reduced-motion: reduce){
  html, body{ scroll-behavior: auto; }
  *, *::before, *::after{
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* Aplicado nos três títulos de seção: só o tamanho e o ritmo da letra.
   A família e a cor de cada um continuam vindo da sua própria regra. */
.titulo-secao{
  font-size: var(--titulo-secao);
  line-height: 0.95;
  letter-spacing: -0.02em;
}

/* =========================================================================
   HERO
   Coluna vertical: rótulo no topo -> assinatura (canvas) ocupando o miolo
   -> texto digitado embaixo. A assinatura usa o espaço que sobra, então o
   hero nunca passa de 100vh nem gera scroll.
   ========================================================================= */

/* <hero> não é uma tag HTML padrão: o navegador a trata como display:inline,
   e em elemento inline height/overflow são IGNORADOS. Por isso o display:flex
   abaixo é obrigatório — sem ele o hero cresce sem controle e o canvas da
   assinatura colapsa pra 0px de altura. */
.hero{
  display: flex;
  flex-direction: column;
  position: relative;
  width: 100%;
  height: 100vh;
  /* svh = a MENOR altura que a tela pode ter, ou seja, com a barra do
     navegador aparecendo. É a mesma unidade que o .hs-sticky já usava, e pelo
     mesmo motivo: com `vh` puro o celular usa a altura GRANDE (barra
     escondida), então parte do hero — justamente o texto que se digita, que
     fica no rodapé da coluna — nascia abaixo da dobra e só aparecia depois de
     rolar. Pior: a barra some e volta durante a rolagem, e com `vh` o hero
     mudava de altura no meio do gesto.
     No desktop svh e vh são o mesmo número, então lá nada muda. Navegador
     antigo ignora a linha e fica com o vh acima. */
  height: 100svh;
  min-height: 480px;
  overflow: hidden;
  z-index: 1;
}


.hero .fixed{
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.nome{
  color: var(--white);
  font-family: 'JetBrains Mono', monospace;
  font-weight: 900;
  font-size: small;
  margin: 40px 0 0 40px;
  flex-shrink: 0;
}

/* ---------- Assinatura animada (canvas) ---------- */

/* QUANTO DA CAIXA A ASSINATURA OCUPA.

   Este número estava preso no `CONFIG.fillRatio` do js/assinatura.js, um valor
   só para todas as telas. Trazido para cá, ele passa a ser o que já é na
   prática: uma decisão de enquadramento, que muda com o tamanho da tela. O
   js/assinatura.js lê esta variável no `layout()`; sem ela, cai no 0.82 de
   antes e nada muda.

   Por que ele pode subir tanto no celular: a assinatura tem proporção 2,15:1 e
   a caixa do celular é quase quadrada, então quem limita é sempre a LARGURA —
   medido em 375px de tela, o desenho ocupava 82% da largura e só 47% da
   altura. Havia mais da metade da caixa vazia embaixo. Enchendo a largura,
   nada é cortado: a altura desenhada vai de 126px para uns 162px numa caixa de
   268px. */
.assinatura-wrap{
  --assinatura-fill: .82;

  flex: 1 1 auto;
  min-height: 180px;      /* garante caixa mesmo se o flex não resolver */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2vh 6vw 0;
  overflow: hidden;
}

#assinatura{
  display: block;
  width: 100%;
  height: 100%;
  background: transparent; /* o canvas é transparente: o fundo escuro aparece */
}

/* ---------- Texto digitado, abaixo da assinatura ---------- */

.overlay{
  position: static;        /* entra no fluxo da coluna do hero */
  flex-shrink: 0;
  padding: 0 7vw 8vh;
  text-align: left;
  pointer-events: none;
  font-family: 'JetBrains Mono', monospace;
}

/* Tamanhos aumentados junto com a redução da assinatura (fillRatio no
   js/assinatura.js): a assinatura passou a ocupar 82% da caixa dela, e esse
   espaço vai pro texto, que agora divide melhor o peso da hero. */
.typed-line{
  margin: 0;
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: clamp(32px, 5.6vw, 6.6rem);
  color: var(--white);
  line-height: 1.1;
}

.typed-nome{
  display: block;
  font-size: clamp(38px, 6.6vw, 7.8rem);
}

.typed-line .auto-type{
  color: var(--lightgreen);
}

.typed-cursor{
  color: #fff;
  font-weight: 400;
}

@media (max-width: 900px){
  .assinatura-wrap{ padding: 2vh 5vw 0; }
}

@media (max-width: 600px){
  .nome{ margin: 28px 0 0 24px; }
  .assinatura-wrap{ padding: 0 4vw; }
  .overlay{ padding: 0 6vw 6vh; }
}

/* =========================================================================
   TRANSIÇÃO EM ONDA (Hero -> Sobre)
   ========================================================================= */

/* margin-top era 100vh: no layout antigo o <hero> era inline (altura ~0) e essa
   margem é que criava a tela do hero, com o .overlay absoluto por cima dela.
   Agora que o .hero ocupa 100vh de verdade, manter a margem somava uma segunda
   tela vazia logo abaixo do texto. Por isso: 0. */
.conteudo{
  position: relative;
  display: flow-root;
  z-index: 2;
  margin-top: 0;
  background: var(--white);
  min-height: 100vh;
}

.conteudo-inner{
  position: relative;
  padding-top: 50px;
}

/* A altura da faixa e a margem negativa TÊM que ser o mesmo número: a margem
   é o que puxa a seção branca por cima do hero, e a altura é o espaço que a
   onda tem pra existir. Por isso as duas saem de uma variável só.

   O SVG usa preserveAspectRatio="none", então ele é esticado até esta altura.
   Ou seja: este valor não é só o tamanho da faixa, é o quanto a onda é
   esticada na vertical. */
.wave-cap{
  /* O TAMANHO DA ONDA MORA AQUI, E SÓ AQUI.

     Vale registrar, porque não é óbvio e já custou tempo: mexer no
     MAX_AMPLITUDE do js/script.js NÃO aumenta a onda. Lá o TOP_PAD é igual ao
     MAX_AMPLITUDE, então o viewBox cresce no mesmo passo que a crista e a
     proporção fica idêntica — a onda já ocupa 99,2% da altura do viewBox com
     qualquer valor. Como o SVG tem preserveAspectRatio="none", ele é esticado
     até a altura desta faixa, e é ESTA medida que decide o tamanho na tela.

     De 354px para 480px: a crista passa de 351px para 476px de altura real,
     uma onda bem mais alta na transição entre o hero e a seção branca. */
  --altura-onda: 480px;

  position: relative;
  margin-top: calc(var(--altura-onda) * -1);
  height: var(--altura-onda);
  width: 100%;
  z-index: 2;
  pointer-events: none;
  overflow: visible;
}

/* No celular 354px são mais de 40% da tela só de transição, e como o SVG é
   esticado até essa altura a onda ainda ganhava altura de sobra justamente
   onde ela já estava espremida na horizontal. Encolhendo a faixa, a curva
   volta a ter a mansidão que tem no desktop e a transição para de ocupar
   meia tela. */
@media (max-width: 900px){
  /* Cresce junto com o desktop, mas com o freio de sempre: a onda é 1,5 ciclos
     na largura da tela, então num celular ela já é curta na horizontal e altura
     demais a transformaria num bico. Subiu na mesma proporção do desktop
     (cerca de 1,35x) e nada além disso. */
  /* A altura da faixa acompanha a LARGURA (55vw), e isso é o que mantém a
     onda com o mesmo temperamento em qualquer celular. Com altura fixa, a
     mesma curva fica proporcionalmente mais alta quanto mais estreita a
     tela — que é como ela virava um bico em 320px. */
  .wave-cap{ --altura-onda: clamp(180px, 55vw, 380px); }
}

.wave-cap svg{
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;

  /* A ONDA CRESCE POR `scaleY`, E NÃO POR UM CAMINHO NOVO A CADA QUADRO.

     O porquê está no `buildCapWavePath` do js/script.js: a forma da onda é a
     mesma em qualquer progresso, só mais baixa — então achatar na vertical dá
     exatamente o mesmo desenho que remontar o `d`. A diferença é que remontar
     repinta a faixa inteira (375x206px no celular, 25% da tela, por quadro) e
     achatar é uma matriz no compositor.

     A ORIGEM É A LINHA DE BASE, não o fundo da caixa. Abaixo da base existe a
     saia que emenda a faixa com a seção branca, e ela não pode encolher junto
     — quem a cobre é o ::after logo abaixo, que é estático. O `--onda-base` é
     escrito pelo js/script.js a partir das mesmas constantes que montam o
     viewBox, para os dois lados não poderem sair de sincronia. */
  transform-origin: 0 calc(var(--altura-onda) * var(--onda-base, 0.9959));

  /* Aqui o `will-change` se paga: ele troca uma repintura de um quarto da tela
     por quadro por UMA rasterização, feita uma vez por largura de janela. O
     caminho nunca muda depois de montado, então a camada nunca é refeita. */
  will-change: transform;
}

/* E QUEM ESCREVE O scaleY É O NAVEGADOR, NÃO O JAVASCRIPT.

   Mesma história do trilho do scroll lateral, e o mesmo conserto. Deixar o
   `scaleY` para o JavaScript já tinha tirado a repintura do caminho, mas
   sobrava o outro problema: quem escrevia só acordava quando chegava um
   EVENTO de scroll, e no celular esses eventos chegam aos punhados. A onda
   crescia aos degraus — os mesmos "20Hz dentro de uma tela de 120Hz" que
   engasgavam o conteúdo dos painéis.

   Ligada à rolagem em CSS, ela é amostrada pelo compositor: nenhum quadro
   passa por aqui, nem um evento é esperado.

   A CURVA NÃO É ENFEITE. O crescimento original é `progress²`, e progress é
   linear na rolagem — então a animação precisa de uma curva que seja
   exatamente x². `cubic-bezier(1/3, 0, 2/3, 1/3)` é ela: com os pontos de
   controle em x = 1/3 e 2/3 o eixo x fica linear, e os y de 0 e 1/3 fazem a
   saída valer t². Não é aproximação, é a mesma parábola.

   As âncoras são posições de rolagem em px (ver `ondaLayout` no
   js/script.js), pelo mesmo motivo do trilho: a barra de endereço do celular
   não pode mexer no que já está em curso. */
@keyframes ondaCresce{
  from{ transform: scaleY(0); }
  to  { transform: scaleY(1); }
}

@supports (animation-timeline: scroll(root block)){
  .wave-cap svg{
    animation-name: ondaCresce;
    animation-duration: auto;
    animation-timing-function: cubic-bezier(.3333, 0, .6667, .3333);
    animation-fill-mode: both;
    animation-timeline: scroll(root block);
    animation-range: var(--onda-inicio, 0px) var(--onda-fim, 100%);
  }
}

/* A SAIA DA ONDA — a emenda com a seção branca, parada.

   Ela vinha desenhada dentro do próprio caminho (os dois últimos comandos,
   descendo da base até o FLOOR_Y). Com o `scaleY` isso deixou de servir: a
   saia encolheria junto com a crista e abriria uma fresta do hero entre a onda
   e a seção branca justamente quando a onda está baixa.

   Aqui ela é um retângulo estático da mesma cor, ancorado na linha de base e
   passando 8px para dentro da seção seguinte — que também é branca, então a
   sobra não aparece. */
.wave-cap::after{
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--altura-onda) * var(--onda-base, 0.9959));
  bottom: -8px;
  background: var(--white);
  pointer-events: none;
}

.wave-cap path{
  fill: var(--white);
  stroke: var(--white);
  stroke-width: 2px;
}

/* =========================================================================
   SOBRE — SCROLL HORIZONTAL PINADO
   ========================================================================= */

.hs-outer{
  position: relative;
  height: 600vh; /* 5 painéis */
}

.hs-sticky{
  position: sticky;
  top: 0;
  height: 100vh;
  /* svh = altura da tela COM a barra do navegador aparecendo, que é a menor
     que ela pode ter. No celular a barra some e volta conforme se rola, e com
     `vh` o painel preso mudava de altura no meio do movimento. Com svh ele
     fica quieto. Navegador antigo ignora a linha e continua no vh acima. */
  height: 100svh;
  overflow: hidden;
}

/* width era 400vw com 5 painéis de 100vw (= 500vw): os painéis não encolhem
   (flex-shrink: 0), então estouravam a caixa da track. max-content deixa a
   largura sempre igual à soma real dos painéis, sem precisar acertar na mão
   toda vez que um painel entra ou sai.
   A transition foi removida: o script.js já faz a suavização por lerp em
   requestAnimationFrame — as duas juntas somavam atraso e davam aquela
   sensação de "gelatina" atrasada em relação ao scroll. */
.hs-track{
  position: relative;   /* caixa de referência da linha desenhada */
  display: flex;
  height: 100vh;
  height: 100svh;
  width: max-content;
}

/* =========================================================================
   O TRILHO MOVIDO PELO PRÓPRIO NAVEGADOR (celular e tablet)

   ESTE É O CONSERTO DO TRAVAMENTO NO CELULAR, e ele não é uma economia de
   trabalho: é uma mudança de THREAD.

   No celular a rolagem acontece na thread do COMPOSITOR — o dedo move a
   página mesmo com a thread principal ocupada, e é por isso que a página em
   si nunca engasga. O trilho, porém, era movido por JavaScript: evento de
   scroll -> thread principal -> rAF -> escreve o transform. Duas
   consequências, as duas ruins:

     · o transform chega SEMPRE pelo menos um quadro depois da rolagem que o
       causou. O fundo já andou, o trilho ainda não. Isso não é queda de FPS,
       mas é exatamente o que se sente como uma;
     · durante um arremesso (fling) o compositor entrega quadros a 120Hz e a
       thread principal não acompanha. Aí os quadros do trilho passam a sair
       aos pares, aos trios, e vira o travamento de verdade.

   Nenhuma otimização de JavaScript conserta isso, porque o problema não é o
   custo do JavaScript — é o fato de ele estar na outra thread.

   `animation-timeline` resolve na raiz: a animação passa a ser amostrada pelo
   COMPOSITOR, a partir do mesmo deslocamento de rolagem e no mesmo quadro. O
   trilho fica grudado no dedo por construção, e o JavaScript deixa de
   participar do movimento.

   A FAIXA É MEDIDA EM PIXELS DE ROLAGEM, E ISSO NÃO É DETALHE.

   A primeira versão disto usava `view-timeline` com faixa `contain`, que é
   definida em relação à ALTURA DA TELA. No iPhone a barra de endereço encolhe
   e cresce o tempo todo enquanto se rola, e cada vez que ela mexe a tela muda
   de altura — então a faixa `contain` mudava de tamanho no meio do gesto e o
   trilho pulava para outra posição. Era o "teletransporte".

   `scroll(root block)` com a faixa em PIXELS resolve na raiz: `--hs-inicio` e
   `--hs-parada` são posições de rolagem em coordenadas do documento, escritas
   pelo js/script.js a cada layout. Elas não dependem da altura da tela, então
   a barra do navegador pode ir e voltar à vontade — a mesma posição de
   rolagem dá sempre o mesmo ponto do percurso.

   As duas âncoras acima do trilho também são estáveis de propósito: o hero é
   `100svh`, e `svh` é a MENOR altura possível da tela, um número que não muda
   quando a barra recolhe. Se ele fosse `dvh`, o topo da seção andaria e o
   pulo voltaria por outro caminho.

   POR QUE SÓ NO TOQUE
   No desktop o trilho é amortecido por lerp (ver HS_SUAVIDADE no
   js/script.js), e esse amortecimento existe para absorver o pulo de ~100px
   de cada clique da roda do mouse. Uma animação ligada à rolagem é exata: ela
   devolveria o pulo. No toque não há pulo nenhum para absorver — o dedo já
   entrega movimento contínuo e o navegador ainda dá a inércia por cima —,
   então lá o exato é justamente o que se quer.

   Sem suporte, nada disto existe e o caminho de JavaScript continua sendo o
   único. É adição, não substituição.
   ========================================================================= */

@keyframes hsCorre{
  from{ transform: translate3d(0, 0, 0); }
  to  { transform: translate3d(calc(var(--hs-percurso, 0px) * -1), 0, 0); }
}

@supports (animation-timeline: view()){
  @media (hover: none), (pointer: coarse){
    /* As longhands são de propósito: o atalho `animation` REINICIA
       `animation-timeline` e `animation-range` para o valor inicial, e as duas
       linhas abaixo seriam apagadas por ele dependendo da ordem. */
    .hs-track{
      animation-name: hsCorre;
      animation-duration: auto;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: scroll(root block);
      animation-range: var(--hs-inicio, 0px) var(--hs-parada, 100%);
    }
  }
}


/* O `will-change` NÃO VALE A PÁGINA INTEIRA.

   O trilho tem 5 telas de largura — 6400x720 num monitor de 1280, o dobro
   disso em cada eixo num aparelho com DPR 2. `will-change: transform` manda o
   navegador manter isso promovido a camada de GPU e rasterizado, e ele
   obedece desde o primeiro quadro da visita até o último: memória de vídeo
   parada por causa de uma seção que ocupa um quinto da página.

   A classe é posta e tirada por um IntersectionObserver com margem de uma tela
   inteira (ver `hsPromoverTrilho`, no js/script.js), então a camada já existe
   bem antes de a seção aparecer e some bem depois de ela sair. A troca
   acontece longe da vista, que é o único cuidado que ela pede. */
.hs-outer.hs-perto .hs-track{
  will-change: transform;
}

/* =========================================================================
   LINHA DESENHADA PELO SCROLL

   Um traço só, atravessando os cinco painéis de ponta a ponta, que vai sendo
   desenhado conforme eles passam. O SVG é montado inteiro pelo js/script.js e
   entra como primeiro filho do .hs-track; o caminho, o gesto de cada painel e
   o porquê de cada decisão estão no cabeçalho de lá.

   O QUE ESTAVA ERRADO AQUI — e não era a cor:

   O SVG é `position: absolute`, e elemento posicionado pinta DEPOIS do
   conteúdo em fluxo dos irmãos — mesmo com z-index menor, mesmo vindo antes
   no DOM. Só dois dos cinco painéis tinham o `.texto` posicionado, então a
   linha passava atrás da palavra nesses dois e POR CIMA das letras nos outros
   três: um traço largo cortando "DEVELOPER", "I ANIMATE." e "SHOW YOU".
   A correção está no `.hs-panel` aqui embaixo: o painel inteiro sobe pro
   z-index 1 e a linha fica no 0. Assim qualquer coisa que entre num painel já
   nasce na frente dela, sem precisar lembrar de posicionar nada.

   SOBRE A COR, que é `--lightgreen` (#d9ff00) sobre o `--white` (#f1ece1)
   desta seção: são 1,02:1 de contraste. Isso é DE PROPÓSITO e só funciona
   agora que o empilhamento está certo — o traço não é para ser lido como
   linha, é uma faixa tonal larga passando por trás das palavras, que aparece
   pelo movimento (ela se desenha) e pela diferença de matiz, não pela
   luminância. Por isso ela também é larga: em 6px seria só sujeira; em 30px é
   uma massa de cor. Quem depende de contraste não perde informação nenhuma —
   o SVG é `aria-hidden` e não carrega conteúdo.
   Se um dia isso tiver que virar linha legível de fato, é `--linha-cor` logo
   abaixo, e aí a espessura precisa cair junto (LINHA.espessuraDivisor, no
   js/script.js).

   Custo: desenhar é animar `stroke-dashoffset`, que repinta o traço. Por isso
   aqui não há filtro, sombra nem gradiente — só o traço, que é a coisa mais
   barata de rasterizar.
   ========================================================================= */

.hs-linha{
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;

  /* os dois knobs do traço. O `--linha-traco` é reescrito pelo script a cada
     layout (largura da tela / 34, entre 10 e 30px); o valor aqui é só o que
     vale antes da primeira medida. */
  --linha-cor: var(--lightgreen);
  --linha-traco: 3px;

  /* A opacidade mora no <svg>, não no path: o traço é fatiado em vários
     <path> e as pontas arredondadas se sobrepõem nas emendas — com alfa por
     path o valor comporia duas vezes ali, um ponto mais escuro em cada junta.
     No <svg> as fatias compõem entre si em alfa cheio e a opacidade vale pro
     conjunto. */
  opacity: .85;
}

/* Os quadros que as fatias usam quando estão ligadas à rolagem (ver
   `hsLinhaLigarNoScroll` no js/script.js). Os dois valores vêm de variáveis
   escritas por lá, uma vez por layout: `--dash` é a fatia inteira escondida e
   `--feito` é ela inteira desenhada. Quem escolhe QUANDO cada uma acontece é a
   `animation-range` de cada `<path>`. */
@keyframes hsLinhaDesenha{
  from{ stroke-dashoffset: var(--dash); }
  to  { stroke-dashoffset: var(--feito); }
}

/* As fatias nascem paradas e só correm quando a seção chega à tela. Uma
   classe, nenhum quadro de JavaScript.

   O par mora aqui, e não no script, porque estilo inline ganha de qualquer
   seletor: um `paused` escrito pelo JavaScript em cada fatia não teria como ser
   destravado por uma classe depois. */
.hs-linha path{
  animation-play-state: paused;
}

.hs-outer.hs-desenhando .hs-linha path{
  animation-play-state: running;
}

.hs-linha path{
  fill: none;
  stroke: var(--linha-cor);
  stroke-width: var(--linha-traco);
  stroke-linecap: round;
  stroke-linejoin: round;
}


.hs-panel{
  flex: 0 0 100vw;
  height: 100vh;
  display: grid;
  place-items: center start;
  margin: 0;
  font-family: 'JetBrains Mono', monospace;
  overflow: hidden;

  /* O PAINEL INTEIRO NA FRENTE DA LINHA.
     Ver o bloco "LINHA DESENHADA PELO SCROLL" acima: o SVG é posicionado e,
     por isso, pintava por cima do conteúdo em fluxo dos painéis mesmo estando
     no z-index 0. Antes só o `.texto` do "I DESIGN." e o do "I DEVELOP."
     escapavam, porque só esses dois eram posicionados — nos outros três a
     linha atravessava as letras por cima.
     Resolver painel por painel dava três regras pra lembrar toda vez que um
     painel novo entrasse. Aqui é uma: o painel sobe pro 1, a linha fica no 0,
     e o que estiver dentro do painel já nasce na frente dela. Os z-index de
     dentro (o .texto no 1, o jardim no 2) continuam valendo entre si — só
     passam a ser contados dentro do painel. */
  position: relative;
  z-index: 1;
}

/* AS EXCEÇÕES: painéis que ficam ATRÁS da linha.

   O padrão é o traço costurar o conteúdo por trás. Nestes dois o gesto é o
   contrário, e por motivos diferentes:

     · "I DEVELOP." — a linha passa por cima da palavra enquanto ela se monta.
       O painel já é sobre coisa sendo construída em cima de coisa; o traço
       cruzando na frente entra como mais uma camada da montagem.
     · "Now, LET ME SHOW YOU" — o fecho. A linha chega na frente da frase,
       como quem sublinha o que acabou de dizer e sai de cena por cima.

   Como funciona: o painel desce pro mesmo z-index da linha (0), e aí quem
   pinta por cima é quem vem depois no DOM. O js/script.js monta o SVG como
   ÚLTIMO filho do .hs-track exatamente por isso. Os outros três painéis
   continuam no 1 e seguem por cima do traço.

   Um detalhe que faz isso ser barato: z-index 0 num elemento posicionado ainda
   cria contexto de empilhamento, então o `.texto` (z-index 1) e o jardim
   (z-index 2) continuam valendo ENTRE SI, dentro do painel. O painel inteiro
   passa por baixo da linha como um bloco só — nada aqui precisa saber o que
   tem dentro dele.

   `.fixado` já existia no HTML do último painel e não era usado por regra
   alguma; agora é o gancho. */
.hs-panel.painel-develop,
.hs-panel.fixado{
  z-index: 0;
}

.hs-panel--mid .texto{
  margin-left: 200px;
}

/* Aqui existia um "long shadow" gigante (70 linhas de text-shadow indo até
   3.15em) que nunca chegou a ser usado, porque a classe .panel-design não
   existia no HTML. Ao ligar a classe, a sombra virou um bloco escuro sólido
   de quase 1000px em cima do "I DESIGN." — no font-size daquele painel
   (clamp até 30rem) o efeito não escala. Removido.
   Pra ter um long shadow legível ali, ele precisa ser curto (uns 8 passos,
   até ~0.4em) e mais claro que o texto, não da mesma cor. */

/* OS TRÊS TAMANHOS SAEM DE UM SÓ.

   Antes cada um tinha o seu clamp, e todos com piso de 24px. No desktop isso
   dava 228 / 85,5 / 28,5 — a hierarquia que o painel precisa. No celular os
   três batiam no piso ao mesmo tempo: o h2 e o h3 ficavam AMBOS em 24px, ou
   seja, "More" e "THAN A" do mesmo tamanho, e o "I" quase do tamanho da
   palavra embaixo. A composição desmontava.

   Agora o corpo do h1 manda, e os outros dois são fração dele. As frações
   (0,375 e 0,125) são exatamente as que o desktop já tinha, então lá nada
   muda. O `max` é só piso de legibilidade: no celular a fração pura daria
   6px no h3. */
.texto{
  --corpo: clamp(24px, 16vw, 30rem);

  margin: 0 0 0 50px;
  color: var(--bg);
}

.texto h3{
  font-size: max(12px, calc(var(--corpo) * 0.125));
  margin: 0;
  line-height: 0.8;
}

.texto h2{
  font-size: max(19px, calc(var(--corpo) * 0.375));
  margin: 0;
  line-height: 0.8;
  font-weight: 900;
}

.texto h1{
  font-size: var(--corpo);
  margin: 0;
  line-height: 0.8;
  font-weight: 900;
}


/* =========================================================================
   PAINEL "I DESIGN." — jardim 3D que se faz junto com o scroll

   COMO A FLORAÇÃO É AMARRADA AO SCROLL
   O js/script.js escreve `--prog` no painel: 0 quando ele ainda está entrando
   pela direita, 1 quando terminou de atravessar. O CSS usa esse número como
   AGULHA das animações, através de um `animation-delay` negativo.

   O truque: uma animação pausada e com delay negativo fica congelada no ponto
   exato do delay. Com `animation-delay: calc((var(--ini) - var(--prog)) * 1s)`
   e `animation-play-state: paused`:
     · --prog menor que --ini  -> delay positivo: ainda não começou, e o
       `fill: both` segura o primeiro quadro;
     · --prog == --ini         -> delay 0: começo exato;
     · --prog == --ini + dur   -> delay -dur: último quadro;
     · --prog maior            -> o `fill: both` segura o último quadro.
   Ou seja: 1 unidade de --prog equivale a 1 segundo de linha do tempo, e
   rolar pra frente e pra trás desenha e desfaz a flor. Não depende de
   animation-timeline, que ainda não existe em todo navegador.

   As flores não têm mais balanço em loop: quem dá o movimento agora é o
   scroll, e assim o painel não gasta nada parado.
   ========================================================================= */

/* =====================================================================
   REGRAS-BASE DOS DOIS PAINÉIS — NÃO APAGAR
   Já se perderam QUATRO vezes em reescritas do bloco das flores, e cada
   uma quebra algo visível:

   · position: relative
       O .jardim usa `inset: 0`. Sem uma âncora de posicionamento aqui, ele
       se prende ao .hs-outer e passa a medir a TRACK INTEIRA (6400px): uma
       flor em `left: 73.2%` vai parar em 4684px, muito além do painel, e
       cresce fora da tela sem ninguém ver.
   · .design-alvo em inline-flex
       Cada letra é um <span> em sua própria linha no HTML, e as quebras
       viram espaço de verdade: a palavra sai "D E S I G N ." esparramada.
   · .d-letra
       A cor das letras.

   Ficam aqui em cima, separadas do bloco do jardim, justamente pra não
   serem levadas junto na próxima reescrita.
   ===================================================================== */

.painel-design,
.painel-develop{
  position: relative;
}

.design-alvo{
  display: inline-flex;
  align-items: baseline;
}

.d-letra{
  display: inline-block;
  color: var(--bg);
}

.jardim{
  position: absolute;
  inset: 0;
  /* POR CIMA do .texto (z-index 1): as flores encostam e passam por cima de
     algumas letras, como adesivos colados na palavra. Antes ficavam atrás e o
     encontro entre flor e letra não aparecia. */
  z-index: 2;
  pointer-events: none;
  /* Sem perspectiva nem inclinação: as flores ficam de frente. A ideia de
     profundidade vem só da diferença de tamanho entre elas e da sombra. */
}

.flor{
  position: absolute;
  left: var(--e);
  top: var(--t);
  /* --escala existe pro jardim inteiro poder encolher no mobile sem precisar
     reescrever o tamanho de cada flor no HTML */
  width: calc(var(--tam) * var(--escala, 1));
  height: calc(var(--tam) * var(--escala, 1));
  margin: calc(var(--tam) * var(--escala, 1) / -2);
  transform: scale(0);
  opacity: 0;

}

.flor svg{
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* ---------- a corola ---------- */

/* As pétalas e o miolo NÃO se animam mais. A flor inteira é uma peça só, e o
   único movimento é ela crescer — que foi o pedido: começar pequena e
   aumentar bastante conforme se rola. Sem pétala abrindo uma a uma, sem
   miolo dando pop: só escala. */

.petala,
.miolo{
  transform-box: view-box;
  transform-origin: 50% 50%;
}

.petala{
  stroke-linejoin: round;
}

/* Nada de anel concêntrico, brilho ou degradê no miolo: um círculo de cor
   chapada e pronto. Era esse tipo de detalhe — junto com a sombra projetada
   e o contorno em tom mais escuro — que dava à flor cara de ilustração
   renderizada em vez de desenho. */

/* ---------- cores ----------
   Tudo CHAPADO: uma cor por pétala, uma cor por miolo, sem degradê e sem
   tom mais escuro fingindo sombra.

   O contorno é da cor do FUNDO do painel, não de uma versão escura da
   pétala. A diferença é justamente a que separa desenho de ilustração
   renderizada: um contorno escuro lê como sombreamento entre as pétalas;
   um contorno da cor do fundo lê como recorte, como se cada pétala fosse
   uma peça de papel colada sobre a outra. */

.petala{
  stroke: var(--white);
  stroke-width: 2.5;
}

.miolo{ fill: #ffd23f; }

.flor--rosa     .petala{ fill: #ff6fae; }
.flor--lilas    .petala{ fill: #520440; }
.flor--vermelha .petala{ fill: #e83b3b; }

/* a de pétala amarela precisa de miolo marrom: amarelo com amarelo sumiria */
.flor--amarela  .petala{ fill: var(--lightgreen); }
.flor--amarela  .miolo { fill: #6b4423; }

/* ---------- o crescimento, com o scroll na agulha ---------- */

/* Uma animação PAUSADA com delay negativo fica congelada no ponto exato do
   delay. Com `calc((var(--ini) - var(--prog)) * 1s)`, 1 unidade de --prog
   vira 1 segundo de linha do tempo:
     --prog menor que --ini     -> delay positivo: ainda não começou, e o
                                   `fill: both` segura o primeiro quadro;
     --prog == --ini            -> começo;
     --prog == --ini + duração  -> último quadro;
     --prog maior               -> o `fill: both` segura o último quadro.
   Rolar pra frente faz crescer, rolar pra trás desfaz. Não depende de
   animation-timeline, que ainda não existe em todo navegador. */
.flor{
  animation-name: florCresce;
  /* cada flor tem a sua duração (--dur, no HTML): as que aparecem mais tarde
     precisam de uma florada mais curta pra ainda fecharem antes de o painel
     terminar de entrar */
  animation-duration: var(--dur, .24s);
  animation-timing-function: linear;
  animation-delay: 0s;   /* o ponteiro é o currentTime, escrito pelo js/script.js */
  animation-play-state: paused;
  animation-fill-mode: both;
  animation-iteration-count: 1;
}

/* de bem pequena a inteira: quase 9 vezes de crescimento */
@keyframes florCresce{
  from{ opacity: 0;   transform: scale(.11); }
  12% { opacity: 1; }
  to  { opacity: 1;   transform: scale(1); }
}

/* ---------- a flutuação, por cima do crescimento ----------
   A flor boia de leve, com um bambeio no meio do caminho.

   Mora no <svg> DE DENTRO, não na .flor. O motivo: a .flor já tem a animação
   do crescimento em `animation-play-state: paused`, e o play-state vale pra
   TODAS as animações de um elemento — uma flutuação declarada ali nasceria
   congelada junto. No filho ela roda livre, e os dois efeitos se compõem:
   o div cresce, o svg boia.

   Só anda com o painel na tela (.painel-vivo), então parado não custa nada. */
.flor svg{
  transform-origin: 50% 60%;    /* pivô mais embaixo: balança como se tivesse
                                   um caule invisível prendendo */
}

.painel-design.painel-vivo .flor svg{
  animation: florBoia var(--boia, 7s) ease-in-out infinite;
  animation-delay: var(--boia-atraso, 0s);
}

/* tempos diferentes por flor: em uníssono pareceria um carrossel */
.painel-design .flor:nth-child(1) svg{ --boia: 6.4s;  --boia-atraso: -0.0s; }
.painel-design .flor:nth-child(2) svg{ --boia: 8.1s;  --boia-atraso: -2.1s; }
.painel-design .flor:nth-child(3) svg{ --boia: 7.2s;  --boia-atraso: -3.4s; }
.painel-design .flor:nth-child(4) svg{ --boia: 9.0s;  --boia-atraso: -1.2s; }

@keyframes florBoia{
  0%, 100%{ transform: translateY(0)     rotate(0deg); }
  25%     { transform: translateY(-3.5%) rotate(2.2deg); }
  50%     { transform: translateY(-1%)   rotate(0deg); }
  75%     { transform: translateY(-4.5%) rotate(-2.2deg); }
}

@media (prefers-reduced-motion: reduce){
  .painel-design.painel-vivo .flor svg{ animation: none; }
}

/* a palavra fica abaixo do jardim: as flores é que passam por cima */
.painel-design .texto{
  position: relative;
  z-index: 1;
}


/* =========================================================================
   PAINEL "I DEVELOP." — montagem
   As letras chegam tombadas pra trás e giram até assentar, uma a uma, como
   peça que encaixa. A régua de build enche no mesmo compasso e fecha junto
   com a última letra. Nada mais em volta.
   ========================================================================= */

.painel-develop .texto{
  position: relative;
  z-index: 1;
}

.dev-alvo{
  /* inline-FLEX: cada letra é um <span> em sua própria linha no HTML, e as
     quebras viram espaço de verdade. Em container flex esse espaço some. */
  display: inline-flex;
  align-items: baseline;

  /* EM, e não px. `perspective` fixa em 700px com uma palavra que vai de 62px
     (celular) a 410px (ultrawide) fazia a razão entre as duas ir de 11,2 a 1,7:
     no celular a rotação virava quase ortográfica e o gesto de "peça sendo
     assentada" sumia. O 3,04 é 700 ÷ 230,4px, o corpo num monitor de 1440 —
     então o desktop continua com os mesmos 700px e o resto ganha a proporção
     para a qual o efeito foi calibrado. */
  perspective: 3.04em;
}

.dv-letra{
  display: inline-block;
  transform-origin: 50% 100%;   /* gira apoiada na base, como peça assentando */
  backface-visibility: hidden;
}

/* AS LETRAS SE MONTAM COM O SCROLL, não quando o observer resolve disparar.
   Antes o repouso era "letra visível" e a animação só entrava com o painel a
   55% na tela — a essa altura a palavra já estava lida, e o efeito acontecia
   depois, do nada. Agora o primeiro quadro da animação (invisível, tombada)
   é o próprio repouso, e o --prog é que empurra cada letra até o lugar.

   A classe `dev-scrub` é posta pelo js/script.js. É ela que autoriza esconder:
   sem o script, nenhuma animação entra e a palavra fica legível do jeito
   normal, em vez de sumir esperando um --prog que não viria. */
/* Os dois números da montagem moram AQUI, e o js/script.js os lê daqui.
   Assim continua se calibrando o efeito no CSS, sem número repetido nos dois
   lados para sair de sincronia. Ver `coletarCena` no js/script.js.

   .40  = ponto do percurso em que a primeira letra começa (54% do painel na tela)
   .04  = intervalo entre uma letra e a seguinte
   As oito fecham em .40 + 7 x .04 + .12 = .80, junto com a régua de build. */
.painel-develop.dev-scrub{
  --dev-inicio: .40;
  --dev-passo: .04;
}

.painel-develop.dev-scrub .dv-letra{
  animation-name: dvEncaixa;
  animation-duration: .12s;
  animation-timing-function: cubic-bezier(.34, 1.4, .64, 1);
  animation-fill-mode: both;
  animation-play-state: paused;

  /* QUANDO A MONTAGEM ACONTECE.

   A régua: a fração do painel que já entrou é 1,35 x prog, então prog .16 é
   21% do painel visível, .40 é 54% e .74 é o painel cheio.

   Começava em .16 e fechava em .63 — as letras se montavam com um quinto do
   painel na tela e terminavam ANTES de ele chegar, então quem rolava até aqui
   encontrava a palavra pronta. Agora começa em .40 e a última letra assenta em
   .80, quando o painel acaba de encher a tela.

   O intervalo caiu de .05 para .04 porque a janela ficou mais curta e as oito
   precisam caber: .40 + 7 x .04 + .12 = .80. */
  animation-delay: 0s;
}

@keyframes dvEncaixa{
  from{ opacity: 0; transform: rotateX(-92deg) translateY(.18em); }
  60% { opacity: 1; }
  to  { opacity: 1; transform: rotateX(0deg) translateY(0); }
}

/* ---------- régua de build ---------- */

/* A régua fica fora do fluxo, e é isso que alinha os painéis.

   O .hs-panel usa `place-items: center`, que centra o .texto inteiro. Como só
   este painel tem a régua dentro dele, o bloco era mais alto que o do "I
   DESIGN." e o do "I ANIMATE." — e centrar uma caixa mais alta empurra o
   conteúdo para cima, exatamente metade da altura da régua (15px no celular,
   21px no desktop).

   Tirando a régua do fluxo o .texto passa a medir só o "I" mais a palavra. O
   `position: relative` que isto exige já existe em `.painel-develop .texto`. */
.build{
  position: absolute;
  top: 100%;
  left: 0;

  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 26px;
  font-family: 'JetBrains Mono', monospace;
}

.build-rotulo{
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: #6d7a3a;
}

.build-barra{
  position: relative;
  width: min(300px, 34vw);
  height: 4px;
  background: rgba(14, 15, 14, 0.14);
  overflow: hidden;
}

.build-barra i{
  position: absolute;
  inset: 0;
  background: var(--lightgreen);
  transform: scaleX(0);
  transform-origin: left center;
}

/* A régua enche no MESMO percurso das letras: começa junto com a primeira
   (.40) e fecha junto com a última (.80) — daí a duração de .40. É isso que
   faz parecer que uma coisa está construindo a outra, e por isso estes dois
   números TÊM que ser mexidos junto com os do .dv-letra acima. Se saírem de
   sincronia, a barra termina antes ou depois da palavra e o efeito perde o
   sentido. */
.painel-develop.dev-scrub .build-barra i{
  animation-name: buildEnche;
  animation-duration: .40s;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-play-state: paused;
  animation-delay: 0s;
}

@keyframes buildEnche{
  to{ transform: scaleX(1); }
}

/* Sem o script (nenhuma classe dev-scrub), a régua ficaria parada em zero e
   pareceria um carregamento travado. Cheia é o estado honesto: o projeto
   está pronto. */
.painel-develop:not(.dev-scrub) .build-barra i{
  transform: scaleX(1);
}

/* ---------- telas pequenas ---------- */

/* No painel de 375px as flores em tamanho cheio se atropelavam e a das
   pontas vazava pela borda, ficando cortada pelo overflow do painel.
   Metade do tamanho resolve as duas coisas de uma vez. */
@media (max-width: 900px){
  /* Duas coisas juntas: as flores encolhem E o espaço em que elas são
     posicionadas se estreita. Só encolher não bastava, porque a rotação 3D
     alarga a caixa projetada — as das pontas continuavam raspando a borda.
     Com o inset lateral, as porcentagens de posição passam a se resolver
     dentro de uma faixa menor, e todas se afastam das bordas de uma vez. */
  .jardim{
    --escala: .40;
    inset: 0 11%;
  }

  .build{ margin-top: 18px; gap: 10px; }
  .build-rotulo{ font-size: 10px; }
}

/* ---------- quem pediu menos movimento não leva nada disso ---------- */

@media (prefers-reduced-motion: reduce){
  .jardim{ display: none; }
  .build-barra i{ transform: scaleX(1); }
}

/* O celular tem o mesmo pin do desktop. Aqui existia um fallback que trocava o
   pin por rolagem horizontal nativa: funcionava, mas nada na tela avisava que
   era preciso arrastar de lado, então quem descia a página passava reto pela
   seção. Sobrou desta media query só o corpo das fontes. */
@media (max-width: 768px){
  /* os painéis usam fontes de até 16vw: no celular "DEVELOPER" estourava a
     largura da tela e ficava cortado pela metade */
  .hs-panel--mid .texto{ margin-left: 40px; }
  .texto{ margin-left: 24px; }
}

/* =========================================================================
   OLHO NO "O" DE SHOW
   ========================================================================= */

.eye-o{
  display: inline-block;
  width: 0.74em;
  height: 0.74em;
  vertical-align: -0.06em;
  overflow: visible;
}

.eye-svg{
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.eye-ring{ fill: var(--bg); }
.eye-white{ fill: var(--white); }
.eye-iris{ fill: var(--lightgreen); }
.eye-pupil{ fill: var(--bg); }
.eye-glint{ fill: var(--white); }

.eye-lid{
  fill: var(--bg);
  transform-box: fill-box;
}

.eye-lid-top{
  transform-origin: top;
  transform: scaleY(0);
}

.eye-lid-bottom{
  transform-origin: bottom;
  transform: scaleY(0);
}

/* sobrancelhas: 3 traços curtos, grossos e curvados, coladinhos no topo do olho */
.eye-brow{
  stroke: var(--bg);
  stroke-width: 7;
  stroke-linecap: round;
  fill: none;
}

/* o piscar (fechar/abrir da pálpebra) é feito via JS, usando a Web Animations
   API (script.js, função initShowEye) — mais confiável em elementos SVG do que
   reiniciar uma animação CSS por classe. Duração e intervalo são ajustados lá. */

@media (max-width: 768px){
  .show-word:hover .eye-lid-top,
  .show-word:hover .eye-lid-bottom{
    animation: none;
  }
}

/* =========================================================================
   PROJETOS
   ========================================================================= */

/* o margin-top negativo existe pra ESCONDER a tela de pausa do pin — os
   projetos sobem por cima dela. No mobile não existe pin nenhum (o CSS troca
   por scroll horizontal nativo), então essa mesma margem subia os projetos
   por cima do "MORE THAN A DEVELOPER" e engolia a seção inteira. */
/* A seção tem 100vh e o índice ocupa pouca altura, então sobrava um vazio
   grande embaixo e o conteúdo ficava grudado no topo. Centralizando na
   vertical, o bloco todo desce e ganha ar dos dois lados. */
.projetos{
  display: flex;
  flex-direction: column;
  justify-content: center;

  padding-top: 60px;
  padding-bottom: 100px;
  position: relative;
  min-height: 100svh;
  width: 100%;
  background: var(--bg);
  z-index: 100;
  box-shadow: 0 -20px 50px rgba(0, 0, 0, 0.4);
}

/* O margin negativo é o que faz os projetos SUBIREM POR CIMA do último painel
   enquanto ele fica preso atrás — é o efeito de uma seção cobrindo a outra.

   Isto vivia atrás de um `min-width: 769px` porque no celular não havia pin
   nenhum: sem a seção presa, a mesma margem subia os projetos por cima do
   "MORE THAN A DEVELOPER" e engolia a seção inteira. Agora que o pin vale em
   qualquer largura, a trava saiu e o celular ganha o mesmo encavalamento. */
.projetos{
  margin-top: -100svh;

  /* A seção é obrigada a ter uma tela de altura — é ela que cobre a tela de
     pausa do pin (o margin negativo acima). Só que o índice ocupa bem menos
     que isso, e centralizar no meio dessa altura jogava o título pra 222px do
     topo, baixo demais. O respiro de baixo maior que o de cima puxa a
     centralização para cima e devolve o título a uma altura de título.

     OS 36vh DE ANTES ERAM CALIBRADOS PARA QUATRO LINHAS. Com três, o índice
     encolheu uma linha inteira e essa folga foi toda parar embaixo: numa tela
     de 720px sobravam 259px de nada entre a última linha e o fim da seção —
     mais que duas linhas da própria lista. 16vh mantém a assimetria que
     levanta o bloco sem transformar o rodapé num vazio. */
  padding-top: 40px;
  padding-bottom: clamp(96px, 16vh, 190px);
}

.portfolio-gallery{
  position: relative;
  padding: 60px 24px 0;
  margin: 0;
}

.pg-header{
  text-align: center;
  margin-bottom: 0;
}

.pg-title{
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  color: var(--white);
  margin: 0;
}

/* ---------- Desktop: pilha 3D ancorada no rodapé da seção ---------- */

/* ---------- Índice de projetos ----------
   Uma linha por trabalho, no lugar do leque de cartas 3D. A régua de cima e
   de baixo de cada linha dá o ar de índice de revista; o número em
   monoespaçada e o nome grande fazem o contraste.

   A arte entra POR DENTRO da linha, num espaço que já estava reservado: a
   altura da linha é fixa e o recorte só abre. Assim, percorrer a lista não
   empurra nada — nenhum salto de layout enquanto o mouse desce. */

.pj-lista{
  list-style: none;
  /* Respiro entre o título e a primeira linha.

     O piso de 48px era o que valia na prática em telas de até ~740px de
     altura (6.5vh só passa dos 48px acima disso), e 48px logo abaixo de um
     título desse corpo lê como linha encostada — ainda mais com o rodapé da
     seção sobrando. O piso agora acompanha o tamanho do título em vez de
     ficar preso num número que só servia às telas altas. */
  margin: clamp(64px, 8vh, 104px) auto 0;
  padding: 0;
  max-width: 1200px;
  width: 100%;
}

.pj-item{
  border-top: 1px solid rgba(241, 236, 225, 0.16);
}

.pj-item:last-child{
  border-bottom: 1px solid rgba(241, 236, 225, 0.16);
}

.pj-link{
  position: relative;
  display: grid;
  /* número | nome (elástico) | tipo | seta */
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: clamp(16px, 3vw, 48px);
  height: clamp(96px, 11.5vh, 138px);
  padding: 0 clamp(16px, 2.5vw, 34px);
  text-decoration: none;
  overflow: hidden;          /* é o que recorta a arte enquanto ela entra */
  transition: background-color .45s cubic-bezier(.22, 1, .36, 1);
}

.pj-link:hover{
  background-color: rgba(217, 255, 0, 0.06);
}

.pj-num{
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .1em;
  color: var(--lightgreen);
  transition: transform .45s cubic-bezier(.22, 1, .36, 1);
}

.pj-nome{
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: clamp(20px, 2.8vw, 40px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--white);
  white-space: nowrap;
  transition: transform .45s cubic-bezier(.22, 1, .36, 1),
              color     .35s ease;
}

.pj-tipo{
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  transition: color .35s ease;
}

/* a seta é desenhada com borda, não com fonte: assim ela não depende do
   Font Awesome ter carregado */
.pj-seta{
  width: 13px;
  height: 13px;
  border-top: 2px solid var(--muted);
  border-right: 2px solid var(--muted);
  transform: rotate(45deg);
  transition: transform .45s cubic-bezier(.22, 1, .36, 1),
              border-color .35s ease;
}

/* ---------- o que muda no hover ---------- */

.pj-link:hover .pj-num  { transform: translateX(6px); }
.pj-link:hover .pj-nome { transform: translateX(18px); color: var(--lightgreen); }
.pj-link:hover .pj-tipo { color: var(--white); }

.pj-link:hover .pj-seta{
  transform: rotate(45deg) translate(4px, -4px);
  border-color: var(--lightgreen);
}

/* ---------- a arte ---------- */

.pj-arte{
  position: absolute;
  top: 50%;
  right: clamp(58px, 7vw, 96px);   /* deixa a seta livre à direita */
  translate: 0 -50%;
  /* Ocupa a ALTURA INTEIRA da linha, encostando em cima e embaixo — é o
     recorte que dá o ar de faixa. Sem aspect-ratio de propósito: a altura
     vem da linha e o object-fit: cover cuida do enquadramento da imagem.
     Sem canto arredondado, já que ela encosta nas bordas. */
  height: 100%;
  width: clamp(150px, 20vw, 260px);
  overflow: hidden;
  pointer-events: none;

  /* fechada pela direita; no hover o recorte abre e ela desliza pra posição */
  clip-path: inset(0 0 0 100%);
  transform: translateX(26px);
  transition: clip-path .55s cubic-bezier(.22, 1, .36, 1),
              transform .55s cubic-bezier(.22, 1, .36, 1);
}

.pj-link:hover .pj-arte{
  clip-path: inset(0 0 0 0);
  transform: translateX(0);
}

.pj-arte img{
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left top;
  display: block;
}

/* ---------- toque e telas estreitas ----------
   Sem mouse não existe hover, então a arte ficaria escondida pra sempre.
   Aqui a linha vira bloco: nome em cima, arte sempre visível embaixo. */

@media (hover: none), (max-width: 860px){
  .pj-link{
    grid-template-columns: auto 1fr;
    grid-template-areas:
      "num  tipo"
      "nome nome"
      "arte arte";
    height: auto;
    gap: 10px clamp(12px, 3vw, 24px);
    padding: 22px clamp(14px, 3vw, 24px) 26px;
    overflow: visible;
  }

  .pj-num { grid-area: num; }
  .pj-tipo{ grid-area: tipo; justify-self: end; }
  .pj-nome{ grid-area: nome; white-space: normal; }

  .pj-seta{ display: none; }

  .pj-arte{
    grid-area: arte;
    position: static;
    translate: none;
    transform: none;
    clip-path: none;
    width: 100%;
    /* No desktop a altura vem da linha (height: 100%). Aqui a linha não tem
       altura fixa, então sem uma proporção a caixa colapsava pra 0 — a
       imagem sumia, porque o <img> dentro dela pede height: 100% de um pai
       de altura zero. */
    height: auto;
    aspect-ratio: 16 / 9;
    margin-top: 6px;
  }
}

/* =========================================================================
   STACK
   ========================================================================= */

/* height: 100vh + overflow: hidden cortava a última linha do marquee em telas
   baixas (num 1280x720 sobravam ~173px de conteúdo escondido). min-height
   mantém a seção com uma tela cheia quando sobra espaço, mas deixa crescer
   quando o conteúdo pede. O overflow agora corta só na horizontal, que é o
   que o marquee full-bleed realmente precisa. */
.stack-section{
  position: relative;
  background: var(--white);
  width: 100%;
  min-height: 100vh;
  padding: 100px 0 80px;
  overflow-x: hidden;
}


/* =========================================================================
   IDIOMAS — bandeiras nas laterais da seção

   A MONTAGEM
   O mastro é RETO: sai da parede lateral a 90°, na horizontal. A bandeira
   pende dele DEITADA — o pano leva um giro de 90°, de modo que a base dela
   aponta para a lateral e a beirada da tralha fica encostada no mastro.

   Por causa desse giro, as fatias do pano voltam a ser VERTICAIS no seu
   próprio sistema: é a rotação do conjunto que as apresenta empilhadas na
   tela. A vantagem é que a dobra continua sendo um rotateY em torno da
   beirada presa, que é como pano presente se comporta de verdade.

   Como .bandeira gira 90°, a caixa dela troca de eixo na tela. Quem segura o
   espaço no layout é o .pano, com as medidas já trocadas: largura = a
   beirada da tralha, altura = o comprimento do pano.

   COMO O PANO FUNCIONA
   Cada bandeira é uma imagem SVG embutida (sem CDN, sem requisição) repartida
   em 14 fatias. Cada fatia mostra o seu pedaço da mesma imagem, e recebe a
   MESMA animação com duas diferenças:

     · o ATRASO cresce com o índice (--f), então a onda percorre o pano em vez
       de todas as fatias irem juntas;
     · a AMPLITUDE (--amp) vai de 0 na fatia presa ao mastro até 1 na ponta
       solta. É essa rampa que prende a bandeira — sem ela o pano balança em
       bloco e parece adesivo tremendo. A rampa vem invertida na bandeira da
       direita, porque o giro joga a beirada presa para o outro lado.
   ========================================================================= */

.stack-topo{
  display: grid;
  grid-template-columns: 1fr auto 1fr;   /* título ao centro, bandeiras nas beiradas */
  align-items: start;
  gap: clamp(20px, 4vw, 70px);
  margin-bottom: 6rem;
  padding: 0;                            /* as bandeiras encostam nas laterais */
}

.stack-topo .stack-title{
  margin-bottom: 0;
  white-space: nowrap;
  align-self: center;
}

.idioma{
  /* --comp = comprimento do pano (o lado longo da bandeira)
     --larg = a beirada da tralha, que fica encostada no mastro */
  --comp: clamp(130px, 14vw, 205px);
  --larg: calc(var(--comp) * 0.7);

  /* a espessura do mastro e a folga acima do pano andam juntas: o mastro é
     encaixado logo acima da beirada, e as duas medidas saem daqui */
  --mastro: 5px;
  --folga: 16px;

  /* o quanto o mastro avança antes de chegar na bandeira: é este recuo que
     desgruda o pano da parede */
  --recuo: clamp(34px, 5vw, 86px);

  /* e o quanto ele passa DA bandeira. A ponta do pano balança no eixo Z e a
     perspectiva joga essa ponta uns 16px além da caixa da bandeira; a sobra
     tira a ponteira de baixo dela. Medido no pior instante do ciclo.

     Repare que ela é multiplicada por --vento: quando a rajada do hover
     aumenta a amplitude, a ponta avança mais, e o mastro precisa crescer junto
     — senão a bandeira volta a atravessar a ponteira. Uma variável só comanda
     as duas coisas, e elas não têm como sair de sincronia. */
  --vento: 1;
  --sobra: calc(clamp(22px, 1.8vw, 26px) * var(--vento));

  position: relative;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: var(--folga);             /* espaço para o mastro */
}

/* O pano encosta na ponta do mastro, não no meio do bloco. Alinhar pela parede
   (e não centralizar) é o que deixa a conta do mastro fechada: ele vale sempre
   --recuo + --larg, sem depender da largura do rótulo. */
.idioma--esq{ align-items: flex-start; padding-left: var(--recuo); }
.idioma--dir{ align-items: flex-end;  padding-right: var(--recuo); }

/* o rótulo fica centrado debaixo da bandeira; se a etiqueta for mais larga que
   o pano, ela sobra para dentro da seção, nunca para fora */
.idioma-txt{ width: var(--larg); }


.idioma--esq{ justify-self: start; }
.idioma--dir{ justify-self: end; }

/* ---------- o mastro ---------- */

/* Reto e na horizontal: 90° em relação à parede.

   Ele nasce colado na parede, atravessa o --recuo, cobre a beirada inteira da
   tralha e ainda passa da bandeira por --sobra. Daí a largura ser
   --recuo + --larg + --sobra.

   O z-index é o segundo cinto: a ponta do pano oscila no eixo Z e chegava a
   passar por cima da ponteira. A sobra já resolve na geometria; o z-index
   garante na ordem de pintura, aconteça o que acontecer com a onda.

   Na vertical ele encosta por baixo no topo do pano: o bloco tem --folga de
   respiro, então a barra começa uma espessura antes disso. */
.mastro{
  position: absolute;
  z-index: 2;
  top: calc(var(--folga) - var(--mastro));
  left: 0;
  width: calc(var(--recuo) + var(--larg) + var(--sobra));
  height: var(--mastro);
  border-radius: 3px;
  background: linear-gradient(90deg, var(--bg), #4a4f4a);
}

.idioma--dir .mastro{
  left: auto;
  right: 0;
  background: linear-gradient(270deg, var(--bg), #4a4f4a);
}

/* a bucha, encostada na parede lateral */
.mastro::before{
  content: '';
  position: absolute;
  top: 50%;
  left: -4px;
  width: 13px;
  height: 13px;
  margin-top: -6.5px;
  border-radius: 3px;
  background: var(--bg);
}

.idioma--dir .mastro::before{ left: auto; right: -4px; }

/* a ponteira na outra extremidade */
.mastro::after{
  content: '';
  position: absolute;
  top: 50%;
  right: -5px;
  width: 11px;
  height: 11px;
  margin-top: -5.5px;
  border-radius: 50%;
  background: var(--lightgreen);
}

.idioma--dir .mastro::after{ right: auto; left: -5px; }

/* ---------- a caixa que segura o espaço ---------- */

/* O .bandeira gira 90°, e transform NÃO muda o espaço que o elemento ocupa no
   layout. Sem esta caixa com as medidas trocadas, o texto embaixo subiria por
   cima do pano. */
.pano{
  position: relative;
  display: block;
  width: var(--larg);
  height: var(--comp);
  flex-shrink: 0;
}

/* A SOMBRA DO PANO É UM ELEMENTO PARADO, E NÃO UM `filter` NO .bandeira.

   Antes ela era `filter: drop-shadow()` no .bandeira. Um filtro no PAI obriga
   o navegador a rasterizar o subtree inteiro numa superfície própria e borrar
   o alfa dela — e o subtree aqui são 34 fatias que se mexem em 3D em todo
   quadro. Ou seja: re-rasterizar e re-borrar as duas bandeiras 60 vezes por
   segundo, o quadro inteiro, só para uma sombra de 32% de opacidade.

   Aqui a sombra é um retângulo do tamanho do pano, borrado uma vez e guardado
   na sua camada. Ele não anima, então nunca é repintado: custo por quadro
   zero. O que se perde é a sombra ondular junto com a dobra; a 32% de alfa e
   8px de borrão isso não se vê.

   A ordem do transform importa: o `translate` do deslocamento vem ANTES do
   `rotate` na lista, então ele é aplicado DEPOIS na composição — o desvio de
   4/6px acontece no eixo da tela, igual ao drop-shadow original, e não no
   eixo girado do pano. */
.pano::before{
  content: '';
  position: absolute;
  z-index: -1;
  top: 50%;
  left: 50%;
  width: var(--comp);
  height: var(--larg);
  transform: translate(-50%, -50%) translate(4px, 6px) rotate(90deg);
  background: rgba(14, 15, 14, .32);
  /* drop-shadow(... 8px) usa RAIO; blur() usa desvio-padrão, que é metade */
  filter: blur(4px);
  pointer-events: none;
}

.idioma--dir .pano::before{
  transform: translate(-50%, -50%) translate(4px, 6px) rotate(-90deg);
}

/* ---------- o pano ---------- */

.bandeira{
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  width: var(--comp);
  height: var(--larg);
  /* gira 90°: a base da bandeira passa a apontar para a lateral e a beirada
     da tralha encosta no mastro. Na direita o giro é ao contrário, para a
     base apontar para a parede daquele lado. */
  transform: translate(-50%, -50%) rotate(90deg);
  transform-origin: center;
  /* a perspectiva mora AQUI, no pano, com o ponto de fuga na beirada presa */
  perspective: 460px;
  perspective-origin: 0% 50%;
  /* SEM `filter` AQUI — ver .pano::before acima. */
}

.idioma--dir .bandeira{
  transform: translate(-50%, -50%) rotate(-90deg);
  perspective-origin: 100% 50%;
}

/* O pano é fatiado porque só assim ele DOBRA: cada fatia gira o seu tanto e o
   conjunto vira uma curva. O preço é a faceta — dá pra contar as fatias.

   Duas coisas apagam as emendas:

   · SÃO 34, não 14. O salto de ângulo entre vizinhas cai de ~2,3° para ~1,2°,
     e o de profundidade de ~1,8px para menos de 1px. Abaixo de um grau o olho
     lê curva, não polígono. O mesmo vale para o sombreado: a variação de luz
     vira degradê em vez de listra.

   · ELAS SE SOBREPÕEM. Ao dobrar, vizinhas se afastam na tela e abrem uma
     fresta — medi até 2,8px — que aparece como risco claro cortando o pano.
     Cada fatia é desenhada --emenda px MAIS LARGA que o seu lugar, e a margem
     negativa devolve essa sobra ao layout, então o pano continua com o
     comprimento exato.

     E a sobra é pixel-correta, não remendo: como o background-position ancora
     a imagem na beirada esquerda da fatia, os pixels a mais são exatamente a
     continuação do desenho — a mesma coisa que a vizinha mostra ali. */
.fio{
  /* Dimensionada pelo PIOR caso, que é a rajada do hover: com --vento em 1,5 a
     onda abre mais as fatias e 6px já deixavam passar 1,7px de fresta.
     Deliberadamente um valor fixo, e não algo multiplicado por --vento: largura
     de fatia é layout, e animá-la em 68 elementos durante o hover custaria um
     recálculo por quadro. Sobrepor demais não tem preço nenhum — o excesso fica
     debaixo da vizinha. */
  --emenda: 12px;

  flex: 0 0 auto;
  width: calc(var(--comp) / 34 + var(--emenda));
  margin-right: calc(var(--emenda) * -1);

  /* UMA CAMADA DE FUNDO, NÃO DUAS — e a sombra da tralha mora DENTRO da
     imagem.

     Aqui havia `background-image: var(--sombra-tralha), var(--pano-img)` com
     `background-blend-mode: multiply, normal`. Três custos de uma vez:

       · duas camadas dobram `background-size` e `background-position`, e os
         dois são `calc(var(--comp) ...)` — resolvidos em 68 elementos a cada
         recálculo de estilo;
       · `background-blend-mode` tira a fatia do caminho rápido de pintura;
       · o próprio `var(--pano-img)` é uma indireção cara: a variável guarda
         3KB de data-URI em TOKENS, e cada recálculo de estilo torna a
         substituí-los e reprocessá-los nas 68 fatias. Medido nesta página:
         2,8ms por recálculo só nessa substituição.

     O degradê da tralha agora é um <rect> dentro do próprio SVG da bandeira,
     com a mesma curva de alfa (o multiply de cinza .34/.12 equivale a preto
     em .22/.078 — mesma conta, mesmo resultado na tela). Ele fatia junto com o
     desenho porque é o mesmo desenho, então continua dobrando com o tecido —
     que era a razão de ele ser camada de fatia e não véu por cima.

     A imagem é declarada direto em `.bandeira--br .fio` / `.bandeira--us .fio`,
     sem passar por variável nenhuma. */
  background-repeat: no-repeat;
  background-size: var(--comp) var(--larg);
  background-position: calc(var(--f) * (var(--comp) / -34)) 0;

  transform-origin: center left;
  backface-visibility: hidden;
}

/* O `will-change: transform` que ficava aqui saiu. Ele era redundante: uma
   animação de transform em curso JÁ promove o elemento a camada própria, e o
   `will-change` só adiantava isso — ao preço de manter 68 camadas de pé
   enquanto a seção estivesse na tela, mesmo com a animação pausada.
   A sombra da tralha da bandeira da direita também saiu daqui: ela agora vem
   virada de lado dentro do próprio SVG de `.bandeira--us`. */

/* a última não invade ninguém — passaria da beirada solta do pano */
.fio:last-of-type{
  width: calc(var(--comp) / 34);
  margin-right: 0;
}

.idioma--dir .fio{ transform-origin: center right; }

/* Bandeira desenhada pela especificação, em módulos: SVG embutido, sem CDN e
   sem requisição. */
/* Bandeira desenhada pela especificação, em módulos: SVG embutido, sem CDN e
   sem requisição. O último <rect> de cada uma é a sombra da tralha — ver o
   comentário do `.fio`: ela vive dentro do desenho para fatiar (e dobrar)
   junto com ele, sem custar uma segunda camada de fundo.

   A URL vai DIRETO na regra, e não por uma variável. Uma custom property
   guarda tokens crus: cada recálculo de estilo tornaria a substituir esses
   3KB nas 34 fatias. Escrita aqui, a imagem é resolvida uma vez. */
.bandeira--br .fio{ background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 14'><defs><linearGradient id='t' x1='0' y1='0' x2='1' y2='0'><stop offset='0' stop-color='%23000' stop-opacity='.22'/><stop offset='.14' stop-color='%23000' stop-opacity='.078'/><stop offset='.38' stop-color='%23000' stop-opacity='0'/></linearGradient></defs><rect width='20' height='14' fill='%23009c3b'/><path d='M10 1.7 18.3 7 10 12.3 1.7 7Z' fill='%23ffdf00'/><circle cx='10' cy='7' r='3.5' fill='%23002776'/><g fill='%23fff'><circle cx='7.143' cy='6.371' r='.15'/><circle cx='7.452' cy='7.843' r='.15'/><circle cx='7.024' cy='8.157' r='.125'/><circle cx='7.795' cy='7.629' r='.071'/><circle cx='8.181' cy='8.19' r='.125'/><circle cx='8.076' cy='8.538' r='.1'/><circle cx='11.086' cy='5.914' r='.15'/><circle cx='12.452' cy='8.229' r='.15'/><circle cx='12.938' cy='8.262' r='.1'/><circle cx='12.595' cy='8.538' r='.125'/><circle cx='11.752' cy='9.271' r='.125'/><circle cx='11.748' cy='9.624' r='.1'/><circle cx='12.1' cy='8.995' r='.1'/><circle cx='12.381' cy='8.819' r='.125'/><circle cx='11.738' cy='8.929' r='.1'/><circle cx='8.667' cy='7.143' r='.125'/><circle cx='10.952' cy='6.824' r='.1'/><circle cx='10' cy='8.571' r='.15'/><circle cx='10.405' cy='7.876' r='.125'/><circle cx='10' cy='7.562' r='.125'/><circle cx='9.648' cy='7.876' r='.1'/><circle cx='9.824' cy='8.119' r='.071'/><circle cx='11.048' cy='9.357' r='.125'/><circle cx='11.348' cy='9.048' r='.1'/><circle cx='10.771' cy='8.962' r='.1'/><circle cx='8.595' cy='8.857' r='.15'/><circle cx='10' cy='9.738' r='.05'/></g><path d='M6.798 5.585A8.5 8.5 0 0 1 13.466 7.49A3.5 3.5 0 0 1 13.341 8.044A8 8 0 0 0 6.612 6.121A3.5 3.5 0 0 1 6.798 5.585Z' fill='%23fff'/><rect width='20' height='14' fill='url(%23t)'/></svg>"); }

.bandeira--us .fio{ background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 14'><defs><linearGradient id='t' x1='1' y1='0' x2='0' y2='0'><stop offset='0' stop-color='%23000' stop-opacity='.22'/><stop offset='.14' stop-color='%23000' stop-opacity='.078'/><stop offset='.38' stop-color='%23000' stop-opacity='0'/></linearGradient></defs><rect width='20' height='14' fill='%23fff'/><rect y='0.0' width='20' height='1.077' fill='%23b31942'/><rect y='2.154' width='20' height='1.077' fill='%23b31942'/><rect y='4.308' width='20' height='1.077' fill='%23b31942'/><rect y='6.462' width='20' height='1.077' fill='%23b31942'/><rect y='8.615' width='20' height='1.077' fill='%23b31942'/><rect y='10.769' width='20' height='1.077' fill='%23b31942'/><rect y='12.923' width='20' height='1.077' fill='%23b31942'/><rect width='8.0' height='7.538' fill='%230a3161'/><g fill='%23fff'><circle cx='0.667' cy='0.754' r='.165'/><circle cx='2.0' cy='0.754' r='.165'/><circle cx='3.333' cy='0.754' r='.165'/><circle cx='4.667' cy='0.754' r='.165'/><circle cx='6.0' cy='0.754' r='.165'/><circle cx='7.333' cy='0.754' r='.165'/><circle cx='1.333' cy='1.508' r='.165'/><circle cx='2.667' cy='1.508' r='.165'/><circle cx='4.0' cy='1.508' r='.165'/><circle cx='5.333' cy='1.508' r='.165'/><circle cx='6.667' cy='1.508' r='.165'/><circle cx='0.667' cy='2.262' r='.165'/><circle cx='2.0' cy='2.262' r='.165'/><circle cx='3.333' cy='2.262' r='.165'/><circle cx='4.667' cy='2.262' r='.165'/><circle cx='6.0' cy='2.262' r='.165'/><circle cx='7.333' cy='2.262' r='.165'/><circle cx='1.333' cy='3.015' r='.165'/><circle cx='2.667' cy='3.015' r='.165'/><circle cx='4.0' cy='3.015' r='.165'/><circle cx='5.333' cy='3.015' r='.165'/><circle cx='6.667' cy='3.015' r='.165'/><circle cx='0.667' cy='3.769' r='.165'/><circle cx='2.0' cy='3.769' r='.165'/><circle cx='3.333' cy='3.769' r='.165'/><circle cx='4.667' cy='3.769' r='.165'/><circle cx='6.0' cy='3.769' r='.165'/><circle cx='7.333' cy='3.769' r='.165'/><circle cx='1.333' cy='4.523' r='.165'/><circle cx='2.667' cy='4.523' r='.165'/><circle cx='4.0' cy='4.523' r='.165'/><circle cx='5.333' cy='4.523' r='.165'/><circle cx='6.667' cy='4.523' r='.165'/><circle cx='0.667' cy='5.277' r='.165'/><circle cx='2.0' cy='5.277' r='.165'/><circle cx='3.333' cy='5.277' r='.165'/><circle cx='4.667' cy='5.277' r='.165'/><circle cx='6.0' cy='5.277' r='.165'/><circle cx='7.333' cy='5.277' r='.165'/><circle cx='1.333' cy='6.031' r='.165'/><circle cx='2.667' cy='6.031' r='.165'/><circle cx='4.0' cy='6.031' r='.165'/><circle cx='5.333' cy='6.031' r='.165'/><circle cx='6.667' cy='6.031' r='.165'/><circle cx='0.667' cy='6.785' r='.165'/><circle cx='2.0' cy='6.785' r='.165'/><circle cx='3.333' cy='6.785' r='.165'/><circle cx='4.667' cy='6.785' r='.165'/><circle cx='6.0' cy='6.785' r='.165'/><circle cx='7.333' cy='6.785' r='.165'/></g><rect width='20' height='14' fill='url(%23t)'/></svg>"); }

/* ---------- a tremulação ---------- */

.idioma .fio{
  animation: panoTremula 2.6s ease-in-out infinite;
  /* Atraso negativo: a onda já nasce espalhada, em vez de todas as fatias
     partirem do zero no primeiro quadro.
     O passo acompanha a contagem de fatias — 26 × 0.062s dá o mesmo 1,56s de
     defasagem ponta a ponta de quando eram 14 × 0.12s. A onda continua com o
     mesmo comprimento; o que mudou foi só a resolução dela. */
  animation-delay: calc(var(--f) * -0.030s);
}

/* --vento multiplica a amplitude inteira da onda. Vale 1 em repouso e sobe no
   hover, o que dá a rajada. Precisa de @property por dois motivos: sem o tipo
   declarado a variável é só um texto, e aí ela nem interpola (a rajada entraria
   de golpe) nem pode ser usada dentro de calc() com segurança. */
@property --vento{
  syntax: '<number>';
  inherits: true;
  initial-value: 1;
}

/* SÓ `transform`. O `filter: brightness()` que acompanhava cada quadro saiu.

   Ele era o reflexo do tecido, e custava caro em dois lugares ao mesmo tempo:
   68 operações de filtro por quadro na GPU, e 1,9ms por recálculo de estilo na
   CPU (medido nesta página). `transform` sozinho é a única propriedade que o
   compositor anima sem tocar em estilo, layout nem pintura — as 68 fatias
   passam a custar uma matriz cada, e nada mais.

   O que sustenta a leitura de tecido continua inteiro: a rampa de amplitude
   (--amp) prende o pano no mastro, o rotateY em perspectiva faz a dobra, e o
   degradê da tralha — agora dentro do próprio SVG — dá o escurecido de quem
   está amarrado. */
@keyframes panoTremula{
  0%, 100%{
    transform: translateZ(0) rotateY(0deg) translateY(0) skewY(0deg);
  }
  25%{
    transform:
      translateZ(calc(var(--amp) * var(--vento) * 24px))
      rotateY(calc(var(--amp) * var(--vento) * 30deg))
      translateY(calc(var(--amp) * var(--vento) * -6px))
      skewY(calc(var(--amp) * var(--vento) * 4deg));
  }
  50%{
    transform: translateZ(0) rotateY(0deg) translateY(0) skewY(0deg);
  }
  75%{
    transform:
      translateZ(calc(var(--amp) * var(--vento) * -24px))
      rotateY(calc(var(--amp) * var(--vento) * -30deg))
      translateY(calc(var(--amp) * var(--vento) * 6px))
      skewY(calc(var(--amp) * var(--vento) * -4deg));
  }
}

/* Toda sombra e todo brilho do tecido moram DENTRO das fatias — é o único
   jeito de acompanharem a dobra. Uma camada com `inset: 0` por cima do pano é
   um retângulo chapado que não dobra, e como o drop-shadow do .bandeira projeta
   a silhueta de tudo que está dentro dele, ela virava uma sombra quadrada
   aparecendo por trás da bandeira. */

/* ---------- o rótulo ---------- */

/* Duas linhas: o nome da língua e, embaixo, o nível numa etiqueta. Ela vem
   sólida no nativo e só contornada no intermediário — a diferença de peso já
   conta o nível sozinha, sem precisar de medidor.

   As cores saem da paleta do site: preto do texto sobre o creme da seção. */
.idioma-txt{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  line-height: 1.1;
  text-align: center;
}

.idioma-txt strong{
  font-family: 'Space Grotesk', sans-serif;
  font-size: clamp(14px, 1.15vw, 18px);
  font-weight: 700;
  letter-spacing: -.01em;
  text-transform: uppercase;
  color: var(--bg);
}

.idioma-nivel{
  padding: 5px 12px 4px;
  border: 1.5px solid var(--bg);
  border-radius: 999px;
  font-family: 'JetBrains Mono', monospace;
  font-size: clamp(8px, .7vw, 10px);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--bg);
  white-space: nowrap;
}

.idioma-nivel--cheio{
  background: var(--bg);
  color: var(--white);
}

/* ---------- a entrada: cada conjunto chega pela sua lateral ----------

   Mastro, pano e rótulo viajam juntos, porque quem se desloca é o bloco
   inteiro — é o mastro sendo enfiado na parede, com a bandeira pendurada
   nele. A seção tem overflow-x: hidden, então a viagem acontece escondida
   fora da borda.

   O estado escondido só existe sob .idiomas-prontos, classe que o próprio JS
   põe na página. Sem script, sem classe, e as bandeiras nascem no lugar —
   nada aqui é capaz de deixar conteúdo invisível por conta própria. */

/* ATENÇÃO: `transition` é atalho e SUBSTITUI a lista inteira do elemento. O
   --vento (a rajada do hover, mais abaixo) precisa estar em TODA lista que
   valha para .idioma, senão ele entra de golpe em vez de crescer. Já quebrei
   hover assim duas vezes neste arquivo. */
.idioma{ transition: --vento .5s ease-out; }

.idiomas-prontos .idioma{
  opacity: 0;
  transition: transform 1.05s cubic-bezier(.19, 1, .22, 1),
              opacity   .5s  ease-out,
              --vento   .5s  ease-out;
}

.idiomas-prontos .idioma--esq{ transform: translateX(calc(-100% - 14vw)); }
.idiomas-prontos .idioma--dir{ transform: translateX(calc( 100% + 14vw)); }

/* a direita sai um tico depois: as duas chegando no mesmo quadro parecem uma
   coisa só se abrindo, e não dois mastros sendo montados.
   O zero final é do --vento: o atraso é da ENTRADA, e não faz sentido a
   rajada do hover chegar atrasada nessa bandeira pelo resto da visita. */
.idiomas-prontos .idioma--dir{ transition-delay: .12s, .12s, 0s; }

/* chegada — vale tanto pela animação quanto pelo caminho de segurança */
.idiomas-prontos .idioma.idioma-entra,
.idiomas-prontos .idioma.idioma-visivel{
  opacity: 1;
  transform: translateX(0);
}

/* ---------- o hover: uma rajada de vento ----------

   Nada de seguir o cursor — a bandeira não sabe nem quer saber onde o mouse
   está dentro dela. O que ela sente é só que alguém chegou perto: o vento
   aperta. A onda ganha amplitude (--vento), acelera (playbackRate, no
   efeitos.js), o pano escurece a sombra como se enchesse, o reflexo fica mais
   forte e a etiqueta do nível acende.

   Acelerar por playbackRate em vez de trocar a duração é de propósito: mudar
   `animation-duration` no meio do ciclo joga a onda para outro ponto do
   percurso e dá um salto feio. O playbackRate muda só o ritmo, e a fase
   continua de onde estava.

   Só onde existe mouse de verdade: no toque, `:hover` gruda depois do tap e a
   bandeira ficaria ventando para sempre. */
@media (hover: hover) and (pointer: fine){

  .idioma:hover{ --vento: 1.5; }

  /* a sombra que cresce no hover é o retângulo parado do .pano::before, não
     um `filter` no pano — ver o comentário de lá */
  .pano::before{
    transition: transform .45s ease-out,
                background-color .45s ease-out,
                filter .45s ease-out;
  }

  .idioma:hover .pano::before{
    transform: translate(-50%, -50%) translate(7px, 11px) rotate(90deg);
    background: rgba(14, 15, 14, .42);
    filter: blur(7px);
  }

  .idioma--dir:hover .pano::before{
    transform: translate(-50%, -50%) translate(7px, 11px) rotate(-90deg);
  }

  /* a ponteira do mastro dá uma inchada, como se o mastro vibrasse junto */
  .mastro::after{ transition: scale .45s cubic-bezier(.34, 1.56, .64, 1); }
  .idioma:hover .mastro::after{ scale: 1.4; }

  /* a etiqueta vazada preenche; a que já é cheia não muda, e é justamente por
     isso que a regra é uma só — o estado final das duas é o mesmo */
  .idioma-nivel{ transition: background-color .35s ease, color .35s ease; }
  .idioma:hover .idioma-nivel{
    background: var(--bg);
    color: var(--white);
  }
}

/* ---------- fora da tela não gasta nada ---------- */

.idioma.fora-da-tela .fio{
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce){
  .idiomas-prontos .idioma{
    opacity: 1;
    transform: none;
    transition: none;
  }
}

@media (max-width: 900px){
  .stack-topo{
    grid-template-columns: 1fr 1fr;
    gap: 18px 12px;
    margin-bottom: 3.2rem;
  }
  /* no estreito o título passa pra cima e as bandeiras dividem a linha */
  .stack-topo .stack-title{ grid-column: 1 / -1; order: -1; }
  .idioma{ --comp: clamp(100px, 27vw, 155px); }

  /* METADE DAS FATIAS NO CELULAR — 17 por bandeira em vez de 34.

     A regra do desktop está no comentário do `.bandeira`: 34 fatias existem
     porque o salto de ângulo entre vizinhas cai para ~1,2°, e abaixo de um
     grau o olho lê curva em vez de polígono. Só que essa conta é feita em
     GRAUS, não em pixels — e aqui o pano tem 100 a 155px em vez de 205px, numa
     tela que se olha de perto mas é fisicamente pequena. Com 17 fatias o salto
     vai a ~2,4° num pano de um terço do tamanho: continua sem faceta visível.

     O que se ganha é o dobro do que parece. Cada fatia animada é uma camada de
     composição própria, com a sua textura e o seu nó no compositor. Cortar de
     68 para 34 corta pela metade as camadas, as animações e o trabalho por
     quadro das bandeiras — exatamente no aparelho onde isso pesa.

     COMO A CONTA CONTINUA FECHANDO: escondendo as fatias de `--f` ÍMPAR e
     dobrando a largura das que ficam, cada uma passa a mostrar dois
     trinta-e-quatro avos da imagem em vez de um. O `background-position` já é
     `--f * (--comp / -34)`, então a fatia `--f: 2k` começa exatamente onde
     antes começava e agora termina onde a vizinha escondida terminava: o
     desenho continua contínuo, sem reposicionar nada.

     A rampa de amplitude e o atraso da onda também seguem valendo — os dois
     saem de `--f`/`--amp`, que vêm do HTML e não mudaram. */
  .idioma .fio:nth-child(even){ display: none; }

  .idioma .fio{ width: calc(var(--comp) / 17 + var(--emenda)); }
}

@media (prefers-reduced-motion: reduce){
  .idioma .fio{ animation: none; }
}

.stack-title{
  margin: 0;
  padding: 0 40px;
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  color: var(--bg);
  text-align: center;
}

.stack-row{
  margin-bottom: 70px;
}

.stack-row:last-child{
  margin-bottom: 0;
}

.stack-row-label{
  display: block;
  max-width: 1300px;
  margin: 0 auto 18px;
  padding: 0 40px;
  font-family: 'Space Grotesk', sans-serif;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  color: var(--bg);
  opacity: 0.65;
}

/* ---------- Marquee (duas trilhas idênticas, para o loop ficar contínuo) ---------- */
/* full-bleed: ocupa 100% da largura da tela, independente do container centralizado do label */

/* O `overflow: hidden` aqui é obrigatório: é ele que esconde a trilha que
   está fora da tela e faz o marquee parecer infinito. Só que ele corta em
   TODAS as direções — então o item que sobe no hover (translateY(-6px), mais
   o ícone crescendo 22%) ficava decapitado na borda de cima.

   A saída é dar respiro vertical por dentro: o corte acontece na borda da
   caixa de padding, não na do conteúdo. Com padding em cima e embaixo, o item
   levantado passa a ter para onde ir. A margem negativa de mesmo valor devolve
   a altura original, então nada na página se desloca por causa disso. */
.stack-marquee{
  display: flex;
  overflow: hidden;
  width: 100vw;
  margin-left: calc(50% - 50vw);

  padding-top: 18px;
  padding-bottom: 18px;
  margin-top: -18px;
  margin-bottom: -18px;
}

/* O vão entre os itens e o padding da direita PRECISAM ser o mesmo número: a
   faixa repete o conteúdo pra rolar sem emenda, e é esse padding que faz o
   último item guardar a mesma distância do primeiro da cópia seguinte. Se os
   dois se separarem, aparece um salto na volta do laço. Por isso, uma
   variável só. */
.stack-marquee-track{
  --vao: 48px;

  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: var(--vao);
  padding-right: var(--vao);

  /* A FAIXA É UMA ANIMAÇÃO DE CSS, E SÓ ISSO.

     Aqui havia um sistema inteiro por cima: um `Agenda.pintar` lia a posição
     da página em todo quadro, calculava o delta, transformava em multiplicador
     de velocidade e escrevia `playbackRate` nas 8 trilhas; um
     requestAnimationFrame próprio ficava rodando atrás disso pra desacelerar
     de volta; e um IntersectionObserver decidia quando ligar. Rolando pra
     cima, as faixas ainda invertiam de sentido.

     Nada disso sobreviveu, e o motivo é o de sempre: o navegador roda esta
     animação no compositor, fora da thread principal — o JavaScript não
     precisa acordar uma vez sequer para ela andar. Assim que alguém escreve
     `playbackRate` a cada quadro, a faixa vira trabalho de CPU por quadro e
     passa a competir com o scroll. O ganho visual não pagava.

     `translate3d` de 0 a -100% da PRÓPRIA caixa: como as duas trilhas irmãs
     são idênticas, quando a primeira andou a própria largura a segunda está
     exatamente onde a primeira começou. O laço fecha sem emenda e sem JS.

     A duração vem de `--marquee-dur`, escrito uma vez por medida no
     js/script.js — ver `marqueeVelocidade` lá. O valor abaixo é só o que vale
     antes da primeira medida. */
  animation: stackMarquee var(--marquee-dur, 24s) linear infinite;
}

/* DOIS CONJUNTOS DE QUADROS, E NÃO UM COM `animation-direction: reverse`.

   O reverse é a forma enxuta de escrever isto e foi o que ficou aqui primeiro.
   No celular o resultado foi inequívoco: as faixas da ESQUERDA (direção
   normal) rodavam lisas e as da DIREITA (reverse) engasgavam — mesma
   animação, mesma duração, mesmo tamanho de camada, só a direção diferente.
   O sintoma aponta para o compositor recusando a animação invertida e
   devolvendo-a à thread principal, que é onde o scroll também está.

   Escrever os quadros já na ordem certa custa uma regra a mais e tira a
   variável do caminho: as duas direções passam a ser animações comuns, tocadas
   para a frente, e o navegador trata as oito trilhas exatamente igual. */
.stack-marquee--right .stack-marquee-track{
  animation-name: stackMarqueeDireita;
}

/* Pausar é a única coisa que o hover faz. `animation-play-state` não repinta
   nada: o compositor simplesmente para de avançar o relógio. */
.stack-marquee:hover .stack-marquee-track{
  animation-play-state: paused;
}

@keyframes stackMarquee{
  from{ transform: translate3d(0, 0, 0); }
  to  { transform: translate3d(-100%, 0, 0); }
}

@keyframes stackMarqueeDireita{
  from{ transform: translate3d(-100%, 0, 0); }
  to  { transform: translate3d(0, 0, 0); }
}

.stack-item{
  display: flex;
  align-items: center;
  gap: 20px;
  white-space: nowrap;
  font-family: 'JetBrains Mono', monospace;
  font-size: clamp(10px, 1vw, 50px);
  font-weight: 600;
  color: var(--bg);
}

.stack-item i{
  font-size: clamp(38px, 2vw, 72px);
  color: var(--bg);
}

/* O ícone do Netlify é um SVG embutido, não um glifo de fonte: o devicon
   2.15.1 (a versão presa no <link>) simplesmente NÃO tem a classe
   `devicon-netlify-plain` — ela só apareceu na 2.16.0 — então o <i> ficava
   com content: none e largura 0, um buraco no meio do marquee.
   Embutir o SVG oficial resolve sem subir a versão do CDN (a 2.16.0 tem o
   dobro do tamanho, 103KB contra 53KB) e sem depender da rede pra esse ícone.
   O tamanho vem de `height: 1em` pra ele acompanhar exatamente a mesma
   escala dos glifos vizinhos, e o fill é currentColor pra herdar a cor —
   inclusive a troca no hover. */
.stack-item .icone-svg{
  height: clamp(38px, 2vw, 72px);
  width: auto;
  color: var(--bg);
  flex-shrink: 0;
}

.stack-dot{
  font-family: 'JetBrains Mono', monospace;
  font-size: clamp(10px, 2vw, 56px);
  color: var(--bg);
  opacity: 0.35;
}

@media (max-width: 600px){
  .stack-title{ margin-bottom: 46px; }
  .stack-row{ margin-bottom: 40px; }
}

/* =========================================================================
   CONTATO (título da seção)
   ========================================================================= */

.ct-title{
  color: var(--white);
  display: block;
  text-align: center;
  font-family: 'JetBrains Mono', monospace;
}

.direitos{
  color: var(--white);
  display: flex;
  justify-content: center;
  align-items: end;
}

/* =========================================================================
   MENU HAMBÚRGUER — BOTÃO FIXO
   Ícone: 3 barrinhas do mesmo tamanho (hambúrguer clássico). O contorno
   do botão fica fixo, sem animação de arredondar. Ao abrir, as 2 de fora
   deslizam até o centro girando pra formar um X (com uma leve "mola" no
   movimento), e a do meio some.
   ========================================================================= */

.menu-toggle{
  position: fixed;
  top: 24px;
  right: 24px;
  z-index: 200; /* precisa ficar acima de TODAS as seções (ex: .projetos usa z-index: 100) */
  width: 48px;
  height: 48px;
  background: transparent;
  border: 3px solid #fff;
  border-radius: 20%;
  cursor: pointer;
  padding: 0;
  /* inverte automaticamente conforme o fundo por trás (claro <-> escuro) */
  mix-blend-mode: difference;
  /* força a própria camada de composição — evita artefatos de subpixel
     nos elementos internos quando usamos mix-blend-mode */
  transform: translateZ(0);
}

/* Com o menu aberto o botão vira opaco: o mix-blend-mode: difference precisa
   de algo atrás para inverter, e sobre o fundo chapado do overlay ele sumia. */
.menu-toggle.open{
  mix-blend-mode: normal;
  background: var(--bg);
}

/* pulinho elástico do botão inteiro a cada clique — a classe é
   removida/readicionada via JS pra poder rodar de novo em cada clique */
.menu-toggle.mt-bounce{
  animation: mtBounce .5s cubic-bezier(.34, 1.56, .64, 1);
}

@keyframes mtBounce{
  0%   { transform: translateZ(0) scale(1); }
  35%  { transform: translateZ(0) scale(.8); }
  70%  { transform: translateZ(0) scale(1.08); }
  100% { transform: translateZ(0) scale(1); }
}

.mt-bar{
  position: absolute;
  left: 50%;
  width: 26px;
  margin-left: -13px;
  height: 3px;
  border-radius: 2px;
  background: #fff;
  mix-blend-mode: difference;
  /* mantém a barra sempre na sua própria camada de composição, com o mesmo
     transform "base" (translateZ(0)) em repouso e depois de girar — assim
     o navegador nunca troca de camada no meio do caminho, o que é o que
     causava a barra 1 renderizar mais fina depois do primeiro clique */
  transform: translateZ(0);
  transition:
    top       .45s cubic-bezier(.34, 1.56, .64, 1),
    transform .45s cubic-bezier(.34, 1.56, .64, 1),
    opacity   .3s  ease;
}

.mt-bar-1{ top: 14px; }
.mt-bar-2{ top: 22px; }
.mt-bar-3{ top: 30px; }

/* aberto: as barras de fora deslizam até o centro e giram formando o X;
   a do meio encolhe e some. Um pequeno atraso nas de fora dá mais
   personalidade ao movimento. */
.menu-toggle.open .mt-bar-1{
  top: 22px;
  transform: translateZ(0) rotate(45deg);
  transition-delay: .05s;
}

.menu-toggle.open .mt-bar-2{
  opacity: 0;
  transform: translateZ(0) scaleX(0);
}

.menu-toggle.open .mt-bar-3{
  top: 22px;
  transform: translateZ(0) rotate(-45deg);
  transition-delay: .05s;
}

@media (max-width: 600px){
  .menu-toggle{ width: 42px; height: 42px; }
  .mt-bar{ width: 22px; margin-left: -11px; }
  .mt-bar-1{ top: 12px; }
  .mt-bar-2{ top: 19px; }
  .mt-bar-3{ top: 26px; }

  .menu-toggle.open .mt-bar-1,
  .menu-toggle.open .mt-bar-3{
    top: 19px;
  }
}

/* =========================================================================
   MENU HAMBÚRGUER — OVERLAY DE PÁGINA INTEIRA
   ========================================================================= */

.menu-panel{
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  /* dvh, e não svh: o overlay é `position: fixed` e precisa cobrir exatamente
     o que está visível AGORA, com a barra do navegador no estado em que ela
     estiver. Com `100vh` (altura grande) o rodapé do menu ficava embaixo da
     barra no celular — os ícones de rede eram os primeiros a sumir.
     Ao contrário do hero, aqui a altura mudar junto com a barra não custa
     nada: o painel não tem conteúdo preso a scroll. */
  height: 100dvh;
  background: var(--bg);
  z-index: 150; /* acima de .projetos (100), abaixo do botão (200) */
  display: flex;
  align-items: center;
  justify-content: center;

  transform: translateY(-100%);
  pointer-events: none;
  /* pointer-events sozinho só tirava o mouse: os 9 links continuavam na ordem
     do TAB e dava pra "entrar" no menu fechado com o teclado, focando coisas
     invisíveis fora da tela. visibility: hidden tira os filhos do foco e ainda
     é animável (por isso o delay: espera o painel subir antes de sumir). */
  visibility: hidden;
  transition:
    transform  .55s cubic-bezier(.65, 0, .35, 1),
    visibility 0s linear .55s;
}

.menu-panel.open{
  transform: translateY(0);
  pointer-events: auto;
  visibility: visible;
  transition:
    transform  .55s cubic-bezier(.65, 0, .35, 1),
    visibility 0s linear 0s;
}

.menu-content{
  position: relative;
  z-index: 1; /* fica acima das linhas onduladas de fundo (.menu-waves) */
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: row; /* fotos à esquerda, links + card de redes à direita */
  align-items: center;
  justify-content: space-between;
  padding: 0 6vw;
  gap: 40px;
  font-family: 'Space Grotesk', sans-serif;

  opacity: 0;
  transform: translateY(-16px);
  transition: opacity .35s ease, transform .35s ease;
}

.menu-panel.open .menu-content{
  opacity: 1;
  transform: translateY(0);
  /* precisa ser IGUAL ou MAIOR que a duração da transição do .menu-panel (.55s)
     — se o conteúdo (fotos rotacionadas) começar a aparecer enquanto o painel
     ainda está descendo, a borda da tela corta as fotos em ângulo reto e revela
     primeiro a ponta rotacionada delas em vez da borda inteira. Esperando o
     painel terminar, o conteúdo só aparece com a tela já 100% coberta. */
  transition-delay: .25s;
}

/* =========================================================================
   MENU — LINHAS ONDULADAS DE FUNDO (decorativas, atrás do .menu-content)
   Várias linhas finas empilhadas, cada uma rolando pra um lado (técnica de
   duas trilhas idênticas, igual ao .stack-marquee), alternando
   as cores var(--white) / var(--lightgreen) e "respirando" em opacidade
   com atrasos diferentes pra não ficarem todas sincronizadas.
   ========================================================================= */

.menu-waves{
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly; /* espalha do topo ao rodapé, não só no meio */
  overflow: hidden;
  pointer-events: none;
}

.menu-wave{
  display: flex;
  width: 100%;
  overflow: hidden;
  animation:
    menuWaveFade 7s ease-in-out infinite var(--wave-fade-delay, 0s),
    menuWaveBob var(--wave-bob-duration, 4s) ease-in-out infinite var(--wave-bob-delay, 0s);
}

.menu-wave-track{
  display: flex;
  flex-shrink: 0;
  width: 100%;
}

.menu-wave--right .menu-wave-track{
  animation-name: menuWaveScrollRight;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-duration: var(--wave-scroll-duration, 22s);
}

.menu-wave--left .menu-wave-track{
  animation-name: menuWaveScrollLeft;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-duration: var(--wave-scroll-duration, 22s);
}

@keyframes menuWaveScrollLeft{
  0%   { transform: translateX(0%); }
  100% { transform: translateX(-100%); }
}

@keyframes menuWaveScrollRight{
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(0%); }
}

@keyframes menuWaveFade{
  0%, 100%{ opacity: .12; }
  50%{ opacity: .55; }
}

/* balanço vertical suave — uma sobe, outra desce, em velocidades e
   atrasos diferentes (definidos por linha via --wave-bob-*), pra dar a
   sensação de várias ondas de verdade se movendo, não só linhas
   deslizando de lado */
@keyframes menuWaveBob{
  0%, 100%{ transform: translateY(-7px); }
  50%{ transform: translateY(7px); }
}

.menu-wave-svg{
  width: 100%;
  height: 26px;
  display: block;
  flex-shrink: 0;
}

.menu-wave-svg path{
  fill: none;
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
}

.menu-wave--white .menu-wave-svg path{ stroke: var(--white); }
.menu-wave--lightgreen .menu-wave-svg path{ stroke: var(--lightgreen); }

/* PAUSA COM O MENU FECHADO — o maior ganho de performance do site inteiro.
   São 16 linhas, cada uma com 2 animações infinitas (fade + bob), mais 32
   trilhas com a animação de deslize: ~64 animações rodando SEM PARAR desde o
   carregamento da página, compondo camadas atrás de um overlay que na maior
   parte do tempo está fechado. O navegador não tem como adivinhar que
   ninguém está vendo.

   Duas sutilezas nesta regra:
   · ela precisa vir DEPOIS de .menu-wave / .menu-wave-track. A propriedade
     `animation` é atalho, e todo atalho reseta o que não foi citado — ou
     seja, aquelas declarações voltam animation-play-state pra `running`.
     Com a mesma especificidade, quem vem por último vence;
   · o seletor usa :not(.open) no painel (0,3,0) em vez de depender só da
     ordem, pra sobreviver a qualquer reordenação futura do arquivo. */
.menu-panel:not(.open) .menu-waves,
.menu-panel:not(.open) .menu-waves *{
  animation-play-state: paused;
}

@media (max-width: 600px){
  .menu-waves{ gap: 22px; }
  .menu-wave-svg{ height: 18px; }
}

.menu-photos{
  display: flex;
  gap: 20px;
  flex-shrink: 0;
}

.menu-photo-col{
  display: flex;
  flex-direction: column;
  gap: 26px;
}

.menu-photo-col.offset{
  margin-top: 90px;
}

.menu-photos img{
  width: 260px;
  height: 330px;
  object-fit: cover;
  border-radius: 2%;
  box-shadow: 0 25px 45px rgba(0,0,0,0.7);
  transform: rotate(var(--rot, 0deg));
  display: block;
  filter: grayscale(1) sepia(0.12) hue-rotate(65deg) saturate(1.3) brightness(1);
  transition: filter .4s ease;
}

.menu-photos img:hover{
  filter: grayscale(0) contrast(1.05);
}

/* will-change força o navegador a manter uma camada de GPU dedicada. Nas 8
   fotos (260x330 cada) isso ficava reservado a página inteira, mesmo com o
   menu fechado o tempo todo — memória de vídeo parada. Só vale a pena
   enquanto o menu está aberto e elas realmente se movem. */
.menu-panel.open .menu-photos img{
  will-change: transform, filter;
}

/* ---------- Wrapper: links + card de redes sociais, empilhados ---------- */

/* O texto dos links NÃO encosta na borda direita do próprio elemento: o
   .text-reveal tem `padding: 0.15em 0.4em` (o respiro que a animação de
   revelar precisa pra não cortar as letras). Com a fonte em 89.6px isso dá
   quase 36px de recuo invisível.
   O divisor e o card de contato, alinhados à borda da CAIXA, ficavam então
   36px mais à direita que o texto que o olho usa de referência — daí a
   sensação de estarem deslocados. Recuando os dois pelo mesmo valor, tudo
   passa a alinhar pela lateral do texto de verdade. */
.menu-right{
  --recuo-link: calc(clamp(24px, 7vw, 6rem) * 0.4);

  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 34px;
}

.menu-social-card{
  margin-right: var(--recuo-link);
}


.menu-links{
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: end;
  gap: 22px;
}

.menu-links a{
  color: var(--white);
  text-decoration: none;
  font-size: clamp(24px, 7vw, 6rem);
  font-weight: 700;
  transition: color .25s ease;
}

.menu-links a:hover{
  color: var(--lightgreen);
}

/* =========================================================================
   MENU — CARD DE REDES SOCIAIS (abaixo dos links do menu)
   ========================================================================= */

/* recuado pelo mesmo tanto que o card, pra encostar na lateral do TEXTO dos
   links e não na borda da caixa deles (ver .menu-right). A largura encolhe
   junto: só a margem empurraria a caixa pra fora do container pela esquerda.
   Esta declaração precisa morar AQUI, no bloco original — declarada antes,
   ela perdia pro `margin: 0` que ficava logo abaixo. */
.menu-divider{
  width: calc(100% - var(--recuo-link));
  max-width: 340px;
  border: none;
  border-top: 1px solid rgba(241, 236, 225, 0.25);
  margin: 0 var(--recuo-link) 0 0;
}

.menu-social-card{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  width: fit-content;
  height: fit-content;
  padding: 0;
  background-color: transparent;
  box-shadow: none;
}

.ms-icon{
  width: 60px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background-color: rgb(44, 44, 44);
  text-decoration: none;
  transition: background-color .3s, transform .3s, border-radius .3s;
}

.ms-icon i{
  font-size: 21px;
  color: #fff;
  transition: color .3s;
}

.ms-icon:hover{
  transform: scale(1.4);
  border-radius: 13px;
}

.ms-icon:active{
  transform: scale(0.9);
  transition-duration: .3s;
}

.ms-instagram:hover{ background-color: var(--lightgreen); }
.ms-linkedin:hover{ background-color: var(--lightgreen); }
.ms-github:hover{ background-color: var(--lightgreen); }
.ms-email:hover{ background-color: var(--lightgreen); }
.ms-instagram:hover i{ color: var(--bg); }
.ms-linkedin:hover i{ color: var(--bg); }
.ms-github:hover i{ color: var(--bg); }
.ms-email:hover i{ color: var(--bg); }

.ms-icon:hover i{
  animation: msSlideInTop .3s both;
}

@keyframes msSlideInTop{
  0%{
    transform: translateY(50px);
    opacity: 0;
  }
  100%{
    transform: translateY(0);
    opacity: 1;
  }
}

@media (max-width: 900px){
  .menu-right{ align-items: center; }
  .menu-divider{ max-width: 260px; }
  .menu-social-card{ gap: 14px; }
  .ms-icon{ width: 48px; height: 48px; }
  .ms-icon i{ font-size: 17px; }
}

/* =========================================================================
   EFEITO "TEXT REVEAL" (usado nos links do menu)
   ========================================================================= */

.text-reveal{
  --tr-duration: 250ms;
  --tr-easing: ease-in-out;
  --tr-stagger: 25ms;
  --tr-color: inherit;
  --tr-hover-color: #b2c73a;

  display: inline-block;
  position: relative;
  text-decoration: none;
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  line-height: 1;
  padding: 0.15em 0.4em;
  overflow: hidden;
  cursor: pointer;
  user-select: none;
  color: var(--tr-color);
  transition: color .35s ease;
}

.text-reveal:hover{
  color: var(--tr-hover-color);
}

.text-reveal .tr-track{
  display: inline-flex;
  overflow: hidden;
  position: relative;
  height: 1em;
}

.text-reveal .tr-char{
  display: inline-block;
  position: relative;
  transition: transform var(--tr-duration) var(--tr-easing);
  transition-delay: calc(var(--tr-i) * var(--tr-stagger));
}

/* `will-change` reserva camada de GPU NA HORA e não devolve enquanto a regra
   valer. Solto no .tr-char, ele mantinha 31 camadas de pé o tempo todo por
   causa de um menu que está fechado — memória de vídeo parada, do primeiro ao
   último pixel da página. Só faz sentido enquanto o menu está aberto, que é
   quando esses caracteres podem de fato se mover. */
.menu-panel.open .text-reveal .tr-char{
  will-change: transform;
}

/* direção "up" (padrão): cópia-fantasma aparece embaixo */
.text-reveal[data-direction="up"] .tr-char{
  text-shadow: 0 1em currentColor;
}
.text-reveal[data-direction="up"]:hover .tr-char{
  transform: translateY(-1em);
}

/* direção "down": cópia-fantasma aparece em cima */
.text-reveal[data-direction="down"] .tr-char{
  text-shadow: 0 -1em currentColor;
}
.text-reveal[data-direction="down"]:hover .tr-char{
  transform: translateY(1em);
}

/* =========================================================================
   "I ANIMATE." — letras animáveis por clique
   O texto é quebrado em <span> (um por letra) pra cada um poder rodar sua
   própria animação. Precisa herdar o mesmo tamanho/peso de .texto h1 pra
   ficar do mesmo tamanho e espaçamento de "DESIGN." e "DEVELOP.".
   ========================================================================= */

.word{
  display: inline-flex; /* remove o espaço "fantasma" entre os <span>, causado pelas quebras de linha no HTML */
  align-items: flex-end;
  font-family: 'JetBrains Mono', monospace;

  /* Mesmo caso do .dev-alvo logo acima, e pelo mesmo motivo: as letras do
     "ANIMATE." giram em rotateX/rotateY e a perspectiva era fixa em 1000px
     enquanto o corpo varia de 62px a 410px. Aqui o fator é 1000 ÷ 230,4, de
     novo o corpo num monitor de 1440 — o desktop não muda em nada e o celular
     passa a ver o mesmo giro. Os dois painéis ficam coerentes entre si, que é
     o que faz a sequência parecer uma coisa só. */
  perspective: 4.34em;
}

.word span{
  cursor: pointer;
  display: inline-block;
  font-size: inherit; /* herda o clamp() de .texto h1 — mesmo tamanho de DESIGN. e DEVELOP. */
  line-height: inherit;
  user-select: none;
}

.word span:nth-child(1).active{
  animation: balance 1.05s ease-out;
  transform-origin: bottom left;
}

@keyframes balance{
  0%, 100% { transform: rotate(0deg); }
  30%, 60% { transform: rotate(-45deg); }
}

.word span:nth-child(2).active{
  animation: shrinkjump .9s ease-in-out;
  transform-origin: bottom center;
}

/* OS SALTOS SÃO EM `em`, NÃO EM PIXEL, e é o mesmo problema que a
   `perspective` do .word já tinha resolvido logo acima — com o mesmo fator.

   O corpo destas letras é `clamp(24px, 16vw, 30rem)`: 60px num celular de
   375, 410px num monitor de 2560. Com o salto travado em 150px, o "N" pulava
   2,5 alturas de letra no celular (saía do painel) e 0,37 no monitor grande
   (mal se via) — a mesma animação lida de sete jeitos diferentes conforme a
   tela. Em `em` o salto passa a ser uma fração da própria letra e o gesto fica
   igual em qualquer largura.

   O divisor é 230,4 — o corpo num monitor de 1440, o mesmo de referência da
   perspectiva. Então 150px ÷ 230,4 = .651em: a 1440 nada muda, e é só fora
   dali que a conta começa a corrigir. */
@keyframes shrinkjump{
  10%, 35% { transform: scale(2, .2) translate(0, 0); }
  45%, 50% { transform: scale(1) translate(0, -.651em); }
  80%      { transform: scale(1) translate(0, 0); }
}

.word span:nth-child(3).active{
  animation: falling 1.35s ease-out;
  transform-origin: bottom center;
}

@keyframes falling{
  12%      { transform: rotateX(240deg); }
  24%      { transform: rotateX(150deg); }
  36%      { transform: rotateX(200deg); }
  48%      { transform: rotateX(175deg); }
  60%, 85% { transform: rotateX(180deg); }
  100%     { transform: rotateX(0deg); }
}

.word span:nth-child(4).active{
  animation: bounce 1s ease-out;
  transform-origin: bottom center;
}

/* em vez de px, pelo motivo explicado no shrinkjump: 130 ÷ 230,4 = .564em,
   45 → .195em, 12 → .052em */
@keyframes bounce{
  0%   { transform: translateY(0) scaleY(1); }
  20%  { transform: translateY(-.564em) scaleY(1); }
  40%  { transform: translateY(0) scaleY(0.65); }
  55%  { transform: translateY(-.195em) scaleY(1.1); }
  70%  { transform: translateY(0) scaleY(0.85); }
  85%  { transform: translateY(-.052em) scaleY(1.05); }
  100% { transform: translateY(0) scaleY(1); }
}

.word span:nth-child(5).active{
  animation: toplong 1.1s linear;
}

@keyframes toplong{
  10%, 40% { transform: translateY(-48vh) scaleY(1); }
  90%      { transform: translateY(-48vh) scaleY(4); }
}

.word span:nth-child(6).active{
  animation: wiggle .9s ease-in-out;
  transform-origin: bottom center;
}

@keyframes wiggle{
  0%, 100% { transform: rotate(0deg); }
  15%      { transform: rotate(-16deg); }
  30%      { transform: rotate(13deg); }
  45%      { transform: rotate(-10deg); }
  60%      { transform: rotate(7deg); }
  75%      { transform: rotate(-4deg); }
}

.word span:nth-child(7).active{
  animation: rotate .9s ease-out;
}

@keyframes rotate{
  20%, 80% { transform: rotateY(180deg); }
  100%     { transform: rotateY(360deg); }
}

/* o "." era a única letra sem animação. Como o JS tira a classe .active no
   evento animationend, e sem @keyframes esse evento nunca dispara, o ponto
   ficava com a classe presa pra sempre — e não reagia a mais nenhum clique. */
.word span:nth-child(8).active{
  animation: dotdrop .9s cubic-bezier(.34, 1.56, .64, 1);
  transform-origin: bottom center;
}

/* em vez de px, pelo motivo explicado no shrinkjump: 80 ÷ 230,4 = .347em,
   16 → .069em */
@keyframes dotdrop{
  0%   { transform: translateY(0) scale(1); }
  30%  { transform: translateY(-.347em) scale(1.35); }
  60%  { transform: translateY(0) scale(1.25, .75); }
  80%  { transform: translateY(-.069em) scale(1); }
  100% { transform: translateY(0) scale(1); }
}

/* =========================================================================
   CONTATO
   ========================================================================= */

.contato{
  background: var(--bg);
  min-height: 90vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 70px;
  padding: 100px 7vw 40px;
}

.contato-grid{
  display: grid;
  grid-template-columns: 1.2fr 0.8fr;
  gap: 60px;
  align-items: center;
  width: 100%;
}

/* ---------- Bio animada ---------- */

.bio-animada{
  text-align: left;
}

/* =========================================================================
   RETRATO RECORTADO (acima do texto "Sobre mim")

   O recorte é PNG sem fundo sobre uma seção preta, e a tentação é só soltar
   ele ali. O problema de fazer isso é que a foto termina onde o moletom
   termina — num corte reto no meio do nada, que denuncia o recorte e faz a
   imagem parecer colada por cima do texto.

   Duas coisas costuram a foto ao bloco:

   · a MÁSCARA em degradê dissolve a base dele no preto da seção, então não
     existe borda inferior nenhuma — ele nasce do próprio fundo;
   · a SOMBRA tem a forma do corpo. Aqui já esteve um degradê radial, e o
     problema dele era exatamente esse: círculo nenhum tem a ver com a
     silhueta de uma pessoa, então lia como uma bola de luz. Agora é
     `drop-shadow`, que no CSS não desenha um retângulo — ele segue o CANAL
     ALFA da imagem. Como o PNG é recortado, a sombra sai no formato dos
     ombros, da cabeça e dos braços cruzados.

     O que separa sombra de borrão é o RAIO. Na primeira versão eu usei 38px
     de desfoque com pouco deslocamento: espalhado demais, o contorno se
     desmancha e o que sobra é uma mancha luminosa em volta. Sombra de
     verdade é deslocada e razoavelmente nítida — dá pra reconhecer a
     silhueta nela. Por isso o raio caiu para menos da metade e o
     deslocamento cresceu.

     Ela vive no <figure>, e não na <img>, de propósito: o navegador aplica
     filtro ANTES da máscara. Na imagem, a máscara cortaria a própria sombra
     pela base. No pai, o filtro age sobre o resultado já mascarado, então a
     sombra também se dissolve embaixo, junto com ele.

     Numa seção quase preta, sombra escura não aparece — por isso ela é
     clara e esverdeada, deslocada como quem projeta numa parede atrás.

   A cor da foto ficou cheia. Cheguei a pôr nela o véu esverdeado das fotos
   do menu, mas ela é o único retrato de rosto do site e pedia cor de
   verdade; quem amarra ela ao resto é a luz atrás, não uma camada por cima.

   A margem negativa embaixo é de propósito: o rótulo sobe e começa
   exatamente onde a foto já se dissolveu.
   ========================================================================= */

.bio-retrato{
  position: relative;
  /* o piso é generoso de propósito: 23vw num celular daria uns 90px, e aí o
     rosto some. Metade da largura da tela é o mínimo pra ele ter presença */
  width: clamp(200px, 23vw, 290px);

  /* o bloco inteiro (foto + rótulo + texto) sobe um tanto: centrado na linha
     do grid ele ficava baixo demais em relação à coluna da direita */
  margin: clamp(-64px, -3.6vw, -26px) 0 -14px;
  line-height: 0;                 /* tira o espaço fantasma embaixo da img */

  /* Aqui já esteve uma sombra verde projetada. Ela saiu: numa seção quase
     preta, sombra clara não lê como sombra, lê como um brilho colorido atrás
     da pessoa. Sem nada atrás, o recorte se resolve sozinho — o moletom é
     branco sobre preto e já tem contraste de sobra. */
}

.bio-retrato img{
  position: relative;
  display: block;
  width: 100%;
  height: auto;

  /* a base some no fundo — sem isso o recorte acaba num corte reto */
  -webkit-mask-image: linear-gradient(180deg,
    #000 0%, #000 66%, rgba(0, 0, 0, .55) 84%, transparent 99%);
  mask-image: linear-gradient(180deg,
    #000 0%, #000 66%, rgba(0, 0, 0, .55) 84%, transparent 99%);

  /* cor cheia. O único ajuste é um empurrãozinho de contraste e saturação,
     porque o moletom é branco sobre preto e sem isso ele lava */
  filter: saturate(1.06) contrast(1.04);
  transition: filter .45s ease;
}

@media (hover: hover) and (pointer: fine){
  .bio-retrato:hover img{ filter: saturate(1.12) contrast(1.06) brightness(1.05); }
}

/* A ENTRADA — revela de cima para baixo, acompanhando o scroll

   O gesto é REVELAR, não deslizar. O clip-path abre uma janela que cresce do
   topo para a base: `inset(0 0 100% 0)` não mostra nada, e conforme o recorte
   de baixo diminui a área visível desce a partir da borda de cima. A foto vai
   surgindo — primeiro o rosto, depois o tronco, por último a barra do moletom.
   Os 34px que descem junto são só para o movimento ter peso; quem faz o efeito
   é a janela.

   O sentido é o único ajuste em relação à versão anterior, que abria de baixo
   para cima (`inset(100% 0 0 0)` com `translateY(34px)`). Duração, easing,
   opacidade e a distância do empurrão continuam iguais — a janela e o
   deslocamento só trocaram de ponta, para os dois andarem no mesmo sentido.

   Casa bem com a máscara do topo deste arquivo, que apaga a base da imagem: a
   revelação TERMINA na parte que já é translúcida, em vez de começar nela.

   Só esconde sob .retrato-scrub, classe que o JS coloca. Sem script, sem
   classe: a foto aparece normalmente.

   QUEM MOVE O EFEITO É O --rp (0 a 1), escrito pelo js/efeitos.js a cada
   quadro de scroll. Aqui já esteve uma @keyframes pausada cujo ponteiro o JS
   empurrava pelo `currentTime`, e era ela que fazia a foto não reagir ao
   scroll no celular: para achar a animação o script precisava de
   `getAnimations()` E da propriedade `animationName` de cada item da lista.

   Essa propriedade só existe no Safari a partir do 16.4, e a lista em si só
   fica pronta depois que o navegador resolve o estilo — momento que cada
   motor escolhe sozinho, e que o `offsetHeight` forçado do script garantia no
   Chrome mas não no WebKit. Falhando qualquer um dos dois, o script não
   achava nada, desistia e devolvia a foto estática. No iPhone era exatamente
   isso que se via: a foto aparecia inteira e não acompanhava o dedo.

   Uma variável não depende de nada disso — é substituição de texto no cálculo
   do estilo, igual em todo motor que entende `var()`, e o valor está no
   elemento em vez de dentro de um objeto de animação que pode não existir.

   O `1` do fallback é a rede de segurança: se o JS não rodar, não escrever a
   variável ou escrever lixo, o cálculo cai no 1 e a foto simplesmente aparece
   — o mesmo destino de quem não tem a classe. */
/* A JANELA É UMA CORTINA QUE DESCE, E NÃO UM `clip-path`.

   O gesto é o mesmo e o resultado na tela é o mesmo; o que muda é quem paga.
   `clip-path` não é uma propriedade que o compositor saiba animar: cada valor
   novo REPINTA o elemento recortado. E aqui o elemento recortado é uma foto
   com máscara em degradê e `filter` por cima — a repintura mais cara que esta
   página tem. Medido no celular emulado: 200x297px por quadro, quase 20% da
   tela, na thread principal, exatamente enquanto o dedo rola. Era isso que se
   sentia como "não está suave".

   A cortina é um retângulo da cor do fundo da seção (`--bg`, que é chapada
   atrás da foto — sem gradiente, sem nada) deslizando para baixo por
   `translateY`. Cobrir com uma cor sólida idêntica ao fundo e recortar dão o
   mesmo pixel; a diferença é que `translateY` o compositor faz sem repintar.

   A FOTO NÃO SE MOVE E NÃO ESMAECE. Aqui havia mais duas coisas montadas em
   cima da revelação: um `translateY` de 34px, que fazia a foto subir e descer
   junto com o dedo em vez de ficar parada, e um `opacity` de 0 a 1, que a
   deixava fantasma enquanto a janela abria. As duas saíram.

   O que restou é só o gesto: a janela descendo de cima para baixo, com a foto
   inteira e opaca desde o primeiro pixel revelado — o recorte aparece com o
   contraste que ele tem, não desbotado. */

.bio-retrato.retrato-scrub::after{
  content: '';
  position: absolute;
  /* a sobra lateral cobre o antialiasing da borda da foto */
  left: -1px;
  right: -1px;
  top: 0;
  height: 100%;
  background: var(--bg);
  transform: translateY(calc(var(--rp, 1) * 100%));
  pointer-events: none;
}

/* E QUEM CONDUZ A REVELAÇÃO É O NAVEGADOR, ONDE ELE SOUBER.

   O `--rp` acima é escrito pelo js/efeitos.js a cada quadro de rolagem, e é
   por isso que a foto aparecia aos degraus no celular: no iOS os eventos de
   scroll chegam aos punhados durante um arremesso, então a revelação andava a
   uns 20Hz numa tela de 120Hz. O mesmo problema do trilho e da onda, no mesmo
   lugar de sempre — não no custo, na thread.

   Ligada à rolagem em CSS, ela é amostrada pelo compositor, no mesmo quadro em
   que o dedo se move. As duas propriedades animadas (`opacity` e o
   `translateY` da cortina) são as duas que o compositor anima sozinho, sem
   pintar nada.

   A FAIXA É A MESMA CONTA DE ANTES, escrita na linguagem do navegador. O
   `percurso` do js/efeitos.js conta quantas alturas da própria foto já
   entraram pela borda de baixo — e é exatamente o que a faixa `entry` de uma
   `view-timeline` mede. Os 0,35 e 1,20 do COMECA/TERMINA de lá viram 35% e
   120% aqui, sem conversão nenhuma.

   E A CURVA TAMBÉM É A MESMA. Lá havia um `p*p*(3-2p)` — o smoothstep, que
   suaviza as duas pontas. `cubic-bezier(1/3, 0, 2/3, 1)` é ele exatamente: com
   os controles em x = 1/3 e 2/3 o eixo x fica linear, e os y de 0 e 1 dão
   3t²-2t³ na saída. Não é parecido, é a mesma curva. */
@keyframes retratoCortina{
  from{ transform: translateY(0); }
  to  { transform: translateY(100%); }
}

@supports (animation-timeline: view()){
  .bio-retrato.retrato-scrub{
    view-timeline-name: --retrato;
    view-timeline-axis: block;
  }

  /* Só a cortina anima: a imagem não tem mais nada a fazer. */
  .bio-retrato.retrato-scrub::after{
    animation-name: retratoCortina;
    animation-duration: auto;
    animation-fill-mode: both;
    animation-timing-function: cubic-bezier(.3333, 0, .6667, 1);
    animation-timeline: --retrato;
    animation-range: entry 35% entry 120%;
  }
}

/* Cinto e suspensório: o JS já sai de cena sozinho com "reduzir movimento"
   ligado (e aí a classe acima nem chega a entrar). Isto cobre só o caso de o
   CSS e o script chegarem ao navegador em versões diferentes. */
/* Na FIGURA, e não na imagem: desde que a revelação virou uma cortina, quem lê
   o `--rp` é o `::after` do `.bio-retrato`. Posto na imagem, este cinto de
   segurança não alcançava mais a cortina e a foto ficaria coberta. */
@media (prefers-reduced-motion: reduce){
  .bio-retrato.retrato-scrub{ --rp: 1; }
}

.contato-label{
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--lightgreen);
  margin-bottom: 18px;
}

.bio-texto{
  font-family: 'JetBrains Mono', monospace;
  font-size: clamp(16px, 1.6vw, 22px);
  line-height: 1.75;
  color: var(--white);
  margin: 0;
  max-width: 640px;
}

/* O desfoque sai palavra a palavra CONFORME SE ROLA, não de uma vez quando o
   parágrafo entra na tela. Quem manda é o --prog, escrito pelo js/script.js a
   partir da posição vertical do bloco, e a animação fica pausada num ponto
   escolhido por um `animation-delay` negativo — mesma técnica do jardim.

   O estado de repouso é TEXTO NORMAL: só quem tem a classe `bio-scrub` (posta
   pelo JS) é escondido. Assim, se o script não rodar, o parágrafo continua
   legível em vez de ficar invisível esperando. */
.bio-texto .bio-word{
  display: inline-block;
}

.bio-texto.bio-scrub .bio-word{
  animation-name: bioPalavra;
  animation-duration: .22s;
  animation-timing-function: ease;
  animation-fill-mode: both;
  animation-play-state: paused;

  /* Atraso zero: quem move o ponteiro é o `currentTime` escrito pelo JS. Deixar
     o CSS calcular o atraso a partir de um --prog obrigava a recalcular o
     estilo das 57 palavras em todo quadro de scroll (3,16ms contra 0,055ms).
     Se este arquivo e o script saírem de sincronia o sintoma é claro: as
     palavras ficam paradas no primeiro quadro, invisíveis. */
  animation-delay: 0s;
}

@keyframes bioPalavra{
  from{ opacity: 0; transform: translateY(14px); filter: blur(6px); }
  to  { opacity: 1; transform: translateY(0);    filter: blur(0); }
}

/* ---------- Ações (redes + currículo) ---------- */

.contato-actions{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 40px;
}

/* ---------- Redes sociais — cubo giratório ---------- */

.social-row{
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* a largura do cubo e a das duas faces PRECISAM ser a mesma, senão a face de
   cima (que gira pra frente no hover) fica desalinhada com a da frente. Antes
   eram dois números soltos — e no mobile tinham divergido feio (cubo 490px,
   faces 190px), estourando a largura da tela. Uma variável só resolve. */
.button-icon{
  /* 260px era pouco: medido, o e-mail "fernandomarquecruz@gmail.com" precisa
     de 269px de texto e transbordava 9px pra fora da caixa; o LinkedIn ficava
     a 29px do limite. Com 330px e o respiro lateral das faces, o mais longo
     sobra com folga. */
  --cube-w: 330px;

  display: flex;
  align-items: center;
  overflow: hidden;
  height: 62px;
  border: 3px solid var(--white);
  width: fit-content;
  max-width: 100%;
  cursor: pointer;
  text-decoration: none;
  transition: border-color .3s ease;
}

.button-icon .icon{
  height: 100%;
  background-color: var(--white);
  padding: 0 16px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.button-icon .icon i{
  font-size: 34px;
  color: var(--bg);
}

.button-icon .cube{
  transition: all 0.4s;
  transform-style: preserve-3d;
  width: var(--cube-w);
  height: 100%;
  position: relative;
}

.button-icon:hover{
  border-color: var(--lightgreen);
}

.button-icon:hover .cube{
  transform: rotateX(90deg);
}

.button-icon .side{
  position: absolute;
  height: 100%;
  width: var(--cube-w);
  padding: 0 16px;          /* o texto nunca encosta na borda da caixa */
  display: flex;
  font-size: 1em;
  justify-content: center;
  align-items: center;
  text-transform: uppercase;
  letter-spacing: .5px;
  font-weight: 700;
  font-family: 'JetBrains Mono', monospace;
}

.button-icon .top{
  background: var(--lightgreen);
  color: var(--bg);
  transform: rotateX(-90deg) translate3d(0, 0px, 2.5em);
}

.button-icon .front{
  background: var(--bg);
  color: var(--white);
  transform: translate3d(0, 0, 1em);
}

/* ---------- Botão currículo ---------- */

.cv-button{
  position: relative;
  display: inline-flex;
  align-items: center;
  width: 20rem;
  height: 4.25rem;
  cursor: pointer;
  text-decoration: none;
  margin: 0 auto;
}

.cv-button .circle{
  transition: all .45s cubic-bezier(.65,0,.076,1);
  position: relative;
  z-index: 1;
  display: block;
  width: 4.25rem;
  height: 4.25rem;
  flex-shrink: 0;
  background: var(--lightgreen);
  border-radius: 2.25rem;
}

.cv-button .circle .icon{
  transition: all .45s cubic-bezier(.65,0,.076,1);
  position: absolute;
  top: 0;
  bottom: 0;
  margin: auto;
  background: var(--bg);
}

.cv-button .circle .icon.arrow{
  transition: all .45s cubic-bezier(.65,0,.076,1);
  left: .9rem;
  width: 1.4rem;
  height: .17rem;
  background: none;
}

.cv-button .circle .icon.arrow::before{
  position: absolute;
  content: "";
  top: -.38rem;
  right: .05rem;
  width: .78rem;
  height: .78rem;
  border-top: .17rem solid var(--bg);
  border-right: .17rem solid var(--bg);
  transform: rotate(45deg);
}

.cv-button .button-text{
  transition: color .45s cubic-bezier(.65,0,.076,1);
  position: absolute;
  z-index: 2;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  padding-left: 6rem;
  color: var(--white);
  font-family: 'JetBrains Mono', monospace;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  pointer-events: none;
}

.cv-button:hover .circle{
  width: 100%;
}

.cv-button:hover .circle .icon.arrow{
  background: var(--bg);
  transform: translate(1rem, 0);
}

.cv-button:hover .button-text{
  color: var(--bg);
}

@media (max-width: 900px){
  .contato{ padding: 80px 6vw 40px; }
  .contato-grid{ grid-template-columns: 1fr; gap: 56px; }
  /* Os botões voltam ao centro. Eles estavam encostados na esquerda para
     acompanharem o texto quando as duas colunas viram uma só — mas os quatro
     cubos e o botão do currículo têm larguras diferentes entre si, e
     alinhados à esquerda a borda direita ficava toda serrilhada. Centrados,
     o bloco fecha como um bloco. */
  .contato-actions{ align-items: center; }
  .cv-button{ margin: 0 auto; }
  /* os quatro cubos não têm exatamente a mesma largura (302 a 306px), então
     sem isto eles ficavam uns 2px fora do eixo uns dos outros */
  .social-row{ align-items: center; }
}

@media (max-width: 600px){
  .button-icon{ height: 52px; --cube-w: min(250px, 60vw); }
  .button-icon .side{ font-size: .72em; padding: 0 6px; text-align: center; }
  /* o e-mail é longo demais pra caber inteiro numa face estreita */
  .button-icon .top{
    transform: rotateX(-90deg) translate3d(0, 26px, 1.6em);
    overflow-wrap: anywhere;
    line-height: 1.2;
  }
  .cv-button{ width: 100%; max-width: 20rem; }
}

/* =========================================================================
   TELAS PEQUENAS — passada geral

   Este bloco fica no FIM do arquivo de propósito. As regras de celular
   espalhadas pelas seções acima continuam valendo; o que está aqui vem
   depois e ganha delas quando precisa corrigir alguma. Quem for mexer:
   procure primeiro aqui.

   Três medidas mandam no layout estreito, e elas moram no :root logo abaixo
   pra não haver dois números diferentes dizendo a mesma coisa.
   ========================================================================= */

/* O recorte não é mais só "900px de largura": celular deitado passou disso —
   um iPhone 15 Pro Max em paisagem é 932x430, o 16 Pro Max é 956x440. Pela
   largura eles passavam como desktop e recebiam a parede de fotos e os links
   grandes; medi um 932x430 com o primeiro link em -47 e as redes terminando em
   478. O teto de 960px pega os celulares deitados sem pegar uma janela de
   desktop estreita e baixa (um 1000x600 continua com as fotos). */
@media (max-width: 900px), (max-width: 960px) and (max-height: 620px){
  :root{
    /* a mesma margem lateral pra todas as seções — antes cada uma tinha a sua
       (24px aqui, 6vw ali, 7vw acolá) e as bordas não se alinhavam */
    --gutter: clamp(20px, 6vw, 34px);
    /* o respiro vertical entre seções, para o ritmo da página não depender de
       quem escreveu cada bloco */
    --respiro: clamp(56px, 11vw, 90px);
  }
}


/* -------------------------------------------------------------------------
   1. MENU — o conserto mais importante desta passada

   O menu estava QUEBRADO no celular, não apertado: quebrado. O
   .menu-content é uma linha (fotos | links) e a parede de fotos tem 540px
   fixos que não encolhem. Numa tela de 390px ela sozinha já estoura, e
   empurrava a coluna dos links pra fora — medi os links começando em 650px
   numa tela de 390. Ou seja: dava pra abrir o menu e não havia como navegar.

   No estreito o menu vira uma coluna centrada. A parede de fotos sai de
   cena: são oito imagens de 260x330 que, encolhidas o bastante pra caber,
   não seriam mais do que selos ilegíveis roubando a tela dos links. As
   linhas onduladas do fundo continuam ali e seguram o clima.
   ------------------------------------------------------------------------- */

@media (max-width: 900px), (max-width: 960px) and (max-height: 620px){
  .menu-content{
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: clamp(28px, 6vh, 52px);
    padding: 96px var(--gutter) calc(var(--gutter) + 16px);
  }

  .menu-photos{ display: none; }

  .menu-right{
    --recuo-link: 0px;          /* sem texto à direita, não há o que compensar */
    align-items: center;
    width: 100%;
    gap: clamp(22px, 4vh, 34px);
  }

  .menu-links{
    align-items: center;
    gap: clamp(2px, 1vh, 10px);
    width: 100%;
  }

  /* O padding não é enfeite: com 35px de altura o link era um alvo pequeno
     demais pro dedo. Subindo pra 52px cada item vira um botão confortável, e
     de quebra o texto ganha o respiro que a animação de revelar precisa. */
  .menu-links a{
    display: block;
    padding: 12px 16px;
    font-size: clamp(30px, 8.6vw, 52px);
    line-height: 1.05;
    text-align: center;
  }

  .menu-divider{
    width: min(62%, 260px);
    margin: 0 auto;
  }

  .menu-social-card{ gap: clamp(14px, 4vw, 22px); }
  .ms-icon{ width: 52px; height: 52px; }
  .ms-icon i{ font-size: 19px; }

  /* no toque o :hover cola depois do tap e o ícone fica preso grande */
  .ms-icon:hover{ transform: none; border-radius: 0; }
}

/* A FAIXA DO MEIO — tablet e notebook pequeno.

   Passar de 900px não resolve sozinho: medi o menu numa tela de 945 e os
   links terminavam em 1013, ou seja 68px fora. A parede de fotos tem 540px
   fixos e os links chegam a 67px de corpo; somados, não cabem.

   Aqui a linha continua (as fotos são metade da graça do menu), mas as duas
   coisas encolhem até caber de verdade. Só abaixo de 900 é que as fotos saem
   de cena — daí pra baixo elas não seriam mais do que selos. */
@media (min-width: 901px) and (max-width: 1180px){
  .menu-content{ gap: 26px; padding: 0 4vw; }

  .menu-photos{ gap: 14px; }
  .menu-photo-col{ gap: 18px; }
  .menu-photo-col.offset{ margin-top: 60px; }

  .menu-photos img{
    width: clamp(118px, 17vw, 190px);
    height: clamp(150px, 21.5vw, 240px);
  }

  .menu-links a{ font-size: clamp(30px, 5.2vw, 60px); }
  .menu-links{ gap: 14px; }
}

/* NOTEBOOK LARGO E BAIXO — o caso do 1280x800.

   O corpo dos links sai de `7vw`, ou seja da LARGURA, mas quem aperta aqui é a
   altura: em 1280x800 a coluna media 824px numa tela de 800, com o primeiro
   link cortado no topo e os ícones de rede no pé.

   A coluna mede 6,75 x corpo + 217px, então cabe enquanto
   corpo <= (altura - 217) / 6,75 — 86,4px em 800, 101px em 900. Daí o recorte
   em 860px: acima disso o desenho sempre coube e nada muda.

   O `min-width: 1181px` também é deliberado: abaixo disso vale a faixa do meio,
   que já usa 5,2vw e cabe sem ajuda. Sem esse piso, esta regra AUMENTARIA a
   fonte num 1024x768 em vez de diminuir. */
@media (min-width: 1181px) and (max-height: 860px){
  .menu-links a{ font-size: clamp(24px, min(7vw, 9.2vh), 6rem); }
  .menu-links{ gap: 14px; }
  .menu-right{ gap: 24px; }
}

/* CELULAR DEITADO — o menu precisa CABER, não virar página rolável.

   Num 844x390 o conteúdo media 538px numa tela de 390: o último link terminava
   no pixel 391 e os quatro ícones de rede caíam abaixo da dobra. A raiz é a
   mesma do bloco acima — o corpo saía de `7vw`, e deitado a largura é o que
   sobra e a altura o que falta. Aqui tudo se mede em `vh`.

   O respiro de cima é menor que o botão do menu de propósito: o botão fica no
   canto superior direito e os links são centrados, então nunca se cruzam. */
@media (max-width: 960px) and (max-height: 620px){
  .menu-content{
    justify-content: flex-start;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: clamp(58px, 17vh, 84px) var(--gutter) clamp(12px, 4vh, 24px);
  }

  .menu-right{ gap: clamp(10px, 2.6vh, 20px); }
  .menu-links{ gap: 0; }

  .menu-links a{
    font-size: clamp(19px, 6.2vh, 34px);
    /* o respiro vertical não é enfeite: é ele que mantém o alvo do dedo em
       torno de 40px mesmo com a fonte encolhida */
    padding: clamp(7px, 1.8vh, 12px) 14px;
  }

  .menu-social-card{ gap: clamp(10px, 3vh, 20px); }
  .ms-icon{ width: clamp(40px, 11vh, 52px); height: clamp(40px, 11vh, 52px); }
  .ms-icon i{ font-size: clamp(15px, 4vh, 19px); }
}


/* -------------------------------------------------------------------------
   2. HERO — devolvendo espaço ao texto

   A assinatura é `flex: 1 1 auto`, então ela come tudo que sobra. No
   desktop isso é ótimo. No celular medi a caixa dela com 671px de 844 — 79%
   da tela — e como o traço é largo e a caixa virou um retângulo em pé, a
   assinatura era desenhada pequena no meio de um vazio enorme.

   Aqui ela passa a ter altura própria e o que sobra se reparte em volta:
   o rótulo em cima, a assinatura no meio, o texto que se escreve embaixo.
   ------------------------------------------------------------------------- */

@media (max-width: 600px){
  .nome{
    margin: 24px 0 0 var(--gutter);
    font-size: clamp(11px, 3.1vw, 13px);
    letter-spacing: .06em;
  }

  .assinatura-wrap{
    flex: 0 0 auto;
    height: clamp(170px, 33vh, 300px);
    min-height: 0;
    margin: auto 0;             /* centra no espaço que sobrou */

    /* O recuo lateral era `var(--gutter)` (22,5px de cada lado numa tela de
       375px), e é ele que estava segurando o tamanho: com a assinatura
       limitada pela largura, cada pixel de recuo é um pixel a menos de
       desenho. Encostando quase nas bordas e enchendo a caixa, o traço cresce
       cerca de 28% sem que nada seja cortado — a folga toda estava na
       vertical. */
    padding: 0 6px;
    --assinatura-fill: .96;
  }

  .overlay{ padding: 0 var(--gutter) 7vh; }
}


/* -------------------------------------------------------------------------
   3. SOBRE — o trilho horizontal

   No celular o pin dá lugar a rolagem horizontal nativa (regra lá em cima).
   Funciona, mas nada na tela conta isso. A dica embaixo do primeiro painel
   resolve, e some sozinha assim que a pessoa arrasta — quem já entendeu não
   fica com um aviso pendurado na tela.
   ------------------------------------------------------------------------- */

/* A dica "arraste →" que morava aqui saiu junto com o fallback de rolagem
   nativa: agora os painéis andam com o scroll da página, como no desktop, e
   não há arrasto lateral nenhum pra avisar. */
@media (max-width: 768px){
  /* Mexer no --corpo desce os três juntos, mantendo a proporção.

     O 13vw daqui era conservador demais: media a palavra mais larga do
     conjunto ("SH●W YOU.") em 281px numa faixa util de 367 — sobravam 86px
     de tela sem uso. Agora o teto é a MEDIDA QUE CABE, não um chute:

       (largura da tela - a margem da esquerda - 10px de respiro) / 5.55

     onde 5,55 é quantas vezes essa palavra é mais larga que o corpo da fonte.
     O `min` com 16vw é o mesmo valor do desktop, e serve para dois fins: não
     passar da escala de lá, e não dar um pulo de tamanho ao cruzar o
     breakpoint. Numa tela de 390 sai 62px no lugar de 50,7. */
  .texto{
    margin-left: var(--gutter);
    --corpo: min(16vw, calc((100vw - var(--gutter) - 10px) / 5.55));
  }
  /* MAIS RESPIRO ENTRE UM PAINEL E O SEGUINTE.

     Os painéis têm 100vw cada e os textos são alinhados à esquerda, então o
     vão que se vê ao arrastar é `100vw menos a largura da palavra` — no
     celular isso dava uns 40px entre o "DEVELOPER" e o "I DESIGN.", perto
     demais para as duas lerem como duas coisas.

     Empurrar só os painéis do MEIO aumenta todos esses vãos de uma vez, sem
     mexer no primeiro (que é a abertura e fica encostado na margem). O teto é
     a largura da palavra mais larga desses painéis: são 8 caracteres num corpo
     de `min(16vw, ...)`, o que numa tela de 375px deixa uns 30px de folga
     depois deste empurrão. */
  .hs-panel--mid .texto{ margin-left: calc(var(--gutter) + 8vw); }
}


/* -------------------------------------------------------------------------
   4. PROJETOS, STACK E CONTATO — acerto de escala e de respiro
   ------------------------------------------------------------------------- */

@media (max-width: 600px){
  /* o padding de .projetos NÃO entra aqui: ele é o que faz a seção cobrir a
     tela de pausa do pin, e mexer nele desalinha o encavalamento */
  .portfolio-gallery{
    padding: 0 var(--gutter);
    min-height: 0;
  }

  /* o nome do projeto é a informação principal da linha e estava do tamanho
     de um parágrafo comum */
  .pj-nome{ font-size: clamp(21px, 5.6vw, 26px); }
  .pj-num,
  .pj-tipo{ font-size: 12px; }

  .stack-section{ padding: var(--respiro) 0; }
  .stack-item{ font-size: 11px; gap: 14px; }

  /* 48px de vão numa tela de 390 é meia dúzia de itens virando três. Cortando
     pela metade eles ainda não se encostam — 24px continua sendo mais que o
     dobro do vão interno do próprio item. */
  .stack-marquee-track{ --vao: 24px; }

  /* 8px era pequeno demais pra ler de longe num celular */
  .idioma-nivel{ font-size: 10px; padding: 5px 11px 4px; }
  .idioma-txt strong{ font-size: clamp(13px, 3.8vw, 16px); }

  .contato{ padding: var(--respiro) var(--gutter) 32px; gap: clamp(40px, 9vw, 70px); }

  /* linha de rodapé: era do mesmo tamanho do texto da bio e competia com ele */
  .direitos{
    font-size: 12px;
    line-height: 1.5;
    text-align: center;
  }
}


/* -------------------------------------------------------------------------
   5. O PISO — telas bem estreitas (360px pra baixo)
   Aqui não é questão de estética: é caber.
   ------------------------------------------------------------------------- */

@media (max-width: 360px){
  .menu-links a{ font-size: clamp(26px, 8vw, 34px); }
  .ms-icon{ width: 46px; height: 46px; }
  .ms-icon i{ font-size: 17px; }

  /* --recuo era 24px e a etiqueta do nível saía pela borda direita da tela.

     A conta: o rótulo é centrado numa caixa da largura do PANO (--larg), mas
     "INTERMEDIÁRIO" em 10px com .12em de espaçamento mede 117px, e o pano
     aqui tem 59. Sobram 29px de etiqueta para cada lado da caixa — e do lado
     direito só havia 24px de recuo até a parede. Medi 5px de etiqueta fora da
     tela em 320px e 3px em 360px.

     Empurrar o mastro 8px mais para dentro resolve com folga e não encosta em
     tipografia nenhuma: o pano, o corpo da letra e a etiqueta continuam
     exatamente do mesmo tamanho. O que muda é a bandeira nascer um dedo mais
     longe da parede, que é justamente para o que esta variável existe. */
  .idioma{ --comp: clamp(84px, 25vw, 120px); --recuo: 32px; }

  .button-icon{ --cube-w: min(210px, 58vw); }
}


/* -------------------------------------------------------------------------
   6. TELAS BAIXAS — celular deitado, notebook antigo, janela espremida

   Este bloco precisa morar NO FIM. O `min-height: 480px` do hero e o
   `min-height: 180px` da assinatura são regras de classe como estas, com a
   mesma especificidade — quem vence é quem vier depois. Escrito lá em cima,
   junto do hero, ele perdia justamente para as duas regras que existe para
   corrigir (medi: a assinatura continuava em 180px e o texto digitado saía
   5px para fora do hero, cortado pelo overflow).

   O QUE ELE CONSERTA: num celular em paisagem a tela tem uns 360-430px de
   altura. O piso de 480px do hero passava a ser MAIOR que a tela inteira, e
   a hero — que é `overflow: hidden` — cortava o rodapé da coluna, ou seja, o
   texto que se digita. Sem o piso, a coluna se acomoda no espaço real: o
   rótulo em cima, a assinatura no meio com um piso menor, o texto embaixo.
   ------------------------------------------------------------------------- */

@media (max-height: 520px){
  .hero{ min-height: 0; }
  .assinatura-wrap{ min-height: 120px; }
  .overlay{ padding-bottom: 4vh; }
  .nome{ margin-top: 24px; }
}
