﻿/* =====================================================================
   Self-hosted type (assets/fonts) — no third-party font origins.
   One variable Archivo file covers 400–900; "Archivo Expanded" is the
   true wdth-125 cut of Archivo, registered under the family name the
   hero line already asks for. (The old Google Fonts request for
   "Archivo Expanded" returned 400 — that family never existed there —
   so the hero line silently fell back to regular Archivo.)
   url() resolves against this stylesheet (/css/styles.css), so the
   ../assets paths work from both / and /admin/.
   ===================================================================== */
@font-face {
  font-family: "Anton";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/anton-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+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url("../assets/fonts/archivo-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+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Archivo Expanded";
  font-style: normal;
  font-weight: 800;
  font-stretch: 125%;
  font-display: swap;
  src: url("../assets/fonts/archivo-expanded-800-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+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@property --theme-luma {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

@property --theme-bg-channel {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

@property --theme-fg-channel {
  syntax: "<number>";
  inherits: true;
  initial-value: 255;
}

@property --theme-inverse-channel {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

:root {
  --black: #000;
  --white: #fff;
  --ink: #090909;
  --theme-bg-channel: 0;
  --theme-fg-channel: 255;
  --theme-inverse-channel: 0;
  --theme-bg-rgb: var(--theme-bg-channel), var(--theme-bg-channel), var(--theme-bg-channel);
  --theme-fg-rgb: var(--theme-fg-channel), var(--theme-fg-channel), var(--theme-fg-channel);
  --theme-inverse-rgb: var(--theme-inverse-channel), var(--theme-inverse-channel), var(--theme-inverse-channel);
  --theme-luma: 0;
  --theme-grid-opacity: 0.18;
  --theme-ambient-opacity: 0.18;
  --theme-field-opacity: 0.56;
  --theme-canvas-opacity: 0.46;
  --theme-grain-opacity: 0.045;
  --soft: rgba(var(--theme-fg-rgb), 0.88);
  --muted: rgba(var(--theme-fg-rgb), 0.66);
  --line: rgba(var(--theme-fg-rgb), 0.18);
  --dark-line: rgba(var(--theme-fg-rgb), 0.14);
  --accent: rgb(var(--theme-fg-rgb));
  --pad: clamp(18px, 3vw, 44px);
  /* Notched phones (landscape especially): never tuck content under the
     safe area. Second declaration wins where env() is supported. */
  --pad: max(clamp(18px, 3vw, 44px), env(safe-area-inset-left), env(safe-area-inset-right));
  --max: 1240px;
  --scroll: 0;
  --display: "Anton", "Arial Narrow", Impact, sans-serif;
  --body: "Archivo", "Helvetica Neue", Arial, sans-serif;
  /* Motion tokens — every new animation should pick from these so the
     whole site shares one timing voice. --ease aliases --ease-out for
     the many existing rules that reference it. */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-snap: cubic-bezier(0.3, 1.25, 0.35, 1);
  --ease: var(--ease-out);
  --motion-fast: 220ms;
  --motion-med: 460ms;
  --motion-slow: 700ms;
  --duration-fast: var(--motion-fast);
  --duration-medium: var(--motion-med);
  --duration-slow: var(--motion-slow);
  --stagger-small: 70ms;
  --stagger-medium: 120ms;
  --section-p: 0;
  --hero-p: 0;
  --theme-p: 0;
  --reveal-y: clamp(18px, 2.4vw, 28px);
  --reveal-scale: 0.985;
  --parallax-depth: clamp(24px, 4svh, 48px);
  color-scheme: dark;
  transition:
    --theme-luma 180ms linear,
    --theme-bg-channel 180ms linear,
    --theme-fg-channel 240ms var(--ease),
    --theme-inverse-channel 240ms var(--ease);
}

* {
  box-sizing: border-box;
}

/* Deliberately NO `scroll-behavior: smooth` here. It animates *every* scroll
   the browser performs, including a scrollbar-thumb drag — the page then eases
   toward the thumb instead of tracking it, so the content (and the scroll-linked
   motion below) visibly trails the cursor. The nav's smooth glide is applied
   per-click in app.js instead, where it belongs. */
html {
  overflow-x: clip;
  -webkit-tap-highlight-color: transparent;
}

/* Selection inverts with the theme — no stock blue on a monochrome site. */
::selection {
  background: rgb(var(--theme-fg-rgb));
  color: rgb(var(--theme-bg-rgb));
  text-shadow: none;
}

body {
  min-width: 0;
  margin: 0;
  overflow-x: clip;
  overflow-y: visible;
  background: rgb(var(--theme-bg-rgb));
  color: rgb(var(--theme-fg-rgb));
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background-color 80ms linear, color 180ms ease;
}

@media (min-width: 761px) {
  html {
    overflow-y: scroll;
    scrollbar-gutter: stable;
    scrollbar-color: rgba(var(--theme-fg-rgb), 0.4) rgb(var(--theme-bg-rgb));
    scrollbar-width: thin;
  }

  html::-webkit-scrollbar {
    width: 8px;
  }

  html::-webkit-scrollbar-track {
    background: rgb(var(--theme-bg-rgb));
  }

  html::-webkit-scrollbar-thumb {
    border: 2px solid rgb(var(--theme-bg-rgb));
    border-radius: 999px;
    background: rgba(var(--theme-fg-rgb), 0.4);
  }
}

body.is-light {
  color-scheme: light;
}

body::before,
body::after {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  content: "";
  transition: opacity 220ms ease, filter 220ms ease;
}

body::before {
  background:
    linear-gradient(rgba(var(--theme-fg-rgb), 0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(var(--theme-fg-rgb), 0.04) 1px, transparent 1px);
  background-size: clamp(62px, 8vw, 118px) clamp(62px, 8vw, 118px);
  opacity: var(--theme-grid-opacity);
  transform: translate3d(0, calc(var(--scroll) * -55px), 0);
}

body::after {
  background:
    radial-gradient(circle at 100% 40%, rgba(var(--theme-fg-rgb), 0.14), transparent 30%),
    linear-gradient(90deg, rgba(var(--theme-inverse-rgb), 0.14), transparent 22%, transparent 76%, rgba(var(--theme-fg-rgb), 0.08)),
    linear-gradient(180deg, transparent 0 58%, rgba(var(--theme-inverse-rgb), 0.22));
  opacity: var(--theme-ambient-opacity);
}

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

button {
  font: inherit;
}

/* Visually hidden but available to assistive tech. Used for the clean,
   readable heading text behind the per-character split animation. */
.sr-only {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.nerve-canvas {
  position: fixed;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  opacity: var(--theme-canvas-opacity);
  pointer-events: none;
  transition: opacity 220ms ease;
}

/* The baked depth layers + source glow live here as compositor-promoted
   elements that app.js slides with `transform` (no per-frame canvas repaint —
   the big mobile win). #nerveCanvas above stays the live pulse overlay. */
.nerve-field {
  position: fixed;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  opacity: var(--theme-canvas-opacity);
  pointer-events: none;
  transition: opacity 220ms ease;
}

.nerve-field .nerve-layer,
.nerve-field .nerve-source {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
  transform: translateZ(0);
  backface-visibility: hidden;
}

.background-field {
  position: fixed;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
  background:
    radial-gradient(circle at 100% 40%, rgba(var(--theme-fg-rgb), 0.16), transparent 33%),
    linear-gradient(90deg, rgba(var(--theme-inverse-rgb), 0.52), rgba(var(--theme-inverse-rgb), 0.2) 48%, rgba(var(--theme-inverse-rgb), 0.06));
  opacity: var(--theme-field-opacity);
  transition: filter 220ms ease, opacity 220ms ease;
}

/* Oversized by the grain-shift travel so the transform-based jitter never
   exposes an edge. Translating the layer is compositor-only; the old
   background-position keyframes repainted the full screen on every step. */
.grain {
  position: fixed;
  inset: -48px;
  z-index: 3;
  pointer-events: none;
  opacity: var(--theme-grain-opacity);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
}

@keyframes grainShift {
  0% { transform: translate3d(0, 0, 0); }
  20% { transform: translate3d(-37px, 29px, 0); }
  40% { transform: translate3d(31px, -41px, 0); }
  60% { transform: translate3d(-23px, 19px, 0); }
  80% { transform: translate3d(43px, 31px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* =====================================================================
   Boot loader. Deliberately hardcoded black/white — the page always
   boots dark at the top, outside the theme system. app.js dismisses it
   (.is-done + body.is-ready) once the hero image and fonts land; the
   failsafe animation hides it after 6s even if JS never finishes, and
   a <noscript> style removes it entirely without JS. pointer-events is
   none throughout so it can never trap the reader.
   ===================================================================== */
.loader {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: grid;
  place-items: center;
  background: #191919;
  color: #fff;
  pointer-events: none;
  transition: transform 680ms var(--ease-out) 140ms, visibility 0s 880ms;
  animation: loaderFailsafe 1ms linear 6s forwards;
}

.loader.is-done {
  visibility: hidden;
  transform: translateY(-101%);
}

@keyframes loaderFailsafe {
  to {
    visibility: hidden;
    opacity: 0;
  }
}

.loader-inner {
  display: grid;
  gap: 16px;
  justify-items: center;
  transition: opacity 220ms ease;
}

.loader.is-done .loader-inner {
  opacity: 0;
}

.loader-mark {
  opacity: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.loader-logo {
  display: block;
  width: min(58vw, 300px);
  height: auto;
}

body.fonts-ready .loader-mark {
  opacity: 1;
  animation: loaderMarkIn 700ms var(--ease-out) backwards;
}

@keyframes loaderMarkIn {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
}

.loader-line {
  width: min(220px, 44vw);
  height: 2px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.16);
}

.loader-line i {
  display: block;
  width: 44%;
  height: 100%;
  background: #fff;
  transform: translateX(-110%);
  animation: loaderSweep 1.1s ease-in-out 200ms infinite;
}

@keyframes loaderSweep {
  to { transform: translateX(260%); }
}

@media (prefers-reduced-motion: reduce) {
  .loader {
    display: none;
  }
}

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  width: calc(100% - (var(--pad) * 2));
  min-height: 74px;
  margin: 0 auto;
  padding-top: 14px;
}

.brand {
  border: 1px solid var(--line);
  background: rgba(var(--theme-bg-rgb), 0.78);
  color: rgb(var(--theme-fg-rgb));
  text-shadow: 0 1px 8px rgba(var(--theme-inverse-rgb), 0.32);
  backdrop-filter: blur(12px);
}

body.is-light .brand {
  border-color: var(--line);
  background: rgba(var(--theme-bg-rgb), 0.72);
  text-shadow: none;
}

/* THE MARK IS TYPE, NOT THE LOGO FILE, and that is a decision rather than an
   oversight: v0.8.9 swapped it for assets/nerve-media-logo.png and it was
   swapped straight back. The tile brings its own black field into a header
   that is a translucent bordered box, so it reads as a sticker sitting on the
   bar instead of as part of it, and at 46px the two stacked words inside a
   512px square lose most of their weight. Set in Archivo at the same tracking
   as the nav, the mark is sharp at every density and costs nothing. */
.brand {
  display: inline-flex;
  align-items: center;
  min-height: 46px;
  padding: 0 15px;
  font-family: var(--body);
  font-size: clamp(0.68rem, 2vw, 0.8rem);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  transition: transform 220ms var(--ease), border-color 220ms ease, background 220ms ease;
}

.brand:hover,
.brand:focus-visible {
  transform: translateY(-2px);
  border-color: currentColor;
}

.brand-mark {
  position: relative;
  white-space: nowrap;
}

.brand-mark::after {
  position: absolute;
  left: 0;
  bottom: -3px;
  width: 100%;
  height: 1px;
  content: "";
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 360ms var(--ease);
}

.brand:hover .brand-mark::after,
.brand:focus-visible .brand-mark::after {
  transform: scaleX(1);
}

.brand-home {
  margin-right: auto;
}

.section-frame {
  width: min(calc(100% - (var(--pad) * 2)), var(--max));
  margin: 0 auto;
}

.hero {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  align-items: end;
  gap: clamp(18px, 2.2vw, 34px);
  width: 100%;
  max-width: none;
  min-height: 100vh;
  min-height: 100svh;
  padding: clamp(124px, 16vh, 164px) var(--pad) clamp(58px, 8vh, 96px);
  overflow: hidden;
}

.hero::before {
  position: absolute;
  inset: 0;
  z-index: 1;
  content: "";
  pointer-events: none;
  background:
    linear-gradient(90deg, rgba(0, 0, 0, 0.66) 0%, rgba(0, 0, 0, 0.36) 40%, rgba(0, 0, 0, 0.08) 68%, transparent 100%),
    linear-gradient(180deg, rgba(0, 0, 0, 0.4) 0%, transparent 30%, transparent 56%, rgba(0, 0, 0, 0.46) 84%, transparent 100%);
}

/* Full-bleed hero background photo. Its base is masked to transparent so the
   fixed nerve-network canvas behind the hero shows through — the image
   dissolves into the nerves instead of stopping on a hard edge. */
.hero-media {
  /* THE RENDERED SIZE OF A `cover` IMAGE, WRITTEN OUT. The shot is 1600x854,
     the box is the viewport, so cover scales it by max(w/1600, h/854) and the
     picture ends up max(100vw, 100svh * 1600/854) wide and max(100svh,
     100vw * 854/1600) tall - bigger than the box on one axis, which is the
     crop. .hero-paint lays a box of exactly that size over it, so a
     percentage inside that box is a coordinate in the PHOTOGRAPH, and the
     mark stays on the same patch of wall at every window shape.

     Two things this leans on: object-position is 50% 50% here (the centres
     coincide), and the hero is exactly one screen tall on desktop. */
  --hero-img-w: max(100vw, 187.354svh);
  --hero-img-h: max(100svh, 53.375vw);
  position: absolute;
  inset: 0;
  z-index: 0;
  margin: 0;
  overflow: hidden;
  background: #08080b;
  -webkit-mask-image: linear-gradient(to bottom, #000 0 62%, rgba(0, 0, 0, 0.9) 74%, rgba(0, 0, 0, 0.4) 88%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0 62%, rgba(0, 0, 0, 0.9) 74%, rgba(0, 0, 0, 0.4) 88%, transparent 100%);
}

.hero-media::after {
  position: absolute;
  inset: 0;
  content: "";
  pointer-events: none;
  background: linear-gradient(120deg, rgba(0, 0, 0, 0.12), transparent 46%, rgba(255, 191, 138, 0.05));
  mix-blend-mode: soft-light;
}

.hero-media-picture {
  display: block;
  width: 100%;
  height: 100%;
}

/* THE SHOT IS LANDSCAPE (1600x853) AND THE HERO IS NOT, so `cover` throws
   away width, and how much depends entirely on the viewport: a 16:10 desktop
   still shows 85% of the frame, a 390px phone shows 25% of it. That is why
   the horizontal anchor moves at the breakpoints below instead of sitting at
   50% everywhere.

   It moves LEFT as the viewport narrows, toward the window light and the
   plant shadows. The wall carries a painted NERVE MEDIA mark between roughly
   44% and 75% of the width, and a narrow crop through it lands on half
   letters, and half a letter reads as a mistake rather than as a backdrop.
   The photograph's own painted wordmark is the hero's only branding now.

   NO scaleX(-1) HERE. The old portrait shot was mirrored so its subject
   faced into the page; mirroring this one would run the wall text
   backwards. */
.hero-media-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  filter: saturate(1.03) contrast(1.05);
  /* Decorative background only. */
  pointer-events: none;
}

/* THE PAINT. Both crops are the same room with a bare wall, so the mark that
   used to be painted into the photograph is drawn back on here.

   The box it sits in IS the photograph (see --hero-img-* above), so every
   offset below is read straight off the frame and holds at any window shape.
   Colour is not a colour: `multiply` at 0.42 darkens whatever the wall is
   already doing by 42%, so the letters take on its texture, its warm falloff
   and the window shadow crossing them, and stay dark where the wall is dark.
   A flat fill cannot do that, and that is the difference between paint and a
   sticker.

   .hero-media's mask makes the stacking context that keeps the blend on the
   photo. Do not move these two out of it. */
.hero-paint {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--hero-img-w);
  height: var(--hero-img-h);
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/* IT IS A SHADOW, SO IT BEHAVES LIKE ONE. Three things do that work: it is
   soft (a cast shadow has no crisp edge - the blur is 3px of a 1600px frame),
   it leans (skewX, as a shape projected onto a wall by light coming in from
   the side does), and it MOVES WITH THE SCROLL. --section-p is the hero's own
   0-to-1 progress, already written by app.js's FX loop, so this costs no new
   JavaScript and stays a pure function of scroll position: the shadow drifts,
   softens and thins as the page moves under it, the way the room's light
   would carry it. It comes to rest the instant scrolling does. */
.hero-paint-line,
.hero-paint-mark {
  --sp: var(--section-p, 0);
  position: absolute;
  opacity: calc(0.46 - var(--sp) * 0.17);
  mix-blend-mode: multiply;
  transform:
    translate3d(calc(var(--hero-img-w) * var(--sp) * 0.016), calc(var(--hero-img-h) * var(--sp) * 0.008), 0)
    skewX(-3deg);
  transform-origin: 0 100%;
  will-change: transform, opacity, filter;
}

.hero-paint-line {
  margin: 0;
  color: #14120f;
  font-family: var(--body);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  text-shadow: none;
}

.hero-paint-mark {
  height: auto;
  /* The file is white on transparent; brightness(0) keeps its alpha and makes
     the ink black. The blur is the shadow's edge, and it grows as the shadow
     travels - the further light throws a shape, the softer it lands. */
  filter: brightness(0) blur(calc(var(--hero-img-w) * (0.0019 + var(--sp) * 0.0022)));
}

/* Type this small cannot take the mark's blur without turning to mud, so it
   gets a tenth of it. */
.hero-paint-line {
  filter: blur(calc(var(--hero-img-w) * (0.0004 + var(--sp) * 0.0007)));
}

/* THE ENTRANCE IS THE LIGHT ARRIVING, not the mark sliding in. It starts wide
   of where it lands, far too soft to read and at nothing, and gathers: that is
   what a shadow does as the sun clears the window frame. Only `from` is
   declared, so it resolves to whatever the scroll-driven rules above compute -
   at load that is the resting state, so there is no snap at the handoff. */
@keyframes heroPaintIn {
  from {
    opacity: 0;
    filter: brightness(0) blur(calc(var(--hero-img-w) * 0.014));
    transform: translate3d(calc(var(--hero-img-w) * -0.018), 0, 0) skewX(-11deg);
  }
}

@keyframes heroPaintLineIn {
  from {
    opacity: 0;
    filter: blur(calc(var(--hero-img-w) * 0.006));
    transform: translate3d(calc(var(--hero-img-w) * -0.012), 0, 0) skewX(-9deg);
  }
}

body.fx.is-ready .hero-paint-mark {
  animation: heroPaintIn 1600ms var(--ease-out) 0.2s backwards;
}

body.fx.is-ready .hero-paint-line {
  animation: heroPaintLineIn 1300ms var(--ease-out) 0.62s backwards;
}

/* No travel, no gather - the shadow is simply there, at its resting softness. */
@media (prefers-reduced-motion: reduce) {
  .hero-paint-line,
  .hero-paint-mark {
    opacity: 0.44;
    transform: skewX(-3deg);
    animation: none;
    will-change: auto;
  }

  .hero-paint-mark {
    filter: brightness(0) blur(calc(var(--hero-img-w) * 0.0019));
  }

  .hero-paint-line {
    filter: none;
  }
}

/* Landscape crop: where the paint was, 44% across and a tenth of the way
   down. The line's size is held to the FRAME, not to the viewport, or it
   drifts off the mark's width as the window changes shape - 21px of a 1600px
   frame, and the mark 490px of it.

   THE min() IS NOT DECORATION. 44% is a coordinate in the photograph, and a
   window taller than it is wide crops the photograph hard from both sides: at
   820x1180 the frame is 2211 wide and only the middle 820 of it is on screen,
   which cuts NERVE MEDIA. off after the D. The second term is the furthest
   left edge that still lands the mark inside the visible window with 3vw to
   spare (50% + 50vw is the window's right edge in frame coordinates, less the
   mark's own 30.6%), and the smaller of the two wins. On any ordinary
   landscape window the second term is far to the right and 44.1% stands. */
.hero-paint-line {
  top: 19%;
  left: min(44.1%, calc(19.4% + 47vw));
  font-size: calc(var(--hero-img-w) * 0.0132);
}

.hero-paint-mark {
  top: 25.8%;
  left: min(44.1%, calc(19.4% + 47vw));
  width: 30.6%;
}

/* Scroll cue — a hairline that repeatedly draws downward at the hero's
   base. Desktop + fx only; it dissolves as soon as scrolling starts. */
.hero-cue {
  position: absolute;
  bottom: clamp(22px, 4vh, 42px);
  left: 50%;
  z-index: 2;
  display: none;
  width: 1px;
  height: 54px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.2);
  transform: translateX(-50%);
}

body.fx .hero-cue {
  display: block;
  opacity: calc(1 - var(--hero-p, 0) * 3.4);
}

.hero-cue i {
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0.85);
  transform: translateY(-101%);
  animation: cueSweep 2.6s var(--ease) 1.6s infinite;
}

@keyframes cueSweep {
  0% { transform: translateY(-101%); }
  52% { transform: translateY(0); }
  100% { transform: translateY(101%); }
}

@media (max-width: 960px) {
  body.fx .hero-cue {
    display: none;
  }
}

@media (min-width: 961px) {
  .manifesto {
    position: relative;
    z-index: 1;
  }
}

.hero-copy {
  position: relative;
  z-index: 2;
  grid-column: 1 / 9;
  max-width: 930px;
}

.hero .eyebrow {
  color: rgba(255, 255, 255, 0.82);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.72);
}

.eyebrow,
.section-label {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 18px;
  color: var(--muted);
  font-family: var(--body);
  font-size: 0.74rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  text-shadow: 0 1px 5px rgba(0, 0, 0, 0.7);
}

.eyebrow::before,
.section-label::before {
  width: 22px;
  height: 1px;
  content: "";
  background: currentColor;
  opacity: 0.7;
}

body.is-light .eyebrow,
body.is-light .section-label {
  color: rgba(var(--theme-fg-rgb), 0.56);
  text-shadow: none;
}

.section-label.dark {
  color: rgba(var(--theme-fg-rgb), 0.56);
  text-shadow: none;
}

h1,
h2,
h3,
p {
  text-wrap: balance;
}

.eyebrow,
.section-label,
h1,
h2,
h3,
p {
  transition: color 240ms ease, text-shadow 240ms ease;
}

h1,
h2,
h3 {
  text-shadow: 0 1px 7px rgba(0, 0, 0, 0.72);
}

body.is-light h1,
body.is-light h2,
body.is-light h3,
.contact h2 {
  text-shadow: none;
}

p {
  text-shadow: 0 1px 5px rgba(0, 0, 0, 0.58);
}

body.is-light p,
.contact p {
  text-shadow: none;
}

.button,
.contact-panel {
  text-shadow: none;
}

.message-card,
.message-card * {
  text-shadow: 0 1px 5px rgba(0, 0, 0, 0.72);
}

.hero h1 {
  position: relative;
  margin: 0;
  color: #fff;
  font-family: var(--display);
  font-size: clamp(5rem, 13vw, 12.8rem);
  font-weight: 400;
  line-height: 0.82;
  text-transform: uppercase;
  letter-spacing: 0.005em;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.42);
}

.hero h1 span {
  display: block;
  transform-origin: left center;
}

.hero h1 .char {
  display: inline-block;
}

/* Entrances key off body.is-ready (added when the boot loader dismisses)
   so the choreography plays as the loader wipes away, not hidden behind
   it. Before is-ready the title sits static under the opaque loader. */
body.is-ready .hero h1[data-animate] .char {
  transform: translateY(108%) rotate(6deg);
  opacity: 0;
  animation: charRise 0.9s var(--ease) forwards;
}

@keyframes charRise {
  to {
    transform: translateY(0) rotate(0);
    opacity: 1;
  }
}

/* Save-Data fallback: the two words rise as units instead of per character. */
body.is-ready .hero h1[data-animate="words"] [data-split] {
  animation: wordRise 0.8s var(--ease-out) backwards;
}

@keyframes wordRise {
  from {
    transform: translateY(0.45em);
    opacity: 0;
  }
}

/* Hero entrance choreography — visual settles, title rises (above), then
   eyebrow → tagline → CTA → cue. Transform/opacity only, fx-scoped so
   reduced-motion and no-JS render the hero fully static. */
@keyframes heroEnter {
  from {
    transform: translateY(18px);
    opacity: 0;
  }
}

body.fx.is-ready .hero .eyebrow {
  animation: heroEnter var(--duration-medium) ease-out 0.1s backwards;
}

body.fx.is-ready .hero-actions {
  animation: heroEnter var(--duration-slow) var(--ease-out) 0.86s backwards;
}

body.fx.is-ready .hero-media {
  animation: heroVisualIn var(--motion-slow) var(--ease-out) backwards;
}

body.fx.is-ready .hero-logo-title {
  animation: heroEnter var(--duration-slow) var(--ease-out) 0.4s backwards;
}

@keyframes heroVisualIn {
  from { opacity: 0; }
}

.hero h1 {
  transition: text-shadow 420ms ease, color 420ms ease;
}

.hero h1:hover {
  text-shadow: 0 0 18px rgba(255, 255, 255, 0.22), 0 1px 7px rgba(0, 0, 0, 0.72);
}

/* Wordmark lockup — replaces the "Nerve Media" display type + tagline.
   Sized to sit where the headline did; drop-shadow keeps it legible over
   the brighter frames of the background video. */
.hero-logo-title {
  margin: 0;
  line-height: 0;
}

.hero-logo {
  display: block;
  width: clamp(232px, 33vw, 520px);
  height: auto;
  filter: drop-shadow(0 3px 22px rgba(0, 0, 0, 0.6));
}

.hero-actions {
  position: relative;
  z-index: 2;
  grid-column: 7 / -1;
  justify-self: end;
  width: min(380px, 100%);
  display: grid;
  gap: 18px;
  margin-bottom: 10px;
}

.button {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-height: 76px;
  padding: 0 20px;
  overflow: hidden;
  border: 1px solid currentColor;
  background: rgb(var(--theme-fg-rgb));
  color: rgb(var(--theme-inverse-rgb));
  font-family: var(--body);
  font-weight: 900;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  transition: transform 240ms var(--ease), box-shadow 240ms var(--ease);
}

.button > span {
  font-size: 0.78rem;
  letter-spacing: 0.14em;
}

.button-arrow {
  position: absolute;
  right: 18px;
  top: 50%;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgb(var(--theme-inverse-rgb));
  color: rgb(var(--theme-fg-rgb));
  transform: translateY(-50%);
  transition: transform 280ms var(--ease);
}

.button-arrow svg {
  width: 15px;
  height: 15px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.button:hover .button-arrow,
.button:focus-visible .button-arrow {
  transform: translateY(-50%) translateX(4px);
}

.button strong {
  white-space: nowrap;
}

.button:hover,
.button:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 14px 40px rgba(var(--theme-fg-rgb), 0.16);
}

.manifesto {
  padding: clamp(54px, 10vw, 150px) 0;
}

.manifesto h2,
.services h2,
.contact h2 {
  max-width: 900px;
  margin: 0;
  font-family: var(--display);
  font-size: clamp(3rem, 8vw, 8rem);
  font-weight: 400;
  line-height: 0.92;
  text-transform: uppercase;
  letter-spacing: 0.004em;
}

.manifesto p {
  max-width: 720px;
  margin: 26px 0 0;
  color: var(--soft);
  font-size: clamp(1.04rem, 1.8vw, 1.28rem);
  line-height: 1.5;
}

.contact-copy p {
  max-width: 56ch;
  margin: clamp(16px, 2vw, 24px) 0 0;
  color: var(--soft);
  font-size: clamp(1rem, 1.5vw, 1.16rem);
  line-height: 1.5;
}

[data-pulse-item] {
  isolation: isolate;
}

[data-pulse-item]:not(.site-header) {
  position: relative;
}

/* ==========================================================================
   ABOUT: mission and vision, and a section that writes itself in.

   THE MOTION IS THE WORDS, AND IT IS ALL ONE NUMBER. app.js writes `--ab` on
   the section (0 as it enters the viewport, 1 as it leaves) and wraps every
   line's words in spans carrying `--wi` (their index) and `--n` (how many
   there are). Each word then derives its OWN progress from `--ab` in a
   calc, so the text wipes into place left to right with no per-frame
   JavaScript, no timers and no class churn: scroll back up and it runs
   backwards, because it is a pure function of scroll POSITION. Never derive
   anything here from speed or deltas.

   There WAS a soft warm light band across this section. Against a monochrome
   page it read as lens flare rather than as daylight, so it is gone. Do not
   reintroduce a colour cast here.

   The cards rise on the same value while their index numerals drift the
   other way, so the block parallaxes against itself instead of sliding up as
   one slab. All of it is gated behind `body.fx`; without it the section is a
   plain, finished, static layout.
   ========================================================================== */

.about {
  position: relative;
  isolation: isolate;
  width: 100%;
  padding: clamp(46px, 5.2vw, 84px) var(--pad) clamp(34px, 4vw, 62px);
  color: rgb(var(--theme-fg-rgb));
}

.about-title {
  margin: 0;
  font-family: var(--display);
  /* One line on a desktop, two on a phone. The vh term keeps short-and-wide
     windows from overshooting, the same guard the contact heading carries. */
  font-size: clamp(2.2rem, min(6vw, 11vh), 5.2rem);
  font-weight: 400;
  line-height: 0.94;
  text-transform: uppercase;
  letter-spacing: 0.004em;
}

/* Anton has no italic cut, so emphasis is a calmer second tone. Same trick as
   the case-study heading, deliberately, so the page keeps one voice. */
.about-title em {
  font-style: normal;
  color: var(--muted);
}

.about-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(14px, 2vw, 30px);
  margin-top: clamp(26px, 3.4vw, 48px);
}

.about-card {
  position: relative;
  display: grid;
  align-content: start;
  gap: clamp(8px, 1vw, 13px);
  padding: clamp(22px, 2.6vw, 38px);
  border-top: 1px solid rgba(var(--theme-fg-rgb), 0.22);
  border-bottom: 1px solid rgba(var(--theme-fg-rgb), 0.22);
  background:
    linear-gradient(90deg, rgba(var(--theme-fg-rgb), 0.07), transparent 54%),
    linear-gradient(180deg, rgba(var(--theme-fg-rgb), 0.05), transparent 42%);
  /* The same readability wash the contact panel uses, so the nerve lines read
     as atmosphere behind the card rather than across the words. */
  box-shadow: inset 0 0 0 100vmax rgba(var(--theme-bg-rgb), 0.62);
}

.about-index {
  font-family: var(--display);
  font-size: clamp(1.5rem, 2.4vw, 2.2rem);
  line-height: 1;
  color: rgba(var(--theme-fg-rgb), 0.26);
  letter-spacing: 0.02em;
}

.about-head {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(1.7rem, 3.2vw, 2.9rem);
  font-weight: 400;
  line-height: 0.98;
  text-transform: uppercase;
  letter-spacing: 0.006em;
}

/* Drawn left to right as the card arrives. transform-origin is why this is a
   `scale` and not a `width`: width would relayout the card every frame. */
.about-rule {
  display: block;
  width: clamp(46px, 6vw, 78px);
  height: 1px;
  background: rgb(var(--theme-fg-rgb));
  opacity: 0.55;
  transform-origin: left center;
}

.about-lead {
  max-width: 42ch;
  margin: clamp(4px, 0.6vw, 8px) 0 0;
  color: rgba(var(--theme-fg-rgb), 0.86);
  font-size: clamp(1rem, 1.35vw, 1.16rem);
  line-height: 1.5;
}

.about-note {
  max-width: 42ch;
  margin: 0;
  color: var(--soft);
  font-size: clamp(0.92rem, 1.1vw, 1rem);
  line-height: 1.5;
}

/* ---- the word split ------------------------------------------------------
   app.js wraps every word of the heading, both card headings and both leads
   in `.aw`, carrying `--wi` (this word's index) and `--n` on the line. The
   display lines are also masked: `.aw-mask` clips, `.aw-i` is the only thing
   that moves, so a word can rise from behind its own baseline instead of
   just fading in.

   The padding/negative-margin pair is what stops the mask from shaving
   descenders and the comma off the heading. It buys the box room below the
   baseline without moving anything on the page. */
.about .aw {
  display: inline-block;
}

.about .aw-mask {
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}

.about .aw-i {
  display: inline-block;
  will-change: transform;
}

/* The heading's second tone survives the split: app.js marks the words that
   came out of the <em> and the colour rides on the class, not the element. */
.about-title .aw.is-em .aw-i {
  color: var(--muted);
}

/* ---- scroll-driven state (FX only) --------------------------------------
   `--ab` runs 0 to 1 across the viewport, and EVERY value below is derived
   from it in a calc. Nothing here is on a timer, so the section runs
   backwards when you scroll back up, and the rAF loop still sleeps the
   moment the scroll stops.

   Each line spreads its own progress across its words: multiplying by
   `--n + 4` and subtracting `--wi` gives word 0 a head start and the last
   word the tail, and dividing by the overlap (2.4) is what keeps it reading
   as one wipe rather than as a queue of separate fades. */
body.fx .about {
  --ab: 0;
}

/* The heading is at the top of the section, so it finishes early: by the
   time the cards are arriving it is already set. */
body.fx .about-title {
  --tp: clamp(0, (var(--ab) - 0.02) * 3.4, 1);
}

body.fx .about-title .aw-i,
body.fx .about-head .aw-i {
  --wp: clamp(0, (var(--lp, var(--tp, 0)) * (var(--n) + 4) - var(--wi)) / 2.4, 1);
  translate: 0 calc((1 - var(--wp)) * 108%);
  opacity: calc(0.08 + var(--wp) * 0.92);
}

body.fx .about-card {
  --p: clamp(0, (var(--ab) - 0.06 - var(--i) * 0.07) * 3.4, 1);
  /* The card heading rides the card's own arrival; the lead trails it, so the
     words are still writing themselves in after the card has landed. */
  --lp: var(--p);
  --bp: clamp(0, (var(--ab) - 0.12 - var(--i) * 0.06) * 2, 1);
  translate: 0 calc((1 - var(--p)) * 26px);
}

body.fx .about-index {
  translate: 0 calc(var(--p) * -16px);
  opacity: calc(0.16 + var(--p) * 0.2);
}

body.fx .about-rule {
  scale: var(--p) 1;
}

/* The body copy wipes in TONE, not position: prose that slides reads as
   unstable, and at this size a rise would fight the line spacing. Each word
   lifts out of the background as the wipe passes it. */
body.fx .about-lead .aw {
  --wp: clamp(0, (var(--bp) * (var(--n) + 4) - var(--wi)) / 2.4, 1);
  opacity: calc(0.14 + var(--wp) * 0.86);
  translate: calc((1 - var(--wp)) * 5px) 0;
}

/* And the closing line arrives last, as one block, once the lead is done. */
body.fx .about-note {
  --np: clamp(0, (var(--bp) - 0.55) * 2.6, 1);
  opacity: calc(0.06 + var(--np) * 0.94);
  translate: 0 calc((1 - var(--np)) * 10px);
}

/* Decoration is the first thing to go, and the layout underneath is already
   the finished design. Everything lands at its final value, including every
   word, so nothing is left half-written. */
@media (prefers-reduced-motion: reduce) {
  body.fx .about-card,
  body.fx .about-index,
  body.fx .about-note,
  body.fx .about-lead .aw,
  body.fx .about-title .aw-i,
  body.fx .about-head .aw-i {
    translate: none;
    opacity: 1;
  }

  body.fx .about-index {
    opacity: 0.26;
  }

  body.fx .about-rule {
    scale: 1 1;
  }
}

.contact {
  width: 100%;
  padding: clamp(48px, 5.4vw, 86px) var(--pad) clamp(52px, 5.8vw, 92px);
  background: transparent;
  color: rgb(var(--theme-fg-rgb));
}

/* The panel's two lines are the section's whole point, so they get their own
   scale instead of the 8vw display ceiling the manifesto and services use —
   at that size "Bring the problem." wrapped and the heading alone ran to four
   lines and ~400px. Measured: the line needs ~7.7x its font-size, and the copy
   column tops out near 770px, so 6rem is the largest that stays on one line.
   The vh term keeps short windows honest, where vw alone would overshoot. */
.contact h2 {
  font-size: clamp(2.3rem, min(6.4vw, 12vh), 6rem);
  line-height: 0.94;
}

.contact-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(280px, 0.42fr);
  gap: clamp(26px, 4vw, 60px);
  align-items: start;
  width: min(100%, var(--max));
  margin: 0 auto;
  padding: clamp(26px, 3.8vw, 50px);
  border-top: 1px solid rgba(var(--theme-fg-rgb), 0.22);
  border-bottom: 1px solid rgba(var(--theme-fg-rgb), 0.22);
  background:
    linear-gradient(90deg, rgba(var(--theme-fg-rgb), 0.07), transparent 54%),
    linear-gradient(180deg, rgba(var(--theme-fg-rgb), 0.05), transparent 42%);
  color: rgb(var(--theme-fg-rgb));
  /* Readability wash behind the contact copy + card — strong enough that
     the network lines read as atmosphere behind the panel, not across it. */
  box-shadow: inset 0 0 0 100vmax rgba(var(--theme-bg-rgb), 0.62);
}

.contact-copy {
  display: grid;
  align-content: center;
}

.contact-copy p {
  color: rgba(var(--theme-fg-rgb), 0.7);
}

.contact h2 span {
  display: block;
}

.contact .section-label.dark {
  color: rgba(var(--theme-fg-rgb), 0.58);
}

/* --- Contact social tiles — monochrome, matching the site's black/white
   palette; the IG/WhatsApp icons render in plain foreground, no brand colors. */
.contact-socials {
  display: grid;
  gap: 11px;
  margin-top: clamp(18px, 2.2vw, 26px);
}

.contact-socials-label {
  color: rgba(var(--theme-fg-rgb), 0.5);
  font-family: var(--body);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.social-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.social-tile {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  padding: 0;
  overflow: hidden;
  border: 1px solid rgba(var(--theme-fg-rgb), 0.26);
  background: rgba(var(--theme-fg-rgb), 0.04);
  color: rgb(var(--theme-fg-rgb));
  transition: transform 220ms var(--ease), border-color 220ms ease, background 220ms ease;
}

.social-tile svg {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1.9;
}

.social-tile-dot {
  fill: currentColor;
  stroke: none;
}

.social-tile::before {
  position: absolute;
  inset: 0;
  z-index: 0;
  content: "";
  background: linear-gradient(120deg, transparent, rgba(var(--theme-fg-rgb), 0.16), transparent);
  transform: translateX(-110%);
  transition: transform 560ms ease;
}

.social-tile > * {
  position: relative;
  z-index: 1;
}

.social-tile:hover,
.social-tile:focus-visible {
  transform: translateY(-3px);
  border-color: rgb(var(--theme-fg-rgb));
  background: rgba(var(--theme-fg-rgb), 0.09);
}

.social-tile:hover::before,
.social-tile:focus-visible::before {
  transform: translateX(110%);
}

/* A tile with no destination yet (LinkedIn) ships as a <span>, so it keeps
   its place in the row and its weight in the design without lifting under
   the cursor: a hover that promises a click nothing answers is worse than
   no hover. Delete nothing here when the URL lands: swapping the span for
   an <a> is enough, and these rules stop matching on their own. */
span.social-tile {
  cursor: default;
}

span.social-tile:hover {
  transform: none;
  border-color: rgba(var(--theme-fg-rgb), 0.26);
  background: rgba(var(--theme-fg-rgb), 0.04);
}

span.social-tile:hover::before {
  transform: translateX(-110%);
}

.message-card {
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  min-height: 200px;
  gap: clamp(16px, 2.4vw, 26px);
  padding: clamp(20px, 2.6vw, 30px);
  overflow: hidden;
  border: 1px solid rgba(var(--theme-fg-rgb), 0.26);
  /* Telemetry surface: a soft glow anchored to the arrow corner over the
     base wash. The dot matrix + scanning beam live in ::before. */
  background:
    radial-gradient(135% 125% at 92% 0%, rgba(var(--theme-fg-rgb), 0.2), transparent 52%),
    linear-gradient(165deg, rgba(var(--theme-fg-rgb), 0.09), transparent 60%),
    rgba(var(--theme-bg-rgb), 0.76);
  color: rgb(var(--theme-fg-rgb));
  text-shadow: none;
  box-shadow: inset 0 0 0 1px rgba(var(--theme-fg-rgb), 0.05);
  transition: transform 240ms ease, box-shadow 240ms ease, border-color 240ms ease, background 240ms ease;
}

.message-card * {
  position: relative;
  z-index: 1;
  text-shadow: none;
}

.message-card::before {
  position: absolute;
  inset: 0;
  z-index: 0;
  content: "";
  background:
    /* diagonal scanning beam */
    linear-gradient(115deg, transparent 0 44%, rgba(var(--theme-fg-rgb), 0.16) 50%, transparent 56%),
    /* dot matrix */
    radial-gradient(rgba(var(--theme-fg-rgb), 0.15) 1.1px, transparent 1.5px);
  background-size: 220% 100%, 20px 20px;
  /* Beam layer (no-repeat, 220% wide) slides via background-position; the dot
     matrix repeats and always covers the full card. Do NOT translate the
     pseudo-element — that would pull one edge off the card and leave the
     texture "cut off" on the right. */
  background-position: 22% 0, 6px 6px;
  background-repeat: no-repeat, repeat;
  opacity: 0.55;
  transition: background-position 620ms ease, opacity 620ms ease;
}

.message-arrow {
  position: absolute;
  right: clamp(18px, 2.4vw, 28px);
  top: clamp(18px, 2.4vw, 28px);
  width: clamp(42px, 4vw, 56px);
  height: clamp(42px, 4vw, 56px);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgb(var(--theme-fg-rgb));
  border-radius: 50%;
  background: rgb(var(--theme-fg-rgb));
  color: rgb(var(--theme-inverse-rgb));
  transition: transform 280ms var(--ease), background 280ms ease, color 280ms ease;
}

.message-arrow svg {
  width: 52%;
  height: 52%;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

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

.message-card:hover,
.message-card:focus-visible {
  transform: translateY(-4px);
  border-color: rgba(var(--theme-fg-rgb), 0.58);
  background:
    radial-gradient(135% 125% at 92% 0%, rgba(var(--theme-fg-rgb), 0.3), transparent 52%),
    linear-gradient(165deg, rgba(var(--theme-fg-rgb), 0.14), transparent 58%),
    rgba(var(--theme-bg-rgb), 0.8);
  box-shadow: 0 24px 70px rgba(var(--theme-inverse-rgb), 0.2);
}

.message-card:hover::before,
.message-card:focus-visible::before {
  opacity: 0.9;
  background-position: 74% 0, 6px 6px;
}

/* Poster composition: label pinned top-left (clearing the arrow), the
   number anchored to the card's base — no floating middle. */
.message-label {
  position: relative;
  align-self: start;
  max-width: 11ch;
  padding-right: clamp(52px, 5vw, 68px);
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2.45rem, 4.4vw, 4.05rem);
  line-height: 0.92;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.message-card strong {
  position: relative;
  display: block;
  width: 100%;
  max-width: 100%;
  padding-right: 0;
  color: rgb(var(--theme-fg-rgb));
  font-size: clamp(1.35rem, 1.8vw, 1.75rem);
  font-weight: 900;
  line-height: 1.1;
  white-space: nowrap;
  word-break: normal;
}

/* Numerals in CTAs and counters sit on a fixed grid — phone numbers and
   the services index never wobble as digits change. */
.button strong,
.message-card strong,
.services-count,
.service-num {
  font-variant-numeric: tabular-nums;
}

.site-footer {
  position: relative;
  z-index: 4;
  display: flex;
  justify-content: space-between;
  width: min(calc(100% - (var(--pad) * 2)), var(--max));
  margin: 0 auto;
  padding: 24px 0 28px;
  padding-bottom: max(28px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-family: var(--body);
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  opacity: 1;
  visibility: visible;
}

.footer-brand {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
}

.footer-brand small {
  color: currentColor;
  font-size: 0.62em;
  font-weight: 700;
  letter-spacing: 0.08em;
  opacity: 0.38;
}

.footer-meta {
  display: inline-flex;
  align-items: baseline;
  gap: 16px;
}

.footer-meta a {
  position: relative;
  color: inherit;
  opacity: 0.72;
  transition: opacity 180ms ease;
}

.footer-meta a::after {
  position: absolute;
  right: 0;
  bottom: -3px;
  left: 0;
  height: 1px;
  content: "";
  background: linear-gradient(90deg, #833ab4, #fd1d1d, #fcb045);
  opacity: 0.5;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 240ms var(--ease);
}

.footer-meta a:hover,
.footer-meta a:focus-visible {
  opacity: 1;
}

.footer-meta a:hover::after,
.footer-meta a:focus-visible::after {
  transform: scaleX(1);
}

[data-reveal] {
  opacity: 0;
  transform: translateY(var(--reveal-y));
  transition:
    opacity var(--motion-slow) ease var(--reveal-delay, 0ms),
    transform var(--motion-slow) var(--ease-out) var(--reveal-delay, 0ms);
}

[data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Set alongside .is-visible by app.js §1.4 when the scroll outran the entrance
   (a scrollbar drag, mostly). Both classes land in the same frame, so there is
   no transition to run — the block is simply already there. */
[data-reveal].is-instant {
  transition: none;
}

.hero [data-reveal] {
  opacity: 1;
  transform: none;
}

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

@media (max-width: 960px) {
  .hero,
  .contact-panel {
    grid-template-columns: 1fr;
  }

  .hero {
    align-content: end;
    gap: 18px;
    padding-top: 46svh;
  }

  .hero::before {
    background:
      linear-gradient(180deg, rgba(0, 0, 0, 0.12) 0 30%, rgba(0, 0, 0, 0.5) 58%, rgba(0, 0, 0, 0.72) 78%, transparent 100%),
      linear-gradient(90deg, rgba(0, 0, 0, 0.42), transparent 78%);
  }

  /* Tablet: about 40% of the frame survives the crop. */
  .hero-media-img {
    object-position: 26% 50%;
  }

  /* Two cards side by side stop being two columns of prose and start being
     two narrow strips: the lead wraps to five and six lines and the pair runs
     taller stacked than it does in one column. */
  .about-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .about {
    padding-top: clamp(38px, 6vw, 60px);
  }

  .hero-copy,
  .hero-actions {
    grid-column: 1;
  }

  .hero-actions {
    justify-self: end;
    width: min(100%, 360px);
    max-width: none;
    margin-right: clamp(16px, 5vw, 56px);
  }
}

@media (max-width: 760px) {
  .brand {
    backdrop-filter: none;
  }

  .background-field {
    background:
      radial-gradient(circle at 96% 34%, rgba(var(--theme-fg-rgb), 0.18), transparent 38%),
      linear-gradient(90deg, rgba(var(--theme-inverse-rgb), 0.46), rgba(var(--theme-inverse-rgb), 0.18) 50%, rgba(var(--theme-inverse-rgb), 0.06));
    opacity: var(--theme-field-opacity);
  }

  .hero {
    align-content: end;
    gap: 16px;
    min-height: 100vh;
    min-height: 100svh;
    padding-top: clamp(118px, 24svh, 340px);
    padding-bottom: 30px;
  }

  .hero::before {
    background:
      linear-gradient(180deg, rgba(0, 0, 0, 0.05) 0 34%, rgba(0, 0, 0, 0.42) 58%, rgba(0, 0, 0, 0.74) 80%, transparent 100%),
      linear-gradient(90deg, rgba(0, 0, 0, 0.3), transparent 82%);
  }

  .hero-media {
    -webkit-mask-image: linear-gradient(to bottom, #000 0 58%, rgba(0, 0, 0, 0.92) 72%, rgba(0, 0, 0, 0.4) 88%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0 58%, rgba(0, 0, 0, 0.92) 72%, rgba(0, 0, 0, 0.4) 88%, transparent 100%);
  }

  /* Back to centre, because below 760px the <picture> has swapped to the
     PORTRAIT crop, which is already framed for this shape: at 0.4625 against
     a 390x844 phone's 0.462 the cover barely trims anything, so there is no
     off-centre anchor left to compensate for. The 22% here was steering the
     landscape frame clear of the painted wall text; it would now push the
     portrait's own composition off the screen. */
  .hero-media-img {
    object-position: 50% 50%;
  }

  /* The portrait crop is 940x1670, so the frame the paint is measured against
     changes shape here and its coordinates change with it: the mark sits over
     the lit half of the wall, above the palm shadow, and it is half the width
     of the frame because a phone has half as much of the room in it. */
  .hero-media {
    --hero-img-w: max(100vw, 56.287svh);
    --hero-img-h: max(100svh, 177.66vw);
  }

  .hero-paint-line {
    top: 20.6%;
    left: 26.4%;
    font-size: calc(var(--hero-img-w) * 0.0226);
  }

  .hero-paint-mark {
    top: 24.5%;
    left: 26%;
    width: 52%;
  }

  .about-title {
    font-size: clamp(2rem, 10.4vw, 3.4rem);
  }

  .hero-copy {
    align-self: end;
  }

  .hero .eyebrow {
    margin-bottom: 12px;
  }

  .hero-actions {
    gap: 10px;
  }

  .button {
    min-height: 64px;
  }

  .manifesto,
  .services {
    padding-top: 76px;
    padding-bottom: 76px;
  }

  .manifesto h2,
  .services h2,
  .contact h2 {
    font-size: clamp(2.7rem, 13vw, 4.2rem);
    line-height: 0.94;
  }

  .service-meta {
    margin-top: 8px;
    line-height: 1.45;
  }

  .contact {
    padding-top: 50px;
    padding-bottom: 62px;
  }

  .contact-panel {
    gap: 26px;
  }

}

@media (pointer: coarse) {
  .brand {
    backdrop-filter: none;
  }

  /* Touch press feedback — the tap equivalent of the desktop hover lifts.
     Transform/opacity only; the elements' existing transitions ease it. */
  .button:active {
    transform: translateY(1px) scale(0.98);
  }

  .message-card:active {
    transform: scale(0.99);
    border-color: rgba(var(--theme-fg-rgb), 0.5);
  }

  .social-tile:active {
    transform: scale(0.96);
  }

  .brand:active {
    transform: scale(0.95);
  }

  .site-nav a:active {
    color: rgb(var(--theme-fg-rgb));
  }

  .background-field {
    background:
      radial-gradient(circle at 96% 34%, rgba(var(--theme-fg-rgb), 0.18), transparent 38%),
      linear-gradient(90deg, rgba(var(--theme-inverse-rgb), 0.46), rgba(var(--theme-inverse-rgb), 0.18) 50%, rgba(var(--theme-inverse-rgb), 0.06));
    opacity: var(--theme-field-opacity);
  }

}

@media (max-width: 620px) {
  :root {
    --pad: 18px;
    --pad: max(18px, env(safe-area-inset-left), env(safe-area-inset-right));
  }

  .site-header {
    right: var(--pad);
    left: var(--pad);
    width: auto;
    min-height: 66px;
    padding-top: 10px;
  }

  .brand {
    min-height: 44px;
    padding: 0 10px;
    font-size: 0.64rem;
    letter-spacing: 0.12em;
  }

  .hero {
    min-height: 100vh;
    min-height: 100svh;
    padding-top: clamp(380px, 59svh, 540px);
    padding-bottom: 34px;
  }

  .hero-actions {
    justify-self: stretch;
    width: 100%;
    max-width: 440px;
    margin-right: 0;
  }

  .brand {
    backdrop-filter: none;
  }

  body::before {
    background-size: 58px 58px;
    opacity: 0.17;
  }

  body::after {
    background:
      radial-gradient(circle at 96% 34%, rgba(var(--theme-fg-rgb), 0.16), transparent 35%),
      linear-gradient(90deg, rgba(var(--theme-inverse-rgb), 0.16), transparent 24%, transparent 70%, rgba(var(--theme-fg-rgb), 0.1)),
      linear-gradient(180deg, transparent 0 52%, rgba(var(--theme-inverse-rgb), 0.28));
    opacity: var(--theme-ambient-opacity);
  }

  .hero h1 {
    font-size: clamp(4rem, 20vw, 6.6rem);
  }

  .button {
    min-height: 64px;
    padding-right: 64px;
  }

  .manifesto h2,
  .services h2,
  .contact h2 {
    font-size: clamp(2.7rem, 13vw, 4.2rem);
  }

  .contact-panel {
    padding: 20px;
  }

  .message-card {
    min-height: 230px;
  }

  .message-label {
    max-width: 10ch;
  }
}

@media (max-width: 420px) {
  .message-card strong {
    font-size: 1.18rem;
  }

  .site-footer {
    align-items: flex-end;
  }
}

@media (max-width: 360px) {
  :root {
    --pad: 16px;
    --pad: max(16px, env(safe-area-inset-left), env(safe-area-inset-right));
  }

  .site-header {
    right: var(--pad);
    left: var(--pad);
  }

  .brand {
    padding-right: 8px;
    padding-left: 8px;
    font-size: 0.6rem;
    letter-spacing: 0.1em;
  }
}

@media (max-width: 760px) and (max-height: 680px) {
  .hero {
    gap: 12px;
    padding-top: clamp(320px, 60svh, 390px);
    padding-bottom: 28px;
  }

  .button {
    min-height: 60px;
  }
}

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

  .hero h1 .char {
    transform: none;
    opacity: 1;
  }

}

/* =====================================================================
   Scroll-driven motion system â€” fx layer.
   Everything below is inert unless app.js adds body.fx (skipped when
   prefers-reduced-motion is set), so the page stays fully readable
   without it.
   ===================================================================== */

main {
  position: relative;
  z-index: 4;
}

/* Scroll-velocity headline skew and the hero latency-echo distortion were
   removed: they fought the content for attention and hurt readability. The
   hero keeps its character entrance and the split-portal drift only. */

body.fx .hero h1 [data-split] {
  transform: translateX(calc(var(--hero-p, 0) * var(--dir, 0) * 16.1vw));
}

body.fx .hero [data-reveal] {
  transition: none;
}

body.fx .hero-copy,
body.fx .hero-actions {
  opacity: calc(1 - var(--hero-p, 0) * 0.9);
  transform: translate3d(0, calc(var(--hero-p, 0) * var(--parallax-depth) * -1), 0);
}

body.fx .hero-media {
  transform: translate3d(0, calc(var(--hero-p, 0) * var(--parallax-depth) * -0.5), 0);
}

@media (max-width: 960px) {
  body.fx .hero h1 {
    transform: translate3d(0, calc(var(--hero-p, 0) * var(--parallax-depth) * -0.52), 0);
  }
}

/* The "servo cooldown settle" that used to live here — footer and hero line
   displaced by scroll VELOCITY, then springing back to rest — is gone. It was
   the only motion on the page driven by scroll speed rather than scroll
   position, which is exactly why it could not stay under the cursor: measured
   30.5px out of place mid-drag and 34px of travel over ~500ms after the drag
   ended. Everything below is a function of scroll position and lands where the
   scrollbar puts it. Keep it that way. */

/* --- Manifesto phrase direction --- */
.manifesto-phrases .phrase {
  display: block;
}

/* Every viewport consumes the same cached scroll progress. Compact devices
   get a shorter distance from app.js, but the timing and sequence stay equal. */
body.fx .manifesto-phrases .phrase {
  opacity: var(--po, 0);
  transform: translate3d(var(--px, 0px), 0, 0);
}

/* --- Polarizer band reveal + phase-locked copy --- */
body.fx [data-polarize] .pword {
  display: inline-block;
  transition: opacity 280ms ease, transform 380ms var(--ease);
}

body.fx [data-polarize] .pword.is-pending {
  opacity: 0.34;
  transform: translateX(4px);
}

body.fx [data-polarize] .pword.is-live {
  opacity: 1;
  transform: none;
}

body.fx [data-polarize] .pword.is-locked {
  opacity: 0.94;
  transform: none;
}


/* --- Grid parallax (desktop; pinned static on touch/compact below) --- */
body.fx::before {
  transform: translate3d(0, calc(var(--scroll) * -55px), 0);
}

/* --- WhatsApp card construction --- */
.card-trace {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.card-trace rect {
  fill: none;
  stroke: rgba(var(--theme-fg-rgb), 0.85);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
}

.message-card.is-built .card-trace rect {
  animation: capCharge 1150ms var(--ease) forwards;
}

body.fx .message-card .message-label,
body.fx .message-card strong {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 600ms ease, transform 600ms var(--ease);
}

body.fx .message-card.is-built .message-label {
  opacity: 1;
  transform: none;
  transition-delay: 420ms;
}

body.fx .message-card.is-built strong {
  opacity: 1;
  transform: none;
  transition-delay: 600ms;
}

body.fx .message-card .message-arrow {
  opacity: 0;
  transition: opacity 500ms ease, transform 280ms var(--ease), background 280ms ease, color 280ms ease;
}

body.fx .message-card.is-built .message-arrow {
  opacity: 1;
  transition: opacity 500ms ease 780ms, transform 280ms var(--ease), background 280ms ease, color 280ms ease;
}

/* phone number transmission */
.tx-char {
  display: inline-block;
}

.tx-digit {
  min-width: 0.62ch;
  text-align: center;
}

body.fx .tx-digit:not(.is-locked) {
  opacity: 0.45;
}

body.fx .tx-digit.is-locked {
  opacity: 1;
}

/* --- Footer energy collapse --- */
.site-footer {
  position: relative;
}

body.fx .site-footer::before {
  position: absolute;
  top: -18px;
  left: 0;
  right: 0;
  height: 1px;
  content: "";
  background: rgba(var(--theme-fg-rgb), 0.4);
  transform: scaleX(calc(1 - var(--endp, 0)));
}

/* =====================================================================
   Round 2: services and extra life
   ===================================================================== */

.hero {
  position: relative;
}

/* --- Services --- */
.services {
  padding: clamp(74px, 11vw, 140px) 0;
}

/* Contact copy resets after the panel leaves, so returning to the section
   replays the reveal alongside the phone transmission. */
body.fx .contact h2 span {
  opacity: 0;
  transform: translate3d(0, 0.62em, 0);
  transform-origin: left bottom;
  transition:
    opacity 420ms ease,
    transform var(--motion-slow) var(--ease-out);
}

body.fx .contact-panel.is-contact-live h2 span:nth-child(1) {
  opacity: 1;
  transform: none;
  transition-delay: 100ms;
}

body.fx .contact-panel.is-contact-live h2 span:nth-child(2) {
  opacity: 1;
  transform: none;
  transition-delay: 240ms;
}

body.fx .contact-copy > p {
  opacity: 0;
  transform: translate3d(0, var(--reveal-y), 0);
  transition:
    opacity 500ms ease,
    transform var(--motion-slow) var(--ease-out);
}

body.fx .contact-panel.is-contact-live .contact-copy > p {
  opacity: 1;
  transform: none;
  transition-delay: 430ms;
}

/* --- Section label line draws in on reveal --- */
.section-label::before {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 600ms var(--ease) 150ms;
}

.section-label.is-visible::before,
.is-visible .section-label::before {
  transform: scaleX(1);
}

body:not(.fx) .section-label::before {
  transform: scaleX(1);
}

/* The infinite CTA glint was removed â€” the button's hover lift + arrow
   nudge already signal interactivity, and a perpetual sweep is exactly the
   kind of idle motion this pass is quieting. */
/* --- Living grain --- */
body.fx .grain {
  animation: grainShift 9s steps(8) infinite;
}

/* =====================================================================
   Round 3: a11y skip link, hero depth, magnetic CTA
   ===================================================================== */

/* --- Skip link: hidden until keyboard-focused --- */
.skip-link {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 200;
  padding: 10px 16px;
  border: 1px solid var(--line);
  background: rgb(var(--theme-bg-rgb));
  color: rgb(var(--theme-fg-rgb));
  font-family: var(--body);
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  transform: translateY(-180%);
  transition: transform 220ms var(--ease);
}

.skip-link:focus-visible {
  transform: translateY(0);
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* The hero background is a self-animating video, so the old Ken Burns
   drift on the still photo is gone. The grain animation is still stilled
   on mobile for the same battery reason it always was. */
@media (max-width: 960px) {
  body.fx .grain {
    animation: none;
  }
}

/* The magnetic CTA pull was removed: hover already communicates
   interactivity, and one interaction motion per element keeps the hierarchy
   clean (and drops a getBoundingClientRect on every pointer move). */

/* =====================================================================
   Round 4: mobile performance.
   The grain layer is a full-screen mix-blend-mode composite â€” invisible
   at phone opacity/size but costly on every repaint, so it goes. The
   theme flip on touch devices is discrete (app.js snaps it with
   hysteresis instead of scrubbing the crossover), so the registered
   channels get one slightly longer ease to make the inversion land as a
   deliberate cut rather than a stutter.
   ===================================================================== */
@media (max-width: 960px), (pointer: coarse) {
  .grain {
    display: none;
  }

  /* The grid + ambient gradients are full-screen fixed layers. Scrubbing
     their transform with --scroll repaints the whole viewport every frame —
     one of the biggest mobile costs. Pin them static; the network field
     (CSS --scroll transform on promoted layers) carries the motion instead. */
  body::before,
  body.fx::before {
    transform: none;
  }

  :root {
    transition:
      --theme-luma 360ms ease,
      --theme-bg-channel 360ms ease,
      --theme-fg-channel 360ms ease,
      --theme-inverse-channel 360ms ease;
  }
}

/* =====================================================================
   Round 5: header nav.
   ===================================================================== */

/* Anchored sections clear the fixed header when jumped to from the nav. */
#services,
#case-study,
#case-breakdown,
#about,
#contact {
  scroll-margin-top: 96px;
}

/* --- Compact primary navigation --- */
/* RIGHT-ALIGNED, IN FLOW. It used to be absolutely centred, which meant the
   nav and the wordmark could in principle collide as the window narrowed and
   the only defence was that they happened not to. As an ordinary flex item
   after .brand-home (which carries margin-right: auto) it sits at the right
   edge and the layout engine guarantees the gap instead. */
.site-nav {
  display: inline-flex;
  align-items: center;
  gap: clamp(18px, 2.4vw, 34px);
  padding: 0 2px 0 12px;
  font-family: var(--body);
  font-size: 0.86rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
}

.site-nav a {
  position: relative;
  color: rgba(var(--theme-fg-rgb), 0.72);
  /* Simple drop shadow behind each nav word so it lifts off the animated
     background. Inverse (dark) blur under the light-on-dark default. */
  text-shadow: 0 2px 10px rgba(var(--theme-inverse-rgb), 0.55);
  transition: color 200ms ease;
}

body.is-light .site-nav a {
  /* On light, the inverse blur would be white (invisible) — use a soft
     foreground (dark) shadow instead so the lift still reads. */
  text-shadow: 0 2px 8px rgba(var(--theme-fg-rgb), 0.26);
}

.site-nav a::after {
  position: absolute;
  left: 0;
  bottom: -4px;
  width: 100%;
  height: 1px;
  content: "";
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 260ms var(--ease);
}

.site-nav a:hover,
.site-nav a:focus-visible {
  color: rgb(var(--theme-fg-rgb));
}

.site-nav a:hover::after,
.site-nav a:focus-visible::after {
  transform: scaleX(1);
}

@media (max-width: 859px) {
  .site-nav {
    display: none;
  }
}

/* ---------------------------------------------------------------------
   Compact navigation: hamburger + sheet.

   Below 859px the inline nav is gone, which used to leave a phone with no
   way to reach a section except scrolling the whole page. The button lives
   in the header (so it rides the same fixed bar as the wordmark) and the
   sheet is a sibling of the header at a LOWER z-index, so the brand and the
   button stay on top of it — the button becomes the X without moving.

   Everything here animates transform and opacity only, so an open or a
   close is a compositor job and never lands on the scroll thread. The sheet
   keeps `visibility: hidden` when closed (not `display: none`, which cannot
   transition) with the visibility switch delayed by the panel's own exit,
   and `inert` is set from JS so nothing inside is tabbable while shut.
   --------------------------------------------------------------------- */
.nav-toggle {
  position: relative;
  z-index: 1;
  display: none;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  padding: 0;
  border: 1px solid var(--line);
  background: rgba(var(--theme-bg-rgb), 0.78);
  color: rgb(var(--theme-fg-rgb));
  backdrop-filter: blur(12px);
  cursor: pointer;
  transition: border-color 220ms ease, background 220ms ease, transform 220ms var(--ease);
}

@media (max-width: 859px) {
  .nav-toggle {
    display: inline-flex;
  }
}

.nav-toggle:hover,
.nav-toggle:focus-visible {
  border-color: currentColor;
}

.nav-toggle:active {
  transform: scale(0.94);
}

.nav-toggle-bars {
  position: relative;
  display: block;
  width: 18px;
  height: 12px;
}

.nav-toggle-bars i {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.5px;
  background: currentColor;
  transition: transform 340ms var(--ease), opacity 180ms ease;
}

.nav-toggle-bars i:nth-child(1) {
  top: 0;
}

.nav-toggle-bars i:nth-child(2) {
  top: 5.25px;
}

.nav-toggle-bars i:nth-child(3) {
  top: 10.5px;
}

body.nav-open .nav-toggle-bars i:nth-child(1) {
  transform: translateY(5.25px) rotate(45deg);
}

body.nav-open .nav-toggle-bars i:nth-child(2) {
  opacity: 0;
  transform: scaleX(0.2);
}

body.nav-open .nav-toggle-bars i:nth-child(3) {
  transform: translateY(-5.25px) rotate(-45deg);
}

.nav-sheet {
  position: fixed;
  inset: 0;
  z-index: 45;
  display: grid;
  align-content: start;
  visibility: hidden;
  pointer-events: none;
  transition: visibility 0s linear 440ms;
}

body.nav-open .nav-sheet {
  visibility: visible;
  pointer-events: auto;
  transition-delay: 0s;
}

.nav-sheet-scrim {
  position: absolute;
  inset: 0;
  padding: 0;
  border: 0;
  background: rgba(var(--theme-inverse-rgb), 0.66);
  backdrop-filter: blur(16px);
  opacity: 0;
  touch-action: none;
  cursor: pointer;
  transition: opacity 360ms var(--ease);
}

body.nav-open .nav-sheet-scrim {
  opacity: 1;
}

.nav-sheet-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  max-height: 100svh;
  padding: 92px var(--pad) 24px;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-bottom: 1px solid var(--line);
  background: rgba(var(--theme-bg-rgb), 0.9);
  transform: translateY(-101%);
  transition: transform 440ms var(--ease);
}

body.nav-open .nav-sheet-panel {
  transform: translateY(0);
}

.nav-sheet-panel a {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 14px 2px;
  border-bottom: 1px solid rgba(var(--theme-fg-rgb), 0.12);
  color: rgb(var(--theme-fg-rgb));
  font-family: var(--display);
  font-size: clamp(1.55rem, 8.4vw, 2.35rem);
  line-height: 1;
  letter-spacing: 0.012em;
  text-transform: uppercase;
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 380ms var(--ease), transform 380ms var(--ease), color 200ms ease,
    border-color 200ms ease;
}

/* The stagger is the whole feel of the thing: each row arrives on its own
   beat, and on the way out they all leave together so a close never feels
   like waiting. --i is set per link in the markup. */
body.nav-open .nav-sheet-panel a {
  opacity: 1;
  transform: none;
  transition-delay: calc(130ms + var(--i, 0) * 54ms);
}

.nav-sheet-panel a:last-child {
  border-bottom: 0;
}

.nav-sheet-panel a em {
  flex: 0 0 auto;
  color: rgba(var(--theme-fg-rgb), 0.4);
  font: 900 0.6rem/1 var(--body);
  font-style: normal;
  letter-spacing: 0.2em;
  transition: color 200ms ease;
}

.nav-sheet-panel a:active,
.nav-sheet-panel a[aria-current="page"] {
  border-color: rgba(var(--theme-fg-rgb), 0.5);
}

.nav-sheet-panel a:active em,
.nav-sheet-panel a[aria-current="page"] em {
  color: rgb(var(--theme-fg-rgb));
}

/* The page behind must not scroll under the sheet. The menu only exists
   below 859px, where scrollbars are overlays, so hiding the overflow costs
   no layout shift. */
body.nav-open {
  overflow: hidden;
}

/* =====================================================================
   Services word machine.
   The desktop list scrubs through a fixed signal band. Compact layouts
   keep the same oversized vocabulary as a natural observer-driven list.
   ===================================================================== */
.services {
  padding: clamp(86px, 11vw, 148px) 0;
}

.services-intro {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "label count"
    "title count";
  gap: 4px clamp(24px, 4vw, 58px);
  align-items: end;
}

.services-intro .section-label {
  grid-area: label;
  align-self: end;
  margin-bottom: 8px;
}

.services-intro h2 {
  grid-area: title;
  margin: 0;
  font-family: var(--display);
  font-size: clamp(3.4rem, 6.4vw, 6.8rem);
  font-weight: 400;
  line-height: 0.86;
  letter-spacing: 0.005em;
  text-transform: uppercase;
}

.services-count {
  grid-area: count;
  display: inline-flex;
  align-items: baseline;
  align-self: end;
  gap: 8px;
  margin: 0 0 4px;
  color: rgb(var(--theme-fg-rgb));
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.services-count em {
  color: var(--muted);
  font-style: normal;
}

.services-count span {
  display: inline-block;
  min-width: 2ch;
  font-family: var(--display);
  font-size: 1.15rem;
  letter-spacing: 0.08em;
}

.services-machine {
  --service-p: 0;
  --service-index: 0;
  --service-row: clamp(118px, 18vh, 160px);
  /* Shared column template so the band's clipped clone aligns exactly
     with the base reel at every breakpoint. */
  --svc-cols: clamp(104px, 12vw, 176px) minmax(0, 1fr);
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: var(--svc-cols);
  height: calc(var(--service-row) * 3);
  overflow: hidden;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.services-band {
  position: absolute;
  top: var(--service-row);
  left: 0;
  right: 0;
  z-index: 1;
  display: grid;
  grid-template-columns: var(--svc-cols);
  height: var(--service-row);
  overflow: hidden;
  background: rgb(var(--theme-fg-rgb));
  pointer-events: none;
}

.services-band::before {
  content: none;
}

/* The clipped inverse reel inside the band. It consumes the same
   --service-p as the base list, offset by the band's one-row origin, so
   both copies are always pixel-locked — a word crossing the band edge
   flips tone exactly at the boundary. */
.services-band-viewport {
  position: relative;
  grid-column: 2;
  height: 100%;
  overflow: hidden;
}

/* body.fx prefix: must outrank the base `body.fx .services-list`
   transform in both viewport media blocks, or the clone rides one full
   row behind the reel. */
body.fx .services-band .services-list {
  transform: translate3d(0, calc(var(--service-row) * var(--service-p, 0) * -1), 0);
  will-change: transform;
  backface-visibility: hidden;
}

.services-band .service-item h3,
.services-band .service-num,
.services-band .service-copy {
  color: rgb(var(--theme-bg-rgb));
}

.services-band .service-meta {
  color: rgba(var(--theme-bg-rgb), 0.62);
}

.services-command {
  position: relative;
  z-index: 2;
  grid-column: 1;
  display: grid;
  align-content: center;
  height: var(--service-row);
  margin-top: var(--service-row);
  padding-left: clamp(14px, 2vw, 28px);
  color: rgb(var(--theme-bg-rgb));
  font-family: var(--body);
  font-size: clamp(0.82rem, 1.25vw, 1.08rem);
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.services-command span:last-child {
  margin-top: 6px;
  font-family: var(--display);
  font-size: clamp(2.6rem, 4.6vw, 4.4rem);
  font-weight: 400;
  letter-spacing: 0.02em;
}

.services-viewport {
  position: relative;
  z-index: 0;
  grid-column: 2;
  height: calc(var(--service-row) * 3);
  overflow: hidden;
}

.services-list {
  padding: 0;
  margin: 0;
  list-style: none;
}

.service-item {
  height: var(--service-row);
}

.service-item article {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(250px, 0.7fr);
  align-items: center;
  gap: clamp(22px, 4vw, 64px);
  height: 100%;
  padding: 0 clamp(34px, 5vw, 74px) 0 clamp(12px, 2vw, 28px);
}

.service-word {
  display: flex;
  align-items: baseline;
  min-width: 0;
}

.service-num {
  flex: 0 0 auto;
  width: clamp(34px, 4vw, 58px);
  margin: 0;
  color: rgba(var(--theme-fg-rgb), 0.28);
  -webkit-text-stroke: 0;
  font-family: var(--body);
  font-size: clamp(0.62rem, 0.9vw, 0.76rem);
  font-weight: 800;
  letter-spacing: 0.14em;
  transition: color 260ms ease;
}

.service-item h3 {
  margin: 0;
  color: rgba(var(--theme-fg-rgb), 0.3);
  font-family: var(--display);
  font-size: clamp(4.2rem, 7.2vw, 7.8rem);
  font-weight: 400;
  line-height: 0.74;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  white-space: nowrap;
  text-shadow: none;
  transition: color 260ms ease, transform 420ms var(--ease-out);
}

.service-copy {
  opacity: 0;
  transform: translateX(24px);
  transition: opacity 240ms ease, transform 420ms var(--ease-out);
}

.service-copy strong {
  display: block;
  margin-bottom: 8px;
  font-size: clamp(0.76rem, 1.04vw, 0.92rem);
  font-weight: 900;
  letter-spacing: 0.12em;
  line-height: 1.25;
  text-transform: uppercase;
}

.service-copy p {
  margin: 0 0 10px;
  font-size: clamp(0.8rem, 1.05vw, 0.98rem);
  line-height: 1.4;
  text-shadow: none;
}

.service-meta {
  display: block;
  margin: 0;
  font-size: clamp(0.58rem, 0.76vw, 0.68rem);
  font-weight: 800;
  letter-spacing: 0.12em;
  line-height: 1.35;
  text-transform: uppercase;
}

/* The base reel keeps one quiet tone even when active — the inverted,
   readable state lives only in the band clone. The base copy column
   never reveals under fx (even black-on-black text would silhouette
   against the network lines); non-fx layouts force it visible below. */
.service-item.is-active h3 {
  transform: translateX(clamp(4px, 0.7vw, 12px));
}

.services-band .service-item.is-active .service-copy {
  opacity: 1;
  transform: none;
}

@media (min-width: 961px) {
  body.fx .services {
    padding: 0;
  }

  body.fx .services-pin {
    height: 452vh;
  }

  body.fx .services.is-pinned .services-stage {
    position: sticky;
    top: 0;
    display: grid;
    grid-template-rows: auto auto;
    align-content: center;
    gap: clamp(18px, 3vh, 34px);
    min-height: 100vh;
    padding: clamp(72px, 9vh, 92px) 0 clamp(20px, 4vh, 38px);
  }

  body.fx .services-list {
    transform: translate3d(0, calc(var(--service-row) * (1 - var(--service-p))), 0);
    will-change: transform;
  }

  body:not(.fx) .services-intro {
    margin-bottom: 54px;
  }

  body:not(.fx) .services-machine,
  body:not(.fx) .services-viewport {
    display: block;
    height: auto;
    overflow: visible;
  }

  body:not(.fx) .services-band,
  body:not(.fx) .services-command {
    display: none;
  }

  body:not(.fx) .service-item {
    height: auto;
    min-height: 320px;
    border-top: 1px solid var(--line);
  }

  body:not(.fx) .service-item:last-child {
    border-bottom: 1px solid var(--line);
  }

  body:not(.fx) .service-item article {
    min-height: inherit;
  }

  body:not(.fx) .service-item h3,
  body:not(.fx) .service-item.is-active h3 {
    color: rgb(var(--theme-fg-rgb));
    transform: none;
  }

  body:not(.fx) .service-item .service-num,
  body:not(.fx) .service-item.is-active .service-num {
    color: var(--muted);
  }

  body:not(.fx) .service-copy,
  body:not(.fx) .service-item.is-active .service-copy {
    color: rgb(var(--theme-fg-rgb));
    opacity: 1;
    transform: none;
  }

  body:not(.fx) .service-meta,
  body:not(.fx) .service-item.is-active .service-meta {
    color: var(--muted);
  }
}

@media (max-width: 960px) {
  .services {
    padding: 88px 0 42px;
  }

  .services-intro {
    display: block;
    margin-bottom: 46px;
  }

  .services-intro .section-label {
    margin-bottom: 18px;
  }

  .services-intro h2 {
    margin-bottom: 0;
    font-size: clamp(3.2rem, 11vw, 5.6rem);
  }

  body:not(.fx) .services-count,
  body:not(.fx) .services-band,
  body:not(.fx) .services-command {
    display: none;
  }

  body:not(.fx) .services-machine,
  body:not(.fx) .services-viewport {
    display: block;
    height: auto;
    overflow: visible;
    border: 0;
  }

  body:not(.fx) .services-list {
    transform: none !important;
  }

  body:not(.fx) .service-item {
    height: auto;
    min-height: 48svh;
    border-top: 1px solid var(--line);
  }

  body:not(.fx) .service-item:last-child {
    border-bottom: 1px solid var(--line);
  }

  body:not(.fx) .service-item article {
    display: grid;
    grid-template-columns: 1fr;
    align-content: center;
    gap: 24px;
    min-height: inherit;
    padding: 46px 0;
  }

  body:not(.fx) .service-item h3,
  body:not(.fx) .service-item.is-active h3 {
    color: rgb(var(--theme-fg-rgb));
    font-size: clamp(4.2rem, 13vw, 7rem);
    white-space: normal;
    transform: none;
  }

  body:not(.fx) .service-num,
  body:not(.fx) .service-item.is-active .service-num {
    width: clamp(34px, 7vw, 54px);
    color: var(--muted);
  }

  body:not(.fx) .service-copy,
  body:not(.fx) .service-item.is-active .service-copy {
    max-width: 580px;
    color: rgb(var(--theme-fg-rgb));
    opacity: 1;
    transform: none;
  }

  body:not(.fx) .service-copy p {
    color: var(--soft);
    font-size: clamp(0.94rem, 2.4vw, 1.08rem);
  }

  body:not(.fx) .service-meta {
    color: var(--muted);
  }

  body.fx .services {
    padding: 0;
  }

  body.fx .services-pin {
    height: 320vh;
    height: 320svh;
  }

  body.fx .services.is-pinned .services-stage {
    position: sticky;
    top: 0;
    display: grid;
    grid-template-rows: auto auto;
    align-content: center;
    gap: clamp(14px, 2.2svh, 22px);
    min-height: 100vh;
    min-height: 100svh;
    padding: clamp(62px, 8svh, 78px) 0 clamp(14px, 2.5svh, 24px);
  }

  body.fx .services-intro {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "label count"
      "title count";
    gap: 3px 14px;
    align-items: end;
    margin: 0;
  }

  body.fx .services-intro .section-label {
    margin-bottom: 5px;
  }

  body.fx .services-intro h2 {
    font-size: clamp(2.7rem, 10.5vw, 5rem);
  }

  body.fx .services-count {
    display: inline-flex;
    margin-bottom: 2px;
  }

  body.fx .services-machine {
    --service-row: clamp(118px, calc((100svh - 300px) / 3), 156px);
    --svc-cols: clamp(50px, 14vw, 74px) minmax(0, 1fr);
    display: grid;
    grid-template-columns: var(--svc-cols);
    height: calc(var(--service-row) * 3);
    overflow: hidden;
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
    contain: layout paint;
  }

  body.fx .services-band,
  body.fx .services-command {
    display: grid;
  }

  body.fx .services-band {
    top: var(--service-row);
  }

  body.fx .services-command {
    height: var(--service-row);
    margin-top: var(--service-row);
    padding-left: 9px;
    font-size: 0.72rem;
    letter-spacing: 0.12em;
  }

  body.fx .services-command span:last-child {
    margin-top: 5px;
    font-size: clamp(1.8rem, 6vw, 2.8rem);
  }

  body.fx .services-viewport {
    position: relative;
    display: block;
    height: calc(var(--service-row) * 3);
    overflow: hidden;
  }

  body.fx .services-list {
    transform: translate3d(0, calc(var(--service-row) * (1 - var(--service-p))), 0);
    will-change: transform;
    backface-visibility: hidden;
  }

  body.fx .service-item {
    display: block;
    position: relative;
    contain: layout;
    height: var(--service-row);
    min-height: 0;
    border: 0;
    opacity: 1;
    pointer-events: auto;
    transform: none;
    backface-visibility: hidden;
  }

  body.fx .service-item article {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-content: center;
    height: 100%;
    min-height: 0;
    padding: 10px clamp(12px, 3vw, 22px);
    opacity: 1;
  }

  body.fx .service-num {
    display: none;
  }

  body.fx .service-item h3 {
    font-size: clamp(2.45rem, 12.6vw, 4.8rem);
    line-height: 0.76;
    white-space: nowrap;
    transform: none;
  }

  body.fx .service-copy {
    display: none;
  }

  .service-copy,
  .service-num {
    display: none;
  }

  body.fx .service-item.is-active h3 {
    transform: translateX(clamp(4px, 1.5vw, 10px));
  }
}

@media (max-width: 560px) {
  body:not(.fx) .services {
    padding-top: 74px;
  }

  body:not(.fx) .services-intro {
    margin-bottom: 28px;
  }

  body:not(.fx) .service-item {
    min-height: 54svh;
  }

  body:not(.fx) .service-item article {
    padding: 40px 0;
  }

  body:not(.fx) .service-item h3,
  body:not(.fx) .service-item.is-active h3 {
    font-size: clamp(2.9rem, 14.5vw, 5.6rem);
    line-height: 0.8;
  }

  body:not(.fx) .service-copy strong {
    font-size: 0.72rem;
  }

  body.fx .services.is-pinned .services-stage {
    gap: clamp(12px, 2svh, 18px);
    padding-top: clamp(38px, 5svh, 48px);
  }

  body.fx .services-intro h2 {
    font-size: clamp(2.5rem, 12vw, 3.8rem);
    line-height: 0.84;
  }

  body.fx .services-count {
    gap: 5px;
    font-size: 0.54rem;
    letter-spacing: 0.12em;
  }

  body.fx .services-count span {
    font-size: 0.92rem;
  }

  body.fx .services-machine {
    --service-row: clamp(92px, calc((100svh - 360px) / 3), 104px);
    --svc-cols: 50px minmax(0, 1fr);
    grid-template-columns: var(--svc-cols);
    border-color: rgba(var(--theme-fg-rgb), 0.28);
  }

  body.fx .services-command {
    display: grid;
    grid-column: 1;
    width: 50px;
    padding-left: 8px;
    font-size: 0.68rem;
    letter-spacing: 0.1em;
  }

  body.fx .services-command span:last-child {
    margin-top: 5px;
    font-size: 2rem;
  }

  body.fx .services-band {
    box-shadow: none;
  }

  body.fx .services-viewport {
    grid-column: 2;
  }

  body.fx .service-item article {
    padding: 12px clamp(5px, 1.5vw, 10px);
  }

  body.fx .service-item h3 {
    line-height: 0.76;
  }
}

@media (max-width: 560px) and (max-height: 680px) {
  body.fx .services.is-pinned .services-stage {
    gap: 8px;
    padding-top: 34px;
    padding-bottom: 10px;
  }

  body.fx .services-intro h2 {
    font-size: clamp(2rem, 9.5vw, 2.8rem);
  }

  body.fx .services-machine {
    --service-row: clamp(86px, calc((100svh - 300px) / 3), 96px);
  }

  body.fx .service-item article {
    padding-top: 9px;
    padding-bottom: 9px;
  }

  body.fx .service-item h3 {
    font-size: clamp(2.4rem, 12.4vw, 3.1rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .services-list,
  .service-item,
  .service-item h3,
  .service-copy {
    transition: none !important;
    transform: none !important;
  }
}

/* =====================================================================
   404 — a static page (no JS): shares the token system and type, needs
   nothing revealed or animated to be complete.
   ===================================================================== */
.notfound {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 6px;
  min-height: 100vh;
  min-height: 100svh;
  padding: 120px var(--pad) 90px;
  text-align: center;
}

.notfound h1 {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(7rem, 26vw, 17rem);
  font-weight: 400;
  line-height: 0.85;
  text-transform: uppercase;
}

.notfound p {
  margin: 0;
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.notfound .button {
  min-width: min(280px, 76vw);
  margin-top: 34px;
  padding-right: 72px;
  text-align: left;
}

/* =====================================================================
   Portfolio + Case Study.
   Both sections inherit the site's tokens: mono theme channels, sharp
   corners, Anton display type, the shared card wash + hover lift, and the
   [data-reveal] entrance system. Nothing here reads as bolted on.
   ===================================================================== */
.portfolio {
  /* The reels are sized from the HEIGHT that's left over, not from the column
     width — that's what keeps the whole section inside one screen at any
     resolution. The subtrahend is the section's own chrome at its widest:
     92px + 76px of padding, the 16px card gap, and the ~40px caption row.
     --reel-w is the width that height implies for a 9:16 pair, and it drives
     the grid track, so the ratio survives the cap. */
  --reel-gap: clamp(14px, 2vw, 30px);
  --reel-h: clamp(240px, calc(100svh - 236px), 640px);
  --reel-w: calc(var(--reel-h) * 9 / 16 * 2 + var(--reel-gap));
  padding: clamp(52px, 6vw, 92px) 0 clamp(44px, 5vw, 76px);
}

/* Copy left, reels right on anything wide enough. The reel pair was already
   capped well short of the 1240px frame, so the copy moves into space the
   section was leaving empty — and the height it used to cost disappears.
   Below the breakpoint this collapses back to the original stack. */
.portfolio-grid {
  display: grid;
  gap: clamp(26px, 3.4vw, 54px);
  align-items: center;
}

.portfolio-copy {
  display: grid;
  align-content: center;
  gap: clamp(18px, 2.4vw, 30px);
}

.portfolio-intro {
  display: grid;
  gap: 14px;
  max-width: 900px;
}

.portfolio-intro h2 {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(3rem, 8vw, 8rem);
  font-weight: 400;
  line-height: 0.92;
  text-transform: uppercase;
  letter-spacing: 0.004em;
}

/* Same two-tone display line the breakdown below used to carry on its own
   heading: Anton has no italic cut, so the emphasis word is a calmer tone
   rather than a slant. */
.portfolio-intro h2 em {
  font-style: normal;
  color: var(--muted);
}

.portfolio-lead {
  max-width: 620px;
  margin: 6px 0 0;
  color: var(--soft);
  font-size: clamp(1.02rem, 1.7vw, 1.24rem);
  line-height: 1.5;
}

/* --- Shared ghost / solid link button ------------------------------- */
.ghost-button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 18px;
  border: 1px solid rgba(var(--theme-fg-rgb), 0.32);
  background: rgba(var(--theme-bg-rgb), 0.4);
  color: rgb(var(--theme-fg-rgb));
  font-family: var(--body);
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-shadow: none;
  transition: transform 240ms var(--ease), border-color 240ms ease, background 240ms ease, color 240ms ease;
}

.ghost-button i {
  font-style: normal;
  transition: transform 260ms var(--ease);
}

.ghost-button:hover,
.ghost-button:focus-visible {
  transform: translateY(-2px);
  border-color: currentColor;
  background: rgba(var(--theme-fg-rgb), 0.08);
}

.ghost-button:hover i,
.ghost-button:focus-visible i {
  transform: translate(3px, -3px);
}

/* --- Instagram showcase header -------------------------------------- */
.showcase-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  font-family: var(--body);
  font-size: 0.74rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.showcase-tag {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--muted);
}

.showcase-tag::before {
  width: 22px;
  height: 1px;
  content: "";
  background: currentColor;
  opacity: 0.7;
}

.showcase-handle {
  position: relative;
  color: rgb(var(--theme-fg-rgb));
  transition: opacity 180ms ease;
}

.showcase-handle::after {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 1px;
  content: "";
  background: linear-gradient(90deg, #feda75, #fa7e1e, #d62976, #962fbf, #4f5bd5);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 300ms var(--ease);
}

.showcase-handle:hover::after,
.showcase-handle:focus-visible::after {
  transform: scaleX(1);
}

/* --- Reel cards (vertical 9:16, always a side-by-side pair) ---------- */
.reel-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--reel-gap);
  /* Narrow viewports fall back to 100% and go back to being width-driven. */
  width: min(100%, var(--reel-w));
}

.reel-card {
  display: grid;
  grid-template-rows: auto auto;
  gap: 16px;
}

.reel-media {
  position: relative;
  display: block;
  aspect-ratio: 9 / 16;
  overflow: hidden;
  border: 1px solid rgba(var(--theme-fg-rgb), 0.2);
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(var(--theme-fg-rgb), 0.12), transparent 60%),
    linear-gradient(180deg, rgba(var(--theme-fg-rgb), 0.06), rgba(var(--theme-inverse-rgb), 0.24)),
    rgb(var(--theme-bg-rgb));
  box-shadow: inset 0 0 0 1px rgba(var(--theme-fg-rgb), 0.04);
  transition: transform 320ms var(--ease), border-color 320ms ease, box-shadow 320ms ease;
}

.reel-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.001);
  transition: transform 620ms var(--ease-out), filter 320ms ease;
}

.reel-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.28) 0%, transparent 26%, transparent 52%, rgba(0, 0, 0, 0.72) 100%);
}

.reel-media.is-empty .reel-scrim {
  background: linear-gradient(180deg, transparent 40%, rgba(0, 0, 0, 0.4) 100%);
}

.reel-play {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  display: grid;
  place-items: center;
  width: clamp(52px, 6.2vw, 66px);
  height: clamp(52px, 6.2vw, 66px);
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: 50%;
  /* Crisp dark glass disc that lifts off any frame — light or dark — via a
     real drop shadow rather than a muddy blur. Inverts to solid white on
     hover (below), on brand with the black/white palette. */
  background: rgba(10, 10, 10, 0.5);
  color: #fff;
  backdrop-filter: blur(2px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.42);
  transform: translate(-50%, -50%) scale(1);
  transition: transform 340ms var(--ease-snap), background 300ms ease,
    border-color 300ms ease, color 300ms ease, box-shadow 300ms ease;
}

.reel-play svg {
  width: 38%;
  height: 38%;
  /* The glyph's bounding box sits right-of-center inside its own viewBox, so
     it reads as drifting right; a light left nudge sits it optically centered. */
  transform: translateX(-4%);
  fill: currentColor;
}

.reel-views {
  position: absolute;
  left: 14px;
  bottom: 14px;
  z-index: 2;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 7px 12px;
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: rgba(0, 0, 0, 0.42);
  color: #fff;
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  backdrop-filter: blur(6px);
}

.reel-views b {
  font-family: var(--display);
  font-size: 1.14rem;
  font-weight: 400;
  letter-spacing: 0.02em;
}

/* Diagonal sheen that sweeps once on hover — the reel equivalent of the
   header icons' shine. */
.reel-shine {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, 0.22) 50%, transparent 60%);
  transform: translateX(-120%);
  transition: transform 720ms var(--ease-out);
}

.reel-card:hover .reel-media,
.reel-media:focus-visible {
  transform: translateY(-6px);
  border-color: rgba(var(--theme-fg-rgb), 0.5);
  box-shadow: 0 26px 60px rgba(var(--theme-inverse-rgb), 0.34);
}

.reel-card:hover .reel-img {
  transform: scale(1.06);
}

.reel-card:hover .reel-play {
  transform: translate(-50%, -50%) scale(1.09);
  background: #fff;
  border-color: #fff;
  color: #0b0b0b;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
}

.reel-card:hover .reel-shine {
  transform: translateX(120%);
}

.reel-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}

.reel-meta {
  display: grid;
  gap: 2px;
  font-family: var(--body);
}

.reel-meta strong {
  font-size: 0.82rem;
  font-weight: 900;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.reel-meta span {
  color: var(--muted);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
}

/* =====================================================================
   Case Study — copy column + interactive 3D laptop stage.
   ===================================================================== */
/* Every vertical measure in this section carries a vh term beside its vw one.
   Width alone is the wrong ruler for a short-and-wide window: at 1362x632 the
   copy column came to 548px and the padding another 109, which overran the
   viewport before the laptop was even considered. */
.case {
  padding: clamp(16px, min(3vw, 4vh), 44px) 0 clamp(36px, min(5vw, 6vh), 72px);
}

.case-grid {
  display: grid;
  /* Tilted toward the stage, unlike the portrait phone it replaces: a
     landscape object is bound by COLUMN WIDTH rather than by leftover
     height, so width is the only lever that makes it bigger. The copy
     still clears 460px at every size the two-column layout survives. */
  grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.12fr);
  gap: clamp(30px, 4vw, 64px);
  align-items: center;
}

.case-copy {
  display: grid;
  gap: clamp(14px, min(1.8vw, 2.2vh), 24px);
}

.case-copy h2 {
  margin: 0;
  font-family: var(--display);
  /* Two lines at most in this column — the old 7vw ceiling pushed the copy
     past the fold on short screens before the stats ever arrived. The vh term
     covers short-and-wide windows, where vw alone would still overshoot. */
  font-size: clamp(2.4rem, min(5.2vw, 10vh), 5.6rem);
  font-weight: 400;
  line-height: 0.9;
  text-transform: uppercase;
  letter-spacing: 0.004em;
}

.case-copy h2 em {
  /* Anton has no italic cut — render the emphasis word as a calm second tone
     for a premium two-tone display line instead. */
  font-style: normal;
  color: var(--muted);
}

/* The -6px used to tuck this under the section's own h2. That heading moved
   up into the frame above in v0.8.7, so when the lead is first in the column
   there is nothing to tuck against and the pull-up just shifts the block. */
.case-copy > .case-lead:first-child {
  margin-top: 0;
}

.case-lead {
  max-width: 46ch;
  margin: -6px 0 0;
  color: var(--soft);
  font-size: clamp(1.04rem, 1.7vw, 1.3rem);
  line-height: 1.45;
}

.case-blocks {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(14px, min(1.8vw, 2.2vh), 24px) clamp(24px, 3vw, 44px);
}

.case-block {
  display: grid;
  gap: 6px;
  padding-top: 13px;
  border-top: 1px solid rgba(var(--theme-fg-rgb), 0.18);
}

.case-block h4 {
  margin: 0;
  color: rgb(var(--theme-fg-rgb));
  font-family: var(--body);
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.case-block p {
  margin: 0;
  color: var(--soft);
  font-size: clamp(0.92rem, 1.3vw, 1.02rem);
  line-height: 1.5;
  text-shadow: none;
}

/* --- 3D laptop stage ------------------------------------------------- */
.case-stage {
  position: relative;
  /* Landscape, matched to the model. laptop.js fits the OPEN pose (its
     largest, including the full cursor lean) by projecting its bounding box
     through the camera, and that projection measures ~3.13 x 2.44 = 1.28 —
     so a 9/7 box has both dimensions bind at once and there is no dead air
     on either axis. Recompute this if the model's proportions change. */
  aspect-ratio: 11 / 9;
  /* The width is capped off the viewport's LEFTOVER height, never the column
     width alone, so the stage can never be what pushes the section past one
     screen (see the v0.6.9 note). laptop.js refits through a ResizeObserver,
     so whatever box it ends up with is safe. */
  width: min(100%, calc((100svh - 150px) * 11 / 9));
  min-height: 250px;
  justify-self: center;
  isolation: isolate;
}

.laptop-glow {
  position: absolute;
  inset: 10% 2% 4%;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(58% 62% at 50% 46%, rgba(var(--theme-fg-rgb), 0.16), transparent 70%);
  filter: blur(8px);
}

.laptop-canvas {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  transition: opacity 700ms var(--ease-out);
  touch-action: pan-y;
}

.case-stage.is-live .laptop-canvas {
  opacity: 1;
}

/* Fallback for no-WebGL: a flat CSS laptop whose screen is the very same
   canvas dashboard, painted in 2D. Hidden the moment the live canvas takes
   over. Sized in % of the stage so it tracks the box at every resolution. */
.laptop-fallback {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  display: grid;
  justify-items: center;
  width: 92%;
  opacity: 0;
  transition: opacity 400ms ease;
  transform: translate(-50%, -50%);
}

.case-stage.has-poster .laptop-fallback {
  opacity: 1;
}

.laptop-fallback-lid {
  width: 88%;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  padding: 1.6%;
  border: 1px solid rgba(var(--theme-fg-rgb), 0.3);
  border-radius: 10px 10px 2px 2px;
  background: #0b0c0e;
  box-shadow: 0 26px 60px rgba(var(--theme-inverse-rgb), 0.42);
}

.laptop-fallback-screen {
  width: 100%;
  height: 100%;
  display: block;
  border-radius: 3px;
  object-fit: cover;
}

/* Percentage padding resolves against WIDTH, which is the only way to keep the
   base in proportion here — the grid parent's height is auto, so a % height
   would not resolve at all. */
.laptop-fallback-base {
  width: 100%;
  height: 0;
  padding-bottom: 2.6%;
  border-radius: 0 0 9px 9px;
  background: linear-gradient(180deg, #b6babe, #8d9195 62%, #6f7276);
}

.case-stage.is-live .laptop-fallback {
  opacity: 0;
  visibility: hidden;
  transition: opacity 500ms ease, visibility 0s 500ms;
}

/* =====================================================================
   Portfolio + Case Study — scroll-linked parallax.
   app.js writes --pf on .portfolio and --cs on .case (a 0->1 section
   progress that tracks the viewport). These rules read that progress and
   drive motion via the INDEPENDENT `translate` / `rotate` properties, so
   the parallax composes with — never overrides — the reveal + hover
   `transform` on the same elements, and updates instantly (no smear).
   Only under body.fx (skipped for reduced motion), so no-JS/RM stays still.
   ===================================================================== */
body.fx .portfolio {
  --pf: 0.5;
}
body.fx .case {
  --cs: 0.5;
}

/* Portfolio heading drifts up a touch as the section passes. */
body.fx .portfolio-intro h2 {
  translate: 0 calc((var(--pf, 0.5) - 0.5) * -34px);
}

/* The two reels counter-drift as you scroll — the pair spreads and settles,
   so the block visibly follows the scroll. No rotation: the reels stay
   upright at every scroll position. */
body.fx .reel-grid .reel-card:first-child {
  translate: 0 calc((var(--pf, 0.5) - 0.5) * 54px);
}
body.fx .reel-grid .reel-card:last-child {
  translate: 0 calc((var(--pf, 0.5) - 0.5) * -54px);
}

/* Case study: the laptop lifts, the copy counter-drifts beneath it. */
body.fx .case-stage {
  translate: 0 calc((var(--cs, 0.5) - 0.5) * -50px);
}
body.fx .case-copy .case-lead {
  translate: 0 calc((var(--cs, 0.5) - 0.5) * -20px);
}

/* =====================================================================
   Portfolio / Case Study — responsive.
   ===================================================================== */

/* Side-by-side portfolio only where there is room for both columns; the
   reels keep their height cap either way. */
@media (min-width: 1000px) {
  /* The 56% ceiling matters just above the breakpoint, where --reel-w would
     otherwise leave the copy about 300px to live in. Past ~1300px the reel
     width wins on its own and the cap never binds. */
  .portfolio-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, min(var(--reel-w), 56%));
  }

  /* Narrower column than the full-bleed original, so the display line lands
     on two lines instead of overrunning the reels. */
  .portfolio-intro h2 {
    font-size: clamp(2.6rem, min(5vw, 11vh), 5.6rem);
  }

  .portfolio-lead {
    max-width: 44ch;
  }
}

/* On wide screens the 1240px shell, not the viewport, is what caps the
   laptop — height is nowhere near binding by then. The case study alone
   gets a wider frame so the stage keeps growing past that point; every
   other section stays on --max. */
@media (min-width: 1360px) {
  .case {
    width: min(calc(100% - (var(--pad) * 2)), 1400px);
  }
}

@media (max-width: 999px) {
  /* Stacked again — the reels lose the copy column's share of the height, so
     the budget tightens by roughly the intro block. */
  .portfolio {
    --reel-h: clamp(260px, calc(100svh - 470px), 620px);
  }

  .reel-grid {
    justify-self: center;
  }
}

@media (max-width: 960px) {
  .case-grid {
    grid-template-columns: 1fr;
    gap: clamp(20px, 3.4vw, 34px);
  }

  /* Laptop first on tablet/mobile so the interactive piece leads, copy below. */
  .case-stage {
    order: -1;
    /* Stacked, the copy sits above rather than beside, so the stage gets what
       is left of the screen. The copy is a steady ~420px through this range
       and the padding + row gap another ~90-110. A landscape stage needs far
       less height than the portrait phone did for the same width, so full
       column width usually wins here and the height cap only binds on short
       viewports. */
    width: min(100%, 560px, calc(clamp(170px, 100svh - 515px, 460px) * 11 / 9));
    min-height: 170px;
  }
}

@media (max-width: 760px) {
  .portfolio {
    /* Two reels stay side by side on phones — they read as a pair. */
    --reel-gap: 12px;
  }

  /* Kill the scroll counter-drift on phones: once the reels sit in a tight
     pair the opposing vertical drift can pull them into each other, so hold
     both cards still on mobile. */
  body.fx .reel-grid .reel-card:first-child,
  body.fx .reel-grid .reel-card:last-child {
    translate: none;
  }
}

@media (max-width: 620px) {
  .portfolio {
    padding: 40px 0 36px;
  }

  /* Every px trimmed here goes straight into the laptop, which on a phone is
     the whole point of the section — the copy below is already as tight as it
     gets without cutting words. The portfolio's own 36px bottom padding still
     keeps the two sections apart. */
  .case {
    padding: 10px 0 12px;
  }

  .case-grid {
    gap: 10px;
  }

  /* Last of the height budget on a phone: the copy (heading, four blocks,
     four stats) is incompressible without cutting words, so the stage takes
     the trim — its own floor, below the tablet one, is what binds here.
     The landscape laptop needs ~0.78 of its width in height where the phone
     needed 1.67, which is what finally buys the 360px-wide case back inside
     one screen.

     And on a phone the stage BLEEDS to both screen edges: it is a landscape
     object on the narrowest screens, so the section's side padding is the
     most expensive 2x18px on the page. `100% + --pad * 2` is the full
     container width without touching `vw` — a vw here would include the
     scrollbar and overflow the document. The grid track stays put and
     `justify-self: center` splits the excess evenly, so nothing else moves.

     The subtrahend is the WHOLE POINT of this rule, so treat it as measured,
     not guessed: it is the copy column plus the row gap plus the section
     padding, which the phone trims below hold at ~484 + 32. At 490 the WIDTH
     binds — i.e. the laptop is full-bleed — on every phone taller than
     ~790px, which is where the section still lands inside one screen. Below
     that the height cap takes over and the 265px FLOOR carries the short
     phones — the floor is deliberately past what one screen allows there
     (375x667 lands at 1.18vh, 320x568 at 1.46), because those sizes already
     overran at 1.10 / 1.37 before this section had a laptop in it at all,
     and on a phone that small the laptop is worth more than the last few %
     of scroll. 265 over 240 is what takes 360-wide phones — the commonest
     small width — from 78% to 90% fill for 2% more scroll. Both numbers were
     measured with tools/case-fit-probe.html; re-run it before touching
     either, and note that probe has to kill the scrollbar to be honest. */
  .case-stage {
    width: min(
      calc(100% + var(--pad) * 2),
      calc(clamp(265px, 100svh - 490px, 520px) * 11 / 9)
    );
    min-height: 150px;
  }

  .portfolio-intro h2,
  .case-copy h2 {
    font-size: clamp(2.4rem, 11vw, 3.6rem);
  }

  /* The reel media is already a link to the post; beside a 199px caption the
     button folded onto two lines, so it goes and the caption carries. */
  .reel-foot .ghost-button {
    display: none;
  }

  /* The four blocks stay two-up on phones. Stacking them added ~160px of
     height for no gain in readability at this measure. */
  .case-blocks {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 11px 16px;
  }

  .case-copy {
    gap: 8px;
  }

  /* Every trim from here down is height the stage above spends on the laptop.
     None of them cut words — they take it out of the chrome around the words
     (rules, cell padding, leading), which is the only slack this copy has
     left. Roughly 44px in total, which is what pays for the full-bleed
     stage on a 844-tall phone. */
  .case-block {
    padding-top: 10px;
    gap: 5px;
  }

  .case-block p {
    line-height: 1.42;
  }

  .contact {
    padding-top: 40px;
    padding-bottom: 44px;
  }

  .message-card {
    min-height: 168px;
  }
}

@media (pointer: coarse) {
  /* Reels reveal their sheen + play state on tap-press, mirroring the
     site's other touch feedback. */
  .reel-card:active .reel-media {
    transform: scale(0.99);
    border-color: rgba(var(--theme-fg-rgb), 0.5);
  }

  /* No hover on touch — give the tap the same white-invert the play button
     gets on desktop hover, so the affordance still reacts. */
  .reel-card:active .reel-play {
    transform: translate(-50%, -50%) scale(1.06);
    background: #fff;
    border-color: #fff;
    color: #0b0b0b;
  }

  .ghost-button:active {
    transform: scale(0.97);
  }
}

@media (prefers-reduced-motion: reduce) {
  .reel-img,
  .reel-play,
  .reel-shine,
  .laptop-canvas {
    transition: none !important;
  }

  .reel-shine {
    display: none;
  }
}
