/* ===========================================================================
   CORREDOR — protótipo de galeria em fluxo de profundidade
   ---------------------------------------------------------------------------
   MECANISMO EM UMA FRASE: as imagens existem num espaço com profundidade e
   caminham continuamente na direção do observador; ao passar por ele, cada uma
   se dissolve e renasce no fundo com outro projeto. Ninguém nasce do gesto —
   o gesto MIRA e ACELERA um fluxo que já estava acontecendo.

   AS LEIS DA CASA, RESPEITADAS ASSIM:
   · motion.css decide COMO: todas as TRANSIÇÕES discretas daqui (o crossfade
     do upgrade de nitidez, a legenda, a cortina do intro) usam os tokens
     --m-dur-* / --m-ease-*, copiados verbatim de src/styles/motion.css.
   · o JS decide QUANDO — e aqui ele decide também ONDE, que é coisa diferente
     de "quando". A posição em z é ESTADO CONTÍNUO, não transição: tem
     velocidade (px/s), não duração nem easing. É por isso que não existe
     keyframe nenhum governando o voo.
   · E há um motivo técnico duro para isso, medido no dossiê: o Chrome NÃO
     re-rasteriza uma camada quando a escala muda por CSS animation / Web
     Animations — ele reaproveita um bitmap velho, e a imagem perde 17-24% de
     detalhe em voo. Escrevendo transform por quadro no JS ele re-rasteriza e a
     imagem fica nítida. Num portfólio de direção de arte, isso não é detalhe.

   POR QUE PERSPECTIVE + FILHOS DIRETOS, E NÃO preserve-3d:
   O <header> real tem overflow:hidden, e overflow != visible ACHATA
   transform-style:preserve-3d em silêncio (o getComputedStyle mente e diz
   "preserve-3d" mesmo achatado). Mas isso só morde 3D ANINHADO. Perspective no
   pai + filhos DIRETOS em translate3d funciona perfeito sob overflow:hidden.
   Portanto: .palco tem a perspectiva e o clip; cada folha é filha DIRETA dele.
   Nada de cenário, nada de preserve-3d, nada de depender da classe-hash
   .framer-15ylwyw-container (que é um hash do Framer e pode sumir num
   re-export).

   CONSEQUÊNCIA DISSO, e é o único truque manual do arquivo: com
   transform-style flat, o navegador NÃO ordena os irmãos por profundidade —
   ele pinta na ordem do documento. Então a oclusão (quem tapa quem) é
   resolvida à mão, escrevendo z-index por ordem de z. Ver ordenarZ().
   =========================================================================== */

/* ---------------------------------------------------------------------------
   O PALCO. Reproduz as condições medidas do hero real:
   1440x900 no desktop, overflow hidden, e o gradiente do <header> como
   background PRÓPRIO (não como camada extra) — porque no site ele é o
   background do header e portanto pinta ABAIXO de todos os filhos.

   O gradiente não é enfeite: ele é a razão de várias decisões aqui. A rampa
   vertical vai de ~243 de luminância no topo a 0 no rodapé. Ou seja: o topo do
   hero é quase BRANCO e o rodapé é preto puro. Isso proíbe usar escurecimento
   como pista de profundidade (o fundo troca de polaridade no meio do caminho)
   e obriga o campo a viver numa banda vertical onde as duas polaridades de
   imagem sobrevivem — daí o parâmetro "centro Y".
   --------------------------------------------------------------------------- */
#palco{
  position:absolute; inset:0;
  overflow:hidden;                 /* igual ao header real */
  perspective: var(--p, 1060px);
  /* O PONTO DE FUGA É MÓVEL. --px/--py são escritos pelo corredor a partir
     do rosto de quem está olhando (ver escreverFoco em corredor.js); sem
     câmera eles nunca saem do padrão e isto é exatamente o que sempre foi.

     E é só a ORIGEM que se move: a `.folha` continua ancorada em
     left:50%/top:var(--cy). É essa diferença que faz o efeito ser PARALAXE
     e não panorâmica — cada folha responde por (1 − escala), então o fundo
     acompanha e o que já está passando por você vai ao contrário. Mover a
     âncora junto daria translação rígida, que lê como travelling de câmera
     e joga fora a única pista de profundidade que o gesto produz. */
  perspective-origin: var(--px, 50%) var(--py, var(--cy, 50%));
  background: linear-gradient(#fff -5%, #00000017 100%);
}
#palco.sem-gradiente{ background:#000; }

/* ---------------------------------------------------------------------------
   A FOLHA. Ele disse "ARQUIVOS se movimentando", não partículas — então cada
   peça é uma prancha: proporção REAL da imagem (sem crop), um leve giro em Y
   para ganhar plano próprio, e uma inclinação mínima em Z para não parecer
   um azulejo alinhado. Nada de sombra, nada de borda, nada de moldura: a
   espacialidade tem de sair da própria imagem.
   --------------------------------------------------------------------------- */
.folha{
  position:absolute;
  left:50%; top:var(--cy, 50%);    /* centro do box no ponto de fuga */
  display:block;
  will-change: transform, opacity;
  backface-visibility:hidden;
  transform-style:flat;            /* explícito: cada folha é um plano só */
  text-decoration:none;
  outline:none;
  -webkit-user-drag:none;
}
.folha img{
  position:absolute; inset:0;
  width:100%; height:100%;
  display:block;
  /* contain, não fill. A caixa deve seguir o arquivo; quando as duas
     divergem um quadro (reciclo, poster vs vídeo), fill ESTICA — é o C do
     CADA virando planta com alças. Cover cortaria, e o pedido é o arquivo
     inteiro. Com a razão certa o contain é o mesmo que preencher. */
  object-fit: contain;
}
/* a segunda camada é o upgrade de nitidez (-960) que entra por cima da -640
   quando a folha chega perto. Crossfade com token, porque ISSO é transição. */
.folha img.hi{
  opacity:0;
  transition: opacity var(--m-dur-2) var(--m-ease);
}
/* a terceira camada: o vídeo em loop, por cima do poster, mesma caixa. */
.folha video.vid{
  position:absolute; inset:0; width:100%; height:100%; display:block;
  object-fit: contain;
  opacity:0; pointer-events:none;
  transition: opacity var(--m-dur-2) var(--m-ease);
}
.folha video.vid:not([poster]){ visibility:hidden; }
.folha:focus-visible{
  outline:1px solid rgba(255,255,255,.7);
  outline-offset:6px;
}


/* A folha é um link: o cursor tem de dizer isso. Só as folhas da frente
   recebem pointer-events (o JS decide, ver atualizar()) — no fundo elas são
   pequenas e rápidas, e clicar ali seria loteria. */
.folha[href]{ cursor:pointer; }

/* --- moldura de referência 1440x900 (só para calibrar) -------------------- */
#moldura{
  position:absolute; left:50%; top:50%; width:1440px; height:900px;
  transform:translate(-50%,-50%);
  border:1px solid rgba(255,0,80,.35);
  pointer-events:none; z-index:35; display:none;
}
#moldura.on{ display:block; }


/* --- cortina do intro (simulação do initIntro real) ----------------------
   O initIntro cobre o hero com uma chapa marrom OPACA do primeiro pixel até
   ~1800ms, e só limpa de verdade em ~2700ms. Consequência de projeto: uma
   galeria que arranque vazia e vá enchendo anima 1,7s no escuro e o visitante
   recebe um palco pela metade. Por isso o corredor nasce JÁ POVOADO em toda a
   profundidade — quando a cortina sobe, o acervo já está em movimento. */
#cortina{
  position:absolute; inset:0; z-index:45; background:#2b1b0d;
  transform-origin: 50% 0%;
  pointer-events:none;
}
#cortina.sobe{
  transform: scaleY(0);
  transition: transform 1000ms var(--m-ease-exit);
}
#cortina.fora{ display:none; }

/* --- HUD ------------------------------------------------------------------ */
#hud{
  position:fixed; right:18px; bottom:18px; z-index:60;
  width:294px; max-height:82vh;
  font-size:10px; line-height:1.5; letter-spacing:.02em;
  background:rgba(8,8,8,.82);
  backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  border:1px solid rgba(255,255,255,.13);
  color:rgba(255,255,255,.82);
}
#hud > summary{
  cursor:pointer; padding:8px 11px; list-style:none;
  text-transform:uppercase; letter-spacing:.16em; font-size:9px;
  color:rgba(255,255,255,.5);
  display:flex; justify-content:space-between; align-items:center;
}
#hud > summary::-webkit-details-marker{ display:none; }
#hud[open] > summary{ border-bottom:1px solid rgba(255,255,255,.1); }
#hud .corpo{ padding:6px 11px 11px; overflow-y:auto; max-height:calc(82vh - 34px); }
#hud fieldset{
  border:0; border-top:1px solid rgba(255,255,255,.08);
  margin:9px 0 0; padding:8px 0 0;
}
#hud legend{
  padding:0 0 3px; font-size:8px; letter-spacing:.2em;
  color:rgba(255,255,255,.38); text-transform:uppercase;
}
#hud .lin{ display:grid; grid-template-columns: 1fr 46px; gap:6px; align-items:center; margin:3px 0; }
#hud .lin > label{ color:rgba(255,255,255,.62); }
#hud .lin > b{ text-align:right; font-weight:400; color:#fff; font-variant-numeric:tabular-nums; }
#hud input[type=range]{
  grid-column:1 / -1; width:100%; height:12px; margin:0 0 2px;
  -webkit-appearance:none; appearance:none; background:transparent; cursor:ew-resize;
}
#hud input[type=range]::-webkit-slider-runnable-track{ height:1px; background:rgba(255,255,255,.26); }
#hud input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none; width:9px; height:9px; margin-top:-4px;
  border-radius:50%; background:#fff;
}
#hud .chk{ display:flex; align-items:center; gap:6px; margin:4px 0; color:rgba(255,255,255,.62); cursor:pointer; }
#hud .chk input{ accent-color:#fff; width:11px; height:11px; margin:0; }
#hud .btns{ display:flex; gap:5px; margin-top:9px; flex-wrap:wrap; }
#hud button{
  flex:1 1 auto; font:inherit; font-size:9px; letter-spacing:.1em; text-transform:uppercase;
  padding:5px 6px; color:rgba(255,255,255,.8); background:transparent;
  border:1px solid rgba(255,255,255,.2); cursor:pointer;
}
#hud button:hover{ background:rgba(255,255,255,.09); color:#fff; }
#hud .medidor{
  display:flex; justify-content:space-between; font-variant-numeric:tabular-nums;
  color:rgba(255,255,255,.44); padding-top:7px; margin-top:4px;
  border-top:1px solid rgba(255,255,255,.08);
}
#hud .nota{ color:rgba(255,255,255,.34); padding-top:6px; }

/* ---------------------------------------------------------------------------
   REDUCED MOTION. O hero real é VAZIO — zero texto, zero <img>, zero <video>
   próprios, só o gradiente. Então em reduced-motion o efeito não pode
   DESLIGAR: sobraria uma tela preta. Ele tem de PARAR.
   Aqui isso sai de graça: como o z é estado escrito por rAF, basta não iniciar
   o loop. O campo é montado com as folhas distribuídas em profundidade, com a
   perspectiva intacta, e fica assim — uma composição estática de arquivos em
   corredor. As imagens, que são o conteúdo, continuam lá e continuam
   clicáveis. Tira-se o movimento e fica a composição, não o vazio.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce){
  .folha img.hi{ transition:none; }
  #cortina{ display:none; }
}

/* ===========================================================================
   O SCROLL — hero cravado, conteúdo sobrepondo, acervo adormecendo

   REFERÊNCIA: studionari.co.uk, medido. Hero fixo e o conteúdo subindo por
   cima, com o scroll mexendo em BLUR e OPACITY — nunca em escala.

   POR QUE NÃO ZOOM, que era o pedido original: o corredor já faz 4,5× de
   escala sozinho (medido: 0,284 → 1,288). Somar zoom por scroll no mesmo
   `scale` não soma — SOBRESCREVE: a última animação da lista ganha e o loop
   autônomo congela. Testado. Não é degradação, é o fluxo morrendo. E mesmo
   compondo com animation-composition:add, dois motores no mesmo eixo z é
   exatamente a competição que se quer evitar. O cotton.design usa zoom, mas
   de 1,0 a 1,1 — dez por cento, num hero que não tem movimento próprio.
   Num eixo que já varia 450%, 10% é invisível e o que dá pra ver, briga.

   Blur e opacity são eixos livres. Não encostam no corredor.
   =========================================================================== */

/* Marcados pelo JS com classes próprias, nunca pelas classes-hash do Framer.
   Sem JS nada disso existe e a home rola como sempre rolou. */
/* A classe é repetida de propósito. O Framer estiliza o hero com
   `.framer-raLMF .framer-btwl28` — duas classes, especificidade (0,2,0), que
   ganha de uma `.fel-hero` sozinha (0,1,0) e mantinha o position:relative.
   Repetir empata em (0,2,0), e como corredor.css é o último <link> do <head>
   (depois do <style> inline do Framer), o empate resolve a meu favor.
   É isso ou !important — e !important eu não teria como desfazer depois. */
.fel-hero.fel-hero{
  position: sticky;
  top: 0;
  /* z-index fica o 7 do Framer: o backdrop-filter fixo do topo (z-index 5)
     precisa continuar ABAIXO do hero, senão desfoca o corredor para sempre. */
}

/* O conteúdo sobe por cima. 10 > 7, e os fixed foram pulados na marcação.
   Mesma repetição, mesmo motivo. */
.fel-sobre-hero.fel-sobre-hero{
  position: relative;
  z-index: 10;
}

/* A faixa de blur progressivo do topo tem de ficar acima do conteúdo, senão o
   conteúdo cruza o topo sem desfocar e o vidro que ele já tinha some. 20 é
   acima do conteúdo (10) e bem abaixo da nav (50), que precisa ficar nítida
   por cima de tudo. */
.fel-vidro-topo.fel-vidro-topo{
  z-index: 20;
}

/* O DESFOQUE.
   Fora do @supports não existe animação nenhuma — o hero fica nítido e apenas
   cravado, que é um estado íntegro. Isso NÃO é conservadorismo: uma
   animation-timeline que o navegador não entende computa para `auto`, e aí a
   animação vira time-based e dispara sozinha no load. Sem o gate, quem abre a
   página num navegador sem suporte vê o hero desfocar inteiro de uma vez, no
   primeiro segundo, e ficar assim. Medido.

   E a ordem importa: `animation` (shorthand) ANTES de `animation-timeline`.
   Invertido, o shorthand reseta a timeline para `auto` e cai no mesmo bug. */
@supports (animation-timeline: scroll()) and (animation-range: 0% 100%){
  .fel-hero #palco{
    animation: fel-adormece linear both;
    animation-timeline: scroll(root);
    animation-range: 0 78vh;   /* mesmo fim da rampa da atenção no JS */
  }
}

/* ESCURECER, não só desaparecer. `opacity` sobre o preto do body também
   escurece, mas por subtração — a imagem some. `brightness` escurece por
   redução de luz e preserva a estrutura: o acervo continua legível como
   massa e forma, só que na penumbra. É a diferença entre apagar a luz e
   tirar a foto da parede.

   A opacidade fica bem mais alta que antes (.72 contra .38): quem carrega o
   escurecimento agora é o brightness, e acumular os dois sumia com tudo.

   O gradiente do hero escurece junto — é background do #palco e portanto
   entra no mesmo filter. Proposital: o topo quase branco virando penumbra é
   o que faz a PÁGINA escurecer, não só as imagens. */
@keyframes fel-adormece{
  from{ filter: blur(0px)  brightness(1);   opacity: 1;   }
  to  { filter: blur(20px) brightness(.28); opacity: .72; }
}

/* Reduced-motion: o hero continua cravado e o conteúdo continua sobrepondo —
   isso é LAYOUT, não movimento, e tirar deixaria a página pior. O que sai é a
   rampa de desfoque, trocada por um estado único e estável. */
@media (prefers-reduced-motion: reduce){
  .fel-hero #palco{ animation: none; }
}

/* ===========================================================================
   A NAV — sobe e cola, em vez de sumir

   MEDIDO EM PRODUÇÃO (by-fel.com, scrollY 0 / 300 / 800): a nav fica
   `position: fixed; top: 50px; opacity: 1` nos TRÊS pontos. Ela nunca some.
   O esconder-ao-rolar é do motion.js — uma adição do projeto, não um
   comportamento do site original. Aqui ele é desfeito na home.

   O escopo é `:has(.fel-hero)`, ou seja SÓ a home: /loja e /projects continuam
   com a máquina de estados do motion.js intacta. Mexer no `.m-nav-hidden`
   global mudaria a nav do site inteiro por causa de um pedido sobre o hero.

   Ela sobe até 16px do topo e cola. O 50 do container fixed é o repouso
   do hero, não a barra — colar lá deixava os chips no meio do vidro.
   O movimento é curto de propósito: a nav é chrome, não é o assunto.
   =========================================================================== */
html:has(.fel-hero) [data-site-nav][data-site-nav]{
  transition:
    translate var(--m-dur-2) var(--m-ease),
    opacity   var(--m-dur-2) var(--m-ease);
}

/* O estado "escondido" do motion.js vira, aqui, o estado "colado no topo".
   Mesma classe, mesma máquina de estados, resposta diferente — que é
   exatamente o que o cabeçalho do motion.css promete: "flips m-nav-hidden on
   <html>; every visual consequence is CSS, so restyling the gesture never
   touches logic." Estou usando o gancho como ele foi desenhado. */
html.m-nav-hidden:has(.fel-hero) [data-site-nav][data-site-nav]{
  /* A distância vem medida do JS (ver corredor.js, NAV_TOPO_ALVO). O fallback
     0px é o que vale se a medição não acontecer: a nav fica onde estava, em
     vez de saltar para um número inventado. */
  translate: 0 calc(-1 * var(--fel-nav-subida, 0px));
  opacity: 1;
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce){
  html:has(.fel-hero) [data-site-nav][data-site-nav]{ transition: none; }
}

/* ---------------------------------------------------------------------------
   MODO B — a nav sobe JUNTO com o escurecimento (default, toggle no HUD)

   Diferença real entre os dois, e é de natureza, não de número:

   · MODO A (.fel-nav-junto ausente) — a subida é um EVENTO. O motion.js
     decide (220px de página + 160px de rolagem comprometida) e a nav faz um
     movimento único de 420ms. Ela reage ao seu gesto: rolar pra cima a traz
     de volta na hora, independente de onde você está.

   · MODO B (.fel-nav-junto) — a subida é um ESTADO. Presa à mesma timeline do
     escurecimento, a nav está sempre exatamente onde o scroll manda. Não há
     decisão nem histerese: 40vh de rolagem = nav no topo, sempre igual, e
     voltar meio caminho devolve a nav ao meio do caminho.

   A rampa é 40vh contra os 78vh do escurecimento: a nav chega ao topo bem
   antes do acervo apagar. Ela precisa estar assentada como barra ANTES de o
   fundo virar penumbra, senão as duas coisas terminam no mesmo instante e o
   olho não sabe qual causou qual.
   --------------------------------------------------------------------------- */
@supports (animation-timeline: scroll()) and (animation-range: 0% 100%){
  html.fel-nav-junto:has(.fel-hero) [data-site-nav][data-site-nav]{
    animation: fel-nav-sobe linear both;
    animation-timeline: scroll(root);
    animation-range: 0 40vh;
    /* a transition do modo A brigaria com a timeline: cada quadro do scroll
       viraria um alvo de 420ms e a nav ficaria arrastando atrás do dedo. */
    transition: none;
  }
  /* No modo B a máquina de estados do motion.js não manda na posição — só a
     timeline manda. Sem isso, um gesto pra cima dispararia o translate do
     modo A por cima do da timeline. */
  html.fel-nav-junto.m-nav-hidden:has(.fel-hero) [data-site-nav][data-site-nav]{
    translate: none;
  }
}

@keyframes fel-nav-sobe{
  from{ translate: 0 0; }
  to  { translate: 0 calc(-1 * var(--fel-nav-subida, 0px)); }
}

@media (prefers-reduced-motion: reduce){
  html.fel-nav-junto:has(.fel-hero) [data-site-nav][data-site-nav]{ animation: none; }
}

/* ===========================================================================
   O ENCAIXE DO SCROLL — chegar nas seções, sem sequestrar a rolagem

   `proximity`, NUNCA `mandatory`, e a razão é medida: a faixa dos projetos
   tem 1415px contra uma viewport de 900 — 515px a mais do que cabe. Sob
   `mandatory` o navegador é obrigado a pousar num ponto de encaixe, então
   parar no meio dessa faixa fica IMPOSSÍVEL: o scroll é puxado para o topo
   dela ou para a próxima parada, e o miolo (que é onde as imagens dos
   projetos vivem) vira território inalcançável. `proximity` só encaixa se
   você já parou perto — quem quiser ficar no meio, fica.

   Isso também é o que mantém a rolagem NATIVA. Não existe aqui nenhum
   sequestro de wheel, nenhuma interpolação em JS, nenhum smooth-scroll
   virtual: a física da rolagem continua a do sistema operacional, com a
   inércia do trackpad que o usuário já conhece. O único acréscimo é um imã
   fraco no fim do gesto.

   scroll-padding: 0 porque o hero é sticky e ocupa o topo — não há barra
   fixa comendo altura de onde a seção deveria começar.
   =========================================================================== */
html:has(.fel-hero){
  scroll-snap-type: y proximity;
  scroll-padding-top: 0;
}

/* O marcador é o ponto de encaixe: 1px no topo da seção, sem altura de
   layout, invisível, fora do alcance de ponteiro e de leitor de tela. A
   seção em si NÃO recebe scroll-snap-align — ver a nota em corredor.js sobre
   o encaixe fantasma no fim de seções altas. */
.fel-marca{
  position: absolute;
  top: 0; left: 0;
  width: 1px; height: 1px;
  margin: 0; padding: 0;
  pointer-events: none;
  opacity: 0;
  scroll-snap-align: start;
}

/* Teclado e âncoras ganham a mesma suavidade do encaixe. Isto afeta APENAS
   rolagem programática (setas, PageDown, links de âncora) — a roda e o
   trackpad continuam nativos, como devem. */
@media (prefers-reduced-motion: no-preference){
  html:has(.fel-hero){ scroll-behavior: smooth; }
}

/* Reduced-motion desliga o encaixe inteiro. Um scroll que se move sozinho no
   fim do gesto é exatamente o tipo de movimento involuntário que a
   preferência existe para evitar — e sem ele a página continua perfeitamente
   utilizável, só sem o imã. */
@media (prefers-reduced-motion: reduce){
  html:has(.fel-hero){ scroll-snap-type: none; scroll-behavior: auto; }
}

/* ===========================================================================
   AS FILEIRAS DE PROJETO — perspectiva do recolhimento e grade mais justa

   .fel-fileira é marcada pelo initCardHover (runtime.js) nas mesmas fileiras
   que ele anima — nenhum seletor de hash do Framer.
   =========================================================================== */

/* A perspectiva foi REMOVIDA (26/08). Com rotateY nos cards ela dava
   profundidade real, mas distorcia as imagens — e o recolhimento agora é
   translação pura, então não há rotação para ancorar e `perspective` só
   custaria uma camada de composição sem efeito nenhum. */

/* A GRADE MAIS JUSTA — só na home. O Framer resolve cada fileira com
   justify-content: space-between numa coluna de 1410px, e o vão de 18px é
   RESIDUAL: (1410 − 5×268) / 4 = 17,5 — ninguém escolheu 18, ele sobrou.
   Centro + gap fixo troca o vão residual por um vão escolhido.

   O BURACO DO WRAP (13/09).
   ---------------------------------------------------------------------------
   `flex-wrap: wrap` + cards a 19% + gap de 12px só cabe quando a fileira
   tem pelo menos 960px: 5×19% + 48 = 95% + 48, e 48 ≤ 5% só acima disso.
   Abaixo — 810, 900, um laptop com sidebar — o quinto card cai sozinho
   no centro e a grade vira tiras com vãos pretos do tamanho da página.

   Por cima disso o Framer crava a ALTURA da fileira e do container
   (medido em 1257: 160, 143, 160, 143, 160, 130, 160, 160) enquanto os
   cards, com 67/40, pedem 139. Uma fileira de 130 recorta; uma de 160
   sobra. O ritmo vertical mente mesmo quando o horizontal está certo.

   A correção é uma fileira FLUIDA de cinco, daqui pra cima (810):
   nowrap, cards `flex:1 1 0` na razão 67/40, altura pelo conteúdo.

   VÃO E BORDA SÃO O MESMO NÚMERO (Felipe, 13/09): 10px. Antes o vão
   era 12 e a moldura 24 — "maior que o vão, para ler como bloco".
   Ele pediu a mesma distância nos dois, então o campo passa a ser
   uma malha só: 10 entre cards, 10 entre fileiras, 10 até a borda.

   O initCardHover mede DEPOIS do CSS (cacheRest + resize), então o
   recolhimento pousa nos lugares novos. Mobile (<810) fica com a
   variante empilhada do Framer: esta regra não passa por cima dela. */
@media (min-width: 810px) {
  html:has(.fel-hero) [data-framer-name="PROJECTS"]{
    padding: 10px;
    row-gap: 10px;
  }
  html:has(.fel-hero) .fel-fileira{
    justify-content: center;
    align-items: stretch;
    gap: 10px;
    flex-wrap: nowrap;
    height: auto !important;
    width: 100%;
    box-sizing: border-box;
  }
  html:has(.fel-hero) :has(> .fel-fileira){
    height: auto !important;
    aspect-ratio: auto !important;
  }
  html:has(.fel-hero) .fel-fileira .fel-card.fel-card.fel-card{
    flex: 1 1 0;
    width: auto;
    min-width: 0;
    max-width: none;
    aspect-ratio: 67 / 40;
    height: auto;
  }
}

/* O MESMO VÃO NA VERTICAL (01/09). A regra acima escolheu 12px entre os cards
   de uma fileira, mas o vão ENTRE fileiras continuou nos 18px do Framer — e
   18 também é residual ali, pelo mesmo motivo: é a sobra da coluna, não uma
   escolha. O olho lê a grade como um campo só, então dois vãos diferentes
   aparecem como desalinho (medido: 12 na horizontal contra 18 na vertical).

   Mexer no `row-gap` do container é seguro e cirúrgico: medidos os 17 itens
   dele, os únicos VISÍVEIS são as 8 fileiras de projeto e um marcador de 1px
   (.fel-marca) — os outros 8 são variantes de breakpoint em display:none, que
   não geram vão. Nenhuma outra seção da home divide este container. */
html:has(.fel-hero) .fel-sobre-hero{
  row-gap: 10px;
}

/* ===========================================================================
   O APAGAMENTO ATÉ O PRETO — segunda fase, e a razão de ela existir

   `brightness()` MULTIPLICA. O gradiente do hero começa em #fff, então
   brightness(.28) × opacity(.72) = 51 — cinza #333, não preto. O rodapé do
   site é #000 opaco. Onde um encosta no outro nascia uma linha dura de cor,
   e ela ficava visível por ~1800px de rolagem, porque a primeira rampa
   termina em 78vh e o resto da página inteira seguia cinza.

   Uma camada preta com opacity 0 → 1 chega a #000 EXATO, que é a mesma cor
   do rodapé: a emenda deixa de existir em vez de ficar disfarçada.

   Por que não simplesmente brightness(0): a primeira fase é o que dá a
   PENUMBRA — o acervo ainda legível como massa atrás do texto — e ela
   precisa terminar cedo (78vh). O apagamento até o preto termina tarde.
   São tempos diferentes, então são camadas diferentes.

   RECALIBRADO (26/08, pedido do Felipe): "no pixel do texto centrado, o
   site já deve estar 90% escuro." O pixel dele é scroll = 100vh — onde o
   snap pousa a viewport do texto. A rampa única antiga (78vh → rodapé, ~2481px)
   estava em ~11% nesse ponto: quase toda a escuridão chegava tarde demais.

   Virou DUAS fases, duas camadas:
   · ::after — 0 → 0,9 entre 78vh e 100vh. O mergulho. Termina exatamente no
     pixel pedido, com o valor pedido.
   · ::before — 0 → 1 entre 100vh e o rodapé (medido). Os 10% finais, lentos,
     garantem o preto PURO na chegada do rodapé — a razão original desta
     camada (a emenda invisível com o #000 do footer) continua valendo.
   As duas se compõem: no fim, o ::before sozinho já é opaco.
   =========================================================================== */
/* O APAGAMENTO NÃO É CHAPADO — o topo fica levemente aberto.

   Pedido do Felipe (10/09), olhando a segunda tela da home (o "FEL is a
   graphic designer…"): "que o degradê ainda apareça de uma forma bem suave,
   como se só o topo ali ainda continuasse levemente transparente".

   Eram duas chapas #000 sólidas, uma indo a .9 e a outra a 1. Somadas, elas
   fecham a tela inteira e o corredor desaparece por completo — o que é
   correto para o rodapé e exagerado para o alto, onde a memória do que estava
   passando é o que dá profundidade ao texto.

   Agora as duas carregam o MESMO gradiente vertical: 55% de preto na primeira
   linha, preto pleno a partir de 38% da altura. Como as duas se multiplicam,
   a transmitância no topo com ambas no fim da rampa é
   (1 − .9×.55) × (1 − 1×.55) ≈ 23% — e 23% de um corredor que já está com
   blur de 20px, brilho em .28 e opacidade .72 é exatamente o fantasma que ele
   descreveu. Do meio da tela para baixo nada muda: continua preto fechado.

   O mesmo gradiente nas duas, e não só na de cima: se apenas uma abrisse, a
   outra fecharia o topo sozinha e o efeito não existiria. */
.fel-hero::after,
.fel-hero::before{
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    rgba(0,0,0,.55) 0%,
    rgba(0,0,0,.88) 22%,
    #000 38%);
  opacity: 0;
  pointer-events: none;
  /* acima do palco (auto) e da legenda (40), dentro do hero — que é z-index 7,
     portanto tudo isto continua abaixo do conteúdo (10) e do vidro (20). */
  z-index: 41;
}

/* DEPOIS DA SEÇÃO SOBRE MIM, PRETO PLENO — e é por isso que só UMA das duas
   chapas carrega o gradiente.

   Eu tinha posto o mesmo gradiente nas duas, e estava errado: elas cobrem
   trechos DIFERENTES da rolagem. A `::after` ramp de 78vh a 100vh — é ela
   que está em cena na tela do "FEL is a graphic designer", onde o Felipe
   quis o topo aberto. A `::before` só começa em 100vh, ou seja, DEPOIS
   daquela tela — e ali ele quer escuro total.

   Então: gradiente na de cima, preto chapado na de baixo. A soma dá
   exatamente o pedido — topo respirando enquanto o texto está na tela, e
   fechado assim que ele passa. */
.fel-hero::before{
  background: #000;
}


@supports (animation-timeline: scroll()) and (animation-range: 0% 100%){
  .fel-hero::after{
    animation: fel-apaga linear both;
    animation-timeline: scroll(root);
    /* do fim da penumbra ao pixel do texto: o grosso do apagamento. */
    animation-range: 78vh 100vh;
  }
  .fel-hero::before{
    animation: fel-apaga-resto linear both;
    animation-timeline: scroll(root);
    /* do pixel do texto até o rodapé entrar na tela — fim MEDIDO em
       corredor.js (apagaFim); 260vh é só o fallback se a medição faltar. */
    animation-range: 100vh var(--fel-apaga-fim, 260vh);
  }
}

@keyframes fel-apaga{
  from{ opacity: 0; }
  to  { opacity: .9; }
}
@keyframes fel-apaga-resto{
  from{ opacity: 0; }
  to  { opacity: 1; }
}

/* Reduced-motion: só `animation: none`, e a opacity volta sozinha para o 0 da
   regra base (linha 530) — que é o estado INICIAL da rampa.

   Tinha `opacity: 1` aqui, que é o estado FINAL: duas chapas #000 opacas em
   z-index 41 cobrindo o corredor em qualquer posição de scroll, inclusive no
   topo. Ou seja, a home nascia PRETA para quem liga "reduzir movimento" — o
   oposto exato do que o comentário de REDUCED MOTION (linha 207) estabelece:
   "o efeito não pode DESLIGAR: sobraria uma tela preta. Ele tem de PARAR...
   Tira-se o movimento e fica a composição, não o vazio."
   O bloco irmão do #palco/#legenda (linha 323) já usava o critério certo. */
@media (prefers-reduced-motion: reduce){
  .fel-hero::after,
  .fel-hero::before{ animation: none; }
}

/* ===========================================================================
   O VIDRO — não mora aqui, e essa é a correção

   Pedido (28/08): "quero que os botoes de nav e hover tenha o mesmo aspecto
   do botão do meu site atual, o mesmo frost effect."

   A resposta é que ele JÁ ERA aquele. A receita do ar está capturada
   verbatim em framer-runtime.css, keyed na classe de variante que o Framer
   usa como discriminador — blur(60px), branco a 20% em repouso, preto opaco
   no hover, e as variantes DARK e LIGHT para os outros contextos.

   O QUE ESCONDIA isso era um override daqui: uma classe `.fel-glass`
   triplicada (0,3,0) que o motion.js pregava nas pílulas da nav e que
   repintava tudo com um vidro líquido de três camadas, borda-gradiente e
   refração por filtro SVG. Só que o motion.js marca apenas os links DENTRO
   da nav — então READ MORE e SEE ALL PROJECTS, que são o mesmo componente
   (.framer-1x0lp31), seguiram com o frost do ar. O site tinha dois vidros
   diferentes para o mesmo botão, e o que divergia era justamente a nav.

   Removido o override, a nav volta sozinha para a captura do ar e os três
   passam a concordar. Nada aqui redeclara a receita: uma fonte só, em
   framer-runtime.css, que é o arquivo cujo contrato é guardar o que foi
   medido. O badge dos projetos também vive lá, na variante LIGHT.
   =========================================================================== */

/* ===========================================================================
   O MOTOR DE ROLAGEM (ver corredor.js, "1½. O MOTOR") — o que sai da frente

   Quando o motor assume (html.fel-motor, posto pelo JS quando não é touch e
   não é reduced-motion), duas coisas nativas PRECISAM sair:

   · scroll-snap-type — o proximity nativo ajusta a posição por conta própria
     depois de cada operação de scroll, e o motor escreve scrollTop a cada
     quadro: os dois brigariam pelo mesmo valor. O ímã continua existindo,
     reimplementado dentro do motor sobre as mesmas .fel-marca, com o mesmo
     raio de 96px que os testes do proximity validaram.
   · scroll-behavior: smooth — transformaria cada write do motor numa
     animação própria do navegador, e a página escorregaria atrás do dedo.

   Sem o motor (touch, reduced-motion), a classe não existe e o comportamento
   antigo — snap nativo + smooth para teclado — segue intacto. As duas regras
   abaixo vêm DEPOIS de html:has(.fel-hero) no arquivo de propósito: mesma
   especificidade, ordem decide.
   =========================================================================== */
html.fel-motor,
html.fel-motor:has(.fel-hero){
  scroll-snap-type: none;
  scroll-behavior: auto;
}

/* ===========================================================================
   O RODAPÉ QUE SOBE — sai do fluxo e passa POR CIMA dos cards

   Pedido (28/08): "quero que o footer do site surja da base numa animação em
   que ele sobrepõe os cards dos projetos, e que ele fique tela cheia, e
   quando a gente chegar no footer, o nav pode desaparecer."

   COMO: o rodapé se descola do fluxo (fixed, colado na base, 100svh) e sobe
   de translateY(100%) até 0 ao longo dos ÚLTIMOS 100svh de rolagem. Por ser
   fixed com z-index acima do conteúdo, ele PASSA POR CIMA dos cards em vez
   de empurrá-los — que é a sobreposição pedida. O bloco que ele deixou vira
   espaçador de 100svh, então a altura do documento não muda: medido, 4282px
   antes e 4282px depois. Sem isso a página encolheria 900px e o fim do
   percurso mudaria de lugar.

   POR QUE QUEM ANIMA É O `html`, e não o rodapé: mesmo motivo que já aparece
   duas vezes neste arquivo — um elemento `fixed` não é descendente do
   scroller para efeito de scroll-driven animation, e a timeline devolve
   progress **null**, em silêncio. Então o `html` anima um número só
   (--rodape-t) e o rodapé CONSOME esse número no transform. @property é o
   que torna o número interpolável; sem registrar o tipo ele saltaria de 0
   para 1 em vez de percorrer.

   ESPECIFICIDADE TRIPLA (0,3,0): o Framer estiliza o bloco com duas classes-
   hash (0,2,0). Medido: com classe simples o `position` NÃO passava — só o
   `transform` — e o rodapé era empurrado 900px para baixo DENTRO do fluxo,
   crescendo o documento em 899px. O sintoma é silencioso; a causa é cascata.

   TUDO DENTRO DO @supports: sem scroll-driven animations o rodapé fica no
   fluxo normal, como sempre foi. Nada de rodapé fixo preso em translateY
   sem nada para animá-lo — que seria um rodapé permanentemente fora da tela.
   =========================================================================== */
@property --rodape-t{
  syntax: '<number>';
  inherits: true;
  initial-value: 0;   /* 0 = escondido embaixo; 1 = tela cheia */
}

/* O CURSO — quanta ROLAGEM o rodapé consome para subir 100svh.
   ---------------------------------------------------------------------------
   Este número é a diferença entre "o rodapé chega" e "o rodapé SOBREPÕE", e
   a razão de ele existir merece a explicação (03/09, pedido do Felipe: "ele
   não tá sobrepondo").

   Antes o espaçador e o curso eram os dois 100svh. Nessa igualdade o conteúdo
   sobe 900px enquanto o rodapé sobe 900px — os dois na mesma velocidade — e o
   topo do rodapé fica COLADO no fim do conteúdo o percurso inteiro. Medido em
   /teva-indices a t=0,5: fim do CARDS-2 em 449,45 e topo do rodapé em 450,00.
   Não é quase sobrepor: é sobreposição zero, por construção geométrica.

   Com o curso menor que a altura do rodapé, ele sobe MAIS RÁPIDO que a página
   rola e ganha terreno sobre o conteúdo. Em 50svh a razão é 2:1 — meia tela
   de sobreposição real, que é o gesto pedido lá atrás ("que ele sobreponha os
   cards dos projetos") e que a igualdade anterior nunca chegou a entregar.

   Um número só, nos dois lugares: a `animation-range` PRECISA casar com a
   altura do espaçador, senão a subida começa antes ou depois do trecho de
   rolagem que o espaçador criou. Deixar os dois amarrados na mesma variável
   é o que impede de mexer num e esquecer o outro. */
/* A FOLGA — quanta rolagem passa com o último projeto inteiro na tela ANTES
   de o rodapé começar a subir.
   ---------------------------------------------------------------------------
   Pedido do Felipe (05/09): "o footer precisa demorar um pouquinho mais para
   entrar, a gente não consegue ver o último projeto completo".

   Ele estava certo, e a medição mostra o quanto: com folga zero, o espaçador
   e o curso eram o mesmo trecho, então a subida começava no instante exato em
   que o último projeto terminava de aparecer. Medido em 1440×900, com a
   última fileira de 206px — ela fica inteira e descoberta numa janela de UM
   pixel de rolagem (base em 899, topo do rodapé em 900). Um pixel depois o
   rodapé já cobre 69px dela.

   A folga cria um trecho em que t continua 0: o espaçador fica MAIOR que o
   curso, e a `animation-range` segue medindo só os últimos `curso` pixels. O
   resultado é um respiro — 30svh, ~1,3 vez a altura de uma fileira — em que o
   último projeto sobe do rodapé da tela para o meio dela, sozinho, antes de o
   rodapé encostar.

   Dois números, dois efeitos, e vale não confundir na hora de calibrar:
     --fel-rodape-folga  atrasa a ENTRADA (mais folga = mais tempo olhando o
                         último projeto)
     --fel-rodape-curso  muda a VELOCIDADE da subida e quanto ela sobrepõe
                         (menos curso = sobe mais rápido, cobre mais) */
:root{
  --fel-rodape-curso: 50svh;
  --fel-rodape-folga: 30svh;
}

@supports (animation-timeline: scroll()) and (animation-range: 0% 100%){
  html:has(.fel-rodape){
    animation: fel-rodape-t linear both;
    animation-timeline: scroll(root);
    /* o último trecho do percurso — exatamente o espaço que o espaçador
       criou, então a subida termina quando a rolagem termina. */
    animation-range: calc(100% - var(--fel-rodape-curso)) 100%;
  }

  .fel-rodape-espaco.fel-rodape-espaco{
    /* folga + curso: o espaçador é MAIOR que a subida de propósito. A parte
       de cima dele é o respiro (t continua 0); só a parte de baixo, do
       tamanho do curso, é que a `animation-range` observa. */
    height: calc(var(--fel-rodape-folga) + var(--fel-rodape-curso));

    /* PRETO ATRÁS DO RODAPÉ — a linha entre as seções.
       O espaçador tem UM único filho, o próprio rodapé (verificado), então
       pintá-lo não afeta mais nada, e ele fica sempre coberto pelo rodapé —
       menos por uma fresta de sub-pixel.

       É essa fresta que aparecia. Medido em /teva-indices: o CARDS-2 termina
       em 449,45 e o rodapé começa em 450,00 — 0,55px em que nenhum dos dois
       pinta, e por baixo mora o fundo BRANCO da página (framer-gdhRZ, e o
       próprio body). Meio pixel de branco entre dois pretos lê como um filete
       claro atravessando a tela inteira.

       Na home a mesma fresta existe (medido: 448,91 contra 450,00), mas o
       apagamento até o preto do hero já pinta preto ali — por isso o defeito
       só aparecia nas páginas de projeto, que não têm hero.

       Preto no espaçador ataca a CAUSA (o que vaza pela fresta) em vez de
       empurrar o rodapé 1px para cima, que seria esconder o sintoma e
       quebrar de novo em qualquer zoom ou densidade de tela diferente. */
    background: #000;
  }

  .fel-rodape.fel-rodape.fel-rodape{
    position: fixed;
    left: 0; right: 0; bottom: 0; top: auto;
    width: auto;
    height: 100svh;            /* tela cheia, como pedido */
    z-index: 55;               /* ACIMA DA NAV, que o motion.css fixa em 50 com
                                  a nota "must never be covered". Aqui ela é
                                  coberta de propósito: é o pedido. Abaixo do
                                  HUD (60), que é ferramenta e não página. */
    transform: translateY(calc((1 - var(--rodape-t, 0)) * 100%));
    will-change: transform;
  }

  /* A NAV SOME quando o rodapé chega. Sai no primeiro quarto da subida (o
     ×4), para não ficar boiando sobre o rodapé durante a travessia.

     SEM clamp(): a primeira versão era `clamp(0, calc(1 - var(...) * 4), 1)` e
     não pintava nada — medido, a nav ficava em opacity 1 o percurso inteiro.
     Com var() dentro, a expressão vira inválida em tempo de VALOR COMPUTADO,
     e nesse caso a propriedade não cai para a regra anterior: cai para o
     valor inicial, que em opacity é 1. Falha silenciosa, sem erro no console.
     E o clamp era supérfluo desde o começo — opacity já satura em [0,1]
     sozinha, então basta a conta.

     OS DOIS SELETORES existem por especificidade, não por capricho. O estado
     "colada no topo" da nav (linha 355) é
       html.m-nav-hidden:has(.fel-hero) [data-site-nav][data-site-nav]
     que dá (0,4,1) e declara `opacity: 1` — e é justamente o estado em que a
     nav está quando o rodapé chega. Com um seletor só, de (0,3,1), a conta
     abaixo perdia e a nav ficava opaca o percurso inteiro. Medido: opacidade
     1 em t=0, t=0,13 e t=1. O segundo seletor empata em (0,4,1) e ganha por
     vir depois no arquivo — sem !important, que aqui seria esconder a causa.

     Sem pointer-events: com o rodapé em z-index 55 e a nav em 50, quando ela
     está invisível o rodapé já está por cima e é ele quem recebe o clique. */
  html:has(.fel-rodape) [data-site-nav][data-site-nav],
  html.m-nav-hidden:has(.fel-rodape) [data-site-nav][data-site-nav]{
    opacity: calc(1 - var(--rodape-t, 0) * 4);
  }
}

@keyframes fel-rodape-t{
  from{ --rodape-t: 0; }
  to  { --rodape-t: 1; }
}

/* Reduced-motion: sem subida. O rodapé volta para o fluxo e o espaçador
   deixa de existir — a página fica como sempre foi, e a nav não desaparece.
   Tirar o movimento não pode tirar o conteúdo. */
@media (prefers-reduced-motion: reduce){
  html:has(.fel-rodape){ animation: none; }
  .fel-rodape.fel-rodape.fel-rodape{
    position: relative;
    transform: none;
    height: auto;
  }
  .fel-rodape-espaco.fel-rodape-espaco{ height: auto; }
  html:has(.fel-rodape) [data-site-nav][data-site-nav],
  html.m-nav-hidden:has(.fel-rodape) [data-site-nav][data-site-nav]{ opacity: 1; }
}

/* ===========================================================================
   OS CARDS DAS FILEIRAS — todos exatamente do mesmo tamanho

   Pedido (28/08): "corrija o tamanho dos cards. eles devem ter exatamente o
   mesmo tamanho, e quando alinhados ao centro, as imagens por trás com
   exatamente os mesmos tamanhos."

   O QUE ESTAVA ACONTECENDO: o Framer dá a cada card uma altura derivada da
   PROPORÇÃO NATIVA do vídeo dentro dele. A largura é a mesma para todos (a
   coluna da grade, 268px em 1440), mas a altura não — medido na home:
     fileira 0  vídeo 720/442  → 160
     fileira 1  vídeo 720/404  → 165
     fileira 5  vídeo 720/450  → 130
     fileira 5, a CAPA central, vídeo 1280/658 → 136
   Ou seja, alturas diferentes entre fileiras E dentro da mesma fileira: a
   capa (o card que fica centralizado, com translate(-50%,-50%)) tinha 136
   contra 130 dos outros quatro. Quando o hover junta tudo no centro, era
   exatamente esse desencontro que aparecia — as imagens por trás sobrando
   alguns pixels em relação à da frente.

   A CORREÇÃO é uma proporção única para o frame. A mídia dentro já é
   `width:100%; height:100%; object-fit:cover`, então ela RECORTA para caber
   em vez de distorcer — nenhuma imagem estica.

   67/40 = 268x160, que é a proporção dominante hoje (5 das 8 fileiras). A
   escolha é essa para o ajuste ser o menor possível: as fileiras que já
   estavam em 160 não mudam nada.

   `height: auto` junto porque o Framer declara altura explícita no frame, e
   altura explícita + largura vencem aspect-ratio — sem zerar a altura, a
   proporção não pinta. Classe tripla (0,3,0) pela mesma razão de sempre: o
   Framer estiliza com duas classes-hash.
   =========================================================================== */
.fel-card.fel-card.fel-card{
  aspect-ratio: 67 / 40;
  height: auto;
}

/* E o MIOLO tem de preencher o card. Uniformizar só o frame não bastava: a
   CAPA (o card que fica centralizado) tem um wrapper próprio dentro dele com
   `aspect-ratio: 1.97037/1` e `position: absolute` — medido, o frame ia para
   160 e esse wrapper continuava em 136, então a imagem da frente ficava menor
   que as de trás justamente no estado convergido, que é onde isso aparece.

   O `:has()` mantém a regra cirúrgica: só wrappers que de fato carregam mídia
   são esticados. Sem ele, qualquer filho do card (rótulo, sobreposição)
   também seria forçado a 100% de altura. */
.fel-card.fel-card.fel-card > *:has(video, img),
.fel-card.fel-card.fel-card > video,
.fel-card.fel-card.fel-card > img{
  aspect-ratio: auto;
  height: 100%;
}

/* ===========================================================================
   A COLUNA DO NOME DO PROJETO — 100px mais estreita

   Pedido (28/08): "quero estreitar a coluna com o nome do projeto. Acho que
   podemos tirar uns 100 px."

   O Framer declara `width: 30%` nessa coluna. Em 1440 a fileira mede 1430 de
   conteúdo, então 30% = 429px — e 23% = 329px, exatamente os 100 pedidos. Fica
   em PORCENTAGEM, e não `calc(30% - 100px)`, porque a coluna precisa encolher
   junto com a janela: um desconto fixo de 100px viraria 130px de coluna em
   768, e as fichas ("BRAND STRATEGY", "MARKET ANALYSIS") empilhariam uma por
   linha. A área dos cards é `flex: 1`, então ela absorve os 100px sozinha.

   A ESPECIFICIDADE AQUI É O PONTO DELICADO, e é por isso que a classe é dupla:
   ela precisa ficar ENTRE duas regras do Framer.
     · a base `.framer-1ikri2k{width:30%}` é (0,1,0) — esta regra tem de vencer;
     · mas nos breakpoints empilhados o Framer manda
       `.framer-NLj4w.framer-v-1luua83 .framer-1ikri2k{width:100%}`, que é
       (0,3,0) — e essa tem de continuar vencendo, ou o mobile perderia o
       empilhamento e ficaria com uma coluna estreita no lugar de largura
       cheia.
   Classe dupla dá (0,2,0): passa por cima da base e passa por baixo das
   variantes. Triplicar aqui quebraria o mobile.
   =========================================================================== */
:root{
  /* A LARGURA DA COLUNA DE NOME, em UM lugar só.

     Ela governa duas coisas que precisam concordar: a coluna de cada fileira
     e a coluna equivalente na barra do topo (a que reserva o espaço à
     esquerda do "SELECTED WORK").      O Framer dava 30% às duas. Uma passagem estreitou para 0.23 (~100px a
     menos) e as tags passaram a quebrar em 3 linhas, empurrando a fileira.
     30% é o número do export e o que o hover do site ao vivo usa.

     Sem unidade para poder ser usada em conta: a fileira multiplica por 100%,
     a barra multiplica pela largura de conteúdo. */
  --fel-col-nome: 0.30;
}

.fel-coluna-nome.fel-coluna-nome{
  width: calc(var(--fel-col-nome) * 100%);
}

/* A linha de hover mora em [data-framer-name="TEXT"], que o Framer já
   declara como coluna space-between (nome+tags no topo, frase embaixo).
   O SSR não traz a frase — o React do Framer a injetava no hover.
   runtime.js (initProjectsHover) recoloca o <p.fel-hover-line>. */
/* A FRASE É PRÊMIO DE HOVER, e onde não há hover ela não existe.

   Até 11/09 era o contrário: sem hover ela ficava SEMPRE visível, para não
   sumir de quem não tem ponteiro. A intenção era boa e o resultado não —
   "a versão mobile tá muito pesada com o texto conceito, mantenha só as
   tags" (Felipe). No empilhado, nome + seis tags + uma frase de 34
   caracteres por fileira viram um paredão de texto antes da primeira
   imagem, e a frase é a única das três que não é informação de catálogo.

   `display: none` e não `opacity: 0`: é preciso tirar do FLUXO, não só da
   vista. O peso que ele descreve é altura, e opacidade não devolve altura.

   Desktop (15/09): as tags ficam no descanso. No hover elas saem e fica
   só a frase, pregada na BASE da coluna. */
.fel-hover-line{
  display: none;
  margin: 0;
  padding: 0 2px 2px;
  font-family: 'Maxeville Regular', 'Maxeville Construct', sans-serif;
  font-size: 14px;
  line-height: 1.3;
  color: #fff;
  max-width: 34ch;
}

@media (hover: hover) and (min-width: 810px){
  .fel-fileira:has(.fel-coluna-nome) [data-framer-name="TEXT"]{
    position: relative;
  }
  /* Fora do fluxo: se a frase ocupasse 38px no pé, o overflow:hidden
     fatiava a última linha de tags (medido 16/09, VISUAL IDENTITY / LOGO
     cortadas no meio). Absoluta no fundo, as tags usam a coluna inteira. */
  .fel-fileira:has(.fel-coluna-nome) .fel-hover-line{
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    opacity: 0;
    margin: 0;
    transition: opacity var(--m-dur-1, 200ms) var(--m-ease, ease);
  }
  .fel-fileira:has(.fel-coluna-nome) [data-framer-name="TEXT"] > * > :not([data-framer-name="Button"]){
    transition: opacity var(--m-dur-1, 200ms) var(--m-ease, ease);
  }
  .fel-fileira:has(.fel-coluna-nome):hover .fel-hover-line,
  .fel-fileira:has(.fel-coluna-nome):focus-visible .fel-hover-line{
    opacity: 1;
  }
  /* O wrap das tags é o irmão do chip do nome (`data-framer-name="Button"`).
     Hash de classe do Framer muda no re-export; o nome do botão não. */
  .fel-fileira:has(.fel-coluna-nome):hover [data-framer-name="TEXT"] > * > :not([data-framer-name="Button"]),
  .fel-fileira:has(.fel-coluna-nome):focus-visible [data-framer-name="TEXT"] > * > :not([data-framer-name="Button"]){
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }
}

/* A COLUNA DA BARRA DO TOPO. A conta não é a mesma porcentagem simples: a
   fileira tem 5px de padding e mede os 23% sobre o conteúdo, a barra não tem
   padding e mede sobre a largura cheia. Reproduzir a fórmula da fileira —
   padding + 23% do conteúdo — faz as duas caírem no MESMO pixel, em vez de
   ficarem a 2-3px uma da outra. */
/* SÓ ACIMA DE 810px, que é o breakpoint do PRÓPRIO Framer — o export declara
   `@media (min-width:810px)` e `@media (max-width:809.98px)`, então o número
   não é chute meu, é o dele.

   Abaixo disso o Framer empilha o layout: a coluna da FILEIRA volta a 100% por
   uma regra de variante (0,3,0) que a minha, dupla (0,2,0), perde de propósito.
   A da barra não tinha adversário equivalente e continuava em 30% mesmo
   empilhada — medido em 768: fileiras já empilhadas e a barra ainda a 23%,
   espremendo a nav contra o "SELECTED WORK". Envolver na mesma media query dá
   à barra a saída de mobile que a fileira já tinha. */
@media (min-width: 810px){
  .fel-cab-coluna.fel-cab-coluna{
    width: calc(5px + (100% - 10px) * var(--fel-col-nome));
    flex: 0 0 auto;
  }
}

/* ===========================================================================
   A TIRA DE /projects — corre para a esquerda no hover, com borda chapada

   Pedido (28/08). O JS (runtime.js, initTiras) pendura as imagens extras na
   área que o Framer já usa para os cards e persegue um alvo em `scrollLeft`.
   Aqui ficam só as coisas que são de CSS.

   1. NOWRAP. A área nasce `flex-wrap: wrap` — com mais imagens do que cabe,
      elas quebrariam para uma segunda linha em vez de sair pela direita. É a
      única declaração que realmente destrava o efeito.

   2. BORDA CHAPADA. Houve aqui um `mask-image` esfumando 96px em cada ponta;
      saiu a pedido (28/08: "não quero esse esfumaçado, deixe 100% flat"). O
      corte agora é seco, feito pelo `overflow: hidden` da própria área.

   3. ROLAGEM MANUAL EM REDUCED-MOTION. Sem a corrida, as imagens extras
      ficariam inalcançáveis — conteúdo escondido atrás de um movimento que a
      pessoa desligou. Então a área vira rolável no eixo X. Tirar o movimento
      não pode tirar o acesso.
   =========================================================================== */
.fel-tira.fel-tira{
  flex-wrap: nowrap;
  scrollbar-width: none;

  /* A ÁREA É O CONTAINER DE MEDIDA. Os cards precisam de uma largura relativa
     à área (três cabem na janela), mas eles são filhos da PISTA, cuja largura
     vem do conteúdo — vários milhares de px.

     A primeira tentativa foi um custom property com porcentagem declarado
     aqui. Não funciona, e o motivo vale registrar: porcentagem em custom
     property resolve ONDE É USADA, não onde é declarada. O `100%` ia resolver
     contra a pista, que por sua vez depende dos filhos — referência circular.
     Medido: pista de 373px e cards de 28x17.

     `container-type: inline-size` resolve de verdade: transforma a área em
     container de consulta, e a unidade `cqi` na regra dos itens passa a medir
     1% da largura DELA, independente de onde o filho esteja. */
  container-type: inline-size;

  /* SEM MÁSCARA (pedido 28/08: "não quero esse esfumaçado, deixe 100% flat").
     Tinha aqui um `mask-image` que apagava 96px em cada ponta. A borda agora é
     corte seco — o `overflow: hidden` da própria área faz o recorte.

     `scroll-behavior: auto` NÃO é redundante, é blindagem. O JS escreve
     scrollLeft a cada quadro; se algum dia esta área herdar ou receber
     `scroll-behavior: smooth`, cada uma dessas escritas viraria uma animação
     própria do navegador e a tira arrastaria atrás do ponteiro. É exatamente o
     bug que já custou caro no motor de rolagem (ver "O MOTOR DE ROLAGEM"),
     e uma linha aqui impede a reincidência. */
  scroll-behavior: auto;
}
.fel-tira.fel-tira::-webkit-scrollbar{ display: none; }

/* A PISTA — o que de fato se move.

   Mover a ÁREA seria mover o próprio recorte, então o conteúdo ganha um
   elemento só para isso. O gap vem da área para cá junto com os cards.

   `transform` e não `scrollLeft`: o scrollLeft arredonda (medido — 137,5
   guardou 138), e a 220px/s isso vira passos de 4/3/4/3px por quadro. O
   transform aceita fração; `will-change` avisa o compositor. */
.fel-tira-pista{
  display: flex;
  gap: 0 8px;
  align-items: center;
  width: max-content;        /* larga como o conteúdo, para haver o que correr */
  will-change: transform;
}

@media (hover: hover){
  /* O arraste precisa se anunciar: sem cursor de mão ninguém descobre que a
     tira pega. */
  .fel-tira-pista{ cursor: grab; }
  .fel-tira-pista.arrastando{ cursor: grabbing; }
  /* Arrastar não pode selecionar as imagens no caminho. */
  .fel-tira-pista.arrastando, .fel-tira-pista.arrastando *{ user-select: none; }

  /* Máquina híbrida — notebook com tela sensível e mouse — resolve
     `(hover: hover)` como VERDADEIRO, então recebe o motor de arraste mas
     ficava sem `touch-action` declarado e sem a rolagem nativa de reserva do
     bloco (hover: none). Com o padrão `auto` o navegador pode assumir o gesto
     como pan e cancelar o ponteiro no meio. `pan-y` devolve o eixo horizontal
     ao JS e mantém a rolagem VERTICAL da página com o navegador — que é a
     regra da casa. Nunca `none`: isso sequestraria a rolagem no toque. */
  .fel-tira-pista{ touch-action: pan-y; }

  /* Nem virar drag-and-drop nativo. O JS já desarma o link (ver runtime.js,
     "O ARRASTE NATIVO DO LINK"), e isto fecha o cerco pelas imagens: uma
     <img> ou <video> arrastável dentro da tira reintroduz o mesmo sequestro
     de pointermove por outra porta. */
  .fel-tira-pista, .fel-tira-pista *{ -webkit-user-drag: none; }

  /* A COR DA FILEIRA NO HOVER, de volta — e só aqui.

     O preenchimento #1a1a1a existia e foi removido porque "lia como uma placa
     cinza quando as miniaturas se recolhiam ao centro" (ver framer-runtime.css,
     "Project rows"). Aquele motivo era da CONVERGÊNCIA, e em /projects ela não
     acontece mais: lá as imagens correm na tira e a fileira permanece cheia.
     Sem o recolhimento não há placa cinza — há uma faixa inteira acendendo,
     que é o retorno que a fileira precisa dar ao ponteiro.

     O `:has(.fel-coluna-nome)` é o que separa as duas páginas: só as fileiras
     de /projects têm coluna de nome. Na home, onde o recolhimento continua, o
     fundo segue preto e a razão original segue de pé. */
  .fel-fileira:has(.fel-coluna-nome){
    transition: background-color var(--m-dur-2, 420ms) var(--m-ease, ease);
  }
  .fel-fileira:has(.fel-coluna-nome):hover{
    background-color: #1a1a1a;
  }
}

/* TOUCH: rolagem NATIVA, sem motor. O momentum do sistema é melhor que
   qualquer inércia escrita à mão, e `pan-y` deixa a rolagem vertical da página
   passar intacta — sequestrar o scroll no toque é a linha que a casa não
   cruza. */
@media (hover: none){
  .fel-tira.fel-tira{
    overflow-x: auto;
    touch-action: pan-y pinch-zoom;
    -webkit-overflow-scrolling: touch;
  }
}

/* A LARGURA DE TODOS OS ITENS DA TIRA, originais e injetados.

   Sem isto a tira sai torta: `.fel-card` dá a PROPORÇÃO (67/40) e a altura,
   mas não a largura — e um <img> sem largura declarada, como item flex
   `0 0 auto`, assume a largura INTRÍNSECA do arquivo. Medido: as injetadas
   saíam em 640×382 (o tamanho do derivado -640) enquanto os cards originais,
   que tinham largura de porcentagem calculada para uma linha que agora não
   quebra mais, colapsavam para 0×0.

   `(100% - 16px) / 3` reproduz exatamente o que a fileira já mostrava: três
   cards com os dois gaps de 8px da área. Ou seja, a janela continua com a
   mesma leitura de antes — o que muda é que agora existe fila esperando fora
   dela. Em porcentagem, não em px, para acompanhar a janela.

   `flex: 0 0` (não encolher) é o que destrava a corrida: com shrink ligado o
   flex espremeria os 13 itens para caber nos 1101px, que é o oposto do efeito. */
/* Classe DOBRADA (0,2,0), não simples. O seletor `.fel-tira-pista > *` dá
   (0,1,0) e EMPATA com as regras que o Framer escreve para cada container de
   card — e o <style> do export vem depois do nosso <link>, então no empate ele
   ganha. Medido quando isto era simples: as <img> injetadas ficavam certas
   (não têm regra do Framer) e os 3 cards ORIGINAIS de cada fileira caíam para
   flex-basis 0 e 1x1 na tela — 45 itens quebrados em 189. */
.fel-tira-pista.fel-tira-pista > *{
  /* 100cqi = a largura da ÁREA (ver container-type acima), não da pista.
     `(100cqi - 16px)/3` reproduz exatamente os três cards com os dois gaps de
     8px que a fileira já mostrava — o que muda é que agora existe fila
     esperando fora do recorte. */
  /* ALTURA FIXA, LARGURA PELA RAZÃO (08/09) — o fim do corte na tira.

     Pedido do Felipe: "as imagens numa proporção diferente de 16:9 estão
     cropando. Elas podem continuar aparecendo, mas o formato delas é 4:5".

     Antes a largura era a base flex e a altura vinha da proporção 67/40 de
     `.fel-card`, igual para todos — então um arquivo 4:5 entrava numa moldura
     panorâmica e o `object-fit: cover` descartava metade da altura da imagem.

     Invertido: a ALTURA é a mesma para todos e a LARGURA sai da razão. A
     altura é exatamente a que a tira já tinha — `(100cqi - 16px)/3` era a
     largura do card e `× 40/67` é a altura que ela produzia — então a linha
     do grid não se move nem um pixel; o que muda é a largura de cada item.

     `!important` nas duas porque `.fel-card` declara `height: auto` com
     classe TRIPLA (0,3,0), para ganhar do Framer, e todo item da tira é um
     `.fel-card`. A razão de cada item chega depois, inline, do runtime (ver
     initTiras, "A MOLDURA SEGUE O ARQUIVO"); o 67/40 abaixo é só o padrão de
     quem não tem razão conhecida — e é a moldura de sempre. */
  flex: 0 0 auto;
  height: calc((100cqi - 16px) / 3 * 40 / 67) !important;
  width: auto !important;
  aspect-ratio: 67 / 40;
  /* min-width: 0 NÃO é adorno — sem ele a base acima é ignorada nas <img>.
     Item flex nasce com `min-width: auto`, que num elemento SUBSTITUÍDO
     resolve para a largura INTRÍNSECA do arquivo. A largura usada vira
     max(base, min-width), então as injetadas ficavam em 640px (o derivado
     -640) enquanto os <div> originais, que não são substituídos, obedeciam a
     base e ficavam em 362. Medido: 362x216 e 640x382 lado a lado na mesma
     tira. Zerar o mínimo devolve o controle à base. */
  min-width: 0;
}
.fel-tira-img.fel-tira-img{
  object-fit: cover;
  display: block;
}

@media (prefers-reduced-motion: reduce){
  /* Sem corrida e sem arraste com inércia — mas o conteúdo continua
     alcançável, agora pela rolagem nativa. Tirar o movimento não pode tirar o
     acesso. O JS nem instala o motor neste caso, então não há transform
     concorrendo com a rolagem. */
  .fel-tira.fel-tira{ overflow-x: auto; }
}

/* ===========================================================================
   O BLOCO DE IMAGENS DAS PÁGINAS DE PROJETO — moldura e proporção
   ---------------------------------------------------------------------------
   Dois pedidos do Felipe (03/09): "equalizar o padding de todos os projetos" e
   "colocar uma proporção de tamanho para as imagens".

   O QUE FOI MEDIDO, nos 15 projetos:

   1. O padding do bloco tinha DOIS regimes, e os dois assimétricos:
        cada, mub3, teva-indices ......  6px 6px 0 3px
        geonav, jbs ...................  5px 5px 0
      Ninguém escolheu nenhum dos dois — são sobras de composição do Framer. O
      vão ENTRE as imagens, esse sim, já era 5px em todos.

   2. As alturas das fileiras iam de 412 a 665 sem critério, porque o Framer
      deixa a razão nativa de cada mídia mandar na altura. Não era corte: era
      falta de lei. As três famílias (marcadas no transform.mjs) são a lei.

   5px NOS QUATRO LADOS: casa a moldura com o vão interno que já existia, então
   o bloco passa a ter uma medida só. O `0` de baixo era o mais visível dos
   defeitos — deixava a última imagem colada na borda enquanto a primeira
   respirava.
   =========================================================================== */
/* O ESCOPO É "Mosaic", NÃO "Project Images" — e a diferença custou uma
   página. As duas existem: o Mosaic envolve o Project Images, e os dois
   embrulham exatamente as mesmas mídias (37 em /teva-indices, medido). Mas o
   Project Images falta em /chris-rozza, que monta o bloco só com o Mosaic.
   Escopar no interno deixava essa página de fora, calada — sem padding e sem
   proporção. O Mosaic está nas 15.

   E ele não pega demais: as mídias do OTHER PROJECTS / CARDS-3 vivem fora do
   Mosaic (verificado na própria chris-rozza), então os cards de rodapé não
   são afetados. */
body[data-project-page] [data-framer-name="Mosaic"]{
  padding: 5px !important;
}

/* A PROPORÇÃO VAI NA MÍDIA, NÃO NO CONTAINER — verificado no navegador antes
   de escrever: com `aspect-ratio` no container o Framer ignora e a altura
   continua vindo da razão intrínseca; na mídia, com `height:auto`, a fileira
   inteira obedece (medido: 614px → 562px ao pedir 16/9).

   `object-fit: cover` é o que já estava lá e é o comportamento certo aqui —
   preserva a razão da arte e corta a sobra, em vez de esticar. Só as 15
   mídias avulsas chegam a ter sobra; as 78 que já nasceram numa família
   entram sem perder um pixel.

   Escopado a [data-framer-name="Mosaic"]: as mesmas mídias aparecem
   no corredor da home e nas tiras de /projects, onde a proporção é outra. */
/* `!important` NO height, e só nele: o Framer escreve `height:100%` no style
   INLINE de cada mídia, e contra inline nenhum seletor de autor ganha sem ele.
   Medido antes de recorrer a isto — 2 das 23 mídias de /teva-indices ficavam
   em 1.604 com a `aspect-ratio` já aplicada: a razão não tinha como agir
   porque largura E altura estavam ambas fixadas em 100%. Com height:auto a
   razão volta a mandar (613,8px em vez de 622,1). É a mesma lição que a cor da
   fileira em /projects já tinha ensinado neste arquivo. */
body[data-project-page] [data-framer-name="Mosaic"] .fel-ar-larga,
body[data-project-page] [data-framer-name="Mosaic"] .fel-ar-169,
body[data-project-page] [data-framer-name="Mosaic"] .fel-ar-retrato{
  height: auto !important;
  object-fit: cover;
}
body[data-project-page] [data-framer-name="Mosaic"] .fel-ar-larga  { aspect-ratio: 1600 / 984; }
body[data-project-page] [data-framer-name="Mosaic"] .fel-ar-169    { aspect-ratio: 16 / 9; }
body[data-project-page] [data-framer-name="Mosaic"] .fel-ar-retrato{ aspect-ratio: 4 / 5; }

/* E A RAZÃO EXATA VENCE A FAMÍLIA, quando ela existe (08/09).
   ---------------------------------------------------------------------------
   As três famílias acima cobrem 90% do acervo com erro abaixo de 2% — mas
   cobrir "quase" com `object-fit: cover` significa CORTAR o resto. Medido: 10
   imagens do geonav a 9,5% de perda, uma do chris-rozza a 10%, uma do goldko
   a 8,4%.

   Aqui vale a mesma regra que o Felipe deu para as tiras em 08/09 ("elas podem
   continuar aparecendo, mas o formato delas é diferente") e o que ele já havia
   pedido em 03/09: "o mais fiel possível ao tamanho ideal do projeto". Fiel é
   a razão do arquivo; a família era a minha aproximação dela.

   As classes ficam — são elas que soltam a altura da mídia e da corrente de
   wrappers acima, e continuam sendo o padrão de quem não tem razão conhecida.
   Esta regra só troca o NÚMERO, e vem depois delas porque empata em
   especificidade (0,3,1) e o desempate é a ordem. */
body[data-project-page] [data-framer-name="Mosaic"] [data-ar]{
  aspect-ratio: var(--fel-ar);
}

/* E A CAIXA TEM DE CEDER JUNTO.
   Soltar a altura só da mídia não bastava, e o defeito era pior que o
   original: em duas fileiras da /geonav o Framer fixa `height: 518px` na
   própria fileira, então a mídia crescia para os 614 da sua razão e VAZAVA
   96px por cima da fileira seguinte. Medido, não suposto.

   Aqui `height:auto` desce por toda a cadeia que embrulha uma mídia marcada —
   fileira, container-hash, o `.ssr-variant` de display:contents no meio — para
   que a razão da mídia mande na altura da caixa em vez de brigar com ela.
   Dentro de "Project Images" não existe outro conteúdo além das mídias, então
   deixar tudo dimensionar pelo conteúdo é coerente, não uma rede de arrasto.

   Verificado depois: zero fileiras vazando, e as alturas caíram nas famílias —
   412 (três retratos), 561 (16:9), 614 (1.63), 621 (dois retratos). */
body[data-project-page] [data-framer-name="Mosaic"] *:has(.fel-ar-larga),
body[data-project-page] [data-framer-name="Mosaic"] *:has(.fel-ar-169),
body[data-project-page] [data-framer-name="Mosaic"] *:has(.fel-ar-retrato){
  height: auto !important;
}

/* ===========================================================================
   A GRADE DA /projects — uma altura só, e nada cortado
   ---------------------------------------------------------------------------
   Pedido do Felipe (05/09): "o padding ainda está desproporcional em alguns
   lugares, unifique toda a grade como um todo".

   ONDE ESTAVA O ERRO. Não era padding — esse já era 5px em todas as fileiras.
   O Framer escreve uma ALTURA EXPLÍCITA no <a> de cada fileira, e em duas
   variantes: medido em 1600px, `253.328px` em cinco fileiras e `239.984px`
   nas outras dez. A prova de que o número não vem do conteúdo é que ele anda
   ao contrário: a fileira de 253 é justamente a de coluna MAIS CURTA (107
   contra 125). São sobras de composição, como o vão de 18px da home era.

   O SEGUNDO DEFEITO, pior e invisível na medição de altura: a coluna do nome
   tem `overflow: hidden` sobre essa altura fixa, então quando as tags quebram
   em mais linhas o conteúdo é CORTADO. Medido a 900px: TEVA INDICES pedia
   162px numa caixa de 140 — 22px decepados. É o "TYPOGRAPHY" faltando no pé
   do TALENTO INCLUIR.

   A CORREÇÃO é devolver a altura para quem sabe: o conteúdo. Sem altura fixa,
   a fileira vira `max(coluna, tira) + 10` de padding, e como a tira é
   uniforme, todas as fileiras passam a medir igual.

   VERIFICADO em cinco larguras, 15 fileiras cada:
     1920 → 273 em todas   (spread 0)
     1600 → 228 em todas   (spread 0)
     1440 → 206 em todas   (spread 0)
     1200 → 173 em todas   (spread 0)
     1024 → 148 em onze, 163 em duas
   Zero conteúdo cortado em qualquer uma.

   AS DUAS DE 1024 SÃO HONESTAS, e por isso ficam: TEVA NXT e TALENTO INCLUIR
   somam 114px de tags mais 38px de descrição = 153, contra os 138 da tira.
   Elas são mais altas porque precisam ser; forçá-las aos 148 seria voltar a
   cortar texto — trocar um defeito visível por um escondido.

   ---------------------------------------------------------------------------
   CORREÇÃO DA CORREÇÃO (05/09). A regra acima igualou as ALTURAS e, sozinha,
   piorou a página: os VÃOS viraram 12, 27, 4, 28 e −4px. O Felipe viu na hora.

   O motivo é que a altura tinha TRÊS donos, não um, e eu só havia soltado o
   do meio:

     1. a lista .......... altura fixa (3788,91px em 1712) + justify:center
     2. a caixa .......... UMA POR PROJETO, com `aspect-ratio` própria do
                           Framer: 6.32/1, 6.67/1 ou ~7.1/1 → 271, 257 ou 240px
     3. o <a> da fileira . altura fixa — este eu soltei no commit anterior

   Com (3) solto em 244px e (2) ainda cravado, a sobra de cada caixa virou o
   vão: 271−244 = 27, 257−244 = 13, 240−244 = −4. O vão negativo é literal —
   em oito projetos a fileira transbordava a própria caixa.

   Soltar os três é o que fecha a conta. Depois disso: caixa e fileira medem
   igual, o vão entre elas é 0 em todas as quinze, e a grade fica uniforme de
   fato — não uniforme numa medida e torta em outra.
   =========================================================================== */
/* `:has(.fel-coluna-nome)` EM TODO SELETOR DESTE BLOCO — a coluna do nome só
   existe em /projects, e é ela que separa esta página da home.

   Sem a guarda a regra vazava, e o estrago foi medido: `.fel-fileira` também
   é marcada nas 8 fileiras de cards da HOME, e lá `height: auto` desmontou o
   ritmo de 12px que o commit a33f157 tinha estabelecido de propósito. Os vãos
   viraram 12, 17, 12, 16, 12, −18 e 12 — inclusive uma sobreposição de 18px.
   As ALTURAS continuavam 160 em todas, então a medição de altura passava
   limpa e o defeito só aparecia no vão. Foi assim que ele me escapou.

   O idioma é o que a própria folha já usa na cor de hover da fileira (ver
   linha 1053): mesma especificidade (0,2,0), sem !important novo. */
.fel-fileira:has(.fel-coluna-nome),
.fel-fileira:has(.fel-coluna-nome) .fel-coluna-nome,
.fel-fileira:has(.fel-coluna-nome) .fel-coluna-nome > *,
.fel-fileira:has(.fel-coluna-nome) .fel-tira{
  height: auto !important;
}

/* `aspect-ratio: auto` E `height: auto` na caixa, nessa ordem de importância:
   só a altura não resolve. Testado — com `height:auto` sozinho as caixas de
   240px cederam (o conteúdo de 244 as empurrava, via min-height:auto) mas as
   de 257 e 271 não se moveram, porque ali quem manda é a proporção, e ela
   ganha de uma altura automática. Zerar a proporção é o que devolve a altura
   ao conteúdo. */
.fel-fileira-caixa.fel-fileira-caixa{
  aspect-ratio: auto !important;
  height: auto !important;
}

/* A lista solta junto — ver a nota no runtime.js: altura travada mais
   justify-content:center transforma qualquer sobra em vão fantasma nas pontas. */
.fel-lista.fel-lista{
  height: auto !important;
}

/* ---------------------------------------------------------------------------
   QUANDO NÃO COUBER, A TAG CEDE — não a altura da fileira.
   Decisão do Felipe (05/09), depois de ver que abaixo de 1200 as fileiras
   ainda variavam: os projetos têm de 4 a 9 tags, e a coluna estreita as
   quebra em números de linhas diferentes.

   A COLUNA DEIXA DE MANDAR NA ALTURA. `min-height: 0` é o que faz isso: um
   item de flex contribui com sua altura de conteúdo por padrão
   (`min-height: auto`), e era essa contribuição que empurrava a fileira. Com
   ela zerada e `align-self: stretch`, a coluna passa a ACOMPANHAR a altura da
   fileira em vez de defini-la — e quem define vira a tira, que é uniforme.

   A DESCRIÇÃO DO HOVER É PROTEGIDA: `flex: 0 0 auto` nela e `flex: 1 1 auto`
   no bloco de tags. Assim quem encolhe é a lista de tags, nunca a frase —
   que é justamente o prêmio da interação. Verificado nas três larguras
   estreitas: nenhuma fileira perdeu a descrição.

   O `overflow: hidden` aqui é só a rede de segurança. Quem escolhe QUAIS tags
   somem é o ajustarTags() no runtime.js, escondendo o chip inteiro — porque
   clipar por altura fatiava a tag ao meio (medido: 8 tags cortadas na
   horizontal a 810px, que é pior que perder a tag). */
.fel-fileira:has(.fel-coluna-nome) .fel-coluna-nome{
  align-self: stretch !important;
  min-height: 0 !important;
  overflow: hidden !important;
}
.fel-fileira:has(.fel-coluna-nome) .fel-coluna-nome > *{
  min-height: 0 !important;
  height: 100% !important;
  overflow: hidden !important;
}
/* NOME E TAGS NO ALTO, DESCRIÇÃO NO PÉ (05/09, pedido do Felipe).

   O `flex: 1 1 auto` faz este bloco CRESCER até o pé da coluna — é o que
   empurra a linha de descrição para baixo, via `space-between` do pai. Mas o
   Framer declara `justify-content: center` aqui dentro, então o nome e as tags
   flutuavam no meio dessa caixa crescida. Medido: 58px de sobra acima em
   PERZONA, 45 em TEVA INDICES, 35 em TEVA NXT.

   O Felipe achou que TEVA INDICES estava certo — estava só menos errado: é o
   que tem mais tags, então a sobra é a menor das três. Nenhum estava no topo.

   `flex-start` prega o conteúdo no alto; o `align-items: flex-start` que o
   Framer já traz cuida da esquerda. A descrição continua no pé porque quem a
   põe lá é a altura deste bloco, não uma regra própria. */
.fel-fileira:has(.fel-coluna-nome) [data-framer-name="TEXT"] > *:not(.fel-hover-line){
  min-height: 0 !important;
  flex: 1 1 auto !important;
  justify-content: flex-start !important;
  overflow: hidden !important;
}
.fel-fileira:has(.fel-coluna-nome) .fel-hover-line{
  flex: 0 0 auto !important;
}

/* ===========================================================================
   MODO CÂMERA — a mão como terceira entrada do corredor

   Só aparece em localhost ou ?camera=1, como o HUD de parâmetros. Em produção
   a home não ganha botão de câmera: quem chegou para ver o portfólio não deve
   ser convidado a acender a webcam.

   O CSS aqui é só chrome. A interação inteira acontece nos três números que o
   camera.js escreve na porta do corredor — este arquivo não move imagem
   nenhuma.
   =========================================================================== */
/* ===========================================================================
   O PAINEL DO MODO CÂMERA — uma peça só, no alto e ao centro.

   CANTO RETO, porque é o que o site é: o grid dos projetos, as folhas, o
   HUD de parâmetros, a nav — nada no byFel tem raio. A pílula arredondada
   da primeira versão era um corpo estranho, e o Felipe viu na hora.

   A CÂMERA DESCE DE DENTRO DO BOTÃO. O painel tem `overflow: hidden`, o
   corpo é uma gaveta em `grid-template-rows: 0fr → 1fr`, e o conteúdo
   dentro dela começa deslocado para cima. Os três juntos fazem o vídeo
   surgir POR TRÁS do cabeçalho e descer, em vez de simplesmente aparecer.
   Em `fr` e não em `max-height`: max-height exige chutar uma altura, e um
   chute alto demais faz a transição sair em velocidade errada.
   =========================================================================== */
.fel-cam{
  position: fixed; z-index: 71; top: 16px; left: 50%;
  transform: translateX(-50%);
  display: flex; flex-direction: column;
  /* AS MEDIDAS SAÍRAM DA NAV, medidas nela e não estimadas: os chips
     FEL/WORK/ABOUT têm 25,6px de altura, padding 5px 8px, fonte de 12px sem
     entreletra extra e canto reto. O painel tinha 38px de altura e 190 de
     largura, e ao lado deles parecia outra peça de outro site — foi o que o
     Felipe viu ("bem maior e bem mais alto, deixe equalizado").

     FECHADO ele é um chip como os outros; ABERTO cresce até caber a câmera.
     A largura é interpolável porque os dois estados são px explícitos: de
     `max-content` para 190px o navegador não sabe animar. */
  /* TRÊS LARGURAS, todas em px explícito porque `max-content` não
     interpola. Repouso é o ÍCONE E MAIS NADA — 15px de desenho entre 8 de
     padding, um quadrado de 31 por 25,6 que é irmão dos chips da nav.
     (Felipe, 11/09: "eu quero só o ícone; quando passar o mouse, aí sim o
     nome da interação".) */
  width: var(--fel-cam-l, 31px);
  /* A CURVA MUDA DE DIREÇÃO, E ERA ELA A ASPEREZA — não o relógio.

     Os dois sentidos usavam --m-ease-exit, e essa curva é de saída por
     construção. Medida aqui, nos 87px que separam o chip fechado do chip
     com o nome: aos 30ms ela já tinha andado 9,3px, cruzava a metade do
     caminho aos 62ms dos 200, e depois arrastava os últimos 6% por 60ms.
     Um tranco seguido de um rastro. Com --m-ease (a mesma duração!) são
     3,7px aos 30ms e a metade aos 100 — o movimento tem meio e tem fim.
     O Felipe pediu "muito mais suave"; o conserto não custou um milésimo.

     Um token só, trocado por estado: ABRIR com --m-ease, FECHAR com
     --m-ease-exit. Abrir é um convite e tem de ter respiro; fechar é
     obedecer, e obediência lenta lê como travamento. A troca funciona
     porque o navegador lê a função de tempo do estilo DEPOIS da mudança:
     ao entrar o ponteiro já vale a curva de abrir, ao sair a de fechar. */
  --fel-cam-curva: var(--m-ease-exit);
  --fel-cam-t: var(--m-dur-1);
  /* 45ms DE ESPERA ANTES DE FECHAR, e este número saiu de uma medição que
     eu não tinha feito: a que pergunta ONDE a moldura alcança a palavra.

     A palavra termina aos 101px dentro de um chip que vai de 110 a 31. A
     moldura a alcança depois de andar 9px — 11,4% do curso. Com a curva de
     saída isso acontece aos 31ms dos 200, e aos 31ms a palavra ainda está a
     69% de opacidade. Ou seja: a cada vez que o ponteiro saía, "GESTURES"
     era comido letra por letra enquanto ainda estava perfeitamente legível.
     É a mesma reticência de antes, só que em movimento — e por isso passava
     despercebida numa captura de tela.

     Duas correções que se encaixam: a palavra sai em 60ms (ver .fel-cam-rot)
     e a moldura espera 45. A moldura alcança a palavra aos 76ms; a palavra
     já não existe desde os 60. Ela fecha sobre espaço vazio, sempre.

     45ms é mentira contada a ninguém: o limiar em que um atraso começa a ser
     sentido como travamento está perto dos 100ms. Aqui o chip parece obedecer
     no mesmo instante — só que obedece na ordem certa. */
  --fel-cam-atraso: 45ms;
  transition: width var(--fel-cam-t) var(--fel-cam-curva) var(--fel-cam-atraso),
              opacity var(--m-dur-3) var(--m-ease),
              /* O DESLOCAMENTO CHEGA ANTES DA OPACIDADE, de propósito. Seis
                 pixels em 800ms dão 7,5 px/s — um quarto da velocidade com
                 que este site revela qualquer outra coisa (--m-rise: 24px em
                 --m-dur-3, 30 px/s), e devagar assim lê como deriva e não
                 como chegada. Em 420ms a peça POUSA aos 420 e segue
                 clareando até os 800. O fade é o que dura; o deslocamento é
                 só a direção de onde ela veio. */
              transform var(--m-dur-2) var(--m-ease-exit),
              /* 400ms e --m-ease: copiado da nav, não escolhido aqui. */
              background-color 400ms var(--m-ease),
              color 400ms var(--m-ease);
  font-size: 12px; text-transform: uppercase;
  color: var(--fel-tinta, #111);
  background: var(--fel-chapa, rgba(245, 245, 245, .2));
  /* A VIDRAÇA É A DA CASA, e esta era a maior costura aparente da peça.
     -----------------------------------------------------------------------
     Medido lado a lado com um chip FEL/WORK/ABOUT, no navegador, hoje:

                        nav                      câmera (antes)
       face      rgba(245,245,245, 0,20)    rgba(255,255,255, 0,55)
       desfoque  blur(60px)                 blur(7px)

     Altura, padding e canto eu já tinha igualado em 10/09, quando o Felipe
     pediu — e parei ali, no que se mede com régua. Mas o que faz duas peças
     parecerem do mesmo sistema não é a régua, é o MATERIAL, e o material
     estava diferente por uma ordem de grandeza. Um desfoque de 60px amassa
     o fundo inteiro num campo quase liso, e por isso a nav sustenta a
     legibilidade com só 20% de face. Um desfoque de 7px quase não mexe no
     fundo, então a face tinha de fazer todo o trabalho a 55% — e 55% de
     branco não é vidro, é adesivo. Era a única peça da tela colada por
     cima; todas as outras são translúcidas de verdade.

     O framer-runtime.css já dizia isto em voz alta, no comentário de
     `--glass-face`: "Um lugar só — a receita já divergiu uma vez por estar
     em dois". Estava em três. Agora está em um. */
  -webkit-backdrop-filter: var(--glass-blur, blur(90px) saturate(160%));
  backdrop-filter: var(--glass-blur, blur(90px) saturate(160%));
  /* SEM BORDA. "Não quero que tenha stroke" (Felipe, 10/09), e ele está
     certo pela segunda vez: os chips FEL/WORK/ABOUT são chapa translúcida
     e mais nada. Quem define a peça é a chapa com desfoque, não um fio em
     volta — e sem o fio a altura bate com a nav sem compensação nenhuma. */
  border: 0;
  /* `clip` E NÃO `hidden`, e a diferença não é cosmética.
     -----------------------------------------------------------------------
     `overflow: hidden` CRIA UM CONTÊINER ROLÁVEL. Ele não mostra barras e
     não rola com a roda do mouse, mas rola por programa — e, o que importa
     aqui, rola sozinho quando o navegador precisa trazer algo para a vista.

     E há sempre o que trazer: o conteúdo da gaveta tem a largura da gaveta
     ABERTA (182px) mesmo com o painel fechado em 31, de propósito, para a
     câmera não espremer enquanto desce. Ou seja, este elemento vive com
     151px de conteúdo transbordando. Basta a chave receber foco — uma
     tabulação, um clique — para o navegador rolar o painel e encostar o
     botão na borda visível.

     Flagrado em teste com a régua: `scrollLeft: 13`, o ícone da mão
     terminando 5px FORA do painel e o rótulo deslocado 13 para a esquerda.
     E não volta: um contêiner escondido que rolou fica rolado para sempre,
     porque nada nunca o rola de volta.

     `overflow: clip` corta igual e não cria contêiner rolável nenhum —
     `scrollLeft` deixa de existir como conceito. Fica o `hidden` na linha
     de cima para quem não entender `clip`; lá o defeito continua possível,
     mas é o comportamento que já existia. */
  overflow: hidden;               /* é isto que corta a câmera enquanto desce */
  overflow: clip;
  /* Sem sombra — regra da casa: contraste vem de fundo, borda ou tinta. */
}
/* NADA ANIMA PARA DENTRO DO PRIMEIRO QUADRO. Sem isto, o insert interpolava
   a largura a partir do auto do UA — um nascer-do-nada, o mesmo zero que o
   `data-entrada="0"` inventava com outro nome. O JS arma `data-pronta`
   depois de duas pinturas; a partir daí a lista de transições de `.fel-cam`
   vale. Pesa (0,2,0) e ganha da base e da regra de reduced-motion. */
.fel-cam:not([data-pronta]){ transition: none; }
/* No hover a peça abre só o quanto o nome pede. Ligada, vai aos 190 da
   câmera — e aí o nome FICA, porque quem ligou precisa reencontrar o botão
   para desligar. */
/* A LARGURA DO NOME VEM MEDIDA DO JS — `--fel-cam-nome`, escrita no monte
   do painel (ver `medirTextos` em camera.js). Os 118px de reserva aqui são
   só para o caso de a medição não acontecer; o valor real hoje dá 110, e a
   diferença de 8px era um vão morto à direita da palavra. Chutar a largura
   de um texto é a única coisa que este arquivo não tem como fazer certo. */
.fel-cam:hover, .fel-cam:focus-within,
.fel-cam[data-entrada],
.fel-cam:has(.fel-cam-chave[aria-expanded="true"]){
  --fel-cam-curva: var(--m-ease);
  --fel-cam-atraso: 0ms;      /* abrir é imediato; só o fechar espera */
}
/* ABRIR NO MESMO TEMPO DA COR. A chapa vira em 400ms (número da nav) e a
   largura abria em 200 — as duas metades do mesmo gesto em compassos
   diferentes, que é a forma mais discreta de uma interação parecer
   remendada. Com --m-dur-2 (420ms) o hover volta a ser UM movimento: a
   moldura cresce enquanto o fundo escurece, e os dois param juntos.

   Fechar continua nos 200ms da base. A assimetria já estava na curva; isto
   leva o mesmo argumento à duração, e não custa um literal novo. */
.fel-cam:hover, .fel-cam:focus-within{
  --fel-cam-l: var(--fel-cam-nome, 118px);
  --fel-cam-t: var(--m-dur-2);
}
/* ===========================================================================
   A ENTRADA — o pedido do Felipe em 11/09: "ao carregar o site, faça uma
   animação suave, aparecendo gestures, e aí sim a redução para a mãozinha,
   e a mãozinha fica mexendo".

   Ele está pedindo que a peça se APRESENTE antes de virar ícone, e isso
   conserta um defeito que eu tinha aceitado sem discutir: um quadrado de
   31px com um risco dentro não diz nada a ninguém. Quem nunca passou o
   mouse ali nunca descobriu que o site tem modo de gesto. O nome existia,
   mas trancado atrás de um hover que só acontece por acidente.

   Os tempos (o JS conduz; ver `montar()` em camera.js):
     —  sem atributo, sem data-pronta
        nasce no REPOUSO (31px, o ícone). Transições desligadas. É o estado
        real da peça — não um zero de teatro.
     1  data-entrada="1"  a largura abre até o nome, nos 420ms do hover
     2  data-entrada="2"  o nome sai, a largura recolhe aos 31px e a mão
                          ganha amplitude, tudo nos mesmos 420ms
     -  sem atributo      repouso. Nada muda na tela ao cair o atributo, e é
                          esse o sinal de que a sequência é um movimento só

   A LARGURA DA ENTRADA É A MESMA DO HOVER, de propósito: quem vir a
   apresentação e depois passar o mouse reencontra exatamente a mesma peça,
   na mesma medida. Uma terceira largura só para o teatro seria uma terceira
   coisa para a pessoa aprender.

   O TEMPO 0 SAIU (13/09). Ele era largo, invisível e 6px acima — um estado
   que o produto não tem. Na recarga a peça voltava daí. Sem ele, a
   apresentação É o hover, dito uma vez. */
.fel-cam[data-entrada="1"]{
  --fel-cam-l: var(--fel-cam-nome, 118px);
  --fel-cam-t: var(--m-dur-2); /* o mesmo 420ms do hover, não os 200 do clique */
}
/* O nome fica à mostra enquanto a apresentação é a apresentação — é ELE a
   apresentação. No tempo 1, portanto; no 2 ele já está de saída. */
.fel-cam[data-entrada="1"] .fel-cam-rot{ opacity: .8; margin-left: 6px; }
/* O RECOLHIMENTO TEM RELÓGIO PRÓPRIO, e essa é a diferença entre uma peça
   que se apresenta e uma que pisca.

   Medido na primeira versão, que não tinha o tempo 2: a largura caía de 118
   para 31px em 160ms e o nome sumia em 80. Os 200ms de --m-dur-1 com a
   curva de saída estão certos para FECHAR NO HOVER, porque ali a peça está
   obedecendo a um ponteiro que já foi embora — demora em obedecer lê como
   travamento. Aqui não há ninguém obedecendo: o recolhimento é coreografia,
   ninguém pediu por ele, e coreografia apressada parece defeito.

   Então o tempo 2 empresta --m-dur-2 (420ms, o tempo das coisas
   estruturais da casa) e a curva de abrir, que tem meio e tem fim. E o
   nome sai em 260ms, terminando MUITO antes dos 420 da caixa: assim a
   moldura se fecha sobre espaço vazio, e nunca sobre uma palavra ainda
   legível sendo cortada. */
.fel-cam[data-entrada="2"]{
  --fel-cam-t: var(--m-dur-2);
  --fel-cam-curva: var(--m-ease);
}
/* No recolhimento a conta é outra porque a caixa é mais lenta: com --m-ease
   em 420ms, a moldura alcança os 101px da palavra aos 102ms. Os 260ms que
   estavam aqui deixavam a palavra a 61% de opacidade nesse instante — o
   mesmo corte do hover, em câmera lenta. 80ms resolve com folga, e continua
   muito mais suave que os 60 do hover, que é o que a coreografia pede. */
.fel-cam[data-entrada="2"] .fel-cam-rot{
  transition: opacity 80ms linear,
              margin-left var(--m-dur-2) var(--m-ease);
}
/* O CLIQUE É COMANDO, E COMANDO SE ATENDE DEPRESSA. A largura do hover
   ganhou 420ms porque ninguém a pediu; esta foi pedida, então volta aos 200.
   Pesa (0,3,0) e por isso ganha da regra de hover, que pesa (0,2,0). */
.fel-cam:has(.fel-cam-chave[aria-expanded="true"]){
  --fel-cam-l: var(--fel-cam-larga, 182px);
  --fel-cam-t: var(--m-dur-1);
}
/* PASSO 3 — A CÂMERA RETRAI E FICA O LEMBRETE: O ÍCONE E UMA COR.

   O pedido do Felipe, palavra por palavra (11/09): "vamos fazer essa câmera
   retrair, e aí fica só um lembrete, UMA COR, sinalizando que a câmera está
   sendo utilizada pelo site". Uma cor. Não um painel de leitura.

   O que estava aqui eram 108px "com o nome à mostra" — só que nesta fase o
   rótulo já não é o nome: é o ESTADO ao vivo, e "open hand · steering" mede
   180px. O chip mostrava, permanentemente, uma frase cortada no meio. E
   pior: o ponto verde é um quinto item do flex, e com o rótulo ocupando
   largura natural dentro de um chip de 108 ele era empurrado para fora do
   `overflow: hidden`. O sinal de CÂMERA LIGADA — a única coisa desta fase
   que tem de ser vista — era o único elemento que não cabia.

   Agora o rótulo recolhe a zero de largura (não a `display: none`: ele
   continua sendo a região `role="status"`, e quem usa leitor de tela não
   pode perder o estado só porque a peça encolheu) e sobra ícone + ponto,
   com o `margin-left: auto` do ponto enfim tendo folga para empurrá-lo
   contra a margem direita. */
.fel-cam[data-fase="lembrete"]{ --fel-cam-l: 52px; }
/* E a espiada: ponteiro OU TECLADO reabre a gaveta, para rever o espelho
   sem desligar e religar. O `:focus-within` faltava — era o único estado
   interativo deste arquivo que não tinha par para quem navega por tabulação
   (a largura, o rótulo, o ícone e a chapa todos têm). A espiada empresta a
   transição da abertura por clique, lá embaixo, em vez de cair nas
   declarações de fechamento: antes a mesma gaveta abria em 620ms pelo
   clique e em 200ms pela espiada.

   É ESTE O ESTADO QUE MANDA NA LARGURA de todo o painel: é o único em que o
   texto de estado e o quadrado verde dividem a mesma linha. Por isso o
   `PONTO` entra na conta de `--fel-cam-larga` lá no camera.js, mesmo nas
   fases que não têm quadrado nenhum. */
.fel-cam[data-fase="lembrete"]:hover,
.fel-cam[data-fase="lembrete"]:focus-within{ --fel-cam-l: var(--fel-cam-larga, 182px); }

/* O SINAL DE CÂMERA LIGADA. Um quadrado de 5px, imóvel, que só existe nesta
   fase. Sem piscar e sem brilho: sinalização de câmera ativa é informação
   de segurança, e informação de segurança que se move vira decoração — o
   ponto verde do sistema operacional também não pisca.

   SAIU de manhã (Felipe, 13/09: "remove esse dot verde de dentro da UI") e
   VOLTOU à noite, no mesmo dia: ele é o único sinal DENTRO da página de que
   a webcam está ligada com a gaveta fechada. O indicador do navegador
   continua; este diz a mesma coisa na peça que a pessoa usa para apagar. */
.fel-cam[data-fase="lembrete"] .fel-cam-chave::after{
  content: ""; width: 5px; height: 5px; flex: none; margin-left: auto;
  background: #35d07f;
}
/* ABERTO, O PONTO SOME. A gaveta já é o estado — o espelho está à vista,
   a frase diz o que o detector vê. Dois sinais do mesmo fato na mesma
   fileira (Felipe, 13/09). Clique e espiada abrem o mesmo menu, então os
   dois caminhos apagam. `content: none` tira o item do flex, sem vão. */
.fel-cam:is(:has(.fel-cam-chave[aria-expanded="true"]),
            [data-fase="lembrete"]:hover,
            [data-fase="lembrete"]:focus-within) .fel-cam-chave::after{
  content: none;
}
/* O QUADRADO NUNCA SAI DA CAIXA (Felipe, 13/09: "o dot parece com algum bug" —
   ele aparecia cortado pela metade na borda direita). Causa: o rótulo aberto é
   um flex item com `white-space: nowrap`, e flex item nasce com
   `min-width: auto` — ele NÃO encolhe abaixo do texto. Bastava a frase medir
   um pixel a mais do que a sonda previu (fonte que chega depois, subpixel de
   entreletra) para o rótulo empurrar o quadrado para fora do `overflow` do
   painel. Com `min-width: 0` quem cede é o ladrilho, que já clipa o texto por
   dentro (`overflow: hidden; text-overflow: clip`) — e o sinal de câmera
   ligada, que é a única coisa desta fase que TEM de ser vista, fica inteiro. */
.fel-cam:is(:has(.fel-cam-chave[aria-expanded="true"]),
            [data-fase="lembrete"]:hover,
            [data-fase="lembrete"]:focus-within) .fel-cam-rot{ min-width: 0; }
/* ===========================================================================
   O BOTÃO VIVE NO CORREDOR, E SÓ NELE.

   "Ao dar um scroll, ele desaparece" (Felipe, 10/09). Mesma rampa que já
   apaga o campo, só que muito mais curta: o corredor desliga em 78vh, mas o
   convite some em 34vh — antes de o grid de projetos entrar, porque ali o
   gesto não tem o que pilotar e o botão vira enfeite sobre o trabalho.

   LIGADA OU NÃO, SOME. Houve um dia em que a câmera acesa segurava o
   chip até 70vh — para o interruptor não sumir com a luz ainda acesa.
   O camera.js agora desliga no MESMO 34vh, então a exceção morreu: não
   há mais luz sem interruptor, e não há mais chip sentado no WORK
   (os dois colam a 16px) enquanto a pessoa já está no grid. Pedido
   de 13/09: o botão não estava desaparecendo ao dar scroll. */
/* A RAMPA DE SCROLL CEDE A VEZ PARA A ENTRADA — `:not([data-entrada])`.

   Esta linha é a solução de um conflito que já me custou uma animação
   inteira: uma `animation` vence qualquer declaração normal da mesma
   propriedade, e esta aqui tem `both`, então FORA do trecho de 4vh a 34vh
   ela continua fixando `opacity: 1` pelo quadro-chave `from`. Qualquer
   transição de opacidade que eu escrevesse em `.fel-cam` morria caladinha
   — foi assim que a entrada anterior sumiu, e eu tinha escrito no lugar um
   comentário dizendo que entrada era desnecessária, que é a forma mais
   elegante de esconder um defeito de si mesmo.

   Em vez de disputar a propriedade, a rampa sai de cena enquanto a
   apresentação acontece: durante `data-entrada` não há animação nenhuma no
   painel, e `opacity` volta a ser uma transição comum. Quando o atributo
   cai, a rampa assume — e assume no valor 1, que é exatamente onde a
   entrada terminou. Sem degrau.

   Bônus verdadeiro: quem rolar a página DURANTE a apresentação também some,
   porque aí quem manda é `[data-fora="1"]` do JS, que sem animação por cima
   finalmente vence por especificidade. */
@supports (animation-timeline: scroll()) and (animation-range: 0% 100%){
  .fel-cam:not([data-entrada]){
    animation: fel-cam-sai linear both;
    animation-timeline: scroll(root);
    animation-range: 4vh 34vh;
  }
}
@keyframes fel-cam-sai{
  from{ opacity: 1; }
  to  { opacity: 0; visibility: hidden; }
}
/* A trava do JS, que vale onde não há linha de tempo de scroll (e com
   reduced-motion, onde a animação acima é desligada).

   Vale também com a câmera ligada: o JS desliga no mesmo 34vh, então
   esconder o chip não abandona uma luz acesa. Reduced-motion cai nesta
   trava — a rampa de cima está `animation: none` para ele. */
.fel-cam[data-fora="1"]{
  opacity: 0; visibility: hidden; pointer-events: none;
}
/* O HOVER É A INVERSÃO — a mesma de todos os outros chips deste site.

   O que havia aqui era invenção minha: a chapa branca "adensava um pouco",
   de 55% para 80%. Uma peça só, com um gesto só dela. O site inteiro faz
   outra coisa, e faz há tempo — está escrito em framer-runtime.css, medido
   do original: nav pill GLASS rgba(245,245,245,.2) -> rgb(0,0,0), em 400ms
   com cubic-bezier(.44,0,.56,1), que é o --m-ease da casa.

   Ou seja: os chips deste site VIRAM ao contrário quando o ponteiro chega.
   É o gesto mais minimalista que existe — nenhum elemento novo, nenhum
   contorno, nenhum brilho, só a peça trocando de lado. O Felipe pediu "ao
   passar o mouse, muito suave e minimalista"; a resposta certa não era
   inventar um quarto comportamento, era parar de ser a única peça que não
   fala a língua das outras.

   Os 400ms são os do original, e não os 200 da largura, de propósito: a
   moldura abre rápido (é ela que precisa dar lugar ao nome) e a cor vira
   devagar por baixo. Duas velocidades, um gesto só — é isso que faz uma
   inversão de valor não parecer um flash. */
.fel-cam:hover, .fel-cam:focus-within,
.fel-cam:has(.fel-cam-chave[aria-pressed="true"]){
  background: var(--fel-chapa-forte, #000);
  color: var(--fel-tinta-forte, #f5f5f5);
}

/* ===========================================================================
   ABERTO, A CHAPA VOLTA A SER VIDRO — E ISSO É O SISTEMA INTEIRO NUMA LINHA.
   ---------------------------------------------------------------------------
   O Felipe redesenhou o painel no Figma (12/09) e a mudança não é de
   espaçamento: é de MATÉRIA. Antes o painel era uma chapa preta com 2px de
   moldura em volta de um vídeo. Agora ele é um CONJUNTO DE LADRILHOS de
   tinta (#111) assentados sobre a chapa, separados por juntas de 3px.

   A junta é a moldura. Não há mais moldura externa — o contorno da peça é o
   contorno dos ladrilhos — e toda a estrutura passa a ser legível por onde
   ela SE PARTE, em vez de por onde ela é contornada.

   E é aqui que a chapa importa. Se ela continuasse preta, os ladrilhos de
   #111 sobre #000 dariam 6,7% de diferença: o ladrilhamento desapareceria e
   a peça voltaria a ser um retângulo preto com o desenho inteiro jogado
   fora. Voltando a VIDRO, as juntas passam a mostrar o corredor desfocado
   pelo mesmo blur de 60 que o chip fechado já usa.

   O que isso compra, e é o motivo de eu preferir vidro a qualquer cinza:

     · a matéria fica CONTÍNUA entre os dois estados. Fechado é uma placa de
       vidro com tinta em cima; aberto é a mesma placa com mais tinta em
       cima. O hover (que enegrece a placa) deixa de ser um estado terminal e
       vira o instante em que a placa está cheia — abrindo, ela se PARTE nos
       ladrilhos, e a chapa preta do hover reaparece como o primeiro deles.
     · a junta nunca é um pedaço de foto. Três pixels de imagem crua seriam
       ruído colorido aleatório; três pixels de vidro são sempre a média
       suave do que está atrás. O painel pega a cor do corredor sem pegar o
       desenho dele.

   A tinta continua clara (os ladrilhos são escuros); o que muda é só a
   chapa. Esta regra vem DEPOIS da de cima de propósito: as duas pesam
   (0,3,0) e quem decide é a ordem. */
/* A GAVETA ABRE POR DOIS CAMINHOS, E SÓ UM ESTAVA ESCRITO AQUI (12/09).
   ---------------------------------------------------------------------------
   `aria-expanded` é o clique. Mas a ESPIADA — passar o ponteiro ou tabular
   para um chip já em fase `lembrete` — abre a MESMA gaveta sem tocar no
   atributo, e isso está declarado três vezes mais abaixo, desde antes deste
   desenho existir. Todas as regras de ladrilho nasceram presas só ao clique.

   O resultado é o que o Felipe fotografou: espiando, o painel voltava ao
   desenho velho INTEIRO — chapa preta, ícone da mão ocupando a frente do
   texto, estado em 12px sem ladrilho nenhum — com o espelho novo embaixo.
   Não era cache: era metade da peça sem regra.

   `:is()` junta os caminhos num lugar só e mantém a especificidade em
   (0,3,0), a mesma de antes — quem decide contra a regra de hover continua
   sendo a ordem no arquivo. Toda regra de estado aberto usa esta cabeça.
   As que já tinham `.fel-cam:hover` na lista (a curva de abertura, a
   amplitude da respiração) não precisam: a espiada É um hover. */
.fel-cam:is(:has(.fel-cam-chave[aria-expanded="true"]),
            [data-fase="lembrete"]:hover,
            [data-fase="lembrete"]:focus-within){
  background: var(--glass-face, rgba(245,245,245,.2));
  color: var(--fel-tinta-forte, #f5f5f5);
  gap: 3px;
}

.fel-cam-chave{
  display: flex; align-items: center; gap: 6px;
  width: 100%;
  padding: 5px 8px;              /* o mesmo da nav, agora que não há borda */
  font: inherit;
  /* SEM `var(--font-display)`: ele mora em tokens.css, que as páginas do
     Framer não carregam (está registrado em copy.css). A variável resolvia
     para nada e o chip caía na fonte do corpo — que é, por sorte, a mesma
     dos chips FEL/WORK/ABOUT. Herdar de propósito é o que eu queria; fingir
     que escolhi uma família que não chega é que não dá. */
  font-size: 12px; line-height: 1.3; text-transform: uppercase;
  color: inherit; background: none; border: 0;
  cursor: pointer;
}
/* 15px e não 22: a altura do chip é ditada pela linha de texto (12 × 1,3 =
   15,6px), e um ícone maior que isso esticaria a peça para fora do padrão
   da nav — que é exatamente o defeito que estamos consertando. */
.fel-cam-icone{ display: block; overflow: visible; flex: none;
  opacity: .62; transition: opacity 200ms linear,
                            --fel-cam-vida 240ms var(--m-ease),
                            width var(--m-dur-1) var(--m-ease),
                            margin-right var(--m-dur-1) var(--m-ease);
  /* A POSTURA SAI DE DUAS VARIÁVEIS VEZES UMA TERCEIRA, e a terceira é a
     única coisa que o hover mexe. Ver a nota grande da respiração. */
  transform: rotate(calc(var(--fel-cam-giro) * var(--fel-cam-vida)));
  /* O PULSO, EM PORCENTAGEM — e aqui havia um erro de dois anos de idade em
     miniatura. O aceno antigo dizia `transform-origin: 12px 21px`, escrito
     como se fossem unidades do viewBox. Não são: em um elemento <svg> a
     origem é medida na CAIXA CSS, que aqui tem 15 por 15px. O pivô caía
     fora do desenho, embaixo e à direita, e o "aceno de pulso" era na
     verdade um balanço de braço inteiro em torno de um ponto no vazio.
     O pulso de verdade está em (12, 20,4) de 24 — ou seja, 50% e 85%, que
     é o que se escreve quando se quer o mesmo ponto em qualquer tamanho. */
  transform-origin: 50% 85%; }
/* O REPOUSO ESCURECE A TINTA, NÃO O PAINEL. Medido: com `opacity` no
   conjunto, a chapa também perdia metade da força e a peça quase sumia por
   cima de uma imagem clara do grid. Agora a chapa fica cheia e só a tinta
   respira — o contraste mínimo é o mesmo em qualquer fundo. */
.fel-cam:hover .fel-cam-icone,
.fel-cam:focus-within .fel-cam-icone,
.fel-cam-chave[aria-pressed="true"] .fel-cam-icone{ opacity: 1; }

/* O GRAFO DE MARCOS, em duas tintas.

   Em 10/09 isto era silhueta chapada, porque ele tinha cortado o traço;
   em 11/09 ele pediu de volta as mãos anatômicas, e o traço volta com
   elas. Fica registrado que a regra geral da casa continua sendo massa —
   este painel é a exceção, e é exceção por um motivo: ele desenha o que o
   rastreador enxerga, e o que o rastreador enxerga são pontos e ossos.

   Os OSSOS recuam (metade da tinta) e os MARCOS são cheios: é a hierarquia
   que faz a mão continuar mão quando o desenho encolhe. Sem ela o grafo
   vira uma teia de peso uniforme e o olho não acha o gesto. */
.fel-cam-mao{ display: block; }
.fel-cam-osso{
  fill: none; stroke: currentColor; opacity: .5;
  stroke-linecap: round; stroke-linejoin: round;
}
.fel-cam-marco{ fill: currentColor; }

/* O RÓTULO É SEGREDO DO HOVER — e em INGLÊS, que é a língua do site
   inteiro (FEL/WORK/ABOUT, e toda a cópia do About). "GESTOS" era a única
   palavra em português na interface, e passava despercebida só porque eu a
   escrevi.

   Ele some por `opacity` E por largura: só opacity deixaria o texto
   ocupando espaço invisível dentro de um chip de 31px, empurrando o ícone
   para fora do centro. */
/* NUNCA TRUNCAR. Eu tinha posto `text-overflow: ellipsis` como rede de
   segurança, e o resultado foi "GESTUR…" num botão de uma palavra — que é
   o pior dos dois mundos: esconde o defeito de mim e mostra para quem usa.
   Rede de segurança que disfarça erro de medida não é rede, é venda.

   `flex: none` faz o rótulo manter a largura natural em vez de encolher
   para caber; quem recorta durante a transição é o painel, com o overflow
   dele. Assim, se algum dia um texto não couber, ele aparece cortado no
   meu teste em vez de virar reticência silenciosa na tela do Felipe. */
.fel-cam-rot{
  white-space: nowrap; flex: none;
  opacity: 0; margin-left: 0;
  /* SAIR É IMEDIATO: 60ms. A palavra tem de estar fora antes de a caixa
     fechar em cima dela — texto cortado pelo próprio painel é o defeito que
     mais faz uma peça parecer mal-acabada. Os 100ms que estavam aqui não
     eram imediatos o bastante: a moldura alcançava a palavra aos 76ms (ver
     a conta em --fel-cam-atraso). 60 e 45 são um par; mudar um sem o outro
     traz o corte de volta. */
  transition: opacity 60ms linear,
              margin-left var(--m-dur-1) var(--m-ease-exit);
}
/* ENTRAR ESPERA A CAIXA. 70ms de atraso, que é quando --m-ease já abriu
   cerca de um terço dos 87px: o nome aparece DENTRO de um espaço que já
   existe, em vez de brotar por cima da borda e empurrar a moldura. */
/* `aria-expanded` E NÃO `aria-pressed`, e a diferença é o pior instante da
   peça. O `aria-pressed` só vira verdadeiro DEPOIS do await: 11,6MB de
   modelo mais o diálogo de permissão do navegador. Até lá a gaveta já está
   aberta a 190px e o rótulo diz "loading tracker…" — mas se o ponteiro
   tiver saído, a regra antiga escondia justamente essa frase. Sobrava uma
   caixa grande e vazia com um ícone de 15px, exatamente nos segundos em que
   a pessoa mais precisa saber que alguma coisa está acontecendo. Quem manda
   no rótulo é a gaveta, que é quem tem lugar para ele. */
.fel-cam:hover .fel-cam-rot,
.fel-cam:focus-within .fel-cam-rot,
.fel-cam:has(.fel-cam-chave[aria-expanded="true"]) .fel-cam-rot{
  opacity: .8; margin-left: 6px;
  /* 140ms de espera e não 70: o atraso é "um terço da caixa abrindo", e a
     caixa agora leva 420. O nome termina aos 310ms, 110 antes da moldura —
     chega dentro de um espaço que já existe, nunca por cima da borda. */
  transition: opacity 170ms linear 140ms,
              margin-left var(--m-dur-1) var(--m-ease) 140ms;
}
/* Sem ícone à frente, a margem que existia para afastá-lo dele não tem
   mais do que se afastar. Vem depois da regra acima, que pesa o mesmo
   (0,3,0), e por isso ganha por ordem. */
.fel-cam:is(:has(.fel-cam-chave[aria-expanded="true"]),
            [data-fase="lembrete"]:hover,
            [data-fase="lembrete"]:focus-within) .fel-cam-rot{ margin-left: 0; }

/* =====================================================================
   UM FATO, UM LADRILHO. DOIS FATOS, DOIS LADRILHOS.
   ---------------------------------------------------------------------
   Esta é a regra que reconcilia os dois mockups do Felipe, que pareciam
   discordar e não discordavam — cada um desenhava metade dela.

   No `panel/searching` a frase é "SHOW YOUR HAND": uma instrução, uma coisa
   só, e ele a centrou num ladrilho de largura inteira. No `panel/tracking` a
   frase é "open hand · steering": um PAR — o gesto que está sendo lido e o
   que ele está fazendo — e ele separou as duas metades para as bordas
   opostas, jogando fora o ponto do meio.

   Ele estava certo nas duas vezes, e a regra que cobre as duas é a mesma que
   já rege a fileira da demonstração logo abaixo: cada informação ganha seu
   ladrilho, e a junta faz o trabalho que o separador fazia. O ponto do meio
   some porque virou espaço — e espaço é um separador melhor, já que é o
   mesmo que separa tudo o mais na peça.

   Por que colunas IGUAIS e não conteúdo: a fileira de baixo (palavra |
   mão) é 1fr 1fr, e duas fileiras empilhadas com pontos de corte diferentes
   leem como desalinhamento, não como hierarquia. A junta do estado cai
   exatamente sobre a junta da legenda.

   Quando isto troca, na prática: uma vez por sessão. Só as frases de gesto
   têm duas partes; "show your hand", "loading tracker…" e os erros são
   instrução ou aviso, e são um fato só. Ou seja, a fileira se parte no
   instante em que a mão é encontrada — que é um acontecimento, e merece
   mesmo uma mudança de forma.

   NOVE PIXELS E NÃO DOZE. Fechado, a peça é um BOTÃO e vive ao lado de
   FEL/WORK/ABOUT: 12px, a medida da nav. Aberta, ela é um INSTRUMENTO, e
   tudo que não é o espelho vira etiqueta — o mesmo 9px da legenda de baixo.
   Duas funções, dois tamanhos, e o corte de 25% entra durante uma abertura
   que cresce 200px: ninguém vê o tipo encolher. */
.fel-cam-rot i{ font-style: normal; }
.fel-cam:is(:has(.fel-cam-chave[aria-expanded="true"]),
            [data-fase="lembrete"]:hover,
            [data-fase="lembrete"]:focus-within) .fel-cam-rot{
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 3px; flex: 1 1 auto; width: auto;
  /* A OPACIDADE VOLTA A 1 AQUI, E A TINTA É QUE PERDE FORÇA LOGO ABAIXO.
     Os 0,8 nasceram quando este elemento era só texto: num rótulo, opacidade
     e tinta são a mesma coisa. Agora ele carrega os LADRILHOS, e opacidade em
     `.fel-cam-rot` desbota a chapa deles junto — medido contra o espelho,
     que é #111 cheio: a fileira do estado ficava visivelmente mais clara que
     o resto da peça, e o ladrilhamento perdia o prumo de cor. É o mesmo erro
     que o repouso do ícone já tinha ensinado em outro canto deste arquivo:
     "o repouso escurece a TINTA, não o painel". */
  opacity: 1;
}
.fel-cam:is(:has(.fel-cam-chave[aria-expanded="true"]),
            [data-fase="lembrete"]:hover,
            [data-fase="lembrete"]:focus-within) .fel-cam-rot i{
  background: var(--fel-tinta, #111);
  display: grid; place-items: center;
  /* SEIS PIXELS DE AR, A MESMA MEDIDA QUE O DESENHO GANHA (12/09).
     -------------------------------------------------------------------
     O ladrilho nascia com 2px em volta e fechava em 16 de altura: 182 por
     16 é 11,4 para 1, uma fresta. Ao lado dele, na mesma peça, a fileira da
     legenda tem 40px porque a mão de 28 recebe 6 de respiro em cima e em
     baixo — ou seja, já existia uma regra de ar nesta peça, e o estado era
     o único que não a seguia.

     Seguindo: 12 de caixa de texto + 6 + 6 = 24, e a proporção cai para
     7,6 para 1. O horizontal continua 2, que é o módulo do ladrilho — o ar
     é do conteúdo, a moldura é do sistema, e não são o mesmo número por
     acaso nem precisam ser.

     Isso põe as duas fileiras de etiqueta em 24 e 40 (3 para 5) em volta do
     espelho, em vez de 16 e 40 (2 para 5), que era a relação que não tinha
     de onde sair. */
  padding: 6px 2px; min-width: 0;
  font-size: 9px; letter-spacing: .03em; line-height: 12px;
  overflow: hidden; text-overflow: clip;
  /* os 0,8 de antes, agora só na tinta. A declaração simples fica de reserva
     para quem não tiver color-mix: perde-se o recuo, não a legenda. */
  color: var(--fel-tinta-forte, #f5f5f5);
  color: color-mix(in srgb, var(--fel-tinta-forte, #f5f5f5) 80%, transparent);
}

/* DOIS PIXELS, ABERTO — "eu quero um padding perfeito de 2px" (Felipe, 11/09).
   ---------------------------------------------------------------------------
   Aberta, a peça deixa de ser um chip e vira um VISOR: o espelho é o
   conteúdo, e tudo o que sobra em volta dele é moldura. Dois pixels em
   volta inteira é o mínimo que ainda lê como moldura em vez de corte —
   abaixo disso o vídeo encosta na borda e o painel some como objeto.

   O cabeçalho encolhe junto (2px em todos os lados, altura 19,6 em vez de
   25,6). Se ele ficasse nos 5px de padding vertical, o vão entre o texto e
   o vídeo seria 5 enquanto os outros três lados teriam 2 — moldura desigual
   é a coisa que o pedido existe para consertar. E a perda de 6px de altura
   acontece durante uma abertura que cresce 220px: ninguém a vê.

   FECHADO CONTINUA 5px 8px, e isso é deliberado. Fechado ele É um chip, e
   vive ao lado de FEL/WORK/ABOUT — que têm exatamente esse padding, medido
   neles. Dois pixels ali deixariam a peça com 19,6px de altura ao lado de
   vizinhos de 25,6, que é o desalinhamento que o Felipe me mandou consertar
   em 10/09. Dois objetos, duas medidas: o chip é irmão da nav, o visor é
   irmão de si mesmo. */
/* ABERTO, O BOTÃO DEIXA DE SER UMA PEÇA E VIRA UMA FILEIRA.
   Sem chapa, sem moldura: quem tem fundo agora são os ladrilhos lá dentro,
   e o que era o padding de 2px do painel virou o padding de 2px de cada
   ladrilho. O vão de 3px é o mesmo de todas as outras juntas. */
.fel-cam:is(:has(.fel-cam-chave[aria-expanded="true"]),
            [data-fase="lembrete"]:hover,
            [data-fase="lembrete"]:focus-within) .fel-cam-chave{ padding: 0; gap: 3px; }

/* O ANEL DE FOCO É DA CASA, NÃO DO NAVEGADOR.
   ---------------------------------------------------------------------------
   O padrão do Chrome é um anel azul de 2px sem afastamento — a única cor
   deste site que não é minha, e ela chega justamente na peça mais cuidada da
   página. `.folha` já tinha o anel da casa (1px de tinta a 70%, afastado 6);
   este botão nunca ganhou o dele.

   `:focus-visible` e não `:focus`: quem clica não precisa de anel (já sabe
   onde está o ponteiro), quem chega pelo teclado precisa. E o afastamento é
   2px e não 6 porque aberta a peça já vale 182 por 206 — um anel a 6px de
   distância disputaria com as folhas do corredor em volta. */
.fel-cam-chave:focus-visible{
  outline: 1px solid currentColor;
  outline-offset: 2px;
}

/* NO LEMBRETE O RÓTULO SAI DA CONTA DE LARGURA, sem sair do documento.
   `width: 0` e não `display: none`: o elemento é a região `role="status"`
   que anuncia o estado do rastreador, e quem depende de leitor de tela não
   pode perder isso só porque a peça encolheu para virar um sinal luminoso.
   Zero de largura com `overflow: hidden` tira o texto do fluxo visual e o
   mantém inteiro na árvore de acessibilidade — que é a divisão de trabalho
   certa entre o que se vê e o que se ouve.

   Vem DEPOIS da regra de cima porque as duas pesam (0,2,0): com a câmera
   ligada, `aria-pressed="true"` também quer acender o rótulo, e aqui quem
   decide é a ordem. Espiando (hover/foco) o painel abre e o nome volta —
   por isso a exceção do seletor. */
/* E O ERRO ESCAPA DESTE RECOLHIMENTO. `passo()` captura exceções e chama
   `dizer(FRASES.glitch, true)` sem tocar em `data-fase` — e a fase normal de
   trabalho, depois da primeira pinça, é justamente `lembrete`. Sem a exceção
   abaixo, o painel alargava para vestir a frase enquanto esta regra (0,5,0)
   mantinha o texto em `opacity:0; width:0`: uma caixa larga e vazia, que é a
   forma mais cara possível de não dizer nada. */
.fel-cam[data-fase="lembrete"]:not(:hover):not(:focus-within):not(:has(.fel-cam-rot[data-erro="1"])) .fel-cam-rot{
  opacity: 0; width: 0; margin-left: 0; overflow: hidden;
  transition: opacity 120ms linear, width var(--m-dur-2) var(--m-ease);
}

/* COM A GAVETA ABERTA O ÍCONE SAI, e o texto vai para a margem.
   ---------------------------------------------------------------------------
   Pedido do Felipe (11/09): "ao abrir o menu da webcam, o ícone da mão pode
   sair, dando espaço pro texto alinhar à esquerda".

   Ele está apontando um desalinhamento entre os DOIS elementos que dividem
   a mesma caixa: o espelho começa a 8px da borda e o texto começava a 35 —
   depois do ícone, do vão e da margem. Vinte e sete pixels de diferença
   entre o título e a imagem que ele titula, um em cima do outro. Não há
   régua que salve isso; o que resolve é tirar o que está empurrando.

   E o ícone pode sair mesmo, porque ali ele não tem função. Fechado, ele É
   a peça — é o único jeito de saber o que aquele quadrado de 31px faz.
   Aberto, quem diz o que a peça é são o espelho e a frase de estado; a
   mãozinha vira enfeite na frente do texto.

   RECOLHE, NÃO SOME. `display: none` tiraria o ícone num quadro só, no
   exato instante do clique, enquanto a caixa ainda cresce — um estalo no
   meio de um movimento de 620ms. Largura a zero com opacidade a zero faz o
   mesmo em 200ms, acompanhando a moldura.

   A MARGEM NEGATIVA É O VÃO DO FLEX. `.fel-cam-chave` tem `gap: 6px`, e um
   item de largura zero continua sendo um item: o vão sobreviveria ao ícone
   e o texto pararia a 14px em vez de 8. Os -6px cancelam exatamente esse
   vão — e o `margin-left: 0` do rótulo, logo abaixo, cancela a margem que
   ele ganha nos outros estados. Somados: 8 + 0 - 6 + 6 + 0 = 8. O texto
   nasce na mesma vertical do espelho. */
.fel-cam:is(:has(.fel-cam-chave[aria-expanded="true"]),
            [data-fase="lembrete"]:hover,
            [data-fase="lembrete"]:focus-within) .fel-cam-icone{
  /* -3 E NÃO -6: a margem negativa existe para cancelar o VÃO do flex, e o
     vão mudou junto com o desenho — aberto, `.fel-cam-chave` usa a junta de
     3px como todo o resto da peça, não os 6px do chip fechado. Com -6 a
     fileira do estado saía 3px à esquerda da borda do painel e media 185
     numa caixa de 182: medido, `.fel-cam-rot` em x=546 contra x=549 do
     painel. Um vão cancelado com o número de outro estado. */
  width: 0; opacity: 0; margin-right: -3px;
}

/* A LUZ DE ESTADO SAIU em 11/09, junto do pedido de "só o ícone". Ela era
   redundante por construção: a gaveta só abre com a câmera ligada, então o
   painel aberto JÁ é o estado, em tamanho de painel. Dois indicadores do
   mesmo fato num chip de 31px é um a mais. */

/* ===========================================================================
   A MÃO RESPIRA — e o hover a faz PARAR.

   Eu tinha escrito aqui, com todas as letras, que laço no topo da página é
   pisca-pisca e vira ruído depois de dois ciclos. O Felipe pediu o
   contrário ("a mãozinha fica mexendo") e ele tem razão, mas não porque a
   objeção fosse boba — e sim porque ela era uma objeção a uma AMPLITUDE,
   disfarçada de objeção a um princípio. O que cansa não é haver movimento:
   é movimento rápido, largo e regular. Um aceno de 9 graus em 620ms com
   curva de mola é publicidade. Isto aqui é outra coisa.

   AS MEDIDAS, e todas saem da mesma conta. O viewBox de 24 unidades cai em
   15px de tela, então 1 unidade = 0,625px. O pulso está em (12, 20,4) e a
   ponta do dedo médio em (11,8, 5,2) — 15,20 unidades de distância, ou
   9,50px depois da redução. Logo cada grau de rotação move a ponta do dedo
   0,166px. A varredura escolhida vai de -5,5° a +3°: 8,5 graus, 1,41px de
   percurso na ponta. Um pixel e meio.

   É POUCO? É — para ler PARADO. Não é para ler EM MOVIMENTO: a visão
   periférica detecta deslocamento muito antes de resolver forma, e é por
   isso que este número pode ser tão baixo. A mão não chama; ela só deixa
   de parecer um glifo. Em 6,2 segundos de volta (≈0,16 Hz, que é a ordem
   da deriva postural de uma mão parada no ar de verdade) o movimento nunca
   é rápido o bastante para disputar com o corredor.

   TRÊS QUADROS-CHAVE E NENHUM NO MEIO DO CAMINHO. Os pontos são só os
   extremos (26% e 62%), espaçados de forma desigual — 26, 36 e 38% do
   ciclo — para que a volta não seja a ida ao contrário. Um quadro-chave no
   meio de um trecho faria a curva desacelerar ali, e desacelerar no meio de
   um gesto lê como engasgo.

   O HOVER TIRA O MOVIMENTO em vez de acrescentar. É o inverso do aceno
   antigo e é a versão certa da mesma ideia: a mão está viva enquanto
   espera, e se AQUIETA quando alguém vai pegá-la. Quem chegou com o
   ponteiro já foi convencido — a partir dali o movimento só atrapalha a
   leitura do nome que está aparecendo ao lado.

   E O AQUIETAR PRECISOU DE UM TRUQUE, porque a forma óbvia não funciona.
   -------------------------------------------------------------------------
   A primeira versão era `animation: none` no hover, com uma transição de
   240ms em `transform` para amortecer a volta ao prumo. Medido aqui, quadro
   a quadro: o ângulo ia de -5,5° a 0° no PRIMEIRO quadro. Zero amortecimento.
   O motivo é de especificação e não de navegador: tirar uma animação não é
   uma mudança de estilo declarado, e transição só dispara com mudança de
   estilo declarado. A mão dava um estalo — exatamente o defeito que a
   transição existia para evitar, e invisível para quem só lê o código.

   Então a animação parou de escrever em `transform`. Ela escreve numa
   variável registrada — `--fel-cam-giro` — e o `transform` lá em cima é uma
   declaração comum que a multiplica por uma segunda, `--fel-cam-vida`. O
   hover transiciona só essa segunda, de 1 para 0.

   O que isso muda na tela: a mão não PARA, ela se ACALMA. A amplitude do
   balanço encolhe até zero em 240ms enquanto o embalo continua correndo por
   baixo, e quando o ponteiro sai ela volta a crescer. É a diferença entre
   uma pessoa que congela e uma que descansa a mão — e é, por acaso, bem
   mais fiel ao que a peça está ilustrando. */
.fel-cam-icone{
  animation: fel-cam-respira 6.2s var(--m-ease) infinite;
}
/* `aria-pressed` VOLTA AQUI ao lado do `aria-expanded`, e a omissão era
   minha: ao trocar um pelo outro para consertar o rótulo durante o
   carregamento, deixei a mão respirando na fase `lembrete` — câmera ligada,
   gaveta fechada. É o estado em que ela menos deveria se mexer: ali o ícone
   não convida mais nada, ele informa que a webcam está no ar, ao lado de um
   sinal verde que existe justamente por não se mexer. */
.fel-cam:hover .fel-cam-icone,
.fel-cam:focus-within .fel-cam-icone,
.fel-cam-chave[aria-pressed="true"] .fel-cam-icone,
.fel-cam:has(.fel-cam-chave[aria-expanded="true"]) .fel-cam-icone{ --fel-cam-vida: 0; }
/* A MÃO NASCE RESPIRANDO, e isso apagou o último buraco da sequência.

   Eu tinha posto um silêncio de quase um segundo entre o recolhimento e o
   primeiro movimento, com um argumento que soava bem: "primeiro a peça diz
   o que é e vira ícone; SÓ DEPOIS o ícone revela que está vivo". O problema
   é que duas ideias separadas por um buraco de um segundo não são duas
   ideias — são um efeito e, mais tarde, outro. Somando a sequência inteira:
   3,3s, dos quais 2,1 eram imobilidade, em três vãos distintos. Era disso
   que vinha a sensação de coisas acontecendo em fila em vez de um movimento.

   Agora a junta usa o eixo que já existia — `--fel-cam-vida`, a amplitude.
   O laço corre desde o monte, mas com amplitude ZERO durante a
   apresentação: invisível, e com o relógio andando. No recolhimento a
   amplitude sobe de 0 a 1 pelos mesmos 420ms da largura, e a mão ganha vida
   na exata medida em que o chip vira mão. Não há transferência para
   esconder porque não há transferência: é a mesma animação o tempo todo, e
   o que muda é o volume.

   Três consumidores num eixo só — a entrada, o hover e a câmera ligada. É
   o contrário de acrescentar estado. */
.fel-cam[data-entrada="1"] .fel-cam-icone{ --fel-cam-vida: 0; }
.fel-cam[data-entrada="2"] .fel-cam-icone{
  --fel-cam-vida: 1;
  transition: --fel-cam-vida var(--m-dur-2) var(--m-ease);
}
/* SÓ ROTAÇÃO — o flutuar vertical de 0,35px saiu, e sair foi o conserto.
   Num traço de 1,2 unidades (0,75px depois da redução para 15px), deslocar
   um terço de pixel na vertical não MOVE o traço: muda a cobertura de
   antialiasing das mesmas linhas. O que se veria não é uma mão respirando e
   sim o desenho piscando de brilho — o único artefato aqui que lê como
   defeito de renderização em vez de vida. Girando em torno do pulso, os
   traços perto do pivô deslizam sobre si mesmos e a cobertura quase não
   muda; o arco de 1,41px na ponta do dedo médio continua inteiro. Menos uma
   propriedade, menos um registro, e o movimento fica mais limpo. */
@keyframes fel-cam-respira{
  0%, 100% { --fel-cam-giro:  0deg;   }
  26%      { --fel-cam-giro: -5.5deg; }
  62%      { --fel-cam-giro:  3deg;   }
}
/* Registradas, senão não interpolam: uma variável não registrada é texto
   para o motor, e trocaria de valor de uma vez na metade do trecho — o
   mesmo motivo do --fel-tinta lá embaixo. Com `inherits: false` porque
   ninguém abaixo do ícone precisa delas, e herança à toa é trabalho de
   recálculo em cada descendente a cada quadro. */
@property --fel-cam-giro{ syntax: '<angle>';  inherits: false; initial-value: 0deg; }
@property --fel-cam-vida{ syntax: '<number>'; inherits: false; initial-value: 1;    }

/* =================================================== OS TRÊS PASSOS
   Uma mão só, passando pelos três gestos em laço de 5,4s (1,8s cada), com
   o nome embaixo em sincronia. Some quando uma mão de verdade aparece.

   O laço é CSS e não JS de propósito: um timer em JS precisaria ser criado,
   pausado com a aba escondida e destruído no desligar — três lugares para
   vazar. A animação declarativa o navegador já pausa e limpa sozinho. */
/* A LEGENDA DEITOU, E FICOU 24px MAIS BAIXA.
   ---------------------------------------------------------------------------
   Era uma mão de 40px EMPILHADA sobre uma frase de largura inteira: 64px de
   altura para dizer uma coisa. O Felipe pôs os dois lado a lado, em dois
   ladrilhos iguais, e a fileira caiu para 40.

   E a repartição muda o que cada metade precisa dizer. Antes a frase carregava
   as duas informações — "FIST · OPEN" — com o desenho repetindo a primeira.
   Agora o DESENHO é o gesto e a PALAVRA é a consequência: um punho ao lado de
   OPEN. Nenhuma das duas metades repete a outra, e a palavra ganha o espaço
   que gastava dizendo o que já estava desenhado ao lado.

   É por isso também que a mão subiu de 20px (o tamanho que ele deixou no
   mockup) para 28: com a palavra encurtada, o ladrilho do desenho não tem
   mais nada para dividir com ninguém, e um grafo de landmarks a 20px vira
   uma teia. 28 num ladrilho de 40 deixa 6px de respiro em cima e embaixo,
   que é a mesma proporção do ladrilho de texto. */
.fel-cam-passos{
  display: grid; grid-template-columns: 1fr 1fr; gap: 3px;
  padding: 0;
  overflow: hidden;
  transition: max-height 380ms cubic-bezier(.2,.7,.2,1),
              opacity 240ms linear, padding 380ms cubic-bezier(.2,.7,.2,1);
  max-height: 40px;
}
/* Cumpriu o papel: a pessoa travou uma folha com a pinça. A demonstração
   recolhe, e não volta nesta sessão. */
.fel-cam[data-fase="lembrete"] .fel-cam-passos{
  /* `margin-top` junto, pelo mesmo motivo do `data-mao`: um item de altura
     zero continua consumindo o `gap` de 3px e deixaria uma junta de vidro
     sem nada do outro lado na base do painel. Duas portas para o mesmo
     recolhimento, o mesmo remédio nas duas. */
  max-height: 0; opacity: 0; padding: 0; margin-top: -3px;
}
/* E A REGRA QUE RECOLHIA A DEMONSTRAÇÃO COM A MÃO EM QUADRO SAIU (13/09).
   ---------------------------------------------------------------------------
   Eu a escrevi em 12/09 lendo "depois que a pessoa já aprendeu os movimentos,
   ele pode desaparecer" como "assim que ela mostra a mão". São coisas
   diferentes, e a diferença é o tutorial inteiro: mostrar a mão é o PRIMEIRO
   passo, não o último. A regra fazia a lição sumir 1,5s depois de a pessoa
   começar a tentar — exatamente quando ela precisava ver a pinça e o punho.

   "Tenta manter esse passo a passo onde a pessoa mostra a mão, e aí a gente
   vai ensinando a ela os movimentos de fechar a mão ou de fazer a pinça"
   (Felipe, 13/09).

   E o pior é que a saída por COMPETÊNCIA já existia e estava certa, no
   camera.js: a fase `vivo` vira `lembrete` quando ela TRAVA UMA FOLHA COM A
   PINÇA (`provou`) ou desiste (`tetoVivo`), nunca antes de um ciclo inteiro
   da demonstração ter passado. Eu não acrescentei um estado do meio: eu pus
   um atalho por cima de um fluxo que já sabia a diferença entre estar
   presente e ter aprendido. */
/* Os dois ladrilhos da fileira. `position: relative` nos dois porque as três
   mãos e as três palavras se empilham em absoluto no mesmo lugar — é isso que
   faz o laço trocar sem a caixa mudar de tamanho. */
.fel-cam-palco{
  position: relative; height: 40px;
  background: var(--fel-tinta, #111);
}
.fel-cam-palco .fel-cam-mao{
  position: absolute; top: 50%; left: 50%;
  width: 28px; height: 28px; margin: -14px 0 0 -14px;
  opacity: 0;
}
.fel-cam-dizeres{
  position: relative; height: 40px;
  background: var(--fel-tinta, #111);
}
.fel-cam-dizeres span{
  position: absolute; inset: 0;
  display: grid; place-items: center;
  text-align: center; white-space: nowrap;
  font-size: 9px; letter-spacing: .1em; opacity: 0;
}
/* Um mesmo par de keyframes para os três: o que muda é o atraso. */
/* NOVE SEGUNDOS, NÃO 5,4 (Felipe, 13/09: "não tá durando o tempo necessário
   para a pessoa realmente entender como fazer").

   1,8s por gesto dava para LER a palavra e não dava para FAZER o gesto —
   são coisas com custos diferentes, e o passo estava dimensionado para a
   barata. Ler "pinch" leva meio segundo; olhar o desenho, entender que os
   dedos se encontram, mudar a própria mão e comparar leva vários. A 3s por
   gesto (25% de 9s de janela cheia, mais os cruzamentos) sobra tempo para
   tentar antes de o desenho trocar debaixo da tentativa.

   `pisoVivo` acompanha no camera.js: ele É "um ciclo inteiro da demonstração",
   e um piso que não acompanha a duração deixa de ser o que o nome diz. */
.fel-cam-palco .p1, .fel-cam-dizeres .p1{ animation: fel-passo 9s linear infinite;          }
.fel-cam-palco .p2, .fel-cam-dizeres .p2{ animation: fel-passo 9s linear infinite  3s;      }
.fel-cam-palco .p3, .fel-cam-dizeres .p3{ animation: fel-passo 9s linear infinite  6s;      }

/* O LAÇO FICA PARADO ATÉ A GAVETA ABRIR, e isto conserta um defeito que só
   apareceu quando alguém contou.

   A animação começava no MONTE do painel (~2,6s depois do load), e não na
   abertura. A gaveta fechada é `grid-template-rows: 0fr` com overflow
   hidden — a caixa existe, está renderizada, e a linha de tempo corre
   dentro dela. Quando a pessoa finalmente clicava, o laço estava num ponto
   qualquer: medido, 33% das vezes no passo 1, 33% no 2 e 33% no 3.

   Ou seja: em dois terços das sessões a primeira coisa ensinada NÃO era a
   mão aberta — e em um terço era o PUNHO, que é o fim da escalada. Ensinar
   "feche a mão para abrir" antes de "aponte" inverte a única coisa que a
   ordem dos três gestos existe para dizer.

   Pausada desde o monte, a animação fica com tempo decorrido zero e os
   atrasos de 1,8s e 3,6s não são consumidos. Ao abrir, o laço parte do
   passo 1 em 100% das sessões.

   DEPOIS do shorthand `animation:` acima, obrigatoriamente: o shorthand
   reseta `animation-play-state` para `running`. */
/* AS LEGENDAS PRECISAM SER NOMEADAS UMA A UMA, e esse detalhe de
   especificidade estava arruinando a demonstração inteira.

   `.fel-cam-dizeres span` pesa (0,1,1). Vinte e quatro linhas acima,
   `.fel-cam-dizeres .p1` pesa (0,2,0) e traz o shorthand `animation:`, que
   RESETA animation-play-state para `running`. Especificidade decide antes
   de ordem de origem, então a pausa nunca chegava às legendas: só as mãos
   paravam.

   Medido no navegador, no painel fechado: mão em currentTime 0, legenda em
   117.828ms — vinte e uma voltas e meia adiante, num laço de 5,4s. Quando
   a gaveta abria, a mão partia do passo 1 e a legenda estava onde calhasse.
   A pessoa via uma mão aberta escrita "fist · open". Todo o cuidado com o
   passo 1 vir sempre primeiro estava sendo desfeito pela linha de baixo.

   Nomeando as três com o mesmo peso (0,2,0), a ordem de origem volta a
   valer e a pausa alcança as seis animações. */
.fel-cam-palco .fel-cam-mao,
.fel-cam-dizeres .p1,
.fel-cam-dizeres .p2,
.fel-cam-dizeres .p3{ animation-play-state: paused; }
.fel-cam[data-fase="pedindo"] .fel-cam-palco .fel-cam-mao,
.fel-cam[data-fase="pedindo"] .fel-cam-dizeres span,
.fel-cam[data-fase="vivo"] .fel-cam-palco .fel-cam-mao,
.fel-cam[data-fase="vivo"] .fel-cam-dizeres span{ animation-play-state: running; }
/* 36% e não 33%: eu tinha escrito "com um respiro de cruzamento" e não
   havia cruzamento nenhum. A janela de um passo terminava em 33% do ciclo e
   o seguinte só começava em 33,33% — um VÃO ESCURO entre os dois, não
   sobreposição. Levando o fim para 36% sobram 2,67% de ciclo de cruzamento
   de verdade: eram 144ms no laço de 5,4s, são 240ms nos 9s de hoje.

   Em porcentagem de propósito: a duração já mudou uma vez (13/09) e vai
   mudar de novo. Quadros-chave proporcionais atravessam isso sem ninguém
   ter de refazer três números à mão. */
@keyframes fel-passo{
  0%    { opacity: 0; transform: scale(.9); }
  5%    { opacity: 1; transform: scale(1); }
  30%   { opacity: 1; transform: scale(1); }
  36%   { opacity: 0; transform: scale(.94); }
  100%  { opacity: 0; transform: scale(.9); }
}
/* O texto não escala junto — tipografia crescendo lê como defeito. */
@keyframes fel-passo-txt{
  0%, 36%, 100%{ opacity: 0 } 5%, 30%{ opacity: 1 }
}
.fel-cam-dizeres .p1{ animation-name: fel-passo-txt }
.fel-cam-dizeres .p2{ animation-name: fel-passo-txt }
.fel-cam-dizeres .p3{ animation-name: fel-passo-txt }

/* A lista continua no documento, invisível, para leitor de tela: a
   animação é `aria-hidden`, e quem não vê animação nenhuma ainda precisa
   saber quais são os três gestos. */
.visually-hidden{
  position: absolute !important; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ------------------------------------------------------- a gaveta que desce */
.fel-cam-corpo{
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows var(--m-dur-1) var(--m-ease-exit);
}
.fel-cam-dentro{
  overflow: hidden;
  display: flex; flex-direction: column; gap: 3px;   /* a junta, igual em toda a peça */
  /* Largura fixa no conteúdo: senão ele encolhe junto com o painel durante
     a animação e a câmera "espreme" enquanto desce, em vez de ser revelada.

     FIXA, MAS DERIVADA. Era `172px` escrito à mão contra um painel de 190:
     172 + 8 + 8 = 188 dentro de 190, e os 2px que sobravam iam todos para o
     lado direito porque a coluna é alinhada ao início. Medido no navegador:
     o espelho ficava com 8px de respiro à esquerda e 10 à direita. Dois
     pixels — e ainda assim visíveis, porque um retângulo desalinhado dentro
     de uma moldura é a coisa que o olho mais rápido pega.

     Agora sai da própria largura do painel, que é medida: qualquer que seja
     ela, sobra exatamente 8 de cada lado. Um número a menos para manter
     sincronizado à mão, e simetria por construção em vez de por sorte. */
  /* LARGURA CHEIA, SEM MOLDURA. Os 2px de cada lado eram a moldura do
     desenho antigo; agora o ladrilho vai de borda a borda e quem molda é a
     junta, que é interna. Um número a menos entre a medida e a peça: o
     conteúdo mede exatamente o que o painel mede. */
  width: var(--fel-cam-larga, 182px); box-sizing: content-box;
  padding: 0;
  /* Começa recolhido para cima: é o que faz a câmera DESCER por trás do
     cabeçalho em vez de crescer a partir do nada. */
  transform: translateY(-10px);
  opacity: 0;
  transition: transform var(--m-dur-1) var(--m-ease-exit),
              opacity 120ms linear,
              padding-top var(--m-dur-1) var(--m-ease-exit),
              padding-bottom var(--m-dur-1) var(--m-ease-exit);
}
/* UMA GAVETA, UMA CHEGADA. Antes eram quatro relógios com quatro linhas de
   chegada: largura aos 200ms, opacidade do conteúdo aos 400, altura e
   deslocamento aos 620. O conteúdo ficava inteiramente opaco 220ms antes de
   a caixa terminar de abrir — ou seja, a última fração do movimento era uma
   moldura crescendo em volta de algo que já estava pronto. É disso que vem
   a sensação de "quase acabou... ah, ainda não".

   Agora a altura, o deslocamento e a opacidade partem juntos aos 200ms (a
   largura terminando) e chegam juntos aos 620. Uma coisa só se movendo.

   O `padding-bottom` entrou na lista, e ele estava de fora das DUAS. Na
   abertura isso quase não se via; no fechamento, sim — os 8px sumiam num
   quadro só, e a gaveta dava um tranco de 8px para cima antes de recolher.
   Uma propriedade esquecida numa lista de transição é sempre um estalo. */
.fel-cam-chave[aria-expanded="true"] ~ .fel-cam-corpo,
.fel-cam[data-fase="lembrete"]:hover .fel-cam-corpo,
.fel-cam[data-fase="lembrete"]:focus-within .fel-cam-corpo{
  grid-template-rows: 1fr;
  /* o atraso é a largura terminando de abrir: 200ms */
  transition: grid-template-rows var(--m-dur-2) var(--m-ease) var(--m-dur-1);
}
/* O RESPIRO DE CIMA SÓ EXISTE COM A GAVETA ABERTA, e a primeira versão
   colocou-o na regra de base — onde ele VAZAVA.

   A gaveta fechada é `grid-template-rows: 0fr`: a faixa tem altura zero e o
   conteúdo é cortado. Mas `.fel-cam-dentro` é `box-sizing: content-box`, e
   num content-box o padding fica FORA da altura. Com altura zero e 3px de
   padding em cima, a caixa de borda media 3px — e transbordava a faixa.

   Resultado medido: o chip fechado passou de 25,6px de altura para 28,6.
   Doze por cento mais alto que os chips FEL/WORK/ABOUT, que é exatamente o
   desalinhamento que eu tinha consertado em 10/09 a pedido do Felipe. Três
   pixels de preto vazio embaixo do texto, em todo estado fechado — foi o
   que ele viu na captura da mensagem de erro.

   Aqui em cima, junto do `padding-bottom`, o respiro só vale quando há
   gaveta para respirar. E entra na transição pelo mesmo motivo que o de
   baixo: propriedade fora da lista é estalo. */
.fel-cam-chave[aria-expanded="true"] ~ .fel-cam-corpo .fel-cam-dentro,
.fel-cam[data-fase="lembrete"]:hover .fel-cam-dentro,
.fel-cam[data-fase="lembrete"]:focus-within .fel-cam-dentro{
  transform: translateY(0); opacity: 1;
  /* Zero nos dois: o vão entre o estado e o espelho é a junta de 3px do
     próprio painel, e embaixo o ladrilho da legenda termina a peça. Continuam
     declarados (e na lista de transição) porque uma propriedade que sai da
     lista é um estalo — a lição que o padding-bottom já deu aqui. */
  padding-top: 0; padding-bottom: 0;
  transition: transform var(--m-dur-2) var(--m-ease) var(--m-dur-1),
              opacity var(--m-dur-2) linear var(--m-dur-1),
              padding-top var(--m-dur-2) var(--m-ease) var(--m-dur-1),
              padding-bottom var(--m-dur-2) var(--m-ease) var(--m-dur-1);
}

/* O ESPELHO fica visível enquanto a câmera roda. Esconder deixaria a tela
   mais limpa e tiraria da pessoa a única forma de ver o que está sendo
   captado — que é justamente o que se deve a quem abriu a própria câmera. */
/* O VISOR É O LADRILHO DO ESPELHO, e agora ele tem duas camadas: o vídeo e
   o grafo da mão desenhado por cima. Ver `desenharTracos` no camera.js. */
.fel-cam-visor{
  position: relative; display: block; width: 100%;
  aspect-ratio: 4 / 3; background: #111;
  overflow: hidden;
}
.fel-cam-espelho{
  display: block; width: 100%; height: 100%;
  object-fit: cover; transform: scaleX(-1);
  background: #111;               /* sem moldura: o próprio vídeo é a borda */
}
/* Os traços NÃO são espelhados por CSS: quem espelha é a conta que projeta
   cada landmark, porque ela precisa saber do recorte do `object-fit: cover`
   de qualquer forma. Espelhar duas vezes — uma no canvas, outra aqui — é o
   tipo de simetria que parece certa e desalinha no dia em que a webcam não
   for 4:3. */
.fel-cam-tracos{
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none;
}
/* O rótulo do cabeçalho carrega o estado quando o painel está aberto, e o
   nome quando está fechado. Um erro vira laranja, como a linha antiga fazia. */
/* O ERRO ACENDE O RÓTULO POR CONTA PRÓPRIA — e por isso precisa repetir a
   margem, que é a única coisa que ele tomava emprestado e não recebia.

   O rótulo só ganha `margin-left: 6px` nas regras de hover, foco e gaveta
   aberta. No estado de erro nenhuma das três vale: a gaveta fecha, o
   ponteiro pode estar longe, e quem torna o texto visível é esta regra
   sozinha. Resultado medido: o vão entre o ícone e a frase era de 6px aqui
   contra 12 em todos os outros estados, e o chip saía 6px largo demais
   porque a MOLDURA do camera.js conta os 12. Duas assimetrias de uma
   omissão só — o texto encostado no ícone à esquerda e sobrando à direita.

   `opacity: 1` e não `.8` porque um erro tem de ser lido, não sugerido. */
/* `.fel-cam` NA FRENTE, e não é enfeite de seletor.
   `.fel-cam:hover .fel-cam-rot{ opacity:.8 }` pesa (0,3,0); isto pesava
   (0,2,0) e PERDIA — por especificidade, não por ordem, então nenhuma
   reordenação salvava. E o estado de erro nasce, por construção, com o
   ponteiro em cima do botão: `falhar()` só é chamado a partir do clique, e
   ele encolhe o painel em torno do mesmo centro, de modo que o cursor
   continua sobre a chave. Ou seja: a mensagem estreava sempre apagada a 80%,
   justo no instante em que ela é a única coisa que a peça tem a dizer. */
.fel-cam .fel-cam-rot[data-erro="1"]{ color: #d98b6a; opacity: 1; margin-left: 6px; }
/* E COM ERRO O CHIP ABRE SOZINHO, sem gaveta. Uma frase de falha dentro de
   um chip de 31px é uma frase que ninguém lê; o `:has` é o que permite a
   moldura reagir a um estado que mora no filho. Volta ao normal quando
   `falhar()` devolve o rótulo ao nome, 5,2s depois — ver camera.js. */
/* O CHIP DE ERRO VESTE A FRASE — MAS SÓ COM A GAVETA FECHADA (13/09).
   ---------------------------------------------------------------------------
   Este é o bug do print: "ao clicar, ele abre um tipo de navegação que aparece
   meio cropado; depois se reajusta".

   A cadeia inteira, e ela é minha, de hoje de manhã:

     1. movi a escrita de `--fel-cam-frase` para dentro de `dizer`, para que o
        caminho do `glitch` (exceção no laço) também vestisse a medida certa;
     2. `glitch` acontece com a GAVETA ABERTA — é uma falha de quadro, não de
        partida — e a palavra é curta: a variável passa a valer ~83px;
     3. esta regra pesa (0,3,0), a mesma da regra de gaveta aberta, e vem
        DEPOIS no arquivo: ganha por ordem;
     4. o painel encolhe para 83px com um espelho de 182 dentro. Medido:
        99px de espelho cortado.

     E o "depois se reajusta" são os 3s de `mudoAte`: passado o silêncio, a
     legenda volta, `data-erro` cai, a variável é removida e o painel devolve
     os 182. O conserto não é a largura — é a PREMISSA que eu quebrei ao mover
     a escrita.

   Antes de 13/09 a premissa era verdadeira por acidente: só `falhar()` escrevia
   a variável, e `falhar()` FECHA a gaveta na mesma função. Largura estreita e
   gaveta fechada sempre viajaram juntas; eu separei as duas sem notar que uma
   dependia da outra.

   Agora está dito: a medida da frase é do CHIP, e chip é o estado fechado.
   Com a gaveta aberta — por clique ou por espiada — quem manda é a largura do
   espelho, e a frase de erro cabe no ladrilho do estado, que já tem 182. */
.fel-cam:has(.fel-cam-rot[data-erro="1"]):has(.fel-cam-chave[aria-expanded="false"]):not([data-fase="lembrete"]:hover):not([data-fase="lembrete"]:focus-within){
  --fel-cam-l: var(--fel-cam-frase, var(--fel-cam-larga, 182px));
}

.fel-cam-guia{
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 3px;
  font-size: 9px; letter-spacing: .1em;
}
/* Duas opacidades multiplicando davam .45 × .7 = .32 — ilegível. Agora a
   opacidade mora num lugar só, e o NOME do gesto pesa mais que o que ele
   faz: é o nome que a pessoa tem de guardar. */
.fel-cam-guia li{ opacity: .42; transition: opacity 160ms linear; }
.fel-cam-guia li b{ font-weight: 400; opacity: 1; margin-right: 5px; }
.fel-cam-guia li{ color: currentColor; }
/* Quem acende é o JS (uma classe), e não um seletor de irmão. */
.fel-cam-guia li.on{ opacity: 1; }
.fel-cam-guia[hidden]{ display: none !important; }

@media (prefers-reduced-motion: reduce){
  .fel-cam-rot, .fel-cam-icone,
  .fel-cam-corpo, .fel-cam-dentro{ transition: none; }
  /* O PAINEL FICA DE FORA DESSA LISTA, e a razão é que eu tinha escrito
     logo abaixo que "a apresentação em si continua" — e não continuava. Com
     `.fel-cam{ transition: none }` a peça surgia inteira num quadro, a 110px,
     e 1,6s depois saltava para 31. Dois cortes secos, documentados como uma
     transição macia. O comentário descrevia a intenção; a regra fazia o
     contrário, e comentário que mente é pior que comentário nenhum.

     Fade NÃO É MOVIMENTO — é a substituição recomendada para movimento, e
     quem marcou a preferência não está pedindo uma tela que pisca. A
     largura fica: ela é a própria informação (o nome recolhendo), e é
     lateral, não parallax. */
  .fel-cam{ transition: opacity var(--m-dur-2) linear,
                        width var(--m-dur-2) var(--m-ease); }
  .fel-cam-chave[aria-expanded="true"] ~ .fel-cam-corpo,
  .fel-cam-chave[aria-expanded="true"] ~ .fel-cam-corpo .fel-cam-dentro{ transition: none; }
  /* Sem a rampa, quem faz o botão sumir no scroll é o data-fora do JS. */
  .fel-cam{ animation: none; }
  /* A RESPIRAÇÃO SAI INTEIRA. Um laço infinito é precisamente a coisa que
     `prefers-reduced-motion` existe para desligar — quem marcou essa
     preferência muitas vezes o fez por enjoo de movimento, e movimento
     perpétuo no alto da tela é o pior caso. `!important` porque a regra
     acima é mais específica (`.fel-cam[data-entrada] .fel-cam-icone`) e
     aqui não pode haver "quase". */
  .fel-cam-icone{ animation: none !important; }
  /* Sem movimento, os três passos viram três quadros parados sobrepostos —
     ilegível. Então a animação sai e a LISTA volta a ser visível. */
  .fel-cam-passos{ display: none; }
  /* E o `running` não pode vazar para cá: sem movimento, os três quadros
     ficariam sobrepostos e ilegíveis. */
  .fel-cam-palco .fel-cam-mao, .fel-cam-dizeres span{ animation: none !important; }
  .fel-cam-guia.visually-hidden{
    position: static !important; width: auto; height: auto; margin: 0;
    overflow: visible; clip-path: none; white-space: normal;
  }
}

/* A TINTA VIRA COM O FUNDO. O topo é branco no hero e preto depois da seção
   sobre, e o painel é fixo: tinta fixa sumiria numa das duas pontas. Mesmo
   trecho de scroll do apagamento (.fel-hero::after) — a tinta e o fundo
   viram juntos, sem JS, sem observer, sem ouvir scroll.

   @property NÃO É ENFEITE AQUI. Uma variável não registrada é texto para o
   navegador: ela até "anima", mas troca de valor de uma vez na metade do
   trecho, e o painel piscaria de preto para branco num degrau. Registrar
   como <color> é o que ensina o motor a interpolar. Mesmo motivo do
   --rodape-t. */
@property --fel-tinta{
  syntax: '<color>';
  inherits: true;
  initial-value: #111111;
}
@property --fel-chapa{
  syntax: '<color>';
  inherits: true;
  initial-value: #ffffff8c;
}
@property --fel-chapa-forte{
  syntax: '<color>';
  inherits: true;
  initial-value: #000000;
}
@property --fel-tinta-forte{
  syntax: '<color>';
  inherits: true;
  initial-value: #f5f5f5;
}
/* OS QUATRO VALORES, E TRÊS DELES SÃO TOKENS DA CASA. Só a tinta clara é
   escrita aqui, e mesmo ela é o mesmo #f5f5f5 que a nav usa por cima do
   preto. Nada nesta lista é uma cor nova. */
:root{
  --fel-tinta: #111;             --fel-chapa: var(--glass-face);
  --fel-tinta-forte: #f5f5f5;    --fel-chapa-forte: var(--glass-face-hover);
}
@supports (animation-timeline: scroll()) and (animation-range: 0% 100%){
  body:has(.fel-hero){
    animation: fel-cam-tinta linear both;
    animation-timeline: scroll(root);
    animation-range: 78vh 110vh;
  }
}
/* A PONTA ESCURA ESPELHA A VARIANTE DARK DA NAV, que está medida do
   original em framer-runtime.css: fundo rgb(18,18,18), e o hover devolve o
   vidro. É exatamente a inversão da ponta clara, que é o que se espera de
   uma peça e o seu negativo.

   FICA REGISTRADO que este trecho quase não é exercido: a rampa de scroll
   apaga o painel aos 34vh e a tinta só começa a virar aos 78vh. Sobra a
   janela em que a CÂMERA ESTÁ LIGADA — aí a rampa não se aplica, o painel
   segue visível, e o desligamento automático só acontece aos 70vh. Uma
   rolagem violenta pode pular de 60 para 90vh num quadro só e mostrar o
   painel meio virado por um instante. É para esse instante que isto existe.
   Não apago porque a alternativa é um painel branco sobre fundo preto, e
   uma rede que só é usada uma vez por ano ainda é uma rede. */
@keyframes fel-cam-tinta{
  from{ --fel-tinta: #111;
        --fel-chapa: var(--glass-face);   --fel-chapa-forte: var(--glass-face-hover);
        --fel-tinta-forte: #f5f5f5; }
  to  { --fel-tinta: #f2f2f2;
        --fel-chapa: rgb(18, 18, 18);     --fel-chapa-forte: var(--glass-face);
        --fel-tinta-forte: #f2f2f2; }
}

/* A MIRA — onde a mão está e QUAL GESTO ela está fazendo.

   Três formas, uma por gesto, e a diferença entre elas é grande de propósito:
   o Felipe pediu interação "muito mais óbvia", e um cursor que muda de dois
   pixels não comunica nada. Anel largo aberto, ponto cheio, anel apertado.

   ABERTA  anel grande e vazado — a mão está pilotando, o campo obedece.
   PUNHO   disco cheio — pegou; o campo desacelera.
   PINÇA   anel pequeno e grosso, fechando sobre o alvo. */
.fel-cam-mira{
  position: fixed; left: 0; top: 0; z-index: 69;
  width: 64px; height: 64px; margin: -32px 0 0 -32px;
  pointer-events: none;
  border-radius: 50%;
  border: 1px solid #ffffff66;
  transition:
    width 180ms var(--m-ease, ease), height 180ms var(--m-ease, ease),
    margin 180ms var(--m-ease, ease), border-width 180ms var(--m-ease, ease),
    background-color 180ms linear, opacity 160ms linear;
}
/* Sem mão em quadro a mira some — apontar para onde a mão não está é mentira. */
.fel-cam-mira[data-gesto="nenhum"]{ opacity: 0; }

.fel-cam-mira[data-gesto="aberta"]{
  width: 74px; height: 74px; margin: -37px 0 0 -37px;
  border-width: 1px; background: #ffffff10;
}
.fel-cam-mira[data-gesto="punho"]{
  width: 26px; height: 26px; margin: -13px 0 0 -13px;
  border-width: 2px; background: #f2f2f2;
}
/* O VERDE É O TEMPO. Punho sem alvo só fecha o anel branco — descansar
   o braço não é clique. Punho COM alvo enche de #35d07f (o mesmo verde
   do ponto de câmera ligada) até completar o círculo: 1,5s e o projeto
   abre. Soltar zera. A mira não inventa um quarto tamanho; ela pinta
   o que já existe. */
.fel-cam-mira[data-gesto="punho"][data-segura="1"]{
  background: conic-gradient(#35d07f calc(var(--fel-punho, 0) * 360deg), #f2f2f2 0);
}
.fel-cam-mira[data-gesto="pinca"]{
  width: 34px; height: 34px; margin: -17px 0 0 -17px;
  border-width: 4px; background: #ffffff22;
}
.fel-cam-mira[data-pego="1"]{ opacity: 0; }

@media (prefers-reduced-motion: reduce){
  .fel-cam-mira{ transition: none; }
}

/* A FOLHA ENCONTRADA — o destaque de quem o ponteiro (ou a mão) alcançou.

   A primeira versão era um campo contínuo: cada folha recebia um número de 0
   a 1 conforme a mão se aproximava, e o CSS multiplicava. Duas coisas a
   derrubaram. Com MOUSE ela não existia — só o camera.js escrevia aquele
   número, então o destaque só valia com a câmera ligada, que é metade do
   comportamento em metade das entradas. E ela custava 21 medições de caixa
   por quadro para conseguir o que o `:hover` do navegador já sabe de graça.

   Agora é binário e vem do próprio corredor, no mesmo `pointerenter` que já
   acendia a legenda. A mão dispara esse evento sinteticamente e cai no mesmo
   lugar: uma verdade, um lugar, e o mouse ganha o comportamento junto.

   O que se vê continua sendo o mínimo que ainda se percebe — um fio de
   contorno e um sopro de claridade. Sem escala (brigaria com a perspectiva
   que o motor aplica) e sem sombra (regra da casa). */
.folha::after{
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  /* Nem borda nem transição de borda: desde que o destaque perdeu o contorno
     (10/09), nada mais colore isto, e uma borda transparente que ninguém
     pinta é código morto com aparência de intenção. */
  background: transparent;
  transition: background-color var(--m-dur-1, 200ms) var(--m-ease, ease);
}
.folha.fel-segura::after{
  /* SEM BORDA (pedido do Felipe, 10/09: "na seleção, pode tirar a border em
     si que aparece no entorno da imagem").

     E sem borda o destaque quase não precisa existir: a folha PARA, e num
     campo onde todas as outras continuam vindo, a que parou é a coisa mais
     evidente da tela. A legenda no rodapé diz qual é. Fica só um véu de 10%
     — o bastante para confirmar qual foi, e pouco o bastante para não
     lavar a imagem que a pessoa quis olhar. */
  background: rgba(255,255,255,.10);
}

/* ===========================================================================
   O OVERLAY VAZIO DO FRAMER — a causa de TUDO isto não funcionar.

   Diagnóstico de 10/09, por um agente que dirigiu o navegador com ponteiro
   confiável: em ~70 movimentos reais de mouse, NENHUMA folha recebeu um único
   `pointerenter`. Não havia destaque piscando entre folhas; não havia dono
   nenhum, nunca.

   `.framer-15ylwyw-container` é um container VAZIO do export, z-index 8,
   `pointer-events` não declarado (logo `auto`), 1280×800 cobrindo o hero
   inteiro. Ele é irmão do `#palco` dentro do `header.fel-hero` e fica por
   cima de todas as folhas no hit-test.

   E o z-index das folhas não salva: o `#palco` tem `perspective: 1060px` e
   `filter`, e cada um cria CONTEXTO DE EMPILHAMENTO — então os 0..20 que o
   `ordenarZ` escreve ordenam as folhas ENTRE SI, dentro do palco, e o palco
   inteiro continua abaixo do 8.

   A prova é um A/B no mesmo quadro, na mesma folha: com o overlay ativo o
   `elementFromPoint` devolve um DIV anônimo; com `pointer-events:none` nele,
   devolve a `<img>` da folha — e um hover real acendeu a classe e a legenda.
   Varredura em grade de 40px na viewport inteira: 0 de 2945 pontos alcançavam
   uma folha em produção.

   O irmão dele, `.framer-x7whlp-container`, TEM `pointer-events:none` no
   próprio export. É inconsistência do Framer, não decisão de design — e é por
   isso que a correção é devolver a regra que falta, e não mexer no z-index do
   palco (que reordenaria coisas que hoje funcionam).

   Se algum dia algo dentro dele precisar de clique, devolve-se
   `pointer-events:auto` naquele filho, não neste pai. */
body:has(.fel-hero) .framer-15ylwyw-container{
  pointer-events: none;
}

/* A FOLHA SENDO ARRASTADA. O cursor precisa dizer que aquilo pegou — e a
   diferença entre "posso pegar" e "estou segurando" é a mesma que qualquer
   mão conhece. Só em (hover:hover): no toque não há cursor para mudar. */
@media (hover: hover){
  .folha[href]{ cursor: grab; }
  .folha.fel-arrastando{ cursor: grabbing; }
}
/* Nada de seleção de texto nem de arraste nativo no meio do gesto. */
.folha.fel-arrastando, .folha.fel-arrastando *{ user-select: none; }
