/* =========================================================
   Tanleo_ (tanleophotography) — base styles
   ========================================================= */

@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400;1,9..144,500&family=Playfair+Display:wght@500;600&family=Inter:wght@300;400;500;600;700&display=swap');

:root{
  --bg: #0a0908;
  --bg-elev: #121110;
  --card: #171614;
  --text: #f3f0e9;
  --text-dim: #a6a29a;
  --text-faint: #706c64;
  --border: rgba(243,240,233,0.09);
  --accent: #c9a876;
  --gap: clamp(1rem, 2.4vw, 2.2rem);
  --page-pad: clamp(1.25rem, 5vw, 4rem);
  --nav-h: 84px;
}

*{ box-sizing: border-box; margin:0; padding:0; }

html{ scroll-behavior: smooth; }

/* Keyboard focus — a single house treatment (coherence pass, 2026-09-08).
   Before this, only the contact form had a considered focus state
   (border-color, further down); every other link/button on the site —
   the side nav, project photographs, quiet-links, the footer — fell
   back to nothing but the browser's own default. :focus-visible keeps
   this from ever showing on a mouse click, only on real keyboard
   navigation, so it changes nothing for the visitor scrolling with a
   trackpad. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible{
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

body{
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', sans-serif;
  font-weight: 300;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  position: relative;
}

/* grain overlay */
body::after{
  content:"";
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

/* ---------------- Site-wide: camera cursor + shutter flash ----------------
   Added 2026-09-08. Desktop/fine-pointer only (see js/script.js,
   initCameraCursor) -- a small black camera glyph replaces the native
   cursor and a brief shutter-flash answers a click on anything
   actually interactive, giving the whole site one small, recognizable
   Tanleo signature without touching any existing visual system. Both
   elements are created once, outside [data-taxi-view], so they
   persist untouched across every Taxi transition -- js/script.js only
   ever repositions/animates them, never recreates them.
   Above the grain overlay (z-index 9999) and everything else on the
   page on purpose -- a cursor that a photo or the nav could cover
   would defeat the point of it. */
.site-cursor{
  position: fixed;
  top: 0; left: 0;
  z-index: 10000;
  width: 16px; height: 16px;
  pointer-events: none;
  opacity: 0;
  /* js/script.js sets this transform directly on every pointermove
     with no CSS transition on it, so tracking is immediate and 1:1
     with the real cursor rather than lagging or easing toward it.
     Only opacity (shown/hidden at the viewport edge) animates here --
     the cursor itself has no separate click/press state; the shutter
     flash below is the one and only click response. */
  transform: translate(-50%,-50%);
  filter: drop-shadow(0 0 1px rgba(243,240,233,0.55)) drop-shadow(0 1px 2px rgba(0,0,0,0.35));
  transition: opacity 0.2s ease;
  will-change: transform;
}
.site-cursor svg{ display:block; width:100%; height:100%; }

/* Native cursor is hidden only once JS confirms a fine (mouse-like)
   pointer -- touch devices never get this class, so the "restore
   native cursor on mobile" requirement needs nothing extra here.
   Editable text fields keep the native text-beam cursor throughout;
   everything else defers entirely to .site-cursor above. */
html.has-camera-cursor,
html.has-camera-cursor *{ cursor: none; }
html.has-camera-cursor input[type="text"],
html.has-camera-cursor input[type="email"],
html.has-camera-cursor input[type="tel"],
html.has-camera-cursor input[type="search"],
html.has-camera-cursor input[type="url"],
html.has-camera-cursor input[type="password"],
html.has-camera-cursor textarea,
html.has-camera-cursor [contenteditable]{ cursor: text; }

.site-cursor-flash{
  position: fixed;
  top: 0; left: 0;
  z-index: 10000;
  width: 22px; height: 22px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle, rgba(243,240,233,0.95) 0%, rgba(243,240,233,0.35) 55%, rgba(243,240,233,0) 78%);
  mix-blend-mode: screen;
  will-change: opacity, transform;
}
@media (pointer: coarse), (hover: none){
  .site-cursor, .site-cursor-flash{ display:none; }
}

/* display:block/max-width keep images from ever overflowing their
   box. The rest of this rule exists for one specific, real bug: on
   a slow connection (or the very first, uncached visit), Safari
   paints an image's alt text — and a small broken/loading icon —
   directly on the page for the second or two before a large photo
   finishes downloading, sitting in the top-left corner of wherever
   that photo was about to be. Chrome mostly doesn't do this, but
   Safari does, and every hero/plate photo on this site is a large
   file, so on Safari it showed up everywhere a big photo loads:
   Portfolio's stage photo, each category page's opening photo, the
   homepage's Featured Photographs. font-size:0/color:transparent
   stop the browser from ever painting that fallback text (Chrome and
   Safari both size and colour it using the img element's own text
   styles) without touching the alt attribute itself — screen readers
   read alt text from the accessibility tree, not from what's
   rendered on screen, so nothing is lost there. The background-color
   means that brief gap before a photo paints is a plain dark panel
   matching the site, instead of blank/white with text floating on
   it. */
img{
  display:block;
  max-width:100%;
  font-size: 0;
  color: transparent;
  background-color: var(--card);
}

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

h1,h2,h3,h4{
  font-family: 'Fraunces', serif;
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.08;
}

.eyebrow{
  font-family:'Inter', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  font-size: 0.72rem;
  color: var(--text-dim);
  font-weight: 500;
}

.container{
  width: 100%;
  max-width: 1440px;
  margin: 0 auto;
  padding: 0 var(--page-pad);
}

/* ---------------- Nav ---------------- */
.site-nav{
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 1000;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--page-pad);
  background: linear-gradient(to bottom, rgba(10,9,8,0.75), rgba(10,9,8,0));
  transition: background 0.4s ease, box-shadow 0.4s ease, backdrop-filter 0.4s ease;
}
.site-nav.scrolled{
  background: rgba(10,9,8,0.86);
  backdrop-filter: blur(14px) saturate(140%);
  box-shadow: 0 1px 0 var(--border);
}
.brand{
  font-family:'Fraunces', serif;
  font-size: 1.3rem;
  letter-spacing: 0.14em;
  font-weight: 500;
  text-transform: uppercase;
}
.brand span{ color: var(--accent); }

/* ---------------- Homepage intro ---------------- */
.intro-overlay{
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: var(--bg);
  display:flex;
  align-items:center;
  justify-content:center;
  transition: transform 1.3s cubic-bezier(0.16,1,0.3,1);
}
.intro-overlay.intro-hide{
  /* Whole panel — name included — drags straight up and off the top
     of the screen as one solid piece, revealing the homepage sitting
     underneath, rather than fading out in place. inset:0 makes this
     element exactly one viewport tall, so -100% is a clean, complete
     exit with no gap or overshoot. */
  transform: translateY(-100%);
  pointer-events: none;
}
.intro-name{
  font-family:'Fraunces', serif;
  font-size: clamp(2.4rem, 8vw, 5.5rem);
  letter-spacing: 0.14em;
  font-weight: 500;
  text-transform: uppercase;
  color: var(--text);
  display:flex;
}
.intro-name span{
  display:inline-block;
  opacity:0;
  transform: translateY(14px);
  animation: introLetterIn 0.7s cubic-bezier(0.16,1,0.3,1) forwards;
  animation-delay: calc(var(--i) * 0.15s);
}
.intro-underscore{ color: var(--accent); }
@keyframes introLetterIn{
  to{ opacity:1; transform:translateY(0); }
}
body.intro-active{ overflow:hidden; }

/* ---------------- Nav: centered brand variant (homepage) ---------------- */
.site-nav-centered{
  display:grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  height: auto;
  min-height: var(--nav-h);
  padding-top: 1.2rem;
  padding-bottom: 1.2rem;
}
.site-nav-centered .brand{
  grid-column: 2;
  justify-self: center;
  font-size: clamp(1.7rem, 3.4vw, 2.6rem);
  line-height: 1;
}
.site-nav-centered nav{
  grid-column: 3;
  justify-self: end;
}
.site-nav-centered .menu-trigger{
  grid-column: 1;
  justify-self: start;
}

/* ---------------- Home: Menu trigger + full-screen overlay ---------------- */
.menu-trigger{
  display:flex;
  align-items:center;
  gap: 0.7rem;
  background:none;
  border:none;
  cursor:pointer;
  color: var(--text);
}
.menu-trigger-icon{
  display:flex;
  flex-direction:column;
  gap: 5px;
}
.menu-trigger-icon span{
  width: 24px; height:1px; background: var(--text);
  transition: background 0.3s ease;
}
.menu-trigger-label{
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-weight: 500;
  color: var(--text-dim);
  transition: color 0.3s ease;
}
.menu-trigger:hover .menu-trigger-label{ color: var(--text); }
.menu-trigger:hover .menu-trigger-icon span{ background: var(--accent); }

.menu-overlay{
  position: fixed;
  inset: 0;
  z-index: 1200;
  background: transparent;
  display:flex;
  flex-direction:row;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-16px);
  transition: opacity 0.45s ease, transform 0.45s ease, visibility 0s linear 0.45s;
  pointer-events: none;
}
.menu-overlay.open{
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity 0.45s ease, transform 0.45s ease;
  pointer-events: auto;
}
/* Used when a menu link navigates away — see closeMenuInstant() in
   js/script.js — so the overlay's own close fade never overlaps the
   destination page's transition, which was already starting
   underneath the overlay's (much higher) usual stacking layer. */
.menu-overlay.no-transition{ transition: none !important; }
.menu-overlay-left{
  width: 42%;
  min-width: 320px;
  padding: var(--page-pad);
  display:flex;
  flex-direction:column;
  background: var(--bg);
}
.menu-overlay-image{
  display:none;
  flex: 1;
  position: relative;
  overflow: hidden;
}
.menu-overlay-image-slide{
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transition: opacity 1.2s ease;
}
.menu-overlay-image-slide.active{ opacity: 1; }
.menu-close{
  display:flex;
  align-items:center;
  gap: 0.7rem;
  align-self: flex-start;
  background:none;
  border:none;
  cursor:pointer;
  color: var(--text);
  margin-top: clamp(0.6rem, 1.5vw, 1.2rem);
}
.menu-close-icon{
  font-size: 1.6rem;
  line-height: 1;
  font-weight: 300;
}
.menu-close-label{
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-weight: 500;
  color: var(--text-dim);
  transition: color 0.3s ease;
}
.menu-close:hover .menu-close-label{ color: var(--text); }
.menu-close:hover .menu-close-icon{ color: var(--accent); }

.menu-overlay-nav{
  flex: 1;
  display:flex;
  align-items:flex-start;
  padding-top: clamp(2rem, 6vw, 4rem);
}
.menu-overlay-nav ul{
  list-style:none;
  display:flex;
  flex-direction:column;
  gap: clamp(0.2rem, 1vw, 0.6rem);
}
.menu-overlay-nav a{
  font-family: 'Inter', sans-serif;
  font-weight: 600;
  font-size: clamp(1.6rem, 4.5vw, 2.8rem);
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--text-dim);
  transition: color 0.35s ease;
}
.menu-overlay-nav a:hover,
.menu-overlay-nav a.active{ color: var(--text); }
.menu-overlay-nav a.active{ color: var(--accent); }
@media (max-width: 560px){
  .menu-trigger-label{ display:none; }
}
@media (max-width: 780px){
  .menu-overlay-image{ display:none; }
  .menu-overlay-left{ width: 100%; min-width: 0; }
}

/* ---------------- Persistent left-side navigation ----------------
   Added 2026-08-15 alongside the single-continuous-scroll rebuild of
   index.html (see claude/continuous-scroll-architecture-proposal.md).
   Replaces the hamburger-triggered .menu-overlay on this page only —
   the four destinations (Home/Projects/About/Contact) are always on
   screen instead of summoned, matching Tan's brief directly. Visual
   language borrows the same restrained pattern used everywhere else
   on the site for "you are here": a small accent-gold dot that scales
   in on the active item.
   js/script.js (see "Persistent side nav: scroll-spy" in
   initPageContent()) toggles .active as the visitor scrolls; this
   file only defines what .active looks like. services.html and
   workshops.html are untouched and keep the older hamburger/overlay
   pattern above, since they're still separate, unmerged pages. */
.side-nav{
  position: fixed;
  left: clamp(1rem, 3vw, 2.2rem);
  /* Anchored just under the fixed top header rather than vertically
     centered — checked empirically (Playwright screenshots) against
     every section: Home's title list, the About heading and the
     Contact form all sit lower on the screen, so this band stays
     clear of them everywhere, which a true vertical center did not
     (it collided with more than one section's own content). */
  top: calc(var(--nav-h) + clamp(1.4rem, 3vw, 2.4rem));
  z-index: 1300;
  display: flex;
  flex-direction: column;
  gap: clamp(1.1rem, 2.4vw, 1.6rem);
}
.side-nav a{
  font-family: 'Inter', sans-serif;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-weight: 500;
  color: var(--text-faint);
  position: relative;
  display: inline-block;
  padding-left: 1.1rem;
  transition: color 0.35s ease;
}
.side-nav a::before{
  content:"";
  position:absolute; left:0; top:50%;
  width:5px; height:5px; border-radius:50%;
  transform: translateY(-50%) scale(0.001);
  background: var(--accent);
  transition: transform 0.35s cubic-bezier(0.16,1,0.3,1);
}
.side-nav a:hover{ color: var(--text-dim); }
.side-nav a.active{ color: var(--text); }
.side-nav a.active::before{ transform: translateY(-50%) scale(1); }
@media (max-width: 780px){
  .side-nav{ left: 0.85rem; gap: 0.85rem; }
  .side-nav a{ font-size: 0.6rem; letter-spacing: 0.12em; padding-left: 0.9rem; }
}

/* The four nav destinations plus the chapter sections folded in
   underneath "Projects" (Street/Architecture/Portraits) all need
   scroll-margin so the fixed header never covers the top of whatever
   was just scrolled to — same reasoning as .category-block already
   used elsewhere on this site. */
#home, #projects, #street, #architecture, #portraits, #about, #contact{
  scroll-margin-top: var(--nav-h);
}

/* ---------------- Cinematic hero + hover-reveal story index (merged, one viewport) ---------------- */
.chero-wrap{
  position: relative;
  height: 175vh;
}
.chero{
  position: sticky;
  top: 0;
  height: 100svh;
  min-height: 640px;
  overflow: hidden;
  display:flex;
  flex-direction: column;
  justify-content: flex-end;
}
.chero-media{
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}
.chero-base{
  position:absolute; inset: -3%;
  background-size: cover;
  background-position: center;
  animation: cheroZoom 26s ease-in-out infinite alternate;
  opacity: 1;
  transition: opacity 1s ease;
}
@keyframes cheroZoom{
  from{ transform: scale(1); }
  to{ transform: scale(1.07); }
}
/* the 5 story images — hidden until their title is hovered */
.chero-media .story-media{
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transform: scale(1.06);
  transition: opacity 1.1s ease, transform 1.6s ease;
}
.chero:has(.s-light:hover) .chero-media .story-media.s-light,
.chero:has(.s-people:hover) .chero-media .story-media.s-people,
.chero:has(.s-motion:hover) .chero-media .story-media.s-motion,
.chero:has(.s-stillness:hover) .chero-media .story-media.s-stillness,
.chero:has(.s-colour:hover) .chero-media .story-media.s-colour,
.chero:has(.s-night:hover) .chero-media .story-media.s-night,
.chero-media .story-media.is-active{
  opacity: 1;
  transform: scale(1);
}
.chero:has(.story-link:hover) .chero-base,
.chero:has(.story-link.is-active) .chero-base{ opacity: 0; }
.chero-scrim{
  position:absolute; inset:0;
  background:
    linear-gradient(to top, rgba(8,7,6,0.88) 0%, rgba(8,7,6,0.28) 46%, rgba(8,7,6,0.45) 100%),
    linear-gradient(to bottom, rgba(8,7,6,0.5) 0%, rgba(8,7,6,0) 26%);
}

.chero-content{
  position: relative;
  z-index: 2;
  padding: 0 var(--page-pad) clamp(2.6rem, 6vw, 4.6rem);
  width:100%;
}
.chero-list{ list-style:none; max-width: 620px; }
.story-row{ }
.story-link{
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: baseline;
  gap: 0.55rem;
  padding: clamp(0.15rem, 0.5vw, 0.3rem) 0;
}
.story-title{
  font-family: 'Inter', sans-serif;
  font-weight: 700;
  font-size: clamp(1.6rem, 3.4vw, 2.6rem);
  line-height: 1.25;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
  text-shadow: 0 2px 20px rgba(0,0,0,0.5);
  transition: opacity 0.4s ease, color 0.4s ease;
}
.story-tag{
  margin-top: 0;
  font-family: 'Inter', sans-serif;
  font-weight: 500;
  font-size: 0.64rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  flex-shrink: 0;
  transition: opacity 0.4s ease;
}
.chero-list:hover .story-title,
.chero-list:hover .story-tag,
.chero-list:has(.story-link.is-active) .story-title,
.chero-list:has(.story-link.is-active) .story-tag{ opacity: 0.8; }
.story-link:hover .story-title,
.story-link.is-active .story-title{ opacity: 1; color: var(--accent); }
.story-link:hover .story-tag,
.story-link.is-active .story-tag{ opacity: 0.8; }

.story-media-m{ display: none; }

.hero-scroll{
  position:absolute;
  bottom: 2rem; right: var(--page-pad);
  z-index:2;
  display:flex;
  color: rgba(243,240,233,0.55);
  opacity: 0.8;
  transition: opacity 0.3s ease, color 0.3s ease;
  animation: heroScrollBounce 2.4s ease-in-out infinite;
}
.hero-scroll:hover{
  color: var(--accent);
  opacity: 1;
}
@keyframes heroScrollBounce{
  0%, 100%{ transform: translateY(0); }
  50%{ transform: translateY(6px); }
}
@media (prefers-reduced-motion: reduce){
  .hero-scroll{ animation: none; }
}

@media (max-width: 780px){
  .chero-wrap{ height: auto; }
  .chero{ position: static; height: auto; min-height: 0; display: block; }
  .chero-media{ position: relative; height: 62vh; min-height: 340px; }
  .chero-base{ animation: none; }
  /* Previously display:none here, with each story's photo shown
     inline instead (.story-media-m below). Now left visible so the
     same auto-cycling background used on desktop (js/script.js,
     "Hero Selected Stories auto-cycle") also drives this banner on
     phones — touch screens have no hover to trigger the swap, so the
     photo just changes on its own every 4.5s instead. */
  .chero-content{ padding-top: clamp(2.4rem, 7vw, 3.4rem); }
  .chero-list{ max-width: none; border-top: 1px solid var(--border); }
  .story-row{ border-bottom: 1px solid var(--border); }
  .story-link{ flex-direction: column; padding: clamp(1.2rem, 4vw, 1.8rem) 0 0; }
  .story-title{ text-shadow: none; }
  .story-tag{ margin-top: 0.2rem; }
  /* No longer needed now the big banner above cycles through the same
     photos on its own — keeping both would just repeat each photo
     twice on the page. */
  .story-media-m{ display: none; }
  .hero-scroll{ display: none; }
}

/* ---------------- Statement ---------------- */
.statement{
  position: relative;
  z-index: 1;
  background: var(--bg);
  min-height: 92vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(3rem, 8vw, 5rem) var(--page-pad);
  text-align: center;
}
.statement-text{
  font-family: 'Fraunces', serif;
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(0.95rem, 1.7vw, 1.65rem);
  line-height: 1.4;
  letter-spacing: 0.02em;
  color: var(--text);
  max-width: 780px;
  margin: 0 auto;
}
@media (max-width: 780px){
  .statement{ min-height: 60vh; }
}

/* ---------------- "Selected Works" title card ----------------
   A single large display line between the manifesto and the Featured
   Photographs sequence, in Playfair Display rather than the site's
   usual Fraunces — a deliberate one-off, matching a specific reference
   image Tanleo pointed to (a tall, high-contrast serif, wide letter
   spacing, all caps). Kept to this one heading rather than adopted
   site-wide, the same way a printed exhibition catalogue sometimes
   sets one title page in a different display face from its body type. */
.selected-works-heading{
  background: var(--bg);
  font-family: 'Playfair Display', serif;
  font-weight: 500;
  text-transform: uppercase;
  text-align: center;
  font-size: clamp(2.2rem, 6vw, 4.6rem);
  letter-spacing: 0.03em;
  line-height: 1.1;
  padding: clamp(3rem, 6.5vw, 4.5rem) var(--page-pad) clamp(3rem, 7vw, 5rem);
  margin: 0;
}

/* ---------------- Featured Photographs ---------------- */
/* A slow, one-at-a-time sequence of standalone photographs — no
   categories, no grid. Each plate is its own full-height "page";
   the image is sized generously but always framed by whitespace,
   like a print rather than a full-bleed banner. */
.featured{ background: var(--bg); }
.plate{
  position: relative;
  min-height: 100vh;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(3rem, 7vw, 5rem) var(--page-pad);
}
.plate img{
  display: block;
  max-width: min(88vw, 1400px);
  max-height: 84vh;
  width: auto;
  height: auto;
  box-shadow: 0 40px 100px rgba(0,0,0,0.5);
}
.plate-num{
  position: absolute;
  bottom: clamp(1.4rem, 3.5vw, 2.2rem);
  right: var(--page-pad);
  font-family: 'Fraunces', serif;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  color: var(--text-faint);
}
@media (max-width: 780px){
  .plate{ min-height: 82vh; padding: 2.4rem var(--page-pad); }
  .plate img{ max-width: 100%; max-height: 72vh; }
}

/* ---------------- Section framing ---------------- */
.section{
  padding: clamp(4.5rem, 9vw, 8rem) 0;
}
.section-head{
  max-width: 60ch;
  margin-bottom: clamp(2.4rem, 5vw, 4rem);
}
.section-head h2{
  font-size: clamp(2.1rem, 4.4vw, 3.4rem);
  margin-top: 0.6rem;
}
.section-head p{
  color: var(--text-dim);
  margin-top: 1.1rem;
  max-width: 52ch;
}

/* ---------------- Page banner (interior pages) ---------------- */
.page-banner{
  position: relative;
  height: 46vh;
  min-height: 320px;
  display:flex; align-items:flex-end;
  background-size: cover;
  background-position: center;
  overflow:hidden;
}
.page-banner::before{
  content:"";
  position:absolute; inset:0;
  background: linear-gradient(to top, rgba(8,7,6,0.95), rgba(8,7,6,0.35) 60%, rgba(8,7,6,0.55));
}
.page-banner-inner{
  position:relative; z-index:1;
  padding: 0 var(--page-pad) 3rem;
}
.page-banner h1{
  font-size: clamp(2.4rem, 6vw, 4.6rem);
}

/* ---------------- Footer ---------------- */
.site-footer{
  border-top: 1px solid var(--border);
  padding: clamp(2.4rem, 5vw, 3.6rem) 0 2rem;
}
.footer-inner{
  display:flex; flex-wrap:wrap; justify-content: space-between;
  align-items:flex-end; gap: 2rem;
}
.footer-brand{
  font-family:'Fraunces', serif;
  font-size: 1.15rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.footer-links{ display:flex; gap: 1.8rem; list-style:none; flex-wrap:wrap; }
.footer-links a{
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-dim);
  transition: color 0.3s ease;
}
.footer-links a:hover{ color: var(--text); }
.footer-meta{
  margin-top: 2rem;
  color: var(--text-faint);
  font-size: 0.78rem;
  display:flex; justify-content:space-between; flex-wrap:wrap; gap:0.6rem;
}

/* ---------------- Buttons ---------------- */
.btn{
  display:inline-flex; align-items:center; gap:0.6rem;
  padding: 0.95rem 1.8rem;
  border: 1px solid var(--border);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-weight: 500;
  transition: border-color 0.3s ease, background 0.3s ease, color 0.3s ease;
}
.btn:hover{ border-color: var(--accent); color: var(--accent); }
.btn-solid{
  background: var(--text);
  color: var(--bg);
  border-color: var(--text);
}
.btn-solid:hover{
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

/* ---------------- Home: preview grid ---------------- */
.preview-grid{
  display:grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap);
}
.preview-item{
  position: relative;
  display:block;
  aspect-ratio: 4/5;
  overflow:hidden;
  background: var(--card);
}
.preview-item img{
  width:100%; height:100%; object-fit:cover;
  transition: transform 0.8s ease, filter 0.8s ease;
  filter: grayscale(0.08);
}
.preview-item:hover img{ transform: scale(1.06); filter: grayscale(0); }
.preview-caption{
  position:absolute; left:1.2rem; bottom:1.2rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--text);
  background: rgba(8,7,6,0.55);
  padding: 0.4rem 0.8rem;
  backdrop-filter: blur(6px);
}
@media (max-width: 760px){
  .preview-grid{ grid-template-columns: 1fr; }
}

/* ---------------- Quiet text link ---------------- */
.quiet-link{
  display:inline-flex; align-items:center; gap:0.6rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-weight: 500;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
  transition: color 0.3s ease, border-color 0.3s ease, gap 0.3s ease;
}
.quiet-link span{ transition: transform 0.3s ease; }
.quiet-link:hover{ color: var(--accent); border-color: var(--accent); gap: 0.9rem; }

/* ---------------- Quiet closing ---------------- */
/* No border-top here on purpose (coherence pass, 2026-09-08) — Statement
   and Closing share the same var(--bg) and are meant to read as one
   continuous quiet passage, not two page sections meeting at a seam.
   The pause between them is carried by padding and the .reveal timing
   alone. */
.closing{
  padding: clamp(6rem, 12vw, 10rem) 0 clamp(2rem, 5vw, 3.5rem);
  text-align:center;
}
/* Bottom padding trimmed 2026-09-09, integrating the approved
   Statement -> Closing -> Projects wall seam (Projects v2): the full
   10rem read as a stretch of dead scroll before the wall's Street
   chapter became visible, like a new section had started rather than
   the same field continuing. Top/side padding unchanged. */
.closing .eyebrow{ display:block; margin-bottom: 1.6rem; }
.closing blockquote{
  font-family: 'Fraunces', serif;
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.7rem, 3.6vw, 2.8rem);
  max-width: 20ch;
  margin: 0 auto clamp(1.8rem, 4vw, 2.6rem);
  color: var(--text);
  line-height: 1.28;
}

/* ---------------- CTA band ---------------- */
.cta-band{
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
}
.cta-inner{
  text-align:center;
  display:flex; flex-direction:column; align-items:center; gap:1.4rem;
}
.cta-inner h2{ font-size: clamp(1.9rem, 4vw, 3rem); max-width: 18ch; }
.cta-inner p{ color: var(--text-dim); max-width: 46ch; }

/* ---------------- Category pages: monumental hero photograph ----------------
   Now the very first thing on the page — one huge photograph, nothing
   else, before any text. This exact element is also the shared-element
   morph target: on the Work index, the same photograph sits inside
   .stage-photo-frame (also a self-clipping box with an object-fit:cover
   image filling it, same pattern as here) — clicking through animates
   that chapter's developed photo directly into this big one via GSAP
   Flip (see js/transitions.js), so the photograph itself is the page
   transition rather than the whole page just changing beneath it.
   Deliberately a plain overflow:hidden box with a filling <img>, not a
   transformed <img> on its own, so the crop stays correct at every size
   Flip passes through mid-flight, not just at the start and end.

   Arrival: by default this is simply visible immediately, so a JS
   failure never hides the page's primary content. Two things opt it
   into a held-still-then-fade-in entrance instead — both toggled by
   the same .intro-pending class, added in js/transitions.js (onEnter,
   before the incoming page is ever painted) and released in
   js/script.js (initPageContent, after a brief pause):
     - arriving normally (nav, footer, a direct/hard link) — the page
       holds still for a beat, then the photograph settles in, giving
       the arrival its own quiet pace rather than looking like loading.
     - arriving via the Work-index photo-morph above gets none of this:
       Flip is already growing this exact photograph into place, which
       is its own arrival moment, so .intro-pending is deliberately
       left off in that case. */
.cat-hero-photo{
  width: 100%;
  height: 100vh;
  min-height: 520px;
  overflow: hidden;
  background: var(--bg-elev);
  opacity: 1;
}
@media (prefers-reduced-motion: no-preference){
  .cat-hero-photo{ transition: opacity 1.4s ease; }
}
.cat-hero-photo.intro-pending{ opacity: 0; }
.cat-hero-photo img{
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
@media (max-width: 780px){
  .cat-hero-photo{ height: 100svh; min-height: 420px; }
}

/* ---------------- Category pages: hero text ----------------
   Now sits below the monumental photograph rather than above it —
   the visitor scrolls past the photograph first, and only then does
   the category name/description arrive. Carries .reveal (see
   js/script.js) so the eyebrow/title/description fade and slide up
   into view as this section reaches the viewport — the font-family
   and weight still match .stage-name on the Work index on purpose, so
   the category name reads as the same piece of type carrying through
   from one page to the next. */
.cat-hero{
  min-height: 62vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(5rem, 10vw, 7rem) var(--page-pad) clamp(3rem, 6vw, 4rem);
}
.cat-hero .eyebrow{ display: block; margin-bottom: 1.2rem; }
.cat-hero-title{
  font-size: clamp(4rem, 13vw, 9rem);
  line-height: 0.95;
}
.cat-hero-desc{
  margin-top: 1.8rem;
  color: var(--text-dim);
  max-width: 46ch;
  font-family: 'Fraunces', serif;
  font-style: italic;
  font-weight: 400;
  font-size: 1.05rem;
  line-height: 1.6;
}

.cat-more-note{
  padding: clamp(3rem, 6vw, 4.5rem) var(--page-pad) clamp(5rem, 10vw, 7rem);
  text-align: center;
  color: var(--text-faint);
  font-size: 0.85rem;
  letter-spacing: 0.03em;
}
.cat-more-note .quiet-link{ margin-top: 1.2rem; }
@media (max-width: 780px){
  .cat-hero{ min-height: 46vh; }
}

/* ---------------- Projects: category sections ---------------- */
.category-block{
  margin-bottom: clamp(3.5rem, 7vw, 5.5rem);
  scroll-margin-top: calc(var(--nav-h) + 1.5rem);
}
.category-block:last-child{ margin-bottom: 0; }
.category-head{ margin-bottom: 1.6rem; }
.category-head h3{ font-size: clamp(1.5rem, 2.6vw, 2rem); }
.category-empty{
  padding: clamp(2rem, 5vw, 3rem);
  border: 1px dashed var(--border);
  text-align:center;
  color: var(--text-faint);
  font-size: 0.92rem;
}

/* ---------------- Projects: Sony Workshops ---------------- */
.workshops-head{
  max-width: 60ch;
  margin-bottom: clamp(2rem, 4.5vw, 3rem);
}
.workshops-head h2{
  font-size: clamp(1.9rem, 3.6vw, 2.7rem);
  margin: 0.6rem 0 0;
}
.workshops-list{
  display: flex;
  flex-direction: column;
  gap: clamp(3rem, 6vw, 4.5rem);
}
.workshop-entry{
  display:grid;
  grid-template-columns: 1fr 0.6fr;
  gap: clamp(2rem, 5vw, 5rem);
  align-items:start;
}
.workshop-entry:not(:first-child){
  padding-top: clamp(3rem, 6vw, 4.5rem);
  border-top: 1px solid var(--border);
}
.workshop-text h3{
  font-family:'Inter', sans-serif;
  font-weight:600;
  font-size: 1.05rem;
  line-height: 1.4;
  color: var(--text-dim);
  margin-bottom: 1.2rem;
}
.workshop-text p{ color: var(--text-dim); max-width: 48ch; }
.workshop-video{
  aspect-ratio: 9/16;
  width: 100%;
  max-width: 340px;
  margin: 0 auto;
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--card);
}
.workshop-video video{
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
@media (max-width: 800px){
  .workshop-entry{ grid-template-columns: 1fr; }
  .workshop-video{ order: -1; }
}

/* ---------------- Projects: bento grid ---------------- */
.bento-grid{
  display:grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 260px;
  grid-auto-flow: dense;
  gap: var(--gap);
}
.bento-item{
  position:relative;
  display:block;
  overflow:hidden;
  background: var(--card);
  border: 1px solid var(--border);
}
.bento-item img{
  width:100%; height:100%; object-fit:cover;
  transition: transform 0.9s ease, filter 0.9s ease;
  filter: grayscale(0.1) contrast(1.02);
}
.bento-item:hover img{ transform: scale(1.055); filter: grayscale(0); }
.bento-item.span-tall{ grid-row: span 2; }
.bento-item.span-wide{ grid-column: span 2; }
.bento-caption{
  position:absolute; left:0; right:0; bottom:0;
  padding: 1.4rem 1.3rem;
  display:flex; flex-direction:column; gap:0.3rem;
  background: linear-gradient(to top, rgba(8,7,6,0.88), rgba(8,7,6,0));
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
.bento-item:hover .bento-caption{ opacity:1; transform: translateY(0); }
.bento-title{ font-family:'Fraunces', serif; font-size:1.05rem; }
.bento-tag{
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--accent);
}
@media (max-width: 980px){
  .bento-grid{ grid-template-columns: repeat(2, 1fr); grid-auto-rows: 240px; }
  .bento-item.span-wide{ grid-column: span 2; }
  .bento-caption{ opacity:1; transform:none; background: linear-gradient(to top, rgba(8,7,6,0.85), rgba(8,7,6,0) 70%); }
}
@media (max-width: 560px){
  .bento-grid{ grid-template-columns: 1fr; grid-auto-rows: 320px; }
  .bento-item.span-tall,
  .bento-item.span-wide{ grid-column: span 1; grid-row: span 1; }
}

/* ---------------- About: body text ---------------- */
.about-body{
  max-width: 68ch;
  margin: 0 auto;
  display:flex; flex-direction:column; gap:1.3rem;
  color: var(--text-dim);
  font-size: 1.02rem;
}
.about-body p:first-child{
  font-family:'Fraunces', serif;
  font-style: italic;
  font-size: 1.25rem;
  color: var(--text);
  font-weight: 400;
}

/* ---------------- About: recognition ---------------- */
.recognition{
  border-top: 1px solid var(--border);
  background: var(--bg-elev);
  padding-bottom: clamp(1rem, 3vw, 2rem);
}
/* ---------------- Services list ---------------- */
.service-list{
  max-width: 900px;
  margin: 0 auto;
  border-top: 1px solid var(--border);
}
.service-row{
  display:grid;
  grid-template-columns: 90px 1fr;
  gap: clamp(1rem, 3vw, 2.4rem);
  padding: clamp(1.8rem, 4vw, 2.6rem) 0;
  border-bottom: 1px solid var(--border);
  align-items: baseline;
  transition: padding-left 0.35s ease;
}
.service-row:hover{ padding-left: 0.6rem; }
.service-num{
  font-family:'Fraunces', serif;
  font-size: 1.1rem;
  color: var(--text-faint);
}
.service-copy h3{
  font-size: clamp(1.35rem, 2.4vw, 1.8rem);
  margin-bottom: 0.7rem;
}
.service-copy p{ color: var(--text-dim); max-width: 60ch; }
@media (max-width: 560px){
  .service-row{ grid-template-columns: 1fr; gap: 0.6rem; }
}

/* ---------------- Contact: full-bleed photo hero ----------------
   See the comment in contact.html for the reasoning — one moody photo
   fills the section, everything else sits on top of it behind a dark
   scrim for legibility, same layering pattern as .page-banner and
   .chero-media elsewhere on the site. */
.contact-hero{
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  overflow: hidden;
  padding: clamp(7rem, 15vw, 10rem) 0 clamp(4rem, 8vw, 6rem);
}
.contact-hero-bg{
  position: absolute; inset: 0;
  z-index: 0;
}
.contact-hero-bg img{
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.contact-hero-scrim{
  position: absolute; inset: 0;
  z-index: 1;
  background:
    linear-gradient(to bottom, rgba(8,7,6,0.8) 0%, rgba(8,7,6,0.5) 32%, rgba(8,7,6,0.6) 72%, rgba(8,7,6,0.85) 100%);
}
.contact-hero-inner{
  position: relative;
  z-index: 2;
  width: 100%;
}
@media (max-width: 780px){
  .contact-hero{ min-height: 0; padding-top: clamp(6rem, 20vw, 7.5rem); }
}

/* ---------------- Contact ---------------- */
.contact-split{
  display:grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: clamp(2.5rem, 6vw, 6rem);
}
.contact-info h1{
  font-size: clamp(1.9rem, 3.6vw, 2.6rem);
  margin: 0.6rem 0 1.2rem;
}
.contact-lead{ color: var(--text-dim); max-width: 46ch; margin-bottom: 2.2rem; }
.contact-detail{
  display:flex; flex-direction:column; gap: 0.3rem;
  padding: 1.1rem 0;
  border-top: 1px solid var(--border);
}
.contact-detail:last-child{ border-bottom: 1px solid var(--border); }
.contact-label{
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--text-faint);
}
.contact-value{
  font-family:'Fraunces', serif;
  font-size: 1.2rem;
  transition: color 0.3s ease;
}
a.contact-value:hover{ color: var(--accent); }

.contact-form{ display:flex; flex-direction:column; gap: 1.3rem; }
.form-row{ display:flex; flex-direction:column; gap: 0.55rem; }
.form-row label{
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-dim);
}
.form-row input,
.form-row select,
.form-row textarea{
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: 'Inter', sans-serif;
  font-size: 0.95rem;
  padding: 0.85rem 1rem;
  resize: vertical;
  transition: border-color 0.3s ease;
}
.form-row input:focus,
.form-row select:focus,
.form-row textarea:focus{
  outline:none;
  border-color: var(--accent);
}
.form-note{
  font-size: 0.76rem;
  color: var(--text-faint);
  line-height: 1.5;
}
.form-note code{
  background: var(--card);
  padding: 0.1rem 0.4rem;
  border: 1px solid var(--border);
}
@media (max-width: 800px){
  .contact-split{ grid-template-columns: 1fr; }
}

/* ---------------- utility ---------------- */
/* Visible by default — JS opts elements into the pre-animation state so
   content never depends on JavaScript/IntersectionObserver to be seen. */
.reveal{
  /* Eased onto the site's one "house" curve (already used for hover
     states and the glass-nav) instead of a plain linear-ish ease, as
     part of the 2026-09-07 coherence pass -- small, sitewide, and the
     kind of thing that reads as considered rather than templated
     once every fade shares one signature instead of several similar
     but not-quite-matching ones. */
  transition: opacity 0.9s cubic-bezier(0.16,1,0.3,1), transform 0.9s cubic-bezier(0.16,1,0.3,1);
}
.reveal.pre{ opacity: 0; transform: translateY(28px); }

.reveal.in{ opacity: 1; transform: translateY(0); }

/* Added 2026-09-07, superseded 2026-09-09 by Projects Index v2's
   simpler Statement -> Closing seam (a trimmed .closing bottom
   padding, see above) — the JS that wrote --proximity onto .closing
   no longer runs (its querySelector target, .project-photo-mat, no
   longer exists), so this rule is inert and just falls back to its
   default, full opacity. Left in place rather than removed — outside
   the scope of the 2026-09-09 integration checkpoint. */
.closing.reveal.in{ opacity: var(--proximity, 1); }

/* ============================================================
   The Photographic Threshold — isolated WebGL module
   Added 2026-09-07. Sits between Closing and the Projects Index.
   All behavior lives in js/threshold.js, which is its own file and
   never touches script.js, transitions.js, or retouching.html.
   This block only lays out the pinned canvas: a tall wrapper
   (.threshold) so scroll has room to drive the camera, containing a
   position:sticky layer (.threshold-pin) that holds the viewport
   while js/threshold.js reads scroll progress and drives the scene.
   Collapsed entirely (display:none) below the same 901px breakpoint
   and under prefers-reduced-motion that gates the Projects proximity
   system, so mobile/reduced-motion visitors go from Closing straight
   to Street exactly as they did before this section existed — no
   second, simpler 3D experience is built for them, per instruction.
   Canvas opacity is set directly by js/threshold.js every frame (no
   CSS transition here) for the same reason --proximity above has
   none: a transition would fight a value already being written every
   animation frame. ============================================================ */
.threshold{
  position: relative;
  height: 110vh; /* reduced 2026-09-10 -- was 220vh, halved to close the dead-scroll gap between the statement and Projects; see Tan's request */
  background: var(--bg);
}
.threshold-pin{
  position: sticky;
  top: 0;
  height: 100vh;
  width: 100%;
  overflow: hidden;
  background: var(--bg);
}
#thresholdCanvas{
  display: block;
  width: 100%;
  height: 100%;
}
@media (max-width: 900px){
  .threshold{ display: none; }
}
@media (prefers-reduced-motion: reduce){
  .threshold{ display: none; }
}

/* ============================================================
   Projects Index v2 — moving photographic wall
   Integrated 2026-09-09 from prototype/projects-index-v2.css,
   verbatim (per "PUSH LOCKED WORK TO GITHUB — PRODUCTION INTEGRATION
   ONLY": the prototype is the source of truth, no redesign). One
   continuous sticky stage; all sizing/transform/opacity/filter math
   is driven per-frame by js/script.js from scroll position — nothing
   below animates on its own. Photographs are matted prints at (near
   enough) their natural aspect ratio, borrowing the same padded-mat +
   shadow language the old .project-photo-frame used, at a size and
   off-centre home position that alternates left/right per chapter.
   ============================================================ */
.wall-projects{
  position: relative;
  background: var(--bg);
}
.wall-stage-wrap{
  position: relative;
  height: 500vh; /* 4 chapters + a half-unit of lead-in/out */
}
.wall-stage{
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  background: var(--bg);
}

/* Soft tracking glow behind the photographs -- interpolated by JS
   between the chapters straddling the current scroll position, so
   the black field reads as a space with light moving through it
   rather than flat webpage background. Pure CSS gradient, no canvas,
   no particles. */
.wall-spotlight{
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  mix-blend-mode: screen;
}

/* Matted print: padded frame + shadow, echoing the site's own
   photograph-as-object language. transform/opacity/filter/z-index
   are all set per-frame by js/script.js from scroll position --
   nothing here animates on its own. Each .wall-photo is a real link
   to its category page (see index.html) with pointer-events toggled
   by script based on presence, so only the photograph currently at
   or near full presence is actually clickable. */
.wall-photo{
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  box-sizing: border-box;
  overflow: hidden;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  padding: clamp(14px, 1.8vw, 30px);
  box-shadow: 0 44px 100px -32px rgba(0,0,0,0.65);
  will-change: transform, opacity, filter;
}
.wall-photo img{
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}

/* Uniform sizing -- 2026-09-09, per Tan: "the street and photo
   retouching cover pages are the same crop size but the rest of the
   two aren't. I want them all aligned and for them to be the same
   crop size please." All four source photographs previously kept
   their own native aspect-ratio inside object-fit:contain, which is
   what produced the mismatch (Street/Retouching at 2:3, Architecture/
   Portraits at 4:5, and none of the four actually sharing the same
   box size either). One shared aspect-ratio and one shared height
   clamp now apply to every chapter, with object-fit:cover (above)
   doing the actual cropping to fill that box -- 4:5 chosen since it's
   already the native ratio of two of the four photographs, so it's
   the gentler crop of the two ratio families rather than forcing
   everything to 2:3. The height clamp is the average of the four
   previous per-chapter clamps, keeping overall photographic presence
   the same as before this change. */
.wall-photo[data-i]{ height: clamp(460px, 70vh, 780px); aspect-ratio: 4 / 5; }

@media (max-width: 900px){
  .wall-photo[data-i]{ height: clamp(355px, 55vh, 565px); }
}

/* ---------------- Exhibition label ----------------
   Restrained wall-plaque reading order: number -> title -> a short
   mood pair ("fleeting / oblique"), kept small and subordinate to the
   photograph. A faint scrim behind it keeps it legible regardless of
   which asymmetric photo position happens to sit nearby. */
.wall-label{
  position: absolute;
  left: var(--page-pad);
  bottom: clamp(2rem, 5.6vw, 3.1rem);
  z-index: 500;
  max-width: 26ch;
  padding: 0.6rem 0.85rem 0.7rem;
  background: rgba(10,9,8,0.38);
  backdrop-filter: blur(6px);
  pointer-events: none;
}
.wall-label-num{
  display: block;
  margin-bottom: 0.6rem;
  font-family: 'Inter', sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.22em;
  color: var(--text-faint);
}
.wall-label-title{
  font-family: 'Fraunces', serif;
  font-weight: 500;
  text-transform: uppercase;
  font-size: clamp(1.3rem, 2.5vw, 1.9rem);
  line-height: 1.02;
  letter-spacing: 0.01em;
  color: var(--text);
  margin: 0 0 0.5rem;
}
.wall-label-mood{
  font-family: 'Fraunces', serif;
  font-style: italic;
  font-weight: 400;
  font-size: 0.8rem;
  letter-spacing: 0.03em;
  color: var(--text-dim);
}

/* ---------------- Progress ---------------- */
.wall-progress{
  position: absolute;
  right: var(--page-pad);
  bottom: clamp(2rem, 5.6vw, 3.1rem);
  z-index: 500;
  font-family: 'Fraunces', serif;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  color: var(--text-faint);
  pointer-events: none;
}
.wall-progress-current{ color: var(--accent); }

@media (prefers-reduced-motion: reduce){
  .wall-photo{ transition: none; }
}

/* ============================================================
   Superseded 2026-09-09 by Projects Index v2 (.wall-projects) above.
   Nothing in index.html references .projects-index/.project-threshold/
   .project-photo-mat/.project-photo-frame or --proximity anymore, so
   everything below this point is inert (and the matching JS block in
   js/script.js no-ops for the same reason: its querySelectorAll calls
   now find zero elements). Left in place rather than removed — outside
   the scope of this production-integration checkpoint; safe for Tan to
   delete later if she wants the file shorter.
   ============================================================ */
/* ============================================================
   Projects Index — four quiet photographic thresholds
   Added 2026-09-07. Continuous scroll, no snap. Only .project-
   photo-mat is a link; .project-meta is plain text beside it,
   never inside the link. Photographs are never cropped — no
   object-fit:cover anywhere here — each keeps its natural aspect
   ratio inside a matted frame, closer to a printed photograph than
   a UI thumbnail. --proximity is a plain CSS custom property
   written by js/script.js on scroll (see the "Projects index:
   photograph proximity" block there) — deliberately no CSS
   transition on the properties that read it, since it's updated
   every animation frame already and a transition would just fight
   that; it defaults to 1 here so the photographs are fully visible
   even if that script never runs.
   ============================================================ */
.projects-index{
  padding: clamp(3rem, 6vw, 5rem) 0 clamp(7rem, 13vw, 10rem);
}
.project-threshold{
  display: flex;
  padding: clamp(2.25rem, 4.5vw, 3.25rem) var(--page-pad);
}
.project-threshold--street{ justify-content: flex-start; }
.project-threshold--architecture{ justify-content: flex-end; }
.project-threshold--portraits{ justify-content: flex-start; }
.project-threshold--retouching{ justify-content: flex-end; }

/* Distinct width (scale) and vertical offset per chapter, so the
   four thresholds never read as identical templates repeating down
   the page — Street and Portraits both sit left but at different
   sizes/heights, Architecture and Retouching both sit right at
   different sizes/heights again. */
.project-threshold--street .project-photo-mat{
  width: clamp(240px, 38vw, 480px);
}
.project-threshold--architecture .project-photo-mat{
  width: clamp(200px, 30vw, 400px);
  margin-top: clamp(2rem, 6vw, 5rem);
}
.project-threshold--portraits .project-photo-mat{
  width: clamp(220px, 34vw, 440px);
  margin-top: clamp(1rem, 3vw, 2.5rem);
}
.project-threshold--retouching .project-photo-mat{
  width: clamp(260px, 40vw, 520px);
}

.project-photo-mat{
  display: block;
  flex: 0 0 auto;
}
.project-photo-frame{
  display: block;
  overflow: hidden;
  background: var(--bg-elev);
  padding: clamp(20px, 3.4vw, 52px);
  box-shadow: 0 40px 90px -30px rgba(0,0,0,0.6);
  opacity: calc(0.35 + var(--proximity, 1) * 0.65);
  transform: translateY(calc((1 - var(--proximity, 1)) * 36px)) scale(calc(0.965 + var(--proximity, 1) * 0.035));
  filter: saturate(calc(0.85 + var(--proximity, 1) * 0.15)) contrast(calc(0.94 + var(--proximity, 1) * 0.06));
}

/* ============================================================
   Chapter dialects — Street / Architecture / Portraits
   Coherence pass, 2026-09-07. Superseding the earlier blur-only
   Street test (which Tan approved only as a proof of the underlying
   grammar, not as the site's answer). All three restate the shared
   .project-photo-frame opacity/filter formula above untouched --
   choreography, timing and every other chapter are unaffected -- and
   add exactly one further property each, per chapter, driven by the
   same --proximity value everything else already reads. Retouching
   keeps the shared rule alone; its own dialect is the whole of
   Beauty Archive and isn't expressed through this frame at all.
   ============================================================ */

/* Street — a fleeting encounter. The photograph arrives very
   slightly off its resting position, as if caught mid-motion, and
   settles exactly into place as it reaches full presence: position,
   not a filter, carries the "caught it" feeling. */
.project-threshold--street .project-photo-frame{
  transform:
    translateY(calc((1 - var(--proximity, 1)) * 36px))
    scale(calc(0.965 + var(--proximity, 1) * 0.035))
    translateX(calc((1 - var(--proximity, 1)) * -16px));
}

/* Architecture — distance, scale, structure. The photograph begins
   very slightly larger than its resting size and settles down to it
   (the inverse of the shared scale-up), reading as "stepping back
   until the whole structure resolves" rather than a zoom-in. */
.project-threshold--architecture .project-photo-frame{
  transform:
    translateY(calc((1 - var(--proximity, 1)) * 36px))
    scale(calc(1.045 - var(--proximity, 1) * 0.045));
}

/* Portraits — attention rewarded, not movement. The photograph
   itself is untouched (no filter, no scale, no translation beyond
   the shared rule); js/script.js adds .portraits-attended to this
   frame's meta text after a sustained dwell at full presence, and
   only the description quietly deepens in response -- see the
   "Projects index: portrait attention reward" block in script.js. */
.project-threshold--portraits .project-desc{
  transition: color 0.6s cubic-bezier(0.16,1,0.3,1), letter-spacing 0.6s cubic-bezier(0.16,1,0.3,1);
}
.project-threshold--portraits .project-meta.portraits-attended .project-desc{
  color: var(--text);
  letter-spacing: 0.005em;
}

.project-photo{
  display: block;
  width: 100%;
  height: auto;
}
@media (hover: hover){
  .project-photo-mat:hover .project-photo-frame{
    transform: translateY(calc((1 - var(--proximity, 1)) * 36px)) scale(calc(0.965 + var(--proximity, 1) * 0.035 + 0.025));
    transition: transform 0.4s cubic-bezier(0.16,1,0.3,1);
  }
}

.project-meta{
  margin-top: clamp(1.1rem, 2.4vw, 1.6rem);
  max-width: 30ch;
}
.project-eyebrow-num{
  display: block;
  font-family: 'Inter', sans-serif;
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  color: var(--text-faint);
}
.project-name{
  font-family: 'Fraunces', serif;
  font-weight: 400;
  font-size: clamp(1.7rem, 3vw, 2.3rem);
  margin-top: 0.5rem;
  color: var(--text);
}
.project-desc{
  font-family: 'Fraunces', serif;
  font-style: italic;
  font-weight: 400;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--text-dim);
  margin-top: 0.6rem;
}

@media (max-width: 900px){
  /* Desktop-only proximity JS stops running below this width (see
     the matchMedia gate in js/script.js, matching the site's other
     scroll-driven effects) — photographs sit at full strength via
     the --proximity default and rely on the shared .reveal fade
     instead, same mobile fallback the rest of the site already
     uses. */
  .project-threshold,
  .project-threshold--street,
  .project-threshold--architecture,
  .project-threshold--portraits,
  .project-threshold--retouching{
    justify-content: center;
  }
  .project-threshold--street .project-photo-mat,
  .project-threshold--architecture .project-photo-mat,
  .project-threshold--portraits .project-photo-mat,
  .project-threshold--retouching .project-photo-mat{
    width: min(70vw, 420px);
    margin-top: 0;
  }
  .project-meta{ max-width: 42ch; }
}
@media (max-width: 560px){
  .project-threshold--street .project-photo-mat,
  .project-threshold--architecture .project-photo-mat,
  .project-threshold--portraits .project-photo-mat,
  .project-threshold--retouching .project-photo-mat{
    width: min(84vw, 360px);
  }
}

/* ============================================================
   Held Print settle-in — shared photograph-presence primitive
   Coherence pass, 2026-09-07. A deliberately different arrival than
   .reveal's translateY fade, which suits text: a photograph doesn't
   slide up into place like a UI card, it settles into focus -- a
   slow opacity/scale-down from just outside 1, on the same .pre/.in
   classes the existing IntersectionObserver already toggles, so no
   new JS is needed, just a slower, heavier transition than text
   gets. Used on About's portrait and Contact's environment
   photograph below. Home, Projects and Beauty Archive already have
   their own, more fully-worked photographic behaviour and are
   untouched by this. */
.photo-presence{
  transition: opacity 1.2s cubic-bezier(0.16,1,0.3,1), transform 1.2s cubic-bezier(0.16,1,0.3,1);
}
.photo-presence.pre{ opacity: 0; transform: scale(1.045); }
.photo-presence.in{ opacity: 1; transform: scale(1); }

/* ============================================================
   About — editorial sequence
   Coherence pass, 2026-09-07. Replaces the previous two-column bio
   card + flex-wrapped award grid with a sequence of held beats,
   matching the one-thing-at-a-time rhythm used everywhere else on
   the site. See index.html for the content mapping (nothing
   invented; the same portrait and the same two bio sentences,
   restructured). The old .intro-split / .recognition-grid rules this
   replaced have been removed (coherence pass, 2026-09-08) — confirmed
   unreferenced by any live markup, including the standalone legacy
   pages, before deleting.
   ============================================================ */
.about-presence{
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  align-items: center;
  gap: clamp(2rem, 5vw, 5rem);
  padding: clamp(6rem, 12vw, 9rem) var(--page-pad) clamp(3rem, 6vw, 4.5rem);
  max-width: 1360px;
  margin: 0 auto;
}
.about-presence-media{
  overflow: hidden;
  aspect-ratio: 4 / 5;
  background: var(--bg-elev);
}
.about-presence-media img{ width: 100%; height: 100%; object-fit: cover; }
.about-presence-caption h1{
  font-size: clamp(2.6rem, 5.5vw, 4.4rem);
  margin-top: 0.6rem;
}
.about-presence-location{
  margin-top: 1rem;
  font-family: 'Inter', sans-serif;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}
@media (max-width: 800px){
  .about-presence{ grid-template-columns: 1fr; }
  .about-presence-media{ aspect-ratio: 5 / 4; }
}

.about-statement-wrap{ padding: clamp(1rem, 3vw, 2rem) var(--page-pad) clamp(3rem, 6vw, 4.5rem); }
.about-statement{
  font-family: 'Fraunces', serif;
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.5rem, 3.1vw, 2.3rem);
  line-height: 1.35;
  max-width: 34ch;
  margin: 0 auto;
  text-align: center;
  color: var(--text);
}

.about-practice-wrap{
  /* No border-bottom here (coherence pass, 2026-09-08) — the recognition
     section right after it already announces itself with its own
     background shift (.recognition uses --bg-elev); a line here on top
     of that read as two seams for one handoff. */
  padding: 0 var(--page-pad) clamp(4rem, 8vw, 6rem);
}
.about-practice{
  font-family: 'Fraunces', serif;
  font-style: italic;
  font-weight: 400;
  font-size: 1.05rem;
  line-height: 1.6;
  max-width: 46ch;
  margin: 0 auto;
  text-align: center;
  color: var(--text-dim);
}

.about-recognition-eyebrow{ margin-bottom: 0; }
.recognition-index{
  list-style: none;
  max-width: 720px;
  margin: clamp(2.4rem, 5vw, 3.4rem) auto 0;
  display: flex;
  flex-direction: column;
}
.recognition-row{
  display: grid;
  grid-template-columns: 2.2rem 1fr auto;
  align-items: baseline;
  gap: clamp(0.75rem, 3vw, 2rem);
  padding: clamp(1.05rem, 2.2vw, 1.4rem) 0;
  border-bottom: 1px solid var(--border);
}
.recognition-row:first-child{ border-top: 1px solid var(--border); }
.recognition-num{
  font-family: 'Inter', sans-serif;
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--text-faint);
}
.recognition-row-title{
  font-family: 'Fraunces', serif;
  font-weight: 400;
  font-size: 1.05rem;
  color: var(--text);
}
.recognition-row-sub{
  font-family: 'Inter', sans-serif;
  font-size: 0.72rem;
  letter-spacing: 0.03em;
  color: var(--text-faint);
  text-align: right;
}
@media (max-width: 620px){
  .recognition-row{ grid-template-columns: 1.6rem 1fr; }
  .recognition-row-sub{ grid-column: 2 / 3; text-align: left; margin-top: 0.3rem; }
}

.about-closing-wrap{ padding: clamp(3.5rem, 7vw, 5.5rem) 0 clamp(5rem, 10vw, 7rem); }
.about-closing{ text-align: center; }

/* ============================================================
   Contact — the environment stays part of the same world
   Coherence pass, 2026-09-07. Only the photograph is touched (see
   .photo-presence above, applied to .contact-hero-bg in index.html,
   which already has overflow:hidden on its .contact-hero parent so
   the settle-in's slight scale never clips visibly) -- the form, its
   fields, and its labels are exactly as they were. */
