/* Layout from design.pdf (2000 × 1124 artboard): content block starts at
   8.3vw / 32.5vh. Type is a fixed 12px; line height and the staircase step
   scale from it. */

:root {
  --bg: #000;
  --fg: #fafafa;
  --dim: rgba(250, 250, 250, 0.42);
  --line: rgba(250, 250, 250, 0.35);
  --shadow: 0 0 18px rgba(0, 0, 0, 0.35);

  --x: 8.3vw;
  --y: 32.5vh;
  --fs: 16px;
  --lh: calc(var(--fs) * 1.36);
  --step: calc(var(--fs) * 2.05);
  --cw: max(34vw, 390px);
  --film-scale: 0.7; /* film size within its screen, wide windows (1 = edge to edge) */
  --band: 44px; /* film keeps clear of this strip top and bottom; bottom labels live in it */
  --font: "Helvetica Neue", "Inter Tight", Helvetica, Arial, sans-serif;
  /* Small cross cursor over the stills. */
  --reticle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='17' height='17' viewBox='0 0 17 17'%3E%3Cpath d='M8.5 1v15M1 8.5h15' stroke='%23000' stroke-opacity='0.45' stroke-width='3.45'/%3E%3Cpath d='M8.5 1v15M1 8.5h15' stroke='%23fafafa' stroke-width='1.15'/%3E%3C/svg%3E") 8 8, crosshair;
  /* Larger cross cursor over the index picture (25% bigger and heavier than the gallery's). */
  --reticle-lg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='21' height='21' viewBox='0 0 21 21'%3E%3Cpath d='M10.5 1v19M1 10.5h19' stroke='%23000' stroke-opacity='0.45' stroke-width='4.3'/%3E%3Cpath d='M10.5 1v19M1 10.5h19' stroke='%23fafafa' stroke-width='1.44'/%3E%3C/svg%3E") 10 10, crosshair;
  /* Over the film: what a click will do. Light with a soft outline, to read on any frame. */
  --cur-play: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='21' height='21' viewBox='0 0 21 21'%3E%3Cg fill='%23fafafa' stroke='%23000' stroke-opacity='0.4' stroke-width='2' stroke-linejoin='round' paint-order='stroke'%3E%3Cpath d='M7 4.5 L16.5 10.5 L7 16.5 Z'/%3E%3C/g%3E%3C/svg%3E") 10 10, pointer;
  --cur-pause: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='21' height='21' viewBox='0 0 21 21'%3E%3Cg fill='%23fafafa' stroke='%23000' stroke-opacity='0.4' stroke-width='2' stroke-linejoin='round' paint-order='stroke'%3E%3Crect x='6' y='4.5' width='3.2' height='12'/%3E%3Crect x='11.8' y='4.5' width='3.2' height='12'/%3E%3C/g%3E%3C/svg%3E") 10 10, pointer;
  --bold: 700; /* weight for names, titles and links: 500 Medium, 700 Bold */
  --title-weight: 700; /* the film title in the player */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

:root[data-theme="light"] {
  --reticle-lg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='21' height='21' viewBox='0 0 21 21'%3E%3Cpath d='M10.5 1v19M1 10.5h19' stroke='%23fafafa' stroke-opacity='0.45' stroke-width='4.3'/%3E%3Cpath d='M10.5 1v19M1 10.5h19' stroke='%23000' stroke-width='1.44'/%3E%3C/svg%3E") 10 10, crosshair;
  --reticle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='17' height='17' viewBox='0 0 17 17'%3E%3Cpath d='M8.5 1v15M1 8.5h15' stroke='%23fafafa' stroke-opacity='0.45' stroke-width='3.45'/%3E%3Cpath d='M8.5 1v15M1 8.5h15' stroke='%23000' stroke-width='1.15'/%3E%3C/svg%3E") 8 8, crosshair;
  --bg: #fafafa;
  --fg: #000;
  --dim: rgba(0, 0, 0, 0.32);
  --line: rgba(0, 0, 0, 0.25);
  --shadow: none;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } /* keep phones from auto-enlarging text */
html, body { overscroll-behavior-x: none; } /* no browser back/forward swipe; horizontal swipes belong to the gallery */
body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-size: var(--fs);
  line-height: var(--lh);
  letter-spacing: -0.005em;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  transition: background-color 0.5s var(--ease), color 0.5s var(--ease);
}
a, button { color: inherit; text-decoration: none; font: inherit; background: none; border: 0; cursor: pointer; }
ol, ul { list-style: none; }
h2 { font-size: inherit; }
img { display: block; }
.strong { font-weight: var(--bold); }
:focus-visible { outline: 1px solid currentColor; outline-offset: 3px; }

.view { position: fixed; inset: 0; animation: fade-in 0.6s var(--ease); }
.view[hidden] { display: none; }
@keyframes fade-in { from { opacity: 0; } }

.block {
  position: absolute;
  left: var(--x);
  top: var(--y);
  z-index: 5;
  text-shadow: var(--shadow);
}
.masthead { display: flex; align-items: center; gap: calc(var(--fs) * 0.9); margin-bottom: calc(var(--lh) * 0.4); }
/* Thin horizontal rule between the name and the second link, sitting at x-height middle. */
.nav-rule { width: 1.5em; height: 1px; background: currentColor; opacity: 0.55; flex: none; transform: translateY(0.05em); }

/* SHARED NAV -------------------------------------------------------------
   A fixed bar whose text lines up with the index masthead on the home page,
   and moves into the top band above the film on project pages. */
.site-nav {
  position: fixed; left: 0; right: 0; z-index: 15;
  top: calc(var(--y) - (var(--band) - var(--lh)) / 2);
  height: var(--band);
  padding: 0 var(--x);
  display: flex; align-items: center; gap: calc(var(--fs) * 0.9);
  pointer-events: none;
  text-shadow: var(--shadow);
  transition: top 0.85s var(--ease), opacity 0.6s var(--ease), background-color 0.4s var(--ease), color 0.6s var(--ease), text-shadow 0.6s var(--ease);
}
.site-nav > * { white-space: nowrap; }
.site-nav a { pointer-events: auto; transition: opacity 0.25s var(--ease); }
.site-nav a:hover { opacity: 0.6; }
/* The home masthead keeps its space in the layout; the shared bar draws over it. */
.view-home .masthead { visibility: hidden; margin-bottom: calc(var(--lh) * 0.4 + 8px); } /* 8px more air above the first title */
body:has(#project:not([hidden])) .site-nav { top: var(--nav-top, 0px); }
/* In the stills the bar follows the gallery as you scroll (outranks the film-page
   position), so it moves without the glide. */
body:has(#project.in-stills:not([hidden])) .site-nav { top: var(--stills-nav-top, 0px); transition: opacity 0.6s var(--ease), background-color 0.4s var(--ease); }
body:has(.view-project.is-idle:not([hidden])) .site-nav { opacity: 0; }
/* In the stills the bar only gets a backing strip when a tall gallery has pushed it
   up to the screen edge (so photos can scroll beneath it). */
body:has(.view-project.in-stills.nav-at-edge:not([hidden])) .site-nav { background: var(--bg); text-shadow: none; }
/* "selected work" only appears once the bar has moved up onto a project. */
.nav-work, .nav-work-rule { display: none; }
/* "selected work" draws out of "information": the dash grows from its left end,
   then the words slide out from behind it. Leaving for the index reverses it. */
body:has(#project:not([hidden])) :is(.nav-work, .nav-work-rule),
body:has(#about:not([hidden])) :is(.nav-work, .nav-work-rule),
.site-nav.is-retracting :is(.nav-work, .nav-work-rule) { display: block; }
.nav-work-rule { transform-origin: left center; }
body:has(#project:not([hidden])) .nav-work-rule,
body:has(#about:not([hidden])) .nav-work-rule { animation: rule-grow 0.45s 0.3s var(--ease) both; }
body:has(#project:not([hidden])) .nav-work,
body:has(#about:not([hidden])) .nav-work { animation: nav-emerge 0.6s 0.45s var(--ease) both; }
.site-nav.is-retracting .nav-work { animation: nav-retract 0.35s var(--ease) both; }
.site-nav.is-retracting .nav-work-rule { animation: rule-shrink 0.3s 0.15s var(--ease) both; }
@keyframes rule-grow { from { transform: translateY(0.05em) scaleX(0); } }
@keyframes rule-shrink { to { transform: translateY(0.05em) scaleX(0); } }
@keyframes nav-emerge { from { opacity: 0; transform: translateX(-1.6em); } }
@keyframes nav-retract { to { opacity: 0; transform: translateX(-1.6em); } }

/* Arriving at the index from the information page: the titles step down one by
   one from "brandon yoon", each sliding in from the step above. */
.index.is-cascading li { animation: stair-in 0.55s var(--ease) both; animation-delay: calc(var(--slot, var(--i)) * 40ms + 120ms); }
@keyframes stair-in { from { opacity: 0; transform: translate(calc(-1 * var(--step)), calc(-1 * var(--lh))); } }
.nav-work { pointer-events: auto; transition: opacity 0.25s var(--ease); }
.nav-work:hover { opacity: 0.6; }
body:has(.work-menu:not([hidden]):not(.is-closing)) .site-nav { opacity: 1; }
body:has(.work-menu:not([hidden]):not(.is-closing)) .site-nav > :not(.nav-work) { opacity: 0.45; }

/* PROJECT SWITCHER ---------------------------------------------------------
   A list hanging under "selected work", with the hovered project's poster to
   its left. The current project stays faintly visible behind. */
.work-menu {
  position: fixed; inset: 0; z-index: 14;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  animation: fade-in 0.35s var(--ease);
  transition: opacity 0.3s var(--ease);
}
.work-menu[hidden] { display: none; }
.work-menu.is-closing { opacity: 0; pointer-events: none; }
.wm-list { position: absolute; left: var(--wm-left, 30vw); top: var(--wm-top, 80px); }
.wm-list li { opacity: 0; transform: translateY(6px); animation: wm-in 0.5s var(--ease) forwards; animation-delay: calc(var(--i) * 22ms + 60ms); }
@keyframes wm-in { to { opacity: 1; transform: none; } }
.wm-list a { color: var(--dim); font-weight: var(--bold); transition: color 0.2s var(--ease); white-space: nowrap; }
.wm-list a.is-current, .wm-list a:hover, .wm-list a:focus-visible { color: var(--fg); }
.wm-list:hover a.is-current:not(:hover) { color: var(--dim); }
.wm-thumb {
  position: absolute; left: var(--x); width: var(--wm-thumb-w, 220px); aspect-ratio: 3 / 2;
  top: var(--wm-thumb-top, 80px); overflow: hidden; background: rgba(128,128,128,0.15);
  transition: top 0.35s var(--ease);
}
.wm-thumb img { width: 100%; height: 100%; object-fit: cover; transition: opacity 0.25s var(--ease); }
.wm-thumb img.is-loading { opacity: 0; }

body:has(#lightbox:not([hidden]):not(.is-closing)) .site-nav { opacity: 0; pointer-events: none; transition: opacity 0.35s var(--ease); }
body:has(#lightbox:not([hidden])) .site-nav a { pointer-events: none; }

/* Information page: the shared bar is its header too (it sits where the index
   header does), so it glides down here from a project. Its own masthead only
   holds the space. */
.view-about .masthead { visibility: hidden; margin-bottom: calc(var(--lh) * 0.57); } /* a touch more air above the bio than the index has */

/* HOME ---------------------------------------------------------------- */
.index li { --a: 0; margin-left: calc(var(--step) * (var(--i) + 1.4)); white-space: nowrap; }
/* Loop mode: each title is placed on a step by app.js (via translate). */
.index.is-loop { position: relative; }
.index.is-loop li { position: absolute; left: 0; top: 0; margin-left: 0; }
.index a {
  color: color-mix(in srgb, var(--fg) calc(var(--a) * 100%), var(--dim));
  font-weight: var(--bold);
  transition: color 0.25s var(--ease);
}
.is-scroll .index a { transition: none; }
.view-home:not(.is-scroll) .index a.is-active { --a: 1; }

.home-bg { position: absolute; inset: 0; background: #000; }

/* Scroll mode: the list stays put; scrolling glides the highlight down the
   staircase and swaps the background. */
.view-home.is-scroll { touch-action: none; overscroll-behavior: none; }
.view-home.is-scroll { cursor: var(--reticle-lg); } /* the whole picture opens the highlighted project */
.home-bg-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.03);
  transition: opacity 0.9s var(--ease), transform 6s linear;
}
.home-bg-img.is-on { opacity: 1; transform: scale(1); }
.home-bg-shade { position: absolute; inset: 0; background: radial-gradient(ellipse at 25% 50%, rgba(0,0,0,0.35), rgba(0,0,0,0.1) 70%); }

/* The index looks the same in both modes: full-bleed project image with light
   text over it. */
:root[data-theme="light"] .view-home {
  --fg: #fafafa; --dim: rgba(250, 250, 250, 0.42);
  --shadow: 0 0 18px rgba(0, 0, 0, 0.35);
}
:root[data-theme="light"] body:has(#home:not([hidden])) .site-nav {
  --fg: #fafafa; color: #fafafa; text-shadow: 0 0 18px rgba(0, 0, 0, 0.35);
}


/* PROJECT ------------------------------------------------------------- */
.stage { position: absolute; inset: 0; background: var(--bg); transition: opacity 0.6s var(--ease); }
.stage-poster, .stage-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
}
.stage-video { opacity: 0; transition: opacity 0.6s var(--ease); }
.stage-video { background: var(--bg); }
.stage.is-playing .stage-video, .stage.has-started .stage-video { opacity: 1; }
/* Once the film is up, drop the poster so letterbox bars are plain black / white. */
.stage-poster { transition: opacity 0.6s var(--ease); }
.stage.has-started .stage-poster { opacity: 0; }
.stage-hit { position: absolute; inset: 0; cursor: var(--cur-play); touch-action: manipulation; /* no double-tap zoom */ }
.stage.is-playing .stage-hit { cursor: var(--cur-pause); }
/* The player block shows the same play/pause cursor; its links and timeline keep theirs. */
.controls { cursor: var(--cur-play); }
.view-project:has(.stage.is-playing) .controls { cursor: var(--cur-pause); }
.controls :is(a, button) { cursor: pointer; }
.view-project.is-idle .controls, .view-project.is-idle .controls * { cursor: none; }
.view-project.is-idle .stage-hit { cursor: none; } /* still hides with the controls while playing */


.controls { width: var(--cw); transition: opacity 0.6s var(--ease); }
.view-project.is-idle .controls { opacity: 0; }
.view-project.is-idle { cursor: none; }

.ctl-row { display: flex; align-items: baseline; gap: calc(var(--fs) * 1.8); white-space: nowrap; }
.ctl-client:empty { display: none; }
/* Arrows beside the picture for projects with more than one film (placed by
   placeFilmArrows in app.js). Same triangle as the still viewer's cursor. */
.film-arrow {
  position: absolute; z-index: 5; width: 32px; height: 48px;
  display: grid; place-items: center; color: var(--fg);
  opacity: 0.85; transition: opacity 0.4s var(--ease);
  filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.35));
}
.film-arrow[hidden] { display: none; }
.film-arrow:hover { opacity: 1; }
.film-arrow::before { content: ""; border: 5px solid transparent; }
.film-prev::before { border-right: 7.7px solid currentColor; border-left: 0; }
.film-next::before { border-left: 7.7px solid currentColor; border-right: 0; }
.view-project.is-idle .film-arrow { opacity: 0; }

/* "1 / 2" film switch, for projects with more than one film. */
.ctl-films:empty { display: none; }
.ctl-films button { opacity: 0.45; transition: opacity 0.25s var(--ease); }
.ctl-films button.is-current, .ctl-films button:hover { opacity: 1; }
.ctl-client { font-weight: 400; overflow: hidden; text-overflow: ellipsis; }
.ctl-tc { margin-left: auto; font-variant-numeric: tabular-nums; }
.ctl-title { font-weight: var(--title-weight); }

/* Mute / unmute, hanging below the timeline so it never moves anything. */
.ctl-sound { position: absolute; left: 0; top: 100%; margin-top: calc(var(--lh) * 0.4); color: var(--dim); text-shadow: var(--shadow); transition: color 0.25s var(--ease); }
.ctl-sound:hover { color: var(--fg); }
.ctl-sound[hidden] { display: none; }
.ctl-bar { position: relative; display: flex; align-items: center; height: calc(var(--lh) * 1.6); }
.bar { position: relative; flex: 1; height: 100%; cursor: pointer; touch-action: none; }
.bar-track, .bar-fill { position: absolute; left: 0; top: 50%; pointer-events: none; box-shadow: var(--shadow); }
.bar-track { right: 0; height: 1px; background: var(--line); }
.bar-fill { width: 0; height: 2px; margin-top: -0.5px; background: var(--fg); }
/* Playhead: a short vertical tick riding the end of the fill. */
.bar-head { position: absolute; left: 0; top: 50%; width: 1.5px; height: calc(var(--fs) * 0.85); transform: translate(-50%, -50%); background: var(--fg); box-shadow: var(--shadow); pointer-events: none; }

.theme-toggle span { opacity: 0.45; transition: opacity 0.25s var(--ease); }
:root[data-theme="dark"] .theme-toggle [data-t="dark"],
:root[data-theme="light"] .theme-toggle [data-t="light"] { opacity: 1; }

/* The page scrolls: film fills the first screen, stills sit underneath. */
.view-project { overflow-y: auto; overscroll-behavior: none; scrollbar-width: none; } /* film <-> gallery paging is handled in app.js */
.view-project::-webkit-scrollbar { display: none; }
/* The film stays pinned while the stills sheet slides up over it.
   --cover (0 → 1) is set from scroll position and recedes the film underneath. */
.hero { position: sticky; top: 0; z-index: 0; height: 100vh; height: 100svh; --cover: 0; }
.hero .stage { top: var(--band); bottom: var(--band); }
@media (min-width: 761px) {
  /* Wide windows: the film sits centred at --film-scale of the available space. */
  .hero:not(.is-stacked):not(:fullscreen) :is(.stage-poster, .stage-video) {
    left: calc((1 - var(--film-scale)) * 50%); top: calc((1 - var(--film-scale)) * 50%);
    width: calc(var(--film-scale) * 100%); height: calc(var(--film-scale) * 100%);
  }
  /* The player keeps the page's left margin (in line with the name above and
     "dark / light" below) and scales its height toward the centre with the film.
     To have it follow the film inward instead, use:
     left: calc(50% + (var(--x) - 50%) * var(--film-scale)); */
  .hero:not(.is-stacked):not(:fullscreen) .controls {
    left: var(--x);
    top: calc(50% + (var(--y) - 50%) * var(--film-scale));
    width: max(24vw, 340px);
  }
}
.hero .stage { transform: scale(calc(1 - var(--cover) * 0.05)); transform-origin: 50% 30%; }
.hero-shade { position: absolute; inset: 0; z-index: 6; pointer-events: none; background: var(--bg); opacity: calc(var(--cover) * 0.7); }

.sheet {
  position: relative; z-index: 2; background: var(--bg);
  min-height: 100vh; min-height: 100svh;
  padding: calc(var(--band) + 4vh) 0;
  /* Centre the gallery in the screen; taller galleries just start at the top. */
  display: flex; flex-direction: column; justify-content: safe center;
}
/* Gallery sits in a centred column about 40% of the page wide. */
.sheet-head, .stills { width: min(100% - 2 * var(--x), max(39vw, 480px)); margin-inline: auto; }
.sheet-head { display: flex; gap: calc(var(--fs) * 1.8); margin-bottom: calc(var(--lh) * 1.6); }
.sheet:has(.stills:empty) { display: none; }
/* Title/director already sit with the film controls just above, so the gallery skips them.
   Delete this line to bring the gallery heading back. */
.sheet-head { display: none; }

.view-project.is-idle .hero-theme, .view-project.is-idle .hero-fs { opacity: 0; }

/* Stacked layout (set by layoutStage for tall windows): controls sit under the film. */
.hero.is-stacked .controls { text-shadow: none; }

.hero-fs {
  position: absolute; right: var(--x); bottom: calc((var(--band) - var(--lh)) / 2); z-index: 5;
  text-shadow: var(--shadow); transition: opacity 0.6s var(--ease);
}
.hero-fs:hover { opacity: 0.6; }
.hero-fs[hidden] { display: none; }

/* Fullscreen: the hero (film + our controls) fills the screen on its own. */
.hero:fullscreen { background: var(--bg); height: 100%; }
.hero:fullscreen .stage { top: var(--band); bottom: var(--band); left: 0; right: 0; transform: none; }
.hero:fullscreen .hero-shade, .hero:fullscreen .hero-theme { display: none; }
.hero:fullscreen .hero-fs { opacity: 1; }
.view-project.is-idle .hero:fullscreen .hero-fs { opacity: 0; }
.view-project.is-idle .hero:fullscreen { cursor: none; }

.hero-theme { position: absolute; left: var(--x); bottom: calc((var(--band) - var(--lh)) / 2); z-index: 5; text-shadow: var(--shadow); transition: opacity 0.6s var(--ease); }

/* Shared bottom row (desktop). It sits above both the film and the stills, so it
   doesn't move as the gallery glides up: "stills ▾" cross-fades to "video ▴" (the
   arrow flips), "fullscreen" fades out, "dark / light" stays exactly as it is.
   Its position (--row-top) is set by app.js. */
.film-row { display: none; }
@media (min-width: 761px) {
  .film-row {
    display: block; position: fixed; left: 0; right: 0; z-index: 8; height: var(--lh);
    top: var(--row-top, calc(100% - var(--band) + (var(--band) - var(--lh)) / 2));
    pointer-events: none; text-shadow: var(--shadow); transition: opacity 0.6s var(--ease);
  }
  .film-row > * { position: absolute; top: 0; pointer-events: auto; }
  .row-theme { left: var(--x); }
  .row-cue { left: 50%; transform: translateX(-50%); display: inline-grid; align-items: center; color: var(--dim); transition: color 0.25s var(--ease); }
  .row-cue .cue-label { grid-area: 1 / 1; transition: opacity 0.4s var(--ease); }
  .row-cue .cue-video { opacity: 0; }
  .view-project.in-stills .row-cue .cue-stills { opacity: 0; }
  .view-project.in-stills .row-cue .cue-video { opacity: 1; }
  .row-cue::after {
    content: ""; grid-area: 1 / 2; margin-left: 0.6em; transform-origin: 50% 50%;
    border: 0.26em solid transparent; border-top: 0.36em solid currentColor; border-bottom: 0;
    transition: transform 0.45s var(--ease);
  }
  .view-project.in-stills .row-cue::after { transform: rotate(180deg); }
  .row-cue:hover { color: var(--fg); }
  .row-fs { right: var(--x); transition: opacity 0.4s var(--ease); }
  .row-fs:hover { opacity: 0.6; }
  .view-project.in-stills .row-fs { opacity: 0; pointer-events: none; }
  .film-row [hidden] { display: none; }
  .view-project.is-idle .film-row { opacity: 0; }
  .view-project.in-stills .film-row { text-shadow: none; }
  /* The film screen's own copies are only used in fullscreen. */
  .hero:not(:fullscreen) :is(.hero-theme, .hero-fs) { display: none; }
}

.stills {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.45vw;
}
.stills:empty { display: none; }
.stills img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; cursor: var(--reticle); background: rgba(128,128,128,0.15); transition: opacity 0.25s var(--ease); }
.stills img {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 0.9s var(--ease), transform 1.1s var(--ease);
  transition-delay: calc(var(--col, 0) * 90ms);
}
.stills img.is-in { opacity: 1; transform: none; }
.stills img.is-in:hover { opacity: 0.8; transition-delay: 0s; }

.lightbox {
  position: fixed; inset: 0; z-index: 20; background: var(--bg);
  display: grid; place-items: center;
  touch-action: none; user-select: none; -webkit-user-select: none;
  transition: opacity 0.45s var(--ease);
}
/* Simple fade in and out. */
.lightbox.is-opening { opacity: 0; transition: none; }
.lightbox.is-closing { opacity: 0; transition: opacity 0.5s var(--ease) 0.08s; pointer-events: none; }
/* Labels: arrive just after the photo, leave just before it. */
.lightbox.is-opening :is(.lightbox-count, .lb-close) { opacity: 0; transition: none; }
.lightbox:not(.is-opening):not(.is-closing) :is(.lightbox-count, .lb-close) { opacity: 1; transition: opacity 0.4s var(--ease) 0.25s, top 0.35s var(--ease), left 0.35s var(--ease), color 0.25s var(--ease); }
.lightbox.is-closing :is(.lightbox-count, .lb-close) { opacity: 0; transition: opacity 0.2s var(--ease); }
.lightbox[hidden] { display: none; }
/* A three-photo filmstrip (previous, current, next) one screen-width apart.
   Dragging moves the strip; letting go eases it to the nearest photo. */
.lb-track { position: absolute; inset: 0; will-change: transform; }
.lb-track.is-settling { transition: transform 0.42s cubic-bezier(0.22, 0.61, 0.36, 1); }
.lightbox .lb-slide {
  position: absolute; left: 50%; top: 50%;
  transform: translate(calc(-50% + var(--slot, 0) * 100vw), -50%);
  /* 70% of the space available, centred. Sized from the photo's shape (--ratio,
     set in app.js) rather than its pixels, so the quick small preview and the
     full-size photo fill exactly the same space. */
  --lb-max-w: 59vw; --lb-max-h: calc((100dvh - 2 * var(--band) - 4vh) * 0.7);
  width: min(var(--lb-max-w), var(--lb-max-h) * var(--ratio, 1.5)); height: auto;
  aspect-ratio: var(--ratio, 1.5); object-fit: contain;
  -webkit-user-drag: none; user-select: none;
}
.lightbox .lb-slide[hidden] { display: none; }
.lightbox .lb-slide.is-loading { visibility: hidden; } /* never show a stale image while the new one loads */
/* Counter (left) and close (right) sit just under the photo, lined up with its
   edges; app.js places them (placeLbLabels) as each photo's size changes. */
.lightbox-count, .lb-close { position: absolute; top: var(--lb-row-top, auto); bottom: auto; transition: top 0.35s var(--ease), left 0.35s var(--ease), color 0.25s var(--ease); }
.lightbox-count { left: var(--lb-left, var(--x)); font-variant-numeric: tabular-nums; }
.lb-close { left: var(--lb-close-left, auto); right: auto; color: var(--dim); }
.lb-close:hover { color: var(--fg); }

/* Navigation: clicking the left or right half of the photo steps back or forward;
   clicking outside the photo closes. With a mouse the cursor becomes the arrow
   (see .lb-cursor); touch screens get small edge arrows alongside swiping. */
.lb-nav { position: absolute; top: 0; bottom: var(--band); width: max(6vw, 48px); display: grid; align-items: center; color: var(--fg); }
.lb-prev { left: 0; justify-items: start; padding-left: 10px; }
.lb-next { right: 0; justify-items: end; padding-right: 10px; }
.lb-nav::before { content: ""; border: 0.4em solid transparent; opacity: 0.5; }
.lb-prev::before { border-right: 0.55em solid currentColor; border-left: 0; }
.lb-next::before { border-left: 0.55em solid currentColor; border-right: 0; }
.lb-cursor { display: none; }

@media (hover: hover) and (pointer: fine) {
  .lb-nav { display: none; }
  .lightbox.cursor-on { cursor: none; }
  /* A triangle that follows the pointer; difference blending keeps it visible
     over both bright and dark parts of a still. */
  .lb-cursor {
    display: block; position: fixed; left: 0; top: 0; z-index: 3;
    pointer-events: none; color: #fff; mix-blend-mode: difference;
    opacity: 0; transition: opacity 0.2s var(--ease);
  }
  /* A single arrow centred on the pointer: ◀ over the left half, ▶ over the right. */
  .lb-cursor::before, .lb-cursor::after {
    content: ""; position: absolute; top: 0; left: 0;
    border: 5.85px solid transparent;
    transition: opacity 0.2s var(--ease);
  }
  .lb-cursor::before { transform: translate(-50%, -50%); border-right: 9px solid currentColor; border-left: 0; }
  .lb-cursor::after { transform: translate(-50%, -50%); border-left: 9px solid currentColor; border-right: 0; }
  .lb-cursor.is-next::before, .lb-cursor.is-prev::after { display: none; }
  .lightbox.cursor-on .lb-cursor { opacity: 1; }
}

/* ABOUT ---------------------------------------------------------------
   Proportions from design.pdf page 4, expressed in text-size units so the
   page keeps its shape at any window size. */
.view-about { overflow-y: auto; }
.view-about .block { position: relative; left: auto; top: auto; padding: var(--y) var(--x) 14vh; }
.bio {
  font-size: calc(var(--fs) * 1.45);
  line-height: 1.15;
  font-weight: 400;
  max-width: calc(var(--fs) * 37);
  margin-bottom: calc(var(--fs) * 2.2);
}
.about-grid {
  margin-left: calc(var(--fs) * 11.75);
  display: grid;
  grid-template-columns: calc(var(--fs) * 13.5) calc(var(--fs) * 16); /* wide enough for a press title and its year on one line */
  row-gap: calc(var(--fs) * 2.4);
  line-height: 1.2;
}
.about-clients { grid-column: 1 / -1; max-width: calc(var(--fs) * 27.5); }
.about-grid h2 { margin-bottom: calc(var(--fs) * 1.1); }
.about-grid p, .about-grid li { font-weight: 400; }
.about-grid li span + span { margin-left: 0.4em; } /* year right after its title */
.contact { margin-top: calc(var(--fs) * 1.3); }
.contact a, #rep a { border-bottom: 1px solid transparent; transition: border-color 0.25s var(--ease); }
.contact a:hover, #rep a:hover { border-color: currentColor; }
.about-toggle { position: fixed; left: var(--x); bottom: calc((var(--band) - var(--lh)) / 2); z-index: 5; }

/* Hover affordance for links across views */
.masthead a, .ctl-title { transition: opacity 0.25s var(--ease); }
.masthead a:hover, .ctl-title:hover { opacity: 0.6; }

/* SMALL SCREENS ------------------------------------------------------- */
@media (max-width: 760px) {
  :root { --x: 16px; --y: 18vh; --step: 10px; --fs: 10.5px; /* 30% smaller than desktop */ }
  body { overflow: auto; }

  .index li { margin-left: calc(var(--step) * (var(--i) + 1)); }


  .hero { height: auto; position: relative; } /* phones: the film scrolls with the page instead of staying pinned under the stills */
  .hero .stage { inset: auto; }
  .wm-thumb { display: none; }
  .site-nav { gap: 7px; }
  .site-nav .nav-rule { width: 12px; }
  .wm-list { left: var(--x); }
  .stage { position: relative; inset: auto; width: 100%; aspect-ratio: 16 / 9; margin-top: 64px; touch-action: pan-y; } /* sideways swipes switch films */
  .controls { position: relative; left: auto; top: auto; width: auto; margin: 28px 16px 0; }
  .ctl-sound { position: static; display: block; margin-top: 6px; } /* phones: its own line */
  /* Phones have no hover, so nothing on the film page fades away while it plays. */
  .view-project.is-idle .controls,
  .view-project.is-idle :is(.hero-theme, .hero-fs, .film-arrow) { opacity: 1; }
  body:has(.view-project.is-idle:not([hidden])) .site-nav { opacity: 1; }
  .ctl-row { gap: 14px; }
  .view-project { overflow-x: clip; }
  .stills { gap: 4px; }
  .sheet-head, .stills { width: auto; margin-inline: 16px; }
  .lightbox .lb-slide { --lb-max-w: calc(100vw - 72px); }
  .lb-nav { width: 36px; }
  .hero-theme { position: static; display: inline-block; margin: 20px 16px 0; }
  .hero-fs { position: static; float: right; margin: 20px 16px 0; }
  .sheet { min-height: 0; padding: 32px 0 48px; display: block; }
  .sheet-head { gap: 14px; }
  .home-bg-shade { background: rgba(0, 0, 0, 0.35); }

  .view-about { overflow-y: auto; }
  .view-about .block { padding: 18vh 16px 64px; }
  .bio { max-width: none; font-size: 12px; }
  .about-grid { margin-left: 0; grid-template-columns: 1fr; }
  .about-clients { max-width: none; }
  .about-toggle { position: static; margin-top: 40px; }
}

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