/* ==========================================================================
   Page-level layouts.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Home — one screen. The portrait is dissolved into the page with a soft
   mask so photograph and background share the same ground.
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  display: flex;
}

.hero__media {
  position: absolute;
  /* above the hairlines, so they run behind him rather than across him */
  /* Dropped below the navigation so the whole head clears it; what the frame
     loses, it loses off the bottom, where the mask is fading anyway. */
  inset: clamp(2rem, 7vh, 4.5rem) 0 0 auto;
  z-index: 3;
  width: min(41vw, 600px);
  pointer-events: none;
  /* The photograph is already dark, so it needs far less help than a bright
     one: a soft left edge into the type, a whisper off the bottom. */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, rgba(0,0,0,.14) 5%, rgba(0,0,0,.36) 11%,
      rgba(0,0,0,.58) 17%, rgba(0,0,0,.76) 23%, rgba(0,0,0,.89) 30%,
      rgba(0,0,0,.97) 37%, #000 44%),
    linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.5) 3%, rgba(0,0,0,.85) 6%,
      #000 10%, #000 72%, rgba(0,0,0,.86) 82%, rgba(0,0,0,.58) 89%,
      rgba(0,0,0,.28) 95%, rgba(0,0,0,.08) 98%, transparent 100%),
    radial-gradient(112% 104% at 74% 46%, #000 34%, rgba(0,0,0,.9) 62%,
      rgba(0,0,0,.62) 80%, rgba(0,0,0,.3) 92%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-image:
    linear-gradient(to right, transparent 0%, rgba(0,0,0,.14) 5%, rgba(0,0,0,.36) 11%,
      rgba(0,0,0,.58) 17%, rgba(0,0,0,.76) 23%, rgba(0,0,0,.89) 30%,
      rgba(0,0,0,.97) 37%, #000 44%),
    linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.5) 3%, rgba(0,0,0,.85) 6%,
      #000 10%, #000 72%, rgba(0,0,0,.86) 82%, rgba(0,0,0,.58) 89%,
      rgba(0,0,0,.28) 95%, rgba(0,0,0,.08) 98%, transparent 100%),
    radial-gradient(112% 104% at 74% 46%, #000 34%, rgba(0,0,0,.9) 62%,
      rgba(0,0,0,.62) 80%, rgba(0,0,0,.3) 92%, transparent 100%);
          mask-composite: intersect;
  filter: brightness(1.06) contrast(1.02);
  animation: heroIn 1.8s var(--ease) both;
}
.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0%;
  /* Growth is anchored near his head, so the frame opens downward and the
     face never drifts out of the top of the picture. */
  transform-origin: 50% 8%;
  scale: 1.02;
  will-change: translate, scale;
  animation: photoCreep 80s linear 1.2s both;
}

/* Twelve and a half per cent over eighty seconds, linear so the rate never
   stalls. Still about a pixel a second — you cannot catch it moving, only
   notice that it has moved. */
@keyframes photoCreep {
  from { scale: 1.02; }
  to   { scale: 1.145; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__media img { animation: none; scale: 1.02; }
}

@keyframes heroIn {
  from { opacity: 0; scale: 1.04; }
  to   { opacity: 1; scale: 1; }
}
@media (prefers-reduced-motion: reduce) { .hero__media { animation: none; } }

/* The wash from the left. It stops earlier than the mask does, so the bright
   part of the photograph carries a little further into the dark. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(96deg,
      var(--bg) 8%,
      rgba(0, 0, 0, .5) 22%,
      rgba(0, 0, 0, .12) 38%,
      transparent 52%);
}

/* --------------------------------------------------------------------------
   Sand. Drawn in assets/js/sand.js — individual grains, so each one can fall
   at its own speed instead of a whole texture sliding. It sits above the
   portrait: dust in a room hangs between the viewer and the subject, and
   grains crossing him are what stop the photograph reading as a separate
   rectangle on the right.
   -------------------------------------------------------------------------- */

.hero__sand {
  position: absolute;
  inset: 0;
  z-index: 4;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* A whisper of grain over everything — the cheapest cure for gradient banding. */
.hero__noise {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  opacity: .022;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

.hero__inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding: calc(var(--nav-h) + clamp(1rem, 3vh, 2.5rem)) var(--gutter) clamp(1.25rem, 3vh, 2.25rem);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(1.5rem, 4vh, 3.25rem);
}

.hero__name {
  width: fit-content;
  font-size: var(--fs-hero);
  font-weight: 500;
  line-height: .86;
  letter-spacing: -.045em;
  text-transform: uppercase;
}
.hero__name > span { display: block; }

.hero .lineindex { max-width: min(70%, 940px); }

.hero__quotes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 19ch), 1fr));
  gap: clamp(1.5rem, 3vw, 3.5rem);
  align-items: start;
  max-width: min(58%, 720px);
}

@media (max-width: 900px) {
  .hero { display: block; height: auto; min-height: 100svh; }
  .hero__media {
    position: relative;
    z-index: 0;
    inset: auto;
    width: 100%;
    height: 52svh;
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 62%, rgba(0,0,0,.5) 84%, transparent 100%);
    -webkit-mask-composite: source-over;
            mask-image: linear-gradient(to bottom, #000 0%, #000 62%, rgba(0,0,0,.5) 84%, transparent 100%);
            mask-composite: add;
  }
  .hero::before { background: linear-gradient(180deg, transparent 40%, var(--bg) 92%); }
  .hero__inner { position: relative; margin-top: -18svh; gap: 2.5rem; }
  .hero .lineindex, .hero__quotes { max-width: none; }
}

/* --------------------------------------------------------------------------
   The opening of every inner page — identical structure, so the pages read
   as one publication rather than three separate designs.
   -------------------------------------------------------------------------- */

.opening {
  padding-top: calc(var(--nav-h) + clamp(4.5rem, 13vh, 9rem));
  padding-bottom: clamp(2rem, 4vw, 3.5rem);
}
/* The block after an opening belongs to it — no second helping of air. */
.opening + .section--tight,
.opening + .section { padding-top: clamp(1.5rem, 3vw, 3rem); }
/* An opening that carries a bleeding photograph needs room for it to read,
   and centres its type against the picture rather than sitting above it. */
.opening--bleed {
  /* The film inside is absolutely placed, and this is what it is placed
     against. Without it the film had no positioned ancestor and measured
     itself against the initial containing block — a viewport-sized box at the
     top of the document, which happens to be the right answer on a desktop
     where the opening is exactly one screen tall, and is the wrong answer
     everywhere else: on a phone the picture ran on past the opening and lay
     over the section below it. */
  position: relative;
  /* a full screen, so nothing from the next section spoils the entrance */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-bottom: clamp(3rem, 7vw, 6rem);
}
@media (max-width: 900px) {
  .opening--bleed { min-height: 0; display: block; }
}

/* --------------------------------------------------------------------------
   The front door's weather, carried inside.

   The landing page is warm because gold grains fall across it; an inner page
   that dropped them read as the same site with the lights off. So the same
   canvas runs here, at about three fifths the density — the front door should
   still be the loudest room — and the grains pass in front of the photograph
   for the same reason they do there: dust between you and the subject is what
   stops a picture reading as a rectangle laid on the page.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Sound's opening: the words stand in the room they describe.
   -------------------------------------------------------------------------- */

.opening--stage .bleed__body { position: relative; z-index: 2; width: 100%; }

/* The lede holds both paragraphs as one column against the picture, rather
   than the statement floating alone above a second block. */
.opening--stage .opening__lede { grid-column: 1 / 8; }
/* One step down from the page statement: at --fs-lg over eight columns this
   ran four long lines and competed with the title beside it. */
.opening--stage .opening__lede .statement {
  font-size: var(--fs-md);
  line-height: var(--leading-normal);
  max-width: 46ch;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}
.opening--stage .opening__title { margin-bottom: clamp(2rem, 4.5vw, 3.5rem); }
@media (max-width: 900px) {
  .opening--stage .opening__lede { grid-column: 1 / -1; }
}

/* The reel: two layers in the same slot, one dissolving into the next. Each
   carries its own grade, because each was shot in a different room. The slow
   scale is the only movement of its own the header has — a fifth of a pixel a
   frame, alternating direction per clip so the rhythm never repeats. */
.reel__layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
  opacity: 0;
  will-change: opacity, scale;
}

/* The photograph a film is going to replace, hidden from the very first
   paint on the screens that are getting the film. `film-first` is written on
   <html> by an inline script in the head of the two pages that have one — see
   the note there. ui.js sets an inline opacity of 1 to bring it back if the
   film never arrives, which beats this rule.

   Only the openings are marked. Every other photograph on the site, including
   the still in the Teaching header that no film ever replaces, is untouched. */
.film-first .film-still { opacity: 0; }

/* A still in the film's slot behaves exactly as the film does — same mask,
   same reading gradient — so swapping one for the other changes nothing but
   the source. */
.opening__film--still img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 46%;
  /* Measured the same way as the recital: this room is warm and red-lit, so
     it is pulled down to sit where every other picture on the site sits. */
  filter: brightness(.42) contrast(1.1) saturate(.4);
}

/* The title has to clear the film, which now spans the whole opening. */
.opening--bleed .bleed__body { z-index: 2; }

/* An opening goes out the way it came in.

   Every page but the first has a photograph on its first screen and a black
   page under everything after it, and the join between the two was a lit
   picture sliding up behind the writing. Now the picture goes with the
   screen it belongs to: --dim is written by startParallax() in motion.js as
   the opening scrolls off, and everything on it fades to nothing over three
   quarters of a viewport.

   The sand goes out with the picture. It was held back on the argument that
   the grains are the page's own weather and belong to the black as much as
   to the photograph — which is true of a room and false of a screen. What it
   actually produced was dust hanging in a void: the photograph gone, the
   studio gone, and a few hundred lit specks still falling through the black
   above the writing, catching a light that was no longer in the picture. */
.opening[data-dim] .opening__film,
.opening[data-dim] .opening__sand,
.opening[data-dim] .bleed__media { opacity: calc(1 - var(--dim, 0)); }

.opening__sand {
  position: absolute;
  inset: 0;
  z-index: 4;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.opening__noise {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  opacity: .022;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* --------------------------------------------------------------------------
   The opening film.

   It lies over the photograph rather than replacing it: the still is what
   paints on first load, and stays the whole story on a phone, on a metered
   connection, or where motion is unwelcome. The film fades in on top only
   once it can actually play.

   The hall in it is lit, warm and wooden and the page is none of those, so it
   is brought down hard to meet the room the rest of the site lives in. The
   mask on .bleed--right does the rest, feathering it into the black exactly
   as it feathers the photograph.
   -------------------------------------------------------------------------- */

/* The film gets the whole opening, not the portrait's column. Cropped into
   that column it kept the clarinet and lost the piano and the pianist, and
   the shot is of two people playing together. Full width, `cover` keeps
   about nine tenths of the frame on a normal laptop — the whole stage. */
.opening__film {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
  /* feathered top and bottom, so it neither cuts under the navigation nor
     ends on a line where the next section begins */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.6) 4%,
      #000 13%, #000 76%, rgba(0,0,0,.5) 92%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.6) 4%,
      #000 13%, #000 76%, rgba(0,0,0,.5) 92%, transparent 100%);
}

/* The reading side. Long ramp, so the title sits on solid black and the stage
   emerges from it without a seam. */
.opening__film::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(96deg, #000 5%, rgba(0, 0, 0, .9) 22%,
      rgba(0, 0, 0, .6) 40%, rgba(0, 0, 0, .26) 58%, rgba(0, 0, 0, .06) 72%,
      transparent 84%);
}

/* Wide enough, the words sit in the black at the left of the frame and the
   gradient above does the rest. Narrow, there is no left to sit in — the text
   runs the full width and the subject is under it — so the ramp turns and
   comes down from the top instead. */
@media (max-width: 900px) {
  .opening__film::after {
    background: linear-gradient(to bottom, #000 6%, rgba(0, 0, 0, .92) 26%,
        rgba(0, 0, 0, .68) 44%, rgba(0, 0, 0, .3) 66%, rgba(0, 0, 0, .08) 84%,
        transparent 100%);
  }
}

/* Cropped to the stage, not to the frame. Read off the picture: the bench
   starts at x=205 of 960, the piano runs to 620, he stands at 480-545, and
   everything past 700 is empty hall — which is what filled the right half of
   the screen. The window kept is x 0-670: pushed as far right as the frame
   allows, which carries the pianist out of the black the title sits on, with
   a little of the hall given back on the right. Enlargement 2.15x.

   Sized from the container's width rather than by `cover`, so that window is
   the same on every screen width: at 160% width one source pixel is 1.6/960
   of the viewport, which makes exactly 600 source pixels visible whatever the
   viewport is. min-height is only a floor for a freakishly tall window, where
   `cover` takes over rather than leaving a gap. */
.bleed__film {
  position: absolute;
  left: 0%;
  top: 50%;
  /* The bias that puts the players, not the ceiling, in the middle of the
     window. Held at 0.537 so the element's own bottom still reaches past the
     container's on a 4:3 window — any higher and a strip of nothing opens
     along the bottom. Checked from 1.28:1 to 1.78:1. */
  translate: 0 -53.7%;
  width: 143.3%;
  height: auto;
  min-height: 110%;   /* the floor the bias needs: 0.5 / (1 - 0.537), with margin */
  max-width: none;
  object-fit: cover;
  /* Opacity belongs to the script and to nothing else. There used to be a
     CSS fade for the arrival as well, and the two took turns on the same
     property: the frame appeared at full strength and only then began to
     fade in. Now there is one mechanism. The film starts at zero and rises
     with the first second of playback — the arrival is the loop's own
     fade-in, which is why the two look identical. */
  opacity: 0;
  /* Graded to land where the previous film landed, not by eye. Measured: this
     hall is bright — mean luminance 141 against the old clip's 58, and half
     again as saturated. Left at the old settings it would sit at 66/255 on a
     page whose film used to sit at 21. These numbers put it at 24. */
  filter: brightness(.26) contrast(1.14) saturate(.3);
}

/* --------------------------------------------------------------------------
   Teaching's opening: what he believes, set as a quotation.

   The page's argument is a paragraph, not a portfolio, so the first screen is
   given to it whole — and given to it as a quotation, with a proper mark at
   display size rather than two typed inches of punctuation inside the text.

   The photograph behind measures 13.5 of 255 mean, darker than anything else
   on the site, with the subject right of centre and the left half of the frame
   already black. So it is barely touched: a light hand where every other
   picture here needed a heavy one.
   -------------------------------------------------------------------------- */

.opening__film--key img {
  /* Pushed a quarter of the screen to the right, which puts him in the right
     third and turns him toward the words instead of standing on them. `cover`
     alone cannot do this: the frame is 3:2 against a 16:9 opening, so it is
     cropped top and bottom and has no horizontal slack to give. Moving the
     whole picture is the honest way — what opens on the left is black, which
     is what the reading gradient was painting there anyway. */
  left: 25%;
  object-position: 50% 44%;
  /* This frame measures 25.8 of 255 against the last one's 13.5 — the shirt
     fills far more of it — so it is taken down further to land at the same
     depth on the page. */
  filter: grayscale(1) brightness(.5) contrast(1.05);
  /* The same two movements the portrait on the first screen has, so the two
     first screens are the same gesture: it arrives out of the black, and
     then it creeps for eighty seconds — a pixel a second, which you cannot
     catch happening and cannot miss having happened. The pair are on
     separate properties, opacity and scale, so neither writes over the
     other; and the arrival is kept off the film itself, which is where the
     scroll fade lives and would be overwritten by an animation holding its
     last frame. Growth is anchored left of centre and high, where he stands
     once the frame has been pushed its quarter-screen to the right. */
  transform-origin: 38% 34%;
  animation: filmIn 1.8s var(--ease) both,
             photoCreep 80s linear 1.2s both;
}
@keyframes filmIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .opening__film--key img { animation: none; scale: 1.02; }
}

/* The same measure and the same step as the lede on Sound and Performance —
   a quotation is still the text under a page title, and this one was reading
   a size larger than every other opening on the site. */
.creed {
  position: relative;
  max-width: min(46ch, 55%);
  padding-top: clamp(2.4rem, 4.4vw, 3.6rem);
}

/* The marks are set in a serif, and they are the only characters on this site
   that are. Inter Tight draws its quotes as two slanted bars — correct inside
   a line of text, and at eighty pixels it is a pause button. A serif quote is
   a pair of drawn commas with a ball and a tail, which is what a quotation
   mark is for, so the display pair borrows one. No download: this is whatever
   serif the reader already has. */
.creed__mark {
  font-family: Georgia, 'Iowan Old Style', 'Times New Roman', serif;
  color: var(--accent);
  opacity: .34;
  pointer-events: none;
  -webkit-user-select: none;
          user-select: none;
}

/* The opening mark sits in the corner the words start from, and arrives a
   beat after them — growing out of that corner rather than fading onto it. */
.creed__mark--open {
  position: absolute;
  top: 0;
  /* Georgia carries a wide left bearing on this glyph, so the mark is pulled
     back until its ink, not its box, lines up with the text below it. */
  left: -.085em;
  font-size: clamp(4.75rem, 8.5vw, 7.75rem);
  line-height: .52;
  opacity: 0;
  scale: .84;
  transform-origin: top left;
  transition: opacity 1.1s var(--ease) .35s, scale 1.2s var(--ease) .35s;
}
.creed.is-in .creed__mark--open { opacity: .34; scale: 1; }

/* The closing mark hangs off the last word instead of sitting in the line, so
   the motto's own left edge stays the straight one. */
.creed__mark--close {
  font-size: 1.75em;
  line-height: 0;
  vertical-align: -.02em;
  margin-left: .06em;
}

.creed__lead {
  font-size: var(--fs-md);
  font-weight: 400;
  line-height: var(--leading-normal);
  color: var(--text-dim);
}



/* The four words the whole page is built on, so they are the brightest thing
   on the screen and they stand on their own line with a brass rule to the
   left of them — the one place on this page the accent is spent. */
.creed__motto {
  position: relative;
  margin-top: clamp(1.1rem, 2.2vw, 1.75rem);
  padding-left: clamp(1rem, 2vw, 1.6rem);
  font-size: var(--fs-md);
  font-weight: 500;
  line-height: 1.45;
  color: var(--text);
}
.creed__motto::before {
  content: '';
  position: absolute;
  left: 0;
  top: .18em;
  bottom: .18em;
  width: 1px;
  background: var(--accent-dim);
}

@media (max-width: 900px) {
  .creed { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .creed__mark--open { transition: none; opacity: .34; scale: 1; }
}

/* The sound control only appears once there is sound to turn on. */
.filmsound {
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  color: var(--text-faint);
  transition: color var(--dur) var(--ease);
}
.filmsound:hover,
.filmsound:focus-visible { color: var(--text); }
.filmsound.is-on { color: var(--accent); }
.filmsound__label { letter-spacing: .16em; text-transform: uppercase; }

.filmsound__bars { display: flex; align-items: flex-end; gap: 2px; height: 12px; }
.filmsound__bars i {
  width: 2px;
  height: 3px;
  background: currentColor;
  transition: height var(--dur) var(--ease);
}
.filmsound.is-on .filmsound__bars i { animation: filmBars 1.1s ease-in-out infinite alternate; }
.filmsound.is-on .filmsound__bars i:nth-child(2) { animation-delay: -.38s; }
.filmsound.is-on .filmsound__bars i:nth-child(3) { animation-delay: -.72s; }
@keyframes filmBars {
  from { height: 3px; }
  to   { height: 12px; }
}
@media (prefers-reduced-motion: reduce) {
  .filmsound.is-on .filmsound__bars i { animation: none; height: 8px; }
}

/* The photographs breathe. The hero creeps one way over eighty seconds because
   it never leaves the screen; these two scroll past, so they draw slowly in
   and slowly back out instead, and are never caught at an end. Growth is
   anchored at the head on both, so the frame opens downward and the face
   holds its place — the same rule the hero follows.

   Five per cent across fifty-eight seconds is about four fifths of a pixel a
   second. You cannot see it move; you notice it has moved. */
@keyframes photoBreath {
  from { scale: 1; }
  to   { scale: 1.05; }
}

.opening--bleed .bleed__media img {
  transform-origin: 50% 12%;
  will-change: translate, scale;
  animation: photoBreath 58s var(--ease) infinite alternate;
}
/* half a period out of step, so the two never swell together */
.bleed--portrait .bleed__media img {
  transform-origin: 50% 15%;
  will-change: translate, scale;
  animation: photoBreath 58s var(--ease) -29s infinite alternate;
}

@media (prefers-reduced-motion: reduce) {
  .opening--bleed .bleed__media img,
  .bleed--portrait .bleed__media img { animation: none; scale: 1; }
}

/* A page title is a marker, not a billboard. It names the chapter and gets out
   of the way; the words that follow are what should be large. */
.opening__title {
  font-size: var(--fs-xl);
  line-height: 1;
  letter-spacing: .055em;
  text-transform: uppercase;
  color: var(--text);
}
.opening__statement {
  grid-column: 3 / 11;
  margin-top: clamp(2rem, 5vw, 4rem);
}
@media (max-width: 900px) {
  .opening__statement { grid-column: 1 / -1; margin-top: 2rem; }
}

.sectionhead {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
  padding-bottom: clamp(1.25rem, 2.5vw, 2rem);
  /* The rule is drawn by the ::before below, not painted here — this only
     holds the height so nothing shifts when it arrives. */
  border-bottom: 1px solid transparent;
  margin-bottom: clamp(1.75rem, 3.5vw, 3rem);
}

/* A block announces itself by drawing its own line.

   The complaint this answers is that a reader might not believe there is
   anything below. Space alone cannot fix that — you can always add more of it
   and the page still looks finished. A line that draws itself across the
   width as the block arrives cannot be mistaken for an ending: something is
   being built, so something follows. And it is the site's own vocabulary —
   the whole page is held together by hairlines — rather than an arrow or the
   word "scroll" bolted on. */
.sectionhead::before {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: 1px;
  background: var(--rule);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 1.15s var(--ease);
}
.sectionhead:has([data-reveal].is-in)::before { transform: scaleX(1); }

/* Without :has, without script, and without motion, the line is simply
   there — it is structure first and an entrance second. */
@supports not selector(:has(*)) {
  .sectionhead::before { transform: scaleX(1); }
}
html:not(.js) .sectionhead::before { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .sectionhead::before { transform: scaleX(1); transition: none; }
}

/* The same light that travels the hairlines on the landing page, taking the
   section rules here in turn. One pass every nine seconds, and never two
   rules lit at once — which is why the cycle is four slots long: the Sound
   page has four section heads, and at 27s its first and fourth lit
   together. */
.sectionhead::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 1px;
  width: 30%;
  background: linear-gradient(90deg, transparent, rgba(194, 161, 92, .5), transparent);
  transform: translate3d(-120%, 0, 0);
  animation: ruleSweep 36s var(--ease) infinite;
  animation-delay: calc(var(--i, 0) * 9s);
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .sectionhead::after { animation: none; opacity: 0; }
}

/* --------------------------------------------------------------------------
   A photograph given a band of its own, contained rather than full-bleed
   -------------------------------------------------------------------------- */

.plate { overflow: hidden; background: var(--bg-raise); position: relative; }
.plate img { width: 100%; height: 100%; object-fit: cover; scale: 1.12; will-change: translate; }
.plate--wide { aspect-ratio: 3 / 2; }
.plate::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(0, 0, 0,.3), rgba(0, 0, 0,0) 32%, rgba(0, 0, 0,.34));
}

/* --------------------------------------------------------------------------
   Shared blocks
   -------------------------------------------------------------------------- */

.videogrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
  gap: clamp(1.5rem, 3vw, 2.75rem);
}

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

/* --- one screen, and no scrolling ---------------------------------------

   Contact is not a page you read down; it is a page you act on. Everything
   it has — the address, the four other ways to reach him, the form and the
   line at the end — fits between the nav and the footer, and the document
   is told not to scroll at all. Only where there is genuinely room for it:
   below 901 wide or 700 tall the page goes back to being a page, because a
   screen that cannot hold the content does not stop being one screen, it
   starts hiding things.
   ------------------------------------------------------------------------- */

.onescreen {
  position: relative;
  overflow: hidden;
  padding-top: var(--nav-h);
}
.onescreen__notes {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}
.onescreen__body {
  position: relative;
  z-index: 1;
  width: 100%;
  padding-block: clamp(1.5rem, 5vh, 4rem);
}

@media (min-width: 901px) and (min-height: 700px) {
  .page--onescreen {
    height: 100dvh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
  .page--onescreen .onescreen {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    align-items: center;
  }
  .page--onescreen .footer { flex: none; }

  /* Everything gives up a little height so that nothing has to give up any.
     The units are vh on purpose: the form is not being made small, it is
     being made to fit the screen it is actually on. */
  .page--onescreen .form { padding: clamp(1.1rem, 2.4vh, 2.4rem); }
  .page--onescreen .form__lead { margin-bottom: clamp(.85rem, 2vh, 1.6rem); }
  .page--onescreen .field { margin-bottom: clamp(.65rem, 1.5vh, 1.2rem); }
  .page--onescreen .field textarea { min-height: clamp(5rem, 14vh, 9rem); }
  .page--onescreen .form__foot {
    margin-top: clamp(.85rem, 1.8vh, 1.6rem);
    padding-top: clamp(.85rem, 1.8vh, 1.6rem);
  }
}

.contact {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: center;
}
@media (max-width: 900px) { .contact { grid-template-columns: 1fr; align-items: start; } }

.contact__side {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(.9rem, 2.2vh, 1.9rem);
}

/* The heading, set as the label it also is. */
.contact__eyebrow { color: var(--text-faint); font-weight: 400; }

/* The address, and a link to write to it. `user-select: all` came off with
   the span: on a link it fights the click it is sitting under, and a single
   press that sometimes opens mail and sometimes selects a line of text is
   worse than either. Selecting it by hand still works the way it does
   anywhere else. */
.contact__mail {
  display: block;
  font-size: clamp(1.5rem, 1rem + 2.1vw, 2.6rem);
  font-weight: 500;
  letter-spacing: -.035em;
  line-height: 1.05;
  word-break: break-word;
  padding-bottom: .3em;
  border-bottom: 1px solid var(--rule-strong);
  transition: color .45s var(--ease-soft), border-color .45s var(--ease-soft);
}
.contact__mail:hover {
  color: var(--accent);
  border-bottom-color: rgba(194, 161, 92, .5);
}
@media (prefers-reduced-motion: reduce) { .contact__mail { transition: none; } }

/* The line at the end. It is the only sentence on the screen, so it is set
   as one — measured narrow, a step down in colour, and left where a reader
   finishes rather than where they start. */
.contact__note {
  font-size: var(--fs-sm);
  line-height: var(--leading-normal);
  color: var(--text-dim);
  max-width: 46ch;
}
