/*!
 * James Kicklighter Theme — main stylesheet
 * Version 2.9.316 — Project FAQ editorial size parity
 *
 * v2.9.316 changelog (2026-05-15):
 *   PROJECT FAQ size canonicalization. Questions and answers on the
 *   project-page FAQ tab now match the standalone /faq/ page sizes:
 *   questions clamp(20px, 2.2vw, 26px), answers var(--h-lede)
 *   (clamp 18-22). Previously sized smaller (clamp 18-22 / clamp
 *   15-17) which made the FAQ tab read as a caption section rather
 *   than editorial content equivalent to Synopsis / Director's Notes
 *   / Press. The project-FAQ-specific mobile media queries (1024px
 *   and 600px) were removed; the base .faq-item mobile rule (line
 *   ~8682) applies to .proj-faq-item too since it's also a .faq-item,
 *   keeping mobile sizing consistent site-wide.
 *
 *   The companion inline <style id="jk-proj-faq-fallback"> emitted
 *   by inc/cleanup.php was deleted in the same release — it had
 *   been pinning rules at higher specificity (.single-project scope)
 *   that fought the cascade. Canonical project-FAQ rules now live
 *   in main.css alone.
 *
 * v2.9.315 changelog (2026-05-15):
 *   STALE MINIFIED FILES REMOVED — root cause of v2.9.311-314 not landing.
 *
 *   The asset resolver in functions.php (jk_enqueue_assets, line ~174)
 *   prefers main.min.css over main.css whenever the minified file
 *   exists, and uses filemtime() of the resolved file for the cache-bust
 *   version string. Production has been serving a stale main.min.css
 *   that was last regenerated before the project FAQ tab feature
 *   shipped — meaning weeks of source-file edits never reached the
 *   browser, including:
 *     - v2.9.311 project FAQ panel CSS (.proj-faq-item rules)
 *     - v2.9.314 .proj-faqs__intro editorial typography upgrade
 *     - all other main.css edits since the last minified regeneration
 *   Same problem affected main.min.js: the v2.9.314 FAQ toggle handler
 *   for rail-height re-sync never reached production because the
 *   minified JS was stale.
 *
 *   v2.9.312 attempted to dodge this for the FAQ CSS specifically by
 *   shipping an inline <style> fallback via wp_head. That worked but
 *   was a patch — it didn't address other source edits that were also
 *   silently shadowed by the stale minified file, and there was no
 *   equivalent inline-JS mechanism so the FAQ toggle handler couldn't
 *   reach production at all.
 *
 *   v2.9.315 fix: assets/css/main.min.css and assets/js/main.min.js
 *   are DELETED from the theme ship. WordPress's resolver falls back
 *   to the source files (main.css, main.js), which contain all the
 *   intended edits. CSS payload grows from ~213KB → 545KB uncompressed
 *   on the wire, but SiteGround gzips so the real network cost is
 *   ~+30KB compressed. PageSpeed may dip slightly. Trade correctness
 *   for performance now; regenerate proper minified files in a later
 *   pass once the FAQ work is stable.
 *
 *   The inline FAQ CSS injection in inc/cleanup.php is RETAINED as
 *   belt-and-suspenders defense against future stale .min.css drift
 *   — it's ~1KB of redundant HTML per project page when the source
 *   CSS already has the same rules, but cheap insurance.
 *
 *   NOT addressed in this release: bellville.min.js and analytics.min.js
 *   also show source-vs-min drift. They're unrelated to project FAQ.
 *   Flagged in deployment notes for a follow-up cleanup pass.
 *
 * v2.9.314 changelog (2026-05-15):
 *   1. Project FAQ intro typography upgraded to editorial parity with
 *      .syn-body (Synopsis, Director's Notes, etc). Previous v2.9.311
 *      treatment used a cramped clamp(14-16px) at opacity 0.7, which
 *      read as a footnote rather than an intro lede. Now matches the
 *      canonical project-tab pattern: display font, weight 300, ps-lede
 *      size, prose line-height, full cream opacity, 760px measure.
 *      Affects both the source CSS (main.css line ~2986) and the inline
 *      injection in inc/cleanup.php so the change takes effect even
 *      when production is still serving the stale main.min.css.
 *      Also removed the hardcoded style="max-width: 720px" inline
 *      attribute from the template's intro <p> — measure now governed
 *      by container CSS in one place.
 *   2. Project FAQ content cutoff fixed (assets/js/main.js). The rail
 *      height sync ran at rAF + 300ms + 700ms after tab switch, but
 *      FAQ <details> elements can be toggled at any time afterward,
 *      changing content height post-measurement. The rail stayed
 *      frozen at the initial height, clipping any newly-expanded
 *      content. Added a capture-phase `toggle` listener on the rail
 *      (toggle doesn't bubble, hence capture) that re-runs
 *      syncRailToActive whenever any .proj-faq-item opens or closes.
 *      Throttled via the same rAF-guard pattern as the BTS handler.
 *
 * v2.9.313 changelog (2026-05-15):
 *   Specificity bump on the inline Project FAQ CSS fallback shipped
 *   in v2.9.312. The first attempt emitted .proj-faq-item rules at
 *   the same specificity as stale rules baked into main.min.css from
 *   an earlier prototype. Since main.min.css loads AFTER the inline
 *   <style> in <head>, the cascade dropped our overrides — questions
 *   rendered in dark verd against the dark project panel, unreadable.
 *   Fix: scope every inline selector under .single-project (the body
 *   class WordPress emits on /project/{slug}/ routes), adding one
 *   class of specificity so the inline rules beat any single-class
 *   prefix rule in the minified file. Also switched hover/open color
 *   to var(--verd-light) (#6DBFAD), which is legible on dark, instead
 *   of var(--verd) (#2D6A5B), which is not.
 *
 * v2.9.312 changelog (2026-05-15):
 *   1. wp_robots filter values cast from int to string (functions.php
 *      line 3591). WP core's wp_robots() uses is_string() to decide
 *      between "key:value" and bare-key emission. Integer -1 was falling
 *      into the bare-key branch and producing "max-snippet" / "max-video-preview"
 *      with no values. Schema/DOM audit flagged 18 pages as a result.
 *      Fix: '-1' (string) routes through is_string() and emits
 *      "max-snippet:-1, max-image-preview:large, max-video-preview:-1".
 *   2. Project FAQ CSS injected inline via wp_head as a defensive
 *      fallback (inc/cleanup.php, jk_emit_inline_proj_faq_css). The
 *      v2.9.311 .proj-faq-item rules below (lines 2939–3048) were never
 *      regenerated into main.min.css during the build, so production
 *      (which serves the minified file) was rendering dark-on-dark text
 *      in the project FAQ tab on desktop/tablet. The inline injection
 *      ships the same rules verbatim, scoped to single project pages,
 *      and is harmless once main.min.css is in sync.
 */


/* Accessibility: visually-hidden by default, becomes visible on keyboard focus.
 * Screen readers announce it; sighted users never see it unless they're
 * tab-navigating. Without these rules, the skip link in header.php renders
 * as plain visible text above the nav, looking like a layout bug. */
.sr-only,
.sr-only-focusable:not(:focus):not(:focus-within) {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
.skip-link:focus,
.skip-link:active {
    position: fixed !important;
    top: 16px;
    left: 16px;
    z-index: 10000;
    width: auto;
    height: auto;
    padding: 10px 16px;
    background: var(--ink, #1A1916);
    color: var(--chalk, #F2EDE4);
    font-family: var(--mono, ui-monospace, monospace);
    font-size: 11px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    text-decoration: none;
    clip: auto;
    white-space: normal;
    overflow: visible;
}

:root {
    /* Off-white with deep verdigris — archival, specific, 2026 */
    --chalk:    #F2EDE4;        /* base background — warm off-white */
    --chalk-mid:#E8E1D6;        /* card/section surfaces */
    --chalk-deep:#DDD5C8;       /* borders, deep surfaces */
    --ink:      #1A1916;        /* primary text on light */
    --ink-mid:  #3D3A34;        /* secondary text */
    --ink-dim:  #7A756C;        /* tertiary / muted */
    --verd:     #2D6A5B;        /* verdigris — accent on light backgrounds */
    --verd-deep:#1E4A3F;        /* accent hover / deep */
    --verd-light:#6DBFAD;       /* verdigris on dark backgrounds — AAA contrast */
    --dark:     #161512;        /* dark sections (hero, panels) */
    --dark-mid: #1F1D19;        /* dark cards */
    --dark-card:#272420;        /* dark card surface */
    --cream:    #F2EDE4;        /* text on dark */
    --rule:     rgba(26,25,22,0.10);
    --rule-light:rgba(242,237,228,0.12);
    --rule-strong:rgba(26,25,22,0.22);

    --display: 'Instrument Serif', 'Instrument Serif Fallback', 'Times New Roman', Times, serif;
    --sans:    'Geist', 'Geist Fallback', -apple-system, system-ui, sans-serif;
    --mono:    'Geist Mono', 'Geist Mono Fallback', ui-monospace, monospace;

    /* Project-panel type scale — centralized so content across
       Synopsis, Videos, Press, Gallery, Credits, Similar Work, and
       Director's Notes shares one typographic rhythm. Fluid clamp()
       on mid-tier sizes scales with viewport; labels are fixed
       because compact uppercase mono reads the same at any size. */
    --ps-micro:    9px;                         /* durations, tiny chips */
    --ps-label:    10px;                        /* eyebrows, meta, filter chip labels */
    --ps-body:     clamp(15px, 1.3vw, 16px);    /* secondary prose (similar why) */
    --ps-emphasis: clamp(17px, 1.5vw, 20px);    /* card titles, quotes, names */
    --ps-lede:     clamp(18px, 1.5vw, 22px);    /* synopsis body, director's notes */
    --ps-heading:  clamp(28px, 4vw, 48px);      /* panel titles */

    /* Global type scale — used by hero + section headers across every
       view (films, press, journal, courses, bag, on-location, faq,
       library, home, inquiries). Before this existed each view
       defined its own clamp() and drift crept in: 5 different hero
       title sizes, 4 different lede sizes, 3 different grid-head
       sizes. Consolidating to these tokens means one edit propagates
       to every page and new views inherit consistency by default.
       Only exception: on-location intentionally overrides to a larger
       masthead scale (see .on-location__hero-title). */
    --h-hero-title:    clamp(48px, 9vw, 128px);  /* top-of-page H1 across views */
    --h-poster-title:  clamp(40px, 6vw, 80px);   /* v2.9.196 — section-head display (between hero and section) */
    --h-section-title: clamp(32px, 4vw, 52px);   /* section head above a card grid */
    --h-mid-title:     clamp(28px, 3vw, 44px);   /* v2.9.196 — secondary editorial headings */
    --h-sub-title:     clamp(22px, 2.6vw, 32px); /* sub-section (e.g. journal entry) */
    --h-lede:          clamp(18px, 1.6vw, 22px); /* supporting paragraph under hero title */

    /* Tracking — all-caps mono labels (eyebrows, slates, filter chips,
       section meta) should feel like one typographic voice. Before:
       drift across 0.18, 0.2, 0.22, 0.25, 0.3em. After: --track-wide
       for display labels, --track-mid for inline mono runs.

       Negative tracking (display type): --track-tight for big poster
       display at hero scale, --track-card for mid-size card titles,
       --track-subtle for small display (body-sized headlines). The
       3-tier scale exists because the same tightening proportion
       reads too tight at small sizes and too loose at large sizes. */
    --track-wide:   0.3em;
    --track-mid:    0.2em;
    --track-narrow: 0.15em;     /* v2.9.194 — narrow all-caps mono (smaller labels, dense type) */
    --track-tight:  -0.02em;
    --track-card:   -0.015em;
    --track-subtle: -0.01em;

    /* Line-heights — five-step scale.
       v2.9.114 — added --lh-tight and --lh-display-medium. The codebase
       was already using line-height: 1.05 (very tight display) and
       line-height: 1.2 (medium display, used for card titles, sub-section
       headings, and sub-title typography) extensively. Without tokens
       for these values, raw 1.05 / 1.1 / 1.15 / 1.2 drift was creeping
       in across the codebase. Tokens make the choice explicit. */
    --lh-display-tight:   0.92; /* v2.9.194 — poster-scale hero display (slightly tighter than --lh-display) */
    --lh-tight:           1.05; /* poster display, very large heads */
    --lh-display:         0.95; /* hero titles — tighter than tight */
    --lh-display-medium:  1.2;  /* card titles, sub-sections */
    --lh-body:            1.55; /* reading body */
    --lh-prose:           1.6;  /* longer-form prose */

    /* Spacing — 4px grid. Compound values (paddings, margins) should
       compose from these, not hand-picked numbers. */
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 20px;
    --sp-6: 24px;
    --sp-8: 32px;
    --sp-10: 40px;
    --sp-12: 48px;
    --sp-16: 64px;
    --sp-20: 80px;
    --sp-24: 96px;

    /* Page-edge padding. Single source of truth so heroes, grids, and
       section wrappers share the same gutter rhythm. Adjust on mobile
       via the @media block (see --page-edge-mobile below). */
    --page-edge:        32px;
    --page-edge-mobile: 20px;

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

  * { box-sizing: border-box; margin: 0; padding: 0; }
  html {
    scroll-behavior: smooth;
    -webkit-text-size-adjust: 100%;
    /* Global offset for anchor jumps — the site has a fixed nav
       (~56-68px tall); without this, any #anchor link lands with
       its target hidden behind the nav bar. scroll-padding-top
       reserves that space so anchors land in the visible region
       automatically, for every anchor on every page. */
    scroll-padding-top: 72px;
  }

  /* Italics are opt-in, not default. UI emphasis uses color + weight; italics reserved for quotes and editorial voice. */
  em { font-style: normal; color: var(--verd); font-weight: 500; }
  body {
    background: var(--chalk);
    color: var(--ink);
    font-family: var(--sans);
    font-weight: 400;
    line-height: 1.5;
    overflow-x: hidden;
    /* Prevent pull-down "rubber-band" effect that drags all page content
       past the top edge on iOS Safari + Chrome Android pull-to-refresh.
       Only affects overscroll at the edges — normal vertical scrolling
       inside the page is unaffected. */
    overscroll-behavior-y: none;
    -webkit-font-smoothing: antialiased;
  }
  /* v2.9.124 — Lock background scroll when the hamburger menu is open.
     Replaces the old inline document.body.style.overflow='hidden' set
     by openMenu() in JS. Class-based approach is preferred because
     class changes batch into the next style recalculation, while
     inline style writes force an immediate synchronous reflow that
     can stutter the menu's first-frame paint. */
  body.menu-open {
    overflow: hidden;
  }
  /* v2.9.17 — single-project pages get a dark body bg.
     The project page stacks dark hero → dark tabs-wrap (sticky) →
     dark trigger button → dark panels. All those surfaces are
     var(--dark), but the document body itself is var(--chalk)
     (cream). Sub-pixel gaps, margin-collapse, or transient
     sticky-element paint races on iOS Safari can briefly reveal
     the cream body underneath as the user scrolls — that's the
     "flash" James reported on mobile. Element-level fixes for
     this (matching backgrounds on hero overlay endpoint,
     tabs-wrap, mobile trigger, panels — v2.9.13 + v2.9.16)
     handle the static case but cannot prevent sub-frame paint
     glitches. Making the body itself dark on project pages
     eliminates the underlying surface so even if something
     flickers, the show-through is dark, not cream. The on-
     location page doesn't have this problem because its
     surrounding section paints --ink, fully covering the body
     in the panels region.

     CRITICAL: the .view section that wraps the entire project
     content also has background: var(--chalk) (set globally for
     the SPA-shell view system). That cream paints OVER the body,
     so making body dark wasn't enough — we also need the
     view-project section itself to be dark. The screen recording
     James shared on Apr 25 showed a cream stripe at the bottom
     of the viewport during scroll, which traced back to .view's
     chalk background showing through where child elements (hero
     overlay, tabs-wrap) didn't fully cover (sub-pixel gap or
     iOS Safari's overscroll painting parent bg). Painting both
     the body AND the .view-project section closes both layers. */
  body.single-project {
    background: var(--dark);
  }
  body.single-project .view-project {
    background: var(--dark);
  }

  /* subtle grain — lighter on light bg */
  body::after {
    content: '';
    position: fixed; inset: 0;
    pointer-events: none; z-index: 100;
    opacity: 0.025;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  }

  /* ════════ FILMOGRAPHY TICKER ════════ */
  .leader-band {
    height: 44px;
    display: flex;
    align-items: center;
    overflow: hidden;
    position: relative;
    background: var(--dark);
    border-top: 1px solid rgba(242,237,228,0.08);
    border-bottom: 1px solid rgba(242,237,228,0.08);
    flex-shrink: 0;
  }
  .leader-band::before,
  .leader-band::after {
    content: '';
    position: absolute;
    top: 0; bottom: 0;
    width: 80px;
    z-index: 2;
    pointer-events: none;
  }
  .leader-band::before { left: 0; background: linear-gradient(to right, var(--dark), transparent); }
  .leader-band::after  { right: 0; background: linear-gradient(to left,  var(--dark), transparent); }
  .leader-band__track {
    display: flex;
    align-items: center;
    animation: scroll-leader 90s linear infinite;
    white-space: nowrap;
    gap: 0;
    flex-shrink: 0;
  }
  .leader-band__track:hover { animation-play-state: paused; }
  .leader-band__item {
    display: inline-flex;
    align-items: center;
    gap: 0;
    padding: 0 32px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.7;
    white-space: nowrap;
  }
  .leader-band__item strong {
    color: var(--verd-light);
    font-weight: 500;
    opacity: 1;
  }
  .leader-band__sep {
    display: inline-block;
    width: 4px; height: 4px;
    background: var(--verd-light);
    border-radius: 50%;
    opacity: 0.4;
    margin: 0 32px;
    flex-shrink: 0;
  }
  @keyframes scroll-leader {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
  }

  /* ════════ NAV ════════ */
  .nav {
    position: fixed; top: 0; left: 0; right: 0;
    /* v2.9.121 — Bumped to z-index 1000 (was 110 in v2.9.120, was 50
       originally). The dropdown was still being obscured by page
       content even at z-index 110, suggesting either CSS cache or a
       stacking context I haven't identified. z-index 1000 is high
       enough to clear absolutely anything reasonable on this site.

       v2.9.133 — Note the original z-index map in this comment was
       wrong. The lightbox (z-index: 210) and embed-modal (z-index:
       200) WERE meaningful — the nav was occluding their close
       buttons because both modals are position:fixed with top:0,
       and the nav at fixed top:0 z-index:1000 painted over them.
       Both modals have been bumped to 1200 to clear the nav. The
       current ordering, low to high:
         50    : (default page chrome)
         1000  : .nav
         1100  : .menu-overlay (mobile hamburger drawer)
         1200  : .lightbox, .embed-modal (need to clear nav)
         9998  : mobile press/filter drawer scrim
         9999  : mobile press/filter drawer body
         10000 : skip-link (always reachable for keyboard a11y)

       isolation:isolate explicitly establishes a stacking context
       without relying on the side-effect of backdrop-filter or
       transform doing so implicitly. With this, the nav layer's
       paint order is unambiguous: the entire layer (including the
       dropdown z-indexed inside it) paints above anything in root
       context with z-index < 1000. */
    z-index: 1000;
    isolation: isolate;
    /* Safe-area-inset aware — on iPhone X+ with notch or home
       indicator, env() values push content away from device chrome.
       max() ensures we never go BELOW the baseline padding; on
       non-notched devices env() returns 0 so no visual change. */
    padding: max(18px, env(safe-area-inset-top)) max(32px, env(safe-area-inset-right)) 18px max(32px, env(safe-area-inset-left));
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: rgba(242, 237, 228, 0.94);
    backdrop-filter: blur(20px) saturate(1.4);
    -webkit-backdrop-filter: blur(20px) saturate(1.4);
    border-bottom: 1px solid var(--rule);
    /* v2.9.10 — GPU-promote the nav to fix the iOS Safari paint flash
       James reported on the project page when scrolling past the
       sticky tab bar. The proj-tabs-wrap was already promoted in
       v2.9.8 but the nav itself wasn't — when both layers stacked
       at the same scroll point, iOS sometimes dropped the backdrop-
       filter mid-recomposite, producing a white/cream flash as the
       blur briefly fell back to its base background.

       v2.9.119 — REMOVED the `transform: translateZ(0)` line. It was
       creating a stacking context that prevented the Films dropdown
       from rendering visibly outside the nav bar — even at z-index 60,
       the dropdown was scoped to the nav's compositor layer and
       couldn't paint over the page below.

       The original Safari flash issue is still defended against:
       backdrop-filter itself creates a compositor layer (per spec),
       will-change: backdrop-filter pre-warms the GPU, and the
       backface-visibility rules below reinforce the layer. The
       redundant transform was overkill and broke the dropdown. If
       the flash returns on iOS 18+ Safari, the right fix is to add
       isolation:isolate (which creates a stacking context WITHOUT a
       containing block, so absolutely-positioned descendants still
       escape upward) — not to put the transform back. */
    will-change: backdrop-filter;
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
    /* v2.9.12 — paint containment was here, REMOVED v2.9.118 — see
       comment in commit history. The nav must NOT establish a paint
       containment box because the dropdown needs to render outside it. */
  }
  .nav__brand {
    display: flex;
    align-items: baseline;
    gap: 10px;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink);
    cursor: pointer;
    text-decoration: none;
    z-index: 52;
  }
  .nav__brand .mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px; height: 22px;
    border: 1.5px solid var(--verd);
    border-radius: 50%;
    color: var(--verd);
    font-size: 9px;
    letter-spacing: 0;
    flex-shrink: 0;
  }
  .nav__links {
    display: flex;
    gap: 4px;
    list-style: none;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
  }
  .nav__links a {
    color: var(--ink);
    /* Was opacity 0.5 — flagged by Lighthouse accessibility audit
       for failing WCAG AA contrast (effective ~2.8:1 against the
       cream ground). Bumped to 0.65 to push effective contrast above
       4.5:1 while keeping the muted "secondary nav" feel. The active
       and hover states still bump to 1.0 for full crispness. */
    opacity: 0.65;
    text-decoration: none;
    transition: all 0.2s var(--ease);
    padding: 8px 14px;
    display: inline-flex;
    align-items: center;
    gap: 8px;
  }
  .nav__links a::before {
    content: attr(data-num);
    color: var(--verd);
    font-size: 8px;
    opacity: 0.7;
  }
  .nav__links a:hover, .nav__links a.active { opacity: 1; }

  /* hamburger button — always in nav, hidden on desktop */
  .nav__menu-btn {
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 5px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 10px;
    z-index: 52;
    position: relative;
    /* 44×44 — HIG + WCAG touch-target floor. This is the most-tapped
       control on the site (every mobile nav starts here). 4px bump from
       previous 40×40 matters: under 44 is the boundary between
       "always hits" and "occasionally mis-taps." */
    width: 44px; height: 44px;
  }
  .nav__menu-btn span {
    display: block;
    width: 22px; height: 1.5px;
    background: var(--ink);
    /* Restrict to transform + opacity — both GPU-compositable.
       `transition: all` would silently animate any future property
       added to this rule (layout-triggering ones would jank). */
    transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
    transform-origin: center;
  }
  .nav__menu-btn.open span:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
  }
  .nav__menu-btn.open span:nth-child(2) {
    opacity: 0;
    transform: scaleX(0);
  }
  .nav__menu-btn.open span:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
  }

  /* FULL-SCREEN MENU OVERLAY */
  .menu-overlay {
    position: fixed;
    inset: 0;
    /* v2.9.122 — Bumped to z-index 1100 (was 51).
       The .nav was raised to z-index 1000 in v2.9.121 to fix the
       Films dropdown obscuring issue, but that put the nav ABOVE
       the menu overlay, which paints the chalk-colored nav bar
       through the dark menu overlay. The overlay now sits above
       the nav so it fully covers everything, including the nav's
       backdrop-filter blur layer. */
    z-index: 1100;
    background: var(--ink);
    display: flex;
    flex-direction: column;
    padding: 84px 32px 32px;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    opacity: 0;
    pointer-events: none;
    /* v2.9.124 — Tightened from 0.4s to 0.22s. The 0.4s read as
       laggy; 0.22s is fast enough to feel immediate while still
       being a fade rather than a snap. */
    transition: opacity 0.22s var(--ease);
    /* v2.9.103 — Promote to GPU compositor layer up-front so first
       open isn't gated on layer creation. Without will-change the
       browser only creates the layer when the transition starts,
       which on slow mobile creates a 1-2 frame stutter at the
       beginning of the animation. */
    will-change: opacity;
    transform: translateZ(0);
    /* v2.9.124 — contain:layout style paint isolates the overlay
       from the rest of the page during animation, so the browser
       doesn't have to recompute layout/paint outside the overlay
       on each frame. Major perf win on slow phones. */
    contain: layout style paint;
  }
  .menu-overlay.open {
    opacity: 1;
    pointer-events: all;
  }

  /* left: large nav links */
  .menu-overlay__links {
    display: flex;
    flex-direction: column;
    gap: 0;
    list-style: none;
    flex: 1 0 auto;
    padding: 0;
    margin: 0;
  }
  .menu-overlay__links li {
    border-bottom: 1px solid var(--rule);
    overflow: hidden;
  }
  .menu-overlay__links a,
  .menu-overlay__links button {
    /* v2.9.183 — selector now includes button to support the Films
       row converting from <a> to <button>. Without 'button' in the
       selector, the button would render with default browser chrome
       (border, gray background, centered text, system font) instead
       of the editorial row layout. Match every property the anchor
       gets so the visual is identical. */
    display: flex;
    align-items: baseline;
    gap: 20px;
    padding: 14px 0;
    text-decoration: none;
    color: var(--cream);
    transform: translate3d(0, 12px, 0);
    opacity: 0;
    /* v2.9.124 — Tightened from 0.5s to 0.32s on transform/opacity,
       and start-distance reduced from 20px to 12px so the cascade
       feels brisker without losing the staggered editorial effect.
       Color transition stays at 0.2s for hover crispness.

       v2.9.124 — Removed will-change: transform, opacity. Was causing
       8 separate compositor layers (one per menu item), which is
       wasteful since all items animate in lockstep. The overlay's
       own will-change:opacity already promotes the parent layer; the
       items animate within it without their own layer cost. */
    transition: transform 0.32s var(--ease), opacity 0.32s var(--ease), color 0.2s var(--ease);
    /* Reset button-specific defaults so it visually matches anchors */
    background: none;
    border: 0;
    width: 100%;
    text-align: left;
    font: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: rgba(45, 106, 91, 0.2);
  }
  .menu-overlay.open .menu-overlay__links a,
  .menu-overlay.open .menu-overlay__links button {
    transform: translate3d(0, 0, 0);
    opacity: 1;
  }
  /* staggered animation — v2.9.124 tighter cadence (24ms vs 30ms)
     so the full 8-item cascade reaches rest in ~192ms instead of
     ~270ms. Editorial cascade preserved, perceived snappiness up. */
  .menu-overlay__links li:nth-child(1) a,
  .menu-overlay__links li:nth-child(1) button { transition-delay: 0.02s; }
  .menu-overlay__links li:nth-child(2) a,
  .menu-overlay__links li:nth-child(2) button { transition-delay: 0.04s; }
  .menu-overlay__links li:nth-child(3) a,
  .menu-overlay__links li:nth-child(3) button { transition-delay: 0.06s; }
  .menu-overlay__links li:nth-child(4) a,
  .menu-overlay__links li:nth-child(4) button { transition-delay: 0.08s; }
  .menu-overlay__links li:nth-child(5) a,
  .menu-overlay__links li:nth-child(5) button { transition-delay: 0.10s; }
  .menu-overlay__links li:nth-child(6) a,
  .menu-overlay__links li:nth-child(6) button { transition-delay: 0.12s; }
  .menu-overlay__links li:nth-child(7) a,
  .menu-overlay__links li:nth-child(7) button { transition-delay: 0.14s; }
  .menu-overlay__links li:nth-child(8) a,
  .menu-overlay__links li:nth-child(8) button { transition-delay: 0.16s; }
  .menu-overlay__links li:nth-child(9) a,
  .menu-overlay__links li:nth-child(9) button { transition-delay: 0.18s; }
  .menu-overlay__links li:nth-child(10) a,
  .menu-overlay__links li:nth-child(10) button { transition-delay: 0.20s; }
  .menu-overlay__links a:hover,
  .menu-overlay__links button:hover { color: var(--verd-light); }
  .menu-overlay__links a:hover .menu-link__num,
  .menu-overlay__links button:hover .menu-link__num { color: var(--cream); }

  .menu-link__num {
    /* v2.9.121 — Fixed-width right-aligned column.
       James asked for: "the span menu link num text should be right
       aligned from the left edge, so it isn't bumping the side of
       the screen." So the number lives in a 48px column with text-align
       right, ensuring "01" and "08" both right-align into a clean
       gutter. Width 48px gives a comfortable breathing space between
       the number and the title; the overlay's own 20px left-padding
       creates the screen-edge offset, and the right-align inside
       this 48px column gives a stable visual rhythm. */
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    color: var(--verd-light);
    width: 48px;
    text-align: right;
    flex-shrink: 0;
    transition: color 0.2s var(--ease);
  }
  .menu-link__title {
    /* v2.9.121 — Fixed 20px size at all viewports. Earlier
       implementations used clamp() which produced different sizes for
       different rows depending on text wrapping; James asked for all
       eight rows to be the same size so the menu reads as a clean
       uniform list, not a hierarchy. */
    font-family: var(--display);
    font-weight: 300;
    font-size: 20px;
    letter-spacing: var(--track-subtle);
    line-height: var(--lh-display-medium);
  }
  .menu-link__title em { font-weight: 400; }
  .menu-link__sub {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.45;
    margin-left: auto;
    align-self: center;
  }

  /* bottom row: contact + social */
  .menu-overlay__footer {
    /* v2.9.121 — Stack social row + CTA vertically, both left-aligned
       to match the menu items' left edge.

       v2.9.122 — Indent the footer by 68px so it aligns with the
       TITLE column, not the number column. James clarified the
       intent: "a straight line of text alignment from home to work
       with james" means the visible text (titles + social labels +
       CTA text) all share a left edge. The numbers column is a
       supporting gutter, not the primary alignment line.

       68px = 48px (number column width) + 20px (gap between number
       and title in the .menu-overlay__links a flex layout). Confirmed
       by inspecting the rendered DOM.

       v2.9.124 — Tightened animation: delay 0.4s → 0.2s, duration
       0.5s → 0.3s. Footer appears as the cascade is mid-flight,
       so the menu reaches full rest state ~150ms faster. */
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 20px;
    padding-left: 68px;
    border-top: 1px solid var(--rule);
    padding-top: 20px;
    margin-top: 24px;
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 0.3s var(--ease) 0.2s, transform 0.3s var(--ease) 0.2s;
    flex-shrink: 0;
  }
  .menu-overlay.open .menu-overlay__footer {
    opacity: 1;
    transform: translateY(0);
  }
  
  
  .menu-footer__contact {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    /* This is the primary CTA in the menu — make it visually dominant
       rather than a subtle bordered chip. Filled verd background matches
       the active filter-chip treatment and the "Get in Touch" button in
       the top nav, so it reads as a clear call-to-action. */
    background: var(--verd);
    border: 1px solid var(--verd);
    padding: 14px 20px;
    transition: all 0.3s var(--ease);
  }
  .menu-footer__contact:hover {
    background: var(--verd-deep);
    border-color: var(--verd-deep);
    color: var(--cream);
    /* Arrow nudge on hover for affordance */
    gap: 14px;
  }

  @media (max-width: 1024px) {
    /* v2.9.287 — bumped from 900px to 1024px so iPad Pro 12.9"
       portrait gets the hamburger menu. v2.9.288 — REVERTED back to
       900px. The 1024px change made the menu visibility inconsistent
       with the rest of the responsive system: on-location, films,
       press, journal, library, and home page sections all use 900px
       as their mobile breakpoint, so at 1024px the user would see
       a hamburger menu (signaling mobile mode) above content rendered
       in desktop-density layouts. That visual mismatch reads as a bug.
       Bumping every site-wide 900px breakpoint to 1024px would be
       the consistent move but cascades through hundreds of rules
       that have been tuned for the 900px transition. The cleaner
       answer is keeping nav at 900px, which means iPad Pro 12.9"
       portrait at 1024px sees a slightly cramped desktop nav. That's
       the trade. Users on iPad Pro 12.9" who find the desktop nav
       cramped can rotate to landscape (1366px) where it has full
       breathing room. */
    .nav { padding: max(14px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) 14px max(20px, env(safe-area-inset-left)); }
    .nav__links { display: none; }   /* hide desktop links on mobile */
    .nav__menu-btn { display: flex; }
    /* v2.9.108 — Mobile menu footer position fix.

       Previous structure: .menu-overlay__links had flex: 1 0 auto,
       which pushed the footer block to the absolute bottom of the
       viewport regardless of content height. On phones with home
       indicators, that put the social icons + "Work with James" CTA
       directly at the screen edge, fighting the iOS home indicator
       and Safari bottom toolbar for tap space.

       Fix: stop the greedy push entirely. With flex: 0 0 auto on
       .menu-overlay__links AND no margin-top:auto on the footer,
       the footer sits directly below the last link (Inquiries) with
       its normal margin-top spacing. On a typical phone viewport,
       this lands the social row + "Work with James" CTA in roughly
       the lower-middle third of the screen — where the thumb
       naturally falls — instead of at the bottom edge fighting
       system UI.

       The empty space below the footer is intentional. Better to
       have an unused bottom region than a CTA jammed against the
       home indicator.

       v2.9.110 — Two refinements based on first install feedback:
       1. padding-top bumped 56→72px so the close button (×) has
          clear room above the first link without visually crowding
          "Films" (item 02). The X button sits at top:14px, so 72px
          gives it ~58px of clearance from the link list start.
       2. Footer margin-top reduced 24→4px so it hugs the last link
          (Inquiries) tightly. Previously the gap between Inquiries
          and the social row read as too wide. */
    .menu-overlay {
      /* v2.9.122 — Updated padding to be safe-area-aware on top.
         iOS Safari with notch adds env(safe-area-inset-top) on top,
         which the overlay needs to respect or content slides under
         the dynamic browser chrome. The 20px baseline still applies
         per James' spec, the env() max() ensures it grows for notched
         devices. */
      padding: max(20px, env(safe-area-inset-top, 20px))
               20px
               calc(20px + env(safe-area-inset-bottom, 0px));
    }
    .menu-overlay__links {
      flex: 0 0 auto;
      /* Push the first link below the close button.
         Close button: 24px tall at top:20px (relative to overlay
         padding) → bottom edge ~44px from top of overlay's content
         box. Add 20px breathing room → links start ~64px from
         overlay's content box top. */
      padding-top: 64px;
    }
    .menu-overlay__links a { padding: 12px 0; }
    .menu-link__sub { display: none; }
    /* v2.9.121 — fixed 20px title size per James. Removed the earlier
       clamp(20px, 5vw, 30px). Same size for every menu row, no
       hierarchy. */
    .menu-link__title { font-size: 20px; }
    .menu-footer__rep { font-size: 9px; line-height: var(--lh-prose); }
    /* Footer hugs the link list — small margin so Inquiries and
       the social/CTA row read as closely related, not separated.

       v2.9.111 — additional tightening based on screenshot feedback.
       The previous v2.9.110 values still left ~80px of perceived gap
       because the footer's hairline border-top + 16px padding-top
       added visual weight. Drop the border-top on mobile (the visual
       transition between text-list and icons is clear enough without
       it) and reduce padding-top to 8px. New total above social row:
       4px margin + 8px padding = 12px, down from ~80px perceived.

       v2.9.112 — !important hammer because user has reported this gap
       persisting across THREE consecutive builds despite the cascade
       working correctly in source. Either there's a later rule
       elsewhere in the codebase clobbering these values that I haven't
       located, or browser/CDN caching is involved. !important on
       these four properties guarantees the values reach the browser
       no matter what comes after in the source order. */
    .menu-overlay__footer {
      margin-top: 4px !important;
      padding-top: 8px !important;
      border-top: none !important;
      flex-shrink: 0 !important;
    }
    /* Close button — v2.9.121 sized at 24x24 per James' spec, sits
       within the 20px overlay padding box at top:20 right:20.
       Caret on the Films submenu toggle uses the same 24x24 sizing
       so the two control glyphs read as visually consistent. */
    .menu-close-btn {
      width: 24px;
      height: 24px;
      top: 20px;
      right: 20px;
      border: none;
      border-radius: 0;
    }
    .menu-close-btn span {
      width: 18px;
    }
  }
  @media (min-width: 1025px) {
    /* v2.9.288 — reverted from 1025px back to 901px. See the
       max-width: 1024px revert above for full reasoning. */
    .menu-overlay { display: none !important; } /* desktop: full nav bar, no overlay needed */
  }

  /* demo switcher */
  .demo-tabs {
    position: fixed; bottom: 20px; left: 50%;
    transform: translateX(-50%); z-index: 60;
    display: flex;
    background: var(--ink);
    border: 1px solid var(--rule-strong);
    padding: 4px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-narrow);
    text-transform: uppercase;
  }
  .demo-tabs button {
    background: none; border: none;
    color: var(--cream);
    opacity: 0.5;
    padding: 8px 14px;
    cursor: pointer;
    transition: all 0.2s var(--ease);
  }
  .demo-tabs button.active {
    background: var(--verd);
    color: var(--ink);
    opacity: 1;
  }

  .view { display: none; background: var(--chalk); }
  .view.active { display: block; }

  /* ═══════════════════════════════════════════════════════════════
     HOMEPAGE — biography-centered
     ═══════════════════════════════════════════════════════════════ */

  /* HERO: portrait + name statement + role chips */
  /* BIO HERO — full-bleed video background with overlaid text.
     Video fills the entire section; a dark gradient scrim lies
     between the video and the text for readability. */
  .bio-hero {
    min-height: 100vh;
    min-height: 100svh; /* in-app browser safe — excludes dynamic chrome */
    padding: 100px 32px 48px;
    position: relative;
    display: flex;
    /* v2.9.232 — flex-start at all viewports (was center). Mirrors
       critical.css. The mobile @media override below (originally
       v2.9.130) is now a harmless no-op since the base value matches.
       See critical.css for the full rationale on why centering hurt
       desktop LCP after v2.9.231's parent floor. */
    align-items: flex-start;
    overflow: hidden;
    /* Dark fallback before video loads. */
    background: linear-gradient(135deg, var(--dark-card) 0%, #140a06 100%);
  }

  /* Video layer — absolutely positioned to fill the entire section,
     behind everything else. */
  .bio-hero__portrait {
    position: absolute;
    inset: 0;
    z-index: 0;
    animation: fadeIn 1.4s var(--ease) 0.4s backwards;
  }
  .bio-hero__portrait-inner {
    position: absolute;
    inset: 0;
    overflow: hidden;
  }

  /* Video fills the portrait/section entirely. object-fit: cover
     crops to the section's aspect ratio — no letterboxing.
     Admin can override via Site Content → Home Hero → Video Fit
     to use 'contain' instead (shows whole frame, letterboxes edges). */
  .bio-hero__video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    /* Transparent background so if admin uses object-fit: contain,
       the letterbox area reveals the .bio-hero dark gradient rather
       than the browser's default solid black video background. */
    background: transparent;
    /* Video is purely decorative — users should never interact with it.
       pointer-events: none prevents iOS Safari from treating tap on the
       video area as a request to open native media controls, which has
       been observed causing a flashed "play button" overlay. */
    pointer-events: none;
    /* Start invisible. The <video> element ships with no <source>
       children (JS injects them to avoid double-loading both variants),
       which means the browser would otherwise render a transparent
       rectangle for a beat — letting the page background flash through
       before the video is ready. Starting at opacity 0 means the
       .bio-hero gradient background shows through cleanly. The
       .is-ready class (added by JS on loadeddata, or by a 3-second
       fallback timer for slow networks) fades the video in. */
    opacity: 0;
    transition: opacity 0.6s var(--ease);
  }
  .bio-hero__video.is-ready { opacity: 1; }
  .bio-hero__video--desktop { display: block; }
  .bio-hero__video--mobile  { display: none;  }
  /* Fallback override — JS adds this class when one variant is being
     used as a stand-in for the other (e.g., no desktop video uploaded,
     mobile video shown on desktop instead). Forces display:block to
     override the default variant-hiding rules. */
  .bio-hero__video--fallback-active {
    display: block !important;
  }

  /* Scrim — sits between the video and the text. Left-to-right
     gradient: dense on the left (text readability) fading toward
     transparent on the right (video visibility).
     Also a top-to-bottom fade to deepen the contrast near the text. */
  .bio-hero__scrim {
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background:
      linear-gradient(
        to right,
        rgba(10, 8, 6, 0.78) 0%,
        rgba(10, 8, 6, 0.55) 40%,
        rgba(10, 8, 6, 0.15) 75%,
        rgba(10, 8, 6, 0.05) 100%
      );
  }

  /* Text layer — on top, constrained width so it doesn't sprawl.
     All text goes light since the background is now dark video. */
  .bio-hero__text {
    position: relative;
    z-index: 2;
    max-width: 620px;
    max-width: min(620px, 56%);
    color: var(--cream);
    /* Translate-only entry animation. Previous version used
       `fadeUp` (opacity 0 → 1 + translateY 30px → 0) with a 0.2s delay
       and `backwards` fill, which meant the element sat at opacity:0
       before animating in. On slow 4G mobile where SPAI's JS blocked
       the main thread for 3+ seconds, Chrome couldn't advance the
       keyframe, leaving the LCP element (bio-hero__lede) effectively
       invisible for 3.7s of render delay. Translate-only keeps the
       subtle motion but paints the text immediately on first frame
       regardless of main-thread pressure. */
    animation: slideUp 1.2s var(--ease) 0.2s backwards;
  }
  /* v2.9.102 — Disable entry animation on mobile to fix LCP regression.
     The 1.4s total animation (0.2s delay + 1.2s duration) keeps
     bio-hero__lede in a non-final position when Lighthouse measures
     LCP. Mobile reading is more performance-sensitive; the editorial
     animation is a desktop nicety. */
  @media (max-width: 1024px) {
    .bio-hero__text { animation: none; }
  }
  /* Respect prefers-reduced-motion regardless of viewport */
  @media (prefers-reduced-motion: reduce) {
    .bio-hero__text { animation: none; }
  }

  /* v2.9.134 — fonts-loaded gate. Mirrors critical.css. Holds hero
     text invisible until document.fonts.ready resolves (or timeout)
     so the user never sees Times → Instrument Serif character reflow.
     v2.9.135 — extended to all hero titles + ledes site-wide so deep-link
     cold loads don't flash either. The min-height reservations on each
     element remain as the box-level CLS defense.
     v2.9.146 — REMOVED on-location selectors to fix mobile LCP 11.5s.
     v2.9.147 — RESTORED on-location selectors AND dropped the safety
     timeout in functions.php from 1500ms to 400ms. On real users
     fonts.ready resolves in 100-300ms (timeout doesn't fire). On
     Lighthouse slow 4G fonts.ready takes 4-8s but the 400ms timeout
     fires first, so LCP commits in a healthy window. Net: LCP stays
     good, CLS stays good (was 0.159 mobile in v2.9.146, target ~0.02).
     v2.9.156 — REMOVED .bio-hero__lede, __roles, __meta from this
     gate. PageSpeed Apr 28 8:08PM showed 2,750ms element render delay
     on the home mobile lede LCP. The 400ms timeout still fires, but
     that's still 400ms of held opacity that Lighthouse counts against
     LCP. The lede is body text (font-swap flash from Times to
     Instrument Serif is barely perceptible on body prose), and the
     roles/meta are mono-font (Geist Mono Fallback is metric-matched).
     Removing all three lets them paint immediately in fallback. The
     240px lede min-height floor (set in v2.9.155) absorbs any reflow
     when Instrument Serif loads. .bio-hero__name STAYS gated — it's
     the visually-distinctive Instrument Serif identity element where
     a Times-fallback flash would actually look wrong. */
  /* v2.9.230 — REMOVED .on-location__hero-title and .on-location__hero-lede
     from the gate (matches the change in critical.css). The lede is
     the LCP element on /on-location/ and the gate was holding it at
     opacity:0 long enough to push element render delay to 8 s on
     desktop / 8.6 s on mobile. The min-height reservations on these
     elements (set further down in this file) prevent box-level CLS
     from the font swap; what remains is a brief Times → Instrument
     Serif character reflow that's strictly preferable to multiple
     seconds of blank text.

     v2.9.285 / v2.9.286 — REVERTED v285's addition of landing hero
     classes to this gate. The v285 attempt to fix mobile CLS 0.804
     by gating .landing-hero__title/__lede/__eyebrow successfully
     dropped mobile CLS to 0 BUT introduced two regressions:
       1. Desktop CLS jumped to 0.973 because the gated hero "appearing"
          during the LCP window was being scored as a shift event.
       2. Safari rendered the page mostly white because the
          document.fonts.ready promise + 100ms timeout race was leaving
          the hero invisible long enough to be perceptibly blank.
     The right fix is to address the CLS issue at the layout level
     (stronger min-height reservations on .landing-hero__title and
     .landing-hero__lede further down) rather than at the visibility
     level (gating opacity). See those rules below for the v286
     reservations that absorb the font-swap reflow without making
     the hero invisible. The metric-matched Instrument Serif Fallback
     handles the in-box character reflow without help. */
  .bio-hero__name,
  .mv-hero__title,
  .mv-hero__lede,
  .press-hero__title,
  .press-hero__lede,
  .courses-hero__title,
  .courses-hero__lede,
  .journal-hero__title,
  .journal-hero__lede,
  .faq-hero__title,
  .faq-hero__lede,
  .favs-hero__eyebrow,
  .favs-hero__title,
  .favs-hero__lede,
  .favs-hero__stats,
  .favs-hero__meta,
  .epk-hero__eyebrow,
  .epk-hero__name,
  .epk-hero__roles,
  .epk-hero__reps,
  .epk-hero__jump,
  .epk-hero__portrait-caption,
  .proj-hero__title,
  .proj-hero__meta,
  .proj-hero__slate,
  .proj-photos-hero__title,
  .proj-photos-hero__meta,
  .standalone-page__title {
    opacity: 0;
    transition: opacity 0.18s ease-out;
  }
  html.fonts-loaded .bio-hero__name,
  html.fonts-loaded .mv-hero__title,
  html.fonts-loaded .mv-hero__lede,
  html.fonts-loaded .press-hero__title,
  html.fonts-loaded .press-hero__lede,
  html.fonts-loaded .courses-hero__title,
  html.fonts-loaded .courses-hero__lede,
  html.fonts-loaded .journal-hero__title,
  html.fonts-loaded .journal-hero__lede,
  html.fonts-loaded .faq-hero__title,
  html.fonts-loaded .faq-hero__lede,
  html.fonts-loaded .favs-hero__eyebrow,
  html.fonts-loaded .favs-hero__title,
  html.fonts-loaded .favs-hero__lede,
  html.fonts-loaded .favs-hero__stats,
  html.fonts-loaded .favs-hero__meta,
  html.fonts-loaded .epk-hero__eyebrow,
  html.fonts-loaded .epk-hero__name,
  html.fonts-loaded .epk-hero__roles,
  html.fonts-loaded .epk-hero__reps,
  html.fonts-loaded .epk-hero__jump,
  html.fonts-loaded .epk-hero__portrait-caption,
  html.fonts-loaded .proj-hero__title,
  html.fonts-loaded .proj-hero__meta,
  html.fonts-loaded .proj-hero__slate,
  html.fonts-loaded .proj-photos-hero__title,
  html.fonts-loaded .proj-photos-hero__meta,
  html.fonts-loaded .standalone-page__title {
    opacity: 1;
  }
  @media (prefers-reduced-motion: reduce) {
    .bio-hero__name,
    .mv-hero__title, .mv-hero__lede,
    .press-hero__title, .press-hero__lede,
    .courses-hero__title, .courses-hero__lede,
    .journal-hero__title, .journal-hero__lede,
    .faq-hero__title, .faq-hero__lede,
    .favs-hero__eyebrow, .favs-hero__title, .favs-hero__lede,
    .favs-hero__stats, .favs-hero__meta,
    .epk-hero__eyebrow, .epk-hero__name, .epk-hero__roles,
    .epk-hero__reps, .epk-hero__jump, .epk-hero__portrait-caption,
    .proj-hero__title, .proj-hero__meta, .proj-hero__slate,
    .proj-photos-hero__title, .proj-photos-hero__meta,
    .standalone-page__title {
      transition: none;
    }
  }

  .bio-hero__name {
    font-family: var(--display);
    font-weight: 300;
    font-size: clamp(56px, 8vw, 128px);
    line-height: var(--lh-display-tight);
    letter-spacing: var(--track-tight);
    color: var(--cream);
    margin-bottom: 32px;
    text-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
    /* v2.9.103 — Per-breakpoint min-height reservation to lock the
       hero name container against Instrument Serif font-swap shifts.
       Lighthouse measured 0.05 CLS attributed to <span class="first">
       James</span> when the web font replaced Times New Roman fallback.
       The metric-matched fallback handles most of the swap, but
       bio-hero__name uses font-weight:300 (synthesized from
       Instrument Serif's only available 400 weight), and synthesis
       differs subtly between fallback Times and Instrument Serif.
       Reservation locks the surrounding layout. Two-line layout
       (first + last) at line-height 0.92.

       Mobile ≤700px:    56px × 2 lines × 0.92 = ~104px
       Tablet ≤1100px:   88px × 2 × 0.92      = ~162px (8vw mid)
       Desktop ≥1100px:  128px × 2 × 0.92     = ~236px */
    min-height: 104px;
  }
  @media (min-width: 700px) {
    .bio-hero__name { min-height: 162px; }
  }
  @media (min-width: 1100px) {
    .bio-hero__name { min-height: 236px; }
  }
  .bio-hero__name .first {
    display: block;
  }
  .bio-hero__name .last {
    display: block;
    font-weight: 400;
    color: var(--verd-light);
    position: relative;
    padding-left: 0.4em;
  }
  .bio-hero__name .last::before {
    content: '—';
    position: absolute;
    left: -0.3em;
    color: var(--cream);
    opacity: 0.4;
    font-style: normal;
  }

  .bio-hero__roles {
    display: flex;
    flex-wrap: wrap;
    gap: 0;
    margin-bottom: 36px;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
  }
  .bio-hero__roles span {
    padding: 8px 16px 8px 0;
    position: relative;
  }
  .bio-hero__roles span:not(:last-child)::after {
    content: '/';
    margin-left: 16px;
    color: var(--verd-light);
  }

  .bio-hero__lede {
    font-family: var(--display);
    font-size: var(--h-lede);
    line-height: var(--lh-body);
    color: var(--cream);
    max-width: 540px;
    margin-bottom: 40px;
    /* Shadow under text for readability over the autoplay video —
       the scrim gradient isn't always enough for lighter frames. */
    text-shadow: 0 1px 12px rgba(0,0,0,0.4);
    /* Disable faux-bold / faux-italic synthesis. Instrument Serif only
       ships at weight 400 and the browser's synthesis step can pause
       paint while computing synthesized glyphs. This is the LCP
       element on the home page — every ms of paint delay matters. */
    font-synthesis: none;
  }
  .bio-hero__lede strong {
    /* Emphasis via color only. Instrument Serif is only shipped at
       weight 400 — requesting 600 here forced browser-synthesized
       faux-bold, which adds a paint delay to the LCP element. Keep
       weight at 400 (overriding the default bold from the global
       strong rule) and rely on color shift for emphasis. */
    color: var(--verd-light);
    font-weight: 400;
  }
  /* em tags in the lede (often used for film titles) need to read
     clearly on the dark video background. The global em rule uses
     verd (dark green) + font-weight: 500, which is low contrast over
     dark bg AND triggers a font-weight lookup that doesn't exist for
     Instrument Serif (only 400 ships). Override with verd-light color
     and lock weight to 400 to prevent browser-synthesized faux-bold
     from delaying LCP paint. Color-only emphasis. */
  .bio-hero__lede em {
    color: var(--verd-light);
    font-weight: 400;
  }

  .bio-hero__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 24px 48px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
  }
  .bio-hero__meta-item {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .bio-hero__meta-item .label {
    color: var(--verd-light);
    opacity: 0.9;
  }
  .bio-hero__meta-item .value {
    color: var(--cream);
    font-size: 12px;
    letter-spacing: 0.12em;
  }

  @keyframes fadeUp {
    from { opacity: 0; transform: translateY(30px); }
    to { opacity: 1; transform: translateY(0); }
  }
  /* Translate-only variant — opacity stays at 1 throughout so the
     animated element paints on first frame. Used on .bio-hero__text
     (the LCP element's parent) to avoid keeping the hero text at
     opacity:0 during animation stalls on slow mobile connections. */
  @keyframes slideUp {
    from { transform: translateY(30px); }
    to { transform: translateY(0); }
  }
  @keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
  }

  @media (max-width: 1024px) {
    .bio-hero {
      padding: 96px 20px 32px;
      min-height: 100vh;
      min-height: 100svh;
      /* v2.9.154 — flex-start (REVERTED from v2.9.127 center).

         PageSpeed Apr 28 2026 7:55PM: mobile CLS regressed to 0.595
         with .bio-hero__text as the sole shift culprit. Root cause
         was align-items:center amplifying every pixel of residual
         font-swap height delta into a layout shift twice as large
         as the actual delta (centering pushes the block up by half
         the delta on the top edge AND down by half on the bottom).
         Even with floors holding ~95% of the height stable, the
         remaining 5% wobble was being doubled and dominated CLS.

         flex-start anchors the top edge of .bio-hero__text. The
         floors continue to prevent the block expanding into adjacent
         layout, so any residual font-swap is contained inside the
         block and doesn't shift anything below it.

         The "empty space" James reported in v2.9.115 is addressed
         by the existing 96px top padding which already places the
         text below the nav and reads as intentional headroom rather
         than visual drift. The bio-hero__name's clamp(48px,12vw,72px)
         keeps the title proportional to viewport so the block fills
         the visual space at the top of the hero without needing to
         vertical-center. */
      align-items: flex-start;
    }
    /* Swap which video element is visible — vertical video on mobile. */
    .bio-hero__video--desktop { display: none;  }
    .bio-hero__video--mobile  { display: block; }
    /* On mobile, the text takes full width rather than a left-column
       blob, so the scrim should fade top-to-bottom more than left-to-
       right. Otherwise the right side of the text would be over an
       under-scrimmed area of video. */
    .bio-hero__scrim {
      background:
        linear-gradient(
          to bottom,
          rgba(10, 8, 6, 0.45) 0%,
          rgba(10, 8, 6, 0.65) 50%,
          rgba(10, 8, 6, 0.8) 100%
        );
    }
    .bio-hero__text {
      max-width: none;
    }
    .bio-hero__meta { gap: 20px 32px; }
    .bio-hero__name { font-size: clamp(48px, 12vw, 72px); }
    .bio-hero__lede { font-size: 15px; line-height: 1.5; }
    /* v2.9.159 — Tighten gap margins on mobile to bring the meta block
       ("Member · Director's Branch · Television Academy") into the
       iPhone first-paint viewport. Previous v2.9.157 attempt tried
       tightening the lede min-height floor (240→220) which regressed
       CLS to 0.87 because the actual fallback rendering height sits
       in the narrow 220-240 window. The floor is the CLS-protective
       reservation and must NOT be tightened.
       Instead we reduce the margins ABOVE the meta block. Saves ~32px
       of vertical space on mobile without touching any reservation:
         name margin-bottom  32 → 24  (saves 8px)
         roles margin-bottom 36 → 24  (saves 12px)
         lede margin-bottom  40 → 28  (saves 12px)
       Composition stays comfortable — the existing 240px lede floor
       provides ~30px of internal trailing space that absorbs the
       perception of "tighter" between sections. CLS unaffected because
       no min-height changes. */
    .bio-hero__name { margin-bottom: 24px; }
    .bio-hero__roles { margin-bottom: 24px; }
    .bio-hero__lede { margin-bottom: 28px; }
  }
  /* Respect users who prefer no motion — pause autoplay */
  @media (prefers-reduced-motion: reduce) {
    .bio-hero__video[autoplay] { animation-play-state: paused; }
  }

  /* ════════ BIO SECTION — paper-flipped, unexpected ════════ */
  /* Here's the "something we don't see often" move: flip to paper/cream mid-page */
  .bio-deep {
    background: var(--chalk-mid);
    color: var(--ink);
    /* v2.9.275 — frame pattern unified with .home-services and
       .lineage. Was padding 48px 32px on the section + max-width
       1500 + margin auto on __inner (Pattern C, "bio-deep
       pattern"). At viewports > 1500px that produced a left edge
       ~32px further inboard than .home-reel / .work / .home-services
       / .lineage. Now full-bleed section (vertical padding only)
       so the chalk-mid background still spans edge-to-edge as a
       separator band, and the 1500px column lives on __inner with
       its own horizontal padding. About Me content edge now lines
       up identically with Director's Reel, Direction across formats,
       Selected Work, and the Lineage section at every viewport
       width. */
    padding: 48px 0;
    position: relative;
    /* Skip rendering work while off-screen. Browser reserves the space
       via contain-intrinsic-size but defers layout/paint until the
       section scrolls into view. Biggest win on slow devices where
       rendering the entire document at first paint is the bottleneck. */
    content-visibility: auto;
    contain-intrinsic-size: 0 1800px;
  }
  .bio-deep__inner {
    /* v2.9.275 — became the actual content column. Mirrors
       .home-services__inner and .lineage__inner exactly: max-width
       1500, centered, with its own horizontal padding so descendant
       left edges line up across the home page. */
    max-width: 1500px;
    margin: 0 auto;
    padding: 0 32px;
  }
  .bio-deep__body {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-lede);
    line-height: var(--lh-prose);
    color: var(--ink);
    /* No block-level max-width — let the body expand to the inner
       container's 1500px. The floated photo now sits toward the right
       third of the viewport instead of being crammed into a narrow
       left column. Per-paragraph max-width below keeps line lengths
       readable everywhere, including below the photo where text no
       longer wraps around anything. */
  }
  .bio-deep__body p {
    margin-bottom: 28px;
    /* Cap measure for readability. 68ch is roughly 720–760px at the
       display font's em width, a comfortable line length. Without
       this, paragraphs below the photo (where wrapping ends) would
       stretch to the full 1500px container and line lengths would
       become unreadable. */
    max-width: 68ch;
  }
  .bio-deep__body p:first-of-type::first-letter {
    font-size: 72px;
    float: left;
    line-height: 0.85;
    margin: 6px 14px 0 0;
    font-weight: 400;
    color: var(--verd-deep);
    font-family: var(--display);
  }
  .bio-deep__body em {
 color: var(--verd-deep); }

  .bio-credentials {
    margin-top: 64px;
    padding-top: 40px;
    border-top: 1px solid var(--rule);
    display: grid;
    /* Use auto-fill with a wider min so items fill more screen real estate
       rather than leaving empty right-side space. Each credential wants
       a minimum of ~240px but will stretch to fill. */
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 32px 40px;
  }
  .bio-credential {
    font-family: var(--mono);
    font-size: 11px;
    line-height: var(--lh-prose);
  }
  .bio-credential__label {
    color: var(--verd-deep);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    margin-bottom: 8px;
  }
  .bio-credential__value {
    color: var(--ink);
    font-family: var(--display);
    font-size: 16px;
    font-weight: 400;
    line-height: 1.4;
  }

  /* ── HOME PRESS STRIP ── */
  .home-press {
    padding-bottom: 72px;
    margin-bottom: 72px;
    border-bottom: 1px solid var(--rule);
  }
  .home-press__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-deep);
    margin-bottom: 32px;
    display: flex;
    align-items: center;
    gap: 16px;
  }
  .home-press__eyebrow::before,
  .home-press__eyebrow::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--rule-strong);
  }
  .home-press__logos {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 40px;
    align-items: center;
    margin-bottom: 64px;
  }
  .home-press__logo {
    height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    opacity: 0.6;
    transition: opacity 0.25s var(--ease);
    filter: grayscale(100%);
  }
  .home-press__logo:hover { opacity: 1; filter: grayscale(0%); }
  .home-press__logo img {
    /* v2.9.153 — DO NOT set width:auto or height:auto here. Doing so
       overrides the browser's implicit aspect-ratio computation from
       the <img>'s HTML width/height attributes (e.g. width="300"
       height="39" → aspect-ratio: 300/39). Without that aspect-ratio,
       each logo rendered at 0×0 before the image data loaded, then
       snapped to its actual size on arrival — producing the 0.588 CLS
       that PageSpeed flagged on /press/ desktop (the "press-publications
       Unsized image element" attribution). With max-width:100% and
       max-height:100% only, the browser uses the HTML attrs to reserve
       layout space pre-load, then constrains the image's box via the
       max- limits + object-fit. Layout stays stable from first paint. */
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
  }
  /* Wireframe placeholder — rendered when img src is empty or missing.
   * Instead of showing raw outlet text, we show a skeleton card with a
   * faint dashed border + small monospace label inside. Reads as an
   * intentional placeholder (like a Figma wireframe) rather than a
   * broken state, and signals visually that "real logos go here".
   */
  .home-press__logo img[src=""] + .home-press__logo-fallback,
  .home-press__logo img:not([src]) + .home-press__logo-fallback {
    display: flex;
  }
  .home-press__logo-fallback {
    display: none;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    min-height: 48px;
    border: 1px dashed rgba(26, 25, 22, 0.25);
    border-radius: 2px;
    background:
      repeating-linear-gradient(
        135deg,
        transparent 0,
        transparent 8px,
        rgba(26, 25, 22, 0.03) 8px,
        rgba(26, 25, 22, 0.03) 9px
      ),
      rgba(242, 237, 228, 0.4);
    font-family: var(--mono);
    font-weight: 400;
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: rgba(26, 25, 22, 0.55);
    padding: 6px 12px;
    text-align: center;
  }
  .home-press__logo img[src=""],
  .home-press__logo img:not([src]) { display: none; }

  .home-press__quotes {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 48px;
  }
  .home-press__quote {
    margin: 0;
    padding: 0;
  }
  .home-press__quote blockquote {
    margin: 0 0 20px 0;
    font-family: var(--display);
    font-weight: 300;
    font-style: italic;
    font-size: 19px;
    line-height: 1.5;
    color: var(--ink);
    padding-left: 20px;
    border-left: 2px solid var(--verd);
    letter-spacing: -0.005em;
  }
  .home-press__quote figcaption {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    padding-left: 20px;
  }
  .home-press__quote figcaption em {
    font-style: italic;
    font-family: var(--display);
    text-transform: none;
    letter-spacing: 0;
    font-size: 13px;
    color: var(--ink);
    opacity: 0.8;
  }

  /* ── ABOUT ME HEADING ── */
  .bio-deep__heading {
    /* v2.9.276 — sized to match the canonical home-page heading
       token. Was clamp(36px, 4.5vw, 64px) with -0.025em tracking and
       lh-tight, which rendered visibly smaller than its peer
       headings (Director's Reel, Direction across formats, Selected
       Work, If this work resonates) at every viewport width — at
       iPad portrait About Me was 36px while the others were 44px;
       at desktop About Me was 64px while the others were 80px.
       James spotted the inconsistency. Now uses --h-poster-title
       (clamp 40 → 80) and --track-tight (-0.02em) and --lh-display,
       identical typography to the other home heads. */
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-poster-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
    margin-bottom: 40px;
  }
  .bio-deep__heading em { color: var(--verd-deep); font-style: italic; font-weight: 400; }

  /* ── ABOUT ME PHOTO (inline float, 4:5 headshot) ──
     Classic magazine treatment: figure floats right, paragraphs wrap.
     The ::first-letter drop cap on the first paragraph still works because
     the figure precedes it — the drop cap paints on the p it's attached to. */
  .bio-deep__photo {
    float: right;
    width: clamp(220px, 32%, 340px);
    aspect-ratio: 4/5;
    margin: 6px 0 16px 40px;
    position: relative;
    overflow: hidden;
    background: var(--chalk-mid);
    border: 1px solid var(--rule);
    shape-outside: margin-box;  /* lets text hug the figure cleanly */
  }
  .bio-deep__photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 20%;  /* default face-favoring crop; override when a real photo lands */
    display: block;
  }
  /* Fallback placeholder glyph — visible when img src is empty (pre-upload state) */
  .bio-deep__photo img[src=""],
  .bio-deep__photo img:not([src]) { display: none; }
  .bio-deep__photo-fallback {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-deep);
    opacity: 0.5;
    background:
      linear-gradient(135deg, var(--chalk-mid) 0%, var(--chalk) 100%);
  }
  .bio-deep__photo img[src=""] ~ .bio-deep__photo-fallback,
  .bio-deep__photo img:not([src]) ~ .bio-deep__photo-fallback { display: flex; }

  .bio-deep__photo-caption {
    position: absolute;
    left: 10px;
    bottom: 10px;
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--chalk);
    background: rgba(26,25,22,0.68);
    backdrop-filter: blur(6px);
    padding: 4px 8px;
    z-index: 2;
  }
  /* Hide caption when there's no real photo (placeholder state reads cleaner) */
  .bio-deep__photo img[src=""] ~ .bio-deep__photo-caption,
  .bio-deep__photo img:not([src]) ~ .bio-deep__photo-caption { display: none; }

  @media (max-width: 1024px) {
    .bio-deep {
      /* v2.9.275 — vertical-only padding (the section is now full-bleed).
         Horizontal padding moved to __inner along with the new frame
         pattern. */
      padding: 32px 0;
    }
    .bio-deep__inner { padding: 0 20px; gap: 24px; }
    .bio-deep__heading {
      /* v2.9.276 — match the mobile heading size used by Director's
         Reel and Selected Work (clamp 30 → 44, scaling at 7vw). The
         desktop --h-poster-title clamp keeps About Me consistent with
         peer headings down to roughly 800px viewport, but below that
         the floor (40px) reads slightly large next to the 30 → 44
         range used by other home heads. */
      font-size: clamp(30px, 7vw, 44px);
    }
    .home-press__logos { grid-template-columns: repeat(3, 1fr); gap: 24px; row-gap: 32px; }
    .home-press__logos .home-press__logo:nth-child(4),
    .home-press__logos .home-press__logo:nth-child(5) { grid-column: auto; }
    .home-press__quotes { grid-template-columns: 1fr; gap: 32px; }
    .home-press__quote blockquote { font-size: 17px; }
    /* On narrow viewports the float becomes a top-of-column hero image.
       shape-outside no longer applies so the paragraphs sit cleanly below. */
    .bio-deep__photo {
      float: none;
      width: min(280px, 70%);
      margin: 0 0 32px 0;
    }
  }
  @media (max-width: 560px) {
    .home-press__logos { grid-template-columns: repeat(2, 1fr); }
    .bio-deep__photo { width: 100%; max-width: 320px; }
  }

  /* ════════ FEATURED WORK — back to dark ════════ */
  .work {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
    background: var(--chalk);
    /* Below-the-fold — skip render work until scrolled into view. */
    content-visibility: auto;
    contain-intrinsic-size: 0 1400px;
  }
  .section-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    margin-bottom: 64px;
    gap: 40px;
    flex-wrap: wrap;
  }
  .section-head__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-poster-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
  }
  .section-head__title em {
    /* v2.9.272 — italic restored. The site-wide `em { font-style: normal }`
       reset (line 160) zeros out italic globally so editors can use
       <em> as a verd-color accent inside body prose without it
       slanting. But for display headings — Director's Reel, Selected
       Work, Direction across formats, If this work resonates — the
       intended treatment is italic accent words, matching how About
       Me renders ("About *Me*"). Without this rule, the section-head
       em rendered upright while .bio-deep__heading em rendered
       italic, producing the inconsistency James spotted on the home
       page. */
    color: var(--verd);
    font-weight: 400;
    font-style: italic;
  }
  .section-head__meta {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    text-align: right;
  }
  .section-head__meta-count {
    display: block;
    font-family: var(--display);
    font-size: 32px;
    color: var(--ink);
    margin-top: 4px;
    font-weight: 300;
  }
  /* On mobile, hide the meta ("2013 — Present" + project count) in section
     heads — the vertical space is better used for the heading itself. */
  @media (max-width: 1024px) {
    .section-head__meta { display: none; }
  }

  .work-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 56px 32px;
  }
  .work-card {
    text-decoration: none;
    color: var(--ink);
    cursor: pointer;
    display: block;
    position: relative;
  }
  .work-card__slate {
    /* film slate aesthetic: the numbered tag above the image */
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    padding-bottom: 10px;
    border-bottom: 1px solid var(--rule);
    margin-bottom: 12px;
  }
  .work-card__img {
    aspect-ratio: 4/5;
    overflow: hidden;
    position: relative;
    margin-bottom: 16px;
    background: var(--dark-card);
  }
  .work-card__img::before {
    /* corner brackets — film frame feel. Fade in slightly AFTER the
       inner image begins scaling on hover — reads like a camera
       locking focus AFTER recomposing. Small timing offset (not a
       separate animation) keeps the effect subtle. */
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 2;
    background:
      linear-gradient(to right, var(--verd) 0, var(--verd) 18px, transparent 18px) 0 0 / 100% 1px no-repeat,
      linear-gradient(to bottom, var(--verd) 0, var(--verd) 18px, transparent 18px) 0 0 / 1px 100% no-repeat,
      linear-gradient(to left, var(--verd) 0, var(--verd) 18px, transparent 18px) 100% 100% / 100% 1px no-repeat,
      linear-gradient(to top, var(--verd) 0, var(--verd) 18px, transparent 18px) 100% 100% / 1px 100% no-repeat;
    opacity: 0;
    transition: opacity 0.3s var(--ease) 0.08s;
  }
  .work-card:hover .work-card__img::before { opacity: 1; }
  .work-card__img-inner {
    width: 100%; height: 100%;
    background: linear-gradient(135deg, var(--dark-card) 0%, var(--dark) 100%);
    transition: transform 0.7s var(--ease);
    position: relative;
  }
  /* Note: the `::after` pseudo-element that used to render the card's
     fallback text label (data-label attribute, shown centered in the
     image area when no poster was loaded) has been removed. The
     card's title underneath already identifies the project; a
     duplicated label inside the image block was redundant and, when
     a real poster WAS loaded, still rendered a faint overlay that
     read as a watermark. Removing it cleans up both states. */
  .work-card:hover .work-card__img-inner { transform: scale(1.03); }

  .work-card__title {
    font-family: var(--display);
    font-weight: 400;
    font-size: 26px;
    letter-spacing: var(--track-card);
    line-height: 1.15;
    margin-bottom: 6px;
    color: var(--ink);
  }
  .work-card__title em { color: var(--verd); }
  .work-card__meta {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-narrow);
    color: var(--ink);
    opacity: 0.6;
    text-transform: uppercase;
  }

  @media (max-width: 1024px) {
    .lineage-favorites { margin: 32px 0; padding: 24px 0; }
    .lineage-favorites__grid { grid-template-columns: 1fr; gap: 32px; }
    .lineage-favorites__label::before,
    .lineage-favorites__label::after { display: none; }
  }

  @media (max-width: 1024px) {
    .lineage-film-card__head { flex-direction: column; gap: 8px; }
    .lineage-film-card__rating { padding-top: 0; }
    .lineage-film-card__why { font-size: 15px; padding-left: 14px; }
  }

  @media (max-width: 1024px) {
    .lineage { padding: 32px 20px; }
    .lineage__body { grid-template-columns: 1fr; }
    .lineage__list { border-right: none; }
    .lineage__detail { padding: 32px 0; border-top: 1px solid var(--rule); }
    .lineage-item { padding: 20px 0; grid-template-columns: 20px 1fr auto; gap: 12px; }
    .lineage-item__name { font-size: 18px; }
  }

  /* ════════ PULL QUOTE BAND ════════ */
  
  
  
  
  

  /* ════════ FOOTER ════════ */
  .footer {
    border-top: 1px solid var(--rule);
    padding: 80px 32px 40px;
    background: var(--dark);
  }
  .footer__inner {
    max-width: 1500px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: 2fr 1fr 1fr 1fr;
    gap: 56px;
    margin-bottom: 56px;
  }
  .footer__heading {
    font-family: var(--mono);
    font-size: 9px;
    font-weight: 400;
    letter-spacing: var(--track-wide);
    line-height: 1.4;
    text-transform: uppercase;
    color: var(--verd-light);
    margin: 0 0 20px;
  }
  .footer ul { list-style: none; font-size: 13px; }
  .footer ul li { margin-bottom: 10px; }
  .footer a { color: var(--cream); opacity: 0.6; text-decoration: none; transition: opacity 0.2s; }
  .footer a:hover { opacity: 1; }

  /* Brand column — wordmark + socials. Spans the larger 2fr column in the
     footer grid so the wordmark has room to breathe. */
  .footer__col {
    color: var(--cream);
  }
  .footer__logo {
    font-family: var(--display);
    font-size: 22px;
    font-weight: 400;
    letter-spacing: var(--track-subtle);
    color: var(--cream);
    margin-bottom: 24px;
    line-height: 1;
  }
  .footer__socials {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
  }
  .footer__socials a {
    color: var(--cream);
    opacity: 0.7;
  }
  .footer__socials a:hover {
    opacity: 1;
    color: var(--verd-light);
  }

  /* Sister-product callout — Lift Log card sits in the brand column,
     beneath the social cluster. Treated as a self-contained ad rather
     than a nav link: bordered box, internal hierarchy (eyebrow + title
     + descriptor), distinct from the simple link-list columns to its
     right. Brand-coherent: same mono/serif pairing, verd accent, dim
     ruling. Subdued but legible. */
  .footer__sister {
    display: block;
    margin-top: 32px;
    padding: 18px 20px 20px;
    border: 1px solid rgba(242, 237, 228, 0.12);
    background: rgba(242, 237, 228, 0.02);
    color: var(--cream);
    text-decoration: none;
    opacity: 1;
    max-width: 320px;
    transition:
      border-color 0.25s var(--ease),
      background 0.25s var(--ease);
  }
  .footer__sister:hover,
  .footer__sister:focus-visible {
    border-color: var(--verd);
    background: rgba(45, 106, 91, 0.08);
    outline: none;
  }
  .footer__sister-eyebrow {
    display: block;
    font-family: var(--mono);
    font-size: 9px;
    font-weight: 400;
    letter-spacing: var(--track-wide);
    line-height: 1.4;
    text-transform: uppercase;
    color: var(--verd-light);
    margin-bottom: 10px;
  }
  .footer__sister-title {
    display: block;
    font-family: var(--display);
    font-size: 22px;
    font-weight: 400;
    letter-spacing: var(--track-subtle);
    line-height: 1;
    color: var(--cream);
    margin-bottom: 8px;
  }
  .footer__sister-arrow {
    display: inline-block;
    color: var(--verd-light);
    width: 12px;
    height: 12px;
    margin-left: 6px;
    vertical-align: 1px;
    transition: transform 0.22s var(--ease);
    will-change: transform;
    flex-shrink: 0;
  }
  .footer__sister:hover .footer__sister-arrow,
  .footer__sister:focus-visible .footer__sister-arrow {
    transform: translate(2px, -2px);
  }
  .footer__sister-desc {
    display: block;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.6;
    line-height: 1.4;
  }
  /* Override .footer a opacity rule which would otherwise dim
     the entire card to 0.6 — we manage opacity per-element above. */
  .footer__sister,
  .footer__sister:hover {
    opacity: 1;
  }

  /* Bottom meta row — copyright + credentials. Sits below the 4-col grid
     with a subtle rule separator. Stacks on mobile. */
  .footer__meta {
    max-width: 1500px;
    margin: 0 auto;
    padding-top: 32px;
    border-top: 1px solid var(--rule);
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 12px 32px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.5;
  }
  .footer__meta-ident {
    /* Right-side credentials line. Same treatment as the copyright span
       next to it; the separate class lets us target just this string
       for text-align tweaks on mobile if needed. */
    text-align: right;
  }
  @media (max-width: 600px) {
    .footer__meta {
      flex-direction: column;
      align-items: flex-start;
    }
    .footer__meta-ident {
      text-align: left;
    }
  }
  
  
  
  
  @media (max-width: 1024px) {
    .footer__inner { grid-template-columns: 1fr 1fr; gap: 32px; }
    .footer__bottom { flex-direction: column; gap: 12px; }
  }

  /* ═══════════════════════════════════════════════════════════════
     PROJECT PAGE
     ═══════════════════════════════════════════════════════════════ */

  /* HERO: key art with title overlaid */
  .proj-hero {
    position: relative;
    /* v2.9.128 — Switched height from 85vh to 85svh (small viewport
       height — excludes mobile browser chrome). Mobile browsers
       dynamically resize 'vh' as the address bar shows/hides during
       scroll, which on Lighthouse measurement was triggering a CLS
       hit of 0.649 attributed to .proj-hero__content as the container
       grew/shrank. svh is a stable measurement (the viewport's
       smallest possible state, address bar visible) so the container
       doesn't resize during the page-load lifecycle.

       Trade-off: hero is 60-100px shorter when the address bar is
       hidden. Acceptable for stability — that's only after scroll
       starts, and the hero is fully painted by then anyway. */
    height: 85svh;
    min-height: 560px;
    overflow: hidden;
    display: flex;
    align-items: flex-end;
    padding-top: 80px;
  }
  .proj-hero__bg {
    position: absolute;
    inset: 0;
    background:
      radial-gradient(ellipse at 30% 40%, var(--dark-card) 0%, transparent 60%),
      radial-gradient(ellipse at 70% 70%, var(--dark-card) 0%, transparent 50%),
      linear-gradient(135deg, var(--dark-mid) 0%, var(--dark) 100%);
    z-index: 0;
  }
  .proj-hero__overlay {
    position: absolute;
    inset: 0;
    /* v2.9.13 — overlay endpoint now matches var(--dark) (#161512)
       exactly so the hero's bottom edge transitions seamlessly into
       the tabs-wrap + panels which are also var(--dark). Previously
       the endpoint was rgba(13,10,7,0.95) ≈ #0E0A09 (much darker
       than --dark), then tabs-wrap painted at --ink (#1A1916,
       lighter), then panels painted at --dark again (#161512). Two
       color jumps producing a visible band as the user scrolled
       past the hero. The on-location mobile dropdown doesn't have
       this problem because the on-location section uses --ink
       throughout, so all surfaces below the hero are continuous.
       Project pages now follow the same single-surface pattern. */
    background: linear-gradient(180deg, rgba(13,10,7,0.3) 0%, transparent 30%, var(--dark) 100%);
    z-index: 1;
  }
  .proj-hero__content {
    position: relative;
    z-index: 2;
    max-width: 1500px;
    margin: 0 auto;
    padding: 0 32px 72px;
    width: 100%;
    /* v2.9.33 — reserve height + bottom-anchor children. See critical.css
       for full rationale. Mirrored here so it survives main.css load.

       v2.9.78 — bumped mobile min-height from 280→380px. Lighthouse
       on Angel of Anywhere mobile kept showing CLS 0.583 attributed
       to this container despite v2.9.73's title-min-height fix and
       v2.9.77's hardcoded per-breakpoint values. Hypothesis: actual
       content (back+slate+title+meta) on AoA mobile exceeds 280px
       under one font state but fits under another, expanding/
       contracting the bottom-anchored container. Setting min-height
       larger than ANY plausible content state means the container
       is locked at 380px — font-swap can't change its size because
       content always fits within reservation. Tradeoff: ~50px more
       empty space at top of mobile hero on short-meta projects.
       Worth it for stable CLS. */
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    /* v2.9.101 — bumped mobile min-height 380→460. Lighthouse on
       AoA mobile reported CLS 0.583 again (regression). Title
       font-swap was changing the title's rendered height enough
       that flex-end bottom-anchoring shifted everything visibly.
       460 reserves enough space for the longest realistic content
       state (4-line title + slate + meta + back) so the container
       doesn't grow when fonts swap. */
    min-height: 460px;
    contain: layout;
  }
  @media (min-width: 700px) {
    .proj-hero__content { min-height: 380px; }
  }
  @media (min-width: 1100px) {
    .proj-hero__content { min-height: 440px; }
  }
  .proj-hero__back {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.7;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 40px;
    cursor: pointer;
    transition: opacity 0.2s var(--ease);
  }
  /* v2.9.33 — when proj-hero__back is a flex item inside the new
     proj-hero__content flex column, default align-items:stretch would
     stretch it full-width. Scoped to .proj-hero__content > so the
     empty-state path (back link directly inside .proj-hero) is unaffected. */
  .proj-hero__content > .proj-hero__back {
    align-self: flex-start;
  }
  .proj-hero__back:hover { opacity: 1; color: var(--verd); }
  .proj-hero__slate {
    display: flex;
    gap: 24px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 20px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--rule);
    max-width: 680px;
  }
  .proj-hero__slate span {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .proj-hero__slate span:not(:last-child)::after {
    content: '·';
    margin-left: 24px;
    color: var(--cream);
    opacity: 0.4;
  }
  .proj-hero__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-hero-title);
    line-height: 0.9;
    letter-spacing: var(--track-tight);
    color: var(--cream);
    margin-bottom: 28px;
    /* v2.9.77 — explicit per-breakpoint min-heights to lock title
       reservation against font-swap content shifts. See critical.css
       for full rationale; values must stay in sync between the two
       files. mobile ≤700px: 88px (2 lines of 48px × 0.9), tablet
       700-1100px: 116px (2 × 64px × 0.9), desktop ≥1100px: 204px
       (2 × 112px × 0.9).

       v2.9.108 — bumped mobile from 88px to 140px. Lighthouse on
       Desires of the Heart mobile reported CLS 0.649 attributed to
       .proj-hero__content shifting. Root cause: "Desires of the
       Heart" wraps to 3 lines under one font state (fallback Arial)
       and 2 lines under another (Instrument Serif italic synthesized
       at weight 300). The 88px reservation only covered 2 lines, so
       the container grew/shrank ~40px on font-swap and the meta
       block + slate moved with it. 140px covers the 3-line worst
       case (3 × 48px × 0.9 = 130px + buffer), making the title
       reservation invariant under font-swap.

       Title is bottom-anchored within itself by leaving the natural
       text flow alone — content fills upward from baseline, so when
       text is shorter than reservation, padding shows above text not
       below. This matches the .proj-hero__content flex-end pattern. */
    min-height: 140px;
  }
  @media (min-width: 700px) {
    .proj-hero__title { min-height: 116px; }
  }
  @media (min-width: 1100px) {
    .proj-hero__title { min-height: 204px; }
  }
  .proj-hero__title em { font-weight: 400;
 color: var(--verd-light);
 }
  .proj-hero__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 24px 40px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    /* v2.9.131 — Reserve worst-case wrap height. 4 meta items
       (ROLE, RUNTIME, COUNTRY, DISTRIBUTION) are mono 10px caps in
       a flex-wrap row. Geist Mono vs Arial fallback widths differ
       just enough that font-swap can flip the wrap count from 2
       rows to 3 rows on 412px-wide phones, which on Angel of
       Anywhere mobile registered as a CLS hit of 0.649 against
       .proj-hero__content (the meta block sits inside it and
       pushes content height through font-swap). Each meta-item is
       label (10px) + value (12px) stacked with 4px gap = ~30px
       tall. With 24px row gap, 3 rows worst case = ~108px. Setting
       min-height: 110px (mobile) holds the block stable across
       1/2/3-row wrap states on any width. Tablet+ has wider canvas
       so worst case is 2 rows = ~64px. Desktop is 1 row at 1500px
       container width = ~30px so reservation can drop to 32px. */
    min-height: 110px;
  }
  @media (min-width: 700px) {
    .proj-hero__meta { min-height: 64px; }
  }
  @media (min-width: 1100px) {
    .proj-hero__meta { min-height: 32px; }
  }
  .proj-hero__meta-item {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .proj-hero__meta-item .label {
    color: var(--verd-light);
    opacity: 0.8;
  }
  .proj-hero__meta-item .value {
    color: var(--cream);
    font-size: 12px;
    letter-spacing: 0.12em;
  }

  /* v2.9.131 — Slate is "NARRATIVE SHORT · 2018" type content.
     Single line on most projects but Cat label + Year can wrap on
     narrow viewports during font-swap. flex-wrap allows 2 rows.
     Worst case = 2 rows of mono 10px caps + 16px padding-bottom +
     1px border + 20px margin-bottom = ~52px. Single line is ~28px.
     Reserve 52px on all sizes — the difference is small enough
     that the reservation overhead is negligible. */
  .proj-hero__slate { min-height: 52px; }

  @media (max-width: 1024px) {
    .proj-hero__content { padding: 0 20px 48px; }
    .proj-hero__back { margin-bottom: 24px; }
    .proj-hero__slate { flex-wrap: wrap; gap: 12px; }
    .proj-hero__slate span:not(:last-child)::after { margin-left: 12px; }
    .proj-hero__meta { gap: 16px 28px; }
  }

  /* MOBILE DROPDOWN NAV — replaces the horizontal tab strip on narrow
     viewports. Hidden on desktop by default; shown at ≤900px via the
     mobile media query below. Tab strip gets the opposite treatment. */
  .proj-tabs-mobile__trigger {
    display: none; /* desktop: hidden; mobile media query shows it */
    width: 100%;
    padding: 14px 20px;
    /* v2.9.16 — bg now matches the surrounding page surfaces.
       The hero overlay ends at var(--dark), proj-tabs-wrap is
       var(--dark), proj-panels is var(--dark). The trigger sits
       between hero and panels and was previously var(--ink) =
       #1A1916 — a noticeably lighter band. Switching to var(--dark)
       makes the trigger blend with its parents so the user sees
       one continuous surface from hero through panels. The on-
       location mobile trigger stays at var(--ink) because that
       page uses --ink throughout, so the trigger blends naturally
       there. */
    background: var(--dark);
    /* Full 1px border so the element reads as a button rather than a
       banner/header. Top border in verd to match site accent. */
    border: 1px solid var(--rule);
    border-top: 2px solid var(--verd);
    color: var(--cream);
    cursor: pointer;
    align-items: center;
    /* Disable double-tap-to-zoom. iOS Safari zooms the viewport when
       the user double-taps any clickable element that doesn't opt out
       explicitly; `manipulation` keeps single taps responsive while
       killing the double-tap gesture. Single-finger pan + pinch-zoom
       for accessibility still work. */
    touch-action: manipulation;
    /* v2.9.8 — kill the iOS Safari tap-highlight grey flash. When the
       user scrolls and their finger passes over the dropdown, iOS
       momentarily fires :active which previously toggled the bg
       color (creating a "white flash" from cream-tinted overlay).
       Setting tap-highlight to transparent disables the system gray
       overlay; user-select:none prevents the iOS callout that can
       also briefly flash. */
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
    user-select: none;
    gap: 14px;
    text-align: left;
    font: inherit;
    /* Display flex is set inside the media query, so all children
       (helper label block + chevron) line up on one row. */
    position: relative;
    transition: background 0.18s var(--ease), border-color 0.18s var(--ease);
  }
  /* v2.9.8 — REMOVED the :hover and :active background-change rules
     for mobile. iOS Safari fires :active transiently during scroll
     when the finger passes over the element, which made the dropdown
     bg flash from --ink to --ink-mid mid-scroll. Now only
     [aria-expanded="true"] (an actual stateful change) shifts the
     background. Hover preserved for desktop via the @media
     (hover: hover) query below. */
  .proj-tabs-mobile__trigger[aria-expanded="true"] {
    background: var(--ink-mid, #1f1e1b);
    border-color: var(--verd-light);
  }
  /* Desktop hover only — mobile devices typically don't match
     hover: hover, so this rule sits dormant on touch devices. */
  @media (hover: hover) {
    .proj-tabs-mobile__trigger:hover {
      background: var(--ink-mid, #1f1e1b);
      border-color: var(--verd-light);
    }
  }
  .proj-tabs-mobile__trigger:focus-visible {
    outline: 2px solid var(--verd-light);
    outline-offset: 2px;
  }
  /* Helper eyebrow — renamed from a single "SECTION" label to a
     two-part "SECTION · TAP TO SWITCH" pair. The call-to-action
     half telegraphs interactivity so admin doesn't have to guess
     whether this is a header or a button. Rendered via ::before
     so markup stays unchanged. */
  .proj-tabs-mobile__trigger::before {
    content: 'SECTION · TAP TO SWITCH';
    position: absolute;
    top: 8px;
    left: 20px;
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-wide);
    color: var(--verd);
    opacity: 0.85;
  }
  .proj-tabs-mobile__trigger-num {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    color: var(--verd-light);
    opacity: 0.7;
    /* Push down below the "SECTION" eyebrow. */
    margin-top: 14px;
  }
  .proj-tabs-mobile__trigger-label {
    font-family: var(--display);
    font-weight: 400;
    font-size: 20px;
    color: var(--cream);
    flex: 1;
    line-height: 1;
    /* Align the label with the number beneath the eyebrow. */
    margin-top: 14px;
  }
  .proj-tabs-mobile__trigger-chev {
    /* Larger chevron (was 14x10) for clearer tap affordance. */
    width: 18px;
    height: 12px;
    color: var(--verd-light);
    opacity: 1;
    transition: transform 0.2s var(--ease);
    flex-shrink: 0;
    /* Align with text, not eyebrow. */
    margin-top: 14px;
  }
  .proj-tabs-mobile__trigger[aria-expanded="true"] .proj-tabs-mobile__trigger-chev {
    transform: rotate(180deg);
  }
  .proj-tabs-mobile__trigger:focus-visible {
    outline: 2px solid var(--verd-light);
    outline-offset: -2px;
  }

  .proj-tabs-mobile__menu {
    position: absolute;
    top: 100%; left: 0; right: 0;
    margin: 0;
    padding: 8px 0;
    list-style: none;
    background: var(--ink);
    border-bottom: 1px solid var(--rule);
    z-index: 41;
    max-height: 70vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
  }
  .proj-tabs-mobile__menu[hidden] { display: none; }
  .proj-tabs-mobile__menu li { list-style: none; }
  .proj-tabs-mobile__item {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    padding: 14px 20px;
    background: none;
    border: none;
    color: var(--cream);
    cursor: pointer;
    text-align: left;
    font: inherit;
    opacity: 0.7;
    transition: opacity 0.15s var(--ease), background 0.15s var(--ease);
    /* Same double-tap-zoom guard as the trigger above. */
    touch-action: manipulation;
  }
  .proj-tabs-mobile__item:hover,
  .proj-tabs-mobile__item:focus-visible {
    opacity: 1;
    background: rgba(109, 191, 173, 0.08);
    outline: none;
  }
  .proj-tabs-mobile__item.is-active {
    opacity: 1;
  }
  .proj-tabs-mobile__item.is-active .proj-tabs-mobile__item-label {
    color: var(--verd-light);
  }
  .proj-tabs-mobile__item-num {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    color: var(--verd-light);
    opacity: 0.7;
    min-width: 24px;
  }
  .proj-tabs-mobile__item-label {
    font-family: var(--display);
    font-weight: 400;
    font-size: 18px;
    line-height: 1.1;
  }

  /* STICKY TAB BAR — horizontal swipe on mobile */
  .proj-tabs {
    position: sticky;
    top: 0;
    z-index: 40;
    background: var(--ink);
    border-top: 1px solid var(--verd);
    border-bottom: 1px solid var(--rule);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .proj-tabs::-webkit-scrollbar { display: none; }
  .proj-tabs__inner {
    display: flex;
    max-width: 1500px;
    margin: 0 auto;
    padding: 0 32px;
  }
  .proj-tab {
    flex-shrink: 0;
    background: none;
    border: none;
    padding: 22px 28px 22px 0;
    margin-right: 12px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.65;
    cursor: pointer;
    white-space: nowrap;
    position: relative;
    transition: opacity 0.2s var(--ease);
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .proj-tab:hover { opacity: 0.92; }
  .proj-tab.active { opacity: 1; color: var(--cream); }
  .proj-tab.active::after {
    content: '';
    position: absolute;
    bottom: -1px; left: 0;
    width: calc(100% - 28px);
    height: 2px;
    background: var(--verd);
  }
  /* Numbers/counts inherit cream from the tab label so they track the
     same active/inactive state as the rest of the tab. Originally these
     used --verd-light (teal) which read as a "wrong color" against the
     cream label — at the inactive 0.65 opacity, teal washed out into
     a muddy mid-tone that James described as blending with the dark
     background. Cream at 0.65 effective opacity reads as a clear muted
     gray; cream at full opacity (active) reads as bright, mirroring
     the label's own state. */
  .proj-tab__num {
    color: var(--cream);
    font-size: 9px;
    opacity: 1;
  }
  .proj-tab__count {
    color: var(--cream);
    font-size: 9px;
    opacity: 1;
  }
  @media (max-width: 1024px) {
    /* Swap tab strip → dropdown on mobile */
    .proj-tabs,
    .proj-tabs__fade { display: none; }
    .proj-tabs-mobile__trigger { display: flex; }
    /* Menu anchors to wrap via absolute positioning */
    .proj-tabs-wrap { position: relative; }

    .proj-tabs__inner { padding: 0 20px; }
    .proj-tab { padding: 18px 20px 18px 0; }
    /* Tab strip is hidden on mobile anyway, but keep the touch-action
       guard in case desktop CSS media query is bypassed. */
    .proj-tabs {
      touch-action: none;
    }
  }

  /* HORIZONTAL SWIPE PANELS */
  /* Desktop: fixed viewport height — tabs stay locked, content scrolls within each panel.
     Mobile: native CSS scroll-snap handles horizontal swipe; vertical within panels scrolls naturally. */
  .proj-panels {
    position: relative;
    overflow: hidden;
    background: var(--dark);
    color: var(--cream);
  }
  /* All text inside panels defaults to cream (on dark bg). The .proj-panel--paper
     modifier inverts this for panels that use the light paper background. */
  .proj-panel { color: var(--cream); }
  .proj-panel--paper { color: var(--ink); }
  .proj-panels__rail {
    display: flex;
    align-items: flex-start;
    transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1),
                height 0.3s var(--ease);
    will-change: transform;
  }
  .proj-panel {
    flex: 0 0 100%;
    min-width: 100%;
    overflow-y: visible;
    overflow-x: hidden;
    padding: 56px 32px 48px;
    box-sizing: border-box;
    /* Note: vid-social-row uses negative margins to escape this overflow */
    /* custom scrollbar */
    scrollbar-width: thin;
    scrollbar-color: var(--verd) transparent;
  }
  .proj-panel::-webkit-scrollbar { width: 3px; }
  .proj-panel::-webkit-scrollbar-track { background: transparent; }
  .proj-panel::-webkit-scrollbar-thumb { background: var(--verd); border-radius: 0; }
  .proj-panel__inner {
    max-width: 1400px;
    margin: 0 auto;
  }

  /* on mobile, panels change via tap (no horizontal swipe) — same
     primitive as desktop. Rail uses transform: translateX driven by
     JS in goToPanel. No swipe gesture competes with vertical scroll. */
  @media (max-width: 1024px) {
    /* Mobile: same model as desktop. The rail is a flex row that
       translates horizontally via transform. Panels are siblings of
       fixed 100% width. No native scroll on the container. */
    .proj-panels {
      overflow: hidden;
    }
    .proj-panels__rail {
      align-items: flex-start;
      /* Keep the transform transition so tap-driven changes slide
         between panels smoothly (like desktop). */
    }
    .proj-panel {
      /* Panels size to their own content on mobile. No min-height
         lock — each tab can be short or tall independently. */
      padding: 40px 20px 48px;
      /* Content flows into normal page scroll — the PAGE scrolls,
         not the panel. Hero above and footer below stay reachable
         as the user scrolls through the project. */
      overflow-y: visible;
      overflow-x: hidden;
    }
    /* Global double-tap-zoom guard across ALL project-page interactive
       elements on mobile. iOS Safari zooms the viewport on double-tap
       of any clickable element unless `touch-action: manipulation`
       opts out — this catches video/press cards, FAQ toggles,
       filmography entries, chip filters, and anything else tappable
       in the project panels. Single-tap responsiveness + pinch-zoom
       accessibility are preserved. */
    .proj-panel a,
    .proj-panel button,
    .proj-panel [role="button"],
    .proj-video-card,
    .proj-press-card,
    .proj-gallery-tile,
    .proj-faq-item summary,
    .proj-filmography-item,
    .proj-mobile-nav__item,
    .proj-chip {
      touch-action: manipulation;
    }
  }

  /* swipe hint — sits inline with panel-head title, appears on mobile only */
  .swipe-hint {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    opacity: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    pointer-events: none;
    animation: hintPulse 2.4s var(--ease) 0.8s 3 forwards;
    margin-left: auto;
  }
  @keyframes hintPulse {
    0% { opacity: 0; transform: translateX(6px); }
    25% { opacity: 0.9; transform: translateX(0); }
    75% { opacity: 0.9; transform: translateX(0); }
    100% { opacity: 0; transform: translateX(-6px); }
  }
  @media (min-width: 1025px) { .swipe-hint { display: none; } }

  /* panel-internal patterns */
  .panel-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    border-bottom: 1px solid var(--rule);
    padding-bottom: 20px;
    margin-bottom: 56px;
  }
  /* Tighter bottom gap on mobile. 56px is appropriate for desktop
     rhythm but leaves too much dead space between the section heading
     and the first interactive element (e.g. the "Press" heading and
     the Filters button underneath). 28px keeps the heading visually
     anchored to the underline rule while letting the next block sit
     close enough to feel related. Applies across all panels (Synopsis,
     Videos, Press, Gallery, Notes, Similar Work) because the same
     compressed rhythm reads right on every tab on small screens. */
  @media (max-width: 1024px) {
    .panel-head {
      margin-bottom: 28px;
    }
  }
  .panel-head__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--ps-heading);
    line-height: 1;
    letter-spacing: var(--track-tight);
    color: var(--cream);
  }
  .panel-head__title em { color: var(--verd-light); }
  

  /* SYNOPSIS PANEL — paper inversion */
  
  .proj-panel--paper .panel-head {
    border-bottom-color: var(--rule);
  }
  .proj-panel--paper .panel-head__title { color: var(--ink); }
  .proj-panel--paper .panel-head__title em { color: var(--verd); }
  
  /* v2.9.53 — Synopsis tab desktop layout. Two-column grid that mirrors
     the Similar Work tab: prose + credits on the left, reception block
     (RT badge → poster → audience reviews) on the right as a sticky
     aside. The left column carries the same .proj-credits sections it
     always did (Where to Watch, Cast, Creative, Crew) — those have NOT
     moved. Only the Reception block was hoisted out into the right
     column so a project's RT score and audience pull-quotes get the
     visual prominence of a sticky sidebar instead of being buried at
     the bottom of the credits stack.
     
     The aside is sticky so the RT badge + poster + reviews remain in
     view as the user scrolls through the credits and watch list on the
     left. When the user reaches the bottom of the longer column, the
     sticky aside naturally releases. */
  .proj-synopsis {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    column-gap: 64px;
    align-items: start;
  }
  .proj-synopsis__visual {
    margin: 0;
    position: sticky;
    top: 32px;
    /* v2.9.69 — aside now contains at most TWO sections (poster +
       RT score) since audience reviews moved to a full-width section
       below the grid. With only 2 items, the gap between them just
       needs enough breathing room — 40px matches the rest of the
       page rhythm without the awkwardness of varying based on
       presence/absence of a third item. */
    display: flex;
    flex-direction: column;
    gap: 40px;
  }
  /* Poster image — desktop only, sized to look like a video-store
     thumbnail / movie poster. The 220px max width keeps the visual
     weight balanced against the RT badge below. Hard-bordered with
     a subtle shadow so it reads as a printed object, not a hero.

     v2.9.67 — enforce 2:3 portrait aspect via aspect-ratio + cover.
     Previously height was auto, so an accidentally-landscape source
     (e.g. a manually-pasted URL pointing at the wrong image) would
     render short-and-wide instead of the expected portrait shape.
     With aspect-ratio:2/3 + object-fit:cover, any source crops
     cleanly to portrait — square images center-cropped, landscape
     center-cropped, portraits filling the box naturally. */
  .proj-synopsis__poster {
    margin: 0;
  }
  .proj-synopsis__poster img {
    display: block;
    width: 100%;
    max-width: 220px;
    aspect-ratio: 2 / 3;
    object-fit: cover;
    object-position: center;
    border-radius: 4px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
  }
  /* The reception section inside the aside doesn't get the parent
     .proj-credits 56px gap, so we rely on the aside's own gap.
     Reset any inherited margin from .proj-credits__section. */
  .proj-synopsis__visual .proj-credits__section--reception {
    margin: 0;
  }

  /* v2.9.69 — Audience reviews row, full-width below the synopsis
     grid on desktop. Lives INSIDE .proj-synopsis (the grid container)
     so it participates in both desktop grid layout and mobile flex
     ordering. grid-column: 1 / -1 spans the full grid width on
     desktop, placing the audience cards on a new row beneath the
     prose+credits column AND the poster+RT aside. 56px gap above
     matches the rest of the page rhythm between major sections. */
  .proj-audience-row {
    grid-column: 1 / -1;
    margin-top: 56px;
  }
  .proj-audience-row .proj-credits__eyebrow {
    margin-bottom: 24px;
  }
  
  /* Mobile (≤900px) collapses the grid to a flex column with explicit
     ordering. v2.9.73 spec — audience reviews moved to LAST position
     on mobile to match desktop, so the rhythm is identical on both:
       1. Prose (synopsis body)
       2. RT badge (in the aside)
       3. Cast / Creative Team / Crew (.proj-credits stack)
       4. Where to Watch
       5. Audience Reviews
     
     display:contents on .proj-synopsis__main flattens its children
     (.syn-body, .proj-credits, .proj-watch) into the parent flex
     container so each can receive its own `order` value.
     
     v2.9.73 order map:
       -2  .syn-body              (prose first)
       -1  .proj-synopsis__visual (poster hidden + RT score)
        0  .proj-credits          (Cast, Creative Team, Crew)
        1  .proj-watch            (Where to Watch)
        2  .proj-audience-row     (audience cards last)
     
     Poster is hidden on mobile so the column doesn't get crowded —
     the project page hero image at the top already does that visual
     job on small screens. */
  @media (max-width: 1024px) {
    .proj-synopsis {
      display: flex;
      flex-direction: column;
      column-gap: 0;
    }
    .proj-synopsis__main {
      display: contents;
    }
    .proj-synopsis__main .syn-body { order: -2; }
    .proj-synopsis__main .proj-credits { order: 0; }
    .proj-synopsis__main .proj-watch { order: 1; }
    .proj-synopsis__visual {
      order: -1;
      position: static;
      top: auto;
      /* v2.9.75 — symmetric rhythm around RT. Per user feedback the
         gap above RT (end of synopsis paragraph) should equal the
         gap below RT (before Cast). Each side gets 24px (margin) +
         the adjacent flex item's margin (24px on the synopsis
         paragraph's bottom; 24px on Cast's new mobile top) = 48px
         visible gap on both sides. Matches the natural paragraph
         break feel the synopsis text already has. */
      margin: 24px 0;
      gap: 40px;
    }
    .proj-synopsis__poster {
      display: none;
    }
    .proj-audience-row {
      order: 2;
    }
  }


  .syn-body {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--ps-lede);
    line-height: var(--lh-prose);
    color: var(--cream);
    max-width: 760px;
  }
  .proj-panel--paper .syn-body { color: var(--ink); }
  .syn-body p { margin-bottom: 24px; }
  /* Drop cap — targets the very first paragraph of the syn-body whether
     paragraphs are direct children (Synopsis layout) or nested inside
     a .syn-body__notes-prose wrapper (Director's Notes two-column layout).
     The two selectors are listed explicitly so the drop cap appears in
     both contexts. */
  .syn-body > p:first-child::first-letter,
  .syn-body__notes-prose > p:first-child::first-letter {
    font-size: 72px;
    float: left;
    line-height: 0.85;
    margin: 8px 14px 0 0;
    font-weight: 400;
    color: var(--verd);
    font-family: var(--display);
  }
  .syn-body em { color: var(--verd-light); }
  .proj-panel--paper .syn-body em { color: var(--verd); }

  /* ─── Director's Notes: two-column layout on desktop ─────────────
     Prose stays in the left column using the existing .syn-body
     typography (font, size, drop cap, em accents). The project's
     hero image sits in the right column as an editorial anchor that
     fills what was previously empty right-half space. Mobile
     collapses to single column and hides the duplicate image
     (the hero already shows above the tabs on mobile). */
  .syn-body--notes {
    /* Override the default 760px max-width so the two-column grid
       can span the full panel width. The inner .syn-body__notes-prose
       keeps the comfortable reading measure for prose. */
    max-width: none;
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: 64px;
    align-items: start;
  }
  .syn-body__notes-prose {
    /* Preserve the original prose measure — wrapping this in a grid
       child lets us keep 760px reading width even though the outer
       grid spans the panel. */
    max-width: 760px;
  }
  .syn-body__notes-visual {
    /* Sticky so the image stays visible as the user scrolls long
       Director's Notes. Top offset clears the panel-head height. */
    position: sticky;
    top: 32px;
    margin: 0;
  }
  .syn-body__notes-visual img {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--rule-strong);
  }
  .proj-panel--paper .syn-body__notes-visual img {
    border-color: var(--rule);
  }

  /* ─── Similar Work: two-column layout on desktop ────────────────
     Primary content (film/director lists) in the left column; the
     positioning statement blockquote anchors the top of the right
     column with the Companion Reading card stacked beneath it as
     editorial context.

     v2.9.51 — quote and companion are now siblings of .lists (was
     a single .visual aside wrapping both). This lets each piece be
     ordered independently per breakpoint. Desktop result is visually
     unchanged: grid-template-areas pins both right-column items
     into the same column with the lists spanning the rows on the
     left. Mobile result: quote goes above the lists (editorial hook
     before the recommendations), and companion drops to the bottom
     so it doesn't interrupt the films + directors flow. */
  /* v2.9.311 — Project FAQ panel.
     Project panels have a dark background (var(--dark)) with cream
     text (var(--cream)). The base .faq-item CSS (in the /faq/ page
     section below) uses var(--ink) for text and var(--rule) for
     borders, both of which target a CREAM background. On the dark
     project panel that color combination is unreadable.
     This block overrides the relevant tokens specifically when
     .faq-item appears inside .proj-faqs, leaving the /faq/ page
     styling untouched. */
  .proj-faqs {
    max-width: 880px;
    /* margin-top gives breathing room from the panel head (which is
       common across all tabs — Synopsis, Videos, Press, etc.). */
    margin-top: 8px;
  }
  .proj-faqs__intro {
    /* v2.9.314 — Match the editorial typography of other project tabs
       (.syn-body for Synopsis, Director's Notes etc). Previously this
       was sized like a footnote (clamp 14-16px, opacity 0.7), which
       broke parity with the rest of the project page. Now uses the
       same display font + ps-lede size + prose line-height + full
       cream opacity that .syn-body uses, so the intro paragraph reads
       as proper editorial lede before the accordion list begins. */
    margin: 0 0 32px;
    color: var(--cream);
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--ps-lede);
    line-height: var(--lh-prose);
    max-width: 760px;
  }
  .proj-faqs__intro p { margin: 0; }
  .proj-faqs__list {
    /* Use --rule-light (rgba cream at 0.12) — the codebase's
       canonical token for borders on dark backgrounds. Mirrors the
       polarity of --rule (rgba ink at 0.10) used on light bg. */
    border-top: 1px solid var(--rule-light);
  }
  /* Override base .faq-item dark-on-light colors → light-on-dark
     for the project panel context. Selector specificity uses the
     .proj-faq-item modifier class added to each <details> in the
     project template, so this only applies inside project panels. */
  .proj-faq-item {
    border-bottom: 1px solid var(--rule-light);
  }
  .proj-faq-item .faq-item__q {
    /* v2.9.316 — Size matches the standalone /faq/ page question
       (base .faq-item__q at line ~8647). Previously sized smaller
       (clamp 18-22) which made questions read as captions under the
       answer body. Now inherits the base clamp(20-26) and only
       overrides color for the dark project-panel context. */
    color: var(--cream);
  }
  /* Hover and open states use verd-light (legible on the dark
     project-panel background; the standalone /faq/ page uses verd
     because it sits on cream). */
  .proj-faq-item .faq-item__q:hover { color: var(--verd-light); }
  .proj-faq-item[open] .faq-item__q { color: var(--verd-light); }
  .proj-faq-item .faq-item__a {
    /* v2.9.316 — Size matches the standalone /faq/ page answer
       (var(--h-lede), inherited from base .faq-item__a). Previously
       sized smaller (clamp 15-17) which broke editorial parity with
       Synopsis/Director's Notes. Override only color + max-width for
       the dark project-panel context. */
    color: var(--cream);
    opacity: 0.85;
    max-width: 640px;
  }
  /* Answer links should keep the verd accent treatment from the base
     /faq/ page — already legible on the dark bg, no override needed. */

  /* v2.9.316 — Mobile sizing intentionally NOT overridden here.
     The base .faq-item__q and .faq-item__a rules already have a
     @media (max-width: 1024px) block (see line ~8720) that sizes
     questions to 18px / answers to 15px. Since .proj-faq-item is
     also a .faq-item, those base rules apply on the project panel
     too — and matching the standalone /faq/ page's mobile sizing
     keeps editorial parity site-wide. The previous, separate
     project-FAQ mobile block (which hard-shrunk questions to 17px
     and intro to 14px) was removed in v2.9.316. */

  .proj-similar {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    grid-template-areas:
      "lists quote"
      "lists companion";
    grid-template-rows: auto 1fr;
    column-gap: 64px;
    row-gap: 24px;
    align-items: start;
  }
  .proj-similar__lists {
    /* Left column — films + directors. Spans both grid rows so the
       right column's quote and companion can stack alongside it. */
    grid-area: lists;
  }
  .proj-similar__visual--quote {
    grid-area: quote;
    /* Sticky so the quote remains visible as the film list scrolls
       past in the left column. Companion sits below in static flow
       so it can scroll naturally. */
    position: sticky;
    top: 32px;
    margin: 0;
  }
  .proj-similar__visual--companion {
    grid-area: companion;
    margin: 0;
  }
  /* Mobile: stack as flex column. Sticky is removed because there's
     no scroll-parallel context to hold the quote next to. Order:
     quote (-1) → films + directors lists (default 0) → companion (1).
     This keeps the cinema "in conversation with" thread intact and
     lands the reading recommendation as the closing note. */
  @media (max-width: 1024px) {
    .syn-body--notes {
      display: block;
      gap: 0;
    }
    .syn-body__notes-visual {
      display: none;
    }
    .proj-similar {
      display: flex;
      flex-direction: column;
      grid-template-areas: none;
      gap: 0;
    }
    .proj-similar__visual--quote {
      position: static;
      order: -1;
      margin-bottom: 32px;
    }
    .proj-similar__lists {
      order: 0;
    }
    .proj-similar__visual--companion {
      order: 1;
      margin-top: 32px;
    }
  }

  @media (max-width: 1024px) {
    .syn-side {
      padding-top: 32px;
      margin-top: 32px;
    }
  }

  /* ═══════════════════════════════════════════════════════════════════
     PROJECT TAB PANELS — Videos, Press, Credits
     ═══════════════════════════════════════════════════════════════════
     Reusable BEM card style for the Videos, Press tabs and trailer hero.
     Replaced the pile of inline styles we had so hover/focus states can
     live in one place and match the rest of the site's slate-rectangle
     aesthetic. */

  .proj-videos {
    display: flex;
    flex-direction: column;
    gap: 32px;
  }
  .proj-videos__section-label {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-top: 8px;
  }
  .proj-videos-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 16px;
  }

  .proj-video-card {
    display: block;
    text-decoration: none;
    color: var(--cream);
    background: rgba(242, 237, 228, 0.03);
    border: 1px solid var(--rule);
    transition: background 0.25s, border-color 0.25s, transform 0.25s;
  }
  .proj-video-card:hover,
  .proj-video-card:focus-visible {
    background: rgba(242, 237, 228, 0.06);
    border-color: var(--verd);
    transform: translateY(-2px);
  }
  .proj-video-card__thumb {
    position: relative;
    aspect-ratio: 16 / 9;
    background:
      radial-gradient(circle at 30% 40%, rgba(45, 106, 91, 0.25), transparent 60%),
      linear-gradient(135deg, #1f1d1a, #2a2824);
    display: grid;
    place-items: center;
    border-bottom: 1px solid var(--rule);
  }
  .proj-video-card__play {
    width: 56px;
    height: 56px;
    border-radius: 999px;
    background: rgba(242, 237, 228, 0.12);
    backdrop-filter: blur(8px);
    display: grid;
    place-items: center;
    color: var(--cream);
    font-size: 18px;
    transition: background 0.25s, transform 0.25s;
  }
  .proj-video-card:hover .proj-video-card__play {
    background: var(--verd);
    transform: scale(1.08);
  }
  .proj-video-card__duration {
    position: absolute;
    bottom: 8px;
    right: 8px;
    font-family: var(--mono);
    font-size: var(--ps-micro);
    letter-spacing: var(--track-narrow);
    padding: 4px 8px;
    background: rgba(26, 25, 22, 0.75);
    color: var(--cream);
  }
  .proj-video-card__body {
    padding: 16px 18px 18px;
  }
  .proj-video-card__title {
    font-family: var(--display);
    font-size: var(--ps-emphasis);
    line-height: 1.3;
    margin-bottom: 4px;
  }
  .proj-video-card__meta {
    font-family: var(--mono);
    font-size: var(--ps-label);
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
  }

  /* Trailer — featured treatment: taller thumb, full-width, larger title */
  .proj-trailer {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .proj-trailer__label {
    font-family: var(--mono);
    font-size: var(--ps-label);
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
  }
  .proj-video-card--trailer .proj-video-card__thumb {
    aspect-ratio: 21 / 9;
  }
  .proj-video-card--trailer .proj-video-card__title {
    font-size: var(--ps-lede);
  }
  .proj-video-card--trailer .proj-video-card__play {
    width: 72px;
    height: 72px;
    font-size: 22px;
  }

  /* PRESS TAB — cards on dark surface, cream text, verd accents */
  .proj-press-list {
    display: grid;
    gap: 16px;
  }
  .proj-press-item {
    display: block;
    padding: 24px;
    text-decoration: none;
    color: var(--cream);
    background: rgba(242, 237, 228, 0.03);
    border: 1px solid var(--rule);
    transition: background 0.25s, border-color 0.25s, transform 0.25s;
  }
  .proj-press-item:hover,
  .proj-press-item:focus-visible {
    background: rgba(242, 237, 228, 0.06);
    border-color: var(--verd);
    transform: translateY(-2px);
  }
  .proj-press-item__meta {
    font-family: var(--mono);
    font-size: var(--ps-label);
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 12px;
  }
  .proj-press-item__quote {
    font-family: var(--display);
    font-size: var(--ps-emphasis);
    font-style: italic;
    line-height: 1.4;
    margin: 0 0 16px;
    color: var(--cream);
  }
  .proj-press-item__read {
    font-family: var(--mono);
    font-size: var(--ps-label);
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
  }

  /* PRESS FILTERS (project tab) — reuses the /press/ page toolbar
     pattern (.press-toolbar) but needs minor overrides because the
     project page has a dark background while /press/ is light. */
  .proj-press-toolbar {
    margin-bottom: 20px;
  }
  .proj-press-toolbar .press-filter-toggle {
    background: rgba(255,255,255,0.1);
    color: var(--cream);
  }
  .proj-press-toolbar .press-filter-toggle:hover {
    background: rgba(255,255,255,0.18);
  }

  /* Project press drawer — dark theme overrides for DESKTOP inline rendering.
     The base .press-filters-block is styled for the light (cream) /press/
     page. When rendered inline under a project panel (dark surface), we need
     to flip text/border colors so the filter chips and labels are readable.
     These overrides are scoped to desktop only because on mobile the drawer
     becomes a bottom sheet with a cream background, where default light-theme
     chip colors are already correct. */
  @media (min-width: 1025px) {
    #projPressFilterDrawer .filter-row {
      border-bottom-color: rgba(255,255,255,0.08);
    }
    #projPressFilterDrawer .filter-row__label {
      color: var(--verd);
    }
    #projPressFilterDrawer .fchip {
      color: var(--cream);
      border-color: rgba(255,255,255,0.18);
      opacity: 0.8;
    }
    #projPressFilterDrawer .fchip:hover {
      border-color: var(--verd);
      color: var(--verd);
      opacity: 1;
    }
    #projPressFilterDrawer .fchip.active {
      background: var(--verd);
      border-color: var(--verd);
      color: var(--ink);
      opacity: 1;
    }
    #projPressFilterDrawer .fchip__count {
      opacity: 0.6;
      margin-left: 6px;
    }
    /* Drawer header is a mobile affordance (dialog title + close button).
       On desktop, hide it — the drawer renders inline so the header looks
       odd above the filter chips. */
    #projPressFilterDrawer .press-drawer__head {
      display: none;
    }
  }
  .proj-press-item.is-hidden {
    display: none;
  }

  /* ── MOBILE HORIZONTAL SWIPE LAYOUT ───────────────────────────────
     On mobile, grids/lists marked .is-mobile-swipe transform into a
     single-row horizontal scroll with snap, mirroring the pattern used
     by .vid-social-row (Short-Form & Social on project pages) which is
     known to work well inside .proj-panel's overflow-hidden layout.

     Desktop (>900px): class has no effect. Grid/list renders as its
     native layout (CSS Grid, flex column, etc). */
  @media (max-width: 1024px) {
    /* Hide-state guards: respect existing visibility-toggle patterns. */
    #favsCardGrid.is-mobile-swipe:not(.active) { display: none !important; }
    .is-mobile-swipe.hidden { display: none !important; }

    /* Core swipe layout. display:flex !important is required because the
       gallery grid has an inline style="display:grid" attribute that
       would otherwise win via specificity of inline styles.

       Bleed to viewport edges via negative margins: the container breaks
       out of its parent's horizontal padding (20px on mobile sections)
       so cards run right up to the viewport edge on both sides, matching
       the pattern used by .vid-social-row elsewhere in this file. The
       matching positive padding on the row keeps the first/last card
       inset from the edge by 20px so they don't look clipped. Body has
       overflow-x:hidden, so any over-bleed is safely clipped.

       Right-edge fade mask signals "swipe for more →" — the last
       visible card softly fades into transparent on the trailing edge,
       cueing the user that there's more content to scroll to. A matching
       left-edge fade applies once the user has scrolled past the first
       card, so the previous card peeking on the left side also softens
       into the edge. With center-snap (below), the "current" card sits
       in the middle with neighbors peeking on both sides, so a two-sided
       fade reads more naturally than a one-sided one.

       Snap behavior: cards snap to the horizontal CENTER of the viewport
       so whichever card the user lands on sits fully visible, with
       neighbors peeking from both sides. For this to work on the first
       and last cards (which otherwise can't scroll far enough to be
       centered), invisible ::before/::after pseudo-element spacers add
       scroll-distance equal to half-the-viewport minus half-a-card,
       giving the outer cards room to center.

       Per-container --swipe-card-w custom property drives both the child
       width and the spacer width, keeping them in sync. */
    .is-mobile-swipe {
      --swipe-card-w: 260px;
      display: flex !important;
      flex-direction: row;
      flex-wrap: nowrap;
      align-items: start;
      gap: 14px;
      grid-template-columns: none;
      overflow-x: auto;
      overflow-y: visible;
      scroll-snap-type: x mandatory;
      -webkit-overflow-scrolling: touch;
      /* touch-action: pan-x pan-y — this is the canonical pattern for a
         horizontal carousel that must not trap vertical page scrolling.
         pan-x allows the rail's horizontal overflow to respond to left/
         right swipes; pan-y allows the nearest vertically-scrollable
         ancestor (the page) to receive up/down swipes when the user's
         finger starts on a card. Using pan-x alone (the prior value)
         trapped vertical gestures on the rail, making it impossible to
         scroll the page away by swiping on a card. */
      touch-action: pan-x pan-y;
      scrollbar-width: none;
      -ms-overflow-style: none;
      padding: 0 0 16px;
      margin-left: -20px;
      margin-right: -20px;
      width: calc(100% + 40px);
      box-sizing: border-box;
      -webkit-mask-image: linear-gradient(to right, transparent, black 40px, black calc(100% - 40px), transparent);
      mask-image: linear-gradient(to right, transparent, black 40px, black calc(100% - 40px), transparent);
    }
    .is-mobile-swipe::-webkit-scrollbar { display: none; }

    /* Spacer pseudo-elements at each end of the rail. Each is sized so
       that the first real card can scroll until its center aligns with
       the viewport center (and same for the last card on the other end).
       calc((100vw - var(--swipe-card-w)) / 2) is the horizontal distance
       between a viewport-centered card and the viewport edge. Using
       100vw here (rather than 100%) is correct because the rail bleeds
       to the viewport edges via the negative margins above. */
    .is-mobile-swipe::before,
    .is-mobile-swipe::after {
      content: '';
      flex: 0 0 auto;
      width: calc((100vw - var(--swipe-card-w)) / 2);
      /* Subtract the rail's gap once — the first real card's left gap
         plus the spacer width should sum to the centering offset. */
      min-width: 0;
    }

    /* Children: fixed width so they line up as uniform swipeable cards.
       flex: 0 0 auto prevents flex-shrink collapsing them into the row.
       align-self:start via the parent's align-items:start means each card
       is only as tall as its own content — shorter cards don't stretch
       to match the tallest sibling, avoiding empty space at the bottom.
       scroll-snap-align: center so each card centers in the viewport
       when the user settles.

       The .is-mobile-swipe selector is repeated on the child to raise
       specificity above the pre-existing .work-card mobile rule (which
       ships its own 78%/82% width for the home-page .work-grid swipe
       rail). Films page .mv-grid.is-mobile-swipe contains .work-card
       children too, so without this boost the home-page rule wins and
       our per-container --swipe-card-w gets ignored. */
    .is-mobile-swipe.is-mobile-swipe > * {
      flex: 0 0 auto;
      scroll-snap-align: center;
      width: var(--swipe-card-w);
      max-width: var(--swipe-card-w);
      min-width: var(--swipe-card-w);
    }
    /* First card snaps to START (left edge), not center. This aligns
       the initial-load position with the label/fchip text above the
       rail instead of the previous behavior that placed a centering
       spacer before the first card. The ::before spacer is shrunk to
       zero at start so the first card touches the leading edge;
       subsequent cards still center-snap naturally as the user swipes
       right. The ::after spacer is preserved so the LAST card can
       still scroll into the center position without hitting the end. */
    .is-mobile-swipe.is-mobile-swipe > *:first-child {
      scroll-snap-align: start;
      /* Margin-left offsets the negative margin on the container so
         this card's left edge aligns with the content above (which is
         padded in at 20px from the viewport edge). */
      margin-left: 20px;
    }
    .is-mobile-swipe::before {
      /* Collapse the leading centering spacer — first card now
         starts at the left edge instead of being pushed to viewport
         center on initial render. */
      width: 0;
    }

    /* Per-container card widths. Tuned so ~1.5 cards show at once,
       signaling swipeability without cramping content. Setting the
       custom property drives both the child width and spacer size. */
    .proj-videos-grid.is-mobile-swipe  { --swipe-card-w: 260px; }
    .proj-press-list.is-mobile-swipe   { --swipe-card-w: 280px; }
    .mv-grid.is-mobile-swipe           { --swipe-card-w: 260px; }
    .work-grid.is-mobile-swipe         { --swipe-card-w: 260px; }
    .press-video__grid.is-mobile-swipe { --swipe-card-w: 300px; }
    .archive-grid.is-mobile-swipe      { --swipe-card-w: 300px; }
    .favs-films-grid.is-mobile-swipe   { --swipe-card-w: 240px; }

    /* v2.9.151 — favs rail compact card mode.
       The library mobile rail had cards rendering at ~440px tall (year
       row, title, director, genre, 2-line desc, IMDb chip). The flex
       container also reserved that height plus padding, leaving 100-200px
       of dead cream space below the visible cards before the next section.

       Solution: compact each card to a tease-only layout — year/country/
       stars row, title, and director name. Genre tag, description,
       themes, and IMDb chip all live in the .favs-sheet detail modal
       that opens on tap. The rail collapses from ~440px to ~160px tall,
       eliminating the dead space and roughly tripling visual density of
       the swipe row.

       Speed bonus: ~3 fewer rendered DOM elements per card × 120 cards
       = 360 fewer text nodes on first paint. Modest LCP/FCP win on mobile.

       The hidden elements stay in the DOM (display:none, not removed)
       because the favs-sheet JS reads from them when populating the
       modal — keeping them present means no template changes needed.

       v2.9.209 — content-visibility on rail cards. Lighthouse mobile on
       /library/ flagged 1,526ms in Style & Layout, dominated by laying
       out all ~120 .favs-film cards even though only 4–5 are in the
       horizontal viewport at any time. CV: auto skips layout/paint for
       cards clipped by the rail's overflow:auto. Width is fixed by
       --swipe-card-w (240px) so intrinsic-size only needs to match the
       compact-rail's ~160px tall card; specifying both keeps the
       layout reservation accurate during the skipped state. */
    .favs-films-grid.is-mobile-swipe > .favs-film {
      padding: 14px 16px !important;
      content-visibility: auto;
      contain-intrinsic-size: 240px 160px;
    }
    .favs-films-grid.is-mobile-swipe > .favs-film .favs-film__desc,
    .favs-films-grid.is-mobile-swipe > .favs-film .favs-film__themes,
    .favs-films-grid.is-mobile-swipe > .favs-film .favs-film__out-row {
      display: none !important;
    }
    /* Title gets a touch more breathing room since it's now the dominant
       content. Allow up to 3 lines so longer titles like "Spider-Man: Into
       the Spider-Verse" or "Are You There God? It's Me, Margaret." don't
       truncate awkwardly mid-word. */
    .favs-films-grid.is-mobile-swipe > .favs-film .favs-film__title {
      font-size: 18px !important;
      line-height: 1.25 !important;
      margin-top: 8px !important;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }
    /* Director is the lower secondary line — single line, ellipsized. */
    .favs-films-grid.is-mobile-swipe > .favs-film .favs-film__director {
      font-size: 13px !important;
      margin-top: 6px !important;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    /* Tighten the gap between rail → dots → next section label on
       mobile. Desktop's 32px flex gap reads as editorial space but
    /* Project videos tab mobile spacing. The parent .proj-videos flex
       column provides the gap between sections; label margin is zeroed
       so it sits close to the dots/rail it's paired with. 28px gap
       gives comfortable breathing room without the 32px desktop feel
       that reads as empty on narrow viewports. */
    .proj-videos {
      gap: 28px;
    }
    .proj-videos__section-label {
      margin-top: 0;
    }

    /* Mobile restyle of .work-card when inside a swipe rail — gives it
       the same compact, 16:9-thumbnail form factor as .mv-card. Fixes
       the "vertical swipes get trapped" issue by shortening the card's
       vertical extent and making horizontal vs vertical intent easier
       for the browser to distinguish. Only applies to .work-cards
       inside an .is-mobile-swipe rail — currently just the films page.
       The home Selected Work section no longer uses this swipe rail
       (it uses the tile grid at all breakpoints) so its .work-cards
       keep their default poster-tall styling on mobile. */
    .is-mobile-swipe > .work-card .work-card__img {
      aspect-ratio: 16/9;
      margin-bottom: 12px;
    }
    .is-mobile-swipe > .work-card .work-card__title {
      font-size: 18px;
      line-height: 1.25;
    }
    .is-mobile-swipe > .work-card .work-card__meta {
      font-size: 10px;
    }

    /* Filter-active state: when a filter chip other than "All" is
       selected on films / commercials+mv pages, the grid is toggled
       into .is-filtered via JS. This disables the expensive swipe
       layout (flex rail + mask + snap) and falls back to a plain
       vertical stack, making filter taps feel instant.

       When the user picks "All" again, the class is removed and
       the swipe rail returns. */
    .is-mobile-swipe.is-filtered {
      display: block !important;
      flex-direction: initial;
      flex-wrap: initial;
      overflow-x: visible;
      scroll-snap-type: none;
      touch-action: auto;
      padding: 0;
      margin-left: 0;
      margin-right: 0;
      width: 100%;
      -webkit-mask-image: none;
      mask-image: none;
    }
    .is-mobile-swipe.is-filtered::before,
    .is-mobile-swipe.is-filtered::after {
      content: none;
    }
    .is-mobile-swipe.is-filtered.is-filtered > * {
      width: 100% !important;
      min-width: 0 !important;
      max-width: none !important;
      margin-bottom: 14px;
      scroll-snap-align: none;
    }
    .is-mobile-swipe.is-filtered > *:last-child { margin-bottom: 0; }
    /* Reset first-child left margin when the rail collapses into a
       filtered vertical stack. The 20px margin-left earlier in this
       file aligns the first card with the label text above in
       horizontal-swipe mode, but in the vertical-stack .is-filtered
       layout (activated for ≤2 matches) that same margin pushes the
       first card 20px to the right of subsequent cards — creating the
       visible misalignment on Films "Feature Documentaries" (2 matches)
       and Commercials & MV "Music Videos" (2 matches). Without the
       override, first-vs-rest cards had different left edges. */
    .is-mobile-swipe.is-filtered > *:first-child {
      margin-left: 0 !important;
      scroll-snap-align: none;
    }
  }
  

  /* TOGGLE BUTTON — previously shown on mobile when a list was collapsed.
     The mobile layout is now horizontal-swipe (.is-mobile-swipe), so the
     expand/collapse toggle is obsolete. Hide these buttons on all viewports
     — if any old markup still renders them, they stay invisible. */
  .proj-collapse-toggle,
  .proj-press-toggle {
    display: none !important;
  }

  /* CREDITS BLOCK — Cast / Creative Team / Crew sections at the end of Synopsis.
     Editorial-magazine feel: serif names, mono caps labels, verd accent line
     on each section eyebrow. Matches the visual language used in press-kit
     and bag-hero eyebrows.

     v2.9.73 — standardized spacing across all metadata blocks under
     the synopsis (.proj-credits, .proj-watch, .proj-audience-row).
     The block-level shell (top border + padding-top) used to live on
     .proj-credits AND .proj-watch separately, which double-bordered
     between Crew and Where to Watch. Now the shell is a unified rule
     applied to whichever block comes FIRST after the prose, and the
     rest use a simple 56px top margin. Same rhythm on both desktop
     and mobile. */
  .proj-credits,
  .proj-watch {
    display: flex;
    flex-direction: column;
    gap: 56px;
    max-width: 880px;
  }
  /* The first metadata block after the prose gets the visual shell —
     top border + padding-top — to mark the transition from synopsis
     to structured data. Whichever element is the first metadata
     child of .proj-synopsis__main (whether .proj-credits or
     .proj-watch) inherits this treatment.

     v2.9.74 — values match .bio-credentials site convention:
     margin-top: 64px, padding-top: 40px, border-top: 1px rule.
     Same visual weight as the home page's "structured data after
     prose" transition (bio-deep → bio-credentials). */
  .proj-synopsis__main > .syn-body + .proj-credits,
  .proj-synopsis__main > .syn-body + .proj-watch,
  .proj-synopsis__main > .proj-credits:first-child,
  .proj-synopsis__main > .proj-watch:first-child {
    margin-top: 64px;
    padding-top: 40px;
    border-top: 1px solid var(--rule);
  }
  /* v2.9.76 — mobile override. On narrow viewports the metadata
     stack flows below the RT aside (which has its own 24px top
     and bottom margins per v2.9.75). The bordered transition
     above isn't doing visual work there because RT already
     separates prose from structured data; the heavy 64+40+border
     produced a visible-gap mismatch where above-RT was ~48px and
     below-RT was 100+px. Strip the border and shrink to 24px so
     the gap below RT mirrors the gap above (each side: 24 RT
     margin + 24 adjacent margin = 48px visible).

     Source-order placement matters: this @media block must come
     AFTER the desktop rule above. Equal-specificity selectors
     resolve by source order; an earlier @media block was
     overridden by the always-applied desktop rule on mobile too. */
  @media (max-width: 1024px) {
    .proj-synopsis__main > .syn-body + .proj-credits,
    .proj-synopsis__main > .syn-body + .proj-watch,
    .proj-synopsis__main > .proj-credits:first-child,
    .proj-synopsis__main > .proj-watch:first-child {
      margin-top: 24px;
      padding-top: 0;
      border-top: none;
    }
  }
  /* Subsequent metadata blocks inside __main (only .proj-watch
     following .proj-credits) get a consistent 56px top margin with
     no border. Plain spacing because they're subdivisions of the
     same metadata zone, not new page sections.

     .proj-audience-row sits OUTSIDE __main (it's a child of
     .proj-synopsis directly, with grid-column: 1 / -1), so it has
     its own margin-top rule earlier in this file. Same 56px value
     so the rhythm matches. */
  .proj-credits + .proj-watch {
    margin-top: 56px;
  }
  .proj-credits__section {
    display: flex;
    flex-direction: column;
    gap: 24px;
  }
  .proj-credits__eyebrow {
    display: flex;
    align-items: center;
    gap: 14px;
    font-family: var(--mono);
    font-size: var(--ps-label);
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
  }
  .proj-credits__eyebrow::before {
    content: '';
    width: 24px;
    height: 1px;
    background: var(--verd);
    flex-shrink: 0;
  }
  .proj-credits__eyebrow::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--rule);
  }

  /* Cast grid — name in display serif, role in mono caps beneath */
  .proj-credits__cast {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 28px 32px;
  }
  .proj-credits__cast-item {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .proj-credits__cast-name {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--ps-emphasis);
    line-height: var(--lh-display-medium);
    color: var(--cream);
  }
  /* Cast row whose person_ref resolves to a published Person CPT entry —
     renders as <a> linking to /person/{slug}/. Visually inherits the
     plain cast-name treatment but adds a verd-green highlight on hover
     to signal interactivity. The highlight is a soft background tint
     (marker-pen feel) rather than an underline, which read as too
     formal/footnote-y in the editorial design language. */
  .proj-credits__cast-name--link {
    text-decoration: none;
    color: var(--cream);
    transition: background-color 0.15s ease, color 0.15s ease;
    padding: 0 4px;
    margin: 0 -4px;
    border-radius: 1px;
  }
  .proj-credits__cast-name--link:hover,
  .proj-credits__cast-name--link:focus-visible {
    background-color: rgba(45, 106, 91, 0.28);
    color: var(--cream);
    text-decoration: none;
    outline: none;
  }
  /* Credit value link — used in Creative Team / Crew rows when a
     repeater credit (writer/producer/editor) has its person_ref set.
     Same visual grammar as cast-name links. Inherits the value
     typography otherwise. */
  .proj-credits__value-link {
    color: inherit;
    text-decoration: none;
    transition: background-color 0.15s ease, color 0.15s ease;
    padding: 0 4px;
    margin: 0 -4px;
    border-radius: 1px;
  }
  .proj-credits__value-link:hover,
  .proj-credits__value-link:focus-visible {
    background-color: rgba(45, 106, 91, 0.28);
    color: inherit;
    text-decoration: none;
    outline: none;
  }
  .proj-credits__cast-role {
    font-family: var(--mono);
    font-size: var(--ps-label);
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
  }

  /* Where-to-Watch grid — sits inside .proj-credits as a peer of Cast/
     Creative Team/Crew. Uses the same visual grammar: auto-fill grid,
     subtle card, icon + name + availability. Replaces the old inline-
     styled standalone watch panel. */
  .proj-watch-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 12px;
  }
  .proj-watch-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 18px 20px;
    border: 1px solid var(--rule);
    background: rgba(242, 237, 228, 0.02);
    text-decoration: none;
    color: var(--cream);
    transition: border-color 0.2s var(--ease), background 0.2s var(--ease);
  }
  .proj-watch-card:hover,
  .proj-watch-card:focus-visible {
    border-color: var(--verd);
    background: rgba(45, 106, 91, 0.08);
  }
  .proj-watch-card__icon {
    height: 24px;
    color: var(--cream);
    display: flex;
    align-items: center;
  }
  .proj-watch-card__icon svg {
    height: 100%;
    width: auto;
    max-width: 100%;
    display: block;
  }
  /* PNG icon uploads in streaming service cards — match the SVG sizing. */
  .proj-watch-card__icon .platform-icon-img {
    height: 100%;
    width: auto;
    max-width: 100%;
    object-fit: contain;
    display: block;
  }
  .proj-watch-card__name {
    font-family: var(--display);
    font-weight: 400;
    font-size: var(--ps-emphasis);
    line-height: var(--lh-display-medium);
    color: var(--cream);
  }
  .proj-watch-card__avail {
    font-family: var(--mono);
    font-size: var(--ps-micro);
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
  }
  /* Paper panel variant — inverted colors for light-bg synopsis */
  .proj-panel--paper .proj-watch-card {
    color: var(--ink);
    border-color: var(--rule-strong);
    background: transparent;
  }
  .proj-panel--paper .proj-watch-card:hover,
  .proj-panel--paper .proj-watch-card:focus-visible {
    border-color: var(--verd);
    background: rgba(45, 106, 91, 0.04);
  }
  .proj-panel--paper .proj-watch-card__icon { color: var(--ink); }
  .proj-panel--paper .proj-watch-card__name { color: var(--ink); }

  /* Creative Team + Crew — two-column definition list */
  .proj-credits__list {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 18px 40px;
    margin: 0;
  }
  .proj-credits__label {
    font-family: var(--mono);
    font-size: var(--ps-label);
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    padding-top: 6px;
    margin: 0;
  }
  .proj-credits__value {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--ps-emphasis);
    line-height: 1.4;
    color: var(--cream);
    margin: 0;
  }
  @media (max-width: 700px) {
    .proj-credits {
      margin-top: 48px;
      padding-top: 32px;
      gap: 40px;
    }
    .proj-credits__list {
      grid-template-columns: 1fr;
      gap: 4px 0;
    }
    .proj-credits__label {
      padding-top: 16px;
    }
    .proj-credits__list > .proj-credits__label:first-child {
      padding-top: 0;
    }
    .proj-credits__cast {
      grid-template-columns: 1fr;
      gap: 20px;
    }
  }

  /* WATCH PANEL — platform cards as slate rectangles */
  .watch-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 16px;
  }
  .watch-card {
    aspect-ratio: 16/10;
    background: var(--dark-card);
    border: 1px solid rgba(242,237,228,0.08);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 24px;
    text-decoration: none;
    color: var(--cream);
    transition: all 0.3s var(--ease);
    position: relative;
    overflow: hidden;
    gap: 10px;
  }
  .watch-card::before {
    content: '';
    position: absolute; inset: 8px;
    pointer-events: none;
    background:
      linear-gradient(to right, var(--verd) 0, var(--verd) 14px, transparent 14px) 0 0 / 100% 1px no-repeat,
      linear-gradient(to bottom, var(--verd) 0, var(--verd) 14px, transparent 14px) 0 0 / 1px 100% no-repeat,
      linear-gradient(to left, var(--verd) 0, var(--verd) 14px, transparent 14px) 100% 100% / 100% 1px no-repeat,
      linear-gradient(to top, var(--verd) 0, var(--verd) 14px, transparent 14px) 100% 100% / 1px 100% no-repeat;
    opacity: 0;
    transition: opacity 0.3s var(--ease);
  }
  .watch-card:hover { background: var(--dark-mid); border-color: var(--verd); }
  .watch-card:hover::before { opacity: 1; }
  
  
  .watch-card:hover .watch-card__logo-wrap img { opacity: 1; }
  /* Fallback text appears when img has no src (placeholder state) */
  
  
  
  /* legacy SVG support (in case any old SVG markup remains) */
  
  .watch-card:hover .watch-card__logo-wrap svg { opacity: 1; }
  

  /* VIDEOS PANEL */
  
  /* Placeholder state when no trailer URL is set — non-interactive */
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

  /* ── VERTICAL / SOCIAL VIDEO ROW ── */
  .vid-social-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin: 48px 0 20px;
    padding-top: 32px;
    border-top: 1px solid rgba(242,237,228,0.1);
    /* Offset for the fixed nav when the #short-form-social anchor is
       targeted from the wayfinder. */
    scroll-margin-top: 80px;
  }
  /* On mobile, the preceding swipe rail (.press-video__grid.is-mobile-swipe
     on the press page, or the project panel's own content above) already
     contributes bottom padding, so the 48px+32px stacked spacing here reads
     as a big gap. Compress on mobile. */
  @media (max-width: 1024px) {
    .vid-social-head {
      margin-top: 24px;
      padding-top: 20px;
    }
  }
  .vid-social-head__label {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .vid-social-head__label::before {
    content: '';
    display: inline-block;
    width: 18px; height: 1px;
    background: var(--verd-light);
  }
  .vid-social-head__note {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.35;
  }
  /* On Camera section project-filter chips — sits above both the video
     grid and the social rail, lets users filter the aggregated videos
     down to a single project.
     v2.9.42 — markup expanded into a toggle + drawer pair (see
     buildOnCameraFilters in main.js for the rendered DOM). On desktop
     the toggle is hidden and the drawer renders inline as a flex row
     of chips, identical to pre-v2.9.42 visual behavior. On mobile the
     drawer collapses into a fixed bottom sheet triggered by the toggle
     pill — same UX vocabulary as /press/ archive's filter drawer.
     Empty (no chips rendered) when data is from 0 projects; otherwise
     visible with "All" + per-project chips. */
  .on-camera-filters {
    /* Container is now relatively-positioned because the mobile drawer
       and backdrop are descendants positioned via fixed; the relative
       container is mostly to scope the descendants and to allow
       JS to read state via classList without affecting parent layout. */
    position: relative;
    margin-bottom: 24px;
    /* v2.9.49 — reserve vertical space so the filter chips arriving
       after JS hydration don't push the on-camera grid down. Without
       this, the empty container is 0px tall on initial paint, then
       grows to ~44px once the chip row is populated, producing a
       cumulative layout shift (PageSpeed flagged this as the dominant
       CLS contributor: 0.080 of a 0.083 total score). 44px covers a
       single chip row on desktop (chips are 28px tall + 8px gaps + a
       little breathing room) and the toggle pill on mobile. */
    min-height: 44px;
  }
  .on-camera-filters:empty {
    display: none;
  }
  /* TOGGLE PILL — visible on mobile only. Shows current filter state
     plus a secondary line ("Filter by project") so the affordance is
     immediately readable. */
  .on-camera-filters__toggle {
    display: none;
    align-items: center;
    gap: 8px;
    padding: 10px 16px;
    background: var(--ink);
    color: var(--chalk);
    border: none;
    border-radius: 4px;
    font-family: var(--mono);
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
  }
  .on-camera-filters__toggle:hover { background: var(--ink-mid); }
  .on-camera-filters__toggle-icon {
    /* Decorative magnifier-funnel glyph; pure CSS would also work but
       a unicode glyph keeps the markup tiny and respects font scaling. */
    font-size: 14px;
    line-height: 1;
    opacity: 0.85;
  }
  .on-camera-filters__toggle-current {
    /* Pill shows currently-applied filter so the user can confirm the
       state without opening the drawer. Verd accent matches the
       press-filter-toggle__count pill on the archive. */
    margin-left: auto;
    padding: 3px 10px;
    background: var(--verd);
    color: var(--chalk);
    border-radius: 10px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: none;
    max-width: 50vw;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* DRAWER — inline strip on desktop, bottom sheet on mobile. */
  .on-camera-filters__drawer {
    /* Default = inline desktop layout. Mobile @media below switches it
       to fixed-position bottom sheet. */
    display: block;
  }
  .on-camera-filters__drawer-head {
    /* Head row only renders on mobile (close button + title). Hidden
       on desktop because the drawer IS the row, no head needed. */
    display: none;
  }
  .on-camera-filters__chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
  }

  /* BACKDROP — only relevant on mobile but lives in DOM at all sizes
     (hidden via [hidden] until JS opens the drawer). */
  .on-camera-filters__drawer-backdrop {
    display: none;
  }

  .on-camera-filters__label {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    /* Force the label to its own line. flex-basis:100% makes it take
       the full row width; subsequent fchips wrap to a new row beneath.
       This is cleaner than the previous inline arrangement where the
       label and fchips fought for horizontal space on narrow viewports. */
    flex-basis: 100%;
    margin-bottom: 4px;
  }

  /* Body-scroll lock used while the mobile drawer is open. Same
     mechanism as press archive drawer; placing on <html> rather than
     <body> avoids interfering with iOS Safari's auto-bouncing top
     content area. */
  .jk-no-scroll { overflow: hidden; }

  /* horizontal scroll container — negative margins escape parent overflow-x:hidden */
  .vid-social-row {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 16px;
    overflow-x: auto;
    overflow-y: visible;
    padding: 0 32px 20px;
    margin-left: -32px;
    margin-right: -32px;
    scrollbar-width: none;
    -ms-overflow-style: none;
    width: calc(100% + 64px);
    box-sizing: border-box;
    /* Mobile-only center-snap treatment to match .is-mobile-swipe.
       scroll-snap-type + spacer pseudo-elements + two-sided fade mask
       give the short-form row the same settle-to-center behavior as
       other swipe rails. Desktop keeps the plain scroll without snap. */
    scroll-snap-type: none;
    touch-action: pan-x pan-y;
  }
  .vid-social-row::-webkit-scrollbar { display: none; }
  @media (max-width: 1024px) {
    .vid-social-row {
      scroll-snap-type: x mandatory;
      padding: 0 0 20px;
      -webkit-mask-image: linear-gradient(to right, transparent, black 40px, black calc(100% - 40px), transparent);
      mask-image: linear-gradient(to right, transparent, black 40px, black calc(100% - 40px), transparent);
    }
    /* Spacer pseudo-elements: .vid-vert is 160px wide, so each spacer
       is (100vw − 160px) / 2 so the first/last .vid-vert can center. */
    .vid-social-row::before,
    .vid-social-row::after {
      content: '';
      flex: 0 0 auto;
      width: calc((100vw - 160px) / 2);
      min-width: 0;
    }
    /* First card aligns to START (left edge) not center — matches the
       sibling rail pattern in .is-mobile-swipe. The leading ::before
       spacer is collapsed to zero; the first real card gets margin-left
       equal to the panel's content padding (20px on mobile) so its left
       edge sits under the "Short-Form & Social" label above. Trailing
       ::after spacer keeps its full width so the LAST card can still
       center-snap without hitting the rail end. */
    .vid-social-row::before { width: 0; }
    .vid-social-row > .vid-vert:first-child {
      scroll-snap-align: start;
      margin-left: 20px;
    }
    /* On the press page .vid-social-row--page has no panel padding,
       so the first-child margin needs to match the press-archive
       section's horizontal padding (20px mobile) instead. Same value
       here — no override needed. */
  }

  /* ──────────────────────────────────────────────────────────────────
     DESKTOP CHEVRON ARROWS for .vid-social-row (v2.9.42)
     ──────────────────────────────────────────────────────────────────
     The row hides its native scrollbar to keep the editorial typography
     uncluttered, but on desktop that meant no affordance for "more
     content this way" — mouse wheel doesn't horizontally scroll a
     non-active container. JS (attachVidSocialArrows) wraps the row in
     .vid-social-row-wrap and injects two chevron buttons; this CSS
     positions them as overlays at the row's vertical center, far left
     and far right, hidden by default and revealed only when (a) the
     viewport is desktop-sized AND (b) the row actually overflows
     (--has-arrows is set by JS).
     ──────────────────────────────────────────────────────────────── */
  .vid-social-row-wrap {
    position: relative;
    /* No layout impact when no arrows present — wrap is just a sibling
       of the row before it was wrapped, and adds no padding/margin. */
  }
  .vid-social-arrow {
    /* Chevrons hidden by default. .vid-social-row-wrap--has-arrows on
       the parent (set by JS when scrollWidth > clientWidth) plus the
       desktop media query below combine to enable display. Mobile
       never shows them — touch swipe is the navigation. */
    display: none;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid var(--rule-strong);
    background: var(--chalk);
    color: var(--ink);
    cursor: pointer;
    align-items: center;
    justify-content: center;
    padding: 0;
    z-index: 4;
    /* Match the editorial shadow vocabulary used elsewhere on the
       press page so the chevrons read as "page chrome" not "form
       control." Drop-shadow keeps the circular shape clean. */
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.06);
    transition: background 0.18s var(--ease), color 0.18s var(--ease),
                opacity 0.18s var(--ease), transform 0.18s var(--ease);
  }
  .vid-social-arrow:hover {
    background: var(--ink);
    color: var(--chalk);
    border-color: var(--ink);
  }
  .vid-social-arrow:focus-visible {
    outline: 2px solid var(--verd);
    outline-offset: 2px;
  }
  .vid-social-arrow:disabled {
    /* Subtle disabled state — still visible so user gets feedback that
       they're at the rail's edge rather than the button vanishing
       entirely (which would feel buggy). */
    opacity: 0.35;
    cursor: default;
    pointer-events: none;
  }
  .vid-social-arrow--prev { left: -8px; }
  .vid-social-arrow--next { right: -8px; }
  /* Reveal the buttons only on desktop AND only when JS has determined
     the row actually overflows (.vid-social-row-wrap--has-arrows). On
     dark-themed projects (where the row sits inside .proj-panel) we
     swap colors below; on press the cream-on-dark default works. */
  @media (min-width: 1025px) {
    .vid-social-row-wrap--has-arrows .vid-social-arrow {
      display: inline-flex;
    }
  }
  /* Project page lives on a dark background — recolor the chevrons to
     match the existing dark-mode card vocabulary so they don't read
     as a foreign light pop-up. */
  .proj-panel .vid-social-arrow,
  .proj-videos .vid-social-arrow {
    background: var(--dark-card);
    color: var(--cream);
    border-color: rgba(242, 237, 228, 0.18);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.2);
  }
  .proj-panel .vid-social-arrow:hover,
  .proj-videos .vid-social-arrow:hover {
    background: var(--verd);
    color: var(--chalk);
    border-color: var(--verd);
  }

  /* each vertical card */
  .vid-vert {
    flex: 0 0 160px;
    width: 160px;
    height: 285px;
    aspect-ratio: unset;
    position: relative;
    cursor: pointer;
    overflow: hidden;
    background: var(--dark-card);
    border: 1px solid rgba(242,237,228,0.08);
    scroll-snap-align: center;
    transition: border-color 0.3s var(--ease);
  }
  .vid-vert:hover { border-color: var(--verd-light); }
  .vid-vert__bg {
    position: absolute; inset: 0;
    background: linear-gradient(160deg, var(--dark-card) 0%, var(--dark) 100%);
  }
  /* subtle gradient scrim at bottom */
  .vid-vert::after {
    content: '';
    position: absolute; inset: 0;
    background: linear-gradient(0deg, rgba(13,10,7,0.88) 0%, transparent 55%);
    z-index: 1;
    pointer-events: none;
  }
  /* platform badge — top left */
  .vid-vert__platform {
    position: absolute;
    top: 12px; left: 12px;
    z-index: 3;
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    background: rgba(13,10,7,0.55);
    backdrop-filter: blur(8px);
    padding: 4px 8px;
    border-radius: 2px;
    display: flex;
    align-items: center;
    gap: 5px;
  }
  .vid-vert__platform svg {
    width: 10px; height: 10px;
    fill: currentColor;
    opacity: 0.9;
    flex-shrink: 0;
  }
  /* mini play button */
  .vid-vert__play {
    position: absolute;
    left: 50%; top: 45%;
    transform: translate(-50%, -50%);
    z-index: 3;
    width: 48px; height: 48px;
    border-radius: 50%;
    border: 1px solid rgba(242,237,228,0.5);
    background: rgba(13,10,7,0.35);
    backdrop-filter: blur(8px);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.3s var(--ease);
  }
  .vid-vert:hover .vid-vert__play {
    background: var(--verd);
    border-color: var(--verd-light);
    transform: translate(-50%, -50%) scale(1.1);
  }
  .vid-vert__play::before {
    content: '';
    width: 0; height: 0;
    border-left: 9px solid var(--cream);
    border-top: 6px solid transparent;
    border-bottom: 6px solid transparent;
    margin-left: 2px;
  }
  /* caption + duration at bottom */
  .vid-vert__info {
    position: absolute;
    bottom: 14px; left: 12px; right: 12px;
    z-index: 3;
  }
  .vid-vert__caption {
    font-family: var(--display);
    font-size: 13px;
    font-weight: 400;
    color: var(--cream);
    line-height: 1.3;
    margin-bottom: 4px;
  }
  .vid-vert__duration {
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-mid);
    color: var(--verd-light);
  }

  /* PRESS PANEL */
  
  .press-filters {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
  }

  
  
  .press-card {
    background: var(--dark-card);
    border: 1px solid var(--rule);
    padding: 32px;
    display: flex;
    flex-direction: column;
    gap: 18px;
    text-decoration: none;
    color: var(--cream);
    transition: all 0.3s var(--ease);
    position: relative;
  }
  .press-card::before {
    content: '"';
    position: absolute;
    top: 8px; left: 20px;
    font-family: var(--display);
    font-size: 88px;
    line-height: 1;
    color: var(--verd-light);
    opacity: 0.2;
    pointer-events: none;
  }
  .press-card:hover {
    border-color: var(--verd-light);
    background: var(--dark-mid);
  }
  .press-card__outlet {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    display: flex;
    align-items: center;
    gap: 12px;
    z-index: 1;
  }
  .press-card__outlet::before {
    content: ''; width: 24px; height: 1px;
    background: var(--verd);
  }
  .press-card__quote {
    font-family: var(--display);
    font-weight: 300;
    font-style: italic;
    font-size: 17px;
    line-height: 1.5;
    color: var(--cream);
    flex: 1;
    position: relative;
    z-index: 1;
  }
  .press-card__footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 16px;
    border-top: 1px solid var(--rule);
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.8;
  }
  .press-card__type {
    display: inline-flex;
    align-items: center;
    gap: 8px;
  }
  .press-card__type::before {
    content: ''; width: 4px; height: 4px;
    background: var(--verd);
    border-radius: 50%;
  }
  .press-card__arrow {
    color: var(--verd-light);
    transition: transform 0.3s var(--ease);
  }
  .press-card:hover .press-card__arrow { transform: translateX(4px); }
  @media (max-width: 1024px) {
    .press-grid { grid-template-columns: 1fr; }
    .press-card { padding: 24px; }
    .press-card__quote { font-size: 16px; }
  }

  /* GALLERY PANEL — film-contact-sheet feel */
  .gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 12px;
  }
  .gallery-tile {
    aspect-ratio: 3/2;
    position: relative;
    overflow: hidden;
    background: linear-gradient(135deg, var(--dark-card) 0%, var(--dark) 100%);
    cursor: pointer;
  }
  
  
  .gallery-tile::after {
    content: '';
    position: absolute; inset: 0;
    border: 1px solid var(--verd);
    opacity: 0;
    transition: opacity 0.3s var(--ease);
  }
  .gallery-tile:hover::after { opacity: 1; }

  /* NOTES PANEL — paper inversion again (statement-level content deserves it) */
  
  
  
  
  
  
  
  
  @media (max-width: 1024px) {
    .notes-grid { grid-template-columns: 1fr; gap: 32px; }
  }
  /* Paint containment — tells browser each card can't affect outside */
  .work-card,
  .press-card,
  .archive-card,
  .audience-card,
  .mv-card,
  .watch-card,
  .vid-vert {
    contain: layout style paint;
  }
  /* Reduce paint work on scroll */
  .proj-panels__rail,
  .vid-social-row,
  .leader-band__track {
    will-change: transform;
  }
  /* GPU layer for smooth tab transitions */
  .proj-panels__rail { transform: translateZ(0); }
  /* Prefer GPU compositing on transforms/opacity only (no layout/paint) */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
  
  
  
  
  
  
  
  
  
  
  
  
  
  

  /* ═══════════════════════════════════════════════════════════════
     GLOBAL PRESS PAGE
     ═══════════════════════════════════════════════════════════════ */

  .press-hero {
    /* v2.9.47.3 — bottom padding reduced from 48px → 24px after the
       hero CTA button was removed (the button used to sit below the
       publications strip, so the larger padding gave it breathing
       room). With just the publications strip ending the hero, 24px
       reads as a clean section close rather than a dead zone. */
    padding: 100px 32px 24px;
    max-width: 1500px;
    margin: 0 auto;
    border-bottom: 1px solid var(--rule);
  }
  .press-hero__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 20px;
    display: flex;
    align-items: center;
    gap: 14px;
  }
  .press-hero__eyebrow::before {
    content: ''; width: 28px; height: 1px; background: var(--verd);
  }
  .press-hero__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-hero-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
    margin-bottom: 28px;
  }
  .press-hero__title em { font-weight: 400;
 color: var(--verd);
 }
  .press-hero__lede {
    font-family: var(--display);
    font-size: var(--h-lede);
    line-height: var(--lh-body);
    color: var(--ink);
    opacity: 0.85;
    max-width: 640px;
    margin-bottom: 40px;
    font-weight: 300;
  }

  /* stats line — communicates scale at a glance */
  
  
  
  

  .press-hero__actions {
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
  }
  .btn-press {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 14px 22px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    text-decoration: none;
    border: 1px solid var(--ink);
    color: var(--ink);
    background: transparent;
    cursor: pointer;
    transition: all 0.3s var(--ease);
    /* 44px touch-target floor — padding alone yields 43 (14+14+15 text), one
       pixel short of HIG minimum. One px off target sounds trivial but
       affects tap accuracy on small screens. */
    min-height: 44px;
  }
  .btn-press:hover { background: var(--dark); color: var(--cream); border-color: var(--dark); }
  .btn-press--primary { background: var(--verd); border-color: var(--verd); color: var(--cream); }
  .btn-press--primary:hover { background: var(--dark); border-color: var(--verd-deep); color: var(--cream); }
  .btn-press__arrow { transition: transform 0.3s var(--ease); }
  .btn-press:hover .btn-press__arrow { transform: translateX(4px); }

  /* Dark-context override — when btn-press lives on a dark card (press kit, contact repr box),
     invert so the cream outline/text reads against the dark background. */
  .press-kit__inner .btn-press,
  .press-kit__inner .btn-press:not(.btn-press--primary) {
    border-color: var(--cream);
    color: var(--cream);
    background: transparent;
  }
  .press-kit__inner .btn-press:not(.btn-press--primary):hover {
    background: var(--cream);
    color: var(--ink);
    border-color: var(--cream);
  }
  /* Keep primary variant green on dark card, but ensure enough contrast on hover */
  .press-kit__inner .btn-press--primary {
    background: var(--verd);
    border-color: var(--verd);
    color: var(--cream);
  }
  .press-kit__inner .btn-press--primary:hover {
    background: var(--verd-light);
    border-color: var(--verd-light);
    color: var(--ink);
  }

  /* CENTRAL WAYFINDER — sits between Archive and Video Press as a
     navigational pause point. Editorial-print aesthetic via two
     hairline rules with a section-mark eyebrow centered between them.
     Buttons reuse .btn-press; the strip itself just provides the
     framing chrome. */
  .press-wayfinder {
    max-width: 1500px;
    margin: 16px auto 24px;
    padding: 0 32px;
  }
  .press-wayfinder__rule {
    height: 1px;
    background: var(--rule);
    margin: 0;
  }
  .press-wayfinder__inner {
    padding: 28px 0;
    display: flex;
    flex-direction: column;
    /* Left-aligned to match the editorial-print layout used elsewhere
       on the page (Selected Coverage, Archive headers, Press Kit are
       all left-aligned). Centered chrome read as a CTA bombardment;
       left-aligned reads as a pause point in the document flow. */
    align-items: flex-start;
    gap: 16px;
  }
  .press-wayfinder__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
  }
  .press-wayfinder__eyebrow-mark {
    /* Section-mark glyph (§) styled to feel like a printer's ornament.
       Reserved space + larger size + verd color reads as intentional
       editorial chrome rather than a typographic leftover. */
    font-family: var(--display);
    font-size: 16px;
    line-height: 1;
    color: var(--verd);
    opacity: 0.7;
  }
  .press-wayfinder__actions {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    /* Match the parent's left-alignment — no need to justify-content:center
       now that the inner block aligns to flex-start. */
    justify-content: flex-start;
  }

  @media (max-width: 1024px) {
    .press-hero { padding: 96px 20px 20px; }
    .press-stats { grid-template-columns: 1fr 1fr; max-width: none; }
    .press-stat { padding: 16px 18px; }
    /* Wayfinder hidden on mobile — at narrow widths the page is short
       enough to scroll naturally and a horizontal jump-nav adds clutter
       without saving meaningful tap distance. The rule keeps the
       wayfinder a desktop-only chrome element. */
    .press-wayfinder { display: none; }
  }

  /* INDEX / BACK-TO-TOP button — mirrors the EPK and on-location pages'
     pattern. Fixed pill in the bottom-right that appears once the user
     has scrolled past the hero. Click → smooth-scroll back to top of
     page. Visible on both desktop and mobile (mobile users actually
     benefit MORE since the press archive can be 100+ entries long and
     the wayfinder isn't there to ground them).
     Class is press-scoped so it doesn't collide with .epk-back-to-top
     which lives in epk.css and only loads on the EPK route. */
  .press-back-to-top {
    position: fixed;
    right: 24px;
    bottom: 32px;
    z-index: 100;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 12px 18px 12px 16px;
    background: rgba(13, 16, 20, 0.9);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid rgba(242, 237, 228, 0.15);
    color: var(--cream);
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    cursor: pointer;
    opacity: 0;
    transform: translateY(8px);
    pointer-events: none;
    transition: opacity 0.22s ease, transform 0.22s ease,
                background 0.18s, border-color 0.18s, color 0.18s;
  }
  .press-back-to-top.is-visible {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }
  .press-back-to-top:hover {
    background: rgba(13, 16, 20, 0.98);
    border-color: var(--verd-light);
    color: var(--verd-light);
  }
  .press-back-to-top:focus-visible {
    outline: 2px solid var(--verd);
    outline-offset: 2px;
  }
  .press-back-to-top__arrow {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--verd);
  }
  .press-back-to-top__label {
    line-height: 1;
  }
  @media (max-width: 720px) {
    .press-back-to-top {
      right: 16px;
      bottom: 20px;
      padding: 10px 14px 10px 12px;
      font-size: 9px;
      letter-spacing: var(--track-mid);
      gap: 8px;
    }
  }

  /* FEATURED BAND — pinned top-tier press */
  .press-featured {
    background: var(--chalk-mid);
    color: var(--ink);
    padding: 48px 32px;
  }
  .press-featured__inner {
    max-width: 1500px;
    margin: 0 auto;
  }
  .press-featured__head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 48px;
    padding-bottom: 20px;
    border-bottom: 1px solid var(--rule);
    flex-wrap: wrap;
    gap: 20px;
  }
  .press-featured__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-section-title);
    line-height: 1;
    letter-spacing: var(--track-tight);
    color: var(--ink);
  }
  .press-featured__title em { color: var(--verd-deep); }
  .press-featured__meta {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-deep);
  }

  .featured-grid {
    display: grid;
    grid-template-columns: 1.3fr 1fr 1fr;
    gap: 0;
    border: 1px solid var(--rule);
  }
  .featured-item {
    padding: 36px;
    text-decoration: none;
    color: var(--ink);
    display: flex;
    flex-direction: column;
    gap: 20px;
    transition: background 0.3s var(--ease);
    border-right: 1px solid var(--rule);
    position: relative;
  }
  .featured-item:last-child { border-right: none; }
  .featured-item:hover { background: rgba(45, 106, 91, 0.06); }
  .featured-item--hero {
    background: var(--dark);
    color: var(--cream);
  }
  .featured-item--hero:hover { background: var(--dark-mid); }
  .featured-item__num {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-deep);
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .featured-item--hero .featured-item__num { color: var(--verd-light); }
  .featured-item__num::before {
    content: ''; width: 18px; height: 1px; background: var(--verd-deep);
  }
  .featured-item--hero .featured-item__num::before { background: var(--verd); }
  .featured-item__outlet {
 font-family: var(--display); font-weight: 400;
 font-size: 22px;
 letter-spacing: var(--track-subtle);
 line-height: 1;
 }
  .featured-item--hero .featured-item__outlet {
    color: var(--verd-light);
    font-size: 28px;
  }
  .featured-item__quote {
    font-family: var(--display);
    font-weight: 300;
    font-size: 18px;
    line-height: 1.5;
    flex: 1;
  }
  .featured-item--hero .featured-item__quote {
    /* v2.9.116 — sized up from 26px to clamp(28px, 3.6vw, 44px).
       The staging-comparison doc flagged that pull quotes need to be
       the focal point of the press page. The hero quote was previously
       only ~25% larger than the surrounding body quotes; now it's
       2x+ on desktop, making it dominate the 3-column press grid as
       a deliberate editorial moment. */
    font-size: clamp(28px, 3.6vw, 44px);
    line-height: var(--lh-display-medium);
  }
  .featured-item--hero .featured-item__quote em {
    color: var(--verd-light);
    font-style: italic;
  }
  .featured-item__footer {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    padding-top: 18px;
    border-top: 1px solid var(--rule);
    display: flex;
    justify-content: space-between;
    opacity: 0.8;
  }
  .featured-item--hero .featured-item__footer { border-top-color: rgba(242, 237, 228, 0.15); opacity: 0.75; }

  @media (max-width: 1100px) {
    .featured-grid { grid-template-columns: 1fr 1fr; }
    .featured-item { border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
    .featured-item:nth-child(2) { border-right: none; }
    .featured-item--hero { grid-column: 1 / -1; }
  }
  @media (max-width: 700px) {
    .featured-grid { grid-template-columns: 1fr; }
    .featured-item { border-right: none; }
    .featured-item--hero { grid-column: auto; }
    .featured-item--hero .featured-item__quote { font-size: 20px; }
  }

  /* ARCHIVE — filters + list/grid toggle + cards */
  .press-archive {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
  }
  .press-archive__head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 40px;
    padding-bottom: 20px;
    border-bottom: 1px solid var(--rule);
    flex-wrap: wrap;
    gap: 20px;
  }
  .press-archive__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-section-title);
    line-height: 1;
    letter-spacing: var(--track-tight);
    color: var(--ink);
  }
  .press-archive__title em { color: var(--verd); }
  .press-archive__view-toggle {
    display: flex;
    gap: 2px;
    background: var(--rule);
    border: 1px solid var(--rule);
    padding: 2px;
  }
  .view-toggle-btn {
    background: none; border: none;
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.6;
    padding: 10px 16px;
    cursor: pointer;
    transition: all 0.2s var(--ease);
    /* 44px touch-target floor */
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .view-toggle-btn:hover { opacity: 1; color: var(--verd); }
  .view-toggle-btn.active {
    background: var(--verd);
    color: var(--cream);
    opacity: 1;
  }

  /* filter stack — multiple axes */
  .press-filters-block {
    margin-bottom: 48px;
    display: grid;
    grid-template-columns: 1fr;
    gap: 16px;
  }
  .filter-row {
    display: flex;
    align-items: center;
    gap: 20px;
    padding: 14px 0;
    border-bottom: 1px solid var(--rule);
    flex-wrap: wrap;
  }
  .filter-row__label {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    min-width: 80px;
    flex-shrink: 0;
  }
  .filter-row__chips {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    flex: 1;
  }
  .fchip {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    padding: 8px 14px;
    min-height: 36px;        /* desktop baseline */
    border: 1px solid var(--rule-strong);
    background: transparent;
    /* Use the real muted-text token (#3D3A34) instead of opacity:0.6 on
       --ink. Opacity-derived dim text fails WCAG AA on the chalk background.
       --ink-mid is hand-picked for ~5.7:1 contrast — passes AA Large + AA. */
    color: var(--ink-mid);
    cursor: pointer;
    transition: all 0.2s var(--ease);
    position: relative;
  }
  .fchip:hover { border-color: var(--verd); color: var(--verd); }
  .fchip.active {
    background: var(--verd);
    border-color: var(--verd);
    color: var(--cream);
  }
  /* Subtle verdigris-tinted dot above the active chip — echoes the
     decorative quote-mark language used on EPK press cards. Distinctive
     touch that signals state without adding visual weight. */
  .fchip.active::before {
    content: '';
    position: absolute;
    top: -3px;
    left: 50%;
    transform: translateX(-50%);
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--verd-light);
    box-shadow: 0 0 0 2px var(--chalk);
  }
  .fchip__count {
    /* Counts: don't fade with opacity — use a deliberately lower-weight
       color that still passes contrast. Inherits text color when chip
       is active so the count stays visible against the verdigris fill. */
    color: var(--ink-dim);
    margin-left: 4px;
    font-weight: 400;
  }
  .fchip.active .fchip__count { color: var(--cream); opacity: 0.75; }

  .press-archive__status {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.7;
    margin-bottom: 24px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 12px;
  }
  .press-archive__status strong {
    color: var(--verd);
    font-weight: 400;
  }
  .clear-filters {
    background: none; border: none;
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  /* CARD VIEW grid */
  .archive-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 20px;
  }
  .archive-grid.hidden { display: none; }
  /* Defer offscreen press cards. The press archive grows with each new
     press mention (reviews, interviews, features) and can reach 100+
     entries — meaningful layout cost on initial render. Each card has
     quote, headline, outlet logo, hero image. Skipping layout/paint/
     decode for cards outside the viewport is pure win. */
  .archive-grid .archive-card {
    content-visibility: auto;
    contain-intrinsic-size: 1px 380px;
  }

  .archive-card {
    background: var(--dark-card);
    border: 1px solid rgba(242,237,228,0.08);
    padding: 28px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    text-decoration: none;
    color: var(--cream);
    transition: all 0.3s var(--ease);
    position: relative;
  }
  .archive-card::before {
    content: '"';
    position: absolute;
    top: 4px; left: 16px;
    font-family: var(--display);
    font-size: 80px;
    line-height: 1;
    color: var(--verd-light);
    opacity: 0.18;
    pointer-events: none;
  }
  .archive-card:hover {
    border-color: var(--verd-light);
    background: var(--dark-mid);
  }
  .archive-card__topline {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    z-index: 1;
  }
  .archive-card__outlet {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
  }
  /* Outlet logo (optional, uploaded per-press-item via ACF outlet_logo).
   * When present, replaces the text outlet name. Rendered in grayscale with
   * slight opacity so it blends with the dark card aesthetic; color restores
   * on hover. Height capped so very wide logos don't break the layout. */
  .archive-card__outlet-logo {
    max-height: 22px;
    max-width: 140px;
    width: auto;
    height: auto;
    object-fit: contain;
    object-position: left center;
    filter: grayscale(100%) brightness(1.2);
    opacity: 0.75;
    transition: filter 0.25s var(--ease), opacity 0.25s var(--ease);
  }
  .archive-card:hover .archive-card__outlet-logo {
    filter: grayscale(0%);
    opacity: 1;
  }
  .archive-card__year {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    color: var(--cream);
    opacity: 0.4;
  }
  .archive-card__quote {
    font-family: var(--display);
    font-weight: 300;
    font-style: italic;
    font-size: 16px;
    line-height: 1.5;
    flex: 1;
    z-index: 1;
    position: relative;
  }
  .archive-card__project {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.6;
  }
  .archive-card__project strong {
    color: var(--cream);
    opacity: 1;
    font-weight: 500;
  }
  .archive-card__footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 14px;
    border-top: 1px solid rgba(242,237,228,0.1);
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    opacity: 0.85;
  }
  .archive-card__type {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--verd-light);
    background: rgba(109,191,173,0.1);
    padding: 3px 8px;
    border-radius: 2px;
    letter-spacing: var(--track-mid);
  }
  .archive-card__type::before {
    content: '';
    width: 4px; height: 4px;
    background: var(--verd-light);
    border-radius: 50%;
    flex-shrink: 0;
  }
  .archive-card__arrow {
    color: var(--verd-light);
    transition: transform 0.3s var(--ease);
  }
  .archive-card:hover .archive-card__arrow { transform: translateX(4px); }

  /* LIST VIEW — dense, scannable, editorial */
  .archive-list {
    display: none;
    border-top: 1px solid var(--rule);
  }
  .archive-list.active { display: block; }
  .archive-row {
    display: grid;
    grid-template-columns: 60px 140px 1.5fr 120px 100px 40px;
    gap: 24px;
    padding: 20px 0;
    border-bottom: 1px solid var(--rule);
    text-decoration: none;
    color: var(--ink);
    align-items: center;
    transition: all 0.2s var(--ease);
  }
  .archive-row:hover { background: rgba(45, 106, 91, 0.04); padding-left: 16px; padding-right: 16px; }
  .archive-row__year {
    font-family: var(--mono);
    font-size: 11px;
    color: var(--verd);
    letter-spacing: var(--track-narrow);
  }
  .archive-row__outlet {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    font-weight: 500;
  }
  .archive-row__headline {
    font-family: var(--display);
    font-style: italic;
    font-weight: 300;
    font-size: 17px;
    line-height: 1.35;
    color: var(--ink);
  }
  .archive-row__project {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
  }
  .archive-row__type {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    display: inline-flex;
    align-items: center;
    gap: 6px;
  }
  .archive-row__type::before {
    content: ''; width: 4px; height: 4px; background: var(--verd); border-radius: 50%;
  }
  .archive-row__arrow {
    font-family: var(--mono);
    color: var(--verd);
    font-size: 14px;
    text-align: right;
    transition: transform 0.3s var(--ease);
  }
  .archive-row:hover .archive-row__arrow { transform: translateX(4px); }

  /* list view responsive: simplify grid on tablet/mobile */
  @media (max-width: 1000px) {
    .archive-row {
      grid-template-columns: 50px 1fr 80px;
      grid-template-rows: auto auto;
      gap: 12px 16px;
      padding: 18px 0;
    }
    .archive-row__year { grid-row: 1; grid-column: 1; }
    .archive-row__outlet { grid-row: 1; grid-column: 2; }
    .archive-row__type { grid-row: 1; grid-column: 3; }
    .archive-row__headline { grid-row: 2; grid-column: 1 / 3; }
    .archive-row__project { grid-row: 2; grid-column: 3; text-align: right; align-self: center; }
    .archive-row__arrow { display: none; }
  }

  /* hidden state for filter */
  .archive-card.hidden, .archive-row.hidden { display: none; }

  /* empty state */
  .archive-empty {
    padding: 32px 20px;
    text-align: center;
    font-family: var(--display);
    font-style: italic;
    font-size: 20px;
    color: var(--ink);
    opacity: 0.5;
    display: none;
  }
  .archive-empty.visible { display: block; }

  /* video press section — for podcasts, video interviews */
  .press-video {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
    border-top: 1px solid var(--rule);
    /* Offset for the fixed nav when the #on-camera anchor is targeted
       from the wayfinder. Mirrors the .press-kit treatment at line 4230. */
    scroll-margin-top: 80px;
    /* Reserve vertical space before the JS-populated grid renders.
       Without this, the section is short on first paint, the wayfinder
       click computes scroll position against the short version, then
       the grid populates and pushes the section down — landing the
       user past the intended target. The min-height keeps geometry
       stable during the JS-populate phase so anchor jumps land
       correctly the FIRST time. Empirically tuned to roughly match
       a populated row of cards plus the social row beneath; if the
       actual content runs taller, the section grows naturally. */
    min-height: 600px;
  }
  /* On Camera grid on the press page uses the same markup as the
     project page's video grid (.proj-video-card). This container
     just needs the equivalent grid layout. The shared .proj-video-card
     rules above provide all the card styling. On mobile the
     .is-mobile-swipe class takes over and the per-container
     --swipe-card-w above drives the child width. */
  .press-video__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 16px;
  }

  /* ══════════════════════════════════════════════════════════════════
     PROJ-VIDEO-CARD — light-background variant for press page
     ──────────────────────────────────────────────────────────────────
     The base .proj-video-card is designed to sit inside the project
     page's dark .proj-panel — its title is cream, meta is light verd,
     card background is a faint lightening of dark bg.

     On the press page, the same markup sits on the cream body. The
     rules below only trigger inside .press-video and remap the
     color values. Structure, dimensions, hover lift, and gradient
     thumb fallback are all preserved exactly as on the project page. */
  .press-video .proj-video-card {
    color: var(--ink);
    background: var(--chalk-mid);
    border-color: var(--rule-strong);
  }
  .press-video .proj-video-card:hover,
  .press-video .proj-video-card:focus-visible {
    background: var(--chalk-deep);
    border-color: var(--verd);
  }
  .press-video .proj-video-card__title {
    color: var(--ink);
  }
  .press-video .proj-video-card__meta {
    color: var(--verd);
  }
  /* Play button stays light-on-dark since the thumb itself is dark. */

  /* press kit CTA block */
  .press-kit {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
    border-top: 1px solid var(--rule);
    /* Offset for the fixed nav when the #press-kit anchor is targeted */
    scroll-margin-top: 80px;
  }
  .press-kit__inner {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 60px;
    align-items: center;
    border: 1px solid var(--rule);
    padding: 56px;
    background: var(--dark-card);
    position: relative;
  }
  .press-kit__inner::before,
  .press-kit__inner::after {
    content: ''; position: absolute;
    width: 20px; height: 20px;
  }
  .press-kit__inner::before {
    top: -1px; left: -1px;
    border-top: 1px solid var(--verd);
    border-left: 1px solid var(--verd);
  }
  .press-kit__inner::after {
    bottom: -1px; right: -1px;
    border-bottom: 1px solid var(--verd);
    border-right: 1px solid var(--verd);
  }
  .press-kit__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    margin-bottom: 16px;
  }
  .press-kit__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: clamp(28px, 3.5vw, 44px);
    line-height: 1.1;
    letter-spacing: var(--track-card);
    color: var(--cream);
    margin-bottom: 20px;
  }
  .press-kit__title em { color: var(--verd-light); }
  .press-kit__body {
    font-size: 14px;
    line-height: var(--lh-prose);
    color: var(--cream);
    opacity: 0.75;
    margin-bottom: 28px;
  }
  .press-kit__actions {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
  }
  .press-kit__list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
  }
  .press-kit__list li {
    list-style: none;
    border-bottom: 1px solid rgba(242,237,228,0.1);
  }
  .press-kit__list li a {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 0;
    color: var(--cream);
    text-decoration: none;
    opacity: 0.7;
    transition: opacity 0.15s, padding 0.15s;
  }
  .press-kit__list li a:hover {
    opacity: 1;
    padding-left: 6px;
  }
  .press-kit__list li > span:first-child,
  .press-kit__list li a > span:first-child {
    display: inline-block;
  }
  .press-kit__type {
    color: var(--verd-light);
    padding: 3px 8px;
    border: 1px solid rgba(109, 191, 173, 0.3);
    border-radius: 2px;
    font-size: 9px;
    letter-spacing: var(--track-narrow);
  }
  .press-kit__list li:not(:has(a)) {
    display: flex;
    justify-content: space-between;
    padding: 12px 0;
    opacity: 0.5; /* dim placeholders to signal they're not yet configured */
  }

  @media (max-width: 1024px) {
    .press-kit__inner {
      grid-template-columns: 1fr;
      padding: 40px 28px;
      gap: 36px;
    }
    .press-archive { padding: 32px 20px; }
    .press-featured { padding: 32px 20px; }
    .press-video { padding: 32px 20px; }
    .press-kit { padding: 32px 20px; }
    .filter-row { gap: 12px; }
    .filter-row__label { min-width: 60px; }
  }

  /* ════════ SOCIAL ICONS ════════ */
  .social-links {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .social-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px; height: 36px;
    /* Default: light-context (contact page, paper sections).
       Ink-on-rule styling so icons read against chalk backgrounds. */
    border: 1px solid var(--rule-strong);
    border-radius: 50%;
    text-decoration: none;
    color: var(--ink);
    opacity: 0.7;
    background: transparent;
    transition: all 0.2s var(--ease);
    flex-shrink: 0;
  }
  .social-link:hover {
    opacity: 1;
    border-color: var(--verd);
    color: var(--verd);
    background: rgba(45,106,91,0.04);
  }
  .social-link svg {
    width: 14px; height: 14px;
    fill: currentColor;
  }
  /* PNG icon uploads — sized to match inline SVGs. PNG can't use
     currentColor, so its look is fixed to whatever the image file has.
     object-fit: contain prevents distortion if the PNG's aspect ratio
     isn't square. */
  .social-link .social-icon-img {
    width: 14px;
    height: 14px;
    object-fit: contain;
    display: block;
  }

  /* Dark-context override — icons sitting on dark surfaces (footer, menu
     overlay, project panels) need cream-on-dim treatment to stay legible. */
  .footer .social-link,
  .footer__social .social-link,
  .menu-overlay .social-link,
  .proj-panel .social-link,
  .press-kit__inner .social-link,
  .dark .social-link {
    border-color: rgba(242,237,228,0.2);
    color: var(--cream);
    opacity: 0.6;
    background: transparent;
  }
  .footer .social-link:hover,
  .footer__social .social-link:hover,
  .menu-overlay .social-link:hover,
  .proj-panel .social-link:hover,
  .press-kit__inner .social-link:hover,
  .dark .social-link:hover {
    opacity: 1;
    border-color: var(--verd-light);
    color: var(--verd-light);
    background: transparent;
  }

  /* footer social — slightly smaller gap */
  .footer__social {
    display: flex;
    gap: 8px;
    margin-top: 20px;
  }
  /* close button */
  .menu-close-btn {
    position: absolute;
    top: 24px; right: 28px;
    width: 44px; height: 44px;
    background: none;
    border: 1px solid rgba(242,237,228,0.2);
    border-radius: 50%;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0;
    z-index: 53;
    transition: border-color 0.2s var(--ease);
  }
  .menu-close-btn:hover { border-color: var(--verd-light); }
  .menu-close-btn span {
    display: block;
    width: 18px; height: 1.5px;
    background: var(--cream);
    position: absolute;
    transition: background 0.2s;
  }
  .menu-close-btn span:first-child  { transform: rotate(45deg); }
  .menu-close-btn span:last-child   { transform: rotate(-45deg); }
  .menu-close-btn:hover span { background: var(--verd-light); }

  
  .footer__bottom--social .social-links { gap: 6px; }
  .footer__bottom--social .social-link {
    width: 28px; height: 28px;
    border-color: rgba(242,237,228,0.15);
  }
  .footer__bottom--social .social-link svg { width: 11px; height: 11px; }
  .footer__bottom--social .social-link .social-icon-img { width: 11px; height: 11px; }

  /* ════════ COURSES PAGE ════════ */
  .courses-hero {
    padding: 100px 32px 48px;
    max-width: 1500px;
    margin: 0 auto;
    border-bottom: 1px solid var(--rule);
  }
  .courses-hero__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 20px;
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .courses-hero__eyebrow::before {
    content: ''; width: 24px; height: 1px; background: var(--verd);
  }
  .courses-hero__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-hero-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
    margin-bottom: 28px;
  }
  .courses-hero__title em { color: var(--verd); font-weight: 400; }
  .courses-hero__lede {
    font-size: var(--h-lede);
    line-height: var(--lh-body);
    color: var(--ink);
    opacity: 0.85;
    max-width: 640px;
    font-family: var(--display);
    font-weight: 300;
  }

  /* ── PLATFORM BAND ── */
  .courses-platform-band {
    background: var(--chalk-mid);
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
    padding: 20px 32px;
    display: flex;
    align-items: center;
    gap: 32px;
    flex-wrap: wrap;
    /* v2.9.198 — Floor reservation to lock bounding box against
       font-swap. PageSpeed measured CLS 0.535 desktop / 0.347 mobile
       attributed to this element shifting. Lighthouse layout shift
       culprits named geist-mono-500.woff2 + instrument-serif fonts as
       the cause: when Geist Mono swaps in over the system mono
       fallback, the band's text reflows with slightly different
       x-height/leading, growing the band's rendered height by a few
       pixels — and pushing everything below it (the courses grid)
       down. Floor min-height eliminates the shift; content can still
       grow above this floor if it wraps on very narrow viewports.
       v2.9.207 — restored after v2.9.206 inadvertently removed it. */
    min-height: 64px;
  }
  .courses-platform-band__label {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    flex-shrink: 0;
  }
  .courses-platform-badge {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-narrow);
    text-transform: uppercase;
    color: var(--ink);
    text-decoration: none;
    padding: 6px 14px;
    border: 1px solid var(--rule-strong);
    transition: all 0.2s var(--ease);
  }
  .courses-platform-badge:hover {
    border-color: var(--verd);
    color: var(--verd);
  }
  .courses-platform-badge svg {
    width: 14px; height: 14px;
    fill: currentColor;
    opacity: 0.7;
  }

  /* ── COURSE GRID ── */
  .courses-grid-section {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
  }
  .courses-grid-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    margin-bottom: 56px;
    gap: 32px;
    flex-wrap: wrap;
  }
  .courses-grid-head__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-section-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
  }
  .courses-grid-head__title em { color: var(--verd); }
  .courses-grid-head__count {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    text-align: right;
  }
  .courses-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
    gap: 32px;
  }
  @media (max-width: 500px) { .courses-grid { grid-template-columns: 1fr; } }

  /* ── COURSE CARD ── */
  .course-card {
    display: flex;
    flex-direction: column;
    text-decoration: none;
    color: var(--ink);
    border: 1px solid var(--rule-strong);
    background: var(--chalk);
    /* Explicit property list — avoid `transition: all` which would
       silently animate any future property added to this rule.
       box-shadow animation is acceptable here because it only runs
       during :hover (one-off); we do NOT want it animating during
       scroll-driven state changes. */
    transition: border-color 0.3s var(--ease), transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
    position: relative;
  }
  .course-card:hover {
    border-color: var(--verd);
    transform: translateY(-3px);
    box-shadow: 0 12px 40px rgba(26,25,22,0.08);
  }
  /* the clickable wrapper holding thumb + body (footer is outside this) */
  .course-card__link-wrap {
    display: flex;
    flex-direction: column;
    text-decoration: none;
    color: inherit;
    flex: 1;
  }
  /* corner bracket on hover */
  .course-card::before {
    content: '';
    position: absolute;
    inset: 0;
    background:
      linear-gradient(to right, var(--verd) 0, var(--verd) 16px, transparent 16px) 0 0 / 100% 1px no-repeat,
      linear-gradient(to bottom, var(--verd) 0, var(--verd) 16px, transparent 16px) 0 0 / 1px 100% no-repeat,
      linear-gradient(to left, var(--verd) 0, var(--verd) 16px, transparent 16px) 100% 100% / 100% 1px no-repeat,
      linear-gradient(to top, var(--verd) 0, var(--verd) 16px, transparent 16px) 100% 100% / 1px 100% no-repeat;
    opacity: 0;
    transition: opacity 0.3s var(--ease);
    pointer-events: none;
    z-index: 1;
  }
  .course-card:hover::before { opacity: 1; }

  .course-card__thumb {
    aspect-ratio: 16/9;
    background: linear-gradient(135deg, var(--dark-card) 0%, var(--dark) 100%);
    position: relative;
    overflow: hidden;
    flex-shrink: 0;
  }
  /* When a Featured Image is set, the card thumb shows the image instead of
     the SHOT LISTING / CAMERA TECH / etc text label. The text label still
     ships in the markup for alt-text / accessibility but is visually hidden. */
  .course-card__thumb--has-image .course-card__thumb-inner {
    opacity: 0;
  }
  .course-card__thumb--has-image::after {
    content: '';
    position: absolute; inset: 0;
    background: linear-gradient(180deg, rgba(0,0,0,0) 50%, rgba(0,0,0,0.35) 100%);
    pointer-events: none;
  }
  .course-card__thumb-inner {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    opacity: 0.5;
  }
  .course-card__platform-tag {
    position: absolute;
    top: 14px; left: 14px;
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    background: rgba(13,10,7,0.7);
    backdrop-filter: blur(8px);
    color: var(--cream);
    padding: 4px 10px;
    z-index: 2;
  }
  .course-card__body {
    padding: 28px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    flex: 1;
  }
  .course-card__meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
  }
  .course-card__category {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    background: rgba(45,106,91,0.08);
    padding: 3px 8px;
    border-radius: 2px;
  }
  .course-card__level {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.45;
  }
  .course-card__title {
    font-family: var(--display);
    font-size: clamp(20px, 2vw, 26px);
    font-weight: 400;
    letter-spacing: var(--track-card);
    line-height: var(--lh-display-medium);
    color: var(--ink);
  }
  .course-card__desc {
    font-size: 14px;
    line-height: var(--lh-prose);
    color: var(--ink);
    opacity: 0.6;
  }

  /* ── STATS BLOCK — format / sessions / duration ── */
  .course-card__stats {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 4px;
    padding: 16px 0;
    margin: 4px 0;
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
  }
  .course-card__stat {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 0 4px;
  }
  .course-card__stat + .course-card__stat {
    border-left: 1px solid var(--rule);
    padding-left: 14px;
  }
  .course-card__stat-label {
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    opacity: 0.85;
  }
  .course-card__stat-value {
    font-family: var(--display);
    font-size: 13px;
    font-weight: 400;
    color: var(--ink);
    line-height: 1.3;
    letter-spacing: -0.005em;
  }

  .course-card__topics {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    flex: 1;
    align-content: flex-start;
  }
  .course-card__topic {
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-narrow);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    border: 1px solid var(--rule-strong);
    padding: 3px 8px;
  }
  .course-card__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 20px 28px;
    border-top: 1px solid var(--rule);
    background: var(--chalk-mid);
    flex-wrap: wrap;
  }
  .course-card__price {
    font-family: var(--display);
    font-size: 22px;
    font-weight: 400;
    letter-spacing: var(--track-subtle);
    color: var(--ink);
  }
  .course-card__price small {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-narrow);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    display: block;
    margin-top: 2px;
  }
  .course-card__actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
  }
  .course-card__btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    padding: 10px 16px;
    text-decoration: none;
    border: 1px solid transparent;
    transition: all 0.2s var(--ease);
    cursor: pointer;
    white-space: nowrap;
  }
  .course-card__btn--primary {
    color: var(--cream);
    background: var(--verd);
    border-color: var(--verd);
  }
  .course-card__btn--primary:hover {
    background: var(--verd-deep);
    border-color: var(--verd-deep);
  }
  .course-card__btn--ghost {
    color: var(--ink);
    background: transparent;
    border-color: var(--rule-strong);
  }
  .course-card__btn--ghost:hover {
    border-color: var(--verd);
    color: var(--verd);
  }
  .course-card__btn-arrow { transition: transform 0.3s var(--ease); }
  .course-card__btn:hover .course-card__btn-arrow { transform: translateX(3px); }
  .course-card__btn--ghost:hover .course-card__btn-arrow { transform: translateY(2px); }

  @media (max-width: 560px) {
    .course-card__stats { grid-template-columns: 1fr; gap: 12px; }
    .course-card__stat + .course-card__stat { border-left: none; padding-left: 0; padding-top: 12px; border-top: 1px solid var(--rule); }
    .course-card__footer { flex-direction: column; align-items: stretch; }
    .course-card__actions { justify-content: stretch; }
    .course-card__btn { flex: 1; justify-content: center; }
  }

  /* ── TEACHING PHILOSOPHY BAND ── */
  .courses-philosophy {
    background: var(--dark);
    padding: 48px 32px;
  }
  .courses-philosophy__inner {
    max-width: 1500px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: 1fr 1.8fr;
    gap: 80px;
    align-items: start;
  }
  .courses-philosophy__label {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    margin-bottom: 20px;
  }
  .courses-philosophy__heading {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-mid-title);
    letter-spacing: var(--track-tight);
    line-height: 1.1;
    color: var(--cream);
  }
  .courses-philosophy__heading em { color: var(--verd-light); }
  .courses-philosophy__body {
    font-family: var(--display);
    font-size: 18px;
    font-weight: 300;
    line-height: 1.7;
    color: var(--cream);
    opacity: 0.8;
  }
  .courses-philosophy__body p { margin-bottom: 20px; }
  .courses-philosophy__body p:last-child { margin-bottom: 0; }
  .courses-philosophy__body strong { color: var(--verd-light); font-weight: 500; opacity: 1; }
  @media (max-width: 1024px) {
    .courses-hero { padding: 96px 20px 32px; }
    .courses-platform-band { padding: 16px 20px; min-height: 56px; }
    .courses-grid-section { padding: 32px 20px; }
    .courses-grid { grid-template-columns: 1fr; }
    .courses-philosophy { padding: 32px 20px; }
    .courses-philosophy__inner { grid-template-columns: 1fr; gap: 40px; }
  }

  /* ════════ THE KICKLIGHTER BAG (JAMAH collaboration) ════════ */
  .bag-hero {
    padding: 100px 32px 48px;
    max-width: 1500px;
    margin: 0 auto;
    display: grid;
    /* v2.9.42 — was 2 columns × 1 row. With the v2.9.42 DOM split that
       broke the text column into text-top + cta-wrap (so the image
       could be positioned between them on mobile), desktop now uses
       grid-template-areas so the left column stacks text-top above
       cta-wrap while the image fills the right column across both
       rows. The 1fr auto row sizing lets the CTA hug its natural
       button height while the text-top rows take whatever is needed
       above. */
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr auto;
    grid-template-areas:
      "text image"
      "cta  image";
    column-gap: 80px;
    row-gap: 24px;
    align-items: center;
  }
  .bag-hero__text-top { grid-area: text; }
  .bag-hero__cta-wrap { grid-area: cta; }
  .bag-hero__image    { grid-area: image; }
  .bag-hero__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 20px;
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .bag-hero__eyebrow::before { content: ''; width: 24px; height: 1px; background: var(--verd); }
  .bag-hero__title {
    font-family: var(--display);
    font-weight: 300;
    /* Slightly smaller than --h-hero-title because the bag page is a
       product detail layout — specs column sits beside the title, so
       the headline needs to share horizontal room with structured data. */
    font-size: clamp(48px, 6vw, 88px);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
    margin-bottom: 24px;
  }
  .bag-hero__title em { color: var(--verd); font-weight: 400; font-style: italic; }
  .bag-hero__brand {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    margin-bottom: 32px;
  }
  .bag-hero__brand a {
    color: var(--verd);
    text-decoration: none;
    opacity: 1;
    border-bottom: 1px solid transparent;
    transition: border-color 0.2s var(--ease);
  }
  .bag-hero__brand a:hover { border-bottom-color: var(--verd); }
  .bag-hero__price {
    font-family: var(--display);
    font-size: 32px;
    font-weight: 300;
    color: var(--ink);
    margin-bottom: 40px;
  }
  .bag-hero__price-from {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    margin-right: 8px;
  }
  .bag-hero__copy {
    font-family: var(--display);
    font-size: 20px;
    line-height: var(--lh-body);
    color: var(--ink);
    margin-bottom: 40px;
    max-width: 500px;
  }
  .bag-hero__copy em { color: var(--verd); font-style: italic; }
  .bag-hero__cta {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    background: var(--verd);
    color: var(--cream);
    padding: 18px 32px;
    text-decoration: none;
    transition: background 0.2s var(--ease);
  }
  .bag-hero__cta:hover { background: var(--verd-deep); }
  .bag-hero__image {
    /* Let the image drive the container's size. The parent grid has
       align-items:center (see .bag-hero), which vertically centers
       this block alongside the text column. */
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
  }
  .bag-hero__image img {
    /* Product photo: show the whole bag at its natural aspect. We
       constrain by height (keeps hero from getting too tall for any
       given image) and let width scale. Because the parent is a flex
       column justified center, the image self-centers horizontally
       in the grid column regardless of its rendered width. This
       prevents the portrait-bag-in-landscape-column letterboxing
       problem where object-fit:contain was adding large transparent
       bands above and below the bag. */
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 640px;
    object-fit: contain;
    filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.15))
            drop-shadow(0 8px 16px rgba(0, 0, 0, 0.10));
  }
  /* Empty-state placeholder — shown when no image has been uploaded yet. */
  .bag-hero__image--empty {
    aspect-ratio: 4/5;
    background: var(--chalk-mid);
    border: 1px solid var(--rule);
  }
  .bag-hero__image-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    opacity: 0.5;
    text-align: center;
    padding: 40px;
  }

  /* ── LEATHERS GRID ── */
  .bag-leathers {
    padding: 80px 32px 60px;
    max-width: 1500px;
    margin: 0 auto;
    border-top: 1px solid var(--rule);
  }
  .bag-leathers__head {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    margin-bottom: 48px;
    flex-wrap: wrap;
    gap: 24px;
  }
  .bag-leathers__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-mid-title);
    letter-spacing: var(--track-tight);
    color: var(--ink);
  }
  .bag-leathers__title em { color: var(--verd); font-style: italic; }
  .bag-leathers__count {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
  }
  .bag-leathers__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 20px;
  }
  .bag-leather-card {
    aspect-ratio: 1;
    position: relative;
    overflow: hidden;
    background: var(--chalk-mid);
    border: 1px solid var(--rule);
    display: flex;
    align-items: flex-end;
    padding: 20px;
    transition: border-color 0.25s var(--ease);
    cursor: default;
  }
  .bag-leather-card:hover { border-color: var(--verd); }
  .bag-leather-card__swatch {
    position: absolute;
    inset: 0;
    z-index: 0;
  }
  .bag-leather-card__name {
    position: relative;
    z-index: 1;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--chalk);
    text-shadow: 0 1px 4px rgba(0,0,0,0.4);
  }
  .bag-leather-card--light .bag-leather-card__name {
    color: var(--ink);
    text-shadow: none;
  }

  /* ── SPECS + STORY BLOCK ── */
  .bag-specs {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
    border-top: 1px solid var(--rule);
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 80px;
  }
  .bag-specs__col h3 {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 24px;
  }
  .bag-specs__list {
    list-style: none;
    padding: 0;
    margin: 0;
  }
  .bag-specs__list li {
    font-family: var(--display);
    font-size: 17px;
    line-height: var(--lh-prose);
    color: var(--ink);
    padding: 14px 0;
    border-bottom: 1px solid var(--rule);
    display: flex;
    justify-content: space-between;
    gap: 24px;
  }
  .bag-specs__list li:last-child { border-bottom: none; }
  .bag-specs__list dt {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    flex-shrink: 0;
  }
  .bag-story p {
    font-family: var(--display);
    font-size: 19px;
    line-height: 1.65;
    color: var(--ink);
    margin-bottom: 20px;
  }
  .bag-story em { color: var(--verd); font-style: italic; }
  .bag-story__tag {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    margin-top: 32px;
    padding-top: 20px;
    border-top: 1px solid var(--rule);
  }

  @media (max-width: 1024px) {
    /* v2.9.42 — bag-hero mobile reorder per James's request. Previous
       behavior on mobile (1-col grid) showed every text element first
       then the image at the bottom, which left a large dead space
       between the "Shop The Kicklighter" CTA and the "Ten Leathers"
       section beneath. Reading order is now:
         eyebrow → title → brand → price → body copy → IMAGE → CTA
       so the bag image appears immediately after the descriptive copy
       (where the user would naturally want to see what the product
       looks like) and the CTA sits flush against the next section,
       collapsing the gap.

       Mechanism: switch the grid container to flex column, set explicit
       `order` on each of the three direct children. Lower `order`
       numbers render first. On desktop this rule doesn't apply and the
       grid layout below (left-column text-top stacked above cta-wrap,
       image in right column) is preserved. */
    .bag-hero {
      display: flex;
      flex-direction: column;
      gap: 24px;
      padding: 96px 20px 32px;
    }
    .bag-hero__text-top  { order: 1; }
    .bag-hero__image     { order: 2; }
    .bag-hero__cta-wrap  { order: 3; }
    .bag-hero__image img {
      /* Image is no longer the first thing the user sees on mobile — it
         sits between body copy and CTA. Cap its height a bit lower
         (was 420) so the surrounding text + CTA stay above the fold of
         a scroll-reveal: at 360px the bag is still clearly the visual
         centerpiece without pushing the CTA off-screen on smaller
         devices like iPhone SE. */
      max-height: 360px;
    }
    .bag-hero__cta-wrap {
      /* Full-width tap target on mobile. The desktop CTA inherits its
         natural intrinsic width from the .bag-hero__cta inline-flex
         button; mobile users scan vertically so a stretched button
         reads better and avoids accidental taps on whitespace. */
      width: 100%;
    }
    .bag-hero__cta {
      width: 100%;
      justify-content: center;
    }
    .bag-leathers { padding: 60px 20px 40px; }
    .bag-specs { grid-template-columns: 1fr; gap: 48px; padding: 32px 20px; }
    .bag-leathers__grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  }

  /* ════════ MUSIC VIDEOS PAGE ════════ */
  .mv-hero {
    padding: 100px 32px 48px;
    max-width: 1500px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 80px;
    align-items: end;
    border-bottom: 1px solid var(--rule);
  }
  @media (max-width: 1024px) { .mv-hero { grid-template-columns: 1fr; gap: 32px; padding: 96px 20px 32px; } }
  .mv-hero__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 20px;
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .mv-hero__eyebrow::before { content: ''; width: 24px; height: 1px; background: var(--verd); }
  .mv-hero__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-hero-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
  }
  .mv-hero__title em { color: var(--verd); font-weight: 400; }
  .mv-hero__right {
    padding-bottom: 8px;
  }
  .mv-hero__lede {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-lede);
    line-height: var(--lh-body);
    color: var(--ink);
    opacity: 0.85;
    margin-bottom: 32px;
  }
  .mv-hero__stats {
    display: flex;
    gap: 40px;
    flex-wrap: wrap;
  }
  .mv-hero__stat-num {
    font-family: var(--display);
    font-size: 36px;
    font-weight: 300;
    letter-spacing: var(--track-tight);
    color: var(--ink);
    line-height: 1;
  }
  .mv-hero__stat-label {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-top: 4px;
  }

  /* ── FEATURED MV — full width, large format ── */
  
  
  
  
  
  
  /* large 16:9 card with overlay */
  
  
  
  /* gradient scrim for text legibility */
  
  
  
  
  
  
  
  

  /* ── MV GRID — all videos ── */
  .mv-grid-section {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
  }
  @media (max-width: 1024px) { .mv-grid-section { padding: 32px 20px; } }
  .mv-grid-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    margin-bottom: 48px;
    gap: 24px;
    flex-wrap: wrap;
    /* v2.9.114 — Reserve the head block height to prevent CLS when
       the title font swaps. PageSpeed flagged .mv-grid-section as
       0.107 mobile / 0.095 desktop CLS — the prior chip-row reserve
       (28px) covered the chips but the title above it shifts when
       Instrument Serif weight-300 swaps from synthesized to native.
       The head's min-height covers the title (~50px desktop / ~36px
       mobile at h-section-title) + the chip row (28px) + the gap
       (24px) — total ~102px desktop / ~88px mobile. */
    min-height: 88px;
  }
  @media (min-width: 700px) {
    .mv-grid-head { min-height: 102px; }
  }
  .mv-grid-head__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-section-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
    /* v2.9.128 — Reserve title height to prevent font-swap CLS.
       PageSpeed on /films/ mobile reported 0.107 shift attributed
       to .mv-grid-section when Instrument Serif weight 300 swapped
       in. The "All Films" title height grows ~10px between Times
       fallback and Instrument Serif, pushing the grid below it
       down. Single-line reservation matches font-size × line-height
       at each breakpoint:
         Mobile (~32px × 1.05):  ~34px → reserve 36px
         Tablet (~52px × 1.05):  ~55px → reserve 56px
         Desktop (~80px × 1.05): ~84px → reserve 86px */
    min-height: 36px;
  }
  @media (min-width: 700px) {
    .mv-grid-head__title { min-height: 56px; }
  }
  @media (min-width: 1100px) {
    .mv-grid-head__title { min-height: 86px; }
  }
  .mv-grid-head__title em { color: var(--verd); }
  /* filter chips reuse existing .fchip style */
  .mv-filters {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    /* v2.9.101 — Reserve chip-row height so JS-rendered filter chips
       don't push the grid down on first paint. Lighthouse on /films
       desktop measured 0.117 CLS attributed to .mv-grid-section__inner;
       the chip row was the upstream cause. min-height matches the chip
       height (28px includes padding + border). */
    min-height: 28px;
  }
  .mv-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 24px;
    /* Pre-render space reservation. JS populates these grids client-side
       (see renderFilmsGrid / renderMvGrid in main.js), so without a min
       reservation the section height jumps from ~0 to actual once the
       cards inject — the entire footer + leader-band moves down, which
       is the CLS PageSpeed flagged on /films/. We reserve a generous
       floor here; once JS sets [data-rendered=1], the floor drops and
       content-visibility on individual cards takes over.

       Desktop only — mobile uses .is-mobile-swipe which is a single
       horizontal row, completely different layout, handled below. */
    min-height: 1200px;
  }
  .mv-grid[data-rendered="1"] { min-height: 0; }
  @media (max-width: 1024px) {
    /* Mobile-swipe: single horizontal row of 260px cards with 16:9
       thumbs (~146px) + 12px margin + title (~22px) + meta (~18px) +
       16px swipe-rail bottom-padding ≈ 215-225px total card height.
       Reservation of 230px matches almost exactly so the post-render
       shrink is < 20px — well below CLS-significance threshold. The
       earlier 2400px reservation assumed a vertical stack and caused
       a ~2100px shift, the original 0.333 CLS PageSpeed flag. */
    .mv-grid { min-height: 230px; }
    .mv-grid:not(.is-mobile-swipe) {
      /* Filtered (non-swipe) mode: vertical stack, ~5 cards tall on
         a typical mobile viewport. Single-column grid, each card
         ~480px (16:9 thumb scaled to 100% width + meta). */
      min-height: 2400px;
    }
  }
  @media (max-width: 500px) { .mv-grid { grid-template-columns: 1fr; } }
  /* Defer offscreen film/commercial/MV cards. Scales with catalog —
     the page renders all projects at once, so a 30-title filmography
     or growing MV archive benefits from skipped offscreen layout. */
  .mv-grid .mv-card {
    content-visibility: auto;
    contain-intrinsic-size: 1px 460px;
  }

  /* ── MV CARD ── */
  .mv-card {
    position: relative;
    overflow: hidden;
    background: var(--dark-card);
    border: 1px solid rgba(242,237,228,0.08);
    cursor: pointer;
    display: block;
    text-decoration: none;
    transition: all 0.3s var(--ease);
  }
  .mv-card:hover { border-color: var(--verd-light); }
  .mv-card__thumb {
    aspect-ratio: 16/9;
    position: relative;
    overflow: hidden;
  }
  .mv-card__bg {
    position: absolute; inset: 0;
    background: linear-gradient(135deg, var(--dark-card) 0%, var(--dark) 100%);
    transition: transform 0.6s var(--ease);
  }
  .mv-card:hover .mv-card__bg { transform: scale(1.04); }
  .mv-card__thumb::after {
    content: '';
    position: absolute; inset: 0;
    background: linear-gradient(180deg, transparent 40%, rgba(13,10,7,0.85) 100%);
    z-index: 1;
    pointer-events: none;
  }
  .mv-card__play {
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    z-index: 3;
    width: 52px; height: 52px;
    border-radius: 50%;
    border: 1px solid rgba(242,237,228,0.45);
    background: rgba(13,10,7,0.3);
    backdrop-filter: blur(8px);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: all 0.3s var(--ease);
  }
  .mv-card__play::before {
    content: '';
    width: 0; height: 0;
    border-left: 10px solid var(--cream);
    border-top: 6px solid transparent;
    border-bottom: 6px solid transparent;
    margin-left: 3px;
  }
  .mv-card:hover .mv-card__play {
    opacity: 1;
    background: var(--verd);
    border-color: var(--verd-light);
  }
  .mv-card__duration {
    position: absolute;
    top: 10px; right: 12px;
    z-index: 3;
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-mid);
    background: rgba(13,10,7,0.7);
    backdrop-filter: blur(6px);
    color: var(--cream);
    padding: 3px 8px;
  }
  .mv-card__genre-tag {
    position: absolute;
    top: 10px; left: 12px;
    z-index: 3;
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    background: rgba(45,106,91,0.75);
    backdrop-filter: blur(6px);
    color: var(--cream);
    padding: 3px 8px;
  }
  .mv-card__body {
    padding: 20px 22px 22px;
    background: var(--dark-card);
  }
  .mv-card__artist {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    margin-bottom: 6px;
    /* Single-line ellipsis — long artist names (bands with very
       long names, or multi-artist collaborations like "feat. ...")
       would otherwise push the card width past its bounds. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .mv-card__title {
    font-family: var(--display);
    font-size: 20px;
    font-weight: 400;
    letter-spacing: var(--track-subtle);
    color: var(--cream);
    line-height: var(--lh-display-medium);
    margin-bottom: 8px;
    /* Clamp to 2 lines. YouTube titles are unpredictable — occasionally
       a video will have a 10+ word title that overflows the card.
       Line-clamp with ellipsis keeps every card the same shape. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* Fixed height so cards with 1-line vs 2-line titles align in
       the grid — 2 lines × 1.2 line-height × 20px = 48px. */
    min-height: calc(1.2em * 2);
  }

  /* Contextual 1-sentence note — italic, subdued, sits between title and meta */
  

  /* Meta list — label : value pairs using the site's mono-label convention */
  
  
  
  

  .mv-card__footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 12px;
    border-top: 1px solid rgba(242,237,228,0.08);
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.4;
  }
  .mv-card__watch-link {
    color: var(--verd-light);
    opacity: 1;
    display: flex;
    align-items: center;
    gap: 6px;
    transition: gap 0.2s var(--ease);
  }
  .mv-card:hover .mv-card__watch-link { gap: 10px; }
  .mv-card.hidden { display: none; }

  /* ════════ VIDEO EMBED MODAL ════════ */
  .embed-modal {
    position: fixed; inset: 0;
    /* v2.9.133 — Bumped from 200 → 1200 alongside .lightbox for the
       same reason. Video/social embed modals were being occluded by
       the fixed nav at z-index 1000, leaving the close-X button and
       modal bar behind the navigation. See .lightbox z-index comment
       above for the full rationale. */
    z-index: 1200;
    display: flex; align-items: center; justify-content: center;
    background: rgba(13,10,7,0.92);
    backdrop-filter: blur(16px);
    opacity: 0; pointer-events: none;
    transition: opacity 0.35s var(--ease);
  }
  .embed-modal.open { opacity: 1; pointer-events: all; }

  /* Prominent corner X — always visible in top-right of modal, above video frame */
  .embed-modal__close-x {
    position: absolute;
    top: 24px; right: 24px;
    z-index: 10;
    width: 48px; height: 48px;
    display: flex; align-items: center; justify-content: center;
    background: rgba(13, 10, 7, 0.72);
    border: 1px solid rgba(242, 237, 228, 0.25);
    border-radius: 50%;
    color: var(--cream);
    cursor: pointer;
    /* Explicit list — avoid `transition: all` so future property
       additions don't silently trigger layout-expensive animations. */
    transition: background 0.2s var(--ease), border-color 0.2s var(--ease), transform 0.2s var(--ease);
    backdrop-filter: blur(8px);
  }
  .embed-modal__close-x svg {
    width: 22px; height: 22px;
    display: block;
  }
  .embed-modal__close-x:hover {
    background: rgba(242, 237, 228, 0.15);
    border-color: var(--cream);
    transform: scale(1.08);
  }
  .embed-modal__close-x:focus-visible {
    outline: 2px solid var(--verd-light);
    outline-offset: 3px;
  }
  @media (max-width: 640px) {
    .embed-modal__close-x {
      top: 14px; right: 14px;
      /* 44×44 is the WCAG touch-target floor; previous 40×40 was under. */
      width: 44px; height: 44px;
    }
    .embed-modal__close-x svg { width: 18px; height: 18px; }
  }

  .embed-modal__inner {
    position: relative;
    width: min(92vw, 1100px);
    background: var(--dark-card);
    border: 1px solid rgba(242,237,228,0.1);
  }
  /* 16:9 wrapper */
  .embed-modal__frame-wrap {
    position: relative;
    width: 100%;
    aspect-ratio: 16/9;
    background: var(--dark);
    overflow: hidden;
  }
  /* 9:16 vertical override */
  .embed-modal__inner.vertical .embed-modal__frame-wrap {
    aspect-ratio: 9/16;
    /* v2.9.133 — Reworked sizing so the modal always fits within
       viewport on mobile.

       Previously: max-width: 400px + aspect-ratio 9/16 → height
       computed as width × 16/9. On a 380px-wide phone the frame
       was ~676px tall — taller than typical mobile viewport (~640px
       with browser chrome). The modal's bar (with close button) sits
       BELOW the iframe and ended up offscreen; users couldn't see or
       tap it. There was also no visible backdrop area to tap-out.

       Strategy now: clamp HEIGHT first (leaving room for bar ~70px
       and tap-out margin ~70px = 140px reserved). aspect-ratio then
       derives width from that height, so the iframe always fits in
       viewport while remaining 9:16. svh = small viewport height
       (browser chrome visible) — the most restrictive case.

       Examples:
         viewport 640px svh →  height: min(711, 500) = 500px → width 281
         viewport 800px svh →  height: min(711, 660) = 660px → width 371
         viewport 900px svh →  height: min(711, 760) = 711px → width 400 (capped)
       max-width 400px caps width when viewport is generously tall
       (otherwise an 8K monitor would give a comically wide vertical). */
    height: min(711px, calc(100svh - 140px));
    max-width: 400px;
    width: auto;
    margin: 0 auto;
  }
  .embed-modal__inner.vertical {
    /* Match the inner container's max-width to the frame so the bar
       (close + open buttons) doesn't extend wider than the video.
       Without this the bar inherits the outer 92vw / 98vw width even
       though the iframe is narrower, leaving the close button floating
       far right of the visible content. */
    max-width: 400px;
    margin: 0 auto;
  }
  .embed-modal__frame-wrap iframe,
  .embed-modal__frame-wrap blockquote {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    border: none;
  }
  .embed-modal__bar {
    display: flex; align-items: center; justify-content: space-between;
    padding: 14px 20px;
    gap: 20px;
    border-top: 1px solid rgba(242,237,228,0.08);
  }
  .embed-modal__info { display: flex; flex-direction: column; gap: 3px; }
  .embed-modal__title {
    font-family: var(--display);
    font-size: 16px; font-weight: 400;
    color: var(--cream);
    letter-spacing: var(--track-subtle);
  }
  .embed-modal__sub {
    font-family: var(--mono);
    font-size: 9px; letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd-light);
  }
  .embed-modal__actions { display: flex; gap: 8px; flex-shrink: 0; }
  .embed-modal__close,
  .embed-modal__open {
    font-family: var(--mono);
    font-size: 9px; letter-spacing: var(--track-mid);
    text-transform: uppercase;
    padding: 8px 14px;
    border: 1px solid rgba(242,237,228,0.2);
    background: none;
    color: var(--cream);
    cursor: pointer;
    text-decoration: none;
    display: inline-flex; align-items: center; gap: 6px;
    transition: all 0.2s var(--ease);
    /* 44px touch-target floor — Close is a primary mobile gesture. */
    min-height: 44px;
  }
  .embed-modal__close:hover { border-color: var(--verd-light); color: var(--verd-light); }
  .embed-modal__open { background: var(--verd); border-color: var(--verd); }
  .embed-modal__open:hover { background: var(--verd-deep); border-color: var(--verd-deep); }
  /* Instagram / TikTok embeds need JS to load — show a link fallback */
  .embed-modal__social-fallback {
    position: absolute; inset: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 16px; padding: 32px;
    text-align: center;
  }
  .embed-modal__social-fallback .platform-icon {
    width: 48px; height: 48px;
    fill: var(--cream); opacity: 0.6;
  }
  .embed-modal__social-fallback p {
    font-family: var(--sans); font-size: 14px;
    color: var(--cream); opacity: 0.6;
    max-width: 280px; line-height: 1.5;
  }
  .embed-modal__social-fallback a {
    font-family: var(--mono);
    font-size: 10px; letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    background: var(--verd);
    padding: 10px 20px;
    text-decoration: none;
    display: inline-flex; align-items: center; gap: 8px;
    transition: background 0.2s;
  }
  .embed-modal__social-fallback a:hover { background: var(--verd-deep); }

  /* ── Modal context note (shown for commercial/music video details) ── */
  /* The context note appears below the video bar when a commercial or
     music video is opened. Italic serif matches the single-page detail
     treatment. Scrolls internally if content is long so the modal doesn't
     grow unbounded. */
  .embed-modal__context {
    padding: 20px 24px 24px;
    border-top: 1px solid rgba(242,237,228,0.06);
    font-family: var(--display);
    font-style: italic;
    font-size: 16px;
    line-height: 1.65;
    color: rgba(242,237,228,0.82);
    max-height: 30vh;
    overflow-y: auto;
  }
  .embed-modal__context p { margin: 0 0 12px; }
  .embed-modal__context p:last-child { margin-bottom: 0; }

  /* ── Click-to-play poster (shown in .embed-modal__frame-wrap before iframe loads) ── */
  /* Keeps iframe off the page until user commits, so nothing autoplays
     and no YouTube/Vimeo connection is made when the modal first opens.
     The whole poster is the click target. */
  .embed-modal__poster {
    position: absolute; inset: 0;
    background-size: cover;
    background-position: center;
    background-color: var(--dark);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: filter 0.2s var(--ease);
  }
  .embed-modal__poster::before {
    content: '';
    position: absolute; inset: 0;
    background: linear-gradient(180deg, rgba(0,0,0,0) 40%, rgba(0,0,0,0.45) 100%);
    pointer-events: none;
  }
  .embed-modal__poster:hover { filter: brightness(1.08); }
  .embed-modal__poster-play {
    position: relative;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: rgba(255,255,255,0.92);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 0.2s var(--ease), background 0.2s var(--ease);
    box-shadow: 0 6px 32px rgba(0,0,0,0.4);
  }
  .embed-modal__poster:hover .embed-modal__poster-play {
    transform: scale(1.08);
    background: var(--cream);
  }
  .embed-modal__poster-play::after {
    content: '';
    width: 0; height: 0;
    border-left: 20px solid var(--dark);
    border-top: 12px solid transparent;
    border-bottom: 12px solid transparent;
    margin-left: 5px; /* optical centering */
  }

  @media (max-width: 700px) {
    .embed-modal__inner { width: 98vw; }
    .embed-modal__bar { padding: 12px 16px; }
    .embed-modal__context { padding: 16px 18px 18px; font-size: 15px; }
    .embed-modal__poster-play { width: 56px; height: 56px; }
    .embed-modal__poster-play::after {
      border-left-width: 16px;
      border-top-width: 9px;
      border-bottom-width: 9px;
      margin-left: 4px;
    }
  }

  /* ── GALLERY LIGHTBOX (project gallery tab) ────────────────────────
     Sibling component to .embed-modal — same visual language (dark
     backdrop, blurred, round close button). Adds left/right nav,
     caption + counter bar, keyboard/swipe support (wired in main.js).
     Separate IDs so it can coexist with the video modal on the same
     page without selector conflicts. */

  /* Gallery tile button — clickable wrapper around each thumbnail. */
  .proj-gallery-tile {
    /* Targeted resets that preserve native button click behavior on
       iOS (`all: unset` was breaking tap targets on Safari). */
    -webkit-appearance: none;
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    /* Layout */
    display: block;
    width: 100%;
    cursor: zoom-in;
    overflow: hidden;
    -webkit-tap-highlight-color: rgba(45,106,91,0.25);
    /* Disable double-tap-to-zoom on iOS Safari. Users tapping a gallery
       tile want the lightbox, not a page zoom. Pinch-zoom still works. */
    touch-action: manipulation;
    transition: transform 0.25s var(--ease);
  }
  /* v2.9.131 — Defensive aspect-ratio fallback for tiles whose img
     lacks intrinsic dim attrs. Most photos have width/height from
     ACF, but older imports/missing-metadata cases emit <img> with
     no dims, causing CLS 1.023 on /angel-of-anywhere/photos mobile
     (Lighthouse flagged the Ser'Darius Blain image specifically as
     "Unsized image element"). The :has() selector applies a 3/2
     ratio ONLY when the inner img has neither width nor height
     attributes — sized photos still flow at their real ratio.
     iOS 16.4+ / Chrome 105+ / FF 121+ baseline; supported by every
     browser in current use as of 2026. Older browsers degrade to
     the previous behavior (no reservation, layout shift on load —
     same as if this rule didn't exist), so it's purely additive. */
  .proj-gallery-tile:has(img:not([width]):not([height])) {
    aspect-ratio: 3 / 2;
  }
  /* Skip layout + paint + image decode for gallery figures outside the
     viewport. Critical for projects with BTS merge: a project page with
     200+ gallery images would otherwise pay layout cost for every tile
     on every page load, even though the user starts on the Synopsis
     tab (Gallery is below-the-fold and inactive at first paint).

     History of intrinsic-size values:
     - 1px 300px (original): worked for small galleries but caused the
       footer to paint underneath gallery content on any project with
       40+ photos — the cumulative underestimate added up to a page-
       height shortfall of thousands of pixels, so the <footer> landed
       inside the gallery area instead of after it. See
       https://jameskicklighter.com/staging/project/angel-of-anywhere/
     - REMOVED: keeping content-visibility but with any fixed estimate
       always mis-predicts height by some amount when image aspect
       ratios vary. The <img width/height> attrs on every tile already
       give the browser aspect-ratio info to reserve correct space as
       tiles scroll into view, so the marginal CV perf win (decode
       skipping on pure off-screen images) doesn't justify the layout
       bug risk.

     If we need the perf optimization back for 200+ image projects in
     the future, better to scope CV to a lazy-loaded tab-pane container
     that only activates when the Gallery tab is selected — not
     per-figure. */
  .proj-gallery-tile img {
    width: 100%;
    height: auto;
    display: block;
    transition: opacity 0.2s var(--ease);
  }
  .proj-gallery-tile:hover img { opacity: 0.9; }
  .proj-gallery-tile:focus-visible {
    outline: 2px solid var(--verd);
    outline-offset: 3px;
  }

  /* ─── PROJECT GALLERY: STILLS + BTS LAYOUT ──────────────────────
     v2.9.3 introduced a two-section gallery for projects with both
     curated stills AND behind-the-scenes photos. Stills section
     renders first (curator-controlled order), BTS section below
     (seeded shuffle for variety). Optional filter tabs at the top
     toggle between All / Stills / BTS via data-filter-state on the
     wrapper. */

  /* Wrapper holds both sections plus the filter tabs. The filter
     state is stored as a data attribute so the CSS can toggle
     section visibility with a single attribute selector. */
  .proj-gallery-wrap {
    display: flex;
    flex-direction: column;
    gap: 40px;
  }

  /* Each section (stills or BTS) has its own header rhythm matching
     the .on-location__shuffle-head treatment used on the on-location
     page — keeps the editorial language consistent across the site. */
  .proj-gallery-section {
    display: flex;
    flex-direction: column;
    gap: 20px;
  }
  .proj-gallery-section__head {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 20px 0;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    /* v2.9.8 — was var(--ink) which is dark text. The project page
       has a DARK background (--ink-ish), so dark-on-dark made the
       header invisible. Switch to cream + cream-alpha borders so
       the header reads as a film-can label on dark, matching the
       on-location__shuffle-head pattern. */
    color: var(--cream);
    border-top: 1px solid rgba(242, 237, 228, 0.08);
    border-bottom: 1px solid rgba(242, 237, 228, 0.08);
    overflow: hidden;
    white-space: nowrap;
  }
  .proj-gallery-section__num {
    /* verd-light reads at AA on dark; verd was failing AA. */
    color: var(--verd-light);
    opacity: 0.95;
  }
  .proj-gallery-section__sep {
    opacity: 0.4;
  }
  .proj-gallery-section__label,
  .proj-gallery-section__count {
    opacity: 0.85;
  }

  /* The actual photo grid inside each section. Was inline-styled
     previously; now class-based so filter-state visibility rules
     can target it. auto-fill + minmax means cards adjust column
     count to viewport width without media queries. */
  .proj-gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 16px;
  }

  /* v2.9.114 — Defensive aspect-ratio reservation on every gallery
     tile image. PageSpeed reported /angel-of-anywhere/photos/ at 1.019
     mobile CLS. Root cause: the page-project-photos.php template only
     emits width + height attributes when the photo's metadata exists
     ($img_w && $img_h check). Photos with stripped or missing
     dimensions render with no inline aspect, take 0px until the image
     loads, then jump to actual size. With 100+ photos in the BTS
     gallery, that's a cumulative 1.0+ shift score.

     Default aspect 3:2 (landscape) is the most common BTS shoot
     orientation; portrait/panoramic photos override this from the
     <img>'s width/height when those attributes ARE present. So this
     rule only catches the missing-dimensions edge case — when dims
     are present, the inline aspect wins via natural <img> behavior. */
  .proj-gallery-grid .proj-gallery-tile img {
    aspect-ratio: 3 / 2;
    width: 100%;
    height: auto;
    display: block;
  }
  /* Honor the inline width/height when they're present — the browser
     uses them to compute aspect-ratio automatically and that should
     win over the 3:2 default above. The :is() guard re-enforces
     "use width/height inline attrs" by setting aspect-ratio: auto
     when both attributes exist. */
  .proj-gallery-grid .proj-gallery-tile img[width][height] {
    aspect-ratio: auto;
  }

  /* Filter tabs above the wrapper. Sit in a horizontal pill row
     with a verd-light underline on the active button. Mono caps to
     match the section heads. */
  .proj-gallery-filter {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-bottom: 24px;
    /* v2.9.8 — was rgba(13,10,7,0.08) (dark on dark, invisible).
       Switched to cream-alpha for visibility on the dark project
       page background. */
    border-bottom: 1px solid rgba(242, 237, 228, 0.12);
  }
  .proj-gallery-filter__btn {
    -webkit-appearance: none;
    appearance: none;
    background: none;
    border: 0;
    padding: 12px 18px;
    margin: 0;
    /* v2.9.6 — flex container for the three new children:
       num (01/02), label (Stills/BTS), count (n). Inline-flex
       with gap keeps them on one line without extra space-or-no-
       space inconsistency from inline whitespace. */
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    /* v2.9.8 — was var(--ink), invisible on the dark project page bg. */
    color: var(--cream);
    opacity: 0.55;
    cursor: pointer;
    /* The underline that grows under the active button. */
    border-bottom: 2px solid transparent;
    margin-bottom: -1px; /* tucks the border under the row's bottom rule */
    transition: opacity 0.2s var(--ease), color 0.2s var(--ease), border-color 0.2s var(--ease);
  }
  /* Number prefix on the Stills/BTS tabs — slightly de-emphasized
     so the label is the primary read but the number is still
     legible as the matching identifier with the section header. */
  .proj-gallery-filter__num {
    font-weight: 600;
    /* v2.9.8 — verd-light hits AA on dark; verd was failing. */
    color: var(--verd-light);
    opacity: 0.95;
  }
  .proj-gallery-filter__label {
    /* No specific styling needed — inherits from the button. */
  }
  .proj-gallery-filter__btn:hover {
    opacity: 0.85;
  }
  .proj-gallery-filter__btn.is-active {
    opacity: 1;
    /* v2.9.8 — verd-light for AA on dark. */
    color: var(--verd-light);
    border-bottom-color: var(--verd-light);
  }
  /* When the button's active, the num span inherits the verd color
     so the whole tab reads as a single highlighted unit. */
  .proj-gallery-filter__btn.is-active .proj-gallery-filter__num {
    opacity: 1;
  }
  .proj-gallery-filter__btn:focus-visible {
    outline: 2px solid var(--verd-light);
    outline-offset: 3px;
    border-radius: 2px;
  }
  .proj-gallery-filter__count {
    opacity: 0.6;
    /* margin-left no longer needed — flex gap on the parent button
       handles inter-child spacing. */
    font-weight: 400;
    letter-spacing: 0.12em;
  }

  /* Filter state visibility — driven by data-filter-state on the
     wrapper. JS updates the attribute on tab clicks; CSS handles
     showing/hiding the matching sections. */
  .proj-gallery-wrap[data-filter-state="still"] .proj-gallery-section--bts,
  .proj-gallery-wrap[data-filter-state="bts"] .proj-gallery-section--stills {
    display: none;
  }

  /* When a filter is active, hide the section header for the
     surviving section since it becomes redundant — there's already
     a visible filter tab labeling what the user is looking at. */
  .proj-gallery-wrap[data-filter-state="still"] .proj-gallery-section--stills .proj-gallery-section__head,
  .proj-gallery-wrap[data-filter-state="bts"] .proj-gallery-section--bts .proj-gallery-section__head {
    display: none;
  }

  /* Mobile filter tabs — slightly tighter spacing. */
  @media (max-width: 700px) {
    .proj-gallery-filter__btn {
      padding: 10px 12px;
      font-size: 10px;
      letter-spacing: 0.14em;
    }
    .proj-gallery-section__head {
      flex-wrap: wrap;
      gap: 8px;
      padding: 16px 12px;
      font-size: 9px;
      letter-spacing: var(--track-mid);
      white-space: normal;
    }
    .proj-gallery-grid {
      grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
      gap: 12px;
    }
  }
  .lightbox {
    position: fixed;
    inset: 0;
    /* v2.9.133 — Bumped from 210 → 1200 to clear the fixed nav at
       z-index 1000 and the mobile menu overlay at 1100. Previously
       the lightbox close button (.lightbox__close) and nav arrows
       (.lightbox__nav) were occluded by the navigation bar at
       top:0, making them unclickable — users were trapped inside
       the lightbox on mobile and on desktop the X button sat under
       the nav. The v2.9.121 nav z-index bump (50→1000) didn't
       account for these existing modal overlays; that note in the
       nav comment claimed "lightbox at 210 doesn't matter" which
       was wrong. 1200 is below the skip-link at 10000 (kept
       reachable for keyboard a11y) and the mobile drawer scrims at
       9998/9999, so existing higher-priority surfaces are still
       above it as intended. */
    z-index: 1200;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(13, 10, 7, 0.95);
    backdrop-filter: blur(18px);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s var(--ease);
    /* Disable double-tap-to-zoom. Horizontal swipe gestures still work
       because main.js handles touchstart/touchend directly. Pinch-zoom
       is still allowed for users who need accessibility zoom. */
    touch-action: manipulation;
  }
  .lightbox[hidden] { display: none; }
  .lightbox.open {
    opacity: 1;
    pointer-events: all;
  }

  .lightbox__close {
    position: absolute;
    top: 24px;
    right: 24px;
    z-index: 10;
    width: 48px;
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(13, 10, 7, 0.72);
    border: 1px solid rgba(242, 237, 228, 0.25);
    border-radius: 50%;
    color: var(--cream);
    cursor: pointer;
    transition: background 0.2s var(--ease), border-color 0.2s var(--ease), transform 0.2s var(--ease);
    backdrop-filter: blur(8px);
  }
  .lightbox__close svg { width: 22px; height: 22px; display: block; }
  .lightbox__close:hover {
    background: rgba(242, 237, 228, 0.15);
    border-color: var(--cream);
    transform: scale(1.08);
  }
  .lightbox__close:focus-visible {
    outline: 2px solid var(--verd-light);
    outline-offset: 3px;
  }

  .lightbox__nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;
    width: 56px;
    height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(13, 10, 7, 0.55);
    border: 1px solid rgba(242, 237, 228, 0.2);
    border-radius: 50%;
    color: var(--cream);
    cursor: pointer;
    transition: background 0.2s var(--ease), border-color 0.2s var(--ease), transform 0.2s var(--ease);
    backdrop-filter: blur(8px);
  }
  .lightbox__nav svg { width: 24px; height: 24px; display: block; }
  .lightbox__nav:hover {
    background: rgba(242, 237, 228, 0.15);
    border-color: var(--cream);
    transform: translateY(-50%) scale(1.08);
  }
  .lightbox__nav:focus-visible {
    outline: 2px solid var(--verd-light);
    outline-offset: 3px;
  }
  .lightbox__nav:disabled,
  .lightbox__nav[aria-disabled="true"] {
    opacity: 0.25;
    cursor: not-allowed;
    pointer-events: none;
  }
  .lightbox__nav--prev { left: 24px; }
  .lightbox__nav--next { right: 24px; }

  .lightbox__stage {
    width: min(92vw, 1600px);
    height: min(85vh, 1200px);
    /* v2.9.12 — flex centering for the image. Drops the absolute-
       positioning gymnastics in favor of the simplest working
       pattern: parent is a flex container, image uses max-width
       and max-height to fit within it. object-fit isn't needed
       because the image's intrinsic aspect ratio drives the layout. */
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    box-sizing: border-box;
  }
  .lightbox__img {
    /* v2.9.12 — simple fit-image. The parent's flex centering
       positions the image; max-width/max-height ensure it never
       overflows; width/height:auto preserves intrinsic aspect.
       
       Why not the inset:0 + object-fit:contain pattern? That
       worked on most browsers but failed in this codebase because
       the stage uses padding:16px with box-sizing:border-box —
       inset:0 anchored to the padding box while width:100% measured
       against content box, producing inconsistent sizing for
       portraits where the aspect difference was large. */
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    display: block;
    user-select: none;
    -webkit-user-drag: none;
    opacity: 0;
    /* 180ms fade-in — quick enough that fast navigation feels
       responsive, slow enough to feel intentional rather than a
       flash. */
    transition: opacity 0.18s var(--ease);
    will-change: opacity;
  }
  /* v2.9.9 — single-image lightbox uses .is-loaded as the visibility
     toggle. JS adds it after the new src has finished loading; the
     250ms transition above fades the photo in. Without this, the
     image was hidden until manually shown via .is-active, which
     drifted out of sync with the load lifecycle. */
  .lightbox__img.is-loaded,
  .lightbox__img.is-active {
    opacity: 1;
  }

  /* Loading bar — thin line at the top of the lightbox that appears
     during image loads. Indeterminate animation: a 30%-width segment
     slides left-to-right indefinitely (just like a browser address-
     bar progress indicator). Hides as soon as the image finishes
     loading and the swap fires. JS toggles .is-loading on .lightbox.
     v2.9.7: gives users a visual signal during fast navigation so
     the lightbox never feels like it's hung between frames. */
  .lightbox__progress {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    overflow: hidden;
    pointer-events: none;
    z-index: 12;  /* above .lightbox__header (z-index 11 implicit) */
    opacity: 0;
    transition: opacity 0.15s var(--ease);
  }
  .lightbox.is-loading .lightbox__progress {
    opacity: 1;
  }
  .lightbox__progress::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 30%;
    background: linear-gradient(
      to right,
      transparent 0%,
      var(--verd-light) 50%,
      transparent 100%
    );
    animation: lightbox-progress-slide 1.2s linear infinite;
  }
  @keyframes lightbox-progress-slide {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(400%); }
  }

  /* Top header bar — album/project name on the left, counter on
     the right. Sits ABOVE the photo stage so it never crowds the
     navigation arrows or the close button at the corner.
     Pointer-events: none so the text doesn't block clicks; the
     close button (also top-right) gets a higher z-index. */
  .lightbox__header {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    padding: 22px 80px 18px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    background: linear-gradient(to bottom, rgba(13, 10, 7, 0.85), rgba(13, 10, 7, 0.0));
    pointer-events: none;
    z-index: 2;
  }
  .lightbox__album {
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    line-height: 1.4;
    /* Keep room on the right for the close button. */
    max-width: calc(100% - 100px);
    /* v2.9.10 — soft drop shadow at 10% opacity. The header gradient
       fades to transparent at the bottom so the text can sit on raw
       photo at the edges; this shadow keeps it legible without
       darkening the visual. */
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.10);
  }
  .lightbox__counter {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    color: var(--cream);
    opacity: 0.65;
    flex-shrink: 0;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.10);
  }

  /* Bottom caption — sits below the photo with enough vertical gap
     above the nav arrows that the text never reads as part of the
     button row. v2.9.3 pivot: previous mono-caps treatment was hard
     to read at length (per James's feedback). Switched to display
     serif (matches the rest of the site's body voice) at a larger
     size with normal sentence case so multi-line captions read like
     real captions, not like signage. */
  .lightbox__caption {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    /* Padding bottom is bumped to clear the nav arrows (which sit
       at bottom: 24px and are 56px tall = needs 80px+ clearance to
       guarantee captions are never visually attached to the
       arrows). 100px gives breathing room. */
    padding: 24px 96px 100px;
    background: linear-gradient(
      to top,
      rgba(13, 10, 7, 0.92) 0%,
      rgba(13, 10, 7, 0.7) 60%,
      rgba(13, 10, 7, 0.0) 100%
    );
    font-family: var(--display);
    /* v2.9.12 — bumped 16px→20px on desktop. Previous size read as
       too quiet against the cinematic photo; 20px is "magazine page
       caption" weight — present without competing with the image. */
    font-size: 20px;
    font-weight: 300;
    letter-spacing: 0;
    text-transform: none;
    color: var(--cream);
    opacity: 0.95;
    line-height: 1.5;
    text-align: center;
    pointer-events: none;
    /* Limit width so very long captions don't stretch edge to edge. */
    max-width: 760px;
    margin: 0 auto;
    /* v2.9.10 — soft drop shadow at 10% opacity. The caption gradient
       fades to transparent at the top, so longer captions extend
       upward into clearer photo. The shadow keeps text legible there
       without darkening the photo. */
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.10);
  }
  .lightbox__caption:empty {
    display: none;
  }
  .lightbox__caption em {
    /* Photo credit — small mono caps as a tertiary line, sets
       it apart from the caption text without competing. */
    display: block;
    margin-top: 10px;
    font-family: var(--mono);
    font-style: normal;
    font-size: 10px;
    font-weight: 500;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    opacity: 0.85;
  }

  /* ─── Sidebar mode for landscape photos on wide viewports ───────
     Toggled by main.js when (a) viewport ≥ 1100px AND (b) the
     current photo is landscape with ≥ 1.3:1 ratio. The standard
     caption-below-photo layout cramps long captions against the
     bottom nav arrows; for landscape photos there's horizontal
     room to give the caption its own sidebar instead. Portrait
     photos already use the page's vertical real estate fully so
     they keep the centered layout (no horizontal room next to a
     portrait without shrinking the photo significantly).
     v2.9.14 — feature added. */
  .lightbox.lightbox--sidebar-mode {
    /* Drop the centering — children will arrange themselves. */
    align-items: stretch;
    justify-content: stretch;
  }
  .lightbox.lightbox--sidebar-mode .lightbox__stage {
    /* Photo takes the left ~62% of the lightbox. Width is generous
       so a 16:9 landscape gets a properly cinematic display. */
    width: 62%;
    height: calc(100vh - 80px);
    height: calc(100svh - 80px);
    margin-left: 0;
    /* 80px top accounts for the header bar + a comfortable breathing
       gap above the photo. */
    margin-top: 80px;
    padding: 0 32px;
  }
  .lightbox.lightbox--sidebar-mode .lightbox__caption {
    /* Caption becomes a right sidebar with proper text width and
       generous vertical padding. No more cramming at the bottom. */
    position: absolute;
    top: 80px;
    right: 0;
    bottom: 0;
    left: 62%;
    width: 38%;
    max-width: none;
    /* v2.9.15 — right padding bumped 56px → 96px to clear the
       right-edge nav arrow (next button at right:24px is 56px wide,
       so its left edge is at right:80px from the lightbox's right
       edge). 96px caption padding leaves a 16px buffer between
       caption text and the arrow's hover hit-area. */
    padding: 0 96px 56px 40px;
    background: none;
    text-align: left;
    margin: 0;
    /* Sidebar captions get more comfortable max width — readable
       column rather than centered banner text. */
    font-size: 18px;
    line-height: var(--lh-body);
    /* Vertical centering of the caption text within the sidebar so
       short captions don't float at the top of an empty column. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* Subtle left rule separates the sidebar from the photo column.
       Reads as "this is its own zone" rather than empty space. */
    border-left: 1px solid rgba(242, 237, 228, 0.08);
  }
  .lightbox.lightbox--sidebar-mode .lightbox__caption em {
    /* Credit line still sits below the caption text but with
       extra spacing in the sidebar context. */
    margin-top: 14px;
  }
  /* Nav arrows in sidebar mode — left arrow stays anchored to the
     lightbox left edge; right arrow moved (v2.9.15) to the lightbox
     right edge so users can navigate from the caption-side without
     dragging their cursor back across the photo column. */
  .lightbox.lightbox--sidebar-mode .lightbox__nav--prev {
    left: 24px;
  }
  .lightbox.lightbox--sidebar-mode .lightbox__nav--next {
    /* v2.9.15 — was left: calc(62% - 80px). Now sits at the right
       edge of the lightbox (in the caption sidebar's right margin)
       for symmetry with the close button and easier navigation. */
    right: 24px;
    left: auto;
  }
  /* Header album/counter still spans the whole top — fine in sidebar
     mode since it's purely informational metadata. */

  /* When the viewport drops below 1100px, JS removes
     .lightbox--sidebar-mode and the standard centered layout takes
     over. The mobile breakpoint below offers a different, mobile-
     specific layout pattern. */

  /* ─── SHEET WRAPPER (mobile swipe-up caption) ─────────────────
     On desktop the sheet wrapper is structural-only — the inner
     caption is positioned absolutely (sidebar mode) or rendered as
     a transparent contents passthrough (centered fallback mode).
     Mobile transforms this into a slide-up bottom sheet via the
     @media (max-width: 700px) block below. */
  .lightbox__sheet {
    /* Default: transparent passthrough. The wrapper exists in DOM
       but doesn't paint anything itself. The inner caption renders
       via its own positioning (absolute in sidebar mode, or static
       in the centered fallback). */
    display: contents;
  }
  .lightbox__sheet-peek {
    /* Hidden on desktop — there's no need for a tap-to-reveal
       affordance when the caption is already visible in a sidebar
       or below the photo. Mobile re-enables this as a bottom
       peek tab. */
    display: none;
  }
  .lightbox__sheet-body {
    /* Desktop: structural-only — the inner caption is the visible
       element. */
    display: contents;
  }
  @media (max-width: 700px) {
    .lightbox__close {
      top: 14px;
      right: 14px;
      width: 40px;
      height: 40px;
    }
    .lightbox__close svg { width: 18px; height: 18px; }
    /* v2.9.16 — arrows now anchored to the photo (stage) instead
       of pinned to the lightbox bottom. Previously the arrows sat
       at bottom: 80px which placed them inside the open caption
       sheet, blocking text. Centering them on the stage means
       they live at the photo's vertical midpoint, always to the
       sides of the image and never overlapping the sheet area
       below. The .lightbox__stage is position:absolute and
       vertically centered on the viewport, so its 50%-Y line is
       roughly the photo center. */
    .lightbox__nav {
      width: 44px;
      height: 44px;
      top: 50%;
      bottom: auto;
      transform: translateY(-50%);
      /* Slight backdrop so arrows stay legible even on light
         photos. The base styles already give the arrow a
         semi-transparent black bg with backdrop-blur. */
    }
    .lightbox__nav svg { width: 20px; height: 20px; }
    .lightbox__nav:hover { transform: translateY(-50%) scale(1.08); }
    .lightbox__nav--prev { left: 8px; }
    .lightbox__nav--next { right: 8px; }
    /* When the sheet is open, hide the arrows entirely. The user
       is reading the caption — hiding the nav reduces visual
       clutter and stops accidental nav taps while the sheet is up.
       Arrows reappear when the sheet closes. */
    .lightbox__sheet[data-sheet-state="open"] ~ .lightbox__stage .lightbox__nav,
    .lightbox.has-open-sheet .lightbox__nav {
      opacity: 0;
      pointer-events: none;
    }
    .lightbox__stage {
      /* v2.9.15 — mobile stage now uses nearly the full viewport
         since the caption is hidden in a slide-up sheet. Reserves
         only the header (~60px) + peek tab (56px) + nav arrow row
         (44 + 24 buffer = ~68px above the peek tab) = 184px chrome.
         Photo gets the rest. Significantly larger than the previous
         100vh - 216px window. */
      position: absolute;
      top: 50%;
      left: 0;
      transform: translateY(-50%);
      width: 100vw;
      height: calc(100vh - 184px - env(safe-area-inset-bottom, 0px));
      height: calc(100svh - 184px - env(safe-area-inset-bottom, 0px));
      padding: 8px;
      margin-top: 0;
    }
    .lightbox__header {
      padding: 14px 64px 10px;
      background: linear-gradient(to bottom, rgba(13, 10, 7, 0.92), rgba(13, 10, 7, 0.4) 70%, rgba(13, 10, 7, 0.0));
    }
    .lightbox__album {
      font-size: 9px;
      letter-spacing: var(--track-mid);
      max-width: calc(100% - 60px);
    }
    .lightbox__counter {
      font-size: 9px;
      letter-spacing: var(--track-mid);
    }

    /* ─── Mobile swipe-up sheet ─────────────────────────────────
       The sheet wrapper transforms into a bottom-anchored slide-up
       panel. Closed state: only the peek tab is visible. Open state:
       the sheet body slides up from the bottom covering ~62% of the
       viewport with the caption text. */
    .lightbox__sheet {
      /* Override the desktop display:contents so the sheet now
         participates in layout as a positioned container. */
      display: block;
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 5;
      pointer-events: none; /* peek tab + body re-enable individually */
    }
    .lightbox__sheet-peek {
      /* Re-enable on mobile (was display:none on desktop). The peek
         tab is the always-visible affordance: a 56px-tall bar near
         the bottom of the viewport showing "Caption ↑" so users
         discover there's text to read.

         v2.9.16 — margin-bottom lifts the peek tab off the absolute
         bottom edge so it doesn't feel jammed against the home
         indicator. The sheet body itself stays at bottom:0 so it
         extends to the viewport edge when open; only the closed-
         state peek tab is lifted. */
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 10px;
      width: 100%;
      height: 56px;
      margin-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
      padding: 0 24px;
      background: linear-gradient(to top, rgba(13, 10, 7, 0.95), rgba(13, 10, 7, 0.7) 60%, rgba(13, 10, 7, 0.0));
      border: none;
      color: var(--cream);
      cursor: pointer;
      font: inherit;
      pointer-events: auto;
      position: relative;
      transition: opacity 0.25s var(--ease);
      touch-action: manipulation;
    }
    .lightbox__sheet-peek-handle {
      /* Tiny horizontal pill above the label — the universal "this
         is a draggable sheet" affordance from iOS / Material. Sits
         centered above the label text. */
      position: absolute;
      top: 6px;
      left: 50%;
      transform: translateX(-50%);
      width: 36px;
      height: 3px;
      background: rgba(242, 237, 228, 0.4);
      border-radius: 2px;
    }
    .lightbox__sheet-peek-label {
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: var(--track-wide);
      text-transform: uppercase;
      color: var(--cream);
      opacity: 0.85;
    }
    .lightbox__sheet-peek-chev {
      width: 10px;
      height: 7px;
      color: var(--verd-light);
      transition: transform 0.3s var(--ease);
    }
    .lightbox__sheet[data-sheet-state="open"] .lightbox__sheet-peek-chev {
      /* Chevron flips when the sheet is open so the affordance
         signals "tap to close" rather than "tap to open." */
      transform: rotate(180deg);
    }
    .lightbox__sheet[data-sheet-state="open"] .lightbox__sheet-peek {
      /* Hide the peek when sheet is open — the open sheet is its
         own dismiss target. The peek staying visible would create
         a confusing duplicate "Caption" label. */
      opacity: 0;
      pointer-events: none;
    }

    .lightbox__sheet-body {
      /* Sheet body: the actual caption content panel. Closed state
         is fully translated below the viewport; open state slides
         up to reveal the caption. */
      display: block;
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      max-height: 62vh;
      max-height: 62svh;
      overflow-y: auto;
      background: rgba(13, 10, 7, 0.98);
      backdrop-filter: blur(20px);
      -webkit-backdrop-filter: blur(20px);
      transform: translateY(100%);
      transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
      pointer-events: none;
      padding-top: 24px;
      padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px));
      border-top-left-radius: 16px;
      border-top-right-radius: 16px;
      box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.5);
    }
    .lightbox__sheet[data-sheet-state="open"] .lightbox__sheet-body {
      transform: translateY(0);
      pointer-events: auto;
    }
    /* Drag handle inside the sheet body — same pill as the peek
       tab, but now serving as the "drag down to dismiss" affordance
       since the peek is hidden when sheet is open. */
    .lightbox__sheet-body::before {
      content: '';
      display: block;
      position: absolute;
      top: 8px;
      left: 50%;
      transform: translateX(-50%);
      width: 40px;
      height: 4px;
      background: rgba(242, 237, 228, 0.4);
      border-radius: 2px;
    }
    /* Caption inside the sheet body — the existing .lightbox__caption
       styles continue to apply for fonts and spacing, but the
       wrapper handles positioning. Override the wide-screen padding
       since the sheet has its own. */
    .lightbox__sheet-body .lightbox__caption {
      position: relative;
      padding: 12px 24px 0;
      font-family: var(--display);
      font-size: 17px;
      font-weight: 300;
      letter-spacing: 0;
      text-transform: none;
      line-height: 1.5;
      max-width: 100%;
      text-align: left;
      pointer-events: auto;
    }
    .lightbox__sheet-body .lightbox__caption em {
      display: block;
      margin-top: 14px;
      font-family: var(--mono);
      font-style: normal;
      font-size: 10px;
      font-weight: 500;
      letter-spacing: var(--track-wide);
      text-transform: uppercase;
      color: var(--verd-light);
      opacity: 0.85;
    }
    /* Empty caption: hide the sheet entirely on mobile when there's
       nothing to show. */
    .lightbox__sheet-body .lightbox__caption:empty {
      display: none;
    }
    .lightbox__sheet:has(.lightbox__caption:empty) .lightbox__sheet-peek {
      display: none;
    }
    /* v2.9.15 — no-caption class fallback for browsers without :has()
       support (and for cases where caption is whitespace-only rather
       than truly :empty). renderChrome() in main.js sets this. */
    .lightbox__sheet.lightbox__sheet--no-caption .lightbox__sheet-peek {
      display: none;
    }
    .lightbox__sheet.lightbox__sheet--no-caption .lightbox__sheet-body {
      /* Also hide the body so a swipe-up from the bottom edge can't
         accidentally reveal an empty sheet. */
      display: none;
    }
    /* Backdrop scrim when sheet is open — slightly darkens the photo
       behind so the caption reads more clearly. The scrim uses
       pointer-events:auto so tapping outside the sheet dismisses it. */
    .lightbox__sheet[data-sheet-state="open"]::before {
      content: '';
      position: fixed;
      inset: 0;
      background: rgba(13, 10, 7, 0.4);
      z-index: -1;
      pointer-events: auto;
      animation: lightboxSheetScrim 0.3s ease forwards;
    }
    @keyframes lightboxSheetScrim {
      from { opacity: 0; }
      to   { opacity: 1; }
    }

    /* Reset the old mobile caption styling — the caption is now
       inside the sheet body, not floating at the bottom of the
       lightbox. The desktop fallback caption block (when sidebar
       mode is off but viewport > 700px) still uses the legacy
       positioning. */
    .lightbox__caption {
      /* Reset positioning that the old "absolute bottom strip"
         pattern depended on. The sheet body wraps and positions
         the caption now. */
      position: static;
      padding: 0;
      background: none;
      max-width: 100%;
    }
  }

  /* ── PRESS PUBLICATION LOGOS ── */
  .press-publications {
    /* v2.9.47.3 — bottom margin dropped (was 32px) since the hero CTA
       button that used to sit below the logos has been removed. The
       hero's own padding-bottom now handles the section close, so an
       extra 32px here only created dead space between the logos and
       the next section. */
    margin: 40px 0 0;
  }
  .press-publications__label {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.4;
    margin-bottom: 20px;
  }

  /* social row in full-page context (press page) — no escape needed */
  .vid-social-row--page {
    margin-left: 0;
    margin-right: 0;
    width: 100%;
    padding: 0 0 20px;
  }

  /* ══════════════════════════════════════════════════════════════════
     JOURNAL — /journal/
     ──────────────────────────────────────────────────────────────────
     A dated notebook. Posts are grouped by year; each entry sits in a
     two-column row (mono date stamp on the left, title + excerpt on the
     right). The whole thing reads like a chronological ledger rather
     than a card grid — distinct from press (logo + quote cards) and
     library (genre-grouped film lists).

     DOM shape:
       section.journal-hero                  — page statement
       section.journal-archive
         .journal-archive__inner
           .journal-toolbar                  — filter chips + search + count
           .journal-archive__list
             .journal-year-group[data-year]
               .journal-year                 — large display serif, e.g. "2024"
               article.journal-entry
                 .journal-entry__date        — Mon / Day stacked
                 .journal-entry__body
                   .journal-entry__cat
                   h3.journal-entry__title
                   p.journal-entry__excerpt
  ══════════════════════════════════════════════════════════════════ */

  /* ── HERO ── */
  .journal-hero {
    padding: 100px 32px 48px;
    max-width: 1500px;
    margin: 0 auto;
    border-bottom: 1px solid var(--rule);
  }
  .journal-hero__inner {
    max-width: 1180px;
    margin: 0 auto;
  }
  .journal-hero__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 20px;
  }
  .journal-hero__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-hero-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
    margin: 0 0 28px;
  }
  .journal-hero__title em { font-weight: 400; color: var(--verd); font-style: italic; }
  .journal-hero__lede {
    font-family: var(--display);
    font-size: var(--h-lede);
    line-height: var(--lh-body);
    font-weight: 300;
    color: var(--ink);
    opacity: 0.85;
    max-width: 640px;
    margin: 0;
  }

  /* ── ARCHIVE ── */
  .journal-archive {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
  }
  .journal-archive__inner {
    max-width: 1180px;
    margin: 0 auto;
  }

  /* Toolbar — filter chips on the left, search + count on the right. */
  .journal-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 24px;
    margin-bottom: 56px;
    padding-bottom: 24px;
    border-bottom: 1px solid var(--rule);
  }
  .journal-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    flex: 1;
    min-width: 0;
  }
  .journal-filters .fchip {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    padding: 8px 14px;
    border: 1px solid var(--rule);
    text-decoration: none;
    color: var(--ink);
    background: transparent;
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
    /* 44px touch-target floor — see proj-picker-btn for rationale. */
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .journal-filters .fchip:hover {
    border-color: var(--verd);
    color: var(--verd);
  }
  .journal-filters .fchip.active {
    background: var(--ink);
    color: var(--cream);
    border-color: var(--ink);
  }
  .journal-filters .fchip--tag {
    color: var(--verd);
    opacity: 0.8;
  }
  .journal-filters .fchip__count {
    opacity: 0.5;
    margin-left: 4px;
  }

  /* Right side of toolbar — post count only (search removed sitewide). */
  .journal-toolbar__right {
    display: flex;
    align-items: center;
    gap: 16px;
  }
  .journal-count {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    opacity: 0.7;
    white-space: nowrap;
  }

  /* Load More — paginated reveal. Mirrors fchip styling (mono, bordered,
     hover-verd) so it feels native to the filter row above it. */
  .journal-loadmore-wrap {
    display: flex;
    justify-content: center;
    margin-top: 48px;
  }
  .journal-loadmore-wrap[hidden] { display: none; }
  .journal-loadmore {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 14px 28px;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    background: transparent;
    border: 1px solid var(--ink);
    border-radius: 0;
    cursor: pointer;
    transition: color 0.2s, background 0.2s, border-color 0.2s;
  }
  .journal-loadmore:hover,
  .journal-loadmore:focus-visible {
    background: var(--ink);
    color: var(--cream);
    outline: none;
  }
  .journal-loadmore__count {
    opacity: 0.5;
  }
  .journal-loadmore:hover .journal-loadmore__count,
  .journal-loadmore:focus-visible .journal-loadmore__count {
    opacity: 0.7;
  }

  /* ── LIST ── */
  .journal-archive__list {
    display: flex;
    flex-direction: column;
    gap: 0;
  }

  /* Hide filtered-out entries. Also hide year groups whose entries are all filtered. */
  .journal-entry.hidden { display: none; }
  .journal-year-group.hidden { display: none; }

  /* Year group wraps a year header and all entries in that year. */
  .journal-year-group {
    margin-bottom: 56px;
  }
  .journal-year-group:last-child {
    margin-bottom: 0;
  }
  .journal-year {
    font-family: var(--display);
    font-weight: 300;
    font-size: clamp(64px, 10vw, 120px);
    line-height: 1;
    letter-spacing: var(--track-tight);
    color: var(--ink);
    opacity: 0.08;
    margin: 0 0 -0.15em -0.04em;
    pointer-events: none;
    user-select: none;
  }

  /* ── ENTRY — single post row ── */
  .journal-entry {
    display: grid;
    grid-template-columns: 100px 1fr;
    gap: 32px;
    padding: 28px 0;
    border-top: 1px solid var(--rule);
    align-items: baseline;
    /* Defer offscreen entries. Journal is paginated (10/page with
       Load More), but after repeated clicks the scroll can grow long.
       Deferring below-viewport entries keeps scroll smooth regardless
       of how far the user paginates. */
    content-visibility: auto;
    contain-intrinsic-size: 1px 180px;
  }
  .journal-entry:last-child {
    border-bottom: 1px solid var(--rule);
  }

  /* Date stamp — stacked Mon / Day, mono uppercase, verd. */
  .journal-entry__date time {
    display: block;
    font-family: var(--mono);
    text-transform: uppercase;
    color: var(--verd);
    line-height: 1;
  }
  .journal-entry__month {
    display: block;
    font-size: 10px;
    letter-spacing: var(--track-wide);
    margin-bottom: 6px;
  }
  .journal-entry__day {
    display: block;
    font-family: var(--display);
    font-size: 36px;
    font-weight: 300;
    letter-spacing: var(--track-tight);
    color: var(--ink);
  }

  /* Entry body — category lozenge, title, excerpt. */
  .journal-entry__body {
    min-width: 0;
  }
  .journal-entry__cat {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    opacity: 0.75;
    margin-bottom: 10px;
  }
  .journal-entry__title {
    font-family: var(--display);
    font-size: var(--h-sub-title);
    font-weight: 400;
    line-height: 1.15;
    letter-spacing: var(--track-card);
    margin: 0 0 10px;
  }
  .journal-entry__title a {
    color: var(--ink);
    text-decoration: none;
    transition: color 0.2s;
    background-image: linear-gradient(var(--verd), var(--verd));
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 0 1px;
    transition: background-size 0.3s cubic-bezier(0.65, 0, 0.35, 1), color 0.2s;
    padding-bottom: 2px;
  }
  .journal-entry__title a:hover {
    color: var(--verd);
    background-size: 100% 1px;
  }
  .journal-entry__excerpt {
    font-family: var(--sans);
    font-size: 15px;
    line-height: var(--lh-prose);
    color: var(--ink);
    opacity: 0.7;
    margin: 0;
    max-width: 640px;
  }

  /* Empty state — message shown when all entries are filtered out. */
  .journal-empty {
    padding: 80px 0;
    text-align: center;
  }
  .journal-empty__title {
    font-family: var(--display);
    font-style: italic;
    font-size: 22px;
    color: var(--ink);
    opacity: 0.5;
    margin: 0 0 8px;
  }
  .journal-empty__hint {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.4;
  }
  .journal-empty__hint a { color: var(--verd); }

  /* ≤ 900px — journal mobile */
  @media (max-width: 1024px) {
    .journal-hero { padding: 96px 20px 32px; }
    .journal-archive { padding: 32px 20px; }

    .journal-toolbar {
      flex-direction: column;
      gap: 16px;
      margin-bottom: 40px;
    }
    .journal-filters {
      flex-wrap: nowrap;
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
      max-width: 100%;
      min-width: 0;
      width: 100%;
      padding-bottom: 4px;
    }
    .journal-filters::-webkit-scrollbar { display: none; }
    .journal-filters .fchip {
      flex-shrink: 0;
      font-size: 10px;
      padding: 7px 12px;
      white-space: nowrap;
    }
    .journal-toolbar__right {
      width: 100%;
      justify-content: flex-start;
    }

    .journal-year-group { margin-bottom: 40px; }
    .journal-year { margin-bottom: -0.1em; }

    .journal-entry {
      grid-template-columns: 72px 1fr;
      gap: 20px;
      padding: 22px 0;
    }
    .journal-entry__day { font-size: 28px; }
    .journal-entry__title { font-size: 20px; }
    .journal-entry__excerpt { font-size: 14px; }
  }

  /* ══════════════════════════════════════════════════════════════════
     FAQ PAGE — /faq/
     ──────────────────────────────────────────────────────────────────
     Hero matches the site's current pattern (journal/courses/press):
     single column, eyebrow + title + lede. Long-form answer prose uses
     the site's canonical long-form voice — display serif at 300 weight,
     clamp(18px, 1.5vw, 22px), 1.6 line-height — matching .bio-deep__body,
     .syn-body, and .journal-post__body so the FAQ answers read as part
     of the same editorial voice as the rest of the site.
  ══════════════════════════════════════════════════════════════════ */

  .faq-hero {
    padding: 100px 32px 48px;
    max-width: 1500px;
    margin: 0 auto;
    border-bottom: 1px solid var(--rule);
  }
  .faq-hero__inner {
    max-width: 1180px;
    margin: 0 auto;
  }
  .faq-hero__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 20px;
  }
  .faq-hero__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-hero-title);
    line-height: var(--lh-display);
    letter-spacing: var(--track-tight);
    color: var(--ink);
    margin: 0 0 28px;
  }
  .faq-hero__title em { font-weight: 400; color: var(--verd); font-style: italic; }
  .faq-hero__lede {
    font-family: var(--display);
    font-size: var(--h-lede);
    line-height: var(--lh-body);
    font-weight: 300;
    color: var(--ink);
    opacity: 0.85;
    max-width: 640px;
    margin: 0;
  }

  .faq-page {
    padding: 64px 32px 80px;
    max-width: 1500px;
    margin: 0 auto;
  }
  .faq-page__inner {
    max-width: 860px;
    margin: 0 auto;
  }
  .faq-page__empty {
    font-family: var(--sans);
    font-size: 16px;
    line-height: 1.7;
    color: var(--ink-mid);
  }

  .faq-list {
    display: flex;
    flex-direction: column;
    gap: 0;
    border-top: 1px solid var(--rule);
  }
  .faq-item {
    border-bottom: 1px solid var(--rule);
    padding: 0;
  }
  .faq-item__q {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 24px;
    align-items: center;
    padding: 28px 0;
    cursor: pointer;
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(20px, 2.2vw, 26px);
    line-height: 1.3;
    letter-spacing: var(--track-subtle);
    color: var(--ink);
    list-style: none;
    transition: color 0.2s;
  }
  .faq-item__q::-webkit-details-marker { display: none; }
  .faq-item__q:hover { color: var(--verd); }
  .faq-item__q-icon {
    font-family: var(--mono);
    font-size: 20px;
    color: var(--verd);
    transition: transform 0.25s ease-out;
    line-height: 1;
  }
  .faq-item[open] .faq-item__q-icon { transform: rotate(45deg); }
  .faq-item__a {
    padding: 0 0 32px 0;
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-lede);
    line-height: var(--lh-prose);
    color: var(--ink);
    opacity: 0.85;
    max-width: 680px;
  }
  .faq-item__a p { margin: 0 0 20px; }
  .faq-item__a p:last-child { margin-bottom: 0; }
  .faq-item__a a {
    color: var(--verd);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
    transition: color 0.2s;
  }
  .faq-item__a a:hover { color: var(--verd-deep); }

  .faq-footer {
    margin-top: 64px;
    padding-top: 32px;
    border-top: 1px solid var(--rule);
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.6;
    max-width: 640px;
  }
  .faq-footer__link {
    color: var(--verd);
    text-decoration: none;
    margin-left: 8px;
    border-bottom: 1px solid var(--verd);
    padding-bottom: 2px;
    transition: color 0.15s, border-color 0.15s;
  }
  .faq-footer__link:hover {
    color: var(--ink);
    border-color: var(--ink);
  }

  /* Mobile FAQ — tighter hero + tighter accordion rows */
  @media (max-width: 1024px) {
    .faq-hero { padding: 96px 20px 32px; }
    .faq-page { padding: 32px 20px 48px; }
    .faq-item__q {
      padding: 22px 0;
      font-size: 18px;
      gap: 16px;
    }
    .faq-item__q-icon { font-size: 18px; }
    .faq-item__a {
      padding: 0 0 24px 0;
      font-size: 15px;
    }
  }

  /* ════════ CONTACT PAGE ════════ */
  .contact-grid { }
  @media (max-width: 800px) { .contact-grid { grid-template-columns: 1fr !important; gap: 40px !important; } }
  .contact-method {
    padding: 20px 0;
    border-bottom: 1px solid var(--rule);
  }
  .contact-method:first-child { border-top: 1px solid var(--rule); }
  .contact-method__label {
    font-family: var(--mono); font-size: 9px; letter-spacing: var(--track-wide);
    text-transform: uppercase; color: var(--verd); margin-bottom: 6px;
  }
  .contact-method__value { font-family: var(--display); font-size: 18px; font-weight: 400; color: var(--ink); }
  .contact-link {
    color: var(--ink); text-decoration: none;
    transition: color 0.2s var(--ease);
  }
  .contact-link:hover { color: var(--verd); }

  /* ── .fchip on dark surface (project panel press section) ──
     Matches the press-kit button treatment: cream border/text on transparent,
     hovering to cream fill with ink text, active state stays green-filled.
     Gives the chips a clear "button" affordance on dark backgrounds where the
     original low-opacity treatment read as passive labels. */
  .proj-panel .fchip {
    color: var(--cream);
    border: 1px solid var(--cream);
    background: transparent;
    opacity: 1;
    padding: 9px 16px;
    font-weight: 400;
  }
  .proj-panel .fchip:hover {
    opacity: 1;
    background: var(--cream);
    border-color: var(--cream);
    color: var(--ink);
  }
  .proj-panel .fchip.active {
    background: var(--verd);
    border-color: var(--verd);
    color: var(--cream);
    opacity: 1;
  }
  .proj-panel .fchip.active:hover {
    background: var(--verd-light);
    border-color: var(--verd-light);
    color: var(--ink);
  }
  /* Count badge inside chips on dark project panels. The global rule
     at the bottom of main.css sets .fchip__count to color:--ink (dark)
     which is correct on cream/light backgrounds but invisible against
     the dark proj-panel background — the dark-on-dark blend was the
     "wrong color, blends with background" bug James reported. Inverting
     to cream with the same 0.72 opacity dampener preserves the secondary-
     label hierarchy (count is dimmer than the chip's main label). The
     hover state needs ink because the chip background flips to cream.
     Active state already handles itself via .fchip.active .fchip__count
     which sets cream + opacity 1 — that wasn't broken. */
  .proj-panel .fchip__count {
    color: var(--cream);
    opacity: 0.72;
  }
  .proj-panel .fchip:hover .fchip__count {
    color: var(--ink);
    opacity: 0.72;
  }

  /* ── PROJECT PICKER (prototype only) ── */
  .proj-picker {
    background: var(--dark-card);
    border-bottom: 1px solid rgba(242,237,228,0.08);
    padding: 10px 32px;
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    overflow-x: auto;
  }
  
  .proj-picker-btn {
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-narrow);
    text-transform: uppercase;
    padding: 5px 12px;
    border: 1px solid rgba(242,237,228,0.15);
    background: transparent;
    color: var(--cream);
    opacity: 0.5;
    cursor: pointer;
    transition: all 0.2s;
    white-space: nowrap;
    flex-shrink: 0;
    /* Touch-target floor — stays out of the way on desktop (inline chip)
       but guarantees the 44px HIG tap-height minimum on any device where
       padding alone isn't getting us there. Mobile typography bump also
       handled in the @media block below. */
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .proj-picker-btn:hover { opacity: 0.85; border-color: rgba(242,237,228,0.3); }
  .proj-picker-btn.active {
    opacity: 1;
    background: rgba(109,191,173,0.15);
    border-color: var(--verd-light);
    color: var(--verd-light);
  }

  /* ════════ CRITICAL RECEPTION PANEL ════════ */
  /* ── SCORE BAR — sized to content, not full-width ── */
  .reception-scores {
    display: flex;
    flex-direction: column;
    align-items: flex-start; /* each badge sizes to its own content */
    gap: 8px;
  }
  .score-badge {
    display: inline-flex;
    align-items: center;
    gap: 16px;
    padding: 16px 24px 16px 20px;
    border-left: 3px solid rgba(242,237,228,0.15);
    background: rgba(242,237,228,0.03);
    text-decoration: none;
    transition: background 0.2s var(--ease);
    /* Size to content width (title + meta) rather than stretching to
       the full panel. Capped at 520px so longer review counts don't
       make the bar awkwardly wide. Falls back to 100% on narrow
       screens so it never overflows the panel. */
    max-width: min(520px, 100%);
    min-width: 0;
  }
  .score-badge:hover { background: rgba(242,237,228,0.07); }
  .score-badge--fresh  { border-left-color: rgba(104,211,66,0.5); }
  .score-badge--certified { border-left-color: #7BE05A; }
  .score-badge__icon {
    font-size: clamp(24px, 4vw, 32px);
    line-height: 1;
    flex-shrink: 0;
  }
  .score-badge__num {
    font-family: var(--display);
    font-size: clamp(36px, 6vw, 52px);
    font-weight: 300;
    letter-spacing: -0.04em;
    line-height: 1;
    color: var(--cream);
    flex-shrink: 0;
  }
  .score-badge--fresh .score-badge__num,
  .score-badge--certified .score-badge__num { color: #7BE05A; }
  .score-badge__info {
    display: flex;
    flex-direction: column;
    gap: 4px;
    flex: 1;
    min-width: 0;
  }
  .score-badge__label {
    font-family: var(--display);
    font-size: clamp(15px, 2vw, 20px);
    font-weight: 300;
    color: var(--cream);
    letter-spacing: var(--track-subtle);
  }
  .score-badge__label strong { color: #7BE05A; font-weight: 500; }
  .score-badge__meta {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.35;
  }
  

  /* ── AUDIENCE REVIEWS ── */
  .audience-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 16px;
  }
  /* When the grid follows the RT badge inside the combined Response
     section, add breathing space between them. */
  .reception-scores + .audience-grid {
    margin-top: 24px;
  }
  @media (max-width: 600px) { .audience-grid { grid-template-columns: 1fr; } }
  .audience-card {
    background: rgba(242,237,228,0.03);
    border: 1px solid rgba(242,237,228,0.08);
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    transition: border-color 0.2s var(--ease);
  }
  .audience-card:hover { border-color: rgba(242,237,228,0.16); }
  .audience-card__stars {
    color: #F5C842;
    font-size: 14px;
    letter-spacing: 2px;
    line-height: 1;
  }
  .audience-card__quote {
    font-family: var(--display);
    font-size: 16px;
    font-weight: 300;
    line-height: var(--lh-prose);
    color: var(--cream);
    opacity: 0.85;
    flex: 1;
  }
  /* Opening mark */
  .audience-card__quote::before {
    content: '“';
    font-size: 28px;
    line-height: 0.6;
    vertical-align: -8px;
    color: var(--verd-light);
    opacity: 0.6;
    margin-right: 4px;
    font-family: var(--display);
  }
  .audience-card__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-top: 14px;
    border-top: 1px solid rgba(242,237,228,0.07);
  }
  .audience-card__author {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.5;
  }
  .audience-card__source {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--mono);
    font-size: 8px;
    letter-spacing: var(--track-narrow);
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.3;
    text-decoration: none;
  }
  .audience-card__source:hover { opacity: 0.6; }
  .audience-card__source-dot {
    width: 6px; height: 6px;
    border-radius: 50%;
    flex-shrink: 0;
  }
  /* Source colors */
  
  
  
  
  
  

  /* No reviews state */
  

  /* ── RECEPTION INLINE IN SYNOPSIS PANEL (paper bg) ── */
  
  .syn-reception .reception-scores { margin-bottom: 28px; }
  .proj-panel--paper .score-badge {
    border-left-color: rgba(26,25,22,0.15);
    background: var(--chalk-mid);
  }
  .proj-panel--paper .score-badge--fresh { border-left-color: rgba(34,139,34,0.4); }
  .proj-panel--paper .score-badge--certified { border-left-color: #2A7A1A; }
  .proj-panel--paper .score-badge__num { color: var(--ink); }
  .proj-panel--paper .score-badge--fresh .score-badge__num,
  .proj-panel--paper .score-badge--certified .score-badge__num { color: #2A7A1A; }
  .proj-panel--paper .score-badge__label { color: var(--ink); }
  .proj-panel--paper .score-badge__label strong { color: #2A7A1A; }
  .proj-panel--paper .score-badge__meta,
  .proj-panel--paper .score-badge__source { color: var(--ink); }
  .proj-panel--paper .score-badge:hover { background: var(--chalk-deep); }
  .proj-panel--paper .audience-card {
    background: var(--chalk-mid);
    border-color: var(--rule-strong);
  }
  .proj-panel--paper .audience-card__quote { color: var(--ink); opacity: 0.8; }
  .proj-panel--paper .audience-card__quote::before { color: var(--verd); }
  .proj-panel--paper .audience-card__footer { border-color: var(--rule); }
  .proj-panel--paper .audience-card__author { color: var(--ink); opacity: 0.45; }
  .proj-panel--paper .audience-card__source { color: var(--ink); opacity: 0.4; }
  .proj-panel--paper .audience-card:hover { border-color: var(--verd); }
  

  /* ════════ FAVORITES / LIBRARY PAGE ════════ */
  #view-favorites {
    background: var(--chalk);
    color: var(--ink);
    min-height: 100vh;
    min-height: 100svh; /* in-app browser safe */
    /* Prevents the mobile horizontal-wiggle the user reported. Without
       this, if any descendant overflows its parent (e.g. a grid item
       that measured wider than the viewport for a frame, or a wide
       SVG), the whole page could scroll horizontally on touch. */
    overflow-x: hidden;
  }
  #view-favorites em { font-family: 'Instrument Serif', serif; font-style: italic; font-weight: 400; }

  /* Desktop base padding — matches the site-wide pattern: heroes get
     96px top (below the fixed header) + 48px bottom, content sections
     get 48px vertical. Horizontal is 32px on desktop, 20px on mobile
     (set below in the media query). */
  .favs-hero {
    padding: 96px 32px 48px;
    max-width: 1500px;
    margin: 0 auto;
  }
  .favs-section {
    padding: 48px 32px;
    max-width: 1500px;
    margin: 0 auto;
  }

  @media (max-width: 720px) { .favs-hero__stats { grid-template-columns: repeat(2, 1fr); } }

  /* Mobile padding — unified with the rest of the site at 900px,
     not 640px. Hero gets the standard 96/20/32 treatment, content
     sections get 32/20. */
  @media (max-width: 1024px) {
    .favs-hero { padding: 96px 20px 32px; }
    .favs-section { padding: 32px 20px; }
    .favs-genre-item { grid-template-columns: 1fr; gap: 4px; }
    .favs-genre-item__meta, .favs-genre-item__stars { text-align: left; }
  }

  /* ════════ FILM JUMP: clickable film titles + pulse highlight ════════ */
  /* Any <button.film-jump> becomes an inline-text-styled link */
  .film-jump {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    text-align: inherit;
    cursor: pointer;
    border-bottom: 1px solid transparent;
    transition: border-color 0.2s ease, color 0.2s ease;
    display: inline;
  }
  .film-jump:hover {
    border-bottom-color: var(--verd);
    color: var(--verd);
  }
  .film-jump:focus-visible {
    outline: 2px solid var(--verd);
    outline-offset: 3px;
    border-radius: 2px;
  }

  /* Pulse animation: plays when a film card is jumped to */
  @keyframes film-jump-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(45, 106, 91, 0); background: transparent; }
    8%   { box-shadow: 0 0 0 6px rgba(45, 106, 91, 0.22); background: rgba(45, 106, 91, 0.06); }
    100% { box-shadow: 0 0 0 0 rgba(45, 106, 91, 0); background: transparent; }
  }
  .film-jump-pulse {
    animation: film-jump-pulse 2.4s ease-out;
  }

  @media (max-width: 640px) {
    .favs-rec-group { padding: 0 20px; }
    .favs-rec-group__head { flex-direction: column; align-items: flex-start; }
    .lineage-recs__grid { grid-template-columns: 1fr; }
  }

  /* Show/hide via JS; default grid active */
  #favsCardGrid { display: grid; }
  #favsCardGrid:not(.active) { display: none; }
  #favsListView { display: none; }
  #favsListView.active { display: block; }

  /* Defer offscreen favorite-film cards. With 100+ films in the library,
     each card rendering year/director/genres/themes/links/description/
     stars is a non-trivial layout cost on initial paint. Deferring the
     cards outside the viewport reduces first-paint work dramatically
     on the /library/ page.

     v2.9.150 — Scope to the desktop grid only (≥901px). On mobile the
     cards render as a horizontal swipe rail (.is-mobile-swipe), where
     content-visibility: auto incorrectly treated cards scrolled off
     the right edge of the viewport as "skipped" and gave them the
     480px intrinsic size — visible to the user as 514px-tall cards
     with large empty gaps before the next section.

     v2.9.151 — Lowered intrinsic-size from 480px → 200px to match the
     new compact card layout (meta + title + director + genre = ~190px
     tall). Off-screen cards in the desktop rail now reserve roughly the
     correct space, so the rail's height is stable and CLS stays at 0.
     Without this update, off-screen cards reserved 480px each, creating
     dead vertical space matching the original mobile bug just on a
     larger screen. */
  @media (min-width: 1025px) {
    #favsCardGrid .favs-film {
      content-visibility: auto;
      contain-intrinsic-size: 1px 200px;
    }
  }

  @media (max-width: 720px) {
    .favs-filters-block .filter-row {
      flex-direction: column; gap: 8px; align-items: stretch;
    }
    .favs-filters-block .filter-row__label { width: auto; padding-top: 0; }
    .favs-list-table thead { display: none; }
    .favs-list-row { display: block; padding: 16px 0; border-bottom: 1px solid rgba(26,25,22,0.1); }
    .favs-list-row td { display: block; padding: 4px 0; border: none; }
    .favs-list-row__title { font-size: 19px; margin-bottom: 4px; }
  }
/* ═══════════════════════════════════════════════════════════════════
   MOBILE TOUCH OPTIMIZATION — WCAG 2.5.5 AA touch targets (44×44px)
   Applied only at <=768px so desktop visual density is preserved.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
  /* Filter chips — expand to meet 44px minimum */
  .fchip {
    min-height: 44px;
    padding: 10px 16px;
    font-size: 11px;
  }

  /* Tap targets inside cards — ensure inner links/buttons meet 44px */
  .work-card,
  .mv-card,
  .favs-grid__card,
  .featured-item,
  .press-card,
  .proj-video-card,
  .vid-thumb,
  .vid-vert {
    /* Cards are already tall enough but ensure focus/hit-target is generous */
    -webkit-tap-highlight-color: rgba(45, 106, 91, 0.2);
  }

  /* Nav links — larger hit area */
  .nav__links a,
  .menu-overlay a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* Social icons in footer — expand tap area */
  .social-link,
  .footer__social a {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* View toggle + clear filters: mobile tap-target sizing.
     44px min-height meets WCAG touch-target guidance.
     view-toggle-btn keeps horizontal padding because it's a visible
     pill with an active-state background that needs breathing room.
     clear-filters drops horizontal padding — it's a plain underlined
     link that should align flush with the surrounding content column
     (matches the left edge of "Showing X of X" above it and the card
     grid beneath). */
  .view-toggle-btn {
    min-height: 44px;
    padding: 10px 18px;
  }
  .clear-filters {
    min-height: 44px;
    padding: 10px 0;
  }

  /* Disable hover states on touch devices (they get stuck after tap) */
  @media (hover: none) {
    .work-card:hover .work-card__img::before,
    .mv-card:hover .mv-card__watch-link,
    .watch-card:hover::before,
    .gallery-tile:hover::after {
      /* Still allow the tap to feel responsive — transition on active instead */
    }
  }
}

/* Honor prefers-reduced-motion across the site */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

  /* ══ PROJECT — SIMILAR WORK TAB ═══════════════════════════════════════
     Styles for the Positioning + Similar Films + Similar Directors
     panel. Context-aware: text is cream by default (dark panel bg),
     inverted to ink on .proj-panel--paper variants. */

  .proj-similar__positioning {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--ps-lede);
    line-height: 1.5;
    /* max-width removed — the right column width (1fr of 2fr/1fr grid)
       now constrains the measure. margin-bottom removed because grid
       gap handles spacing between blocks. */
    margin: 0;
    padding: 20px 0 20px 30px;
    border-left: 3px solid var(--verd);
    color: var(--cream);
  }
  .proj-panel--paper .proj-similar__positioning { color: var(--ink); }

  .proj-similar__section { margin-bottom: 48px; }
  .proj-similar__section:last-child { margin-bottom: 0; }

  .proj-similar__eyebrow {
    font-family: var(--mono);
    font-size: var(--ps-label);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--verd-light);
    margin-bottom: 20px;
    font-weight: 500;
  }
  .proj-panel--paper .proj-similar__eyebrow { color: var(--verd); }

  .proj-similar__list {
    display: grid;
    grid-template-columns: 1fr;
    gap: 24px;
  }

  .proj-similar-film,
  .proj-similar-director {
    padding-bottom: 24px;
    border-bottom: 1px solid var(--rule-light);
  }
  .proj-similar-film:last-child,
  .proj-similar-director:last-child { border-bottom: none; }
  .proj-panel--paper .proj-similar-film,
  .proj-panel--paper .proj-similar-director {
    border-bottom-color: var(--rule);
  }

  .proj-similar-film__title {
    font-family: var(--display);
    font-size: var(--ps-emphasis);
    line-height: 1.35;
    margin-bottom: 6px;
    color: var(--cream);
  }
  .proj-panel--paper .proj-similar-film__title { color: var(--ink); }

  .proj-similar-film__title em {
    font-style: italic;
    font-weight: 400;
    color: var(--cream);
  }
  .proj-panel--paper .proj-similar-film__title em { color: var(--ink); }

  .proj-similar-film__meta {
    font-family: var(--mono);
    font-style: normal;
    font-size: var(--ps-label);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--cream);
    opacity: 0.6;
    margin-left: 4px;
  }
  .proj-panel--paper .proj-similar-film__meta { color: var(--ink); opacity: 0.5; }

  .proj-similar-director__name {
    font-family: var(--display);
    font-size: var(--ps-emphasis);
    line-height: 1.35;
    margin-bottom: 6px;
    color: var(--cream);
  }
  .proj-panel--paper .proj-similar-director__name { color: var(--ink); }

  .proj-similar__why {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--ps-body);
    line-height: var(--lh-prose);
    max-width: 640px;
    color: var(--cream);
    opacity: 0.8;
  }
  .proj-panel--paper .proj-similar__why { color: var(--ink-mid); opacity: 1; }

  @media (max-width: 1024px) {
    .proj-similar__positioning {
      padding: 16px 0 16px 20px;
      margin-bottom: 32px;
    }
    .proj-similar__section { margin-bottom: 40px; }
  }

  /* ══ COMPANION READING ════════════════════════════════════════════════
     Renders inside the Similar Work tab's right-column aside, alongside
     (or replacing) the positioning blockquote. Built as a verd-toned
     card so it reads as a complementary recommendation rather than a
     pull-quote — the visual distinction matches the content distinction
     (one is editorial framing for THIS project, the other is a pointer
     to a separate book). When the project has a companion_book_url, the
     entire card is wrapped in <a> and gets a subtle hover lift; when
     there's no URL the card renders as static info.

     Like the rest of .proj-similar, the styles apply on the dark panel
     by default and get inverted via .proj-panel--paper for paper variants. */

  /* Spacing between the positioning quote and the companion card
     is now handled by the grid row-gap on desktop and an explicit
     margin-top on the companion aside on mobile (see .proj-similar
     above). The old `.positioning + .companion` adjacent-sibling
     selector no longer matches because the two are siblings of
     .proj-similar__lists rather than each other. */

  .proj-companion {
    display: block;
    padding: 24px 24px 22px;
    background: rgba(45, 106, 91, 0.10);
    border: 1px solid rgba(45, 106, 91, 0.35);
    border-top: 2px solid var(--verd);
    color: var(--cream);
    text-decoration: none;
    /* Anchor for the card-as-link variant; transition keeps the hover
       feel cohesive with .proj-similar__list cards on the left column. */
    transition: background 0.22s var(--ease),
                border-color 0.22s var(--ease),
                transform 0.22s var(--ease);
  }

  /* Linked variant — only applied when companion_book_url is present.
     The wrapping <a> gets the hover lift; static <div> variant doesn't. */
  .proj-companion--linked {
    cursor: pointer;
  }
  .proj-companion--linked:hover,
  .proj-companion--linked:focus-visible {
    background: rgba(45, 106, 91, 0.18);
    border-color: var(--verd);
    transform: translateY(-2px);
    outline: none;
  }
  .proj-companion--linked:focus-visible {
    /* Visible focus ring overrides the lift-only state for keyboard
       users — outline doubles as the accessibility cue without removing
       the hover affordance. */
    box-shadow: 0 0 0 2px var(--verd-light);
  }

  .proj-companion__eyebrow {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    margin-bottom: 14px;
    font-weight: 500;
  }
  .proj-companion__eyebrow-mark {
    /* The pilcrow (¶) is a printer's-mark companion to the wayfinder
       section-mark (§). Using a different glyph here keeps the visual
       grammar consistent (these are editorial ornaments) while
       distinguishing this card from the positioning quote above. */
    font-family: var(--display);
    font-size: 14px;
    line-height: 1;
    color: var(--verd);
    opacity: 0.85;
  }

  .proj-companion__title {
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(20px, 2vw, 24px);
    line-height: var(--lh-display-medium);
    color: var(--cream);
    margin-bottom: 8px;
    /* The title field arrives wrapped in <em> from the template so it
       reads italicized — the convention for book titles in editorial
       prose. The em wrapper inherits color from this rule. */
  }
  .proj-companion__title em {
    font-style: italic;
    color: var(--cream);
  }

  .proj-companion__meta {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-narrow);
    text-transform: uppercase;
    color: var(--verd-light);
    opacity: 0.85;
    margin-bottom: 14px;
  }

  .proj-companion__note {
    font-family: var(--display);
    font-weight: 300;
    font-size: 15px;
    line-height: var(--lh-body);
    color: var(--cream);
    opacity: 0.85;
    margin-bottom: 16px;
  }
  .proj-companion__note:last-child {
    margin-bottom: 0;
  }

  .proj-companion__cta {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd-light);
    transition: gap 0.18s var(--ease), color 0.18s var(--ease);
  }
  .proj-companion__cta-arrow {
    font-family: var(--mono);
    transition: transform 0.18s var(--ease);
  }
  .proj-companion--linked:hover .proj-companion__cta {
    gap: 10px;
    color: var(--cream);
  }
  .proj-companion--linked:hover .proj-companion__cta-arrow {
    transform: translate(2px, -2px);
  }

  /* Paper-panel variant — invert the palette for projects whose Similar
     Work tab renders on a cream/light background. The verd structure
     stays; only text colors flip from cream → ink. */
  .proj-panel--paper .proj-companion {
    background: rgba(45, 106, 91, 0.06);
    border-color: rgba(45, 106, 91, 0.25);
    border-top-color: var(--verd);
    color: var(--ink);
  }
  .proj-panel--paper .proj-companion--linked:hover,
  .proj-panel--paper .proj-companion--linked:focus-visible {
    background: rgba(45, 106, 91, 0.12);
    border-color: var(--verd);
  }
  .proj-panel--paper .proj-companion__eyebrow,
  .proj-panel--paper .proj-companion__meta { color: var(--verd); }
  .proj-panel--paper .proj-companion__title,
  .proj-panel--paper .proj-companion__title em,
  .proj-panel--paper .proj-companion__note { color: var(--ink); }
  .proj-panel--paper .proj-companion__cta { color: var(--verd); }
  .proj-panel--paper .proj-companion--linked:hover .proj-companion__cta { color: var(--verd-deep); }

  /* Mobile — looser internal padding plus margin-top adjustment. The
     mobile layout already drops the column structure (see proj-similar
     @media block above), so the companion sits inline with the rest of
     the content. */
  @media (max-width: 1024px) {
    .proj-companion {
      padding: 20px 18px 18px;
    }
  }

  /* ══ JOURNAL POSTS (single.php) ═══════════════════════════════════════
     Long-form reading layout for imported journal posts. Uses the site's
     display serif (Instrument Serif) for editorial body copy, Geist Mono
     for metadata. Responsive to mobile + respects site palette. */

  .journal-post {
    max-width: 720px;
    margin: 0 auto;
    padding: 100px 32px 48px;
    color: var(--ink);
  }
  @media (max-width: 1024px) {
    .journal-post { padding: 96px 20px 32px; }
  }

  /* HEADER */
  .journal-post__header {
    margin-bottom: 48px;
    padding-bottom: 32px;
    border-bottom: 1px solid var(--rule);
  }

  /* Back-link — small editorial affordance returning to /journal/. Sits
     above the eyebrow so it reads as a navigational frame, not part of
     the article's own content. */
  .journal-post__back {
    display: inline-block;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink-dim);
    text-decoration: none;
    margin-bottom: 32px;
    transition: color 0.15s;
  }
  .journal-post__back:hover { color: var(--verd); }

  /* Eyebrow — "Journal · Category" in mono uppercase verd. Matches
     the .courses-hero__eyebrow pattern used on the index + other heroes. */
  .journal-post__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    margin-bottom: 20px;
  }

  .journal-post__meta {
    font-family: var(--mono);
    text-transform: uppercase;
    letter-spacing: var(--track-mid);
    font-size: 10px;
    color: var(--ink-dim);
    margin-bottom: 28px;
  }

  .journal-post__title {
    font-family: var(--display);
    font-weight: 300;
    font-size: clamp(40px, 7vw, 84px);
    line-height: 1;
    letter-spacing: var(--track-tight);
    color: var(--ink);
    margin: 0 0 28px;
  }
  .journal-post__title em { color: var(--verd); font-weight: 400; font-style: italic; }

  .journal-post__dek {
    font-family: var(--display);
    font-size: var(--h-lede);
    line-height: var(--lh-body);
    font-weight: 300;
    font-style: italic;
    color: var(--ink);
    opacity: 0.75;
    margin: 0;
  }

  .journal-post__hero {
    margin: 40px 0 0;
    padding: 0;
  }
  .journal-post__hero-img,
  .journal-post__hero img {
    width: 100%;
    height: auto;
    display: block;
  }
  .journal-post__hero-caption {
    font-family: var(--mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: var(--track-narrow);
    color: var(--ink-dim);
    margin-top: 12px;
  }

  /* BODY — WordPress post content
     Matches the canonical long-form body style used in .syn-body and
     .bio-deep__body: 300 weight display serif, 1.5vw clamp scale, 1.6
     line-height, paragraph bottom margin for readability. Any tweaks to
     this block should be mirrored into those two for consistency. */
  .journal-post__body {
    font-family: var(--display);
    font-weight: 300;
    font-size: var(--h-lede);
    line-height: var(--lh-prose);
    color: var(--ink);
  }
  .journal-post__body p { margin: 0 0 24px; }
  .journal-post__body p:last-child { margin-bottom: 0; }

  /* Drop cap on first paragraph of imported posts — matches syn-body style */
  .journal-post__body > p:first-child::first-letter {
    font-size: 72px;
    float: left;
    line-height: 0.85;
    margin: 8px 14px 0 0;
    font-weight: 400;
    color: var(--verd);
    font-family: var(--display);
  }

  .journal-post__body h2 {
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(26px, 3.5vw, 36px);
    line-height: var(--lh-display-medium);
    letter-spacing: var(--track-card);
    color: var(--ink);
    margin: 1.8em 0 0.6em;
  }
  .journal-post__body h2 em { color: var(--verd); font-style: italic; }

  .journal-post__body h3 {
    font-family: var(--display);
    font-weight: 400;
    font-size: clamp(22px, 2.8vw, 28px);
    line-height: 1.25;
    letter-spacing: var(--track-subtle);
    color: var(--ink);
    margin: 1.6em 0 0.5em;
  }
  .journal-post__body h4 {
    font-family: var(--mono);
    font-weight: 500;
    font-size: 12px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--verd);
    margin: 1.6em 0 0.5em;
  }

  .journal-post__body a {
    color: var(--verd);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
    transition: color 0.2s;
  }
  .journal-post__body a:hover {
    color: var(--verd-deep);
  }

  .journal-post__body em,
  .journal-post__body i {
    font-style: italic;
    color: inherit;
    font-weight: inherit;
  }
  .journal-post__body strong,
  .journal-post__body b {
    font-weight: 600;
    color: var(--ink);
  }

  .journal-post__body blockquote {
    font-family: var(--display);
    font-size: clamp(22px, 2.6vw, 28px);
    line-height: 1.4;
    font-style: italic;
    color: var(--ink);
    padding: 24px 0 24px 32px;
    margin: 1.4em 0;
    border-left: 3px solid var(--verd);
    max-width: 640px;
  }
  .journal-post__body blockquote cite {
    display: block;
    font-family: var(--mono);
    font-size: 11px;
    font-style: normal;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-dim);
    margin-top: 16px;
  }

  .journal-post__body ul,
  .journal-post__body ol {
    padding-left: 24px;
    margin: 1.2em 0;
  }
  .journal-post__body li {
    margin-bottom: 0.5em;
    line-height: 1.65;
  }
  .journal-post__body li::marker { color: var(--verd); }

  .journal-post__body hr {
    border: none;
    height: 1px;
    background: var(--rule);
    margin: 2.5em 0;
  }

  .journal-post__body img,
  .journal-post__body figure {
    max-width: 100%;
    height: auto;
    margin: 1.6em 0;
    display: block;
  }
  .journal-post__body figure figcaption {
    font-family: var(--mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: var(--track-narrow);
    color: var(--ink-dim);
    margin-top: 10px;
    text-align: center;
  }

  /* JK_Signature recolor — specific to this one signature PNG.
     The source file at /wp-content/uploads/2026/04/JK_Signature.png
     is white-on-transparent. The CSS filter chain below recolors it
     to verdigris (--verd, #2D6A5B) without requiring the original
     PNG to be edited or replaced.
     The [src*=] match catches both the original filename and any
     WordPress intermediate sizes (JK_Signature-300x100.png, etc.)
     that the editor might insert.
     Filter mechanics:
     - brightness(0) saturate(100%) forces ANY non-transparent pixel
       to pure black, neutralizing the source color (works for white,
       black, or any tone in between). This makes the recolor robust
       against future signature replacements.
     - The remaining chain (computed for #2D6A5B with loss 0.03/255)
       transforms pure black into verdigris while preserving the
       PNG's alpha channel — outline strokes, anti-aliased edges,
       and transparent areas remain pixel-perfect.
     If James adds a different signature file later, this rule's
     [src*="JK_Signature"] match keeps it scoped to just this one.
     The home page uses a separate mask-image pattern in
     .bio-deep__signature-img (see ~line 12100) which doesn't need
     this filter chain — mask-image only uses the alpha channel,
     so the source color there is also irrelevant. */
  .journal-post__body img[src*="JK_Signature"] {
    filter: brightness(0) saturate(100%) invert(32%) sepia(57%) saturate(418%) hue-rotate(115deg) brightness(92%) contrast(84%);
    /* v2.9.270 — fixed width across the board on journal posts.
       Without this constraint the signature renders at its natural
       PNG dimensions (often 1200px+ from the WordPress upload), which
       reads as cartoonishly oversized inside the 720px-wide journal
       prose column. 240px reads at roughly the same proportion as
       a real handwritten signature would in a typeset memoir page.
       max-width keeps it from overflowing on small phones; height:auto
       preserves the aspect ratio. The display:block + margin gives
       it the same vertical rhythm as a paragraph. */
    width: 240px;
    height: auto;
    max-width: 100%;
    display: block;
    margin: 32px 0;
  }

  /* Responsive containment for all embedded media in blog posts.
     Imported WordPress content often includes YouTube iframes with default
     width="560" attributes, Twitter/Instagram embeds, wide tables, and
     inline SVGs — any of which can push the page wider than the mobile
     viewport if not constrained.

     All embedded iframes are width-constrained to parent. Only video-embed
     iframes (YouTube, Vimeo) get a forced 16:9 aspect ratio — other embeds
     (Twitter, Instagram, Spotify, SoundCloud) keep their native height
     from the provider's markup since their heights aren't 16:9. */
  .journal-post__body iframe,
  .journal-post__body embed,
  .journal-post__body object,
  .journal-post__body video {
    max-width: 100%;
    display: block;
    margin: 1.6em 0;
    border: 0;
  }
  /* Force 16:9 only on known video embeds so YouTube/Vimeo iframes stay
     proportional when the container shrinks. Matches both direct iframes
     and Gutenberg wp-block-embed wrappers. */
  .journal-post__body iframe[src*="youtube.com/embed"],
  .journal-post__body iframe[src*="youtube-nocookie.com/embed"],
  .journal-post__body iframe[src*="youtu.be"],
  .journal-post__body iframe[src*="player.vimeo.com"],
  .journal-post__body .wp-block-embed-youtube iframe,
  .journal-post__body .wp-block-embed-vimeo iframe {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
  }
  .journal-post__body .wp-block-embed,
  .journal-post__body .wp-block-embed__wrapper,
  .journal-post__body .wp-embedded-content {
    max-width: 100%;
    width: 100%;
    margin: 1.6em 0;
  }
  .journal-post__body .wp-block-embed iframe {
    margin: 0;  /* wrapper handles spacing */
  }
  .journal-post__body svg {
    max-width: 100%;
    height: auto;
  }

  .journal-post__body pre,
  .journal-post__body code {
    font-family: var(--mono);
    font-size: 13px;
    background: var(--chalk-mid);
    border-radius: 2px;
  }
  .journal-post__body code {
    padding: 2px 6px;
  }
  .journal-post__body pre {
    padding: 20px;
    overflow-x: auto;
    margin: 1.4em 0;
  }
  .journal-post__body pre code {
    background: none;
    padding: 0;
  }

  .journal-post__body table {
    width: 100%;
    border-collapse: collapse;
    margin: 1.4em 0;
    font-family: var(--sans);
    font-size: 15px;
  }
  .journal-post__body th,
  .journal-post__body td {
    padding: 12px 16px;
    text-align: left;
    border-bottom: 1px solid var(--rule);
  }
  .journal-post__body th {
    font-family: var(--mono);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--ink-dim);
  }

  /* FOOTER */
  .journal-post__footer {
    margin-top: 64px;
    padding-top: 32px;
    border-top: 1px solid var(--rule);
    font-family: var(--mono);
    text-transform: uppercase;
    letter-spacing: var(--track-mid);
    font-size: 11px;
    color: var(--ink-dim);
    display: flex;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
  }
  .journal-post__footer a { color: inherit; text-decoration: none; }
  .journal-post__footer a:hover { color: var(--verd); }

  /* RELATED POSTS */
  .journal-post__related {
    margin-top: 80px;
    padding-top: 48px;
    border-top: 1px solid var(--rule);
  }
  .journal-post__related-eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    margin: 0 0 24px;
  }
  .journal-post__related-list {
    list-style: none;
    margin: 0 0 32px;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .journal-post__related-link {
    display: grid;
    grid-template-columns: 110px 1fr;
    gap: 20px;
    padding: 16px 0;
    border-bottom: 1px solid var(--rule);
    text-decoration: none;
    align-items: baseline;
    transition: padding 0.2s;
  }
  .journal-post__related-link:hover {
    padding-left: 8px;
  }
  .journal-post__related-date {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--ink-dim);
    text-transform: uppercase;
  }
  .journal-post__related-title {
    font-family: var(--display);
    font-size: var(--h-lede);
    line-height: 1.3;
    color: var(--ink);
    transition: color 0.2s;
  }
  .journal-post__related-link:hover .journal-post__related-title { color: var(--verd); }
  .journal-post__related-all {
    display: inline-block;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--verd);
    text-decoration: none;
    padding: 8px 0;
    transition: color 0.2s;
  }
  .journal-post__related-all:hover { color: var(--verd-deep); }

  @media (max-width: 640px) {
    .journal-post__body > p:first-child::first-letter {
      font-size: 56px;
    }
    .journal-post__body blockquote {
      padding-left: 20px;
    }
    .journal-post__related-link {
      grid-template-columns: 1fr;
      gap: 6px;
    }
  }

  /* ══════════════════════════════════════════════════════════════════
     PRESS ARCHIVE — MOBILE OPTION-2 TREATMENT
     ──────────────────────────────────────────────────────────────────
     Mirrors library pattern: search input + filter drawer on mobile.
     Press cards already link out to the source, so no detail sheet —
     tapping goes straight to the article as users expect.
  ══════════════════════════════════════════════════════════════════ */

  /* TOOLBAR — filter button only (search input removed sitewide).
     Hidden by default at all viewports because the filter block below
     renders inline on desktop (the drawer class is only activated at
     mobile widths via the @media rule below). Shown only on mobile
     where the filter block becomes a drawer and needs a trigger. */
  .press-toolbar {
    display: none;
    align-items: center;
    padding: 16px 0;
    border-bottom: 1px solid var(--rule);
    margin-bottom: 8px;
  }

  /* FILTER TOGGLE BUTTON — hidden on desktop (filter block is inline),
     shown on mobile where it triggers the bottom-sheet drawer */
  .press-filter-toggle {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 16px;
    font-family: var(--mono);
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    background: var(--ink);
    color: var(--chalk);
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }
  .press-filter-toggle:hover { background: var(--ink-mid); }
  .press-filter-toggle__icon { font-size: 14px; line-height: 1; }
  .press-filter-toggle__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    background: var(--verd);
    color: var(--chalk);
    border-radius: 10px;
    font-size: 11px;
    font-weight: 600;
  }

  /* BACKDROP — cannot transition display, so we gate via opacity +
     visibility + pointer-events and keep the element in layout. */
  .press-drawer-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.55);
    z-index: 9998;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.25s ease, visibility 0s linear 0.25s;
  }
  .press-drawer-backdrop.open {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.25s ease, visibility 0s linear 0s;
  }

  /* DRAWER HEAD — only shown on mobile when drawer is open */
  .press-drawer__head {
    display: none;
    align-items: center;
    justify-content: space-between;
    padding: 18px 20px 14px;
    border-bottom: 1px solid var(--rule);
    margin-bottom: 12px;
  }
  .press-drawer__title {
    font-family: var(--display);
    font-size: 22px;
    font-weight: 400;
    color: var(--ink);
    margin: 0;
  }
  .press-drawer__close {
    width: 44px; height: 44px;
    background: none;
    border: none;
    font-size: 28px;
    line-height: 1;
    color: var(--ink);
    cursor: pointer;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .press-drawer__close:hover { color: var(--verd); }

  /* ════════════════════════════════════════════════════════════════
     ≤ 900px — on-camera filter drawer (v2.9.42)
     ────────────────────────────────────────────────────────────────
     Mobile transformation of the .on-camera-filters chip strip into
     the same bottom-sheet drawer pattern used by /press/ #archive.
     The toggle pill sticks to the top of the press section so the
     filter remains reachable as the user scrolls past — addresses
     James's "doesn't jump" requirement (the previous chip-strip
     scrolled out of view and required scrolling back to access).

     Toggle pill stays in normal flow but uses position:sticky with a
     top offset that clears the fixed site nav. Drawer opens via the
     .on-camera-filters--open class on the container (set by JS).
     Backdrop is fixed-inset so the page behind goes dim while the
     sheet is up.
     ════════════════════════════════════════════════════════════════ */
  @media (max-width: 1024px) {
    .on-camera-filters__toggle {
      display: inline-flex;
      /* Sticky at the top of the press section so the filter is always
         within reach. 64px clears the fixed nav (--nav height ~54px
         plus a small gutter). z-index above content but below the
         drawer (which uses 9999). */
      position: sticky;
      top: 64px;
      z-index: 50;
      align-self: flex-start;
      max-width: 100%;
    }
    /* Drawer head visible on mobile (title + close X). */
    .on-camera-filters__drawer-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 24px 20px 14px;
      border-bottom: 1px solid var(--rule);
      margin: 0 -20px 12px;
    }
    .on-camera-filters__drawer-title {
      font-family: var(--display);
      font-size: 22px;
      font-weight: 400;
      color: var(--ink);
    }
    .on-camera-filters__drawer-close {
      width: 44px; height: 44px;
      background: none;
      border: none;
      font-size: 28px;
      line-height: 1;
      color: var(--ink);
      cursor: pointer;
      padding: 0;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .on-camera-filters__drawer-close:hover { color: var(--verd); }

    /* DRAWER — fixed bottom sheet. Default = closed (transformed
       out of view). The .on-camera-filters--open modifier on the
       parent slides it in. */
    .on-camera-filters__drawer {
      position: fixed;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 9999;
      max-height: min(75vh, calc(100vh - 64px));
      overflow-y: auto;
      background: var(--chalk);
      border-top-left-radius: 16px;
      border-top-right-radius: 16px;
      transform: translateY(100%);
      transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
      padding: 0 20px 24px;
      box-shadow: 0 -4px 24px rgba(0,0,0,0.2);
    }
    .on-camera-filters--open .on-camera-filters__drawer {
      transform: translateY(0);
    }

    /* BACKDROP — fixed inset, dim while drawer open. Default = hidden
       on mobile too (was display:block here, which overrode the
       [hidden] attribute and produced a permanent dark overlay
       covering the page on every mobile load — the page appeared
       dim and untappable because z-index 9998 caught every click).
       Now: hidden by default, only visible when the parent has the
       .on-camera-filters--open class, matching the drawer's own
       open-state gating. */
    .on-camera-filters__drawer-backdrop {
      display: none;
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.55);
      z-index: 9998;
    }
    .on-camera-filters--open .on-camera-filters__drawer-backdrop {
      display: block;
    }

    /* Chips inside the drawer stack as a wrap row with a top label
       repeating the drawer title context. The label that lives in
       the chip array is hidden because the drawer head already says
       "Filter by project". */
    .on-camera-filters__chips {
      gap: 8px;
      padding: 0;
    }
    .on-camera-filters__chips .on-camera-filters__label {
      display: none;
    }
    .on-camera-filters__chips .fchip {
      font-size: 12px;
      padding: 8px 14px;
    }
  }

  /* ════════════════════════════════════════════════════════════════
     ≤ 900px — press archive mobile
     ════════════════════════════════════════════════════════════════ */
  @media (max-width: 1024px) {
    .press-archive { padding: 32px 20px !important; }
    .press-archive__head {
      flex-direction: column;
      align-items: flex-start !important;
      gap: 14px;
    }
    .press-archive__title { font-size: clamp(28px, 6vw, 40px) !important; }
    .press-archive__view-toggle {
      align-self: flex-start;  /* don't stretch to full width — sit at content width */
      display: inline-flex;
      gap: 2px;
    }
    .press-toolbar {
      display: flex;
      padding: 14px 0;
    }
    .press-filter-toggle {
      align-self: flex-start;
    }

    /* FILTER DRAWER — bottom sheet on mobile */
    .press-filters-block.press-drawer {
      position: fixed;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 9999;
      /* Cap to 75vh and leave 64px clearance from the top of the
         viewport so the close X never crowds the notch / nav. Was
         80vh which on shorter phones (e.g. 667px iPhone SE) put the
         X within 130px of the top edge — felt cramped per user
         report. With 75vh + max(0,calc(100vh - 75vh - 64px)) min-top
         the drawer top is guaranteed to sit at least 64px from the
         viewport top regardless of viewport height. */
      max-height: min(75vh, calc(100vh - 64px));
      overflow-y: auto;
      background: var(--chalk);
      border-top-left-radius: 16px;
      border-top-right-radius: 16px;
      transform: translateY(100%);
      transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
      padding: 0 20px 24px;
      box-shadow: 0 -4px 24px rgba(0,0,0,0.2);
      margin: 0 !important;
    }
    .press-filters-block.press-drawer.open { transform: translateY(0); }
    .press-drawer__head {
      display: flex;
      /* Bumped from 18px to 24px top so the title + close X sit a
         little lower below the rounded drawer top edge, matching the
         visual rhythm users expect from iOS/Android bottom sheets. */
      padding: 24px 20px 14px;
    }

    /* Filter rows stack vertically inside drawer */
    .press-filters-block .filter-row {
      flex-direction: column !important;
      align-items: flex-start !important;
      gap: 8px !important;
      padding: 12px 0 !important;
      border-bottom: 1px solid var(--rule);
    }
    .press-filters-block .filter-row:last-child { border-bottom: none; }
    .press-filters-block .filter-row__label {
      font-size: 11px !important;
      color: var(--verd) !important;
      margin-bottom: 2px;
    }
    .press-filters-block .filter-row__chips {
      display: flex !important;
      flex-wrap: wrap !important;
      gap: 6px !important;
      padding: 0 !important;
    }
    .press-filters-block .fchip {
      font-size: 12px !important;
      padding: 7px 12px !important;
    }

    /* Status row */
    .press-archive__status {
      padding: 12px 0 !important;
      font-size: 13px !important;
      flex-direction: column;
      align-items: flex-start !important;
      gap: 8px;
      /* Tighten the status → tiles gap on mobile. Desktop's 24px
         margin-bottom plus the 12px bottom padding adds up to 36px,
         which reads as a disconnect between "showing N items" and the
         tiles it describes. 8px keeps the bar as a distinct element
         but closer to what it refers to. */
      margin-bottom: 8px !important;
    }

    /* Card grid — 1 column */
    .archive-grid {
      grid-template-columns: 1fr !important;
      gap: 10px !important;
    }
    .archive-card {
      padding: 18px !important;
    }
    .archive-card__quote { font-size: 14px !important; }

    /* Lock body scroll when drawer open */
    body.press-modal-open {
      overflow: hidden;
      position: fixed;
      width: 100%;
    }
  }

  /* ══════════════════════════════════════════════════════════════════
     PROJECT TABS — MOBILE POSITION FEEDBACK
     ──────────────────────────────────────────────────────────────────
     Adds:
       - Edge fades on the horizontal tab strip (visual scrollability cue)
       - Dot indicator row below tabs (position at a glance)
     Desktop unchanged. Only activates at ≤900px.
  ══════════════════════════════════════════════════════════════════ */

  /* Mobile: tabs sticky below the fixed nav bar so users can navigate
     between panels even while scrolled deep into a long panel. Uses
     --nav-height CSS variable set from JS to match actual nav size
     (which can vary with font loading, orientation, etc). Falls back
     to 56px if JS hasn't run. */
  .proj-tabs-wrap {
    position: sticky;
    top: var(--nav-height, 56px);
    z-index: 40;
    /* v2.9.13 — was --ink (#1A1916). Switched to --dark (#161512) so
       proj-tabs-wrap paints the same color as the .proj-panels below
       it AND the proj-hero__overlay's gradient endpoint above. Three
       contiguous regions on a single color = no visible flash on
       scroll-into-sticky transitions. */
    background: var(--dark);
    /* v2.9.8 — GPU-promote this layer to fix the iOS Safari sticky
       paint flash. iOS sometimes briefly paints a sticky element with
       the wrong background during the transition to/from sticky
       state. Promoting to its own compositor layer via translateZ(0)
       lets the GPU keep its rendered output stable through the
       state change. will-change hints the browser to prepare the
       layer up front. */
    transform: translateZ(0);
    -webkit-transform: translateZ(0);
    will-change: transform;
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
  }
  .proj-tabs-wrap .proj-tabs {
    position: relative; /* wrap is sticky now — inner must NOT be sticky too */
  }
  .proj-tabs__fade {
    display: none;
    position: absolute;
    top: 0;
    bottom: 0;
    width: 32px;
    pointer-events: none;
    z-index: 41; /* above tab strip */
  }
  .proj-tabs__fade--left {
    left: 0;
    /* v2.9.13 — fade now blends from --dark to transparent to match
       the parent .proj-tabs-wrap's new background color. */
    background: linear-gradient(to right, var(--dark), rgba(22,21,18,0));
  }
  .proj-tabs__fade--right {
    right: 0;
    background: linear-gradient(to left, var(--dark), rgba(22,21,18,0));
  }

  /* DOT INDICATORS — hidden at all viewports. They originally served
     as a position indicator on mobile, but users mis-read them as a
     swipe-between-panels affordance (the project page uses a dropdown
     for panel navigation, not swipe gestures). Markup + JS left intact
     so we can revive them if the navigation model ever changes. */
  .proj-dots {
    display: none;
  }

  @media (max-width: 1024px) {
    .proj-tabs__fade { display: block; }
  }

  /* ─── Swipe rail position indicator dots ──────────────────────────
     Injected by JS (see initSwipeDots in main.js) as a sibling after
     each .is-mobile-swipe rail on mobile. Affordance for "this is
     swipable". Hidden on desktop and when the rail is in filtered
     stack mode (≤2 matches — see is-filtered handling above). */
  .is-mobile-swipe-dots {
    display: none; /* desktop default; mobile media query shows it */
  }
  @media (max-width: 1024px) {
    .is-mobile-swipe-dots {
      display: flex;
      justify-content: center;
      gap: 6px;
      padding: 10px 0 2px;
      margin-top: -8px;
    }
    .is-mobile-swipe-dots__dot {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--ink);
      opacity: 0.25;
      transition: opacity 0.2s ease, background 0.2s ease, transform 0.2s ease;
    }
    .is-mobile-swipe-dots__dot.is-active {
      background: var(--verd);
      opacity: 1;
      transform: scale(1.3);
    }
    /* When rail falls back to vertical stack (<=2 filtered matches),
       dots don't make sense — suppress via adjacent-sibling selector. */
    .is-mobile-swipe.is-filtered + .is-mobile-swipe-dots {
      display: none;
    }
    /* On dark panel surfaces (project page tab panels), use cream
       instead of ink so dots are visible on the dark bg. */
    .proj-panel .is-mobile-swipe-dots__dot {
      background: var(--cream);
      opacity: 0.3;
    }
    .proj-panel .is-mobile-swipe-dots__dot.is-active {
      background: var(--verd-light);
      opacity: 1;
    }
    /* Project-panel dots tweak. Base dots rule has margin-top: -8px
       which works on most contexts. On project panels the combination
       of rail padding-bottom + flex-gap creates slightly more space
       than ideal; -4px additional pull centers the dots visually
       between the rail and whatever follows. */
    .proj-panel .is-mobile-swipe-dots {
      margin-top: -12px;
    }
  }

  /* ══════════════════════════════════════════════════════════════════
     SELECTED WORK — HOME PAGE TILE LAYOUT
     ──────────────────────────────────────────────────────────────────
     Desktop: multi-column grid (auto-fill, minmax(340px, 1fr)) —
     the default .work-grid rule near the top of this file.

     Mobile: scaled-down version of the same tile grid. 2 columns on
     typical phones, smaller type/padding to match the narrower card
     footprint. This was briefly a horizontal swipe rail; that was
     retired and the page went back to the tile aesthetic.

     The shared .is-mobile-swipe treatment still exists elsewhere
     (films page, press page, library, etc.) and still styles any
     .work-card children inside an .is-mobile-swipe rail with the
     compact 16:9 treatment — see the .is-mobile-swipe > .work-card
     rules earlier in this file. It just doesn't apply to home
     anymore because the home markup doesn't use that class. */

  @media (max-width: 1024px) {
    /* 2-column tile grid on mobile. auto-fill with a 140px minimum
       yields 2 columns on a ~390px viewport, 3 on tablet widths. */
    .work-grid {
      grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
      gap: 32px 16px;
    }
    /* Scale card typography down to match the smaller tile footprint.
       Desktop is 26px title / 10px meta — mobile drops to 18px title
       / 9px meta. The 4:5 poster aspect ratio is unchanged — tiles
       read as mini versions of the desktop card. */
    .work-card__title {
      font-size: 18px;
      line-height: 1.15;
      margin-bottom: 4px;
    }
    .work-card__meta {
      font-size: 9px;
      letter-spacing: 0.12em;
    }
    /* Hide the slate ("NO. 01 · DOCUMENTARY / 2024") on mobile. At
       the smaller 2-column tile footprint it wraps awkwardly and adds
       visual noise on top of the poster; the project title and meta
       below carry the project identity just as well. Desktop tiles
       retain the slate (see base .work-card__slate rule above). */
    .work-card__slate {
      display: none;
    }
    .work-card__img {
      margin-bottom: 10px;
    }

    /* Section section follows the content-section standard on mobile:
       32px vertical, 20px horizontal — same as bio-deep above and
       home-press/lineage below. */
    .work {
      padding: 32px 20px;
    }
    /* v2.9.277 — promoted out of the .work parent scope to a bare
       .section-head__title selector. The class is used in two places
       on the home page: inside .work (Selected Work) and inside
       .home-services (Direction across formats). Previously only
       .work got the mobile override, so "Direction across formats"
       rendered at clamp(40, 6vw, 80) on mobile (40px floor) while
       "Selected Work" rendered at clamp(30, 7vw, 44) (30px floor).
       James spotted the size mismatch on mobile. Now both heads use
       the same mobile clamp. The !important is preserved because
       the desktop --h-poster-title cascade still wins otherwise.
       Verified .section-head__title is used ONLY on the home page
       (grep across template-parts confirms 2 occurrences, both on
       view-home.php). */
    .section-head__title {
      font-size: clamp(30px, 7vw, 44px) !important;
    }
  }

  /* ══════════════════════════════════════════════════════════════════
     HOME PRESS QUOTES — mobile collapse behavior
     ──────────────────────────────────────────────────────────────────
     Desktop: all quotes visible, toggle hidden.
     Mobile: only first quote visible; additional quotes collapse behind
     a "View N more" button to reduce initial page weight.
  ══════════════════════════════════════════════════════════════════ */

  .home-press__toggle {
    display: none; /* shown only on mobile */
    margin-top: 20px;
    background: none;
    border: 1px solid var(--rule);
    padding: 12px 20px;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--verd);
    cursor: pointer;
    width: 100%;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
  }
  .home-press__toggle:hover {
    border-color: var(--verd);
    background: rgba(45, 106, 91, 0.05);
  }

  @media (max-width: 1024px) {
    .home-press__toggle { display: inline-block; }
    .home-press__quote--collapsible {
      display: none; /* hidden until user taps "View more" */
    }
    .home-press__quotes.expanded .home-press__quote--collapsible {
      display: block;
      animation: homePressFade 0.35s ease;
    }
    @keyframes homePressFade {
      from { opacity: 0; transform: translateY(-8px); }
      to   { opacity: 1; transform: translateY(0); }
    }
  }

  /* ══════════════════════════════════════════════════════════════════
     MV/FILMS/COURSES — MOBILE CLEANUP
     ──────────────────────────────────────────────────────────────────
     These pages had some existing mobile CSS (grid → 1 column), but
     lacked polish around filters and section containers. Adds:
       - mv-grid-section--films replaces former inline style
       - mv-filters horizontal-scroll on mobile so chips don't wrap wall
       - tighter mobile stats + hero typography
  ══════════════════════════════════════════════════════════════════ */

  /* Former inline-style container on view-films.php + mirrored for
     view-music-videos.php. Both pages use the same wrapper structure
     so their dots spacing and visual framing read identically. */
  .mv-grid-section--films,
  .mv-grid-section--mv {
    background: var(--chalk);
    max-width: none;
    margin: 0;
    padding-left: 32px;
    padding-right: 32px;
  }
  .mv-grid-section__inner {
    max-width: 1500px;
    margin: 0 auto;
  }

  @media (max-width: 1024px) {
    /* Filter chip rows — horizontal scroll on mobile (matches journal).
       Previously used -20px side margins to bleed chips to screen edges, but
       that caused the whole page to overflow horizontally when chip count
       exceeded viewport width. Keeping chips within the parent padding is
       safer and visually aligned with the grid content below. */
    .mv-filters {
      display: flex !important;
      flex-wrap: nowrap !important;
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
      padding: 0 0 4px;
      gap: 8px;
      /* Prevent the scroll container from pushing beyond the parent's width.
         max-width:100% with min-width:0 is the flex-item overflow fix that
         lets horizontal scroll work inside a flex column layout. */
      max-width: 100%;
      min-width: 0;
      width: 100%;
    }
    /* In the horizontal-scroll filter row, chips must not shrink — they
       keep their natural width from label length, and the container
       scrolls when their combined width exceeds viewport. Without these
       two lines, the default flex-shrink behavior squishes chips to zero. */
    .mv-filters .fchip {
      flex-shrink: 0;
      white-space: nowrap;
    }
    .mv-filters::-webkit-scrollbar { display: none; }
    .mv-filters > * {
      flex-shrink: 0;
    }

    /* Section head stacks vertically with room for scrolling filters */
    .mv-grid-head {
      flex-direction: column;
      align-items: flex-start !important;
      gap: 14px;
      /* Mobile tightens the header-to-tiles gap from desktop's 48px
         down to 20px. The editorial breathing room that works at
         wider viewports reads as an empty gap on phones. */
      margin-bottom: 20px !important;
    }

    /* Films + MV page container padding on mobile */
    .mv-grid-section--films,
    .mv-grid-section--mv {
      padding-left: 20px;
      padding-right: 20px;
    }

    /* Hero stats — tighter on mobile */
    .mv-hero__stats {
      gap: 24px !important;
    }
    .mv-hero__stat-num {
      font-size: 32px !important;
    }
  }

  @media (max-width: 500px) {
    .mv-hero__title {
      font-size: clamp(40px, 10vw, 56px) !important;
    }
    .mv-hero__lede {
      font-size: 15px !important;
    }
  }

  /* Courses empty-state styling (admin-only, when no courses exist yet) */
  .courses-grid__empty {
    grid-column: 1 / -1;
    padding: 60px 32px;
    text-align: center;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    border: 1px dashed var(--rule);
  }
  .courses-grid__empty a {
    color: var(--verd);
    text-decoration: underline;
  }

  /* ══════════════════════════════════════════════════════════════════
     INQUIRIES — REPRESENTATION BLOCK (ACF repeater driven)
     ──────────────────────────────────────────────────────────────────
     Replaces former inline-styled hardcoded UTA + Rain blocks. Each
     representative is an editable row in Site Content → Representation.
  ══════════════════════════════════════════════════════════════════ */

  /* Each .contact-method__rep carries name + role + phone/email for one
     representative in the left-hand column. */
  .contact-method__rep {
    font-family: var(--display);
    font-size: 18px;
    color: var(--ink);
    line-height: 1.5;
  }
  .contact-method__rep-name {
    font-weight: 500;
  }
  .contact-method__rep-title {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-mid);
    text-transform: uppercase;
    color: var(--ink);
    opacity: 0.5;
    margin-top: 4px;
  }
  .contact-method__rep-lines {
    margin-top: 10px;
    font-size: 15px;
  }

  /* Right-side panel card — dark surface, lists reps' company links. */
  .rep-panel {
    background: var(--dark-card);
    padding: 40px;
    border: 1px solid rgba(242, 237, 228, 0.08);
  }
  .rep-panel__eyebrow {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    color: var(--verd-light);
    margin-bottom: 16px;
  }
  .rep-panel__headline {
    font-family: var(--display);
    font-weight: 300;
    font-size: 28px;
    color: var(--cream);
    letter-spacing: var(--track-card);
    margin: 0 0 24px;
    line-height: 1.15;
  }
  .rep-panel__body {
    font-size: 14px;
    line-height: 1.65;
    color: var(--cream);
    opacity: 0.6;
    margin: 0 0 32px;
  }
  .rep-panel__actions {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }

  @media (max-width: 1024px) {
    .rep-panel { padding: 28px 24px; }
    .rep-panel__headline { font-size: 24px; }
  }

  /* Press archive — author byline styling (added when author/journalist
     field is populated on a press item). Appears under the quote on
     cards, inline with outlet on list rows. */
  .archive-card__byline {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--verd);
    opacity: 0.75;
    margin-top: 6px;
  }
  .archive-row__author {
    color: var(--ink-mid);
    font-weight: 400;
    opacity: 0.7;
  }

  /* ══════════════════════════════════════════════════════════════════
     MOBILE TYPOGRAPHY SCALE — +10% for body/prose and meta
     ──────────────────────────────────────────────────────────────────
     Scoped to <1024px (v2.9.244). Long-form body prose bumps 18px → 20px,
     and small meta labels/eyebrows bump one step up so they're not
     fighting for legibility on phones and iPads.

     v2.9.244 — Breakpoint extended from 900px → 1024px to cover iPad
     portrait. James reported the footer rendering too small on iPad
     (1024px portrait viewport). At <900px, all iPad-portrait sizes
     except iPad Pro hit the bump; iPad Pro at 1024px exactly was
     stranded on desktop sizing where .footer__heading is 9px. This
     was the sole reason for the bump — fonts in the 8-11px range are
     hard to read on a hand-held screen regardless of whether the
     layout grid has flipped to mobile yet. Layout grids stay on the
     900px breakpoint (heroes, footers, multi-col sections all switch
     to single-column at 900px), but typography readability scales
     with the larger 1024px window.

     Selector lists generated by auditing every rule with font-size
     below 12px. Grouped by current effective mobile pixel size:
       8px → 10px  (14 classes — micro-captions, platform tags)
       9px → 10px  (43 classes — small labels, meta text)
       10px → 11px (65 classes — eyebrows, dates, buttons)

     Body prose families:
       .journal-post__body, .bio-deep__body, .syn-body, .faq-item__a
       18px → 20px (clamp min raised, keeps desktop the same)
  ══════════════════════════════════════════════════════════════════ */
  @media (max-width: 1024px) {
    /* ── Long-form prose (18px → 20px on mobile) ── */
    .journal-post__body,
    .bio-deep__body,
    .syn-body,
    .faq-item__a {
      font-size: 20px;
      line-height: var(--lh-prose);
    }

    /* Drop cap in journal posts scales with body bump */
    .journal-post__body > p:first-child::first-letter {
      font-size: 62px;
    }

    /* Journal entry excerpt (small body variant) */
    .journal-entry__excerpt {
      font-size: 15px;
    }

    /* ── 8px → 10px ── */
    .audience-card__source,
    .bio-deep__photo-caption,
    .bio-hero__frame-num,
    .course-card__platform-tag,
    .course-card__stat-label,
    .course-card__topic,
    .mv-card__duration,
    .mv-card__footer,
    .mv-card__genre-tag,
    .nav__links a::before,
    .press-pub-placeholder span,
    .proj-picker-btn,
    .vid-vert__duration,
    .vid-vert__platform {
      font-size: 10px;
    }

    /* ── 9px → 10px ── */
    .archive-card__footer,
    .archive-card__outlet,
    .archive-card__project,
    .archive-card__year,
    .archive-row__project,
    .archive-row__type,
    .audience-card__author,
    .bio-credential__label,
    .clear-filters,
    .contact-method__label,
    .course-card__btn,
    .course-card__category,
    .course-card__level,
    .course-card__price small,
    .course-card__thumb-inner,
    .courses-philosophy__label,
    .courses-platform-band__label,
    .embed-modal__close,
    .embed-modal__open,
    .embed-modal__sub,
    .featured-item__footer,
    .featured-item__num,
    .filter-row__label,
    .footer__heading,
    .home-press__logo-fallback,
    .journal-entry__cat,
    .mv-card__artist,
    .mv-hero__stat-label,
    .press-card__footer,
    .press-card__outlet,
    .press-kit__type,
    .press-publications__label,
    .proj-tab__count,
    .proj-tab__num,
    .score-badge__meta,
    .swipe-hint,
    .vid-social-head__label,
    .vid-social-head__note,
    .view-toggle-btn {
      font-size: 10px;
    }

    /* ── 10px → 11px ── */
    .archive-card__byline,
    .archive-row__outlet,
    .bag-hero__eyebrow,
    .bag-hero__price-from,
    .bag-leather-card__name,
    .bag-leathers__count,
    .bag-specs__col h3,
    .bag-specs__list dt,
    .bag-story__tag,
    .bio-deep__photo-fallback,
    .bio-hero__meta,
    .bio-hero__portrait-inner::after,
    .btn-press,
    .contact-method__rep-title,
    .courses-grid-head__count,
    .courses-grid__empty,
    .courses-hero__eyebrow,
    .courses-platform-badge,
    .demo-tabs,
    .embed-modal__social-fallback a,
    .faq-hero__eyebrow,
    .home-press__eyebrow,
    .home-press__quote figcaption,
    .home-press__toggle,
    .journal-count,
    .journal-empty__hint,
    .journal-entry__month,
    .journal-filters .fchip,
    .journal-hero__eyebrow,
    .journal-post__back,
    .journal-post__body figure figcaption,
    .journal-post__eyebrow,
    .journal-post__hero-caption,
    .journal-post__meta,
    .journal-post__related-eyebrow,
    .leader-band__item,
    .mv-hero__eyebrow,
    .on-camera-filters__label,
    .press-featured__meta,
    .press-hero__eyebrow,
    .press-kit__eyebrow,
    .press-kit__list,
    .proj-credits__cast-role,
    .proj-credits__eyebrow,
    .proj-credits__label,
    .proj-hero__back,
    .proj-hero__meta,
    .proj-hero__slate,
    .proj-press-filter,
    .proj-press-item__meta,
    .proj-press-item__read,
    .proj-tab,
    .proj-trailer__label,
    .proj-video-card__duration,
    .proj-video-card__meta,
    .proj-videos__section-label,
    .rep-panel__eyebrow,
    .section-head__meta,
    .work-card__img-inner::after,
    .work-card__meta,
    .work-card__slate {
      font-size: 11px;
    }
  }

/* ═══════════════════════════════════════════════════════════════════
   ON LOCATION — Behind-the-Scenes Photo Archive
   ───────────────────────────────────────────────────────────────────
   Editorial layout evoking a photo book or contact sheet. Each
   production is a "reel" — horizontal filmstrip on desktop, vertical
   2-up grid on mobile. Between reels, a typographic slate divider
   recites YEAR · LOCATION · PROJECT in a single wide mono line.

   Design rationale:
   - Black page, cream text — matches the archive feel of a working
     darkroom or edit suite. Distinct from the rest of the site
     (mostly cream-on-cream) so users feel they've entered a different
     room dedicated to photography.
   - Varied frame sizes within each strip create rhythm without feeling
     random — enforced via data-orientation + every-7th "hero" pattern.
   - Photo captions sit below each frame in mono caps. Terse.
   ═══════════════════════════════════════════════════════════════════ */

.on-location,
.view-on-location {
  background: var(--ink);
  color: var(--cream);
  min-height: 100vh;
  min-height: 100svh; /* in-app browser safe */
  /* v2.9.14 — clamp horizontal overflow at the page-section root.
     The reel strips use margin-left/-right: -32px to bleed past the
     parent's padding. At viewport widths where this bleed extends
     past the body width (e.g. desktop ≥ ~1632px where the centered
     max-width:1600px content leaves <32px gutter on each side),
     the strip can push the page's scroll width past the viewport,
     producing a horizontal page scrollbar that appears intermittently
     when the user pans. Clamping overflow-x at the on-location root
     prevents any descendant from triggering page-level horizontal
     scroll regardless of viewport size. The strips themselves still
     scroll horizontally via their own overflow-x: auto. */
  overflow-x: hidden;
}

/* ─── Hero masthead ───────────────────────────────────────────── */

/* The TOC-like index — table of contents for all the reels on the page.
   Listed in order, clickable to jump. Feels like the front-matter of
   a photo book. */
/* Standalone placement: now used AFTER the daily shuffle, outside the
   hero header's narrow wrapper. Needs its own max-width and horizontal
   padding to align with the page grid (matches .on-location__shuffle). */

/* ─── Slate divider (between reels) ───────────────────────────── */

/* ─── Back-to-nav floating button (desktop) ──────────────────────
   Fixed on the right edge of the viewport, appears once the user
   has scrolled past the hero's index nav. Clicking returns them
   to the index so they can pick another production.
   Hidden at mobile breakpoint — mobile uses the sticky dropdown. */

/* Mobile variant of the back-to-nav button. Hidden on desktop by
   default (only the base .on-location__back-to-nav shows there).
   Revealed on mobile via the @media (max-width:720px) block below,
   where the base (desktop) variant gets hidden and the --mobile
   one displays. */

/* ─── EPK Back-to-Index button ───────────────────────────────────
   Same styling pattern as .on-location__back-to-nav but scoped to
   the EPK page (lives outside .on-location, so needs its own class).
   Single variant works across desktop and mobile — EPK hero doesn't
   differ enough between breakpoints to warrant two buttons.
   Visibility is controlled by the .is-visible class (opacity +
   transform). The JS in main.js sets it based on whether the hero's
   jump-nav has scrolled out of view. */
/* Mobile: slightly more compact spacing, bottom offset accounts for
   iOS Safari's floating URL bar. */

/* ─── Mobile inline jump dropdown ────────────────────────────────
   Custom styled dropdown (not a native <select>) that matches the
   project-page section picker pattern. Lives inline in the document
   flow between the Featured Daily Shuffle and the mobile hint —
   stays in place after a pick is made; the page scrolls past it
   naturally as the reel loads below. Hidden on desktop. */
/* SECTION · TAP TO SWITCH eyebrow — ::before so markup stays clean */

/* Show mobile jump on mobile, hide desktop back-to-nav there */

/* ─── Reel section ────────────────────────────────────────────── */
/* Edge fade overlays — left and right 48px-wide gradient bands
   over the strip's visible edges. The strip uses negative margins
   to escape .reel's 32px padding, so the strip extends past the
   .reel's content box. The fades sit on top of the strip's
   bleeding region, masking it down to transparent so it reads as
   "extending past the page" rather than abruptly cut off. */
/* Mobile: no horizontal scroll, no edge fade needed. */
/* Leaving the first-reel opt-out rule in place (now a no-op) in case
   we revisit content-visibility in the future. The selector itself is
   harmless without content-visibility on .reel. */
/* The first reel on the page is always rendered immediately — it's
   the LCP candidate. Opt it out of content-visibility so the browser
   paints it on first pass without waiting for scroll proximity. */

/* ─── Film slate ───────────────────────────────────────────────
   Sits above the reel eyebrow and emulates a 35mm production slate:
   key/value pairs in a chunky monospace, with a light bezel border
   and inset background that reads as a stamped metal surface.

   Inspired by the rhythm of REEL/SCENE/TAKE/FRAMES that appears at
   the top of every clapper. The "Frames" count carries real signal
   (how many photos in this reel) — Scene/Take are decorative but
   anchor the slate visually as a complete object.

   Hidden from screen readers (aria-hidden in the markup) because
   the eyebrow + title beneath carry the same semantic content in
   prose form. The slate is a visual flourish only. */
/* Registration crosshair on the top-right corner of the slate —
   the little cross an AC uses to square the clapperboard to frame.
   Two thin lines forming a "+" via two radial-gradients on
   pseudo-elements. Small and faint enough to feel authentic, not
   decorative. */
/* Scanning bar on the left edge of the slate — like the moving
   light line on a film-scanner or tape-machine edit deck. Slowly
   pulses up and down the slate's height. Adds life to the slate
   without distracting from the numeric values. */
@keyframes reelSlateScan {
  0%, 100% { transform: translateY(0%); }
  50%      { transform: translateY(20%); }
}

/* CAM block — the leftmost slate position, tinted to read as the
   production stamp. Subtle verd-tinted background so the cam ID
   (A-CAM, B-CAM, etc.) pops slightly vs the rest of the slate.
   Mirrors how real clapperboards have the camera ID in a colored
   field distinct from scene/take. */

/* ─── Card state (collapsed reel) ──────────────────────────────
   New default state for On Location reels — before the full body
   is expanded. Shows a filmstrip tease: 1 hero frame + 3 thumbs,
   with a "See all N frames" button below. Clicking the button
   (or the card itself via the global handler) hides this card
   and reveals .reel__body.

   Layout: CSS grid with hero in the first column (taller), 3
   smaller thumbs in the second column stacked. Feels like a single
   mounted film cell rather than a standard photo row. */
/* ─── The filmstrip ───────────────────────────────────────────── */

/* ─── Film scratches + dust overlay on the strip ────────────────
   A pseudo-element layer positioned absolutely over the strip
   with a tall, steep repeating-linear-gradient that renders as
   vertical scratches. The pattern is randomized in opacity and
   position via multiple overlapping gradients. Animated with a
   very slow horizontal drift so scratches appear to creep across
   the reel as if the film is moving through a gate.

   v2.8.3 perf: animation switched from background-position-x to
   transform: translateX. background-position can't be GPU-composited
   so the browser had to repaint the entire strip layer every frame
   — Lighthouse flagged this as one of 12 non-composited animations.
   Transform animations run on the compositor thread, no CPU repaint.
   The pseudo is widened by 200% via the right offset so the drift
   doesn't expose blank edges as it scrolls. */
/* v2.9.15 — taller scrollbar (was 10px) + no inset border so the
   verd handle reads clearly at rest. The strip uses overflow-x:
   scroll (not auto) so the scrollbar is always visible, but a
   thin handle wasn't catching enough attention as a navigation
   affordance. 14px reads as deliberate UI, not residual chrome. */
/* v2.9.17 — force the reel strip scrollbar to be persistently
   visible regardless of macOS "Show scrollbars" system preference.
   macOS Sonoma defaults to "Automatically" which causes webkit
   scrollbars to fade out when the user isn't actively scrolling,
   even when the container has overflow-x: scroll. Setting
   -webkit-appearance: none + explicit display: block + height
   overrides the system's auto-hide. */
/* v2.9.19 — native webkit scrollbar HIDDEN. We now render a
   custom .reel__scrollbar UI below the strip (see template-parts/
   view-on-location.php) that's always visible regardless of macOS
   "Show scrollbars" preference. Hiding the native scrollbar
   prevents a double-scrollbar look on Linux/Windows browsers
   where the native bar would still be drawn alongside ours. */

/* ─── Custom scrollbar UI ──────────────────────────────────────
   Always-visible draggable progress indicator under each reel
   strip. JS module pairs each .reel__scrollbar with its sibling
   .reel__strip via matching data-reel-strip-scrollbar /
   data-reel-strip anchors. Bidirectional: scrolling the strip
   moves the thumb, dragging the thumb scrolls the strip.

   The bar lives in the section's left/right padding so it spans
   the visual width of the reel content area (its track width
   matches the visible strip width). Recalculation happens on
   resize and after each progressive batch-load (which grows the
   strip's scrollWidth). */
/* While dragging, suppress hover state changes anywhere else on
   the page so the cursor stays "grabbing" even if the user's
   pointer leaves the bar momentarily during a fast drag. */

/* Mobile: the strip becomes a vertical 2-col grid (see line 10151
   region) — there's no horizontal scroll, so the custom scrollbar
   has nothing to represent. Hide it. */

/* ─── Individual frames ───────────────────────────────────────── */
/* ─── Pizzazz: SMPTE timecode stamp (desktop only) ─────────────
   Every frame gets a bottom-right stamp reading its "timecode" —
   HH:MM:SS:FF derived from the frame's index offset into the reel.
   Reads like a real post-production deliverable from an editing
   bay rather than a plain 01/02/03 counter. attr(data-tc) is
   set by PHP jk_frame_to_smpte() at render time; the CSS counter
   fallback below handles any frame that lost its data-tc. */
/* Caption sits at the bottom, revealed on hover. Always in mono caps,
   short. If the caption text is long, it ellipsizes — user gets the
   full text in the lightbox. */

/* Orientation variants — grid column spans.
   - landscape: 1 column (340px), 2 rows → ~340×656 slot
   - portrait: 1 column, 1 row → 340×320 slot (wait, that's short)
   Actually: reconsidering. For a filmstrip feel, we want ALL frames
   in the same row, not a 2-row grid. Let me simplify. */

/* ─── Reel footer ─────────────────────────────────────────────── */

/* ─── Empty state ─────────────────────────────────────────────── */

/* ─── Mobile — collapse filmstrip to 2-up vertical grid ────────── */

/* ─── On Location — filter toggle + stream divider + collections ─── */

/* EDITORIAL SHUFFLE — daily-random mosaic above the full reels.
   Gives the page visual presence before the user scrolls into the
   long reels. Mosaic uses asymmetric tile sizes for editorial feel:
   first tile is 2-wide hero, tiles 3 and 5 are 2-tall verticals,
   rest are 1×1. On mobile the grid collapses to 2 columns and the
   spans are reset so everything fits naturally. */
/* Shuffle head matches .on-location__slate rhythm — horizontal
   mono-caps film-can label with middot separators, reading
   "00 · FEATURED · DAILY SHUFFLE · N FRAMES". The "00" numeric
   prefix positions this as the zeroth card before the numbered
   reels below. Top + bottom rules in cream-alpha for the same
   editorial boundary look the slate has. */
/* Per-orientation aspect ratios. The tile shape comes from the
   photo's native orientation (set by PHP via $photo['orientation']),
   not from grid position. This guarantees no portrait gets squished
   into a landscape slot and vice versa. */
/* v2.9.12 — panoramic bucket. Photos with aspect >= 2:1 get this
   modifier — wider tile aspect that respects the photo's actual
   shape instead of cropping to 3:2. Renders at 21:9 nominal aspect
   (close enough to 16:9 / 2:1 / etc that most pano sources fit
   without crop). */

/* ─── CONTACT SHEET TREATMENT ──────────────────────────────────────
   When the shuffle wears the .on-location__shuffle--lighttable
   modifier, the section becomes an editor's contact sheet sitting
   on a charcoal slate workbench. The slate reads as a slightly
   different surface than the surrounding page (so the section
   feels like "the editor's table") without breaking the dark
   editorial palette of the rest of the site.

   v2.9.3 swapped the v2.9.2 cream light-table for charcoal slate
   — the cream surface, however authentic to a real light table,
   read as a different brand entirely against the surrounding
   dark page. Charcoal slate keeps the contact-sheet metaphor
   (prints with white borders + crop marks + frame IDs) while
   matching the site's editorial palette.

   Removing the modifier class restores the prior dark-on-dark
   masonry without requiring any reverts. */

/* Surrounding "studio" treatment — subtle vignette behind the
   slate so it reads as a workbench in a dim editing room. */

/* The slate surface itself — deep charcoal with a subtle inner
   gradient suggesting overhead light catching the slate top. */
/* Subtle slate texture — fine repeating noise to suggest a matte
   editor's slate. Very low opacity so it doesn't compete with
   the prints sitting on top. */

/* Shuffle head sits ABOVE the slate on the studio surface. */

/* Tile inside the slate — the button background goes transparent
   so the print border (.on-location__shuffle-frame) shows
   through. Captions are hidden on contact-sheet tiles per James's
   feedback — album/project metadata moves to the lightbox header. */

/* SWITCH FROM column-count masonry to a true CSS grid on light-
   table mode. The masonry was producing inconsistent stacking
   on mobile (per James's screenshot 4 — photos overlapping). A
   simple 2-or-4-col grid with explicit rows is fully predictable. */

/* CRITICAL OVERRIDE for lighttable mode: cancel the aspect-ratio
   rules from the base masonry styles (.on-location__shuffle-tile
   --wide / --tall / --square in lines 9635-9647). Those rules
   force every tile to a fixed aspect ratio (3/2, 2/3, 1/1) which
   was correct for column-count masonry where tiles flow vertically
   to fill columns. But in fixed CSS Grid, the aspect-ratio fights
   with grid-template-columns: each cell wants both 1fr width AND
   a forced aspect ratio, which can cause the tile to overflow its
   column. The user reported "photos way off the page parameters"
   which is exactly this overflow.

   In lighttable mode, the photo itself (with width=N, height=M
   attributes) provides the aspect ratio naturally via the <img>
   element's intrinsic dimensions. The tile becomes a transparent
   wrapper around the photo + frame border. The grid cell stays
   exactly 1fr wide, the photo fills it width-100%, and height
   adjusts to maintain aspect ratio. No overflow possible. */

/* Contact sheet print border — warm photo paper around the photo,
   like a 4×5 print's white margin. NOT pure white — that read
   as too clinical against the dark slate. */

/* Corner crop / registration marks. Dark ink that reads on the
   warm cream-paper print border. */

/* Frame exposure ID — sits below the print border on the slate.
   Light-on-dark since the slate is dark. */

/* Captions on contact-sheet tiles — REMOVED from face per James's
   feedback. Album/project label is now ONLY in the lightbox
   header so it doesn't clutter the contact sheet aesthetic. */

/* Hover affordance — slight lift, no scale. Real prints don't
   zoom; they get picked up. */

/* Editor's keeper mark — wax-pencil circle + PICK annotation. The
   circle is now SYMMETRIC (inset: -4px) so it stays centered on
   the photo. v2.9.2 had asymmetric extension which made the circle
   sit halfway on/off the print, distracting per James's feedback. */

/* Narrow-viewport treatment for the shuffle head. The desktop version
   is a single-line "00 · FEATURED · DAILY SHUFFLE · N FRAMES" with
   heavy letter-spacing. At <=720px that total width exceeds the
   viewport and gets clipped by overflow:hidden. Here we let it wrap,
   shrink font + gap + letter-spacing to keep the editorial feel. */


/* ─── Edge code ─────────────────────────────────────────────────
   Emulates the Kodak/Fuji edge codes printed between sprocket holes
   on real 35mm stock. Tiny mono caps, very low contrast — meant to
   read as a watermark stamped along the edge of the film, not as
   UI copy. Only renders once the peek strip is unhidden (just like
   the drag hint). Overflow: hidden + white-space: nowrap means
   long strings get clipped gracefully rather than wrapping, and
   the inner span gets a subtle shimmer animation that makes the
   edge code feel alive without calling attention to itself.

   Desktop-only — on mobile the page flattens to a vertical grid
   where sprocket holes + edge codes aren't part of the visual
   language (and screen width is too narrow to render the text
   legibly even if we wanted to). */

/* On mobile, hide the scroll hint since the strip becomes a 2-col
   vertical grid (no horizontal scroll to teach). Mobile uses the
   picker dropdown + IntersectionObserver-driven vertical batch
   reveal in the existing applyMobilePagination path. */

/* Filter toggle: three-button row beneath the hero. All / Productions /
   Collections. Minimal row, mono caps, verd active state. */

/* Filter state hide/show — applied to .on-location root by JS.
   When the article has .is-filtered-productions, only the productions
   stream is visible; same for .is-filtered-collections. Default (no
   filter class) shows both. */

/* Also hide the second-stream divider when filtering to collections
   (the divider only makes sense between both streams). */

/* ─── Mobile-only dropdown-pick reel architecture ───────────────
   On mobile (≤720px), all production + collection reels are hidden
   by default. User must pick from the sticky .on-location__mobile-jump
   dropdown to reveal a single reel at a time. Reduces initial DOM
   weight + image fetch pressure dramatically on bandwidth-constrained
   devices, and the native mobile picker is faster UX than scroll-
   scanning a long stack of cards anyway.

   State transitions:
   - Default mobile state: both streams hidden, hint placeholder visible
   - User picks → article gets data-mobile-pick="reel-slug" + class
     .is-mobile-pick-active; hint fades, picked reel shows expanded
     (skipping the card state)
   - User picks again → data-mobile-pick updates, old reel hides,
     new reel shows

   Desktop is unaffected — these rules only apply in the mobile media
   query and don't fire elsewhere. */

/* Big divider between the two streams. Only shown when both exist.
   Typographic rule with a mono-caps label centered between the rules. */

/* Collection reel — most styles share with .reel (base). A few tweaks
   to mark it visually as belonging to the collections stream. */

/* Hero index second label styling (between Productions list and
   Collections list). Give it a bit more top breathing room. */

/* Mobile adjustments for filter toggle + stream divider */

/* ═══════════════════════════════════════════════════════════════════
   PRINT SAFETY — override content-visibility
   ───────────────────────────────────────────────────────────────────
   Several grid patterns (project gallery tiles, press cards, film
   cards, library cards, journal entries, on-location reels) use
   content-visibility: auto for scroll performance. The browser skips
   layout and paint for offscreen elements until they're near the
   viewport — great for scrolling, but when a user prints the page or
   saves to PDF, some browsers render only what was visible at print
   time and leave the deferred content blank.

   Force all content visible in print mode. The perf benefit only
   matters for interactive scroll; printing is a one-shot render where
   we want every card to be laid out.
   ═══════════════════════════════════════════════════════════════════ */
@media print {
  .proj-gallery-grid figure,
  .archive-grid .archive-card,
  .mv-grid .mv-card,
  #favsCardGrid .favs-film,
  .journal-entry,
  .reel {
    content-visibility: visible !important;
    contain-intrinsic-size: auto !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   DIGITAL EPK — /epk/ view
   Interactive press kit. Reuses the site's editorial tokens heavily
   (display serif, mono eyebrows, verd accent, chalk/ink/cream palette).
   Card treatments borrow from .archive-card + .featured-item but
   diverge where EPK context calls for a more formal, document-like feel.
   ═══════════════════════════════════════════════════════════════════ */

/* ── HERO ── */

/* ── BIOGRAPHY ── */

/* ── FEATURED FILMS ── */

/* Previous work compressed grid inside .epk-film--compact */

/* ── PRESS COVERAGE ── */

/* Filter chips */

/* Press grid */

/* ─── Linked press card (anchor variant) ─────────────────────────
   When the source URL is available, the card is rendered as <a>.
   Reset anchor inheritance, add hover affordance, and surface a
   small "Read →" CTA in the footer so visitors know it's clickable.
   The whole card is the click target, not just the CTA — generous
   tap surface is the point. */
a.epk-press-card {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: border-color 0.18s var(--ease), transform 0.18s var(--ease), box-shadow 0.18s var(--ease);
}
a.epk-press-card:hover {
  border-color: var(--verd);
  transform: translateY(-2px);
  box-shadow: 0 8px 24px -12px rgba(45, 106, 91, 0.35);
}
a.epk-press-card:hover .epk-press-card__headline { color: var(--verd-deep); }
a.epk-press-card:hover .epk-press-card__cta { color: var(--verd); }
a.epk-press-card:hover .epk-press-card__cta-arrow { transform: translateX(3px); }
a.epk-press-card:focus-visible {
  outline: 2px solid var(--verd);
  outline-offset: 3px;
  border-color: var(--verd);
}

/* ── PHILANTHROPY ── */

/* ── CONTACT ── */

/* ── EPK DOWNLOADS ──
   Rendered between Philanthropy and Contact when press_kit_assets
   is populated. Editorial list treatment matching site design language —
   no buttons, just clean typographic rows with file-type badges.
*/

/* ════════════════════════════════════════════════════════════════════
   v2.9.10 — A11Y CONTRAST OVERRIDES
   ════════════════════════════════════════════════════════════════════
   Lighthouse a11y audits flagged a cluster of decorative mono-caps
   labels for failing WCAG AA contrast (4.5:1). Common pattern:
   color:var(--ink) with opacity:0.4 or 0.5 — visually intentional
   "secondary metadata" treatment that comes out around 2.5-3.0:1
   effective contrast against the cream/light backgrounds these labels
   typically sit on.

   Fix: bump opacity to 0.7 (effective ~4.7:1, just over AA threshold)
   for the specific failing classes. Keeping the ink color preserves
   the design language; only the opacity changes. For elements on
   dark backgrounds, the pattern is the same but with cream as the
   base color.

   Affected pages (from Lighthouse runs Apr 25 2026):
   - Press: AS FEATURED IN, filter chip counts, archive year, press
     kit type badges, view-toggle, clear-filters, filter count
   - Courses: AVAILABLE ON label
   - Inquiries: AGENT/MANAGER role labels
   - EPK: REPRESENTATION label, agency names, accolade labels, year
     spans, press card num/meta/author, prev work role labels

   These overrides live at the end of the stylesheet so they win on
   source-order specificity without raising selectors. ───────────── */

.press-publications__label,
.courses-platform-band__label,
.contact-method__rep-title,
.epk-hero__rep-agency,
.epk-press-card__num,
.epk-press-card__meta,
.epk-prev__role,
.epk-film__accolade-label {
  /* Was opacity 0.4-0.6 — failing AA. 0.72 puts effective contrast at
     ~4.6:1 against the cream paper bg. */
  opacity: 0.72;
}

.archive-card__year {
  /* Cream on ink-dark background — 0.4 was failing AA. 0.78 hits AA. */
  opacity: 0.78;
}

.view-toggle-btn {
  /* Inactive view-toggle. The :hover and .active states already
     resolve to opacity 1 with verd color, so they pass; only the
     resting state was failing. */
  opacity: 0.72;
}

.fchip__count {
  /* Was --ink-dim (~#7a756c) with no opacity dampener. The dim color
     on the cream/light background was around 4.0:1 — failing AA by a
     hair. Switch to ink at opacity 0.7 = ~4.6:1. */
  color: var(--ink);
  opacity: 0.72;
}
.fchip.active .fchip__count {
  /* Active-state count needs to invert against the verd fill. Cream
     on verd is high-contrast, no dampening needed here. */
  color: var(--cream);
  opacity: 1;
}

.press-kit__type {
  /* verd-light text on cream paper bg with the alpha border was at
     ~3.5:1. Bumping to verd-deep raises to ~4.7:1 while staying within
     the design palette. The border color is fine — it's decorative. */
  color: var(--verd-deep, #2a4d44);
}

/* CLEAR FILTERS — using verd on cream was passing on most pages
   (~4.7:1) but Lighthouse caught it on press where the surrounding
   text uses ink, making the verd feel underweight. Underline +
   verd-deep gives clearly-affordance + AAA contrast. */
.clear-filters {
  color: var(--verd-deep, #2a4d44);
  text-decoration: underline;
  text-underline-offset: 3px;
}

#filterCount {
  /* The "X RESULTS" indicator above the press archive grid — strong
     enough text (no opacity dampener already) but failing AA because
     it inherits from a parent with reduced opacity. Force full
     opacity here. */
  opacity: 1;
}

/* v2.9.10 — affordance hint for wheel/drag scrolling on the on-location
   reel strips. James reported the desktop strips appeared frozen at 12
   photos because mouse-only users couldn't tell the strip was
   horizontally scrollable. The JS module now hijacks vertical wheel +
   adds click-drag pan; this cursor change tells the user the strip is
   grab-and-drag interactive. Mobile keeps default cursor since the
   strip is a vertical 2-col grid below 900px. */

/* ─── PROJECT PHOTOS PAGE (v2.9.83) ──────────────────────────────────
   Slim photos-only template for /project/{slug}/photos/. Shares the
   gallery grid styling with view-project.php (proj-gallery-grid,
   proj-gallery-tile, etc. — those rules already exist above) but
   needs its own hero strip styling since it's a much simpler header
   than the project page's hero+tabs. */
/* v2.9.99 — Photos page in DARK MODE to match project page styling.
   Previous v2.9.84 cream background made gallery filter tabs invisible
   (those tabs were styled for dark bg per v2.9.8) and felt
   inconsistent with the rest of the site. Now matches /project/x/
   chrome: dark ink background, chalk text, verd-light accents.
   v2.9.101 — Typography aligned to project hero (--display weight 300,
   bigger size clamp, meta-item structure with label/value pairs). */
.project-photos-main {
  padding-top: var(--nav-height, 56px);
  background: var(--ink, #1A1916);
  color: var(--chalk, #F2EDE4);
  min-height: 100vh;
}
.proj-photos-hero {
  border-bottom: 1px solid rgba(242, 237, 228, 0.15);
  padding: 64px 0 56px;
}
.proj-photos-hero__inner {
  max-width: 1500px;
  margin: 0 auto;
  padding: 0 32px;
}
.proj-photos-hero__back {
  /* v2.9.101 — Match .proj-hero__back exactly. Was a generic verd
     link before; now uses the mono caps treatment with cream-alpha
     hover that the project page uses. */
  display: inline-block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd-light, #6DBFAD);
  text-decoration: none;
  margin-bottom: 32px;
  transition: opacity 0.15s ease;
}
.proj-photos-hero__back:hover,
.proj-photos-hero__back:focus-visible {
  text-decoration: underline;
  opacity: 0.8;
}
.proj-photos-hero__title {
  /* v2.9.101 — Mirror .proj-hero__title: --display, weight 300,
     clamp(48px, 8vw, 112px), 0.9 line-height, -0.02em letter spacing.
     Was --serif weight 400 at clamp(36px, 6vw, 72px), which read as
     a different page.

     v2.9.114 — Now uses var(--h-hero-title) instead of the raw clamp.
     The token is clamp(48px, 9vw, 128px) — 9vw vs 8vw means slightly
     steeper scaling at mid-viewport, 128 vs 112 means wider screens
     get a slightly bigger headline. Both improvements move toward the
     token's "this is the hero" definition.

     v2.9.128 — Per-breakpoint min-height reservation. PageSpeed on
     mobile reported CLS 1.022 attributed to .view-project-photos
     (the page wrapper) — root cause is this title element changing
     height when Instrument Serif weight 300 swaps in over the
     Times-fallback rendering. Two-line title at clamp values:
       Mobile (48px × 0.9):  ~86px → reserve 90px
       Tablet (~72px × 0.9): ~130px → reserve 135px
       Desktop (128px × 0.9): ~230px → reserve 234px
     The "Angel of Anywhere — Photos" title wraps to 2 lines on
     mobile so reservations are sized for that worst case. */
  font-family: var(--display);
  font-weight: 300;
  font-size: var(--h-hero-title);
  line-height: 0.9;
  letter-spacing: var(--track-tight);
  color: var(--chalk, #F2EDE4);
  margin: 0 0 28px;
  min-height: 90px;
}
@media (min-width: 700px) {
  .proj-photos-hero__title { min-height: 135px; }
}
@media (min-width: 1100px) {
  .proj-photos-hero__title { min-height: 234px; }
}
.proj-photos-hero__title-sub {
  /* The italic " — Photos" suffix. Match .proj-hero__title em's
     verd-light accent treatment so it reads as the same visual
     language as project page hero italics. */
  font-style: italic;
  font-weight: 400;
  color: var(--verd-light, #6DBFAD);
}
.proj-photos-hero__meta {
  /* v2.9.101 — Match .proj-hero__meta layout: gap 24/40, mono caps,
     small size, wide letter-spacing. Was a separated-by-middot run-on. */
  display: flex;
  flex-wrap: wrap;
  gap: 24px 40px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-mid);
  text-transform: uppercase;
  margin: 0;
}
.proj-photos-hero__meta-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.proj-photos-hero__meta-item .label {
  color: var(--verd-light, #6DBFAD);
  opacity: 0.8;
}
.proj-photos-hero__meta-item .value {
  color: var(--chalk, #F2EDE4);
  font-size: 12px;
  letter-spacing: 0.12em;
}
@media (max-width: 1024px) {
  .proj-photos-hero { padding: 40px 0 36px; }
  .proj-photos-hero__inner { padding: 0 20px; }
  .proj-photos-hero__back { margin-bottom: 24px; }
  .proj-photos-hero__meta { gap: 16px 28px; }
}

/* Wrap below the hero — same grid styling as the project page's
   gallery panel pulls in via .proj-gallery-grid; here we just give
   it some breathing room. */
.proj-photos-wrap {
  max-width: 1500px;
  margin: 0 auto;
  padding: 32px;
}

.proj-photos-empty {
  max-width: 600px;
  margin: 80px auto;
  padding: 0 32px;
  text-align: center;
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.05em;
  /* v2.9.99 — chalk on dark */
  color: var(--chalk, #F2EDE4);
  opacity: 0.7;
}
.proj-photos-empty a {
  /* v2.9.99 — verd-light for AA on dark bg */
  color: var(--verd-light, #6DBFAD);
  text-decoration: underline;
}

@media (max-width: 700px) {
  .proj-photos-hero {
    padding: 32px 0 28px;
  }
  .proj-photos-hero__inner {
    padding: 0 20px;
  }
  .proj-photos-hero__back {
    margin-bottom: 18px;
  }
  .proj-photos-hero__title {
    margin-bottom: 12px;
  }
  .proj-photos-wrap {
    padding: 20px;
  }
}

/* v2.9.89 — "View all N →" links in BTS section header + footer.
   When the project page caps the BTS section to top 100 by AI score,
   these links route to /project/{slug}/photos/ for the full archive. */
.proj-gallery-section__viewall {
  font-family: var(--mono);
  font-size: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--cream);
  text-decoration: underline;
  text-decoration-color: rgba(242, 237, 228, 0.4);
  text-underline-offset: 3px;
  font-weight: 500;
  transition: color 0.15s ease, text-decoration-color 0.15s ease;
}
.proj-gallery-section__viewall:hover,
.proj-gallery-section__viewall:focus-visible {
  color: var(--verd-light);
  text-decoration-color: var(--verd-light);
}

.proj-gallery-section__foot {
  margin-top: 32px;
  padding-top: 20px;
  border-top: 1px solid var(--rule);
  text-align: center;
}
.proj-gallery-section__foot-link {
  display: inline-block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd-light);
  text-decoration: none;
  padding: 14px 28px;
  border: 1px solid rgba(109, 191, 173, 0.3);
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.proj-gallery-section__foot-link:hover,
.proj-gallery-section__foot-link:focus-visible {
  border-color: var(--verd-light);
  background: rgba(109, 191, 173, 0.08);
  color: var(--cream);
}

/* v2.9.90 — Top-of-Gallery "View all N photos →" CTA. Only renders
   when the BTS section is capped. Verd-green primary styling so it's
   visually prominent as the main action — sits ABOVE the filter
   tabs (Stills/BTS/All) since it's the most-likely click for users
   browsing photo-heavy projects. */
.proj-gallery-cta {
  margin: 0 0 32px;
  text-align: center;
}
.proj-gallery-cta__btn {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 16px 32px;
  background: var(--verd);
  border: 1px solid var(--verd);
  color: var(--cream);
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-decoration: none;
  transition: background 0.18s ease, border-color 0.18s ease, transform 0.15s ease;
  cursor: pointer;
}
.proj-gallery-cta__btn:hover,
.proj-gallery-cta__btn:focus-visible {
  background: var(--verd-light);
  border-color: var(--verd-light);
  color: var(--ink);
  transform: translateY(-1px);
  outline: none;
}
.proj-gallery-cta__arrow {
  display: inline-block;
  transition: transform 0.2s ease;
}
.proj-gallery-cta__btn:hover .proj-gallery-cta__arrow {
  transform: translateX(4px);
}
.proj-gallery-cta__note {
  margin: 12px 0 0;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-mid);
  text-transform: uppercase;
  color: var(--cream);
  opacity: 0.55;
}
@media (max-width: 700px) {
  .proj-gallery-cta__btn {
    padding: 14px 24px;
    font-size: 11px;
    letter-spacing: var(--track-mid);
  }
}

/* ─────────────────────────────────────────────────────────────────────
   v2.9.95 — Journal comments
   Editorial print aesthetic to match the rest of the site. No avatars,
   no rounded corners, no gradient backgrounds, no boxy WordPress
   defaults. Mono labels, hairline rules between comments, verd accents
   on interactive elements.
   ─────────────────────────────────────────────────────────────────── */
.jk-comments {
  margin-top: 80px;
  padding-top: 48px;
  border-top: 1px solid var(--rule, rgba(26, 25, 22, 0.15));
}
.jk-comments__inner {
  max-width: 720px;
  margin: 0 auto;
}
.jk-comments__header {
  margin-bottom: 32px;
}
.jk-comments__eyebrow {
  display: block;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Consolas, monospace);
  font-size: 11px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd, #2D6A5B);
  margin-bottom: 8px;
}
.jk-comments__count {
  font-family: var(--serif, Georgia, serif);
  font-size: 28px;
  font-weight: 400;
  letter-spacing: var(--track-subtle);
  margin: 0;
  color: var(--ink, #1A1916);
}

/* Comment list — hairline rule between each entry, no boxes */
.jk-comments__list,
.jk-comments .children {
  list-style: none;
  padding: 0;
  margin: 0;
}
.jk-comments__list > li,
.jk-comments .children > li {
  list-style: none;
  border-top: 1px solid var(--rule, rgba(26, 25, 22, 0.12));
  padding: 24px 0;
}
.jk-comments__list > li:first-child {
  border-top: none;
  padding-top: 0;
}
.jk-comments .children {
  margin-top: 16px;
  padding-left: 24px;
  border-left: 2px solid var(--verd-light, #6DBFAD);
}
.jk-comments .children > li {
  border-top: 1px dashed var(--rule, rgba(26, 25, 22, 0.1));
  padding: 16px 0;
}
.jk-comments .children > li:first-child {
  border-top: none;
  padding-top: 0;
}

.jk-comment__header {
  font-family: var(--mono, monospace);
  font-size: 11px;
  letter-spacing: var(--track-narrow);
  text-transform: uppercase;
  color: var(--ink-dim, #7A756C);
  margin-bottom: 12px;
}
.jk-comment__author {
  color: var(--ink, #1A1916);
  font-weight: 600;
}
.jk-comment__author a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--rule, rgba(26, 25, 22, 0.2));
  transition: border-color 0.15s, color 0.15s;
}
.jk-comment__author a:hover {
  color: var(--verd, #2D6A5B);
  border-bottom-color: var(--verd, #2D6A5B);
}
.jk-comment__sep {
  margin: 0 8px;
  color: var(--ink-dim, #7A756C);
  opacity: 0.5;
}
.jk-comment__date {
  font-weight: 400;
}
.jk-comment__pending {
  margin-left: 12px;
  font-style: italic;
  color: #b07c1a;
  text-transform: none;
  letter-spacing: 0;
}
.jk-comment__body {
  font-family: var(--serif, Georgia, serif);
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink, #1A1916);
}
.jk-comment__body p {
  margin: 0 0 12px;
}
.jk-comment__body p:last-child {
  margin-bottom: 0;
}
.jk-comment__body a {
  color: var(--verd, #2D6A5B);
  text-decoration: underline;
  text-decoration-color: rgba(45, 106, 91, 0.4);
  text-underline-offset: 3px;
}
.jk-comment__footer {
  margin-top: 12px;
  font-family: var(--mono, monospace);
  font-size: 11px;
  letter-spacing: var(--track-narrow);
  text-transform: uppercase;
}
.jk-comment__reply a,
.jk-comment__edit a {
  color: var(--verd, #2D6A5B);
  text-decoration: none;
  margin-right: 16px;
  transition: color 0.15s;
}
.jk-comment__reply a:hover,
.jk-comment__edit a:hover {
  color: var(--ink, #1A1916);
}

.jk-comments__nav {
  margin: 32px 0;
  display: flex;
  justify-content: space-between;
  font-family: var(--mono, monospace);
  font-size: 11px;
  letter-spacing: var(--track-mid);
  text-transform: uppercase;
}
.jk-comments__nav a {
  color: var(--verd, #2D6A5B);
  text-decoration: none;
}
.jk-comments__nav a:hover {
  color: var(--ink, #1A1916);
}

.jk-comments__closed {
  font-family: var(--mono, monospace);
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--ink-dim, #7A756C);
  font-style: italic;
}

/* Comment form — flat, editorial, full-width inputs */
.jk-comments__form {
  margin-top: 56px;
  padding-top: 40px;
  border-top: 1px solid var(--rule, rgba(26, 25, 22, 0.15));
}
.jk-comments__form .comment-reply-title {
  font-family: var(--serif, Georgia, serif);
  font-size: 22px;
  font-weight: 400;
  margin: 0 0 20px;
  color: var(--ink, #1A1916);
}
.jk-comments__form .comment-reply-title small a {
  font-size: 12px;
  margin-left: 12px;
  color: var(--verd, #2D6A5B);
  font-family: var(--mono, monospace);
  letter-spacing: var(--track-narrow);
  text-transform: uppercase;
  text-decoration: none;
}
.jk-comments__field {
  margin: 0 0 16px;
}
.jk-comments__field label {
  display: block;
  font-family: var(--mono, monospace);
  font-size: 10px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-mid, #3D3A34);
  margin-bottom: 6px;
}
.jk-comments__req {
  color: var(--verd, #2D6A5B);
}
.jk-comments__hint {
  color: var(--ink-dim, #7A756C);
  text-transform: none;
  letter-spacing: 0;
  margin-left: 6px;
  font-size: 10px;
}
.jk-comments__field input[type="text"],
.jk-comments__field input[type="email"],
.jk-comments__field input[type="url"],
.jk-comments__field textarea {
  width: 100%;
  padding: 12px 14px;
  font-family: var(--serif, Georgia, serif);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink, #1A1916);
  background: var(--chalk, #F2EDE4);
  border: 1px solid var(--rule, rgba(26, 25, 22, 0.2));
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
  transition: border-color 0.15s, background 0.15s;
}
.jk-comments__field input:focus,
.jk-comments__field textarea:focus {
  outline: none;
  border-color: var(--verd, #2D6A5B);
  background: #fff;
}
.jk-comments__field--cookies label {
  display: inline;
  font-family: var(--sans, system-ui, sans-serif);
  font-size: 13px;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink-mid, #3D3A34);
  margin: 0;
}
.jk-comments__field--cookies input[type="checkbox"] {
  margin-right: 8px;
  vertical-align: middle;
}

.jk-comments__submit-row {
  margin-top: 24px;
}
.jk-comments__submit,
.jk-comments__form input[type="submit"],
.jk-comments__form .submit {
  font-family: var(--mono, monospace);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  padding: 14px 28px;
  background: var(--verd, #2D6A5B);
  color: var(--cream, #F2EDE4);
  border: 1px solid var(--verd, #2D6A5B);
  border-radius: 0;
  cursor: pointer;
  transition: background 0.18s, border-color 0.18s, transform 0.15s;
}
.jk-comments__submit:hover,
.jk-comments__form input[type="submit"]:hover,
.jk-comments__form .submit:hover {
  background: var(--verd-light, #6DBFAD);
  border-color: var(--verd-light, #6DBFAD);
  color: var(--ink, #1A1916);
  transform: translateY(-1px);
}

/* Reply-form context (shown when replying to a specific comment) */
#cancel-comment-reply-link {
  margin-left: 12px;
  font-size: 11px;
  color: var(--ink-dim, #7A756C);
}

@media (max-width: 700px) {
  .jk-comments {
    margin-top: 56px;
    padding-top: 32px;
  }
  .jk-comments__count {
    font-size: 22px;
  }
  .jk-comments__form .comment-reply-title {
    font-size: 18px;
  }
  .jk-comments .children {
    padding-left: 14px;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   v2.9.95 — Standalone pages (Privacy Policy, Terms, etc.)
   Uses page.php which renders the WP editor content with the same
   editorial print aesthetic as journal-post. Mirrors .journal-post__*
   structure intentionally so the typography reads consistent with
   long-form content elsewhere on the site.
   ─────────────────────────────────────────────────────────────────── */
.standalone-page {
  max-width: 720px;
  margin: 0 auto;
  padding: 120px 32px 80px;
}
.standalone-page__header {
  margin-bottom: 56px;
  padding-bottom: 32px;
  border-bottom: 1px solid var(--rule, rgba(26, 25, 22, 0.15));
}
.standalone-page__eyebrow {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Consolas, monospace);
  font-size: 11px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd, #2D6A5B);
  margin-bottom: 16px;
}
.standalone-page__title {
  font-family: var(--serif, "Instrument Serif", Georgia, serif);
  font-size: clamp(36px, 5vw, 56px);
  font-weight: 300;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-card);
  margin: 0 0 20px;
  color: var(--ink, #1A1916);
}
.standalone-page__meta {
  font-family: var(--mono, monospace);
  font-size: 11px;
  letter-spacing: var(--track-narrow);
  text-transform: uppercase;
  color: var(--ink-dim, #7A756C);
}
.standalone-page__meta time {
  color: var(--ink-mid, #3D3A34);
  margin-left: 4px;
}

/* Body content — match journal-post body typography. WP-generated
   content lands here as bare h2/h3/p/ul/ol/blockquote/a, so we style
   each element directly rather than relying on Gutenberg block classes. */
/* Body content — match journal-post body typography. WP-generated
   content lands here as bare h2/h3/p/ul/ol/blockquote/a, so we style
   each element directly rather than relying on Gutenberg block classes.

   v2.9.308 — Added `contain: layout` to isolate the prose container
   from reflow events caused by late-loading site chrome (nav submenu
   portaling, SPAI background prep, font swap). PageSpeed flagged
   /photos/license/ at CLS 0.375 desktop with the body container as
   the culprit; the long prose accumulated millimeter-scale shifts
   across every paragraph during font swap into a high cumulative
   score. `contain: layout` tells the browser this element's internal
   layout is independent — shifts inside don't propagate up, and
   chrome changes outside don't propagate down. */
.standalone-page__body {
  font-family: var(--serif, "Instrument Serif", Georgia, serif);
  font-size: 18px;
  line-height: 1.7;
  color: var(--ink, #1A1916);
  contain: layout;
}
.standalone-page__body p {
  margin: 0 0 22px;
}
.standalone-page__body p:last-child {
  margin-bottom: 0;
}
.standalone-page__body h2 {
  font-family: var(--serif, "Instrument Serif", Georgia, serif);
  font-size: 28px;
  font-weight: 400;
  letter-spacing: var(--track-subtle);
  line-height: var(--lh-display-medium);
  margin: 56px 0 16px;
  padding-top: 24px;
  border-top: 1px solid var(--rule, rgba(26, 25, 22, 0.12));
  color: var(--ink, #1A1916);
}
.standalone-page__body h2:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}
.standalone-page__body h3 {
  font-family: var(--serif, "Instrument Serif", Georgia, serif);
  font-size: 22px;
  font-weight: 400;
  line-height: 1.25;
  margin: 36px 0 12px;
  color: var(--ink, #1A1916);
}
.standalone-page__body h4 {
  font-family: var(--mono, monospace);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: var(--track-mid);
  text-transform: uppercase;
  margin: 28px 0 10px;
  color: var(--verd, #2D6A5B);
}
.standalone-page__body ul,
.standalone-page__body ol {
  margin: 0 0 22px;
  padding-left: 24px;
}
.standalone-page__body li {
  margin-bottom: 8px;
}
.standalone-page__body li:last-child {
  margin-bottom: 0;
}
.standalone-page__body blockquote {
  margin: 28px 0;
  padding: 4px 0 4px 24px;
  border-left: 2px solid var(--verd-light, #6DBFAD);
  font-style: italic;
  color: var(--ink-mid, #3D3A34);
}
.standalone-page__body a {
  color: var(--verd, #2D6A5B);
  text-decoration: underline;
  text-decoration-color: rgba(45, 106, 91, 0.4);
  text-underline-offset: 3px;
  transition: color 0.15s, text-decoration-color 0.15s;
}
.standalone-page__body a:hover {
  color: var(--ink, #1A1916);
  text-decoration-color: var(--ink, #1A1916);
}
.standalone-page__body strong {
  font-weight: 600;
  color: var(--ink, #1A1916);
}
.standalone-page__body code {
  font-family: var(--mono, monospace);
  font-size: 0.9em;
  padding: 2px 6px;
  background: var(--chalk-mid, #E8E1D6);
  border-radius: 0;
}
.standalone-page__body hr {
  border: 0;
  border-top: 1px solid var(--rule, rgba(26, 25, 22, 0.15));
  margin: 48px 0;
}
.standalone-page__body img {
  max-width: 100%;
  height: auto;
  margin: 28px 0;
}

@media (max-width: 700px) {
  .standalone-page {
    padding: 88px 20px 56px;
  }
  .standalone-page__header {
    margin-bottom: 40px;
    padding-bottom: 24px;
  }
  .standalone-page__body {
    font-size: 17px;
  }
  .standalone-page__body h2 {
    font-size: 24px;
    margin: 40px 0 12px;
    padding-top: 20px;
  }
  .standalone-page__body h3 {
    font-size: 19px;
    margin: 28px 0 10px;
  }
}

/* v2.9.307 — Photos License page. Same .standalone-page__* base
   styles apply (typography, max-width, header). The .photos-license__*
   selectors below add the licensing-specific bits: a lede paragraph
   in slightly larger type, section headings styled like h2 but
   distinct because we're rendering from a repeater (no automatic
   first-child treatment), and a contact block at the bottom that
   visually anchors the call-to-action. */
.photos-license__lede {
  font-family: var(--serif, "Instrument Serif", Georgia, serif);
  font-size: 22px;
  line-height: 1.55;
  color: var(--ink, #1A1916);
  margin: 0 0 48px;
  padding-bottom: 32px;
  border-bottom: 1px solid var(--rule, rgba(26, 25, 22, 0.12));
}
.photos-license__section {
  margin: 48px 0;
}
.photos-license__section-heading {
  font-family: var(--serif, "Instrument Serif", Georgia, serif);
  font-size: 26px;
  font-weight: 400;
  letter-spacing: var(--track-subtle);
  line-height: var(--lh-display-medium);
  margin: 0 0 16px;
  color: var(--ink, #1A1916);
}
.photos-license__section-body {
  font-family: var(--serif, "Instrument Serif", Georgia, serif);
  font-size: 18px;
  line-height: 1.7;
  color: var(--ink, #1A1916);
}
.photos-license__section-body p {
  margin: 0 0 18px;
}
.photos-license__section-body p:last-child {
  margin-bottom: 0;
}
.photos-license__section-body ul,
.photos-license__section-body ol {
  margin: 0 0 18px;
  padding-left: 24px;
}
.photos-license__section-body li {
  margin-bottom: 6px;
}
.photos-license__section-body a {
  color: var(--verd, #2D6A5B);
  text-decoration: underline;
  text-decoration-color: rgba(45, 106, 91, 0.4);
  text-underline-offset: 3px;
  transition: color 0.15s, text-decoration-color 0.15s;
}
.photos-license__section-body a:hover {
  color: var(--ink, #1A1916);
  text-decoration-color: var(--ink, #1A1916);
}
.photos-license__contact {
  margin: 64px 0 0;
  padding: 40px 0 0;
  border-top: 1px solid var(--rule, rgba(26, 25, 22, 0.15));
}
.photos-license__contact-email {
  font-family: var(--mono, monospace);
  font-size: 14px;
  letter-spacing: var(--track-narrow);
  margin: 24px 0 12px;
}
.photos-license__contact-email a {
  color: var(--verd, #2D6A5B);
  text-decoration: underline;
  text-decoration-color: rgba(45, 106, 91, 0.4);
  text-underline-offset: 3px;
  transition: color 0.15s, text-decoration-color 0.15s;
}
.photos-license__contact-email a:hover {
  color: var(--ink, #1A1916);
  text-decoration-color: var(--ink, #1A1916);
}
.photos-license__response-time {
  font-family: var(--mono, monospace);
  font-size: 11px;
  letter-spacing: var(--track-narrow);
  text-transform: uppercase;
  color: var(--ink-dim, #7A756C);
  margin: 0;
}

/* v2.9.307 — Small licensing CTA used on /on-location/ and on
   /project/{slug}/photos/ pages. Single-line link with subtle
   underline and an arrow that translates on hover. Lives below
   the gallery as an unobtrusive footer-style nudge. */
.photos-license-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 64px auto 24px;
  padding: 24px 32px;
  font-family: var(--mono, monospace);
  font-size: 11px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-dim, #7A756C);
  text-align: center;
  border-top: 1px solid var(--rule, rgba(26, 25, 22, 0.08));
  max-width: 720px;
}
.photos-license-cta a {
  color: var(--verd, #2D6A5B);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  transition: color 0.2s var(--ease, ease);
}
.photos-license-cta a:hover {
  color: var(--ink, #1A1916);
}
.photos-license-cta__arrow {
  display: inline-block;
  width: 10px;
  height: 10px;
  transition: transform 0.22s var(--ease, ease);
  will-change: transform;
}
.photos-license-cta a:hover .photos-license-cta__arrow {
  transform: translateX(3px);
}
/* On dark surfaces (the /on-location/ reel page uses a dark
   background), the CTA's default ink-dim color is invisible. The
   .photos-license-cta--dark modifier flips the palette to read on
   the dark surface while keeping the verd accent. */
.photos-license-cta--dark {
  color: rgba(242, 237, 228, 0.5);
  border-top-color: rgba(242, 237, 228, 0.1);
}
.photos-license-cta--dark a {
  color: var(--verd-light, #6DBFAD);
}
.photos-license-cta--dark a:hover {
  color: var(--cream, #F2EDE4);
}
@media (max-width: 700px) {
  .photos-license__lede { font-size: 19px; }
  .photos-license__section-heading { font-size: 22px; }
  .photos-license__section-body { font-size: 17px; }
}

/* v2.9.96 — Spam protection captcha field. Inherits form styling
   from .jk-comments__field. Honeypot has its own hide rule inline
   on the element since it must be defensive against any reset CSS
   that might unhide it accidentally. */
.jk-comments__field--captcha label {
  /* Make the question stand out a bit — it's the visible challenge */
  color: var(--ink, #1A1916);
  font-size: 11px;
}
.jk-comments__field--captcha input[type="text"] {
  max-width: 120px;
  text-align: center;
  font-family: var(--mono, monospace);
  font-size: 16px;
  letter-spacing: 0.05em;
}

/* ─────────────────────────────────────────────────────────────────────
   v2.9.100 — Photos page dark mode (full coverage)
   The previous v2.9.99 attempt only set background on .project-photos-main,
   but the .view parent wrapper paints --chalk over our content (line 609)
   and the body itself is --chalk too. So the page rendered cream despite
   the inner element being dark. Force dark on body + view + main here
   under the .jk-project-photos-page body class so it doesn't bleed
   onto other pages.
   ─────────────────────────────────────────────────────────────────── */
body.jk-project-photos-page {
  background: var(--ink, #1A1916) !important;
  color: var(--chalk, #F2EDE4);
}
body.jk-project-photos-page .view-project-photos {
  background: var(--ink, #1A1916) !important;
}
body.jk-project-photos-page .project-photos-main {
  background: var(--ink, #1A1916) !important;
  color: var(--chalk, #F2EDE4);
}
/* Footer/nav cream surfaces inside the photos page should NOT be
   overridden — they get their own backgrounds via existing rules
   (template-parts/nav.php, template-parts/footer.php). The selectors
   above target only the body/view/main containers, not nested chrome. */

/* ─────────────────────────────────────────────────────────────────────
   v2.9.103 — Explicit body backgrounds per page type for mobile safety.
   The .view rule at line 609 paints --chalk, but on iOS Safari the
   body itself shows through where any sub-pixel gap exists between
   stacked sections. James reported the bag page rendering "white" on
   mobile — chalk (#F2EDE4) is so close to pure white that any tiny
   bleed or display-profile shift can flatten it. Setting the body
   bg per-page-type on mobile guarantees the chalk warm-paper tone
   stays consistent regardless of which inner element is the topmost
   painter at any moment.
   ─────────────────────────────────────────────────────────────────── */
@media (max-width: 1024px) {
  body.page-template-page-bag,
  body.page-bag,
  body.page-films,
  body.page-press,
  body.page-courses,
  body.page-on-location,
  body.page-inquiries,
  body.page-library {
    background: var(--chalk, #F2EDE4);
  }
  /* Same for the parent .view containers when active */
  body.page-template-page-bag .view-bag.active,
  body.page-films .view-films.active,
  body.page-press .view-press.active,
  body.page-courses .view-courses.active,
  body.page-inquiries .view-contact.active,
  body.page-library .view-library.active {
    background: var(--chalk, #F2EDE4);
  }
}

/* ─────────────────────────────────────────────────────────────────────
   v2.9.103 — Outlet logos on project press tab + EPK press cards
   The project page renders on dark (--ink) and most outlet logos are
   designed for light backgrounds. Sit them on a chalk-colored pill so
   their original colors read without filter manipulation. Constrains
   to 120×48 max so even tall logos (Hollywood Reporter) stay visually
   compact alongside the press item text. EPK press cards already
   render on chalk so logos there sit directly without the pill.
   ─────────────────────────────────────────────────────────────────── */
.proj-press-item__logo {
  display: inline-block;
  background: var(--chalk, #F2EDE4);
  padding: 8px 14px;
  border-radius: 4px;
  margin-bottom: 14px;
  /* Subtle border so on lighter areas the pill still reads as a
     contained badge rather than a floating image */
  border: 1px solid rgba(45, 106, 91, 0.15);
  /* Constrain max width so a wide logo doesn't span the whole card */
  max-width: 160px;
  line-height: 0;
}
.proj-press-item__logo img {
  display: block;
  width: auto;
  height: 32px;       /* fixed display height — 32px reads comfortably */
  max-width: 100%;
  object-fit: contain;
  /* Drops the pure-black logos to a dark grey so they don't punch
     out against the chalk pill — softens the editorial feel without
     losing readability. Skip for color logos via mix-blend-mode? Too
     fragile; let original colors come through. */
}
.proj-press-item:hover .proj-press-item__logo {
  border-color: rgba(45, 106, 91, 0.4);
}

/* EPK press cards already sit on chalk — logo can render directly,
   no pill needed */
.epk-press-card__logo {
  display: block;
  margin-bottom: 12px;
  height: 28px;
  width: auto;
  max-width: 140px;
  object-fit: contain;
}

/* ═══════════════════════════════════════════════════════════════════
   v2.9.104 — GLOBAL HERO CLS RESERVATIONS

   Single, centralized block of min-height reservations for every hero
   pattern in the theme. Replaces the per-page spot fixes that were
   accumulating across recent versions (.proj-hero__content,
   .bio-hero__name, etc).

   Why one block:
   - Font-swap CLS is a SITEWIDE pattern. Instrument Serif (display)
     loads via font-display:swap, replacing Times fallback. Even with
     the metric-matched fallback @font-face declarations
     (ascent-override 84%, size-adjust 99%), at large display sizes
     and with synthesized weight-300 (Instrument Serif only ships at
     400), the swap visibly shifts text by 2-6px.
   - Geist swap from Arial fallback also shifts body text used in
     hero-lede paragraphs.
   - Per-element fixes left gaps. Browsing through Lighthouse reports
     across home, films, project, on-location, photos, journal,
     library, courses, press, inquiries showed every hero with text
     shifting somewhere between 0.005 and 0.155 CLS.

   How the values are calculated:
   - Title min-height = font-size × line-height × line-count
   - For display titles using --h-hero-title clamp(48px, 9vw, 128px):
       Mobile  ≤700px:    48px × 1 line  × 0.95 = ~46px (reserve 96px for 2-line wraps)
       Tablet  ≤1100px:   88px × 2 lines × 0.95 = ~167px
       Desktop ≥1100px:   128px × 2 lines × 0.95 = ~243px
   - For lede paragraphs using --h-lede clamp(18px, 1.6vw, 22px):
       Mobile:  18px × 3 lines × 1.5 = ~81px (reserve 90px)
       Tablet:  20px × 3 lines × 1.5 = ~90px
       Desktop: 22px × 3 lines × 1.5 = ~99px

   The reservation pattern: assume worst-case wrapping at each
   breakpoint, lock surrounding layout. Even if title wraps to fewer
   lines, the title element itself doesn't grow shorter when the web
   font loads — it just renders as before. Layout below stays stable.

   Notes:
   - .proj-hero__content and .bio-hero__name still have their previous
     reservations; this block ADDS reservations for the others. The
     dual coverage on bio-hero/proj-hero is harmless (Inner reservation
     just stops layout traversal).
   - Hero-lede reservations are critical because the lede is the LCP
     element on home + several other pages. font-swap on the lede
     changes paragraph height which shifts everything below.
   ═══════════════════════════════════════════════════════════════ */

/* ─── Bio hero (home) — REPLACES & EXTENDS v2.9.103 reservation ─── */
/* The previous v2.9.103 fix reserved only .bio-hero__name. CLS
   regressed to 0.155 because .bio-hero__lede's font-swap was still
   shifting the section below. Reserve the lede too.

   v2.9.114 — bumped mobile reservation 90→120px because PageSpeed
   reported bio-hero__text mobile CLS at 0.295 — the prior 90px floor
   covered ~4 lines of text, but the lede is editor-supplied and can
   wrap to 5+ lines on narrow phones (375px viewport). 120px covers
   5 lines at mobile font-size 15px × line-height 1.5 = 112.5px with
   room to spare. The over-reservation costs <30px of empty space if
   the actual content is shorter, but eliminates the layout shift
   when the font swaps from synthesized to native weight-300.

   v2.9.130 — Mobile CLS regressed to 0.414 (PageSpeed Apr 28). Two
   shifts of 0.259 + 0.155 both attributed to .bio-hero__text. Root
   cause: .bio-hero__roles, .bio-hero__meta, and the lede ITSELF were
   still shifting on font-swap because:
     1. .bio-hero__roles had no reservation. Mono-font swap from
        Menlo (fallback) to Geist Mono moves baseline by ~2-3px,
        and at 11px size that's a measurable shift in a 20-character
        single-line element.
     2. .bio-hero__meta had no reservation. Same mono-swap issue,
        and on mobile the meta items wrap from 1 line to 2 lines
        depending on content length.
     3. The 120px lede reservation undersized real-world content —
        bio lede in production is ~135px tall on a 375px viewport
        with the editor's actual prose. Bump to 160px mobile.
   Reservations now cover every font-swap surface inside the
   bio-hero__text container, so the parent's overall height stays
   stable from first paint through font-loaded paint. */
/* v2.9.155 — Mobile floor bumped from 160px to 240px after PageSpeed
   Apr 28 2026 8:04PM showed CLS 0.155 (improved from 0.595 but not
   yet sub-0.05). Layout-shift culprit was still .bio-hero__text via
   instrument-serif-400.woff2 swap. v2.9.154 fixed the centering
   amplifier; this fix sizes the lede floor large enough to actually
   contain the lede content in both fallback and final renderings.

   Live lede content (~280 chars: "Telling stories about loss and
   identity, directing and producing films since 2006. Recent work
   includes The American Question (100% fresh on Rotten Tomatoes) and
   The Sound of Identity (91% fresh).") at 15px / line-height 1.5 on a
   ~340px content area renders to ~9 lines = ~203px. The previous
   160px floor was below natural content height in BOTH renderings,
   so it never engaged and font-swap reflow cost was unmasked. 240px
   sits above worst-case rendered height of either font, so the
   reservation actually holds and font-swap reflow happens inside it.

   The cost: up to ~37px of empty space below the lede when content
   is shorter than the worst case. Acceptable trade for sub-0.05 CLS
   on the home page LCP-adjacent element.

   v2.9.157 EXPERIMENT (REVERTED v2.9.158): tried tightening to 220px
   to bring meta block into iPhone viewport. PageSpeed showed CLS
   regressed 0 → 0.87, two shifts on .bio-hero__text totaling 0.87.
   Indicates the actual fallback rendering is between 220-240px, so
   220px is below the floor and 240px is above it. The 20px window
   we estimated as safe was wrong. Reverted to 240px to restore CLS:0.
   Future attempts to recover the visual fix should look at content
   above the lede (name/roles padding) rather than the lede floor. */
.bio-hero__lede { min-height: 240px; }
@media (min-width: 700px)  { .bio-hero__lede { min-height: 95px; } }
@media (min-width: 1100px) { .bio-hero__lede { min-height: 99px; } }

/* v2.9.130 — Bio-hero roles bar. Single-line on desktop, may wrap to
   two lines on mobile depending on role count + width. We reserve
   for 2 lines on mobile (52px = 2× 22px line-height + 8px padding)
   and 1 line on tablet+ (32px). */
.bio-hero__roles { min-height: 52px; }
@media (min-width: 700px) { .bio-hero__roles { min-height: 32px; } }

/* v2.9.130 — Bio-hero meta block. flex-wrap with 24px row-gap and
   ~38px row height per meta-item (label 14px + 4px gap + value 14px
   + line-height padding). With 2 default items they fit on one row
   at all viewports above 360px. Reserve enough for 2 rows on mobile
   in case content is longer or viewport narrower. */
.bio-hero__meta { min-height: 100px; }
@media (min-width: 700px) { .bio-hero__meta { min-height: 50px; } }

/* v2.9.231 — parent container floor (mirrors critical.css). The per-child
   floors above are reservations (minimums), not caps. PSI mobile reported
   CLS 0.818 attributed to .bio-hero__text itself in May 2026 because each
   child rendered a few pixels taller than its floor on font-swap, and
   four children's worth of residual delta accumulates on the parent.
   Adding min-height to the parent locks the outer box and contains the
   shift. Math computed from sum of child floors + their margin-bottoms
   plus a safety buffer for font-swap delta. See critical.css for the
   per-breakpoint derivation.

   v2.9.232 — desktop floor tightened 560→540 alongside flipping .bio-hero
   from align-items:center to flex-start (which removed the LCP-candidate-
   repositioning amplification that the larger floor was protecting against). */
.bio-hero__text { min-height: 600px; }
@media (min-width: 700px)  { .bio-hero__text { min-height: 480px; } }
@media (min-width: 1100px) { .bio-hero__text { min-height: 540px; } }

/* ─── Films / Music Videos / Inquiries (mv-hero pattern) ─── */
.mv-hero__title { min-height: 96px; }
@media (min-width: 700px)  { .mv-hero__title { min-height: 167px; } }
@media (min-width: 1100px) { .mv-hero__title { min-height: 243px; } }
.mv-hero__lede { min-height: 90px; }
@media (min-width: 1100px) { .mv-hero__lede { min-height: 99px; } }

/* ─── Press hero ─── */
.press-hero__title { min-height: 96px; }
@media (min-width: 700px)  { .press-hero__title { min-height: 167px; } }
@media (min-width: 1100px) { .press-hero__title { min-height: 243px; } }
.press-hero__lede { min-height: 90px; }
@media (min-width: 1100px) { .press-hero__lede { min-height: 99px; } }

/* ─── Courses hero ─── */
.courses-hero__title { min-height: 96px; }
@media (min-width: 700px)  { .courses-hero__title { min-height: 167px; } }
@media (min-width: 1100px) { .courses-hero__title { min-height: 243px; } }
.courses-hero__lede { min-height: 90px; }
@media (min-width: 1100px) { .courses-hero__lede { min-height: 99px; } }

/* ─── Journal hero ─── */
.journal-hero__title { min-height: 96px; }
@media (min-width: 700px)  { .journal-hero__title { min-height: 167px; } }
@media (min-width: 1100px) { .journal-hero__title { min-height: 243px; } }
.journal-hero__lede { min-height: 90px; }
@media (min-width: 1100px) { .journal-hero__lede { min-height: 99px; } }

/* ─── Favorites / Library hero ─── */
.favs-hero__title { min-height: 96px; }
@media (min-width: 700px)  { .favs-hero__title { min-height: 167px; } }
@media (min-width: 1100px) { .favs-hero__title { min-height: 243px; } }
.favs-hero__lede { min-height: 90px; }
@media (min-width: 1100px) { .favs-hero__lede { min-height: 99px; } }

/* ─── On Location hero ─── */
.on-location__hero-title { min-height: 96px; }
@media (min-width: 700px)  { .on-location__hero-title { min-height: 167px; } }
@media (min-width: 1100px) { .on-location__hero-title { min-height: 243px; } }
.on-location__hero-lede { min-height: 90px; }
@media (min-width: 1100px) { .on-location__hero-lede { min-height: 99px; } }

/* ─── Standalone page (page.php) ─── */
/* Smaller scale: clamp(36px, 5vw, 56px) line-height 1.05
   Mobile:  36 × 1 × 1.05 = ~38px  (reserve 60 for 2-line wraps)
   Desktop: 56 × 2 × 1.05 = ~118px */
.standalone-page__title { min-height: 60px; }
@media (min-width: 1100px) { .standalone-page__title { min-height: 118px; } }

/* ─── mv-grid-section (films page list area) ─── */
/* Lighthouse reported 0.087 desktop CLS on .mv-grid-section__inner
   from the films page. Filter-chip row ('ALL 8 / DOC SHORT 1 / etc')
   reflows when fonts swap. Reserve the chip row height. */
.mv-filters {
  min-height: 36px;
}
@media (max-width: 700px) {
  .mv-filters { min-height: 28px; }
}

/* ─── v2.9.107 — Press archive card cleanup ──────────────────────────
   Per James: keep the .press-publications "Featured publications" strip
   on /press/ (it appears on both home and press, which is intended).
   Hide the visual logo treatments on:
   - .archive-card__outlet-logo: per-card thumbnails on the press archive list
   - .proj-press-item__logo: per-card outlet logo on project page press tab
   The metadata stays in the DB so schema/EPK keep working. */
.archive-card__outlet-logo,
.proj-press-item__logo { display: none; }

/* ─── Press social rail scrollbar ──────────────────────────────────
   Replaces the overlaid chevron arrows that obscured video edges on
   desktop with an on-location-style draggable thumb scrollbar BELOW
   the row. The chevrons stay on project pages (dark cards absorb them
   visually). Press-only via .vid-social-row--page scope.

   Light-theme palette: translucent ink track, verd thumb. Mirrors the
   .reel__scrollbar pattern in on-location.css but recolored for the
   cream press background. */
.vid-social-row--page-wrap { position: relative; }
.vid-social-row--page-wrap .vid-social-arrow { display: none !important; }

.vid-social-row__scrollbar {
  position: relative;
  width: 100%;
  height: 6px;
  margin-top: 12px;
  margin-bottom: 4px;
  background: rgba(26, 25, 22, 0.08);
  border-radius: 3px;
  overflow: hidden;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
  transition: background 0.2s var(--ease);
}
.vid-social-row__scrollbar:hover { background: rgba(26, 25, 22, 0.14); }
.vid-social-row__scrollbar-thumb {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 80px;
  background: var(--verd);
  border-radius: 3px;
  cursor: grab;
  transition: background 0.18s var(--ease);
  will-change: transform;
}
.vid-social-row__scrollbar-thumb:hover { background: var(--verd-light); }
.vid-social-row__scrollbar.is-dragging .vid-social-row__scrollbar-thumb,
.vid-social-row__scrollbar-thumb:active {
  cursor: grabbing;
  background: var(--verd-light);
}
.vid-social-row__scrollbar.is-dragging,
.vid-social-row__scrollbar.is-dragging * { cursor: grabbing !important; }
@media (max-width: 1024px) {
  .vid-social-row__scrollbar { display: none; }
}

/* ─── v2.9.110 — On-Camera grid: desktop single-row treatment ─────────
   On the press page, the "On Camera" video grid (#onCameraGrid)
   previously displayed as a multi-row CSS grid on desktop. Per James:
   collapse to a single horizontal row with a draggable scrollbar
   below, matching the "Short-Form & Social" rail directly above it.

   The scrollbar thumb is wired up via the same attachVidSocialScrollbar
   helper used for #onCameraSocial. The class names .vid-social-row*
   are CSS hooks, not strict semantic tags, so reusing them keeps the
   pattern consistent without code duplication.

   Mobile (≤900px) is unchanged — the existing .is-mobile-swipe rules
   already provide a swipe-with-snap horizontal layout there. This
   block is desktop-only via the (min-width: 1025px) media query. */
@media (min-width: 1025px) {
  .press-video__grid#onCameraGrid {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 16px;
    overflow-x: auto;
    overflow-y: visible;
    scrollbar-width: none;
    -ms-overflow-style: none;
    grid-template-columns: none; /* override the inherited grid rule */
    /* Negative margin + matching padding keeps the row's edge-to-edge
       look while letting the scrollbar (which lives in a wrapping
       parent) sit aligned to the same content edge. */
    margin-left: 0;
    margin-right: 0;
    padding: 0 0 4px;
  }
  .press-video__grid#onCameraGrid::-webkit-scrollbar { display: none; }
  .press-video__grid#onCameraGrid > .proj-video-card {
    flex: 0 0 320px;
    max-width: 320px;
    min-width: 320px;
  }
}

/* ─── v2.9.112 — Press Archive: desktop single-row treatment ──────────
   Same pattern as #onCameraGrid above. The press archive defaults to
   the card view (vs. compact list); the card view on desktop now
   collapses to a horizontal scrolling row with the draggable scrollbar
   below. List view (#archiveList) is a separate vertical-row layout
   and isn't affected by this rule.

   Per-card width 360px — slightly wider than On Camera (320px) because
   archive cards carry quote text + outlet + project meta and read
   better with a bit more horizontal room.

   Mobile (≤900px) keeps the existing .is-mobile-swipe behavior.

   v2.9.114 — Bug fix: same root cause as the Library list-view fix.
   The :not(.hidden) qualifier ensures the JS toggle (which sets .hidden
   when the user picks the List view) actually hides the card grid. Without
   the qualifier this rule's display:flex overrode the .hidden { display:none }
   rule due to higher source-order precedence. */
@media (min-width: 1025px) {
  .archive-grid:not(.hidden) {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 20px;
    overflow-x: auto;
    overflow-y: visible;
    scrollbar-width: none;
    -ms-overflow-style: none;
    grid-template-columns: none;
    margin-left: 0;
    margin-right: 0;
    padding: 0 0 4px;
  }
  .archive-grid::-webkit-scrollbar { display: none; }
  .archive-grid:not(.hidden) > .archive-card {
    flex: 0 0 360px;
    max-width: 360px;
    min-width: 360px;
  }
}

/* ─── v2.9.112 — Library (#favsCardGrid): desktop single-row ──────────
   The Library page (page-library.php → view-favorites.php) defaults to
   the card grid (vs. the table list view). Same conversion to a single
   horizontal scrolling row at desktop, with the same scrollbar pattern.

   Library cards have posters at 2:3 aspect ratio plus title + meta, so
   they're vertically tall. Width 220px keeps the cards compact enough
   that 5-7 visible at once on a typical desktop.

   List view (#favsListView) stays a stacked table — unaffected.

   v2.9.114 — Bug fix: when toggling to List view, the JS removes
   .active from #favsCardGrid and the line ~8126 rule
   `#favsCardGrid:not(.active) { display: none; }` should hide it.
   But the v2.9.112 rule below has higher specificity (id+class vs id
   alone) AND no .active qualifier, so it kept setting display:flex
   regardless. Adding the :not(.active) qualifier here makes the rule
   only apply when the grid IS active, letting the hide-when-inactive
   rule win when the user is in list mode. */
@media (min-width: 1025px) {
  .favs-films-grid#favsCardGrid.active {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: flex-start;
    gap: 20px;
    overflow-x: auto;
    overflow-y: visible;
    scrollbar-width: none;
    -ms-overflow-style: none;
    grid-template-columns: none;
    margin-left: 0;
    margin-right: 0;
    padding: 0 0 4px;
  }
  .favs-films-grid#favsCardGrid.active::-webkit-scrollbar { display: none; }
  .favs-films-grid#favsCardGrid.active > .favs-film {
    flex: 0 0 220px;
    max-width: 220px;
    min-width: 220px;
  }

  /* v2.9.151 — Desktop rail compact card mode (matches the mobile-swipe
     compact treatment for visual consistency). Cards show meta + title +
     director + genre. Description, themes, and IMDb chip live in the
     .favs-sheet detail modal that opens on tap.
     
     Same speed + UI win as the mobile version: rail collapses to match
     the actual content, eliminating dead vertical space below the cards.
     Genre is kept on desktop (per James's request) since the wider screen
     can accommodate the extra line cleanly. */
  .favs-films-grid#favsCardGrid.active > .favs-film .favs-film__desc,
  .favs-films-grid#favsCardGrid.active > .favs-film .favs-film__themes,
  .favs-films-grid#favsCardGrid.active > .favs-film .favs-film__out-row {
    display: none;
  }
  /* Title with 3-line clamp for long titles. */
  .favs-films-grid#favsCardGrid.active > .favs-film .favs-film__title {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  /* Director: single line, ellipsized. Multi-director credits like
     "Joaquim Dos Santos, Kemp Powers, Justin K. Thompson" truncate
     cleanly at the card edge. */
  .favs-films-grid#favsCardGrid.active > .favs-film .favs-film__director {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* ─── v2.9.115 — Home About Me signature ──────────────────────────────
   Editor uploads a PNG of James's signature (transparent background,
   any color). The PNG is used as a CSS mask, which means the rendered
   color is set by background-color and the PNG's original color is
   irrelevant. Default tint: --ink (near-black). Hover lifts to --verd
   so the signature feels like a subtle interactive sign-off without
   adding a noisy hover state.

   Implementation notes:
   - The actual <img> element isn't used because <img>s can't be
     re-tinted via CSS. Instead the PNG URL is passed in via a custom
     property (--jk-signature-url) and applied as both -webkit-mask-image
     and mask-image (Safari needs the prefixed version through Safari 16).
   - aspect-ratio is intentionally NOT set: signature PNGs vary in
     proportion, and the image will fill its container at the set
     width while preserving its native aspect via the mask-size:contain
     rule. height: clamp covers worst-case wide-but-short signatures.
   - The signature is decorative (role/aria-label provided for AT users
     who do parse it). No visible link or interaction. Block container
     gives it breathing space above and below the prose. */
.bio-deep__signature {
  margin: 32px 0 8px;
  display: block;
}
.bio-deep__signature-img {
  display: inline-block;
  /* v2.9.270 — bumped width from clamp(180, 30vw, 280) → clamp(280, 40vw, 420)
     and height proportionally. James asked for it to be bigger on the home
     page; previous size was reading more like a thumbnail than a real
     signature in a memoir. The mask-image pattern means we control the
     rendered size purely through CSS box dimensions, so no image-asset
     swap is needed. */
  width: clamp(280px, 40vw, 420px);
  height: clamp(72px, 11vw, 108px);
  /* v2.9.137 — default tint is now --verd to match the signature
     recolor on journal pages. The mask-image pattern means the source
     PNG's color (white, black, or anything in between) is irrelevant;
     only the alpha channel drives the rendered shape. Hover lifts to
     --verd-deep for a subtle interactive cue without breaking the
     palette. */
  background-color: var(--verd);
  -webkit-mask-image: var(--jk-signature-url);
  mask-image: var(--jk-signature-url);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: left center;
  mask-position: left center;
  -webkit-mask-size: contain;
  mask-size: contain;
  transition: background-color 0.25s var(--ease);
}
@media (hover: hover) {
  .bio-deep__signature:hover .bio-deep__signature-img {
    background-color: var(--verd-deep);
  }
}
@media (max-width: 1024px) {
  .bio-deep__signature { margin: 24px 0 4px; }
  .bio-deep__signature-img {
    /* v2.9.270 — mobile signature scaled up to match the larger
       desktop sizing. Was clamp(160, 50vw, 220) / clamp(40, 12vw, 56). */
    width: clamp(220px, 65vw, 320px);
    height: clamp(56px, 16vw, 80px);
  }
}

/* ─── v2.9.116 — Home Director's Reel ─────────────────────────────────
   Sits between bio-deep and the work grid. Aesthetic: full-bleed
   16:9 video container with editorial framing (eyebrow + meta line).
   Click-to-play (no autoplay), so the layout is stable from first
   paint and there's no autoplay LCP penalty. Section background is
   ink so it reads as a "moment" between the cream bio and the
   chalk-bg work grid below.

   The .home-reel__player is a button containing a poster image and
   a play affordance. JS delegates click → openEmbed() handler. */
/* HOME REEL — Director's Reel section between bio and work grid.

   Renders a click-to-play sizzle reel embed.

   v2.9.120 — Restyled to match the surrounding "Selected Work" /
   "About Me" sections rather than render as a standalone dark band.
   Was: ink (near-black) background, 80px padding, mono-eyebrow head.
   Now: chalk (cream) background, 48px padding to match .work, a real
   .section-head with display-serif heading "Director's <em>Reel</em>".
   The video poster itself stays dark (it's a video frame, not page
   chrome), but the surrounding container reads as continuous with
   the rest of the page.

   The .home-reel__player is a button containing a poster image and
   a play affordance. JS delegates click → openEmbed() handler. */
.home-reel {
  background: var(--chalk);
  /* Match .work padding/maxwidth/margin so the reel sits in the same
     content column as Selected Work. The .work section comes right
     after this one and uses the same container shape, so visitor's
     eye reads them as a continuous editorial spread. */
  padding: 48px 32px;
  max-width: 1500px;
  margin: 0 auto;
  position: relative;
}
.home-reel__inner {
  /* No max-width here — let the inner content fill the .home-reel
     container, which is already capped at 1500px. The earlier 1280px
     cap was centering the reel and visually disconnecting it from
     the work grid below (which fills 1500px). */
  margin: 0;
}
.home-reel__head {
  /* Mirrors .section-head — flex, baseline-aligned, generous bottom
     margin. Replaces the earlier compact mono-eyebrow header. */
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  margin-bottom: 64px;
  gap: 40px;
  flex-wrap: wrap;
}
.home-reel__title {
  /* Matches .section-head__title from .work — display serif, weight
     300, large clamp size, ink color on the chalk ground. The <em>
     accent in "Director's <em>Reel</em>" picks up the verd green
     same as "Selected <em>Work</em>" and "About <em>Me</em>". */
  font-family: var(--display);
  font-weight: 300;
  font-size: var(--h-poster-title);
  line-height: var(--lh-display);
  letter-spacing: var(--track-tight);
  color: var(--ink);
}
.home-reel__title em {
  /* v2.9.272 — italic restored, same reasoning as section-head em
     above. The site-wide em reset zeros italic globally; display
     headings re-enable it so verd accent words render as designed. */
  color: var(--verd);
  font-weight: 400;
  font-style: italic;
}
/* Legacy eyebrow rule kept for back-compat in case any hand-edited
   admin content still uses it — but the template now emits the
   .home-reel__title heading instead. */
.home-reel__eyebrow {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd);
  font-weight: 500;
}
.home-reel__meta {
  /* Right-aligned mono meta text — matches .section-head__meta on the
     work grid (year range + count). Lives in the head row beside the
     title. */
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-mid);
  text-transform: uppercase;
  color: var(--verd);
  text-align: right;
  opacity: 0.85;
}
.home-reel__player {
  /* Reset button defaults */
  appearance: none;
  background: var(--dark-card, #1A1916);
  border: none;
  padding: 0;
  margin: 0;
  width: 100%;
  cursor: pointer;
  position: relative;
  display: block;
  /* aspect-ratio reserves the slot before the poster image loads —
     critical for CLS, since otherwise the section would jump from
     near-zero to ~720px tall on poster load */
  aspect-ratio: 16 / 9;
  overflow: hidden;
  outline-offset: 2px;
  outline-color: var(--verd);
}
.home-reel__poster {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.6s var(--ease, ease);
}
.home-reel__poster--placeholder {
  background: linear-gradient(135deg, var(--dark-card, #1A1916) 0%, var(--ink) 100%);
}
.home-reel__player:hover .home-reel__poster:not(.home-reel__poster--placeholder) {
  transform: scale(1.02);
}
.home-reel__play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 88px;
  height: 88px;
  border-radius: 50%;
  background: rgba(45, 106, 91, 0.92); /* verd, slightly translucent */
  color: var(--chalk);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none; /* button gets the click, this is decorative */
  transition: transform 0.25s var(--ease, ease), background-color 0.25s var(--ease, ease);
  /* offset the SVG slightly to optically center the play triangle */
  padding-left: 4px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.home-reel__player:hover .home-reel__play {
  transform: translate(-50%, -50%) scale(1.08);
  background: var(--verd);
}
@media (max-width: 1024px) {
  .home-reel { padding: 32px 20px; }
  .home-reel__head { margin-bottom: 32px; }
  .home-reel__play { width: 64px; height: 64px; }
  .home-reel__play svg { width: 22px; height: 22px; }
  /* Hide the meta on mobile — same pattern as .section-head__meta on
     the work grid (year range + count is desktop-only context). */
  .home-reel__meta { display: none; }
  /* v2.9.125 — Match the mobile size of .work .section-head__title
     so "Director's Reel" reads as identical typography to "Selected
     Work" and the "If This Work Resonates" press section. The .work
     rule above (line ~9637) uses clamp(30px, 7vw, 44px) !important
     on mobile; we apply the same here. !important needed because the
     desktop rule has higher selector specificity (parent .work) than
     the mobile override would otherwise produce inside this media
     query. */
  .home-reel__title { font-size: clamp(30px, 7vw, 44px) !important; }
}

/* ─── v2.9.116 — Films page production-status chip ────────────────────
   Renders inside .work-card__slate next to year, only for projects
   with a non-default productionStatus. Color-coded by status so a
   scanning executive can pick out "Filming" or "In Development" at a
   glance. */
.work-card__status {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: var(--track-mid);
  text-transform: uppercase;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid currentColor;
  margin-left: auto; /* pushes to the right end of the slate row */
  white-space: nowrap;
}
/* Status color encodes meaning: filming/active = verd (active hand),
   pre/post/development = cream (work in progress, no hand yet),
   optioned = a softer tone signaling "in market". */
.work-card__status--filming     { color: var(--verd-light); }
.work-card__status--pre         { color: var(--cream); }
.work-card__status--post        { color: var(--cream); }
.work-card__status--development { color: var(--cream); }
.work-card__status--optioned    { color: var(--verd-light); opacity: 0.85; }

/* ─── v2.9.116 — Streaming platform chips on work-cards ───────────────
   Compact strip below the meta line. Up to 4 chips fit comfortably on
   a 320px-wide card; CSS truncates with overflow-hidden + nowrap so
   excess platforms past 4 don't break the card layout. */
.work-card__streaming {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 8px;
  align-items: center;
}
.work-card__streaming-chip {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 500;
  padding: 3px 7px;
  border-radius: 3px;
  background: rgba(45, 106, 91, 0.12); /* verd-tint */
  color: var(--verd-deep, #1f4d40);
  white-space: nowrap;
  line-height: var(--lh-display-medium);
}
/* Per-platform brand-color overrides — keeps recognizable services
   (Apple TV, Prime, etc.) visually distinct. Falls through to default
   for unknown services. */
.work-card__streaming-chip[data-platform="apple-tv"],
.work-card__streaming-chip[data-platform="apple-tv-plus"] {
  background: rgba(0, 0, 0, 0.08); color: #1d1d1f;
}
.work-card__streaming-chip[data-platform="prime-video"],
.work-card__streaming-chip[data-platform="amazon-prime-video"],
.work-card__streaming-chip[data-platform="amazon-prime"] {
  background: rgba(0, 168, 225, 0.12); color: #00688A;
}
.work-card__streaming-chip[data-platform="starz"] {
  background: rgba(0, 0, 0, 0.08); color: #1d1d1f;
}
.work-card__streaming-chip[data-platform="max"],
.work-card__streaming-chip[data-platform="hbo-max"] {
  background: rgba(0, 38, 153, 0.12); color: #002699;
}
.work-card__streaming-chip[data-platform="netflix"] {
  background: rgba(229, 9, 20, 0.10); color: #B30810;
}
.work-card__streaming-chip[data-platform="hulu"] {
  background: rgba(28, 231, 131, 0.10); color: #0F7843;
}
.work-card__streaming-chip[data-platform="tubi"] {
  background: rgba(255, 102, 0, 0.10); color: #B34700;
}
.work-card__streaming-chip[data-platform="youtube"],
.work-card__streaming-chip[data-platform="youtube-tv"] {
  background: rgba(255, 0, 0, 0.08); color: #B30000;
}

/* ─── v2.9.116 — Films page quick-jump anchor strip ───────────────────
   Sits below .mv-grid-head, above the cards. Editorial mono-caps
   row-of-links with smooth-scroll-on-click (handled in main.js). The
   grid wraps at desktop ~1280px and stays a horizontal scroll-snap
   row on mobile so it doesn't dominate small screens. */
.films-quickjump {
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-bottom: 24px;
  padding-top: 8px;
  border-top: 1px solid var(--rule);
  padding: 16px 0 0;
  flex-wrap: wrap;
}
.films-quickjump__label {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.5;
  font-weight: 500;
  flex-shrink: 0;
}
.films-quickjump__list {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  align-items: baseline;
}
.films-quickjump__link {
  font-family: var(--display);
  font-size: 14px;
  font-weight: 400;
  letter-spacing: -0.005em;
  color: var(--ink);
  opacity: 0.7;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: opacity 0.2s var(--ease, ease), color 0.2s var(--ease, ease), border-color 0.2s var(--ease, ease);
  padding: 2px 0;
}
.films-quickjump__link:hover {
  opacity: 1;
  color: var(--verd);
  border-bottom-color: var(--verd-light);
}
@media (max-width: 1024px) {
  .films-quickjump {
    /* On mobile, become a horizontal scroll strip so it doesn't
       eat 4-5 lines of vertical real estate */
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding: 12px 20px 0;
    margin-left: -20px;
    margin-right: -20px;
  }
  .films-quickjump::-webkit-scrollbar { display: none; }
  .films-quickjump__list {
    flex-wrap: nowrap;
    gap: 14px;
  }
  .films-quickjump__link { white-space: nowrap; }
}
/* Highlight pulse on quickjump-targeted card */
@keyframes filmsQuickjumpPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(45, 106, 91, 0); }
  20%      { box-shadow: 0 0 0 4px rgba(45, 106, 91, 0.4); }
  60%      { box-shadow: 0 0 0 6px rgba(45, 106, 91, 0.2); }
}
.films-quickjump__target-pulse {
  animation: filmsQuickjumpPulse 1.6s var(--ease, ease) both;
}

/* ─── v2.9.116 — Commercials & Music Videos brand-logo strip ──────────
   Sits between hero and grid. Grayscale logos arranged in a
   wrap-flex row, with a subtle eyebrow label above. Hovering a
   logo lifts opacity and saturation so the brand color reads. */
.mv-brands {
  padding: 32px 32px 48px;
  max-width: 1500px;
  margin: 0 auto;
  border-bottom: 1px solid var(--rule);
}
.mv-brands__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
}
.mv-brands__eyebrow {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.5;
  font-weight: 500;
}
.mv-brands__list {
  display: flex;
  flex-wrap: wrap;
  gap: 32px 48px;
  align-items: center;
  justify-content: center;
}
.mv-brands__item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Container reserves a stable slot regardless of logo aspect — keeps
     the strip's vertical rhythm consistent across many brand uploads */
  width: 120px;
  height: 56px;
  text-decoration: none;
  filter: grayscale(1);
  opacity: 0.55;
  transition: filter 0.3s var(--ease, ease), opacity 0.3s var(--ease, ease);
}
.mv-brands__item:hover {
  filter: grayscale(0);
  opacity: 1;
}
.mv-brands__logo {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
}
.mv-brands__name-fallback {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
}
@media (max-width: 1024px) {
  .mv-brands { padding: 24px 20px 32px; }
  .mv-brands__list { gap: 20px 32px; }
  .mv-brands__item { width: 96px; height: 44px; }
}

/* ─── v2.9.116 — Inquiries page rep category styling ──────────────────
   When a rep row has an inquiry_category set (e.g. "Film & Television",
   "Management", "Commercials"), it renders as a verd-light label
   inline with the company name. Helps executives identify which rep
   handles what at a glance, per the staging-comparison doc. */
.contact-method__label-category {
  color: var(--verd);
  font-weight: 500;
}
.contact-method__label-sep {
  margin: 0 6px;
  opacity: 0.5;
}
.contact-method__label-company {
  /* No special color — inherits from .contact-method__label */
}

/* ─── v2.9.117 — Desktop nav Films dropdown ───────────────────────────
   Hover-triggered submenu under the Films link in the top nav. Sits
   below the active nav row, anchored to the parent <li>. Visible only
   on desktop (the hamburger menu handles mobile via the accordion
   pattern below).

   Trigger: hover OR keyboard focus (li:focus-within). Delay-out via
   transition + a brief opacity/transform animation so the dropdown
   doesn't disappear instantly when the cursor crosses a small gap
   between the link and the menu.

   The submenu has its own spacing rhythm — Instrument Serif film titles
   in a vertical column, with mono "All Films →" footer styled like
   the rest of the editorial micro-copy. Colors pull from the same
   ink/verd/cream palette as the main nav. */
.nav__has-submenu {
  position: relative;
}
/* v2.9.120 — Hover bridge.
   The dropdown is positioned at top:100% of the parent <li>, which
   means it touches the <li>'s bottom edge — but the parent <li>'s
   own height is only the <a>'s padding, so when the cursor moves
   from "Films" downward toward a dropdown item, there's a moment
   where it's between the parent's bottom and the dropdown's top.
   Combined with the dropdown's translateY(-4px) opening offset,
   that gap can break :hover and the dropdown immediately starts
   fading out.

   Solution: a transparent ::before that sits in the gap, extending
   the parent's hover surface upward to overlap with the dropdown.
   It only renders on hover (avoids accidental triggers from cursor
   passing through the area without intending to open the dropdown). */
.nav__has-submenu:hover::before,
.nav__has-submenu:focus-within::before,
.nav__has-submenu.is-open::before {
  content: '';
  position: absolute;
  top: 100%;
  left: -8px;
  right: -8px;
  height: 16px;
  background: transparent;
  z-index: 1;
}
.nav__submenu {
  position: absolute;
  top: 100%;
  left: 0;
  /* v2.9.120 — explicit margin-top:0 so the dropdown touches the
     parent's bottom edge with no gap. Combined with the hover bridge
     above, this means once hover starts, the cursor can't accidentally
     slip between parent and dropdown. */
  margin: 0;
  min-width: 280px;
  padding: 12px 0;
  list-style: none;
  background: var(--chalk);
  /* v2.9.124 — Dropped the 1px outer border. The drop shadow alone
     gives sufficient visual separation from the page; the border
     was reading as a hard outline that fought the soft editorial
     feel of the rest of the site. */
  box-shadow: 0 12px 32px rgba(26, 25, 22, 0.12);
  /* Hidden by default — shown on hover/focus-within */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-4px);
  transition: opacity 0.18s var(--ease), transform 0.18s var(--ease), visibility 0s linear 0.18s;
  /* z-index: 100 within the .nav stacking context (z-index: 1000).
     The nav's isolation:isolate confines z-stacking to within the
     nav layer; the dropdown at 100 paints above the nav links (z
     auto) but the entire nav layer paints above anything else in
     the page at z<1000. */
  z-index: 100;
}
.nav__has-submenu:hover .nav__submenu,
.nav__has-submenu:focus-within .nav__submenu,
.nav__has-submenu.is-open .nav__submenu,
.nav__submenu.is-open {
  /* The .nav__submenu.is-open selector handles the portaled case:
     v2.9.123 moves .nav__submenu to body so the descendant selectors
     above no longer match, but the JS adds .is-open to the submenu
     itself in parallel. */
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0);
  transition-delay: 0s, 0s, 0s;
}

/* Portaled dropdown — no longer inside .nav, so the nav's stacking
   context can't trap it. Pin to a high z-index so it paints above
   everything except intentional overlays. */
.nav__submenu--portaled {
  z-index: 1500 !important;
}
.nav__submenu li {
  margin: 0;
}
.nav__submenu a {
  /* Override the all-caps mono treatment of the main nav. Submenu
     items are film titles in display serif — the visual rhythm of a
     screening list, not a nav row.

     v2.9.125 — Explicit text-decoration:none. This used to be inherited
     from .nav__links a (which had text-decoration:none on the desktop
     nav), but v2.9.123 portaled the submenu out of .nav__links to <body>
     to escape the nav's backdrop-filter stacking context. With the
     submenu no longer a descendant of .nav__links, the reset stopped
     applying and browser-default underlines appeared on every film
     title. The fix is to set the property directly on .nav__submenu a
     so it applies regardless of DOM position.

     v2.9.126 — Added position:relative and slight padding-left bump
     to make room for the hover-state vertical hairline (rendered via
     ::before below). The hairline animates in from 0 to full height
     on hover, in verd green — an editorial mark in keeping with the
     site's hairline-and-rule design language. */
  position: relative;
  font-family: var(--display);
  font-size: 14px;
  letter-spacing: -0.005em;
  text-transform: none;
  text-decoration: none;
  color: var(--ink);
  opacity: 0.85;
  padding: 7px 20px 7px 24px;
  display: block;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  max-width: 360px;
  /* v2.9.189 — Removed padding-left from the transition list. Padding
     transitions force layout recalc on every hover-in/hover-out, which
     Lighthouse was flagging as 40 non-composited animations on desktop
     /on-location/ (one per submenu item). The visible animation users
     see is the hairline ::after scaling in from 0; the 4px text nudge
     was secondary. With padding-left removed from transition, the text
     snaps instantly when hover engages, while the hairline still
     animates over 0.22s. Net visual: nearly identical, minus the
     layout-recalc cost. */
  transition: color 0.2s var(--ease), opacity 0.2s var(--ease);
}
/* v2.9.126 — Vertical hairline that animates in on hover.
   Sits at left:14px (inside the link's 24px left-padding column),
   with a 1px width and verd-green background. Default state has
   transform:scaleY(0) so the line is invisible; the transform-origin
   anchored at top means it grows downward on hover, like ink filling
   a margin. The 0.22s ease-out transition matches the rest of the
   site's micro-interaction timing.
   The link's padding-left also nudges 4px on hover (24 → 28) so the
   text moves slightly right as the hairline appears, reinforcing the
   "this row is selected" reading. */
.nav__submenu a::after {
  content: '';
  position: absolute;
  left: 14px;
  top: 8px;
  bottom: 8px;
  width: 1px;
  background: var(--verd);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.22s var(--ease);
}
.nav__submenu a:hover {
  background: rgba(45, 106, 91, 0.06);
  color: var(--verd);
  opacity: 1;
  padding-left: 28px;
}
.nav__submenu a:hover::after,
.nav__submenu a:focus-visible::after {
  transform: scaleY(1);
}
.nav__submenu a:focus-visible {
  outline: none;
  background: rgba(45, 106, 91, 0.06);
  color: var(--verd);
  padding-left: 28px;
}
.nav__submenu-all {
  /* v2.9.124 — Dropped the border-top. Whitespace alone separates
     the "All Films →" link from the film list above. Cleaner. */
  margin-top: 8px;
  padding-top: 8px;
}
.nav__submenu-all a {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-mid);
  text-transform: uppercase;
  text-decoration: none;
  font-weight: 500;
  color: var(--verd);
  opacity: 1;
}
.nav__submenu-all a:hover {
  color: var(--verd-deep, var(--verd));
  background: rgba(45, 106, 91, 0.12);
}
/* Hide the dropdown on mobile — hamburger accordion handles it instead */
@media (max-width: 1024px) {
  .nav__submenu { display: none; }
}

/* ─── v2.9.117 — Mobile hamburger Films accordion ─────────────────────
   Inside the menu overlay, the Films <li> has:
     - the .menu-overlay__has-submenu-link (the link to /films/, takes
       the user to the page if tapped — same as before)
     - the .menu-overlay__submenu-toggle (a separate button that
       expands/collapses the submenu in place, never navigates)
     - the .menu-overlay__submenu (the inline list)
   Two affordances on one row: the link wants to go to the page, the
   button wants to drill in. Side-by-side with the toggle as a small
   caret on the right edge.

   Animation: the submenu uses [hidden] for true accessibility (screen
   readers don't announce a hidden subtree). Visual collapse uses
   max-height transition for a smooth slide-down. We use a generous
   max-height ceiling (1200px) since 12+ films can be tall — the actual
   height is the content height, max-height just bounds the transition. */
.menu-overlay__has-submenu {
  position: relative;
}
/* v2.9.183 — Whole-row toggle button (.menu-overlay__has-submenu-toggle).
   The button inherits the row layout from the .menu-overlay__links button
   selector group above (display:flex, gap:20px, padding:14px 0, etc.). It
   also gets .menu-overlay__has-submenu-link for backward compat with any
   existing styling that targeted that class.

   The chevron sits at the right edge of the row via the caret wrap's
   margin-left:auto trick, pushing it past the flex children to the
   container's right edge. Rotates 180° on aria-expanded="true". */
.menu-overlay__submenu-caret-wrap {
  margin-left: auto;
  /* v2.9.186 — Bumped margin-right from 6px to 26px. The 6px math was
     based on padding-edge math that didn't account for the visual
     difference between the X close button's circular boundary (44px)
     and the chevron's naked 24px wrap. User screenshot showed clear
     misalignment after 6px; empirically ~20px more inset gets the
     chevron's visual center near the X glyph's visual center. */
  margin-right: 26px;
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  opacity: 0.7;
  transition: opacity 0.2s var(--ease);
  flex-shrink: 0;
}
.menu-overlay__has-submenu-toggle:hover .menu-overlay__submenu-caret-wrap,
.menu-overlay__has-submenu-toggle:focus-visible .menu-overlay__submenu-caret-wrap {
  opacity: 1;
}
.menu-overlay__submenu-caret {
  /* 24x24 to match the close-X */
  width: 24px;
  height: 24px;
  display: block;
  transition: transform 0.25s var(--ease);
}
.menu-overlay__has-submenu-toggle[aria-expanded="true"] .menu-overlay__submenu-caret {
  transform: rotate(180deg);
}
.menu-overlay__submenu {
  list-style: none;
  margin: 0;
  /* v2.9.122 — Indent matches the title column position (68px) so
     submenu items align with their parent's title, not the number
     gutter. Adds a subtle 1px left-rule in verd-light at low opacity
     to visually group the items as children of the parent without
     needing per-row border-bottoms (which extended past the chevron
     and read as visual noise).
     v2.9.182 — Added 6px top spacing inside the padding so the first
     submenu item (post-expand) doesn't sit flush against the chevron's
     tap zone. Reduces accidental taps when fingers are still in motion
     after tapping to expand. */
  padding: 10px 0 12px 68px;
  margin-left: 0;
  position: relative;
}
.menu-overlay__submenu::before {
  content: '';
  position: absolute;
  /* The vertical rule sits in the gutter to the LEFT of submenu
     items, occupying the gap between the number column and the
     titles. Visual cue: "these belong to the row above." */
  left: 53px;
  top: 4px;
  bottom: 12px;
  width: 1px;
  background: rgba(109, 191, 173, 0.25);
}
.menu-overlay__submenu li {
  margin: 0;
  padding: 0;
  /* v2.9.193 — Removed hairline border-bottom. The chevron indicator
     beside each submenu item is sufficient visual chunking; the
     hairline added a busy second affordance for the same purpose. */
}
.menu-overlay__submenu a {
  /* Smaller, indented, less prominent than the main link rows.
     v2.9.122 — Removed border-bottom: was extending across the full
     row width and reading as graphic noise. The vertical rule on
     .menu-overlay__submenu::before plus consistent padding does the
     visual grouping job more cleanly.
     v2.9.182 — Bumped vertical padding from 8px to 10px to make per-
     row tap targets closer to the 44px Apple/Google touch guideline.
     With 12 films, tighter rows were causing fat-finger errors.
     v2.9.184 — Bumped to 12px → ~46px tap target (just above guideline).
     Combined with the new li border-bottom rule above, rows now have
     both physical AND visual boundaries — measurably easier to hit
     the intended target on touch. */
  display: block;
  font-family: var(--display);
  font-size: 16px !important; /* defeat any inherited display-size */
  font-weight: 300;
  color: var(--cream);
  opacity: 0.75;
  padding: 12px 0;
  text-decoration: none;
  letter-spacing: -0.005em;
  transition: opacity 0.2s var(--ease), color 0.2s var(--ease);
}
.menu-overlay__submenu a:hover,
.menu-overlay__submenu a:focus-visible {
  opacity: 1;
  color: var(--verd-light);
  outline: none;
}
.menu-overlay__submenu-all a {
  /* "All Films →" footer — mono-caps to match the rest of the menu's
     micro-typography. */
  font-family: var(--mono);
  font-size: 10px !important;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd-light);
  font-weight: 500;
  margin-top: 4px;
  padding-top: 12px;
  opacity: 0.85;
}


/* ─── v2.9.130 — Grid container reservations ──────────────────────────
   Both #filmsGrid and #combinedGrid are JS-rendered (the films grid
   has a server-side cached HTML fallback when available, but on first
   uncached pageview and always for combinedGrid the container starts
   empty and JS pumps in cards on DOMContentLoaded). The "all cards
   appear at once" event was responsible for CLS 0.111 on /films/
   mobile and CLS 0.488 on /commercialsandmusicvideos/ desktop.

   Reserving min-height stops the parent section from collapsing to
   zero before hydration, which is what Lighthouse measured as a
   layout shift when cards filled in.

   Heights are calibrated to the median rendered grid:
     - films: ~9 cards in 3 columns at desktop = 3 rows × 280px = 840px
     - films mobile: 1 column horizontal-swipe rail = ~360px tall
     - combined: ~14 cards in 3 columns = 5 rows × 280px = 1400px
     - combined mobile: horizontal-swipe rail = ~360px

   Once JS hydrates and adds cards, content overrides min-height
   (because explicit content height > min-height takes precedence).
   So this only affects the pre-hydration window. Empty post-hydration
   states (filtered to zero, etc.) intentionally collapse — handled
   by the .is-filtered class logic, not min-height.
   ─────────────────────────────────────────────────────────────────── */
#filmsGrid:not([data-rendered="1"]),
#combinedGrid:not([data-rendered="1"]) {
  min-height: 360px;
}
@media (min-width: 1025px) {
  #filmsGrid:not([data-rendered="1"]) { min-height: 840px; }
  #combinedGrid:not([data-rendered="1"]) { min-height: 1400px; }
}

/* ─── v2.9.130 — Footer height reservation ────────────────────────────
   Desktop CLS 0.318 attributed to <footer class="footer"> on
   commercials/. The footer renders late because the grid above it
   pumps in content via JS, pushing the footer down. Reserve a stable
   height so the footer's first paint position matches its final
   position.

   Footer mobile is single-column stacked, ~480px. Desktop is
   multi-column ~280px. */
.footer {
  min-height: 480px;
}
@media (min-width: 1025px) {
  .footer { min-height: 280px; }
}



/* ═══════════════════════════════════════════════════════════════════
   LANDING PAGES (CPT: landing) — v2.9.266
   
   SEO capture pages from jk-schema-generator v1.12.16+. Each landing
   has its own slug-driven URL and is rendered by single-landing.php.
   
   v266 corrections vs v265:
   - Frame width changed 1180 → 1500 to match the rest of the site
     (.epk-hero__inner, .epk-bio__inner, etc. all use 1500px).
   - All sections left-aligned to match site convention; body and
     reach-out content no longer center inside the frame.
   - Memberships rules duplicated from epk.css into this block so
     they apply on landing pages (epk.css only enqueues on /epk/,
     so on landings the .epk-memberships classes had nothing to
     reference and rendered as default browser bullets).
   - Reach-out .contact-link underlines removed — phone numbers and
     email addresses render as plain text-color with verd-on-hover.
   - Footer CTA section left-aligned + 1500 frame.
   ═══════════════════════════════════════════════════════════════════ */

/* Article wrapper — chalk background to match EPK + Library. */
.landing {
  background: var(--chalk);
  color: var(--ink);
  min-height: 100vh;
  min-height: 100svh;
}

/* Shared inner-content frame. Every section's __inner uses this width
   so the page reads as a single vertical column on desktop, no
   alternating gutters. 1500px matches every other page on the site
   (.epk-hero__inner, .epk-bio__inner, .epk-films__inner, etc.) so
   when a visitor moves from a landing to /epk/ or /films/ the
   horizontal alignment carries through visually. */
.landing-hero__inner,
.landing-body__inner,
.landing-aside__inner,
.landing-reachout__inner,
.landing-links__inner,
.landing-cta__inner {
  max-width: 1500px;
  margin: 0 auto;
}

/* HERO ───────────────────────────────────────────────────────────── */
.landing-hero {
  padding: 120px 32px 64px;
  border-bottom: 1px solid var(--rule);
}
.landing-hero__eyebrow {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--verd);
  margin-bottom: 24px;
}
.landing-hero__title {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(48px, 8vw, 120px);
  line-height: 0.92;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin: 0 0 32px;
  /* v2.9.277 — min-height reservation to prevent layout shift when
     Instrument Serif swaps in for the system serif fallback.
     PageSpeed reported CLS 0.612 (mobile) / 1.000 (desktop) on
     /documentary-filmmaker/ landing, with the entire <section
     class="landing-body"> shifting because the landing-hero above
     it grew vertically when Instrument Serif loaded. The hero title
     uses clamp(48, 8vw, 120) with line-height 0.92, so the metrics
     box is tight against the glyphs — even a small font-metric
     difference between fallback and Instrument Serif produces a
     visible swap shift.

     2.1em with 0.92 line-height reserves enough vertical space for
     2 lines plus a small buffer (~1.93 line-heights). All four
     landing titles fit on 1-2 lines at every viewport width:
     "Documentary Filmmaker" (1 line desktop, 2 mobile), "Film
     Director, Los Angeles" (1-2 lines), "Commercial Director" (1
     line), "Filmmaker, Speaker & Panelist" (1-2 lines). The
     reservation never under-shoots, so no clipping; on shorter
     titles there's a small empty band below the title before the
     lede starts, which reads as breathing room rather than a bug.

     v2.9.286 — INCREASED to 2.4em (was 2.1em). v2.9.285 attempted
     to fix CLS via the fonts-loaded gate but introduced a Safari
     white-page regression and pushed desktop CLS to 0.973. Reverting
     the gate and instead beefing up the reservation here. 2.4em
     covers the worst-case scenario: a 2-line title in Times
     fallback (where wider advance widths force more wrapping) at
     the largest viewport. Previously 2.1em was sized for the
     Instrument Serif rendering; once Times fallback rendered with
     wider character widths, some titles wrapped to a longer second
     line that exceeded the reservation. The extra 0.3em (~36px at
     hero scale) absorbs that worst-case difference. */
  min-height: 2.4em;
}
.landing-hero__title em {
  font-weight: 400;
  color: var(--verd);
  font-style: italic;
}
.landing-hero__lede {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(18px, 1.6vw, 22px);
  line-height: 1.55;
  color: var(--ink);
  opacity: 0.85;
  max-width: 720px;
  margin: 0 0 40px;
  /* v2.9.277 — min-height reservation, same reasoning as the title
     above. The lede typically wraps to 2-3 lines at desktop and 4-5
     lines at mobile. 6em with 1.55 line-height = ~3.87 line-heights
     of reserved space; enough for 3-4 lines without clipping any
     existing landing copy. Slightly over-reserves for the shorter
     ledes (Documentary Filmmaker, Commercial Director) but the
     visual cost is small breathing room between lede and CTA, which
     reads cleaner than the 0.6+ CLS penalty without it.

     v2.9.286 — INCREASED to 8em (was 6em). Reverting v285's
     fonts-loaded gate (which caused Safari white) means the lede
     text reflows visibly when fonts swap, so the reservation
     needs to cover the worst-case Times-fallback wrap count.
     Mobile-narrow viewports wrap landing ledes to 5-6 lines in
     Times (vs 3-4 in Instrument Serif). 8em with 1.55 line-height
     = ~5.16 line-heights, enough to absorb the worst case. The
     small visual cost on shorter ledes (extra breathing room
     between lede and CTA) is preferable to layout shift. */
  min-height: 8em;
}
.landing-hero__cta {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
}

/* BUTTONS ─────────────────────────────────────────────────────────── */
.landing-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 16px 32px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid;
  transition: background-color 0.25s var(--ease),
              color 0.25s var(--ease),
              border-color 0.25s var(--ease);
  cursor: pointer;
  line-height: 1;
}
.landing-btn--primary {
  background: var(--verd);
  color: var(--cream);
  border-color: var(--verd);
}
.landing-btn--primary:hover,
.landing-btn--primary:focus-visible {
  background: var(--verd-deep, #1e4a3f);
  border-color: var(--verd-deep, #1e4a3f);
  color: var(--cream);
}
.landing-btn--secondary {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink);
}
.landing-btn--secondary:hover,
.landing-btn--secondary:focus-visible {
  background: var(--ink);
  color: var(--cream);
  border-color: var(--ink);
}

/* BODY SECTIONS ──────────────────────────────────────────────────── */
.landing-body {
  /* v2.9.272 — split top/bottom padding. Top stays generous (80px)
     for breathing room below the hero CTAs; bottom reduced to 32px
     so the Memberships card hugs the prose more cleanly without a
     dead-air gap. The visual rhythm reads as: hero → CTA breath
     → body prose flows directly into evidence (memberships) →
     reach out → continue → final CTA. Previously the 80px bottom
     created a gap that made memberships feel like a disconnected
     standalone module rather than a continuation of the body. */
  padding: 80px 32px 32px;
  /* v269 — content-visibility: auto removed. The original v265
     introduction was meant to prevent CLS from below-fold paint, but
     on mobile Chrome the auto-rendering heuristic was flipping
     between rendered/non-rendered states as the viewport scrolled,
     producing a 0.185 CLS score on real PSI runs. Server-rendered
     flexible_content already avoids the original CLS problem because
     the .landing-block divs ship in the initial HTML — no JavaScript
     hydration needed. Removing the CV hint trades a tiny render-
     skip win on offscreen sections for a clean mobile CLS score.
     Same change applied to .landing-aside and .landing-reachout. */
}
.landing-body__inner {
  /* 1500 frame, but body prose constrains to a 720px reading measure
     INSIDE that frame, left-aligned. This matches how .epk-bio works:
     prose is at the canonical reading width but anchored left, with
     the right side empty so other modules (sidebars, asides) can sit
     beside it on wider screens. Without max-width on the prose, lines
     would run to 1500px which is unreadable for serif body text. */
}
.landing-body__inner > .landing-block {
  max-width: 720px;
}
.landing-block {
  margin-bottom: 56px;
}
.landing-block:last-child {
  margin-bottom: 0;
}
.landing-block__heading {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(28px, 3.5vw, 40px);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0 0 20px;
}
.landing-block__intro {
  font-family: var(--display);
  font-size: 19px;
  line-height: 1.55;
  color: var(--ink);
  opacity: 0.85;
  margin: 0 0 24px;
}
.landing-block__body {
  font-family: var(--display);
  font-size: 19px;
  line-height: 1.65;
  color: var(--ink);
  opacity: 0.92;
}
.landing-block__body p {
  margin: 0 0 18px;
}
.landing-block__body p:last-child {
  margin-bottom: 0;
}
.landing-block__body a {
  color: var(--verd);
  text-decoration: underline;
  text-decoration-color: rgba(45, 106, 91, 0.4);
  text-underline-offset: 3px;
  transition: text-decoration-color 0.2s var(--ease);
}
.landing-block__body a:hover {
  text-decoration-color: var(--verd);
}

.landing-block__list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.landing-block__list li {
  font-family: var(--display);
  font-size: 18px;
  line-height: 1.5;
  color: var(--ink);
  padding-left: 24px;
  position: relative;
}
.landing-block__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 14px;
  width: 12px;
  height: 1px;
  background: var(--verd);
  opacity: 0.6;
}

.landing-block__links {
  display: flex;
  flex-direction: column;
  gap: 0;
  border-top: 1px solid var(--rule);
}
.landing-block__link {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 18px 0;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  color: var(--ink);
  transition: padding-left 0.2s var(--ease);
}
.landing-block__link:hover {
  padding-left: 8px;
}
.landing-block__link-label {
  font-family: var(--display);
  font-size: 22px;
  font-weight: 400;
  color: var(--verd);
  letter-spacing: -0.01em;
}
.landing-block__link-desc {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.55;
}

/* CONTINUE EXPLORING / INTERNAL-LINKS SECTION ───────────────────── */
/* v2.9.271 — internal_links_block emission location. Sits between
   the Reach Out module and the final CTA, gives the visitor a
   "where to next" branch after they've seen contact options. The
   inner div takes the same 1500px frame used by every other landing
   section (defined in the grouped rule near the top of this block).

   v2.9.272 — background reverted to chalk (transparent / inherit) so
   it alternates cleanly with the chalk-mid Reach Out band above and
   matches the chalk Open-the-EPK / Reach Out final CTA below. Visual
   rhythm down the page: chalk (body) → chalk (memberships) →
   chalk-mid (reach-out, the only colored band) → chalk (continue) →
   chalk (final CTA). Reach-Out becomes the explicit "stop and contact"
   visual moment, sandwiched between chalk blocks. */
.landing-links {
  padding: 64px 32px;
  /* v2.9.284 — kept at 64px which was already the harmonized value
     among the supporting sections. No background / border treatment
     beyond the .landing-section border-top inherited from the wrapper
     class. Was chalk-mid + top/bottom borders in v271; that doubled
     the visual weight of the Reach Out section directly above and
     read as "two contact-zone bands" rather than alternating sections. */
}
.landing-links__inner > .landing-block--links {
  /* Same prose-column constraint as .landing-body so the links list
     sits at the same x-coordinate as body prose above and reach-out
     content elsewhere on the page. */
  max-width: 720px;
  margin: 0;
}
@media (max-width: 1024px) {
  .landing-links {
    padding: 48px 20px;
    /* v2.9.284 — bumped from 40px to 48px to harmonize with
       .landing-aside, .landing-other-ways, .landing-selected-work
       mobile padding. The 8px difference was barely noticeable but
       contributed to the "messy" rhythm feeling. */
  }
}

/* v2.9.280 — `.landing-other-ways` is the auto-rendered cross-landing
   links section. Sits below the body's editor-managed
   internal_links_block (`.landing-links`) and above the footer CTA.
   Single source of truth: the editor maintains the global cross-link
   list at Site Content → Landing Cross-Links, and the template
   automatically excludes the current page so each landing shows its
   three siblings. Reuses .landing-block / .landing-block--links
   markup so all link styling, hover, and typography come from those
   classes for free. The .landing-other-ways wrapper just owns the
   section-level spacing + the hairline divider above.

   v2.9.281 — increased bottom padding from 56px / 44px to 120px / 80px.
   The footer CTA section was removed in v281 (redundant with hero
   buttons), making `.landing-other-ways` the last in-page section
   before the global site footer. The previous tighter padding was
   sized to alternate with the CTA below it; without that buffer,
   the cross-links sat too close to the global footer and read as
   cramped. The new padding matches what the old `.landing-cta` had,
   keeping the page's vertical rhythm intact at the bottom. */
.landing-other-ways {
  padding: 64px 32px 120px;
  /* v2.9.284 — top padding harmonized with .landing-aside and
     .landing-links at 64px. Keeping 120px bottom for breathing room
     before the global site footer (this is the last in-page section
     on landings since the footer CTA was removed in v281). The
     border-top is now provided by the .landing-section wrapper class
     so the explicit border-top here was redundant. */
}
.landing-other-ways__inner {
  max-width: 1500px;
  margin: 0 auto;
}
.landing-other-ways__inner > .landing-block--links {
  max-width: 720px;
  margin: 0;
}
@media (max-width: 1024px) {
  .landing-other-ways {
    padding: 48px 20px 80px;
    /* v2.9.284 — top padding bumped from 36px to 48px to harmonize
       with .landing-aside, .landing-links, .landing-selected-work
       mobile padding. The 80px bottom is preserved for global footer
       breathing room. */
  }
}

/* ─────────────────────────────────────────────────────────────────────
   UNIFIED LANDING SECTION HEAD — v2.9.284
   ─────────────────────────────────────────────────────────────────────
   Visual sectioning pattern shared by the four supporting sections
   on landing pages: Memberships, Reach Out, Continue Exploring, Other
   Ways James Works. Each gets the same eyebrow + title treatment, so
   the page reads as a sequence of consistent editorial sections rather
   than four sections that each invented their own heading style.

   Pattern:
     [hairline rule prepending eyebrow]
     EYEBROW (mono, verd-deep, 9-10px, uppercase, tracked wide)
     Title (display serif, 22-26px, with verd italic accent)

   Note that this is INTENTIONALLY smaller than the body block headings
   (28-40px) and the H1 in the hero (much larger). The size hierarchy
   reads: H1 hero (largest) → body section heads (medium) → supporting
   section heads (smaller). Without this hierarchy, every section was
   competing for attention and the page felt like four pages stacked
   together.

   The .landing-section wrapper class on each section sets up the
   shared vertical rhythm. Padding 64px / 32px desktop, 48px / 20px
   mobile. Hairline border-top divides each section from the next so
   the sectioning is explicit. Reach Out keeps its colored chalk-mid
   band as the conversion-moment exception; everything else sits on
   chalk/cream uniformly. */
.landing-section {
  border-top: 1px solid var(--rule);
}
.landing-section__head {
  margin-bottom: 32px;
  max-width: 720px;
}
.landing-section__eyebrow {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd-deep);
  margin-bottom: 12px;
  display: flex;
  align-items: center;
  gap: 12px;
}
.landing-section__eyebrow::before {
  content: '';
  width: 24px;
  height: 1px;
  background: var(--verd-deep);
  /* The 24px hairline rule before each eyebrow is the editorial-design
     anchor that ties this pattern to the existing memberships eyebrow
     it grew out of. Reads as "section marker" without needing a heavy
     visual treatment. */
}
.landing-section__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(22px, 2.2vw, 26px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0;
}
.landing-section__title em {
  color: var(--verd);
  font-style: italic;
}
.landing-section__subtitle {
  /* Used when an internal_links_block has multiple sub-blocks and the
     2nd+ block's heading needs to render. The first block's heading
     becomes the section title above; subsequent sub-blocks get this
     smaller treatment so the page doesn't have multiple competing
     section-titles in a row. */
  font-family: var(--display);
  font-weight: 400;
  font-size: 18px;
  line-height: 1.3;
  letter-spacing: 0;
  color: var(--ink);
  margin: 32px 0 12px;
  opacity: 0.85;
}
@media (max-width: 1024px) {
  .landing-section__head {
    margin-bottom: 24px;
  }
  .landing-section__title {
    font-size: clamp(20px, 5vw, 24px);
  }
}

/* On landings only: hide the inner .epk-memberships eyebrow because
   the section head above it is now providing the equivalent treatment.
   Without this, the page would render TWO eyebrows ("RECOGNITION" and
   "MEMBERSHIPS") stacked above the membership list. The inner eyebrow
   stays visible on /epk/ where the memberships card is the heading
   and there's no section wrapper providing the editorial frame. */
.landing-section.landing-aside .epk-memberships__eyebrow {
  display: none;
}
.landing-section.landing-aside .epk-memberships {
  /* When the section head provides the eyebrow, the inner card no
     longer needs its top eyebrow margin; tighten so the list sits
     closer to the section title above. */
  padding-top: 32px;
}

/* ─────────────────────────────────────────────────────────────────────
   STAT STRIP — v2.9.283
   ─────────────────────────────────────────────────────────────────────
   Credibility-anchor band rendered between hero and body when the
   editor toggles `landing_show_stats` and populates `landing_stats`.
   Pure visual rhythm. Three or four stats at most, each with a large
   display-serif value and a small mono caption underneath.

   Layout: full-bleed band with a hairline rule above and below to
   set it off from hero (above) and body prose (below). Inner is a
   horizontal flex row, items separated by hairline rules between
   them at desktop. Stacks to a column at mobile widths. Verd accent
   on values to pull the eye, mono captions in dim ink for context.

   Why hairline rules between items, not background fills: a band
   with colored fills would compete with the rest of the page's
   chalk/cream palette. Hairline rules are the editorial-design
   move that magazine spreads use — divides the items typographically
   without adding ornament.
*/
.landing-stats {
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: 0;
  background: var(--cream);
}
.landing-stats__inner {
  max-width: 1500px;
  margin: 0 auto;
  display: flex;
  align-items: stretch;
  /* Nothing here on the wrapper itself — items handle their own
     padding so the dividing rules between items extend full-height
     naturally without needing pseudo-elements. */
}
.landing-stats__item {
  flex: 1 1 0;
  padding: 32px 28px;
  border-right: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  /* Min-width:0 prevents text overflow from forcing item widths
     uneven when one stat's value is much longer than the others
     (e.g. 'Television Academy Directors Branch' vs 'STARZ'). */
}
.landing-stats__item:last-child {
  border-right: 0;
}
.landing-stats__value {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(24px, 2.2vw, 32px);
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--verd);
  /* Verd on the value itself does the credibility-accent work the
     verd-italic accent does in headlines elsewhere on the site —
     keeps the visual language consistent. */
}
.landing-stats__caption {
  margin-top: 10px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.65;
  line-height: 1.4;
}

@media (max-width: 1024px) {
  .landing-stats__inner {
    flex-direction: column;
  }
  .landing-stats__item {
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    padding: 24px 20px;
  }
  .landing-stats__item:last-child {
    border-bottom: 0;
  }
  .landing-stats__value {
    font-size: clamp(22px, 5vw, 26px);
  }
}

/* TABLET — 2-up grid for 4 items, otherwise stays as flex row.
   Specifically helps when there are 4 stats and a single horizontal
   row would crush each item too narrow. At 901-1100px the 4 stats
   become a 2x2 grid; from 1101+ they go back to single horizontal. */
@media (min-width: 1025px) and (max-width: 1100px) {
  .landing-stats__inner:has(> :nth-child(4)) {
    flex-wrap: wrap;
  }
  .landing-stats__inner:has(> :nth-child(4)) .landing-stats__item {
    flex: 0 0 50%;
    border-bottom: 1px solid var(--rule);
  }
  .landing-stats__inner:has(> :nth-child(4)) .landing-stats__item:nth-child(2n) {
    border-right: 0;
  }
  .landing-stats__inner:has(> :nth-child(4)) .landing-stats__item:nth-last-child(-n+2) {
    border-bottom: 0;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   SELECTED WORK TILES — v2.9.283
   ─────────────────────────────────────────────────────────────────────
   3-up project tile grid between body and memberships. Each tile is
   a clickable anchor to a project's detail page. Pulls poster image
   and title from the project itself; year and caption are editable
   per landing so the same project can read differently across landings.

   Layout pattern follows the home page's .work-grid for visual
   consistency — tiles share borders, hover state fills with cream,
   typography matches. The differences from home work-grid: smaller
   eyebrow, image takes more vertical real estate per tile, captions
   are tighter (one line of context not full project description).
*/
.landing-selected-work {
  background: var(--cream);
  padding: 64px 32px;
  /* v2.9.284 — padding harmonized with .landing-aside, .landing-links,
     and .landing-other-ways top padding at 64px. Border-top is now
     provided by the .landing-section wrapper class so the explicit
     declaration here was redundant. The section's eyebrow + title
     also moved to the shared .landing-section__head pattern, so the
     three formerly-bespoke .landing-selected-work__head/eyebrow/title
     rules below were removed. */
}
.landing-selected-work__inner {
  max-width: 1500px;
  margin: 0 auto;
}
.landing-selected-work__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 0;
  border-top: 1px solid var(--rule);
  /* v2.9.285 — added overflow-x: clip to handle iPad portrait edge
     case where the auto-fit grid + tile borders + 16:10 aspect-ratio
     reservations could combine to produce a hairline overflow at
     viewport widths between the mobile and tablet breakpoints.
     Clip is preferred over hidden because it doesn't establish a
     containing block for fixed-position descendants. */
  overflow-x: clip;
  max-width: 100%;
}
.landing-work-tile {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: var(--ink);
  border-bottom: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  background: transparent;
  transition: background-color 0.25s var(--ease);
  /* v2.9.285 — min-width:0 prevents grid items from refusing to
     shrink below their intrinsic content width. Without this, long
     project titles or wide poster images at iPad portrait widths
     were causing the tile to overflow its grid track by 1-2px,
     producing the "hanging over slightly" effect James reported. */
  min-width: 0;
}
.landing-work-tile:hover,
.landing-work-tile:focus-visible {
  background: var(--cream-mid, #f4f0e8);
}
.landing-work-tile:last-child {
  border-right: 0;
}
.landing-work-tile__media {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--cream-mid, #f4f0e8);
  /* Aspect-ratio reservation prevents CLS when the image loads.
     16:10 matches the home work-grid pattern for visual consistency.
     Falls back to cream-mid background while images load or when
     a project has no featured image set. */
}
.landing-work-tile__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.5s var(--ease);
}
.landing-work-tile:hover .landing-work-tile__media img {
  transform: scale(1.02);
  /* Subtle scale on hover — the editorial equivalent of a hover
     state without being too dramatic. 2% is the right amount: noticeable
     but not theme-park. */
}
.landing-work-tile__meta {
  padding: 20px 24px 28px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.landing-work-tile__year {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd);
}
.landing-work-tile__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(20px, 1.6vw, 24px);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0;
}
.landing-work-tile__caption {
  font-family: var(--display);
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink);
  opacity: 0.78;
  margin: 0;
}

@media (max-width: 1024px) {
  .landing-selected-work__grid {
    /* v2.9.285 — extended single-column behavior up to 1024px (was
       900px) to cover iPad portrait viewports cleanly. iPad portrait
       widths range from 768px (older iPad) through 820px (iPad Air)
       to 1024px (iPad Pro 12.9"). The previous 900px breakpoint
       caught the smaller iPads but iPad Pro portrait at 1024px hit
       the 901-1100 tablet rule and was being forced into 2-column,
       which produced the "hanging over slightly" overflow James
       reported. Single column at all iPad portrait widths is the
       cleaner editorial read anyway — three tiles stacked vertically
       with full poster widths reads better than two tiles cramped
       side-by-side at near-mobile widths. */
    grid-template-columns: 1fr;
  }
  .landing-work-tile {
    border-right: 0;
  }
}
@media (max-width: 1024px) {
  .landing-selected-work {
    padding: 48px 20px;
    /* v2.9.284 — harmonized with .landing-aside, .landing-links,
       .landing-other-ways mobile padding (48px / 20px). Was 56px;
       overriding to single-padding-shorthand keeps consistency. */
  }
  .landing-work-tile__meta {
    padding: 16px 0 24px;
  }
}

/* TABLET — 2 columns. v2.9.285 narrowed range from 901-1100 to
   1025-1100 so iPad Pro 12.9" portrait (1024px) gets single-column
   treatment along with the smaller iPads. Devices at 1025-1100px
   are typically iPad Pro 12.9" landscape or smaller laptops, where
   2-column tiles read well with enough horizontal space per tile. */
@media (min-width: 1025px) and (max-width: 1100px) {
  .landing-selected-work__grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .landing-work-tile:nth-child(2n) {
    border-right: 0;
  }
  .landing-work-tile:nth-child(2n+1) {
    border-right: 1px solid var(--rule);
  }
}

.landing-block--quote {
  border-left: 3px solid var(--verd);
  padding-left: 24px;
  margin: 64px 0;
  max-width: 720px;
}
.landing-block__quote {
  margin: 0;
  font-family: var(--display);
}
.landing-block__quote p {
  font-size: 26px;
  line-height: 1.35;
  font-style: italic;
  font-weight: 300;
  color: var(--ink);
  margin: 0 0 12px;
}
.landing-block__quote-attr {
  font-family: var(--mono);
  font-style: normal;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--verd);
  opacity: 0.75;
}

/* MEMBERSHIPS ASIDE ──────────────────────────────────────────────────
   v266 — duplicated the .epk-memberships rules from epk.css here so
   the card renders correctly on landing pages. epk.css only enqueues
   on /epk/, so on a landing the classes referenced by single-landing.php
   had no styles attached and rendered as default browser bullets.
   Inner content stays left-aligned (matches .epk-bio structure on
   the EPK page where memberships sits in the right column but is
   still left-aligned within its column). */
.landing-aside {
  padding: 64px 32px;
  /* v269 — content-visibility removed for the same reason as
     .landing-body. Memberships card is short and ships in initial
     HTML; the CV hint added more risk than benefit on mobile.

     v2.9.284 — padding harmonized with the other supporting sections
     (.landing-links, .landing-other-ways) so vertical rhythm is
     consistent. Was 32px which read as cramped next to 80px on the
     adjacent reach-out section. The card itself constrains its width
     via .epk-memberships max-width below. */
}
.landing-aside__inner {
  /* No max-width override — uses the parent 1500px. The card
     itself constrains its width via max-width on .epk-memberships
     so it doesn't stretch absurdly wide on the empty desktop side. */
}
.landing-aside .epk-memberships {
  /* Constrain the card on wide viewports so it doesn't span the full
     1500px frame — that would feel disconnected from the body prose
     above it. Match the ~720 reading column. */
  max-width: 720px;
  padding: 36px 32px;
  border: 1px solid var(--rule);
  background: var(--cream, transparent);
}
.landing-aside .epk-memberships__eyebrow {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd-deep);
  margin-bottom: 24px;
  display: flex;
  align-items: center;
  gap: 12px;
}
.landing-aside .epk-memberships__eyebrow::before {
  content: '';
  width: 24px;
  height: 1px;
  background: var(--verd-deep);
}
.landing-aside .epk-memberships__list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-family: var(--display);
  font-weight: 300;
  font-size: 17px;
  line-height: 1.4;
  color: var(--ink);
}
.landing-aside .epk-memberships__list li {
  padding: 16px 0;
  border-top: 1px solid var(--rule);
}
.landing-aside .epk-memberships__list li:first-child {
  border-top: 0;
  padding-top: 0;
}
.landing-aside .epk-memberships__list li:last-child {
  padding-bottom: 0;
}
@media (max-width: 1024px) {
  .landing-aside {
    padding: 48px 20px;
    /* v2.9.284 — harmonized with .landing-links, .landing-other-ways,
       .landing-selected-work mobile padding (48px / 20px). */
  }
}

/* REACH-OUT MODULE ───────────────────────────────────────────────────
   Left-aligned in the 1500 frame. Constrains the methods column to
   720px so the data-dense rep cards stay readable. Underlines on
   contact-link removed — phone/email render as plain text with
   verd-on-hover for affordance. */
.landing-reachout {
  padding: 80px 32px;
  background: var(--chalk-mid, rgba(45, 106, 91, 0.04));
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  /* v269 — content-visibility removed for the same reason as
     .landing-body. Server-rendered contact-method blocks ship with
     the initial HTML so no layout reservation is needed. */
  /* When the secondary CTA at the top of the page jumps to
     #landing-reachout, scroll-margin-top gives the section ~24px of
     breathing room above the eyebrow so it lands at a natural reading
     position rather than flush with the viewport top. */
  scroll-margin-top: 24px;
}
/* v2.9.284 — `.landing-reachout__eyebrow`, `.landing-reachout__heading`,
   and `.landing-reachout__heading em` removed. The eyebrow + heading
   are now provided by the shared `.landing-section__head` /
   `.landing-section__eyebrow` / `.landing-section__title` pattern,
   which all four supporting sections (Memberships, Reach Out,
   Continue, Other Ways) use for visual consistency. */
.landing-reachout__methods {
  max-width: 720px;
}
.landing-reachout__methods .contact-method {
  padding: 20px 0;
  border-bottom: 1px solid var(--rule);
}
.landing-reachout__methods .contact-method:first-child {
  border-top: 1px solid var(--rule);
}
.landing-reachout__methods .contact-method__label {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd);
  margin-bottom: 6px;
}
.landing-reachout__methods .contact-method__label-sep {
  margin: 0 8px;
  opacity: 0.4;
}
.landing-reachout__methods .contact-method__value {
  font-family: var(--display);
  font-size: 18px;
  font-weight: 400;
  color: var(--ink);
}
.landing-reachout__methods .contact-method__rep-name {
  font-size: 18px;
}
.landing-reachout__methods .contact-method__rep-title {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.6;
  margin-top: 4px;
}
.landing-reachout__methods .contact-method__rep-lines {
  font-family: var(--display);
  font-size: 16px;
  margin-top: 8px;
}
/* Phone + email — plain text appearance, NO underline. Verd-on-hover
   provides the affordance instead. Matches how /inquiries/ renders
   the same elements. */
.landing-reachout__methods .contact-link {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 0;
  transition: color 0.2s var(--ease);
}
.landing-reachout__methods .contact-link:hover {
  color: var(--verd);
}

/* FOOTER CTA ─────────────────────────────────────────────────────── */
.landing-cta {
  padding: 80px 32px 120px;
  /* Left-aligned to match site convention. The buttons used to be
     centered, which read as a different page-system than the
     left-aligned hero + body above. */
}
.landing-cta__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  /* Removed justify-content: center — buttons align flush left. */
  align-items: center;
}

/* MOBILE ─────────────────────────────────────────────────────────── */
@media (max-width: 1024px) {
  .landing-hero {
    padding: 80px 20px 48px;
  }
  .landing-hero__title {
    font-size: clamp(40px, 10vw, 72px);
  }
  .landing-hero__lede {
    font-size: 17px;
  }
  .landing-hero__cta {
    flex-direction: column;
    align-items: stretch;
  }
  .landing-btn {
    justify-content: center;
    padding: 14px 24px;
  }
  .landing-body {
    padding: 56px 20px;
  }
  .landing-block {
    margin-bottom: 40px;
  }
  .landing-block__heading {
    font-size: clamp(24px, 6vw, 32px);
  }
  .landing-block__body,
  .landing-block__intro {
    font-size: 17px;
  }
  .landing-block__list li {
    font-size: 16px;
  }
  .landing-block__link-label {
    font-size: 19px;
  }
  .landing-block__quote p {
    font-size: 21px;
  }
  .landing-aside {
    padding: 24px 20px;
  }
  .landing-aside .epk-memberships {
    padding: 28px 24px;
  }
  .landing-aside .epk-memberships__list {
    font-size: 16px;
  }
  .landing-reachout {
    padding: 56px 20px;
  }
  .landing-cta {
    padding: 56px 20px 80px;
  }
  .landing-cta__inner {
    flex-direction: column;
    align-items: stretch;
  }
}


/* ═══════════════════════════════════════════════════════════════════
   HOME-SERVICES SECTION — v2.9.271
   
   Cards on the home page linking to the Landing CPT posts (SEO
   capture URLs at /director-los-angeles/, /documentary-filmmaker/,
   /commercial-director/, /director-for-hire/, etc.). Renders between
   the home reel and the Selected Work grid.
   
   v2.9.271 alignment fix (returns to v269 pattern with corrections):
   the section background is full-bleed chalk-mid so the section
   reads as a separator band rather than a bordered card-block.
   The 1500px content frame lives on .home-services__inner — same
   pattern .bio-deep uses, so About Me's left edge and the Direction
   across formats heading edge land at the SAME x-coordinate.
   
   Card content alignment: cards lose their left padding (was 28px)
   so the DIRECTING eyebrow and Film Director title sit flush with
   the section heading above. Top/right/bottom padding stays for
   breathing room. The grid borders that separate the cards still
   render correctly because borders aren't padding-dependent.
   
   Below 901px: cards stack and DO get left padding back, since at
   mobile widths the grid borders disappear and the cards need their
   own left rhythm anchor — but it's reduced to 0 to match the
   section heading edge consistently.
   ═══════════════════════════════════════════════════════════════════ */
.home-services {
  background: var(--chalk-mid, rgba(45, 106, 91, 0.04));
  padding: 96px 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
.home-services__inner {
  max-width: 1500px;
  margin: 0 auto;
  padding: 0 32px;
}
/* Modifier on .section-head when used inside home-services — adds
   bottom margin matching the lineage intro spacing, and lets the
   head block stay column-narrow rather than spreading to the full
   1500px width. The default .section-head behavior elsewhere on the
   site is unaffected. */
.home-services .section-head--has-lede {
  margin-bottom: 56px;
  align-items: flex-start;
}
.home-services .section-head--has-lede > div {
  max-width: 760px;
}
.home-services__eyebrow {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--verd);
  margin-bottom: 20px;
}
.home-services__lede {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(17px, 1.4vw, 20px);
  line-height: 1.55;
  color: var(--ink);
  opacity: 0.85;
  margin: 24px 0 0;
  max-width: 640px;
}
.home-services__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0;
  border-top: 1px solid var(--rule);
}
.home-service-card {
  display: flex;
  flex-direction: column;
  /* v2.9.282 — gap reduced from 14px to 10px since the card now
     contains only two elements (eyebrow + title) instead of four
     (eyebrow + title + desc + cta). The looser 14px gap was sized
     for the multi-element card; with the lean card it produced too
     much air between eyebrow and title. */
  gap: 10px;
  /* v2.9.271 — left padding removed so card content (DIRECTING
     eyebrow, Film Director title) sits flush with the section
     heading above. Right padding preserved so card content doesn't
     run into the border. Top/bottom unchanged for vertical rhythm.

     v2.9.282 — vertical padding reduced from 32px to 28px. The
     card's content height halved when desc and CTA were removed,
     so the original 32px padding read as too tall around a
     two-line card. 28px keeps the card breathing without lofting. */
  padding: 28px 28px 28px 0;
  border-bottom: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  background: transparent;
  text-decoration: none;
  color: var(--ink);
  transition: background-color 0.25s var(--ease);
}
.home-service-card:hover,
.home-service-card:focus-visible {
  background: var(--cream);
}
/* Cards 2..n inside the grid get internal left padding to give the
   border-separator visual weight; the FIRST card stays flush so its
   text-edge aligns with the section heading above. */
.home-service-card:not(:first-child) {
  padding-left: 28px;
}
.home-service-card:last-child {
  border-right: 0;
}
.home-service-card__eyebrow {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--verd);
}
.home-service-card__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(22px, 2vw, 28px);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0;
}
.home-service-card__title em {
  color: var(--verd);
  font-style: italic;
}
/* v2.9.282 — .home-service-card__desc and .home-service-card__cta
   removed. Both elements were stripped from the markup at the same
   version because the card descriptions duplicated text already on
   each landing's hero, and the "Read more →" CTA repeated what the
   anchored card already signals (cards are clickable, the hover
   state confirms it). The lean card now shows only eyebrow + title.
   This change does not affect schema or LLM signals: landing_hero_lede
   is still read directly from the landing posts by the schema emitter
   and the on-landing rendering, both independently of how the home
   page chooses to display things. */

/* MOBILE — stack cards full width.
   At mobile widths the grid borders disappear (cards stack
   vertically, no visible column separator), so each card becomes
   self-contained. ALL cards lose their left padding so they line up
   with the section heading the same way they do on desktop. */
@media (max-width: 1024px) {
  .home-services {
    padding: 48px 0;
  }
  .home-services__inner {
    padding: 0 20px;
  }
  .home-services .section-head--has-lede {
    margin-bottom: 32px;
  }
  .home-service-card,
  .home-service-card:not(:first-child) {
    /* v2.9.282 — mobile padding reduced from 28px to 22px in concert
       with desktop reduction. Same rationale: lean card with only
       eyebrow + title needs less vertical breathing room than the
       previous 4-element card. */
    padding: 22px 0;
    border-right: 0;
  }
}

/* TABLET — 2 columns */
@media (min-width: 1025px) and (max-width: 1200px) {
  .home-services__grid {
    grid-template-columns: repeat(2, 1fr);
  }
  /* In a 2-up grid, the first card (col 1) and third card (col 1
     of row 2) need flush-left, the second and fourth need internal
     left padding since they're in column 2. Reset the universal
     :not(:first-child) rule above and re-apply by column position. */
  .home-service-card:nth-child(2n+1),
  .home-service-card:nth-child(2n+1):not(:first-child) {
    padding-left: 0;
  }
  .home-service-card:nth-child(2n) {
    padding-left: 28px;
    border-right: 0;
  }
  .home-service-card:nth-child(2n+1) {
    border-right: 1px solid var(--rule);
  }
}

/* ─────────────────────────────────────────────────────────────────────
   IPAD PORTRAIT TEXT-SIZE AUDIT — v2.9.290 (corrected v2.9.291)
   ─────────────────────────────────────────────────────────────────────
   v2.9.289 bumped the mobile breakpoint from 900px to 1024px to fix
   Safari iPad's Request-Desktop-Site behavior (which reports 1024px
   regardless of physical screen size). That correctly serves iPad
   portrait the mobile/tablet treatment, but the existing mobile rules
   were tuned for phone viewports (320-414px), where body text at
   14-16px reads correctly given the narrow column width and short
   reading distance. At iPad portrait (768-1024px), those same sizes
   read as undersized — there's more screen, more reading distance,
   and the content has more horizontal space to fill.

   This block adds an intermediate-tier text size layer specifically
   for the iPad portrait viewport range (720-1024px). It bumps
   body-text and listing-text sizes back UP toward desktop values,
   while leaving heading sizes alone (those are sized via clamp()
   already and scale appropriately) and leaving caption/eyebrow text
   alone (those are mono/uppercase utility text that should stay
   small for editorial weight contrast).

   v2.9.291 IMPORTANT — this block originally (v290) included
   .journal-post__body, .bio-deep__body, .syn-body, and .faq-item__a
   at sizes 17-19px. That was a regression: the v2.9.244 mobile
   typography scale block (around line ~10502) already correctly
   sets those four selectors to 20px on iPad portrait. v290's
   "audit" was overriding good v244 work with worse values. The
   v291 audit caught this conflict and removed those four selectors
   from this block entirely, so the v244 rules continue to govern
   long-form prose on iPad. This block now ONLY targets selectors
   that v244 didn't cover.

   Audit principles applied:
     1. Body prose for surfaces NOT covered by v244 (lede paragraphs,
        excerpts, archive quotes) bumps to 16-18px.
     2. Listing/card titles bump to 19-22px — give buyer-scanning
        content visual hierarchy.
     3. Captions, eyebrows, mono utility text stays at 9-11px (or
        whatever v244 set) — editorial weight contrast is the point.
     4. Headings already use clamp() and scale fine — no changes.
     5. Long-form prose families are HANDLED BY v244 — not duplicated
        here.

   The 720px lower boundary is deliberate: smaller iPad mini portrait
   (744px) and all iPad portrait viewports (768-1024px) are covered;
   phones (≤414px on most devices, ≤720px on absolute extremes like
   iPad mini landscape) still get the smaller phone-tuned sizing. */

@media (min-width: 720px) and (max-width: 1024px) {
  /* Body prose — page-level reading content */
  .bio-hero__lede { font-size: 17px; }
  .lineage-film-card__why { font-size: 16px; }
  .journal-entry__excerpt { font-size: 16px; }
  .archive-card__quote { font-size: 16px !important; }
  .press-card__quote { font-size: 18px; }
  .press-archive__status { font-size: 14px !important; }
  .favs-films-grid.is-mobile-swipe > .favs-film .favs-film__director { font-size: 14px !important; }

  /* Listing/card titles — buyer-scanning weight */
  .journal-entry__title { font-size: 22px; }
  .lineage-item__name { font-size: 19px; }
  .is-mobile-swipe > .work-card .work-card__title { font-size: 19px; }
  .favs-films-grid.is-mobile-swipe > .favs-film .favs-film__title { font-size: 20px !important; }

  /* NOTE: .journal-post__body, .bio-deep__body, .syn-body, .faq-item__a
     are intentionally NOT in this block. The v2.9.244 mobile typography
     scale at line ~10502 already handles them correctly at 20px on
     iPad portrait. v2.9.290 originally tried to bump them to 19px, but
     that REGRESSED them from the v244-correct 20px. v2.9.291 audit
     caught this and removed the conflicting overrides. */

  /* Editorial body/quote weights — landing-specific (not covered by v244) */
  .landing-block__body,
  .landing-block__intro { font-size: 18px; }
  .landing-block__list li { font-size: 17px; }
  .landing-block__link-label { font-size: 21px; }
  .landing-block__quote p { font-size: 23px; }
  .landing-aside .epk-memberships__list { font-size: 17px; }
  .landing-hero__lede { font-size: 18px; }

  /* On-location lede also bumps for tablet readability */
  .on-location__hero-lede { font-size: 17px; }

  /* FAQ question — too thin at 18px on tablet, bump to 20px.
     (FAQ answer .faq-item__a stays at 20px from v244.) */
  .faq-item__q { font-size: 20px; }

  /* Press archive — drawer chips and labels were sized for phone
     touch targets; on tablet they look too small */
  .press-filters-block .filter-row__label { font-size: 12px !important; }
  .press-filters-block .fchip { font-size: 13px !important; }

  /* On-camera filters chips — same logic as press archive */
  .on-camera-filters__chips .fchip { font-size: 13px; }

  /* Journal filter chips bump for tablet */
  .journal-filters .fchip { font-size: 11px; }

  /* Rep panel headline scales between phone (24px) and desktop (28px) */
  .rep-panel__headline { font-size: 26px; }
}
