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

/* Hide the scrollbar without disabling scrolling. The page still scrolls by
   wheel, trackpad, touch, arrow keys, Page Up/Down, Home/End and spacebar, and
   the scroll-linked reveals are unaffected because they read scrollY rather
   than the scrollbar. What is lost is the position indicator and drag-to-scroll. */
html {
  /* `clip` rather than `hidden`: hidden makes the element a scroll container,
     which on mobile still permits panning and can break position:fixed
     children. clip simply clips, and unlike hidden it does not force
     overflow-y to auto.

     Needed because several things deliberately extend past the viewport:
     .construct sits at right:-40% on mobile, and the .sweep band is 120vw wide
     travelling from -140vw to 120vw. Without this they create scrollable area,
     which is what allowed the page to be panned sideways and pinched around. */
  overflow-x: clip;
  scrollbar-width: none;        /* Firefox */
  -ms-overflow-style: none;     /* legacy Edge */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;                /* Chrome, Safari, current Edge */
}

body {
  background: var(--void);
  color: var(--ink);
  font-family: var(--sans);
  overflow-x: clip;
  max-width: 100%;
  -webkit-font-smoothing: antialiased;
}
::selection {
  background: var(--red);
  color: #fff;
}
@media (pointer: fine) {
  .has-custom-cursor body,
  .has-custom-cursor a {
    cursor: none;
  }
}

/* ═══════════ TEXTURE ═══════════ */
.tex {
  position: fixed;
  inset: 0;
  pointer-events: none;
  background-repeat: no-repeat;
  background-size: cover;
}
/* Halftone dot field, drawn to canvas so the wave can be driven by scroll.
   Replaces the static CSS radial-gradient version. */
.tex.dotfield {
  z-index: 1;
  display: block;
}

/* ── the dirt ──
   All three use `screen`, not `overlay`. Overlay computes 2 × backdrop × source,
   so against a #060607 ground (luminance ~0.02) it returns ~0.04 × source -
   which is still black. Overlay preserves the backdrop's shadows, so on a dark
   site it does essentially nothing. Screen adds light instead, which is what
   makes a texture visible over near-black.

   `grain.webp` is procedural noise on black: dark pixels contribute nothing
   under screen, and the ~10% of lit pixels land as speckle. An earlier version
   of this file was solid black, negating white stucco and then boosting
   contrast crushed every value to zero, so it composited nothing at all. */
.tex.ground {
  z-index: 0;
  background-image: url(/assets/textures/ground-plaster.webp);
  background-size: cover;
  background-position: center;
  opacity: 0;
  mix-blend-mode: screen;
}
.tex.smudge {
  z-index: 998;
  background-image: url(/assets/textures/smudge.webp);
  background-size: cover;
  opacity: 0.5;
  mix-blend-mode: multiply;
  transform: scale(1);
  animation: none;
}
@keyframes drift {
  to {
    transform: scale(1.9) translate(-4%, 3%);
  }
}
/* THE overlay, sits above every asset and every glyph, including the text.
   Static: the jitter that used to shift this every 1.1s read as flicker rather
   than film, and repainting a full-screen blended layer on a timer was the
   single largest per-frame cost on the page. */
.tex.grain {
  z-index: 9990;
  background-image: url(/assets/textures/grain.webp);
  background-size: 256px 256px;
  background-repeat: repeat;
  opacity: 0.1;
  mix-blend-mode: screen;
}

.checker {
  background-image: conic-gradient(
    rgba(233, 230, 225, 0.055) 25%,
    transparent 0 50%,
    rgba(233, 230, 225, 0.055) 0 75%,
    transparent 0
  );
  background-size: 9px 9px;
}

/* datamosh tear */
.tex.mosh {
  z-index: 8;
  mix-blend-mode: screen;
  opacity: 0;
  animation: mosh 13s steps(1, end) infinite;
}
.tex.mosh i {
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--red);
  opacity: 0.5;
}
.tex.mosh i:nth-child(2) {
  height: 9px;
  background: repeating-linear-gradient(
    90deg,
    var(--red) 0 3px,
    transparent 3px 11px
  );
  opacity: 0.32;
  transform: translateX(9px);
}
.tex.mosh i:nth-child(3) {
  height: 1px;
  background: var(--cyan);
  transform: translateX(-16px);
  opacity: 0.45;
}
@keyframes mosh {
  0%,
  88% {
    opacity: 0;
  }
  88.5% {
    opacity: 1;
    transform: translateY(0);
  }
  90% {
    opacity: 1;
    transform: translateY(34vh);
  }
  90.6% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

/* ═══════════ CUSTOM CURSOR, full-bleed guides + trailing orb ═══════════ */
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
.gx-v,
.gx-h,
.orb {
  position: fixed;
  z-index: 9995;
  pointer-events: none;
  will-change: transform;
}
/* guide lines span the whole viewport, track the pointer 1:1 */
.gx-v {
  top: 0;
  left: 0;
  width: 1px;
  height: 100vh;
  background: linear-gradient(
    180deg,
    transparent,
    rgba(228, 6, 19, 0.16) 12%,
    var(--red) 50%,
    rgba(228, 6, 19, 0.16) 88%,
    transparent
  );
}
.gx-h {
  top: 0;
  left: 0;
  height: 1px;
  width: 100vw;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(228, 6, 19, 0.16) 12%,
    var(--red) 50%,
    rgba(228, 6, 19, 0.16) 88%,
    transparent
  );
}
/* the orb lags, then eases home to the intersection when the pointer rests */
.orb {
  top: 0;
  left: 0;
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  border: 1px solid var(--red);
  border-radius: 50%;
  box-shadow: 0 0 18px rgba(228, 6, 19, 0.34),
    inset 0 0 10px rgba(228, 6, 19, 0.16);
  transition: width 0.32s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.32s cubic-bezier(0.16, 1, 0.3, 1),
    margin 0.32s cubic-bezier(0.16, 1, 0.3, 1), background 0.32s, opacity 0.3s;
}
.orb::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 3px;
  height: 3px;
  margin: -1.5px 0 0 -1.5px;
  background: var(--red);
  border-radius: 50%;
  box-shadow: 0 0 8px var(--red);
  transition: opacity 0.3s;
}
.orb.rest {
  animation: orbpulse 2.6s ease-in-out infinite;
}
@keyframes orbpulse {
  50% {
    box-shadow: 0 0 30px rgba(228, 6, 19, 0.55),
      inset 0 0 14px rgba(228, 6, 19, 0.26);
  }
}
body.hot .orb {
  width: 62px;
  height: 62px;
  margin: -31px 0 0 -31px;
  background: rgba(228, 6, 19, 0.09);
}
body.hot .orb::after {
  opacity: 0;
}
.rdg {
  position: fixed;
  z-index: 9995;
  pointer-events: none;
  font-family: var(--mono);
  font-size: 0.53rem;
  letter-spacing: 0.16em;
  color: var(--red-hot);
  opacity: 0.8;
  white-space: nowrap;
  will-change: transform;
}
@media (pointer: coarse) {
  .gx-v,
  .gx-h,
  .orb,
  .rdg {
    display: none;
  }
}

/* ═══════════ FRAME ═══════════ */
.frame {
  position: fixed;
  inset: var(--gut);
  pointer-events: none;
  z-index: 5;
  border: 1px solid var(--hair-2);
}
.reg {
  position: absolute;
  width: 13px;
  height: 13px;
}
.reg::before,
.reg::after {
  content: "";
  position: absolute;
  background: var(--red);
}
.reg::before {
  width: 100%;
  height: 1px;
  top: 50%;
}
.reg::after {
  height: 100%;
  width: 1px;
  left: 50%;
}
.reg.tl {
  top: -7px;
  left: -7px;
}
.reg.tr {
  top: -7px;
  right: -7px;
}
.reg.bl {
  bottom: -7px;
  left: -7px;
}
.reg.br {
  bottom: -7px;
  right: -7px;
}
/* Edge telemetry sits in the gutter OUTSIDE the frame, clear of the content
   column. `writing-mode` rather than `rotate`: a rotated element still occupies
   its full horizontal text width for layout and turns about its own centre, so
   its final x lands at `left + textWidth / 2`, the longer the string, the
   further it drifts into the body. Vertical writing mode makes the box actually
   narrow, so `left` means what it says and the position no longer depends on
   how much text is in the label. */
.edge {
  position: fixed;
  top: 50%;
  z-index: 5;
  font-family: var(--mono);
  font-size: 0.57rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink-3);
  pointer-events: none;
  white-space: nowrap;
  writing-mode: vertical-rl;
  max-height: 78vh;
  overflow: hidden;
}
.edge.l {
  left: max(0.2rem, calc(var(--gut) / 2 - 0.45rem));
  transform: translateY(-50%) rotate(180deg);
}
.edge.r {
  right: max(0.2rem, calc(var(--gut) / 2 - 0.45rem));
  transform: translateY(-50%);
}

header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: calc(var(--gut) + 0.55rem) calc(var(--gut) + 1.3rem);
}
.brand {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-family: var(--mono);
  font-size: 0.63rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.brand .dia {
  width: 9px;
  height: 9px;
  background: var(--red);
  transform: rotate(45deg);
  box-shadow: 0 0 14px rgba(228, 6, 19, 0.75);
}
nav {
  display: flex;
  gap: 1.7rem;
  font-family: var(--mono);
  font-size: 0.63rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-2);
}
nav a {
  color: inherit;
  text-decoration: none;
  position: relative;
}
nav a::before {
  content: "";
  position: absolute;
  left: -0.62rem;
  top: 0.42em;
  width: 3px;
  height: 3px;
  background: var(--red);
  opacity: 0;
  transition: opacity 0.28s;
}
nav a:hover {
  color: var(--ink);
}
nav a:hover::before {
  opacity: 1;
}

.pad {
  position: relative;
  z-index: 3;
  padding: 0 calc(var(--gut) + 1.3rem);
}

/* ═══════════ HERO, asymmetric ═══════════ */
.hero {
  min-height: 100vh;
  position: relative;
  display: flex;
  align-items: flex-end;
  padding-bottom: clamp(5rem, 15vh, 10rem);
}
.construct {
  position: absolute;
  right: -16%;
  top: 46%;
  transform: translateY(-50%);
  width: min(112vh, 1080px);
  aspect-ratio: 1;
  pointer-events: none;
  z-index: 1;
  -webkit-mask-image: linear-gradient(
    102deg,
    transparent 4%,
    rgba(0, 0, 0, 0.18) 24%,
    #000 54%
  );
  mask-image: linear-gradient(
    102deg,
    transparent 4%,
    rgba(0, 0, 0, 0.18) 24%,
    #000 54%
  );
}
.construct svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}
.construct [stroke] {
  vector-effect: non-scaling-stroke;
}
.dr {
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
  animation: draw 2.4s cubic-bezier(0.3, 0.7, 0.2, 1) forwards;
}
@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}
@keyframes spinr {
  to {
    transform: rotate(-360deg);
  }
}
.rot {
  transform-origin: 400px 400px;
  animation: spin 150s linear infinite;
}
.rotr {
  transform-origin: 400px 400px;
  animation: spinr 96s linear infinite;
}
.core {
  transform-origin: 400px 400px;
  animation: corepulse 3.6s ease-in-out infinite;
}
@keyframes corepulse {
  50% {
    opacity: 0.45;
    transform: scale(0.94);
  }
}

/* legibility scrim under the title, left-anchored */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(
    96deg,
    var(--void) 6%,
    rgba(6, 6, 7, 0.86) 32%,
    rgba(6, 6, 7, 0.15) 56%,
    transparent 72%
  );
}

.title {
  position: relative;
  z-index: 4;
  max-width: min(62%, 780px);
}
.status {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 1.6rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  opacity: 0;
  animation: fade 0.9s ease 1.4s forwards;
}
.status s {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--red);
  text-decoration: none;
  box-shadow: 0 0 10px var(--red);
  animation: blip 2.2s ease-in-out infinite;
}
@keyframes blip {
  50% {
    opacity: 0.25;
  }
}
h1 {
  font-size: clamp(2.6rem, 8.6vw, 8rem);
  font-weight: 600;
  letter-spacing: -0.052em;
  line-height: 0.88;
  text-transform: uppercase;
  text-shadow: 0 2px 40px rgba(6, 6, 7, 0.95);
}
h1 b {
  display: block;
  overflow: hidden;
  font-weight: inherit;
}
h1 b i {
  display: block;
  font-style: normal;
  transform: translateY(104%);
  animation: up 1.1s cubic-bezier(0.16, 1, 0.3, 1) 0.8s forwards;
}
h1 b:nth-child(2) i {
  animation-delay: 0.92s;
  color: transparent;
  -webkit-text-stroke: 1.2px var(--ink);
}
@keyframes up {
  to {
    transform: none;
  }
}
@keyframes fade {
  to {
    opacity: 1;
  }
}
.tagline {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.13em;
  color: var(--ink-2);
  margin-top: 1.7rem;
  opacity: 0;
  animation: fade 0.9s ease 1.7s forwards;
}
.tagline u {
  text-decoration: none;
  color: var(--red-hot);
}

.hstrip {
  position: absolute;
  bottom: calc(var(--gut) + 1.6rem);
  left: calc(var(--gut) + 1.3rem);
  right: calc(var(--gut) + 1.3rem);
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  z-index: 4;
  font-family: var(--mono);
  font-size: 0.59rem;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--ink-3);
  opacity: 0;
  animation: fade 0.9s ease 2s forwards;
  gap: 1rem;
}
.hstrip .ck {
  width: 74px;
  height: 9px;
  flex: none;
}

/* ═══════════ GLITCH / TRON TEXT ═══════════ */
/* rgb split on hover */
.gx {
  position: relative;
  display: inline-block;
}
.gx:hover {
  animation: gjit 0.34s steps(2) 2;
}
@keyframes gjit {
  0% {
    transform: translate(0);
  }
  25% {
    transform: translate(-2px, 1px);
  }
  50% {
    transform: translate(2px, -1px);
  }
  75% {
    transform: translate(-1px, -1px);
  }
}
.gx:hover::before,
.gx:hover::after {
  content: attr(data-t);
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  pointer-events: none;
}
/* Decorative chromatic-aberration layer over readable --ink text; hover-only,
   carries no information of its own. Exempt from the --red-on-text rule so the
   red/cyan split stays intact. See spec §4. */
.gx:hover::before {
  color: var(--red);
  transform: translate(-2.5px, 0);
  opacity: 0.85;
  clip-path: polygon(0 0, 100% 0, 100% 46%, 0 46%);
}
.gx:hover::after {
  color: var(--cyan);
  transform: translate(2.5px, 0);
  opacity: 0.7;
  clip-path: polygon(0 52%, 100% 52%, 100% 100%, 0 100%);
}
/* scanline sweep across a claim as it enters */
/* The band is wider than the viewport and taller than the heading it belongs
   to, so the wipe reads as light crossing the whole section rather than a small
   highlight skimming one line. `overflow: visible` is what lets it bleed past
   the element; the band is pointer-transparent and clipped by nothing. */
.sweep {
  position: relative;
  overflow: visible;
}
.sweep::after {
  content: "";
  position: absolute;
  top: -1.2em;
  bottom: -1.2em;
  width: 120vw;
  pointer-events: none;
  z-index: -1;
  left: -140vw;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(228, 6, 19, 0.3) 34%,
    rgba(255, 46, 56, 0.42) 50%,
    rgba(59, 232, 224, 0.24) 64%,
    transparent 100%
  );
}
.sweep.in::after {
  animation: swp 1.9s cubic-bezier(0.33, 0, 0.2, 1) both;
}
@keyframes swp {
  to {
    left: 120vw;
  }
}
/* scramble target */
.scr {
  font-variant-ligatures: none;
}

/* ═══════════ SECTIONS ═══════════ */
.sec {
  padding: clamp(6rem, 15vw, 13rem) 0 0;
}
.seclabel {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding-bottom: 0.85rem;
  border-bottom: 1px solid var(--hair);
  margin-bottom: 3.4rem;
}
.seclabel u {
  text-decoration: none;
  color: var(--red-hot);
}
.readout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(1.6rem, 4vw, 4rem);
  align-items: center;
}
.rlede {
  font-size: clamp(1.25rem, 2.5vw, 1.9rem);
  font-weight: 500;
  letter-spacing: -0.025em;
  line-height: 1.28;
}
.rlede em {
  font-style: normal;
  color: var(--red-hot);
}
.rnote {
  font-family: var(--mono);
  font-size: 0.7rem;
  line-height: 1.95;
  color: var(--ink-2);
  margin-top: 1.5rem;
}
.panel {
  position: relative;
  border: 1px solid var(--hair);
  background: linear-gradient(
    180deg,
    rgba(12, 12, 15, 0.92),
    rgba(6, 6, 7, 0.4)
  );
  padding: 1.7rem 1.8rem 1.4rem;
}
.ptitle {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-3);
  display: flex;
  justify-content: space-between;
  margin-bottom: 1.5rem;
}
.bar {
  display: grid;
  grid-template-columns: 2.9rem 1fr 3rem;
  align-items: center;
  gap: 0.85rem;
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--ink-3);
  margin-bottom: 0.6rem;
}
.track {
  height: 8px;
  position: relative;
  overflow: hidden;
  background-size: 9px 9px;
  background-image: conic-gradient(
    rgba(233, 230, 225, 0.06) 25%,
    transparent 0 50%,
    rgba(233, 230, 225, 0.06) 0 75%,
    transparent 0
  );
}
.fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: var(--red);
  transition: width 1.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.bar.on .fill {
  width: var(--w);
}
.bar:last-of-type .fill {
  box-shadow: 0 0 18px rgba(228, 6, 19, 0.85);
}
.bar b {
  color: var(--ink-2);
  font-weight: 400;
  text-align: right;
}
.bar:last-of-type b {
  color: var(--red-hot);
}
.pfoot {
  font-family: var(--mono);
  font-size: 0.59rem;
  color: var(--ink-3);
  line-height: 1.7;
  margin-top: 1.1rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--hair-2);
}

.moves {
  padding: clamp(6rem, 14vw, 12rem) 0 9rem;
}
.move {
  position: relative;
  display: grid;
  grid-template-columns: 3.4rem minmax(0, 1fr) minmax(0, 40ch);
  gap: clamp(1.2rem, 3vw, 3rem);
  padding: 3.1rem 0;
  border-top: 1px solid var(--hair-2);
  align-items: start;
}
.move:last-child {
  border-bottom: 1px solid var(--hair-2);
}
.move::after {
  content: "";
  position: absolute;
  left: 0;
  top: -1px;
  width: 0;
  height: 1px;
  background: var(--red);
  transition: width 1.1s cubic-bezier(0.16, 1, 0.3, 1);
}
.move.in::after {
  width: 100%;
}
.idx {
  font-family: var(--mono);
  font-size: 0.62rem;
  color: var(--red-hot);
  letter-spacing: 0.13em;
  padding-top: 0.55rem;
}
.claim {
  font-size: clamp(1.45rem, 3vw, 2.45rem);
  font-weight: 500;
  letter-spacing: -0.032em;
  line-height: 1.1;
}
.claim em {
  font-style: normal;
  color: var(--red-hot);
}
.detail {
  font-family: var(--mono);
  font-size: 0.74rem;
  line-height: 2;
  color: var(--ink-2);
  padding-top: 0.5rem;
}
.detail b {
  color: var(--ink);
  font-weight: 400;
}
.detail code {
  color: var(--red-hot);
  font-family: inherit;
}
.src {
  margin-top: 1.3rem;
  font-size: 0.6rem;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--ink-3);
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.src s {
  width: 14px;
  height: 1px;
  background: var(--ink-3);
  text-decoration: none;
  flex: none;
}

.rv {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 1s cubic-bezier(0.16, 1, 0.3, 1),
    transform 1s cubic-bezier(0.16, 1, 0.3, 1);
}
.rv.in {
  opacity: 1;
  transform: none;
}
@media (max-width: 900px) {
  .readout,
  .move {
    grid-template-columns: 1fr;
  }
  .edge {
    display: none;
  }
  .construct {
    right: -40%;
    opacity: 0.5;
  }
  .title {
    max-width: 100%;
  }
}
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: -1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* Constraint: reduced motion must restore a usable pointer, not just freeze it. */
  body,
  a {
    cursor: auto !important;
  }
  .gx-v,
  .gx-h,
  .orb,
  .rdg {
    display: none !important;
  }
  .tex.mosh,
  .tex.grain {
    animation: none !important;
  }
}

/* ── prose: post and work-entry bodies ── */
.prose {
  max-width: 68ch;
  font-size: 0.95rem;
  line-height: 1.85;
  color: var(--ink-2);
}
.prose h2 {
  font-family: var(--sans);
  font-size: clamp(1.2rem, 2.2vw, 1.7rem);
  font-weight: 500;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin: 2.8rem 0 0.9rem;
}
.prose h3 {
  font-family: var(--sans);
  font-size: 1.05rem;
  font-weight: 500;
  color: var(--ink);
  margin: 2rem 0 0.6rem;
}
.prose p {
  margin-bottom: 1.25rem;
}
.prose a {
  color: var(--red-hot);
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 46, 56, 0.35);
}
.prose a:hover {
  border-bottom-color: var(--red-hot);
}
.prose code {
  font-family: var(--mono);
  font-size: 0.86em;
  color: var(--red-hot);
}
.prose pre {
  background: var(--panel);
  border: 1px solid var(--hair-2);
  padding: 1.1rem 1.2rem;
  overflow-x: auto;
  margin-bottom: 1.4rem;
}
.prose pre code {
  color: var(--ink-2);
}
.prose ul,
.prose ol {
  margin: 0 0 1.25rem 1.2rem;
}
.prose li {
  margin-bottom: 0.4rem;
}
.prose blockquote {
  border-left: 2px solid var(--red);
  padding-left: 1.1rem;
  margin: 0 0 1.4rem;
  color: var(--ink-2);
  font-style: normal;
}

/* ── index rows: shared by /work and /writing ── */
.rows {
  border-top: 1px solid var(--hair-2);
}
.row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 42ch);
  gap: clamp(1.2rem, 3vw, 3rem);
  padding: 2.6rem 0;
  border-bottom: 1px solid var(--hair-2);
  text-decoration: none;
  color: inherit;
  position: relative;
  align-items: start;
}
.row::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 0;
  height: 1px;
  background: var(--red);
  transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.row:hover::after {
  width: 100%;
}
.row h2 {
  font-size: clamp(1.3rem, 2.6vw, 2rem);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1.12;
}
.row p {
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.95;
  color: var(--ink-2);
}
.rowmeta {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-top: 0.9rem;
  display: flex;
  gap: 0.8rem;
  flex-wrap: wrap;
  align-items: center;
}
.rowmeta .st {
  color: var(--red-hot);
}
.stats {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.13em;
  color: var(--ink-3);
  margin-top: 1.1rem;
}

/* ── footer ── */
footer {
  position: relative;
  z-index: 3;
  margin-top: clamp(5rem, 12vw, 9rem);
  padding: 2.4rem calc(var(--gut) + 1.3rem) calc(var(--gut) + 2.4rem);
  border-top: 1px solid var(--hair-2);
  display: flex;
  justify-content: space-between;
  gap: 1.4rem;
  flex-wrap: wrap;
  font-family: var(--mono);
  font-size: 0.63rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
}
footer a {
  color: var(--ink-2);
  text-decoration: none;
}
footer a:hover {
  color: var(--red-hot);
}
footer nav {
  display: flex;
  gap: 1.5rem;
}
@media (max-width: 900px) {
  .row {
    grid-template-columns: 1fr;
  }
}

.movelink {
  display: inline-block;
  margin-top: 1.3rem;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--red-hot);
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 46, 56, 0.3);
  padding-bottom: 2px;
}
.movelink:hover {
  border-bottom-color: var(--red-hot);
}

/* ── live links on a work entry ── */
.livelinks {
  display: flex;
  gap: 0.9rem;
  flex-wrap: wrap;
  margin-bottom: 2.6rem;
}
.livelinks a {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--red-hot);
  text-decoration: none;
  border: 1px solid rgba(255, 46, 56, 0.3);
  padding: 0.5rem 0.8rem;
  transition: border-color 0.3s, background 0.3s;
}
.livelinks a::before {
  content: "↗";
  margin-right: 0.5rem;
  opacity: 0.7;
}
.livelinks a:hover {
  border-color: var(--red-hot);
  background: rgba(228, 6, 19, 0.07);
}

/* ═══════════ HERO COUNTERS ═══════════ */
.counters {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1.6rem, 4.4vw, 3.8rem);
  margin-top: 2.4rem;
  opacity: 0;
  animation: fade 0.9s ease 1.9s forwards;
}
.ct {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.ct b {
  font-family: var(--sans);
  font-size: clamp(2.1rem, 5.2vw, 4rem);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.ct b.settled {
  color: var(--red-hot);
  transition: color 0.5s ease;
}
.ct span {
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.ct i {
  display: block;
  height: 1px;
  width: 0;
  background: var(--red);
  margin-top: 0.15rem;
  transition: width 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.ct b.settled + span + i,
.ct.done i {
  width: 100%;
}

/* ═══════════ TYPEWRITER ═══════════ */
/* Characters are wrapped in spans and revealed by opacity, so the text occupies
   its final space from the start, no reflow, and the full string stays in the
   DOM for screen readers and for select-and-copy. */
/* `.armed` is added by JS immediately before splitting. Without JS the rule
   never applies and the text renders normally, hiding it in plain CSS would
   make the page blank for anyone whose scripts fail to load. */
/* `.armed` is added by JS to every typewriter target, the .tw blurbs and every
   .prose p, so these are scoped to `.armed` alone rather than to `.tw`. */
.armed span {
  opacity: 0;
}
.armed span.on {
  opacity: 1;
  transition: opacity 90ms linear;
}
/* Exactly one caret per page. JS moves the `.at` class to the single character
   at the frontier of whichever paragraph is currently mid-reveal. */
span.at {
  position: relative;
}
span.at::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 0.08em;
  width: 0.46em;
  height: 1.02em;
  background: var(--red);
  animation: caret 0.9s steps(1) infinite;
  pointer-events: none;
}
.pfoot b {
  color: var(--red-hot);
  font-weight: 400;
}
@keyframes caret {
  50% {
    opacity: 0;
  }
}

/* ═══════════ ANAGLYPH, red/cyan 3D ═══════════ */
/* The alternative to .gx's clipped RGB split: a full-glyph offset in red and
   cyan, like a frame shot for 3D glasses. */
.ana {
  position: relative;
  text-shadow: -2px 0 rgba(228, 6, 19, 0.75), 2px 0 rgba(59, 232, 224, 0.6);
}
.ana:hover {
  animation: anadrift 1.4s ease-in-out infinite;
}
@keyframes anadrift {
  0%,
  100% {
    text-shadow: -2px 0 rgba(228, 6, 19, 0.75), 2px 0 rgba(59, 232, 224, 0.6);
  }
  50% {
    text-shadow: -5px 0 rgba(228, 6, 19, 0.85), 5px 0 rgba(59, 232, 224, 0.7);
  }
}

/* ═══════════ H1 GLITCH ═══════════ */
/* Fires periodically from JS rather than on hover, the hero name is not
   something a visitor thinks to hover. */
h1 b i.glitching {
  animation: hglitch 0.32s steps(2) 2;
}
@keyframes hglitch {
  0% {
    transform: translate(0);
    clip-path: none;
  }
  25% {
    transform: translate(-3px, 1px);
    clip-path: polygon(0 0, 100% 0, 100% 44%, 0 44%);
  }
  50% {
    transform: translate(3px, -2px);
    clip-path: polygon(0 38%, 100% 38%, 100% 100%, 0 100%);
  }
  75% {
    transform: translate(-1px, -1px);
    clip-path: none;
  }
}
h1 b i.glitching::before {
  content: attr(data-final);
  position: absolute;
  left: -3px;
  top: 0;
  color: var(--red);
  opacity: 0.8;
  pointer-events: none;
}
h1 b i.glitching::after {
  content: attr(data-final);
  position: absolute;
  left: 3px;
  top: 0;
  color: var(--cyan);
  opacity: 0.65;
  pointer-events: none;
}
h1 b {
  position: relative;
}

/* build identity in the footer */
.buildno {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  color: var(--ink-3);
}
.buildno code {
  font-family: inherit;
  color: var(--red-hot);
}

/* ═══════════ SPLASH ═══════════
   display:none by default. The inline head script adds .splashing to <html>
   only when this is a first visit in the session and motion is not reduced,
   so the overlay is present from the very first painted frame or not at all. */
.splash {
  display: none;
}
.splashing .splash {
  display: grid;
  place-items: center;
  position: fixed;
  inset: 0;
  z-index: 10000;
  background: #060607;
  /* One declaration for both properties. Two separate `transition` rules do not
     merge, the later one replaces the former, which is how the opacity fade got
     silently dropped and the overlay started vanishing instantly. */
  /* The single source of truth for the fade. JS reads this property rather
     than carrying its own copy, so the removal timer can never disagree with
     the transition and cut it short. */
  --splash-fade: 2s;
  /* ease-in: holds near full opacity, then accelerates away. The true inverse
     of `ease` was cubic-bezier(0.75, 0, 0.75, 0.9), which ended too abruptly -
     its final control point pulls hard toward the end of the curve. ease-in is
     the same intent with a gentler landing. */
  --splash-ease: ease-in;
  transition: opacity var(--splash-fade) var(--splash-ease),
              background var(--splash-fade) var(--splash-ease);
}
.splashing body {
  overflow: hidden;
}
.splash.out {
  opacity: 0;
  background: transparent;
  pointer-events: none;
}
.splash-art {
  width: min(46vmin, 340px);
  aspect-ratio: 1;
}

/* ═══════════ MOBILE NAV ═══════════ */
.navtoggle {
  display: none;
  position: relative;
  width: 46px;
  height: 46px;
  padding: 0;
  border: 1px solid var(--hair-2);
  background: transparent;
  cursor: pointer;
}
.navtoggle:hover {
  border-color: var(--red);
}
.navtoggle .burger-art {
  position: absolute;
  inset: -6px;
  display: block;
}
/* Plain bars, so the button is a working control before the Lottie arrives and
   if it never does. .has-art is added only once the animation is mounted. */
.burger-bars {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 5px;
}
.burger-bars i {
  display: block;
  width: 18px;
  height: 1px;
  background: var(--ink-2);
}
.navtoggle.has-art .burger-bars {
  display: none;
}

.mobilenav {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 2.4rem;
  padding: calc(var(--gut) + 4rem) calc(var(--gut) + 1.3rem);
  background: rgba(6, 6, 7, 0.97);
  opacity: 0;
  transition: opacity 0.32s ease;
}
.mobilenav.open {
  opacity: 1;
}
.mobilenav nav {
  display: flex;
  flex-direction: column;
  gap: 1.6rem;
}
.mobilenav nav a {
  font-family: var(--sans);
  font-size: clamp(2rem, 11vw, 3.4rem);
  font-weight: 500;
  letter-spacing: -0.04em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
}
.mobilenav nav a[aria-current="page"] {
  color: var(--red-hot);
}
.mobilenav-foot {
  display: flex;
  gap: 1.4rem;
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.mobilenav-foot a {
  color: var(--ink-3);
  text-decoration: none;
}
body.navlocked {
  overflow: hidden;
}

@media (max-width: 900px) {
  .desknav {
    display: none;
  }
  .navtoggle {
    display: block;
  }
  .mobilenav {
    display: flex;
  }
  .mobilenav:not(.open) {
    pointer-events: none;
  }
}

/* ── the mark the splash settles into ──
   Same size and centre as .splash-art. It is mounted while the splash is still
   covering the screen and is NOT animated in: when the overlay fades, the logo
   is simply already there, which is the whole point. Fading it in produced a
   logo that vanished and then returned. */
.heromark {
  /* The hero starts at the top of the document, so 50dvh down from its top is
     the centre of the viewport, which is exactly where the fixed splash art
     sits. Centring on the hero's own height only matched while the hero was
     one viewport tall; on mobile it grows past that and the logo jumped when
     the overlay faded.

     dvh rather than vh because the splash is a fixed element measured against
     the *visual* viewport, and on mobile vh ignores the browser chrome while
     dvh does not. Absolute rather than fixed so it scrolls away with the hero
     instead of following the reader down the page. */
  position: absolute;
  top: 50dvh;
  left: 50%;
  width: min(46vmin, 340px);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  z-index: 0;
  opacity: 0.15;
  pointer-events: none;
  /* The splash animation's final frame, rendered once at authoring time by
     tools/extract-logo-svg.mjs and saved as a static SVG.

     It carries the same viewBox (0 0 1000 1000) as the running animation, so in
     an identically sized box it draws identical geometry. No scale factor, no
     measured artwork bounds: earlier attempts matched a separate PNG export by
     estimating the Lottie's artwork extent, and every such estimate is wrong by
     a little because shape vertices ignore layer transforms and bezier control
     points bulge past their vertices.

     514KB raw, 12KB brotli - it is eleven stacked copies of the same paths. */
  background: url(/assets/brand/logo-mark.svg) center / contain no-repeat;
}

/* Scope tag under each counter's rule. Two different corpora sit in this row,
   personal and employer, and without this the reader is invited to compare
   numbers that measure unrelated things. */
.ct .scope {
  font-family: var(--mono);
  font-size: clamp(0.85rem, 1.7vw, 1.15rem);
  letter-spacing: 0.16em;
  color: var(--red-hot);
  text-decoration: none;
  margin-top: 0.4rem;
}
