/* ==========================================================================
   Components. Every block aligns to the 12-column spine defined in base.css:
   labels at column 1, reading text at column 3, images at column 9.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Navigation
   -------------------------------------------------------------------------- */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-nav);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  border-bottom: 1px solid transparent;
  transition: background var(--dur) var(--ease),
              backdrop-filter var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.nav.is-scrolled {
  background: rgba(0, 0, 0, .76);
  backdrop-filter: blur(14px) saturate(130%);
  -webkit-backdrop-filter: blur(14px) saturate(130%);
  border-bottom-color: var(--rule-soft);
}

.nav__inner {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
}

.nav__mark {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text);
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-out);
}
.nav__mark:hover { color: var(--accent); }

.nav__links { display: flex; align-items: center; gap: clamp(1.25rem, 2.4vw, 2.5rem); }

.nav__link {
  position: relative;
  font-size: var(--fs-sm);
  color: var(--text-dim);
  padding-block: .35rem;
  transition: color var(--dur-fast) var(--ease-out);
}
.nav__link::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}
.nav__link:hover { color: var(--text); }
.nav__link:hover::after { transform: scaleX(1); }
.nav__link[aria-current='page'] { color: var(--text); }
.nav__link[aria-current='page']::after { transform: scaleX(1); background: var(--rule-strong); }

.nav__cta {
  font-size: var(--fs-sm);
  border: 1px solid var(--rule);
  padding: .55rem 1.15rem;
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.nav__cta:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); }

.nav__burger { display: none; width: 34px; height: 34px; position: relative; z-index: calc(var(--z-menu) + 1); }
.nav__burger i {
  position: absolute;
  left: 5px;
  width: 24px; height: 1px;
  background: var(--text);
  transition: transform var(--dur) var(--ease);
}
.nav__burger i:nth-child(1) { top: 13px; }
.nav__burger i:nth-child(2) { top: 20px; }
body.menu-open .nav__burger i:nth-child(1) { transform: translateY(3.5px) rotate(45deg); }
body.menu-open .nav__burger i:nth-child(2) { transform: translateY(-3.5px) rotate(-45deg); }

.menu {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  background: var(--bg);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--nav-h) var(--gutter) var(--gutter);
  clip-path: inset(0 0 100% 0);
  transition: clip-path .7s var(--ease), visibility 0s linear .7s;
  visibility: hidden;
  overscroll-behavior: contain;
}
body.menu-open .menu {
  clip-path: inset(0 0 0 0);
  visibility: visible;
  transition: clip-path .7s var(--ease), visibility 0s linear 0s;
}

.menu__list { display: flex; flex-direction: column; gap: .35rem; }
.menu__item a {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  font-size: clamp(2.25rem, 11vw, 3.5rem);
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  line-height: 1.12;
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .5s var(--ease), transform .5s var(--ease), color var(--dur-fast) linear;
}
body.menu-open .menu__item a {
  opacity: 1;
  transform: none;
  transition-delay: calc(.18s + var(--i, 0) * .06s);
}
.menu__item a:hover { color: var(--accent); }

.menu__foot {
  margin-top: auto;
  padding-top: 2rem;
  border-top: 1px solid var(--rule-soft);
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 1.5rem;
}

@media (max-width: 860px) {
  .nav__links { display: none; }
  .nav__burger { display: block; }
}

/* --------------------------------------------------------------------------
   2. Line index — the three disciplines on the home page
   -------------------------------------------------------------------------- */

.lineindex { border-top: 1px solid var(--rule); }

.lineindex__row {
  display: block;
  border-bottom: 1px solid var(--rule);
  padding-block: clamp(.7rem, 1.5vh, 1.35rem);
}
.lineindex__inner {
  display: flex;
  align-items: baseline;
  gap: clamp(.75rem, 2vw, 2rem);
  padding-left: var(--offset, 0%);
  transition: padding-left .8s var(--ease);
}
.lineindex__label {
  font-size: clamp(1.15rem, min(2.6vw, 4vh), 2.5rem);
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  line-height: 1;
  white-space: nowrap;
  transition: color var(--dur) var(--ease);
}
/* A slow light travels along each rule in turn — the page's only moving part
   that touches the type, and it borrows a line that is already there rather
   than adding decoration. One pass roughly every nine seconds. */
.hero .lineindex__row {
  position: relative;
  overflow: hidden;
}
.hero .lineindex__row::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 1px;
  width: 30%;
  background: linear-gradient(90deg, transparent, rgba(194, 161, 92, .605), transparent);
  transform: translate3d(-120%, 0, 0);
  animation: ruleSweep 27s var(--ease) infinite;
  animation-delay: calc(var(--i, 0) * 9s);
  pointer-events: none;
}
@keyframes ruleSweep {
  0%        { transform: translate3d(-120%, 0, 0); }
  34%, 100% { transform: translate3d(400%, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .hero .lineindex__row::after { animation: none; opacity: 0; }
}

.lineindex__row:hover .lineindex__inner,
.lineindex__row:focus-visible .lineindex__inner { padding-left: calc(var(--offset, 0%) + 1.5rem); }
.lineindex__row:hover .lineindex__label { color: var(--accent); }

/* --------------------------------------------------------------------------
   3. Pull quote — a quotation marked by a rule and a mono attribution,
      never by italics.
   -------------------------------------------------------------------------- */

.pull { display: flex; flex-direction: column; gap: 1rem; }
.pull::before {
  content: '';
  width: 2.5rem;
  height: 1px;
  background: var(--accent);
}
.pull__text {
  font-size: var(--fs-md);
  line-height: 1.42;
  letter-spacing: -.015em;
  color: var(--text);
  max-width: 26ch;
}
.pull__by { display: flex; flex-direction: column; gap: .25rem; }
.pull__name { font-size: var(--fs-sm); color: var(--text); }

/* --------------------------------------------------------------------------
   4. Voices — testimonials as one quote at a time, chosen from a list
   -------------------------------------------------------------------------- */

.voices {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: var(--col-gap);
  row-gap: clamp(2rem, 4vw, 3rem);
  align-items: start;
}

.voices__list {
  grid-column: 1 / 3;
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--rule);
}

.voices__btn {
  display: flex;
  align-items: baseline;
  gap: .7rem;
  width: 100%;
  text-align: left;
  padding: .7rem 0;
  border-bottom: 1px solid var(--rule-soft);
  color: var(--text-faint);
  font-size: var(--fs-sm);
  line-height: 1.3;
  transition: color var(--dur) var(--ease), padding-left var(--dur) var(--ease);
}
.voices__btn:hover { color: var(--text-dim); padding-left: .45rem; }
.voices__btn[aria-selected='true'] { color: var(--text); }
.voices__btn[aria-selected='true'] 
.voices__stage {
  grid-column: 3 / 11;
  min-height: 14rem;
  transition: opacity .35s var(--ease-out), transform .45s var(--ease-out);
}
.voices__stage.is-out { opacity: 0; transform: translateY(10px); }

.voices__quote {
  font-size: var(--fs-lg);
  line-height: 1.36;
  letter-spacing: -.022em;
  color: var(--text);
  max-width: 42ch;
  text-wrap: pretty;
}
.voices__meta {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  display: flex;
  flex-direction: column;
  gap: .3rem;
}
.voices__name { font-size: var(--fs-md); color: var(--text); }

@media (max-width: 900px) {
  .voices { grid-template-columns: 1fr; }
  .voices__list, .voices__stage { grid-column: 1 / -1; }
  .voices__stage { min-height: 0; }
}

/* --------------------------------------------------------------------------
   5b. The philosophy — an index and a stage. Built by renderCreedset().

   The whole of this block rests on one idea: it never changes height. The
   four texts share a single grid cell, so the stage is as tall as the
   longest of them from the first frame, and choosing between them moves
   nothing but the words. Every effect below is affordable only because of
   that — motion on a block that is also reflowing is just noise.
   -------------------------------------------------------------------------- */

/* One object, not three columns that happen to be near each other. The panel
   is a whisper above the page — #0C0C0C on #030303 — with a light laid along
   its top edge, which is the only way to give something volume on a black
   page: a shadow under it would be black on black and say nothing, so the
   thing is lit from above instead of shaded from below. */
.creedset {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: var(--col-gap);
  align-items: stretch;
  padding: clamp(1.5rem, 2.8vw, 2.6rem);
  background:
    linear-gradient(180deg,
      rgba(237, 232, 222, .03),
      rgba(237, 232, 222, .008) 34%,
      transparent 78%),
    var(--bg-raise);
  border: 1px solid var(--rule-soft);
  border-radius: 16px;
}

/* --- the index ---------------------------------------------------------- */

/* Spread over the whole height of the panel rather than bunched at the top.
   Four names in the first third of a tall column left the other two thirds
   empty, and an empty two thirds reads as something missing rather than as
   room. Spread, they are an index down the side of the block, the rail runs
   the full drop, and the mark has somewhere to travel. */
.creedset__index {
  grid-column: 1 / 4;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

/* A hairline down the whole index and a brass mark on it that slides to
   whichever name is showing. One mark that travels, not four that light up:
   a thing that moves between two places says they are two places in one
   list, and four that switch on and off say nothing at all. */
.creedset__rail {
  position: absolute;
  inset: 0 auto 0 0;
  width: 1px;
  background: var(--rule);
}
.creedset__mark {
  position: absolute;
  left: -1px;
  width: 2px;
  height: var(--mh, 0);
  translate: 0 var(--my, 0);
  background: linear-gradient(180deg,
                transparent,
                rgba(194, 161, 92, .9) 22%,
                rgba(194, 161, 92, .9) 78%,
                transparent);
  transition: translate .62s var(--ease-soft), height .62s var(--ease-soft);
}

.creedrow {
  padding: clamp(.75rem, 1.5vw, 1.05rem) 0 clamp(.75rem, 1.5vw, 1.05rem)
           clamp(1.1rem, 2vw, 1.6rem);
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  font-size: var(--fs-lg);
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  color: var(--text-faint);
  transition: color .45s var(--ease-soft);
}
.creedrow:hover { color: var(--text-dim); }
.creedrow[aria-selected="true"] { color: var(--text); }

/* The name leans in a couple of pixels as it is chosen — the distance a page
   moves when a finger holds it down, and the only reason it is here is that
   the eye needs somewhere to land after the click and before the words on
   the far side of the block have finished arriving. */
.creedrow__name {
  display: block;
  translate: 0;
  transition: translate .5s var(--ease-soft);
}
.creedrow[aria-selected="true"] .creedrow__name { translate: 3px 0; }

/* --- the stage ---------------------------------------------------------- */

.creedset__stage {
  grid-column: 4 / 10;
  align-self: start;
  display: grid;
  border-top: 1px solid var(--rule);
  padding-top: clamp(1.3rem, 2.4vw, 2rem);
}

/* All four in the one cell. The stage takes the height of the tallest and
   keeps it, which is the whole trick — a pane set to `display: none` would
   give its share of the cell back and the stage would shrink to whatever was
   showing, which is the jumping this block was built to be rid of.

   So the three that are not showing keep their space and are turned off
   another way: `inert`, set in JS, takes a pane out of the tab order and out
   of the screen reader, and opacity does the fading. It used to be a delayed
   `visibility` transition doing both jobs at once, which is a state that only
   arrives if frames keep coming — in a background tab they do not, and the
   pane stayed in the reading order of the page indefinitely. A property is a
   worse place to keep a fact than a property that cannot lapse. */
.creedtext {
  grid-area: 1 / 1;
  position: relative;
  max-width: 62ch;
  opacity: 0;
  pointer-events: none;
  transition: opacity .26s var(--ease-soft);
}
.creedtext.is-on {
  opacity: 1;
  pointer-events: auto;
  transition: opacity .4s var(--ease-soft);
}

/* A light crosses the rule above the stage, once, each time the text under it
   changes. The animation is declared on the pane that is showing, so it
   starts by itself whenever a pane starts showing — nothing has to restart
   it, and there is no state anywhere that says whether it has run. */
.creedtext::before {
  content: '';
  position: absolute;
  top: calc(clamp(1.3rem, 2.4vw, 2rem) * -1 - 1px);
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg,
                transparent,
                rgba(194, 161, 92, .75) 45%,
                rgba(194, 161, 92, .75) 55%,
                transparent);
  opacity: 0;
  pointer-events: none;
}
.creedtext.is-on::before { animation: creedSweep 1.05s var(--ease) both; }
@keyframes creedSweep {
  0%   { opacity: 0;   clip-path: inset(0 100% 0 0); }
  18%  { opacity: .9; }
  70%  { opacity: .5; }
  100% { opacity: 0;   clip-path: inset(0 0 0 0); }
}

/* The paragraphs come up in order rather than together. Same reason a
   sentence is read and not seen: two paragraphs arriving at the same instant
   are a block of grey, and half a beat apart they are a first one and a
   second one. */
.creedtext p + p { margin-top: 1.1em; }
/* `backwards` and not `both`: the paragraph's resting state is visible, and
   the animation only holds it back during its delay. Filled forwards, the
   words would be readable *because* an animation had finished — and anything
   that can fail to run, from a background tab to a browser that never got to
   the frame, would leave the text invisible with nothing to say why. */
.creedtext.is-on p {
  animation: creedRise .62s var(--ease-soft) backwards;
  animation-delay: calc(var(--p, 0) * .09s + .06s);
}
@keyframes creedRise {
  from { opacity: 0; translate: 0 14px; }
  to   { opacity: 1; translate: 0 0; }
}

/* --- him ---------------------------------------------------------------- */

/* A studio portrait on a light backdrop. It ends where it ends — the bottom
   was dissolving into the page before, which was the right answer while the
   photograph was sitting directly on the black and the wrong one now: inside
   a panel it is a picture in a room, and a picture in a room has four edges.

   Its corners are rounded a little less than the panel's, which is what a
   thing inside another thing does. */
.creedset__figure {
  grid-column: 10 / 13;
  margin: 0;
  align-self: start;
}
.creedset__figure img {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  object-position: 50% 18%;
  border-radius: 10px;
  /* Held a step under, so the brightest thing in the block stays the words.
     A high-key photograph next to reading text wins every time otherwise,
     and this block is something to be read. */
  filter: brightness(.88) contrast(1.03);
}

/* Narrow: the index goes above the stage, the words under it, and him last.
   The same object turned through nothing at all — the names are still a list
   with a mark on it, and the words are still underneath. He is capped at a
   width a phone can hold, and stays on the left edge with everything else
   rather than floating in the middle of the column. */
@media (max-width: 900px) {
  .creedset { grid-template-columns: 1fr; row-gap: clamp(1.5rem, 4vw, 2.2rem); }
  .creedset__index,
  .creedset__stage,
  .creedset__figure { grid-column: 1 / -1; }
  /* Nothing to spread into down here: the column is only as tall as the four
     names, and space-between with nothing spare is a list with even gaps. */
  .creedset__index { justify-content: flex-start; }
  .creedset__figure { max-width: 19rem; }
  .creedtext { max-width: none; }
}

/* Without motion the mark is where it belongs, the text is simply there, and
   the light never runs. Nothing is lost: every one of those three was saying
   something the colour and the position already say. */
@media (prefers-reduced-motion: reduce) {
  .creedset__mark,
  .creedrow,
  .creedrow__name,
  .creedtext { transition: none; }
  .creedrow[aria-selected="true"] .creedrow__name { translate: 0; }
  .creedtext.is-on::before { animation: none; }
  .creedtext.is-on p { animation: none; }
}

/* --------------------------------------------------------------------------
   6. Journal cards — image, number, title, description
   -------------------------------------------------------------------------- */

.journal {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  gap: clamp(1.5rem, 3vw, 3rem);
}
.journal__item { display: flex; flex-direction: column; gap: .85rem; }

.journal__media {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border: 1px solid var(--rule);
  background: var(--bg-raise);
  margin-bottom: .35rem;
}
.journal__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.04) brightness(.95);
  transition: transform 1.3s var(--ease), filter .9s var(--ease);
}
.journal__item:hover .journal__media img {
  transform: scale(1.05);
  filter: grayscale(0) contrast(1) brightness(1);
}
.journal__title { font-size: var(--fs-lg); font-weight: 500; letter-spacing: -.028em; line-height: var(--leading-snug); }
.journal__body { font-size: var(--fs-sm); line-height: 1.62; color: var(--text-dim); text-wrap: pretty; }

/* --------------------------------------------------------------------------
   7. Accordion (repertoire)
   -------------------------------------------------------------------------- */

.acc { border-top: 1px solid var(--rule); }
.acc__group { border-bottom: 1px solid var(--rule); }

.acc__head {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding-block: clamp(1rem, 2vw, 1.5rem);
  text-align: left;
  transition: color var(--dur-fast) var(--ease-out);
}
.acc__head:hover { color: var(--accent); }
.acc__title {
  font-size: var(--fs-lg);
  font-weight: 500;
  letter-spacing: -.028em;
  display: flex;
  align-items: baseline;
  gap: 1rem;
}

.acc__sign { position: relative; width: 14px; height: 14px; flex: none; }
.acc__sign::before, .acc__sign::after {
  content: '';
  position: absolute;
  background: currentColor;
  transition: transform .5s var(--ease), opacity .3s linear;
}
.acc__sign::before { top: 6.5px; left: 0; width: 14px; height: 1px; }
.acc__sign::after  { left: 6.5px; top: 0; width: 1px; height: 14px; }
.acc__group.is-open .acc__sign::after { transform: scaleY(0); opacity: 0; }

.acc__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .6s var(--ease); }
.acc__group.is-open .acc__panel { grid-template-rows: 1fr; }
.acc__panelInner { overflow: hidden; }

.acc__list { padding-bottom: 1.75rem; columns: 2; column-gap: clamp(2rem, 5vw, 5rem); }
@media (max-width: 780px) { .acc__list { columns: 1; } }

/* Set as one running line, the way the printed repertoire sets it: full name,
   dash, work. A reserved composer column can't hold both "Bug" and "Mario
   Castelnuovo-Tedesco" without going ragged, and the names are the point.
   Wrapped lines hang under the composer rather than under the margin. */
.acc__item {
  display: block;
  padding-block: .5rem;
  padding-left: 1.15rem;
  text-indent: -1.15rem;
  border-top: 1px solid var(--rule-soft);
  font-size: var(--fs-sm);
  line-height: 1.5;
  break-inside: avoid;
}
.acc__composer { color: var(--text); }
.acc__dash { color: var(--text-faint); padding-inline: .42em; }
.acc__work { color: var(--text-dim); transition: color var(--dur) var(--ease); }
.acc__item:hover .acc__work { color: var(--text); }

/* --------------------------------------------------------------------------
   8. Video — poster first, iframe only once the user clicks
   -------------------------------------------------------------------------- */

.video {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--bg-raise);
  border: 1px solid var(--rule-soft);
  cursor: pointer;
  width: 100%;
}
.video img { width: 100%; height: 100%; object-fit: cover; transition: transform 1.2s var(--ease), opacity var(--dur) var(--ease); }
.video:hover img { transform: scale(1.03); opacity: .78; }
.video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.video__play { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; }
.video__play i {
  width: clamp(56px, 6vw, 78px);
  aspect-ratio: 1;
  border: 1px solid rgba(237, 232, 222, .5);
  border-radius: 50%;
  display: grid;
  place-items: center;
  backdrop-filter: blur(6px);
  background: rgba(0, 0, 0, .3);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.video__play i::after {
  content: '';
  border-left: 11px solid var(--text);
  border-block: 7px solid transparent;
  margin-left: 3px;
  transition: border-left-color var(--dur) var(--ease);
}
.video:hover .video__play i { background: var(--accent); border-color: var(--accent); }
.video:hover .video__play i::after { border-left-color: #000000; }

.video__cap { margin-top: .9rem; display: flex; justify-content: space-between; gap: 1rem; font-size: var(--fs-sm); }

/* --------------------------------------------------------------------------
   8a. Video tiles — the films in order, one card each

   The same rectangle as the services: a card, a hairline, the poster inside
   it and the title underneath. Four films read as four things you can pick,
   which is what a portfolio is, rather than one player and a list.
   -------------------------------------------------------------------------- */

/* Held narrower than the page: at full width two films came to 650px each
   and read as a slideshow rather than as a portfolio of four. */
.vtiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.75rem);
  max-width: 64rem;
  margin-inline: auto;
}

.vtile {
  background: var(--bg-raise);
  border: 1px solid var(--rule);
  border-radius: 14px;
  padding: clamp(.7rem, 1.1vw, 1rem);
  transition: border-color .45s var(--ease), background .45s var(--ease),
              translate .45s var(--ease);
}
.vtile:hover {
  border-color: rgba(194, 161, 92, .28);
  background: var(--bg-elev);
  translate: 0 -2px;
}

.vtile .video { border: 0; border-radius: 9px; }

/* Title and credit stack: the card is half the width the caption row was
   written for, and two lines read better there than one squeezed line. */
.vtile .video__cap {
  margin-top: .85rem;
  padding: 0 .3rem .2rem;
  flex-direction: column;
  align-items: flex-start;
  gap: .45rem;
  line-height: 1.35;
}
/* The credit is set in the label face, not the reading face — it is metadata
   about the record, and the page marks metadata that way everywhere else. */
.vtile .video__cap .mono { color: var(--text-faint); }

@media (max-width: 900px) {
  .vtiles { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .vtile { transition: none; }
}

/* --------------------------------------------------------------------------
   7c. Reviews — the whole testimonial, with a face and a rating

   Clipped quotes read as advertising copy; the full text reads as somebody
   talking. Each one is laid on the page's own spine — who on the left, what
   they said on the right — and separated by the same hairline the section
   heads use, so five voices are five entries and not one slab.
   -------------------------------------------------------------------------- */

.reviews {
  display: grid;
  gap: clamp(2.25rem, 4.5vw, 4rem);
}

.review {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1.25rem, 2.5vw, 2rem);
  padding-top: clamp(1.5rem, 2.6vw, 2.25rem);
  border-top: 1px solid var(--rule);
  position: relative;
}
/* The brass hairline draws itself across as the review arrives — the page's
   one accent, doing the one thing it does everywhere else. */
.review::before {
  content: '';
  position: absolute;
  top: -1px;
  left: 0;
  width: 0;
  height: 1px;
  background: var(--accent);
  transition: width .9s var(--ease);
  transition-delay: calc(var(--i, 0) * .09s);
}
.review.is-in::before { width: 3.5rem; }

@media (min-width: 900px) {
  .review { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); }
}

/* --- who said it -------------------------------------------------------- */

.review__who {
  display: flex;
  align-items: center;
  gap: clamp(.9rem, 1.6vw, 1.25rem);
  align-self: start;
}

.review__photo {
  flex: none;
  width: clamp(3.25rem, 5vw, 4.5rem);
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  background: var(--bg-raise);
  border: 1px solid var(--rule);
}
.review__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* No photograph yet: initials, not an empty ring that reads as a broken one. */
.review__photo--mono { display: grid; place-items: center; }
.review__initials {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: .08em;
  color: var(--text-faint);
}

.review__id { display: grid; gap: .3rem; min-width: 0; }
.review__name { font-size: var(--fs-md); color: var(--text); line-height: 1.2; }
.review__role { color: var(--text-faint); }

/* --- what they said ----------------------------------------------------- */

.review__body { display: grid; gap: clamp(.9rem, 1.6vw, 1.25rem); justify-items: start; }

.review__stars { display: flex; gap: .3rem; }
.star {
  width: clamp(.85rem, 1.1vw, 1rem);
  height: auto;
  fill: var(--accent);
  /* Struck one after another as the review arrives, left to right, the way
     someone would actually award them. */
  opacity: .16;
  scale: .55;
  transform-origin: center;
  transition: opacity .5s var(--ease), scale .5s var(--ease);
  transition-delay: calc(var(--i, 0) * .09s + var(--s) * .1s + .25s);
}
.review.is-in .star { opacity: 1; scale: 1; }

.review__text {
  font-size: var(--fs-md);
  line-height: var(--leading-normal);
  color: var(--text-dim);
  max-width: 62ch;
}
.review__text p { text-wrap: pretty; }

.review__context { color: var(--text-faint); }

@media (prefers-reduced-motion: reduce) {
  .review::before { transition: none; width: 3.5rem; }
  .star { transition: none; opacity: 1; scale: 1; }
}

/* --------------------------------------------------------------------------
   7b. Services — four stages of the work, one screenful

   Four separate cards with air between them, not one ruled table. A card
   carries its mark and its name together in the middle until you reach for
   it; then the pair steps aside and the writing takes its place.

   Every mark keeps a slow loop running whether or not the card is open —
   long cycles, small distances, so the motion sits just at the edge of
   noticing.
   -------------------------------------------------------------------------- */

/* The section is a single screenful and the scroll settles onto it. The snap
   is not CSS: the browser's own is instant and grabs from half a screen away.
   settleOnSection() in motion.js waits for the scroll to stop, only acts if
   you already parked near the middle, and eases in over most of a second —
   see there for the numbers. */
/* An ordinary section, headed like every other one — a .sectionhead drawing
   its own rule. It used to be a forced screenful with a centred heading set
   in caps, the only one on the site that was either; once the writing on the
   cards was given room to be read, the screenful no longer fit anyway.

   Its spacing comes from .section now, like every other block. The class
   stays because the scroll-settle in motion.js is aimed at it by name. */

.tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 1fr;     /* one height for all four */
  gap: clamp(.75rem, 1.5vw, 1.375rem);
  perspective: 1800px;
}

/* The tile is only the hinge and the depth. Everything you can see — the
   ground, the hairline, the corner — belongs to the card, which leans when
   the hand arrives: nothing else on the page tips in space, so a card that
   does reads as an object with something behind it.

   It is also raised out of the row while the hand is on it, because it grows
   a little then and grid items after it in the source would otherwise be
   painted over its corner. */
.tile {
  position: relative;
  background: none;
  border: 0;
  padding: 0;
  width: 100%;
  cursor: pointer;
}
.tile:hover,
.tile:focus-visible { z-index: 2; }

/* Four upright cards in a row. The sound services lie wide because their
   writing is wide; these are four names and four numbers, and standing them
   up puts all four on one screen without any of them being a slab.

   Both faces share one grid cell, so the row takes the height of the taller
   of them — which is always the back, and which nobody can see until the card
   is turned. The min-height below is only a floor, so that four short fronts
   do not collapse into four labels. */
.tiles--upright { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.tiles--upright .tile__flip { min-height: clamp(20rem, 27vw, 25.5rem); }
.tiles--upright .tile__card {
  min-height: 0;
  align-content: center;
  gap: clamp(.7rem, 1.4vw, 1.05rem);
}
.tiles--upright .tile__head { flex-direction: column; gap: clamp(.9rem, 1.8vw, 1.35rem); }
.tiles--upright .tile__mark { width: clamp(2.9rem, 4.3vw, 4rem); }
.tiles--upright .tile__title { font-size: var(--fs-md); text-align: center; }
.tiles--upright .tile__rate { flex-direction: column; align-items: center; gap: .35em; }

@media (max-width: 1100px) {
  .tiles--upright { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .tiles--upright { grid-template-columns: 1fr; }
  .tiles--upright .tile__flip { min-height: clamp(17rem, 62vw, 21rem); }
}

.tile__card {
  position: relative;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: .9rem;
  height: 100%;
  min-height: clamp(11rem, 25vh, 16rem);
  padding: clamp(1.1rem, 2vw, 1.6rem);
  padding-bottom: clamp(2.4rem, 3.4vw, 3rem);   /* room for the corner label */
  background: var(--bg-raise);
  border: 1px solid var(--rule);
  border-radius: 14px;
  transition: transform .73s var(--ease-soft),
              border-color .49s var(--ease-soft),
              box-shadow .59s var(--ease-soft),
              background .49s var(--ease-soft);
}

/* Picked up: the hairline warms, the ground lifts a step, and a brass bloom
   comes up around the card. The bloom is doing what a shadow would do on a
   white page — saying which one of the four is in hand — but a shadow on a
   black page is nothing at all, so the card is lit instead of shaded. Two
   stops: a close one that reads as an edge, and a wide low one that is
   barely a colour and mostly a sense that the black around the card is not
   as black as the black around the others. */
.tile:hover  .tile__card,
.tile:focus-visible .tile__card {
  border-color: rgba(194, 161, 92, .28);
  background: var(--bg-elev);
  box-shadow: 0 4px 20px -6px rgba(194, 161, 92, .26),
              0 10px 64px -12px rgba(194, 161, 92, .2);
}
/* Only the cards that do not turn lean by themselves; where there is a hinge,
   the hinge does the leaning and the faces ride on it. */
.tile:not(.tile--flip):hover .tile__card,
.tile:not(.tile--flip):focus-visible .tile__card { transform: rotateY(-7deg) scale(1.03); }

.tile__head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(.6rem, 1.1vw, .95rem);
}

.tile__title {
  font-size: var(--fs-lg);
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}

/* The number on the front. The section promises rates, so a card that has to
   be opened before it says anything about money is a card that breaks the
   promise — it sits under the name, quiet, in the label face. */
.tile__rate {
  display: flex;
  align-items: baseline;
  gap: .5em;
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
  transition: color .49s var(--ease-soft);
}
.tile__price { color: var(--text-dim); }
.tile:hover .tile__price,
.tile:focus-visible .tile__price { color: var(--text); }

/* --- the turn ------------------------------------------------------------

   A card with a paragraph behind it turns over; a card with an essay behind it
   opens a sheet. These are the first kind. Two faces in one cell, one hinged
   in front of the other, and the lean on hover is what says there is a back
   at all — nothing else on the page tips in space.
   ------------------------------------------------------------------------- */

.tile__flip {
  position: relative;
  display: grid;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform .73s var(--ease-soft);
}
.tile--flip:hover .tile__flip,
.tile--flip:focus-visible .tile__flip { transform: rotateY(-7deg) scale(1.03); }
.tile--flip.is-open .tile__flip { transform: rotateY(180deg); }
.tile--flip.is-open:hover .tile__flip,
.tile--flip.is-open:focus-visible .tile__flip { transform: rotateY(187deg) scale(1.03); }

.tile__face {
  grid-area: 1 / 1;
  backface-visibility: hidden;
}
/* Named through .tiles--upright as well, and not because the back needs to
   know it is upright: the upright rule centres a card's content, it is one
   step more specific than this one, and it was quietly winning here. Centred,
   each back sat at a different height according to how long its paragraph
   was, and the four rules under the four heads landed on four levels. The
   backs start at the top, so the heads and the rules line up across the row
   whatever the writing below them does. */
.tiles--upright .tile--flip .tile__face--back,
.tile__face--back {
  transform: rotateY(180deg);
  align-content: start;
  justify-items: start;
  gap: 0;
  text-align: left;
  /* A little more room under the writing than the front needs: the corner
     label lives in that space, and at the narrowest four-across width the
     last line of the longest paragraph was arriving within a pixel of it. */
  padding-bottom: clamp(3.1rem, 4.4vw, 3.8rem);
}

/* The name again, small, at the head of the back. Not the price: that is
   asked once, on the front, where the four can be compared side by side.
   Turned over, the card is being read, and a number at the top of a
   paragraph is a number being asked about twice. */
.tile__backhead {
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
}

.tile__rule {
  width: 100%;
  height: 1px;
  background: var(--rule);
  margin: clamp(.75rem, 1.5vw, 1.05rem) 0 clamp(.85rem, 1.7vw, 1.2rem);
}

.tile__body {
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--text-dim);
  text-wrap: pretty;
}
.tile__body p + p { margin-top: .9em; }

/* Turned over, the way back is not something to go looking for. */
.tile--flip.is-open .tile__face--back .tile__turn { color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .tile__flip { transition: none; }
  .tile--flip:hover .tile__flip,
  .tile--flip:focus-visible .tile__flip { transform: none; }
  .tile--flip.is-open:hover .tile__flip,
  .tile--flip.is-open:focus-visible .tile__flip { transform: rotateY(180deg); }
}

/* --- the flag: the one card people arrive for ---------------------------- */

/* Brass, and the only brass on an unhovered card — that alone is enough to
   pick it out of four. A pill rather than bare type, because the corner
   opposite already holds bare type and two of them would read as a pair of
   labels instead of a claim and an action. */
.tile__flag {
  position: absolute;
  top: clamp(1rem, 1.9vw, 1.5rem);
  left: clamp(1.25rem, 2.4vw, 2rem);
  padding: .42em .7em .38em;
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-glow);
  border: 1px solid rgba(194, 161, 92, .3);
  border-radius: 999px;
  overflow: hidden;
}

/* One glint crossing the pill, with long gaps between passes — the same
   gesture the blade mark makes, borrowed because the page already speaks it.
   It is brass moving over brass, so it catches the eye without ever becoming
   a thing that blinks at you: nine seconds, and six of them are still. */
.tile__flag::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(102deg,
              transparent 30%,
              rgba(237, 232, 222, .3) 50%,
              transparent 70%);
  translate: -150% 0;
  /* Linear, not eased: an eased sweep spends most of the pass already gone
     and reads as a flicker. At this speed the crossing takes a second and a
     half of the nine, and the other seven and a half are still. */
  animation: flagGlint 9s linear infinite;
}
@keyframes flagGlint {
  0%, 66% { translate: -150% 0; }
  82%     { translate: 150% 0; }
  100%    { translate: 150% 0; }
}

/* The card itself sits a shade warmer than its neighbours — under the
   threshold on its own, but it is what makes the flag look attached to the
   card rather than dropped on top of it. */
.tile--flagged .tile__card { border-color: rgba(194, 161, 92, .2); }
.tile--flagged:hover .tile__card,
.tile--flagged:focus-visible .tile__card { border-color: rgba(194, 161, 92, .42); }

/* --- the corner: what opens --------------------------------------------- */

.tile__turn {
  position: absolute;
  right: clamp(1.25rem, 2.4vw, 2rem);
  bottom: clamp(1rem, 1.9vw, 1.5rem);
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
  transition: color .49s var(--ease-soft);
}
/* A rule that draws itself in under the word as the hand arrives — the same
   gesture the section heads use. */
.tile__turn::after {
  content: '';
  position: absolute;
  left: 0;
  right: 100%;
  bottom: -.4em;
  border-bottom: 1px solid currentColor;
  transition: right .49s var(--ease-soft);
}
.tile:hover .tile__turn,
.tile:focus-visible .tile__turn { color: var(--accent); }
.tile:hover .tile__turn::after,
.tile:focus-visible .tile__turn::after { right: 0; }

/* --- the marks ---------------------------------------------------------- */

.tile__mark {
  position: relative;
  flex: none;
  display: block;
  width: clamp(1.75rem, 2.6vw, 2.35rem);
  color: var(--text-dim);
  transition: color .49s var(--ease-soft), scale .66s var(--ease-soft);
}
/* There was a brass glow behind every mark here, lit on hover — a wide soft
   radial under the drawing, so the card read as lit from below rather than
   outlined. It is gone: four cards each with a warm cloud under the picture
   made the row look upholstered, and the card already answers the hand three
   other ways — the hairline warms, the ground lifts, and a bloom comes up
   around the whole card. The mark now answers with its own colour and its
   own size, which is what it was always doing underneath the glow.

   `position: relative` stays on the mark. Nothing sits behind it any more,
   but the marks are drawn inside a card that turns in 3D, and taking the
   positioning off changes which stacking context they land in. */
.tile__mark .mark { position: relative; }

/* The mark takes the light with the name it stands next to. It is drawn in
   one colour throughout, so this is the whole of it lifting at once — and a
   touch of size on top, which the card is doing too, so the two agree. */
.tile:hover .tile__mark,
.tile:focus-visible .tile__mark {
  color: var(--text);
  scale: 1.06;
}
.mark { display: block; width: 100%; height: auto; overflow: visible; }
.mark * { vector-effect: non-scaling-stroke; }
.mark__body,
.mark__ring,
.mark__case,
.mark__split,
.mark__knob,
.mark__screen,
.mark__throw { fill: none; stroke: currentColor; stroke-width: 1.1; }
/* Every corner in every mark is a turned corner. Without this the clarinet's
   mouthpiece — the sharpest angle drawn anywhere on the site — puts a miter
   spike out past the end of the instrument. */
.mark * { stroke-linejoin: round; }
.mark__split { stroke-width: .9; opacity: .55; }

/* record light — a proper lamp: full red at the top of the beat, nearly out
   at the bottom, and a halo that comes and goes with it.

   Three drop-shadows stacked rather than one: a tight one that reads as the
   lamp's own edge, a middle one for the body of the glow, and a wide faint
   one that is barely a colour and mostly a sense that the black around the
   lamp is warm. One shadow at a single radius has a visible rim where it
   ends, which on black looks like a drawn circle rather than light.

   The zero frame carries the same three shadows at zero radius and zero
   alpha instead of `filter: none`. A filter list only interpolates against a
   list of the same shape — against `none` the browser has nothing to
   animate towards and the whole halo appears and vanishes in one frame,
   which is the opposite of what a lamp does.

   The radii are in the mark's own units, not device pixels, so the halo
   grows with the drawing: the same lamp at 40px on a card and at 200px
   somewhere else has a glow of the same proportion, not the same width. */
.mark--rec .mark__dot {
  fill: #E2483C;
  opacity: .32;
  animation: recBlink 3.8s var(--ease) infinite paused;
}
@keyframes recBlink {
  0%, 100% { opacity: .24;
             filter: drop-shadow(0 0 0 rgba(226, 72, 60, 0))
                     drop-shadow(0 0 0 rgba(226, 72, 60, 0))
                     drop-shadow(0 0 0 rgba(226, 72, 60, 0)); }
  44%      { opacity: 1;
             filter: drop-shadow(0 0 1.6px rgba(226, 72, 60, .8))
                     drop-shadow(0 0 4.5px rgba(226, 72, 60, .5))
                     drop-shadow(0 0 10px rgba(226, 72, 60, .28)); }
}

/* the shears — two snips and a long rest, the way a hand actually cuts.

   One pair of keyframes for both arms. The second arm is drawn mirrored
   about y=12, which reverses its rotation for free, so the same negative
   turn closes one blade upward and the other down and they meet at the
   screw. The rest between snips is most of the cycle: a pair of scissors
   opening and shutting without pause is a machine, and these are a hand
   stopping to look at what it has cut. */
.mark--shears .mark__ring { stroke-width: 1.25; }
.mark--shears .mark__handle {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.45;
  stroke-linecap: round;
}
.mark--shears .mark__blade { fill: currentColor; }
.mark--shears .mark__pivot { fill: none; stroke: currentColor; stroke-width: 1; }
.mark--shears .mark__shear {
  transform-origin: 11.4px 12px;
  animation: snip 5.77s var(--ease) infinite paused;
  /* Parked a quarter-second short of the first cut.

     The cycle opens with the three-second rest, so a mark that starts at zero
     answers the first hand three seconds after it arrives — long enough that
     nobody waits that long, and the one person who does has already decided
     the drawing is dead. A negative delay is not a delay: it places the
     animation partway along its own timeline before anything starts it, so
     the shears sit already wound at 2.75s of 5.77 and cut a quarter-second
     after the pointer lands.

     It costs nothing after that first time. The animation is paused, not
     restarted, so it keeps whatever place it reached; the offset is where it
     begins, once. */
  animation-delay: -2.75s;
}
/* The close is quick and the release is slower — a cut is a snap and a
   let-go. The second snip does not reopen all the way before it goes again,
   because a hand cutting twice does not. */
/* Ten and a half degrees, and not a degree more: that is the angle at which
   the two rings arrive at each other. Past it they would pass through one
   another, which is the one thing a pair of scissors cannot do.

   The cut takes 2.77s and the pause between cuts is 3s flat, which is what
   the 5.77s cycle is made of. Both numbers are set on purpose rather than
   left to fall out of a percentage: a pause you can count is a pause you can
   ask for, and the percentages below are only that arithmetic written in the
   units keyframes happen to take. */
@keyframes snip {
  0%, 52%   { rotate: 0deg; }
  60.7%     { rotate: -10.5deg; }
  72.4%     { rotate: -2deg; }
  79.6%     { rotate: -10.5deg; }
  100%      { rotate: 0deg; }
}

/* the desk — faders riding, each on its own long beat */
.mark--desk .mark__cap { fill: currentColor; }
.mark--desk .mark__knob { opacity: .8; }
.mark--desk .mark__screen { opacity: .5; }
/* Linear, and a whole cycle rather than an alternating half — an eased
   alternation comes to a stop at each end, and these are never to stop. */
.mark--desk .mark__fader .mark__cap {
  animation: faderRide var(--fd, 7.2s) linear infinite paused;
  animation-delay: calc(var(--f) * -1.7s);
}
@keyframes faderRide {
  0%   { translate: 0 -2.6px; }
  50%  { translate: 0 2.6px; }
  100% { translate: 0 -2.6px; }
}

/* waveform — the envelope is drawn, the wave runs through it */
.mark--wave .mark__bar {
  fill: currentColor;
  transform-origin: 12px 12px;
  animation: waveRun var(--d, 3.6s) linear infinite paused;
  animation-delay: calc(var(--b) * -.31s);
}
@keyframes waveRun {
  0%   { scale: 1 .54; }
  11%  { scale: 1 .78; }
  22%  { scale: 1 .70; }
  33%  { scale: 1 1.06; }
  44%  { scale: 1 1.30; }
  55%  { scale: 1 1.12; }
  66%  { scale: 1 1.22; }
  77%  { scale: 1 .88; }
  88%  { scale: 1 .62; }
  100% { scale: 1 .54; }
}

/* --- the four things you can study ---------------------------------------

   Four marks and four motions, none of them on the same clock. They keep the
   rule the sound marks keep — a long cycle, a small distance — and add one
   of their own: a rest inside every cycle long enough that you are never
   watching a loop, only catching a thing that happened to move.
   ------------------------------------------------------------------------- */

.mark__window,
.mark__band,
.mark__hole,
.mark__stand,
.mark__stem,
.mark__beam,
.mark__arm,
.mark__groove,
.mark__pad { fill: none; stroke: currentColor; stroke-width: 1.1; }
.mark--reed .mark__body,
.mark--screen .mark__stand,
.mark--screen .mark__beam,
.mark--screen .mark__stem,
.mark--decks .mark__arm { stroke-linecap: round; }

/* the clarinet — the body is an outline with the card showing through it,
   which is the photograph's dark wood arriving on a black page without being
   asked for. The bands across it are the ligature, the barrel and the two
   tenons; nothing runs along it.

   The six tone holes are the only thing that moves, and what they do is one
   long note being fingered: they close in order from the mouthpiece down,
   the whole instrument stands closed for a second, and then the hand comes
   off backwards — the last hole down is the first one up.

   Down in one order and up in the other is why each hole has its own part
   written out here. One keyframe list and a delay per hole would do the
   first half beautifully and then get the second half wrong: a delay shifts
   a hole's whole timeline, its closing and its opening together, so the
   opening would arrive in the same order as the closing and there is no way
   to tell it to run the other way. Six lists, then, with the timing in them
   rather than in the delay. */
.mark--reed .mark__band { stroke-width: 1.3; }
.mark--reed .mark__window { stroke-width: 1; opacity: .8; }

.mark--reed .mark__hole {
  fill: currentColor;
  fill-opacity: .06;
  animation-duration: 9s;
  animation-timing-function: var(--ease);
  animation-iteration-count: infinite;
  animation-play-state: paused;
}
.mark--reed [data-hole="0"] { animation-name: reedHole0; }
.mark--reed [data-hole="1"] { animation-name: reedHole1; }
.mark--reed [data-hole="2"] { animation-name: reedHole2; }
.mark--reed [data-hole="3"] { animation-name: reedHole3; }
.mark--reed [data-hole="4"] { animation-name: reedHole4; }
.mark--reed [data-hole="5"] { animation-name: reedHole5; }

/* first down, last up — shut for five and a third of the nine seconds */
@keyframes reedHole0 {
  0%      { fill-opacity: .06; }
  4.444%  { fill-opacity: 1; }
  63.33%  { fill-opacity: 1; }
  67.78%  { fill-opacity: .06; }
  100%    { fill-opacity: .06; }
}
@keyframes reedHole1 {
  0%      { fill-opacity: .06; }
  4.667%  { fill-opacity: .06; }
  9.111%  { fill-opacity: 1; }
  58.67%  { fill-opacity: 1; }
  63.11%  { fill-opacity: .06; }
  100%    { fill-opacity: .06; }
}
@keyframes reedHole2 {
  0%      { fill-opacity: .06; }
  9.333%  { fill-opacity: .06; }
  13.78%  { fill-opacity: 1; }
  54%     { fill-opacity: 1; }
  58.44%  { fill-opacity: .06; }
  100%    { fill-opacity: .06; }
}
@keyframes reedHole3 {
  0%      { fill-opacity: .06; }
  14%     { fill-opacity: .06; }
  18.44%  { fill-opacity: 1; }
  49.33%  { fill-opacity: 1; }
  53.78%  { fill-opacity: .06; }
  100%    { fill-opacity: .06; }
}
@keyframes reedHole4 {
  0%      { fill-opacity: .06; }
  18.67%  { fill-opacity: .06; }
  23.11%  { fill-opacity: 1; }
  44.67%  { fill-opacity: 1; }
  49.11%  { fill-opacity: .06; }
  100%    { fill-opacity: .06; }
}
/* last down, first up — shut for one second, and the whole instrument is
   shut together for that second */
@keyframes reedHole5 {
  0%      { fill-opacity: .06; }
  23.33%  { fill-opacity: .06; }
  27.78%  { fill-opacity: 1; }
  40%     { fill-opacity: 1; }
  44.44%  { fill-opacity: .06; }
  100%    { fill-opacity: .06; }
}

/* the monitor — the take runs on the right, the two faders ride on the left
   at rates that never meet, and the note stays where it is. Something has to
   hold still or there is nothing to read the movement against. */
.mark--screen .mark__head,
.mark--screen .mark__bar,
.mark--screen .mark__cap { fill: currentColor; }
.mark--screen .mark__head { opacity: .9; }
.mark--screen .mark__beam { stroke-width: 1.6; }
.mark--screen .mark__stand { opacity: .8; }

.mark--screen .mark__bar {
  opacity: .8;
  transform-box: view-box;
  transform-origin: 0 10.4px;
  animation: takeRun 4.6s linear infinite paused;
  animation-delay: calc(var(--b) * -.33s);
}
@keyframes takeRun {
  0%   { scale: 1 .58; }
  12%  { scale: 1 .84; }
  24%  { scale: 1 .72; }
  36%  { scale: 1 1.14; }
  48%  { scale: 1 1.34; }
  60%  { scale: 1 1.06; }
  72%  { scale: 1 1.2; }
  84%  { scale: 1 .8; }
  100% { scale: 1 .58; }
}

/* Eased at both ends, not linear: a fader is ridden by a hand, and a hand
   arrives at a level and holds it before it moves again.

   Named apart from the desk's faderRide, which it collided with — two
   keyframe lists under one name are not two animations, they are the second
   one, and the desk's faders were riding sideways because of it. */
.mark--screen .mark__cap {
  animation: faderGlide var(--gd, 9.1s) ease-in-out infinite paused;
}
@keyframes faderGlide {
  0%, 100% { translate: -1.4px 0; }
  28%      { translate:   .5px 0; }
  56%      { translate:  -.7px 0; }
  80%      { translate:  1.4px 0; }
}

/* the keyboard — a tune, not a scale: C, E flat, G, E flat, F, E flat, D, G,
   F, C. The fifth note, the eighth and the ninth are held twice as long as
   the rest, which is what makes it a phrase instead of a run: everything
   after a held note is pushed back by the extra beat, so the tune is
   thirteen beats long and not ten. About a hundred and twenty to the
   minute, and then three seconds of nobody playing.

   Five of those ten notes are a key struck again. A key can only be handed
   one starting time, so a repeated note cannot be a delay — the second
   strike would be a second animation writing over the first, and the first
   would never be seen. Each key that plays carries its own part instead,
   written out strike by strike against the same nine-and-a-half-second bar.
   The keyboard is scored, and a held note is a key that stays at full for a
   third of a second before it starts letting go — the same falling-away as
   every other note, with a flat held on to the front of it, which is what
   makes it burn for exactly twice as long rather than merely fade slower.

   A struck key takes the light and gives it back. Nothing moves: the black
   keys used to dip at the front, which is what a key does but not what a key
   looks like from over the top of it — from up there a dipping key is a key
   getting shorter, and a key that gets shorter is a key that is broken. */
.mark--keys .mark__black { fill: currentColor; fill-opacity: .66; }
.mark--keys .mark__white { fill: currentColor; fill-opacity: 0; }

.mark--keys [data-note] {
  animation-duration: 9.6s;
  animation-timing-function: var(--ease);
  animation-iteration-count: infinite;
  animation-play-state: paused;
}
.mark--keys [data-note="c"]  { animation-name: pianoC; }
.mark--keys [data-note="d"]  { animation-name: pianoD; }
.mark--keys [data-note="eb"] { animation-name: pianoEb; }
.mark--keys [data-note="f"]  { animation-name: pianoF; }
.mark--keys [data-note="g"]  { animation-name: pianoG; }


/* C on one and on ten */
@keyframes pianoC {
  0%      { fill-opacity: 0; }
  1.521%  { fill-opacity: .3; }
  6.479%  { fill-opacity: 0; }
  63%     { fill-opacity: 0; }
  64.52%  { fill-opacity: .3; }
  /* The last note of the phrase is held: struck at 64.5 and let go at 74.4,
     twice as long as any note before it. A tune that ends on the same short
     note it began with does not end, it stops. */
  69.48%  { fill-opacity: .3; }
  74.44%  { fill-opacity: 0; }
  100%    { fill-opacity: 0; }
}
/* D, once, on seven */
@keyframes pianoD {
  0%      { fill-opacity: 0; }
  36.75%  { fill-opacity: 0; }
  38.27%  { fill-opacity: .3; }
  43.23%  { fill-opacity: 0; }
  100%    { fill-opacity: 0; }
}
/* E flat three times — the note the tune keeps coming back to, and the one
   black key in it, so it rests lit and is struck to full rather than up
   from nothing */
@keyframes pianoEb {
  0%      { fill-opacity: .66; }
  5.25%   { fill-opacity: .66; }
  6.771%  { fill-opacity: 1; }
  11.73%  { fill-opacity: .66; }
  15.75%  { fill-opacity: .66; }
  17.27%  { fill-opacity: 1; }
  22.23%  { fill-opacity: .66; }
  31.5%   { fill-opacity: .66; }
  33.02%  { fill-opacity: 1; }
  37.98%  { fill-opacity: .66; }
  100%    { fill-opacity: .66; }
}
/* F on five and on nine, both held */
@keyframes pianoF {
  0%      { fill-opacity: 0; }
  21%     { fill-opacity: 0; }
  22.52%  { fill-opacity: .3; }
  25.96%  { fill-opacity: .3; }
  30.92%  { fill-opacity: 0; }
  52.5%   { fill-opacity: 0; }
  54.02%  { fill-opacity: .3; }
  57.46%  { fill-opacity: .3; }
  62.42%  { fill-opacity: 0; }
  100%    { fill-opacity: 0; }
}
/* G on three, and on eight, held */
@keyframes pianoG {
  0%      { fill-opacity: 0; }
  10.5%   { fill-opacity: 0; }
  12.02%  { fill-opacity: .3; }
  16.98%  { fill-opacity: 0; }
  42%     { fill-opacity: 0; }
  43.52%  { fill-opacity: .3; }
  46.96%  { fill-opacity: .3; }
  51.92%  { fill-opacity: 0; }
  100%    { fill-opacity: 0; }
}

/* the decks — the platters turn at rates that are close but not equal, so
   the two markers drift apart and meet again on a cycle far too long to sit
   and count. Linear, because a platter has no ends to arrive at. */
.mark--decks .mark__groove { opacity: .28; stroke-width: .9; }
.mark--decks .mark__hub,
.mark--decks .mark__stud { fill: currentColor; }
.mark--decks .mark__hub { opacity: .7; }
.mark--decks .mark__led { fill: currentColor; opacity: .36; }

.mark--decks .mark__spin {
  transform-box: view-box;
  transform-origin: var(--ox) var(--oy);
  animation: deckSpin var(--sd, 10s) linear infinite paused;
}
@keyframes deckSpin { to { rotate: 360deg; } }

/* the crossfader, pushed across and held there — the hand is the slow part */
.mark--decks .mark__cap {
  fill: currentColor;
  animation: crossFade 13.5s var(--ease) infinite paused;
}
@keyframes crossFade {
  0%, 9%    { translate: -1.55px 0; }
  47%, 56%  { translate:  1.55px 0; }
  94%, 100% { translate: -1.55px 0; }
}

.mark--decks .mark__pad {
  fill: currentColor;
  fill-opacity: 0;
  animation: padHit 6.8s var(--ease) infinite paused;
  animation-delay: calc(var(--p) * 1.05s);
}
@keyframes padHit {
  0%, 2%  { fill-opacity: 0; }
  7%      { fill-opacity: .78; }
  19%     { fill-opacity: 0; }
  100%    { fill-opacity: 0; }
}

/* --- what moves, and when ------------------------------------------------

   Nothing, until a hand arrives. Four cards each running a loop of its own,
   all the time, side by side, is four things asking to be looked at and
   nothing being looked at; and a clarinet that fingers a note every nine
   seconds whether or not anybody is there is a clock, not an instrument.
   So the marks hold still, and the one card under the pointer plays.

   Every animation in this section is declared where the thing it moves is
   described, and declared `paused`. This is the whole of the mechanism: the
   hand does not start an animation, it un-pauses one. That matters, because
   a paused animation keeps its place. Take the hand away and the clarinet
   stops with four holes down and stays there; come back to it and the fifth
   goes down next. The alternative — handing out the animation on hover and
   taking it away again — restarts the phrase from the top every single time,
   and a phrase that begins again whenever you look at it is not a phrase.

   The selector is deliberately over-specific. The rules that declare these
   animations run to three classes (`.mark--desk .mark__fader .mark__cap`),
   and a shorthand carries its own play state, so anything less would lose to
   them and the desk's faders would sit still while everything else ran.

   The rule sits inside a no-preference query, so a reader who has asked for
   less motion is never handed the un-pausing at all: the animations are
   declared for them too, held at their first frame, which is the resting
   picture every one of them was drawn to be. */
@media (prefers-reduced-motion: no-preference) {
  .tiles .tile:is(:hover, :focus-visible) .mark * { animation-play-state: running; }

  /* And inside the opened sheet, unconditionally. The pause above exists
     because four loops running side by side is four things asking to be
     looked at; in a sheet there is only one card, it is the only thing on
     the screen, and it is being read. So the mark next to the title plays
     for as long as the sheet is up — the record light blinks while you read
     about recording — and stops when it closes, keeping its place for the
     next time that card is opened. */
  .sheet.is-open .sheet__mark .mark * { animation-play-state: running; }
}

/* A phone turns the same card with a tap; only the column count changes.

   Everything here is aimed away from the upright cards. They carry their own
   ladder — four across, two under 1100, one under 560 — and it was being
   quietly overruled: this block is further down the file than that ladder is,
   the two rules weigh the same, and the later one wins a tie. So from 561 to
   900 pixels the four upright cards stood in a single column, each one seven
   hundred pixels wide around a name, a number and a forty-pixel drawing. The
   height went the same way: an upright card is two faces sharing a cell and
   needs the pair to fill it, and `height: auto` was letting the front of it
   shrink to its own writing. */
@media (max-width: 900px) {
  .tiles:not(.tiles--upright) { grid-template-columns: 1fr; grid-auto-rows: auto; }
  .tiles:not(.tiles--upright) .tile__card {
    height: auto;
    min-height: clamp(9rem, 40vw, 12rem);
  }
  .tiles:not(.tiles--upright) .tile__title { font-size: var(--fs-md); }
  .tile__flag { top: 1rem; left: 1rem; }
}

/* Without motion the card stays flat and stays its own size — the hairline,
   the ground and the bloom answer the hand, and nothing else does. The marks
   need no undoing here: their animations are only ever handed out inside a
   no-preference query, so under this one they were never given. */
@media (prefers-reduced-motion: reduce) {
  .tile__card,
  .tile__turn,
  .tile__turn::after,
  .tile__mark { transition: none; }
  .tile:not(.tile--flip):hover .tile__card,
  .tile:not(.tile--flip):focus-visible .tile__card { transform: none; }
  .tile:hover .tile__mark,
  .tile:focus-visible .tile__mark { scale: 1; }
  .tile__flag::after { animation: none; opacity: 0; }
  .mark__dot { opacity: .8; }
}

/* --------------------------------------------------------------------------
   8b. Video stage — one player, and an index of what else there is
   -------------------------------------------------------------------------- */

.stage {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 3vw, 3.25rem);
  align-items: start;
}
.stage__main { min-width: 0; }

.stage__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--bg-raise);
  border: 1px solid var(--rule-soft);
  cursor: pointer;
}
/* The four thumbnails come from four different halls — a gold title card, a
   red curtain, two dark stages. One shared treatment pulls them into the same
   register as the page, so the strip reads as one thing rather than four. */
.stage__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: brightness(.62) saturate(.72) contrast(1.05);
  transition: filter 1.1s var(--ease), scale 1.6s var(--ease);
}
.stage__frame:hover img,
.stage__frame:focus-visible img { filter: brightness(.85) saturate(.88) contrast(1.02); scale: 1.015; }

/* A foot of shade so the frame settles into its caption instead of ending. */
.stage__frame::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 42%;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(transparent, rgba(0, 0, 0, .6));
}
.stage__frame .video__play { z-index: 2; }

.stage__frame.is-playing { cursor: default; }
.stage__frame.is-playing::after { display: none; }
.stage__frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  z-index: 2;
}

.stage__cap {
  margin-top: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  font-size: var(--fs-sm);
}
.stage__cap .mono { color: var(--text-dim); flex: none; }

/* The index: the same rule-and-label idea the rest of the site runs on. */
.stage__index { border-top: 1px solid var(--rule); }
.stage__row {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  border-bottom: 1px solid var(--rule);
  padding: clamp(.8rem, 1.5vw, 1.15rem) 0;
  transition: padding-left .7s var(--ease);
}
.stage__row:hover,
.stage__row:focus-visible,
.stage__row[aria-current="true"] { padding-left: 1.15rem; }

.stage__row::before {
  content: '';
  position: absolute;
  left: 0;
  top: calc(clamp(.8rem, 1.5vw, 1.15rem) + .55em);
  width: 0;
  height: 1px;
  background: var(--accent);
  transition: width .7s var(--ease);
}
.stage__row[aria-current="true"]::before { width: .65rem; }

.stage__rowtitle {
  display: block;
  font-size: var(--fs-sm);
  line-height: 1.3;
  letter-spacing: var(--tracking-tight);
  color: var(--text-dim);
  transition: color var(--dur) var(--ease);
}
.stage__rowmeta {
  display: block;
  margin-top: .3rem;
  font-size: var(--fs-mono);
  color: var(--text-faint);
}
.stage__row:hover .stage__rowtitle,
.stage__row:focus-visible .stage__rowtitle { color: var(--text); }
.stage__row[aria-current="true"] .stage__rowtitle { color: var(--accent); }

@media (max-width: 900px) {
  .stage { grid-template-columns: 1fr; gap: clamp(1.25rem, 4vw, 2rem); }
}

/* --------------------------------------------------------------------------
   9. Streaming embeds
   -------------------------------------------------------------------------- */

.embeds {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
  gap: clamp(1rem, 2vw, 1.75rem);
}
.embed__frame {
  border: 1px solid var(--rule-soft);
  background: var(--bg-raise);
  border-radius: 12px;
  overflow: hidden;
  min-height: 352px;
}
.embed iframe { width: 100%; border: 0; display: block; }

/* What he did on the record, set the same way as the credit under a film. */
.embed__role { margin-top: .8rem; padding-left: .3rem; }

/* --------------------------------------------------------------------------
   10. Sessions — a band of rooms that drifts, and that you can push yourself

   The band is a scroll container, not a transform: it has to answer a finger
   and a wheel as well as the clock, and a scroll position is the one thing
   all three can move. The strip is laid three times — the one you see and a
   spare either side — so crossing the repeat in either direction can be
   undone without anything appearing to happen.
   -------------------------------------------------------------------------- */

.marquee {
  --gap: clamp(.75rem, 1.5vw, 1.25rem);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-behavior: auto;          /* the drift writes scrollLeft every frame */
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
  margin-inline: calc(var(--gutter) * -1);
  /* The band clips at its own edges — `overflow-y: hidden` is what keeps a
     tall picture from pushing the page about — so a picture that grows on
     hover needs somewhere to grow into. At 1.035 the tallest sheet gains
     about six pixels top and bottom; eight leaves a margin. */
  padding-block: 8px;
  /* Both ends dissolve rather than cut, so the band reads as passing through
     the page instead of starting and stopping at its edges. */
  mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.marquee::-webkit-scrollbar { display: none; }

.marquee__track { display: flex; gap: var(--gap); width: max-content; }
.marquee__row   { display: flex; gap: var(--gap); }

/* --- one room ----------------------------------------------------------- */

.marquee__item {
  /* The ceiling is in rem, not px, so the pictures grow with everything else.
     The root scales from 16px up to 26px between 1800 and 3618 of viewport —
     that is how every type size and every rule on this site gets bigger on a
     big screen. A band capped at a flat 460px did not: on a 32-inch display
     the writing beside it had grown by three fifths and the photographs had
     not moved, so the band read as a row of thumbnails under full-size type.
     28.75rem is the same 460px at the default root. */
  flex: 0 0 clamp(255px, 30vw, 28.75rem);
  position: relative;
  display: block;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  transition: scale .5s var(--ease);
}
.marquee__item img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--bg-raise);
  transition: filter .5s var(--ease);
}

/* Printed sheets rather than rooms: portrait, landscape, a photograph of a
   noticeboard — they have nothing in common but a height. So they keep their
   own widths and the row measures itself from them, where the session band
   crops every picture to one frame. */
/* Same again for the sheets, which are sized by height rather than width:
   20.625rem is the same 330px at the default root, and it grows from there. */
.marquee--sheets { --sheet-h: clamp(190px, 33vh, 20.625rem); }
.marquee--sheets .marquee__item { flex: none; }
/* The ratio comes from the data, printed on each picture as `--ar`, and not
   from the file. `aspect-ratio: auto` means "whatever the image turns out to
   be", and a lazy image that has not arrived yet is nothing at all — so with
   `width: auto` beside it every unloaded sheet measured two pixels across
   and the band was a row of hairlines with three posters in it. Worse, each
   one that landed widened the strip under the drift, and the run lurched
   every time.

   With the ratio declared up front the width is known before a byte of the
   picture is fetched: the row is the right length from the first frame and
   nothing moves as the images fill in. */
.marquee--sheets .marquee__item img {
  width: auto;
  height: var(--sheet-h);
  max-width: none;
  aspect-ratio: var(--ar, auto);
}

/* Reached for: the picture grows a hair and comes up out of the dark.

   It used to grow inside a frame that stayed put — the picture scaled while
   the border held its size and `overflow: hidden` cropped the difference, so
   reaching for a photograph took a slice off every edge of it. A picture
   that loses its corners when you look at it is not being offered, it is
   being trimmed.

   Now the whole thing grows, frame and all, and there is no frame: the
   hairline was there to hold an edge that the crop needed and nothing else
   asked for. Small numbers — the band is scenery, and scenery that leaps is
   no longer scenery. */
.marquee__item:hover,
.marquee__item:focus-visible {
  scale: 1.035;
  z-index: 2;                     /* over its neighbours as it grows past them */
}
.marquee__item:hover img,
.marquee__item:focus-visible img { filter: brightness(1.12); }

@media (prefers-reduced-motion: reduce) {
  .marquee__item,
  .marquee__item img { transition: none; }
  .marquee__item:hover { scale: 1; }
  /* Nothing drifts, so the spares are only the same rooms said twice more. */
  .marquee__row + .marquee__row { display: none; }
}

/* The round arrow, shared by the poster wall and the lightbox. It outlived
   the sessions rail it was drawn for. */
.rail__nav { display: flex; align-items: center; align-self: center; gap: .6rem; }
.rail__btn {
  width: 42px; aspect-ratio: 1;
  border: 1px solid var(--rule);
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--text-dim);
  font-size: var(--fs-sm);
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease),
              background var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.rail__btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); }
.rail__btn:disabled { opacity: .28; cursor: default; }

/* --------------------------------------------------------------------------
   11. Poster wall — a hung row that drifts, and opens when asked
   -------------------------------------------------------------------------- */

/* Everything hangs at one height and keeps its own width, so tall recital
   posters and wide stage photographs share a line without any of them being
   cropped to a common rectangle. The row simply moves; nothing is turned,
   scaled or dimmed to manufacture a centre. */
/* --- opened ------------------------------------------------------------- */

/* Not a full-screen viewer. The sheet is lifted to about twice the height it
   has in the row and the page is dimmed behind it — enough to put it in front
   and hold the eye, and no more. Filling the screen turns a poster into a
   wallpaper and loses the thing entirely. */
/* --------------------------------------------------------------------------
   The rate sheet — one service on its own ground.

   Same sheet the lightbox uses: black, blurred behind, a panel that settles
   rather than snaps. Two columns, because the writing and the number are two
   different kinds of reading and neither should interrupt the other; below
   900px they stack, writing first.
   -------------------------------------------------------------------------- */

.sheet {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: grid;
  place-items: center;
  padding: clamp(1rem, 4vw, 3.5rem);
  background: rgba(0, 0, 0, .86);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  opacity: 0;
  transition: opacity .45s var(--ease);
}
.sheet[hidden] { display: none; }
.sheet.is-open { opacity: 1; }

/* The dialog holds the focus while it is up, so that Escape and the arrow
   keys have somewhere to be and a reader is not left standing on the page
   behind. It is a container and not a control, so it draws no ring — the
   global :focus-visible rule is for things you press. */
.sheet__panel:focus,
.sheet__panel:focus-visible { outline: none; }

.sheet__panel {
  position: relative;
  width: min(64rem, 100%);
  max-height: min(86vh, 52rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: clamp(1.75rem, 3.4vw, 3.25rem);
  padding-top: clamp(2.5rem, 4vw, 3.25rem);
  background: var(--bg-raise);
  border: 1px solid var(--rule);
  border-radius: 16px;
  translate: 0 10px;
  scale: .985;
  transition: translate .6s var(--ease), scale .6s var(--ease);
}
.sheet.is-open .sheet__panel { translate: 0 0; scale: 1; }

.sheet__close {
  position: absolute;
  top: clamp(.5rem, 1.4vw, 1rem);
  right: clamp(.5rem, 1.4vw, 1rem);
  width: 42px;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  color: var(--text-faint);
  font-size: 1.4rem;
  line-height: 1;
  transition: color var(--dur) var(--ease);
}
.sheet__close:hover { color: var(--accent); }

/* --- turning the sheet ---------------------------------------------------

   Four stages of one job, read in order. The arrows stand in gutters of their
   own beside the panel rather than on top of it: an arrow laid over the first
   line of a paragraph is a thing to read around, and this panel already
   scrolls. The gutters cost nothing when there is nothing to turn to — the
   buttons are display:none and an `auto` column with nothing in it is zero
   wide.
   ------------------------------------------------------------------------- */

.sheet { grid-template-columns: auto minmax(0, 1fr) auto; gap: clamp(.35rem, 1.4vw, 1.4rem); }

.sheet__nav {
  display: none;
  align-items: center;
  gap: .7rem;
  padding: clamp(.5rem, 1vw, .8rem);
  color: var(--text-faint);
  transition: color .49s var(--ease-soft), translate .49s var(--ease-soft);
}
.sheet--nav .sheet__nav { display: flex; }
.sheet__nav:hover,
.sheet__nav:focus-visible { color: var(--accent); }
/* Each leans the way it goes. A hairline chevron has no weight to it, and a
   colour change alone on a black page is barely an answer to the hand. */
.sheet__nav--prev:hover, .sheet__nav--prev:focus-visible { translate: -3px 0; }
.sheet__nav--next:hover, .sheet__nav--next:focus-visible { translate:  3px 0; }

.sheet__chev {
  flex: none;
  display: block;
  width: clamp(1.5rem, 2.2vw, 2rem);
  overflow: visible;
}

/* The writing goes out, is replaced while nothing is on screen, and comes
   back. Swapping a title, three paragraphs and a rate in one frame reads as
   a fault; a beat of black reads as a page turning. Short on the way out and
   longer on the way in, so the new card arrives rather than snaps. */
.sheet__panel.is-turning .sheet__head,
.sheet__panel.is-turning .sheet__grid,
.sheet__panel.is-turning .sheet__foot { opacity: 0; }
.sheet__head,
.sheet__grid,
.sheet__foot { transition: opacity .3s var(--ease-soft); }
.sheet__panel.is-turning .sheet__head,
.sheet__panel.is-turning .sheet__grid,
.sheet__panel.is-turning .sheet__foot { transition-duration: .14s; }

/* Narrow: the gutters are gone, so the two arrows go under the panel and
   split the width between them. The panel gives up some height to make the
   room — a control below the fold is a control nobody finds. */
@media (max-width: 900px) {
  .sheet { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8rem; }
  .sheet__panel { grid-column: 1 / -1; max-height: min(78vh, 46rem); }
  .sheet--nav .sheet__nav { justify-content: center; }
  .sheet__nav--prev { grid-column: 1; }
  .sheet__nav--next { grid-column: 2; }
}

@media (prefers-reduced-motion: reduce) {
  .sheet__nav { transition: none; }
  .sheet__nav--prev:hover, .sheet__nav--prev:focus-visible,
  .sheet__nav--next:hover, .sheet__nav--next:focus-visible { translate: none; }
  .sheet__head, .sheet__grid, .sheet__foot { transition: none; }
}

.sheet__head {
  display: flex;
  align-items: center;
  gap: clamp(.7rem, 1.2vw, 1.05rem);
  padding-bottom: clamp(1.1rem, 2vw, 1.6rem);
  margin-bottom: clamp(1.4rem, 2.4vw, 2.1rem);
  border-bottom: 1px solid var(--rule-soft);
}
.sheet__mark {
  flex: none;
  display: block;
  width: clamp(1.75rem, 2.6vw, 2.35rem);
  color: var(--text-dim);
}
.sheet__mark[hidden] { display: none; }
.sheet__title {
  font-size: var(--fs-lg);
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}

/* A sheet with nothing to price is one column and a narrower panel. */
.sheet__panel--plain { width: min(46rem, 100%); }
.sheet__panel--plain .sheet__grid { grid-template-columns: minmax(0, 1fr); }

.sheet__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(13rem, 24%, 18rem);
  gap: clamp(1.5rem, 3.4vw, 3.25rem);
  align-items: start;
}

.sheet__body {
  max-width: 62ch;
  font-size: var(--fs-sm);
  line-height: 1.62;
  color: var(--text-dim);
}
.sheet__body p + p { margin-top: 1em; }

/* The number, set against the writing rather than under it — a panel of its
   own, one hairline in from the text column. */
.sheet__rate {
  padding-left: clamp(1.25rem, 2.6vw, 2.25rem);
  border-left: 1px solid var(--rule-soft);
}
.sheet__rate[hidden] { display: none; }

.sheet__ratehead {
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: .9rem;
}
.sheet__price {
  font-size: var(--fs-lg);
  font-weight: 500;
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}
.sheet__unit {
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
  margin-top: .55rem;
}
.sheet__rule {
  height: 1px;
  background: var(--rule-soft);
  margin: clamp(1.1rem, 2vw, 1.5rem) 0;
}

.sheet__includes {
  list-style: none;
  display: grid;
  gap: .55rem;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--text-dim);
}
.sheet__includes li {
  position: relative;
  padding-left: 1.6em;
}
.sheet__includes li::before {
  content: '\2014';
  position: absolute;
  left: 0;
  color: var(--accent-dim);
}

.sheet__foot {
  margin-top: clamp(1.6rem, 3vw, 2.5rem);
  padding-top: clamp(1.1rem, 2vw, 1.6rem);
  border-top: 1px solid var(--rule-soft);
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--text-faint);
}
.sheet__cta {
  color: var(--accent);
  white-space: nowrap;
  border-bottom: 1px solid var(--accent-dim);
  padding-bottom: 1px;
  transition: border-color var(--dur) var(--ease);
}
.sheet__cta:hover { border-color: var(--accent); }

@media (max-width: 900px) {
  .sheet__grid { grid-template-columns: 1fr; }
  .sheet__rate {
    padding-left: 0;
    padding-top: clamp(1.25rem, 4vw, 1.75rem);
    border-left: 0;
    border-top: 1px solid var(--rule-soft);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sheet, .sheet__panel { transition: none; }
}

.lb {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: clamp(1.25rem, 4vw, 3.5rem);
  background: rgba(0, 0, 0, .86);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  opacity: 0;
  transition: opacity .45s var(--ease);
}
.lb[hidden] { display: none; }
.lb.is-open { opacity: 1; }

.lb__img {
  /* Sized against the viewport, never against the grid area: a percentage
     max-height inside an auto-sized row resolves to nothing, which is how
     this came to fill the screen and spill off the bottom. */
  max-height: min(62vh, 620px);
  max-width: min(64vw, 720px);
  width: auto;
  height: auto;
  scale: .96;
  transition: scale .6s var(--ease);
}
.lb.is-open .lb__img { scale: 1; }

.lb__close {
  position: absolute;
  top: clamp(1rem, 2.5vw, 2rem);
  right: clamp(1rem, 2.5vw, 2rem);
  width: 42px;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  color: var(--text-dim);
  font-size: 1.4rem;
  line-height: 1;
  transition: color var(--dur) var(--ease);
}
.lb__close:hover { color: var(--accent); }

.lb__nav {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
}
.lb__nav--prev { left: clamp(.5rem, 2vw, 1.75rem); }
.lb__nav--next { right: clamp(.5rem, 2vw, 1.75rem); }

@media (max-width: 700px) {
  .lb__img { max-height: 58vh; max-width: 84vw; }
}

@media (prefers-reduced-motion: reduce) {
  .lb, .lb__img { transition: none; }
}

/* --------------------------------------------------------------------------
   12. Figures
   -------------------------------------------------------------------------- */

.figure { overflow: hidden; background: var(--bg-raise); }
.figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Heads live at the top of these frames, so any crop is taken off the
     bottom — never off the forehead. */
  object-position: 50% 14%;
  scale: 1.04;
  will-change: translate;
}
.figure--tall  { aspect-ratio: 2 / 3; }
.figure--photo { aspect-ratio: 3 / 4; }
.figure--wide  { aspect-ratio: 3 / 2; }
.figure--exact img { scale: 1; object-position: 50% 0; }
.figure__cap { margin-top: .85rem; }

/* --------------------------------------------------------------------------
   12b. Type list — kept for the Sound page's "What I do" block when it returns
   -------------------------------------------------------------------------- */

.typelist { border-top: 1px solid var(--rule); }
.typelist__row {
  display: flex;
  align-items: baseline;
  gap: clamp(1rem, 3vw, 2.5rem);
  padding-block: clamp(.85rem, 1.8vw, 1.5rem);
  border-bottom: 1px solid var(--rule);
  transition: padding-left .7s var(--ease);
}
.typelist__row:hover { padding-left: 1.25rem; }
.typelist__row:hover .typelist__label { color: var(--accent); }
.typelist__label {
  font-size: var(--fs-lg);
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  transition: color var(--dur) var(--ease);
}

/* --------------------------------------------------------------------------
   13. Definition list (contact)
   -------------------------------------------------------------------------- */

.deflist { border-top: 1px solid var(--rule); }
.deflist__row {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: .35rem clamp(1.5rem, 3vw, 3rem);
  padding-block: clamp(.9rem, 1.6vw, 1.25rem);
  border-bottom: 1px solid var(--rule);
}
.deflist__desc { font-size: var(--fs-md); color: var(--text); }
.deflist__desc a { transition: color var(--dur-fast) var(--ease-out); }
.deflist__desc a:hover { color: var(--accent); }
/* Tighter rows, for the screen that has to hold everything at once. */
.deflist--tight { align-self: stretch; }
.deflist--tight .deflist__row {
  grid-template-columns: minmax(0, 9rem) minmax(0, 1fr);
  padding-block: clamp(.5rem, 1.3vh, .9rem);
}
/* The narrow rule above is written on one class and this variant on two, so
   it would otherwise outweigh it and keep two columns on a phone. */
@media (max-width: 640px) {
  .deflist--tight .deflist__row { grid-template-columns: 1fr; }
}
@media (max-width: 640px) { .deflist__row { grid-template-columns: 1fr; } }

/* --------------------------------------------------------------------------
   14. Audio player — appears only once audio samples are added
   -------------------------------------------------------------------------- */

.player { border-top: 1px solid var(--rule); }
.track {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 2rem);
  padding-block: clamp(1rem, 2vw, 1.5rem);
  border-bottom: 1px solid var(--rule);
}
.track.is-playing .track__title { color: var(--accent); }
.track__btn {
  width: clamp(40px, 4vw, 48px);
  aspect-ratio: 1;
  border: 1px solid var(--rule);
  border-radius: 50%;
  display: grid;
  place-items: center;
  flex: none;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.track__btn:hover { border-color: var(--accent); background: var(--accent-glow); }
.track.is-playing .track__btn { border-color: var(--accent); background: var(--accent); }
.track__btn::after { content: ''; border-left: 8px solid var(--text); border-block: 5.5px solid transparent; margin-left: 2px; }
.track.is-playing .track__btn::after {
  border: 0; width: 9px; height: 10px; margin-left: 0; background: #000000;
  -webkit-mask: linear-gradient(#000 0 0) left / 3px 100% no-repeat, linear-gradient(#000 0 0) right / 3px 100% no-repeat;
          mask: linear-gradient(#000 0 0) left / 3px 100% no-repeat, linear-gradient(#000 0 0) right / 3px 100% no-repeat;
}
.track__title { font-size: var(--fs-md); font-weight: 500; letter-spacing: -.015em; transition: color var(--dur) var(--ease); }
.track__wave { display: flex; align-items: center; gap: 2px; height: 34px; cursor: pointer; margin-top: .55rem; }
.track__wave i { flex: 1; min-width: 2px; background: var(--rule-strong); height: var(--h, 40%); transition: background .25s linear; pointer-events: none; }
.track__wave i.on { background: var(--accent); }
.track__time { font-variant-numeric: tabular-nums; flex: none; }
@media (max-width: 640px) {
  .track { grid-template-columns: auto minmax(0, 1fr); }
  .track__time { grid-column: 2; margin-top: -.25rem; }
}

/* --------------------------------------------------------------------------
   15. Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: .75rem;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  padding: 1rem 1.6rem;
  border: 1px solid var(--rule);
  position: relative;
  overflow: hidden;
  color: var(--text);
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.btn span { position: relative; z-index: 1; }
.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: translateY(101%);
  transition: transform .55s var(--ease);
}
.btn:hover { color: #000000; border-color: var(--accent); }
.btn:hover::before { transform: translateY(0); }
.btn--solid { background: var(--text); color: #000000; border-color: var(--text); }
.btn__arrow { transition: transform var(--dur) var(--ease); }
.btn:hover .btn__arrow { transform: translateX(4px); }

/* --------------------------------------------------------------------------
   16. Form
   -------------------------------------------------------------------------- */

/* The form is one object, lit along its top edge, the same way the
   philosophy block is: a shadow under a panel on a black page is black on
   black and says nothing, so the thing is lit from above instead.

   The ground is a wash rather than `--bg-raise`, and it comes out at exactly
   the same colour: .038 of the cream over #030303 is #0C0C0C, which is what
   `--bg-raise` is. The difference is that a wash is see-through, so whatever
   is drifting behind the panel keeps drifting. On the contact screen that is
   the notes, and it is the whole reason they cross the screen instead of
   stopping at the edge of the form. The fields inside stay solid — you
   should not be able to see weather through the box you are typing in. */
.form {
  padding: clamp(1.4rem, 2.6vw, 2.4rem);
  background:
    linear-gradient(180deg,
      rgba(237, 232, 222, .03),
      rgba(237, 232, 222, .008) 34%,
      transparent 78%),
    rgba(237, 232, 222, .038);
  border: 1px solid var(--rule-soft);
  border-radius: 16px;
  transition: border-color .49s var(--ease-soft),
              background .49s var(--ease-soft),
              box-shadow .59s var(--ease-soft);
}

/* Picked up, and held while it is being worked in. The service cards answer
   a hand the same way — the hairline warms, the ground lifts a step, and a
   brass bloom comes up around the whole thing, which is what a shadow would
   be doing on a white page: saying which thing is in hand. On black a shadow
   is nothing at all, so the panel is lit instead of shaded.

   The bloom is wider and weaker than a card's. A glow tuned for something
   the size of a postcard, put around something the size of half the screen,
   stops being an edge and becomes a haze.

   Two differences from the cards, both deliberate. It does not scale: a card
   is a target and may grow towards the hand, but a form is a surface being
   worked on, and a text field that shifts by two pixels as the pointer
   arrives is a text field you have to aim at twice. And it answers
   `:focus-within` as well as `:hover`, so it stays lit for as long as
   somebody is typing in it rather than going out the moment the pointer
   leaves for the keyboard. */
.form:hover,
.form:focus-within {
  border-color: rgba(194, 161, 92, .26);
  background:
    linear-gradient(180deg,
      rgba(237, 232, 222, .034),
      rgba(237, 232, 222, .01) 34%,
      transparent 78%),
    rgba(237, 232, 222, .058);
  box-shadow: 0 6px 30px -10px rgba(194, 161, 92, .2),
              0 16px 90px -18px rgba(194, 161, 92, .16);
}

@media (prefers-reduced-motion: reduce) { .form { transition: none; } }
.form__lead { margin-bottom: clamp(1.4rem, 2.6vw, 2rem); }

/* Two short answers on one line, and one column the moment there is not
   room for two. */
.form__row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 clamp(1rem, 2vw, 1.5rem);
}
@media (max-width: 560px) { .form__row { grid-template-columns: 1fr; } }

.field { position: relative; margin-bottom: clamp(1.1rem, 2vw, 1.5rem); }
.field label {
  display: block;
  margin-bottom: .55rem;
  color: var(--text-faint);
  transition: color .4s var(--ease-soft);
}
.field:focus-within label { color: var(--accent); }

/* A well cut into the panel: the page is #030303, the panel a step above it,
   and the boxes back down at the page's own black. Underlined fields were
   the trouble — a rule under a line of text is what writing already on a
   page looks like, and a form is asking for the opposite: somewhere the
   writing is going to go. A box that is darker than what surrounds it is a
   hole, and a hole is unmistakably a place to put something. */
.field input, .field textarea, .field select {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: .82rem 1rem;
  font-size: var(--fs-md);
  line-height: var(--leading-snug);
  color: var(--text);
  transition: border-color .4s var(--ease-soft),
              background .4s var(--ease-soft),
              box-shadow .4s var(--ease-soft);
}
.field input:hover, .field textarea:hover, .field select:hover {
  border-color: rgba(237, 232, 222, .22);
}
.field textarea { resize: vertical; min-height: 9rem; display: block; }

.field__ctl { position: relative; display: block; }
.field select { appearance: none; cursor: pointer; padding-right: 2.6rem; }
.field select option { background: var(--bg-elev); color: var(--text); }
/* Centred on the box, because it hangs off the box and not off the field. */
.field__ctl::after {
  content: '';
  position: absolute;
  right: 1.05rem;
  top: 50%;
  width: 7px; height: 7px;
  border-right: 1px solid var(--text-dim);
  border-bottom: 1px solid var(--text-dim);
  translate: 0 -70%;
  rotate: 45deg;
  pointer-events: none;
}

/* Held: the hairline warms and a brass light sits just outside it. Not the
   browser's ring — that lands on a square outside a rounded box — and not a
   thicker border either, which would move every word in the field by a pixel
   as it grew. */
.field input:focus, .field textarea:focus, .field select:focus {
  outline: 0;
  border-color: rgba(194, 161, 92, .55);
  background: #050505;
  box-shadow: 0 0 0 3px rgba(194, 161, 92, .09);
}
.field input::placeholder, .field textarea::placeholder { color: var(--text-faint); }

.field--error input, .field--error textarea, .field--error select {
  border-color: rgba(184, 92, 79, .65);
  box-shadow: 0 0 0 3px rgba(184, 92, 79, .1);
}
.field--error label { color: #B85C4F; }
.field__error { font-size: var(--fs-sm); color: #B85C4F; margin-top: .5rem; display: none; }
.field--error .field__error { display: block; }

.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* The button and what the form has to say for itself, on one line, under a
   rule that closes the panel. The status used to sit below the button with
   nothing around it, so a form that had just been sent looked like a form
   with a stray sentence under it. */
.form__foot {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.4vw, 1.75rem);
  flex-wrap: wrap;
  margin-top: clamp(1.3rem, 2.4vw, 1.9rem);
  padding-top: clamp(1.3rem, 2.4vw, 1.9rem);
  border-top: 1px solid var(--rule-soft);
}
.form__status { font-size: var(--fs-sm); color: var(--text-dim); min-height: 1.5em; }
.form__status.is-ok   { color: var(--accent); }
.form__status.is-fail { color: #B85C4F; }

/* --------------------------------------------------------------------------
   17. Footer
   -------------------------------------------------------------------------- */

.footer { border-top: 1px solid var(--rule); padding-block: clamp(1.5rem, 2.4vw, 2rem); }
.footer__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem 2rem;
  flex-wrap: wrap;
}
.footer__social { display: flex; gap: clamp(1rem, 2vw, 1.75rem); flex-wrap: wrap; }
.footer__social a { color: var(--text-dim); transition: color var(--dur-fast) var(--ease-out); }
.footer__social a:hover { color: var(--accent); }

/* --------------------------------------------------------------------------
   18. Page transition veil
   -------------------------------------------------------------------------- */

.pageveil {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  background: var(--bg);
  pointer-events: none;
  opacity: 0;
  transition: opacity .38s var(--ease-out);
}
.pageveil.is-on { opacity: 1; }

/* --------------------------------------------------------------------------
   19. The name on the first screen — letters lift and warm under the cursor
   -------------------------------------------------------------------------- */

.hero__name { cursor: default; }
.hero__name .split__char {
  transition: transform .6s var(--ease), color .5s var(--ease);
  transition-delay: calc(var(--i, 0) * .018s);
}
.hero__name:hover .split__char { transform: translate3d(0, -.055em, 0); color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .hero__name:hover .split__char { transform: none; } }


/* --------------------------------------------------------------------------
   20. Bleed — a photograph that runs off the edge of the screen and dissolves
   on its inner side. These are shot on black and the page is black, so only
   the lit edge needs feathering; the outer edge simply meets the screen.
   -------------------------------------------------------------------------- */

.bleed { position: relative; }

.bleed__media {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
/* The two bleeds crop in opposite directions, so they need opposite rules.
   The landscape photograph is wider than its frame, so `cover` trims the sides
   and shows the full height — it needs only a sliver of slack for the drift.
   The standing portrait is narrower than its frame, so `cover` trims top and
   bottom, and every trimmed pixel has to come off the feet, never the head. */
.bleed__media img {
  position: absolute;
  left: 0;
  width: 100%;
  will-change: translate;
  object-fit: cover;
}

/* Subject begins 10% down this frame, so 6% of slack above it is free — and it
   all sits above the head, where the drift needs it. */
.bleed--right .bleed__media img {
  top: -6%;
  height: 106%;
  object-position: 50% 50%;
}

/* Subject begins 6.7% down and ends at 85%, so the crop comes off the feet and
   a quarter of the spare goes above the head as drift slack. */
.bleed--left .bleed__media img {
  top: 0;
  height: 100%;
  object-position: 50% 15%;
}

/* A section built around a standing portrait has to be tall enough to hold
   one, and centres its text against the picture. */
.bleed--portrait {
  min-height: 80svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
@media (max-width: 900px) {
  .bleed--portrait { min-height: 0; display: block; }
}

.bleed--right .bleed__media {
  right: 0;
  width: min(58vw, 940px);
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, rgba(0,0,0,.1) 6%, rgba(0,0,0,.34) 13%,
      rgba(0,0,0,.62) 21%, rgba(0,0,0,.85) 28%, rgba(0,0,0,.97) 34%, #000 40%),
    linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.85) 1%, #000 3%,
      #000 82%, rgba(0,0,0,.6) 94%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-image:
    linear-gradient(to right, transparent 0%, rgba(0,0,0,.1) 6%, rgba(0,0,0,.34) 13%,
      rgba(0,0,0,.62) 21%, rgba(0,0,0,.85) 28%, rgba(0,0,0,.97) 34%, #000 40%),
    linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.85) 1%, #000 3%,
      #000 82%, rgba(0,0,0,.6) 94%, transparent 100%);
          mask-composite: intersect;
}

.bleed--left .bleed__media {
  left: 0;
  /* narrower than the landscape bleed: a 2:3 portrait in a wide frame loses
     its head and its feet to the crop */
  width: min(40vw, 600px);
  -webkit-mask-image:
    linear-gradient(to left, transparent 0%, rgba(0,0,0,.08) 7%, rgba(0,0,0,.28) 16%,
      rgba(0,0,0,.55) 26%, rgba(0,0,0,.82) 36%, rgba(0,0,0,.96) 45%, #000 54%),
    linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.85) 1%, #000 3%,
      #000 82%, rgba(0,0,0,.6) 94%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-image:
    linear-gradient(to left, transparent 0%, rgba(0,0,0,.08) 7%, rgba(0,0,0,.28) 16%,
      rgba(0,0,0,.55) 26%, rgba(0,0,0,.82) 36%, rgba(0,0,0,.96) 45%, #000 54%),
    linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.85) 1%, #000 3%,
      #000 82%, rgba(0,0,0,.6) 94%, transparent 100%);
          mask-composite: intersect;
}

.bleed__body {
  position: relative;
  z-index: 1;
}

/* text keeps clear of the picture on each kind of bleed */
.bleed--right .bleed__body .statement,
.bleed--right .bleed__body .spine__text { grid-column: 3 / 8; }
.bleed--left  .bleed__body .spine__text { grid-column: 7 / 13; }

@media (max-width: 900px) {
  .bleed__media {
    position: relative;
    top: auto;
    bottom: auto;
    width: 100% !important;
    height: 58svh;
    margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 62%, rgba(0,0,0,.5) 84%, transparent 100%) !important;
    -webkit-mask-composite: source-over !important;
            mask-image: linear-gradient(to bottom, #000 0%, #000 62%, rgba(0,0,0,.5) 84%, transparent 100%) !important;
            mask-composite: add !important;
  }
  .bleed--right .bleed__body .statement,
  .bleed--right .bleed__body .spine__text,
  .bleed--left  .bleed__body .spine__text { grid-column: 1 / -1; }
}

/* A long sentence set in two narrow columns instead of one wide slab. */
.prose--two {
  columns: 2;
  column-gap: clamp(2rem, 4vw, 4rem);
}
@media (max-width: 780px) { .prose--two { columns: 1; } }


/* A deeper entrance than the default reveal: the block arrives from further
   down and a beat later, so text and photograph do not move as one plane. */
.js [data-reveal="lift"] {
  transform: translate3d(0, 52px, 0);
  transition-duration: 1.15s;
}
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal="lift"] { transform: none; }
}


/* --------------------------------------------------------------------------
   22. Letters that lift and warm under the cursor — the landing page's move,
   reused wherever a title is set large enough to carry it.
   -------------------------------------------------------------------------- */

.charhover .split__char {
  transition: transform .6s var(--ease), color .5s var(--ease);
  transition-delay: calc(var(--i, 0) * .018s);
}
.charhover:hover .split__char {
  transform: translate3d(0, -.055em, 0);
  color: var(--accent);
}
@media (prefers-reduced-motion: reduce) {
  .charhover:hover .split__char { transform: none; }
}


/* --------------------------------------------------------------------------
   23. Chapter bands — one full-width screen per chapter, the photograph
   behind the words and drifting at its own rate. Measured: the left half of
   the first two photographs sits at 3–5/255, so the reading column lands on
   something already black; the third is a bright object shot and is dimmed to
   join the series.
   -------------------------------------------------------------------------- */

/* The three bands are stacked, not queued: each pins to the top of the screen
   and the next one rides up over it. The one underneath recedes rather than
   scrolling away, which is what makes the hand-over worth scrolling for. */
.bandstack { position: relative; }

.chapterband {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
  /* opaque, so an arriving band genuinely covers the one it replaces */
  background: var(--bg);
}

.chapterband__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  /* only the horizontal edges need feathering — the sides meet the screen */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.55) 5%,
      #000 15%, #000 85%, rgba(0,0,0,.5) 95%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,.55) 5%,
      #000 15%, #000 85%, rgba(0,0,0,.5) 95%, transparent 100%);
}
/* Anchored to the top and never transformed: what the frame shows at rest is
   what it shows throughout. Every trimmed pixel comes off the bottom. */
.chapterband__media img {
  position: absolute;
  /* Some frames are shot with the head hard against the top edge, so there is
     no headroom to crop toward — it has to be added. Dropping the picture
     inside the band leaves the band's own black above it, which is invisible
     against a photograph that is black up there anyway. */
  top: var(--drop, 0);
  /* These photographs are 3:2 and the band is wider than that, so `cover`
     crops them vertically and leaves no horizontal spare at all: the picture
     is exactly as wide as the band. Sliding it therefore bares a strip at one
     edge — which is fine here, because the direction of the slide and the
     direction of the reading gradient always agree. The figure moves away
     from the words, so the strip opens on the side the gradient has already
     taken to solid black. That keeps the picture at its true size; widening
     it to cover the strip is what made these frames look zoomed. */
  left: var(--shiftx, 0px);
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0%;
  will-change: opacity;
}

/* The bright object shot is brought down to meet the other two. */
.chapterband--light .chapterband__media img { filter: brightness(.42) contrast(1.08); }

/* The reading side. Long ramp, so it never draws a line of its own. */
.chapterband::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(96deg, #000 4%, rgba(0,0,0,.9) 24%, rgba(0,0,0,.62) 42%,
      rgba(0,0,0,.28) 58%, rgba(0,0,0,.06) 72%, transparent 84%);
}

/* Mirrored band: picture on the left, words on the right. */
.chapterband--right::before {
  background: linear-gradient(264deg, #000 4%, rgba(0,0,0,.9) 24%, rgba(0,0,0,.62) 42%,
      rgba(0,0,0,.28) 58%, rgba(0,0,0,.06) 72%, transparent 84%);
}
.chapterband--right .spine__text { grid-column: 7 / 13; }

.chapterband__body { position: relative; z-index: 2; width: 100%; }


.chapterband__title {
  font-size: var(--fs-xl);
  font-weight: 500;
  letter-spacing: -.03em;
  text-transform: uppercase;
  margin-bottom: clamp(1.25rem, 2.5vw, 2rem);
}

.chapterband .prose { max-width: 46ch; }

@media (max-width: 900px) {
  .chapterband { min-height: 0; padding-block: clamp(3.5rem, 12vh, 6rem); }
  .chapterband::before {
    background: linear-gradient(180deg, rgba(0,0,0,.55) 0%, rgba(0,0,0,.8) 45%, #000 85%);
  }
}


/* The words of a band that is being covered drift up and out of the way. */
.chapterband__body {
  will-change: opacity, translate;
}

@media (max-width: 900px) {
  .chapterband { position: relative; height: auto; }
  /* The words move below the picture here, so there is nothing to clear —
     and the reading gradient turns vertical, so it would no longer be
     holding the bared edge. The frame goes back to square. */
  .chapterband__media img { left: 0; }
  .chapterband--right .spine__text { grid-column: 1 / -1; }
  .chapterband--right::before {
    background: linear-gradient(180deg, rgba(0,0,0,.55) 0%, rgba(0,0,0,.8) 45%, #000 85%);
  }
}
