/* ============================================================
   Core — what every page shares: fonts, tokens, reset, type scale (with the hover reveal),
   header, mobile menu, furniture bar, the <main> grid, the cursor label, inner-page layout,
   the work grid, the mission statement (home and Social Studio), the footer, the team card,
   the page transition, the gates, reduced motion and the shared phone rules.
   Every page links this first, then its own sheet(s).
   Split out of the former style.css on 2026-10-18; rules unchanged, in their
   original order. ============================================================ */

/* ============================================================
   Fonts
   ============================================================ */

@font-face {
  font-family: 'Epilogue';
  src: url('../fonts/Epilogue-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Epilogue';
  src: url('../fonts/Epilogue-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Epilogue';
  src: url('../fonts/Epilogue-SemiBold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Libre Caslon Display';
  src: url('../fonts/LibreCaslonDisplay-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Libre Caslon Text';
  src: url('../fonts/LibreCaslonText-Italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

/* Body cut — Libre Caslon Text Regular */
@font-face {
  font-family: 'Libre Caslon Text';
  src: url('../fonts/LibreCaslonText-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Libre Caslon Text';
  src: url('../fonts/LibreCaslonText-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}


/* ============================================================
   Tokens
   ============================================================ */

/* Theme. Two grounds, one of them current: --bg/--fg is the theme's — dark by
   default, and html[data-theme="light"] flips it (theme.js: the circle at the
   left end of the furniture bar, remembered in localStorage and applied by each
   page's head script before first paint) — and --bg-inverse/--fg-inverse is the
   opposite ground, for the few things that sit on it (the intro's loader, the
   filter chips). Every page and panel paints with these; nothing hardcodes a
   ground any more, so light is a true mirror of dark. Media stays media: the
   case-study player and the card viewer are black with white type on either. */
html[data-theme="light"] {
  --bg: #F3F3F3;
  --fg: #080808;
  --bg-inverse: #080808;
  --fg-inverse: #FFFFFF;
  --art-invert: 1;                     /* the white artwork (logos, wordmark) goes dark on the light ground */
  --theme-bg: #F3F3F3;
  --theme-fg: #080808;
  --theme-bg-inverse: #080808;
  --theme-fg-inverse: #FFFFFF;
  --theme-art-invert: 1;
}

/* Inverted pages (the user's call, 2026-09-25): All Work and Post Production
   run on the opposite ground — light while the site is dark, dark while it
   is light (the team member page was inverted for an hour and put back). A
   swap of the same tokens on <html> (so the overscroll matches too), nothing
   else: every component paints with --bg / --fg, and the artwork follows
   --art-invert. */
html:has(:is(body.work-all, body.post-page)) {
  --bg: #F3F3F3;
  --fg: #080808;
  --bg-inverse: #080808;
  --fg-inverse: #FFFFFF;
  --art-invert: 1;
}
html[data-theme="light"]:has(:is(body.work-all, body.post-page)) {
  --bg: #080808;
  --fg: #FFFFFF;
  --bg-inverse: #F3F3F3;
  --fg-inverse: #080808;
  --art-invert: 0;
}
/* Social Studio follows the theme like every other page (user, 2026-10-14;
   until then it was pinned black on either theme, from before the toggle
   came back on 2026-10-06). Its media — the hero cards, loops, stills — are
   content and keep their own colours. */
/* What opens over a page keeps the theme's ground, not the page's: the phone
   menu and the About panel read the --theme-* copies, so over All Work or Post
   Production they stay as they are everywhere else. --muted / --hairline are
   re-derived from the panel's ink. The header, which stays up over the About
   panel (2026-10-22), takes the panel's colours while it is open. */
.mobile-menu,
.about-modal,
html.about-open .site-header {
  --bg: var(--theme-bg);
  --fg: var(--theme-fg);
  --bg-inverse: var(--theme-bg-inverse);
  --fg-inverse: var(--theme-fg-inverse);
  --art-invert: var(--theme-art-invert);
  --muted: color-mix(in srgb, var(--fg) 55%, transparent);
  --hairline: color-mix(in srgb, var(--fg) 18%, transparent);
}

:root {
  /* Colors — Figma color styles (color/bg/dark, color/bg/light, color/text/white) */
  --bg: #080808;
  --fg: #FFFFFF;
  --bg-inverse: #F3F3F3;
  --fg-inverse: #080808;
  --art-invert: 0;                     /* 1 = invert the white artwork (light ground) */
  /* The theme's own ground, untouched by an inverted page — for what opens
     OVER a page (the panels below) and must keep the site's colours there. */
  --theme-bg: #080808;
  --theme-fg: #FFFFFF;
  --theme-bg-inverse: #F3F3F3;
  --theme-fg-inverse: #080808;
  --theme-art-invert: 0;
  --muted: color-mix(in srgb, var(--fg) 55%, transparent);    /* derived: secondary text */
  --hairline: color-mix(in srgb, var(--fg) 18%, transparent); /* derived: faint rules / dividers */

  --ff-sans:  'Epilogue', system-ui, sans-serif;
  --ff-serif: 'Libre Caslon Display', ui-serif, Georgia, serif;      /* headlines, section heads */
  --ff-serif-text: 'Libre Caslon Text', ui-serif, Georgia, serif;    /* Body (needs Regular woff2) */
  --ff-serif-italic: 'Libre Caslon Text', ui-serif, Georgia, serif;  /* director credits, caption italic */
  --ff-editorial: 'PP Editorial New', 'Libre Caslon Text', ui-serif, Georgia, serif;   /* Social Studio intro: counter + italic words (ultralight italic only) */

  /* Type scale line-heights — Figma text styles. Sizes live on the .t-* utilities below. */
  --lh-h1: 1;       /* H1 */
  --lh-h2: 1;      /* H2 */
  --lh-cap-i: 1.2; /* Caption · Italic */
  --lh-body: 1.4;  /* Label, Caption · Normal, Body */
  --lh-link: 1.6;  /* link */

  /* Underline — proportional to font size (Figma: 2% thickness / 10% offset;
     was 5% thickness until 2026-09-24). Every underline on the site reads these. */
  --ul-thickness: 0.02em;
  --ul-offset: 0.1em;

  /* The two fluid display scales, interpolated between the Figma mobile (390px)
     and desktop (1920px) anchors. H1/H2 are the ONLY type that scales with the
     viewport. Tokens because several components need the same value — they used
     to carry copies of these clamps and could drift apart. */
  --type-h1: clamp(48px, calc(27.6px + 5.23vw), 128px);   /* 48 → 128 */
  --type-h2: clamp(28px, calc(22.9px + 1.31vw), 48px);    /* 28 → 48  */

  /* Motion. --blur-reveal is the depth of the site's signature reveal and is
     read by motion.js, so the value lives here for both CSS and JS.
     --ease-out-expo is the CSS twin of GSAP's expo.out, so a transition and a
     tween of the same gesture actually match. */
  --blur-reveal: 5px;
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);

  --pad-inline: 10px;
  /* The phone's safe areas (the home indicator, the rounded corners, the notch
     in landscape): the pages declare viewport-fit=cover, so the fixed chrome
     adds these to its own margins and everything that clears the chrome reads
     them through --furniture-clear and --nav-h (user, 2026-10-14). */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  /* The fixed furniture bar's own height: its 12px line (×1.6 leading) plus its
     10px bottom padding. */
  --furniture-h: 32px;
  /* Breathing room between the end of page content and the top of the bar.
     The single knob for how much air every page ends on. Halved on mobile
     (below) — 40px reads generous on a 1920 desktop and oversized on a 390
     phone, where it is a tenth of the screen width. */
  --furniture-gap: 40px;
  /* What page content reserves at the foot, site-wide: the bar's own height
     plus that gap. Every page bottom reads from this, so the bar's height and
     the space under content can never drift apart. */
  --furniture-clear: calc(var(--furniture-h) + var(--furniture-gap) + var(--safe-bottom));

  /* Hero lockup size. The footer wordmark is set to this same value so the two
     stay locked together — change it here and both follow. */
  --hero-title-size: 8.6vw;                          /* 192px @ 1920 */
  --marquee-h: clamp(108px, 13.2vw, 192px);
  --logo-w: 70px;                                   /* RTW lockup — CONTACT matches it */
  --nav-h: calc(46px + var(--safe-top));             /* 26px logo + header's 10px top/bottom padding, plus the status bar / notch it now sits under */
  --marquee-gap: clamp(6px, 0.6vw, 12px);

  /* Spacing scale — Figma. Only the steps actually in use are kept. */
  --space-8: 8px;
  --space-20: 20px;
  --space-40: 40px;

  /* Work grid — 12 × 6, 8px gutter. Height scales with viewport
     (760px @ 1920 → 39.6vw) so the whole grid adapts to width. */
  --work-cols: 12;
  --work-rows: 6;
  --work-gap: var(--space-8);   /* 8px */
  --work-row-h: 39.6vw;

  /* Page rhythm (2026-10-18) — the one set of distances every page head and
     grid reads, so Work, Post and Directors & Creators cannot drift apart:
       --page-top   header → the page's first label (80px @ 1920)
       --head-gap   label → headline, and the beats inside a masthead
                    (trigger row → chips → dropdown)
       --head-foot  the head block → the content under it (160px @ 1920)
       --list-gap   between the values of a list, rows and columns alike
       --card-gap   between the cards of a work grid on phones (desktop grids
                    set their own, in vw)
     Phone values in the block below. */
  --page-top: 4.17vw;
  --head-gap: 40px;
  --head-foot: 8.33vw;
  --list-gap: 10px;
  --card-gap: 8.33vw;
}
/* Mobile ends on less air: 40px is a tenth of a 390 screen. Overriding the gap
   rather than the reserve keeps --furniture-clear the one thing pages read. */
@media (max-width: 48em) {
  :root {
    --furniture-gap: 20px;
    --page-top: 15.38vw;      /* 60px @ 390 */
    --head-gap: 20px;
    --head-foot: 15.38vw;     /* 60px @ 390 */
    --card-gap: 60px;         /* 60 on every phone grid (user, 2026-10-18 evening; was 40 that afternoon) */
  }
}



/* ============================================================
   Reset
   ============================================================ */

*,
*::before,
*::after { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

html {
  -webkit-text-size-adjust: 100%;
  cursor: crosshair;                 /* crosshair is the default site cursor */
  /* On iOS, clipping on body alone doesn't stop a sideways pan when anything is
     wider than the screen (QA: captions cut off at the left) — clip at the root
     too and refuse the horizontal overscroll. */
  overflow-x: clip;
  overscroll-behavior-x: none;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--ff-sans);
  font-weight: 400;
  letter-spacing: -0.01em;             /* -1% default; hero overrides to -3% */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
  min-height: 100svh;
}

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

/* Links use the regular pointer cursor on hover; everything else keeps
   the site-wide crosshair set on <html>. */
a:hover {
  cursor: pointer;
}
/* Same for the things that behave like links without being <a>: the team pages'
   names (hover targets, not links — there is no person page) and the clock's
   city buttons. The other serif list rows set it on their own rules. */
.clock-city {
  cursor: pointer;
}
@media (hover: hover) and (min-width: 64.0625em) {
  .axis-name { cursor: pointer; }     /* no hover effect below desktop, so no hand either */
}

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

img, svg {
  display: block;
  max-width: 100%;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;             /* Safari/Chrome: prevent dragging the image off-page */
  -webkit-touch-callout: none;         /* iOS Safari: suppress long-press image menu */
}


/* ============================================================
   Type scale — utility classes mirroring the Figma text styles.
   The hero <h1> and loader text are CUSTOM sizes (set elsewhere);
   everything else adheres to these. "Bold" maps to Epilogue 600
   (heaviest weight loaded).
   ============================================================ */

/* ---- The two shared recipes -------------------------------------------
   Declared once here with every consumer listed, so a family, weight or
   tracking change lands everywhere at once. Components below keep only what
   genuinely differs between them: size, line-height and text-transform. */

/* DISPLAY — Libre Caslon, tight tracking. H1/H2 scale with the viewport
   (var(--type-h1) / --type-h2); everything else here is fixed. */
.t-h1,
.t-h2,
.t-h2--mobile,
.t-display,
.t-display-sm,
.mission-text,
.footer-list,
.mobile-menu-nav,
.mobile-menu-list,
.page-title,
.page-lede,
.work-cta a,
.work-prose,
.work-credit-value,
.work-overview,
.work-facets-intro,
.work-facet-list,
.work-chip,
.post-services .post-list,
.axis-list {
  font-family: var(--ff-serif);
  font-weight: 400;
  letter-spacing: -0.03em;
}

/* UI LABEL — Epilogue Medium. Sizes vary (12px / 10px / 9px), so they stay
   on the components. */
.t-h4,
.t-caption,
.t-label,
.nav,
.nav-contact,
.menu-toggle,
.subnav,
.corner,
.footer-ticker,
.footer-social,
.mobile-menu-head,
.page-eyebrow,
.page-note {
  font-family: var(--ff-sans);
  font-weight: 500;
}

/* ---- Utilities --------------------------------------------------------- */
.t-h1 {                       /* H1 · 48→128 · 95% */
  font-size: var(--type-h1);
  line-height: var(--lh-h1);
}
.t-h2 {                       /* H2 · 28→48 · 100% */
  font-size: var(--type-h2);
  line-height: var(--lh-h2);
}
/* The Figma "H2/Mobile" text style used at its fixed size at every width —
   where a desktop board sets type in the mobile style (the person page's
   Video / Stills tabs). Same recipe as .t-h2 (the DISPLAY group above); only
   the scale is pinned to the 390 anchor. */
.t-h2--mobile {               /* H2/Mobile · 28 · 100% */
  font-size: 28px;
  line-height: var(--lh-h2);
}
/* The page headline (2026-10-18): the Post title, the Work triggers — 72px at
   1920, 28px on a phone. One size, so the pages' heads match. Listed here
   rather than classed in the markup where the element already has a name. */
.t-display,
.post-hero-title,
.work-filter-btn {
  font-size: 3.75vw;          /* 72px @ 1920 */
  line-height: 1;
}
/* …and its small companion: the Work chips and dropdown values, always the
   same size as each other (user, 2026-10-18). */
.t-display-sm,
.work-chip,
.work-facet-list {
  font-size: 28px;
  line-height: 1;
}
@media (max-width: 48em) {
  .t-display, .post-hero-title, .work-filter-btn { font-size: 7.18vw; }   /* 28px @ 390 */
  .t-display-sm, .work-chip, .work-facet-list { font-size: 5.13vw; }       /* 20px @ 390 */
}

/* ---- Page head (component) -------------------------------------------------
   A page's opening stack — the small label, then the headline (or the Work
   triggers) — starting --page-top under the header with --head-gap between
   its rows. What follows it stands --head-foot below (the page sets that on
   its own container, as Post's gap and Work's padding do). */
.page-head {
  display: flex;
  flex-direction: column;
  gap: var(--head-gap);
  padding-top: var(--page-top);
}

/* ---- Count (recipe) --------------------------------------------------------
   A parenthesised superscript count after a word — "Director (3)", "All (42)"
   — on the Work triggers and dropdown values. `.keep` wraps the count with the
   word it belongs to, so "Photography + Production (2)" never breaks between
   the two (user, 2026-10-18). */
.keep { white-space: nowrap; }
.count {
  display: inline-block;
  font-size: 0.645em;
  margin-left: clamp(6px, 0.11em, 8px);   /* 8px at 72px, 6px at 28px */
}
.count::before { content: '('; }
.count::after  { content: ')'; }
.t-label {                     /* Label · Epilogue Medium · 12/160 · UPPERCASE
                                  Family + weight come from the UI LABEL group
                                  above. 1.6 (not --lh-body's 1.4) matches
                                  .about-label and Figma's link/Regular token. */
  font-size: 12px;
  line-height: 1.6;
  text-transform: uppercase;
}
.t-h4 { /* alias — kept for existing markup */
  font-size: 12px;
  line-height: var(--lh-body);
  text-transform: uppercase;
}
.t-body {                     /* Body · Libre Caslon Text · 14/140 · −2% */
  font-family: var(--ff-serif-text);
  font-weight: 400;
  font-size: 14px;
  line-height: var(--lh-body);
  letter-spacing: -0.02em;
}
.t-caption {                  /* Caption · Epilogue Medium · 10/140 · UPPERCASE */
  font-size: 10px;
  line-height: var(--lh-body);
  text-transform: uppercase;
}
.t-caption-italic {           /* Caption · Italic · Libre Caslon Text Italic · 12/120 */
  font-family: var(--ff-serif-italic);
  font-style: italic;
  font-weight: 400;
  font-size: 12px;
  line-height: var(--lh-cap-i);
}
/* UNDERLINE — one declaration, every consumer listed. Was repeated at 7 sites,
   which meant --ul-offset/--ul-thickness could be honoured inconsistently.
   (The mobile `.work-cta a` keeps its own copy: it lives inside a media query,
   so it can't join a base rule without applying at every width.) */
/* After the intro, furniture rests visible through this class (hero.js clears the
   GSAP inline opacity on complete), so the :hover / .is-current dim rules just
   below can win — inline opacity:1 would otherwise beat every :hover here. Placed
   before that rule so the equal-specificity .nav-contact:hover wins on order. */
.intro-done [data-furniture] { opacity: 1; }

.t-link--underline,
.nav a:hover,
.nav-contact:hover,
.subnav a:hover,
.corner a:hover,
.footer-social a:hover,
.mission-cta:hover,
.work-cta a:hover,
.work-btn:hover .work-btn-label,
.work-filter-btn:hover,
.work-v2 .work-facet-list button:hover,
.work-v2 .work-filter-btn[aria-expanded="true"],
.nav a.is-current,
.nav-contact.is-current,
.clock-city.is-active,
.page-links a:hover,
.site-footer a:hover,
.sl-cta:hover {
  text-decoration: underline;
  text-underline-offset: var(--ul-offset);
  text-decoration-thickness: var(--ul-thickness);
  opacity: 30%;
}

/* PRESS ARROW — the press rows' hover (press-arrow.js): the drawn Back / Next
   arrow pointing up and right, just to the right of the link, there the
   instant the pointer is on it and gone the instant it leaves (user,
   2026-10-18: no blur). Sized in em so it follows the
   list's type; the link's own width never changes, the arrow sits in the gap. */
.press-arrow {
  position: absolute;
  left: calc(100% + 0.35em);
  top: 50%;
  width: 0.9em;
  height: 0.5em;
  translate: 0 -50%;
  rotate: -45deg;
  color: currentColor;
  pointer-events: none;
  opacity: 0;                        /* instant, no blur (user, 2026-10-18) */
}
.press-arrow-stroke { display: block; width: 100%; height: 100%; }
a:has(> .press-arrow) { position: relative; }
@media (hover: hover) {
  a:hover > .press-arrow { opacity: 1; }
}

/* HOVER REVEAL — the site's one treatment for a list whose rows show an image
   on hover (hover-bleed.js initBleed; made site-wide 2026-10-06 — the user's
   call, from the Directors & Creators card). The row's still comes up as a
   16:9 card centred on the viewport, on the middle six columns of the 12-col /
   10px-gutter grid (columns 4–9: six columns and five gutters wide, which on a
   page inset --pad-inline each side is half the page less 15px), OVER the
   list: every other row drops to 10%, and the hovered row alone comes up over
   the card, opaque and underlined (z 2 against the card's 1). Nothing outside
   the list changes. Instant both ways — visibility, not an opacity fade — so
   the still is there in the same frame the pointer arrives, and gone the
   moment it is off a row.

   Consumers: the homepage footer directory, the Post page's services, the
   Directors & Creators roster, and the Social Studio services (the portrait
   variant below). The contract: the layer is .hover-bleed, prepended to the
   list's own section where the list sits inside a stacking context; the root
   carries data-hover-root and .is-hovering; rows carry data-still and
   .is-active — all set by the module, none by hand. Desktop pointers only,
   gated on the same query the module checks live, so a stale class can never
   style anything at phone width. */
.hover-bleed {
  position: fixed;
  inset: 0;
  container-type: size;                /* so 50cqw below means half the LAYER's width, in height: too */
  z-index: 1;                          /* above the rows' auto, under the furniture bar's 600 */
  pointer-events: none;                /* the rows under the card still take the hover */
}
.hover-bleed :is(img, video) {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--hover-still-w, calc(50cqw - 15px));
  aspect-ratio: var(--hover-still-ratio, 16 / 9);
  translate: -50% -50%;
  object-fit: cover;
  visibility: hidden;
}
.hover-bleed :is(img, video).is-active { visibility: visible; }
/* The project's loop, over its still: fades up only once hover-bleed.js
   confirms playback, so the still is never replaced by a black box. */
.hover-bleed video {
  opacity: 0;
  transition: opacity 0.3s var(--ease-out-expo);
}
.hover-bleed video.is-playing { opacity: 1; }
/* A vertical film takes its own shape — the height of the 16:9 card, the
   width its frame needs (--still-ratio from the portrait cover), nothing either
   side. There is no box to fill here, so it is not drawn on black. */
.hover-bleed :is(img, video).is-portrait {
  width: auto;
  height: calc(var(--hover-still-w, calc(50cqw - 15px)) * 9 / 16);   /* cqw, not %: a % here would resolve against the layer's HEIGHT and the tall card came out shorter (QA) */
  aspect-ratio: var(--still-ratio, 9 / 16);
  object-fit: cover;                   /* the box is the frame's own shape — fill it, no black */
  background: transparent;
}
/* The portrait cut (Social Studio services): a 400 × 752 still centred in its
   section rather than the viewport. Width from the section (--hover-still-w). */
.hover-bleed--portrait { position: absolute; --hover-still-ratio: 400 / 752; }
/* …and every still in that cut takes the SAME box (user, 2026-10-11): a
   vertical film's frame fills the 400 × 752 like a landscape one is cropped
   to it, so the overlays all stand the same height. Overrides the site-wide
   own-shape rule for .is-portrait above. */
.hover-bleed--portrait :is(img, video).is-portrait {
  width: var(--hover-still-w, calc(50cqw - 15px));
  height: auto;
  aspect-ratio: var(--hover-still-ratio, 400 / 752);
}
@media (hover: hover) and (min-width: 64.0625em) {
  [data-hover-root].is-hovering [data-still] { opacity: 0.1; }
  [data-hover-root].is-hovering [data-still].is-active {
    position: relative;
    z-index: 2;
    opacity: 1;
    text-decoration: underline;
    text-decoration-thickness: var(--ul-thickness);
    text-underline-offset: var(--ul-offset);
  }
  /* A row split into inline-block word leaves (the services' phrases): the
     decoration does not propagate into inline-blocks, so it is set on them too. */
  [data-hover-root].is-hovering [data-still].is-active :is(span, em):not(:has(*)) {
    text-decoration: underline;
    text-decoration-thickness: var(--ul-thickness);
    text-underline-offset: var(--ul-offset);
  }
}
@media (hover: none), (max-width: 64em) {
  .hover-bleed { display: none; }     /* belt and braces for touch and the mobile-chrome range */
}

/* A hover only fires with the pointer on the text itself. List rows are blocks,
   so by default each one is as wide as its column and the empty space beside a
   short label would trigger the hover (and the card peek / full-bleed still that
   ride on the same elements). Shrinking the row to its text fixes that at the
   source, for CSS :hover and the JS pointer handlers alike. Rows in a right-
   aligned or centred column also need the matching auto margin, below. */
.footer-list li,
.post-list p {
  width: fit-content;
  max-width: 100%;
}
.footer-col--press .footer-list li { margin-left: auto; }
.footer-col--clients .footer-list li { margin-inline: auto; }

@media (max-width: 48em) {
  /* --pad-inline stays at its 10px root value on mobile, so the header and
     furniture bar keep the same side padding as desktop.
     H1/H2 sizes are handled fluidly by clamp() above — no step needed here.
     The rest of the mobile layout lives in one block at the end of this file:
     these rules override component rules defined further down, and a media
     query adds no specificity, so it has to come after them to win. */
  :root {
    /* svh, not vh: on iOS `vh` resolves against the LARGE viewport (toolbars
       retracted), so the strip ran taller than intended and the card width —
       calc(--marquee-h * --ar) — inflated with it. .hero-pin is 100svh, so
       both now resolve against the same viewport. */
    --marquee-h: 27svh;       /* QA 2026-10-08: +20% on the 22.5svh it was */
  }
}


/* ============================================================
   Header / Nav — 12-column grid layout
   ============================================================ */

.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  padding: var(--pad-inline);
  padding-top: calc(var(--pad-inline) + var(--safe-top));
  padding-left: calc(var(--pad-inline) + var(--safe-left));
  padding-right: calc(var(--pad-inline) + var(--safe-right));
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  align-items: center;
  column-gap: clamp(8px, 1vw, 24px);
  z-index: 50;
  pointer-events: none;
  transition: opacity 0.4s ease;   /* fades with the page transition / hover takeover */
}

.site-header > * { pointer-events: auto; }

/* The header is fixed over every section and inherits the page's colour
   (`a`, .menu-toggle — one declaration covers the lot). The logo is white
   artwork, so on the light theme it flips with a filter (see .site-logo). */
.site-header {
  transition: color 0.2s ease, transform 0.35s ease, opacity 0.3s ease;
}

/* Nav hidden behind the card takeover / work page (card-viewer.js toggles
   .viewer-open on <html>). The header is a body-level sibling above
   .page-stack's stacking context, so the viewer can't cover it by z-index. */
.viewer-open .site-header {
  opacity: 0;
  pointer-events: none;
}

/* Retracts on scroll down, returns on scroll up (main.js toggles this).
   translateY on the header is safe — it has no fixed-position descendants
   that a new containing block would break. */
.site-header.is-hidden {
  transform: translateY(-100%);
}
.site-header .site-logo img {
  transition: filter 0.2s ease;
}

.site-logo {
  grid-column: 1 / span 2;
  display: block;
}

/* Fixed lockup size — 70 × 26 */
.site-logo img {
  width: var(--logo-w);
  height: 26px;
  object-fit: contain;
}
/* All the artwork is white — the lockup PNG wherever it appears (header, mobile
   menu, the two panels) and the footer wordmark. On the light theme it is
   inverted, in one place. (The case-study header over the player opts back
   out: that is media.) */
:is(.site-logo img, .about-logo img, .footer-mark) { filter: invert(var(--art-invert)); }   /* follows the ground, not the theme (inverted pages) */
/* The Social case-study client marks are not filtered (user, 2026-10-14):
   each white mark has a drawn black twin (assets/img/social/logo-*-black.*)
   shown on the light theme; coloured marks serve both themes as they are. */
.cs-logo .cs-logo-light { display: none; }                         /* (.cs-logo img sets display: block — these outrank it) */
html[data-theme="light"] .cs-logo .cs-logo-light { display: block; }
html[data-theme="light"] .cs-logo .cs-logo-dark { display: none; }

/* Three direct grid items span the header: the logo at the left end,
   the five primary links (.nav) across the middle 4 columns (5–8), and
   the standalone CONTACT link at the right end. */
/* link style — Epilogue Medium, 12/160 (header nav + footer nav) */
.nav,
.nav-contact {
  font-size: 12px;
  line-height: var(--lh-link);
}

/* The five links share one container across the middle 6 columns.

   Five equal cells with each label centred in its own — so POST, the middle
   one, lands dead centre of the span. A grid rather than `flex: 1 0 0` links:
   the cell is the track, and the link itself stays the size of its label, which
   keeps its hover on the text instead of the whole fifth. A label wider than
   its cell overflows evenly to both sides (justify-items: center). */
.nav {
  grid-column: 4 / span 6;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  justify-items: center;
  align-items: center;
  column-gap: clamp(12px, 1.5vw, 28px);
}
.nav a {
  font-weight: 500;
  white-space: nowrap;
}

/* CONTACT stands alone in the last column, right-aligned. */
.nav-contact {
  grid-column: 12;
  justify-self: end;          /* hugs its label, so the hover is the text only */
  white-space: nowrap;
}

.sep { opacity: 0.7; }


/* ============================================================
   Mobile menu — MENU toggle + full-screen panel. Both are
   display:none above 48em; the mobile block near the end of this
   file switches them on.
   ============================================================ */

/* Button reset, then the same 12px Epilogue Medium as .nav */
.menu-toggle {
  display: none;
  appearance: none;
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
  color: inherit;
  cursor: pointer;
  font-size: 12px;
  line-height: var(--lh-link);
  text-transform: uppercase;
}

.mobile-menu {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1000;              /* body-level, so nothing traps this */
  background: var(--bg);         /* the page's ground (it was the inverse until 2026-09-24) */
  color: var(--fg);              /* cascades to the nav links, list and toggle */
  padding: var(--pad-inline);
  padding-top: calc(var(--pad-inline) + var(--safe-top));
  padding-bottom: calc(var(--pad-inline) + var(--safe-bottom));
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.mobile-menu[hidden] { display: none; }

.mobile-menu-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: none;
}

/* Serif Title Case links, centred and filling the panel */
.mobile-menu-nav {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: clamp(34px, 11vw, 56px);
  line-height: var(--lh-h2);
}

.mobile-menu-contact {
  flex: none;
  text-align: center;
  padding-bottom: var(--space-20);
}

.mobile-menu-head {
  margin: 0 0 var(--space-20);
  font-size: 12px;
  line-height: var(--lh-link);
  text-transform: uppercase;
}

.mobile-menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: clamp(22px, 7vw, 34px);
  line-height: var(--lh-h2);
}

/* Blur reveal targets — menu.js sets the from-state before unhiding */
.mobile-menu-nav a,
.mobile-menu-list li {
  will-change: opacity, filter;
}


/* ============================================================
   Hero — lockup + sub-nav, stacked and centred
   ============================================================ */

main {
  display: grid;
  grid-template-rows: 1fr auto;        /* hero · marquee — the bar is fixed, out of flow */
  min-height: 100svh;
  /* The fixed bar overlays the foot of the viewport, so every page reserves its
     height here: content scrolls clear of the bar instead of under it. */
  padding-bottom: var(--furniture-clear);
}
/* …unless the page ends on the in-flow footer, which sits after <main> and
   carries that reserve itself (.footer-pin) — two would leave a dead band
   above the wordmark. The placeholder is matched too, so nothing shifts when
   footer.js swaps it in. */
main:has(+ [data-footer-pin]),
main:has(+ .footer-pin--static) {
  padding-bottom: 0;
}


/* ============================================================
   Furniture bar — clock (left) + socials (right) on one line,
   sitting just above the carousel.
   ============================================================ */

/* Fixed to the foot of the viewport on every page, at body level.
   Body level is not incidental: `position: fixed` inside a transformed ancestor
   anchors to that ancestor rather than the viewport, and `mix-blend-mode` only
   blends within its own stacking context — this page is full of both.

   `exclusion` inverts the bar against whatever scrolls under it, so one white
   bar reads correctly on the light pages and the dark ones without needing a
   variant per theme. */
.furniture-bar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 600;                        /* above the cursor (500), the highest layer */
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-inline: calc(var(--pad-inline) + var(--safe-left)) calc(var(--pad-inline) + var(--safe-right));
  padding-bottom: calc(var(--pad-inline) + var(--safe-bottom));   /* 10px — the page margin, on every bar — above the home indicator */
  color: #fff;
  mix-blend-mode: exclusion;
  pointer-events: none;                /* only the links inside are interactive */
}
.furniture-bar a,
.furniture-bar button { pointer-events: auto; }

/* The theme toggle: a 12px circle in the middle of the bar (theme.js flips
   html[data-theme] and remembers it). White like the rest of the bar, and the
   bar's exclusion blend does the rest — it reads white on the dark ground and
   black on the light one, which is the whole affordance. Centred on the page,
   not between the two corners (they differ in width), and on the corners'
   text line: the bar's bottom pad plus half of what the 12px label's line
   height adds. The circle is 8px at 30% at rest, 12px and solid on hover (a
   scale from its centre, so it grows in place), drawn by the pseudo-element;
   the button itself is 4px larger all round, an invisible hit area. */
.theme-toggle {
  position: absolute;
  left: 50%;
  bottom: calc(var(--pad-inline) + var(--safe-bottom) + (12px * var(--lh-link) - 8px) / 2 - 4px);   /* the bar's bottom pad includes the phone's safe area: without it the circle sat under the bar's line, in the home indicator (user, 2026-10-14) */
  translate: -50% 0;
  width: 16px;
  height: 16px;
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
  cursor: pointer;
}
.theme-toggle::before {
  content: '';
  display: block;
  width: 8px;
  height: 8px;
  margin: 4px;
  border-radius: 50%;
  background: #fff;
  opacity: 0.3;
  transition: scale 0.2s ease, opacity 0.2s ease;
}
@media (hover: hover) {
  .theme-toggle:hover::before { scale: 1.5; opacity: 1; }
}

/* Case study: the pinned player's controls own the bottom edge, so the bar
   waits until they're covered or gone (work-page.js toggles .is-clear). */
.work-detail .furniture-bar {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s;
}
.work-detail .furniture-bar.is-clear {
  opacity: 1;
  visibility: visible;
}

.corner {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 12px;         /* the site's standard label size (.t-label) */
  line-height: var(--lh-link);
}

/* Hide all data-furniture elements at first paint — GSAP fades them in.
   Prevents the brief flash of nav/subnav/corners before JS init runs. */
[data-furniture] {
  opacity: 0;
}

/* Clock — hover / click a city to switch zones. The active city reads at
   full strength with a nav-style underline; the other dims back. */
.clock-city {
  appearance: none;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
  opacity: 0.4;
  transition: opacity 0.4s ease;
}

.clock-city.is-active {
  opacity: 1;
}

/* NY LA TYO LDN / 00:00AM — the one slash sits between the cities and the
   time, spaced by .corner's gap like everything else in the bar. */


/* Cursor label (cursor-label.js) — one word in place of the pointer: "General
   Inquiries" on the footer wordmark, "View" on cards that print their own
   caption, "Email" on every other mailto link. Type is .t-caption-italic, the director-name style (Libre Caslon Text
   Italic 12/120, not uppercased), centred on the pointer by JS. White +
   difference, so it reads against whatever is under it — light ground, dark
   ground or a card's image — the same idea as the fixed bar's exclusion blend. */
.cursor-label {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 950;                    /* over the Directors & Creators popup (590) and the bar (600) — it has
                                      links that take a label too; under the mobile menu (1000) */
  pointer-events: none;
  white-space: nowrap;
  /* Always plain white, every tone, every theme (user, 2026-10-19) — no
     difference blend, no theme ink. */
  color: #fff;
  mix-blend-mode: normal;
  opacity: 0;
  will-change: transform;
}
.cursor-label.is-visible { opacity: 1; }
.cursor-label--dark { color: #fff; }
/* The action word over a film — PAUSE / PLAY on the project page: the UI label
   recipe (t-label, set by cursor-label.js) in plain white, no blend; it idles
   away with the player's other furniture (html.film-idle, work-page.js). */
.cursor-label--sans {
  color: #fff;
  mix-blend-mode: normal;
  transition: opacity 0.25s ease;
}
html.film-idle .cursor-label--sans.is-visible { opacity: 0; }
/* Play / Pause over a film in the serif (user, 2026-10-19): plain white, no
   blend, idling away with the player's furniture like the sans label did. */
.cursor-label--light {
  color: #fff;
  mix-blend-mode: normal;
  transition: opacity 0.25s ease;
}
html.film-idle .cursor-label--light.is-visible { opacity: 0; }
/* The footer's General Inquiries (user, 2026-10-22): white with the exclusion
   blend, like the furniture bar — it inverts against the wordmark and the
   ground, so it reads on the light pages and the dark ones. */
.cursor-label--exclusion {
  color: #fff;
  mix-blend-mode: exclusion;
}

/* The real cursor goes wherever the label stands in for it. Class-gated, so it
   is only hidden while cursor-label.js is actually running. !important: some
   browser extensions stamp an inline `cursor: pointer` on every <a>, which no
   ordinary rule can beat — and a hand on top of the label defeats the point. */
@media (hover: hover) {
  .footer-marquee.has-cursor-label,
  .has-cursor-label .work-item,
  .has-cursor-label .work-item * { cursor: none !important; }
}


/* Site-wide page transition — a full-screen black blur that fades in on exit and
   out on enter, bridging every navigation. It's the root's ::before so no page
   needs markup; opacity is driven by the rtw-fade class on <html> (set on exit,
   and before paint by a head script on arrival). z-index above everything so it
   covers the nav too (the nav "goes away" with it). See transition.js. */
html::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: var(--bg);
  -webkit-backdrop-filter: blur(28px);
  backdrop-filter: blur(28px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.55s cubic-bezier(0.85, 0, 0.15, 1);
}
html.rtw-fade::before {
  opacity: 1;
  pointer-events: auto;
}


/* ============================================================
   Inner pages — shared header revealed, static content layout.
   Scoped to `body.page`; the homepage body has no `.page` class,
   so none of this touches the hero/intro.
   ============================================================ */

body.page {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

/* No intro on inner pages — the nav/logo/corner furniture is visible at load
   (the homepage keeps [data-furniture] hidden until GSAP fades it in). */
body.page [data-furniture],
body.work-detail [data-furniture] { opacity: 1; }

/* The header stays fixed and overlays; push page content clear of it. */
.page-main {
  flex: 1 0 auto;
  padding:
    calc(var(--nav-h) + clamp(40px, 8vh, 96px))
    var(--pad-inline)
    clamp(60px, 10vh, 120px);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: clamp(8px, 1vw, 24px);
  row-gap: clamp(24px, 4vh, 48px);
  align-content: start;
}

/* eyebrow → H4 (Epilogue Medium 500, 12/140, uppercase) */
.page-eyebrow {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 12px;
  line-height: var(--lh-body);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* page title → H1 (fluid 48→128) */
.page-title {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--type-h1);
  line-height: var(--lh-h1);
  text-transform: uppercase;
}

/* italic-serif emphasis word inside a page title */
.page-title em {
  font-family: var(--ff-serif-italic);
  font-style: italic;
  font-weight: 400;
}

/* lede → H2 (fluid 28→48) */
.page-lede {
  grid-column: 1 / span 7;
  margin: 0;
  font-size: var(--type-h2);
  line-height: var(--lh-h2);
}

/* note → Caption (Epilogue Medium 500, 10/140, uppercase, muted) */
.page-note {
  grid-column: 9 / span 4;
  margin: 0;
  font-size: 10px;
  line-height: var(--lh-body);
  text-transform: uppercase;
  color: var(--muted);
}

/* Contact-style large mailto / links block */
.page-links {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: clamp(8px, 1vw, 14px);
}
.page-links a {                /* CTA links — Epilogue, fixed (non-H1/H2) */
  font-family: var(--ff-sans);
  font-weight: 500;
  font-size: 12px;
  line-height: var(--lh-body);
  text-transform: uppercase;
}

/* Footer — 12-col, monochrome, hairline top rule */
.site-footer {
  flex-shrink: 0;
  padding: clamp(40px, 6vh, 80px) var(--pad-inline) var(--pad-inline);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: clamp(8px, 1vw, 24px);
  row-gap: 24px;
  border-top: 1px solid var(--hairline);
  font-family: var(--ff-sans);
  font-weight: 400;
  font-size: 12px;
  line-height: var(--lh-link);
  text-transform: uppercase;
}
.foot-brand { font-weight: 600; }
.foot-brand { grid-column: 1 / span 4; }
.foot-links { grid-column: 5 / span 4; display: flex; flex-direction: column; gap: 6px; }
.foot-meta  { grid-column: 9 / span 4; justify-self: end; text-align: right; }

@media (max-width: 48em) {
  .site-header { column-gap: 8px; }
  .nav { grid-column: 1 / -1; }
  .page-lede,
  .page-note,
  .foot-brand,
  .foot-links,
  .foot-meta {
    grid-column: 1 / -1;
    justify-self: start;
    text-align: left;
  }
}


/* ============================================================
   Work grid — editorial scatter. A 12 × 6 cell grid, 760px tall,
   10px gutter (Figma). Each item spans an arbitrary block of cells;
   placement via inline `grid-column` / `grid-row` on .work-item.
   Rows stack to build the full Selected Work section.
   ============================================================ */

.work {
  display: flex;
  flex-direction: column;
  gap: 10px;                    /* vertical gap between the work rows */
  padding-inline: var(--pad-inline);
  padding-block: 10px;
  /* Dark from here down — the work grid, the CTA and the footer are all
     permanently black on white text. The mission above stays light. */
  background: var(--bg);
  color: var(--fg);
}

/* work.js renders the rows into this wrapper. display:contents dissolves its
   box so the .work-row elements become direct flex children of .work and pick
   up its column gap (10px desktop / 32px mobile) between rows. */
[data-work-grid] {
  display: contents;
}

.work-row {
  display: grid;
  grid-template-columns: repeat(var(--work-cols), 1fr);
  grid-template-rows: repeat(var(--work-rows), 1fr);
  gap: var(--work-gap);
  height: var(--work-row-h);
}

.work-item {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  margin: 0;                /* reset the default <figure> margin */
}

/* Image sits in an overflow-hidden .work-media frame that fills the cell block
   above the caption; the img is oversized with headroom top+bottom so it can
   drift (parallax, card-parallax.js) without ever leaving a gap — the same
   treatment as the case-study stills. */
.work-media {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
.work-item img,
.work-card-video {
  position: absolute;
  top: -9%;
  left: 0;
  width: 100%;
  height: 118%;
  object-fit: cover;
  display: block;
}

/* Hover loop, over the still. Only fades up once card-video.js confirms
   playback started — a 404 or a blocked autoplay leaves the still alone. */
.work-card-video {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s var(--ease-out-expo);
}
.work-card-video.is-playing { opacity: 1; }

/* Cursor-hover variant: hide the real cursor over cards so the floating
   project + director label (from work-cursor.js) stands in for it.
   Scoped to .is-cursor (added by JS) so it degrades gracefully.
   [data-work] covers both the homepage .work section and the all-work
   .work-grid (both carry data-work). */
[data-work].is-cursor .work-item { cursor: none; }

/* PROJECT NAME (H3) + Director Name (Caption Italic) — below the image.
   Hidden by default: on desktop the hover cursor (work-cursor.js) stands in
   for these. The mobile block below prints them, since touch has no hover. */
.work-caption {
  display: none;
  flex: 0 0 auto;
  margin-top: var(--space-8);
  color: inherit;   /* black — the work section stays light */
}
.work-caption .project {   /* H3 project title */
  margin: 0;
  font-family: var(--ff-sans);
  font-weight: 600;
  font-size: 12px;
  line-height: var(--lh-body);
  text-transform: uppercase;
}
.work-caption .director {
  margin: 0;
  font-family: var(--ff-serif-italic);
  font-style: italic;
  font-weight: 400;
  font-size: 12px;
  line-height: var(--lh-cap-i);
}

/* On mobile the scatter grid becomes one full-width column with the caption
   printed under each uniformly-cropped image. */
@media (max-width: 48em) {
  .work { gap: var(--card-gap); }   /* between rows: --card-gap, 60px (user 2026-10-18) */
  .work-row {
    display: flex;
    flex-direction: column;
    height: auto;
    gap: var(--card-gap);       /* between items within a row — same rhythm */
  }
  .work-item {
    /* Block, not flex. As a column-flex item with `flex: none`, Safari sizes
       the image from its intrinsic aspect at the fixed height instead of
       honouring width:100% on the cross axis — it renders far too wide and gets
       clipped. Chrome resolves it from width:100%, so this only shows on iOS.
       Nothing here needs flex: it's an image with a caption beneath it. */
    display: block;
    /* the inline grid-column/grid-row become inert once .work-row isn't a grid */
    grid-column: auto;
    grid-row: auto;
  }
  /* No parallax on mobile (native scroll stays jank-free): the image is a
     plain 16:9 crop (QA 2026-10-08; it was 82.05vw tall).
     The frame stays positioned and clipped so the loop can play inside it
     (card-video.js plays a card's loop while it is on screen on touch devices)
     and nothing — a vertical frame at its own width included — can escape it
     and widen the page. */
  .work-media { position: relative; overflow: hidden; }
  .work-item img {
    position: static;
    top: auto;
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;       /* QA 2026-10-08: 16:9 cards on phones */
    object-fit: cover;
  }
  .work-caption { display: block; }

  /* The loop, over the still, filling the frame; fades up on .is-playing like
     on desktop (card-video.js starts it when the card is ≥50% on screen). */
  .work-card-video {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }

  /* No hover effect on mobile: hide the floating project/director cursor and
     restore a normal pointer (the JS still runs, but there's nothing to show). */
  [data-work-cursor] { display: none; }
  [data-work].is-cursor .work-item { cursor: auto; }

  /* Cap the grid at 6 projects. Marked per-figure in the markup rather than
     computed with nth-child: the items are split across .work-row wrappers
     (3 + 2 + 3 + 2) and CSS can't count across parents, so any positional
     rule would break the moment the rows are re-composed. Marking them also
     makes *which* projects drop an explicit choice. */
  [data-mobile-hide] { display: none; }
}

/* "all work" CTA — centered serif H1, 120px (vw) vertical padding. Hover is
   underline only (see the shared rule above) — the typeface stays put. */
.work-cta {
  display: flex;
  justify-content: center;
  padding-block: 6.25vw;   /* 120px @ 1920 */
  background: var(--bg);
  color: var(--fg);
}


/* ============================================================
   Mission statement — dark, centered single column. Flowing
   H1-scale text with inline images at fixed positions (line breaks
   authored to match the comp), then centered white body + CTA.
   Inline images ([data-mission-img]) are the animation targets.
   ============================================================ */

.mission {
  background: var(--bg);         /* dark section — #080808 */
  color: var(--fg);              /* white text on black */
  padding: 8.33vw .52vw;        /* 160px / 20px @ 1920 (vertical / sides) */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 40px;   /* fixed 40px on desktop (2.08vw only equals 40px at exactly 1920) */
}

/* Hidden from first paint until mission.js has applied the reveal's from-state
   (it sets data-mission-ready). Without this the copy renders in its final
   visible state while we wait on document.fonts.ready, then visibly pops to
   hidden once the split runs — the flicker on first load.

   `visibility`, not `opacity`/`display`: the elements keep their layout boxes,
   so SplitText still measures line wrapping correctly while hidden and there's
   no reflow on reveal. Gated on .js-anim (set by the inline script in the head)
   so this can never apply without JS. */
html.js-anim :is(.mission, .services, .case-studies):not([data-mission-ready]) :is(.mission-text, .mission-body, .mission-cta) {
  visibility: hidden;
}

/* Nothing is animated under reduced motion, so never hide it. */
@media (prefers-reduced-motion: reduce) {
  html.js-anim :is(.mission, .services, .case-studies):not([data-mission-ready]) :is(.mission-text, .mission-body, .mission-cta) {
    visibility: visible;
  }
}

.mission-statement { width: 100%; }

/* Per-breakpoint line breaks in .mission-text. A <br> only breaks when it
   generates a box, so display:none suppresses it; the opposite set stays inline.
   .br-m (mobile-only breaks) are off by default; the mobile block below flips
   them on and hides .br-d. */
.br-m { display: none; }

/* H1-scale, fluid vw (scales like H1/H2 — the only type that scales) */
.mission-text {
  margin: 0;
  text-align: center;
  font-size: var(--type-h1);
  line-height: var(--lh-h1);   /* 95% (H1) */
}

/* Word leaves created by mission.js — inline-block so the blur/opacity
   reveal renders per word (filter on a raw inline box is unreliable).
   No mask: the words rack into focus in place, like the hero lockup.
   `will-change` is required here, not optional: without layer promotion the
   browser re-rasterizes every blurred word each frame, which visibly
   shimmers/glitches while scrolling. */
.mission-text :is(span, em):not(:has(*)) {
  display: inline-block;
}

/* Layer promotion ONLY while the section is on screen (mission.js toggles
   .is-revealing from the timeline's ScrollTrigger). Promoting ~39 elements
   permanently is what pushed iOS Safari over its layer budget — it starts
   evicting layers and skipping repaints elsewhere on the spage. Scoped like
   this the anti-shimmer promotion still applies exactly when it's needed. */
:is(.mission, .services, .case-studies).is-revealing .mission-text :is(span, em):not(:has(*)),
:is(.mission, .services, .case-studies).is-revealing .mission-line {
  will-change: opacity, filter;
}

/* Clipping wrapper — the scroll animation widens THIS from 0 while the
   image inside keeps its own fixed width, so the image wipes open from
   the centre instead of squashing. The natural width lives here in CSS,
   so with no JS (or reduced motion) it simply renders fully open.

   Sizes are tokens with the home page's values as defaults, so the Social
   Studio services (/social-1/, portrait images) run on these same rules:
   --media-w (revealed width, em), --media-ratio, --media-gap. */
.mission-media {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
  width: var(--media-w, 1.4375em);          /* 184 / 128 — revealed width */
  flex: none;
  vertical-align: baseline;
  margin: 0 var(--media-gap, 2vw);          /* side spacing (literal spaces removed in markup) */
}

.mission-img,
.mission-video {
  /* 184 × 92 at the 128px H1 size. Sized in em so the images scale in
     lockstep with the fluid mission text — i.e. with the viewport width. An
     inline video sits in the same chip (its middle, cropped), so the typeset
     line count never depends on what the clip's shape turns out to be. */
  width: var(--media-w, 1.4375em);          /* 184 / 128 */
  aspect-ratio: var(--media-ratio, 184 / 92);   /* → 92px tall at the 128px text size */
  height: auto;
  object-fit: cover;
  background: #000;
  display: block;
  flex: none;               /* never shrink as the wrapper closes */
  max-width: none;
}

.mission-aside {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-20);
  text-align: center;
}

.mission-body {
  margin: 0;
  /* No width cap: line breaks are authored with <br>, so the paragraph sizes to
     its widest forced line and stays centred. A 40ch cap re-wrapped those lines
     into stray fragments ("the", "brands with"). */
  max-width: none;
  color: inherit;           /* black, from the light section */
}

.mission-cta {
  width: fit-content;
  /* Underlined at rest (user, 2026-10-18): the site's one underline recipe;
     the shared hover rule above keeps it and dims the link. */
  text-decoration: underline;
  text-underline-offset: var(--ul-offset);
  text-decoration-thickness: var(--ul-thickness);
}

/* Line wrappers created by SplitText (linesClass in mission.js). Their layer
   promotion is handled by the .is-revealing rule above, alongside the word
   leaves — a blurred element without it re-rasterizes every frame while
   scrubbing, which shimmers. */

@media (max-width: 48em) {
  .mission { padding: 15.38vw 10px; }

  /* Swap the authored break set: desktop breaks off, mobile breaks on. */
  .br-d { display: none; }
  .br-m { display: inline; }

  /* 10% larger than the base clamp (every term ×1.1). The inline images are
     sized in em off this font-size, so they grow by the same 10% — no separate
     image rule needed. */
  .mission-text { font-size: 12vw; }   /* ~47px @ 390 — the inline images are
                                          em-sized off this, so they scale
                                          with it */
}


/* ============================================================
   Footer — 3-column directory · wordmark · bottom bar. Dark; sits
   in .page-flow so it scrolls over the pinned hero. Spacing in vw
   (12 / 20 / 40 / 160px @ 1920). List items = H2 (scale); headers,
   ticker and nav = fixed small UI type.
   ============================================================ */

/* The footer paints with the site's theme (its own Dark | Light option went
   with the theme toggle, 2026-09-24). The pin is a body child, so during the
   homepage intro it follows the body's animated tokens — it is off screen then. */
.home-footer {
  background: var(--bg);
  color: var(--fg);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.625vw;     /* 80px top/bottom · 12px sides @ 1920 */
  /* The wordmark and bottom bar moved out to .footer-pin, so the old `gap`
     that separated them from the directory becomes trailing padding — it keeps
     the columns off the pinned section revealed below. */

}

.footer-content {
  align-self: stretch;
  display: flex;
  align-items: flex-start;
  gap: 1.04vw;             /* 20px */
}

.footer-col {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2.08vw;             /* 40px — header↔list, group↔group */
}
.footer-col--press     { align-items: flex-end;  text-align: right;  }
.footer-col--clients   { align-items: center;     text-align: center; }
.footer-col--locations { align-items: flex-start; text-align: left;   }

/* head + list wrapper. On desktop it just holds the pair together (the column's
   gap now spaces group↔group); on mobile it becomes the label-left/list-right
   grid. Inherits the column's align-items, so text stays right/center/left. */
.footer-group {
  display: flex;
  flex-direction: column;
  gap: 2.08vw;   /* head↔list — the value the column gap used to provide */
}

/* list items = H2 (serif, fluid) */
.footer-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--type-h2);
  line-height: var(--lh-h2);
}

/* Hover is underline only (see the shared rule above). */
.footer-list li {
  cursor: pointer;   /* hand, not the text I-beam these <li>s would default to */
}

/* 4px between entries. On the adjacent sibling so it spaces the items apart
   without adding a leading gap under the column heading. */
.footer-list li + li {
  margin-top: 4px;
}

/* Bottom counterpart to .hero-pin: pinned to the viewport bottom, sitting
   below .page-flow (z-index 2), which is opaque — so it stays hidden until the
   page scrolls up off it.

   `sticky`, not `fixed`: sticky keeps its space in the flow, so the document
   grows by this section's height on its own. Fixed would take it out of flow
   and need its height measured in JS to add an equivalent scroll runway.

   z-index must be explicit — at `auto` a positioned element later in the DOM
   paints *above* .page-flow, which would put the footer over the page instead
   of behind it. */
.footer-pin {
  position: sticky;
  bottom: 0;
  z-index: 0;
  background: var(--bg);
  color: var(--fg);
  display: flex;
  flex-direction: column;
  /* No inline padding at any width — the marquee runs edge to edge; a gutter
     here shows as a strip of background either side of the moving wordmark. */
  padding-top: 4vw;
  padding-bottom: var(--furniture-clear);   /* the pin's own bar is gone; this
                                    keeps the wordmark clear of the fixed bar */
}

/* The wordmark scrolls as a marquee at every breakpoint. No filter on the
   copies — the artwork is already #F3F3F3, which reads light on the dark bg. */
.footer-marquee {
  display: block;
  width: 100%;
  overflow: hidden;
  opacity: 10%;
  transition: opacity 0.3s ease;
}

/* Hover: the wordmark comes up to full strength, and the pointer becomes a
   "General Inquiries" label (footer.js → cursor-label.js) — the wordmark is a
   mailto link. No underline. */
@media (hover: hover) {
  .footer-marquee:hover { opacity: 1; }
}
/* No hover on phones, so the wordmark reads at full strength there (the user's
   call, 2026-09-24) instead of resting at 10% with no way up. */
@media (max-width: 48em) {
  .footer-marquee { opacity: 1; }
}

/* Off the homepage there's no .page-stack to be revealed from behind, so the
   wordmark sits in flow at the end of the page (what mobile does everywhere). */
.footer-pin--static { position: static; }

/* Clients=None — the wordmark on its own: 60px above it @ 1920. */
.footer-pin--solo { padding-top: 3.125vw; }

/* The artwork is #F3F3F3; on the light theme it is inverted (see .site-logo) to
   near-black, and the marquee's 10% opacity takes it to the comp's pale grey. */

.footer-marquee-track {
  display: flex;
  width: max-content;
  will-change: transform;
}

.footer-mark {
  /* 0.792× the hero lockup's type size. Still tracks the shared token — change
     --hero-title-size and the headline + this wordmark move together, this one
     at 0.792× the scale. */
  height: calc(var(--hero-title-size) * 0.792);
  width: auto;
  flex: none;
  margin-right: 6vw;   /* gap between repeats */
}

.footer-ticker,
.footer-social {
  display: flex;
  align-items: baseline;
  gap: 6px;                        /* match the hero corner */
  font-size: 12px;                 /* the site's standard label size */
  line-height: var(--lh-link);
  text-transform: uppercase;
}


/* ============================================================
   Footer — mobile (≤48em). Must follow the base footer rules above
   so these overrides win (media queries add no specificity).
   ============================================================ */
@media (max-width: 48em) {
  .home-footer {
    padding: var(--pad-inline);       /* 20px sides, from the 2.4px vw value */
    padding-top: calc(var(--pad-inline) + 10px);   /* 10px more above the directory */
    padding-bottom: 4vw;             /* gap down to the marquee below */
  }

  /* ---- Directory variants (footer.js VARIANT picks one) -------------------
     Both stack one category per row on mobile; they differ only in alignment.
     Desktop is unaffected — these live inside the mobile query. */

  /* --- 'left-aligned': label in a fixed left gutter, list fills the right --- */

  /* align-items:stretch (the base is flex-start) makes each column span the
     full container width, so the groups and their list columns fill it rather
     than shrinking to content. */
  .home-footer--left-aligned .footer-content {
    flex-direction: column;
    align-items: stretch;
    gap: 40px;                        /* between categories */
  }
  .home-footer--left-aligned .footer-col {
    /* undo the desktop per-column alignment — every category is left-aligned */
    align-items: stretch;
    text-align: left;
    gap: 40px;                        /* between the two groups in the locations col */
  }
  .home-footer--left-aligned .footer-col--press,
  .home-footer--left-aligned .footer-col--clients,
  .home-footer--left-aligned .footer-col--locations {
    align-items: stretch;
    text-align: left;
  }

  /* Label in a FIXED left column, list fills the rest. A fixed width (not 1fr)
     is what aligns every list to the same left edge: `1fr` is minmax(auto,1fr),
     so each label column floored at its group's content min-width and drifted.
     It also fits the longest label ("LOCATIONS") and gives the list the
     remaining width, which minimises wrapping of the multi-word entries. */
  .home-footer--left-aligned .footer-group {
    display: grid;
    grid-template-columns: 30.77vw 1fr;
    gap: var(--label-gap);
    align-items: start;               /* label sits at the first list item's line */
  }

  /* --- 'centered': label centred above its own centred list ---------------- */
  .home-footer--centered .footer-content {
    flex-direction: column;
    align-items: center;
    gap: 15.38vw;                      /* between categories */
  }
  .home-footer--centered .footer-col {
    align-items: center;
    text-align: center;
    gap: 15.38vw;                      /* Locations ↔ Contact in the same column */
  }
  /* Out-specifies the desktop per-column right/left alignment (0,2,0 vs 0,1,0). */
  .home-footer--centered .footer-col--press,
  .home-footer--centered .footer-col--clients,
  .home-footer--centered .footer-col--locations {
    align-items: center;
    text-align: center;
  }
  /* Rows hug their text (see the shared hover rule), so each one is centred
     itself — this also overrides desktop's right-aligned Press rows. */
  .home-footer--centered .footer-list li { margin-inline: auto; }
  /* flex, not the left-aligned variant's grid — label sits above the list. */
  .home-footer--centered .footer-group {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--label-gap);            /* tighter than the 60px between groups, so
                                         each label groups with its own list */
  }

  /* In-flow, not pinned — a plain footer stacked at the document end (no reveal,
     no fixed layer, so no over-drag gap and no grey-band re-compositing). Desktop
     keeps its sticky reveal. */
  .footer-pin {
    position: static;
  }

  /* Marquee overrides — the base rules above carry the layout. 7.57vw would be
     only ~30px here, so the lockup gets a fixed height instead. */
  .footer-marquee { padding-block: 5.13vw; }   /* 20px @ 390 */
  /* .footer-mark needs no override — its base rule already reads
     --hero-title-size, which this breakpoint redefines. */

  .footer-ticker { grid-column: auto; }
}


/* ============================================================
   Reduced motion — show everything instantly, no animation
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  .loader {
    opacity: 0;
    pointer-events: none;
  }

  .hero-title[data-animated-words] :is(span, em):not(:has(*)) {
    opacity: 1;
    filter: none;
  }

  [data-furniture] {
    opacity: 1 !important;
  }

  .marquee-card {
    transform: none !important;
  }
}


/* ============================================================
   Mobile layout (≤48em) — MUST stay last.
   These override component rules defined earlier in this file, and a media
   query contributes no specificity, so source order is what decides. An
   earlier block silently loses to the base rules.
   ============================================================ */

/* ---- Header → MENU, phone AND tablet ------------------------------------
   One definition covering both: the nav's six labels stop fitting well before
   the phone breakpoint, so the swap happens from 1024 down rather than 768.

   The `:has(.menu-toggle)` guard matters — pages sharing this stylesheet that
   have no toggle button yet would otherwise be left with no navigation at all.
   They switch over on their own once they get the button. */
@media (max-width: 64em) {
  .site-header:has(.menu-toggle) .nav,
  .site-header:has(.menu-toggle) .nav-contact {
    display: none;
  }

  .menu-toggle {
    display: block;
    grid-column: 12;                 /* .site-header is a 12-col grid at every width */
    justify-self: end;
  }

  /* .mobile-menu is display:none by default and menu.js only toggles [hidden],
     so without this the overlay would open into nothing. */
  .mobile-menu:not([hidden]) {
    display: flex;
  }

  /* The lockup takes the phone's ratio from here down — nearly edge to edge
     rather than the desktop's centred setting. It lives in this block, not the
     48em one, so the headline and the nav change at the same width: once the
     nav is the MENU button the page reads as mobile, and a desktop-scaled
     lockup above a mobile nav was the mismatch. Set on the token so the footer
     wordmark tracks it (see .footer-mark). */
  :root { --hero-title-size: 15.5vw; }
}


/* ---- Mobile layout (≤48em) — MUST stay last in this sheet: media queries
   add no specificity, so source order decides. ---- */
@media (max-width: 48em) {
  :root {
    --label-gap: 20px;            /* Figma mobile: consistent label↔content spacing */
  }
  .site-logo img {
    width: 54px;
    height: 20px;
  }
  /* NOTE: .hero-pin deliberately has no backface-visibility / transform here.
     It was added speculatively for the black-hero issue and never confirmed to
     help, and it forces a permanent full-viewport composited layer beneath the
     whole page — raster budget the compositor needs elsewhere. If the hero
     goes black again, prefer moving .loader and .card-cursor out of .hero-pin
     first so a real translateZ(0) can be used without breaking their fixed
     positioning. */

  /* Clock + social links. Set on .corner because .clock-city is `font: inherit`
     and .clock-time inherits too, so this one value covers the whole bar. */
  /* One step down from the desktop 10px, with the gap scaled to match.
     Letter-spacing needs no override — it's -0.01em on body, so it's already
     proportional. */
  .corner {
    font-size: 12px;
    gap: 5px;
  }
}

/* ============================================================
   Load reveal gate — see assets/js/section-reveal.js
   Class-gated so no-JS hides nothing, and desktop-only so touch
   devices paint immediately. section-reveal.js takes the hidden
   state over in GSAP and removes the class; the inline <head>
   script clears it on a timer if that module never loads.
   ============================================================ */
@media (min-width: 48.0625em) {
  html.js-reveal [data-reveal] { opacity: 0; }
}


/* ============================================================
   CMS copy gate — see assets/js/cms/text.js
   Copy bound from Sanity ([data-cms]) and lists rendered from it
   ([data-cms-list]) stay hidden until the page's content is in,
   so the HTML fallback never flashes before the edited text
   replaces it. `visibility`, not `display`: layout is kept, so
   nothing jumps when it appears. The inline <head> script lifts
   the class on a timer if the page's scripts never run.
   ============================================================ */
html.cms-pending [data-cms],
html.cms-pending [data-cms-list] { visibility: hidden; }

/* Sections with no content are hidden with the `hidden` attribute; make that
   win over each section's own display (grid/flex) everywhere. */
[hidden] { display: none !important; }


/* ============================================================
   VERTICAL FILMS IN LANDSCAPE BOXES
   A project with a portrait cover is a vertical film. Wherever its frame or
   loop lands in a box that keeps its own shape — grid cards and their hover
   loops, a horizontal marquee slot, the project page poster, the homepage
   hover takeover — it stands full height, centred, with black either side.
   Never the letterboxed cover. Late in the file so it outranks the earlier
   `object-fit: cover` rules by source order. (The hover reveals and the
   person-page stills instead take the frame's own shape — see their rules.)
   ============================================================ */
img.is-portrait,
video.is-portrait {
  object-fit: contain;
  background: #000;
}
/* Grid cards: the frame always fills the cell's height. Sized by height with
   the width following the frame, centred — a frame narrower than the cell gets
   black either side; one wider is cropped at the sides. Never bars above and
   below (a 4:5 photo in a tall slot would otherwise float between them). The
   homepage cards keep their 118% parallax headroom; the uniform grids are 100%. */
.work-item img.is-portrait,
.work-item .work-card-video.is-portrait {
  /* Centred with auto margins between left:0 and right:0 — not translate,
     which the parallax tween (GSAP transforms) resets. */
  left: 0;
  right: 0;
  width: auto;
  max-width: none;
  margin-inline: auto;
  object-fit: cover;
  background: none;
}
.work-media:has(> .is-portrait) { background: #000; }
/* Phones (QA 2026-10-08): every grid card is a 16:9 box; a vertical frame
   stands centred in it on black rather than filling the height at its own
   width (the desktop rule above), which at natural size overflowed the card. */
@media (max-width: 48em) {
  .work-item img.is-portrait,
  .work-item .work-card-video.is-portrait {
    width: 100%;
    max-width: 100%;
    margin: 0;
    object-fit: contain;
    background: #000;
  }
  .work-item img.is-portrait { height: auto; aspect-ratio: 16 / 9; }
}
.work-poster.is-portrait,
.card-viewer.is-portrait .card-viewer-bg {
  background-size: contain;
  background-repeat: no-repeat;
  background-color: #000;
}
.lightbox-frame .work-video.is-portrait video {
  object-fit: contain;
  background: #000;
}
