/* ============================================================
   Chris — Creative Director Portfolio
   Editorial / cinematic. One stylesheet, organized by section.
   ============================================================ */

/* -------------------- 1. Tokens -------------------- */
:root {
  --bg-primary:    #FAFAF7;
  --bg-secondary:  #F0EFEB;
  --bg-deep:       #1B1A17;
  --text-primary:  #1A1A1A;
  --text-secondary:#6B6B6B;
  --text-invert:   #EFEAE0;
  --accent:        #B8553A;   /* muted terracotta — used sparingly */
  --divider:       #E0DED8;

  --font-display:  'Instrument Serif', 'Times New Roman', serif;
  --font-body:     'DM Sans', system-ui, -apple-system, sans-serif;

  --maxw:          1200px;
  --gutter:        clamp(20px, 4vw, 56px);
  --section-pad:   clamp(96px, 14vw, 200px);

  --ease-out:      cubic-bezier(0.22, 1, 0.36, 1);
}

/* -------------------- 2. Reset -------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

img, video { max-width: 100%; display: block; }
[hidden] { display: none !important; }
button { font: inherit; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; padding: 0; margin: 0; }

::selection {
  background: var(--accent);
  color: var(--bg-primary);
}

/* -------------------- 3. Base -------------------- */
body {
  font-family: var(--font-body);
  background: var(--bg-primary);
  color: var(--text-primary);
  font-size: clamp(16px, 1.05vw, 19px);
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
  opacity: 0;
  transition: opacity 0.5s ease;
}
body.is-loaded {
  opacity: 1;
}

main { display: block; }

/* Reveal baseline — JS will animate to visible */
[data-reveal] {
  opacity: 0;
  transform: translateY(28px);
  will-change: opacity, transform;
}
.no-js [data-reveal],
html.reduced-motion [data-reveal] {
  opacity: 1;
  transform: none;
}

/* -------------------- 4. Site nav (hidden until end of hero) -------------------- */
.site-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 22px var(--gutter);
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-primary);
  pointer-events: none;
  opacity: 0;                 /* hidden during hero — JS fades in */
  mix-blend-mode: difference;
  color: #FAFAF7;
}

/* Once revealed by JS, restore pointer events */
.site-nav.is-visible { pointer-events: auto; }

.site-nav a { pointer-events: auto; }

.site-nav__mark {
  font-family: var(--font-display);
  font-size: 22px;
  letter-spacing: 0;
  text-transform: none;
  font-style: italic;
}

.site-nav__links {
  display: flex;
  gap: clamp(18px, 2.4vw, 36px);
}

.site-nav__links a {
  position: relative;
  transition: opacity 0.25s ease;
}
.site-nav__links a:hover { opacity: 0.6; }

/* -------------------- 5. Hero -------------------- */
/* The .hero section is a *scroll container* only — it provides 400vh of
   scroll distance for the GSAP timeline. The visible stage is fixed to
   the viewport (not pinned via spacer), so the moment the user scrolls
   past .hero, the stage hides and the next section appears immediately
   with no trailing "scroll-out" frame. */
.hero {
  position: relative;
  width: 100%;
  height: 400vh;
  background: var(--bg-primary);
}

.hero__stage {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  z-index: 1;
  background: var(--bg-primary);
  /* No transition by default — re-entering the hero (scrolling back up)
     restores the stage instantly with no fade-in. */
}

.hero.is-past .hero__stage {
  opacity: 0;
  pointer-events: none;
  /* Transition is declared on the destination state only, so it applies
     when going TO is-past (fade out → reveals work below) but NOT when
     leaving is-past (snaps back instantly). */
  transition: opacity 0.7s var(--ease-out);
}

/* Real video (when uncommented) */
.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 1;
}

/* Placeholder shown until the video file is dropped in */
.hero__placeholder {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Warm off-white with a subtle vignette to suggest a captured frame */
  background:
    radial-gradient(120% 90% at 50% 45%, #FCFAF3 0%, #F4F0E5 60%, #E6DFD0 100%);
  color: var(--text-primary);
}

/* Hand-drawn-feeling rectangle — represents the finger frame */
.hero__frame-svg {
  position: absolute;
  /* Scaled to roughly the proportion of the eventual zoomed-in frame */
  width: min(72vw, 980px);
  height: min(46vw, 580px);
  color: var(--text-primary);
  opacity: 0.85;
  /* Slight rotation gives it a more held-by-hand quality */
  transform: rotate(-0.4deg);
  filter: drop-shadow(0 0 0.5px rgba(0,0,0,0.12));
}

/* Film grain — pure CSS via SVG noise data URI */
.hero__grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.55;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.18 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size: 220px 220px;
}

.hero__placeholder-tag {
  position: absolute;
  bottom: 28px;
  right: 32px;
  font-size: 10px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--text-secondary);
  opacity: 0.6;
}

/* HTML text overlays — two "stages" both centered in the viewport.
   Intro stage holds name + tag together; mission stage holds the
   single mission line on its own. Both occupy the full hero, so each
   is dead-center when its lines are visible. */
.hero__overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.hero__stage-text {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(20px, 4vh, 60px);
  text-align: center;
  padding: 0 var(--gutter);
}

.hero__line {
  margin: 0;
  color: var(--text-primary);
  opacity: 0;                  /* JS animates in/out on scroll */
  will-change: opacity, transform;
  max-width: min(900px, 92vw);
}

.hero__line--name {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(48px, 7vw, 110px);
  line-height: 1;
  letter-spacing: -0.015em;
  font-style: italic;
}

.hero__line--tag {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(13px, 1.1vw, 17px);
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--text-primary);
}

.hero__line--mission {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(44px, 6.5vw, 104px);
  line-height: 1.05;
  letter-spacing: -0.012em;
  color: var(--text-primary);
  max-width: 18ch;
  text-wrap: balance;
}
.hero__line--mission em {
  font-style: italic;
  color: var(--accent);
}

/* No-JS / reduced-motion: show overlay text immediately */
.no-js .hero__line,
html.reduced-motion .hero__line { opacity: 1; }

.hero__scroll-hint {
  position: absolute;
  bottom: clamp(28px, 4vh, 56px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  pointer-events: none;
  will-change: opacity, transform;
  /* Stay legible against any video frame */
  color: #FAFAF7;
  text-shadow: 0 1px 18px rgba(0,0,0,0.45);
  filter: drop-shadow(0 1px 8px rgba(0,0,0,0.35));
}

.hero__scroll-hint-label {
  font-family: var(--font-body);
  font-size: 11px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  font-weight: 500;
}

.hero__scroll-hint-chev {
  width: 22px;
  height: 12px;
  display: block;
  animation: scrollChev 1.8s var(--ease-out) infinite;
}

@keyframes scrollChev {
  0%   { transform: translateY(-4px); opacity: 0.4; }
  40%  { transform: translateY(0px);  opacity: 1; }
  70%  { transform: translateY(6px);  opacity: 0.6; }
  100% { transform: translateY(10px); opacity: 0; }
}

/* -------------------- 6. Section primitives -------------------- */
.section-eyebrow {
  display: inline-block;
  font-size: 12px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: 24px;
}

.section-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(40px, 6vw, 88px);
  line-height: 1.02;
  letter-spacing: -0.015em;
  margin: 0 0 0.4em;
  max-width: 18ch;
}

/* Scroll offset so fixed nav doesn't overlap anchor targets.
   #work is excluded — it sits at the hero exit boundary, and
   an 80px offset would land inside the hero's scroll zone. */
#about, #contact {
  scroll-margin-top: 80px;
}

/* -------------------- 7. Selected Work -------------------- */
.work {
  padding: var(--section-pad) 0 calc(var(--section-pad) * 0.6);
  background: var(--bg-primary);
}

.work__header {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
  margin-bottom: clamp(60px, 9vw, 140px);
}

/* Client grouping — a chapter divider naming the brand and the role.
   Every project inside one .client-group shares a client; future
   clients get their own group with the same header pattern. */
.client-group__header {
  max-width: var(--maxw);
  margin: 0 auto clamp(60px, 8vw, 110px);
  padding: 24px var(--gutter) 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px 24px;
  position: relative;
}
/* Hairline sits inside the gutters, not edge to edge. */
.client-group__header::before {
  content: "";
  position: absolute;
  top: 0;
  left: var(--gutter);
  right: var(--gutter);
  height: 1px;
  background: var(--divider);
}
.client-group__client {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(30px, 3.4vw, 48px);
  line-height: 1;
  margin: 0;
  color: var(--text-primary);
}
.client-group__role {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0;
}

.project { margin-bottom: clamp(80px, 14vw, 200px); }
.project:last-of-type { margin-bottom: 0; }

/* Layout wrappers for split / quiet projects (cards are no longer
   wrapped in a single anchor — only the video and the Learn More
   button are interactive). */
.project__layout {
  color: inherit;
}

.project__media {
  position: relative;
  overflow: hidden;
  background: var(--bg-secondary);
  transition: transform 0.4s ease, box-shadow 0.4s ease;
}
.project__media:hover,
.project__media:focus-visible {
  transform: translateY(-10px) scale(1.04);
  box-shadow:
    0 22px 48px rgba(0, 0, 0, 0.20),
    0 6px 16px rgba(0, 0, 0, 0.12);
}
.project__video {
  transition: transform 0.5s ease;
}
.project__media:hover .project__video,
.project__media:focus-visible .project__video {
  transform: scale(1.04);
}

/* Video media — fills the container exactly like a placeholder/image,
   matches the same hover scale, same overflow clipping. */
.project__media--video { background: #0E0E0C; }

.project__media--video.project__media--bleed { aspect-ratio: 2 / 1; }
.project__media--video.project__media--split { aspect-ratio: 4 / 5; }
.project__media--video.project__media--quiet { aspect-ratio: 1 / 1; }

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

/* Clickable video media — opens full version in new tab */
.project__media--clickable {
  display: block;
  cursor: pointer;
  color: inherit;
}

/* "Watch with sound" CTA badge — sits over the video, top-right */
.project__media-cta {
  position: absolute;
  top: clamp(16px, 2vw, 28px);
  right: clamp(16px, 2vw, 28px);
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  background: rgba(20, 20, 18, 0.62);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: #FAFAF7;
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  font-weight: 500;
  border-radius: 999px;
  border: 1px solid rgba(250, 250, 247, 0.18);
  z-index: 2;
  pointer-events: none;       /* click goes to the parent anchor */
  transition: background 0.3s ease, transform 0.3s var(--ease-out);
}

.project__media-cta-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--accent);
  color: #FAFAF7;
}

.project__media--clickable:hover .project__media-cta {
  background: rgba(20, 20, 18, 0.82);
  transform: translateY(-2px);
}


/* --- Project 2 card: scroll-scrubbed rebrand plate ---
   GSAP scrubs the background from a pale tint of the Prostasis steel
   to the Tonum beige while the wordmarks crossfade. Nothing here is
   clickable — the card's only CTA is Learn More — and the plate is
   4:3, not 4:5, so the horizontal lockups aren't floating in a tall
   box. Mark widths differ (58% vs 72%) because the two logos' aspect
   ratios differ (3.43:1 vs 5.23:1): equal *optical* weight, not equal
   pixel width. */
.brandshift {
  position: relative;
  aspect-ratio: 4 / 3;
  width: 100%;
  /* Start state — 30% of sampled Prostasis ink #6F7880 over paper.
     GSAP owns this; the static value is the pre-scroll state. */
  background: #C9CBCC;
}
.brandshift::after {
  content: "";
  position: absolute;
  inset: 18px;
  /* Fixed ink instead of currentColor: the plate colour animates
     underneath, and the hairline must hold on both surfaces. */
  border: 1px solid rgba(42, 38, 32, 0.18);
  pointer-events: none;
}
.brandshift__mark {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: auto;
  display: block;
}
.brandshift__mark--before { width: 58%; }
.brandshift__mark--after  { width: 72%; opacity: 0; }

/* Script-less fallback: show the current brand, not the dead one. */
.no-js .brandshift { background: #E5DCC9; }
.no-js .brandshift__mark--before { opacity: 0; }
.no-js .brandshift__mark--after  { opacity: 1; }

.project__caption {
  display: flex;
  flex-direction: column;
}

.project__meta {
  display: flex;
  align-items: baseline;
  gap: 18px;
  margin-bottom: 14px;
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.project__num {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 18px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--accent);
}

.project__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(32px, 4.6vw, 72px);
  line-height: 1.05;
  letter-spacing: -0.012em;
  margin: 0 0 16px;
  transition: color 0.4s ease;
}


.project__desc {
  font-size: clamp(16px, 1.2vw, 19px);
  line-height: 1.55;
  color: var(--text-primary);
  max-width: 44ch;
  margin: 0;
}

.project__pull {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(20px, 2vw, 28px);
  line-height: 1.3;
  color: var(--text-primary);
  border-top: 1px solid var(--divider);
  padding-top: 18px;
  margin-top: 28px;
  max-width: 28ch;
}

/* --- Learn More toggle --- */
.project__toggle {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin-top: clamp(24px, 3vw, 36px);
  display: inline-flex;
  align-items: center;
  gap: 14px;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-primary);
  transition: color 0.3s ease;
}
.project__toggle:hover { color: var(--accent); }

.project__toggle-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 1px solid currentColor;
  border-radius: 50%;
  transition: transform 0.45s var(--ease-out), background 0.3s ease, color 0.3s ease;
}
.project__toggle:hover .project__toggle-icon {
  background: var(--accent);
  color: #FAFAF7;
  border-color: var(--accent);
}
.project__toggle[aria-expanded="true"] .project__toggle-icon {
  transform: rotate(45deg);
}

/* --- Case study panel --- */
.case-study {
  height: 0;
  overflow: hidden;
  opacity: 0;
  background: var(--bg-secondary);
  margin-top: clamp(40px, 6vw, 80px);
  /* Break full-bleed-style background out of the page max-width.
     The inner content respects the gutter. */
  position: relative;
}

.case-study__inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: clamp(60px, 9vw, 120px) var(--gutter) clamp(60px, 9vw, 120px);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: clamp(16px, 2vw, 32px);
  row-gap: clamp(40px, 6vw, 80px);
  align-items: start;
}

.case-study__label {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 18px;
}

.case-study__brief,
.case-study__insight {
  font-family: var(--font-display);
  font-size: clamp(22px, 2.2vw, 32px);
  line-height: 1.3;
  color: var(--text-primary);
}
.case-study__brief p,
.case-study__insight p { margin: 0; max-width: 28ch; }

/* Floating image placeholders — same visual language as project__placeholder.
   Editorial base offset and hover delta both feed into a single transform
   via CSS variables, so a base translateY(-30px) plus a hover translateY(-10px)
   compose smoothly into translateY(-40px) scale(1.04) without snapping. */
.case-study__img {
  --offset-y: 0px;          /* editorial base nudge per variant */
  --hover-y: 0px;           /* hover delta — lifts further from base */
  --hover-s: 1;             /* hover scale */
  background: var(--p-bg, var(--bg-secondary));
  color: var(--p-fg, var(--text-primary));
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  padding: clamp(18px, 2vw, 28px);
  position: relative;
  font-family: var(--font-body);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  transform: translateY(calc(var(--offset-y) + var(--hover-y))) scale(var(--hover-s));
  transition: transform 0.4s ease, box-shadow 0.4s ease;
  cursor: default;
}
/* Universal hover for any case-study tile — lifts the whole container,
   zooms it slightly, and drops a soft layered shadow. */
.case-study__img:hover,
.case-study__img:focus-visible {
  --hover-y: -10px;
  --hover-s: 1.04;
  box-shadow:
    0 22px 48px rgba(0, 0, 0, 0.20),
    0 6px 16px rgba(0, 0, 0, 0.12);
}
.case-study__img::after {
  content: "";
  position: absolute;
  inset: 14px;
  border: 1px solid currentColor;
  opacity: 0.18;
  pointer-events: none;
  transition: border-color 0.3s ease, opacity 0.3s ease;
}
.case-study__img:hover::after,
.case-study__img:focus-visible::after {
  opacity: 0.45;
}
/* NOTE: a `.case-study__img span` rule used to live here, styling the
   bare <span> labels inside the old colour-block placeholder tiles
   ("Hero Print Spread" etc). Those tiles are all gone, but the rule
   outlived them — and at (0,1,1) it out-specified every component
   class, silently forcing `position: relative` and `opacity: 0.85`
   onto every span inside a tile: the crossfade image wrappers (which
   then collapsed to 0x0 and hid the bottles), their captions, both
   "Read brand book" / "Watch fullscreen" badges, and the carousel's
   visually-hidden live region. Do not reintroduce a bare type
   selector scoped to this tile — style the component class instead. */

/* Variant for tiles backed by a real image instead of a color block.
   The img fills the tile edge-to-edge; the inset border still rides
   on top for the same editorial framed-plate feel. */
.case-study__img--has-image {
  padding: 0;
  background: #0E0E0C;
  overflow: hidden;
  cursor: zoom-in;
}
.case-study__img--has-image img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.case-study__img--has-image::after {
  border-color: rgba(250, 250, 247, 0.28);
  z-index: 1;
}
.case-study__img--has-image:hover::after,
.case-study__img--has-image:focus-visible::after {
  border-color: rgba(250, 250, 247, 0.55);
}
/* Tiny "expand" affordance — fades in on hover, no extra HTML needed. */
.case-study__img--has-image::before {
  content: "";
  position: absolute;
  top: 14px;
  right: 14px;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: rgba(250, 250, 247, 0.92);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'><path d='M7 1V13M1 7H13' fill='none' stroke='%231A1A1A' stroke-width='1.4' stroke-linecap='round'/></svg>");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 12px;
  opacity: 0;
  transform: scale(0.85);
  transition: opacity 0.3s ease, transform 0.3s var(--ease-out);
  z-index: 2;
  pointer-events: none;
}
.case-study__img--has-image:hover::before,
.case-study__img--has-image:focus-visible::before {
  opacity: 1;
  transform: scale(1);
}

/* ----- Lightbox overlay ----- */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(12, 12, 10, 0.94);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(40px, 6vh, 80px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s var(--ease-out), visibility 0s linear 0.35s;
}
.lightbox.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.35s var(--ease-out), visibility 0s linear 0s;
}
.lightbox__img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
  transform: scale(0.97);
  transition: transform 0.4s var(--ease-out);
}
.lightbox.is-open .lightbox__img { transform: scale(1); }

.lightbox__close {
  position: absolute;
  top: clamp(20px, 3vh, 32px);
  right: clamp(20px, 3vw, 32px);
  width: 44px;
  height: 44px;
  border: 1px solid rgba(250, 250, 247, 0.35);
  border-radius: 50%;
  background: rgba(250, 250, 247, 0.04);
  color: #FAFAF7;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.25s ease, border-color 0.25s ease;
}
.lightbox__close:hover {
  background: rgba(250, 250, 247, 0.16);
  border-color: rgba(250, 250, 247, 0.7);
}

/* Per-position overrides: when a real image lives in a slot, switch
   the tile to 16:9 and clear the editorial offset. (0,2,0) specificity.
   Variant-level rules use (0,3,0) to outrank this — see variant B. */
.case-study__img--a.case-study__img--has-image,
.case-study__img--b.case-study__img--has-image,
.case-study__img--c.case-study__img--has-image,
.case-study__img--d.case-study__img--has-image {
  aspect-ratio: 16 / 9;
  grid-row: auto;
  align-self: start;
}

/* --- Video tile (inline looping cut inside a case study) ---
   Same framed-plate treatment as --has-image, minus the lightbox
   affordances: there is nothing to open, so no zoom-in cursor and no
   "+" badge. The fill rules come from .project__video, which also puts
   the tile in scope of the existing play-when-visible observer. */
.case-study__img--has-video {
  padding: 0;
  background: #0E0E0C;
  overflow: hidden;
}
.case-study__img--has-video::after {
  border-color: rgba(250, 250, 247, 0.28);
  z-index: 1;
}
.case-study__img--has-video:hover::after,
.case-study__img--has-video:focus-visible::after {
  border-color: rgba(250, 250, 247, 0.55);
}

/* Full-cover click target for fullscreen playback. Sits above the video
   but below the tile's inset border, and reuses the same CTA badge as
   the project cards so the affordance reads the same site-wide. */
.case-study__fs {
  position: absolute;
  inset: 0;
  z-index: 2;
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  color: inherit;
}
.case-study__fs:hover .project__media-cta,
.case-study__fs:focus-visible .project__media-cta {
  background: rgba(20, 20, 18, 0.82);
  transform: translateY(-2px);
}

/* Slot A in case study C is a 3:4 portrait plate, which would crop a
   16:9 cut down to roughly 40% of its width. Same convention as
   --has-image: real media takes its own ratio. The three-class selector
   is needed to outrank `.case-study--c .case-study__img--a`, which is
   equally specific but declared later. align-self keeps the tile from
   stretching to fill slot A's two-row span, which would otherwise
   override the aspect ratio.

   Widened 4 -> 7 columns (~1.8x wider, ~3.3x the area). Seven is the
   ceiling that still leaves the brief its five columns; at four the
   brief's 32px display serif wraps too tight to read. */
.case-study--c .case-study__img--a.case-study__img--has-video {
  grid-column: 6 / span 7;
  /* Row 1 only. The base --a spans rows 1–2, which reserved the space
     beside the carousel even though align-self:start meant the video
     never drew into it — that reservation is what kept the insight
     from sitting there. */
  grid-row: 1;
  aspect-ratio: 16 / 9;
  align-self: start;
}
/* Brief keeps its width and gives up its one-column indent instead, so
   the extra room comes from the margin rather than the text. */
.case-study--c:has(.case-study__img--has-video) .case-study__brief {
  grid-column: 1 / span 5;
}

/* --- Launch carousel tile (fanned flip-through + view-all row) ---
   The tile is a bare stage, not a framed plate: no background, no inset
   border, no padding — the cards float on the panel background the way
   the drink-mix bottles float in their section. All card transforms
   belong to GSAP; CSS only sizes and decorates. */
.case-study__img--launch {
  --card-w: clamp(190px, 24vw, 264px);
  --offset-y: 0px;          /* real media drops the editorial nudge */
  display: block;
  padding: 0;
  background: none;
}
.case-study__img--launch::after { content: none; }
/* Excluded from the universal tile hover — the affordance here is the
   front card's own lift, and a whole-tile zoom under a card fan would
   double the motion. */
.case-study__img--launch:hover,
.case-study__img--launch:focus-visible {
  --hover-y: 0px;
  --hover-s: 1;
  box-shadow: none;
}

/* Slot B in variant C is a 3-col square plate nudged 20px down; the
   carousel takes the full free width beside the Park Vegas video
   (cols 1–5, the video holds 6–12) and sizes to its content instead of
   a forced ratio. The 264px cards still fan inside a 437px stage —
   deepest tier offset is ±88px, ~2px of spill. Three classes to
   outrank the variant-C slot rule, which also carries the 20px
   --offset-y this zeroes. */
.case-study--c .case-study__img--b.case-study__img--launch {
  grid-column: 1 / span 5;
  aspect-ratio: auto;
  align-self: start;
  --offset-y: 0px;
  /* Vertical nudge knob. Historically this had to be `top` because the
     panel reveal left an inline transform that killed --offset-y; the
     reveal now clears its transforms (see expandPanel), so either would
     work — `top` stays because it's the knob Chris already tunes, and
     it shifts the tile without reflowing the grid either way. */
  top: 40px;
}

.launch__stage {
  position: relative;
  /* 4:5 card + 24px top inset + shadow breathing below. Height lives in
     CSS (not JS) so the panel's expand-height measurement is right even
     while the case study is still collapsed. */
  height: calc(var(--card-w) * 1.25 + 48px);
  touch-action: pan-y;      /* horizontal swipes are ours; page keeps vertical */
}
.launch__stage.is-fan { cursor: pointer; }
.launch__stage:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}
/* Row mode turns the stage into a horizontal strip. Absolutely
   positioned cards extend a scroller's overflow, so no inner spacer is
   needed. */
.launch__stage.is-row {
  overflow-x: auto;
  cursor: default;
  scrollbar-width: thin;
  /* The fan's pan-y would block native horizontal strip scrolling. */
  touch-action: auto;
}

.launch__card {
  position: absolute;
  top: 24px;
  left: 0;
  width: var(--card-w);
  aspect-ratio: 4 / 5;
  /* Low pivot so fan rotation splays the tops outward, like a hand of
     cards held from below. */
  transform-origin: 50% 85%;
  will-change: transform;
}
/* The img is the card face: it carries the shadow and the front-card
   hover lift, so GSAP (which owns the card div's transform) and the CSS
   hover never write to the same element. */
.launch__card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  box-shadow:
    0 10px 28px rgba(0, 0, 0, 0.14),
    0 2px 8px rgba(0, 0, 0, 0.08);
  transition: transform 0.35s var(--ease-out), box-shadow 0.35s ease;
}
.launch__stage.is-fan .launch__card.is-front:hover img {
  transform: translateY(-5px);
  box-shadow:
    0 18px 38px rgba(0, 0, 0, 0.18),
    0 4px 12px rgba(0, 0, 0, 0.10);
}

/* Instagram-style dots. The 6px dot rides inside a 16px button so the
   tap target isn't dot-sized. */
.launch__dots {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-top: 18px;
}
.launch__dot {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  width: 16px;
  height: 16px;
  display: grid;
  place-items: center;
  cursor: pointer;
}
.launch__dot::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: 1px solid var(--text-secondary);
  transition: background 0.25s ease, border-color 0.25s ease, transform 0.25s ease;
}
.launch__dot:hover::before { border-color: var(--text-primary); }
.launch__dot.is-active::before {
  background: var(--text-primary);
  border-color: var(--text-primary);
  transform: scale(1.15);
}

/* Quiet, subordinate to the flip interaction. */
.launch__viewall {
  display: block;
  margin: 12px auto 0;
  appearance: none;
  background: none;
  border: 0;
  padding: 4px 6px;
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color 0.25s ease;
}
.launch__viewall:hover { color: var(--accent); }

/* --- Document tile (e.g. clickable PDF cover) --- */
.case-study__img--doc {
  display: block;
  background: #0E0E0C;
  overflow: hidden;
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  padding: 0;
}
.case-study__img--doc img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.case-study__img--doc::after {
  border-color: rgba(250, 250, 247, 0.28);
  z-index: 1;
}
.case-study__img--doc:hover::after,
.case-study__img--doc:focus-visible::after {
  border-color: rgba(250, 250, 247, 0.55);
}

/* --- Scroll-driven product crossfade (.xfade) ---
   Two stacked bottles; GSAP scrubs their opacity in opposition as the
   tile travels the viewport, reversible on scroll-up. Each caption +
   tile pair is wrapped in a .case-study__img--xfade-unit so they move
   as one grid child with a small internal gap. The .xfade container
   inside carries role="img"; the images are decorative. */

/* Wrapper: caption above tile, bound as one unit. No plate chrome —
   the wrapper is invisible; the tile inside carries its own border. */
.case-study__img--xfade-unit {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
  padding: 0;
  background: none;
  aspect-ratio: auto;
  cursor: default;
}
.case-study__img--xfade-unit::after { content: none; }
.case-study__img--xfade-unit:hover,
.case-study__img--xfade-unit:focus-visible {
  --hover-y: 0px;
  --hover-s: 1;
  box-shadow: none;
}

/* The tile inside the wrapper — light plate, dark inset border. */
.xfade__tile {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--bg-primary);
  border-radius: 0;
}
.xfade__tile::after {
  content: "";
  position: absolute;
  inset: 14px;
  border: 1px solid rgba(26, 26, 26, 0.16);
  pointer-events: none;
  transition: opacity 0.3s ease;
}

.xfade__imgs {
  position: absolute;
  inset: 0;
  display: block;
}
.xfade__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 4px clamp(14px, 2vw, 22px) clamp(14px, 2vw, 22px);
  box-sizing: border-box;
  object-fit: contain;
  display: block;
}
.xfade__img--after { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .xfade__img--before { opacity: 0; }
  .xfade__img--after  { opacity: 1; }
}
.no-js .xfade__img--before { opacity: 0; }
.no-js .xfade__img--after  { opacity: 1; }

/* Caption: letterspaced caps naming the change, display italic
   glossing it. Stacked so height is predictable at every width. */
.xfade__caption {
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: left;
}
.xfade__caption-pair {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
/* .about__credits em treatment: 17px Instrument Serif italic. */
.xfade__caption-gloss {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 17px;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text-primary);
}

/* Standalone section subheads inside a case study ("The Line",
   "The System") — label type over a hairline, spanning the grid. */
.case-study__subhead {
  border-top: 1px solid var(--divider);
  padding-top: 18px;
  margin: 0;
  align-self: start;
}

/* Brand-book specimen plates: exported PDF pages at 1999x1545, so the
   tile takes that exact ratio and cover crops nothing. */
.case-study__img--plate {
  aspect-ratio: 1999 / 1545;
}

/* --- Wordmark chronology (project 2's hero moment) ---
   Unframed on purpose: no plate, no inset border, no hover, nothing
   clickable. Both marks float on the panel background and hierarchy
   does the explaining — the past is small, grayscale, at 0.3 opacity;
   the present is ~2.7x wider at full ink, dropped below and to the
   right so the pair reads upper-left → lower-right as a timeline.
   Normal flow throughout (no absolute positioning), so height comes
   from content + padding and the panel's expand measurement is right.
   The two logos' mismatched aspect ratios stopped mattering the moment
   they left the shared contain box — each is sized by width alone. */
/* The two marks are a tight chronological pair, so the gap between
   them is fixed rather than distributed. The block is top-aligned
   (align-self: start in variant B) so the Prostasis mark's top edge
   sits level with the Brief's first line. */
.case-study__wordmarks {
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(14px, 2vw, 28px);
}
.case-study__wordmark img {
  width: 100%;
  height: auto;
  display: block;
}
/* Both marks flush to the same left axis. That implied vertical line
   is what binds them into one sequence — it does the work the diagonal
   used to do, now that the column is too narrow for a diagonal. */
.case-study__wordmark--past {
  width: clamp(120px, 28%, 185px);
  margin-left: 0;
}
/* 0.22, not 0.3: scaling the past mark up recovered visual weight it
   is not supposed to have, so the opacity signal deepens to compensate
   rather than shrinking it back down. */
.case-study__wordmark--past img {
  opacity: 0.22;
  filter: grayscale(1);
}
/* ~3.3x the width of the past mark. Pushed past the diagonal layout's
   2.7x because the marks' aspect ratios differ (5.23:1 vs 3.43:1), so
   a width ratio yields a much softer height ratio — 3.3x wide reads as
   only ~2.2x tall. */
/* 88% leaves ~12% of the track clear at every breakpoint, so the
   lockup never runs to the column edge. The 520px cap is a backstop
   that only engages if the track ever grows past ~590px. */
.case-study__wordmark--now {
  width: clamp(340px, 88%, 520px);
}
/* Roughly half the inter-mark gap at every breakpoint, so each date
   stays visibly bound to the mark above it once the marks tighten. */
.case-study__wordmark-date {
  margin: clamp(8px, 1vw, 12px) 0 0;
}

/* Editorial translateY offsets are dropped when a tile carries a real
   image — the nudges were a "feels hand-placed" cue for the colour
   plate, but on a real photo they just read as misaligned. */
.case-study__img--b.case-study__img--has-image,
.case-study__img--d.case-study__img--has-image {
  --offset-y: 0px;
}

/* Default asymmetric layout — variants below tweak it per project */
.case-study__brief    { grid-column: 1 / span 5;  grid-row: 1; }
.case-study__img--a   { grid-column: 7 / span 6;  grid-row: 1 / span 2; aspect-ratio: 4 / 5; }
.case-study__img--b   { grid-column: 1 / span 4;  grid-row: 2; aspect-ratio: 1 / 1; --offset-y: -30px; }
.case-study__insight  { grid-column: 6 / span 5;  grid-row: 3; padding-top: clamp(20px, 3vw, 60px); }
.case-study__img--c   { grid-column: 8 / span 5;  grid-row: 4; aspect-ratio: 16 / 10; }
.case-study__img--d   { grid-column: 2 / span 5;  grid-row: 4; aspect-ratio: 4 / 3; --offset-y: 40px; }
.case-study__results  {
  grid-column: 1 / -1;
  grid-row: 5;
  margin-top: clamp(40px, 5vw, 60px);
  border-top: 1px solid var(--divider);
  padding-top: clamp(24px, 3vw, 36px);
  text-align: center;
}
.case-study__results .case-study__label { margin-bottom: 12px; }
.case-study__results p {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(20px, 2vw, 28px);
  margin: 0;
  color: var(--text-primary);
}

/* Variant B (project 2) — seven rows, no two blocks in a row sharing
   a column span. Asymmetric plate sizing replaces the old 2×2 matrix.
     r1  brief (1–5)            | wordmarks (7–12, align-start)
     r2  "The Line" (1 / -1)
     r3  Motus unit (1–4)  ·  Nouro unit (5–8, +32px)  |  insight (9–12)
     r4  "The System" (1 / -1)
     r5  type specimen (1–7)    | palette (9–12)
     r6  marks (1–5)            | clear space (7–12, +32px)
     r7  brand book (1–4)       | results (6–12)
   The Nouro +32px offset is applied via margin-top on the wrapper so
   it survives the expand stagger's clearProps: 'transform'. */
.case-study--b .case-study__brief      { grid-column: 1 / span 5;  grid-row: 1; }
.case-study--b .case-study__wordmarks  { grid-column: 7 / span 6;  grid-row: 1; align-self: start; }
.case-study--b .case-study__subhead--line { grid-column: 1 / -1;   grid-row: 2; }
.case-study--b .case-study__img--b     { grid-column: 1 / span 4;  grid-row: 3; --offset-y: 0px; aspect-ratio: auto; }
.case-study--b .case-study__img--d     { grid-column: 5 / span 4;  grid-row: 3; --offset-y: 0px; aspect-ratio: auto; margin-top: 32px; }
.case-study--b .case-study__insight    { grid-column: 9 / span 4;  grid-row: 3; padding: 0; align-self: center; }
.case-study--b .case-study__subhead--sys { grid-column: 1 / -1;    grid-row: 4; }
/* Three-class selectors (0,3,0) outrank the --has-image combined rule
   (0,2,0) so grid placements and aspect ratios survive reordering. */
.case-study--b .case-study__img--plate-type.case-study__img--has-image  { grid-column: 1 / span 7;  grid-row: 5; aspect-ratio: 1999 / 1545; }
.case-study--b .case-study__img--plate-color.case-study__img--has-image { grid-column: 9 / span 4;  grid-row: 5; aspect-ratio: 1999 / 1545; }
.case-study--b .case-study__img--plate-marks.case-study__img--has-image { grid-column: 1 / span 5;  grid-row: 6; aspect-ratio: 1999 / 1545; }
.case-study--b .case-study__img--plate-clear.case-study__img--has-image { grid-column: 7 / span 6;  grid-row: 6; aspect-ratio: 1999 / 1545; margin-top: 32px; }
.case-study--b .case-study__img--c.case-study__img--doc { grid-column: 1 / span 4;  grid-row: 7; aspect-ratio: 1999 / 1545; }
.case-study--b .case-study__results    { grid-column: 6 / span 7;  grid-row: 7; text-align: left; margin-top: 0; }

/* Variant C (project 3) — more whitespace, smaller plates */
.case-study--c .case-study__brief    { grid-column: 2 / span 5; }
.case-study--c .case-study__img--a   { grid-column: 8 / span 4; aspect-ratio: 3 / 4; }
.case-study--c .case-study__img--b   { grid-column: 1 / span 3; aspect-ratio: 1; --offset-y: 20px; }
/* Insight rides beside the carousel in row 2, taking the columns the
   video vacated. Rows below shift up one so the vacated row 3 doesn't
   leave an empty band (implicit rows collapse to 0 height, but their
   row-gaps don't). */
/* The paragraph is capped at 28ch, so left-aligned in a 7-column band
   it pins itself against the carousel and dumps all the slack on the
   right. justify-self shrinks the block to its text width and centres
   it in the band; align-self centres it against the much taller card
   fan. Text stays left-aligned — the block moves, not the ragging. */
.case-study--c .case-study__insight  {
  grid-column: 6 / span 7;
  grid-row: 2;
  padding-top: 0;
  justify-self: center;
  align-self: center;
}
/* Slots C and D carry no tiles in this case study any more; the rules
   stay so a re-added tile lands in the layout rather than falling
   through to the base grid. */
.case-study--c .case-study__img--c   { grid-column: 9 / span 4; grid-row: 3; aspect-ratio: 1; }
/* Row 3, not 4 — an empty row collapses to zero height but keeps the
   row-gaps either side of it, which would leave a ~80px dead band. */
.case-study--c .case-study__results  { grid-row: 3; }
.case-study--c .case-study__img--d   { grid-column: 3 / span 5; aspect-ratio: 16 / 9; }

/* Variant A — image-a is landscape (16:9), so the layout collapses
   to a tighter 4-row composition: insight rides next to image-b in
   row 2, and image-c / image-d move up to row 3. Eliminates the
   right-column dead space between hero still and insight. */
.case-study--a .case-study__insight  {
  grid-column: 6 / span 7;
  grid-row: 2;
  padding-top: 0;
  align-self: start;
}
.case-study--a .case-study__img--c   { grid-row: 3; }
.case-study--a .case-study__img--d   { grid-row: 3; }
.case-study--a .case-study__results  { grid-row: 4; }

/* --- 7a. Project 1 --- */
.project--bleed .project__media--bleed {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0;
}

.project__caption--bleed {
  max-width: var(--maxw);
  margin: 32px auto 0;
  padding: 0 var(--gutter);
}

/* --- 7b. Project 2 — Split, offset --- */
.project--split .project__layout--split {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(40px, 6vw, 96px);
  /* Plate and caption centre on each other — the rebrand plate should
     sit level with the title/copy, not ride high against an offset
     caption. */
  align-items: center;
}

.project--split .project__media--split { grid-column: 1; }

.project--split .project__caption--split {
  grid-column: 2;
  /* No offset padding: it was the old editorial down-push, and it
     would skew the centring the layout now does. */
}

/* --- 7c. Project 3 — Quiet, asymmetric --- */
.project--quiet .project__layout--quiet {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: 0.42fr 0.58fr;
  gap: clamp(40px, 6vw, 100px);
  align-items: end;
}

.project--quiet .project__caption--quiet {
  grid-column: 1;
  padding-bottom: clamp(20px, 4vw, 80px);
}

.project--quiet .project__media--quiet {
  grid-column: 2;
  margin-right: calc(var(--gutter) * -0.5);
}


/* --- 7d. Project 3 — Motus Drink Mix product lineup ---
   Centered caption stacked over three transparent PNGs in a row. The
   centring is deliberate: project 2 runs media-left/caption-right and
   project 4 runs caption-left/media-right, so a centred spread between
   them breaks the alternating rhythm. No plates or per-product
   captions. The shadow uses filter: drop-shadow (not box-shadow) so it
   traces the product silhouette instead of the image's rectangular
   bounding box. */
.project--lineup .project__caption--lineup {
  max-width: var(--maxw);
  margin: 0 auto clamp(48px, 7vw, 96px);
  padding: 0 var(--gutter);
  /* .project__caption is a flex column — centring the cross axis pulls
     the meta row, title, and description into a centred stack. */
  align-items: center;
  text-align: center;
}

.project--lineup .project__desc { max-width: 52ch; }

/* The max-width rail lives on the stage, not the grid, so the
   absolutely-positioned copy panel is bounded by the same 1200px
   container as the bottles. Anchored to the stage while the stage ran
   full-bleed, the copy drifted out toward the viewport edge on wide
   screens while the fan stayed on the rail. */
.lineup__inner {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(24px, 4vw, 64px);
  align-items: end;
  justify-items: center;
}

/* Two things worth knowing about this figure:
   1. Its transform belongs to GSAP (the fan), so the hover lift rides
      on the inner button instead. Two owners on one transform would
      mean a CSS transition easing against GSAP's per-frame inline
      writes, which reads as lag.
   2. It shrink-wraps its bottle — the image is height-driven with
      width:auto, so the box is the silhouette's bounding box rather
      than a wide letterboxed cell. That keeps the click target on the
      product instead of 100px of empty space beside it, and lets the
      fan measure overlap straight off the element width. */
.lineup__item {
  margin: 0;
  /* Fan rotation pivots at the base so the bottles splay like a hand of
     cards rather than pinwheeling about their middles. */
  transform-origin: bottom center;
  transition: filter 0.4s ease;
  filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.10));
}

.lineup__btn {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  display: block;
  color: inherit;
  cursor: pointer;
  transition: transform 0.4s ease;
}

.lineup__item img {
  width: auto;
  height: clamp(320px, 38vw, 560px);
  display: block;
  pointer-events: none;
}

.lineup__item:hover .lineup__btn,
.lineup__btn:focus-visible {
  transform: translateY(-5px);
}
.lineup__item:hover {
  filter: drop-shadow(0 22px 34px rgba(0, 0, 0, 0.18));
}

/* Once fanned, GSAP owns every bottle's position — suppress the hover
   lift so it can't stack on top of the choreographed offsets. */
.lineup__stage.is-fanned .lineup__btn { transform: none; }

/* ----- Fanned-state copy panel ----- */
.lineup__stage {
  position: relative;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* Centring is done with top/bottom + justify-content, never a
   translate, because GSAP animates this element's x on reveal. */
/* Anchored from the left so the copy sits just clear of the fan rather
   than pinned to the far edge with dead space between them. The fan
   clusters around 28% of the rail and spans out to roughly 49%, so 52%
   picks it up shortly after. Line length is governed by the subhead's
   own max-width, not a fixed panel width. */
.lineup__copy {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 52%;
  right: var(--gutter);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 16px;
  opacity: 0;
  pointer-events: none;
}
.lineup__stage.is-fanned .lineup__copy { pointer-events: auto; }

.lineup__copy-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(32px, 4.6vw, 68px);
  line-height: 1.05;
  letter-spacing: -0.012em;
  margin: 0;
  color: var(--text-primary);
}

.lineup__copy-sub {
  font-family: var(--font-body);
  font-size: clamp(15px, 1.15vw, 18px);
  line-height: 1.55;
  color: var(--text-secondary);
  max-width: 34ch;
  margin: 0;
}

.lineup__close {
  position: absolute;
  top: 0;
  right: 0;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--divider);
  border-radius: 50%;
  background: none;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color 0.25s ease, border-color 0.25s ease;
}
.lineup__close:hover {
  color: var(--text-primary);
  border-color: var(--text-primary);
}

/* -------------------- 8. About -------------------- */
.about {
  padding: var(--section-pad) 0;
  background: var(--bg-primary);
  border-top: 1px solid var(--divider);
}

.about__inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: 0.42fr 0.58fr;
  gap: clamp(40px, 6vw, 120px);
}

.about__side { position: relative; }

.about__pull {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(34px, 4vw, 60px);
  line-height: 1.1;
  letter-spacing: -0.01em;
  margin: 0 0 64px;
  color: var(--text-primary);
}

.about__quote {
  color: var(--accent);
  font-style: normal;
}

.about__credits {
  display: flex;
  flex-direction: column;
  gap: 18px;
  border-top: 1px solid var(--divider);
  padding-top: 24px;
}

.about__credits li {
  display: grid;
  grid-template-columns: 110px 1fr;
  gap: 24px;
  font-size: 13px;
  line-height: 1.5;
}
.about__credits span {
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.about__credits em {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 17px;
  color: var(--text-primary);
}

.about__lede {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(24px, 2.4vw, 34px);
  line-height: 1.3;
  letter-spacing: -0.005em;
  margin: 0 0 1.4em;
  color: var(--text-primary);
}

.about__body p { margin: 0 0 1.3em; max-width: 56ch; }
.about__body p:last-child { margin-bottom: 0; }


/* -------------------- 9. Contact (closing frame) -------------------- */
.contact {
  background: var(--bg-deep);
  color: var(--text-invert);
  padding: clamp(120px, 18vw, 240px) 0 48px;
  position: relative;
  overflow: hidden;
}

.contact__inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
  position: relative;
}

.contact__eyebrow {
  color: rgba(239,234,224,0.55) !important;
  margin-bottom: 32px;
}

.contact__cta {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(60px, 11vw, 180px);
  line-height: 0.92;
  letter-spacing: -0.02em;
  margin: 0 0 80px;
}
.contact__cta em {
  font-style: italic;
  color: var(--accent);
}

.contact__links {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(20px, 4vw, 56px);
  align-items: baseline;
  margin-bottom: 96px;
}

.contact__links a {
  font-size: clamp(16px, 1.4vw, 20px);
  letter-spacing: 0.04em;
  border-bottom: 1px solid rgba(239,234,224,0.25);
  padding-bottom: 4px;
  transition: color 0.3s ease, border-color 0.3s ease;
}
.contact__links a:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.contact__primary {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(28px, 3.2vw, 44px) !important;
  letter-spacing: 0 !important;
}

.contact__rule {
  height: 1px;
  background: rgba(239,234,224,0.18);
  margin-bottom: 28px;
}

.contact__colophon {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(239,234,224,0.55);
}

/* -------------------- 10. Focus / a11y -------------------- */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 2px;
}

/* Visually hidden but announced by screen readers (live regions etc.) */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* -------------------- 11. Responsive -------------------- */
@media (max-width: 1024px) {
  .about__inner { grid-template-columns: 1fr; }
  .about__side { margin-bottom: 48px; }

  .project--split .project__layout--split,
  .project--quiet .project__layout--quiet {
    grid-template-columns: 1fr;
    gap: 32px;
  }
  .project--split .project__caption--split { padding-top: 0; }
  .project--quiet .project__media--quiet { margin-right: 0; }

  /* Lineup stays three-across, just scaled down */
  .lineup__item img { height: clamp(240px, 34vw, 400px); }

  /* Case studies keep the 12-column grid on tablets but tighten the
     rhythm; variant B's paired tiles widen to full halves so nothing
     renders below ~330px. */
  .case-study__inner { row-gap: clamp(32px, 5vw, 56px); }
  /* Row 1 stays two-up here: the 6-column wordmark track is still
     ~443px, so the Tonum lockup clears its 280px floor. The brief needs
     no override — it already holds 5 columns at desktop and must keep
     them, or it would collide with the wordmarks sharing the row. */
  /* Row 3 un-splits here. Three-column tiles would render ~220px wide
     at this width — too small to read a bottle — so the pair takes the
     full row and the insight drops beneath it rather than being
     squeezed alongside. */
  /* Variant B tablet: bottle units widen to halves, insight drops
     below. No :has() selectors — the wrapper classes are enough. */
  .case-study--b .case-study__img--b  { grid-column: 1 / span 6; margin-top: 0; }
  .case-study--b .case-study__img--d  { grid-column: 7 / span 6; margin-top: 0; }
  .case-study--b .case-study__insight { grid-column: 1 / span 9; grid-row: 4; align-self: start; }
  .case-study--b .case-study__subhead--sys  { grid-row: 5; }
  .case-study--b .case-study__img--plate-type.case-study__img--has-image  { grid-column: 1 / span 7; grid-row: 6; }
  .case-study--b .case-study__img--plate-color.case-study__img--has-image { grid-column: 9 / span 4; grid-row: 6; }
  .case-study--b .case-study__img--plate-marks.case-study__img--has-image { grid-column: 1 / span 5; grid-row: 7; }
  .case-study--b .case-study__img--plate-clear.case-study__img--has-image { grid-column: 7 / span 6; grid-row: 7; margin-top: 0; }
  .case-study--b .case-study__img--c.case-study__img--doc { grid-column: 1 / span 5; grid-row: 8; }
  .case-study--b .case-study__results { grid-column: 6 / span 7; grid-row: 8; }

  .hero__frame-svg { width: 84vw; height: 56vw; }
}

@media (max-width: 768px) {
  .site-nav { padding: 16px var(--gutter); font-size: 11px; letter-spacing: 0.18em; }
  .site-nav__mark { font-size: 18px; }
  .site-nav__links { gap: 16px; }

  /* Mobile: no scroll-driven timeline, so hero is just a normal 100vh
     block and the stage scrolls naturally with the document. */
  .hero { height: 100vh; }
  .hero__stage { position: relative; }

  .hero__frame-svg { width: 88vw; height: 64vw; }
  .hero__line--name { font-size: clamp(44px, 12vw, 76px); }
  .hero__line--tag  { font-size: 12px; letter-spacing: 0.26em; }
  .hero__placeholder-tag { right: 18px; bottom: 18px; }

  .work { padding: 96px 0 48px; }
  .project { margin-bottom: 96px; }

  /* Stays three-across rather than stacking: the fan is a horizontal
     gesture, so a vertical column would have nothing to fan into. The
     copy reflows beneath the row instead of sitting beside it. */
  .lineup__inner { gap: 8px; }
  .lineup__item img { height: clamp(180px, 42vw, 280px); }

  .lineup__copy {
    position: static;
    width: 100%;
    max-width: none;
    margin-top: 36px;
    padding-top: 8px;
    align-items: center;
    text-align: center;
  }
  .lineup__close {
    position: static;
    order: -1;
    margin-bottom: 4px;
  }
  .lineup__copy-sub { max-width: 40ch; }

  /* Case study collapses to single column on small screens.
     The !important is deliberate: tiles are placed by two- and
     three-class desktop rules, and out-ranking them selector by
     selector has caused three separate mobile regressions (crossfade
     pair, pinned video, carousel span). The collapse now wins by force
     for every panel child, present and future — single-column order is
     simply DOM order. */
  .case-study__inner {
    grid-template-columns: 1fr;
    row-gap: 32px;
  }
  .case-study__inner > * {
    grid-column: 1 / -1 !important;
    grid-row: auto !important;
    transform: none;
    padding-top: 0;
  }
  /* The carousel's desktop nudge is an editorial device for the
     two-column composition; in a single column it is just a gap. */
  .case-study--c .case-study__img--b.case-study__img--launch { top: 0; }

  /* Insight's centring only makes sense beside the carousel. Stacked,
     it would be the one indented block among full-width siblings. */
  .case-study--c .case-study__insight {
    justify-self: stretch;
    align-self: start;
  }

  /* Carousel cards scale to the narrow viewport; the stage height
     follows automatically via its --card-w calc. */
  .case-study__img--launch { --card-w: min(58vw, 240px); }
  .case-study__img { aspect-ratio: 4 / 3; }
  /* Keep the cut at its native ratio rather than the 4:3 default. */
  .case-study__img--has-video { aspect-ratio: 16 / 9; }
  /* Xfade units collapse cleanly as single-column stacked blocks.
     Neutralise the editorial margin-top offsets from desktop. The
     wrapper needs aspect-ratio: auto so it sizes to its content
     rather than the 4:3 catch-all above. */
  .case-study__img--xfade-unit { aspect-ratio: auto; }
  .case-study--b .case-study__img--d { margin-top: 0; }
  .case-study--b .case-study__img--plate-clear.case-study__img--has-image { margin-top: 0; }
  .case-study--b .case-study__results { text-align: center; margin-top: clamp(40px, 5vw, 60px); }
  /* Wordmarks are already stacked on a shared left edge at desktop;
     dropping into one column only needs the percentage widths
     re-based against the now-full-width track, keeping the same scale
     and opacity contrast. This rule sits after the collapse catch-all,
     so its padding survives the catch-all's padding-top reset. */
  .case-study__wordmarks { padding: 8px 0 0; gap: 40px; }
  /* 110px cap, not 140: against the 330px Tonum mark that holds the
     ~3x width contrast the composition depends on. At 140 it fell to
     2.4x and the hierarchy went soft. */
  .case-study__wordmark--past { width: clamp(90px, 26%, 110px); }
  .case-study__wordmark--now  { width: min(88%, 330px); }
  /* Specimen plates keep the exact export ratio (the 4:3 default above
     would crop the page edges). Restated because this one-class rule
     loses to the equally-specific default by source order. */
  .case-study__img--plate { aspect-ratio: 1999 / 1545; }

  .about__credits li { grid-template-columns: 1fr; gap: 4px; }

  .contact { padding: 120px 0 36px; }
  .contact__cta { font-size: clamp(52px, 14vw, 92px); margin-bottom: 56px; }
  .contact__links { flex-direction: column; gap: 18px; margin-bottom: 64px; }
  .contact__colophon { font-size: 10px; }
}

/* -------------------- 12. Reduced motion -------------------- */
@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1 !important; transform: none !important; }
  .hero__line { opacity: 1 !important; transform: none !important; }
  .site-nav { opacity: 1 !important; pointer-events: auto !important; }
  .hero__scroll-line::after { animation: none; }

  /* No fan choreography — the bottles hold their row, so the copy has
     to drop beneath them rather than into space that never opens up. */
  .lineup__copy {
    position: static;
    width: 100%;
    max-width: none;
    margin-top: 40px;
    align-items: center;
    text-align: center;
  }
  .lineup__close {
    position: static;
    order: -1;
    margin-bottom: 4px;
  }
  .lineup__btn { transition: none; }
}

/* -------------------- 13. Scroll progress tracker -------------------- */
/* Desktop-only orientation rail on the right edge. Kept as one block —
   component, breakpoints and reduced-motion together — so the whole
   feature reads (and lifts out) in one piece.

   Colour: this passes over both --bg-primary (warm white) and the
   contact section's --bg-deep (near-black), so it borrows the site
   nav's trick — mix-blend-mode: difference over a light currentColor.
   That inverts against whatever is behind it, which is why the parts
   below set weight with `opacity` rather than naming --divider /
   --text-primary / --text-secondary directly: a literal grey would
   read correctly on one ground and disappear on the other.

   The opacities are picked so the BLENDED result lands on the weight
   the token would have had. difference over #FAFAF7 resolves to black
   on the warm ground and to near-white on --bg-deep, so e.g. the track
   at 0.28 reads ~rgb(180) on warm white — a --divider-weight hairline —
   and ~rgb(80) against the near-black closing frame, still present. A
   literal 0.4 would have been roughly twice --divider's weight. */

.scroll-rail {
  display: none;              /* enabled only in the desktop query below */
  position: fixed;
  right: clamp(28px, 3vw, 44px);
  top: 50%;
  height: clamp(200px, 40vh, 400px);
  width: 1px;
  z-index: 45;                /* under .site-nav (50) */
  transform: translateY(-50%);
  pointer-events: none;
  mix-blend-mode: difference;
  color: #FAFAF7;

  /* Present from page load, not keyed to hero scroll progress: it has
     to say "this page scrolls" AND offer a way past the 400vh sequence,
     and neither works if it only appears once you're already scrolling.
     Rides in behind the body's .is-loaded fade with a short delay so it
     doesn't compete with the opening frame — settled well inside the
     first second. */
  opacity: 0;
  transition: opacity 0.55s var(--ease-out) 0.35s;
}
body.is-loaded .scroll-rail { opacity: 1; }

/* Generous invisible hit area so hovering doesn't need pixel precision.
   Only this strip and the links take pointer events — the container
   stays transparent to clicks. */
.scroll-rail::before {
  content: '';
  position: absolute;
  top: -24px;
  bottom: -24px;
  right: -20px;
  width: 40px;
  pointer-events: auto;
}

.scroll-rail__track {
  position: absolute;
  inset: 0;
  background: currentColor;
  border-radius: 1px;
  opacity: 0.28;              /* ≈ --divider weight once blended */
  transition: opacity 0.3s var(--ease-out);
}

.scroll-rail__pill {
  position: absolute;
  top: 0;
  left: 50%;
  width: 3px;
  height: 28px;
  margin-left: -1.5px;
  border-radius: 999px;
  background: currentColor;
  will-change: transform;
}

.scroll-rail__nav {
  position: absolute;
  inset: 0;
}

/* --tick-pos is a px offset written by JS, measured on the same scale
   the pill travels on, so a tick and the pill's centre coincide exactly
   when scroll progress reaches that section. */
.scroll-rail__link {
  position: absolute;
  right: 0;
  top: var(--tick-pos, 0px);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 14px;
  padding: 6px 0;
  transform: translateY(-50%);
  pointer-events: auto;
}

.scroll-rail__tick {
  flex: none;
  width: 7px;
  height: 1px;
  margin-right: -3px;         /* straddles the 1px rail */
  background: currentColor;
  opacity: 0.35;
  transition: opacity 0.3s var(--ease-out), width 0.3s var(--ease-out);
}

/* .case-study__label treatment — matched on type, not colour (see note
   at the top of this section). Legible at rest: restraint comes from the
   muted weight and 11px type, not from hiding it behind a hover nobody
   would think to perform at the right edge of the window. */
.scroll-rail__label {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0.45;
  transition: opacity 0.3s var(--ease-out);
}

/* Hover / focus-within is an EMPHASIS layer, not a reveal — everything
   is already on screen, this just lifts it. */
.scroll-rail:hover .scroll-rail__track,
.scroll-rail:focus-within .scroll-rail__track { opacity: 0.85; }

.scroll-rail:hover .scroll-rail__tick,
.scroll-rail:focus-within .scroll-rail__tick { opacity: 0.6; }

.scroll-rail:hover .scroll-rail__label,
.scroll-rail:focus-within .scroll-rail__label { opacity: 0.85; }

/* Active section — JS keeps .is-active and aria-current in sync with
   scroll position. Declared AFTER the hover rules: same specificity
   (0,3,0), so source order is what keeps the active label pinned at
   full opacity while the rest lift under the cursor. */
.scroll-rail__link.is-active .scroll-rail__label { opacity: 1; }

.scroll-rail__link.is-active .scroll-rail__tick {
  opacity: 0.9;
  width: 13px;
}

/* Desktop only, and only where hover actually means something — keeps
   it off large touch screens where the reveal is unreachable. */
@media (min-width: 1025px) and (hover: hover) and (pointer: fine) {
  .scroll-rail { display: block; }
}

/* Positional feedback, so it stays — but the pill maps straight to
   scroll position (easing is dropped in JS) and the reveals cut in. */
@media (prefers-reduced-motion: reduce) {
  .scroll-rail__track,
  .scroll-rail__tick,
  .scroll-rail__label { transition: none; }
}
