/* ============================================================================
   Luan Taraschi - portfólio 1-bit
   Sistema: canvas carvão + tinta areia, dithering Bayer, zero raio, sombra
   dura deslocada. Paleta amostrada das referências - ver brand-spec.md.
   O acento é a própria inversão de polaridade, não um hue novo.

   Ordem: 1 tokens · 2 base · 3 tipografia · 4 dither · 5 componentes
          6 seções · 7 responsivo · 8 acessibilidade/motion
   ========================================================================== */

/* ---------- 1. TOKENS ---------------------------------------------------- */
/* Duas paletas, um par de tokens. `.invert` significa sempre "o contrário da
   página", então ele troca de lado sozinho quando o interruptor troca o tema. */

/* Cada modo tem o seu próprio par. O escuro é carvão+areia (board da marca);
   o claro é solarized - cinza quente, tinta ardósia e highlights saturados,
   amostrado de ms7uqgui-diethehehet.png. Trocar o tema troca as duas cores.
   Literais de propósito: var() dentro de custom property resolve no elemento
   que declara, e congelaria a cor errada dentro de .invert. */

/* carvão - página no modo escuro */
:root {
  --paper:      #1a1917;   /* canvas dominante (ref 2) */
  --paper-2:    #252420;   /* painel escuro (ref 1)    */
  --ink:        #fce4a6;   /* tinta areia (ref 1)      */
  --ink-2:      #9a948a;
  --rule:       #fce4a6;
  --rule-soft:  #413e37;
  --accent:     #fce4a6;
  --tex:        rgba(252, 228, 166, 0.06);
  --tex-hi:     rgba(252, 228, 166, 0.30);  /* mesma retícula, acesa pelo cursor */
  --shadow:     4px 4px 0 #fce4a6;
}

/* areia - painel invertido dentro da página carvão.
   `.guia--inv` entra junto porque a guia é fixa e passa por cima de seções de
   polaridades diferentes: ela precisa dos mesmos tokens que a seção embaixo
   dela, mas não do `background` que a regra .invert lá de baixo pinta. */
.invert,
.guia--inv {
  --paper:      #fce4a6;
  --paper-2:    #f3d792;
  --ink:        #1a1917;
  --ink-2:      #6b6558;
  --rule:       #1a1917;
  --rule-soft:  #d6bd76;
  --accent:     #1a1917;
  --tex:        rgba(26, 25, 23, 0.08);
  --tex-hi:     rgba(26, 25, 23, 0.34);
  --shadow:     4px 4px 0 #1a1917;
}

/* solarized claro - página no modo claro */
:root[data-theme='light'] {
  --paper:      #eee8d5;   /* base2  */
  --paper-2:    #e3ddc7;   /* derivado */
  --ink:        #073642;   /* base02 - ardósia, nunca preto */
  --ink-2:      #4c6068;   /* base01 escurecido até passar 4.5:1 */
  --rule:       #073642;
  --rule-soft:  #cfc8b0;
  --accent:     #073642;
  --tex:        rgba(7, 54, 66, 0.10);
  --tex-hi:     rgba(42, 161, 152, 0.55);   /* a retícula sob o cursor acende ciano */
  --shadow:     4px 4px 0 #073642;
}

/* solarized escuro - painel invertido dentro da página clara */
:root[data-theme='light'] .invert,
:root[data-theme='light'] .guia--inv {
  --paper:      #002b36;   /* base03 */
  --paper-2:    #073642;   /* base02 */
  --ink:        #eee8d5;
  --ink-2:      #93a1a1;   /* base1  */
  --rule:       #eee8d5;
  --rule-soft:  #20505c;
  --accent:     #eee8d5;
  --tex:        rgba(238, 232, 213, 0.07);
  --tex-hi:     rgba(238, 232, 213, 0.30);
  --shadow:     4px 4px 0 #eee8d5;
}

/* 1.1 os highlights. No escuro não existe terceira cor - os três apontam para
   a tinta. No claro cada um é um acento solarized, e eles só pintam detalhe:
   ponto de retícula, sublinhado, marcador, rastro do clique. Texto nunca. */
:root                     { --pop-1: #fce4a6; --pop-2: #fce4a6; --pop-3: #fce4a6; }
:root[data-theme='light'] { --pop-1: #dc322f; --pop-2: #268bd2; --pop-3: #2aa198; }

/* E dentro de um painel invertido eles têm que virar junto. Isto faltava, e o
   preço era um componente inteiro sumindo: no modo escuro os três apontam para
   a tinta da página, que é exatamente a cor do papel do painel invertido -
   #fce4a6 sobre #fce4a6. Os pontos da linha do tempo estavam lá o tempo todo,
   desenhados, invisíveis, em qualquer largura de tela. Mesma armadilha da
   barrinha de paciência do retrato, um ano-luz depois.
   No modo claro o painel vira solarized escuro e os três acentos continuam
   valendo: é para fundo escuro que eles foram desenhados. */
.invert, .guia--inv       { --pop-1: #1a1917; --pop-2: #1a1917; --pop-3: #1a1917; }
:root[data-theme='light'] .invert,
:root[data-theme='light'] .guia--inv { --pop-1: #dc322f; --pop-2: #268bd2; --pop-3: #2aa198; }

/* 1.2 o retrato tem polaridade fixa: cabelo escuro sobre fundo claro nos dois
   modos, porque inverter uma foto de rosto dá um negativo ilegível. O que
   muda é só o par de cores - por isso ele é máscara, não bitmap colorido. */
:root                     { --face-paper: #fce4a6; --face-ink: #1a1917; }
:root[data-theme='light'] { --face-paper: #eee8d5; --face-ink: #073642; }

/* 1.3 a cor que a cortina carrega. Literal porque ela pinta o tema que ainda
   não entrou - não dá para ler do :root, que nesse instante é o tema velho. */
.wipe--claro  { --wipe-cor: #eee8d5; }
.wipe--escuro { --wipe-cor: #1a1917; }

:root                     { color-scheme: dark; }
:root[data-theme='light'] { color-scheme: light; }

:root {
  --alert:      #ff6b3d;   /* só erro de formulário */

  /* Três vozes, uma família de origem. Silkscreen continua sendo a marca.
     Space Grotesk no corpo: grotesca de terminais quadrados, com o "a" e o "g"
     de desenho técnico - some a cara de sistema operacional que o texto tinha.
     Space Mono nos metadados, que é literalmente a mãe da Space Grotesk, então
     os dois pesos combinam sem parecerem duas decisões diferentes. */
  --font-display: 'Silkscreen', 'Courier New', ui-monospace, monospace;
  /* só o nome do hero. A Silkscreen é um grid de 5x7: em corpo de rótulo ela é
     ótima, em corpo de 200px cada letra vira meia dúzia de tijolos e o nome
     deixa de se ler. A Pixelify Sans tem grid bem mais fino, então o mesmo
     tamanho carrega muito mais desenho. Carregada só no index. */
  --font-title:   'Pixelify Sans', 'Silkscreen', ui-monospace, monospace;
  --font-body:    'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-mono:    'Space Mono', ui-monospace, 'SF Mono', Menlo, 'Cascadia Mono', monospace;

  --wrap:   1240px;
  --gutter: clamp(20px, 5vw, 56px);
  --bay:    clamp(64px, 9vw, 116px); /* respiro vertical de seção */
}

/* ---------- 2. BASE ------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 84px;
}

body {
  background-color: var(--paper);
  /* textura: retícula de 1px quase imperceptível, vira junto com o tema */
  background-image: radial-gradient(var(--tex) 0.8px, transparent 0.9px);
  background-size: 10px 10px;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* a retícula do fundo acende sob o cursor: mesma grade de 10px, só mais forte,
   recortada por uma máscara redonda.

   Antes isso era um elemento do tamanho do viewport com a máscara centrada em
   --mx/--my. Mover o centro de uma máscara é pintura, e a área pintada era a
   tela inteira, a cada quadro em que o ponteiro andasse - numa tela grande, uns
   3,7 milhões de pixels rasterizados de novo por quadro só para acender um
   borrão de 190px. Agora a caixa tem 320px, a máscara é fixa no meio dela e
   quem anda é um translate3d: composição pura, nada repinta.

   A grade continua casando com a do body porque o JS trava o deslocamento em
   múltiplos de 10px - o mesmo passo do background-size. O salto de 10px é
   invisível num borrão que só fica sólido no miolo. */
.grid-glow {
  position: fixed;
  top: 0;
  left: 0;
  width: 320px;
  height: 320px;
  margin: -160px 0 0 -160px;   /* múltiplo de 10: mantém a fase da retícula */
  transform: translate3d(-500px, -500px, 0);
  z-index: -1;                 /* acima do fundo do body, abaixo do conteúdo */
  pointer-events: none;
  opacity: 0;
  background-image: radial-gradient(var(--tex-hi) 1px, transparent 1.1px);
  background-size: 10px 10px;
  -webkit-mask-image: radial-gradient(circle 190px at 50% 50%,
                        #000 0%, rgba(0, 0, 0, 0.4) 46%, transparent 74%);
          mask-image: radial-gradient(circle 190px at 50% 50%,
                        #000 0%, rgba(0, 0, 0, 0.4) 46%, transparent 74%);
  transition: opacity 260ms ease-out;
}
.grid-glow.is-lit { opacity: 1; }

/* 2.1 o cursor faz parte do sistema: seta 1-bit desenhada, com contorno.
   No :active a mesma seta vem menor (20x32 -> 17x27): o ponteiro afunda junto
   com o botão, igual a todo o resto do site. As URLs moram aqui em cima porque
   são quatro desenhos x dois estados; as regras lá embaixo ficam curtas.

   Sobre o clicável não basta inverter a polaridade: invertida no modo claro a
   seta vira clarinha com contorno escuro, que é exatamente a cara do ponteiro
   padrão do Windows - dava a impressão de que o cursor do site tinha sumido bem
   na hora em que ele mais precisa aparecer. Então o estado de link troca de
   FORMA, não de cor: seta em escada de pixel, sem diagonal nenhuma, que nenhum
   sistema operacional desenha. `crispEdges` segura o degrau duro no scale. */
:root {
  --seta:        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='32' viewBox='0 0 12 19'%3E%3Cpath d='M1 1L1 16L4.6 12.6L6.9 18L9.3 17L7 11.8L11.4 11.8Z' fill='%23fce4a6' stroke='%231a1917' stroke-width='1.6' stroke-linejoin='miter' paint-order='stroke'/%3E%3C/svg%3E");
  --seta-press:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='17' height='27' viewBox='0 0 12 19'%3E%3Cpath d='M1 1L1 16L4.6 12.6L6.9 18L9.3 17L7 11.8L11.4 11.8Z' fill='%23fce4a6' stroke='%231a1917' stroke-width='1.6' stroke-linejoin='miter' paint-order='stroke'/%3E%3C/svg%3E");
  --seta-inv:    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='32' viewBox='0 0 12 19'%3E%3Cpath d='M0 0H1V1H2V2H3V3H4V4H5V5H6V6H7V7H8V8H9V9H5V15H3V11H0Z' fill='%23fce4a6' stroke='%231a1917' stroke-width='0.9' stroke-linejoin='miter' paint-order='stroke' shape-rendering='crispEdges'/%3E%3C/svg%3E");
  --seta-inv-press: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='17' height='27' viewBox='0 0 12 19'%3E%3Cpath d='M0 0H1V1H2V2H3V3H4V4H5V5H6V6H7V7H8V8H9V9H5V15H3V11H0Z' fill='%23fce4a6' stroke='%231a1917' stroke-width='0.9' stroke-linejoin='miter' paint-order='stroke' shape-rendering='crispEdges'/%3E%3C/svg%3E");
}
:root[data-theme='light'] {
  --seta:        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='32' viewBox='0 0 12 19'%3E%3Cpath d='M1 1L1 16L4.6 12.6L6.9 18L9.3 17L7 11.8L11.4 11.8Z' fill='%23073642' stroke='%23eee8d5' stroke-width='1.6' stroke-linejoin='miter' paint-order='stroke'/%3E%3C/svg%3E");
  --seta-press:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='17' height='27' viewBox='0 0 12 19'%3E%3Cpath d='M1 1L1 16L4.6 12.6L6.9 18L9.3 17L7 11.8L11.4 11.8Z' fill='%23073642' stroke='%23eee8d5' stroke-width='1.6' stroke-linejoin='miter' paint-order='stroke'/%3E%3C/svg%3E");
  --seta-inv:    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='32' viewBox='0 0 12 19'%3E%3Cpath d='M0 0H1V1H2V2H3V3H4V4H5V5H6V6H7V7H8V8H9V9H5V15H3V11H0Z' fill='%23073642' stroke='%23eee8d5' stroke-width='0.9' stroke-linejoin='miter' paint-order='stroke' shape-rendering='crispEdges'/%3E%3C/svg%3E");
  --seta-inv-press: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='17' height='27' viewBox='0 0 12 19'%3E%3Cpath d='M0 0H1V1H2V2H3V3H4V4H5V5H6V6H7V7H8V8H9V9H5V15H3V11H0Z' fill='%23073642' stroke='%23eee8d5' stroke-width='0.9' stroke-linejoin='miter' paint-order='stroke' shape-rendering='crispEdges'/%3E%3C/svg%3E");
}

/* a ponta da seta lisa cai em (1,1) do viewBox; a de escada cai em (0,0), então
   a âncora muda junto - senão o clique sai um pixel abaixo de onde a ponta está */
:root { cursor: var(--seta) 2 2, auto; }
:root:active { cursor: var(--seta-press) 2 2, auto; }

:is(a, button, summary, label, select, [role='switch'], [role='button']) {
  cursor: var(--seta-inv) 1 1, pointer;
}
:is(a, button, summary, label, select, [role='switch'], [role='button']):active {
  cursor: var(--seta-inv-press) 1 1, pointer;
}
/* campo de texto continua com cursor de texto - `cursor` é herdado */
input, textarea { cursor: text; }

/* 2.2 a cortina: uma faixa estreita da cor que está entrando cruza a tela da
   direita para a esquerda. Nunca cobre tudo - a ideia é uma emenda passando,
   não um blecaute. O JS vira o tema quando ela está no meio do caminho, então
   o corte acontece debaixo dela. Frente sólida, rastro em retícula de 50%,
   que deixa a página aparecer por trás enquanto a faixa sai. */
.wipe {
  position: fixed;
  top: 0;
  bottom: 0;
  left: var(--wipe-de, 100vw);
  width: 22vw;
  min-width: 130px;
  z-index: 240;
  pointer-events: none;
  opacity: 0;
  background-image: repeating-conic-gradient(var(--wipe-cor) 0 25%, transparent 0 50%);
  background-size: 4px 4px;
}
.wipe::before {                     /* a frente da faixa, sem furo */
  content: '';
  position: absolute;
  inset: 0 62% 0 0;
  background-color: var(--wipe-cor);
}
.wipe.is-on {
  opacity: 1;
  animation: wipe 420ms steps(7, end) 1;
}
@keyframes wipe {
  from { transform: translateX(0); }
  to   { transform: translateX(var(--wipe-ate, calc(-100vw - 22vw))); }
}

/* a faixa vai e volta: uma troca varre para a esquerda, a seguinte para a
   direita. Só inverte de onde ela parte, para onde vai e de que lado fica a
   frente sólida - a retícula do rastro continua atrás dela nos dois sentidos.
   ponytail: os 130px repetem o min-width lá de cima. Se aquele valor mudar,
   muda aqui também, senão a faixa larga não limpa a tela em celular. */
.wipe--volta {
  --wipe-de: calc(0px - 22vw - 130px);
  --wipe-ate: calc(100vw + 22vw + 130px);
}
.wipe--volta::before { inset: 0 0 0 62%; }

/* o rastro do clique: um quadrado 1-bit que abre e some. Um só elemento,
   reposicionado a cada pointerdown - ver módulo 12. */
.click-fx {
  position: fixed;
  z-index: 300;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border: 2px solid var(--pop-1);
  pointer-events: none;
  opacity: 0;
}
.click-fx.on { animation: click-fx 260ms steps(4, end) 1; }
@keyframes click-fx {
  from { opacity: 1; transform: scale(0.4); }
  to   { opacity: 0; transform: scale(3.4); }
}

img, canvas, svg { display: block; max-width: 100%; }

a { color: inherit; }

/* 2.3 os lugares onde o navegador pinta por cima do sistema.
   O cursor, a cortina e a retícula são escolha minha. Isto aqui é defesa: a
   seleção de texto, a barra de rolagem e o preenchimento automático são
   desenhados pelo sistema operacional, e cada um entra na tela com uma cor
   que não existe em lugar nenhum da paleta. Numa página de duas cores, um
   azul de seleção é a coisa mais visível que tem. */

/* selecionar texto é inverter a polaridade, que é o único acento do site.
   Em token e não em literal: dentro de um .invert a seleção vira sozinha, e
   no modo claro ela é ardósia sobre areia sem precisar de segunda regra. */
::selection {
  background: var(--ink);
  color: var(--paper);
  text-shadow: none;
}

/* a barra de rolagem. Em token, então ela troca de lado junto com o tema -
   inclusive a do bloco de código, que rola na horizontal dentro do painel.
   Sem isso, a única faixa de cinza do sistema operacional ficava encostada na
   borda da página o tempo todo, do lado direito, sempre visível.

   As duas linhas padrão bastam em Chrome 121+, Firefox e Safari 18.2+. O bloco
   -webkit- embaixo é só para quem veio antes: onde a propriedade padrão existe
   ela desliga esses pseudo-elementos, e os dois nunca valem ao mesmo tempo.
   Medido: com as duas linhas a barra sai 10px, e o `width: 13px` de baixo é
   simplesmente ignorado. Sem o @supports isso seria código morto se passando
   por regra viva. */
:root {
  scrollbar-width: thin;
  scrollbar-color: var(--ink) var(--paper);
}
@supports not (scrollbar-color: auto) {
  ::-webkit-scrollbar { width: 13px; height: 13px; }
  /* o trilho é a mesma retícula de 10px do fundo da página, sem interrupção */
  ::-webkit-scrollbar-track {
    background-color: var(--paper);
    background-image: radial-gradient(var(--tex) 0.8px, transparent 0.9px);
    background-size: 10px 10px;
  }
  /* a alça engorda no hover encolhendo a própria borda: a caixa não muda de
     tamanho, só de recheio - o mesmo gesto de afundar que os botões usam */
  ::-webkit-scrollbar-thumb {
    background-color: var(--ink);
    border: 3px solid var(--paper);
  }
  ::-webkit-scrollbar-thumb:hover { border-width: 1px; }
  ::-webkit-scrollbar-corner { background-color: var(--paper); }
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--ink);
  color: var(--paper);
  padding: 12px 18px;
  font: 13px/1 var(--font-mono);
  letter-spacing: 0.08em;
}
.skip:focus { left: 8px; top: 8px; }

/* ---------- 3. TIPOGRAFIA ------------------------------------------------ */
.h1 {
  font-family: var(--font-display);
  font-size: clamp(34px, 7.4vw, 74px);
  line-height: 1.06;
  letter-spacing: 0.06em;   /* caixa alta bitmap pede tracking positivo */
  text-transform: uppercase;
  font-weight: 400;
}

/* `balance` e não o `pretty` do corpo de texto: os dois consertam quebra de
   linha, mas problemas diferentes. O `pretty` cuida da viúva no fim de um
   parágrafo longo; o `balance` reparte as linhas de um bloco curto em larguras
   parecidas, que é o defeito de título - duas linhas cheias e uma terceira com
   uma palavra sozinha. Fica fora do .h1: o nome do hero é medido em JS letra
   por letra, e reequilibrar por baixo do JS é uma briga sem prêmio. */
.h2 {
  font-family: var(--font-display);
  font-size: clamp(24px, 3.6vw, 40px);
  line-height: 1.15;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 400;
  text-wrap: balance;
}

.h3 {
  font-family: var(--font-display);
  font-size: clamp(16px, 2vw, 20px);
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 400;
  text-wrap: balance;
}

.kicker {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* a rolagem da página escrita em ASCII ao lado do kicker: [####······] */
.kicker--barra { margin-left: 12px; letter-spacing: 0.02em; }

/* a Space Grotesk aguenta peso 300 em corpo grande sem sumir, e é aí que ela
   fica bonita: a entrada de cada seção vem leve e mais apertada que o corpo */
.lead {
  font-size: clamp(17px, 2vw, 20px);
  font-weight: 300;
  letter-spacing: -0.012em;
  line-height: 1.5;
  max-width: 58ch;
  text-wrap: pretty;
}

.body-text { max-width: 62ch; text-wrap: pretty; }
.body-text + .body-text { margin-top: 1em; }

.mono {
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.04em;
}

.mono-caps {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* placeholder honesto: conteúdo ainda não fornecido */
.slot {
  color: var(--ink-2);
  border-bottom: 2px dotted var(--rule-soft);
  text-decoration: none;
}

/* ---------- 4. DITHER ---------------------------------------------------- */
/* Três densidades ordenadas. Usadas como preenchimento de painéis, thumbs e
   faixas de transição. A máscara em rampa simula o degradê pontilhado. */
/* --dot é o que a retícula pinta. Por padrão é a tinta; a rampa troca banda a
   banda pelos acentos, que é onde a cor do modo claro aparece de verdade. */
.dither-12,
.dither-25,
.dither-50 {
  --dot: var(--ink);
  background-color: var(--paper-2);
  background-repeat: repeat;
}
.dither-12 {
  background-image: radial-gradient(var(--dot) 0.8px, transparent 0.9px);
  background-size: 9px 9px;
}
.dither-25 {
  background-image: radial-gradient(var(--dot) 0.85px, transparent 0.95px);
  background-size: 5px 5px;
}
.dither-50 {
  background-image: repeating-conic-gradient(var(--dot) 0 25%, transparent 0 50%);
  background-size: 4px 4px;
}

.dither-fade {
  -webkit-mask-image: linear-gradient(155deg, #000 8%, rgba(0, 0, 0, 0.45) 52%, transparent 92%);
          mask-image: linear-gradient(155deg, #000 8%, rgba(0, 0, 0, 0.45) 52%, transparent 92%);
}

/* Faixa divisória: a dissolução entre a página e o painel invertido, numa
   grade só.

   Duas versões erradas antes desta, e as duas ensinam a mesma coisa.

   A primeira fazia a rampa com `mask-image` em degradê de alfa: alfa parcial em
   cima de ponto sólido produz ponto meio transparente, ou seja meio-tom, e
   meio-tom é exatamente o que um sistema de 1 bit não tem. Saía um borrão liso
   no meio de uma página que é toda ponto cheio ou papel.

   A segunda - esta aqui até agora - eram DUAS peças: esta rampa de 81px, com
   pontos de tinta crescendo sobre o papel, e depois um `::before` de 54px dentro
   da seção, com pontos OUTROS, de papel, encolhendo sobre a tinta. Corrigi o
   tom da emenda entre elas e continuou feio, porque o defeito não era o tom: a
   segunda peça não continua a primeira, ela recomeça. A grade troca de dono no
   meio do caminho, e o olho lê duas transições emendadas em vez de uma.

   O jeito certo é o que a retícula de verdade sempre fez, e não tem inversão
   nenhuma: UMA grade, um círculo por célula, o raio só crescendo. Enquanto o
   raio é menor que meia célula sai ponto sobre o fundo. Passando de 4.5 num
   ladrilho de 9 os círculos encostam nos vizinhos, e o que sobra do fundo vira
   sozinho um losango côncavo entre quatro círculos - que encolhe até fechar em
   6.36, a meia diagonal. O "ponto de papel diminuindo" no fim da faixa não é
   desenhado por ninguém: é o buraco que sobra. Por isso não tem costura, não
   tem recomeço e não tem o que emendar.

   Como a peça agora é opaca (`--paper` de base) ela cobre o próprio pedaço da
   seção invertida em que pinta, e por isso a margem negativa e o z-index: os
   135px são os mesmos 81 + 54 de antes, no mesmo lugar, e a altura do documento
   não mexe um pixel. Conferido: mesma posição de título, mesmo scrollHeight.

   Os quinze raios saem de πr²/81 invertida para cobertura em passo igual de
   6,25% - com a correção da união de círculos a partir de 4.5, que é onde a
   área para de ser πr². Medido depois: 8 11 24 26 32 43 46 55 56 65 75 82 85
   89 95, monotônico, e a última fileira emenda nos 100% do painel. */
.ramp {
  display: grid;
  grid-template-rows: repeat(3, 45px);
  height: 135px;
  /* os 54px que a peça avança por cima do topo da seção - o mesmo pedaço que o
     `::before` ocupava. Sem isto o conteúdo da seção desceria 54px. */
  margin-bottom: -54px;
  position: relative;
  /* a seção é `position: relative`, então pinta na camada dos posicionados e
     passaria por cima desta peça. Aqui ela sobe um degrau e ganha. O trecho
     coberto é só o miolo do padding do topo, onde não mora conteúdo nenhum. */
  z-index: 1;
  background-color: var(--paper);
}
.ramp > span {
  background-color: transparent;
  background-repeat: repeat-x;
  background-size: 9px 9px;
  background-position: 0 0, 0 9px, 0 18px, 0 27px, 0 36px;
}
/* A cor troca junto com o raio, e o corte é onde o ponto ainda tem folga: os
   acentos vão até a sétima fileira, que é 44% de cobertura e 2,3px de vão entre
   um ponto e o vizinho. Da oitava em diante manda `--ink`, que é a cor do painel
   e é nela que a faixa tem que terminar. Deixar o acento nas fileiras densas era
   o defeito visível do modo claro: em 62% o ponto vermelho quase encosta, a
   fileira deixa de ser retícula e vira tarja - e três tarjas coloridas é o que
   se via. No modo escuro os três `--pop` apontam para a tinta e nada disso
   aparece; esta regra existe inteira para o modo claro. */
.ramp > span:nth-child(1) {
  background-image:
    radial-gradient(circle, var(--pop-3) 1.27px, transparent 1.47px),
    radial-gradient(circle, var(--pop-3) 1.8px, transparent 2px),
    radial-gradient(circle, var(--pop-3) 2.2px, transparent 2.4px),
    radial-gradient(circle, var(--pop-2) 2.54px, transparent 2.74px),
    radial-gradient(circle, var(--pop-2) 2.84px, transparent 3.04px);
}
.ramp > span:nth-child(2) {
  background-image:
    radial-gradient(circle, var(--pop-1) 3.11px, transparent 3.31px),
    radial-gradient(circle, var(--pop-1) 3.36px, transparent 3.56px),
    radial-gradient(circle, var(--ink) 3.59px, transparent 3.79px),
    radial-gradient(circle, var(--ink) 3.81px, transparent 4.01px),
    radial-gradient(circle, var(--ink) 4.01px, transparent 4.21px);
}
/* Daqui pra baixo os círculos encostam (4.5 é meia célula) e quem desenha o
   ritmo é o vão, não o ponto. */
.ramp > span:nth-child(3) {
  background-image:
    radial-gradient(circle, var(--ink) 4.21px, transparent 4.41px),
    radial-gradient(circle, var(--ink) 4.4px, transparent 4.6px),
    radial-gradient(circle, var(--ink) 4.59px, transparent 4.79px),
    radial-gradient(circle, var(--ink) 4.87px, transparent 5.07px),
    radial-gradient(circle, var(--ink) 5.28px, transparent 5.48px);
}

/* A rampa se escreve fileira a fileira ao entrar em cena, uma vez só, junto com
   o .reveal do módulo 5. Era `background-size` animado, que agora deslocaria as
   fileiras umas por cima das outras - com posição em pixel, encolher o ladrilho
   embaralha a pilha inteira. `clip-path` não mexe em nada disso: ele só decide
   quanto já apareceu. `steps(3)` com três fileiras por banda dá uma fileira por
   degrau, e os atrasos fazem as três bandas caírem em cascata.
   O `to` é escrito: `inset()` não interpola com o `none` do repouso, e sem ele
   a banda pularia de invisível a inteira num quadro só. */
@keyframes erodir {
  from { clip-path: inset(0 0 100% 0); }
  to   { clip-path: inset(0 0 0 0); }
}
.ramp.is-in > span { animation: erodir 420ms steps(5, end) backwards; }
.ramp.is-in > span:nth-child(2) { animation-delay: 120ms; }
.ramp.is-in > span:nth-child(3) { animation-delay: 240ms; }

/* ---------- 5. COMPONENTES ---------------------------------------------- */

/* 5.1 botões - a pancada tátil vive aqui.
   Nenhum componente daqui pra baixo declara `cursor`. Era esse o bug: `.btn`,
   `.lamp`, `.nav__toggle` e `.hero__me` traziam um `cursor: pointer` próprio, e
   classe ganha de elemento na especificidade - então justo em cima do que é
   clicável o desenho de 1 bit perdia pro ponteiro do sistema. O cursor mora
   inteiro na seção 2.1 e mais em lugar nenhum. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 48px;
  padding: 13px 22px;
  border: 2px solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  box-shadow: var(--shadow);
  transition: transform 90ms steps(2, end), box-shadow 90ms steps(2, end), background 90ms;
}
.btn:hover  { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 var(--ink); }
.btn:active { transform: translate(4px, 4px);  box-shadow: 0 0 0 var(--ink); }

/* primário = inversão de polaridade, a "seleção" do sistema 1-bit */
.btn--primary { background: var(--ink); border-color: var(--ink); color: var(--paper); }

/* O secundário era `transparent`, e numa página de retícula isso não é "sem
   fundo": é o ponteado passando por dentro do botão e por dentro da sombra
   dele. O botão perdia a borda de objeto - lia como um contorno desenhado por
   cima do papel, não como uma peça pousada nele. E o efeito piorava no hover,
   onde ele anda 2px e a textura de trás anda junto, denunciando que ali não
   tem chapa nenhuma. O que separa o secundário do primário é a polaridade, e
   polaridade precisa de duas cores: papel com tinta em vez de tinta com papel.
   Vazio não é uma delas. */
.btn--ghost   { background: var(--paper); }

.btn--tiny {
  min-height: 40px;
  padding: 9px 14px;
  font-size: 11px;
  box-shadow: 3px 3px 0 var(--ink);
}
.btn--tiny:hover  { box-shadow: 5px 5px 0 var(--ink); }
.btn--tiny:active { box-shadow: 0 0 0 var(--ink); }

/* 5.2 janela - barra de título em pinstripe, herança Mac 1-bit */
.panel {
  border: 2px solid var(--ink);
  background: var(--paper);
  box-shadow: var(--shadow);
}
.panel__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border-bottom: 2px solid var(--ink);
  background-image: repeating-linear-gradient(to bottom, var(--ink) 0 1px, transparent 1px 3px);
}
.panel__box {
  flex: none;
  width: 13px;
  height: 13px;
  border: 2px solid var(--ink);
  background: var(--paper);
}
.panel__title {
  background: var(--paper);
  padding: 2px 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.panel__body { padding: clamp(18px, 3vw, 28px); }

/* 5.2b bloco de código
   O site descrevia código muito bem e não mostrava nenhum. É um painel como os
   outros, com o nome do arquivo na barra e o link pro repositório na ponta.

   Sem colorização de sintaxe: numa paleta de duas cores ela viraria ruído ou
   mentira. O único destaque é o comentário em --ink-2, que é a mesma hierarquia
   que o resto do site usa pra texto secundário. */
.codigo { margin: clamp(22px, 3vw, 32px) 0 0; }
.codigo__repo {
  margin-left: auto;
  background: var(--paper);
  padding: 2px 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
  text-decoration: none;
  border: 1px solid var(--rule-soft);
}
/* só o que é link reage ao cursor: a etiqueta de repositório privado é aviso de
   procedência, e fazê-la acender convidaria a um clique que não existe */
a.codigo__repo:hover { color: var(--ink); border-color: var(--ink); }
span.codigo__repo { border-style: dashed; }
/* A barra do bloco de código carrega três coisas (marca, nome do arquivo,
   procedência) e as outras barras de painel carregam uma. Num celular de 360px
   isso não cabe numa linha: a etiqueta de procedência não encolhe, e a barra
   empurrava a página 16px pra fora, com a rolagem horizontal que vem junto.
   Só aqui, porque só aqui a barra tem três itens. O `min-width: 0` é o par
   obrigatório do `overflow-wrap`: sem ele o item flex se recusa a ficar menor
   que o próprio nome de arquivo e a quebra nunca acontece. */
.codigo .panel__bar { flex-wrap: wrap; }
.codigo .panel__title { min-width: 0; overflow-wrap: anywhere; }
.codigo pre {
  margin: 0;
  padding: clamp(14px, 2.4vw, 22px);
  /* a linha longa rola dentro da caixa, e não empurra a página inteira */
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.65;
  tab-size: 2;
  /* chegando ao fim da linha de código, o gesto continuava na página: em
     trackpad o navegador lê rolagem horizontal que passou do batente como
     "voltar", e a pessoa saía do case sem ter clicado em nada. `contain`
     segura o gesto dentro da caixa. */
  overscroll-behavior-x: contain;
}
/* rolar com o teclado exige poder focar a caixa; sem isto quem não usa mouse
   não alcança o fim da linha */
.codigo pre:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; }
.codigo .c { color: var(--ink-2); }
@media (max-width: 620px) { .codigo pre { font-size: 12px; } }

/* 5.3 navegação */
.nav {
  position: sticky;
  top: 0;
  z-index: 60;
  border-bottom: 2px solid var(--ink);
  /* opaca de propósito. Um backdrop-filter aqui obrigava o browser a recortar
     e borrar a faixa de trás da barra a cada quadro de rolagem - e por cima de
     um fundo 90% sólido o borrão era invisível. Custo alto, efeito nenhum. */
  background: var(--paper);
}
.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 62px;
}
.nav__mark {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  /* a marca é o único link do site que não tinha resposta nenhuma ao mouse.
     Ela usa a mesma gramática dos cartões: sobe no hover, afunda no clique. */
  transition: transform 110ms steps(2, end);
}
.nav__mark:hover  { transform: translate(-2px, -2px); }
.nav__mark:active { transform: translate(1px, 1px); }

.nav__glyph {
  width: 18px;
  height: 18px;
  border: 2px solid var(--ink);
  background-color: var(--paper);
  background-image: repeating-conic-gradient(var(--ink) 0 25%, transparent 0 50%);
  background-size: 4px 4px;
  box-shadow: 0 0 0 var(--ink);
  transition: background-position 140ms steps(4, end),
              box-shadow 110ms steps(2, end);
}
/* o glifo vira o negativo de si mesmo e ganha sombra dura: o quadriculado
   troca de fase meio quadrado, que em 4px é exatamente a inversão. */
.nav__mark:hover .nav__glyph,
.nav__mark:focus-visible .nav__glyph {
  background-color: var(--ink);
  background-image: repeating-conic-gradient(var(--paper) 0 25%, transparent 0 50%);
  background-position: 2px 2px;
  box-shadow: 3px 3px 0 var(--ink);
}
.nav__mark:active .nav__glyph { box-shadow: 0 0 0 var(--ink); }
.nav__links { display: none; gap: 4px; }
.nav__link {
  padding: 10px 12px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  border: 2px solid transparent;
  /* a seleção sobe de baixo em 4 degraus; a cor do texto vira só quando o
     preenchimento chega em cima, senão fica papel sobre papel no meio */
  background-image: linear-gradient(var(--ink), var(--ink));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0;
  transition: background-size 140ms steps(4, end), color 0ms;
}
.nav__link:hover,
.nav__link:focus-visible {
  background-size: 100% 100%;
  color: var(--paper);
  transition: background-size 140ms steps(4, end), color 0ms 140ms;
}
.nav__link[aria-current] { border-color: var(--ink); }

/* 5.3.1 lâmpada - o interruptor de tema, sempre visível no topo */
/* O interruptor não dizia o que era. Era um switch genérico com um `title` que
   só aparece depois de segundos de cursor parado em cima, e o que ele controla
   (a luz da página) não estava escrito em lugar nenhum visível.
   Agora ele é ladeado por lua e sol, e o lado ATIVO acende: além de dizer o que
   o controle faz, diz em que modo você está agora, que a régua sozinha não
   dizia. Os dois glifos são desenho de CSS em traço de 2px, mesma regra dos
   glifos da órbita: nada de fonte de ícone e nada de SVG novo. */
.lamp {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: none;
  min-height: 44px;
  padding: 0 2px;
  border: 0;
  background: none;
}
.lamp__icone {
  position: relative;
  flex: none;
  width: 13px;
  height: 13px;
  color: var(--ink-2);
  transition: color 120ms steps(2, end);
}
/* lua: disco cheio com uma mordida de papel deslocada. O papel aqui é o fundo
   da barra, então ela some no recorte em vez de virar um segundo disco. */
.lamp__icone--lua::before {
  content: '';
  position: absolute;
  inset: 1px;
  border-radius: 50%;
  background: currentColor;
}
.lamp__icone--lua::after {
  content: '';
  position: absolute;
  inset: -1px -3px 3px 4px;
  border-radius: 50%;
  background: var(--paper);
}
/* sol: miolo cheio e quatro raios nos eixos, tudo em pixel inteiro */
.lamp__icone--sol::before {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: currentColor;
}
.lamp__icone--sol::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2px;
  height: 2px;
  margin: -1px;
  box-shadow: 0 -6px 0 currentColor, 0 6px 0 currentColor,
              -6px 0 0 currentColor, 6px 0 0 currentColor;
}
/* o lado em que você está agora fica em tinta cheia; o outro espera apagado */
.lamp[aria-checked='false'] .lamp__icone--lua,
.lamp[aria-checked='true']  .lamp__icone--sol { color: var(--ink); }
.lamp__track {
  position: relative;
  display: block;
  width: 42px;
  height: 24px;
  border: 2px solid var(--ink);
  background-color: var(--paper);
  background-image: repeating-conic-gradient(var(--ink) 0 25%, transparent 0 50%);
  background-size: 4px 4px;
  transition: box-shadow 90ms steps(2, end), transform 90ms steps(2, end);
}
.lamp__knob {
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 2px;
  width: 14px;
  border: 2px solid var(--ink);
  background: var(--paper);
  transition: transform 160ms steps(4, end);
}
.lamp[aria-checked='true'] .lamp__knob { transform: translateX(20px); }
.lamp:hover  .lamp__track { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 var(--ink); }
.lamp:active .lamp__track { transform: translate(2px, 2px);   box-shadow: 0 0 0 var(--ink); }

/* 5.3b o seletor de idioma. Um link só, mostrando a língua para a qual ele
   leva, e não a que está aberta: dois botões com um marcado obrigam a pessoa a
   descobrir qual dos dois é o estado e qual é a ação.
   O `lang` no elemento é o que faz o leitor de tela pronunciar "English" com
   sotaque inglês no meio de uma página em português, em vez de soletrar. */
.lang {
  flex: none;
  padding: 5px 9px;
  border: 2px solid var(--rule-soft);
  color: var(--ink-2);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: 0.12em;
  transition: color 110ms steps(2, end), border-color 110ms steps(2, end),
              transform 110ms steps(2, end);
}
.lang:hover,
.lang:focus-visible { color: var(--ink); border-color: var(--ink); transform: translate(-1px, -1px); }
.lang:active { transform: translate(1px, 1px); }

.nav__toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 8px 14px;
  border: 2px solid var(--ink);
  background: var(--paper);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.nav__panel {
  display: grid;
  border-top: 2px solid var(--ink);
  background: var(--paper);
}
.nav__panel[hidden] { display: none; }
.nav__panel a {
  padding: 15px var(--gutter);
  border-bottom: 1px solid var(--rule-soft);
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
}
.nav__panel a:last-child { border-bottom: 0; }

/* 5.4 cabeçalho de seção */
.sec { padding-block: var(--bay); }
.sec--tight { padding-block: calc(var(--bay) * 0.62); }

/* O navegador pula layout e pintura das seções que ainda estão fora da tela.
   Numa home com sete seções, uma órbita animada e dois canvas, isso é trabalho
   que não precisa acontecer antes de alguém rolar até lá.

   O `auto` do contain-intrinsic-size é o que torna isto seguro: os 900px são só
   o palpite da primeira vez, e depois de a seção ser desenhada uma vez o
   navegador guarda a altura real e passa a usar ela. Sem o `auto`, toda seção
   não desenhada mediria 900px, a altura total da página seria ficção e a barra
   de progresso do topo (e a rolagem junto) pularia conforme você desce.

   `.hero` fica de fora de propósito: ele está sempre na primeira tela, então
   não haveria o que pular, e adiar a pintura dele atrasaria justamente o que o
   visitante vê primeiro.

   Medido antes de entrar, em 05/08/2026, na home a 1280px: uma passada de
   layout forçado sai por 0,9ms com a regra e 2,5ms sem ela (mediana de 40).
   As seis seções medem exatamente a mesma altura nos dois casos, então nada
   muda de lugar. Se algum dia a barra de progresso do topo começar a pular ao
   rolar, é aqui que se olha primeiro. */
@supports (content-visibility: auto) {
  .sec {
    content-visibility: auto;
    contain-intrinsic-size: auto 900px;
  }
}
.sec__head { margin-bottom: clamp(28px, 4vw, 48px); }
.sec__head .h2 { margin-top: 12px; }
.sec__head .lead { margin-top: 16px; }

/* Chegar por link de âncora era a única navegação do site que acontecia em
   silêncio. Quem clica em "projetos" no topo, ou volta de um case pelo
   "‹ todos os projetos", é jogado no meio de uma página comprida sem nada
   dizendo onde caiu - e como todas as seções têm a mesma cara, dá aquele
   segundo de dúvida se o link levou ao lugar certo.

   O kicker da seção alvo pisca duas vezes invertido. É o rótulo, não o
   título: ele é pequeno, é exatamente o nome que a pessoa clicou, e piscar
   um H2 de 40px seria escândalo pra uma confirmação. Dois piscos e para -
   isto responde uma pergunta, não chama atenção pra si.

   `steps(1, end)` de propósito: sem passo intermediário nenhum, aceso ou
   apagado, que é como todo o resto do site se acende. */
.sec:target .sec__head .kicker { animation: alvo 260ms steps(1, end) 2; }
@keyframes alvo {
  0%, 49%   { background: var(--ink); color: var(--paper); box-shadow: 0 0 0 4px var(--ink); }
  50%, 100% { background: transparent; color: var(--ink-2); box-shadow: none; }
}

/* 5.5 cards de projeto */
.grid-projects {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 270px), 1fr));
  gap: clamp(16px, 2.4vw, 26px);
}
.card {
  display: flex;
  flex-direction: column;
  border: 2px solid var(--ink);
  background: var(--paper);
  color: inherit;
  text-decoration: none;
  box-shadow: 0 0 0 var(--ink);
  transition: transform 110ms steps(2, end), box-shadow 110ms steps(2, end);
}
.card:hover { transform: translate(-3px, -3px); box-shadow: 6px 6px 0 var(--ink); }
.card:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }

/* Os dois com cliente pagando e uso diário atravessam a linha inteira e deitam:
   captura à esquerda, texto à direita. Numa grade de nove caixas iguais, um ERP
   em produção com 3.500 testes ocupava exatamente a mesma área que um jogo de
   adivinhar música, e o olho não lê o argumento da seção, lê o tamanho.

   980px é onde a grade passa a ter três colunas (medido, não chutado), e o
   destaque só vale a partir dali - por contagem, não por estética. Tirando os
   dois da grade sobram três projetos mais a porta do GitHub: em três colunas
   isso é uma linha cheia mais uma faixa; em duas seria uma linha cheia e um
   card sozinho ao lado de um buraco. E buraco em grade de portfólio lê como
   projeto que faltou, que é justamente o problema que o cartão do GitHub
   existe pra resolver. Abaixo de 980 todo mundo volta a ser card normal: seis
   cartões em duas colunas fecham três linhas exatas, e numa fecham seis. */
@media (min-width: 980px) {
  /* a porta do GitHub vira faixa junto: ela não é projeto, e como quarto item
     numa linha de três seria justamente o buraco */
  .card--destaque,
  .card--mais { grid-column: 1 / -1; flex-direction: row; align-items: stretch; }

  .card--mais .card__thumb {
    flex: 0 0 20%;
    aspect-ratio: auto;
    border-bottom: 0;
    border-right: 2px dashed var(--ink);
  }
  .card--mais .card__body { align-content: center; padding: clamp(18px, 2.2vw, 28px); }

  .card--destaque .card__thumb {
    flex: 0 0 42%;
    /* a razão fixa dos cards pequenos daria uma caixa mais baixa que o texto ao
       lado e sobraria papel embaixo da captura; aqui quem manda na altura é a
       coluna de texto */
    aspect-ratio: auto;
    border-bottom: 0;
    border-right: 2px solid var(--ink);
  }
  .card--destaque .card__body {
    flex: 1;
    align-content: center;
    padding: clamp(20px, 2.6vw, 34px);
  }
  /* o resumo é a razão de o card ser grande: em corpo de legenda ele não usa o
     espaço que ganhou */
  .card--destaque .card__body .mono { font-size: 15px; line-height: 1.55; }
}
/* Em duas colunas, uma grade de contagem ímpar deixa o último card sozinho ao
   lado de meia linha vazia. É o caso dos três games. `:last-child:nth-child(odd)`
   em vez de mirar a seção: se um quarto jogo entrar, a regra para de valer
   sozinha, e se um sétimo projeto entrar ela passa a valer sem ninguém lembrar. */
@media (min-width: 640px) and (max-width: 979px) {
  .grid-projects > .card:last-child:nth-child(odd) {
    grid-column: 1 / -1;
    flex-direction: row;
    align-items: stretch;
  }
  .grid-projects > .card:last-child:nth-child(odd) .card__thumb {
    flex: 0 0 38%;
    aspect-ratio: auto;
    border-bottom: 0;
    border-right: 2px solid var(--ink);
  }
  .grid-projects > .card--mais:last-child:nth-child(odd) .card__thumb {
    border-right-style: dashed;
  }
  .grid-projects > .card:last-child:nth-child(odd) .card__body {
    flex: 1;
    align-content: center;
  }
}

.card__thumb {
  position: relative;
  aspect-ratio: 4 / 3;
  border-bottom: 2px solid var(--ink);
  /* a textura anda e rala: os pontos se afastam, a forma se desfaz */
  transition: background-position 240ms steps(4, end), background-size 240ms steps(4, end);
}
.card:hover .card__thumb { background-position: 6px 6px; }
.card:hover .dither-50 { background-size: 7px 7px; }
.card:hover .dither-25 { background-size: 9px 9px; }
.card:hover .dither-12 { background-size: 15px 15px; }
.card:hover .card__idx   { background: var(--ink); color: var(--paper); }

/* o sexto da grade não é projeto, é a porta pro resto. Contorno tracejado para
   o olho separar antes de ler o título - e o mesmo hover de todo cartão, porque
   ele continua sendo um link e mentir sobre isso seria pior que o buraco. */
.card--mais { border-style: dashed; }
.card--mais .card__thumb { border-bottom-style: dashed; }

/* o cursor rala o dither de perto: num raio em volta do ponteiro o papel volta
   por cima e os pontos somem. É a erosão da referência virando resposta ao toque.
   ponytail: papel por cima em vez de furo na máscara, porque o thumb já gasta a
   máscara dele com o .dither-fade. Se um dia precisar do furo de verdade, é
   mask-composite: intersect com as duas. */
.card__thumb::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle 84px at var(--cx, 50%) var(--cy, 50%),
                var(--paper-2) 0%, transparent 72%);
  transition: opacity 160ms steps(2, end);
}
.card:hover .card__thumb::after { opacity: 1; }

/* 5.5.1 o produto por trás do dither.
   Em repouso o cartão continua sendo o mesmo bloco de retícula do resto do
   site: a captura está lá, mas escura e coberta pelo véu, lendo como textura.
   No hover o véu abre e a tela do produto aparece por baixo. É o gesto de
   erosão da referência aplicado ao contrário — em vez de desfazer a imagem,
   ele revela a que estava embaixo o tempo todo.

   `object-position: top left` porque toda captura de sistema tem a informação
   que importa no topo: centralizar cortaria justamente o cabeçalho. */
.card__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* padrão para captura de sistema: a informação que importa mora no topo, e
     centralizar cortaria justamente o cabeçalho */
  object-position: top left;
  /* escura o bastante para o véu ainda mandar na leitura, clara o bastante para
     dar pra reconhecer que ali tem um sistema. Abaixo disto o repouso virava
     retícula pura e o cartão não prometia nada; acima, o hover não revelava
     nada porque já estava tudo à mostra. */
  filter: grayscale(1) contrast(1.15) brightness(0.6);
  transition: filter 260ms steps(4, end);
}
.card:hover .card__shot,
.card:focus-visible .card__shot { filter: grayscale(1) contrast(1.02) brightness(1); }

/* no modo claro escurecer é o contrário do que se quer: as capturas são de
   interface escura, e sobre o papel creme elas viravam um bloco preto no meio
   da grade. Ali o repouso é a mesma imagem quase transparente, um fantasma
   cinza sobre o papel, e o hover traz ela inteira - a mesma regra que o
   componente .shot das páginas de case já segue. */
:root[data-theme='light'] .card__shot {
  opacity: 0.32;
  filter: grayscale(1) contrast(1.1);
  transition: opacity 260ms steps(4, end);
}
:root[data-theme='light'] .card:hover .card__shot,
:root[data-theme='light'] .card:focus-visible .card__shot { opacity: 1; }

/* o véu carrega a classe .dither-*, e não o thumb, quando existe captura: o
   background do thumb ficaria atrás da imagem e não apareceria. Ele herda de
   graça a erosão que o .card:hover já aplica a qualquer .dither-* descendente. */
.card__veu {
  position: absolute;
  inset: 0;
  z-index: 1;
  transition: opacity 260ms steps(4, end),
              background-position 240ms steps(4, end),
              background-size 240ms steps(4, end);
}
.card:hover .card__veu,
.card:focus-visible .card__veu { opacity: 0.18; }

.card__idx {
  position: absolute;
  z-index: 2;                  /* acima do véu e do papel que o cursor derrama */
  left: 10px;
  bottom: 8px;
  font-family: var(--font-display);
  font-size: 26px;
  letter-spacing: 0.04em;
  color: var(--ink);
  background: var(--paper);
  padding: 2px 8px;
  border: 2px solid var(--ink);
}
.card__body { padding: 16px 16px 18px; display: grid; gap: 8px; }
.card__tags {
  margin-top: 4px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.tag {
  border: 1px solid var(--rule-soft);
  padding: 4px 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
}
/* "visto" é a única tag que o JS escreve, e a única que fala do leitor em vez
   de falar do projeto - por isso ela inverte, para não se misturar na fila de
   tecnologias ao lado. Entra como .tag de propósito: a grade já tem esse
   vocabulário, e inventar um segundo tipo de etiqueta pra dizer uma coisa só
   seria mais desenho do que a informação merece. */
.tag--visto {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

/* "no ar" é a única tag que promete uma coisa clicável, e ela não pode ser um
   <a>: o card inteiro já é um link e HTML não aninha âncora. Então ela ao menos
   se veste de link - borda cheia em vez do traço apagado das tecnologias ao
   lado, pra olho que varre a grade parar nela. */
.tag--ar {
  border-color: var(--ink);
  color: var(--ink);
}

/* 5.6b prova social
   Uma citação só, e larga. Duas ou três viravam parede de elogio, que é o
   formato que ninguém lê porque ninguém acredita. A moldura é a mesma dos
   cards - borda de 2px e sombra dura - pra não parecer widget colado de fora. */
.prova {
  border: 2px solid var(--ink);
  box-shadow: var(--shadow);
  background: var(--paper);
  padding: clamp(26px, 4vw, 44px);
  margin-top: clamp(34px, 5vw, 56px);
  max-width: 68ch;
}
.prova blockquote {
  margin: 0;
  font-size: clamp(18px, 2.2vw, 24px);
  line-height: 1.45;
  text-wrap: pretty;
}
.prova figcaption {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--rule-soft);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* 5.7 timeline */
/* o mínimo do clamp subiu de 20 para 28: no celular o texto encostava no
   trilho, e é justamente onde o ponto de cada item mora - ele nasce metade
   dentro, metade fora da linha, e sem folga o parágrafo passava por cima dele.
   O `left` do ponto lê o mesmo clamp, então os dois andam juntos. */
.tl { border-left: 2px solid var(--ink); padding-left: clamp(28px, 3vw, 30px); }
.tl__item { position: relative; padding-bottom: clamp(28px, 4vw, 40px); }
.tl__item:last-child { padding-bottom: 0; }
.tl__item::before {
  content: '';
  position: absolute;
  top: 6px;
  /* ponytail: o clamp aqui é o mesmo padding-left do .tl. Se um mudar sem o
     outro, o ponto sai de cima do trilho. */
  left: calc(-1 * clamp(28px, 3vw, 30px) - 7px);
  width: 12px;
  height: 12px;
  background: var(--pop-1);
  transition: transform 140ms steps(2, end);
}
.tl__item:nth-child(3n+2)::before { background: var(--pop-2); }
.tl__item:nth-child(3n)::before   { background: var(--pop-3); }
.tl__item:hover::before { transform: scale(1.45); }
.tl__year {
  font-family: var(--font-display);
  font-size: 15px;
  letter-spacing: 0.08em;
}
/* o cargo é display font e no celular quebra em três linhas; sem line-height
   próprio elas encostavam umas nas outras */
.tl__role { margin-top: 10px; line-height: 1.24; }
/* ano, cargo e texto vinham colados num bloco só, e no celular - onde tudo
   quebra em mais linhas - não dava pra ver onde um acabava e o outro começava */
.tl__role + .body-text { margin-top: 12px; }
@media (max-width: 620px) {
  .tl__item { padding-bottom: 40px; }
  /* ano e cargo são os dois em display font e de corpo parecido: sem uma folga
     maior entre eles no celular, as cinco linhas viravam um bloco só */
  .tl__year { font-size: 14px; }
  .tl__role { margin-top: 14px; }
}

/* a linha do tempo desperdiçava metade da largura em tela grande. A coluna da
   direita recebe o redemoinho, preso enquanto os cargos rolam por baixo.
   ponytail: some abaixo de 940px em vez de virar faixa horizontal - no celular
   a coluna não existe, e um canvas a mais só custaria bateria. */
.tl__aside { display: none; }
@media (min-width: 940px) {
  .tl-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
    gap: clamp(28px, 4vw, 64px);
    align-items: start;
  }
  .tl__aside { display: block; position: sticky; top: 100px; }
}
.tl__swirl {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  border: 2px solid var(--ink);
  box-shadow: var(--shadow);
  image-rendering: pixelated;   /* pintado em ~180px e esticado: pixel é pixel */
}
.tl__cap {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 10px;
  color: var(--ink-2);
}

/* 5.8 formulário */
.field { display: grid; gap: 7px; margin-bottom: 18px; }
.field > label {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
/* o campo também fala quando dá certo. Mora no rótulo, e não no [role=alert]
   logo abaixo: escrever no alerta faria o leitor de tela anunciar a cada tecla. */
.field.is-ok > label::after {
  content: ' ✓';
  color: var(--accent);
}
/* o contador da barra do painel: mesma pastilha do título, encostada à direita */
.panel__conta {
  margin-left: auto;
  background: var(--paper);
  padding: 2px 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
/* vazio ele seria só uma pastilha de papel pendurada na barra */
.panel__conta:empty { display: none; }
.field input,
.field textarea {
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  border: 2px solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  border-radius: 0;
  /* o traço que pisca é o último detalhe do campo que ainda vinha do sistema */
  caret-color: var(--accent);
}
.field textarea { min-height: 132px; resize: vertical; line-height: 1.5; }

/* O preenchimento automático do Chrome pinta o campo de #e8f0fe e escreve o
   valor em azul-escuro. É a única cor de terceiro que consegue entrar na
   página, e ela entra justamente no formulário, que é onde a visita termina.

   Não dá para trocar o `background`: o navegador ganha de qualquer declaração
   nessa propriedade, com !important e tudo. O que ele não toca é o
   `box-shadow`, então uma sombra interna de 100px cobre o campo inteiro com o
   papel do tema, e o -webkit-text-fill-color devolve a tinta ao valor. Truque
   velho, mas é o único que funciona, e o site é escuro por padrão: sem isto o
   campo autopreenchido vira um retângulo claro no meio do painel. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus,
.field textarea:-webkit-autofill {
  -webkit-box-shadow: inset 0 0 0 100px var(--paper);
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--accent);
}

/* a caixa de mensagem cresce com o que está sendo escrito, em vez de rolar
   dentro de uma altura fixa. Um campo de 132px é convite pra três linhas; se
   ele acompanha, a pessoa escreve o que veio escrever. Dentro de @supports
   porque é recente: quem não tem continua com a caixa fixa e a alça de
   arrastar, que é o comportamento de sempre. O teto existe pra mensagem longa
   não empurrar o botão de enviar pra fora da tela. */
@supports (field-sizing: content) {
  .field textarea { field-sizing: content; max-height: 46vh; }
}
.field input:focus-visible,
.field textarea:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.field input[aria-invalid='true'],
.field textarea[aria-invalid='true'] {
  border-color: var(--alert);
  background-color: var(--paper-2);
  background-image: radial-gradient(rgba(255, 107, 61, 0.3) 0.9px, transparent 1px);
  background-size: 4px 4px;
}
/* A caixa da mensagem existe antes da mensagem. Ela era `display: none`
   enquanto vazia, então cada erro que aparecia empurrava tudo que vinha
   embaixo - e como a validação roda no blur, a pessoa saía de um campo e via o
   resto do formulário descer. Com três campos errados de uma vez o painel
   inteiro crescia, que é o que se vê comparando as duas telas.
   O `min-height` segura exatamente uma linha, e as três mensagens couberam em
   uma linha depois de encurtadas (módulo 3): medi o campo em 270px num celular
   de 360 e a maior delas ia a 352px, ou seja, duas linhas garantidas. */
.field__err {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  line-height: 1.35;
  color: var(--alert);
  min-height: 1.35em;
}

.form__status {
  margin-top: 14px;
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.04em;
  min-height: 1.4em;
}

/* 5.9 linha de contato */
.contact-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  padding: 14px 10px;
  margin-inline: -10px;
  border-bottom: 1px solid var(--rule-soft);
  transition: background-color 140ms steps(2, end);
}
.contact-row:hover { background-color: var(--paper-2); }
.contact-row__key {
  flex: none;
  width: 92px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.contact-row__val { flex: 1 1 200px; word-break: break-word; }
/* O rótulo de 92px mais o e-mail já ocupavam a linha inteira, e o botão copiar
   era empurrado pra linha de baixo sozinho, encostado na margem esquerda
   debaixo do rótulo - parecia botão perdido, não o botão daquela linha. Aqui o
   rótulo toma a linha toda e valor e botão dividem a de baixo.
   @container e não @media porque o gatilho é a largura da linha, não a da
   tela: a partir de 700px esta coluna vira uma de duas e cada linha fica com
   uns 315px, mais apertada do que era no celular. Media query aqui consertava
   o telefone e deixava o notebook quebrado do mesmo jeito. */
.contact__grid > div { container-type: inline-size; }
@container (max-width: 420px) {
  .contact-row { align-items: baseline; gap: 8px 12px; }
  .contact-row__key { width: 100%; }
  /* base 0 e não auto: com base automática a largura natural do e-mail entrava
     na conta da quebra e empurrava o botão pra linha seguinte de novo, agora
     numa coluna de 307px. Com base 0 o valor cede espaço em vez de expulsar o
     botão, e quebra dentro de si mesmo - o word-break acima já cuida disso. */
  .contact-row__val { flex: 1 1 0; min-width: 0; }
}

/* 5.10 rodapé */
/* A linha do rodapé era 2px de tinta cheia, igual à borda de painel, de card e
   de seção. Com o mesmo peso de tudo, ela lia como mais uma divisória do
   sistema em vez de "acabou a página". Aqui ela recua: mesma espessura, tinta
   parcial, então continua estruturando sem competir.
   O `--rule-soft` na primeira linha é o piso para quem não tem color-mix; quem
   tem sobrescreve na linha seguinte e ganha o valor exato em qualquer um dos
   quatro temas, porque a mistura é feita sobre o --ink de cada um. */
/* Opaco, como a barra do topo. Antes o rodapé era só uma régua fina: a
   retícula do fundo da página atravessava ele inteiro e o bloco não terminava
   em lugar nenhum - a página simplesmente parava. Um fundo próprio dá o fim.
   `--paper-2` e não `--paper`: é o mesmo degrau que os painéis usam, então o
   rodapé lê como peça do sistema e não como uma faixa pintada por cima. */
.foot {
  background: var(--paper-2);
  border-top: 2px solid var(--rule-soft);
  border-top-color: color-mix(in srgb, var(--ink) 38%, transparent);
  padding-block: 34px;
}
.foot__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: space-between;
  align-items: baseline;
}

/* No celular os três pedaços não cabem lado a lado e o flex os embrulhava em
   três linhas de 14px de respiro, empilhadas no pé da tela - a impressão era
   de sobra de página, não de rodapé.
   Aqui eles viram linhas de verdade, cada uma com régua e folga própria, na
   mesma ordem do DOM. E os chips passam a ocupar a largura toda, um por
   linha: os três somam 334px numa coluna de 310 num aparelho de 360, então
   lado a lado eles quebram de qualquer jeito - empilhados eles ainda ganham
   alvo de toque de 44px, que lado a lado nunca teriam. */
@media (max-width: 700px) {
  .foot { padding-block: 26px 34px; }
  .foot__inner {
    display: grid;
    gap: 0;
    align-items: stretch;
  }
  .foot__inner > * { padding-block: 15px; }
  .foot__inner > * + * { border-top: 1px solid var(--rule-soft); }
  .foot__inner > :first-child { padding-top: 0; }
  .foot__inner > :last-child { padding-bottom: 0; }
  .foot__inner .social {
    display: grid;
    gap: 2px;
    align-self: stretch;
  }
  /* `flex` no lugar de `inline-flex`: assim o link ocupa a linha inteira e o
     alvo de toque é a faixa toda, não só os 96px do texto "GitHub". Medido em
     44px de altura por 320 de largura, contra 96x44 antes. */
  .foot__inner .social__l {
    display: flex;
    justify-content: flex-start;
    padding: 11px 10px;
    margin-inline: -10px;
  }
}

/* 5.9 redes - o glifo mora no CSS, não no HTML.
   São três ícones em quatro páginas: inline dariam doze blocos de <svg> pra
   manter em sincronia. Como máscara, o desenho existe uma vez só e o
   `background: currentColor` faz ele acompanhar tema e polaridade de graça.
   Tudo em retângulo de grade 16, sem curva nenhuma: é a mesma regra dos glifos
   da órbita, marca de terceiro colorida não sobrevive a dois tons. */
.social {
  list-style: none;
  display: flex;
  /* os três chips somam 349px e a coluna do rodapé tem 340 num celular de
     360: sem poder quebrar, o "Instagram" saía 9px pra fora e ficava cortado
     na borda da tela */
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 0;
  /* o rodapé alinha por baseline; o chip tem 7px de respiro em cima, então pela
     baseline ele subia esses 7px em relação às duas legendas ao lado */
  align-self: center;
}
.social__l {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 11px;
  border: 2px solid transparent;
  color: var(--ink-2);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition: transform 90ms steps(2, end), box-shadow 90ms steps(2, end);
}
.social__l::before {
  content: '';
  flex: none;
  width: 17px;
  height: 17px;
  background: currentColor;
  -webkit-mask: var(--ic) center / contain no-repeat;
          mask: var(--ic) center / contain no-repeat;
}
.social__l:hover,
.social__l:focus-visible {
  color: var(--ink);
  border-color: var(--ink);
  background: var(--paper-2);
  transform: translate(-2px, -2px);
  box-shadow: 3px 3px 0 var(--ink);
}
.social__l:active { transform: translate(1px, 1px); box-shadow: 0 0 0 var(--ink); }

/* ramo do git: dois nós na esquerda, um na direita, e o galho que sai */
.social__l--gh {
  --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='%23000'%3E%3Crect x='2' y='2' width='4' height='4'/%3E%3Crect x='2' y='10' width='4' height='4'/%3E%3Crect x='10' y='2' width='4' height='4'/%3E%3Crect x='3' y='6' width='2' height='4'/%3E%3Crect x='5' y='7' width='6' height='2'/%3E%3Crect x='11' y='6' width='2' height='2'/%3E%3C/g%3E%3C/svg%3E");
}
/* quadro com "in" em bitmap */
.social__l--li {
  --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='%23000'%3E%3Crect x='1' y='1' width='14' height='2'/%3E%3Crect x='1' y='13' width='14' height='2'/%3E%3Crect x='1' y='1' width='2' height='14'/%3E%3Crect x='13' y='1' width='2' height='14'/%3E%3Crect x='4' y='4' width='2' height='2'/%3E%3Crect x='4' y='7' width='2' height='5'/%3E%3Crect x='8' y='7' width='2' height='5'/%3E%3Crect x='10' y='7' width='2' height='2'/%3E%3Crect x='10' y='9' width='2' height='3'/%3E%3C/g%3E%3C/svg%3E");
}
/* 5.10 a guia lateral - onde você está na página, sem pedir atenção.
   Um traço por seção, encostado na direita. O da seção em que você está cresce
   e enche da esquerda pra direita conforme a seção passa, então o traço ativo é
   ao mesmo tempo posição e progresso. O nome só aparece no traço ativo ou
   quando o cursor chega perto; parado, a guia é sete risquinhos e mais nada.

   Ela é <nav> de verdade com âncora de verdade: sem JS continua sendo um índice
   funcional, só sem saber qual é a seção atual. Só aparece com folga de sobra
   na lateral, senão brigaria com o texto. */
.guia {
  display: none;
  position: fixed;
  right: clamp(16px, 2.4vw, 34px);
  top: 50%;
  translate: 0 -50%;
  z-index: 40;
}
@media (min-width: 1180px) { .guia { display: block; } }
@media print { .guia { display: none; } }

.guia__lista {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 15px;
}
.guia a {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  color: var(--ink-2);
  text-decoration: none;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* o traço. Fora da seção ativa é sólido e curto; na ativa ele cresce e o
   `background-image` de duas paradas duras vira a régua de progresso - sem
   gradiente macio, é corte seco, igual ao resto. */
.guia a::after {
  content: '';
  flex: none;
  width: 14px;
  height: 2px;
  background-color: currentColor;
  transition: width 160ms steps(4, end);
}
.guia a:hover { color: var(--ink); }
.guia a:hover::after { width: 24px; }
.guia a[aria-current] { color: var(--ink); }
.guia a[aria-current]::after {
  width: 34px;
  background-image: linear-gradient(90deg,
    var(--ink) 0 var(--p, 0%), var(--rule-soft) var(--p, 0%) 100%);
}
/* o nome entra deslizando da direita, em três degraus */
.guia a > span {
  opacity: 0;
  translate: 8px 0;
  transition: opacity 140ms steps(3, end), translate 140ms steps(3, end);
}
.guia a[aria-current] > span,
.guia:hover a > span,
.guia a:focus-visible > span { opacity: 1; translate: 0 0; }

/* câmera: moldura, lente e a luz no canto */
.social__l--ig {
  --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='%23000'%3E%3Crect x='1' y='1' width='14' height='2'/%3E%3Crect x='1' y='13' width='14' height='2'/%3E%3Crect x='1' y='1' width='2' height='14'/%3E%3Crect x='13' y='1' width='2' height='14'/%3E%3Crect x='5' y='5' width='6' height='2'/%3E%3Crect x='5' y='9' width='6' height='2'/%3E%3Crect x='5' y='5' width='2' height='6'/%3E%3Crect x='9' y='5' width='2' height='6'/%3E%3Crect x='10' y='3' width='2' height='1'/%3E%3C/g%3E%3C/svg%3E");
}

/* ---------- 6. SEÇÕES ---------------------------------------------------- */

/* 6.1 hero */
.hero { padding-block: clamp(48px, 8vw, 92px) var(--bay); }

/* Em tela baixa cada pixel de respiro sai do argumento. Sem isto o nome tinha
   de encolher muito mais para os botões caberem na abertura (o módulo 9 é
   quem faz essa conta). O gatilho é a altura e não a largura: monitor largo e
   baixo tem exatamente o mesmo problema que notebook. */
@media (min-width: 900px) and (max-height: 1000px) {
  .hero { padding-block: clamp(28px, 3vw, 44px) var(--bay); }
  .masthead { margin-block: 8px clamp(18px, 2.4vw, 28px); }
}
.hero__grid {
  display: grid;
  gap: clamp(30px, 5vw, 56px);
  align-items: center;
  grid-template-columns: minmax(0, 1fr);
}
/* 6.1.1 masthead - o nome é a imagem, não o rótulo acima dela. Cada linha é
   medida no JS e ganha um corpo em px inteiro (fonte bitmap só fica limpa em
   tamanho cheio), então as duas encostam nas bordas da coluna.

   Antes sangrava a tela inteira. Duas coisas quebravam: em corpo de ~230px a
   Silkscreen mostrava o grid de 5x7 cru e "Taraschi" virava um friso de blocos
   sem palavra dentro, e o nome era o único elemento da página fora da medida
   de leitura. Agora mora no `.wrap` como todo o resto e usa a Pixelify Sans,
   que tem pixel bem menor. Menos tamanho, mais desenho por letra. */
.masthead {
  margin-block: clamp(10px, 2vw, 20px) clamp(26px, 4vw, 46px);
  font-family: var(--font-title);
  font-weight: 700;          /* o peso cheio é o que engrossa a haste de 1 px */
  letter-spacing: 0.04em;    /* grid mais fino pede menos ar que a Silkscreen */
  line-height: 0.86;
  font-size: 0;              /* mata o espaço em branco entre as duas linhas */
}
.masthead__line {
  display: block;
  width: max-content;
  white-space: nowrap;
  font-size: 12vw;           /* valor de partida; o JS refina */
  /* a última letra também ganha tracking - puxar de volta para encostar */
  margin-right: -0.04em;
}

/* 6.1.2 letra a letra: a que está sob o cursor sobe um fio e ganha um risco
   embaixo, que entra em três degraus da esquerda para a direita. Nada de
   inverter o bloco inteiro - em corpo de 12vw aquilo virava um tapa. No modo
   claro cada terça letra pega um acento diferente; no escuro os três são areia. */
.masthead__line .ch {
  display: inline-block;
  position: relative;
  z-index: 2;                /* acima das cópias do glitch */
  background-image: linear-gradient(var(--pop-1), var(--pop-1));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 0.05em;
  transition: transform 90ms steps(2, end),
              background-size 110ms steps(3, end);
}
.masthead__line .ch:nth-child(3n+2) { background-image: linear-gradient(var(--pop-2), var(--pop-2)); }
.masthead__line .ch:nth-child(3n)   { background-image: linear-gradient(var(--pop-3), var(--pop-3)); }
.masthead__line .ch:hover {
  transform: translateY(-0.02em);
  background-size: 100% 0.05em;
}

/* o acidente: a cada meio minuto uma faixa da linha desliza e volta.
   Duas cópias recortadas em bandas, deslocadas em sentidos opostos. */
.masthead__line { position: relative; }
.masthead__line::before,
.masthead__line::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--paper);
  pointer-events: none;
  opacity: 0;
}
.masthead__line.tear::before {
  opacity: 1;
  clip-path: inset(26% 0 48% 0);
  animation: tear-a 180ms steps(2, end) 1;
}
.masthead__line.tear::after {
  opacity: 1;
  clip-path: inset(64% 0 12% 0);
  animation: tear-b 180ms steps(2, end) 1;
}
@keyframes tear-a {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(-9px); }
}
@keyframes tear-b {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(6px); }
}

.hero__role {
  margin-top: 0;
  font-family: var(--font-mono);
  /* a Space Mono é larga: 0.14em nesta linha longa empurrava ela pra duas */
  font-size: clamp(12px, 1.5vw, 14px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.hero__desc { margin-top: 20px; }
.hero__cta { margin-top: 32px; display: flex; flex-wrap: wrap; gap: 12px; }

/* A coluna de texto do hero vira o container das duas regras abaixo. É ela que
   aperta, não a tela: no celular ela ocupa a largura toda, mas a partir de
   940px ela divide o hero com o retrato e volta a ficar estreita. Media query
   consertava o telefone e deixava o notebook com o mesmo defeito. */
.hero__grid > div { container-type: inline-size; }

/* Três botões numa coluna curta caíam 2 + 1 e sobrava um buraco quadrado do
   lado do terceiro, que lia como botão que faltou carregar. Empilhados na
   largura toda não tem sobra pra ficar torta, e o alvo do polegar cresce. */
@container (max-width: 500px) {
  .hero__cta .btn { flex: 1 1 100%; }
}
/* 6.1.3 ficha - a coluna da esquerda acabava muito antes do painel e sobrava
   um buraco. Quatro linhas de metadado resolvem o vazio e são conteúdo real. */
.ficha {
  margin: clamp(30px, 4vw, 44px) 0 0;
  border-top: 2px solid var(--ink);
}
.ficha > div {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px 16px;
  padding: 11px 10px;
  margin-inline: -10px;
  border-bottom: 1px solid var(--rule-soft);
  transition: background-color 140ms steps(2, end);
}
.ficha > div:hover { background-color: var(--paper-2); }
.ficha dt {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
/* o rótulo é mono miúdo, então o valor precisa de peso pra segurar a linha.
   `tabular-nums` porque os valores estão alinhados à direita numa coluna: com
   algarismo de largura proporcional, o "1" é mais estreito que o "9" e as
   linhas que terminam em número deixam de bater com as de cima e de baixo. */
.ficha dd {
  margin: 0;
  text-align: right;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
/* Rótulo à esquerda e valor à direita só funciona enquanto os dois cabem na
   mesma linha. Numa coluna curta "LangGraph, MCP e Claude Code" não cabia,
   quebrava pra linha de baixo e ia pro canto esquerdo - e aí uma linha da
   ficha estava alinhada de um jeito e as outras três de outro. Empilha tudo à
   esquerda. Mesmo container do bloco de botões, pela mesma razão. */
@container (max-width: 430px) {
  .ficha > div { display: block; }
  .ficha dd { text-align: left; margin-top: 3px; }
}

.hero__figure { min-width: 0; }
.hero__canvas-wrap {
  position: relative;
  aspect-ratio: 1 / 1;
  background: var(--paper);
  overflow: hidden;
}
.hero__canvas-wrap canvas {
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  cursor: crosshair;
  /* `none` aqui entregava todo gesto pro canvas, inclusive o de rolar: no
     celular o dedo passando por cima da esfera girava a luz e a página ficava
     parada. `pan-y` devolve a rolagem vertical pro browser e o canvas fica só
     com o que sobra - que é o que ele precisa, porque seguir o ponteiro é
     gesto de mouse, e no toque a esfera não tem o que perder. */
  touch-action: pan-y;
}
.hero__cap {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 10px;
  border-top: 2px solid var(--ink);
  color: var(--ink-2);
}
/* mesma troca que a dica de cor da legenda das capturas faz: a instrução só
   aparece onde ela é executável. `display: none` e não `visibility`, senão o
   espaço vazio ficaria no meio do space-between e a legenda desalinharia. */
.hero__cap--toque { display: none; }
@media (hover: none) {
  .hero__cap--ponteiro { display: none; }
  .hero__cap--toque { display: inline; }
}

/* 6.2 sobre */
.about__grid {
  display: grid;
  gap: clamp(28px, 4vw, 48px);
  grid-template-columns: minmax(0, 1fr);
  /* `start` e não o `stretch` que a grade dá de graça. A coluna da direita tem
     uma ficha de seis linhas ao lado de uma coluna de texto bem mais alta:
     esticada, ela virava um painel emoldurado com 480px de nada dentro, e
     moldura vazia lê como conteúdo que não carregou. Terminando na última
     linha, o que sobra vira fundo de página, que não promete nada.
     Medido: 929px de painel para 449px de conteúdo. */
  align-items: start;
}
.spec-row {
  display: flex;
  gap: 14px;
  padding: 11px 0;
  border-bottom: 1px solid var(--rule-soft);
}
.spec-row:last-child { border-bottom: 0; }
.spec-row dt {
  flex: none;
  width: 104px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.spec-row dd { margin: 0; flex: 1 1 auto; }

/* 6.3 skills */
/* Eram cinco cards num `auto-fit` de 240px, e cinco não fecha fila nenhuma:
   quatro colunas acima de 1180 deixavam três buracos, três colunas deixavam
   um, duas colunas deixavam outro. Medido de 600 a 1600 e sobrava em todas.
   É o mesmo buraco dos números do case, só que na seção que existe pra
   impressionar e no card mais novo da pilha.

   Seis trilhos em vez de `auto-fit`: o card normal toma dois (três colunas)
   ou três (duas colunas), e a fila que sobra incompleta se abre até encostar
   nas bordas. Três colunas em vez de quatro no desktop é ganho de tabela: o
   card fica ~360px em vez de ~270 e os títulos de duas palavras param de
   quebrar no meio.

   As duas regras da cauda são estruturais de propósito, como a dos games: se
   um sexto card entrar, as duas param de valer sozinhas e a grade volta a
   fechar duas filas cheias. Nenhuma delas sabe que hoje são cinco. */
.grid-skills {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(14px, 2vw, 20px);
}
@media (min-width: 560px) {
  .grid-skills { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .skill { grid-column: span 3; }
  /* fila final com um card só: ele atravessa em vez de ficar ao lado do vazio */
  .skill:last-child:nth-child(odd) { grid-column: span 6; }
}
@media (min-width: 900px) {
  .skill,
  .skill:last-child:nth-child(odd) { grid-column: span 2; }
  /* dois na última fila de três: cada um toma metade e a fila fecha */
  .skill:nth-last-child(2):nth-child(3n+1),
  .skill:nth-last-child(2):nth-child(3n+1) ~ .skill { grid-column: span 3; }
  /* um só na última fila de três: atravessa */
  .skill:last-child:nth-child(3n+1) { grid-column: span 6; }
}
.skill {
  position: relative;
  border: 2px solid var(--ink);
  background: var(--paper);
  padding: 18px;
  transition: box-shadow 120ms steps(2, end);
}
/* a retícula inunda o card da esquerda para a direita em seis degraus, a mesma
   ideia do `varrer` dos títulos. O ::before entra antes no DOM e os filhos são
   `position: relative`, então a textura fica atrás do texto sem precisar de
   z-index. O card continua sem levantar: não é clicável, e um card que se
   comporta como botão sem ser um é mentira de interface. */
.skill::before {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--paper-2);
  /* Os pontos eram `--ink`, a mesma tinta cheia do título, e o parágrafo é
     `--ink-2`. Ou seja: ao passar o cursor, o fundo ficava MAIS CLARO que a
     letra e o texto sumia dentro da própria textura. Em `--rule-soft` a
     retícula continua inundando o card, só que agora por baixo da leitura, que
     é onde textura de fundo deveria estar desde o começo. */
  background-image: radial-gradient(var(--rule-soft) 0.8px, transparent 0.9px);
  background-size: 9px 9px;
  clip-path: inset(0 100% 0 0);
  transition: clip-path 300ms steps(6, end);
}
.skill:hover { box-shadow: var(--shadow); }
.skill:hover::before { clip-path: inset(0 0 0 0); }
/* e a outra metade do conserto: o texto sobe para a tinta cheia enquanto a
   retícula entra, então o contraste aumenta no hover em vez de cair */
.skill:hover p { color: var(--ink); }

.skill__bar {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
}
.skill__swatch {
  flex: none;
  width: 26px;
  height: 26px;
  border: 2px solid var(--ink);
  transition: background-position 240ms steps(4, end),
              background-color 240ms steps(2, end);
}
/* a amostra vai no sentido contrário do resto: o card ganha pontos, ela troca
   de polaridade. Densidade que sobe de um lado e inverte do outro é o que dá o
   estalo de 1 bit. */
.skill:hover .skill__swatch {
  --dot: var(--paper);
  background-position: 4px 4px;
  background-color: var(--ink);
}
.skill p { position: relative; color: var(--ink-2); font-size: 15px; }

/* 5.6.1 a órbita da stack ------------------------------------------------
   Círculos inteiros, centrados na esfera. A primeira versão cortava tudo na
   borda de baixo para virar horizonte, e o que saía era um nascer do sol.

   Nada aqui gira liso. O anel anda em 60 degraus, um pulo de 6 graus por vez:
   é a mesma regra do resto do site, e é o que separa isto de um carrossel. O
   chip roda ao contrário na mesma cadência para o rótulo nunca ficar de
   cabeça para baixo.

   O ângulo fixo de cada braço mora na propriedade `rotate` e o giro animado em
   `transform`. São propriedades independentes e se compõem, o que dispensa
   guardar o ângulo inicial numa variável só para a keyframe somá-lo de volta. */
.orb {
  --h: clamp(300px, 46vw, 520px);
  position: relative;
  height: var(--h);
  margin-top: clamp(34px, 5vw, 60px);
}
/* tudo parado até o módulo 16 dizer que a órbita está em cena. Sem JS fica um
   diagrama estático, que é uma degradação honesta. */
.orb :is(.orb__ring, .orb__node) { animation-play-state: paused; }
.orb.is-girando :is(.orb__ring, .orb__node) { animation-play-state: running; }

/* o sprite dos glifos não desenha nada sozinho, só guarda os <g> */
.orb__defs { position: absolute; width: 0; height: 0; }

/* o globo é a esfera do módulo 5 - mesmo canvas, mesma paleta, mesma erosão
   por scroll. Não vale reescrever partícula nenhuma: já está no arquivo. */
.orb__globo,
.orb__ring {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--d);
  height: var(--d);
  margin: calc(var(--d) / -2) 0 0 calc(var(--d) / -2);
  border-radius: 50%;
}
/* o canvas é pintado num buffer de ~40x40 e esticado. Sem `pixelated` o browser
   interpola e a esfera vira mancha; sem tirar o `border-radius` sobra um recorte
   redondo suavizado por cima de pixels duros. A silhueta redonda já vem pintada. */
.orb__globo {
  --d: calc(var(--h) * 0.30);
  border-radius: 0;
  image-rendering: pixelated;
}

/* o aro é um ::before, não o próprio anel: `mask-image` recorta o elemento E
   todos os descendentes, então a máscara em cima do anel comia os chips e
   deixava só a lasca que cruzava a faixa de 3px. */
.orb__ring { pointer-events: none; animation: orbitar var(--dur) steps(60, end) infinite; }
.orb__ring::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background-image: repeating-conic-gradient(
    var(--ink) 0 calc(var(--passo) / 2), transparent 0 var(--passo));
  -webkit-mask-image: radial-gradient(closest-side, transparent calc(100% - 4px), #000 calc(100% - 4px));
          mask-image: radial-gradient(closest-side, transparent calc(100% - 4px), #000 calc(100% - 4px));
}
/* o anel do meio corre ao contrário: dois sentidos numa composição só já dão a
   sensação de mecanismo, três dariam confusão.

   `--passo` é o período do tracejado em graus e sempre divide os 6deg de cada
   degrau do steps(60). Isso é o conserto do borrão: antes o aro era uma grade
   quadrada de 3px girando junto, e grade quadrada em rotação nunca cai em cima
   do pixel - reamostrava a cada quadro e virava cinza. Tracejado angular que fecha
   um número inteiro de períodos por degrau desenha exatamente igual sempre. */
.orb__ring--1 { --d: calc(var(--h) * 0.42); --dur: 26s; --passo: 6deg; }
.orb__ring--2 { --d: calc(var(--h) * 0.66); --dur: 34s; --passo: 3deg; animation-name: contragiro; }
.orb__ring--3 { --d: calc(var(--h) * 0.90); --dur: 46s; --passo: 2deg; }

.orb__braco {
  position: absolute;
  top: 0;
  left: 50%;
  width: 0;
  height: 50%;
  transform-origin: bottom center;
  rotate: var(--a);
}
.orb__node {
  position: absolute;
  top: 0;
  left: 0;
  translate: -50% -50%;
  rotate: calc(-1 * var(--a));
  animation: contragiro var(--dur) steps(60, end) infinite;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 6px 10px;
  border: 2px solid var(--ink);
  background: var(--paper);
  box-shadow: 3px 3px 0 var(--ink);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  /* o braço tem `width: 0`, então o chip cai no shrink-to-fit contra um espaço
     disponível de zero e o browser arredonda a largura pra baixo: sobrava sempre
     um algarismo de fora ("POSTGRESQ", "CLAUDE COD"). No celular o corpo menor
     ficava abaixo do arredondamento e por isso lá aparecia inteiro. */
  width: max-content;
}
.orb__ring--2 .orb__node { animation-name: orbitar; }

/* o chip responde ao mouse. O anel inteiro é `pointer-events: none` para a
   máscara do aro não roubar o ponteiro, e isso desce pros filhos: sem devolver
   aqui, o chip nunca receberia hover nenhum.

   Tudo isto é CSS puro e sem estado - foi de propósito. A versão que deixava
   arrastar o chip para fora dependia de congelar a órbita, medir o encaixe e
   desenhar uma corda entre os dois, e bastava um ponteiro perdido no meio para
   a corda ficar esticada na tela sem chip nenhum na ponta. */
.orb__node {
  pointer-events: auto;
  -webkit-user-select: none;
          user-select: none;
  color: var(--ink);
  text-decoration: none;
  transition: translate 90ms steps(2, end), box-shadow 90ms steps(2, end);
}
/* a elevação vai em `translate`, não em `transform`: o contragiro do chip roda
   em `transform`, e mexer lá derrubaria a rotação que mantém o rótulo de pé */
.orb__node:hover,
.orb__node:focus-visible {
  translate: calc(-50% - 2px) calc(-50% - 2px);
  box-shadow: 6px 6px 0 var(--ink);
}
/* a seta de link externo, mesma marca dos cartões de projeto. Some no repouso
   por opacidade e não por display: assim ela já ocupa a largura dela desde
   sempre e o chip não cresce um caractere na hora do hover. */
.orb__node::after {
  content: '↗';
  margin-left: 1px;
  opacity: 0;
  transition: opacity 90ms steps(2, end);
}
.orb__node:hover::after,
.orb__node:focus-visible::after { opacity: 1; }

/* no clique afunda e troca de polaridade, igual a todo botão do site */
.orb__node:active {
  translate: calc(-50% + 2px) calc(-50% + 2px);
  box-shadow: 0 0 0 var(--ink);
  background: var(--ink);
  color: var(--paper);
}
/* alvo em movimento é alvo difícil: com o cursor num chip - ou com o teclado
   parado nele - a órbita inteira para. Sem o :focus-within, tabular pela órbita
   seria perseguir um link que anda. Especificidade acima da regra do
   .is-girando, senão nada acontece. */
.orb:has(.orb__node:hover) :is(.orb__ring, .orb__node),
.orb:focus-within :is(.orb__ring, .orb__node) { animation-play-state: paused; }

/* traço de 2px sem antialias: o glifo fica com a mesma dureza do resto */
.orb__ico {
  flex: none;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  shape-rendering: crispEdges;
}

@keyframes orbitar    { to { transform: rotate(360deg); } }
@keyframes contragiro { to { transform: rotate(-360deg); } }

@media (max-width: 640px) {
  .orb { --h: clamp(280px, 78vw, 400px); }
  .orb__node { gap: 5px; padding: 4px 7px; font-size: 9px; box-shadow: 2px 2px 0 var(--ink); }
  .orb__ico { width: 13px; height: 13px; }
}

/* 6.4 contato */
.contact__grid {
  display: grid;
  gap: clamp(28px, 4vw, 48px);
  grid-template-columns: minmax(0, 1fr);
}

/* 6.5 inversão local - o ritmo do board de referência (tokens no bloco 1) */
.invert {
  background: var(--paper);
  color: var(--ink);
}

/* 6.5c a virada de polaridade dissolve em vez de cortar.
   O site inteiro fala em retícula e as seis trocas de cor eram seis linhas
   retas de régua, atravessando a tela. A `.ramp` que já existia não resolve
   isso e nem tentava: ela mora dentro do `.wrap`, tem largura de coluna, e é
   um divisor entre blocos - o corte da polaridade é sangrado, de borda a borda.

   A faixa mora DENTRO da seção invertida, e não pendurada fora dela: `.sec` usa
   `content-visibility: auto`, que implica contenção de pintura, e tudo que
   passasse da caixa seria recortado sem aviso. Dentro, ela pinta na cor de
   quem está do outro lado - que é `--ink` aqui, porque a inversão de polaridade
   é justamente essa: a tinta do bloco invertido É o papel da página. Um token
   novo para isso seria um quinto lugar para desalinhar.

   Cinco fileiras de 9px, cada uma com o ponto menor que a de cima. Degrau e não
   rampa contínua: o resto do site anima em `steps()` e um degradê suave aqui
   seria a única superfície interpolada da página. `repeat-x` é o que prende
   cada camada na sua fileira - sem ele cada gradiente ladrilharia a faixa
   inteira e as cinco densidades virariam uma sopa só.

   z-index negativo porque a contenção de pintura já fez da seção um contexto
   de empilhamento: a faixa cai atrás do texto e continua na frente do fundo,
   que é exatamente onde ela precisa estar. */
.sec.invert { position: relative; }
.sec.invert::before,
.sec.invert::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 54px;
  z-index: -1;
  pointer-events: none;
  background-repeat: repeat-x;
  background-size: 9px 9px;
  /* A primeira fileira tem raio maior que meia célula de propósito: os círculos
     encostam nos vizinhos e só sobram os quatro vãos das quinas. Sem ela a
     faixa começava em 53% de cobertura e ficava uma costura visível entre o
     bloco cheio e o primeiro degrau. */
  background-image:
    radial-gradient(circle, var(--ink) 4.7px, transparent 4.9px),
    radial-gradient(circle, var(--ink) 3.7px, transparent 3.9px),
    radial-gradient(circle, var(--ink) 2.9px, transparent 3.1px),
    radial-gradient(circle, var(--ink) 2.1px, transparent 2.3px),
    radial-gradient(circle, var(--ink) 1.4px, transparent 1.6px),
    radial-gradient(circle, var(--ink) 0.8px, transparent 1px);
  background-position: 0 0, 0 9px, 0 18px, 0 27px, 0 36px, 0 45px;
}
.sec.invert::before { top: 0; }
/* A de baixo é a mesma lista de imagens com as posições ao contrário, para a
   fileira cheia encostar na borda de baixo: o bloco desmancha dos dois lados.
   Inverter aqui, e não com um `scaleY(-1)`, porque espelhar por transform
   obriga a origem a ficar no centro - e a origem é justamente o que a revelação
   logo abaixo precisa ter na borda. */
.sec.invert::after {
  bottom: 0;
  background-position: 0 45px, 0 36px, 0 27px, 0 18px, 0 9px, 0 0;
}

/* Onde tem rampa na frente, esta faixa some: a rampa já faz a dissolução
   inteira, dos 135px, numa grade só, e ela é opaca justamente para poder cobrir
   este pedaço. Duas peças aqui era o defeito - a de baixo não continuava a de
   cima, recomeçava com outra grade, e o olho lia duas transições emendadas.
   O porquê inteiro está no comentário da `.ramp`, no módulo 4.

   As outras três continuam: o #games não tem rampa e encosta em bloco chapado,
   e as duas de baixo também. Aí a fileira de 4.7 emenda em cor cheia da própria
   cor dela, que é para o que ela foi calculada, e continua certa. */
.ramp + .sec.invert::before { display: none; }

/* E a faixa se revela fileira a fileira conforme a virada sobe na tela, presa à
   rolagem: `animation-timeline: view()` é o relógio, sem um listener de scroll
   e sem uma linha de JS. `steps(6, end)` com seis fileiras de 9px dá exatamente
   uma fileira por degrau - a erosão acontece em pixel inteiro, que é a única
   forma honesta num sistema de 1 bit.
   `clip-path` e não `scaleY`: escalar achataria os pontos em elipses no meio do
   caminho, e ponto de retícula esmagado é a coisa que mais denuncia efeito.
   Onde a API ainda não existe, a faixa fica inteira - que já é o desenho certo.
   A animação é o tempero, nunca o prato. */
@supports (animation-timeline: view()) {
  @media not (prefers-reduced-motion: reduce) {
    .sec.invert::before,
    .sec.invert::after {
      animation: linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 42%;
      animation-timing-function: steps(6, end);
    }
    .sec.invert::before { animation-name: brotar-desce; }
    .sec.invert::after  { animation-name: brotar-sobe; }
  }
}
/* O `to` é escrito, e não deixado implícito como no `erodir` lá de cima: sem
   ele o valor de chegada é o `clip-path: none` do repouso, e `inset()` não
   interpola com `none` - a faixa pulava de invisível a inteira num quadro só,
   no meio do percurso. Medido: `none` já no primeiro terço da rolagem. */
@keyframes brotar-desce {
  from { clip-path: inset(0 0 100% 0); }
  to   { clip-path: inset(0 0 0 0); }
}
@keyframes brotar-sobe {
  from { clip-path: inset(100% 0 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* 6.5b a lâmpada na barra do retrato ------------------------------------- */
/* Desenhada aqui, não importada: é bulbo mais bocal em traço de 2px, na mesma
   regra dos glifos da órbita. O `right` abre passagem para o ilhó do .pinned,
   que mora em right:13px e não pode ser coberto. */
.panel__luz {
  position: relative;
  margin-left: auto;
  margin-right: 22px;
  display: grid;
  place-items: center;
  width: 26px;
  height: 22px;
  padding: 0;
  border: 0;
  /* fundo sólido pela mesma razão que o panel__title tem um: a barra é hachura
     de 1px em 3px, e um glifo de traço fino desenhado por cima dela vira ruído.
     O papel abre a janela onde a lâmpada cabe. */
  background: var(--paper);
}
/* alvo de toque de 44px sem inchar a barra do painel, que tem 34px de altura */
.panel__luz::after { content: ''; position: absolute; inset: -11px; }
.panel__luz-glifo {
  position: relative;
  display: block;
  width: 11px;
  height: 11px;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--paper);
  transition: background-color 120ms steps(2, end);
}
/* o bocal, embaixo do bulbo */
.panel__luz-glifo::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 100%;
  width: 7px;
  height: 3px;
  margin-left: -3.5px;
  border: 2px solid var(--ink);
  border-top: 0;
  background: var(--paper);
}
/* acesa: o bulbo enche de tinta e solta quatro raios nos eixos */
.panel__luz[aria-checked='true'] .panel__luz-glifo { background: var(--ink); }
.panel__luz[aria-checked='true'] .panel__luz-glifo::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2px;
  height: 2px;
  margin: -1px;
  box-shadow: 0 -9px 0 var(--ink), 0 9px 0 var(--ink),
              -9px 0 0 var(--ink), 9px 0 0 var(--ink);
}
.panel__luz:hover .panel__luz-glifo { transform: translate(-1px, -1px); }
.panel__luz:active .panel__luz-glifo { transform: translate(1px, 1px); }

/* 6.6 ilhós - os painéis da referência são pendurados, não flutuam */
.pinned { position: relative; }
.pinned::before,
.pinned::after {
  content: '';
  position: absolute;
  top: 10px;
  width: 7px;
  height: 7px;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--paper);
  z-index: 2;
}
.pinned::before { left: 13px; }
.pinned::after  { right: 13px; }

/* 6.7 página de detalhe */
.detail__hero {
  aspect-ratio: 16 / 9;
  border: 2px solid var(--ink);
  box-shadow: var(--shadow);
}
.gallery {
  display: grid;
  gap: clamp(14px, 2vw, 22px);
  grid-template-columns: minmax(0, 1fr);
}
.gallery__cell {
  aspect-ratio: 4 / 3;
  border: 2px solid var(--ink);
  display: grid;
  place-items: end start;
  padding: 10px;
}
.pager {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: space-between;
  border-top: 2px solid var(--ink);
  padding-top: 22px;
}

/* 6.8 o retrato - vive no canto do hero e é o interruptor de luz ---------- */
/* ponytail: sem seção nem rótulo próprio - a cara dele é o único gatilho de
   tema do site. Achar é parte da graça; o title= dá a dica só no hover. */
.hero__me {
  position: absolute;
  left: 14px;
  bottom: 14px;
  z-index: 3;
  display: block;
  width: min(30%, 120px);            /* 120px = largura nativa do frame */
  padding: 0;
  background-color: var(--face-paper);
  transition: transform 110ms steps(2, end), box-shadow 110ms steps(2, end);
}
.hero__me:hover  { transform: translate(-3px, -3px); box-shadow: 7px 7px 0 var(--ink); }
.hero__me:active { transform: translate(3px, 3px);   box-shadow: 0 0 0 var(--ink); }

/* barra de paciência: enche a cada cutucada e estoura na quinta.
   `--face-ink` e não `--ink`: o cartão do rosto é claro nos dois temas (o
   --face-paper não inverte), e no escuro o --ink é a mesma areia #fce4a6 do
   fundo dele. A barra existia e ninguém via, porque era areia sobre areia. */
.hero__me::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 4px;
  background: var(--face-ink);
  transform: scaleX(calc(var(--pokes, 0) / 5));
  transform-origin: left;
  transition: transform 120ms steps(2, end);
}
/* Encher é resposta a clique e tem que ser seco. Esvaziar é a paciência dele
   voltando, e aí vale ser lento: oito degraus em meio segundo, dá pra ver o
   progresso indo embora em vez de ele sumir. Quem põe e tira a classe é o
   módulo 7. */
.hero__me.esvaziando::after { transition: transform 520ms steps(8, end); }

/* cada cutucada dá um empurrão seco; a quinta chama o flinch inteiro */
@keyframes poke {
  0%   { transform: translate(0, 0); }
  45%  { transform: translate(2px, 2px); }
  100% { transform: translate(0, 0); }
}
.hero__me.poke { animation: poke 140ms steps(2, end) 1; }

/* tira de 22 frames ditherizados, avançada por steps() - sem GIF, sem vídeo.
   O arquivo é uma máscara de alfa (tinta opaca, papel furado), não um bitmap
   colorido: assim o retrato assume a paleta do tema em vez de ficar preso nos
   tons em que foi assado. A cor sai de --face-ink, que não inverte.
   ponytail: a máscara não respeita image-rendering, então o desenho suaviza um
   fio quando não cai em 1:1 - por isso o .hero__me trava em 120px, a largura
   nativa do frame. Se um dia precisar de outro tamanho, assar de novo. */
.film {
  --frames: 22;
  --film-dur: 760ms;
  display: block;
  width: 100%;
  aspect-ratio: 120 / 200;
  background-color: var(--face-ink);
  -webkit-mask-image: url('../assets/luan-film-mask.png');
          mask-image: url('../assets/luan-film-mask.png');
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: calc(var(--frames) * 100%) 100%;
          mask-size: calc(var(--frames) * 100%) 100%;
  /* --parado é o quadro de repouso do tema; --espia é quatro quadros adentro,
     onde a mão já está levantada e a caminho dos óculos. É a pose de quem
     parou esperando: em dois ou três ainda parece tremida, em seis ele já
     pegou e a espiada entrega a piada de graça.
     Os valores são i/21, porque a tira tem 22 quadros. */
  --parado: 100%;                      /* frame 21: sem óculos = escuro */
  --espia: 80.952%;                    /* frame 17 */
  -webkit-mask-position: var(--parado) 0;
          mask-position: var(--parado) 0;
  /* transição, e não animação, é o que faz a espiada PARAR onde chegou em vez
     de voltar sozinha: enquanto o cursor está em cima o valor final continua
     sendo --espia, e ele fica lá. Sair do retrato desfaz pelo mesmo caminho,
     de graça, sem segunda animação. */
  transition: -webkit-mask-position 300ms steps(5, jump-none),
                      mask-position 300ms steps(5, jump-none);
}
:root[data-theme='light'] .film {      /* frame 0: de óculos */
  --parado: 0%;
  --espia: 19.048%;                    /* frame 4 */
}

/* Segura o quadro do tema anterior enquanto a cortina passa e a reação não
   chegou. Sem isto o repouso troca junto com o tema e os óculos aparecem de
   estalo antes da animação que existe justamente para mostrá-los sendo postos.
   Quem tira é o módulo 6, no mesmo instante em que solta a animação. */
.film.congelado {
  -webkit-mask-position: var(--congela) 0;
          mask-position: var(--congela) 0;
}

/* Ele estava parado esperando clique, e ninguém cutuca uma foto que parece
   foto. Aqui a mão sobe três quadros e volta: não conta a piada, só avisa que
   tem alguém ali dentro. É o gatilho de curiosidade que faltava para o caminho
   das cinco cutucadas existir.
   O :not() dos dois é obrigatório: esta regra é mais específica que
   `.film.play-on`, e sem ele passar o cursor no meio da troca de tema
   sequestraria a animação da piada. */
/* O aceno de chegada, uma vez só: aqui a ida e a volta são de propósito,
   porque não tem cursor parado em cima para segurar o quadro.
   steps(5), não steps(4): `jump-none` distribui N valores em N-1 intervalos.
   A primeira versão usava 3 para 3 quadros e o valor do meio caía em 92,857%,
   que não é fronteira de quadro nenhum: a máscara mostrava metade do quadro 20
   e metade do 19 e o retrato aparecia com duas caras. Para N quadros de
   distância são N+1 valores. */
@keyframes espiar {
  0%, 100% { -webkit-mask-position: var(--parado) 0; mask-position: var(--parado) 0; }
  50%      { -webkit-mask-position: var(--espia) 0;  mask-position: var(--espia) 0; }
}
.film.espia:not(.play-on):not(.play-off) {
  animation: espiar 520ms steps(5, jump-none) 1;
}
/* E o hover, que segura. Sem animação: só o valor final mudando, com a
   transição do .film levando ele até lá em cinco degraus. Os :not() protegem a
   troca de tema - esta regra é mais específica que .film.play-on e sem eles
   passar o cursor no meio da piada sequestraria a máscara. */
@media (hover: hover) and (pointer: fine) {
  .hero__me:hover .film:not(.play-on):not(.play-off):not(.espia):not(.congelado) {
    -webkit-mask-position: var(--espia) 0;
            mask-position: var(--espia) 0;
  }
}

@keyframes film-strip {
  from { -webkit-mask-position: 0% 0;   mask-position: 0% 0; }
  to   { -webkit-mask-position: 100% 0; mask-position: 100% 0; }
}
/* claro → escuro: ele tira os óculos (tira para frente) */
.film.play-off { animation: film-strip var(--film-dur) steps(22, jump-none) 1 both; }
/* escuro → claro: a luz incomoda, ele põe os óculos (tira ao contrário) */
.film.play-on  { animation: film-strip var(--film-dur) steps(22, jump-none) 1 reverse both; }

/* o susto com a luz acesa: deslocamentos secos, sem interpolação */
@keyframes flinch {
  0%   { transform: translate(0, 0); }
  18%  { transform: translate(-3px, 2px); }
  36%  { transform: translate(3px, -2px); }
  54%  { transform: translate(-2px, -2px); }
  72%  { transform: translate(2px, 1px); }
  100% { transform: translate(0, 0); }
}
.hero__me.flinch { animation: flinch 260ms steps(1, end) 1; }

/* revelação ao rolar. A subida continua contínua - é entrada, não reação - mas
   a opacidade pula em quatro quadros: um sistema de 1 bit não tem meio-tom, e
   o fade liso era a única coisa no site que desmentia isso.
   O --i vem do módulo 6 e escalona irmãos do mesmo pai, teto de 5. */
.reveal { opacity: 0; transform: translateY(18px); }
.reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 380ms steps(4, end), transform 420ms ease-out;
  transition-delay: calc(var(--i, 0) * 60ms);
}
/* na linha do tempo a entrada vem do lado, no sentido do próprio trilho */
.tl__item.reveal { transform: translateX(-18px); }

/* o título de seção é varrido, na mesma direção da cortina do tema. Sem
   fill-mode na saída de propósito: quando a animação acaba o clip deixa de
   existir, e nada fica recortado depois. */
@keyframes varrer { from { clip-path: inset(0 100% 0 0); } }
.sec__head.is-in .kicker { animation: varrer 320ms steps(6, end) backwards; }
.sec__head.is-in .h2     { animation: varrer 520ms steps(9, end) 120ms backwards; }

/* o trilho da linha do tempo se desenha sozinho: uma tira de papel da largura
   da borda recua de baixo para cima e a linha aparece atrás dela. É cobertura,
   não a linha em si - sem JS a borda continua inteira, como sempre foi. */
.tl { position: relative; }
/* o .tl entra no observador só para disparar o trilho: quem aparece são os
   itens dele, não o bloco inteiro */
.tl.reveal { opacity: 1; transform: none; }
@keyframes destracar { to { transform: scaleY(0); } }
.tl.is-in::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -2px;
  width: 2px;
  background: var(--paper);
  transform-origin: bottom;
  animation: destracar 820ms steps(14, end) forwards;
}
/* os marcadores nascem depois de o trilho passar por eles */
@keyframes marcar { from { transform: scale(0); } }
.tl__item.is-in::before {
  animation: marcar 240ms steps(3, end) calc(200ms + var(--i, 0) * 160ms) backwards;
}

/* thumb e amostra entram erodindo - mesmo gesto da rampa, mesma keyframe */
.card.is-in .card__thumb,
.skill.is-in .skill__swatch {
  animation: erodir 560ms steps(4, end) calc(var(--i, 0) * 60ms) backwards;
}

/* ---------- 6.9 TRANSIÇÃO ENTRE PÁGINAS ---------------------------------- */
/* Era o único corte seco que sobrava: tudo no site tem continuidade, e clicar
   num card piscava branco e trocava de página como se fosse outro produto.
   Isto é a API nativa de navegação entre documentos - sem JS, sem biblioteca,
   e quem não suporta (Firefox por enquanto) continua vendo o corte de hoje.

   Dentro do @media: transição de página é movimento como qualquer outro, e
   quem pediu para reduzir movimento não pediu menos, pediu nenhum. */
@media not (prefers-reduced-motion: reduce) {
  @view-transition { navigation: auto; }
}

/* O par que se transforma: o quadrado dithered do card vira o print do topo do
   case. Os dois já são o mesmo desenho em tamanhos diferentes, então o browser
   só precisa interpolar a caixa. O nome entra no thumb clicado pelo JS (módulo
   19) porque só um par pode carregar o mesmo nome por vez - marcar os seis de
   antemão faria o browser desistir da transição inteira por nome repetido. */
.case__head + .shot { view-transition-name: case-capa; }

/* passos em vez de curva: o resto do site inteiro anima em steps() e uma
   interpolação suave aqui denunciaria que veio de outro lugar */
::view-transition-group(case-capa) {
  animation-duration: 320ms;
  animation-timing-function: steps(6, end);
}

/* ---------- 7. RESPONSIVO ------------------------------------------------ */
@media (min-width: 700px) {
  .about__grid  { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); }
  .contact__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); }
}

@media (min-width: 900px) {
  .nav__links  { display: flex; }
  .nav__toggle { display: none; }
  .nav__panel  { display: none !important; }
  .hero__grid  { grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); }
}

@media (min-width: 1180px) {
  .hero__grid { gap: 72px; }
}

/* aparelho de toque: o que só existe para o ponteiro sai do caminho, e o que
   pesa no scroll de celular fica mais barato. Consulta por capacidade, não por
   largura - um notebook estreito continua com o site inteiro. */
@media (hover: none) {
  /* a retícula acesa nunca acende sem ponteiro fino (módulo 9); sem isso ela
     fica de camada fixa em cima do fundo, repintando à toa */
  .grid-glow { display: none; }
  /* sem hover, o :active é a única resposta ao toque - e o realce do sistema
     por cima dele desmancha a pancada 1-bit */
  a, button, summary, label, [role='switch'] { -webkit-tap-highlight-color: transparent; }
  /* o alvo de toque do menu chega aos 48px do botão */
  .nav__panel a { padding-block: 17px; }
  /* o dedo mora onde o cursor passaria: o thumb entrega a textura ralada no
     toque, já que o :hover nunca vai chegar */
  .card:active .card__thumb { background-position: 6px 6px; }
  /* e a erosão acontece onde o dedo encostou, não no meio do card: o --cx/--cy
     é o mesmo par que o ponteiro escreve no módulo 12, e quem preenche no
     toque é o módulo 22. Sem isto o toque só deslocava a textura em bloco,
     que é resposta de card inteiro - não de onde a pessoa tocou. */
  .card:active .card__thumb::after { opacity: 1; }
}

/* ---------- 7b. PÁGINA DE CASE ------------------------------------------- */
/* Estava inline no projeto.html. Saiu de lá quando o segundo case nasceu:
   oito páginas carregando o mesmo bloco de estilo é o mesmo CSS oito vezes. */
.case__head { padding-block: clamp(28px, 4.5vw, 52px) clamp(24px, 4vw, 40px); }

/* A saída, no começo da leitura e não só no fim. Seis dos oito cases só tinham
   "anterior" e "próximo" no rodapé: quem abria um case pelo meio da corrente
   não tinha nenhum caminho de volta para a grade sem usar a barra do topo.

   Fica acima do kicker, alinhado com ele, na mesma mono em caixa alta: é
   navegação, não chamada para ação, e competir com o título seria pior que o
   problema que resolve. A seta desloca no hover em vez de acender, que é o
   vocabulário que o resto do site já usa. */
.case__voltar {
  /* `flex` e não `inline-flex`: o kicker logo abaixo é inline-block, e os dois
     inline dividiam a mesma linha ("‹ todos os projetos projetos/05"). O
     fit-content devolve a largura do texto, senão o sublinhado do hover
     atravessaria a coluna inteira. */
  display: flex;
  width: fit-content;
  align-items: center;
  gap: 8px;
  margin-bottom: 18px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 3px;
}
.case__voltar::before {
  content: '‹';
  font-size: 15px;
  transition: transform 140ms steps(3, end);
}
.case__voltar:hover { color: var(--ink); border-bottom-color: var(--ink); }
.case__voltar:hover::before { transform: translateX(-3px); }
@media (prefers-reduced-motion: reduce) {
  .case__voltar::before { transition: none; }
  .case__voltar:hover::before { transform: none; }
}
.case__title { margin-top: 16px; font-size: clamp(30px, 5.4vw, 56px); }
.case__lead { margin-top: 18px; }
.case__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
  gap: 0;
  border: 2px solid var(--ink);
  box-shadow: var(--shadow);
  margin-top: clamp(26px, 4vw, 38px);
}
.case__meta > div { padding: 16px 18px; border-bottom: 1px solid var(--rule-soft); }
.case__meta dt {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-bottom: 6px;
}
.case__meta dd { margin: 0; }
.case__shot { aspect-ratio: 16 / 9; border: 2px solid var(--ink); box-shadow: var(--shadow); }
/* `minmax(0, 1fr)` e não o `1fr` implícito, pelo mesmo motivo que o .about__grid
   e o .contact__grid já usam: item de grid nasce com `min-width: auto`, então
   ele cresce até caber o conteúdo em vez de encolher. Enquanto tudo aqui dentro
   era texto que quebra linha, isso nunca apareceu. O primeiro <pre> mudou o
   caso: com `white-space: pre` ele não quebra, empurrava o bloco para 646px numa
   tela de 360, e o `overflow-x: hidden` do body escondia a metade de fora em vez
   de deixar rolar. */
.case__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(30px, 4vw, 44px);
  margin-top: var(--bay);
}
.case__block h2 { margin-bottom: 14px; }

/* O índice do case. Nasce em JS (módulo 25) a partir dos próprios <h2>: são
   oito páginas com títulos diferentes, e escrever a lista à mão em cada uma
   seria copiar oito vezes uma coisa que o documento já diz - e que ficaria
   mentindo no dia em que alguém trocasse um título.

   Ele não marca onde você está, de propósito. A partir de 820px o corpo do
   case vira grade de duas colunas, então dois blocos dividem a mesma altura:
   no JVB, "O problema" e "A decisão" começam os dois em y=1492. Qualquer
   marcação de "seção atual" acertaria um e erraria o outro. Quem responde
   "quanto falta" é a barra em ASCII do topo, que é linear e não tem como
   errar; este aqui responde "o que tem aqui dentro". */
.sumario { margin-top: clamp(26px, 4vw, 38px); }
.sumario__lista {
  list-style: none;
  margin: 0;
  padding: clamp(12px, 2vw, 18px);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
  gap: 2px clamp(12px, 2vw, 24px);
  counter-reset: nenhum;
}
.sumario__lista a {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 7px 8px;
  margin-inline: -8px;
  text-decoration: none;
  color: var(--ink);
  text-wrap: pretty;
  transition: background-color 140ms steps(2, end);
}
.sumario__lista a:hover,
.sumario__lista a:focus-visible { background-color: var(--paper-2); }
/* o número é a mesma pastilha do índice do card: some no fundo até você
   passar por cima, e aí vira o negativo dele */
.sumario__n {
  flex: none;
  border: 2px solid var(--rule-soft);
  padding: 1px 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  transition: background-color 140ms steps(2, end), color 140ms steps(2, end),
              border-color 140ms steps(2, end);
}
.sumario__lista a:hover .sumario__n,
.sumario__lista a:focus-visible .sumario__n {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.case__gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: clamp(14px, 2vw, 20px);
  margin-top: clamp(26px, 4vw, 38px);
}
.case__gallery > div { aspect-ratio: 4 / 3; border: 2px solid var(--ink); }
/* sem isto a grade estica todo quadro até a altura do mais alto, e o que sobra
   embaixo dos menores é o dither de fundo do <figure> aparecendo por baixo da
   legenda - lê como falha de carregamento, não como textura */
.case__gallery > .shot { align-self: start; }
/* A saída do case. Eram dois botões pequenos jogados nos extremos de uma
   coluna de 1128px, com 625px de vazio no meio: não liam como um par, liam
   como dois restos. E o rodapé começa no pixel seguinte ao fim deles, então a
   sombra dura de 4px caía dentro do rodapé - no hover, que joga a sombra para
   6px, caía mais fundo ainda.

   Agora eles fecham a linha inteira, em partes iguais, com o rótulo encostado
   na borda de fora de cada ponta: quem volta puxa para a esquerda, quem avança
   empurra para a direita, e o par ocupa a largura que o texto do case ocupa.
   Mais altos porque numa metade de 554px o corpo de 13px sozinho boiava.

   `grid-auto-flow: column` e não uma contagem fixa: seis dos oito cases têm
   TRÊS links (anterior, todos, próximo) e dois têm dois. Numa grade de duas
   colunas os de três abririam exatamente o buraco dos cards de stack. Aqui a
   grade nasce do número de filhos e não sabe quantos são. */
.case__nav {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: clamp(12px, 1.6vw, 18px);
  border-top: 2px solid var(--ink);
  margin-top: var(--bay);
  padding-top: 26px;
  padding-bottom: clamp(34px, 5vw, 58px);
}
/* o do meio, quando existe, é o "todos os projetos": ele não aponta para lado
   nenhum, então fica centrado enquanto as pontas se encostam nas bordas */
.case__nav .btn { min-height: 64px; justify-content: center; text-align: center; }
.case__nav .btn:first-child { justify-content: flex-start; text-align: left; }
.case__nav .btn:last-child  { justify-content: flex-end;   text-align: right; }

/* No fim do case, avançar é a ação principal: quem chegou até aqui leu tudo, e
   o próximo case é o que a página tem a oferecer em seguida. A polaridade faz
   essa hierarquia sem precisar de mais nenhum desenho.
   `:not(.btn--ghost)` em vez de `:last-child` porque o HTML já marca qual é o
   forte, e nas oito páginas ele é sempre um só - no POV, que é o último da
   fila, o forte é o "todos os projetos", que ali é justamente o avançar. */
.case__nav .btn:not(.btn--ghost) {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

/* Rótulo partido em duas linhas dentro do botão é pior que pilha, então abaixo
   de uma certa largura o par vira pilha - e aí encostar nas bordas de fora não
   quer dizer mais nada, porque não há mais fora: todos voltam para o meio.

   1100px e não um número redondo qualquer: medido nas dezesseis páginas, o
   rótulo mais longo é "‹ Anterior: Seamless AI Dub" (e o "Previous" do inglês,
   que empata), e ele quebra quando a célula desce de 303px - o que acontece em
   1060px de janela com três links. Cem pixels de folga em cima disso para a
   fonte render um fio mais larga em outro sistema sem estourar a conta. */
@media (max-width: 1099px) {
  .case__nav {
    grid-auto-flow: row;
    grid-auto-columns: auto;
  }
  .case__nav .btn:first-child,
  .case__nav .btn:last-child { justify-content: center; text-align: center; }
}
/* números do case: a escala do sistema dita de uma vez, sem virar parágrafo */
/* O placar da home. É o mesmo .case__nums de dentro dos cases, só que aqui ele
   vem sozinho e precisa de respiro dos dois lados em vez do encaixe apertado
   que tem no meio de um bloco de texto. */
.placar { margin-block: clamp(30px, 5vw, 56px) clamp(10px, 2vw, 20px); }
.placar .case__nums { margin-top: 0; }

/* Só no placar da home as placas são link. Nos cases elas continuam sendo
   placa, porque lá o número já está na página que ele descreve e não teria
   para onde levar. */
.placar a.case__num {
  position: relative;
  display: block;
  overflow: hidden;
  color: inherit;
  text-decoration: none;
  transition: box-shadow 120ms steps(2, end), transform 120ms steps(2, end);
}
/* A inversão sobe do rodapé da placa, como marcador de placar enchendo. É outro
   gesto que o das ferramentas de trabalho, onde a retícula varre de lado: dois
   blocos com o mesmo movimento leriam como o mesmo componente. */
.placar a.case__num::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--ink);
  clip-path: inset(100% 0 0 0);
  transition: clip-path 260ms steps(5, end);
}
.placar a.case__num > * { position: relative; }
.placar a.case__num::after {
  content: '›';
  position: absolute;
  top: 12px;
  right: 15px;
  font-family: var(--font-mono);
  font-size: 17px;
  line-height: 1;
  color: var(--ink-2);
  transition: transform 140ms steps(3, end);
}
.placar a.case__num:hover::before,
.placar a.case__num:focus-visible::before { clip-path: inset(0 0 0 0); }
/* A placa desce para dentro da própria sombra: ela anda os 4px do deslocamento
   e a sombra some no mesmo passo. Crescer a sombra era o caminho óbvio e ficava
   errado, porque ela é da cor da tinta e a placa invertida também: as duas
   viravam uma mancha só, como se o card estivesse vazando. */
.placar a.case__num:hover,
.placar a.case__num:focus-visible {
  box-shadow: 0 0 0 var(--ink);
  transform: translate(4px, 4px);
}
/* o texto vira papel junto com o fundo virando tinta: a placa troca de
   polaridade inteira, sem passar por nenhum estado de baixo contraste */
.placar a.case__num:hover b, .placar a.case__num:focus-visible b,
.placar a.case__num:hover span, .placar a.case__num:focus-visible span,
.placar a.case__num:hover::after, .placar a.case__num:focus-visible::after {
  color: var(--paper);
}
.placar a.case__num:hover::after,
.placar a.case__num:focus-visible::after { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) {
  .placar a.case__num:hover::after,
  .placar a.case__num:focus-visible::after { transform: none; }
  /* sem o passo, o afundar viraria um salto seco de 4px; a inversão sozinha já
     dá o retorno, então aqui a placa fica parada e só troca de polaridade */
  .placar a.case__num:hover,
  .placar a.case__num:focus-visible { transform: none; box-shadow: var(--shadow); }
}

.case__nums {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
  gap: clamp(14px, 2vw, 18px);
  margin-top: clamp(22px, 3vw, 30px);
}
/* Três placas numa grade de duas colunas deixam a terceira sozinha, com um
   vazio do tamanho de uma placa ao lado dela - e vazio em grade lê como item
   que faltou, não como fim da lista. É o mesmo defeito que a grade de projetos
   já resolve com `:last-child:nth-child(odd)`, e a solução é a mesma: a última
   atravessa a linha inteira.

   Medido: as duas grades (o placar da home e as dos cases) passam a três
   colunas em 560px e ficam com duas abaixo disso, então a regra vale só nessa
   faixa. Com quatro placas nada acontece, porque aí a última é par e a grade
   fecha 2+2 sozinha. */
@media (max-width: 559px) {
  .case__nums > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
/* O `background` faltava desde sempre, e é o defeito mais visível da página:
   a placa tinha borda, sombra e nada dentro, então a retícula do fundo e a
   faixa que dissolve a seção passavam por dentro dela. Sombra dura sobre
   superfície vazada é contradição - a sombra promete um objeto pousado no
   papel, e o vazado mostra que não tem objeto nenhum. */
.case__num {
  position: relative;
  border: 2px solid var(--ink);
  background: var(--paper);
  padding: 16px 18px;
  box-shadow: var(--shadow);
}

/* As placas de dentro do case não são link e não vão virar um: no case o
   número já está na página que ele descreve e não teria para onde levar.
   Mas superfície que não responde a nada lê como imagem colada, não como
   parte da página - e essas são quatro caixas grandes, no meio da leitura.
   Então elas fazem o que os cards da stack fazem, que é a resposta reservada
   ao que não é clicável: a retícula sobe por dentro e o rótulo ganha tinta
   cheia. Nada levanta, nada ganha sombra, nada se move. Card que se comporta
   como botão sem ser um é mentira de interface.

   `:not(a)` e não `.case__block .case__num`: a condição de verdade é não ser
   link, e escrita assim ela continua valendo no dia em que uma placa dessas
   aparecer em outro lugar da página. */
.case__num:not(a)::before {
  content: '';
  position: absolute;
  inset: 0;
  /* a chapa junto com os pontos, como no card de stack: só a retícula em
     `--rule-soft` sobre `--paper` é diferença de dois por cento de luminância
     no tema escuro, e o realce não chegava a acontecer. Quem faz a superfície
     mudar é o `--paper-2`; a retícula é o que diz que a mudança é desta casa */
  background-color: var(--paper-2);
  background-image: radial-gradient(var(--rule-soft) 0.8px, transparent 0.9px);
  background-size: 9px 9px;
  clip-path: inset(100% 0 0 0);
  transition: clip-path 320ms steps(6, end);
}
.case__num:not(a) > * { position: relative; }
.case__num:not(a):hover::before { clip-path: inset(0 0 0 0); }
.case__num:not(a):hover span { color: var(--ink); }
.case__num span { transition: color 200ms steps(3, end); }
.case__num b {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(26px, 3.4vw, 38px);
  line-height: 1;
  font-weight: 400;
  /* enquanto sobe, o número troca de dígito a cada quadro: sem largura fixa de
     algarismo ele tremeria de lado a lado até parar */
  font-variant-numeric: tabular-nums;
}
.case__num span {
  display: block;
  margin-top: 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
@media (min-width: 820px) {
  .case__meta > div { border-bottom: 0; border-right: 1px solid var(--rule-soft); }
  .case__meta > div:last-child { border-right: 0; }
  .case__body { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .case__block--wide { grid-column: 1 / -1; }
}

/* ---------- 7c. MÍDIA DE CASE (print, gif e captura de tela) ------------- */
/* O slot existe antes do arquivo. Vazio, ele é a retícula e reserva o buraco em
   16:9 - a página não pula de layout no dia em que a captura entrar. Com mídia
   dentro, a altura passa a ser a do arquivo e a retícula fica escondida atrás.
   Publicar uma captura é acrescentar UMA linha; nada de trocar classe. */
.shot {
  position: relative;
  margin: 0;
  border: 2px solid var(--ink);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.shot:not(:has(img, video)) { aspect-ratio: 16 / 9; }
.shot--tall:not(:has(img, video)) { aspect-ratio: 4 / 3; }

/* descendente, não filho direto: as capturas moram dentro de um <picture> para
   servir WebP a quem aceita e PNG a quem não aceita, e o `>` parava de alcançar
   a <img> no dia em que o <picture> entrou no meio. */
.shot img,
.shot video {
  display: block;
  width: 100%;
  height: auto;
  /* captura de tela é colorida e o sistema aqui é de 1 bit: a página mostra o
     print dessaturado, e devolve a cor real no hover/foco. Prova que o produto
     existe sem que a paleta dele brigue com a do portfólio. */
  filter: grayscale(1) contrast(1.06);
  transition: filter 0.45s ease;
}
.shot:hover img,
.shot:hover video,
.shot:focus-within img,
.shot:focus-within video { filter: none; }

.shot figcaption {
  border-top: 2px solid var(--ink);
  background: var(--paper-2);
  padding: 10px 14px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  justify-content: space-between;
}
/* a legenda diz o que olhar; a dica de cor só faz sentido onde existe hover */
.shot__cor { display: none; }
@media (hover: hover) {
  .shot:has(img, video) .shot__cor { display: inline; }
}

/* ---------- 7d. A CAPTURA EM TAMANHO DE VERDADE -------------------------- */
/* Seis dos oito cases têm exatamente uma captura, e ela mora numa coluna de
   42% de largura onde nenhum texto de interface se lê. Clicar numa imagem pra
   ver grande é o gesto mais automático que existe, e até aqui não acontecia
   nada. O botão nasce em JS (módulo 24): sem JS não existe ampliação, e um
   botão morto no HTML seria pior que a falta dele.

   Só imagem. Dois dos três vídeos são laço mudo sem controle e o terceiro tem
   controle nativo - uma camada de clique por cima deles roubaria o play. */
.shot__lupa {
  position: absolute;
  inset: 0;
  z-index: 0;
  border: 0;
  padding: 0;
  background: transparent;
}
/* a legenda fica acima da camada de clique pra continuar selecionável: ela é
   texto que descreve a imagem, e texto que não dá pra copiar irrita */
.shot figcaption { position: relative; z-index: 1; }

/* o distintivo: dois cantos de moldura, que é o desenho de "abre maior" sem
   nenhuma curva. Lupa de verdade tem círculo e cabo, e círculo é justamente o
   que não existe em lugar nenhum deste site. */
.shot__lupa-glifo {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 26px;
  height: 26px;
  border: 2px solid var(--ink);
  background: var(--paper);
  opacity: 0;
  transition: opacity 140ms steps(2, end), transform 140ms steps(2, end);
}
.shot__lupa-glifo::before,
.shot__lupa-glifo::after {
  content: '';
  position: absolute;
  width: 7px;
  height: 7px;
}
.shot__lupa-glifo::before {
  top: 4px;
  left: 4px;
  border-top: 2px solid var(--ink);
  border-left: 2px solid var(--ink);
}
.shot__lupa-glifo::after {
  bottom: 4px;
  right: 4px;
  border-bottom: 2px solid var(--ink);
  border-right: 2px solid var(--ink);
}
.shot:hover .shot__lupa-glifo,
.shot__lupa:focus-visible .shot__lupa-glifo { opacity: 1; }
.shot__lupa:active .shot__lupa-glifo { transform: translate(1px, 1px); }
/* sem ponteiro não existe hover pra revelar o distintivo, e uma imagem que
   responde ao toque sem dizer que responde é a mesma armadilha do retrato */
@media (hover: none) {
  .shot__lupa-glifo { opacity: 1; }
}

/* O diálogo é nativo: `showModal()` entrega Escape, foco preso e resto da
   página inerte sem uma linha disso escrita aqui.

   O véu mora no próprio <dialog>, e não no ::backdrop, de propósito: o
   ::backdrop só passou a herdar do elemento de origem faz pouco tempo, e sem
   herança ele não enxerga nenhum token de tema. O elemento em si é filho do
   body na árvore, então var() resolve nele como resolve em qualquer outro. */
/* `inset: 0` e não `width: 100dvw`. O <dialog> já nasce `position: fixed`, e
   as unidades de viewport contam a barra de rolagem junto: com 100dvw o véu
   ficava mais largo que a área útil e a página inteira ganhava rolagem
   horizontal enquanto a lupa estava aberta. Medido. */
.lupa {
  inset: 0;
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  border: 0;
  /* o <dialog> nasce com `overflow: auto` no estilo do navegador. Com o véu
     do tamanho da tela isso rendia uma barra de rolagem por 6px de sobra, e
     como o elemento cobre o viewport a barra aparecia colada no rodapé da
     tela, como se a página tivesse quebrado. Quem controla a própria altura é
     o quadro aqui dentro; o véu não tem o que rolar. */
  overflow: hidden;
  padding: clamp(12px, 3vw, 40px);
  color: var(--ink);
  background-color: var(--paper);   /* fallback pra quem não tem color-mix */
  background-color: color-mix(in srgb, var(--paper) 93%, transparent);
  background-image: radial-gradient(var(--tex) 0.8px, transparent 0.9px);
  background-size: 10px 10px;
}
/* `minmax(0, 1fr)` e não a faixa automática: numa faixa `auto` a largura sai do
   max-content do conteúdo, e o conteúdo aqui é uma barra de painel em `nowrap`.
   Num celular de 390px ela pedia 413, a faixa obedecia, e o quadro inteiro
   ficava mais largo que a tela - o botão de fechar e o fim da legenda saíam
   pela direita, sem barra de rolagem nenhuma dizendo isso, porque o véu é
   `overflow: hidden`. Medido depois que o botão 1:1 entrou e empurrou a conta
   para fora; antes dele passava raspando, o que é a mesma falha esperando. */
.lupa[open] { display: grid; grid-template-columns: minmax(0, 1fr); place-items: center; }
.lupa::backdrop { background: transparent; }

/* e a barra pode quebrar em duas linhas em vez de empurrar: são seis coisas
   nela, e num aparelho estreito seis não cabem em fila. O nome do arquivo é o
   que cede primeiro - ele é o rótulo, não o controle. */
.lupa .panel__bar { flex-wrap: wrap; }
.lupa .panel__title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 100% e max-* do próprio pai pelo mesmo motivo: o padding do <dialog> já
   reservou a margem, e vw/vh aqui somariam a barra de rolagem de novo */
.lupa__quadro {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 1500px;
  max-height: 100%;
  min-height: 0;
}
/* a barra do painel, com o nome do arquivo à esquerda e o contador à direita -
   igual à do retrato.bmp e à do bloco de código */
.lupa__fechar {
  background: var(--paper);
  border: 2px solid var(--ink);
  color: var(--ink);
  padding: 3px 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  box-shadow: 2px 2px 0 var(--ink);
  transition: transform 110ms steps(2, end), box-shadow 110ms steps(2, end);
}
.lupa__fechar:hover  { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--ink); }
.lupa__fechar:active { transform: translate(2px, 2px);   box-shadow: 0 0 0 var(--ink); }

/* O botão 1:1. Ele só aparece onde a captura chegou encolhida a ponto de a
   interface dentro dela parar de se ler - quem decide é o módulo 24, pela
   razão entre o tamanho na tela e o natural, então na maioria dos desktops
   ele nunca é visto. Pressionado, inverte a polaridade como qualquer outro
   estado ligado deste site. */
.lupa__perto {
  background: var(--paper);
  border: 2px solid var(--ink);
  color: var(--ink);
  padding: 3px 9px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  box-shadow: 2px 2px 0 var(--ink);
  transition: transform 110ms steps(2, end), box-shadow 110ms steps(2, end);
}
.lupa__perto:hover  { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--ink); }
.lupa__perto:active { transform: translate(2px, 2px);   box-shadow: 0 0 0 var(--ink); }
.lupa__perto[aria-pressed='true'] { background: var(--ink); color: var(--paper); }

/* A vista aproximada: o palco vira janela e a imagem volta ao tamanho natural.
   `place-content: start` é obrigatório e não é estética - item de grade maior
   que a área centralizado tem o próprio topo e a própria esquerda fora do
   alcance da rolagem, e metade da captura fica inatingível. */
.lupa--pode-perto .lupa__palco { cursor: zoom-in; }
.lupa--perto .lupa__palco {
  overflow: auto;
  overscroll-behavior: contain;
  place-content: start;
  cursor: zoom-out;
}
.lupa--perto .lupa__img {
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  object-fit: none;
}

/* a dica só existe enquanto o botão existe, e some junto */
.lupa__dica { color: var(--ink-2); }
.lupa__dica::before { content: ' · '; }

/* `flex: 1` mais `min-height: 0` é o que dá altura definida ao palco. Sem
   isso ele mede o próprio conteúdo, o `max-height: 100%` da imagem cai numa
   referência circular, o browser desiste da porcentagem e a captura renderiza
   no tamanho natural - medi 812px de imagem dentro de 733px de palco, cortada
   embaixo sem nenhuma barra de rolagem dizendo isso. */
.lupa__palco {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  overflow: hidden;
  background: var(--paper-2);
  padding: clamp(8px, 1.6vw, 16px);
}
/* o <picture> é que seria o item da grade, e a <img> mediria contra ele em vez
   de contra o palco - a mesma referência circular um nível abaixo. Com
   `contents` o wrapper some do layout e quem se posiciona é a imagem. */
.lupa__palco picture { display: contents; }
/* Aqui a captura vem em cor. Na página ela é dessaturada porque a paleta do
   produto brigaria com a do portfólio; quem ampliou está justamente pedindo
   pra ver o produto, então a regra se desfaz onde deixou de fazer sentido. */
/* A imagem ocupa a célula inteira e o `object-fit` cuida de encaixar os pixels
   dentro dela. Tentei antes o caminho óbvio - imagem no tamanho natural,
   centralizada, com `max-height: 100%` segurando - e ele não funciona: a altura
   do palco vem do flex e a porcentagem do filho não resolve contra ela, então o
   browser descarta o limite e a captura sai cortada embaixo, sem barra de
   rolagem nenhuma denunciando. Aqui quem decide o tamanho é a célula, que tem
   medida de verdade, e a imagem só se acomoda dentro. */
.lupa__img {
  filter: none;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: contain;
}
.lupa__cap {
  border-top: 2px solid var(--ink);
  background: var(--paper);
  padding: 10px 14px;
  color: var(--ink-2);
  text-wrap: pretty;
}
.lupa__cap:empty { display: none; }

/* Andar entre as capturas do mesmo case sem fechar e reabrir. Só aparece onde
   existe mais de uma - no JVB são cinco, nos outros é uma só e um par de setas
   desligadas seria promessa falsa. */
.lupa__setas {
  display: flex;
  gap: 10px;
  margin-left: auto;
}
.lupa__seta {
  background: var(--paper);
  border: 2px solid var(--ink);
  color: var(--ink);
  width: 28px;
  height: 24px;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1;
  transition: transform 110ms steps(2, end);
}
.lupa__seta:hover { transform: translate(-1px, -1px); }
.lupa__seta:active { transform: translate(1px, 1px); }

/* a entrada: o quadro cresce em passos, como tudo aqui. A saída é a mesma
   coisa ao contrário, e existe uma classe pra ela porque animar a saída de um
   <dialog> em CSS puro exige `allow-discrete`, que ainda não está em todo
   lugar - o JS segura o close por 180ms e pronto. */
@keyframes lupa-entra {
  from { transform: scale(0.88); opacity: 0; }
  to   { transform: scale(1); opacity: 1; }
}
.lupa[open] .lupa__quadro { animation: lupa-entra 200ms steps(4, end) 1; }
.lupa__quadro.saindo { animation: lupa-entra 180ms steps(4, end) 1 reverse; }

/* ---------- 8. ACESSIBILIDADE / MOTION ----------------------------------- */
:where(a, button, input, textarea, [tabindex]):focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

/* O sistema tem duas preferências de acessibilidade que o navegador entrega de
   graça, e o site só atendia uma. Quem liga "aumentar contraste" no Windows ou
   no macOS está dizendo que o cinza do meio não serve: o --ink-2 existe só pra
   baixar hierarquia, e aqui ele deixa de existir. A régua fina vira a régua
   cheia pelo mesmo motivo.

   Redeclarado nos quatro escopos, e não só no :root, porque var() dentro de
   custom property resolve no elemento que declara - a mesma armadilha do
   comentário lá do topo. Se isto morasse só no :root, o painel invertido
   herdaria a tinta da página em vez da dele e o texto sumiria dentro dele. */
@media (prefers-contrast: more) {
  :root,
  :root[data-theme='light'],
  .invert, .guia--inv,
  :root[data-theme='light'] .invert,
  :root[data-theme='light'] .guia--inv {
    --ink-2: var(--ink);
    --rule-soft: var(--rule);
  }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
    /* sem zerar o atraso, a escada do --i deixaria elemento invisível por meio
       segundo mesmo com a animação já cortada */
    animation-delay: 0.001ms !important;
    transition-delay: 0.001ms !important;
  }
  .reveal { opacity: 1; transform: none; }
}

@media print {
  @page { margin: 14mm; }

  /* No papel a polaridade inverte: tinta escura sobre branco. E aqui vale a
     mesma regra dos quatro escopos do resto do arquivo, que faltava justo
     onde ninguém testa: `.invert` declara os próprios tokens, então zerar só
     o `:root` deixava a seção invertida com a tinta dela. Imprimindo do modo
     claro isso era texto #eee8d5 sobre papel branco - três seções inteiras
     saíam em branco na folha, e não tem como perceber olhando a tela. */
  :root,
  :root[data-theme='light'],
  .invert,
  .guia--inv,
  :root[data-theme='light'] .invert,
  :root[data-theme='light'] .guia--inv {
    --paper: #fff; --paper-2: #f0f0ee; --ink: #111; --ink-2: #555;
    --rule: #111; --rule-soft: #bbb; --accent: #111;
    --pop-1: #111; --pop-2: #111; --pop-3: #111;
    --tex: rgba(0, 0, 0, 0.08); --tex-hi: rgba(0, 0, 0, 0.08);
    --shadow: none;
  }

  /* O que só existe na tela sai da folha: navegação, guia lateral, cortina,
     rastro do clique, seletor de idioma, lupa e a barra de rolagem em ASCII.
     Junto vão as duas retículas decorativas - a faixa que dissolve a virada de
     polaridade e a rampa - porque no papel elas viram só toner gasto numa
     borda que a folha já separa sozinha. */
  .nav, .nav__panel, .hero__figure, .grid-glow, .wipe, .guia, .click-fx,
  .lang, .lupa, .shot__lupa, .ramp, [data-progresso], .orb { display: none !important; }
  .sec.invert::before, .sec.invert::after { display: none; }

  body { background: #fff; background-image: none; }
  /* o corpo do masthead vem do JS, medido em pixel de tela - no papel não serve */
  .masthead { max-width: none !important; }
  .masthead__line { font-size: 38px !important; margin-right: 0; }
  .card, .panel, .detail__hero, .case__meta, .case__shot { box-shadow: none; }
  .invert { background: #f0f0ee; }

  /* `content-visibility: auto` é economia de pintura em tela. Na impressão o
     documento inteiro é uma página só, e vale ser explícito. */
  .sec { content-visibility: visible; }

  /* Nada de bloco partido no meio pela quebra de página. */
  .card, .skill, .tl__item, .case__block, .field, .spec-row { break-inside: avoid; }
  .sec__head, .h2, .h3 { break-after: avoid; }

  /* Link no papel não clica. O endereço vira texto ao lado - só o externo e o
     do próprio site, porque âncora interna (#projetos) impressa não leva a
     lugar nenhum e só suja a linha. */
  main a[href^="http"]::after,
  main a[href^="mailto:"]::after {
    content: ' <' attr(href) '>';
    font-family: var(--font-mono);
    font-size: 0.82em;
    word-break: break-all;
  }
  main a[href^="http"].btn::after,
  main a[href^="http"] .tag::after { content: none; }
}
