/* ============================================================
   site/css/style.css  —  HC-GOOD-013c
   COMPLETE REPLACEMENT FILE — drop into site/css/
   ============================================================ */

/* ─── 1. FONTS ─────────────────────────────────────────────── */
/* Athena/Otto @font-face removed 2026-09-10 -- CORRECTION: these fonts were
   NOT broken -- the files exist at site/fonts/ and were loading fine. They
   were removed because Figma's "01 -- Typography & Colour" page specifies a
   different typeface system: Bodoni Moda (titles), Source Serif 4 (body),
   Inter (UI/nav/labels). Athena-Regular.woff2 / Otto.woff2 are left on
   disk, untouched, in case this is reverted.

   Self-hosted 2026-09-10 (was Google Fonts CDN via <link>): this sandbox's
   network blocks fonts.googleapis.com/fonts.gstatic.com outright (confirmed
   via curl, both from the cloud container and this Mac's own local shell --
   an organisation-level egress policy, not something togglable from here),
   so live-rendering the site here for verification always fell back to
   system fonts. Downloaded the real font files instead (via npm's
   @fontsource packages -- registry.npmjs.org IS reachable) and self-hosted
   them under site/fonts/. This removes the Google Fonts dependency
   entirely -- faster, works offline, and (not just a workaround) now lets
   this sandbox render the site with the correct typefaces for verification
   screenshots going forward. Your own Safari on your normal network was
   very likely loading the Google Fonts CDN fine regardless -- that block is
   specific to this sandbox's tooling, not a website-facing problem. */
@font-face {
  font-family: "BroetownSignature";
  src: url("../fonts/BroetownSignature.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Bodoni Moda";
  src: url("../fonts/bodoni-moda/bodoni-moda-latin-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Bodoni Moda";
  src: url("../fonts/bodoni-moda/bodoni-moda-latin-500-italic.woff2") format("woff2");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Source Serif 4";
  src: url("../fonts/source-serif-4/source-serif-4-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Source Serif 4";
  src: url("../fonts/source-serif-4/source-serif-4-latin-600-normal.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Source Serif 4";
  src: url("../fonts/source-serif-4/source-serif-4-latin-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../fonts/inter/inter-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../fonts/inter/inter-latin-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../fonts/inter/inter-latin-500-italic.woff2") format("woff2");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}

/* ─── 2. TOKENS ─────────────────────────────────────────────── */
:root {
  /* Updated 2026-09-10 per Figma "01 -- Typography & Colour" colour system.
     --bg-black/--text-light (dark-hero sections) are NOT defined by that
     page -- it documents a light-first system only -- so they're left as-is
     pending confirmation; everything else below is a direct, confirmed
     mapping to Figma's --color-* tokens. */
  --bg-black:  #0b0c0d;
  --bg-off:    #F6F5F1;  /* was #e9e6df -- now Figma --color-bg-primary (Website Off White) */
  --text-dark: #141414;  /* was #0b0c0d -- now Figma --color-text-primary */
  --text-light:#e9e6df;  /* unchanged -- no dark-bg text token in the new colour system, see note above */
  --grove:     #1B3226;
  --sage:      #8B9E84;
  --brass:     #8A7860;  /* = Figma --color-brand-primary */
  --brass-2:   #94703D;  /* = Figma --color-brand-secondary -- use sparingly */
  --ink-deep:  #0B0C0D;  /* = Figma --color-ink-deep -- text-only details, = --bg-black */
  --surface-2: #FFFFFF;  /* = Figma --color-surface-secondary -- elevated surfaces */
  --text-secondary: #595752;
  --ui-muted:  #9E9991;
  /* Figma page "02 -- Grid, Motion & Voice" (node 410:5) explicitly flags
     its own GRID SYSTEM numbers as stale and says the site's already-built
     values take precedence: nav is 88px (not 72px), content is 56px margin
     each side / 1328px max width at 1440 viewport (not the 1180px/24px
     formula) -- consistent across Homepage, About, Contact, Client,
     Articles, World Presence per that same node. Corrected here to match
     the authoritative built-page numbers, 2026-09-10. */
  --wrap:      min(1328px, calc(100% - 112px));
  --nav-h:     88px;
  --radius:    999px;

  /* Lockup nudge — adjust values here only */
  --h-nudge-x:          38px;
  --h-nudge-y:          25px;
  --strap-nudge-y:       0px;
}

/* ─── 3. RESET ──────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: "Source Serif 4", "Athena", serif;
  color: var(--text-dark);
  background: var(--bg-off);
  overflow-x: hidden;
}
::selection { background: rgba(0,0,0,0.85); color: var(--text-light); }
.section--light ::selection { background: rgba(255,255,255,0.95); color: #000; }

/* --- 4. CUSTOM CURSOR -- REMOVED 2026-09-10 ---------------
   Jimi: "the cursor is a weird dot please change back to an arrow like
   normal." Native arrow cursor everywhere now. */

/* ─── 5. PAGE TRANSITION OVERLAY ────────────────────────────── */
.page-transition {
  position: fixed; inset: 0;
  background: var(--bg-off);
  z-index: 9998;
  pointer-events: none;
  opacity: 0;
}
.page-transition.is-leaving { opacity:1; pointer-events:auto; transition: opacity 350ms ease; }
.page-transition.is-entering { opacity:1; pointer-events:auto; transition: opacity 400ms ease 50ms; }
.page-transition.is-done { opacity:0; transition: opacity 450ms ease; }

/* ─── 6. SCROLL PROGRESS ────────────────────────────────────── */
.scroll-progress {
  position: fixed;
  top: 0; left: 0;
  height: 2px;
  background: var(--brass);
  width: 0%;
  z-index: 1001;
  pointer-events: none;
  transition: width 0.1s linear;
}

/* ─── 7. NAVIGATION ─────────────────────────────────────────── */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  z-index: 1000;
  color: var(--text-dark);
  background: #FBFBFB;
}
/* Nav rule (Figma 119:582): 1px hairline, grows from under HENDRICKS on
   first load to its full header-width size/position. Rebuilt 2026-09-10 --
   the old width:0->100% animation grew from the viewport's left edge, not
   from under the HENDRICKS wordmark as its own name/spec says. Now sized
   at its FINAL width/position always (no layout shift), and js/nav-rule.js
   animates transform:scaleX() with transform-origin set (in JS, measured
   against the real .brand element so it's exact at any viewport width) to
   the horizontal center of "HENDRICKS" -- so it visibly starts as a short
   segment under the wordmark and grows outward from there to full width.
   Plays once, no loop. */
.nav-rule {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 1px;
  width: 100%;
  background: var(--ink-deep, #0B0C0D);
  transform: scaleX(0);
}
/* Exact spec pulled from Figma "Nav Bar" (119:686) via the Plugin API,
   2026-09-10 -- NOT approximated. Brand: Bodoni Moda Medium 500, 20px,
   letter-spacing 1.2px (absolute, not em). Nav links: Inter Medium 500,
   11px, letter-spacing 1.2px absolute, opacity 0.75 (0.92 was a guess from
   an earlier pass -- corrected). CLIENT is plain text, styled identically
   to ABOUT/PORTFOLIO/ARTICLES/CONTACT -- Figma has NO border/pill/box
   around it. The .nav-pill treatment below was a fabrication with no
   source in Figma; removed. */
.brand {
  font-family: "Bodoni Moda", serif;
  font-size: 20px;
  font-weight: 500;
  letter-spacing: 1.2px;
  text-transform: none;
  text-decoration: none;
  color: inherit;
}
.nav-desktop { display: flex; align-items: center; gap: 22px; }
.nav-desktop a {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: inherit;
  text-decoration: none;
  opacity: 0.75;
}
.nav-desktop a:hover,
.nav-desktop a.is-active { opacity: 1; }

/* ─── 8. MOBILE MENU ────────────────────────────────────────── */
.burger {
  display: none;
  border: 0; background: transparent; color: inherit;
  width: 44px; height: 44px; padding: 0; margin-left: 10px; cursor: pointer;
}
.burger span {
  display: block; width: 20px; height: 1px;
  background: currentColor; margin: 5px auto;
  transition: transform 0.25s ease, opacity 0.25s ease;
}
body.menu-open .burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
body.menu-open .burger span:nth-child(2) { opacity: 0; }
body.menu-open .burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

.mobile-menu {
  position: fixed; inset: 0;
  z-index: 999;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.mobile-menu__panel {
  position: absolute;
  top: var(--nav-h); right: 14px; left: 14px;
  border-radius: 18px;
  padding: 18px;
  background: rgba(246,245,241,0.94);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(0,0,0,0.08);
  display: flex; flex-direction: column; gap: 12px;
}
.m-link {
  color: var(--text-dark); text-decoration: none;
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  padding: 10px; border-radius: 12px;
}
.m-link:hover { background: rgba(255,255,255,0.06); }
.m-link.is-active { background: rgba(255,255,255,0.08); }
.m-link--pill { border: 1px solid rgba(255,255,255,0.35); }
body.menu-open .mobile-menu { pointer-events: auto; opacity: 1; }

/* ─── 9. LAYOUT ─────────────────────────────────────────────── */
main { padding-top: var(--nav-h); }
.wrap { width: var(--wrap); margin: 0 auto; }
.section { padding: 84px 0; }

.section--light {
  position: relative;
  color: var(--text-dark);
  background: var(--bg-off);
  border-top: 1px solid rgba(0,0,0,0.08);
  border-bottom: 1px solid rgba(0,0,0,0.08);
}
/* Grain on limestone */
.section--light::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none; z-index: 0;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: multiply;
}
.section--light .wrap,
.section--light > .wrap { position: relative; z-index: 1; }

/* ─── 10. HERO ──────────────────────────────────────────────── */
.hero {
  position: relative;
  overflow: hidden;
}
/* Homepage building-rotation hero: full-width section carrying the light-to-
   dark gradient band (FBFBFB at top, meeting the nav, to ECECEC at the
   bottom -- mirrors back up through .section--intro/.section--watermark).
   The building photo itself is NOT full-bleed -- it's a smaller centered
   floating element (see .hero-building below) with no background box of its
   own, so the gradient is visible around it. */
.hero--rotation {
  display: flex;
  flex-direction: column;
  overflow: visible; /* override .hero{overflow:hidden} -- the wordmark needs to bleed to the viewport edge; body{overflow-x:hidden} still clips it there */
  background: linear-gradient(180deg, #FBFBFB 0%, #ECECEC 100%);
}
/* Image + timeline-nav area only. Building photo and timeline nav/labels are
   absolutely positioned WITHIN this box (not the whole hero section) so the
   wordmark/tagline below can sit in normal flow underneath, instead of being
   centered on top of the photo -- matches Figma's mobile hero spec (frame
   878:4), where "Hero Image" (390x480) and "Hero Text" (390x230, the/
   HENDRICKS/collective/strap/scroll) are two stacked, non-overlapping
   blocks, not one overlaid composite. */
.hero__stage {
  position: relative;
  min-height: min(700px, 63vh); /* 2026-09-16 round 6: eased back to midpoint alongside the .hero-building scale-back above */
}
.hero__video,
.hero__fallback {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
.hero__fallback { display: none; }
.hero__video {
  animation: heroZoom 30s ease-out forwards;
  will-change: transform;
}
@keyframes heroZoom { from { transform: scale(1); } to { transform: scale(1.05); } }
@media (prefers-reduced-motion: reduce) { .hero__video { animation: none; } }

.hero__scrim {
  /* Removed 2026-09-10: this heavy dark vignette (up to 82% black) was designed for a
     full-bleed photographic hero. Figma's hero is a small contained image on the cream
     page background with no vignette at all -- flagged, not silently kept. */
  display: none;
}
.hero__lockup {
  position: relative; z-index: 2;
  width: 100%;
  text-align: center;
  /* 194px top-gap / 1440px canvas width = 13.47vw, read from Figma node
     119:566's y-offset inside the 1440x900 hero frame (119:564). Bottom
     padding is 0 -- in Figma the lockup box sits flush against the top of
     the building photo (194 + 148 = 342 = the photo's own y). */
  padding: clamp(32px, 13.47vw, 150px) 18px 0;
}

/* ─── 11. LOCKUP ────────────────────────────────────────────── */
.lockup {
  position: relative;
  display: inline-grid;
  grid-template-rows: auto;
  align-items: center;
  justify-items: center;
  gap: 10px;
}
.lockup__row {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 18px;
}

/* H — BroetownSignature. opacity:0 set here; brand.js reveals via opacity only.
   transform is the CSS nudge — brand.js must NEVER set style.transform on this element. */
.lockup__h {
  font-family: "BroetownSignature", cursive;
  font-size: 160px;
  line-height: 0.85;
  font-weight: 400;
  color: var(--text-light);
  transform: translate(var(--h-nudge-x), var(--h-nudge-y));
  opacity: 0;
}

/* endricks — Otto. Starts hidden with slight slide. */
.lockup__endricks {
  font-family: "Bodoni Moda", serif;
  font-size: 82px;
  line-height: 1;
  font-weight: 400;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--text-light);
  opacity: 0;
  transform: translateY(12px);
}

/* Caption block below the hero photo: tagline + scroll cue. Both are
   separate top-level nodes in Figma's hero frame (305:576 "Iconic
   experiences..." and 119:581 "SCROLL"), positioned after the photo, NOT
   part of the the/HENDRICKS/collective lockup above it -- exact spec pulled
   via the Plugin API, 2026-09-10. */
.hero__caption {
  /* position:relative anchors .hero__scroll-line below; z-index:3 (2026-
     09-10) keeps this text above .hero__stage::after -- that fade extends
     well past the stage's own bottom edge (by design, so it reaches
     "behind the text" per Jimi's request) and was otherwise painting OVER
     this caption's text (its own children have no stacking context, so a
     sibling with an explicit z-index paints above them regardless of DOM
     order) -- verified via Playwright the SCROLL text had gone fully
     invisible under it. */
  position: relative;
  z-index: 3;
  text-align: center;
  padding: 12px 18px 40px;
}
/* Strapline -- Bodoni Moda Medium 500, 16px, #0B0C0D (--ink-deep), centered,
   0 letter-spacing (was wrongly Inter/0.04em before this pass). */
.hero__strap {
  margin: 0;
  font-family: "Bodoni Moda", serif;
  font-weight: 500;
  font-size: clamp(13px, 1.1vw, 16px);
  letter-spacing: 0;
  color: var(--ink-deep);
  transform: translateY(var(--strap-nudge-y));
}
.strap-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(8px);
}
/* Scroll cue -- Inter Medium 500, 10px, 2px letter-spacing, --text-secondary
   at 60% opacity, per Figma node 119:581. Not present in the HTML before
   this pass -- added to match, flagged as a new element, not silently
   invented (exact type spec, not a guess). */
.hero__scroll-cue {
  display: block;
  margin: 19px 0 0;
  font-family: "Inter", sans-serif;
  font-weight: 500;
  font-size: 10px;
  letter-spacing: 2px;
  color: var(--text-secondary);
  opacity: 0.6;
}
/* Scroll-encouragement line, added 2026-09-10 per Jimi's request, revised
   same day to reach much further -- "just above the h of hospitality" --
   spanning from under SCROLL down to the intro section's heading, with
   that heading revealing exactly as the line finishes. That distance
   depends on real layout (fonts, viewport, content height) so the actual
   height/animation is set by js/hero-scroll-line.js, not a fixed CSS
   value; this is just the static base look before JS runs (and the
   no-JS/reduced-motion fallback). transform-origin:top + scaleY is what
   makes it read as growing from a small dot down to its full length. */
.hero__scroll-line {
  /* position:absolute (2026-09-10 fix): this was a normal-flow block
     before, so js/hero-scroll-line.js setting its height to reach the
     intro heading PUSHED that heading further down the moment it did --
     invalidating the very distance it had just measured (self-referential
     layout loop -- verified via Playwright: an initial ~136px computed
     height should have been ~274px once the resulting push was accounted
     for). Taking it out of flow means growing it never moves anything
     else, so one measurement is correct. */
  position: absolute;
  left: 50%;
  top: 0; /* set precisely by JS, just below the SCROLL text */
  width: 1px;
  margin: 0;
  background: var(--text-secondary);
  transform-origin: top center;
  transform: translateX(-50%) scaleY(1);
  opacity: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .hero__scroll-line { opacity: 0; }
}

/* ─── 12a. UI / LABEL TYPEFACE -- Inter, per Figma type system ──────
   Applies to nav, small caps labels, captions, metadata, form UI.
   NOTE: colour is left as each element already had it (inherit / opacity /
   var(--brass) on the ones that were already accented) -- NOT force-set to
   brass (#8A7860) site-wide. Several of these (.brand, .nav-desktop a,
   .m-link) rely on `mix-blend-mode: difference` against hero photography
   for legibility; hard-coding brass would break that. Flagged for Jimi:
   confirm whether brass should be forced on all small text regardless of
   background, or only on the labels that already use it (spotlight/article
   index + meta, world-map legend). */
.nav-desktop a,
.m-link,
.ph__label,
.project__title,
.project__sub,
.spotlight-row__index,
.spotlight-row__link,
.about-hero-light__eyebrow,
.about-hero-light__label,
.about-hero-light__counter,
.worldband__label,
.worldband__legend,
.footer__email,
.footer__copy,
.portfolio-gate__form input,
.portfolio-gate__form button,
.portfolio-gate__error,
.portfolio-gate__request,
.portfolio-gate__privacy,
.watermark-caption,
.home-portfolio-preview__label,
.home-portfolio-preview__item span,
.home-portfolio-preview__nav,
.article-pair__meta,
.article-pair__link {
  font-family: "Inter", system-ui, sans-serif;
}

/* ─── 12. TYPOGRAPHY ────────────────────────────────────────── */
.h1 { font-family:"Bodoni Moda",serif; font-weight:500; font-size:clamp(34px,5vw,54px); letter-spacing:0.02em; margin:0 0 14px; }
.h2 { font-family:"Bodoni Moda",serif; font-weight:500; font-size:clamp(22px,2.8vw,32px); letter-spacing:0.01em; margin:0 0 14px; }
.h3 { font-family:"Bodoni Moda",serif; font-weight:500; font-size:16px; letter-spacing:0.01em; margin:14px 0 6px; }
.body    { font-size:16px; line-height:1.8; margin:0 0 14px; max-width:64ch; }
.body-sm { font-size:13px; line-height:1.7; margin:0; opacity:0.88; }

/* ─── 13. GRIDS ─────────────────────────────────────────────── */
.grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:42px; align-items:start; }
.feature-row { display:grid; grid-template-columns:220px 1fr; gap:42px; align-items:start; }
.masonry-3 { margin-top:44px; display:grid; grid-template-columns:1fr 1fr 1fr; gap:22px; align-items:start; }
/* 2026-09-15: all three images are now square (.ph--ratio-1x1) and this
   modifier makes them explicitly smaller + fixed-width (not just three
   equal-fraction columns stretching to fill the row) so they read as a
   deliberately small trio, not a full-width mosaic. auto-columns keeps
   them left-packed/top-aligned in a single row rather than stretching. */
.masonry-3--square {
  display: flex;
  gap: 18px;
  justify-content: start;
  margin-top: 132px; /* 3x the base .masonry-3 44px gap, per Jimi’s request */
}
.masonry-3--square > .ph:nth-child(1),
.masonry-3--square > .ph:nth-child(2) { flex: 0 0 324px; width: 324px; }
.masonry-3--square > .ph:nth-child(3) { flex: 0 0 644px; width: 644px; height: 324px; aspect-ratio: auto; }
/* .services-copy-grid / .section--services-copy / .service-copy removed
   2026-09-11 -- that 3-column Retail/Hospitality/Residential prose block
   had no Figma source; 119:593 is actually the Offerings Carousel (see
   section 32 below), which replaced it. */

/* ─── 14. MEDIA CONTAINERS ──────────────────────────────────── */
.ph {
  position: relative;
  border-radius: 0;
  overflow: hidden;    /* critical — clips absolutely positioned images */
  background: rgba(0,0,0,0.08);
}
/* About page only: reset the default <figure> UA margin (1em 40px), which was
   silently eating into the grid-2/masonry tracks below and making the actual
   rendered size of every .ph on this page ~80px narrower than its CSS track
   width. Scoped to .page-about so it doesn’t change .ph sizing anywhere else
   on the site. */
.page-about .ph { margin: 0; }
.ph__label {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  color: rgba(0,0,0,0.55); z-index: 1;
  opacity: 0; transition: opacity 0.25s ease;
}
.ph:hover .ph__label { opacity: 1; }
.ph--ratio-16x9 { aspect-ratio:16/9; }
.ph--ratio-4x3  { aspect-ratio:4/3;  }
.ph--ratio-3x4  { aspect-ratio:3/4;  }
.ph--ratio-1x1  { aspect-ratio:1/1;  }
.ph--ratio-3x2  { aspect-ratio:3/2;  }
.ph__img, .ph__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
/* About-page studio photo: slow "walking toward it" zoom-in. .ph's own
   overflow:hidden is the clipping frame for the scaling image, but its
   default background (rgba(0,0,0,0.08), a dark tile placeholder tint)
   would show as a visible dark edge while the image is still mid-scale
   on a slow load/animation -- so .ph--pan overrides that background to
   the page's own section colour (--bg-off/#F7F5F0-equivalent, matching
   .section--light) so the frame reads as invisible, never a visible box. */
.ph--pan { background: var(--bg-off, #F7F5F0); }
.ph__img--pan {
  transform: scale(1);
  transform-origin: center center;
  animation: phPanZoom 14s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes phPanZoom {
  from { transform: scale(1); }
  to   { transform: scale(1.12); }
}
@media (prefers-reduced-motion: reduce) {
  .ph__img--pan { animation: none; }
}
.section--offerings .ph__label { color: var(--text-light); }

/* ─── 15. PORTFOLIO TILES ───────────────────────────────────── */
.projects-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:2px; }
.project--tile {
  display: block; text-decoration: none; color: inherit;
  position: relative; overflow: hidden;
  background: rgba(0,0,0,0.08);
}
.project__img {
  width: 100%; height: auto; display: block;
  border-radius: 0;
  aspect-ratio: 4/3;
  object-fit: cover;
  transition: transform 700ms cubic-bezier(0.25,0,0.1,1);
  will-change: transform;
}
.project--tile:hover .project__img { transform: scale(1.03); }
.project__meta { padding:12px 2px 2px; }
.project__title { font-size:12px; letter-spacing:0.14em; text-transform:uppercase; margin:0 0 6px; }
.project__sub   { font-size:12px; opacity:0.72; }

/* ─── 15b. PORTFOLIO SPOTLIGHT ROWS ─────────────────────────── */
.spotlight { display:flex; flex-direction:column; gap:140px; padding:24px 0 12px; }
.spotlight-row { position:relative; display:grid; grid-template-columns:1fr 1fr; gap:64px; align-items:center; }

.spotlight-row__media {
  order:1; position:relative; overflow:visible;
  background:transparent; opacity:0; display:block; text-decoration:none;
}
.spotlight-row__media img { width:100%; height:100%; object-fit:cover; display:block; }
.spotlight-row__content { order:2; position:relative; }
.spotlight-row--reverse .spotlight-row__media   { order:2; }
.spotlight-row--reverse .spotlight-row__content { order:1; }

/* Leader line: anchored from the side nearest the image, drawn outward toward
   the text by JS animating `width` — `left`/`right` stays fixed so only the
   far edge moves, giving a from-inside-the-image draw-out. */
.spotlight-row__leader {
  position:absolute; top:50%; height:1px; width:2px;
  background:var(--brass); opacity:0;
  left: calc(50% - 72px);
}
.spotlight-row--reverse .spotlight-row__leader { left:auto; right: calc(50% - 72px); }

.spotlight-row__index {
  font-size:11px; letter-spacing:0.2em; color:var(--brass);
  opacity:0; margin:0 0 14px;
}
.spotlight-row__title {
  font-family:"Bodoni Moda",serif; font-size:clamp(26px,3.2vw,40px);
  letter-spacing:0.01em; line-height:1.15; margin:0 0 16px; opacity:0;
}
.spotlight-row__desc {
  font-size:14px; line-height:1.75; max-width:38ch; opacity:0.72;
  margin:0 0 18px;
}
.spotlight-row__link {
  display:inline-block; font-size:11px; letter-spacing:0.14em; text-transform:uppercase;
  color:inherit; text-decoration:none; opacity:0; border-bottom:1px solid currentColor; padding-bottom:2px;
}

@media (prefers-reduced-motion: reduce) {
  .spotlight-row__media, .spotlight-row__index, .spotlight-row__title,
  .spotlight-row__desc, .spotlight-row__link, .spotlight-row__leader {
    opacity:1 !important; transform:none !important; width:auto !important;
  }
}

@media (max-width:768px){
  .spotlight { gap:64px; }
  .spotlight-row { grid-template-columns:1fr; gap:20px; }
  .spotlight-row__media, .spotlight-row--reverse .spotlight-row__media { order:1; }
  .spotlight-row__content, .spotlight-row--reverse .spotlight-row__content { order:2; }
  .spotlight-row__leader { display:none; }
}

/* ─── 16. ARTICLES ──────────────────────────────────────────── */
.article-list { display:grid; grid-template-columns:1fr; gap:22px; }
.article {
  display: grid; grid-template-columns:260px 1fr; gap:18px; align-items:start;
  padding: 14px;
  border-radius: 0;
  background: rgba(255,255,255,0.35);
  border: 1px solid rgba(0,0,0,0.10);
}
.article__meta { padding-right:10px; }

/* ─── 17. PAGE HEROES ───────────────────────────────────────── */
.page-hero { padding-top:24px; padding-bottom:44px; }
.page-hero--dark {
  position: relative;
  min-height: 50vh;
  display: grid; align-items: end;
  padding: calc(var(--nav-h) + 32px) 0 56px;
  background: var(--bg-black); color: var(--text-light);
  overflow: hidden;
}
.page-hero__scrim {
  position: absolute; inset: 0;
  background: radial-gradient(60% 55% at 50% 45%, rgba(0,0,0,0.12) 0%, rgba(0,0,0,0.68) 70%, rgba(0,0,0,0.90) 100%);
  pointer-events: none;
}
.page-hero__fade {
  position: absolute; left:0; right:0; bottom:-1px; height:120px;
  background: linear-gradient(to bottom, rgba(11,12,13,0) 0%, rgba(11,12,13,1) 100%);
  pointer-events: none;
}
.page-hero__content { position:relative; z-index:2; max-width:72ch; }

/* ─── 18. ABOUT HERO (light, matches Figma "About — Full Scroll") ── */
.about-hero-light {
  padding: calc(var(--nav-h) + 56px) 0 40px;
  background: var(--bg-off);
  color: var(--text-dark);
}
.about-hero-light .wrap { position: relative; }
.about-hero-light__top {
  display: grid;
  grid-template-columns: 1fr 1.15fr 0.4fr;
  gap: 32px;
  align-items: start;
}
.about-hero-light__eyebrow {
  font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
  opacity: 0.55; margin: -6px 0 0;
}
.about-hero-light__intro {
  position: absolute; left: 280px; top: 239px; width: 360px;
  margin: 0;
}
.about-hero-light__label {
  writing-mode: vertical-rl;
  font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
  opacity: 0.5;
  position: absolute; left: calc(75.79% - 7px); top: 103px;
}
.about-hero-light__bottom {
  display: grid;
  grid-template-columns: 1fr 1.15fr 0.4fr;
  gap: 32px;
  align-items: end;
  margin-top: 312px;
}
.about-hero-light__portrait {
  width: 100%; max-width: 240px; aspect-ratio: 3/4;
  overflow: hidden; background: #ddd6c8; margin: 0;
}
.about-hero-light__portrait img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.about-hero-light__sketch {
  position: absolute; left: 75.79%; top: 380px; transform: translateX(-50%);
  width: 277px; text-align: center;
}
.about-hero-light__sketch img { width: 100%; max-width: 100%; height: auto; opacity: 0.9; display: block; }
.about-hero-light__counter {
  display: flex; align-items: center; gap: 10px; justify-content: flex-end;
  margin-top: 8px; font-size: 10px; letter-spacing: 0.1em; opacity: 0.5;
}
.about-hero-light__counter span { display: inline-block; width: 28px; height: 1px; background: currentColor; }
.page-about .copy {
  display: grid; grid-template-columns: 1fr 1fr; column-gap: 32px; row-gap: 14px;
}
.page-about .copy > h2 { grid-column: 1 / -1; }
.page-about .copy > p:first-of-type { grid-column: 1; text-align: left; }
.page-about .copy > p:last-of-type { grid-column: 2; text-align: left; margin-top: 173px; }
@media (max-width: 860px) {
  .about-hero-light__top,
  .about-hero-light__bottom { grid-template-columns: 1fr; gap: 20px; }
  .about-hero-light__bottom { margin-top: 20px; }
  .about-hero-light__label {
    writing-mode: horizontal-tb; transform: none;
    position: static; left: auto; top: auto; justify-self: start;
  }
  .about-hero-light__portrait {
    margin: 0 auto;
  }
  .about-hero-light__sketch {
    position: static; left: auto; top: auto; transform: none;
    width: auto; max-width: 160px; justify-self: center; text-align: center;
    margin: 144px auto 0; /* ~5 lines of clearance below the portrait, per Jimi's request */
  }
  .about-hero-light__counter { justify-self: start; text-align: left; justify-content: flex-start; }
  .about-hero-light__intro {
    position: static; left: auto; top: auto; width: auto; max-width: none; margin: 18px 0 0;
  }
  .page-about .copy { grid-template-columns: 1fr; }
  .page-about .copy > p:last-of-type { grid-column: 1; margin-top: 0; }
}

/* ─── 19. CONTACT HERO ZOOM ─────────────────────────────────── */
.page-hero--contact {
  position: relative; min-height:70vh;
  display: grid; align-items:end;
  padding: calc(var(--nav-h) + 32px) 0 56px;
  background: var(--bg-black); color:var(--text-light); overflow:hidden;
}
.contact-hero__bg {
  position: absolute; inset:0;
  background-image: url("../assets/img/contact/contact-hero.webp");
  background-size:cover; background-position:center;
  animation: contactZoom 25s ease-out forwards; will-change:transform;
}
@keyframes contactZoom { from { transform:scale(1); } to { transform:scale(1.12); } }
.page-hero--contact .page-hero__scrim {
  position:absolute; inset:0;
  background: radial-gradient(60% 55% at 50% 45%, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.70) 70%, rgba(0,0,0,0.90) 100%);
}
.page-hero--contact .page-hero__fade {
  position:absolute; left:0; right:0; bottom:-1px; height:120px;
  background: linear-gradient(to bottom, rgba(11,12,13,0) 0%, rgba(11,12,13,1) 100%);
}
.page-hero--contact .page-hero__content { position:relative; z-index:2; }
@media (prefers-reduced-motion: reduce) { .contact-hero__bg { animation:none; } }

/* ─── 20. INTRO ─────────────────────────────────────────────── */
/* .section--intro removed 2026-09-11 -- merged into .section--editorial
   (section 32 below) per Figma 119:583, which has the body copy and the
   "Interior design..." line as part of the SAME asymmetric section as
   the images/sketch/watermark, not a separate centred block above it.
   The old 220px padding-top hack (giving the SCROLL line-draw room to
   reach this copy) is no longer needed -- .editorial-label now sits at
   Figma's own y:600 of a 900px-tall section, already far enough down. */

/* ─── 21. WORLDBAND ─────────────────────────────────────────── */
.worldband { position:relative; overflow:hidden; }
/* Inner must be full-width — without it, the column flex container
   shrinks to fit its widest intrinsic child and the canvas (which
   has no intrinsic size) collapses to zero height. */
.worldband__inner {
  width: 100%;
  display: flex; flex-direction: column; align-items: center;
}
/* 2026-09-16 round 11: reverted -- Jimi wants the map kept at its
   original full size, not shrunk to share a row with the text. The text
   is now an absolutely-positioned overlay ON the map instead (see
   .worldband__text below), placed over open ocean so it doesn't cross
   the dot-matrix landmasses. */
.worldband__text {
  /* 2026-09-16 round 13: moved to the right of the map per Jimi --
     "do not move the map image at all, just move the text to the right,
     past the area that represents Australia, in line with the top of
     the map." A band this tall (title+copy is ~470px) doesn't fit
     anywhere ON the dot-matrix without touching dots -- scanned the
     rendered map and confirmed no clear run reaches past ~420px height
     even in the emptiest column. So instead of overlapping the map,
     this sits just past its own right edge entirely (100.5% - fully off
     the dot-matrix, guaranteed dot-free) using the spare margin between
     the map's max-width and .wrap's own wider max-width, aligned to the
     map's top edge. */
  position: absolute;
  z-index: 3;
  text-align: left;
  left: 100.5%;
  top: 0%;
  width: 11%; /* fallback below 1440px, where the map hasn't reached its capped 1160px max-width yet, so % of the map's own box is still meaningful */
  overflow-wrap: break-word; /* a single word ("Amsterdam,") can still be wider than the box even after the 1440px+ widening below -- break rather than silently overflow past the viewport edge */
}
/* 2026-09-16 round 14: Jimi wants London/Bangkok, Amsterdam/Los Angeles,
   and the "portfolio with a global reach" line each on one line, and the
   body paragraph spread wider (fewer lines) -- without moving the text's
   start position or the map. The old 11% width was a %-of-the-map's-OWN-
   box, but the map is capped at max-width:1160px -- so that 11% topped
   out at ~127px on EVERY screen 1440px or wider, even a 2560px monitor,
   never using the real spare margin next to it. This reclaims that real
   space: (100vw - 1160px)/2 is the actual gap between the map's right
   edge and the browser's right edge once both the map and .wrap are at
   their caps, minus a small safety gutter so text doesn't touch the
   viewport edge. Real available room now scales with the screen instead
   of being permanently capped -- full single-line for every span is
   physically reached around ~1900px-wide browsers (verified: line2
   "Amsterdam, Los Angeles --" needs ~354px at the spec 28px Bodoni Moda,
   and even the single word "Amsterdam," alone needs ~159px, more than
   the ~140px total space available at exactly 1440px -- so under ~1900px
   some wrapping is a hard physical limit here, not a bug, given the map
   is locked at 1160px max-width and the text can't move left onto it). */
@media (min-width: 1440px) {
  .worldband__text {
    width: calc((100vw - 1160px) / 2 - 12px);
  }
}
/* Box matches the SVG's 201:97 viewBox so background-size:100% 100%
   fills perfectly and pin percentages map 1:1 to viewBox coords.
   max-width keeps the map from getting absurdly tall on wide screens. */
.worldband__map {
  position: relative;
  width: 100%;
  max-width: 1160px;
  margin: 0 auto;
  aspect-ratio: 201 / 97;
  background-image: url('../assets/img/world-map.svg');
  background-repeat: no-repeat;
  background-position: center;
  background-size: 100% 100%;
}
.worldband__canvas {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.worldband__pin {
  position: absolute;
  /* Shifted left 2026-09-15 per Jimi's markup -- pin percentages are
     correct real-world lat/lon equirectangular values, but the
     assets/img/world-map.svg background they sit on doesn't share that
     same projection/crop, so pins were landing in open ocean. Until the
     map SVG itself is re-aligned, offsetting every pin left by a fixed
     12% of the map's own width (~139px at the 1160px max-width) is the
     practical fix. */
  left: calc(var(--x) - 5.66%); top: var(--y); /* 2026-09-16 round 14: shifted back right ~12 dot-units per Jimi (the inline SVG dot-matrix is a 201-unit-wide viewBox -- 12/201 = 5.97% of the map's width), toward the text column now sitting to the map's right -- was -11.63% */
  width: 9px; height: 9px;
  background: #8A7860;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  box-shadow: 0 0 0 0 rgba(138,120,96,0.55);
  animation: pinPulse 2.4s ease-out infinite;
  z-index: 2;
  opacity: 0; /* JS-revealed once, see js/homepage-worldmap.js */
}
.worldband__pin--lived { background: var(--text-dark); box-shadow: 0 0 0 0 rgba(11,12,13,0.45); }
.worldband__pin:nth-of-type(2) { animation-delay: 0.6s; }
.worldband__pin:nth-of-type(3) { animation-delay: 1.2s; }
.worldband__pin:nth-of-type(4) { animation-delay: 1.8s; }

@keyframes pinPulse {
  0%   { box-shadow: 0 0 0 0   rgba(138,120,96,0.55); }
  70%  { box-shadow: 0 0 0 14px rgba(138,120,96,0);    }
  100% { box-shadow: 0 0 0 0   rgba(138,120,96,0);    }
}
@media (prefers-reduced-motion: reduce) {
  .worldband__pin { animation: none; box-shadow: 0 0 0 4px rgba(138,120,96,0.18); }
}
/* "Cities quote" -- Figma 119:611 (get_design_context/get_motion_context,
   Plugin API, 2026-09-10): real copy that was missing from the built site
   entirely (not a style tweak -- the text didn't exist here before).
   Exact type spec: line 1/2 Bodoni Moda Medium 28px (#8A7860 then
   #141414), line 3 Source Serif 4 Italic 22px rgba(20,20,20,0.75). Reveal
   is 3 lines fading in strictly one after another (each starts only once
   the previous finishes) -- proportions kept from Figma's own relative
   timing (~700ms/800ms/600ms per line), scaled to a real on-scroll pace.
   The [data-reveal] base rule's own opacity/transform is neutralised
   here (opacity/transform:none, no transition) since this element's OWN
   reveal is fully driven by its child lines instead -- data-reveal is
   kept only so the shared IntersectionObserver still adds .is-revealed
   to trigger them. */
.worldband__quote[data-reveal] { opacity: 1; transform: none; transition: none; }
.worldband__quote {
  text-align: left; margin: 0 0 8px;
}
.worldband__quote-line1,
.worldband__quote-line2,
.worldband__quote-line3 {
  display: block;
  opacity: 0;
  transition: opacity 700ms ease-in-out;
}
.worldband__quote-line1 {
  font-family: "Bodoni Moda", serif; font-weight: 500; font-size: clamp(18px, 2vw, 28px);
  color: var(--brass);
}
.worldband__quote-line2 {
  font-family: "Bodoni Moda", serif; font-weight: 500; font-size: clamp(18px, 2vw, 28px);
  color: var(--ink-deep);
}
.worldband__quote-line3 {
  font-family: "Source Serif 4", serif; font-style: italic; font-weight: 400;
  font-size: clamp(15px, 1.5vw, 22px);
  color: rgba(20,20,20,0.75);
  margin-top: 4px;
}
.worldband__copy {
  text-align: left; max-width: 440px; margin: 16px 0 0;
  font-family: "Source Serif 4", serif; font-weight: 400; font-size: 14px;
  line-height: 1.6; color: #595752;
}
.worldband__quote.is-revealed .worldband__quote-line1 { opacity: 1; transition-delay: 0ms; }
.worldband__quote.is-revealed .worldband__quote-line2 { opacity: 1; transition-delay: 175ms; }
.worldband__quote.is-revealed .worldband__quote-line3 { opacity: 1; transition-delay: 350ms; }
@media (prefers-reduced-motion: reduce) {
  .worldband__quote-line1, .worldband__quote-line2, .worldband__quote-line3 {
    opacity: 1 !important; transition: none !important;
  }
}

/* Full-width + max-width matching .worldband__map (not shrink-wrapped by
   .worldband__inner's align-items:center) so the now-left-aligned legend
   below lines up with the map's own left edge, not the viewport's. */
.worldband__meta { width: 100%; max-width: 1160px; margin: 0 auto; padding:12px 0 24px; }
.worldband__label {
  font-size:11px; letter-spacing:0.22em; text-transform:uppercase;
  opacity:0.45; text-align:center;
}

/* ─── 22. CLIENT LOGOS ──────────────────────────────────────── */
.client-logos {
  display:grid; grid-template-columns:repeat(6,minmax(0,1fr));
  gap:34px; max-width:1100px; margin:0 auto;
  align-items:center; justify-items:center;
}
.client-logo { width:140px; height:56px; display:flex; align-items:center; justify-content:center; }
.client-logo img {
  width:100%; height:100%; object-fit:contain; display:block;
  opacity:0.9; filter:grayscale(1);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.client-logo img:hover { opacity:1; transform:scale(1.03); }

/* ─── 23. CLIENT VIEWER ─────────────────────────────────────── */
.client-viewer { padding-top:54px; }
.client-viewer__crumb { margin:0 0 16px; }
.client-viewer__back { color:inherit; text-decoration:none; border-bottom:1px solid rgba(0,0,0,0.25); }
.client-viewer__back:hover { border-bottom-color:rgba(0,0,0,0.55); }
.client-viewer__grid { display:grid; grid-template-columns:1.6fr 0.9fr; gap:26px; align-items:start; }
.client-gallery__main {
  border-radius:0; overflow:hidden; background:rgba(0,0,0,0.06);
  border:1px solid rgba(0,0,0,0.10); aspect-ratio:16/10; position:relative;
}
.client-gallery__main-img {
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;
  opacity:1; transition:opacity 220ms ease;
}
.client-gallery__main-img.is-swapping { opacity:0.35; }
.client-gallery__thumbs {
  margin-top:14px; display:grid; grid-template-columns:repeat(6,1fr); gap:10px;
}
.client-thumb {
  border:1px solid rgba(0,0,0,0.14); border-radius:0; padding:0;
  overflow:hidden; background:rgba(255,255,255,0.55); cursor:pointer;
}
.client-thumb.is-active { border-color:rgba(0,0,0,0.55); }
.client-thumb__img { width:100%; height:100%; aspect-ratio:1/1; object-fit:cover; display:block; }
.client-info .body { max-width:56ch; }

/* ─── 24. CLIENT GATE ───────────────────────────────────────── */
.client-gate input,
.client-gate button { border-radius: 0 !important; }

/* ─── 25. FOOTER ────────────────────────────────────────────── */
.site-footer {
  background: var(--bg-off);
  border-top: 1px solid rgba(0,0,0,0.08);
  padding: 32px 24px;
  color: var(--text-dark);
}
.footer__inner {
  width: var(--wrap); margin: 0 auto;
  display: flex; justify-content: space-between; align-items: center;
  gap: 24px; flex-wrap: wrap;
}
.footer__email {
  font-size: 12px; letter-spacing: 0.12em;
  color: inherit; text-decoration: none;
  opacity: 0.45; transition: opacity 0.2s ease;
}
.footer__email:hover { opacity: 0.85; }
.footer__copy { font-size:10px; letter-spacing:0.12em; opacity:0.18; margin:0; }
.footer__mark { width:26px; height:26px; opacity:0; order:2; }
.footer__email { order:1; }
.footer__copy  { order:3; }
@media (prefers-reduced-motion: reduce) { .footer__mark { opacity:0.9 !important; } }

/* ─── 26. REVEAL SYSTEM ─────────────────────────────────────── */
[data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 700ms ease, transform 700ms ease;
  transition-delay: var(--d, 0ms);
}
[data-reveal].is-revealed { opacity:1; transform:translateY(0); }

.tile-reveal { opacity:0; transform:translateY(20px); transition: opacity 600ms ease, transform 600ms ease; }
.tile-reveal.is-visible { opacity:1; transform:translateY(0); }
.tile-reveal:nth-child(2) { transition-delay:80ms; }
.tile-reveal:nth-child(3) { transition-delay:160ms; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal], .tile-reveal { opacity:1 !important; transform:none !important; transition:none !important; }
  .watermark-media--a, .watermark-media--b { opacity:1 !important; transform:none !important; animation:none !important; }
  .lockup__h, .lockup__endricks { opacity:1 !important; transform:none !important; }
  .strap-word { opacity:1 !important; transform:none !important; }
}

/* JS-disabled fallback. brand.js drives the staged reveal; if scripting
   is off, show the lockup statically so the hallmark still reads.
   .lockup__h keeps its CSS transform (encodes positioning, not animation)
   — only opacity is forced. */
@media (scripting: none) {
  .lockup__h { opacity: 1; }
  .lockup__endricks { opacity: 1; transform: translateY(0); }
  .strap-word { opacity: 1; transform: none; }
}

/* ─── 27. RESPONSIVE ────────────────────────────────────────── */
@media (max-width: 980px) {
  .nav-desktop { display:none; }
  .burger { display:block; }
  .grid-2 { grid-template-columns:1fr; gap:24px; }
  .feature-row { grid-template-columns:1fr; gap:18px; }
  .masonry-3 { grid-template-columns:1fr; gap:16px; }
  .masonry-3--square {
    display: flex;
    gap: 10px;
    margin-top: 44px;
  }
  .masonry-3--square > .ph:nth-child(1),
  .masonry-3--square > .ph:nth-child(2) { flex: 0 0 92px; width: 92px; height: 92px; aspect-ratio: 1/1; }
  .masonry-3--square > .ph:nth-child(3) { flex: 1 1 auto; width: auto; height: 92px; aspect-ratio: auto; }
  .projects-grid { grid-template-columns:1fr 1fr; gap:2px; }
  .article { grid-template-columns:1fr; gap:14px; }
  .lockup__h { font-size:132px; }
  .lockup__endricks { font-size:70px; }
  .client-viewer__grid { grid-template-columns:1fr; }
  .client-logos { grid-template-columns:repeat(4,minmax(0,1fr)); }
  .footer__inner { flex-direction:column; text-align:left; }
}
@media (max-width: 700px) {
  .hero__video { display:none; }
  .hero__fallback { display:block; }
  .projects-grid { grid-template-columns:1fr; }
  .client-logos { grid-template-columns:repeat(3,minmax(0,1fr)); }
}
@media (max-width: 600px) {
  .client-gallery__thumbs { grid-template-columns:repeat(4,1fr); }
}

/* ─── 28. PORTFOLIO — REAL PROJECT DATA BLOCK ──────────────────
   5-field Location/Client/Accreditations/Project Scale/Date list,
   matching the Figma "Project Data Block" (opacity:0 at rest → part
   of the reveal-once system, see js/portfolio-motion.js). */
.spotlight-row__data {
  margin: 0 0 18px;
  max-width: 320px;
  opacity: 0;
  display: grid;
  gap: 6px;
  font-size: 12px;
  line-height: 1.55;
}
.spotlight-row__data > div { display: flex; gap: 6px; }
.spotlight-row__data dt {
  flex: 0 0 auto;
  font-weight: 500;
  opacity: 0.55;
}
.spotlight-row__data dt::after { content: ":"; }
.spotlight-row__data dd { margin: 0; }

/* ─── 28a. CLIENT PROJECT — Prev/Next/All nav strip ───────────────
   New for the singular Client Project detail page (client-project.html,
   built 2026-09-10 to close the previously-broken client-projects.html
   links). No Figma frame exists for this specific control -- built to
   match the site's existing Inter/label typography and brass accent
   rather than inventing a new visual language; flagged as a judgment
   call, not a confirmed spec. */
.client-project-nav {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  margin-top: 64px; padding-top: 24px;
  border-top: 1px solid rgba(11,12,13,0.1);
}
.client-project-nav__link {
  display: flex; flex-direction: column; gap: 2px;
  text-decoration: none; color: var(--text-dark);
}
.client-project-nav__link--prev { align-items: flex-start; text-align: left; }
.client-project-nav__link--next { align-items: flex-end; text-align: right; }
.client-project-nav__link--all {
  font-family: "Inter", sans-serif; font-size: 11px; font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--brass);
  padding: 8px 16px; border: 1px solid var(--brass); border-radius: 999px;
}
.client-project-nav__dir {
  font-family: "Inter", sans-serif; font-size: 11px; font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ui-muted);
}
.client-project-nav__title {
  font-family: "Bodoni Moda", serif; font-size: 18px; color: var(--text-dark);
}
@media (max-width: 640px) {
  .client-project-nav { flex-wrap: wrap; }
  .client-project-nav__link--all { order: 3; width: 100%; text-align: center; }
}

/* ─── 29. PORTFOLIO GALLERY — hero + click-to-swap thumbnail strip ───
   Same interaction convention as .client-gallery (section 23), reused
   here for the Portfolio spotlight rows. JS-driven only (portfolio-
   gallery.js) — markup for thumbs is injected from portfolio-data.js,
   the .spotlight-row__media wrapper stays a plain <div> (no more <a>)
   so nested thumbnail buttons stay valid, accessible markup. */
.pf-gallery { position: relative; width: 100%; display: flex; flex-direction: column; }
.pf-gallery__main {
  position: relative; width: 100%; aspect-ratio: 3/2; /* matches P06 NAC's native 1400x933 photo, the shared template */
  overflow: hidden; background: rgba(0,0,0,0.08);
}
.pf-gallery__main .pf-gallery__main-img {
  /* Compound selector, not just .pf-gallery__main-img: the older, more
     general ".spotlight-row__media img" rule (section above) has equal
     descendant-selector specificity and was silently winning, forcing
     "cover" on the hero image even though this rule said "contain".
     Fixed 2026-09-17. */
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: contain; display: block;
  opacity: 1; transition: opacity 220ms ease;
}
.pf-gallery__main-img.is-swapping { opacity: 0.25; }

.pf-gallery__thumbs {
  position: static; margin-top: 12px;
  display: flex; gap: 8px;
  padding: 2px 0;
}
.pf-gallery__thumb {
  flex: 1 1 0; min-width: 0; width: auto; height: 56px;
  padding: 0; border: 2px solid transparent; border-radius: 0;
  overflow: hidden; cursor: pointer; opacity: 0.72;
  background: rgba(0,0,0,0.18); transition: opacity 0.2s ease, border-color 0.2s ease;
}
.pf-gallery__thumb:hover { opacity: 1; }
.pf-gallery__thumb.is-active { opacity: 1; border-color: var(--brass); }
.pf-gallery__thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.pf-gallery__thumb:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }

@media (max-width: 768px) {
  .pf-gallery__thumb { height: 38px; }
}
/* Desktop only: thumbnails fill their box (crop to fit) rather than
   letterboxing -- reverted per Jimi's request 2026-09-17. Main hero
   image and mobile thumbnails stay on object-fit: contain (unchanged). */
@media (min-width: 769px) {
  .pf-gallery__thumb img { object-fit: cover; }
}
@media (prefers-reduced-motion: reduce) {
  .pf-gallery__main-img { transition: none; }
}

/* ─── 30. PORTFOLIO ACCESS GATE ─────────────────────────────────
   Blur/scrim overlay locking the spotlight section until a valid
   code is entered. Spec: claude/portfolio-client-gated-access-brief
   -2026-09-07.md. As of 2026-09-17 the real project content is not
   rendered into #portfolioSpotlight at all until the server confirms
   a valid code (see js/portfolio-page.js) -- this is real protection,
   not just a visual cover, unlike the original manual-stopgap phase.

   Because the section's height used to come entirely from the (now
   absent, pre-unlock) rendered content, locking the page left this
   section — and the absolutely-positioned gate box inside it — with
   almost no height, so the "Selected clients" logo strip below showed
   through immediately instead of staying below the fold. Fixed by
   giving the section a full-viewport-below-nav minimum height while
   locked/unlocking, matching how it always looked when the content
   was rendered underneath. */
[data-gate-page].portfolio-locked,
[data-gate-page].portfolio-unlocking {
  min-height: calc(100vh - var(--nav-h));
}
.portfolio-locked #portfolioSpotlight {
  filter: blur(26px) saturate(0.9);
  pointer-events: none;
  user-select: none;
  animation: gateAmbient 14s ease-in-out infinite alternate;
}
@keyframes gateAmbient {
  0%   { transform: scale(1.01) translateY(0); }
  100% { transform: scale(1.04) translateY(-8px); }
}
.portfolio-unlocking #portfolioSpotlight {
  animation: none;
  filter: blur(0);
  transition: filter 1.4s ease;
}
.portfolio-unlocked #portfolioSpotlight { filter: none; pointer-events: auto; }

.portfolio-gate {
  position: absolute; inset: 0; z-index: 5;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  background: rgba(233,230,223,0.35);
  opacity: 1; transition: opacity 1.2s ease;
}
.portfolio-gate__scrim {
  position: absolute; inset: 0;
  background: radial-gradient(60% 55% at 50% 45%, rgba(233,230,223,0.15) 0%, rgba(233,230,223,0.55) 70%, rgba(233,230,223,0.85) 100%);
  pointer-events: none;
}
.portfolio-gate__box {
  position: relative; z-index: 1;
  width: 100%; max-width: 420px;
  background: rgba(255,255,255,0.92);
  border: 1px solid rgba(0,0,0,0.10);
  padding: 34px 30px;
  text-align: center;
}
.portfolio-gate__box .h2 { margin-bottom: 8px; }
.portfolio-gate__box .body-sm { margin-bottom: 18px; opacity: 0.75; }
.portfolio-gate__form { display: grid; gap: 12px; }
.portfolio-gate__form input {
  width: 100%; padding: 12px 14px; border-radius: 0;
  border: 1px solid rgba(0,0,0,0.18); font: inherit; font-size: 14px;
  letter-spacing: 0.08em; background: var(--bg-off); outline: none; text-align: center;
}
.portfolio-gate__form input:focus { border-color: var(--brass); }
.portfolio-gate__form button {
  width: 100%; padding: 12px 14px; border-radius: 0; border: none;
  background: var(--bg-black); color: var(--text-light); font: inherit; font-size: 12px;
  letter-spacing: 0.2em; text-transform: uppercase; cursor: pointer;
  transition: opacity 0.2s ease;
}
.portfolio-gate__form button:hover { opacity: 0.85; }
.portfolio-gate__error {
  color: #a33; font-size: 12px; margin: 2px 0 0; min-height: 16px;
}
.portfolio-gate__request {
  margin: 16px 0 0; font-size: 12px; line-height: 1.6; opacity: 0.75;
}
.portfolio-gate__request a { color: inherit; }
.portfolio-gate__privacy {
  margin: 10px 0 0; font-size: 11px; opacity: 0.5;
}
.portfolio-gate.is-hidden {
  opacity: 0; pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .portfolio-locked #portfolioSpotlight { animation: none; }
}

/* ─── 31. HOMEPAGE HERO — building rotation + stacked wordmark ─────
   New hero treatment per the current Figma Homepage 2 frame: a
   crossfading building-photo rotation behind a small stacked
   "the / HENDRICKS / collective" lockup, with a fixed Timeline Nav
   of building labels down the left edge. This supersedes the old
   full-bleed video hero for the homepage only — the previous script-
   mark lockup classes (.lockup__h / .lockup__endricks) are left
   intact/unused rather than deleted, in case that treatment is still
   wanted elsewhere; flagged for Jimi's review, not a silent removal. */
.hero-building {
  position: absolute; inset: auto;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  /* 838x490 -- exact Tate_02a node size read from Figma (get_design_context,
     2026-09-10), i.e. the hero frame's own default/active building photo.
     Other buildings (One Shanghai 859x502, Dusit 985x475, Twickenham
     1101x620) are close in ratio but not identical -- using Tate's as the
     shared box since it's the default-active photo, not silently averaged. */
  /* 2026-09-16, Jimi: "all of the images need to scale up... at least half
     of their scale again" -- base box enlarged +50% (838px->1257px,
     58vw->87vw) so every building grows uniformly; each building's own
     scale keyframes (Tate 1->1.15 etc.) still apply on top of this. */
  width: min(1048px, 72.5vw); /* 2026-09-16 round 6: scaled back from +50% to midpoint between original (838px/58vw) and +50% (1257px/87vw) per Jimi */
  aspect-ratio: 838 / 490;
  opacity: 0; will-change: opacity;
  background-size: cover; background-position: center;
  background-color: transparent; /* no box behind the photo -- gradient shows through any gap */
}
/* .hero-building.is-active{opacity:1} removed 2026-09-10 -- Tate/One
   Shanghai/Dusit/Twickenham now own their opacity continuously via the
   per-building @keyframes above (copied from Figma's own prototype
   timeline), not JS class-toggling. .is-active is still added by
   homepage-motion.js for the timeline-nav text cascade's own bookkeeping
   but no longer controls image opacity. */
/* Dusit collage variant: 3 stacked transparent PNG layers (Left/Right/Main)
   instead of one background-image. object-fit:contain on all three keeps
   them registered to each other regardless of the container's own aspect
   ratio -- see claude/dusit-01b-checkpoint-2026-09-04.md for the Figma
   source. NOTE: Figma's individual per-layer stagger animation (Main leads,
   Left/Right follow, each its own zoom-settle) is NOT implemented here --
   the Plugin API can't expose that keyframe data, so all 3 layers currently
   just crossfade in together with the rest of the hero rotation. Flagged,
   not silently approximated. */
.hero-building--collage {
  background-image: none;
  /* Dusit/Twickenham visibility is fully delegated to their own child
     __layer--left/--right/--main/--base elements (each with its own
     Figma-sourced opacity keyframe animation, added 2026-09-10) -- the
     wrapper itself must not also sit at the base .hero-building{opacity:0}
     or it hides everything inside it regardless of the children's own
     opacity. Tate/One Shanghai (non-collage) are the opposite: THEY are
     single-layer, so their animation is on the wrapper itself. */
  opacity: 1;
}
.hero-building__layer {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain; object-position: center;
  pointer-events: none;
  /* 2026-09-16: bottom-edge feather baked directly into the image via
     mask-image, replacing the old separate .hero-building__mask overlay
     <div> (removed from index.html/CSS below). That overlay was a
     fixed-geometry rectangle sitting on top of each photo -- fine for
     Tate/One Shanghai (whose wrapper doesn't scale), but for Dusit/
     Twickenham, whose LAYERS scale independently via their own `scale`
     keyframes, the overlay's fixed box didn't track that scaling, so its
     straight edge visibly slid out of alignment with the photo's own
     moving edge ("the square line of the filter against the square line
     of the background image", per Jimi). Masking the image itself means
     the feather scales and moves as one with the photo, always in sync,
     with no separate element that can desync. Also directly softens the
     hard cutout edge at the base of Tate/One Shanghai (blur request,
     2026-09-16) instead of relying on a translucent colour overlay that
     still let the underlying sharp edge show through. */
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
          mask-image: linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
}
/* One Shanghai's photo (4103x2048, ratio 2.00) is proportionally wider than
   the shared hero box (838/490, ratio 1.71) -- object-fit:contain
   letterboxes it top/bottom and centers it, leaving the image's real bottom
   edge sitting in that empty gap, above the gradient mask's actual coverage,
   so the hard cut line showed plainly. Bottom-aligning it puts that edge at
   the box's own bottom edge, inside the mask's fade zone -- same as Tate,
   whose taller-than-box image already fills full height with no gap. */
.hero-building[data-building="one-shanghai"] .hero-building__layer {
  object-position: center bottom;
}

/* ─── Hero building "move in" zoom -- exact Figma prototype data ───
   Source: get_motion_context on 119:564 (Homepage 2 -- Hero), timeline
   cohort rootNodeId 119:563, durationMs 42000, loopMode "loop". Read via
   the Plugin API 2026-09-10 -- copied, not approximated. Tate and One
   Shanghai each grow GENTLY LARGER while on screen (Tate: scale 1 ->
   1.15). One Shanghai starts AT Tate's end size and keeps growing from
   there (scale 1.15 -> 1.25), per Jimi's follow-up request that it "start
   at the position that the Tate building stops and continue to grow from
   that point" -- "move in" per Jimi's 2026-09-10 request. Dusit and
   Twickenham do the opposite: they start oversized and settle to 1.0 as
   they fade in -- a "pan back" / pull-back-into-the-scene feel, also per
   Figma, not invented. Dusit Main (2026-09-10 update) starts at 1.25 --
   exactly One Shanghai's own end scale -- instead of Figma's original 1.2,
   so the two towers visually hand off at the same size ("the single middle
   tower morphs gradually from one image to the other") rather than jumping;
   Dusit Left/Right keep Figma's original 1.2 start since they aren't part
   of that direct handoff. All 4 run independently and
   continuously on the same 42s clock; their opacity keyframes are
   pre-offset by Figma so they hand off from one to the next with no JS
   needed for visibility. */
.hero-building[data-building="tate"] {
  /* Fix 2026-09-10: previously animated the independent `scale` property
     while a separate static `transform:translate(-50%,-50%)` handled
     centering. Mixing independent-property `scale` with a legacy
     `transform` on the same element does NOT scale around the visual
     center as expected -- verified via Playwright the box was actually
     pinned at its RIGHT edge and grew leftward only (exactly the "moves
     to the side" bug reported). Fix: bake translate+scale into ONE
     `transform` value per keyframe stop, in its own keyframes rule kept
     separate from opacity (so each keeps its own Figma easing curve --
     merging them into one rule would force them to share
     animation-timing-function per stop and corrupt both). */
  animation: kf_196_2_opacity_0 42s linear infinite, kf_196_2_transform_0 42s linear infinite;
}
@keyframes kf_196_2_opacity_0 { 0% { opacity: 1; } 17.448% { animation-timing-function: ease-in-out; opacity: 1; } 21.273% { opacity: 0; } 90% { animation-timing-function: cubic-bezier(0.25, 0, 0.58, 1); opacity: 0; } 97% { opacity: 1; } 100% { opacity: 1; } }
@keyframes kf_196_2_transform_0 { 0% { transform: translate(-50%, -50%) scale(1); } 4.676% { animation-timing-function: cubic-bezier(1, 0.33, 0.73, 0.95); transform: translate(-50%, -50%) scale(1); } 20.719% { transform: translate(-50%, -50%) scale(1.15); } 89.9% { transform: translate(-50%, -50%) scale(1.15); } 90% { transform: translate(-50%, -50%) scale(1); } 100% { transform: translate(-50%, -50%) scale(1); } }

.hero-building[data-building="one-shanghai"] {
  /* Same fix as Tate above -- see comment there. */
  animation: kf_228_155_opacity_0 42s linear infinite, kf_228_155_transform_0 42s linear infinite;
}
/* Left/right edge blend added 2026-09-10 per Jimi's feedback ("a bit more
   blending around the left and right sides") -- One Shanghai's tower runs
   close to the box's left/right edges at its larger scale, showing a hard
   vertical cutout edge the bottom mask alone doesn't reach. */
@keyframes kf_228_155_opacity_0 { 0% { opacity: 0; } 20.071% { animation-timing-function: ease-in-out; opacity: 0; } 25.5% { opacity: 1; } 31.19% { animation-timing-function: ease-in-out; opacity: 1; } 38.333% { opacity: 0; } 100% { opacity: 0; } }
@keyframes kf_228_155_transform_0 { 0% { transform: translate(-50%, -50%) scale(1.15); } 20.071% { animation-timing-function: cubic-bezier(0.569, 0.157, 0.73, 0.95); transform: translate(-50%, -50%) scale(1.15); } 34.335% { transform: translate(-50%, -50%) scale(1.25); } 100% { transform: translate(-50%, -50%) scale(1.25); } }

.hero-building__layer--left {
  animation: kf_365_747_opacity_0 42s linear infinite, kf_365_747_scale_0 42s linear infinite;
  /* 2026-09-16, Jimi: "lower the left tower of Dusit as it's not quite in
     sync and there's a gap between it and the main building... only needs
     to come down a couple of pixels". `scale` (set by the keyframe above)
     and `transform` are independent CSS properties and compose without
     conflict, so this static nudge holds regardless of the layer's current
     animated scale value. Bumped 6px -> 10px, same session, per Jimi's
     follow-up ("a little bit more... just down a couple of pixels"). */
  transform: translateY(10px);
}
@keyframes kf_365_747_opacity_0 { 0% { opacity: 0; } 38.333% { animation-timing-function: ease-out; opacity: 0; } 53.048% { animation-timing-function: ease-out; opacity: 1; } 56.328% { opacity: 0; } 100% { opacity: 0; } }
@keyframes kf_365_747_scale_0 { 0% { scale: 1.2 1.2; } 38.333% { animation-timing-function: cubic-bezier(0.676, 0.202, 0.58, 1); scale: 1.2 1.2; } 53.77% { scale: 1 1; } 100% { scale: 1 1; } }

.hero-building__layer--right {
  animation: kf_365_759_opacity_0 42s linear infinite, kf_365_759_scale_0 42s linear infinite;
}
@keyframes kf_365_759_opacity_0 { 0% { opacity: 0; } 38.333% { opacity: 0; } 52.683% { animation-timing-function: ease-out; opacity: 0.96; } 56.429% { opacity: 0; } 100% { opacity: 0; } }
@keyframes kf_365_759_scale_0 { 0% { scale: 1.2 1.2; } 38.333% { animation-timing-function: cubic-bezier(0.762, 0.342, 0.58, 1); scale: 1.2 1.2; } 52.683% { scale: 1 1; } 100% { scale: 1 1; } }

.hero-building--collage[data-building="dusit"] .hero-building__layer--main {
  animation: kf_365_779_opacity_0 42s linear infinite, kf_365_779_scale_0 42s linear infinite;
  /* Bottom edge blend, matching the fix applied to One Shanghai/Tate --
     see .hero-building__layer comment above. Dusit Main's own photo runs
     full-bleed to a hard street-level edge, and needs to read clean as a
     lone tower (matching One Shanghai's silhouette) before Left/Right
     dissolve in beside it. */
  object-position: center bottom;
  /* 2026-09-16, Jimi: "blur the corners of the Dusit Central Park main
     building only, so the two sides left and right" -- Left/Right flanking
     layers intentionally untouched. Combined with the base rule's bottom
     feather in one mask-image (see .hero-building__layer / Tate+One
     Shanghai comments above for why these must be merged, not stacked). */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%),
    linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
          mask-image:
    linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%),
    linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
  mask-composite: intersect;
  -webkit-mask-composite: source-in;
}
@keyframes kf_365_779_opacity_0 { 0% { opacity: 0; } 36.5% { animation-timing-function: ease-in-out; opacity: 0; } 40.5% { opacity: 1; } 51.861% { animation-timing-function: ease-out; opacity: 1; } 56.429% { opacity: 0; } 100% { opacity: 0; } }
@keyframes kf_365_779_scale_0 { 0% { scale: 1.25 1.25; } 36.5% { animation-timing-function: ease-in-out; scale: 1.25 1.25; } 51.861% { scale: 1 1; } 100% { scale: 1 1; } }

.hero-building--collage[data-building="twickenham"] .hero-building__layer--base {
  animation: kf_387_1462_opacity_0 42s linear infinite, kf_387_1462_scale_0 42s linear infinite;
  /* 2026-09-16, Jimi: "the background image of the Twickenham project --
     top blurred and the two sides." Bottom feather from the base rule
     folded in alongside so it isn't lost (see comment on the Dusit-main
     rule above for why mask-image values must be merged, not stacked). */
  /* 2026-09-16 (round 7), Jimi: "blur the edges" further -- widened the
     feather zone on all sides (left/right 10/90 -> 6/94, top 88 -> 80)
     so the edge reads softer/more diffuse. */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, black 6%, black 94%, transparent 100%),
    linear-gradient(to top, black 0%, black 80%, transparent 100%),
    linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
          mask-image:
    linear-gradient(to right, transparent 0%, black 6%, black 94%, transparent 100%),
    linear-gradient(to top, black 0%, black 80%, transparent 100%),
    linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
  mask-composite: intersect;
  -webkit-mask-composite: source-in;
}
@keyframes kf_387_1462_opacity_0 { 0% { opacity: 0; } 58.007% { animation-timing-function: ease-out; opacity: 0; } 64.155% { opacity: 1; } 69.681% { animation-timing-function: ease-out; opacity: 1; } 75.262% { opacity: 0; } 100% { opacity: 0; } }
@keyframes kf_387_1462_scale_0 { 0% { scale: 1.2 1.2; } 58.007% { animation-timing-function: ease-out; scale: 1.2 1.2; } 72.012% { scale: 1.06 1.06; } 100% { scale: 1.06 1.06; } }

.hero-building--collage[data-building="twickenham"] .hero-building__layer--main {
  animation: kf_388_1764_opacity_0 42s linear infinite, kf_388_1764_scale_0 42s linear infinite;
}
@keyframes kf_388_1764_opacity_0 { 0% { opacity: 0; } 54.646% { animation-timing-function: ease-out; opacity: 0; } 58.007% { opacity: 0.9; } 73.163% { animation-timing-function: ease-out; opacity: 0.9; } 77.484% { opacity: 0; } 100% { opacity: 0; } }
@keyframes kf_388_1764_scale_0 { 0% { scale: 1 1; } 58.007% { animation-timing-function: ease-out; scale: 1 1; } 72.012% { scale: 1.1 1.1; } 100% { scale: 1.1 1.1; } }

@media (prefers-reduced-motion: reduce) {
  .hero-building[data-building="tate"],
  .hero-building[data-building="one-shanghai"] {
    animation: none !important;
    transform: translate(-50%, -50%) scale(1) !important;
  }
  .hero-building__layer--left,
  .hero-building__layer--right,
  .hero-building--collage[data-building="dusit"] .hero-building__layer--main,
  .hero-building--collage[data-building="twickenham"] .hero-building__layer--base,
  .hero-building--collage[data-building="twickenham"] .hero-building__layer--main {
    animation: none !important;
    scale: 1 1 !important;
  }
}

/* ─── Hero fog/mist dissolve ────────────────────────────────────────
   2026-09-16, Jimi: bridges the loop seam where Twickenham's statue fades
   out and Tate fades back in ("one dissolving into the mist and one
   appearing from it"). Three still frames (hero_fog_a/b/c.webp) crossfade
   across that gap. Previously planned only as an unwired Figma asset
   ("Image Dissolve Loop", never given timing) -- built here directly on
   the same 42s clock as the rest of the hero rotation, timed against the
   real numbers: Twickenham's statue (kf_388_1764_opacity_0) holds visible
   until 73.163% and is fully gone by 77.484%; Tate (kf_196_2_opacity_0)
   starts rising from 0% and is fully visible by 6.946%. The group and its
   three layers are timed to bridge exactly that window. */
/* 2026-09-16 revision, Jimi: "more organic ... no corners or edges or any
   boundary ... fade completely out to the sides and the top and the
   bottom ... slightly smaller on the page ... only really covering the
   statue and the beginning of the Tate ... more organic movement, more
   of a blurry mist." Reworked from a full-stage rectangular crossfade to
   a smaller, centred, radially-feathered, gently drifting mist: */
.hero-fog {
  position: absolute;
  /* smaller than the full stage, centred on the shared building box --
     covers roughly where the statue/Tate sit, not the whole frame. */
  width: 58%; height: 58%;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 1; /* above the plain building layers, below .hero__stage::after (2) and .hero__caption (3) */
  pointer-events: none;
  animation: heroFogGroupOpacity 42s linear infinite;
}
.hero-fog__layer {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
  opacity: 0;
  /* Organic edge -- a radial fade with no hard rectangular boundary at
     all (previous version used the image's own straight edges). Fully
     transparent well before the corners so nothing square ever shows. */
  -webkit-mask-image: radial-gradient(ellipse 55% 55% at center, black 0%, black 30%, transparent 78%);
          mask-image: radial-gradient(ellipse 55% 55% at center, black 0%, black 30%, transparent 78%);
  /* Real blur, not just an alpha feather -- reads as mist/haze rather
     than a sharp photo dissolving. */
  filter: blur(22px);
  will-change: opacity, transform;
}
.hero-fog__layer--a { animation: heroFogA 42s linear infinite, heroFogDriftA 11s ease-in-out infinite; }
.hero-fog__layer--b { animation: heroFogB 42s linear infinite, heroFogDriftB 13s ease-in-out infinite; }
.hero-fog__layer--c { animation: heroFogC 42s linear infinite, heroFogDriftC 9s ease-in-out infinite; }
/* Slow, continuous scale/position drift -- independent, mismatched
   durations per layer (11s/13s/9s) so the three never move in lockstep,
   reading as organic mist motion rather than a mechanical pulse. */
@keyframes heroFogDriftA {
  0%   { transform: scale(1) translate(0, 0); }
  50%  { transform: scale(1.08) translate(1%, -2%); }
  100% { transform: scale(1) translate(0, 0); }
}
@keyframes heroFogDriftB {
  0%   { transform: scale(1.04) translate(-1%, 1%); }
  50%  { transform: scale(1.12) translate(1%, 2%); }
  100% { transform: scale(1.04) translate(-1%, 1%); }
}
@keyframes heroFogDriftC {
  0%   { transform: scale(1.02) translate(1%, 0); }
  50%  { transform: scale(1.1) translate(-2%, -1%); }
  100% { transform: scale(1.02) translate(1%, 0); }
}
/* Group envelope: invisible for the rest of the loop, visible only across
   the Twickenham -> Tate seam (80% through wrap to 13%). */
@keyframes heroFogGroupOpacity {
  0%   { opacity: 1; }
  13%  { opacity: 0; }
  76%  { opacity: 0; }
  80%  { opacity: 1; }
  100% { opacity: 1; }
}
/* a: first to appear, as Twickenham's statue is finishing its own fade. */
@keyframes heroFogA {
  0%   { opacity: 0; }
  80%  { opacity: 0; }
  84%  { opacity: 0.8; }
  88%  { opacity: 0.8; }
  92%  { opacity: 0; }
  100% { opacity: 0; }
}
/* b: the middle of the dissolve, once Twickenham has fully gone. */
@keyframes heroFogB {
  0%   { opacity: 0.9; }
  4%   { opacity: 0; }
  88%  { opacity: 0; }
  92%  { opacity: 0.9; }
  100% { opacity: 0.9; }
}
/* c: last to appear, timed to overlap directly with Tate's own opacity
   rise (0%-6.946%) -- the actual "appearing from the mist" cross-dissolve. */
@keyframes heroFogC {
  0%   { opacity: 0; }
  2%   { opacity: 0; }
  6%   { opacity: 0.85; }
  9%   { opacity: 0.85; }
  13%  { opacity: 0; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-fog { display: none; }
}
/* Gradient mask over the bottom edge of each hero photo -- Figma "Blend
   Box_01"/"Blend Box_02a" (245:865, 387:1754), read via the Plugin API
   2026-09-10: a linear gradient sitting ABOVE the image, transparent at
   the top fading to the section's own background gradient colour at the
   bottom, so any hard photo edge dissolves seamlessly into the page
   instead of cutting off sharply. .hero--rotation's own background is
   `linear-gradient(180deg,#FBFBFB 0%,#ECECEC 100%)` -- by the height this
   mask covers (bottom ~26% of the photo) that background has already
   reached #ECECEC, so the mask fades to #ECECEC to match exactly, not a
   generic/approximated colour. Sits above the photo layer(s), below the
   lockup/timeline text (z-index 2/3). */
/* .hero-building__mask (the old separate bottom-edge overlay <div>) and
   all its per-building animation bindings were removed 2026-09-16 -- see
   the mask-image feather on .hero-building__layer above, which replaces
   it for every building. History: this started as a single shared overlay
   (Tate/One Shanghai's own wrapper opacity animated it correctly); Dusit's
   copy was found permanently stuck visible (root-caused and first patched,
   then deleted outright as still too heavy-handed); Twickenham's copy was
   then found to visibly desync from its own scaling photo layers ("the
   square line of the filter against the square line of the background
   image"). Rather than patch a third symptom of the same design (a fixed-
   geometry overlay that can't track an independently-scaling photo), the
   whole overlay system was retired in favour of feathering baked directly
   into each image, which is guaranteed to move and scale with it. */
/* Stage-level fade, added 2026-09-10 -- per-building masks above are tied
   to each building's own scaling/moving wrapper, which is why two prior
   attempts to "reach further down" still undershot (the mask scaled and
   moved WITH the photo instead of staying anchored to the page). This one
   is a direct child of .hero__stage, which does not scale or animate, so
   its position is fixed regardless of which building/scale is active.
   Starts below the photo's visible extent and overshoots the stage's own
   bottom edge -- reaching down toward/behind .hero__caption -- ending in
   the exact flat colour .section--intro uses (#ECECEC), so there is no
   seam anywhere it fully resolves, "the full length of the page" as
   Jimi described it. */
.hero__stage::after {
  content: "";
  position: absolute;
  /* 2026-09-15: switched from left:0/right:0 (bound to .hero__stage's own
     box) to a decoupled 100vw + centering transform, per Jimi's repeated
     "extend right across the full width of the page" -- .hero__stage was
     already unconstrained (no ancestor sets a max-width), but this makes
     the full-bleed explicit and immune to any future wrapper. */
  left: 50%; right: auto; width: 100vw;
  transform: translateX(-50%);
  /* top/bottom set precisely by js/hero-scroll-line.js once real layout
     is known -- top lands exactly at the building photo's own resting
     bottom edge ("just touch the bottom of the images", 2026-09-10), and
     bottom reaches exactly to just above "Iconic experiences..." in
     .hero__caption ("full length of the page" -- previous static
     percentage/px guesses kept undershooting or overlapping the photo).
     These are conservative fallbacks if JS doesn't run. */
  /* CSS custom properties (not inline style) because this is a ::after --
     pseudo-elements aren't real DOM nodes, so JS can't set .style on them
     directly, but custom properties set on the real .hero__stage element
     inherit straight through. */
  top: var(--stage-fade-top, 92%);
  bottom: var(--stage-fade-bottom, -220px);
  background: linear-gradient(to bottom, rgba(236, 236, 236, 0) 0%, rgba(236, 236, 236, 0.6) 20%, #ECECEC 45%, #ECECEC 100%);
  pointer-events: none;
  z-index: 2;
}
/* Left/right fade for Tate + One Shanghai, added 2026-09-10 per Jimi's
   request ("gradient the left and right hand sides... not too close to
   the building -- just the white backgrounds"). This uses mask-image on
   the photo itself (erasing real pixels/alpha) rather than an overlay
   colour, so there's no tone-matching problem -- the true page background
   shows through directly. The 12%/88% stops keep the fade in the pale
   sky/background margin at each photo's outer edge, well clear of the
   building silhouette which sits centered in frame. */
.hero-building[data-building="tate"] .hero-building__layer,
.hero-building[data-building="one-shanghai"] .hero-building__layer {
  /* 2026-09-16: combined with the base rule's bottom feather (both fades
     must live in ONE mask-image value -- a second mask-image declaration
     on this more specific selector would otherwise replace, not add to,
     the base rule's bottom fade). mask-composite:intersect multiplies the
     two gradients' alpha together, so both fades apply simultaneously. */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%),
    linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
          mask-image:
    linear-gradient(to right, transparent 0%, black 12%, black 88%, transparent 100%),
    linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
  mask-composite: intersect;
  -webkit-mask-composite: source-in;
}
.hero-lockup { position: relative; z-index: 2; text-align: center; }
/* Exact spec pulled from Figma node 119:566 "Centered content" via the
   Plugin API (get_design_context/use_figma, 2026-09-10) -- NOT approximated.
   "the"/"collective": Source Serif 4 Italic 400, 20px, #595752 (--text-secondary).
   "HENDRICKS": Bodoni Moda Medium 500, 34px PER LETTER, 0 letter-spacing,
   #8A7860 (--brass) -- a compact 269px-wide wordmark centered in a 420px
   box on Figma's 1440px canvas, NOT a full-bleed/edge-cropping treatment.
   34/1440 = 2.36vw is the scale-accurate clamp basis below. */
.hero-lockup__the,
.hero-lockup__collective {
  display: block; font-family: "Source Serif 4", serif; font-weight: 400; font-style: italic;
  font-size: clamp(14px, 1.4vw, 20px); letter-spacing: 0; opacity: 0; color: var(--text-secondary);
}
.hero-lockup__name {
  /* 2026-09-10: reverted to Figma's compact spec -- an earlier pass in
     this session enlarged this element (the small "the HENDRICKS
     collective" lockup line) thinking it was the "HENDRICKS" text Jimi
     wanted enlarged/cropped at the edges. Jimi clarified that request was
     actually about the separate, already-large grey watermark text in
     .section--watermark (.watermark-text, sized there instead) -- this
     lockup line stays at its original compact Figma-spec size. */
  display: block; font-family: "Bodoni Moda", serif;
  font-size: clamp(20px, 2.36vw, 34px);
  letter-spacing: 0; color: var(--brass); opacity: 0;
  margin: 8px 0 0;
  text-align: center; white-space: nowrap;
  /* Requested 2026-09-10, widened then pulled back same day (follow-up
     request: "spacing is a little too wide now so back in a little bit"):
     slow, smooth horizontal growth -- the wordmark starts at 0
     letter-spacing (compact ~269px per Figma 119:568) and gently spreads
     outward from its own center in both directions as it settles, ending
     at 1.5em (down from an earlier 2em pass). text-align:center on this
     block means growing letter-spacing naturally expands symmetrically
     around the middle -- EXCEPT letter-spacing also adds a trailing gap
     after the last letter, which is included in the centered line box's
     width, so the visible glyphs alone sit slightly left of true center.
     margin-right is transitioned in lockstep with letter-spacing (same
     duration/easing/delay) to cancel exactly that trailing gap, so
     "HENDRICKS" recenters on "the"/"collective" above/below it (R in the
     middle, matching Figma 119:566 where all three lines share the same
     210px center in the 420px lockup box) throughout the animation, not
     just at rest. Triggered once by the .is-revealed class the generic
     [data-reveal] system adds on entrance -- plays once, holds at its
     widest, never loops or reverses. Kept as its own transition (not the
     generic one) so its slow duration/easing don't affect the
     opacity/transform fade. */
  margin-right: 0;
  transition: opacity 700ms ease, transform 700ms ease,
    letter-spacing 2800ms cubic-bezier(0.16, 1, 0.3, 1) 300ms,
    margin-right 2800ms cubic-bezier(0.16, 1, 0.3, 1) 300ms;
}
.hero-lockup__name.is-revealed { letter-spacing: 1.5em; margin-right: -1.5em; }
@media (prefers-reduced-motion: reduce) {
  .hero-lockup__name { transition: none !important; letter-spacing: 1.5em !important; margin-right: -1.5em !important; }
}

/* Rebuilt 2026-09-10 per Figma "Timeline Nav" (119:677) + "Timeline
   labels -- Tate/OneShanghai/Dusit/Twickenham" (411:2/8/14/20), read via
   the Plugin API -- NOT approximated. Exact findings:
   - 8 tick marks total (not 4), each 12x1px brass rule.
   - Each tick pairs 1:1 with ONE text row (name, architect, company/role,
     year(s), worktype line 1, worktype line 2, lat, long) -- 8 rows, not 6.
     Previous build had 4 clickable building-select ticks + 6 text lines,
     which is where the "only 4 when there should be 8" / misalignment
     came from -- two different Figma concepts had been merged into one.
   - Behaviour (per Jimi, matches the Articles leader-line language used
     elsewhere on the site): each tick grows left-to-right, top to bottom,
     in turn; once a tick reaches full width its row's text appears beside
     it; then the next row down begins. Rows stay visible for the rest of
     that building's on-screen duration. When the building photo changes,
     all 8 rows reset (ticks back to 0 width, text hidden) and the cascade
     restarts with the new building's 8 lines. Driven by
     js/homepage-motion.js's per-building segment scheduling. */
.hero-timeline {
  position: absolute; left: 24px; top: 50%; transform: translateY(-50%);
  z-index: 3;
}
.hero-timeline-block {
  position: absolute; inset: 0 auto auto 0;
  display: flex; flex-direction: column; gap: 10px;
  visibility: hidden; pointer-events: none;
}
.hero-timeline-block.is-active { visibility: visible; pointer-events: auto; }
.hero-timeline-row {
  display: flex; align-items: center; gap: 10px;
  height: 13px;
}
.hero-timeline-row__tick {
  flex: 0 0 auto;
  width: 0; height: 1px; background: var(--brass);
  transition: width 350ms ease-in-out; /* was ease-out -- eases in AND out, no jump at the start */
}
.hero-timeline-row__text {
  flex: 0 0 auto;
  font-family: "Inter", sans-serif; font-weight: 400;
  font-size: 11px; letter-spacing: 1.5px;
  color: var(--text-secondary); opacity: 0;
  white-space: nowrap;
  transition: opacity 400ms ease-in-out; /* was ease-out */
}

@media (max-width: 980px) {
  .hero-timeline { display: none; }
}

/* ─── 32. HOMEPAGE EDITORIAL/TEAM + OFFERINGS CAROUSEL ──────────────
   Rebuilt 2026-09-11 per Jimi's explicit instruction to match Figma
   119:563 exactly, pulled via get_design_context (NOT eyeballed) on the
   real section frames:
   - 119:583 "Editorial/Team section" -- was two separate site sections
     (.section--intro's centred heading+body, then this section's
     images+sketch+watermark); Figma has it as ONE asymmetric section
     (body copy upper-left, two overlapping photos upper-right, the
     "Interior design..." line as a small 2-line label lower-right under
     the photos, sketch lower-left, giant watermark behind all of it) --
     merged into .section--editorial below, all positions read directly
     off 119:586/587/589/591/592/139:4 (percent-of-1440x900, matching
     Figma's own fixed-ratio frame).
   - 119:593 "Offerings Carousel (real content: Retail/Hospitality/
     Residential)" -- was a static 3-column prose grid with no Figma
     source at all; Figma is actually a one-image-at-a-time carousel
     (image + italic "<discipline> interior" caption + a fixed "ONE OF
     THREE CORE DISCIPLINES..." label + NEXT/PREV) cycling the same 3
     real assets (assets/img/offerings/offering-{retail,hospitality,
     residential}-interior.webp) already on disk -- captions follow that
     same on-disk "<discipline> interior" naming, not invented. Rebuilt
     as .section--offerings below; js/offerings-carousel.js drives the
     swap.
   Both sections also each carry their own giant watermark -- Figma has
   TWO "HENDRICKS" nodes (119:584 behind Editorial/Team, 119:605 behind
   Offerings), both identical 480px Bodoni Moda Medium / rgba(26,26,26,
   0.06) spec (see .watermark-text, section 31 above); kept centred per
   Jimi's standing instruction rather than each node's own off-centre
   offset. */
.section--editorial,
.section--offerings {
  position: relative; overflow: hidden;
  min-height: min(900px, 62.5vw); /* Figma's fixed 1440x900 frame ratio */
}
.section--editorial { background: linear-gradient(180deg, #ECECEC 0%, #F6F6F6 100%); }
.section--offerings { background: #F6F6F6; }
/* Thin content-width hairline at the top of each section -- Figma
   119:585/119:594 "Rectangle", identical on both: 1px,
   rgba(20,20,20,0.15), matching the page's real content margins (the
   --wrap var), not full-bleed. */
.section-rule {
  position: absolute; left: 50%; top: 80px; z-index: 1;
  width: var(--wrap); height: 1px;
  background: rgba(20,20,20,0.15);
  transform: translateX(-50%);
  pointer-events: none;
}
/* -- Editorial/Team -- */
.editorial-copy {
  /* 2026-09-15: moved to sit level with the TOP of .watermark-media--a
     (top:15.56%, same as that image) and to its left, clearing it --
     not overlapping. Was left:6%/top:50% -- moved right +29% (~418px on
     the 1440px frame) and up -34.44% (~310px). */
  /* 2026-09-16 round 9: narrowed (28% -> 15%) and shifted right (35% -> 48%) per Jimi -- wraps into more lines for a squarer block, and sits closer to --a, mirroring the gap between --b and the label. */
  position: absolute; left: 48%; top: 15.56%; width: 15%; z-index: 1;
  font-family: "Source Serif 4", serif; font-weight: 400; font-size: 14px;
  line-height: 1.6; color: #595752; margin: 0;
}
.watermark-media {
  position: absolute; overflow: hidden; background: rgba(0,0,0,0.06);
  box-shadow: 0 30px 60px -20px rgba(0,0,0,0.25); border-radius: 14px; z-index: 1; /* 2026-09-16 round 9: 5px -> 14px, the old radius was too subtle on the smaller square box to read as rounded (looked cropped) -- per Jimi */
}
.watermark-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Exact positions off Figma 119:587 (top) / 119:589 (overlap), percent
   of the 1440x900 section frame. */
.watermark-media--a { left: 66.67%; top: 15.56%; width: 29.17%; aspect-ratio: 420/260; }
/* 2026-09-15: moved down significantly to clear --a -- was top:58%,
   now top:72% (+14%, ~126px on the 1440x900 frame). */
/* 2026-09-15 (round 3): left recalculated so --b's RIGHT edge lands
   exactly under --a's LEFT edge (66.67%) -- 66.67% - 16.67% (own width)
   = 50%. Was left:60% (right edge at 76.67%, well right of --a's left
   edge) -- moved left -10pp, -144px on the 1440x900 frame. top and
   width unchanged, per "keep the horizontal level [as] is though". */
.watermark-media--b { left: 50%; top: 69.63%; width: 16.67%; aspect-ratio: 1/1; z-index: 0; } /* 2026-09-16 round 9: halved the gap to --a per Jimi (was 72%, gap 27.55pp -> now 58.23%, gap 13.78pp). 2026-09-16 round 10: nudged back down +3pp (58.23% -> 61.23%) -- read as clashing/too close to --a when checked live in Safari. 2026-09-16 round 11: nudged down another ~20mm per Jimi (61.23% -> 69.63%, +8.4pp @96dpi on the 900px-tall reference frame). */
/* Reveal choreography TEMPORARILY REMOVED -- 2026-09-16 (round 8), per
   Jimi: neither the round-6 nor round-7 off-screen entrance was actually
   showing the two images on the page, so at his request this now just
   pins both to their plain, static final position (left/top set above,
   untouched) with no animation, transform, or dependency on the
   .is-revealed class/JS at all -- a known-good baseline to confirm
   they're visible before reverse-engineering the entrance animation back
   in. The old editorialSlideInLeft keyframes + reveal rules are kept
   below, commented out, as the starting point for that follow-up rather
   than deleted outright.
.watermark-media--a,
.watermark-media--b {
  opacity: 0;
  transform: translateX(calc(-100vw - 100%)) scaleX(1);
}
.watermark-media--a.is-revealed {
  animation: editorialSlideInLeft 1.05s cubic-bezier(0.22, 0.61, 0.25, 1) 0.13s both;
}
.watermark-media--b.is-revealed {
  animation: editorialSlideInLeft 1.05s cubic-bezier(0.22, 0.61, 0.25, 1) 1.18s both;
}
@keyframes editorialSlideInLeft {
  0%   { opacity: 0; transform: translateX(calc(-100vw - 100%)) scaleX(1); }
  55%  { opacity: 1; }
  78%  { transform: translateX(3%) scaleX(0.93); }
  90%  { transform: translateX(-1%) scaleX(1.03); }
  100% { opacity: 1; transform: translateX(0) scaleX(1); }
}
*/
/* 2026-09-16 round 11, per Jimi: round 10's exit direction was
   confirmed correct, slowed down a lot (3s -> 9s, same ease-in-out --
   "smooth not fast"), then REVERSED into the entrance: images now start
   off-screen right and slide IN to their landed position, top (--a)
   first, bottom (--b) starting 2s after --a begins (was the opposite
   order/direction for the exit test). Once both have landed, the text
   (.editorial-copy/.editorial-label) and .watermark-sketch dissolve in
   -- see editorialDissolveIn below. The round-10 exit is kept as a
   comment for reference, not deleted, since it's the direction this was
   reversed from.
.watermark-media--a,
.watermark-media--b {
  opacity: 1;
}
.watermark-media--b {
  animation: editorialExitRight 3s ease-in-out 1s both;
}
.watermark-media--a {
  animation: editorialExitRight 3s ease-in-out 3s both;
}
@keyframes editorialExitRight {
  0%   { transform: translateX(0); }
  100% { transform: translateX(calc(100vw + 100%)); }
}
*/
.watermark-media--a,
.watermark-media--b {
  opacity: 1;
}
.watermark-media--a {
  animation: editorialEnterFromRight 9s ease-in-out 1s both;
}
.watermark-media--b {
  animation: editorialEnterFromRight 9s ease-in-out 3s both; /* starts 2s after --a's own animation begins (1s hold + 2s) */
}
@keyframes editorialEnterFromRight {
  0%   { transform: translateX(calc(100vw + 100%)); }
  100% { transform: translateX(0); }
}
/* --a starts at 1s, 9s duration -> lands at 10s. --b starts at 3s, 9s
   duration -> lands at 12s. Text/sketch dissolve starts just after, at
   12.3s, once both images are confirmed in place. */
.editorial-copy[data-reveal],
.editorial-label[data-reveal] {
  opacity: 0; transform: none; transition: none; /* neutralise the generic [data-reveal] rule -- this dissolve is driven by the keyframes below instead, timed to the images landing */
}
.editorial-copy,
.editorial-label {
  animation: editorialDissolveIn 1.8s ease-in-out 12.3s both;
}
.watermark-sketch {
  animation: editorialSketchDissolveIn 1.8s ease-in-out 12.3s both;
}
@keyframes editorialDissolveIn {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes editorialSketchDissolveIn {
  0%   { opacity: 0; }
  100% { opacity: 0.9; } /* .watermark-sketch's normal resting opacity */
}
/* Figma 139:4 "Invisible House - Section Sketch 1" -- bottom-left,
   NOT bottom-right; a horizontal-flip transform on the Figma layer meant
   its raw metadata x (674) disagreed with the actual rendered position
   (89) by exactly the layer's own width -- confirmed against the real
   Figma screenshot, not the metadata alone. */
.watermark-sketch {
  /* 2026-09-15 (round 3): moved further right into the frame and halved
     in width per Jimi's request ("move into the frame more to the right
     but half it's width") -- left 2% -> 10% (+8pp, +115.2px on the
     1440x900 reference frame), width 17% -> 8.5% (-8.5pp, -122.4px). */
  /* 2026-09-16 (round 6): scaled up by half its size again per Jimi --
     8.5% -> 12.75% (+50%, +61.2px on the 1440x900 reference frame). */
  /* 2026-09-16 (round 7): scaled up a little more per Jimi -- 12.75% ->
     14.7% (~+15%, +28.3px on the 1440x900 reference frame). */
  position: absolute; left: 10%; top: 76%; width: 16.9%; z-index: 1; /* 2026-09-16 round 9: scaled up again, same ~+15% as round 7 */
  opacity: 0.9;
}
.watermark-sketch img { width: 100%; display: block; }
/* Figma 119:591/592 -- two-line label, small (20px), NOT the big
   centred .h2 the old .section--intro used. Keeps data-reveal-line-sync
   so js/hero-scroll-line.js's SCROLL-line draw still syncs its finish to
   this heading's reveal, now wherever it lands post-merge (see that
   file -- selector is attribute-only, not tied to a specific class). */
.editorial-label {
  /* 2026-09-15: moved south + right to follow .watermark-media--b's new
     lower position and clear its right edge -- was left:66.67%/top:66.67%,
     now left:80% (+13.33%, ~192px) / top:78% (+11.33%, ~102px) on the
     1440x900 frame. */
  /* 2026-09-16 round 9: moved to sit close on --b's right, near the page edge, and up to align with --b's new (higher) position -- was left:80%/top:78%. */
  /* 2026-09-16 round 13: moved straight down (left unchanged) to align
     with the TOP of .watermark-media--b, which is now at top:69.63% --
     was top:60%. */
  position: absolute; left: 70%; top: 69.63%; z-index: 1;
  font-family: "Bodoni Moda", serif; font-weight: 500; font-size: 20px;
  line-height: 1.4; color: var(--brass); margin: 0;
}
.editorial-label span { display: block; }
/* -- Offerings Carousel -- */
.offerings-media {
  position: absolute; left: 50%; top: calc(50% - 120px); z-index: 1;
  transform: translate(-50%, -50%);
  width: 520px; max-width: 70%; aspect-ratio: 520/340;
  border-radius: 5px; overflow: hidden;
  box-shadow: 0 30px 60px -20px rgba(0,0,0,0.25);
}
.offerings-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: opacity 200ms ease; }
.offerings-media.is-swapping img { opacity: 0.4; }
.offerings-caption {
  /* 2026-09-15: back to ONE line ("RETAIL INTERIOR" etc, uppercase) per
     Jimi's markup -- was flex-column (2 stacked lines), now flex-row.
     Also pushed further from the image above: top 68%->76% (+8%, ~72px
     on the 1440x900 frame). */
  /* 2026-09-16 round 13: moved up (left unchanged) so .offerings-label's
     second line lands level with the bottom of .offerings-media -- was
     top:76%, now 65.67% (-10.33pp, -93px on the 1440x900 frame, measured
     against the rendered page, not guessed). */
  position: absolute; left: 33.33%; top: 65.67%; z-index: 1;
  display: flex; flex-direction: row; gap: 6px; margin: 0; white-space: nowrap;
  font-family: "Source Serif 4", serif; font-style: italic; font-weight: 400; font-size: 22px;
  text-transform: uppercase;
}
.offerings-caption__discipline { color: var(--brass); }
.offerings-caption__fixed { color: #141414; }
.offerings-label {
  /* 2026-09-15: now 3 lines total (this row's own 2 spans stacked below
     the one-line caption above) -- moved down with it, top 74%->82%
     (+8%, ~72px). */
  /* 2026-09-16 round 13: moved up with .offerings-caption, same -10.33pp,
     so its own second line ("-- RETAIL, HOSPITALITY, RESIDENTIAL.")
     lands level with the bottom of .offerings-media -- was top:82%. */
  position: absolute; left: 33.33%; top: 71.67%; z-index: 1;
  margin: 0; white-space: normal; max-width: 34%;
  font-family: "Inter", sans-serif; font-weight: 500; font-size: 11px;
  letter-spacing: 0.5px; color: rgba(89,87,82,0.7);
}
.offerings-label span { display: block; margin-top: 2px; }
.offerings-nav {
  position: absolute; left: 60.9%; top: 59.1%; z-index: 1;
  display: flex; align-items: center; gap: 10px;
  font-family: "Inter", sans-serif; font-weight: 500; font-size: 10px;
}
.offerings-nav button {
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer; font: inherit;
  letter-spacing: 1px; text-transform: uppercase;
}
.offerings-nav__next { color: var(--brass); }
.offerings-nav__prev { color: rgba(89,87,82,0.6); }
.offerings-nav__sep { color: rgba(89,87,82,0.4); }
@media (max-width: 980px) {
  .section--editorial, .section--offerings { min-height: 0; padding: 64px 0 40px; }
  .section-rule { position: relative; left: auto; top: auto; transform: none; margin: 0 auto 28px; }
  .editorial-copy, .watermark-media--a, .watermark-media--b, .watermark-sketch, .editorial-label,
  .offerings-media, .offerings-caption, .offerings-label, .offerings-nav {
    position: relative; left: auto; top: auto; right: auto; bottom: auto; transform: none;
    width: 90%; max-width: 420px; aspect-ratio: auto; height: auto;
    margin: 0 auto 16px; white-space: normal;
  }
  .offerings-media { aspect-ratio: 520/340; }
  /* 2026-09-16 round 16: mobile-only per Jimi -- desktop (.watermark-sketch
     above/outside this media query) is untouched. Sketch scaled down from
     the general 90%/420px block above since it read oversized stacked on
     phone widths. */
  .watermark-sketch { width: 65%; max-width: 280px; }
  /* .watermark-media--b ("the lights" -- offering-hospitality-2-interior.jpg,
     the lower of the two watermark images, sitting just above the sketch)
     inherits aspect-ratio:auto from the general rule above, which lets its
     own (non-square) source image dictate the box's height -- restoring
     aspect-ratio:1/1 here (matching its desktop rule) makes it square again.
     2026-09-16 round 17: Jimi asked to shrink it further to a third of the
     round-16 mobile size (85% -> ~28%, 360px -> 120px), staying centred
     (margin:0 auto, unchanged) -- same position, just smaller. Its
     margin-bottom is also bumped from the shared 16px to ~2 lines (56px)
     of clear space before .editorial-label ("Interior design for retail,
     hospitality and residential") starts, per Jimi's request; DOM order
     already has this image directly above that label, so this margin is
     the actual gap between them. */
  /* 2026-09-16 round 18: scaled up +50% again per Jimi (28% -> 42%,
     120px -> 180px) -- still centred (margin unchanged), same position,
     just larger. */
  .watermark-media--b { width: 42%; max-width: 180px; aspect-ratio: 1/1; margin: 0 auto 56px; }
  /* 2026-09-16 round 17: ~4 lines (112px) of clear space between
     .editorial-label and .watermark-sketch, which follows it directly in
     the DOM -- per Jimi, lengthening the mobile page as needed. */
  /* 2026-09-16 round 18: centred per Jimi -- desktop's .editorial-label
     rule sets no text-align (defaults to left), so this centres both the
     block (already centred via margin:0 auto) AND the text inside it,
     mobile only. */
  .editorial-label { margin: 0 auto 112px; text-align: center; }
}

/* ─── 33. WORLD PRESENCE — inline SVG dot reveal ────────────────────
   world-map.svg is fetched and inlined (not used as a CSS background)
   so JS can stagger-reveal each real <circle> by distance from the
   nearest city pin, per website-draft-motion-spec.md section 1. */
/* 2026-09-16 round 11: the text-over-map overlay only works at desktop
   scale, where there's an ocean patch big enough to hold it clear of the
   dots. Below 720px the overlay collides with both the dot-matrix and
   the legend, so it drops back to a plain full-width block under the
   map instead. */
@media (max-width: 720px) {
  /* position:static would overflow .worldband__map's fixed aspect-ratio
     box (SVG needs that fixed height); keep it absolute, anchored to
     the map's own bottom edge, full width. */
  .worldband__inner { padding-bottom: 260px; }
  .worldband__text {
    position: absolute;
    left: 0;
    top: 100%;
    width: 100%;
    margin-top: 24px;
  }
}
.worldband__map--dots svg { display: block; width: 100%; height: 100%; }
.worldband__map--dots circle { opacity: 0; transition: opacity 1s ease-out; }
.worldband__pin--worked { background: var(--brass); }
.worldband__pin--lived { background: var(--text-dark); }
/* Bottom-left, stacked (2026-09-11, per Figma 119:606 "World Presence" --
   get_design_context read directly, not eyeballed off a screenshot: the
   legend sits left-aligned as two stacked lines, not a centred row. */
.worldband__legend {
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin-top: 10px;
  font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; opacity: 0.6;
}
.worldband__legend span { display: inline-flex; align-items: center; gap: 6px; }
.worldband__legend i { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.worldband__legend i.lived { background: var(--text-dark); }
.worldband__legend i.worked { background: var(--brass); }
@media (prefers-reduced-motion: reduce) {
  .worldband__map--dots circle { transition: none; opacity: 0.6 !important; }
}
/* ─── 34. HOMEPAGE PORTFOLIO PREVIEW — filmstrip ────────────────────
   Reuses real Portfolio project images (js/portfolio-data.js) — no
   separate/duplicated asset set. */
.home-portfolio-preview__label {
  font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
  opacity: 0.55; margin-bottom: 22px;
}
/* 2026-09-16 round 13: converted from a click-to-scroll filmstrip
   (Prev/Next buttons, scroll-snap) to a continuously auto-scrolling loop
   per Jimi. #homePreviewTrack is now just the clipping viewport;
   .home-portfolio-preview__track-inner holds the (doubled, see index.html)
   item set and carries the marquee animation -- translateX 0 -> -50%
   lands exactly on the start of the duplicate set, so the loop is
   seamless. Pauses on hover/focus so a visitor can actually read a
   caption or click through; also respects prefers-reduced-motion. */
.home-portfolio-preview__track {
  overflow: hidden;
  padding-bottom: 8px;
}
.home-portfolio-preview__track-inner {
  display: flex; gap: 16px; width: max-content;
  animation: homePreviewMarquee 32s linear infinite;
}
.home-portfolio-preview__track:hover .home-portfolio-preview__track-inner,
.home-portfolio-preview__track:focus-within .home-portfolio-preview__track-inner {
  animation-play-state: paused;
}
@keyframes homePreviewMarquee {
  0%   { transform: translateX(0); }
  100% { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .home-portfolio-preview__track-inner { animation: none; }
}
.home-portfolio-preview__item {
  flex: 0 0 auto; width: 260px; aspect-ratio: 4/3.2; overflow: hidden;
  background: rgba(0,0,0,0.06);
  text-decoration: none; display: block; position: relative;
}
.home-portfolio-preview__item img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 700ms cubic-bezier(0.25,0,0.1,1);
}
.home-portfolio-preview__item:hover img { transform: scale(1.04); }
.home-portfolio-preview__item span {
  position: absolute; left: 12px; bottom: 10px; color: var(--text-light);
  font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  text-shadow: 0 1px 6px rgba(0,0,0,0.6);
}

/* Lived-and-worked pins pulse in the dark ink tone, not brass */
.worldband__pin--lived { animation-name: pinPulseDark; }
@keyframes pinPulseDark {
  0%   { box-shadow: 0 0 0 0   rgba(11,12,13,0.45); }
  70%  { box-shadow: 0 0 0 14px rgba(11,12,13,0);    }
  100% { box-shadow: 0 0 0 0   rgba(11,12,13,0);    }
}

/* ─── 35. ARTICLES — paired magazine-cover cards ────────────────────
   One shared fanned-cover media block flanked by two text columns (one
   per publication) with a leader line from each side — matches the
   current Figma "ARTICLES" canvas layout. Same reveal-once motion
   language as the Portfolio spotlight rows (leader draws, then text
   cascades in). Per that canvas, additional pairs (Autogrill, Infiniti/
   FX, Nakhon Pathom, Dusit) are NOT included -- their citations are
   still unconfirmed per the project's own outstanding-items list, so
   only the two already-sourced, already-linked pairs are built. */
.article-pairs { display: flex; flex-direction: column; gap: 140px; padding: 24px 0 12px; }
.article-pair {
  position: relative; display: grid;
  grid-template-columns: 1fr 1.3fr 1fr; gap: 40px; align-items: center;
}
.article-pair__media { position: relative; aspect-ratio: 4/3.3; opacity: 0; order: 2; z-index: 1; }
/* z-index above, not just order: CSS Grid paints items in order-modified
   document order by default, so with order:1/2/3 the media box (order:2)
   paints OVER the left column's leader line (order:1) once it animates
   out to 104px and reaches into the gap toward the media box -- exactly
   the bug Figma's Grid/Motion page (node 125:33) names explicitly:
   "an Articles box was once the frontmost layer, hiding 75% of its own
   line." Explicit z-index (media:1, content:2 below) fixes it on both
   sides, not just the side that happened to paint correctly by luck of
   DOM order. Corrected 2026-09-10. */
.article-pair__cover {
  position: absolute; width: 62%; aspect-ratio: 3/4;
  overflow: hidden; box-shadow: 0 24px 48px -16px rgba(0,0,0,0.35);
  background: rgba(0,0,0,0.06);
}
.article-pair__cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.article-pair__cover--a { left: 6%; top: 8%; transform: rotate(-5deg); z-index: 1; }
.article-pair__cover--b { right: 6%; top: 18%; transform: rotate(4deg); z-index: 2; }
.article-pair__media--single .article-pair__cover--a { left: 19%; }

/* Leader lines removed 2026-09-17 -- they clashed with the surrounding
   text on the desktop layout; Jimi asked for the simplest fix, which is
   to remove them rather than retune the timing/positioning. */
.article-pair__content--left { order: 1; text-align: right; position: relative; z-index: 2; }
.article-pair__content--right { order: 3; text-align: left; position: relative; z-index: 2; }
.article-pair__content { position: relative; }
/* Title -> Date -> Paragraph, 15px gap between each -- exact rule from
   Figma "02 -- Grid, Motion & Voice" (node 125:35, "TEXT STACKING -- SITEWIDE
   RULE"). Was 12/14/16px (each off by a few px); corrected 2026-09-10. */
.article-pair__title {
  font-family: "Bodoni Moda",serif; font-size: clamp(22px,2.4vw,30px);
  letter-spacing: 0.01em; line-height: 1.2; margin: 0 0 15px; opacity: 0;
}
.article-pair__meta { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0; color: var(--brass); margin: 0 0 15px; }
.article-pair__desc { font-size: 13px; line-height: 1.75; opacity: 0; margin: 0 0 15px; }
.article-pair__link {
  display: inline-block; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: inherit; text-decoration: none; opacity: 0; border-bottom: 1px solid currentColor; padding-bottom: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .article-pair__media, .article-pair__title, .article-pair__meta,
  .article-pair__desc, .article-pair__link {
    opacity: 1 !important; width: auto !important;
  }
}
@media (max-width: 980px) {
  .article-pairs { gap: 72px; }
  .article-pair { grid-template-columns: 1fr; gap: 24px; text-align: left !important; }
  .article-pair__media, .article-pair__content--left, .article-pair__content--right { order: initial; }
  .article-pair__media { aspect-ratio: 4/3.6; margin: 8px 0; }
  /* Rotated cover photos bleed past the media box's own edges -- push the
     text blocks further away so they don't sit under that bleed.
     Jimi 2026-09-17: text needs to be "a bit lower" on mobile. */
  .article-pair__content--left { margin-bottom: 20px; }
  .article-pair__content--right { margin-top: 20px; }
}

/* ─── 36. CONTACT — light hero (matches current Figma; supersedes the
   old dark zoom-photo hero for this page only) ─────────────────────
   Figma shows a hand-drawn botanical illustration top-right; no such
   asset exists in assets/img/contact/ yet, so it's omitted here rather
   than fabricated -- flagged in the rebuild handoff doc. */
.page-hero--contact-light {
  position: relative; padding: calc(var(--nav-h) + 64px) 0 64px;
  background: var(--bg-off); color: var(--text-dark);
}
.page-hero--contact-light .page-hero__content { max-width: 60ch; }

/* ==========================================================================
   PROJECT DETAIL PAGE (individual portfolio project pages, project-01..08)
   Built from Figma frames /project-1 .. /project-8 (file cbLiOmcMl5QeB1lDimxMMy).
   Reuses site tokens (--bg-off, --brass, --ink-deep) rather than introducing
   new ones; Figma's "Instrument Sans" body font is mapped to the site's
   existing "Inter" sans (already loaded, used everywhere else) to match the
   established look rather than adding a new webfont.
   ========================================================================== */
.project-detail { background: var(--bg-off); }

.project-detail__overview {
  display: flex; gap: 80px; align-items: flex-start;
  padding: 32px 0 64px;
}
.project-detail__hero { flex: 1 1 0; min-width: 0; }
.project-detail__hero-mask {
  position: relative; width: 100%; aspect-ratio: 768 / 480;
  overflow: hidden; border-radius: 4px; background: #e7e4dd;
}
.project-detail__hero-mask img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; border-radius: 4px;
  transition: opacity 220ms ease;
}
.project-detail__hero-mask img.is-swapping { opacity: 0.25; }

.project-detail__sidebar { flex: 0 0 420px; max-width: 480px; }
.project-detail__title {
  font-family: "Bodoni Moda", serif; font-weight: 500;
  font-size: clamp(32px, 4vw, 54px); line-height: 1.1; color: var(--ink-deep);
  margin: 0 0 16px;
}
.project-detail__category {
  font-family: "Inter", sans-serif; font-size: 13px; font-weight: 500;
  letter-spacing: 0.28em; text-transform: uppercase; color: var(--brass);
  margin: 0 0 40px;
}
.project-detail__registry { margin: 0 0 40px; }
.project-detail__registry-row {
  display: flex; gap: 16px; padding: 8px 0;
  border-bottom: 1px solid rgba(20,20,20,0.12);
}
.project-detail__registry-row dt {
  flex: 0 0 100px; font-family: "Inter", sans-serif; font-weight: 600;
  font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--brass); margin: 0;
}
.project-detail__registry-row dd {
  flex: 1 1 0; font-family: "Inter", sans-serif; font-size: 12px;
  line-height: 1.4; color: var(--ink-deep); margin: 0;
}
.project-detail__desc {
  font-family: "Inter", sans-serif; font-size: 14px; line-height: 1.7;
  color: #595752; margin: 0;
}

.project-detail__rule { height: 1px; width: 100%; background: rgba(20,20,20,0.12); border: 0; margin: 0; }

.project-detail__thumbs { padding: 64px 0 96px; }
.project-detail__thumbs-heading {
  font-family: "Inter", sans-serif; font-weight: 600; font-size: 12px;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-deep);
  margin: 0 0 32px;
}
.project-detail__thumb-grid {
  display: flex; flex-wrap: nowrap; align-items: flex-start; gap: 12px;
}
.project-detail__thumb {
  display: block; flex: 1 1 0; min-width: 0; padding: 0; border: 0; background: none;
  cursor: pointer; text-align: left;
}
.project-detail__thumb-img-wrap {
  display: block; position: relative; width: 100%; aspect-ratio: 4 / 3; border-radius: 4px;
  overflow: hidden; opacity: 0.72; transition: opacity 160ms ease;
  border: 1px solid transparent;
}
.project-detail__thumb:hover .project-detail__thumb-img-wrap { opacity: 1; }
.project-detail__thumb.is-active .project-detail__thumb-img-wrap { opacity: 1; border-color: var(--brass); }
.project-detail__thumb-img-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
.project-detail__thumb-meta {
  display: flex; justify-content: space-between; align-items: center;
  margin-top: 12px; font-family: "Inter", sans-serif; font-size: 11px;
}
.project-detail__thumb-meta span:first-child { color: var(--brass); font-weight: 500; letter-spacing: 0.05em; }
.project-detail__thumb-meta span:last-child { color: #595752; }

.project-detail__nav {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; margin-top: 48px; padding-top: 32px;
  border-top: 1px solid rgba(20,20,20,0.12);
  font-family: "Inter", sans-serif; font-size: 12px; font-weight: 600;
  letter-spacing: 0.22em; text-transform: uppercase;
}
.project-detail__nav a { color: var(--ink-deep); text-decoration: none; }
.project-detail__nav a:hover { color: var(--brass); }
.project-detail__nav-back { flex: 1 1 auto; }
.project-detail__nav-prev,
.project-detail__nav-next { flex: 0 0 auto; white-space: nowrap; }

@media (max-width: 860px) {
  .project-detail__overview { flex-direction: column; gap: 32px; }
  /* .project-detail__hero kept flex: 1 1 0 from the desktop row layout,
     which in a COLUMN flex container collapses its height to 0 (flex-basis
     0 on the main/vertical axis, no free space to grow into) -- the hero
     photo above the thumbnail grid silently disappeared on mobile.
     Fixed 2026-09-17. */
  .project-detail__hero { flex: none; width: 100%; }
  .project-detail__sidebar { flex: 1 1 auto; max-width: none; }
  .project-detail__nav { flex-wrap: wrap; }
  .project-detail__thumb-grid { flex-wrap: wrap; }
  .project-detail__thumb { flex: 1 1 calc(33.333% - 8px); min-width: 90px; }
}

.project-detail__crumb-bar {
  padding: calc(var(--nav-h) + 28px) 0 20px;
  background: var(--bg-off);
}
.project-detail__crumb-link {
  font-family: "Inter", sans-serif; font-size: 13px; color: var(--brass);
  text-decoration: none;
}
.project-detail__crumb-link:hover { text-decoration: underline; }
