/* ─────────────────────────────────────────────────────────────
   style.css — Personal Portfolio

   Table of contents:
     1.  Design tokens
     2.  Reset & base
     3.  Typography helpers
     4.  Nav & footer
     5.  Hero           (slow-breathing radial glow)
     6.  Region base    (shared section styles)
     7.  Coding section (deep navy grid-drift background)
     8.  Creative section (warm slate radial-pulse background)
     9.  Project explorer split-pane (shared: coding + creative)
     10. About & headshot
     11. Scroll-reveal animations
     12. Responsive
     13. Reduced-motion overrides
     14. Lightbox (expanded image viewer)
   ───────────────────────────────────────────────────────────── */


/* ── 1. Design Tokens ──────────────────────────────────────── */
:root {
  /* Backgrounds */
  --bg:          #0A0E17;   /* page base / hero */
  --bg-coding:   #0B1220;   /* deep navy for coding section */
  --bg-creative: #120E18;   /* dark warm slate for creative section */
  --bg-card:     #131929;   /* card & panel surfaces */
  --bg-surface:  #0F1726;   /* slightly lighter surface */

  /* Text */
  --text:        #EDEEF2;
  --text-sub:    #8891A4;
  --text-muted:  #3D4560;

  /* Accents */
  --accent-coding:   #4FC3E8;   /* cool cyan  */
  --accent-creative: #E8A857;   /* warm gold  */
  --accent-hero-a:   #3FE0C5;   /* teal  */
  --accent-hero-b:   #6C5CE7;   /* violet */

  /* RGB triples of the two above, for rgba() tints — see --accent-rgb */
  --accent-coding-rgb:   79, 195, 232;
  --accent-creative-rgb: 232, 168, 87;

  /* Borders */
  --border:       rgba(255, 255, 255, 0.055);
  --border-strong: rgba(255, 255, 255, 0.10);

  /* Typography */
  --font-display: 'Space Grotesk', system-ui, sans-serif;
  --font-body:    'Inter', system-ui, sans-serif;
  --font-mono:    'IBM Plex Mono', 'Courier New', monospace;

  /* Layout */
  --section-v:  clamp(5rem, 10vw, 9rem);
  --content-h:  clamp(1.25rem, 5vw, 3rem);
  --max-w:      1200px;
  --nav-h:      60px;

  /* Easing */
  --ease-out:   cubic-bezier(0.16, 1, 0.3, 1);
}


/* ── 2. Reset & Base ───────────────────────────────────────── */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scroll-behavior: smooth;
  scroll-padding-top: var(--nav-h);
}

body {
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.65;
  overflow-x: hidden;
  /* Prevents double-tap-to-zoom on mobile without blocking normal
     scrolling or pinch-zoom (WCAG 1.4.4). */
  touch-action: manipulation;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img {
  display: block;
  max-width: 100%;
  height: auto;
}

a { color: inherit; text-decoration: none; }
ul { list-style: none; }

:focus-visible {
  outline: 2px solid var(--accent-hero-a);
  outline-offset: 4px;
  border-radius: 3px;
}


/* ── 3. Typography Helpers ─────────────────────────────────── */
h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.025em;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-muted);
  display: block;
  margin-bottom: 0.85rem;
}

.region--coding   .eyebrow { color: var(--accent-coding); }
.region--creative .eyebrow { color: var(--accent-creative); }
.region--about    .eyebrow { color: var(--text-sub); }


/* ── 4. Nav & Footer ───────────────────────────────────────── */
#site-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--content-h);
  background: rgba(10, 14, 23, 0.88);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border-bottom: 1px solid var(--border);
  z-index: 100;
  transform: translateY(-100%);
  transition: transform 0.45s var(--ease-out);
}

#site-nav.nav--visible {
  transform: translateY(0);
}

.nav-name {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--text-sub);
  letter-spacing: 0.06em;
}

.nav-links { display: flex; gap: 2rem; }

.nav-links a {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-sub);
  transition: color 0.2s;
}

.nav-links a:hover { color: var(--text); }

/* Shared pill-button look — accent-hero-a outline that inverts to a
   filled pill on hover. Also backs .contact-button in the About section
   (10. About & Headshot, below); each context layers its own padding/
   font sizing on top instead of repeating the border/color/hover. */
.pill-button {
  border: 1px solid var(--accent-hero-a);
  border-radius: 20px;
  color: var(--accent-hero-a);
  transition: background 0.2s, color 0.2s;
}

.pill-button:hover {
  background: var(--accent-hero-a);
  color: var(--bg);
}

.nav-links a.nav-link--resume {
  padding: 5px 14px;
}

/* .nav-links a (above) sets color/transition at higher specificity than
   .pill-button alone (an extra .nav-links class), so it would otherwise
   win and leave the resume pill's text gray with no background
   transition — this re-asserts both at matching specificity. */
.nav-links a.pill-button {
  color: var(--accent-hero-a);
  transition: background 0.2s, color 0.2s;
}

/* .nav-links a:hover (above) is likewise more specific than
   .pill-button:hover alone, so it would otherwise win and leave
   the resume pill's hover text the wrong color — this re-asserts the
   pill's hover at matching specificity. */
.nav-links a.pill-button:hover {
  background: var(--accent-hero-a);
  color: var(--bg);
}

.site-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2rem var(--content-h);
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-sub);
}

.footer-links { display: flex; gap: 1.75rem; }
.footer-links a { transition: color 0.2s; }
.footer-links a:hover { color: var(--text); }


/* ── 5. Hero ───────────────────────────────────────────────── */
.hero {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--bg);
  position: relative;
  overflow: hidden;
}

/* Slow-breathing radial glow behind hero content */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 70% 55% at 50% 38%, rgba(63, 224, 197, 0.13) 0%, transparent 70%),
    radial-gradient(ellipse 45% 40% at 72% 68%, rgba(108, 92, 231, 0.09) 0%, transparent 65%),
    radial-gradient(ellipse 35% 30% at 22% 60%, rgba(63, 224, 197, 0.05) 0%, transparent 60%);
  animation: hero-breathe 7s ease-in-out infinite;
  pointer-events: none;
}

@keyframes hero-breathe {
  0%, 100% { opacity: 0.55; }
  50%       { opacity: 1.0; }
}

/* Skill-logo swarm canvas — see setupSkillSwarm() in script.js. Sits
   between .hero::before (no z-index, i.e. base stacking) and
   .hero-content (z-index: 1) purely via DOM order, so it never covers
   the text. pointer-events: none is mandatory: the canvas overlaps
   .scroll-cue's clickable area, and this is what lets clicks pass
   through to it untouched. */
.skill-swarm {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.hero-content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 2rem var(--content-h);
  max-width: 700px;
}

/* Wraps .hero-name + .hero-tagline so they can be nudged down together
   (below) without moving .scroll-cue. display/flex-direction/align-items
   here just re-establish the same flex-item sizing .hero-name and
   .hero-tagline had as direct children of .hero-content — otherwise, as
   plain block children of this wrapper, they'd stretch to its full width
   instead of each shrinking to its own text width, throwing off
   text-align: center. */
.hero-heading {
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* .hero-content is centered as a whole (title + tagline + scroll cue), so
   the title/tagline pair itself sits above that midpoint — the scroll
   cue's height below has no equal counterweight above. Nudge the pair
   down so it lands in the center of #skill-swarm's orbit ring, which is
   centered on the raw hero midpoint (see setupSkillSwarm() in script.js).
   Only applied where that ring is actually drawn — same fine-pointer,
   motion-safe condition script.js checks before animating it — so touch/
   reduced-motion visitors keep the plain centered layout. 4.125rem is
   measured (half of the fixed vertical space below the pair: .hero-
   tagline's 4rem margin-bottom + .scroll-cue's 1rem margin-top + its own
   height — none of which scale with viewport); re-measure if any of
   those change. */
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .hero-heading {
    transform: translateY(4.125rem);
  }
}

.hero-name {
  font-size: clamp(3rem, 8vw, 6.5rem);
  font-weight: 700;
  letter-spacing: -0.03em;
  margin-bottom: 1rem;
  background: linear-gradient(135deg, var(--accent-hero-a) 0%, var(--accent-hero-b) 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.hero-tagline {
  font-family: var(--font-body);
  font-size: clamp(0.95rem, 2vw, 1.1rem);
  letter-spacing: 0;
  color: var(--text-sub);
  line-height: 1.65;
  max-width: 560px;
  margin-bottom: 4rem;
}

.scroll-cue {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  margin-top: 1rem;
  color: var(--text-muted);
  transition: color 0.2s;
  animation: bob 2.5s ease-in-out infinite;
}

.scroll-cue:hover { color: var(--text-sub); }
.scroll-cue-label { font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.22em; text-transform: uppercase; }
.scroll-cue-arrow { font-size: 1.1rem; }

@keyframes bob {
  0%, 100% { transform: translateY(0); }
  50%       { transform: translateY(10px); }
}


/* ── 6. Region Base ────────────────────────────────────────── */
/* Shared by both content sections — each adds its own background */
.region {
  position: relative;
  overflow: hidden;
  padding: var(--section-v) var(--content-h);
  max-width: 100%;
  border-top: 1px solid var(--border);
}

/* Inner content wrapper caps width and centers horizontally */
.region-inner {
  max-width: var(--max-w);
  margin: 0 auto;
}

.region-header {
  max-width: var(--max-w);
  margin: 0 auto clamp(2.5rem, 5vw, 4rem);
}

.region-header h2 {
  font-size: clamp(2.4rem, 5vw, 3.8rem);
  margin-bottom: 0.85rem;
}

.region--coding   .region-header h2 { color: var(--accent-coding); }
.region--creative .region-header h2 { color: var(--accent-creative); }

/* Short decorative line before the eyebrow */
.region--coding   .eyebrow::before,
.region--creative .eyebrow::before {
  content: '';
  display: inline-block;
  width: 20px;
  height: 1px;
  vertical-align: middle;
  margin-right: 0.6rem;
  opacity: 0.7;
}

.region--coding   .eyebrow::before { background: var(--accent-coding); }
.region--creative .eyebrow::before { background: var(--accent-creative); }

.region-sub {
  font-size: 0.92rem;
  color: var(--text-sub);
  line-height: 1.75;
  /* No max-width — let it run the same width as the explorer/images
     below instead of sitting in its own narrower column. */
}

.region-sub + .region-sub {
  margin-top: 1.875rem;
}

.region-sub--emphasis {
  font-weight: 600;
  color: var(--text);
}


/* ── 7. Coding Section Background ──────────────────────────── */
/* Deep navy with a drifting fine-grid overlay — technical/systematic feel */
.region--coding {
  background-color: var(--bg-coding);
  /* Shared explorer/gallery/lightbox component styles reference the
     generic --accent(-rgb) rather than a hardcoded color, so the same
     markup re-themes automatically per section. */
  --accent:     var(--accent-coding);
  --accent-rgb: var(--accent-coding-rgb);
}

.region--coding::before {
  content: '';
  position: absolute;
  inset: 0;
  /* Faint grid: two repeating gradients, one horizontal one vertical */
  background-image:
    repeating-linear-gradient(
      0deg,
      transparent,
      transparent 39px,
      rgba(79, 195, 232, 0.04) 39px,
      rgba(79, 195, 232, 0.04) 40px
    ),
    repeating-linear-gradient(
      90deg,
      transparent,
      transparent 39px,
      rgba(79, 195, 232, 0.03) 39px,
      rgba(79, 195, 232, 0.03) 40px
    );
  background-size: 40px 40px;
  /* Animating backgroundPosition makes the grid appear to drift */
  animation: grid-drift 18s linear infinite;
  pointer-events: none;
  z-index: 0;
}

@keyframes grid-drift {
  from { background-position: 0 0; }
  to   { background-position: 40px 40px; }
}

/* Content sits above the animated pseudo-element */
.region--coding > * { position: relative; z-index: 1; }


/* ── 8. Creative Section Background ────────────────────────── */
/* Dark warm slate with a slowly pulsing radial glow — organic/creative feel */
.region--creative {
  background-color: var(--bg-creative);
  --accent:     var(--accent-creative);
  --accent-rgb: var(--accent-creative-rgb);
}

.region--creative::before {
  content: '';
  position: absolute;
  /* Oversized so the scale animation doesn't clip */
  inset: -30%;
  background: radial-gradient(
    ellipse 60% 55% at 55% 45%,
    rgba(232, 168, 87, 0.07) 0%,
    rgba(180, 80, 120, 0.04) 45%,
    transparent 70%
  );
  animation: art-pulse 10s ease-in-out infinite;
  pointer-events: none;
  z-index: 0;
}

@keyframes art-pulse {
  0%, 100% { transform: scale(1)   rotate(0deg);  opacity: 0.6; }
  33%       { transform: scale(1.2) rotate(4deg);  opacity: 1.0; }
  66%       { transform: scale(0.9) rotate(-3deg); opacity: 0.7; }
}

.region--creative > * { position: relative; z-index: 1; }


/* ── 9. Project Explorer (Shared: Coding + Creative) ───────── */
.project-explorer {
  display: grid;
  /* Sidebar column sizes to the widest tab label (no wrap/scroll needed);
     minmax floors it at 220px so a short project list doesn't look cramped. */
  grid-template-columns: minmax(220px, max-content) 1fr;
  border: 1px solid var(--border);
  max-width: var(--max-w);
  margin: 0 auto;
  min-height: 540px;
  /* Clip the corners of both child panels */
  border-radius: 6px;
  overflow: hidden;
}

/* LEFT: sidebar project list */
.explorer-sidebar {
  background: rgba(10, 14, 23, 0.6);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: 0.5rem 0;
  overflow-y: auto;
  /* Setting only overflow-y computes overflow-x as auto too (per spec),
     which can surface a stray horizontal scrollbar from sub-pixel
     overflow. The column is now sized to fit its content, so this
     should never be needed — but state it explicitly to be sure. */
  overflow-x: hidden;
}

/* Mobile-only project picker — see Responsive. Same options as the
   tab list below, swapped in below the breakpoint since a horizontal
   list of tabs doesn't fit small screens well. */
.explorer-mobile-nav {
  display: none;
}

.explorer-tab {
  all: unset;   /* wipe browser button defaults */
  display: block;
  /* No explicit width: the sidebar's flex `align-items: stretch`
     default already fills the cross axis. An explicit width: 100%
     here conflicts with the grid column's max-content sizing pass
     above and causes a few px of horizontal overflow. */
  padding: 0.85rem 1.2rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  color: var(--text-sub);
  cursor: pointer;
  border-left: 3px solid transparent;
  transition: color 0.18s, background 0.18s, border-color 0.18s;
  text-align: left;
  line-height: 1.4;
}

.explorer-tab:hover {
  color: var(--text-sub);
  background: rgba(var(--accent-rgb), 0.05);
}

.explorer-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.explorer-tab.active {
  color: var(--accent);
  border-left-color: var(--accent);
  background: rgba(var(--accent-rgb), 0.08);
}

/* Small category label under the project name in the tab */
.explorer-tab-category {
  display: block;
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: 2px;
  opacity: 0.7;
}

.explorer-tab.active .explorer-tab-category {
  opacity: 1;
}

/* RIGHT: detail panel */
.explorer-panel {
  background: var(--bg-card);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Preview zone. Two layouts share this box:
   - default: one image/video (or placeholder) at a time — Coding
   - .explorer-preview--grid: a grid of thumbnails — Creative */
.explorer-preview {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #090E1B;
  overflow: hidden;
  position: relative;
  flex-shrink: 0;
  border-bottom: 1px solid var(--border);
}

.explorer-preview--grid {
  aspect-ratio: auto;
  max-height: 420px;
  overflow-y: auto;
  /* Setting only overflow-y computes overflow-x as auto too (per
     spec), which can surface a stray horizontal scrollbar — state
     it explicitly instead. */
  overflow-x: hidden;
  position: static;
}

/* ── Slide layout (Coding): one item at a time, arrows + dots ── */
.gallery {
  position: relative;
  width: 100%;
  height: 100%;
}

.gallery-slide {
  width: 100%;
  height: 100%;
  cursor: zoom-in;
}

/* Videos play inline via their own controls rather than expanding
   into the lightbox, so the zoom-in cursor doesn't apply to them. */
.gallery-slide:has(video) {
  cursor: default;
}

.gallery-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.gallery-slide video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  background: #000;
}

.gallery-arrow {
  all: unset;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  line-height: 1;
  color: var(--text);
  background: rgba(10, 14, 23, 0.55);
  border-radius: 50%;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.18s, background 0.18s;
}

.gallery:hover .gallery-arrow,
.gallery:focus-within .gallery-arrow {
  opacity: 1;
}

/* Touch devices have no :hover state to reveal the arrows on, so
   show them plainly there — swipe is available too, but shouldn't
   be the only visible way to move between images. */
@media (hover: none) {
  .gallery-arrow {
    opacity: 1;
  }
}

.gallery-arrow:hover  { background: rgba(10, 14, 23, 0.85); }
.gallery-arrow--prev  { left: 0.7rem; }
.gallery-arrow--next  { right: 0.7rem; }

.gallery-dots {
  position: absolute;
  left: 50%;
  bottom: 0.7rem;
  transform: translateX(-50%);
  display: flex;
  gap: 0.4rem;
}

.gallery-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.3);
  cursor: pointer;
  transition: background 0.18s, transform 0.18s;
}

.gallery-dot.active {
  background: var(--text);
  transform: scale(1.25);
}

.gallery-arrow:focus-visible,
.gallery-dot:focus-visible {
  outline: 2px solid var(--accent-hero-a);
  outline-offset: 2px;
}

/* ── Grid layout (Creative): all thumbnails at once, click to expand ── */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 0.6rem;
  padding: 0.75rem;
}

.gallery-cell {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 4px;
  overflow: hidden;
  background: #0F1522;
}

/* Only image cells expand into the lightbox — videos use their own
   native controls, so no zoom-in affordance on those. */
.gallery-cell--clickable {
  cursor: zoom-in;
  transition: opacity 0.18s;
}

.gallery-cell--clickable:hover,
.gallery-cell--clickable:focus-visible {
  opacity: 0.85;
}

.gallery-cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.gallery-cell video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  background: #000;
}

.gallery-cell .gallery-placeholder-icon  { font-size: 1.6rem; }
.gallery-cell .gallery-placeholder-label { font-size: 0.56rem; }

/* ── Placeholder (shared by both layouts) ── */
.gallery-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

.gallery-placeholder-icon {
  font-size: 2rem;
  opacity: 0.15;
}

.gallery-placeholder-label {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
  opacity: 0.4;
  text-align: center;
  padding: 0 0.4rem;
}

/* Info area below the preview */
.explorer-info {
  padding: 1.8rem 2rem 2rem;
  flex: 1;
  overflow-y: auto;
}

.explorer-info-category {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.6rem;
}

.explorer-info h3 {
  font-size: clamp(1.3rem, 2.2vw, 1.6rem);
  font-family: var(--font-display);
  margin-bottom: 0.5rem;
}

/* Outcome line — one proof point rendered just below the title */
.explorer-outcome {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--accent);
  line-height: 1.55;
  margin-bottom: 0.9rem;
}

.explorer-info-desc {
  font-size: 0.88rem;
  color: var(--text-sub);
  line-height: 1.75;
  margin-bottom: 1rem;
}

/* "What I learned" block */
.explorer-learned {
  margin: 1.1rem 0 1rem;
  padding: 0.9rem 1.1rem;
  background: rgba(var(--accent-rgb), 0.04);
  border-left: 2px solid rgba(var(--accent-rgb), 0.3);
  border-radius: 0 3px 3px 0;
}

.explorer-learned-label {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.55rem;
}

.explorer-learned-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.explorer-learned-list li {
  font-size: 0.84rem;
  color: var(--text-sub);
  line-height: 1.6;
  padding-left: 1.1rem;
  position: relative;
}

.explorer-learned-list li::before {
  content: '—';
  position: absolute;
  left: 0;
  font-family: var(--font-mono);
  color: rgba(var(--accent-rgb), 0.5);
}

/* Tag chips */
.explorer-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 1.25rem;
}

.explorer-tag {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  background: rgba(var(--accent-rgb), 0.14);
  border: 1px solid rgba(var(--accent-rgb), 0.4);
  padding: 3px 9px;
  border-radius: 2px;
}

/* Links row */
.explorer-links {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.explorer-link {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 7px 16px;
  border: 1px solid var(--accent);
  color: var(--accent);
  transition: background 0.18s, color 0.18s;
  border-radius: 2px;
}

.explorer-link:hover {
  background: var(--accent);
  color: var(--bg);
}

.explorer-link.secondary {
  border-color: var(--border-strong);
  color: var(--text-sub);
}

.explorer-link.secondary:hover {
  border-color: var(--text-sub);
  color: var(--text);
  background: transparent;
}

/* Transition for panel content swaps */
.explorer-panel.swapping {
  opacity: 0;
  transition: opacity 0.15s;
}

.explorer-panel.swapping-in {
  opacity: 1;
  transition: opacity 0.2s;
}



/* ── 10. Skills Filter Bar (inside Technical section) ───────── */
/* Sits between the region header and the explorer it targets — see
   index.html — so a click never has to leave the section, only swap
   the already-visible explorer to the matching project. */
/* Same low-opacity dark blue + grey outline as .explorer-sidebar /
   .project-explorer below, so the skills bar reads as part of the
   same container system instead of its own distinct card style. */
.skills-filter {
  max-width: var(--max-w);
  box-sizing: border-box;
  margin: 0 auto clamp(1.75rem, 3.5vw, 2.5rem);
  padding: 1.25rem 1.5rem;
  background: rgba(10, 14, 23, 0.6);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.skills-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* Colored via the --chip-color custom property set inline by
   setupSkills() (one per tag, from SKILL_COLOR_GROUPS) — that's the
   whole "grouped by color instead of a label" mechanism: same-hue
   chips read as one cluster with no group heading printed. A tag
   with no assigned color (--chip-color unset) falls back to the
   neutral border/text below. */
.skill-chip {
  all: unset;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  padding: 6px 13px;
  border: 1px solid var(--chip-color, var(--border-strong));
  border-radius: 20px;
  color: var(--chip-color, var(--text-sub));
  transition: background 0.18s, transform 0.12s;
}

.skill-chip:hover,
.skill-chip:focus-visible {
  background: rgba(255, 255, 255, 0.08);
  transform: translateY(-1px);
}

.skill-chip:focus-visible {
  outline: 2px solid var(--chip-color, var(--accent-hero-a));
  outline-offset: 2px;
}

.skill-chip:active { transform: scale(0.96); }

/* Tags with no matching project still show, just not clickable proof
   — dimmed so the interactive ones read as the primary path. In
   practice every current tag matches at least one project. */
.skill-chip:disabled {
  cursor: default;
  opacity: 0.4;
}

/* Cross-highlight: set on a sidebar tab in the Technical explorer by
   setupSkills() in script.js while a matching skill chip has
   hover/focus. --skill-match-color is set inline to that same chip's
   color, so the highlight reinforces the chip's own color-coding
   rather than introducing a separate signal color. */
.explorer-tab.explorer-tab--skill-match {
  background: rgba(255, 255, 255, 0.05);
  border-left-color: var(--skill-match-color, var(--accent-hero-a));
  box-shadow: inset 0 0 0 1px var(--skill-match-color, var(--accent-hero-a));
}


/* ── 11. About & Headshot ───────────────────────────────────── */
.region--about {
  background-color: var(--bg);
  max-width: 100%;
  padding-left: var(--content-h);
  padding-right: var(--content-h);
}

.about-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
  max-width: var(--max-w);
  margin: 0 auto;
}

.about-text h2 {
  font-size: clamp(2.5rem, 5vw, 4rem);
  margin: 0.5rem 0 1.6rem;
}

.about-text p {
  font-size: 0.92rem;
  color: var(--text-sub);
  line-height: 1.85;
  margin-bottom: 1rem;
}

.about-text em { font-style: italic; color: var(--text); }

.contact-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 1.5rem;
}

/* .pill-button (4. Nav & Footer, above) provides the border/color/hover;
   this just adds this context's sizing. */
.contact-button {
  display: inline-block;
  padding: 7px 16px;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
}

.headshot-wrap {
  aspect-ratio: 1 / 1;
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: inset 0 0 80px rgba(63, 224, 197, 0.03);
}

.headshot-wrap img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}


/* ── 12. Scroll-Reveal ─────────────────────────────────────── */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition:
    opacity 0.65s var(--ease-out),
    transform 0.65s var(--ease-out);
}

.reveal.revealed {
  opacity: 1;
  transform: translateY(0);
}


/* ── 13. Responsive ────────────────────────────────────────── */

/* Explorer: collapse to stacked layout at medium width */
@media (max-width: 860px) {
  .project-explorer {
    grid-template-columns: 1fr;
  }

  /* Sidebar becomes a dropdown project picker at the top; the tab
     list doesn't fit small screens well, so it's hidden here. */
  .explorer-sidebar {
    border-right: none;
    border-bottom: 1px solid var(--border);
    padding: 0.75rem;
  }

  .explorer-tab {
    display: none;
  }

  /* Styled to read as an obviously-tappable control: accent border,
     a visible chevron (native selects render one inconsistently,
     especially on mobile), a raised shadow, and a couple of brief
     pulses on load to draw the eye to it the first time. */
  .explorer-mobile-nav {
    display: block;
    width: 100%;
    padding: 0.85rem 2.5rem 0.85rem 1rem;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    font-weight: 500;
    letter-spacing: 0.05em;
    color: var(--text);
    background-color: rgba(var(--accent-rgb), 0.1);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23EDEEF2' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 1rem center;
    background-size: 12px 8px;
    appearance: none;
    -webkit-appearance: none;
    border: 1.5px solid var(--accent);
    border-radius: 6px;
    box-shadow: 0 2px 12px rgba(var(--accent-rgb), 0.18);
    cursor: pointer;
    transition: background-color 0.2s, box-shadow 0.2s, transform 0.15s;
    animation: dropdown-attention 1.6s ease-in-out 0.4s 2;
  }

  .explorer-mobile-nav:hover {
    background-color: rgba(var(--accent-rgb), 0.16);
    box-shadow: 0 3px 16px rgba(var(--accent-rgb), 0.28);
  }

  .explorer-mobile-nav:active {
    transform: scale(0.98);
  }

  .explorer-mobile-nav:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }

  @keyframes dropdown-attention {
    0%, 100% {
      box-shadow: 0 2px 12px rgba(var(--accent-rgb), 0.18);
      border-color: var(--accent);
    }
    50% {
      box-shadow: 0 2px 22px rgba(var(--accent-rgb), 0.5);
      border-color: var(--accent-hero-a);
    }
  }

  .about-layout {
    grid-template-columns: 1fr;
    gap: 2.5rem;
  }

  /* Photo before bio on mobile — a visual-only reorder (DOM/reading
     order is unchanged, so screen readers still hit the bio first). */
  .headshot-wrap {
    order: -1;
    max-width: 360px;
    margin: 0 auto;
  }
}

@media (max-width: 640px) {
  /* Hide the section jump-links (the sections are one scroll away
     anyway on a phone) but keep the Resume pill reachable — it
     shouldn't take scrolling all the way to the contact card to find
     it on mobile. */
  .nav-links > li:not(.nav-item--resume) { display: none; }
  .explorer-info { padding: 1.2rem 1.2rem 1.5rem; }

  .site-footer {
    flex-direction: column;
    gap: 0.75rem;
    text-align: center;
  }
}


/* ── 14. Reduced-Motion ────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  /* Pause all CSS keyframe animations */
  .hero::before,
  .region--coding::before,
  .region--creative::before {
    animation-play-state: paused;
  }

  .scroll-cue { animation: none; }
  .explorer-mobile-nav { animation: none; }

  /* setupSkillSwarm() already checks prefers-reduced-motion itself and
     skips creating the canvas entirely — this is just a defensive
     CSS-side backstop in case JS ever runs ahead of that check. */
  .skill-swarm { display: none; }

  /* Show reveal elements instantly */
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }

  /* Suppress hover transitions */
  .explorer-tab,
  .explorer-link,
  .nav-links a,
  .contact-button,
  .footer-links a,
  .skill-chip {
    transition: none;
  }
}


/* ── 15. Lightbox (Expanded Image Viewer) ──────────────────── */
/* Full-screen overlay shared by every gallery on the page.
   Hidden by default; script.js's Lightbox module toggles [hidden]. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(6, 8, 14, 0.92);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

/* Author rules for `display` always beat the browser's default
   `[hidden] { display: none }` rule, so it must be restated
   explicitly here or the overlay never actually disappears. */
.lightbox[hidden] {
  display: none;
}

/* Body scroll lock while the lightbox is open */
body.lightbox-open {
  overflow: hidden;
}

/* Column: image/video stage on top, caption (title + description)
   below — sized to fit the viewport with the stage shrinking first. */
.lightbox-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  width: min(88vw, 1100px);
  max-height: 88vh;
}

.lightbox-stage {
  position: relative;
  width: 100%;
  /* Explicit flex-basis rather than `auto`: img/video/placeholder are
     all `position: absolute` (see below) and so contribute nothing to
     content-based sizing — with `auto`, the stage would have no size
     to grow/shrink from and collapse to 0 height. This basis is also
     what the flex algorithm shrinks below its max-height for a tall
     caption, and what the absolutely-positioned children then fill
     exactly via `inset: 0`. */
  flex: 1 1 min(70vh, 640px);
  min-height: 0;
  max-height: min(70vh, 640px);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.lightbox-caption {
  width: 100%;
  flex-shrink: 0;
  max-height: 30vh;
  overflow-y: auto;
  padding: 0.9rem 1.1rem;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
}

.lightbox-title {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
}

.lightbox-desc {
  font-size: 0.85rem;
  line-height: 1.65;
  color: var(--text-sub);
  margin-top: 0.4rem;
}

/* Absolutely positioned to fill .lightbox-stage exactly (`inset: 0`)
   rather than sized with `max-height: 100%` — percentage heights
   don't reliably resolve against a flex item whose own height is
   content-driven/clamped rather than definite (see .lightbox-stage),
   which let oversized images render unconstrained and spill into the
   caption below. Absolute positioning sizes against the stage's
   final, already-resolved box instead, so this fits it exactly no
   matter how tall the caption forces the stage to shrink. */
.lightbox-stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

/* The global `img { display: block }` reset (see Reset & Base) beats
   the browser's default `[hidden] { display: none }`, so it must be
   restated here or the image never actually disappears behind the
   placeholder for projects without a real image at this slide. */
.lightbox-stage img[hidden] {
  display: none;
}

/* Same reasoning as .lightbox-stage img above. */
.lightbox-stage video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

/* Same [hidden]-vs-author-display issue as the other lightbox pieces. */
.lightbox-stage video[hidden] {
  display: none;
}

/* Same reasoning as .lightbox-stage img/video above: `position:
   absolute; inset: 0` fills the stage exactly, where a `height: 100%`
   here would have the same non-definite-parent resolution problem. */
.lightbox-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  border: 1px dashed var(--border-strong);
  border-radius: 4px;
}

/* Author rules for `display` always beat the browser's default
   `[hidden] { display: none }` rule, so it must be restated
   explicitly here or the placeholder never actually disappears
   behind a real image. */
.lightbox-placeholder[hidden] {
  display: none;
}

.lightbox-placeholder-icon {
  font-size: 3rem;
  opacity: 0.2;
}

.lightbox-placeholder-label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* z-index is required, not decorative: .lightbox-content is
   non-positioned, but the <img>/<video> inside it are `position:
   absolute` (see .lightbox-stage img above) and come later in the DOM
   than these controls, so without an explicit stack level the image
   paints (and captures clicks) on top of them wherever it geometrically
   overlaps -- e.g. a tall portrait image on a narrow phone screen can
   reach up under the close button or out past the stage into the
   arrows, silently swallowing taps meant for the controls. */
.lightbox-close,
.lightbox-arrow {
  all: unset;
  position: absolute;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  cursor: pointer;
  transition: background 0.18s, border-color 0.18s;
}

/* Same as .lightbox[hidden]: author `display` beats the browser's
   default `[hidden] { display: none }`, so single-image projects'
   prev/next arrows would otherwise stay visible. */
.lightbox-arrow[hidden] {
  display: none;
}

.lightbox-close:hover,
.lightbox-arrow:hover {
  background: rgba(255, 255, 255, 0.14);
  border-color: var(--text-sub);
}

.lightbox-close:focus-visible,
.lightbox-arrow:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
}

.lightbox-close {
  top: 1.25rem;
  right: 1.25rem;
  width: 40px;
  height: 40px;
  font-size: 1.4rem;
  line-height: 1;
}

/* Positioned relative to the full overlay (.lightbox is the nearest
   `position`d ancestor — see index.html), in the gutter beside
   .lightbox-content, so they sit outside the image instead of on top
   of it. `top` here is just a pre-JS fallback: script.js
   (Lightbox.positionControls) overwrites it with the actual vertical
   center of .lightbox-stage once the overlay is visible and
   measurable, since a caption of unknown height below the stage means
   that center can't be expressed as a static CSS percentage of the
   overlay. */
.lightbox-arrow {
  top: 50%;
  transform: translateY(-50%);
  width: 52px;
  height: 52px;
  font-size: 1.8rem;
  line-height: 1;
}

.lightbox-arrow--prev { left: clamp(0.75rem, 4vw, 3rem); }
.lightbox-arrow--next { right: clamp(0.75rem, 4vw, 3rem); }

/* Same top-position handoff to script.js as .lightbox-arrow above,
   pinned just above .lightbox-stage's bottom edge instead of its
   center. */
.lightbox-counter {
  position: absolute;
  z-index: 2;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--text-sub);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}

@media (max-width: 640px) {
  .lightbox-content { width: 92vw; }
  .lightbox-arrow { width: 42px; height: 42px; font-size: 1.5rem; }
  .lightbox-close { width: 36px; height: 36px; }
}
