/* ============================================================
   HUDSON TINCH — site styles
   Monochrome ink on near-black, grain, PCB hero. Main pages only
   (desktop.html / mac.html have their own stylesheets).
   ============================================================ */

:root {
  --bg:         #0b0b0b;
  /* Full black, and opaque. The cards are objects SET INTO the sand: their
     faces stay clear and the grain banks up in the gaps around them, rather
     than piling on top and hazing the text. See the card block in sand.js. */
  --bg-card:    #000000;
  --bg-card-h:  #0a0a0a;
  --border:     rgba(255, 255, 255, 0.10);
  --border-h:   rgba(255, 255, 255, 0.14);
  --divider:    rgba(255, 255, 255, 0.06);
  --text:       #eaeaea;
  --text-muted: #9a9a9a;
  --text-faint: #6f6f6f;
  --accent:     #eaeaea;   /* monochrome — buttons, glows, board */
  --accent2:    #bdbdbd;   /* soft white — labels, meta accents */
  --amber:      #ffb02e;
  --gold:       #c9a96e;   /* champagne — micro-accent only */
  --font:       ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono:  ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --font-pixel: var(--font-mono); /* legacy var used by game styles */
  --page-width: 1080px;
  --section-gap: 112px;    /* between major sections */
  --head-gap: 16px;        /* section heading -> first body element */
  --stack-gap: 32px;       /* between entries within a section */
  --radius: 14px;          /* cards */
  --radius-s: 6px;         /* buttons, chips */
}
@media (max-width: 700px) {
  :root { --section-gap: 88px; }
}

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

html {
  scroll-behavior: smooth;
  background: #050505;
  overscroll-behavior-y: none;
  /* Clip stray horizontal overflow from the fixed .bg-blob ambients. `clip`,
     not `hidden`: it does not create a scroll container, so position:sticky
     (the nav and the projects showcase) still resolves against the viewport. */
  overflow-x: hidden;
  /* Hide the scrollbar for a cleaner, more immersive page — scrolling still
     works by wheel, trackpad, keyboard, and drag. */
  scrollbar-width: none;         /* Firefox */
  -ms-overflow-style: none;      /* old Edge */
}
@supports (overflow: clip) { html { overflow-x: clip; } }
html::-webkit-scrollbar, body::-webkit-scrollbar { width: 0; height: 0; display: none; }  /* Chrome/Safari */

body {
  background: radial-gradient(120% 80% at 50% 0%, #141414 0%, #0b0b0b 48%, #050505 100%) fixed var(--bg);
  color: var(--text);
  font-family: var(--font);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}

a { color: var(--accent2); text-decoration: none; }
img { max-width: 100%; }

::selection { background: #eaeaea; color: #0b0b0b; }

:focus-visible {
  outline: 2px solid var(--accent2);
  outline-offset: 2px;
  border-radius: 2px;
}

.hidden { display: none !important; }

/* ── Cursor: angular retro arrow, monochrome ─────────────── */
body {
  cursor: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M7%205.4%20L7%2016.2%20Q7%2018.6%208.6%2016.9%20L11.1%2014.2%20L15.0%2014.15%20Q17.6%2014.1%2015.9%2012.6%20L8.3%205.3%20Q7%204.0%207%205.4%20Z%22%20fill%3D%22%230b0b0b%22%20stroke%3D%22%23eaeaea%22%20stroke-width%3D%221.2%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E") 7 4, auto;
}
a, button, [onclick], .pcb-comp, .nf-sample-btn, .nf-upload, label.nf-check {
  cursor: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M7%205.4%20L7%2016.2%20Q7%2018.6%208.6%2016.9%20L11.1%2014.2%20L15.0%2014.15%20Q17.6%2014.1%2015.9%2012.6%20L8.3%205.3%20Q7%204.0%207%205.4%20Z%22%20fill%3D%22%23eaeaea%22%20stroke%3D%22%23060708%22%20stroke-width%3D%221.2%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E") 7 4, pointer;
}


/* ── Page transitions ───────────────────────────────────── */
/* THE DATUM SCAN, one pass, no held black frame.

   Chromium/Safari: cross-document View Transitions carry it — the OLD page
   snapshot holds still while the NEW page wipes down over it behind the
   rule, so the line scans directly from yesterday into tomorrow. The JS in
   transitions.js rides the pagereveal event to draw the rule itself.  */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: none; }
::view-transition-new(root) {
  animation: ptScanIn 800ms cubic-bezier(.22, .68, .16, 1) both;   /* 440 and 640 both read as rushed */
}
@keyframes ptScanIn {
  from { clip-path: inset(0 0 100% 0); }
  to   { clip-path: inset(0 0 0 0); }
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
}

/* Firefox (no cross-document VT): the click navigates natively — paint
   holding keeps the old page on screen while the prefetched document loads —
   and the new page arrives pre-covered, then a SINGLE sweep reveals it.
   One pass, no leave sweep, no held black.

   The arrival half: the inline head gate stamps html.pt-covered before
   first paint whenever the previous page left through the scan, and this
   pseudo-element owns the screen until transitions.js swaps it for the real
   sweeping cover. No DOM needed, so there is no flash-of-new-page ever. */
html.pt-covered::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 99990;
  background: var(--bg);
}
.pt-scan-cover {
  position: fixed;
  inset: 0;
  z-index: 99991;
  background: var(--bg);
  pointer-events: none;
}
.pt-scan-line {
  position: fixed;
  left: 0; right: 0; top: 0;
  height: 1px;
  z-index: 99992;
  background: var(--text);
  box-shadow: 0 0 12px rgba(234, 234, 234, 0.65);
  pointer-events: none;
  will-change: transform;
}
.pt-scan-line span {
  position: absolute;
  right: 14px; top: -17px;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.14em;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
}

/* ── Ambient background ─────────────────────────────────── */
.bg-blob {
  position: fixed;
  border-radius: 50%;
  filter: blur(120px);
  pointer-events: none;
  z-index: 0;
}
.bg-blob-1 { width: 480px; height: 480px; top: -160px; right: -120px; background: rgba(255, 255, 255, 0.028); }
.bg-blob-2 { width: 420px; height: 420px; bottom: -140px; left: -140px; background: rgba(255, 255, 255, 0.02); }
.bg-grid { display: none; }

/* ── Navigation ─────────────────────────────────────────── */
/* Datum Rule: the header is a machinist's rule. A faint hairline runs the
   full width; a brighter segment covers exactly the content column; the name
   and links stand on it, each with a graduation tick. The current page's tick
   is taller and white — that is the whole active state, no pill, no fill. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(11, 11, 11, 0.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);   /* faint rule, full bleed */
  transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.4s ease;
}
.site-header.nav-hidden {
  transform: translateY(-101%);
  opacity: 0;
  pointer-events: none;
}
.site-nav {
  --rule-gap: 13px;                 /* baseline of the row down to the rule */
  position: relative;
  max-width: 1080px;
  margin: 0 auto;
  padding: 17px 24px var(--rule-gap);
  display: flex;
  align-items: flex-end;            /* everything stands on the rule */
  justify-content: space-between;
  gap: 20px;
}
/* the measured span — brighter segment covering exactly the column */
.site-nav::after {
  content: "";
  position: absolute;
  left: 24px; right: 24px; bottom: -1px;
  height: 1px;
  background: rgba(255, 255, 255, 0.16);
}
/* settle canvas rides the rule; grain gathers to the active tick on arrival */
.nav-sand {
  position: absolute;
  left: 0; right: 0; bottom: -8px;
  width: 100%; height: 16px;
  pointer-events: none;
  z-index: 1;
}

.nav-logo { position: relative; display: flex; align-items: center; gap: 9px; }
/* The PLINTH mark, seated top-left on the rule. On the home page's first
   visit it is hidden until the boot's built mark flies up and docks here —
   the swap happens the frame the flight ends, geometry identical, so it
   reads as the same object coming to rest. Everywhere else it just is. */
.nav-mark { width: 18px; height: 18px; flex: 0 0 18px; }
.nav-mark svg { display: block; width: 100%; height: 100%; fill: var(--text); }
html.booting .nav-mark { opacity: 0; }
.nav-logo::before {           /* datum mark centered under the wordmark */
  content: "";
  position: absolute;
  left: 50%; bottom: calc(-1 * var(--rule-gap));
  width: 1px; height: 8px;
  transform: translateX(-50%);
  background: rgba(255, 255, 255, 0.16);
}
.nav-logo-text {
  display: block;
  font-weight: 700;
  font-size: 15px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--text);
  white-space: nowrap;
  cursor: pointer;
}

.nav-links { display: flex; align-items: flex-end; gap: 28px; }

.nav-link {
  position: relative;
  display: block;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1;
  transition: color 0.15s ease;
}
/* graduation tick: 1px, rises off the rule at the label's left edge */
.nav-link::before {
  content: "";
  position: absolute;
  left: 50%; bottom: calc(-1 * var(--rule-gap));
  width: 1px; height: 5px;
  background: rgba(255, 255, 255, 0.2);
  transform: translateX(-50%);           /* tick centered under the label */
  transform-origin: 50% 100%;
  transition: transform 0.18s ease, background-color 0.18s ease, height 0.2s cubic-bezier(0.2,0.7,0.2,1);
}
.nav-link:hover { color: var(--text); }
.nav-link:hover::before { transform: translateX(-50%) scaleY(1.8); background: rgba(255, 255, 255, 0.55); }
.nav-link:focus-visible { color: var(--text); outline-offset: 5px; }
.nav-link:focus-visible::before { transform: translateX(-50%) scaleY(1.8); background: var(--text); }
.nav-link[aria-current="page"], .nav-link.active { color: var(--text); }
.nav-link[aria-current="page"]::before,
.nav-link.active::before { height: 9px; background: var(--text); transform: translateX(-50%); }
/* honest metadata — the link is literally a PDF */
.nav-ext {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  color: var(--text-faint);
  margin-left: 6px;
  transition: color 0.15s ease;
}
.nav-link:hover .nav-ext { color: var(--text-muted); }
.nav-cta { display: flex; gap: 10px; }

@media (max-width: 480px) {
  .site-nav { --rule-gap: 11px; padding: 15px 16px var(--rule-gap); gap: 16px; }
  .site-nav::after { left: 16px; right: 16px; }
  /* phone header: fewer competing weights, more air. The PDF chip is honest
     metadata but at this width it is the thing making the row feel crowded. */
  .nav-logo { gap: 7px; }
  .nav-mark { width: 15px; height: 15px; flex: 0 0 15px; }
  .nav-logo-text { font-size: 13.5px; }
  .nav-links { gap: 14px; }
  .nav-link { font-size: 12px; }
  .nav-ext { display: none; }
}

.btn-ghost {
  display: inline-block;
  background: none;
  font-family: var(--font);
  cursor: pointer;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 500;
  border: 1px solid var(--border-h);
  border-radius: 999px;
  padding: 8px 17px;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.btn-ghost:hover { border-color: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.05); }

.btn-primary {
  display: inline-block;
  background: #eaeaea;
  color: #0b0b0b;
  font-weight: 650;
  font-size: 14px;
  border: 0;
  border-radius: 999px;
  padding: 11px 22px;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.btn-primary:hover {
  transform: translateY(-1px);
  background: #fff;
  box-shadow: 0 6px 22px rgba(255, 255, 255, 0.12);
}

/* ── PCB hero ───────────────────────────────────────────── */
.hero {
  position: relative;
  min-height: 640px;
  height: calc(100vh - 120px);
  max-height: 820px;
  border-bottom: 1px solid var(--border);
  overflow: hidden;
  z-index: 1;
}
#pcb-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.hero-panel {
  position: absolute;
  left: 12%;
  top: 46%;
  transform: translateY(-50%);
  z-index: 2;
  max-width: min(460px, 46%);
  background: rgba(13, 13, 13, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 6px;
  padding: 30px 32px 28px;
  box-shadow: 0 0 44px rgba(255, 255, 255, 0.04);
}
.hero-panel::before {
  /* pin-1 indicator, like a real IC */
  content: "";
  position: absolute;
  top: 10px;
  left: 10px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.5);
}
.hero-title {
  font-size: clamp(26px, 3.4vw, 38px);
  line-height: 1.14;
  font-weight: 750;
  letter-spacing: -0.02em;
  color: #f2f2f2;
  margin: 0 0 12px;
  text-wrap: balance;
}
.hero-subtitle {
  color: var(--text-muted);
  font-size: 15px;
  line-height: 1.55;
  margin: 0 0 22px;
}
.hero-actions { display: flex; gap: 10px; flex-wrap: wrap; }

.hero-now {
  margin: 20px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--divider);
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.hero-now b { color: var(--accent2); font-weight: 400; }
/* Keeps the availability phrase whole so a phone breaks after the
   location rather than inside "seeking internship". */
.hero-now .hd-seek { white-space: nowrap; }

.pcb-comp {
  position: absolute;
  z-index: 2;
  background: rgba(15, 15, 15, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 5px;
  padding: 10px 14px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  color: #c9c9c9;
  cursor: pointer;
  user-select: none;
  transition: box-shadow 0.25s ease, border-color 0.25s ease, color 0.2s ease;
}
.pcb-comp .c-des {
  display: block;
  font-size: 8.5px;
  letter-spacing: 0.14em;
  color: var(--text-faint);
  margin-bottom: 3px;
}
.pcb-comp:hover,
.pcb-comp.lit {
  border-color: var(--accent);
  color: #fff;
  box-shadow: 0 0 22px rgba(255, 255, 255, 0.15);
}
.pcb-comp:hover .c-des,
.pcb-comp.lit .c-des { color: var(--accent2); }

.hero-comp-row {
  position: absolute;
  left: 8%;
  bottom: 12%;
  z-index: 2;
  display: flex;
  gap: 12px;
}
.pcb-comp { position: static; }

.hero-hint {
  position: absolute;
  bottom: 14px;
  right: 18px;
  z-index: 3;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  pointer-events: none;
}

@media (max-width: 860px) {
  /* The hero used to collapse to its content on mobile — about 390px inside
     an 844px viewport — so the swells were crammed into the top third and
     then chopped by the section border. Give it real height so the field has
     somewhere to go, and centre the panel within it. svh, not vh: the small
     viewport unit doesn't change when the URL bar collapses, so the terrain
     can't be resized mid-scroll. */
  .hero {
    height: auto;
    min-height: 78svh;
    padding: 56px 0 72px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* the mask below handles the transition; a hard rule here was the seam */
    border-bottom: none;
  }
  #pcb-canvas {
    opacity: 0.85;
    /* Let the swells dissolve into the page instead of ending on an edge. */
    -webkit-mask-image: linear-gradient(to bottom, #000 0 58%, transparent 100%);
            mask-image: linear-gradient(to bottom, #000 0 58%, transparent 100%);
  }
  .hero-panel {
    position: relative;
    left: auto;
    top: auto;
    transform: none;
    margin: 0 auto;
    max-width: min(460px, calc(100% - 48px));
  }
  .pcb-comp {
    position: relative;
    inset: auto !important;
    display: inline-block;
  }
  .hero-comp-row {
    position: relative;
    left: auto;
    bottom: auto;
    z-index: 2;
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
    margin: 22px auto 0;
    padding: 0 24px;
  }
  .hero-hint { display: none; }
}


/* ── Sections ───────────────────────────────────────────── */
section { position: relative; z-index: 1; }

.about-section,
.projects-teaser,
.resume-section,
#projects {
  max-width: var(--page-width);
  margin: 0 auto;
  padding: var(--section-gap) 24px 0;
}

.section-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 10px;
}
.section-title {
  font-size: clamp(24px, 3vw, 32px);
  font-weight: 750;
  letter-spacing: -0.02em;
  margin-bottom: var(--head-gap);
  text-wrap: balance;
}
.section-description {
  color: var(--text-muted);
  font-size: 15.5px;
  line-height: 1.7;
  max-width: 68ch;
  margin-bottom: 8px;
}
.section-description strong { color: var(--text); }

/* word-split title animation (applied by interactive.js) */
/* The reveal clips to the line box, but headings run line-height < 1
   (.proj-title is 0.95), so a descender — the j in "Projects" — fell
   outside it and got sheared off. Pad the clip box downward and pull the
   same amount back off the margin so nothing shifts, then start the inner
   lower so it still clears the taller box while hidden. */
.word-wrap {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  /* Bottom room for descenders (the j in "Projects") and side room for the
     right-hand overhang of heavy glyphs (the s in "Projects" was shearing on
     the clip edge). The negative margins cancel the padding so nothing shifts. */
  padding: 0 0.08em 0.2em;
  margin: 0 -0.08em -0.2em;
}
.word-inner {
  display: inline-block;
  transform: translateY(135%);
  opacity: 0;
  transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1), opacity 0.45s ease;
}
.word-inner.word-in { transform: translateY(0); opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .word-inner { transform: none; opacity: 1; transition: none; }
}

/* ── About ──────────────────────────────────────────────── */
.avatar-row { display: flex; gap: 10px; margin: 18px 0 6px; }
.avatar-link { display: inline-block; }
.about-avatar {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--border-h);
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.about-avatar:hover { border-color: var(--accent); transform: translateY(-1px); }

.about-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  margin-top: var(--stack-gap);
}
@media (max-width: 1000px) { .about-grid { grid-template-columns: repeat(2, 1fr); } }
/* Phone screens sit the cards against a brighter surround and the seep reads
   differently at one column, so pure black loses its edge entirely. Lift them
   just clear of the ground. Desktop keeps #000000. */
@media (max-width: 860px) {
  :root { --bg-card: #121212; --bg-card-h: #181818; }
}
@media (max-width: 560px)  { .about-grid { grid-template-columns: 1fr; } }
.about-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px 22px;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.about-card:hover { border-color: var(--border-h); background: var(--bg-card-h); }
.konami-hint {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.86em;
  letter-spacing: 0.14em;
  color: var(--text);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 8px;
  margin-left: 2px;
  white-space: nowrap;
}
.about-card-icon {
  color: var(--accent2);
  margin-bottom: 12px;
}
.about-card-icon svg { width: 20px; height: 20px; }
.about-card h3 { font-size: 15.5px; font-weight: 650; margin-bottom: 8px; }
.about-card p { font-size: 13.5px; color: var(--text-muted); }

/* ── Featured project cards (home teaser) ───────────────── */
.projects-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
  gap: 16px;
  margin-top: var(--stack-gap);
}
.project-card {
  position: relative;
  display: block;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px 26px;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}
.project-card:hover {
  border-color: rgba(255, 255, 255, 0.3);
  background: var(--bg-card-h);
  transform: translateY(-2px);
}
.project-card-tag {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent2);
  margin-bottom: 10px;
}
.project-card h3 { font-size: 17px; font-weight: 700; margin-bottom: 8px; color: var(--text); }
.project-card p { font-size: 13.5px; color: var(--text-muted); }
.project-card-arrow {
  position: absolute;
  top: 22px;
  right: 22px;
  color: var(--text-faint);
  transition: color 0.15s ease, transform 0.15s ease;
}
.project-card:hover .project-card-arrow { color: var(--accent2); transform: translateX(2px); }

/* ── Projects page: case studies ────────────────────────── */
.page-head { padding-top: 8px; }
.page-head p { color: var(--text-muted); font-size: 15.5px; line-height: 1.7; max-width: 68ch; }

.case {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-top: 24px;
  padding: 30px 32px;
  transition: border-color 0.2s ease;
}
.case:hover { border-color: var(--border-h); }
.case-meta {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent2);
  margin-bottom: 10px;
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}
.case-meta .dim { color: var(--text-faint); }
.case h3 { font-size: 21px; font-weight: 750; margin-bottom: 10px; }
.case > p { font-size: 14.5px; color: var(--text-muted); max-width: 72ch; margin-bottom: 16px; }
.case > p strong { color: var(--text); }

.spec-strip {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 18px;
  max-width: 100%;
}
.spec {
  padding: 10px 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
}
.spec b {
  display: block;
  font-family: var(--font-mono);
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.spec span {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.case-links { display: flex; gap: 18px; flex-wrap: wrap; }
.case-links a { font-size: 13.5px; font-weight: 600; }
.case-links .quiet { color: var(--text-faint); font-weight: 400; font-size: 13px; }

.case-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 16px;
  margin-top: 24px;
}
.case-sm {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color 0.2s ease, transform 0.2s ease;
}
.case-sm:hover { border-color: var(--border-h); transform: translateY(-2px); }
.case-sm img {
  width: 100%;
  height: 170px;
  object-fit: cover;
  display: block;
  border-bottom: 1px solid var(--border);
  filter: saturate(0.9);
}
.case-sm-body { padding: 18px 20px 20px; }
.case-sm .case-meta { margin-bottom: 6px; }
.case-sm h3 { font-size: 16px; font-weight: 700; margin-bottom: 6px; }
.case-sm p { font-size: 13px; color: var(--text-muted); margin-bottom: 10px; }
.case-sm a { font-size: 13px; font-weight: 600; }

/* ── Timeline ───────────────────────────────────────────── */
.resume-cols {
  display: grid;
  grid-template-columns: 3fr 2fr;
  gap: 56px;
}
@media (max-width: 900px) { .resume-cols { grid-template-columns: 1fr; } }

.timeline { margin-top: var(--stack-gap); position: relative; }
.tl-item {
  position: relative;
  padding: 0 0 30px 28px;
  border-left: 1px solid var(--border);
}
.tl-item:last-child { border-left-color: transparent; padding-bottom: 0; }
.tl-dot {
  position: absolute;
  left: -5px;
  top: 4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
}
.tl-dot.edu { background: var(--accent2); }
.tl-dot.done { background: transparent; border: 1px solid var(--border-h); }
.tl-date {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.tl-body h4 { font-size: 15.5px; font-weight: 650; margin: 4px 0 2px; }
.tl-org { display: block; font-size: 13px; color: var(--accent2); margin-bottom: 6px; }
.tl-body p { font-size: 13.5px; line-height: 1.6; color: var(--text-muted); max-width: 56ch; }
.tl-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--text);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--radius-s);
  padding: 4px 10px;
  margin-top: 2px;
}
.tl-badge svg { width: 12px; height: 12px; color: var(--accent2); }

/* ── Skills + awards ────────────────────────────────────── */
.skills-groups { margin-top: var(--stack-gap); display: grid; gap: 22px; }
.skill-group h4 {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 10px;
  font-weight: 600;
}
.skill-tags { display: flex; gap: 8px; flex-wrap: wrap; }
.skill-tag {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 5px 11px;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.skill-tag:hover { border-color: rgba(255, 255, 255, 0.3); color: var(--text); }

.awards-list {
  margin-top: 34px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 22px 24px;
}
.awards-list h4 {
  font-size: 13px;
  font-weight: 650;
  color: var(--text);
  margin-bottom: 14px;
}
.award-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--text-muted);
  padding: 5px 0;
}
.award-icon { color: var(--accent2); display: inline-flex; }
.award-icon svg { width: 14px; height: 14px; }

/* ── Scroll presentation (scroll-fx.js writes --p; CSS owns motion) ── */
.case, .case-sm, .project-card, .about-card { --p: 1; }
html.fx .case, html.fx .case-sm, html.fx .project-card, html.fx .about-card {
  --pe: calc(var(--p) * var(--p) * (3 - 2 * var(--p)));  /* smoothstep */
  transform: translate3d(0, calc((1 - var(--pe)) * 2.5rem), 0);
  opacity: calc(0.2 + var(--pe) * 0.8);
  will-change: transform, opacity;
}

html.settled .pcb-comp {
  transition: box-shadow 0.25s ease, border-color 0.25s ease, color 0.2s ease;
}

/* ── Open hero: type stands directly on the field ── */
.hero-panel.hero-open {
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  left: 8%;
  top: 22%;
  transform: none;
  max-width: 680px;
}
.hero-panel.hero-open::before { display: none; }
.hero-panel.hero-open .hero-title {
  font-size: clamp(34px, 4.6vw, 58px);
  text-shadow: 0 2px 26px rgba(0, 0, 0, 0.9);
}
.hero-panel.hero-open .hero-subtitle { font-size: 15.5px; }
.hero-panel.hero-open .hero-now { border-top: 0; padding-top: 6px; }
@media (max-width: 860px) {
  .hero-panel.hero-open {
    position: relative;
    margin: 0 0 0 24px;
    max-width: calc(100% - 48px);
  }
}

/* ── Pinned LIFT showcase v2 (html.fx only) ───────────────── */
#showcase { position: relative; }
/* break out of #projects' max-width container — the sticky stage is
   overflow:hidden, and a 1032px stage was clipping the 1100px wide
   slide's outer glyphs on both sides */
html.fx #showcase {
  width: 100vw;
  margin-left: calc(50% - 50vw);
}
#showcase .sc-stage { position: relative; }
.sc-rail { display: none; }
html.fx #showcase .sc-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  perspective: 70rem;
}
html.fx .sc-slide {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(780px, 90vw);
  transform: translate(-50%, -50%);
  will-change: transform, opacity;
}
html.fx .sc-slide .case { margin: 0; max-height: 76vh; overflow-y: auto; }
/* In the corridor the ghost index numeral sits BEHIND the card — a touch of
   translucency lets its LED dots glow through the plate. Scoped here so the
   home-page cards stay the solid objects-in-sand Hudson approved. */
html.fx .sc-slide .case { background: rgba(8, 8, 8, 0.82); }
html.fx .sc-slide .case:hover { background: rgba(12, 12, 12, 0.86); }
html.fx .sc-slide .case-grid { margin: 0; }
html.fx .sc-slide.wide { width: min(1100px, 94vw); }
html.fx .sc-slide.wide .case-grid {
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
}
html.fx .sc-slide.wide .case-sm img { height: 170px; }
html.fx .sc-slide.wide .case-sm p { font-size: 12.5px; }
html.fx .sc-slide img {
  transform: translateY(calc(var(--q, 0) * -14px));
  scale: 1.06;
  transition: none;
}

/* giant ghost index behind the slides — outlined, drifting at its
   own parallax rate, fading through slide handoffs */
.sc-ghost { display: none; }
html.fx .sc-ghost {
  display: flex;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: min(56vh, 44vw);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.26);
  opacity: 0;
  pointer-events: none;
  user-select: none;
  z-index: 0;
  will-change: transform;

  /* The numeral is painted as a real LED matrix onto a canvas (see
     scroll-fx.js). A CSS dot mask was tried first and rejected: masking
     punches holes through a solid glyph, so any stroke narrower than the
     pitch fragments and the digit reads as chopped rather than pixelated.
     Quantising the glyph to the grid instead lights whole cells, so
     strokes stay continuous runs of dots. */
  font-size: 0;
}
html.fx .sc-ghost canvas { display: block; }

/* photos: LED-dot renders; hover gives a slow zoom */
html.fx .sc-slide.wide .case-sm img { height: 215px; }
@media (hover: hover) {
  html.fx .sc-slide .case-sm img {
    transition: scale 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
  }
  html.fx .sc-slide .case-sm:hover img { scale: 1.13; }
}

/* projects splash: monument — centered, outlined 06 behind, jump index */
/* MASTHEAD. The splash used to stack three competing ideas on a centre
   axis: a 430px outlined numeral floating behind the title, with the eyebrow
   above it and the meta line below it in an identical mono treatment, so the
   block read mirrored rather than composed. This goes flush-left, hangs the
   two metadata lines off the ends of a single rule, and sets the title to the
   width of the column beneath it. Scale contrast does the work — no ornament.

   The ghost numeral is deleted rather than restyled: it was
   -webkit-text-stroke over transparent text with no fallback, so it rendered
   as nothing at all in Firefox. */
.proj-head {
  position: relative;
  text-align: left;
  padding-top: 34px;
}
.proj-head > * { position: relative; z-index: 1; }

/* eyebrow and meta paired on one rail, hung at its two ends */
.proj-band {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px 24px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.proj-band .section-label { margin-bottom: 0; }

.proj-title {
  /* Deliberately capped well below the column width. Every other
     .section-title on the site tops out at 32px; past roughly 190px this
     stops being emphasis and becomes the loudest thing on the whole site. */
  font-size: clamp(52px, 13vw, 190px);
  line-height: 0.86;
  letter-spacing: -0.045em;
  margin: clamp(18px, 2.4vw, 34px) 0 0;
}
.proj-meta {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.proj-index {
  list-style: none;
  margin: clamp(30px, 3.6vw, 52px) 0 0;
  padding: 0;
  columns: 2;
  column-gap: 60px;
  text-align: left;
  border-top: 1px solid var(--border);
}
.proj-index li { break-inside: avoid; }
@media (max-width: 700px) {
  .proj-index { columns: 1; max-width: 560px; }
}
.proj-index button {
  width: 100%;
  display: flex;
  align-items: baseline;
  gap: 16px;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  padding: 14px 4px;
  font-family: inherit;
  font-size: 15.5px;
  font-weight: 600;
  color: var(--text-muted);
  text-align: left;
  cursor: pointer;
  transition: color 0.2s ease, padding-left 0.3s ease;
}
/* Was `li:last-child, li:nth-child(3)` — which hardcoded "six entries in two
   columns" into CSS while the list is actually generated from the slides, so
   a seventh project would have silently broken the frame. One rule across the
   top of the list, one under every row, correct at any count. */
.proj-index button:hover { color: var(--text); padding-left: 16px; }
.proj-index .idx {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
}
.proj-index .arr {
  margin-left: auto;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.proj-index button:hover .arr { opacity: 1; }
/* mobile: the hardware trio stacks as three compact media rows.
   The filmstrip needed a horizontal swipe INSIDE a scroll-driven corridor,
   which iOS fought — cards showed up cut off mid-frame. Rows always fit,
   nothing inside a slide scrolls or swipes. */
@media (max-width: 768px) {
  html.fx .sc-slide.wide { width: 92vw; }
  html.fx .sc-slide.wide .case-grid {
    display: flex;
    flex-direction: column;
    gap: 9px;
    overflow: visible;
    padding-bottom: 0;
  }
  html.fx .sc-slide.wide .case-sm {
    display: grid;
    grid-template-columns: 86px 1fr;
    gap: 0 12px;
    align-items: start;
    width: 100%;
    padding: 10px 12px;
  }
  html.fx .sc-slide.wide .case-sm img {
    width: 86px; height: 86px; object-fit: cover;
    margin: 0; border-radius: 6px;
  }
  html.fx .sc-slide.wide .case-sm h3 { font-size: 13px; margin: 2px 0 4px; }
  html.fx .sc-slide.wide .case-sm .case-meta { font-size: 8.5px; }
  html.fx .sc-slide.wide .case-sm p {
    font-size: 11px; line-height: 1.45;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  html.fx .sc-slide.wide .case-sm a { font-size: 11px; }

  /* Cards never scroll inside themselves on a phone — they FIT instead.
     The Hollis Bloom card is the tall one, so the compression is sized to it:
     tighter body type, 2x2 stat tiles at reduced padding. */
  html.fx .sc-slide .case {
    max-height: none;
    overflow: visible;
    padding: 18px 16px 16px;
  }
  html.fx .sc-slide .case p { font-size: 12px; line-height: 1.5; }
  html.fx .sc-slide .case h3 { font-size: 19px; }
  .spec-strip { gap: 8px; }
  .spec { padding: 8px 10px; }
  .spec b { font-size: 14px; }
  .spec span { font-size: 8.5px; }
  html.fx .sc-slide.wide .case-sm img { height: 190px; }
}
.slide-eyebrow { display: none; }
html.fx .slide-eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 14px;
}
html.fx .sc-rail {
  display: flex;
  position: absolute;
  right: 26px;
  top: 50%;
  transform: translateY(-50%);
  flex-direction: column;
  gap: 10px;
}
html.fx .sc-slide .case > *,
html.fx .sc-slide .slide-eyebrow,
html.fx .sc-slide .case-sm {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}
html.fx .sc-slide.active .case > *,
html.fx .sc-slide.active .slide-eyebrow,
html.fx .sc-slide.active .case-sm {
  opacity: 1;
  transform: none;
}
html.fx .sc-slide.active .case > :nth-child(2), html.fx .sc-slide.active .case-sm:nth-child(2) { transition-delay: 0.07s; }
html.fx .sc-slide.active .case > :nth-child(3), html.fx .sc-slide.active .case-sm:nth-child(3) { transition-delay: 0.14s; }
html.fx .sc-slide.active .case > :nth-child(4) { transition-delay: 0.21s; }
html.fx .sc-slide.active .case > :nth-child(5) { transition-delay: 0.28s; }

.sc-counter {
  display: none;
  position: absolute;
  top: 82px;
  left: 32px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.sc-counter b { color: var(--text); font-weight: 600; }
html.fx .sc-counter {
  display: block;
  opacity: 0;
  transition: opacity 0.35s ease;
}
html.fx .sc-counter.show { opacity: 1; }

.sc-rail i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  transition: background 0.2s, transform 0.2s;
}
.sc-rail i.on { background: #eaeaea; transform: scale(1.5); }

/* ── Contact band ───────────────────────────────────────── */
.contact-band {
  max-width: var(--page-width);
  margin: var(--section-gap) auto 0;
  padding: 0 24px;
  position: relative;
  z-index: 1;
}
.contact-inner {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 40px 44px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}
.contact-inner h2 {
  font-size: clamp(20px, 2.6vw, 26px);
  font-weight: 750;
  letter-spacing: -0.02em;
  margin-bottom: 6px;
}
.contact-inner p { color: var(--text-muted); font-size: 14.5px; max-width: 52ch; line-height: 1.6; }
.contact-actions { display: flex; gap: 10px; flex-wrap: wrap; }
@media (max-width: 700px) { .contact-inner { padding: 30px 26px; } }

/* ── Footer ─────────────────────────────────────────────── */
.site-footer {
  border-top: 1px solid var(--border);
  margin-top: var(--section-gap);
  position: relative;
  z-index: 1;
}
.footer-inner {
  max-width: 1080px;
  margin: 0 auto;
  padding: 26px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.footer-logo { font-weight: 700; font-size: 14px; color: var(--text); display: inline-flex; align-items: center; gap: 8px; }
.footer-copy { font-size: 12.5px; color: var(--text-faint); text-align: center; }
.footer-copy b { color: var(--text-muted); font-weight: 400; font-variant-numeric: tabular-nums; }
.footer-links { display: flex; gap: 18px; }
.footer-link { font-size: 13px; color: var(--text-muted); transition: color 0.15s ease; }
.footer-link:hover { color: var(--text); }
.led {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px rgba(255, 255, 255, 0.7);
}

/* ============================================================
   EASTER-EGG / OVERLAY STYLES (ported — used by interactive.js,
   features.js, main.js; keep selectors intact)
   ============================================================ */

/* Particle constellation background */
#particle-canvas { display: none; } /* constellation retired — gradient + grain instead */

/* Section indicator (bottom-right label) */
#section-indicator {
  display: none !important;
  position: fixed;
  bottom: 32px;
  right: 28px;
  z-index: 200;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(236, 233, 241, 0.22);
  opacity: 0;
  transform: translateX(12px);
  transition: opacity 0.4s ease, transform 0.4s ease, color 0.3s;
  pointer-events: none;
}
#section-indicator.sec-visible { opacity: 1; transform: translateX(0); }

/* Scroll hint */
#scroll-hint {
  position: fixed;
  bottom: 36px;
  left: 50%;
  transform: translateX(-50%) translateY(20px);
  opacity: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  z-index: 200;
  pointer-events: none;
  transition: opacity 0.6s ease, transform 0.6s ease;
}
#scroll-hint.hint-show { opacity: 1; transform: translateX(-50%) translateY(0); }
#scroll-hint.hint-hide { opacity: 0; transform: translateX(-50%) translateY(10px); }
.scroll-hint-line {
  width: 1px;
  height: 40px;
  background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.55));
  /* a slow breath — at 1.6s this read as blinking */
  animation: scrollLineAnim 3.2s cubic-bezier(.45,0,.35,1) infinite;
}
@keyframes scrollLineAnim {
  0%   { transform: scaleY(0); transform-origin: top; }
  42%  { transform: scaleY(1); transform-origin: top; }
  50%  { transform: scaleY(1); transform-origin: top; }
  51%  { transform: scaleY(1); transform-origin: bottom; }
  92%  { transform: scaleY(0); transform-origin: bottom; }
  100% { transform: scaleY(0); transform-origin: bottom; }
}
.scroll-hint-text {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(236, 233, 241, 0.25);
}

/* Hidden decode message at footer */
#scroll-decode-wrap {
  padding: 14px 16px 6px;
  text-align: center;
  cursor: default;
  line-height: 1.8;
}
/* keyboard-only easter-egg hints — pointless and cramped on touch screens */
@media (max-width: 768px) {
  #scroll-decode-wrap { display: none; }
}
#scroll-decode-hint {
  font-family: var(--font);
  font-size: 10px;
  color: rgba(255, 255, 255, 0.4);
  letter-spacing: 2px;
  opacity: 0;
  margin-bottom: 6px;
  user-select: none;
  transition: opacity 0.4s ease, color 0.35s ease, text-shadow 0.35s ease;
}
#scroll-decode-msg {
  text-align: center;
  font-family: var(--font);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 4px;
  color: var(--accent2);
  opacity: 0;
  user-select: none;
  text-shadow: 0 0 12px rgba(255, 255, 255, 0.7);
  transition: opacity 0.4s ease, color 0.35s ease, text-shadow 0.35s ease, letter-spacing 0.35s ease;
}
#scroll-decode-wrap:hover #scroll-decode-hint {
  color: rgba(255, 255, 255, 0.8);
  text-shadow: 0 0 8px rgba(255, 255, 255, 0.4);
}
#scroll-decode-wrap:hover #scroll-decode-msg {
  color: #ffffff;
  letter-spacing: 6px;
  text-shadow: 0 0 14px rgba(255, 255, 255, 0.9), 0 0 32px rgba(255, 255, 255, 0.5), 0 0 60px rgba(255, 255, 255, 0.2);
}

/* Old static grain retired in favor of the drifting sand layer */
#grain-canvas { display: none; }

/* Drifting sand. z-index 2 draws it OVER the solid cards (screen blend), and
   the shader mounds the grain over each card — densest at the card centre,
   fading to nothing at the edges — so it reads as a pile of sand sitting on
   the card. Kept low-opacity so the copy underneath stays legible. */
#sand-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
  opacity: 0.3;
  /* No mix-blend-mode. The grain carries its own alpha (see sand.js), so it
     composites normally and lands on top of the cards in every engine. The
     blend mode used to decide, per browser, whether the sand sat over the
     cards or behind them. */
}

/* Mouse spotlight */
#mouse-spotlight {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  transition: background 0.12s ease;
}

/* Scroll progress bar */
#scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 0;
  background: rgba(255, 255, 255, 0.6);
  z-index: 300;
  pointer-events: none;
}

/* ---------- Clicker game (hidden avatar easter egg) ---------- */
.clicker-game {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 24px;
  max-width: 600px;
  margin: 40px auto;
  text-align: center;
}
.clicker-area {
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--border-h);
  border-radius: 8px;
  height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  margin: 16px 0;
  transition: background 0.2s;
}
.clicker-area:hover  { background: rgba(255, 255, 255, 0.05); }
.clicker-area:active { background: rgba(255, 255, 255, 0.08); }
.clicker-area h3 { font-size: 1.4em; color: #fff; }
.score-display {
  font-family: var(--font-pixel);
  font-size: 11px;
  color: var(--text-muted);
  margin-top: 12px;
}
.clicker-game.hidden { display: none !important; }

/* ── Cursor: angular retro arrow, monochrome ─────────────── */
body {
  cursor: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M7%205.4%20L7%2016.2%20Q7%2018.6%208.6%2016.9%20L11.1%2014.2%20L15.0%2014.15%20Q17.6%2014.1%2015.9%2012.6%20L8.3%205.3%20Q7%204.0%207%205.4%20Z%22%20fill%3D%22%230b0b0b%22%20stroke%3D%22%23eaeaea%22%20stroke-width%3D%221.2%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E") 7 4, auto;
}
a, button, [onclick], .pcb-comp, .nf-sample-btn, .nf-upload, label.nf-check {
  cursor: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M7%205.4%20L7%2016.2%20Q7%2018.6%208.6%2016.9%20L11.1%2014.2%20L15.0%2014.15%20Q17.6%2014.1%2015.9%2012.6%20L8.3%205.3%20Q7%204.0%207%205.4%20Z%22%20fill%3D%22%23eaeaea%22%20stroke%3D%22%23060708%22%20stroke-width%3D%221.2%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E") 7 4, pointer;
}


/* ── Page transitions ───────────────────────────────────── */
/* (page transition defined once above, via @view-transition) */

/* ---------- Firmware console intro (mobile-safe) ---------- */
#firmware-overlay {
  position: fixed;
  inset: 0;
  background: #0b0b0b;
  z-index: 999999;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  font-family: 'Courier New', monospace;
  transition: opacity 0.5s ease;
  cursor: pointer;
}
#firmware-overlay.fw-fade { opacity: 0; pointer-events: none; }
#firmware-inner { width: 480px; max-width: 94vw; }
.fw-topbar {
  background: #1e1f2e;
  border-radius: 6px 6px 0 0;
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 7px;
  border: 1px solid #2a2b3d;
  border-bottom: none;
}
.fw-dot          { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.fw-dot-red      { background: #ff5f57; }
.fw-dot-yellow   { background: #febc2e; }
.fw-dot-green    { background: #28c840; }
.fw-title {
  font-size: 11px;
  color: #555;
  margin-left: 10px;
  font-family: 'Courier New', monospace;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fw-body {
  background: #1a1b26;
  border: 1px solid #2a2b3d;
  border-radius: 0 0 6px 6px;
  padding: 18px 22px 22px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(255, 255, 255, 0.03);
  max-height: 80vh;
  overflow-y: auto;
}
.fw-output-line {
  font-size: 12.5px;
  color: #ccc;
  margin-bottom: 2px;
  font-family: 'Courier New', monospace;
  line-height: 1.7;
  white-space: pre;
}
.fw-output-line.fw-art {
  font-size: clamp(7px, 2.1vw, 12.5px);
  line-height: 1.5;
}
@media (max-width: 480px) {
  .fw-body { padding: 14px 16px 18px; }
  .fw-output-line { font-size: 11px; line-height: 1.65; }
}

/* hero staggers in as the POST fades */
html.fw-boot .hero-panel > *,
html.fw-boot .pcb-comp {
  opacity: 0;
  transform: translateY(10px);
}
html.fw-booted .hero-panel > *,
html.fw-booted .pcb-comp {
  opacity: 1;
  transform: none;
  transition: opacity 0.55s ease, transform 0.55s ease;
}
html.fw-booted .hero-panel > :nth-child(2) { transition-delay: 0.07s; }
html.fw-booted .hero-panel > :nth-child(3) { transition-delay: 0.14s; }
html.fw-booted .hero-panel > :nth-child(4) { transition-delay: 0.21s; }
html.fw-booted .hero-panel > :nth-child(5) { transition-delay: 0.28s; }
html.fw-booted .pcb-comp { transition-delay: 0.4s; }
@media (prefers-reduced-motion: reduce) {
  html.fw-boot .hero-panel > *, html.fw-boot .pcb-comp { opacity: 1; transform: none; }
}

/* ---------- CLI terminal overlay ---------- */
#cli-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  z-index: 99998;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
#cli-overlay.cli-open { display: flex; }
#cli-window {
  width: 700px;
  max-width: 95vw;
  max-height: 72vh;
  background: #1a1b26;
  border: 1px solid #2a2b3d;
  border-radius: 6px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(255, 255, 255, 0.04);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: cliSlideIn 0.16s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes cliSlideIn {
  from { opacity: 0; transform: scale(0.97) translateY(8px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}
#cli-titlebar {
  background: #16161e;
  border-bottom: 1px solid #2a2b3d;
  padding: 8px 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  user-select: none;
}
#cli-titlebar-label {
  font-size: 12px;
  color: #565f89;
  font-family: 'Courier New', monospace;
  letter-spacing: 0.5px;
}
#cli-close-btn {
  color: #3b3c54;
  font-size: 14px;
  cursor: pointer;
  line-height: 1;
  transition: color 0.15s;
  user-select: none;
  padding: 1px 4px;
  border-radius: 3px;
}
#cli-close-btn:hover { color: #f7768e; background: rgba(247, 118, 142, 0.12); }
#cli-output {
  flex: 1;
  overflow-y: auto;
  padding: 14px 18px 4px;
  font-family: 'Courier New', monospace;
  font-size: 13px;
  line-height: 1.65;
  color: #a9b1d6;
  scrollbar-width: thin;
  scrollbar-color: #2a2b3d #1a1b26;
}
.cli-line     { white-space: pre-wrap; word-break: break-word; }
.cli-cmd      { color: #a9b1d6; }
.cli-out      { color: #a9b1d6; }
.cli-err      { color: #f7768e; }
.cli-info     { color: #7dcfff; }
.cli-success  { color: #9ece6a; }
.cli-dim      { color: #3b3c54; }
.cli-p-user   { color: #9ece6a; font-weight: 700; }
.cli-p-at     { color: #a9b1d6; }
.cli-p-host   { color: #9ece6a; font-weight: 700; }
.cli-p-tilde  { color: #7dcfff; font-weight: 700; }
.cli-p-dollar { color: #bb9af7; font-weight: 700; }
.cli-nf-art   { color: #7dcfff; }
.cli-nf-key   { color: #7dcfff; font-weight: 700; }
.cli-nf-val   { color: #a9b1d6; }
#cli-input-row {
  display: flex;
  align-items: center;
  padding: 6px 18px 12px;
  border-top: 1px solid #1e1f2e;
  flex-shrink: 0;
  gap: 0;
}
#cli-prompt {
  font-family: 'Courier New', monospace;
  font-size: 13px;
  white-space: nowrap;
  flex-shrink: 0;
  user-select: none;
}
#cli-input {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: #c0caf5;
  font-family: 'Courier New', monospace;
  font-size: 13px;
  caret-color: #7dcfff;
}
#cli-input::placeholder { color: #2a2b3d; }
#cli-slash-hint {
  display: none !important;
  position: fixed;
  bottom: 18px;
  right: 22px;
  font-size: 9px;
  font-family: var(--font);
  font-weight: 700;
  color: rgba(236, 233, 241, 0.18);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  pointer-events: auto;
  cursor: pointer;
  padding: 10px 12px;
  z-index: 200;
  user-select: none;
  transition: color 0.2s ease;
}
#cli-slash-hint:hover { color: rgba(236, 233, 241, 0.55); }
@media (hover: none) { #cli-slash-hint {
  display: none !important; color: rgba(236, 233, 241, 0.35); } }

/* ---------- Blueprint mode ---------- */
body.blueprint {
  --bg:        #002a56;
  --bg-card:   rgba(255, 255, 255, 0.04);
  --bg-card-h: rgba(255, 255, 255, 0.07);
  --border:    rgba(255, 255, 255, 0.2);
  --border-h:  rgba(255, 255, 255, 0.38);
  --text:      #ffffff;
  --text-muted: rgba(255, 255, 255, 0.68);
  --text-faint: rgba(255, 255, 255, 0.35);
  --accent:    #90caff;
  --accent2:   #b8dfff;
  --amber:     #ffe9a8;
  background: #002a56 !important;
}
@media (hover: hover) {
  body.blueprint,
  body.blueprint a,
  body.blueprint button,
  body.blueprint [role="button"] {
    cursor: url('assets/icons/eraser_cursor.svg') 6 26, auto !important;
  }
}
body.blueprint #cursor-dot,
body.blueprint #cursor-ring { opacity: 0 !important; }
body.blueprint::before {
  content: '';
  position: fixed;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px);
  background-size: 40px 40px;
  pointer-events: none;
  z-index: 0;
}
body.blueprint .bg-blob { opacity: 0 !important; }
body.blueprint .hero { border-bottom-style: dashed; }
body.blueprint #pcb-canvas { opacity: 0.35; }
body.blueprint .section-label { color: #b8dfff; }
body.blueprint .btn-primary {
  background: linear-gradient(180deg, rgba(16, 54, 98, 0.9), rgba(9, 32, 63, 0.92)) !important;
  color: #e8f5ff !important;
  border: 1.5px dashed rgba(144, 202, 255, 0.75) !important;
  border-radius: 4px;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.14),
    0 0 0 1px rgba(144, 202, 255, 0.22),
    0 0 18px rgba(144, 202, 255, 0.2) !important;
  font-family: 'Courier New', monospace !important;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}
body.blueprint .btn-primary:hover {
  background: linear-gradient(180deg, rgba(24, 74, 128, 0.95), rgba(11, 42, 82, 0.95)) !important;
  border-color: rgba(184, 223, 255, 0.95) !important;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.2),
    0 0 0 1px rgba(184, 223, 255, 0.32),
    0 0 24px rgba(144, 202, 255, 0.28) !important;
}
body.blueprint .footer-logo { color: #b8dfff; text-shadow: none; }
body.blueprint .site-header {
  background: rgba(0, 20, 55, 0.88) !important;
  border-bottom-color: rgba(255, 255, 255, 0.2) !important;
}
body.blueprint .resume-section { border-top-color: rgba(255, 255, 255, 0.2) !important; }
body.blueprint .site-footer    { border-top-color: rgba(255, 255, 255, 0.2) !important; background: transparent !important; }
body.blueprint .hero-panel { background: transparent; border-style: dashed; box-shadow: none; }
body.blueprint .pcb-comp   { background: transparent; border-style: dashed; box-shadow: none; }
body.blueprint .about-card,
body.blueprint .project-card,
body.blueprint .case,
body.blueprint .case-sm,
body.blueprint .awards-list,
body.blueprint .skill-tag {
  border-style: dashed !important;
  border-color: rgba(180, 220, 255, 0.3) !important;
}
body.blueprint .about-card,
body.blueprint .project-card,
body.blueprint .case { position: relative; }
body.blueprint .about-card::after,
body.blueprint .project-card::after,
body.blueprint .case::after {
  content: '■ DETAIL';
  position: absolute;
  bottom: 7px;
  right: 10px;
  font-size: 8px;
  letter-spacing: 1.5px;
  color: rgba(180, 220, 255, 0.22);
  font-family: 'Courier New', monospace;
  pointer-events: none;
  user-select: none;
}
body.blueprint .tl-dot      { background: #90caff; box-shadow: 0 0 0 1px #90caff; }
body.blueprint .tl-dot.edu  { background: #b8dfff; box-shadow: 0 0 0 1px #b8dfff; }
body.blueprint .tl-dot.done { background: transparent; box-shadow: 0 0 0 1px rgba(144, 202, 255, 0.3); border-color: rgba(144, 202, 255, 0.3); }
body.blueprint *:not(i):not(svg):not(path):not(circle):not(line):not(polyline):not(rect):not(polygon) {
  font-family: 'Courier New', monospace !important;
}

/* Mode toast */
#mode-toast {
  position: fixed;
  bottom: 22px;
  left: 50%;
  transform: translateX(-50%) translateY(16px);
  background: rgba(13, 11, 18, 0.92);
  border: 1px solid var(--border-h);
  border-radius: 6px;
  padding: 7px 20px;
  font-size: 9px;
  font-family: var(--font);
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-muted);
  opacity: 0;
  pointer-events: none;
  z-index: 99999;
  transition: opacity 0.3s ease, transform 0.3s ease;
  user-select: none;
}
#mode-toast.toast-show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Blueprint interactive (sketch, drag, smudge, crinkle) */
body.blueprint .about-card,
body.blueprint .project-card,
body.blueprint .case,
body.blueprint .tl-item,
body.blueprint .skill-group,
body.blueprint .award-row,
body.blueprint .hero-title,
body.blueprint .hero-subtitle,
body.blueprint .section-title,
body.blueprint .section-description,
body.blueprint .section-label,
body.blueprint .nav-logo,
body.blueprint .nav-links,
body.blueprint .site-footer {
  filter: url(#bp-sketch);
}
body.blueprint [data-lucide],
body.blueprint svg {
  filter: url(#bp-sketch) drop-shadow(0 0 1px rgba(144, 202, 255, 0.5));
}
body.blueprint h1, body.blueprint h2, body.blueprint h3, body.blueprint h4,
body.blueprint p, body.blueprint span, body.blueprint a, body.blueprint li {
  text-shadow: 0.4px 0.4px 0 rgba(144, 202, 255, 0.35), -0.2px 0.2px 0 rgba(80, 160, 255, 0.2) !important;
}
body.blueprint a { pointer-events: none !important; }
body.blueprint [data-bp-drag] a { pointer-events: none !important; }
body.blueprint [data-bp-drag] {
  cursor: url('assets/icons/eraser_cursor.svg') 6 26, grab !important;
  user-select: none !important;
  position: relative;
  z-index: 1;
}
body.blueprint [data-bp-drag].bp-dragging {
  cursor: url('assets/icons/eraser_cursor.svg') 6 26, grabbing !important;
  z-index: 200 !important;
}
body.blueprint [data-bp-drag]:hover {
  outline: 1px dashed rgba(144, 202, 255, 0.45) !important;
  outline-offset: 5px;
}
body.blueprint .bp-smudged-1 { filter: blur(1.5px) !important; opacity: 0.72 !important; }
body.blueprint .bp-smudged-2 { filter: blur(3.5px) !important; opacity: 0.56 !important; }
body.blueprint .bp-smudged-3 { filter: blur(7px) !important; opacity: 0.38 !important; }
body.blueprint.bp-ruined,
body.blueprint.bp-ruined * {
  cursor: url('assets/icons/eraser_cursor.svg') 6 26, pointer !important;
}
.bp-ruined-overlay {
  position: fixed;
  inset: 0;
  z-index: 99998;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 10, 28, 0.72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  animation: bp-overlay-in 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes bp-overlay-in { from { opacity: 0; } to { opacity: 1; } }
.bp-ruined-card {
  background: rgba(0, 22, 55, 0.94);
  border: 1.5px dashed rgba(144, 202, 255, 0.45);
  border-radius: 4px;
  padding: 44px 52px 40px;
  text-align: center;
  max-width: 380px;
  width: 90vw;
  font-family: 'Courier New', monospace !important;
  animation: bp-card-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
  position: relative;
}
@keyframes bp-card-in {
  from { opacity: 0; transform: scale(0.88) rotate(-1.5deg); }
  to   { opacity: 1; transform: scale(1) rotate(-0.4deg); }
}
.bp-ruined-stamp {
  font-size: 11px;
  letter-spacing: 5px;
  color: rgba(144, 202, 255, 0.35);
  margin-bottom: 18px;
  font-family: 'Courier New', monospace !important;
}
.bp-ruined-title {
  font-family: 'Courier New', monospace !important;
  font-size: 26px;
  font-weight: 700;
  color: #ffffff;
  letter-spacing: 2px;
  margin: 0 0 10px;
  text-shadow: 0 0 24px rgba(144, 202, 255, 0.4);
  filter: url(#bp-sketch);
}
.bp-ruined-sub {
  font-family: 'Courier New', monospace !important;
  font-size: 11px;
  letter-spacing: 1.5px;
  color: rgba(144, 202, 255, 0.55);
  margin: 0 0 32px;
}
.bp-ruined-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
}
.bp-btn {
  font-family: 'Courier New', monospace !important;
  font-size: 10px;
  letter-spacing: 2px;
  padding: 10px 22px;
  border-radius: 3px;
  cursor: url('assets/icons/eraser_cursor.svg') 6 26, pointer !important;
  border: 1.5px dashed;
  background: transparent;
  transition: background 0.18s, color 0.18s, border-color 0.18s;
  pointer-events: all !important;
  text-transform: lowercase;
}
.bp-btn-crinkle { color: #ffffff; border-color: rgba(144, 202, 255, 0.6); }
.bp-btn-crinkle:hover {
  background: rgba(144, 202, 255, 0.12);
  border-color: rgba(144, 202, 255, 1);
  color: #b8dfff;
}

/* ============================================================
   FIRST-VISIT BOOT
   The overlay is in the markup and shown by html.booting, which
   an inline <head> script sets before the body is parsed — added
   later by JS it would flash the page first. Colours are literal
   rather than tokens so the boot can never inherit a theme.
   ============================================================ */
#boot {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #0b0b0b;
  display: none;
  align-items: center;
  justify-content: center;
}
html.booting #boot { display: flex; }
html.booting, html.booting body { overflow: hidden; }
#boot svg { width: min(30vmin, 280px); height: auto; display: block; }
#boot rect { fill: #eaeaea; transform-box: fill-box; }
/* never hold a reduced-motion visitor behind a held animation */
@media (prefers-reduced-motion: reduce) {
  #boot { display: none !important; }
  html.booting, html.booting body { overflow: auto; }
}

/* ============================================================
   PAGE TRANSITION FALLBACK
   @view-transition above only fires in browsers with CROSS-document
   view transitions. Firefox has the same-document API but not the
   navigation one, so there the declaration is inert and every page
   change is a bare reload. transitions.js detects that and adds
   .pt-fade, which turns on this opacity-only crossfade instead.
   Nothing moves, so it is safe under prefers-reduced-motion — it
   just runs shorter (see --pt-out).
   ============================================================ */
html.pt-fade body {
  transition: opacity var(--pt-out, 170ms) ease;
}
html.pt-fade.pt-enter body {
  opacity: 0;
}

/* Blueprint mode is a drafting table, not a beach — no grain in it, ever.
   sand.js writes opacity inline as you scroll, so this has to win outright.
   It also sidesteps the fixed-positioning trouble blueprint's perspective
   transform causes for a position:fixed canvas. */
body.blueprint #sand-canvas {
  display: none !important;
}

/* The way out is on the sheet: a drafting-table control, blueprint voice */
#bp-scratch-btn {
  position: fixed;
  right: 18px; bottom: 18px;
  z-index: 99500;
  font-family: 'Courier New', monospace;
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #b8dfff;
  background: rgba(0, 22, 55, 0.92);
  border: 1.5px dashed rgba(144, 202, 255, 0.5);
  border-radius: 3px;
  padding: 10px 14px;
  cursor: pointer !important;
  display: none;
}
body.blueprint #bp-scratch-btn { display: block; }
#bp-scratch-btn:hover {
  background: rgba(144, 202, 255, 0.16);
  border-style: solid;
  color: #e4f2ff;
}
