/* ===========================================================================
   Cinestyle — one-pager stylesheet
   ---------------------------------------------------------------------------
   Order (SPEC §37):
     0 self-hosted fonts
     1 tokens · 2 reset/base · 3 typography · 4 utilities · 5 layout
     6 header/nav · 7 buttons · 8 hero · 9 clients · 10 about · 11 services
     12 process · 13 work · 14 team · 15 contact · 16 footer · 17 motion
     18 responsive · 19 reduced motion

   Two conventions carry most of the file:

   * Light and dark bands do not duplicate rules. Every section declares the
     local roles --fg / --fg-muted / --rule / --focus, and `.is-dark` reassigns
     them. Components then only ever speak in local roles.
   * Motion is opt-in. Reveal targets are fully visible by default; only
     `html.motion-ready` (set by JS, and only when it can actually animate)
     hides them before their turn. See §17 and SPEC §18.
   =========================================================================== */

/* --- 0. self-hosted fonts (SIL OFL; latin subsets; no network requests) ---
 *
 * Latin subsets cover the whole page — English copy plus ’ — · — so no unicode-range
 * declarations are needed; anything outside the subset falls through to the token's
 * fallback stack on its own.
 *
 * Both Cormorant weights are the real faces: the Medium (500) is the official latin subset
 * from fonts.gstatic.com, so h3 titles render at their intended weight.
 */

@font-face {
  font-family: "Cormorant Garamond";
  src: url("../assets/fonts/cormorant-garamond-regular.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Cormorant Garamond";
  src: url("../assets/fonts/cormorant-garamond-medium.woff2") format("woff2");
  font-style: normal;
  font-weight: 500;
  font-display: swap;
}

@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter-regular.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter-medium.woff2") format("woff2");
  font-style: normal;
  font-weight: 500;
  font-display: swap;
}

/* --- 1. tokens ---------------------------------------------------------- */

:root {
  /* Core semantic colors */
  --bg: #f1eee7;
  --surface: #faf8f3;
  --surface-alt: #e8e3da;
  --ink: #111315;
  /* --muted and --accent-dark are darkened against the SPEC's starting values so
     body-size text on --bg clears WCAG AA: 5.9:1 and 5.4:1 instead of 4.4:1/4.3:1. */
  --muted: #5c5a54;
  --accent: #bf9556;
  --accent-dark: #7a5a2e;
  --line: rgba(17, 19, 21, 0.16);

  /* Dark sections */
  --dark: #0c0e0f;
  --dark-soft: #151719;
  --on-dark: #f6f1e8;
  --on-dark-muted: rgba(246, 241, 232, 0.70);

  /* Typography */
  --font-heading: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --font-body: "Inter", Arial, Helvetica, sans-serif;

  /* Geometry */
  --radius: 0px;
  --radius-small: 2px;
  --container: 1180px;
  /* Declared because the spec's token set defines it, but nothing uses it since the header
     joined the content grid. It is the reserved width for a future full-bleed, image-led
     band — editing it in the Theme panel changes nothing today. */
  --container-wide: 1440px;
  --page-padding: clamp(1.25rem, 4vw, 3rem);
  --section-gap: clamp(5rem, 9vw, 9rem);

  /* Effects */
  --shadow: 0 18px 60px rgba(17, 19, 21, 0.10);

  /* Type scale */
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: clamp(1.05rem, 1.4vw, 1.25rem);
  --display-sm: clamp(2.5rem, 5vw, 4.6rem);
  /*
   * One scale for the whole hero headline — no breakpoint ever overrides it, so the type
   * cannot jump, re-break and re-settle while the window is dragged.
   *
   * The ceiling is the copy column: --hero-copy (§8) is itself continuous, and the longest
   * line, "No unnecessary layers.", measures 8.38 x font-size in Cormorant Garamond at this
   * tracking. Both fluid terms are held at 86-91% of column-width / 8.38 across 320-2560px,
   * which is what keeps the three <br> lines unbroken at every width. The margin is not
   * decoration: Georgia, the first fallback, sets the same line 16.7% wider, so a headline
   * pushed closer to its column would re-break and snap back during a slow font swap.
   *
   * Two terms rather than one because the column grows far faster than the viewport below
   * ~440px (it is page-padding-bound there) and far slower above it. min() picks whichever
   * is currently the constraint, and the two meet at ~440px — a crossover, not a step.
   */
  --display-lg: min(0.3rem + 8vw, 2rem + 1.82vw, 4.2rem);

  /* Spacing */
  --space-1: 0.5rem;
  --space-2: 0.75rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2rem;
  --space-6: 3rem;
  --space-7: 4.5rem;
  --space-8: 6rem;

  /* Motion */
  --motion-fast: 180ms;
  --motion-base: 420ms;
  --motion-slow: 760ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  /* One dissolve between the frames of a cycling media box. Long enough to read as a change
     of scene rather than a cut; JS holds each frame for several times this before starting
     the next. Symmetrical easing, because a dissolve has no leading edge to lead with. */
  --frame-fade: 1600ms;
  --ease-dissolve: cubic-bezier(0.4, 0, 0.2, 1);

  /* Header height, also the anchor scroll offset. */
  --header-h: 80px;
}

/* --- 2. reset / base --------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

h1,
h2,
h3,
p,
figure,
blockquote {
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--focus, var(--accent-dark));
  outline-offset: 3px;
}

/* Sections own the local color roles; components never name a palette token. */
section,
.site-footer {
  --fg: var(--ink);
  --fg-muted: var(--muted);
  --rule: var(--line);
  --focus: var(--accent-dark);
  color: var(--fg);
}

.is-dark {
  --fg: var(--on-dark);
  --fg-muted: var(--on-dark-muted);
  --rule: rgba(246, 241, 232, 0.18);
  --focus: var(--accent);
  background: var(--dark);
}

/* Sticky header must not cover the top of a jump target. */
:target,
section[id] {
  scroll-margin-top: calc(var(--header-h) + var(--space-3));
}

.skip-link {
  position: absolute;
  left: var(--space-3);
  top: var(--space-3);
  z-index: 100;
  padding: var(--space-2) var(--space-3);
  background: var(--ink);
  color: var(--surface);
  font-size: var(--text-sm);
  letter-spacing: 0.04em;
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: none;
}

/* --- 3. typography ---------------------------------------------------- */

.display,
.hero__title,
.contact__title {
  font-family: var(--font-heading);
  font-weight: 400;
  line-height: 1.03;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.display {
  font-size: var(--display-sm);
}

h3 {
  font-family: var(--font-heading);
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.01em;
}

p {
  text-wrap: pretty;
}

.eyebrow {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
}

/* On warm light bands the bronze needs to be the darker mix to stay readable. */
.eyebrow--dark {
  color: var(--accent-dark);
}

/*
 * The "01 / Perspective" sequence. Hidden for now at the owner's request — the numerals stay
 * in the markup, and the styling below stays with them, so bringing the sequence back is
 * deleting one declaration rather than rebuilding five section heads.
 */
.section-numeral {
  font-size: var(--text-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-muted);
  display: none;
}

/* Whatever followed a numeral now opens its block, so it must not keep the top margin that
   was only there to hold it clear of one. The type selector is what makes these win against
   the section rules that set those margins further down the file. */
p.section-numeral + .eyebrow,
p.section-numeral + .display {
  margin-top: 0;
}

.statement-line {
  font-family: var(--font-heading);
  font-size: clamp(1.5rem, 2.4vw, 2.1rem);
  line-height: 1.25;
  color: var(--fg);
}

.placeholder {
  color: var(--fg-muted);
  font-style: normal;
  box-shadow: inset 0 -1px 0 0 var(--rule);
}

/* --- 4. utilities ----------------------------------------------------- */

.lead,
.section-head__intro {
  font-size: var(--text-lg);
  color: var(--fg-muted);
  max-width: 48ch;
}

.media-frame {
  position: relative;
  overflow: hidden;
  background: var(--surface-alt);
  border-radius: var(--radius);
}

.media-frame > img {
  width: 100%;
  height: 100%;
  /* OnePage Studio overrides object-position for crop/focal point — nothing here
     may depend on a transform for framing (SPEC §19.4). */
  object-fit: cover;
}

/*
 * A frame that cycles between several photographs (§17 owns the timing, JS owns the turn).
 * Every frame is stacked on the same box and only opacity separates them, so nothing in the
 * layout moves as they swap. The markup ships with the first one already carrying
 * .is-current: a page without JS, or one honouring prefers-reduced-motion, shows that frame
 * and never learns the others exist.
 *
 * isolate keeps the frame indices — and any overlay the section stacks on top of them — from
 * having to be reasoned about against the rest of the page.
 */
.media-frame--frames {
  isolation: isolate;
  /* How many frames stand on screen together, and therefore how many the cycle moves at a
     time. A section raises it where its layout shows more than one at once (§13); CSS owns
     the number because CSS owns the breakpoint that changes it. */
  --frames-visible: 1;
}

.media-frame--frames > img {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--frame-fade) var(--ease-dissolve);
}

/* The frame on screen, fading in above the one it replaces. */
.media-frame--frames > img.is-current {
  opacity: 1;
  z-index: 2;
}

/* The frame being replaced. It stays fully opaque directly underneath until the dissolve is
   over, then is dropped without a transition once it is safely covered. Fading the pair
   against each other instead would let the background show through both at the halfway
   point — a dip to black, which is a different effect from a dissolve. */
.media-frame--frames > img.is-holding {
  opacity: 1;
  z-index: 1;
  transition: none;
}

.media-frame--landscape {
  aspect-ratio: 16 / 10;
}

.media-frame--portrait {
  aspect-ratio: 4 / 5;
  /* Single-column widths would turn 4:5 into a scroll of its own; the cap crops through
     object-fit instead, which is also what Studio's focal point steers. */
  max-height: 78svh;
}

.media-frame--wide {
  aspect-ratio: 21 / 9;
}

.media-frame--square {
  aspect-ratio: 1 / 1;
}

/* --- 5. layout -------------------------------------------------------- */

.container {
  width: min(calc(100% - 2 * var(--page-padding)), var(--container));
  margin-inline: auto;
}

main > section {
  padding-block: var(--section-gap);
}

#hero {
  padding-block: 0;
}

.clients {
  padding-block: clamp(2.5rem, 4vw, 3.5rem);
  background: var(--surface);
  border-block: 1px solid var(--rule);
}

.about {
  background: var(--bg);
}

.services {
  background: var(--surface);
}

.work {
  background: var(--bg);
}

.team {
  background: var(--surface-alt);
}

.section-head {
  max-width: 62ch;
  margin-bottom: clamp(3rem, 5vw, 5rem);
}

.section-head .eyebrow {
  margin-top: var(--space-3);
}

.section-head .display {
  margin-top: var(--space-3);
}

.section-head__intro {
  margin-top: var(--space-4);
}

/* --- 6. header / nav -------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  --fg: var(--on-dark);
  --focus: var(--accent);
  color: var(--fg);
  background: transparent;
  transition: background-color var(--motion-base) var(--ease-out),
              backdrop-filter var(--motion-base) var(--ease-out);
}

/* The bottom rule is a pseudo-element, not a border. A real border adds its pixel to the
   header's box, but the hero only pulls back --header-h — so the header measured 81px against
   an 80px offset and left a lit hairline of page background above the photograph. */
.site-header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: transparent;
  transition: background-color var(--motion-base) var(--ease-out);
}

.site-header.is-scrolled {
  background: rgba(12, 14, 15, 0.82);
  backdrop-filter: blur(10px) saturate(120%);
}

.site-header.is-scrolled::after {
  background: rgba(246, 241, 232, 0.14);
}

/* Carries the shared `.container` class, so the logo's left edge and the nav's right edge sit
   on exactly the same grid lines as every section's content — including the hero copy column,
   whose left edge is pinned to the same rail. Width and centring come from `.container`; only
   the header's own layout lives here. */
.site-header__inner {
  min-height: var(--header-h);
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

/*
 * The client's own wordmark, not type: a single compound path plus the bronze square, shipped
 * as an <img> so OnePage Studio can still replace it. Its cream base fill lives in the file
 * because an <img> cannot inherit currentColor — the header is always dark, so one colourway
 * is enough. The viewBox is tight to the artwork, so `height` sets the letter height directly.
 */
.wordmark {
  display: inline-flex;
  align-items: center;
  margin-right: auto;
  /* The image is ~30px tall; the padding is what makes the link a comfortable tap target. */
  padding-block: var(--space-1);
}

.wordmark img {
  height: clamp(26px, 2.1vw, 32px);
  width: auto;
}

.site-nav__list {
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 2.4vw, 2.5rem);
}

.site-nav__link {
  position: relative;
  font-size: var(--text-sm);
  letter-spacing: 0.08em;
  padding-block: var(--space-2);
  color: var(--fg);
}

.site-nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.35em;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--motion-fast) var(--ease-out);
}

.site-nav__link:hover::after,
.site-nav__link:focus-visible::after {
  transform: scaleX(1);
}

.site-nav__link--cta {
  padding: 0.7em 1.35em;
  border: 1px solid rgba(246, 241, 232, 0.45);
}

.site-nav__link--cta::after {
  display: none;
}

.site-nav__link--cta:hover,
.site-nav__link--cta:focus-visible {
  background: var(--on-dark);
  color: var(--dark);
  border-color: var(--on-dark);
}

/* The toggle only exists once JS can operate the panel; without JS the links stay
   in flow and wrap (SPEC §36). */
.nav-toggle {
  display: none;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: var(--space-2);
}

.nav-toggle__bars {
  position: relative;
  width: 22px;
  height: 9px;
  border-top: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
}

/* --- 7. buttons ------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0.9em 1.8em;
  border: 1px solid transparent;
  border-radius: var(--radius-small);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition: transform var(--motion-fast) var(--ease-out),
              background-color var(--motion-fast) var(--ease-out),
              color var(--motion-fast) var(--ease-out),
              border-color var(--motion-fast) var(--ease-out);
}

.btn--primary {
  background: var(--on-dark);
  color: var(--dark);
  border-color: var(--on-dark);
}

.btn--primary:hover,
.btn--primary:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--dark);
  transform: translateY(-2px);
}

.btn--ghost {
  border-color: rgba(246, 241, 232, 0.45);
  color: var(--fg);
}

.btn--ghost:hover,
.btn--ghost:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
  transform: translateY(-2px);
}

.btn:active {
  transform: none;
}

.btn--large {
  min-height: 56px;
  padding: 1.05em 2.4em;
}

/* --- 8. hero ---------------------------------------------------------- */

.hero {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: max(85svh, 620px);
  margin-top: calc(-1 * var(--header-h));
  padding-top: var(--header-h);
  overflow: hidden;
  background: var(--dark);
  /*
   * The copy column's width, and the only number the hero layout is built on. It is a
   * continuous function of the viewport with no breakpoint anywhere, which is what removes
   * the old 33% -> 50% -> 100% staircase: as the window narrows the column loses absolute
   * width slowly while gaining share of the frame steadily — ~32% at 1920, ~41% at 1280,
   * ~48% at 1024, ~58% at 800, and the full content width by ~480px, where min() below hands
   * over to the page grid. Nothing here changes at the mobile switch.
   */
  --hero-copy: clamp(26rem, 22rem + 14vw, 40rem);
}

.hero__media {
  position: absolute;
  inset: 0;
  background: var(--dark);
}

/* Stacking, opacity and the dissolve come from .media-frame--frames in §4; what is left here
   is only what makes these frames the hero's. */
.hero__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Every hero asset is shot with its subject on the right and a quiet, near-empty left
     third, so the frame is anchored to the right edge at every width (SPEC §3, §23) and the
     left is what the crop eats first. */
  object-position: 100% 50%;
  /* --parallax is written by JS as 0…24px; the wrapper moves, never object-position. The
     -12px re-centres that swing so the 1.06 scale has headroom on both sides — offset from
     0 it ran out at the bottom of the travel and exposed a hairline of backdrop. */
  transform: translate3d(0, calc(var(--parallax, 12px) - 12px), 0) scale(1.06);
  will-change: transform;
}

/*
 * Two decorative layers, both belonging to the photograph rather than to the section, so the
 * mobile composition — where the media stops being a backdrop and becomes a band at the top
 * of the hero — inherits them without a second set of rules. CSS only, never an asset.
 *
 * Above the whole frame stack: .hero__image carries a transform, which paints it as though
 * positioned, so without an index of their own the photographs would cover both overlays.
 */
.hero__media::before,
.hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

/* Layer 1 — horizontal legibility veil. Fixed stops, deliberately: they are percentages of
   the frame, so the veil keeps its shape while the window is dragged instead of re-aiming at
   a moving target. The copy column's right edge lands between 45% and 62% of the frame across
   the whole horizontal band, which is inside the 0.68 stop everywhere. */
.hero__media::before {
  background: linear-gradient(
    100deg,
    rgba(12, 14, 15, 0.95) 0%,
    rgba(12, 14, 15, 0.90) 30%,
    rgba(12, 14, 15, 0.68) 52%,
    rgba(12, 14, 15, 0.30) 70%,
    rgba(12, 14, 15, 0.10) 84%,
    rgba(12, 14, 15, 0.28) 100%
  );
}

/* Layer 2 — vertical fade. Here it is the desktop bottom weighting the CTAs sit on, plus a
   short scrim under the header so the nav stays legible over a bright asset. §18 re-aims the
   same layer downward for the mobile band. */
.hero__media::after {
  background:
    linear-gradient(
      to bottom,
      rgba(12, 14, 15, 0.45) 0%,
      rgba(12, 14, 15, 0) calc(var(--header-h) + 3rem)
    ),
    linear-gradient(
      to top,
      rgba(12, 14, 15, 0.88) 0%,
      rgba(12, 14, 15, 0.28) 22%,
      rgba(12, 14, 15, 0) 48%
    );
}

/*
 * The copy is a left column, not a centered container: everything beside it has to stay
 * readable image. Its left edge sits on the page grid — the same x as every other section's
 * content, at every width — and its width is --hero-copy until the page grid is narrower,
 * at which point the copy simply is the content width. One rule, no breakpoints, so the
 * column is continuous from 320px to 2560px.
 */
.hero__inner {
  position: relative;
  z-index: 2;
  width: min(calc(100% - 2 * var(--page-padding)), var(--hero-copy));
  margin-inline-start: max(var(--page-padding), calc((100% - var(--container)) / 2));
  margin-inline-end: auto;
  padding-block: clamp(3.5rem, 8vh, 7rem);
}

/* The two spans in the markup are the only legal break points; below ~360px the line splits
   after the ampersand and "Content Agency" stays whole. The tracking gives way before the
   break does — an eyebrow this short should run out of letter-spacing, not out of line. */
.hero .eyebrow {
  letter-spacing: clamp(0.16em, 0.06em + 0.35vw, 0.22em);
}

.hero .eyebrow span {
  white-space: nowrap;
}

.hero__title {
  margin-top: var(--space-4);
  font-size: var(--display-lg);
}

.hero__lead {
  margin-top: var(--space-5);
  max-width: 46ch;
  font-size: var(--text-lg);
  color: var(--fg-muted);
}

/*
 * nowrap is the point: with wrap the pair stacked wherever the column happened to be a few
 * pixels too narrow and un-stacked again further down, because the column and the buttons
 * were shrinking on different schedules. The row now gives up gap, then padding, then type
 * size, in that order, and stays a row down to 320px.
 */
.hero__actions {
  display: flex;
  flex-wrap: nowrap;
  gap: clamp(0.5rem, 0.25rem + 0.9vw, 1rem);
  margin-top: var(--space-6);
}

.hero__actions .btn {
  white-space: nowrap;
  font-size: clamp(0.75rem, 0.5rem + 0.8vw, 0.875rem);
  padding-inline: clamp(0.9rem, 0.4rem + 2.2vw, 1.8em);
}

/* --- 9. clients ------------------------------------------------------- */

.clients__label {
  font-size: var(--text-xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--fg-muted);
  text-align: center;
}

/*
 * A grid rather than a wrapping flex row, because wrapping decides how many fit and leaves
 * whatever is left over stranded — six marks in a row that fits five is a row of five and a
 * row of one. A grid is told the column count instead, and the counts below are the divisors
 * of the number of marks, so every row is full: 6, then 3 + 3, then 2 + 2 + 2.
 *
 * TODO OWNER: these three numbers are divisors of six. Changing how many client marks the
 * list holds means changing them to that number's divisors, or the last row strands again.
 */
.clients__list {
  display: grid;
  grid-template-columns: repeat(2, auto);
  justify-content: center;
  justify-items: center;
  align-items: center;
  gap: clamp(1.75rem, 4vw, 3.5rem);
  margin-top: var(--space-5);
  --client-mark-h: 52px;
}

/* Two rows of three. Below this three marks and their gaps no longer clear the content
   column, and the grid would scale them down to fit. The threshold moved out with the marks
   themselves: at 52px each box is 104px wide, so three of them need a wider column than
   three 80px ones did. */
@media (min-width: 28rem) {
  .clients__list {
    grid-template-columns: repeat(3, auto);
  }
}

/*
 * Tablet and up the marks grow by half and then keep growing to 72px, which is as tall as
 * they can be while six of them still clear the content column. The column count stays at
 * three until 64rem: six of the larger marks do not fit before that, and a full row of three
 * big marks reads better than a cramped row of six small ones.
 *
 * Worth knowing when this looks smaller than it measures: the box is what grows, and every
 * supplied file carries its own white margin inside the frame — the artwork spans between
 * 43% and 81% of frame height across the current six. So a 72px box renders a mark somewhere
 * between 31px and 58px, and the marks look uneven with each other. Only the files can fix
 * that; exported tight to the artwork, the same box would render them all near 72px.
 */
@media (min-width: 48rem) {
  .clients__list {
    --client-mark-h: clamp(60px, 5.6vw, 72px);
  }
}

/* One row of six, from the width where all six clear the column at the larger size — with
   room to spare for a scrollbar, which takes ~15px out of the content column and left only
   two at 64rem. */
@media (min-width: 66rem) {
  .clients__list {
    grid-template-columns: repeat(6, auto);
  }
}

/* The marks are supplied as JPEGs, which cannot carry transparency, so each arrives on a
   white rectangle. multiply drops white to the backdrop and leaves everything darker than it
   alone, which is what lifts the mark off its box without needing cut-out artwork. */
.clients__list li {
  /* The blend needs a known backdrop. Without this the reveal's own opacity turns each item
     into an isolated group while it fades in, and a mark would multiply against nothing —
     showing its white box for the length of the animation and then snapping. */
  background: var(--surface);
}

/*
 * A definite box plus scale-down, which is "contain, but never larger than the file itself":
 * a mark supplied smaller than the row height renders at its own size rather than being blown
 * up past the resolution it was given at, and one supplied larger is fitted inside.
 *
 * The box is definite on purpose. Sizing to the image instead — max-height with auto width —
 * gives the same cap, but an image that has not loaded yet has no intrinsic size to be capped,
 * so every mark measures 0x0 until it arrives and the row snaps open underneath the reader.
 * A fixed box reserves the space from the first layout, which is what the width and height
 * attributes in the markup are there to support.
 */
.clients__list img {
  height: var(--client-mark-h);
  width: calc(var(--client-mark-h) * 2);
  object-fit: scale-down;
  opacity: 0.55;
  filter: grayscale(1);
  mix-blend-mode: multiply;
  transition: opacity var(--motion-base) var(--ease-out);
}

.clients__list img:hover {
  opacity: 0.85;
}

/* --- clients: the rest of the roster ---
 *
 * A disclosure rather than a longer logo wall: names carry no artwork to align and no crop to
 * argue with, so twelve of them cost one line of type each.
 */

.clients__more {
  margin-top: clamp(2rem, 4vw, 3rem);
  text-align: center;
}

/* Hidden until JS can operate the panel. Without it the names below simply stay open, which
   is the same bargain the header nav makes (§6). */
.clients__more-toggle {
  display: none;
  align-items: center;
  gap: var(--space-2);
  margin-inline: auto;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-muted);
  transition: color var(--motion-fast) var(--ease-out);
}

.js-ready .clients__more-toggle {
  display: inline-flex;
}

/*
 * The toggle is one-way: once the roster is open there is nothing left for it to say, so it
 * fades out over the same beat the panel opens on and leaves the flow only once it is already
 * invisible. Removing it at the moment of the click instead would snap everything below up by
 * its own height while the panel was still on its way down.
 */
.js-ready.clients-open .clients__more-toggle {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-base) var(--ease-out);
}

.js-ready.clients-done .clients__more-toggle {
  display: none;
}

.clients__more-toggle:hover,
.clients__more-toggle:focus-visible {
  color: var(--fg);
}

/* A chevron drawn from two borders, so it turns with the panel instead of being swapped for
   a second glyph. */
.clients__chevron {
  width: 0.5em;
  height: 0.5em;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-0.1em) rotate(-45deg);
  transition: transform var(--motion-base) var(--ease-out);
}

.js-ready.clients-open .clients__chevron {
  transform: translateY(0.1em) rotate(45deg);
}

/*
 * The panel's height is animated by a grid row rather than clipped, because this one is in
 * flow: the rest of the page has to be pushed down as it opens, not covered by it. The names
 * are a block list, so unlike the nav's flex column they keep their own height while the row
 * is short and are simply cut off.
 */
.js-ready .clients__panel {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition: grid-template-rows var(--motion-base) var(--ease-out),
              visibility 0s linear var(--motion-base);
}

.js-ready.clients-open .clients__panel {
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows var(--motion-base) var(--ease-out),
              visibility 0s;
}

.js-ready .clients__panel-inner {
  overflow: hidden;
  min-height: 0;
}

/* Space above the names is a margin on the list, not padding on the clipped box: padding does
   not collapse to nothing at 0fr and would stand as a gap under a closed panel. */
.clients__names {
  margin-top: var(--space-4);
  /* Two columns all the way down to 320px. The minimum column width is what decides that, not
     the count: at 10rem a pair no longer cleared the content column below ~430px and the list
     silently fell back to one. */
  columns: 2 7rem;
  column-gap: clamp(1rem, 4vw, 3rem);
  text-align: left;
  font-size: var(--text-sm);
  color: var(--fg-muted);
}

/* Room above and below each name, and a hairline between them — half the weight of the rule
   the rest of the page uses, because here it separates single words rather than blocks. */
.clients__names > li {
  break-inside: avoid;
  padding-block: 0.9em;
  border-top: 1px solid color-mix(in srgb, var(--rule) 55%, transparent);
}

/* --- 10. about -------------------------------------------------------- */

.about__grid {
  display: grid;
  gap: clamp(2.5rem, 5vw, 5rem);
  align-items: start;
}

.about__title {
  margin-top: var(--space-3);
}

.about__copy > p:not(.section-numeral) {
  margin-top: var(--space-4);
  max-width: 52ch;
  color: var(--fg-muted);
}

.about__copy .statement-line {
  color: var(--fg);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
  max-width: 36ch;
}

/* --- 11. services ----------------------------------------------------- */

.services__grid {
  display: grid;
  gap: 0;
  border-top: 1px solid var(--rule);
}

.service {
  /* flow-root contains the floated sketch below the column switch, where it can run
     taller than the two text lines beside it. */
  display: flow-root;
  padding-block: clamp(2rem, 3.5vw, 3rem);
  padding-inline: 0;
  border-bottom: 1px solid var(--rule);
  transition: transform var(--motion-base) var(--ease-out);
}

/* Below the column switch (§18) the cards run full width, so the sketch shares the
   card's first rows instead of stacking above them: floated right, the title and lead
   break beside it and the card stays short. */
.service__sketch {
  float: right;
  width: clamp(114px, 11.5vw, 156px);
  margin-left: var(--space-3);
}

.service__sketch img {
  display: block;
  width: 100%;
  height: auto;
}

.service__title {
  margin-top: var(--space-3);
  font-size: clamp(1.9rem, 3vw, 2.6rem);
}

.service__lead {
  margin-top: var(--space-2);
  font-weight: 500;
}

.service__body {
  margin-top: var(--space-3);
  color: var(--fg-muted);
  max-width: 38ch;
}

.services__closing {
  margin-top: clamp(3rem, 5vw, 4.5rem);
  padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
  font-family: var(--font-heading);
  font-size: clamp(1.3rem, 2vw, 1.7rem);
  line-height: 1.3;
  max-width: 52ch;
}

/* --- 12. process ------------------------------------------------------ */

.process__statement {
  margin-top: var(--space-5);
  font-family: var(--font-heading);
  font-size: clamp(1.6rem, 2.8vw, 2.4rem);
  line-height: 1.25;
  max-width: 44ch;
}

.process__track {
  position: relative;
  padding-top: var(--space-6);
}

.process__line {
  position: absolute;
  top: var(--space-5);
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(to right, var(--accent), rgba(191, 149, 86, 0.15));
  transform-origin: left;
}

.process__steps {
  display: grid;
  gap: clamp(2rem, 3.5vw, 3rem);
  counter-reset: step;
}

.process-step {
  position: relative;
  padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
}

.process-step::before {
  content: "";
  position: absolute;
  top: -3px;
  left: 0;
  width: 5px;
  height: 5px;
  background: var(--accent);
}

.process-step__index {
  font-family: var(--font-heading);
  font-size: var(--text-sm);
  letter-spacing: 0.2em;
  color: var(--accent);
}

.process-step__title {
  margin-top: var(--space-2);
  font-size: clamp(1.4rem, 2vw, 1.8rem);
}

.process-step__lead {
  margin-top: var(--space-2);
  font-weight: 500;
}

.process-step__body {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--fg-muted);
}

.process__closing {
  margin-top: clamp(3rem, 5vw, 4.5rem);
  padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
  font-family: var(--font-heading);
  font-size: clamp(1.3rem, 2vw, 1.7rem);
  line-height: 1.3;
  max-width: 52ch;
}

/* --- 13. work --------------------------------------------------------- */

.work__grid {
  display: grid;
  gap: clamp(2.5rem, 5vw, 4.5rem);
}

/* One column: every case study is a landscape band, so four stacked frames do not make the
   section twice as tall as it needs to be on a phone. */
.case-study__figure {
  margin: 0;
  aspect-ratio: 16 / 9;
}

@media (max-width: 61.99rem) {
  /*
   * The landscape version of a portrait case is its frames standing side by side — not a
   * centre crop, which would throw away half of a picture composed as a 4:5. Each half comes
   * out near 8:9 inside the 16:9 band against a 4:5 source, so it loses about a tenth off the
   * top and bottom.
   *
   * Two frames are on screen at once, which is what --frames-visible tells §17: the cycle
   * moves a pair at a time here. A case with only two frames therefore has nothing to move
   * to and simply stands still; one with four alternates between its two pairs.
   */
  .case-study--tall .case-study__figure {
    --frames-visible: 2;
  }

  /* Pinned to a half each rather than laid out in a grid, so a pair still dissolves into the
     next pair instead of cutting. inset already supplies the other three edges. */
  .case-study--tall .case-study__figure > img {
    width: calc(50% - var(--space-1) / 2);
  }

  .case-study--tall .case-study__figure > img:nth-child(even) {
    left: auto;
    right: 0;
  }
}

/* Above the diptych a case shows one frame, but the markup ships two as current so that a
   diptych is whole without JS. JS corrects the set on load; this covers the case where there
   is none. */
@media (min-width: 62rem) {
  html:not(.js-ready) .case-study--tall .case-study__figure > img:nth-child(n + 2) {
    opacity: 0;
  }
}

/*
 * The hover zoom and the dissolve share these images, so this must add a transition rather
 * than declare one: the shorthand it used to be matched §4's frames rule on specificity and
 * won on order, which dropped the opacity transition and left the work section cutting between
 * frames while every other frame box dissolved. Longhand lists keep both. .is-holding still
 * overrides them with transition: none — it is a class deeper, so it outranks this.
 */
.case-study__figure img {
  transition-property: opacity, transform;
  transition-duration: var(--frame-fade), var(--motion-slow);
  transition-timing-function: var(--ease-dissolve), var(--ease-out);
}

.case-study:hover .case-study__figure img {
  transform: scale(1.03);
}

.case-study__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin-top: var(--space-4);
  font-size: var(--text-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.case-study__tags {
  color: var(--accent-dark);
}

.case-study__title {
  margin-top: var(--space-2);
  font-size: clamp(1.6rem, 2.4vw, 2.2rem);
}

.case-study > p:not(.case-study__meta) {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  max-width: 46ch;
}

.case-study__label {
  display: block;
  font-size: var(--text-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-bottom: 0.25em;
}

/* The proof carries the credibility of the case, so it reads a step above the descriptive
   paragraphs — a thin rule and full text tone, nothing louder. */
.case-study__proof {
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
  color: var(--fg);
}

/* Dormant until a verified client quote exists; the template lives commented out in each
   case. Its own max-width is needed because the blockquote is not a <p> and therefore not
   covered by the `.case-study > p` rule above. */
.case-study__quote {
  margin-top: var(--space-3);
  max-width: 46ch;
  padding-left: var(--space-4);
  border-left: 2px solid var(--accent);
  font-family: var(--font-heading);
  font-size: clamp(1.2rem, 1.6vw, 1.45rem);
  line-height: 1.4;
}

.case-study__quote footer {
  margin-top: var(--space-2);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* --- 14. team --------------------------------------------------------- */

.team__grid {
  display: grid;
  gap: clamp(2.5rem, 5vw, 5rem);
  align-items: start;
}

.team__copy .display {
  margin-top: var(--space-3);
}

.team__copy > p:not(.section-numeral):not(.eyebrow) {
  margin-top: var(--space-4);
  max-width: 52ch;
  color: var(--fg-muted);
}

.team__copy .statement-line {
  color: var(--fg);
  max-width: 40ch;
}

/*
 * The square takes the narrow column and needs no rule of its own — a figure already fills its
 * grid track, and --square supplies the height. It comes out at roughly 500px at 1440, which
 * is around 100px taller than the copy beside it, so the two are centred against each other at
 * the breakpoint below — the same pairing About makes with its portrait. Measured at
 * 390/768/1024/1440.
 */

/*
 * Dormant people row. Nothing renders until the owner uncomments the template in
 * index.html — the section is finished as it stands, and anonymous portraits would be worse
 * than none. Layout-tested at 390/768/1024/1440/1920 with 1, 3 and 5 items before the
 * template was re-commented.
 */
.team__people {
  grid-column: 1 / -1;
  margin-top: clamp(2rem, 4vw, 3.5rem);
  padding-top: var(--space-6);
  border-top: 1px solid var(--rule);
  display: grid;
  gap: clamp(2rem, 4vw, 3rem);
}

.person__name {
  margin-top: var(--space-3);
  font-size: clamp(1.4rem, 2vw, 1.8rem);
}

.person__role {
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.person__bio {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  color: var(--fg-muted);
  max-width: 34ch;
}

/* --- 15. contact ------------------------------------------------------ */

.contact__inner {
  text-align: center;
}

.contact__title {
  font-size: clamp(3rem, 6.5vw, 6rem);
}

.contact__body {
  margin-top: var(--space-5);
  margin-inline: auto;
  max-width: 46ch;
  font-size: var(--text-lg);
  color: var(--fg-muted);
}

.contact__inner > p:last-child {
  margin-top: var(--space-6);
}

/* --- 16. footer ------------------------------------------------------- */

.site-footer {
  padding-block: clamp(2.5rem, 4vw, 4rem);
  background: var(--dark-soft);
  --fg: var(--on-dark);
  --fg-muted: var(--on-dark-muted);
  --rule: rgba(246, 241, 232, 0.18);
  --focus: var(--accent);
  color: var(--fg);
}

.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: end;
  justify-content: space-between;
}

/* Same asset as the header, one step smaller — a typographic wordmark here would read as a
   second, competing logo. */
.site-footer__wordmark {
  height: 24px;
  width: auto;
}

.site-footer__descriptor,
.site-footer__legal {
  font-size: var(--text-sm);
  color: var(--fg-muted);
}

/* --- 17. motion ------------------------------------------------------- */

/* Default state is the finished state: with no JS, or before the observer runs,
   everything is visible and in place. */
[data-reveal],
[data-reveal-group] > * {
  opacity: 1;
  transform: none;
}

.motion-ready [data-reveal],
.motion-ready [data-reveal-group] > * {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--motion-slow) var(--ease-out),
              transform var(--motion-slow) var(--ease-out);
}

.motion-ready [data-reveal].is-visible,
.motion-ready [data-reveal-group] > .is-visible {
  opacity: 1;
  transform: none;
}

/* The process line draws with a transform, never width (SPEC §18). */
.motion-ready .process__line {
  transform: scaleX(0);
  transition: transform 1.1s var(--ease-out);
}

.motion-ready .process__line.is-visible {
  transform: scaleX(1);
}

/* --- 18. responsive --------------------------------------------------- */

@media (min-width: 48rem) {
  .site-footer__inner {
    flex-wrap: nowrap;
  }
}

/* The hero has no breakpoint between here and 2560px: column width, headline size, CTA
   metrics and both overlays are continuous functions of the viewport (§8). The only hero
   media query in the file is the composition switch at the foot of this section. */

/* One desktop breakpoint for every multi-column band. Splitting them earlier was tried and
   rejected: at 768 the three service columns fall to ~214 px, which is roughly 13 characters
   per line — collapsed, not designed. */
@media (min-width: 62rem) {
  .about__grid {
    grid-template-columns: 5fr 7fr;
    align-items: center;
  }

  .services__grid {
    grid-template-columns: repeat(3, 1fr);
    column-gap: clamp(1.5rem, 3vw, 3rem);
  }

  .service {
    border-bottom: 0;
    border-left: 1px solid var(--rule);
    padding-inline: clamp(1.25rem, 2.5vw, 2rem);
  }

  /* In three narrow columns there is no room beside the title — the sketch goes back
     above it, keeping the right edge. */
  .service__sketch {
    float: none;
    margin-left: auto;
  }

  .service:first-child {
    border-left: 0;
    padding-inline-start: 0;
  }

  .team__grid {
    grid-template-columns: 6fr 5fr;
    align-items: center;
  }

  .team__people {
    grid-template-columns: repeat(3, 1fr);
  }

  .process__steps {
    grid-template-columns: repeat(4, 1fr);
    gap: clamp(1.5rem, 2.5vw, 2.5rem);
  }

  /*
   * Two columns, sized so a landscape frame and the portrait frame beside it come out exactly
   * the same height — which is what stops the copy under them starting at two different
   * heights. At 16:9 and 4:5 equal heights means widths in a 20:9 ratio, so the row is 20 + a
   * gutter + 9 = 30 tracks. Both frames land at 10/30 of the row.
   *
   * The gutter is a track, not a column-gap: a gap is subtracted from both items equally, so
   * a 20-wide and a 9-wide item would no longer be in a 20:9 ratio and the two frames would
   * land a few pixels out of step. With column-gap at 0 the ratio is exact.
   *
   * The staggered offsets the two rows used to carry — align-self on the second case, a
   * margin-top on the fourth — are gone for the same reason.
   */
  .work__grid {
    grid-template-columns: repeat(30, 1fr);
    column-gap: 0;
    row-gap: clamp(3rem, 6vw, 6rem);
  }

  .case-study:nth-child(1) {
    grid-column: 1 / span 20;
  }

  .case-study:nth-child(2) {
    grid-column: 22 / span 9;
  }

  .case-study:nth-child(3) {
    grid-column: 1 / span 9;
  }

  .case-study:nth-child(4) {
    grid-column: 11 / span 20;
  }

  /* A portrait case is only a portrait while there is a landscape one next to it. */
  .case-study--tall .case-study__figure {
    aspect-ratio: 4 / 5;
  }
}

/*
 * The one real composition breakpoint in the file, and the only place the hero changes by
 * step rather than by slope. Two things happen here and nothing else does: the desktop nav
 * row becomes a drop-down panel, and the photograph stops being a backdrop behind the copy
 * and becomes a band above it. Everything the copy itself is made of — column width,
 * headline size, eyebrow tracking, CTA metrics — passes through this width unchanged.
 */
@media (max-width: 49.99rem) {
  :root {
    --header-h: 64px;
  }

  /* No padding-block. The toggle is the tallest thing in the bar at ~45px, so --header-h
     already holds it with room to spare; the padding pushed the rendered header to 68.8px
     against a 64px hero offset and lit a 4.8px band of page background above the image. */
  .site-header__inner {
    flex-wrap: wrap;
  }

  .js-ready .nav-toggle {
    display: inline-flex;
  }

  /*
   * A genuine drop-down: absolutely positioned, so opening it cannot reflow the hero, and
   * revealed by clip-path rather than by growing a box.
   *
   * The distinction matters. The panel is a flex column, and a container shorter than its
   * content shrinks every item in it: animating the height — a 0fr → 1fr grid row, a
   * max-height, anything — collapses all five links into a pile under the header at frame
   * one and then slides them apart as the box grows, which read as the "Let's talk" button
   * starting at the top and dropping to the bottom. Clipping animates nothing but the
   * visible region: the menu is fully built and every link is already in its final place
   * before the first frame. It unrolls, it does not assemble.
   *
   * visibility, not display, carries the closed panel out of the tab order and the
   * accessibility tree — clip-path alone hides it from the eye but not from the keyboard.
   * It flips instantly on open and waits out the close so the collapse stays visible.
   */
  .js-ready .site-nav {
    --nav-duration: 380ms;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    clip-path: inset(0 0 100% 0);
    visibility: hidden;
    background: rgba(12, 14, 15, 0.96);
    backdrop-filter: blur(10px) saturate(120%);
    transition: clip-path var(--nav-duration) var(--ease-out),
                visibility 0s linear var(--nav-duration);
  }

  .js-ready.nav-open .site-nav {
    clip-path: inset(0 0 0 0);
    visibility: visible;
    transition: clip-path var(--nav-duration) var(--ease-out),
                visibility 0s;
  }

  .js-ready .site-nav__list {
    width: min(calc(100% - 2 * var(--page-padding)), var(--container));
    margin-inline: auto;
    padding-block: var(--space-4) var(--space-5);
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
  }

  .js-ready.nav-open .site-header {
    background: rgba(12, 14, 15, 0.96);
    backdrop-filter: blur(10px);
  }

  .js-ready.nav-open .site-header::after {
    background: rgba(246, 241, 232, 0.18);
  }

  .js-ready .site-nav__link {
    font-size: 1.05rem;
  }

  /* Without JS the links stay in the header and wrap as a row — five stacked links would
     make a 200 px+ sticky bar. Sticky is dropped too, so the tall header scrolls away. */
  .site-nav__list {
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
  }

  /* Without JS the header is a solid bar in the flow, not a transparent overlay: at this
     width the wrapped link row makes it ~131px, so the hero's -64px pull used to slide the
     photograph over the lower half of the nav while the upper half showed cream page
     background behind cream text. Opting the hero out of the overlay is the whole fix. */
  html:not(.js-ready) .site-header {
    position: static;
    background: var(--dark);
  }

  html:not(.js-ready) .hero {
    margin-top: 0;
  }

  .site-nav__link--cta {
    margin-top: var(--space-2);
  }

  /* --- hero: horizontal composition becomes vertical --- */

  /* padding-top goes with the flex layout: the image is meant to run up under the still
     transparent header, exactly as the photograph did when it was the backdrop. */
  .hero {
    display: block;
    min-height: 0;
    padding-top: 0;
  }

  /*
   * The media joins the flow as a band at the top of the hero. Its height is fluid while its
   * width is the viewport, so the frame's aspect narrows continuously as the phone gets
   * narrower — and because the image is anchored right, that is the left third cropping
   * itself away a little at a time rather than at a breakpoint: roughly the full frame at
   * 800px, the right 83% at 600px, the right 62% at 390px. svh caps it on landscape phones.
   */
  .hero__media {
    position: relative;
    inset: auto;
    height: clamp(17rem, 14.5rem + 23vw, 30rem);
    max-height: 62svh;
  }

  /* Right-anchored origin so the parallax zoom eats the quiet left side, never the subject.
     1.1 rather than 1.06 because the band is short and the same 24px of travel needs more
     headroom in it. */
  .hero__image {
    transform-origin: 100% 50%;
    transform: translate3d(0, calc(var(--parallax, 12px) - 12px), 0) scale(1.1);
  }

  /* Layer 1 all but gone — there is no copy beside the image left to make legible, and at
     full strength it was flattening the photograph into the background. */
  .hero__media::before {
    opacity: 0.16;
  }

  /* Layer 2 takes over: the image melts into the hero's own --dark over its bottom 40%, so
     the band and the copy below read as one section rather than a card on a panel. The copy
     can then sit slightly inside the fade without losing contrast. */
  .hero__media::after {
    background:
      linear-gradient(
        to bottom,
        rgba(12, 14, 15, 0.55) 0%,
        rgba(12, 14, 15, 0) calc(var(--header-h) + 2.5rem)
      ),
      linear-gradient(
        to top,
        #0c0e0f 0%,
        rgba(12, 14, 15, 0.88) 12%,
        rgba(12, 14, 15, 0) 40%
      );
  }

  .hero__inner {
    margin-top: calc(-1 * clamp(1rem, 5vw, 2.5rem));
    padding-block: clamp(1.5rem, 5vw, 2.5rem) clamp(3rem, 9vw, 4.5rem);
  }

  .contact__inner {
    text-align: left;
  }

  .contact__body {
    margin-inline: 0;
  }
}

/* --- 19. reduced motion ---------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  .hero__image {
    transform: scale(1.06);
  }

  /* The blanket rule above only zeroes durations, and these panels time their close with a
     delay — left standing, each would still take its full duration to leave the tab order. */
  .site-nav,
  .clients__panel {
    transition-delay: 0s !important;
  }
}
