/* ===========================================================================
   site.css — the page itself.
   ---------------------------------------------------------------------------
   Ordered the way the page is: chrome, hero, work, capabilities, stills,
   quote, about, contact, footer, then the overlays (lightbox, cursor, dock)
   and finally the responsive rules.
   =========================================================================== */

/* ================================================================ CHROME == */

/* -- the middle chapter ----------------------------------------------------
   This used to flip to off-white paper, which is a perfectly good way to cut
   a one-pager into three chapters — but it fought everything else the page is
   doing. Colour only reads as light if there is dark for it to fall into, and
   a bright document in the middle third kept resetting that.

   So the chapter break survives, but it is now made of VALUE rather than
   inversion: a lifted, faintly violet ground instead of a white one. Same
   structural job, and the room stays dark enough to be lit. */

.theme-room {
  background: #0E0C15;
  position: relative;
  isolation: isolate;
}

/* Light sources, one at each end of the chapter, with the testimonial sitting
   in the shadow between them. This is the show's actual grammar — a warm
   practical on one side, a cold one on the other, and the subject lit from
   both — rather than a colour wash laid over the whole surface. */
.caps::before,
.about::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

.caps::before {
  background: radial-gradient(58% 70% at 8% 0%, rgba(104, 26, 116, 0.34), transparent 68%);
}

.about::before {
  background: radial-gradient(56% 72% at 96% 100%, rgba(38, 84, 116, 0.34), transparent 68%);
}

/* -- nav ------------------------------------------------------------------
   A name and one link. A five-item navigation on a one-page site is
   decoration that costs a real click target at the top of the page. */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: clamp(1rem, 2.2vw, 1.75rem) var(--gutter);
  mix-blend-mode: normal;
  pointer-events: none;
  transition: color var(--d-mid) var(--ease-hk);
  color: var(--bone);
}

.nav > * { pointer-events: auto; }

.nav__name { font-weight: 500; color: currentColor; }
.nav__cta {
  position: relative;
  color: currentColor;
  padding-bottom: 2px;
}
.nav__cta::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--d-fast) var(--ease-hk);
}
.nav__cta:hover::after { transform: scaleX(1); transform-origin: left; }

html.is-light .nav { color: var(--ink); }

/* -- scroll progress ------------------------------------------------------
   Native CSS scroll-driven animation. No JavaScript at all, and in browsers
   that don't support it the bar simply stays at zero width and is invisible. */

.progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: calc(var(--z-nav) + 1);
  pointer-events: none;
}

/* Carries the chroma too, at 2px. It spans the full width of the page as you
   read, so the palette is quietly present the whole way down without ever
   occupying area. */
.progress__bar {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--chroma);
  transform-origin: 0 50%;
  transform: scaleX(0);
}

@supports (animation-timeline: scroll()) {
  .progress__bar {
    animation: hk-progress linear both;
    animation-timeline: scroll(root block);
  }
  @keyframes hk-progress {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
}

/* -- grain ----------------------------------------------------------------
   One of only two blended layers on the page. A third turns footage to mud
   and costs real fill-rate over full-screen video. */

.grain {
  position: fixed;
  inset: -50%;
  z-index: var(--z-grain);
  pointer-events: none;
  /* Fine and faint. Film grain is a texture you feel rather than see — at
     any strength where you can actually make out the speckles it stops
     reading as emulsion and starts reading as a broken JPEG over her work.
     Nudged up from 0.13: the reference look wears its grain openly, and at
     the old value it read as a clean digital page with a rumour of texture. */
  opacity: 0.19;
  mix-blend-mode: overlay;
  background-size: 190px 190px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='190' height='190'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.7' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='190' height='190' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  will-change: transform;
  animation: hk-grain 0.7s steps(1) infinite;
}

@keyframes hk-grain {
  0%   { transform: translate3d(0, 0, 0); }
  25%  { transform: translate3d(-3%, 2%, 0); }
  50%  { transform: translate3d(2%, -3%, 0); }
  75%  { transform: translate3d(-2%, -2%, 0); }
  100% { transform: translate3d(3%, 1%, 0); }
}

/* -- letterbox ------------------------------------------------------------
   Desktop only. On a phone these bars would eat 15% of the viewport for
   atmosphere, which is a bad trade on the device where space is scarcest. */

.bars {
  position: fixed;
  inset: 0;
  z-index: var(--z-sticky);
  pointer-events: none;
}
.bars i {
  position: absolute;
  left: 0; right: 0;
  height: 0;
  background: var(--ink);
  transition: height var(--d-slow) var(--ease-hk);
}
.bars__top { top: 0; }
.bars__btm { bottom: 0; }
html.is-cinema .bars i { height: clamp(18px, 4.2vh, 54px); }

/* Retraction is driven from motion.js now (see the .caps trigger). The old
   rule keyed off the light chapter, which the page no longer has. */

/* ================================================================== HERO == */

.hero {
  position: relative;
  background: var(--ink);
}

.hero__pin {
  position: relative;
  height: 100svh;
  min-height: 34rem;
  overflow: hidden;
}

/* Media sits behind everything and is always full-bleed. */
.hero__media,
.hero__media .frame__media {
  position: absolute;
  inset: 0;
  aspect-ratio: auto !important;
}

/* The chroma fill.

   Painted in pure CSS, so it is there on the very first frame — no waiting on
   JavaScript, and nothing to download.

   Oil-slick, not rainbow, and dusty rather than neon. It stays inside one
   narrow arc of the wheel — teal-green, through slate blue, into deep plum —
   at low saturation and low value, so it reads as light pooling in a dark
   room. Warm hues are deliberately absent: the moment yellow, orange or pink
   enter, it stops reading as petrol on wet tarmac and starts reading as a
   spectrum. Vibrance is the other trap; pushed bright it turns to neon and
   loses the film reference entirely.

   Built from overlapping soft blooms rather than a linear sweep: a sweep
   bands, and visible banding is what makes a gradient look like a gradient
   instead of like light. Everything sits over a violet-black rather than a
   neutral one, which is what keeps the shadows coloured. */
/* Positions matter more than colours here. The wordmark sits in the middle
   third of the viewport, so blooms parked in the corners fade out exactly
   where the letters are and the fill goes muddy — they are pulled inward to
   sit under the type, and overlap generously so the hue shifts across a
   letter rather than between letters. */
.hero__media {
  background:
    radial-gradient(62% 62% at 15% 22%, rgba(26, 108, 94, 0.92), transparent 70%),
    radial-gradient(64% 60% at 72% 24%, rgba(48, 68, 108, 0.92), transparent 70%),
    radial-gradient(58% 56% at 90% 46%, rgba(34, 84, 96, 0.70), transparent 68%),
    radial-gradient(70% 64% at 38% 84%, rgba(104, 26, 116, 0.94), transparent 72%),
    radial-gradient(58% 56% at 86% 84%, rgba(78, 30, 104, 0.86), transparent 70%),
    radial-gradient(62% 52% at 50% 54%, rgba(52, 44, 92, 0.60), transparent 74%),
    #12112A;
}

/* Scanlines. Fine enough to read as a signal rather than as stripes — this is
   the layer that stops the gradient looking like a flat wash. */
.hero__media::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0) 0 2px,
    rgba(0, 0, 0, 0.22) 2px 3px
  );
}

/* Video grain, and the reason the fill reads as footage rather than as CSS.
   Deliberately heavier than the page-wide .grain layer: this one is only ever
   seen through the letterforms, where a texture that is merely felt elsewhere
   needs to be plainly visible. Stepped, not eased, so it chatters the way
   emulsion does instead of breathing. */
.hero__media::after {
  content: '';
  position: absolute;
  inset: -35%;
  pointer-events: none;
  /* soft-light, not overlay: overlay barely moves a near-black base, so the
     grain vanished into the very shadows it is supposed to sit in. */
  opacity: 0.72;
  mix-blend-mode: soft-light;
  background-size: 150px 150px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='hn'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='1.6' intercept='-0.18'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23hn)'/%3E%3C/svg%3E");
  will-change: transform;
  animation: hk-heroGrain 0.55s steps(1) infinite;
}

@keyframes hk-heroGrain {
  0%   { transform: translate3d(0, 0, 0); }
  20%  { transform: translate3d(-4%, 3%, 0); }
  40%  { transform: translate3d(3%, -4%, 0); }
  60%  { transform: translate3d(-3%, -2%, 0); }
  80%  { transform: translate3d(4%, 2%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .hero__media::after { animation: none; }
}

.hero__media .frame__poster,
.hero__media .hk-preview,
.hero__media .hk-placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The wordmark knockout. An opaque sheet the colour of the page covers the
   footage, with her name punched through it. Scrolling scales the name up
   until the hole is larger than the screen and the film is simply playing. */

.hero__cut {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
}

.hero__cover { fill: var(--ink); }

/* Two nested groups, deliberately.

   The INNER one carries the load-in beat as a plain CSS animation, so it
   always completes — even if requestAnimationFrame is throttled, even if
   GSAP never loads, even in a background tab. Essential content is never
   left depending on a tween finishing.

   The OUTER one is what GSAP scrubs on scroll. Keeping them separate also
   avoids a genuinely nasty conflict: GSAP writes `transform-origin` in
   user units computed from the element's bounding box, which `transform-box:
   view-box` then reinterprets against the viewBox instead — and the wordmark
   drifts off centre as it scales. */

.hero__cutGroup {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: hk-cut-in 1.6s var(--ease-out-expo) both;
}

@keyframes hk-cut-in {
  from { transform: scale(0.87); }
  to   { transform: scale(1); }
}

/* Light spilling out of the letters.

   A twin of the wordmark drawn AFTER the cover sheet, because anything
   painted before it is hidden by it.

   The twin is STROKED, not filled. A filled copy sits on top of the letters
   and screens them into a flat wash, erasing the teal-to-plum gradient the
   whole hero is built on; an outline rides the letter edge instead, so the
   glow hugs the forms and leaves their centres clear. That is also how neon
   actually behaves: bright at the rim, hollow in the middle.

   Blur softens the stroke into light, and the two drop-shadows carry the
   spill outwards in stages so the falloff is fast near the letter and slow
   further out. Screen-blended, so it only ever adds light. */
.hero__glow {
  /* One shadow, not two. Each pass is re-rasterised on every scroll frame
     while GSAP scales this group, so passes are the thing to spend
     sparingly.

     Referenced as url(), NOT as blur()/drop-shadow(). WebKit ignores CSS
     shorthand filters on SVG children entirely — see the <filter> definition
     in index.html. Chromium honours both, so url() is the one form that
     works in both engines. */
  filter: url(#hk-glow);
  opacity: 0.62;
  mix-blend-mode: screen;
  pointer-events: none;
}

.hero__glow .hero__cutText {
  fill: none;
  stroke: #7C88E4;
  stroke-width: 3;
  stroke-linejoin: round;
}

@media (prefers-reduced-motion: reduce) {
  .hero__glow { opacity: 0.66; }
}

.hero__cutText {
  font-family: var(--font-display);
  font-weight: 900;     /* Inter Black */
  font-size: clamp(3.1rem, 15.2vw, 14rem);
  letter-spacing: -0.045em;
  dominant-baseline: central;
}

/* Hero UI sits above the knockout. */
.hero__ui {
  position: absolute;
  inset: auto var(--gutter) clamp(1.5rem, 4vh, 3rem) var(--gutter);
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: clamp(1rem, 2.4vh, 1.75rem);
  align-items: flex-start;
}

.hero__role {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  color: var(--bone);
  flex-wrap: wrap;
}
.hero__sep {
  display: inline-block;
  width: 1.75rem;
  height: 1px;
  background: currentColor;
  opacity: 0.4;
}

.hero__actions {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  flex-wrap: wrap;
}

.hero__hint {
  opacity: 0.55;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
}
.hero__hint::after {
  content: '';
  width: 1px;
  height: 1.6rem;
  background: currentColor;
  animation: hk-hint 2.4s var(--ease-hk) infinite;
  transform-origin: top;
}
@keyframes hk-hint {
  0%, 100% { transform: scaleY(0.3); opacity: 0.3; }
  50%      { transform: scaleY(1); opacity: 1; }
}

/* -- buttons -------------------------------------------------------------- */

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.85rem 1.6rem;
  border-radius: 100px;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  border: 1px solid var(--line);
  color: var(--bone);
  overflow: hidden;
  transition: color var(--d-fast) var(--ease-hk), border-color var(--d-fast) var(--ease-hk);
}

.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bone);
  transform: translate3d(0, 101%, 0);
  transition: transform var(--d-mid) var(--ease-hk);
  z-index: -1;
}
.btn:hover::before { transform: translate3d(0, 0, 0); }
.btn:hover { color: var(--ink); border-color: var(--bone); }

.btn__label { position: relative; }
.btn__meta { opacity: 0.6; position: relative; }

.btn--play { background: rgba(237, 235, 230, 0.04); }
.btn--ghost { background: transparent; }

/* ================================================================= PROOF == */

.proof {
  background: var(--ink);
  border-block: 1px solid var(--line-soft);
  padding-block: clamp(0.9rem, 2vh, 1.4rem);
  overflow: hidden;
}

.marquee { overflow: hidden; }

.marquee__track {
  display: flex;
  width: max-content;
  will-change: transform;
}

.marquee__run {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  padding-right: clamp(1.5rem, 4vw, 3.5rem);
  flex: none;
}

.marquee__item {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.1rem, 2.4vw, 2rem);
  letter-spacing: -0.02em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--bone-dim);
}

.marquee__dot { color: var(--signal); font-size: 0.7em; }

/* ================================================================== WORK == */

.work { background: var(--ink); }

.work__head {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin-bottom: clamp(2.5rem, 7vh, 5rem);
}

/* -- the stack ------------------------------------------------------------
   Two featured films as sticky cards. `position: sticky` does the whole
   effect with no JavaScript and no pin, which is exactly why it earns its
   place: it buys a held, physical moment for zero bytes and it degrades to
   a plain stacked layout wherever sticky isn't supported. */

.stack {
  padding-inline: var(--gutter);
  max-width: var(--maxw);
  margin-inline: auto;
  margin-bottom: clamp(3rem, 9vh, 7rem);
}

.stack__card {
  position: sticky;
  top: clamp(4.5rem, 12vh, 8rem);
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center;
  padding: clamp(1rem, 2.5vw, 2rem);
  margin-bottom: clamp(2rem, 6vh, 4rem);
  background: var(--ink-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-lg);
}

.frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--ink-3);
}

.stack__card .frame__media {
  aspect-ratio: 16 / 9 !important;
  position: relative;
  overflow: hidden;
  width: 100%;
  background: var(--ink-3);
}

.frame__poster,
.hk-preview,
.hk-placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The generated placeholder is rendered tiny and stretched, which is both
   nearly free and reads as convincingly defocused rather than as low-res. */
.hk-placeholder {
  filter: blur(0.5px) saturate(1.15);
  transform: scale(1.04);
  opacity: 0;
  animation: hk-fade-in 1.2s var(--ease-hk) 0.15s forwards;
}
@keyframes hk-fade-in { to { opacity: 1; } }

.hk-preview { opacity: 0; transition: opacity var(--d-mid) var(--ease-hk); }
.hk-preview[data-playing] { opacity: 1; }

/* Counter-parallax: the image drifts slightly against its frame as the card
   travels. The only parallax that is always worth having, because it reads
   as depth rather than as an effect. */
.frame__poster { will-change: transform; }

.stack__hit {
  position: absolute;
  inset: 0;
  z-index: 3;
  cursor: pointer;
}

.stack__body { display: flex; flex-direction: column; gap: 1rem; }
.stack__eyebrow { display: flex; align-items: center; gap: 0.7rem; }
.stack__sep {
  display: inline-block;
  width: 0.9rem;
  height: 1px;
  background: currentColor;
  opacity: 0.45;
  margin-inline: 0.35rem;
  vertical-align: middle;
}
.stack__title {
  font-size: var(--t-h2);
  font-family: var(--font-display);
  font-weight: 900;
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-head);
  text-transform: uppercase;
}
.stack__blurb { color: var(--bone-dim); max-width: 40ch; }
.stack__meta { display: flex; align-items: center; flex-wrap: wrap; }

/* -- the index ------------------------------------------------------------
   The remaining eight films as a typographic list. Ten identical thumbnails
   is the single most common reason a videographer's site reads as generic;
   an index reads as an edit. Hovering previews the film beside the cursor. */

.index {
  list-style: none;
  border-top: 1px solid var(--line);
}

.index__row { border-bottom: 1px solid var(--line); }

.index__hit {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr) 12rem 11rem 4rem;
  align-items: baseline;
  gap: 1.5rem;
  width: 100%;
  padding: clamp(1rem, 2.4vh, 1.6rem) 0;
  text-align: left;
  position: relative;
  transition: color var(--d-fast) var(--ease-hk);
}

.index__hit::before {
  content: '';
  position: absolute;
  left: -1rem; right: -1rem; top: 0; bottom: 0;
  background: var(--ink-2);
  opacity: 0;
  transition: opacity var(--d-fast) var(--ease-hk);
  z-index: -1;
  border-radius: var(--r-sm);
}
.index__hit:hover::before { opacity: 1; }

.index__n { color: var(--signal); }

.index__title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.25rem, 2.6vw, 2.1rem);
  line-height: 1.05;
  letter-spacing: var(--tr-head);
  text-transform: uppercase;
  transition: transform var(--d-mid) var(--ease-hk);
}
.index__hit:hover .index__title { transform: translateX(0.6rem); }

.index__year { text-align: right; }

/* Self-initiated work, marked in the column that already holds the client
   name — so the two tracks separate at a glance without spending a column,
   a chip, or a line of copy on it. The accent is doing exactly the job it is
   reserved for: one small mark that means something. */
.index__row[data-kind='personal'] .index__client {
  color: var(--signal);
}

/* The floating preview that follows the cursor across the index. */
.peek {
  position: fixed;
  top: 0; left: 0;
  width: clamp(180px, 17vw, 300px);
  aspect-ratio: 16 / 9;
  z-index: var(--z-raised);
  pointer-events: none;
  border-radius: var(--r-md);
  overflow: hidden;
  opacity: 0;
  transform: translate3d(-50%, -50%, 0) scale(0.9);
  transition: opacity var(--d-fast) var(--ease-hk), transform var(--d-fast) var(--ease-hk);
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
}
.peek.is-on { opacity: 1; transform: translate3d(-50%, -50%, 0) scale(1); }
.peek img { width: 100%; height: 100%; object-fit: cover; }

/* ========================================================== CAPABILITIES == */

.caps__inner {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(2rem, 6vw, 6rem);
  align-items: start;
}

.caps__intro { display: flex; flex-direction: column; gap: 1rem; position: sticky; top: 18vh; }

.caps__list { list-style: none; display: flex; flex-direction: column; }

.caps__item {
  display: grid;
  grid-template-columns: 3rem 1fr;
  gap: 0.4rem 1.5rem;
  padding-block: clamp(1.1rem, 2.6vh, 1.8rem);
  border-top: 1px solid var(--line);
}
.caps__item:last-child { border-bottom: 1px solid var(--line); }

.caps__n { grid-row: span 2; color: var(--bone-faint); padding-top: 0.45rem; }
.caps__name { margin: 0; }
.caps__detail { color: var(--bone-dim); }

/* ================================================================ STILLS == */

.stills { padding-block: var(--section-y); overflow: hidden; }

.stills__head {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin-bottom: clamp(2rem, 6vh, 4rem);
  max-width: 46rem;
}

.stills__viewport { width: 100%; }

.stills__track {
  display: flex;
  gap: clamp(1rem, 2vw, 1.75rem);
  padding-inline: var(--gutter);
  width: max-content;
  will-change: transform;
}

.still {
  position: relative;
  flex: none;
  height: clamp(20rem, 52vh, 34rem);
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--ink-3);
  margin: 0;
}
.still[data-ratio] { aspect-ratio: auto; }
.still[data-ratio='16x9'] { width: calc(clamp(20rem, 52vh, 34rem) * 16 / 9); }
.still[data-ratio='4x5']  { width: calc(clamp(20rem, 52vh, 34rem) * 4 / 5); }
.still[data-ratio='9x16'] { width: calc(clamp(20rem, 52vh, 34rem) * 9 / 16); }

.still__img { width: 100%; height: 100%; object-fit: cover; }

.still__cap {
  position: absolute;
  left: 0.9rem;
  bottom: 0.9rem;
  padding: 0.4rem 0.7rem;
  background: rgba(8, 9, 11, 0.78);
  color: #EDEBE6;
  border-radius: var(--r-sm);
}

.stills__hint {
  margin-top: clamp(1rem, 3vh, 2rem);
  padding-inline: var(--gutter);
  max-width: var(--maxw);
  margin-inline: auto;
  color: var(--bone-faint);
}

/* ================================================================= QUOTE == */

/* The stills run used to sit immediately before this, so the quote could be
   modest and still read as a deliberate beat. With that section gone it is
   the only thing in its chapter, and a small block in a large field reads as
   floating rather than composed — so it runs large to own the space, on
   tighter padding than the default section rhythm.

   Sizing was originally set against a fifteen-character placeholder. The real
   testimonial is 211 characters, which at the old scale ran to roughly eleven
   lines of 88px type, so the ramp is now half what it was. */
.quote { padding-block: clamp(2.75rem, 6.5vh, 5.5rem); }

.quote__body { margin: 0; }

/* No measure cap: the quote spans the full `.wrap` content width, matching the
   capabilities block above it rather than sitting in a narrow column beside a
   field of dead space.

   If a cap is ever reintroduced it has to live on the text itself, not on the
   blockquote: `ch` is relative to the element's OWN font, and the blockquote is
   still at body size. Setting it on the parent silently produced a 260px column
   for 58px type — nineteen lines of pull quote. */
.quote__text {
  max-width: none;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(1rem, 3.2vw, 2.75rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
  margin-bottom: clamp(1.5rem, 3.5vh, 2.5rem);
}

/* Each paragraph is its own block, so a closing line can be given room to land
   instead of running on from the sentence before it. The gap is in `em` so it
   tracks the clamped font size rather than fighting it. */
.quote__para { display: block; }
.quote__para + .quote__para { margin-top: 0.6em; }

/* Each word starts dim and brightens as the line scrubs past. It makes a
   sentence people would otherwise skip genuinely hard to skip. */
.quote__word { color: var(--bone-faint); transition: color 0.2s linear; }
.quote__word.is-lit { color: var(--bone); }

.quote__by { display: flex; align-items: center; flex-wrap: wrap; }

/* ================================================================= ABOUT == */

.about__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}

.about__portrait {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--ink-3);
  /* The source frame is 16:25 — tall enough that it dominated the section as
     a towering slab. 4:5 is what the config asks for anyway, and it lets the
     copy beside it hold its own. */
  aspect-ratio: 4 / 5;
}
.about__img { width: 100%; height: 100%; object-fit: cover; }

/* Practicals falling across the frame, same grammar as the film cards: warm
   off one shoulder, cold off the other. `screen` because this is light being
   ADDED to the image, not a tint laid over it — a tint greys the skin, light
   keeps it. Scoped past the placeholder state, which owns ::after already. */
.about__portrait:not(.about__portrait--empty)::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background:
    radial-gradient(62% 55% at 100% 6%, rgba(150, 58, 168, 0.26), transparent 62%),
    radial-gradient(58% 55% at 0% 100%, rgba(40, 96, 140, 0.24), transparent 62%);
}

.about__portrait--empty::after {
  content: 'Portrait goes here — see README';
  position: absolute;
  inset: auto 0 0 0;
  padding: 0.7rem;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #EDEBE6;
  background: rgba(8, 9, 11, 0.6);
}

.about__body { display: flex; flex-direction: column; gap: 1.25rem; }
.about__copy { display: flex; flex-direction: column; gap: 1rem; }
/* This block is set in Inter 400 like every other paragraph on the site. What
   used to make it read as a different voice was that it alone changed colour
   AND weight from one paragraph to the next: bright, then dim, then bold 700.
   Nothing else on the page does that, so the eye read it as a foreign object
   rather than as emphasis. Line-height now matches body copy too. */
.about__p { font-size: var(--t-lead); line-height: 1.55; max-width: 46ch; }
.about__p:first-child { color: var(--bone); }
.about__p ~ .about__p { color: var(--bone-dim); }
/* The closing line still lands, but on colour rather than a weight jump. */
.about__p:last-child { font-weight: 500; color: var(--bone); }

.about__kit {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}
.about__kitItem {
  padding: 0.45rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 100px;
  color: var(--bone-dim);
}

/* =============================================================== CONTACT == */

.contact {
  background: var(--ink);
  min-height: 88svh;
  display: flex;
  align-items: center;
}

.contact__inner { display: flex; flex-direction: column; gap: 1.1rem; align-items: flex-start; }

.contact__email {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(1.5rem, 6.4vw, 5.5rem);
  /* Was 1.02, which cropped the descenders. `background-clip: text` paints
     the gradient only inside the element's own box, so the tails of g, j and
     @ fell outside it and had nothing to paint. The box has to be tall enough
     to contain the glyphs, not just the line. */
  line-height: 1.18;
  letter-spacing: -0.035em;
  text-transform: none;
  margin-top: clamp(1rem, 3vh, 2rem);
  /* Same reason: a little room below the baseline so the deepest descender
     still sits on painted background. */
  padding-bottom: 0.08em;
  position: relative;
  display: inline-block;
  word-break: break-word;
  transition: color var(--d-fast) var(--ease-hk);
}

/* The page's second and last chroma moment. Her name opens it and her email
   closes it, both in the same light — which is what makes the colour read as
   a system rather than as decoration. Everything between them stays neutral,
   because a third instance would spend what the bookend is worth.

   Guarded: without background-clip support the transparent fill would leave
   the address invisible, so the plain bone text is the fallback. */
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .contact__email {
    background-image: var(--chroma);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

.contact__email::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0.16em;
  height: 2px;
  /* Not currentColor — the text is deliberately transparent above, so an
     underline inheriting from it would never paint. */
  background: var(--bone);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--d-mid) var(--ease-hk);
}
.contact__email:hover::after { transform: scaleX(1); }

.contact__phone a { color: var(--bone-dim); }
.contact__phone a:hover { color: var(--bone); }

/* ================================================================ FOOTER == */

.foot {
  background: var(--ink);
  border-top: 1px solid var(--line-soft);
  padding-block: clamp(1.5rem, 4vh, 2.5rem);
  padding-bottom: calc(clamp(1.5rem, 4vh, 2.5rem) + env(safe-area-inset-bottom, 0px));
}

.foot__grid {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}

.foot__dot {
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--signal);
  margin-right: 0.6rem;
  animation: hk-pulse 2.4s var(--ease-hk) infinite;
}
@keyframes hk-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }

.foot__social { display: flex; gap: 1.5rem; list-style: none; }
.foot__social a { color: var(--bone-dim); transition: color var(--d-fast); }
.foot__social a:hover { color: var(--signal); }

/* ================================================================== DOCK == */
/* Most people never reach the contact section. This is what actually
   converts: two items, always reachable, never a navigation. */

.dock {
  position: fixed;
  right: var(--gutter);
  bottom: calc(clamp(1rem, 3vh, 2rem) + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-sticky);
  display: flex;
  gap: 0.6rem;
  opacity: 0;
  visibility: hidden;
  transform: translate3d(0, 1.5rem, 0);
  transition: opacity var(--d-mid) var(--ease-hk), transform var(--d-mid) var(--ease-hk), visibility 0s linear var(--d-mid);
}

.dock.is-on {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

.dock__btn {
  padding: 0.75rem 1.25rem;
  border-radius: 100px;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  background: #0D0F12;
  border: 1px solid var(--line);
  color: #EDEBE6;
  transition: background var(--d-fast) var(--ease-hk), color var(--d-fast) var(--ease-hk);
}
.dock__btn:hover { background: #EDEBE6; color: #08090B; }
.dock__btn--primary { background: var(--bone); color: var(--ink); border-color: var(--bone); }
.dock__btn--primary:hover { background: #EDEBE6; }

/* ================================================================ CURSOR == */
/* Desktop, fine pointer only. Its value is the contextual label — a ring
   that says PLAY over a film is worth more than a ring that says nothing. */

.cursor { display: none; }

html.has-fine-pointer .cursor {
  display: block;
  position: fixed;
  top: 0; left: 0;
  z-index: var(--z-cursor);
  pointer-events: none;
  mix-blend-mode: normal;
  /* Hidden until the pointer actually moves. Otherwise it sits parked in the
     middle of the hero on load, which reads as a rendering artefact. */
  opacity: 0;
  transition: opacity var(--d-fast) var(--ease-hk);
}

html.cursor-ready .cursor { opacity: 1; }

.cursor__dot,
.cursor__ring,
.cursor__label {
  position: absolute;
  top: 0; left: 0;
  transform: translate3d(-50%, -50%, 0);
}

.cursor__dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--signal);
}

.cursor__ring {
  width: 38px; height: 38px;
  border-radius: 50%;
  border: 1px solid rgba(237, 235, 230, 0.4);
  transition: width var(--d-fast) var(--ease-hk), height var(--d-fast) var(--ease-hk),
              background var(--d-fast) var(--ease-hk), border-color var(--d-fast) var(--ease-hk);
}

.cursor__label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0;
  transition: opacity var(--d-fast) var(--ease-hk);
  white-space: nowrap;
}

/* Neutral, not accent. The accent is sized for a 6px dot and a 2px rule; blown
   up to a 76px disc under the pointer it stops being a mark and becomes a
   field, which is far too much colour for a page whose whole argument is that
   the footage supplies the colour. Bone reads as a lens flag instead. */
html.cursor-active .cursor__ring {
  width: 76px; height: 76px;
  background: var(--bone);
  border-color: var(--bone);
}
html.cursor-active .cursor__label { opacity: 1; }
html.cursor-active .cursor__dot { opacity: 0; }

html.is-light .cursor__ring { border-color: rgba(8, 9, 11, 0.35); }

/* ============================================================== LIGHTBOX == */

.hk-lb {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  /* Belt to the `[hidden]` reset's braces: during the ~320ms fade-out the
     element is still displayed, and an invisible full-viewport layer at
     z-index 9500 would otherwise eat every click aimed at the page beneath. */
  pointer-events: none;
  transition: opacity var(--d-fast) var(--ease-hk);
}
.hk-lb.is-open { opacity: 1; pointer-events: auto; }

.hk-lb__scrim {
  position: absolute;
  inset: 0;
  background: rgba(4, 5, 6, 0.97);
}

.hk-lb__shell {
  position: relative;
  width: min(100%, 82rem);
  max-height: 100dvh;
  padding: clamp(3.5rem, 8vh, 5rem) var(--gutter) clamp(1.5rem, 4vh, 2.5rem);
  display: flex;
  flex-direction: column;
  gap: clamp(1rem, 2.5vh, 1.75rem);
  transform: scale(0.97);
  transition: transform var(--d-mid) var(--ease-hk);
}
.hk-lb.is-open .hk-lb__shell { transform: scale(1); }

.hk-lb__bar {
  position: absolute;
  inset: clamp(1rem, 3vh, 1.75rem) var(--gutter) auto var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.hk-lb__counter {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--tr-label);
  color: var(--bone-faint);
}

.hk-lb__close {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--bone-dim);
  transition: color var(--d-fast);
}
.hk-lb__close:hover { color: var(--signal); }
.hk-lb__close svg { width: 16px; height: 16px; }

.hk-lb__frame {
  position: relative;
  width: 100%;
  background: #000;
  border-radius: var(--r-md);
  overflow: hidden;
  max-height: 72dvh;
}
.hk-lb__frame[data-ratio='16x9'] { aspect-ratio: 16 / 9; }
.hk-lb__frame[data-ratio='9x16'] { aspect-ratio: 9 / 16; max-width: 40dvh; margin-inline: auto; }
.hk-lb__frame[data-ratio='4x5']  { aspect-ratio: 4 / 5;  max-width: 58dvh; margin-inline: auto; }

.hk-player { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.hk-lb__empty {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  text-align: center;
  padding: var(--gutter);
  background: var(--ink-2);
}
.hk-lb__emptyTitle {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-h3);
  text-transform: uppercase;
  letter-spacing: var(--tr-head);
}
.hk-lb__emptyBody { color: var(--bone-dim); max-width: 34ch; }
.hk-lb__emptyBody code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  color: var(--signal);
}

.hk-lb__meta { display: flex; flex-direction: column; gap: 0.75rem; }
.hk-lb__title {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--t-h3);
  text-transform: uppercase;
  letter-spacing: var(--tr-head);
  margin: 0;
}
.hk-lb__facts { display: flex; flex-wrap: wrap; gap: 0.5rem 2rem; margin: 0; }
.hk-lb__fact dt {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--bone-faint);
  margin-bottom: 0.15rem;
}
.hk-lb__fact dd { margin: 0; font-size: var(--t-small); color: var(--bone); }
.hk-lb__blurb { color: var(--bone-dim); max-width: 56ch; font-size: var(--t-small); }

.hk-lb__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  color: var(--bone-dim);
  background: rgba(13, 15, 18, 0.6);
  transition: color var(--d-fast), border-color var(--d-fast);
}
.hk-lb__nav:hover { color: var(--signal); border-color: var(--signal); }
.hk-lb__nav svg { width: 18px; height: 18px; }
.hk-lb__nav--prev { left: 0.5rem; }
.hk-lb__nav--next { right: 0.5rem; }

html.hk-lb-open { overflow: hidden; }

/* ============================================================== NOSCRIPT == */

.noscript {
  padding: var(--gutter);
  margin: var(--gutter);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--ink-2);
}
.noscript a { color: var(--signal); text-decoration: underline; }

/* ============================================================ RESPONSIVE == */

@media (max-width: 1100px) {
  .index__hit { grid-template-columns: 3rem minmax(0, 1fr) 9rem 4rem; }
  .index__role { display: none; }
}

@media (max-width: 900px) {
  .stack__card { grid-template-columns: 1fr; }
  .caps__inner,
  .about__grid { grid-template-columns: 1fr; }
  .caps__intro { position: static; }
  .about__portrait { max-width: 22rem; }
}

@media (max-width: 768px) {
  /* No pinning, no letterbox, no cursor. Everything below is chosen so the
     phone version is a genuinely good page rather than a degraded one. */

  .hero__pin { height: 100svh; }

  /* The wordmark carries the whole hero on a phone, where there is no scrub
     to open it. Push it larger than the desktop ratio so it still lands. */
  .hero__cutText { font-size: 19vw; letter-spacing: -0.05em; }

  .hero__ui { gap: 1rem; }

  .stack__card {
    position: static;
    padding: 0.9rem;
    background: transparent;
    border: 0;
    border-bottom: 1px solid var(--line-soft);
    border-radius: 0;
    padding-bottom: 2rem;
  }

  .index__hit {
    grid-template-columns: 2.5rem minmax(0, 1fr) 3.5rem;
    gap: 0.75rem;
    row-gap: 0.25rem;
    align-items: center;
  }
  .index__client { grid-column: 2; font-size: 0.6875rem; }
  .index__year { grid-column: 3; grid-row: 1; }
  .index__title { grid-column: 2; grid-row: 1; }
  .index__n { grid-row: 1; }
  .index__hit:hover .index__title { transform: none; }

  /* Native swipe beats any script here. */
  .stills__viewport {
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .stills__viewport::-webkit-scrollbar { display: none; }
  .stills__track { width: max-content; }
  .still { scroll-snap-align: center; height: 60vw; max-height: 24rem; }
  .stills__hint { display: none; }

  .dock {
    left: var(--gutter);
    right: var(--gutter);
    bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
  }
  .dock__btn { flex: 1; text-align: center; }

  .hk-lb__nav { display: none; }
  .hk-lb__shell { padding-inline: 1rem; }

  .grain { opacity: 0.35; }
}

@media (max-width: 480px) {
  .hero__actions { width: 100%; }
  .btn { padding: 0.75rem 1.1rem; }
  .foot__grid { flex-direction: column; align-items: flex-start; gap: 1rem; }
}

/* Coarse pointer: kill hover-only affordances that would otherwise stick. */
@media (hover: none) {
  .index__hit:hover::before { opacity: 0; }
  .btn:hover::before { transform: translate3d(0, 101%, 0); }
  .btn:hover { color: var(--bone); border-color: var(--line); }
}

/* -------------------------------------------------------- reduced motion -- */

@media (prefers-reduced-motion: reduce) {
  /* The cursor never initialises under reduced motion, so remove it outright
     rather than leaving an inert element in the layer tree. */
  html.has-fine-pointer .cursor { display: none; }
  .grain { animation: none; }
  .hero__hint::after { animation: none; transform: scaleY(1); opacity: 0.6; }
  .foot__dot { animation: none; }
  .hk-placeholder { animation: none; opacity: 1; }
  .marquee__track { transform: none !important; }
  .progress__bar { animation: none; }
}

/* ================================================================= PRINT ==
   Forty lines of cheap credibility. Someone will print this — a producer
   building a shortlist deck, or Heather herself sending a one-sheet — and a
   portfolio that prints as a black rectangle with no film titles on it reads
   as carelessness. Printed, it should be a usable index of the work. */

@media print {
  *, *::before, *::after {
    background: #fff !important;
    color: #000 !important;
    box-shadow: none !important;
    text-shadow: none !important;
  }

  /* Everything that only makes sense on a screen. */
  .grain, .bars, .cursor, .progress, .dock, .peek,
  .hero__cut, .hk-lb, .hero__hint, .stills__hint,
  .marquee__dot, .nav__cta { display: none !important; }

  html, body { font-size: 11pt; }

  .hero__pin { height: auto; min-height: 0; page-break-after: avoid; }
  .hero__media { position: static; height: 0; overflow: hidden; }

  .hero__ui { position: static; padding: 0; }

  /* The visually-hidden h1 becomes the actual masthead on paper. */
  #hero-name {
    position: static !important;
    width: auto !important; height: auto !important;
    clip: auto !important; clip-path: none !important;
    overflow: visible !important; white-space: normal !important;
    font-family: 'Inter', Arial, sans-serif;
    font-size: 24pt; font-weight: 900; text-transform: uppercase;
    letter-spacing: -0.02em; margin-bottom: 6pt;
  }

  .section, .hero { padding-block: 12pt; }

  /* Media boxes collapse: the frames mean nothing in greyscale. */
  .frame__media, .about__portrait { display: none !important; }

  /* The index is the point of a printed copy — keep every row intact. */
  .index__row, .stack__card, .caps__item { page-break-inside: avoid; }
  .index__hit { grid-template-columns: 2.5rem 1fr 10rem 9rem 3rem; }
  .index__title { font-size: 13pt; }

  .stack__card { position: static; border: 0; padding: 0; margin-bottom: 10pt; }
  .stack__title { font-size: 15pt; }

  /* Spell out where links actually go — a printed mailto is useless. */
  .contact__email::after { content: ''; }
  .foot__social a::after { content: ' (' attr(href) ')'; font-size: 8pt; }

  .contact__email { font-size: 16pt; }
  h2, h3 { page-break-after: avoid; }
}
