/* ============================================================
   Photon Creatives — 2026 redesign
   Ported from the Claude Design canvas "Photon Creatives.dc.html".
   Paper + ink, Playfair Display display over Lato text.
   ============================================================ */

:root {
  --paper: #ffffff; /* page ground */
  --ink: #1b1917; /* text + dark sections */
  --bright: #f7f5f1; /* text over dark imagery */
  --stone: #e7e1d8; /* image placeholder ground */
  --sand: #ffffff; /* ground for banded sections */

  /* Ink at the opacities the page uses, named by that opacity.

     The canvas set these much lighter — the small tracked labels ran at 0.45
     and the body prose at 0.66, which over the white ground is 2.9:1 and
     5.6:1. On a screen that is not a design tool the labels simply went grey.
     Every value here is chosen against its measured contrast on white:
     text carrying meaning clears 4.5:1, body prose clears 7:1, and only the
     placeholder — which repeats what its label already says — sits below.

        alpha   contrast on #fff
        0.35     2.2   hairlines and underlines only
        0.45     2.9   placeholders
        0.62     4.9   numerals, section meta
        0.66     5.6   eyebrows and field labels
        0.70     6.5
        0.72     6.9
        0.80     9.3   body prose
        0.85    11.1 */
  --ink-85: rgba(27, 25, 23, 0.85);
  --ink-80: rgba(27, 25, 23, 0.8);
  --ink-72: rgba(27, 25, 23, 0.72);
  --ink-70: rgba(27, 25, 23, 0.7);
  --ink-66: rgba(27, 25, 23, 0.66);
  --ink-62: rgba(27, 25, 23, 0.62);
  --ink-45: rgba(27, 25, 23, 0.45);
  --ink-35: rgba(27, 25, 23, 0.35);
  --rule: rgba(27, 25, 23, 0.16);
  --rule-2: rgba(27, 25, 23, 0.14);
  --rule-3: rgba(27, 25, 23, 0.1);

  --serif:
    "Playfair Display", "Playfair Fallback", Georgia, "Times New Roman", serif;
  --sans:
    Lato, "Lato Fallback", Helvetica, -apple-system, BlinkMacSystemFont,
    sans-serif;
  --mono:
    Lato, "Lato Fallback", Helvetica, -apple-system, BlinkMacSystemFont,
    sans-serif;

  --pad: 40px;
  --maxw: 1480px;

  /* The canvas's easing — a long, settled decelerate. */
  --ease: cubic-bezier(0.16, 0.8, 0.24, 1);
  /* Softer sibling, for type that should settle rather than snap. */
  --ease-soft: cubic-bezier(0.22, 0.61, 0.36, 1);

  /* Scroll-motion tuning, all in one place. The two offsets are positions in
     the element's pass through the viewport: it starts arriving at --reveal-from
     and has landed by --reveal-to. The steps are how much further along each
     sibling — or each word — begins. */
  --reveal-from: 5%;
  --reveal-to: 40%;
  --reveal-step: 3.5%;
  --word-step: 0.4%;

  /* Diagonal hatch standing in for photography. */
  --hatch-dark: repeating-linear-gradient(
    115deg,
    rgba(244, 241, 236, 0.07) 0px,
    rgba(244, 241, 236, 0.07) 1px,
    rgba(0, 0, 0, 0) 1px,
    rgba(0, 0, 0, 0) 12px
  );
  --hatch-light: repeating-linear-gradient(
    115deg,
    rgba(27, 25, 23, 0.07) 0px,
    rgba(27, 25, 23, 0.07) 1px,
    rgba(0, 0, 0, 0) 1px,
    rgba(0, 0, 0, 0) 12px
  );
}

@media (max-width: 720px) {
  :root {
    --pad: 22px;
  }
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

/* The canvas parks anchored sections 10px below the viewport top, sliding
   them under the (blend-mode, transparent) masthead. */
[id] {
  scroll-margin-top: 10px;
}

html,
body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  /* 400, not the canvas's 300: at 10.5px with 0.26em tracking a Lato Light
     stroke is thin enough to read as grey however dark the colour is. The
     display faces set their own weight, so this only moves the sans. */
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

body {
  min-height: 100vh;
  /* Scoped to body (which propagates to the viewport) rather than html+body,
     matching where the canvas puts it — on its wrapper, not the root. */
  overflow-x: hidden;

  /* `min-height: 100vh` alone stretches the body on a page whose content is
     shorter than the window, but it does not stretch anything inside it — so
     the footer stays where the content ends and the leftover height paints in
     the body background, a white band under the dark footer. Every 2026 page
     is one <main> followed by the footer, so growing that <main> hands the
     spare height to the content and leaves the footer on the bottom edge.
     Pages long enough to scroll never reach this — which is why only /studio
     showed it, and only on a window taller than about 1500px. */
  display: flex;
  flex-direction: column;
}

body > main {
  flex: 1 0 auto;
}

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

::selection {
  background: var(--ink);
  /* Not --paper: that is the page ground, now pure white. Over the dark band
     the type wants the warm off-white the rest of the footer is set in. */
  color: var(--bright);
}

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

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

/* Visible only to screen readers. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

@keyframes phoRise {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes phoFade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes phoWipe {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}
@keyframes phoScaleIn {
  from {
    transform: scale(1.14);
  }
  to {
    transform: none;
  }
}

/* ---------- Shared primitives ---------- */

.shell {
  max-width: var(--maxw);
  margin: 0 auto;
}

/* The index runs on a tighter measure than the project pages. It is a page of
   type with photographs set into it, where the line length is what has to be
   right; a project page is a gallery, and there the frames want the room. Set
   on <body> so the shared footer follows the page it is on. */
.page--index {
  --maxw: 1150px;
  --hero-maxw: clamp(1150px, 65vw, 1680px);
}

/* Small uppercase tracked label — the canvas's connective tissue.
   Also worn by section <h2>s, hence the heading resets. */
.eyebrow {
  margin: 0;
  font-size: 13px;
  font-weight: inherit;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ink-66);
}

/* Section head: label left, meta right, hairline under. */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 20px;
}

.section-head__meta {
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-62);
}

/* Image placeholder. Carries a caption until a photograph replaces it. */
.frame {
  position: relative;
  width: 100%;
  overflow: hidden;
  background-image: var(--hatch-light);
  background-color: var(--stone);
}

.frame > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.frame__label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
  text-align: center;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink-62);
}

/* Full-bleed dark hero ground, used by both the index and project heroes. */
.plate {
  position: relative;
  width: 100%;
  overflow: hidden;
}

.plate__ground {
  position: absolute;
  inset: 0;
  background-image:
    var(--hatch-dark), linear-gradient(160deg, #3a352e, #1d1a17);
}

.plate__label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  text-align: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: rgba(244, 241, 236, 0.4);
}

.plate__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Legibility wash so display type holds over any frame. */
.plate__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(20, 18, 16, 0.4) 0%,
    rgba(20, 18, 16, 0.05) 40%,
    rgba(20, 18, 16, 0.78) 100%
  );
}

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

/* difference blending flips the mark from white over the dark hero
   to near-black once the paper sections scroll under it. */
.masthead {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 60;
  padding: 24px var(--pad);
}

/* At the top of the page the bar is nothing but its two marks — the design
   wants the masthead sitting on the picture, not on a band of chrome. Once the
   page has moved, a glass ground fades in behind them so the marks stop
   colliding with whatever line of type is passing underneath.

   It is a pseudo-element rather than a background on the bar itself because
   `backdrop-filter` blurs what is behind the element it sits on: put it on
   .masthead and it would blur the marks' own backdrop along with the page. */
.masthead::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(255, 255, 255, 0);
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  border-bottom: 1px solid transparent;
  transition:
    background 0.4s ease,
    backdrop-filter 0.4s ease,
    -webkit-backdrop-filter 0.4s ease,
    border-color 0.4s ease;
}

[data-scrolled] .masthead::before {
  background: rgba(255, 255, 255, 0.72);
  backdrop-filter: blur(10px) saturate(120%);
  -webkit-backdrop-filter: blur(10px) saturate(120%);
  border-bottom-color: var(--rule-3);
}

/* Without the blur the tint is doing the legibility work on its own, so it has
   to carry more of it. */
@supports not (
  (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))
) {
  [data-scrolled] .masthead::before {
    background: rgba(255, 255, 255, 0.94);
  }
}

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  width: 100%;
  max-width: calc(var(--hero-maxw, var(--maxw)) + 80px);
  margin: 0 auto;
}

/* On a white page the marks are simply ink. The difference blend is kept for
   the one case that needs it — a page whose hero is a full-bleed dark
   photograph — where it flips them to white over the picture and back to
   near-black as the paper sections scroll under. Against white there is
   nothing to flip, and blending would only wash them out.

   Where it does run, the blend sits on the two marks rather than on the bar:
   a blended element is re-composited against its backdrop on every frame of
   the scroll, and the region that costs is the element's own box. On the bar
   that is a full-width band; on the marks it is two short runs of text. The
   bar's own pixels are transparent and the marks never overlap, so the result
   is identical. */
.masthead__brand,
.masthead__nav {
  color: var(--ink);
}

.page--dark-hero .masthead__brand,
.page--dark-hero .masthead__nav {
  color: #ffffff;
  mix-blend-mode: difference;
}

/* The blend exists to flip the marks against whatever is behind them. Once the
   glass ground is in, what is behind them is the glass — so the flip is both
   unnecessary and wrong (it would resolve against the tint rather than the
   page) and the marks go back to being plain ink. */
[data-scrolled] .page--dark-hero .masthead__brand,
[data-scrolled] .page--dark-hero .masthead__nav {
  color: var(--ink);
  mix-blend-mode: normal;
}

.masthead__brand {
  font-family: var(--serif);
  font-size: 21px;
  letter-spacing: 0.2em;
  white-space: nowrap;
}

.masthead__brand-fill {
  display: inline-block;
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  transform: translateX(-0.35em);
  vertical-align: bottom;
  transition:
    max-width 0.8s var(--ease),
    opacity 0.35s ease,
    transform 0.8s var(--ease);
}

.masthead__brand:hover .masthead__brand-fill,
.masthead__brand:focus-visible .masthead__brand-fill {
  max-width: 5em;
  opacity: 1;
  transform: translateX(0);
  transition:
    max-width 1.05s var(--ease-soft),
    opacity 0.45s ease,
    transform 1.05s var(--ease-soft);
}

.masthead__nav {
  display: flex;
  gap: 32px;
  font-size: 13px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
}

.masthead__nav a {
  opacity: 0.85;
  transition: opacity 0.4s ease;
}

.masthead__nav a:hover,
.masthead__nav a:focus-visible {
  opacity: 1;
}

@media (max-width: 560px) {
  .masthead {
    padding: 18px var(--pad);
  }
  .masthead__brand {
    font-size: 18px;
  }
  .masthead__nav {
    gap: 18px;
    font-size: 9.5px;
    letter-spacing: 0.18em;
  }
}

/* ---------- Hero ---------- */

/* Editorial, not full-bleed: the whole composition shares one centered measure
   so the type and the photograph belong to the same object. The measure opens
   up on large displays without changing the laptop composition. The photograph
   is positioned rather than laid out as a column, because the lede has to sit
   *over* it — that overlap is the composition, and a grid track cannot give it. */
.hero {
  position: relative;
  overflow: hidden;
  /* The page padding lives here and the measure inside it, exactly as the
     sections below do it, so every left edge on the page lines up. */
  padding: 0 var(--pad);
}

.hero__inner {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  max-width: var(--hero-maxw, var(--maxw));
  min-height: 100vh;
  min-height: 100svh; /* dynamic mobile toolbars */
  margin: 0 auto;
}

.hero__plate {
  position: absolute;
  top: 50%;
  right: 0;
  width: 62%;
  height: 70vh;
  min-height: 420px;
  max-height: 920px;
  overflow: hidden;
  transform: translateY(-50%);
  background: var(--stone);
  /* Opens from the left, so it arrives behind the type rather than under it. */
  animation: phoWipe 1.5s var(--ease) both;
}

.hero__plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Settles as the wipe uncovers it: the panel opens onto a moving frame. */
  animation: phoScaleIn 2s var(--ease) both;
}

/* No z-index and no animation of its own: either would make this a stacking
   context, and the headline's blend would then resolve against the lede rather
   than against the photograph behind it. The entrance sits on the parts. */
.hero__lede {
  position: relative;
  /* Wider than the photograph's left edge on purpose: the headline is meant to
     run across the join, and the blend above lets it. */
  max-width: 680px;
}

.hero__eyebrow {
  font-size: 10.5px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-70);
  margin-bottom: 26px;
  animation: phoRise 0.9s var(--ease-soft) 0.15s both;
}

/* The headline runs off the paper and onto the photograph, so it is set in
   white and differenced — the same device the masthead uses. Over the paper
   ground that resolves to very nearly the ink colour; over the photograph it
   inverts, so the line holds whatever is behind it without a wash over the
   picture. The blend needs this element to be free of its own opacity or
   transform, which is why the entrance is on the words inside it. */
.hero__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(30px, 3.4vw, 50px);
  line-height: 1.08;
  letter-spacing: -0.02em;
  margin: 0 0 28px;
  text-wrap: balance;
  color: #ffffff;
  mix-blend-mode: difference;
}

.hero__title em {
  font-style: italic;
}

.hero__body {
  font-size: 14.5px;
  line-height: 1.85;
  color: var(--ink-80);
  margin: 0;
  max-width: 38ch;
  text-wrap: pretty;
  animation: phoRise 0.9s var(--ease-soft) 0.5s both;
}

.hero__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 30px;
  margin-top: 38px;
  animation: phoRise 0.9s var(--ease-soft) 0.66s both;
}

.hero__cta {
  font-size: 10.5px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--ink-62);
  padding-bottom: 5px;
  transition: border-color 0.4s ease;
}

.hero__cta:hover,
.hero__cta:focus-visible {
  border-color: var(--ink);
}

.hero__booking {
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-62);
}

/* Below this the overlap has nowhere left to go — holding it would squeeze the
   lede narrower than its own headline. The composition unstacks instead: type
   first, then the photograph at full width. */
@media (max-width: 1000px) {
  .hero__inner {
    flex-direction: column;
    align-items: stretch;
    min-height: 0;
    padding-top: 150px;
  }
  /* The photograph is written first so the headline can blend over it. Once
     they are stacked there is nothing to blend against and the type has to
     lead, so the reading order is restored here. */
  .hero__lede {
    order: -1;
    max-width: none;
  }
  .hero__title {
    color: var(--ink);
    mix-blend-mode: normal;
  }
  .hero__plate {
    position: relative;
    top: 0;
    right: 0;
    width: 100%;
    height: auto;
    min-height: 0;
    max-height: none;
    aspect-ratio: 16 / 10;
    margin-top: 52px;
    transform: none;
  }
}

@media (max-width: 560px) {
  .hero__inner {
    padding-top: 112px;
  }
  .hero__plate {
    aspect-ratio: 4 / 3;
    margin-top: 38px;
  }
}

/* ---------- Studio ---------- */

.studio {
  padding: 150px var(--pad) 140px;
  border-bottom: 1px solid var(--rule-2);
}

.studio__grid {
  display: grid;
  grid-template-columns: 210px minmax(0, 1fr);
  gap: 56px;
}

.studio__label {
  padding-top: 16px;
}

.studio__statement {
  font-family: var(--serif);
  font-size: clamp(27px, 3.2vw, 48px);
  line-height: 1.26;
  letter-spacing: -0.01em;
  margin: 0 0 44px;
  max-width: 23ch;
  text-wrap: pretty;
}

.studio__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 44px;
  max-width: 900px;
}

.studio__more {
  display: inline-block;
  margin-top: 40px;
}

.studio__cols p {
  font-size: 14.5px;
  line-height: 1.9;
  color: var(--ink-80);
  margin: 0;
}

@media (max-width: 860px) {
  .studio {
    padding: 96px var(--pad) 88px;
  }
  .studio__grid {
    grid-template-columns: 1fr;
    gap: 30px;
  }
  .studio__cols {
    grid-template-columns: 1fr;
  }
  .studio__label {
    padding-top: 0;
  }
}

/* ---------- Work index ---------- */

/* Two columns run independently, the right one dropped by 130px, so the index
   reads as a staggered spread rather than a grid of matched cards. They are
   real columns and not one grid: a grid would pull the two sides back into
   line at every row boundary, which is the alignment the offset exists to
   break. */
.work {
  padding: 120px var(--pad) 20px;
}

.work__head {
  margin-bottom: 64px;
}

.work__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 80px 60px;
}

.work__col {
  display: flex;
  flex-direction: column;
  gap: 110px;
}

.work__col--offset {
  padding-top: 130px;
}

.card {
  display: block;
  transition: opacity 0.5s ease;
}

.card:hover,
.card:focus-visible {
  opacity: 0.72;
}

.card__frame {
  aspect-ratio: 4 / 5;
}

.card__line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  padding-top: 18px;
}

.card__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 26px;
  letter-spacing: -0.005em;
  margin: 0;
}

.card__year {
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-66);
  white-space: nowrap;
}

.card__kind {
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-70);
  padding-top: 6px;
}

@media (max-width: 860px) {
  .work {
    padding: 88px var(--pad) 0;
  }
  /* One column, and the cards return to the order they are written in.
     `display: contents` drops the two column boxes — and the offset padding
     with them — so the cards become direct children of the stack and the
     `order` the page sets from each project's own index can sequence them. */
  .work__grid {
    display: flex;
    flex-direction: column;
    gap: 64px;
  }
  .work__col {
    display: contents;
  }
}

.projects-index {
  padding-bottom: 180px;
}

.projects-index__hero {
  padding: 180px var(--pad) 112px;
}

.projects-index__title {
  margin: 30px 0 54px;
  font-family: var(--serif);
  font-size: clamp(58px, 9vw, 132px);
  font-weight: 400;
  line-height: 0.86;
  letter-spacing: -0.045em;
}

.projects-index__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  color: var(--ink-66);
  font-size: 10.5px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

.projects-index__listing {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 112px 16px;
  padding: 0 var(--pad);
}

.projects-index__card {
  grid-column: span 4;
}

.projects-index__card--wide {
  grid-column: span 8;
}

.projects-index__card--wide .card__title {
  font-size: 30px;
}

@media (max-width: 760px) {
  .projects-index {
    padding-bottom: 110px;
  }
  .projects-index__hero {
    padding-top: 132px;
    padding-bottom: 72px;
  }
  .projects-index__title {
    margin: 24px 0 40px;
    font-size: clamp(48px, 16vw, 84px);
  }
  .projects-index__meta {
    font-size: 9.5px;
    letter-spacing: 0.16em;
  }
  .projects-index__listing {
    grid-template-columns: 1fr;
    gap: 68px;
  }
  .projects-index__card,
  .projects-index__card--wide {
    grid-column: auto;
  }
  .projects-index__card--wide .card__title {
    font-size: 26px;
  }
}

/* ---------- Services ---------- */

.services {
  padding: 170px var(--pad) 150px;
}

.services__head {
  margin-bottom: 44px;
}

/* The intro paragraph from the services copy, above the three blocks. */
.services__lede {
  font-size: 15px;
  line-height: 1.9;
  color: var(--ink-80);
  margin: 0 0 74px;
  max-width: 74ch;
}

.services__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 70px 60px;
}

.service__n {
  font-family: var(--serif);
  font-size: 15px;
  letter-spacing: 0.1em;
  color: var(--ink-62);
  margin-bottom: 22px;
}

.service__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 28px;
  line-height: 1.2;
  margin: 0 0 18px;
}

.service__body {
  font-size: 14px;
  line-height: 1.85;
  color: var(--ink-80);
  margin: 0;
  max-width: 40ch;
}

.service__body + .service__body {
  margin-top: 18px;
}

@media (max-width: 860px) {
  .services {
    padding: 100px var(--pad) 96px;
  }
  .services__head {
    margin-bottom: 32px;
  }
  .services__lede {
    margin-bottom: 48px;
  }
  .services__grid {
    gap: 48px;
  }
}

/* ---------- Clients / voices ---------- */

.voices {
  padding: 120px var(--pad) 160px;
  background: var(--sand);
  color: var(--ink);
}

.voices .section-head {
  border-color: var(--rule);
}

.voices .eyebrow,
.voices .section-head__meta {
  color: var(--ink-66);
}

.voices__grid {
  display: grid;
  grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
  gap: 80px;
  align-items: start;
  padding-top: 62px;
  /* The photograph is a full-width plate behind both columns, so it resolves
     against the grid rather than the quote's own stage. */
  position: relative;
}

.voices__list {
  display: flex;
  flex-direction: column;
  /* Over the plate, not under it. */
  position: relative;
  z-index: 1;
}

.voices__content {
  min-width: 0;
}

.voices__stage {
  position: relative;
  /* Holds the tallest quote so switching tabs doesn't shift the page. */
  min-height: 620px;
  padding: 64px 0 48px;
}

.voices__media {
  position: absolute;
  inset: 16px -60px -40px -60px;
  overflow: hidden;
  background: var(--stone);
}

.voices__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.06);
  transition:
    opacity 0.8s ease,
    transform 1.4s var(--ease);
}

.voices__image.is-active {
  opacity: 1;
  transform: none;
}

/* Type sits over the whole plate now, so the picture carries a dark veil and
   the type goes bright again. Slightly heavier on the left, where the client
   list and the opening of the quote sit, easing off to the right so the
   photograph keeps some of its own contrast. */
.voices__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    rgba(18, 16, 15, 0.72) 0%,
    rgba(18, 16, 15, 0.62) 38%,
    rgba(18, 16, 15, 0.5) 72%,
    rgba(18, 16, 15, 0.44) 100%
  );
  pointer-events: none;
}

.voice {
  display: block;
  width: 100%;
  padding: 18px 0;
  border: 0;
  background: none;
  color: var(--bright);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: padding-left 0.45s var(--ease);
}

.voice:hover,
.voice:focus-visible {
  padding-left: 8px;
}

/* Both are spans inside the tab button, so they need to be blocks to stack. */
.voice__name {
  display: block;
  font-family: var(--serif);
  font-size: 21px;
  line-height: 1.2;
  letter-spacing: -0.005em;
  color: rgba(247, 245, 241, 0.45);
  transition: color 0.45s ease;
}

.voice__meta {
  display: block;
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-top: 6px;
  color: rgba(247, 245, 241, 0.3);
  transition: color 0.45s ease;
}

.voice[aria-selected="true"] .voice__name {
  color: var(--bright);
}
.voice[aria-selected="true"] .voice__meta {
  color: rgba(247, 245, 241, 0.6);
}

.voices__quote {
  position: relative;
  z-index: 1;
  max-width: 34ch;
  margin: 0;
  color: var(--bright);
  font-family: var(--serif);
  font-size: clamp(20px, 1.9vw, 30px);
  line-height: 1.45;
  text-shadow: 0 1px 18px rgba(0, 0, 0, 0.35);
  text-wrap: pretty;
}

.voices__foot {
  display: flex;
  align-items: center;
  margin-top: 28px;
  position: relative;
  z-index: 1;
}

.voices__track {
  position: relative;
  flex: 1;
  height: 1px;
  background: rgba(247, 245, 241, 0.28);
}

.voices__bar {
  position: absolute;
  left: 0;
  top: 0;
  height: 1px;
  width: 0;
  background: var(--bright);
}

@media (max-width: 860px) {
  .voices {
    padding: 88px var(--pad) 100px;
  }
  .voices__grid {
    grid-template-columns: 1fr;
    gap: 48px;
    padding-top: 44px;
  }
  .voices__stage {
    min-height: clamp(410px, 62vw, 520px);
  }
  /* One column, so the plate simply covers the stack, bleeding to the edges. */
  .voices__media {
    inset: 0 calc(var(--pad) * -1);
  }
}

@media (max-width: 560px) {
  .voices__stage {
    min-height: 470px;
    padding: 52px 18px 42px;
  }
  .voices__quote {
    max-width: none;
  }
}

/* ---------- Footer ---------- */

.footer {
  padding: 130px var(--pad) 48px;
  background: var(--ink);
  color: var(--paper);
}

.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: 70px;
  align-items: end;
}

.footer__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(34px, 4.6vw, 72px);
  line-height: 1.06;
  letter-spacing: -0.02em;
  margin: 0;
  max-width: 16ch;
  text-wrap: balance;
}

.footer__cols {
  display: flex;
  flex-direction: column;
  gap: 26px;
}

.footer__label {
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: rgba(244, 241, 236, 0.8);
  margin-bottom: 10px;
}

.footer__email {
  font-size: 16px;
  color: rgba(244, 241, 236, 0.95);
  border-bottom: 1px solid rgba(244, 241, 236, 0.3);
  padding-bottom: 3px;
  transition:
    border-color 0.4s ease,
    color 0.4s ease;
}

.footer__email:hover,
.footer__email:focus-visible {
  border-color: rgba(244, 241, 236, 0.9);
  color: #ffffff;
}

.footer__based {
  font-size: 14px;
  line-height: 1.7;
  color: rgba(244, 241, 236, 0.9);
}

.footer__social {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(244, 241, 236, 0.8);
}

.footer__social a {
  transition: color 0.4s ease;
}
.footer__social a:hover,
.footer__social a:focus-visible {
  color: #ffffff;
}

.footer__bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  margin-top: 110px;
  padding-top: 22px;
  border-top: 1px solid rgba(244, 241, 236, 0.14);
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(244, 241, 236, 0.72);
}

@media (max-width: 860px) {
  .footer {
    padding: 90px var(--pad) 40px;
  }
  .footer__grid {
    grid-template-columns: 1fr;
    gap: 48px;
    align-items: start;
  }
  .footer__bottom {
    margin-top: 64px;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
}

/* ---------- Project ---------- */

.project {
  animation: phoFade 0.6s ease both;
}

.project__hero {
  height: 88vh;
  height: 88svh; /* dynamic mobile toolbars */
  min-height: 560px;
}

.project__hero-content {
  position: absolute;
  left: var(--pad);
  right: var(--pad);
  bottom: 46px;
  color: var(--bright);
}

.project__hero-eyebrow {
  font-size: 10.5px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: rgba(247, 245, 241, 0.72);
  margin-bottom: 20px;
}

.project__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(44px, 6.4vw, 104px);
  line-height: 0.98;
  letter-spacing: -0.02em;
  margin: 0;
}

.project__intro {
  padding: 110px var(--pad) 96px;
  border-bottom: 1px solid var(--rule-2);
}

.project__intro-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 80px;
}

.project__lead {
  font-family: var(--serif);
  font-size: clamp(22px, 2.4vw, 34px);
  line-height: 1.4;
  margin: 0;
  max-width: 28ch;
  text-wrap: pretty;
}

.project__meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 34px 30px;
  align-content: start;
  padding-top: 8px;
  margin: 0;
}

.project__meta dt {
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-62);
  margin-bottom: 10px;
}

.project__meta dd {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
  color: rgba(27, 25, 23, 0.92);
}

/* The note reads as a pull quote between the first and second chapter,
   indented to sit under the same column the studio statement uses. */
.project__aside {
  padding: 104px var(--pad);
}

.project__note {
  font-family: var(--serif);
  font-size: clamp(20px, 2.1vw, 29px);
  line-height: 1.55;
  color: var(--ink-85);
  margin: 0 0 0 clamp(0px, 18vw, 266px);
  max-width: 34ch;
  text-wrap: pretty;
}

/* ---------- Project chapters ---------- */

/* Ledger: table of contents and delivery list in one. Every project is told in
   the same movements, so this is also how a visitor learns the page's shape. */
.ledger {
  padding: 92px var(--pad) 0;
}

.ledger__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.ledger__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: baseline;
  padding: 22px 0;
  border-bottom: 1px solid var(--rule-3);
  transition: padding-left 0.45s var(--ease);
}

.ledger__item:hover,
.ledger__item:focus-visible {
  padding-left: 10px;
}

.ledger__title {
  font-family: var(--serif);
  font-size: clamp(23px, 2.6vw, 36px);
  line-height: 1.1;
  letter-spacing: -0.005em;
}

.project__chapters {
  position: relative;
}

.chapter-nav {
  position: fixed;
  z-index: 45;
  top: 50%;
  right: max(14px, calc((100vw - var(--maxw)) / 2 - 174px));
  width: 160px;
  padding: 14px 12px 10px;
  border-radius: 4px;
  background: linear-gradient(
    135deg,
    rgba(255, 255, 255, 0.84),
    rgba(255, 255, 255, 0.62)
  );
  box-shadow: 0 14px 40px rgba(27, 25, 23, 0.08);
  backdrop-filter: blur(16px) saturate(1.08);
  -webkit-backdrop-filter: blur(16px) saturate(1.08);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate3d(12px, -50%, 0);
  transition:
    opacity 0.35s ease,
    visibility 0s linear 0.35s,
    transform 0.5s var(--ease);
}

.chapter-nav[data-visible] {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translate3d(0, -50%, 0);
  transition-delay: 0s;
}

.chapter-nav__label {
  padding: 2px 8px 11px;
  font-size: 9px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-62);
}

.chapter-nav__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.chapter-nav__link {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
  min-height: 42px;
  padding: 7px 8px;
  border-top: 1px solid var(--rule-3);
  font-size: 10px;
  line-height: 1.35;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink-66);
  transition:
    background 0.35s ease,
    color 0.35s ease;
}

.chapter-nav__link:hover,
.chapter-nav__link:focus-visible {
  color: var(--ink);
}

.chapter-nav__link[aria-current="location"] {
  color: var(--ink);
  box-shadow: inset 2px 0 0 var(--ink);
}

.chapter {
  padding: 116px var(--pad) 0;
}

/* Title left, what the block is for in the middle —
   the same 210px label column the studio section uses. */
.chapter__head {
  display: grid;
  grid-template-columns: 210px minmax(0, 1fr);
  gap: 56px;
  align-items: start;
  border-top: 1px solid var(--rule);
  padding-top: 28px;
  margin-bottom: 58px;
}

.chapter__marker {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.chapter__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(25px, 2.8vw, 38px);
  line-height: 1.08;
  letter-spacing: -0.01em;
  margin: 0;
}

.chapter__blurb {
  font-size: 14.5px;
  line-height: 1.9;
  color: var(--ink-80);
  margin: 0;
  max-width: 62ch;
}

.chapter__rows {
  display: flex;
  flex-direction: column;
  gap: 26px;
}

.chapter__wide {
  width: min(100%, calc(84svh * var(--frame-ratio)));
  margin-inline: auto;
}

.chapter__pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 26px;
}

.chapter__pair--portrait {
  width: min(100%, calc(104svh + 26px), 1146px);
  margin-inline: auto;
}

/* Breaks a long run of paired portraits: one frame in the same centered grid,
   alternating which column it occupies. */
.chapter__offset {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  width: min(100%, calc(104svh + 26px), 1146px);
  margin-inline: auto;
  gap: 26px;
}

.chapter__offset--right > * {
  grid-column: 2;
}

/* Vertical social cuts. preload="none" on the element keeps the page cheap
   until someone actually presses play. */
.films {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 340px));
  justify-content: center;
  gap: 26px;
}

.film {
  position: relative;
  margin: 0;
  overflow: hidden;
  background: var(--stone);
}

.film video {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--ink);
}

.project__next {
  padding: 120px var(--pad) 140px;
}

.project__next-inner {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 40px;
  border-top: 1px solid var(--rule);
  padding-top: 34px;
}

.project__back {
  font-size: 10.5px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-72);
  transition: color 0.4s ease;
}

.project__back:hover,
.project__back:focus-visible {
  color: var(--ink);
}

.project__next-link {
  text-align: right;
}

.project__next-label {
  font-size: 10.5px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-66);
  margin-bottom: 12px;
}

.project__next-title {
  font-family: var(--serif);
  font-size: clamp(30px, 4vw, 58px);
  line-height: 1;
  letter-spacing: -0.015em;
}

@media (max-width: 1080px) {
  .chapter-nav {
    top: auto;
    right: var(--pad);
    bottom: calc(14px + env(safe-area-inset-bottom));
    left: var(--pad);
    width: auto;
    padding: 5px;
    transform: translate3d(0, 14px, 0);
  }
  .chapter-nav[data-visible] {
    transform: none;
  }
  .chapter-nav__label {
    display: none;
  }
  .chapter-nav__list {
    display: flex;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .chapter-nav__list::-webkit-scrollbar {
    display: none;
  }
  .chapter-nav__list li {
    flex: 0 0 auto;
  }
  .chapter-nav__link {
    display: flex;
    gap: 8px;
    min-height: 44px;
    padding: 8px 14px;
    border-top: 0;
    white-space: nowrap;
  }
  .chapter-nav__link[aria-current="location"] {
    color: var(--bright);
    background: var(--ink);
    box-shadow: none;
  }
}

@media (max-width: 860px) {
  .project__intro {
    padding: 72px var(--pad) 64px;
  }
  .project__intro-grid {
    grid-template-columns: 1fr;
    gap: 44px;
  }
  .project__aside {
    padding: 64px var(--pad);
  }
  .project__note {
    margin-left: 0;
  }
  .project__next {
    padding: 80px var(--pad) 96px;
  }

  .ledger {
    padding: 64px var(--pad) 0;
  }
  .ledger__item {
    grid-template-columns: minmax(0, 1fr);
    padding: 18px 0;
  }

  .chapter {
    padding: 72px var(--pad) 0;
  }
  .chapter__head {
    grid-template-columns: 1fr;
    gap: 22px;
    margin-bottom: 34px;
  }
  .chapter__pair {
    grid-template-columns: 1fr;
  }
  /* The offset only reads when there is a column to hang off. */
  .chapter__offset {
    grid-template-columns: 1fr;
  }
  .chapter__offset--right > * {
    grid-column: 1;
  }
}

/* ---------- Studio ---------- */

/* A page of type with one photograph set into it, so it runs on the index's
   tighter measure rather than the galleries' wide one. */
.about {
  --maxw: 1150px;
  padding-bottom: 170px;
}

.about__grid {
  display: grid;
  grid-template-columns: 360px minmax(0, 1fr);
  gap: 90px;
  align-items: start;
  /* First thing on the page now, so it carries the clearance the masthead
     needs rather than inheriting it from a header above. Generous, because
     with the hero gone there is nothing above it to set the page off. */
  padding: 260px var(--pad) 0;
}

/* The prose is dropped against the portrait's top edge, the same 130px-family
   offset the work index uses to break a matched row. */
.about__bio {
  padding-top: 46px;
}

.about__name {
  margin: 0 0 8px;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(30px, 3.2vw, 46px);
  line-height: 1.08;
  letter-spacing: -0.02em;
}

.about__role {
  margin: 0 0 34px;
  font-size: 10.5px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-66);
}

.about__body {
  margin: 0;
  max-width: 46ch;
  font-size: 15px;
  line-height: 1.9;
  color: var(--ink-80);
  text-wrap: pretty;
}

.about__body + .about__body {
  margin-top: 26px;
}

/* Named things inside the prose — a school, a discipline — sit in italic
   rather than in bold: the page has no bold weight anywhere else. */
.about__body em {
  font-style: italic;
  color: var(--ink);
}

@media (max-width: 900px) {
  .about {
    padding-bottom: 110px;
  }
  .about__grid {
    grid-template-columns: 1fr;
    gap: 44px;
    padding-top: 170px;
  }
  .about__portrait {
    max-width: 360px;
  }
  .about__bio {
    padding-top: 0;
  }
}

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

/* A page of type, like the index rather than the galleries — the measure is
   what has to be right here, so it borrows the index's tighter shell. */
.contact {
  --maxw: 1150px;
  padding-bottom: 170px;
}

.contact__hero {
  padding: 180px var(--pad) 76px;
}

.contact__title {
  margin: 30px 0 44px;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(58px, 9vw, 132px);
  line-height: 0.86;
  letter-spacing: -0.045em;
}

.contact__title em {
  font-style: italic;
}

.contact__lede {
  max-width: 44ch;
  margin: 0 0 56px;
  font-size: 15px;
  line-height: 1.85;
  color: var(--ink-80);
  text-wrap: pretty;
}

.contact__grid {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  gap: 90px;
  align-items: start;
  padding: 0 var(--pad);
}

/* ---- Studio details ---- */

.contact__detail {
  padding: 20px 0;
}

.contact__label {
  margin-bottom: 9px;
  font-size: 10.5px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-66);
}

.contact__value {
  font-family: var(--serif);
  font-size: clamp(18px, 1.5vw, 23px);
  line-height: 1.3;
  letter-spacing: -0.01em;
  word-break: break-word;
}

.contact__value a {
  border-bottom: 1px solid var(--ink-35);
  padding-bottom: 2px;
  transition: border-color 0.4s ease;
}

.contact__value a:hover,
.contact__value a:focus-visible {
  border-color: var(--ink);
}

.contact__note {
  margin: 26px 0 0;
  max-width: 32ch;
  font-size: 13px;
  line-height: 1.8;
  color: var(--ink-70);
}

/* ---- Form ---- */

/* Rows, not boxes, and no rules between them — the label sets each field off
   on its own, so the form reads as a column of answers rather than a ledger. */
.form__row {
  display: grid;
  grid-template-columns: 180px minmax(0, 1fr);
  gap: 32px;
  align-items: start;
  padding: 20px 0;
}

.form__row > label {
  padding-top: 11px;
  font-size: 10.5px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-66);
}

.form__row > label .req {
  color: var(--ink-85);
}

/* A hairline under each field — the label alone left the fields hard to tell
   apart. It sits at the same weight as the rules elsewhere on the page, and
   darkens on focus so the active answer is obvious. */
.form input,
.form textarea {
  width: 100%;
  padding: 3px 0 7px;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule);
  outline: 0;
  transition: border-color 0.3s var(--ease);
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(18px, 1.6vw, 25px);
  line-height: 1.4;
  letter-spacing: -0.01em;
  color: var(--ink);
  resize: none;
}

.form input:hover,
.form textarea:hover {
  border-bottom-color: var(--ink-35);
}

.form input:focus,
.form textarea:focus {
  border-bottom-color: var(--ink);
}

.form input::placeholder,
.form textarea::placeholder {
  /* A step down from the answer itself, and lighter — a prompt, not a value. */
  font-size: clamp(15px, 1.15vw, 18px);
  color: var(--ink-35);
  font-style: italic;
}

.form textarea {
  min-height: 128px;
}

/* Netlify's honeypot. Off-screen rather than display:none — a bot reads the
   DOM, not the layout, and a hidden field it skips is no trap. */
.form__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 28px;
  padding-top: 30px;
}

.form__fine {
  margin: 0;
  max-width: 40ch;
  font-size: 12px;
  line-height: 1.75;
  color: var(--ink-66);
}

.form__actions {
  display: grid;
  justify-items: end;
  gap: 12px;
  margin-left: auto;
}

.form__status {
  max-width: 42ch;
  margin: 0;
  font-size: 12px;
  line-height: 1.65;
  text-align: right;
  color: var(--ink-70);
}

.form__status[data-state="error"] {
  color: #8c3028;
}

/* The rule under the button is drawn twice: a resting hairline, and a full-ink
   line on top of it that wipes in from the left on hover. */
.form__send {
  position: relative;
  padding: 0 0 5px;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--ink-62);
  font-family: var(--sans);
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink);
  cursor: pointer;
}

.form__send::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease);
}

.form__send:hover::after,
.form__send:focus-visible::after {
  transform: scaleX(1);
}

.form__send:disabled {
  cursor: wait;
  opacity: 0.62;
}

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

/* ---- Sent ---- */

.contact__thanks {
  padding: 0 var(--pad);
}

.contact__thanks-title {
  margin: 0 0 26px;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(34px, 4.4vw, 62px);
  line-height: 1.08;
  letter-spacing: -0.02em;
  max-width: 18ch;
}

.contact__thanks-title em {
  font-style: italic;
}

@media (max-width: 900px) {
  .contact {
    padding-bottom: 110px;
  }
  .contact__hero {
    padding-top: 132px;
    padding-bottom: 56px;
  }
  .contact__title {
    margin: 24px 0 34px;
    font-size: clamp(48px, 16vw, 84px);
  }
  .contact__grid {
    grid-template-columns: 1fr;
    gap: 60px;
  }
  .form__row {
    grid-template-columns: 1fr;
    gap: 12px;
  }
  .form__row > label {
    padding-top: 0;
  }
}

/* ---------- Scroll motion ---------- */

/* Reveals are driven by the scroll position itself — `animation-timeline: view()`
   — so there is no script, no observer and nothing to hydrate. The whole system
   is gated twice: a browser without scroll-driven animations, or a reader who
   asks for reduced motion, is served the finished page with no hidden state at
   all. `--i` staggers a group; each sibling starts a little further along.

   Because the timeline *is* the scroll position, a reveal scrubs: scroll back
   past an element and it eases out again. The ranges below finish early enough
   that this only happens at the very edge of the viewport. */

@keyframes phoReveal {
  from {
    opacity: 0;
    transform: translate3d(0, 22px, 0);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes phoFrame {
  from {
    opacity: 0;
    transform: translate3d(0, 18px, 0) scale(0.985);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Travel is the frame's own height times the overscale, so the drift can never
   pull an edge into view. Deep is for the full-bleed plates, soft for gallery
   frames — both keep half their overscale as margin at each end. */
@keyframes phoDriftDeep {
  from {
    transform: translate3d(0, -6%, 0) scale(1.12);
  }
  to {
    transform: translate3d(0, 6%, 0) scale(1.12);
  }
}

@keyframes phoDriftSoft {
  from {
    transform: translate3d(0, -3%, 0) scale(1.06);
  }
  to {
    transform: translate3d(0, 3%, 0) scale(1.06);
  }
}

/* Words are inline-blocks so they can be moved at all. This one rule sits
   outside the gates: without them the word simply never animates, and an
   inline-block word is indistinguishable from an inline one here. */
.word {
  display: inline-block;
}

/* Type that plays as the page opens rather than as it scrolls. Time-based, so
   it works everywhere; the reduced-motion block below collapses it. */
[data-words-on="load"] .word {
  animation: phoReveal 0.78s var(--ease-soft) both;
  animation-delay: calc(var(--i) * 55ms);
}

/* The index hero opens as one block (phoRise on .hero__lede) with only its
   headline staggered, so this is the project hero's alone. */
.project__hero-eyebrow {
  animation: phoReveal 0.9s var(--ease-soft) both;
  animation-delay: 0.1s;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* `cover` rather than `entry`: an entry range is exactly as long as the
       element is tall, so on a short one the whole reveal would be spent in the
       bottom few pixels of the screen and never really be seen. A cover range
       is the element's full pass through the viewport, which keeps the trigger
       point steady whether the element is a caption or a full-bleed frame. */
    [data-reveal],
    [data-frame],
    [data-words-on="scroll"] .word {
      --i: 0;
      --step: var(--reveal-step);
      animation-name: phoReveal;
      animation-duration: auto;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: view();
      animation-range: cover calc(var(--reveal-from) + var(--i) * var(--step))
        cover calc(var(--reveal-to) + var(--i) * var(--step));
    }

    /* A sentence has many more units than a row of cards, so it needs a much
       finer step to finish inside the same stretch of scroll. */
    [data-words-on="scroll"] .word {
      --step: var(--word-step);
    }

    [data-frame] {
      animation-name: phoFrame;
    }

    /* Parallax runs over the frame's whole pass through the viewport rather
       than over its entrance.

       It cannot use an anonymous `view()` here: that resolves against the
       nearest ancestor scroll container, and the frame and the plate both clip
       their overflow — which makes them scroll containers that never scroll,
       leaving the timeline permanently inactive. Naming the timeline on the
       clipping element and referring to it from the image inside is the way
       past that. */
    .frame,
    .plate {
      view-timeline-name: --media;
    }

    [data-parallax] {
      animation-name: phoDriftSoft;
      animation-duration: auto;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: --media;
      animation-range: cover 0% cover 100%;
    }

    [data-parallax="deep"] {
      animation-name: phoDriftDeep;
    }

    /* Small screens pay for this motion in a way a desktop does not. Every
       drifting photograph is a compositor layer rasterised at its overscale —
       1.06x on a gallery frame, 1.12x on a plate that is already the full
       width of the screen — and every word of a scrolling paragraph is a
       separate running animation, which on a project page is well over a
       hundred of them at once. A mid-range phone spends its frame budget on
       that rather than on the scroll.

       So below the layout's own breakpoint the two costly parts come off and
       the cheap part stays: photographs sit still, paragraphs rise as one
       block rather than word by word, and the frame and section reveals —
       one animation each, on elements that are on the page anyway — carry it.
       A 3% drift is a handful of pixels on a phone; it is not what is being
       given up here. */
    @media (max-width: 860px) {
      /* Removing the animation rather than pausing it also drops the overscale
         it was holding, so each photograph returns to its natural raster size.
         The timelines go with it — by then nothing refers to them. */
      .frame,
      .plate {
        view-timeline-name: none;
      }

      [data-parallax],
      [data-parallax="deep"] {
        animation: none;
      }

      /* The stagger collapses into one reveal on the paragraph itself. `--i`
         still applies there, so a group of them keeps its sequence. */
      [data-words-on="scroll"] .word {
        animation: none;
      }

      [data-words-on="scroll"] {
        --i: 0;
        animation-name: phoReveal;
        animation-duration: auto;
        animation-timing-function: linear;
        animation-fill-mode: both;
        animation-timeline: view();
        animation-range: cover
          calc(var(--reveal-from) + var(--i) * var(--reveal-step)) cover
          calc(var(--reveal-to) + var(--i) * var(--reveal-step));
      }
    }
  }
}

/* A little life on the index cards. */
.card .frame > img {
  transition: transform 1.1s var(--ease);
}

.card:hover .frame > img,
.card:focus-visible .frame > img {
  transform: scale(1.035);
}

/* ---------- Motion preferences ---------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  /* Staggered starts would otherwise leave type invisible for the length of
     their delay, even with the durations collapsed above. */
  * {
    animation-delay: 0s !important;
  }
}
