/* byrxnsford°, media that asks you to stay a while
   dark photo-wall build, aerra-direction, switzer light + plex mono */

:root {
  --build: "202608241454";
  --bg: #0E0E0D;
  --bg-2: #171715;
  --paper: #F2EEE2;
  --muted: rgba(242, 238, 226, .58);
  --faint: rgba(242, 238, 226, .34);
  --line: rgba(242, 238, 226, .13);
  --red: #E8442E;
  --glass: rgba(20, 20, 19, .62);
  /* where the tab row pins, and so how tall the strip above it is */
  --pin: 48px;
  /* hairline gutter between tiles, matching the reference: the wall runs
     flush to the viewport edges and the tiles almost touch */
  --pad: 6px;
  /* one inset for every piece of chrome, so the fixed nav, the hero line,
     the tab bar and its note all hang off the same edge. The pictures keep
     their own hairline gutter and bleed past it, which is the point. */
  --edge: clamp(16px, 3vw, 40px);
  --grot: 'Switzer', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --mono: 'IBM Plex Mono', 'SF Mono', Menlo, monospace;
  /* geometric sans for the gallery overlay: circular bowls, monoline strokes,
     flat terminals, in the manner of the reference wordmark */
  --geo: 'Outfit', 'Futura', 'Century Gothic', var(--grot);
}

* { box-sizing: border-box; }
html, body {
  height: 100%;
  /* the document itself no longer scrolls: .page does */
  overflow: hidden;
  max-width: 100%;
}
html {
  /* the canvas takes this, which is what fills anything outside the page box */
  background: var(--bg);
}

/* The scroller. Being an ordinary element rather than the document, Safari
   does not extend it under the status bar and does not shorten the viewport
   underneath it, so the chrome covers everything it is meant to. */
.page {
  position: fixed;
  inset: 0;
  overflow-y: auto;
  /* nothing here scrolls sideways, and a stray pixel used to make the whole
     page pannable, which dragged the sticky chrome away from the fixed chrome */
  overflow-x: clip;
  -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
  /* Switching tabs resizes the page, and the browser's scroll anchoring reacts
     by dragging the scroll position to keep content still. That fought every
     deliberate scroll on this site, including the mark's jump home. */
  overflow-anchor: none;
  background: var(--bg);
}
/* the viewer takes over the screen, so the page behind it holds still */
body.lb-open .page { overflow: hidden; }
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--paper);
  font-family: var(--grot);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

::selection { background: var(--red); color: var(--paper); }

.mono {
  font-family: var(--mono);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .14em;
}
.deg { font-size: .5em; vertical-align: .5em; letter-spacing: 0; }
.deg-red { color: var(--red); }
.dim { color: var(--muted); }

/* Read but not seen: headings for the reader the layout already implies. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* The first tab stop on the page. Parked above the screen until it is
   focused, then it drops in as a small plain label. */
.skip {
  position: fixed;
  top: -48px;
  left: var(--edge);
  z-index: 999;
  padding: 12px 16px;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--paper);
  text-decoration: none;
  font-size: 10.5px;
  transition: top .15s ease;
}
.skip:focus-visible { top: 12px; }

a { color: inherit; }
:focus-visible { outline: 1.5px solid var(--red); outline-offset: 3px; }
/* The ring belongs to the keyboard. Safari counts a tap on a button as
   focus-visible, so pressing a tab left a red rectangle drawn around it that
   stayed there afterwards. Switched on by the first Tab press and off again by
   any pointer: anyone finding their way round by key keeps it, and a finger
   never sees it. */
body:not(.by-key) :focus-visible { outline: none; }
img, video { max-width: 100%; display: block; }


/* plain text, no chrome around it: the only affordance is the colour shift */
.chrome-nav {
  position: fixed;
  top: calc(20px + env(safe-area-inset-top, 0px));
  right: var(--edge);
  z-index: 400;
  display: flex;
  gap: clamp(18px, 2.4vw, 34px);
  /* Gone the moment the page moves. This is the hero's own chrome, and the
     hero is over as soon as you leave it: there is no reason for its line
     and its three words to ride down the screen with you. */
  transition: opacity .3s ease;
}
body.scrolled .chrome-nav { opacity: 0; pointer-events: none; }
/* Past the hero the tab row says the same three words in the same order, so
   this stands down and the row takes over. Two identical menus, one above the
   other, is not a menu, it is a stutter. */
body.past-hero .chrome-nav { opacity: 0; pointer-events: none; }

.chrome-nav a {
  font-family: var(--mono);
  /* matched to the tab row it hands over to, size and weight */
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--paper);
  text-decoration: none;
  transition: color .25s ease;
}
@media (hover: hover) {
  .chrome-nav a:hover,
  .chrome-nav a:focus-visible { color: var(--red); }
}


/* The header is one piece: this is the ground the fixed nav sits on, and it
   arrives at the exact moment the tab row pins beneath it, so the two read as
   a single bar rather than a strip that appears and is later joined. */
/* On .page, not on body. The scroller is position: fixed, which makes it a
   stacking context, so a band living on body sat above everything inside it
   including the tab row: the row was being blurred as backdrop rather than
   sitting on the glass. Inside the same context it can be layered under the
   row properly. The mark stays outside .page and so rides above both. */
.page::before {
  content: '';
  position: fixed;
  top: 0; left: 0; right: 0;
  /* Covers the mark and the tab row as ONE surface. Two adjacent frosted
     panels each blur their own patch of backdrop and leave a seam where they
     meet, so the row below turns its own background off and lets this carry
     both. The height comes from the row's real bottom edge. */
  height: var(--header-h, 92px);
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  /* No rule along the bottom. The blur is edge enough: a hairline drew a hard
     border across the picture behind it and the bar read as a separate panel
     sat on top of the page rather than part of it. */
  /* under the row, over everything the row scrolls past */
  z-index: 4;
  /* Nothing on the hero: the picture runs clean to the top edge there. The
     glass belongs to the bar, and the bar arrives with the tab row. */
  opacity: 0;
  pointer-events: none;
  transition: opacity .18s linear;
}
body.past-hero .page::before { opacity: 1; }
/* pinned: the strip is the glass, so the row adds none of its own */
body.past-hero .tabs {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* the gallery index is full bleed, so the header rides on the picture */
/* The gallery is its own page: no hero above it to scroll back into, so
   there is nothing to slide the gallery away to. */
body.tab-gallery .hero,
body.tab-about .hero { display: none; }
body.tab-gallery .work-section,
body.tab-about .work-section { padding-top: calc(var(--pin) + env(safe-area-inset-top, 0px)); }
/* about is a page rather than a strip at the bottom of one, so the panel
   carries the inset the slab used to */
.about-panel {
  padding: clamp(24px, 3.5vh, 44px) var(--edge) clamp(64px, 9vh, 110px);
}
/* The copy does not fill a screen, so a work section holding its own 100svh
   left the footer hanging 44px below the fold: one screen of reading and a
   pointless scroll to see the line under it. The page becomes the column
   instead, and the footer takes the bottom of it. */
body.tab-about .page {
  display: flex;
  flex-direction: column;
  min-height: 100%;
}
body.tab-about .work-section { flex: 1 0 auto; min-height: 0; }
/* on the index the row floats free of the flow, so the picture starts at the
   very top of the page and the page is exactly one screen: nothing to scroll */
body.gal-cover .work-section { padding-top: 0; min-height: 0; }
body.gal-cover .tabs {
  position: fixed;
  top: calc(var(--pin) + env(safe-area-inset-top, 0px));
  left: 0; right: 0;
}

body.gal-cover .page::before { opacity: 0; }
body.gal-cover .tabs { background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; }

/* 34% is fine on flat black; over a photograph it disappears */
body.gal-cover .tab-count { color: rgba(242, 238, 226, .70); }

/* the phone copy of the tagline, in the hero's own flow above the wordmark */
.hero-thesis-inline { display: none; }

/* The scroll cue. Nothing on a desktop, where a mouse has a wheel and the
   page is plainly a page; a phone gets the hero full bleed with no menu on
   it, so it is worth saying quietly that there is more underneath. */
.hero-scroll { display: none; }
/* Upward, because that is the way the thumb goes. The page travels down, but
   nobody pushes a page down: they push the screen up and the page follows. A
   cue that runs the way the content moves is telling you about the page; this
   one is telling you what to do with your hand. */
@keyframes hero-scroll {
  0%        { transform: translateY(262%); }
  55%, 100% { transform: translateY(-100%); }
}

/* thesis sits top-left, balancing the nav on the right */
.hero-thesis {
  position: fixed;
  top: 20px;
  left: var(--edge);
  z-index: 400;
  margin: 0;
  font-size: 12px;
  /* Set to match the three words in the opposite corner, which is the only
     other thing on this line: same size, same capitals, same tracking, and a
     step heavier. They were already the same size and did not look it, because
     capitals read larger than lower case at the same measurement, so matching
     the number alone was never going to match the eye. The phone keeps the
     sentence, further down: there is nothing in its top right to agree with. */
  text-transform: uppercase;
  letter-spacing: .16em;
  font-weight: 500;
  color: var(--paper);
  /* no transition on the fade: it is drawn by the scroll itself, and a
     duration here would only make it lag behind the finger */
}
/* the hero's line belongs to the hero: it clears out with it, which is what
   frees the top-left for the tab bar */
/* Driven by the scroll, not by a class arriving: --swap runs 1 to 0 as the tab
   row closes on the line these two share, so the words go and the mark comes
   in step with the finger rather than in a fixed length of time after it. The
   transition is only for when it jumps, as it does when a tab is switched. */
.hero-thesis { transition: opacity .3s ease; }
body.scrolled .hero-thesis { opacity: 0; pointer-events: none; }
body.past-hero .hero-thesis { opacity: 0; pointer-events: none; }

/* the mark takes that corner once the line has gone, so there is always a way
   back to the top without hunting for one */
.chrome-mark {
  position: fixed;
  /* measured against the nav's baseline at this size: text on one line should
     sit on one line, whatever the size difference */
  top: 12.5px;
  left: var(--edge);
  /* and the b's bearing pulled back so its ink sits on the column edge */
  margin-left: -0.047em;
  z-index: 400;
  font-size: 21px;
  font-weight: 300;
  letter-spacing: -0.022em;
  line-height: 1;
  color: var(--paper);
  text-decoration: none;
  opacity: 0;
  pointer-events: none;
  /* opacity is the scroll's to draw; the colour shift is still its own */
  transition: color .25s ease;
}
.chrome-mark .deg {
  /* proportionally larger than on the big wordmark: at .34em of 15px the ring
     came out under 2px wide and simply vanished */
  font-size: .46em;
  vertical-align: 1.02em;
  margin-left: -.02em;
  color: var(--red);
  letter-spacing: 0;
}
/* Handed over rather than dissolved into one another. Both live in this same
   corner, so fading them across each other left the name printed through the
   middle of the line for the length of the swap. The line is gone by halfway
   and the mark comes up out of the other half: one leaves, then one arrives. */
/* Off until the tab row has landed on its line, then up. Nothing scroll-
   driven here any more: the mark belongs to the bar, and the bar is either
   there or it is not. */
.chrome-mark { opacity: 0; transition: opacity .3s ease, color .25s ease; }
body.past-hero .chrome-mark { opacity: 1; pointer-events: auto; }
@media (hover: hover) {
  .chrome-mark:hover, .chrome-mark:focus-visible { color: var(--red); }
}

/* ============ tabbed work ============ */
/* The hero is pinned, so whatever follows it has to be at least a screen tall
   or it can never cover the hero completely: scrolled to the bottom of a short
   gallery the page simply runs out of travel and the hero shows through at the
   top. This guarantees the travel exists. */
.work-section {
  padding-top: clamp(12px, 1.8vh, 22px);
  min-height: 100vh;
  min-height: 100svh;
}
.tabs {
  /* A solid shadow thrown upward, no blur: it paints as part of this row's own
     box rather than as a separate fixed layer, so it cannot be left behind the
     way a fixed band can while iOS is mid-scroll. Third route to the same
     black, and the cheapest of the three. */
  /* pinned under the chrome band, so it stops sliding beneath the nav and
     becomes the work section's own heading instead */
  position: sticky;
  top: calc(var(--pin) + env(safe-area-inset-top, 0px));
  z-index: 5;
  display: flex;
  align-items: baseline;
  gap: clamp(18px, 2.6vw, 38px);
  flex-wrap: wrap;
  /* Frosted in its own right on the way down, where nothing else covers it.
     Once it pins, the strip above takes over and this stands down: two panes
     of glass stacked one on the other each blur their own patch of backdrop,
     and the join between them reads as a step. */
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  /* no hairline here either, for the same reason as the strip above */
  padding: 6px var(--edge) clamp(10px, 1.5vh, 15px);
}
.tab {
  /* tells the browser this is a plain tap target: no double-tap-to-zoom to
     wait for, so the tap is acted on immediately */
  touch-action: manipulation;
  font-family: var(--mono);
  /* up a step and a weight heavier: at 11px regular the three words were
     the smallest thing on a screen they are meant to steer */
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .16em;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  /* 34% was legible on flat black and vanished over the gallery covers. One
     value for every view: a brightness that changed with the backdrop read
     as a fault rather than a state. The selected tab still separates at full
     paper, plus its red degree sign. */
  color: rgba(242, 238, 226, .70);
  transition: color .25s ease;
}
@media (hover: hover) {
  .tab:hover { color: var(--red); }
}
.tab.is-on { color: var(--paper); }
/* No space in the content. The row is letter-spaced, so a literal space here
   was carrying that spacing on both sides of itself and the degree ended up
   floating a good ten pixels clear of the word instead of belonging to it.
   The label's own trailing letter-space is gap enough. The wordmark and the
   menu links have always set it this way. */
.tab.is-on::after { content: '°'; color: var(--red); letter-spacing: 0; }
/* The line goes on the left, directly under the wordmark, and the three tabs
   go to the right of the row. It reads better that way round: the name and
   what the work is are one thought and belong in one column, and the way
   round the site is the other side of the row rather than sat between them.
   Ordered rather than reordered in the markup, because the markup order is
   the reading order and the tabs still come first for anyone on a keyboard. */
.tab-count {
  order: -1;
  margin-right: auto;
  font-size: 10.5px;
  font-weight: 500;
  color: var(--muted);
  /* No bearing correction here, and that is deliberate rather than an
     oversight. Mono centres each letter in a fixed advance, so the C sits
     0.67px inside its box; the wordmark's b, pulled back by its own bearing,
     lands 0.63px past the column edge. Measured, the two inks line up within
     four hundredths of a pixel on their own. Nudging either one would be the
     thing that knocked them apart. */
}
/* The last tab carries a letter-space after its final letter, the way every
   tracked line does, so its ink stopped two pixels inside the column while
   the wordmark's sat exactly on it. Pulled out by that much so the right hand
   edge is as true as the left. */
.tabs .tab:last-of-type { margin-right: -0.185em; }

.tab-panel[hidden] { display: none; }
/* The panel settles in rather than snapping when you switch. The movement is
   on the CONTENT, not the panel: a transform on the panel would make it the
   containing block for anything fixed inside it, which broke the gallery's
   full-bleed backdrop. */
.tab-panel { animation: panel-fade .42s ease both; }
/* Not the gallery index. Its backdrop is a film that is already moving, and
   fading the whole panel up from black put a beat of black in front of it
   on every press: the list still rises, the picture is simply there. */
.tab-panel[data-panel="gallery"] { animation: none; }
@keyframes panel-fade { from { opacity: 0; } to { opacity: 1; } }
.tab-panel > .wall,
.tab-panel .gal-list,
.tab-panel .gal-wall { animation: panel-rise .42s cubic-bezier(.2,.7,.2,1) both; }
@keyframes panel-rise { from { transform: translateY(8px); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .tab-panel, .tab-panel > .wall, .tab-panel .gal-list, .tab-panel .gal-wall { animation: none; }
}


/* gallery detail: rows of equal height, each flush to both edges, tiles
   sized by their own ratio so shape survives */
.gal-wall { padding: 0 var(--pad) var(--pad); }
.gal-row { display: flex; gap: var(--pad); margin-bottom: var(--pad); }
.gal-row:last-child { margin-bottom: 0; }
.gal-row .tile {
  flex: 0 0 auto;
  height: 100%;
  aspect-ratio: auto;   /* the wall's 4/5 box does not apply here */
  margin: 0;
}
.gal-row .tile img, .gal-row .tile video { width: 100%; height: 100%; object-fit: cover; }

/* signature under the about copy */
/* Was scoped to .slab h2 to out-rank the big section headings. About is its
   own panel now and no longer inherits those, so it stands on its own. */
h2.about-name {
  /* same optical correction as the address: heavier, larger glyphs carry more
     side bearing than the body text they have to line up with */
  margin: 0 0 0 -0.06em;
  font-size: clamp(1.45rem, 2.1vw, 1.95rem);
  font-weight: 500;
  letter-spacing: -0.022em;
  line-height: 1.14;
}
.about-role {
  margin: 7px 0 0 -0.03em;
  font-size: clamp(1rem, 1.15vw, 1.1rem);
  font-weight: 300;
  letter-spacing: -0.008em;
  color: rgba(242, 238, 226, .68);
}
.about-text .slab-body { margin-top: clamp(24px, 3.4vh, 36px); }
/* The opening line is a statement, not supporting copy, so it carries more
   size and more of the paper. Anything after it sets as normal body. */
.about-text .slab-body p:first-child {
  max-width: 26em;
  font-size: clamp(1.12rem, 1.45vw, 1.4rem);
  line-height: 1.55;
  letter-spacing: -0.008em;
  color: rgba(242, 238, 226, .94);
}
/* the sign-off: what he does, set apart from the writing above it */
.about-sign {
  /* italics lean, so they need a touch more pull than the upright text */
  margin: clamp(24px, 3.6vh, 36px) 0 0 -0.12em;
  font-size: clamp(.94rem, 1.08vw, 1.02rem);
  font-style: italic;
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: -0.004em;
  color: rgba(242, 238, 226, .6);
}


/* ============ galleries ============ */
.gal-detail-head {
  display: flex;
  align-items: baseline;
  gap: clamp(14px, 2vw, 28px);
  flex-wrap: wrap;
  padding: 0;
}
/* Inside the bar rather than under it. As its own frosted strip it made the
   gallery read as two stacked bars with a line between them, which is a tier,
   not a header. Sitting in the same surface it is simply the bar's second
   line, and there is one bar on every view. */
/* inside a set the tagline stands aside for the breadcrumb */
.tabs:has(> .gal-detail-head) .tab-count { display: none; }
.tabs > .gal-detail-head {
  /* on the row's own line, out to the right where the tagline sits, so a set
     does not make the bar twice as tall */
  margin-left: auto;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: 0;
  position: static;
}
.gal-back {
  font-family: var(--mono);
  /* the same line as the tabs, so the same size and weight as the tabs */
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .16em;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--muted);
  transition: color .25s ease;
}
@media (hover: hover) {
  .gal-back:hover { color: var(--red); }
}
.gal-detail-title {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--paper);
}


/* ============ gallery index ============
   A full-bleed backdrop with the sets listed over it: pointing at a name
   brings that set's picture up behind the list. The opened gallery is
   untouched; this is only the way in. */
.gal-index {
  position: relative;
  /* Exactly one screen tall, so once it reaches the top of the viewport it
     fills it. Bounded rather than fixed: a fixed backdrop hangs over the hero
     on the way down, which is the tearing you get mid-scroll. */
  height: 100vh;
  height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
}
.gal-stage {
  position: absolute;
  inset: 0;
  /* Its own stacking layer. The backdrops raise their z-index as they take
     turns, and without this they climb straight past the names and cover
     them, which is the text vanishing. Contained here, they can never rise
     above the list no matter how many times you sweep across it. */
  z-index: 0;
  overflow: hidden;
  background: var(--bg-2);
}
.gal-bg {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  /* a touch oversize on the way in, settling back as it arrives: the picture
     eases into place rather than being switched on */
  transform: scale(1.025);
  /* Eased in and out at both ends rather than driven hard from the start:
     .32s on a fast curve arrived abruptly, .85s left the old frame hanging
     about. This sits between the two and softens the entry and the landing. */
  transition: opacity .5s cubic-bezier(.37, 0, .24, 1),
              transform 1.1s cubic-bezier(.22, .61, .24, 1);
  will-change: opacity, transform;
  /* Two tiers, and that is the whole mechanism: the live one rises above the
     rest and they cross over. Nothing counts, holds or expires. Every desync
     this had came from trying to drive it by hand instead. */
  z-index: 1;
}
.gal-bg.is-on { opacity: 1; z-index: 2; transform: scale(1); }
/* A set that opens on a film plays it here, in the same slot and on the same
   cross-over as the pictures. It sits one layer above the still it was cut
   from, so the two are the same frame until it begins to move: there is no
   change of image, only a picture that turns out to be alive. */
.gal-film {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
  z-index: 2;
  transition: opacity .5s cubic-bezier(.37, 0, .24, 1);
}
.gal-film.is-on { opacity: 1; z-index: 3; }
/* the moment the index opens: whatever is live is shown outright, no
   crossfade from the still to the film, which is the jump you could see
   where the two did not line up to the pixel */
.gal-stage.no-fade .gal-bg { transition: none; }
.gal-stage.no-fade .gal-film { transition: opacity .12s linear; }
/* the names have to hold against whatever is behind them */

/* The scrim lives OUTSIDE the backdrop stage. Inside it, the cross-dissolve
   raises each incoming picture's z-index in turn, and they climbed straight
   over a scrim that sat in the same layer, leaving the header and the names
   on bare photograph. Out here it is always between the pictures and the
   type, whatever the backdrops do among themselves. */
.gal-index::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(90deg,
      rgba(14,14,13,.80) 0%, rgba(14,14,13,.64) 26%, rgba(14,14,13,.34) 50%,
      rgba(14,14,13,.05) 74%, rgba(14,14,13,0) 100%),
    /* and a band across the top so the header always has ground under it.
       Tuned against the brightest cover (league, open sky top-right, exactly
       where the nav sits): worst case 3.78:1, and gone by 210px so it reads
       as a vignette rather than the black bar. */
    linear-gradient(180deg, rgba(14,14,13,.86) 0px, rgba(14,14,13,.52) 100px, rgba(14,14,13,0) 210px);
}

.gal-list {
  position: relative;
  z-index: 2;
  list-style: none;
  margin: 0;
  padding: clamp(28px, 5vh, 56px) var(--edge);
  width: 100%;
}
.gal {
  touch-action: manipulation;
  display: flex;
  align-items: baseline;
  gap: clamp(12px, 1.4vw, 20px);
  width: 100%;
  padding: clamp(4px, .7vh, 10px) 0;
  background: none;
  border: none;
  text-align: left;
  font: inherit;
  color: var(--paper);
  cursor: pointer;
}
.gal-thumb { display: none; }
.gal-num {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: .16em;
  color: var(--faint);
  transition: color .25s ease;
}
.gal-name {
  font-family: var(--grot);
  font-weight: 300;
  /* Down from 3.3rem. At that size the list was the page and the photograph
     behind it was wallpaper; this leaves the names a list over a picture. */
  font-size: clamp(1.5rem, 2.7vw, 2.5rem);
  /* Eased with the size. Negative tracking is for big type; at this size
     -0.028em was closing letters up on each other. */
  letter-spacing: -0.018em;
  line-height: 1.08;
  /* An italic terminal leans out past the end of the text's own box: the y of
     Runity and the t of Notjust finish about a tenth of an em outside it, and
     the box does not grow to hold them. Nothing happened to be clipping it,
     but .gal-index hides its overflow and the tail was running into the
     tagline's gap, so it was one narrow window from being cut off. The box is
     widened to contain its own ink and the same amount comes back off the
     margin, so the type has not moved. */
  padding-right: .1em;
  margin-right: -.1em;
  transition: color .3s ease, transform .4s cubic-bezier(.2,.7,.2,1);
}
/* a capital mid-word (NotJust): the script wraps it, this gives it room */
.gal-name .cap { margin-left: .035em; }
/* Pointer states only where there is a pointer. iOS leaves :hover stuck on
   whatever you last touched, and these outrank the phone's own live state by a
   selector, so the name you tapped stayed lit alongside the one you had
   scrolled to. Gated, they cannot reach the phone at all. */
@media (hover: hover) {
  /* the whole list steps back so the one under the pointer comes forward */
  .gal-list:hover .gal-name { color: rgba(242, 238, 226, .5); }
  /* .is-live is the set the backdrop is showing: the first one when the
     index opens, and whichever the pointer last crossed after that. It reads
     exactly like a hover, so the index never opens with nothing chosen. */
  .gal-list .gal:hover .gal-name,
  .gal-list .gal:focus-visible .gal-name,
  .gal-list .gal.is-live .gal-name {
    color: var(--red);
    font-style: italic;
    transform: translateX(clamp(10px, 1.2vw, 20px));
  }
  .gal:hover .gal-num, .gal:focus-visible .gal-num, .gal.is-live .gal-num { color: var(--red); }
}

/* the set's line, arriving alongside the name */
.gal-tag {
  align-self: center;
  font-size: 9.5px;
  letter-spacing: .18em;
  color: var(--muted);
  opacity: 0;
  transform: translateX(-14px);
  transition: opacity .35s ease, transform .45s cubic-bezier(.2,.7,.2,1);
  white-space: nowrap;
}
@media (hover: hover) {
  .gal:hover .gal-tag, .gal:focus-visible .gal-tag, .gal.is-live .gal-tag {
    opacity: 1;
    transform: translateX(clamp(10px, 1.2vw, 20px));
  }
}

.gal-hint { display: none; }

/* Built from the reference's own numbers, measured rather than guessed: a row
   17.5svh tall on a 31.5svh pitch, the live one picked by nearest to the middle
   of the screen and followed live down the scroll, and a crossfade just under a
   second doing the work underneath. No snap: the reference has none, and snap
   is what made ours feel stepped rather than carried. */
@media (hover: none) {
  .gal-index { display: block; height: auto; min-height: 100vh; min-height: 100svh; }
  /* its own compositor layer, and the slow scroll-linked zoom rides here */
  .gal-stage {
    position: fixed;
    transform: translateZ(0) scale(var(--stage-zoom, 1.03));
    transform-origin: 50% 50%;
  }
  .gal-index::before {
    position: fixed;
    transform: translateZ(0);
    /* The desktop scrim is heavy on the left because the names sit there.
       Here they run the full width, so it has to dim evenly, and it was doing
       it at .58 the whole way down: the photograph never got above half
       brightness and the covers looked flat on a phone. So this keeps a band
       at the top for the header and an even, light dim below it, and the
       names sit on that with nothing behind them: no halo, no shadow. */
    background: linear-gradient(180deg,
      rgba(14,14,13,.82) 0px, rgba(14,14,13,.34) 190px, rgba(14,14,13,.34) 100%);
  }
  /* the stage carries the zoom, so the pictures inside it hold still */
  .gal-bg, .gal-bg.is-on { transform: none; }
  .gal-bg { transition: opacity .9s cubic-bezier(.4, 0, .2, 1); }
  body.gal-cover .work-section { min-height: 0; }

  /* The first screen shows 001 and 002 and nothing of 003: with the top
     padding and the pitch below, the third name starts six percent past the
     bottom of the screen, so nothing is caught half-way at the edge, even
     once Safari's toolbar has folded away and given the page a little more
     height. */
  .gal-list { padding: 46vh var(--edge) 46vh; padding: 46svh var(--edge) 46svh; }
  .gal {
    display: block;
    padding: 0;
    /* the pitch comes in with the type: the reference's 31.5svh was measured
       against names a third larger, and kept as it was it left the smaller
       names floating in too much air */
    min-height: 15vh;
    min-height: 15svh;
    margin-bottom: 15vh;
    margin-bottom: 15svh;
  }
  .gal-list li:last-child .gal { margin-bottom: 0; }

  .gal-num { display: block; margin-bottom: 6px; }
  .gal-name {
    display: block;
    /* Down from 11.5vw, which put RTE x Apple Dinner edge to edge across a
       phone and left the photograph as a backdrop to type. The longest name
       now sits inside two thirds of the width on a 375px screen. */
    font-size: clamp(1.6rem, 8vw, 2.3rem);
    /* the reference's own curve and duration */
    transition: color .38s cubic-bezier(.4, 0, .2, 1);
  }
  .gal-tag { display: block; margin-top: 8px; transform: none; }

  /* The names stay legible whether or not they are live. The reference does
     not fade the list out to lift one name; it lets the colour and the italic
     do that, which is why the list still reads as a list. */
  .gal-name { color: rgba(242, 238, 226, .78); }
  .gal-num  { color: rgba(242, 238, 226, .5); }
  .gal-tag  { opacity: 0; transition: opacity .38s ease; }

  .gal.is-live .gal-name { color: var(--red); font-style: italic; }
  .gal.is-live .gal-num  { color: var(--red); opacity: .9; }
  .gal.is-live .gal-tag  { opacity: 1; }

  /* says its piece once, then leaves on the first scroll */
  .gal-hint {
    display: flex;
    align-items: center;
    gap: 8px;
    position: fixed;
    left: 50%;
    bottom: calc(env(safe-area-inset-bottom, 0px) + 26px);
    z-index: 6;
    margin: 0;
    padding: 11px 17px;
    font-size: 9.5px;
    letter-spacing: .18em;
    color: var(--paper);
    background: rgba(20, 20, 19, .58);
    backdrop-filter: blur(16px) saturate(1.3);
    -webkit-backdrop-filter: blur(16px) saturate(1.3);
    border: 1px solid var(--line);
    border-radius: 999px;
    pointer-events: none;
    white-space: nowrap;
    opacity: 0;
    transform: translate(-50%, 10px);
    transition: opacity .5s ease, transform .5s cubic-bezier(.2,.7,.2,1);
  }
  .gal-hint.is-up { opacity: 1; transform: translate(-50%, 0); }
  .gal-hint.is-gone { opacity: 0; transform: translate(-50%, -6px); }
  .gal-hint-arrow {
    display: inline-block;
    font-size: 11px;
    line-height: 1;
    animation: hint-float 1.9s ease-in-out infinite;
  }
  @keyframes hint-float {
    0%, 100% { transform: translateY(2px); }
    50%      { transform: translateY(-3px); }
  }
}
@media (prefers-reduced-motion: reduce) {
  .gal-bg, .gal-name, .gal-num, .gal-tag { transition: none; }
}

/* ============ hero ============ */
.hero {
  /* the floor the name and the scroll cue both stand on: one value, so a
     change to it moves the pair together */
  --hero-floor: calc(clamp(56px, 7vh, 76px) + env(safe-area-inset-bottom, 0px));
  /* pinned: the work rises over the hero instead of the page cutting from a
     dark frame straight into a dark band of tiles */
  position: sticky;
  top: 0;
  z-index: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  display: flex;
  will-change: opacity;
}
/* everything after the hero is opaque so it reads as a sheet sliding up */
.work-section, .slab {
  position: relative;
  /* No z-index here on purpose. It used to be 2, which made this a stacking
     context and trapped the tab row inside it: the row could never rise above
     the header glass, whatever z-index it was given. Without one it still
     paints over the pinned hero, being a later positioned element. */
}
.slab { background: var(--bg); }
/* The black belongs to the panels, not to the section around them. With it on
   the section, the strip the bar sits in was solid black, so on the way down
   the glass had nothing behind it to frost and read as a plain dark bar. The
   panels still paint their own ground, so the wall's gutters stay black. */
.tab-panel { background: var(--bg); }
.hero-slide {
  position: absolute;
  inset: 0;
  display: block;
  opacity: 0;
  transition: opacity .9s ease;
}
.hero-slide.is-on { opacity: 1; }
.hero-slide, .hero-inner { will-change: transform; }
.hero-bg {
  width: 100%; height: 100%;
  object-fit: cover;
}
/* with the frame changing underneath it, the type needs a bed that works
   for every one of them rather than art direction tuned to a single shot */
@media (prefers-reduced-motion: reduce) {
  .hero-slide { transition: none; }
}
.hero-shade {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(14,14,13,.42) 0%, rgba(14,14,13,.05) 32%, rgba(14,14,13,.26) 62%, rgba(14,14,13,.9) 100%),
    linear-gradient(90deg, rgba(14,14,13,.46) 0%, rgba(14,14,13,0) 48%);
}
.hero-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  align-self: flex-end;
  /* The name has to clear whatever the browser puts along the bottom of the
     screen. Safari's bar is counted in the viewport, so svh already allows for
     it; Arc floats its own bar on top of the page instead, and the name was
     being cut in half by it. The inset covers the home indicator, and the
     floor is lifted to about the height of a browser's bar so there is
     somewhere to stand even where nothing is reported at all. */
  padding: 0 clamp(16px, 3vw, 40px) var(--hero-floor);
}

.wordmark {
  /* negative margin cancels the 'b' side bearing so ink aligns to the column */
  margin: 0 0 0 -0.071em;
  /* sized so it finishes before the subject rather than running across him */
  font-size: clamp(1.75rem, 6.2vw, 6.2rem);
  font-weight: 300;
  /* -0.045em crowded the round letters at display size; this lets the
     bowls breathe while still reading as one tight word */
  letter-spacing: -0.022em;
  line-height: .9;
  /* the font ships real kerning pairs, so let the browser use them */
  font-kerning: normal;
  font-feature-settings: "kern" 1, "liga" 1;
  font-variant-ligatures: common-ligatures;
}
/* the degree sits tight to the 'd' by default, and reads as part of the
   word rather than a mark, so give it its own air and optical size */
/* sits it up at the ascender so it reads as a mark on the word, not a
   character floating inside it; measured against Switzer at display size */
.wordmark .deg {
  font-size: .34em;
  /* measured: at 1.2em the ring sat 6.4px under the top of the d */
  vertical-align: 1.437em;
  margin-left: -.02em;
  letter-spacing: 0;
  color: var(--red);
  /* Stated as a fill as well as a colour. The word sets a fill of its own to
     let the sweep show through its letters, and a fill set on a parent beats
     a colour set on a child: without this the degree went with it and came
     out paper instead of red. */
  -webkit-text-fill-color: var(--red);
}


.hero-services {
  list-style: none;
  margin: 0; padding: 0;
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .14em;
  line-height: 2.1;
  color: rgba(242, 238, 226, .82);
  text-align: right;
}
.hero-services .num { color: rgba(242, 238, 226, .5); margin-right: .9em; }


/* ============ the wall ============ */
.wall-head h2 {
  margin: 0;
  font-size: clamp(1.5rem, 3vw, 2.6rem);
  font-weight: 300;
  letter-spacing: -0.02em;
}
.wall-count {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--muted);
}

.wall {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--pad);
  padding: 0 0 var(--pad);
  grid-auto-flow: dense;
}
.tile {
  touch-action: manipulation;
  position: relative;
  display: block;
  overflow: hidden;
  /* square: rounded corners left dark notches between neighbouring tiles */
  border-radius: 0;
  /* gallery tiles are buttons, which carry a default UA border and padding;
     the wall's are links and never showed it */
  border: none;
  padding: 0;
  font: inherit;
  text-align: left;
  background: var(--bg-2);
  aspect-ratio: 4 / 5;
  text-decoration: none;
  color: inherit;
  /* No per-tile entrance. The script marks every tile .in as soon as the
     wall is built; the rule below is kept so a tile is never left invisible
     if the script has not run, and nothing animates on the way in. */
  opacity: 1;
  transform: none;
}
.tile.in { opacity: 1; transform: none; }
.tile.wide { grid-column: span 2; aspect-ratio: 8 / 5; }
.tile img, .tile video {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .9s cubic-bezier(.2,.6,.2,1), opacity .5s;
}
@media (hover: hover) {
  .tile:hover img, .tile:hover video { transform: scale(1.04); }
}
.tile video { position: absolute; inset: 0; opacity: 0; }
.tile.playing video { opacity: 1; }
.tile.playing img { opacity: 0; }

.tile-play {
  position: absolute;
  top: 12px; right: 12px;
  width: 26px; height: 26px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--glass);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--line);
  font-size: 8px;
  color: var(--paper);
  padding-left: 2px;
}
.tile.hidden { display: none; }
/* after a filter change the surviving tiles settle back in rather than
   snapping into their new positions */
.wall.filtering .tile:not(.hidden) {
  animation: tile-settle .5s cubic-bezier(.2, .7, .2, 1) both;
}
@keyframes tile-settle {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .wall.filtering .tile:not(.hidden) { animation: none; }
}

/* ============ lightbox ============ */
.lb:focus { outline: none; }
.lb {
  position: fixed;
  inset: 0;
  z-index: 900;
  /* near solid: at .96 the hero wordmark ghosted through behind the work */
  background: rgba(9, 9, 8, .995);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  display: grid;
  grid-template-rows: 1fr auto;
}
body.lb-open { overflow: hidden; }
/* horizontal snap track: one full-width slide per piece, so a trackpad
   swipe or drag moves through the work and every frame lands centred */
.lb-track {
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  /* No scroll-behavior here: the arrows are the only thing that scrolls this
     by instruction rather than by hand, and they ask for smooth themselves,
     so the property would only decide the same thing twice. */
  min-height: 0;
  scrollbar-width: none;
  -ms-overflow-style: none;
  overscroll-behavior-x: contain;
  /* The same trap the page itself fell into. When anything inside a scroller
     changes, the browser helpfully shifts the scroll to keep what you were
     looking at still. In here the only thing that ever changes is a film
     slide swapping its still frame for the film, so that help arrived as a
     lurch, and only ever next to a film. */
  overflow-anchor: none;
}
.lb-track::-webkit-scrollbar { display: none; }
.lb-slide {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: center;
  /* kept as parts rather than one line so the film can be laid over the still
     frame on exactly the same box, without the numbers written twice */
  /* Equal above and below. The arrows and the play/pause beat sit on the
     viewer's centre line, and the picture is centred inside this padding, so
     the two only agree when the padding is the same at both ends. The bottom
     used to be thin because a bar sat under it; the bar is gone. */
  --slide-t: 64px;
  --slide-x: clamp(56px, 8vw, 120px);
  --slide-b: 64px;
  padding: var(--slide-t) var(--slide-x) var(--slide-b);
}
.lb-slide img, .lb-slide video {
  object-fit: contain;
  border-radius: 0;
}
/* width/height 100% inside the padded slide gives the media a definite
   box, so tall portrait frames fit the screen instead of running off it.
   Every slide has a still frame, film or photograph alike, and it is what
   gives the slide its shape. */
.lb-slide img {
  /* the very box the film below is given, coordinate for coordinate: the
     two used to arrive at the same rectangle by different sums (padding on
     one, absolute maths on the other), and a fraction of a pixel between
     them showed as a tiny snap the moment the film took over */
  position: absolute;
  top: var(--slide-t);
  left: var(--slide-x);
  width: calc(100% - var(--slide-x) * 2);
  height: calc(100% - var(--slide-t) - var(--slide-b));
}
/* The film lies over its own still frame on the same box, out of the flow,
   and is not rendered at all until you have stopped on it.

   Both halves of that matter. On a phone a film is drawn in a layer of its
   own, and that layer lags a scrolling track: it paints where the track was
   a moment ago, which is the piece sitting off to one side and then catching
   up. While you are moving there is now no film here to lag. And being out
   of the flow, showing it or hiding it changes no layout whatever, so the
   track is never made to work its snapping out again. */
.lb-slide video {
  position: absolute;
  top: var(--slide-t);
  left: var(--slide-x);
  /* Sized, not inset. A video is a replaced element, so `auto` against four
     insets resolves to its own intrinsic size and the insets are dropped,
     which puts it at the wrong size in the wrong place. */
  width: calc(100% - var(--slide-x) * 2);
  height: calc(100% - var(--slide-t) - var(--slide-b));
  display: none;
}
.lb-slide.is-live video { display: block; }
/* The poster goes once the film is truly rolling, not the moment it is
   asked to roll: is-rolling arrives with the first presented frame. (It
   still goes, because the two round their edges separately and a sliver of
   poster showing around the film read as a pale outline drawn round it.) */
.lb-slide.is-rolling img { visibility: hidden; }
@media (prefers-reduced-motion: reduce) { .lb-track { scroll-behavior: auto; } }
.lb-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  /* The target stays the size a thumb needs; only the mark inside it is
     small. A button you can hit is not the same thing as a button you can
     see, and the pill around this one was the part doing the shouting: a
     frosted disc sat on top of the photograph, competing with it. The arrow
     alone is enough. */
  width: 46px; height: 46px;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  padding: 0;
  border-radius: 0;
  color: var(--paper);
  opacity: .5;
  /* the angle marks are punctuation-sized glyphs, so they take a larger size
     than the arrows did to hold the same optical weight */
  font-size: 27px;
  line-height: 1;
  /* they sit on the text baseline, a touch low for a mark floating alone */
  padding-bottom: 3px;
  cursor: pointer;
  transition: opacity .25s;
}
@media (hover: hover) {
  .lb-nav:hover { opacity: 1; }
}
.lb-prev { left: clamp(10px, 2vw, 26px); }
.lb-next { right: clamp(10px, 2vw, 26px); }
.lb-close {
  position: absolute;
  top: 14px; right: 14px;
  z-index: 2;
  /* Plain type, like every other control on the site. This was the last
     frosted pill left standing: everything else up here is a word that
     brightens, so a lozenge with a border read as another site's button.
     The padding keeps the 44px press target the pill used to provide. */
  height: 44px;
  padding: 0 14px;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 10.5px;
  cursor: pointer;
  transition: color .25s ease;
}
@media (hover: hover) {
  .lb-close:hover { color: var(--paper); }
}
/* The play/pause flash. Centred on the viewer, which is centred on the film,
   so it lands in the middle of the picture. It is never a control, only an
   answer: shown larger and fading as it shrinks back, the way every player
   does it, because the eye reads that as "done" rather than "press me". */
.lb-flash {
  position: absolute;
  left: 50%; top: 50%;
  width: 72px; height: 72px;
  margin: -36px 0 0 -36px;
  z-index: 3;
  display: grid;
  place-items: center;
  color: var(--paper);
  filter: drop-shadow(0 2px 10px rgba(14, 14, 13, .8));
  pointer-events: none;
  opacity: 0;
}
.lb-flash svg { width: 44px; height: 44px; grid-area: 1 / 1; display: none; }
.lb-flash.play .play, .lb-flash.pause .pause { display: block; }
.lb-flash.is-on { animation: lb-flash .7s ease-out forwards; }
@keyframes lb-flash {
  0%   { opacity: .95; transform: scale(.7); }
  25%  { opacity: .95; transform: scale(1); }
  100% { opacity: 0;   transform: scale(1.35); }
}
@media (prefers-reduced-motion: reduce) {
  .lb-flash.is-on { animation: lb-flash-still .7s ease-out forwards; }
  @keyframes lb-flash-still { 0%, 40% { opacity: .95; } 100% { opacity: 0; } }
}

.lb-sound.is-unplaced { visibility: hidden; }
.lb-sound {
  /* Sat in the film's own corner by the script, which measures where the
     picture actually paints. The mark alone, no disc behind it. */
  position: absolute;
  z-index: 3;
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--paper);
  filter: drop-shadow(0 1px 3px rgba(14, 14, 13, .9)) drop-shadow(0 0 10px rgba(14, 14, 13, .6));
  transition: opacity .45s ease;
}
.lb-sound svg { width: 22px; height: 22px; }
/* The waves are always there. Muted, one long slash cuts through the whole
   mark; aloud, the slash lifts and the waves go red. */
.lb-sound .slash { opacity: 1; }
.lb-sound[aria-pressed="true"] .wave { stroke: var(--red); }
.lb-sound[aria-pressed="true"] .slash { opacity: 0; }
/* Once the film has been rolling a moment the mark bows out, the way every
   player's controls do, and any touch or movement over the film brings it
   back. Faded it cannot be pressed, so a blind tap in that corner is a tap
   on the film, not a surprise unmute. */
.lb-sound.is-faded { opacity: 0; pointer-events: none; }
/* Kept in the bar when there is no film to mute, so the bar is one height
   for good. See sync(): coming and going, it resized the track under the
   swipe. */
.lb-sound.is-away { visibility: hidden; pointer-events: none; }
/* the film fills the slide and takes the click for play/pause */
.lb-slide video { cursor: pointer; }
.lb-project { color: var(--muted); text-decoration: none; }
@media (hover: hover) {
  .lb-project:hover { color: var(--paper); }
}
@media (max-width: 620px) {
  /* the target keeps its 44, which is the smallest a finger should be asked
     to find; the arrow in it comes down a step */
  .lb-nav { width: 44px; height: 44px; font-size: 24px; }
  .lb-slide { --slide-t: 60px; --slide-x: 8px; --slide-b: 60px; }
  .lb-prev { left: 8px; }
  .lb-next { right: 8px; }
}

/* ============ about + contact ============ */
/* No rule between sections: the hairline ran the full width while the content
   under it did not, which read as an unfinished table rather than a divide.
   The space does the separating instead. */
.slab {
  padding: clamp(72px, 10vh, 132px) var(--edge);
}
/* Two sections back to back were each paying full padding, so the gap between
   them came to double what either intended. The upper one's bottom padding is
   the whole space now. */
.slab + .slab { padding-top: 0; }

.slab-kick {
  display: flex;
  align-items: baseline;
  gap: 14px;
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .18em;
  color: var(--muted);
  margin-bottom: clamp(20px, 3vh, 32px);
}
.slab h2 {
  margin: 0 0 .55em;
  font-size: clamp(1.9rem, 4.4vw, 3.9rem);
  font-weight: 300;
  letter-spacing: -0.02em;
  line-height: 1.06;
}
.about-row {
  display: grid;
  grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
  gap: clamp(24px, 5vw, 86px);
  /* the block of text sits against the middle of the portrait */
  align-items: center;
}
.about-portrait {
  margin: 0;
  overflow: hidden;
  border-radius: 10px;
  background: var(--bg-2);
  aspect-ratio: 1 / 1;
}
.about-portrait img { width: 100%; height: 100%; object-fit: cover; }
/* if the portrait is missing its figure is removed, so collapse the
   reserved column rather than leaving a hole beside the text */
.about-row:not(:has(.about-portrait)) { grid-template-columns: 1fr; }

.slab-body { max-width: 44em; }
.slab-body p {
  margin: 0;
  font-size: clamp(1rem, 1.2vw, 1.12rem);
  line-height: 1.72;
  color: rgba(242, 238, 226, .82);
}
.slab-body p + p { margin-top: 1.25em; }
.tbc {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .22em;
  color: var(--faint);
}


.contact-mail {
  display: inline-block;
  /* optical alignment: at this size the b's side bearing is ~5.5px against
     0.7px on the mono around it, so the address reads indented unless it is
     pulled back onto the edge. In em so it holds through the clamp.
     The top margin lives in the shorthand: set as a longhand above it, the
     shorthand below quietly put it back to zero and the address sat on the
     sign-off. */
  margin: clamp(30px, 4.2vh, 46px) 0 0 -0.065em;
  /* It was sized to end on the portrait's edge, which made it the loudest
     thing on the page. It sits inside the about copy now, so it answers the
     name above it instead: a step up from the body, not a headline. */
  font-size: clamp(1.35rem, 3.4vw, 2rem);
  font-weight: 300;
  letter-spacing: -0.03em;
  line-height: 1.05;
  overflow-wrap: anywhere;
  /* set as an actual link rather than large type: an address this size reads
     as a heading otherwise, and nothing says it can be clicked */
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .14em;
  text-decoration-color: rgba(242, 238, 226, .32);
  transition: color .3s, text-decoration-color .3s;
}
.contact-mail::after {
  /* Drawn, not typed. U+2197 is given emoji presentation on iOS, and a colour
     emoji glyph ignores `color` outright, so the arrow came out as the system
     blue tick no matter what was set on it. An SVG owes nothing to a font. */
  content: '';
  display: inline-block;
  width: .42em;
  height: .42em;
  margin-left: .18em;
  vertical-align: .34em;
  background: no-repeat center/contain
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 9.5 9.5 2.5M4 2.5h5.5V8' fill='none' stroke='%23E8442E' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  transition: transform .3s cubic-bezier(.2,.7,.2,1);
}
@media (hover: hover) {
  .contact-mail:hover,
  .contact-mail:focus-visible {
    color: var(--red);
    text-decoration-color: var(--red);
  }
}
.about-sign + .contact-mail { display: block; width: fit-content; }

/* The Instagrams, under the address: the mark once, then the three handles
   on one line, close together, underlined the same way the address is so
   they read as links. One line on every screen: on a phone they come down a
   size rather than wrapping. */
.about-ig {
  list-style: none;
  margin: clamp(16px, 2.2vh, 24px) 0 0;
  padding: 0;
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 14px;
  font-size: 10.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  white-space: nowrap;
}
.about-ig-mark { display: flex; color: rgba(242, 238, 226, .6); margin-right: 2px; }
.about-ig-mark svg { width: 14px; height: 14px; }
.about-ig a {
  color: rgba(242, 238, 226, .78);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .22em;
  text-decoration-color: rgba(242, 238, 226, .32);
  padding: 8px 0; margin: -8px 0;     /* a finger's worth of target, no taller a row */
  transition: color .25s ease, text-decoration-color .25s ease;
}
@media (hover: hover) {
  .about-ig a:hover, .about-ig a:focus-visible { color: var(--red); text-decoration-color: var(--red); }
}
/* a phone: the same one line, a size down so all three fit beside the mark */
@media (max-width: 760px) {
  .about-ig { gap: 10px; font-size: 9px; letter-spacing: .04em; }
  .about-ig-mark svg { width: 13px; height: 13px; }
}
  .about-ig-mark { margin-bottom: -2px; }
}
@media (hover: hover) {
  .contact-mail:hover::after,
  .contact-mail:focus-visible::after { transform: translate(.14em, -.14em); }
}


/* ============ work detail pages ============ */
.work-stage {
  position: relative;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: var(--bg-2);
}
.work-stage > img, .work-stage > video {
  width: 100%; height: 100%;
  object-fit: cover;
}
.work-stage::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(14,14,13,.5) 0%, rgba(14,14,13,0) 26%, rgba(14,14,13,0) 62%, rgba(14,14,13,.72) 100%);
  pointer-events: none;
}
.work-details {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 400;
  width: min(380px, calc(100vw - 28px));
}
.details-toggle {
  margin-left: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  width: 100%;
  padding: 0 16px;
  height: 42px;
  background: var(--glass);
  backdrop-filter: blur(18px) saturate(1.3);
  -webkit-backdrop-filter: blur(18px) saturate(1.3);
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--paper);
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .16em;
  cursor: pointer;
}
.details-toggle .chev { transition: transform .3s; }
.work-details.open .details-toggle .chev { transform: rotate(180deg); }
.details-card {
  margin-bottom: 6px;
  padding: 22px;
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(1.3);
  -webkit-backdrop-filter: blur(20px) saturate(1.3);
  border: 1px solid var(--line);
  border-radius: 12px;
  opacity: 0;
  pointer-events: none;
  transform: translateY(10px);
  transition: opacity .35s, transform .4s cubic-bezier(.2,.6,.2,1);
}
.work-details.open .details-card { opacity: 1; pointer-events: auto; transform: none; }
.details-card h1 {
  margin: 0 0 10px;
  font-size: 1.5rem;
  font-weight: 300;
  letter-spacing: -0.015em;
}
.details-card p {
  margin: 0 0 18px;
  font-size: .92rem;
  line-height: 1.6;
  color: rgba(242, 238, 226, .78);
}
.details-meta {
  display: grid;
  gap: 7px;
  margin: 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .14em;
}
.details-meta div { display: flex; justify-content: space-between; gap: 16px; }
.details-meta dt { color: var(--faint); }
.details-meta dd { margin: 0; color: var(--paper); text-align: right; }

.work-more {
  padding: var(--pad) 0 var(--pad);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--pad);
}
.work-more figure { margin: 0; overflow: hidden; border-radius: 0; background: var(--bg-2); aspect-ratio: 4 / 5; }
.work-more figure.wide { grid-column: span 3; aspect-ratio: 16 / 9; }
.work-more img, .work-more video { width: 100%; height: 100%; object-fit: cover; }

.work-nav {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 24px clamp(16px, 3vw, 40px) 30px;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .14em;
}
.work-nav a { color: var(--muted); text-decoration: none; }
@media (hover: hover) {
  .work-nav a:hover { color: var(--paper); }
}

/* ============ responsive ============ */
@media (max-width: 1500px) {
  .wall { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 1100px) {
  /* 4 columns holds down to tablet: 20 tiles divide exactly by 5, 4 and 2,
     so the last row is never left half empty */
  .work-more { grid-template-columns: repeat(2, 1fr); }
  .work-more figure.wide { grid-column: span 2; }
  /* the full name would run into the centred filter pill here, so keep
     the face and drop the words rather than losing the pill entirely */
  .id-label { display: none; }
  
}
@media (max-width: 1100px) {
  .gal-grid { grid-template-columns: repeat(2, 1fr); }
}
/* the about column pair collapsed to "319px 0px" on a phone: the text was
   squeezed to zero width and wrapped a character at a time, running the
   section to 2678px tall. One column below tablet. */
@media (max-width: 900px) {
  .about-row { grid-template-columns: 1fr; gap: clamp(18px, 4vw, 28px); }
  /* the portrait keeps its square; only the run of gaps beneath it comes in,
     so the handles sit a step higher without anything feeling packed */
  .about-portrait { max-width: 420px; }
  .about-text .slab-body { margin-top: 18px; }
  .about-text .slab-body p { margin: 0; }
  .about-sign { margin-top: 18px; }
  .contact-mail { margin-top: 24px; }
  .about-ig { margin-top: 16px; }
}
/* a gallery cover at 179px wide is not a cover, it is a thumbnail */
@media (max-width: 620px) {
  .gal-grid { grid-template-columns: 1fr; }
}
@media (max-width: 760px) {
  .wall { grid-template-columns: repeat(2, 1fr); }

  /* The line keeps the top left corner and the mark keeps the bottom one, so
     the hero reads as two things in two corners rather than a stack. */
  .hero-thesis {
    display: block;
    top: calc(18px + env(safe-area-inset-top, 0px));
    /* Left as a sentence here, and left alone. The capitals upstairs are there
       to agree with the words in the opposite corner, and on a phone that
       corner is empty: there is nothing for them to agree with, and a line of
       tracked-out capitals across the top of a photograph is a label on it
       rather than something anybody reads. */
    font-size: 9.5px;
    text-transform: none;
    letter-spacing: .02em;
    font-weight: 400;
  }
  .hero-thesis-inline { display: none; }

  /* the mark and the three nav items do not both fit at 375px: together they
     came to 350px inside 343px of room. Both come down a step. */
  .chrome-mark { font-size: 17px; top: 14.5px; }
  /* No nav on a phone. The three words are on the top line themselves one
     screen down, so anything up here was a second copy of them, and taking it
     away leaves the hero as a picture and a name with swiping as the way on.
     The links stay in the markup for the desktop, which shows them across the
     top; there is simply nothing to reveal them here. */
  .chrome-nav { display: none; }

  /* One line across the top of the screen: the hero's line on it, and past the
     hero the mark on the left with the three words on the right. Each is hung
     from its middle rather than handed a top of its own and left to land where
     it lands, so whatever their heights they sit on the same axis. */
  /* The line everything up here hangs from, and now also half the height of
     the bar behind it, since the glass is centred on it. Bringing the line up
     takes the same amount off the top and the bottom at once, so the bar comes
     in without the type going off centre again. */
  :root { --chrome-line: 21px; }
  .hero-thesis,
  .chrome-mark {
    top: calc(var(--chrome-line) + env(safe-area-inset-top, 0px));
    transform: translateY(-50%);
  }


  /* The mark has the bottom of the screen to itself now, so it can be the
     size it always wanted to be rather than sharing the corner with a line
     of type above it. */
  .wordmark { font-size: clamp(3rem, 16vw, 5rem); }

  /* A hairline in the bottom right, which is the corner the name does not
     use, with a lit segment running up it. No arrow and no word: the movement
     is the whole message, and anything more is a sign on a photograph. Sat on
     the name's own baseline so it belongs to the composition rather than
     floating in the corner on its own. */
  .hero-scroll {
    display: block;
    position: absolute;
    right: var(--edge);
    /* the same floor the name stands on, so they rise and fall together */
    bottom: calc(var(--hero-floor) + 8px);
    width: 2px;
    height: 34px;
    background: rgba(242, 238, 226, .22);
    overflow: hidden;
    pointer-events: none;
    transition: opacity .35s ease;
  }
  .hero-scroll::after {
    content: '';
    position: absolute;
    left: 0; top: 0;
    width: 2px;
    height: 13px;
    background: var(--paper);
    box-shadow: 0 0 12px rgba(14, 14, 13, .9);
    animation: hero-scroll 2.6s cubic-bezier(.45, 0, .2, 1) infinite;
  }
  /* it has said its piece the moment you scroll */
  body.past-hero .hero-scroll { opacity: 0; }

  /* the tagline already appears in the hero and in the about block, and
     keeping it here forced the tab bar onto two rows */
  .tab-count { display: none; }

  /* The header is 44px on a phone rather than 48, and every piece of it hangs
     off this one value: the row's pin, the strip above it, and the padding the
     gallery and about pages use to clear it. */
  :root { --pin: 44px; }

  /* The three words come up onto the mark's own line, out to the right, so
     the top of the screen is one row rather than two stacked on each other.

     Hung from its middle on the same axis the mark and the tagline hang from,
     rather than given a top of its own. Setting a top means picking a number
     that only holds for one type size, and this row's size is a thing worth
     being free to change: with even padding above and below, its letters and
     theirs sit on one line whatever size any of them end up.

     Left alone inside a gallery, where this row is that set's own name and
     its way back out rather than a menu. Written from `body` so it outranks
     the gallery index's own positioning of the same row. */
  /* Inside a set the three tabs stand down and the set's own line takes the
     top right instead. They were stacking: the mark on one line, the tabs on
     the next, the set's line under that, three rows of header down a phone
     for a page that is meant to be photographs. Nothing is lost by it, since
     ← all galleries is the way out and the tabs are waiting where it lands. */
  body .tabs:has(> .gal-detail-head) > .tab,
  body .tabs:has(> .gal-detail-head) > .tab-count { display: none; }

  /* Inside a set the row is the first thing on the page, so it has nothing to
     rise from: it was sitting where the section's own top padding left it,
     a whole line below the mark, and only joined it once you scrolled. Given
     exactly the padding that puts it on the line to begin with, the set's
     heading and the mark are one line from the moment it opens, and the
     photographs start under one header rather than beneath two. */
  body.tab-gallery:not(.gal-cover) .work-section {
    padding-top: calc(var(--chrome-line) - (var(--row-h, 27px) / 2)
                      + env(safe-area-inset-top, 0px));
  }

  /* The index names itself gal-cover and pins this row further up the file, at
     a weight this rule cannot beat on its own. Naming that state here as well
     puts the two on level terms, and being the later of the two this one wins.
     Only the line it sits on is taken back: the index still floats the row
     free of the flow, which is its own doing and still wanted. */
  body .tabs,
  body.gal-cover .tabs {
    /* Sat half its own height above the line the mark hangs from, so its type
       lands on that line when it pins.

       Done with `top`, never with a transform. A transform moves what you see
       and leaves the space it occupied behind: half a row of empty section
       between the bar and the photographs underneath it, which is exactly the
       gap. Positioned properly, the bar sits flush on top of the section and
       travels up with it. The height is measured in the script, so this holds
       whatever size the row ends up. */
    top: calc(var(--chrome-line) - (var(--row-h, 27px) / 2) + env(safe-area-inset-top, 0px));
    justify-content: flex-end;
    gap: 18px;
    padding: 7px var(--edge);
  }

  /* About opens unscrolled, so a sticky row is never pulled up onto the line
     and sits where it fell in the flow, under the mark rather than beside it.
     Pinned outright there instead.

     Only there. The wall keeps its row in the flow, because the hero's
     hand-over is measured from that row travelling up and arriving: pin it
     and it is always arrived, so the bar and the mark never appear at all.
     The gallery already pins this row for its own reasons, further up. */
  body.tab-about .tabs {
    position: fixed;
    left: 0; right: 0;
  }
  body.tab-about .work-section {
    padding-top: calc(var(--header-h, 44px) + 8px);
  }

  /* Finger targets. The tab text is 15px tall and the nav 12px, which is far
     too small to hit reliably; the padding grows the hit box and the negative
     margin keeps it from growing the bar. */
  /* vertical only: horizontal padding pushed the text off the column edge */
  .tab {
    padding: 11px 0; margin: -11px 0;
    /* Down a step now the mark shares the line: at 11px the three words and
       the mark came within twelve pixels of each other on a narrow phone,
       which reads as one crowded string rather than two things. The hit box
       above is untouched, so they are no harder to press for being smaller. */
    font-size: 11px;
    font-weight: 500;
    letter-spacing: .12em;
  }
  .gal-back { padding: 10px 0; margin: -10px 0; }
}

/* ============ reduced motion ============ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .tile { opacity: 1; transform: none; transition: none; }
  .tile img, .tile video { transition: none; }
  /* the segment parks at the top of the line rather than running up it:
     still legible as a cue, with nothing moving */
  .hero-scroll::after { animation: none; }
}


/* ============ pointer dot ============
   Over a piece of media the system cursor is replaced by this, so it is
   obvious the tile does something. */
.cursor-dot {
  position: fixed;
  top: 0; left: 0;
  width: 9px; height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  /* Drawn rather than filled, so it is the same mark as the ring in the
     wordmark and the one this throws off when a piece opens. A solid dot was
     the only red thing on the site that was a disc; everything else red is a
     circle you can see through. Same 1.5px the ring below uses, and with
     border-box the outer size and the centring above are untouched by it. */
  background: none;
  border: 1.5px solid var(--red);
  pointer-events: none;
  z-index: 950;
  opacity: 0;
  transition: opacity .22s ease,
              width .3s cubic-bezier(.2,.7,.2,1),
              height .3s cubic-bezier(.2,.7,.2,1),
              margin .3s cubic-bezier(.2,.7,.2,1);
}
.cursor-dot.is-on { opacity: 1; }
.cursor-dot.is-over {
  width: 16px; height: 16px;
  margin: -8px 0 0 -8px;
}
/* a ring leaving the dot as the media opens: on its own element so it can
   animate transform without fighting the positioning on the dot itself */
.cursor-dot::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 16px; height: 16px;
  margin: -8px 0 0 -8px;
  border-radius: 50%;
  border: 1.5px solid var(--red);
  opacity: 0;
}
.cursor-dot.is-firing::after { animation: dot-ring .52s cubic-bezier(.2, .7, .2, 1); }
@keyframes dot-ring {
  from { transform: scale(.55); opacity: .95; }
  to   { transform: scale(4.4); opacity: 0; }
}

/* the dot is the pointer over media, so the arrow gets out of the way */
.wall .tile, .gal-row .tile { cursor: none; }

@media (hover: none) {
  .cursor-dot { display: none; }
  .wall .tile, .gal-row .tile { cursor: pointer; }
}
@media (prefers-reduced-motion: reduce) {
  .cursor-dot { transition: opacity .2s ease; }
  .cursor-dot.is-firing::after { animation: none; }
}
