/* ==========================================================================
   Uncultured Studios
   One stylesheet for the whole site. No inline styles anywhere, because the
   Content-Security-Policy in public/_headers refuses 'unsafe-inline' - an
   inline style="" attribute will simply be dropped by the browser.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Font. Self-hosted so the site makes zero third-party requests.
   VT323 (SIL Open Font License 1.1) - the retro terminal face.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/vt323-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/vt323-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/vt323-vietnamese.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
    U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
    U+1EA0-1EF9, U+20AB;
}

/* --------------------------------------------------------------------------
   2. Tokens. Every size and colour on the site resolves to something here,
   which is what keeps the type uniform across pages.
   -------------------------------------------------------------------------- */

:root {
  --bg: #000000;
  --ink: #f9f4ee; /* the cream from the original site */
  --ink-dim: #8f8981;
  --ink-faint: #4a463f;
  --rule: #26231f;
  --rule-bright: #454039;
  --cyan: #35d6ff;
  --magenta: #ff30bb;
  --panel: #070706;

  /* Matrix rain. Swap --rain to #f9f4ee or #ffb000 for cream / amber phosphor
     instead of green - it is the only place the rain colour is defined. */
  --rain: #00ff5f;
  --rain-head: #d9ffe8;
  --rain-opacity: 0.85;

  /* One type scale, used everywhere. VT323 is a pixel face and reads small for
     its nominal size, so these run larger than a normal scale would. */
  --fs-micro: clamp(0.95rem, 0.4vw + 0.85rem, 1.15rem);
  --fs-small: clamp(1.1rem, 0.45vw + 1rem, 1.35rem);
  --fs-nav: clamp(1.25rem, 0.5vw + 1.1rem, 1.5rem);
  --fs-body: clamp(1.3rem, 0.55vw + 1.15rem, 1.6rem);
  --fs-lead: clamp(1.6rem, 1vw + 1.3rem, 2.1rem);
  --fs-h3: clamp(2rem, 2vw + 1.4rem, 3rem);
  --fs-h2: clamp(2.4rem, 3vw + 1.5rem, 4rem);
  --fs-h1: clamp(3.2rem, 7vw + 1rem, 7.5rem);

  --lh-tight: 0.95;
  --lh-normal: 1.35;

  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.75rem;
  --space-4: 3rem;
  --space-5: 5rem;
  --space-6: 8rem;

  --measure: 62ch;
  --shell: 1180px;

  --focus: #00ff5f;
}

/* --------------------------------------------------------------------------
   3. Reset / base
   -------------------------------------------------------------------------- */

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

html {
  background: var(--bg);
  /* Anchor jumps land below the fixed masthead instead of under it. */
  scroll-padding-top: 6rem;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100svh;
  /* Sticky footer. On a short page the main content grows to fill the viewport
     so the footer sits at the bottom instead of floating mid-screen with the
     background showing beneath it. The fixed-position children - the masthead,
     the backdrop canvases, the accessibility button - are out of flow and are
     not flex items, so they are unaffected. */
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--ink);
  font-family: "VT323", ui-monospace, "Courier New", monospace;
  font-size: var(--fs-body);
  line-height: var(--lh-normal);
  /* VT323 is a bitmap-derived face; smoothing it off keeps the pixel edges. */
  -webkit-font-smoothing: none;
  font-smooth: never;
  text-rendering: optimizeSpeed;
  overflow-x: hidden;
}

/* A nearly invisible raster sits above page art and below the navigation.
   It joins the photo, canvas and text pages into one broadcast surface without
   lowering contrast or intercepting a single click. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 10;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent 3px,
    rgba(255, 255, 255, 0.018) 3px,
    rgba(255, 255, 255, 0.018) 4px
  );
}

::selection {
  color: var(--bg);
  background: var(--rain);
}

h1,
h2,
h3,
p,
ul,
figure {
  margin: 0;
}

ul {
  padding: 0;
  list-style: none;
}

img,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
}

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

/* --------------------------------------------------------------------------
   4. Typography roles
   -------------------------------------------------------------------------- */

.t-h1 {
  font-size: var(--fs-h1);
  line-height: var(--lh-tight);
  font-weight: 400;
  letter-spacing: 0.01em;
}

.t-h2 {
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
  font-weight: 400;
}

.t-h3 {
  font-size: var(--fs-h3);
  line-height: 1.05;
  font-weight: 400;
}

.t-lead {
  font-size: var(--fs-lead);
  line-height: 1.25;
}

.t-body {
  font-size: var(--fs-body);
  max-width: var(--measure);
}

.t-micro {
  font-size: var(--fs-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* --------------------------------------------------------------------------
   5. Layout
   -------------------------------------------------------------------------- */

.shell {
  width: min(100% - 2.5rem, var(--shell));
  margin-inline: auto;
}

.page {
  /* Just breathing room under the masthead, NOT clearance for it.
     The masthead is sticky, so it occupies its own space in the flow and
     cannot land on top of this. See the note there. */
  padding-top: clamp(2.5rem, 5vw, 3.5rem);
  padding-bottom: var(--space-6);
  /* Above .rain and .rain-veil. */
  position: relative;
  z-index: 2;
  /* Grows to push the footer down - see the flex note on body. */
  flex: 1 0 auto;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: var(--space-1) var(--space-2);
  background: var(--ink);
  color: var(--bg);
  font-size: var(--fs-nav);
}

.skip-link:focus {
  left: var(--space-2);
  top: var(--space-2);
}

/* --------------------------------------------------------------------------
   6. Masthead / nav
   -------------------------------------------------------------------------- */

/* Sticky, not fixed.
 *
 * Fixed meant the bar was out of the flow, so the page had to reserve a
 * hardcoded gap for it - and that gap was a guess about the bar's height. On a
 * narrow screen the five nav items wrap onto two lines, the bar grows from 55px
 * to 99px, and the guess was 19px short: the "Uncultured" heading was sliced
 * off behind the nav on every phone.
 *
 * Sticky keeps the identical pinned-on-scroll behaviour but leaves the bar IN
 * the flow, so it always takes exactly the room it needs and content starts
 * after it. The overlap cannot come back, whatever the nav ends up containing.
 */
.masthead {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--rule);
  box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.38);
}

.masthead__inner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 3.4rem;
}

/* Three one-pixel signal faults are the shared visual signature in the bar. */
.masthead__inner::after {
  content: "";
  width: 1.9rem;
  height: 2px;
  margin-left: auto;
  background: linear-gradient(
    90deg,
    var(--cyan) 0 18%,
    transparent 18% 42%,
    var(--magenta) 42% 62%,
    transparent 62% 78%,
    var(--rain) 78% 100%
  );
  opacity: 0.7;
}

.nav {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1rem, 3vw, 2.5rem);
}

/* .textlink is the same treatment for links OUTSIDE the masthead - "back to
   the guide" and so on. Same look, different name, so a back link is never
   mistaken for a navigation item by a screen reader, a test, or you. */
.nav__link,
.textlink {
  display: inline-block;
  padding: 0.35rem 0;
  font-size: var(--fs-nav);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 2px solid transparent;
  transition: color 120ms linear, border-color 120ms linear;
}

.nav__link:hover,
.nav__link:focus-visible,
.textlink:hover,
.textlink:focus-visible {
  color: var(--rain);
  border-bottom-color: var(--rain);
}

.nav__link[aria-current="page"] {
  color: var(--ink);
  border-bottom-color: var(--rain);
  text-shadow: 0 0 0.75rem rgba(0, 255, 95, 0.18);
}

@media (max-width: 34rem) {
  .masthead__inner {
    justify-content: center;
  }

  .nav {
    width: 100%;
    flex-wrap: nowrap;
    justify-content: space-between;
    gap: 0.35rem;
  }

  .nav__link {
    font-size: 1.08rem;
    letter-spacing: 0.09em;
  }

  .masthead__inner::after {
    display: none;
  }

  :root[data-text="lg"] .nav,
  :root[data-text="xl"] .nav {
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.4rem 1rem;
  }
}

/* --------------------------------------------------------------------------
   7. Matrix rain backdrop
   -------------------------------------------------------------------------- */

/* Stacking note: these deliberately use positive z-indexes rather than the
   obvious negative ones. body is not a stacking context, so a negative z-index
   here would escape up to the root context and paint BEHIND body's background -
   i.e. the rain would run, cost CPU, and be completely invisible. Everything
   that must sit above the rain gets z-index 2 (see .page and .footer). */
.rain {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  opacity: var(--rain-opacity);
  pointer-events: none;
}

/* Neon tubes in the side margins - see assets/js/neon.js. Same stacking rules
   as the rain: positive z-index, with content lifted above it. */
.neon {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Keeps the centre of the page readable through the rain. */
.rain-veil {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
    ellipse 72% 62% at 50% 45%,
    rgba(0, 0, 0, 0.6) 0%,
    rgba(0, 0, 0, 0.38) 45%,
    rgba(0, 0, 0, 0.04) 100%
  );
}

/* --------------------------------------------------------------------------
   8. The television
   The screen inset is measured off the source artwork (the glass sits at
   x 372..1650, y 92..1090 of a 2000x1435 image), so the static lands exactly
   inside the bezel at any size. Every TV on the site uses this one component,
   which is what makes the guide TVs match the home page TV.
   -------------------------------------------------------------------------- */

.tv {
  position: relative;
  width: 100%;
  /* The artwork is cropped to the television itself (tools/optimize-assets.mjs),
     so this box IS the set - no invisible margin. That is what lets the remote
     be placed against the cabinet instead of against empty transparent pixels. */
  aspect-ratio: 1400 / 1359;
}

/* The artwork sits ON TOP of the picture, and its glass has been cut out
   (tools/optimize-assets.mjs), so the cabinet frames the video with its own
   real edge - curved corners, bezel shadow and all.
   This is what makes it seamless. The previous arrangement put a rectangular
   video over the artwork, and a rectangle meeting a curved bezel always shows
   a hard straight seam no matter how carefully it is positioned. */
.tv__set {
  position: relative;
  z-index: 2;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
}

/* Sits BEHIND the artwork, and deliberately a little larger than the hole in
   it. The overspill is hidden by the cabinet, which guarantees the picture
   reaches every edge of the glass with no sliver of background showing. */
.tv__screen {
  position: absolute;
  z-index: 1;
  left: 5.8%;
  top: 4.8%;
  width: 89.6%;
  height: 72.8%;
  overflow: hidden;
  background: #050505;
  pointer-events: none;
}

/* A set you can actually switch on needs to receive clicks. Only the playable
   one does; the rest stay inert so they never swallow a stray tap. */
.tv--playable .tv__screen {
  pointer-events: auto;
}

.tv__static {
  width: 100%;
  height: 100%;
  /* The noise canvas is rendered tiny and blown up; nearest-neighbour keeps it
     crunchy instead of turning it into grey soup. */
  image-rendering: pixelated;
  opacity: 0.62;
}

/* A short muted loop sitting on the glass, so the set reads as switched on and
   actually playing something. Stacked over .tv__static, which stays underneath
   as the "signal" the picture is fighting through. */
/* The idle loop is blurred on purpose: the set should read as switched on and
   showing something, without actually giving the film away to anyone who has
   not chosen to watch it. The blur is lifted only when the visitor presses
   play - see the [data-playing="full"] rule below.
   The slight scale hides the soft transparent edge that blur creates. */
.tv__video {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.92;
  filter: blur(clamp(4px, 1.1vw, 13px)) saturate(1.15);
  /* Only enough to keep the blur's soft edge out of sight; the screen box is
     already larger than the hole, so this no longer has to do much work. */
  transform: scale(1.04);
}

/* Faint snow still bleeding through the picture. */
.tv__screen:has(.tv__video) .tv__static {
  opacity: 0.3;
}

/* Playing for real: sharp, unblurred, and the snow gets out of the way. */
.tv__video[data-playing="full"] {
  opacity: 1;
  filter: none;
  transform: none;
}

.tv__screen:has(.tv__video[data-playing="full"]) .tv__static {
  opacity: 0;
}

/* The scanlines sit over the picture, but they must not sit over the native
   playback controls or they eat the clicks. */
.tv__screen:has(.tv__video[data-playing="full"])::after {
  display: none;
}

/* "Switch the set on" - covers the whole glass so the target is the television
   itself, not a small button floating on top of it. */
.tv__play {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-content: center;
  gap: 0.6rem;
  justify-items: center;
  padding: 0;
  cursor: pointer;
  border: 0;
  background: rgba(0, 0, 0, 0.28);
  color: var(--ink);
  font-family: inherit;
  transition: background-color 160ms linear;
}

.tv__play:hover,
.tv__play:focus-visible {
  background: rgba(0, 0, 0, 0.12);
}

.tv__play[hidden] {
  display: none;
}

.tv__play-icon {
  width: clamp(2.6rem, 7vw, 4.2rem);
  height: clamp(2.6rem, 7vw, 4.2rem);
  border: 2px solid currentColor;
  border-radius: 50%;
  display: grid;
  place-content: center;
  transition: transform 160ms ease-out, color 160ms linear;
}

/* The triangle, drawn with a border rather than an image. */
.tv__play-icon::before {
  content: "";
  border-style: solid;
  border-width: 0.55em 0 0.55em 0.9em;
  border-color: transparent transparent transparent currentColor;
  margin-left: 0.22em;
}

.tv__play:hover .tv__play-icon,
.tv__play:focus-visible .tv__play-icon {
  color: var(--rain);
  transform: scale(1.08);
}

.tv__play-label {
  font-size: var(--fs-lead);
  line-height: 1;
  text-align: center;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.95);
}

.tv__play-sub {
  margin-top: -0.35rem;
  font-size: var(--fs-micro);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-align: center;
  color: var(--ink-dim);
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.95);
}

.tv__play:hover .tv__play-sub,
.tv__play:focus-visible .tv__play-sub {
  color: var(--ink);
}

/* Phosphor lines over the picture. The heavy edge vignette that used to be here
   is gone: the cabinet's own bezel shadow does that job now, and doubling it up
   just made the picture look like it stopped short of the glass. */
.tv__screen::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Above both the static and the video - the glass is in front of the picture. */
  z-index: 3;
  pointer-events: none;
  background: repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0.32) 0px,
      rgba(0, 0, 0, 0.32) 1px,
      transparent 1px,
      transparent 3px
    ),
    radial-gradient(
      ellipse 95% 95% at 50% 48%,
      transparent 72%,
      rgba(0, 0, 0, 0.28) 100%
    );
}

/* --------------------------------------------------------------------------
   9. Home
   -------------------------------------------------------------------------- */

.hero {
  display: grid;
  justify-items: center;
  gap: clamp(2rem, 4vw, var(--space-4));
  text-align: center;
}

.hero__title {
  position: relative;
  display: grid;
  gap: 0.1em;
  text-wrap: balance;
}

.hero__title::before {
  content: "";
  width: 3.4rem;
  height: 1px;
  margin-bottom: 0.35rem;
  justify-self: center;
  background: var(--rain);
  box-shadow:
    -0.75rem 0 var(--cyan),
    0.75rem 0 var(--magenta);
  opacity: 0.72;
}

.hero__tv {
  /* Preserve the full-width phone treatment, but use the available viewport
     height on larger screens so the complete cabinet fits in the opening
     composition. The cap keeps it prominent on tall displays. */
  width: min(100%, clamp(20rem, calc(100svh - 23rem), 680px));
}

.page-home .hero__tv,
.page-error .hero__tv {
  filter: drop-shadow(0 1rem 1.8rem rgba(0, 0, 0, 0.72));
}

.page-home .hero__tv {
  transition: filter 180ms linear, transform 180ms ease-out;
}

.page-home .hero__tv:has(.tv__play:hover),
.page-home .hero__tv:has(.tv__play:focus-visible) {
  filter:
    drop-shadow(0 1rem 1.8rem rgba(0, 0, 0, 0.72))
    drop-shadow(0 0 0.8rem rgba(0, 255, 95, 0.2));
  transform: translateY(-0.2rem);
}

.hero__tagline {
  display: grid;
  gap: 0.15em;
  /* Let the television own the first impression. The unfinished phrase sits
     lower, and the countdown begins after a deliberate pause so it is found
     by scrolling rather than competing with the set. */
  margin-top: var(--space-3);
  margin-bottom: var(--space-5);
}

.hero__tagline::after {
  content: "";
  width: 1px;
  height: clamp(2rem, 5vw, 3.5rem);
  margin-top: var(--space-2);
  justify-self: center;
  background: var(--rule-bright);
  box-shadow: 0 0 0.6rem rgba(0, 255, 95, 0.16);
}

/* --------------------------------------------------------------------------
   Countdown
   Built to read as a broadcast panel rather than a widget: four bordered cells
   in the same hairline-and-dim-label language as the channel guide and the
   journal fields, with a faint phosphor glow on the digits.
   -------------------------------------------------------------------------- */

.countdown__units {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(0.4rem, 1.2vw, 0.9rem);
  padding: clamp(0.8rem, 2vw, 1.35rem);
  border-top: 1px solid var(--rule-bright);
  border-bottom: 1px solid var(--rule-bright);
}

.countdown__units::before,
.countdown__units::after {
  content: "+";
  position: absolute;
  top: -0.7em;
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  line-height: 1;
  background: var(--bg);
}

.countdown__units::before {
  left: -0.2em;
}

.countdown__units::after {
  right: -0.2em;
}

.countdown__unit {
  display: grid;
  justify-items: center;
  gap: 0.3rem;
  /* Wide enough for three digits, so the row never reflows as the numbers
     count down - 365 days is the widest it ever gets. */
  min-width: clamp(4.2rem, 7vw, 5.6rem);
  padding: 0.55rem 0.7rem;
  border-left: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  background: transparent;
}

.countdown__num {
  font-size: var(--fs-h3);
  line-height: 1;
  color: var(--ink);
  /* The glass glow, dialled well down. */
  text-shadow: 0 0 14px rgba(249, 244, 238, 0.28);
  font-variant-numeric: tabular-nums;
}

.countdown__label {
  font-size: var(--fs-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* High contrast: the glow is the first thing to go. */
:root[data-contrast="high"] .countdown__num {
  text-shadow: none;
}

:root[data-contrast="high"] .countdown__label {
  color: #ffffff;
}

@media (max-width: 34rem) {
  .page-home .page {
    padding-top: 2.25rem;
  }

  .hero {
    gap: var(--space-3);
  }

  .hero__tv {
    width: min(100%, 22rem);
  }

  .hero__tagline {
    margin-top: var(--space-2);
    margin-bottom: var(--space-4);
  }

  .countdown__units {
    gap: 0.3rem;
    padding-inline: 0;
  }

  .countdown__unit {
    min-width: 4rem;
    padding-inline: 0.45rem;
  }
}

/* --------------------------------------------------------------------------
   10. Channel guide
   -------------------------------------------------------------------------- */

.guide__head {
  position: relative;
  display: grid;
  gap: var(--space-2);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--rule);
}

.guide__head::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: -1px;
  width: clamp(2.5rem, 8vw, 6rem);
  height: 2px;
  background: var(--rule-bright);
  box-shadow:
    -0.55rem 0 var(--magenta),
    -0.9rem 0 var(--cyan);
}

/* Centred across the full page, while the post blocks below start at the left
   edge. The two sit on different axes deliberately - the title belongs to the
   page, the posts are a column running down its left side. */
.guide__head--centred {
  width: 100%;
  text-align: center;
}

/* "N's Log", where the N is the television sprite. Baseline-aligned to the
   apostrophe by eye; sized in em so it scales with the heading. */
.logtitle {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 0.06em;
}

.logtitle__tv {
  height: 1.05em;
  width: calc(1.05em * 9 / 10);
  align-self: center;
  margin-bottom: -0.12em;
  image-rendering: pixelated;
  filter: drop-shadow(0 0 0.45rem rgba(0, 255, 95, 0.32));
}

.channel {
  position: relative;
  display: grid;
  /* The stage column gets the larger share so the television stays a decent
     size even after 40% of the stage is given over to the gap and the remote. */
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(1.5rem, 4vw, 4rem);
  padding-left: clamp(4.5rem, 7vw, 6.5rem);
  padding-block: var(--space-5);
  border-bottom: 1px solid var(--rule);
}

.page-guide main {
  counter-reset: channel-index;
}

.page-guide .channel::before {
  counter-increment: channel-index;
  content: counter(channel-index, decimal-leading-zero);
  position: absolute;
  left: 0.7rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-dim);
  font-size: var(--fs-h3);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.page-guide .channel::after {
  content: "";
  position: absolute;
  left: 2.25rem;
  top: 17%;
  bottom: 17%;
  width: 1px;
  background: var(--rule-bright);
}

.page-guide .channel:first-of-type {
  box-shadow: inset 3px 0 rgba(0, 255, 95, 0.38);
}

.page-guide .channel:first-of-type::before {
  color: var(--rain);
  text-shadow: 0 0 0.75rem rgba(0, 255, 95, 0.28);
}

.page-guide .channel:first-of-type::after {
  background: var(--rain);
  box-shadow: 0 0 0.65rem rgba(0, 255, 95, 0.24);
}

.channel__body {
  position: relative;
  display: grid;
  gap: var(--space-2);
  justify-items: start;
  padding-right: 3rem;
}

/* A tiny level meter makes active and dormant stations readable before the
   text is parsed. The first channel is live; the others remain faint. */
.channel__body::after {
  content: "";
  position: absolute;
  right: 0;
  top: 50%;
  width: 0.45rem;
  height: 3.4rem;
  transform: translateY(-50%);
  background: repeating-linear-gradient(
    to bottom,
    var(--ink-faint) 0 0.22rem,
    transparent 0.22rem 0.45rem
  );
  opacity: 0.65;
}

.page-guide .channel:first-of-type .channel__body::after {
  background: repeating-linear-gradient(
    to bottom,
    var(--rain) 0 0.22rem,
    transparent 0.22rem 0.45rem
  );
  opacity: 0.9;
}

.channel__no {
  color: var(--ink-dim);
}

/* Holds the TV and the remote together so the remote can be placed against the
   set. The stage is full width; the TV takes 80% of it and the remote occupies
   the right-hand 34%, which means they overlap by ~14% and the remote reads as
   sitting in front of the screen, aimed at it. Nothing sticks out past the
   stage, so there is no horizontal overflow at any width. */
.channel__stage {
  position: relative;
}

/* The set, the gap and the remote add up to the full width of the stage:
     60% television + 6% gap + 34% remote = 100%
   Because both images are cropped to their own content, those percentages are
   literal - 6% of the stage really is 6% of clear space between the cabinet and
   the remote. The same three numbers apply at every screen size, so the spacing
   never changes proportion as the page resizes. */
.channel__stage .tv {
  width: 60%;
}

/* Applied to an <a> on Channel 1 (it goes somewhere) and to a bare <img> on
   Channels 2 and 3 (they do not, yet). Identical either way, so the three rows
   look the same. */
.channel__remote {
  position: absolute;
  z-index: 3;
  right: 0;
  top: 26%;
  width: 34%;
  height: auto;
  display: block;
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.75));
}

/* Only the one that is actually a link reacts to being pointed at. */
a.channel__remote {
  transition: transform 160ms ease-out, filter 160ms linear;
}

a.channel__remote:hover,
a.channel__remote:focus-visible {
  transform: translateX(-2.5%);
  filter:
    drop-shadow(0 8px 20px rgba(0, 255, 95, 0.4))
    brightness(1.08);
}

@media (max-width: 52rem) {
  /* Single column. The 60/6/34 split is deliberately NOT overridden here - that
     is what keeps the gap looking the same on a phone as on a desktop. Only the
     vertical aim shifts slightly, because the stage is wider relative to the
     text beside it. */
  .channel__remote {
    top: 30%;
  }
}

/* Every channel now carries the same single line of copy at the same size
   (.t-micro), so there is no separate call-to-action style. The remote beside
   Channel 1 is what takes you to the film. */

@media (max-width: 52rem) {
  .channel {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-3);
    padding-left: 3.4rem;
  }

  .page-guide .channel::before {
    left: 0.35rem;
    top: var(--space-5);
    transform: none;
    font-size: var(--fs-lead);
  }

  .page-guide .channel::after {
    left: 1.35rem;
    top: calc(var(--space-5) + 2.6rem);
    bottom: var(--space-5);
  }

  .channel__body {
    padding-right: 2.25rem;
  }
}

@media (max-width: 34rem) {
  .guide__head {
    padding-bottom: var(--space-3);
  }

  .channel {
    padding-block: var(--space-4);
    padding-left: 2.8rem;
  }

  .page-guide .channel::before {
    top: var(--space-4);
  }

  .page-guide .channel::after {
    left: 1.05rem;
    top: calc(var(--space-4) + 2.2rem);
    bottom: var(--space-4);
  }
}

/* --------------------------------------------------------------------------
   11. Journal entry
   A channel page. The piece comes first, then the writing about it: media,
   title, date, body. Narrower than the rest of the site on purpose - this is
   the one place people are meant to READ, and a reading column wants to be
   about 70 characters wide, not the full 1180px shell.

   The whole thing is one <article>, so adding a second entry later is a matter
   of copying the block, not designing a new page.
   -------------------------------------------------------------------------- */

/* The journal: a stack of entries, newest at the top. New ones are added ABOVE
   the existing ones, so the page reads as a descending timeline. */
/* Starts at the left edge of the page. */
.journal {
  position: relative;
  width: min(100%, 700px);
  display: grid;
  gap: var(--space-3);
  padding-left: 4.8rem;
  counter-reset: entry-index;
}

.journal::before {
  content: "";
  position: absolute;
  left: 1.55rem;
  top: 1.25rem;
  bottom: 1.25rem;
  width: 1px;
  background: var(--rule-bright);
}

/* Each entry is its own block: a bordered card rather than a run of text with
   a rule between. Stacked down the page they read as separate posts, which is
   what they are. Still deliberately compact - entries are meant to accumulate,
   and several have to read as a list rather than as pages stapled together. */
.entry {
  position: relative;
  counter-increment: entry-index;
  display: grid;
  gap: 0.85rem;
  padding: clamp(1rem, 2.5vw, 1.4rem);
  border: 1px solid var(--rule-bright);
  background: rgba(255, 255, 255, 0.012);
  box-shadow: inset 2px 0 rgba(0, 255, 95, 0.22);
}

.entry::before {
  content: counter(entry-index, decimal-leading-zero);
  position: absolute;
  right: calc(100% + 1.25rem);
  top: 1rem;
  min-width: 2.6rem;
  padding: 0.25rem 0.35rem;
  border: 1px solid var(--rule-bright);
  color: var(--ink-dim);
  background: var(--bg);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Capped rather than full width. The video was by far the tallest thing in a
   post, and shrinking it is most of what makes several entries fit on a screen
   without changing anything about how they look. */
.entry__media {
  width: min(100%, 430px);
  border: 1px solid var(--rule-bright);
  background: #050505;
  box-shadow: 0 0.7rem 1.6rem rgba(0, 0, 0, 0.45);
}

.entry__media video {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

/* Logbook fields: a small label in the left column, the value in the right.
   Baseline alignment is what holds it together - the labels sit on the same
   line as the first line of their value, whatever size that value is. */
/* Labels sit ABOVE their values, not beside them.
   Side-by-side was tried and looked wrong: the label column has to be as wide
   as the longest label, so "DATE / LOCATION:" pushed every value a third of
   the way across the entry and left the short labels - TITLE:, USER: - trailing
   a big ragged gutter. Stacking removes the problem outright, survives any
   label length you invent later, and needs no separate mobile layout. */
.entry__head {
  display: grid;
  gap: 0.7rem;
}

.entry__head > .entry__row + .entry__row {
  padding-top: 0.7rem;
  border-top: 1px solid var(--rule);
}

.entry__row {
  display: grid;
  gap: 0.1rem;
}

.entry__label {
  font-size: var(--fs-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-dim);
  white-space: nowrap;
}

/* The colon lives in CSS so a new field never has to remember to include one,
   and so it can never end up inconsistent between rows. The letter-spacing
   above puts a small gap in front of it, which is the look asked for. */
.entry__label::after {
  content: ":";
}

.entry__value {
  font-size: var(--fs-body);
  color: var(--ink);
  min-width: 0;
}

/* A field that is withheld rather than empty.
   Dimmed and loosened so it reads as a deliberate redaction - something being
   kept from you - instead of a value nobody got round to typing. */
.entry__value--cipher {
  color: var(--ink-dim);
  letter-spacing: 0.08em;
  /* The blocks are wide; on a narrow screen the line may need to break. */
  overflow-wrap: anywhere;
}

/* The title is the one field that outranks the others visually - but only just,
   since entries stack and a big heading every few hundred pixels gets loud. */
.entry__value--title {
  font-size: var(--fs-lead);
  line-height: 1;
}

/* The written part, in a box - a field you type into rather than loose text on
   the page. One word today, paragraphs later; the grid gap means multiple <p>
   elements space themselves without further work. */
.entry__body {
  display: grid;
  gap: var(--space-2);
  align-content: start;
  /* Enough to still read as a field you write into, not a line of text. */
  min-height: 3rem;
  padding: 0.7rem var(--space-2);
  border: 1px solid var(--rule-bright);
  background: var(--panel);
}

/* The quality toggle under a player. Same hairline-and-dim-label language as
   the fields around it, so it reads as part of the post rather than browser
   furniture bolted on. */
.quality {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.quality__label {
  font-size: var(--fs-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.quality__opts {
  display: flex;
  gap: 0.35rem;
}

.quality__opt {
  font-family: inherit;
  font-size: var(--fs-micro);
  letter-spacing: 0.12em;
  color: var(--ink-dim);
  background: transparent;
  border: 1px solid var(--ink-faint);
  padding: 0.25rem 0.6rem;
  cursor: pointer;
  transition: color 120ms linear, border-color 120ms linear, background-color 120ms linear;
}

.quality__opt:hover,
.quality__opt:focus-visible {
  color: var(--rain);
  border-color: var(--rain);
}

.quality__opt[aria-checked="true"] {
  color: var(--bg);
  background: var(--ink);
  border-color: var(--ink);
}

/* Signed off at the end. This one row runs inline rather than stacked - it is
   two or three characters, and stacking a label over a single letter against
   the right edge reads as a mistake rather than a signature. */
.entry__row--sign {
  grid-auto-flow: column;
  justify-content: end;
  align-items: baseline;
  gap: 0.5rem;
}

/* Sits under the journal column, sharing its left edge. */
.backlink {
  width: min(100%, 700px);
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  padding-left: 4.8rem;
  border-top: 1px solid var(--rule);
  text-align: left;
}

@media (max-width: 44rem) {
  .journal {
    padding-left: 0;
  }

  .journal::before,
  .entry::before {
    display: none;
  }

  .backlink {
    padding-left: 0;
    text-align: center;
  }
}

/* --------------------------------------------------------------------------
   12. Support
   The runner, and the row of links under it.
   -------------------------------------------------------------------------- */

.runner {
  width: min(100%, 960px);
  margin-inline: auto;
  display: grid;
  gap: var(--space-2);
}

.runner__frame {
  position: relative;
  border: 1px solid var(--rule-bright);
  background: var(--panel);
  padding: clamp(0.45rem, 1vw, 0.75rem);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.018),
    0 1rem 2.4rem rgba(0, 0, 0, 0.45);
}

.runner__frame::before,
.runner__frame::after {
  content: "";
  position: absolute;
  z-index: 2;
  top: -1px;
  width: 2.5rem;
  height: 2px;
  pointer-events: none;
}

.runner__frame::before {
  left: -1px;
  background: var(--rain);
}

.runner__frame::after {
  right: -1px;
  background: var(--magenta);
}

/* The canvas is 600x160 internally and stretched to fit. Nearest-neighbour
   scaling is the whole point - it keeps the pixels square and chunky instead
   of smoothing them into mush. */
.runner__canvas {
  display: block;
  width: 100%;
  aspect-ratio: 600 / 160;
  image-rendering: pixelated;
  touch-action: manipulation;
  cursor: pointer;
}

.runner__canvas:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.runner__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  padding-top: 0.65rem;
  border-top: 1px solid var(--rule);
}

.runner__scores {
  display: flex;
  gap: var(--space-3);
}

.runner__scores span {
  color: var(--ink);
}

/* Shown only where there is no keyboard to press space with. */
.runner__jump {
  display: block;
  font-family: inherit;
  font-size: var(--fs-nav);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--ink-faint);
  padding: 0.4rem 1.2rem;
  cursor: pointer;
  transition: color 120ms linear, border-color 120ms linear, box-shadow 120ms linear;
}

.runner__jump:hover,
.runner__jump:focus-visible {
  border-color: var(--rain);
  color: var(--rain);
  box-shadow: 0 0 0.65rem rgba(0, 255, 95, 0.18);
}

/* --- the ask, under the game --------------------------------------------- */

.help {
  width: min(100%, 960px);
  margin-inline: auto;
  margin-top: var(--space-5);
  display: grid;
  gap: clamp(1.5rem, 3vw, var(--space-3));
}

.help__title {
  position: relative;
  padding-block: var(--space-2);
  text-align: center;
  text-wrap: balance;
}

.help__title::before,
.help__title::after {
  content: "";
  position: absolute;
  top: 50%;
  width: min(12vw, 7rem);
  height: 1px;
  background: var(--rule-bright);
}

.help__title::before {
  left: 0;
}

.help__title::after {
  right: 0;
}

/* Three columns side by side, each with its label above its contents. Built as
   a list rather than a <table> - it looks like one, but it is not tabular data,
   and a real <table> would have screen readers announcing rows and columns that
   carry no meaning. */
.help__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border: 1px solid var(--rule-bright);
}

.help__col {
  position: relative;
  display: grid;
  justify-items: center;
  align-content: start;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-2);
  text-align: center;
  background: rgba(255, 255, 255, 0.008);
}

/* Dividers run between the columns, so the outer border is never doubled. */
.help__col + .help__col {
  border-left: 1px solid var(--rule);
}

/* Same cream as the heading above them, not the dimmed grey used for labels
   elsewhere - these three are the point of the section, not captions. */
.help__label {
  font-size: var(--fs-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
}

/* The glitching studio head at the end of the sentence. Sized off the text so
   it scales with the heading; the sprite is 9x14, so the width follows. */
.help__enemy {
  display: inline-block;
  height: 1.5em;
  width: calc(1.5em * 9 / 14);
  margin-left: 0.4em;
  vertical-align: -0.32em;
  image-rendering: pixelated;
}

.help__cell {
  display: grid;
  justify-content: center;
  justify-items: center;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  width: 100%;
}

.help__cell::before {
  content: "";
  width: min(100%, 9rem);
  height: 1px;
  margin-bottom: 0.25rem;
  background: var(--rain);
  box-shadow:
    -0.75rem -2px var(--rain),
    0.9rem 1px var(--rain),
    2rem -1px var(--rain);
  opacity: 0.42;
}

.help__pending {
  font-size: var(--fs-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--rain) 58%, var(--ink-faint));
}

.help__fighter {
  display: grid;
  justify-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--rule);
}

.help__fighter-callout {
  position: relative;
  margin: 0;
  padding: 0.35rem 0.9rem 0.45rem;
  border: 1px solid var(--ink);
  color: var(--ink);
  background: var(--bg);
  font-size: var(--fs-small);
  letter-spacing: 0.08em;
  line-height: 1;
  box-shadow:
    0 0 0.45rem rgba(53, 214, 255, 0.34),
    0 0 0.9rem rgba(255, 48, 187, 0.18);
}

.help__fighter-callout::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -0.32rem;
  width: 0.55rem;
  height: 0.55rem;
  border-right: 1px solid var(--ink);
  border-bottom: 1px solid var(--ink);
  background: var(--bg);
  transform: translateX(-50%) rotate(45deg);
}

.help__fighter-sprite {
  display: block;
  width: min(62vw, 15rem);
  height: auto;
  image-rendering: pixelated;
  filter: drop-shadow(0 0.7rem 1rem rgba(0, 0, 0, 0.75));
}

/* Three columns stop being readable well before the phone breakpoint - the
   labels alone need the room - so they stack earlier than most of the site. */
@media (max-width: 44rem) {
  .help__grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .help__col + .help__col {
    border-left: 0;
    border-top: 1px solid var(--rule);
  }

  .help__title::before,
  .help__title::after {
    display: none;
  }
}

/* --- link buttons, used inside the rows ---------------------------------- */

.links__item {
  display: inline-block;
  padding: 0.45rem 1.2rem;
  border: 1px solid var(--ink-faint);
  font-size: var(--fs-nav);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  transition: color 120ms linear, border-color 120ms linear, transform 120ms ease-out;
}

.links__item:hover,
.links__item:focus-visible {
  color: var(--rain);
  border-color: var(--rain);
  transform: translateY(-2px);
}

/* --------------------------------------------------------------------------
   13. About
   Name, one question, a short description. Deliberately little else.
   -------------------------------------------------------------------------- */

.about {
  /* Wide enough that each line of the question sits on one line at this size,
     but no wider - every pixel added here is a pixel taken off the margins the
     neon needs. 760 is the balance point. */
  width: min(100%, 760px);
  margin-inline: auto;
  display: grid;
  justify-items: center;
  gap: 0;
  text-align: center;
}

/* Each part of the About page earns its own scroll moment: the name first,
   then the question, then the summary. */
.about__stage {
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  justify-items: center;
  align-items: center;
  width: 100%;
  min-height: calc(100svh - 9rem);
}

.about__stage::before {
  position: absolute;
  left: -5.5rem;
  top: 50%;
  transform: translateY(-50%);
  padding-bottom: 0.45rem;
  border-bottom: 1px solid var(--rule-bright);
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  letter-spacing: 0.12em;
  line-height: 1;
}

.about__stage--name::before {
  content: "01 / 03";
}

.about__stage--quote::before {
  content: "02 / 03";
}

.about__stage--summary::before {
  content: "03 / 03";
}

.about__logo {
  position: relative;
  margin: 0;
  line-height: 0;
}

.about__logo::before,
.about__logo::after {
  content: "";
  position: absolute;
  z-index: 3;
  left: 50%;
  width: 118%;
  height: 1px;
  transform: translateX(-50%);
  pointer-events: none;
}

.about__logo::before {
  top: 28%;
  background: var(--cyan);
  box-shadow: 1.1rem 3px var(--magenta);
  opacity: 0.35;
}

.about__logo::after {
  bottom: 22%;
  background: var(--magenta);
  box-shadow: -1.4rem -2px var(--cyan);
  opacity: 0.28;
}

.about__logo img {
  display: block;
  width: min(74vw, 25rem);
  height: auto;
  filter:
    drop-shadow(0 0 0.2rem rgba(244, 248, 255, 0.95))
    drop-shadow(0 0 0.8rem rgba(53, 214, 255, 0.7))
    drop-shadow(0 0 1.7rem rgba(255, 48, 187, 0.42));
}

/* The outer logo frame and its existing inner grid form the two squares of a
 * tesseract. These four quiet perspective links make the otherwise empty
 * margin read as a glowing wireframe rather than padding. */
.about__tesseract {
  position: absolute;
  z-index: 1;
  inset: 0;
  pointer-events: none;
}

.about__tesseract-edge {
  position: absolute;
  width: 14.5%;
  height: 1px;
  background: currentColor;
  box-shadow:
    0 0 0.25rem currentColor,
    0 0 0.7rem currentColor;
  opacity: 0.84;
  transform-origin: left center;
}

.about__tesseract-edge--tl,
.about__tesseract-edge--br {
  color: #35d6ff;
}

.about__tesseract-edge--tr,
.about__tesseract-edge--bl {
  color: #ff30bb;
}

.about__tesseract-edge--tl {
  left: 0;
  top: 0;
  transform: rotate(45deg);
}

.about__tesseract-edge--tr {
  right: 0;
  top: 0;
  transform-origin: right center;
  transform: rotate(-45deg);
}

.about__tesseract-edge--bl {
  bottom: 0;
  left: 0;
  transform: rotate(-45deg);
}

.about__tesseract-edge--br {
  right: 0;
  bottom: 0;
  transform-origin: right center;
  transform: rotate(45deg);
}

.about__stage--quote {
  min-height: 88svh;
}

.about__stage--summary {
  grid-template-rows: 1fr;
  min-height: 72svh;
}

/* A chevron, drawn from two borders on a rotated square rather than typed as a
   character - no glyph to be missing from the font, at any size. */
.about__cue {
  width: 0.85rem;
  height: 0.85rem;
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(45deg);
  animation: about-cue 2.4s ease-in-out infinite;
}

@keyframes about-cue {
  0%,
  100% {
    transform: rotate(45deg) translate(0, 0);
    opacity: 0.55;
  }
  50% {
    transform: rotate(45deg) translate(0.22rem, 0.22rem);
    opacity: 1;
  }
}

/* Revealed on scroll - see assets/js/reveal.js. The [hidden] state is only
   ever applied BY that script, so the content is visible without it. */
[data-reveal="hidden"] {
  opacity: 0;
  transform: translateY(1.75rem);
}

[data-reveal="shown"] {
  opacity: 1;
  transform: none;
  transition: opacity 700ms ease-out, transform 700ms ease-out;
}

/* The question. Rules above and below rather than quotation marks - the marks
   would fight the pixel font, the rules frame it cleanly when centred. */
.about__quote {
  margin: 0;
  display: grid;
  /* Between the lead-in and the question itself. At 0.4rem they read as one
     wrapped sentence rather than a set-up and a punchline. */
  gap: 1rem;
  padding-block: clamp(2.25rem, 4vw, 3.25rem);
  border-top: 1px solid var(--ink-faint);
  border-bottom: 1px solid var(--ink-faint);
  /* Clearly below the name. At --fs-h3 the two read as competing headings;
     --fs-lead makes the quote subtext, which is what it is. */
  font-size: var(--fs-lead);
  line-height: 1.25;
  /* Keeps the wrap even if it ever does have to break. */
  text-wrap: balance;
}

/* The lead-in sits a step back so the question itself carries the weight.
 *
 * It was --fs-body, which is what the description paragraphs use - so two
 * different roles on the same page rendered at exactly the same size and the
 * hierarchy collapsed. Dropping it to --fs-micro gives the page four clearly
 * separate steps: name, question, description, lead-in. The letter-spacing
 * makes the smaller size read as deliberate rather than as small text. */
.about__quote p:first-child {
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  letter-spacing: 0.14em;
}

/* The word carries the emphasis, so it is set in caps. Done with
   text-transform rather than typed in capitals for the same reason as the
   question mark below: the underlying text stays ordinary lower case, which is
   what search snippets and copy-and-paste get, and screen readers say the word
   instead of spelling it out letter by letter. */
.about__word {
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--magenta);
  text-shadow: 0 0 0.8rem rgba(255, 48, 187, 0.28);
}

/* The gap before the question mark is set here rather than typed as a space in
   the HTML. The text stays "uncultured?" everywhere it is read AS text - search
   snippets, screen readers, copy and paste - while looking spaced out on the
   page, and the mark can never wrap onto a line of its own. */
.about__mark {
  margin-left: 0.4em;
  color: var(--rain);
}

.about__body {
  display: grid;
  /* One step up from --space-2: three short centred paragraphs at 1rem apart
     read as a single block of text rather than three separate statements. */
  gap: var(--space-3);
  max-width: var(--measure);
  padding-block: clamp(2rem, 5vw, 3.5rem);
  border-top: 1px solid var(--rule-bright);
  border-bottom: 1px solid var(--rule-bright);
}

/* Centred text with a ragged last line looks like a mistake rather than a
   choice, so the browser is asked to even the lines out. */
.about__body p {
  text-wrap: balance;
}

/* The final About beat is one indistinct signal. The native 128x176 strip keeps
   its jacket movement, while two displaced copies tear the silhouette across
   different horizontal bands. */
.about__avatar-scene {
  display: grid;
  place-items: end center;
  width: 100%;
  min-height: clamp(9rem, 24vw, 11.5rem);
  margin-top: var(--space-2);
}

@media (max-width: 58rem) {
  .about__stage::before {
    left: 0;
    top: 1.5rem;
    transform: none;
  }
}

@media (max-width: 34rem) {
  .about__stage {
    min-height: calc(100svh - 6rem);
  }

  .about__stage--quote {
    min-height: 82svh;
  }

  .about__stage--summary {
    min-height: 82svh;
  }

  .about__stage::before {
    top: 0.65rem;
  }

  .about__quote {
    padding-inline: 0.35rem;
  }

  .about__body {
    padding-inline: 0.25rem;
  }
}

.about__avatar {
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: clamp(6.5rem, 20vw, 8rem);
  aspect-ratio: 128 / 176;
  background: url("/assets/img/about-avatar-terminal-strip.png?v=1") 0 0 / 300% 100%
    no-repeat;
  image-rendering: pixelated;
  opacity: 0.76;
  filter:
    grayscale(1)
    brightness(0.52)
    blur(4.5px)
    drop-shadow(0 0 0.65rem rgba(215, 215, 215, 0.28));
  will-change: opacity, transform;
  animation:
    about-avatar-idle 1.4s steps(1, end) infinite,
    about-avatar-phase 1.85s steps(1, end) infinite;
}

.about__avatar::before,
.about__avatar::after {
  content: "";
  position: absolute;
  inset: 0;
  background: inherit;
  pointer-events: none;
  opacity: 0;
  filter: grayscale(1) blur(0.8px) brightness(1.9) contrast(1.2);
  mix-blend-mode: screen;
  animation:
    about-avatar-idle 1.4s steps(1, end) infinite,
    about-avatar-glitch 1.75s steps(1, end) infinite;
}

.about__avatar::before {
  clip-path: inset(0);
}

.about__avatar::after {
  clip-path: inset(36% 0 10% 0);
  animation-delay: 0s, -0.22s;
}

@keyframes about-avatar-idle {
  0%,
  24% {
    background-position: 0 0;
  }
  25%,
  49% {
    background-position: 50% 0;
  }
  50%,
  74% {
    background-position: 100% 0;
  }
  75%,
  99% {
    background-position: 50% 0;
  }
  100% {
    background-position: 0 0;
  }
}

@keyframes about-avatar-phase {
  0%,
  57%,
  64%,
  78%,
  84%,
  100% {
    opacity: 0.76;
    transform: translate(0);
  }
  58%,
  80% {
    opacity: 0.42;
    transform: translateX(-0.08rem);
  }
  59%,
  82% {
    opacity: 0.9;
    transform: translateX(0.1rem);
  }
  61%,
  83% {
    opacity: 0.54;
    transform: translateX(-0.04rem);
  }
  63%,
  79%,
  81% {
    opacity: 0.68;
    transform: translateX(0.04rem);
  }
}

@keyframes about-avatar-glitch {
  0%,
  48%,
  55%,
  72%,
  78%,
  90%,
  96%,
  100% {
    opacity: 0;
    transform: translate(0);
  }
  49%,
  73%,
  91% {
    opacity: 0.85;
    transform: translate(-0.55rem, -0.05rem) scaleX(1.04);
  }
  50%,
  75%,
  93% {
    opacity: 0.48;
    transform: translate(0.42rem, 0.04rem) scaleX(0.97);
  }
  51%,
  74%,
  92% {
    opacity: 0.95;
    transform: translate(-0.3rem, 0) scaleX(1.02);
  }
  52%,
  76%,
  94% {
    opacity: 0.34;
    transform: translate(0.2rem, -0.03rem);
  }
  54%,
  77%,
  95% {
    opacity: 0.72;
    transform: translate(0.5rem, 0.02rem) scaleX(1.03);
  }
}

/* Kept for any page held back to just its background. */
.blank {
  min-height: 62svh;
}

/* --------------------------------------------------------------------------
   Anomaly (contact)
   The whole page is one object and one line of text, centred in the viewport.
   -------------------------------------------------------------------------- */

.anomaly {
  position: relative;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--space-3);
  min-height: calc(100svh - 3.4rem - 5.2rem);
  padding: clamp(1.5rem, 4vw, 3rem);
  border: 1px solid var(--rule-bright);
}

.page-contact .page {
  padding-block: clamp(1.5rem, 3vw, 2.5rem);
}

.anomaly::before,
.anomaly::after {
  content: "";
  position: absolute;
  width: 2.25rem;
  height: 2.25rem;
  top: 0.75rem;
  pointer-events: none;
}

.anomaly::before {
  left: 0.75rem;
  border-left: 1px solid var(--rain);
  border-top: 1px solid var(--rain);
}

.anomaly::after {
  right: 0.75rem;
  border-right: 1px solid var(--magenta);
  border-top: 1px solid var(--magenta);
}

.anomaly__canvas {
  display: block;
  width: min(100%, 640px);
  aspect-ratio: 3 / 2;
  /* Drawn at 180x120 and blown up here. Without this the browser would smooth
     the upscale and undo the entire point of rendering it low-resolution. */
  image-rendering: pixelated;
  filter: drop-shadow(0 0 1.2rem rgba(0, 255, 95, 0.15));
}

/* A small destination held at the edge of the anomaly rather than another
   menu item. It is still a real link with a generous hit area, so the mystery
   never comes at the expense of keyboard or touch access. */
.anomaly__planet {
  position: absolute;
  z-index: 3;
  top: 50%;
  right: clamp(1.1rem, 4vw, 3.75rem);
  display: grid;
  justify-items: center;
  gap: 0.65rem;
  width: clamp(6.5rem, 11vw, 8.5rem);
  padding: 0.7rem 0.35rem;
  color: var(--ink-dim);
  transform: translateY(-50%);
  transition: color 140ms linear, transform 140ms linear;
}

.anomaly__planet-orbit {
  position: relative;
  display: grid;
  place-items: center;
  width: clamp(4rem, 6vw, 5rem);
  aspect-ratio: 1;
  filter: drop-shadow(0 0 0.7rem rgba(53, 214, 255, 0.2));
  animation: anomaly-planet-drift 3.8s steps(4, end) infinite;
}

.anomaly__planet-orbit::before {
  content: "";
  position: absolute;
  z-index: -1;
  width: 118%;
  height: 34%;
  border: 2px solid var(--cyan);
  border-left-color: var(--rain);
  border-right-color: var(--magenta);
  border-radius: 50%;
  transform: rotate(-18deg);
  opacity: 0.72;
}

.anomaly__planet-core {
  position: relative;
  width: 64%;
  aspect-ratio: 1;
  clip-path: polygon(
    25% 0,
    75% 0,
    100% 25%,
    100% 75%,
    75% 100%,
    25% 100%,
    0 75%,
    0 25%
  );
  border: 2px solid var(--ink);
  background:
    linear-gradient(90deg, transparent 0 68%, rgba(0, 0, 0, 0.62) 68% 100%),
    linear-gradient(
      180deg,
      var(--cyan) 0 17%,
      #12333b 17% 36%,
      var(--rain) 36% 52%,
      #0a2415 52% 72%,
      var(--magenta) 72% 82%,
      #161219 82% 100%
    );
  box-shadow: inset 0 0 0 3px #080808;
}

.anomaly__planet-core::after {
  content: "";
  position: absolute;
  inset: 18% 22% auto auto;
  width: 18%;
  aspect-ratio: 1;
  background: var(--rain-head);
  box-shadow: -0.45rem 0.75rem 0 #08130c;
}

.anomaly__planet-label {
  font-size: var(--fs-micro);
  line-height: 1;
  letter-spacing: 0.14em;
  text-align: center;
  text-transform: uppercase;
  border-bottom: 1px solid transparent;
}

.anomaly__planet:hover,
.anomaly__planet:focus-visible {
  color: var(--rain);
  transform: translateY(-50%) scale(1.04);
}

.anomaly__planet:hover .anomaly__planet-label,
.anomaly__planet:focus-visible .anomaly__planet-label {
  border-bottom-color: currentColor;
  text-shadow: 0 0 0.8rem rgba(0, 255, 95, 0.45);
}

@keyframes anomaly-planet-drift {
  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-0.35rem);
  }
}

.anomaly__label {
  font-size: var(--fs-h3);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
  color: var(--ink);
  /* Phosphor green, matching the readout above it - this was left cyan from an
     earlier version of the anomaly and clashed once it went green. */
  text-shadow: 0 0 20px rgba(0, 255, 95, 0.3);
  padding: 0.55rem clamp(0.5rem, 4vw, 2rem);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

:root[data-contrast="high"] .anomaly__label {
  text-shadow: none;
}

/* --------------------------------------------------------------------------
   Business signal
   A deliberately spare destination: one message, centred in an otherwise
   black transmission. The regular navigation remains available as the exit.
   -------------------------------------------------------------------------- */

.page-business .page {
  padding-block: clamp(2rem, 5vw, 4rem);
}

.business-signal {
  display: grid;
  place-items: center;
  min-height: calc(100svh - 3.4rem - 5.2rem);
  text-align: center;
}

.business-signal__message {
  position: relative;
  display: grid;
  justify-items: center;
  gap: var(--space-3);
  width: min(100%, 34rem);
  padding: clamp(2rem, 6vw, 4rem) clamp(1rem, 5vw, 3rem);
}

.business-signal__message::before,
.business-signal__message::after {
  content: "";
  width: clamp(3rem, 9vw, 5.5rem);
  height: 1px;
  background: var(--rule-bright);
}

.business-signal__message::before {
  box-shadow: -0.65rem 0 var(--cyan), 0.65rem 0 var(--rain);
}

.business-signal__message::after {
  box-shadow: -0.65rem 0 var(--magenta), 0.65rem 0 var(--cyan);
}

.business-signal__copy {
  max-width: 30ch;
  font-size: var(--fs-lead);
  line-height: 1.3;
  text-wrap: balance;
}

.business-signal__sign {
  color: var(--rain);
  font-size: var(--fs-body);
  letter-spacing: 0.14em;
  text-shadow: 0 0 0.8rem rgba(0, 255, 95, 0.28);
}

/* Present for screen readers and crawlers, invisible on screen, so the page is
   not literally an empty document. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   14. Footer
   -------------------------------------------------------------------------- */

.footer {
  position: relative;
  z-index: 2;
  border-top: 1px solid var(--rule);
  padding-block: var(--space-3);
  text-align: center;
  background: var(--bg);
}

.footer::before {
  content: "";
  display: block;
  width: 2.5rem;
  height: 1px;
  margin: calc(var(--space-3) * -1) auto var(--space-2);
  background: var(--rain);
  box-shadow:
    -0.55rem 0 var(--cyan),
    0.55rem 0 var(--magenta);
  opacity: 0.48;
}

/* The error state uses the same archive geometry without borrowing the home
   page's tall first-act pacing. */
.page-error .page {
  padding-block: clamp(2rem, 4vw, 3rem);
}

.page-error .hero {
  position: relative;
  gap: clamp(1.25rem, 3vw, 2.25rem);
}

.page-error .hero::before,
.page-error .hero::after {
  content: "";
  position: absolute;
  top: 58%;
  width: min(20vw, 13rem);
  height: 1px;
  background: var(--rule-bright);
}

.page-error .hero::before {
  left: 0;
  box-shadow: 1rem 0 var(--rain);
}

.page-error .hero::after {
  right: 0;
  box-shadow: -1rem 0 var(--magenta);
}

.page-error .hero__tv {
  z-index: 1;
  width: min(100%, 34rem);
}

.page-error .hero > .t-micro {
  color: var(--rain);
}

@media (max-width: 34rem) {
  .anomaly {
    min-height: calc(100svh - 3.4rem - 5rem);
    padding: 1rem 0.5rem;
  }

  .anomaly__planet {
    top: 3.25rem;
    right: 0.55rem;
    width: 5.5rem;
    padding: 0.45rem 0.2rem;
    transform: none;
  }

  .anomaly__planet-orbit {
    width: 3.45rem;
  }

  .anomaly__planet-label {
    font-size: 0.92rem;
    letter-spacing: 0.09em;
  }

  .anomaly__planet:hover,
  .anomaly__planet:focus-visible {
    transform: scale(1.04);
  }

  .anomaly__label {
    max-width: none;
    white-space: nowrap;
    font-size: 1.95rem;
    letter-spacing: 0.12em;
  }

  .page-error .hero::before,
  .page-error .hero::after {
    display: none;
  }

  .page-error .hero__tv {
    width: min(100%, 22rem);
  }

  .footer {
    padding-right: 4rem;
  }
}

/* --------------------------------------------------------------------------
   15. Accessibility panel
   The floating button and its settings drawer, built by assets/js/a11y.js.
   -------------------------------------------------------------------------- */

.a11y {
  position: fixed;
  right: clamp(0.75rem, 2vw, 1.5rem);
  bottom: clamp(0.75rem, 2vw, 1.5rem);
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
}

.a11y__toggle {
  width: 3.2rem;
  height: 3.2rem;
  display: grid;
  place-items: center;
  padding: 0;
  cursor: pointer;
  color: var(--ink);
  background: #060606;
  border: 1px solid var(--ink-dim);
  border-radius: 50%;
  font-family: inherit;
  box-shadow:
    0 0.55rem 1.4rem rgba(0, 0, 0, 0.72),
    0 0 0.65rem rgba(249, 244, 238, 0.08);
  transition: background-color 120ms linear, color 120ms linear, transform 120ms ease-out;
}

.a11y__toggle svg {
  width: 62%;
  height: 62%;
}

.a11y__toggle:hover,
.a11y__toggle:focus-visible {
  color: var(--bg);
  background: var(--rain);
  border-color: var(--rain);
  transform: scale(1.06);
}

.a11y__panel {
  width: min(19rem, calc(100vw - 2rem));
  max-height: min(30rem, calc(100svh - 8rem));
  overflow-y: auto;
  padding: var(--space-2);
  background: #0a0a0a;
  border: 1px solid var(--ink-faint);
  color: var(--ink);
  display: grid;
  gap: 0.7rem;
}

.a11y__panel[hidden] {
  display: none;
}

.a11y__title {
  color: var(--ink);
  margin-bottom: 0.2rem;
}

.a11y__row {
  display: block;
}

.a11y__label {
  font-size: var(--fs-nav);
  line-height: 1.2;
  text-align: left;
}

.a11y__group {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.35rem;
}

/* Shared look for every control in the panel. */
.a11y__opt,
.a11y__switch,
.a11y__reset {
  font-family: inherit;
  font-size: var(--fs-nav);
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--ink-faint);
  cursor: pointer;
  padding: 0.3rem 0.6rem;
  transition: border-color 120ms linear, color 120ms linear, background-color 120ms linear;
}

.a11y__opt:hover,
.a11y__switch:hover,
.a11y__reset:hover {
  border-color: var(--rain);
  color: var(--rain);
}

.a11y__opt {
  min-width: 3rem;
}

.a11y__opt[aria-checked="true"] {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
}

.a11y__switch {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

/* The on/off indicator: an empty box that fills in when the switch is on. */
.a11y__pip {
  flex: none;
  width: 2.6rem;
  height: 1.2rem;
  border: 1px solid var(--ink-faint);
  position: relative;
}

.a11y__pip::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: calc(50% - 3px);
  height: calc(100% - 4px);
  background: var(--ink-faint);
  transition: left 120ms ease-out, background-color 120ms linear;
}

.a11y__switch[aria-checked="true"] .a11y__pip {
  border-color: var(--rain);
}

.a11y__switch[aria-checked="true"] .a11y__pip::after {
  left: calc(50% + 1px);
  background: var(--rain);
}

.a11y__reset {
  justify-self: start;
}

.a11y__note {
  font-size: var(--fs-micro);
  color: var(--ink-dim);
  line-height: 1.3;
}

/* --------------------------------------------------------------------------
   16. What the accessibility settings actually do.
   a11y.js only writes these attributes onto <html>; every visual change lives
   here, so a setting can be re-styled without touching any JavaScript.
   -------------------------------------------------------------------------- */

/* Text size. Every size token is in rem, so moving the root size moves the
   whole scale together and the page stays in proportion. */
:root[data-text="lg"] {
  font-size: 118%;
}

:root[data-text="xl"] {
  font-size: 136%;
}

/* High contrast: pure white on pure black, hard rules, and the rain dialled
   right back so it stops competing with the text. */
:root[data-contrast="high"] {
  --ink: #ffffff;
  --ink-dim: #ffffff;
  --ink-faint: #cccccc;
  --rule: #ffffff;
  --rain: #ffffff;
  --rain-head: #ffffff;
  --rain-opacity: 0.12;
}

:root[data-contrast="high"] .rain-veil {
  background: rgba(0, 0, 0, 0.72);
}

:root[data-contrast="high"] .tv__static,
:root[data-contrast="high"] .tv__video {
  opacity: 0.35;
}

:root[data-contrast="high"] .t-micro,
:root[data-contrast="high"] .channel__no {
  color: #ffffff;
}

/* Motion off. The scripts stop their loops and pause the video themselves;
   this covers the CSS transitions and hover animations. */
:root[data-motion="off"] *,
:root[data-motion="off"] *::before,
:root[data-motion="off"] *::after {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}

:root[data-motion="off"] .channel__remote:hover,
:root[data-motion="off"] .channel__remote:focus-visible {
  transform: rotate(-8deg);
}

:root[data-motion="off"] .about__avatar {
  animation: none;
  background-position: 0 0;
}

:root[data-motion="off"] .about__avatar::before,
:root[data-motion="off"] .about__avatar::after {
  display: none;
}

/* Underlined links, for anyone who cannot pick links out by colour alone. */
:root[data-links="underline"] a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Looser tracking. VT323 is a pixel face and sets tight by default, which is
   exactly the situation this helps. */
:root[data-spacing="loose"] body {
  letter-spacing: 0.06em;
  word-spacing: 0.14em;
  line-height: 1.65;
}

:root[data-spacing="loose"] .t-h1,
:root[data-spacing="loose"] .t-h2,
:root[data-spacing="loose"] .t-h3 {
  line-height: 1.2;
}

/* --------------------------------------------------------------------------
   17. Reduced motion. The scripts also check this and render a single static
   frame instead of animating, so nothing disappears - it just stops moving.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .about__avatar {
    animation: none;
    background-position: 0 0;
  }

  .about__avatar::before,
  .about__avatar::after {
    display: none;
  }
}
