/* ============================================================
   VOID GALACTICA, amber-phosphor flight computer
   ============================================================ */

@font-face {
  font-family: "ModeNine";
  src: url("../assets/fonts/MODENINE.TTF") format("truetype");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "PixelOperator8";
  src: url("../assets/fonts/PixelOperator8.ttf") format("truetype");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "LilliputSteps";
  src: url("../assets/fonts/lilliput-steps.regular.otf") format("opentype");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

/* VT323 is the fallback face behind the three above; self-hosted (Google
   Fonts subsets, OFL) so no visitor request ever leaves this origin */
@font-face {
  font-family: "VT323";
  src: url("../assets/fonts/vt323-latin.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
  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";
  src: url("../assets/fonts/vt323-latin-ext.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
  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;
}

:root {
  --void: #0a0f0b;
  --void-deep: #060906;
  --panel: rgba(15, 24, 18, 0.72);
  --panel-solid: #0f1812;
  --line: rgba(143, 230, 174, 0.22);
  --line-dim: rgba(143, 230, 174, 0.1);
  --amber: #8fe6ae;
  --amber-hi: #ccf5da;
  --ember: #d96a55;
  --teal: #5abf8a;
  --text: #d5e6d9;
  /* long-form reading tone: the full --text is a headline weight against
     this background, and a wall of it glares */
  --text-soft: #a6bfad;
  --text-dim: #7d9a84;
  /* Pixel Operator carries every terminal-voiced element, body copy stays
     on ModeNine, which is what the game's in-game text uses */
  --font-term: "PixelOperator8", "VT323", monospace;
  --font-display: "PixelOperator8", "VT323", monospace;
  --font-body: "ModeNine", "VT323", monospace;
  --glow-amber: 0 0 12px rgba(143, 230, 174, 0.35);
}

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

/* ---------- line breaking ---------- */

/* Orphans — one word stranded alone on a paragraph's last line — read as a
   mistake, and at this measure with a monospaced face they happen constantly.
   `pretty` lets the browser look ahead over the whole block and pull a word
   down from the line above to avoid it; it also limits consecutive hyphens.
   Body copy only: `pretty` is deliberately conservative and cheap.

   Headings get `balance` instead, which evens the lines rather than filling
   greedily — the right call for two- or three-line titles like the card heads,
   and browsers cap it at a few lines so it never runs on a paragraph.

   Both degrade to normal wrapping where unsupported. Nothing here overrides
   the hero's explicit nowrap lines. */
p,
li,
figcaption,
dd,
.sec-sub,
.led-desc { text-wrap: pretty; }

h1, h2, h3, h4,
.sec-head,
.card h3,
.led-name { text-wrap: balance; }

/* Fluid type scale: ~13px on phones, ~14.5px on full HD, 18px on 4K. ModeNine
   is a pixel face and reads dense, so it wants a smaller base than a normal
   sans would. Everything sized in rem scales with it. */
html {
  scroll-behavior: smooth;
  font-size: clamp(13px, 10px + 0.24vw, 18px);
}

body {
  background: var(--void);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.65;
  overflow-x: hidden;
}

/* Starfield canvas sits behind everything */
#starfield {
  position: fixed;
  inset: 0;
  z-index: -2;
  display: block;
}

/* No global CRT scanline overlay: it was a fixed z-999 sheet of
   rgba(0,0,0,0.14) lines across the whole page, dimming the hero video and the
   screenshots. If the texture comes back, scope it to specific frames rather
   than the body. */

::selection { background: var(--amber); color: var(--void-deep); }

/* phrase highlight inside body copy: no extra brightness, just a greener
   cast at the same weight. Reads as a tint, not a flash. */
.hl { color: #a4e2bd; }

a { color: var(--amber); text-decoration: none; }
a:hover { color: var(--amber-hi); }
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 3px;
}

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

/* ---------- layout ---------- */

.wrap {
  width: 100%;
  margin: 0 auto;
  padding-inline: clamp(1rem, 3.5vw, 4rem);
}

main { padding-bottom: 5rem; }

section { padding: clamp(5rem, 9vw, 9rem) 0; } /* air between sections */

/* ---------- nav ---------- */

/* The bar holds off until the page has spoken: with scripting on it starts
   hidden and each label types itself in, confirming with a short flash the
   way a terminal echoes an accepted command. No JS, no hiding. */
.js .site-nav { opacity: 0; }

/* the empty bar arrives first, its labels type in later */
.js .site-nav.nav-bar-in {
  opacity: 1;
  transition: opacity 0.35s ease;
}

.nav-type { display: inline-block; }

/* Hidden until it types, and hidden by the resting style rather than by the
   animation's forwards fill: WebKit holds a steps() animation on its last
   step, so a label finishing on fill lost its final character. Resting state
   is the finished one; the animation only masks while it runs. The vertical
   insets go negative because Pixel Operator's glyph box overshoots a
   line-height:1 box, and a flat inset(0) would shave the top pixel row. */
.js .site-nav:not(.nav-in) .nav-type {
  clip-path: inset(-30% 100% -30% 0);
}

/* Nothing of a link may show before it types: the active link's > < markers
   and the CTA's frame live outside the typed span, so hold them too. */
.js .site-nav:not(.nav-in) .nav-links a::before,
.js .site-nav:not(.nav-in) .nav-links a::after { opacity: 0; }

.js .site-nav.nav-in .nav-links a::before,
.js .site-nav.nav-in .nav-links a::after {
  opacity: 1;
  transition: opacity 0.3s ease 0.45s;
}

/* beats .nav-links a.nav-cta's own border shorthand */
.nav-links a.nav-cta.fx-frame {
  --frame-color: var(--amber);
  border-color: transparent;
}

.nav-in .nav-type {
  animation:
    nav-type 0.3s steps(var(--chars, 6)) var(--delay, 0s) backwards,
    echo-flash 0.3s ease-out calc(var(--delay, 0s) + 0.3s) both;
}

@keyframes nav-type {
  from { clip-path: inset(-30% 100% -30% 0); }
  to { clip-path: inset(-30% 0 -30% 0); }
}

/* The echo: one bright beat, then settle. Every element that draws itself or
   types itself in ends on this, the way a terminal acknowledges input. */
@keyframes echo-flash {
  0% { filter: brightness(2.1) drop-shadow(0 0 6px rgba(143, 230, 174, 0.75)); }
  35% { filter: brightness(1.35) drop-shadow(0 0 3px rgba(143, 230, 174, 0.35)); }
  100% { filter: none; }
}

.flash { animation: echo-flash 0.3s ease-out both; }

/* Reachable by a screen reader, absent to the eye. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- mobile menu toggle ---------- */

/* Bracket corners, the same mark the wordmark and the nav icon carry, drawn as
   eight background slivers rather than pseudo-elements: ::before and ::after
   are spent on the bars, and four corners want four boxes. Each corner is one
   horizontal and one vertical bar pinned to its own edge, so the frame holds
   its shape at any size without the arms ever meeting. */
.nav-toggle {
  --bracket: 2px;
  --arm: 11px;
  /* 40 x 36 of bracket inside 44 x 44 of button: the frame is inset rather
     than the control shrunk to it, so the tap target keeps the 44px a thumb
     wants while the mark matches the nav icon's exactly. */
  --frame-inset-x: 2px;
  --frame-inset-y: 4px;
  display: none; /* only below the breakpoint, and only with scripting */
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background-color: transparent;
  color: var(--amber);
  cursor: pointer;
  background-image:
    linear-gradient(currentColor, currentColor), linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor), linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor), linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor), linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-size:
    var(--arm) var(--bracket), var(--bracket) var(--arm),
    var(--arm) var(--bracket), var(--bracket) var(--arm),
    var(--arm) var(--bracket), var(--bracket) var(--arm),
    var(--arm) var(--bracket), var(--bracket) var(--arm);
  background-position:
    left var(--frame-inset-x) top var(--frame-inset-y),
    left var(--frame-inset-x) top var(--frame-inset-y),
    right var(--frame-inset-x) top var(--frame-inset-y),
    right var(--frame-inset-x) top var(--frame-inset-y),
    left var(--frame-inset-x) bottom var(--frame-inset-y),
    left var(--frame-inset-x) bottom var(--frame-inset-y),
    right var(--frame-inset-x) bottom var(--frame-inset-y),
    right var(--frame-inset-x) bottom var(--frame-inset-y);
  filter: drop-shadow(0 0 4px rgba(143, 230, 174, 0.4));
  transition: color 0.15s ease, filter 0.25s ease;
}

/* Held back with the rest of the bar and drawn on its own beat. On a phone the
   links are behind this button, so it is the only thing after the wordmark
   with anything to say — it arrives right after it rather than waiting out a
   stagger nobody can see. Ends on the same echo-flash as every other element
   here that draws or types itself in. */
.js .site-nav:not(.nav-in) .nav-toggle { opacity: 0; }

.nav-in .nav-toggle {
  animation:
    nav-toggle-in 0.34s ease-out 0.3s both,
    echo-flash 0.3s ease-out 0.64s both;
}

@keyframes nav-toggle-in {
  from { opacity: 0; transform: scale(0.72); }
  to { opacity: 1; transform: scale(1); }
}

.nav-toggle:hover,
.nav-toggle[aria-expanded="true"] {
  color: var(--amber-hi);
  filter: drop-shadow(0 0 7px rgba(143, 230, 174, 0.65));
}

/* Three bars: the span is the middle one, its two pseudo-elements the outer
   pair, so the whole control is one element in the markup. */
.nav-toggle-bars {
  position: relative;
  display: block;
  width: 18px;
  height: var(--bracket);
  background: currentColor;
}

.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: var(--bracket);
  background: currentColor;
}

.nav-toggle-bars::before { top: -6px; }
.nav-toggle-bars::after { top: 6px; }

/* Open: the outer bars close into a cross and the middle one gets out of the
   way. Only the bars move — the brackets are the frame and stay put. */
.nav-open .nav-toggle-bars { background: transparent; }
.nav-open .nav-toggle-bars::before { transform: translateY(6px) rotate(45deg); }
.nav-open .nav-toggle-bars::after { transform: translateY(-6px) rotate(-45deg); }

.js .nav-logo img { opacity: 0; }

.nav-in .nav-logo img {
  opacity: 1;
  transition: opacity 0.4s ease;
}

/* the footage powers up once the bar is on screen */
.js .hero-bg { opacity: 0; }

.hero-bg.bg-in {
  opacity: 1;
  transition: opacity 0.9s ease;
}

.site-nav {
  position: sticky;
  top: 0;
  z-index: 100;
  /* Let the hero footage and the starfield show through. The blur is what
     keeps the links readable — at this alpha alone, moving video behind the
     labels is legible but restless. */
  background: rgba(2, 4, 3, 0.68);
  -webkit-backdrop-filter: blur(12px) saturate(1.15);
  backdrop-filter: blur(12px) saturate(1.15);
  border-bottom: 2px solid var(--line);
}

/* Firefox with backdrop-filter off, and anything older: no blur means no
   protection for the type, so the bar goes back to being nearly solid. */
@supports not (backdrop-filter: blur(1px)) {
  .site-nav { background: rgba(2, 4, 3, 0.94); }
}

.site-nav .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.7rem;
}

.nav-logo {
  font-family: var(--font-term);
  font-size: 1.7rem;
  letter-spacing: 0.08em;
  color: var(--amber);
  text-shadow: var(--glow-amber);
  white-space: nowrap;
}

.nav-logo .blink { animation: blink 1.1s steps(1) infinite; }

.nav-logo img {
  height: 46px;
  width: auto;
  display: block;
}

.nav-links {
  display: flex;
  gap: 2.2rem;
  list-style: none;
  flex-wrap: wrap;
  flex: 1;
  justify-content: flex-end;
}

.nav-links li:last-child { margin-left: 0; } /* the flex gap already spaces it evenly */

.nav-links a {
  font-family: var(--font-term);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  text-transform: uppercase;
  transition: color 0.15s, text-shadow 0.25s ease;
}

.nav-links a:hover {
  color: var(--amber-hi);
  text-shadow:
    0 0 6px rgba(143, 230, 174, 0.5),
    0 0 20px rgba(143, 230, 174, 0.25);
}

/* the CTA is a filled block on hover, the .nav-cta rules below clear this */
.nav-links a.active:not(.nav-cta) {
  color: var(--amber);
  text-shadow:
    0 0 5px rgba(143, 230, 174, 0.45),
    0 0 18px rgba(143, 230, 174, 0.2);
}

.nav-links a.active {
  color: var(--amber);
}

.nav-links a.active:not(.nav-cta)::before {
  content: ">";
  margin-right: 0.15em;
}

.nav-links a.active:not(.nav-cta)::after {
  content: "<";
  margin-left: 0.15em;
}

.nav-links {
  align-items: center;
}

/* Small label, unchanged frame: the padding carries the difference so the box
   keeps the footprint it had at an 11px label. */
.nav-links a.nav-cta {
  border: 2px solid var(--amber);
  color: var(--amber);
  font-size: 9px;
  letter-spacing: 0.09em;
  line-height: 1;
  padding: 0.74rem 1.4rem 0.68rem;
  transition: background 0.15s, color 0.15s, box-shadow 0.25s ease;
}

.nav-links a.nav-cta:hover {
  background: var(--amber);
  color: var(--void-deep);
  text-shadow: none;
  box-shadow:
    0 0 10px rgba(143, 230, 174, 0.35),
    0 0 26px rgba(143, 230, 174, 0.18);
}

.nav-links a.nav-cta.active {
  background: var(--amber);
  color: var(--void-deep);
}

/* ---------- hero ---------- */

.hero {
  position: relative;
  min-height: calc(100svh - var(--nav-h, 71px)); /* viewport minus measured nav bar */
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Even padding, so the block sits dead centre between the nav bar and the
     foot of the screen. It used to carry more at the bottom to ride the
     wordmark and headline upward together; with the mark gone there is
     nothing above the headline that wants the extra room. */
  padding: 3rem 0;
}

.hero-bg {
  --hero-shift: 480px; /* video starts this far in, clearing the text column */
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  /* Long, eased falloff: a two-stop mask puts the whole fade in the last
     third and the ramp reads as a band. Starting at 25% and stepping the
     alpha down unevenly spreads it over most of the frame, so the footage
     dissolves into the page instead of ending at a line. */
  --hero-fade:
    linear-gradient(
      180deg,
      rgba(0, 0, 0, 1) 0%,
      rgba(0, 0, 0, 1) 25%,
      rgba(0, 0, 0, 0.92) 42%,
      rgba(0, 0, 0, 0.72) 58%,
      rgba(0, 0, 0, 0.45) 72%,
      rgba(0, 0, 0, 0.22) 84%,
      rgba(0, 0, 0, 0.07) 93%,
      rgba(0, 0, 0, 0) 100%
    );
  -webkit-mask-image: var(--hero-fade);
  mask-image: var(--hero-fade);
}

.hero-bg video {
  margin-left: var(--hero-shift);
  width: calc(100% - var(--hero-shift));
  height: 100%;
  object-fit: cover;
  /* The source footage is genuinely near-black: mean luma 41/255, with the
     whole frame inside a 35-48 band. Lift it here rather than re-grading the
     file. No contrast bump, it pivots on mid-grey and would crush this. */
  filter: brightness(1.6) saturate(1.15);
  /* its own left edge dissolves, so the shift never reads as a hard seam.
     Vertical fade stays on .hero-bg, separate elements, so no compositing. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, black 220px);
  mask-image: linear-gradient(90deg, transparent 0, black 220px);
}

/* no room to give away once the copy goes full width */
@media (max-width: 860px) {
  .hero-bg { --hero-shift: 0px; }
  .hero-bg video {
    -webkit-mask-image: linear-gradient(90deg, transparent 0, black 120px);
    mask-image: linear-gradient(90deg, transparent 0, black 120px);
  }
}

/* Now that the video is brightened, the text column needs its ground back.
   Stops are in px, not %, so the dim tracks the copy width rather than the
   viewport, it holds under the title, then clears the footage entirely. */
.hero-shade {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(4, 8, 5, 0.6) 0, rgba(4, 8, 5, 0.5) 420px, transparent 900px);
}

/* held back until the title finishes typing (main.js adds the classes) */
.hero-hold {
  opacity: 0;
  transform: translateY(10px);
}

.hero-hold.in {
  opacity: 1;
  transform: none;
  transition: opacity 0.5s ease, transform 0.5s ease;
}

/* build stamp above the title, in the voice of a status line */
.hero-tag {
  display: flex;
  /* the badge carries a border and uneven padding, so match the two on their
     text baselines rather than on their boxes */
  align-items: baseline;
  gap: 0.7rem;
  flex-wrap: wrap;
  margin-bottom: 2.2rem; /* stand the stamp well clear of the headline */
  font-family: var(--font-term);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.hero-ver {
  border: 2px solid var(--line);
  line-height: 1;
  padding: 0.36rem 0.5rem 0.18rem;
  color: var(--amber);
  background: rgba(6, 9, 6, 0.55);
}

.hero-title {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 5.5vw, 3rem);
  line-height: calc(1.15em + 2px);
  color: var(--amber);
  /* Two glows around the glyphs, then a cast shadow under them. The headline
     types over moving footage, so a dark green offset gives the strokes an
     edge to sit on when a bright frame passes behind. Green-tinted rather
     than neutral black, which would grey the amber down where the two meet.
     Listed last because an earlier shadow paints over a later one, and the
     glow has to stay on top of the cast rather than under it. */
  text-shadow:
    0 0 4px rgba(143, 230, 174, 0.24),
    0 0 18px rgba(143, 230, 174, 0.08),
    0 2px 5px rgba(5, 22, 13, 0.55),
    0 5px 18px rgba(5, 22, 13, 0.38);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.hero-title .line2 { color: var(--amber-hi); }

/* the headline always breaks where the markup says, never mid-line */
.hero-title .hero-line { white-space: nowrap; }

/* Typing effect: the ghost holds the real text so it reserves exact height at
   any width and stays readable to screen readers; the typed copy overlays it.
   Without JS `is-typing` never lands and the ghost just renders normally. */
.hero-title[data-type],
.sec-head.is-typing,
.sec-head.typed { position: relative; }
.is-typing .type-ghost { opacity: 0; }
.type-out {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* the ghost's trailing caret only reserves the room the live one will need */
.type-ghost .type-caret { visibility: hidden; animation: none; }

/* One BASIC cursor for every terminal-voiced heading, hero h1 and the section
   heads share this element so the blob reads identical at any size. It rides
   inside a .nobr tail span (see main.js) so a wrap can never orphan it onto a
   line of its own. */
.nobr { white-space: nowrap; }

.type-caret {
  display: inline-block;
  /* Pixel Operator 8 has capHeight 0.875em and no descender overshoot, and an
     empty inline-block rests its bottom edge on the baseline, so a 0.875em
     block matches the capitals exactly with no vertical nudging. Width keeps
     the CPC cursor's 1 : 1.2 w:h so it reads as a cursor, not a square. */
  /* One character cell, not just the cap band: it runs a touch above the caps
     and dips below the baseline, the way a block cursor fills its slot. */
  width: 0.73em;
  height: 1em;
  /* Was -0.08em, which dropped the block well under the baseline and left it
     hanging below the caps it is supposed to sit beside. A full-cell cursor
     should dip only a hair under the baseline, the way a firmware cursor
     overshoots its own row. */
  vertical-align: -0.02em;
  margin-left: 0.18em;
  background: var(--amber-hi);
  /* Same falloff radii as .hero-title's text-shadow, but stronger alphas: a
     text-shadow accumulates across many thin glyph strokes, while this only
     rims one solid block, so matching the numbers reads far weaker. */
  box-shadow:
    0 0 4px rgba(143, 230, 174, 0.4),
    0 0 11px rgba(143, 230, 174, 0.2),
    0 0 20px rgba(143, 230, 174, 0.1);
  animation: caret-burn 1.6s ease-out infinite;
}

/* Amstrad rhythm, slowed down: the CPC firmware's default flash period is 10
   frame flybacks on and 10 off, 1/5s each at 50Hz, a 0.4s cycle
   (SPEED INK 10,10), stretched 4x here, which reads calm at heading size
   while keeping the lit/dark ratio. Within the cycle: instant strike on the loop
   wrap, quick phosphor decay, then a dark hold. Opacity carries the
   box-shadow down with the block. */
@keyframes caret-burn {
  0%, 50% { opacity: 1; }
  62% { opacity: 0.28; }
  75%, 100% { opacity: 0.04; }
}

.hero-sub {
  margin-top: 1.2rem;
  max-width: 34rem;
  font-size: 1rem;
  color: var(--text);
}

.hero-actions {
  margin-top: 2rem;
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}

/* ---------- buttons ---------- */

.btn {
  position: relative;
  font-family: var(--font-term);
  /* px, not rem: an 8px pixel face lands on fractional sizes at some root
     scales and the stems come out uneven. Fixed size keeps it clean. */
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1;
  padding: 0.95rem 1.5rem 0.85rem;
  border: 2px solid transparent; /* the drawn segments below supply the colour */
  background: transparent;
  color: var(--amber);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, box-shadow 0.25s ease;
}

/* Two L-shaped halves make the frame: one runs the top then down the right,
   the other the bottom then up the left. Sat still they read as one box; on
   .btn--draw they sweep out from opposite corners the way the game's panels
   strike their borders in. */
.btn::before,
.btn::after {
  content: "";
  position: absolute;
  box-sizing: border-box;
  width: calc(100% + 4px);
  height: calc(100% + 4px);
  pointer-events: none;
}

.btn::before {
  top: -2px;
  left: -2px;
  border-top: 2px solid currentColor;
  border-right: 2px solid currentColor;
}

.btn::after {
  bottom: -2px;
  right: -2px;
  border-bottom: 2px solid currentColor;
  border-left: 2px solid currentColor;
}

/* armed: waiting its turn, so neither frame nor label is on screen yet */
.btn--armed::before,
.btn--armed::after { width: 2px; height: 2px; }

.btn--armed .btn-label { clip-path: inset(-30% 100% -30% 0); }

.btn--draw::before,
.btn--draw::after { animation: btn-draw 0.42s steps(9) both; }

@keyframes btn-draw {
  0% { width: 2px; height: 2px; }
  55% { width: calc(100% + 4px); height: 2px; }
  100% { width: calc(100% + 4px); height: calc(100% + 4px); }
}

/* the label types in behind the frame, one character cell at a time */
.btn-label { display: inline-block; }

/* backwards, not both: the finished state is the button's own unclipped
   style, so a browser that parks a steps() fill on its last step cannot
   leave the label a character short */
.btn--draw .btn-label {
  animation: btn-type 0.34s steps(var(--chars, 10)) 0.34s backwards;
}

@keyframes btn-type {
  from { clip-path: inset(-30% 100% -30% 0); }
  to { clip-path: inset(-30% 0 -30% 0); }
}

.btn:hover {
  background: var(--amber);
  color: var(--void-deep);
  /* tight core + wide soft halo */
  box-shadow:
    0 0 10px rgba(143, 230, 174, 0.35),
    0 0 26px rgba(143, 230, 174, 0.18);
}

.btn--ember {
  /* colour only: a real border here would paint the frame before it draws */
  color: var(--ember);
}

.btn--ember:hover {
  background: var(--ember);
  color: var(--void-deep);
  box-shadow:
    0 0 10px rgba(217, 106, 85, 0.4),
    0 0 26px rgba(217, 106, 85, 0.2);
}

/* ---------- section headers as terminal prompts ---------- */

.sec-head {
  /* ModeNine, same voice as the home page's body and boot screen. The pixel
     face (--font-term) stays on the terminal chrome and the ledger digits. */
  font-family: var(--font-body);
  /* ModeNine ships one weight; an h2's default bold would be synthesized by
     smearing the pixels a stroke wider. Normal is the face as drawn. */
  font-weight: 400;
  font-size: clamp(1.3rem, 3vw, 1.9rem);
  color: var(--amber);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  /* just enough halo to read as phosphor, not as a lamp */
  text-shadow:
    0 0 3px rgba(143, 230, 174, 0.2),
    0 0 12px rgba(143, 230, 174, 0.07);
  line-height: 1.25;
  margin-bottom: 1.5rem;
}

/* a heading that opens a new subject inside a section, rather than
   continuing one: it needs the air a section break would have given it */
.sec-head--gap { margin-top: clamp(4rem, 7vw, 6.5rem); }

.sec-sub {
  color: var(--text-dim);
  margin-bottom: 2.2rem;
  max-width: 40rem;
}

/* ---------- video frame (opening video placeholder) ---------- */

/* sections that centre their heading and sub; cards keep reading left */
.sec-center .wrap { text-align: center; }
.sec-center .sec-sub { margin-inline: auto; }
.sec-center .card { text-align: left; }

.video-frame {
  position: relative;
  /* Wide enough to read the HUD text in the footage, capped at the source's
     own 1280 so the frame never upscales what it is given. Still short of the
     full wrap, which keeps it a screen on the page rather than a takeover. */
  max-width: 1100px;
  margin-inline: auto;
  border: 2px solid var(--line);
  background: var(--void-deep);
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.video-frame img,
.video-frame video,
.video-frame iframe {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
}

.video-frame .rec {
  position: absolute;
  top: 14px;
  right: 40px;
  z-index: 3;
  font-family: var(--font-term);
  font-size: 0.85rem;
  color: var(--ember);
  letter-spacing: 0.15em;
}

.video-frame .rec::before {
  content: "●";
  margin-right: 0.4em;
  animation: blink 1.4s steps(1) infinite;
}

/* A real <button> since it loads the player on click, so the browser's own
   button chrome has to come off before the layout below reads the same. */
.play-overlay {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.8rem;
  background: rgba(5, 9, 6, 0.35);
  cursor: pointer;
  transition: background 0.2s;
}

.play-overlay:hover { background: rgba(5, 9, 6, 0.15); }

/* The script hides the button with the hidden attribute once the clip is
   running. An author display wins over the one the attribute carries, so the
   flex above would otherwise keep painting the triangle over the video. */
.play-overlay[hidden] { display: none; }

/* The line under the frame: length, sound warning, and where else it lives. */
.video-note {
  margin-top: 1rem;
  font-family: var(--font-term);
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  color: var(--text-dim);
}

/* keyboard users get the ring; a mouse press does not light it up */
.play-overlay:focus-visible {
  outline: 2px solid var(--amber-hi);
  outline-offset: -4px;
}

.play-overlay .tri {
  width: 0;
  height: 0;
  border-left: 34px solid var(--amber);
  border-top: 22px solid transparent;
  border-bottom: 22px solid transparent;
  filter: drop-shadow(0 0 10px rgba(143, 230, 174, 0.6));
}

.play-overlay .label {
  font-family: var(--font-term);
  font-size: 1rem;
  letter-spacing: 0.2em;
  color: var(--amber-hi);
  text-transform: uppercase;
}

/* ---------- feature grid ---------- */

/* Three columns, as the name says. It was auto-fit minmax(260px, 1fr), which
   is fine for the three cards on About but put all six Command Systems cards
   in a single row on a wide screen — columns too narrow to read and six
   different heights along the bottom. A fixed track count wraps them 3 + 3
   instead, and grid stretches every card in a row to the tallest, so the rows
   end level. */
.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1.2rem;
}

@media (max-width: 900px) {
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
  .grid-3 { grid-template-columns: 1fr; }
}

.card {
  border: 2px solid var(--line);
  background: var(--panel);
  padding: 1.6rem;
}

.card h3 {
  font-family: var(--font-term);
  font-size: 1.15rem;
  color: var(--amber);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 0.6rem;
}

.card .sys {
  font-family: var(--font-term);
  font-size: 0.8rem;
  color: var(--teal);
  letter-spacing: 0.15em;
  display: block;
  margin-bottom: 0.8rem;
}

.card p { color: var(--text-dim); font-size: 0.95rem; }

/* ---------- what-is split (copy left, screenshots right) ---------- */

.split {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: 3rem;
  align-items: center; /* copy block centered against the taller shot grid */
}

.split-copy p {
  color: var(--text-dim);
  max-width: 34rem;
  margin-bottom: 1rem;
}

.split-copy p:last-child { margin-bottom: 0; }

.shot-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

.shot {
  border: 2px solid var(--line);
  background: var(--void-deep);
  overflow: hidden;
}

.shot img {
  display: block;
  width: 100%;
  /* the width/height attributes on the tag are there to reserve space, but
     their presentational height would beat aspect-ratio without this */
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* the whole tile image is the zoom trigger */
.shot-zoom {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
  position: relative;
}

.shot-zoom img { transition: filter 0.2s ease; }
.shot-zoom:hover img,
.shot-zoom:focus-visible img { filter: brightness(1.18); }

/* enlarge hint, only on hover or keyboard focus */
.shot-zoom::after {
  content: "[ ENLARGE ]";
  position: absolute;
  right: 0.5rem;
  bottom: 0.5rem;
  font-family: var(--font-term);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--amber);
  background: rgba(6, 9, 6, 0.8);
  padding: 0.15rem 0.4rem;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.shot-zoom:hover::after,
.shot-zoom:focus-visible::after { opacity: 1; }

/* ---------- lightbox ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  padding: clamp(1rem, 4vw, 3rem);
  background: rgba(4, 8, 5, 0.94);
  cursor: zoom-out;
}

.lightbox[hidden] { display: none; }

/* arrows either side of the frame; they collapse to nothing when the entry
   has a single frame to show */
.lightbox-frame {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: stretch;
}

.lightbox-step {
  flex: none;
  display: none;
  align-items: center;
  justify-content: center;
  width: clamp(44px, 6vw, 72px);
  background: transparent;
  border: 0;
  color: var(--teal);
  cursor: pointer;
  padding: 0;
}

.lightbox.has-steps .lightbox-step { display: flex; }

.lightbox-step svg { image-rendering: pixelated; }

.lightbox-step:hover { color: var(--amber-hi); }

.lightbox-stage {
  position: relative;
  display: block;
  line-height: 0;
  border: 2px solid var(--line);
}

.lightbox img {
  max-width: min(100%, 1600px);
  max-height: 78vh;
  width: auto;
  cursor: default;
}

/* the decode canvas covers the shot only while the mosaic is resolving */
.lightbox canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  opacity: 0;
  pointer-events: none;
}

.lightbox.decoding canvas { opacity: 1; }
.lightbox.decoding img { visibility: hidden; }

.lightbox figcaption {
  font-family: var(--font-term);
  font-size: 0.85rem;
  letter-spacing: 0.12em;
  color: var(--teal);
}

.lightbox-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
}

/* close / step controls share the terminal button look */
.lightbox button {
  font-family: var(--font-term);
  font-size: 11px;
  letter-spacing: 0.1em;
  line-height: 1;
  color: var(--amber);
  background: transparent;
  border: 2px solid var(--line);
  padding: 0.55rem 1rem 0.45rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.lightbox button:hover {
  background: var(--amber);
  color: var(--void-deep);
}

.lightbox-close {
  position: absolute;
  top: clamp(0.75rem, 2vw, 1.5rem);
  right: clamp(0.75rem, 2vw, 1.5rem);
}

.lightbox-steps { display: flex; gap: 0.6rem; }

.shot figcaption {
  font-family: var(--font-term);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  color: var(--teal);
  padding: 0.45rem 0.6rem;
  border-top: 2px solid var(--line);
}

/* stagger the fade so the four shots arrive in reading order */
.shot-grid .reveal:nth-child(2) { transition-delay: 0.12s; }
.shot-grid .reveal:nth-child(3) { transition-delay: 0.24s; }
.shot-grid .reveal:nth-child(4) { transition-delay: 0.36s; }

@media (max-width: 860px) {
  .split { grid-template-columns: 1fr; gap: 2rem; }
  .split-copy p { max-width: none; }
}

/* ---------- posts (devlog + blog) ---------- */

/* posts read as a card wall: one up on phones, two across, three once there
   is room. Used by the devlog, the blog, and the home page's latest three. */
.post-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.6rem;
  align-items: stretch;
}

@media (min-width: 720px) {
  .post-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1280px) {
  .post-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* the way on to the rest of the log sits at the end of the wall, on the
   side the eye leaves it */
.log-more {
  margin-top: 2rem;
  text-align: right;
}

.post {
  border: 2px solid var(--line);
  background: var(--panel);
  margin-bottom: 2.2rem;
}

/* inside the grid the gap does the spacing */
.post-grid .post { margin-bottom: 0; }

/* Centred, not baseline-aligned: the stamp is a filled chip and the date is
   bare text, and a shared baseline leaves the chip's fill hanging low against
   it. Both sides run at line-height 1 in the same pixel face, so centring the
   two boxes lands the glyphs on the same rows exactly — a baseline here was
   landing them a rounded half-pixel apart. */
.post-head {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  flex-wrap: wrap;
  padding: 0.9rem 1.1rem 0;
}

/* The build stamp is a filled chip, so its label has to sit in the middle of
   the fill rather than on a text baseline with whatever padding is left over.
   Inline-flex centres it against an explicit line box; the trailing letter-
   spacing is cancelled on the right, or the tracking after the last glyph
   makes the chip look padded harder on one side than the other. */
.post-tag {
  font-family: var(--font-term);
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  color: var(--void-deep);
  background: var(--amber);
  display: inline-flex;
  align-items: center;
  line-height: 1;
  padding: 0.32em 0.5em 0.32em 0.55em;
  text-indent: 0;
  text-transform: uppercase;
}

/* the tracking belongs between the glyphs, not after the last one */
.post-tag::after {
  content: "";
  margin-left: -0.14em;
}

.post-tag--ember { background: var(--ember); }

.post-date {
  font-family: var(--font-term);
  font-size: 0.8rem;
  line-height: 1; /* matches the chip, so the two centre on the same rows */
  color: var(--text-dim);
  letter-spacing: 0.1em;
}

.post h2 {
  font-family: var(--font-term);
  font-size: 1.15rem;
  /* a pixel face at this size sets tight: the uppercase card titles run two
     and three lines and need the air to stay legible */
  line-height: 1.75;
  /* green at rest, brightening under the cursor: the card lights up on
     hover rather than dimming into it */
  color: var(--amber);
  letter-spacing: 0.04em;
  padding: 0.9rem 1.1rem 0;
  text-transform: uppercase;
}

.post-body { padding: 0.6rem 1.1rem 1.1rem; }

/* Devlog copy is quoted commit text: identifiers like _draw_preview_feed_
   and paths carry no break opportunity, and one of them is wider than a
   phone column, which pushed the whole card past the viewport. */
.post h2,
.post-body p,
.post-commits li { overflow-wrap: anywhere; }

.post-body p {
  margin-bottom: 0.9rem;
  color: var(--text-soft);
  font-size: 0.92rem;
  line-height: 1.65;
}
.post-body p:last-child { margin-bottom: 0; }

.post-media {
  margin: 1.2rem 0;
  border: 2px solid var(--line-dim);
  background: var(--void-deep);
}

/* height:auto or the tag's presentational height wins and the frame comes
   out stretched: the width/height attributes are there to reserve space
   while it loads, not to size it. Same trap as .shot img above. */
.post-media img {
  width: 100%;
  height: auto;
}

.post-media video,
.post-media iframe {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  border: 0;
}

.post-media figcaption {
  font-family: var(--font-term);
  font-size: 0.85rem;
  color: var(--text-dim);
  letter-spacing: 0.08em;
  padding: 0.5rem 0.9rem;
  border-top: 2px solid var(--line-dim);
}

.post-media figcaption::before { content: "// "; color: var(--teal); }

/* ---------- devlog: the sweep's own record of a day ----------
   Version and count chips, the day's commit list, and the verbatim engine
   output for builds that fell over. A devlog about building a game with AI
   is more honest with the bad days visible, so they are folded, not cut. */

.post-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 1rem;
}

.post-chip {
  font-family: var(--font-term);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  border: 1px solid var(--line-dim);
  padding: 0.22rem 0.5rem;
  line-height: 1;
}

.post-chip--ver {
  color: var(--amber);
  border-color: var(--line);
}

/* The day's commits read as a plain list of what got done: no hashes, no
   counts, nothing that only means something inside the repo. */
.post-commits {
  list-style: none;
  margin: 0 0 1.4rem;
  padding: 0;
}

.post-commits li {
  font-family: var(--font-term);
  font-size: 0.88rem;
  /* a wrapped bullet line sits on the same rhythm as body copy; only the
     gap BETWEEN bullets is bigger, so each item reads as one block */
  line-height: 1.65;
  color: var(--text-soft);
  margin-bottom: 0.85rem;
  padding-left: 1.2rem;
  position: relative;
}
.post-commits li:last-child { margin-bottom: 0; }

.post-commits li::before {
  content: "-";
  position: absolute;
  left: 0;
  color: var(--teal);
}

.post-commits-more { color: var(--line); font-style: italic; }

.post-errors {
  border: 1px solid var(--line-dim);
  margin-top: 1.2rem;
}

.post-errors summary {
  font-family: var(--font-term);
  font-size: 0.72rem;
  letter-spacing: 0.09em;
  color: var(--ember);
  padding: 0.5rem 0.7rem;
  cursor: pointer;
}

.post-errors ul {
  list-style: none;
  padding: 0 0.7rem 0.7rem;
  margin: 0;
}

.post-errors li {
  font-family: var(--font-term);
  font-size: 0.7rem;
  line-height: 1.5;
  color: var(--text-dim);
  /* engine errors are long single lines: let them scroll, never widen the card */
  overflow-x: auto;
  white-space: pre;
  margin-bottom: 0.3rem;
}

/* nothing published yet: a card that says so and stays out of the way */
.post-placeholder {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 180px;
  padding: 2rem 1.2rem;
  border-style: dashed;
  background: transparent;
  cursor: default;
}

.post-placeholder p {
  font-family: var(--font-term);
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-align: center;
}

/* ---------- devlog card ----------
   A card is a trailer for the day, not the day: cover, title, version, and
   three lines that stop mid-thought. The rest opens in the reader. */

/* Every card is the height of the tallest one in its row, so the wall reads
   as a grid rather than a ragged stack: the excerpt takes the slack and the
   read-more sits on the bottom edge whatever the title did. */
.post-card {
  cursor: pointer;
  transition: border-color 0.2s ease, box-shadow 0.25s ease;
  display: flex;
  flex-direction: column;
  height: 100%;
}

.post-card .post-body {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.post-more {
  font-family: var(--font-term);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--amber);
  border-top: 2px solid var(--line-dim);
  padding: 0.7rem 1.1rem;
  margin-top: auto;
  text-align: right;
}

.post-more::after { content: " >"; color: var(--teal); }

.post-card:hover .post-more,
.post-card:focus-visible .post-more { color: var(--amber-hi); }

.post-card:hover,
.post-card:focus-visible {
  border-color: var(--amber);
  box-shadow: 0 0 18px rgba(143, 230, 174, 0.14);
}

.post-card:hover h2,
.post-card:focus-visible h2 { color: var(--amber-hi); }

.post-cover {
  border-bottom: 2px solid var(--line-dim);
  background: var(--void-deep);
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.post-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* three lines, then it trails off */
.post-excerpt {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

.post-card .post-chips { margin-bottom: 0.6rem; }

/* ---------- layout switch ---------- */

.view-switch {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  margin-bottom: 1.2rem;
}

.view-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--font-term);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: transparent;
  border: 2px solid var(--line-dim);
  padding: 0.4rem 0.7rem;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

/* the icons are drawn on a 24px pixel grid: keep them on whole pixels or
   the stems blur */
.view-btn svg {
  width: 16px;
  height: 16px;
  image-rendering: pixelated;
}

.view-btn:hover { color: var(--amber); border-color: var(--line); }

.view-btn.is-on {
  color: var(--void-deep);
  background: var(--amber);
  border-color: var(--amber);
}

/* ---------- devlog list view ----------
   Same cards, laid on their side: a thumbnail to recognise the day by and
   the title doing the work, so 45 days scan in one screen. */

.post-grid--list { grid-template-columns: 1fr; gap: 0.8rem; }

.post-grid--list .post-card {
  display: grid;
  grid-template-columns: minmax(0, 148px) minmax(0, 1fr);
  grid-template-areas:
    "cover head"
    "cover title"
    "cover body"
    "cover more";
  align-items: start;
  column-gap: 1.1rem;
}

.post-grid--list .post-cover {
  grid-area: cover;
  align-self: stretch;
  height: 100%;
  border-bottom: 0;
  border-right: 2px solid var(--line-dim);
  aspect-ratio: auto;
}

.post-grid--list .post-head { grid-area: head; padding: 0.9rem 1.1rem 0 0; }
.post-grid--list .post-card h2 { grid-area: title; padding: 0.9rem 1.1rem 0 0; }
.post-grid--list .post-body { grid-area: body; padding: 0.5rem 1.1rem 0 0; }
.post-grid--list .post-more {
  grid-area: more;
  border-top: 0;
  padding: 0.4rem 1.1rem 0.8rem 0;
}

/* A row is one fixed height whatever the title did, so the list scans as a
   ledger rather than a stack of different-sized slabs. Everything inside is
   clamped to fit that height: two lines of title, two of copy. */
.post-grid--list { grid-auto-rows: 158px; }
.post-grid--list .post-card { height: 100%; overflow: hidden; }

.post-grid--list .post-card h2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* long measure is hard to read: hold the copy to a comfortable line length
   instead of letting it run the width of a 4K screen */
.post-grid--list .post-card h2,
.post-grid--list .post-body { max-width: 68ch; }

.post-grid--list .post-excerpt { -webkit-line-clamp: 2; line-clamp: 2; }

@media (max-width: 620px) {
  .post-grid--list { grid-auto-rows: 118px; }
  .post-grid--list .post-card {
    grid-template-columns: minmax(0, 96px) minmax(0, 1fr);
  }
  .post-grid--list .post-excerpt { display: none; }
}

/* ---------- devlog reader ----------
   The day opens over the page rather than on its own URL: same card of the
   browser, scrolls on its own, and ends on the days either side of it. */

/* a native <dialog>: the browser supplies the backdrop, the focus trap and
   the inert page behind it, so this only has to undo the default chrome */
.reader {
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  border: 0;
  padding: clamp(0.5rem, 4vh, 3rem) clamp(0.5rem, 3vw, 2rem);
  background: transparent;
  color: inherit;
  display: none;
  justify-content: center;
}

.reader[open] { display: flex; }

.reader::backdrop {
  background: rgba(3, 6, 4, 0.88);
  backdrop-filter: blur(3px);
}

body.reader-open { overflow: hidden; }

.reader-panel {
  position: relative;
  width: min(860px, 100%);
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--panel-solid);
  border: 2px solid var(--line);
  padding: clamp(1.2rem, 3vw, 2.4rem);
}

.reader-panel:focus { outline: none; }

.reader-close {
  position: sticky;
  top: 0;
  float: right;
  margin-left: 1rem;
  font-family: var(--font-term);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  color: var(--amber);
  background: var(--panel-solid);
  border: 0;
  cursor: pointer;
  padding: 0.2rem 0.1rem;
}

.reader-close:hover { color: var(--amber-hi); }

.reader-post .post-head { padding: 0 0 0.2rem; }

/* the era loses the stamp to the version, and carries on as a plain label */
.post-era {
  font-family: var(--font-term);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  color: var(--text-dim);
}

.post-era::before { content: "// "; color: var(--teal); }
.reader-post h2 { padding: 0.3rem 0 1rem; font-size: 2rem; line-height: 1.3; }
.reader-post p {
  color: var(--text-soft);
  font-size: 0.92rem;
  line-height: 1.65;
  margin-bottom: 0.9rem;
  overflow-wrap: anywhere;
}

.reader-sub {
  font-family: var(--font-term);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 1.4rem 0 0.7rem;
}

.reader-post .post-media { margin: 1rem 0; }

/* a devlog is a conversation, not a broadcast: every entry ends by saying
   how to keep up and how to answer back */
.reader-cta {
  font-family: var(--font-term);
  font-size: 0.72rem;
  line-height: 1.6;
  color: var(--text-dim);
  border-top: 2px solid var(--line-dim);
  margin-top: 1.6rem;
  padding-top: 1rem;
}

/* the days either side, the way a gallery hands you the next one */
.reader-nav {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
  margin-top: 2rem;
  border-top: 2px solid var(--line-dim);
  padding-top: 1.4rem;
}

@media (min-width: 620px) {
  .reader-nav { grid-template-columns: 1fr 1fr; }
}

/* Both steps read the same way round, left to right, whichever direction
   they go: mirroring one of them just makes it harder to scan. The arrow
   says which way it goes, and the thumbnail says which day. Both boxes
   are one height, and the cue sits on the bottom edge of each. */
.reader-nav { align-items: stretch; }

.reader-step {
  display: grid;
  grid-template-columns: 136px minmax(0, 1fr);
  gap: 0;
  align-items: stretch;
  min-height: 118px;
  text-align: left;
  background: transparent;
  border: 2px solid var(--line-dim);
  padding: 0;
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.2s ease;
}

.reader-step:hover { border-color: var(--amber); }

/* fills its column and crops to it, so the frame is never letterboxed
   and never squeezed */
.reader-step-thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-right: 2px solid var(--line-dim);
  background: var(--void-deep);
}

.reader-step-thumb--none { display: block; }

.reader-step-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 0.7rem 0.9rem;
}

/* pushed to the bottom of the box, so both cues sit the same distance
   off the border however long the titles ran */
.reader-step-cue {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: 0.5rem;
  font-family: var(--font-term);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--teal);
}

.reader-step-arrow {
  width: 14px;
  height: 14px;
  image-rendering: pixelated;
}

.reader-step:hover .reader-step-cue { color: var(--amber); }

/* two lines at most: a three-line title on one side and a one-line title
   on the other is what made the pair sit at different heights */
.reader-step-title {
  font-family: var(--font-term);
  font-size: 0.78rem;
  color: var(--amber-hi);
  line-height: 1.45;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* the date leads, and keeps its distance from the title */
.reader-step-date {
  font-family: var(--font-term);
  font-size: 0.62rem;
  color: var(--text-dim);
  letter-spacing: 0.1em;
  margin-bottom: 0.55rem;
}

/* ---------- full-res frame ----------
   A second <dialog> over the reader. The browser stacks it in the top
   layer, so the reader stays where it was and comes back when this
   closes. Fitted to the viewport on open; a click goes to actual pixels
   and the stage scrolls, dragged or not. */

.lightbox {
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  display: none;
  flex-direction: column;
}

.lightbox[open] { display: flex; }

.lightbox::backdrop {
  background: rgba(3, 6, 4, 0.96);
  backdrop-filter: blur(3px);
}

.lightbox-close {
  position: absolute;
  top: 0.6rem;
  right: 0.9rem;
  z-index: 2;
  font-family: var(--font-term);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  color: var(--amber);
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 0.3rem 0.2rem;
}

.lightbox-close:hover { color: var(--amber-hi); }

.lightbox-stage {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: auto;
  overscroll-behavior: contain;
  padding: clamp(0.6rem, 2vw, 1.6rem);
}

/* -webkit-user-drag off, or a drag on the frame is the browser's own
   picture drag - the file-drop cursor - instead of the pan */
.lightbox-img {
  max-width: 100%;
  max-height: 100%;
  border: 2px solid var(--line-dim);
  background: var(--void-deep);
  cursor: zoom-in;
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

/* a frame smaller than the stage has nothing to zoom to */
.lightbox.is-fitted .lightbox-img { cursor: default; }

/* Actual pixels: the stage becomes the window and the frame overflows it.
   Block layout, not flex - a flex item shrinks to its container, so at
   natural size the frame was squeezed back to the stage width and neither
   axis had anything to scroll. touch-action is ours because the drag below
   pans it; leaving it to the browser scrolled it twice per gesture. */
.lightbox-stage.is-zoomed {
  display: block;
  touch-action: none;
}

.lightbox-stage.is-zoomed .lightbox-img {
  display: block;
  max-width: none;
  max-height: none;
  width: auto;
  height: auto;
  cursor: grab;
}

.lightbox-stage.is-dragging .lightbox-img { cursor: grabbing; }

.lightbox-cap {
  flex: none;
  font-family: var(--font-term);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--text-dim);
  text-align: center;
  padding: 0.6rem 1rem 1rem;
}

/* the frames in the reader are the way in, so they say so */
.reader-post .post-media img { cursor: zoom-in; }

/* ---------- about page ---------- */

.spec-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-term);
  font-size: 0.95rem;
  margin: 1.5rem 0;
}

.spec-table td {
  border: 2px solid var(--line-dim);
  padding: 0.5rem 1rem;
  letter-spacing: 0.06em;
}

/* section heads for the credits: quieter than a sec-head, loud enough to
   break a long table into things you can find */
.credit-head {
  font-family: var(--font-term);
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--amber);
  margin: 3.4rem 0 0.9rem;
}

/* the first one follows its own intro, so it does not need the full gap */
.credit-head:first-of-type { margin-top: 2rem; }

.credit-head::before { content: "// "; color: var(--teal); }

/* the credits table runs long-form: links wrap, the label column holds */
.spec-table td a { text-decoration: underline; }
.spec-table td:last-child { overflow-wrap: anywhere; }

.spec-table td:first-child {
  color: var(--teal);
  text-transform: uppercase;
  white-space: nowrap;
  width: 1%;
}

.prose p { margin-bottom: 1.1rem; max-width: 46rem; }

/* ---------- contact ---------- */

.contact-grid {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: 2rem;
}

@media (max-width: 760px) {
  .contact-grid { grid-template-columns: 1fr; }
}

.field { margin-bottom: 1.2rem; }

/* The channel list starts level with the first input rather than with the
   label above it, so the two columns read as one row of boxes. The offset
   is the label's own height: 0.9rem of type on the 1.65 body leading, plus
   the 0.3rem beneath it. */
@media (min-width: 761px) {
  .contact-side { padding-top: calc(0.9rem * 1.65 + 0.3rem); }
}

.field label {
  display: block;
  font-family: var(--font-term);
  font-size: 0.9rem;
  letter-spacing: 0.06em;
  color: var(--teal);
  text-transform: uppercase;
  margin-bottom: 0.3rem;
}

.field input,
.field textarea {
  width: 100%;
  background: var(--void-deep);
  border: 2px solid var(--line);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  padding: 0.6rem 0.8rem;
}

.field textarea { min-height: 8rem; resize: vertical; }

/* Honeypot. Moved off-screen rather than display:none — some bots skip fields
   that are not rendered, and this one is meant to be found and filled. Lives
   here rather than in home.css: the contact form uses one too, and that page
   does not load home.css, so the trap was rendering in plain sight. */
.hs-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.form-status {
  font-family: var(--font-term);
  font-size: 0.95rem;
  color: var(--teal);
  margin-top: 0.8rem;
  min-height: 1.5em;
}

/* ---------- social links ---------- */

.social-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
}

.social-list a {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  min-width: 0;
  overflow-wrap: anywhere;
  font-family: var(--font-term);
  font-size: 1rem;
  letter-spacing: 0.05em;
  color: var(--text);
  border: 2px solid var(--line-dim);
  padding: 0.6rem 1rem;
  transition: border-color 0.15s, color 0.15s;
}

.social-list a:hover {
  border-color: var(--amber);
  color: var(--amber);
}

.social-list svg {
  width: 20px;
  height: 20px;
  fill: currentColor;
  flex-shrink: 0;
}

/* ---------- footer ---------- */

.site-footer {
  border-top: 2px solid var(--line);
  background: rgba(5, 9, 6, 0.85);
  padding: 3rem 0 1.5rem;
}

.footer-grid {
  display: grid;
  /* the middle column carries the site map, now two files wide */
  grid-template-columns: 1.1fr 1.6fr 1.1fr;
  gap: 2.5rem;
  margin-bottom: 2.2rem;
}

@media (max-width: 820px) {
  .footer-grid { grid-template-columns: 1fr; gap: 2rem; }
}

.footer-logo {
  display: block;
  height: 56px;
  width: auto;
  margin-bottom: 1rem;
}

.footer-brand p {
  color: var(--text-dim);
  font-size: 0.95rem;
  max-width: 26rem;
}

.footer-head {
  font-family: var(--font-term);
  font-size: 1.05rem;
  letter-spacing: 0.08em;
  color: var(--teal);
  text-transform: uppercase;
  margin-bottom: 0.9rem;
}

/* Two columns, filled down rather than across: the map is read as a list, and
   row-wise fill would put Home next to Opening Transmission and break the
   order. The row count is fixed rather than derived — CSS cannot count the
   items — so nine links land 5 + 4. Add a tenth and it evens out at 5 + 5;
   an eleventh opens a third column, which is the point to raise this number. */
.footer-map {
  list-style: none;
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(5, auto);
  /* the column gap has to clear the "//" markers by more than the space
     between two words, or the second file reads as a wrapped first one */
  gap: 0.45rem 3.2rem;
  justify-content: start;
}

/* On a phone the columns are stacked already and the labels are long enough
   to wrap against each other, so the map goes back to one file. */
@media (max-width: 520px) {
  .footer-map {
    grid-auto-flow: row;
    grid-template-rows: none;
  }
}

.footer-map a {
  font-family: var(--font-term);
  font-size: 0.95rem;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  text-transform: uppercase;
}

.footer-map a::before { content: "// "; color: var(--line); }

.footer-map a:hover { color: var(--amber-hi); text-shadow: var(--glow-amber); }

.footer-bottom {
  border-top: 2px solid var(--line-dim);
  padding-top: 1.2rem;
  font-family: var(--font-term);
  font-size: 0.62rem;
  color: var(--text-dim);
  letter-spacing: 0.1em;
}

/* ---------- mobile ---------- */

@media (max-width: 700px) {
  section { padding: 3rem 0; }

  .site-nav .wrap {
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 0.5rem;
    padding-block: 0.8rem;
  }

  /* Larger than the desktop wordmark's 46, not smaller: this is a compact
     mark rather than a line of type, so it needs the height to carry the
     same weight in the bar. */
  .nav-logo img { height: 54px; }

  .site-nav .wrap { justify-content: space-between; }

  /* Everything below is scoped to .js, and deliberately. The panel is only
     collapsed because scripting is there to open it again, so with JS off the
     links stay in the bar exactly as they did before and no button is offered
     that could not do anything. */
  .js .nav-toggle { display: inline-flex; }

  /* Fixed to the viewport rather than pushed into the bar, so opening the menu
     never changes nav.offsetHeight — main.js feeds that to --nav-h and the
     hero sizes against it, so a growing bar would resize the hero underneath
     the reader mid-tap.

     It starts below the bar, not over it: the button that closes this has to
     stay reachable, and covering it would leave a back gesture as the only way
     out. Height comes off --nav-h for the same reason, so the panel meets the
     bar exactly whatever the measured height turns out to be. */
  /* Absolute, not fixed. .site-nav carries a backdrop-filter, and a
     backdrop-filter establishes a containing block for FIXED descendants
     exactly as transform and filter do — so top/bottom resolved against the
     80px bar instead of the viewport and the panel collapsed to nothing but
     its own padding, which is why only the first row ever showed.

     Absolute resolves against that same bar, deliberately: the panel hangs off
     its bottom edge and states its height rather than inferring one from a
     containing block that is not the viewport. The bar is sticky, so this
     tracks it down the page. */
  .js .nav-links {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: calc(100vh - var(--nav-h, 71px));
    height: calc(100dvh - var(--nav-h, 71px));
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: 0.45rem;
    /* Heavier bottom padding rides the centred block upward, the same trick
       .hero uses. The inline padding is not decoration: overflow-y on its own
       computes overflow-x to auto rather than leaving it visible, so the panel
       clips horizontally too — which is what was shearing the left edge off
       the Contact frame, its border sitting flush against the boundary. */
    padding: 1.5rem 1.1rem max(5rem, env(safe-area-inset-bottom));
    overflow-y: auto;
    background: rgba(2, 4, 3, 0.97);
    -webkit-backdrop-filter: blur(14px) saturate(1.15);
    backdrop-filter: blur(14px) saturate(1.15);
  }

  /* The ground fades up, then the rows type themselves in on the stagger
     main.js already wrote onto each label for the bar's own reveal — same
     keyframes, same rhythm, so the menu opens in the site's voice rather than
     simply appearing. Re-triggered by nav-open matching and unmatching, so it
     replays on every open. */
  .js .site-nav.nav-open .nav-links {
    display: flex;
    animation: nav-panel-in 0.28s ease-out both;
  }

  .js .site-nav.nav-open .nav-links .nav-type {
    animation:
      nav-type 0.3s steps(var(--chars, 6)) calc(var(--delay, 0s) + 0.1s) backwards,
      echo-flash 0.3s ease-out calc(var(--delay, 0s) + 0.4s) both;
  }

  @keyframes nav-panel-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }

  /* full-width rows: the tap target is the line, not the word */
  .js .nav-links li { width: 100%; text-align: center; }

  /* The .nav-cta selector is repeated at the same weight in home.css, which
     loads after this file and would otherwise keep Contact at 11px while every
     other row went to 1.35rem. Naming it here outranks it. */
  .js .nav-links a,
  .js .nav-links a.nav-cta {
    display: block;
    font-size: 1.35rem;
    /* the row is the tap target, and a thumb wants it unmissable: this plus
       the list's gap puts roughly 60px between one label and the next */
    padding: 1.15rem 0;
  }

  /* Contact is the one framed row, and a border reads tighter than the plain
     rows either side of it — the same gap looks smaller once there is an edge
     drawn across it. So it gets its own air above, and shrinks to its label
     instead of running the full width, which keeps its frame away from the
     panel's clipping edge entirely. */
  .js .nav-links li:has(.nav-cta) { margin-top: 1.2rem; }

  .js .nav-links a.nav-cta {
    display: inline-block;
    width: auto;
    padding: 1.05rem 2rem;
  }

  /* The page must not scroll behind a full-height overlay: on a phone that
     reads as the menu itself moving, and leaves the reader somewhere else
     entirely when it closes. */
  body.nav-locked { overflow: hidden; }

  .nav-links {
    flex-basis: 100%;
    justify-content: center;
    gap: 0.7rem 1rem;
  }

  .nav-links li:first-child,
  .nav-links li:last-child { margin-left: 0; }

  .nav-links a { font-size: 0.72rem; }

  .hero { padding: 2rem 0 1.5rem; }

  /* The base 0.95/0.85 is set for a pointer. Under a thumb the control wants
     more height than a cursor ever needed, and the label more size — 11px of
     8-bit face is a desk measurement. Whole pixels only: this is a pixel font
     and fractional sizes come out with uneven stems. */
  .btn { padding: 1.2rem 1.6rem 1.1rem; font-size: 14px; }

  .hero-actions .btn {
    flex: 1 1 auto;
    text-align: center;
  }

  .card { padding: 1.2rem; }

  .post-head { padding: 1rem 1rem 0; }
  .post h2 { padding: 0.3rem 1rem 0; }
  .post-body { padding: 0.8rem 1rem 1.2rem; }

  .spec-table { font-size: 1rem; }
  .spec-table td { padding: 0.4rem 0.6rem; }

  .video-frame .rec { right: 34px; top: 10px; }

  .social-list a { font-size: 0.9rem; }
}

/* ---------- misc ---------- */

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

.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.reveal.in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal { opacity: 1; transform: none; transition: none; }
  .blink, .video-frame .rec::before, .type-caret, .nav-logo img { animation: none; }
  .btn--draw::before, .btn--draw::after, .btn--draw .btn-label { animation: none; }
  .nav-type { clip-path: none; animation: none; }
  .flash { animation: none; }
  .nav-toggle { animation: none; }
  .js .site-nav, .js .nav-logo img, .js .hero-bg,
  .js .site-nav:not(.nav-in) .nav-toggle { opacity: 1; }
  /* these carry more weight than the blanket .nav-type rule above, so they
     have to be named at their own specificity to be turned off */
  .js .site-nav.nav-open .nav-links,
  .js .site-nav.nav-open .nav-links .nav-type { animation: none; }
  .js .site-nav.nav-open .nav-links .nav-type { clip-path: none; }
}

/* ============================================================
   TERMINAL FX: every frame draws itself, every block of text
   prints itself, and both sign off with the same short echo.
   Classes are applied from main.js as elements scroll in.
   ============================================================ */

/* no slide-up any more, the draw and print effects carry the arrival */
.reveal { opacity: 1; transform: none; transition: none; }

.fx-frame {
  position: relative;
  border-color: transparent; /* the drawn halves below supply the line */
}

/* The halves sit inside the padding box, not hung 2px outside it: the shot
   tiles and the trailer window clip their overflow, which would have eaten a
   frame drawn beyond their edge. The element keeps its 2px transparent
   border, so the line lands exactly where the border used to sit. */
.fx-frame::before,
.fx-frame::after {
  content: "";
  position: absolute;
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
}

.fx-frame::before {
  top: 0;
  left: 0;
  border-top: 2px solid var(--frame-color, var(--line));
  border-right: 2px solid var(--frame-color, var(--line));
}

.fx-frame::after {
  bottom: 0;
  right: 0;
  border-bottom: 2px solid var(--frame-color, var(--line));
  border-left: 2px solid var(--frame-color, var(--line));
}

.fx-frame--armed::before,
.fx-frame--armed::after { width: 2px; height: 2px; }

/* A card used to arrive with its panel and its cover already on screen and
   only the border sweeping in, which read as a frame drawn around something
   that was always there. Now the whole tile waits: the frame strikes its
   corners, the contents come up behind it, and the cover signs off with the
   same echo every other element in the page ends on. */
.post-card.fx-frame--armed > * { visibility: hidden; }

.post-card.fx-frame--draw > * {
  animation: fx-appear 0.18s ease-out 0.4s backwards;
}

.post-card.fx-frame--draw .post-cover img {
  animation:
    fx-appear 0.14s steps(2) 0.42s backwards,
    echo-flash 0.4s ease-out 0.56s both;
}

.fx-frame--draw::before,
.fx-frame--draw::after { animation: fx-draw 0.5s steps(10) both; }

@keyframes fx-draw {
  0% { width: 2px; height: 2px; }
  55% { width: 100%; height: 2px; }
  100% { width: 100%; height: 100%; }
}

/* Waiting text hides with visibility, not clip-path: a clipped box reports a
   zero intersection rect, so an observer would never see it come into view. */
.fx-type--armed,
.fx-reveal--armed { visibility: hidden; }

/* single-line text types across, character by character */
.fx-type--go { animation: btn-type var(--dur, 0.4s) steps(var(--chars, 12)) backwards; }

/* a paragraph arrives whole, one after another, each with its own echo:
   printing a block line by line makes it unreadable while it runs */
.fx-reveal--go { animation: fx-appear 0.16s ease-out both; }

@keyframes fx-appear {
  from { opacity: 0; }
  to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .fx-frame--armed::before,
  .fx-frame--armed::after { width: calc(100% + 4px); height: calc(100% + 4px); }
  .fx-frame--draw::before,
  .fx-frame--draw::after,
  .fx-type--go,
  .fx-reveal--go { animation: none; }
  .fx-type--armed,
  .fx-reveal--armed { visibility: visible; }
  .post-card.fx-frame--armed > * { visibility: visible; }
  .post-card.fx-frame--draw > *,
  .post-card.fx-frame--draw .post-cover img { animation: none; }
}

/* ---------- consent banner ---------- */

/* A hailing panel in the corner rather than a bar across the foot of the
   screen: the footer already lives there, and a full-width strip over it hides
   the one control that reopens this. Bottom-left keeps it clear of the mobile
   menu button too. Not a modal — nothing on the page is gated on answering. */
.consent {
  position: fixed;
  left: 1.25rem;
  bottom: 1.25rem;
  z-index: 60;
  max-width: 25rem;
  opacity: 0;
  transform: translateY(0.75rem);
  transition: opacity 0.26s ease-out, transform 0.26s ease-out;
}

.consent.is-in {
  opacity: 1;
  transform: none;
}

.consent-panel {
  border: 2px solid var(--line);
  background: var(--panel-solid);
  padding: 1.1rem 1.2rem 1.2rem;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 12px 32px rgba(0, 0, 0, 0.55);
}

.consent-head {
  font-family: var(--font-term);
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--amber);
  text-shadow: var(--glow-amber);
  margin-bottom: 0.6rem;
}

.consent-body {
  font-family: var(--font-body);
  font-size: 0.8rem;
  line-height: 1.6;
  color: var(--text-soft);
}

.consent-actions {
  display: flex;
  gap: 0.6rem;
  margin-top: 1rem;
}

/* Sized down from .btn — this is an interruption, and full-size CTA framing
   would give it the weight of the page's own calls to action. */
.consent-btn {
  font-family: var(--font-term);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1;
  padding: 0.7rem 1.1rem 0.6rem;
  border: 2px solid var(--amber);
  background: transparent;
  color: var(--amber);
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.consent-btn:hover {
  background: var(--amber);
  color: var(--void-deep);
}

.consent-btn--ghost {
  border-color: var(--line);
  color: var(--text-dim);
}

.consent-btn--ghost:hover {
  background: transparent;
  border-color: var(--text-soft);
  color: var(--text-soft);
}

/* Sits in the copyright row and has to read as part of that line, not as a
   button parked in it. */
.consent-link {
  font: inherit;
  letter-spacing: inherit;
  border: 0;
  padding: 0;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.consent-link:hover { color: var(--amber); }

@media (max-width: 700px) {
  .consent {
    left: 0.75rem;
    right: 0.75rem;
    bottom: 0.75rem;
    max-width: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .consent {
    transition: none;
    opacity: 1;
    transform: none;
  }
}

/* ---------- legal pages ---------- */

.legal-stamp {
  font-family: var(--font-term);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* The reset strips list markers site-wide, which is right everywhere else —
   here the items are read as a list and need to look like one. Hyphen rather
   than a bullet: it is what the devlog's own lists use. */
.legal ul {
  list-style: none;
  margin: 0 0 1.1rem;
  max-width: 46rem;
}

.legal li {
  position: relative;
  padding-left: 1.3rem;
  margin-bottom: 0.55rem;
  line-height: 1.6;
}

.legal li::before {
  content: "-";
  position: absolute;
  left: 0;
  color: var(--teal);
}

.legal a { text-decoration: underline; text-underline-offset: 3px; }

/* Cookie and storage keys are literals a reader may have to match against
   what their browser shows them, so they get the terminal face. */
.legal code {
  font-family: var(--font-term);
  font-size: 0.82em;
  color: var(--amber);
}
