/* ==========================================================================
   grahamkramer.com
   Palette + type inherited from the Fantom canon (ink / paper / ember,
   Archivo + JetBrains Mono) so the personal site reads as the same universe,
   with an editorial layout language of its own.
   ========================================================================== */

:root {
  color-scheme: dark;

  --ink:        #08090A;
  --ink-2:      #101214;
  --ink-3:      #171A1C;
  --paper:      #F1F0EC;
  --paper-2:    #E4E2DC;
  --white:      #FFFFFF;
  --fog:        #8B9196;
  --slate:      #55595E;
  --ember:      #FF4D2E;

  --hairline:   rgba(255,255,255,.14);
  --hairline-d: rgba(8,9,10,.14);
  --white-04:   rgba(255,255,255,.04);
  --white-28:   rgba(255,255,255,.28);
  --white-64:   rgba(255,255,255,.64);

  /* semantic — flipped by [data-surface="paper"] */
  --bg:        var(--ink);
  --bg-raised: var(--ink-2);
  --fg:        var(--white);
  --fg-muted:  var(--fog);
  --fg-faint:  var(--white-28);
  --rule:      var(--hairline);
  --wash:      var(--white-04);

  --font: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --t-11:  clamp(10px, .605rem + .083vw, 11px);
  --t-13:  clamp(11.5px, .689rem + .124vw, 13px);
  --t-15:  clamp(13.5px, .814rem + .124vw, 15px);
  --t-17:  clamp(15px, .897rem + .165vw, 17px);
  --t-21:  clamp(17px, .982rem + .331vw, 21px);
  --t-28:  clamp(21px, 1.172rem + .579vw, 28px);
  --t-40:  clamp(27px, 1.426rem + 1.074vw, 40px);
  --t-64:  clamp(36px, 1.686rem + 2.314vw, 64px);
  --t-96:  clamp(44px, 1.702rem + 4.298vw, 96px);
  --t-140: clamp(50px, 1.477rem + 7.273vw, 140px);

  --wrap:  1320px;
  --gut:   clamp(20px, 4vw, 64px);
  --pad-y: clamp(72px, 9vw, 152px);

  --ease: cubic-bezier(.22,.61,.36,1);
}

[data-surface="paper"] {
  color-scheme: light;
  --bg:        var(--paper);
  --bg-raised: var(--paper-2);
  --fg:        var(--ink);
  --fg-muted:  var(--slate);
  --fg-faint:  rgba(8,9,10,.34);
  --rule:      var(--hairline-d);
  --wash:      rgba(8,9,10,.04);
}

/* ── reset ─────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--ink);
  color: var(--white);
  font-family: var(--font);
  font-size: var(--t-17);
  line-height: 1.48;
  font-variation-settings: 'wdth' 100;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img, picture, video { display: block; max-width: 100%; }
h1, h2, h3, p, ul, figure, blockquote { margin: 0; }
ul { list-style: none; padding: 0; }
a { color: inherit; text-decoration: none; }
::selection { background: var(--ember); color: var(--white); }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--ember); color: #fff; padding: 12px 18px;
  font: 500 var(--t-13)/1 var(--mono); letter-spacing: .12em; text-transform: uppercase;
}
.skip:focus { left: 12px; top: 12px; }

:focus-visible { outline: 2px solid var(--ember); outline-offset: 3px; }

/* ── shared ────────────────────────────────────────────────────────────── */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gut); }

.eyebrow {
  font-family: var(--mono);
  font-size: var(--t-11);
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.section {
  background: var(--bg);
  color: var(--fg);
  padding-block: var(--pad-y);
  position: relative;
}

.sec-head { max-width: 62ch; }
.sec-title {
  font-size: var(--t-96);
  font-weight: 700;
  line-height: .96;
  letter-spacing: -.025em;
  font-variation-settings: 'wdth' 112;
  margin-top: clamp(16px, 1.6vw, 24px);
}
.sec-sub {
  font-size: var(--t-21);
  line-height: 1.5;
  color: var(--fg-muted);
  margin-top: clamp(18px, 2vw, 28px);
  max-width: 56ch;
}

.btn {
  display: inline-flex; align-items: center; gap: 10px;
  margin-top: clamp(24px, 2.6vw, 36px);
  padding: 13px 22px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-13);
  letter-spacing: .1em;
  text-transform: uppercase;
  transition: background .35s var(--ease), border-color .35s var(--ease), color .35s var(--ease);
}
.btn:hover { background: var(--ember); border-color: var(--ember); color: #fff; }

.link {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--mono);
  font-size: var(--t-13);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg);
  padding-bottom: 4px;
  border-bottom: 1px solid var(--rule);
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.link:hover { color: var(--ember); border-color: var(--ember); }

/* reveal-on-scroll */
.reveal { opacity: 0; transform: translateY(22px); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; transition: none; } }

/* ── nav ───────────────────────────────────────────────────────────────── */
/* No bar, ever — same treatment as the robotics site. The header is
   transparent and mix-blend-mode:difference makes the white marks invert
   themselves over light and dark alike, so no JS colour-switching is needed.
   The blend must live on .nav itself: a fixed element isolates its
   descendants' blending, so it can only work at this level. Every colour
   inside must therefore be a literal white or black, never a themed var. */
.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  background: none;
  border: 0;
  mix-blend-mode: difference;
  pointer-events: none;          /* only the marks themselves catch clicks */
}
.nav > * { pointer-events: auto; }
.nav__in {
  max-width: var(--wrap); margin-inline: auto;
  padding: 18px var(--gut);
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
}
.nav__mark {
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.2vw, 16px);
  color: #fff;
  white-space: nowrap;
}
/* GK monogram, outlined artwork. Colour follows `color` via currentColor. */
.gk-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.nav__gk {
  height: clamp(17px, 1.5vw, 22px);
  width: auto;
  aspect-ratio: 1557 / 783;
  display: block;
  transition: opacity .3s var(--ease);
}
.nav__mark:hover .nav__gk { opacity: .62; }
@media (max-width: 520px) {
  .nav__mark-full { display: none; }
}
.nav__links { display: flex; align-items: center; gap: clamp(14px, 2vw, 30px); }
.nav__links a {
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(255,255,255,.66);
  transition: color .3s var(--ease), background .3s var(--ease), border-color .3s var(--ease);
}
.nav__links a:hover { color: #fff; }
/* Under difference blending a white fill inverts the page beneath it, so the
   label has to go black to stay legible inside that inverted pill. */
.nav__cta {
  border: 1px solid rgba(255,255,255,.34);
  border-radius: 999px;
  padding: 8px 16px;
  color: #fff !important;
}
.nav__cta:hover { background: #fff; border-color: #fff; color: #000 !important; }
@media (max-width: 640px) {
  .nav__links a:not(.nav__cta) { display: none; }
}

/* ── hero ──────────────────────────────────────────────────────────────── */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background: #000;
}
/* Exactly the hero's box — an oversized box would make object-fit:cover scale
   the plate up to fill it and crop the sides, losing GRIM and the humanoid.
   The parallax pans object-position instead, which never changes the scale. */
.hero__media { position: absolute; inset: 0; }
/* <picture> is a block with auto height, so an img set to height:100% inside it
   has no definite basis and quietly falls back to auto — the photo then renders
   at its own 3:2 ratio and leaves bare #000 below it on any window narrower
   than 1.5x its height. The picture has to carry the height for cover to work. */
.hero__media picture {
  display: block;
  width: 100%; height: 100%;
}
.hero__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 32%;
  will-change: object-position;
}
@media (prefers-reduced-motion: reduce) {
  .hero__media img { will-change: auto; }
}

/* No scrim. The plate behind the name averages L=59/255, which puts white type
   at roughly 11:1 — the black wash was costing the photograph for nothing.
   Legibility over the few bright patches is handled by the type's own shadow. */
.hero__scrim { display: none; }
.hero__grain {
  position: absolute; inset: 0;
  opacity: .16; mix-blend-mode: overlay; pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Same treatment as the header mark: the type is pure white and
   mix-blend-mode:difference inverts it against the photograph, so it needs
   neither a scrim nor a shadow to hold. Colours inside must stay literal. */
.hero__body {
  position: relative;
  width: 100%; max-width: var(--wrap);
  margin-inline: auto;
  padding: 0 var(--gut) clamp(72px, 9vh, 128px);
  mix-blend-mode: difference;
}
.hero__eyebrow { color: rgba(255,255,255,.72); }
.hero__eyebrow::before {
  content: ''; display: inline-block;
  width: 28px; height: 1px; background: var(--ember);
  vertical-align: middle; margin-right: 12px;
}
.hero__name {
  font-size: var(--t-140);
  font-weight: 700;
  line-height: .9;
  letter-spacing: -.03em;
  font-variation-settings: 'wdth' 108;
  margin-top: clamp(14px, 1.6vw, 22px);
  /* must be free to break — at phone widths "Graham Kramer" is wider than the
     viewport on one line, and body overflow-x:hidden would clip the last letter */
  overflow-wrap: break-word;
  hyphens: none;
}
@media (max-width: 620px) {
  .hero__name {
    font-variation-settings: 'wdth' 100;
    letter-spacing: -.035em;
    line-height: .94;
  }
}
.hero__line {
  margin-top: clamp(18px, 2vw, 26px);
  max-width: 40ch;
  font-size: var(--t-21);
  line-height: 1.45;
  color: rgba(255,255,255,.78);
}

.hero__scroll {
  position: absolute; left: 50%; bottom: 26px;
  transform: translateX(-50%);
  width: 1px; height: 56px;
  background: rgba(255,255,255,.18);
  overflow: hidden;
}
.hero__scroll span {
  position: absolute; inset: 0;
  background: var(--ember);
  animation: run 2.6s var(--ease) infinite;
}
@keyframes run {
  0%   { transform: translateY(-100%); }
  55%  { transform: translateY(0); }
  100% { transform: translateY(100%); }
}
@media (prefers-reduced-motion: reduce) { .hero__scroll span { animation: none; } }
@media (max-width: 720px) {
  .hero__media img { object-position: 50% 22%; }
  .hero__scroll { display: none; }
}

/* ── featured strip ────────────────────────────────────────────────────── */
.featured {
  background: var(--ink);
  border-bottom: 1px solid var(--hairline);
  padding-block: clamp(14px, 1.6vw, 22px);
}
.featured__in {
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.6vw, 20px);
  flex-wrap: wrap;
}
.featured__k {
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--white-28);
  white-space: nowrap;
}
.featured__logo {
  display: block;
  opacity: .82;
  transition: opacity .3s var(--ease);
}
.featured__logo:hover { opacity: 1; }
.featured__logo img {
  height: clamp(13px, 1.2vw, 17px);
  width: auto;
}
.featured__note {
  font-size: var(--t-11);
  line-height: 1.5;
  color: var(--white-28);
  max-width: 52ch;
}
@media (max-width: 560px) {
  .featured__note { flex-basis: 100%; }
}

/* ── bio ───────────────────────────────────────────────────────────────── */
.lede {
  font-size: var(--t-40);
  line-height: 1.22;
  letter-spacing: -.018em;
  font-weight: 500;
  max-width: 24ch;
  margin-top: clamp(20px, 2.4vw, 32px);
}
.lede--sm { font-size: var(--t-28); max-width: 34ch; line-height: 1.3; margin-top: 0; }
.lede strong { font-weight: 700; }

/* ── bio portrait ──────────────────────────────────────────────────────────
   Sits beside the lede on desktop and stacks under it on phones. aspect-ratio
   reserves the box before decode, so the facts grid below never shifts. The
   caption uses --fg-muted, not --fg-faint: the faint tier measures 2.26:1 on
   paper and fails AA. */
.bio__top {
  display: grid;
  gap: clamp(28px, 4vw, 48px);
  align-items: start;
}
@media (min-width: 900px) {
  .bio__top {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: clamp(40px, 5vw, 72px);
  }
}
/* Capped, not fluid. A free 0.9fr column renders ~560px wide and ~700px tall
   next to a three-line lede, which reads as a photo with a caption attached
   rather than a bio with a portrait. 360px keeps the lede the lead element. */
.portrait { margin: 0; max-width: 360px; }
.portrait picture { display: block; }
.portrait img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  background: var(--bg-raised);
}
.portrait__cap {
  margin-top: 14px;
  font-family: var(--mono);
  font-size: var(--t-11);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1px;
  background: var(--rule);
  border-block: 1px solid var(--rule);
  margin-top: clamp(44px, 5vw, 72px);
}
.fact {
  background: var(--bg);
  padding: clamp(20px, 2.2vw, 28px) clamp(16px, 1.8vw, 24px);
  display: flex; flex-direction: column; gap: 8px;
}
.fact__k {
  font-family: var(--mono); font-size: var(--t-11);
  letter-spacing: .16em; text-transform: uppercase; color: var(--fg-faint);
}
.fact__v { font-size: var(--t-17); font-weight: 600; letter-spacing: -.01em; }

.prose {
  margin-top: clamp(44px, 5vw, 72px);
  columns: 2;
  column-gap: clamp(32px, 4vw, 72px);
  max-width: 96ch;
}
.prose p {
  break-inside: avoid;
  margin-bottom: 1.35em;
  font-size: var(--t-17);
  line-height: 1.62;
  color: var(--fg-muted);
}
.prose strong { color: var(--fg); font-weight: 600; }
@media (max-width: 860px) { .prose { columns: 1; max-width: 62ch; } }

/* ── divisions ─────────────────────────────────────────────────────────── */
.fantom { background: var(--ink); }
.divisions { margin-top: clamp(56px, 7vw, 104px); }

.div {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  align-items: center;
  gap: clamp(28px, 4vw, 72px);
  max-width: var(--wrap);
  margin-inline: auto;
  padding: clamp(40px, 5vw, 80px) var(--gut);
  border-top: 1px solid var(--hairline);
}
.div:last-child { border-bottom: 1px solid var(--hairline); }
.div--flip .div__media { order: 2; }

.div__media {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--ink-2);
}
.div__media img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 1.1s var(--ease), filter 1.1s var(--ease);
  filter: saturate(.9) contrast(1.02);
}
.div:hover .div__media img { transform: scale(1.035); filter: saturate(1) contrast(1.04); }

.div__num {
  font-family: var(--mono); font-size: var(--t-11);
  letter-spacing: .2em; color: var(--ember);
}
.div__name {
  font-size: var(--t-40);
  font-weight: 700;
  letter-spacing: -.022em;
  font-variation-settings: 'wdth' 108;
  margin-top: 10px;
  line-height: 1.02;
}
.div__product {
  font-family: var(--mono); font-size: var(--t-13);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--white-64);
  margin-top: 10px;
}
.div__copy {
  margin-top: clamp(16px, 1.8vw, 22px);
  color: var(--fog);
  line-height: 1.62;
  max-width: 46ch;
}
.div__specs {
  margin-top: clamp(20px, 2.4vw, 30px);
  border-top: 1px solid var(--hairline);
}
.div__specs li {
  display: flex; justify-content: space-between; gap: 16px;
  padding: 11px 0;
  border-bottom: 1px solid var(--hairline);
  font-family: var(--mono);
  font-size: var(--t-13);
}
.div__specs li span:first-child { color: var(--white-28); letter-spacing: .1em; text-transform: uppercase; }
.div__specs li span:last-child { color: #fff; text-align: right; }
.div .link { margin-top: clamp(22px, 2.4vw, 30px); }

@media (max-width: 900px) {
  .div { grid-template-columns: 1fr; }
  .div--flip .div__media { order: 0; }
}

/* ── motion reel ───────────────────────────────────────────────────────── */
.reel {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(12px, 1.6vw, 24px);
  max-width: var(--wrap);
  margin: clamp(48px, 6vw, 88px) auto 0;
  padding-inline: var(--gut);
}
.reel__item { margin: 0; }
.reel__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--ink-2);
  border: 1px solid var(--hairline);
}
.reel__video {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(.92) contrast(1.03);
  transform: scale(1.001);        /* kills a 1px edge shimmer on some browsers */
}
.reel figcaption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 14px;
  padding-top: clamp(12px, 1.4vw, 18px);
}
.reel__name {
  font-family: var(--mono);
  font-size: var(--t-13);
  letter-spacing: .18em;
  color: var(--ember);
}
.reel__desc {
  flex: 1 1 16ch;
  font-size: var(--t-13);
  line-height: 1.55;
  color: var(--fog);
}
@media (max-width: 760px) {
  .reel { grid-template-columns: 1fr; gap: clamp(24px, 5vw, 36px); }
}

/* pull quote */
.pull {
  margin-top: clamp(64px, 8vw, 128px);
  text-align: center;
  max-width: 26ch;
  margin-inline: auto;
}
.pull__mark { width: 46px; margin: 0 auto clamp(24px, 3vw, 36px); opacity: .8; }
.pull blockquote p {
  font-size: var(--t-40);
  line-height: 1.24;
  letter-spacing: -.02em;
  font-weight: 500;
}
.pull figcaption {
  margin-top: clamp(20px, 2.4vw, 28px);
  font-family: var(--mono); font-size: var(--t-11);
  letter-spacing: .18em; text-transform: uppercase; color: var(--white-28);
}

/* ── before fantom ─────────────────────────────────────────────────────── */
.before__grid {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: clamp(36px, 5vw, 88px);
  margin-top: clamp(44px, 5vw, 72px);
  align-items: start;
}
.before__lead p + p {
  margin-top: clamp(20px, 2vw, 28px);
  color: var(--fg-muted);
  line-height: 1.62;
  max-width: 46ch;
}

.ledger { border-top: 1px solid var(--rule); }
.ledger li {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 6px 20px;
  padding: clamp(18px, 2vw, 26px) 0;
  border-bottom: 1px solid var(--rule);
}
.ledger__k { font-size: var(--t-21); font-weight: 700; letter-spacing: -.015em; }
.ledger__v {
  font-family: var(--mono); font-size: var(--t-11);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ember); align-self: center; text-align: right;
}
.ledger__n { grid-column: 1 / -1; color: var(--fg-muted); font-size: var(--t-15); line-height: 1.5; }
@media (max-width: 860px) { .before__grid { grid-template-columns: 1fr; } }

/* ── press links ───────────────────────────────────────────────────────── */
.links { margin-top: clamp(40px, 5vw, 64px); border-top: 1px solid var(--hairline); }
.links li { border-bottom: 1px solid var(--hairline); }
.links a {
  display: grid;
  grid-template-columns: minmax(140px, 18%) 1fr auto;
  align-items: center;
  gap: 16px;
  padding: clamp(20px, 2.3vw, 30px) clamp(4px, 1vw, 16px);
  transition: background .35s var(--ease), padding-left .35s var(--ease);
}
.links a:hover { background: var(--white-04); padding-left: clamp(14px, 2vw, 28px); }
.links__k {
  font-family: var(--mono); font-size: var(--t-11);
  letter-spacing: .16em; text-transform: uppercase; color: var(--white-28);
}
.links__t { font-size: var(--t-21); letter-spacing: -.012em; }
.links__a { color: var(--white-28); transition: color .3s var(--ease), transform .3s var(--ease); }
.links a:hover .links__a { color: var(--ember); transform: translate(3px, -3px); }
@media (max-width: 640px) {
  .links a { grid-template-columns: 1fr auto; }
  .links__k { grid-column: 1 / -1; }
}

/* ── contact ───────────────────────────────────────────────────────────── */
.contact { background: var(--ink-2); border-top: 1px solid var(--hairline); }
.contact__title {
  font-size: var(--t-64);
  font-weight: 700;
  letter-spacing: -.028em;
  font-variation-settings: 'wdth' 110;
  margin-top: clamp(14px, 1.6vw, 22px);
}
.contact__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: clamp(28px, 3.4vw, 56px);
  margin-top: clamp(40px, 4.6vw, 68px);
}
.contact__k {
  font-family: var(--mono); font-size: var(--t-11);
  letter-spacing: .16em; text-transform: uppercase; color: var(--white-28);
}
.contact__v {
  display: inline-block;
  margin-top: 12px;
  font-size: var(--t-28);
  letter-spacing: -.018em;
  border-bottom: 1px solid var(--hairline);
  padding-bottom: 4px;
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.contact__v:hover { color: var(--ember); border-color: var(--ember); }

/* ── footer ────────────────────────────────────────────────────────────── */
.foot { background: var(--ink); border-top: 1px solid var(--hairline); padding-block: clamp(44px, 5vw, 72px); }
.foot__gk {
  height: clamp(30px, 3vw, 42px);
  width: auto;
  aspect-ratio: 1557 / 783;
  display: block;
  color: var(--white);
  margin-bottom: clamp(18px, 2vw, 26px);
}
.foot__name {
  font-size: var(--t-21); font-weight: 700; letter-spacing: -.015em;
}
.foot__meta {
  margin-top: 8px;
  font-family: var(--mono); font-size: var(--t-11);
  letter-spacing: .14em; text-transform: uppercase; color: var(--fog);
}
.foot__legal {
  margin-top: clamp(24px, 2.6vw, 36px);
  max-width: 72ch;
  font-size: var(--t-13);
  line-height: 1.6;
  color: var(--white-28);
}

/* ==========================================================================
   MOTION
   Everything below animates transform / opacity / clip-path only, so the
   compositor does the work and nothing re-lays-out mid-scroll. Two triggers:
   `.is-loaded` on <html> drives the hero opening beat, and `.is-in`, set by
   the IntersectionObserver, drives everything further down the page.
   Every block has a prefers-reduced-motion escape at the foot of the file.
   ========================================================================== */

:root {
  --ease-out:  cubic-bezier(.16,.84,.34,1);
  --ease-mask: cubic-bezier(.16,1,.30,1);
}

/* -- masked line reveal ----------------------------------------------------
   .m is a clipping window and its single child starts pushed below the sill,
   so type rises into view instead of fading in place.

   The clip is clip-path, not overflow:hidden, and that is load-bearing:
   overflow makes the wrapper a block formatting context, which stops its
   margins collapsing with its neighbours and pushed the whole hero stack 2px.
   clip-path clips without touching layout at all. The inset is negative on
   three sides because these headings run line-height under 1, so the glyph
   box overflows its line box and a flush clip would shave the caps. Only the
   bottom edge does real work — the child sits 130% below it until released. */
.m { display: block; clip-path: inset(-40% -8% -0.14em -8%); }
.m > * {
  display: block;
  transform: translateY(130%);
  transition: transform 1.05s var(--ease-mask);
}
.m.is-in > *,
.is-in .m > *,
.is-loaded .hero__body .m > * { transform: none; }

/* -- opening beat ----------------------------------------------------------
   The plate settles from a shade over-scale to exactly the framing it was
   composed at, so the resting state is the one that was signed off and the
   motion only ever exists on the way in. */
.hero__media img { opacity: 0; transform: scale(1.055); }
.is-loaded .hero__media img {
  opacity: 1; transform: none;
  transition: opacity 1.2s linear, transform 2.6s var(--ease-out);
}
.is-loaded .hero__body .m--eyebrow > * { transition-delay: .20s; }
.is-loaded .hero__body .m--name    > * { transition-delay: .32s; }
.is-loaded .hero__body .m--line    > * { transition-delay: .48s; }

.nav__in { opacity: 0; }
.is-loaded .nav__in { opacity: 1; transition: opacity .9s var(--ease) .55s; }
.hero__scroll { opacity: 0; }
.is-loaded .hero__scroll { opacity: 1; transition: opacity .9s var(--ease) 1.2s; }

/* Hero copy drifts up a little slower than the page and dissolves as it goes.
   Under difference blending, fading the type out simply hands the frame back
   to the photograph, which is the effect we want anyway. */
.hero__body { will-change: transform, opacity; }

/* Grain that sits perfectly still reads as a texture overlay; jittering it a
   few percent on a stepped timer reads as film. Oversized and offset so the
   displaced edges never expose a seam. */
.hero__grain {
  inset: auto; top: -3%; left: -3%; width: 106%; height: 106%;
  animation: grainShift 6s steps(1, end) infinite;
}
@keyframes grainShift {
  0%   { transform: translate3d(0, 0, 0); }
  12%  { transform: translate3d(-1.5%, -2%, 0); }
  24%  { transform: translate3d(1%, -1.2%, 0); }
  36%  { transform: translate3d(-2%, 1.4%, 0); }
  48%  { transform: translate3d(1.6%, 2%, 0); }
  60%  { transform: translate3d(-1%, -1.6%, 0); }
  72%  { transform: translate3d(2%, .8%, 0); }
  84%  { transform: translate3d(-.8%, 1.8%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* -- scroll progress -------------------------------------------------------
   A 2px rule, not a bar: the header still has no background and still
   self-inverts. This sits under it in the stack and only ever fills in. */
.prog {
  position: fixed; top: 0; left: 0; right: 0; height: 2px;
  z-index: 90; pointer-events: none;
}
.prog i {
  display: block; height: 100%; width: 100%;
  background: var(--ember);
  transform: scaleX(0); transform-origin: 0 50%;
  will-change: transform;
}

/* -- nav -------------------------------------------------------------------
   Wipes out under the cursor and back in from the other side. Literal white,
   as everything inside the difference-blended header has to be. */
.nav__links a:not(.nav__cta) { position: relative; }
.nav__links a:not(.nav__cta)::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -5px; height: 1px;
  background: #fff;
  transform: scaleX(0); transform-origin: 100% 50%;
  transition: transform .45s var(--ease-mask);
}
.nav__links a:not(.nav__cta):hover::after { transform: scaleX(1); transform-origin: 0 50%; }

/* -- rules that draw -------------------------------------------------------
   The hairlines between the three programmes are the layout spine, so they
   are worth drawing rather than simply being there when you arrive. */
.div { position: relative; border-top: 0; }
.div::before,
.div:last-child::after {
  content: ''; position: absolute; left: 0; right: 0; height: 1px;
  background: var(--hairline);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 1.3s var(--ease-mask);
}
.div::before { top: 0; }
.div:last-child { border-bottom: 0; }
.div:last-child::after { bottom: 0; transition-delay: .22s; }
.div.is-in::before,
.div.is-in:last-child::after { transform: scaleX(1); }

/* Spec table fills in row by row once the card itself has landed. */
.div__specs li { opacity: 0; transform: translateY(9px); transition: opacity .55s var(--ease), transform .55s var(--ease); }
.div.is-in .div__specs li { opacity: 1; transform: none; }
.div.is-in .div__specs li:nth-child(1) { transition-delay: .30s; }
.div.is-in .div__specs li:nth-child(2) { transition-delay: .38s; }
.div.is-in .div__specs li:nth-child(3) { transition-delay: .46s; }
.div.is-in .div__specs li:nth-child(4) { transition-delay: .54s; }

/* -- programme stills, parallax --------------------------------------------
   The <picture> carries the drift and the <img> keeps the hover scale, so the
   two never fight over one transform. The baked overscale is what stops the
   travel from pulling an empty edge into frame. */
.div__media picture {
  position: absolute; inset: 0;
  display: block; width: 100%; height: 100%;
  transform: translate3d(0, 0, 0) scale(1.08);
  will-change: transform;
}

/* -- reel ------------------------------------------------------------------
   Frames wipe open from the bottom edge; captions follow. */
.reel__frame { clip-path: inset(0 0 100% 0); transition: clip-path 1.15s var(--ease-mask); }
.reel.is-in .reel__frame { clip-path: inset(0 0 0 0); }
.reel.is-in .reel__item:nth-child(2) .reel__frame { transition-delay: .14s; }
.reel figcaption { opacity: 0; transform: translateY(12px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.reel.is-in figcaption { opacity: 1; transform: none; transition-delay: .55s; }
.reel.is-in .reel__item:nth-child(2) figcaption { transition-delay: .68s; }

/* -- pull quote ------------------------------------------------------------
   The ghost breathes. Slow enough that you notice it only if you stop. */
.pull__mark { animation: drift 7.5s ease-in-out infinite; }
@keyframes drift {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-9px); }
}

/* -- ledgers and link lists ------------------------------------------------
   Rows arrive in sequence so a long list reads as a list and not a wall. */
.ledger li, .links li { opacity: 0; transform: translateY(12px); transition: opacity .65s var(--ease), transform .65s var(--ease); }
.ledger.is-in li, .links.is-in li { opacity: 1; transform: none; }
.ledger.is-in li:nth-child(1), .links.is-in li:nth-child(1) { transition-delay: .05s; }
.ledger.is-in li:nth-child(2), .links.is-in li:nth-child(2) { transition-delay: .13s; }
.ledger.is-in li:nth-child(3), .links.is-in li:nth-child(3) { transition-delay: .21s; }
.links.is-in li:nth-child(4) { transition-delay: .29s; }
.links.is-in li:nth-child(5) { transition-delay: .37s; }
.links.is-in li:nth-child(6) { transition-delay: .45s; }
.links.is-in li:nth-child(7) { transition-delay: .53s; }

/* -- contact and footer ----------------------------------------------------*/
.contact__cols > div { opacity: 0; transform: translateY(14px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.contact__cols.is-in > div { opacity: 1; transform: none; }
.contact__cols.is-in > div:nth-child(2) { transition-delay: .10s; }
.contact__cols.is-in > div:nth-child(3) { transition-delay: .20s; }

.foot__gk { opacity: 0; transform: translateY(14px); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.foot__in.is-in .foot__gk { opacity: 1; transform: none; }

/* ── roaming robots ────────────────────────────────────────────────────────
   GHOST and HOUND cross the page as transparent sprites, one lane per
   section. Each lane is a clipping strip pinned to the bottom of its section;
   the sprite inside is translated along it by a clock, not by scroll.

   Driving the travel from scroll was the obvious first move and it is wrong:
   an animated WebP plays at its own baked frame rate, so the moment you stop
   scrolling the legs carry on walking while the robot stands still. Feet that
   slide give the whole thing away. A clock keeps stride and ground speed
   locked to each other, and GHOST's speed below is derived from its own cycle
   rather than picked by eye — see the JS.

   The lane sits behind everything the section already had, so a robot passes
   behind the type instead of over it, and no text loses contrast. */
.lane {
  position: absolute;
  left: 0; right: 0;
  height: var(--lane-h);
  bottom: var(--lane-b);
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.section > .wrap,
.section > .divisions,
.section > .reel { position: relative; z-index: 1; }

.lane__unit {
  position: absolute;
  bottom: 0; left: 0;
  height: 100%;
  will-change: transform;
  transform: translate3d(-120%, 0, 0);
  /* Just off full strength, so they sit a step back from the type without
     going ghostly. Fading them was the first attempt at keeping text readable
     where a robot crossed underneath and it does not work: the faintest body
     colour over the sprite's black shells measures 1.34:1 at 55% and only
     reaches 4.5:1 somewhere around 15%, by which point there is no robot left
     to look at. Legibility is bought with the clear band below instead. */
  opacity: .92;
}
/* The <picture> has to carry the height. It is a plain inline box with auto
   height, so an img set to height:100% inside it has no definite basis to
   resolve against and silently falls back to its natural 540px — the same
   trap the hero plate hit further up this file. */
.lane__unit picture { display: block; height: 100%; }
.lane__unit img {
  height: 100%; width: auto; display: block;
  /* the plates were both shot facing left, so a lane running left-to-right
     mirrors the sprite rather than needing a second render */
  transform: scaleX(var(--face, 1));
}
.lane[data-dir="ltr"] { --face: -1; }

/* Contact shadow. The sprite is matted off a white cyclorama with its own
   ground shadow removed, so without this the robot reads as pasted on. Kept
   as a separate ellipse rather than a drop-shadow so it stays flat on the
   floor instead of tracing the silhouette. */
.lane__unit::after {
  content: '';
  position: absolute;
  left: 4%; right: 4%; bottom: -1px;
  height: 9px;
  background: radial-gradient(50% 50% at 50% 50%, rgba(8, 9, 10, .34), rgba(8, 9, 10, 0) 72%);
  filter: blur(2px);
}
[data-surface="paper"] .lane__unit::after { background: radial-gradient(50% 50% at 50% 50%, rgba(8, 9, 10, .26), rgba(8, 9, 10, 0) 72%); }

/* One source for each robot's size, because the section below has to reserve
   exactly as much room as the lane above takes up. Two hard-coded numbers
   drifting apart is how a robot ends up back on top of a paragraph. */
:root { --hound-h: 118px; --ghost-h: 260px; --lane-cut: 46px; }
@media (max-width: 900px) { :root { --hound-h: 88px; --ghost-h: 195px; --lane-cut: 22px; } }
@media (max-width: 560px) { :root { --hound-h: 66px; --ghost-h: 148px; --lane-cut: 22px; } }

.lane--ghost { --lane-h: var(--ghost-h); --lane-b: clamp(18px, 2.4vw, 34px); }
.lane--hound { --lane-h: var(--hound-h); --lane-b: clamp(18px, 2.4vw, 34px); }

/* The floor. Every section already ends with about 108px of air, which is less
   than either robot is tall, so a lane pinned to the bottom edge walks up into
   the last paragraph. This buys the difference — the robot gets a strip of its
   own and the copy above it is never crossed. */
/* --lane-cut is how much of the robot's height the section's own trailing air
   already covers. It is smaller on narrow screens because --pad-y shrinks with
   the viewport while the sprite does not shrink as fast, and a flat 46px left
   the list rows four pixels into the lane on a phone. */
.section:has(> .lane--hound) { padding-bottom: calc(var(--pad-y) + var(--hound-h) - var(--lane-cut)); }
.section:has(> .lane--ghost) { padding-bottom: calc(var(--pad-y) + var(--ghost-h) - var(--lane-cut)); }

@media (prefers-reduced-motion: reduce) {
  /* parked, and the markup serves the still frame instead of the loop */
  .lane__unit { transform: translate3d(0, 0, 0); left: 8%; }
}

/* -- the escape hatch ------------------------------------------------------
   One place to turn all of it off. Anything added above must land here too,
   or the page ships motion to people who have asked not to receive it. */
@media (prefers-reduced-motion: reduce) {
  .m { clip-path: none; }
  .m > * { transform: none; transition: none; }
  .hero__media img { opacity: 1; transform: none; transition: none; }
  .nav__in, .hero__scroll { opacity: 1; transition: none; }
  .hero__grain { animation: none; }
  .prog { display: none; }
  .div::before, .div:last-child::after { transform: scaleX(1); transition: none; }
  .div__specs li,
  .reel figcaption,
  .ledger li, .links li,
  .contact__cols > div,
  .foot__gk { opacity: 1; transform: none; transition: none; }
  .div__media picture { transform: none; }
  .reel__frame { clip-path: none; transition: none; }
  .pull__mark { animation: none; }
}
