/* ──────────────────────────────────────────────────────────────────
   Po-Hsu Chen, Portfolio
   Vanilla HTML / CSS / JS. Edit content in script.js.
   ────────────────────────────────────────────────────────────────── */

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

/* ──────────────────────────────────────────────────────────────────
   Self-hosted fonts (woff2, latin subset). Replaces Google Fonts so no
   visitor IP is sent to a third-party CDN and the CSP can stay 'self'.
   Big Shoulders Display, Inter Tight and Fraunces are variable fonts;
   the weight ranges let one file cover every weight in use.
   ────────────────────────────────────────────────────────────────── */
@font-face {
  font-family: 'Big Shoulders Display';
  font-style: normal;
  font-weight: 500 900;
  font-display: swap;
  src: url('fonts/big-shoulders-display.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('fonts/inter-tight.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter Tight';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/inter-tight-italic.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/jetbrains-mono.woff2') format('woff2');
}
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 400 500;
  font-display: swap;
  src: url('fonts/fraunces-italic.woff2') format('woff2');
}

:root {
  --bg:        #e8dfc9;
  --bg-alt:    #ddd2b8;
  --ink:       #110d09;
  --ink-soft:  #5c5446;
  --rule:      #b6a994;
  --accent:    #c8431b;

  /* Background shape colours - red, rose, gray (darker / stronger still) */
  --shape-red:  rgba(84, 22, 14, 0.92);    /* very deep brick red */
  --shape-rose: rgba(76, 46, 40, 0.88);    /* very deep dusty rose */
  --shape-gray: rgba(50, 42, 34, 0.92);    /* very deep warm gray */

  --dark:      #110d09;
  --dark-mid:  rgba(232, 223, 201, 0.55);
  --dark-rule: rgba(232, 223, 201, 0.16);

  --display: 'Big Shoulders Display', 'Helvetica Neue', Arial, sans-serif;
  --body:    'Inter Tight', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --mono:    'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --serif:   'Fraunces', Georgia, serif;

  --gutter:  clamp(20px, 4vw, 56px);
  --col-gap: clamp(16px, 2vw, 32px);

  --ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* Frosted glass tints */
  --glass-light: rgba(232, 223, 201, 0.55);
  --glass-dark:  rgba(17, 13, 9, 0.74);
  --glass-blur:  blur(70px) saturate(1.3);
}

html {
  font-size: 16px;
  scroll-behavior: smooth;
  background: var(--bg);
  /* No perspective on html, that turned html into the containing
     block for fixed descendants and broke the cursor's tracking.
     Perspective is applied per-section below. */
  /* (Desktop browsers correctly propagate <body>'s overflow-x: hidden
     to the viewport; the html-level horizontal clip + touch-action
     lock is only needed on mobile, see the @media block at the bottom
     of this section.) */
}

/* Scroll lock, applied while loader curtain (or lightbox) is on screen.
   Note: do NOT set `height: 100%` here, that collapses the document to
   viewport height and the browser clamps scrollY back to 0, which jumps
   the page to the top when the lock is removed. Just disabling overflow
   is enough; scroll position is preserved. */
html.is-locked,
html.is-locked body {
  overflow: hidden;
  touch-action: none;
}

html {
  /* Stop iOS Safari from inflating font-size on landscape rotation. */
  -webkit-text-size-adjust: 100%;
          text-size-adjust: 100%;
  /* Suppress the blue / grey tap-highlight on iOS so the magnetic +
     custom-cursor look isn't undermined by a system rectangle. */
  -webkit-tap-highlight-color: transparent;
  /* Block iOS Safari's elastic rubber-band scroll at the top and
     bottom of the page. `none` blocks the bounce outright on
     iOS 16+; the `contain` fallback also blocks it on older iOS
     while permitting the parent (nothing, since html is root) to
     scroll, which is moot here. */
  overscroll-behavior: none;
  overscroll-behavior-y: none;
}

body {
  background: transparent;
  color: var(--ink);
  font-family: var(--body);
  font-size: 15px;
  font-weight: 400;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-feature-settings: 'kern', 'liga';
  max-width: 1760px;
  margin: 0 auto;
  /* On desktop browsers, overflow-x: hidden on <body> reliably
     propagates to the viewport, so the .marquee 150% bleed (applied
     above 900px) gets clipped at the page edges with no horizontal
     scroll. iOS Safari's propagation is unreliable - the mobile-only
     html clip below picks up the slack. */
  overflow-x: hidden;
  overscroll-behavior: none;
  position: relative;
}

/* ──────────────────────────────────────────────────────────────────
   Mobile-only horizontal-overflow lock
   ──────────────────────────────────────────────────────────────────
   On iOS Safari (and in some Android Chrome cases), <body>'s
   `overflow-x: hidden` does NOT propagate up to the viewport when
   <body> has `max-width + margin: auto`. The document ends up
   horizontally scrollable, and the user can drag the page to the
   right to reveal a blank strip - whatever sits past the viewport,
   typically the .marquee strip when its desktop 150% bleed leaks
   over (the bleed itself is already scoped to ≥900px in the
   .marquee rule, so it should not apply here, but the html clip is
   the belt-and-braces for any future overflow regardless).

   The fix is two declarations on <html>, scoped to mobile widths so
   it does not interfere with the desktop layout:
     • overflow-x: clip - clips horizontal overflow at the html box
       without creating a scroll container (modern browsers).
       overflow-x: hidden is declared first as the fallback for
       iOS Safari < 16, which does not yet understand `clip`.
     • touch-action: pan-y pinch-zoom - refuses horizontal pan
       gestures outright on iOS, pinch-zoom is preserved so
       accessibility zoom still works.
*/
@media (max-width: 899px) {
  /* iOS Safari quirk: 100vw measures the LAYOUT viewport, which can
     briefly exceed the visual viewport during URL-bar show/hide and
     during pinch-zoom snapback. That mismatch is the documented cause
     of the "drag the page sideways after the loader lifts" bug on
     real iPhones (it never reproduces in DevTools' mobile emulation
     because Chrome's emulator doesn't run the iOS visual/layout
     viewport split). The fix is to chain 100% through html → body
     → main → marquee instead: each element inherits its width from
     its parent, the chain terminates at <html> whose containing
     block IS the visual viewport, and the whole tree contracts and
     expands together with iOS's address-bar transitions.

     position: relative on every link in the chain forces each
     element to establish a containing block, which on iOS Safari
     stops fixed-positioned descendants (the loader curtain mid-
     slide, the .bg shapes layer) from leaking phantom horizontal
     overflow into the document. */
  html {
    width: 100% !important;
    max-width: 100% !important;
    overflow-x: hidden !important;
    overflow-x: clip !important;
    position: relative !important;
    /* Forbid horizontal pan gestures outright; pinch-zoom preserved
       for accessibility. */
    touch-action: pan-y pinch-zoom;
  }
  body {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 !important;
    overflow-x: hidden !important;
    overflow-x: clip !important;
    position: relative !important;
  }
  /* Also lock <main>, which is the marquee's actual parent and the
     most likely path for a wide descendant to inflate the document. */
  main {
    width: 100% !important;
    max-width: 100% !important;
    overflow-x: hidden !important;
    overflow-x: clip !important;
    position: relative !important;
  }
}

a       { color: inherit; text-decoration: none; }
img     { display: block; max-width: 100%; }
button  { background: none; border: none; cursor: inherit; font: inherit; color: inherit; }

::selection { background: var(--ink); color: var(--bg); }

/* ──────────────────────────────────────────────────────────────────
   Focus indicators, keyboard users only. Mouse users never see the
   ring (browsers' :focus-visible heuristic suppresses it after a
   click). Inverted ring tone inside the lightbox so the dark backdrop
   doesn't swallow it.
   ────────────────────────────────────────────────────────────────── */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}
.lightbox__backdrop:focus-visible,
.lightbox__media:focus-visible,
.lightbox__play:focus-visible,
.lightbox__content:focus-visible {
  outline-color: rgba(255, 255, 255, 0.92);
}

/* Skip-to-content link, visually hidden until focused, then jumps in
   from the top-left so a keyboard user can bypass the masthead and
   land directly on <main>. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: 12px 16px;
  background: var(--ink);
  color: var(--bg);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  z-index: 10000;
}
.skip-link:focus { left: 16px; top: 16px; }

[data-magnetic] {
  /* No always-on will-change: the inline translate3d/perspective transform
     already promotes the layer while an element is actively pulled, and it's
     cleared on idle. Pinning it here meant ~100 permanent GPU layers, which
     hurt memory on low-RAM phones for no benefit. */
  transform-style: preserve-3d;
}

.caption {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.caption--light { color: var(--dark-mid); }
.muted { color: var(--ink-soft); }

/* ──────────────────────────────────────────────────────────────────
   Background, geometric shapes only (frosted glass is per-section)
   ────────────────────────────────────────────────────────────────── */

.bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
}

.bg__shapes {
  position: absolute;
  /* svh instead of vh so the shape layer doesn't grow/shrink as
     iOS Safari hides the URL bar during scroll (would shift the
     visible shape silhouettes inside the .bg overflow clip). */
  inset: -25svh -20vw;
}

.bg__layer {
  position: absolute;
  left: var(--x, 0%);
  top:  var(--y, 0vh);
  width:  var(--size, 320px);
  height: var(--size, 320px);
  will-change: transform;
}

.bg__shape {
  width: 100%;
  height: 100%;
}

.bg__shape--circle { border-radius: 50%; }
.bg__shape--square { border-radius: 0; }

.bg__shape--red  { background: var(--shape-red); }
.bg__shape--rose { background: var(--shape-rose); }
.bg__shape--gray { background: var(--shape-gray); }

/* Global frosted glass overlay, slightly more transparent, heavy blur so
   shapes still read as soft colour washes (no recognisable silhouettes). */
.bg__glass {
  position: absolute;
  inset: 0;
  /* Fallback for browsers without backdrop-filter (older Firefox/Safari):
     a near-opaque cream wash so the shapes still read as soft colour and
     never as hard silhouettes. Overridden below where the filter works. */
  background: rgba(232, 223, 201, 0.86);
}
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .bg__glass {
    background: rgba(232, 223, 201, 0.48);
    /* 120px (was 180px), still fully dissolves the 260-340px shapes into
       washes, but ~a third cheaper to repaint while parallax scrolls. */
    -webkit-backdrop-filter: blur(120px) saturate(1.6);
    backdrop-filter: blur(120px) saturate(1.6);
  }
}

/* The .bg layer is at z-index: -1, so all in-flow content paints on top. */
.cursor, .loader { z-index: 9000; }

/* ──────────────────────────────────────────────────────────────────
   Cursor
   ────────────────────────────────────────────────────────────────── */

.cursor {
  position: fixed;
  top: 0; left: 0;
  pointer-events: none;
  z-index: 9999;
  mix-blend-mode: difference;
  opacity: 0;
  transition: opacity 500ms ease;
}
.cursor__dot {
  position: fixed;
  width: 6px; height: 6px;
  background: #fff;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  will-change: transform;
}
.cursor__ring {
  position: fixed;
  width: 32px; height: 32px;
  border: 1px solid #fff;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  will-change: transform;
  transition: width 350ms var(--ease), height 350ms var(--ease),
              opacity 350ms var(--ease);
  opacity: 0.6;
}

@media (hover: hover) and (pointer: fine) {
  html.cursor-on body,
  html.cursor-on a,
  html.cursor-on button { cursor: none; }
  html.cursor-on.is-loaded .cursor { opacity: 1; }
}

html.cursor-hover-link  .cursor__ring { width: 56px; height: 56px; opacity: 0.85; }
html.cursor-hover-media .cursor__ring { width: 80px; height: 80px; opacity: 0.5; }

/* ──────────────────────────────────────────────────────────────────
   Loader, page-work has light bg / dark name; page-about inverted
   ────────────────────────────────────────────────────────────────── */

.loader {
  position: fixed;
  inset: 0;
  background: var(--bg);
  z-index: 9000;
  display: grid;
  place-items: center;
  transition: transform 1100ms var(--ease);
}
.loader__name {
  font-family: var(--display);
  font-size: clamp(40px, 7vw, 88px);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  color: var(--ink);
  opacity: 0;
  animation: loader-name 700ms ease 100ms forwards;
}
@keyframes loader-name {
  to { opacity: 1; }
}
html.is-loaded .loader {
  transform: translateY(-101%);
  /* Don't intercept clicks/scrolls while sliding up */
  pointer-events: none;
}

.page-about .loader            { background: var(--dark); }
.page-about .loader__name      { color: var(--bg); }

/* ──────────────────────────────────────────────────────────────────
   Word-mask reveal (data-split)
   ────────────────────────────────────────────────────────────────── */

.word {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  line-height: inherit;
}
.word__inner {
  display: inline-block;
  transform: translateY(110%);
}
.word__inner.is-revealed {
  animation: word-rise 1100ms var(--ease) forwards;
  animation-delay: calc(var(--i, 0) * 28ms);
}
@keyframes word-rise {
  to { transform: translateY(0); }
}

/* ──────────────────────────────────────────────────────────────────
   Masthead, frosted glass on every page
   ────────────────────────────────────────────────────────────────── */

.masthead {
  padding: 22px var(--gutter);
  background: transparent;
}
.masthead--dark {
  background: var(--ink);
  color: var(--bg);
}

.masthead__row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
}

.masthead__name {
  font-family: var(--display);
  font-size: 16px;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  display: inline-block;
}

.masthead__about {
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  position: relative;
  display: inline-block;
  padding: 6px 14px;
  border: 1px solid currentColor;
  border-radius: 999px;
  transition: background-color 240ms var(--ease), color 240ms var(--ease);
}
.masthead__about:hover {
  background: var(--ink);
  color: var(--bg);
}
.masthead--dark .masthead__about:hover {
  background: var(--bg);
  color: var(--ink);
}

/* ──────────────────────────────────────────────────────────────────
   Hero (light frosted glass)
   ────────────────────────────────────────────────────────────────── */

.hero {
  display: flex;
  flex-direction: column;
  /* iOS Safari has three viewport-height units:
       100vh  - the maximum possible viewport, INCLUDES the area
                behind the auto-hidden URL bar (causes overflow
                on first paint)
       100dvh - DYNAMIC, tracks the visible area but changes as
                the URL bar shows/hides during scroll, so the
                hero re-sizes mid-scroll and pushes the page
                below it up and down (the "elements move when
                I scroll" bug)
       100svh - SMALL, locked to the smaller-of-two-states
                (URL bar visible). The hero never grows once
                the URL bar disappears, so the layout below
                stays put. Safari 15.4+, Chrome 108+. */
  min-height: calc(100vh - 70px);
  min-height: calc(100svh - 70px);
  padding: clamp(24px, 4svh, 56px) var(--gutter) clamp(28px, 4svh, 48px);
  text-align: center;
  align-items: center;
  background: transparent;
}

.hero__caption {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.hero__center {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  padding: clamp(8px, 2svh, 20px) 0;
}

.hero__title {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(60px, 14.5vw, 220px);
  line-height: 0.86;
  letter-spacing: -0.04em;
  text-transform: uppercase;
  display: flex;
  flex-direction: column;
  gap: 0;
  margin-bottom: clamp(20px, 3svh, 36px);
}
.hero__title-line {
  display: block;
  overflow: hidden;
  line-height: 0.92;
}
.hero__title-inner {
  display: inline-block;
  transform: translateY(102%);
  will-change: transform;
}
html.is-loaded .hero__title-inner {
  animation: title-rise 1300ms var(--ease) forwards;
}
.hero__title-line:nth-child(1) .hero__title-inner { animation-delay: 200ms; }
.hero__title-line:nth-child(2) .hero__title-inner { animation-delay: 320ms; }
@keyframes title-rise {
  to { transform: translateY(0); }
}

.hero__sub {
  font-family: var(--body);
  font-size: clamp(14px, 1.05vw, 16px);
  font-weight: 400;
  line-height: 1.55;
  max-width: 520px;
  color: var(--ink);
}
.hero__sub em {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
}

/* ──────────────────────────────────────────────────────────────────
   Picker, uniform 5-column grid, no number header
   ────────────────────────────────────────────────────────────────── */

.picker {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(8px, 1vw, 18px);
  width: 100%;
  max-width: 1200px;
  margin-top: clamp(28px, 4svh, 56px);
}

.picker__item {
  display: flex;
  flex-direction: column;
  text-align: center;
  position: relative;
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  opacity: 0;
}
.picker__item.is-in {
  animation: picker-in 1100ms var(--ease) forwards;
  animation-delay: calc(700ms + 90ms * var(--i, 0));
}
@keyframes picker-in {
  to { opacity: 1; }
}

.picker__media {
  width: 100%;
  aspect-ratio: 4/5;
  overflow: hidden;
  background: var(--bg-alt);
  position: relative;
}
.picker__media::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(17, 13, 9, 0.06);
  z-index: 2;
}

.picker__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 800ms ease;
}
.picker__media img.is-loaded { opacity: 1; }
.picker__media img { position: relative; z-index: 1; }

.picker__foot {
  margin-top: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.picker__name {
  font-family: var(--display);
  font-size: clamp(16px, 1.6vw, 22px);
  font-weight: 800;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--ink);
  position: relative;
  padding-bottom: 6px;
}
.picker__name::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%) scaleX(0);
  width: 28px;
  height: 1.5px;
  background: var(--ink);
  transition: transform 600ms var(--ease);
}
.picker__item.is-active .picker__name::after {
  transform: translateX(-50%) scaleX(1);
}

.picker__count {
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ink-soft);
}

@media (max-width: 640px) {
  .picker { grid-template-columns: repeat(2, 1fr); gap: 14px; }
}

/* ──────────────────────────────────────────────────────────────────
   Marquee
   ────────────────────────────────────────────────────────────────── */

.marquee {
  position: relative;
  /* Default to the natural body width on small screens. On wider screens
     we bleed the strip past the body's content gutters (see media query
     below). The 150% / -25% trick was leaking ~25vw off the right edge
     on mobile and was the cause of the "blank strip on the right" that
     let users drag the page sideways. */
  width: 100%;
  left: 0;
  border-top: 1.5px solid var(--ink);
  border-bottom: 1.5px solid var(--ink);
  overflow: hidden;
  white-space: nowrap;
  font-family: var(--display);
  font-size: clamp(14px, 1.4vw, 20px);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 800;
  color: var(--ink);
  padding: 18px 0;
  background: transparent;
}

/* Mobile: pin the strip to exact screen width using viewport units
   so it can never inherit any parent constraint (gutter, max-width,
   etc.). 100vw == screen width on mobile (no vertical scrollbar to
   inflate it), so the strip lines up edge-to-edge with the device.
   We stack three independent clipping mechanisms so at least one
   always wins on every mobile browser:
     • overflow: hidden, the historical baseline
     • contain: paint, GPU clip, beats inline-flex leaks on Safari
     • clip-path: inset(0), vector clip honoured by every modern
       engine, including iOS Safari versions where `contain` is buggy
   `isolation: isolate` forces the marquee into its own stacking
   context so the magnetic transform on its parent can never lift
   the contents above sibling clipping ancestors. */
@media (max-width: 899px) {
  .marquee {
    /* width: 100% chains off the locked-to-viewport <main> above,
       which is the iOS-Safari-safe equivalent of 100vw (see the
       html/body/main lock block at the top of this file). */
    width: 100% !important;
    max-width: 100% !important;
    margin-left: 0 !important;
    left: 0 !important;
    right: auto !important;
    transform: none !important;
    position: relative !important;
    contain: paint;
    clip-path: inset(0);
    isolation: isolate;
  }
  /* The track is inline-flex and naturally wider than the marquee
     - belt-and-braces, pin its position so any future layout shift
     can't make it report a wider intrinsic box to its parent. */
  .marquee__track {
    max-width: none;
    will-change: transform;
  }
}

/* Restore the wider-than-content bleed on desktop, where the body has
   real horizontal padding so 150% still fits inside the viewport. */
@media (min-width: 900px) {
  .marquee {
    width: 150%;
    left: -25%;
  }
}

.marquee__track {
  display: inline-flex;
  align-items: center;
  animation: marquee 50s linear infinite;
}

.marquee__set {
  display: inline-flex;
  align-items: center;
  gap: 36px;
  padding-right: 36px;
  flex-shrink: 0;
}

.marquee__item { display: inline-block; }
.marquee__sep  { font-size: 10px; color: var(--accent); }

.marquee:hover .marquee__track { animation-play-state: paused; }

@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* ──────────────────────────────────────────────────────────────────
   Works (light frosted glass)
   ────────────────────────────────────────────────────────────────── */

.works {
  padding: clamp(80px, 12vw, 144px) var(--gutter) clamp(80px, 12vw, 160px);
  background: transparent;
}

.works__head {
  text-align: center;
  margin-bottom: clamp(64px, 9vw, 112px);
}

.works__caption { margin-bottom: 16px; }

.works__title {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(48px, 9vw, 144px);
  line-height: 0.9;
  letter-spacing: -0.035em;
  text-transform: uppercase;
  margin-bottom: 18px;
  display: inline-block;
}

.works__meta {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.works__grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: var(--col-gap);
  row-gap: clamp(56px, 8vw, 96px);
  transition: opacity 320ms ease;
  min-height: 50vh;   /* fallback for browsers without small-viewport units */
  min-height: 50svh;
}
.works__grid.is-fading { opacity: 0; }

.work {
  grid-column: var(--col, 1 / span 12);
  margin-top: var(--offset, 0px);
  min-width: 0;
  opacity: 0;
  transition: opacity 1100ms var(--ease);
}
.work.is-revealed { opacity: 1; }

.work__media {
  aspect-ratio: var(--aspect, 4/5);
  background: var(--bg-alt);
  overflow: hidden;
  position: relative;
}
.work__media::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(17, 13, 9, 0.06);
  z-index: 2;
}

.work__media img {
  width: 100%;
  height: 100%;
  /* contain so the whole image is always visible, no top/bottom or
     left/right crop. The work__media background colour fills any
     remaining space if the aspect doesn't match exactly. */
  object-fit: contain;
  opacity: 0;
  transition: opacity 900ms ease;
}
.work__media img.is-loaded { opacity: 1; }
.work__media img { position: relative; z-index: 1; }

/* Fallback for browsers without `aspect-ratio` (Safari < 15): give the media
   slots a height floor so they never collapse to 0 before the image loads.
   No effect on modern browsers, which honour the aspect-ratio above. */
@supports not (aspect-ratio: 1) {
  .picker__media { min-height: 280px; }
  .work__media   { min-height: 240px; }
}

/* The loading slot is just the warm cream tone (--bg-alt) the cards sit
   on, with the inset frame border on ::after for definition. Image
   fades in over 900 ms via the existing opacity transition once it
   loads. The .is-ready class still lands on the parent for aria-busy
   management; it just doesn't trigger any visual placeholder removal
   anymore. */

/* Play-button overlay for video works (work__media--video).
   Sits centred over the thumbnail; pointer-events: none so the parent
   .work__media still receives the click. */
.work__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  transition: transform 320ms var(--ease), opacity 320ms ease;
  opacity: 0.92;
}
.work__play svg {
  width: clamp(48px, 6vw, 72px);
  height: clamp(48px, 6vw, 72px);
  filter: drop-shadow(0 4px 14px rgba(0, 0, 0, 0.35));
}
.work__play circle {
  fill: rgba(0, 0, 0, 0.55);
  stroke: rgba(255, 255, 255, 0.92);
  stroke-width: 1.5;
}
.work__play polygon {
  fill: rgba(255, 255, 255, 0.96);
}
.work__media--video:hover .work__play,
.work__media--link:hover  .work__play {
  transform: scale(1.06);
  opacity: 1;
}
/* Link variant, same circle background, arrow icon instead of triangle */
.work__play--link polygon { display: none; }
.work__play--link path {
  fill: none;
  stroke: rgba(255, 255, 255, 0.96);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.work__caption {
  margin-top: 18px;
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: 24px;
  row-gap: 4px;
  align-items: baseline;
}

.work__num {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.16em;
  color: var(--ink-soft);
  grid-column: 1 / -1;
  text-transform: uppercase;
}

.work__title {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: 17px;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.work__meta {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-soft);
  white-space: nowrap;
}

@media (max-width: 760px) {
  .work { grid-column: 1 / span 12 !important; margin-top: 0 !important; }
  .works__grid { row-gap: 56px; }
}

/* ──────────────────────────────────────────────────────────────────
   About, dark frosted glass
   ────────────────────────────────────────────────────────────────── */

.about {
  background: var(--ink);
  color: var(--bg);
  padding: clamp(96px, 14vw, 180px) var(--gutter) clamp(80px, 12vw, 160px);
  text-align: center;
}

/* Hero variant on the about page, sits tight to the masthead with
   matching top/bottom padding, mirroring the work hero's top spacing. */
.about--hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: clamp(24px, 4svh, 56px) var(--gutter);
}
.about--hero .about__inner {
  max-width: 780px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.about__caption { margin-bottom: 28px; }

.about__title {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(56px, 12vw, 200px);
  line-height: 0.86;
  letter-spacing: -0.04em;
  text-transform: uppercase;
  color: var(--bg);
  margin-bottom: clamp(40px, 6vw, 80px);
  display: inline-block;
}

.about__note {
  max-width: 640px;
  margin: 0 auto clamp(40px, 6vw, 64px);
  font-family: var(--body);
  font-size: clamp(16px, 1.3vw, 20px);
  line-height: 1.55;
  text-align: center;
  color: var(--bg);
}
.about__note p { margin-bottom: 18px; }
.about__note p:last-child { margin-bottom: 0; }

.about__contact {
  margin-bottom: clamp(40px, 6vw, 80px);
}

.about__email {
  display: inline-block;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(20px, 2vw, 28px);
  letter-spacing: -0.005em;
  color: var(--bg);
  text-transform: lowercase;
  position: relative;
  padding-bottom: 4px;
}
.about__email::before,
.about__email::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1.5px;
  background: currentColor;
  transition: transform 600ms var(--ease);
}
.about__email::before { transform-origin: right; }
.about__email::after  { transform-origin: left; transform: scaleX(0); }
.about__email:hover::before { transform: scaleX(0); }
.about__email:hover::after  { transform: scaleX(1); transition-delay: 240ms; }

.about__more {
  display: inline-block;
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--bg);
  padding: 10px 22px;
  border: 1px solid var(--bg);
  border-radius: 999px;
  margin-top: clamp(40px, 6vw, 64px);
  transition: background-color 240ms var(--ease), color 240ms var(--ease);
}
.about__more:hover { background: var(--bg); color: var(--ink); }

/* ──────────────────────────────────────────────────────────────────
   Legal / disclaimer section (about page)
   ────────────────────────────────────────────────────────────────── */

.legal {
  background: var(--ink);
  color: var(--bg);
  padding: clamp(56px, 9vw, 96px) var(--gutter) clamp(64px, 10vw, 112px);
  border-top: 1px solid var(--dark-rule);
}

.legal__inner {
  max-width: 780px;
  margin: 0 auto;
  text-align: center;
}

.legal__caption {
  margin-bottom: 16px;
}

.legal__title {
  font-family: var(--display);
  font-size: clamp(20px, 2.4vw, 30px);
  font-weight: 800;
  letter-spacing: -0.005em;
  text-transform: uppercase;
  color: var(--bg);
  margin-bottom: 18px;
}

.legal__body {
  font-family: var(--body);
  font-size: clamp(13px, 1vw, 15px);
  line-height: 1.65;
  color: var(--dark-mid);
  text-align: left;
}

/* ──────────────────────────────────────────────────────────────────
   Colophon, dark frosted glass
   ────────────────────────────────────────────────────────────────── */

.colophon {
  padding: clamp(56px, 9vw, 96px) var(--gutter) clamp(56px, 8vw, 80px);
  border-top: 1px solid var(--dark-rule);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(20px, 2.4vw, 30px);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  background: var(--ink);
  color: var(--bg);
  text-align: center;
}

.colophon__icons {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(12px, 1.4vw, 20px);
}

.colophon__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 999px;
  color: var(--bg);
  opacity: 0.78;
  transition: opacity 220ms ease, transform 220ms var(--ease), background 220ms ease;
}
.colophon__icon svg {
  width: 20px;
  height: 20px;
  fill: currentColor;
}
.colophon__icon:hover {
  opacity: 1;
  background: rgba(232, 223, 201, 0.08);
}

.colophon__email {
  display: inline-block;
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--bg);
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
  transition: opacity 200ms ease;
}
.colophon__email:hover { opacity: 0.7; }

.colophon__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
}

.colophon__nav {
  display: inline-block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--bg);
  padding: 8px 18px;
  border: 1px solid var(--bg);
  border-radius: 999px;
  transition: background-color 220ms ease, color 220ms ease, opacity 220ms ease;
}
.colophon__nav:hover {
  background: var(--bg);
  color: var(--ink);
}
.colophon__nav.is-current {
  opacity: 0.45;
  pointer-events: none;
}

.colophon__meta {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.12em;
  color: var(--dark-mid);
  text-transform: uppercase;
}

/* ──────────────────────────────────────────────────────────────────
   Lightbox, opens on .work__media click
   ────────────────────────────────────────────────────────────────── */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 9500;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 280ms var(--ease);
}
.lightbox.is-open {
  opacity: 1;
  pointer-events: auto;
}

.lightbox__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.82);
  cursor: pointer;
}

.lightbox__content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  max-width: 95vw;
  max-height: 95vh;
  /* Long descriptions + disclaimers (e.g. M1 Garand) can push the info
     pane past the bottom of the viewport. Allow internal scroll, and
     give the disclaimer breathing room so it never sits flush against
     the bottom edge. */
  overflow-x: hidden;   /* never scroll or pan sideways */
  overflow-y: auto;     /* long descriptions still scroll vertically */
  overscroll-behavior: contain;   /* no scroll chaining out of the modal */
  touch-action: pan-y;  /* block horizontal finger panning on touch */
  padding-bottom: 32px;
  pointer-events: none;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
}
.lightbox__content::-webkit-scrollbar { width: 6px; }
.lightbox__content::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.25);
  border-radius: 3px;
}
/* Only allow the image + link to receive pointer events while the
   lightbox is actually open. Without this, the invisible (opacity:0)
   image sits in the centre of the viewport and intercepts clicks
   meant for work thumbnails underneath. */
.lightbox.is-open .lightbox__content > * { pointer-events: auto; }
.lightbox:not(.is-open) .lightbox__content > * { pointer-events: none; }

/* Clicking the image closes the lightbox. */
.lightbox__media {
  display: block;
  width: auto;
  height: auto;
  /* Default landscape cap; .lightbox__media[data-portrait] swaps to portrait cap */
  max-width: min(1920px, 90vw);
  max-height: min(1080px, 80vh);
  object-fit: contain;
  background: rgba(255, 255, 255, 0.04);
  transform: scale(0.96);
  transition: transform 320ms var(--ease), opacity 220ms ease;
  opacity: 1;
  cursor: zoom-out;
}
/* While a new src is loading, hide the (still-cached) previous image */
.lightbox__media.is-loading { opacity: 0; }
.lightbox__media[data-portrait] {
  max-width: min(1080px, 90vw);
  max-height: min(1920px, 80vh);
}
.lightbox.is-open .lightbox__media { transform: scale(1); }

/* Wrapper holds the image + play overlay so the overlay sits exactly
   on the image (not the whole content column). */
.lightbox__media-wrap {
  position: relative;
  display: inline-block;
  line-height: 0;
}

/* Lightbox play overlay, only visible when the lightbox is in video mode. */
.lightbox__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 220ms ease, transform 320ms var(--ease);
}
.lightbox__play svg {
  width: clamp(72px, 9vw, 112px);
  height: clamp(72px, 9vw, 112px);
  filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.45));
}
.lightbox__play circle {
  fill: rgba(0, 0, 0, 0.55);
  stroke: rgba(255, 255, 255, 0.92);
  stroke-width: 1.5;
}
.lightbox__play polygon {
  fill: rgba(255, 255, 255, 0.96);
}
/* Visual state, show overlay whenever lightbox is in video OR link mode */
.lightbox.is-video .lightbox__play,
.lightbox.is-link  .lightbox__play {
  opacity: 1;
  cursor: pointer;
}
.lightbox.is-video .lightbox__media,
.lightbox.is-link  .lightbox__media { cursor: pointer; }
.lightbox.is-video:hover .lightbox__play,
.lightbox.is-link:hover  .lightbox__play { transform: scale(1.04); }
/* Clickable only while lightbox is actually open, prevents the
   invisible overlay from intercepting clicks meant for the grid
   thumbnail behind it after a close. */
.lightbox.is-video.is-open .lightbox__play,
.lightbox.is-link.is-open  .lightbox__play { pointer-events: auto; }
.lightbox:not(.is-open)    .lightbox__play { pointer-events: none; }
/* Toggle which icon is shown inside the circle based on mode. */
.lightbox__play path     { display: none; }
.lightbox.is-link .lightbox__play path     { display: inline; }
.lightbox.is-link .lightbox__play polygon  { display: none; }
.lightbox.is-link .lightbox__play path {
  fill: none;
  stroke: rgba(255, 255, 255, 0.96);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Info pane below the image, just description (and optional disclaimer). */
.lightbox__info {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(720px, 90vw);
  text-align: center;
  color: rgba(255, 255, 255, 0.92);
}

.lightbox__desc {
  font-family: var(--body);
  font-size: 13.5px;
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.86);
  overflow-wrap: anywhere;   /* a long token can't widen the info pane */
}
.lightbox__desc:empty { display: none; }

.lightbox__disclaimer {
  font-family: var(--mono);
  font-size: 10px;
  line-height: 1.55;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.5);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  padding-top: 8px;
  margin-top: 4px;
}
.lightbox__disclaimer:empty { display: none; }

/* Clickable Live / Code / Watch links, populated from each work's `links`
   array. Sits below the description (which is plain text and not clickable). */
.lightbox__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 6px;
}
.lightbox__links:empty { display: none; }
.lightbox__link {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.92);
  padding: 7px 14px;
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 999px;
  transition: background 240ms var(--ease), border-color 240ms var(--ease), color 240ms var(--ease);
}
.lightbox__link:hover,
.lightbox__link:focus-visible {
  background: rgba(255, 255, 255, 0.95);
  border-color: rgba(255, 255, 255, 0.95);
  color: #0b0b0b;
}

/* While the lightbox is open, hide the custom cursor for clarity */
html.lightbox-open .cursor { opacity: 0; }
html.lightbox-open body { cursor: auto; }

/* ──────────────────────────────────────────────────────────────────
   Reduced motion
   ────────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  html { scroll-behavior: auto; }
  .word__inner, .hero__title-inner { transform: none !important; }
  .loader { display: none !important; }
  html.cursor-on body { cursor: auto !important; }
  .bg__layer { transform: none !important; }
}

/* Windows High Contrast / forced-colors mode, the OS replaces colours
   with the user's system palette. Hide our decorative shapes + frosted
   glass + magnetic cursor so the high-contrast view stays readable
   instead of showing blocks of system colour where shapes used to be. */
@media (forced-colors: active) {
  .bg, .bg__layer, .bg__glass, .cursor { display: none !important; }
  body { background: Canvas; color: CanvasText; }
  a, .picker__name, .work__title { color: LinkText; }
}

/* No-JS visitors never get is-ready added (bindImageLoad doesn't run),
   so the image-fade transition would never resolve. Force the image
   to opaque so native loading="lazy" still shows the picture. */
.no-js .work__media img,
.no-js .picker__media img {
  opacity: 1;
}

/* Snapshot mode (?screenshot=1), used by headless capture tools.
   Loader is hidden, all rise-in elements are instantly in place, so the
   page is fully painted at any point in time. */
html.is-snapshot .loader { display: none !important; }
html.is-snapshot .word__inner,
html.is-snapshot .hero__title-inner { transform: none !important; animation: none !important; }
html.is-snapshot .picker__item       { opacity: 1 !important; animation: none !important; }
html.is-snapshot .work               { opacity: 1 !important; }
