/* =============================================================================
   spongebong.dev
   -----------------------------------------------------------------------------
   The mix, deliberately: ~40% polished glass, 20% tech, 20% hacker, 20% metro.

     glass   translucent panels, real backdrop blur, a bright top edge and a
             moving sheen so surfaces read as lit rather than merely transparent
     tech    precise measurements, monospace metadata, thin hairline rules
     hacker  terminal green, scanlines, bracket cues, a cursor that blinks
     metro   hard-edged tiles, a strict grid, flat accent blocks, no fake depth

   Everything animated respects prefers-reduced-motion at the bottom of the file.
   ============================================================================= */

/* ---------------------------------------------------------------- tokens -- */
:root {
  color-scheme: dark;

  /* Base. Near-black with a blue cast, so accents read as light in a dark room. */
  --bg: #07070c;
  --bg-2: #0b0b14;
  --fg: #eef0f6;
  --muted: #a2a6bb;
  --dim: #6a6f87;

  /* Accents. Cyan leads, violet supports, lime is the hacker/terminal note. */
  --cyan: #4de3ff;
  --violet: #a583ff;
  --lime: #7dff9b;
  --amber: #ffc46b;
  --rose: #ff7ba8;
  --accent: var(--cyan);
  --accent-2: var(--violet);

  /* Glass. One place to retune every panel on the site. */
  --glass-bg: rgba(255, 255, 255, 0.045);
  /* Cards do not blur what is behind them, so they need a fill that stands on its own. */
  --card-bg: rgba(20, 22, 38, 0.62);
  --glass-bg-strong: rgba(255, 255, 255, 0.07);
  --glass-border: rgba(255, 255, 255, 0.12);
  --glass-edge: rgba(255, 255, 255, 0.5);
  --glass-blur: 20px;
  --glass-shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.9), 0 2px 10px -4px rgba(0, 0, 0, 0.6);

  --radius: 18px;
  --radius-sm: 10px;
  --radius-pill: 999px;

  /* Metro tiles are square-ish; glass panels are round. The contrast is the point. */
  --radius-tile: 4px;

  --gap: clamp(1rem, 2.5vw, 1.75rem);
  --pad: clamp(1.1rem, 2.5vw, 1.75rem);
  --shell: min(1180px, 92vw);

  --sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Per-card accent, set from data-accent in the markup. */
/* The shell's `theme` command sets this on <html>, so one command recolours the site. */
:root[data-theme="cyan"] { --accent: var(--cyan); --accent-2: var(--violet); }
:root[data-theme="violet"] { --accent: var(--violet); --accent-2: var(--rose); }
:root[data-theme="lime"] { --accent: var(--lime); --accent-2: var(--cyan); }
:root[data-theme="amber"] { --accent: var(--amber); --accent-2: var(--rose); }
:root[data-theme="rose"] { --accent: var(--rose); --accent-2: var(--violet); }

[data-accent="cyan"] { --accent: var(--cyan); }
[data-accent="violet"] { --accent: var(--violet); }
[data-accent="lime"] { --accent: var(--lime); }
[data-accent="amber"] { --accent: var(--amber); }
[data-accent="rose"] { --accent: var(--rose); }

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

html {
  scroll-behavior: smooth;
  /* The sticky navbar would otherwise cover an anchor target. */
  scroll-padding-top: 6.5rem;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.65 var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
}

main { flex: 1; }

h1, h2, h3 { line-height: 1.1; letter-spacing: -0.025em; margin: 0; text-wrap: balance; }
p { margin: 0; text-wrap: pretty; }
ul { margin: 0; padding: 0; list-style: none; }

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

.mono { font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.04em; }
.dim { color: var(--dim); }
.ok { color: var(--lime); }

/* A visible focus ring everywhere, because every surface here is interactive. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip {
  position: fixed;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 100;
  padding: 0.6rem 1rem;
  border-radius: var(--radius-sm);
  background: var(--bg-2);
  border: 1px solid var(--glass-border);
  transform: translateY(-200%);
  transition: transform 0.25s var(--ease-out);
}
.skip:focus { transform: translateY(0); }

/* ------------------------------------------------------------ background -- */
.bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  background:
    radial-gradient(120% 80% at 50% -10%, #12122a 0%, transparent 60%),
    linear-gradient(180deg, var(--bg-2), var(--bg) 60%);
}

/* Metro: a strict grid, dropped to a whisper so it reads as texture. */
.bg-grid {
  position: absolute;
  inset: -58px;   /* one tile of slack, so the drifting transform never reveals an edge */
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.038) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.038) 1px, transparent 1px);
  background-size: 56px 56px;
  /* Fades out toward the bottom so text lower on the page stays clean. */
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.35) 55%, transparent 100%);
  transform: translate3d(0, 0, 0);
  animation: grid-drift 40s linear infinite;
}

/* Animating background-position repaints the whole masked layer every frame. A transform on an
 * oversized layer moves the same pixels on the compositor instead, which costs nothing per
 * frame. The layer is inset by a tile so the shift never exposes an edge. */
@keyframes grid-drift {
  to { transform: translate3d(56px, 56px, 0); }
}

/* Two slow blooms give the glass something to actually refract. */
/* No filter: blur() here, deliberately.
 *
 * These are two 46vw circles that drift on a 26s loop. With a 90px blur the browser re-rendered
 * an enormous filtered surface every frame they moved, and hiding them alone took the page from
 * 137ms to 66ms per frame: half the total cost of the site, for something nobody looks directly
 * at. The gradient stop does the softening instead, which is free, and the result is close
 * enough that the difference is not visible behind the content sitting on top. */
.bg-bloom {
  position: absolute;
  width: 52vw;
  height: 52vw;
  min-width: 440px;
  min-height: 440px;
  border-radius: 50%;
  opacity: 0.75;
  will-change: transform;
}
.bg-bloom-a {
  top: -14vw;
  left: -6vw;
  background: radial-gradient(circle, rgba(77, 227, 255, 0.34), rgba(77, 227, 255, 0.10) 42%, transparent 70%);
  animation: bloom-a 26s var(--ease-out) infinite alternate;
}
.bg-bloom-b {
  bottom: -18vw;
  right: -8vw;
  background: radial-gradient(circle, rgba(165, 131, 255, 0.32), rgba(165, 131, 255, 0.09) 42%, transparent 70%);
  animation: bloom-b 32s var(--ease-out) infinite alternate;
}
@keyframes bloom-a { to { transform: translate3d(12vw, 8vh, 0) scale(1.15); } }
@keyframes bloom-b { to { transform: translate3d(-10vw, -8vh, 0) scale(1.2); } }

/* Hacker: CRT scanlines, barely there. Any more and it becomes a costume. */
.bg-scan {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.022) 0 1px,
    transparent 1px 3px
  );
  opacity: 0.6;
  mix-blend-mode: overlay;
}

.bg-traces { position: absolute; inset: 0; width: 100%; height: 100%; }

.bg-vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 100% at 50% 40%, transparent 45%, rgba(0, 0, 0, 0.55) 100%);
}

/* A light that follows the pointer, so glass looks lit from where you are looking.
   JS sets --mx/--my; without JS it simply stays at 0 opacity and nothing is missed. */
.cursor-glow {
  position: fixed;
  top: 0;
  left: 0;
  width: 520px;
  height: 520px;
  margin: -260px 0 0 -260px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  background: radial-gradient(circle, rgba(77, 227, 255, 0.13), transparent 62%);
  transform: translate3d(var(--mx, 50vw), var(--my, 50vh), 0);
  transition: opacity 0.5s ease;
}
body.has-pointer .cursor-glow { opacity: 1; }

/* ----------------------------------------------------------------- glass -- */
.glass {
  position: relative;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  box-shadow: var(--glass-shadow);
  backdrop-filter: blur(var(--glass-blur)) saturate(150%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%);
}

/* The bright top edge. This one line is most of why a panel reads as glass and
   not as a grey box with opacity. */
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(180deg, var(--glass-edge), rgba(255, 255, 255, 0.04) 35%, transparent 70%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: 0.55;
  pointer-events: none;
}

/* ---------------------------------------------------------------- navbar -- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  padding: clamp(0.6rem, 1.6vw, 1.1rem) 0;
  /* The bar floats; this container must not eat clicks on the page behind it. */
  pointer-events: none;
}

.nav {
  pointer-events: auto;
  width: var(--shell);
  margin: 0 auto;
  padding: 0.55rem 0.6rem 0.55rem 0.9rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  border-radius: var(--radius-pill);
  transition: background 0.35s var(--ease-out), border-color 0.35s var(--ease-out),
    box-shadow 0.35s var(--ease-out), transform 0.35s var(--ease-out);
}

/* Scrolled: the bar tightens and gains contrast, so content passing under it
   never fights the links for legibility. JS toggles the class. */
.nav.is-stuck {
  background: rgba(10, 10, 18, 0.72);
  border-color: rgba(255, 255, 255, 0.16);
  box-shadow: 0 18px 50px -22px rgba(0, 0, 0, 1);
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  padding: 0.2rem 0.35rem;
  border-radius: var(--radius-pill);
}

.brand-mark {
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  background: linear-gradient(135deg, rgba(77, 227, 255, 0.9), rgba(165, 131, 255, 0.9));
  box-shadow: 0 0 18px rgba(77, 227, 255, 0.45);
  transition: transform 0.5s var(--ease-spring), box-shadow 0.4s var(--ease-out);
}
.brand-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #07070c;
  transition: transform 0.5s var(--ease-spring);
}
.brand:hover .brand-mark { transform: rotate(90deg) scale(1.08); box-shadow: 0 0 26px rgba(165, 131, 255, 0.6); }
.brand:hover .brand-dot { transform: scale(1.5); }

.brand-text { display: inline-flex; align-items: baseline; }
.brand-name { color: var(--fg); }
.brand-tld { color: var(--accent); }

.nav-links {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.15rem;
  position: relative;
}

/* The travelling highlight behind the links. JS moves and sizes it; it is the
   single clearest signal of where you are, and it makes hover feel physical. */
.nav-ink {
  position: absolute;
  top: 50%;
  left: 0;
  height: 2.1rem;
  width: 0;
  translate: 0 -50%;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.09);
  border: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
  opacity: 0;
  transition: transform 0.42s var(--ease-spring), width 0.42s var(--ease-spring), opacity 0.25s ease;
  pointer-events: none;
}
.nav-links.ink-ready .nav-ink { opacity: 1; }

.nav-link {
  position: relative;
  padding: 0.45rem 0.95rem;
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-size: 0.94rem;
  font-weight: 520;
  transition: color 0.25s var(--ease-out);
}
.nav-link:hover { color: var(--fg); }
.nav-link.is-current { color: var(--fg); }

/* Hacker touch: the current page gets terminal brackets that ease in. */
.nav-link.is-current .nav-link-text::before,
.nav-link.is-current .nav-link-text::after {
  color: var(--accent);
  font-family: var(--mono);
  opacity: 0.85;
}
.nav-link.is-current .nav-link-text::before { content: "["; margin-right: 0.2rem; }
.nav-link.is-current .nav-link-text::after { content: "]"; margin-left: 0.2rem; }

.nav-toggle {
  display: none;
  margin-left: auto;
  width: 40px;
  height: 34px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
}
.nav-toggle-bar {
  display: block;
  width: 16px;
  height: 1.5px;
  background: var(--fg);
  transition: transform 0.3s var(--ease-out), opacity 0.2s ease;
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:first-child { transform: translateY(3.25px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:last-child { transform: translateY(-3.25px) rotate(-45deg); }

/* --------------------------------------------------------------- shell/l -- */

.section {
  width: var(--shell);
  margin: clamp(3.5rem, 9vw, 7rem) auto 0;
}

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: clamp(1.25rem, 3vw, 2rem);
}

.section-title {
  font-size: clamp(1.5rem, 3.4vw, 2.25rem);
  font-weight: 640;
}

/* Metro: a flat accent block, no gradient, hard edges. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--dim);
  margin-bottom: 0.7rem;
}
.eyebrow::before {
  content: "";
  width: 22px;
  height: 3px;
  background: var(--accent);
  border-radius: 1px;
}

.section-note { color: var(--dim); font-family: var(--mono); font-size: 0.78rem; }

/* ------------------------------------------------------------------ hero -- */
.hero {
  width: var(--shell);
  margin: clamp(2rem, 6vw, 4.5rem) auto 0;
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.85fr);
  gap: var(--gap);
  align-items: start;
}

/* When the terminal is closed the side column has nothing in it, and a fixed two-column
 * grid would hold the empty track open. Collapsing to one column keeps the page composed
 * rather than leaving a visible hole where the window used to be. */
.hero:has(.hero-side > .win[hidden]) { grid-template-columns: minmax(0, 1fr); }
/* The nudge points at a window that is no longer there, so retire it with the window. */
.hero:has(.hero-side > .win[hidden]) .hero-aside { display: none; }

.hero-main { padding: clamp(1.6rem, 4vw, 3rem); overflow: hidden; }

.hero-title {
  font-size: clamp(2.6rem, 8vw, 5rem);
  font-weight: 700;
  letter-spacing: -0.04em;
  margin-bottom: 1rem;
}

/* The gradient sweeps slowly, so the headline is alive without moving. */
.hero-title .grad {
  background: linear-gradient(100deg, var(--cyan), var(--violet) 45%, var(--lime) 90%);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: grad-slide 9s ease-in-out infinite alternate;
}
@keyframes grad-slide { to { background-position: 100% 0; } }

.hero-lede {
  font-size: clamp(1rem, 1.6vw, 1.18rem);
  color: var(--muted);
  max-width: 46ch;
  margin-bottom: 1.6rem;
}

/* Hacker: a prompt line that types itself out, with a blinking block cursor. */
.term-prompt { color: var(--dim); }

.hero-actions { display: flex; flex-wrap: wrap; gap: 0.7rem; }

/* --------------------------------------------------------------- buttons -- */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.7rem 1.25rem;
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-pill);
  background: var(--glass-bg-strong);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--fg);
  font: 500 0.92rem/1 var(--sans);
  cursor: pointer;
  overflow: hidden;
  transition: transform 0.3s var(--ease-spring), border-color 0.3s var(--ease-out),
    box-shadow 0.3s var(--ease-out), background 0.3s var(--ease-out);
}
.btn:hover {
  transform: translateY(-2px);
  border-color: rgba(255, 255, 255, 0.28);
  box-shadow: 0 14px 34px -14px rgba(0, 0, 0, 0.9);
}
.btn:active { transform: translateY(0) scale(0.985); }

/* A light sweep across the button on hover: the "polished" in polished glass. */
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(110deg, transparent 20%, rgba(255, 255, 255, 0.22) 50%, transparent 80%);
  transform: translateX(-120%);
  transition: transform 0.7s var(--ease-out);
}
.btn:hover::after { transform: translateX(120%); }

.btn-primary {
  /* Built from the accent tokens, not from literals, so `theme` in the shell reaches it. */
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--accent) 90%, transparent),
    color-mix(in srgb, var(--accent-2) 88%, transparent));
  border-color: transparent;
  color: #05050a;
  font-weight: 620;
  box-shadow: 0 10px 34px -12px color-mix(in srgb, var(--accent) 65%, transparent);
}
.btn-primary:hover { box-shadow: 0 18px 46px -14px rgba(120, 180, 255, 0.8); }

.btn-arrow { transition: transform 0.35s var(--ease-spring); }
.btn:hover .btn-arrow { transform: translateX(4px); }

/* ------------------------------------------------------------- stat tiles -- */
/* Metro: square tiles, flat fills, a hard grid. */
/* The terminal stretches to the hero's height rather than sitting short with dead space
 * under it. align-self is on the child because the grid uses align-items: start. */
.hero-side { display: grid; gap: var(--gap); align-self: stretch; }
.hero-side > .term { align-self: stretch; display: flex; flex-direction: column; }
/* The body is a grid, so letting it flex-grow stretched every row and spaced the lines
 * out like a poem. Grow the box but keep the rows their natural height, pinned to the top. */
.hero-side > .term .term-body { flex: 1; max-height: none; align-content: start; }

/* The nudge toward the terminal. Without it, most people never realise it is real. */
.hero-aside {
  display: flex;
  align-items: center;
  flex-wrap: wrap;   /* the kbd hint pushed this a few px past its container at 390px */
  gap: 0.5rem;
  margin: 0 0 1.1rem;
  color: var(--dim);
  font-family: var(--mono);
  font-size: 0.78rem;
}
.hero-aside kbd {
  display: inline-block;
  padding: 0.05rem 0.34rem;
  border-radius: 4px;
  border: 1px solid var(--glass-border);
  border-bottom-width: 2px;
  background: rgba(255, 255, 255, 0.07);
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.72rem;
  line-height: 1.5;
}

.hero-aside-arrow {
  color: var(--accent);
  animation: aside-point 2.4s ease-in-out infinite;
}
@keyframes aside-point {
  0%, 100% { transform: translateX(0); opacity: 0.65; }
  50% { transform: translateX(4px); opacity: 1; }
}

/* now.txt rows: a label and a plain sentence, not a spec table. */
.now-line { display: flex; gap: 0.6rem; margin: 0 0 0.6rem; align-items: baseline; }
.now-key {
  flex: none;
  min-width: 5.2rem;
  color: var(--accent);
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
}




/* path.log: the order things were learned in, as a timeline down the sidebar rather than a
   list of languages in a row. A row of chips would be the stack-as-personality thing the whole
   site is trying not to do; a dated path is a story about someone. */
.panel-lede {
  margin: 0 0 1rem;
  color: var(--muted);
  font-size: 0.86rem;
}
.path { list-style: none; margin: 0; padding: 0 0 0 1.1rem; position: relative; }
/* The spine is a pseudo-element on the list rather than a border on each item, so it stops at
   the last marker instead of running past it into the panel padding. */
.path::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 0.45rem;
  bottom: 0.5rem;
  width: 1px;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--accent) 45%, transparent),
    color-mix(in srgb, var(--accent) 8%, transparent));
}
.path-step { position: relative; margin: 0 0 0.95rem; }
.path-step:last-child { margin-bottom: 0; }
.path-step::before {
  content: '';
  position: absolute;
  left: -1.1rem;
  top: 0.42rem;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--bg);
  border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent);
}
.path-step.is-now::before {
  background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.path-when {
  display: block;
  color: var(--accent);
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  opacity: 0.85;
}
.path-what { display: block; font-size: 0.92rem; line-height: 1.5; }

/* Inline code in prose. Named commands were rendering as bare monospace, which read as an
   accident of the font stack rather than as a deliberate reference to a program. */
.prose code {
  font-family: var(--mono);
  font-size: 0.86em;
  padding: 0.08em 0.36em;
  border-radius: 5px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 18%, transparent);
  color: color-mix(in srgb, var(--accent) 82%, #fff);
}

/* A small live terminal panel, for the tech/hacker share of the mix. */
.term {
  padding: 0;
  overflow: hidden;
  font-family: var(--mono);
  font-size: 0.79rem;
}
.term-bar {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.6rem 0.85rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.03);
}
.term-name { margin-left: 0.4rem; color: var(--dim); font-size: 0.72rem; }
.term-body {
  padding: 0.85rem;
  display: grid;
  gap: 0.32rem;
  color: var(--muted);
  /* Capped so a long session scrolls inside the window instead of stretching the page. */
  max-height: 19rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.16) transparent;
}
.term-body b { color: var(--accent); font-weight: 500; }
.term-body .out { color: var(--dim); white-space: pre-wrap; }
.term-body .out.dim { color: rgba(255, 255, 255, 0.32); }
.term-body .out.err { color: var(--rose); }
.term-body .term-echo { color: var(--text); }
.term-path { color: var(--accent-2); }

/* The prompt row. It is a real form, so Enter submits and the browser does the work. */
.term-form {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.85rem 0.75rem;
  border-top: 1px solid rgba(255, 255, 255, 0.07);
}
.term-form .term-prompt {
  display: flex;
  gap: 0.15rem;
  white-space: nowrap;
  color: var(--dim);
  font-size: 0.76rem;
}
.term-form .term-prompt b { color: var(--accent); font-weight: 500; }
.term-input {
  flex: 1;
  min-width: 0;
  background: none;
  border: 0;
  padding: 0;
  color: var(--text);
  font: inherit;
  caret-color: var(--accent);
}
.term-input:focus { outline: none; }
/* The prompt is the main thing to touch on this page, and line-height alone left it at 21px.
 * Padding the row rather than the input keeps the caret aligned with the prompt text. */
.term-form { min-height: 44px; flex-wrap: wrap; }
.term-input { min-height: 24px; min-width: 8ch; }
/* On a very narrow screen the prompt label ate the row and left the input 8px wide, which is
 * unusable. Below this width the prompt sits on its own line and the field gets the full row. */
@media (max-width: 340px) {
  .term-form .term-prompt { flex-basis: 100%; }
  .term-input { flex-basis: 100%; }
}
/* The window itself shows focus, since the input has no border of its own to ring. */
.term:focus-within { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.term-noscript { padding: 0.85rem; color: var(--dim); font-size: 0.76rem; margin: 0; }
.term-noscript a { color: var(--accent); }
/* Without scripting the prompt cannot do anything, and an input that silently ignores you
 * is worse than no input. Hide the form and let the noscript message stand in its place. */
.no-js .term-form { display: none; }
.no-js .term-body { display: none; }

/* -------------------------------------------------------------- windows -- */
/* Panels with working controls. The dots are buttons, not decoration. */
.win-bar {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.6rem 0.85rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.03);
}
.win-title {
  margin-left: 0.4rem;
  color: var(--dim);
  font-family: var(--mono);
  font-size: 0.72rem;
}
.win-btn {
  width: 11px;
  height: 11px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.18);
  transition: transform 0.15s ease, filter 0.15s ease;
}
.win-btn-close { background: #ff5f57; }
.win-btn-min { background: #febc2e; }
.win-btn-zoom { background: #28c840; }
.win-btn:hover { transform: scale(1.25); filter: brightness(1.15); }
/* The dot is 11px because that is what reads as window chrome, but 11px is far below a usable
 * tap target. An invisible pseudo-element pads the hit area out to 30px without changing how
 * the control looks. Positioned rather than sized so the dots keep their spacing. */
.win-btn { position: relative; }
.win-btn::before {
  content: '';
  position: absolute;
  inset: -10px;
  border-radius: 50%;
}
.win-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* A hint that these actually do something, on hover of the bar. */
.win-bar:hover .win-btn::after {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}

.win { transition: opacity 0.2s ease, transform 0.2s ease; }
.win.is-closing { opacity: 0; transform: scale(0.96) translateY(6px); }
.win.is-opening { animation: win-in 0.3s ease; }
@keyframes win-in {
  from { opacity: 0; transform: scale(0.97) translateY(8px); }
  to { opacity: 1; transform: none; }
}
/* Rolled up: only the title bar remains, like a shaded window. */
.win.is-rolled > *:not(.win-bar):not(.term-bar) { display: none; }
.win.is-zoomed {
  transform: scale(1.02);
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
  z-index: 3;
}

/* ----------------------------------------------------------------- dock -- */
/* Where closed windows go, so nothing is ever lost for good. */
.dock {
  position: fixed;
  left: 50%;
  bottom: 1rem;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  gap: 0.5rem;
  padding: 0.45rem 0.55rem;
  border-radius: 999px;
  background: rgba(14, 15, 26, 0.75);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
}
.dock[hidden] { display: none; }
.dock.is-nudging { animation: dock-nudge 0.5s ease 2; }
@keyframes dock-nudge {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(-5px); }
}
.dock-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.8rem;
  border-radius: 999px;
  border: 1px solid var(--glass-border);
  background: rgba(255, 255, 255, 0.05);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.74rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.dock-chip:hover { background: rgba(255, 255, 255, 0.1); color: var(--text); transform: translateY(-2px); }
.dock-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Konami. Deliberately mild: a hue rotation on the decoration only, so the page stays
 * readable and the joke does not cost anyone their eyesight. */
.is-party .bg-traces,
.is-party .bg-bloom,
.is-party .cursor-glow { animation: party 6s linear infinite; }
@keyframes party {
  from { filter: hue-rotate(0deg); }
  to { filter: hue-rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .is-party .bg-traces,
  .is-party .bg-bloom,
  .is-party .cursor-glow { animation: none; filter: hue-rotate(140deg); }
}

.dock-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent);
}

/* ------------------------------------------------------------ typewriter -- */
/* A block cursor that sits after the last typed character. It is an ::after on the paragraph
   currently being written, so it moves with the text rather than being positioned. */
[data-typewriter] p.is-cursor::after,
[data-typewriter].is-typed p:last-child::after {
  content: '';
  display: inline-block;
  width: 0.5em;
  height: 1em;
  margin-left: 0.12em;
  vertical-align: -0.14em;
  background: var(--accent);
  opacity: 0.85;
  animation: type-blink 1.05s steps(1) infinite;
}
@keyframes type-blink { 50% { opacity: 0; } }

/* Text being dragged over before it is cut. Uses the browser's own selection colours rather
   than the accent, because a selection that does not look like a selection is just a highlight. */
.type-sel {
  background: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--text);
  border-radius: 1px;
}

/* While typing, the empty paragraphs must not collapse to nothing, or the first line jumps as
   soon as a character lands. */
[data-typewriter].is-typing p { min-height: 1.6em; }

@media (prefers-reduced-motion: reduce) {
  /* Text is left as authored, so a blinking cursor would be pointing at nothing in progress. */
  [data-typewriter] p::after { display: none; }
}

/* ----------------------------------------------------------------- cards -- */
.grid {
  display: grid;
  /* min() so the track can never demand more than the viewport offers. A flat 300px broke
   * at 280px wide (Galaxy Fold, and any browser with a large zoom), pushing every card past
   * the right edge and forcing horizontal scroll. */
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: var(--gap);
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: var(--pad);
  border-radius: var(--radius);
  /* Deliberately no backdrop-filter. Every card having one meant a full blur pass per card on
     every frame the background canvas painted, which measured at 126ms/frame; the cards sit on
     opaque page background, so it was buying nearly nothing. A slightly more opaque fill reads
     the same and costs nothing. */
  background: var(--card-bg);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
  overflow: hidden;
  isolation: isolate;
  transition: transform 0.4s var(--ease-out), border-color 0.4s var(--ease-out),
    box-shadow 0.4s var(--ease-out), background 0.4s var(--ease-out);
}

/* The accent bar along the top: metro colour-coding, one glance per project. */
.card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--accent);
  opacity: 0.55;
  transform: scaleX(0.12);
  transform-origin: left;
  transition: transform 0.5s var(--ease-out), opacity 0.4s var(--ease-out);
}
.card:hover::before { transform: scaleX(1); opacity: 1; }

/* The sheen tracks the pointer via --px/--py, so light appears to fall on the
   panel from where the cursor is. Without JS it simply never shows. */
.card-sheen {
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0;
  background: radial-gradient(
    420px circle at var(--px, 50%) var(--py, 0%),
    color-mix(in srgb, var(--accent) 20%, transparent),
    transparent 62%
  );
  transition: opacity 0.4s var(--ease-out);
}
.card:hover .card-sheen { opacity: 1; }

a.card:hover,
.card:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: var(--glass-bg-strong);
  box-shadow: 0 30px 70px -28px rgba(0, 0, 0, 1),
    0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent);
}

/* Tilt is applied by JS as a CSS variable transform, kept in one place so the
   hover lift and the tilt cannot fight each other. */
.card.tilt { transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateY(var(--lift, 0)); }

.card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }

.card-glyph {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 12px;
  font-size: 1.15rem;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
  transition: transform 0.45s var(--ease-spring), background 0.4s var(--ease-out);
}
.card:hover .card-glyph {
  transform: translateY(-2px) rotate(-6deg) scale(1.06);
  background: color-mix(in srgb, var(--accent) 20%, transparent);
}

/* A drawn mark wants the whole tile; a dingbat wants to sit on the text baseline. The rotate on
   hover is dropped for marks, because a tilted moth reads as a broken moth where a tilted ◈ just
   reads as a tilted ◈. */
.card-glyph.has-mark { padding: 6px; }
.card:hover .card-glyph.has-mark { transform: translateY(-2px) scale(1.06); }
.card-glyph .mark {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}

.card-status {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--dim);
}
.pulse {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 60%, transparent);
  animation: pulse 2.4s ease-out infinite;
}
@keyframes pulse {
  70% { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.card-title { font-size: 1.28rem; font-weight: 620; }
/* Whose thing this is. Some entries are other people's projects that I contribute to, and
   saying so on the card is more honest than letting a portfolio grid imply ownership. */
.card-role {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--accent);
  opacity: 0.85;
  margin: -0.2rem 0 0.1rem;
}

.card-tagline { color: var(--fg); opacity: 0.9; font-size: 0.95rem; }
.card-blurb { color: var(--muted); font-size: 0.9rem; }

/* What a project actually does, as a list. Kept compact and two-column so a long feature set
   does not make one card tower over its neighbours in the row. */
.card-highlights {
  display: grid;
  /* Two columns, never three. auto-fit filled the wider cards on the projects page with a third
     column and then wrapped almost every item onto two lines: more columns, less readable.
     A fixed pair that collapses to one below the breakpoint is what this list actually wants. */
  grid-template-columns: 1fr 1fr;
  gap: 0.1rem 0.9rem;
  margin: 0.1rem 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.82rem;
  color: var(--muted);
}
.card-highlights li {
  position: relative;
  padding-left: 0.85rem;
  line-height: 1.5;
}
.card-highlights li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 4px;
  height: 4px;
  border-radius: 1px;
  background: var(--accent);
  opacity: 0.75;
}

.stack { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.15rem; }
.stack-item {
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--dim);
  padding: 0.16rem 0.45rem;
  border-radius: var(--radius-tile);
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid rgba(255, 255, 255, 0.06);
}

.card-foot {
  margin-top: auto;
  padding-top: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  border-top: 1px solid rgba(255, 255, 255, 0.07);
}

.chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.chip {
  font-size: 0.7rem;
  color: var(--muted);
  padding: 0.18rem 0.55rem;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.07);
}

.card-year { font-family: var(--mono); font-size: 0.72rem; color: var(--dim); }

.card-cue {
  position: absolute;
  right: var(--pad);
  bottom: calc(var(--pad) + 2.6rem);
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--accent);
  opacity: 0;
  transform: translateX(-6px);
  transition: opacity 0.3s var(--ease-out), transform 0.3s var(--ease-out);
}
.card:hover .card-cue { opacity: 1; transform: translateX(0); }
.card-cue.is-quiet { color: var(--dim); }

/* A project still carrying seeded placeholder copy. Deliberately conspicuous: this
   text is plausible enough to be mistaken for finished, so it has to say otherwise. */
.card-draft {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
  padding: 0.2rem 0.6rem;
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #07070c;
  background: var(--amber);
  border-bottom-left-radius: var(--radius-tile);
}
.card.is-draft { border-style: dashed; }

/* An age restriction is information someone needs before they click, not a footnote. It sits
   beside the draft badge and shifts left when both are present, so neither is covered. */
.card-adult {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
  padding: 0.2rem 0.55rem;
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: #07070c;
  background: var(--rose);
  border-bottom-left-radius: var(--radius-tile);
}
.card-draft ~ .card-adult { right: auto; left: 0; border-radius: 0 0 var(--radius-tile) 0; }

/* A featured card earns a slightly brighter surface and a full-strength accent bar, so the
   homepage row reads as a selection rather than an arbitrary first three. */
.card-featured {
  background: var(--glass-bg-strong);
  border-color: color-mix(in srgb, var(--accent) 22%, var(--glass-border));
}
.card-featured::before { transform: scaleX(1); opacity: 0.85; }

/* The featured card on the homepage spans wider on large screens, so the grid
   has a focal point instead of six equal rectangles. */
@media (min-width: 900px) {
  .grid-featured { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ---------------------------------------------------------------- filters -- */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: var(--gap);
}
.filter {
  font-family: var(--mono);
  font-size: 0.76rem;
  padding: 0.35rem 0.8rem;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  color: var(--muted);
  cursor: pointer;
  transition: all 0.25s var(--ease-out);
}
.filter:hover { color: var(--fg); border-color: rgba(255, 255, 255, 0.25); transform: translateY(-1px); }
.filter.is-active {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--fg);
}

/* Filtered-out cards collapse out of the grid rather than leaving holes. */
.card.is-hidden { display: none; }

.empty {
  padding: 2rem;
  text-align: center;
  color: var(--dim);
  font-family: var(--mono);
  font-size: 0.85rem;
}

/* The note under the archive. Deliberately quiet: it is a status line, not a heading, and it
   must not compete with the cards above it. */
.more-soon {
  margin: clamp(2rem, 5vw, 3.25rem) 0 0;
  text-align: center;
  color: var(--dim);
  font-family: var(--mono);
  font-size: 0.82rem;
  letter-spacing: 0.08em;
}
.more-soon[hidden] { display: none; }

/* ----------------------------------------------------------------- sheet -- */
/* The medium tier. A <dialog>, so the top layer, the backdrop and focus handling are the
   browser's job rather than a stack of z-index guesses. */
.sheet {
  width: min(680px, calc(100vw - 2rem));
  max-height: min(82vh, 900px);
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--glass-border));
  border-radius: var(--radius);
  background: var(--card-bg);
  color: var(--text);
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 1),
    0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent);
  overflow: hidden;
}
/* The accent is set on the dialog per project, the same way cards do it, so the whole sheet
   picks up the colour of the thing it is describing. */
.sheet[data-accent='cyan'] { --accent: var(--cyan); }
.sheet[data-accent='violet'] { --accent: var(--violet); }
.sheet[data-accent='lime'] { --accent: var(--lime); }
.sheet[data-accent='amber'] { --accent: var(--amber); }
.sheet[data-accent='rose'] { --accent: var(--rose); }

.sheet::backdrop {
  background: rgba(4, 4, 9, 0.72);
  backdrop-filter: blur(3px);
}

.sheet-inner {
  padding: clamp(1.5rem, 4vw, 2.5rem);
  overflow-y: auto;
  max-height: min(82vh, 900px);
}
.sheet-close {
  position: absolute;
  top: 0.65rem;
  right: 0.8rem;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  border: 1px solid var(--glass-border);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  color: var(--muted);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.25s var(--ease-out), border-color 0.25s var(--ease-out);
}
.sheet-close:hover { color: var(--text); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

.sheet-eyebrow {
  margin: 0 0 0.4rem;
  color: var(--accent);
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
}
.sheet-title { margin: 0 0 0.3rem; font-size: clamp(1.5rem, 4vw, 2rem); letter-spacing: -0.02em; }
.sheet-adult { margin: 0.2rem 0 0.6rem; color: var(--muted); font-size: 0.85rem; }
.sheet-tagline { margin: 0 0 1.2rem; color: var(--muted); font-size: 1rem; }
.sheet-body > p { margin: 0 0 0.9rem; line-height: 1.7; }
.sheet-body > p:last-child { margin-bottom: 0; }
.sheet .stack, .sheet .chips { margin-top: 1.1rem; }
.sheet-actions { margin-top: 1.6rem; }

/* The open animation is a transition on the dialog, gated by the same reduced-motion rule as
   everything else on this site. */
@media (prefers-reduced-motion: no-preference) {
  .sheet[open] { animation: sheet-in 0.28s var(--ease-spring); }
  .sheet[open]::backdrop { animation: sheet-fade 0.28s var(--ease-out); }
}
@keyframes sheet-in {
  from { opacity: 0; transform: translateY(12px) scale(0.985); }
  to { opacity: 1; transform: none; }
}
@keyframes sheet-fade { from { opacity: 0; } to { opacity: 1; } }

/* ---------------------------------------------------------------- detail -- */
/* The full tier: a project's own page. */
.detail { width: var(--shell); margin: clamp(1.5rem, 5vw, 3rem) auto 0; }
.detail-back { margin: 0 0 1.5rem; font-family: var(--mono); font-size: 0.8rem; }

.detail-head { margin-bottom: clamp(2rem, 5vw, 3rem); }
.detail-title-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
/* The mark is bigger here than on a card: this is the one place it is the subject rather than
   a label on something else. */
.detail-glyph { width: 58px; height: 58px; border-radius: 16px; }
.detail-title { margin: 0; font-size: clamp(2rem, 6vw, 3.2rem); letter-spacing: -0.03em; }
.detail-role {
  margin: 0.25rem 0 0;
  color: var(--accent);
  font-family: var(--mono);
  font-size: 0.78rem;
}
/* Sits next to the title rather than at the far edge of the header: with margin-left:auto it
   ended up a third of the page away from the thing it describes, reading as unrelated. */
.detail-status { align-self: center; }
.detail-adult { margin: 1rem 0 0; color: var(--muted); font-size: 0.9rem; }
.detail-tagline {
  margin: 1.25rem 0 0;
  font-size: clamp(1.05rem, 2.5vw, 1.3rem);
  color: var(--text);
  max-width: 60ch;
}

.detail-body {
  display: grid;
  /* The sidebar holds a short summary, so it gets a fixed comfortable width and the prose takes
     the rest. Proportional tracks gave the text a narrow column and the summary a large empty
     one, which looked like a layout bug rather than a choice. */
  grid-template-columns: minmax(0, 1fr) minmax(240px, 300px);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
}
.detail-main { display: grid; gap: clamp(1.75rem, 4vw, 2.5rem); }
.detail-block > p { margin: 0 0 0.9rem; line-height: 1.75; max-width: 74ch; color: var(--muted); }
.detail-block > p:last-child { margin-bottom: 0; }
.detail-heading {
  margin: 0 0 0.85rem;
  font-size: 1.15rem;
  letter-spacing: -0.01em;
}
/* A rule under each heading, in the accent, so the sections read as a document rather than as
   a run of paragraphs. */
.detail-heading::after {
  content: '';
  display: block;
  width: 32px;
  height: 2px;
  margin-top: 0.6rem;
  border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 65%, transparent);
}

.detail-side { display: grid; gap: var(--gap); align-self: start; grid-auto-rows: min-content; }
.detail-side .panel { padding: clamp(1.25rem, 3vw, 1.75rem); }
/* One column in the sidebar. The two-column highlights list is right on a wide card and wrong
   in a 300px column, where every second item wrapped onto two lines. */
.detail-side .card-highlights { grid-template-columns: 1fr; gap: 0.35rem; }
.detail-year {
  margin: 1.1rem 0 0;
  color: var(--dim);
  font-family: var(--mono);
  font-size: 0.75rem;
}
.detail-actions { display: grid; }

/* Inline 18+ badge, for prose rather than for the corner of a card. */
.card-adult.is-inline { position: static; display: inline-block; margin-right: 0.4rem; }

@media (max-width: 860px) {
  .detail-body { grid-template-columns: 1fr; }
  /* At one column the sidebar is a summary of what was just read, so it goes last. */
  .detail-side { order: 2; }
}

/* ------------------------------------------------------------------- CTA -- */
.cta {
  position: relative;
  width: var(--shell);
  margin: clamp(3.5rem, 9vw, 7rem) auto 0;
  padding: clamp(2rem, 5vw, 3.5rem);
  text-align: center;
  overflow: hidden;
}
.cta-title {
  font-size: clamp(1.5rem, 3.6vw, 2.4rem);
  font-weight: 640;
  margin-bottom: 0.75rem;
}
.cta-sub { color: var(--muted); margin: 0 auto 1.75rem; max-width: 52ch; }

/* -------------------------------------------------------------- prose/ab -- */
.prose { display: grid; gap: 1rem; max-width: 62ch; }
.prose p { color: var(--muted); }
.prose strong { color: var(--fg); font-weight: 600; }
.prose a.link {
  color: var(--accent);
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  transition: border-color 0.25s ease, color 0.25s ease;
}
.prose a.link:hover { border-color: var(--accent); }
/* An inline link is only as tall as its text, which is under the touch minimum. Padding it
 * vertically without affecting line box height keeps the paragraph looking unchanged. */
.prose a.link {
  display: inline-block;
  padding-block: 0.28rem;
  margin-block: -0.28rem;
}

.panel { padding: clamp(1.4rem, 3vw, 2.25rem); }
/* A windowed panel puts its title bar flush against the edges, the way the terminal does.
 * Padding the bar with the panel left it floating inset, which reads as a card inside a
 * card rather than as a window chrome. */
.panel.win { padding: 0; overflow: hidden; }
.panel.win > .prose { padding: clamp(1.4rem, 3vw, 2.25rem); }

.split {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
  gap: var(--gap);
  align-items: start;
}
/* .hero-side is a stretching grid because on the homepage it holds one terminal that should
   fill the column. In a .split it holds several panels, and equal rows gave each one a share of
   the taller left column, so both ended in a pool of empty glass. Here they size to content and
   stack from the top. */
.split .hero-side { align-self: start; grid-auto-rows: min-content; }


/* ---------------------------------------------------------------- footer -- */
.footer { width: var(--shell); margin: clamp(4rem, 10vw, 8rem) auto clamp(1.5rem, 4vw, 3rem); }
.footer-inner {
  padding: clamp(1.25rem, 3vw, 2rem);
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) auto auto;
  gap: var(--gap);
  align-items: center;
}
.footer-brand { font-weight: 600; letter-spacing: -0.02em; }
.footer-note { color: var(--dim); font-size: 0.84rem; margin-top: 0.35rem; }
.footer-nav { display: flex; gap: 0.2rem; flex-wrap: wrap; }
.footer-nav .nav-ink { display: none; }
.footer-meta { display: grid; gap: 0.25rem; justify-items: end; text-align: right; }

/* ----------------------------------------------------------------- reveal -- */
/* Elements start slightly low and transparent, then settle as they scroll in.
   The no-js class on <html> is removed by JS immediately, so with scripting
   disabled everything is simply visible from the start. */
.reveal { opacity: 0; transform: translateY(18px); }
html.js .reveal {
  transition: opacity 0.7s var(--ease-out), transform 0.7s var(--ease-out);
  transition-delay: var(--delay, 0ms);
}
.reveal.is-in { opacity: 1; transform: none; }
html:not(.js) .reveal { opacity: 1; transform: none; }

/* ------------------------------------------------------------ responsive -- */
@media (max-width: 940px) {
  .hero { grid-template-columns: 1fr; }
  /* Stacked, the terminal is below rather than beside, so an arrow pointing right lies. */
  .hero-aside-arrow { transform: rotate(90deg); }
  @keyframes aside-point-down {
    0%, 100% { transform: rotate(90deg) translateX(0); opacity: 0.65; }
    50% { transform: rotate(90deg) translateX(4px); opacity: 1; }
  }
  .hero-aside-arrow { animation-name: aside-point-down; }
  .split { grid-template-columns: 1fr; }
  .footer-inner { grid-template-columns: 1fr; }
  .footer-meta { justify-items: start; text-align: left; }
}

@media (max-width: 720px) {
  /* One column: two would leave roughly ten characters per item on a phone. */
  .card-highlights { grid-template-columns: 1fr; }

  .nav-toggle { display: flex; }
  .nav { padding-right: 0.55rem; }

  /* The mobile menu drops out of the bar as its own glass sheet. */
  .nav-links {
    position: absolute;
    top: calc(100% + 0.55rem);
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0.2rem;
    padding: 0.5rem;
    border-radius: var(--radius);
    background: rgba(10, 10, 18, 0.9);
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    opacity: 0;
    transform: translateY(-8px) scale(0.98);
    pointer-events: none;
    transition: opacity 0.25s var(--ease-out), transform 0.3s var(--ease-out);
  }
  .nav-links.is-open { opacity: 1; transform: none; pointer-events: auto; }
  .nav-ink { display: none; }
  .nav-link { padding: 0.7rem 0.9rem; }
  .footer-nav { display: none; }
}

/* ------------------------------------------------------- reduced motion -- */
/* Movement is decoration here, never information: with it off, everything still
   works and every state is still visible. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .bg-traces, .bg-scan { display: none; }
  .card.tilt { transform: none; }
}
