/* timb.us — phosphor CRT theme */

/* === Theme tokens === */
:root {
  --bg:           #050a06;
  --bg-vignette:  #0c1a0e;
  --phos:         #39ff14;
  --phos-dim:     #1a8a1a;
  --phos-soft:    #cfe6cb;
  --phos-faint:   #143a16;
  --grid-major:   #143a16;
  --grid-minor:   #0f3014;

  /* RGB triplet behind every rgba(...) call so the amber palette can swap
     all phosphor-glow alpha rules with one token override. */
  --phos-rgb:     57, 255, 20;

  /* Same idea for the major graticule grid lines (body background-image
     uses rgba() with alpha for the soft graticule wash). */
  --grid-major-rgb: 20, 58, 22;

  --bloom-text:   0 0 6px rgba(var(--phos-rgb), 0.45);
  --bloom-strong: 0 0 12px rgba(var(--phos-rgb), 0.60);

  /* Glitch durations exposed as vars; main.js can override at runtime. */
  --flick-period:    4.3s;
  --tear-period:     25s;
  --roll-period:     30s;
  --jitter-period:   0.18s;
  --sweep-period:    7s;
  --boot-duration:   700ms;
}

/* === Amber phosphor palette (P3 — classic warm amber CRT) ===
   Activated by [data-phosphor="amb"] (explicit user override) OR by
   prefers-color-scheme: dark when no override is set (AUTO mode).
   Amber maps to dark mode because P3 phosphors were marketed as the
   easier-on-the-eyes choice for long sessions — fits the "softer at
   night" intent of system dark mode. */
:root[data-phosphor="amb"] {
  --bg:           #0a0805;
  --bg-vignette:  #1a1208;
  --phos:         #ffb000;
  --phos-dim:     #8a5a00;
  --phos-soft:    #f5e6c8;
  --phos-faint:   #3a2710;
  --grid-major:   #3a2710;
  --grid-minor:   #2a1c0c;
  --phos-rgb:        255, 176, 0;
  --grid-major-rgb:  58, 39, 16;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-phosphor]) {
    --bg:              #0a0805;
    --bg-vignette:     #1a1208;
    --phos:            #ffb000;
    --phos-dim:        #8a5a00;
    --phos-soft:       #f5e6c8;
    --phos-faint:      #3a2710;
    --grid-major:      #3a2710;
    --grid-minor:      #2a1c0c;
    --phos-rgb:        255, 176, 0;
    --grid-major-rgb:  58, 39, 16;
  }
}

/* === Fonts (self-hosted) === */
@font-face {
  font-family: 'Major Mono Display';
  src: url('/assets/fonts/MajorMonoDisplay-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Source Code Pro';
  src: url('/assets/fonts/SourceCodePro-Light.woff2') format('woff2');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Source Code Pro';
  src: url('/assets/fonts/SourceCodePro-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* === Reset / base === */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { background: var(--bg); }

body {
  min-height: 100vh;
  /* Layered background:
     1) major graticule grid — vertical lines at fifths of viewport
     2) major graticule grid — horizontal lines at fifths of viewport
     3) center-axis minor ticks — short crosshairs at viewport center
     4) radial phosphor vignette toward the center
     5) base near-black phosphor bg
     All fixed so the graticule stays put as content scrolls. */
  background-color: var(--bg);
  background-image:
    linear-gradient(to right,  rgba(var(--grid-major-rgb), 0.45) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(var(--grid-major-rgb), 0.45) 1px, transparent 1px),
    radial-gradient(ellipse at center, var(--bg-vignette) 0%, var(--bg) 80%);
  background-size: 20vw 100%, 100% 20vh, 100% 100%;
  background-position: 0 0, 0 0, 0 0;
  background-repeat: repeat-x, repeat-y, no-repeat;
  background-attachment: fixed, fixed, fixed;
  color: var(--phos-soft);
  font-family: 'Source Code Pro', ui-monospace, monospace;
  font-weight: 300;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  position: relative;
  overflow-x: hidden;
}

/* Center-axis minor ticks — short crosshairs at viewport center,
   spec'd at "minor ticks on the center axes". Drawn via fixed pseudo
   so they're cheap to position with translate(-50%) without affecting
   layout. */
body::before {
  content: "";
  position: fixed;
  left: 50%;
  top: 50%;
  width: 24px;
  height: 24px;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: -1;
  background-image:
    linear-gradient(to right,  var(--grid-minor) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-minor) 1px, transparent 1px);
  background-size: 1px 100%, 100% 1px;
  background-position: center, center;
  background-repeat: no-repeat;
  opacity: 0.5;
}

/* All headings phosphor-bright by default. */
h1, h2, h3 {
  font-family: 'Major Mono Display', ui-monospace, monospace;
  font-weight: 400;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  letter-spacing: 2px;
  margin: 0;
}

a {
  color: var(--phos);
  text-decoration: none;
  text-shadow: var(--bloom-text);
}

/* === Visually hidden utility (for sr-only announcer) === */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* === Skip link === */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--bg);
  color: var(--phos);
  padding: 8px 12px;
  border: 1px solid var(--phos);
  z-index: 1000;
}
.skip-link:focus { left: 12px; top: 12px; }

/* === Persistent scanlines overlay === */
.scanlines {
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0) 0px,
    rgba(0, 0, 0, 0) 2px,
    rgba(0, 0, 0, 0.18) 3px
  );
  mix-blend-mode: multiply;
  z-index: 100;
}

/* === Slow scan sweep === */
.sweep {
  position: fixed;
  left: 0;
  right: 0;
  height: 28%;
  top: -28%;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    rgba(var(--phos-rgb), 0.04) 60%,
    rgba(var(--phos-rgb), 0.10) 100%
  );
  animation: sweep var(--sweep-period) linear infinite;
  z-index: 99;
}
@keyframes sweep {
  from { top: -28%; }
  to   { top: 100%; }
}

/* === Rolling bright bar (sync drift) === */
.roll-bar {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  height: 6%;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent,
    rgba(var(--phos-rgb), 0.18),
    transparent
  );
  mix-blend-mode: screen;
  z-index: 99;
  opacity: 0;
}
.roll-bar.rolling { animation: roll 1.4s linear; }
@keyframes roll {
  0%   { opacity: 0;    transform: translateY(0); }
  10%  { opacity: 0.45; transform: translateY(0); }
  90%  { opacity: 0.45; transform: translateY(60vh); }
  100% { opacity: 0;    transform: translateY(60vh); }
}

/* === Base flicker on the whole page ===
   Spec: ~3–4% opacity oscillation, irregular keyframes (no perfect sine).
   Mobile uses flick-mobile (amplitude halved) — see the mobile media query below. */
body { animation: flick var(--flick-period) infinite; }
@keyframes flick {
  0%, 100% { opacity: 1; }
  23%      { opacity: 0.96; }
  24%      { opacity: 0.99; }
  61%      { opacity: 0.97; }
  62%      { opacity: 1; }
}
@keyframes flick-mobile {
  0%, 100% { opacity: 1; }
  23%      { opacity: 0.98; }
  24%      { opacity: 0.995; }
  61%      { opacity: 0.985; }
  62%      { opacity: 1; }
}

/* === Page frame layout === */
.screen-top, .screen-bottom { padding: 14px 32px; }
.screen-main {
  padding: 24px 32px 64px;
  max-width: 1160px;
  margin: 0 auto;
  position: relative;
  min-height: calc(100vh - 200px);
  /* Spec channel cross-fade ~180ms, project detail fade ~120ms.
     main.js sets --screen-fade per navigation kind. */
  transition: opacity var(--screen-fade, 180ms) ease;
}
.screen-main:focus { outline: none; }
.screen-main.is-fading { opacity: 0; }

/* Detail-only fade — same channel, project selection change. ~120ms.
   main.js scopes the fade to the .detail element so the project list
   stays at full opacity (spec: only the detail should swap on this kind
   of nav, not the whole channel). */
.detail {
  transition: opacity var(--screen-fade, 120ms) ease;
}
.detail.is-fading { opacity: 0; }

/* === Top status line === */
.status-line.top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 11px;
  letter-spacing: 1.5px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  border-bottom: 0.5px solid rgba(var(--phos-rgb), 0.35);
  padding-bottom: 6px;
  margin-bottom: 14px;
}
.status-line .brand { font-family: 'Source Code Pro', monospace; }
.status-line .rec { opacity: 0.7; }
/* Breathing dot — reproduces the Apple PowerBook sleep-LED algorithm:
   asymmetric two-half Gaussian curve (narrower rise, wider fall), 5s cycle,
   ~1s pause at trough, I:E ratio ~1:1.67. Reference:
   https://avital.ca/notes/a-closer-look-at-apples-breathing-light
   Each keyframe is the Gaussian sample value linearly interpolated between
   --phos-faint (off) and --phos (peak), pre-resolved as sRGB hex so the
   curve is exact regardless of how the browser interpolates between stops.
   Linear timing so the discrete samples aren't further smoothed. */
.status-line .rec-dot {
  display: inline-block;
  animation: rec-breathe 5s linear infinite;
}

@keyframes rec-breathe {
  0%        { color: #143c16; }
  5%        { color: #154216; }
  10%       { color: #195616; }
  15%       { color: #207b15; }
  20%       { color: #2bb215; }
  25%       { color: #35e714; }
  30%       { color: #39ff14; }
  35%       { color: #38f714; }
  40%       { color: #33df14; }
  45%       { color: #2dc015; }
  50%       { color: #279d15; }
  55%       { color: #217d15; }
  60%       { color: #1c6316; }
  65%       { color: #185216; }
  70%       { color: #164616; }
  75%       { color: #154016; }
  80%       { color: #143c16; }
  85%, 100% { color: #143a16; }
}

/* Active state — JS toggles .is-active when there's recent pointer/keyboard
   activity (cleared after 5s idle). Faster, sine-like oscillation between
   full --phos and the midpoint (#279d15, 50% on the phos-faint↔phos line).
   Keyframes peak at 0%/100% so the animation starts (and ends) at full
   phos, matching the colour the wake-up fade lands on with no jump. */
.status-line .rec-dot.is-active {
  animation: rec-active 1.6s ease-in-out infinite;
}

@keyframes rec-active {
  0%, 100% { color: var(--phos); }
  50%      { color: #279d15; }
}

/* Amber-mode equivalents — same brightness curve mapped from the green
   stops onto the amber palette (#3a2710 → #ffb000). Overridden via
   animation-name so the green keyframes stay byte-exact under default. */
@keyframes rec-breathe-amber {
  0%        { color: #3c2810; }
  5%        { color: #422c0f; }
  10%       { color: #563a0e; }
  15%       { color: #7b540b; }
  20%       { color: #b37b06; }
  25%       { color: #e79f02; }
  30%       { color: #ffb000; }
  35%       { color: #f7ab01; }
  40%       { color: #df9a03; }
  45%       { color: #c08405; }
  50%       { color: #9d6c08; }
  55%       { color: #7d560b; }
  60%       { color: #64440d; }
  65%       { color: #52380e; }
  70%       { color: #462f0f; }
  75%       { color: #402b10; }
  80%       { color: #3c2810; }
  85%, 100% { color: #3a2710; }
}
@keyframes rec-active-amber {
  0%, 100% { color: var(--phos); }
  50%      { color: #9d6c08; }
}
:root[data-phosphor="amb"] .status-line .rec-dot {
  animation-name: rec-breathe-amber;
}
:root[data-phosphor="amb"] .status-line .rec-dot.is-active {
  animation-name: rec-active-amber;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-phosphor]) .status-line .rec-dot {
    animation-name: rec-breathe-amber;
  }
  :root:not([data-phosphor]) .status-line .rec-dot.is-active {
    animation-name: rec-active-amber;
  }
}

/* Wake-up and fall-asleep transitions. JS captures the current animated
   color, freezes it inline, then transitions toward the target (peak for
   waking, trough for falling). animation: none disables the running
   breathe/active animation while the transition runs. Wake-up is faster
   so the dot feels responsive to user input; fall-asleep is slower so the
   dot eases into the sleeping breathe pattern. */
.status-line .rec-dot.is-waking-up {
  animation: none;
  transition: color 500ms ease-out;
}
.status-line .rec-dot.is-falling-asleep {
  animation: none;
  transition: color 800ms ease-out;
}

/* === Channel-tab nav === */
.channels {
  display: flex;
  gap: 28px;
  flex-wrap: nowrap;
  overflow-x: auto;
  padding-bottom: 8px;
  position: relative;
  animation: tab-tear var(--tear-period) infinite;
}
.channels a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  opacity: 0.5;
  padding: 4px 0;
  border-bottom: 1px solid transparent;
  white-space: nowrap;
  transition: opacity 180ms ease, border-bottom-color 180ms ease;
}
.channels a:hover,
.channels a:focus-visible { opacity: 0.8; }
.channels a[aria-current="page"] {
  opacity: 1;
  border-bottom-color: var(--phos);
}
/* Once main.js hydrates, swap the static per-tab border for the floating
   underline below — JS measures the active tab and slides the underline
   between tabs on channel change. */
body.is-hydrated .channels a[aria-current="page"] {
  border-bottom-color: transparent;
}
.channel-underline {
  position: absolute;
  bottom: 8px;
  left: 0;
  height: 1px;
  background: var(--phos);
  box-shadow: 0 0 6px rgba(var(--phos-rgb), 0.55);
  transform: translateX(var(--ul-x, 0));
  width: var(--ul-w, 0);
  opacity: var(--ul-opacity, 0);
  pointer-events: none;
  transition: transform 180ms ease, width 180ms ease;
}
.channels .ch-num {
  font-size: 11px;
  letter-spacing: 2px;
  opacity: 0.85;
}
/* Per-channel waveform glyphs (inline SVGs sized to 20x14 in markup) */
.channels .glyph {
  width: 22px;
  height: 14px;
  flex: 0 0 auto;
}

/* Horizontal-tear glitch on the channel-tab row */
@keyframes tab-tear {
  0%, 92%, 100% { transform: translate(0, 0); }
  93%   { transform: translate(-3px, 0); }
  93.5% { transform: translate(2px, 0); }
  94%   { transform: translate(0, 0); }
}

/* === Bottom status / footer fingerprint === */
.screen-bottom {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  letter-spacing: 1.5px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  opacity: 0.55;
  border-top: 0.5px solid rgba(var(--phos-rgb), 0.35);
  padding-top: 10px;
  margin-top: 32px;
}
.screen-bottom .fingerprint { white-space: nowrap; }

/* Channels row wraps the .channels nav and the phosphor-toggle so the
   toggle anchors to the right while the channels nav scrolls independently
   on mobile. */
.channels-row {
  display: flex;
  align-items: center;
  gap: 24px;
}
.channels-row .channels { flex: 1 1 auto; min-width: 0; }
.channels-row .phosphor-toggle { flex: 0 0 auto; }

/* Phosphor-mode toggle. Sized to match the channel-tab font/letter-spacing
   so the row reads as a single continuous strip; gap-driven spacing keeps
   the labels visually distinct. */
.phosphor-toggle {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  white-space: nowrap;
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  padding-bottom: 8px;
}
.phosphor-toggle::before { content: "["; opacity: 0.5; margin-right: 2px; }
.phosphor-toggle::after  { content: "]"; opacity: 0.5; margin-left: 2px; }
.phosphor-toggle button {
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  opacity: 0.4;
  cursor: pointer;
  text-transform: uppercase;
  text-shadow: none;
}
.phosphor-toggle button:hover { opacity: 0.85; }
.phosphor-toggle button[aria-pressed="true"] {
  opacity: 1;
  text-shadow: var(--bloom-text);
}
.phosphor-toggle button:focus-visible {
  outline: 1px dotted var(--phos);
  outline-offset: 2px;
}
.phosphor-toggle .sep { opacity: 0.5; }

/* === One-shot boot sweep on first load === */
.boot-sweep {
  position: fixed;
  left: 0;
  right: 0;
  height: 20vh;
  top: -20vh;
  pointer-events: none;
  z-index: 200;
  background: linear-gradient(
    to bottom,
    transparent,
    rgba(var(--phos-rgb), 0.18) 80%,
    rgba(var(--phos-rgb), 0.45) 100%
  );
  animation: boot-sweep var(--boot-duration) ease-out forwards;
}
@keyframes boot-sweep {
  from { top: -20vh; }
  to   { top: 100vh; }
}

/* Page content stays hidden behind the sweep until it's mostly past. */
body.booting .screen-main { opacity: 0; }
body.booting.booted .screen-main {
  opacity: 1;
  transition: opacity 240ms ease-out;
}

/* === Per-character jitter (applied via .jit class on <span>s) === */
.jit { display: inline-block; animation: char-jitter var(--jitter-period) steps(2) infinite; }
@keyframes char-jitter {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(0.6px, -0.4px); }
}

/* === Channel container shared === */
.channel { position: relative; }
.channel-title {
  font-size: clamp(40px, 7vw, 72px);
  letter-spacing: 6px;
  text-shadow: var(--bloom-strong);
  margin-bottom: 2px;
}
.channel-title .channel-prefix {
  display: block;
  font-family: 'Source Code Pro', monospace;
  font-size: clamp(10px, 1.3vw, 14px);
  font-weight: 400;
  letter-spacing: 4px;
  text-shadow: var(--bloom-text);
  opacity: 0.7;
  margin-bottom: 6px;
}
.channel-meta {
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  opacity: 0.65;
  margin-bottom: 16px;
}
hr.channel-rule {
  border: 0;
  border-top: 0.5px solid rgba(var(--phos-rgb), 0.5);
  margin: 0 0 24px 0;
}

/* === About === */
.about-bio {
  font-size: 15px;
  line-height: 1.7;
  color: var(--phos-soft);
  max-width: 64ch;
  margin: 0 0 28px 0;
}
.fact-stack {
  /* Single shared grid so every fact-row's value column starts at the same
     x-position regardless of label length. Same subgrid pattern as the
     credentials block below. */
  display: grid;
  grid-template-columns: auto 1fr;
  row-gap: 8px;
  column-gap: 12px;
  font-family: 'Source Code Pro', monospace;
  font-size: 12px;
  letter-spacing: 1.5px;
}
.fact-row {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  color: var(--phos-soft);
}
.fact-label {
  color: var(--phos);
  text-shadow: var(--bloom-text);
}

/* === Credentials sub-section ===
   Lives inside .channel-about after .fact-stack. Mirrors the channel-meta /
   channel-rule typography for the heading, then a stack of three-column
   dot-leader rows: name | leader | issuer | leader | year. The two `1fr`
   leader cells render as horizontal dotted borders along the text baseline. */
.credentials-meta {
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  opacity: 0.65;
  margin: 28px 0 8px 0;
}
hr.credentials-rule {
  border: 0;
  border-top: 0.5px solid rgba(var(--phos-rgb), 0.5);
  margin: 0 0 16px 0;
}
.credentials-stack {
  /* Single shared grid so the name / issuer / year columns line up across
     every row. With per-row grids the wider entries (e.g. "U.S. Government")
     shrink that row's leader cells and pull the issuer left. */
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto;
  row-gap: 8px;
  column-gap: 12px;
  font-family: 'Source Code Pro', monospace;
  font-size: 12px;
  letter-spacing: 1.5px;
}
.credential-row {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: baseline;
}
.credential-name,
.credential-year {
  color: var(--phos);
  text-shadow: var(--bloom-text);
  text-transform: uppercase;
}
.credential-issuer {
  color: var(--phos-soft);
  opacity: 0.7;
  text-transform: uppercase;
}
/* Leader gap — dotted bottom border lifted to sit on the text baseline. */
.credential-leader {
  border-bottom: 1px dotted rgba(var(--phos-rgb), 0.4);
  align-self: end;
  margin-bottom: 0.35em;
  min-width: 0;
}

/* === Project channel split layout === */
.split {
  display: grid;
  grid-template-columns: minmax(280px, 40%) 1fr;
  gap: 32px;
  position: relative;
}
.split::before {
  /* vertical phosphor divider */
  content: "";
  position: absolute;
  left: 40%;
  top: 8px;
  bottom: 8px;
  width: 0;
  border-left: 0.4px solid rgba(var(--phos-rgb), 0.45);
  pointer-events: none;
}

/* === Project list === */
.project-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
.project-row {
  display: grid;
  grid-template-columns: 28px 1fr auto;
  gap: 12px;
  align-items: start;
  padding: 12px 8px 14px 8px;
  position: relative;
  border-bottom: 0.4px solid rgba(var(--phos-rgb), 0.18);
  text-decoration: none;
  color: var(--phos-soft);
}
.project-row:hover { filter: brightness(1.15); }
.project-row:focus-visible {
  outline: 2px solid var(--phos);
  outline-offset: -2px;
  filter: brightness(1.15);
}
.project-row[aria-current="page"] {
  background: linear-gradient(to right, rgba(var(--phos-rgb), 0.06), transparent);
}
.project-row[aria-current="page"]::before {
  /* cursor triangle */
  content: "";
  position: absolute;
  left: -10px;
  top: 18px;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 4px 0 4px 8px;
  border-color: transparent transparent transparent var(--phos);
  filter: drop-shadow(0 0 4px rgba(var(--phos-rgb), 0.6));
}

.project-row .status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-top: 6px;
  background: var(--phos);
  box-shadow: 0 0 6px rgba(var(--phos-rgb), 0.7);
}
.project-row[data-status="archived"] {
  opacity: 0.6;
}
.project-row[data-status="archived"] .status-dot {
  background: transparent;
  border: 1px solid var(--phos);
  box-shadow: none;
}

.project-row .name {
  font-family: 'Source Code Pro', monospace;
  font-weight: 500;
  font-size: 14px;
  letter-spacing: 1.5px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
}
.project-row .summary {
  display: block;
  font-size: 11.5px;
  letter-spacing: 0.3px;
  color: var(--phos-soft);
  opacity: 0.75;
  margin-top: 4px;
  line-height: 1.4;
}
.project-row .status-text {
  font-size: 10px;
  letter-spacing: 1.5px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  opacity: 0.75;
  align-self: start;
  margin-top: 4px;
}

/* === Detail pane === */
.detail {
  padding-left: 16px;
  min-height: 280px;
  position: relative;
  /* aria-live region — content swaps when selection changes */
}
.detail-name {
  font-size: clamp(28px, 3.5vw, 42px);
  letter-spacing: 4px;
  margin: 0 0 12px 0;
}
.tag-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 16px 0;
}
.tag-chip {
  font-size: 10px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  padding: 4px 10px;
  border: 0.6px solid var(--phos);
  border-radius: 2px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  opacity: 0.8;
  background: transparent;
}
.detail-rule {
  border: 0;
  border-top: 0.4px solid rgba(var(--phos-rgb), 0.4);
  margin: 0 0 16px 0;
}
.detail-body {
  font-size: 13px;
  line-height: 1.7;
  color: var(--phos-soft);
  margin: 0 0 24px 0;
  max-width: 64ch;
}
.detail-body p { margin: 0 0 10px 0; }
.detail-body p:last-child { margin-bottom: 0; }

/* === Phosphor link button ===
   Spec: 1px #39ff14 border, 8px radius, Source Code Pro Regular ~13px
   tracked +1.5px, padding 10px 18px, transparent bg. Hover brightens
   the *border* opacity from ~70% → 100% (text stays bright) and adds an
   inner phosphor glow. Active inverts to filled phosphor / near-black. */
.btn-phosphor {
  display: inline-block;
  font-family: 'Source Code Pro', monospace;
  font-weight: 400;
  font-size: 13px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--phos);
  background: transparent;
  border: 1px solid rgba(var(--phos-rgb), 0.7);
  border-radius: 8px;
  padding: 10px 18px;
  text-shadow: var(--bloom-text);
  text-decoration: none;
  transition: border-color 120ms ease, background 120ms ease, color 120ms ease, box-shadow 120ms ease;
}
.btn-phosphor:hover {
  border-color: var(--phos);
  box-shadow: inset 0 0 12px rgba(var(--phos-rgb), 0.18);
}
.btn-phosphor:focus-visible {
  outline: 2px solid var(--phos);
  outline-offset: 3px;
  border-color: var(--phos);
}
.btn-phosphor:active {
  background: var(--phos);
  color: var(--bg);
  border-color: var(--phos);
  text-shadow: none;
}

/* === Empty channel state === */
.channel-empty .no-trace {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 360px;
  gap: 14px;
}
.no-trace-glyph {
  width: clamp(140px, 28vw, 220px);
  color: var(--phos);
  opacity: 0.75;
}
.no-trace-line {
  font-family: 'Major Mono Display', monospace;
  font-size: 16px;
  letter-spacing: 4px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
}
.no-trace-sub {
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  opacity: 0.6;
}

/* === Contact channel === */
.contact-stack {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 720px;
}
.contact-row {
  display: grid;
  grid-template-columns: 56px 1fr 24px;
  gap: 16px;
  align-items: center;
  padding: 18px 20px;
  border: 1px solid rgba(var(--phos-rgb), 0.35);
  border-radius: 6px;
  text-decoration: none;
  color: var(--phos-soft);
  background: transparent;
  transition: background 120ms ease, border-color 120ms ease;
}
.contact-row:hover,
.contact-row:focus-visible {
  background: rgba(var(--phos-rgb), 0.06);
  border-color: var(--phos);
  outline: none;
}
.contact-glyph {
  width: 36px;
  height: 36px;
  color: var(--phos);
  filter: drop-shadow(0 0 6px rgba(var(--phos-rgb), 0.4));
}
.contact-text .service {
  font-family: 'Major Mono Display', monospace;
  font-size: 14px;
  letter-spacing: 3px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  display: block;
}
.contact-text .handle {
  font-family: 'Source Code Pro', monospace;
  font-size: 13px;
  color: var(--phos-soft);
  opacity: 0.75;
}
.contact-arrow {
  color: var(--phos);
  text-shadow: var(--bloom-text);
  font-size: 18px;
  text-align: right;
}

/* === Lissajous corner widget === */
.lissa {
  /* Absolute (not fixed) so the canvas anchors to the page near the
     section heading and scrolls away with the rest of the content.
     - top: aligns with the top of the channel-title heading
     - right: mirrors .screen-main's centered max-width gutter + its
       inner padding so the canvas right edge ends where channel-rule does
     - width: sized so canvas + label ends just above the channel-rule */
  position: absolute;
  top: 124px;
  right: calc((100vw - min(1160px, 100vw)) / 2 + 32px);
  width: clamp(70px, 14vmin, 124px);
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  pointer-events: none;
  z-index: 50;
}
.lissa #lissa-canvas {
  width: 100%;
  height: 100%;
  display: block;
}
.lissa-label {
  font-family: 'Source Code Pro', monospace;
  font-size: 9px;
  letter-spacing: 1.5px;
  color: var(--phos);
  text-shadow: var(--bloom-text);
  opacity: 0.55;
  margin-top: 4px;
  white-space: nowrap;
}

/* === Mobile === */
@media (max-width: 720px) {
  .screen-top, .screen-bottom { padding: 12px 16px; }
  .screen-main { padding: 16px; }
  /* Stack the toggle below the channel tabs so the tabs get full-width
     scrolling and the toggle sits cleanly on its own line. */
  .channels-row {
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
  }
  .channels-row .phosphor-toggle {
    /* Left-align so the toggle clears the fixed Lissajous canvas in the
       top-right corner — right-aligning would put it underneath. */
    align-self: flex-start;
    padding: 0 16px 4px;
  }
  .channels {
    gap: 18px;
    padding: 8px 16px;
    margin: 0 -16px;
    -webkit-overflow-scrolling: touch;
    /* Spec: edge fade sits below the active-tab underline so the underline
       remains full-opacity even when the active tab is near the strip's
       edge. Implemented with sticky pseudo-element overlays (below) rather
       than mask-image, which would dim the underline along with the tabs. */
  }
  .channels::before,
  .channels::after {
    content: '';
    position: sticky;
    flex-shrink: 0;
    width: 24px;
    align-self: stretch;
    pointer-events: none;
    z-index: 1;
  }
  .channels::before {
    left: 0;
    margin-right: -24px;
    background: linear-gradient(to right, var(--bg), rgba(5, 10, 6, 0));
  }
  .channels::after {
    right: 0;
    margin-left: -24px;
    background: linear-gradient(to left, var(--bg), rgba(5, 10, 6, 0));
  }
  .channel-underline {
    z-index: 2;
  }
  .split {
    grid-template-columns: 1fr;
    gap: 18px;
  }
  .split::before {
    /* horizontal divider in the stacked layout */
    left: 0;
    right: 0;
    top: auto;
    bottom: auto;
    height: 0;
    width: auto;
    border-left: none;
    border-top: 0.4px solid rgba(var(--phos-rgb), 0.45);
    /* place between list and detail; CSS-grid stacking is fine */
  }
  .channel-title { letter-spacing: 4px; }
  /* Mobile header is taller (status + tabs + toggle), so push lissa
     down to align with the section heading. Width sized so canvas + XY
     label end just above the channel-rule, matching desktop framing. */
  .lissa {
    top: 144px;
    right: 16px;
    width: 78px;
  }
  .screen-bottom { font-size: 9px; gap: 8px; flex-wrap: wrap; }
  .screen-bottom .fingerprint {
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
    white-space: nowrap;
  }

  /* Credentials collapse to two lines: name + leader + year on row 1,
     issuer indented and faint on row 2. Each row becomes its own grid
     (overrides the desktop subgrid) so issuer can wrap below. Parent
     reverts to flex column since per-row alignment isn't needed at this
     width. */
  .credentials-stack {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .credential-row {
    grid-column: auto;
    grid-template-columns: auto 1fr auto;
    grid-template-areas:
      "name leader year"
      "issuer issuer issuer";
    row-gap: 2px;
  }
  .credential-name   { grid-area: name; }
  .credential-year   { grid-area: year; }
  .credential-issuer { grid-area: issuer; padding-left: 4px; opacity: 0.6; }
  .credential-leader { grid-area: leader; }
  /* Only the first leader cell renders; the second is unused on mobile. */
  .credential-row > .credential-leader:nth-of-type(2) { display: none; }

  :root {
    --boot-duration: 500ms;
    --tear-period:   50s;   /* halved frequency on mobile */
    --roll-period:   60s;
  }

  /* Spec: halve the flicker amplitude on mobile. */
  body { animation-name: flick-mobile; }
}

/* === Reduced motion === */
@media (prefers-reduced-motion: reduce) {
  body { animation: none; }
  .sweep,
  .roll-bar,
  .channels { animation: none !important; }
  .boot-sweep { display: none; }
  body.booting .screen-main { opacity: 1; }
  .jit { animation: none; }
  .screen-main,
  .detail { transition: none !important; }
  .screen-main.is-fading,
  .detail.is-fading { opacity: 1 !important; }
  .channels a { transition: none !important; }
  .channel-underline { transition: none !important; }
  .rec-dot { animation: none !important; color: var(--phos); }
}
