/* ============================================================
   Self-hosted brand fonts
   ============================================================ */
@font-face {
  font-family: 'Gotham Bold';
  src: url('fonts/Gotham Bold Regular.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ============================================================
   ENDEAVOR FILMS — design tokens
   ============================================================ */
:root {
  /* palette */
  --ink:        #0e0e0f;   /* near-black — hero / nav / footer base */
  --ink-soft:   #1c1d1f;
  --paper:      #ffffff;   /* body background */
  --stone:      #f3f1ea;   /* warm neutral for alternating panels */
  --teal:       #3d6a80;   /* steel teal — wedge dividers, links (darkened from ref for AA) */
  --teal-deep:  #29465a;
  --sage:       #66704f;   /* olive sage — testimonials / footer (darkened from ref for AA) */
  --sage-deep:  #4e5640;
  --slate:      #3f6371;   /* deep slate-teal — footer gradient (darkened from ref for AA) */
  --lime:       #a9ba17;   /* single bold accent, used sparingly */
  --text:       #17181a;
  --text-soft:  rgba(23, 24, 26, 0.66);
  --text-on-dark: rgba(255, 255, 255, 0.94);
  --text-on-dark-soft: rgba(255, 255, 255, 0.68); /* only safe on --ink — huge luminance headroom */

  /* type */
  --font-display: 'Space Grotesk', 'Helvetica Neue', Arial, sans-serif;
  --font-body: 'Lato', 'Helvetica Neue', Arial, sans-serif;

  /* rhythm */
  --edge: clamp(1.5rem, 5vw, 4.5rem);
  --radius: 4px;

  /* shared by .hero, .section-chevron-hero and .films — the hero's
     bottom V-notch and the chevron's top notch have to stay the exact
     same depth to interlock, and Films' own min-height accounts for
     the chevron's footprint so [chevron + Films] together still fill
     one screen the same way Photography does on its own. One source
     of truth so a future resize can't desync them. */
  --hero-chev: 110px;
}

/* ============================================================
   Reset & base
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

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

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.08;
  margin: 0;
}

p { margin: 0; }

button {
  font-family: inherit;
  cursor: pointer;
  background: none;
  border: none;
  color: inherit;
}

ul { list-style: none; margin: 0; padding: 0; }

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--ink);
  color: var(--paper);
  padding: 0.75rem 1.25rem;
  z-index: 1000;
}
.skip-link:focus {
  left: var(--edge);
  top: 0.75rem;
}

:focus-visible {
  outline: 2px solid var(--lime);
  outline-offset: 3px;
}

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

/* ============================================================
   Shared bits
   ============================================================ */
.eyebrow {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-on-dark-soft);
  margin: 0 0 1.1rem;
}
.eyebrow-dark { color: var(--teal-deep); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.85rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 1rem 1.9rem;
  border-radius: 999px;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
  min-height: 44px;
}
.btn:hover { transform: translateY(-1px); }

.btn-outline-light {
  border: 1.5px solid rgba(255,255,255,0.55);
  color: var(--text-on-dark);
}
.btn-outline-light:hover {
  border-color: var(--teal);
  color: var(--teal);
}
.btn-play {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--text-on-dark);
  color: var(--ink);
}
.btn-accent {
  /* muted sage, not the site's lime accent — sits better with the
     palette than a bright chartreuse call-to-action would here */
  background: var(--sage);
  color: var(--text-on-dark);
}
.btn-accent:hover { background: var(--sage-deep); }

.section-head {
  max-width: 640px;
  margin: 0 0 3rem;
}
.section-head h2 {
  font-size: clamp(2.1rem, 4vw, 3rem);
  margin-bottom: 1rem;
}
/* Scoped to just these two headings — every other section heading
   (Studio, Testimonials, CTA) stays on the shared rule above. Matches
   the hero eyebrow's Gotham Bold/all-caps treatment (.hero-eyebrow-large)
   but sized down and tightened so it reads as related-but-subordinate
   rather than competing with the hero at the same visual weight. */
.films .section-head h2,
.photography .section-head h2 {
  font-family: 'Gotham Bold', 'Helvetica Neue', Arial, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: clamp(1.9rem, 3.4vw, 2.75rem);
}
.section-intro {
  font-size: 1.05rem;
  color: var(--text-soft);
  max-width: 56ch;
}

/* Standalone chevron divider — the faceted motif echoed from the mark.
   Used where a section can't clip itself (e.g. bridging two blocks that
   don't share a background). */
.section-chevron {
  --chev: 55px;
  position: relative;
  z-index: 6;
  height: calc(var(--chev) * 2);
  margin-top: calc(var(--chev) * -1);
  background: linear-gradient(180deg, var(--teal) 0%, var(--sage) 100%);
  clip-path: polygon(
    0 0,
    50% var(--chev),
    100% 0,
    100% var(--chev),
    50% calc(var(--chev) * 2),
    0 var(--chev)
  );
  pointer-events: none;
}
/* CTA divider: downward-V top over the white section above, flat bottom
   that butts directly into the dark CTA (no white gap into a dark block).
   Same shape reused by .section-chevron-footer below (studio->CTA and
   CTA->footer are both "solid rectangle sits under a plain color block
   that doesn't self-clip" cases) — only the gradient colors differ per
   pair of sections being bridged. */
.section-chevron-cta,
.section-chevron-footer {
  clip-path: polygon(0 0, 50% var(--chev), 100% 0, 100% 100%, 0 100%);
}
.section-chevron-cta {
  background: linear-gradient(180deg, var(--sage) 0%, var(--slate) 100%);
}
/* Contact -> footer divider: .cta-band's own bottom is solid --ink (its
   gradient's last stop) and .site-footer's top is solid --sage (its
   gradient's first stop). Per the notched-gradient rule above, this
   shape's flat bottom (the only part visible at the notch's center, x
   50%) shows the gradient's 50%-100% window — so the gradient completes
   to solid sage by the 50% stop rather than still fading, keeping that
   center strip a uniform sage flush against the footer instead of a
   visible seam. */
.section-chevron-footer {
  background: linear-gradient(180deg, var(--ink) 0%, var(--sage) 50%, var(--sage) 100%);
}
/* Hero divider: nests into the hero's clipped V the same way the old
   Mission section did, but it's just the bare chevron ribbon now — no
   heading/copy. Solid white (not a gradient) so it just closes the gap
   left by the hero's own V-notch and dissolves flush into the Films
   section below (same background color, no seam to blend). */
.section-chevron-hero {
  /* twice the base ribbon thickness, matched to .hero's own --chev */
  --chev: var(--hero-chev);
  margin-top: calc(var(--chev) * -1 - 1px);
  background: var(--paper);
}

/* ============================================================
   Header
   ============================================================ */
.site-header {
  /* --hdr goes 0→1 linearly with scroll position (set by script.js);
     opacity caps at 0.92 once fully scrolled in */
  --hdr: 0;
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 500;
  background: rgba(14, 14, 15, calc(var(--hdr) * 0.92));
  backdrop-filter: saturate(140%) blur(calc(var(--hdr) * 6px));
  box-shadow: 0 1px 0 rgba(255, 255, 255, calc(var(--hdr) * 0.06));
}
.header-inner {
  max-width: 1400px;
  margin: 0 auto;
  padding: 1.1rem var(--edge);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}
.brand {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.brand-logo {
  height: 54px;
  width: auto;
}

.main-nav { position: relative; }
.main-nav ul {
  display: flex;
  gap: 2.1rem;
}
.main-nav a {
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-on-dark-soft);
  padding: 0.5rem 0.1rem;
  border-bottom: 1px solid transparent;
  transition: color 0.2s ease;
}
.main-nav a:hover,
.main-nav a:focus-visible,
.main-nav a.is-active { color: var(--text-on-dark); }

/* sliding active-section indicator (desktop only) */
.nav-indicator {
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 2px;
  /* the blue used on the left-of-center "Justin" diamond, not the lime
     accent — a deliberate, separate choice for this indicator */
  background: var(--teal);
  border-radius: 2px;
  opacity: 0;
  transform: translateX(0);
  transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1),
              width 0.55s cubic-bezier(0.65, 0, 0.35, 1),
              opacity 0.3s ease;
  pointer-events: none;
  will-change: transform, width;
}
.nav-indicator.is-visible { opacity: 1; }

/* "Contact" is a plain nav link like the rest now — no special-cased
   color, so it behaves identically (soft by default, bright on
   hover/active) instead of always sitting brighter than its siblings. */

.header-right {
  display: flex;
  align-items: center;
  gap: 1.4rem;
}
.social-row {
  display: flex;
  gap: 0.9rem;
}
.social-row a {
  color: var(--text-on-dark-soft);
  transition: color 0.2s ease;
}
.social-row a:hover { color: var(--text-on-dark); }

.menu-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
}
.menu-toggle span {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--text-on-dark);
  transition: transform 0.25s ease, opacity 0.25s ease;
}

/* ============================================================
   Hero
   ============================================================ */
.hero {
  /* matched to .section-chevron-hero's own --chev below — these two
     have to stay equal or the interlocking V notches misalign */
  --chev: var(--hero-chev);
  position: relative;
  min-height: 100svh;
  background: var(--ink);
  color: var(--text-on-dark);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* bottom edge dips to a downward V so the chevron ribbon below masks
     the rectangular video corners */
  clip-path: polygon(
    0 0,
    100% 0,
    100% calc(100% - var(--chev)),
    50% 100%,
    0 calc(100% - var(--chev))
  );
}

.hero-video {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  background: var(--ink);
}
.hero-video video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(100deg, rgba(8,9,10,0.88) 0%, rgba(8,9,10,0.6) 42%, rgba(8,9,10,0.32) 68%, rgba(8,9,10,0.55) 100%),
    linear-gradient(0deg, rgba(8,9,10,0.5) 0%, rgba(8,9,10,0) 30%, rgba(8,9,10,0) 75%, rgba(8,9,10,0.6) 100%);
}

.hero-grain {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0.3;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.35'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
}

.hero-inner {
  position: relative;
  z-index: 2;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  max-width: 1400px;
  margin: 0 auto;
  width: 100%;
  padding: 7.5rem var(--edge) 1rem;
}

.hero-copy { max-width: 620px; }
.hero-copy-centered {
  max-width: 800px;
  margin: 0 auto;
  text-align: center;
}
/* same highlight treatment as the Mission section's buttons */
.hero-copy-centered .btn-outline-light:hover {
  border-color: var(--teal-deep);
  color: var(--text-on-dark);
  background: var(--teal-deep);
}

.hero h1 {
  font-family: 'Gotham Bold', 'Helvetica Neue', Arial, sans-serif;
  font-weight: 700;
  letter-spacing: -0.015em;
  margin: 0 0 2rem;
}
.eyebrow.hero-eyebrow-large {
  font-family: 'Gotham Bold', 'Helvetica Neue', Arial, sans-serif;
  font-weight: 700;
  letter-spacing: 0.08em;
  font-size: clamp(2.4rem, 5.2vw, 4.2rem);
  color: var(--text-on-dark);
  margin: 0 0 0.5rem;
  /* opens larger (scale, not font-size, so it's GPU-composited and stays
     fluid at every viewport width) and settles to its real size at 1.5s */
  transform-origin: center;
  animation: hero-name-intro 0.7s cubic-bezier(0.65, 0, 0.35, 1) 1.5s both;
}
.hero h1.hero-heading-small {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: 0.01em;
  font-size: clamp(1.2rem, 2.4vw, 1.7rem);
  margin: 0 auto 2rem;
  max-width: 44ch;
  /* stays invisible until the name has settled, then fades in */
  opacity: 0;
  animation: hero-fade-in 0.6s ease-out 1.5s both;
}
@keyframes hero-name-intro {
  from { transform: scale(1.35); }
  to { transform: scale(1); }
}
@keyframes hero-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
#watch-reel-btn {
  opacity: 0;
  animation: hero-fade-in 0.6s ease-out 1.5s both;
}

/* buttons carried over from the old Mission section, now anchored at
   the very bottom of the hero, outside the vertically-centered copy */
.hero-mission-actions {
  position: relative;
  z-index: 2;
  flex-shrink: 0;
  padding: 0 var(--edge) clamp(2.25rem, 6vh, 3.5rem);
}
.mission-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
}
/* these two buttons highlight in the same blue as the carousel dots,
   not the site's lime accent — scoped here so the hero/CTA buttons
   elsewhere keep their usual hover color */
.mission-actions .btn-outline-light:hover {
  border-color: var(--teal-deep);
  color: var(--text-on-dark);
  background: var(--teal-deep);
}

/* ============================================================
   Films
   ============================================================ */
.films {
  /* Matches Photography's own recipe (fill one viewport, center the
     content in it) rather than centering the *scroll position* on
     Films itself — that was tried and, on a section this short,
     scrolled up far enough to reveal the hero's background video
     above the chevron. Accounting for the chevron's own footprint
     here means [[chevron] + [Films]] together still fill one screen,
     so a plain anchor jump lands the same way Photography's does. */
  /* The negative-margin trick previously used to close the gap only
     pulled the content block up within this box — since justify-
     content:center still divides free space evenly around whatever
     the block's new (shrunken-by-margin) height comes out to, that
     shift added the exact same amount of space back below, and the
     section stopped reading as centered. Actually shrinking the box
     itself (instead of faking it with margins) lets a plain
     justify-content:center keep the gap symmetric while both gaps
     get smaller together. */
  min-height: calc(100svh - var(--hero-chev) - 10rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* matches Photography's own top/bottom padding — same recipe, same
     rhythm, since the two now sit right next to each other in flow */
  padding: 2.75rem var(--edge);
  /* The fixed .site-header floats over this section once scrolled to
     (it isn't in normal flow, so it doesn't push content down on its
     own). With the gap above already tightened, centered content was
     landing partly behind it. Reserving the header's real measured
     height as top padding means justify-content:center now centers
     within the space actually visible below the header, not the full
     box — the top gap still reads as "closed," just measured from
     where the header ends instead of from the true top edge. */
  padding-top: calc(var(--header-h, 96px) + 1.5rem);
  /* raised from 1400px so the wider carousel below has room — the
     section-head/section-intro elements have their own 640px/56ch caps,
     so this only actually widens the tabs row and the carousel */
  max-width: 1650px;
  margin: 0 auto;
  width: 100%;
}
.films .section-head { margin-left: auto; margin-right: auto; text-align: center; }
.films .tabs { justify-content: center; }

.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
  border-bottom: 1px solid #e4e1d6;
  margin-bottom: 2.5rem;
}
.tab {
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
  padding: 0 0 1rem;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 0.2s ease, border-color 0.2s ease;
  min-height: 44px;
}
.tab:hover { color: var(--text); }
.tab.is-active {
  color: var(--teal-deep);
  border-color: var(--teal);
}
/* Reels is the only tab with a shortened mobile label ("Reels" instead
   of "Previous Reels") — see the 640px media query below. Desktop always
   shows the full label. */
.tab-label-short { display: none; }

.film-carousel {
  /* The ACTIVE CARD (not the carousel box) is the thing that should read
     as 70% of the page: card = 82% of the rail (see .film-card below),
     and — now that the arrows live below the rail instead of flanking it
     (.film-controls) — the rail IS the carousel's width, with nothing
     else sharing the row to subtract. So carousel width = 70% / 0.82,
     which nets the card out to exactly 70% of .films' content box once
     the 82% is applied. If that 0.82 ratio below ever changes, this
     divisor must change with it — they're two halves of one equation. */
  width: calc(70% / 0.82);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1.5rem;
}
.film-controls {
  display: flex;
  justify-content: center;
  gap: 1rem;
}
.film-controls .slide-arrow {
  border-color: rgba(23, 24, 26, 0.18);
  color: var(--text);
}
.film-controls .slide-arrow:hover { border-color: var(--teal); color: var(--teal-deep); }

.film-rail {
  position: relative;
  width: 100%;
  min-width: 0;
  overflow: hidden;
  /* horizontal drag is captured by the swipe handler in script.js;
     pan-y keeps vertical page scroll working under a touch drag that
     starts on the rail instead of the swipe capturing it too. */
  touch-action: pan-y;
}
/* Fades the peeking neighbor cards into the page at each edge. This is
   a pair of solid overlays painted over the rail (not a mask-image
   gradient on the rail itself) — mask-image here was unreliable across
   browsers: it rendered asymmetrically and would only repaint one side
   correctly after an unrelated hover forced a reflow. Plain overlays
   have no such repaint dependency.
   Multi-stop (not a flat 2-stop linear fade): a straight opacity ramp
   reads as an abrupt wall right at the outer edge because the eye is
   far more sensitive to alpha change near full-opacity than near
   transparent. Easing through intermediate stops removes that "hard
   edge" look and — combined with the wider zone below — keeps more of
   each peeking card visible through partial opacity instead of fully
   hidden under solid paper near the rail's edge. */
.film-rail::before,
.film-rail::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: clamp(40px, 11%, 110px);
  z-index: 3;
  pointer-events: none;
}
.film-rail::before {
  left: 0;
  background: linear-gradient(
    90deg,
    var(--paper) 0%,
    rgba(255, 255, 255, 0.85) 20%,
    rgba(255, 255, 255, 0.45) 55%,
    rgba(255, 255, 255, 0) 100%
  );
}
.film-rail::after {
  right: 0;
  background: linear-gradient(
    270deg,
    var(--paper) 0%,
    rgba(255, 255, 255, 0.85) 20%,
    rgba(255, 255, 255, 0.45) 55%,
    rgba(255, 255, 255, 0) 100%
  );
}
.film-track {
  position: relative;
  display: flex;
  gap: 1.25rem;
  will-change: transform;
  transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1);
}
.film-card {
  /* the active card reads at 82% of the rail's width — paired with
     .film-carousel's width:calc(70%/0.82) above so the card itself
     nets out to exactly 70% of the page; the remaining 18% is what
     lets the previous/next cards peek in at the sides */
  flex: 0 0 82%;
  min-width: 0;
  position: relative;
  /* close to a native 1920x1080 frame, so it reads as video rather
     than a photo tile */
  aspect-ratio: 16 / 9;
  /* Downward cap on the height the 16:9 ratio would otherwise derive
     from the (width-driven) flex-basis above — on wide desktop
     viewports that width can be tall enough that section-head + tabs +
     card + arrows together exceed 100svh, pushing the arrows below the
     fold. max-width mirrors max-height at the fixed 16:9 ratio via one
     shared custom property, so whichever constraint actually binds, the
     other is derived from it and the card can never render off-ratio.
     (Previously max-width had no counterpart: width stayed pinned to
     the full flex-basis while only height shrunk, so the card visibly
     stretched wider-than-16:9 whenever this clamp bound — cropped
     acceptably by background-size:cover, but a real aspect-ratio
     violation the user asked to eliminate, not just hide via cropping.)
     Never taller than the natural aspect-ratio size, so it's a no-op on
     mobile's much narrower rail. .film-card.is-vertical inherits both
     properties too, but its own narrower 26% flex-basis / 9:16 ratio
     never approaches either ceiling, and its real on-screen size is set
     directly by sizeVerticalCards()'s inline styles regardless. */
  --film-card-max-h: clamp(260px, calc(100svh - var(--header-h, 96px) - 27rem), 640px);
  max-height: var(--film-card-max-h);
  max-width: calc(var(--film-card-max-h) * 16 / 9);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  color: var(--text-on-dark);
  isolation: isolate;
}
/* Portrait ("vertical", 9:16) videos keep the SAME rendered height as
   their 16:9 siblings but a narrower, true-9:16 width. The precise pixel
   width/height are set inline by sizeVerticalCards() in script.js
   (measured from a sibling's real rendered height — the same "measure
   real DOM layout" approach positionFilmTrack already uses for centering,
   needed because .film-card's max-height clamp above makes a pure-CSS
   aspect-ratio-from-flex-basis derivation unreliable). The rule below is
   only a same-shape approximation for the brief window before that JS
   runs, or the unlikely case where a tab has no non-vertical card to
   measure against. On mobile (see the 640px breakpoint) this whole
   sizing model changes — see the comment there. */
.film-card.is-vertical {
  flex: 0 0 26%;
  aspect-ratio: 9 / 16;
}
.film-card[data-video-id] { cursor: pointer; }
/* The thumbnail proper — split out from .film-card (which used to carry
   the background-image/gradient/play-button directly) so the caption can
   move from an overlay on top of it to plain text below it at the 640px
   breakpoint without needing different markup per breakpoint. Desktop:
   absolutely fills .film-card, reproducing the old single-element look
   exactly. */
.film-card-media {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  /* explicit even though `cover` alone should prevent visible tiling —
     script.js used to set the placeholder via the `background`
     shorthand inline, which silently resets background-repeat (and
     background-size!) to their initial values for that element,
     overriding these two lines entirely. Harmless for a gradient (it
     has no intrinsic size to tile), but it meant a real photo dropped
     in later via backgroundImage rendered at native size and tiled —
     the "repeats in a 4-grid" bug. Fixed at the source (that inline
     style is background-image-only now, a longhand, so it can't
     clobber these), but keeping this explicit is cheap insurance. */
  background-repeat: no-repeat;
}
.film-card-media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0) 40%, rgba(0,0,0,0.78) 100%);
  z-index: 1;
}
.film-card-body {
  position: relative;
  z-index: 2;
  padding: 0.9rem 1rem;
  width: 100%;
}
.film-card-tag {
  display: block;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-on-dark-soft);
  margin-bottom: 0.3rem;
}
.film-card-title { font-family: var(--font-display); font-size: 1rem; font-weight: 600; }
.film-card-play {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  width: 44px; height: 44px;
  border-radius: 50%;
  background: rgba(255,255,255,0.16);
  border: 1px solid rgba(255,255,255,0.55);
  display: flex; align-items: center; justify-content: center;
  color: var(--text-on-dark);
  backdrop-filter: blur(1px);
  transition: transform 0.2s ease, background 0.2s ease;
}
.film-card[data-video-id]:hover .film-card-play,
.film-card[data-video-id]:focus-visible .film-card-play {
  transform: translate(-50%, -50%) scale(1.08);
  background: rgba(255,255,255,0.28);
}

/* ============================================================
   Photography
   ============================================================ */
.photography {
  /* fills one viewport when jumped to from the nav — same recipe as
     Films just above it, same top/bottom padding for a matching rhythm */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 2.75rem var(--edge);
  /* Films already reserves space for the fixed header via this same
     padding-top override (see .films above) so centered content never
     lands partly behind it on a nav jump — Photography never got the
     equivalent fix, which is why the eyebrow used to render cut off
     above the fold. */
  padding-top: calc(var(--header-h, 96px) + 1.5rem);
  /* 1400 * 1.4 — widened so the grid itself renders ~40% wider on large
     screens; this does narrow the outer edge margins somewhat on very
     large monitors, an accepted tradeoff. */
  max-width: 1960px;
  margin: 0 auto;
  width: 100%;
}
.photography .section-head { margin-left: auto; margin-right: auto; text-align: center; margin-bottom: 1.75rem; }

/* Single unified 3-across × 2-tall gallery (no tabs, no filtering) — a
   fixed 6-item curated set from photoData spanning all three categories,
   replacing the old 3-tab/6-items-per-tab design. Sized as a percentage
   of .photography's own (now wider) content box, same pattern as
   .film-carousel.
   --photo-card-max lives HERE (not on .photo-card) so both this grid's
   own column tracks and .photo-card's max-height/max-width (inherited
   down) read the exact same value — see the column-width comment below
   for why that single source of truth matters. --photo-gap is likewise
   hoisted so the same "how much room between cards" number drives the
   real gap, the row-gap subtracted in --photo-card-max's budget, and
   the column-width formula below, instead of three copies that could
   drift out of sync. */
.photo-rail {
  display: grid;
  /* Each column is the SMALLER of --photo-card-max (the vertical fit
     budget — see .photo-card below) and an equal three-way share of
     this rail's own actual width. Plain `repeat(3, 1fr)` was the bug:
     1fr always divides the full rail width evenly regardless of
     .photo-card's own (usually smaller) max-width cap, so on any
     viewport where the height budget was the binding constraint, each
     card rendered narrower than its column — and since grid's default
     item alignment is "stretch falls back to start" when an item can't
     fill its track, every card hugged the LEFT edge of its own column,
     leaving a big empty gap on the right of each one. Confirmed live at
     1440x900: nominal gap was 1.5rem (24px) but the real rendered gap
     between cards measured ~98px, and the whole 3-card cluster's true
     center sat ~37px left of the page's actual center. min() here
     removes that slack at the source — each track is exactly as wide as
     the card that fills it, so gap alone determines the space between
     cards, and justify-content:center below centers the (now correctly
     width-fitted) cluster as a whole. The calc() share also guarantees
     this can never overflow horizontally even on an unusually tall,
     narrow window where --photo-card-max's height-derived value would
     otherwise exceed what 3 columns can fit. */
  grid-template-columns: repeat(3, min(var(--photo-card-max), calc((100% - 2 * var(--photo-gap)) / 3)));
  justify-content: center;
  gap: var(--photo-gap);
  width: 90%;
  margin: 0 auto;
  --photo-gap: 1rem;
  /* Real fit-to-one-screen budget, not a safety net: reserves the
     section's actual vertical chrome (padding-top via --header-h,
     the section-head's live-measured height via --photo-head-h — see
     script.js's measurePhotoHeadHeight — its margin-bottom, the
     section's own padding-bottom, and the one row-gap between the grid's
     2 rows) and splits what's left across those 2 rows. A flat rem
     guess here previously went stale the moment the grid changed from
     4x2 to 3x2 (wider columns = taller squares) without anyone touching
     this formula — driving it off real measurements instead means it
     can't drift out of sync with a future content/layout change again. */
  --photo-card-max: clamp(150px, calc((100svh - var(--header-h, 96px) - 1.5rem - var(--photo-head-h, 112px) - 1.75rem - 2.75rem - var(--photo-gap)) / 2), 420px);
}
.photo-card {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  color: var(--text-on-dark);
  /* max-width mirrors max-height via the --photo-card-max custom
     property inherited from .photo-rail above (both read the SAME
     value as the grid's own column-width formula) so the square
     aspect-ratio can never be forced out of shape the way .film-card's
     height-only clamp used to (see below) — grid-stretch would
     otherwise hold width at the full column-track size while only
     height gets capped. */
  max-height: var(--photo-card-max);
  max-width: var(--photo-card-max);
  /* longhand, not the `background` shorthand — this card can also carry
     a real <img> cover (.photo-card-media) painted on top; keeping this
     the image-only longhand avoids the shorthand-resets-size/repeat
     gotcha documented in CLAUDE.md if a background-image swap is ever
     added here later the way film-card's placeholder-then-real-thumbnail
     swap works. */
  background-image: linear-gradient(155deg, #4a4a4a 0%, #1c1c1c 130%);
  cursor: pointer;
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.photo-card:hover,
.photo-card:focus-visible {
  transform: scale(1.035);
  box-shadow: 0 12px 28px rgba(0,0,0,0.28);
}
/* Real cover photo, when a gallery has one — sits between the gradient
   placeholder (behind it) and the ::after darkening overlay + body text
   (both above it, via the stacking rules below). No z-index needed here:
   it's earlier in source order than ::after, and .photo-card-body's
   explicit z-index already lifts it above any auto-stacked sibling. */
.photo-card-media {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Achilles' cover.jpg is a 6632x9651 portrait source — a plain center
   crop into this card's square box removes ~15.6% off both top and
   bottom, which lands inside the subject's helmet/head (measured near
   the top ~8.5% of the frame), cutting it off. Targeted via the
   data-gallery attribute already on .photo-card (no new markup/data
   field needed) rather than a generic rule, since other galleries'
   covers crop fine at plain center. */
.photo-card[data-gallery="corporate/achilles-hope-possibility-2026"] .photo-card-media {
  object-position: center 20%;
}
.photo-card::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0) 40%, rgba(0,0,0,0.8) 100%);
}
.photo-card-body {
  position: relative;
  z-index: 1;
  padding: 1.1rem 1.2rem;
  width: 100%;
}
.photo-card-tag {
  display: block;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-on-dark-soft);
  margin-bottom: 0.3rem;
}
/* 3-line card body: tag (above), then two more lines whose CLASS names
   no longer match what they display — photoCardMarkup() (script.js)
   deliberately renders the project name into .photo-card-org (this,
   the bigger/bolder line) and the org/client name into
   .photo-card-project (the smaller line below), an explicit content
   swap requested after the classes were named. Don't rename the
   classes to "fix" this; the field->class mapping lives in
   photoCardMarkup() only. */
.photo-card-org {
  display: -webkit-box;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
  /* At 2-column mobile widths a single long word ("Commencement") can be
     wider than the card's own content box — with nothing to break at (no
     space), it overflowed straight past the card's rounded edge instead
     of wrapping. Confirmed live at 375px. hyphens:auto (with index.html's
     existing lang="en") gives a real hyphenated break instead of an
     arbitrary mid-word split where the browser supports it; overflow-wrap
     is the fallback everywhere else. */
  overflow-wrap: break-word;
  hyphens: auto;
  /* .photo-card-body is a flex item pinned to the card's bottom edge
     (.photo-card's align-items:flex-end) inside a fixed-height,
     overflow:hidden card — a body taller than the card gets clipped from
     the TOP, not the bottom. Since photoCardMarkup() now puts the
     (variable-length) project name in this line rather than the always-
     short org name, an unbounded line count let a long project name
     ("9/11 Memorial Stair Climb 2025") grow to 4 lines at mobile width
     and push .photo-card-tag entirely off the top of the visible card —
     confirmed live (tag measured 28px above the card's own top edge, i.e.
     invisible). Line-clamping this to a fixed 2 lines guarantees a
     predictable max body height regardless of future project-name length,
     so the tag line can never be pushed out again. */
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.photo-card-project {
  display: block;
  font-family: var(--font-body);
  font-size: 0.85rem;
  font-weight: 400;
  /* Solid white like .photo-card-org (which gets it for free via
     .photo-card's own color), not the semi-transparent --text-on-dark-soft
     .photo-card-tag uses — this line (now displaying org, see the
     .photo-card-org comment above) reads as a second, still-prominent
     identifier, not a soft caption. */
  color: var(--text-on-dark);
  margin-top: 0.15rem;
  overflow-wrap: break-word;
  hyphens: auto;
}

/* ============================================================
   Testimonials / Clients — the whole section is the chevron
   ============================================================ */
.testimonials {
  --chev: 55px;
  position: relative;
  /* fallback shown when the reel is absent (reduced motion / load) */
  background: linear-gradient(155deg, var(--sage) 0%, var(--sage-deep) 100%);
  color: var(--text-on-dark);
  padding: calc(var(--chev) + 4rem) var(--edge) calc(var(--chev) + 3rem);
  overflow: hidden;
  clip-path: polygon(
    0 0,
    50% var(--chev),
    100% 0,
    100% calc(100% - var(--chev)),
    50% 100%,
    0 calc(100% - var(--chev))
  );
}
/* looping reel plays behind the section, dimmed by the sage overlay */
.testimonials-video {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--ink);
}
.testimonials-video video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  /* darken the footage so white copy stays legible under the sage overlay */
  filter: brightness(0.5);
}
.testimonials-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(155deg, var(--sage) 0%, var(--sage-deep) 100%);
  opacity: 0.85;
}
.testimonials-inner {
  position: relative;
  z-index: 2;
}
/* mirrors the studio section head: eyebrow + big display line, centered */
.testimonials-head {
  margin: 0 auto 2.5rem;
  text-align: center;
}
.testimonials-head h2 { color: var(--text-on-dark); }

.testimonial-slider {
  max-width: 900px;
  margin: 0 auto 3.5rem;
  display: flex;
  align-items: center;
  gap: 1.5rem;
}
.slide-arrow {
  flex-shrink: 0;
  width: 44px; height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.4);
  display: flex; align-items: center; justify-content: center;
  color: var(--text-on-dark);
  transition: border-color 0.2s ease, color 0.2s ease;
}
.slide-arrow:hover { border-color: var(--teal); color: var(--teal); }

/* height is fixed in px by script.js to whichever testimonial is
   tallest, so switching slides never resizes the track (and therefore
   never resizes the chevron the whole section is clipped to) */
.testimonial-track {
  position: relative;
  flex: 1;
  height: 220px;
  overflow: hidden;
  touch-action: pan-y;
  transition: height 0.35s ease;
}
.testimonial {
  position: absolute;
  inset: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
  opacity: 0;
  transform: translateX(100%);
  transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.4s ease;
}
.testimonial.is-active { opacity: 1; transform: translateX(0); }
.testimonial p {
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 2vw, 1.4rem);
  font-weight: 500;
  line-height: 1.5;
  margin-bottom: 1.25rem;
}
.testimonial cite {
  font-style: normal;
  font-size: 0.9rem;
  font-weight: 300;
  letter-spacing: 0.04em;
  color: var(--text-on-dark);
}

/* infinite horizontal scroll: the track holds the logo list twice back
   to back, and animates exactly -50% (the width of one full copy) — the
   instant the first copy scrolls out of view the second is sitting in
   its exact starting spot, so the loop is seamless */
.client-marquee {
  max-width: 1200px;
  margin: 0 auto;
  padding-top: 2.75rem;
  border-top: 1px solid rgba(255,255,255,0.22);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 6%, #000 94%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 6%, #000 94%, transparent 100%);
}
.client-marquee-track {
  /* fallback only — a plain -50% is mathematically wrong here (see
     below) and script.js overwrites this with the exact measured value
     once logos have laid out */
  --marquee-shift: -50%;
  display: flex;
  align-items: center;
  gap: 2.75rem;
  width: max-content;
  padding: 0.25rem 0;
  animation: client-marquee-scroll 38s linear infinite;
}
/* Each set is its own flex group so the loop shift can be measured
   directly. With one flat 40-item row, translateX(-50%) assumes the
   midpoint of the total width lands exactly on the seam between set A
   and set B — but with an odd count of gaps (39, for 40 items) across
   the whole row, that midpoint is off by half a gap, which is the
   visible "jump" at the loop point. Splitting into two explicit groups
   lets script.js measure set B's real starting offset and animate to
   that exact pixel value instead of assuming it. */
.client-marquee-set {
  display: flex;
  align-items: center;
  gap: 2.75rem;
  flex-shrink: 0;
}
/* pause to actually read a name — cheap, and the loop math holds
   regardless of when it's paused since the shift is a fixed value */
.client-marquee:hover .client-marquee-track { animation-play-state: paused; }
@keyframes client-marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(var(--marquee-shift)); }
}
.client-logo {
  height: 34px;
  width: auto;
  max-width: 150px;
  object-fit: contain;
  opacity: 0.9;
  flex-shrink: 0;
}

/* ============================================================
   Studio / Who we are
   ============================================================ */
.studio {
  /* shared by the section-head width calc below and the diamond grid,
     so the text column can be sized to match the grid exactly.
     24vw hits its 240px ceiling at any viewport >= ~1000px, making the
     5-diamond row a fixed 5.791*diamond = ~1390px block from there up —
     fine on wide desktops with room to spare, but wider than a 1280px
     (or 1366px) viewport itself, causing a real horizontal scrollbar.
     Confirmed live. The min() here caps diamond size to what actually
     fits the section's own available width (100vw minus its edge
     padding on both sides) whenever that's tighter than 24vw or the
     240px ceiling — everything downstream (the touching-geometry
     translateX multipliers, the section-head width calc) stays
     proportionally correct since they're all still driven by this one
     variable, nothing else was touched. */
  --diamond: clamp(90px, min(24vw, (100vw - 2 * var(--edge)) / 5.791), 240px);
  padding: 6rem var(--edge) 7rem;
  max-width: 1400px;
  margin: 0 auto;
  text-align: center;
}
.studio .section-head {
  margin-left: auto;
  margin-right: auto;
  text-align: center;
  /* as wide as the 5-diamond row itself. The row's true rendered width
     (outer vertex of d1 to outer vertex of d5) is 5.791·diamond — see the
     touching-geometry derivation above .diamond-grid below, which is what
     this coefficient comes from (not a round/eyeballed number). */
  max-width: calc(var(--diamond) * 5.791);
}
.studio .section-head h2 { line-height: 1.16; padding-top: 0.12em; }
.studio .section-intro { margin-left: auto; margin-right: auto; }
.studio-mission {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  /* no max-width of its own — the wider section-head above already
     caps it, and letting it fill that width means fewer line-wraps
     per paragraph, i.e. less vertical space overall */
  max-width: none;
}
.studio-mission-lead {
  color: var(--text);
  font-weight: 700;
  margin-top: 0.5rem;
}

.diamond-grid {
  /* Grid with every diamond stacked in the SAME single cell (grid-area:1/1
     below) rather than flex — a normal flex/grid ROW gives each item its
     own native slot, so any translateX on top of that STACKS with the
     layout's own automatic per-item spacing instead of replacing it (this
     is exactly what happened when this was `display:flex`: each diamond's
     computed translateX was the FULL required offset, added on top of the
     row's own native 1×diamond-per-item spacing, roughly doubling every
     step and pushing d1/d5 off-screen). With every diamond in the same
     cell, all 5 start from the identical base position, so translateX/Y
     below are the ONLY source of position — no native spacing to
     double-count. */
  display: grid;
  /* justify-content centers the single grid TRACK within the container's
     leftover width (equivalent to flex's old justify-content:center for
     "center the whole row on the page"); place-items centers each diamond
     within that shared track/cell. Both are needed — they control
     different things (track position vs. item-within-track position). */
  justify-content: center;
  place-items: center;
  /* d1/d5 are now the highest tiles (raised 0.64·diamond above the row
     baseline, see the cascade below) — clearance above has to cover
     that full rise plus their own rotated-corner poke (≈0.21·S) */
  margin-top: calc(var(--diamond) * 0.85 + 2.5rem);
  /* d3 is now the lowest tile (sits at the row baseline, unshifted) —
     only its own corner poke needs clearing below */
  margin-bottom: calc(var(--diamond) * 0.21);
}
.diamond {
  /* Without this, grid's default auto-flow puts each diamond in its own
     new ROW (no columns/placement defined), stacking them vertically by
     layout before translateY is even applied — that's what produced the
     diagonal-descending row instead of the intended valley shape. Forcing
     all 5 into the exact same cell is what makes translateX/Y the ONLY
     source of position (the actual point of switching to grid). */
  grid-area: 1 / 1;
  width: var(--diamond);
  height: var(--diamond);
  padding: 0;
  transform: rotate(45deg);
  cursor: pointer;
  /* ease-in-out (not plain ease) so both directions — growing in on
     hover and shrinking back out on mouseleave/moving to a neighbor —
     ease smoothly at both ends instead of snapping at the start */
  transition: transform 0.5s ease-in-out;
}
/* TOUCHING GEOMETRY — read this before changing any position/scale number
   below. Each diamond is a square of side `--diamond` (call it S) rotated
   45°. Two rotated squares of scale k1,k2 (sizes k1·S, k2·S), whose centers
   are offset by a FIXED vertical distance m·S (m=0.32, the cascade step)
   and an unknown horizontal distance x·S, touch with exactly zero overlap
   and zero gap when x = (√2/2)·(k1+k2) − m. (Derivation: rotating into the
   square's own unrotated coordinate frame turns "touch a 45°-rotated
   square" into "touch an axis-aligned square," where the classic
   center-to-center touching distance is just the sum of the two half-
   sides — projecting that back through the 45° rotation is where the
   √2/2 and the −m come from.) Every X position and every hover-state scale
   pairing below is this formula evaluated for the relevant k1,k2 — none of
   these decimals are eyeballed. At rest (k1=k2=1): step = √2 − 0.32 =
   1.0942. On hover, only ONE diamond is ever scaled to 1.12 (grown) at a
   time, the rest to 0.92 (shrunk), so only two step sizes ever occur:
   0.92+0.92 → 0.9811 (two shrunk diamonds, pulled slightly CLOSER than
   resting spacing — they're both smaller, so less distance is needed to
   still touch), and 0.92+1.12 → 1.1225 (a shrunk diamond next to the grown
   one, pushed slightly FARTHER than resting spacing). This is why, e.g.,
   hovering d3 pulls d1 and d5 visibly closer to center rather than further
   away — counterintuitive if you're picturing "pushing," but it's what
   zero-overlap-always requires: the grown diamond only needs *its own*
   neighbors pushed back, while every pair of shrunk diamonds needs LESS
   room than they used at full size. translateY (the symmetric cascade,
   each step out from center rises 0.32·diamond) never changes with hover —
   only translateX and scale do. */
.d1 { background: linear-gradient(135deg, #2b2b2b, #4a4a4a); transform: translateX(calc(var(--diamond) * -2.1884)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg); }
.d2 { background: linear-gradient(135deg, var(--teal), var(--teal-deep)); transform: translateX(calc(var(--diamond) * -1.0942)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg); }
.d3 { background: var(--ink); }
.d4 { background: linear-gradient(135deg, var(--sage), var(--sage-deep)); transform: translateX(calc(var(--diamond) * 1.0942)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg); }

/* Hover/focus: grow the hovered diamond, shrink every other one, and
   reposition ALL FIVE along X (see the touching-geometry comment above)
   so every adjacent pair keeps touching with zero overlap/zero gap at the
   new sizes. translateY never changes. One rule per (trigger, target)
   pair — 25 total (5 self + 20 cross) — because the required X position
   for a given diamond depends on which side of the hovered one it's on
   and how many shrunk-shrunk vs. shrunk-grown seams are between it and
   center, which differs for every trigger. IMPORTANT: because every
   diamond shares one grid cell (see grid-area:1/1 above), translateX:0
   means "the shared cell's center," NOT "this diamond's own resting
   spot" — d1's resting spot is -2.1884·diamond, not 0. So a diamond's
   OWN hover rule (the "self, grows" one) must still carry its own resting
   translateX, not omit it — omitting it silently teleports that diamond
   to center on hover instead of growing in place. (This is also why the
   other 20 "someone else is hovered" values below aren't the same numbers
   as the plain touching-step table in the comment above .d1: each set is
   that relative table shifted by the hovered diamond's own resting X, so
   the whole fanned-out row is anchored on the hovered diamond staying
   where it already was, not on the grid's shared center point.) Uses
   :has() so no JS is needed to track which diamond is hovered/focused. */
#diamond-grid:has(.d1:hover, .d1:focus-visible) .d1 {
  transform: translateX(calc(var(--diamond) * -2.1884)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(1.12);
}
#diamond-grid:has(.d1:hover, .d1:focus-visible) .d2 {
  transform: translateX(calc(var(--diamond) * -1.0659)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d1:hover, .d1:focus-visible) .d3 {
  transform: translateX(calc(var(--diamond) * -0.0849)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d1:hover, .d1:focus-visible) .d4 {
  transform: translateX(calc(var(--diamond) * 0.8962)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d1:hover, .d1:focus-visible) .d5 {
  transform: translateX(calc(var(--diamond) * 1.8773)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(0.92);
}

#diamond-grid:has(.d2:hover, .d2:focus-visible) .d1 {
  transform: translateX(calc(var(--diamond) * -2.2167)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d2:hover, .d2:focus-visible) .d2 {
  transform: translateX(calc(var(--diamond) * -1.0942)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(1.12);
}
#diamond-grid:has(.d2:hover, .d2:focus-visible) .d3 {
  transform: translateX(calc(var(--diamond) * 0.0283)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d2:hover, .d2:focus-visible) .d4 {
  transform: translateX(calc(var(--diamond) * 1.0094)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d2:hover, .d2:focus-visible) .d5 {
  transform: translateX(calc(var(--diamond) * 1.9904)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(0.92);
}

#diamond-grid:has(.d3:hover, .d3:focus-visible) .d1 {
  transform: translateX(calc(var(--diamond) * -2.1036)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d3:hover, .d3:focus-visible) .d2 {
  transform: translateX(calc(var(--diamond) * -1.1225)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d3:hover, .d3:focus-visible) .d3 {
  transform: rotate(45deg) scale(1.12);
}
#diamond-grid:has(.d3:hover, .d3:focus-visible) .d4 {
  transform: translateX(calc(var(--diamond) * 1.1225)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d3:hover, .d3:focus-visible) .d5 {
  transform: translateX(calc(var(--diamond) * 2.1036)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(0.92);
}

#diamond-grid:has(.d4:hover, .d4:focus-visible) .d1 {
  transform: translateX(calc(var(--diamond) * -1.9904)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d4:hover, .d4:focus-visible) .d2 {
  transform: translateX(calc(var(--diamond) * -1.0094)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d4:hover, .d4:focus-visible) .d3 {
  transform: translateX(calc(var(--diamond) * -0.0283)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d4:hover, .d4:focus-visible) .d4 {
  transform: translateX(calc(var(--diamond) * 1.0942)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(1.12);
}
#diamond-grid:has(.d4:hover, .d4:focus-visible) .d5 {
  transform: translateX(calc(var(--diamond) * 2.2167)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(0.92);
}

#diamond-grid:has(.d5:hover, .d5:focus-visible) .d1 {
  transform: translateX(calc(var(--diamond) * -1.8773)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d5:hover, .d5:focus-visible) .d2 {
  transform: translateX(calc(var(--diamond) * -0.8962)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d5:hover, .d5:focus-visible) .d3 {
  transform: translateX(calc(var(--diamond) * 0.0849)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d5:hover, .d5:focus-visible) .d4 {
  transform: translateX(calc(var(--diamond) * 1.0659)) translateY(calc(var(--diamond) * -0.32)) rotate(45deg) scale(0.92);
}
#diamond-grid:has(.d5:hover, .d5:focus-visible) .d5 {
  transform: translateX(calc(var(--diamond) * 2.1884)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg) scale(1.12);
}

/* photo diamond: clip to the (unrotated, local) square first, then an
   img exactly filling that square is counter-rotated + scaled up around
   its own center so it reads upright and fully covers the diamond with
   no gaps at the corners. Scale (not oversized width/height percentages)
   is used deliberately — it's applied around the element's own center
   regardless of how the percentages resolve, so coverage is guaranteed
   symmetric on all four sides. */
.diamond-photo {
  position: relative;
  overflow: hidden;
}
.diamond-photo-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* lower Y % = crop window shifts UP the source photo (more headroom
     kept above); lower X % = crop window shifts LEFT the source photo
     (more kept on the left, less cropped off the right). Tuned per
     photo below, since each headshot's framing differs. */
  object-position: 50% 50%;
  transform-origin: center;
  /* scale(1.5) > sqrt(2) ≈ 1.414, the minimum needed for a square to
     fully cover itself after a 45° rotation */
  transform: rotate(-45deg) scale(1.5);
}
.diamond-photo-img-d1 { object-position: 70% 30%; } /* Mizzen */
.diamond-photo-img-d2 { object-position: 50% 20%; } /* Justin */
.d3 .diamond-photo-img { object-position: 30% -5%; } /* Whit Wales */
/* Zeb: 10% more zoom than the shared 1.5 base, crop shifted down 5% */
.diamond-photo-img-d4 { object-position: 50% 45%; transform: rotate(-45deg) scale(1.65); }
.diamond-photo-img-d5 { object-position: 50% -9%; } /* Colin Gosselin */
.d5 {
  background: var(--ink);
  transform: translateX(calc(var(--diamond) * 2.1884)) translateY(calc(var(--diamond) * -0.64)) rotate(45deg);
}

/* ============================================================
   CTA band + contact form
   ============================================================ */
.cta-band {
  /* top edge is a flat, uniform slate so the sage→slate chevron above
     blends into it with no visible seam */
  background: linear-gradient(180deg, var(--slate) 0%, var(--ink) 90%);
  color: var(--text-on-dark);
  padding: 3rem var(--edge);
  /* The fixed .site-header floats over this section once scrolled to via
     the nav's #contact link (same header-overlap issue already fixed for
     Films/Photography — see their own padding-top comment). Reserving
     the header's real measured height as top padding means justify-
     content:center below centers within the space actually visible below
     the header, instead of the "Work with us" eyebrow/heading landing
     partly hidden behind it. */
  padding-top: calc(var(--header-h, 96px) + 1.5rem);
  text-align: center;
  /* contact form + footer together fill the last viewport exactly;
     --footer-h is measured by script.js */
  min-height: calc(100svh - var(--footer-h, 185px));
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.cta-inner { max-width: 640px; margin: 0 auto; width: 100%; }
/* heading/eyebrow now just use the site-wide .section-head recipe (same
   as Studio's "Who we are" / "A small crew, on purpose.") instead of a
   bespoke Gotham Bold treatment, so Contact reads like every other
   section instead of a one-off */
.cta-band .section-head { margin-left: auto; margin-right: auto; }

.contact-form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem 1.5rem;
  text-align: left;
}
.form-field {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.form-field-wide { grid-column: 1 / -1; }
/* Honeypot for contact-handler.php: off-screen (not display:none —
   some bots skip hidden fields entirely, this is harder to detect as
   a trap) rather than visually hidden in place. Real visitors never
   see or reach it (aria-hidden + tabindex="-1" in the markup too). */
.form-field-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.form-field label {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-on-dark);
}
.form-field input,
.form-field select,
.form-field textarea {
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--text-on-dark);
  background: rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.28);
  border-radius: var(--radius);
  padding: 0.8rem 0.9rem;
  min-height: 44px;
  transition: border-color 0.2s ease, background-color 0.2s ease;
}
.form-field input::placeholder,
.form-field textarea::placeholder { color: rgba(255,255,255,0.38); }
.form-field input:hover,
.form-field select:hover,
.form-field textarea:hover { border-color: rgba(255,255,255,0.45); }
.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
  border-color: var(--teal);
  background: rgba(255,255,255,0.1);
  outline: none;
}
.form-field textarea { resize: vertical; min-height: 140px; }
.form-field select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ffffff' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.85rem center;
  background-size: 16px;
  padding-right: 2.4rem;
}
.form-field select:required:invalid { color: rgba(255,255,255,0.38); }
.form-field select option { color: var(--text); }
/* the "Other" org-type text field is only editable once its dropdown
   sibling is set to Other — see the org-type change handler in
   script.js */
.form-field input:disabled,
.form-field select:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  background: rgba(255,255,255,0.03);
}
.form-status {
  grid-column: 1 / -1;
  font-size: 0.9rem;
  color: var(--teal);
  min-height: 1.2em;
}
.contact-form .btn-accent {
  grid-column: 1 / -1;
  justify-self: center;
}

/* ============================================================
   Footer
   ============================================================ */
.site-footer {
  background: linear-gradient(180deg, var(--sage) 0%, var(--slate) 100%);
  color: var(--text-on-dark);
  padding: 1.75rem var(--edge) 1.25rem;
}
.footer-inner {
  max-width: 700px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  text-align: center;
}
.footer-social a { color: var(--text-on-dark); transition: color 0.2s ease; }
.footer-social a:hover { color: var(--teal); }
.footer-email, .footer-phone {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 500;
  color: var(--text-on-dark);
}
.footer-email:hover, .footer-phone:hover { color: var(--teal); }
.footer-mark {
  opacity: 0.9;
  margin-top: 0.5rem;
  width: 36px;
  height: auto;
}
.footer-copy {
  font-size: 0.78rem;
  color: var(--text-on-dark);
  margin-top: 0;
}

/* ============================================================
   Reel modal
   ============================================================ */
.reel-modal {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.reel-modal[hidden] { display: none; }
.reel-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 9, 10, 0.88);
}
.reel-modal-dialog {
  --reel-ratio: 1.7778; /* 16/9 fallback until the real oEmbed ratio loads */
  position: relative;
  z-index: 1;
  /* sized to fit an 80vw × 80vh box while preserving the video's own
     aspect ratio — never cropped, never letterboxed */
  width: min(80vw, calc(80vh * var(--reel-ratio)));
}
.reel-modal-close {
  position: absolute;
  top: -1.25rem;
  right: -1.25rem;
  z-index: 2;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(14, 14, 15, 0.65);
  color: var(--text-on-dark);
  border: 1px solid rgba(255,255,255,0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.2s ease, color 0.2s ease;
}
.reel-modal-close:hover { border-color: var(--teal); color: var(--teal); }
.reel-embed {
  position: relative;
  width: 100%;
  aspect-ratio: var(--reel-ratio);
  background: transparent;
  border-radius: var(--radius);
  overflow: hidden;
}
.reel-embed iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  background: transparent;
}

/* ============================================================
   Photo lightbox
   ============================================================ */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  /* Plain `center` can clip the start of an overflowing centered flex
     item in some engines when the container scrolls — `safe center`
     (supported in current Chrome/Firefox/Safari) keeps it centered when
     it fits and falls back to start-aligned, scrollable-to-the-top
     behavior when it doesn't. Unsupported browsers just ignore this
     line and keep the plain `center` above. */
  align-items: safe center;
  justify-content: center;
  padding: 1.5rem;
  /* Safety net: grid view (see .lightbox-grid below) can end up taller
     than the viewport at some window sizes — this lets the WHOLE modal
     scroll rather than silently clipping whatever doesn't fit, so every
     photo in the grid is always reachable. */
  overflow-y: auto;
}
.lightbox[hidden] { display: none; }
.lightbox-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 9, 10, 0.92);
}
.lightbox-dialog {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 1040px;
  display: flex;
  align-items: center;
  gap: 1rem;
}
.lightbox-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  z-index: 2;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(255,255,255,0.08);
  color: var(--text-on-dark);
  border: 1px solid rgba(255,255,255,0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.2s ease, color 0.2s ease;
}
.lightbox-close:hover { border-color: var(--teal); color: var(--teal); }
.lightbox-arrow {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.4);
  color: var(--text-on-dark);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.2s ease, color 0.2s ease;
}
.lightbox-arrow:hover { border-color: var(--teal); color: var(--teal); }
/* the base rule's `display: flex` otherwise outranks the browser's
   default [hidden]{display:none} (equal specificity, author styles win) */
.lightbox-arrow[hidden] { display: none; }
.lightbox-stage {
  flex: 1;
  min-width: 0;
}
.lightbox-image {
  width: 100%;
  aspect-ratio: 3/2;
  max-height: 72vh;
  border-radius: var(--radius);
  background: linear-gradient(155deg, #4a4a4a 0%, #1c1c1c 130%);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--text-on-dark-soft);
}
/* the base rule's `display: flex` otherwise outranks the browser's
   default [hidden]{display:none} (equal specificity, author styles win)
   — same gotcha as .lightbox-arrow[hidden] above; showGridView() sets
   this element's `hidden` attribute to swap it out for the grid. */
.lightbox-image[hidden] { display: none; }
/* Real-photo mode (a gallery with actual images, e.g. Five Boro Bike
   Tour) — same "agnostic stage + per-photo frame" pattern as
   .team-lightbox-image/.team-lightbox-frame: a transparent, height-bound
   stage hosting one frame per photo, each sized to ITS OWN aspect ratio
   via --img-ratio (set from the loaded image's natural size in script.js)
   rather than every photo being forced into one fixed 3:2 box. */
.lightbox-image--photo {
  aspect-ratio: auto;
  height: min(72vh, 640px);
  background: transparent;
  position: relative;
  overflow: hidden;
}
.lightbox-frame {
  --img-ratio: 1.5;
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(100%, calc(var(--img-ratio) * min(72vh, 640px)));
  aspect-ratio: var(--img-ratio);
  border-radius: var(--radius);
  overflow: hidden;
  opacity: 1;
  transform: translate(-50%, -50%) translateX(0);
  transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.5s ease;
}
.lightbox-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.lightbox-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 1rem;
}
.lightbox-caption {
  font-size: 0.85rem;
  color: var(--text-on-dark);
}
/* Toggles the stage between one photo at a time and a scrollable grid of
   the whole set — only shown for galleries with real photos (a
   placeholder-only item has nothing worth gridding). Label/icon swap in
   script.js's updateViewToggle() rather than two separate buttons, so
   there's one control to wire up instead of keeping two in sync. */
.lightbox-view-toggle {
  flex-shrink: 0;
  font-family: var(--font-display);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-on-dark);
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.4);
  border-radius: 99px;
  padding: 0.5rem 1rem;
  transition: border-color 0.2s ease, color 0.2s ease;
}
.lightbox-view-toggle:hover { border-color: var(--teal); color: var(--teal); }
.lightbox-view-toggle[hidden] { display: none; }

/* Grid view: replaces .lightbox-image in the stage while active — a
   scrollable grid of every photo in the set. Sized generously (not
   pinned to the single-image stage's own min(72vh,640px) footprint,
   since with the image hidden and the arrows gone there's more room to
   work with) so it visually fills the modal rather than looking small
   in a mostly-empty dark overlay; .lightbox's own overflow-y:auto above
   is the real guarantee that the last row is always reachable even if
   this height guess runs long on an unusual viewport. */
.lightbox-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(195px, 1fr));
  gap: 1rem;
  width: 100%;
  /* max-height, NOT height — a definite `height` on a grid container
     that has more content than fits (needing its own overflow-y:auto)
     broke `grid-auto-rows:auto` track sizing outright: confirmed live
     that with a definite height, every row's track computed to roughly
     1/6th its items' real rendered size regardless of whether those
     items sized via `aspect-ratio` or the older padding-percentage
     hack — neither is what was wrong. Switching to `max-height` (grid
     grows naturally with `height:auto` up to the cap, only scrolling
     once content actually exceeds it) fixed row sizing immediately,
     with no other change needed. Don't change this back to a definite
     `height` without re-verifying real photos don't overlap row to row. */
  max-height: min(82vh, 900px);
  overflow-y: auto;
  padding-right: 0.3rem;
}
.lightbox-grid[hidden] { display: none; }
.lightbox-grid-item {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--radius);
  overflow: hidden;
  cursor: pointer;
  margin: 0;
  padding: 0;
  font: inherit;
  box-sizing: border-box;
  background: none;
  border: 2px solid transparent;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.lightbox-grid-item:hover,
.lightbox-grid-item:focus-visible { border-color: var(--teal); transform: scale(1.03); }
.lightbox-grid-item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.lightbox-grid-item.is-current { border-color: var(--teal); }

/* ============================================================
   Team lightbox (Who We Are gallery)
   ============================================================ */
.team-lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.team-lightbox[hidden] { display: none; }
.team-lightbox-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 9, 10, 0.92);
}
.team-lightbox-count {
  position: absolute;
  top: 1rem;
  left: 1rem;
  z-index: 2;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  color: var(--text-on-dark);
}
.team-lightbox-dialog {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 900px;
  display: flex;
  align-items: center;
  gap: 1rem;
}
.team-lightbox-stage { flex: 1; min-width: 0; }
/* A plain, photo-agnostic viewport — it no longer takes on any one
   photo's shape. Each photo gets its own "frame" (below) sized to its
   own ratio, so the box and the image inside it move as one joined
   element during a transition instead of the image sliding around
   inside a container that separately snaps to a new shape. */
.team-lightbox-image {
  /* an invisible bounding stage, not a visible box — it exists only to
     size the modal and clip the sideways slide motion. Its own height
     (min(70vh, 620px)) is the largest space any frame is ever allowed
     to use; every frame below is sized to fit entirely inside both that
     height AND the stage's width, so nothing is ever cropped. */
  position: relative;
  width: 100%;
  height: min(70vh, 620px);
  background: transparent;
  overflow: hidden;
}
.team-lightbox-frame {
  /* plain number (not "W/H"); script.js sets this per-photo from its
     natural size, so the frame's own shape matches that photo exactly */
  --img-ratio: 1.333;
  position: absolute;
  top: 50%;
  left: 50%;
  /* "contain" math, not a clamp on one axis alone: this is the width the
     frame would need at full stage height, capped by the stage's own
     width — whichever is smaller wins, and height always follows via
     aspect-ratio, so both dimensions shrink together and the full photo
     is always visible, never cropped */
  width: min(100%, calc(var(--img-ratio) * min(70vh, 620px)));
  aspect-ratio: var(--img-ratio);
  border-radius: var(--radius);
  overflow: hidden;
  opacity: 1;
  transform: translate(-50%, -50%) translateX(0);
  transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.5s ease;
}
.team-lightbox-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.team-lightbox-caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* a slim footer strip, not a panel — hard cap + internal scroll so it
     can never grow past this however long a bio gets. The prev/next
     arrows are flex siblings of .team-lightbox-stage, positioned
     independently of this box's height, so raising this cap doesn't
     creep toward them — it only trades against how much of the photo
     stays visible within the stage's own fixed height. Paired with the
     length-aware font-size on .team-lightbox-bio below so the two
     longest bios (Whit, Colin) fit without needing the scroll this
     overflow is otherwise a safety net for. */
  max-height: 32%;
  overflow-y: auto;
  /* scroll still works via wheel/touch/drag — just no visible track/thumb */
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding: 0.75rem 1.5rem 0.65rem;
  background: rgba(8, 9, 10, 0.62);
  backdrop-filter: blur(3px);
  text-align: center;
  /* the prev/next arrows sit outside this box entirely (siblings in the
     dialog's flex row), so pointer-events can stay enabled here — needed
     for the internal scroll (overflow-y: auto above) to actually work */
  touch-action: pan-y;
}
.team-lightbox-caption::-webkit-scrollbar { display: none; }
.team-lightbox-name {
  font-family: var(--font-body);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.85rem;
  color: var(--text-on-dark);
  margin: 0 0 0.15rem;
}
.team-lightbox-role {
  font-family: var(--font-body);
  font-weight: 300;
  letter-spacing: 0.04em;
  font-size: 0.75rem;
  color: var(--text-on-dark);
  margin: 0 0 0.35rem;
}
.team-lightbox-role:empty { display: none; margin: 0; }
.team-lightbox-bio {
  /* --bio-len (set in script.js on every caption update) is the current
     bio's character count. Today's shortest real bio (Justin, 480 chars)
     stays at the full 0.76rem; length beyond that shrinks continuously
     down to a 0.64rem floor, rather than a hand-tuned per-person cutoff —
     generalizes to any future bio length instead of being calibrated to
     today's longest (Whit, 838 chars). */
  --bio-len: 0;
  font-size: clamp(0.64rem, calc(0.76rem - max(0, var(--bio-len) - 480) * 0.00028rem), 0.76rem);
  line-height: 1.42;
  color: var(--text-on-dark);
  /* use the full width of whatever frame the current photo has, rather
     than an arbitrary character cap — more chars per line means fewer
     lines, which is what actually avoids needing to scroll */
  max-width: 100%;
  margin: 0 auto;
}
.team-lightbox-bio:empty { display: none; margin: 0; }

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 860px) {
  .main-nav {
    /* Full-page covering menu, not a narrow side panel — inset:0 (not
       0 0 0 auto) + width:100% takes the whole viewport. Centered
       vertically via flex now that there's a full screen of height to
       work with, rather than the old fixed 6.5rem top padding, which was
       sized to clear the header for a slim panel that started near the
       top — that anchoring reads oddly once the menu owns the whole
       screen. */
    position: fixed;
    inset: 0;
    width: 100%;
    background: var(--ink);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: 2rem;
    transform: translateX(100%);
    transition: transform 0.3s ease;
    z-index: 400;
  }
  .main-nav.is-open { transform: translateX(0); }
  /* .brand and .header-right are #main-nav's own SIBLINGS inside
     .header-inner (not ancestors) — flex items with no z-index of their
     own paint at stack level 0, which sits BELOW any sibling with an
     explicit positive z-index, so #main-nav's z-index:400 would paint
     over them regardless of DOM order. Never visible with the old narrow
     side panel (no geometric overlap with the logo/toggle button), but
     the full-page panel covers exactly where they sit. Explicit z-index
     above main-nav's keeps the logo and close (X) button visible and
     clickable while the menu is open. */
  .brand, .header-right { position: relative; z-index: 401; }
  .main-nav ul { flex-direction: column; align-items: center; gap: 1.5rem; }
  .main-nav a { font-size: 0.95rem; display: inline-block; }
  .main-nav a.is-active { border-bottom-color: var(--teal); }
  .nav-indicator { display: none; }
  .menu-toggle { display: flex; }
  .header-right .social-row { display: none; }
  /* mobile keeps a solid header at all times, and MUST explicitly reset
     backdrop-filter to none (not just omit it) — backdrop-filter makes an
     element the containing block for its own position:fixed descendants,
     and #main-nav is a position:fixed DESCENDANT of .site-header at this
     breakpoint. Omitting the property here doesn't clear it: the desktop
     rule's backdrop-filter (blur(calc(var(--hdr) * 6px))) still cascades
     in, and even blur(0px) at --hdr:0 counts as a non-"none" value for
     this purpose. With any non-none backdrop-filter present, #main-nav's
     inset:0 0 0 auto resolved against the slim header bar's own ~136px
     height instead of the viewport, collapsing the slide-in panel down to
     the header's height — its background painted correctly within that
     collapsed box, but the actual link list (taller than 136px, nothing
     clips it) overflowed straight past it onto bare hero content below,
     which is what read as "transparent." The header is already a solid
     92%-opacity background on mobile, so the blur was doing very little
     visually anyway — removing it costs nothing. */
  .site-header { background: rgba(14,14,15,0.92); backdrop-filter: none; }

  .menu-toggle.is-active span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  .menu-toggle.is-active span:nth-child(2) { opacity: 0; }
  .menu-toggle.is-active span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
}

@media (max-width: 640px) {
  .hero-inner { padding-top: 6.5rem; }
  .films, .photography, .studio { padding-left: var(--edge); padding-right: var(--edge); }
  .testimonial-slider { flex-direction: row; gap: 0.75rem; }
  /* the arrows now sit below the rail (.film-controls), not flanking it,
     so the width:calc(70%/0.82) formula on .film-carousel needs no
     mobile override — it already nets the card out to 70% of the page
     at any breakpoint without anything else in the row to subtract. */
  .film-carousel { gap: 1rem; }
  /* .film-track's ROW cross-axis default is align-items:stretch, which
     was always harmless before this breakpoint's caption-below-
     thumbnail redesign because .film-card still had a fixed
     aspect-ratio here too (a definite height is immune to stretch).
     Once .film-card switches to aspect-ratio:auto below (height now
     driven by its own content — media + caption), stretch started
     inflating EVERY card in the row (including off-screen clones) to
     match the row's single tallest item, which itself only got that
     tall via the same stretch feeding back into a vertical card's
     media sizing — confirmed live: every card measured an identical,
     wildly inflated ~547px, and .films overflowed the viewport by
     ~230px, pushing the prev/next arrows below the fold. Each card
     sizing to its own natural content height instead fixes both. */
  .film-track { align-items: flex-start; }
  .film-card-play { width: 36px; height: 36px; }
  .film-card-play svg { width: 13px; height: 13px; }
  /* Caption moves from an overlay on the thumbnail to plain text below
     it — a portrait (9:16) card's overlaid title/tag badly wrapped
     against its much-narrower image at this width. .film-card becomes a
     centered column (thumbnail, then caption) instead of relying on
     .film-card-media absolutely filling it; media becomes a normal
     top-to-bottom flex child sized by its own aspect-ratio (moved off
     .film-card, whose own aspect-ratio/max-height no longer apply now
     that total card height is media + caption, not one fixed ratio).
     Horizontal cards keep the existing 82%-of-rail flex-basis unchanged
     — their captions always comfortably fit under that width. Vertical
     cards switch to flex:0 0 auto so plain flexbox column sizing
     ("width = widest child") sizes the card to whichever is wider — the
     9:16 thumbnail or the tag/title text — centering the narrower one
     with even space on both sides, per an explicit design request; no
     JS text measurement needed; .film-card-body below is what makes
     this work (a flex column with align-items:center shrink-wraps to
     its widest child by default). A defensive max-width on .film-card
     (below) keeps a long title from ever growing a card past the rail,
     which would swallow the peeking-neighbor fade this carousel relies
     on — confirmed live that a real title ("TD Garden Customer Benefits
     Social") needs ~97% of the rail's own width on one line at this
     font size, so capping the card and truncating the title with
     ellipsis (the first approach here) was cutting off real, non-
     pathological titles; per an explicit choice, the tag stays a single
     truncated line (short, controlled vocabulary — "Corporate-Social"
     etc.) but the title now wraps to as many lines as it needs instead
     of ever being cut off. The vertical thumbnail's own pixel
     height/width are still set inline by sizeVerticalCards() in
     script.js, same idea as desktop, just measured against a sibling's
     .film-card-media height instead of a sibling .film-card's own
     height (see that function's mobile branch). */
  .film-card {
    aspect-ratio: auto;
    max-height: none;
    /* Matches horizontal cards' own 82%-of-rail flex-basis exactly, so
       peek amount is consistent across card types — measured live that
       92% left only ~11px of peek per side on a wrapped-title vertical
       card (vs. ~26px for a horizontal one), most of which sat behind
       the rail's own 40px fade overlay and was barely perceptible. */
    max-width: 82%;
    overflow: visible;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  /* The base (desktop) rule below sets aspect-ratio:9/16 on
     .film-card.is-vertical (2-class specificity) — .film-card's own
     aspect-ratio:auto reset just above this is only 1-class specificity,
     so it NEVER actually overrode the base rule for vertical cards.
     Height kept computing as width × 16/9 regardless of actual content
     (confirmed via an isolated local repro, not a browser bug or a
     flexbox/wrapping-text quirk as previously suspected) — a single
     lone .film-card.is-vertical with nothing but a one-line title still
     rendered ~400px tall, exactly matching that math. Must explicitly
     reset aspect-ratio here too, at matching specificity, or this silently
     comes back the next time anything else here changes. */
  .film-card.is-vertical { flex: 0 0 auto; aspect-ratio: auto; }
  .film-card-media {
    position: relative;
    inset: auto;
    width: 100%;
    aspect-ratio: 16 / 9;
    border-radius: var(--radius);
    overflow: hidden;
  }
  .film-card.is-vertical .film-card-media { width: auto; aspect-ratio: 9 / 16; }
  .film-card-media::after { display: none; }
  .film-card-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: auto;
    max-width: 100%;
    padding: 0.6rem 0 0;
    color: var(--text);
    /* align-items:center above only centers each line's own BOX (already
       as narrow as its own text since each line shrink-wraps) — once the
       title wraps to a second, shorter line, that line's actual text
       still needs its own text-align:center or it sits flush left under
       the longer first line instead of centered beneath it. */
    text-align: center;
  }
  .film-card-tag {
    white-space: nowrap;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--text-soft);
  }
  .film-card-title {
    max-width: 100%;
    color: var(--text);
  }
  .slide-arrow { width: 38px; height: 38px; }
  .reel-modal-close { top: 0.6rem; right: 0.6rem; }
  /* Mobile is 2 columns x 3 rows (not desktop's 3x2) — the desktop
     formula above divides by 2 rows and reserves 1 row-gap; re-derive
     the same budget for 3 rows / 2 gaps here rather than reusing a
     divisor that would understate how much room 3 rows actually need.
     Column width is min(cap, 1-gap-of-2-columns-share) — same reasoning
     as the desktop rule above, just N=2 (1 gap between 2 columns, not
     2 gaps between 3). */
  .photo-rail {
    grid-template-columns: repeat(2, min(var(--photo-card-max), calc((100% - var(--photo-gap)) / 2)));
    width: 90%;
    --photo-card-max: clamp(110px, calc((100svh - var(--header-h, 96px) - 1.5rem - var(--photo-head-h, 130px) - 1.75rem - 2.75rem - 3rem) / 3), 260px);
  }
  /* Films' 4 tabs (Corporate/Academic/Non-Profit/Previous Reels) don't
     fit one row at phone widths at the desktop type scale — rather than
     scrolling (Reels fell off-screen with no visual hint there was more)
     or wrapping to a 2nd row (steals height from a section already tuned
     to fit above the fold), all 4 stay on one row at a tightened type
     scale, and "Previous Reels" swaps to its shorter "Reels" label — the
     only one long enough to need it. Verified live down to 320px (iPhone
     SE) with no wrap/overflow; see EF_website guide.md for the 3
     alternatives this was chosen over. */
  .films .tabs {
    flex-wrap: nowrap;
    justify-content: center;
  }
  .tab {
    white-space: nowrap;
    font-size: 0.7rem;
    letter-spacing: 0.05em;
  }
  .films .tabs { gap: 0.65rem; }
  .tab-label-full { display: none; }
  .tab-label-short { display: inline; }
  .cta-band { padding: 2.5rem var(--edge); }
  .contact-form { grid-template-columns: 1fr; }
  .footer-inner { gap: 0.9rem; }
  .lightbox-dialog { gap: 0.5rem; }
  .lightbox-arrow { width: 34px; height: 34px; }
  .lightbox-image { font-size: 0.9rem; }
  /* Side-by-side caption + "View Gallery" button (desktop) squeezes the
     caption into a narrow column that wraps to several lines and
     collides with the button once the dialog itself is this narrow —
     confirmed live at 375px. Stack them instead. */
  .lightbox-footer { flex-direction: column; gap: 0.6rem; }
  .lightbox-caption { text-align: center; }
  .lightbox-grid { grid-template-columns: repeat(auto-fill, minmax(144px, 1fr)); gap: 0.6rem; max-height: min(75vh, 900px); }
  .studio { --diamond: clamp(48px, 13vw, 90px); }
  .team-lightbox-dialog { gap: 0.5rem; }
  /* .team-lightbox-caption's max-height (32%, see the desktop rule) was
     sized against how many lines the longest bios wrap to at desktop
     widths. The same text wraps to more/shorter lines in a narrower
     column, so the fixed percentage isn't enough room at phone widths —
     confirmed live: Whit's bio (the longest) still needed to scroll at
     428px even though it fit at 1280px. Claim more of the image's height
     here since there's less width to spread text across regardless. */
  .team-lightbox-caption { max-height: 46%; }
  .hero, .testimonials, .section-chevron { --chev: 34px; }
  /* .films' min-height calc reads --hero-chev directly (it has no
     .section-chevron-hero class of its own to override), so it needs
     updating here too or it'd assume the desktop-sized chevron */
  :root { --hero-chev: 34px; }
  .client-logo { height: 26px; }
}

@media (max-width: 400px) {
  :root { --edge: 1.25rem; }
}

/* Short phones (e.g. 375x667): .team-lightbox-image's min(70vh, 620px)
   cap resolves against 70vh here, well under the 620px ceiling — a
   shorter image means the caption's percentage-based max-height (46%,
   sized for the ~926px-tall phones the previous rule targeted) is no
   longer enough room for the same bio text at the same font size.
   Confirmed live: Whit's bio (longest, 838 chars) still needed to
   scroll at 375x667 even after the 428x926 fix. Claim more of the
   (already-shorter) image height here rather than shrinking the font
   further, which would hurt readability more than showing less photo. */
@media (max-width: 640px) and (max-height: 700px) {
  .team-lightbox-caption { max-height: 68%; }
}
