/* ============================================================
   MIST — homepage

   Two colours, three faces, two column widths. That is the whole
   system; if something needs a fourth of anything, the layout is
   wrong rather than the palette.

   COLOUR
     --paper  #FFF6E9  primary text
     --dim    #7B7264  secondary text
   Both on a warm near-black. See the note on --dim below before
   using it for anything small.

   TYPE
     --mono     Consolas    the rail, labels, quotes
     --body     Roboto      prose
     --display  Cabinet Gr. masthead and tagline
   Roboto and Cabinet Grotesk are self-hosted in assets/fonts/.
   Consolas is NOT — it ships with Windows and nothing else, so
   the stack falls to the already-self-hosted IBM Plex Mono, which
   is the closest match in the project.

   WIDTH
     --col-text  the reading measure — masthead and prose
     --col-wide  the galleries, deliberately wider than the text
   Both centred in the main area. The step between them is what
   makes the pictures feel like they open out.
   ============================================================ */
:root {
  --ink: #0C0B0A;
  /* the rail is darker than the page, not the same — without the
     step there is nothing telling you where the column ends and
     the content begins */
  --rail-bg: #030303;
  --raise: #131211;
  --line: #24221F;
  --line-2: #3A3630;
  --paper: #FFF6E9;

  /* #7B7264 measures 4.1:1 against this background — under the
     4.5:1 that WCAG AA asks of body text. It is fine on the large
     stuff. Everything small that uses it is listed in one place at
     the bottom of this file; --dim-safe is the same hue lifted to
     5.0:1 for those. Swap --dim to it if you want one colour. */
  --dim: #7B7264;
  --dim-safe: #8A8071;

  /* Consolas is Windows-only and not redistributable, so it can
     never be self-hosted; IBM Plex Mono is the shipped stand-in
     for everyone else. Roboto and Cabinet Grotesk ARE self-hosted,
     which is why neither needs a webfont fallback behind it —
     system-ui is only there for the moment before they land. */
  --mono: Consolas, 'IBM Plex Mono', ui-monospace, 'Courier New', monospace;
  --body: 'Roboto', system-ui, -apple-system, sans-serif;
  --display: 'Cabinet Grotesk', system-ui, sans-serif;

  --rail: 216px;
  --col-text: 700px;
  --col-wide: 1000px;
  /* the reel reaches past everything else on purpose — it is the
     one element allowed to run wider than the reading measure */
  --col-reel: 1180px;
  --pad: 40px;

  /* ---- masthead tuning, all in one place ----
     All three are MULTIPLES of --mast (the slash font-size), so
     they hold at every screen width rather than only at the one
     width I happened to test.

     The signature overflows the slashes VERTICALLY — it is about
     1.4x their height and rides high, so its strokes break past
     them top and bottom. It does not sit on top of them
     horizontally; there is a real gap.

     --mast-gap-l / -r  and the gap is ASYMMETRIC on purpose. "/"
                  leans, so the left group ends with a stroke whose
                  top juts toward the signature while the right
                  group opens with one whose foot is nearest. Equal
                  gaps therefore look unequal. The left needs more
                  air than the right; that is the whole trick, and
                  it is why drawn boxes read as uneven where the
                  real glyph does not.
     --mast-rise  lifts it off the centre line for the same reason
                  — the slant throws optical weight down-right, so
                  a mathematically centred signature reads as
                  sagging. Nudge these by eye. Do not compute them. */
  --mast-gap-l: .21;
  --mast-gap-r: .15;
  --mast-rise: -.05;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* PULL TO REFRESH IS LEFT ALONE — deliberately, after taking it
     away once and putting it back.

     It was disabled on the theory that an overscroll at the top
     reloads the page and dumps you back where you started. That was a
     guess at a bug, the bug turned out to be something else, and the
     gesture is one every phone user has. A guess is not a good enough
     reason to remove a platform behaviour: this page has no state
     worth protecting from a reload, and someone reaching for refresh
     has a reason. Containment belongs on the modal scroller inside
     the viewer, not on the document — see .stage. */
  /* Always reserve the scrollbar's width, whether or not there is
     one. Two things this fixes:

     · the gallery measures its column, fills it, and the page
       becomes tall enough to grow a scrollbar — which takes ~15px
       back off the column the rows were just sized against. The
       grid then overhangs its own column until something triggers
       a re-layout.
     · navigating from a short page (legal) to a tall one (art)
       shifted the whole layout sideways by the scrollbar width.
       The rail and masthead visibly jumped. */
  scrollbar-gutter: stable;
}

body {
  background: var(--ink);
  color: var(--paper);
  font-family: var(--body);
  font-weight: 300;
  /* Roboto Light. the prose is set in it, so it is the default
     rather than something re-declared per block */
  font-size: 17px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  display: flex;
  align-items: flex-start;
  min-height: 100vh;
}

body.locked {
  overflow: hidden
}

img,
svg {
  max-width: 100%;
  display: block
}

a {
  color: inherit;
  text-decoration: none
}

/* the body is Roboto LIGHT, and 700 next to 300 is too big a jump —
   emphasis starts shouting. 500 is the pair. Declared globally so
   no stray <b> anywhere on the site can pull in a weight the repo
   no longer ships. */
b,
strong {
  font-weight: 500
}

:focus-visible {
  outline: 2px solid var(--paper);
  outline-offset: 3px
}

.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap
}

.skip {
  position: absolute;
  left: -9999px;
  background: var(--paper);
  color: var(--ink);
  padding: 10px 16px;
  font-family: var(--mono);
  font-size: 13px;
  z-index: 300
}

.skip:focus {
  left: 8px;
  top: 8px
}

/* ============================================================
   the rail

   `order` puts it on the right while it stays first in the markup,
   so keyboard focus reaches navigation before the gallery. It is
   sticky rather than fixed: fixed would take it out of flow and
   the main column would slide under it.
   ============================================================ */
.rail {
  order: 2;
  flex: none;
  width: var(--rail);
  position: sticky;
  top: 0;
  height: 100vh;
  padding: 34px 26px 30px;
  display: flex;
  flex-direction: column;
  background: var(--rail-bg);
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1.55;
}

/* Mist.svg is 665×357. Declaring anything else makes `contain`
   letterbox it, and the artwork then renders smaller than the box
   it was given — which is exactly what made the old masthead look
   undersized. Never guess this ratio. */
.rail-mark {
  display: block;
  width: 112px;
  aspect-ratio: 665/357;
  /* centred in the column: auto margins centre the BOX, and the mask
     is centred too so the ink sits in the middle of that box rather
     than hugging its left edge. Both are needed — either one alone
     leaves it visibly off to one side. */
  margin: 0 auto 30px;
  background: var(--paper);
  -webkit-mask: url('../assets/Mist.svg') center/contain no-repeat;
  mask: url('../assets/Mist.svg') center/contain no-repeat;
}

/* the > marker is absolutely positioned so showing and hiding it
   never nudges the label it belongs to */
.rail-lang button,
.rail-nav a,
.rail-nav .soon {
  display: block;
  position: relative;
  padding-left: 15px;
  background: none;
  border: none;
  font: inherit;
  text-align: left;
  color: var(--dim);
  cursor: pointer;
  transition: color .12s;
}

/* ---- the three depths ----
   d0 root · d1 a zone on this page · d2 the full page behind it.

   It reads as a `tree` listing, so it has to behave like one:
   EVERY row is the same size and the same distance from the row
   above it. Only the indent changes. The moment rows get their own
   margins or their own font-size, it stops looking like output
   and starts looking like a menu somebody hand-spaced — which is
   what it was doing before.

   One step is one character of the rail's monospace, so the
   indents land on the character grid rather than an arbitrary
   pixel. */
.rail-nav a,
.rail-nav .soon {
  margin: 0;
  font-size: inherit;
  line-height: 1.65;
}

.rail-nav .d0 {
  padding-left: 15px
}

.rail-nav .d1 {
  padding-left: calc(15px + 2ch)
}

.rail-nav .d2 {
  padding-left: calc(15px + 4ch)
}

/* the > for an indented row sits at ITS indent, not at the column
   edge, or the depth stops reading */
.rail-nav .d1::before {
  left: 2ch
}

.rail-nav .d2::before {
  left: 4ch
}

.rail-lang button::before,
.rail-nav a::before,
.rail-nav .soon::before {
  content: ">";
  position: absolute;
  left: 0;
  opacity: 0;
  transition: opacity .12s;
}

.rail-lang button:hover,
.rail-nav a:hover {
  color: var(--paper)
}

.rail-lang button:hover::before,
.rail-nav a:hover::before {
  opacity: 1
}

.rail-lang button[aria-pressed="true"],
.rail-nav a[aria-current] {
  color: var(--paper)
}

.rail-lang button[aria-pressed="true"]::before,
.rail-nav a[aria-current]::before {
  opacity: 1
}

.rail-lang {
  margin-bottom: 18px
}

/* not built yet: reads as part of the list, but is not a target */
.rail-nav .soon {
  cursor: default;
  opacity: .55
}

/* margin-top:auto is what creates the long empty middle in the
   design — the block is pinned to the bottom of the viewport
   rather than sitting under the nav with a guessed gap */
/* centred, unlike the old site's footer — that one was
   space-between, name left and copyright right. Centring works
   here because the rail is a narrow column: a 160px measure ragged
   on both sides reads as debris, and centred reads as a block. */
.rail-foot {
  margin-top: auto;
  text-align: center;
  font-size: 12.5px;
  color: var(--dim-safe);
}

.rail-links a {
  border-bottom: 1px solid currentColor;
  transition: color .12s
}

.rail-links a:hover {
  color: var(--paper)
}

.rail-copy {
  margin-top: 14px
}

.rail-quote {
  margin-top: 4px;
  font-style: italic
}

.rail-quote::before {
  content: "> "
}

/* ============================================================
   main
   ============================================================ */
main {
  order: 1;
  flex: 1;
  min-width: 0;
  padding: 0 var(--pad) 120px;
}

.masthead,
.intro {
  max-width: var(--col-text);
  margin-inline: auto
}

.reel,
#art {
  max-width: var(--col-wide);
  margin-inline: auto
}

/* ---------- masthead ----------

   Sized from the slashes outward. --mast is the cap height of the
   "/" glyphs; the signature is a MULTIPLE of it, so the overlap
   holds its proportions at every width instead of only at the one
   I happened to test. */
.masthead {
  padding-block: clamp(56px, 9vw, 118px) 0;
  /* the measuring stick for everything inside — see .mast-row */
  container-type: inline-size;
}

.mast-row {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Fallback for browsers without container queries. */
  --mast: clamp(58px, 11vw, 140px);
}

/* THE MASTHEAD FILLS ITS COLUMN.

   The whole row measures about 4.85x --mast (two slash groups, the
   signature at 1.9x, and the two gaps), so dividing the column by
   4.85 is the size at which it exactly spans the width available.
   140px caps it, which is what it already reached on desktop — so
   this changes nothing there.

   On a phone it changes a lot. The old floor of 58px left the row
   at 281px inside 335px of usable width, and since the tagline is
   locked to the row, that wasted 54px came straight off the type:
   13.6px, too small to read comfortably. Filling the width fixes
   the tagline as a side effect.

   cqw, not vw: vw is the whole window and knows nothing about the
   216px rail, so at ~900px wide it would size the masthead for
   space that the rail is already using and overflow the column. */
@supports (container-type: inline-size) {
  .mast-row {
    --mast: min(140px, calc(100cqw / 4.85));
  }
}

/* real "/" glyphs in Cabinet Grotesk. line-height:1 and a block
   display stop the font's own leading from padding the row out. */
.slashes {
  flex: none;
  display: block;
  font-family: var(--display);
  font-weight: 900;
  font-size: var(--mast);
  line-height: 1;
  letter-spacing: -.02em;
  color: var(--paper);
  user-select: none;
}

/* Taller than the slashes — that is where the overflow lives. The
   1.9 is set against the slash size, not the column, so the
   relationship survives every breakpoint. */
.mast-mark {
  flex: none;
  position: relative;
  z-index: 1;
  width: calc(var(--mast) * 1.9);
  aspect-ratio: 665/357;
  /* the real ratio. see the note on .rail-mark. */
  margin-left: calc(var(--mast) * var(--mast-gap-l));
  margin-right: calc(var(--mast) * var(--mast-gap-r));
  margin-top: calc(var(--mast) * var(--mast-rise));
  background: var(--paper);
  -webkit-mask: url('../assets/Mist.svg') center/contain no-repeat;
  mask: url('../assets/Mist.svg') center/contain no-repeat;
}

.tagline {
  /* tight to the signature — the two are one object, not a heading
     with a subtitle under it */
  margin-top: clamp(4px, 0.8vw, 11px);
  text-align: center;
  font-family: var(--display);
  font-weight: 800;
  /* a STARTING size only. fitTagline() overwrites it — see below. */
  font-size: 30px;
  /* 130 tracking in Affinity is 130/1000 em */
  letter-spacing: .13em;
  text-transform: uppercase;
  line-height: 1.08;

  /* THE MASTHEAD IS ONE BLOCK.

     The tagline is scaled by fitTagline() in js/site.js to span
     exactly the width of the /// mist /// row above it — never
     wider, never narrower. So its edges line up with the outer
     edges of the slashes at every screen size, and the two rows
     read as a single locked-up mark instead of a headline with a
     caption that happens to sit near it.

     That also settles Polish for free: the longer string simply
     resolves to a slightly smaller size against the same width,
     rather than overflowing or wrapping. Both languages occupy the
     identical footprint.

     nowrap is what makes the measurement meaningful — scrollWidth
     only reports the true single-line width if it is not allowed
     to wrap. */
  white-space: nowrap;
}

/* no margin: the separators carry a real space either side in the
   markup now, and adding margin on top of that double-spaces them */
.tagline em {
  font-style: normal;
  color: var(--dim);
}

/* ---------- intro ---------- */
.intro {
  padding-top: clamp(48px, 7vw, 86px);
  font-size: clamp(16.5px, 1.35vw, 19px);
}

.intro p {
  margin-bottom: 26px
}

.intro p:last-child {
  margin-bottom: 0
}

/* Light for the prose, Medium for the names picked out inside it.
   Bold next to a 300 weight is too big a jump and the names start
   shouting. */
.intro b {
  font-weight: 500
}

.quote {
  font-family: var(--mono);
  font-size: 14.5px;
  font-style: italic;
  color: var(--dim-safe);
  margin-bottom: 34px !important;
}

.quote::before {
  content: "> "
}

/* styled exactly like a link, because the design says so, but it
   is a <span> — the sections these promise do not exist yet */
.pending {
  border-bottom: 1px solid var(--paper)
}

a.inline {
  border-bottom: 1px solid var(--paper)
}

/* ============================================================
   the reel — decorative scrolling strip

   Two copies of the same set sit end to end and the track is
   translated by exactly -50%, which lands copy two precisely where
   copy one began. That is why the loop has no seam. Any other
   distance would jump.
   ============================================================ */
/* The fade is a hand-eased ramp, not a straight one. A plain
   two-stop linear gradient goes transparent in a way you can see
   the edge of — the midpoint arrives too early and the last
   stretch lingers. These stops approximate an ease-in-out, so the
   image thins out gradually and the far end is genuinely gone
   rather than faintly there. It also reaches much further in:
   30% of the width per side instead of 14%. */
.reel {
  margin-top: clamp(60px, 9vw, 104px);
  max-width: var(--col-reel);
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  --fade: linear-gradient(90deg,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, .04) 6%,
      rgba(0, 0, 0, .16) 12%,
      rgba(0, 0, 0, .38) 18%,
      rgba(0, 0, 0, .66) 24%,
      rgba(0, 0, 0, .89) 29%,
      rgb(0, 0, 0) 34%,
      rgb(0, 0, 0) 66%,
      rgba(0, 0, 0, .89) 71%,
      rgba(0, 0, 0, .66) 76%,
      rgba(0, 0, 0, .38) 82%,
      rgba(0, 0, 0, .16) 88%,
      rgba(0, 0, 0, .04) 94%,
      rgba(0, 0, 0, 0) 100%);
  -webkit-mask-image: var(--fade);
  mask-image: var(--fade);
}

/* no gap — the pieces butt straight up against each other and read
   as one continuous band rather than a row of separate cards */
.reel-track {
  display: flex;
  align-items: center;
  gap: 0;
  width: max-content;
  animation: reel 80s linear infinite;
}

.reel img {
  height: clamp(210px, 29vw, 330px);
  width: auto;
  flex: none;
  background: var(--raise);
}

@keyframes reel {
  to {
    transform: translateX(-50%)
  }
}

/* ============================================================
   the archive — the gallery, unchanged from v2 apart from the
   hover fix noted below
   ============================================================ */
/* ============================================================
   section headers

   Same face as the masthead tagline, small, all caps, heavily
   tracked. No rule, no glyph, no eyebrow — the SPACE above it is
   what separates one section from the next. A divider line would
   be a second thing saying what the padding already says, and on a
   page this bare every extra mark counts double.

   The letter-spacing trick: CSS puts the tracking AFTER the last
   letter too, so a centred, heavily tracked word sits visibly off
   to the right by half its tracking. Pulling one tracking-width
   off the right margin cancels exactly that.
   ============================================================ */
.zone-head {
  font-family: var(--display);
  font-weight: 800;
  /* SAME SIZE AS THE TAGLINE, at every width.

     --tagline-size is published by fitTagline() in js/site.js after
     it solves the masthead fit, so the two stay locked together
     instead of drifting apart the moment the viewport changes.

     The fallback is not decoration: subpages have no masthead to
     measure, so fitTagline never runs there and this clamp is what
     they actually use. It is tuned to land where the homepage's
     measured value lands, so a header looks the same on both. */
  font-size: var(--tagline-size, clamp(16px, 2.2vw, 29px));
  letter-spacing: .38em;
  margin-right: -.38em;
  /* optical centring — see above */
  text-transform: uppercase;
  text-align: center;
  color: var(--paper);
  padding-top: clamp(72px, 10vw, 132px);
  padding-bottom: clamp(26px, 3.4vw, 44px);
}

/* on a subpage the header IS the page's opening, not a divider
   between two blocks — so it does not need a section's worth of air
   above it, and the work can start high up the page */
.pagehead .zone-head {
  padding-top: clamp(32px, 4vw, 56px);
  /* nothing below: .archive supplies the gap to the first row, and
     two paddings stacked is what put the gallery 334px down */
  padding-bottom: 0;
}

#art {
  padding-top: 0
}

.jrow {
  display: flex;
  gap: 14px;
  margin-bottom: 14px
}

/* the final row when it has too few pictures to fill the width.
   centred, because a short row pushed against the left edge reads
   as a layout that broke rather than one that ran out of work */
.jrow-tail {
  justify-content: center
}

/* THE HOVER FIX.

   Two separate faults were stacked here:

   1. The tile used to translate(-2px,-2px) on hover. At the bottom
      and right edges that moves the tile OUT from under the
      pointer, which unhovers it, which moves it back — a flicker
      loop you can sit in forever. Any hover effect that shifts an
      element away from the cursor has this bug.

   2. The shadow animated from `none`, so the first frames had
      nothing to interpolate from and it appeared to pop in late
      and half a pixel off.

   Fix for both: the TILE never moves, and the shadow is declared
   at rest in its final geometry with a transparent colour, so
   hover animates COLOUR only — no layout, no re-rasterising,
   nothing to land on a fractional pixel.

   The zoom is back, and it is safe, because it scales the IMAGE
   inside a box with overflow:hidden. The tile's own edges do not
   budge, so the pointer can never fall outside what it is
   hovering. That was always the difference: moving the picture is
   fine, moving the target is not. */
.tile {
  position: relative;
  flex: none;
  padding: 0;
  overflow: hidden;
  background: var(--raise);
  border: 1px solid var(--line);
  cursor: pointer;
  box-shadow: 6px 6px 0 transparent;
  transition: border-color .15s ease, box-shadow .15s ease;
}

.tile:hover {
  border-color: var(--paper);
  box-shadow: 6px 6px 0 var(--line-2)
}

.tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .4s cubic-bezier(.2, .7, .3, 1);
}

.tile:hover img {
  transform: scale(1.07)
}

.tile .tag,
.tile .plus {
  position: absolute;
  top: 9px;
  z-index: 1;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  background: rgba(12, 11, 10, .85);
  border: 1px solid var(--line);
  padding: 3px 7px;
  color: var(--dim-safe);
}

.tile .tag {
  left: 9px
}

.tile .plus {
  right: 9px;
  color: var(--paper)
}

.tile .name {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 30px 12px 10px;
  text-align: left;
  background: linear-gradient(transparent, rgba(12, 11, 10, .92));
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--paper);
  opacity: 0;
  transition: opacity .2s;
}

.tile:hover .name {
  opacity: 1
}

/* ---------- view all ----------
   A grey hyperlink sitting just under the last row, and nothing
   more. It follows sixteen loud pictures; a bordered button there
   is a second thing asking to be looked at, and it wins that fight
   against the work purely by being a shape. A link is enough — the
   person reaching for it has already decided. */
.after {
  display: flex;
  justify-content: center;
  padding-top: 18px;
}

.view-all {
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: .1em;
  color: var(--dim-safe);
  border-bottom: 1px solid transparent;
  transition: color .15s, border-color .15s;
}

.view-all::after {
  content: " \2192"
}

.view-all:hover {
  color: var(--paper);
  border-bottom-color: var(--paper)
}

/* ---------- lightbox ---------- */
.lb {
  position: fixed;
  inset: 0;
  z-index: 120;
  background: rgba(8, 7, 6, .95);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 26px;
}

.lb.open {
  display: flex
}

.panel {
  background: var(--ink);
  border: 1px solid var(--line-2);
  box-shadow: 0 30px 80px rgba(0, 0, 0, .6);
  max-width: 96vw;
  max-height: calc(100vh - 52px);
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.panel-h {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 24px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--line);
  flex: none;
}

.panel-h h3 {
  font-family: var(--display);
  font-weight: 800;
  font-size: 21px;
  margin-bottom: 4px
}

.meta {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--dim-safe)
}

.tools {
  display: flex;
  gap: 10px;
  flex: none
}

.tools a,
.tools button {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  border: 1px solid var(--line-2);
  background: none;
  color: var(--paper);
  padding: 7px 12px;
  cursor: pointer;
}

.tools a:hover,
.tools button:hover {
  border-color: var(--paper)
}

.stage {
  overflow-y: auto;
  overflow-x: hidden;
  /* EVERY PIECE OPENS AT ITS TOP.

     Scroll anchoring is normally the right behaviour — it keeps what
     you are reading still while images load above it. Here it did the
     opposite: js/site.js sets scrollTop to 0 while the stage is still
     empty, then the frames load, and the browser helpfully restores
     the offset the LAST piece was left at. Open a five-frame piece,
     scroll to the end, close it, open it again and you land in the
     middle of it with no idea there was anything above. A viewer has
     one obvious starting position and it is the top. */
  overflow-anchor: none;
  /* HERE it is right, where it was wrong on <html>. This is a modal
     scroller: reaching its end must not hand the rest of the gesture
     to the page underneath, or to the browser's own pull-to-refresh
     while a picture is filling the screen. */
  overscroll-behavior: contain;
  min-height: 0;
  min-width: 0;
  padding: 18px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  scrollbar-width: thin;
  scrollbar-color: var(--line-2) transparent;
}

/* THE SHORTFALL IS THE MESSAGE.

   No frame is allowed to fill the stage. A tall poster left to take
   the full height lands its bottom edge exactly at the fold, the
   piece looks like a single picture, and the four frames under it
   are invisible until somebody happens to try scrolling.

   There used to be a label saying "4 more below". This replaces it,
   and does the job better: holding every frame short means the top
   edge of the NEXT one is always showing. That is concrete,
   permanent, needs no timeout or dismiss button, and needs no
   translating. A picture of the thing beats a sentence about it.

   ~20px, not the 40 it started at. The signal only has to be
   unambiguous — enough of the next frame to read as another picture
   rather than as a border — and past that point every extra pixel is
   taken off the piece somebody actually came to look at. The cover is
   the work; the hint is a hint.

   Only tall frames are affected — a landscape image runs out of
   WIDTH long before it reaches this, so nothing wide gets smaller. */
.stage img {
  max-width: 100%;
  max-height: calc(100vh - 210px);
  width: auto;
  height: auto;
  cursor: zoom-in
}

/* --frame-cap is the cover's rendered width, measured in
   js/site.js once it has loaded. Every frame below the cover is
   held to it, so a supporting shot can never render larger than
   the piece it supports. */
.stage img:not(:first-of-type) {
  max-width: min(100%, var(--frame-cap, 100%));
}

.frametag {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--dim-safe)
}

.lb-arrow {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 130;
  background: var(--ink);
  border: 1px solid var(--line-2);
  color: var(--paper);
  font-family: var(--mono);
  font-size: 15px;
  padding: 13px 16px;
  cursor: pointer;
}

.lb-arrow:hover {
  border-color: var(--paper)
}

.lb-prev {
  left: 18px
}

.lb-next {
  right: 18px
}

/* zoomed, NOTHING but the picture. the veil covers the viewer, and
   these two would otherwise sit on top of it: the arrows are fixed
   at z-index 130 and the caption rides the panel. Arrow KEYS still
   work while zoomed — it is only the on-screen furniture that goes. */
.lb.zooming .panel-h,
.lb.zooming .lb-arrow {
  opacity: 0;
  pointer-events: none
}

/* darker once a picture is filling the screen — the backdrop stops
   being a panel behind a viewer and becomes the room the work is
   hanging in. Everything not the artwork should recede. */
.lb.zooming {
  background: rgba(2, 2, 2, .985)
}

.lb {
  transition: background .3s ease
}

/* ---------- the veil ----------
   Above the whole viewer, below the enlarged picture. Everything
   that is not the artwork disappears behind it.

   pointer-events:none: clicking off the picture must still reach
   .lb underneath, which is what steps back out of the zoom. */
.zveil {
  position: fixed;
  inset: 0;
  z-index: 150;
  background: #000;
  opacity: .93;
  pointer-events: none;
}

/* ---------- the zoom toolbar ----------

   −   48%   +        ×

   NO PLATE. A panel with a border and a blurred backdrop is the
   answer to a contrast problem, and there is no contrast problem
   here: the veil is already at 93% black, so the type is sitting on
   near-solid ink and a box around it is drawing a box for its own
   sake. The rest of this site never draws one either — the rail is
   bare text on the ground, and this should read as the same voice
   in a different room.

   What carries it instead is what carries the rail: Consolas, wide
   tracking, quiet at rest and full --paper under the pointer.

   WHITE, though, not --dim-safe. The rail can be dim because the
   rail sits on near-black and nothing ever moves underneath it. This
   sits over the artwork, and a 12px brown-grey over a bright
   photograph is not restrained, it is illegible. So the restraint is
   carried by OPACITY rather than by hue: paper at three quarters
   reads as plain white text, still recedes next to the work, and
   still has somewhere to go when you point at it.

   WHAT A TEXT SHADOW COULD NOT DO. The first pass leaned on a glow
   alone, and over a pale poster — a white-on-ice-blue can filling
   the screen — it vanished completely. A glow can rescue type from a
   busy ground; it cannot rescue white type from a white one.

   So the contrast comes from a SCRIM: the top of the screen darkened
   on a gradient that reaches nothing by 76px. It is still not a box.
   A box has edges, sits at a size, and reads as a panel laid over the
   picture; this has no edge anywhere, and over the veil — which is
   already black — it is not visible at all. It only appears when the
   artwork itself is bright enough to need it, which is the same
   principle the glow was reaching for, done at a scale that works.

   pointer-events stay off across the whole band so that clicking the
   dark strip still steps out of the zoom like clicking anywhere else
   off the picture. Only the three controls take clicks. */
.zbar {
  position: fixed;
  z-index: 215;
  top: 0;
  left: 0;
  right: 0;
  height: 76px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 15px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .12em;
  color: rgba(255, 246, 233, .82);
  background: linear-gradient(to bottom,
      rgba(0, 0, 0, .72) 0%,
      rgba(0, 0, 0, .52) 34%,
      rgba(0, 0, 0, .24) 66%,
      rgba(0, 0, 0, 0) 100%);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .9);
  user-select: none;
  pointer-events: none;
}

.zbar button {
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  line-height: 1;
  cursor: pointer;
  text-shadow: inherit;
  pointer-events: auto;
  /* nothing is drawn, but the target is still 30px square — a
     control you cannot reliably hit is not minimal, it is small */
  min-width: 30px;
  height: 30px;
  transition: color .15s;
}

.zbar button:hover:not(:disabled) {
  color: var(--paper)
}

/* at the fit level there is no out, at the ceiling there is no in.
   Dimmed rather than removed — a control that disappears takes the
   row's width with it and you lose the one next to it mid-click. */
.zbar button:disabled {
  color: rgba(255, 246, 233, .26);
  cursor: default
}

.zbar .zstep {
  font-size: 16px
}

/* THE READOUT GETS THE SAME BOX AS THE BUTTONS.

   It is a bare <span>, so its height was one line of 12px type while
   the buttons next to it were 30px tall. With the row aligned to its
   top, the number sat against the ceiling and the − and + sat centred
   in their taller boxes, a good 8px lower. Nothing was wrong with the
   type; the boxes were different heights.

   Centring both inside an identical 30px box also settles the other
   half of it — the steps are set at 16px and the number at 12px, so
   even matched boxes would not have shared a baseline. Optical centre
   is the right alignment here anyway: − and + are symmetrical marks
   about the maths axis, not letters sitting on a baseline.

   Tabular figures, or 8% / 88% / 188% are three different widths and
   the two buttons shuffle sideways as you scroll. */
.zbar .zlvl {
  display: grid;
  place-items: center;
  height: 30px;
  min-width: 54px;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* set apart from the other two — leaving is a different kind of verb
   from zooming, and the gap says so without a divider having to */
.zbar .zclose {
  margin-left: 20px;
  font-size: 16px;
}

/* ---------- pan indicators ----------
   Sized and positioned from --len / --pos, both set in js/site.js as
   percentages of the viewport, which is what `position:fixed` sizes
   against. --vis is 0 on an axis with nothing to pan.

   The idle rule wins on specificity (0,2,0 against 0,1,0), so no
   !important is needed to fade them out. */
.panbar {
  position: fixed;
  z-index: 210;
  background: var(--paper);
  border-radius: 2px;
  pointer-events: none;
  opacity: var(--vis, 0);
  transition: opacity .3s ease;
  /* a dark ring rather than a blend mode. mix-blend-mode:difference
     would stay legible on any brightness, but it INVERTS HUE — over
     the orange in a poster it comes back cyan, which is the one
     thing a monochrome site cannot afford to put on top of the
     artwork. A hairline outline is legible everywhere and adds no
     colour of its own. */
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .55);
}

body.panning-idle .panbar {
  opacity: 0
}

.panbar-x {
  bottom: 10px;
  height: 3px;
  left: var(--pos, 0);
  width: var(--len, 0)
}

.panbar-y {
  right: 10px;
  width: 3px;
  top: var(--pos, 0);
  height: var(--len, 0)
}

/* ============================================================
   subpages — art, labs, legal, 404

   They wear the same rail and the same masthead treatment at a
   smaller size, so a subpage is recognisably the same site without
   repeating the full-size wordmark on every screen.
   ============================================================ */
.pagehead {
  max-width: var(--col-text);
  margin-inline: auto;
  padding-block: clamp(48px, 7vw, 90px) 0;
}

.pagehead .crumb {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--dim-safe);
  margin-bottom: 20px;
}

.pagehead .crumb a {
  border-bottom: 1px solid currentColor
}

.pagehead .crumb a:hover {
  color: var(--paper)
}

/* The big treatment, still used by legal / labs / 404 — pages that
   are mostly text and need an opening that arrives.

   :not(.zone-head) matters: `.pagehead h1` is specificity (0,1,1)
   and `.zone-head` is (0,1,0), so without it this rule silently
   wins on any subpage that opts into the tracked header and the
   class appears to do nothing. */
.pagehead h1:not(.zone-head) {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(38px, 6.5vw, 82px);
  line-height: .95;
  letter-spacing: .01em;
  text-transform: uppercase;
}

/* a pagehead built around the tracked label carries no padding of
   its own — the label brings its own, and doubling them is what
   pushes the work off the first screen */
.pagehead:has(.zone-head) {
  padding-block: 0;
}

.pagehead p.sub {
  margin-top: 16px;
  color: var(--dim-safe);
  max-width: 56ch;
}

/* the full archive: same grid, no row cap, a little more room */
.archive {
  max-width: var(--col-reel);
  margin-inline: auto;
  padding-top: clamp(40px, 6vw, 70px);
}

/* ---------- a document, not a console ---------- */
.doc {
  max-width: 74ch;
  margin-inline: auto;
  padding-block: clamp(36px, 5vw, 60px) 40px;
}

.doc h2 {
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: .16em;
  text-transform: uppercase;
  font-weight: 400;
  color: var(--paper);
  margin: 44px 0 14px;
  padding-top: 26px;
  border-top: 1px solid var(--line);
}

.doc h2:first-of-type {
  border-top: none;
  padding-top: 0;
  margin-top: 0
}

.doc p,
.doc li {
  color: var(--dim-safe);
  margin-bottom: 15px
}

.doc ul {
  padding-left: 20px
}

.doc li {
  margin-bottom: 9px
}

.doc a {
  color: var(--paper);
  border-bottom: 1px solid var(--line-2)
}

.doc a:hover {
  border-bottom-color: var(--paper)
}

.doc code {
  font-family: var(--mono);
  font-size: .9em;
  color: var(--paper)
}

.doc table {
  border-collapse: collapse;
  width: 100%;
  margin: 0 0 20px;
  font-size: 15px
}

.doc td {
  border: 1px solid var(--line);
  padding: 10px 13px;
  color: var(--dim-safe);
  vertical-align: top
}

.doc td:first-child {
  color: var(--paper);
  font-family: var(--mono);
  font-size: 12px;
  white-space: nowrap
}

.doc .updated {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  opacity: .7;
  margin-top: 40px;
}

/* ---------- an honest empty state ---------- */
.empty {
  max-width: var(--col-text);
  margin-inline: auto;
  padding-block: clamp(36px, 5vw, 60px) 40px;
  border-top: 1px solid var(--line);
  margin-top: 40px;
}

.empty p {
  color: var(--dim-safe);
  max-width: 56ch
}

/* ============================================================
   responsive

   Below 820 the rail stops being a column and becomes a header
   strip. It keeps `order` so the markup is untouched.
   ============================================================ */
/* ============================================================
   the rail on a narrow screen

   It stops being a column and becomes a BOTTOM bar, fixed, so it
   is reachable at thumb height and stays put while the galleries
   scroll past it. The indent tree goes with it: depth is a
   vertical idea and there is no vertical room here, so the tree
   flattens to one honest row and the full-page entries get an
   arrow instead — "Art →" reads as "the whole thing", which is
   what the indent was saying.
   ============================================================ */
@media (max-width:820px) {
  body {
    flex-direction: column;
    align-items: stretch;
  }

  main {
    order: 1;
    /* --rail-h is measured in js/site.js, because the bar's height
       depends on how many lines the nav wraps to, which depends on
       the language. A hardcoded number is wrong the moment a label
       gets longer. The fallback covers the frame before JS runs. */
    padding-bottom: calc(var(--rail-h, 128px) + 24px);
  }

  .rail {
    order: 2;
    position: fixed;
    inset: auto 0 0 0;
    z-index: 90;
    width: auto;
    height: auto;
    padding: 9px var(--pad) calc(9px + env(safe-area-inset-bottom, 0px));
    display: flex;
    /* the base rail is a column; this MUST be reset or the bar is
       a wrapping column and every group lands on its own line */
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 5px 16px;
    border-top: 1px solid var(--line);
    background: rgba(3, 3, 3, .96);
    backdrop-filter: blur(10px);
    font-size: 12.5px;
  }

  /* the masthead already shows the signature, twice over */
  .rail-mark {
    display: none
  }

  /* the nav owns its own line; the toggle and the small print
     share the next one, which saves a whole row of bar height */
  .rail-nav {
    order: 1;
    flex: 1 0 100%;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 1px 3px;
  }

  .rail-lang {
    order: 2;
    margin: 0;
    display: flex;
    gap: 2px;
  }

  .rail-foot {
    order: 3;
    margin: 0;
    display: flex;
    align-items: baseline;
    gap: 14px;
    flex-wrap: wrap;
    font-size: 11.5px;
  }

  /* one flat row: no indents, no > markers — both cost horizontal
     room that a phone does not have */
  .rail-lang button,
  .rail-nav a,
  .rail-nav .soon,
  .rail-nav .d0,
  .rail-nav .d1,
  .rail-nav .d2 {
    padding: 4px 7px;
    margin: 0;
    font-size: inherit;
  }

  .rail-lang button::before,
  .rail-nav a::before,
  .rail-nav .soon::before {
    content: none
  }

  /* depth becomes direction */
  .rail-nav .d2::after {
    content: " \2192"
  }

  .rail-lang button[aria-pressed="true"],
  .rail-nav a[aria-current] {
    color: var(--paper);
    text-decoration: underline;
    text-underline-offset: 5px;
  }

  .rail-links {
    display: flex;
    gap: 16px
  }

  .rail-links br {
    display: none
  }

  .rail-copy,
  .rail-quote {
    margin-top: 0
  }

  /* a random line is a lovely thing and the first to go when the
     bar has to stay under two rows */
  .rail-quote {
    display: none
  }

  /* the reel is the widest thing on the page; on a phone it should
     bleed to the edges rather than sit in a gutter */
  .reel {
    margin-inline: calc(var(--pad) * -1);
    max-width: none;
    width: calc(100% + var(--pad) * 2);
  }
}

@media (max-width:560px) {
  :root {
    --pad: 20px
  }

  body {
    font-size: 16px
  }

  /* the bar is navigation; on a phone it should not also be a
     colophon. dropping the copyright line is what lets the toggle
     and the two links share one row instead of taking two, and it
     is the least useful thing in there. legal is still linked. */
  .rail-copy {
    display: none
  }

  .lb {
    padding: 0
  }

  .panel {
    max-width: 100vw;
    max-height: 100vh;
    height: 100%;
    border: none
  }

  .lb-arrow {
    display: none
  }
}

/* the reel is motion for its own sake — it is the first thing to
   go, and it degrades to a plain static strip rather than vanishing */
@media (prefers-reduced-motion:reduce) {

  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important
  }

  html {
    scroll-behavior: auto
  }
}