/* ==========================================================================
   CARTER HUBER — PORTFOLIO THEME
   All colors, fonts, and spacing are defined once, here, as CSS variables.
   Change a value in :root and it updates everywhere on the site.
   See README-EDITING.md for a plain-language walkthrough.
   ========================================================================== */

:root {
  /* ---- Color: dark, warm-neutral base with two accent tracks ---- */
  --bg: #0b0b0d;                 /* page background */
  --bg-elevated: #141417;        /* cards, panels */
  --bg-elevated-2: #1c1c20;      /* hover / nested surfaces */
  --line: rgba(244, 241, 233, 0.10);
  --line-strong: rgba(244, 241, 233, 0.22);

  --text: #f4f1e9;               /* warm off-white */
  --text-dim: #a6a39a;           /* secondary text */
  --text-faint: #6c6a64;         /* tertiary / meta text */

  --accent-pro: #c9a15a;         /* brass/gold — Professional track */
  --accent-pro-dim: #8a743f;
  --accent-personal: #5fc9c2;    /* cool teal — Personal track */
  --accent-personal-dim: #3f8a85;

  --scrim: rgba(8, 8, 9, 0.72);

  /* ---- Type ---- */
  --font-display: "Fraunces", Georgia, serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;

  --size-xs: 0.72rem;
  --size-sm: 0.85rem;
  --size-base: 1rem;
  --size-md: 1.15rem;
  --size-lg: 1.6rem;
  --size-xl: 2.4rem;
  --size-2xl: 3.6rem;
  --size-3xl: 5.4rem;

  --tracking-wide: 0.14em;
  --tracking-wider: 0.22em;

  /* ---- Spacing scale ---- */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-sm: 1rem;
  --space-md: 1.5rem;
  --space-lg: 2.5rem;
  --space-xl: 4rem;
  --space-2xl: 6rem;
  --space-3xl: 9rem;

  /* ---- Layout ---- */
  --container-max: 1400px;
  --nav-height: 64px;
  --radius-sm: 4px;
  --radius-md: 10px;
  --radius-lg: 20px;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
/* reserve scrollbar space permanently so opening/closing the drawer
   (which locks body scroll) never shifts the layout width and forces
   a reflow of the page underneath — a real source of felt "lag" */
html { scrollbar-gutter: stable; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--size-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
ul { list-style: none; margin: 0; padding: 0; }
h1, h2, h3, h4, p { margin: 0; }

::selection { background: var(--accent-personal); color: #0b0b0d; }

/* Visible focus for keyboard users only (:focus-visible skips mouse
   clicks) — the default outline is nearly invisible on this dark theme. */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent-personal);
  outline-offset: 2px;
  border-radius: 4px;
}
/* the dialog container itself receives programmatic focus — no ring */
.drawer:focus { outline: none; }

/* ---- Background texture ----
   Two fixed, feint layers sit behind everything: a couple of very soft
   color blobs that drift slowly (the "movement"), plus a faint static
   grain (the "texture"). Both are pure CSS on pseudo-elements — cheap,
   GPU-composited, and never touched by JS — so they can't affect the
   performance work done elsewhere on the site. Opacity is intentionally
   low so photos stay the clear focus. */
body::before {
  content: "";
  position: fixed;
  inset: -12%;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(38% 30% at 18% 22%, rgba(95, 201, 194, 0.10), transparent 60%),
    radial-gradient(34% 28% at 82% 68%, rgba(201, 161, 90, 0.07), transparent 60%),
    radial-gradient(30% 34% at 60% 12%, rgba(95, 201, 194, 0.06), transparent 60%);
  animation: bgDrift 60s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes bgDrift {
  0%   { transform: translate(0, 0) rotate(0deg); }
  50%  { transform: translate(2.5%, -2%) rotate(1.5deg); }
  100% { transform: translate(-2%, 2.5%) rotate(-1.5deg); }
}
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
@media (prefers-reduced-motion: reduce) {
  body::before { animation: none; }
}

/* ---- Spotlights (ambient glow + hidden "flash" easter egg) ----
   Rendered by js/components/Spotlights.jsx. Pure background light: sits
   BEHIND all content (z-index -1, above only the body's own gradient
   blobs at -2), so photos and text are never tinted — the glow shows in
   the dark space around and between them. */
.spotlight-field {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}
.spotlight-drift {
  position: absolute;
  width: 0;
  height: 0;
}
.spotlight-glow {
  position: absolute;
  border-radius: 50%;
  filter: blur(18px);
  mix-blend-mode: screen;
  opacity: 0.16;
  transition: opacity 1.4s ease;
  will-change: transform;
}
.spotlight-field.is-lightshow .spotlight-glow {
  opacity: 0.6;
}
.spotlight-field.is-lightshow .spotlight-drift {
  animation-duration: 6s !important;
}

@keyframes spotDriftA {
  0%   { transform: translate(0, 0) scale(1); }
  50%  { transform: translate(6vw, 4vh) scale(1.15); }
  100% { transform: translate(-4vw, 7vh) scale(0.95); }
}
@keyframes spotDriftB {
  0%   { transform: translate(0, 0) scale(1); }
  50%  { transform: translate(-7vw, -3vh) scale(0.9); }
  100% { transform: translate(3vw, -6vh) scale(1.1); }
}
@keyframes spotDriftC {
  0%   { transform: translate(0, 0) scale(1); }
  50%  { transform: translate(5vw, -5vh) scale(1.1); }
  100% { transform: translate(-6vw, -2vh) scale(0.92); }
}
@keyframes spotDriftD {
  0%   { transform: translate(0, 0) scale(1); }
  50%  { transform: translate(-5vw, 5vh) scale(0.95); }
  100% { transform: translate(6vw, 3vh) scale(1.12); }
}

/* Camera-flash flicker — plays once, at the exact moment the easter egg
   fires, as a little payoff before the light show starts. */
.camera-flash {
  position: fixed;
  inset: 0;
  /* renders as a sibling of the spotlight field (not inside it), so it can
     sit above all content even though the glows live behind everything */
  z-index: 600;
  background: #fff;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease-out;
}
.camera-flash.is-active {
  opacity: 0.85;
  transition: opacity 0.03s linear;
}

@media (prefers-reduced-motion: reduce) {
  .spotlight-drift { animation: none !important; }
  .spotlight-glow { transition: none; }
}

/* ---- Scrollbar (subtle, matches dark theme) ---- */
::-webkit-scrollbar { width: 15px; height: 15px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--bg-elevated-2); border-radius: 10px; border: 3px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: #2a2a30; }

/* ---- Utility ---- */
.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--space-lg);
}
@media (max-width: 720px) {
  .container { padding: 0 var(--space-md); }
}

/* Full-bleed variant: same side padding as .container, but no max-width
   cap — used for the gallery so it fills the viewport edge-to-edge on
   wide screens instead of being boxed to --container-max. */
.container.full-bleed {
  max-width: none;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
}

.glass {
  background: rgba(14, 14, 16, 0.55);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: none;
}


.tag {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  padding: 0.3em 0.7em;
  border-radius: 100px;
  border: 1px solid var(--line-strong);
  color: var(--text-dim);
}
.tag.pro { color: var(--accent-pro); border-color: var(--accent-pro-dim); }
.tag.personal { color: var(--accent-personal); border-color: var(--accent-personal-dim); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  padding: 0.7em 1.1em;
  border-radius: 100px;
  border: 1px solid var(--line-strong);
  color: var(--text);
  transition: border-color 0.2s var(--ease-out), background 0.2s var(--ease-out);
}
.btn:hover { border-color: var(--text); background: var(--bg-elevated); }
.btn.active { background: var(--text); color: var(--bg); border-color: var(--text); }

/* ==========================================================================
   NAV
   ========================================================================== */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 200;
  height: var(--nav-height);
  display: flex;
  align-items: center;
  border-bottom: 1px solid var(--line);
  background: rgba(11, 11, 13, 0.72);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.nav-inner {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.nav-mark {
  font-family: var(--font-display);
  font-size: var(--size-md);
  letter-spacing: 0.02em;
}
.nav-mark span { color: var(--text-dim); font-weight: 300; }
.nav-links {
  display: flex;
  gap: var(--space-lg);
}
.nav-links a {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-dim);
  position: relative;
  padding: 0.4em 0;
  transition: color 0.2s var(--ease-out);
}
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0; right: 100%;
  bottom: -2px;
  height: 1px;
  background: var(--text);
  transition: right 0.25s var(--ease-out);
}
.nav-links a:hover { color: var(--text); }
.nav-links a:hover::after { right: 0; }
.nav-links a.is-active { color: var(--text); }
.nav-links a.is-active::after { right: 0; background: var(--accent-personal); }
@media (max-width: 640px) {
  .nav-links { gap: var(--space-md); }
  .nav-links a { font-size: 0.62rem; }
}

/* ==========================================================================
   HERO / CURVED SPLIT MARQUEE
   ========================================================================== */
.hero {
  position: relative;
  height: 66vh;
  min-height: 620px;
  max-height: 920px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* clips at the section boundary — rows themselves clip only horizontally
     (see .marquee-row) so the curve can swing freely without getting cut
     off before it reaches the hero's own edge */
  overflow: hidden;
  /* The vignette must fade to TRANSPARENT, not to var(--bg): an opaque
     hero background occludes body::before's fixed ambient color blobs,
     while below the hero they show through — that luminance step was the
     faint straight "line" visible across the page right under the hero. */
  background:
    radial-gradient(120% 90% at 50% 50%, rgba(20,20,23,0.2), transparent 70%);
}

.hero-rows {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(22px, 4vw, 56px);
  padding: clamp(8px, 2vh, 24px) 0;
}

.marquee-row {
  position: relative;
  width: 100%;
  height: clamp(230px, 26vw, 400px);
  /* fully visible on purpose — the hero itself clips at its edges.
     (overflow-x: hidden + overflow-y: visible silently computes to
     overflow-y: auto, which made each row its own tiny scroll container:
     the mouse wheel dragged the marquee a few px before the page moved.) */
  overflow: visible;
}

/* Pin the top row in its slightly-raised position (the look preferred when
   the old accidental row-scroll nudged it up). Transform only — no layout
   impact on the arc math or the track animation. */
.hero-rows .marquee-row:first-child {
  transform: translateY(-22px);
}

.marquee-track {
  position: absolute;
  top: 0; left: 0;
  display: flex;
  align-items: center;
  height: 100%;
  width: max-content;
  will-change: transform;
}

.marquee-track.dir-left { animation: scrollLeft linear infinite; }
.marquee-track.dir-right { animation: scrollRight linear infinite; }

@keyframes scrollLeft {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
@keyframes scrollRight {
  from { transform: translateX(-50%); }
  to   { transform: translateX(0); }
}

.marquee-tile {
  flex: 0 0 auto;
  width: clamp(160px, 19vw, 340px);
  height: clamp(116px, 13.5vw, 246px);
  margin: 0 clamp(7px, 1.1vw, 16px);
  padding: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-elevated);
  border: 1px solid var(--line);
  transform-origin: 50% 50%;
  transition: border-color 0.2s var(--ease-out);
  will-change: transform;
  cursor: pointer;
}
.marquee-tile:hover { border-color: var(--line-strong); }
.marquee-tile img {
  width: 100%; height: 100%;
  object-fit: cover;
}

/* Center glass identity panel. The box itself only holds the text — the
   glass/blur glow is two separate stacked layers (below) sized
   independently via negative inset, so the blur's visible footprint isn't
   tied to (or hidden by) the text's own padding box. */
.hero-id {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  padding: clamp(20px, 3vw, 40px) clamp(24px, 4vw, 56px);
  text-align: center;
}

/* The glow behind the name is built from two stacked, independently sized
   layers instead of one flat blurred rectangle:
   - ::before is the wide halo — softly blurred, sized to spill past the
     marquee rows themselves (matches the size Carter sketched out).
   - ::after is a tight, much more strongly blurred core concentrated
     right behind the text, so the blur genuinely reads as stronger in
     the middle and falls off toward the edge.
   Both masks are `black 0%, transparent 100%` with `closest-side` sizing,
   which means the fade starts at the very center point and reaches zero
   exactly at each layer's own edge — there is no solid plateau anywhere,
   so no ring or hard line can appear at any zoom/screen size. */
.hero-id::before,
.hero-id::after {
  content: "";
  position: absolute;
  z-index: -1;
  pointer-events: none;
}
.hero-id::before {
  /* Kept small enough to stay inside the hero on short viewports, and the
     mask now reaches fully transparent at 78% of the radius — so even if
     the hero's overflow clip does catch the layer's outer edge, the part
     being cut is already invisible. (The old 100% fade meant the clip
     landed mid-fade, leaving a faint straight seam under the hero on the
     right, where the warm background glow made it visible.) */
  top: clamp(-160px, -18vh, -110px);
  bottom: clamp(-160px, -18vh, -110px);
  left: clamp(-160px, -14vw, -90px);
  right: clamp(-160px, -14vw, -90px);
  /* faint teal wash (same family as the drifting spotlights) over the
     dark glass */
  background:
    radial-gradient(ellipse at 50% 50%, rgba(110, 214, 200, 0.09), rgba(110, 214, 200, 0) 70%),
    rgba(10, 10, 12, 0.32);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  /* solid inner zone so the blur holds full strength around the text,
     with eased intermediate stops (approximating a smooth curve rather
     than a straight linear fade) so no ring is visible where the inner
     core layer hands off to this halo — still fully transparent by 78%
     so the hero's clip edge can never show a seam */
  -webkit-mask-image: radial-gradient(ellipse closest-side at 50% 50%, black 0%, black 35%, rgba(0,0,0,0.55) 55%, rgba(0,0,0,0.2) 68%, transparent 78%);
  mask-image: radial-gradient(ellipse closest-side at 50% 50%, black 0%, black 35%, rgba(0,0,0,0.55) 55%, rgba(0,0,0,0.2) 68%, transparent 78%);
}
.hero-id::after {
  top: clamp(-130px, -15vh, -50px);
  bottom: clamp(-130px, -15vh, -50px);
  left: clamp(-110px, -9vw, -40px);
  right: clamp(-110px, -9vw, -40px);
  /* subtle greenish-teal hue concentrated behind the name, fading out
     before the core's own rim */
  background:
    radial-gradient(ellipse at 50% 50%, rgba(110, 214, 200, 0.22), rgba(110, 214, 200, 0) 72%),
    rgba(10, 10, 12, 0.58);
  backdrop-filter: blur(48px);
  -webkit-backdrop-filter: blur(48px);
  /* near-opaque hold behind the text itself, then a long eased falloff —
     the extra stops kill the visible ring the old two-stop fade produced */
  -webkit-mask-image: radial-gradient(ellipse closest-side at 50% 50%, black 0%, black 40%, rgba(0,0,0,0.72) 60%, rgba(0,0,0,0.38) 78%, rgba(0,0,0,0.14) 90%, transparent 100%);
  mask-image: radial-gradient(ellipse closest-side at 50% 50%, black 0%, black 40%, rgba(0,0,0,0.72) 60%, rgba(0,0,0,0.38) 78%, rgba(0,0,0,0.14) 90%, transparent 100%);
}
/* Hero eyebrow matches the tagline color (not the fainter global eyebrow). */
.hero-id .eyebrow {
  color: var(--text-dim);
}
.hero-id-name {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.2rem, 6.4vw, 4.4rem);
  line-height: 0.98;
  letter-spacing: 0.01em;
}
.hero-id-name em {
  display: block;
  font-style: normal;
  font-weight: 300;
  color: var(--text-dim);
}
.hero-id-tagline {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-dim);
  max-width: 30ch;
}
.hero-scroll-cue {
  position: absolute;
  bottom: clamp(18px, 4vh, 36px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--text-faint);
  transition: opacity 0.4s var(--ease-out), transform 0.4s var(--ease-out);
  animation: floatY 2.6s ease-in-out infinite;
}
@keyframes floatY {
  0%, 100% { transform: translate(-50%, 0); }
  50% { transform: translate(-50%, 6px); }
}

/* Respect reduced-motion: freeze the auto-scrolling marquee and the
   bobbing scroll cue (background drift + spotlights already stop). */
@media (prefers-reduced-motion: reduce) {
  .marquee-track { animation: none; }
  .hero-scroll-cue { animation: none; }
}

/* ==========================================================================
   PAGE HEADERS / INTRO
   ========================================================================== */
.page-head {
  /* tightened so the whole About spread fits one screen without scrolling */
  padding: calc(var(--nav-height) + var(--space-lg)) 0 var(--space-lg);
}
.page-head-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 5vw, var(--size-3xl));
  line-height: 1.02;
  max-width: 16ch;
}
.page-head-desc {
  margin-top: var(--space-md);
  max-width: 56ch;
  color: var(--text-dim);
  font-size: var(--size-md);
  font-weight: 300;
}

/* Compact — unlike .page-head, deliberately doesn't push content down, so
   the gallery grid appears right under the hero with no dead gap (the
   goal is a real photo visible in the ~bottom third of the initial view). */
.gallery-lead {
  padding: var(--space-md) 0 var(--space-sm);
}
.gallery-lead .eyebrow {
  font-size: var(--size-sm);
  letter-spacing: var(--tracking-wide);
}

/* Project quick-nav — a thumbnail + title chip per project, right under
   the gallery eyebrow, so a visitor can jump straight to any project
   instead of scrolling through the whole page. Deliberately minimal:
   just the thumbnail and the title, nothing else. */
.project-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm) var(--space-md);
  padding: var(--space-sm) 0 var(--space-lg);
  border-bottom: 1px solid var(--line);
}
.project-nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-3xs) var(--space-sm) var(--space-3xs) var(--space-3xs);
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg-elevated);
  transition: border-color 0.2s var(--ease-out), transform 0.2s var(--ease-out);
}
.project-nav-item:hover {
  border-color: var(--line-strong);
  transform: translateY(-2px);
}
.project-nav-thumb {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  overflow: hidden;
  flex: 0 0 auto;
  background: var(--bg-elevated-2);
}
.project-nav-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.project-nav-title {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

/* ==========================================================================
   BACK TO TOP — fixed pill, bottom-left, fades in after scrolling past hero
   ========================================================================== */
.back-to-top {
  position: fixed;
  left: var(--space-md);
  bottom: var(--space-md);
  z-index: 300;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-sm);
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg-elevated);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 0.25s var(--ease-out), transform 0.25s var(--ease-out),
    border-color 0.2s var(--ease-out);
}
.back-to-top.is-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.back-to-top:hover {
  border-color: var(--line-strong);
  transform: translateY(-2px);
}
.back-to-top-arrow {
  font-size: 1em;
  line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
  .back-to-top { transition: opacity 0.25s linear; }
  .back-to-top:hover { transform: none; }
}

/* ==========================================================================
   FILTER TOGGLE
   ========================================================================== */
.filter-row {
  display: flex;
  gap: var(--space-2xs);
  padding-bottom: var(--space-lg);
  flex-wrap: wrap;
}

/* ==========================================================================
   GRID / MASONRY
   ========================================================================== */
.project-grid {
  columns: 3 320px;
  column-gap: var(--space-md);
  padding-bottom: var(--space-3xl);
}
@media (max-width: 900px) { .project-grid { columns: 2 260px; } }
@media (max-width: 560px) { .project-grid { columns: 1; } }

.project-grid.even {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-md);
}

/* Irregular "bento" gallery grid — every card is still one project (its own
   grouped mosaic of images), just at varying footprints for visual rhythm
   instead of a uniform grid. Base unit is one column/row of the 4x1
   template below; size-* classes span multiple units. */
.project-grid.bento {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: minmax(220px, 26vw);
  grid-auto-flow: dense;
  gap: var(--space-md);
  padding-bottom: var(--space-3xl);
}
.card.size-md { grid-column: span 1; grid-row: span 1; }
.card.size-lg { grid-column: span 2; grid-row: span 2; }
.card.size-wide { grid-column: span 2; grid-row: span 1; }
.card.size-tall { grid-column: span 1; grid-row: span 2; }

@media (max-width: 900px) {
  .project-grid.bento { grid-template-columns: repeat(2, 1fr); grid-auto-rows: minmax(200px, 44vw); }
  .card.size-lg, .card.size-wide { grid-column: span 2; }
  .card.size-tall { grid-column: span 1; }
}

/* On very wide/large monitors the gallery now spans the full viewport
   (see .container.full-bleed) — add columns so cards stay a sensible
   size instead of stretching huge, and cap row height in px so rows
   don't grow unbounded with viewport width. */
@media (min-width: 1700px) {
  .project-grid.bento { grid-template-columns: repeat(6, 1fr); grid-auto-rows: minmax(220px, 420px); }
}
@media (min-width: 2200px) {
  .project-grid.bento { grid-template-columns: repeat(8, 1fr); }
}
@media (max-width: 560px) {
  .project-grid.bento { grid-template-columns: 1fr; grid-auto-rows: minmax(240px, 78vw); }
  .card.size-md, .card.size-lg, .card.size-wide, .card.size-tall {
    grid-column: span 1; grid-row: span 1;
  }
}

/* ==========================================================================
   PROJECT SECTION — one full-width "spread" per project, used on Home in
   place of the old preview-card grid. Sections just flow one into the next
   down the page (no card chrome around a section itself) so scrolling from
   one project's photos into the next project's title feels continuous. The
   title always sits centered above that project's grid. */
.project-section {
  padding: var(--space-xl) 0;
}
/* :first-of-type, not :first-child — the sections sit after .gallery-lead
   and the project nav inside the container, so :first-child never matched
   and the first gallery kept its full top padding. */
.project-section:first-of-type { padding-top: var(--space-sm); }

.project-section-head {
  max-width: 62ch;
  margin: 0 auto var(--space-lg);
  text-align: center;
}
.project-section-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 4.2vw, 3.1rem);
  line-height: 1.05;
  margin: var(--space-2xs) 0 var(--space-sm);
}
.project-section-blurb {
  color: var(--text-dim);
  font-weight: 300;
  font-size: var(--size-md);
  margin: 0 auto var(--space-sm);
  max-width: 60ch;
}
.project-section-head .card-tags { justify-content: center; }

/* Photo grid — a deterministic, EXPLICITLY-positioned CSS Grid. Every tile
   and band gets its exact grid-column/grid-row computed in JS
   (ProjectSection.jsx) from a fixed set of shapes (single "md" tiles, 2-col
   "wide" tiles, 2x2 "lg" tiles, and full-width 2/3/4-photo bands) that are
   assembled row-group by row-group so each group exactly tiles all 4
   columns before the next one starts. This guarantees — by construction,
   not by browser auto-placement/packing — that there are never any empty
   cells and the gap is identical everywhere. Every tile fills its cell
   exactly via object-fit: cover (a deliberate crop trade-off for a
   perfectly regular, gapless grid). */
:root { --tile-row: clamp(190px, 16vw, 280px); }
.project-grid.masonry {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: var(--tile-row);
  gap: var(--space-md);
}

.grid-photo {
  display: block;
  overflow: hidden;
  border-radius: var(--radius-md);
  cursor: pointer;
  background: var(--bg-elevated);
  width: 100%;
  height: 100%;
}
.grid-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Crop anchored toward the upper third rather than dead center: when a
     tall photo has to be cropped into a shorter cell, this keeps heads and
     faces in frame (a centered crop was cutting faces off on portrait
     shots). Horizontal crops are unaffected (x stays centered), and photos
     that only need a slight vertical trim shift almost imperceptibly. */
  object-position: 50% 35%;
  display: block;
  transition: transform 0.5s var(--ease-out);
}
.grid-photo:hover img { transform: scale(1.03); }

/* Full-width band — 2, 3, or 4 photos sharing the full row width, split
   evenly and cropped to a shared height so the row reads as one clean
   strip. Its own grid-row span (on the outer grid) comes from the same
   row-group placement as everything else. */
.photo-band {
  grid-column: 1 / -1;
  display: grid;
  gap: var(--space-md);
  height: 100%;
}
.photo-band.cols-2 { grid-template-columns: repeat(2, 1fr); }
.photo-band.cols-3 { grid-template-columns: repeat(3, 1fr); }
.photo-band.cols-4 { grid-template-columns: repeat(4, 1fr); }

/* Below 1100px the explicit shape system gets cramped (2x2 "lg" tiles and
   3/4-way bands don't have room to breathe), so it steps down to a simple,
   even auto-flow grid instead — every inline grid-column/grid-row from the
   desktop layout is overridden back to auto here. */
@media (max-width: 1100px) {
  .project-grid.masonry { grid-template-columns: repeat(2, 1fr); grid-auto-rows: auto; }
  .project-grid.masonry > .grid-photo,
  .project-grid.masonry > .photo-band {
    grid-column: auto !important;
    grid-row: auto !important;
  }
  .project-grid.masonry > .grid-photo { height: clamp(200px, 34vw, 320px); }
  .photo-band { height: auto; }
  .photo-band.cols-3, .photo-band.cols-4 { grid-template-columns: repeat(2, 1fr); }
  .photo-band .grid-photo { height: clamp(220px, 38vw, 400px); }
}
@media (max-width: 620px) {
  .project-grid.masonry { grid-template-columns: 1fr; }
  .project-grid.masonry > .grid-photo { height: clamp(220px, 68vw, 380px); }
  .photo-band.cols-2, .photo-band.cols-3, .photo-band.cols-4 { grid-template-columns: 1fr; }
  .photo-band .grid-photo { height: clamp(220px, 62vw, 380px); }
}

.card {
  display: flex;
  flex-direction: column;
  height: 100%;
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--bg-elevated);
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.25s var(--ease-out), transform 0.25s var(--ease-out);
  /* skip layout/paint work for cards off-screen — a big win on long grids */
  content-visibility: auto;
  contain-intrinsic-size: 0 380px;
}
.card:hover { border-color: var(--line-strong); transform: translateY(-3px); }
.card-media {
  position: relative;
  width: 100%;
  background: var(--bg-elevated-2);
  overflow: hidden;
}
.card-media img {
  width: 100%; height: auto;
  display: block;
  transition: transform 0.5s var(--ease-out);
}
.card:hover .card-media img { transform: scale(1.045); }
.card-media.ratio-pro { aspect-ratio: 4 / 3; }
.card-media.ratio-pro img { height: 100%; object-fit: cover; }
.card-body {
  padding: var(--space-sm) var(--space-md) var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}
.card-meta {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}
.card-title {
  font-family: var(--font-display);
  font-size: var(--size-lg);
  line-height: 1.15;
}
.card-year {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  color: var(--text-faint);
}
.card-blurb {
  color: var(--text-dim);
  font-size: var(--size-sm);
}
.card-tags {
  display: flex;
  gap: var(--space-3xs);
  flex-wrap: wrap;
  margin-top: var(--space-3xs);
}

.card-media.placeholder-media {
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(155deg, rgba(201,161,90,0.14), rgba(20,20,23,0.6));
}
.card-media.placeholder-media svg { width: 40%; opacity: 0.55; }

/* Mosaic media — several images per card instead of a single cover.
   flex: 1 so it fills whatever height the bento grid gives its card
   (rather than a fixed aspect-ratio), with card-body's content sized to
   its own content below it. */
.card-mosaic {
  display: grid;
  gap: 2px;
  flex: 1 1 auto;
  min-height: 0;
  background: var(--bg);
}
.card-mosaic img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.5s var(--ease-out); }
.card:hover .card-mosaic img { transform: scale(1.06); }
.card-mosaic.n1 { grid-template-columns: 1fr; }
.card-mosaic.n2 { grid-template-columns: 1fr 1fr; }
.card-mosaic.n3 { grid-template-columns: 1.3fr 1fr; grid-template-rows: 1fr 1fr; }
.card-mosaic.n3 > *:first-child { grid-row: 1 / 3; }
.card-mosaic.n4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }

/* ==========================================================================
   PROJECT DRAWER / MODAL
   ========================================================================== */
.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: 500;
  /* solid-ish dim instead of a blurred backdrop — backdrop-filter is one
     of the most expensive things a browser can composite, and doing it
     over a whole viewport right at click time is what "delay" usually is */
  background: rgba(6, 6, 7, 0.86);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  animation: fadeIn 0.14s ease-out;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

/* Near-fullscreen, three-column lightbox:
   [ info sidebar | large image | vertical thumbnail rail ]
   The drawer has a fixed footprint (most of the viewport, per sketch) and
   never scrolls as a whole — the sidebar and the rail each scroll
   independently if their content overflows, and the image pane always
   keeps a stable size (no layout shifting between portrait/landscape
   photos). */
.drawer {
  position: relative;
  width: min(94vw, 2000px);
  height: min(92vh, 1300px);
  display: grid;
  grid-template-columns: clamp(240px, 21vw, 340px) minmax(0, 1fr) clamp(240px, 21vw, 340px);
  grid-template-areas: "side main rail";
  overflow: hidden;
  background: var(--bg-elevated);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  animation: drawerIn 0.16s ease-out;
}
@keyframes drawerIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.drawer-close {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  z-index: 10;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: rgba(11,11,13,0.65);
  border: 1px solid var(--line-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  transition: border-color 0.2s var(--ease-out), background 0.2s var(--ease-out);
}
.drawer-close:hover { border-color: var(--text); background: rgba(11,11,13,0.9); }

/* ---- Left: info sidebar ---- */
.drawer-side {
  grid-area: side;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-lg);
  border-right: 1px solid var(--line);
  overflow-y: auto;
  min-height: 0;
}
.drawer-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.7rem, 2.4vw, 2.6rem);
  line-height: 1.08;
}
.drawer-blurb { color: var(--text-dim); font-size: var(--size-base); font-weight: 300; }
.drawer-tags { display: flex; gap: var(--space-3xs); flex-wrap: wrap; }
.drawer-side-foot {
  margin-top: auto;
  padding-top: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  border-top: 1px solid var(--line);
}
.drawer-side-counter {
  font-family: var(--font-mono);
  font-size: var(--size-md);
  color: var(--text);
}
.drawer-side-counter em { font-style: normal; color: var(--text-faint); }
.drawer-side-hint {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  color: var(--text-faint);
}

/* ---- Middle: the image ---- */
.drawer-main {
  grid-area: main;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* matches the info sidebar, so the letterbox bars around photos that
     don't fill the pane read as one continuous surface instead of black
     borders */
  background: var(--bg-elevated);
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}
.drawer-main img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  animation: drawerImgIn 0.22s var(--ease-out);
}
@keyframes drawerImgIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
.drawer-main .placeholder-slide {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(155deg, rgba(201,161,90,0.16), rgba(20,20,23,0.6));
}
.drawer-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 44px; height: 44px;
  border-radius: 50%;
  background: rgba(11,11,13,0.6);
  border: 1px solid var(--line-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  color: var(--text);
  transition: background 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.drawer-nav:hover { background: rgba(11,11,13,0.85); border-color: var(--text); }
.drawer-nav.prev { left: var(--space-md); }
.drawer-nav.next { right: var(--space-md); }

/* ---- Right: vertical thumbnail rail ---- */
.drawer-rail {
  grid-area: rail;
  /* same width as the info sidebar, so thumbnails sit two-up */
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2xs);
  align-content: start;
  overflow-y: auto;
  min-height: 0;
  /* top padding clears the floating close button */
  padding: calc(var(--space-sm) + 44px) var(--space-sm) var(--space-sm);
  border-left: 1px solid var(--line);
  background: var(--bg-elevated);
  scrollbar-width: thin;
}
.drawer-thumb {
  flex: 0 0 auto;
  width: 100%;
  /* 4:3 via the padding trick, NOT aspect-ratio: grid row sizing ignores
     aspect-ratio-derived heights here, which made rows shorter than the
     thumbs and stacked them overlapping each other. Padding-bottom
     resolves against the column width, so the row height is always
     exactly right. */
  height: 0;
  padding: 0 0 75%;
  position: relative;
  border-radius: 6px;
  overflow: hidden;
  border: 2px solid transparent;
  opacity: 0.45;
  transition: opacity 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.drawer-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.drawer-thumb:hover { opacity: 0.85; }
.drawer-thumb.active { opacity: 1; border-color: var(--accent-personal); }

/* ---- Narrow screens: stack image on top, thumbs as a horizontal strip,
   info below — the three-column layout has no room to breathe here. ---- */
@media (max-width: 900px) {
  .drawer {
    width: 96vw;
    height: 94vh;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto auto;
    grid-template-areas: "main" "rail" "side";
  }
  .drawer-side {
    border-right: none;
    border-top: 1px solid var(--line);
    padding: var(--space-md);
    max-height: 32vh;
  }
  .drawer-rail {
    display: flex;
    flex-direction: row;
    overflow-y: hidden;
    overflow-x: auto;
    padding: var(--space-xs);
    border-left: none;
    border-top: 1px solid var(--line);
  }
  .drawer-thumb { width: 84px; height: 63px; padding: 0; }
}

/* ==========================================================================
   ABOUT
   ========================================================================== */
.about-grid {
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  gap: var(--space-2xl);
  padding-bottom: var(--space-3xl);
  align-items: start;
}
@media (max-width: 860px) { .about-grid { grid-template-columns: 1fr; gap: var(--space-lg); } }
.about-copy { color: var(--text-dim); font-size: var(--size-md); font-weight: 300; display: flex; flex-direction: column; gap: var(--space-md); max-width: 62ch; }
.about-copy strong { color: var(--text); font-weight: 500; }
/* Right column: portrait photo with a compact facts list beneath it —
   no big bordered box, just tight hairline rows. */
.about-side {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}
.about-portrait {
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-elevated);
}
.about-portrait img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  /* crop anchored toward the bottom of the frame so the camera in hand
     stays fully in view (the default center crop clipped it) */
  object-position: 50% 88%;
  display: block;
}
.about-facts {
  display: flex;
  flex-direction: column;
  margin: 0;
}
.about-fact {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-sm);
  padding: var(--space-2xs) 0;
  border-bottom: 1px solid var(--line);
}
.about-fact:first-child { border-top: 1px solid var(--line); }
.about-fact dt {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
}
.about-fact dd {
  margin: 0;
  font-size: var(--size-sm);
  text-align: right;
}
.about-fact dd a { color: var(--text); }
.about-fact dd a:hover { color: var(--accent-personal); }

/* Candid collage under the bio text — a tall shot on the left spanning
   two rows, two 4:3 shots stacked on the right. Fills the empty space
   beneath the paragraphs in the left column. */
.about-collage {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  margin-top: var(--space-2xs);
}
.about-collage img {
  width: 100%;
  height: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-md);
  display: block;
  background: var(--bg-elevated);
}
.about-collage img.tall {
  grid-row: 1 / 3;
  aspect-ratio: auto;
}

/* Gear: one unified spec strip with internal dividers, instead of four
   floating outlined boxes. */
.gear-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  margin: 0 0 var(--space-3xl);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-elevated);
}
.gear-item {
  padding: var(--space-sm) var(--space-md);
  border-left: 1px solid var(--line);
}
.gear-item:first-child { border-left: none; }
.gear-item dt {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent-personal);
}
.gear-item dd {
  margin: 0.35em 0 0;
  color: var(--text);
  font-size: var(--size-sm);
}
@media (max-width: 860px) {
  .gear-list { grid-template-columns: 1fr; }
  .gear-item { border-left: none; border-top: 1px solid var(--line); }
  .gear-item:first-child { border-top: none; }
}

/* ==========================================================================
   FOOTER
   ========================================================================== */
.footer {
  border-top: 1px solid var(--line);
  padding: var(--space-xl) 0;
}
.footer-inner {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--space-lg);
}
.footer-mark {
  font-family: var(--font-display);
  font-size: var(--size-lg);
}
.footer-links {
  display: flex;
  gap: var(--space-md);
  flex-wrap: wrap;
}
.footer-links a {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-dim);
}
.footer-links a:hover { color: var(--text); }
.footer-meta {
  font-family: var(--font-mono);
  font-size: var(--size-xs);
  color: var(--text-faint);
}

/* ==========================================================================
   PAGE FADE (route transitions)
   ========================================================================== */
.page-enter {
  animation: pageIn 0.16s ease-out;
}
@keyframes pageIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
