/* ---------------------------------------------------------------
   Reel: an experiment. Project list pinned left, prints running
   right to left as the page scrolls down.
   Motion is native CSS scroll-driven animation. No library.
   Browsers without it fall back to a plain horizontal scroller.
   --------------------------------------------------------------- */

@font-face {
  font-family: "Geist";
  src: url("fonts/geist-latin.woff2") format("woff2");
  font-weight: 300 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light dark;
  --wall: #faf9f7;
  --ink: #1a1917;
  --ink-soft: #6f6a61;
  --hairline: #e2ded7;
  --print-shadow: 0 1px 2px rgba(60, 50, 40, 0.05),
    0 8px 24px rgba(60, 50, 40, 0.07);
  /* same shape, lifted: the print reads as picked up off the wall */
  --print-shadow-hover: 0 3px 8px rgba(60, 50, 40, 0.13),
    0 22px 55px rgba(60, 50, 40, 0.28);
  /* between the two: a print filling a phone screen needs more lift than one
     of twenty in the reel, but it is not being hovered either */
  --viewer-shadow: 0 2px 6px rgba(60, 50, 40, 0.09),
    0 14px 38px rgba(60, 50, 40, 0.16);
  --sans: "Geist", -apple-system, BlinkMacSystemFont, "Helvetica Neue",
    "Segoe UI", Arial, sans-serif;
  /* the signature, drawn by hand and scanned: the box is clipped to the
     initials at rest and opens to the full name on hover */
  /* wider than the rail's text column, running into its right padding — the
     rail clips at its padding edge, so there is room without widening it */
  --sig-w: 216px;             /* the full signature, drawn edge to edge */
  --sig-h: 52px;              /* the scan's own 980x236 ratio */
  --sig-rest: 92px;           /* clipped to where "From VV" ends, 42% in */

  --rail: 208px;              /* the list, wide enough for the signature */
  /* svh, not dvh: Safari grows and shrinks the viewport as its toolbar hides
     and returns, and dvh follows it — so every print resized while you were
     scrolling, which is most of the jumpiness. svh is the toolbar-visible
     height and never moves. */
  --h: min(80svh, 780px);     /* print height in the reel */
  --h-open: min(84svh, 820px);/* print height once a project is opened */
  --cover-w: 186px;           /* covers sit as thin slices until opened */
  --pad: clamp(1.25rem, 3vw, 3rem);
  --gap: clamp(2.75rem, 6vw, 6rem);      /* between prints in an open set */
  --reel-gap: clamp(1rem, 2vw, 2rem);    /* between the covers themselves */
  --endgap: clamp(4rem, 10vw, 11rem); /* breathing room after an open set */
  --startgap: clamp(3rem, 7vw, 8rem);  /* before the first cover */
  --tailgap: 600px;                    /* empty wall after the last one */
  --meta-w: clamp(12rem, 19vw, 17rem);   /* the notes column on an open set */
  --meta-step: calc(var(--meta-w) * 0.1); /* notes step toward the rail, not flush */
  --theme-fade: 0.4s ease;    /* light/dark crossfade */
}

/* Dark palette, reached two ways: the OS asks for it and the visitor hasn't
   overridden, or the visitor picked it outright. The two selectors can't be
   merged — one lives inside a media query — so the block is written twice.
   Deliberate: the alternative, light-dark(), breaks to unstyled text on any
   browser that doesn't know the function, and an unreadable page is a worse
   trade than six repeated lines. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --wall: #141312;
    --ink: #edeae5;
    --ink-soft: #948d83;
    --hairline: #2b2926;
    --print-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
    --print-shadow-hover: 0 22px 55px rgba(0, 0, 0, 0.85);
    --viewer-shadow: 0 12px 40px rgba(0, 0, 0, 0.62);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --wall: #141312;
  --ink: #edeae5;
  --ink-soft: #948d83;
  --hairline: #2b2926;
  --print-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
  --print-shadow-hover: 0 22px 55px rgba(0, 0, 0, 0.85);
  --viewer-shadow: 0 12px 40px rgba(0, 0, 0, 0.62);
}

:root[data-theme="light"] { color-scheme: light; }

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

/* Opening a project adds thousands of pixels to the page's height, and a
   browser's scroll anchoring answers that by nudging scrollY to keep what you
   were looking at still. Here scrollY *is* the reel's sideways position, so a
   nudge meant to hold the view steady throws the reel forward instead. */
html, body, .stage, .track { overflow-anchor: none; }

body {
  margin: 0;
  background: var(--wall);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  /* light/dark eases across rather than snapping. Every rule that paints a
     token carries the same duration, so the whole page turns together. */
  transition: background-color var(--theme-fade), color var(--theme-fade);
}

img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* --- the pinned rail --------------------------------------------- */

.rail {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  width: var(--rail);
  height: 100svh;
  /* a little inset on the left so the names aren't against the edge, and room
     on the right so they don't run up against the rule */
  padding: var(--pad) 2.25rem var(--pad) 2rem;
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
  overflow-y: auto;
  /* the list keeps its natural width while collapsing; the rail clips it,
     so nothing reflows on the way in or out */
  overflow-x: hidden;
  overscroll-behavior: contain;
  border-right: 1px solid var(--hairline);
  transition: width 0.45s cubic-bezier(0.16, 1, 0.3, 1),
              padding 0.45s cubic-bezier(0.16, 1, 0.3, 1),
              border-color var(--theme-fade);
}

.rail-top {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  font-size: 0.9375rem;
  transition: gap 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

/* theme toggle: a sun/moon glyph, quiet like the rail links rather than
   looking like a control */
.theme {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: 1rem;
  line-height: 1;
  color: var(--ink-soft);
  overflow: hidden;            /* so it can collapse to zero width */
  transition: color 0.2s ease,
              width 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

.theme:hover { color: var(--ink); }

/* collapse arrow: first in the row, so it keeps its place when the rail
   narrows to just this button */
.rail-toggle {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: 0.875rem;
  line-height: 1;
  color: var(--ink-soft);
  display: inline-flex;        /* so the rotation turns the glyph on its centre */
  transition: color 0.2s ease,
              transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

.rail-toggle:hover { color: var(--ink); }

/* collapsed: the rail keeps just enough width for the arrow, and .stage
   follows because its margin is the same --rail the arrow shrinks */
:root[data-rail="hidden"] { --rail: 3rem; }

:root[data-rail="hidden"] .rail { padding-inline: 0.875rem; }

/* fade rather than display:none — the contents keep their box while the rail
   narrows over them, so the names don't rewrap mid-transition. visibility
   flips only once the fade has finished, which also takes them out of the
   tab order while collapsed.
   This replaces the transition list on .theme and .rail-foot rather than
   adding to it, so width and colour have to be repeated here. */
.rail nav,
.rail .theme,
.rail-foot {
  transition: opacity 0.25s ease 0.12s,
              visibility 0s,
              width 0.45s cubic-bezier(0.16, 1, 0.3, 1),
              color var(--theme-fade);
}

:root[data-rail="hidden"] .rail nav,
:root[data-rail="hidden"] .rail .theme,
:root[data-rail="hidden"] .rail-foot {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* width is here for .theme: this rule replaces the transition list on the
     base selector, so leaving it out would snap the button shut */
  transition: opacity 0.2s ease,
              width 0.45s cubic-bezier(0.16, 1, 0.3, 1),
              visibility 0s linear 0.2s;
}

/* one glyph that turns, rather than swapping ← for → */
:root[data-rail="hidden"] .rail-toggle { transform: rotate(180deg); }

/* collapsed, the arrow is the only thing left in the row: drop the gap and
   let the theme button shrink to nothing, so the arrow centres in the rail
   instead of sitting where the two-button row used to start */
:root[data-rail="hidden"] .rail-top {
  gap: 0;
  justify-content: center;
}

:root[data-rail="hidden"] .rail .theme { width: 0; }

/* tightly grouped: no list bullets, minimal leading */
.rail ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.0625rem;
}

.rail ol a {
  display: block;
  padding: 0.0625rem 0;
  font-size: 0.875rem;
  line-height: 1.45;
  color: var(--ink-soft);
  transition: color 0.2s ease;
}

.rail ol a:hover { color: var(--ink); }

/* set by reel.js only when a project is opened, not while scrolling past it */
.rail ol a[aria-current="true"] {
  color: var(--ink);
  font-weight: 500;
}

.rail-foot {
  margin-top: auto;
  font-size: 0.75rem;
  color: var(--ink-soft);
  display: grid;
  justify-items: start;
  gap: 0.375rem;
}

.rail-foot a {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  color: inherit;
  text-decoration: none;
  transition: color 0.2s ease;
}

.rail-foot a:hover { color: var(--ink); }

/* only ever a thing on touch, where the rail is a drawer over the reel */
.rail-scrim { display: none; }

@media (max-width: 1024px) {
  .rail-scrim { display: block; }
}

/* The signature: two scans of the same hand on the same canvas, one reading
   "From VV" and one the full name, stacked and clipped. Hovering opens the box
   first — the initials sit still while the space beside them grows — and the
   full name is crossfaded in behind that, which is the second beat. */
.rail-sig {
  position: relative;
  margin: 1.25rem 0 0;
  width: var(--sig-rest);
  height: var(--sig-h);
  overflow: hidden;
  cursor: default;
}

.rail-sig img {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--sig-w);        /* both sit at full size; the box does the hiding */
  height: var(--sig-h);
  max-width: none;
  object-fit: contain;
}

/* Three scans, three beats: "From VV", then the two V's stood apart, then the
   whole name. The box opens continuously while the scans hand over at the
   moments the opening reaches them.
   Every fade is linear and every pair hands over at the same instant — an
   eased pair crosses below full opacity in the middle and the wall flashes
   through the gap.
   The delays here are the way back: on leaving, the full scan goes first, then
   the middle, and the initials return last. :hover replaces them with the
   forward order. */
/* .rail-sig .sig-x, not .sig-x: the shorthand above is (0,1,1) and would
   otherwise outrank a bare class and reset every delay to zero */
.rail-sig img { transition: opacity 0.12s linear; }

/* Leaving. The ink retreats right to left and stops just past the first V, at
   35% — the name un-writes back to the initials rather than wiping off the
   page. Both scans put that V in the same place, and the second V is showing
   underneath from the middle scan the whole time, so when the full scan drops
   out at the end of the retreat nothing on screen changes. */
.rail-sig .sig-hidden { opacity: 1; transition-delay: 0.44s; }
.rail-sig .sig-middle { opacity: 0; transition-delay: 0.44s; }
.rail-sig .sig-full {
  opacity: 0;
  clip-path: inset(0 65% 0 0);      /* 35% across: the first V's far edge */
  transition: clip-path 0.42s cubic-bezier(0.4, 0, 0.4, 1) 0s,
              opacity 0.01s linear 0.42s;
}

/* the paper closes after the ink has gone, not during */
.rail-sig { transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.42s; }

/* Arriving: initials, then the V's apart, then the name written in from the
   left. The full scan appears already clipped to 35% — where the retreat
   stopped — so it takes over from the middle scan without a seam. */
.rail-sig:hover { transition: width 0.52s cubic-bezier(0.16, 1, 0.3, 1) 0s; }
.rail-sig:hover .sig-hidden { opacity: 0; transition-delay: 0.14s; }
.rail-sig:hover .sig-middle { opacity: 1; transition-delay: 0.14s; }
.rail-sig:hover .sig-full {
  opacity: 1;
  clip-path: inset(0 0 0 0);
  transition: clip-path 0.62s cubic-bezier(0.45, 0, 0.3, 1) 0.34s,
              opacity 0.01s linear 0.34s;
}

.rail-sig:hover { width: var(--sig-w); }

/* black ink disappears on the dark wall, so it is flipped to white there */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .rail-sig img { filter: invert(1); }
}
:root[data-theme="dark"] .rail-sig img { filter: invert(1); }

@media (prefers-reduced-motion: reduce) {
  .rail-sig, .rail-sig img, .rail-sig .sig-full,
  .rail-sig:hover, .rail-sig:hover .sig-full { transition: none; }
}

/* --- the reel ---------------------------------------------------- */

.stage {
  margin-left: var(--rail);
  /* the reel slides over as the rail collapses rather than snapping */
  transition: margin-left 0.45s cubic-bezier(0.16, 1, 0.3, 1);
  /* height and --pan are both set by reel.js, and reset whenever a
     project opens or closes and the track gets longer */
  min-height: 100svh;
  view-timeline-name: --reel;
  view-timeline-axis: block;
}

.pin {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  display: flex;
  align-items: center;
}

.track {
  display: flex;
  align-items: center;
  gap: var(--reel-gap);
  /* extra room on the left so the first cover clears the rail instead of
     starting hard against it, and a long run of wall after the last one —
     the reel should end on empty wall, not stop dead at the final print */
  padding: 0 var(--tailgap) 0 var(--startgap);
  width: max-content;          /* never let the row shrink to fit the pin */
  will-change: transform;
}

@keyframes pan {
  to { transform: translate3d(calc(-1 * var(--pan, 0px)), 0, 0); }
}

@supports (animation-timeline: view()) {
  .track {
    animation: pan linear both;
    animation-timeline: --reel;
    animation-range: contain 0% contain 100%;
  }
}

/* reel.js has taken the pan over: two movers on one track fight each other */
:root[data-pan="js"] .track { animation: none !important; }

/* group = one project: a cover, plus the rest folded away behind it */
.group {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  padding-bottom: 3.25rem;     /* room for the label and the hint */
}

.group img {
  flex: 0 0 auto;
  height: var(--h);
  width: auto;
  max-width: none;             /* the global img rule would cap these */
  background: none;            /* the blurred stand-in sits on the figure */
  box-shadow: var(--print-shadow);
  /* fades up over its own placeholder once the file lands, rather than
     replacing it between one frame and the next */
  opacity: 0;
  transition: height 0.72s cubic-bezier(0.16, 1, 0.3, 1),
              width 0.72s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.55s ease,
              box-shadow var(--theme-fade);
}

.group img.ready { opacity: 1; }

/* the cover is a thin vertical slice of the first print. opening the
   project widens it back out to the print's real 2:3 */
.cover .face {
  width: var(--cover-w);
  object-fit: cover;
}

/* --face-w is set by reel.js from the project's own prints; the 2:3 is
   only the fallback for a set whose prints haven't loaded yet */
.group.open .cover .face {
  width: var(--face-w, calc(var(--h-open) * 2 / 3));
}

.cover .face { position: relative; z-index: 3; }

/* Surreal's cover sits a touch right of centre */
#g-surreal .cover .face { object-position: 55% center; }

/* India's cover sits a touch right of centre */
#g-india .cover .face { object-position: 50% center; }

/* London, likewise */
#g-london .cover .face { object-position: 47% center; }

/* St Johns, likewise */
#g-st-johns .cover .face { object-position: 70% center; }

/* Sicily, likewise */
#g-sicily .cover .face { object-position: 70% center; }

/* Berlin 26, likewise */
#g-berlin .cover .face { object-position: 30% center; }

/* Athens, likewise */
#g-athens .cover .face { object-position: 90% center; }

/* Garnish, likewise */
#g-garnish .cover .face { object-position: 64% center; }

/* Minimal, likewise */
#g-minimal .cover .face { object-position: 83% center; }

#g-safari .cover .face { object-position: 40% center; }

/* Mirror, likewise */
#g-mirror .cover .face { object-position: 52% center; }

/* India Journalistic, likewise */
#g-india-journalistic .cover .face { object-position: 60% center; }

/* the hover deck is gone: a cover is just a cover */
.peek { display: none; }

.group:not(.open):has(.cover:hover)::after,
.group:not(.open):has(.cover:focus-visible)::after { color: var(--ink); }

/* and a hint of what the click does, tucked under the label */
.group::before {
  content: "Open";
  position: absolute;
  left: 0;
  bottom: 0;
  font-size: 0.6875rem;
  color: var(--ink-soft);
  opacity: 0;
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.group:not(.open):has(.cover:hover)::before,
.group:not(.open):has(.cover:focus-visible)::before { opacity: 1; }

/* once open, the notes column carries a real Close button, so the hover
   hint on the cover is just a second thing saying the same word */
.group.open::before { content: none; }
.cover .face { transition: height 0.72s cubic-bezier(0.16, 1, 0.3, 1),
                          width 0.72s cubic-bezier(0.16, 1, 0.3, 1),
                          opacity 0.55s ease,
                          box-shadow 0.25s ease; }

/* hovering a collection lifts its cover off the wall. Faster than the
   open/close easing above: a hover should answer immediately. Closed only —
   an open cover no longer does anything on click, so it should not keep
   offering itself as a target. */
.group:not(.open) .cover:hover .face,
.group:not(.open) .cover:focus-visible .face {
  box-shadow: var(--print-shadow-hover);
}

/* an open cover no longer does anything on click, so stop offering */
.group.open .cover { cursor: default; }

.cover {
  appearance: none;
  position: relative;          /* anchors the hover deck */
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
  display: block;
  flex: 0 0 auto;
}

/* --- the notes column -------------------------------------------------
   Sits in the wall to the left of the first print, and like .rest it is a
   flex item whose width animates from zero, so a closed project still reads
   as a bare slice of cover. Built by reel.js from the group's data-* attrs. */
.meta {
  flex: 0 0 auto;
  width: 0;
  overflow: hidden;
  align-self: center;
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--ink-soft);
  transition: width 0.72s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.3s ease;
  opacity: 0;
}

.group.open .meta {
  width: var(--meta-w);
  margin-left: calc(-1 * var(--meta-step));
  margin-right: var(--gap);
  opacity: 1;
  transition: width 0.72s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.4s ease 0.25s;      /* fade in once there is room */
}

/* the text must not reflow while the column is still opening */
.meta-inner { width: var(--meta-w); }

/* the project name carries the column: set well above the rail and the
   prose so the eye lands here first */
.meta h2 {
  margin: 0 0 0.75rem;
  font-size: clamp(1.5rem, 2.1vw, 2.125rem);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--ink);
  transition: color var(--theme-fade);
}

/* type and year: a label, not prose. Small and lettered out so it reads as
   a different tier from both the name above and the note below. */
.meta-line {
  margin: 0 0 1.125rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  line-height: 1.4;
}

.meta-line span::after {
  content: "·";
  margin-left: 0.5rem;
  opacity: 0.5;
}
.meta-line span:last-child::after { content: none; }

/* the note is the one place that wants comfortable reading */
.meta p {
  margin: 0 0 1rem;
  font-size: 0.8125rem;
  line-height: 1.7;
}

.meta-link {
  display: block;
  margin: 1.25rem 0 0;
  font-size: 0.75rem;
  color: var(--ink-soft);
  text-decoration: none;
  border-bottom: 1px solid var(--hairline);
  padding-bottom: 1px;
  width: fit-content;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.meta-link:hover {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

.meta-close {
  appearance: none;
  margin: 1.75rem 0 0;
  font-size: 0.75rem;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  color: var(--ink-soft);
  border-bottom: 1px solid var(--hairline);
  padding-bottom: 1px;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.meta-close:hover {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

/* the fan-out. width is animated from 0 to its measured natural width */
.rest {
  display: flex;
  align-items: center;
  gap: var(--gap);
  width: 0;
  /* Clips the prints to the animating width exactly as overflow:hidden did,
     but only on the horizontal axis. The captions hang below their figures,
     and overflow:hidden cut every one of them off — you cannot ask for
     hidden-x and visible-y, so the clip is expressed as a path instead. */
  clip-path: inset(-100vh 0 -100vh 0);
  transition: width 0.72s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Arrived. The clip has done its job and now only hurts: it cuts prints off at
   the fan's own left edge as they pan away, well before they reach the rail.
   reel.js adds the class on transitionend and drops it on close. */
.group.open.settled .rest { clip-path: none; }

/* Closed, the fan is 0px wide but its prints still overflow it, and clip-path
   clips only the painting — the track's scrollWidth counts them, so the reel
   ran on into hundreds of pixels of empty wall past the last project. Safe
   because the set is closed: the captions overflow would cut off are at
   opacity 0 until it opens. */
.group:not(.open) .rest { overflow: hidden; }

/* the gap between cover and fan only exists once it is open */
.rest > :first-child { margin-left: var(--gap); }

/* --- captions ---------------------------------------------------------
   Every print is a figure now, including the cover. The default UA margin
   on figure would knock the whole reel sideways, so it goes first. */
.cover-fig,
.rest figure {
  margin: 0;
  padding: 0;
  position: relative;
  flex: 0 0 auto;
  /* reel.js hangs a 20px version of the print here; the hairline is what
     shows for anything the manifest doesn't cover */
  background: var(--hairline) center / cover no-repeat;
  box-shadow: var(--print-shadow);
  transition: background-color var(--theme-fade), box-shadow var(--theme-fade);
}

/* Out of flow on purpose. reel.js sizes the fan from the prints' own
   ratios and --h-open; a caption that took part in layout would add height
   the pin has to clip and width the measurement never accounted for. */
figcaption {
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  margin-top: 0.75rem;
  font-size: 0.75rem;
  font-style: italic;
  line-height: 1.5;
  color: var(--ink-soft);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease, color var(--theme-fade);
}

/* closed, a cover is a bare slice in the reel — no caption under it */
.group.open figcaption { opacity: 1; transition-delay: 0.25s; }

/* used while a set off to the left is folded away in one frame, so its
   cover doesn't keep narrowing for half a second afterwards */
.no-anim, .no-anim * { transition: none !important; }

.group.open { z-index: 2; }
.group.open img { height: var(--h-open); }

/* the prints behind the cover keep their real proportions */
.rest img { object-fit: contain; }

/* an open set ends in empty wall and a full-height rule, so the next
   project doesn't arrive on top of the last print. The element is added
   by reel.js; its width is part of the width the fan animates to. */
.rest > :last-child { margin-right: var(--endgap); }

.endline {
  position: absolute;
  top: -100dvh;                /* the pin clips it to the viewport */
  bottom: -100dvh;
  width: 1px;
  background: var(--hairline);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease 0.3s, background-color var(--theme-fade);
}

.endline { right: 0; }

/* Unconditional, including the first project: the lead-in is no longer only
   separation from whatever ran into it, it is also the wall the notes column
   starts after. Exempt the first one and it opens hard against the rail
   while every other project gets a lead-in. */
.group.open { margin-left: var(--endgap); }

.group.open .endline { opacity: 1; }

/* a quiet label under the start of each project's run */
.group::after {
  content: attr(data-name);
  position: absolute;
  left: 0;
  bottom: 1.35rem;
  max-width: var(--cover-w);
  font-size: 0.875rem;
  padding-top: 0.25rem;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  transition: max-width 0.72s cubic-bezier(0.16, 1, 0.3, 1),
              color var(--theme-fade);
}

/* the label under the cover names a closed slice. Open, the notes column
   already has the title as a heading, so drop the duplicate. */
.group.open::after { content: none; }

/* --- fallbacks --------------------------------------------------- */

/* No scroll-driven animation: reel.js drives the pan by hand instead and sets
   data-pan="js" to say so. This is the last resort behind that — a plain
   horizontal scroller for a browser with neither, i.e. no JS at all. */
@supports not (animation-timeline: view()) {
  :root:not([data-pan="js"]) .stage { height: auto; }
  :root:not([data-pan="js"]) .pin {
    position: static;
    height: 100svh;
    overflow-x: auto;
    overflow-y: hidden;
  }
  :root:not([data-pan="js"]) .track { transform: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  .rail, .stage, .rail-toggle,
  .rail nav, .rail .theme, .rail-foot { transition: none; }
  :root { --theme-fade: 0s; }
  .stage { height: auto; }
  .pin { position: static; height: 100svh; overflow-x: auto; overflow-y: hidden; }
  .track { animation: none !important; transform: none !important; }
}

/* --- touch: phone and tablet --------------------------------------------
   Up to 1024px the rail stops being a column of the layout and becomes a
   drawer over it. Everything else about the reel survives: the same cover
   slices, the same vertical-scroll-drives-horizontal-pan. What changes is
   that a cover opens the fullscreen viewer instead of fanning out sideways,
   since there is no room to the right to fan into. */
@media (max-width: 1024px) {
  :root {
    /* print and cover scale together — the slice keeps the same proportion,
       there is just more of it */
    --h: min(72svh, 720px);
    --cover-w: 216px;
    --pad: 1.125rem;
    --startgap: 3.5rem;
  }

  /* the drawer. --rail is left alone here: .stage no longer reserves it */
  .rail {
    width: min(78vw, 320px);
    /* top padding matches the fixed toggle's own offset less its tap padding,
       so the row inside the drawer lands on the arrow's line exactly */
    padding: calc(var(--pad) - 0.5rem) 1.5rem var(--pad);
    gap: 1.5rem;
    background: var(--wall);
    z-index: 20;
    /* slid with `left`, not `transform`: a transformed ancestor becomes the
       containing block for position:fixed, which would drag the toggle button
       off-screen with the drawer it is supposed to bring back */
    transition: left 0.38s cubic-bezier(0.16, 1, 0.3, 1),
                border-color var(--theme-fade);
  }

  :root[data-rail="hidden"] .rail {
    left: calc(-1 * min(78vw, 320px) - 2px);   /* clears its own border */
    padding-inline: 1.5rem;      /* undo the desktop collapse-to-a-sliver */
  }

  /* the contents ride out with the drawer rather than fading: fading them
     leaves an empty white panel sliding around on its own */
  :root[data-rail="hidden"] .rail nav,
  :root[data-rail="hidden"] .rail .theme,
  :root[data-rail="hidden"] .rail-foot {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
  }

  /* the toggle is the one thing that must not ride out with the drawer, so it
     leaves the row — the padding holds its seat, and the moon keeps its place
     beside it instead of dropping to a line of its own */
  .rail-top { padding-left: 2.25rem; min-height: 2.125rem; }
  .rail-toggle {
    position: fixed;
    top: var(--pad);
    left: var(--pad);
    z-index: 30;
    font-size: 1.125rem;
    padding: 0.5rem;             /* 44px tap target, per Apple's minimum */
    margin: -0.5rem;
  }
  :root[data-rail="hidden"] .rail .theme { width: auto; }
  :root[data-rail="hidden"] .rail-top { gap: 0.875rem; justify-content: flex-start; }

  /* the arrow holds the left of the row from its fixed seat, so the theme
     glyph takes the far right rather than floating beside nothing */
  .rail .theme { margin-left: auto; font-size: 1.125rem; }

  .rail ol { gap: 0.375rem; }
  .rail ol a { font-size: 1rem; padding: 0.25rem 0; }

  /* the two links read as the end of the same list, not as small print */
  .rail-foot { gap: 0.5rem; }
  .rail-foot a { font-size: 1rem; gap: 0.625rem; }
  .rail-foot svg { width: 16px; height: 16px; }

  .rail-scrim {
    position: fixed;
    inset: 0;
    z-index: 15;
    appearance: none;
    border: 0;
    padding: 0;
    background: rgba(0, 0, 0, 0.35);
    opacity: 1;
    transition: opacity 0.38s ease, visibility 0s;
  }

  :root[data-rail="hidden"] .rail-scrim {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.3s ease, visibility 0s linear 0.3s;
  }

  /* the page behind the drawer must not scroll under your thumb */
  :root:not([data-rail="hidden"]) body { overflow: hidden; }

  .stage { margin-left: 0; }
  /* a phone screen is narrow enough that 600px of wall is a long dead swipe */
  .track { padding-right: var(--endgap); }


  /* the desktop hover affordances have no meaning on touch */
  .group::before { content: none; }
}

/* --- the fullscreen viewer ----------------------------------------------
   Touch only, built by reel.js on first open. One horizontal strip of
   full-width slides with scroll-snap: the swipe is the browser's, not ours.
   Lives outside the media query because it is display:none until opened and
   only ever opened from the touch branch in reel.js. */
.viewer {
  /* The strip is a fixed frame at the house 2:3, as tall as the width allows
     or the screen allows, whichever runs out first. Every portrait print fills
     it exactly and the odd landscape one letterboxes inside it — which is what
     lets the labels sit on the photograph's edges AND hold still: the edges
     they hug belong to the frame, and the frame is the same on every slide. */
  /* white space either side of the print. Widening it is what keeps the frame
     short enough to stay clear of the close button: the height is derived from
     the width, so more inset means a shorter frame on every slide alike. */
  --frame-inset: 2.25rem;
  --frame-h: min(calc(100svh - 15rem),
                 calc((100vw - 2 * var(--frame-inset)) * 1.5));

  position: fixed;
  inset: 0;
  z-index: 40;
  background: var(--wall);
  display: flex;
  flex-direction: column;
  transition: background-color var(--theme-fade);
}

.viewer[hidden] { display: none; }

/* Opening used to be a cut: reel one frame, fullscreen print the next. This
   is the print coming up to meet you — reel.js drops .opening on the frame
   after it unhides, and the transition carries it the rest of the way. */
.viewer.opening { opacity: 0; }
.viewer.opening .viewer-track { transform: scale(0.965); }

.viewer {
  transition: opacity 0.26s ease, background-color var(--theme-fade);
}

.viewer-track {
  transition: transform 0.34s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  .viewer, .viewer-track { transition: none; }
  .viewer.opening { opacity: 1; }
  .viewer.opening .viewer-track { transform: none; }
}

.viewer-track {
  /* takes whatever the bar leaves, so the bar sits on the bottom edge of the
     screen and the photograph centres in the room above it */
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

.viewer-track::-webkit-scrollbar { display: none; }

/* swiping is the main way through, so the photograph gets the full width and
   nothing is laid over it */
.viewer-slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  scroll-snap-stop: always;    /* one print per swipe, never two */
  display: grid;
  place-items: center;
  /* Top: clearance under the close button, and since the print is centred in
     what is left, the heavier top padding also settles it slightly lower.
     Bottom: somewhere for the drop shadow to land — the track clips its
     overflow, so without this the shadow is cut off square at the caption. */
  padding: 5.5rem var(--frame-inset) 3rem;
  min-width: 0;
}

/* shrink-wrapped to the print, so the placeholder is exactly its size */
.viewer-frame {
  max-width: 100%;
  width: fit-content;
  background: center / cover no-repeat;
  box-shadow: var(--viewer-shadow);
  transition: box-shadow var(--theme-fade);
}

.viewer-slide img.ready { opacity: 1; }

.viewer-slide img {
  display: block;
  opacity: 0;
  transition: opacity 0.55s ease;
  max-width: 100%;
  /* the frame's own height, not 100%: the percentage resolves against a slide
     whose height the flex track only stretches to, and a tall print then runs
     straight past the bottom of the frame and gets clipped */
  max-height: var(--frame-h);
  width: auto;
  height: auto;
  object-fit: contain;
  pointer-events: none;        /* the swipe belongs to the track */
  user-select: none;
  -webkit-user-drag: none;
}

/* --- the two bars ---------------------------------------------------------
   Fixed to the overlay rather than carried by the slides. A label that rides
   its own slide swipes away under your thumb, and one that sits on a
   photograph's edge jumps as each photograph's height changes. These hold
   still; reel.js swaps the text. */
/* one bar, under the photograph, inset to the frame's own edges so it reads as
   hanging off the print rather than off the screen */
.viewer-foot {
  flex: 0 0 auto;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem var(--frame-inset) calc(2.5rem + env(safe-area-inset-bottom));
  min-height: 3.5rem;          /* the bar never changes height */
  font-size: 0.8125rem;
  color: var(--ink-soft);
}

.viewer-meta { min-width: 0; }

.viewer-head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding-bottom: 0.125rem;
}

.viewer-title {
  font-size: 0.9375rem;        /* the one thing in the bar set above the rest */
  color: var(--ink);
  transition: color var(--theme-fade);
}

.viewer-count {
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

.viewer-cap {
  margin: 0;
  font-style: italic;
  line-height: 1.5;
}

.viewer-navs {
  display: flex;
  flex: 0 0 auto;
  gap: 0.25rem;
  margin-right: -0.625rem;   /* the tap padding, pulled back off the edge */
}

.viewer-close,
.viewer-nav {
  z-index: 41;
  appearance: none;
  margin: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  color: var(--ink-soft);
  padding: 0.625rem;           /* 44px tap target */
  line-height: 1;
  transition: color 0.2s ease, opacity 0.2s ease;
}

.viewer-close {
  position: fixed;
  top: 0.5rem;
  right: 0.75rem;
  font-size: 1.5rem;
}

.viewer-nav { font-size: 1rem; }

/* at the ends the arrow stays in place but stops offering — moving it or
   removing it makes the row jump under your thumb */
.viewer-nav[disabled] { opacity: 0.2; pointer-events: none; }

/* --- image protection -------------------------------------------- */

.group img {
  user-select: none;
  -webkit-user-drag: none;
}

/* touch: no long-press -> Save Image. The tap still reaches .cover, since
   the button is the parent and the img is what stops taking events. */
@media (hover: none) {
  .group img { pointer-events: none; }
}

#copy-notice {
  position: absolute;
  z-index: 2147483647;
  width: 200px;
  padding: 8px;
  border-radius: 4px;
  background: var(--wall);
  color: var(--ink);
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.35);
  font-size: 11px;
  line-height: 1.5;
  pointer-events: none;
}

/* --- the copyright line -------------------------------------------
   The page's scroll is the reel's sideways travel, so a footer in normal
   flow lands past the end of the track and under the sticky pin. It sits
   fixed in the corner instead, and lets clicks through to the prints. */
.site-foot {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 9;
  color: var(--ink-soft);
  font-size: 0.625rem;
  letter-spacing: 0.02em;
  pointer-events: none;
  transition: color var(--theme-fade);
}
