@import url("fonts.css");

/* ==========================================================================
   Joaquín Varela — portfolio
   Design premise: the interface is a grading suite. A dark, neutral surround
   (like the bias environment around a reference monitor) so the work's own
   colour reads accurately, with a single tungsten-amber accent reserved for
   controls and timecode. Structure borrows the editor's instrument: the
   timeline. Durations are real runtimes, never decorative numbering.
   ========================================================================== */

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

:root {
  --ink:      #0a0c0f;
  --ink-2:    #14181e;
  --ink-3:    #1c222a;
  --line:     #262e38;
  --line-2:   #3a4552;
  --paper:    #ece8e1;
  --mute:     #7c8794;
  --signal:   #ffb020;

  --display: "Anybody", "Archivo", system-ui, sans-serif;
  --body:    "Archivo", system-ui, -apple-system, sans-serif;
  --mono:    "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --gutter: clamp(1.25rem, 4vw, 4rem);
  --measure: 62ch;
  --bar: 3.5rem;

  /* The reel window during the intro. One source of truth: the intro carves a
     slot this size out of the sentence, and the reel sizes its video to the
     same box, so the video sits inside the line of type rather than behind it.
     16:9 matches the master (1920x1080). */
  --frame-w: clamp(8.5rem, 21vw, 14rem);
  --frame-h: calc(var(--frame-w) * 9 / 16);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-io: cubic-bezier(0.65, 0, 0.35, 1);
}

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--body);
  font-size: clamp(0.95rem, 0.9rem + 0.2vw, 1.0625rem);
  line-height: 1.6;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
}

body.is-locked { overflow: hidden; }

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

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

:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 3px;
  border-radius: 2px;
}

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

.skip {
  position: absolute; left: var(--gutter); top: -100vh;
  z-index: 200; padding: 0.6rem 1rem;
  background: var(--signal); color: var(--ink);
  font: 500 0.75rem/1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase;
}
.skip:focus { top: 1rem; }

.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* --- type roles ---------------------------------------------------------- */

.display {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 118%;
  line-height: 0.92;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  margin: 0;
}

.label {
  font: 500 0.6875rem/1 var(--mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mute);
  margin: 0;
}

.tc { font: 400 0.75rem/1 var(--mono); letter-spacing: 0.04em; font-variant-numeric: tabular-nums; }

.prose { max-width: var(--measure); }
.prose p { margin: 0 0 1.1em; }
.prose p:last-child { margin-bottom: 0; }

/* --- shell --------------------------------------------------------------- */

.shell { padding-inline: var(--gutter); }
.rule { height: 1px; background: var(--line); border: 0; margin: 0; }

/* ==========================================================================
   Masthead
   ========================================================================== */

.masthead {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  display: flex; align-items: center; justify-content: space-between;
  height: var(--bar); padding-inline: var(--gutter);
  mix-blend-mode: difference;
  transition: opacity 0.5s var(--ease);
}
.masthead[data-hidden="true"] { opacity: 0; pointer-events: none; }

/* The intro is transparent now, so the bar would otherwise sit on top of the
   loading sequence. It fades in with everything else when the frame opens. */
.intro:not([data-state="done"]) ~ .masthead { opacity: 0; pointer-events: none; }

.masthead__name {
  font: 500 0.6875rem/1 var(--mono);
  letter-spacing: 0.16em; text-transform: uppercase;
}

.masthead__nav { display: flex; gap: clamp(1rem, 3vw, 2.5rem); }

.masthead__link {
  font: 500 0.6875rem/1 var(--mono);
  letter-spacing: 0.16em; text-transform: uppercase;
  position: relative; padding-block: 0.35rem;
}
.masthead__link::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 1px; background: currentColor;
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.45s var(--ease);
}
.masthead__link:hover::after,
.masthead__link[aria-current="page"]::after { transform: scaleX(1); }

/* ==========================================================================
   Intro — phrase, charging bar, then the frame opens to fullscreen
   ========================================================================== */

/* No background of its own — the reel sits directly behind, already sized to
   the slot, so the video reads as part of the sentence. */
.intro {
  position: fixed; inset: 0; z-index: 90;
  padding: var(--gutter);
  transition: opacity 0.8s var(--ease), visibility 0.8s;
}
.intro[data-state="done"] { opacity: 0; visibility: hidden; }

/* Equal 1fr rails above and below put the slot on the exact vertical centre of
   the viewport whatever the two lines of type measure — which is what keeps it
   registered with the video box, which is centred the same way. */
.intro__inner {
  height: 100%;
  display: grid;
  grid-template-rows: 1fr auto 1fr;
  justify-items: center;
  width: min(60rem, 100%);
  margin-inline: auto;
  text-align: center;
}

.intro__line {
  font-family: var(--display);
  font-weight: 800; font-stretch: 118%;
  line-height: 0.94; letter-spacing: -0.02em; text-transform: uppercase;
  font-size: clamp(1.5rem, 0.7rem + 3.1vw, 3.5rem);
  text-wrap: balance;
  margin: 0;
}
.intro__line:first-of-type { align-self: end; padding-bottom: clamp(0.75rem, 2.2vh, 1.5rem); }
.intro__line:last-of-type  { align-self: start; padding-top: clamp(0.75rem, 2.2vh, 1.5rem); }
.intro__line em { font-style: normal; color: var(--signal); }

.intro__slot { width: var(--frame-w); height: var(--frame-h); }

.intro__foot {
  position: absolute;
  left: var(--gutter); right: var(--gutter); bottom: var(--gutter);
}

/* words rise in on load */
.intro__w {
  display: inline-block;
  transform: translateY(0.9em);
  opacity: 0;
  animation: word-in 0.85s var(--ease) forwards;
  animation-delay: calc(var(--i) * 42ms);
}
@keyframes word-in { to { transform: translateY(0); opacity: 1; } }

.intro__track {
  position: relative; height: 1px; background: var(--line);
}
.intro__fill {
  position: absolute; inset: 0; transform-origin: left;
  transform: scaleX(0); background: var(--signal);
}
.intro__readout {
  display: flex; justify-content: flex-end;
  margin: 0 0 0.6rem; color: var(--mute);
}

/* ==========================================================================
   Reel
   ========================================================================== */

/* No min-height: the reel must be exactly viewport-tall so its centre and the
   intro slot's centre are the same point. */
.reel {
  position: relative;
  height: 100svh;
  overflow: hidden;
  background: var(--ink);
}

/* The video box itself is resized, not masked: during the count the whole
   frame shows, shrunk into the same 16:9 slot the intro leaves in the
   sentence, and on release the box grows to fill the screen from that spot.
   (A mask on a full-screen video only ever showed a cropped centre.) */
.reel__video {
  position: absolute; top: 50%; left: 50%;
  width: var(--frame-w); height: var(--frame-h);
  transform: translate(-50%, -50%);
  object-fit: cover;
  border-radius: 4px;
  transition: width 1.15s var(--ease-io), height 1.15s var(--ease-io), border-radius 1.15s var(--ease-io);
}
.reel[data-open="true"] .reel__video { width: 100%; height: 100%; border-radius: 0; }

/* The title sits over the middle of the picture now, so the scrim has to hold
   the centre as well as the bottom — kept light so the grade still reads. */
.reel__scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(115% 75% at 50% 46%, rgba(10,12,15,0.58) 0%, rgba(10,12,15,0.34) 42%, rgba(10,12,15,0.12) 72%),
    linear-gradient(180deg, rgba(10,12,15,0.5) 0%, rgba(10,12,15,0) 24%, rgba(10,12,15,0) 58%, rgba(10,12,15,0.82) 100%);
  opacity: 0; transition: opacity 0.9s var(--ease) 0.35s;
}
.reel[data-open="true"] .reel__scrim { opacity: 1; }

.reel__meta {
  position: absolute; inset: 0;
  display: grid; place-content: center; justify-items: center;
  padding-inline: var(--gutter);
  text-align: center;
}
/* The entrance runs on each line, not on the block: a fading parent is sealed
   off from what is behind it, which would hold the name's blend off until the
   fade finished and then snap it on. */
.reel__meta > * {
  opacity: 0; transform: translateY(1rem);
  transition: opacity 0.9s var(--ease) 0.6s, transform 0.9s var(--ease) 0.6s;
}
.reel[data-open="true"] .reel__meta > * { opacity: 1; transform: none; }

.reel__name {
  font-size: clamp(1.25rem, 0.75rem + 2vw, 2.25rem);
  /* Same treatment as the masthead name: the letters invert whatever passes
     behind them, so the title reads as part of the picture. No shadow — it
     would be inverted too and leave a halo. */
  mix-blend-mode: difference;
}
.reel__role {
  margin: 0.6rem 0 0;
  font-size: 0.6875rem;
  color: var(--signal);
  letter-spacing: 0.2em; text-transform: uppercase;
}
.reel__line {
  max-width: 38ch;
  margin: 0.7rem 0 0;
  font: 400 clamp(0.8125rem, 0.78rem + 0.1vw, 0.9375rem)/1.55 var(--body);
  color: var(--paper);
  text-shadow: 0 1px 16px rgba(10, 12, 15, 0.55);
  text-wrap: balance;
}

.reel__scroll {
  position: absolute; left: 0; right: 0;
  bottom: calc(var(--gutter) + 3.25rem);
  display: flex; justify-content: center; align-items: center; gap: 0.6rem;
  margin: 0; color: var(--mute);
  opacity: 0; transition: opacity 0.9s var(--ease) 0.7s;
}
.reel[data-open="true"] .reel__scroll { opacity: 1; }
.reel__scroll svg { animation: nudge 2.4s var(--ease-io) infinite; }
@keyframes nudge { 0%,100% { transform: translateY(0); } 50% { transform: translateY(4px); } }

/* --- transport ----------------------------------------------------------- */

.transport {
  position: absolute; left: var(--gutter); right: var(--gutter);
  bottom: var(--gutter);
  display: flex; align-items: center; gap: clamp(0.75rem, 2vw, 1.25rem);
  opacity: 0; transform: translateY(1rem);
  transition: opacity 0.9s var(--ease) 0.75s, transform 0.9s var(--ease) 0.75s;
}
.reel[data-open="true"] .transport { opacity: 1; transform: none; }

.transport__btn {
  flex: none;
  display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem;
  padding: 0; border: 1px solid var(--line-2); border-radius: 50%;
  background: rgba(10,12,15,0.5);
  color: var(--paper); cursor: pointer;
  backdrop-filter: blur(6px);
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease), color 0.3s var(--ease);
}
.transport__btn:hover { border-color: var(--signal); color: var(--signal); }
.transport__btn svg { width: 0.9375rem; height: 0.9375rem; fill: currentColor; }
.transport__btn .icon-pause, .transport__btn .icon-on { display: none; }
.transport__btn[aria-pressed="true"] .icon-on { display: block; }
.transport__btn[aria-pressed="true"] .icon-off { display: none; }
.reel[data-playing="true"] .transport__btn .icon-pause { display: block; }
.reel[data-playing="true"] .transport__btn .icon-play { display: none; }

.transport__time { flex: none; color: var(--paper); opacity: 0.85; }

/* Native range keeps keyboard + screen-reader support; restyled to read as a
   timeline rather than a form control. */
.scrub {
  flex: 1 1 auto; min-width: 4rem;
  height: 1.5rem; margin: 0; padding: 0;
  background: transparent; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.scrub::-webkit-slider-runnable-track {
  height: 2px; border-radius: 2px;
  background: linear-gradient(to right, var(--signal) var(--pct, 0%), rgba(236,232,225,0.25) var(--pct, 0%));
}
.scrub::-moz-range-track { height: 2px; border-radius: 2px; background: rgba(236,232,225,0.25); }
.scrub::-moz-range-progress { height: 2px; border-radius: 2px; background: var(--signal); }
.scrub::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 0.75rem; height: 0.75rem; margin-top: -0.3125rem;
  border: 0; border-radius: 50%; background: var(--signal);
  transition: transform 0.2s var(--ease);
}
.scrub::-moz-range-thumb {
  width: 0.75rem; height: 0.75rem; border: 0; border-radius: 50%; background: var(--signal);
}
.scrub:hover::-webkit-slider-thumb { transform: scale(1.25); }

/* ==========================================================================
   Work grid
   ========================================================================== */

.section { padding-block: clamp(4rem, 10vh, 8rem); }

.section__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; margin-bottom: 2.5rem;
}

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--line);
  border-block: 1px solid var(--line);
}

/* Landscape tiles keep the whole grid browsable in roughly a screen and a half.
   Sources are a mix of 16:9 and 9:16, so posters are chosen with the subject
   centred and everything is centre-cropped to one ratio. */
.tile {
  position: relative;
  display: block;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--ink-2);
  isolation: isolate;
}

.tile__img {
  width: 100%; height: 100%;
  object-fit: cover;
  filter: saturate(0.82) contrast(1.02);
  transform: scale(1.02);
  transition: transform 0.9s var(--ease), filter 0.6s var(--ease);
}
.tile:hover .tile__img,
.tile:focus-visible .tile__img { transform: scale(1.07); filter: saturate(1) contrast(1); }

.tile__veil {
  position: absolute; inset: 0;
  /* A definite full-width column: with auto tracks the column sized to the
     caption, so each logo's percentage cap quietly depended on how long its
     project title was. */
  display: grid; grid-template-columns: minmax(0, 1fr);
  align-content: center; justify-items: center;
  gap: 1rem; padding: 1.5rem; text-align: center;
  background: rgba(10,12,15,0.72);
  opacity: 0;
  transition: opacity 0.45s var(--ease);
}
.tile:hover .tile__veil,
.tile:focus-visible .tile__veil { opacity: 1; }

/* Marks arrive at very different aspect ratios, so size comes from a base box
   times a per-mark scale. HBO's and Airbnb's source files were mostly empty
   canvas (66% and 74% padding) — trimmed now, and scaled up to read larger. */
.tile__logo {
  --mark-h: 3.25rem;
  --mark-w: 11rem;
  --mark-scale: 1;
  width: auto;
  max-width: min(58%, calc(var(--mark-w) * var(--mark-scale)));
  max-height: calc(var(--mark-h) * var(--mark-scale));
  object-fit: contain;
  transform: translateY(0.5rem);
  transition: transform 0.5s var(--ease) 0.04s;
}
.tile:hover .tile__logo,
.tile:focus-visible .tile__logo { transform: none; }

.tile__logo[data-mark="hbo"]    { --mark-scale: 0.949; }
.tile__logo[data-mark="hyq"]    { --mark-scale: 1.155; }
.tile__logo[data-mark="airbnb"] { --mark-scale: 0.774; }

/* Netflix has no supplied mark; set the title instead of faking one. */
.tile__wordmark {
  font-family: var(--display); font-weight: 800; font-stretch: 118%;
  font-size: clamp(1.1rem, 2.4vw, 1.6rem); letter-spacing: 0.02em;
  text-transform: uppercase; line-height: 1;
  transform: translateY(0.5rem);
  transition: transform 0.5s var(--ease) 0.04s;
}
.tile:hover .tile__wordmark,
.tile:focus-visible .tile__wordmark { transform: none; }

.tile__caption {
  transform: translateY(0.5rem);
  transition: transform 0.5s var(--ease) 0.09s;
}
.tile:hover .tile__caption,
.tile:focus-visible .tile__caption { transform: none; }

.tile__title {
  display: block;
  font: 500 0.8125rem/1.35 var(--body);
  letter-spacing: 0.01em; margin: 0;
}
.tile__sub {
  display: block;
  margin: 0.35rem 0 0; color: var(--mute);
  font: 400 0.6875rem/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase;
}
.tile__sub span { color: var(--signal); }

/* ==========================================================================
   Info
   ========================================================================== */

/* Everything runs the full measure now: the old label-left / content-right
   split left the whole left half of the section empty. */
.info__label { margin-bottom: clamp(1.75rem, 4vh, 2.5rem); }

/* Same voice as the contact close: display face, capitals, the last words in
   amber. One step smaller than it, so the closing line stays the loudest. */
.info__lead {
  font-size: clamp(2rem, 0.8rem + 5vw, 5.25rem);
  text-wrap: balance;
}
.info__lead span { color: var(--signal); }

.info__prose {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.25rem clamp(1.5rem, 4vw, 4.5rem);
  max-width: 76rem; margin-top: clamp(2rem, 5vh, 3.25rem);
}
.info__prose p { margin: 0; }

.info__block { margin-top: clamp(3rem, 8vh, 5rem); }
.info__block > .label { margin-bottom: 1rem; }

/* Services read across like a row of markers on a timeline. */
.services {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1.25rem 1.5rem;
  list-style: none; margin: 0; padding: 0;
}
.services li { padding-top: 0.9rem; border-top: 1px solid var(--line-2); }
.services__title { margin: 0; font: 500 1rem/1.3 var(--body); letter-spacing: 0.01em; }
.services__body { margin: 0.45rem 0 0; font-size: 0.875rem; line-height: 1.5; color: var(--mute); max-width: 34ch; }
/* Spotlight on hover: the service under the pointer lifts to full brightness
   and its title grows slightly from its left edge; the others step back.
   :has() keeps the others lit while the pointer is only in the gaps between
   items. Hover-capable screens only — touch keeps everything at full strength. */
@media (hover: hover) {
  .services li { transition: opacity 0.3s var(--ease); }
  .services__title {
    opacity: 0.82; transform-origin: left center;
    transition: opacity 0.3s var(--ease), transform 0.3s var(--ease),
                color 0.3s var(--ease), text-shadow 0.3s var(--ease);
  }
  .services__body { transition: color 0.3s var(--ease); }
  /* The lit service goes past paper-white to pure white with a soft halo,
     and its description comes up from muted grey. */
  .services li:hover .services__title {
    opacity: 1; transform: scale(1.08);
    color: #fff; text-shadow: 0 0 18px rgba(255, 255, 255, 0.35);
  }
  .services li:hover .services__body { color: var(--paper); }
  .services:has(li:hover) li:not(:hover) { opacity: 0.4; }
}

/* Client marks on an endless belt, running left to right. Both tracks slide
   together by exactly one track width, so at the end of each pass the second
   copy sits where the first started and the loop has no seam. Full-bleed, with
   the ends faded so marks drift in and out rather than being cut off. */
.marquee {
  --gap: clamp(3rem, 7vw, 6.5rem);
  --belt-scale: 1;
  display: flex; overflow: hidden;
  margin-inline: calc(var(--gutter) * -1);
  padding-block: clamp(1.4rem, 3.5vh, 2.25rem);
  border-block: 1px solid var(--line);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
.marquee__track {
  flex: none; min-width: 100%;
  display: flex; align-items: center; justify-content: space-around; gap: var(--gap);
  margin: 0; padding: 0 calc(var(--gap) / 2); list-style: none;
  animation: belt 38s linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
@keyframes belt { from { transform: translateX(-100%); } to { transform: translateX(0); } }

.marquee__item { flex: none; display: flex; }
.marquee__item img {
  width: auto; max-width: none;
  height: calc(var(--h, 2rem) * var(--belt-scale));
  opacity: 0.82; transition: opacity 0.3s var(--ease);
}
.marquee__item img:hover { opacity: 1; }

/* Per-mark heights balance visual weight: a thin wide wordmark needs less
   height than a dense square mark to read at the same size. */
.marquee__item img[data-mark="hbo"]          { --h: 2.35rem; }
.marquee__item img[data-mark="netflix"]      { --h: 1.808rem; }
.marquee__item img[data-mark="airbnb"]       { --h: 2.189rem; }
.marquee__item img[data-mark="lululemon-01"] { --h: 2.517rem; }
.marquee__item img[data-mark="crocs-pfw"]    { --h: 1.44rem; }
.marquee__item img[data-mark="acm"]          { --h: 2.064rem; }

/* ==========================================================================
   Contact
   ========================================================================== */

.contact { padding-block: clamp(4rem, 12vh, 9rem); }

.contact__head {
  font-size: clamp(2.25rem, 1rem + 6.5vw, 7rem);
  text-wrap: balance; margin-bottom: 2.5rem;
}
.contact__head span { color: var(--signal); }

.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }

.btn {
  display: inline-flex; align-items: center; gap: 0.75rem;
  padding: 0.9rem 1.4rem;
  border: 1px solid var(--line-2); border-radius: 100px;
  font: 500 0.75rem/1 var(--mono); letter-spacing: 0.12em; text-transform: uppercase;
  background: transparent; color: var(--paper); cursor: pointer;
  transition: background 0.35s var(--ease), color 0.35s var(--ease), border-color 0.35s var(--ease);
}
.btn:hover { background: var(--paper); color: var(--ink); border-color: var(--paper); }
.btn--primary { background: var(--signal); border-color: var(--signal); color: var(--ink); }
.btn--primary:hover { background: var(--paper); border-color: var(--paper); }
.btn svg { width: 0.75rem; height: 0.75rem; fill: currentColor; }

/* --- enquiry form -------------------------------------------------------- */

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

.form { display: grid; gap: 1.25rem; }
.form[data-busy="true"] { opacity: 0.55; pointer-events: none; }

.field { display: grid; gap: 0.5rem; margin: 0; }
.field input,
.field textarea {
  width: 100%;
  padding: 0.85rem 1rem;
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: 3px;
  color: var(--paper);
  font: 400 0.9375rem/1.5 var(--body);
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.field textarea { min-height: 7.5rem; resize: vertical; }
.field input:hover, .field textarea:hover { border-color: var(--line-2); }
/* Outline is left to the global :focus-visible ring so keyboard focus stays visible. */
.field input:focus, .field textarea:focus { border-color: var(--signal); background: var(--ink-3); }
.field input::placeholder, .field textarea::placeholder { color: var(--mute); }

.form__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.25rem; }
.form__note { color: var(--mute); }
.form__note[data-state="ok"] { color: var(--signal); }
.form__note[data-state="error"] { color: #ff7a6b; }

.contact__aside { display: grid; gap: 2.25rem; align-content: start; }
.contact__mail {
  display: inline-block; margin-top: 0.65rem;
  font: 400 0.9375rem/1.5 var(--body);
  border-bottom: 1px solid var(--line-2); padding-bottom: 2px;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease);
}
.contact__mail:hover { color: var(--signal); border-color: var(--signal); }
.contact__aside .actions { margin-top: 0.75rem; }

/* 404 — same voice as the rest, with a way back. */
.notfound {
  min-height: 100svh;
  display: grid; align-content: center; justify-items: start;
  gap: 1.25rem; padding-block: var(--bar);
}
.notfound__head { font-size: clamp(2rem, 1rem + 5vw, 5rem); text-wrap: balance; }
.notfound__text { max-width: 44ch; margin: 0; color: var(--mute); }
.notfound .actions { margin-top: 0.75rem; }

.colophon {
  display: flex; flex-wrap: wrap; gap: 1rem 2rem;
  justify-content: space-between; align-items: center;
  padding-block: 1.5rem; border-top: 1px solid var(--line);
  color: var(--mute);
}
.colophon a:hover { color: var(--paper); }

/* ==========================================================================
   Project page
   ========================================================================== */

.pj { padding-top: calc(var(--bar) + clamp(2rem, 8vh, 5rem)); }

.pj__eyebrow { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; margin-bottom: 1.25rem; }
.pj__title { font-size: clamp(2.25rem, 1rem + 6vw, 6.5rem); margin-bottom: 1.5rem; }

.pj__logo { height: clamp(1.75rem, 3.5vw, 2.75rem); width: auto; object-fit: contain; margin-bottom: 2rem; opacity: 0.9; }

.pj__body {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
  gap: clamp(2rem, 5vw, 5rem);
  padding-block: clamp(2.5rem, 6vh, 4.5rem);
  border-top: 1px solid var(--line);
}

.pj__aside { align-self: start; position: sticky; top: calc(var(--bar) + 1.5rem); }

.pj__block { margin-bottom: 2rem; }
.pj__block p { margin: 0.6rem 0 0; font-size: 0.9375rem; }

.spec { width: 100%; border-collapse: collapse; margin-top: 0.75rem; }
.spec th, .spec td {
  text-align: left; vertical-align: top;
  padding: 0.6rem 0; border-bottom: 1px solid var(--line);
  font-size: 0.8125rem; font-weight: 400;
}
.spec th { width: 8.5rem; color: var(--mute); font: 400 0.6875rem/1.5 var(--mono); letter-spacing: 0.1em; text-transform: uppercase; }

.pj__media { display: grid; gap: 1px; background: var(--line); }
.pj__media > * { background: var(--ink); }

/* Vertical 9:16 cuts would otherwise run ~1300px tall in this column, so cap
   the player to the viewport and centre whatever shape the video is. */
.player { position: relative; background: #000; display: grid; place-items: center; }
.player video { width: auto; max-width: 100%; max-height: 82svh; }

.figure { margin: 0; }
.figure img { width: 100%; height: auto; }
.figure figcaption { padding: 0.65rem 0; color: var(--mute); }

.stills { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; background: var(--line); }
.stills .figure img { display: block; }
.stills--wide { grid-template-columns: 1fr; }
.stills[data-cols="3"] { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 40rem) { .stills[data-cols="3"] { grid-template-columns: 1fr; } }

.loops { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); }

/* Alternate cuts: a small control row under the player. */
.altcut { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; padding: 1rem; }
.altcut .btn { padding: 0.6rem 1rem; }
.altcut .btn[aria-pressed="true"] { background: var(--signal); border-color: var(--signal); color: var(--ink); }

/* Next project: full-bleed, same as the work grid. */
.next { padding-block: clamp(3rem, 8vh, 5rem) 1.5rem; }
.next__head { font-size: clamp(1.5rem, 0.9rem + 2vw, 2.75rem); }
.next__grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px; background: var(--line);
  border-block: 1px solid var(--line);
}
.loop { width: 100%; height: auto; display: block; }

/* Two stacks side by side (p.split in projects.json). The page sets --split
   so each column's width is inverse to its piece count and both end level. */
.split { display: grid; grid-template-columns: var(--split, 1fr 1fr); column-gap: 1px; background: var(--line); }
.split__col { background: var(--ink); }
.split__stack { display: grid; gap: 1px; background: var(--line); }
.split__stack .figure img { display: block; }
@media (max-width: 40rem) { .split { grid-template-columns: 1fr; row-gap: 1px; } }

/* Before/after comparator. Both frames are the same size and stacked; the top
   one is clipped at --pos so the handle always sits on the seam. The control
   is a real range input laid over the picture: pointer, touch and arrow keys
   all work, and with no JS it stays a readable half-and-half split. */
.ba__grid { display: grid; gap: 1px; background: var(--line); }
.ba__grid[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ba__grid[data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* Three vertical frames across is fine on a wide screen, cramped below it. */
@media (max-width: 60rem) { .ba__grid[data-cols="3"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 40rem) { .ba__grid[data-cols="2"], .ba__grid[data-cols="3"] { grid-template-columns: 1fr; } }

.ba {
  --pos: 50%;
  position: relative; overflow: hidden;
  background: var(--ink-2); touch-action: pan-y;
}
.ba:focus-within { outline: 2px solid var(--signal); outline-offset: -2px; }
.ba__img { display: block; width: 100%; height: auto; }
.ba__img--before {
  position: absolute; inset: 0; height: 100%; object-fit: cover;
  clip-path: inset(0 calc(100% - var(--pos)) 0 0);
}

.ba__handle {
  position: absolute; top: 0; bottom: 0; left: var(--pos);
  width: 2px; margin-left: -1px; background: var(--paper);
  pointer-events: none;
}
.ba__handle::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 2.75rem; height: 2.75rem; margin: -1.375rem 0 0 -1.375rem;
  border: 1px solid var(--paper); border-radius: 50%;
  background: rgba(10, 12, 15, 0.45); backdrop-filter: blur(6px);
}

.ba__label {
  position: absolute; bottom: 0.85rem; z-index: 2;
  padding: 0.3rem 0.6rem; border-radius: 2px;
  background: rgba(10, 12, 15, 0.6); color: var(--paper);
  letter-spacing: 0.14em; text-transform: uppercase; pointer-events: none;
}
.ba__label--before { left: 0.85rem; }
.ba__label--after { right: 0.85rem; }

/* Invisible, full-bleed, and still a real slider underneath. */
.ba__range {
  position: absolute; inset: 0; z-index: 3;
  width: 100%; height: 100%; margin: 0; padding: 0;
  opacity: 0; cursor: ew-resize;
  -webkit-appearance: none; appearance: none; background: transparent;
}

@media (max-width: 40rem) {
  .ba__handle::after { width: 2.25rem; height: 2.25rem; margin: -1.125rem 0 0 -1.125rem; }
}
/* ==========================================================================
   Responsive
   ========================================================================== */

@media (max-width: 60rem) {
  .grid, .next__grid { grid-template-columns: repeat(2, 1fr); }
  .services { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pj__body { grid-template-columns: 1fr; }
  .pj__aside { position: static; }
  .loops { grid-template-columns: repeat(3, 1fr); }
  .contact__grid { grid-template-columns: 1fr; }
}

@media (max-width: 40rem) {
  :root { --bar: 3rem; }
  .grid, .next__grid, .stills { grid-template-columns: 1fr; }
  /* One-up on phones: a 3:2 crop of vertical footage is too lossy at full width. */
  .tile { aspect-ratio: 4 / 3; }
  /* Three across would be ~130px wide — too small to read the technique. */
  .loops { grid-template-columns: 1fr; }
  .info__prose { grid-template-columns: 1fr; }
  .services { grid-template-columns: 1fr; }
  /* Centred block would collide with the transport on a short phone screen. */
  .reel__meta { align-content: center; padding-bottom: 3rem; }
  .reel__scroll { display: none; }
  .transport__time { font-size: 0.6875rem; }
  .spec th { width: 7rem; }
  /* Tiles are tap targets, so show the caption rather than hiding it behind hover. */
  .tile__veil {
    opacity: 1;
    background: linear-gradient(180deg, rgba(10,12,15,0) 40%, rgba(10,12,15,0.85) 100%);
    place-content: end center;
  }
  .tile__logo, .tile__wordmark, .tile__caption { transform: none; }
  .tile__logo { --mark-h: 2.25rem; --mark-w: 9rem; }
}

@media (hover: none) {
  .tile__veil {
    opacity: 1;
    background: linear-gradient(180deg, rgba(10,12,15,0) 40%, rgba(10,12,15,0.85) 100%);
    place-content: end center;
  }
  .tile__logo, .tile__wordmark, .tile__caption { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .intro__w { opacity: 1; transform: none; }
  .reel__video { width: 100%; height: 100%; border-radius: 0; }
}
