/* alien.hartle.tech
 *
 * Biomechanical-horror homage, built from primitives only: no film assets, no
 * studio marks, no third-party fonts or scripts. Every visual here is CSS and
 * every sound is synthesised at runtime — see CREDITS.md for why.
 *
 * The whole page must stay legible with animation disabled. Anything that
 * conveys meaning is static; motion is decoration, and the reduced-motion
 * block at the bottom removes it wholesale.
 */

:root {
  --void:   #05070a;
  --hull:   #0b0f10;
  --shell:  #121a1c;
  --bio:    #3fe86c;   /* acid green — the product colour */
  --bio-dim:#1d6b39;
  --plasma: #ffb000;   /* amber — warnings and heat */
  --blood:  #c1121f;
  --ice:    #53d8ff;
  --bone:   #d9e3e0;
  --ash:    #7d8c88;

  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --gut: clamp(1.25rem, 4vw, 3rem);
}

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

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--void);
  color: var(--bone);
  font-family: var(--sans);
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* ── Atmosphere ─────────────────────────────────────────────────────────────
   Two fixed overlays sit above everything and below nothing interactive.
   pointer-events:none is load-bearing: without it the whole page is dead. */

.scanlines, .vignette {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9000;
}

.scanlines {
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0) 0px,
    rgba(0, 0, 0, 0) 2px,
    rgba(0, 0, 0, 0.22) 3px,
    rgba(0, 0, 0, 0.22) 4px
  );
  mix-blend-mode: multiply;
  opacity: 0.55;
}

.vignette {
  background:
    radial-gradient(ellipse at 50% 0%,   rgba(63, 232, 108, 0.07), transparent 55%),
    radial-gradient(ellipse at 50% 100%, rgba(0, 0, 0, 0.85),      transparent 60%);
}

/* A slow, barely-there horizontal drift across the screen: the sense that
   something is refreshing, without a distracting sweep. */
.scanlines::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  height: 22vh;
  background: linear-gradient(to bottom, transparent, rgba(63, 232, 108, 0.05), transparent);
  animation: crawl 7s linear infinite;
}

@keyframes crawl {
  from { transform: translateY(-25vh); }
  to   { transform: translateY(125vh); }
}


/* ── Depth ──────────────────────────────────────────────────────────────────
   Small readouts scattered across the whole viewport rather than one centred
   panel. Each mote gets its own scale, blur and opacity, so the background
   reads as several displays at different distances instead of one flat plane
   parked behind the text. */

.motes { position: fixed; inset: 0; z-index: 0; pointer-events: none; }

.mote {
  position: absolute;
  font-family: var(--mono);
  font-size: 0.62rem;
  line-height: 1.6;
  letter-spacing: 0.08em;
  color: var(--bio);
  white-space: pre;
  opacity: 0;
  transition: opacity 2400ms ease;
  /* Smear rather than glow: these are the furthest things on screen and should
     read as light bleeding through a panel, not as legible labels. */
  text-shadow:
    0 0 8px rgba(63, 232, 108, 0.7),
    5px 0 10px rgba(63, 232, 108, 0.3),
    -3px 0 8px rgba(63, 232, 108, 0.2);
  border-left: 1px solid rgba(63, 232, 108, 0.18);
  padding-left: 0.5rem;
  animation:
    moteDrift var(--drift, 17s) ease-in-out infinite alternate,
    moteFlicker var(--flick, 5.5s) steps(3) infinite;
}

.mote.on { opacity: var(--o, 0.3); }

@keyframes moteDrift {
  0%   { translate: 0 0; }
  50%  { translate: 7px -5px; }
  100% { translate: -5px 4px; }
}

/* Panel flicker: not a smooth fade — LCD and phosphor both fail in steps. */
@keyframes moteFlicker {
  0%, 100% { filter: none; }
  47%      { filter: brightness(1.5); }
  49%      { filter: brightness(0.45); }
  51%      { filter: brightness(1.15); }
}
.mote.warn { color: var(--plasma); border-left-color: var(--plasma); text-shadow: 0 0 6px rgba(255,176,0,.6); }
.mote.crit { color: var(--blood); border-left-color: var(--blood); text-shadow: 0 0 8px rgba(193,18,31,.7); }

/* ── The passer-by ──────────────────────────────────────────────────────────
   A dark mass crosses in front of the display. It has no outline, no limbs and
   no face — the effect comes entirely from occlusion plus a rim of scattered
   light, which is how you actually perceive something moving through a dark
   room. Giving it a shape made it identifiable, and identifiable is not
   frightening.

   The body is near-opaque so it genuinely blocks the scenes behind it; the rim
   is a wide, soft green bloom on the leading edge. */

.passer {
  position: fixed;
  inset: -20vh -30vw;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  background:
    radial-gradient(ellipse 26% 62% at 30% 50%,
      rgba(63, 232, 108, 0.10) 0%,
      rgba(63, 232, 108, 0.04) 34%,
      transparent 62%),
    radial-gradient(ellipse 22% 56% at 34% 50%,
      rgba(2, 4, 3, 0.97) 0%,
      rgba(2, 4, 3, 0.9) 46%,
      rgba(2, 4, 3, 0.5) 72%,
      transparent 88%);
  filter: blur(16px);
}

/* Crossing: unhurried, right to left, close enough to blot out most of the
   frame as it goes. */
.passer.cross { animation: cross 17s cubic-bezier(.35, 0, .65, 1) forwards; }

@keyframes cross {
  0%   { opacity: 0;   transform: translateX(58vw) scaleX(1); }
  14%  { opacity: .85; transform: translateX(40vw) scaleX(1.04); }
  50%  { opacity: 1;   transform: translateX(2vw)  scaleX(1.1); }
  86%  { opacity: .8;  transform: translateX(-38vw) scaleX(1.04); }
  100% { opacity: 0;   transform: translateX(-58vw) scaleX(1); }
}

/* Looming: it does not cross, it comes toward the glass and withdraws. */
.passer.loom { animation: loom 13s ease-in-out forwards; }

@keyframes loom {
  0%   { opacity: 0;   transform: translateX(24vw) scale(.55); }
  30%  { opacity: .7;  transform: translateX(16vw) scale(.9); }
  55%  { opacity: .95; transform: translateX(6vw)  scale(1.5); }
  78%  { opacity: .6;  transform: translateX(2vw)  scale(1.15); }
  100% { opacity: 0;   transform: translateX(-6vw) scale(.8); }
}

.passer.flip { scale: -1 1; }

/* ── CRT faults ─────────────────────────────────────────────────────────────
   Old tubes and old signal chains do not fail gracefully. Three separate
   faults, fired independently, because one repeating "glitch" animation reads
   as an effect while three irregular ones read as a failing display. */

/* Horizontal tear: a band of the picture slips sideways. */
.glitch .scenes { animation: tear 220ms steps(4) 1; }

@keyframes tear {
  0%   { transform: perspective(70rem) rotateX(1.6deg) scale(1.055) translateX(0); }
  25%  { transform: perspective(70rem) rotateX(1.6deg) scale(1.055) translateX(-11px) skewX(1.6deg); }
  50%  { transform: perspective(70rem) rotateX(1.6deg) scale(1.058) translateX(9px)  skewX(-1.2deg); }
  75%  { transform: perspective(70rem) rotateX(1.6deg) scale(1.055) translateX(-4px); }
  100% { transform: perspective(70rem) rotateX(1.6deg) scale(1.055) translateX(0); }
}

/* Colour separation: the beams stop landing together. */
.rgbsplit .scenes { animation: rgb 260ms steps(2) 1; }

@keyframes rgb {
  0%, 100% { filter: blur(1.2px); }
  50% {
    filter: blur(1.2px)
      drop-shadow(3px 0 0 rgba(255, 30, 80, 0.55))
      drop-shadow(-3px 0 0 rgba(50, 160, 255, 0.5));
  }
}

/* Signal loss: the picture is simply not there for a moment. */
.dropout {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  background: #000;
}

.dropout.on { animation: dropout 320ms steps(5) 1; }

@keyframes dropout {
  0%, 100% { opacity: 0; }
  20% { opacity: 0.85; }
  40% { opacity: 0.1; }
  60% { opacity: 0.7; }
  80% { opacity: 0.05; }
}

/* Vertical roll: the whole frame loses hold and slips upward once. */
.roll .scenes, .roll .motes { animation: roll 700ms linear 1; }

@keyframes roll {
  0%   { translate: 0 0; }
  45%  { translate: 0 -38vh; }
  46%  { translate: 0 62vh; }
  100% { translate: 0 0; }
}

/* ── Boot sequence ──────────────────────────────────────────────────────────
   Shown once per tab. It is a curtain, not a gate: it dismisses itself, and
   any key or click skips it. Never trap someone behind an animation. */

#boot {
  position: fixed;
  inset: 0;
  z-index: 9500;
  background: var(--void);
  display: grid;
  align-content: center;
  padding: var(--gut);
  font-family: var(--mono);
  font-size: clamp(0.7rem, 1.6vw, 0.95rem);
  color: var(--bio);
  transition: opacity 700ms ease, visibility 700ms;
}

#boot.done { opacity: 0; visibility: hidden; }

#boot pre { margin: 0 auto; max-width: 62ch; white-space: pre-wrap; }

#boot .skip {
  margin: 2rem auto 0;
  color: var(--ash);
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.cursor::after {
  content: "\2588";
  animation: blink 1s steps(2) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

/* ── Layout ─────────────────────────────────────────────────────────────── */

main { position: relative; z-index: 1; }

section {
  max-width: 78rem;
  margin-inline: auto;
  padding: clamp(3.5rem, 9vw, 7rem) var(--gut);
}

h1, h2, h3 { font-weight: 700; line-height: 1.1; letter-spacing: -0.01em; }

.eyebrow {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--bio-dim);
  margin-bottom: 1rem;
}

.lede { color: var(--ash); max-width: 60ch; font-size: 1.05rem; }

/* ── Hero ───────────────────────────────────────────────────────────────── */

.hero {
  min-height: 100svh;
  display: grid;
  align-content: center;
  position: relative;
  text-align: center;
}

/* ── The scenes ─────────────────────────────────────────────────────────────
   The background is not one fixed panel. It is a set of themes — an incident
   log, a motion tracker, a deck schematic, life support, a crew roster, dock
   status — that fade in, run, and fade away, so the page never settles into a
   single static image.

   Everything here sits behind the content and is translucent and blurred by
   design: the centre of the screen belongs to the product, and this must read
   as something glowing in the background rather than as information anyone is
   expected to parse. */

.scenes {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  font-family: var(--mono);
  color: var(--bio);
  /* Blur: this is depth-of-field, not a second reading pane. The opacity is
     the dial that matters — high enough to be unmistakably there, low enough
     that nobody tries to read it instead of the install command. */
  /* Deliberately hazy. Crisp text back here competes with the foreground and
     reads as a second content column; smeared into the panel it reads as a
     display someone is not looking directly at. */
  filter: blur(2.6px) saturate(0.85);
  opacity: 0.3;

  /* ── Lens ────────────────────────────────────────────────────────────────
     Without this the scenes read as flat artwork parked behind the content.
     Three cues together sell a curved tube being looked at through glass:

       perspective + rotateX   the plane is tilted away from the viewer, so it
                               is a surface in space rather than a backdrop
       scale                   overscan, so the mask can eat the edges without
                               revealing where the layer stops
       radial mask             falls off toward the corners the way a lens
                               vignettes, which is the actual barrel cue —
                               curvature is read from the corners, not the middle

     A true barrel warp needs an SVG feDisplacementMap and a hand-built
     displacement map; it costs a full-screen filter pass every frame for a
     difference nobody consciously registers under 1.2px of blur. */
  transform: perspective(70rem) rotateX(1.6deg) scale(1.055);
  transform-origin: 50% 45%;
  /* A permanent slow warp. A background that is perfectly static reads as an
     image; one that never quite settles reads as a signal. */
  animation: drift 19s ease-in-out infinite alternate;
  mask:
    radial-gradient(ellipse 74% 72% at 50% 50%, #000 52%, rgba(0,0,0,.45) 82%, transparent 100%);
}

@keyframes drift {
  0%   { transform: perspective(70rem) rotateX(1.6deg) scale(1.055) translate(0, 0) skewX(0deg); }
  33%  { transform: perspective(70rem) rotateX(1.9deg) scale(1.062) translate(-6px, 3px) skewX(.25deg); }
  66%  { transform: perspective(70rem) rotateX(1.3deg) scale(1.05)  translate(5px, -3px) skewX(-.2deg); }
  100% { transform: perspective(70rem) rotateX(1.7deg) scale(1.058) translate(-2px, 2px) skewX(.1deg); }
}

/* The glass in front of the tube: corner darkening and a bright edge where the
   curve catches the light. */
.lens {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(ellipse 96% 94% at 50% 50%, transparent 62%, rgba(0,0,0,.4) 92%, rgba(0,0,0,.65) 100%),
    radial-gradient(ellipse 99% 97% at 50% 50%, transparent 93%, rgba(63,232,108,.05) 98%, transparent 100%);
}

.scene {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  align-content: center;
  opacity: 0;
  transition: opacity 2400ms ease;
  text-align: center;
}

.scene.on { opacity: 1; }

.scene-title {
  letter-spacing: 0.42em;
  text-transform: uppercase;
  font-size: 0.66rem;
  color: var(--bio-dim);
  margin-bottom: 1.2rem;
}

/* ── phosphor ───────────────────────────────────────────────────────────────
   The look of a long-persistence green CRT: everything glows, the glow is
   wider than the glyph, and the whole tube breathes slightly. Applied to the
   scene text rather than the page so the actual content stays crisp and
   readable — atmosphere must not cost legibility. */

.scene, .scene * {
  text-shadow:
    0 0 2px rgba(63, 232, 108, 0.9),
    0 0 10px rgba(63, 232, 108, 0.55),
    0 0 28px rgba(63, 232, 108, 0.28),
    /* Chromatic aberration. A real lens cannot focus every wavelength on the
       same point, and the eye reads that split as "through glass" even at a
       sub-pixel offset it could not consciously identify. */
    0.7px 0 rgba(255, 40, 90, 0.28),
    -0.7px 0 rgba(60, 170, 255, 0.24),
    /* Horizontal persistence. A slow phosphor drags the beam sideways, which
       is the smear that separates a CRT from a crisp LCD render of one. */
    6px 0 12px rgba(63, 232, 108, 0.22),
    -4px 0 10px rgba(63, 232, 108, 0.16);
}

/* Every scene also breathes and slips very slightly on its own clock, so the
   layer never locks into one rhythm with the drift above. */
.scene > *:not(.scene-title) { animation: seethe 11s ease-in-out infinite alternate; }

@keyframes seethe {
  0%   { transform: translate(0, 0) scale(1); filter: blur(0); }
  40%  { transform: translate(3px, -2px) scale(1.006); filter: blur(.4px); }
  100% { transform: translate(-2px, 2px) scale(.997); filter: blur(.2px); }
}

/* A screen-blended bloom layer over the whole page. Very low alpha — it is the
   difference between "green text" and "green text on a tube". */
.phosphor {
  position: fixed;
  inset: 0;
  z-index: 8999;
  pointer-events: none;
  mix-blend-mode: screen;
  background: radial-gradient(ellipse at 50% 45%, rgba(63, 232, 108, 0.055), transparent 62%);
  animation: tube 5.5s ease-in-out infinite;
}

@keyframes tube {
  0%, 100% { opacity: 0.85; }
  47%      { opacity: 1; }
  49%      { opacity: 0.7; }   /* the small irregular dip is what sells "old" */
  51%      { opacity: 0.95; }
}

/* ── scene: incident log ────────────────────────────────────────────────── */

.s-log { width: min(64rem, 88vw); font-size: 0.8rem; line-height: 1.7; text-align: left; }
.s-log .t   { color: #2c4a3a; margin-right: 0.6rem; }
.s-log .warn { color: var(--plasma); }
.s-log .crit { color: var(--blood); }
.s-log > div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── scene: motion tracker ──────────────────────────────────────────────── */

.s-radar { width: min(34rem, 62vw); aspect-ratio: 1; position: relative; }

.s-radar .rings {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle, transparent 0 24%, rgba(63,232,108,.28) 24% 24.4%, transparent 24.4%),
    radial-gradient(circle, transparent 0 49%, rgba(63,232,108,.24) 49% 49.4%, transparent 49.4%),
    radial-gradient(circle, transparent 0 74%, rgba(63,232,108,.2)  74% 74.4%, transparent 74.4%),
    radial-gradient(circle, transparent 0 99%, rgba(63,232,108,.3)  99% 100%,  transparent 100%);
}

/* The sweep leaves a wedge of afterglow behind it, which is the single most
   recognisable property of a phosphor radar. */
.s-radar .sweep {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    rgba(63, 232, 108, 0.85) 0deg 0.6deg,
    rgba(63, 232, 108, 0.16) 3deg,
    rgba(63, 232, 108, 0.04) 26deg,
    transparent 46deg 360deg);
  animation: sweep 4s linear infinite;
}

@keyframes sweep { to { rotate: 360deg; } }

.s-radar .blip {
  position: absolute;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1px solid var(--plasma);
  background: none;
  translate: -50% -50%;
  box-shadow: 0 0 10px var(--plasma);
  animation: decay 4s linear infinite;
}

.s-radar .blip.hostile { border-color: var(--blood); box-shadow: 0 0 12px var(--blood); }

@keyframes decay { 0% { opacity: 1; } 70% { opacity: 0.15; } 100% { opacity: 0; } }

/* ── scene: decks ───────────────────────────────────────────────────────────
   Wireframe only. An earlier version filled each compartment with a solid
   colour block and the result read as a chart sitting on top of the page
   rather than as something glowing behind it — big filled shapes pull the eye
   far harder than outlines do. State is now carried by stroke colour and, for
   the worst cases, a struck-through diagonal drawn with a gradient. */

.s-decks { display: grid; grid-template-columns: repeat(12, 1fr); gap: 7px; width: min(48rem, 78vw); }

.s-decks i {
  aspect-ratio: 1;
  background: none;
  border: 1px solid rgba(63, 232, 108, 0.34);
  transition: border-color 700ms, opacity 700ms;
}

.s-decks i.breach { border-color: var(--plasma); border-style: dashed; }

/* The diagonal is a gradient, not an element: it keeps the cell a single node
   and the stroke stays hairline at any size. */
.s-decks i.vent {
  border-color: var(--blood);
  background:
    linear-gradient(to top right, transparent calc(50% - 0.5px), rgba(193,18,31,.75) 50%, transparent calc(50% + 0.5px));
}

.s-decks i.hot {
  border-color: var(--blood);
  background:
    linear-gradient(to top right, transparent calc(50% - 0.5px), rgba(193,18,31,.75) 50%, transparent calc(50% + 0.5px)),
    linear-gradient(to bottom right, transparent calc(50% - 0.5px), rgba(193,18,31,.75) 50%, transparent calc(50% + 0.5px));
  animation: throb 1.4s ease-in-out infinite;
}

@keyframes throb { 50% { opacity: 0.25; } }

/* ── scene: vitals ──────────────────────────────────────────────────────── */

.s-vitals { width: min(38rem, 74vw); font-size: 0.82rem; }
.s-vitals div { display: grid; grid-template-columns: 9rem 1fr 4rem; align-items: center; gap: 0.8rem; margin-bottom: 0.7rem; }
.s-vitals span:first-child { letter-spacing: 0.14em; text-align: left; text-transform: uppercase; font-size: 0.7rem; }
.s-vitals .bar { height: 10px; border: 1px solid rgba(63, 232, 108, 0.3); position: relative; }
.s-vitals .bar b { position: absolute; inset: 50% auto auto 0; height: 2px; translate: 0 -50%; background: var(--bio); transition: width 1200ms ease; }
.s-vitals .bar.low b  { background: var(--plasma); }
.s-vitals .bar.crit b { background: var(--blood); }
.s-vitals .val { text-align: right; }

/* ── scene: roster ──────────────────────────────────────────────────────── */

.s-roster { width: min(40rem, 76vw); font-size: 0.8rem; text-align: left; column-count: 2; column-gap: 3rem; }
.s-roster div { line-height: 1.9; }
.s-roster .lost { color: var(--blood); text-decoration: line-through; opacity: 0.75; }
.s-roster .unk  { color: var(--plasma); }

/* ── scene: docks ───────────────────────────────────────────────────────── */

.s-docks { width: min(40rem, 76vw); font-size: 0.82rem; text-align: left; }
.s-docks div { display: grid; grid-template-columns: 8rem 1fr auto; gap: 1rem; line-height: 2; border-bottom: 1px solid rgba(63,232,108,.14); }
.s-docks .ok    { color: var(--bio); }
.s-docks .warn  { color: var(--plasma); }
.s-docks .crit  { color: var(--blood); }

/* ── scene: wireframe geometry ──────────────────────────────────────────────
   Vector only: concentric polygons, an orbit ring and a hull outline, all
   stroked and none filled. This is the scene that carries the "old vector
   display" idea most directly, so it is deliberately the least literal one. */

.s-wire { width: min(40rem, 70vw); aspect-ratio: 1; }
.s-wire svg { width: 100%; height: 100%; overflow: visible; }
.s-wire [stroke] { fill: none; vector-effect: non-scaling-stroke; }
.s-wire .spin-slow { transform-origin: 50% 50%; animation: spin 44s linear infinite; }
.s-wire .spin-rev  { transform-origin: 50% 50%; animation: spin 31s linear infinite reverse; }
.s-wire .pulse     { animation: wirepulse 3.4s ease-in-out infinite; }

@keyframes spin { to { rotate: 360deg; } }
@keyframes wirepulse { 50% { opacity: 0.25; } }

/* A red wash for the worst events. Low alpha and short: a full-screen flash on
   a landing page is an accessibility problem, not a mood. */
.alarm {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 50%, rgba(193, 18, 31, 0.2), transparent 70%);
  opacity: 0;
  transition: opacity 900ms ease;
}

.alarm.on { opacity: 1; }

/* ── Boot sequence ──────────────────────────────────────────────────────────
   Shown once per tab. It is a curtain, not a gate: it dismisses itself, and
   any key or click skips it. Never trap someone behind an animation. */

#boot {
  position: fixed;
  inset: 0;
  z-index: 9500;
  background: var(--void);
  display: grid;
  align-content: center;
  padding: var(--gut);
  font-family: var(--mono);
  font-size: clamp(0.7rem, 1.6vw, 0.95rem);
  color: var(--bio);
  transition: opacity 700ms ease, visibility 700ms;
}

#boot.done { opacity: 0; visibility: hidden; }

#boot pre { margin: 0 auto; max-width: 62ch; white-space: pre-wrap; }

#boot .skip {
  margin: 2rem auto 0;
  color: var(--ash);
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.cursor::after {
  content: "\2588";
  animation: blink 1s steps(2) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

/* ── Layout ─────────────────────────────────────────────────────────────── */

main { position: relative; z-index: 1; }

section {
  max-width: 78rem;
  margin-inline: auto;
  padding: clamp(3.5rem, 9vw, 7rem) var(--gut);
}

h1, h2, h3 { font-weight: 700; line-height: 1.1; letter-spacing: -0.01em; }

.eyebrow {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--bio-dim);
  margin-bottom: 1rem;
}

.lede { color: var(--ash); max-width: 60ch; font-size: 1.05rem; }

/* ── Hero ───────────────────────────────────────────────────────────────── */

.hero {
  min-height: 100svh;
  display: grid;
  align-content: center;
  position: relative;
  text-align: center;
}

/* ── The console ────────────────────────────────────────────────────────────
   Set dressing for the hero: a ship's terminal still reporting on something
   that has already happened to the crew. Two panes pinned to the edges so the
   centre stays clear for the wordmark and the install command — the atmosphere
   must never compete with the one line people came for.

   Hidden entirely below 60rem: on a phone there is no room for both, and the
   product wins. */

.console {
  position: absolute;
  inset: 0;
  pointer-events: none;
  font-family: var(--mono);
  font-size: 0.68rem;
  line-height: 1.5;
  color: var(--bio-dim);
  opacity: 0.8;
  display: none;

  /* The centre of the layer is masked out entirely. Positioning the panes to
     dodge the wordmark works at exactly one viewport size and collides at
     every other; punching an elliptical hole in the whole layer means the
     product always sits in clear space, at any size, whatever the log happens
     to be printing. */
  mask: radial-gradient(ellipse 40% 33% at 50% 52%, transparent 35%, #000 100%);
}

@media (min-width: 60rem) and (min-height: 40rem) {
  .console { display: block; }
}

.console-pane { position: absolute; width: 20rem; text-align: left; }
.console-log  { left: var(--gut);  top: 5rem; }
.console-ship { right: var(--gut); top: 5rem; }

.pane-title {
  letter-spacing: 0.28em;
  text-transform: uppercase;
  font-size: 0.6rem;
  color: var(--bio-dim);
  border-bottom: 1px solid rgba(63, 232, 108, 0.18);
  padding-bottom: 0.35rem;
  margin-bottom: 0.55rem;
}

.pane-sub { color: var(--blood); opacity: 0.85; }

/* Fixed height with a fade at the bottom, so lines appear to scroll off into
   the hull rather than piling up and pushing the layout around. */
.log {
  height: 13rem;
  overflow: hidden;
  mask: linear-gradient(to bottom, #000 60%, transparent);
}

.log div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.log .t   { color: #2c4a3a; margin-right: 0.5rem; }
.log .warn { color: var(--plasma); }
.log .crit { color: var(--blood); }
.log .new  { animation: flare 900ms ease-out; }

@keyframes flare {
  0%   { color: var(--bone); text-shadow: 0 0 12px rgba(63, 232, 108, 0.7); }
  100% { color: inherit; text-shadow: none; }
}

/* Compartment grid: 8×4 cells that change state. Colour is the only signal
   here, which is fine — it is decoration, and the real content says the same
   things in words further down the page. */
.compartments {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 3px;
}

.compartments i {
  aspect-ratio: 1;
  background: rgba(63, 232, 108, 0.1);
  border: 1px solid rgba(63, 232, 108, 0.16);
  transition: background 400ms, border-color 400ms;
}

.compartments i.breach { background: rgba(255, 176, 0, 0.3);  border-color: var(--plasma); }
.compartments i.vent   { background: rgba(193, 18, 31, 0.35); border-color: var(--blood); }
.compartments i.hot    { background: rgba(193, 18, 31, 0.6);  border-color: var(--blood); animation: throb 1.1s ease-in-out infinite; }

@keyframes throb { 50% { opacity: 0.35; } }

.vitals div { display: grid; grid-template-columns: 6.5rem 1fr 3rem; align-items: center; gap: 0.5rem; margin-bottom: 0.3rem; }
.vitals span:first-child { letter-spacing: 0.1em; }
.vitals .bar { height: 4px; background: rgba(63, 232, 108, 0.12); position: relative; }
.vitals .bar b { position: absolute; inset: 0 auto 0 0; background: var(--bio); transition: width 900ms ease; }
.vitals .bar.low b  { background: var(--plasma); }
.vitals .bar.crit b { background: var(--blood); }
.vitals .val { text-align: right; }

/* ── scene: wireframe geometry ──────────────────────────────────────────────
   Vector only: concentric polygons, an orbit ring and a hull outline, all
   stroked and none filled. This is the scene that carries the "old vector
   display" idea most directly, so it is deliberately the least literal one. */

.s-wire { width: min(40rem, 70vw); aspect-ratio: 1; }
.s-wire svg { width: 100%; height: 100%; overflow: visible; }
.s-wire [stroke] { fill: none; vector-effect: non-scaling-stroke; }
.s-wire .spin-slow { transform-origin: 50% 50%; animation: spin 44s linear infinite; }
.s-wire .spin-rev  { transform-origin: 50% 50%; animation: spin 31s linear infinite reverse; }
.s-wire .pulse     { animation: wirepulse 3.4s ease-in-out infinite; }

@keyframes spin { to { rotate: 360deg; } }
@keyframes wirepulse { 50% { opacity: 0.25; } }

/* A red wash for the worst events. Kept very low-alpha and short: a full-screen
   flash on a landing page is an accessibility problem, not a mood. */
.alarm {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 50%, rgba(193, 18, 31, 0.22), transparent 70%);
  opacity: 0;
  transition: opacity 900ms ease;
}

.alarm.on { opacity: 1; }

.wordmark {
  font-family: var(--mono);
  font-size: clamp(3rem, 15vw, 10rem);
  letter-spacing: clamp(0.3rem, 2.5vw, 1.6rem);
  margin: 0;
  color: var(--bio);
  text-shadow: 0 0 28px rgba(63, 232, 108, 0.45), 0 0 90px rgba(63, 232, 108, 0.18);
  position: relative;
}

.hero-logo { width: clamp(64px, 12vw, 120px); margin-inline: auto; display: block; }

.tagline {
  font-size: clamp(1rem, 2.4vw, 1.45rem);
  color: var(--bone);
  margin: 0.5rem 0 0;
}

.subline { color: var(--ash); margin-top: 0.4rem; }

/* ── The install command ────────────────────────────────────────────────── */

.command {
  display: flex;
  align-items: stretch;
  gap: 0;
  max-width: 44rem;
  margin: 2.5rem auto 0;
  border: 1px solid var(--bio-dim);
  background: rgba(11, 15, 16, 0.82);
  border-radius: 4px;
  overflow: hidden;
  text-align: left;
  box-shadow: 0 0 0 1px rgba(63, 232, 108, 0.08), 0 18px 60px rgba(0, 0, 0, 0.6);
}

.command code {
  font-family: var(--mono);
  font-size: clamp(0.72rem, 1.9vw, 0.95rem);
  color: var(--bone);
  padding: 1rem 1.1rem;
  flex: 1;
  overflow-x: auto;
  white-space: nowrap;
}

.command code .sigil { color: var(--bio); user-select: none; }

.copy {
  border: 0;
  border-left: 1px solid var(--bio-dim);
  background: transparent;
  color: var(--bio);
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding-inline: 1.15rem;
  cursor: pointer;
  transition: background 160ms, color 160ms;
  white-space: nowrap;
}

.copy:hover, .copy:focus-visible { background: var(--bio); color: var(--void); outline: none; }
.copy.ok { background: var(--bio); color: var(--void); }

.command-note { margin-top: 0.85rem; font-size: 0.8rem; color: var(--ash); }
.command-note a { color: var(--bio); }

/* ── Cards ──────────────────────────────────────────────────────────────── */

/* minmax is tuned so a wide viewport lands on THREE columns, not four: there
   are six cards, and four columns leaves two dead cells in the second row. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: 1px;
  background: rgba(63, 232, 108, 0.14);
  border: 1px solid rgba(63, 232, 108, 0.14);
  margin-top: 2.5rem;
}

.card {
  background: var(--hull);
  padding: 1.75rem;
  position: relative;
  transition: background 220ms ease;
}

/* "Thermal vision": the pointer warms the card it is over. Implemented with a
   CSS variable the pointer handler writes, so there is one repaint and no
   per-card listener soup. */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 260ms ease;
  background: radial-gradient(
    22rem circle at var(--mx, 50%) var(--my, 50%),
    rgba(255, 176, 0, 0.16),
    rgba(193, 18, 31, 0.07) 40%,
    transparent 68%
  );
  pointer-events: none;
}

.card:hover::before { opacity: 1; }

.card h3 { margin: 0 0 0.6rem; font-size: 1.1rem; color: var(--bone); }
.card p  { margin: 0; color: var(--ash); font-size: 0.94rem; }
.card .idx {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--bio-dim);
  letter-spacing: 0.2em;
  display: block;
  margin-bottom: 0.9rem;
}
.card strong { color: var(--bio); font-weight: 600; }

/* ── Comparison ─────────────────────────────────────────────────────────── */

.readout {
  width: 100%;
  border-collapse: collapse;
  margin-top: 2.5rem;
  font-size: 0.92rem;
}

.readout th, .readout td {
  border-bottom: 1px solid rgba(63, 232, 108, 0.12);
  padding: 0.7rem 0.9rem;
  text-align: left;
}

.readout thead th {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--bio-dim);
  border-bottom-color: var(--bio-dim);
}

.readout td:not(:first-child), .readout th:not(:first-child) { text-align: center; width: 9rem; }
.readout tbody tr:hover { background: rgba(63, 232, 108, 0.04); }
.readout .yes  { color: var(--bio); }
.readout .no   { color: #4a5654; }
.readout .part { color: var(--plasma); }
.readout .edge td:first-child { color: var(--bone); font-weight: 600; }
.readout .edge td:first-child::before { content: "▸ "; color: var(--bio); }
.readout small { color: var(--ash); display: block; font-size: 0.72rem; }

/* ── Install tabs ───────────────────────────────────────────────────────── */

.tabs { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 2.5rem; }

.tab {
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 0.5rem 0.95rem;
  border: 1px solid rgba(63, 232, 108, 0.22);
  background: transparent;
  color: var(--ash);
  cursor: pointer;
  transition: all 160ms;
}

.tab[aria-selected="true"] { color: var(--void); background: var(--bio); border-color: var(--bio); }
.tab:hover { color: var(--bone); }
.tab[aria-selected="true"]:hover { color: var(--void); }

.panel { display: none; margin-top: 1.25rem; }
.panel.on { display: block; }

/* ── Request ────────────────────────────────────────────────────────────── */

.request {
  border: 1px solid var(--bio-dim);
  background:
    linear-gradient(rgba(11, 15, 16, 0.9), rgba(11, 15, 16, 0.9)),
    radial-gradient(ellipse at 20% 0%, rgba(63, 232, 108, 0.16), transparent 60%);
  padding: clamp(2rem, 5vw, 3.5rem);
  text-align: center;
}

.btn {
  display: inline-block;
  margin-top: 1.6rem;
  font-family: var(--mono);
  font-size: 0.82rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 0.9rem 1.9rem;
  border: 1px solid var(--bio);
  color: var(--bio);
  text-decoration: none;
  transition: all 180ms;
}

.btn:hover, .btn:focus-visible { background: var(--bio); color: var(--void); outline: none; }

/* ── Sound toggle ───────────────────────────────────────────────────────── */

#sound {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 9600;
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 0.55rem 0.85rem;
  background: rgba(5, 7, 10, 0.9);
  border: 1px solid var(--bio-dim);
  color: var(--ash);
  cursor: pointer;
  transition: all 180ms;
}

#sound[aria-pressed="true"] { color: var(--bio); border-color: var(--bio); }
#sound:hover { color: var(--bone); }

/* ── Reveal on scroll ─────────────────────────────────────────────────────
   Opt-IN, not opt-out. `.reveal` on its own does nothing; the hidden state
   only applies once alien.js has added `reveal-ready` to <html> and is
   committed to revealing it again.

   Written the other way round — hide in CSS, show in JS — a single thrown
   exception anywhere earlier in the script leaves the entire page blank with
   no error a visitor could act on. Content must never depend on script to be
   visible. */

.reveal-ready .reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 700ms ease, transform 700ms ease;
}

.reveal-ready .reveal.seen { opacity: 1; transform: none; }

/* ── Footer ─────────────────────────────────────────────────────────────── */

footer {
  border-top: 1px solid rgba(63, 232, 108, 0.12);
  padding: 3rem var(--gut);
  text-align: center;
  color: var(--ash);
  font-size: 0.8rem;
}

footer a { color: var(--bio); }

/* ── Motion and contrast preferences ───────────────────────────────────────
   Not a checkbox exercise. A biomech horror page with a rotating radar and a
   crawling scanline is exactly the sort of thing that makes someone with a
   vestibular disorder close the tab. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .scanlines::after { animation: none; display: none; }
  .s-decks i.hot { animation: none; }
  .passer { display: none; }
  .glitch .scenes, .rgbsplit .scenes, .roll .scenes, .roll .motes { animation: none; }
  .dropout { display: none; }
  .mote { transition: none; animation: none; }
  .scenes { animation: none; }
  .scene > * { animation: none; }
  .s-wire .spin-slow, .s-wire .spin-rev, .s-wire .pulse { animation: none; }
  .s-radar .sweep { animation: none; }
  .phosphor { animation: none; }
  .alarm { display: none; }
  .cursor::after { animation: none; }
  .reveal { opacity: 1; transform: none; transition: none; }
  *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

/* ── Utilities ──────────────────────────────────────────────────────────────
   These exist because the site sets `style-src 'self'` on itself, which blocks
   inline `style=""` attributes outright — nine of them were being dropped in
   production while the page still returned 200, so the layout silently
   degraded and nothing failed loudly.

   Loosening the CSP with 'unsafe-inline' would have been the one-line fix and
   the wrong one: the policy is the reason the page can claim it loads nothing
   it did not ship. */

.stack       { margin-top: 1rem; }
.pad-top     { margin-top: 2rem; }
.pad-top-sm  { margin-top: 1.5rem; }
.centred     { margin-inline: auto; }
.alert       { color: var(--plasma); }
.scroll-x    { overflow-x: auto; }
.fineprint   { max-width: 52rem; margin: 1rem auto 0; opacity: 0.75; }
