/* ============================================================
   magnus archive — stylesheet
   off-white, geist, mono coordinates, no chrome.
   ============================================================ */

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

/* No text selection or caret anywhere — same site-wide rule as the main
   portfolio (owner: "no text karat ANYWHERE on the site, as there is no
   typing"). Highlight boxes and text carets never appear over photos,
   captions, or chrome. */
html, body {
  -webkit-user-select: none;
  user-select: none;
  caret-color: transparent;
}
img {
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
/* …EXCEPT the submission form's fields — the archive is the one place
   on the site with real typing, and a field with no caret and no
   selection is unusable. */
input, textarea {
  -webkit-user-select: text;
  user-select: text;
  caret-color: auto;
}

:root {
  /* Page-tint: the dynamic full-page background color. Defaults to warm paper.
     Driven by JS RAF lerp from the active photo's dominant color. */
  --page-tint: #f4f1ea;
  --page-tint-soft: #faf8f3;  /* slightly lighter for stacked surfaces */

  --bg:    var(--page-tint);     /* alias — body uses --page-tint */
  --bg-2:  var(--page-tint-soft);
  --paper: #ffffff;

  /* Ink colors swap via body.bg-dark when bg luminance < 0.45 (WCAG).
     Mobile audit bumped --mute alpha 0.42 → 0.55 because mute-colored
     text (.chrome__meta, .label, .tile__cap, .spectrum__readout) was
     falling under the 4.5:1 WCAG AA threshold on mid-tone tints
     (browns / olives near the dark-flip boundary). 0.55 holds AA on
     all photo tints in the dataset. */
  --ink:    rgba(20, 20, 20, 0.92);
  --ink-2:  rgba(20, 20, 20, 0.62);
  --mute:   rgba(20, 20, 20, 0.55);
  --mute-2: rgba(20, 20, 20, 0.32);
  --hair:   rgba(0, 0, 0, 0.14);
  --hair-soft: rgba(0, 0, 0, 0.08);
  /* Hatch token for the spectrum rail's empty / no-photo regions —
     was hardcoded to a paper color (rgba(246,245,240,0.92)) which
     looked correct on the paper bg but inverted the visual intent on
     dark photo tints (empty stripes appeared brighter than filled,
     opposite of expectation). Tokenizing lets us swap the hatch
     color in body.bg-dark — see the dark-mode override below. */
  --hatch:  rgba(20, 20, 20, 0.06);

  --font: "Geist", -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, "Liberation Mono", "Courier New", monospace;

  --pad-x: clamp(14px, 2.4vw, 28px);
  --pad-y: clamp(10px, 1.4vw, 16px);
  --max: 1680px;

  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --quick: 120ms;
}

/* Dark-mode ink colors — applied when page-tint luminance < 0.45 (with hysteresis).
   Text transitions slightly faster than bg so legibility is restored as the tint
   arrives, not after. (Spotify trick.) */
body.bg-dark {
  --ink:    rgba(252, 248, 240, 0.95);
  --ink-2:  rgba(252, 248, 240, 0.70);
  --mute:   rgba(252, 248, 240, 0.62);
  --mute-2: rgba(252, 248, 240, 0.35);
  --hair:   rgba(255, 255, 255, 0.20);
  --hair-soft: rgba(255, 255, 255, 0.10);
  --hatch:  rgba(255, 255, 255, 0.10);
}

/* Honour user's contrast preference — bail out of the tint system on `more`. */
@media (prefers-contrast: more) {
  :root { --page-tint: #f6f5f0 !important; }
  body.bg-dark { --page-tint: #141414 !important; }
}

html, body {
  background: var(--page-tint);
  color: var(--ink);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: -0.01em;
  font-feature-settings: "ss01", "cv11";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
/* Mobile bump — 14px reads as 9-10pt on retina iPhones which is below
   WCAG AAA readability for editorial body. 15px keeps the editorial
   feel without compromising mobile legibility. Mono captions get a
   floor of 11.5px in the relevant mobile media query (see below).
   Also bump explicit-size editorial text in .prose and selected
   chrome/wordmark elements that were on 14px hard values. */
@media (max-width: 600px) {
  html, body { font-size: 15px; }
  .prose p { font-size: 15px; line-height: 1.6; }
  .prose ul li { font-size: 12px; padding: 4px 0; }
  .wordmark { font-size: 15px; }
  /* Mono labels and meta — floor at 11.5px so they don't shrink
     below readable on iPhones. */
  .label, .chrome__meta, .home__counter, .home__cap,
  .tile__id, .spectrum__hint, .spectrum__focus-cap,
  .viewer__coord, .archive__reset {
    font-size: 11.5px;
  }
}

/* Text + chrome transitions — limited to color & border-color so layout never
   animates. Text arrives 420ms; bg from JS lerp takes longer — text leads. */
.wordmark, .nav a, .label, .mono, .chrome__meta,
.foot, .section__title, .section__meta,
.tile__name, .tile__id,
.viewer__name, .viewer__coord, .link,
.home__cap, .home__counter,
.spectrum__readout, .spectrum__hint, .spectrum__head .section__title,
.spectrum__focus-cap {
  transition: color 420ms var(--ease);
}
.chrome, .foot, .section, .ph-head, .list li, .home__photo,
.spectrum__head, .spectrum__stage {
  transition: border-color 420ms var(--ease);
}

body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  overflow-x: hidden;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; letter-spacing: inherit; }
img { display: block; max-width: 100%; }

/* Crosshair cursor EVERYWHERE — the custom crosshair is the ONLY cursor on
   the archive, exactly like the main portfolio. The `*` rule forces every
   element to inherit it, overriding the archive's own pointer / grab /
   resize cursors so no other cursor ever shows. `html` (and `body.bg-dark`
   for the white-on-dark variant) carry `!important` so they win over the
   `*` inherit rule for their own elements. Gated to fine pointers so touch
   devices don't fetch a cursor image. */
@media (pointer: fine) {
  html {
    cursor:
      url("/cursors/updated-small.png") 7 7,
      url("/cursors/updated.cur") 8 8,
      crosshair !important;
  }
  body.bg-dark {
    cursor:
      url("/cursors/updated-small-white.png") 7 7,
      crosshair !important;
  }
  *,
  *::before,
  *::after {
    cursor: inherit !important;
  }
}
b, strong { font-weight: 500; }

::selection { background: var(--ink); color: var(--bg); }

.mono {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0;
  font-feature-settings: normal;
}

/* ---------- chrome ---------- */
.chrome {
  position: sticky;
  top: 0;
  z-index: 50;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: clamp(20px, 4vw, 56px);
  padding: var(--pad-y) var(--pad-x);
  /* iOS safe-area-inset-top on notched iPhones — the sticky chrome
     would otherwise sit under the status bar / dynamic island. We
     add max() so the larger of (env safe area) or (the normal
     pad-y) wins; both clamps still apply at desktop sizes where
     safe-area-inset-top is 0. */
  padding-top: max(var(--pad-y), env(safe-area-inset-top));
  background: color-mix(in srgb, var(--page-tint) 88%, transparent);
  border-bottom: 1px solid var(--hair);
}

.wordmark {
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  letter-spacing: -0.015em;
}

.nav {
  display: flex;
  gap: clamp(10px, 1.6vw, 20px);
  flex-wrap: wrap;
}
.nav a {
  font-size: 13.5px;
  /* Bumped from --ink-2 (62% alpha) to --ink-1 equivalent (75%) for
     idle nav links — at 0.62 alpha on mid-tone tints, idle links
     fell below 4.5:1 WCAG AA. Active link stays at --ink. */
  color: rgba(20, 20, 20, 0.78);
  padding: 2px 0;
  position: relative;
}
body.bg-dark .nav a { color: rgba(252, 248, 240, 0.82); }
.nav a:hover { color: var(--ink); }
.nav a.is-active { color: var(--ink); }
.nav a.is-active::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -3px;
  height: 1px;
  background: var(--ink);
}

.chrome__meta {
  color: var(--mute);
  white-space: nowrap;
}

/* ---------- main ---------- */
#main {
  flex: 1;
  width: 100%;
  outline: none;
}
.view {
  width: 100%;
  padding: clamp(20px, 3vw, 36px) var(--pad-x) clamp(48px, 6vw, 96px);
  max-width: var(--max);
  margin: 0 auto;
}

.loader {
  display: grid;
  place-items: center;
  min-height: 50vh;
  color: var(--mute);
}

/* tiny labels — used for section labels in mono */
.label {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
  text-transform: lowercase;
}

/* a section divider — section header for each view */
.section {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: clamp(14px, 2vw, 22px) 0;
  border-bottom: 1px solid var(--hair);
  margin-bottom: clamp(20px, 3vw, 36px);
  flex-wrap: wrap;
}

/* Archive filter row — mono select pills */
.archive__filters {
  display: flex;
  align-items: center;
  gap: clamp(8px, 1vw, 14px);
  flex-wrap: wrap;
}
.archive__sel {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid var(--hair);
  padding: 4px 8px;
  border-radius: 100px;
  letter-spacing: 0;
  cursor: pointer;
  transition: border-color var(--quick) var(--ease), color var(--quick) var(--ease);
  /* Reset native appearance just enough to look custom but stay usable */
  appearance: none;
  -webkit-appearance: none;
  background-image: none;
  padding-right: 22px;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 12px) calc(50% - 1px), calc(100% - 8px) calc(50% - 1px);
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
}
.archive__sel:hover { border-color: var(--ink); color: var(--ink); }
.archive__sel:focus { outline: none; border-color: var(--ink); color: var(--ink); }
.archive__reset {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
  letter-spacing: 0;
  padding: 4px 6px;
  border-bottom: 1px dashed var(--hair);
}
.archive__reset:hover { color: var(--ink); border-color: var(--ink); }
.archive__count { font-family: var(--mono); font-size: 13px; }

@media (max-width: 720px) {
  .archive__filters { width: 100%; }
}

/* Color swatch row — clickable circles, one per category. Active state gets
   an ink ring around the circle. */
.archive__swatches {
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.6vw, 18px);
  padding: 4px 0 clamp(18px, 3vw, 32px);
  margin-bottom: clamp(8px, 1.5vw, 14px);
  flex-wrap: wrap;
}
.ar-sw {
  /* Outer button is a 44×44 invisible hit area on mobile (Apple HIG /
     WCAG min tap target); the visible dot stays its original 18-22px
     visual size, centered inside via padding. Desktop keeps the
     compact 22px button since pointer precision is finer there. */
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: transparent;
  padding: 0;
  position: relative;
  cursor: pointer;
  transition: transform 160ms var(--ease);
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.ar-sw__dot {
  display: block;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--sw, transparent);
  margin: 2px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.10);
  transition: box-shadow 200ms var(--ease);
}
.ar-sw:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
@media (max-width: 600px) {
  .ar-sw {
    width: 44px;
    height: 44px;
  }
  .ar-sw__dot {
    width: 24px;
    height: 24px;
    margin: 0;
  }
  .archive__swatches { gap: 8px; }
}
body.bg-dark .ar-sw__dot {
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}
.ar-sw--all .ar-sw__dot {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--hair);
}
.ar-sw:hover { transform: translateY(-1px); }
.ar-sw:hover .ar-sw__dot { box-shadow: inset 0 0 0 1.5px var(--ink); }
.ar-sw.is-active {
  /* Active gets an outer ring set just outside the dot */
  box-shadow: 0 0 0 1.5px var(--ink);
}
.ar-sw.is-active .ar-sw__dot { box-shadow: inset 0 0 0 1px var(--page-tint); }
.section__title {
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
}
.section__meta {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
}
.section__meta a { border-bottom: 1px solid var(--mute-2); }
.section__meta a:hover { color: var(--ink); border-color: var(--ink); }

/* ============================================================
   HOME — single-photo hero, full-page tint
   Two-layer crossfade stack, signature coordinate strip, counter.
   Chrome is sticky on top, so the home grid lives BELOW it with explicit
   row layout (stage + caption) and intrinsic spacing.
   ============================================================ */
.home {
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  padding: clamp(28px, 4vw, 56px) var(--pad-x) clamp(28px, 3vw, 56px);
  max-width: var(--max);
  margin: 0 auto;
  width: 100%;
  /* fill the area below the sticky chrome */
  min-height: calc(100vh - 56px);
}

/* Photo stage — holds two stacked img layers for crossfade */
.home__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  cursor: pointer;
}
.home__layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  will-change: opacity;
}
.home__layer img {
  max-height: min(72vh, 820px);
  max-width: min(68vw, 1040px);
  width: auto;
  height: auto;
  object-fit: contain;
  box-shadow: 0 28px 80px -32px rgba(0, 0, 0, 0.18);
  background: transparent;
}
body.bg-dark .home__layer img {
  box-shadow: 0 28px 80px -32px rgba(0, 0, 0, 0.55);
}

/* Coordinate strip — the signature quirk. Sits below the photo. */
.home__caprow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: clamp(16px, 3vw, 36px);
  padding-top: clamp(16px, 2vw, 28px);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-2);
}

/* "view all →" floats in the bottom-right corner, parked away from the caprow.
   Subtle, only-when-noticed link. */
.home__viewall {
  position: fixed;
  bottom: 16px;
  right: 18px;
  z-index: 30;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-2);
  letter-spacing: 0;
  padding: 4px 2px;
  border-bottom: 1px solid transparent;
  transition: color 200ms var(--ease), border-color 200ms var(--ease), transform 160ms var(--ease);
}
.home__viewall:hover {
  color: var(--ink);
  border-color: var(--ink);
  transform: translateX(2px);
}
.home__viewall:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}
/* Touch-affordance fallback — on devices without hover, the link
   would otherwise sit as silent mono text with no underline. Give
   it a permanent border-bottom + a larger tap target. */
@media (hover: none) {
  .home__viewall {
    border-bottom-color: var(--mute);
    padding: 8px 4px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
}

@media (max-width: 600px) {
  /* ── HOME view mobile cleanup ──────────────────────────────────────
     Goals: ONE non-scrolling screen, bigger photo, no text overlap.

     1. Pin the home view to the dynamic viewport and clip overflow so
        it can never scroll. `100dvh` (not 100vh) tracks the actual
        visible area as the mobile address bar shows/hides, so the
        bottom caption never gets stranded behind the URL bar. Scoped
        to `body[data-view="home"]` so the scrolling views (archive
        grid, photographers) are untouched. */
  body[data-view="home"] {
    height: 100dvh;
    min-height: 100dvh;
    overflow: hidden;
  }
  /* 2. Make #main a flex column on home so .home fills exactly the
        space left below the sticky chrome (min-height:0 lets it shrink
        to fit rather than forcing the page taller than the viewport —
        the old `min-height: calc(100vh - 56px)` overflowed because the
        mobile chrome wraps to ~2 rows, taller than the assumed 56px). */
  body[data-view="home"] #main {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  .home {
    flex: 1;
    min-height: 0;
    padding: 10px var(--pad-x) 14px;
  }
  /* 3. Bigger photo — was capped at 68vw (too small, big empty
        margins). Now 90vw wide and free to use the full vertical stage
        area (max-height:100% of the 1fr grid row). object-fit:contain
        keeps it whole; it ends up width-bound on most phones, notably
        larger than before. */
  .home__layer img {
    max-width: 90vw;
    max-height: 100%;
  }
  /* 4. Caprow stays a single clean metadata line. */
  .home__caprow {
    padding-top: 10px;
    font-size: 11px;
  }
  /* 5. Drop the floating "view all" link on mobile — it was overlapping
        the caption (the overlap the user flagged) and is redundant with
        the "Archive" tab in the nav, which goes to the same place. */
  .home__viewall { display: none; }
}
.home__cap {
  letter-spacing: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* min-width: 0 lets this flex child shrink below its content width so
     the ellipsis actually kicks in (flex defaults to min-width:auto =
     content size, which would push the counter off-screen instead of
     truncating the caption on narrow phones). */
  min-width: 0;
}
.home__cap .dot { color: var(--mute); padding: 0 4px; }
.home__cap .hex { color: var(--ink); }
.home__cap .name { color: var(--ink); }
.home__counter {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
  white-space: nowrap;
  flex-shrink: 0;
}
.home__counter strong { color: var(--ink); font-weight: 400; }

/* Click-to-advance regions */
.home__zone {
  position: absolute;
  top: 0; bottom: 0;
  width: 40%;
  cursor: w-resize;
  z-index: 1;
}
.home__zone--next { right: 0; cursor: e-resize; }
.home__zone--prev { left: 0; }

/* Tint toggle — only visible on views that actually have a tint to toggle:
   home (single photo + solid bg), spectrum (continuous scrub), and when the
   viewer lightbox is open. Hidden on archive grid, photographers list, about,
   etc. — where the page is plain paper anyway. */
.tint-toggle {
  position: fixed;
  top: 14px;
  right: 14px;
  z-index: 60;
  font-family: var(--mono);
  font-size: 12px;
  /* Bumped from --ink-2 to --ink for solid label color — the dot
     also flips to solid below (was using currentColor at 0.62, which
     blended into the 78%-opaque button bg on mid-tone tints). */
  color: var(--ink);
  /* Background opacity bumped 78%→92% so the toggle's rim and dot
     remain distinct against the dynamic tint — at 78% the toggle was
     "fading into" the page color mid-transition. */
  background: color-mix(in srgb, var(--page-tint) 92%, transparent);
  padding: 7px 14px;
  border-radius: 100px;
  border: 1px solid var(--hair);
  display: none;  /* hidden by default — see selectors below */
  align-items: center;
  gap: 8px;
  cursor: pointer;
  letter-spacing: 0.02em;
  /* Added background to the transition list so it lerps in sync with
     the body bg + ink color when the tint changes. Previously the bg
     of the toggle (a color-mix of --page-tint) snapped while the
     surrounding bg lerped — produced a visible "pop" mid-fade. */
  transition: color 200ms var(--ease), border-color 200ms var(--ease),
              background-color 420ms var(--ease), transform 120ms var(--ease);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}
/* Show on tinted views + when viewer is open */
body[data-view="home"] .tint-toggle,
body[data-view="spectrum"] .tint-toggle,
body.viewer-open .tint-toggle { display: flex; }
.tint-toggle:hover { color: var(--ink); border-color: var(--ink); transform: translateY(-1px); }
.tint-toggle:active { transform: translateY(0); }
.tint-toggle__dot {
  width: 10px;
  height: 10px;
  /* Always solid ink — the previous `currentColor` at 0.62 alpha was
     blending into the toggle's translucent bg mid-tint. Forcing
     --ink (0.92/0.95 alpha) keeps the dot a clean indicator. */
  background: var(--ink);
  border-radius: 50%;
  flex-shrink: 0;
}
.tint-toggle[aria-pressed="false"] .tint-toggle__dot { background: transparent; border: 1.5px solid var(--ink); }
/* Focus-visible ring for keyboard users (iPad with BT keyboard,
   desktop tab nav). Doesn't show on touch tap (which uses :focus
   not :focus-visible per UA heuristics). */
.tint-toggle:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

@media (max-width: 600px) {
  /* Mobile: collapse the pill into a compact CIRCLE with just the dot —
     matching the portfolio site's circular tint toggle. The "color"
     label is hidden; the dot alone (filled = color on, outline = off)
     carries the meaning. 44×44 keeps it a valid tap target. */
  .tint-toggle {
    top: max(10px, env(safe-area-inset-top));
    right: 10px;
    width: 44px;
    height: 44px;
    min-height: 44px;
    padding: 0;
    border-radius: 50%;
    justify-content: center;
    gap: 0;
  }
  .tint-toggle__txt { display: none; }
  .tint-toggle__dot { width: 15px; height: 15px; }
}

/* ============================================================
   GRID — bare images, native aspect, mono caption underneath
   JS-driven masonry (in app.js): items are positioned absolutely into
   the shortest column. Gives tight packing AND row-major reading order
   (newest first across the top). Tunables exposed as CSS vars so the
   JS reads them from getComputedStyle.
   ============================================================ */
.grid {
  position: relative;
  /* tunables read by applyMasonry() */
  --mason-min: 280;
  --mason-gx:  18;
  --mason-gy:  32;
}
@media (max-width: 1280px) { .grid { --mason-min: 240; } }
@media (max-width: 760px)  { .grid { --mason-min: 200; --mason-gx: 12; --mason-gy: 24; } }
/* Inserted 540px breakpoint to avoid the bad "2-col but each track is
   too narrow" zone the audit flagged (421-600px): 421px viewport with
   --mason-min 200 yielded two ~196px tracks that crammed captions
   and made image faces too small to read. Now 540px and below jumps
   to a single 280-wide track, then 420px stays at 1 col. */
@media (max-width: 540px)  { .grid { --mason-min: 280; --mason-gx: 10; --mason-gy: 20; } }
@media (max-width: 420px)  { .grid { --mason-min: 9999; } /* forces 1 column */ }

.tile {
  display: block;
  position: absolute;
  top: 0; left: 0;
  /* width + transform set by JS; transition makes layout shifts smooth.
     NO will-change here: hinting all 436 tiles promoted every one to its
     own GPU layer (100+MB of layer memory on the grid). Browsers promote
     automatically for the duration of a running transform transition,
     which is the only time tiles actually move. */
  transition: transform 360ms cubic-bezier(0.22, 1, 0.36, 1);
}
.tile.is-warmup {
  /* first-paint state — no transition, just snap in */
  transition: none;
}
.tile__img {
  width: 100%;
  height: auto;
  display: block;
  opacity: 0;
  transition: opacity 240ms var(--ease);
  background: var(--bg-2);
}
.tile__img.is-loaded { opacity: 1; }
/* Hover dim — gated to devices with a real hover so it doesn't leave
   touch users without a tap-feedback affordance (mobile audit flagged
   that :hover was the only "I'm tappable" signal). Touch devices get
   :active dim instead, plus a tile__cap baseline that makes the
   caption itself a tappable affordance. */
@media (hover: hover) {
  .tile:hover .tile__img { opacity: 0.92; }
}
.tile:active .tile__img { opacity: 0.85; }
.tile:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

.tile__cap {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--mute);
  margin-top: 6px;
  display: flex;
  justify-content: space-between;
  gap: 8px;
}
.tile__name { color: var(--ink-2); }
.tile__id { color: var(--mute); flex-shrink: 0; display: inline-flex; align-items: center; gap: 6px; }
.tile__swatch {
  display: inline-block;
  width: 7px;
  height: 7px;
  background: var(--swatch, var(--paper));
  border: 1px solid var(--hair-soft);
}
/* Dark-mode override for the swatch border — the literal rgba(0,0,0,
   0.08) edge disappeared against dark photo tints, leaving the swatch
   floating without an outline. Now tied to --hair-soft which already
   has a light variant in body.bg-dark. */
body.bg-dark .tile__swatch { border-color: rgba(255, 255, 255, 0.18); }

/* ============================================================
   PHOTOGRAPHERS — flat list + sort bar
   ============================================================ */
/* Sort bar — mono text-buttons (alphabetical / most works / shuffle).
   Sits between the section title and the directory list. Treated as
   inline text-buttons rather than chunky form controls so the mono
   typographic language stays consistent with the rest of the chrome
   (chrome__meta, section__meta, etc). The underline marks the active
   sort — same affordance as the chrome nav uses for `.is-active`. */
.ph-sort {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 8px;
  /* Negative margin-top cancels the .section's bottom margin so the
     sort row sits snug below the title line instead of with a double
     gap. Bottom padding then provides breathing room before the list. */
  margin-top: calc(-1 * clamp(20px, 3vw, 36px) + 6px);
  padding-bottom: clamp(14px, 2vw, 22px);
  font-size: 11.5px;
  color: var(--mute);
}
.ph-sort__label {
  color: var(--mute);
  margin-right: 4px;
}
.ph-sort__btn {
  font: inherit;
  color: var(--ink-2);
  padding: 2px 0;
  letter-spacing: 0;
  border-bottom: 1px solid transparent;
  transition: color 180ms var(--ease), border-color 180ms var(--ease);
  /* Reset the universal button rule so this looks like an inline
     text button, not a default chunky one. */
  background: none;
  cursor: pointer;
}
.ph-sort__btn:hover {
  color: var(--ink);
}
.ph-sort__btn.is-active {
  color: var(--ink);
  border-bottom-color: var(--ink);
}
.ph-sort__btn:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}
.ph-sort__sep {
  color: var(--mute-2);
  user-select: none;
  pointer-events: none;
}

.list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  border-top: 1px solid var(--hair);
}
.list li { list-style: none; border-bottom: 1px solid var(--hair); }
.list a {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 10px 9px 0;
  font-size: 13.5px;
  color: var(--ink-2);
  min-height: 44px;
}
.list a:hover { color: var(--ink); padding-left: 8px; background: var(--bg-2); }
.list a:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}
.list__count {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
}

/* photographer detail head */
.ph-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: clamp(14px, 2vw, 22px) 0;
  border-bottom: 1px solid var(--hair);
  margin-bottom: clamp(20px, 3vw, 36px);
  flex-wrap: wrap;
}
.ph-head__name { font-size: 14px; font-weight: 500; }
.ph-head__back {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
  margin-right: 12px;
}
.ph-head__back:hover { color: var(--ink); }

/* ============================================================
   SPECTRUM — the signature feature. studio rebuild.
   ============================================================ */
.spectrum { padding: 0; max-width: 100%; }
.spectrum__head {
  padding: clamp(14px, 2vw, 22px) var(--pad-x);
  max-width: var(--max);
  margin: 0 auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  border-bottom: 1px solid var(--hair);
}
.spectrum__head .label { font-size: 11px; }

/* stage = horizontal photo river. Background is the page tint. Stage takes
   most of the viewport — head bar + hint were removed, photos breathe. */
.spectrum__stage {
  position: relative;
  height: clamp(420px, 70vh, 780px);
  overflow: hidden;
  background: transparent;
  border-bottom: 1px solid var(--hair);
  border-top: 1px solid var(--hair);
  touch-action: none;
  cursor: grab;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 80px, #000 calc(100% - 80px), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 80px, #000 calc(100% - 80px), transparent 100%);
}
/* Mobile: the 80px left/right mask is too aggressive on a 375px-wide
   viewport (consumes 43% of the visible stage width and cuts the
   focused photo's edges). Halve it on small screens so the user
   sees more of the photo. */
@media (max-width: 600px) {
  .spectrum__stage {
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
            mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
  }
}
.spectrum__stage.is-dragging { cursor: grabbing; }
.spectrum__river {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  gap: clamp(24px, 3vw, 56px);
  will-change: transform;
}
.spectrum__photo {
  flex-shrink: 0;
  height: 78%;
  display: grid;
  place-items: center;
  position: relative;
  cursor: pointer;
  transition: transform 240ms cubic-bezier(.2,.8,.2,1),
              filter   240ms cubic-bezier(.2,.8,.2,1),
              opacity  240ms cubic-bezier(.2,.8,.2,1);
}
.spectrum__photo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  opacity: 0;
  transition: opacity 240ms cubic-bezier(0.4, 0, 0.2, 1);
}
.spectrum__photo img.is-loaded { opacity: 1; }
/* Hide the dominant-color placeholder once the photo has loaded. */
.spectrum__photo:has(img.is-loaded) { background: transparent !important; }
.spectrum__photo.is-focus { transform: scale(1.06); filter: none; opacity: 1; }
.spectrum__photo:not(.is-focus) { filter: saturate(0.78); transform: scale(0.86); opacity: 0.6; }

/* photo metadata that lives above the focused photo */
.spectrum__focus-cap {
  position: absolute;
  left: 50%;
  bottom: clamp(8px, 1.6vw, 18px);
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--ink-2);
  white-space: nowrap;
  pointer-events: none;
  background: color-mix(in srgb, var(--page-tint) 80%, transparent);
  padding: 3px 8px;
  letter-spacing: 0;
  z-index: 2;
}

/* the rail — informational, not just decorative */
.spectrum__rail {
  position: relative;
  max-width: var(--max);
  margin: clamp(40px, 5vw, 64px) auto 0;
  padding: 0 var(--pad-x);
}
.spectrum__rail-stack {
  position: relative;
}

/* density histogram — sits directly above the rail */
.spectrum__rail-histogram {
  height: 30px;
  display: flex;
  align-items: flex-end;
  gap: 1px;
  margin-bottom: 4px;
  pointer-events: none;
}
.spectrum__rail-histogram .bar {
  flex: 1;
  background: var(--bar-color);
  min-height: 1px;
  opacity: 0.85;
  transition: opacity 200ms var(--ease);
}
.spectrum__rail-stack:hover .spectrum__rail-histogram .bar { opacity: 1; }

/* the actual rainbow rail */
.spectrum__rail-bar {
  height: 16px;
  width: 100%;
  background: linear-gradient(
    to right,
    hsl(0,72%,52%) 0%,
    hsl(20,82%,55%) 5%,
    hsl(40,86%,52%) 12%,
    hsl(60,78%,50%) 18%,
    hsl(95,50%,46%) 26%,
    hsl(130,55%,42%) 36%,
    hsl(165,55%,45%) 45%,
    hsl(195,62%,50%) 55%,
    hsl(225,68%,55%) 65%,
    hsl(260,55%,55%) 75%,
    hsl(300,52%,55%) 85%,
    hsl(335,68%,55%) 93%,
    hsl(360,72%,52%) 100%
  );
  cursor: ew-resize;
  position: relative;
  touch-action: none;
}
.spectrum__rail-stack.is-dragging .spectrum__rail-bar { cursor: grabbing; }
/* Mobile: bump the rail to a touch-friendly height + add an invisible
   ::before that EXPANDS the hit area vertically (12px above/below) so
   it tap-scrubs reliably without a precision-finger problem. Visual
   stays at 28px so the rainbow track is still legible. */
@media (max-width: 760px) {
  .spectrum__rail-bar {
    height: 28px;
  }
  .spectrum__rail-bar::before {
    content: "";
    position: absolute;
    inset: -12px 0;
    /* transparent expansion zone — picks up pointer events for the
       parent via pointer-events: auto, which is the default. */
  }
}

/* empty hue regions overlay (no photos here) — uses the --hatch token
   so the stripes invert correctly in body.bg-dark (was hardcoded to a
   paper-cream rgba which inverted the visual intent on dark tints,
   making empty regions look brighter than filled). */
.spectrum__rail-empty {
  position: absolute;
  top: 0; bottom: 0;
  background:
    repeating-linear-gradient(45deg,
      transparent 0, transparent 4px,
      var(--hatch) 4px, var(--hatch) 8px);
  pointer-events: none;
}

/* per-photo tick marks below the rail */
.spectrum__rail-ticks {
  position: relative;
  height: 14px;
  margin-top: 6px;
  pointer-events: none;
}
.spectrum__rail-tick {
  position: absolute;
  top: 0;
  width: 1px;
  height: 6px;
  /* Tokenized to --mute so it inverts properly in body.bg-dark.
     Previously hardcoded rgba(20,20,20,0.32) which became invisible
     on dark photo tints. */
  background: var(--mute);
  transform: translateX(-0.5px);
}
.spectrum__rail-tick.is-focus {
  background: var(--ink);
  height: 14px;
  width: 2px;
  transform: translateX(-1px);
}

/* the playhead — horizontal indicator */
.spectrum__playhead {
  position: absolute;
  top: -10px;
  width: 2px;
  height: 38px;
  background: var(--ink);
  transform: translate3d(-1px, 0, 0);
  pointer-events: none;
  z-index: 4;
  will-change: transform;
}
.spectrum__playhead::before,
.spectrum__playhead::after {
  content: "";
  position: absolute;
  left: 50%;
  width: 7px;
  height: 7px;
  background: var(--ink);
  transform: translateX(-50%) rotate(45deg);
}
.spectrum__playhead::before { top: -4px; }
.spectrum__playhead::after  { bottom: -4px; }

/* hover tooltip — appears above the rail as the cursor moves */
.spectrum__rail-tip {
  position: absolute;
  bottom: calc(100% + 8px);
  transform: translate3d(0, 4px, 0);
  font-family: var(--mono);
  font-size: 10.5px;
  background: var(--ink);
  color: var(--bg);
  padding: 5px 8px;
  letter-spacing: 0;
  pointer-events: none;
  white-space: nowrap;
  opacity: 0;
  /* Added background-color + color to the transition list so the tip
     fades in lockstep with the page-tint lerp instead of snapping
     400ms ahead. Previously tip's bg (var(--ink)) and text (var(--bg))
     would re-resolve to the new tint values instantly while the
     surrounding bg was mid-lerp — produced a momentary dark-on-dark
     or light-on-light flash during fast hue-scrubbing. */
  transition:
    background-color 420ms var(--ease),
    color 420ms var(--ease),
    opacity 140ms var(--ease),
    transform 140ms var(--ease);
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 8px;
}
.spectrum__rail-tip .tipSwatch {
  display: inline-block;
  width: 7px; height: 7px;
  background: var(--tip-color, #fff);
}
.spectrum__rail-tip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: var(--tip-arrow, 12px);
  width: 0; height: 0;
  border: 4px solid transparent;
  border-top-color: var(--ink);
  transform: translateX(-50%);
}
.spectrum__rail-stack.is-hover:not(.is-dragging) .spectrum__rail-tip,
.spectrum__rail-stack.is-dragging .spectrum__rail-tip {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* readout strip below */
.spectrum__readout {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
  padding: 28px var(--pad-x) 6px;
  max-width: var(--max);
  margin: 0 auto;
}
/* Readout breakpoints — old setup was 4-col until 520px, where it
   dropped straight to 2-col. At 521-720px (large phones, small
   tablets) four 11px-mono columns wrapped badly with "photographer:
   Henri Cartier-Bresson" running off the track. Now 2-col by 720px,
   1-col by 380px. */
@media (max-width: 720px) {
  .spectrum__readout {
    grid-template-columns: 1fr 1fr;
    gap: 12px;
  }
}
@media (max-width: 380px) {
  .spectrum__readout {
    grid-template-columns: 1fr;
    gap: 6px;
    padding-top: 16px;
  }
}
.spectrum__readout strong { color: var(--ink); font-weight: 400; }
.spectrum__readout .col { display: flex; align-items: center; gap: 8px; }
.spectrum__readout .swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  background: var(--current-color, transparent);
  flex-shrink: 0;
  transition: background 220ms var(--ease);
}
.spectrum__hint {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
  text-align: center;
  padding: 4px var(--pad-x) clamp(28px, 4vw, 48px);
}
.spectrum__hint kbd {
  font-family: var(--mono);
  font-size: 10px;
  background: var(--bg-2);
  border: 1px solid var(--hair);
  padding: 1px 5px;
  color: var(--ink-2);
}

/* prevent text selection while dragging */
html.is-scrubbing,
html.is-scrubbing * {
  user-select: none !important;
  -webkit-user-select: none !important;
  /* Crosshair-only: keep no-select during a spectrum drag, but DON'T swap to
     a grabbing cursor — the base html/* rules above keep the crosshair. */
}

@media (max-width: 520px) {
  .spectrum__rail-histogram { height: 18px; }
  .spectrum__focus-cap { font-size: 9.5px; padding: 2px 6px; }
}

/* ============================================================
   ABOUT / SUBMIT — sober text
   ============================================================ */
.prose {
  max-width: 56ch;
  margin: 0 auto;
}
.prose h1 {
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 18px;
}
.prose h2 {
  font-size: 14px;
  font-weight: 500;
  margin-top: 28px;
  margin-bottom: 8px;
  color: var(--ink);
}
.prose p {
  font-size: 14px;
  line-height: 1.55;
  margin-bottom: 14px;
  color: var(--ink-2);
}
.prose p a, .prose a.in {
  border-bottom: 1px solid var(--mute-2);
  color: var(--ink);
}
.prose p a:hover { border-color: var(--ink); }
.prose ul {
  list-style: none;
  margin-bottom: 14px;
}
.prose ul li {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-2);
  padding: 3px 0;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  border-bottom: 1px dashed var(--hair);
}
.prose ul li span:last-child { color: var(--mute); }
.prose hr {
  border: 0;
  border-top: 1px solid var(--hair);
  margin: 28px 0;
}

/* About — full-archive download. Hairline row. Hover gives the main label an
   underline and bumps the arrow down — no box fill. Quiet, intentional. */
.about__download {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 22px 4px;
  margin: 32px 0;
  border-top: 1px solid var(--hair);
  border-bottom: 1px solid var(--hair);
  color: var(--ink);
  transition: border-color 220ms var(--ease);
}
.about__download:hover { border-color: var(--ink); }

.about__download-label { display: flex; flex-direction: column; gap: 4px; }
.about__download-main {
  font-size: 18px;
  font-weight: 500;
  letter-spacing: -0.015em;
  position: relative;
  display: inline-block;
}
.about__download-main::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: -3px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
}
.about__download:hover .about__download-main::after { transform: scaleX(1); }

.about__download-meta {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
  letter-spacing: 0;
}

.about__download-arrow {
  flex-shrink: 0;
  color: var(--ink-2);
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1), color 220ms var(--ease);
}
.about__download:hover .about__download-arrow { transform: translateY(3px); color: var(--ink); }

/* "— magnus holmes · personal work" signature row */
.about__sig {
  display: flex !important;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
  margin-top: 28px;
}
.about__sig .about__link {
  color: var(--ink-2);
  border-bottom: 1px solid var(--hair);
  padding-bottom: 1px;
  transition: color 200ms var(--ease), border-color 200ms var(--ease);
}
.about__sig .about__link:hover { color: var(--ink); border-color: var(--ink); }

/* submit form */
.submit-form {
  display: grid;
  gap: 14px;
  margin-top: 18px;
}
.submit-form .field { display: grid; gap: 6px; }
.submit-form label {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--mute);
}
.submit-form input, .submit-form textarea {
  font-family: var(--font);
  font-size: 14px;
  padding: 6px 0;
  border: 0;
  border-bottom: 1px solid var(--hair);
  background: transparent;
  color: var(--ink);
  outline: none;
  letter-spacing: -0.01em;
}
.submit-form input:focus, .submit-form textarea:focus { border-color: var(--ink); }
.submit-form input:focus-visible,
.submit-form textarea:focus-visible {
  border-color: var(--ink);
  /* Subtle outline ring for keyboard / a11y users without removing
     the existing border-bottom focus indicator. */
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}
.submit-form textarea { resize: vertical; min-height: 70px; }
/* iOS form zoom prevention — iOS Safari auto-zooms any focused field
   with computed font-size < 16px, trapping the user at a zoomed
   viewport. Bump inputs to 16px on mobile only so desktop typography
   stays at 14px. Also bump tap-area padding to 12px vertical so the
   field is a 44px+ tap target. */
@media (max-width: 720px) {
  .submit-form input,
  .submit-form textarea {
    font-size: 16px;
    padding: 12px 0;
  }
  .submit-form label {
    font-size: 11.5px;
  }
  .submit-form button {
    font-size: 13px;
    padding: 10px 0;
    min-height: 44px;
  }
}
.submit-form button {
  justify-self: start;
  margin-top: 8px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink);
  padding: 2px 0;
  border-bottom: 1px solid var(--ink);
}
.submit-form button:hover { color: var(--mute); border-color: var(--mute); }
.submit-form button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* ============================================================
   VIEWER — sits on top of the page tint (no own bg color anymore).
   Close, prev, next are minimal icon buttons with hover rings.
   ============================================================ */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  grid-template-columns: 1fr 280px;
  background: var(--page-tint);
  opacity: 0;
  pointer-events: none;
  /* visibility keeps the CLOSED viewer's controls (close/prev/next/
     download) out of the tab order and the accessibility tree —
     opacity+pointer-events alone left keyboard users tabbing through
     invisible controls on every page (aria-hidden with focusable
     content). The 0s/220ms visibility delay preserves the fade-out;
     is-open re-declares the full shorthand so the fade-in is intact. */
  visibility: hidden;
  transition: opacity 220ms var(--ease), visibility 0s linear 220ms;
}
.viewer.is-open {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
  transition: opacity 220ms var(--ease), visibility 0s;
}

.viewer__stage {
  display: grid;
  place-items: center;
  padding: clamp(24px, 4vw, 60px);
  position: relative;
}
.viewer__img {
  max-width: 100%;
  max-height: calc(100vh - 80px);
  object-fit: contain;
  transition: opacity 220ms var(--ease);
}

/* Icon buttons — minimal SVG, hover ring expands, no fill. Sit on the page-tint.
   Bumped to 44×44 for WCAG / Apple HIG touch target compliance (was
   36×36 — sub-threshold on phones). The visible icon stays its
   original 16-22px size inside the larger button. */
.viewer__close,
.viewer__nav {
  position: absolute;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  transition: color 200ms var(--ease), border-color 200ms var(--ease), background 200ms var(--ease), transform 160ms var(--ease);
}
.viewer__close:hover,
.viewer__nav:hover {
  color: var(--ink);
  border-color: var(--ink);
  background: color-mix(in srgb, var(--page-tint) 60%, transparent);
}
.viewer__close:focus-visible,
.viewer__nav:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.viewer__close {
  /* iOS safe-area on notched phones — close button would otherwise
     sit under the status bar / dynamic island. */
  top: max(18px, env(safe-area-inset-top) + 6px);
  right: 18px;
}
.viewer__nav { top: 50%; transform: translateY(-50%); }
.viewer__nav--prev { left: 18px; }
.viewer__nav--next { right: calc(280px + 18px); }
.viewer__nav:hover { transform: translateY(-50%) scale(1.06); }
.viewer__close:hover { transform: scale(1.06); }

.viewer__panel {
  border-left: 1px solid var(--hair);
  padding: clamp(20px, 2.4vw, 32px) clamp(18px, 2vw, 24px);
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: inherit;
}

.viewer__coord { color: var(--mute); }
.viewer__name { font-size: 14px; font-weight: 500; color: var(--ink); }

.viewer__actions {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 14px;
}
.link {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink);
  padding: 1px 0;
  text-align: left;
  width: fit-content;
  border-bottom: 1px solid transparent;
  transition: border-color 180ms var(--ease), color 180ms var(--ease);
}
.link:hover { border-color: currentColor; }

.viewer__details {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--hair);
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 14px;
  row-gap: 4px;
  font-size: 11px;
}
.viewer__details dt { color: var(--mute); }
/* word-break: break-word + overflow-wrap: anywhere lets long EXIF
   values, photographer URLs, and ISO/aperture strings wrap at word
   boundaries first (or character boundaries if forced) — way more
   readable than `break-all`, which was hyphenating every single
   character on narrow phone viewports. */
.viewer__details dd {
  color: var(--ink-2);
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* Footer removed — stats live on the About page now. */
.foot { display: none; }

/* ---------- responsive ---------- */
@media (max-width: 820px) {
  .chrome {
    grid-template-columns: 1fr auto;
    row-gap: 6px;
    /* Tighter padding on the sticky chrome so it doesn't eat ~13% of
       a phone viewport. Was clamping to ~16px each side; now 8/10. */
    padding-top: max(8px, env(safe-area-inset-top));
    padding-bottom: 8px;
  }
  .chrome__meta { display: none; }
  .nav {
    grid-column: 1 / -1;
    gap: 14px;
    /* Slight font bump on mobile nav so the 4 links fit on one line
       at 320-360px without wrapping. The 44-tap-target rule below
       gives them adequate hit area. */
    font-size: 13px;
  }
  .nav a {
    /* Bumped vertical padding to push the tap target up to ~44px
       (text at 13px line-height 1.5 ≈ 20px + 12+12 padding = 44).
       Visual rhythm of the nav text is unchanged because we use
       flex baseline alignment. */
    padding: 12px 0;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .home { grid-template-columns: 1fr; }
  .viewer { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
  .viewer__panel {
    border-left: 0;
    border-top: 1px solid var(--hair);
    padding: 16px 18px 18px;
    /* Panel height capped to 35vh so the photo stage gets the
       majority of the viewport (was 40vh — photos felt
       letterboxed on tall portrait shots). overflow-y: auto so
       long EXIF tables still scroll inside the panel. */
    max-height: 35vh;
    overflow-y: auto;
  }
  /* Prev/Next at mobile — move to bottom corners of the stage so a
     one-handed thumb can reach both without crossing the photo. The
     panel sits below the stage so we offset by panel height + a
     little gap. */
  .viewer__nav {
    top: auto;
    bottom: calc(35vh + 12px);
    transform: none;
  }
  .viewer__nav--prev { left: 14px; }
  .viewer__nav--next { right: 14px; }
  .viewer__nav:hover { transform: scale(1.06); }
  .viewer__img {
    /* Image cap relaxed — let it use the stage's full height (minus
       padding + the bottom buttons' clearance). Portrait photos
       finally fill vertical space on phones. */
    max-height: calc(65vh - 24px);
  }
}
/* Landscape phones — viewport is wide-ish but very short (e.g. iPhone
   in landscape: 844×390). The default 35vh panel = ~136px which is
   most of the screen; shrink it further so the photo gets the room. */
@media (max-width: 820px) and (orientation: landscape) {
  .viewer__panel { max-height: 30vh; }
  .viewer__nav { bottom: calc(30vh + 10px); }
  .viewer__img { max-height: calc(70vh - 20px); }
}
@media (max-width: 520px) {
  .foot { grid-template-columns: 1fr; }
  .foot > span:nth-child(2), .foot > span:last-child { text-align: left; }
  /* .spectrum__readout 2-col handled by the 720px rule above; on
     <380px it drops further to 1-col. */
  /* Mobile-only download-row layout: stack vertically (less awkward
     wrapping), tighter padding, smaller meta. */
  .about__download {
    padding: 18px 0;
    gap: 10px;
    flex-wrap: nowrap;
  }
  .about__download-main {
    font-size: 16px;
  }
  .about__download-meta {
    font-size: 10.5px;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
