:root {
  --bg: #fff;
  --sand: #141414; /* ink on white */
  --sand-dim: rgba(20, 20, 20, 0.48);
  --sand-soft: rgba(20, 20, 20, 0.7); /* lighter than full ink — less "bold"-reading titles */
  --sand-mute: rgba(20, 20, 20, 0.22);
  --line: rgba(20, 20, 20, 0.12);
  --accent: #d8c8af; /* warm sand for rare accents */
  --bg-rgb: 255, 255, 255;
  --safe: clamp(1.5rem, 3vw, 2.6rem);
  --header-h: clamp(5.5rem, 9vw, 7.5rem);
  --matte: clamp(0.5in, 5vw, 1in);
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --max: 1180px;
}

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

html {
  background: var(--bg);
  color: var(--sand);
  -webkit-font-smoothing: antialiased;
  scroll-behavior: smooth;
}

body {
  font-family: "Inter Tight", system-ui, sans-serif;
  background: var(--bg);
  color: var(--sand);
  min-height: 100dvh;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; text-align: left; }
:focus-visible { outline: 1px solid var(--sand); outline-offset: 3px; }
img { display: block; max-width: 100%; }
.cover { width: 100%; height: 100%; object-fit: cover; }

/* —— First-load progress bar —— */
/* z-index sits below the header (40) so "LARISSA UMULINGA" stays
   visible top-left the whole time, untouched — this overlay only ever
   covers the rest of the page, never the header. */
.loading-overlay {
  position: fixed;
  inset: 0;
  z-index: 38;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  transition: background-color 0.15s var(--ease);
}
.loading-overlay.is-leaving {
  background-color: transparent;
  pointer-events: none;
}
.loading-box { width: min(22rem, 78vw); text-align: center; }
.loading-track {
  position: relative;
  height: 2.75rem;
  border-bottom: 3px solid var(--sand);
  transition: opacity 0.15s var(--ease);
}
/* A little stick figure walks the track from 0 to 100% instead of a
   bar filling. left is set in px by setProgress() in site.js (not a
   CSS %, which — given the figure's real width — would run it off
   both ends of the track) so it starts flush at the road's beginning
   and ends flush at the road's end. Its transition duration/timing is
   also set from there (figure.style.transition), matching the step
   interval exactly so consecutive moves never pause between steps —
   not set here, to keep that one source of truth. transform-origin:
   bottom + a static forward tilt pivots the lean from the feet (where
   it's anchored) rather than the center, which is what actually reads
   as walking forward into the direction of travel instead of
   ambiguous/backward. */
.loading-figure {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 0.9rem;
  height: 2.2rem;
  transform-origin: bottom center;
  transform: rotate(6deg);
}
/* Torso stays static (no sway) — at this size, one more independently
   animating part on top of four limb joints read as jittery rather
   than connected motion. Just the lean above, plus the legs/arms. */
.fig-torso { position: absolute; inset: 0; }
.fig-head {
  position: absolute;
  top: 0;
  left: 50%;
  width: 0.5rem;
  height: 0.5rem;
  margin-left: -0.25rem;
  border-radius: 50%;
  background: var(--sand);
}
.fig-body {
  position: absolute;
  top: 0.55rem;
  left: 50%;
  width: 2px;
  height: 0.75rem;
  margin-left: -1px;
  background: var(--sand);
}
.fig-arm,
.fig-thigh {
  position: absolute;
  left: 50%;
  width: 2px;
  background: var(--sand);
  transform-origin: top center;
}
.fig-arm { top: 0.65rem; height: 0.55rem; }
.fig-thigh { top: 1.3rem; height: 0.5rem; }
/* Shin pivots at the knee — nested inside the thigh, so it inherits
   the thigh's own rotation (hip) plus its own independent bend. */
.fig-shin {
  position: absolute;
  top: 0.5rem;
  left: -1px;
  width: 2px;
  height: 0.5rem;
  background: var(--sand);
  transform-origin: top center;
}
/* Walking cadence — opposite-phase arm/leg pairs (contralateral, same
   as natural gait), unhurried, with a modest knee bend each stride to
   clear the ground. */
.fig-arm-l,
.fig-thigh-r { animation: fig-swing-a 0.7s ease-in-out infinite; }
.fig-arm-r,
.fig-thigh-l { animation: fig-swing-b 0.7s ease-in-out infinite; }
.fig-thigh-r .fig-shin { animation: fig-knee-a 0.7s ease-in-out infinite; }
.fig-thigh-l .fig-shin { animation: fig-knee-b 0.7s ease-in-out infinite; }
@keyframes fig-swing-a {
  0%, 100% { transform: rotate(20deg); }
  50% { transform: rotate(-20deg); }
}
@keyframes fig-swing-b {
  0%, 100% { transform: rotate(-20deg); }
  50% { transform: rotate(20deg); }
}
/* Knee stays ~straight through stance (early cycle, weight-bearing),
   bends gently to clear the ground mid-swing, straightens again as the
   foot plants ahead for the next stride — fewer, evenly-spaced stops
   and a smaller bend than earlier, which was reading as jittery rather
   than a clean step at this size. */
@keyframes fig-knee-a {
  0%, 100% { transform: rotate(0deg); }
  50% { transform: rotate(-22deg); }
}
@keyframes fig-knee-b {
  0%, 100% { transform: rotate(-22deg); }
  50% { transform: rotate(0deg); }
}
/* Finish line: every limb freezes to a neutral standing pose —
   triggered right as the figure reaches 100%, just before the screen
   clears. Stands still, no wave, no further motion. */
.loading-figure.is-still .fig-thigh,
.loading-figure.is-still .fig-shin,
.loading-figure.is-still .fig-arm-l,
.loading-figure.is-still .fig-arm-r {
  animation: none;
  transform: rotate(0deg);
}
.loading-pct {
  margin-top: 0.6rem;
  text-align: right;
  font-family: "Inter Tight", system-ui, sans-serif;
  font-weight: 600;
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  color: var(--sand);
  transition: opacity 0.15s var(--ease);
}
.loading-overlay.is-leaving .loading-track,
.loading-overlay.is-leaving .loading-pct {
  opacity: 0;
}

/* —— Header —— */
.site-header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--header-h);
  padding: 0 var(--safe);
  background: transparent;
}

.brand {
  font-family: "Poppins", sans-serif;
  font-weight: 300;
  font-size: clamp(2.2rem, 4.2vw, 4.4rem);
  line-height: 1.05;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  z-index: 2;
}
.brand:hover { opacity: 0.7; }

.menu-toggle {
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  z-index: 3;
}

/* The only nav on the site — one "menu" button, every page, every
   screen size, opening this same full-screen panel. */
.site-menu {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 35;
  background: rgba(255,255,255,0.98);
  padding: calc(var(--header-h) + 1.5rem) var(--safe) 2rem;
  flex-direction: column;
}
.site-menu.is-open { display: flex; }
.site-menu a {
  display: block;
  font-family: "Bebas Neue", sans-serif;
  font-size: clamp(2.4rem, 10vw, 4.2rem);
  letter-spacing: 0.04em;
  line-height: 1.05;
  padding: 0.2rem 0;
  color: var(--sand-mute);
  transition: color 0.25s var(--ease);
}
.site-menu a:hover,
.site-menu a.is-active { color: var(--sand); }

body.menu-open { overflow: hidden; }

/* —— Footer —— */
.site-footer {
  max-width: var(--max);
  margin: 0 auto;
  padding: 1.75rem var(--safe) 2.5rem;
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
}
.site-footer .left { color: var(--sand-dim); }
.site-footer .right { text-align: right; display: flex; gap: 1.25rem; justify-content: flex-end; }
.site-footer a:hover { opacity: 0.75; }

.page-wrap { padding-bottom: 3rem; }
.page-wrap.has-footer-space { min-height: 100dvh; display: flex; flex-direction: column; }
.page-wrap.has-footer-space > main { flex: 1; }

/* —— Inner page chrome —— */
.page-hero {
  padding: calc(var(--header-h) + 2.5rem) var(--safe) 2rem;
  max-width: var(--max);
  margin: 0 auto;
}
.page-kicker {
  font-size: 0.65rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sand-dim);
  margin-bottom: 0.85rem;
}
.page-title {
  font-family: "Bebas Neue", sans-serif;
  font-size: clamp(3rem, 8vw, 5.5rem);
  letter-spacing: 0.04em;
  line-height: 0.92;
}
.page-lede {
  margin-top: 1rem;
  max-width: 34rem;
  font-family: "Instrument Serif", Georgia, serif;
  font-style: italic;
  font-size: 1.2rem;
  color: var(--sand-dim);
  line-height: 1.45;
}


/* —— Filter bar (landing page, Photo / All / Concert / Editorial) —— */
.filter-bar {
  max-width: var(--max);
  margin: 0 auto;
  padding: calc(var(--header-h) + 2rem) var(--safe) 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.6rem;
}
.filter-label {
  font-family: "Instrument Serif", Georgia, serif;
  font-size: 1.05rem;
  color: var(--sand-dim);
}
.filter-label em { font-style: italic; }
.filter-tabs {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 1.75rem;
}
.filter-tab {
  font-family: "Bebas Neue", sans-serif;
  font-size: 0.95rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sand-dim);
  background: none;
  border: none;
  padding: 0.15rem 0;
  cursor: pointer;
  transition: color 0.25s var(--ease);
}
.filter-tab:hover { color: var(--sand); }
.filter-tab.is-active { color: var(--sand); }

/* —— Scatter grid (landing page) —— */
.scatter-grid {
  max-width: var(--max);
  margin: 0 auto;
  padding: 1.5rem var(--safe) 4rem;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2.5rem 1.5rem;
}
.scatter-cell {
  position: relative;
  display: block;
  aspect-ratio: 3 / 4;
  /* don't let the long-press-to-preview gesture trigger iOS's copy/
     open-in-new-tab callout or a text-selection highlight */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.scatter-photo {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #f2f2f2;
  transition: transform 0.6s var(--ease);
}
.scatter-cell:hover .scatter-photo,
.scatter-cell:focus-visible .scatter-photo { transform: scale(1.03); }
/* Two stacked layers, not one — crossfading opacity on a single <img>
   would flash the background between photos. "base" is always fully
   opaque; "top" loads the next photo and fades in directly over it, a
   true blend between the two photos with nothing showing through. */
.scatter-photo img.layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.scatter-photo img.layer.top {
  opacity: 0;
  transition: opacity 0.6s var(--ease);
}

/* One shared set of corner brackets (appended to <body>, not per photo)
   that glides between whichever photo is hovered — see positionFrame()
   in site.js. Absolute (document-relative), not fixed, so they scroll
   along with the page instead of staying put in the viewport while the
   photo they're framing scrolls away underneath them. */
.scatter-corner {
  position: absolute;
  top: 0;
  left: 0;
  width: 1.5rem;
  height: 1.5rem;
  border: 3px solid #000;
  pointer-events: none;
  opacity: 0;
  z-index: 50;
  transition: transform 0.55s cubic-bezier(0.16, 0.84, 0.3, 1), opacity 0.2s var(--ease);
}
.scatter-corner.is-visible { opacity: 1; }
.scatter-corner.tl { border-right: 0; border-bottom: 0; }
.scatter-corner.tr { border-left: 0; border-bottom: 0; }
.scatter-corner.bl { border-right: 0; border-top: 0; }
.scatter-corner.br { border-left: 0; border-top: 0; }

.scatter-label {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.5rem;
}
.scatter-label .count {
  font-family: "Instrument Serif", Georgia, serif;
  font-style: italic;
  font-size: 0.8rem;
  color: var(--sand-dim);
  min-height: 1.2em;
}
.scatter-label .label-text {
  font-family: "Bebas Neue", sans-serif;
  font-size: 2.1rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  min-height: 1.1em;
  transition: opacity 0.2s var(--ease);
}

/* —— Artist project page (full-bleed filmstrip carousel) —— */
.carousel {
  position: relative;
  height: 100dvh;
  overflow: hidden;
}
.carousel-frame {
  position: absolute;
  /* as little reserved space as the header + (now smaller) thumbnail
     strip need — object-fit: contain means the photo's rendered size
     is driven by this box's height, so trimming the margins here is
     what actually makes the photo itself appear bigger */
  inset: calc(var(--header-h) + 0.75rem) 0 4.5rem;
  background: var(--bg);
  overflow: hidden;
}
/* A horizontally scrolling filmstrip — several photos visible at once,
   peeking past the viewport edges, snapping smoothly from one to the
   next rather than hard-cutting between single images. */
.carousel-track {
  display: flex;
  align-items: center;
  height: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  cursor: ns-resize;
}
.carousel-track::-webkit-scrollbar { display: none; }

/* Only the current photo is ever visible — each slide is exactly one
   frame wide, so sliding to the next/previous one swaps the whole view
   rather than revealing neighboring photos at the sides. */
.carousel-slide {
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: center;
  scroll-snap-stop: always;
}
.carousel-slide img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
}

/* The filmstrip's last "slide" — closes out a project by introducing
   the next one, instead of just stopping dead at the last photo. */
.carousel-slide.next-up {
  justify-content: center;
  gap: clamp(1.5rem, 4vw, 3rem);
  padding: 0 var(--safe);
  text-decoration: none;
  color: inherit;
  max-width: var(--max);
  margin: 0 auto;
}
.next-up-photo {
  flex: none;
  width: clamp(9rem, 22vw, 16rem);
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: #f2f2f2;
}
.next-up-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease);
}
.next-up:hover .next-up-photo img { transform: scale(1.04); }
.next-up-copy { max-width: 26rem; }
.next-up-label {
  font-size: 0.65rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sand-dim);
  margin-bottom: 0.6rem;
}
.next-up-credit {
  font-family: "Instrument Serif", Georgia, serif;
  font-style: italic;
  font-size: 1.1rem;
  color: var(--sand-dim);
}
.next-up-name {
  margin-top: 0.3rem;
  font-family: "Bebas Neue", sans-serif;
  font-size: clamp(2.2rem, 6vw, 4rem);
  letter-spacing: 0.03em;
  line-height: 0.95;
  text-transform: uppercase;
}
.next-up-cta {
  margin-top: 1.25rem;
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--sand-dim);
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  transition: color 0.2s var(--ease);
}
.next-up:hover .next-up-cta { color: var(--sand); }
.next-up-cta span {
  display: inline-block;
  width: 1.5rem;
  height: 1px;
  background: currentColor;
}

/* A compact, centered row below the photo — narrower than the full
   width so it doesn't compete with the photo above it. */
.carousel-strip {
  position: absolute;
  left: 50%;
  bottom: 1rem;
  transform: translateX(-50%);
  z-index: 3;
  display: flex;
  justify-content: center;
  gap: 0.4rem;
  max-width: min(32rem, calc(100% - 2 * var(--safe)));
  overflow-x: auto;
  padding: 0.25rem;
}
.carousel-strip button {
  flex: none;
  width: 3rem;
  height: 3rem;
  padding: 0;
  overflow: hidden;
  border: 2px solid transparent;
  opacity: 0.6;
  cursor: pointer;
  transition: opacity 0.2s var(--ease), border-color 0.2s var(--ease);
}
.carousel-strip button img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.carousel-strip button:hover { opacity: 0.9; }
.carousel-strip button.is-active {
  opacity: 1;
  border-color: var(--sand);
}

.artist-nav {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--safe) 2rem;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sand-dim);
  border-top: 1px solid var(--line);
  padding-top: 1.5rem;
}
.artist-nav a:hover { color: var(--sand); }

/* —— Publications —— */
.pub-list {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0.5rem var(--safe) 3rem;
  display: flex;
  flex-direction: column;
  gap: 0;
}
.pub-item {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: 2.5rem;
  align-items: center;
  padding: 2.5rem 0;
  border-top: 1px solid var(--line);
}
.pub-item:last-child { border-bottom: 1px solid var(--line); }
.pub-label {
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--sand-mute);
  margin-bottom: 0.75rem;
}
.pub-title {
  font-family: "Bebas Neue", sans-serif;
  font-size: clamp(2rem, 4vw, 3rem);
  letter-spacing: 0.04em;
  line-height: 1;
  margin-bottom: 0.85rem;
}
.pub-body {
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--sand-dim);
  max-width: 32rem;
}
.pub-body p + p { margin-top: 0.85rem; }
.pub-visual {
  position: relative;
  overflow: hidden;
  background: #f2f2f2;
  aspect-ratio: 4 / 5;
}
.pub-visual img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.pub-link {
  display: inline-block;
  margin-top: 1.25rem;
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
}
.pub-link:hover { border-color: var(--sand); }


/* —— Curations / publications (simple list) —— */
.cv-section {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--safe) 3rem;
}
.cv-heading {
  font-size: 0.65rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sand-dim);
  margin-bottom: 1.25rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--line);
}
.cv-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0;
}
.cv-row {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: 1rem 1.5rem;
  padding: 1.1rem 0;
  border-bottom: 1px solid var(--line);
  align-items: baseline;
}
.cv-year {
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  color: var(--sand-mute);
  font-variant-numeric: tabular-nums;
}
.cv-title a { border-bottom: 1px solid transparent; }
.cv-title a:hover { border-bottom-color: currentColor; }
.cv-title {
  font-family: "Bebas Neue", sans-serif;
  font-size: clamp(1.45rem, 3vw, 1.9rem);
  letter-spacing: 0.04em;
  line-height: 1.05;
  margin-bottom: 0.25rem;
}
.cv-meta {
  font-size: 0.88rem;
  line-height: 1.45;
  color: var(--sand-dim);
}
@media (max-width: 640px) {
  .cv-row {
    grid-template-columns: 3.25rem minmax(0, 1fr);
    gap: 0.75rem 1rem;
  }
}

/* —— About —— */
.about-block {
  max-width: var(--max);
  margin: 0 auto;
  padding: calc(var(--header-h) + 3rem) var(--safe) 3rem;
}
.about-title {
  font-family: "Bebas Neue", sans-serif;
  font-size: clamp(1.8rem, 5vw, 4.2rem);
  letter-spacing: 0.02em;
  line-height: 1.05;
  margin-bottom: 2.5rem;
}
.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: 3rem;
  align-items: stretch;
  /* floor under the row height so the photo gets more room than the
     text alone would give it, cutting down on the crop */
  min-height: clamp(30rem, 48vw, 44rem);
}
.about-photo {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}
/* The image is taken out of flow (absolute) so it can never influence
   this row's height — only .about-body's text does. The frame then
   just fills whatever space is left after the caption, and the image
   crops to fill the frame exactly. */
.about-photo-frame {
  position: relative;
  flex: 1;
  min-height: 0;
}
.about-photo-frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 22%;
  background: #f2f2f2;
}
.about-aside {
  font-size: 0.65rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--sand-dim);
  padding-top: 0.75rem;
}
.about-body {
  font-size: 1.05rem;
  line-height: 1.7;
  color: var(--sand-dim);
  max-width: 36rem;
}
.about-body p + p { margin-top: 1.1rem; }
.about-body strong { color: var(--sand); font-weight: 500; }
.about-contact {
  margin-top: 2.25rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 1.75rem;
  align-items: center;
}
.inquire {
  display: inline-block;
  border: 1px solid var(--sand-dim);
  padding: 0.65rem 1.15rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-size: 0.68rem;
  transition: border-color 0.25s, background 0.25s, color 0.25s;
}
.inquire:hover {
  border-color: var(--sand);
  background: var(--sand);
  color: #fff;
}
.about-contact .ghost {
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sand-dim);
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
}
.about-contact .ghost:hover { color: var(--sand); border-color: var(--sand); }


/* —— Quiet sitemap (projects.html fallback) —— */
.sitemap-list {
  max-width: var(--max);
  margin: 0 auto 2rem;
  padding: 0 var(--safe);
  list-style: none;
  counter-reset: work;
}
.sitemap-list li {
  counter-increment: work;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.95rem;
  letter-spacing: 0.02em;
  color: var(--sand-dim);
}
.sitemap-list li::before {
  content: counter(work, decimal-leading-zero);
  display: inline-block;
  width: 2.2rem;
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  color: var(--sand-mute);
  margin-right: 0.85rem;
  font-variant-numeric: tabular-nums;
}
.sitemap-list a {
  font-family: "Bebas Neue", sans-serif;
  font-size: 1.55rem;
  letter-spacing: 0.04em;
  margin-right: 0.5rem;
}
.sitemap-list a:hover { color: var(--sand); }

/* —— Responsive —— */
@media (max-width: 960px) {
  /* Its own snap point, not just padding to clear the header — without
     this, mandatory snap below has nowhere valid to rest at the top
     and forces the scroll straight to the first photo instead. */
  .filter-bar { padding: calc(var(--header-h) + 1.5rem) var(--safe) 0; scroll-snap-align: start; }
  .filter-tabs { gap: 1.1rem; }

  /* One photo per screen on mobile — snap-scrolling instead of a grid
     of small tiles, so a swipe always lands on exactly the next photo
     filling the screen, never half-showing two at once.

     This scrolls its own dedicated container (.page-wrap), not the
     document itself. Snapping the root scroller (body, or body+html)
     was tried first and both have real bugs on iOS: html+body together
     flips the whole page 180deg during the rubber-band bounce, and
     body alone still let a fast swipe rest in between two photos. A
     plain overflow:auto element is the well-tested path instead — it's
     the same pattern already used for the project carousel's
     horizontal swipe (.carousel-track) with no such issues.
     body.photo-page is only set on index.html, so no other page is
     restructured by this. */
  body.photo-page { height: 100dvh; overflow: hidden; }
  body.photo-page .page-wrap {
    height: 100%;
    padding-bottom: 0;
    overflow-y: auto;
    /* mandatory — proximity was tried to keep the filter bar reachable
       (mandatory with no snap point at the top was auto-scrolling past
       it on load), but proximity doesn't guarantee a snap at all, so a
       swipe could rest halfway between two photos again. The actual
       fix for both bugs at once: give the filter bar its own snap
       point too (below), so mandatory always has somewhere valid to
       rest at the top instead of being forced to evict to the first
       photo — and every swipe between photos still lands exactly on
       one, never in between. */
    scroll-snap-type: y mandatory;
    -webkit-overflow-scrolling: touch;
  }
  .scatter-grid {
    grid-template-columns: 1fr;
    gap: 0;
    /* No top padding here — the filter bar directly above it already
       clears the fixed header with its own top padding; adding header-h
       again would just leave a blank gap before the first photo. */
    padding: 0;
  }
  .scatter-cell {
    scroll-snap-align: start;
    /* Never let a fast flick skip past a photo without snapping to it
       — without this, some mobile browsers let a hard swipe sail past
       a snap point, landing half between two photos. */
    scroll-snap-stop: always;
    aspect-ratio: auto;
    /* 100vh first as a fallback for browsers that don't support dvh
       (treated as invalid and ignored, so vh stands); 100dvh then
       overrides it where the dynamic viewport unit is supported, to
       account for a mobile browser's address bar showing/hiding. */
    height: 100vh;
    height: 100dvh;
  }
  /* The lean-in zoom on :hover is a desktop mouse affordance, but iOS
     approximates :hover on a touch-and-hold too — so a mobile long-
     press was zooming the photo in (and visibly softening it) on top
     of the slideshow rotation. The rotation itself only ever swaps
     which photo is showing, never scales it; this just stops the zoom
     from riding along with it on mobile. */
  .scatter-cell:hover .scatter-photo,
  .scatter-cell:focus-visible .scatter-photo { transform: none; }
  /* The label cell only ever fills in on hover/long-press, neither of
     which a swipe-through mobile feed has — rather than a separate
     blank slide, each photo gets its own project title overlaid
     directly in the bottom-left corner instead. */
  .scatter-label { display: none; }
  .scatter-cell::before {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 38%;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.78), rgba(0, 0, 0, 0));
    pointer-events: none;
    /* .scatter-photo is a later DOM child with the same auto z-index,
       so without this the gradient (an earlier ::before) would paint
       underneath the opaque photo and never actually show. */
    z-index: 1;
  }
  .scatter-cell::after {
    content: attr(data-name);
    position: absolute;
    left: 1.25rem;
    right: 1.25rem;
    bottom: 1.25rem;
    z-index: 1;
    font-family: "Bebas Neue", sans-serif;
    font-size: 1.9rem;
    letter-spacing: 0.03em;
    color: #fff;
  }

  .carousel-slide.next-up { flex-direction: column; text-align: center; gap: 1.25rem; }
  .next-up-photo { width: clamp(8rem, 40vw, 11rem); }

  .pub-item { grid-template-columns: 1fr; gap: 1.25rem; }
  .pub-visual { max-width: 22rem; }
  .about-grid { grid-template-columns: 1fr; gap: 1.5rem; min-height: 0; }
  .about-photo { height: auto; }
  .about-photo-frame { flex: none; aspect-ratio: 3 / 4; }

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

