@import url('https://fonts.googleapis.com/css2?family=Tilt+Warp&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300;1,9..40,400&family=Instrument+Serif:ital@0;1&display=swap');

:root {
  --bg: #040404;
  --bg-content: #080810;
  --surface: #0e0e1c;
  --border: #1a1a30;
  --cyan: #00f5d4;
  --magenta: #f72585;
  --amber: #ffbe0b;
  --violet: #7b2ff7;
  --text: #e4e2ee;
  --text2: #b0aec6;
  --dim: #8a87a6;
  --dark: #2a2a44;
  --display: 'Tilt Warp', 'DM Sans', -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
  --body: 'DM Sans', -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
  --serif: 'Instrument Serif', Georgia, serif;
}

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

/* Match main column + footer so subpixel gaps at the scroll end don’t reveal
   a different body color (reads as dark horizontal bands above the chrome). */
html {
  background-color: var(--bg-content);
  height: 100%;
  /* Avoid 1px horizontal “jumps” when the vertical scrollbar appears (layout shift + seams). */
  scrollbar-gutter: stable;
}
body {
  min-height: 100%;
  font-family: var(--body);
  background-color: var(--bg-content);
  color: var(--text);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* ════════════════════════════════════════
   REVEAL BACKGROUND VIDEO
   A fixed full-viewport <video> that's invisible by default. When a word's
   reward triggers it, body + html get .has-bg-video which fades the video in
   and clears the dark backgrounds so the video reads as the page bg.
   Cubes, text, dust, glow all stay layered on top.
   ════════════════════════════════════════ */
.bg-video {
  /* Anchored to the top of the document (not the viewport), so it scrolls
     with the page and disappears when you scroll past the hero. */
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  object-fit: cover;
  z-index: 0;
  opacity: 0;
  transition: opacity 1.6s ease;
  pointer-events: none;
  background: var(--bg);
}
body.has-bg-video .bg-video {
  opacity: 0.85;
  pointer-events: auto;
  cursor: pointer;
  transition: opacity 1.6s ease, filter 0.3s ease;
}
body.bg-video-paused .bg-video { filter: brightness(0.45); }

/* "Click to play" hint shown when the bg-video is user-paused. Tracks the
   bg-video's bounds (top of document, 100vh tall) so it scrolls away with
   the video. pointer-events:none so the click falls through to .bg-video. */
.bg-video-pause {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.bg-video-pause span {
  font-family: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
  font-size: clamp(0.85rem, 1vw + 0.5rem, 1.25rem);
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.92);
  text-shadow: 0 0 16px rgba(0, 0, 0, 0.85), 0 2px 8px rgba(0, 0, 0, 0.7);
}
body.bg-video-paused .bg-video-pause { opacity: 1; }

/* ════════════════════════════════════════
   PET CREATURE — spawned when 'PET' is matched. Position + rotation are set
   per-frame by JS via inline transform on .pet. The inner <img> swaps src
   based on state: fall / bounce / walk / squeeze. Drop replacement art into
   images/pet-{state}.png with the same names and it'll just work.
   ════════════════════════════════════════ */
.pet {
  position: fixed;
  top: 0;
  left: 0;
  width: 76px;
  height: 76px;
  z-index: 70;
  pointer-events: auto;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  will-change: transform;
  filter: drop-shadow(0 10px 16px rgba(0, 0, 0, 0.45));
}
.pet.is-dragging { cursor: grabbing; }
.pet img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* ════════════════════════════════════════
   PERMS — Linux CLI permission command typed out in terminal green monospace.
   Sits below MET's slot so the two never overlap if both fire.
   ════════════════════════════════════════ */
.perms-message {
  position: fixed;
  top: 22%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 70;
  pointer-events: none;
  font-family: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
  font-size: clamp(0.9rem, 1.0vw + 0.55rem, 1.35rem);
  letter-spacing: 0.03em;
  color: var(--cyan);        /* aqua, matched to site accent */
  text-shadow: 0 0 12px rgba(0, 245, 212, 0.45), 0 2px 14px rgba(0, 0, 0, 0.7);
  white-space: pre;
  opacity: 1;
  transition: opacity 1.2s ease;
}
.perms-message::after {
  content: '_';
  margin-left: 4px;
  animation: perms-cursor 0.6s steps(2, end) infinite;
}
.perms-message.is-fading { opacity: 0; }
@keyframes perms-cursor {
  0%, 50%   { opacity: 0.9; }
  51%, 100% { opacity: 0; }
}

/* ════════════════════════════════════════
   MET — cyan typewriter transmission, top-center of the page.
   ════════════════════════════════════════ */
.met-message {
  position: fixed;
  top: 22%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 70;
  pointer-events: none;
  font-family: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
  font-size: clamp(0.9rem, 1.0vw + 0.55rem, 1.35rem);
  letter-spacing: 0.03em;
  color: var(--cyan);        /* aqua, matched to site accent */
  text-shadow: 0 0 12px rgba(0, 245, 212, 0.45), 0 2px 14px rgba(0, 0, 0, 0.7);
  white-space: pre;
  opacity: 1;
  transition: opacity 1.2s ease;
}
.met-message::after {
  content: '_';
  margin-left: 4px;
  animation: met-cursor 0.6s steps(2, end) infinite;
  opacity: 0.8;
}
.met-message.is-fading { opacity: 0; }
@keyframes met-cursor {
  0%, 50%   { opacity: 0.85; }
  51%, 100% { opacity: 0; }
}

/* ════════════════════════════════════════
   MYSTERY / MYSTERYPX — full-viewport colorful flash + magenta typewriter
   hint that other words still hide in the same letters.
   ════════════════════════════════════════ */
.mystery-flash {
  position: fixed;
  inset: 0;
  z-index: 64;
  pointer-events: none;
  opacity: 0;
  background:
    linear-gradient(135deg,
      rgba(255, 70, 180, 0.55) 0%,
      rgba(255, 200, 60, 0.55) 35%,
      rgba(60, 220, 255, 0.55) 70%,
      rgba(180, 110, 255, 0.55) 100%);
  mix-blend-mode: screen;
  animation: mystery-flash 1.9s ease-out forwards;
}
.mystery-flash--grand {
  animation-duration: 2.7s;
  filter: saturate(1.25) brightness(1.08);
}
@keyframes mystery-flash {
  0%   { opacity: 0;    transform: scale(1.04); }
  8%   { opacity: 0.95; }
  30%  { opacity: 0.7; }
  100% { opacity: 0;    transform: scale(1); }
}
.mystery-hint {
  position: fixed;
  top: 22%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 71;
  pointer-events: none;
  font-family: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
  font-size: clamp(0.95rem, 1.05vw + 0.6rem, 1.4rem);
  letter-spacing: 0.04em;
  color: #ff79e0;
  text-shadow: 0 0 14px rgba(255, 121, 224, 0.5), 0 2px 14px rgba(0, 0, 0, 0.7);
  white-space: pre;
  opacity: 1;
  transition: opacity 1.2s ease;
}
.mystery-hint::after {
  content: '_';
  margin-left: 4px;
  animation: met-cursor 0.6s steps(2, end) infinite;
  opacity: 0.85;
}
.mystery-hint.is-fading { opacity: 0; }

/* ════════════════════════════════════════
   TERMS — wall of legalese typed into the hero background, behind the cubes.
   Lives inside .hero (absolute, inset:0) at z-index 2 so it stacks between
   layer-back (1) and layer-cubes (4). Same terminal-green console look as
   .met-message. pointer-events:none keeps the cubes fully interactive.
   ════════════════════════════════════════ */
.terms-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  font-family: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
  font-size: clamp(0.62rem, 0.55vw + 0.5rem, 0.88rem);
  line-height: 1.5;
  letter-spacing: 0.02em;
  color: var(--cyan);
  text-shadow: 0 0 10px rgba(0, 245, 212, 0.4), 0 1px 6px rgba(0, 0, 0, 0.7);
  padding: 4vh 6vw;
  overflow: hidden;
  opacity: 1;
  transition: opacity 1.5s ease;
}
.terms-overlay.is-fading { opacity: 0; }
.terms-overlay-text {
  white-space: pre-wrap;
  max-width: 880px;
  margin: 0 auto;
}
.terms-overlay-text::after {
  content: '_';
  margin-left: 2px;
  animation: met-cursor 0.6s steps(2, end) infinite;
  opacity: 0.85;
}

/* ════════════════════════════════════════
   PX — pixelation reward. Body filter swaps in a chunky low-res look.
   image-rendering on raster sources keeps them crisp-pixel rather than
   bilinear-filtered for the duration of the effect.
   ════════════════════════════════════════ */
body.is-pixelated {
  filter: url(#mp-pixelate);
}
body.is-pixelated img,
body.is-pixelated video,
body.is-pixelated canvas {
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
  image-rendering: crisp-edges;
}

/* ════════════════════════════════════════
   SET — sweeping stage spotlights. Each variant sweeps from one corner to
   the opposite corner via transform translate. Composited additively.
   ════════════════════════════════════════ */
.set-spotlight {
  position: fixed;
  width: 80vmax;
  height: 80vmax;
  pointer-events: none;
  z-index: 65;
  mix-blend-mode: screen;
  background: radial-gradient(
    circle,
    rgba(255, 250, 230, 0.85) 0%,
    rgba(255, 240, 215, 0.45) 22%,
    rgba(255, 230, 200, 0.18) 42%,
    transparent 65%
  );
  filter: blur(14px);
  opacity: 0;
}
.set-spotlight--tl {
  top: -50vmax; left: -50vmax;
  animation: set-sweep-tl 1.9s ease-in-out forwards;
}
.set-spotlight--tr {
  top: -50vmax; right: -50vmax;
  animation: set-sweep-tr 1.9s ease-in-out forwards;
}
.set-spotlight--bl {
  bottom: -50vmax; left: -50vmax;
  animation: set-sweep-bl 1.9s ease-in-out forwards;
}
.set-spotlight--br {
  bottom: -50vmax; right: -50vmax;
  animation: set-sweep-br 1.9s ease-in-out forwards;
}
@keyframes set-sweep-tl {
  0%   { transform: translate(0, 0);                   opacity: 0; }
  18%  {                                               opacity: 1; }
  60%  { transform: translate(50vmax, 50vmax); }
  100% { transform: translate(110vmax, 110vmax);       opacity: 0; }
}
@keyframes set-sweep-tr {
  0%   { transform: translate(0, 0);                   opacity: 0; }
  18%  {                                               opacity: 1; }
  60%  { transform: translate(-50vmax, 50vmax); }
  100% { transform: translate(-110vmax, 110vmax);      opacity: 0; }
}
@keyframes set-sweep-bl {
  0%   { transform: translate(0, 0);                   opacity: 0; }
  18%  {                                               opacity: 1; }
  60%  { transform: translate(50vmax, -50vmax); }
  100% { transform: translate(110vmax, -110vmax);      opacity: 0; }
}
@keyframes set-sweep-br {
  0%   { transform: translate(0, 0);                   opacity: 0; }
  18%  {                                               opacity: 1; }
  60%  { transform: translate(-50vmax, -50vmax); }
  100% { transform: translate(-110vmax, -110vmax);     opacity: 0; }
}

/* ════════════════════════════════════════
   PRY — watching eyes that track the cursor. Pupil position is set by JS
   via inline transform; CSS handles open/close animations and blink.
   ════════════════════════════════════════ */
.pry-eye {
  position: fixed;
  width: 96px;
  height: 64px;
  z-index: 70;
  pointer-events: auto;
  cursor: pointer;
  opacity: 0;
  animation: pry-eye-open 0.8s cubic-bezier(0.32, 1.4, 0.42, 1) forwards;
  filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.6));
}
/* Poke-to-blink: when the eye gets clicked, .is-poked retriggers a quick
   blink that overrides the ambient pry-blink animation for 420ms. */
.pry-eye.is-poked .pry-eye-sclera { animation: pry-blink-once 0.4s ease-in-out; }
@keyframes pry-blink-once {
  0%, 100% { transform: scaleY(1); }
  45%, 55% { transform: scaleY(0.05); }
}
.pry-eye-sclera {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: radial-gradient(
    ellipse,
    rgba(220, 248, 252, 0.96) 0%,
    rgba(150, 210, 230, 0.85) 60%,
    rgba(80, 160, 180, 0.7) 100%
  );
  box-shadow:
    0 0 20px rgba(0, 245, 212, 0.32),
    inset 0 0 8px rgba(0, 0, 0, 0.18);
  overflow: hidden;
  animation: pry-blink 5.8s ease-in-out infinite;
  transition: background 0.6s ease, box-shadow 0.6s ease;
}

/* STY — bloodshot/swollen stye applied to the PRY eye. The sclera tint
   shifts to inflamed pink and a small red bump (::before) sprouts on the
   upper-right of the eye and pulses gently. */
.pry-eye.is-styed .pry-eye-sclera {
  background: radial-gradient(
    ellipse at 50% 60%,
    rgba(255, 235, 225, 0.96) 0%,
    rgba(252, 200, 195, 0.92) 50%,
    rgba(225, 130, 125, 0.85) 100%
  );
  box-shadow:
    0 0 22px rgba(220, 80, 80, 0.55),
    inset 0 0 14px rgba(180, 60, 60, 0.4);
}
.pry-eye.is-styed::before {
  content: '';
  position: absolute;
  top: -10%;
  right: 22%;
  width: 22%;
  height: 30%;
  background: radial-gradient(circle at 35% 30%, #d35a5a 0%, #6f2222 80%);
  border-radius: 50%;
  box-shadow:
    0 0 8px rgba(180, 50, 50, 0.6),
    inset 1px 1px 3px rgba(255, 200, 200, 0.4);
  z-index: 5;
  pointer-events: none;
  animation: sty-pulse 2.4s ease-in-out infinite;
}
@keyframes sty-pulse {
  0%, 100% { transform: scale(1);    opacity: 1; }
  50%      { transform: scale(1.12); opacity: 0.85; }
}
.pry-eye-pupil {
  position: absolute;
  width: 36%;
  height: 56%;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #2a2a45, #0a0a18 70%);
  transform: translate(-50%, -50%);
  transition: transform 0.08s ease-out;
  box-shadow: inset 1px 1px 3px rgba(255, 255, 255, 0.25);
}
.pry-eye-pupil::after {
  content: '';
  position: absolute;
  width: 22%;
  height: 22%;
  top: 18%;
  left: 22%;
  background: rgba(255, 255, 255, 0.85);
  border-radius: 50%;
}
.pry-eye.is-closing { animation: pry-eye-close 0.8s ease forwards; }
@keyframes pry-eye-open {
  0%   { opacity: 0; transform: scale(0.4); }
  100% { opacity: 1; transform: scale(1); }
}
@keyframes pry-eye-close {
  0%   { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.4); }
}
@keyframes pry-blink {
  0%, 92%, 96%, 100% { transform: scaleY(1); }
  93%, 95%           { transform: scaleY(0.06); }
}

/* ════════════════════════════════════════
   PEST — column of ants. Each ant is a small dark dot positioned at viewport
   bottom-left; JS animates transform to march it up the left edge.
   ════════════════════════════════════════ */
.pest-ant {
  position: fixed;
  top: 0;
  left: 0;
  background: #14141f;
  border-radius: 50% 50% 60% 40% / 55% 50% 50% 45%;
  /* Subtle white outline so dark ants stay visible against dark bg too */
  box-shadow:
    0 0 0 0.5px rgba(255, 255, 255, 0.15),
    0 0 2px rgba(0, 0, 0, 0.8);
  z-index: 65;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
}

/* ════════════════════════════════════════
   YES — wave of green tint sweeps across the cubes.
   Layered on top of the per-cube flicker brightness filter.
   ════════════════════════════════════════ */
.cube.is-yes-green .cube-state {
  filter:
    brightness(var(--cube-flicker, 1))
    sepia(1)
    hue-rotate(60deg)
    saturate(2.6);
  transition: filter 0.32s ease;
}

/* PYRYTE — gold wave (sepia-base + slight negative hue-rotate gives that
   warm, metallic-yellow tint). Layered on the per-cube flicker like YES. */
.cube.is-pyryte-gold .cube-state {
  filter:
    brightness(calc(var(--cube-flicker, 1) * 1.05))
    sepia(1)
    hue-rotate(-22deg)
    saturate(3);
  transition: filter 0.32s ease;
}

/* ════════════════════════════════════════
   TREX — body-level screen shake while .is-trex-shaking is on.
   ════════════════════════════════════════ */
@keyframes trex-shake {
  0%, 100% { transform: translate(0, 0); }
  10%      { transform: translate(-4px,  3px); }
  20%      { transform: translate( 5px, -2px); }
  30%      { transform: translate(-3px, -4px); }
  40%      { transform: translate( 4px,  3px); }
  50%      { transform: translate(-5px,  2px); }
  60%      { transform: translate( 3px, -3px); }
  70%      { transform: translate(-4px,  4px); }
  80%      { transform: translate( 5px,  2px); }
  90%      { transform: translate(-2px, -3px); }
}
body.is-trex-shaking { animation: trex-shake 0.32s linear infinite; }

/* ════════════════════════════════════════
   STEM — fractal plant that grows recursively from the bottom of the screen.
   Each branch is 1/φ the length of its parent (golden-ratio falloff). Sits
   in the bg via mix-blend-mode: screen.
   ════════════════════════════════════════ */
.fractal-plant {
  /* Lives inside .hero so it scrolls with the hero (out of view past it) and
     stacks WITHIN hero's z-order — z:2 sits above the back texture (z:1) but
     below the cubes (z:4), the glow (z:7), and the scroll cue (z:8). */
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.4s ease;
  mix-blend-mode: screen;
  overflow: visible;
}
body.is-fractal-plant .fractal-plant { opacity: 0.9; }
.fractal-plant.is-fading-out {
  opacity: 0 !important;
  transition: opacity 2.6s ease;
}
.fractal-plant-path {
  fill: none;
  stroke: rgba(180, 240, 230, 0.9);
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 0.10px rgba(0, 245, 212, 0.5));
}

/* ════════════════════════════════════════
   SIMON GAME MODE — animated gradient backdrop, cube flash highlights,
   status HUD. Triggered when STEP is matched.
   ════════════════════════════════════════ */
.simon-bg {
  position: fixed;
  inset: 0;
  z-index: 9;             /* above hero contents, below content-area (z10) */
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s ease;
  /* Vivid full-saturation gradient — opacity controls how much shows through */
  background: linear-gradient(
    135deg,
    rgb(0, 245, 212) 0%,
    rgb(123, 47, 247) 25%,
    rgb(247, 37, 133) 50%,
    rgb(255, 190, 11) 75%,
    rgb(0, 245, 212) 100%
  );
  background-size: 400% 400%;
  animation: simon-bg-shift 9s linear infinite;
  /* `screen` adds the gradient as light on top of the page — keeps the
     textured backdrop visible while colorizing it strongly. */
  mix-blend-mode: screen;
  filter: saturate(1.15);
}
body.is-simon .simon-bg { opacity: 0.5; }
@keyframes simon-bg-shift {
  0%   { background-position:   0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position:   0% 50%; }
}

/* Cube highlight when computer is playing OR when user makes a correct hit */
.cube.is-simon-flash {
  filter:
    drop-shadow(0 0 32px rgba(255, 255, 255, 0.95))
    drop-shadow(0 0 14px rgba(0, 245, 212, 0.85))
    brightness(1.55) !important;
  transition: filter 0.05s ease;
}
/* Wrong-cube flash on game over */
.cube.is-simon-error {
  filter:
    drop-shadow(0 0 30px rgba(255, 80, 80, 0.95))
    drop-shadow(0 0 14px rgba(255, 60, 60, 0.7))
    brightness(1.4)
    hue-rotate(-30deg) !important;
  transition: filter 0.08s ease;
}

/* HUD — small, fixed, top-center; only visible during game */
.simon-hud {
  position: fixed;
  top: 80px;
  left: 50%;
  transform: translateX(-50%) translateY(-20px);
  z-index: 70;
  pointer-events: none;
  text-align: center;
  font-family: var(--display);
  color: #fff;
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.85);
  opacity: 0;
  transition: opacity 0.35s ease, transform 0.35s ease;
}
body.is-simon .simon-hud {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
.simon-hud-label {
  font-size: 0.92rem;
  letter-spacing: 0.36em;
  color: var(--cyan);
  text-shadow: 0 0 22px rgba(0, 245, 212, 0.65);
  margin-bottom: 8px;
}
.simon-hud-level {
  font-size: 4.6rem;
  line-height: 1;
  font-weight: 400;
  letter-spacing: 0.02em;
}
.simon-hud-status {
  font-size: 0.84rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text2);
  margin-top: 10px;
  min-height: 1em;
}

/* Suppress the hover-state image swap while in Simon mode (the hover glow
   competes with the game's flash highlights). Override of the .cube:hover
   rule lives further down the file (after that rule) for source-order to win. */
body.is-simon .cube:hover:not(.is-pressed):not(.is-flashing) .cube-state--up    { opacity: 1; }
body.is-simon .cube:hover:not(.is-pressed):not(.is-flashing) .cube-state--hover { opacity: 0; }

/* TV-static channel-change — sits in the BG layer (z0) like the video, so cubes
   and text stay on top while the bg flickers. */
.bg-static {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  opacity: 0;
  display: none;
}
.bg-static.is-on { opacity: 1; display: block; }

/* `has-bg-takeover` clears just the HERO's bg + fades its texture so the bg
   layer (static or video) shows through. The rest of the page (body,
   content-area, footer) keeps its normal dark backgrounds — that way, when
   the video scrolls out of view past the hero, the content sections below
   read as normal. */
body.has-bg-takeover .hero {
  background-color: transparent;
}
body.has-bg-takeover .layer-back {
  opacity: 0.18;
  transition: opacity 1.6s ease;
}

/* ════════════════════════════════════════
   AMBIENT BOTTOM-EDGE GLOW
   Fixed to viewport, throbs via CSS brightness, opacity scaled by scroll JS.
   z below content (10) so content covers it as it scrolls up.
   ════════════════════════════════════════ */
.hero-glow {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 42vh;
  max-height: 420px;
  pointer-events: none;
  /* Above hero vignette (z6); below content-area (z10) so content covers it on scroll. */
  z-index: 7;
  /* Faint baseline so the bottom edge is always anchored — canvas does the moving mist on top.
     Tinted to the site aqua (#00f5d4) instead of the previous neutral teal so it reads
     as vivid cyan rather than greyish fog. */
  background:
    linear-gradient(
      to top,
      rgba(0, 245, 212, 0.045) 0%,
      rgba(0, 245, 212, 0.022) 35%,
      transparent 100%
    );
  mix-blend-mode: screen;
  filter: brightness(1);
  animation: hero-glow-throb 7.8s ease-in-out infinite;
  will-change: opacity, filter;
  overflow: hidden;
}
.hero-glow canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  filter: blur(18px);
  /* Soft top fade so the canvas doesn't show a hard upper edge where it clips */
  -webkit-mask-image: linear-gradient(to top, black 18%, rgba(0,0,0,0.85) 38%, rgba(0,0,0,0.4) 70%, transparent 100%);
          mask-image: linear-gradient(to top, black 18%, rgba(0,0,0,0.85) 38%, rgba(0,0,0,0.4) 70%, transparent 100%);
}
@keyframes hero-glow-throb {
  0%, 100% { filter: brightness(0.86); }
  50%      { filter: brightness(1.06); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-glow,
  .hero-glow canvas { animation: none; }
}

/* ════════════════════════════════════════
   HERO — 3×3 INTERACTIVE CUBE GRID
   ════════════════════════════════════════ */
.hero {
  position: relative;
  height: 100vh;
  min-height: 600px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  --cue-opacity: 0.6;
  /* Mobile: JS sets from cube grid bottom; fallback until measured */
  --hero-tagline-top: 52vh;
}

.hero-layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: transform;
  --pl-x: 0px;
  --pl-y: 0px;
  transform: translate3d(var(--pl-x), var(--pl-y), 0);
}

.layer-back  { z-index: 1; pointer-events: none; }
.layer-cubes { z-index: 4; pointer-events: none; }

.layer-back img {
  /* ~3x previous scale — texture pattern reads at a more atmospheric scale.
     Hero overflow:hidden clips the overflow; image stays centered. */
  width: min(158vw, 149vh);
  height: min(158vw, 149vh);
  max-width: none;
  max-height: none;
  object-fit: contain;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}

/* ── Cube grid ─────────────────────────────── */
.cube-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 0;
  width: min(calc(56vw * 0.8), calc(58vh * 0.8));
  height: min(calc(56vw * 0.8), calc(58vh * 0.8));
  pointer-events: auto;
  /* Tilt the grid back in 3D so the cubes read as sitting on a floor
     receding from the viewer (matches the per-cube top-down perspective
     baked into each PNG). Tunable: perspective distance + rotateX angle. */
  transform: perspective(1400px) rotateX(16deg);
  transform-origin: center 65%;
  transform-style: preserve-3d;
}

.cube {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  background: transparent;
  border: 0;
  cursor: pointer;
  outline: none;
  --cube-flicker: 1;
  --cube-skew-x:        0deg;   /* lean of the TOP 75% (the body + cap) */
  --cube-skew-x-bottom: 0deg;   /* lean of the BOTTOM 25% (the front-facing rim) */
  filter: drop-shadow(0 10px 22px rgba(0, 0, 0, 0.55));
  transform-origin: center bottom;
  transition: filter 0.14s ease, transform 0.12s cubic-bezier(0.32, 1.4, 0.42, 1);
  -webkit-tap-highlight-color: transparent;
}
/* Per-column skews. Top and bottom share the seam pivot, so each shifts its OUTER
   edge while the seam stays put. For the bottom half to lean the same direction
   as the top in screen space (cube looks consistently tilted toward middle), the
   bottom skew sign is opposite the top's. Tune both freely. */
.cube[data-pos$="c0"] {            /* left col: tilted toward middle (right) */
  --cube-skew-x:        -6deg;
  --cube-skew-x-bottom:  8deg;
}
.cube[data-pos$="c2"] {            /* right col: tilted toward middle (left) */
  --cube-skew-x:         6deg;
  --cube-skew-x-bottom: -8deg;
}

/* ── Cube halves ──
   Each cube is two stacked "viewports" of the same image. The top half clips to
   the top 75% of the cube and skews; the bottom half clips to the bottom 25% and
   stays flat. Both show identical pixels at the 75% seam — no visible join. */
.cube-half {
  position: absolute;
  left: 0;
  right: 0;
  overflow: hidden;
  pointer-events: none;
}
.cube-half--top    { top: 0;   height: 75%; }
.cube-half--bottom { top: 75%; bottom: 0;   }
.cube:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 4px;
}

.cube-state {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
  opacity: 0;
  transition: opacity 0.09s ease;
  /* Dying-lightbulb flicker — driven by JS via --cube-flicker on the cube */
  filter: brightness(var(--cube-flicker, 1));
}

/* Inside each half the cube image renders at FULL cube height; the half just
   acts as a clipping window. Math: top half is 75% of cube ⇒ inner needs to be
   100/75 = 133.333% of half so the inner = full cube. Bottom half is 25% of
   cube ⇒ inner needs to be 100/25 = 400% of half, shifted up by 75%/25% = 300%
   so the cube's bottom 25% lands inside the half's visible region. */
.cube-half--top .cube-state {
  inset: auto;
  top: 0;
  left: 0;
  width: 100%;
  height: 133.333%;
  transform: skewX(var(--cube-skew-x));
  transform-origin: center 75%;   /* pivot at the seam so it stays put under skew */
}
.cube-half--bottom .cube-state {
  inset: auto;
  top: -300%;
  left: 0;
  width: 100%;
  height: 400%;
  transform: skewX(var(--cube-skew-x-bottom));
  transform-origin: center 75%;   /* same seam pivot as top half so they stay aligned */
}
.cube-state--up { opacity: 1; }

/* Hover — swap to the hover-state PNG (which has its own glow baked in). No CSS glow added. */
.cube:hover:not(.is-pressed):not(.is-flashing) .cube-state--up    { opacity: 0; }
.cube:hover:not(.is-pressed):not(.is-flashing) .cube-state--hover { opacity: 1; }

/* Touch / no-hover devices: :hover can latch on tap and stick the hover PNG.
   Placed AFTER the hover rule above so source order wins. */
@media (hover: none) {
  .cube:hover:not(.is-pressed):not(.is-flashing) .cube-state--up    { opacity: 1; }
  .cube:hover:not(.is-pressed):not(.is-flashing) .cube-state--hover { opacity: 0; }
}

/* Pressed — show pressed state, push down, soften shadow */
.cube.is-pressed .cube-state--up      { opacity: 0; }
.cube.is-pressed .cube-state--hover   { opacity: 0; }
.cube.is-pressed .cube-state--pressed { opacity: 1; }
.cube.is-pressed {
  transform: translateY(3px) scale(0.985);
  filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.55));
}

/* Releasing (the all-9 fail animation) — pop back up briefly brighter */
.cube.is-releasing {
  transform: translateY(-2px) scale(1.01);
  transition: transform 0.18s ease, filter 0.18s ease;
  filter:
    drop-shadow(0 14px 28px rgba(0, 0, 0, 0.55))
    drop-shadow(0 0 18px rgba(255, 255, 255, 0.25));
}

/* Match flash — all cubes briefly bloom together */
.cube.is-flashing {
  filter:
    drop-shadow(0 0 36px rgba(255, 255, 255, 0.85))
    drop-shadow(0 0 14px rgba(0, 245, 212, 0.7))
    brightness(1.45);
  animation: cube-flash 0.7s ease-out;
}
@keyframes cube-flash {
  0%   { filter: drop-shadow(0 0 0   rgba(255,255,255,0))   drop-shadow(0 0 0 rgba(0,245,212,0)) brightness(1); }
  18%  { filter: drop-shadow(0 0 50px rgba(255,255,255,1))  drop-shadow(0 0 22px rgba(0,245,212,0.9)) brightness(1.65); }
  60%  { filter: drop-shadow(0 0 28px rgba(255,255,255,0.6)) drop-shadow(0 0 14px rgba(0,245,212,0.55)) brightness(1.25); }
  100% { filter: drop-shadow(0 0 0   rgba(255,255,255,0))   drop-shadow(0 0 0 rgba(0,245,212,0)) brightness(1); }
}

/* Konami easter egg — full-grid cyan bloom */
.cube.konami-glow {
  filter:
    drop-shadow(0 0 50px rgba(0, 245, 212, 0.9))
    drop-shadow(0 0 16px rgba(0, 245, 212, 0.7))
    brightness(1.4);
  transition: filter 0.08s ease;
}
.cube.konami-fadeout {
  transition: filter 1.5s ease;
  filter: drop-shadow(0 10px 22px rgba(0, 0, 0, 0.55));
}

/* TYRE — outer 8 cubes orbit the centre E for a full 360°. Transform is
   set inline by JS each frame, so this rule just disables hover transforms
   / pointer events / transitions for the duration so nothing fights the
   inline transform. z-index lift keeps them above the centre cube while
   passing through its column/row during the spin. */
.cube.is-tyre-spinning {
  pointer-events: none;
  transition: none;
  z-index: 6;
}

/* EMPTY — cubes fall off-screen with gravity, gap, then drop back from above
   and bounce-settle into place. Per-cube CSS variables randomize horizontal
   drift + spin so the tumble doesn't look uniform. */
.cube.is-empty-falling-away {
  animation: empty-fall-away 850ms cubic-bezier(0.55, 0, 0.85, 0.4) forwards;
  pointer-events: none;
  z-index: 5;
}
@keyframes empty-fall-away {
  0%   { transform: translate(0, 0) rotate(0deg); }
  100% { transform: translate(var(--empty-drift-x, 0px), 130vh) rotate(var(--empty-spin, 18deg)); }
}
.cube.is-empty-falling-in {
  /* fill-mode "both" applies the 0% keyframe (off-screen above) during the
     staggered animation-delay too — without this, cubes briefly snap back
     to their grid position before falling in. */
  animation: empty-fall-in 1100ms both;
  pointer-events: none;
  z-index: 5;
}
@keyframes empty-fall-in {
  0% {
    transform: translate(var(--empty-drift-in-x, 0px), -120vh) rotate(var(--empty-spin-in, -10deg));
    animation-timing-function: cubic-bezier(0.55, 0, 0.85, 0.4);
  }
  60% {
    transform: translate(0, 0) rotate(0deg);
    animation-timing-function: cubic-bezier(0.34, 0, 0.64, 0.66);
  }
  72% {
    transform: translate(0, -18px) rotate(0deg);
    animation-timing-function: cubic-bezier(0.55, 0, 0.85, 0.4);
  }
  84% {
    transform: translate(0, 0) rotate(0deg);
    animation-timing-function: cubic-bezier(0.34, 0, 0.64, 0.66);
  }
  92% {
    transform: translate(0, -6px) rotate(0deg);
    animation-timing-function: cubic-bezier(0.55, 0, 0.85, 0.4);
  }
  100% { transform: translate(0, 0) rotate(0deg); }
}

/* PREY — cubes lean/recoil away from the cursor. JS rAF loop sets the
   --prey-* vars per frame; the short transition smooths jitter without
   adding visible lag. Defined after .is-pressed so its transform wins
   when both classes are present mid-recoil. */
.cube.is-prey-recoil {
  transform: translate(var(--prey-tx, 0), var(--prey-ty, 0)) rotate(var(--prey-rot, 0));
  transition: transform 90ms linear;
}

/* Vignette + bottom fade — canvas with noise dither to eliminate banding */
.hero-vignette {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
}
.hero-vignette canvas {
  width: 100%;
  height: 100%;
  display: block;
}

/* Bottom stack: mobile shows tagline + scroll; desktop tagline hidden (see intro). */
.hero-ui {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 8;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: clamp(10px, 2vmin, 20px);
  padding-bottom: max(clamp(20px, 4vmin, 36px), env(safe-area-inset-bottom, 0px));
  pointer-events: none;
}
/* Tagline sits just under the cube grid on every viewport. Position is
   driven by --hero-tagline-top, which JS sets from the grid's bottom edge.
   Fades out the first time a cube word fires (.is-secrets-revealed on .hero). */
.hero-tagline {
  display: block;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: var(--hero-tagline-top);
  z-index: 8;
  margin: 0;
  pointer-events: none;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.05rem, 1.2vmin + 0.85rem, 1.85rem);
  letter-spacing: clamp(0.26em, 0.55vmin, 0.4em);
  text-transform: uppercase;
  color: var(--cyan);
  line-height: 1.3;
  max-width: 92vw;
  text-align: center;
  text-shadow: 0 0 18px rgba(0, 245, 212, 0.35), 0 2px 20px rgba(0, 0, 0, 0.75);
  transition: opacity 0.9s ease;
}
.hero.is-secrets-revealed .hero-tagline { opacity: 0; }
body.is-simon .hero-tagline { display: none; }
/* When the tagline is also a glitch-hint, keep its display font, glow, and
   wide tracking — the global .glitch-hint rule otherwise switches it to
   Menlo and shortens the shadow. Pointer events back on so hover/tap can
   land. */
.hero-tagline.glitch-hint {
  font-family: var(--display);
  letter-spacing: clamp(0.26em, 0.55vmin, 0.4em);
  pointer-events: auto;
  text-shadow: 0 0 18px rgba(0, 245, 212, 0.35), 0 2px 20px rgba(0, 0, 0, 0.75);
}
.hero-tagline.glitch-hint .glitch-ch {
  /* Display font is variable-width, so .glitch-ch min-width: 1ch isn't
     enough — JS measures and sets explicit widths after build. The rule
     below just suppresses min-width so the JS-set width is authoritative. */
  min-width: 0;
}
/* The duplicate "We build secrets" in the intro section is no longer needed
   on any viewport — the hero tagline now serves that role across the page. */
.intro .tagline--below-fold { display: none; }
.scroll-cue {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 1.2vmin, 12px);
  opacity: var(--cue-opacity);
}
.scroll-cue span {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.05rem, 1.6vmin + 0.75rem, 1.85rem);
  letter-spacing: clamp(0.22em, 0.5vmin, 0.36em);
  text-transform: uppercase;
  color: var(--cyan);
  text-shadow: 0 0 14px rgba(0, 245, 212, 0.35);
}
.scroll-cue .arr {
  width: clamp(10px, 1.4vmin, 18px);
  height: clamp(10px, 1.4vmin, 18px);
  border-right: clamp(1.5px, 0.2vmin, 2.5px) solid var(--cyan);
  border-bottom: clamp(1.5px, 0.2vmin, 2.5px) solid var(--cyan);
  transform: rotate(45deg);
  animation: bob 2.5s ease-in-out infinite;
}
@keyframes bob {
  0%,100% { transform: rotate(45deg) translateY(0); opacity: .3; }
  50%     { transform: rotate(45deg) translateY(4px); opacity: .8; }
}

/* ════════════════════════════════════════
   TRANSITION + CONTENT
   ════════════════════════════════════════ */
.transition-zone {
  position: relative;
  z-index: 10;
  /* Solid base — long vertical gradients between near-black hexes read as horizontal banding
     that shifts while scrolling on many displays. Short top blend only, then flat fill. */
  background-color: var(--bg-content);
  background-image: linear-gradient(to bottom, var(--bg) 0%, var(--bg-content) 100%);
  background-repeat: no-repeat;
  background-size: 100% min(160px, 28vh);
  background-position: top center;
  padding: 70px 0 0;
}

.content-area {
  background: var(--bg-content);
  position: relative;
  z-index: 10;
}

.wrap {
  max-width: 960px;
  margin: 0 auto;
  padding: 0 40px;
}

/* ─── Intro ─── */
.intro {
  text-align: center;
  padding: 0 0 80px;
  opacity: 0; transform: translateY(16px);
  transition: opacity 0.8s ease, transform 0.8s ease;
}
.intro.show { opacity: 1; transform: translateY(0); }

.intro .tagline {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.2rem, 4.2vw, 2.2rem);
  letter-spacing: clamp(0.28em, 1.2vw, 0.42em);
  text-transform: uppercase;
  color: var(--cyan);
  text-shadow: 0 0 18px rgba(0, 245, 212, 0.3);
  margin: 0 0 20px;
  line-height: 1.35;
}
.intro .rule {
  display: block;
  width: 72px; height: 1px;
  background: var(--border);
  margin: 0 auto;
  border: none;
}

/* ─── Pitch block — sits below the intro logo rail. Spans the full .wrap
   width so it lines up with the marquee above; body copy is hand-clamped
   so it stays readable even when the eyebrow goes wide. ─── */
.pitch {
  margin: 32px auto 36px;
  text-align: left;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
}
/* Eyebrow matches the WE BUILD SECRETS tagline: same display font, cyan,
   uppercase, wide letter-spacing, glow. */
.pitch-eyebrow {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.2rem, 4.2vw, 2.2rem);
  letter-spacing: clamp(0.28em, 1.2vw, 0.42em);
  text-transform: uppercase;
  color: var(--cyan);
  text-shadow: 0 0 18px rgba(0, 245, 212, 0.3);
  line-height: 1.35;
  margin: 0;
}
.pitch-lede {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(0.78rem, 0.4vw + 0.62rem, 0.95rem);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text2);
  margin: 0;
}
.pitch-lede strong { color: var(--text); font-weight: 400; }
.pitch-lede a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--border);
  transition: color .2s, border-color .2s;
}
.pitch-lede a:hover { color: var(--cyan); border-color: var(--cyan); }
.pitch-body {
  font-size: clamp(1.02rem, 0.35vw + 0.95rem, 1.12rem);
  line-height: 1.85;
  color: var(--text2);
  margin: 0;
  max-width: 700px;
}
.pitch-foot {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(0.7rem, 0.35vw + 0.55rem, 0.82rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 4px 0 0;
}
.pitch-mail {
  color: var(--cyan);
  text-decoration: none;
  border-bottom: 1px solid rgba(0, 245, 212, 0.35);
  transition: border-color .2s, text-shadow .2s;
}
.pitch-mail:hover {
  border-color: var(--cyan);
  text-shadow: 0 0 12px rgba(0, 245, 212, 0.45);
}

/* ─── Sections ─── */
.sec {
  padding: 70px 0;
  opacity: 0; transform: translateY(18px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.sec.show { opacity: 1; transform: translateY(0); }

/* ─── Section background gallery ───
   Reusable: any section can opt in by adding `.sec--with-gallery` and a
   `.sec-gallery > .sec-gallery-img[]` block, plus wrapping its text in
   `.sec-content`. JS auto-crossfades while the section is in view. */
.sec--with-gallery { position: relative; }
/* The header wrapper is the gallery's positioning context, so the gallery
   only spans the height of the lbl/h2/prose block — not any tags, logos,
   stats, cards, or steps that live as siblings below. */
.sec-header { position: relative; }
.sec-gallery {
  position: absolute;
  top: 0;
  bottom: 0;
  /* Start 15% in from the section's left edge — the video sits partially
     behind the text column. The .sec-gallery-fade gradient on top of the
     video keeps the overlapping portion readable while the right plays
     clear and bleeds off the page.
     --mp-gallery-offset (set per gallery in galleries.js) shifts both
     edges in lockstep so the box keeps the same width but slides left
     (negative) or right (positive). Default 0px. */
  left: calc(15% + var(--mp-gallery-offset, 0px));
  right: calc(50% - 50vw - var(--mp-gallery-offset, 0px));
  pointer-events: none;
  z-index: 0;
  /* overflow visible on desktop so the video element can bleed past the
     right edge of the gallery (the videos are scaled to fit the gallery's
     height and extend at their natural aspect — the right side of the clip
     ends up off-page). body { overflow-x: hidden } prevents a scrollbar.
     Mobile re-clips the overflow further down. */
  overflow: visible;
}
/* Narrow column: text stops at the gallery edge so the video has room.
   Set per-section in galleries.js via the `narrow` flag (true by default). */
.sec--with-gallery--narrow .sec-content { max-width: 54%; }
.sec-gallery-item {
  position: absolute;
  top: 0;
  bottom: 0;
  /* Horizontal alignment of the video inside the gallery box. The gallery
     sets --mp-gallery-align (0..100) per galleries.js; left + translate
     match that percentage so the video's corresponding edge meets the
     gallery's (0 = left edges meet, 50 = centred, 100 = right edges meet).
     Default 0 for any gallery that didn't set the var. */
  left: calc(var(--mp-gallery-align, 0) * 1%);
  transform: translateX(calc(var(--mp-gallery-align, 0) * -1%));
  /* Height fills the gallery; width tracks the video's natural aspect.
     Whatever extends past the gallery on either side bleeds into the
     gutter and is clipped at the viewport. */
  height: 100%;
  width: auto;
  max-width: none;
  opacity: 0;
  transition: opacity 1.6s ease-in-out;
  user-select: none;
  -webkit-user-drag: none;
  z-index: 1;

  /* Edge fade as a CSS mask on the video element. Putting the mask here
     (not on .sec-gallery-fade) means it tracks the video's actual
     rendered bounds — so the right-edge fade always lands on the clip,
     even when the video's natural width doesn't reach the gallery's
     right edge. Inputs:
       --mp-gallery-fade-width        (default 100%) — left ramp end
       --mp-gallery-fade-right-width  (default 0%)   — right ramp width
     The left ramp is clamped via min() so it can't overrun the right
     ramp's start when the two widths sum to more than 100%. */
  --_mp-fade-l: min(
    var(--mp-gallery-fade-width, 100%),
    calc(100% - var(--mp-gallery-fade-right-width, 0%))
  );
  --_mp-fade-r: var(--mp-gallery-fade-right-width, 0%);
  -webkit-mask-image: linear-gradient(
    to right,
    rgba(0, 0, 0, 0)    0%,
    rgba(0, 0, 0, 0.22) calc(var(--_mp-fade-l) * 0.25),
    rgba(0, 0, 0, 0.45) calc(var(--_mp-fade-l) * 0.5),
    rgba(0, 0, 0, 0.78) calc(var(--_mp-fade-l) * 0.75),
    rgba(0, 0, 0, 1)    var(--_mp-fade-l),
    rgba(0, 0, 0, 1)    calc(100% - var(--_mp-fade-r)),
    rgba(0, 0, 0, 0.78) calc(100% - var(--_mp-fade-r) * 0.75),
    rgba(0, 0, 0, 0.45) calc(100% - var(--_mp-fade-r) * 0.5),
    rgba(0, 0, 0, 0.22) calc(100% - var(--_mp-fade-r) * 0.25),
    rgba(0, 0, 0, 0)    100%
  );
          mask-image: linear-gradient(
    to right,
    rgba(0, 0, 0, 0)    0%,
    rgba(0, 0, 0, 0.22) calc(var(--_mp-fade-l) * 0.25),
    rgba(0, 0, 0, 0.45) calc(var(--_mp-fade-l) * 0.5),
    rgba(0, 0, 0, 0.78) calc(var(--_mp-fade-l) * 0.75),
    rgba(0, 0, 0, 1)    var(--_mp-fade-l),
    rgba(0, 0, 0, 1)    calc(100% - var(--_mp-fade-r)),
    rgba(0, 0, 0, 0.78) calc(100% - var(--_mp-fade-r) * 0.75),
    rgba(0, 0, 0, 0.45) calc(100% - var(--_mp-fade-r) * 0.5),
    rgba(0, 0, 0, 0.22) calc(100% - var(--_mp-fade-r) * 0.25),
    rgba(0, 0, 0, 0)    100%
  );
}
.sec-gallery-item.is-active {
  opacity: 1;
  pointer-events: auto;
  cursor: pointer;
  transition: opacity 1.6s ease-in-out, filter 0.3s ease;
}
.sec-gallery.is-paused .sec-gallery-item.is-active { filter: brightness(0.45); }

/* "Click to play" hint, shown when the gallery is user-paused. Sits over
   the gallery box; pointer-events:none lets the click fall through to the
   active video item, which owns the toggle handler. */
.sec-gallery-pause {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.sec-gallery-pause span {
  /* Pinned to the active video's centre via --mp-gallery-pause-anchor
     (set in JS each time the active item changes or the viewport
     resizes). The video's rendered position is decoupled from viewport
     width — only depends on gallery height + video aspect ratio — so
     once anchored the hint tracks the video 1:1 regardless of how wide
     the browser is. `pauseOffset` from galleries.js still applies as a
     final translateX nudge for fine-tuning per section. */
  position: absolute;
  top: 50%;
  left: var(--mp-gallery-pause-anchor, 50%);
  transform: translate(-50%, -50%) translateX(var(--mp-gallery-pause-offset, 0px));
  white-space: nowrap;
  font-family: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
  font-size: clamp(0.78rem, 0.85vw + 0.45rem, 1.05rem);
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.92);
  text-shadow: 0 0 14px rgba(0, 0, 0, 0.85), 0 2px 8px rgba(0, 0, 0, 0.7);
}
.sec-gallery.is-paused .sec-gallery-pause { opacity: 1; }

/* Left-edge dark mask: fades from a moderately-dark veil on the left (under
   the text) all the way to transparent on the right. Tuned so the video is
   still visible-as-shadow behind the text rather than fully blacked out. */
/* Legacy overlay element — kept in the DOM so existing JS that adds it
   doesn't need to change, but the actual edge fade now lives on the
   .sec-gallery-item via mask-image (which follows the video's real
   rendered bounds, not the gallery box). This element is now a no-op. */
.sec-gallery-fade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  background: transparent;
}
.sec-content {
  position: relative;
  z-index: 1;
}

.lbl {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(0.72rem, 0.9vw + 0.48rem, 1.05rem);
  letter-spacing: clamp(0.2em, 0.45vw, 0.3em);
  text-transform: uppercase; color: var(--cyan);
  margin-bottom: 18px;
  display: flex; align-items: center; gap: 12px;
}
.lbl::before {
  content: ''; width: 6px; height: 6px;
  flex-shrink: 0;
  background: var(--cyan);
  box-shadow: 0 0 8px var(--cyan);
}

/* Serif section titles — same scale in content blocks and closing CTA */
.sec h2,
.cta h2 {
  font-family: var(--serif);
  font-size: clamp(1.85rem, 4.8vw, 3.15rem);
  font-weight: 400;
  font-style: italic;
  line-height: 1.28;
}
.sec h2 { margin-bottom: 22px; }

/* Primary body copy — sections, cards, process steps */
.prose,
.card p,
.step p {
  font-size: clamp(1.02rem, 0.35vw + 0.95rem, 1.12rem);
  line-height: 1.85;
  color: var(--text2);
}
.prose { max-width: 640px; }
.prose strong { color: var(--text); font-weight: 600; }
.prose a {
  color: var(--text2);
  text-decoration: none;
  border-bottom: 1px solid var(--border);
  transition: color .2s, border-color .2s;
}
.prose a:hover { color: var(--text); border-color: var(--dim); }
.prose strong a {
  color: var(--text);
  border-bottom-color: var(--border);
}
.prose strong a:hover { color: var(--cyan); border-color: var(--cyan); }
.prose + .prose { margin-top: 16px; }

/* ─── Cards ─── */
/* Capabilities row: flex so partial rows (e.g. 5 cards → 3 + 2) center the
   wrap row instead of left-aligning. Width is capped to ~1/3 minus the gap
   so the layout reads as a 3-per-row grid on desktop. */
.cards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 14px;
  margin-top: 30px;
}
.cards > .card {
  flex: 0 1 calc((100% - 28px) / 3);
  max-width: calc((100% - 28px) / 3);
  min-width: 0;
}
.card {
  background: var(--surface); border: 1px solid var(--border);
  padding: 24px 20px; position: relative; overflow: hidden;
  transition: border-color .25s, box-shadow .25s;
  box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}
/* Hover uses shadow only — avoid transform here so layer promotion doesn’t
   repaint the whole page and fight the logo-rail transform animation (flicker bar). */
.card:hover {
  border-color: var(--cyan);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(0, 245, 212, 0.12);
}
.card::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
}
.card:nth-child(1)::before { background: var(--cyan); }
.card:nth-child(2)::before { background: var(--magenta); }
.card:nth-child(3)::before { background: var(--amber); }
.card:nth-child(4)::before { background: var(--violet); }
.card:nth-child(5)::before { background: var(--cyan); }

/* Capabilities cards: replace the cursor with the card's emoji while
   hovering. SVG-in-data-URL renders the emoji as text; the trailing
   "pointer" is the fallback for browsers that refuse the URL cursor. */
.cards:not(.cards--beliefs) > .card:nth-child(1) {
  cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><text y='26' font-size='26'>🎮</text></svg>") 12 16, pointer;
}
.cards:not(.cards--beliefs) > .card:nth-child(2) {
  cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><text y='26' font-size='26'>🧩</text></svg>") 12 16, pointer;
}
.cards:not(.cards--beliefs) > .card:nth-child(3) {
  cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><text y='26' font-size='26'>🚀</text></svg>") 12 16, pointer;
}
.cards:not(.cards--beliefs) > .card:nth-child(4) {
  cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><text y='26' font-size='26'>📦</text></svg>") 12 16, pointer;
}
.cards:not(.cards--beliefs) > .card:nth-child(5) {
  cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><text y='26' font-size='26'>✨</text></svg>") 12 16, pointer;
}
/* Beliefs: fixed 2×2 on every viewport — overrides the .cards flex layout. */
.cards--beliefs {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}
.cards--beliefs > .card {
  flex: unset;
  max-width: none;
}
.card .ico {
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
  font-size: clamp(1.65rem, 3vw, 2.1rem);
  margin-bottom: 12px; display: block; line-height: 1;
}
/* Pixel subheads — service cards, ethos cards */
.card h3 {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(0.72rem, 0.55vw + 0.58rem, 0.95rem);
  margin-bottom: 8px;
  line-height: 1.45;
}
/* Process step heading reads as a continuation of the number: same font,
   same size, same cyan, same line-height. The number lives in .step-n. */
.step h4 {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.3rem;
  color: var(--cyan);
  margin-bottom: 10px;
  line-height: 1;
}
/* ─── Tags ─── */
.tags { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; }
.tag {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(0.58rem, 0.35vw + 0.48rem, 0.78rem);
  letter-spacing: .1em; text-transform: uppercase;
  padding: 8px 16px; border: 1px solid var(--border); color: var(--dim);
  transition: border-color .2s, color .2s;
}
.tag:hover { border-color: var(--cyan); color: var(--text2); }

/* ─── Logo rail ─── */
.logo-rail-wrap {
  margin-top: 26px;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  background: transparent;
  overflow: hidden;
  contain: paint;
  isolation: isolate;
  transform: translateZ(0);
}
.logo-rail {
  display: flex;
  width: max-content;
  animation: logo-scroll 24s linear infinite;
  padding: 24px 0;
  backface-visibility: hidden;
}
/* Top intro: marquee moves left (default). Bottom section: reverse = moves right. */
.logo-rail--right {
  animation-duration: 30s;
  animation-direction: reverse;
}
.logo-rail-wrap:hover .logo-rail { animation-play-state: paused; }
.logo-rail-wrap--intro {
  margin-top: 12px;
  margin-bottom: 28px;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.logo-group {
  display: flex;
  align-items: center;
  gap: 14px;
  padding-right: 14px;
}
/* Equal-width columns so the flex gap reads as even space between marks. */
.logo-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 clamp(150px, 18vw, 228px);
  width: clamp(150px, 18vw, 228px);
  box-sizing: border-box;
  padding: 0 4px;
}
.logo-img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: clamp(48px, 8.25vmin, 69px);
  object-fit: contain;
  object-position: center;
}
/* Flatten all marks to pure white on the dark rail */
.logo-img--si,
.logo-img--wm,
.logo-img--wordmark {
  filter: brightness(0) invert(1);
  opacity: 1;
}
/* Hand wordmarks: tight SVG crop + wider slot so they read larger than icon marks. */
.logo-slot--wordmark {
  flex: 0 0 clamp(198px, 30vw, 390px);
  width: clamp(198px, 30vw, 390px);
  padding: 0 4px;
}
.logo-img--wordmark {
  max-height: clamp(51px, 9.75vmin, 81px);
}
@keyframes logo-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .logo-rail { animation: none; }
}

/* ─── Stats ─── */
.stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px; margin-top: 28px;
}
.stat {
  text-align: center; padding: 22px 10px;
  border: 1px solid var(--border); background: var(--surface);
}
.stat-n {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.6rem;
  color: var(--cyan);
  margin-bottom: 6px;
}
.stat-l { font-size: .7rem; color: var(--dim); letter-spacing: .06em; text-transform: uppercase; }

/* ─── Steps ─── */
.steps { margin-top: 28px; display: flex; flex-direction: column; gap: 20px; }
.step { display: flex; gap: 18px; align-items: flex-start; }
.step-n {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.3rem;
  color: var(--cyan);
  flex-shrink: 0; width: 36px; text-align: right; line-height: 1;
}
/* ════════════════════════════════════════
   TESTIMONIALS
   Two presentations, one config (testimonials.js → MP_TESTIMONIALS):
     1. .hero-testimonials — small fading rotator at the top of the hero.
        CSS grid stacks all items in the same cell; the .is-active item
        fades in over the others. .hero.is-secrets-revealed dims it out
        for the rest of the session, matching the tagline behaviour.
     2. .sec--testimonials — capsule grid lower in the page. Each card
        uses --mp-i (set in JS) to stagger its transition-delay, so they
        cascade in once .is-revealed is added by the IntersectionObserver.
   ════════════════════════════════════════ */
.hero-testimonials {
  position: absolute;
  /* JS sets --hero-testimonials-mid each frame from the distance between
     the hero's top and the cube grid's top (px). Combined with
     translate(-50%, -50%) below this vertically centres the rotator in
     the band above the keypad on every viewport, mobile and desktop.
     Fallback applies before the first measurement lands. */
  top: var(--hero-testimonials-mid, 18vh);
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(90vw, 720px);
  /* Above the .hero-vignette canvas (z-index: 6) so the quote stays
     readable instead of being washed out by the bottom fade. */
  z-index: 7;
  display: grid;
  grid-template-areas: "stack";
  /* Stack all items in the same cell AND vertically centre each one
     within it. Without align-items:center, a short testimonial sits at
     the top of a cell sized for the tallest one — which makes the active
     item look pinned to the top of the band even when the container
     itself is correctly centred. */
  align-items: center;
  text-align: center;
  pointer-events: auto;
  opacity: 1;
  transition: opacity 0.9s ease;
}
/* Hide while the user is interacting with the cube keypad; fade back in
   after the idle timeout (handled in JS by toggling .is-keypad-active). */
.hero.is-keypad-active .hero-testimonials,
/* Stay hidden for the entire Simon round — the page is busy enough. */
body.is-simon .hero-testimonials {
  opacity: 0;
  pointer-events: none;
}
.hero-testimonials-item {
  grid-area: stack;
  opacity: 0;
  transition: opacity 1.2s ease;
  pointer-events: none;
}
.hero-testimonials-item.is-active {
  opacity: 1;
  pointer-events: auto;
}
.hero-testimonials .testimonial-quote {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(1.25rem, 1vw + 0.9rem, 1.75rem);
  line-height: 1.4;
  color: var(--text);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.7);
}
.hero-testimonials .testimonial-cite {
  margin-top: 10px;
  font-family: var(--display);
  font-size: clamp(0.7rem, 0.45vw + 0.55rem, 0.85rem);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text2);
}
.hero-testimonials .testimonial-cite a {
  color: var(--cyan);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s ease;
}
.hero-testimonials .testimonial-cite a:hover { border-bottom-color: var(--cyan); }

.sec--testimonials { padding-top: 12px; }
.testimonials-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 18px;
  margin-top: 30px;
}
.testimonial-capsule {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 26px 24px;
  position: relative;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.7s ease, transform 0.7s ease, border-color 0.25s ease, box-shadow 0.25s ease;
  /* JS sets --mp-i on each card; the cascade staggers on reveal. */
  transition-delay: calc(var(--mp-i, 0) * 90ms);
}
.sec--testimonials.is-revealed .testimonial-capsule {
  opacity: 1;
  transform: translateY(0);
}
.testimonial-capsule:hover {
  border-color: var(--cyan);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(0, 245, 212, 0.12);
}
.testimonial-capsule .testimonial-quote {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(1rem, 0.6vw + 0.75rem, 1.2rem);
  line-height: 1.55;
  color: var(--text);
}
.testimonial-capsule .testimonial-quote::before { content: "\201C"; }
.testimonial-capsule .testimonial-quote::after  { content: "\201D"; }
.testimonial-capsule .testimonial-cite {
  margin-top: 18px;
  font-family: var(--display);
  font-size: 0.78rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text2);
}
.testimonial-capsule .testimonial-cite a {
  color: var(--cyan);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s ease;
}
.testimonial-capsule .testimonial-cite a:hover { border-bottom-color: var(--cyan); }

@media (max-width: 720px) {
  /* Width-only override on mobile — vertical position is JS-driven via
     --hero-testimonials-mid so the rotator stays vertically centred in
     the band above the keypad on any phone height. */
  .hero-testimonials { width: 92vw; }
  /* Tighter wrapping on mobile so a long quote takes fewer lines and
     leaves room above the cube grid. */
  .hero-testimonials .testimonial-quote { line-height: 1.25; }
  /* Capsule quotes read small at the desktop scale on a phone — bump
     them up for the standalone-card presentation only (top rotator
     keeps its own scaling above). */
  .testimonial-capsule .testimonial-quote {
    font-size: clamp(1.15rem, 4vw, 1.4rem);
    line-height: 1.5;
  }
  /* Push the keypad noticeably lower on mobile so the overlays (MET, PERMS,
     Simon HUD) and the testimonial rotator have room above it without
     bumping into the cube faces. align-items:center on .layer-cubes
     means margin-top here just shifts the centered grid downward. */
  .cube-grid { margin-top: 18vh; }
}

/* ─── Divider ─── */
.divider {
  width: 40px; height: 1px;
  background: var(--border);
  margin: 0 auto;
  border: none;
}

/* ─── CTA ─── */
.cta {
  text-align: center;
  padding: 72px 0 48px;
  opacity: 0; transform: translateY(16px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.cta.show { opacity: 1; transform: translateY(0); }
.cta h2 { margin-bottom: 16px; }
.cta .sub {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.2rem, 4.2vw, 2.2rem);
  letter-spacing: clamp(0.06em, 0.5vw, 0.14em);
  color: var(--text2);
  line-height: 1.35;
  margin-bottom: 28px;
}
.cta-email {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  max-width: 100%;
  padding: 22px 32px 24px;
  border: 2px solid var(--cyan);
  color: var(--cyan);
  background: transparent;
  text-decoration: none;
  transition: background .25s, color .25s, box-shadow .25s, border-color .25s;
  box-shadow: 0 0 0 rgba(0,245,212,0);
}
.cta-email-prefix {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(0.68rem, 0.4vw + 0.55rem, 0.88rem);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text2);
  transition: color .25s;
}
.cta-email-addr {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.2rem, 3.8vw, 1.85rem);
  letter-spacing: 0.04em;
  color: var(--cyan);
  line-height: 1.25;
  text-align: center;
  word-break: break-word;
  transition: color .25s;
}
.cta-email:hover {
  background: var(--cyan);
  box-shadow: 0 0 28px rgba(0,245,212,.15);
}
.cta-email:hover .cta-email-prefix,
.cta-email:hover .cta-email-addr {
  color: var(--bg);
}
.cta-prose {
  max-width: 640px;
  margin: 0 auto 28px;
  text-align: center;
}
.cta-note {
  margin-top: 22px;
  font-family: var(--display);
  font-size: clamp(0.68rem, 0.4vw + 0.55rem, 0.82rem);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--dim);
}

/* ─── Footer ─── */
footer {
  padding: 44px 0 36px;
  border-top: 1px solid var(--border);
  text-align: center;
  position: relative; z-index: 10;
  background: var(--bg-content);
}
.footer-mark {
  display: flex;
  justify-content: center;
  margin-bottom: 16px;
}
.footer-mark-grid {
  display: grid;
  grid-template-columns: repeat(3, auto);
  grid-template-rows: repeat(3, auto);
  gap: 0.55rem 0.75rem;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(0.72rem, 1.1vw + 0.52rem, 1.12rem);
  line-height: 1;
  letter-spacing: 0.06em;
  color: var(--text2);
}
.footer-mark-grid span {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 1.35em;
  min-height: 1.4em;
  padding: 0.2em 0.15em;
}
.fc {
  font-size: clamp(0.75rem, 0.45vw + 0.62rem, 0.95rem);
  color: var(--dark);
  line-height: 1.55;
}
.fc a {
  color: var(--dim); text-decoration: none;
  border-bottom: 1px solid var(--border);
  transition: color .2s, border-color .2s;
}
.fc a:hover { color: var(--text2); border-color: var(--dim); }
.fc.footer-credit { margin-top: 10px; }
.secret-line {
  margin-top: 14px;
  font-size: clamp(0.65rem, 0.35vw + 0.55rem, 0.78rem);
  color: var(--cyan);
  letter-spacing: .18em;
  opacity: 0;
  transition: opacity 1.2s ease;
}
.secret-line.is-revealed { opacity: 1; }
.secret-line--always { opacity: 1; }

/* Glitch hint — cycles random glyphs, stabilises on hover (desktop) or
   per-character on tap (touch). Monospace + fixed-width slots so the line
   never reflows as glyphs change. The line as a whole inherits the cyan
   from .secret-line. */
.glitch-hint {
  font-family: 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace;
  letter-spacing: .14em;
  cursor: default;
  user-select: none;
  -webkit-user-select: none;
  text-shadow: 0 0 10px rgba(0, 245, 212, 0.35);
}
.glitch-ch {
  display: inline-block;
  min-width: 1ch;
  text-align: center;
  transition: text-shadow .15s ease, color .15s ease;
}
/* Each word's chars live inside a .glitch-word so the browser only ever
   breaks lines at the real spaces between words, never mid-word — without
   this every .glitch-ch is its own wrap candidate. */
.glitch-word {
  display: inline-block;
  white-space: nowrap;
}
.glitch-ch.is-hover-revealed,
.glitch-ch.is-touch-revealed {
  color: var(--text);
  text-shadow: 0 0 14px rgba(0, 245, 212, 0.85);
}

/* ─── Responsive ─── */
@media (max-width: 860px) {
  .sec { padding: 30px 0; }
  /* Capabilities: 2 per row at tablet width. Last row still center-aligns
     because .cards uses justify-content:center. */
  .cards > .card {
    flex: 0 1 calc((100% - 14px) / 2);
    max-width: calc((100% - 14px) / 2);
  }
  /* Beliefs: stays 2×2. */
  .stats { grid-template-columns: repeat(2, 1fr); }
  .wrap { padding: 0 28px; }
  /* On narrow screens the gallery falls beneath the text, so drop the
     54% column constraint regardless of which sections are flagged narrow. */
  .sec--with-gallery--narrow .sec-content { max-width: 100%; }
  /* Narrow viewports: gallery falls below the text as a full-width image
     band. aspect-ratio matches the source (4:3) so nothing crops; the
     section's natural height grows, pushing the next section down. */
  .sec-gallery {
    position: relative;
    left: 0;
    right: 0;
    top: auto;
    bottom: auto;
    width: 100%;
    aspect-ratio: 4 / 3;
    margin: 0;
    /* Re-clip on mobile — the video is sized to cover the band below,
       not bleed past it like on desktop. */
    overflow: hidden;
  }
  .sec-gallery-item {
    /* Mobile shows the video centred in a full-width band — the desktop
       per-section --mp-gallery-align knob is ignored here so every clip
       reads centred regardless of how it's tuned for the side-by-side
       desktop layout. object-fit:contain so nothing crops; the band is
       sized via aspect-ratio above and any gap fills with the section bg. */
    width: 100%;
    height: 100%;
    left: 0;
    transform: none;
    object-fit: contain;
    object-position: center center;
    /* Override the desktop mask gradient (which is per-gallery and tuned
       to fade behind the side-by-side text column). On mobile the video
       sits on its own line, so just soften the left/right edges with a
       small symmetric fade instead of the elaborate per-section mask. */
    -webkit-mask-image: linear-gradient(
      to right,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, 1) 7%,
      rgba(0, 0, 0, 1) 93%,
      rgba(0, 0, 0, 0) 100%
    );
            mask-image: linear-gradient(
      to right,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, 1) 7%,
      rgba(0, 0, 0, 1) 93%,
      rgba(0, 0, 0, 0) 100%
    );
  }
  /* Text is above the video on mobile, not beside it — drop the left fade.
     (.sec-content is already widened above via .sec--with-gallery--narrow.) */
  .sec-gallery-fade { display: none; }
  /* Mobile videos are centred in a full-width band, so neither the
     per-gallery `pauseOffset` nor the JS-set --mp-gallery-pause-anchor
     applies here — keep the "Click to play" hint dead-centre over the
     full-width gallery. */
  .sec-gallery-pause span {
    left: 50%;
    transform: translate(-50%, -50%);
  }

  /* On mobile the keypad is pushed lower (.cube-grid margin-top above), so
     the MET/PERMS console messages and the Simon HUD now sit ABOVE the
     cubes — closer to the desktop layout. They share the slot vacated by
     the testimonial rotator while the keypad is in active use, so the
     overlap-with-testimonials case never happens in practice. */
  .met-message,
  .perms-message,
  .mystery-hint {
    top: 14vh;
    bottom: auto;
  }
  .simon-hud {
    top: 6vh;
    bottom: auto;
  }
  /* The 70px section padding stacks with the gallery margins on mobile and
     reads as way too much air. Drop it on this section — gallery margins
     are doing the spacing now. */
  .sec--with-gallery {
    padding: 0;
  }
}
@media (max-width: 520px) {
  /* Capabilities: stack to one column on phones. */
  .cards > .card {
    flex: 0 1 100%;
    max-width: 100%;
  }
  /* Beliefs stays 2×2 even at phone widths per design — no override needed. */
  .stats { grid-template-columns: repeat(2, 1fr); }
  /* Keep the desktop row layout on mobile too — number sits next to the
     title, description wraps below them. (Was previously stacked vertically.) */
  .step-n { text-align: left; width: auto; }
  /* Hero: cube centered in the band above tagline + scroll (vh reflows when the viewport shrinks) */
  .hero {
    min-height: 100svh;
    min-height: 100dvh;
    height: 100svh;
    height: 100dvh;
    /* Reserve bottom chrome so layers center in “above the fold” content, not full 100vh */
    --hero-mobile-chrome-bottom: clamp(150px, 38vh, 270px);
  }
  /* Vignette behind the stack so cubes stay bright; slightly softer in any gaps */
  .hero-vignette {
    z-index: 0;
    opacity: 0.72;
  }
  .hero-layer {
    align-items: center;
    justify-content: center;
    padding-top: max(12.5vh, 56px, env(safe-area-inset-top, 0px));
    padding-bottom: var(--hero-mobile-chrome-bottom);
    box-sizing: border-box;
  }
  .hero-layer img {
    width: min(76vw, 100%);
    height: auto;
    /* Cap by viewport minus top inset + bottom chrome so centered stack never clips */
    max-height: min(
      76vh,
      600px,
      calc(100svh - var(--hero-mobile-chrome-bottom) - max(12.5vh, 56px) - env(safe-area-inset-top, 0px) - 12px)
    );
    object-fit: contain;
    object-position: center;
  }
  /* Back texture: scale ~3x on mobile too so the pattern reads atmospheric, not cramped. */
  .layer-back img {
    width: min(228vw, 220vh);
    height: min(228vw, 220vh);
    max-width: none;
    max-height: none;
  }
  .cube-grid {
    width: min(76vw, 480px);
    height: min(76vw, 480px);
    max-height: min(
      76vh,
      480px,
      calc(100svh - var(--hero-mobile-chrome-bottom) - max(12.5vh, 56px) - env(safe-area-inset-top, 0px) - 12px)
    );
    /* Force square */
    max-width: min(
      76vh,
      480px,
      calc(100svh - var(--hero-mobile-chrome-bottom) - max(12.5vh, 56px) - env(safe-area-inset-top, 0px) - 12px)
    );
    gap: 0;
  }
  /* Mobile retunes the tagline scale to track .scroll-cue span on narrow
     viewports — base positioning is handled by the desktop rule above. */
  .hero-tagline {
    font-size: clamp(1.05rem, 1.6vmin + 0.75rem, 1.85rem);
    letter-spacing: clamp(0.26em, 0.8vmin, 0.4em);
  }
  /* Mobile hero-ui collapses to a transparent shell so the absolute-positioned
     scroll-cue can pin to the bottom directly — no flex column. */
  .hero-ui {
    top: 0;
    gap: 0;
    padding-bottom: 0;
    display: block;
  }
  /* ~2× scroll cue vs default mobile clamp end */
  .scroll-cue {
    position: absolute;
    left: 50%;
    bottom: max(20px, env(safe-area-inset-bottom, 0px) + 12px);
    transform: translateX(-50%);
    margin-top: 0;
    gap: clamp(10px, 2vmin, 16px);
  }
  .scroll-cue span {
    font-size: clamp(1.05rem, 1.6vmin + 0.75rem, 1.85rem);
    letter-spacing: clamp(0.26em, 0.8vmin, 0.4em);
  }
  .scroll-cue .arr {
    width: clamp(18px, 2.6vmin, 26px);
    height: clamp(18px, 2.6vmin, 26px);
    border-right-width: clamp(2px, 0.35vmin, 3.5px);
    border-bottom-width: clamp(2px, 0.35vmin, 3.5px);
  }
  .wrap { padding: 0 20px; }
  .lbl {
    font-size: clamp(1.44rem, 2.2vw + 0.85rem, 2.1rem);
    letter-spacing: clamp(0.18em, 0.55vw, 0.34em);
    margin-bottom: 14px;
    gap: 14px;
  }
  .lbl::before {
    width: 9px;
    height: 9px;
  }
  .sec h2,
  .cta h2 {
    font-size: clamp(2.65rem, 7.2vw + 0.6rem, 4.75rem);
    line-height: 1.22;
  }
  .card h3 {
    font-size: clamp(1.44rem, 1.4vw + 1.05rem, 1.9rem);
    line-height: 1.4;
    margin-bottom: 10px;
  }
  /* .step h4 stays at 1.3rem on mobile too so it keeps matching .step-n. */
  .cards:not(.cards--beliefs) .card .ico {
    font-size: clamp(2.35rem, 5vw, 3.1rem);
    margin-bottom: 14px;
  }
  .tags {
    gap: 12px;
    margin-top: 24px;
  }
  .tag {
    font-size: clamp(1.16rem, 0.85vw + 0.88rem, 1.56rem);
    letter-spacing: 0.09em;
    padding: 11px 18px;
  }
  .logo-slot {
    flex: 0 0 clamp(126px, 30vw, 177px);
    width: clamp(126px, 30vw, 177px);
    padding: 0 3px;
  }
  .logo-group {
    gap: 8px;
    padding-right: 8px;
  }
  .logo-slot--wordmark {
    flex: 0 0 clamp(162px, 63vw, 300px);
    width: clamp(162px, 63vw, 300px);
    padding: 0 3px;
  }
}
