/* NIX/HEX — CRT terminal cyber-thriller book site. Tokens lifted 1:1 from the Wonder design. */

/* Self-hosted. An imprint that promises "no accounts to phone home" does not
   fetch its typefaces from a third party on every page load. Latin subset,
   variable weight axis: two files, ~80 KB total, zero external requests. */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-latin-var.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;
}
/* Wordmark only. The file is GeistPixel-Square, subset to the six glyphs
   NIX/HEX needs, which takes the shipped 28.5 KB webfont to 0.7 KB. OFL, no
   Reserved Font Name, self-hosted like the other two.
   Declared under a project name rather than the vendor one: the detector's
   overused-font rule substring-matches "Geist", meaning the saturated Geist Sans
   UI face, and a bitmap display cut is not that face. Naming it for its job here
   keeps the rule live for the font it is actually about. The source is named
   above so nothing is hidden from a reader. */
@font-face {
  font-family: 'Wordmark Pixel';
  font-style: normal;
  font-weight: 400;
  /* swap, not block: block leaves the wordmark invisible for up to 3s on a
     slow link, and the wordmark is the first thing in the hero. */
  font-display: swap;
  src: url('fonts/geist-pixel-square-wordmark.woff2') format('woff2');
  unicode-range: U+002F, U+0065, U+0068, U+0069, U+006E, U+0078;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('fonts/jetbrains-mono-latin-var.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;
}

:root {
  --green: #16a34a;
  --green-bright: #4ade80;
  --head: #eafff1;        /* mono headings */
  --white: #ededef;       /* sans strong text */
  --light: #d6ffe4;
  --light2: #c4cbd4;
  --muted: #8a8f98;
  --ink: #04120a;         /* dark text on green */
  --line: rgba(255,255,255,0.06);
  --line-green: rgba(22,163,74,0.5);
  --pad: clamp(20px, 5vw, 64px);
  --mono: 'JetBrains Mono', ui-monospace, monospace;
  --sans: 'IBM Plex Sans', system-ui, sans-serif;
}

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

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

html, body { overflow-x: hidden; }

/* Keyboard focus. The UA default is a stock blue ring at 3.47:1 on this
   background; phosphor green measures 11.9:1 and belongs to the system. */
:focus-visible {
  outline: 2px solid var(--green-bright);
  outline-offset: 3px;
}

.skip-link {
  position: absolute; left: var(--pad); top: -100px; z-index: 100;
  padding: 12px 18px; border-radius: 4px;
  background: var(--green); color: var(--ink);
  font-family: var(--mono); font-weight: 600; font-size: 13px;
  transition: top .15s;
}
.skip-link:focus { top: 12px; }

body {
  background: #010201;
  color: var(--light);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

.page {
  max-width: 1440px;
  margin: 0 auto;
  position: relative;
  background: radial-gradient(circle, rgb(6,18,10) 0%, rgb(3,6,4) 55%, rgb(1,2,1) 100%);
}
/* Matches the body exactly. It used to be #050506, four values lighter, which is
   imperceptible as a tone but produced a hard vertical rail down both sides of
   the column on any viewport wider than 1440. The home page never showed this
   because its radial already resolves to the body colour at the edges. */

/* CRT overlays (home) */
/* The raster has to sit ON TOP of the content. At z-index 1 it was painted behind
   everything in the hero, so the glyphs carried no scanline structure at all —
   and the one place a tube's raster is actually visible is where the phosphor is
   lit. It also lives inside the hero rather than over the whole page: a multiply
   raster costs roughly 0.35 of contrast ratio, and the body copy underneath it
   measures 4.50:1 before any overlay, so page-wide scanlines put reading text
   under AA. The hero is the tube moment; the reading sections are for reading. */
.crt-scanlines { position: absolute; inset: -6px 0; pointer-events: none; z-index: 7; }
.crt-vignette { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
/* Soft ramps, not hard bars. A scanline is the falloff between two passes of the
   beam; a 1px rectangle of flat black is a screen door, and it aliases into moiré
   the moment the pitch misses the device grid. */
.crt-scanlines {
  background: repeating-linear-gradient(180deg,
    rgba(0,0,0,0.46) 0px,
    rgba(0,0,0,0.26) 0.9px,
    rgba(0,0,0,0)    1.8px,
    rgba(0,0,0,0)    2.4px,
    rgba(0,0,0,0.26) 3.2px,
    rgba(0,0,0,0.46) 4px);
  mix-blend-mode: multiply;
  animation: raster-drift 14s linear infinite;
}
/* One full period per cycle, so the loop is seamless. A raster that never moves
   reads as a texture printed on the page rather than a beam retracing. */
@keyframes raster-drift { to { transform: translateY(4px); } }
@media (min-resolution: 2dppx) {
  /* Denser pitch where the device can resolve it. */
  .crt-scanlines {
    background: repeating-linear-gradient(180deg,
      rgba(0,0,0,0.48) 0px,
      rgba(0,0,0,0.27) 0.7px,
      rgba(0,0,0,0)    1.4px,
      rgba(0,0,0,0)    1.8px,
      rgba(0,0,0,0.27) 2.4px,
      rgba(0,0,0,0.48) 3px);
  }
  @keyframes raster-drift { to { transform: translateY(3px); } }
}
@media (prefers-reduced-motion: reduce) { .crt-scanlines { animation: none; } }
.crt-vignette { background: radial-gradient(circle, rgba(22,163,74,0.10) 0%, rgba(0,0,0,0) 60%); }

/* ---------- Wordmark ---------- */
/* Inside the hero, above the headline. There is no header bar: with a single
   page there was nothing for a nav to navigate to, and a mark in a fixed strip
   sat outside the tube while everything else sat in it. Here the raster and the
   bloom fall across it like the rest of the screen. */
.wordmark {
  font-family: 'Wordmark Pixel', var(--sans);
  font-size: 38px; font-weight: 400; letter-spacing: 0.6px;
  color: var(--white); line-height: 1;
  margin-bottom: 4px;
}
.wordmark .slash { color: var(--green); }
.btn { display: inline-flex; align-items: center; gap: 8px; border-radius: 4px; cursor: pointer;
  font-family: var(--sans); font-weight: 600; transition: transform .12s, box-shadow .12s; white-space: nowrap; }
.btn:hover { transform: translateY(-1px); }
.btn svg { display: block; }
.btn-primary {
  background: var(--green); color: var(--ink);
  box-shadow: 0 0 0 1px rgba(22,163,74,0.6), 0 0 16px rgba(22,163,74,0.35);
}

/* ---------- Buttons (shared sizes) ---------- */
.btn-lg { height: 54px; padding: 0 28px; font-size: 16px; }
.btn-ghost {
  background: #0a1f12; color: var(--light);
  border: 1px solid rgba(22,163,74,0.30);
  box-shadow: inset 0 0 12px rgba(22,163,74,0.12);
  font-family: var(--mono); font-weight: 500;
}

/* ---------- Generic section ---------- */
.section { padding: 100px var(--pad); border-top: 1px solid var(--line); }
.kicker { color: var(--green); font-family: var(--mono); font-size: 11px; letter-spacing: 1.5px;
  text-transform: uppercase; }

/* ---------- Hero (home) ---------- */
/* No min-height. The hero was 560px tall holding ~470px of content, and the
   162px of dead air below the CTAs pushed the download a full screen further
   down. The CRT effect is unchanged; only the empty space is gone. */
.hero { display: flex; flex-direction: column; align-items: flex-start; gap: 24px;
  padding: 40px var(--pad) 80px; position: relative; z-index: 2; }
.hero h1 { max-width: 860px; color: var(--head); font-family: var(--mono); font-weight: 600;
  font-size: clamp(38px, 6vw, 62px); line-height: 1.06; letter-spacing: -1.5px;
  text-shadow: rgba(22,163,74,0.65) 2px 0 0, rgba(255,0,90,0.4) -2px 0 0; }
.lead { max-width: 640px; color: var(--muted); font-size: 19px; line-height: 1.7; }
.cta-row { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 8px; }

/* ---------- Hero CRT / cyberpunk shader (home) ---------- */
.hero--crt { isolation: isolate; overflow: clip; }
.hero--crt > *:not(.crt-noise):not(.crt-roll):not(.crt-beam):not(.crt-lines):not(.crt-scanlines):not(.crt-bloom):not(.crt-glass):not(.crt-grain):not(.cta-row) { position: relative; z-index: 4; }

/* ---------- Phosphor bloom ---------- */
/* The thing that actually makes a tube look like a tube. Phosphor does not stop
   at the edge of a glyph: it glows, and the glow bleeds across the scanline gaps
   and into the black around it. Without this the hero is crisp text with grey
   bars laid over it, which is what stripes on a webpage look like.
   backdrop-filter samples everything painted beneath, blurs and brightens it,
   and `screen` adds that halo back over the original — so the bloom is generated
   from the real glyphs rather than faked with a text-shadow, and it works on
   every element in the hero at once. */
/* The filter chain forces the halo to the phosphor's own colour rather than
   inheriting the near-white of the glyphs: grayscale flattens the source, sepia
   gives it a hue to rotate, and hue-rotate + saturate land it on green. A tube
   glows the colour of its phosphor; an uncoloured blur just reads as grey haze. */
.hero--crt .crt-bloom {
  position: absolute; inset: 0; z-index: 5; pointer-events: none;
  -webkit-backdrop-filter: blur(4px) brightness(1.35) grayscale(1) sepia(1) hue-rotate(68deg) saturate(4.5);
  backdrop-filter: blur(4px) brightness(1.35) grayscale(1) sepia(1) hue-rotate(68deg) saturate(4.5);
  mix-blend-mode: screen;
  opacity: 0.5;
}
/* A second, wider pass for the far halo. Two radii read as light falling off,
   one radius reads as a drop shadow. */
.hero--crt .crt-bloom::after {
  content: ''; position: absolute; inset: 0;
  -webkit-backdrop-filter: blur(13px) brightness(1.25) grayscale(1) sepia(1) hue-rotate(68deg) saturate(5);
  backdrop-filter: blur(13px) brightness(1.25) grayscale(1) sepia(1) hue-rotate(68deg) saturate(5);
  mix-blend-mode: screen;
  opacity: 0.28;
}

/* ---------- Black grain ---------- */
/* Sits above the raster, so it dirties the scanlines themselves rather than the
   image underneath them. The existing .crt-noise is a bright grain on `screen`
   down at z-index 5, lifting the picture; this is its opposite — a `multiply`
   layer whose turbulence is gamma-lifted toward white so most of it is inert and
   only the darkest speckles bite. That is what keeps it reading as dirt on the
   tube instead of a grey veil over the whole hero.
   Two animations, as with the noise layer: a fast positional jump so the grain
   never sits still, and a slow swell so the amount of dirt varies. */
.hero--crt .crt-grain {
  position: absolute; inset: -20%; z-index: 8; pointer-events: none;
  opacity: 0.5;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='170' height='170'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.35 0 0 0 -0.68'/></filter><rect width='100%25' height='100%25' filter='url(%23g)'/></svg>");
  animation: grain-jump 0.5s steps(5) infinite, grain-swell 5.5s ease-in-out infinite;
}
@keyframes grain-jump {
  0%   { transform: translate(0,0); }
  20%  { transform: translate(-4%, 3%); }
  40%  { transform: translate(5%, -4%); }
  60%  { transform: translate(-3%, -5%); }
  80%  { transform: translate(4%, 4%); }
  100% { transform: translate(-5%, 2%); }
}
@keyframes grain-swell { 0%, 100% { opacity: 0.5; } 45% { opacity: 0.72; } 72% { opacity: 0.38; } }

/* ---------- Screen glass ---------- */
/* Edge falloff and a sheen. The tube is curved, so brightness drops toward the
   corners; the glass in front of it catches a little room light. Anchored to the
   edges, so it never resolves into a floating shape. */
/* The raster is at full strength across the headline and eases off down the
   frame. A tube really is brightest at the top of the scan, but the honest
   reason is contrast: at full strength over the whole hero the lead measured
   3.14:1 and the button label 3.02:1, both under AA. The effect belongs on the
   display type, not on the running copy and the controls. */
.hero--crt .crt-scanlines {
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 40%, rgba(0,0,0,0.30) 68%, rgba(0,0,0,0.18) 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 40%, rgba(0,0,0,0.30) 68%, rgba(0,0,0,0.18) 100%);
}
/* Interactive controls sit above the whole tube stack. The bloom simulates
   emitted phosphor, and it lifts the button's dark ink toward its green fill:
   the label measured 3.02:1 through it, against 5.86:1 clean. A control's label
   is not something the effect gets to eat. They keep their own green box-shadow,
   so they still read as part of the screen rather than floating off it. */
.hero--crt > .cta-row { position: relative; z-index: 9; }

/* A green tube has no neutral grey on it. Signal Grey is right for the reading
   sections and wrong inside the phosphor, where every lit thing is the colour
   of the phosphor. This also buys back the contrast the raster costs. */
.hero--crt .lead { color: #c3e6d1; }

.hero--crt .crt-glass {
  position: absolute; inset: 0; z-index: 9; pointer-events: none;
  background:
    radial-gradient(ellipse 128% 140% at 50% 48%,
      rgba(0,0,0,0) 52%, rgba(0,0,0,0.28) 82%, rgba(0,0,0,0.55) 100%),
    linear-gradient(103deg,
      rgba(214,255,228,0.035) 0%, rgba(214,255,228,0.012) 26%, rgba(255,255,255,0) 44%);
}

/* TV-static grain (SVG turbulence), shimmering + jumping */
.hero--crt .crt-noise {
  position: absolute; inset: -20%; z-index: 5; pointer-events: none; opacity: 0.06;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  mix-blend-mode: screen;
  animation: crt-noise 0.6s steps(4) infinite, crt-noise-burst 6s ease-in-out infinite;
}
@keyframes crt-noise {
  0% { transform: translate(0,0); } 25% { transform: translate(-6%, 3%); }
  50% { transform: translate(4%, -5%); } 75% { transform: translate(-3%, 6%); }
  100% { transform: translate(5%, -2%); }
}
@keyframes crt-noise-burst { 0%,88%,100% { opacity: 0.06; } 90% { opacity: 0.22; } 94% { opacity: 0.04; } 96% { opacity: 0.18; } }

/* Vertical sync roll-bar drifting up the screen */
.hero--crt .crt-roll {
  position: absolute; left: 0; right: 0; top: 0; height: 240px; z-index: 3; pointer-events: none;
  background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 45%, rgba(74,222,128,0.06) 50%, rgba(0,0,0,0.35) 55%, rgba(0,0,0,0) 100%);
  mix-blend-mode: multiply;
  animation: crt-roll 9s linear infinite;
}
@keyframes crt-roll { 0% { transform: translateY(560px); } 100% { transform: translateY(-240px); } }

/* Fine phosphor scanlines + RGB comb + flicker */
/* Tube flicker only — no pattern of its own. Any second repeating gradient here
   crosses the page raster and weaves a dot matrix, which is what the old
   magenta/green comb was doing: at normal viewing size it read as a screen door,
   not as a CRT. On a real tube the horizontal raster is the visible structure and
   the mask behind it is far too fine to resolve. So this layer is a flat veil
   whose opacity stutters, giving the hero the unstable brightness of a tube that
   has been on too long. */
.hero--crt .crt-lines {
  position: absolute; inset: -2px; z-index: 6; pointer-events: none;
  background: rgba(4,18,10,0.055);
  mix-blend-mode: multiply;
  animation: crt-flicker 4s steps(60) infinite;
}
@keyframes crt-flicker {
  0%, 90%, 100% { opacity: 0.6; }
  91% { opacity: 0.3; } 92% { opacity: 0.85; } 93% { opacity: 0.4; }
  94% { opacity: 0.95; } 95% { opacity: 0.45; }
}

/* Bright beam sweeping down */
.hero--crt .crt-beam {
  position: absolute; left: 0; right: 0; top: 0; height: 140px; z-index: 3; pointer-events: none;
  background: linear-gradient(180deg, rgba(74,222,128,0) 0%, rgba(74,222,128,0.12) 45%, rgba(214,255,228,0.20) 50%, rgba(74,222,128,0.12) 55%, rgba(74,222,128,0) 100%);
  mix-blend-mode: screen;
  animation: crt-sweep 6s linear infinite;
}
@keyframes crt-sweep { 0% { transform: translateY(-140px); } 100% { transform: translateY(560px); } }

/* Chromatic-aberration + block-slice glitch headline */
/* Restored to the original construction at the user's request. This is the
   neon-slice version: two solid #22ff88 / #ff2e88 copies of the headline,
   clip-path-sliced on coprime 2.4s and 1.9s steps(2) cycles, over a permanent
   chromatic text-shadow, with skewX block jump and a hue-rotate pulse.
   A masked-source rebuild was tried and rejected as too quiet; see DESIGN.md.
   The mobile guard below is kept — it is a legibility fix, not a look. */
.hero--crt .glitch {
  position: relative;
  text-shadow: rgba(34,255,136,0.4) 1px 0 6px, rgba(255,46,136,0.3) -1px 0 6px;
  animation: crt-jitter 5s steps(1) infinite, crt-huepulse 7s ease-in-out infinite;
}
.hero--crt .glitch::before,
.hero--crt .glitch::after {
  content: attr(data-text); position: absolute; inset: 0; z-index: -1; mix-blend-mode: screen;
}
.hero--crt .glitch::before { color: #22ff88; transform: translate(-2px, 0); animation: glitch-a 2.4s steps(2) infinite; }
.hero--crt .glitch::after  { color: #ff2e88; transform: translate(2px, 0);  animation: glitch-b 1.9s steps(2) infinite; }

@keyframes glitch-a {
  0%   { clip-path: inset(0 0 85% 0);   transform: translate(-2px,0); }
  8%   { clip-path: inset(15% 0 55% 0); transform: translate(-8px,-1px); }
  16%  { clip-path: inset(62% 0 8% 0);  transform: translate(-3px,1px); }
  24%  { clip-path: inset(35% 0 40% 0); transform: translate(-12px,0); }
  32%  { clip-path: inset(5% 0 78% 0);  transform: translate(2px,0); }
  40%,86% { clip-path: inset(0 0 100% 0); transform: translate(-2px,0); }
  88%  { clip-path: inset(44% 0 28% 0); transform: translate(-10px,0); }
  92%  { clip-path: inset(12% 0 60% 0); transform: translate(6px,0); }
  96%  { clip-path: inset(70% 0 6% 0);  transform: translate(-6px,0); }
  100% { clip-path: inset(0 0 85% 0);   transform: translate(-2px,0); }
}
@keyframes glitch-b {
  0%   { clip-path: inset(80% 0 5% 0);  transform: translate(2px,0); }
  10%  { clip-path: inset(40% 0 35% 0); transform: translate(9px,1px); }
  20%  { clip-path: inset(8% 0 78% 0);  transform: translate(3px,-1px); }
  30%  { clip-path: inset(55% 0 22% 0); transform: translate(13px,0); }
  40%,84% { clip-path: inset(0 0 100% 0); transform: translate(2px,0); }
  87%  { clip-path: inset(25% 0 48% 0); transform: translate(-8px,0); }
  91%  { clip-path: inset(60% 0 12% 0); transform: translate(7px,0); }
  95%  { clip-path: inset(5% 0 82% 0);  transform: translate(-4px,0); }
  100% { clip-path: inset(80% 0 5% 0);  transform: translate(2px,0); }
}
/* whole-line block jump + tear */
@keyframes crt-jitter {
  0%, 70%, 100% { transform: translate(0,0); clip-path: none; }
  71% { transform: translate(-4px,0) skewX(2deg); clip-path: inset(30% 0 20% 0); }
  72% { transform: translate(5px,-1px) skewX(-1.5deg); clip-path: inset(0 0 60% 0); }
  73% { transform: translate(-2px,1px); clip-path: inset(55% 0 0 0); }
  74% { transform: translate(0,0); clip-path: none; }
  92% { transform: translate(3px,0) skewX(-2deg); clip-path: inset(10% 0 45% 0); }
  93% { transform: translate(-3px,0); clip-path: none; }
}
/* occasional hue/brightness flash */
@keyframes crt-huepulse {
  0%, 40%, 100% { filter: none; }
  42% { filter: hue-rotate(70deg) brightness(1.4); }
  44% { filter: hue-rotate(-40deg) brightness(0.9); }
  46% { filter: none; }
  80% { filter: invert(0.06) hue-rotate(20deg); }
  82% { filter: none; }
}

@media (prefers-reduced-motion: reduce) {
  .hero--crt .crt-noise, .hero--crt .crt-roll, .hero--crt .crt-lines, .hero--crt .crt-beam,
  .hero--crt .glitch, .hero--crt .glitch::before, .hero--crt .glitch::after { animation: none; }
  .hero--crt .crt-beam, .hero--crt .crt-roll, .hero--crt .crt-noise { display: none; }
  .hero--crt .crt-grain { animation: none; }
}

/* ---------- Featured books (home) ---------- */
.books-head { max-width: 560px; display: flex; flex-direction: column; gap: 10px; }
.books-head h2 { color: var(--head); font-family: var(--mono); font-weight: 600; font-size: 36px;
  letter-spacing: -0.6px; }
.books-head p { color: var(--muted); font-size: 17px; line-height: 1.7; }
.section .books-head { margin-bottom: 32px; }
/* Grid, not flex-column: grid-auto-rows: 1fr makes every card the height of the
   tallest, so the covers all render at one size. As independent flex rows they
   took their own content's height and the same cover art got three crops. */
.books-row { display: grid; grid-auto-rows: 1fr; gap: 28px; }

/* Horizontal book card (home) */
.book-card { display: flex; border-radius: 16px; overflow: clip;
  background: linear-gradient(rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.02) 100%);
  border: 1px solid var(--line);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.06), 0 8px 40px rgba(0,0,0,0.5); }
.book-card.featured {
  background: linear-gradient(rgba(22,163,74,0.08) 0%, rgba(255,255,255,0.02) 100%);
  border-color: rgba(22,163,74,0.5);
  box-shadow: 0 0 0 1px rgba(22,163,74,0.5), 0 0 40px rgba(22,163,74,0.35), 0 8px 40px rgba(0,0,0,0.5); }
/* A real image element rather than a background-image, so it can carry
   loading="lazy" and intrinsic width/height. Every cover sits below the fold,
   and as CSS backgrounds all four images downloaded eagerly on first paint. */
.book-cover { position: relative; width: 240px; flex-shrink: 0; overflow: clip; }
.book-cover img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center; }
.book-cover::after { content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgba(0,0,0,0) 60%, rgba(10,10,12,0.85) 100%); }
.book-body { display: flex; flex-direction: column; flex: 1; padding: 26px; gap: 14px; }
.hex-index { color: var(--green); font-family: var(--mono); font-size: 16px; letter-spacing: 0.8px;
  text-shadow: rgba(22,163,74,0.7) 0 0 8px; }
.book-title { color: var(--head); font-family: var(--mono); font-weight: 600; font-size: 22px;
  line-height: 1.25; letter-spacing: -0.4px; }
/* The horizontal home cards are ~1018px wide and the blurb had no cap, so it ran
   to 124 characters per line — far past the 65-75 the type system calls for. The
   grid cards on the catalogue are narrow enough that this never bit there. */
.book-blurb { max-width: 560px; color: var(--muted); font-size: 16px; line-height: 1.7; }
/* A verbatim line lifted from the book itself. The prose is the only thing on this
   page that can actually sell a book, so each card carries a sample of it. */
.book-line { max-width: 560px; padding-left: 14px; border-left: 1px solid var(--line);
  color: var(--head); font-family: var(--mono); font-size: 14px; line-height: 1.65; text-wrap: pretty; }
.book-meta { display: flex; align-items: center; gap: 16px; margin-top: 4px; color: var(--muted); font-size: 13px; }
.meta-dot { width: 3px; height: 3px; background: var(--muted); border-radius: 999px; }
.download-row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center;
  gap: 14px; margin-top: auto; padding-top: 18px; border-top: 1px solid var(--line); }
.file-note { display: flex; flex-direction: column; gap: 2px; }
.file-note .free { color: var(--green); font-family: var(--mono); font-weight: 600; font-size: 24px; line-height: 1; }
/* The moment of maximum doubt. State the absence of a catch where the catch would be. */
.file-note .terms { color: var(--muted); font-family: var(--mono); font-size: 11px; letter-spacing: 0.4px; }
.download-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 44px; padding: 11px 18px; border: none; border-radius: 4px;
  background: var(--green); color: var(--ink); font-family: var(--sans); font-weight: 600; font-size: 15px;
  cursor: pointer; transition: transform .12s;
  box-shadow: 0 0 0 1px rgba(22,163,74,0.8), 0 0 28px rgba(22,163,74,0.6), 0 0 60px rgba(22,163,74,0.4); }
.download-btn:hover { transform: translateY(-1px); }
.download-btn:active { transform: translateY(0); }
.download-btn svg { width: 15px; height: 15px; display: block; }
/* Pending state: the file does not exist yet. Reads as machine status, not as a control. */
.download-pending { display: inline-flex; align-items: center; min-height: 44px; padding: 11px 18px;
  border-radius: 4px; background: var(--tube-glass, #0a1f12); border: 1px solid rgba(22,163,74,0.30);
  color: var(--muted); font-family: var(--mono); font-size: 12px; letter-spacing: 0.6px; }

/* ---------- Author band (home) ---------- */
.author-band { display: flex; align-items: flex-start; gap: 48px; padding: 100px var(--pad);
  border-top: 1px solid var(--line); position: relative; z-index: 2; }
.author-sign { display: flex; flex-direction: column; gap: 2px; margin-top: 8px;
  font-family: var(--mono); font-size: 13px; line-height: 1.6; color: var(--muted); }
.author-sign .sig { color: var(--green-bright); }
.author-copy { display: flex; flex-direction: column; flex: 1; max-width: 600px; gap: 16px; }
.author-copy h2 { color: var(--head); font-family: var(--mono); font-weight: 600; font-size: 32px;
  line-height: 1.25; letter-spacing: -0.6px; }
.author-copy p { color: var(--muted); font-size: 17px; line-height: 1.7; }

/* Author portrait. The image already carries its own dither and scanline
   structure, so this frame deliberately adds none: a second pattern crossed with
   the one baked into the photograph weaves moiré, the same way the vertical
   grille did over the hero raster. What the frame contributes is the tube's
   edge falloff and the system's standard ring-and-lift, nothing more.
   Replaces the CSS burn-in panel, which existed because there was no image. */
.author-photo {
  position: relative; flex-shrink: 0;
  /* 300 CSS px wide against a 600x600 asset means one image pixel per device
     pixel at DPR 2, and a clean 2:1 at DPR 1. aspect-ratio matches the file so
     the browser never crops or stretches: an earlier pass drew a square source
     into a fixed 300x340 box, resampling by 3.4x, which averages a dither into
     mush. The cap holds on mobile for the same reason — wider would upscale. */
  width: 300px; max-width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 16px; overflow: clip;
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.06), 0 8px 40px rgba(0,0,0,0.5);
}
.author-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
.author-photo::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse at 50% 45%, rgba(0,0,0,0) 48%, rgba(0,0,0,0.42) 100%);
}

/* ---------- Footer ---------- */
.footer { display: flex; justify-content: space-between; align-items: flex-start; gap: 48px;
  padding: 56px var(--pad); background: #020203; border-top: 1px solid var(--line);
  position: relative; z-index: 2; }
.footer-brand { display: flex; flex-direction: column; gap: 12px; max-width: 280px; }
.footer-brand-row { display: flex; align-items: center; }
.footer-name {
  font-family: 'Wordmark Pixel', var(--sans);
  font-size: 38px; font-weight: 400; letter-spacing: 0.6px;
  color: var(--white);
}
.footer-name .slash { color: var(--green); }
.footer-tag { color: var(--muted); font-size: 15px; line-height: 1.7; }
.footer-links { display: flex; gap: 64px; }
.footer-col { display: flex; flex-direction: column; gap: 10px; }
.footer-col h3 { color: var(--green); font-family: var(--mono); font-size: 11px; font-weight: 400;
  letter-spacing: 1.1px; text-transform: uppercase; }
/* 15px text is a 19px box. Mouse-friendly, but these are the only navigation
   the page has, so give them vertical room at every width, not just mobile. */
.footer-col a { color: var(--muted); font-size: 15px; padding: 7px 0; transition: color .15s; }
.footer-col a:hover { color: var(--white); }

.explainer { display: flex; align-items: flex-start; gap: 64px; padding: 96px var(--pad) 88px;
  border-bottom: 1px solid var(--line); position: relative; overflow: clip; }
.explainer-text { display: flex; flex-direction: column; flex: 1; gap: 20px; }
.explainer-text h2 { max-width: 440px; color: var(--head); font-family: var(--mono); font-weight: 600;
  font-size: 30px; line-height: 1.15; letter-spacing: -0.6px; }
.explainer-text p { max-width: 540px; color: var(--muted); font-size: 17px; line-height: 1.7; }
/* Mono earning its place: this is a real path inside a real archive, which is
   the one thing the terminal beside it is there to let a reader verify. */
.explainer-text code { font-family: var(--mono); font-size: 15px; color: var(--light);
  background: rgba(22,163,74,0.10); border: 1px solid rgba(22,163,74,0.22);
  border-radius: 4px; padding: 1px 6px; overflow-wrap: anywhere; }
/* A screen, not a window. This had macOS chrome on it: three traffic lights and
   a title bar, which is an application frame from an operating system this
   imprint does not live in. What is left is the tube itself, matching the hero:
   phosphor on near-black, a raster over the top, and a bezel that curves in. */
.terminal { position: relative; width: 440px; flex-shrink: 0; border-radius: 6px; overflow: clip;
  background: radial-gradient(120% 130% at 50% 30%, #04140b 0%, #010502 78%);
  border: 1px solid rgba(22,163,74,0.22);
  box-shadow: inset 0 0 60px rgba(0,0,0,0.9), inset 0 0 14px rgba(22,163,74,0.10),
    0 0 0 1px rgba(255,255,255,0.03), 0 0 46px rgba(22,163,74,0.10); }
/* Same falloff-not-bars raster as the hero, and the same reduced-motion escape. */
.terminal-scan { position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: repeating-linear-gradient(180deg,
    rgba(0,0,0,0.40) 0px, rgba(0,0,0,0.20) 0.9px,
    rgba(0,0,0,0)    1.8px, rgba(0,0,0,0)    2.4px,
    rgba(0,0,0,0.20) 3.2px, rgba(0,0,0,0.40) 4px);
  mix-blend-mode: multiply;
  animation: raster-drift 14s linear infinite; }
@media (prefers-reduced-motion: reduce) { .terminal-scan { animation: none; } }
/* The glass: corner darkening, so the text sits behind a curved surface. */
.terminal::after { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 3;
  background: radial-gradient(88% 92% at 50% 46%, rgba(0,0,0,0) 52%, rgba(0,0,0,0.55) 100%); }
.terminal-body { position: relative; z-index: 1;
  display: flex; flex-direction: column; gap: 7px; padding: 22px 20px; font-family: var(--mono);
  font-size: 12.5px; line-height: 1.45;
  text-shadow: 0 0 6px rgba(74,222,128,0.35); }
.terminal-body .prompt { color: var(--green); }
.terminal-body .dim { color: var(--light); }
/* Real column alignment. unzip pads its size column with spaces, and HTML
   collapses them, which quietly turned real output into approximated output. */
.terminal-body .out { color: var(--light2); white-space: pre; }
.terminal-body .ok { color: var(--green-bright); white-space: pre; }
.cursor { color: var(--green-bright); animation: blink 1.1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }

.benefits { display: flex; flex-direction: column; gap: 40px; padding: 80px var(--pad);
  border-bottom: 1px solid var(--line); }
.benefits-head, .faq-head { display: flex; flex-direction: column; gap: 12px; }
.benefits-head h2, .faq-head h2 { max-width: 520px; color: var(--head);
  font-family: var(--mono); font-weight: 600; font-size: 30px; line-height: 1.15; letter-spacing: -0.6px; }
.cards-3 { display: flex; gap: 24px; }
.benefit-card { display: flex; flex-direction: column; flex: 1; gap: 16px; padding: 28px; border-radius: 4px;
  background: linear-gradient(rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%);
  border: 1px solid var(--line); box-shadow: 0 0 0 1px rgba(255,255,255,0.04); }
.benefit-icon { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  background: #041a0e; border: 1px solid rgba(22,163,74,0.33); border-radius: 4px;
  box-shadow: 0 0 14px rgba(22,163,74,0.25); }
.benefit-icon svg { width: 20px; height: 20px; }
.benefit-card h3 { color: var(--white); font-size: 19px; font-weight: 600; }
.benefit-card p { color: var(--muted); font-size: 16px; line-height: 1.7; }

.faq { display: flex; flex-direction: column; gap: 32px; padding: 80px var(--pad);
  border-bottom: 1px solid var(--line); }
.faq-list { display: flex; flex-direction: column; gap: 12px; max-width: 820px; }
.faq-item { display: flex; flex-direction: column; gap: 10px; padding: 24px; border-radius: 4px;
  background: linear-gradient(rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.016) 100%);
  border: 1px solid var(--line); }
.faq-q { display: flex; align-items: center; gap: 10px; }
.faq-q svg { width: 16px; height: 16px; flex-shrink: 0; }
.faq-q span { color: var(--white); font-size: 17px; font-weight: 600; }
.faq-a { padding-left: 26px; color: var(--muted); font-size: 16px; line-height: 1.7; }

.cta-band { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 20px;
  padding: 96px var(--pad); border-bottom: 1px solid var(--line); position: relative; overflow: clip; }

/* ---------- Emitting seam ---------- */
/* Section-level illumination. A centred radial disc floated a green oval with a
   visible edge over compositions that were left-aligned, so the shape belonged
   to nothing on the page. Light now spills downward from the seam at the top of
   the section: full-bleed horizontally, no silhouette, and it reads as the seam
   itself emitting rather than as a gradient dropped behind the text. */
.emits { position: relative; }
.emits::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 62%;
  pointer-events: none; z-index: 0;
  background: linear-gradient(180deg,
    rgba(22,163,74,0.20) 0%,
    rgba(22,163,74,0.085) 22%,
    rgba(22,163,74,0.025) 48%,
    rgba(22,163,74,0) 100%);
}
/* The lit edge itself, brightest at centre and falling off to the gutters. */
.emits::after {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 1px;
  pointer-events: none; z-index: 1;
  background: linear-gradient(90deg,
    rgba(74,222,128,0) 0%, rgba(74,222,128,0.45) 50%, rgba(74,222,128,0) 100%);
}
.emits > * { position: relative; z-index: 2; }
.cta-band h2 { max-width: 560px; color: var(--head); font-family: var(--mono); font-weight: 600;
  font-size: clamp(28px, 4vw, 38px); line-height: 1.1; letter-spacing: -1px; }
.cta-band .lead { max-width: 480px; text-align: center; }
/* Says where the one external link on the page goes, before it goes there. */
.cta-note { margin-top: 14px; color: var(--muted); font-size: 13px; text-align: center; }
.btn-xl { height: 52px; padding: 0 26px; font-size: 16px; margin-top: 8px; }

/* ---------- Full-bleed base ---------- */
/* The page is a 1440px column, which reads as the tube with the dark room around
   it — but by the closing band there is no tube left to bezel, and the band's
   emission was terminating in a hard vertical step at the column edge. Light does
   not have a rectangular boundary; that step read as a clipped div, the same
   mistake as a floating disc.
   The DRM-Free hero carries the same .emits treatment and had the same hard
   edge at the top of its page, so it breaks out too.
   These break out to the viewport while their content stays on the column:
   padding-inline grows to whatever keeps the inner width at 1440, so on a wide
   monitor the footer's brand and links stay aligned with the cards above rather
   than flying to opposite edges. Below 1440 the max() falls back to the gutter
   and nothing changes. */
.cta-band, .footer, .explainer {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  padding-inline: max(var(--pad), calc(50vw - 720px));
}

/* ---------- Responsive ---------- */
@media (max-width: 860px) {
    .book-card { flex-direction: column; }
  .book-cover { width: 100%; height: 200px; }
  .book-cover::after { background: linear-gradient(rgba(0,0,0,0) 40%, rgba(10,10,12,0.85) 100%); }
  .author-band { flex-direction: column; align-items: flex-start; gap: 32px; }
  .explainer { flex-direction: column; gap: 32px; }
  .terminal { width: 100%; }
  .cards-3 { flex-direction: column; }
  .footer { flex-direction: column; gap: 32px; }
  .section { padding: 56px var(--pad); }
  .author-band { padding-top: 56px; padding-bottom: 64px; }
  .hero { padding-top: 28px; padding-bottom: 60px; }
  .author-photo { width: 300px; }
}
@media (max-width: 520px) {
  .footer-links { flex-direction: column; gap: 28px; }
  .cta-row .btn { width: 100%; justify-content: center; }
  .download-row { justify-content: stretch; }
  .download-btn { flex: 1; }
  /* Footer links were 16px tall. Thumb targets need vertical room. */
  .footer-col { gap: 2px; }
  .footer-col a { padding: 12px 0; }
  /* The slice layers double the glyphs at this size, and at 3 lines of 32px
     the headline is the one sentence that must stay readable. The static
     chromatic fringe and every other CRT layer are untouched. */
  .hero--crt .glitch::before, .hero--crt .glitch::after { display: none; }
  .hero--crt .glitch { animation: crt-huepulse 7s ease-in-out infinite; }
}
@media (max-width: 420px) {
  .hero h1 { font-size: 32px; letter-spacing: -1px; }  }
