@font-face {
  font-family: "Geist Pixel";
  src: url("./assets/fonts/geist-pixel/GeistPixel-Regular-VariableFont_ELSH.ttf")
    format("truetype");
  font-display: swap;
  font-style: normal;
  font-weight: 400;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("./assets/fonts/jetbrains-mono/JetBrainsMono-VariableFont_wght.ttf")
    format("truetype");
  font-display: swap;
  font-style: normal;
  font-weight: 100 800;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("./assets/fonts/jetbrains-mono/JetBrainsMono-Italic-VariableFont_wght.ttf")
    format("truetype");
  font-display: swap;
  font-style: italic;
  font-weight: 100 800;
}

:root {
  --font-pixel: "Geist Pixel", "JetBrains Mono", monospace;
  --font-mono: "JetBrains Mono", monospace;
  --font-weight: 400;
  --font-style: normal;
  --pixel-shape: 0;
  --paper: #ffffff;
  --muted: #bfc9d1;
  --ink: #25343f;
  --accent: #ff9b51;
  --void: #020304;
  --window-bar-h: 2rem;
  /* Window-chrome frame: grayer than paper, softer than ink. */
  --chrome-frame: #9aa7b1;
}

* {
  box-sizing: border-box;
}

html {
  overflow-x: hidden;
}

body {
  margin: 0;
  min-height: 100vh;
  overflow-x: hidden;
  /* Clear the fixed window chrome bar. */
  padding-top: var(--window-bar-h);
  display: flex;
  flex-direction: column;
  color: var(--ink);
  /* Paper with a faint pixel starfield — the space identity bleeding through
     the page. Two tile scales, crisp squares, quiet enough to sit under text. */
  background-color: var(--paper);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cg fill='%2325343f' fill-opacity='0.18'%3E%3Crect x='12' y='28' width='2' height='2'/%3E%3Crect x='84' y='9' width='2' height='2'/%3E%3Crect x='139' y='63' width='2' height='2'/%3E%3Crect x='47' y='104' width='2' height='2'/%3E%3Crect x='118' y='138' width='2' height='2'/%3E%3C/g%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='230' height='230'%3E%3Cg fill='%2325343f' fill-opacity='0.22'%3E%3Crect x='31' y='11' width='1' height='1'/%3E%3Crect x='142' y='47' width='1' height='1'/%3E%3Crect x='201' y='96' width='1' height='1'/%3E%3Crect x='88' y='73' width='1' height='1'/%3E%3Crect x='9' y='157' width='1' height='1'/%3E%3Crect x='170' y='189' width='1' height='1'/%3E%3Crect x='63' y='214' width='1' height='1'/%3E%3Crect x='118' y='131' width='1' height='1'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat: repeat;
}

.font-pixel {
  font-family: var(--font-pixel);
  font-weight: 400;
  font-variation-settings: "ELSH" var(--pixel-shape);
}

.font-mono {
  font-family: var(--font-mono);
  font-style: var(--font-style);
  font-weight: var(--font-weight);
}

.font-italic {
  --font-style: italic;
  font-style: var(--font-style);
}

.weight-thin {
  --font-weight: 100;
  font-weight: var(--font-weight);
}

.weight-extra-light {
  --font-weight: 200;
  font-weight: var(--font-weight);
}

.weight-light {
  --font-weight: 300;
  font-weight: var(--font-weight);
}

.weight-regular {
  --font-weight: 400;
  font-weight: var(--font-weight);
}

.weight-medium {
  --font-weight: 500;
  font-weight: var(--font-weight);
}

.weight-bold {
  --font-weight: 700;
  font-weight: var(--font-weight);
}

.weight-extra-bold {
  --font-weight: 800;
  font-weight: var(--font-weight);
}

.pixel-shape-regular {
  --pixel-shape: 0;
}

.pixel-shape-square {
  --pixel-shape: 1;
}

.pixel-shape-circle {
  --pixel-shape: 20;
}

.pixel-shape-grid {
  --pixel-shape: 40;
}

.pixel-shape-triangle {
  --pixel-shape: 60;
}

.pixel-shape-line {
  --pixel-shape: 80;
}

.loader-screen {
  position: fixed;
  inset: 0;
  z-index: 10;
  overflow: hidden;
  isolation: isolate;
  color: var(--paper);
  background: var(--void);
  transition:
    opacity 500ms cubic-bezier(0.16, 1, 0.3, 1),
    visibility 0s linear 500ms;
}

.black-hole-canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  pointer-events: none;
}

.black-hole-occlusion-canvas {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  pointer-events: none;
}

.loader-panel {
  position: absolute;
  top: clamp(0.75rem, 2vw, 1.5rem);
  left: clamp(0.75rem, 2vw, 1.5rem);
  z-index: 2;
  width: auto;
  padding: 0;
  border: 0;
  color: var(--paper);
  background: transparent;
  opacity: 1;
  filter: none;
  transform: none;
  transform-origin: 50% 50%;
  transition:
    opacity 240ms linear,
    visibility 0s linear 240ms;
}

.loader-label {
  margin: 0 0 0.35rem;
  font-size: clamp(1rem, 2.4vw, 1.9rem);
  line-height: 1;
  text-align: left;
  letter-spacing: 0;
  text-shadow: 2px 2px 0 var(--void);
}

body.is-loader-swallowed .loader-panel {
  opacity: 0;
  visibility: hidden;
}

.loader-percent {
  margin: 0;
  font-size: clamp(3.5rem, 9vw, 7.5rem);
  line-height: 0.85;
  text-align: left;
  text-shadow:
    2px 2px 0 var(--void),
    4px 4px 0 var(--ink);
}

.home-page {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
  background: var(--paper);
  opacity: 0;
  visibility: hidden;
  transition: opacity 550ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* Coded starfield: index.js draws seeded ink-pixel stars with a Milky Way
   band, carving clearings around the measured statue and copy rects. */
.hero-stars {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

body.is-loaded .loader-screen {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

body.is-loaded .home-page {
  opacity: 1;
  visibility: visible;
}

@media (max-width: 640px) {
  .loader-panel {
    max-width: calc(100vw - 1.5rem);
  }
}

/* Typewriter width/steps/duration are tuned to the headline length
   (30 chars ≈ 14.5em in Geist Pixel) — retune all three if the text changes. */
.line-1 {
  position: relative;
  width: 14.5em;
  max-width: 100%;
  margin: 0;
  border-right: 2px solid rgba(37, 52, 63, 0.75);
  font-size: clamp(0.95rem, 0.5rem + 1.2vw, 1.5rem);
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
}

.anim-typewriter {
  width: 0;
  animation: none;
}

body.is-loaded .anim-typewriter {
  animation: typewriter 3900ms steps(30) 250ms 1 normal both,
    blinkTextCursor 700ms steps(50) infinite normal;
}

@media (prefers-reduced-motion: reduce) {
  .loader-screen,
  .home-page {
    transition: none;
  }

  body.is-loaded .anim-typewriter {
    width: 14.5em;
    animation: none;
  }
}

@keyframes typewriter {
  from {
    width: 0;
  }

  to {
    width: 14.5em;
  }
}

@keyframes blinkTextCursor {
  from {
    border-right-color: black;
  }

  to {
    border-right-color: white;
  }
}

/* Bayer 4x4 ordered-dither masks (25/50/75% pixel coverage) for the statue
   dissolve — the classic pixel-art screen transition. */
:root {
  --dither-0: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3C/svg%3E");
  --dither-25: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath fill='%23fff' d='M0 0h1v1H0zM2 0h1v1H2zM0 2h1v1H0zM2 2h1v1H2z'/%3E%3C/svg%3E");
  --dither-50: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath fill='%23fff' d='M0 0h1v1H0zM2 0h1v1H2zM0 2h1v1H0zM2 2h1v1H2zM1 1h1v1H1zM3 1h1v1H3zM1 3h1v1H1zM3 3h1v1H3z'/%3E%3C/svg%3E");
  --dither-75: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath fill='%23fff' d='M0 0h1v1H0zM1 0h1v1H1zM2 0h1v1H2zM3 0h1v1H3zM0 2h1v1H0zM1 2h1v1H1zM2 2h1v1H2zM3 2h1v1H3zM1 1h1v1H1zM3 1h1v1H3zM1 3h1v1H1zM3 3h1v1H3z'/%3E%3C/svg%3E");
}

.hero {
  position: relative;
  z-index: 1;
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  align-items: end;
  column-gap: clamp(1.5rem, 4vw, 4rem);
  width: min(100%, 76rem);
  margin: 0 auto;
  padding: clamp(2.5rem, 8vh, 5rem) clamp(1rem, 4vw, 3rem) 0;
}

.hero-copy {
  align-self: center;
  padding-bottom: clamp(2rem, 8vh, 5rem);
}

.hero-nav {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
  margin-top: clamp(1.8rem, 4vh, 3rem);
}

.hero-nav-label {
  margin: 0 0 0.9rem;
  font-size: clamp(0.72rem, 1.1vw, 0.85rem);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(37, 52, 63, 0.62);
  opacity: 0;
}

.hero-nav-link {
  position: relative;
  display: inline-block;
  padding: 0.6rem 1.15rem;
  border: 2px solid var(--ink);
  /* Paper face with faint CRT scanlines. */
  background-color: var(--paper);

  box-shadow: 4px 4px 0 var(--ink);
  color: var(--ink);
  font-size: clamp(0.82rem, 1.2vw, 0.95rem);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  opacity: 0;
  transition:
    border-color 120ms steps(2, end),
    transform 120ms steps(2, end),
    box-shadow 120ms steps(2, end);
}

/* Channel-split ghost copies of the label, shown only during glitch bursts. */
.hero-nav-link::before,
.hero-nav-link::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  padding: 0.6rem 1.15rem;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
}

.hero-nav-link::before {
  color: var(--accent);
}

.hero-nav-link::after {
  color: rgba(37, 52, 63, 0.55);
}

.hero-nav-link:hover,
.hero-nav-link:focus-visible {
  border-color: var(--accent);
}

.hero-nav-link:hover::before,
.hero-nav-link:focus-visible::before {
  animation: glitch-slice-a 360ms steps(1, end);
}

.hero-nav-link:hover::after,
.hero-nav-link:focus-visible::after {
  animation: glitch-slice-b 360ms steps(1, end);
}

.hero-nav-link:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.hero-nav-link:active {
  transform: translate(4px, 4px);
  box-shadow: 0 0 0 var(--ink);
}

@keyframes glitch-slice-a {
  0% {
    opacity: 1;
    clip-path: inset(0 0 62% 0);
    transform: translateX(-3px);
  }

  25% {
    clip-path: inset(38% 0 34% 0);
    transform: translateX(3px);
  }

  50% {
    clip-path: inset(72% 0 2% 0);
    transform: translateX(-2px);
  }

  75% {
    clip-path: inset(14% 0 58% 0);
    transform: translateX(2px);
  }

  100% {
    opacity: 1;
    clip-path: inset(44% 0 28% 0);
    transform: translateX(-1px);
  }
}

@keyframes glitch-slice-b {
  0% {
    opacity: 1;
    clip-path: inset(58% 0 6% 0);
    transform: translateX(3px);
  }

  25% {
    clip-path: inset(8% 0 74% 0);
    transform: translateX(-3px);
  }

  50% {
    clip-path: inset(30% 0 48% 0);
    transform: translateX(3px);
  }

  75% {
    clip-path: inset(64% 0 12% 0);
    transform: translateX(-2px);
  }

  100% {
    opacity: 1;
    clip-path: inset(20% 0 66% 0);
    transform: translateX(1px);
  }
}

/* A twitch every few seconds — an old set losing signal for a frame. The
   pop-in stagger delays double as phase offsets so buttons never twitch in
   unison. */
@keyframes tv-jitter {
  0%,
  96% {
    transform: none;
  }

  96.5% {
    transform: translate(1px, -1px);
  }

  97.5% {
    transform: translate(-1px, 1px);
  }

  98.5%,
  100% {
    transform: none;
  }
}

body.is-loaded .hero-nav-label {
  animation: pixel-pop-in 200ms steps(2, end) 750ms both;
}

body.is-loaded .hero-nav-link {
  animation:
    pixel-pop-in 200ms steps(2, end) both,
    tv-jitter 6400ms steps(1, end) infinite;
}

body.is-loaded .hero-nav-link:nth-of-type(1) {
  animation-delay: 900ms;
}

body.is-loaded .hero-nav-link:nth-of-type(2) {
  animation-delay: 1040ms;
}

body.is-loaded .hero-nav-link:nth-of-type(3) {
  animation-delay: 1180ms;
}

body.is-loaded .hero-nav-link:nth-of-type(4) {
  animation-delay: 1320ms;
}

body.is-loaded .hero-nav-link:nth-of-type(5) {
  animation-delay: 1460ms;
}

/* Opacity only: a filled animation keeps overriding its properties after it
   ends, so transforms stay free for :active presses. */
@keyframes pixel-pop-in {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

.hero-statue {
  position: relative;
  align-self: end;
  justify-self: end;
  margin: 0;
  width: min(100%, 30rem, 60vh);
  overflow: hidden;
}

.hero-statue-img {
  display: block;
  width: 100%;
  height: auto;
  /* Pedestal dips just below the fold, cropped by the figure. */
  margin-bottom: -4.5%;
  /* Source art's background is (253,254,255); a slight brightness push clamps
     it to pure white, and multiply makes that white transparent so the statue
     sits directly on the starfield with no box. */
  filter: brightness(1.04);
  mix-blend-mode: multiply;
  opacity: 0;
  -webkit-mask-size: 16px 16px;
  -webkit-mask-repeat: repeat;
  mask-size: 16px 16px;
  mask-repeat: repeat;
}

body.is-loaded .hero-statue-img {
  animation: statue-dither-in 2000ms steps(1, end) 2200ms both;
}

@keyframes statue-dither-in {
  0%,
  19.9% {
    opacity: 1;
    -webkit-mask-image: var(--dither-0);
    mask-image: var(--dither-0);
  }

  20%,
  39.9% {
    -webkit-mask-image: var(--dither-25);
    mask-image: var(--dither-25);
  }

  40%,
  59.9% {
    -webkit-mask-image: var(--dither-50);
    mask-image: var(--dither-50);
  }

  60%,
  79.9% {
    -webkit-mask-image: var(--dither-75);
    mask-image: var(--dither-75);
  }

  80%,
  100% {
    opacity: 1;
    -webkit-mask-image: none;
    mask-image: none;
  }
}

@supports not ((mask-image: none) or (-webkit-mask-image: none)) {
  body.is-loaded .hero-statue-img {
    animation: statue-fade-in 1400ms steps(7) 2200ms both;
  }
}

@keyframes statue-fade-in {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

/* Screen blend tints only the statue's dark pixels — over the white paper it
   is a no-op — so the torch flame is the single spot of color. */
.hero-torch-glow {
  position: absolute;
  left: 1%;
  top: -6%;
  width: 32%;
  aspect-ratio: 1;
  background: radial-gradient(
    circle,
    var(--accent) 0%,
    var(--accent) 30%,
    rgba(255, 155, 81, 0.55) 52%,
    rgba(255, 155, 81, 0) 72%
  );
  mix-blend-mode: screen;
  opacity: 0;
  /* The flame is a live trigger: clicking it sends the page haywire. */
  cursor: pointer;
}

/* Ignition lands just after the typewriter finishes (250 + 3900ms). */
body.is-loaded .hero-torch-glow {
  animation:
    torch-ignite 500ms steps(3, end) 4300ms both,
    torch-flicker 1600ms steps(1, end) 4800ms infinite;
}

/* Three charges spent: the fire dims and stops answering until a reload. */
.hero-torch-glow.is-spent,
body.is-loaded .hero-torch-glow.is-spent {
  animation: none;
  opacity: 0.35;
  cursor: default;
}

@keyframes torch-ignite {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

@keyframes torch-flicker {
  0%,
  100% {
    opacity: 1;
    transform: none;
  }

  50% {
    opacity: 0.78;
    transform: scale(0.96);
  }
}

/* Click transition: the clicked button bursts harder than on hover, and for
   page links the paper wipes over the screen in Bayer steps before index.js
   navigates. Destination pages run the reverse wipe on entry. */
.hero-nav-link.is-departing {
  border-color: var(--accent);
}

.hero-nav-link.is-departing::before {
  animation: glitch-depart-a 420ms steps(1, end);
}

.hero-nav-link.is-departing::after {
  animation: glitch-depart-b 420ms steps(1, end);
}

@keyframes glitch-depart-a {
  0% {
    opacity: 1;
    clip-path: inset(0 0 55% 0);
    transform: translateX(-6px);
  }

  20% {
    clip-path: inset(42% 0 22% 0);
    transform: translateX(5px);
  }

  40% {
    clip-path: inset(68% 0 0 0);
    transform: translateX(-7px);
  }

  60% {
    clip-path: inset(10% 0 62% 0);
    transform: translateX(6px);
  }

  80% {
    clip-path: inset(50% 0 30% 0);
    transform: translateX(-4px);
  }

  100% {
    opacity: 1;
    clip-path: inset(28% 0 44% 0);
    transform: translateX(5px);
  }
}

@keyframes glitch-depart-b {
  0% {
    opacity: 1;
    clip-path: inset(62% 0 4% 0);
    transform: translateX(6px);
  }

  20% {
    clip-path: inset(4% 0 78% 0);
    transform: translateX(-5px);
  }

  40% {
    clip-path: inset(34% 0 40% 0);
    transform: translateX(7px);
  }

  60% {
    clip-path: inset(74% 0 6% 0);
    transform: translateX(-6px);
  }

  80% {
    clip-path: inset(18% 0 58% 0);
    transform: translateX(4px);
  }

  100% {
    opacity: 1;
    clip-path: inset(46% 0 26% 0);
    transform: translateX(-5px);
  }
}

.page-wipe {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: var(--paper);
  pointer-events: none;
  visibility: hidden;
  -webkit-mask-size: 24px 24px;
  -webkit-mask-repeat: repeat;
  mask-size: 24px 24px;
  mask-repeat: repeat;
}

body.is-wiping .page-wipe {
  visibility: visible;
  animation: page-wipe-cover 420ms steps(1, end) both;
}

.page-wipe-reveal {
  visibility: visible;
  animation: page-wipe-reveal 480ms steps(1, end) 60ms both;
}

@keyframes page-wipe-cover {
  0%,
  24.9% {
    -webkit-mask-image: var(--dither-25);
    mask-image: var(--dither-25);
  }

  25%,
  49.9% {
    -webkit-mask-image: var(--dither-50);
    mask-image: var(--dither-50);
  }

  50%,
  74.9% {
    -webkit-mask-image: var(--dither-75);
    mask-image: var(--dither-75);
  }

  75%,
  100% {
    -webkit-mask-image: none;
    mask-image: none;
  }
}

@keyframes page-wipe-reveal {
  0%,
  24.9% {
    -webkit-mask-image: none;
    mask-image: none;
  }

  25%,
  49.9% {
    -webkit-mask-image: var(--dither-75);
    mask-image: var(--dither-75);
  }

  50%,
  74.9% {
    -webkit-mask-image: var(--dither-50);
    mask-image: var(--dither-50);
  }

  75%,
  99.9% {
    -webkit-mask-image: var(--dither-25);
    mask-image: var(--dither-25);
  }

  100% {
    -webkit-mask-image: var(--dither-0);
    mask-image: var(--dither-0);
  }
}

/* Page switches hit harder. Arrival (any page entered through the dither
   reveal): content and chrome jolt while the reveal plays. Departure
   (is-wiping): a stronger jolt plus the bar ghosts firing the hover burst.
   Separate in/out keyframes on purpose — reusing one name would keep the
   already-finished arrival jolt from restarting on departure. Ambient loops
   are restated before the one-shots so the loop survives the override and
   the one-shot (listed last) wins while it runs. */
body:has(.page-wipe-reveal) main {
  animation: page-jolt-in 420ms steps(1, end) 120ms;
}

body:has(.page-wipe-reveal) .window-bar {
  animation:
    tv-jitter 7300ms steps(1, end) -1800ms infinite,
    page-jolt-in 420ms steps(1, end) 120ms;
}

body.is-wiping main,
body.is-wiping .site-footer {
  animation: page-jolt-out 420ms steps(1, end);
}

body.is-wiping .window-bar {
  animation:
    tv-jitter 7300ms steps(1, end) -1800ms infinite,
    page-jolt-out 420ms steps(1, end);
}

body.is-wiping .window-bar-ghost--a {
  animation:
    bar-ambient-a 19700ms steps(1, end) -7300ms infinite,
    glitch-slice-a 360ms steps(1, end);
}

body.is-wiping .window-bar-ghost--b {
  animation:
    bar-ambient-b 19700ms steps(1, end) -7300ms infinite,
    glitch-slice-b 360ms steps(1, end);
}

/* Back button departing = the same shove the nav buttons take. */
.section-back.is-departing {
  border-color: var(--accent);
  transform: translate(3px, 3px);
  box-shadow: 0 0 0 var(--ink);
}

/* Hard frames, ~84ms each — a jolt, not a wiggle. Out hits harder than in. */
@keyframes page-jolt-in {
  0% {
    transform: translate(-4px, 2px);
  }

  20% {
    transform: translate(3px, -2px);
  }

  40% {
    transform: translate(-2px, -1px);
  }

  60% {
    transform: translate(3px, 1px);
  }

  80% {
    transform: translate(-1px, 1px);
  }

  100% {
    transform: none;
  }
}

@keyframes page-jolt-out {
  0% {
    transform: translate(-6px, 2px);
  }

  20% {
    transform: translate(5px, -3px);
  }

  40% {
    transform: translate(-3px, -2px);
  }

  60% {
    transform: translate(4px, 2px);
  }

  80% {
    transform: translate(-2px, 1px);
  }

  100% {
    transform: none;
  }
}

/* Easter egg: clicking Prometheus' flame throws the whole page into a retro-TV
   breakdown for two seconds. Two layers keep it smooth yet 8-bit: haywire-vhold
   glides the picture on a vertical-hold wobble (transform) that damps to rest
   by the end, while haywire-signal hard-cuts the signal on top — blocky
   row-tears (SVG displacement) and palette-safe color dropouts that taper off.
   Both settle to nothing at 100% and hold (forwards), so the picture eases
   back instead of snapping. index.js grants the torch three charges per visit;
   only a reload refuels it. */
body.is-haywire main,
body.is-haywire .site-footer {
  animation:
    haywire-vhold 2000ms ease-in-out forwards,
    haywire-signal 2000ms steps(1, end) forwards;
}

body.is-haywire .window-bar {
  animation:
    tv-jitter 7300ms steps(1, end) -1800ms infinite,
    haywire-signal 2000ms steps(1, end) forwards;
}

body.is-haywire .window-bar-ghost--a {
  animation:
    bar-ambient-a 19700ms steps(1, end) -7300ms infinite,
    glitch-slice-a 460ms steps(1, end) infinite;
}

body.is-haywire .window-bar-ghost--b {
  animation:
    bar-ambient-b 19700ms steps(1, end) -7300ms infinite,
    glitch-slice-b 610ms steps(1, end) infinite;
}

/* The SVG defs holding the displacement filters take no layout space. */
.fx-defs {
  position: absolute;
  width: 0;
  height: 0;
}

/* Full-viewport TV overlays, above the page (z 25) but under the wipes (30).
   ::before is 1-bit snow — ink static with sparse accent sparks — jumping
   every 60ms; ::after is the rolling hum bar with a hot accent line chasing
   it down the frame at its own speed. */
body.is-haywire::before,
body.is-haywire::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 25;
  opacity: 0;
  pointer-events: none;
}

body.is-haywire::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28'%3E%3Cpath fill='%2325343f' fill-opacity='0.6' d='M2 4h2v2H2zM9 1h2v2H9zM16 6h2v2h-2zM23 3h2v2h-2zM5 12h2v2H5zM13 15h2v2h-2zM20 11h2v2h-2zM1 20h2v2H1zM8 25h2v2H8zM15 22h2v2h-2zM24 18h2v2h-2zM11 8h2v2h-2zM19 26h2v2h-2z'/%3E%3Cpath fill='%23ff9b51' fill-opacity='0.8' d='M26 8h2v2h-2zM3 26h2v2H3z'/%3E%3C/svg%3E");
  /* haywire-snow only jumps position now; the veil owns opacity so it can
     fade in at the start and, crucially, fade out smoothly at the end. */
  animation:
    haywire-snow 240ms steps(1, end) infinite,
    haywire-veil-snow 2000ms ease forwards;
}

body.is-haywire::after {
  background-image:
    /* Static CRT scanlines (1px dark every 3px) — the tube's fixed raster. */
    repeating-linear-gradient(
      0deg,
      rgba(37, 52, 63, 0.11) 0 1px,
      transparent 1px 3px
    ),
    /* Soft hum band that rolls down the frame... */
    linear-gradient(
      180deg,
      rgba(37, 52, 63, 0) 0%,
      rgba(37, 52, 63, 0.16) 32%,
      rgba(37, 52, 63, 0.26) 50%,
      rgba(37, 52, 63, 0) 68%
    ),
    /* ...with a hot accent retrace line chasing it. */
    linear-gradient(var(--accent), var(--accent));
  background-repeat: repeat, no-repeat, no-repeat;
  background-size: 100% 100%, 100% 30%, 100% 3px;
  /* Linear, not stepped: the roll glides like a real analog vertical slip.
     The veil fades the whole overlay out at the end so it doesn't just vanish. */
  animation:
    haywire-roll 1100ms linear infinite,
    haywire-veil-roll 2000ms ease forwards;
}

/* Smooth layer: a vertical-hold wobble whose amplitude damps toward 0 by the
   end (a signal re-locking), so the picture eases to rest instead of the
   infinite wobble snapping off when the effect clears. */
@keyframes haywire-vhold {
  0% {
    transform: translateY(0);
  }

  7% {
    transform: translateY(-11px);
  }

  18% {
    transform: translateY(10px);
  }

  30% {
    transform: translateY(-8px);
  }

  42% {
    transform: translateY(7px);
  }

  55% {
    transform: translateY(-5px);
  }

  68% {
    transform: translateY(3px);
  }

  80% {
    transform: translateY(-2px);
  }

  90% {
    transform: translateY(1px);
  }

  100% {
    transform: translateY(0);
  }
}

/* Glitch layer: hard-cut signal states over 2s — blocky row-tears (the SVG
   displacement filters) and palette-safe color dropouts (grayscale, warm
   sepia, brightness). Brief `none` frames let the signal almost recover, so
   it reads as a set fighting to hold rather than uniform noise. */
@keyframes haywire-signal {
  0% {
    filter: url(#haywire-warp-a) grayscale(1) contrast(1.4);
  }

  8% {
    filter: none;
  }

  15% {
    filter: url(#haywire-warp-b);
  }

  22% {
    filter: sepia(0.9) saturate(1.5) contrast(1.1);
  }

  30% {
    filter: url(#haywire-warp-a) brightness(0.8);
  }

  38% {
    filter: none;
  }

  45% {
    filter: url(#haywire-warp-b) grayscale(0.85);
  }

  53% {
    filter: grayscale(1) contrast(1.5) brightness(1.15);
  }

  61% {
    filter: url(#haywire-warp-a);
  }

  69% {
    filter: none;
  }

  76% {
    filter: url(#haywire-warp-b) sepia(0.85) saturate(1.6);
  }

  /* Tail winds down: last hard tear, then only gentle color drains fading to
     none, so the signal eases back rather than cutting out mid-glitch. */
  82% {
    filter: url(#haywire-warp-a) grayscale(0.6);
  }

  88% {
    filter: grayscale(0.4) contrast(1.1);
  }

  93% {
    filter: sepia(0.3) brightness(1.05);
  }

  97% {
    filter: grayscale(0.12);
  }

  100% {
    filter: none;
  }
}

@keyframes haywire-snow {
  0% {
    background-position: 0 0;
  }

  25% {
    background-position: -7px 9px;
  }

  50% {
    background-position: 11px -5px;
  }

  75% {
    background-position: -13px -11px;
  }

  100% {
    background-position: 5px 13px;
  }
}

@keyframes haywire-roll {
  from {
    /* Scanlines fixed; hum band and retrace line start above the frame. */
    background-position: 0 0, 0 -40vh, 0 -6vh;
  }

  to {
    background-position: 0 0, 0 110vh, 0 144vh;
  }
}

/* Opacity envelopes for the overlays: a quick fade in at the start, a hold,
   then a smooth ease-out at the end — this is what stops the snow and hum bar
   from snapping off when the effect clears. */
@keyframes haywire-veil-snow {
  0% {
    opacity: 0;
  }

  8% {
    opacity: 0.5;
  }

  70% {
    opacity: 0.5;
  }

  100% {
    opacity: 0;
  }
}

@keyframes haywire-veil-roll {
  0% {
    opacity: 0;
  }

  8% {
    opacity: 1;
  }

  70% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

/* macOS pixel window chrome: purely decorative title bar pinned to the top of
   every page — 8-bit traffic lights plus a glitching title. z-index 5 keeps it
   under the loader (10) and the dither wipes (30), so those cover it for free. */
.window-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  height: var(--window-bar-h);
  padding: 0 0.65rem;
  /* Paper face worn like old hardware: faint hairline scratches and specks
     on a 160x32 tile. The bottom frame line is a gradient (not a border) so
     it can stop 14px short of each side and hand off to the outward corner
     curves drawn by the pseudo-elements below. */
  background-color: var(--paper);
  background-image:
    linear-gradient(var(--chrome-frame), var(--chrome-frame)),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='32'%3E%3Cpath fill='%2325343f' fill-opacity='0.08' d='M12 6h8v1h-8zM55 23h11v1H55zM98 4h6v1h-6zM130 17h9v1h-9zM30 25h2v1h-2zM32 26h2v1h-2zM34 27h2v1h-2zM143 8h2v1h-2zM145 9h2v1h-2zM76 13h1v4h-1zM108 27h5v1h-5z'/%3E%3Cpath fill='%2325343f' fill-opacity='0.13' d='M5 28h1v1H5zM22 12h2v1h-2zM44 3h1v1h-1zM63 15h1v1h-1zM84 24h1v1h-1zM91 9h1v1h-1zM118 21h1v1h-1zM126 2h1v1h-1zM151 27h2v1h-2zM39 18h1v1h-1zM70 29h1v1h-1zM156 12h1v1h-1z'/%3E%3C/svg%3E");
  background-size: calc(100% - 28px) 2px, auto;
  background-position: 50% 100%, 0 0;
  background-repeat: no-repeat, repeat;
  /* Own duration + phase so the bar never twitches in sync with the nav
     buttons (6400ms) or the titles (5500ms). */
  animation: tv-jitter 7300ms steps(1, end) -1800ms infinite;
}

/* Channel-split ghosts of the whole bar — the exact burst the traffic lights
   fire, scaled up: accent and ink slabs sliced into bands for a few frames.
   Real elements because the bar's pseudos are spent on the corners; one long
   19.7s cycle keeps the misfire rare. */
.window-bar-ghost {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}

.window-bar-ghost--a {
  background: var(--accent);
  animation: bar-ambient-a 19700ms steps(1, end) -7300ms infinite;
}

.window-bar-ghost--b {
  background: rgba(37, 52, 63, 0.55);
  animation: bar-ambient-b 19700ms steps(1, end) -7300ms infinite;
}

@keyframes bar-ambient-a {
  0%,
  89.9% {
    opacity: 0;
  }

  90% {
    opacity: 1;
    clip-path: inset(0 0 62% 0);
    transform: translateX(-3px);
  }

  90.7% {
    clip-path: inset(38% 0 34% 0);
    transform: translateX(3px);
  }

  91.4% {
    clip-path: inset(72% 0 2% 0);
    transform: translateX(-2px);
  }

  92.1%,
  100% {
    opacity: 0;
  }
}

@keyframes bar-ambient-b {
  0%,
  90.2% {
    opacity: 0;
  }

  90.3% {
    opacity: 1;
    clip-path: inset(58% 0 6% 0);
    transform: translateX(3px);
  }

  91% {
    clip-path: inset(8% 0 74% 0);
    transform: translateX(-3px);
  }

  91.7% {
    clip-path: inset(30% 0 48% 0);
    transform: translateX(2px);
  }

  92.4%,
  100% {
    opacity: 0;
  }
}

/* Outward window corners: the frame line curves down past the bar and out to
   the screen sides — a concave fillet, with paper filling the wedge above
   the curve. Sized to meet the 2px edge line tangentially. */
.window-bar::before,
.window-bar::after {
  content: "";
  position: absolute;
  top: calc(100% - 2px);
  width: 14px;
  height: 14px;
  pointer-events: none;
}

.window-bar::before {
  left: 0;
  background: radial-gradient(
    circle at 100% 100%,
    transparent 12px,
    var(--chrome-frame) 12px 14px,
    var(--paper) 14px
  );
}

.window-bar::after {
  right: 0;
  background: radial-gradient(
    circle at 0 100%,
    transparent 12px,
    var(--chrome-frame) 12px 14px,
    var(--paper) 14px
  );
}

/* 8x8 pixel-art circles (1px ink ring + corner shine) as data: URIs — the
   dither-mask trick — shown at exactly 2x so the pixels stay crisp. */
.window-dot {
  position: relative;
  flex: none;
  width: 16px;
  height: 16px;
  background-size: 100% 100%;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}

.window-dot--close {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%2325343f' d='M2 0h4v1H2zM1 1h1v1H1zM6 1h1v1H6zM0 2h1v4H0zM7 2h1v4H7zM1 6h1v1H1zM6 6h1v1H6zM2 7h4v1H2z'/%3E%3Cpath fill='%23e8564e' d='M2 1h4v1H2zM1 2h6v4H1zM2 6h4v1H2z'/%3E%3Cpath fill='%23f2a29e' d='M2 2h2v1H2zM2 3h1v1H2z'/%3E%3C/svg%3E");
}

.window-dot--min {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%2325343f' d='M2 0h4v1H2zM1 1h1v1H1zM6 1h1v1H6zM0 2h1v4H0zM7 2h1v4H7zM1 6h1v1H1zM6 6h1v1H6zM2 7h4v1H2z'/%3E%3Cpath fill='%23e2a93c' d='M2 1h4v1H2zM1 2h6v4H1zM2 6h4v1H2z'/%3E%3Cpath fill='%23efd094' d='M2 2h2v1H2zM2 3h1v1H2z'/%3E%3C/svg%3E");
}

.window-dot--max {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%2325343f' d='M2 0h4v1H2zM1 1h1v1H1zM6 1h1v1H6zM0 2h1v4H0zM7 2h1v4H7zM1 6h1v1H1zM6 6h1v1H6zM2 7h4v1H2z'/%3E%3Cpath fill='%234fae57' d='M2 1h4v1H2zM1 2h6v4H1zM2 6h4v1H2z'/%3E%3Cpath fill='%239ed2a3' d='M2 2h2v1H2zM2 3h1v1H2z'/%3E%3C/svg%3E");
}

/* Channel-split ghost squares: invisible except during a burst. Left alone,
   each dot still misfires once per 8.9s loop (the hover slices compressed
   into the tail of the cycle), phase-shifted so they go off one at a time. */
.window-dot::before,
.window-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}

.window-dot::before {
  background: var(--accent);
  animation: dot-ambient-a 8900ms steps(1, end) infinite;
}

.window-dot::after {
  background: rgba(37, 52, 63, 0.55);
  animation: dot-ambient-b 8900ms steps(1, end) infinite;
}

.window-dot--min::before,
.window-dot--min::after {
  animation-delay: -3000ms;
}

.window-dot--max::before,
.window-dot--max::after {
  animation-delay: -6100ms;
}

/* Hover out-ranks the ambient loop and fires the burst on demand. */
.window-dot:hover::before {
  animation: glitch-slice-a 360ms steps(1, end);
}

.window-dot:hover::after {
  animation: glitch-slice-b 360ms steps(1, end);
}

/* Touching the tab disturbs its pixel structure: a small ripple of orange
   pixels blooms at the pointer and settles — like pressing a broken LCD and
   watching the cells stir. It lives entirely inside the bar band (fixed to
   the top strip, clipped by overflow) and stays low-contrast, so it reads as
   the tab's own structure flickering rather than an overlay spilling onto the
   page. index.js / transitions.js spawn a couple staggered at --ripple-x. */
.bar-ripple {
  position: fixed;
  top: 0;
  left: var(--ripple-x);
  height: var(--window-bar-h);
  width: 60px;
  transform: translateX(-50%);
  z-index: 6;
  overflow: hidden;
  pointer-events: none;
}

.bar-ripple::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  /* Concentric rings of pixels (bright inner + mid, faint outer). */
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill='%23ff9b51' fill-opacity='0.9' d='M10 7h1v1h-1zM13 10h1v1h-1zM10 13h1v1h-1zM7 10h1v1h-1zM12 8h1v1h-1zM12 12h1v1h-1zM8 12h1v1h-1zM8 8h1v1h-1zM10 4h1v1h-1zM16 10h1v1h-1zM10 16h1v1h-1zM4 10h1v1h-1zM14 6h1v1h-1zM14 14h1v1h-1zM6 14h1v1h-1zM6 6h1v1h-1z'/%3E%3Cpath fill='%23ff9b51' fill-opacity='0.45' d='M10 2h1v1h-1zM18 10h1v1h-1zM10 18h1v1h-1zM2 10h1v1h-1zM15 8h1v1h-1zM15 12h1v1h-1zM5 8h1v1h-1zM5 12h1v1h-1zM8 15h1v1h-1zM12 15h1v1h-1zM8 5h1v1h-1zM12 5h1v1h-1z'/%3E%3C/svg%3E") center / contain no-repeat;
  image-rendering: pixelated;
  animation: bar-ripple 520ms steps(4, end) forwards;
}

/* Rings step outward from the touch and dissolve — chunky, brief, quiet. */
@keyframes bar-ripple {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.35);
  }

  30% {
    opacity: 0.65;
  }

  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.4);
  }
}

@keyframes dot-ambient-a {
  0%,
  93.9% {
    opacity: 0;
  }

  94% {
    opacity: 1;
    clip-path: inset(0 0 62% 0);
    transform: translateX(-3px);
  }

  95.5% {
    clip-path: inset(38% 0 34% 0);
    transform: translateX(3px);
  }

  97% {
    clip-path: inset(72% 0 2% 0);
    transform: translateX(-2px);
  }

  98.5%,
  100% {
    opacity: 0;
  }
}

@keyframes dot-ambient-b {
  0%,
  94.4% {
    opacity: 0;
  }

  94.5% {
    opacity: 1;
    clip-path: inset(58% 0 6% 0);
    transform: translateX(3px);
  }

  96% {
    clip-path: inset(8% 0 74% 0);
    transform: translateX(-3px);
  }

  97.5% {
    clip-path: inset(30% 0 48% 0);
    transform: translateX(2px);
  }

  99%,
  100% {
    opacity: 0;
  }
}

/* Classic-Mac scrollbar in the site palette: dithered track, chunky accent
   thumb with a hard two-tone bevel. Styling ::-webkit-scrollbar opts macOS
   out of overlay scrollbars, so the chrome stays on show. */
/* Firefox only understands thumb/track colors — and in Chromium, setting
   scrollbar-color would switch off the ::-webkit-scrollbar styling below,
   so it must only apply where the webkit selector doesn't exist. */
@supports not selector(::-webkit-scrollbar) {
  html {
    scrollbar-color: var(--accent) var(--paper);
  }
}

::-webkit-scrollbar {
  width: 14px;
  height: 14px;
}

::-webkit-scrollbar-track {
  /* 50% checkerboard at 2px — the classic System-7 track pattern in ink. */
  background-color: var(--paper);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath fill='%2325343f' fill-opacity='0.15' d='M0 0h2v2H0zM2 2h2v2H2z'/%3E%3C/svg%3E");
}

::-webkit-scrollbar-track:vertical {
  border-left: 2px solid var(--ink);
}

::-webkit-scrollbar-track:horizontal {
  border-top: 2px solid var(--ink);
}

/* Bad-connection glitch: most of a ~16.5s cycle the thumb sits solid accent,
   then a short burst tears it into channel-split bands (a stripe overlay via
   --sb-glitch) while the fill washes and the bevel light flips — the same
   glitch language as the title and the chrome bar, just rarer and punchier.
   Every stripe/wash stays in the warm accent family, off-palette by design
   like the traffic lights. */
html {
  --sb-thumb: var(--accent);
  --sb-glitch: none;
  --sb-hi: #ffbe8e;
  --sb-lo: #d97a35;
  animation: sb-signal 9000ms steps(1, end) infinite;
}

@keyframes sb-signal {
  0%,
  43.9% {
    --sb-thumb: var(--accent);
    --sb-glitch: none;
    --sb-hi: #ffbe8e;
    --sb-lo: #d97a35;
  }

  /* Mid-cycle stutter — a quick two-frame tear so a glitch shows up roughly
     twice as often as the burst alone would give. */
  44% {
    --sb-thumb: #ffd3ae;
    --sb-glitch: repeating-linear-gradient(0deg, var(--accent) 0 3px, #ffd8b0 3px 4px, #b3611f 4px 6px);
    --sb-hi: #d97a35;
    --sb-lo: #ffbe8e;
  }

  44.8% {
    --sb-thumb: #e8823a;
    --sb-glitch: none;
    --sb-hi: #d97a35;
    --sb-lo: #ffbe8e;
  }

  45.6%,
  85.9% {
    --sb-thumb: var(--accent);
    --sb-glitch: none;
    --sb-hi: #ffbe8e;
    --sb-lo: #d97a35;
  }

  86% {
    /* tear */
    --sb-thumb: #ffd3ae;
    --sb-glitch: repeating-linear-gradient(0deg, var(--accent) 0 3px, #ffd8b0 3px 4px, #b3611f 4px 6px);
    --sb-hi: #d97a35;
    --sb-lo: #ffbe8e;
  }

  87.4% {
    /* wash */
    --sb-thumb: #ffd3ae;
    --sb-glitch: none;
    --sb-hi: #d97a35;
    --sb-lo: #ffbe8e;
  }

  88.8% {
    /* tear, bands offset */
    --sb-thumb: var(--accent);
    --sb-glitch: repeating-linear-gradient(0deg, #b3611f 0 2px, var(--accent) 2px 5px, #ffd8b0 5px 6px);
    --sb-hi: #d97a35;
    --sb-lo: #ffbe8e;
  }

  90.2% {
    /* dark dropout */
    --sb-thumb: #e8823a;
    --sb-glitch: none;
    --sb-hi: #d97a35;
    --sb-lo: #ffbe8e;
  }

  91.6% {
    /* one last tear */
    --sb-thumb: var(--accent);
    --sb-glitch: repeating-linear-gradient(0deg, var(--accent) 0 2px, #ffd8b0 2px 3px, #b3611f 3px 6px);
    --sb-hi: #ffbe8e;
    --sb-lo: #d97a35;
  }

  93%,
  100% {
    /* recovered */
    --sb-thumb: var(--accent);
    --sb-glitch: none;
    --sb-hi: #ffbe8e;
    --sb-lo: #d97a35;
  }
}

::-webkit-scrollbar-thumb {
  background-color: var(--sb-thumb);
  background-image: var(--sb-glitch);
  border: 2px solid var(--ink);
  /* Pixel bevel lit from the top-left, like the traffic-light shine. */
  box-shadow:
    inset 2px 2px 0 var(--sb-hi),
    inset -2px -2px 0 var(--sb-lo);
}

::-webkit-scrollbar-thumb:hover {
  background-color: #ffab6e;
}

::-webkit-scrollbar-thumb:active {
  background-color: #f28438;
}

::-webkit-scrollbar-corner {
  background: var(--paper);
}

/* Left-edge mirror of the accent scrollbar, but only while the Prometheus torch
   throws the page haywire: it strikes in on the left to answer the orange
   scrollbar glitching on the right, then fades out with the rest of the
   breakdown. Painted like the scrollbar thumb (same --sb-* vars, ink inner edge,
   pixel bevel) so the two sides read as a pair. Hidden every other moment and on
   every other page — the torch (and so .is-haywire) only lives on the home page.
   z-index 26 sits it just above the TV snow/roll overlays (25), below the
   dither wipes (30); it starts below the window chrome so it clears the lights. */
html::before {
  content: "";
  position: fixed;
  left: 0;
  top: var(--window-bar-h);
  bottom: 0;
  width: 14px;
  z-index: 26;
  pointer-events: none;
  background-color: var(--sb-thumb);
  background-image: var(--sb-glitch);
  border-right: 2px solid var(--ink);
  box-shadow:
    inset 2px 2px 0 var(--sb-hi),
    inset -2px -2px 0 var(--sb-lo);
  opacity: 0;
  visibility: hidden;
}

html:has(body.is-haywire)::before {
  visibility: visible;
  animation: haywire-leftbar 2000ms steps(1, end) forwards;
}

/* Strikes in with a stepped jitter, flickers hard while the signal breaks up,
   holds, then fades out by the end so it clears with the veil rather than
   snapping off. */
@keyframes haywire-leftbar {
  0% {
    opacity: 0;
    transform: translateX(-7px);
  }

  6% {
    opacity: 1;
    transform: translateX(0);
  }

  12% {
    opacity: 0.3;
    transform: translateX(4px);
  }

  17% {
    opacity: 1;
    transform: translateX(-3px);
  }

  26% {
    opacity: 0.55;
    transform: translateX(2px);
  }

  33% {
    opacity: 1;
    transform: translateX(0);
  }

  70% {
    opacity: 0.92;
    transform: translateX(0);
  }

  84% {
    opacity: 0.6;
    transform: translateX(-2px);
  }

  100% {
    opacity: 0;
    transform: translateX(-5px);
  }
}

/* Section pages (experience.html, and siblings to come): small-dot starfield
   comes free from the body background; figure pinned to the page edge. */
.section-body {
  min-height: 100svh;
}

.section-page {
  position: relative;
  z-index: 1;
  padding: clamp(1.5rem, 4vw, 3.5rem);
}

.section-header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem 1.5rem;
}

.section-title {
  margin: 0;
  font-size: clamp(2.2rem, 6vw, 4.5rem);
  line-height: 0.95;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink);
}

.section-back {
  flex: none;
  padding: 0.5rem 0.9rem;
  border: 2px solid var(--ink);
  background: var(--paper);
  box-shadow: 3px 3px 0 var(--ink);
  color: var(--ink);
  font-size: 0.85rem;
  text-transform: uppercase;
  text-decoration: none;
  transition:
    border-color 120ms steps(2, end),
    transform 120ms steps(2, end),
    box-shadow 120ms steps(2, end);
}

.section-back:hover,
.section-back:focus-visible {
  border-color: var(--accent);
}

.section-back:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.section-back:active {
  transform: translate(3px, 3px);
  box-shadow: 0 0 0 var(--ink);
}

.section-content {
  max-width: 55rem;
  margin-top: clamp(2rem, 6vh, 4rem);
}

.section-placeholder {
  margin: 0;
  font-size: 0.95rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(37, 52, 63, 0.55);
}

/* The broadcast never stabilizes: channel-split ghosts crawl through the
   title forever, plus the ambient tv-jitter twitch. */
.glitch-title {
  position: relative;
  animation: tv-jitter 5500ms steps(1, end) infinite;
}

.glitch-title::before,
.glitch-title::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.glitch-title::before {
  color: var(--accent);
  animation: title-glitch-a 2700ms steps(1, end) infinite;
}

.glitch-title::after {
  color: rgba(37, 52, 63, 0.5);
  animation: title-glitch-b 3400ms steps(1, end) infinite;
}

@keyframes title-glitch-a {
  0%,
  10.9% {
    clip-path: inset(12% 0 74% 0);
    transform: translate(-3px, 0);
  }

  11%,
  25.9% {
    clip-path: inset(52% 0 34% 0);
    transform: translate(2px, 0);
  }

  26%,
  38.9% {
    clip-path: inset(78% 0 8% 0);
    transform: translate(-2px, 1px);
  }

  39%,
  54.9% {
    clip-path: inset(32% 0 56% 0);
    transform: translate(3px, 0);
  }

  55%,
  71.9% {
    clip-path: inset(64% 0 22% 0);
    transform: translate(-4px, 0);
  }

  72%,
  87.9% {
    clip-path: inset(4% 0 84% 0);
    transform: translate(2px, -1px);
  }

  88%,
  100% {
    clip-path: inset(44% 0 44% 0);
    transform: translate(-2px, 0);
  }
}

@keyframes title-glitch-b {
  0%,
  13.9% {
    clip-path: inset(66% 0 14% 0);
    transform: translate(3px, 0);
  }

  14%,
  30.9% {
    clip-path: inset(8% 0 80% 0);
    transform: translate(-2px, 0);
  }

  31%,
  46.9% {
    clip-path: inset(40% 0 42% 0);
    transform: translate(4px, 1px);
  }

  47%,
  63.9% {
    clip-path: inset(84% 0 2% 0);
    transform: translate(-3px, 0);
  }

  64%,
  79.9% {
    clip-path: inset(22% 0 64% 0);
    transform: translate(2px, 0);
  }

  80%,
  100% {
    clip-path: inset(56% 0 30% 0);
    transform: translate(-3px, -1px);
  }
}

.section-figure {
  position: fixed;
  right: -1%;
  bottom: 0;
  z-index: 0;
  margin: 0;
  /* The art is pre-cropped to its content (no internal margins). */
  width: min(30vw, 36rem);
  pointer-events: none;
}
.section-figure2 {
  position: fixed;
  right: -1%;
  bottom: 0;
  z-index: 0;
  margin: 0;
  /* The art is pre-cropped to its content (no internal margins). */
  width: min(30vw, 36rem);
  pointer-events: none;
}

.section-figure2-img {
  position: fixed;
  right: 0;
  bottom: -1%;
  z-index: 0;
  margin: 0;
  /* The art is pre-cropped to its content (no internal margins). */
  width: min(30vw, 36rem);
  pointer-events: none;
}
@media (max-width: 760px) {
  .section-figure2 {
    width: min(70vw, 24rem);
  }
}

/* Paper clearing under the art so the body dots don't speckle through the
   multiply blend. */
.section-figure::before {
  content: "";
  position: absolute;
  inset: -8%;
  background: radial-gradient(
    ellipse at center,
    var(--paper) 55%,
    rgba(255, 255, 255, 0) 78%
  );
}

.section-figure-img {
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  filter: brightness(1.04);
  mix-blend-mode: multiply;
}

@media (max-width: 760px) {
  .section-figure {
    width: min(70vw, 24rem);
  }
}

/* Projects hand — motion + crack. The hand drifts on a slow idle float so it
   is never quite still, and every ~13s it fractures for a few frames: two ghost
   copies tear sideways in hard bands (the site's channel-split language) while
   an accent bolt strikes across it. The ghosts and the bolt live inside
   .section-figure, so they ride the float and stay locked to the hand as it
   moves. */
.section-figure {
  animation: hand-float 7s ease-in-out infinite;
}

/* Ghost copies stacked exactly over the original hand. No brightness bump and a
   multiply blend so they drop their white ground onto the paper like the main
   image does; invisible until a burst displaces a band of them. */
.section-figure-ghost {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  pointer-events: none;
  mix-blend-mode: multiply;
}

.section-figure-ghost--a {
  animation: hand-crack-a 13s steps(1, end) infinite;
}

.section-figure-ghost--b {
  animation: hand-crack-b 13s steps(1, end) infinite;
}

/* The crack: a jagged accent bolt raked across the back of the hand, painted
   normally (not multiplied) so it glows warm over the ink linework. Dark until
   the burst window lights it. */
.section-figure-crack {
  position: absolute;
  left: 6%;
  top: 30%;
  width: 88%;
  height: 34%;
  margin: 0;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    118deg,
    rgba(255, 155, 81, 0) 0%,
    var(--accent) 42%,
    #ffd8b0 50%,
    var(--accent) 58%,
    rgba(255, 155, 81, 0) 100%
  );
  /* A zig-zag lightning sliver rather than a solid band. */
  clip-path: polygon(
    0 44%, 16% 36%, 28% 58%, 42% 26%, 54% 52%, 68% 20%, 82% 46%, 100% 32%,
    100% 44%, 82% 58%, 68% 32%, 54% 64%, 42% 38%, 28% 70%, 16% 48%, 0 56%
  );
  animation: hand-crack-bolt 13s steps(1, end) infinite;
}

@keyframes hand-float {
  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-10px);
  }
}

/* Ghost A: three displaced bands during the burst, each shoved a few px one
   way. inset() picks the horizontal slice; translateX shoves it. */
@keyframes hand-crack-a {
  0%,
  88.9% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }

  89% {
    opacity: 0.85;
    transform: translateX(-8px);
    clip-path: inset(18% 0 64% 0);
  }

  90.3% {
    opacity: 0.85;
    transform: translateX(6px);
    clip-path: inset(50% 0 32% 0);
  }

  91.6% {
    opacity: 0.8;
    transform: translateX(-5px);
    clip-path: inset(72% 0 12% 0);
  }

  92.8%,
  100% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }
}

/* Ghost B: the counter-shove, offset the other way and on slightly different
   bands, so the two together read as a channel-split tear. */
@keyframes hand-crack-b {
  0%,
  88.9% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }

  89.4% {
    opacity: 0.8;
    transform: translateX(7px);
    clip-path: inset(30% 0 52% 0);
  }

  90.7% {
    opacity: 0.8;
    transform: translateX(-6px);
    clip-path: inset(60% 0 22% 0);
  }

  92% {
    opacity: 0.75;
    transform: translateX(4px);
    clip-path: inset(8% 0 78% 0);
  }

  92.8%,
  100% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }
}

/* The bolt flickers hot a couple of times across the same burst window. */
@keyframes hand-crack-bolt {
  0%,
  88.9% {
    opacity: 0;
  }

  89% {
    opacity: 0.95;
  }

  90% {
    opacity: 0.25;
  }

  90.7% {
    opacity: 1;
  }

  91.8% {
    opacity: 0.4;
  }

  92.6%,
  100% {
    opacity: 0;
  }
}

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

  .section-figure-ghost,
  .section-figure-crack {
    animation: none;
    opacity: 0;
  }
}

/* Placed after the hero animation rules on purpose: these overrides tie on
   specificity, so they must come later in the file to win. */
@media (prefers-reduced-motion: reduce) {
  body.is-loaded .hero-nav-label,
  body.is-loaded .hero-nav-link,
  body.is-loaded .hero-statue-img,
  body.is-loaded .hero-torch-glow {
    animation: none;
    opacity: 1;
  }

  .hero-nav-link:hover::before,
  .hero-nav-link:hover::after,
  .hero-nav-link:focus-visible::before,
  .hero-nav-link:focus-visible::after,
  .hero-nav-link.is-departing::before,
  .hero-nav-link.is-departing::after,
  html,
  .window-bar,
  .window-bar-ghost,
  .window-dot::before,
  .window-dot::after,
  .window-dot:hover::before,
  .window-dot:hover::after,
  .glitch-title {
    animation: none;
  }

  .glitch-title::before,
  .glitch-title::after {
    animation: none;
    opacity: 0;
  }

  .page-wipe,
  .page-wipe-reveal,
  body.is-wiping .page-wipe {
    visibility: hidden;
    animation: none;
  }

  body:has(.page-wipe-reveal) main,
  body:has(.page-wipe-reveal) .window-bar,
  body.is-wiping main,
  body.is-wiping .site-footer,
  body.is-wiping .window-bar,
  body.is-wiping .window-bar-ghost--a,
  body.is-wiping .window-bar-ghost--b,
  body.is-haywire main,
  body.is-haywire .site-footer,
  body.is-haywire .window-bar,
  body.is-haywire .window-bar-ghost--a,
  body.is-haywire .window-bar-ghost--b,
  body.is-haywire::before,
  body.is-haywire::after {
    animation: none;
  }
}

@media (max-width: 760px) {
  .hero {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
    padding-top: clamp(2rem, 6vh, 3rem);
    row-gap: 1.75rem;
  }

  .hero-copy {
    align-self: start;
    padding-bottom: 0;
  }

  .hero-statue {
    justify-self: center;
    width: min(74vw, 21rem);
  }
}
.site-footer {
  position: relative;
  width: 100%;
  height: 60vh;
  min-height: 380px;
  margin-top: auto;
  overflow: hidden;
  isolation: isolate;
  color: var(--paper);
  background: var(--void);
}

.footer-black-hole {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  pointer-events: none;
}

.footer-content {
  position: absolute;
  left: clamp(1.5rem, 5vw, 5rem);
  bottom: clamp(1.5rem, 5vw, 4rem);
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.85rem;
  max-width: min(90vw, 30rem);
  text-align: left;
}

.footer-avatar {
  width: min(150px, 42vw);
  height: auto;
  display: block;
}

.footer-name {
  margin: 0;
  font-size: clamp(1.15rem, 2.4vw, 1.9rem);
  line-height: 1.05;
  letter-spacing: 0.01em;
  color: var(--paper);
}

.footer-links {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.45rem;
  margin-top: 0.15rem;
}

.footer-links a {
  font-size: clamp(0.82rem, 1.4vw, 0.98rem);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--paper);
  text-decoration: none;
  transition: opacity 200ms ease;
}

.footer-links a::after {
  content: " ↗";
  display: inline-block;
  color: var(--paper);
  transition: transform 200ms ease;
}

.footer-links a:hover,
.footer-links a:focus-visible {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.footer-links a:hover::after,
.footer-links a:focus-visible::after {
  transform: translate(2px, -2px);
}

.footer-links a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 2px;
}

.footer-copyright {
  margin: 0.5rem 0 0;
  font-size: clamp(0.68rem, 1.1vw, 0.78rem);
  letter-spacing: 0.04em;
  color: var(--paper);
}

@media (max-width: 760px) {
  /* Stack the hole and the info instead of overlaying them, so text never
     lands on the bright ring on narrow screens. */
  .site-footer {
    height: auto;
    display: flex;
    flex-direction: column;
    padding-bottom: clamp(2rem, 8vw, 3.5rem);
  }

  .footer-black-hole {
    position: relative;
    inset: auto;
    flex: none;
    width: 100%;
    height: 46vh;
    min-height: 300px;
  }

  .footer-content {
    position: static;
    inset: auto;
    max-width: none;
    padding: 0 1.25rem;
    align-items: center;
    text-align: center;
  }

  .footer-links {
    align-items: center;
  }
}

/* My Thoughts hull breach: effect layers remain opt-in so a failed renderer
   leaves a conventional paper page rather than floating type over a void. */
.thoughts-body {
  background-color: var(--paper);
}

.thoughts-blackhole,
.thoughts-hull {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  visibility: hidden;
  display: block;
  pointer-events: none;
}

.thoughts-body.is-fracture-ready {
  background: var(--void);
}

.thoughts-body.is-fracture-ready .thoughts-blackhole,
.thoughts-body.is-fracture-ready .thoughts-hull {
  visibility: visible;
}

.thoughts-blackhole {
  z-index: 0;
}

.thoughts-hull {
  z-index: 1;
}

.thoughts-body .section-page {
  position: relative;
  z-index: 2;
}

.thoughts-body .window-bar-fracture {
  position: absolute;
  top: 0;
  left: 57%;
  z-index: 2;
  width: 160px;
  height: calc(var(--window-bar-h) + 24px);
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='56'%3E%3Cpath fill='%23070b0f' d='M0 0h16v4h8v4h8v4h8v4h8v4h8v4h8v4H56v-4h-8v-4h-8v-4h-8v-4h-8v-4h-8v-4H0zM48 20h16v-4h12v-4h12v4H76v4H64v4H48zM56 28h16v4h24v4H80v20H68v-4H56z'/%3E%3Cpath fill='%2325343f' d='M0 4h12v4h8v4h8v4h8v4h8v4h8v4h8v4H56v-4h-8v-4h-8v-4h-8v-4h-8v-4h-8v-4H0zM52 24h12v-4h12v-4h12v4H80v4H68v4H52zM60 32h12v4h24v4H84v20H72v-4H60z'/%3E%3Crect x='20' y='8' width='2' height='2' fill='%23eaeFEF'/%3E%3Crect x='49' y='18' width='2' height='2' fill='%23ff9b51'/%3E%3Crect x='72' y='13' width='2' height='2' fill='%23eaeFEF'/%3E%3Crect x='83' y='17' width='2' height='2' fill='%23ff9b51'/%3E%3Crect x='72' y='45' width='2' height='2' fill='%23eaeFEF'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  image-rendering: pixelated;
}

.thoughts-body .window-bar-fracture::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='56'%3E%3Crect x='44' y='20' width='4' height='4' fill='%23ff9b51'/%3E%3Crect x='60' y='28' width='8' height='4' fill='%23ff9b51'/%3E%3Crect x='78' y='17' width='4' height='4' fill='%23ff9b51'/%3E%3Crect x='72' y='40' width='4' height='8' fill='%23ff9b51'/%3E%3Crect x='84' y='36' width='8' height='4' fill='%23ff9b51'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  image-rendering: pixelated;
  opacity: 0;
  animation: none;
}

.thoughts-body.is-home-hanging .window-bar-fracture::after {
  opacity: 1;
  animation: thoughts-bar-dent-signal 1600ms steps(1, end) infinite;
}

@keyframes thoughts-bar-dent-signal {
  0% {
    opacity: 0.72;
    clip-path: inset(0 42% 44% 0);
    transform: translate(-3px, 0);
  }

  25% {
    opacity: 0.38;
    clip-path: inset(32% 0 18% 30%);
    transform: translate(4px, 1px);
  }

  50% {
    opacity: 0.9;
    clip-path: inset(48% 24% 0 0);
    transform: translate(-2px, -1px);
  }

  75%,
  100% {
    opacity: 0.48;
    clip-path: inset(12% 0 58% 18%);
    transform: translate(3px, 0);
  }
}

.thoughts-body .section-content {
  max-width: min(
    72ch,
    calc(78vw - clamp(1.5rem, 4vw, 3.5rem) - 32px)
  );
}

.thoughts-body .section-back {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-color: var(--muted);
  background: var(--paper);
  box-shadow: 3px 3px 0 var(--ink);
  color: var(--ink);
  image-rendering: pixelated;
  text-shadow: 1px 0 0 var(--accent), -1px 0 0 var(--muted);
  transform-origin: 11px 0;
  transform: translate(0, 0) rotate(0deg);
  animation: none;
}

.thoughts-body.is-home-hanging .section-back {
  animation:
    thoughts-home-drop 960ms steps(6, end) both,
    thoughts-home-hang 7600ms steps(8, end) 960ms infinite;
}

.thoughts-body.is-home-hang-settled .section-back {
  animation: thoughts-home-hang 7600ms steps(8, end) infinite;
}

.thoughts-body.is-home-hanging .section-back.is-departing {
  animation: none;
}

.thoughts-body .section-back::before,
.thoughts-body .section-back::after {
  content: attr(data-text);
  position: absolute;
  inset: -2px;
  display: flex;
  align-items: center;
  box-sizing: border-box;
  padding: calc(0.5rem + 2px) calc(0.9rem + 2px);
  background: var(--paper);
  border: 2px solid currentColor;
  box-shadow: 3px 3px 0 var(--ink);
  font: inherit;
  letter-spacing: inherit;
  pointer-events: none;
}

.thoughts-body .section-back::before {
  z-index: 1;
  color: var(--accent);
  background-image: linear-gradient(
    90deg,
    transparent 0 13%,
    var(--accent) 13% 21%,
    transparent 21% 53%,
    var(--accent) 53% 60%,
    transparent 60% 100%
  );
  animation: thoughts-home-ghost-a 2600ms steps(1, end) infinite;
}

.thoughts-body .section-back::after {
  z-index: 2;
  color: var(--muted);
  background-image: linear-gradient(
    90deg,
    transparent 0 31%,
    var(--muted) 31% 38%,
    transparent 38% 72%,
    var(--muted) 72% 78%,
    transparent 78% 100%
  );
  animation: thoughts-home-ghost-b 3600ms steps(1, end) infinite;
}

.thoughts-body .section-back:hover,
.thoughts-body .section-back:focus-visible {
  border-color: var(--accent);
  color: var(--ink);
}

.thoughts-body .section-back.is-departing::before,
.thoughts-body .section-back.is-departing::after {
  animation: none;
  opacity: 0;
}

@keyframes thoughts-home-drop {
  0% {
    transform: translate(0, 0) rotate(0deg);
  }

  34% {
    transform: translate(1px, 1px) rotate(5deg);
  }

  67% {
    transform: translate(-1px, 2px) rotate(14deg);
  }

  100% {
    transform: translate(-1px, 3px) rotate(20deg);
  }
}

@keyframes thoughts-home-hang {
  0%,
  100% {
    transform: translate(-1px, 3px) rotate(20deg);
  }

  18% {
    transform: translate(0, 4px) rotate(24deg);
  }

  43% {
    transform: translate(1px, 4px) rotate(18deg);
  }

  67% {
    transform: translate(0, 3px) rotate(26deg);
  }

  84% {
    transform: translate(-1px, 4px) rotate(22deg);
  }
}

@keyframes thoughts-home-ghost-a {
  0% {
    opacity: 0.42;
    clip-path: inset(16% 0 64% 0);
    transform: translate(-2px, 0);
  }

  25% {
    opacity: 0.24;
    clip-path: inset(54% 0 25% 0);
    transform: translate(3px, 0);
  }

  50% {
    opacity: 0.36;
    clip-path: inset(76% 0 7% 0);
    transform: translate(-2px, 0);
  }

  75%,
  100% {
    opacity: 0.2;
    clip-path: inset(34% 0 45% 0);
    transform: translate(2px, 0);
  }
}

@keyframes thoughts-home-ghost-b {
  0% {
    opacity: 0.28;
    clip-path: inset(68% 0 12% 0);
    transform: translate(2px, 0);
  }

  25% {
    opacity: 0.16;
    clip-path: inset(8% 0 75% 0);
    transform: translate(-2px, 0);
  }

  50% {
    opacity: 0.3;
    clip-path: inset(45% 0 32% 0);
    transform: translate(2px, 0);
  }

  75%,
  100% {
    opacity: 0.18;
    clip-path: inset(82% 0 3% 0);
    transform: translate(-2px, 0);
  }
}

@media (max-width: 760px) {
  .thoughts-body .section-header {
    flex-direction: column;
    align-items: flex-start;
  }

  .thoughts-body .section-content {
    max-width: calc(84vw - clamp(1.5rem, 4vw, 3.5rem) - 32px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .thoughts-body .window-bar-fracture::after,
  .thoughts-body .section-back,
  .thoughts-body .section-back::before,
  .thoughts-body .section-back::after {
    animation: none;
  }

  .thoughts-body .section-back::before,
  .thoughts-body .section-back::after {
    opacity: 0;
  }

  .thoughts-body .window-bar-fracture::after {
    opacity: 0;
  }
}

/* Projects: the Creation-of-Adam hand descends out of a fracture torn into
   the paper — the same pixel hull-crack language as My Thoughts. The crack is
   pinned to the page (it never rides the hand); the limb wrapper clips the
   hand so it can only exist below the crack's mouth, and the inner wrapper
   carries the pour-out, the reach, and the glitch bursts.

   Timeline: 0–540ms page wipe → 560–1760ms the crack crawls in from the right
   viewport edge in stepped bursts → 2160–3560ms the hand pours out of the
   mouth → from 4200ms a shared 6800ms clock drives the reach and every glitch
   layer, so the signal tears exactly when the hand strains forward. */
.projects-body .project-hand {
  right: clamp(1.25rem, 5vw, 6rem);
  /* Lifted off the floor so the next-project frame fits under the fingertip. */
  bottom: clamp(90px, 14vh, 150px);
  width: min(18vw, 22rem);
  animation: none;
}

/* Reserve a corridor for the hand scene: between the mobile stage and full
   desktop, the grid would otherwise slide under the crack and hand. The
   content column gives up the figure's width plus breathing room, shrinking
   the grid to fewer columns instead of hiding the scene. */
.projects-body .section-content {
  max-width: min(
    55rem,
    calc(
      100vw - min(18vw, 22rem) - clamp(1.25rem, 5vw, 6rem) -
        clamp(1.5rem, 4vw, 3.5rem) - 3rem
    )
  );
}

/* The Sistine gap: the next project frame waits just below-left of the
   resting fingertip. It lives outside the limb wrappers (world layer, under
   the hand) so the finger closes the gap and lands on its corner when the
   reach pushes down-left. */
.projects-body .project-next-frame {
  position: absolute;
  left: calc(35% - 13.5rem - 4px);
  top: calc(85% + 12px);
  z-index: 0;
  display: flex;
  flex-direction: column;
  width: 13.5rem;
  min-height: 8.5rem;
  margin: 0;
  border: 2px dashed rgba(37, 52, 63, 0.3);
  background: var(--paper);
  box-shadow: 5px 5px 0 rgba(191, 201, 209, 0.55);
  image-rendering: pixelated;
  pointer-events: none;
  animation:
    project-frame-boot 460ms steps(4, end) 1130ms both,
    project-next-touch 6800ms steps(1, end) 4200ms infinite;
}

.projects-body .project-next-frame .project-frame-bar {
  border-bottom-style: dashed;
  border-bottom-color: rgba(37, 52, 63, 0.3);
}

.projects-body .project-next-frame .project-frame-dot {
  border-color: rgba(37, 52, 63, 0.35);
}

.projects-body .project-next-frame .project-frame-body {
  align-items: center;
  justify-content: center;
}

.projects-body .project-next-frame .project-frame-empty-label {
  animation: project-next-label 6800ms steps(1, end) 4200ms infinite;
}

/* The fracture: a void mouth above the wrist, a hairline tail dying off to
   the left, and a trunk that runs off the right edge of the screen — rooted
   at the viewport edge like the Thoughts breach. Drawn on a 4px grid: ink
   shadow offset (4,4) under a --void core, muted lip ticks, starlight pixels
   inside the opening, two accent signal pixels. */
.projects-body .project-hand-crack {
  position: absolute;
  top: -20px;
  left: -16%;
  right: calc(clamp(1.25rem, 5vw, 6rem) * -1 - 4px);
  height: 80px;
  z-index: 0;
  pointer-events: none;
  animation: project-crack-tear 1200ms steps(1, end) 560ms both;
}

.projects-body .project-hand-crack::before {
  content: "";
  position: absolute;
  left: calc(54% - 160px);
  top: 0;
  width: 320px;
  height: 80px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='80'%3E%3Cg transform='translate(4 4)'%3E%3Cpath fill='%2325343f' d='M0 44h12v4h-12zM12 40h8v4h-8zM20 40h4v8h-4zM24 36h8v8h-8zM32 32h8v12h-8zM40 28h8v20h-8zM48 24h8v28h-8zM56 24h4v32h-4zM60 20h8v36h-8zM68 20h4v40h-4zM72 16h8v44h-8zM80 16h8v48h-8zM88 12h4v52h-4zM92 12h8v56h-8zM100 8h4v60h-4zM104 0h4v68h-4zM108 8h4v60h-4zM112 4h8v64h-8zM120 4h8v68h-8zM128 0h12v72h-12zM140 4h4v68h-4zM144 4h8v64h-8zM152 8h4v64h-4zM156 8h4v64h-4zM160 8h4v72h-4zM164 8h4v64h-4zM168 4h8v64h-8zM176 4h8v64h-8zM184 8h4v60h-4zM188 8h8v56h-8zM196 4h4v60h-4zM200 4h4v56h-4zM204 0h4v60h-4zM208 8h4v52h-4zM212 12h8v44h-8zM220 12h4v40h-4zM224 16h8v36h-8zM232 16h4v32h-4zM236 20h8v28h-8zM244 20h4v24h-4zM248 24h8v20h-8zM256 24h4v16h-4zM260 24h8v12h-8zM268 28h12v8h-12zM280 28h40v8h-40z'/%3E%3C/g%3E%3Cpath fill='%23020304' d='M0 44h12v4h-12zM12 40h8v4h-8zM20 40h4v8h-4zM24 36h8v8h-8zM32 32h8v12h-8zM40 28h8v20h-8zM48 24h8v28h-8zM56 24h4v32h-4zM60 20h8v36h-8zM68 20h4v40h-4zM72 16h8v44h-8zM80 16h8v48h-8zM88 12h4v52h-4zM92 12h8v56h-8zM100 8h4v60h-4zM104 0h4v68h-4zM108 8h4v60h-4zM112 4h8v64h-8zM120 4h8v68h-8zM128 0h12v72h-12zM140 4h4v68h-4zM144 4h8v64h-8zM152 8h4v64h-4zM156 8h4v64h-4zM160 8h4v72h-4zM164 8h4v64h-4zM168 4h8v64h-8zM176 4h8v64h-8zM184 8h4v60h-4zM188 8h8v56h-8zM196 4h4v60h-4zM200 4h4v56h-4zM204 0h4v60h-4zM208 8h4v52h-4zM212 12h8v44h-8zM220 12h4v40h-4zM224 16h8v36h-8zM232 16h4v32h-4zM236 20h8v28h-8zM244 20h4v24h-4zM248 24h8v20h-8zM256 24h4v16h-4zM260 24h8v12h-8zM268 28h12v8h-12zM280 28h40v8h-40z'/%3E%3Cpath fill='%23bfc9d1' d='M72 14h8v2h-8zM144 2h8v2h-8zM212 10h8v2h-8zM256 22h8v2h-8zM24 34h8v2h-8zM84 68h8v2h-8zM132 76h10v2h-10zM192 68h8v2h-8z'/%3E%3Cpath fill='%23fff1b8' d='M96 24h4v4h-4zM176 48h2v2h-2z'/%3E%3Cpath fill='%23eaefef' d='M136 16h2v2h-2zM156 60h2v2h-2zM216 32h2v2h-2z'/%3E%3Cpath fill='%23ff9b51' d='M124 8h2v2h-2zM240 40h2v2h-2z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  image-rendering: pixelated;
}

/* The trunk running to (and past) the right screen edge: a repeating 4px
   zigzag hairline with sparse ink under-ticks, seam-matched to the mouth's
   right stub at y28. */
.projects-body .project-hand-crack::after {
  content: "";
  position: absolute;
  left: calc(54% + 160px);
  right: 0;
  top: 28px;
  height: 12px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='12'%3E%3Cpath fill='%23070b0f' d='M0 0h12v4h-12zM12 4h8v4h-8zM20 0h12v4h-12zM32 4h4v4h-4zM36 0h12v4h-12z'/%3E%3Cpath fill='%2325343f' d='M12 8h8v2h-8zM32 8h4v2h-4z'/%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-position: left top;
  image-rendering: pixelated;
}

/* Accent pixels around the mouth that flicker on the burst clock — the same
   dent-signal idea the Thoughts window bar uses. */
.projects-body .project-hand-crack-signal {
  position: absolute;
  left: calc(54% - 20px);
  top: 4px;
  width: 4px;
  height: 4px;
  background: var(--accent);
  box-shadow:
    48px 26px 0 var(--accent),
    -36px 34px 0 var(--accent),
    74px 44px 0 rgba(255, 155, 81, 0.75),
    16px 58px 0 rgba(255, 155, 81, 0.6);
  opacity: 0;
  pointer-events: none;
  animation: project-crack-signal 6800ms steps(1, end) 4200ms infinite;
}

/* The limb window: everything above the mouth's underside is "still inside
   the void", so the hand can only exist below it. The padding gives the art
   headroom so it rests just under the crack. */
.projects-body .project-hand-limb {
  position: relative;
  z-index: 1;
  padding-top: 36px;
  clip-path: inset(52px 0 0 0);
}

.projects-body .project-hand-limb-inner {
  position: relative;
  animation:
    project-hand-pour 1400ms steps(1, end) 2160ms both,
    project-hand-reach 6800ms steps(1, end) 4200ms infinite;
}

/* Signal-loss dropout: two paper bars that blank slices of the hand for a
   frame or two during the burst. */
.projects-body .project-hand-limb-inner::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  opacity: 0;
  background: linear-gradient(
    to bottom,
    transparent 0 37%,
    var(--paper) 37% 42%,
    transparent 42% 70%,
    var(--paper) 70% 74%,
    transparent 74% 100%
  );
  pointer-events: none;
  animation: project-hand-dropout 6800ms steps(1, end) 4200ms infinite;
}

/* The art's white JPEG ground becomes true alpha via the inline SVG filters,
   so the hand genuinely overlaps the frame below it — no white box, and no
   reliance on blend modes that the animated wrappers would isolate. */
.projects-body .project-hand-image {
  z-index: 1;
  mix-blend-mode: normal;
  filter: url("#ink-alpha");
  image-rendering: pixelated;
}

.projects-body .section-figure-ghost--a,
.projects-body .section-figure-ghost--b {
  mix-blend-mode: normal;
  filter: url("#ink-alpha");
}

.projects-body .project-hand-glitch {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  opacity: 0;
  filter: url("#ink-alpha-accent");
  mix-blend-mode: normal;
  image-rendering: pixelated;
  pointer-events: none;
  animation: project-hand-glitchband 6800ms steps(1, end) 4200ms infinite;
}

/* The dark ghosts counter-tear against the orange bands: projects runs them
   on the shared burst clock instead of the base 13s loop. */
.projects-body .section-figure-ghost--a {
  animation: project-ghost-a 6800ms steps(1, end) 4200ms infinite;
}

.projects-body .section-figure-ghost--b {
  animation: project-ghost-b 6800ms steps(1, end) 4200ms infinite;
}

/* The accent seam across the knuckles: a pixel-stepped bolt (no smooth
   gradients, no diagonal anti-aliasing) that only lights during bursts. */
.projects-body .section-figure-crack {
  left: 12%;
  top: 42%;
  width: 160px;
  height: 24px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='24'%3E%3Cpath fill='%23ff9b51' d='M0 12h12v4h-12zM12 8h12v4h-12zM24 12h8v4h-8zM32 16h12v4h-12zM44 12h12v4h-12zM56 8h8v4h-8zM64 4h12v4h-12zM76 8h12v4h-12zM88 12h8v4h-8zM96 8h12v4h-12zM108 12h12v4h-12zM120 16h8v4h-8zM128 12h12v4h-12zM140 8h12v4h-12zM152 12h8v4h-8z'/%3E%3Cpath fill='%23ffd8b0' d='M16 9h6v2h-6zM66 5h6v2h-6zM98 9h6v2h-6zM142 9h6v2h-6z'/%3E%3Cpath fill='%23fff1b8' d='M30 4h2v2h-2zM124 20h2v2h-2z'/%3E%3C/svg%3E")
    no-repeat left top;
  clip-path: none;
  z-index: 3;
  image-rendering: pixelated;
  animation: project-bolt-flash 6800ms steps(1, end) 4200ms infinite;
}

.projects-body .project-touch-signal {
  position: absolute;
  left: 34%;
  top: 83%;
  z-index: 5;
  width: 8px;
  height: 8px;
  opacity: 0.55;
  background: var(--accent);
  box-shadow:
    -5px 0 0 var(--accent),
    5px 0 0 var(--accent),
    0 -5px 0 var(--accent),
    0 5px 0 var(--accent);
  image-rendering: pixelated;
  pointer-events: none;
  animation: project-touch-signal 6800ms steps(1, end) 4200ms infinite;
}

/* Return visits within a browsing session land on the settled scene — the
   crack already torn, the hand already through — while the reach/glitch
   loops keep running. The inline script in projects.html decides, so
   reloads and fresh sessions still replay the entrance. */
html.is-return-visit .projects-body .project-hand-crack {
  animation: none;
}

html.is-return-visit .projects-body .project-hand-limb-inner {
  animation: project-hand-reach 6800ms steps(1, end) 4200ms infinite;
}

html.is-return-visit .projects-body .project-next-frame {
  animation: project-next-touch 6800ms steps(1, end) 4200ms infinite;
}

html.is-return-visit .project-frame {
  animation: none;
}

/* Stress travels through the hull in seeded bursts: uneven holds between
   steps, right edge first, tail tip last. */
@keyframes project-crack-tear {
  0% {
    clip-path: inset(-20px 0 -20px 100%);
  }

  9% {
    clip-path: inset(-20px 0 -20px 86%);
  }

  22% {
    clip-path: inset(-20px 0 -20px 74%);
  }

  30% {
    clip-path: inset(-20px 0 -20px 68%);
  }

  44% {
    clip-path: inset(-20px 0 -20px 52%);
  }

  58% {
    clip-path: inset(-20px 0 -20px 40%);
  }

  72% {
    clip-path: inset(-20px 0 -20px 26%);
  }

  84% {
    clip-path: inset(-20px 0 -20px 12%);
  }

  100% {
    clip-path: inset(-20px 0 -20px 0);
  }
}

/* The hand pours out of the mouth: the clip grows downward while the art
   drops, so it emerges from the darkness instead of sliding over the paper. */
@keyframes project-hand-pour {
  0% {
    transform: translateY(-236px);
    clip-path: inset(0 0 96% 0);
  }

  10% {
    transform: translateY(-236px);
    clip-path: inset(0 0 88% 0);
  }

  22% {
    transform: translateY(-192px);
    clip-path: inset(0 0 74% 0);
  }

  34% {
    transform: translateY(-192px);
    clip-path: inset(0 0 70% 0);
  }

  44% {
    transform: translateY(-144px);
    clip-path: inset(0 0 56% 0);
  }

  56% {
    transform: translateY(-96px);
    clip-path: inset(0 0 38% 0);
  }

  68% {
    transform: translateY(-56px);
    clip-path: inset(0 0 22% 0);
  }

  80% {
    transform: translateY(-24px);
    clip-path: inset(0 0 9% 0);
  }

  92%,
  100% {
    transform: translateY(0);
    clip-path: inset(0 0 0 0);
  }
}

/* The reach now strains down-left, toward the waiting frame: the fingertip
   crosses the gap at 70% and rests on the frame's corner for a few frames. */
@keyframes project-hand-reach {
  0%,
  56%,
  100% {
    transform: translate(0, 0);
  }

  64% {
    transform: translate(-5px, 3px);
  }

  70% {
    transform: translate(-14px, 10px);
  }

  77% {
    transform: translate(-9px, 6px);
  }

  85% {
    transform: translate(-3px, 1px);
  }
}

/* Contact reaction: a spark jumps the gap just before the touch, then the
   frame jolts and flashes accent while the finger rests on it. */
@keyframes project-next-touch {
  0%,
  66.9% {
    border-color: rgba(37, 52, 63, 0.3);
    box-shadow: 5px 5px 0 rgba(191, 201, 209, 0.55);
    transform: translate(0, 0);
  }

  67% {
    border-color: var(--accent);
    box-shadow: 5px 5px 0 rgba(191, 201, 209, 0.55);
    transform: translate(0, 0);
  }

  68.5% {
    border-color: rgba(37, 52, 63, 0.3);
    box-shadow: 5px 5px 0 rgba(191, 201, 209, 0.55);
    transform: translate(0, 0);
  }

  70% {
    border-color: var(--accent);
    box-shadow: 5px 5px 0 rgba(255, 155, 81, 0.8);
    transform: translate(1px, 2px);
  }

  73.5% {
    border-color: var(--accent);
    box-shadow: 5px 5px 0 rgba(255, 155, 81, 0.5);
    transform: translate(0, 1px);
  }

  77% {
    border-color: rgba(37, 52, 63, 0.45);
    box-shadow: 5px 5px 0 rgba(191, 201, 209, 0.55);
    transform: translate(0, 0);
  }

  85%,
  100% {
    border-color: rgba(37, 52, 63, 0.3);
    box-shadow: 5px 5px 0 rgba(191, 201, 209, 0.55);
    transform: translate(0, 0);
  }
}

@keyframes project-next-label {
  0%,
  69.9% {
    color: rgba(37, 52, 63, 0.4);
  }

  70% {
    color: var(--accent);
  }

  77% {
    color: rgba(37, 52, 63, 0.55);
  }

  85%,
  100% {
    color: rgba(37, 52, 63, 0.4);
  }
}

/* Orange channel bands: a small mid-cycle tick, then the main tear synced
   with the reach push (63–74%). */
@keyframes project-hand-glitchband {
  0%,
  20.9% {
    opacity: 0;
    clip-path: inset(100% 0 0 0);
    transform: translate(0, 0);
  }

  21% {
    opacity: 0.72;
    clip-path: inset(58% 0 34% 0);
    transform: translate(-6px, 0);
  }

  23.4% {
    opacity: 0.5;
    clip-path: inset(24% 0 68% 0);
    transform: translate(5px, 0);
  }

  25.6%,
  62.9% {
    opacity: 0;
    clip-path: inset(100% 0 0 0);
    transform: translate(0, 0);
  }

  63% {
    opacity: 0.92;
    clip-path: inset(50% 0 40% 0);
    transform: translate(-10px, 1px);
  }

  65.4% {
    opacity: 0.6;
    clip-path: inset(18% 0 72% 0);
    transform: translate(8px, -1px);
  }

  68.8% {
    opacity: 0.85;
    clip-path: inset(64% 0 24% 0);
    transform: translate(-7px, 0);
  }

  71.6% {
    opacity: 0.7;
    clip-path: inset(32% 0 58% 0);
    transform: translate(9px, 1px);
  }

  74.4%,
  100% {
    opacity: 0;
    clip-path: inset(100% 0 0 0);
    transform: translate(0, 0);
  }
}

@keyframes project-ghost-a {
  0%,
  21.4% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }

  21.5% {
    opacity: 0.8;
    transform: translateX(-7px);
    clip-path: inset(30% 0 58% 0);
  }

  24% {
    opacity: 0.7;
    transform: translateX(5px);
    clip-path: inset(66% 0 24% 0);
  }

  26%,
  63.4% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }

  63.5% {
    opacity: 0.85;
    transform: translateX(-9px);
    clip-path: inset(12% 0 74% 0);
  }

  66.2% {
    opacity: 0.85;
    transform: translateX(7px);
    clip-path: inset(46% 0 40% 0);
  }

  69.8% {
    opacity: 0.8;
    transform: translateX(-6px);
    clip-path: inset(70% 0 18% 0);
  }

  73%,
  100% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }
}

@keyframes project-ghost-b {
  0%,
  22.2% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }

  22.3% {
    opacity: 0.75;
    transform: translateX(6px);
    clip-path: inset(48% 0 40% 0);
  }

  24.8% {
    opacity: 0.6;
    transform: translateX(-5px);
    clip-path: inset(10% 0 80% 0);
  }

  26.6%,
  64.4% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }

  64.5% {
    opacity: 0.8;
    transform: translateX(8px);
    clip-path: inset(30% 0 58% 0);
  }

  67.6% {
    opacity: 0.8;
    transform: translateX(-7px);
    clip-path: inset(55% 0 30% 0);
  }

  71.2% {
    opacity: 0.75;
    transform: translateX(5px);
    clip-path: inset(6% 0 82% 0);
  }

  74.2%,
  100% {
    opacity: 0;
    transform: translateX(0);
    clip-path: inset(0 0 100% 0);
  }
}

@keyframes project-hand-dropout {
  0%,
  64.4% {
    opacity: 0;
    transform: translateY(0);
  }

  64.5% {
    opacity: 1;
    transform: translateY(0);
  }

  66.8% {
    opacity: 0;
    transform: translateY(0);
  }

  70.4% {
    opacity: 1;
    transform: translateY(3px);
  }

  72.4%,
  100% {
    opacity: 0;
    transform: translateY(0);
  }
}

@keyframes project-bolt-flash {
  0%,
  20.9% {
    opacity: 0;
    transform: translateX(0);
  }

  21% {
    opacity: 0.55;
    transform: translateX(0);
  }

  23.2%,
  62.9% {
    opacity: 0;
    transform: translateX(0);
  }

  63% {
    opacity: 0.95;
    transform: translateX(0);
  }

  65.5% {
    opacity: 0.25;
    transform: translateX(0);
  }

  68% {
    opacity: 1;
    transform: translateX(-4px);
  }

  70.8% {
    opacity: 0.4;
    transform: translateX(2px);
  }

  73.2%,
  100% {
    opacity: 0;
    transform: translateX(0);
  }
}

@keyframes project-crack-signal {
  0%,
  7.9% {
    opacity: 0;
    transform: translate(0, 0);
  }

  8% {
    opacity: 0.5;
    transform: translate(0, 0);
  }

  10.4%,
  62.9% {
    opacity: 0;
    transform: translate(0, 0);
  }

  63% {
    opacity: 0.9;
    transform: translate(-3px, 0);
  }

  65.5% {
    opacity: 0.45;
    transform: translate(4px, 1px);
  }

  68.4% {
    opacity: 1;
    transform: translate(-2px, -1px);
  }

  71.4% {
    opacity: 0.5;
    transform: translate(3px, 0);
  }

  74%,
  100% {
    opacity: 0;
    transform: translate(0, 0);
  }
}

@keyframes project-touch-signal {
  0%,
  58%,
  100% {
    opacity: 0.55;
    transform: translate(0, 0);
  }

  64% {
    opacity: 0.45;
    transform: translate(-2px, 1px);
  }

  70% {
    opacity: 1;
    transform: translate(-3px, 3px);
  }

  77% {
    opacity: 0.6;
    transform: translate(-2px, 2px);
  }

  85% {
    opacity: 0.2;
    transform: translate(0, 0);
  }
}

/* Project frames: each project gets its own little pixel window — the same
   chrome language as the top bar, shrunk to a card. Filled frames snap up a
   notch on hover; empty frames are dashed slots waiting for an upload. */
.section-placeholder-accent {
  color: var(--accent);
}

.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1.5rem;
  margin-top: clamp(1.75rem, 4vh, 2.75rem);
}

.project-frame {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 11.5rem;
  border: 2px solid var(--ink);
  background: var(--paper);
  box-shadow: 5px 5px 0 var(--ink);
  image-rendering: pixelated;
  animation: project-frame-boot 460ms steps(4, end) 620ms both;
}

.project-frame:nth-child(2) {
  animation-delay: 790ms;
}

.project-frame:nth-child(3) {
  animation-delay: 960ms;
}

.project-frame:nth-child(n + 4) {
  animation-delay: 1130ms;
}

.project-frame:not(.is-empty):hover {
  transform: translate(-2px, -2px);
  border-color: var(--accent);
  box-shadow: 7px 7px 0 var(--ink);
}

.project-frame-bar {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 8px;
  border-bottom: 2px solid var(--ink);
}

.project-frame-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border: 1px solid var(--ink);
}

.project-frame-dot--close {
  background: #e8564e;
}

.project-frame-dot--min {
  background: #e2a93c;
}

.project-frame-dot--max {
  background: #4fae57;
}

.project-frame-id {
  margin-left: auto;
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  color: rgba(37, 52, 63, 0.45);
}

.project-frame-body {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: 0.55rem;
  padding: 0.95rem 1rem 1.1rem;
}

.project-frame-title {
  margin: 0;
  font-size: 1.02rem;
  letter-spacing: 0.02em;
  color: var(--ink);
}

.project-frame-meta {
  margin: 0;
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(37, 52, 63, 0.55);
}

.project-frame-text {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.55;
  color: var(--ink);
}

.project-frame-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: auto 0 0;
  padding-top: 0.35rem;
}

.project-frame-tags span {
  padding: 2px 7px;
  border: 1px solid rgba(37, 52, 63, 0.4);
  background: rgba(191, 201, 209, 0.16);
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(37, 52, 63, 0.7);
}

.project-frame.is-empty {
  border-style: dashed;
  border-color: rgba(37, 52, 63, 0.3);
  box-shadow: 5px 5px 0 rgba(191, 201, 209, 0.55);
}

.project-frame.is-empty .project-frame-bar {
  border-bottom-style: dashed;
  border-bottom-color: rgba(37, 52, 63, 0.3);
}

.project-frame.is-empty .project-frame-dot {
  background: transparent;
  border-color: rgba(37, 52, 63, 0.35);
}

.project-frame.is-empty .project-frame-body {
  align-items: center;
  justify-content: center;
}

.project-frame-empty-label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(37, 52, 63, 0.4);
}

.project-frame-cursor {
  flex: none;
  width: 7px;
  height: 13px;
  background: var(--accent);
  animation: project-cursor-blink 1100ms steps(1, end) infinite;
}

/* Frames boot like little CRTs: bar first, then the body scans in. */
@keyframes project-frame-boot {
  0% {
    opacity: 0;
    clip-path: inset(0 0 100% 0);
  }

  35% {
    opacity: 1;
    clip-path: inset(0 0 78% 0);
  }

  70% {
    opacity: 1;
    clip-path: inset(0 0 34% 0);
  }

  100% {
    opacity: 1;
    clip-path: inset(0 0 0 0);
  }
}

@keyframes project-cursor-blink {
  0%,
  54% {
    opacity: 1;
  }

  55%,
  100% {
    opacity: 0;
  }
}

@media (max-width: 760px) {
  .projects-body .project-hand {
    right: -3vw;
    bottom: clamp(110px, 20vh, 170px);
    width: min(56vw, 20rem);
  }

  .projects-body .project-hand-crack {
    right: calc(-3vw - 4px);
  }

  .projects-body .project-next-frame {
    width: 11.5rem;
    min-height: 7.5rem;
    left: calc(35% - 11.5rem - 4px);
  }

  /* On the phone stage the hand scene lives below the content, so the
     corridor reservation no longer applies. */
  .projects-body .section-content {
    max-width: 55rem;
  }

  /* The frames paint over the fixed hand (content wins), so reserve a clean
     stage at the end of the page: scrolling past the last frame reveals the
     full crack-and-hand scene on empty paper. */
  .projects-body .section-page {
    padding-bottom: min(180vw, 58rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .projects-body .project-hand,
  .projects-body .project-hand-crack,
  .projects-body .project-hand-limb-inner,
  .projects-body .project-hand-limb-inner::after,
  .projects-body .project-hand-image,
  .projects-body .project-hand-glitch,
  .projects-body .section-figure-ghost--a,
  .projects-body .section-figure-ghost--b,
  .projects-body .section-figure-crack,
  .projects-body .project-hand-crack-signal,
  .projects-body .project-touch-signal,
  .projects-body .project-next-frame,
  .projects-body .project-next-frame .project-frame-empty-label,
  .project-frame,
  .project-frame-cursor {
    animation: none;
  }

  .projects-body .project-hand-glitch,
  .projects-body .project-hand-limb-inner::after,
  .projects-body .section-figure-ghost--a,
  .projects-body .section-figure-ghost--b,
  .projects-body .section-figure-crack,
  .projects-body .project-hand-crack-signal {
    opacity: 0;
  }

  /* Reduced motion outranks the return-visit shortcut, which would
     otherwise re-arm the reach and touch loops. */
  html.is-return-visit .projects-body .project-hand-limb-inner,
  html.is-return-visit .projects-body .project-next-frame {
    animation: none;
  }
}

/* Experience: the checkmate. One artwork sliced into clip-path layers — the
   queen starts standing (the fallen art rotated upright on the corner of its
   base), the suited hand descends holding the pawn, and on contact the queen
   topples into the exact drawn pose while the impact sparks flash. The
   whole-figure idle float from .section-figure keeps running underneath. */
.experience-body .chess-scene {
  position: relative;
  animation: chess-jolt 3400ms 560ms both;
}

.experience-body .chess-layer {
  display: block;
  width: 100%;
  height: auto;
  mix-blend-mode: normal;
  filter: url("#ink-alpha");
  image-rendering: pixelated;
}

.experience-body .chess-floor {
  position: relative;
  clip-path: inset(97.9% 43% 0 12.5%);
}

.experience-body .chess-queen,
.experience-body .chess-sparks,
.experience-body .chess-hand {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.experience-body .chess-queen {
  clip-path: polygon(
    6.5% 41.5%,
    24.9% 41.5%,
    24.9% 66.5%,
    34.6% 66.5%,
    34.6% 74.9%,
    45% 74.9%,
    45% 97.8%,
    6.5% 97.8%
  );
  transform-origin: 29.4% 97.6%;
  animation: chess-queen-fall 3400ms 560ms both;
}

.experience-body .chess-sparks {
  clip-path: inset(47.3% 68.8% 33.45% 24.96%);
  opacity: 0;
  animation:
    chess-sparks-hit 3400ms 560ms both,
    chess-sparks-glint 9400ms steps(1, end) 4200ms infinite;
}

.experience-body .chess-hand {
  clip-path: polygon(
    29.4% 0%,
    100% 0%,
    100% 62%,
    52% 62%,
    52% 74.7%,
    34.5% 74.7%,
    34.5% 43.5%,
    29.4% 43.5%
  );
  animation: chess-hand-descend 3400ms 600ms both;
}

/* The hand rides in from the top-right corner in chunky sprite steps, then
   the strike gets physical: a smooth follow-through into the queen and a
   small recoil as the force transfers. */
@keyframes chess-hand-descend {
  0%,
  1.2% {
    transform: translate(34%, -32%);
    animation-timing-function: steps(1, end);
  }

  5.2% {
    transform: translate(27%, -25.5%);
    animation-timing-function: steps(1, end);
  }

  9.9% {
    transform: translate(20.5%, -19%);
    animation-timing-function: steps(1, end);
  }

  14.9% {
    transform: translate(14%, -13%);
    animation-timing-function: steps(1, end);
  }

  19.9% {
    transform: translate(8%, -7.5%);
    animation-timing-function: steps(1, end);
  }

  24.9% {
    transform: translate(3.5%, -3%);
    animation-timing-function: steps(1, end);
  }

  29.2% {
    transform: translate(0, 0);
    animation-timing-function: ease-out;
  }

  30.4% {
    transform: translate(-0.5%, 0.55%);
    animation-timing-function: ease-in-out;
  }

  33% {
    transform: translate(0.85%, -1.05%);
    animation-timing-function: ease-in-out;
  }

  36.5% {
    transform: translate(-0.15%, 0.15%);
    animation-timing-function: ease-out;
  }

  39.5%,
  100% {
    transform: translate(0, 0);
  }
}

/* The topple, in acts: the pawn's knock rocks the queen, she teeters at the
   tipping point for a beat, then gravity wins — a long accelerating fall,
   a hard landing, and two damped corner bounces. */
@keyframes chess-queen-fall {
  0%,
  29.4% {
    transform: rotate(32deg);
    animation-timing-function: ease-out;
  }

  32% {
    transform: rotate(29.6deg);
    animation-timing-function: ease-in-out;
  }

  35.5% {
    transform: rotate(31.7deg);
    animation-timing-function: ease-in-out;
  }

  39% {
    transform: rotate(30.1deg);
    animation-timing-function: ease-in-out;
  }

  43% {
    transform: rotate(29.8deg);
    animation-timing-function: cubic-bezier(0.72, 0.02, 0.94, 0.38);
  }

  73% {
    transform: rotate(0deg);
    animation-timing-function: cubic-bezier(0.16, 0.84, 0.44, 1);
  }

  78% {
    transform: rotate(5.4deg);
    animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
  }

  82.5% {
    transform: rotate(0deg);
    animation-timing-function: cubic-bezier(0.16, 0.84, 0.44, 1);
  }

  86.5% {
    transform: rotate(1.9deg);
    animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
  }

  90%,
  100% {
    transform: rotate(0deg);
  }
}

/* The board shudders as the queen lands, decaying smoothly. */
@keyframes chess-jolt {
  0%,
  72.9% {
    transform: translate(0, 0);
    animation-timing-function: ease-out;
  }

  74% {
    transform: translate(-1px, 3px);
    animation-timing-function: ease-in-out;
  }

  77% {
    transform: translate(1px, -1px);
    animation-timing-function: ease-in-out;
  }

  80% {
    transform: translate(-0.5px, 0.5px);
    animation-timing-function: ease-out;
  }

  83%,
  100% {
    transform: translate(0, 0);
  }
}

/* Impact sparks, twice: a bright clash when the pawn strikes, a lingering
   flicker while she falls, and a second burst on landing before settling
   lit — they are part of the drawn artwork's final state. */
@keyframes chess-sparks-hit {
  0%,
  29% {
    opacity: 0;
    transform: translate(0, 0);
    animation-timing-function: ease-out;
  }

  30% {
    opacity: 1;
    transform: translate(2px, -2px);
    animation-timing-function: ease-in;
  }

  34% {
    opacity: 0.3;
    transform: translate(0, 0);
    animation-timing-function: ease-in-out;
  }

  38.5% {
    opacity: 0.65;
    transform: translate(-1px, 1px);
    animation-timing-function: ease-in-out;
  }

  45% {
    opacity: 0.25;
    transform: translate(0, 0);
    animation-timing-function: ease-in-out;
  }

  60% {
    opacity: 0.4;
    transform: translate(0, 0);
    animation-timing-function: ease-in-out;
  }

  72.9% {
    opacity: 0.45;
    transform: translate(0, 0);
    animation-timing-function: ease-out;
  }

  74.5% {
    opacity: 1;
    transform: translate(-2px, 1px);
    animation-timing-function: ease-in-out;
  }

  81% {
    opacity: 0.55;
    transform: translate(0, 0);
    animation-timing-function: ease-in-out;
  }

  90%,
  100% {
    opacity: 1;
    transform: translate(0, 0);
  }
}

@keyframes chess-sparks-glint {
  0%,
  96.9% {
    opacity: 1;
  }

  97% {
    opacity: 0.25;
  }

  98.2%,
  100% {
    opacity: 1;
  }
}

/* Return visits within a session land on the settled checkmate; reloads and
   fresh sessions replay it (the inline script in experience.html decides). */
html.is-return-visit .experience-body .chess-scene,
html.is-return-visit .experience-body .chess-queen,
html.is-return-visit .experience-body .chess-hand {
  animation: none;
}

html.is-return-visit .experience-body .chess-sparks {
  opacity: 1;
  animation: chess-sparks-glint 9400ms steps(1, end) 3000ms infinite;
}

@media (prefers-reduced-motion: reduce) {
  .experience-body .chess-scene,
  .experience-body .chess-queen,
  .experience-body .chess-sparks,
  .experience-body .chess-hand,
  html.is-return-visit .experience-body .chess-sparks {
    animation: none;
  }

  .experience-body .chess-sparks {
    opacity: 1;
  }
}
