/* Azeret Mono is a variable face — one file carries 300–700, so the whole
   system loads as a single request instead of four static cuts. Self-hosted:
   an @import made the browser wait for style.css to parse before it could even
   ask for the font, and put a third party in the path of the brand's only
   typeface. */
@font-face {
  font-family: 'Azeret Mono';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../assets/fonts/azeret-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Azeret Mono';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../assets/fonts/azeret-mono-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --ink: #000;
  --paper: #fff;

  /* Greyscale from the palette file — tonal steps, not colour. Display type
     and the mark stay at pure ink/paper; these carry the supporting layers. */
  --off-white: #f0f0f0;
  --grey-mid: #a0a0a0;
  --grey-dark: #505050;

  /* Oxblood. The primary action and nothing else — one red element per view.
     Never on the mark, never over imagery. */
  --accent: #ac0808;

  --nav-h: 72px;
  --ease: cubic-bezier(0.19, 1, 0.22, 1);
  --slow: 1.1s;

  /* Dimmed mono labels sat at 0.45, which reads as 3.36:1 — under the 4.5:1
     floor for 11px text. These are the lowest values that clear it on each
     ground, so the labels stay quiet without dropping out. */
  --dim: 0.56;
}

.page-dark { --dim: 0.5; }

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

html, body {
  margin: 0;
  padding: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: 'Azeret Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  -webkit-font-smoothing: antialiased;
}

/* A short page used to stop at its content height and let the black html
   background show underneath as a bar — most obvious on 404, which is the
   shortest page on the site. */
body { min-height: 100svh; }

/* The theme class lives on body, so html keeps its own black unless told
   otherwise. That black is what shows when a trackpad overscrolls past the
   end of a light page. */
html:has(body.page-light) { background: var(--paper); }

/* The I-beam over body copy reads like an input field and buries the focus
   effect under a cursor change. Text stays fully selectable — only the pointer
   shape changes. Fields below keep the caret, since there it means something. */
body { cursor: default; }

a,
button,
label,
summary,
[role='button'] { cursor: pointer; }

input,
textarea { cursor: text; }

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

/* The nav blends with `difference` and the wall is near-black, so a UA focus
   ring lands on whatever happens to be behind it. Pin it to a fixed pair that
   reads on both grounds. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.page-dark :focus-visible { outline-color: var(--paper); }
.page-light :focus-visible { outline-color: var(--ink); }

/* Intrinsic width/height sit on the tags so the browser reserves the right box
   before the PNG lands; height:auto keeps them scaling to their real ratio. */
img, video { display: block; max-width: 100%; height: auto; }

/* Named to match the identity board's own `.red` class. Display type only —
   the pair of operative nouns in the position line, nothing else. */
.red { color: var(--accent); }

/* ---------- Lens ----------
   A soft field that follows the pointer instead of blurring a whole element.
   The radial mask is what makes it a lens: fully opaque at the centre where
   the blur reads at full strength, falling to nothing at the rim, so focus
   returns gradually as type leaves the middle. Suppressed over the reel and
   the wall, which carry their own effects. */
.lens {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 8;
  width: 165px;
  height: 165px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  backdrop-filter: blur(1.1px);
  -webkit-backdrop-filter: blur(1.1px);
  mask-image: radial-gradient(circle closest-side,
    rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.38) 34%, rgba(0, 0, 0, 0.12) 64%, transparent 100%);
  -webkit-mask-image: radial-gradient(circle closest-side,
    rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.38) 34%, rgba(0, 0, 0, 0.12) 64%, transparent 100%);
  transition: opacity 0.45s var(--ease);
}

.lens.is-live { opacity: 1; }

/* Touch has no pointer to follow, and reduced motion should not get a field
   chasing the cursor either. */
@media (hover: none) {
  .lens { display: none; }
}


.u-label {
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* ---------- Texture ----------
   Both layers are the spray-can grain of the mark, pushed into the page.
   They drift constantly so nothing on screen is ever fully still. */

.grain {
  position: fixed;
  inset: -60px;
  z-index: 9;
  pointer-events: none;
  opacity: 0.17;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grain-drift 0.8s steps(3) infinite;
}

.page-light .grain { mix-blend-mode: multiply; opacity: 0.1; }

@keyframes grain-drift {
  0%   { transform: translate(0, 0); }
  33%  { transform: translate(-14px, 8px); }
  66%  { transform: translate(9px, -11px); }
  100% { transform: translate(0, 0); }
}

/* ---------- Page transition ---------- */

/* Lifts via CSS animation, not script. If JS never runs the page is still
   visible — the veil must never be what stands between a visitor and content. */
.veil {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--ink);
  pointer-events: none;
  opacity: 0;
}

/* Only added when the document is actually visible, so a throttled animation
   in a background tab can never park the veil on its opaque first frame. */
.veil.is-intro { animation: veil-lift 0.75s var(--ease); }

@keyframes veil-lift {
  from { opacity: 1; }
  to   { opacity: 0; }
}

.veil.is-closing {
  animation: none;
  opacity: 1;
  pointer-events: all;
  transition: opacity 0.5s var(--ease);
}

/* ---------- Splash ---------- */

/* 78svh, not 100. Together with the reduced intro padding below, this puts the
   top of the headline about 80px inside the first screen — the page announces
   that it continues instead of looking like a slide that ends. */
.splash {
  position: relative;
  height: 78svh;
  display: grid;
  place-items: center;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 40%, #1a1a1a 0%, #000 72%),
    repeating-linear-gradient(115deg, rgba(255,255,255,0.03) 0 2px, transparent 2px 7px);
}

/* The reel used to stop at a hard edge. This dissolves it into the page ink
   instead, so there is no seam between the film and the type below it. */
.splash::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  z-index: 1;
  height: calc(42% + 1px);
  pointer-events: none;
  /* Solid ink from 92%, not 100%. Reaching full black exactly at the edge left
     a hairline of video showing through: the reel is clipped at a fractional
     pixel and the gradient's own last row is not quite opaque. */
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0) 0%,
    rgba(0, 0, 0, 0.45) 46%,
    rgba(0, 0, 0, 0.9) 78%,
    var(--ink) 92%,
    var(--ink) 100%
  );
}

/* Plays in full colour — the spotlight below is what drains it.
   `scale` carries the slow push so `transform` stays free for the scroll
   parallax; the two compose instead of overwriting each other. */
.splash__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: contrast(1.05) brightness(0.72);
  animation: slow-push 24s ease-in-out infinite alternate;
  transform: translate3d(0, calc(var(--sp, 0) * 12vh), 0);
  opacity: calc(1 - var(--sp, 0) * 0.55);
}

/* The mark lifts and clears out as the headline arrives, so the hand-off from
   film to type is one movement rather than a cut. */
.splash__inner {
  transform: translate3d(0, calc(var(--sp, 0) * -7vh), 0);
  opacity: calc(1 - var(--sp, 0) * 1.35);
}

/* Absolute, not fixed. Fixed let the puck hang below the reel and drain the
   colour out of the headline underneath — most obvious on the red words, which
   went grey. .splash has overflow:hidden, so absolute clips it to the film. */
/* The blend lives here, not on the canvas. .spot is absolutely positioned with
   a z-index, which makes it its own stacking context — a blend applied inside
   it composites against that empty context and paints literal grey. Applied to
   .spot itself it composites against the reel below, which is the point. */
.spot {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  mix-blend-mode: saturation;
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}

.spot.is-live { opacity: 1; }

/* One canvas painted with the pointer's path, not a row of discs. Stacked
   circles read as circles no matter how much they overlap; a stroked line with
   round caps is a single continuous mark.

   mix-blend-mode: saturation is what does the work. The canvas is painted in
   flat grey, which has zero saturation, so wherever it is opaque the reel
   underneath keeps its hue and luminosity but loses its colour. Transparent
   pixels leave the video untouched, and partial alpha desaturates partially,
   which is what makes the tail fade rather than end. */
.spot__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* `scale` rather than `transform: scale()` so the scroll parallax can own
   `transform` without one clobbering the other. */
@keyframes slow-push {
  from { scale: 1.06; }
  to   { scale: 1.16; }
}

.splash__inner {
  position: relative;
  z-index: 2;
  display: grid;
  justify-items: center;
  gap: 44px;
  padding: 0 24px;
  text-align: center;
}

.splash__markwrap {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 18px;
}

/* Light smear behind the mark — the wordmark reads as caught mid-motion */
.splash__markwrap::before {
  content: '';
  position: absolute;
  left: -20%;
  right: -20%;
  top: 30%;
  height: 22%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.14) 45%, rgba(255,255,255,0.14) 55%, transparent);
  filter: blur(16px);
  animation: smear 7s var(--ease) infinite;
  pointer-events: none;
}

@keyframes smear {
  0%, 100% { opacity: 0.35; transform: scaleX(0.9); }
  50%      { opacity: 0.9;  transform: scaleX(1.06); }
}

.splash__mark {
  width: min(420px, 62vw);
  filter: drop-shadow(0 0 40px rgba(0,0,0,0.7));
  animation: mark-in 1.6s var(--ease) both;
}

@keyframes mark-in {
  from { opacity: 0; transform: scale(1.1); filter: blur(9px); }
  to   { opacity: 1; transform: none; filter: none; }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(12px); }
}

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

.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  height: var(--nav-h);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 30px;
  padding: 0 30px;
}

/* The blend moved off the bar and onto the parts that need it. Under
   `difference` the accent inverts toward cyan over a bright frame, so the
   Contact button has to render its own colour. */
.nav__link,
.nav__mark { mix-blend-mode: difference; }

/* Light pages have no video to blend against, and `difference` there turns the
   black nav white against the paper. Setting it on .nav--light alone is not
   enough now the blend lives on the children. */
.nav--light .nav__link,
.nav--light .nav__mark { mix-blend-mode: normal; }

/* Which leaves the button legible on its own over whatever the reel is doing. */
.page-dark .nav__cta {
  backdrop-filter: brightness(0.5);
  -webkit-backdrop-filter: brightness(0.5);
}

.page-dark .nav__cta:hover,
.page-dark .nav__cta:focus-visible {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.nav__group { display: flex; align-items: center; gap: 30px; }
.nav__group--right { justify-self: end; }

.nav__link {
  padding: 6px 0;
  opacity: 0.7;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: opacity 0.4s var(--ease), background-size 0.6s var(--ease);
}

.nav__link:hover,
.nav__link:focus-visible,
.nav__link[aria-current='page'] { opacity: 1; }

.nav__link:hover,
.nav__link:focus-visible,
.nav__link[aria-current='page'] { background-size: 100% 1px; }

.nav__mark {
  width: 46px;
  /* Padding is tap area only; the 46x32 mark does not move. */
  padding: 6px 0;
}

.nav__mark img {
  transition: transform var(--slow) var(--ease);
}

.nav__mark:hover img { transform: scale(1.16); }

.nav__cta {
  padding: 8px 15px;
  border: 1px solid currentColor;
  transition: background 0.45s var(--ease), color 0.45s var(--ease);
}

.nav__cta:hover,
.nav__cta:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Light pages carry the nav in ink, so difference blending is dropped. */
.nav--light {
  mix-blend-mode: normal;
  color: var(--ink);
  background: linear-gradient(to bottom, rgba(255,255,255,0.94) 0%, rgba(255,255,255,0.7) 62%, transparent 100%);
}

.nav--light .nav__cta:hover,
.nav--light .nav__cta:focus-visible {
  background: var(--ink);
  color: var(--paper);
}

.wrap--nav { padding-top: var(--nav-h); }

/* ---------- Work grid ----------
   Every tile runs its own muted loop, all at once, before anything is clicked.
   Grayscale keeps the wall raw; hover is the only thing that returns colour. */

/* No top padding: the reel is the first thing on the page and runs under the
   fixed nav, which blends rather than sits on a bar. The intro below carries
   its own spacing. */
.work { padding-top: 0; }

/* Uniform cells. Sizing every tile to its own ratio was tried and read as
   crowded: fifteen different shapes competing is busier than one repeated. */
.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0;
}

.tile {
  --p: 0;
  --lift: 1;
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: #070707;
  display: block;
  transform: scale(var(--lift));
  transition: transform 0.7s var(--ease), box-shadow 0.7s var(--ease);
}

/* `display: block` above outranks the browser's own [hidden] rule, so a tile
   held back by "Show more" would still occupy its space in the grid. */
.tile[hidden] { display: none; }

/* Closes the last row when the tile count does not divide by the column count.
   main.js sets its span, so one element fills a gap of one, two or three at any
   breakpoint. No aspect-ratio: there is always a real tile beside it, and that
   tile sets the row height for both. */
.tile--cta {
  aspect-ratio: auto;
  display: grid;
  place-items: center;
  padding: 24px 18px;
  background: #0d0d0d;
  color: rgba(255, 255, 255, 0.6);
  text-align: center;
  transition: transform 0.7s var(--ease), box-shadow 0.7s var(--ease),
              color 0.45s var(--ease), background 0.45s var(--ease);
}

/* A 7% lift reads fine on a square tile but is heavy on one spanning three
   columns, so this one rises less. Red on approach rather than on the whole
   tile: the wall still carries one accent at a time. */
.tile--cta:hover,
.tile--cta:focus-visible,
.tile--cta.is-near {
  --lift: 1.02;
  color: var(--accent);
  background: #111;
}

/* Focus is either the pointer or, on touch, the tile nearest the viewport
   centre. Both land on the same state so the wall behaves identically. */
.tile:hover,
.tile:focus-visible,
.tile.is-near {
  --lift: 1.07;
  z-index: 6;
  box-shadow: 0 26px 70px rgba(0,0,0,0.75);
}

.tile__media {
  position: absolute;
  inset: -2%;
  width: 104%;
  height: 104%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.15) brightness(0.85);
  transform: translate3d(0, calc(var(--p) * -14px), 0) scale(1.02);
  transition: filter 0.9s var(--ease), transform 1.2s var(--ease);
}

.tile:hover .tile__media,
.tile:focus-visible .tile__media,
.tile.is-near .tile__media {
  filter: grayscale(0) contrast(1) brightness(1.03);
  transform: translate3d(0, calc(var(--p) * -14px), 0) scale(1.05);
}


/* Living placeholder — keeps the wall in motion until footage lands */
.tile__placeholder {
  --rot: 0deg;
  --scl: 1;
  --tone: 90% 70% at 50% 45%;
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background:
    radial-gradient(var(--tone), #1a1a1a 0%, #050505 76%),
    repeating-linear-gradient(135deg, rgba(255,255,255,0.03) 0 1px, transparent 1px 9px);
  transform: translate3d(0, calc(var(--p) * -14px), 0);
  transition: transform 1.2s var(--ease);
}

.tile__placeholder img {
  width: 34%;
  opacity: 0.14;
  transform: rotate(var(--rot)) scale(var(--scl));
  animation: breathe 9s ease-in-out infinite;
  transition: opacity 0.8s var(--ease);
}

.tile:hover .tile__placeholder img { opacity: 0.34; }

@keyframes breathe {
  0%, 100% { scale: 1; }
  50%      { scale: 1.09; }
}

.tile:nth-child(6n+1) .tile__placeholder { --rot: -14deg; --scl: 0.86; --tone: 70% 60% at 30% 35%; }
.tile:nth-child(6n+2) .tile__placeholder { --rot: 9deg;   --scl: 1.12; --tone: 110% 80% at 65% 55%; }
.tile:nth-child(6n+3) .tile__placeholder { --rot: -3deg;  --scl: 0.72; --tone: 80% 90% at 50% 20%; }
.tile:nth-child(6n+4) .tile__placeholder { --rot: 21deg;  --scl: 0.98; --tone: 95% 65% at 75% 70%; }
.tile:nth-child(6n+5) .tile__placeholder { --rot: -8deg;  --scl: 1.24; --tone: 120% 95% at 40% 60%; }
.tile:nth-child(6n+6) .tile__placeholder { --rot: 4deg;   --scl: 0.8;  --tone: 65% 70% at 55% 40%; }

.tile__placeholder img { animation-delay: calc(var(--i, 0) * -1.3s); }

.tile::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, transparent 48%);
  opacity: 0;
  transition: opacity 0.7s var(--ease);
}

.tile:hover::before,
.tile:focus-visible::before,
.tile.is-near::before { opacity: 1; }

/* Texture only — no link, no caption, no index. */
.tile--decor { cursor: default; }
.tile--decor::before { display: none; }

.tile__index {
  position: absolute;
  z-index: 2;
  top: 14px;
  left: 16px;
  font-size: 10px;
  letter-spacing: 0.2em;
  opacity: 0.35;
  transition: opacity 0.6s var(--ease);
}

.tile:hover .tile__index,
.tile.is-near .tile__index { opacity: 1; }

.tile__caption {
  position: absolute;
  z-index: 2;
  left: 16px;
  right: 16px;
  bottom: 14px;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.65s var(--ease), transform 0.65s var(--ease);
}

.tile:hover .tile__caption,
.tile:focus-visible .tile__caption,
.tile.is-near .tile__caption {
  opacity: 1;
  transform: none;
}

.tile__caption span:last-child { opacity: 0.6; }

/* ---------- Intro / above the fold ---------- */

/* Top padding is deliberately short: it is the gap between the reel and the
   headline, and the headline needs to break the fold. */
.intro {
  padding: clamp(58px, 7vw, 96px) 0 clamp(46px, 6vw, 74px);
}

/* Monospace makes the longest line exactly 24 characters wide, which overran
   the 1150px wrap by a hair at the old 78px cap. */
.intro__head {
  margin: 0 0 26px;
  font-size: clamp(34px, 6.4vw, 74px);
  font-weight: 500;
  letter-spacing: -0.035em;
  line-height: 0.98;
  text-transform: uppercase;
}

.intro__sub {
  max-width: 54ch;
  margin: 0 0 38px;
  font-size: 13px;
  font-weight: 300;
  line-height: 1.95;
  opacity: 0.66;
}

.intro__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px 24px;
}

.stats {
  display: grid;
  /* 210px, not 150px: "Ricardo Ramirez" measures 199px on one line at 21px, so
     a 150px floor let two columns survive down to a 152px width and broke both
     values across lines — "Las Vegas," / "NV" reads as a fault rather than a
     wrap. The floor is the widest value plus ~5%, because 199px is Chromium's
     metrics and other engines lay the same font out slightly wider. Below a
     ~430px grid auto-fit now drops to one column instead. */
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 26px 30px;
  margin: clamp(50px, 7vw, 80px) 0 0;
  padding: 30px 0 0;
  border-top: 1px solid rgba(255,255,255,0.15);
}

.stats--light { border-top-color: rgba(0,0,0,0.14); }

.stats dt { opacity: var(--dim); }

.stats dd {
  margin: 10px 0 0;
  font-size: clamp(21px, 2.6vw, 30px);
  font-weight: 500;
  letter-spacing: -0.02em;
}


/* ---------- Quotes ---------- */

.quotes {
  padding: clamp(70px, 10vw, 120px) 0 0;
}

.quotes .wrap {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: 44px 50px;
}

.quotes blockquote {
  margin: 0;
  padding-top: 24px;
  border-top: 1px solid rgba(255,255,255,0.16);
}

.quotes p {
  margin: 0 0 16px;
  font-size: clamp(15px, 1.6vw, 19px);
  font-weight: 300;
  line-height: 1.62;
  letter-spacing: -0.01em;
}

.quotes cite {
  font-style: normal;
  opacity: 0.45;
}

/* ---------- Capability ----------
   Deliberately not the stats grid and not the close block — a third layout so
   the page doesn't read as the same row repeated. No cards: the items are
   grouped by space and one hairline, not boxed. */

/* ---------- Wall reveal ----------
   Plain text, not a framed button: this is a way to see more work, not the
   thing the page is asking for, and a second bordered control this close to
   the closing CTA would compete with it.

   Still a real <button> in the markup. Stripping the chrome is a styling
   decision; dropping the element would cost keyboard access and stop screen
   readers announcing it as a control, and it looks identical either way.

   Red on hover and on press, which is the interaction state the rest of the
   site already uses. The tap target stays finger-sized through padding, since
   most visits are phones. */

.more {
  margin-top: clamp(30px, 4vw, 46px);
  text-align: center;
}

.more__btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 16px 20px;
  border: 0;
  background: none;
  color: rgba(255, 255, 255, 0.62);
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.45s var(--ease);
}

.more__btn:hover,
.more__btn:focus-visible,
.more__btn:active {
  color: var(--accent);
}

/* The button's only affordance used to be colour on hover, which does nothing
   on a phone — where most of the traffic is. The arrow reads as a control at
   rest, and the nudge points at what the tap does. Long dwell, short move: it
   should register as intent, not as a blinking widget. The global
   reduced-motion rule collapses the duration, and the keyframe ends where it
   starts, so the arrow simply sits still. */
.more__btn svg {
  width: 13px;
  height: 13px;
  transform: rotate(90deg);
  animation: more-nudge 2.8s var(--ease) infinite;
}

/* translateX inside a 90deg rotation moves down the screen, not across. */
@keyframes more-nudge {
  0%, 62%, 100% { transform: rotate(90deg) translateX(0); }
  78%           { transform: rotate(90deg) translateX(3px); }
}

.more__btn:hover svg,
.more__btn:focus-visible svg { animation-play-state: paused; }

.capability {
  padding: clamp(70px, 10vw, 120px) 0 0;
}

.capability__head {
  margin: 0 0 clamp(30px, 4vw, 46px);
  font-size: clamp(22px, 2.6vw, 30px);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-transform: uppercase;
}

.capability__list {
  display: grid;
  /* 290px, sized to the list it holds rather than guessed. "Documentary &
     Storytelling" is 254px on one line; the old 230px floor let auto-fit pack
     five columns at exactly 230px on a 1150px .wrap, which wrapped three of the
     six items at full desktop width.

     290 also fixes the row shape. Four columns would need 1160px and .wrap caps
     at 1150, so the count can only ever be 1, 2 or 3 — and six divides evenly by
     all three, so the list always ends on a complete row. Adding a seventh item
     breaks that: 7 only divides by 1, so re-check this floor if the list grows. */
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid rgba(255, 255, 255, 0.15);
}

.capability__list li {
  padding: 22px 0;
  font-size: clamp(15px, 1.6vw, 18px);
  font-weight: 300;
  letter-spacing: -0.01em;
}

/* ---------- Closing block ---------- */

.close {
  padding: clamp(80px, 12vw, 150px) 0 clamp(50px, 7vw, 90px);
  border-top: 1px solid rgba(255,255,255,0.14);
}

.close--light { border-top-color: rgba(0,0,0,0.14); }

/* Opt out of the divider where the closing section already follows something
   that has closed itself off — the facts block on the homepage opens with its
   own rule, and About's founder sign-off is the end of a thought rather than a
   section wanting a line under it. Project keeps its divider: there the closing
   ask follows body prose and needs the break. */
.close--flush { border-top: 0; }

.close--light .close__cta { border-color: rgba(0,0,0,0.28); }

.close--light .close__cta:hover,
.close--light .close__cta:focus-visible {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

.close__lead {
  margin: 0 0 40px;
  font-size: clamp(30px, 5.4vw, 62px);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1.02;
  text-transform: uppercase;
}

.close__cta {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 17px 28px;
  border: 1px solid rgba(255,255,255,0.3);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  transition: background 0.5s var(--ease), border-color 0.5s var(--ease), color 0.5s var(--ease);
}

.close__cta svg {
  width: 13px;
  height: 13px;
  transition: transform 0.5s var(--ease);
}

/* Red is the interaction state now, on both grounds. visual-system.md allows
   the accent for "an active state" as well as for the primary action, so this
   is the same rule doing a second job rather than red spreading as decoration:
   at rest the page still shows one red mark, or none. */
.close__cta:hover,
.close__cta:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.close--light .close__cta:hover,
.close--light .close__cta:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.close__cta:hover svg { transform: translateX(4px); }

/* Physical press, and the only way the accent can appear on a touch screen —
   there is no hover there, so without this the red states are dead on a phone. */
.close__cta:active,
.nav__cta:active,
.social:active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.close__cta:active { transform: translateY(1px); }

/* The social links carry the accent on their glyph rather than as a fill. */
.social:active { background: none; color: var(--accent); }

/* The one red mark on a view. Work carries two instances of this button —
   the hero and the close — so only the close takes the accent and the count
   stays at one. Hover drops the red rather than shifting its shade; two reds
   in a piece is the thing that breaks it. */
.close__cta--primary,
.close--light .close__cta--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Already red at rest, so hover presses to ink rather than reaching for a
   second shade of red, which the palette forbids. */
.close__cta--primary:hover,
.close__cta--primary:focus-visible,
.close--light .close__cta--primary:hover,
.close--light .close__cta--primary:focus-visible {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

/* ---------- Inverted pages ---------- */

.page-light {
  background: var(--paper);
  color: var(--ink);
}

.masthead {
  display: grid;
  place-items: center;
  padding: 40px 30px 0;
}

.masthead img { width: 74px; }

.wrap {
  width: min(1150px, 100% - 60px);
  margin-inline: auto;
}

/* Past ~1400px a centred container strands the opening headline in the middle
   of the screen with equal dead space either side. Only the intro anchors left:
   applied to every .wrap it dragged the footer with it, and a space-between
   footer rule stopping two thirds across reads as broken, not deliberate. */
@media (min-width: 1400px) {
  .intro .wrap { margin-inline: clamp(64px, 7vw, 150px) auto; }
}

.back {
  display: inline-block;
  margin: 70px 0 45px;
  font-size: 12px;
  letter-spacing: 0.16em;
  transition: opacity 0.4s var(--ease), transform 0.5s var(--ease);
}

.back:hover { opacity: 0.55; transform: translateX(-6px); }

.titleblock {
  margin: 0 0 30px;
  font-size: clamp(15px, 2vw, 19px);
  font-weight: 500;
  letter-spacing: 0.05em;
  line-height: 1.75;
  text-transform: uppercase;
}

.titleblock span {
  display: block;
  font-weight: 300;
  opacity: var(--dim);
}

.player {
  display: block;
  width: 100%;
  /* 16/9 until main.js reads the file's real shape; see the player block there */
  aspect-ratio: 16 / 9;
  margin-inline: auto;
  background: var(--ink);
  border: 0;
}

.player--empty {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 18px;
}

.player--empty img {
  width: 96px;
  opacity: 0.3;
  animation: breathe 9s ease-in-out infinite;
}

.player--empty p {
  margin: 0;
  color: rgba(255,255,255,0.45);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.credits {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 30px;
  margin: 50px 0 0;
  padding: 30px 0 0;
  border-top: 1px solid rgba(0,0,0,0.14);
}

.credits div p {
  margin: 0 0 6px;
  font-size: 13px;
  letter-spacing: 0.03em;
}

.credits div > .u-label,
.credits dt {
  display: block;
  margin-bottom: 12px;
  opacity: var(--dim);
}

.summary {
  max-width: 62ch;
  margin: 34px 0 0;
  font-size: clamp(15px, 1.7vw, 19px);
  font-weight: 300;
  line-height: 1.72;
  letter-spacing: -0.01em;
}

/* 34px is the gap that separates the prose from the video above it. Between
   paragraphs of the same passage it reads as a break rather than a rhythm, so
   consecutive ones sit closer. */
.summary + .summary { margin-top: 1.15em; }

.next {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 20px;
  margin-top: 54px;
  padding: 30px 0;
  border-top: 1px solid rgba(0,0,0,0.14);
  transition: opacity 0.4s var(--ease);
}

.next:hover { opacity: 0.55; }

.next .u-label { opacity: var(--dim); }

.next__title {
  font-size: clamp(24px, 3.6vw, 44px);
  font-weight: 500;
  letter-spacing: -0.03em;
  text-transform: uppercase;
}

.lede {
  max-width: 30ch;
  margin: 0 0 60px;
  font-size: clamp(30px, 6vw, 64px);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1.02;
  text-transform: uppercase;
}

.lede em { font-style: normal; opacity: 0.3; }

.cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 50px 40px;
  padding-bottom: 110px;
}

.cols p {
  margin: 0 0 16px;
  font-size: 13px;
  font-weight: 300;
  line-height: 1.95;
}

.cols > div > .u-label {
  display: block;
  margin-bottom: 12px;
  opacity: var(--dim);
}

.cols a {
  border-bottom: 1px solid currentColor;
  transition: opacity 0.4s var(--ease);
}

.cols a:hover { opacity: 0.5; }

/* ---------- Enquiry form ----------
   Square, mono, no radius — the button and label vocabulary already on the
   site, applied to inputs. Nothing here introduces a new surface language. */

.enquiry {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: 60px 70px;
  padding-bottom: 110px;
}

.form { display: grid; gap: 26px; }

.field { display: grid; gap: 10px; }

.field--pair {
  /* 230px, not 180px: "When it shoots Optional" is 214px on one line, so a
     180px floor let two columns survive at widths where neither could hold the
     label and "Optional" dropped to its own line. It only bit at the widths
     where two narrow columns happen to fit — 430px (iPhone Pro Max) and 834px
     (iPad Pro 11" portrait) — which is why it looked arbitrary. Tying the floor
     to the label's own width means two columns can only ever appear when both
     can hold it. The headroom over 214 is for engines that set the font wider. */
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 26px;
  /* Bottom-aligned so the two inputs line up whatever the labels do. "When it
     shoots Optional" wraps in a 180px column and "Budget Optional" does not, so
     top-aligning left the inputs 19px out of step. Aligning the ends keeps them
     level and lets the taller label grow upward. */
  align-items: end;
}

.field label { opacity: var(--dim); }

.form__opt {
  margin-left: 8px;
  letter-spacing: 0.14em;
  opacity: 0.7;
}

.form input,
.form textarea {
  width: 100%;
  padding: 14px 15px;
  border: 1px solid rgba(0, 0, 0, 0.24);
  border-radius: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  font-weight: 300;
  line-height: 1.6;
  transition: border-color 0.4s var(--ease);
}

.form textarea { resize: vertical; min-height: 120px; }

.form input::placeholder,
.form textarea::placeholder { color: var(--grey-mid); opacity: 1; }

.form input:hover,
.form textarea:hover { border-color: rgba(0, 0, 0, 0.45); }

.form input:focus,
.form textarea:focus { border-color: var(--ink); outline: none; }

/* The field itself carries the focus ring, so the outline would double up. */
.form input:focus-visible,
.form textarea:focus-visible { outline: none; }

/* Driven by aria-invalid rather than :invalid, so the border and the message a
   screen reader hears are always the same state — and nothing turns red until
   a submit has actually been attempted. */
.form [aria-invalid='true'] { border-color: var(--accent); }

.field__error {
  margin: 0;
  color: var(--accent);
  font-size: 12px;
  font-weight: 300;
  line-height: 1.5;
}

/* No empty gap sitting under every field in the clean state. */
.field__error:empty { display: none; }

.form__submit {
  justify-self: start;
  border: 1px solid var(--accent);
  cursor: pointer;
  font-family: inherit;
}

.form__submit[disabled] { cursor: default; opacity: 0.55; }

.form__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__status {
  margin: 0;
  font-size: 12px;
  line-height: 1.7;
  min-height: 1.7em;
}

.form__status.is-error { color: var(--accent); }

.form__done {
  margin: 0;
  padding: 26px 0;
  border-top: 1px solid rgba(0, 0, 0, 0.14);
  font-size: clamp(15px, 1.7vw, 19px);
  font-weight: 300;
  line-height: 1.72;
}

.enquiry__side { display: grid; align-content: start; gap: 34px; }

.enquiry__side > div > .u-label {
  display: block;
  margin-bottom: 12px;
  opacity: var(--dim);
}

.enquiry__side p {
  margin: 0;
  font-size: 13px;
  font-weight: 300;
  line-height: 1.95;
}

.enquiry__side a {
  border-bottom: 1px solid currentColor;
  transition: opacity 0.4s var(--ease);
}

.enquiry__side a:hover { opacity: 0.55; }

@media (max-width: 820px) {
  .enquiry { grid-template-columns: 1fr; gap: 50px; }
}

.foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 14px;
  padding: 26px 0 34px;
  border-top: 1px solid rgba(0,0,0,0.14);
}

/* Dimmed by colour rather than opacity. Opacity on the row would drag the
   social links down with it and the accent would land washed out on hover;
   the alpha here matches --dim so the contrast measurement is unchanged. */
.page-light .foot { color: rgba(0, 0, 0, 0.56); }

.page-dark .foot {
  border-top-color: rgba(255,255,255,0.15);
  color: rgba(255, 255, 255, 0.5);
}

.foot__social {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Mark and label share one grid cell, so the cell is as wide as the label and
   the row never reflows when the two swap. */
/* The padding is tap area, not spacing: the glyph is only 15px, which is under
   the 24px minimum on its own. It buys a 44px target without moving anything. */
.social {
  display: inline-grid;
  place-items: center;
  min-height: 16px;
  padding: 15px 0;
  transition: color 0.35s var(--ease);
}

.social__icon,
.social__name {
  grid-area: 1 / 1;
  transition: opacity 0.32s var(--ease), transform 0.32s var(--ease);
}

.social__icon {
  display: block;
  width: 15px;
  height: 15px;
}

.social__name {
  opacity: 0;
  transform: translateY(3px);
  white-space: nowrap;
}

.social:hover .social__icon,
.social:focus-visible .social__icon {
  opacity: 0;
  transform: translateY(-3px);
}

.social:hover .social__name,
.social:focus-visible .social__name {
  opacity: 1;
  transform: none;
}

.social:hover,
.social:focus-visible { color: var(--accent); }

/* ---------- Reveal ----------
   Long and soft, so entries read as drift rather than a snap. */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity var(--slow) var(--ease), transform var(--slow) var(--ease);
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* ---------- Responsive ---------- */

/* The headline breaks on its own sentence boundaries, which only holds while a
   24-character line fits. Below that the breaks would strand single words, so
   they drop out and the line wraps on its own. */
@media (max-width: 1100px) {
  .u-linebreak { display: none; }
}

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

@media (max-width: 768px) {
  :root { --nav-h: 62px; }
  .grid { grid-template-columns: repeat(2, 1fr); }

  /* Nav links sat at 24px tall, which is a miss on a thumb. The nav bar is
     62px, so there is room to take them to 44 without changing the layout. */
  .nav__link { padding: 16px 0; }
  .nav__cta { padding: 15px 15px; }

  /* iOS zooms the whole page when a focused field is under 16px, so every tap
     on this form would jolt the layout. 16px is the threshold, not a
     preference. */
  .form input,
  .form textarea { font-size: 16px; }

  /* Both were text-height only; the padding is tap area. */
  .back { margin: 31px 0 17px; padding: 15px 0; }
  .enquiry__side a { display: inline-block; padding: 14px 0; }
  .nav { gap: 16px; padding: 0 18px; }
  .nav__group { gap: 18px; }
  .wrap { width: calc(100% - 40px); }
  .back { margin: 46px 0 32px; }
}

@media (max-width: 520px) {
  .grid { grid-template-columns: 1fr; }
  .nav__link { font-size: 10px; letter-spacing: 0.12em; }
}

/* Scroll-focus stands in for hover on touch, so the lift stays modest and the
   tile is never scaled out past the edge of a narrow screen. */
@media (hover: none) {
  .tile.is-near { --lift: 1.04; }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .js [data-reveal] { opacity: 1; transform: none; }
  .veil { display: none; }
}

/* ---------- Not found ----------
   The other inner pages invert to paper. This one stays on ink so a dead URL
   does not look like another About page, and the code sits permanently soft:
   a frame that never pulled focus, which is the same lens language the rest of
   the site uses on hover. */

.page-404 {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

/* Pushes the footer to the bottom of the screen instead of letting it sit
   directly under short content. */
.notfound { flex: 1 0 auto; }
.page-404 > .wrap:last-of-type { margin-top: auto; }

.notfound {
  display: flex;
  align-items: center;
  padding: calc(var(--nav-h) + clamp(40px, 7vw, 90px)) 0 clamp(50px, 7vw, 80px);
}

.notfound__code {
  margin: 0 0 clamp(10px, 2vw, 22px);
  font-size: clamp(74px, 15vw, 190px);
  font-weight: 500;
  letter-spacing: -0.05em;
  line-height: 0.82;
  color: var(--paper);
  opacity: 0.13;
  filter: blur(3px);
  user-select: none;
}

.notfound__head {
  margin: 0 0 26px;
  font-size: clamp(32px, 6vw, 68px);
  font-weight: 500;
  letter-spacing: -0.035em;
  line-height: 1.0;
  text-transform: uppercase;
}

/* Reads as a status line off a machine, not a sentence written for you. */
.notfound__line {
  margin: 0 0 clamp(34px, 5vw, 52px);
  padding-top: 16px;
  border-top: 1px solid rgba(255, 255, 255, 0.15);
  opacity: var(--dim);
}

.notfound__routes {
  display: flex;
  flex-wrap: wrap;
  gap: 14px 34px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 13px;
  font-weight: 300;
}

.notfound__routes a {
  padding-bottom: 3px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.3);
  transition: color 0.4s var(--ease), border-color 0.4s var(--ease);
}

.notfound__routes a:hover,
.notfound__routes a:focus-visible {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

@media (max-width: 600px) {
  .notfound__routes { flex-direction: column; gap: 16px; }
}

/* ---------- Stills ----------
   Frames from the piece above, not a gallery. Gapless like the wall, so they
   read as part of the same body of work rather than a separate section. */

.stills {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0;
  margin: clamp(40px, 6vw, 70px) 0 0;
}

/* Full colour at rest, unlike the wall. These sit under a piece the visitor
   already chose to open, so the withholding has done its job by then and
   draining them would only make the frames harder to read. */
.stills img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* Sign-off at the end of About. The company line and the email were a column
   of facts that repeated the headline and the footer, so only this is left. */
.founder {
  margin: 0;
  padding: clamp(44px, 6vw, 72px) 0 0;
  /* Dimmed with colour, not opacity: [data-reveal] animates opacity from 0 to
     1, so an opacity here is either overwritten or fights the reveal and the
     line never appears. Same value .bts__line uses on this page. */
  color: rgba(0, 0, 0, 0.56);
}

/* ---------- Why Arkived exists / What we believe ----------
   Ricardo's own writing, and the only long-form prose on the site. Wider type
   and a measured line length than the .cols blocks beside it, because those are
   facts to scan and this is meant to be read. Same top rule as the On set strip
   so the page keeps one rhythm down its length. */

.manifesto {
  padding: clamp(50px, 8vw, 84px) 0 0;
  border-top: 1px solid rgba(0, 0, 0, 0.14);
}

.manifesto + .manifesto {
  padding-top: clamp(44px, 6vw, 70px);
}

.manifesto__head {
  margin: 0 0 clamp(20px, 3vw, 30px);
  font-size: clamp(22px, 2.6vw, 30px);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-transform: uppercase;
}

.manifesto__body {
  max-width: 68ch;
  padding-bottom: clamp(40px, 6vw, 64px);
}

.manifesto__body p {
  margin: 0 0 1.15em;
  font-size: clamp(14px, 1.5vw, 17px);
  font-weight: 300;
  line-height: 1.8;
  letter-spacing: -0.005em;
}

.manifesto__body p:last-child { margin-bottom: 0; }

/* ---------- Behind the scenes ----------
   Process instead of proof. With no client logos to show, evidence that the
   work happens is the trust signal available, and it is an honest one. Kept
   to a single strip on About rather than the homepage: the homepage is the
   work itself, and this is the answer to "can they actually do it". */

.bts {
  padding: clamp(50px, 8vw, 90px) 0 clamp(30px, 5vw, 50px);
  border-top: 1px solid rgba(0, 0, 0, 0.14);
}

.bts__head {
  margin: 0 0 12px;
  font-size: clamp(22px, 2.6vw, 30px);
  font-weight: 500;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

.bts__line {
  margin: 0 0 clamp(26px, 4vw, 40px);
  font-size: 13px;
  font-weight: 300;
  line-height: 1.9;
  color: rgba(0, 0, 0, 0.56);
}

/* Gapless, like the wall — the same language, at a smaller scale. */
.bts__strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0;
}

/* The cell holds the ratio so the clips inside can stack and crossfade. The
   strip was built for stills and the real set footage arrived vertical, which
   the 3/4 ratio already wanted. */
.bts__cell {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  /* Sits under the clips, so the tile is never the white page for the frame
     before the first one decodes. */
  background: #0d0d0d;
}

.bts__strip img,
.bts__cell video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.08) brightness(0.96);
  transition: filter 0.8s var(--ease);
}

.bts__strip img { aspect-ratio: 3 / 4; }

/* Two clips are stacked per cell so the next one is decoded and running before
   the current fades out. Opacity is driven inline from main.js, which beats
   :first-child without a specificity fight; the rule below is only the state
   before JS gets there, and the whole no-JS case. */
.bts__cell video {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.7s var(--ease), filter 0.8s var(--ease);
}

.bts__cell video:first-child { opacity: 1; }

.bts__strip img:hover,
.bts__cell:hover video { filter: grayscale(0) contrast(1) brightness(1); }

@media (max-width: 700px) {
  .bts__strip { grid-template-columns: repeat(2, 1fr); }
}
