/* GLITCH — ring layout matched to reference: nav + circular portrait ring + inner type */

/* Register the manifesto-chip magnet variables as <length>-typed custom
   properties so transitions on them interpolate smoothly (without
   @property they'd be treated as raw strings and would jump-snap). */
@property --push-x {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --push-y {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

:root {
  --canvas: #fafaf9;
  --ink: #141413;
  --ink-soft: rgba(20, 20, 19, 0.42);
  --sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --display: "Instrument Serif", "Times New Roman", Georgia, serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

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

html {
  height: 100%;
}

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 1rem;
  padding: 0.45rem 0.85rem;
  background: var(--ink);
  color: var(--canvas);
  z-index: 100;
  font-size: 0.65rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
}

.skip:focus {
  left: 1rem;
}

/* ——— Top nav ——— */
.ref-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  padding: 1.5rem clamp(1rem, 4vw, 2rem) 0.65rem;
  background: linear-gradient(180deg, var(--canvas) 78%, transparent 100%);
  pointer-events: none;
}

.ref-nav__inner {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: clamp(2.25rem, 7vw, 4.25rem);
  pointer-events: auto;
}

.ref-nav__link {
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-soft);
  padding: 0.2rem 0;
  border-bottom: 1px solid transparent;
  transition:
    color 0.2s ease,
    border-color 0.2s ease;
}

.ref-nav__link:hover,
.ref-nav__link:focus-visible {
  color: var(--ink);
  outline: none;
}

.ref-nav__link.is-active {
  color: var(--ink);
  font-weight: 600;
  border-bottom-color: var(--ink);
}

/* ——— Stage ——— */
.ref-main {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(4.75rem, 11vh, 6rem) clamp(0.75rem, 3vw, 1.5rem) clamp(1.5rem, 5vh, 2.5rem);
}

/*
  Ring math (matches “portal” reference):
  - --ring = square diameter (px/vmin)
  - thumb width = fraction of ring (narrow portrait tiles)
  - --orbit = distance from center to each thumb’s transform origin (tuned so tiles sit on a circle with even gaps)
*/
.ref-ring {
  --count: 19;
  --ring: min(94vmin, 800px);
  position: relative;
  width: var(--ring);
  max-width: 100%;
  aspect-ratio: 1;
  margin-inline: auto;
  isolation: isolate;
  /* Tighter orbit + slightly wider tiles = continuous ring (no wedge “gaps”) */
  --orbit: calc(var(--ring) * 0.382);
}

.ref-ring__orbit {
  position: absolute;
  inset: 0;
  z-index: 1;
  margin: 0;
  padding: 0;
  list-style: none;
  transform-origin: 50% 50%;
  will-change: transform;
}

@media (prefers-reduced-motion: no-preference) {
  .ref-ring__orbit {
    animation: ring-spin 90s linear infinite;
  }
}

@keyframes ring-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.ref-ring__cell {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--ring) * 0.108);
  aspect-ratio: 4 / 3;
  z-index: 2;
  transform-origin: center center;
  --angle: calc(var(--i) * (360deg / var(--count)) - 90deg);
  --offset: 0deg;
  transform: translate(-50%, -50%)
    translate(
      calc(cos(var(--angle)) * var(--orbit)),
      calc(sin(var(--angle)) * var(--orbit))
    )
    rotate(calc(var(--angle) + 90deg + var(--offset)));
}

.ref-thumb {
  margin: 0;
  width: 100%;
  height: 100%;
  padding: 3px;
  background: #fff;
  border: 1px solid rgba(20, 20, 19, 0.06);
  border-radius: 6px;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.95) inset,
    0 3px 10px rgba(20, 20, 19, 0.06);
  overflow: hidden;
}

.ref-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  border-radius: 4px;
}

/* Inner copy: width capped to the visual “hole” inside the ring */
.ref-ring__hub {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 20;
  width: min(calc(var(--ring) * 0.44), 20rem);
  max-width: calc(100% - 2rem);
  text-align: center;
  pointer-events: none;
}

.ref-ring__headline {
  margin: 0 0 1.25rem;
  font-family: var(--display);
  font-style: italic;
  font-size: clamp(1.5rem, calc(var(--ring) * 0.052), 2.25rem);
  font-weight: 400;
  line-height: 1.18;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-wrap: balance;
}

.ref-ring__cta {
  margin: 0;
  font-size: 0.5625rem;
  font-weight: 500;
  letter-spacing: 0.48em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

@media (prefers-reduced-motion: no-preference) {
  .ref-ring__cta {
    animation: hub-fade 4.8s ease-in-out infinite;
  }
}

@keyframes hub-fade {
  0%,
  100% {
    opacity: 0.52;
  }
  50% {
    opacity: 1;
  }
}

/* ——— Bottom figure overlay (robot.html) ——— */
/*
  The figure is fixed flush to the bottom of the viewport at full width.
  Stacking on .has-figure pages:
    orbit / cells (inside .ref-ring's isolated context) → behind figure
    .figure-overlay  z-index 10 → above the ring
    .ref-ring__hub   z-index 30 → above the figure (centered text stays readable)
  Page canvas is forced to pure white so the image's white background
  blends seamlessly with no visible seam.
*/

/* Force a pure-white canvas on the figure page so the image background
   matches the page exactly. Override --canvas so all cascading uses
   (body bg, nav gradient, .skip color, etc.) update together.
   Also: position: relative on body makes it the containing block for
   .figure-overlay's absolute positioning, and min-height > 100vh
   creates scrollable space so the figure's lower portion sits below
   the initial viewport — scrolling reveals the rest of her. */
.has-figure {
  --canvas: #ffffff;
  background: var(--canvas);
  position: relative;
}

/* Portrait robot movie locked to the bottom-center of the viewport. The
   editorial copy scrolls behind it, leaving a center gap for the figure. */
/* Robot fills the hero — anchored to the top of the body and absolutely
   positioned so it scrolls AWAY with the page rather than staying pinned. */
.has-figure .robot-movie {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  width: auto;
  height: 100vh;
  max-width: 100%;
  object-fit: contain;
  object-position: bottom center;
  z-index: 15;
  pointer-events: none;
  background: transparent;
}

@media (max-aspect-ratio: 1/1) {
  .has-figure .robot-movie {
    width: 100%;
    height: auto;
    max-height: 100vh;
  }
}

/* Pixel-art logo pinned to the top center of the figure page. Keeps the
   image crisp at any size by disabling smoothing on the upscaled bitmap. */
.has-figure .glitch-logo {
  position: fixed;
  top: clamp(1rem, 2.5vh, 2rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 40;
  display: block;
  line-height: 0;
}

.has-figure .glitch-logo img {
  display: block;
  height: clamp(48px, 10vh, 110px);
  width: auto;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  background: transparent;
}

/* Top-right "Archive" button — links to the legacy ring layout. */
.has-figure .archive-link {
  position: fixed;
  top: clamp(1.25rem, 3vh, 2.25rem);
  right: clamp(1.25rem, 3vw, 2.25rem);
  z-index: 40;
  display: inline-flex;
  align-items: center;
  font-family: var(--mono);
  font-size: clamp(0.7rem, 1.1vw, 0.85rem);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  padding: 0.45rem 0.95rem;
  border: 1px solid var(--ink);
  background: transparent;
  transition:
    background 0.18s ease,
    color 0.18s ease;
}

.has-figure .archive-link:hover,
.has-figure .archive-link:focus-visible {
  background: var(--ink);
  color: var(--canvas);
  outline: none;
}

.figure-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 75vh;
  z-index: 10;
  pointer-events: none;
  display: flex;
  justify-content: center;
  align-items: flex-end;
}

.figure-overlay img {
  display: block;
  width: auto;
  height: 100%;
  max-width: 100%;
  object-fit: contain;
  object-position: bottom center;
}

/* On narrow/portrait screens, the natural image is taller-than-wide
   when scaled to fit width; switch to width-bound scaling so the
   figure shrinks vertically while staying as large as possible. */
@media (max-aspect-ratio: 1/1) {
  .figure-overlay img {
    width: 100%;
    height: auto;
    max-height: 75vh;
  }
}

/* Title is fixed to the viewport, hovering just above the robot's head.
   `top: auto` clears the base .ref-ring__hub rule's top: 50%. */
/* Title sits within the hero (top of the page) and scrolls away with the
   page like the robot. Anchored from the top so it stays in a consistent
   spot just above the robot's head regardless of viewport height. */
.has-figure .ref-ring__hub {
  position: absolute;
  left: 50%;
  top: clamp(20vh, 24vh, 28vh);
  bottom: auto;
  transform: translateX(-50%);
  z-index: 30;
  width: min(94vw, 60rem);
  text-align: center;
}

.has-figure .ref-ring__headline {
  font-family: var(--display);
  font-style: normal;
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  font-size: clamp(0.95rem, 1.7vw, 1.3rem);
  line-height: 1.25;
}

/* ---------- Editorial: dense two-column block flanking the robot --------- */

/* The page is a scrollable wall of magazine-style copy. CSS columns split it
   into two columns with a wide column-gap; the fixed robot video sits in
   that gap so the text reads as flowing on the left and right of the figure.
   Top padding leaves room for the logo, archive button, and headline. */
/* Editorial container: plain block flow holding two children — a 2-column
   split that flanks the robot, then a full-width single-column section
   that runs underneath it. */
.has-figure .editorial {
  display: block;
  min-height: auto;
  /* Top padding sits the editorial just barely above the fold (~2vh of
     content peeks at the bottom of the initial viewport). */
  padding: clamp(44rem, 98vh, 60rem) clamp(1.25rem, 3vw, 3rem)
    clamp(3rem, 8vh, 6rem);
  margin: 0;

  font-family: var(--display);
  font-size: clamp(1.1rem, 1.55vw, 1.5rem);
  line-height: 1.25;
  letter-spacing: -0.005em;
  color: var(--ink);
}

/* The two-column split that flanks the robot. The center grid track is
   reserved space for the absolutely-positioned video — no copy crosses
   into it. */
.has-figure .editorial__split {
  display: grid;
  grid-template-columns: 1fr clamp(34vw, 40vw, 46vw) 1fr;
  column-gap: clamp(1.25rem, 2.5vw, 2.5rem);
  align-items: start;
}

/* Full-width single-column section that runs UNDERNEATH the robot. */
.has-figure .editorial__full {
  margin: clamp(2rem, 5vh, 4rem) auto 0;
  max-width: min(72ch, 92vw);
  text-align: justify;
  hyphens: auto;
}

.has-figure .editorial__full p {
  margin: 0 0 0.7rem;
}

.has-figure .editorial__col {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.has-figure .editorial__col--left {
  grid-column: 1;
}

.has-figure .editorial__col--right {
  grid-column: 3;
}

.has-figure .editorial p {
  margin: 0;
  text-align: justify;
  hyphens: auto;
  orphans: 2;
  widows: 2;
}

/* The questions feel like a poetic provocation at the top of the column —
   italic for voice, but matched to the body copy in size. */
.has-figure .editorial__question {
  font-style: italic;
  text-align: left;
  hyphens: manual;
}

/* The lede statement is the manifesto line — uppercase so it anchors the
   reader after the questions, but matched to the body copy in size. */
.has-figure .editorial__lede {
  font-style: normal;
  text-align: left;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  margin-top: 0.6em;
  hyphens: manual;
}

/* Embedded photos float within their column so the body text wraps tightly
   around them — magazine style. The two columns mirror each other: the
   left column leads with a left-floating photo, the right column leads
   with a right-floating photo. Each column then alternates from there. */
.has-figure .editorial__photo {
  width: clamp(55%, 60%, 70%);
  shape-outside: margin-box;
}

.has-figure .editorial__col--left .editorial__photo {
  float: left;
  margin: 0.35em 0.9em 0.55em 0;
}

.has-figure .editorial__col--left .editorial__photo:nth-of-type(even) {
  float: right;
  margin: 0.35em 0 0.55em 0.9em;
}

.has-figure .editorial__col--right .editorial__photo {
  float: right;
  margin: 0.35em 0 0.55em 0.9em;
}

.has-figure .editorial__col--right .editorial__photo:nth-of-type(even) {
  float: left;
  margin: 0.35em 0.9em 0.55em 0;
}

.has-figure .editorial__photo img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
}

/* Flex columns can't host floats, so let the columns lay out as plain
   block flow which is what enables float + text-wrap to work. */
.has-figure .editorial__col {
  display: block;
}

.has-figure .editorial__col > p {
  margin: 0 0 0.55rem;
}

/* On narrower screens collapse the split to a single column and let the
   robot sit above all the copy. */
@media (max-width: 960px) {
  .has-figure .editorial {
    padding-top: clamp(82vh, 88vh, 92vh);
    padding-left: clamp(1.25rem, 5vw, 2rem);
    padding-right: clamp(1.25rem, 5vw, 2rem);
    font-size: clamp(1rem, 2.6vw, 1.2rem);
    line-height: 1.35;
  }

  .has-figure .editorial__split {
    grid-template-columns: 1fr;
    column-gap: 0;
  }

  .has-figure .editorial__col--left,
  .has-figure .editorial__col--right {
    grid-column: 1;
  }
}

/* With only 4 cells (one per approved image), the default cell width
   (~10.8% of ring) reads too sparse — bump cells up so they fill the
   orbit visually like prominent thumbnails at each quadrant. */
.has-figure .ref-ring__cell {
  width: calc(var(--ring) * 0.22);
}

@media (max-width: 540px) {
  .has-figure .ref-ring__cell {
    width: calc(var(--ring) * 0.24);
  }
}

/* ——— Static cluster behind the figure ———
   Stop the orbit rotation and position the four cells as a horizontal
   group right at the figure's head/shoulder level. The cells already
   live in .ref-ring's isolated stacking context which sits below the
   figure-overlay (z-index 10), so they naturally tuck behind her.
*/
/*
  Parallax: as the user scrolls down, the orbit container (containing
  all the arc cells) translates upward in addition to natural scroll —
  so the carousel "flies away" upward faster than the robot, which
  scrolls 1:1 with the page. The arc animation on each cell continues
  unchanged inside this translating wrapper (transforms compose).

  Uses scroll-driven CSS animations: Chrome 115+, Edge 115+, Firefox
  132+, Safari 26+. On older browsers the rule degrades to no parallax
  (cells scroll 1:1, no breakage).
*/
.has-figure .ref-ring__orbit {
  animation: orbit-parallax linear both;
  animation-timeline: scroll(root block);
}

@keyframes orbit-parallax {
  to { transform: translateY(-22vh); }
}

/* Replace the polar (orbit) positioning with a simple horizontal
   cluster. Each cell gets its own --tx / --ty / --tilt set per
   nth-child below for a slightly fanned, organic placement. */
.has-figure .ref-ring__cell {
  --tx: 0px;
  --ty: 0px;
  --tilt: 0deg;
  transform: translate(-50%, -50%)
    translate(var(--tx), var(--ty))
    rotate(var(--tilt));
  width: calc(var(--ring) * 0.2);
}

/*
  Continuous horseshoe arc — cells sweep along a wide upside-down U
  passing over the figure's head. Each cell starts off-screen at one
  end of the arc, fades in, traverses over the head, fades out at the
  other end, and loops back. Four cells are staggered with negative
  animation-delays so they're at different points on the arc at any
  given moment.

      ─────── ∩ ───────
     ╱                   ╲
    fade-in            fade-out
    (left edge)       (right edge)
*/
.has-figure .ref-ring__cell {
  animation: cell-arc 18s linear infinite;
}

/* 4 cells spaced evenly around the 18s cycle = 4.5s offset each, so at
   any moment one cell is fading-in / one is fading-out / two are
   mid-arc. */
.has-figure .ref-ring__cell:nth-child(1) { animation-delay:    0s;   }  /*   0% */
.has-figure .ref-ring__cell:nth-child(2) { animation-delay:   -4.5s; }  /*  25% */
.has-figure .ref-ring__cell:nth-child(3) { animation-delay:   -9s;   }  /*  50% */
.has-figure .ref-ring__cell:nth-child(4) { animation-delay:  -13.5s; }  /*  75% */

/* Static fallback positions per cell — only visible when the animation
   is paused (prefers-reduced-motion). Spread as a shallow horseshoe
   so the page still reads correctly without motion. */
.has-figure .ref-ring__cell:nth-child(1) { --tx: -22vmin; --ty:  4vh; --tilt: -7deg; }
.has-figure .ref-ring__cell:nth-child(2) { --tx:  -8vmin; --ty: -3vh; --tilt: -2deg; }
.has-figure .ref-ring__cell:nth-child(3) { --tx:   8vmin; --ty: -3vh; --tilt:  2deg; }
.has-figure .ref-ring__cell:nth-child(4) { --tx:  22vmin; --ty:  4vh; --tilt:  7deg; }

@keyframes cell-arc {
  /* Path: x(t) = -20vw·cos(π·t),  y(t) = 0vh − 8vh·sin(π·t)
     Compact horseshoe constrained to 40vw total width (±20vw). The
     arc is lifted so the bases sit just above her head (y=0, body
     y≈50vh) and the peak (y=-8vh) floats well above her forehead.
     Cells fade in 0–8% and out 92–100%. */
  0%   { transform: translate(-50%,-50%) translate(-20vw,    0vh)   rotate(-5deg);    opacity: 0; }
  8%   {                                                                              opacity: 1; }
  10%  { transform: translate(-50%,-50%) translate(-19vw,   -2.5vh) rotate(-4deg);   }
  20%  { transform: translate(-50%,-50%) translate(-16.2vw, -4.7vh) rotate(-3deg);   }
  30%  { transform: translate(-50%,-50%) translate(-11.8vw, -6.5vh) rotate(-2deg);   }
  40%  { transform: translate(-50%,-50%) translate( -6.2vw, -7.6vh) rotate(-1deg);   }
  50%  { transform: translate(-50%,-50%) translate(   0vw,  -8vh)   rotate(0deg);    }
  60%  { transform: translate(-50%,-50%) translate(  6.2vw, -7.6vh) rotate(1deg);    }
  70%  { transform: translate(-50%,-50%) translate( 11.8vw, -6.5vh) rotate(2deg);    }
  80%  { transform: translate(-50%,-50%) translate( 16.2vw, -4.7vh) rotate(3deg);    }
  90%  { transform: translate(-50%,-50%) translate(  19vw,  -2.5vh) rotate(4deg);    }
  92%  {                                                                              opacity: 1; }
  100% { transform: translate(-50%,-50%) translate(  20vw,   0vh)   rotate(5deg);     opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .has-figure .ref-ring__cell,
  .has-figure .ref-ring__orbit {
    animation: none;
  }
}

@media (max-width: 540px) {
  .has-figure .ref-ring__cell {
    width: calc(var(--ring) * 0.24);
  }
}

@media (max-width: 540px) {
  .ref-ring {
    --ring: min(92vmin, calc(100vw - 1.25rem));
    --orbit: calc(var(--ring) * 0.376);
  }

  .ref-ring__cell {
    width: calc(var(--ring) * 0.118);
  }

  .ref-ring__hub {
    width: min(calc(var(--ring) * 0.48), 19rem);
  }

  .ref-nav__inner {
    gap: clamp(1.5rem, 5vw, 2.5rem);
  }

  .ref-nav__link {
    letter-spacing: 0.28em;
  }
}

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

/* ============================================================
   MANIFESTO — magazine-spread layout for /manifesto.html
   Big bold headline-style sentence with the four approved images
   dropped INLINE between words, sized to the cap-height of the
   surrounding text. Inspired by editorial spreads like Apostrophe
   "REPS".
   ============================================================ */

.has-manifesto {
  --manifesto-bg: #ff48b0;
  --manifesto-ink: #00b74f;
  background: var(--manifesto-bg);
  color: var(--manifesto-ink);
  min-height: 100vh;
  margin: 0;
  transition:
    background 0.4s ease,
    color 0.4s ease;
}

/* High-contrast mode: white background, green ink. */
.has-manifesto.is-contrast {
  --manifesto-bg: #ffffff;
  --manifesto-ink: #00b74f;
}

/* Mode toggle button — must be discoverable by colorblind users, so it
   relies on LUMINANCE contrast (black/white) rather than just hue.
   Small pill that inverts against its background: black-on-white in
   art mode, white-on-black in high-contrast mode. */
.has-manifesto .manifesto__mode-toggle {
  appearance: none;
  border: 1.5px solid #000;
  padding: 0.4rem 0.85rem;
  margin: 0;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
  white-space: nowrap;
  border-radius: 999px;
  background: #ffffff;
  color: #000000;
  line-height: 1;
  transition:
    background 0.2s ease,
    color 0.2s ease,
    transform 0.18s ease;
}

.has-manifesto.is-contrast .manifesto__mode-toggle {
  background: #000000;
  color: #ffffff;
  border-color: #000000;
}

.has-manifesto .manifesto__mode-toggle:hover,
.has-manifesto .manifesto__mode-toggle:focus-visible {
  outline: none;
  transform: translateY(-1px);
}

/* The pixel logo is itself pink, which disappears against the pink
   manifesto background. Hide it on this page — the GL/TCH wordmark in
   the body copy already does the branding work. */
.has-manifesto .glitch-logo {
  display: none;
}

/* Flex column layout: the manifesto container fills at least one
   viewport. The spread holds the headline and is sized to fill
   100vh minus the rail's height, so the rail sits flush at the
   bottom of the first viewport on initial load. As the user scrolls
   down, the rail scrolls UP with the page and then sticks to the
   top of the viewport (see .manifesto__rail below), and the venn
   section scrolls beneath the stuck rail. The robot is absolutely
   positioned within .manifesto__spread, so it scrolls with the
   headline and disappears once the user scrolls past the spread. */
.has-manifesto .manifesto {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  padding-top: clamp(1rem, 3vh, 2.5rem);
  padding-left: clamp(1.5rem, 4vw, 4rem);
  padding-right: clamp(1.5rem, 4vw, 4rem);
  position: relative;
  box-sizing: border-box;
  /* --rail-h is measured & set by manifesto.js so the spread can size
     itself to (100vh - rail height) and the rail lands flush at the
     bottom of the first viewport. Falls back to a sensible static
     guess if JS is disabled. */
  --rail-h: 4.5rem;
}

.has-manifesto .manifesto__spread {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  /* Fill the first viewport minus the rail height (and container's top
     padding), so the rail naturally sits flush at the bottom of the
     visible viewport on initial load. flex-shrink: 0 so the spread
     doesn't compress when the venn section pushes total content past
     100vh. */
  flex: 0 0 calc(100vh - var(--rail-h) - clamp(1rem, 3vh, 2.5rem));
  min-height: 0;
  /* Containing block for the absolutely-positioned robot so the robot
     anchors to the spread's bottom edge and scrolls AWAY with the
     headline (rather than following the viewport into the venn
     section below). */
  position: relative;
}

/* (Spread layout rules consolidated above — block flow, with margin
   below it that creates the gap before the rail. No flex needed.) */

/* Robot pinned to the viewport bottom-right, just above the fixed
   bottom rail. Both are position: fixed so they stay anchored to the
   visible viewport regardless of how tall the headline content gets.
   Mix-blend-mode makes the GIF's white background "punch through" the
   pink page so it reads as if it were transparent. */
.has-manifesto .manifesto__robot {
  /* Anchored to the spread's bottom-right (NOT the viewport). This
     way the robot scrolls with the headline section and disappears
     once the user scrolls past the spread into the venn area —
     rather than following them down the page.
     The `right` value is negated so the robot's right edge sits flush
     with the VIEWPORT edge (cancelling the .manifesto container's
     horizontal padding) instead of with the spread's content edge. */
  position: absolute;
  right: calc(-1 * clamp(1.5rem, 4vw, 4rem));
  bottom: 0;
  width: clamp(14rem, 30vw, 32rem);
  height: auto;
  display: block;
  pointer-events: none;
  z-index: 5;
  mix-blend-mode: multiply;
  opacity: 0.55;
}

/* The headline sentence. Tight, chunky, condensed-feeling Inter Black —
   clamps from ~36px to ~104px so it fills the spread on big screens but
   stays readable on phones. Word-break: keep-all + small letter-spacing
   keeps the magazine "block" feel. */
.has-manifesto .manifesto__copy {
  margin: 0;
  font-family: "Inter", system-ui, sans-serif;
  font-weight: 900;
  /* Wishful "as big as possible" CSS starting size. JS in manifesto.js
     measures the rendered height after layout and shrinks the font-
     size in a single pass if the headline would overflow 90vh — see
     fitManifestoCopy(). The CSS only has to cap at the upper bound
     where the text would obviously overshoot on huge displays. */
  font-size: clamp(1.4rem, min(8vw, 14vh), 7rem);
  line-height: 0.95;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  text-align: left;
  max-width: 85vw;
  hyphens: none;
}

.has-manifesto .manifesto__copy span {
  display: inline;
  vertical-align: baseline;
}

/* In high-contrast mode every other technology term flips to pink so
   the list alternates green / pink down the page — matching the
   art-mode rhythm. */
.has-manifesto.is-contrast .manifesto__tech--alt {
  color: #ff48b0;
}

/* Per-letter glitch.

   JS wraps every word in the manifesto headline with a non-breaking
   .manifesto__word-wrap and every character inside that with a
   .manifesto__letter. A pointermove handler sets --glitch (0..1) on
   each letter based on its distance to the cursor. The styles below
   convert that scalar into a heavy, busy, "broken pixels" visual:

     - bigger stepped jitter (translate, rotate, skew, scale)
     - 7-layer text-shadow chromatic aberration
       (cyan / magenta / yellow + brand pink / brand green + ghost
       trails in currentColor)
     - blur + contrast pump
     - hard fade-to-0 so letters near the cursor fully vanish

   At rest (--glitch: 0) every property folds to its no-op value so the
   headline reads as plain inline text. */

.has-manifesto .manifesto__word-wrap {
  display: inline-block;
  white-space: nowrap;
}

.has-manifesto .manifesto__letter {
  /* --glitch is the max of cursor proximity and ambient noise so the
     two effects compose without fighting each other. */
  --cursor-glitch: 0;
  --noise-glitch: 0;
  --glitch: max(var(--cursor-glitch), var(--noise-glitch));
  --glitch-dx: 0;
  --glitch-dy: 0;
  --glitch-dr: 0;
  display: inline-block;
  position: relative;
  transform:
    translate(
      calc(var(--glitch-dx) * var(--glitch) * 26px),
      calc(var(--glitch-dy) * var(--glitch) * 20px)
    )
    rotate(calc(var(--glitch-dr) * var(--glitch) * 16deg))
    skewX(calc(var(--glitch-dx) * var(--glitch) * 18deg))
    scale(calc(1 + var(--glitch) * 0.25));
  /* Multiplier > 1 so the letter fully vanishes well before glitch
     reaches its peak — cursor "burns" through the text. */
  opacity: calc(1 - var(--glitch) * 1.5);
  filter:
    blur(calc(var(--glitch) * 1.4px))
    contrast(calc(1 + var(--glitch) * 1.6))
    saturate(calc(1 + var(--glitch) * 2));
  text-shadow:
    /* Wide RGB chromatic split */
    calc(var(--glitch) * -7px) 0 0 #00ffff,
    calc(var(--glitch) * 7px) 0 0 #ff00ff,
    /* Yellow/cyan vertical aberration */
    0 calc(var(--glitch) * -4px) 0 #ffff00,
    0 calc(var(--glitch) * 4px) 0 #00ffff,
    /* Brand-color far ghosts */
    calc(var(--glitch) * 11px) calc(var(--glitch) * 1px) 0 #ff48b0,
    calc(var(--glitch) * -11px) calc(var(--glitch) * -1px) 0 #00b74f,
    /* CurrentColor smear trails */
    calc(var(--glitch) * 2px) calc(var(--glitch) * 3px) 0 currentColor,
    calc(var(--glitch) * -2px) calc(var(--glitch) * -3px) 0 currentColor;
  /* Stepped easing on structural properties → reads as pixely / dropped
     frames rather than a smooth tween. */
  transition:
    transform 60ms steps(2, end),
    opacity 50ms steps(2, end),
    filter 60ms linear,
    text-shadow 60ms linear;
  will-change: transform, opacity, filter;
}

@media (prefers-reduced-motion: reduce) {
  .has-manifesto .manifesto__letter {
    transition: none;
    transform: none;
    opacity: 1;
    filter: none;
    text-shadow: none;
  }
}

/* Inline image chips — the LAYOUT box is sized to roughly the cap-height
   of the surrounding type (height: 1em), so the chips don't push the
   line-height. They are then visually SCALED UP via transform: scale()
   inside the jitter keyframes — transforms don't affect layout, so the
   visual image overflows into the lines above/below without changing
   the bounding box. This gives chips a "lower bounding hit box" while
   still reading as bold photo blocks within the headline. */
.has-manifesto .manifesto__chip {
  --chip-scale: 1.53;
  --push-x: 0px;
  --push-y: 0px;
  /* Pixelate-disappear: 0..1, driven by JS based on how close the
     cursor is to the chip. The blur + extreme contrast trick
     posterizes the chip into chunky pixel blocks before opacity
     finishes the job. */
  --pixelize: 0;
  display: inline-block;
  height: 1em;
  width: auto;
  max-width: 1em;
  object-fit: cover;
  vertical-align: middle;
  margin: 0 0.45em;
  border-radius: 2px;
  will-change: transform, filter, opacity;
  animation: manifesto-jitter 4.6s steps(10, end) infinite;
  transform: scale(var(--chip-scale));
  filter:
    blur(calc(var(--pixelize) * 8px))
    contrast(calc(1 + var(--pixelize) * 9))
    saturate(calc(1 + var(--pixelize) * 2));
  opacity: calc(1 - var(--pixelize));
  /* Crisp scaled rendering keeps the high-contrast posterized output
     looking blocky rather than smooth. */
  image-rendering: pixelated;
  transition:
    --push-x 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.2),
    --push-y 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.2),
    filter 40ms linear,
    opacity 40ms linear;
}

/* Each chip uses a slightly different scale + animation timing so the
   composition feels alive rather than synchronized. */
.has-manifesto .manifesto__chip--a {
  --chip-scale: 1.67;
  animation-duration: 4.2s;
  animation-delay: 0s;
}

.has-manifesto .manifesto__chip--b {
  --chip-scale: 1.53;
  animation-duration: 5.4s;
  animation-delay: -1.3s;
}

.has-manifesto .manifesto__chip--c {
  --chip-scale: 1.76;
  animation-duration: 4.8s;
  animation-delay: -2.4s;
}

.has-manifesto .manifesto__chip--d {
  --chip-scale: 1.58;
  animation-duration: 5.0s;
  animation-delay: -3.5s;
}

.has-manifesto .manifesto__chip--e {
  --chip-scale: 1.71;
  animation-duration: 4.5s;
  animation-delay: -1.9s;
}

/* Glitchy 10-step keyframe — wider XY translates so the chips visibly
   drift over adjacent words at times, plus a tiny rotation. Each keyframe
   re-applies scale(var(--chip-scale)) so the visual size persists across
   the jitter motion. `steps(10, end)` on the chip makes the jumps snap
   between frames instead of tweening — stop-motion / TV-static feel. */
/* Each keyframe combines the jitter offset with the cursor "push" via
   var(--push-x) / var(--push-y). The push is set by JS in response to
   pointermove so chips drift away from the cursor like magnets. */
@keyframes manifesto-jitter {
  0% {
    transform:
      translate(var(--push-x), var(--push-y))
      rotate(0deg)
      scale(var(--chip-scale));
  }
  10% {
    transform:
      translate(calc(var(--push-x) - 2px), calc(var(--push-y) + 1px))
      rotate(-0.6deg)
      scale(var(--chip-scale));
  }
  20% {
    transform:
      translate(calc(var(--push-x) + 3px), calc(var(--push-y) - 1px))
      rotate(0.9deg)
      scale(var(--chip-scale));
  }
  30% {
    transform:
      translate(calc(var(--push-x) - 3px), calc(var(--push-y) - 1px))
      rotate(-0.45deg)
      scale(var(--chip-scale));
  }
  40% {
    transform:
      translate(calc(var(--push-x) + 1px), calc(var(--push-y) + 3px))
      rotate(0.75deg)
      scale(var(--chip-scale));
  }
  50% {
    transform:
      translate(calc(var(--push-x) - 1px), calc(var(--push-y) - 3px))
      rotate(-0.75deg)
      scale(var(--chip-scale));
  }
  60% {
    transform:
      translate(calc(var(--push-x) + 4px), calc(var(--push-y) + 1px))
      rotate(0.45deg)
      scale(var(--chip-scale));
  }
  70% {
    transform:
      translate(calc(var(--push-x) - 4px), calc(var(--push-y) + 1px))
      rotate(-0.9deg)
      scale(var(--chip-scale));
  }
  80% {
    transform:
      translate(calc(var(--push-x) + 2px), calc(var(--push-y) - 2px))
      rotate(0.6deg)
      scale(var(--chip-scale));
  }
  90% {
    transform:
      translate(calc(var(--push-x) - 1px), calc(var(--push-y) + 2px))
      rotate(-0.45deg)
      scale(var(--chip-scale));
  }
  100% {
    transform:
      translate(var(--push-x), var(--push-y))
      rotate(0deg)
      scale(var(--chip-scale));
  }
}

@media (prefers-reduced-motion: reduce) {
  .has-manifesto .manifesto__chip {
    animation: none;
  }
}

/* Bottom rail: tagline on the left, meta on the right, separated like a
   masthead/colophon. */
.has-manifesto .manifesto__rail {
  /* Sits in document flow at the bottom of the first viewport thanks
     to .manifesto__spread's flex-basis. As the user scrolls down, the
     rail moves up with the page and then sticks to the top once it
     reaches the top edge — staying anchored while the venn section
     scrolls beneath it. */
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  /* Counter-bleed: extend horizontally to the viewport edges by
     using negative margins equal to the parent's horizontal padding,
     so the sticky bar reads as a full-width band at the top. The
     inner padding then re-creates the original inset for the text. */
  margin: 0 calc(-1 * clamp(1.5rem, 4vw, 4rem));
  padding: clamp(1.25rem, 2.5vh, 2rem) clamp(1.5rem, 4vw, 4rem)
    clamp(1.1rem, 2.2vh, 1.6rem);
  border-top: 1px solid currentColor;
  /* Opaque so the venn diagram scrolling beneath doesn't bleed
     through once the rail is stuck. */
  background: var(--manifesto-bg);
  font-family: "Inter", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(0.7rem, 0.95vw, 0.95rem);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.has-manifesto .manifesto__rail-tag,
.has-manifesto .manifesto__rail-meta {
  margin: 0;
}

.has-manifesto .manifesto__rail-meta {
  text-align: right;
  letter-spacing: 0.16em;
}

/* On narrow screens make the robot a smaller corner accent so it doesn't
   dominate the spread. */
@media (max-width: 720px) {
  .has-manifesto .manifesto__robot {
    width: clamp(10rem, 38vw, 16rem);
    /* Same edge-flush trick as the desktop rule — counter the
       narrower .manifesto padding on small screens. */
    right: calc(-1 * clamp(1.5rem, 4vw, 4rem));
  }

  .has-manifesto .manifesto__copy {
    font-size: clamp(1.6rem, 7vw, 2.6rem);
    line-height: 1.02;
  }

  .has-manifesto .manifesto__rail {
    flex-direction: column;
    gap: 0.4rem;
  }

  .has-manifesto .manifesto__rail-meta {
    text-align: left;
    letter-spacing: 0.1em;
  }
}

/* ----------------------------------------------------------------------
   Manifesto Venn diagram
   Sits below the manifesto rail, revealed by scroll. Two big black
   outlined circles overlapping ~30% with "fear" / "utopia" titles
   centered above each non-overlapping bulb.
   ---------------------------------------------------------------------- */
.has-manifesto .manifesto-venn {
  position: relative;
  /* Counter-bleed: extend horizontally to the viewport edges by
     using negative margins equal to the parent's horizontal padding
     (matches the rail's bleed treatment so the section reads as a
     full-width band beneath the sticky rail). */
  margin: 0 calc(-1 * clamp(1.5rem, 4vw, 4rem));
  /* Lock the section at exactly 90vh so the fear / utopia diagram
     occupies one viewport-height of vertical space — no more. The
     venn box itself is sized below to fit comfortably inside this
     with a bit of breathing room from flex centering. */
  height: 90vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 clamp(1rem, 4vw, 3rem);
  background: var(--manifesto-bg);
  overflow: hidden;
}

.has-manifesto .venn {
  /* Diameter is capped by THREE constraints so the venn always fits:
     - 56vw (so it doesn't blow out wide on landscape monitors)
     - 75vh (so label + gap + diameter still fit inside the 90vh
       section bound on .manifesto-venn, leaving breathing room)
     - 820px absolute max for huge displays
     ...and a 260px floor so it stays legible on small viewports. */
  --venn-d: clamp(260px, min(56vw, 75vh), 820px);
  --venn-overlap: 0.32;
  --venn-label-fs: clamp(2rem, 5vw, 4.5rem);
  --venn-label-h: var(--venn-label-fs);
  --venn-gap: clamp(0.6rem, 1.5vh, 1.5rem);
  /* Stroke + label color: green in art mode, black in high-contrast.
     Overridden below for .has-manifesto.is-contrast. */
  --venn-ink: #00b74f;
  position: relative;
  width: calc(var(--venn-d) * (2 - var(--venn-overlap)));
  height: calc(var(--venn-label-h) + var(--venn-gap) + var(--venn-d));
  max-width: 100%;
}

.has-manifesto.is-contrast .venn {
  --venn-ink: #000;
}

.has-manifesto .venn__circle {
  position: absolute;
  /* Drop below the label row so titles sit ABOVE the circles. */
  top: calc(var(--venn-label-h) + var(--venn-gap));
  width: var(--venn-d);
  height: var(--venn-d);
  border-radius: 50%;
  border: clamp(4px, 0.55vw, 8px) solid var(--venn-ink);
  /* Don't intercept pointer events — let them fall through to the
     ghost <img> elements underneath (which DO want pointer-events:
     auto so they can be dragged). The circles paint AFTER the
     ghost wrapper in DOM, so without this they'd swallow every
     click/touch even though they're just outlines. */
  pointer-events: none;
}

.has-manifesto .venn__circle--left {
  left: 0;
}

.has-manifesto .venn__circle--right {
  right: 0;
}

/* Labels sit in the top "lane" of the venn box, each centered
   horizontally above its corresponding circle. Match the manifesto
   headline's display treatment: Inter Black uppercase, tight tracking. */
.has-manifesto .venn__label {
  position: absolute;
  top: 0;
  width: var(--venn-d);
  height: var(--venn-label-h);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  font-family: "Inter", system-ui, sans-serif;
  font-weight: 900;
  font-size: var(--venn-label-fs);
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--venn-ink);
  white-space: nowrap;
  margin: 0;
  /* Don't intercept clicks meant for the ghost clips drifting beneath. */
  pointer-events: none;
}

.has-manifesto .venn__label--left {
  left: 0;
}

.has-manifesto .venn__label--right {
  right: 0;
}

@media (max-width: 640px) {
  .has-manifesto .venn {
    /* Diameter is bumped ~2% on mobile (58vw -> 59.16vw) so the
       diagram reads slightly larger on phones without overflowing the
       padded venn section. Paired with a 5% ghost-clip boost in
       manifesto.js (see MOBILE_GHOST_BOOST). */
    --venn-d: calc(58vw * 1.02);
    --venn-overlap: 0.4;
    --venn-label-fs: clamp(1.6rem, 7vw, 3rem);
  }

  /* On phones the two labels sit very close together because the
     circles overlap by 40% of their diameter. Pad the INNER edge of
     each label so the centered text gets pushed outward, opening up
     breathing room between "FEAR" and "UTOPIA". The flex container's
     `justify-content: center` re-evaluates against the reduced
     content width, so padding-right on the left label nudges its
     text leftward, and vice versa. */
  .has-manifesto .venn__label--left {
    padding-right: 18%;
  }
  .has-manifesto .venn__label--right {
    padding-left: 18%;
  }
}

/* Ghost clips drifting between fear and utopia.
   Each image is positioned absolutely inside the venn box and rendered
   at full opacity. Motion is fully JS-driven — see the ghost physics
   loop in manifesto.js. Each ghost has its own velocity vector (vx,
   vy) and bounces off the venn box edges like a ping-pong ball;
   bounces add random kicks to the perpendicular component so
   trajectories never repeat, and a periodic nudge rotates the velocity
   vector for the "switches direction randomly" feel.
   The clips are also touch/mouse-draggable: pointer-events: auto and
   touch-action: none on each <img> let the user grab a clip and slide
   it to a new spot. While grabbed the physics integration for that
   ghost is paused; on release it resumes drifting from its new home
   in a fresh random direction. */
.has-manifesto .venn__ghosts {
  position: absolute;
  inset: 0;
  /* The wrapper itself is non-interactive — only the actual image
     children below override this with pointer-events: auto so empty
     space between ghosts doesn't catch clicks. */
  pointer-events: none;
  /* Sit BEHIND the circle borders and labels so the ghosts feel like
     they're drifting through the diagram. */
  z-index: 0;
}

.has-manifesto .venn__ghost {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--ghost-w, 12rem);
  height: calc(var(--ghost-w, 12rem) * var(--ghost-aspect, 0.85));
  object-fit: cover;
  opacity: 1;

  /* Pointer interaction. */
  pointer-events: auto;
  touch-action: none; /* So touch-drag doesn't trigger page scroll. */
  cursor: grab;
  user-select: none;
  -webkit-user-drag: none;

  /* JS sets transform: translate3d(x, y, 0) rotate(...) every frame.
     will-change tells the browser to promote to its own layer. */
  will-change: transform;
}

.has-manifesto .venn__ghost.is-grabbed {
  cursor: grabbing;
  /* Lift the grabbed clip above its peers so it visually leads while
     dragging. */
  z-index: 1;
}

/* Once a clip has been dropped (pinned), hovering it triggers a
   pixelate-and-disappear glitch — the same vocabulary as the manifesto
   headline's inline chips, so the page feels coherent. We use
   `steps()` on the transitions so the dissolution stutters in
   discrete frames instead of smoothly fading, which reads more like
   a glitch than a fade-out.

   Only pinned clips get this treatment — they're stable hover targets,
   unlike the drifting clips (which is the only state we'd actually
   call "ghosts" — they stop being ghosts the moment they're placed).
   The `.venn__ghost` class name in the markup is kept as-is because
   that's the default state every clip is born in.

   On hover-OUT the transition has no delay so the image snaps back
   fast; on hover-IN there's a brief delay so the image stays visible
   for a beat right after the user pinned it (cursor is still over
   the clip at that moment). */
.has-manifesto .venn__ghost.is-pinned {
  transition:
    filter 0.4s steps(8, end),
    opacity 0.4s steps(8, end);
}

.has-manifesto .venn__ghost.is-pinned:hover {
  /* Glitch HARD but don't vanish — opacity bottoms out at ~35% so the
     image stays present on the page, just heavily distorted, rather
     than dissolving entirely. */
  filter: blur(7px) contrast(8) saturate(2.5) hue-rotate(8deg);
  opacity: 0.35;
  transition:
    filter 0.4s steps(8, end) 0.2s,
    opacity 0.4s steps(8, end) 0.2s;
}

