/* NextWell Lab — home page.
   Reskinned onto the section shell language (shared/viewer.css): near-black
   shell, one fg alpha ladder, rounded "window" surfaces with faint 1px borders,
   rounded uppercase eyebrows, 36px icon-button chrome. */

/* Unbounded 600 — the display face of the gallery welcome slides, reused here
   for the brand marks and the CTA labels so the landing and the sections speak
   with the same voice. gf.css does not carry it (only the interactive section's
   sheet does), so the three cuts are declared locally; the paths are
   root-absolute because this page is served from the site root and the same
   files back the section covers. Split by unicode-range exactly as upstream
   ships it, so a latin visitor never downloads the cyrillic cut. There is no
   CJK cut in this family — zh/ja/ko fall through to the system stack rather
   than to a synthesised bold. */
@font-face {
  font-family: 'Unbounded';
  font-style: normal; font-weight: 600; font-display: swap;
  src: url('/shared/fonts/unbounded/unbounded-latin-600-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Unbounded';
  font-style: normal; font-weight: 600; font-display: swap;
  src: url('/shared/fonts/unbounded/unbounded-latin-ext-600-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Unbounded';
  font-style: normal; font-weight: 600; font-display: swap;
  src: url('/shared/fonts/unbounded/unbounded-cyrillic-600-normal.woff2') format('woff2');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* Nunito — the label voice: every small uppercase eyebrow, tile caption, tag
   and counter on the page. It replaces JetBrains Mono, which was a typewriter
   in a skin that has nothing else typed about it. Round, humanist, generously
   curved, and — unlike the usual "Gilroy-ish" free stand-ins (Manrope ships one
   weight here, Poppins has no cyrillic at all) — it carries latin, latin-ext
   AND a real cyrillic cut in two weights, so ru/uk/pl labels are the same face
   rather than a system substitute. Nothing here is monospaced any more, so
   wherever digits have to line up the rule asks for tabular-nums explicitly.
   Declared exactly like Unbounded above: root-absolute paths (this page is
   served from the site root), split by unicode-range as upstream ships it, so a
   latin visitor never downloads the cyrillic cut. Both cuts are declared and
   only the weights a rule actually asks for get fetched. No CJK cut exists —
   zh/ja/ko fall through to the system stack. */
@font-face {
  font-family: 'Nunito';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/shared/fonts/nunito/nunito-latin-400-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Nunito';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/shared/fonts/nunito/nunito-latin-ext-400-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Nunito';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('/shared/fonts/nunito/nunito-cyrillic-400-normal.woff2') format('woff2');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
  font-family: 'Nunito';
  font-style: normal; font-weight: 700; font-display: swap;
  src: url('/shared/fonts/nunito/nunito-latin-700-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Nunito';
  font-style: normal; font-weight: 700; font-display: swap;
  src: url('/shared/fonts/nunito/nunito-latin-ext-700-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Nunito';
  font-style: normal; font-weight: 700; font-display: swap;
  src: url('/shared/fonts/nunito/nunito-cyrillic-700-normal.woff2') format('woff2');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

/* The accent (--acid & friends) is written inline on <html> by the head script
   (random palette entry per load) and rotated by main.js as PLAIN custom
   properties. Native animation/transition of registered @property colours on
   the root is what used to power this — it left descendants holding stale
   paint, forced a full-document style recalc every frame and leaked memory in
   Chromium, so it is gone for good. */

:root {
  --bg: #0a0b10;
  --fg: #f4f2ff;
  --fg-a05: rgba(244, 242, 255, 0.05);
  --fg-a08: rgba(244, 242, 255, 0.08);
  --fg-a12: rgba(244, 242, 255, 0.12);
  --fg-a25: rgba(244, 242, 255, 0.25);
  --fg-a40: rgba(244, 242, 255, 0.4);
  --fg-a55: rgba(244, 242, 255, 0.55);
  --fg-a75: rgba(244, 242, 255, 0.75);
  /* Slightly lifted, slightly translucent — the aurora tints through it. */
  --card: rgba(17, 18, 27, 0.62);
  --card-solid: #111218;
  --acid: #c8ff2e;       /* overridden per load by inline head script */
  --acid-dark: #9ac700;
  --acid-ink: #0b0b10;
  /* One chevron for the whole page — a bare corner, no shaft. Masked into
     pseudo-elements (::after marks cannot live in the markup: the i18n loader
     rewrites innerHTML on every locale switch) and inlined as <svg> where the
     markup is i18n-free. Rotated -45deg it reads as an "opens elsewhere" mark. */
  --chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 6l6 6-6 6'/%3E%3C/svg%3E");
  /* And the same idea for "opens elsewhere": the top-right corner of a frame. */
  --corner: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 6h10v10'/%3E%3C/svg%3E");
  --radius: 16px;
  --shadow-raise: 0 30px 70px -30px rgba(0, 0, 0, 0.7), 0 2px 12px -6px rgba(0, 0, 0, 0.45);
  --shadow-hover: 0 18px 44px -20px rgba(0, 0, 0, 0.65);
  --display: 'Science Gothic', 'Space Grotesk', 'Inter', system-ui, sans-serif;
  /* Brand voice: the marks and the calls to action only. Unbounded is wide and
     loud — anywhere it touches, size and tracking are retuned for it. */
  --brand: 'Unbounded', 'Space Grotesk', 'Inter', system-ui, sans-serif;
  /* The reading voice. Inter is a fine interface face and a characterless one;
     Nunito puts a circle back into every o, a and e, which is the one thing the
     page's own drawing (rounded plates, pill buttons, circular status dots) was
     asking the copy to echo. It also carries latin, latin-ext and cyrillic in
     both weights, so no locale drops out of the family — and it is already on
     the page as the label face, so the switch costs no extra request. */
  --text: 'Nunito', 'Nunito Sans', 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  /* Header chrome only: the nav pills and the language toggle. A compass-drawn
     geometric with genuinely circular bowls — loud enough to carry three words
     at 12px, far too round to read a paragraph in. */
  /* The label role — tracked uppercase micro-type. Nunito is proportional, so
     every slot that used to lean on a monospace advance for its rhythm was
     re-measured for it: sizes up about half a pixel, tracking down a notch, and
     700 rather than 500 (the family ships 400/700, and 500 would have resolved
     down to 400 and gone thin at 9–11px). */
  --label: 'Nunito', 'Nunito Sans', 'Inter', system-ui, sans-serif;
  --max-w: 1160px;
  --head-h: 64px;
  --ease-out: cubic-bezier(.2, .8, .2, 1);
  color-scheme: dark;
}

html { scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--text);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: 1.5;
  overflow-x: hidden;
  /* Kills the 300ms double-tap-to-zoom delay on touch without taking panning or
     scrolling away — the pinch-zoom lock itself is the viewport meta. */
  touch-action: manipulation;
}

a { color: inherit; text-decoration: none; }
::selection { background: var(--acid); color: var(--acid-ink); }
a:focus-visible, button:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--acid) 70%, #fff);
  outline-offset: 2px;
}

/* The class name is the page's label hook, not a claim about the face — it is
   kept because the markup carries it and the section galleries use the same
   name. */
.mono {
  font-family: var(--label);
  font-size: 11px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--fg-a40);
}

/* cursor spotlight — a fixed-size halo moved with a transform, so following
   the pointer costs a composite instead of a full-viewport gradient repaint */
.spotlight {
  position: fixed; left: -560px; top: -560px;
  width: 1120px; height: 1120px;
  pointer-events: none;
  z-index: 1;
  background: radial-gradient(circle closest-side,
              color-mix(in srgb, var(--acid) 6%, transparent), transparent 65%);
  transform: translate3d(var(--mx, 50vw), var(--my, 30vh), 0);
  transition: transform .12s linear;
  will-change: transform;
}

/* reveal on scroll */
.reveal { opacity: 0; transform: translateY(22px); transition: opacity .7s var(--ease-out), transform .7s var(--ease-out); }
.reveal.in { opacity: 1; transform: none; }

/* Anchor jumps must clear the sticky header. */
#featured, #sections { scroll-margin-top: calc(var(--head-h) + 12px); }

/* ===== HEADER ===== */
.site-head {
  position: sticky; top: 0; z-index: 90;
  display: flex; align-items: center; gap: 10px;
  padding: 0 clamp(18px, 4vw, 48px);
  height: var(--head-h);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  border-bottom: 1px solid var(--fg-a05);
}
.wordmark {
  display: flex; align-items: center; gap: 10px;
  margin-right: auto;
  /* Unbounded runs about a fifth wider than Inter at the same size: at the old
     14.5px "NextWell Lab" pushed the language pill and the burger off a 360px
     screen. Fluid size plus a touch of negative tracking claws that back, and
     the box is pinned so a flex squeeze can never dent it. */
  font-family: var(--brand);
  font-size: clamp(11.5px, 3.1vw, 13px);
  font-weight: 600; letter-spacing: -.02em;
  white-space: nowrap;
}
/* The tilt is the mark's signature — it came from the experimental skin and it
   belongs to the logo, not to a stylesheet. Retuned for this plate: the old one
   was a hard-cornered 30px square, this one is 32px and rounded, and a rounded
   square shows its slant on the corners rather than along the edges, so -4deg
   carries further here than it did there — enough that the angle needs no help,
   and the springy overshoot does the rest of the work on hover. The
   rotation costs nothing in layout (a 4deg tilt grows the painted box by ~1px a
   side against 16px of header air) and the glitch copies inherit it for free —
   they are absolutely positioned inside this box, so they are already in its
   rotated coordinate space and their own translate/clip stays square to the
   plate at any angle. */
.wordmark-box {
  position: relative; /* the glitch copies pin to it */
  display: grid; place-items: center;
  width: 32px; height: 32px; flex: 0 0 32px;
  border-radius: 9px;
  background: var(--acid); color: var(--acid-ink);
  font-family: var(--brand); font-size: 10.5px; font-weight: 600; letter-spacing: -.03em;
  /* The plate centres its two letters itself (display: grid + place-items), so
     the inherited 1.5 line box only ever pushed the glyphs off-centre. */
  line-height: 1;
  box-shadow: 0 4px 18px -6px color-mix(in srgb, var(--acid) 70%, transparent);
  transform: rotate(-4deg);
  transition: transform .25s cubic-bezier(.3, 1.6, .4, 1);
}
.wordmark-text { position: relative; }
/* Through the upright and out the other side — the spring in the easing is what
   makes it read as a flick rather than a slide. */
.wordmark:hover .wordmark-box { transform: rotate(4deg) scale(1.08); }
.wordmark em { font-style: normal; color: var(--acid); }

/* ===== wordmark glitch =====
   The old skin tore the hero heading; the new heading is a background-clipped
   gradient, which a duplicated text layer cannot share, so the effect moved to
   the mark it fits better anyway. Same character, a quarter of the scale.

   Four absolutely positioned copies read `data-text` — the wordmark is
   i18n-free markup, so nothing in main.js ever rewrites them. Every copy is
   out of flow and only ever moves by transform/clip-path, so a tear cannot
   shift the header by a pixel.

   ::before is the fill (the accent poured into the letterforms / the plate
   duplicated); ::after is the chromatic ghost, tinted from --goo3 — a rotating
   palette slot, so no hue is named here either.

   Nothing runs continuously. A tear takes a single band — a fifth of the
   element's height — throws it two or three pixels sideways and puts it back
   inside ~60ms. The four layers share one clock but are staggered a few
   milliseconds apart, so the burst reads as one compound dropout every ~12s
   rather than as four separate tics. */
.wordmark-box::before, .wordmark-box::after,
.wordmark-text::before, .wordmark-text::after {
  content: attr(data-text);
  position: absolute; inset: 0;
  opacity: 0;
  pointer-events: none;
}
/* --o is the per-layer offset inside the burst; it is added to whichever
   animation-delay the ambient or the hover rule below sets. */
.wordmark-text::before { --o: 20ms; }
.wordmark-box::after { --o: 40ms; }
.wordmark-text::after { --o: 60ms; }
/* The plate copies carry the plate; the word copies carry only ink. */
.wordmark-box::before, .wordmark-box::after {
  display: grid; place-items: center;
  border-radius: inherit;
}
.wordmark-box::before { background: var(--acid); color: var(--acid-ink); }
.wordmark-box::after {
  background: color-mix(in srgb, var(--goo3, var(--acid)) 72%, transparent);
  color: var(--acid-ink);
}
.wordmark-text::before { color: var(--acid); }
/* Combed into slivers, so the chromatic layer can never show as a whole second
   copy of the word — only a few scanlines of it ever light up. */
.wordmark-text::after {
  color: var(--goo3, var(--acid));
  -webkit-mask-image: repeating-linear-gradient(180deg, #000 0 1px, transparent 1px 4px);
          mask-image: repeating-linear-gradient(180deg, #000 0 1px, transparent 1px 4px);
}

/* Ambient tearing is the header mark's alone — one logo doing this is a detail,
   two on the same page is a tic. The 6s lead-in also keeps the page from
   glitching in the visitor's first glance. */
.site-head .wordmark-box::before,
.site-head .wordmark-box::after,
.site-head .wordmark-text::before,
.site-head .wordmark-text::after {
  animation: nw-tear 12.4s steps(1, end) calc(6s + var(--o, 0ms)) infinite;
}
/* Hover/focus swaps the animation *name*, which is the only thing that restarts
   a CSS animation — so the mark tears the instant the pointer lands on it
   instead of waiting out the ambient clock. Leaving restarts the ambient one,
   lead-in and all, so waving across the logo stays quiet. */
.wordmark:hover .wordmark-box::before,
.wordmark:hover .wordmark-box::after,
.wordmark:hover .wordmark-text::before,
.wordmark:hover .wordmark-text::after,
.wordmark:focus-visible .wordmark-box::before,
.wordmark:focus-visible .wordmark-box::after,
.wordmark:focus-visible .wordmark-text::before,
.wordmark:focus-visible .wordmark-text::after {
  animation: nw-tear-hi 2.8s steps(1, end) var(--o, 0ms) 1;
}

/* steps(1, end): every frame below holds until the next one, so these values
   are the tears themselves rather than something to interpolate through.
   At 12.4s the three bands land 56ms / 118ms / 180ms in and are gone by 230ms.
   Every band is kept inside 20%–80% of the box: at a 19.5px line height the
   glyphs only occupy the middle ~55%, and a band outside that tears empty air. */
@keyframes nw-tear {
  0%, 100% { opacity: 0; }
  0.45% { opacity: 1; transform: translate3d(-3px, 0, 0); clip-path: inset(24% 0 58% 0); }
  0.95% { opacity: 1; transform: translate3d(3px, 1px, 0); clip-path: inset(52% 0 28% 0); }
  1.45% { opacity: 1; transform: translate3d(-2px, 0, 0); clip-path: inset(36% 0 46% 0); }
  1.85% { opacity: 0; }
}
/* On hover: a 170ms triple tear on arrival, one lone band at ~1.5s, quiet after
   that — enough to answer the pointer, not enough to strobe if it rests there. */
@keyframes nw-tear-hi {
  0%   { opacity: 1; transform: translate3d(-4px, 0, 0);   clip-path: inset(20% 0 60% 0); }
  2%   { opacity: 1; transform: translate3d(4px, -1px, 0); clip-path: inset(46% 0 34% 0); }
  4%   { opacity: 1; transform: translate3d(-2px, 1px, 0); clip-path: inset(62% 0 20% 0); }
  6%   { opacity: 0; }
  52%  { opacity: 1; transform: translate3d(3px, 0, 0);    clip-path: inset(30% 0 52% 0); }
  55%, 100% { opacity: 0; }
}
/* Without motion the copies are just a doubled, smeared logo — there is nothing
   left of the effect to keep, so they stay hidden rather than freezing mid-tear.
   (`html.no-anim` is covered by the global animation kill at the end of the
   file, which leaves them at their opacity: 0 rest state.) */
@media (prefers-reduced-motion: reduce) {
  .site-head .wordmark-box::before,
  .site-head .wordmark-box::after,
  .site-head .wordmark-text::before,
  .site-head .wordmark-text::after,
  .wordmark:hover .wordmark-box::before,
  .wordmark:hover .wordmark-box::after,
  .wordmark:hover .wordmark-text::before,
  .wordmark:hover .wordmark-text::after,
  .wordmark:focus-visible .wordmark-box::before,
  .wordmark:focus-visible .wordmark-box::after,
  .wordmark:focus-visible .wordmark-text::before,
  .wordmark:focus-visible .wordmark-text::after { animation: none; }
}

.site-nav { display: flex; align-items: center; gap: 2px; }
.site-nav a {
  display: inline-flex; align-items: center; gap: 6px;
  height: 36px;
  padding: 0 13px;
  border-radius: 10px;
  font-family: var(--label);
  font-size: 12.5px; font-weight: 600; letter-spacing: .005em;
  color: var(--fg-a55);
  transition: color .18s, background .18s;
}
.site-nav a:hover { color: var(--fg); background: var(--fg-a05); }
.site-nav .nav-ext {
  margin-left: 6px;
  background: var(--fg-a05);
  border: 1px solid var(--fg-a05);
}
/* The external-link mark: the top-right corner of a frame — still a corner
   with no shaft, but its apex points out of the pill, where a tipped chevron
   read as a bare right angle. */
.site-nav .nav-ext::after {
  content: '';
  width: 12px; height: 12px; flex: 0 0 12px;
  background-color: var(--fg-a40);
  -webkit-mask: var(--corner) no-repeat center / contain;
          mask: var(--corner) no-repeat center / contain;
  transition: background-color .18s, transform .18s;
}
.site-nav .nav-ext:hover { background: var(--fg-a12); }
.site-nav .nav-ext:hover::after { background-color: var(--fg); transform: translate(1px, -1px); }

/* Lang selector — the same pill + panel the section galleries use. */
.lang-selector { position: relative; }
.lang-toggle {
  display: inline-flex; align-items: center; gap: 7px;
  height: 36px;
  padding: 0 12px;
  background: var(--fg-a05);
  border: 1px solid var(--fg-a05);
  border-radius: 10px;
  color: var(--fg); cursor: pointer;
  /* Same face as the nav pills beside it. */
  font-family: var(--label);
  font-size: 12px; font-weight: 600; letter-spacing: .06em;
  transition: background .2s, border-color .2s;
}
.lang-toggle svg { color: var(--fg-a55); }
.lang-toggle:hover { background: var(--fg-a12); }
.lang-dropdown {
  position: absolute; top: calc(100% + 10px); right: 0;
  width: 200px;
  background: var(--card-solid);
  border: 1px solid var(--fg-a12);
  border-radius: 14px;
  padding: 6px;
  display: flex; flex-direction: column; gap: 1px;
  box-shadow: 0 24px 60px -18px rgba(0, 0, 0, .55);
  z-index: 95;
  /* Kept in the DOM and folded away — so opening/closing can animate. It unfurls
     from the toggle (top-right), and the options below fall in one after another. */
  transform-origin: top right;
  opacity: 0; visibility: hidden; pointer-events: none;
  transform: translateY(-6px) scale(.98);
  transition: opacity .18s ease, transform .22s var(--ease-out), visibility .18s;
}
.lang-selector.open .lang-dropdown {
  opacity: 1; visibility: visible; pointer-events: auto;
  transform: none;
}
.lang-option {
  --d: 0ms; /* per-item reveal delay; set from --i only while open (staircase) */
  display: flex; align-items: center; gap: 9px;
  padding: 7px 10px;
  border-radius: 8px;
  background: none; border: none; cursor: pointer;
  font-family: var(--text); font-size: 13px; color: var(--fg-a75);
  text-align: left;
  opacity: 0; transform: translateY(-4px);
  /* reveal (opacity/transform) is staggered by --d; hover (background/color) never is */
  transition: opacity .22s ease var(--d), transform .22s ease var(--d), background .12s ease, color .12s ease;
}
.lang-selector.open .lang-option {
  --d: calc(var(--i, 0) * 24ms);
  opacity: 1; transform: none;
}
.lang-option:hover { background: var(--fg-a08); color: var(--fg); }
.lang-option.active { color: var(--fg); font-weight: 600; }
.lang-option.active::after {
  content: '✓';
  margin-left: auto;
  color: var(--acid);
  font-weight: 600;
}
.flag-icon { width: 18px; height: 14px; object-fit: cover; border-radius: 2px; }

/* Burger (mobile) — the 36px icon-button chrome; kept square as a flex child. */
.burger {
  display: none;
  width: 36px; height: 36px;
  min-width: 36px; min-height: 36px;
  flex: 0 0 36px;
  padding: 0;
  background: var(--fg-a05);
  border: 1px solid var(--fg-a05);
  border-radius: 10px;
  cursor: pointer;
  flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  transition: background .2s;
}
.burger:hover { background: var(--fg-a12); }
.burger span {
  display: block; width: 14px; height: 1.5px;
  border-radius: 1px;
  background: var(--fg);
  transition: transform .25s, opacity .2s;
}
.burger.open span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.burger.open span:nth-child(2) { opacity: 0; }
.burger.open span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

.mobile-menu {
  position: fixed; inset: var(--head-h) 0 0 0;
  z-index: 85;
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  padding: 28px clamp(18px, 6vw, 40px) 20px;
  display: flex; flex-direction: column; gap: 30px;
  overflow-y: auto;
}
.mobile-menu[hidden] { display: none; }
.mobile-menu-group { display: flex; flex-direction: column; }
.mobile-menu-group .mono { margin-bottom: 12px; color: var(--acid); }
.mobile-menu-group a {
  font-family: var(--display);
  font-size: 22px; font-weight: 700; letter-spacing: -.01em; text-transform: uppercase;
  padding: 9px 0;
  border-bottom: 1px solid var(--fg-a08);
  transition: color .15s, padding-left .2s;
}
.mobile-menu-group a:hover { color: var(--acid); padding-left: 10px; }
/* Twelve featured projects at the section size would be a scroll of their own
   before the sections list came into view, so this group alone goes two-up and
   a size down. The heading stays full width. */
.mobile-menu-group--featured {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 16px;
}
.mobile-menu-group--featured .mono { grid-column: 1 / -1; }
.mobile-menu-group--featured a { font-size: 15px; padding: 7px 0; }
.mobile-menu-foot {
  margin-top: auto;
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  font-family: var(--label); font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  font-variant-numeric: tabular-nums; /* the year */
  color: var(--fg-a40);
  padding-top: 18px;
  border-top: 1px solid var(--fg-a08);
}
.mobile-menu-foot a { color: var(--acid); }

/* ===== LAYOUT ===== */
main { max-width: var(--max-w); margin: 0 auto; padding: 0 clamp(18px, 4vw, 48px); position: relative; z-index: 2; }

/* ===== HERO =====
   Echoes the sections' welcome slides: mono eyebrow · display title with a
   subtle vertical white→accent gradient · lede · CTA row. */
/* A container, so the heading can be sized against the space it actually has
   rather than against the viewport — see .hero h1. */
.hero { padding: clamp(64px, 11vh, 120px) 0 0; position: relative; container-type: inline-size; }

/* Nunito sets wider than the old monospace did at the same nominal size once
   you count the tracking, so the eyebrow trades .08em of letter-spacing for the
   half pixel of size — same measure on the line, more of the face's shape. */
.hero-eyebrow {
  margin-bottom: 28px;
  display: flex; align-items: center; gap: 12px;
  font-family: var(--label);
  font-size: 11.5px; font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--acid);
  opacity: .9;
}
.hero-eyebrow::before { content: ''; width: 22px; height: 1px; background: var(--acid); }

/* Science Gothic rather than the page face: a variable grotesk with a
   technical, lab-report edge — and it ships a cyrillic cut, so ru/uk keep
   the same face. */
.hero h1 {
  font-family: var(--display);
  /* Both lines have to hold one line each, and how much that costs depends
     entirely on the locale: "One lab / 128 experiments" runs 11.3× the font
     size at its longest, "Jedno laboratorium" runs 14.5×. So the size is not a
     number here — it is that measurement, inverted: --h1-fit is 100 divided by
     the longest of the locale's two lines (measured in this face, at this
     tracking, uppercased), with 2% held back for rendering slack. Multiplied by
     1cqi it says "fill the column, and stop".
     Container units rather than viewport units is what makes that hold at every
     width: cqi is a share of the very column the line has to fit inside, so a
     ratio checked at 1440 is the same ratio at 360. The vw and px terms only
     cap it. Anything not listed below keeps the default, which is the tightest
     of the twelve. */
  --h1-fit: 8;
  font-size: min(9.2vw, 118px, calc(var(--h1-fit) * 1cqi));
  font-weight: 700;
  line-height: .96;
  letter-spacing: -.03em;
  text-transform: uppercase;
  margin-bottom: 36px;
  /* The welcome slides' title treatment: white pouring into the rotating
     accent well below the baseline, so only the lower line picks up a tint. */
  background: linear-gradient(180deg, #fff 0%, var(--acid) 135%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}
/* One entry per locale, each the inverse of that locale's longest heading line.
   Polish is the widest of the twelve ("Jedno laboratorium"), Cyrillic next;
   Italian and French are the roomiest. zh/ja/ko fall back to a system face
   whose metrics are not ours to measure, so they are given the value the 118px
   cap would allow anyway rather than one tuned to a font this machine happens
   to have. */
html:lang(pl) .hero h1 { --h1-fit: 6.75; }
html:lang(ru) .hero h1 { --h1-fit: 7.1; }
html:lang(uk) .hero h1 { --h1-fit: 7.4; }
html:lang(es) .hero h1 { --h1-fit: 8; }
html:lang(tr) .hero h1 { --h1-fit: 8.25; }
html:lang(de) .hero h1,
html:lang(en) .hero h1 { --h1-fit: 8.7; }
html:lang(fr) .hero h1 { --h1-fit: 8.75; }
html:lang(it) .hero h1 { --h1-fit: 8.9; }
html:lang(zh) .hero h1,
html:lang(ja) .hero h1,
html:lang(ko) .hero h1 { --h1-fit: 11; }
/* Both lines flush left, the second one carrying the rotating accent solid
   rather than the tail of the heading's gradient: colour is what separates the
   two, so the pair still reads as one block of type spanning the column instead
   of as two sentences stacked. (The second line used to be set against the
   opposite edge — that off-balance is the experimental skin's idea, and it is
   still that skin's, along with the opposite treatments it has since given its
   two lines: the accent print on the first, a texture per glyph on the second.
   So the alignment and the fill below are both written for this skin alone. The
   fill has to be written at all because the h1 pours a gradient through the
   glyphs and hands its children a transparent fill.) */
.hero h1 .outline {
  display: block;
}
/* main.js splits the second line into one span per glyph, which is what lets
   the experimental skin fill each letter with a pattern of its own. Only the
   word wrapper it puts around them matters here, and it matters in both skins:
   with every glyph its own inline box, a line is free to break between any two
   letters. Nothing else about the split is visible outside that skin. */
.hero h1 .hero-word { white-space: nowrap; }
html:not(.theme-legacy) .hero h1 .outline {
  text-align: left;
  color: var(--acid);
  -webkit-text-fill-color: var(--acid);
}

.hero-row {
  display: flex; gap: clamp(24px, 5vw, 72px);
  align-items: flex-end;
  flex-wrap: wrap;
  margin-bottom: clamp(44px, 7vh, 76px);
  justify-content: space-between;
}
.hero .lede { max-width: 500px; flex: 1 1 380px; font-size: 15px; line-height: 1.75; color: var(--fg-a55); }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }

/* CTA — built in layers rather than one flat fill: a translucent blurred body,
   a 1px edge lit from the top (a gradient painted through a transparent border:
   fill clipped to padding-box, edge to border-box — no extra element), a top
   inner highlight, a low outer shadow that deepens on hover, and the chevron on
   its own small plate. The chevron is a pseudo-element because the i18n loader
   replaces the element's innerHTML wholesale on every locale switch. */
.btn {
  position: relative;
  display: inline-flex; align-items: center; gap: 6px;
  /* One notch more vertical padding than the Inter label needed: the smaller
     Unbounded size shrinks the line box, and the pill has to keep its height.
     The right side carries the chevron's plate plus the air the mark needs to
     read as a separate object rather than as the last letter of the label —
     see the negative offset on ::after below. */
  padding: 14px 26px 14px 18px;
  border: 1px solid transparent;
  border-radius: 14px;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .085), rgba(255, 255, 255, .02) 58%, rgba(255, 255, 255, .045)) padding-box,
    linear-gradient(180deg, rgba(255, 255, 255, .3), rgba(255, 255, 255, .08) 45%, rgba(255, 255, 255, .05)) border-box;
  background-repeat: no-repeat;
  backdrop-filter: blur(12px) saturate(1.15);
  -webkit-backdrop-filter: blur(12px) saturate(1.15);
  /* The brand face on the calls to action, matched to the wordmark. It is wide
     enough that the old 13px + positive tracking overflowed the pill on the
     longer locales ("Ausgewählte Projekte"), so the label drops to 11.5px and
     the tracking to zero — Unbounded needs no help spreading out. */
  font-family: var(--brand);
  font-size: 11.5px; font-weight: 600; letter-spacing: 0;
  color: var(--fg);
  /* top highlight · an inner hairline that only lights up on hover · contact
     shadow · lift. Only the shadow moves, so hover stays a cheap repaint. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .06),
    inset 0 0 0 1px rgba(255, 255, 255, 0),
    0 2px 5px -3px rgba(0, 0, 0, .6),
    0 14px 32px -18px rgba(0, 0, 0, .9);
  transition: box-shadow .3s var(--ease-out), transform .18s var(--ease-out), color .25s;
}
/* the plate the chevron sits on */
.btn::before {
  content: '';
  /* 10px, not 8: the chevron is nudged 12px back out of the widened padding
     (see ::after) and lands with its centre 22px in from the edge — the plate
     has to sit under it, not under where it used to be. */
  position: absolute; top: 50%; right: 10px; z-index: 0;
  width: 24px; height: 24px;
  border-radius: 8px;
  background: rgba(255, 255, 255, .05);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .05), inset 0 1px 0 rgba(255, 255, 255, .07);
  transform: translateY(-50%);
  pointer-events: none;
  transition: background .25s, box-shadow .25s, transform .25s var(--ease-out);
}
/* The chevron itself — a mask, so it inherits the button's text colour. The
   flex gap alone put the mark right up against the label; the pill's right
   padding is what opens that distance, and the negative offset then walks the
   chevron back out into it so the pill does not just grow a bald edge. */
.btn::after {
  content: '';
  position: relative; right: -12px; z-index: 1;
  width: 16px; height: 16px; flex: 0 0 16px;
  background-color: currentColor;
  -webkit-mask: var(--chev) no-repeat center / contain;
          mask: var(--chev) no-repeat center / contain;
  transition: transform .22s var(--ease-out);
}
.btn:hover {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .1),
    inset 0 0 0 1px rgba(255, 255, 255, .09),
    0 3px 8px -4px rgba(0, 0, 0, .6),
    0 20px 44px -20px rgba(0, 0, 0, 1);
}
/* Both shift by the same 2px, or the mark slides off-centre inside its plate. */
.btn:hover::before { background: rgba(255, 255, 255, .1); transform: translate(2px, -50%); }
.btn:hover::after { transform: translateX(2px); }
/* pressed: sink, and pull the shadow in under the pill */
.btn:active {
  transform: scale(.98);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .05),
    0 1px 2px -1px rgba(0, 0, 0, .6),
    0 5px 14px -10px rgba(0, 0, 0, .9);
}
.btn:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--acid) 78%, #fff);
  outline-offset: 3px;
}

/* Primary — the rotating accent as a lit pill: --acid over --acid-dark (both
   rewritten at runtime, so nothing here names a hue), a coloured glow in place
   of the black shadow, and a specular band parked off the left edge that sweeps
   across once per hover. */
.btn-primary {
  color: var(--acid-ink);
  /* Its fill is opaque, so the inherited blur buys nothing and would cost a
     backdrop snapshot on every frame of the sheen. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background:
    linear-gradient(100deg, rgba(255, 255, 255, 0) 42%, rgba(255, 255, 255, .4) 50%, rgba(255, 255, 255, 0) 58%) padding-box,
    /* Shallow on purpose: some palettes have a properly dark --acid-dark, and
       the label has to stay readable over the foot of the pill. */
    linear-gradient(180deg, color-mix(in srgb, var(--acid) 92%, #fff), var(--acid) 48%, color-mix(in srgb, var(--acid-dark) 70%, var(--acid))) padding-box,
    linear-gradient(180deg, rgba(255, 255, 255, .6), rgba(255, 255, 255, .18) 50%, rgba(255, 255, 255, .08)) border-box;
  background-size: 220% 100%, auto, auto;
  background-position: 130% 0, 0 0, 0 0;
  background-repeat: no-repeat;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .4),
    0 2px 6px -3px color-mix(in srgb, var(--acid) 45%, transparent),
    0 16px 38px -14px color-mix(in srgb, var(--acid) 55%, transparent);
}
.btn-primary::before {
  background: color-mix(in srgb, var(--acid-ink) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--acid-ink) 8%, transparent);
}
.btn-primary:hover {
  animation: btn-sheen 1s var(--ease-out);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .5),
    0 3px 9px -4px color-mix(in srgb, var(--acid) 32%, transparent),
    0 22px 48px -14px color-mix(in srgb, var(--acid) 44%, transparent);
}
.btn-primary:hover::before { background: color-mix(in srgb, var(--acid-ink) 16%, transparent); }
/* Pressed = the darker cut of the same accent (rotated together with --acid);
   only the images are swapped, so the per-layer size/position/clip lists hold. */
.btn-primary:active {
  background-image:
    none,
    /* Bottomed at the same 70% mix the rest state uses: raw --acid-dark drops
       the ink label under 4.5:1 on the darkest palette rows. */
    linear-gradient(180deg, color-mix(in srgb, var(--acid-dark) 55%, var(--acid)), color-mix(in srgb, var(--acid-dark) 70%, var(--acid))),
    linear-gradient(180deg, rgba(255, 255, 255, .34), rgba(255, 255, 255, .06));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .22),
    0 2px 9px -6px color-mix(in srgb, var(--acid) 55%, transparent);
}
/* A background percentage slides the oversized layer leftwards, so sweeping the
   band across the pill left-to-right means counting down from 130% to -30%. */
@keyframes btn-sheen {
  from { background-position: 130% 0, 0 0, 0 0; }
  to { background-position: -30% 0, 0 0, 0 0; }
}

/* stats — a mono-labelled tile row inside one window-styled card */
.hero-stats {
  display: grid; grid-template-columns: repeat(4, 1fr);
  background: var(--card);
  border: 1px solid var(--fg-a08);
  border-radius: var(--radius);
  box-shadow: var(--shadow-raise);
  overflow: hidden;
}
.hero-stats > div {
  padding: 22px 24px;
  border-right: 1px solid var(--fg-a05);
  border-bottom: 1px solid var(--fg-a05);
  transition: background .2s;
}
.hero-stats > div:hover { background: var(--fg-a05); }
.hero-stats > div:nth-child(4n) { border-right: none; }
.hero-stats > div:nth-child(n+5) { border-bottom: none; }
@media (max-width: 680px) {
  .hero-stats { grid-template-columns: repeat(2, 1fr); }
  .hero-stats > div:nth-child(2n) { border-right: none; }
  .hero-stats > div:nth-child(n+5) { border-bottom: 1px solid var(--fg-a05); }
  .hero-stats > div:nth-child(n+7) { border-bottom: none; }
}
/* The odometers live here (main.js counts them up on first sight), so the
   figures are asked for in their tabular cut: with proportional digits a 1
   rolling into an 8 shifts everything to its right by a hair, sixty times a
   second. The tiles themselves cannot move — the grid is four 1fr columns and
   the padding is fixed, so nothing outside the number is measured off it. */
.hero-stats dt {
  font-family: var(--display);
  font-size: clamp(26px, 3.6vw, 40px); font-weight: 500; letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  color: var(--acid);
}
.hero-stats dd {
  font-family: var(--label);
  font-size: 10px; font-weight: 700; color: var(--fg-a40);
  letter-spacing: .14em; text-transform: uppercase;
  margin-top: 8px;
}

/* ===== AURORA =====
   Quiet drifting blobs tinted by the rotating accent (--goo1..4 are the palette
   slots the head script / main.js keep writing). Scoped to the hero box: its
   container-type containment captures this fixed layer, so it scrolls away with
   the hero rather than following the page. */
.hero-aurora {
  position: fixed;
  top: 0; bottom: 0; left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  pointer-events: none;
  overflow: hidden;
  opacity: .5;
  z-index: 0;
  -webkit-mask-image: radial-gradient(75% 80% at 50% 38%, #000 40%, transparent 100%);
          mask-image: radial-gradient(75% 80% at 50% 38%, #000 40%, transparent 100%);
}
/* Laid out at half size and scaled ×2, so the blur runs on a quarter of the
   pixels (main.js maps pointer coordinates to the halved space to match). */
.aurora-field {
  position: absolute; left: 0; top: 0;
  width: 50%; height: 50%;
  transform: scale(2);
  transform-origin: 0 0;
  filter: blur(34px);
}
.aurora-field i {
  position: absolute;
  width: 26vmin; height: 26vmin;
  border-radius: 50%;
  mix-blend-mode: screen;
  opacity: .32;
}
.aurora-field .g1 { background: radial-gradient(circle at center, var(--goo1, var(--acid)) 0, transparent 62%); top: 2%; left: 12%; animation: aurora-a 34s ease-in-out infinite; }
.aurora-field .g2 { background: radial-gradient(circle at center, var(--goo2, var(--acid)) 0, transparent 62%); top: 24%; left: 54%; animation: aurora-b 44s ease-in-out infinite -9s; opacity: .26; }
.aurora-field .g3 { background: radial-gradient(circle at center, var(--goo3, var(--acid)) 0, transparent 62%); top: -10%; left: 64%; animation: aurora-a 52s ease-in-out infinite -21s; opacity: .22; }
.aurora-field .g4 { background: radial-gradient(circle at center, var(--goo4, var(--acid)) 0, transparent 62%); top: 36%; left: -6%; animation: aurora-b 40s ease-in-out infinite -5s; opacity: .18; }
.aurora-field .g-pointer {
  width: 14vmin; height: 14vmin;
  background: radial-gradient(circle at center, var(--acid) 0, transparent 60%);
  opacity: .22;
  top: 0; left: 0;
  will-change: transform;
}
/* travel distances are halved too — the ×2 scale doubles them back */
@keyframes aurora-a {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50% { transform: translate(5vmin, 7vmin) scale(1.12); }
}
@keyframes aurora-b {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50% { transform: translate(-6vmin, -5vmin) scale(.92); }
}

/* ===== MARQUEE ===== */
.tape {
  margin: clamp(8px, 2vh, 20px) calc(-50vw + 50%) 0;
  border-top: 1px solid var(--fg-a05);
  border-bottom: 1px solid var(--fg-a05);
  background: rgba(244, 242, 255, .02);
  overflow: hidden;
  white-space: nowrap;
  padding: 13px 0;
  /* The strip dissolves at the viewport edges instead of hitting them. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.tape-track { display: inline-block; animation: tape 38s linear infinite; }
.tape .w,
.tape .d {
  /* Outside every Nunito subset: name a face that has it rather than letting
     the fallback chain fetch a family for one glyph. */
  font-family: system-ui, sans-serif;
  display: inline-block;
  font-family: var(--label);
  font-size: 11.5px; font-weight: 700; letter-spacing: .22em; text-transform: uppercase;
  color: var(--fg-a40);
  margin: 0 17px; /* equal gap on both sides of every token */
}
.tape .d { color: var(--acid); letter-spacing: 0; opacity: .7; }
@keyframes tape { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ===== BLOCKS =====
   The band between two sections is the only thing separating a stats card from
   an ordinal eyebrow, and at 60–100px the two were reading as one column of
   boxes. Half again as much air, and the numbered headings start behaving like
   chapter openings. */
.block { padding: clamp(88px, 13vh, 150px) 0; }
.block-head { position: relative; display: flex; align-items: baseline; gap: 20px; margin-bottom: 14px; }
/* Ordinal eyebrow — numbers are locale-neutral, so no i18n string needed. */
.block-head::before {
  position: absolute; top: -24px; left: 1px;
  font-family: var(--label);
  font-size: 11px; font-weight: 700; letter-spacing: .22em;
  font-variant-numeric: tabular-nums;
  color: var(--acid);
  opacity: .9;
}
#sections .block-head::before { content: '01'; }
#featured .block-head::before { content: '02'; }
.block-head h2 {
  font-family: var(--display);
  font-size: clamp(28px, 4.2vw, 46px);
  font-weight: 700; letter-spacing: -.02em; text-transform: uppercase;
  line-height: 1;
}
.block-sub { max-width: 560px; font-size: 14px; color: var(--fg-a55); line-height: 1.7; margin-bottom: clamp(28px, 5vh, 48px); }
#featured .block-sub { max-width: 570px; }

/* ===== FEATURED — window cards ===== */
/* Twelve cards in two columns. minmax(0, 1fr) rather than a bare 1fr: a grid
   track sizes to its content by default, and one long project name would push
   its column wider than its neighbour for the whole list. */
.feature-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.feature-row {
  position: relative;
  display: grid;
  /* Two tracks now: the status badge left the flow for the top-right corner. */
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: clamp(12px, 1.6vw, 22px);
  /* The top padding is floored at the badge's own height, so content always
     starts below the corner tag — no width can make the two meet. */
  padding: clamp(24px, 3vh, 28px) clamp(16px, 1.8vw, 22px) clamp(16px, 2.4vh, 20px);
  background: var(--card);
  border: 1px solid var(--fg-a05);
  border-radius: var(--radius);
  overflow: hidden;
  /* the CTA's top inner highlight, at surface strength */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .045);
  transition: border-color .25s, transform .25s var(--ease-out), box-shadow .3s;
}
/* accent orb in the corner — lights up on hover */
.feature-row::before {
  content: '';
  position: absolute; top: -80px; right: -50px;
  width: 240px; height: 240px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--acid) 0, transparent 65%);
  filter: blur(40px);
  opacity: 0;
  transition: opacity .35s;
  pointer-events: none;
}
.feature-row:hover {
  border-color: color-mix(in srgb, var(--acid) 20%, var(--fg-a12));
  box-shadow: var(--shadow-hover), inset 0 1px 0 rgba(255, 255, 255, .07);
}
.feature-row:hover::before { opacity: .12; }
/* The orb and the running light are pseudo-elements, i.e. positioned siblings
   that paint after the in-flow text; one layer up keeps the content clean. */
.feature-body, .feature-arrow { position: relative; }
.feature-body, .feature-status, .feature-arrow { z-index: 1; }
.feature-body { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.feature-name {
  font-family: var(--display);
  /* Half the width it used to have, so the upper bound comes down with it —
     at 24px a two-word name wrapped inside a column card. */
  font-size: clamp(17px, 1.8vw, 21px);
  font-weight: 700; letter-spacing: -.01em; text-transform: uppercase;
}
.feature-desc { font-size: 13px; color: var(--fg-a55); }
/* The host is the one thing these twelve gained by leaving the lab: each is a
   real site now. Printing it makes the list scannable as an index of live
   addresses rather than a set of promises. */
.feature-host {
  margin-top: 2px;
  font-family: var(--label);
  font-size: 10.5px; font-weight: 700; letter-spacing: .09em;
  color: var(--fg-a40);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: color .2s;
}
.feature-row:hover .feature-host { color: color-mix(in srgb, var(--acid) 70%, var(--fg-a55)); }
/* A corner tag rather than a floating pill: flush into the card's top-right,
   so the only corner it rounds off is the free one (bottom-left) plus the card's
   own at top-right — minus the 1px border, since it sits inside it. The two
   edges that run along the card's borders are square and get the hairline; the
   other two have nothing to sit against. */
.feature-status {
  position: absolute; top: 0; right: 0;
  display: inline-flex; align-items: center; gap: 6px;
  height: 24px;
  padding: 0 12px 0 11px;
  font-family: var(--label);
  font-size: 9.5px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--fg-a55);
  background: var(--fg-a05);
  border: 0;
  border-left: 1px solid var(--fg-a05);
  border-bottom: 1px solid var(--fg-a05);
  border-radius: 0 calc(var(--radius) - 1px) 0 10px;
  white-space: nowrap;
}
.feature-status::before {
  content: '';
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--acid);
  box-shadow: 0 0 8px color-mix(in srgb, var(--acid) 80%, transparent);
  animation: status-pulse 2.4s ease-in-out infinite;
}
@keyframes status-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}
/* Three statuses, and the dot is what tells them apart at a glance: a pulsing
   accent means the thing is answering requests right now, a still grey dot means
   it is not. Nothing here may read as "live" unless it actually is. */
.feature-status--dev::before,
.feature-status--proto::before {
  background: var(--fg-a40);
  box-shadow: none;
  animation: none;
}
.feature-arrow { display: inline-flex; align-items: center; color: var(--fg-a40); transition: color .18s, transform .3s var(--ease-out); }
.feature-row:hover .feature-arrow { color: var(--acid); transform: translateX(5px); }

/* Running light along the rows. The old skin sent it down the hairline between
   rows; the rows are separate cards now, so it runs along each card's own
   bottom rail instead — same read, and the rounded corners clip the ends where
   the gradient has already faded out. One 7s pass, split evenly between the
   cards by negative delays, so at most one light is ever crossing. --fi is the
   card's index, written inline in the markup: with twelve of them, enumerating
   :nth-child() rules would be a list nobody keeps in step with the HTML. It
   steps aside on hover: the row lights its own edge then. */
.feature-row::after {
  transition: opacity .25s;
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background-image: linear-gradient(90deg, transparent, color-mix(in srgb, var(--acid) 80%, #fff) 50%, transparent);
  background-size: 45% 100%;
  background-repeat: no-repeat;
  background-position: -100% 0;
  opacity: .45;
  animation: sep-run 7s linear infinite;
  animation-delay: calc(var(--fi, 0) * -7s / 12);
  pointer-events: none;
}
.feature-row:hover::after { opacity: 0; }
/* A background percentage slides the oversized layer, so a left-to-right pass
   means counting up from -100% (fully off the left) to 200%. */
@keyframes sep-run {
  from { background-position: -100% 0; }
  to { background-position: 200% 0; }
}

/* ===== SECTIONS GRID — window cards, one per gallery, each carrying its own
   accent hue via --card-accent, set by a card--<section> modifier class below.
   The six hues are picked to read as unmistakably different at a glance —
   gold / green / cyan / violet / blue / crimson — rather than shades of one
   family, which is what made every card's hover read as the same purple
   before. ===== */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: 14px;
}
.card {
  --card-accent: var(--acid); /* fallback; every card sets its own hue below */
  position: relative;
  display: flex; flex-direction: column;
  gap: 10px;
  padding: clamp(20px, 2.6vw, 26px);
  min-height: 230px;
  background: var(--card);
  border: 1px solid var(--fg-a08);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .045);
  /* No transform in the list: the card does not move on hover (see :hover). */
  transition: border-color .25s, box-shadow .3s;
}
/* Same specificity as .card above (one class each), so source order is what
   makes these win over its var(--acid) fallback — keep them below it. */
.card--apps        { --card-accent: #fbbf24; } /* gold — one-page apps & tools */
.card--games       { --card-accent: #34d399; } /* green — arcade "go" energy */
.card--interactive { --card-accent: #22d3ee; } /* cyan — cursors, canvas, AMOLED */
.card--s3d         { --card-accent: #c084fc; } /* violet — isometric 3D worlds */
.card--clocks      { --card-accent: #60a5fa; } /* blue — steel dials, precision */
.card--pomodoro    { --card-accent: #f43f5e; } /* crimson — the tomato itself */
/* section-tinted orb in the corner — a faint always-on hint of the hue, so a
   touch visitor (no real :hover to reach the bolder state further down) still
   sees at a glance which card belongs to which section. */
.card::before {
  content: '';
  position: absolute; top: -70px; right: -60px;
  width: 220px; height: 220px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--card-accent) 0, transparent 65%);
  filter: blur(36px);
  opacity: .1;
  transition: opacity .35s;
  pointer-events: none;
}
/* Pointer-tracked wash plus a soft corner gradient, both in the section's own
   hue, both riding one opacity fade. --px/--py are two percentages written by
   one rAF-throttled listener in main.js; they default to the card's centre, so
   with a coarse pointer — or with motion off, when main.js does not listen at
   all — the first layer degrades to a plain centred glow. The old skin flooded
   the whole card with the accent and inverted the text; that belonged to the
   flat-panel look. This is the same "the card lights up under your hand" idea
   rebuilt for a translucent window surface, plus the section-tinted background
   gradient riding along with it. */
.card::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  background:
    radial-gradient(circle 220px at var(--px, 50%) var(--py, 50%),
      color-mix(in srgb, var(--card-accent) 22%, transparent), transparent 72%),
    linear-gradient(155deg, color-mix(in srgb, var(--card-accent) 14%, transparent), transparent 60%);
  opacity: 0;
  transition: opacity .3s;
  pointer-events: none;
}
/* Both card pseudo-elements are positioned siblings and paint after the in-flow
   text, so the content is lifted one layer clear of the wash. */
.card h3, .card p, .card-tech { position: relative; }
.card h3, .card p, .card-tech, .card-count, .card-arrow { z-index: 1; }
.card-count {
  position: absolute; top: 20px; right: 20px;
  font-family: var(--label);
  font-size: 10.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  /* the scene count sits at a fixed corner — tabular digits keep 4 and 56 on
     the same rhythm down the grid */
  font-variant-numeric: tabular-nums;
  color: var(--fg-a40);
  transition: color .25s;
}
.card-count b { font-weight: 700; color: var(--card-accent); transition: text-shadow .25s; }
.card h3 {
  font-family: var(--display);
  font-size: clamp(20px, 2.4vw, 24px);
  font-weight: 700; letter-spacing: -.01em; text-transform: uppercase;
  margin-top: auto;
}
/* Reserve 3 lines so descriptions of different length keep the bottom-anchored
   title / desc / tech group the same height — otherwise the titles sit at
   different heights across the row. */
.card p { font-size: 13px; color: var(--fg-a55); line-height: 1.65; min-height: 4.95em; }
/* The longest stack line ("Canvas 2D · Three.js · JS") has to clear the arrow
   chip on a 300px card, so this slot takes the tracking cut rather than the
   size bump the other labels got. */
.card-tech {
  font-family: var(--label);
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--fg-a40);
  margin-top: 6px;
  padding-right: 44px; /* keep clear of the corner arrow chip */
  transition: color .25s;
}
/* Same plate as the CTA chevron, one size up; the mark is tipped 45deg so it
   points out of the card. */
.card-arrow {
  position: absolute; bottom: 18px; right: 18px;
  display: grid; place-items: center;
  width: 30px; height: 30px;
  border-radius: 9px;
  border: 1px solid var(--fg-a08);
  background: var(--fg-a05);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05);
  color: var(--fg-a55);
  transition: background .25s, color .25s, border-color .25s, box-shadow .3s, transform .25s var(--ease-out);
}
/* Upright, not tipped: a section card leads further in, and two equal arms at
   45deg read as a crop mark rather than a direction. On hover the chip charges:
   it fills with the section hue and throws a short glow of it underneath.

   Everything below is gated on (hover: hover): a tap on a touch screen fires
   :hover just long enough to look half-applied mid-navigation, so those
   visitors keep only the quieter always-on hint above — never broken, just
   less loud. The site's own animation kill switch (html.no-anim) still wins
   here for free, since it blanks every transition on the page, this one
   included. */
@media (hover: hover) {
  .card:hover {
    border-color: color-mix(in srgb, var(--card-accent) 45%, var(--fg-a12));
    box-shadow:
      var(--shadow-hover),
      inset 0 1px 0 color-mix(in srgb, var(--card-accent) 45%, rgba(255, 255, 255, .07)),
      0 0 32px -10px color-mix(in srgb, var(--card-accent) 65%, transparent);
  }
  .card:hover::before { opacity: .34; }
  .card:hover::after { opacity: 1; }
  .card:hover .card-count { color: color-mix(in srgb, var(--card-accent) 55%, var(--fg-a75)); }
  .card:hover .card-count b { text-shadow: 0 0 14px color-mix(in srgb, var(--card-accent) 75%, transparent); }
  .card:hover .card-tech { color: var(--fg-a55); }
  .card:hover .card-arrow {
    background: color-mix(in srgb, var(--card-accent) 28%, transparent);
    border-color: color-mix(in srgb, var(--card-accent) 45%, transparent);
    color: var(--card-accent);
    transform: translateX(2px);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, .09),
      0 0 16px -4px color-mix(in srgb, var(--card-accent) 55%, transparent);
  }
}

/* ===== FOOTER ===== */
.site-foot {
  position: relative; z-index: 2;
  margin-top: 0;
  background: #08090e;
  overflow: hidden;
}
/* accent hairline along the top edge */
.foot-accent {
  position: relative; z-index: 3;
  height: 1px;
  background: linear-gradient(90deg,
    transparent,
    color-mix(in srgb, var(--acid) 50%, transparent) 30%,
    color-mix(in srgb, var(--acid) 65%, transparent) 50%,
    color-mix(in srgb, var(--acid) 50%, transparent) 70%,
    transparent);
}
/* Fine checkered grid under the top edge — the same 22px lattice the previous
   skin drew, re-tinted onto the fg ladder instead of the retired --ink vars so
   it sits a step under the footer's own hairlines. Two scales: the 22px weave
   at a05 for texture, a 110px lattice at a08 every fifth line to give it the
   read of engineering paper rather than a noise field. The domed radial mask
   keeps it to the lit part of the footer, fading out well before the columns —
   past that it would only make the link lists harder to scan. */
.site-foot::before {
  content: '';
  position: absolute; inset: 0; z-index: 0;
  background-image:
    linear-gradient(rgba(244, 242, 255, .13) 1px, transparent 1px),
    linear-gradient(90deg, rgba(244, 242, 255, .13) 1px, transparent 1px),
    linear-gradient(var(--fg-a08) 1px, transparent 1px),
    linear-gradient(90deg, var(--fg-a08) 1px, transparent 1px);
  background-size: 18px 18px;
  -webkit-mask-image: radial-gradient(85% 78% at 50% -6%, #000 8%, transparent 62%);
          mask-image: radial-gradient(85% 78% at 50% -6%, #000 8%, transparent 62%);
  pointer-events: none;
  opacity: .25;
}
/* soft lamp glow spilling from the top edge (about half visible) */
.foot-glow {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(1100px, 90vw);
  height: 440px;
  z-index: 1;
  background: radial-gradient(50% 60% at 50% 50%,
  color-mix(in srgb, var(--acid) 20%, transparent), transparent 70%);
  filter: blur(28px);
  opacity: .55;
  pointer-events: none;
}
.foot-inner { position: relative; z-index: 2; max-width: var(--max-w); margin: 0 auto; padding: 0 clamp(18px, 4vw, 48px); }

/* top band: the brand, alone above the section columns */
.foot-top {
  display: flex; align-items: center;
  padding: clamp(40px, 6vh, 60px) 0 32px;
  border-bottom: 1px solid var(--fg-a08);
}
.foot-word { margin-right: 0; }

/* section columns */
.foot-grid {
  padding: clamp(28px, 5vh, 44px) 0 24px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 28px 20px;
}
.foot-col { display: flex; flex-direction: column; gap: 8px; }
/* The column heads carry the accent, as they do in the experimental skin: six
   lit markers are what let the eye find the six sections in a wall of grey
   links, and the rotating hue ties the footer to the rest of the page. */
.foot-head {
  font-family: var(--label);
  font-size: 11px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: var(--acid);
  margin-bottom: 6px;
  width: fit-content;
  transition: color .15s;
}
/* Already accent at rest, so hover has to go somewhere else: towards white,
   which brightens every palette entry the rotation can land on. */
.foot-head:hover { color: color-mix(in srgb, var(--acid) 60%, #fff); }
.foot-col a:not(.foot-head) {
  font-size: 12px; color: var(--fg-a55);
  width: fit-content;
  transition: color .15s;
}
.foot-col a:not(.foot-head):hover { color: var(--fg); }
/* Five scenes are a taste, not a table of contents — every column ends with the
   way into the rest of its section. Set apart from the picks above it by a
   little air and by the accent, and marked with the page's one chevron. The
   mark is a pseudo-element because the i18n loader rewrites the link's
   innerHTML on every locale switch and would eat anything put in the markup. */
.foot-col a.foot-more {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 4px;
  color: var(--acid);
  transition: color .15s, gap .2s var(--ease-out);
}
.foot-col a.foot-more::after {
  content: '';
  width: 11px; height: 11px; flex: 0 0 11px;
  background-color: currentColor;
  -webkit-mask: var(--chev) no-repeat center / contain;
          mask: var(--chev) no-repeat center / contain;
}
.foot-col a.foot-more:hover { color: color-mix(in srgb, var(--acid) 60%, #fff); gap: 10px; }

.foot-bar {
  border-top: 1px solid var(--fg-a08);
  display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 20px 0 26px;
  font-family: var(--label); font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  font-variant-numeric: tabular-nums; /* the year */
  color: var(--fg-a40);
}
.foot-bar a { color: var(--acid); transition: opacity .15s; }
.foot-bar a:hover { opacity: .75; }

/* ===== FOOTER CURTAIN =====
   On a desktop screen the footer is not pushed up the page by the content — it
   is parked against the bottom of the viewport and the content is an opaque
   sheet sliding over it. Scrolling the last screen therefore *uncovers* the
   footer from its top edge down, the way a curtain goes up, instead of dragging
   it into view. Nothing animates: it is two boxes and a stacking order, so it
   costs nothing per frame and behaves identically with animations switched off.

   Three parts, and all three are needed:
     · .foot-dock is taken out of the flow and pinned to the bottom, one layer
       down (z-index 0);
     · .page is everything above it — header, hero, blocks — raised one layer up
       and given the page background, because a transparent sheet would show the
       footer through it from the first screen;
     · and the flow still has to reserve the footer's height, or the last section
       would scroll straight past it. That is .foot-space, standing in for the
       box that left the flow, sized from --foot-h — measured by main.js, because
       how tall the footer ends up depends on how its six columns wrap.

   Below 768px none of it applies: the dock is a plain block again and footer
   follows content the ordinary way. */
.page { position: relative; }
.foot-dock { position: relative; }

@media (min-width: 768px) {
  .page {
    z-index: 1;
    background: var(--bg);
  }
  .foot-dock {
    position: fixed; left: 0; right: 0; bottom: 0;
    z-index: 0;
  }
  .foot-space { height: var(--foot-h, 0px); }
  /* The hem of the sheet, lit in the rotating accent. Without it the reveal is
     two near-black fields meeting at an invisible line and the footer looks
     like it is being clipped rather than uncovered; with it there is an edge
     travelling up the screen. It is the footer's own top rule, moved onto the
     thing that actually moves — which is why the real one steps aside below. */
  .page::after {
    content: '';
    position: absolute; left: 0; right: 0; bottom: 0;
    height: 1px;
    background: linear-gradient(90deg,
      transparent,
      color-mix(in srgb, var(--acid) 50%, transparent) 30%,
      color-mix(in srgb, var(--acid) 65%, transparent) 50%,
      color-mix(in srgb, var(--acid) 50%, transparent) 70%,
      transparent);
    pointer-events: none;
  }
  /* Only the property is overridden, not the paint — so the experimental skin's
     own thicker rule stays exactly as it is and simply stops being drawn while
     the curtain owns that job. */
  .foot-dock .foot-accent { visibility: hidden; }
}
/* Escape hatch. A viewport shorter than the footer would clip its top edge off
   the screen, and there is no scrolling inside a fixed box — so main.js
   measures both and puts the footer back in the flow when it will not fit. */
html.foot-static .page { z-index: auto; }
html.foot-static .foot-dock { position: relative; }
html.foot-static .foot-space { height: 0; }
/* …and with it the lit hem, which has nothing to uncover any more, and the
   footer's own top rule, which has its job back. */
html.foot-static .page::after { display: none; }
html.foot-static .foot-dock .foot-accent { visibility: visible; }
/* The sheet is its own layer now, and the switch rail sits outside it — which
   would float the two pills over an open mobile menu. They were always hidden
   behind it; keep them there. */
body:has(.mobile-menu:not([hidden])) .corner-switches { opacity: 0; pointer-events: none; }

/* ===== RESPONSIVE ===== */
/* The 108px indent this replaces was solving a real collision — the switch rail
   is fixed in the bottom-left corner and the footer's last line ends up right
   under it — but it solved it sideways, which knocked the copyright out of
   alignment with the six columns above it at every width, including the ones
   where the rail was nowhere near. The rail reaches into the column only below
   about 1300px (past that the centred 1160 column starts far enough in), so the
   clearance is taken there, and downwards. */
@media (max-width: 1295px) {
  .foot-bar { padding-bottom: 68px; }
}
@media (max-width: 900px) {
  .site-nav { display: none; }
  .burger { display: flex; }
  /* Two columns stop paying for themselves once a card is under ~400px: the
     corner badge and the host line start colliding with the name. */
  .feature-list { grid-template-columns: 1fr; }
}
@media (max-width: 760px) {
  /* The status badge is in the corner now, so the row is just body + arrow. */
  .feature-row { grid-template-columns: 1fr 32px; }
  .feature-arrow { align-self: center; justify-self: end; }
  .foot-grid { grid-template-columns: repeat(2, 1fr); }
  .spotlight { display: none; }
}
/* The eyebrow is one long tracked string and the locales stretch it further —
   "NextWell · Laboratorio de experimentos" is 38 characters against the English
   36. It wrapped on a 390px screen once, so below 430px the size and the
   tracking both come down and the rule and its gap shrink with them. Re-checked
   against Nunito, which sets a touch narrower than the monospace it replaced
   (~0.65em a cap against a flat 0.6em advance, but no tracking wasted on the
   spaces): at 10px/.12em the longest locale runs ~295px against the 354px a
   390px screen leaves, and still clears 360px. Nothing is asked to hold a
   second line. */
@media (max-width: 430px) {
  .hero-eyebrow {
    font-size: 10px;
    letter-spacing: .12em;
    gap: 8px;
    margin-bottom: 22px;
  }
  .hero-eyebrow::before { width: 14px; }
}
/* Narrow phones: the CTAs go full width and split, so a long localized label
   can never push a pill past the edge — it wraps inside instead. */
@media (max-width: 420px) {
  .hero-actions { width: 100%; }
  .btn { flex: 1 1 100%; justify-content: space-between; }
}
@media (max-width: 760px), (prefers-reduced-motion: reduce) {
  .hero-aurora { display: none; }
  /* A light crossing a card is a desktop-scale gesture; on a phone the four
     rows fill the screen and it reads as flicker. */
  .feature-row::after { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .tape-track { animation: none; }
  .feature-status::before { animation: none; }
  .btn-primary:hover { animation: none; }
  .reveal { opacity: 1; transform: none; transition: none; }
  *, *::before, *::after { transition-duration: 0.01ms !important; }
}

/* ===== corner switches (bottom-left) =====
   Two independent controls sharing one rail and one chrome: animations on/off
   (html.no-anim, remembered in lab-home-motion) and the skin switch
   (html.theme-legacy, lab-home-theme). Each is a 40px square at rest and
   unfurls its own label on hover.

   Bottom-right, not bottom-left: the eye finishes a page on that side and the
   controls are found without hunting. row-reverse keeps the DOM order — and
   with it the tab order — while putting the animations switch, the one reached
   for in a hurry, in the corner itself. Because the rail is pinned by its right
   edge, an unfurling label grows the row leftwards into empty space instead of
   pushing a button off the screen. */
.corner-switches {
  position: fixed; right: 16px; bottom: 16px; z-index: 80;
  display: flex; flex-direction: row-reverse; align-items: flex-end; gap: 0;
  /* An unfurled label plus a second pill must never reach past the viewport;
     the buttons are allowed to shrink and clip their own label instead. */
  max-width: calc(100vw - 32px);
}
.corner-btn {
  display: flex; align-items: center;
  flex: 0 1 auto; min-width: 40px;
  height: 40px;
  padding: 0;
  background: rgba(13, 14, 20, 0.85);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  border: 1px solid var(--fg-a08); border-radius: 12px;
  color: var(--fg-a55); cursor: pointer;
  overflow: hidden;
  transition: color .2s, border-color .2s;
}
.corner-btn:hover,
.corner-btn:focus-visible { color: var(--fg); border-color: var(--fg-a25); }
/* With the gap closed the two buttons read as one segmented control, so only
   the outer edges of the pair are rounded and the seam in the middle is square.
   The sides look swapped because the rail is row-reverse: the first button in
   the DOM sits at the right-hand end of it. Kept away from the experimental
   skin, whose chrome is hard-cornered throughout. */
html:not(.theme-legacy) .corner-switches button { border-radius: 0; }
html:not(.theme-legacy) .corner-switches button:first-child { border-radius: 0 12px 12px 0; }
html:not(.theme-legacy) .corner-switches button:last-child { border-radius: 12px 0 0 12px; }
.motion-icon {
  display: flex; align-items: center; justify-content: center; gap: 3px;
  width: 38px; height: 38px; flex: 0 0 38px;
}
.motion-icon svg { display: block; }
.motion-icon i { width: 2.5px; height: 12px; border-radius: 1px; background: currentColor; }
/* Paused state swaps the two pause bars for a play triangle (see .no-anim
   below). The theme switch carries an svg flask in the same slot, so both the
   bars and the triangle are scoped to the animations button. */
.motion-icon::before {
  content: ""; display: none;
  width: 0; height: 0; margin-left: 3px;
  border: 7px solid transparent;
  border-right: 0;
  border-left: 11px solid var(--acid);
}
.motion-label {
  max-width: 0; opacity: 0;
  overflow: hidden; white-space: nowrap;
  font-family: var(--label);
  font-size: 10.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  transition: max-width .35s ease, opacity .25s ease, padding-right .35s ease;
}
.corner-btn:hover .motion-label,
.corner-btn:focus-visible .motion-label { max-width: 260px; opacity: 1; padding-right: 14px; }
.motion-on { display: none; }
/* Each switch reads its own state. The animations one keys off the class it
   sets, so the label is right even when the class arrives from localStorage
   before main.js runs; the theme one keys off aria-pressed, which is the same
   hook theme-legacy.css uses from its side. */
html.no-anim .motion-toggle .motion-on { display: inline; }
html.no-anim .motion-toggle .motion-off { display: none; }
html.no-anim .motion-toggle .motion-icon i { display: none; }
html.no-anim .motion-toggle .motion-icon::before { display: block; }
.theme-toggle[aria-pressed="true"] .motion-on { display: inline; }
.theme-toggle[aria-pressed="true"] .motion-off { display: none; }

/* ===== animations disabled (.no-anim on <html>, toggled by the left button of
   the rail above, and restored from lab-home-motion before the first paint).
   The accent stays on the random colour picked at load; main.js stops the
   rotation. Everything self-moving (marquee, aurora, spotlight, status dots,
   the wordmark glitch, the running lights) is off; hover states stay but snap
   instantly — the global animation/transition kill below is what makes them
   instant. The glitch layers need no rule of their own: killing the animation
   leaves them at their opacity: 0 rest state. main.js checks the same class
   before tracking the pointer across the section cards. ===== */
html.no-anim *,
html.no-anim *::before,
html.no-anim *::after {
  animation: none !important;
  transition: none !important;
}
html.no-anim { scroll-behavior: auto; }
html.no-anim .spotlight,
html.no-anim .hero-aurora,
html.no-anim .feature-row::after { display: none; }
html.no-anim .reveal { opacity: 1; transform: none; }

/* ===== custom scrollbar =====
   The native bar is hidden and replaced by an overlay thumb, and the reason is
   not decoration. This page is long and has a scrollbar; every section page
   (/apps/, /games/, …) fits the viewport and has none. A native bar takes
   layout width, so moving between the two shifted the whole page sideways by
   its width — a visible jolt on every navigation, in both directions. An
   overlay thumb takes no width at all, so both kinds of page now lay out
   identically and nothing moves.

   It is drawn, not simulated: the document still scrolls itself, with the wheel,
   the keyboard, a trackpad and the scrollbar keys all untouched. What follows
   only paints a thumb and lets it be dragged. */
.scrollbar {
  position: fixed;
  top: 0;
  right: 2px;
  bottom: 0;
  width: 10px;
  z-index: 70;          /* under the corner switches, over the page */
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}

/* Visible while scrolling, while the pointer is near it, and while dragging;
   the rest of the time it is gone. */
.scrollbar.is-visible,
.scrollbar.is-dragging { opacity: 1; pointer-events: auto; }
.scrollbar:hover { opacity: 1; pointer-events: auto; }

.scrollbar-thumb {
  position: absolute;
  right: 0;
  width: 6px;
  min-height: 32px;
  border-radius: 999px;
  background: var(--fg-a25);
  transition: background .2s ease, width .15s ease;
}

.scrollbar:hover .scrollbar-thumb,
.scrollbar.is-dragging .scrollbar-thumb {
  width: 10px;
  background: var(--fg-a55);
}

/* The document keeps its own scrolling; only the bar is ours. Firefox needs the
   standard property, WebKit the pseudo-element, and neither reserves a gutter
   afterwards — which is the entire point. */
html.has-custom-scrollbar { scrollbar-width: none; }
html.has-custom-scrollbar::-webkit-scrollbar { width: 0; height: 0; }

html.no-anim .scrollbar,
html.no-anim .scrollbar-thumb { transition: none; }

/* Coarse pointers have overlay scrollbars of their own and no hover to reveal
   ours with. */
@media (hover: none), (pointer: coarse) {
  .scrollbar { display: none; }
}
