/* ═══════════════════════════════════════════════════════════
   Ashworth & Stone — kiln-fired brick at dusk
   Display: Zodiak (Fontshare) · UI: Archivo (Google)
   ═══════════════════════════════════════════════════════════ */

:root {
  --bg:          oklch(1 0 0);
  --surface:     oklch(0.955 0.006 355);
  --ink:         oklch(0.18 0.012 350);
  --muted:       oklch(0.42 0.02 350);
  --brick:       oklch(0.4 0.13 5);
  --brick-deep:  oklch(0.33 0.115 5);
  --dark:        oklch(0.16 0.015 350);
  --on-dark:     oklch(0.965 0.008 85);
  --on-dark-dim: oklch(0.78 0.015 85);
  --line:        oklch(0.18 0.012 350 / 0.14);
  --line-dark:   oklch(1 0 0 / 0.16);

  --font-display: 'Zodiak', Georgia, 'Times New Roman', serif;
  --font-ui: 'Archivo', 'Segoe UI', system-ui, sans-serif;

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  --z-nav: 10;
  --z-menu: 20;

  /* Shape of every project card — reel tile, wheel card and outlet card all derive from this,
     and the thumbnail files are cropped to match it exactly (scratchpad/fillcrop.ps1), so the
     picture fills its card with no padding and CSS crops nothing.
     0.55 is not arbitrary. These photos are upright shopfronts: measured across all 43 they
     run 0.42–0.75 with a median of 0.543. The old 3/4 (0.75) frame was the worst possible
     choice — it threw away 26% of the average photo and cut 37 of 42 by more than 10%, which
     is why logos kept losing their edges. At 0.55 the average loss is 7%, the median tile
     keeps 94%, and 36 of 43 keep 90%+. If thumbnails are ever replaced, re-run the ratio
     analysis before changing this. */
  --frame-ar: 0.55;
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

img { display: block; max-width: 100%; }

h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; }

.skip-link {
  position: absolute; left: 1rem; top: -3rem;
  background: var(--ink); color: var(--bg);
  padding: 0.5rem 1rem; z-index: var(--z-menu);
  transition: top 0.2s var(--ease-out);
}
.skip-link:focus { top: 1rem; }

/* ── Navigation ─────────────────────────────────────────── */

.nav {
  position: fixed; inset: 0 0 auto 0;
  z-index: var(--z-nav);
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.1rem clamp(1.25rem, 4vw, 3rem);
  color: var(--on-dark);
  background: transparent;
  transition: color 0.35s var(--ease-out);
}
/* clear the whole way; text flips dark over light sections so it stays legible */
.nav.on-light { color: var(--ink); }
.nav.on-light .brand-name,
.nav.on-light .nav-links a { color: var(--ink); }
.nav.on-light .nav-toggle span { background: var(--ink); }

.nav-brand {
  display: inline-flex; align-items: center; gap: 0.6rem;
  color: inherit; text-decoration: none;
}
.brand-mark {
  inline-size: 3rem; block-size: auto;
  flex-shrink: 0;
}
.brand-name {
  font-family: var(--font-display);
  font-size: 1.15rem; letter-spacing: 0.01em;
}

.visually-hidden {
  position: absolute; inline-size: 1px; block-size: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.nav-links { display: flex; align-items: center; gap: clamp(1.2rem, 2.5vw, 2.2rem); }
.nav-links a {
  color: inherit; text-decoration: none;
  font-size: 0.8rem; font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase;
  opacity: 0.92;
  transition: opacity 0.2s var(--ease-out);
}
.nav-links a:hover { opacity: 1; text-decoration: underline; text-underline-offset: 0.45em; text-decoration-thickness: 1px; }

.nav-cta, .btn-primary {
  background: var(--brick);
  color: oklch(1 0 0);
  border: 0; border-radius: 999px;
  padding: 0.8rem 1.4rem;
  font-family: var(--font-ui); font-size: 0.8rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
  cursor: pointer; text-decoration: none;
  transition: background 0.25s var(--ease-out), transform 0.25s var(--ease-out);
}
.nav-links a.nav-cta:hover { background: var(--brick-deep); opacity: 1; text-decoration: none; transform: translateY(-1px); }
.btn-primary:hover { background: var(--brick-deep); transform: translateY(-1px); }

.nav-toggle {
  display: none;
  background: none; border: 0; cursor: pointer;
  padding: 0.6rem; margin: -0.6rem;
  flex-direction: column; gap: 7px;
}
.nav-toggle span {
  display: block; inline-size: 26px; block-size: 2px;
  background: var(--on-dark);
  transition: transform 0.3s var(--ease-out);
}
.nav-open .nav-toggle span:first-child { transform: translateY(4.5px) rotate(45deg); }
.nav-open .nav-toggle span:last-child { transform: translateY(-4.5px) rotate(-45deg); }

/* ── Hero ───────────────────────────────────────────────── */

.hero { position: relative; min-block-size: 100svh; }
/* pinned viewport; when the hero animation loads, JS stretches .hero and scroll
   scrubs the video's playhead while this stays pinned */
.hero-pin {
  position: sticky;
  inset-block-start: 0;
  min-block-size: 100svh;
  display: grid;
  place-items: center;
  color: var(--on-dark);
  overflow: hidden;
}

/* The dark brand panel behind the hero film. It is what shows while the film slot in
   index.html is empty (the client's new office film lands December) — the hero then reads as
   a deliberate dark title card rather than a blank gap, and the headline keeps the contrast
   it was designed against. It also covers the moment before a film has downloaded, so this
   stays useful once one is in. */
.hero-media {
  position: absolute; inset: 0;
  background:
    radial-gradient(120% 80% at 50% 0%, oklch(0.26 0.02 350) 0%, transparent 60%),
    radial-gradient(90% 70% at 80% 100%, oklch(0.24 0.03 20) 0%, transparent 55%),
    linear-gradient(180deg, oklch(0.17 0.014 350), oklch(0.12 0.012 350));
}
.hero-video {
  inline-size: 100%; block-size: 100%;
  object-fit: cover;
}
/* Ken Burns drift, but ONLY once there is something to drift. Gated on [src] so an empty
   slot doesn't animate a blank element for nothing. */
.hero-video[src] { animation: kenburns 26s var(--ease-out) infinite alternate; }
.hero.has-video .hero-video { animation: none; }
@keyframes kenburns {
  from { transform: scale(1); }
  to   { transform: scale(1.09) translateY(-1.5%); }
}
.hero-scrim {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    oklch(0.1 0.01 350 / 0.62) 0%,
    oklch(0.1 0.01 350 / 0.28) 38%,
    oklch(0.1 0.01 350 / 0.34) 62%,
    oklch(0.1 0.01 350 / 0.66) 100%);
}

.hero-inner {
  position: relative;
  z-index: 1;
  text-align: center;
  padding: 6rem clamp(1.25rem, 5vw, 3rem) 4rem;
  max-inline-size: 60rem;
}
/* hero photo dissolves into the white page below */
.hero-pin::after {
  content: '';
  position: absolute;
  inset-inline: 0; inset-block-end: 0;
  block-size: clamp(6rem, 18svh, 13rem);
  background: linear-gradient(180deg, transparent, var(--bg));
  pointer-events: none;
}

.hero-crest {
  display: flex; flex-direction: column; align-items: center;
  margin: 0 0 1.6rem;
}
.crest-lockup { inline-size: clamp(7rem, 14vw, 9.5rem); block-size: auto; }

.hero-rule {
  display: flex; align-items: center; justify-content: center; gap: 1rem;
  margin: 0 0 2.2rem;
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.28em; text-transform: uppercase;
  color: oklch(0.88 0.02 85);
}
.hero-rule span { flex: 0 1 4.5rem; block-size: 1px; background: oklch(1 0 0 / 0.4); }

.hero-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.5rem, 7vw, 4.6rem);
  line-height: 1.08;
  letter-spacing: -0.02em;
}
.hero-title em { font-style: italic; }

.hero-sub {
  margin: 2rem auto 0;
  max-inline-size: 44ch;
  font-size: clamp(0.95rem, 1.6vw, 1.05rem);
  line-height: 1.75;
  color: oklch(0.95 0.008 85);
  text-shadow: 0 1px 2px oklch(0.1 0.01 350 / 0.55), 0 0 28px oklch(0.1 0.01 350 / 0.5);
}
.hero-title { text-shadow: 0 2px 6px oklch(0.1 0.01 350 / 0.3); }

/* ── Curtain reveal (about page) ────────────────────────── */

.mission {
  --p: 1; /* 0 = images parted out · 1 = assembled into the stack */
  position: relative;
  min-block-size: 100svh;
  display: grid;
  align-items: center;
  overflow: hidden;
  background: var(--bg);
}
.mission-text {
  position: relative;
  z-index: 1;
  padding: 6rem clamp(1.5rem, 6vw, 5rem) 4rem;
  margin-inline-start: 47%;
  max-inline-size: 42rem;
}
.mission-text h1 {
  font-family: 'Lato', var(--font-ui);
  font-weight: 900;
  font-size: clamp(3.2rem, 8vw, 6rem);
  line-height: 1.02;
  letter-spacing: -0.015em;
  color: var(--brick);
  margin: 0 0 1.6rem;
}
.mission-text p {
  margin: 0;
  font-family: 'Lato', var(--font-ui);
  font-size: clamp(1.15rem, 1.9vw, 1.4rem);
  color: var(--muted);
  line-height: 1.75;
}

/* offset editorial stack on the left: portrait over landscape */
.mission-stack {
  position: absolute;
  inset-block: 0; inset-inline-start: 0;
  inline-size: 47%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(1rem, 2.5vh, 2rem);
  padding: clamp(3rem, 8vh, 6rem) clamp(1.5rem, 3vw, 3rem);
}
.mission-img { margin: 0; overflow: hidden; border-radius: 6px; box-shadow: 0 20px 50px oklch(0.18 0.012 350 / 0.28); cursor: zoom-in; }
.mission-img img {
  inline-size: 100%; block-size: 100%; object-fit: cover; display: block;
  /* base scale >1 so the cursor tilt never exposes the frame corners */
  transform: perspective(800px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) scale(var(--z, 1.05));
  transition: transform 0.3s var(--ease-out);
}
.mission-img img.zoomed { --z: 1.18; }
.mission-img:has(img.zoomed) { cursor: zoom-out; }
.mission-img--portrait {
  align-self: flex-start;
  inline-size: min(66%, 20rem);
  aspect-ratio: 3 / 4;
  /* swoops in from the left, easing to place as --p → 1 */
  transform: translateX(calc((1 - var(--p)) * -125%));
  opacity: calc(0.2 + 0.8 * var(--p));
}
.mission-img--landscape {
  align-self: flex-end;
  inline-size: min(94%, 30rem);
  aspect-ratio: 4 / 3;
  margin-block-start: -12%; /* overlap the portrait for a collage feel */
  /* rises in from the bottom */
  transform: translateY(calc((1 - var(--p)) * 135%));
  opacity: calc(0.2 + 0.8 * var(--p));
}
.mission-img { transition: transform 1.1s var(--ease-out), opacity 1.1s var(--ease-out); }

/* ── 7 steps journey (about page) ───────────────────────── */

/* Same layout at every size: a vertical list on a lit rail, on the page's normal light
   background. Each step itself is a static black card with white copy, always showing
   (no hover/tap reveal, no scroll fade-in — the .revealed rule below only lights the
   rail dot as a scroll-progress cue, it doesn't hide any content), and not interactive. */
.steps {
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
  max-inline-size: 56rem;
  margin-inline: auto;
  padding: 5rem 1.5rem 5rem 3.5rem;
}

.steps-title {
  margin: 0 0 1.5rem;
  text-align: center;
  font-family: 'Lato', var(--font-ui);
  font-weight: 900;
  font-size: clamp(2.8rem, 5.5vw, 5rem);
  line-height: 1.05;
  color: var(--ink);
}
.steps-title b {
  font-size: 2.2em;
  line-height: 0;
  vertical-align: -0.12em;
  color: oklch(0.72 0.11 80);
  padding-inline: 0.06em;
}

.step {
  position: relative;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 1.5rem 1.4rem;
  font-family: 'Lato', var(--font-ui);
  border: 1px solid var(--dark);
  border-radius: 6px;
  background: var(--dark);
  color: var(--on-dark);
}
.step-n, .step-body { grid-area: 1 / 1; }
.step-body { display: grid; gap: 0.45rem; opacity: 1; }

.step-n {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 3.2rem;
  line-height: 1;
  color: oklch(0.85 0.03 80);
  opacity: 0.18; /* watermark behind the always-visible copy */
}
.step-t {
  display: block;
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.25;
  color: var(--on-dark);
}
.step-p { display: block; font-size: 0.88rem; line-height: 1.5; color: var(--on-dark-dim); }

/* the guide: a vertical rail with a dot per step, lighting up as you scroll past it */
.step::before {
  content: '';
  position: absolute;
  inset-inline-start: -1.75rem;
  inset-block: -1.2rem 0;
  inline-size: 2px;
  background: oklch(0.9 0.008 80);
  transition: background 0.5s var(--ease-out);
}
.step:first-of-type::before { inset-block-start: 1.95rem; }          /* begin at the dot */
.step:last-of-type::before { inset-block-end: calc(100% - 1.95rem); } /* end at the dot */
.step::after {                                                        /* the marker */
  content: '';
  position: absolute;
  inset-inline-start: calc(-1.75rem - 4px);
  inset-block-start: 1.6rem;
  inline-size: 10px; block-size: 10px;
  border-radius: 50%;
  background: oklch(0.9 0.008 80);
  transition: background 0.45s var(--ease-out), scale 0.45s var(--ease-out);
}
.step.revealed::before, .step.revealed::after { background: oklch(0.72 0.11 80); }
.step.revealed::after { scale: 1.2; }

/* ── Meet our team (about page): expanding duotone panels ─ */

/* the page crosses from its light half into the dark half here, in one long fade */
.team { background: var(--dark); }
.team::before {
  content: '';
  display: block;
  block-size: clamp(7rem, 18svh, 12rem);
  background: linear-gradient(180deg, var(--bg), oklch(0.16 0.015 350 / 0));
}
.team-title {
  margin: 0 auto clamp(2rem, 5vh, 3.5rem);
  padding-block-start: clamp(2rem, 5vh, 3.5rem);
  text-align: center;
  font-family: 'Lato', var(--font-ui);
  font-weight: 900;
  font-size: clamp(2.8rem, 5.5vw, 5rem);
  line-height: 1.05;
  color: var(--on-dark);
}
.team-title b { color: oklch(0.72 0.11 80); }

.team-panels {
  display: flex;
  block-size: min(88svh, 52rem);
  background: var(--dark);
  /* so .member img can size itself against the ROW, which never changes, instead of
     against .member, which changes every frame of the expand. See .member img below. */
  container-type: inline-size;
}
.member {
  position: relative;
  flex: 1;
  overflow: hidden;
  cursor: pointer;
  transition: flex 0.7s var(--ease-out);
}
.member:hover, .member.open, .member:focus-visible { flex: 2.4; }
.member:focus-visible { outline: 2px solid var(--brick); outline-offset: -2px; }
.member img {
  position: absolute; inset-block: 0;
  /* Fixed width, centred on the panel — NOT inline-size:100%.
     .member animates `flex` for 0.7s, so its width changes on every frame. With a 100%
     image that means the image's own layout box is invalidated 40-odd times per expand,
     and each invalidation repaints a 1.2MP photo through the four-function filter chain
     below. Pinning the width to 71cqi (the row's inline size — 2.4/3.4 = 70.6% is the
     widest a panel ever gets, rounded up) makes the image's box constant, so the expand
     is just .member's overflow clip moving over an already-painted layer.
     Renders identically: the panel's HEIGHT is fixed here, so object-fit:cover was
     already painting at a constant scale and only the crop was changing. */
  /* Centred by hand, not by `margin: auto`: the image is deliberately WIDER than the panel
     it sits in, and for an over-constrained absolute box whose auto margins would come out
     negative, CSS pins it to the start edge instead of centring (CSS 2.1 10.3.7). That put
     the face outside the squeezed panel. 50% less half the width centres it in every state. */
  inset-inline-start: 50%;
  margin-inline-start: -36cqi;
  /* the global `img { max-width: 100% }` reset on line 53 would clamp 71cqi straight back
     down to the panel's own width, which is the whole thing we're avoiding */
  max-inline-size: none;
  /* 2.4/3.4 = 70.6% is the widest a panel ever gets; 72 leaves a little headroom for
     sub-pixel rounding, and stays under the 689px the photo paints at, so `cover` keeps
     taking its scale from the height and the painted size stays put. */
  inline-size: 72cqi; block-size: 100%;
  object-fit: cover;
  /* On a wide monitor the panel is wider than a portrait photo can fill, so `cover` takes
     its scale from the WIDTH and has to crop vertically. Centred, that beheads them (the
     old code did this too, but only once you expanded a panel). Top-aligned it crops the
     hem instead and the head is always whole. No effect on narrower screens, where the
     height drives the scale and there is no vertical crop to place. */
  object-position: 50% 0%;
  /* gold duotone at rest, true colour when the panel takes the stage */
  filter: grayscale(1) sepia(0.4) saturate(1.5) hue-rotate(-12deg) brightness(0.72);
  transition: filter 0.7s var(--ease-out), transform 0.7s var(--ease-out);
}
.member:hover img, .member.open img, .member:focus-visible img {
  filter: none;
  transform: scale(1.03);
}
.member::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 40%, oklch(0.1 0.01 350 / 0.88));
}
.member-info {
  position: absolute;
  inset-block-end: 0;
  /* fixed width, anchored to the panel's OUTER edge: text never rides the moving seam */
  inset-inline: 0 auto;
  inline-size: min(44rem, 66vw);
  z-index: 1;
  display: grid;
  gap: 0.5rem;
  padding: clamp(1.2rem, 3vw, 2.5rem);
  color: var(--on-dark);
  font-family: 'Lato', var(--font-ui);
}
.member:last-child .member-info {
  inset-inline: auto 0;
  text-align: right;
}
.member-info h3 {
  margin: 0;
  font-weight: 900;
  font-size: clamp(1.4rem, 2.6vw, 2.1rem);
  line-height: 1.1;
}
.member-role {
  margin: 0;
  font-size: 0.78rem; font-weight: 700;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: oklch(0.78 0.1 80);
}
.member-stat {
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.3rem, 2.4vw, 1.9rem);
  color: oklch(0.82 0.09 80);
}
.member-bio {
  margin: 0 0 0.6rem;
  max-inline-size: 44rem;
  font-size: 0.92rem;
  line-height: 1.65;
  color: var(--on-dark-dim);
}
.member-stat, .member-bio {
  opacity: 0;
  translate: 0 14px;
  /* LEAVING: quick and immediate. The 0.15s delay belongs only on the way in — applied
     to both directions it made switching between the two panels feel gluey, because the
     outgoing copy sat there for 150ms before it even began to go. */
  transition: opacity 0.25s var(--ease-out), translate 0.25s var(--ease-out);
}
.member:hover .member-stat, .member.open .member-stat, .member:focus-visible .member-stat,
.member:hover .member-bio, .member.open .member-bio, .member:focus-visible .member-bio {
  opacity: 1;
  translate: 0 0;
  /* ARRIVING: slower, and held back so the copy lands after the panel has opened */
  transition-duration: 0.5s;
  transition-delay: 0.18s;
}

/* ── Sections shared ────────────────────────────────────── */

.section-head {
  max-inline-size: 46rem;
  margin: 0 auto;
  text-align: center;
  padding-inline: clamp(1.25rem, 5vw, 3rem);
}
.section-head h2, .services h2, .contact h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.1rem, 5vw, 3.4rem);
  line-height: 1.08;
  letter-spacing: -0.015em;
  margin: 0 0 1.2rem;
}
.section-head p { color: var(--muted); max-inline-size: 52ch; margin-inline: auto; }

/* ── All projects: 3D thumbnail ring ─────────────────────── */

.allprojects {
  background: var(--dark);
  color: var(--on-dark);
  text-align: center;
  padding: clamp(4rem, 10vh, 7rem) 0 clamp(2rem, 5vh, 4rem);
  overflow: hidden;
}
.allprojects-title {
  margin: 0;
  font-family: 'Lato', var(--font-ui);
  font-weight: 900;
  font-size: clamp(2.2rem, 5vw, 3.8rem);
  line-height: 1.05;
}
.allprojects-title b { color: oklch(0.72 0.11 80); }
.allprojects-hint {
  margin: 0.8rem 0 0;
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: oklch(0.62 0.03 80);
}

.ring-stage {
  --item-w: clamp(96px, 13vw, 190px);
  position: relative;
  block-size: clamp(24rem, 46vh, 36rem);
  margin-block-start: clamp(2rem, 5vh, 4rem);
  perspective: 1600px;
  cursor: ew-resize;
}
.ring {
  position: absolute;
  inset: 0;
  margin: auto;
  inline-size: var(--item-w);
  --ring-h: calc(var(--item-w) / var(--frame-ar));
  block-size: var(--ring-h);
  transform-style: preserve-3d;
}
/* two rings crossing in opposite tilts → they intertwine. Keep the tilt shallow:
   past ~12deg the wheels stop reading as wheels and collide into a bowtie. */
/* --ring-shift is set by app.js: perspective magnifies the near (lower) tiles, so the
   rendered wheel hangs below the ring's own box — this nudges it back to centre. */
.ring--a { transform: translateY(var(--ring-shift, 0px)) rotateZ(9deg) rotateX(-6deg) rotateY(var(--spin-a, 0deg)); }
.ring--b { transform: translateY(var(--ring-shift, 0px)) rotateZ(-9deg) rotateX(-6deg) rotateY(var(--spin-b, 0deg)); }
.ring-item {
  position: absolute;
  /* every card identical, matching the 3:4 thumbnail files */
  inset: 0;
  backface-visibility: hidden; /* only the front half of the ring shows */
  border-radius: 6px;
  overflow: hidden;
  background: oklch(0.16 0.008 350); /* mat behind the contained thumbnail */
  box-shadow: 0 12px 30px oklch(0.05 0.01 350 / 0.5);
}
/* cover — the thumbnail files are pre-cropped to 3:4 around the signage, so this fills the
   card without cutting anything. Same reasoning as .slot img. */
.ring-item img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
/* depth vignette so the sides recede into the dark */
.ring-stage::before, .ring-stage::after {
  content: '';
  position: absolute; inset-block: 0; inline-size: clamp(4rem, 16vw, 12rem);
  z-index: 2; pointer-events: none;
}
.ring-stage::before { inset-inline-start: 0; background: linear-gradient(90deg, var(--dark), transparent); }
.ring-stage::after { inset-inline-end: 0; background: linear-gradient(270deg, var(--dark), transparent); }

/* ── Projects: 32-slot select reel (video-game character select) ── */

.projects { background: var(--dark); color: var(--on-dark); }
.projects-pin {
  position: sticky;
  inset-block-start: 0;
  block-size: 100svh;
  display: grid;
  grid-template-rows: auto 1fr auto;
  overflow: hidden;
}
.projects-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding: 5.5rem clamp(1.25rem, 4vw, 3rem) 1rem;
}
.projects-head h2 {
  margin: 0;
  font-family: 'Lato', var(--font-ui);
  font-weight: 900;
  font-size: clamp(2rem, 4.5vw, 3.4rem);
  line-height: 1.05;
}
.projects-head b { color: oklch(0.72 0.11 80); }
.projects-count {
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.4rem, 2.6vw, 2.1rem);
  color: oklch(0.78 0.1 80);
  white-space: nowrap;
}
.reel {
  /* every tile is this tall; only the width varies, by photo */
  --tile-h: min(clamp(25rem, 40vw, 34rem), 58svh);
  display: flex;
  align-items: center;
  gap: clamp(2.5rem, 5vw, 4.5rem);
  inline-size: max-content;
  /* pad half a viewport so slot 01 and slot 32 can occupy the centre spot */
  padding-inline: calc(50vw - min(clamp(8.5rem, 13.5vw, 12.5rem), 21svh));
  will-change: transform;
}
.slot {
  position: relative;
  /* Every tile is the SAME size — the client wants a consistent row, not a filmstrip of
     varying widths. That works without cropping because the thumbnail files are themselves
     all 600x800: each holds its whole photo centred, with the margin filled by extending the
     photo's own edge pixels (see scratchpad/uniform.ps1 and the project memory). So the box
     and the artwork are the same shape and `cover` below crops nothing.
     Stated as an explicit calc rather than `aspect-ratio` + `inline-size: auto`: these are
     flex items, and flex resolves the main size from content, which ignored the ratio. */
  block-size: var(--tile-h);
  inline-size: calc(var(--tile-h) * var(--frame-ar));
  flex-shrink: 0;
  /* Do NOT add `content-visibility: auto` here. It was tried 2026-08-17 to skip rendering
     off-screen tiles, and it makes them visibly pop in unpainted as the strip slides — the
     tiles read as grey and the centre one's colour and scale arrive late. Its measured gain
     was inside the noise anyway (6 vs 10 long frames over 21s, alternating warmed-up test),
     so it bought a real visual regression for nothing. */
  border: 1px solid oklch(1 0 0 / 0.16);
  border-radius: 6px;
  overflow: hidden;
  padding: 0;
  /* the mat the thumbnail sits on — see object-fit below */
  background: oklch(0.16 0.008 350);
  cursor: pointer;
  transition: border-color 0.3s var(--ease-out);
}
.slot img {
  position: absolute; inset: 0;
  inline-size: 100%; block-size: 100%;
  /* cover, and it crops nothing: every thumbnail file is now itself 3:4, pre-cropped around
     its signage (see _thumb-backup/ for the originals and the note in the project memory).
     Cover always crops from the CENTRE, which is what cut the brand names off before — the
     fix was to move that decision out of CSS and into the files, where the crop could be
     placed over the sign instead of the middle. Don't switch this to `contain`: that
     letterboxes and the client rejected it. */
  object-fit: cover;
  filter: grayscale(0.85) brightness(0.75);
  transition: filter 0.3s var(--ease-out);
}
.slot::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 55%, oklch(0.1 0.01 350 / 0.85));
}
.slot-n {
  position: absolute; inset-block-start: 0.6rem; inset-inline-start: 0.8rem;
  z-index: 1;
  font-family: var(--font-display); font-style: italic;
  font-size: 1.6rem;
  color: oklch(0.82 0.09 80);
  text-shadow: 0 1px 4px oklch(0.1 0.01 350 / 0.6);
}
.slot-t {
  position: absolute; inset-block-end: 1rem; inset-inline: 1rem;
  z-index: 1;
  text-align: left;
  font-family: 'Lato', var(--font-ui);
  font-weight: 900;
  font-size: clamp(1.2rem, 1.8vw, 1.5rem);
  line-height: 1.1;
  letter-spacing: 0.02em;
  color: var(--on-dark);
}
.slot-t em {
  display: block;
  margin-block-start: 0.3rem;
  font-style: normal;
  font-weight: 500;
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: oklch(0.78 0.1 80);
}
/* JS drives each slot's scale by distance from centre; ONLY the centre tile is lit */
.slot:focus-visible, .slot.is-center {
  border-color: oklch(0.72 0.11 80);
  z-index: 1;
}
.slot:focus-visible img, .slot.is-center img { filter: none; }
.slot:focus-visible { outline: 2px solid oklch(0.72 0.11 80); outline-offset: 3px; }
.projects-hint {
  margin: 0;
  padding: 1rem clamp(1.25rem, 4vw, 3rem) 1.6rem;
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: oklch(0.62 0.03 80);
}

/* fullscreen project viewer — fades and rises in via native dialog transitions */
body:has(.project-view[open]) { overflow: hidden; }

.project-view {
  inline-size: 100vw; block-size: 100dvh;
  max-inline-size: none; max-block-size: none;
  margin: 0; padding: 0; border: 0;
  background: var(--dark); color: var(--on-dark);
  overflow-y: auto;
  /* Reserve the scrollbar even before it appears. layoutPhotos() sizes each row to the
     measured width, so a scrollbar arriving afterwards used to steal ~15px and push every
     row into overflow — a 3.67-ratio panorama came out 888px wide in an 874px column and
     broke the rows apart. */
  scrollbar-gutter: stable;
  overscroll-behavior: contain;
  opacity: 0;
  transform: translateY(18px) scale(0.985);
  transition:
    opacity 0.4s var(--ease-out),
    transform 0.4s var(--ease-out),
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete;
}
.project-view[open] { opacity: 1; transform: none; }
@starting-style {
  .project-view[open] { opacity: 0; transform: translateY(18px) scale(0.985); }
}
.project-view::backdrop {
  background: oklch(0.1 0.01 350 / 0);
  transition: background 0.4s var(--ease-out), display 0.4s allow-discrete, overlay 0.4s allow-discrete;
}
.project-view[open]::backdrop { background: oklch(0.1 0.01 350 / 0.8); }
@starting-style {
  .project-view[open]::backdrop { background: oklch(0.1 0.01 350 / 0); }
}
.project-view h3 {
  margin: 0;
  padding: 1.4rem clamp(1.25rem, 4vw, 3rem);
  font-family: 'Lato', var(--font-ui);
  font-weight: 900;
  font-size: clamp(1.4rem, 2.3vw, 1.95rem);
  line-height: 1.1;
}
.project-view h3 em {
  display: block;
  margin-block-start: 0.35rem;
  font-style: normal;
  font-weight: 500;
  font-size: clamp(0.85rem, 1.4vw, 1rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: oklch(0.78 0.1 80);
}
.view-close {
  position: fixed; inset-block-start: 1rem; inset-inline-end: clamp(1.25rem, 4vw, 3rem);
  z-index: 1;
  inline-size: 2.8rem; block-size: 2.8rem;
  border-radius: 50%;
  border: 1px solid oklch(0.82 0.09 80);
  background: oklch(0.16 0.015 350 / 0.85);
  color: oklch(0.82 0.09 80);
  font-size: 1.5rem; line-height: 1;
  cursor: pointer;
}
/* ── Outlet chooser: shown when a brand tile covers several shops ── */

.view-back {
  display: block;
  margin: 0 clamp(1.25rem, 4vw, 3rem) 0.5rem;
  padding: 0.5rem 0.9rem;
  font-family: var(--font-ui);
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--on-dark-dim);
  background: oklch(1 0 0 / 0.06);
  border: 1px solid oklch(1 0 0 / 0.16);
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.25s var(--ease-out), border-color 0.25s var(--ease-out);
}
.view-back:hover { color: var(--on-dark); border-color: oklch(0.72 0.11 80); }
.view-back[hidden] { display: none; }

.view-outlets {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(1rem, 2vw, 1.75rem);
  padding: clamp(1rem, 3vw, 2.5rem) clamp(1.25rem, 4vw, 3rem) clamp(2.5rem, 6vw, 4.5rem);
}
.view-outlets[hidden] { display: none; }
.outlet {
  position: relative;
  inline-size: clamp(13rem, 22vw, 19rem);
  /* matches the thumbnail files, so nothing is cropped */
  aspect-ratio: var(--frame-ar);
  padding: 0;
  border: 1px solid oklch(1 0 0 / 0.16);
  border-radius: 6px;
  overflow: hidden;
  background: oklch(0.16 0.008 350);
  cursor: pointer;
  transition: border-color 0.3s var(--ease-out), transform 0.3s var(--ease-out);
}
.outlet img {
  position: absolute; inset: 0;
  inline-size: 100%; block-size: 100%;
  /* crops nothing — the files are already 3:4 */
  object-fit: cover;
  filter: grayscale(0.6) brightness(0.7);
  transition: filter 0.3s var(--ease-out);
}
.outlet::after {   /* keeps the place name legible whatever the photo underneath */
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 45%, oklch(0.1 0.01 350 / 0.9));
}
.outlet-loc {
  position: absolute; inset-block-end: 0; inset-inline: 0;
  z-index: 1;
  padding: 0.9rem 1rem;
  font-family: var(--font-ui);
  font-size: 0.95rem;
  font-weight: 600;
  text-align: start;
  color: var(--on-dark);
}
.outlet:hover, .outlet:focus-visible { border-color: oklch(0.72 0.11 80); transform: translateY(-3px); }
.outlet:hover img, .outlet:focus-visible img { filter: grayscale(0) brightness(0.92); }
@media (prefers-reduced-motion: reduce) { .outlet { transition: none; } .outlet:hover { transform: none; } }

/* Justified rows, not masonry columns. `columns: 3` was fixed regardless of how many
   photos a project has, so a 2-photo project left a whole column blank, and because
   these sets mix landscape (~1.8) with the odd portrait (~0.5) the columns ended at
   wildly different heights — project 17 measured 412 / 186 / 763px, i.e. most of a
   column standing empty.
   Here each row instead fills the full width: a photo's width is proportional to its own
   aspect ratio (--ar, set from naturalWidth/naturalHeight in app.js once it loads), and
   because flex-grow is proportional too, every photo in a row lands on exactly the same
   height. The box then matches the photo's real shape, so nothing is cropped. */
.view-photos {
  --row-h: clamp(9rem, 14vw, 13rem);   /* target row height; rows flex around it */
  display: flex;
  flex-wrap: wrap;
  /* a final row that can't fill sits centred, rather than left-aligned against a void */
  justify-content: center;
  gap: clamp(0.75rem, 1.5vw, 1.5rem);
  padding: clamp(1.2rem, 4vw, 4rem);
  padding-block-start: 0.5rem;
}
.ph {
  margin: 0;
  /* --w is computed per photo by layoutPhotos() in app.js, which picks the row breaks;
     flexbox on its own breaks rows by flex-basis and can't find the fill that makes a
     row come out level. The basis here is only the pre-JS / no-JS fallback. */
  flex: 0 0 auto;
  inline-size: var(--w, calc(var(--ar, 1.5) * var(--row-h)));
  aspect-ratio: var(--ar, 1.5);
  /* slow independent drift so the wall feels alive; amplitude stays small
     (JS feeds --dx/--dy ≈ ±8px) so photos never fully cover one another */
  animation: ph-drift var(--dur, 12s) var(--delay, 0s) ease-in-out infinite alternate;
  will-change: transform;
  /* the box is correctly shaped before the photo arrives, so give it a tone: an empty
     frame reads as one still loading, an empty hole reads as broken */
  background: oklch(0.19 0.012 350);
  border-radius: 4px;
}
.ph:has(img.zoomed) { position: relative; z-index: 5; }
.view-photos img {
  display: block;
  inline-size: 100%; block-size: 100%;
  object-fit: cover;   /* the box already matches --ar, so this never actually crops */
  border-radius: 4px;
  cursor: zoom-in;
  /* 3D tilt follows the cursor (JS feeds --rx/--ry); click adds --z zoom */
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) scale(var(--z, 1));
  transition: transform 0.25s var(--ease-out);
}
/* a lone photo has no row to justify against — give it a sensible centred maximum */
.ph:only-child {
  flex: 0 1 auto;
  max-inline-size: 52rem;
  margin-inline: auto;
}
.view-photos img.zoomed {
  --z: 1.07;
  cursor: zoom-out;
  position: relative;
  z-index: 1;
  box-shadow: 0 16px 40px oklch(0.05 0.01 350 / 0.6);
}
@keyframes ph-drift {
  from { transform: translate3d(0, 0, 0) rotate(var(--rot, 0deg)); }
  to   { transform: translate3d(var(--dx, 0px), var(--dy, 0px), 0) rotate(calc(var(--rot, 0deg) * -1)); }
}

/* project description — featured beneath the drifting photo wall */
.view-desc {
  max-inline-size: 60ch;
  margin: 0 auto;
  /* trailing space trimmed too — the old 6.5rem tail left a dead band under every project */
  padding: clamp(1.25rem, 3vw, 2.25rem) clamp(1.25rem, 4vw, 3rem) clamp(2.5rem, 5vw, 4rem);
  font-family: 'Zodiak', Georgia, serif;
  /* was clamp(1.45rem, 2.5vw, 2.1rem) — which the large-screen root bump pushed to ~38px,
     so the blurb shouted over the photos. The photos are the point of this view. */
  font-size: clamp(1rem, 1.35vw, 1.25rem);
  line-height: 1.65;
  text-align: center;
  text-wrap: balance;
  color: oklch(0.9 0.02 85);
}
.view-desc p { margin: 0; }
.view-desc p + p { margin-block-start: 1.1em; }
.view-desc p.lead { font-weight: 700; }
.view-desc p.lead + p { margin-block-start: 0.5em; }
.view-desc::before {
  content: "";
  display: block;
  inline-size: 3rem; block-size: 2px;
  margin: 0 auto clamp(1.2rem, 3vw, 2rem);
  background: oklch(0.78 0.1 80);
}
.view-desc[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) { .ph { animation: none; } }
/* Shorter target rows as the viewport narrows, plus a width floor on phones — without
   it a run of portrait photos (basis ∝ aspect ratio) would pack three-up and end tiny. */
@media (max-width: 1100px) { .view-photos { --row-h: clamp(9rem, 21vw, 13rem); } }
@media (max-width: 700px) { .view-photos { --row-h: 12rem; } }

/* ── Services ───────────────────────────────────────────── */

.services {
  background: var(--dark);
  color: var(--on-dark);
  padding: clamp(5rem, 12vh, 8rem) clamp(1.25rem, 4vw, 3rem);
  margin-top: clamp(3rem, 7vh, 5rem);
}
.services-inner { max-inline-size: 70rem; margin-inline: auto; }
.services h2 { max-inline-size: 20ch; }

.service-list {
  list-style: none; margin: clamp(2rem, 5vh, 3.5rem) 0 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(2rem, 4vw, 3.5rem) clamp(2rem, 4vw, 4rem);
}
.service-list li { border-top: 1px solid var(--line-dark); padding-top: 1.4rem; }
.service-list h3 {
  font-family: var(--font-display); font-weight: 400;
  font-size: 1.5rem; margin: 0 0 0.6rem;
}
.service-list p { margin: 0; color: var(--on-dark-dim); font-size: 0.98rem; line-height: 1.7; }

/* ── Process ────────────────────────────────────────────── */

.process { padding: clamp(5rem, 12vh, 9rem) clamp(1.25rem, 4vw, 3rem); }

.process-steps {
  list-style: none; margin: clamp(3rem, 7vh, 5rem) auto 0; padding: 0;
  max-inline-size: 62rem;
  display: grid; gap: clamp(3rem, 8vh, 5.5rem);
}
.process-steps li {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center;
}
.process-steps li:nth-child(even) img { order: 2; }
.process-steps img {
  inline-size: 100%; block-size: clamp(13rem, 30vw, 19rem);
  object-fit: cover; border-radius: 3px;
}
.process-steps h3 {
  display: flex; align-items: baseline; gap: 0.9rem;
  font-family: var(--font-display); font-weight: 400;
  font-size: 1.6rem; margin: 0 0 0.7rem;
}
.process-steps h3 span {
  font-style: italic; font-size: 2.4rem; line-height: 1;
  color: var(--brick);
}
.process-steps p { margin: 0; color: var(--muted); line-height: 1.75; }

/* ── Quote band ─────────────────────────────────────────── */

.quote-band {
  background: var(--brick);
  color: oklch(1 0 0);
  padding: clamp(4.5rem, 11vh, 7.5rem) clamp(1.25rem, 5vw, 3rem);
  text-align: center;
}
.quote-band blockquote { margin: 0 auto; max-inline-size: 52rem; }
.quote-band p {
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(1.5rem, 3.4vw, 2.3rem);
  line-height: 1.32; margin: 0;
}
.quote-band cite {
  display: block; margin-top: 2rem;
  font-style: normal; font-size: 0.82rem; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: oklch(0.9 0.045 355);
}

/* ── Contact ────────────────────────────────────────────── */

.contact { padding: clamp(5rem, 12vh, 9rem) clamp(1.25rem, 4vw, 3rem); }
.contact-inner { max-inline-size: 44rem; margin-inline: auto; text-align: center; }
.contact-inner > p { color: var(--muted); max-inline-size: 48ch; margin: 0 auto 3rem; }

.contact-form { text-align: left; display: grid; gap: 1.4rem; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; }
.contact-form label {
  display: grid; gap: 0.45rem;
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink);
}
.contact-form input, .contact-form textarea {
  font: inherit; font-size: 1rem;
  font-family: var(--font-ui);
  color: var(--ink);
  background: var(--bg);
  border: 1px solid oklch(0.18 0.012 350 / 0.3);
  border-radius: 3px;
  padding: 0.75rem 0.9rem;
  transition: border-color 0.2s var(--ease-out);
}
.contact-form ::placeholder { color: var(--muted); opacity: 1; }
.contact-form input:focus-visible, .contact-form textarea:focus-visible {
  outline: none; border-color: var(--brick);
  box-shadow: 0 0 0 1px var(--brick);
}
.contact-form textarea { resize: vertical; }
.contact-form .btn-primary { justify-self: center; margin-top: 0.6rem; padding-inline: 2.4rem; }
.form-note { min-block-size: 1.4em; margin: 0; text-align: center; color: var(--brick); font-weight: 500; }

/* ── Footer ─────────────────────────────────────────────── */

.footer {
  background: var(--dark);
  color: var(--on-dark);
  padding: clamp(3.5rem, 8vh, 5.5rem) clamp(1.25rem, 4vw, 3rem);
  text-align: center;
}
.footer-inner { display: grid; gap: 1.1rem; }
.footer-brand { margin: 0; }
.footer-lockup { inline-size: clamp(9rem, 16vw, 11.5rem); block-size: auto; margin-inline: auto; }
.footer-line { margin: 0; font-size: 0.85rem; color: var(--on-dark-dim); line-height: 1.8; }
.footer-contact { margin: 0; font-size: 0.9rem; }
.footer-contact a { color: var(--on-dark); text-underline-offset: 0.35em; text-decoration-thickness: 1px; }
.socials {
  display: flex;
  justify-content: center;
  gap: 0.7rem;
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
}
.social {
  display: grid;
  place-items: center;
  inline-size: 2.6rem; block-size: 2.6rem;
  border-radius: 50%;
  border: 1px solid oklch(1 0 0 / 0.22);
  color: var(--on-dark-dim);
  text-decoration: none;
  transition: transform 0.25s var(--ease-out), filter 0.25s var(--ease-out),
              border-color 0.25s var(--ease-out);
}
.social svg { inline-size: 1.1rem; block-size: 1.1rem; }
/* The Xiaohongshu wordmark stands in for a glyph. Three Han characters are denser than
   Latin, so they need their own size and an explicit CJK stack — Archivo carries no Han
   glyphs, and left to fall back the three would render in whatever the OS picks. */
.social-word {
  font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans SC', var(--font-ui), sans-serif;
  font-size: 0.6rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0;
}
/* ── Brand colours ──
   Each circle carries its own platform's colour. Glyphs go white on all four, and the
   borders are dropped — a ring on top of a filled brand colour just reads as a mistake. */
.social--ig, .social--fb, .social--xhs, .social--tt { color: #fff; border-color: transparent; }

/* Instagram has no flat brand colour — the mark is this corner-anchored gradient */
.social--ig {
  background: radial-gradient(circle at 30% 107%,
    #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285aeb 90%);
}
.social--fb { background: #1877f2; }
.social--xhs { background: #ff2442; }
/* TikTok's is black, which would vanish into this footer, so the circle keeps a faint ring
   to hold its edge and the glyph gets the cyan/magenta offset that makes the mark
   recognisable in the first place. */
.social--tt {
  background: #010101;
  border-color: oklch(1 0 0 / 0.32);
}
.social--tt svg {
  filter: drop-shadow(-1.4px -1.4px 0 #25f4ee) drop-shadow(1.4px 1.4px 0 #fe2c55);
}

/* Hover can't recolour these any more — the colour IS the identity — so it lifts and
   brightens instead. */
.social:hover, .social:focus-visible { transform: translateY(-2px); filter: brightness(1.15); }
.social:focus-visible { outline: 2px solid var(--on-dark); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .social:hover, .social:focus-visible { transform: none; } }

.footer-legal { margin: 1.2rem 0 0; font-size: 0.75rem; color: oklch(0.62 0.015 85); }

/* ── Recognition: awards & accreditations ───────────────── */

.awards {
  /* #fbfbfb, not pure white: it's the exact backing baked into the four crops, so they
     sit flush instead of reading as faint grey rectangles. */
  background-color: #fbfbfb;
  /* a 7rem fade out of the footer's dark into that light, so the two sections meet
     without a hard edge — painted as a band, not a full-height gradient, so the
     content below always sits on flat light ground */
  background-image: linear-gradient(180deg, var(--dark) 0, #fbfbfb 100%);
  background-repeat: no-repeat;
  background-size: 100% 7rem;
  color: var(--ink);
  padding: clamp(8.5rem, 15vh, 10rem) clamp(1.25rem, 4vw, 3rem) clamp(3.5rem, 8vh, 5.5rem);
  text-align: center;
}
.awards-title {
  margin: 0 0 clamp(2.25rem, 5vh, 3.25rem);
  font-family: 'Lato', var(--font-ui);
  font-weight: 900;
  font-size: clamp(2rem, 4vw, 3.2rem);
  line-height: 1.05;
  color: var(--ink);
}
.awards-row {
  display: flex;
  /* one unbroken line — the four scale down together rather than wrapping the last
     one onto a row of its own */
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: clamp(1rem, 2.5vw, 2rem);
  /* held to a narrow measure so they sit as one tight cluster rather than drifting
     apart across a wide desktop */
  max-inline-size: 62rem;
  margin-inline: auto;
  padding: 0;
  list-style: none;
}
/* min-inline-size:0 lets a flex item shrink below its content width — without it the
   row would overflow instead of scaling on a narrow desktop */
.awards-row li { display: flex; align-items: center; min-inline-size: 0; }
.awards-row img { inline-size: auto; block-size: auto; max-inline-size: 100%; max-block-size: 8.5rem; }
/* needs to out-specify `.awards-row img` above — as a bare `.awards-cert` it loses
   the cascade and the certificate silently caps at the logo height */
.awards-row img.awards-cert { max-block-size: 14rem; } /* a document, not a mark — needs the height to stay legible */

/* ── Reveal motion (JS-gated enhancement) ───────────────── */

.js .reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out);
  transition-delay: var(--reveal-delay, 0s);
}
.js .reveal.in { opacity: 1; transform: none; }

/* ── Large screens ──────────────────────────────────────────
   Every `clamp()` on this page tops out somewhere between ~1000px and ~1600px of
   viewport, and body copy is a flat 1.0625rem that never scaled at all — so past those
   caps the design freezes while the monitor keeps widening. Measured: going 1280px →
   1920px left the hero title at 74px and the steps column at 896px, dropping the content
   from 70% of the screen to 47%. That's what reads as "everything got smaller".
   Because the layout is expressed in rem almost throughout — clamp minima AND maxima,
   every `max-inline-size`, all the spacing — nudging the root size lifts the whole thing
   in step, rather than chasing dozens of individual values.
   Percentages, not px, so a visitor's own browser font-size preference still applies. */
@media (min-width: 1500px) { html { font-size: 106.25%; } }
@media (min-width: 1800px) { html { font-size: 115%; } }
@media (min-width: 2400px) { html { font-size: 131.25%; } }

/* ── Mobile ─────────────────────────────────────────────── */

@media (max-width: 760px) {
  .nav-toggle { display: flex; }
  .nav-links {
    position: fixed; inset: 0;
    z-index: -1;
    flex-direction: column; justify-content: center;
    gap: 2.2rem;
    background: oklch(0.14 0.012 350 / 0.97);
    opacity: 0; visibility: hidden;
    transition: opacity 0.3s var(--ease-out), visibility 0.3s;
  }
  .nav-open .nav-links { opacity: 1; visibility: visible; }
  /* comfortable thumb targets (≥44px) for the menu, burger and footer links */
  .nav-links a {
    font-size: 1rem;
    display: inline-flex; align-items: center;
    min-block-size: 44px; padding-inline: 1rem;
  }
  .nav-toggle { min-block-size: 44px; align-items: center; }
  .footer-contact a {
    display: inline-block;
    padding-block: 0.6rem;
  }
  .nav.nav-open { background: transparent; box-shadow: none; }
  /* menu overlay is dark, so keep its links light even over a light section */
  .nav.on-light.nav-open .nav-links a { color: var(--on-dark); }

  /* mission: stack images above the text, both always assembled (no slide on touch) */
  .mission { display: flex; flex-direction: column; }
  .mission-stack {
    position: static; inline-size: auto;
    flex-direction: row; gap: 0.8rem;
    padding: 5rem 1.25rem 0;
  }
  .mission-img--portrait, .mission-img--landscape {
    inline-size: 50%; margin: 0;
    transform: none; opacity: 1;
  }
  .mission-text { margin-inline-start: 0; padding: 2rem 1.25rem 3rem; }

  .steps { padding: 3.5rem 1.25rem 3.5rem 3.5rem; }

  /* four abreast only works while there's width for it — on a phone `nowrap` would
     shrink them to illegible stamps, so let them stack instead */
  /* Recognition on a phone: a fixed 2x2 grid rather than letting flex wrap.
     Wrapping went on the marks' intrinsic widths and produced a ragged 2 / 1 / 1 with two
     orphans on their own lines — the four are a portrait certificate, a squarish badge and
     two wide logos, so their natural widths never balance. */
  .awards-row {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.5rem 1rem;
    max-inline-size: 21rem;
  }
  /* One height for every cell, with the mark centred inside it. This is what keeps the two
     rows level: sizing the images instead would stagger them, because a portrait
     certificate and a wide logo can't share a height AND a width. */
  .awards-row li { block-size: 7rem; justify-content: center; }
  /* both selectors needed — the desktop `.awards-row img.awards-cert` rule out-specifies a
     bare `.awards-row img`, so the certificate would otherwise keep its 14rem cap here */
  .awards-row img,
  .awards-row img.awards-cert { max-block-size: 100%; max-inline-size: 100%; }

  /* team: stack panels; tap expands */
  .team-panels { flex-direction: column; block-size: auto; }
  .member-info, .member:last-child .member-info {
    inset-inline: 0; inline-size: auto; text-align: left;
  }
  /* stacked, so it's the HEIGHT that changes here, not flex — and `flex` was the only
     thing the base rule transitioned, so the panel snapped open instantly */
  .member {
    min-block-size: 22rem;
    transition: min-block-size 0.55s var(--ease-out);
  }
  .member:hover, .member.open, .member:focus-visible { flex: 1; }
  .member.open { min-block-size: 34rem; }
  /* Same reason as the base rule, but here it's the HEIGHT that animates, so the fixed
     dimension has to be the height — pinned to the open size and anchored to the top so
     the face stays put and the panel reveals downward. block-size:100% would rescale the
     photo on every frame of the 22rem -> 34rem move, which is the expensive case:
     object-fit:cover takes its scale from the height here. */
  .member img {
    inline-size: 100%; block-size: 34rem;
    inset-block: 0 auto; inset-inline-start: 0; margin-inline-start: 0;
  }
  .member.open .member-bio, .member.open .member-stat { opacity: 1; translate: 0 0; }
  .member.open::after {
    background: linear-gradient(180deg, transparent 8%, oklch(0.1 0.01 350 / 0.9) 42%);
  }

  /* Phone-sized tiles; the swipe-strip rules themselves live in their own query below,
     because they now cover tablets too.
     Override the HEIGHT, never the width — width is derived as `--tile-h * --frame-ar` and
     setting it directly desynchronises the card from the artwork, which starts cropping the
     signage again. This rule used to be a flat `inline-size: 17rem`, which did exactly that
     and silently pinned every tile to 272px. */
  .reel { --tile-h: 32rem; }

  .process-steps li { grid-template-columns: 1fr; }
  .process-steps li:nth-child(even) img { order: 0; }

  .form-row { grid-template-columns: 1fr; }
}

/* ── Projects reel: swipe strip instead of the pinned scroll-driven slide ──
   Anything touch-driven browses projects by swiping sideways rather than by scrolling the
   page vertically — that's what a finger expects, and on an iPad the pinned version made
   you scroll down to move something sideways.
   Width alone can't decide this: an iPad Pro in landscape is 1366px, the same as plenty of
   laptops. So it's width OR a coarse pointer, which catches every iPad in any orientation
   while leaving a mouse-driven desktop on the pinned cinematic version.
   The JS `pinnable` query in app.js is the exact complement of this — keep them in step. */
/* The projects reel is a swipe strip on EVERY device (2026-08-18). This was wrapped in
   `@media (max-width: 1100px), (pointer: coarse)`, with wide mouse-driven screens instead
   getting a pinned section where vertical scroll drove the strip sideways. That pinning is
   gone: it made the page 19,281px tall, so the self-scrolling tour took 214s to cross it and
   read as not moving at all. Its JS counterpart is `DESKTOP`/`DESKTOP_MOTION` in app.js,
   which are now `not all` — change both together. */
.projects-pin { block-size: auto; }
.reel {
  inline-size: auto;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* the centred tile scales to 1.12, so it needs ~6% of its own height as headroom
     top AND bottom — `overflow-x:auto` also forces overflow-y to a clipping value,
     so anything past this padding gets sliced off the tile instead of overflowing */
  padding-block: 2.25rem 2.5rem;
}
.slot { scroll-snap-align: center; }
/* the counter was driven by the pinned scroll handler, which no longer runs */
.projects-count { display: none; }

/* ── Reduced motion ─────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .hero-video { animation: none; }
  .mission-img { transition: none; transform: none; opacity: 1; }
  .mission-img img { transition: none; }
  .step::before, .step::after { transition: none; }
  .member, .member img, .member-stat, .member-bio { transition: none; }
  /* projects reel: JS skips pinning; give the strip native horizontal scroll */
  .projects-pin { block-size: auto; }
  .reel { inline-size: auto; overflow-x: auto; padding-block: 1rem 2rem; }
  .slot { transition: none; transform: none !important; }
  .project-view, .project-view::backdrop { transition: none; }
  .project-view { opacity: 1; transform: none; }
  .js .reveal { opacity: 1; transform: none; transition: none; }
  .nav, .nav-links, .nav-cta, .btn-primary { transition: none; }
}

