/* ---------- Fonts ---------- */

@font-face {
  font-family: "Monument Extended";
  src: url("../fonts/HelveticaNeue/MonumentExtended-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

@font-face {
  font-family: "Helvetica Neue LV";
  src: url("../fonts/HelveticaNeue/HelveticaNeueLight.otf") format("opentype");
  font-weight: 300;
  font-style: normal;
  font-display: block;
}

@font-face {
  font-family: "Helvetica Neue LV";
  src: url("../fonts/HelveticaNeue/HelveticaNeueBold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: block;
}

@font-face {
  font-family: "Helvetica Neue LV";
  src: url("../fonts/HelveticaNeue/HelveticaNeueBoldItalic.otf") format("opentype");
  font-weight: 700;
  font-style: italic;
  font-display: block;
}

/* One weight only - the file carries usWeightClass 400 and no companion face -
   so it is declared at 400 and used at 400. Asking for 700 anywhere would make
   the browser smear the outlines into a synthetic bold instead. TrueType
   outlines in a .ttf, hence the format hint. */
@font-face {
  font-family: "Bileha";
  src: url("../fonts/bileha-font/Bileha.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

/* ---------- Base ---------- */

:root {
  /* 1px = the 1920px reference layout; scales down proportionally below that */
  --u: clamp(0.5px, 100vw / 1920, 1px);
}

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

html {
  background: #000;
  /* The bar is hidden, not the scrolling: wheel, touch, keyboard and the
     smooth scroller all behave exactly as before. */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* legacy Edge */
}

html::-webkit-scrollbar,
body::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none; /* Chrome, Safari */
}

body {
  margin: 0;
  min-height: 100vh;
  background: #000;
  color: #fff;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ---------- Header ---------- */

/* The wordmark and nav invert whatever is behind them, pixel by pixel: black
   footage gives white type, a bright frame gives dark type, a colour gives its
   opposite. Doing it as a blend rather than by sampling the video means it is
   exact and per-letter - a bright patch passing behind one word flips only that
   part - and it costs nothing to run.

   Nothing above these in the tree isolates them (no opacity, transform, filter
   or isolation on body or html), which is what lets the blend reach the video. */
.header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 10;
  mix-blend-mode: difference;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding-top: calc(24.752 * var(--u));
  padding-right: calc(49.553 * var(--u));
  padding-left: calc(54.365 * var(--u));
}

/* Logo */

.logo {
  display: inline-block;
  /* Pure white on purpose: the difference blend above subtracts the footage
     from this, so white is what makes the result a true inverse. */
  color: #fff;
  text-decoration: none;
  font-family: "Monument Extended", sans-serif;
  font-weight: 400;
  font-size: calc(46.125 * var(--u));
  line-height: 1;
  letter-spacing: 0;
  white-space: nowrap;
  /* Deliberately inert on hover - nothing dims, nothing moves. What colour it
     appears is decided by the blend against the footage, not by the pointer. */
}

.logo__mark {
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: 0.1703em;
  vertical-align: baseline;
  position: relative;
  top: -3.043em;
  letter-spacing: 0;
}

/* Nav */

.nav {
  display: flex;
  align-items: baseline;
  gap: calc(53.344 * var(--u));
}

.nav__link {
  color: #fff;
  text-decoration: none;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: calc(21.37 * var(--u));

  /* The clipping box each letter rolls inside is exactly this tall, and the
     letter's twin is drawn exactly this far below it - so the two must be the
     same number or the roll lands off its own mark. One shared value, used by
     both.

     It cannot be 1: a line box of one em is shorter than the font's natural
     line, and with the box clipping its overflow the tops of the letters were
     being cut off. */
  --roll-line: 1.28;
  line-height: var(--roll-line);
  letter-spacing: 0;
  white-space: nowrap;
}

/* ---------- Nav: per-letter roll ---------- */

/* Each letter is clipped to exactly one line and carries a duplicate of itself
   one line below, drawn by text-shadow rather than by extra markup. Rolling the
   glyph up by 100% slides that twin into the slot it just vacated, so the
   letter appears to roll over rather than simply leave. currentColor keeps the
   twin locked to the ink colour the footage is driving. */
.ltr {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
}

.ltr__g {
  display: block;
  /* One box-height below, matching the roll's travel exactly. */
  text-shadow: 0 calc(var(--roll-line, 1.28) * 1em) 0 currentColor;
  transform: translateY(0);
  transition: transform 0.56s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i) * var(--roll-stagger, 30ms));
}

@media (hover: hover) {
  .nav__link:hover .ltr__g {
    transform: translateY(-100%);
  }
}

/* ---------- Page ---------- */

.page {
  min-height: 100vh;
  background: #000;
}

/* ---------- Full-bleed page image ---------- */

.page--media {
  position: relative;
  overflow: hidden;
}

.page--media picture {
  display: block;
}

.page__image {
  display: block;
  width: 100%;
  height: 100vh;
  height: 100svh;
  object-fit: cover;
  object-position: center;
}

/* Opening black. Below the header, so the wordmark and nav sit on top of it
   from the first frame and are never dimmed by the fade. */
.page__veil {
  position: absolute;
  inset: 0;
  z-index: 5;
  background: #000;
  pointer-events: none;
}

/* ---------- Infos: one scroll-driven cinematic timeline ---------- */

/* The track supplies the scroll distance. The stage sticks to the viewport
   for the whole of it, so scroll position alone drives every visual state. */
.infos {
  --infos-screens: 7;
  position: relative;
  height: calc(var(--infos-screens) * 100vh);
  height: calc(var(--infos-screens) * 100svh);
  background: #000;
}

.infos__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: #000;
}

/* The opening frame is the video's own first frame, served as the poster, so
   there is no image-to-video transition at all - it is the same pixels. */
.infos__video {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
  pointer-events: none;
  /* Promote to its own layer up front, so the compositor does not do it
     mid-scroll (which costs a frame, and that frame reads as a black flash). */
  will-change: transform;
  backface-visibility: hidden;
}

/* Opening black. Sits below the header so the logo and nav are never affected. */
.infos__veil {
  position: absolute;
  inset: 0;
  z-index: 5;
  background: #000;
  pointer-events: none;
}

/* ---------- Infos: typography ---------- */

.infos__cue,
.infos__copy {
  position: absolute;
  z-index: 6;
  pointer-events: none;
}

.infos__cue {
  top: 50%;
  right: clamp(24px, 6.8vw, 130px);
  transform: translateY(-50%);
  margin: 0;
  opacity: 0;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(12px, 0.78vw, 15px);
  letter-spacing: 0.2em;
  color: #f2f2f2;
  white-space: nowrap;
}

/* The cue stays simple: once it has appeared it just loops faint to full.
   The loop lives on the inner span so it multiplies with the scroll-driven
   opacity on the parent instead of fighting it, and it only starts once the
   cue has arrived, so the reveal itself is a clean 0 -> 100%. */
.infos__cue span {
  display: inline-block;
}

.infos__cue.is-cued span {
  animation: cue-breathe 4s ease-in-out infinite;
}

/* The cycle has to START at full, because that is where the reveal leaves the
   cue. Beginning it at the faint end instead makes the first frame of the loop
   snap 100% -> 35%. Same loop either way; only the phase differs. */
@keyframes cue-breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

.infos__copy {
  inset: 0;
  display: grid;
  place-items: center end;
  padding-inline: clamp(24px, 6.8vw, 130px);
}

.infos__text {
  grid-area: 1 / 1;
  margin: 0;
  max-width: 31ch;
  opacity: 0;
  /* The script writes translate3d only while the text is moving and clears it
     to none at rest, so the type is never left sitting on a composited layer
     where it would render soft. */
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(19px, 1.85vw, 34px);
  line-height: 1.52;
  letter-spacing: 0.004em;
  color: #f4f4f4;
}

.infos__text em {
  font-weight: 700;
  font-style: italic;
}

/* ---------- Infos: responsive ---------- */

/* On a portrait screen a centred cover crop would cut the subject out of the
   frame entirely, so bias the framing left and move the copy to the bottom. */
@media (max-width: 860px), (max-aspect-ratio: 1 / 1) {
  .infos__stage {
    background-position: 30% center;
  }

  .infos__video {
    object-position: 30% center;
  }

  .infos__copy {
    place-items: end center;
    padding-inline: clamp(22px, 7vw, 60px);
    padding-bottom: 15vh;
  }

  .infos__text {
    max-width: 34ch;
    font-size: clamp(17px, 4.3vw, 26px);
    line-height: 1.5;
  }

  .infos__cue {
    top: auto;
    bottom: 9vh;
    right: 50%;
    transform: translateX(50%);
  }
}


/* ---------- Entry loader ---------- */

/* Hidden by default. The inline script in the head adds .has-loader only on a
   real arrival, so clicking between pages never flashes it up. */
.loader {
  display: none;
}

/* Deliberately no background of its own. The two curtains and the band between
   them already cover the full height, and they are what the viewer sees; a
   black on this element as well would sit behind them and stay put while they
   drew apart, so the separation would happen against black and be invisible -
   the page would simply appear when this node was removed. */
.has-loader .loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  /* Swallows input while it is up: the page behind must not be clickable
     through the black. */
  pointer-events: auto;
}

/* Two halves of the same black field, meeting at the band. Each retracts
   towards its own outer edge, so the page is uncovered outward from the middle
   rather than simply fading away. */
.loader__curtain {
  flex: 1 1 0%;
  width: 100%;
  background: #000;
  transform: scaleY(1);
  /* A long ease-out rather than the ease-in-out the band uses: it should leave
     briskly and then slow all the way to a stop, so the last of the black
     drifts off the picture instead of arriving at the edge and halting. The
     short delay lets the band finish closing first, which turns the reveal
     into a sequence - the bar shuts, then the black draws back - rather than
     two things happening at once. */
  transition: transform 1.25s cubic-bezier(0.22, 1, 0.36, 1) 0.18s;
}

.loader__curtain--top {
  transform-origin: center top;
}

.loader__curtain--bottom {
  transform-origin: center bottom;
}

.loader.is-open .loader__curtain {
  transform: scaleY(0);
}

/* The band the two halves part at. Black underneath the white, because the
   fill only covers the part it has swept - and the rest of the band would
   otherwise be a transparent slot showing whatever is on the page behind it. */
.loader__band {
  position: relative;
  flex: 0 0 auto;
  width: 100%;
  height: calc(25 * var(--u));
  background: #000;
  overflow: hidden;
  transform: scaleY(1);
  transform-origin: center center;
  transition: transform 0.42s cubic-bezier(0.76, 0, 0.24, 1);
}

.loader.is-open .loader__band {
  transform: scaleY(0);
}

/* Scaled rather than resized, so the sweep runs on the compositor. */
.loader__fill {
  position: absolute;
  inset: 0;
  background: #fff;
  transform: scaleX(0);
  transform-origin: left center;
}

/* The words sit inside the band, on the same left rail as the wordmark.

   They invert against the band rather than choosing a colour: white while the
   black is still showing, and black the moment the white fill sweeps past
   them. The band is its own stacking context - it clips and it scales - so the
   blend sees exactly the two things inside it and nothing else, which is what
   makes the switch land on the sweeping edge itself. */
.loader__label {
  position: absolute;
  left: calc(54.365 * var(--u));
  top: 50%;
  transform: translateY(-50%);
  color: #fff;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(9px, 0.62vw, 12px);
  line-height: 1;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  white-space: nowrap;
  mix-blend-mode: difference;
  pointer-events: none;
}

/* Once the curtains start parting the page underneath belongs to the viewer. */
.loader.is-open {
  pointer-events: none;
}

/* Nothing moves while the loader is up. overflow: hidden alone is not enough -
   the smooth scroller takes the wheel and scrolls the page itself, which is
   programmatic and sails straight past it - so the script also swallows the
   input. This is the belt to that pair of braces. */
.has-loader.is-loading,
.has-loader.is-loading body {
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: none;
}

/* The contact page is laid out to fit one screen exactly and never scrolls. */
.no-scroll,
.no-scroll body {
  overflow: hidden;
  height: 100%;
}

/* While the media explorer is open the page underneath is frozen: the only
   thing that scrolls is the grid inside the window. Lenis is stopped in
   home-gallery.js at the same time - this handles native scroll and any
   momentum that would otherwise leak to the document. */
.home-gallery-open,
.home-gallery-open body {
  overflow: hidden;
  height: 100%;
  overscroll-behavior: none;
  touch-action: none;
}

/* The four links are held back while the scrim is up and fade in as it lifts.
   The wordmark is deliberately not included: it is already standing in the
   middle of the loading screen, and fading it in would mean fading in a word
   that is visibly already there. This is the one difference between it and the
   links either side of it. */
.has-loader .herolinks .nav__link {
  transition: opacity 0.85s cubic-bezier(0.16, 1, 0.3, 1);
}

.has-loader.is-loading .herolinks .nav__link {
  opacity: 0;
  transition: none;
}

/* ---------- The rows on a narrow screen ---------- */

@media (max-width: 700px) {
  .herolinks {
    padding: 0 clamp(10px, 3.2vw, 20px);

    /* The site's type unit bottoms out at 0.5px, so below a 960px screen the
       words stop shrinking while the screen carries on - by 320px the five of
       them are nearly touching. Overriding the unit locally keeps them scaling
       down with the screen, and because both the wordmark and the links are
       sized from it, the whole row shrinks in proportion.

       The 0.5px ceiling is what makes this a no-op from 375px up: it matches
       the global floor exactly, so nothing above that width moves. */
    --u: clamp(0.34px, 100vw / 750, 0.5px);
  }

  /* Only the home page's first screen stacks - it has a whole screen of room
     and no reason to crowd. Every other row stays on one line: the one in the
     statement section, and the one at the top of the interior pages, which is
     a navigation bar and has to behave like one. */
  .herolinks--hero {
    grid-auto-flow: row;
    justify-content: center;
    justify-items: center;
    align-items: center;
    gap: clamp(14px, 2.6vh, 26px);
  }

  /* Stacked, the equal-width trick from the script would pad words out against
     an edge that is no longer there. */
  .herolinks--hero .nav__link {
    width: auto;
    text-align: center;
  }

  /* The row at the top of the interior pages is set from the same unit as the
     type, and that unit is at its floor here - which leaves the links all but
     touching the top edge of a phone. Given its own breathing room instead,
     plus whatever the device reserves for a notch or a status bar. */
  .herolinks--top {
    padding-top: calc(clamp(22px, 4.4vh, 38px) + env(safe-area-inset-top, 0px));
    padding-bottom: clamp(14px, 2.4vh, 22px);
  }
}

/* ---------- Cursor ---------- */

/* Only ever built for a fine pointer; the script bails out on touch before it
   adds either element, and this keeps the styles from applying even if it did. */
@media (any-pointer: fine) and (min-width: 1025px) {
  /* Applied by the script, so a page without JS keeps its ordinary pointer. */
  .has-cursor,
  .has-cursor * {
    cursor: none;
  }

  .cursor {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10000;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.25s linear;
    will-change: transform;
  }

  .cursor.is-awake {
    opacity: 1;
  }

  .cursor-suspended .cursor,
  .cursor-suspended .cursor * {
    visibility: hidden !important;
    transition: none !important;
  }

  .cursor--dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #fff;
    mix-blend-mode: difference;
  }

  /* Difference rather than a fixed colour: the ring reads over the black
     field, the white loading band and the footage without ever picking one. */
  .cursor--ring {
    width: 36px;
    height: 36px;
    border: 1px solid #fff;
    border-radius: 50%;
    mix-blend-mode: difference;
  }

  /* The pill is two elements, and has to be.

     The outer one only ever carries position - the script writes a translate
     into its transform every frame. The inner one carries the growth.

     Keeping them apart is the whole point. The individual transform properties
     (translate, rotate, scale) compose OUTSIDE the transform property, so a
     scale of 0.4 on the same element multiplies the translation as well: a pill
     told to sit at 871px lands at 348px instead, and then travels out to the
     pointer as the scale animates to 1. It reads as the thing flying in from
     the top-left corner of the screen. */
  .cursor--hint {
    z-index: 9999;
    opacity: 1;
    transition: none;
  }

  /* Painted before the dot, so the dot's difference blend inverts to black
     over the white and reads as part of it. */
  .cursor--hint__body {
    display: flex;
    align-items: center;
    padding: 1.05em 1.7em;
    border-radius: 999px;
    background: #fff;
    color: #000;
    font-family: "Helvetica Neue LV", sans-serif;
    font-weight: 700;
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.24em;
    text-indent: 0.24em; /* balances the trailing letter-space, so it sits centred */
    white-space: nowrap;
    opacity: 0;
    transform-origin: center;
    /* Opens from a size rather than from nothing - scaling up from zero reads
       as a pop; from 0.4 it reads as arriving. */
    transform: scale(0.4);
    transition: opacity 0.22s linear, transform 0.46s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .cursor--hint.is-shown .cursor--hint__body {
    opacity: 1;
    transform: scale(1);
  }
}


/* ---------- Statement ---------- */

/* Above the section below it, so the second row of links is genuinely behind
   the film rather than merely dark on dark: anything the statement section
   holds above its own top edge is covered by this. */
.screen {
  position: relative;
  z-index: 2;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: #000;
}

.screen__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
  pointer-events: none;
}

/* The centre of the first screen: the wordmark with two links either side.

   Absolutely placed rather than flexed into the middle, so it is centred on the
   screen itself and takes no part in the layout - the video keeps the whole
   section to itself.

   1fr auto 1fr is what holds the wordmark on the centre line. A plain flex row
   would centre the row as a whole, which puts the wordmark off to one side by
   half the difference between GALLERY WORKS and INFOS CONTACT. */
.herolinks {
  position: absolute;
  inset: 0;
  padding: 0 clamp(20px, 2.83vw, 54px);

  /* A grid rather than a flex row, because this has to do two things at once:
     hold the line on the vertical centre of the screen, and align the five
     words on their shared baseline. align-content centres the single track;
     align-items sits the words on the baseline within it.

     Absolutely no transform here. A transform would make this its own stacking
     context, and the links below blend with what is behind them - inside a
     context of their own there would be nothing to blend against, and the
     inversion would quietly stop working. */
  display: grid;
  grid-auto-flow: column;
  align-content: center;
  align-items: baseline;

  /* space-between is what makes the spacing equal: whatever room is left over
     after the five words is divided into four identical gaps, so the distance
     from each word to the next is the same number at every screen width. The
     two ends land on the gutters. */
  justify-content: space-between;

  /* The box covers the whole screen in order to centre its contents, so it
     must not swallow the pointer everywhere else. */
  pointer-events: none;
}

.herolinks > * {
  pointer-events: auto;
}

/* Compact navigation labels, with a readable minimum on smaller screens. */
.herolinks .nav__link {
  font-size: clamp(10px, 0.8333333333vw, 16px);
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* The inversion is per item rather than on the row as a whole - that is what
   lets one of them opt out while the rest carry on inverting. */
.herolinks .nav__link,
.herolinks .herolinks__logo {
  mix-blend-mode: difference;
}

/* The wordmark on the home page's first screen stays plain white whatever the
   film is doing behind it. Only there: on the contact page, which is white,
   an un-inverted wordmark would be white on white. */
.herolinks--hero .herolinks__logo {
  mix-blend-mode: normal;
}

/* At the top of every page other than the home one, where the header used to
   be - same row, same spacing, pinned to the top rather than centred.

   This row does not invert against what is behind it, and cannot: position
   fixed makes an element its own stacking context on its own, so anything
   blending inside it has no backdrop to blend with and simply paints as it is.
   Blending the row as a whole would work, but then every item in it inverts
   together and the wordmark could not stay white.

   So the colour is stated outright instead - white over the dark pages, black
   over the one light one. The letters of the roll follow, since their clipped
   twin is drawn in currentColor. */
.herolinks--top {
  position: fixed;
  inset: 0 0 auto;
  z-index: 10;
  padding-top: calc(24.752 * var(--u));
  padding-bottom: calc(24.752 * var(--u));
  color: #fff;
}

.theme-light .herolinks--top {
  color: #000;
}

.herolinks--top .nav__link,
.herolinks--top .herolinks__logo {
  color: inherit;
  mix-blend-mode: normal;
}

.herolinks > * {
  white-space: nowrap;
}

/* The wordmark is the third of five, which is what holds it on the centre
   line. It is exact only if the two words on its left measure the same as the
   two on its right, so each flanking pair is set to a shared width - the wider
   of the two, measured once and written to these variables by the script. A
   pair that has not been measured yet simply keeps its natural width, which is
   within a few pixels anyway. */
.herolinks .nav__link:nth-child(1),
.herolinks .nav__link:nth-child(5) {
  width: var(--outer-w, auto);
}

.herolinks .nav__link:nth-child(2),
.herolinks .nav__link:nth-child(4) {
  width: var(--inner-w, auto);
}

/* Each pair is set flush against the outside, so widening one to match the
   other adds its slack on the inside, where the gaps absorb it. */
.herolinks .nav__link:nth-child(1) {
  text-align: left;
}

.herolinks .nav__link:nth-child(5) {
  text-align: right;
}

.herolinks .nav__link:nth-child(2) {
  text-align: right;
}

.herolinks .nav__link:nth-child(4) {
  text-align: left;
}

/* The wordmark keeps its own type; only the header's fixed placement is undone. */
.herolinks__logo {
  position: static;
}

/* Content sits towards the top of the screen rather than centred in it, with
   the call to action carried below on a generous gap. */
.statement {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  /* The rhythm of the column - between the statement, the note that qualifies
     it, the film, and the invitation at the foot.

     Named because two other things are measured from it: the note cancels most
     of it to sit close under the statement, and the call to action takes
     whatever is left after it.

     Four gaps now rather than three, which is why this is half what it was.
     At the old measure the column came to more than the screen had, before the
     film was given a single pixel of it. */
  --stack-gap: clamp(24px, 4.2vh, 64px);
  gap: var(--stack-gap);
  padding: clamp(36px, 6vh, 84px) clamp(24px, 6.8vw, 130px) 10vh;
  background: #000;
}

.statement__text {
  margin: 0;
  max-width: 32em;
  text-align: center;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(14px, 1.22vw, 23px);
  line-height: 1.28;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  color: #fff;
}

/* The small film between the writing and the invitation.

   Sized against the width of the screen rather than set in pixels, with a
   ceiling: it is an aside in the middle of a page of type, and it should read
   as one at any width rather than growing into a second hero. The height
   follows from the frame's own proportions, so nothing is cropped and the
   space it takes is predictable before it has loaded.

   It sits in the column like the paragraphs do, so the section's own gap
   spaces it and the call to action below still takes whatever is left. */
/* A box that takes the room the column has left, and a film that fills it.

   Everything else in this section is as big as it is going to be: the
   statement, the note, the invitation. What is left over is the film's, so the
   box grows into it - large on a tall screen, giving way first on a short one,
   and the section is one screen either way without a number being guessed at.

   It grows from a basis of nothing, which is the only way it can take room
   that is genuinely spare. From a basis of auto it would start at the film's
   own five hundred and forty pixels and shrink only if forced - and the
   section is a minimum height rather than a fixed one, so nothing forces it:
   it simply grew, and took the page past one screen.

   The proportions are the film's own, kept by giving it the box's full height
   and letting its width follow. An aspect-ratio here instead fought the
   flexing - on a phone the two constraints settled on a box that was not the
   right shape at all. */
.statement__reel {
  flex: 1 1 0;
  width: 100%;
  min-height: clamp(92px, 13vh, 150px);
  /* Never taller than the width on offer can carry at the film own shape.
     Without the second term the box keeps growing, the film width runs into its
     own limit, and what is left is a crop - on a phone a sixteen-by-nine frame
     was being shown as a six-by-five one, with the sides simply gone. */
  max-height: min(clamp(190px, 34vh, 420px), calc(88vw * 9 / 16));
  display: flex;
  align-items: center;
  justify-content: center;
}

.statement__film {
  display: block;
  height: 100%;
  width: auto;
  /* Where the screen is narrower than the film is wide, the width wins and the
     frame is cropped rather than squeezed out of shape. */
  max-width: 88vw;
  object-fit: cover;
  background: #0b0b0b;
  pointer-events: none;
}

/* A phone is tall and narrow, so there is height to spare where there is no
   width. Let the film grow further into it than a monitor would - the width is
   what will stop it first there anyway. */
@media (max-width: 700px) {
  .statement__reel {
    max-height: min(46vh, calc(88vw * 9 / 16));
  }
}

/* ---------- Home gallery: phone media library ---------- */

@media (max-width: 760px), (hover: none) {
  .home-gallery::before {
    background: rgba(4, 4, 4, 0.42);
    -webkit-backdrop-filter: blur(16px) saturate(0.8);
    backdrop-filter: blur(16px) saturate(0.8);
  }

  .home-gallery__window {
    left: 0;
    top: 0;
    width: 100vw;
    height: 100svh;
    border: 0;
    border-radius: 0;
    background: rgba(8, 8, 8, 0.72);
    transform: translateY(18px);
  }

  .home-gallery.is-open .home-gallery__window {
    transform: translateY(0);
  }

  .home-gallery__bar {
    height: 62px;
    padding: 18px 16px 10px;
    background: transparent;
  }

  .home-gallery__lights,
  .home-gallery__controls button:not(.home-gallery__close),
  .home-gallery__sidebar,
  .home-gallery__crumb,
  .home-gallery__status { display: none; }

  .home-gallery__bar {
    position: static;
    height: 0;
    padding: 0;
    border: 0;
    background: transparent;
  }

  .home-gallery__title {
    color: #fff;
    font-size: 20px;
    letter-spacing: 0;
  }

  .home-gallery__body,
  .home-gallery__content {
    display: block;
    height: 100%;
  }

  .home-gallery__content {
    padding: 0 2px 24px;
  }

  .home-gallery__grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 2px;
    height: 100%;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-y;
    padding: 0;
  }

  .home-gallery__item {
    aspect-ratio: 1;
    border: 0;
    border-radius: 0;
  }

  .home-gallery__close {
    position: fixed;
    top: 10px;
    right: 10px;
    z-index: 5;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
  }
}

/* The note between the statement and the call to action. Set small on purpose
   and in the light weight: it is there to be read by whoever wants it, not to
   compete with the statement above, which is why it is sentence case against
   that one's bold uppercase.

   Set close under the statement rather than at the section's own gap, which is
   sized for the distance between the big blocks. This belongs to the statement
   - it is the same thought at a quieter volume - so it is pulled up against it
   by cancelling most of that gap and leaving a line's worth behind. */
.statement__note {
  margin: calc(clamp(14px, 2.4vh, 32px) - var(--stack-gap)) 0 0;
  max-width: 46em;
  text-align: center;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(10px, 0.66vw, 13px);
  line-height: 1.62;
  letter-spacing: 0.012em;
  color: #fff;
}

/* ---------- Text reveal ---------- */

/* The line is the mask. Each character sits a full line below it until called
   up, so nothing is visible outside the run of text itself. */
.rv__line {
  display: block;
  overflow: hidden;
}

.rv__w {
  display: inline-block;
  white-space: nowrap;
}

/* No transition: the script writes the transform straight from the scroll
   position every frame. A transition here would fight it, adding a lag that
   made stopping mid-scroll drift on instead of holding still. The starting
   offset matches the script's, so nothing jumps on the first frame. */
/* A 2D translate on purpose: translate3d promotes every one of ~150 spans to
   its own compositor layer, which a phone feels immediately. */
.rv__c {
  display: inline-block;
  transform: translateY(105%);
}


/* ---------- Call to action ---------- */

.cta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 3.4vw, 66px);
  color: #fff;
  text-decoration: none;
  max-width: 100%;

  /* Held at the foot of the section, and this does not starve the film above
     it: flex resolves growing first and hands an auto margin only what is left
     over, so the film takes its room and this takes the remainder.

     It matters wherever the film has hit its ceiling rather than the column's
     end - on a tall phone it stops well short of filling the column, and
     without this the whole thing bunched at the top with the invitation
     stranded halfway up the screen. */
  margin-top: auto;
}

/* Four concentric arcs a side, the right set simply turned around. They are
   stacked in one box and lit in sequence, so a pulse travels outward away from
   the words and, at the trough, only the widest one is left. */

.cta__body {
  display: block;
  min-width: 0;
  text-align: center;
}

.cta__small {
  display: block;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(28px, 3.6vw, 68px);
  line-height: 1.1;
  letter-spacing: 0.002em;
  margin-bottom: clamp(6px, 0.9vw, 18px);
}

/* Both phrases sit here at once, stacked on top of each other. This box is
   also the wall: it clips to a single line, so a letter moved beyond its edge
   is genuinely gone rather than merely transparent. */
.cta__rotator {
  position: relative;
  display: block;
  overflow: hidden;
  height: 1em;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(38px, 5.8vw, 110px);
  line-height: 1;
  letter-spacing: -0.012em;
}

.cta__phrase {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: pre;
}

/* Movement only - no opacity anywhere. A letter is hidden because it has been
   pushed past the edge of the box above, not because it faded; it slides out
   from behind the line and slides back under it. Each carries its own delay,
   rolled fresh on every swap, and that scatter is the whole effect. */
.cta__l {
  display: inline-block;
  transform: translateY(var(--y, 1.2em));
  transition: transform 0.62s cubic-bezier(0.33, 0, 0.15, 1);
  transition-delay: var(--d, 0s);
}

.cta__phrase.is-on .cta__l {
  transform: translateY(0);
}

/* Used for the one frame in which a waiting phrase is moved back above the
   line, so the reset never animates. */
.cta__phrase.is-instant .cta__l {
  transition: none;
}

/* ---------- Contact ---------- */

/* The one light page on the site. Set from a class on <html> written by the
   head script rather than from a rule here, so the white is there in the very
   first painted frame - the header inverts whatever is behind it, and a page
   that started black would flash the wordmark from white to black. */
.theme-light,
.theme-light body {
  background: #fff;
}

.contact {
  /* Taken from the reference: a hairline that darkens on approach, and a muted
     label ink that resolves to black. */
  --c-line: rgba(255, 255, 255, 0.35);
  --c-line-on: #fff;
  --c-mute: rgba(255, 255, 255, 0.62);
  --c-ink: #fff;

  min-height: 100vh;
  min-height: 100svh;
  color: var(--c-ink);
  background: #000;
}

/* The form is the page: the two columns sit inside it and the send bar is its
   last child, so the bar is page-width by construction. Holding the horizontal
   padding on the columns rather than on the page is what lets it be - a bar
   inside a padded grid cell can only ever reach the edge by guessing at the
   offset it has to cancel. */
.cform {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
}

/* Two equal tracks, a single gutter between them, and the whole block centred
   in the page. Equal tracks either side of one gap is what puts the two columns
   the same distance from the middle of the screen; the auto side margins on a
   capped width, with the same padding left and right, is what leaves the same
   air outside them. Nothing inside is nudged with left, top or a transform -
   every position on this page comes from the grid. */
.contact__cols {
  flex-grow: 1;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(40px, 6vw, 120px);
  width: min(100%, 1280px);
  margin-inline: auto;
  align-items: start;
  /* The columns stretch to the height of the screen, and free space handed to
     the rows would push the last of them - the send button - to the bottom of
     the page. The rows are packed to the top instead, so the button stays where
     the form leaves it. */
  align-content: start;
  padding: clamp(150px, 16vh, 220px) clamp(20px, 4vw, 64px) clamp(30px, 4vh, 60px);
}

/* ---- the column of details ---- */

/* The left half of the pair. It fills its track rather than being sized and
   then pushed, and it starts on the same row as the form so the two tops line
   up without either being offset. */
.contact__aside {
  grid-column: 1;
  grid-row: 2;
  align-self: start;
  justify-self: start;
  width: 100%;
  margin: 0;
  padding: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(18px, 2.2vh, 30px);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(11px, 0.73vw, 13px);
  line-height: 1.5;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: left;
}

.contact__eyebrow {
  margin: 0 0 clamp(10px, 1.8vh, 22px);
  max-width: none;
  font-weight: 700;
  line-height: 1.15;
  align-self: center;
}

/* The column's own gap does the separating; this is only the small extra that
   sets the address apart from the list of links above it. */
.contact__mail {
  margin-top: clamp(6px, 1.2vh, 16px);
  color: var(--c-ink);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(16px, 1.25vw, 24px);
  text-decoration: none;
  transition: opacity 0.28s ease-in-out;
}

.contact__mail:hover { opacity: 0.58; }

/* Transparent to the grid, so the field block, the note and the send row are
   each placed in the right-hand track themselves rather than inheriting a
   wrapper's width and then being nudged back into line. */
.contact__body {
  display: contents;
}

.contact__portrait {
  width: clamp(170px, 25vw, 360px);
  aspect-ratio: 3 / 4;
  margin: 0 auto clamp(22px, 3.5vh, 42px);
  overflow: hidden;
  background: #111;
}

.contact__portrait img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
  filter: grayscale(1) contrast(1.08);
}

.contact__list {
  margin: 0;
  display: flex;
  flex-direction: column;
}

.contact__list dt {
  margin-top: 18px;
  font-weight: 700;
}

/* The small dot the reference sets between one block of details and the next. */
.contact__list dt::before {
  content: "";
  display: block;
  width: 5px;
  height: 5px;
  margin-bottom: 18px;
  border-radius: 50%;
  background: currentColor;
}

.contact__list dt:first-child {
  margin-top: 0;
}

.contact__list dt:first-child::before {
  display: none;
}

.contact__list dd {
  margin: 6px 0 0;
}

.contact__list a,
.contact__social a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 0.28s ease-in-out;
}

.contact__list a:hover,
.contact__social a:hover {
  border-bottom-color: currentColor;
}

.contact__social {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.contact__social-title {
  display: block;
  margin-bottom: 12px;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(22px, 1.8vw, 30px);
  line-height: 1.1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.contact__icon {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: auto;
  height: 28px;
  border-bottom: 0;
  opacity: 0.85;
  transition: opacity 0.28s ease-in-out;
}

.contact__icon:hover {
  /* An underline under an icon reads as a mistake, so this one answers with
     its own weight instead. */
  border-bottom: 0;
  opacity: 1;
}

.contact__icon svg {
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  display: block;
}

.contact__icon span {
  font-family: "Helvetica Neue LV", sans-serif;
  font-size: clamp(11px, 0.73vw, 13px);
  font-weight: 300;
  letter-spacing: 0.02em;
  text-transform: none;
}

.contact__social ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* ---- the heading ---- */

/* Spans both tracks and is centred within that span. Because the span is the
   whole centred block and the block's side padding is the same on both sides,
   the middle of the heading is the middle of the viewport - it is not centred
   on either column. */
.contact__title {
  grid-column: 1 / -1;
  grid-row: 1;
  justify-self: stretch;
  margin: 0 0 clamp(82px, 10vh, 132px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  line-height: 0.83;
  color: #fff;
  text-align: center;
}

.contact__question {
  max-width: 18ch;
  margin: 0 0 clamp(18px, 2.5vh, 32px);
  font-family: "Helvetica Neue LV", sans-serif;
  font-size: clamp(34px, 3.7vw, 68px);
  font-weight: 700;
  line-height: 0.96;
  letter-spacing: -0.02em;
  color: #fff;
  align-self: flex-start;
}

.contact__intro {
  max-width: 42ch;
  margin: 0 0 clamp(20px, 3vh, 38px);
  color: var(--c-mute);
  font-family: "Helvetica Neue LV", sans-serif;
  font-size: clamp(13px, 0.95vw, 17px);
  font-weight: 300;
  line-height: 1.35;
  align-self: flex-start;
}

.contact__title-a {
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(46px, 5.5vw, 98px);
  letter-spacing: -0.01em;
}

.contact__title-b {
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(46px, 5.5vw, 98px);
  letter-spacing: -0.01em;
}

/* ---- the form ---- */

/* The right half of the pair: the three fields in one stack, filling the track
   it is given. The width is the track's width, which is why the three
   underlines below - name, email, message - and the send row are all exactly
   as long as each other at every screen size. */
.cform__grid {
  grid-column: 2;
  grid-row: 2;
  width: 100%;
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  /* Held back until the page has settled; the delay lives on the ready state. */
  opacity: 0;
  transition: opacity 0.28s ease-in-out;
}

.is-form-ready .cform__grid {
  opacity: 1;
  /* The pause before the fields arrive is the whole of the entrance: the page
     is quiet for a beat first. */
  transition-delay: 1s;
}

.cform__col {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* The field. Everything about the interaction happens on this box and its
   label, on one shared 0.28s ease-in-out: the border darkens, the label
   darkens, and the label shrinks out of the way of what is being typed. */
.cform__item {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 14px 0 12px;
  border: 0;
  border-bottom: 1px solid var(--c-line);
  border-radius: 0;
  transition: 0.28s ease-in-out;
}

.cform__item:hover {
  border-color: var(--c-line-on);
}

.cform__item:hover .cform__label,
.cform__item:hover .cform__sub-label,
.cform__item:hover .cform__file-text {
  color: var(--c-ink);
}

/* Written by the script: focused, or holding something. */
.cform__item.is-active,
.cform__item.is-filled {
  border-color: var(--c-line-on);
}

.cform__item.is-active .cform__label,
.cform__item.is-filled .cform__label {
  color: var(--c-ink);
}

/* The shrink - only on the boxes whose label sits over the typing area. The
   ones with a fixed label at the top have nothing to move out of the way of. */
.cform__item.is-active:not(.cform__item--fixed) .cform__label,
.cform__item.is-filled:not(.cform__item--fixed) .cform__label {
  font-size: clamp(10px, 0.73vw, 14px);
}

.cform__label {
  position: static;
  display: block;
  margin: 0 0 8px;
  color: var(--c-mute);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(14px, 1.146vw, 22px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  transition: 0.28s ease-in-out;
  pointer-events: none;
}

.cform__label--fixed {
  position: static;
  margin-bottom: auto;
}

/* What is typed is set large in the display serif, against the small upright
   labels - the pairing the reference uses throughout. */
.cform__input {
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  outline: 0;
  background: none;
  color: var(--c-ink);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 400;
  font-size: clamp(13px, 0.95vw, 17px);
  line-height: 0.95;
}

.cform__input::placeholder {
  color: rgba(255, 255, 255, 0.38);
}

.cform__input--area {
  min-height: 110px;
  max-height: 180px;
  overflow-y: auto;
  resize: none;
  line-height: 1.05;
}

.cform__item--area {
  min-height: 140px;
}

.cform__item--fixed {
  justify-content: flex-end;
  padding-top: 20px;
}

.cform__row {
  margin-top: 14px;
}

.cform__row .cform__input {
  font-size: clamp(20px, 1.9vw, 36px);
}

/* The small upright annotations: the site field under the company name, and
   the two labels inside the contact box. */
.cform__sub {
  margin-top: auto;
  display: flex;
  align-items: baseline;
  gap: 6px;
}

.cform__sub-label {
  display: block;
  color: var(--c-mute);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(11px, 0.73vw, 13px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color 0.28s ease-in-out;
}

.cform__sub-input {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  outline: 0;
  background: none;
  color: var(--c-ink);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(11px, 0.73vw, 13px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* ---- the upload ---- */

.cform__file {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.cform__file input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  clip-path: circle(0);
}

.cform__file-text {
  color: var(--c-mute);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(11px, 0.73vw, 13px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: color 0.28s ease-in-out;
}

.cform__file:hover .cform__file-text,
.cform__item.is-filled .cform__file-text {
  color: var(--c-ink);
}

/* Focus has to stay visible on a control whose input is hidden. */
.cform__file input:focus-visible + .cform__file-text {
  outline: 1px solid var(--c-ink);
  outline-offset: 4px;
}

/* ---- something went wrong ---- */

/* The reference turns the whole box a flat pink rather than colouring an
   edge, which is far harder to miss on a white page. */
.cform__item.is-error {
  background: #e6b0b0;
  border-color: var(--c-line-on);
}

.cform__item.is-error .cform__label,
.cform__item.is-error .cform__sub-label {
  color: var(--c-ink);
}

.cform__item.is-error .cform__input {
  color: #7d4a4a;
}

/* ---- agreement and send ---- */

/* The status line, under the fields and in the same track. It is empty until
   the form has something to say, and an empty row with a margin would hold the
   send button away from the message underline for no reason - so the margin is
   only there once there is text in it. */
.cform__foot {
  grid-column: 2;
  grid-row: 3;
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px clamp(16px, 2vw, 40px);
}

.cform__check {
  position: relative;
  display: flex;
  align-items: center;
}

.cform__check input {
  position: absolute;
  margin: 0;
  padding: 0;
  font-size: 0;
  opacity: 0;
  clip-path: circle(0);
}

.cform__check label {
  position: relative;
  padding-left: 34px;
  color: var(--c-ink);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(11px, 0.73vw, 13px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
}

/* Two stacked squares: the empty one, and the filled one that springs up over
   it when the box is ticked. */
.cform__check label::before,
.cform__check label::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 20px;
  height: 20px;
  margin-top: -10px;
  border: 1px solid var(--c-ink);
  border-radius: 6px;
  background: #000;
}

.cform__check label::before {
  z-index: 2;
  transition: background-color 0.3s, border-color 0.3s;
}

.cform__check label::after {
  z-index: 3;
  border-color: transparent;
  background-color: #000;
  background-image: url("../pics/check.svg");
  background-position: center center;
  background-size: 60%;
  background-repeat: no-repeat;
  opacity: 0;
  transform: scale(0);
  transition: transform 0.3s, opacity 0.3s;
}

.cform__check input:checked + label::after {
  opacity: 1;
  transform: scale(1);
}

.cform__check input:focus-visible + label::before {
  outline: 1px solid var(--c-ink);
  outline-offset: 3px;
}

.cform__check.is-error label::before {
  background: #e6b0b0;
}

.cform__note {
  margin: 0;
  color: var(--c-ink);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 700;
  font-size: clamp(11px, 0.73vw, 13px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* The paragraph is empty until the script writes into it, and an empty block
   takes no height - so the air only has to be there once there are words to
   hold apart from the send button. */
.cform__note:not(:empty) {
  margin-top: clamp(18px, 2.4vh, 30px);
}

/* Same track and same width as the fields above it, which is what puts the
   button's right edge exactly on the right end of the message underline. */
.cform__submit-wrap {
  grid-column: 2;
  grid-row: 4;
  width: 100%;
  display: flex;
  justify-content: flex-end;
}

.cform__submit {
  width: auto;
  margin: clamp(22px, 3vh, 34px) 0 0;
  padding: 10px 0 8px;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.42);
  background: transparent;
  color: #fff;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(12px, 0.95vw, 17px);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity 0.28s ease-in-out;
}

.cform__submit:hover {
  border-bottom-color: #fff;
  opacity: 0.62;
}

.cform__submit[disabled] {
  opacity: 0.45;
  cursor: default;
}

/* ---- narrow screens ---- */

/* Two columns side by side need room for two; below that they become one
   column read top to bottom. The pair stops being a grid and becomes a single
   stack, every block in it full width, with the order set rather than the
   position - so the spacing stays the one gap between neighbours and nothing
   has to be un-placed. */
@media (max-width: 860px) {
  .contact__cols {
    display: flex;
    flex-direction: column;
    gap: clamp(40px, 6vh, 70px);
    width: 100%;
  }

  /* The heading still reads first, then the form, then the details - which is
     the same order as the columns, read left to right. */
  .contact__title {
    order: 0;
    margin-bottom: 0;
  }

  .contact__body {
    order: 1;
    display: block;
    width: 100%;
  }

  /* The details read as a footer once there is no room for a column. */
  .contact__aside {
    order: 2;
    width: 100%;
  }

  .cform__grid,
  .cform__foot,
  .cform__submit-wrap {
    width: 100%;
    grid-column: auto;
    grid-row: auto;
  }
}

/* One line at every width, phones included. There is just enough room: at the
   narrow end the unit that drives the type is already at its floor, so the five
   words stop shrinking while the screen keeps narrowing - which is why the
   gutter closes up here to give the gaps something to work with. */
@media (max-width: 700px) {
  .herolinks {
    padding: 0 clamp(10px, 3.2vw, 20px);

    /* The site's type unit bottoms out at 0.5px, so below a 960px screen the
       words stop shrinking while the screen carries on - by 320px the five of
       them are nearly touching. Overriding the unit locally keeps them scaling
       down with the screen, and because both the wordmark and the links are
       sized from it, the whole row shrinks in proportion.

       The 0.5px ceiling is what makes this a no-op from 375px up: it matches
       the global floor exactly, so nothing above that width moves. */
    --u: clamp(0.34px, 100vw / 750, 0.5px);
  }

  /* Only the home page's first screen stacks - it has a whole screen of room
     and no reason to crowd. Every other row stays on one line: the one in the
     statement section, and the one at the top of the interior pages, which is
     a navigation bar and has to behave like one. */
  .herolinks--hero {
    grid-auto-flow: row;
    justify-content: center;
    justify-items: center;
    align-items: center;
    gap: clamp(14px, 2.6vh, 26px);
  }

  /* Stacked, the equal-width trick from the script would pad words out against
     an edge that is no longer there. */
  .herolinks--hero .nav__link {
    width: auto;
    text-align: center;
  }

  /* The row at the top of the interior pages is set from the same unit as the
     type, and that unit is at its floor here - which leaves the links all but
     touching the top edge of a phone. Given its own breathing room instead,
     plus whatever the device reserves for a notch or a status bar. */
  .herolinks--top {
    padding-top: calc(clamp(22px, 4.4vh, 38px) + env(safe-area-inset-top, 0px));
    padding-bottom: clamp(14px, 2.4vh, 22px);
  }
}

/* The second row, at the top of the statement section. Full width so it lines
   up with the one on the first screen - it is wider than the section's text
   column, and centring a wider child overflows it evenly on both sides, which
   is exactly full bleed. Its resting place is here; the script holds it above
   this until the section is scrolled up to. */
.herolinks--stack {
  position: static;
  transform: none;
  width: 100vw;
  flex: 0 0 auto;
  will-change: transform;
}

/* Sits on the flat black of the section, where inverting white against black
   only gives white back. */
.herolinks--stack .nav__link,
.herolinks--stack .herolinks__logo {
  mix-blend-mode: normal;
}

/* ---------- Contact: made to fit, because it does not scroll ---------- */

/* With scrolling off, anything past the fold is simply lost - so the page is
   sized to the viewport instead of assuming it. The form becomes exactly one
   screen tall and the parts that can give way do: the field boxes are empty
   containers whose height is a matter of proportion, so they take whatever is
   left after the type, rather than insisting on a minimum and pushing the
   send bar off the bottom. */
.no-scroll .cform {
  height: 100vh;
  height: 100svh;
  min-height: 0;
}

.no-scroll .contact__cols,
.no-scroll .contact__body,
.no-scroll .cform__grid,
.no-scroll .cform__col {
  min-height: 0;
}

.no-scroll .cform__item,
.no-scroll .cform__item--area {
  min-height: 0;
}

/* A phone has the least room of all, and the column layout stacks the details
   under the form rather than beside it - so the spacing is tightened across
   the board to buy back the height. */
@media (max-width: 860px) {
  .no-scroll .contact__cols {
    padding-top: clamp(64px, 8vh, 88px);
    padding-bottom: clamp(12px, 1.6vh, 20px);
    gap: clamp(12px, 1.6vh, 20px);
  }

  /* The stack's one gap does the separating, so tightening it is the whole of
     the adjustment - the blocks themselves keep no margins to argue with. */
  .no-scroll .cform__note:not(:empty) {
    margin-top: clamp(10px, 1.4vh, 16px);
  }

  .no-scroll .contact__aside {
    gap: clamp(10px, 1.4vh, 16px);
  }

  .no-scroll .contact__list dt {
    margin-top: 10px;
  }

  .no-scroll .contact__list dt::before {
    margin-bottom: 10px;
  }

  .no-scroll .contact__social-title {
    margin-bottom: 7px;
  }
}

/* ---------- Gallery: a desk, and what is scattered on it ---------- */

/* The wall behind everything. Fixed rather than scrolled with the icons: on a
   narrow screen the desk grows past one screen and scrolls, and a photograph
   sliding up behind the icons would read as a page rather than a surface. */
.desk {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  background: #000;
  overflow: hidden;
}

.desk__wall {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  pointer-events: none;
  user-select: none;
}

/* Held back until the script has dealt the positions. Without this every icon
   is drawn stacked in the corner for a frame and then jumps to its place. */
.desk__icons {
  position: absolute;
  inset: 0;
  z-index: 1;
  margin: 0;
  padding: 0;
  list-style: none;
  opacity: 0;
  transition: opacity 0.5s ease;
}

.desk.is-ready .desk__icons {
  opacity: 1;
}

/* ---------- Gallery: one icon ---------- */

.icon {
  position: absolute;
  top: 0;
  left: 0;
  width: clamp(62px, 5.4vw, 86px);

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  padding: 7px 5px 6px;
  border-radius: 4px;

  cursor: grab;
  /* The browser must not claim the gesture: a drag here is ours, and a scroll
     that starts on an icon would otherwise take it away mid-movement. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;

  /* Position is written by the script every frame of a drag. Nothing else may
     set a transform here or the two would fight. */
  will-change: transform;
  transition: background-color 0.2s ease;
}

.icon:hover,
.icon:focus-visible {
  background: rgba(255, 255, 255, 0.11);
  outline: none;
}

.icon:focus-visible {
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5);
}

.icon.is-dragging {
  cursor: grabbing;
  background: rgba(255, 255, 255, 0.16);
  /* No transition while it is being dragged: the transform is already being
     written once a frame, and easing it as well would put the icon behind the
     hand holding it. */
  transition: none;
}

.icon__shot {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: 2px;
  background: #0c0c0c;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.55);
}

.icon__shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.icon__name {
  display: none;
  max-width: 100%;
  color: #fff;
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(7px, 0.46vw, 9px);
  line-height: 1.35;
  letter-spacing: 0.06em;
  text-align: center;
  /* A name too long for the icon trails off rather than wrapping and pushing
     its neighbours out of line. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Legible over any part of the photograph behind it. */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 8px rgba(0, 0, 0, 0.6);
}

.home-gallery__media-name,
[data-media-name] {
  display: none;
}

/* ---------- Gallery: the file explorer ----------

   The panel the thirty icons are listed in. It is a window like any other on
   the site - js/winbox.js gives it the dragging, the eight edges, the fill and
   the put-away - so what is written here is only its look. */

/* Fixed, not absolute, for two reasons: the script places it in viewport
   coordinates, and .desk both scrolls and clips - an absolute panel would be
   measured against the desk and then cut off by its overflow the moment it was
   dragged near an edge. A fixed box is not clipped by a plain positioned
   ancestor, so it can be moved anywhere on the screen. */
.explorer {
  position: fixed;
  left: 50%;
  top: 50%;
  /* Under .winlayer (20): the pictures opened out of the folder belong in
     front of the folder they came from. */
  z-index: 10;
  width: min(1080px, calc(100vw - 48px));
  height: min(680px, calc(100vh - 88px));
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 14px;
  background: rgba(18, 18, 18, 0.72);
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08);
  -webkit-backdrop-filter: blur(22px) saturate(1.1);
  backdrop-filter: blur(22px) saturate(1.1);
  transform: translate(-50%, -50%);
  /* A window is furniture, not prose: nothing in the chrome is text to select,
     and a drag across it would otherwise leave a trail of highlight. */
  user-select: none;
  -webkit-user-select: none;
}

.explorer__bar {
  position: relative;
  z-index: 2;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 14px;
  height: 44px;
  padding: 0 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.07);
  cursor: grab;
}

.explorer.is-winbox-moving .explorer__bar {
  cursor: grabbing;
}

.explorer__lights {
  display: flex;
  gap: 6px;
}

.explorer__lights span {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.42);
}

.explorer__title,
.explorer__status,
.explorer__section-label,
.explorer__meta,
.explorer__path,
.explorer__place,
.explorer__footer {
  font-family: "Helvetica Neue LV", sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.explorer__title {
  flex: 1;
  color: rgba(255, 255, 255, 0.82);
  font-size: 10px;
}

.explorer__status {
  color: rgba(255, 255, 255, 0.4);
  font-size: 9px;
}

.explorer__controls {
  display: flex;
  gap: 4px;
}

.explorer__controls button {
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 7px;
  background: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.78);
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.18s ease;
}

.explorer__controls button:hover,
.explorer__controls button:focus-visible {
  background: rgba(255, 255, 255, 0.2);
  outline: none;
}

/* min-height:0 on both: a grid cell and a flex child each refuse to shrink
   below their content by default, which would let the icon list push the
   window taller instead of scrolling inside it. */
.explorer__body {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: 170px 1fr;
}

.explorer__sidebar {
  display: flex;
  flex-direction: column;
  gap: 7px;
  overflow: hidden;
  padding: 20px 14px;
  border-right: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.03);
}

.explorer__section-label {
  color: rgba(255, 255, 255, 0.34);
  font-size: 8px;
}

.explorer__section-label--lower {
  margin-top: 14px;
}

.explorer__place {
  border-radius: 6px;
  padding: 6px 8px;
  color: rgba(255, 255, 255, 0.62);
  font-size: 10px;
  text-decoration: none;
  transition: background-color 0.18s ease, color 0.18s ease;
}

.explorer__place:hover,
.explorer__place.is-current {
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
}

.explorer__meta {
  color: rgba(255, 255, 255, 0.34);
  font-size: 8px;
}

.explorer__content {
  display: flex;
  min-width: 0;
  min-height: 0;
  flex-direction: column;
  padding: 18px 18px 14px;
}

.explorer__path {
  display: flex;
  gap: 6px;
  color: rgba(255, 255, 255, 0.4);
  font-size: 9px;
}

.explorer__path strong {
  color: rgba(255, 255, 255, 0.88);
  font-weight: 300;
}

.explorer__rule {
  margin: 12px 0;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

/* The icon list, inside the explorer. On the desk these are scattered by
   transform; here they are a plain grid and the script leaves them alone. */
.desk[data-explorer] .desk__icons {
  position: static;
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  align-content: start;
  gap: 14px 10px;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 2px;
  /* Keep the scroll and its momentum to the list rather than chaining it to
     the desk behind. */
  overscroll-behavior: contain;
  /* The list scrolls, but the bar itself is never drawn. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.desk[data-explorer] .desk__icons::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

/* Laid out by the grid, not placed by the script - so the absolute positioning
   the scattered desk uses has to come off, and with it the grab cursor: a file
   in a list is opened, not carried. */
.desk[data-explorer] .icon {
  position: static;
  width: auto;
  cursor: pointer;
}

.explorer__footer {
  flex: 0 0 auto;
  display: flex;
  justify-content: space-between;
  padding-top: 12px;
  color: rgba(255, 255, 255, 0.3);
  font-size: 8px;
}

/* A phone has no room for a window and nothing to drag it to: the panel gives
   up its chrome and becomes the page. */
@media (max-width: 760px), (hover: none) {
  html.gallery-page,
  html.gallery-page body {
    height: auto;
    min-height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior-y: auto;
    touch-action: pan-y;
  }

  .desk[data-explorer] {
    background: #000;
    overflow: visible;
  }

  .desk[data-explorer] .desk__wall {
    display: none;
  }

  .explorer {
    position: static;
    width: auto;
    height: auto;
    min-height: 100svh;
    border: 0;
    border-radius: 0;
    transform: none;
    background: #000;
    box-shadow: none;
  }

  .explorer__bar {
    height: 64px;
    padding: 18px 16px 10px;
    border-bottom: 0;
    background: #000;
    cursor: default;
  }

  .explorer__lights,
  .explorer__controls,
  .explorer__sidebar,
  .explorer__path,
  .explorer__rule,
  .explorer__footer { display: none; }

  .explorer__title {
    color: #fff;
    font-size: 21px;
    letter-spacing: 0;
  }

  .explorer__status {
    margin-left: auto;
    color: rgba(255, 255, 255, 0.5);
    font-size: 9px;
  }

  .explorer__body,
  .explorer__content {
    display: block;
    height: auto;
    min-height: 0;
  }

  .explorer__content {
    padding: 0 3px 28px;
  }

  .desk[data-explorer] .desk__icons {
    position: static;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 2px;
    width: 100%;
    overflow: visible;
    padding: 0;
    padding-bottom: 40px;
    touch-action: pan-y;
    overscroll-behavior-y: contain;
  }

  .desk[data-explorer] .icon {
    position: static;
    width: auto;
    min-width: 0;
    display: block;
    padding: 0;
    border-radius: 0;
    cursor: pointer;
  }

  .desk[data-explorer] .icon__shot {
    width: 100%;
    aspect-ratio: 1;
    border-radius: 0;
    box-shadow: none;
  }
}

/* ---------- Gallery: the viewer ---------- */

/* The desk is never locked now. Windows float over it, so the icons behind
   them stay reachable and the page still scrolls where it needs to. */
/* A finger, not a pointer. Icons are not dragged here - they are only opened -
   so the browser keeps the gesture and the desk scrolls from anywhere on it,
   including from an icon. Handing every touch to the script instead meant a
   swipe that began on an icon moved the icon rather than the page, and with
   thirty of them there was little left to scroll from. */
@media (hover: none) {
  .icon {
    width: clamp(78px, 24vw, 108px);
    touch-action: auto;
    cursor: pointer;
  }
}
/* ---------- Gallery: the blur behind an open picture ---------- */

/* Above the wall and the icons, below anything opened over them. What it
   softens is therefore exactly the background: put on the desk itself the
   filter would take the windows down with it, since they are inside it.

   It also stays below the row of links, which keeps its way out of the page
   sharp and legible, and it catches no pointer at all - the icons behind it
   are still there to be clicked between the windows. */
.haze {
  position: fixed;
  inset: 0;
  z-index: 9;
  pointer-events: none;

  background: rgba(0, 0, 0, 0.34);
  -webkit-backdrop-filter: blur(16px) saturate(0.85);
  backdrop-filter: blur(16px) saturate(0.85);

  opacity: 0;
  transition: opacity 0.34s ease;
}

.haze.is-on {
  opacity: 1;
}

/* ---------- Gallery: a picture on a phone ---------- */

/* display below would override the browser's own rule for the hidden
   attribute, so this puts it back. Without it the sheet is never hidden at
   all - a full-screen layer, invisible only because its opacity is zero,
   swallowing every tap meant for an icon. */
.sheet[hidden] {
  display: none;
}

.sheet {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  /* Barely anything: the blur beneath is what separates the picture from the
     desk, and painting solid black over it would hide the very thing that
     was asked to show through. */
  background: rgba(0, 0, 0, 0.28);
  opacity: 0;
  transition: opacity 0.3s ease;
  /* The gesture is ours: the page must not take it for a scroll partway
     through a drag. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.sheet.is-open {
  opacity: 1;
}

.sheet__img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  will-change: transform;
  -webkit-user-drag: none;
  scale: 0.97;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
    scale 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.sheet.is-open .sheet__img {
  scale: 1;
}

/* While a finger is on it the picture goes exactly where the finger goes -
   an eased picture is a picture lagging behind the hand. */
.sheet.is-shoving .sheet__img {
  transition: none;
}

.sheet.is-shoving {
  transition: none;
}

/* Nothing behind it moves while a picture is up. */
.peeking,
.peeking body {
  overflow: hidden;
}

/* ---------- Gallery: the windows ---------- */

/* The layer they live on. It does not catch anything itself - the desk beneath
   has to stay reachable between the windows - so only the windows do. */
.winlayer {
  position: fixed;
  inset: 0;
  z-index: 20;
  pointer-events: none;
}

.win {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: auto;

  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 10px;
  background: #171717;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.62), 0 0 0 1px rgba(255, 255, 255, 0.07);

  /* Nothing in a window is text to be selected, and a drag that crosses one
     would otherwise leave a trail of highlight behind it. */
  user-select: none;
  -webkit-user-select: none;

  /* Position is a transform and size is width and height. Moving a window is
     then work the compositor can do on its own, and only resizing - which
     really does change the box - costs a layout. */
  will-change: transform;

  opacity: 0;
  transform-origin: 50% 0;
  scale: 0.96;
  transition: opacity 0.24s ease, scale 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}

.win.is-open {
  opacity: 1;
  scale: 1;
}

.win.is-going {
  opacity: 0;
  scale: 0.96;
}

/* Put away: it shrinks towards the foot of the screen rather than simply
   vanishing, so it is clear it went somewhere. */
.win.is-min {
  opacity: 0;
  scale: 0.82;
  transform-origin: 50% 100%;
}

/* Only while a size is changing under its own steam - never during a drag,
   where an eased window is a window lagging behind the hand.

   Slower and gentler than the rest. Growing to fill the screen covers far
   more ground than opening or closing does, and the same brisk curve over the
   same short time reads as a snap rather than a movement. This one leaves
   slowly, carries most of the distance in the middle, and settles rather than
   stops.

   The hint is worth having here and nowhere else: width and height are the
   two properties that genuinely relayout the window on every frame, and this
   is the only moment they are animated. */
.win.is-easing {
  transition: opacity 0.24s ease, scale 0.24s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.46s cubic-bezier(0.4, 0.02, 0.16, 1),
    width 0.46s cubic-bezier(0.4, 0.02, 0.16, 1),
    height 0.46s cubic-bezier(0.4, 0.02, 0.16, 1);
  will-change: width, height, transform;
}

.win.is-moving {
  transition: none;
}

/* The one in front. The difference is deliberately small - a brighter edge and
   a deeper shadow - because a stack of photographs should not look like a stack
   of dialog boxes. */
.win.is-active {
  box-shadow: 0 32px 90px rgba(0, 0, 0, 0.72), 0 0 0 1px rgba(255, 255, 255, 0.16);
}

.win:not(.is-active) .win__bar {
  background: #1e1e1e;
}

.win:not(.is-active) .win__title {
  color: rgba(255, 255, 255, 0.42);
}

/* ---------- Gallery: the title bar ---------- */

.win__bar {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: 34px;
  padding: 0 12px;
  background: #262626;
  border-bottom: 1px solid rgba(0, 0, 0, 0.5);
  cursor: grab;
  /* The bar is the handle: the browser must not take the gesture for a scroll
     halfway through a drag. */
  touch-action: none;
}

.win.is-moving .win__bar {
  cursor: grabbing;
}

.win__lights {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
}

.win__light {
  width: 12px;
  height: 12px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  display: block;
  cursor: pointer;
  transition: filter 0.15s ease;
}

.win__light:hover {
  filter: brightness(1.18);
}

.win__light:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.win__light--min {
  background: #28c840;
}

.win__light--max {
  background: #febc2e;
}

.win__light--close {
  background: #ff5f57;
}

.win__title {
  position: absolute;
  left: 0;
  right: 0;
  text-align: center;
  color: rgba(255, 255, 255, 0.82);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: 11px;
  letter-spacing: 0.1em;
  pointer-events: none;
  padding: 0 70px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- Gallery: what is in the window ---------- */

.win__body {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  background: #0b0b0b;
}

/* Contained rather than cropped, so the whole picture is always there whatever
   shape the window is dragged into - and scaled from the middle on top of
   that, which is the zoom. */
.win__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform-origin: 50% 50%;
  user-select: none;
  -webkit-user-drag: none;
}

/* Once there is more picture than window, the cursor says so. */
.win.is-zoomed .win__body {
  cursor: grab;
}

.win.is-zoomed .win__body:active {
  cursor: grabbing;
}

/* ---------- Gallery: the edges you take hold of ---------- */

.win__grip {
  position: absolute;
  touch-action: none;
}

.win.is-max .win__grip {
  display: none;
}

.win__grip--n, .win__grip--s { left: 10px; right: 10px; height: 8px; }
.win__grip--n { top: -3px; cursor: ns-resize; }
.win__grip--s { bottom: -3px; cursor: ns-resize; }

.win__grip--e, .win__grip--w { top: 10px; bottom: 10px; width: 8px; }
.win__grip--e { right: -3px; cursor: ew-resize; }
.win__grip--w { left: -3px; cursor: ew-resize; }

.win__grip--ne, .win__grip--nw, .win__grip--se, .win__grip--sw {
  width: 16px;
  height: 16px;
}

.win__grip--nw { top: -4px; left: -4px; cursor: nwse-resize; }
.win__grip--se { bottom: -4px; right: -4px; cursor: nwse-resize; }
.win__grip--ne { top: -4px; right: -4px; cursor: nesw-resize; }
.win__grip--sw { bottom: -4px; left: -4px; cursor: nesw-resize; }

/* ---------- Gallery: where minimised windows wait ---------- */

.dock {
  position: fixed;
  left: 50%;
  bottom: 14px;
  translate: -50% 0;
  z-index: 25;
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 92vw;
  overflow-x: auto;
  padding: 6px;
  border-radius: 10px;
  background: rgba(20, 20, 20, 0.82);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(8px);
}

.dock__chip {
  flex: 0 0 auto;
  appearance: none;
  border: 0;
  border-radius: 6px;
  padding: 6px 12px;
  background: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.86);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: 10px;
  letter-spacing: 0.12em;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.18s ease;
}

.dock__chip:hover {
  background: rgba(255, 255, 255, 0.2);
}

/* Nothing anywhere is selectable while a window is being moved or sized. */
.dragging,
.dragging * {
  user-select: none !important;
  -webkit-user-select: none !important;
  cursor: inherit;
}

/* A finger has no edge to catch, so the grips are given more to aim at and the
   windows open larger. */
@media (hover: none) {
  .win__grip--n, .win__grip--s { height: 16px; }
  .win__grip--n { top: -7px; }
  .win__grip--s { bottom: -7px; }
  .win__grip--e, .win__grip--w { width: 16px; }
  .win__grip--e { right: -7px; }
  .win__grip--w { left: -7px; }

  .win__grip--ne, .win__grip--nw, .win__grip--se, .win__grip--sw {
    width: 24px;
    height: 24px;
  }
}

/* ---------- Works: a row of stills on one line ---------- */

/* One screen. The page itself has no length: the strip runs endlessly, so
   there is nothing for a scrollbar to describe and nothing below to reach. */
.works {
  /* The two measures the whole composition is built from - the black between
     any two panels, and the black above and below the row. Declared once each
     and used nowhere else, so the rhythm cannot drift.

     The gap is a whole number of pixels on purpose. A fractional one is still
     a single value and still perfectly equal in the layout, but the screen has
     no fractional pixels to paint it on: a 13.68px gap comes out 13px between
     one pair and 14px between the next, depending on where each panel's edge
     happens to fall. Whole pixels leave nothing to round, and every gap in the
     row paints identically. */
  --gap: 14px;
  --edge: clamp(12px, 2vh, 24px);

  position: relative;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: #000;
}

/* The stage is the eye. The script arranges the panels on a shallow arc, and
   the perspective is what turns that arc into foreshortening - panels growing
   towards the ends of the row and falling away through the middle - rather
   than a flat fan of parallelograms. */
.works__stage {
  position: absolute;
  inset: 0;
  overflow: hidden;

  /* The browser is told up front that it will not be scrolling this, so it
     never waits to find out whether a touch was meant for it. Without this a
     finger can spend the first frames of a swipe in the browser gesture
     handling instead of ours, which is felt as the row not setting off. */
  touch-action: none;
  /* In viewport widths rather than pixels, for the same reason the arc is
     described by an angle: the depth of the curve and the strength of the
     perspective have to scale together, or the composition changes shape with
     the window. The bounds only stop it going strange on a phone or on a very
     wide display. */
  perspective: clamp(560px, 80vw, 1600px);
  perspective-origin: 50% 50%;
}

/* The grain and the grade now live in the picture files themselves.

   They used to be a blended noise layer over the row and a filter on each
   picture, and both were free to write and expensive to run. A blend mode
   over the row means the compositor cannot simply move the panels: it has to
   re-composite the whole stage every frame. A filtered element re-rasterises
   whenever its scale changes - and every panel's scale changes continuously,
   because that is what travelling along the arc does. Twenty-four of them at
   once was what made the scrolling stutter.

   Baked into the files the result is identical and costs nothing to draw. */

/* Moved only by the wheel, and only while it is turning: the strip carries the
   scrolling it has been given and holds still the moment that stops. It is
   wound on by exactly one sequence whenever it has travelled that far, landing
   on an identical arrangement - so it can run in either direction for as long
   as anyone cares to keep going. */
.works__track {
  display: flex;
  /* Centred rather than stretched, so the panels sit in the middle of the
     screen with the same air above them as below. */
  align-items: center;

  /* The one measure between every pair of panels in the row. Nothing else in
     this block puts a margin on a panel and nothing positions one by hand, so
     every gap is identical by construction rather than by care. */
  gap: var(--gap);

  height: 100%;
  width: max-content;
  will-change: transform;

  /* Without this the track would flatten its children, and every panel would
     face straight ahead however far the script has turned it. */
  transform-style: preserve-3d;

  /* And with it, the track has to be taken out of the pointer's way.

     Every panel is pushed back behind the plane of the screen, so that none
     of them ever stands in front of it. The track's own box stays on that
     plane - and inside a preserve-3d context the browser sorts by depth, so
     the track ends up in front of every panel it contains. The pointer lands
     on the track, the panel underneath is never entered, and :hover on it can
     never fire.

     It has nothing to catch in any case: the wheel is taken on the window and
     the touch on the stage. The panels take it back below. */
  pointer-events: none;
}

.panel {
  position: relative;
  flex: 0 0 auto;

  /* Taken back from the track above, which had to give it up. */
  pointer-events: auto;

  /* One width for every panel - narrow, and the same all the way along. It
     scales with the screen rather than being fixed, so the same number stays
     in view whatever the display, but the floor and ceiling stop them turning
     into slivers on a phone or slabs on a very wide monitor. */
  /* Rounded for the same reason as the gap: the panel edge is where the gap
     beside it starts, so a fractional width would put the rounding back. The
     unrounded value is declared first, for anything too old to know round(). */
  width: clamp(132px, 15.5vw, 240px);
  width: round(clamp(132px, 15.5vw, 240px), 1px);
  height: calc(100% - 2 * var(--edge));
  margin: 0;

  /* The picture and its title, one above the other. The figure is what the
     script turns, so putting the title inside it is what gives the title the
     arc: it is carried by the same transform rather than tracking it. */
  display: flex;
  flex-direction: column;

  /* Promoted once, and then only ever moved. Everything that used to force a
     repaint as a panel travelled - the filter, the blurred shadow, the blend
     above it - has been taken off it, so the compositor can hand the whole
     row to the GPU and simply move the pieces about.

     Deliberately no will-change here. It would pin all twenty-four panels into
     layers of their own and hold them there - on a phone, at three device
     pixels to one, that is tens of megabytes of texture for a row of which
     seven are ever on screen, and running the GPU out of memory is its own kind
     of stutter. Containment lets the browser skip the ones it cannot see
     instead. It sits on the frame rather than here, now that the figure has a
     title under it that must not be clipped away. */

  /* The turn itself is written by the script, every frame, from where the
     panel has reached on the screen - so nothing here may set a transform of
     its own. It turns about its own centre, which leaves the slot the row
     spaced it into exactly where it was: the gap is a property of the layout,
     and the layout never moves. */
  transform-origin: 50% 50%;
  backface-visibility: hidden;

  /* Held back until the page is handed over, then brought up in order. */
  opacity: 0;
  transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * 45ms);
}

.is-loaded .panel {
  opacity: 1;
}

/* The rectangle. It takes whatever height the title leaves, so the panel as a
   whole stays exactly as tall as it was and the row still fits the screen
   between its margins.

   The clipping lives here rather than on the figure, because the figure now
   holds the title too and that must not be cropped. */
.panel__frame {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  background: #fff;
  contain: paint;
}

.panel__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: center center;
}

.panel__frame > .media-picture,
.project-overlay__frame > .media-picture {
  position: absolute;
  inset: 0;
  display: block;
}

.media-picture > .panel__img {
  position: absolute;
  inset: 0;
}

/* The title, under its own rectangle. Small and quiet: it names the work
   without competing with it. A title too long for the panel trails off rather
   than wrapping and pushing the picture shorter than its neighbours. */
.panel__cap {
  flex: 0 0 auto;
  margin-top: clamp(7px, 0.9vh, 13px);
  color: rgba(255, 255, 255, 0.66);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: clamp(8px, 0.58vw, 11px);
  line-height: 1.3;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A shadow drawn into the edges of every panel, which lifts off the one being
   pointed at. This is where the depth comes from: the panel cannot actually
   come forward without moving, so instead it comes out of shadow, and reads as
   nearer for it. Drawn inside the panel's own box, so it changes no geometry
   and nothing beside it moves. */
.panel__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* Flat, not a blurred shadow. A blur has to be painted, and painted again
     every time the panel it sits in changes size - which, on the arc, is
     constantly. A plain wash is one colour and the compositor can fade it
     without redrawing anything.

     Light, now that it sits on white rather than on a photograph: enough to
     hold the panels a shade off true white, so that lifting it under the
     pointer still reads as one of them coming forward. It is the whole of the
     hover effect here - there is no longer a picture to zoom. */
  background: rgba(0, 0, 0, 0.14);
  opacity: 1;
  transition: opacity 0.52s cubic-bezier(0.22, 1, 0.36, 1);
}

@media (min-width: 761px) and (hover: hover) {
  .gallery-page .icon[data-video="true"] { display: none; }
}

/* Only while the row is standing still. A strip sliding along under a still
   pointer keeps handing it one panel after another, and each would take the
   zoom for the moment it passed underneath - which reads as the pictures
   flinching as they go by, not as anything the reader did. The script marks
   the row as moving and clears it the moment the movement settles, so the
   panel actually under the pointer takes the zoom back on its own.

   Which panel that is comes from the script rather than from :hover. The
   browser only reconsiders :hover when the mouse moves - not when the page
   moves under a mouse that is holding still - so after a scroll it still
   pointed at whichever panel had since slid away. */
@media (hover: hover) {
  .works__track:not(.is-moving) .panel.is-hovered .panel__frame::after {
    opacity: 0;
  }
}

/* Pointing at a panel changes only how it is lit. It keeps its width, its
   place in the row and its lean, so no gap changes and no neighbour moves -
   the panel does not come forward, it comes into focus, while the rest of the
   row stays exactly as it was. */
/* ---------- Works: the navigation floating over it ---------- */

/* Fixed to the screen rather than carried by the track, so the panels drift
   behind it and it reads as a layer of its own.

   White type would now be white on white: the panels are white, and the row
   passes straight across them. So it inverts what is behind it instead, the
   way the wordmark does over the film on the home page - black where it
   crosses a panel, white where it crosses the gaps between them, and correct
   at every position without anything having to know where the panels are.

   The blend is on the row and not on each word, and it has to be. This is a
   fixed element with a z-index, which makes it a stacking context of its own -
   a word blending inside it would find nothing behind it but the row, and the
   inversion would quietly do nothing. The row itself has the page as its
   backdrop. */
.herolinks--mid {
  position: fixed;
  inset: 0;
  z-index: 10;
  color: #fff;
  mix-blend-mode: difference;
}

.herolinks--mid .nav__link,
.herolinks--mid .herolinks__logo {
  color: inherit;
  mix-blend-mode: normal;
}

/* On a phone the five words will not go across the screen, so the row becomes
   a column down the middle of it - the same way the home page's first screen
   stacks.

   It has to sit here, after the rules above rather than up with the other
   narrow-screen rules, because it overrides them at the same specificity and
   the later one is the one that wins. */
@media (max-width: 700px) {
  .herolinks--mid {
    grid-auto-flow: row;
    justify-content: center;
    justify-items: center;
    align-items: center;
    gap: clamp(16px, 3vh, 30px);
  }

  /* Stacked, the equal-width trick from the script would pad words out
     against an edge that is no longer there. */
  .herolinks--mid .nav__link {
    width: auto;
    text-align: center;
  }
}

/* Works project overlay */
.project-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: none;
  overflow: hidden;
  background: transparent;
}

.project-overlay.is-open {
  display: block;
}

.project-overlay.is-closing {
  pointer-events: none;
}

.project-overlay.is-settled {
  background: #000;
  overflow-y: auto;
}

.project-overlay {
  overscroll-behavior: contain;
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.project-overlay::-webkit-scrollbar {
  display: none;
}

.project__hero {
  position: relative;
  height: 100vh;
  height: 100dvh;
}

.project-overlay.is-settled .project-overlay__frame {
  position: absolute;
  transition: none;
  will-change: auto;
}

.project__details {
  display: grid;
  gap: clamp(24px, 5vw, 72px);
  padding: clamp(24px, 5vw, 72px) clamp(16px, 5vw, 72px) 100px;
  color: #fff;
}

.project__detail {
  position: relative;
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
}

.project__details--pairs {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(12px, 2vw, 28px);
  /* Both columns take the height of the taller one, so that whatever the
     shorter is missing becomes space it can hand out - see the column rule
     below. */
  align-items: stretch;
}

.project__detail > img,
.project__detail > video {
  display: block;
  width: 100%;
  height: auto;
  background: #151515;
  object-fit: contain;
}

.project__detail > .media-picture {
  position: relative;
  display: block;
}

.project__detail > .media-picture > img {
  display: block;
  width: 100%;
  height: auto;
  background: #151515;
  object-fit: contain;
}

.project__detail > video {
  aspect-ratio: 4 / 3;
  max-height: 85vh;
}

.project__details--pairs .project__detail--intro {
  grid-column: 1 / -1;
  width: min(100%, 440px, 38.33vh);
}

.project__detail--intro > video {
  aspect-ratio: 23 / 45;
}

.project__details--pairs .project__detail--full {
  grid-column: 1 / -1;
}

.project__detail-column {
  display: grid;
  gap: inherit;
  min-width: 0;
  align-content: start;
}

.project-overlay[data-project="style-rare"] .project__details {
  gap: clamp(16px, 2vw, 28px);
}

.project-overlay[data-project="style-rare"] .project__detail-column {
  gap: clamp(16px, 2vw, 28px);
  align-content: start;
}

.project__detail-column--left { grid-column: 1; }
.project__detail-column--right { grid-column: 2; }

.project__detail figcaption {
  display: none !important;
}

.project-overlay[data-project="jordan"] .project__detail figcaption {
  display: none !important;
}

.project__detail-link.project__youtube-cover {
  position: relative;
  display: block;
  width: 100vw;
  max-width: none;
  height: 100vh;
  max-height: none;
  margin-left: 50%;
  transform: translateX(-50%);
  overflow: hidden;
  background: #000;
  cursor: pointer;
  border: 0;
  padding: 0;
}

.project__detail-link.project__youtube-cover img,
.project__detail-link.project__youtube-cover iframe {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
  border: 0;
  background: #000;
}

.project__detail-link.project__youtube-cover::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: clamp(78px, 8vw, 118px);
  height: clamp(48px, 5vw, 74px);
  transform: translate(-50%, -50%);
  border-radius: 14px;
  background: #ff0033;
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.28);
  z-index: 1;
}

.project__detail-link.project__youtube-cover::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  transform: translate(-18%, -50%);
  border-left: 22px solid #fff;
  border-top: 14px solid transparent;
  border-bottom: 14px solid transparent;
  z-index: 2;
}

.project__detail-link.project__youtube-cover.is-playing::before,
.project__detail-link.project__youtube-cover.is-playing::after {
  display: none;
}

.project__detail-link.project__youtube-cover iframe {
  position: absolute;
  inset: 0;
}

.project__media-status {
  position: absolute;
  z-index: 1;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  gap: 12px;
  justify-items: center;
  text-align: center;
  color: #fff;
  font: 14px/1.5 "Helvetica Neue LV", sans-serif;
  text-shadow: 0 1px 4px #000;
  pointer-events: none;
}

.project__media-status button,
.project__image-retry {
  pointer-events: auto;
  border: 1px solid #fff;
  border-radius: 3px;
  padding: 12px 18px;
  background: #151515;
  color: #fff;
  cursor: pointer;
}

.project__media-status button[hidden],
.project__image-retry[hidden] { display: none; }

.project__image-retry {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.project-open .works__stage,
.project-open .herolinks--mid {
  pointer-events: none;
}

.project-open .herolinks--mid {
  opacity: 0;
}

.project-overlay__frame {
  position: fixed;
  left: 0;
  top: 0;
  overflow: hidden;
  background: #000;
  contain: paint;
  border-radius: 10px;
  will-change: left, top, width, height;
  transition: left 0.95s cubic-bezier(0.76, 0, 0.24, 1), top 0.95s cubic-bezier(0.76, 0, 0.24, 1), width 0.95s cubic-bezier(0.76, 0, 0.24, 1), height 0.95s cubic-bezier(0.76, 0, 0.24, 1), border-radius 0.95s cubic-bezier(0.76, 0, 0.24, 1);
}

.project-overlay__frame.is-expanded {
  left: 0 !important;
  top: 0 !important;
  width: 100vw !important;
  height: 100vh !important;
  border-radius: 0;
}

.project-overlay__frame > .panel__img,
.project-overlay__frame > .media-picture > .panel__img,
.project-overlay__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.project-overlay__frame > .panel__img,
.project-overlay__frame > .media-picture > .panel__img {
  opacity: 1;
  object-position: center center;
}

.project-overlay__video {
  opacity: 0;
  transition: opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.project-overlay__video.is-ready {
  opacity: 1;
}

.project__info {
  position: absolute;
  z-index: 2;
  left: clamp(14px, 1vw, 16px);
  bottom: clamp(12px, 2.5vh, 24px);
  width: min(620px, calc(100vw - 30px));
  color: #fff;
  mix-blend-mode: difference;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
}

.project__info.is-revealed {
  opacity: 1;
}

.project__rule {
  position: relative;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 13px 12px 15px;
  font-family: "Helvetica Neue LV", sans-serif;
  font-size: clamp(12px, 1.35vw, 18px);
  font-weight: 300;
  line-height: 1;
  text-transform: uppercase;
}

.project__rule::before,
.project__rule::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background: #fff;
  transform: scaleX(0);
  transform-origin: left center;
}

.project__rule::before {
  top: 0;
}

.project__rule::after {
  bottom: 0;
}

.project__info.is-revealed .project__rule::before,
.project__info.is-revealed .project__rule::after {
  animation: project-rule-in 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

.project__word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.8em);
}

.project__info.is-revealed .project__word {
  animation: project-word-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  animation-delay: var(--word-delay, 0s);
}

@keyframes project-rule-in {
  to { transform: scaleX(1); }
}

@keyframes project-word-in {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.project__info h1 {
  margin: 12px 0 7px;
  font-family: "Helvetica Neue LV", sans-serif;
  font-size: clamp(42px, 4.2vw, 72px);
  font-weight: 700;
  line-height: 0.95;
  letter-spacing: 0;
  transform: scaleX(1.12);
  transform-origin: left center;
}

.project__credit {
  font-family: "Helvetica Neue LV", sans-serif;
  font-size: clamp(12px, 1.35vw, 18px);
  font-weight: 300;
  line-height: 1.1;
  text-transform: uppercase;
}

.project__info p {
  margin: 0;
  font-family: "Helvetica Neue LV", sans-serif;
  font-size: clamp(12px, 1.35vw, 18px);
  font-weight: 300;
  line-height: 1.1;
  text-transform: uppercase;
}

.project__close {
  position: fixed;
  top: clamp(16px, 1.8vw, 24px);
  left: clamp(16px, 1.8vw, 24px);
  z-index: 2;
  padding: 0;
  border: 0;
  background: transparent;
  color: #fff;
  cursor: pointer;
  mix-blend-mode: difference;
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition: opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.project__switch {
  position: fixed;
  top: clamp(16px, 1.8vw, 24px);
  right: clamp(16px, 2vw, 30px);
  z-index: 2;
  display: flex;
  gap: clamp(24px, 3vw, 46px);
  mix-blend-mode: difference;
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition: opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.project__switch button {
  padding: 0;
  border: 0;
  background: transparent;
  color: #fff;
  cursor: pointer;
  font-family: "Helvetica Neue LV", sans-serif;
  font-size: clamp(10px, 1vw, 14px);
  font-weight: 300;
  line-height: 1;
  text-transform: uppercase;
}

.project__switch button:hover {
  opacity: 0.6;
}

.project-overlay.is-video-playing .project__close {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
}

.project-overlay.is-video-playing .project__switch {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
}

.project-overlay.is-open .project__close,
.project-overlay.is-open .project__switch {
  pointer-events: auto;
  visibility: visible;
}

@media (max-width: 760px), (hover: none) {
  .works .panel__frame {
    background: #000;
  }

  .works .panel__img:not(.is-loaded) {
    opacity: 0;
  }

  .works .panel__img.is-error {
    display: none;
  }

  .project-overlay__frame {
    transition: left 0.95s cubic-bezier(0.76, 0, 0.24, 1), top 0.95s cubic-bezier(0.76, 0, 0.24, 1), width 0.95s cubic-bezier(0.76, 0, 0.24, 1), height 0.95s cubic-bezier(0.76, 0, 0.24, 1), border-radius 0.95s cubic-bezier(0.76, 0, 0.24, 1);
  }

  .project-overlay__frame.is-expanded {
    left: 0 !important;
    top: 0 !important;
    width: 100vw !important;
    height: 100dvh !important;
  }
}

@media (min-width: 761px) and (hover: hover) {
  .project__switch {
    top: auto;
    bottom: clamp(30px, 4.5vh, 48px);
    right: clamp(46px, 3.2vw, 52px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .project-overlay .project-overlay__frame,
  .project-overlay__video { transition: none; }
}

@media (max-width: 760px), (hover: none) {
  .works__stage {
    perspective: none;
  }

  .works__track {
    transform-style: flat;
  }

  .works .panel {
    will-change: auto;
  }
}

/* ---------- Windows: the shared behaviour ----------

   Everything that presents itself as a window - the home media explorer, the
   gallery's file explorer, and each item opened out of either - is given these
   states by js/winbox.js. They are deliberately kept apart from any one panel's
   look, so a panel keeps its own glass and its own chrome and only borrows the
   behaviour. */

/* Once the script has taken a window over, it writes left/top/width/height
   itself - so the centring transform the stylesheet opens it with has to go, or
   the two describe different places and the window jumps on first drag. */
.is-anchored {
  transform: none !important;
  margin: 0 !important;
}

/* Filling the screen and coming back are eased; dragging and sizing never are.
   A window easing towards the pointer is a window lagging behind it. */
.is-winbox-easing {
  transition: left 0.34s cubic-bezier(0.22, 1, 0.36, 1),
              top 0.34s cubic-bezier(0.22, 1, 0.36, 1),
              width 0.34s cubic-bezier(0.22, 1, 0.36, 1),
              height 0.34s cubic-bezier(0.22, 1, 0.36, 1) !important;
}

.is-winbox-moving {
  transition: none !important;
}

/* A filled window has no edge left to pull, so the grips are taken away rather
   than left sitting uselessly over the screen edge. */
.is-winbox-max > .winbox__grip {
  display: none;
}

/* Put away: it shrinks towards the foot of the screen rather than simply
   vanishing, so it is clear it went somewhere and can be got back. */
.is-winbox-min {
  opacity: 0 !important;
  transform: scale(0.82) translateY(60px) !important;
  transform-origin: 50% 100%;
  pointer-events: none;
  transition: opacity 0.22s ease, transform 0.22s ease !important;
}

/* ---------- Windows: the edges you take hold of ---------- */

.winbox__grip {
  position: absolute;
  z-index: 6;
  touch-action: none;
}

.explorer[hidden], .win[hidden], .dock[hidden] {
  display: none;
}

/* Seated just INSIDE the edge, not straddling it.

   These panels are glass with a rounded border and clip their own contents, so
   a grip hung outside the box - the usual trick - is simply cut away by the
   overflow and there is nothing left to catch. Sitting them inside costs a few
   pixels of the frame, which is border and padding rather than anything the
   reader is trying to click. */
.winbox__grip--n, .winbox__grip--s { left: 12px; right: 12px; height: 6px; }
.winbox__grip--n { top: 0; cursor: ns-resize; }
.winbox__grip--s { bottom: 0; cursor: ns-resize; }

.winbox__grip--e, .winbox__grip--w { top: 12px; bottom: 12px; width: 6px; }
.winbox__grip--e { right: 0; cursor: ew-resize; }
.winbox__grip--w { left: 0; cursor: ew-resize; }

/* Larger than the edges. These windows have a rounded corner, and the few
   pixels right at the diagonal are outside the element's shape and cannot be
   clicked at all - so the corner needs to reach far enough back along both
   edges to be caught just inside the curve. */
.winbox__grip--ne, .winbox__grip--nw,
.winbox__grip--se, .winbox__grip--sw {
  width: 20px;
  height: 20px;
  z-index: 7; /* over the edges, so a corner wins where the two overlap */
}

.winbox__grip--nw { top: 0; left: 0; cursor: nwse-resize; }
.winbox__grip--se { bottom: 0; right: 0; cursor: nwse-resize; }
.winbox__grip--ne { top: 0; right: 0; cursor: nesw-resize; }
.winbox__grip--sw { bottom: 0; left: 0; cursor: nesw-resize; }

/* A finger has no edge to catch, so the grips are given more to aim at. */
@media (hover: none) {
  .winbox__grip--n, .winbox__grip--s { height: 12px; }
  .winbox__grip--e, .winbox__grip--w { width: 12px; }
  .winbox__grip--ne, .winbox__grip--nw,
  .winbox__grip--se, .winbox__grip--sw { width: 20px; height: 20px; }
}

/* ---------- Home: glass media explorer ---------- */
.home-gallery {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  overflow: hidden;
  isolation: isolate;
}

/* Keep the full-screen blur on its own surface so it can repaint independently
   of the glass windows when the browser restores a background tab. */
.home-gallery::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: rgba(4, 4, 4, 0.58);
}

.home-gallery.is-open {
  display: block;
}

.home-gallery__window,
.home-gallery__media-window {
  position: absolute;
  container-type: inline-size;
  left: 50%;
  top: 50%;
  width: min(900px, calc(100vw - 32px));
  height: min(620px, calc(100vh - 32px));
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 18px;
  background: rgba(24, 24, 24, 0.62);
  box-shadow: 0 30px 100px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.1);
  transform: translate(-50%, -50%) scale(0.96);
  opacity: 0;
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.35s ease;
}

.home-gallery.is-open .home-gallery__window {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
}

.home-gallery__bar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 14px;
  height: 48px;
  padding: 0 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.07);
  cursor: grab;
  touch-action: none;
  user-select: none;
}

.home-gallery__bar:active,
.home-gallery__media-window.is-dragging .home-gallery__bar {
  cursor: grabbing;
}

.home-gallery__lights {
  display: flex;
  gap: 6px;
}

.home-gallery__lights span {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.42);
}

.home-gallery__lights span:first-child,
.home-gallery__lights span:nth-child(2),
.home-gallery__lights span:last-child { background: rgba(255, 255, 255, 0.42); }

.home-gallery__title,
.home-gallery__crumb,
.home-gallery__status,
.home-gallery__aside-note {
  font-family: "Helvetica Neue LV", sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.home-gallery__title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: rgba(255, 255, 255, 0.82);
  font-size: 10px;
}

.home-gallery__controls {
  display: flex;
  flex-shrink: 0;
  gap: 4px;
}

.home-gallery__controls button {
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 7px;
  background: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.78);
  cursor: pointer;
}

.home-gallery__controls button:hover,
.home-gallery__controls button:focus-visible {
  background: rgba(255, 255, 255, 0.2);
  outline: none;
}

.home-gallery__body {
  display: grid;
  grid-template-columns: 170px 1fr;
  height: calc(100% - 48px);
}

.home-gallery__sidebar {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 22px 16px;
  border-right: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(0, 0, 0, 0.16);
}

.home-gallery__label {
  margin-bottom: 10px;
  color: rgba(255, 255, 255, 0.45);
  font-size: 9px;
}

.home-gallery__path {
  border: 0;
  border-radius: 8px;
  padding: 10px 12px;
  background: transparent;
  color: rgba(255, 255, 255, 0.58);
  font: 300 11px "Helvetica Neue LV", sans-serif;
  text-align: left;
  cursor: pointer;
}

.home-gallery__path:hover,
.home-gallery__path.is-current {
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}

.home-gallery__aside-note {
  margin-top: auto;
  color: rgba(255, 255, 255, 0.3);
  font-size: 8px;
  line-height: 1.6;
}

.home-gallery__content {
  display: flex;
  min-width: 0;
  /* A grid cell will not shrink below its content by default, which would let
     a tall grid push past the window; this lets the column bound its grid. */
  min-height: 0;
  flex-direction: column;
  padding: 22px;
}

.home-gallery__crumb {
  display: flex;
  gap: 8px;
  margin-bottom: 18px;
  color: rgba(255, 255, 255, 0.46);
  font-size: 9px;
}

.home-gallery__crumb strong { color: rgba(255, 255, 255, 0.9); font-weight: 300; }

.home-gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 104px), 1fr));
  /* Row gap is deliberately larger than the column gap: the tiles lift on
     hover (translateY(-3px)), and a gap matched to the columns left them
     running into the row above. */
  column-gap: 14px;
  row-gap: 14px;
  /* Every row is one square tile tall, whatever shape the thumbnail inside it
     happens to be.

     This has to be stated. Left to size themselves, the tracks were measured
     from the raw image - so the landscape picture thumbs gave 54px rows that
     the 128px tiles then overlapped, while the portrait video thumbs gave
     219px rows with 90px of dead air under each tile. Same stylesheet, same
     gap, two completely different layouts.

     `auto` here would go back to measuring the image; a fr unit would divide
     the container's height instead. The tile itself carries the square, and
     the track is told to take whatever that comes to. */
  grid-auto-rows: min-content;
  /* flex child in a column: take the leftover height and scroll inside it.
     min-height:0 is what lets it shrink below its content and actually
     overflow, rather than pushing the window taller. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* Keep the scroll - and its momentum - to the grid; do not chain to the
     page behind. touch-action re-enables finger scrolling here, since the
     open document sets touch-action:none. */
  overscroll-behavior: contain;
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
  /* Room for the hover lift on the first row and for the last row to clear
     the status line, now that no scrollbar reserves any width. */
  padding: 3px 2px 6px;
  /* The grid scrolls, but the bar itself is never drawn. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.home-gallery__grid::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

/* The tile is the square. The row track is sized from this, so a portrait
   video thumb and a landscape picture thumb occupy an identical slot and the
   two folders lay out the same way. */
.home-gallery__item {
  width: 100%;
  aspect-ratio: 1;
  overflow: hidden;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 9px;
  /* Opaque, so a thumbnail that has not decoded yet reads as a solid slot
     instead of the window showing through. */
  background: #141414;
  cursor: pointer;
  transition: transform 0.25s ease, border-color 0.25s ease;
}

.home-gallery__item:hover,
.home-gallery__item:focus-visible {
  border-color: rgba(255, 255, 255, 0.7);
  outline: none;
  transform: translateY(-3px);
}

/* Shown as taken: full colour, full opacity, no wash over it. */
.home-gallery__item img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  opacity: 1;
}

.home-gallery__status {
  display: flex;
  justify-content: space-between;
  padding-top: 14px;
  color: rgba(255, 255, 255, 0.36);
  font-size: 8px;
}

.home-gallery__media-window {
  z-index: 3;
  width: min(910px, calc(100vw - 32px));
  height: min(680px, calc(100vh - 32px));
  transform: translate(-50%, -50%);
  opacity: 1;
}

/* An opened item is an explorer too: a strip of file detail beside the stage,
   the same two-column body the folder window uses. */
.home-gallery__media-frame {
  display: grid;
  grid-template-columns: 130px minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  height: calc(100% - 48px);
}

.home-gallery__media-side {
  display: flex;
  flex-direction: column;
  gap: 7px;
  overflow: hidden;
  padding: 18px 14px;
  border-right: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.03);
}

.home-gallery__meta {
  color: rgba(255, 255, 255, 0.58);
  font-family: "Helvetica Neue LV", sans-serif;
  font-weight: 300;
  font-size: 9px;
  letter-spacing: 0.1em;
  /* A long filename trails off rather than forcing the column wider. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.home-gallery__label--lower {
  margin-top: 10px;
}

.home-gallery__media-body {
  position: relative;
  /* The stage fills the cell now, not the window: the frame above owns the
     height and this is one of its two columns. */
  height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #101010;
}

.home-gallery__media-stage {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  padding: 16px 44px;
  min-width: 0;
  min-height: 0;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
}

.home-gallery__media {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: contain;
}

.home-gallery__picture-viewport {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  touch-action: none;
  user-select: none;
  cursor: zoom-in;
}
.home-gallery__picture-viewport.is-zoomed { cursor: grab; }
.home-gallery__picture-viewport.is-zoomed.is-panning { cursor: grabbing; }
.home-gallery__picture-viewport img { display: block; transform-origin: center; }

.media-player {
  position: absolute;
  bottom: 10px;
  left: 12px;
  right: 12px;
  z-index: 3;
  display: grid;
  gap: 4px;
  padding: 8px 10px;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: #fff;
  font: 300 10px "Helvetica Neue LV", sans-serif;
  user-select: none;
}

.media-player[hidden], .home-gallery__media-controls[hidden] { display: none; }
.media-player button {
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  cursor: pointer;
}
.media-player__timeline {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
}
.media-player__time { white-space: nowrap; font-variant-numeric: tabular-nums; }
.media-player__actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.media-player__volume { display: flex; align-items: center; gap: 6px; }
.media-player input {
  min-width: 0;
  margin: 0;
  accent-color: #fff;
  cursor: pointer;
  touch-action: none;
}
.media-player [data-media-volume] { width: 64px; height: 28px; }
.media-player [data-media-seek] { width: 100%; height: 24px; }
.media-player input:disabled { opacity: 0.4; cursor: wait; }
.media-player button:focus-visible, .media-player input:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}
.home-gallery__media-body.has-video .home-gallery__media-stage { padding-bottom: 96px; }
.win__body.has-video .win__img { height: calc(100% - 94px); }
.sheet.has-video { padding-bottom: 104px; }

/* Controls float over the media; they do not reserve a strip that pushes the
   picture toward the top of the viewer. */
.home-gallery__media-body.has-video .home-gallery__media-stage,
.win__body.has-video .win__img,
.sheet.has-video {
  padding-bottom: 0;
}

.win__body.has-video .win__img {
  height: 100%;
}

.home-gallery__media-arrow {
  position: absolute;
  top: 50%;
  z-index: 2;
  border: 0;
  background: transparent;
  color: rgba(255, 255, 255, 0.75);
  font-size: 28px;
  cursor: pointer;
}

.home-gallery__media-arrow--prev { left: 16px; }
.home-gallery__media-arrow--next { right: 16px; }

.home-gallery__tray {
  position: fixed;
  left: 50%;
  bottom: 16px;
  z-index: 80;
  display: flex;
  gap: 6px;
  max-width: calc(100vw - 24px);
  overflow-x: auto;
  padding: 4px;
  transform: translateX(-50%);
}

/* An empty tray is no tray. Without this the hidden attribute loses to the
   display:flex above and an empty bar sits at the foot of the screen. */
.home-gallery__tray[hidden] {
  display: none;
}

.home-gallery__tray-item {
  flex: 0 0 auto;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 8px;
  padding: 8px 12px;
  background: rgba(20, 20, 20, 0.7);
  color: #fff;
  font: 300 9px "Helvetica Neue LV", sans-serif;
  cursor: pointer;
}

@container (max-width: 560px) {
  .home-gallery__body { grid-template-columns: 112px minmax(0, 1fr); }
  .home-gallery__content { padding: 16px 12px; }
  .home-gallery__sidebar { padding: 18px 10px; }
  .home-gallery__lights { display: none; }
  .home-gallery__bar { gap: 6px; padding: 0 10px; }
  .home-gallery__media-frame { grid-template-columns: minmax(0, 1fr); }
  .home-gallery__media-side { display: none; }
  .home-gallery__media-stage { padding: 12px 32px; }
  .home-gallery__media-arrow--prev { left: 6px; }
  .home-gallery__media-arrow--next { right: 6px; }
}

@media (max-width: 700px) {
  .home-gallery__window,
  .home-gallery__media-window {
    width: calc(100vw - 20px);
    height: calc(100vh - 20px);
  }

  .home-gallery__body { grid-template-columns: 112px 1fr; }
  .home-gallery__content { padding: 16px 12px; }
  .home-gallery__sidebar { padding: 18px 10px; }
  /* Keep the row gap clear of the column gap here too, for the same reason:
     `gap` shorthand would flatten the two back together. */
  .home-gallery__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: 10px;
    row-gap: 10px;
  }
}

@media (max-width: 760px), (hover: none) {
  html.gallery-page .herolinks--top {
    position: fixed;
    z-index: 20;
    top: 0;
    left: 0;
    right: 0;
    padding: 18px 12px 12px;
    background: #000;
  }

  html.gallery-page .desk[data-explorer] .explorer__content {
    padding-top: 62px !important;
  }

  .explorer__title {
    font-size: 0;
  }

  .explorer__title::after {
    content: "Gallery";
    display: block;
    color: #fff;
    font-size: 21px;
    letter-spacing: 0;
  }

  .home-gallery__title {
    font-size: 0;
  }

  .home-gallery__title::after {
    content: "Gallery";
    display: block;
    color: #fff;
    font-size: 20px;
    letter-spacing: 0;
  }
}

.media-player {
  transition: opacity 0.3s ease;
}

.media-player.is-idle {
  opacity: 0;
  pointer-events: none;
}

@media (max-width: 760px), (hover: none) {
  .sheet {
    padding: 0;
    background: rgba(0, 0, 0, 0.38);
  }

  .sheet__img {
    width: 100vw;
    height: 100svh;
    max-width: 100vw;
    max-height: 100svh;
    object-fit: contain;
  }

  .winlayer .win {
    top: 0 !important;
    left: 0 !important;
    width: 100vw !important;
    height: 100svh !important;
    border-radius: 0;
  }

  .winlayer .win__bar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 4;
    background: rgba(0, 0, 0, 0.2);
    border-bottom: 0;
  }

  .winlayer .win__body {
    width: 100%;
    height: 100%;
  }

  .winlayer .media-player {
    left: 12px;
    right: 12px;
    bottom: max(16px, env(safe-area-inset-bottom));
  }
}

/* ---------- Standalone mobile gallery ---------- */

@media (max-width: 760px), (hover: none) {
  .desk[data-explorer] {
    min-height: 100svh;
    background: #000;
    overflow: visible;
  }

  .desk[data-explorer] .desk__wall {
    display: none;
  }

  .desk[data-explorer] .explorer {
    min-height: 100svh;
    background: #000;
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
  }

  .desk[data-explorer] .explorer__bar {
    display: none !important;
  }

  .desk[data-explorer] .explorer__title {
    grid-column: 1;
    grid-row: 1;
    align-self: end;
  }

  .desk[data-explorer] .explorer__status {
    display: block;
    grid-column: 1;
    grid-row: 2;
    align-self: start;
    margin: 3px 0 0;
    color: #fff;
    font-size: 14px;
    letter-spacing: 0;
    text-transform: none;
  }

  .desk[data-explorer] .explorer__controls {
    display: flex;
    grid-column: 2;
    grid-row: 1 / span 2;
    align-items: center;
    gap: 8px;
  }

  .desk[data-explorer] .explorer__controls button {
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: #27292b;
    color: #fff;
    font-size: 0;
  }

  .desk[data-explorer] .explorer__controls button::before {
    content: "";
    font-size: 18px;
  }

  .desk[data-explorer] .explorer__controls button:first-child::before {
    content: "=";
  }

  .desk[data-explorer] .explorer__controls button:last-child {
    width: 78px;
    border-radius: 24px;
    background: #4169d8;
  }

  .desk[data-explorer] .explorer__controls button:last-child::before {
    content: "Select";
    font-size: 15px;
    font-weight: 300;
  }

  .desk[data-explorer] .explorer__body,
  .desk[data-explorer] .explorer__content {
    display: block;
    height: auto;
    min-height: 0;
  }

  .desk[data-explorer] .explorer__content {
    padding: 2px 2px 28px;
  }

  .desk[data-explorer] .explorer__body {
    display: block !important;
    height: auto !important;
  }

  .desk[data-explorer] .explorer__content {
    width: 100%;
    padding: 0 2px 28px !important;
  }

  .desk[data-explorer] .explorer__sidebar,
  .desk[data-explorer] .explorer__path,
  .desk[data-explorer] .explorer__rule,
  .desk[data-explorer] .explorer__footer {
    display: none !important;
  }

  .desk[data-explorer] .desk__icons {
    position: static;
    display: grid !important;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 2px;
    width: 100%;
    height: auto;
    overflow: visible;
    padding: 0;
  }

  .desk[data-explorer] .icon {
    position: static;
    width: auto;
    min-width: 0;
    display: block;
    padding: 0;
    border-radius: 0;
  }

  .desk[data-explorer] .icon__shot {
    width: 100%;
    aspect-ratio: 1;
    border-radius: 0;
    box-shadow: none;
  }
}
