/* ==================================================================
   Ported from the Claude Design source "Sudhanshu Jaisinghani v2".

   The design is dark-only. Its palette becomes the dark theme verbatim;
   the light theme is derived from it — same warmth, same structure, with
   the gold deepened so it still holds contrast on paper.
   ================================================================== */

html[data-theme="dark"] {
  --bg:       #0a0a09;
  --ink:      #ede9e2;   /* headings, emphasis */
  --lead:     #c4bfb6;   /* lead paragraphs */
  --body:     #8e8981;   /* body copy */
  --label:    #7d7871;   /* mono labels, nav */
  --faint:    #6a655f;   /* placeholder text, footer aside */
  --accent:   #cbb48c;

  --rule:     rgba(237, 233, 226, 0.07);
  --rule-2:   rgba(237, 233, 226, 0.12);
  --rule-3:   rgba(237, 233, 226, 0.16);
  --tile:     rgba(237, 233, 226, 0.015);
  --hatch:    rgba(237, 233, 226, 0.045);
  --wash:     rgba(203, 180, 140, 0.06);
  --wash-2:   rgba(203, 180, 140, 0.05);
  --accent-line: rgba(203, 180, 140, 0.30);
  --accent-under: rgba(203, 180, 140, 0.35);
  --glow:     rgba(203, 180, 140, 0.07);
  --vignette: rgba(0, 0, 0, 0.55);
  --select:   rgba(203, 180, 140, 0.22);
}

html[data-theme="light"] {
  --bg:       #f6f3ec;
  --ink:      #191713;
  --lead:     #3a372f;
  --body:     #56524a;
  --label:    #78726a;
  --faint:    #948d84;
  --accent:   #8a6a2f;

  --rule:     rgba(25, 23, 19, 0.10);
  --rule-2:   rgba(25, 23, 19, 0.16);
  --rule-3:   rgba(25, 23, 19, 0.22);
  --tile:     rgba(25, 23, 19, 0.02);
  --hatch:    rgba(25, 23, 19, 0.06);
  --wash:     rgba(138, 106, 47, 0.08);
  --wash-2:   rgba(138, 106, 47, 0.06);
  --accent-line: rgba(138, 106, 47, 0.35);
  --accent-under: rgba(138, 106, 47, 0.40);
  --glow:     rgba(138, 106, 47, 0.05);
  --vignette: rgba(0, 0, 0, 0.05);
  --select:   rgba(138, 106, 47, 0.20);
}

:root {
  /* The page's edge, defined once. Everything that should line up with the
     body copy reads this rather than repeating a number, so the deck's
     full-bleed pieces can break out of it by exactly as much as they need. */
  --gutter: 40px;
  --display: "Cormorant Garamond", Georgia, serif;
  --serif:   Newsreader, Georgia, serif;
  --mono:    "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --col:     1080px;
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; background: var(--bg); }

html {
  /* `clip`, not `hidden`. overflow-x:hidden on body makes body a scroll
     container, which silently kills position:sticky on the rails and gives
     scroll-driven timelines the wrong scroller. `clip` contains overflow
     without creating one. */
  overflow-x: clip;
  scroll-behavior: smooth;
}

body {
  color: var(--ink);
  font-family: var(--mono);
  font-weight: 300;
  -webkit-font-smoothing: antialiased;
  transition: background 400ms ease, color 400ms ease;
}

a { color: var(--ink); text-decoration: none; }
a:hover { color: var(--accent); }
::selection { background: var(--select); }

em { font-style: italic; color: var(--accent); }
strong { font-weight: inherit; color: var(--ink); }
sup { font-size: 0.62em; vertical-align: baseline; color: var(--accent); }

@keyframes maskUp   { from { opacity: 0; transform: translateY(0.42em); } to { opacity: 1; transform: none; } }
@keyframes softIn   { from { opacity: 0; } to { opacity: 1; } }
@keyframes breathe  { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }

/* ruleGrow was declared in the design and never used. It is the right
   gesture for a hairline arriving, so every rule that separates a section
   now draws itself in as you reach it. */
@keyframes ruleGrow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes riseIn   { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }

/* The reference brings its feature blocks in blurred and tilted back in 3D,
   then lifts them level and into focus as you scroll. Text stays sharp
   throughout — only media and panels get the treatment, because blurred
   body copy just reads as a rendering fault. */
@keyframes liftIn {
  from {
    opacity: 0;
    transform: translateY(54px) scale(0.93) rotateX(18deg);
    filter: blur(16px);
  }
  60% { opacity: 1; }
  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}
@keyframes heroDrift { to { transform: translateY(-56px); opacity: 0.42; } }
@keyframes progressGrow { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* ------------------------------------------------------------------
   Scroll choreography

   Two implementations of one idea. Where `animation-timeline: view()`
   exists, arrival is *scrubbed* by scroll position: it runs on the
   compositor, never touches the main thread, and reverses when you scroll
   back — so the page feels attached to the wheel rather than triggered by
   it. Everywhere else an IntersectionObserver adds `.in` and a transition
   does the same journey once.

   The initial hidden state is applied only under `html.js-reveal`, which
   JS adds after confirming it can finish the job. No JS, or reduced
   motion, and every element simply renders visible.
------------------------------------------------------------------ */
html.js-reveal .reveal {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 780ms cubic-bezier(0.16, 1, 0.3, 1),
              transform 780ms cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * 70ms);
  will-change: opacity, transform;
}
html.js-reveal .reveal.in { opacity: 1; transform: none; }

html.js-reveal .reveal-lift {
  opacity: 0;
  transform: translateY(54px) scale(0.93);
  filter: blur(16px);
  transition: opacity 900ms cubic-bezier(0.16, 1, 0.3, 1),
              transform 900ms cubic-bezier(0.16, 1, 0.3, 1),
              filter 900ms cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * 80ms);
  will-change: opacity, transform, filter;
}
html.js-reveal .reveal-lift.in { opacity: 1; transform: none; filter: blur(0); }

/* the tilt needs somewhere to be tilted in */
.cards, .about-head, .case-stats { perspective: 1400px; }

@supports (animation-timeline: view()) {
  html.js-reveal .reveal {
    opacity: 1;
    transform: none;
    transition: none;
    animation: riseIn linear both;
    animation-timeline: view();
    /* each item in a row starts a little later than the one before it */
    animation-range-start: entry calc(4% + var(--i, 0) * 6%);
    animation-range-end: cover 24%;
  }

  html.js-reveal .reveal-lift {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
    animation: liftIn linear both;
    animation-timeline: view();
    animation-range-start: entry calc(2% + var(--i, 0) * 7%);
    animation-range-end: cover 30%;
  }

  /* the hero recedes as it leaves rather than just scrolling away */
  html.js-reveal .hero-top {
    animation: heroDrift linear both;
    animation-timeline: view();
    animation-range: exit 0% exit 100%;
  }

  /* section hairlines draw themselves */
  html.js-reveal .band > .rail::after,
  html.js-reveal .stats::before {
    animation: ruleGrow linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 18%;
  }
}

/* Offscreen sections stop animating and give their compositor layers back.
   Paired with gateOffscreen() in app.js. */
.offstage, .offstage * {
  animation-play-state: paused !important;
  will-change: auto !important;
}

/* scroll progress — a hairline of the same gold as everything else */
.progress {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  z-index: 60;
  pointer-events: none;
  opacity: 0.85;
}
@supports (animation-timeline: scroll()) {
  .progress {
    animation: progressGrow linear both;
    animation-timeline: scroll(root block);
  }
}

/* ------------------------------------------------------------------
   Ambience: a gold pool that follows the cursor, over a vignette
------------------------------------------------------------------ */
.glow, .vignette {
  position: fixed;
  inset: 0;
  pointer-events: none;
}
.glow {
  z-index: 0;
  background: radial-gradient(620px 460px at var(--mx, 50%) var(--my, 18%),
              var(--glow), transparent 70%);
  transition: background 500ms ease;
}
.vignette {
  z-index: 1;
  background: radial-gradient(120% 90% at 50% 0%, transparent 40%, var(--vignette) 100%);
}

.shell { position: relative; z-index: 2; padding: 0 var(--gutter) 140px; }
.inner { max-width: var(--col); margin: 0 auto; }

/* ------------------------------------------------------------------
   Header
------------------------------------------------------------------ */
.masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 32px 0 30px;
  border-bottom: 1px solid var(--rule);
  animation: softIn 900ms ease both;
}

.brand {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 14px;
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  font: inherit;
}
.brand .name {
  font-family: var(--display);
  font-size: 19px;
  letter-spacing: 0.06em;
  color: var(--ink);
}
.pulse {
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--accent);
  animation: breathe 3.2s ease-in-out infinite;
  flex: none;
}

.masthead nav {
  display: flex;
  align-items: center;
  gap: 32px;
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.masthead nav a { color: var(--label); }
.masthead nav a:hover { color: var(--ink); }

/* theme toggle — same control as before, now living in the header */
.theme {
  width: 30px; height: 30px;
  border: 1px solid var(--rule-2);
  border-radius: 7px;
  background: transparent;
  cursor: pointer;
  display: grid;
  place-items: center;
  padding: 0;
  flex: none;
  transition: border-color 200ms ease, background 200ms ease;
}
.theme:hover { border-color: var(--accent); }
.theme span {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--ink);
  transition: background 300ms ease, box-shadow 300ms ease;
}
html[data-theme="dark"] .theme span {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--ink);
}

/* ------------------------------------------------------------------
   Avatar — the cursor-tracking head. avatar.js is untouched; this is
   only the frame it sits in.
------------------------------------------------------------------ */
.avatar {
  --tilt: 0deg; --shift: 0px; --rx: 0deg; --ry: 0deg; --px: 0px;
  --grey: 1;
  position: relative;
  border-radius: 50%;
  overflow: hidden;
  flex: none;
  background: var(--tile);
  box-shadow: inset 0 0 0 1px var(--rule-2);
  isolation: isolate;
}
.avatar.portrait { width: 168px; height: 168px; }

/* the head beside the headline */
.avatar.hero-head { width: 280px; height: 280px; }
@media (max-width: 1200px) { .avatar.hero-head { width: 220px; height: 220px; } }

.avatar canvas { display: block; width: 100%; height: 100%; }

.avatar .head-frame {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  filter: grayscale(var(--grey)) contrast(1.05);
  will-change: opacity, transform;
}
.avatar .head-frame.on { opacity: 1; }

.avatar[data-mode="frames"] { perspective: 620px; perspective-origin: 50% 45%; }
.avatar[data-mode="frames"] .head-frame.on {
  transform: rotateY(var(--ry)) rotateX(var(--tilt)) translateY(var(--shift));
  transform-origin: 50% 70%;
}
.avatar[data-mode="tilt"] { perspective: 420px; }
.avatar[data-mode="tilt"] .head-frame {
  transform: rotateY(var(--ry)) rotateX(var(--rx)) translateX(var(--px)) scale(1.08);
}

/* ------------------------------------------------------------------
   Hero
------------------------------------------------------------------ */
.eyebrow {
  font-size: 10px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--label);
}

.hero { padding: 120px 0 0; }
.hero .eyebrow { animation: softIn 900ms 120ms ease both; }

/* headline and head share a row; minmax(0,…) lets the headline shrink
   rather than push the head off the grid */
.hero-top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 56px;
  align-items: center;
}

.hero h1 {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(42px, 7.6vw, 104px);
  line-height: 0.98;
  letter-spacing: -0.02em;
  margin: 34px 0 0;
  max-width: 19ch;
  text-wrap: balance;
  overflow: hidden;
  color: var(--ink);
}
.hero h1 span { display: block; }
.hero h1 span:nth-child(1) { animation: maskUp 1000ms 200ms cubic-bezier(0.16, 1, 0.3, 1) both; }
.hero h1 span:nth-child(2) { animation: maskUp 1000ms 320ms cubic-bezier(0.16, 1, 0.3, 1) both; }
.hero h1 span:nth-child(3) { animation: maskUp 1000ms 440ms cubic-bezier(0.16, 1, 0.3, 1) both; }

.hero-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 72px;
  margin-top: 64px;
  max-width: 900px;
  animation: softIn 1200ms 600ms ease both;
}
.hero-body p {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 20px;
  line-height: 1.62;
  margin: 0;
  text-wrap: pretty;
}
.hero-body p:first-child { color: var(--lead); }
.hero-body p + p { color: var(--body); }

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 56px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  animation: softIn 1200ms 760ms ease both;
}
.btn {
  display: inline-block;
  padding: 15px 30px;
  border-radius: 2px;
  border: 1px solid var(--rule-3);
  color: var(--lead);
  transition: border-color 250ms ease, color 250ms ease, background 250ms ease;
}
.btn:hover { border-color: var(--accent); color: var(--ink); }
.btn.primary {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
}
.btn.primary:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); }
html[data-theme="light"] .btn.primary:hover { color: #fff; }

/* ------------------------------------------------------------------
   Stats strip
------------------------------------------------------------------ */
.stats {
  position: relative;
  margin-top: 140px;
  padding-top: 36px;
  border-top: 1px solid var(--rule);
  display: grid;
  /* Column count follows however many figures content.js declares — this was
     repeat(3, 1fr) while there were three, and dropping one left the last
     column empty rather than widening the two that remained. */
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 56px;
}
.figure {
  font-family: var(--display);
  font-weight: 300;
  font-size: 62px;
  line-height: 1;
  color: var(--ink);
  letter-spacing: -0.02em;
}
.figure em {
  font-family: var(--serif);
  font-size: 0.58em;
}
.stats .cap, .case-stats .cap {
  font-size: 10.5px;
  line-height: 1.9;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--label);
  margin-top: 18px;
}

/* ------------------------------------------------------------------
   Numbered sections: sticky rail + content
------------------------------------------------------------------ */
.band {
  margin-top: 150px;
  scroll-margin-top: 24px;
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 56px;
  align-items: start;
}
.rail { position: sticky; top: 40px; }
.band > .rail::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: -37px;
  height: 1px;
  background: var(--rule);
  transform-origin: 0 50%;
  display: none;
}
.rail .num {
  font-size: 10px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--accent);
}
.rail .what {
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--label);
  margin-top: 12px;
}

h2.band-title {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(34px, 4.6vw, 54px);
  line-height: 1.06;
  letter-spacing: -0.015em;
  margin: 0;
  max-width: 22ch;
  color: var(--ink);
}
/* Chapters 02—05. Section 01 keeps the full-size title so the hero and Now
   stay the two largest moments on the page. */
h2.band-title.compact {
  font-size: clamp(30px, 3.0vw, 38px);
  max-width: 26ch;
}
.band-lede {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 20px;
  line-height: 1.62;
  color: var(--body);
  max-width: 56ch;
  margin: 24px 0 0;
  text-wrap: pretty;
}

/* A chapter opener hands off to whatever it shows next — the diagram, a list
   of cases, the cards, the projects. The :not() is load-bearing: without it
   this rule outranks .band-lede's own margin and blows every title-to-lede
   gap out to 52px. */
.band-lede + *,
.band-title + *:not(.band-lede) { margin-top: 52px; }

/* Reads out of a chapter into the case study behind it. Same grammar as
   .side-link and footer .mail; the margin is its own because it follows the
   diagram rather than the lede. */
.band-link {
  display: inline-block;
  margin-top: 40px;
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  border-bottom: 1px solid var(--accent-under);
  padding-bottom: 3px;
  transition: border-color 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.band-link:hover { border-bottom-color: var(--accent); }

/* ------------------------------------------------------------------
   Work list
------------------------------------------------------------------ */
.case-row {
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  background: none;
  cursor: pointer;
  border: 0;
  border-top: 1px solid var(--rule);
  padding: 36px 0;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 32px;
  align-items: start;
  transition: padding 400ms cubic-bezier(0.16, 1, 0.3, 1),
              background 400ms cubic-bezier(0.16, 1, 0.3, 1);
}
.case-row:last-child { border-bottom: 1px solid var(--rule); }
.case-row:hover {
  padding-left: 18px;
  background: linear-gradient(90deg, var(--wash-2), transparent 60%);
}
.case-row .meta,
.case-study .meta {
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--label);
}
.case-row .ctitle {
  font-family: var(--display);
  font-weight: 300;
  /* one step under h2.band-title.compact — a chapter heading has to outrank
     the items listed beneath it */
  font-size: clamp(26px, 3.0vw, 34px);
  line-height: 1.08;
  letter-spacing: -0.015em;
  margin-top: 12px;
  color: var(--ink);
}
.case-row p {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 19px;
  line-height: 1.55;
  color: var(--body);
  max-width: 58ch;
  margin: 14px 0 0;
  text-wrap: pretty;
}
.case-row .read {
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  white-space: nowrap;
  padding-top: 30px;
}

/* ------------------------------------------------------------------
   Thinking cards
------------------------------------------------------------------ */
.cards { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; }
.card {
  border: 1px solid var(--rule-2);
  padding: 28px 26px;
  min-height: 170px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.card .kind {
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--label);
}
.slot {
  font-size: 11px;
  line-height: 1.8;
  color: var(--faint);
  background: repeating-linear-gradient(135deg, var(--hatch) 0 1px, transparent 1px 8px);
  padding: 16px;
  border: 1px dashed var(--rule-2);
}
.card .ctitle {
  font-family: var(--display);
  font-weight: 300;
  font-size: 26px;
  line-height: 1.15;
  color: var(--ink);
}
.card .hook {
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.5;
  color: var(--body);
  margin: 8px 0 0;
}

/* ------------------------------------------------------------------
   About
------------------------------------------------------------------ */
.about-intro {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 24px;
  line-height: 1.58;
  color: var(--lead);
  margin: 0;
  max-width: 52ch;
  text-wrap: pretty;
}
.about-intro em { color: var(--ink); }

.about-head { display: flex; align-items: flex-start; gap: 40px; }
.about-head .avatar { margin-top: 4px; }

.toolkit {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 40px;
  margin-top: 56px;
  padding-top: 36px;
  border-top: 1px solid var(--rule);
}
.toolkit .group {
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}
.toolkit ul {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 16px;
  line-height: 2;
  color: var(--body);
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}

/* .side used to sit inside the About column, where the 56/36/border-top
   idiom marked a divider within a chapter — the same one .toolkit still
   uses. Side projects are their own chapter now, so it gets a chapter's
   spacing from `.band-lede + *` and no rule of its own. */
.side-item {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 24px;
  align-items: baseline;
}
.side-item + .side-item {
  margin-top: 28px;
  padding-top: 28px;
  border-top: 1px solid var(--rule);
  /* baseline, like the first item — `center` floated the date mid-paragraph
     once the entries grew past a couple of lines */
  align-items: baseline;
}
.side-item h3 {
  font-family: var(--display);
  font-weight: 300;
  /* clamped so it tracks .case-row .ctitle instead of overtaking it on narrow
     screens — the two lists should read as siblings */
  font-size: clamp(24px, 2.6vw, 28px);
  line-height: 1.15;
  margin: 0;
  color: var(--ink);
}
.side-item p {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 18px;
  line-height: 1.55;
  color: var(--body);
  max-width: 56ch;
  margin: 10px 0 0;
}
.side-item .side-link {
  display: inline-block;
  margin-top: 14px;
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  border-bottom: 1px solid var(--accent-under);
  padding-bottom: 3px;
  transition: border-color 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.side-item .side-link:hover { border-bottom-color: var(--accent); }
.side-item .when {
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--label);
  white-space: nowrap;
}

/* ------------------------------------------------------------------
   The deck — side projects, one project per screen

   `.deck` is its own full-bleed section rather than a child of the band.
   The band's content column is offset by the 180px rail, so the usual
   `margin-left: calc(50% - 50vw)` would have centred the break-out on the
   wrong axis. `.inner` is centred on the viewport, so a direct child of
   `main` breaks out correctly with no magic numbers.

   Everything below is gated on scroll-driven timelines. Without them the
   whole thing is a plain vertical list, because four cards stacked in one
   grid cell with no animation to separate them is one unreadable pile.
------------------------------------------------------------------ */
.deck-figure { margin: 0; }
.deck-shot {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule-2);
  border-radius: 3px;
  /* Two of the three screenshots are near-white on a near-black page, which
     punches a hole in it. The site already has an idiom for photographic
     content — `.avatar .head-frame` desaturates and lifts contrast — so the
     shots take the same treatment, resolving to full colour on hover. */
  filter: grayscale(0.4) contrast(1.04) brightness(0.9);
  transition: filter 420ms cubic-bezier(0.16, 1, 0.3, 1);
}
.deck-card:hover .deck-shot { filter: grayscale(0) contrast(1.05) brightness(1); }
.deck-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 20px;
}
/* bullets are the card's copy, the paragraph is the list's — one at a time */
.deck-points { display: none; }
.deck-card { grid-template-columns: 1fr; gap: 22px; align-items: start; }

@supports (animation-timeline: view()) {
  .deck {
    /* Full bleed. 100vw can sit under the scrollbar; html's overflow-x:clip
       contains that without creating a scroll container. */
    width: 100vw;
    margin-left: calc(50% - 50vw);
    /* One screen of stage plus one generous screen of scroll per project, so
       each project's three beats have room to land separately. `contain` on a
       subject taller than the scrollport spans exactly the pinned window, and
       every beat range below is cut from it. */
    height: calc(100svh + var(--deck-n, 4) * 90svh);
    view-timeline-name: --deck;
    view-timeline-axis: block;
  }
  .deck-stage {
    position: sticky;
    top: 0;
    height: 100svh;
    display: grid;
    align-content: center;
    overflow: clip;
  }

  /* Each card owns the whole screen. They share one cell and stack in source
     order, so the card arriving always paints over the one leaving. */
  .deck-card {
    grid-area: 1 / 1;
    align-self: stretch;
    display: grid;
    align-content: center;
    padding: 0 40px;
    /* The last card sits on top by z-index even before it arrives, so without
       this it swallows every click meant for the card on screen. deckLive
       hands it over as the card enters.

       Only one animation may touch this: a second one declaring the same
       property synthesises its opening keyframe from the *base* value rather
       than from what the first produced, and being declared later it would
       win permanently. */
    pointer-events: none;
    animation: deckLive linear both;
    animation-timeline: --deck;
    animation-range: contain var(--in-a) contain var(--in-b);
  }

  /* the content keeps the page's column so a full-bleed card is not unmoored */
  .deck-body {
    width: 100%;
    max-width: var(--col);
    margin: 0 auto;
    display: grid;
    grid-template-columns: minmax(0, 40%) minmax(0, 60%);
    gap: 56px;
    align-items: center;
    /* beat three: text and shot leave together */
    animation: deckGroupOut linear both;
    animation-timeline: --deck;
    animation-range: contain var(--out-a) contain var(--out-b);
  }
  /* nothing to move aside for, so the description simply holds the middle */
  .deck-card.no-shot .deck-body {
    grid-template-columns: minmax(0, 1fr);
    max-width: 760px;
  }

  /* beat one: the description arrives, on its own, centred */
  .deck-text-in {
    animation: deckTextIn linear both;
    animation-timeline: --deck;
    animation-range: contain var(--in-a) contain var(--in-b);
  }
  /* beat two: it gives up the middle and takes the left column.
     Split across two elements because two animations on one element both
     writing `transform` do not compose — the later one wins outright. */
  .deck-text {
    animation: deckTextShift linear both;
    animation-timeline: --deck;
    animation-range: contain var(--shift-a) contain var(--shift-b);
  }
  .deck-card.no-shot .deck-text { animation: none; }

  /* beat two, other half: the screenshot comes up on the right */
  .deck-figure {
    align-self: center;
    animation: deckShotIn linear both;
    animation-timeline: --deck;
    animation-range: contain var(--shot-a) contain var(--shot-b);
  }
  /* The full height of the screen. The sources are 1.74:1, so at 80svh they
     want about 1570px of width — far more than the column has. Cropping to fit
     would throw away a third of the UI, so the shot keeps its aspect, overflows
     its grid track and runs off the right edge, which the stage clips. The
     track is minmax(0, …) so the overflow is visual only and moves nothing. */
  /* A clipped window rather than a loose image. The figure keeps its grid
     track — which is what the text's shift percentage is computed against —
     and reaches the viewport's right edge with a negative margin equal to the
     gap between the body's edge and the screen's. min() keeps that at zero
     once the viewport is narrower than the column. */
  .deck-figure {
    height: 100svh;
    overflow: clip;
    margin-right: min(0px, calc(var(--col) / 2 - 50vw));
  }
  .deck-card .deck-shot {
    width: 100%;
    height: 100%;
    max-width: none;
    object-fit: cover;
    /* Which slice of the shot the window shows. 0% is its left edge, 50% the
       middle, 100% its right. Panning here moves what is visible inside a
       fixed frame; translating the image instead slid it bodily over the copy. */
    object-position: var(--shot-x, 0%) center;
  }

  .deck-card h3 { font-size: clamp(34px, 5vw, 60px); }
  .deck-card.with-points p { display: none; }
  .deck-card .deck-points {
    display: grid;
    gap: 15px;
    list-style: none;
    margin: 26px 0 0;
    padding: 0;
  }
  .deck-points li {
    position: relative;
    padding-left: 26px;
    font-family: var(--serif);
    font-weight: 300;
    font-size: 18px;
    line-height: 1.5;
    color: var(--body);
  }
  /* a hairline rather than a bullet — the page's gesture, at the scale of a
     line of type */
  .deck-points li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.66em;
    width: 13px;
    height: 1px;
    background: var(--accent-line);
  }
  .deck-card .side-link { margin-top: 26px; }
  .deck-card:hover .side-link { border-bottom-color: var(--accent); }
  .deck-card + .deck-card { margin-top: 0; padding-top: 0; border-top: 0; }
}

@keyframes deckLive {
  from { pointer-events: none; }
  to   { pointer-events: auto; }
}
/* the measured blur-to-focus curve, as everything else on the page arrives */
@keyframes deckTextIn {
  from { opacity: 0; transform: translateY(50px); }
  to   { opacity: 1; transform: none; }
}
/* 40% column, so its centre sits at 20% of the row and the middle is at 50% —
   a shift of 30% of the row, which is 75% of the column's own width */
@keyframes deckTextShift {
  from { transform: translateX(75%); }
  to   { transform: none; }
}
/* Straight up from underneath. It used to slide in from the right out of a
   blur, borrowing the page's blur-to-focus arrival — but that belongs to small
   panels. At four-fifths of the screen a blur reads as a rendering fault, and
   the rise is what makes the screenshot feel like it is being raised into
   place rather than resolving into existence. */
@keyframes deckShotIn {
  from { opacity: 0; transform: translateY(120px); }
  to   { opacity: 1; transform: none; }
}
@keyframes deckGroupOut {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-80px); }
}

/* ------------------------------------------------------------------
   Footer
------------------------------------------------------------------ */
footer.site {
  margin-top: 170px;
  border-top: 1px solid var(--rule);
  padding-top: 56px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 48px;
  align-items: end;
}
footer.site .big {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(34px, 5vw, 56px);
  line-height: 1.06;
  letter-spacing: -0.015em;
  max-width: 24ch;
  color: var(--ink);
}
footer.site .mail {
  display: inline-block;
  margin-top: 28px;
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--accent);
  border-bottom: 1px solid var(--accent-under);
  padding-bottom: 4px;
}
footer.site .aside {
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--faint);
  line-height: 2.4;
  text-align: right;
}

/* ------------------------------------------------------------------
   Case study view
------------------------------------------------------------------ */
.case-study { padding: 88px 0 0; max-width: var(--col); animation: softIn 700ms ease both; }
.back {
  background: none; border: 0; padding: 0;
  font: inherit;
  cursor: pointer;
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--label);
  margin-bottom: 56px;
  transition: color 200ms ease;
}
.back:hover { color: var(--accent); }

.case-grid { display: grid; grid-template-columns: 180px 1fr; gap: 56px; align-items: start; }
.case-grid .rail .num { letter-spacing: 0.24em; }
.case-grid .rail .what {
  font-size: 10.5px;
  line-height: 2;
  letter-spacing: 0.14em;
  margin-top: 14px;
}
.case-body { max-width: 660px; }
.case-body h1 {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(38px, 5.6vw, 62px);
  line-height: 1.04;
  margin: 0;
  letter-spacing: -0.02em;
  color: var(--ink);
}
.standfirst {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 23px;
  line-height: 1.58;
  color: var(--lead);
  margin: 32px 0 0;
  text-wrap: pretty;
}
.block-label {
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--accent);
  margin-top: 48px;
}
.block-label:first-of-type { margin-top: 60px; }
.case-body .block-body {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 19px;
  line-height: 1.72;
  color: var(--body);
  margin: 16px 0 0;
  text-wrap: pretty;
}
.case-stats {
  margin-top: 48px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
  padding-top: 36px;
  border-top: 1px solid var(--rule);
}
.case-stats .figure { font-size: 52px; }
.case-stats .cap { font-size: 10px; letter-spacing: 0.16em; margin-top: 14px; line-height: 1.9; }
.all-work {
  background: none; border: 0; padding: 32px 0 0;
  font: inherit; cursor: pointer;
  margin-top: 80px;
  border-top: 1px solid var(--rule);
  width: 100%;
  text-align: left;
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--label);
  transition: color 200ms ease;
}
.all-work:hover { color: var(--accent); }

/* ------------------------------------------------------------------
   Responsive
------------------------------------------------------------------ */
@media (max-width: 900px) {
  :root { --gutter: 22px; }
  .shell { padding-bottom: 100px; }
  .hero { padding: 84px 0 0; }
  .hero-top { grid-template-columns: 1fr; gap: 28px; }
  .avatar.hero-head { width: 150px; height: 150px; }
  .hero-body { grid-template-columns: 1fr; gap: 26px; margin-top: 44px; }
  .stats { grid-template-columns: 1fr; gap: 40px; margin-top: 100px; }
  .band, .case-grid { grid-template-columns: 1fr; gap: 26px; margin-top: 100px; }
  .rail { position: static; display: flex; align-items: baseline; gap: 16px; }
  .rail .what { margin-top: 0; }
  .cards, .toolkit { grid-template-columns: 1fr; gap: 20px; }
  /* Four cards at 100svh is most of a phone's scroll for four short text
     blocks. The list this degrades to is the better small-screen reading. */
  .deck { height: auto; view-timeline-name: none; }
  .deck-stage { position: static; height: auto; display: block; overflow: visible; }
  .deck-card {
    animation: none;
    background: none; border: 0;
    z-index: auto;
    /* the handover above rides on animations that are off here */
    pointer-events: auto;
    display: block;
    /* The deck is the one full-bleed section, so its copy was the one copy on
       the page not sitting on the gutter — flush to the screen edge while
       every other section was inset. It lines up with the rest now, and the
       shot alone breaks out. */
    padding: 0 var(--gutter);
  }
  .deck-body {
    animation: none;
    display: block;
    max-width: none;
    opacity: 1;
    transform: none;
  }
  .deck-text {
    animation: none;
    opacity: 1;
    transform: none;
  }
  /* the three beats do not survive the single column, but arriving does:
     the shot comes up from below and the words follow it in */
  .deck-text-in { animation: none; opacity: 1; transform: none; }
  html.js-reveal .deck-text-in { opacity: 0; transform: translateY(24px);
    transition: opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
                transform 700ms cubic-bezier(0.16, 1, 0.3, 1); }
  html.js-reveal .deck-text-in.in { opacity: 1; transform: none; }
  /* the deck stretches the figure and crops the shot to fill a column; in a
     stacked list there is no column to fill, so the image goes back to its
     own shape and takes its frame back off the figure */
  .deck-figure {
    align-self: start; overflow: visible; border: 0;
    animation: none; opacity: 1; transform: none; filter: none;
    height: auto;
    /* edge to edge, by exactly the gutter its card now has */
    margin: 24px calc(var(--gutter) * -1) 0;
  }
  html.js-reveal .deck-figure { opacity: 0; transform: translateY(38px);
    transition: opacity 820ms cubic-bezier(0.16, 1, 0.3, 1),
                transform 820ms cubic-bezier(0.16, 1, 0.3, 1); }
  html.js-reveal .deck-figure.in { opacity: 1; transform: none; }

  @supports (animation-timeline: view()) {
    /* scrubbed by scroll on the compositor, same as the rest of the page */
    html.js-reveal .deck-figure {
      opacity: 1; transform: none; transition: none;
      animation: deckShotIn linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 26%;
    }
    html.js-reveal .deck-text-in {
      opacity: 1; transform: none; transition: none;
      animation: riseIn linear both;
      animation-timeline: view();
      animation-range: entry 12% cover 30%;
    }
  }
  /* qualified to match the deck rule's specificity — as a plain `.deck-shot`
     this lost to `.deck-card .deck-shot` and mobile kept the full-height
     image, overflowing the viewport by hundreds of pixels */
  .deck-card .deck-shot {
    height: auto;
    transform: none;
    object-fit: fill;
    object-position: 50% 50%;
    border: 1px solid var(--rule-2);
    border-radius: 3px;
    width: 100%;
    max-width: 100%;
  }
  .deck-card .deck-points { display: none; }
  .deck-card.with-points p { display: block; }
  .deck-card + .deck-card {
    margin-top: 28px; padding-top: 28px; border-top: 1px solid var(--rule);
  }
  .case-stats { grid-template-columns: 1fr; gap: 28px; }
  footer.site { grid-template-columns: 1fr; gap: 28px; align-items: start; }
  footer.site .aside { text-align: left; }
  .about-head { flex-direction: column; gap: 24px; }
  .masthead { flex-wrap: wrap; gap: 16px; }
  .masthead nav { gap: 20px; font-size: 10px; }
}

@media (max-width: 560px) {
  .side-item, .case-row { grid-template-columns: 1fr; }
  .case-row .read { padding-top: 0; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  html { scroll-behavior: auto; }
  .reveal, .reveal-lift {
    opacity: 1 !important; transform: none !important; filter: none !important;
  }
  .progress { display: none; }
  /* A scroll-driven animation is progress-based, so clamping its duration
     above does nothing. Take the deck apart instead and let the cards fall
     back into the ordinary list. */
  .deck { height: auto !important; }
  .deck-stage {
    position: static !important; height: auto !important;
    display: block !important; overflow: visible !important;
  }
  .deck-card {
    animation: none !important;
    opacity: 1 !important; transform: none !important; filter: none !important;
    background: none !important; border: 0 !important; padding: 0 !important;
    /* without this every card is unclickable: the handover is animation-driven
       and animations are switched off here */
    pointer-events: auto !important;
  }
  .deck-card { display: block !important; }
  .deck-body {
    animation: none !important; display: block !important; max-width: none !important;
    opacity: 1 !important; transform: none !important;
  }
  .deck-text-in, .deck-text {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  .deck-figure {
    align-self: start !important; overflow: visible !important; border: 0 !important;
    animation: none !important; opacity: 1 !important; transform: none !important;
    margin-top: 22px !important;
    height: auto !important; margin-right: 0 !important;
  }
  .deck-shot {
    filter: none !important;
    transform: none !important;
    object-position: 50% 50% !important;
    height: auto !important;
    object-fit: fill !important;
    border: 1px solid var(--rule-2) !important;
    border-radius: 3px !important;
    width: 100% !important;
    max-width: 100% !important;
  }
  /* the list reads better as a sentence than as three fragments */
  .deck-card .deck-points { display: none !important; }
  .deck-card.with-points p { display: block !important; }
  .deck-card + .deck-card {
    margin-top: 28px !important; padding-top: 28px !important;
    border-top: 1px solid var(--rule) !important;
  }
}
