/* ==========================================================================
   AARYA SARAF — FILM PORTFOLIO
   Design tokens: near-black noir base, warm amber primary accent,
   crimson secondary accent used sparingly (rec dot, sprockets, hover glow).
   Type: Syne (display) / Instrument Sans (body) / JetBrains Mono (timestamps, tags).
   ========================================================================== */

/* Fonts are loaded via <link rel="stylesheet"> in index.html <head>, not a
   CSS @import here — @import can only be discovered after this whole file
   downloads and parses, which delays font fetch until deep into the load
   and can flash a fallback font on titles first. See index.html <head>. */

:root {
  /* Color */
  --bg: #0a0908;
  --bg-elevated: #141215;
  --bg-elevated-2: #1c191b;
  --fg: #f2eee6;
  --fg-dim: #cec7b9;
  --fg-muted: #948c7e;
  --border: rgba(242, 238, 230, 0.1);
  --border-strong: rgba(242, 238, 230, 0.2);

  --amber: #d1a34a;
  --amber-bright: #ecc373;
  --amber-dim: rgba(209, 163, 74, 0.35);

  --crimson: #a83a2c;
  --crimson-bright: #d1503c;
  --crimson-dim: rgba(168, 58, 44, 0.35);

  /* Type */
  --font-display: 'Syne', 'Helvetica Neue', sans-serif;
  --font-body: 'Instrument Sans', -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, monospace;
  --font-serif: 'Newsreader', Georgia, serif;

  /* Spacing (8px rhythm, mid density) */
  --sp-1: 0.5rem;
  --sp-2: 0.75rem;
  --sp-3: 1rem;
  --sp-4: 1.5rem;
  --sp-5: 2rem;
  --sp-6: 3rem;
  --sp-7: 4rem;
  --sp-8: 6rem;
  --sp-9: 8rem;

  --container: 1320px;
  --radius-sm: 3px;
  --radius-md: 6px;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  /* The long, soft deceleration used by the archive button's hover — one shared
     curve for every part of it so the pieces move as a single gesture. */
  --ease-silk: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 200ms;
  --dur-med: 350ms;
  --dur-slow: 600ms;

  --z-grain: 5;
  --z-nav: 100;
  --z-modal: 400;
  /* Must sit above --z-modal: the custom cursor is the only cursor on the
     site (body is cursor:none), so anything stacked over it — the project
     modal — leaves the pointer invisible. */
  --z-cursor: 500;
}

/* ------------------------------ Reset ------------------------------ */

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

html {
  background: var(--bg);
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  /* The custom ring+dot (see .cursor-dot/.cursor-ring) is the only cursor
     site-wide — it already shows its own hover state on every link/button,
     so the native pointer isn't needed there. Restored explicitly on form
     fields below, where the native I-beam is a real functional need, not
     just decoration. Touch devices never render a cursor either way. */
  cursor: none;
}

@media (hover: none), (max-width: 900px) {
  body { cursor: auto; }
}

img, video { max-width: 100%; display: block; }

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

button {
  font-family: inherit;
  background: none;
  border: none;
  color: inherit;
  cursor: none;
}

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

h1, h2, h3, h4, p { margin: 0; }

::selection { background: var(--amber); color: #14110a; }

/* Focus visibility (keyboard nav) */
:focus-visible {
  outline: 2px solid var(--amber-bright);
  outline-offset: 3px;
}

a, button, [tabindex] {
  cursor: none;
}

/* Scrollbar (webkit) */
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--bg-elevated-2); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--fg-muted); }

.container {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--sp-4);
}

@media (min-width: 768px) {
  .container { padding-inline: var(--sp-6); }
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------ Global texture ------------------------------ */
/* Subtle film grain + vignette, fixed over the whole viewport, non-interactive */

.grain-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.vignette-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.55) 100%);
}

/* ------------------------------ Custom cursor ------------------------------ */

.cursor-dot {
  position: fixed;
  top: 0; left: 0;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--amber-bright);
  z-index: var(--z-cursor);
  pointer-events: none;
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity var(--dur-fast) ease, background var(--dur-fast) ease;
  will-change: transform;
}

.cursor-dot.is-visible, .cursor-ring.is-visible { opacity: 1; }

/* Hides the dot specifically while a VIEW/DRAG label is showing in the ring
   — the dot sits dead-center, same spot as the label text, and was making
   it harder to read. */
.cursor-dot.is-label-active { opacity: 0; }
.cursor-ring.is-visible.is-active { opacity: 1; }

.cursor-ring {
  position: fixed;
  top: 0; left: 0;
  width: 44px; height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  z-index: var(--z-cursor);
  pointer-events: none;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  color: var(--fg);
  opacity: 0;
  filter: blur(0px);
  /* Idle "alive" cue: a soft halo breathes outward via box-shadow rather
     than transform/opacity, since JS already drives those every frame for
     position/visibility — animating the same property from both a
     keyframe and inline JS would fight each other every tick. filter gets
     its own slightly longer transition than opacity/border so a soft
     rack-focus blur pulse can play under the scale change on hover (see
     initCursor's is-focusing toggle) instead of a hard snap. */
  transition: opacity var(--dur-fast) ease, width var(--dur-slow) var(--ease-out), height var(--dur-slow) var(--ease-out), border-color var(--dur-med) ease, filter var(--dur-slow) ease;
  animation: cursor-ring-breathe 4s ease-in-out infinite;
  will-change: transform;
}

@keyframes cursor-ring-breathe {
  0%, 100% { box-shadow: 0 0 0 0 rgba(242,238,230,0.14); }
  50% { box-shadow: 0 0 0 5px rgba(242,238,230,0); }
}

.cursor-ring.is-active { opacity: 1; animation: none; box-shadow: none; }
.cursor-ring.is-view { width: 84px; height: 84px; background: rgba(168,58,44,0.15); border-color: var(--crimson-bright); }
.cursor-ring.is-focusing { filter: blur(3px); }

/* Inside the project modal the ring shrinks and warms to amber — a quieter
   "you are here" marker for reading, rather than the larger grey ring used
   over the busy page — and swells gently over anything clickable. */
body:has(.modal-overlay.is-open) .cursor-ring { width: 30px; height: 30px; border-color: rgba(236,195,115,0.5); }
body:has(.modal-overlay.is-open) .cursor-ring.is-active { width: 48px; height: 48px; border-color: var(--amber-bright); background: rgba(209,163,74,0.08); }

@media (hover: none), (max-width: 900px) {
  .cursor-dot, .cursor-ring { display: none; }
}

/* ------------------------------ Nav ------------------------------ */

.site-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-4) var(--sp-4);
  background: linear-gradient(to bottom, rgba(10,9,8,0.85), transparent);
  transition: background var(--dur-med) ease, padding var(--dur-med) ease, backdrop-filter var(--dur-med) ease, transform var(--dur-med) var(--ease-in-out);
}

@media (min-width: 768px) {
  .site-nav { padding: var(--sp-4) var(--sp-6); }
}

.site-nav.is-hidden { transform: translateY(-100%); }

.site-nav.is-scrolled {
  background: rgba(10, 9, 8, 0.82);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
  padding-block: var(--sp-2);
}

.nav-mark {
  display: inline-flex;
}

.nav-mark-text {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.2rem;
  letter-spacing: 0.03em;
  background: linear-gradient(100deg, var(--amber) 0%, var(--amber-bright) 25%, #fff6e0 45%, var(--amber-bright) 65%, var(--amber) 100%);
  background-size: 260% 100%;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: nav-mark-shimmer 5s ease-in-out infinite;
  transition: transform var(--dur-med) var(--ease-out);
  display: inline-block;
}

.nav-mark:hover .nav-mark-text {
  transform: scale(1.06);
}

@keyframes nav-mark-shimmer {
  0%, 15% { background-position: 100% 0%; }
  85%, 100% { background-position: -160% 0%; }
}

.nav-links {
  display: none;
  align-items: center;
  gap: var(--sp-6);
  /* space-between across 3 unequal-width flex children (logo / links / CTA)
     doesn't actually center the middle one — it gets pulled toward whichever
     side is narrower. Taking it out of flow and centering it against the
     nav bar itself is the only way to guarantee true centering regardless
     of how wide the logo or CTA happen to be. */
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

@media (min-width: 900px) {
  .nav-links { display: flex; }
}

/* Slides between links to show which section is actually in view right now
   (driven by the same IntersectionObserver as the side rail, see
   initSectionRail) — a functional read on the nav, not just a hover trick. */
.nav-links-indicator {
  position: absolute;
  top: 50%;
  left: 0;
  height: 30px;
  width: 0;
  /* Vertical centering via margin, not a transform: translateY — GSAP owns
     `transform` outright once it starts tweening x for the horizontal
     slide, and letting it establish that from a clean baseline avoids it
     having to parse/preserve a percentage-based CSS transform. */
  margin-top: -15px;
  background: rgba(209,163,74,0.08);
  border: 1px solid rgba(209,163,74,0.22);
  border-radius: 999px;
  opacity: 0;
  pointer-events: none;
  z-index: 0;
  transition: opacity var(--dur-med) ease;
}

.nav-links-indicator.is-visible { opacity: 1; }

.nav-links a {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-dim);
  position: relative;
  z-index: 1;
  padding-bottom: 4px;
  transition: color var(--dur-fast) ease, letter-spacing var(--dur-med) var(--ease-out);
}

.nav-links a::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  height: 1px; width: 0;
  background: var(--amber);
  transition: width var(--dur-med) var(--ease-out);
}

.nav-links a:hover { color: var(--fg); letter-spacing: 0.18em; }
.nav-links a:hover::after { width: 100%; }

/* Frame-number reveal: a small counter clicks into view on hover — the
   nav's own version of the timecode/frame-count language used everywhere
   else on the site (REC tag, filmstrip frame codes, etc). */
.nav-num {
  display: inline-block;
  max-width: 0;
  margin-right: 0;
  overflow: hidden;
  white-space: nowrap;
  color: var(--amber-bright);
  vertical-align: middle;
  opacity: 0;
  transition: max-width var(--dur-med) var(--ease-out), margin-right var(--dur-med) var(--ease-out), opacity var(--dur-fast) ease;
}

.nav-links a:hover .nav-num {
  max-width: 22px;
  margin-right: 6px;
  opacity: 1;
}

/* Fill-sweep: an amber panel wipes in from the left behind the label, text
   flips to dark against it. The ::before layer carries the fill so the
   border + label transitions stay independent of it. */
.nav-cta {
  display: none;
  position: relative;
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border: 1px solid var(--border-strong);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius-sm);
  transition: border-color var(--dur-fast) ease, color var(--dur-med) var(--ease-out);
}

.nav-cta::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, var(--amber-bright), var(--amber) 75%);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-med) var(--ease-out);
  z-index: -1;
}

@media (min-width: 900px) {
  .nav-cta { display: inline-block; }
}

.nav-cta:hover {
  border-color: var(--amber-bright);
  color: var(--bg);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.4), 0 4px 14px rgba(209,163,74,0.35);
}

.nav-cta:hover::before { transform: scaleX(1); }

.nav-toggle {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: var(--sp-2);
  z-index: calc(var(--z-nav) + 2);
}

@media (min-width: 900px) {
  .nav-toggle { display: none; }
}

.nav-toggle span {
  width: 22px; height: 1.5px;
  background: var(--fg);
  transition: transform var(--dur-med) var(--ease-out), opacity var(--dur-fast) ease, background var(--dur-fast) ease;
}

.nav-toggle.is-open span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav-toggle.is-open span:nth-child(2) { opacity: 0; }
.nav-toggle.is-open span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-nav) + 1);
  background: var(--bg);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--sp-5);
  padding: var(--sp-6);
  transform: translateY(-100%);
  transition: transform var(--dur-slow) var(--ease-in-out);
}

.mobile-menu.is-open { transform: translateY(0); }

.mobile-menu-close {
  position: absolute;
  top: var(--sp-4); right: var(--sp-4);
  width: 44px; height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  color: var(--fg);
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease, transform var(--dur-fast) ease;
}

.mobile-menu-close:hover {
  border-color: var(--crimson-bright);
  color: var(--crimson-bright);
  transform: rotate(90deg);
}

.mobile-menu a {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 2.4rem;
  color: var(--fg-dim);
}

.mobile-menu a:hover { color: var(--amber); }

.mobile-menu .rec-tag { margin-top: var(--sp-4); }

@media (min-width: 900px) {
  .mobile-menu { display: none; }
}

/* ------------------------------ REC tag ------------------------------ */

.rec-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  color: var(--amber-bright);
}

.rec-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--crimson-bright);
  box-shadow: 0 0 8px var(--crimson-bright);
  animation: rec-pulse 1.6s ease-in-out infinite;
}

@keyframes rec-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

/* ------------------------------ Hero ------------------------------ */

.hero {
  position: relative;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
  background: var(--bg);
}

/* Scroll-expand media: starts as a small rounded 16:9 "viewfinder" window and
   grows to full-bleed as the visitor scrolls (driven by GSAP ScrollTrigger
   pin+scrub in initHeroExpand — see main.js).
   The frame is always a true 16:9 box (the montage is 1280x720, every clip
   cropped to its real picture area to match) — specifically the smallest
   16:9 box that still covers the whole hero, centered. At its start scale it
   is therefore a 16:9 window showing the full, uncropped video at the same
   shape on every screen; at scale(1) it overfills the viewport and the
   hero's overflow:hidden trims the excess, which is the only place any
   cropping happens. Scale is uniform, so the picture is never squashed and
   the rounded corners stay circular (initHeroExpand sets the radius in
   frame units so it reads as 28px on screen at the start).
   Still scale, not width/height: animating literal width/height on every
   scroll tick forces a full layout reflow each frame; scale is
   compositor-only, so the expand tracks scroll smoothly instead of lagging
   behind it (which read as a black gap opening up between hero and the next
   section on anything less than a very fast machine). The CSS values below
   are only the pre-JS fallback; GSAP overwrites them on init. */
.hero-media-frame {
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, calc(100dvh * 16 / 9));
  aspect-ratio: 16 / 9;
  transform: translate(-50%, -50%) scale(0.44);
  border-radius: 64px;
  overflow: hidden;
  z-index: 0;
  box-shadow: 0 40px 100px rgba(0,0,0,0.55), 0 0 0 1px rgba(242,238,230,0.08);
}

.hero-media-inner { position: absolute; inset: 0; }

.hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Placeholder cinematic backdrop: layered gradients + slow drifting light leak,
   sits behind the video as a fallback while it loads (or if it fails). */
.hero-media-inner .placeholder-scene {
  position: absolute;
  inset: -10%;
  background:
    radial-gradient(ellipse 60% 50% at 25% 30%, rgba(168,58,44,0.16), transparent 60%),
    radial-gradient(ellipse 50% 45% at 80% 70%, rgba(209,163,74,0.14), transparent 60%),
    linear-gradient(160deg, #0d0c0b 0%, #050505 55%, #0a0807 100%);
  animation: drift 22s ease-in-out infinite alternate;
}

.hero-media-inner .placeholder-scanlines {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(0deg, rgba(255,255,255,0.025) 0px, rgba(255,255,255,0.025) 1px, transparent 1px, transparent 3px);
  mix-blend-mode: overlay;
  opacity: 0.5;
  z-index: 1;
}

/* Baked into the box itself (not just the page-level .hero-dim) so the bottom
   edge is always cleanly dark no matter where the box currently sits during
   the scroll-expand — .hero-dim alone left a bright seam at the box's own
   boundary while it was still small and floating, since outside the box was
   already near-black but inside it was only partially dimmed at that point
   in the page-level gradient. */
.hero-media-vignette {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to top, rgba(6,5,4,0.92) 0%, transparent 22%);
}

@keyframes drift {
  0% { transform: scale(1.05) translate(0, 0); }
  100% { transform: scale(1.15) translate(-2%, -1.5%); }
}

/* Always full-bleed regardless of the media frame's current size, so hero
   text stays legible throughout the expand animation. The bottom stretch is
   fully solid (not just near-solid) so no video texture can ever show through
   right at the seam into the next section. */
/* Deliberately never reaches full opacity anywhere — it's purely the
   atmospheric vignette. Full black at the bottom is .hero-floor-wrap's job
   alone (below), so there's only ever one system claiming "this pixel is
   truly opaque," never two independently-opaque layers meeting at a seam. */
.hero-dim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to top, rgba(10,9,8,0.72) 0%, rgba(10,9,8,0.72) 30%, rgba(10,9,8,0.42) 55%, rgba(10,9,8,0.55) 100%);
}

/* A guaranteed opaque strip at the very bottom edge — belt-and-braces on top
   of .hero-dim's gradient. The gradient math checks out on paper, but this
   bright-seam bug kept resurfacing across viewport sizes, so rather than
   keep chasing percentages this makes the bottom unconditionally solid,
   sized with deep overlap into .hero-dim's own range (rather than just
   abutting its edge) so there's never a seam where two independently-timed
   darkening systems meet. Generous on purpose: it costs nothing visually
   since that whole zone was already dark, and it buys certainty. */
.hero-floor-wrap {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 1;
  pointer-events: none;
  display: flex;
  flex-direction: column;
}

/* The gradient fade sits above a genuinely flat solid block — splitting them
   means the "guaranteed opaque" zone below has zero gradient computation at
   all (just background-color), removing any possibility of a GPU rounding
   artifact at a gradient stop from ever showing at the true seam. */
.hero-floor-fade {
  height: 18vh;
  min-height: 90px;
  background: linear-gradient(to top, var(--bg) 0%, transparent 100%);
}

.hero-floor {
  height: 26vh;
  min-height: 130px;
  background: var(--bg);
}

.hero-frame-corner {
  position: absolute;
  width: 22px; height: 22px;
  border-color: rgba(242,238,230,0.45);
  z-index: 2;
}

.hero-frame-corner.tl { top: 84px; left: var(--sp-4); border-top: 1px solid; border-left: 1px solid; }
.hero-frame-corner.tr { top: 84px; right: var(--sp-4); border-top: 1px solid; border-right: 1px solid; }

@media (min-width: 768px) {
  .hero-frame-corner.tl { left: var(--sp-6); }
  .hero-frame-corner.tr { right: var(--sp-6); }
}

.hero-meta-top {
  position: absolute;
  top: 84px;
  left: 0; right: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--sp-4);
}

@media (min-width: 768px) {
  .hero-meta-top { padding-inline: var(--sp-6); }
}

.hero-content {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 0 var(--sp-4) var(--sp-7);
}

@media (min-width: 768px) {
  .hero-content { padding: 0 var(--sp-6) var(--sp-8); }
}

.hero-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--amber-bright);
  margin-bottom: var(--sp-3);
  overflow: hidden;
  text-wrap: balance; /* on phones the longer line breaks evenly, not with a lone word */
}

.hero-name {
  position: relative;
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.5rem, 6.2vw, 5.75rem);
  line-height: 0.92;
  letter-spacing: -0.02em;
  color: var(--fg);
  white-space: nowrap;
}

.hero-name .line { display: block; overflow: hidden; }
.hero-name .line > span { display: inline-block; }

/* Cursor spotlight: a colorful duplicate of the text sits exactly on top of
   the real text, clipped to a small circle that follows the pointer. Only
   the letter strokes inside that circle are visible (via background-clip:
   text) — the gaps/background stay whatever they already are, never tinted.
   Shared by the hero name and the big section headings (.section-title,
   .contact-headline) — same interaction language everywhere large display
   text appears, not just the hero. */
.heading-echo {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* background-image, not the `background` shorthand — the shorthand resets
     every sub-property it doesn't mention (including background-clip) back
     to its initial value, which silently undid the `background-clip: text`
     below whenever a later same-specificity rule (the per-heading color
     modifiers) set `background:` again. That's what turned the letter-clipped
     reveal into a solid color block covering the text. background-image is a
     longhand — it can't touch background-clip, so this is bug-proof. */
  background-image: conic-gradient(from 0deg, #00d4ff, #ff2f6d, #ffd166, #00d4ff);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  clip-path: circle(var(--echo-r, 22px) at -200px -200px);
  opacity: 0;
  transition: opacity 200ms ease;
}

.heading-echo.is-active { opacity: 1; }

@media (hover: none), (max-width: 900px) {
  .heading-echo { display: none; }
}

/* Each big heading gets its own color identity instead of all six sharing
   one conic-gradient — moving the cursor from one heading to the next now
   reveals a different combo every time. Pure vivid variety, not tied to
   section mood: the point is contrast between headings, not narrative logic. */

/* Hero — the original structure (a small ~22px pointer circle over a conic
   colour wheel centred on the heading) in the aurora palette: pink, violet,
   blue, cyan, mint, gold, coral, back to pink, every colour melting into the
   next. While hovered the wheel turns slowly (--echo-angle is a registered
   angle so it can animate); where @property isn't supported it simply stays
   still. */
@property --echo-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.heading-echo--hero {
  background-image: conic-gradient(from var(--echo-angle),
    #ff4f93, #c25bff, #6f6bff, #2bb8ff, #22e6c8, #ffe066, #ff9a5c, #ff4f93);
}

.heading-echo--hero.is-active { animation: echo-spin 9s linear infinite; }

@keyframes echo-spin {
  to { --echo-angle: 360deg; }
}

@media (prefers-reduced-motion: reduce) {
  .heading-echo--hero.is-active { animation: none; }
}

/* The Archive (Work) — sunset pop: magenta through orange to yellow. */
.heading-echo--archive {
  background-image: conic-gradient(from 0deg, #ff3d81, #ff8a3d, #ffe14d, #ff3d81);
}

/* Behind the Lens (About) — fresh cool pop: lime through teal to blue. */
.heading-echo--about {
  background-image: conic-gradient(from 0deg, #b6ff3d, #2fe6b0, #2f8bff, #b6ff3d);
}

/* Playback (Testimonials) — synthwave pop: violet, hot pink, cyan. */
.heading-echo--playback {
  background-image: conic-gradient(from 0deg, #8b3dff, #ff3dcb, #3ddcff, #8b3dff);
}

/* Let's roll. (Contact) — electric ember pop: gold, red, hot pink. */
.heading-echo--contact {
  background-image: conic-gradient(from 0deg, #ffd23d, #ff5b3d, #ff3d8f, #ffd23d);
}

/* work.html's filtered-view heading — its own identity, separate from the
   homepage section headings it visually echoes (.section-title). */
.heading-echo--work-filter {
  background-image: conic-gradient(from 0deg, #2fe6c8, #5b5bff, #ff3dab, #2fe6c8);
}

.hero-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4);
  /* Snug under the name so eyebrow, name and line read as one evenly spaced
     stack (the eyebrow-to-name gap is ~16px). */
  margin-top: var(--sp-2);
  max-width: 640px;
}

/* A single short line set in the serif italic (a real italic is loaded — see
   the Newsreader request in each page's <head>). Soft and warm against the
   wide, hard display name above it; 'felt' alone takes the site's amber. */
.hero-sub p {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.2rem, 1.9vw, 1.65rem);
  color: var(--fg-dim);
  line-height: 1.5;
  letter-spacing: 0.005em;
  text-wrap: balance;
}

.hero-sub-hl { color: var(--amber-bright); }

/* ------------------------------ Film strip marquee ------------------------------ */

.filmstrip {
  position: relative;
  overflow: hidden;
  background: linear-gradient(180deg, #08070a, #0d0a08 50%, #08070a);
  border-block: 1px solid var(--border);
  height: 236px;
  box-shadow:
    inset 0 23px 34px -23px rgba(0,0,0,0.9),
    inset 0 -23px 34px -23px rgba(0,0,0,0.9);
}


/* Fine dust/scratch pass over the whole reel, on top of every frame */
.filmstrip::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0.5;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='156'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.012 0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23s)' opacity='0.6'/%3E%3C/svg%3E");
}

.filmstrip-track {
  display: flex;
  align-items: center;
  height: 100%;
  width: max-content;
  animation: filmstrip-scroll 50s linear infinite;
}

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

.filmstrip-frame {
  position: relative;
  flex: none;
  width: 286px;
  height: 203px;
  z-index: 1;
  border-inline: 1px solid rgba(242,238,230,0.09);
  background:
    linear-gradient(180deg, transparent 0 34px, rgba(242,238,230,0.05) 34px 36px, transparent 36px calc(100% - 36px), rgba(242,238,230,0.05) calc(100% - 36px) calc(100% - 34px), transparent calc(100% - 34px) 100%);
}

.filmstrip-frame::before,
.filmstrip-frame::after {
  content: '';
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 29px; height: 18px;
  border-radius: 4px;
  background: var(--bg);
  border: 1px solid rgba(242,238,230,0.18);
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.6);
}

.filmstrip-frame::before { top: 10px; }
.filmstrip-frame::after { bottom: 10px; }

.frame-code {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: 0.73rem;
  letter-spacing: 0.08em;
  color: rgba(242, 238, 230, 0.2);
  z-index: 1;
}

.frame-code.top { top: 39px; }
.frame-code.bottom { bottom: 39px; }

.filmstrip-frame .frame-inner {
  position: absolute;
  inset: 52px 10px;
  background-color: var(--bg-elevated);
  background-size: cover;
  overflow: hidden;
  filter: saturate(0.85) brightness(0.88);
}

.filmstrip-frame .frame-inner::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
  opacity: 0.4;
}


/* ------------------------------ Section scaffolding ------------------------------ */

.section {
  padding: var(--sp-8) 0;
  position: relative;
}

@media (min-width: 900px) {
  .section { padding: var(--sp-9) 0; }
}

.section-head {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-bottom: var(--sp-6);
}

/* Desktop: eyebrow and title in the left column, the context line in a right
   column on the TITLE's row (not bottom-aligned to it), its first line
   dropped so its cap height meets the title's cap height. The wrapper <div>
   is dissolved (display: contents) so the three pieces share one grid. */
.section-head { --title-fs: clamp(2rem, 5vw, 3.4rem); }

@media (min-width: 900px) {
  .section-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 360px;
    column-gap: var(--sp-6);
    row-gap: var(--sp-2);
    align-items: start;
    margin-bottom: var(--sp-7);
  }
  .section-head > div { display: contents; }
  .section-head .section-eyebrow { grid-column: 1; grid-row: 1; }
  .section-head .section-title { grid-column: 1; grid-row: 2; }
  .section-head .section-note {
    grid-column: 2;
    grid-row: 2;
    /* title cap-top offset (0.208em of the title size) minus the note's own
       half-leading, so the two first lines' capitals line up */
    margin-top: max(0px, calc(var(--title-fs) * 0.208 - 0.5em));
  }
}

.section-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--amber-bright);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.section-eyebrow::before {
  content: '';
  width: 24px; height: 1px;
  background: var(--crimson-bright);
}

.section-title {
  position: relative;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--title-fs, clamp(2rem, 5vw, 3.4rem));
  letter-spacing: -0.01em;
  text-transform: uppercase;
  line-height: 1.05;
}

/* The context line: a thin amber rule on its left sets it apart as a
   deliberate caption, and its key phrase picks up the site's amber. */
.section-note {
  font-size: 0.95rem;
  color: var(--fg-muted);
  max-width: 360px;
  border-left: 1px solid rgba(236,195,115,0.5);
  padding-left: var(--sp-3);
}

.section-note-hl { color: var(--amber-bright); }

/* ------------------------------ work.html filtered header ------------------------------ */
/* Only present on work.html — a project's modal chip/tag links here with
   ?role= or ?category=, and initProjectGrid() (main.js) fills in the crumb
   value + heading text and hides this block entirely when the page is
   visited with no filter (i.e. it's just the Archive again). */

.work-filter-section {
  padding-top: calc(var(--sp-9) + var(--sp-4));
}

@media (min-width: 900px) {
  .work-filter-section { padding-top: calc(var(--sp-9) + var(--sp-6)); }
}

.work-filter-header {
  margin-bottom: var(--sp-7);
}

.work-filter-crumb {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-bottom: var(--sp-3);
}

.work-filter-crumb a {
  color: var(--fg-muted);
  transition: color var(--dur-fast) ease;
}

.work-filter-crumb a:hover { color: var(--amber-bright); }

.work-filter-crumb-value { color: var(--amber-bright); }

.work-filter-heading {
  margin-bottom: var(--sp-4);
}

.work-filter-clear {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-dim);
  border-bottom: 1px solid var(--border-strong);
  padding-bottom: 2px;
  transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

.work-filter-clear:hover {
  color: var(--amber-bright);
  border-color: var(--amber-bright);
}

.work-filter-empty {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--fg-muted);
  padding: var(--sp-7) 0;
}

.work-filter-empty a {
  color: var(--amber-bright);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ------------------------------ Work grid ------------------------------ */

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--sp-5);
}

/* The Selected Work header sits closer to its filters than the other section
   headers sit to their content — just enough air to stay calm. */
#work .section-head { margin-bottom: var(--sp-5); }

/* Liquid-glass pill: translucent gradient + blur + inset highlight/shadow to
   read as frosted glass, with a subtle amber glow worked into the hover state. */
.filter-btn {
  position: relative;
  font-family: var(--font-mono);
  font-size: 0.79rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.9rem 1.85rem;
  border-radius: 999px;
  color: var(--fg-muted);
  background: linear-gradient(160deg, rgba(255,255,255,0.09), rgba(255,255,255,0.02));
  border: 1px solid rgba(255,255,255,0.12);
  backdrop-filter: blur(10px) saturate(160%);
  -webkit-backdrop-filter: blur(10px) saturate(160%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.18),
    inset 0 -6px 10px -6px rgba(0,0,0,0.4),
    0 2px 8px rgba(0,0,0,0.25);
  transition: color 200ms ease, border-color 300ms ease, box-shadow 300ms ease, transform 250ms var(--ease-out), background 300ms ease;
}

.filter-btn:hover {
  color: var(--fg);
  transform: translateY(-1px);
  border-color: rgba(236,195,115,0.45);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.22),
    inset 0 -6px 10px -6px rgba(0,0,0,0.4),
    0 0 16px rgba(209,163,74,0.28),
    0 6px 16px rgba(0,0,0,0.3);
}

.filter-btn.is-active {
  color: var(--bg);
  background: linear-gradient(160deg, rgba(236,195,115,0.95), rgba(209,163,74,0.9));
  border-color: rgba(236,195,115,0.7);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.5),
    inset 0 -4px 8px -4px rgba(120,80,10,0.35),
    0 4px 14px rgba(209,163,74,0.3);
}

.work-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
}

@media (min-width: 700px) {
  .work-grid { grid-template-columns: repeat(2, 1fr); gap: var(--sp-5) var(--sp-4); }
}

@media (min-width: 1150px) {
  .work-grid { grid-template-columns: repeat(3, 1fr); }
}

/* Entry point into the two-tier structure: the homepage's 6 stay exactly as
   they are, this is the only new thing added to it — a way out to
   archive.html's full body of work. */
.archive-cta-wrap {
  display: flex;
  justify-content: center;
  margin-top: var(--sp-7);
}

/* The archive link is an outline button in the same family as the nav's
   "Start a Project" (mono caps, thin border, amber fill sweeping in from the
   left on hover) so it reads as a clear, native call to action. Its border
   is a touch more amber at rest than the nav button's, so it holds its own
   at the end of a section. */
.archive-cta {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  gap: 0.85em;
  font-family: var(--font-mono);
  font-size: 0.9rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg);
  border: 1px solid rgba(236,195,115,0.5);
  border-radius: var(--radius-sm);
  padding: 1.2rem 2.3rem;
  transition: border-color var(--dur-fast) ease, color var(--dur-med) var(--ease-out), box-shadow var(--dur-med) var(--ease-out);
}

.archive-cta::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, var(--amber-bright), var(--amber) 75%);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-med) var(--ease-out);
  z-index: -1;
}

/* The first word of the label is a two-row slot: on hover "View" rolls up and
   "Play" takes its place. Both are four mono characters, so the button never
   changes width. The slot is exactly one row tall and its top and bottom
   edges fade softly (only in the margins above and below the capitals), so
   the roll is gentle rather than cropped, and at rest only "View" shows. */
.archive-cta-label {
  display: inline-flex;
  gap: 1ch;
  white-space: nowrap;
}

.archive-cta-verb {
  display: inline-block;
  height: 1.5em;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 24%, #000 76%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 24%, #000 76%, transparent 100%);
}

.archive-cta-verb-track {
  display: flex;
  flex-direction: column;
  transition: transform 650ms var(--ease-silk);
}

.archive-cta-verb-track > i {
  display: block;
  height: 1.5em;
  line-height: 1.5em;
  font-style: normal;
}

.archive-cta:hover .archive-cta-verb-track,
.archive-cta:focus-visible .archive-cta-verb-track { transform: translateY(-1.5em); }

/* A small play mark before the label — amber at rest, taking the button's
   colour on hover with the rest of the text. */
.archive-cta-play {
  width: 0.7em;
  height: 0.7em;
  color: var(--amber-bright);
  transition: color var(--dur-med) var(--ease-out), transform var(--dur-med) var(--ease-out);
}

.archive-cta:hover .archive-cta-play,
.archive-cta:focus-visible .archive-cta-play { color: var(--bg); transform: scale(1.15); }

.archive-cta-arrow {
  width: 1.15em;
  height: 1.15em;
  transition: transform var(--dur-med) var(--ease-out);
}

.archive-cta:hover,
.archive-cta:focus-visible {
  border-color: var(--amber-bright);
  color: var(--bg);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.4), 0 4px 14px rgba(209,163,74,0.35);
}

.archive-cta:hover::before,
.archive-cta:focus-visible::before { transform: scaleX(1); }

.archive-cta:hover .archive-cta-arrow,
.archive-cta:focus-visible .archive-cta-arrow { transform: translateX(4px); }

/* The last row of project cards eases back while the link is hovered, so
   attention goes to it. */
.work-grid .project-card:nth-last-child(-n+3) {
  transition: border-color var(--dur-med) ease, opacity 400ms ease, filter 900ms var(--ease-silk), box-shadow var(--dur-med) ease;
}

body:has(.archive-cta:hover) .work-grid .project-card:nth-last-child(-n+3) {
  filter: brightness(0.76);
}

@media (prefers-reduced-motion: reduce) {
  .archive-cta::before,
  .archive-cta-verb-track,
  .archive-cta-play,
  .archive-cta-arrow { transition: none; }
}

.project-card {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-elevated);
  transition: border-color var(--dur-med) ease, opacity 400ms ease, filter 400ms ease, box-shadow var(--dur-med) ease;
}

.project-card:hover {
  border-color: rgba(242,238,230,0.4);
  box-shadow: 0 0 0 1px rgba(242,238,230,0.06), 0 16px 40px rgba(0,0,0,0.45);
}

/* Spotlight: hovering any card quietly dims the other five so attention has
   somewhere obvious to land, without desaturating them — color loss is what
   reads as "washed out," so this leans on opacity/brightness alone and
   keeps every still's actual color intact, just dimmer. */
.work-grid:hover .project-card:not(:hover) {
  opacity: 0.8;
  filter: brightness(0.82);
}

.project-still {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
}

.project-still .still-bg {
  position: absolute;
  inset: 0;
  transform: scale(1.08);
  transition: transform 900ms var(--ease-out), filter var(--dur-med) ease;
  filter: saturate(0.9) brightness(0.92);
}

.project-card:hover .still-bg { transform: scale(1.0); filter: saturate(1.05) brightness(1); }

.project-still .still-bg::after {
  content: '';
  position: absolute; inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
  opacity: 0.35;
}

.project-still .still-vignette {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.4) 55%, rgba(0,0,0,0.78) 100%);
}

/* Muted hover preview — sits above the still, below the vignette/title, so
   the darkening and text stay legible over real footage the same way they
   do over the static image. Lazy-loaded on first hover (see
   initProjectPreviews in main.js); a project with no data-preview never
   gets one of these wired up at all. */
.still-preview-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 300ms ease;
}

.still-preview-video.is-active { opacity: 1; }

.project-tag {
  position: absolute;
  top: var(--sp-3); left: var(--sp-3);
  z-index: 2;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 5px 9px;
  border: 1px solid rgba(242,238,230,0.25);
  border-radius: 2px;
  background: rgba(10,9,8,0.5);
  backdrop-filter: blur(4px);
  color: var(--fg-dim);
}

.project-index {
  position: absolute;
  top: var(--sp-3); right: var(--sp-3);
  z-index: 2;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--fg-muted);
}

.project-info {
  position: absolute;
  left: var(--sp-3); right: var(--sp-3); bottom: var(--sp-3);
  z-index: 2;
}

.project-info h3 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.5rem, 2.6vw, 2.1rem);
  line-height: 1.02;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  text-shadow: 0 2px 16px rgba(0,0,0,0.7);
}

/* Same .role-chip component the modal uses — wraps to a 2nd row rather
   than truncating, so it scales to however many roles a project has instead
   of maxing out around two the way the old plain-text meta line did. */
.project-info .project-roles {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: var(--sp-2);
}

/* Grid-card role chips only (not the modal's identical-looking ones) get
   the native pointer back — see the paired mouseenter/mouseleave in
   initCursor() (main.js) that hides the custom ring+dot while over one of
   these, so this is the only cursor actually visible during that hover. */
.project-roles .role-chip {
  cursor: pointer;
}

/* ------------------------------ Archive (light-treatment cards) ------------------------------ */
/* archive.html's "everything else" tier — denser than .work-grid (3 per row,
   not up to 3 at a much larger min-width), no hover-preview video, no role
   chips in the grid itself. Each .archive-card also carries .project-card,
   so the site's real openModal() (see initModal in main.js) picks it up for
   free — it already renders a complete modal with no video section when a
   project has none, exactly like Pause does today. The only thing built
   here is the compact grid presentation; the detail view is the same
   component the homepage's 6 flagship projects use. */

.archive-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-5);
}

@media (min-width: 640px) {
  .archive-grid { grid-template-columns: repeat(3, 1fr); }
}

.archive-card {
  position: relative;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-elevated);
  cursor: pointer;
}

.archive-still {
  position: relative;
  aspect-ratio: 4 / 5;
  background-size: cover;
  background-position: center;
  filter: saturate(0.85) brightness(0.8);
  transition: transform 500ms var(--ease-out), filter 400ms ease;
}

.archive-card:hover .archive-still {
  transform: scale(1.03);
  filter: saturate(1) brightness(0.94);
}

/* Same "not identifiable as clickable" lesson as the role chips — a plain
   static image gave no hint it opens to anything, so hovering surfaces an
   explicit label rather than relying on the cursor alone. */
.archive-view-hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg);
  background: rgba(10,9,8,0.35);
  opacity: 0;
  transition: opacity var(--dur-med) ease;
}

.archive-card:hover .archive-view-hint { opacity: 1; }

.archive-info { padding: var(--sp-3) var(--sp-1) 0; }

.archive-info h4 {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: 1.15rem;
  line-height: 1.15;
  margin-bottom: 3px;
}

.archive-meta {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.archive-empty {
  grid-column: 1 / -1;
  text-align: center;
  padding: var(--sp-8) var(--sp-4);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--fg-muted);
}

/* ------------------------------ Modal ------------------------------ */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: rgba(6,5,4,0.9);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-4);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-med) ease;
}

.modal-overlay.is-open { opacity: 1; pointer-events: auto; }

/* Same focus-pull language as the section reveals (blur + scale settling
   into place) rather than a new motion idea just for the modal — a bit
   faster here since it's answering a click, not arriving on scroll. */
.modal-panel {
  width: 100%;
  max-width: 880px;
  max-height: 88vh;
  overflow-y: auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  transform: translateY(16px) scale(0.95);
  filter: blur(6px);
  opacity: 0;
  transition: transform var(--dur-slow) var(--ease-out), filter var(--dur-slow) var(--ease-out), opacity var(--dur-med) ease;
}

.modal-overlay.is-open .modal-panel { transform: translateY(0) scale(1); filter: blur(0px); opacity: 1; }

.modal-still {
  position: relative;
  aspect-ratio: 16 / 8;
}

/* Empty (no size, no content) whenever a project has no data-youtube — the
   static still behind it just shows through, unchanged from before this
   existed. */
.modal-video {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.modal-video iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.modal-still.has-hosted-video { aspect-ratio: 16 / 9; background-color: #000; }

.modal-still.has-hosted-video .modal-unmute { bottom: auto; top: var(--sp-4); }

.modal-video video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}

.modal-unmute {
  position: absolute;
  bottom: var(--sp-4); left: var(--sp-4);
  z-index: 3;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg);
  background: rgba(10,9,8,0.6);
  border: 1px solid rgba(242,238,230,0.25);
  backdrop-filter: blur(4px);
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease, background var(--dur-fast) ease;
}

.modal-unmute:hover {
  border-color: rgba(236,195,115,0.5);
  color: var(--amber-bright);
}

/* .modal-unmute's own `display: flex` above beats the UA stylesheet's
   `[hidden] { display: none }` rule (author styles always win over UA
   styles at equal specificity), so JS setting the hidden attribute — the
   only thing renderVideo() does when a project has no YouTube link — was
   silently not hiding the button at all. Pause has had a visible, non-
   functional "Unmute" button this whole time because of it. This rule is
   more specific than the plain class alone, so it wins regardless of
   source order and actually restores the native hidden behavior. */
.modal-unmute[hidden] {
  display: none;
}

.modal-close {
  position: absolute;
  top: var(--sp-4); right: var(--sp-4);
  z-index: 3;
  width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(242,238,230,0.3);
  border-radius: 50%;
  background: rgba(10,9,8,0.5);
  backdrop-filter: blur(4px);
  color: var(--fg);
  transition: border-color var(--dur-fast) ease, background var(--dur-fast) ease, transform var(--dur-fast) ease;
}

.modal-close:hover { border-color: var(--crimson-bright); background: rgba(168,58,44,0.2); transform: rotate(90deg); }

.modal-body {
  padding: var(--sp-5) var(--sp-5) var(--sp-6);
}

@media (min-width: 768px) {
  .modal-body { padding: var(--sp-6) var(--sp-7) var(--sp-7); }
}

.modal-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}

/* Resets the default <button> chrome so the clickable category pill looks
   identical to the plain .project-tag span it used to be. */
.filter-btn[hidden], .filter-bar[hidden] { display: none; }

/* On a grid card the genre tag keeps the mono face of the plain tag it
   replaces (the base button reset above inherits the body font). */
.project-card .project-tag--btn { font-family: var(--font-mono); }

.project-tag--btn {
  font-family: inherit;
  appearance: none;
  cursor: pointer;
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease, background var(--dur-fast) ease;
}

/* Same always-visible arrow as .role-chip, scoped to the clickable modal
   variant only — the plain .project-tag span on grid-card stills isn't
   interactive, so it stays arrow-free. Drawn as a masked currentColor shape
   (not an <img>/emoji) so it recolors with the button on hover for free. */
.project-tag--btn::after {
  content: '';
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: 5px;
  vertical-align: middle;
  opacity: 0.6;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='7' y1='17' x2='17' y2='7'/%3E%3Cpolyline points='7 7 17 7 17 17'/%3E%3C/svg%3E") no-repeat center / contain;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='7' y1='17' x2='17' y2='7'/%3E%3Cpolyline points='7 7 17 7 17 17'/%3E%3C/svg%3E") no-repeat center / contain;
  transition: opacity var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
}

.project-tag--btn:hover {
  border-color: rgba(236,195,115,0.5);
  color: var(--amber-bright);
}

.project-tag--btn:hover::after {
  opacity: 1;
  transform: translate(1px, -1px);
}

/* Pushed to the far end of .modal-tags via margin-left:auto — category
   (+ year) sit left, roles sit right, on the one line. Empty and
   completely invisible (no gap taken) when a project has no roles set. */
.modal-roles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-left: auto;
}

.role-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 5px 8px 5px 9px;
  border: 1px solid rgba(242,238,230,0.25);
  border-radius: 2px;
  background: rgba(10,9,8,0.5);
  color: var(--fg-dim);
  cursor: pointer;
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease, background var(--dur-fast) ease;
}

/* Always visible, not just on hover — a hover-only cue never gets seen by
   someone who hasn't already moved the mouse over the chip, which was
   exactly the "not identifiable as clickable" problem. Borrows the same
   arrow already used on .contact-email so it reads as one visual language
   for "this leads somewhere" across the whole site. */
.role-chip-arrow {
  width: 8px;
  height: 8px;
  opacity: 0.55;
  flex: none;
  transition: opacity var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
}

.role-chip:hover {
  border-color: rgba(236,195,115,0.5);
  color: var(--amber-bright);
  background: rgba(209,163,74,0.1);
}

.role-chip:hover .role-chip-arrow {
  opacity: 1;
  transform: translate(1px, -1px);
}

.modal-title {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: clamp(1.8rem, 4vw, 2.6rem);
  margin-bottom: var(--sp-3);
}

.modal-desc {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.05rem, 1.6vw, 1.2rem);
  line-height: 1.55;
  color: var(--fg);
  max-width: 60ch;
  margin-bottom: var(--sp-5);
}

/* A short director's note in the site's serif, with a small mono label.
   Hidden entirely when a film has no note yet. */
.modal-desc::before {
  content: 'Director\2019s note';
  display: block;
  margin-bottom: var(--sp-1);
  font-family: var(--font-mono);
  font-style: normal;
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--amber);
}

.modal-specs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4) var(--sp-7);
  border-top: 1px solid var(--border);
  padding-top: var(--sp-4);
}

.modal-specs[hidden] { display: none; }

.modal-specs dt {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-bottom: 4px;
}

.modal-specs dd {
  margin: 0;
  font-size: 0.9rem;
  color: var(--fg);
}

/* Reading spotlight: while the pointer is in the specs, the row under it
   stays at full strength with a thin amber tick and amber value; the others
   step back. Pointer-only (no hover on touch), CSS-only, and fully
   reversible the moment the pointer leaves. */
.modal-specs > div,
.award-badge {
  position: relative;
  transition: opacity var(--dur-med) ease;
}

.modal-specs dd { transition: color var(--dur-fast) ease; }

.modal-specs > div::before {
  content: '';
  position: absolute;
  left: -10px;
  top: 2px;
  bottom: 2px;
  width: 2px;
  border-radius: 1px;
  background: var(--amber-bright);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform var(--dur-med) var(--ease-out);
}

@media (hover: hover) {
  .modal-specs:hover > div:not(:hover),
  .modal-awards:hover .award-badge:not(:hover) { opacity: 0.45; }
  .modal-specs > div:hover::before { transform: scaleY(1); }
  .modal-specs > div:hover dd { color: var(--amber-bright); }
}

/* Festival credits — only rendered when a project card carries data-awards
   (see initModal in main.js). Empty and untouched otherwise, so most
   projects never show this row at all. */
.modal-watch-link {
  display: inline-block;
  margin-top: var(--sp-4);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-muted);
  border-bottom: 1px solid var(--border-strong);
  padding-bottom: 2px;
  transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.modal-watch-link[hidden] { display: none; } /* the class's own display would otherwise override [hidden] */
.modal-watch-link:hover { color: var(--amber); border-color: var(--amber); }

.modal-awards {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.modal-awards:not(:empty) {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--border);
}

.award-badge {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.award-badge-icon {
  flex: none;
  width: 30px;
  height: 30px;
  color: var(--amber-bright);
}

.award-badge-icon svg { width: 100%; height: 100%; }

.award-badge-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.award-badge-text strong {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg);
  font-weight: 500;
}

.award-badge-text span {
  font-size: 0.78rem;
  color: var(--fg-muted);
}

/* ------------------------------ About ------------------------------ */
/* Back to the classic two-column layout — portrait on one side, lede/body/
   stats/tools on the other. Both the type-on-photo overlap and the
   editorial pull-quote were tried and didn't earn their keep; this reads
   cleanest with a real photo in the frame. */

.about-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-7);
}

@media (min-width: 950px) {
  /* The text column is pinned to exactly the width it had in the original
     0.85fr / 1fr split (1/1.85 of the row after a --sp-7 gap) so the copy
     wraps as it always did; the portrait takes whatever is left, with a
     tighter --sp-5 gutter. align-items: stretch + a height-filling portrait
     (no fixed aspect ratio) makes the photo's top and bottom edges land on
     the text block's, instead of guessing at a ratio. */
  .about-grid {
    grid-template-columns: 1fr calc((100% - var(--sp-7)) / 1.85);
    column-gap: var(--sp-5);
    align-items: stretch;
  }
  /* The photo keeps its original 4:5 crop (nothing more of the picture is
     cut off than before). Its height is stretched to the row — top at the
     lede's first line (8px = the lede's half-leading), bottom on the stats
     block's bottom border, which is the row's bottom edge — and its width
     follows from the 4:5 ratio, pinned to the left edge of its column. */
  /* Narrower desktops: the text wraps taller, and a 4:5 box stretched to that
     height would be wider than its column (and push the text off-screen). So
     here the photo takes the column's width at its true 4:5 and stays pinned
     while the text scrolls past. */
  .about-grid { align-items: start; }
  .about-grid .about-portrait-stage { justify-self: stretch; align-self: start; position: sticky; top: 96px; width: 100%; margin-block: 8px 0; }
}

@media (min-width: 1280px) {
  /* Wide enough that the stretched-to-the-text 4:5 fits its column. */
  .about-grid { align-items: stretch; }
  .about-grid .about-portrait-stage { justify-self: start; align-self: stretch; position: relative; top: auto; width: auto; margin-block: 8px 0; }
}

/* The stage is the grid item: it holds the 4:5 box, the reveal, and the two
   effects behind the photo — an amber outline offset and tilted slightly like
   a print on a mount, and a warm light that slowly breathes behind it, as if
   spilling from the photo. Both sit behind the frame (negative z-index inside
   the stage's own stacking context), so they never touch the photo itself. */
.about-portrait-stage {
  position: relative;
  aspect-ratio: 4 / 5;
  isolation: isolate;
}

.about-portrait-stage::before {
  content: '';
  position: absolute;
  inset: -12% -18%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 55%, rgba(209,163,74,0.30), rgba(168,58,44,0.16) 42%, transparent 70%);
  filter: blur(34px);
  animation: portrait-breathe 8s ease-in-out infinite;
}

.about-portrait-stage::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  border: 1px solid rgba(236,195,115,0.5);
  border-radius: var(--radius-md);
  transform: translate(16px, 14px) rotate(1.4deg);
}

@keyframes portrait-breathe {
  0%, 100% { opacity: 0.7; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.04); }
}

@media (max-width: 949px) {
  .about-portrait-stage::after { transform: translate(10px, 9px) rotate(1.2deg); }
}

@media (prefers-reduced-motion: reduce) {
  .about-portrait-stage::before { animation: none; opacity: 0.85; }
}

/* Prompter: an illustrated face rises from behind the frame's bottom-right
   corner with a small glass speech bubble over the jacket (never over the
   face). It breathes gently while it waits. It stays until the visitor
   really plays with the photo, then grins, changes its line, holds a moment
   and leaves (initPortraitPrompt). Hidden on touch and narrow screens, where
   the hover effect doesn't run. */
.portrait-prompt {
  position: absolute;
  right: -24px;
  bottom: -30px;
  z-index: 3;
  display: flex;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;
  opacity: 0;
  transform: translateY(26px) scale(0.94);
  transform-origin: 100% 100%;
  transition: opacity 900ms var(--ease-out), transform 1100ms var(--ease-out);
}

.portrait-prompt.is-visible { opacity: 1; transform: none; }

.portrait-prompt.is-leaving {
  opacity: 0;
  transform: translateY(18px) scale(0.96);
  transition: opacity 900ms ease, transform 1100ms ease;
}

.portrait-prompt-face {
  display: block;
  width: 78px;
  height: auto;
  transform-origin: 50% 100%;
  filter: drop-shadow(0 8px 16px rgba(0,0,0,0.5));
}

.portrait-prompt.is-visible .portrait-prompt-face { animation: prompt-breathe 5.5s ease-in-out infinite; }
.portrait-prompt.is-reacting .portrait-prompt-face { animation: prompt-cheer 0.8s var(--ease-out); }

.portrait-prompt-bubble {
  position: relative;
  margin-bottom: 46px;
  max-width: 156px;
  padding: 9px 13px;
  background: rgba(16,14,11,0.88);
  border: 1px solid rgba(236,195,115,0.42);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: var(--fg);
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 0.98rem;
  line-height: 1.25;
  border-radius: 12px 12px 12px 3px;
  box-shadow: 0 10px 28px rgba(0,0,0,0.45);
  transition: opacity 240ms ease;
}

.portrait-prompt-bubble::after {
  content: '';
  position: absolute;
  right: -6px;
  bottom: 9px;
  width: 11px;
  height: 11px;
  background: rgba(16,14,11,0.88);
  border-top: 1px solid rgba(236,195,115,0.42);
  border-right: 1px solid rgba(236,195,115,0.42);
  border-radius: 0 2px 0 0;
  transform: rotate(45deg);
}

@keyframes prompt-breathe {
  0%, 100% { transform: rotate(-1.5deg) translateY(0); }
  50% { transform: rotate(1.5deg) translateY(-2px); }
}

@keyframes prompt-cheer {
  0% { transform: scale(1) rotate(0deg); }
  35% { transform: scale(1.12) rotate(-4deg); }
  70% { transform: scale(1.04) rotate(2deg); }
  100% { transform: scale(1) rotate(0deg); }
}

@media (hover: none), (max-width: 900px) {
  .portrait-prompt { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .portrait-prompt.is-visible .portrait-prompt-face,
  .portrait-prompt.is-reacting .portrait-prompt-face { animation: none; }
}

/* Keeps the copy above the light bleed where the two overlap. */
.about-content { position: relative; z-index: 1; }

.about-portrait {
  position: absolute;
  inset: 0;
  /* It's a <figure>, whose UA default is margin: 1em 40px — that was the
     extra horizontal padding around the photo and a 16px vertical offset
     against the text beside it. */
  margin: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--border);
  background:
    radial-gradient(ellipse at 30% 20%, rgba(209,163,74,0.16), transparent 55%),
    radial-gradient(ellipse at 75% 85%, rgba(168,58,44,0.18), transparent 55%),
    linear-gradient(155deg, #16141a, #0a0908 65%);
}

/* (No grain overlay on the portrait — it softened the headshot. The photo is shown clean and sharp.) */

/* Headshot framing. The box is always 4:5, and the photo (2:3, face a touch
   left of centre) is placed by hand rather than with object-fit: scaled to
   107% of the frame's width (a slightly wider view than the earlier 120%),
   shifted so the face's centre sits on the frame's vertical centre line, the
   top of the hair is ~8% from the top edge and the eyes land about a third
   of the way down, with the shoulders filling the lower part. Percentages here are of the frame:
   left is of its width, top of its height. */
.portrait-photo {
  position: absolute;
  left: -1.4%;
  top: -12.5%;
  width: 107%;
  height: auto;
  max-width: none;
}

/* A 6-segment snake of expressions over the portrait — the real photo
   underneath is never covered or dimmed. Position is driven every animation
   frame by initPortraitExpressions() (main.js): the head (node 0) lerps
   toward the live cursor and records its own path as it moves; every other
   segment samples a point further back along THAT recorded path (classic
   Snake-game follow-the-leader), not a chase-chain toward the segment in
   front of it — which is what keeps the body from sliding together into a
   muddy overlap when the cursor moves slowly or stops, since the path itself
   only grows when the head actually goes somewhere new. Not CSS transitions
   — continuous per-frame movement isn't something a transition can drive.
   Only the group's own fade in/out on enter/leave is a transition. */
.portrait-trail {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity 160ms ease;
}

.portrait-trail.is-active {
  opacity: 1;
}

.portrait-trail-node {
  position: absolute;
  left: 0;
  top: 0;
  width: 82px;
  height: 82px;
  object-fit: contain;
  will-change: transform;
}

/* Without an explicit stacking order, elements with no z-index paint in DOM
   order — node 0 (the head) is FIRST in the markup, so nodes 1-5 were all
   painting on top of it, covering the one segment that's actually meant to
   read clearly. Explicit descending z-index puts the head unambiguously on
   top regardless of DOM order. */
.portrait-trail-node[data-node="0"] { z-index: 6; }
.portrait-trail-node[data-node="1"] { z-index: 5; }
.portrait-trail-node[data-node="2"] { z-index: 4; }
.portrait-trail-node[data-node="3"] { z-index: 3; }
.portrait-trail-node[data-node="4"] { z-index: 2; }
.portrait-trail-node[data-node="5"] { z-index: 1; }

/* A gentler taper than the previous 2-ghost version — with a full 6-segment
   body meant to read as a chain of faces (not one bold lead plus near-
   invisible ghosts), each segment should still be legible, just progressively
   quieter toward the tail. Scale itself is baked into the per-frame
   transform in JS (kept alongside position so both update in the same
   write), only opacity lives here. */
/* The head is the one segment meant to actually be read, not just glanced
   at — a soft drop-shadow (not available to the trailing ghosts) lifts it
   off the photo whenever it lands on a background close to its own
   brightness/color, which a flat opacity bump alone can't fix. */
.portrait-trail-node[data-node="0"] {
  opacity: 1;
  filter: drop-shadow(0 4px 10px rgba(0,0,0,0.45));
}
.portrait-trail-node[data-node="1"] { opacity: 0.85; }
.portrait-trail-node[data-node="2"] { opacity: 0.7; }
.portrait-trail-node[data-node="3"] { opacity: 0.58; }
.portrait-trail-node[data-node="4"] { opacity: 0.46; }
.portrait-trail-node[data-node="5"] { opacity: 0.36; }

@media (hover: none), (max-width: 900px) {
  .portrait-trail { display: none; }
}

.about-lede {
  font-family: var(--font-serif);
  font-style: normal;
  font-weight: 500;
  font-size: clamp(1.35rem, 2.7vw, 2rem);
  line-height: 1.5;
  color: var(--fg);
  margin-bottom: var(--sp-5);
}

.about-lede .hl { color: var(--amber-bright); }

/* Words are wrapped at runtime (see initLedeHighlight in main.js) and start
   dim, brightening to full opacity as the visitor scrolls through the
   paragraph — the color itself never changes, only opacity, so this degrades
   safely to a fully-legible static paragraph if JS/GSAP is unavailable. */
.lede-word {
  display: inline-block;
  color: var(--fg);
}

.lede-word--hl { color: var(--amber-bright); }

.about-body p {
  color: rgba(242, 238, 230, 0.7);
  font-size: 1rem;
  line-height: 1.7;
  margin-bottom: var(--sp-3);
  max-width: 56ch;
}

/* The stats row. <dd> carries a 40px UA indent, which is what pushed every
   label off its number's left edge — zeroed here so number and label share
   one left edge, and each column starts on a hairline of its own. */
.about-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: var(--sp-5);
  margin: var(--sp-6) 0 0;
  padding: 0;
}

.about-stats > div {
  border-top: 1px solid var(--border-strong);
  padding-top: var(--sp-3);
}

.about-stats dt,
.about-stats dd { margin: 0; }

.about-stats .stat-num {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.8rem, 3.2vw, 2.4rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
}

.about-stats .stat-num span { color: var(--crimson-bright); }

.about-stats .stat-label {
  margin-top: var(--sp-2);
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--fg-muted);
}

/* ------------------------------ Testimonials (SD-card stagger deck) ------------------------------ */

/* The Archive runs cool and neutral; Playback is where the site should feel
   like it's settled somewhere warmer for a moment — a slow amber-black wash
   from the section's own top edge, so the shift reads as you scroll in
   rather than snapping at a hard boundary. Reaches full warm tone by 65%
   and HOLDS flat for the rest — .contact-section starts flat at that same
   exact color too (see its own comment), so the seam between them is a
   constant, unchanging color on both sides, not two gradients trying to
   line up. A perceptible "kink" in how fast a gradient is changing reads as
   a visible line even when the color value matches exactly at the
   boundary — holding both sides flat is what actually kills that. */
.testi-section {
  position: relative;
  overflow: hidden;
  background: linear-gradient(180deg, var(--bg) 0%, #150f0a 35%, #1b130b 65%, #1b130b 100%);
}

/* Reuses .collage-frame (defined with .contact-collage further down) for
   the individual photos so both groups read as one continuous sheet. This
   wrapper's own opacity is what actually fades in — driven by GSAP scrub in
   initTestiCollageLead (main.js), not a CSS animation, since it has to be
   pinned to scroll position, not a fixed duration. */
.testi-collage-lead {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
}

@media (max-width: 700px) {
  /* Matches .contact-collage's own mobile cutoff — no scattered-photo
     treatment on narrow viewports for either half of this effect. */
  .testi-collage-lead { display: none; }
}

.testi-deck {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.testi-stack {
  position: relative;
  width: 100%;
  height: 460px;
  margin-bottom: var(--sp-6);
  perspective: 1600px;
  touch-action: pan-y;
}

@property --gr { syntax: '<number>'; inherits: true; initial-value: 209; }
@property --gg { syntax: '<number>'; inherits: true; initial-value: 163; }
@property --gb { syntax: '<number>'; inherits: true; initial-value: 74; }

/* Ambient light pooling behind the active card — idles with a slow breathing
   drift, and gets a brief boost each time the active card changes. */
.testi-glow {
  position: absolute;
  top: 48%;
  left: 50%;
  width: 420px;
  height: 420px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(var(--gr), var(--gg), var(--gb), 0.4), rgba(168,58,44,0.12) 55%, transparent 75%);
  filter: blur(50px);
  z-index: 0;
  pointer-events: none;
  animation: testi-glow-breathe 6s ease-in-out infinite;
  transition: filter 400ms ease, --gr 900ms ease, --gg 900ms ease, --gb 900ms ease;
}

/* A brief brightness flash on top of the idle breathing, cued by JS each
   time the active card changes — keeps transform/opacity on the keyframe
   animation and this on a separate property so the two never fight. */
.testi-glow.is-pulsing { filter: blur(50px) brightness(1.6); }

@keyframes testi-glow-breathe {
  0%, 100% { opacity: 0.7; transform: translate(-50%, -50%) scale(1); }
  50% { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
}

@media (max-width: 600px) {
  .testi-stack { height: 500px; }
}

.testi-card {
  position: absolute;
  top: 0;
  left: 50%;
  width: min(480px, 88vw);
  min-height: 420px;
  display: flex;
  flex-direction: column;
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  background: linear-gradient(155deg, var(--bg-elevated-2), var(--bg-elevated) 70%);
  box-shadow: inset 0 0 0 1px var(--border-strong), 0 24px 48px rgba(0,0,0,0.5);
  /* Notched top-left corner, like a memory card pulled from a camera body */
  clip-path: polygon(28px 0, 100% 0, 100% 100%, 0 100%, 0 28px);
  will-change: transform, opacity, filter;
  transition: box-shadow 400ms ease;
  /* Dragging across a card's own quote text would otherwise fire the
     browser's native text-selection instead of (or alongside) our drag
     handling — this is what made drag-to-navigate feel broken/unusable. */
  user-select: none;
  -webkit-user-select: none;
}

/* The in-focus card gets a touch more presence beyond just sharpness/opacity
   — a faint amber edge and a deeper shadow lift it off the ones behind it. */
.testi-card.is-active {
  box-shadow:
    inset 0 0 0 1px rgba(236,195,115,0.3),
    0 32px 64px rgba(0,0,0,0.6),
    0 0 32px rgba(209,163,74,0.1);
}

/* A card tied to a real film: that film's still sits behind the words, dimmed
   and faded out toward the left so the quote stays fully legible. */
.testi-card--still { isolation: isolate; }
.testi-card--still::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--card-still) 70% 40% / cover no-repeat;
  opacity: 0.42;
  filter: saturate(0.85) contrast(1.05);
  -webkit-mask-image: linear-gradient(to left, #000 0%, rgba(0,0,0,0.55) 42%, transparent 82%);
  mask-image: linear-gradient(to left, #000 0%, rgba(0,0,0,0.55) 42%, transparent 82%);
}

.testi-card:focus-visible { outline-offset: 4px; }

.testi-card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--sp-4);
  padding-left: 6px;
}

.testi-pins {
  display: flex;
  align-items: center;
  gap: 3px;
}

.testi-pins span {
  width: 7px;
  height: 16px;
  border-radius: 1px;
  background: linear-gradient(180deg, var(--amber-bright), var(--amber) 60%, #8a6a2c);
  box-shadow: inset 0 1px 1px rgba(255,255,255,0.35);
}

/* Client / company logo, top right of each testimonial card. Whatever logo
   goes in (an <img> swapped in for the placeholder) is flattened to a soft
   white silhouette so a mix of colours and artwork reads as one consistent,
   quiet row of marks on the dark card. */
.testi-logo {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-height: 30px;
  min-width: 104px;
}

.testi-logo-link { display: flex; align-items: center; }
.testi-logo-link:hover img { opacity: 1; }

.testi-logo img {
  display: block;
  width: auto;
  height: auto;
  max-width: 128px;
  max-height: 46px; /* roomy enough for a stacked/square mark, not just a wide wordmark */
  object-fit: contain;
  filter: brightness(0) invert(1);
  opacity: 0.78;
}

.testi-logo-placeholder {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-muted);
  padding: 8px 12px;
  border: 1px dashed rgba(242,238,230,0.22);
  border-radius: 2px;
}

.testi-quote {
  margin: 0 0 var(--sp-4);
  font-family: var(--font-serif);
  font-weight: 500; /* same weight as the About lede */
  font-size: clamp(1rem, 1.6vw, 1.15rem);
  line-height: 1.55;
  color: var(--fg);
  flex-grow: 1;
}

.testi-card--slate .testi-quote { color: var(--fg-muted); font-style: italic; }
.testi-card--slate .testi-name { color: var(--fg-muted); }
.testi-card--slate .testi-desig { color: var(--fg-muted); }

.testi-quote::before { content: '\201C'; }
.testi-quote::after { content: '\201D'; }

.testi-meta {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: var(--sp-4);
}

/* Name and designation are the second thing read after the quote, so they
   carry real weight: an amber rule to anchor them, the name large in the
   display face, the designation in bright mono caps and the company a step
   quieter. */
.testi-meta::before {
  content: '';
  width: 28px;
  height: 2px;
  margin-bottom: 8px;
  background: var(--amber);
}

.testi-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.15rem, 1.9vw, 1.35rem);
  line-height: 1.15;
  letter-spacing: 0.01em;
  color: var(--fg);
}

.testi-role {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.testi-desig { color: var(--amber-bright); }
.testi-company { color: var(--fg-muted); }
.testi-company::before { content: '/'; margin-right: 10px; color: var(--border-strong); }

.testi-card-bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}

.testi-code {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--amber-bright);
}

.testi-duration {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--fg-muted);
}

.testi-watch {
  padding: 0 0 2px;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border-strong);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-muted);
  cursor: pointer;
  transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.testi-watch:hover { color: var(--amber-bright); border-color: var(--amber-bright); }

.testi-controls {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}

.testi-arrow {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  color: var(--fg-dim);
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease, background var(--dur-fast) ease;
}

.testi-arrow svg { width: 18px; height: 18px; }

.testi-arrow:hover {
  border-color: var(--amber);
  color: var(--amber-bright);
  background: rgba(209,163,74,0.08);
}

.testi-dots {
  display: flex;
  align-items: center;
  gap: 8px;
}

.testi-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--border-strong);
  transition: background var(--dur-fast) ease, transform var(--dur-fast) ease;
}

.testi-dot.is-active {
  background: var(--amber-bright);
  transform: scale(1.3);
}

/* ------------------------------ Contact ------------------------------ */

/* Starts flat at the exact same warm color Playback ends on (#1b130b) and
   holds it — no gradient motion at all — through the first 40%, then eases
   to black gradually over the back half. Matched with .testi-section
   holding that same flat color through ITS last 35%, so there's a long
   flat, identical band straddling the seam on both sides: nothing for a
   visible line to form out of, by construction. */
.contact-section {
  position: relative;
  overflow: hidden;
  padding-top: var(--sp-6);
  background: linear-gradient(180deg, #1b130b 0%, #1b130b 40%, var(--bg) 90%, var(--bg) 100%);
}

/* A little extra room under the Playback deck on desktop, so the lead-in row
   of stills can sit entirely below the testimonial cards and their controls
   instead of behind them (see initContactSheet in main.js). */
@media (min-width: 701px) {
  .testi-section { padding-bottom: calc(var(--sp-9) + 3.5rem); }
}

.contact-inner .section-eyebrow { width: fit-content; }

/* ---- Contact-sheet collage (easy to remove — see the HTML comment above
   .contact-collage in index.html for the full undo instructions) ---- */
.contact-collage {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  transition: opacity 500ms ease;
}

/* Recedes further the moment someone actually starts filling the form —
   paired with the glow switching on behind the form itself (see
   .contact-form::before) so attention shifts to the form specifically on
   interaction, not just passively. */
.contact-section.is-form-focused .contact-collage {
  opacity: 0.45;
}

/* Sits above the scattered frames, below the real content. Two layers: a
   horizontal one darkest over the form column (left ~65% of the section,
   matching .contact-grid's own split) so the collage recedes furthest
   exactly where someone needs to focus to type, and a vertical one that
   picks up the warm tone Playback hands off (see .contact-section's own
   background) instead of cutting straight to neutral black — the collage
   sitting on top must not un-do that gradual handoff. */
.contact-collage::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Vertical stops mirror .contact-section's own background exactly (warm
     hold to 40%, settled by 90%) — this scrim sits on top of that gradient,
     so if the two ever drift out of sync the scrim's neutral tone wins and
     quietly cancels the slow fade underneath, which is exactly what caused
     the "sudden change" bug. Keep these two in lockstep. */
  background:
    linear-gradient(90deg, rgba(10,9,8,0.7) 0%, rgba(10,9,8,0.4) 55%, rgba(10,9,8,0.22) 100%),
    linear-gradient(180deg, rgba(27,19,11,0.4) 0%, rgba(27,19,11,0.4) 40%, rgba(10,9,8,0.85) 90%, rgba(10,9,8,0.92) 100%);
  /* The two gradients above are full-strength the instant this element
     starts — fine for their own internal blend, but Playback (the section
     just above) carries no scrim at all, so the jump from "no scrim" to
     "full scrim" at y=0 read as a hard line even though the base colors
     matched exactly. Masking the whole scrim's opacity up from zero over
     the first ~14% papers over that jump with a continuous ramp instead. */
  mask-image: linear-gradient(180deg, transparent 0%, black 14%, black 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, black 14%, black 100%);
}

/* One contact-sheet still: a full 16:9 frame (nothing cropped — the img is
   shown at its own ratio, never object-fit: cover) with a slim print border
   and a mono edge-code strip underneath, like the margin of a contact sheet.
   Position and tilt are set per frame by initContactSheet (main.js), which
   places every frame inside whatever free space is left around the real
   content; --w is the frame width, --r its tilt. */
.collage-frame {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--w, 200px);
  margin: 0;
  z-index: 1;
  transform: rotate(var(--r, 0deg));
  background: #0c0a08;
  border: 1px solid rgba(242,238,230,0.14);
  border-radius: 2px;
  overflow: hidden;
  box-shadow: 0 20px 40px rgba(0,0,0,0.5);
  opacity: 0.35;
  line-height: 0;
  transition: left 600ms var(--ease-out), top 600ms var(--ease-out);
}

.collage-frame img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  filter: grayscale(0.9) sepia(0.08) contrast(1.1) brightness(0.85);
}

.collage-frame figcaption {
  display: flex;
  justify-content: space-between;
  height: 16px;
  padding: 0 7px;
  align-items: center;
  font-family: var(--font-mono);
  font-size: 0.5rem;
  letter-spacing: 0.14em;
  line-height: 1;
  color: rgba(242,238,230,0.42);
  border-top: 1px solid rgba(242,238,230,0.08);
}

@media (max-width: 700px) {
  /* The scattered layout needs real width to breathe — on a phone it just
     reads as clutter behind the form, so it steps back to the plain
     gradient background instead of shrinking in place. */
  .contact-collage { display: none; }
}

.contact-inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

.contact-headline {
  position: relative;
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: clamp(2.4rem, 8vw, 6rem);
  line-height: 0.98;
  letter-spacing: -0.01em;
  max-width: 900px;
  /* Shrink-wrapped to its text so its box is the headline's true extent —
     initContactSheet treats that box as occupied space. */
  width: fit-content;
}

/* "watching" now reads as plain text, matching the rest of the headline —
   the interactive cursor color-reveal (see .heading-echo) carries the
   section's personality instead of a static outline treatment. */

.contact-email {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--font-mono);
  font-size: clamp(0.95rem, 1.8vw, 1.1rem);
  color: var(--fg);
  border-bottom: 1px solid var(--border-strong);
  padding-bottom: var(--sp-2);
  width: fit-content;
  transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

.contact-email:hover { color: var(--amber-bright); border-color: var(--amber-bright); }

.contact-email svg { width: 18px; height: 18px; transition: transform var(--dur-med) var(--ease-out); }
.contact-email:hover svg { transform: translate(3px, -3px); }

/* ------------------------------ Contact form ------------------------------ */

.contact-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-7);
  margin-top: var(--sp-6);
}

@media (min-width: 950px) {
  .contact-grid { grid-template-columns: 1.35fr 0.65fr; align-items: start; }
}

.contact-form {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* A soft glow switches on behind the form once you start filling it —
   the other half of "spotlight the form," alongside the collage dimming
   further at the same moment (see .contact-section.is-form-focused). */
.contact-form::before {
  content: '';
  position: absolute;
  inset: -40px -60px;
  z-index: -1;
  background: radial-gradient(ellipse at 30% 15%, rgba(209,163,74,0.06), transparent 65%);
  opacity: 0;
  transition: opacity 700ms ease;
  pointer-events: none;
}

.contact-section.is-form-focused .contact-form::before {
  opacity: 1;
}

/* Deliberately uneven spans (4+2, then 2+2+2) instead of a uniform 2-up
   grid on every row — the even-columns-everywhere pattern is what made this
   read as a generated form rather than a considered one. */
.form-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
}

@media (min-width: 600px) {
  .form-grid { grid-template-columns: repeat(6, 1fr); }
  .field-name { grid-column: span 4; }
  .field-email { grid-column: span 2; }
  .field-type { grid-column: span 2; }
  .field-timeline { grid-column: span 2; }
  .field-budget { grid-column: span 2; }
  .field-details { grid-column: span 6; }
}

.form-field { display: flex; flex-direction: column; gap: 8px; }

.form-field label {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* Wraps each input so the focus scan-line (below) has a bounding box to
   sweep within, clipped to the field's own rounded corners. */
.field-shell {
  position: relative;
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.form-field input,
.form-field select,
.form-field textarea {
  width: 100%;
  /* The one exception to the site-wide custom cursor — typing needs the
     native I-beam to show caret position, which our ring can't convey. */
  cursor: auto;
  font-family: var(--font-body);
  font-size: 0.95rem;
  color: var(--fg);
  background: linear-gradient(160deg, rgba(255,255,255,0.05), rgba(255,255,255,0.015));
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: var(--radius-sm);
  padding: var(--sp-3);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease, background var(--dur-fast) ease;
}

.form-field textarea { resize: vertical; min-height: 110px; line-height: 1.5; }

.form-field select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23948c7e' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--sp-3) center;
  background-size: 16px;
  padding-right: calc(var(--sp-3) + 16px + var(--sp-2));
}

/* Project-type list: dark, legible options, and the "Choose a project type"
   placeholder reads muted until a type is picked. */
.form-field select option,
.form-field select optgroup { background: #14110a; color: var(--fg); }
.form-field select:has(option[value=""]:checked) { color: var(--fg-muted); }

.form-field input::placeholder,
.form-field textarea::placeholder { color: var(--fg-muted); }

.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
  outline: none;
  border-color: rgba(236,195,115,0.55);
  box-shadow: 0 0 0 3px rgba(209,163,74,0.14), inset 0 1px 0 rgba(255,255,255,0.15);
  background: linear-gradient(160deg, rgba(255,255,255,0.07), rgba(255,255,255,0.02));
}

/* Focus glow: "Ambient Settle" — no sweep, no motion. The field just
   cross-fades to a faint warm tint the moment you focus it and holds there
   for as long as you're typing, instead of a one-off flash. Chosen over a
   moving scan-line as the calmest of three options (a slowed sweep and a
   center bloom were the other two) — deliberately the most understated. */
.scan-line {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(160deg, rgba(236,195,115,0.22), rgba(236,195,115,0.06));
  opacity: 0;
  mix-blend-mode: screen;
  transition: opacity 600ms ease;
}

.field-shell:focus-within .scan-line {
  opacity: 1;
}

.form-submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  align-self: flex-start;
  margin-top: var(--sp-2);
  padding: var(--sp-3) var(--sp-6);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bg);
  background: linear-gradient(160deg, rgba(236,195,115,0.95), rgba(209,163,74,0.9));
  border: 1px solid rgba(236,195,115,0.7);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), 0 4px 14px rgba(209,163,74,0.3);
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) ease;
}

.form-submit svg { width: 16px; height: 16px; transition: transform var(--dur-med) var(--ease-out); }

.form-submit:hover {
  transform: translateY(-1px);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), 0 0 20px rgba(209,163,74,0.4), 0 6px 18px rgba(209,163,74,0.3);
}

.form-submit:hover svg { transform: translate(3px, -3px); }

.form-submit:disabled { opacity: 0.6; cursor: none; transform: none; }

.form-note {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  min-height: 1em;
}

.form-note.is-error { color: var(--crimson-bright); }
.form-note.is-success { color: var(--amber-bright); }

/* A bordered, gently-lit card rather than a plain stack of text lists — the
   form is the only thing with visual weight otherwise, which is what made
   this side read flat next to it. */
.contact-aside {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  padding: var(--sp-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background:
    radial-gradient(ellipse at 20% 0%, rgba(209,163,74,0.08), transparent 60%),
    linear-gradient(160deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
}

/* Brass nameplate, not a glass pill — a rectangular plate with a bevel
   (light edge top/left, dark edge bottom/right, via inset shadows instead of
   a flat border) and a soft diagonal sheen standing in for brushed metal.
   Deliberately abstract: no rivets, no texture image — just the shape, the
   bevel, and the engraved text (see .aside-status-text) are what read as
   "plate" rather than the glassy pill used everywhere else on the site. */
.aside-status {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  align-self: flex-start;
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius-sm);
  background: linear-gradient(115deg,
    rgba(154, 118, 61, 0.35) 0%,
    rgba(236, 195, 115, 0.4) 38%,
    rgba(154, 118, 61, 0.28) 60%,
    rgba(94, 70, 34, 0.22) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.25),
    inset 0 -1px 0 rgba(0,0,0,0.35),
    inset 1px 0 0 rgba(255,255,255,0.08),
    inset -1px 0 0 rgba(0,0,0,0.25),
    0 1px 3px rgba(0,0,0,0.3);
}

.aside-status .rec-dot { width: 8px; height: 8px; }

.aside-status-text {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--amber-bright);
  /* Engraved, not embossed: the dark edge falls upper-left (where light is
     blocked by the groove) and the faint highlight lower-right (where it
     catches the far edge) — the reverse of a raised/stamped letter. */
  text-shadow: 1px 1px 0 rgba(255,255,255,0.18), -1px -1px 1px rgba(0,0,0,0.55);
  transition: color var(--dur-fast) ease;
}

/* Flipped by main.js on the form's first focus — the crimson "idle" dot
   (matching the REC tag used elsewhere) switches to a brighter, quicker
   amber pulse, like the badge itself is now actively logging input. */
.aside-status.is-active {
  background: linear-gradient(115deg,
    rgba(154, 118, 61, 0.45) 0%,
    rgba(236, 195, 115, 0.55) 38%,
    rgba(154, 118, 61, 0.35) 60%,
    rgba(94, 70, 34, 0.28) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.3),
    inset 0 -1px 0 rgba(0,0,0,0.35),
    inset 1px 0 0 rgba(255,255,255,0.1),
    inset -1px 0 0 rgba(0,0,0,0.25),
    0 0 12px rgba(209,163,74,0.25),
    0 1px 3px rgba(0,0,0,0.3);
}

.aside-status.is-active .rec-dot {
  background: var(--amber-bright);
  box-shadow: 0 0 10px var(--amber-bright);
  animation-duration: 0.8s;
}

.aside-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--fg-muted);
}

.aside-socials {
  display: flex;
  gap: var(--sp-3);
}

.social-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  color: var(--fg-dim);
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease, background var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
}

.social-icon svg {
  width: 18px;
  height: 18px;
}

.social-icon:hover {
  border-color: var(--amber-bright);
  color: var(--amber-bright);
  background: rgba(209,163,74,0.08);
  transform: translateY(-3px);
}

/* ------------------------------ Footer ------------------------------ */

.site-footer {
  border-top: 1px solid var(--border);
  padding: var(--sp-5) 0;
}

.footer-row {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  align-items: flex-start;
}

@media (min-width: 700px) {
  .footer-row { flex-direction: row; align-items: center; justify-content: space-between; }
}

.footer-row small {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  color: var(--fg-muted);
}

.back-to-top {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-muted);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: color var(--dur-fast) ease;
}

.back-to-top:hover { color: var(--amber-bright); }

/* ------------------------------ Reveal utility (JS hooks) ------------------------------ */

.reveal { opacity: 1; }

/* ------------------------------ Preloader ------------------------------ */
/* "Find / Tell / Feel your story." Big, thin, centred serif on the site's
   near-black; the changing verb is amber italic and rolls through a soft-
   masked slot. A hairline and a mono timecode at the bottom show real
   progress. The exit is a rack-focus reveal: the line lifts and dissolves
   upward while the black veil thins out and its blur pulls from heavy to
   zero, so the hero comes up through frosted glass into focus
   (initPreloader, main.js). */

.preloader {
  position: fixed;
  inset: 0;
  z-index: 999;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
}

.pl-line {
  margin: 0;
  display: flex;
  align-items: flex-start;
  gap: 0.26em;
  font-family: var(--font-serif);
  font-weight: 300;
  font-size: clamp(2.3rem, 6.4vw, 5.4rem);
  line-height: 1.15;
  letter-spacing: -0.012em;
  color: var(--fg);
  white-space: nowrap;
  position: relative;
  z-index: 1;
  /* Held back until the serif has loaded (initPreloader), so the line never
     flashes in a fallback face. */
  opacity: 0;
  filter: blur(12px);
}

.pl-verb {
  display: block;
  overflow: hidden;
  /* Three rows tall with the current word on the middle row: the words
     above and below are only ever visible as they pass through the soft
     fades at the top and bottom, so the roll flows instead of being cropped
     by a hard edge. The vertical margins give the extra rows back so the
     line's own height is still one row. */
  height: 3.45em;
  margin: -1.15em -0.16em -1.15em -0.1em;
  /* Room for the italic's overhang on both sides so no letter is clipped.
     initPreloader adds this padding to each word's width (the box is
     border-box); the equal negative margins keep the visible text exactly
     where it would sit without the padding. */
  padding: 0 0.16em 0 0.1em;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, transparent 29%, #000 36%, #000 60%, transparent 67%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, transparent 29%, #000 36%, #000 60%, transparent 67%, transparent 100%);
}

.pl-verb-track { display: flex; flex-direction: column; }

.pl-verb-track > i {
  /* only the current word is ever opaque (initPreloader crossfades them), so
     no neighbouring word can show through under or over it */
  opacity: 0;
  display: block;
  width: max-content;
  height: 1.15em;
  line-height: 1.15;
  font-style: italic;
  font-weight: 300;
  color: var(--amber-bright);
}

.pl-meter {
  position: absolute;
  left: 50%;
  bottom: clamp(48px, 11vh, 110px);
  transform: translateX(-50%);
  width: min(260px, 56vw);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  z-index: 1;
  opacity: 0;
}

.pl-timecode {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.2em;
  color: var(--fg-muted);
}

.pl-hairline {
  display: block;
  width: 100%;
  height: 1px;
  background: rgba(242,238,230,0.14);
  overflow: hidden;
}

.pl-hairline-fill {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, rgba(236,195,115,0.25), var(--amber-bright));
  transform: scaleX(0);
  transform-origin: left center;
}


/* About: the one pull-quote line. Sits a clear step below the lede (2rem at
   its largest) and a clear step above the body copy, so the section reads
   lede -> quote -> body instead of competing headlines. */
.about-body .about-pull {
  font-family: var(--font-serif);
  font-weight: 500;
  color: var(--fg);
  font-size: clamp(1.1rem, 1.9vw, 1.42rem);
  line-height: 1.45;
  max-width: none;
  margin: var(--sp-4) 0 var(--sp-4);
  padding: 2px 0 2px var(--sp-4);
  border-left: 2px solid var(--amber-dim);
}
.about-body .about-pull em { font-style: italic; color: var(--amber-bright); }
