
/* ---------- tokens ---------- */

:root {
  --ground:   #F2F1EF;
  --band:     #D8CFD0;
  --rule:     #B1A6A4;
  --slate:    #697184;
  --ink:      #413F3D;
  --burgundy: #7A2233;
  --deep:     #1D2B2B;
  --lime:     #CCE0A1;
  --accent:   #CCF0A1;
  --quote:    #1D2B2B;

  --measure: 1180px;
  --gutter: 40px;      /* page content — holds at 40px on every screen */
  --gutter-nav: 40px;  /* header only — still steps down on small screens */
  --mark-pull: 0px;    /* keep the mark aligned with the page content */
  --nav-h: 49px;       /* 36px logo + 6px padding each side + 1px rule */

  /* type scale — ratio 1.333, dramatic top end, tight bottom */
  --t-display: clamp(38px, 6.2vw, 76px);
  --t-lead:    clamp(20px, 2.4vw, 27px);
  --t-title:   21px;

  /* Vertical rhythm. Two steps only: --gap-block between things inside a
     section (a heading and its media, one case row and the next), and
     --gap-section between sections. Every page uses the same two, so a
     reader moving from the homepage into a case study meets the same
     spacing. Adjacent blocks each supply one --gap-block, which is how
     two blocks end up --gap-section apart. */
  --gap-block:   40px;
  --gap-section: 80px;
  --t-body:    15px;
  --t-small:   13px;
  --t-micro:   11.5px;
}

/* ---------- reset ---------- */

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 96px; } /* fixes the sticky-header-covering-headings bug */
html, body { overflow-x: clip; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: "Instrument Sans", system-ui, sans-serif;
  font-size: var(--t-body);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}
img { display: block; max-width: 100%; height: auto; }
a { color: inherit; }

.wrap { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter); }

/* ---------- homepage ground ---------- */

/* Soft pools of --band at low opacity, set at fixed page heights so they
   drift past as the visitor scrolls. No image: it is the palette, so it
   cannot clash with a thumbnail, and it costs nothing to load. The
   opacities are deliberately low — the projects are the picture. */
body.home {
  background-color: var(--ground);
  background-image:
    radial-gradient(ellipse 70vw 52vh at 88% 10%,
      color-mix(in srgb, var(--band) 42%, transparent), transparent 70%),
    radial-gradient(ellipse 60vw 46vh at 4% 44%,
      color-mix(in srgb, var(--band) 34%, transparent), transparent 70%),
    radial-gradient(ellipse 72vw 50vh at 96% 78%,
      color-mix(in srgb, var(--band) 30%, transparent), transparent 70%),
    radial-gradient(ellipse 56vw 40vh at 10% 100%,
      color-mix(in srgb, var(--band) 26%, transparent), transparent 70%);
  background-repeat: no-repeat;
  background-attachment: scroll;
}

/* ---------- header ---------- */

.nav {
  position: sticky; top: 0; z-index: 50;
  background: var(--deep);
  border-bottom: 1px solid rgb(255 255 255 / .12);
}
.nav__in {
  max-width: var(--measure); margin: 0 auto;
  padding: 6px var(--gutter-nav);
  display: flex; align-items: center; justify-content: space-between;
}
/* Logo sits 40px proud of the content column, so the mark reads as
   an anchor for the page rather than another item in the row. */
.mark {
  display: flex; align-items: center; gap: 14px;
  margin-left: calc(var(--mark-pull) * -1);
  text-decoration: none; color: var(--ground);
}
.mark img {
  height: 36px; width: auto; flex: none;
  /* the artwork carries its own circle and a white ground —
     multiply drops the white onto the page colour without a matte */
  mix-blend-mode: normal;
  filter: brightness(0) invert(1) contrast(1.05);
  opacity: .86;
}
.mark__name {
  font-size: 14px; font-weight: 500;
  letter-spacing: -0.01em; line-height: 1.25;
  color: var(--ground);
}
.nav__links { display: flex; gap: 26px; font-size: 14px; }
.nav__links a {
  text-decoration: none; color: rgb(242 241 239 / .78);
  padding-bottom: 3px; border-bottom: 1px solid transparent;
  transition: color .18s ease, border-color .18s ease;
}
.nav__links a:hover { color: var(--ground); border-bottom-color: rgb(242 241 239 / .28); }

/* Inlined rather than an <img> so it takes currentColor and sits at the
   same weight as the links, instead of arriving as a black block. */
.nav__social {
  display: inline-flex; align-items: center;
  color: rgb(242 241 239 / .78);
  transition: color .18s ease;
}
.nav__social svg {
  width: 18px; height: 18px; display: block;
  fill: currentColor;
}
.nav__social:hover { color: var(--ground); }
.nav__links a[aria-current="page"] { color: var(--ground); border-bottom-color: var(--accent); }

/* ---------- hero ---------- */

/* No image: the hero sits on the same soft ground as the rest of the
   page, so the opening reads as the top of one continuous surface rather
   than a picture with a page underneath it. Height is natural — a full
   viewport of near-empty ground would be air for its own sake. */
.hero-band {
  display: flex; align-items: center;
  padding-block: clamp(72px, 12vw, 150px) var(--gap-block);
}
/* padding-block, not the shorthand: a `padding` shorthand here would
   zero out the horizontal gutter that .wrap sets */
.hero { padding-block: 40px; width: 100%; }
/* the heading is a bold sentence a step above lead size, and the
   paragraph runs straight on from it at lead, so the two still read
   as one statement */
.hero h1 {
  font-size: clamp(27px, 3.4vw, 40px);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -.02em;
  margin: 0 0 6px;
  max-width: 46ch;
}
.hero p {
  font-size: var(--t-lead);
  line-height: 1.5;
  letter-spacing: -.01em;
  max-width: 46ch;
  margin: 0;
}
.hero__rule {
  height: 1px; background: var(--rule);
  margin-top: 56px;
  transform-origin: left center;
}

/* the single orchestrated moment: one page-load sequence, then the page stays still */
@media (prefers-reduced-motion: no-preference) {
  .hero h1, .hero p { animation: rise .85s cubic-bezier(.22,.68,.24,1) both; }
  .hero p { animation-delay: .1s; }
  .hero__rule { animation: draw .9s cubic-bezier(.22,.68,.24,1) .22s both; }
}
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* ---------- section furniture ---------- */

/* padding-block so .wrap keeps its side gutter — see note on .hero.
   No top padding: the block above supplies the space, so sections stay
   --gap-section apart wherever they sit. */
.sec { padding-block: 0 var(--gap-section); }
.sec#cv,
.sec#education { padding-block-end: 40px; }
/* first section on a page with no hero — one block gap from the header.
   After a case hero there is no extra padding: the project-type row's
   own padding is the gap, and its hairline is switched off, so the row
   starts straight under the footage on every project page alike. */
.sec--first { padding-block-start: var(--gap-block); }
/* the last section before the footer supplies half the gap; the footer
   supplies the other half */
main > .sec:last-child { padding-block-end: var(--gap-block); }
.sec__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 20px; margin-bottom: 26px;
}
.sec__head h2 {
  font-size: var(--t-title); font-weight: 500;
  letter-spacing: -.015em; margin: 0;
}
.sec__note { font-size: var(--t-small); color: var(--slate); }

/* ---------- featured case study ---------- */

.featured {
  display: grid; grid-template-columns: 1.05fr .95fr;
  gap: 56px; align-items: center;
}
/* The render is exported flat on the page colour — every edge within
   3 units of --ground — so it needs no card, no border and no feather.
   Keep the background transparent: any fill would reintroduce an edge. */
.featured__stage {
  display: block;
  background: none;
  text-decoration: none;
  border-radius: 6px; overflow: hidden;   /* same corners as every other tile */
}
.featured__stage img,
.featured__stage video { width: 100%; height: auto; display: block; }
.featured__link {
  text-decoration: none; color: inherit;
  border-bottom: 1px solid transparent;
  transition: border-color .2s ease;
}
.featured__link:hover { border-bottom-color: var(--burgundy); }

.featured__meta h3 {
  font-size: clamp(26px, 3vw, 34px); font-weight: 500;
  letter-spacing: -.025em; line-height: 1.1; margin: 0;
}
.role {
  font-size: var(--t-small); color: var(--slate);
  margin: 10px 0 0;
}
.featured__meta > p.desc {
  margin: 18px 0 0; max-width: 42ch;
}

.tag {
  display: inline-block; vertical-align: middle;
  font-size: var(--t-micro); letter-spacing: .04em;
  padding: 3px 9px; border-radius: 4px;
  margin-left: 12px;
}
.tag--shipped { background: var(--ink); color: var(--ground); }
.tag--concept { border: 1px solid var(--rule); color: var(--slate); }

/* burgundy appears here and nowhere else — that scarcity is what gives it weight */
.metrics { display: flex; gap: 46px; margin-top: 30px; }
.metric b {
  display: block;
  font-size: 40px; font-weight: 600;
  letter-spacing: -.035em; line-height: 1;
  color: var(--burgundy);
  font-variant-numeric: tabular-nums;
}
.metric span {
  display: block; font-size: var(--t-small);
  color: var(--slate); margin-top: 7px;
}

.more {
  display: inline-block; margin-top: 34px;
  font-size: var(--t-small); text-decoration: none;
  border-bottom: 1px solid var(--rule); padding-bottom: 2px;
  transition: border-color .2s ease;
}
.more:hover { border-bottom-color: var(--burgundy); }

/* ---------- project grid ---------- */

.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; }

.card { text-decoration: none; display: block; }
.card__stage {
  position: relative; overflow: hidden;
  border-radius: 6px; background: var(--band);
  aspect-ratio: 4 / 3;
}
/* The artwork stays put and a scrim slides over it, naming what is on the
   other side of the click. Revealing a label beats sliding the only image
   away to expose an empty panel. */
.card__shot {
  position: absolute; inset: 0;
  background: var(--band);
}
.card__img {
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
/* Per-card zoom, so a tile composed smaller in its own artwork can be
   matched optically to the others. The stage clips the overflow. */
.card__img--zoom { transform: scale(var(--card-zoom, 1.15)); }
.card__cta {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center; gap: 9px;
  background: color-mix(in srgb, var(--ink) 58%, transparent);
  color: var(--ground); font-size: var(--t-small);
  letter-spacing: -.005em;
}
.card__cta svg { width: 15px; height: 15px; flex: none; }

@media (prefers-reduced-motion: no-preference) {
  .card__cta {
    opacity: 0; transform: translateY(18%);
    transition: opacity .28s ease, transform .38s cubic-bezier(.22,.68,.24,1);
  }
  .card:hover .card__cta,
  .card:focus-visible .card__cta,
  .strip__thumb:hover .card__cta,
  .strip__thumb:focus-within .card__cta { opacity: 1; transform: none; }
}
/* no hover to depend on, so it simply stays hidden and the title carries it */
@media (prefers-reduced-motion: reduce) {
  .card__cta { opacity: 0; }
  .card:hover .card__cta, .card:focus-visible .card__cta,
  .strip__thumb:hover .card__cta { opacity: 1; }
}

/* one caption style for every thumbnail on the page, UX cards and
   product tiles alike */
.card h3, .card__name {
  font-size: var(--t-body); font-weight: 500;
  margin: 14px 0 0; letter-spacing: -.01em;
}
.card__sub { font-size: var(--t-small); color: var(--slate); margin: 4px 0 0; }

/* ---------- product design strip ---------- */

.strip { border-top: 1px solid var(--rule); padding-top: 34px; }
/* two products, so two columns — a three-column grid with one empty cell
   reads as a project missing rather than a pair */
.strip__grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px; }
.strip__thumb {
  position: relative;                /* anchors the hover overlay */
  aspect-ratio: 4 / 3; border-radius: 6px;   /* matches the UX cards */
  background: var(--band); overflow: hidden;
}
.strip__thumb img,
.strip__thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }
.strip__link { display: block; text-decoration: none; color: inherit; }

/* ---------- case study page ---------- */

/* Full-bleed video header, sized like the homepage hero so the first
   section sits just past the fold. */
.case-hero {
  position: relative;
  height: calc(100vh - var(--nav-h));
  height: calc(100svh - var(--nav-h));
  background: var(--band);
  overflow: hidden;
}
/* absolute, so the hero itself is free to lay out a title over the top */
.case-hero video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}

/* Photographic variant of the same header. The image is 124% of the
   frame — that crop is the zoom, and the surplus height is the room the
   parallax has to travel through.
   Title sits low left: it clears the phone in the upper frame, and its
   left edge lines up with every heading in the page below. */
.case-hero--image,
.case-hero--titled { display: flex; align-items: flex-end; padding-bottom: 78px; }
/* --hero-focus picks which part of the frame survives the crop: 0% holds
   the top of the image, 100% the bottom. Set it per page. */
.case-hero__img {
  position: absolute; top: 0; left: 0;
  width: 100%; height: 124%;
  object-fit: cover; object-position: center var(--hero-focus, 50%);
  will-change: transform;
}
/* Scrim sits above the image and stays put while it drifts. Weighted to
   the left, where the title is: the phone screen on the right is close
   to white, and white type over it at any workable opacity would fail. */
.case-hero__scrim {
  position: absolute; inset: 0;
  background:
    /* second pass along the bottom, where the title now sits */
    linear-gradient(to top,
      rgb(18 16 15 / .45) 0%,
      rgb(18 16 15 / .12) 34%,
      rgb(18 16 15 / 0) 58%),
    linear-gradient(to right,
      rgb(18 16 15 / .66) 0%,
      rgb(18 16 15 / .46) 38%,
      rgb(18 16 15 / .28) 68%,
      rgb(18 16 15 / .22) 100%);
}
.case-hero--image .case-hero__back,
.case-hero--titled .case-hero__back { z-index: 3; }
/* keeps .wrap's gutter, so it lands on the same edge as every heading */
.case-hero__title { position: relative; z-index: 2; width: 100%; }
.case-hero__title h1 {
  margin: 0; color: #fff;
  font-size: clamp(34px, 5.4vw, 68px); font-weight: 500;
  letter-spacing: -.03em; line-height: 1.04;
  max-width: 16ch;
  text-shadow: 0 1px 28px rgb(0 0 0 / .45);
}

/* WCAG 2.2.2: anything that plays automatically for more than five
   seconds needs a way to stop it. Sound starts off — browsers refuse
   to autoplay audio — so the second button hands it back. */
/* nudged out to the same 24px inset as the back link, and the gap opened
   up now that there are no boxes to separate them */
.case-hero__controls {
  position: absolute; bottom: 24px; z-index: 3;   /* above the scrim */
  right: max(var(--gutter), calc((100% - var(--measure)) / 2 + var(--gutter)));
  display: flex; gap: 6px;
}
/* Floats over the video rather than waiting below it, so the way out is
   visible before the visitor has scrolled. No chrome — it sits in the
   footage at 78% white, and the soft dark shadow is what keeps it legible
   if the frame behind it turns pale. Hover brings it to full white and
   nudges the arrow back, which is the affordance doing the talking. */
/* left edge follows the content column, not the viewport, so it stacks
   with the title below it and every heading further down the page */
.case-hero__back {
  position: absolute; z-index: 2; top: 24px;
  left: max(var(--gutter), calc((100% - var(--measure)) / 2 + var(--gutter)));
  display: inline-flex; align-items: center; gap: 9px;
  font-size: var(--t-small); letter-spacing: -.005em;
  color: rgb(255 255 255 / .78);
  text-decoration: none;
  text-shadow: 0 1px 16px rgb(0 0 0 / .55), 0 0 3px rgb(0 0 0 / .35);
  transition: color .2s ease;
}
.case-hero__back svg {
  width: 17px; height: 17px; flex: none;
  filter: drop-shadow(0 1px 10px rgb(0 0 0 / .5));
  transition: transform .2s cubic-bezier(.22,.68,.24,1);
}
.case-hero__back:hover { color: #fff; }
.case-hero__back:hover svg { transform: translateX(-3px); }
/* No chrome, matching the back link: the icon sits in the footage at 78%
   white and the shadow is what keeps it legible over a pale frame. The
   44px box stays for the touch target even though nothing draws it. */
.case-btn {
  width: 44px; height: 44px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; border-radius: 0;
  color: rgb(255 255 255 / .78);
  cursor: pointer;
  transition: color .2s ease, transform .2s cubic-bezier(.22,.68,.24,1);
}
.case-btn svg {
  width: 26px; height: 26px; display: block;
  filter: drop-shadow(0 1px 10px rgb(0 0 0 / .5)) drop-shadow(0 0 3px rgb(0 0 0 / .35));
}
.case-btn:hover { color: #fff; transform: scale(1.08); }
.case-btn:active { transform: scale(.96); }

/* Logo iterations: one row per concept, its treatments sharing that row
   equally. The hairline is what separates a white tile from the page —
   without it the light versions bleed into the ground. */
/* All three concepts across one row, each stacking its own positive and
   negative. Every mark is then on screen at once — a set of concepts is
   only judged by comparison — and the section costs a third of the
   height it did as three stacked rows. */
.logo-set {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
/* four concepts across the full column */
.logo-set--four { grid-template-columns: repeat(4, 1fr); gap: 16px; }
.logo-concept { margin: 0; }
.logo-concept__pair {
  display: grid; grid-auto-flow: row;
  gap: 10px;
}
.logo-tile {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--rule); border-radius: 6px;
}
.logo-caption {
  font-size: var(--t-small); color: var(--slate);
  margin-top: 10px;
}
.logo-final { margin: 10px 0 var(--gap-block); }
.logo-final img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--rule); border-radius: 6px;
}
.logo-final .logo-caption { margin-top: 10px; }

/* Case studies with no video header open with type instead. The title
   has to be visible here — there is no footage carrying it. */
.case-head { padding-block: var(--gap-block) 0; }
.case-head h1 {
  font-size: clamp(32px, 4.6vw, 58px); font-weight: 500;
  letter-spacing: -.03em; line-height: 1.04; margin: 0;
  max-width: 18ch;
}
.case-head .case-credit { margin-top: 14px; }

/* the hero version of this link is white and shadowed; on the page
   ground it just needs to be quiet */
.case-back {
  display: inline-flex; align-items: center; gap: 9px;
  margin-bottom: 20px;
  font-size: var(--t-small); color: var(--slate);
  text-decoration: none;
  transition: color .2s ease;
}
.case-back svg {
  width: 17px; height: 17px; flex: none;
  transition: transform .2s cubic-bezier(.22,.68,.24,1);
}
.case-back:hover { color: var(--ink); }
.case-back:hover svg { transform: translateX(-3px); }

.case-credit {
  font-size: var(--t-small); color: var(--slate);
  margin: 0 0 4px;
}

/* present for search engines and screen readers, absent on screen */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Heading left, prose right — the same asymmetry as the about page,
   so the section label never competes with the reading column. */
.case-row {
  display: grid; grid-template-columns: 300px 1fr;
  gap: 56px; align-items: start;
  padding-block: calc(var(--gap-section) / 2);
  border-top: 1px solid var(--rule);
}
/* larger than the homepage section headings — on a case study these are
   the only navigation through a long read, so they carry more weight */
/* heading above, content below at full column width — for media that
   earns more room than the 1fr track gives it */
.case-row--stack { grid-template-columns: 1fr; gap: 26px; }
/* kept above media pulled up into the gap, so the image never covers descenders */
.case-row--stack h2 { position: relative; z-index: 1; }
.case-row h2 {
  font-size: 30px; font-weight: 500;
  letter-spacing: -.025em; line-height: 1.1; margin: 0;
}
/* 75ch is the upper bound of comfortable reading — much wider than the
   46ch body measure elsewhere, so the section stays short, but not so
   wide that the eye loses its place on the return sweep */
.case-row p { max-width: 75ch; margin: 0 0 18px; }
/* Project metadata, the first thing under the hero: two label/value
   rows in the same 300px/prose grid as the case rows, so the labels
   line up with the section headings below. Labels at title size and
   values small and slate, so it reads as a spec sheet rather than the
   opening section of the story. No hairline: it starts straight under
   the footage. */
.case-meta {
  display: grid; grid-template-columns: 300px 1fr;
  gap: 14px 56px; align-items: baseline;
  margin: 0; padding-block: calc(var(--gap-section) / 2);
}
.case-meta dt {
  font-size: var(--t-title); font-weight: 500;
  letter-spacing: -.015em; line-height: 1.2;
}
.case-meta dd {
  margin: 0; font-size: var(--t-small); color: var(--slate);
}

/* sub-heading within a section — a step below the 30px h2 */
.case-sub {
  font-size: var(--t-title); font-weight: 500;
  letter-spacing: -.015em; margin: 0 0 14px;
}

/* One of the two jobs burgundy is allowed to do. It works because it is
   rare — this is the only burgundy on the page besides the nav marker. */
.pull {
  margin: 34px 0 0; padding: 0;
  font-size: clamp(21px, 2.3vw, 27px); font-weight: 600;
  color: var(--quote);
  letter-spacing: -.025em; line-height: 1.3;
  max-width: 44ch;
}
.statband {
  padding-block: var(--gap-block);
}
.statband > .case-row {
  position: relative;
  border-top: 0;
}
.statband > .case-row::before {
  content: "";
  position: absolute;
  top: 0; left: var(--gutter); right: var(--gutter);
  border-top: 1px solid var(--rule);
}
/* for a section running straight into a band — its own bottom padding
   would stack with the band's top padding and open a canyon */
.sec--flush { padding-block-end: 0; }
/* Prototype video: native controls, since a 99-second walkthrough needs
   scrubbing and a pause. No autoplay — it would start itself as the
   visitor scrolled past and compete with everything above it.
   The frame crops ~41px of empty ground from the top and bottom of the
   source, which is all there is before the phone's corners get clipped.
   Done in CSS, so the file is untouched. */
/* Full-width artwork inside a case section: spans the whole content
   column rather than the narrower prose track beside a heading. */
.case-still, .proto-still {
  width: 100%; height: auto; display: block;
  border-radius: 6px;
}
.case-still { margin: 6px 0 var(--gap-block); }
.case-still--link { display: block; cursor: zoom-in; }

/* explains what a diagram above it is showing */
.case-caption {
  margin: -28px 0 var(--gap-block);
  font-size: var(--t-small); font-style: italic; font-weight: 600;
  color: var(--slate); max-width: 70ch;
}

/* Two boards sharing a row. Each links to its own full-size file, because
   at half the column width the text on a 1920px board is unreadable. */
.case-pair {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 20px; margin: 6px 0 var(--gap-block);
}
.case-pair figure { margin: 0; }
.case-pair img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--rule); border-radius: 6px;
}
.case-pair figcaption {
  margin-top: 10px;
  font-size: var(--t-small); color: var(--slate);
}
.case-pair a { text-decoration: none; color: inherit; }
.case-pair a:hover img { border-color: var(--ink); }
.carousel--additional-ui { transform: translateY(-30px); }
.carousel--reelestate-mvp { transform: translateY(-30px); }
/* tight to the video below it — the still and the walkthrough are one
   block, not two separate items */
/* the export carries a band of empty ground above the phones; pulling it
   up closes the gap to the heading without re-cropping the image */
.proto-still { margin: -40px 0 10px; }
/* One shape for both the poster and the video, so pressing play changes
   nothing about the frame. */
.proto-frame {
  position: relative;
  aspect-ratio: 2048 / 1070;
  overflow: hidden; border-radius: 6px;
  background: var(--band);
}

/* A designed cover instead of a poster frame. Quick Look returns a
   representative frame, not frame zero, so an extracted poster never
   matched what the video actually opens on. Hidden until JS confirms it
   can be dismissed — otherwise it would trap the video behind it. */
.proto-cover { display: none; }
.proto-frame.is-ready .proto-cover {
  display: block;
  position: absolute; inset: 0; z-index: 2;
  width: 100%; padding: 0; border: 0;
  background: #112230;
  cursor: pointer; font: inherit;
}
/* cover, so the poster fills the frame edge to edge. Its own ground is
   flat and its artwork is centred, so the crop takes only ground — and
   with no letterbox there is no join to colour-match. */
.proto-cover__bg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
/* the artwork centres its mark, so the label sits low and clear of it */
.proto-cover__foot {
  position: absolute; left: 0; right: 0; bottom: 34px;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.proto-cover__title {
  font-size: var(--t-title); font-weight: 500;
  letter-spacing: -.015em; color: #fff;
}
.proto-cover__play {
  display: flex; align-items: center; gap: 9px;
  font-size: var(--t-small); color: rgb(255 255 255 / .78);
}
.proto-cover__play svg { width: 17px; height: 17px; }
.proto-frame.is-ready .proto-cover:hover .proto-cover__play { color: #fff; }

@media (prefers-reduced-motion: no-preference) {
  .proto-cover, .proto-cover__play { transition: opacity .3s ease, color .2s ease; }
  .proto-cover.is-gone { opacity: 0; pointer-events: none; }
}
.proto {
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}

/* Radial chart. Segment reach encodes the value; each segment translates
   along its own bisector on hover, which is what --dx/--dy carry. */
.radial {
  display: grid; grid-template-columns: 330px 1fr;
  gap: 54px; align-items: center;
}
.radial__chart { position: relative; }
.radial__chart svg { width: 100%; height: auto; display: block; overflow: visible; }
.radial__track circle { fill: none; stroke: var(--rule); stroke-width: 1; }
/* dashed, not solid: it reads as a notional maximum rather than as a
   fourth piece of data — but it has to survive being seen. */
.radial__max { stroke-dasharray: 4 7; stroke-width: 2; }

.radial__seg {
  fill: color-mix(in srgb, var(--deep) 48%, var(--ground));
  stroke: var(--ground); stroke-width: 2;
  cursor: pointer;
}
.radial__seg[data-key="b"] { fill: color-mix(in srgb, var(--deep) 72%, var(--ground)); }
.radial__seg[data-key="c"] { fill: var(--deep); }
/* lime only on the active segment, so the static chart stays in deep-green shades */
.radial__seg.is-active { fill: var(--accent); }

/* hub reads out whichever segment is active */
.radial__hub {
  position: absolute; inset: 0;
  display: grid; place-content: center; text-align: center;
  pointer-events: none;
}
.radial__hub b {
  display: block; font-size: 34px; font-weight: 600;
  letter-spacing: -.035em; line-height: 1; color: var(--deep);
  font-variant-numeric: tabular-nums;
}
.radial__hub span {
  display: block; margin: 6px auto 0; max-width: 12ch;
  font-size: var(--t-small); color: var(--slate); line-height: 1.3;
}

.radial__legend { list-style: none; margin: 0; padding: 0; }
.radial__item {
  display: grid; grid-template-columns: 12px 1fr; gap: 14px;
  padding: 16px 0; border-top: 1px solid var(--rule);
  cursor: pointer;
}
.radial__item:last-child { border-bottom: 1px solid var(--rule); }
.radial__swatch {
  width: 12px; height: 12px; border-radius: 50%;
  margin-top: 7px; background: color-mix(in srgb, var(--deep) 48%, var(--ground));
}
.radial__item[data-key="b"] .radial__swatch { background: color-mix(in srgb, var(--deep) 72%, var(--ground)); }
.radial__item[data-key="c"] .radial__swatch { background: var(--deep); }
.radial__item.is-active .radial__swatch { background: var(--accent); }

.radial__item b {
  display: block; font-size: var(--t-title); font-weight: 500;
  letter-spacing: -.015em; font-variant-numeric: tabular-nums;
}
.radial__label { display: block; font-size: var(--t-small); color: var(--slate); }
/* The detail always occupies its space — only its opacity changes. A
   collapsing height would resize the legend, and since the chart is
   centred against it, the whole section would jump on every hover. */
.radial__detail {
  margin: 8px 0 0; max-width: 42ch;
  font-size: var(--t-small); color: var(--slate); line-height: 1.45;
}

@media (prefers-reduced-motion: no-preference) {
  .radial__seg { transition: transform .4s cubic-bezier(.22,.68,.24,1), fill .28s ease; }
  .radial__seg.is-active { transform: translate(var(--dx), var(--dy)); }
  .radial__swatch { transition: background .28s ease; }
  .radial__detail { opacity: 0; transition: opacity .28s ease; }
  .radial__item.is-active .radial__detail { opacity: 1; }
}
/* the sample size belongs beside the percentages, not buried — a
   percentage without an n is the first thing a researcher will query */
.stats__note {
  margin: 32px 0 0; text-align: center;
  font-size: var(--t-small); color: var(--slate);
}

.pull__source {
  display: block; margin-top: 14px;
  font-size: var(--t-small); font-weight: 400;
  letter-spacing: 0; color: var(--slate);
}
.case-row p:last-child { margin-bottom: 0; }

/* ---------- about page ---------- */

/* Portrait sits beside the bio rather than centred over a band —
   asymmetric, and it keeps the reading column intact. */
/* stretch, not start: the portrait takes its height from the text beside
   it, so the two columns end on the same line */
.bio { display: grid; grid-template-columns: 300px 1fr; gap: 56px; align-items: stretch; }
.bio__portrait {
  width: 100%; height: 100%;
  object-fit: cover; object-position: center 42%;
  border-radius: 6px; background: var(--band);
}
/* same measure as the case study prose, so a reader moving between the
   two pages meets one column width */
.bio p { max-width: 75ch; margin: 0 0 18px; }
.bio p:last-child { margin-bottom: 0; }

/* ---------- skills strip ---------- */

/* Four marks drawn in the site's own hairline language rather than stock
   icons. Each one is structural: it diagrams the idea instead of
   illustrating it, and on hover it moves to act the idea out. */
.traits { display: grid; grid-template-columns: repeat(4, 1fr); gap: 26px; }
/* the traits lead straight into the CV, so a tighter gap ties them to it */
.sec:has(> .traits) { padding-block-end: calc(var(--gap-section) - 30px); }
.trait { text-align: center; }
.trait__disc {
  width: 132px; height: 132px; margin: 0 auto 20px;
  border-radius: 50%; background: var(--deep);
  display: grid; place-items: center;
}
.trait__disc svg { width: 58px; height: 58px; overflow: visible; }
.trait__disc svg * {
  stroke: #fff; fill: none;
  /* thinner at this size, or the marks read heavy against the hairlines
     used everywhere else on the page */
  stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round;
}
.trait h3 {
  font-size: var(--t-title); font-weight: 500;
  letter-spacing: -.015em; margin: 0;
}
/* the proof, not a description — revealed rather than always shouting */
.trait p {
  font-size: var(--t-small); color: var(--slate);
  margin: 8px 0 0; max-width: 26ch; margin-inline: auto;
}

@media (prefers-reduced-motion: no-preference) {
  .trait p { opacity: 0; transform: translateY(-4px); transition: opacity .3s ease, transform .3s ease; }
  .trait:hover p { opacity: 1; transform: none; }

  .traits .trait__disc {
    opacity: 0;
    transform: scale(.48) rotate(-28deg);
    animation: dh-circle 1.05s cubic-bezier(.18,.82,.22,1) 2.1s forwards;
  }
  .traits .trait:nth-child(2) .trait__disc { animation-delay: 2.25s; }
  .traits .trait:nth-child(3) .trait__disc { animation-delay: 2.4s; }
  .traits .trait:nth-child(4) .trait__disc { animation-delay: 2.55s; }

  .trait__disc svg * { transition: stroke .3s ease; }
  .trait:hover .trait__disc svg * { stroke: var(--lime); }
  .trait__disc svg g { transition: transform .45s cubic-bezier(.22,.68,.24,1); }
  /* nodes draw together */
  .trait:hover .t-team g:nth-child(1) { transform: translate(2.2px, 2px); }
  .trait:hover .t-team g:nth-child(2) { transform: translate(-2.2px, 2px); }
  /* the exchange runs both ways */
  .trait:hover .t-comms g:nth-child(1) { transform: translateX(2.5px); }
  .trait:hover .t-comms g:nth-child(2) { transform: translateX(-2.5px); }
  /* two sides step toward each other */
  .trait:hover .t-client g:nth-child(1) { transform: translateX(2.5px); }
  .trait:hover .t-client g:nth-child(2) { transform: translateX(-2.5px); }
  /* the tangle resolves into a straight line */
  .t-solve .t-solve__knot { transition: opacity .35s ease; }
  .t-solve .t-solve__line { opacity: 0; transition: opacity .35s ease; }
  .trait:hover .t-solve__knot { opacity: 0; }
  .trait:hover .t-solve__line { opacity: 1; }
}

/* no hover on touch, so the proof lines simply stay visible — and the
   chart legend's detail lines likewise, or they leave a blank space */
@media (hover: none) {
  .trait p { opacity: 1; transform: none; }
  .radial__detail { opacity: 1; }
}

/* CV rows: date left, detail right. A hairline per row does the
   separating, so no boxes or cards are needed. */
.cv { border-top: 1px solid var(--rule); }
.cv__row {
  display: grid; grid-template-columns: 190px 1fr; gap: 30px;
  padding: 16px 0; border-bottom: 1px solid var(--rule);
}
.cv__when { font-size: var(--t-small); color: var(--slate); margin: 0; }
.cv__what { margin: 0; font-weight: 500; }
.cv__where { margin: 2px 0 0; font-size: var(--t-small); color: var(--slate); }

/* Logo wall, no captions. The logos come in three proportions, so a grid
   of equal squares would shrink the wordmarks to a fraction of the icons.
   Sizing by height instead, with wordmarks set shorter, balances them by
   the area they actually occupy — which is what reads as uniform. */
/* space-between pins the first and last logo to the content edges and
   distributes the rest evenly, so the row spans the full measure and
   reads as balanced rather than as a cluster */
.software {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between;
  gap: 30px 24px;
}
.software img { display: block; width: auto; }
/* square and near-square app icons */
.software .sw-icon { height: 76px; }
/* horizontal wordmarks — shorter, but wider, so they carry equal weight */
.software .sw-mark { height: 42px; }

/* ---------- contact ---------- */

.contact { display: grid; grid-template-columns: 300px 1fr; gap: 56px; align-items: start; }
.contact__intro { position: relative; }
.contact__intro h1 {
  position: relative; z-index: 1;   /* the plane passes behind it */
  font-size: clamp(32px, 4.2vw, 48px); font-weight: 500;
  letter-spacing: -.03em; line-height: 1.04; margin: 0 0 18px;
}

/* ---------- the paper plane ---------- */

/* Fixed pixel stage so the flight path and the drawn trail share one
   coordinate space — the SVG is sized 1:1 with the CSS path below. */
.plane-stage {
  position: absolute; top: -46px; left: -52px;
  width: 480px; height: 170px;
  pointer-events: none; z-index: 0;
}
.plane-trail path {
  fill: none; stroke: var(--rule); stroke-width: 1;
  stroke-dasharray: 3 7;
}
.plane {
  position: absolute; top: 0; left: 0;
  width: 26px; height: 26px;
  fill: none; stroke: var(--ink);
  stroke-width: 1.4; stroke-linejoin: round; stroke-linecap: round;
}

@media (prefers-reduced-motion: no-preference) {
  .plane-trail path {
    stroke-dashoffset: 900;
    animation: trail 2.4s cubic-bezier(.32,.7,.35,1) .25s forwards;
  }
  .plane {
    offset-path: path("M-50 140 C 40 140, 80 40, 170 40 C 250 40, 268 106, 214 114 C 170 120, 168 56, 260 50 C 340 45, 400 30, 490 16");
    offset-rotate: auto;            /* banks into its own turns */
    offset-distance: 0%;
    opacity: 0;
    animation: fly 2.4s cubic-bezier(.32,.7,.35,1) .25s forwards;
  }
}

/* the trail is drawn dash by dash, then fades once the plane has gone */
@keyframes trail {
  0%   { stroke-dashoffset: 900; opacity: 1; }
  70%  { stroke-dashoffset: 0;   opacity: 1; }
  100% { stroke-dashoffset: 0;   opacity: 0; }
}
@keyframes fly {
  0%   { offset-distance: 0%;   opacity: 0; }
  8%   { opacity: 1; }
  88%  { opacity: 1; }
  100% { offset-distance: 100%; opacity: 0; }
}

/* no animation to run, so nothing should be left sitting on the page */
@media (prefers-reduced-motion: reduce) {
  .plane-stage { display: none; }
}
.contact__intro p { max-width: 34ch; margin: 0 0 14px; }
.contact__direct a { color: var(--slate); text-decoration: none; border-bottom: 1px solid var(--rule); }
.contact__direct a:hover { color: var(--ink); border-bottom-color: var(--ink); }

/* Outlined and rounded. Kept to a 1px --rule hairline rather than a
   heavier border, so four boxes sit in the page rather than on top of it. */
.form { max-width: 34rem; }
.form__row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.form__field { margin-bottom: 22px; }
.form__field label {
  display: block; margin-bottom: 7px;
  font-size: var(--t-small); color: var(--slate);
}
.form__field input,
.form__field textarea {
  width: 100%; padding: 12px 14px;
  font: inherit; color: var(--ink);
  background: none;
  border: 1px solid var(--rule);
  border-radius: 10px;
  transition: border-color .18s ease;
}
.form__field textarea { resize: vertical; line-height: 1.6; }
.form__field input:focus,
.form__field textarea:focus { outline: none; border-color: var(--accent); }
.form__field input:focus-visible,
.form__field textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* honeypot: present for bots, gone for everyone else */
.form__trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.btn {
  font: inherit; font-size: var(--t-small);
  color: var(--ground); background: var(--ink);
  border: 1px solid var(--ink); border-radius: 10px;   /* matches the fields */
  padding: 12px 24px; cursor: pointer;
  transition: background .18s ease, color .18s ease;
}
.btn:hover { background: transparent; color: var(--ink); }
.btn:disabled { opacity: .55; cursor: default; }
.btn:disabled:hover { background: var(--ink); color: var(--ground); }

.form__status {
  margin: 16px 0 0; min-height: 1.4em;
  font-size: var(--t-small); color: var(--slate);
}
.form__status.is-good { color: var(--ink); }
.form__status.is-bad { color: var(--burgundy); }

/* ---------- carousel ---------- */

/* Same width as .case-still, so the slider's edges line up exactly with
   the full-width board above it. */
.carousel { position: relative; margin: 6px 0 var(--gap-block); }
/* for a carousel that follows straight on from the text above it */
.carousel--tight { margin-top: -14px; }

/* Narrower frame for artwork with little resolution to spare: showing it
   smaller raises the pixel density, so rendered screens sharpen up rather
   than being stretched across the full column. */
.carousel--narrow { max-width: 780px; margin-inline: auto; }
.carousel__viewport { overflow: hidden; border-radius: 6px; }
.carousel__track {
  display: flex;
  transition: transform .45s cubic-bezier(.22,.68,.24,1);
}
.carousel__track > * {
  flex: 0 0 100%; width: 100%;
}
.carousel__track img { width: 100%; height: auto; display: block; }

/* For a set whose images are not all the same shape: the frame is fixed
   and each slide fills it, so the carousel never changes height as it
   moves. --slide-ratio sets that frame. */
.carousel--uniform .carousel__viewport { aspect-ratio: var(--slide-ratio, 16 / 9); }
.carousel--uniform .carousel__track { height: 100%; }
.carousel--uniform .carousel__track img { height: 100%; object-fit: cover; }

/* Caption lives inside the slide, so it travels with the track — the text
   arrives with its own image rather than being swapped underneath it.
   Clipping here stops a scaled image bleeding into the slide beside it. */
.carousel__slide { position: relative; overflow: hidden; }
.carousel__caption {
  /* sits in the gap between the prev button and the phone, pushed in
     towards the device rather than hugging the frame edge */
  position: absolute; left: 18%; top: 50%; transform: translateY(-50%);
  width: 28%; color: var(--ink); z-index: 1;
}
.carousel__caption h3 {
  margin: 0 0 .5em;
  font-size: clamp(17px, 2.2vw, 30px); font-weight: 600;
  letter-spacing: -.02em; line-height: 1.1;
}
.carousel__caption p {
  margin: 0;
  font-size: clamp(11px, 1.3vw, 17px); line-height: 1.45;
  color: var(--slate);
}

/* The white is gone from the files themselves — keyed to transparency
   with a soft edge — so nothing depends on a blend mode rendering
   consistently across five images. */
.carousel--blend .carousel__viewport { border-radius: 0; }
.carousel--blend .carousel__track img {
  /* shifted left so the device closes on the caption and the pair sits
     centred in the frame rather than spread to its edges */
  transform: translateX(var(--slide-shift, -4%)) scale(var(--slide-zoom, 1.04));
}

/* float over the artwork, frosted so they read on a light or dark frame */
.carousel__btn {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 46px; height: 46px; padding: 0; z-index: 2;
  display: grid; place-items: center;
  color: var(--ink);
  background: color-mix(in srgb, var(--ground) 86%, transparent);
  backdrop-filter: blur(10px);
  border: 1px solid var(--rule); border-radius: 50%;
  cursor: pointer;
  transition: border-color .18s ease, background .18s ease, opacity .18s ease;
}
.carousel__btn:hover { border-color: var(--ink); }
.carousel__btn svg { width: 18px; height: 18px; display: block; }
.carousel__btn--prev { left: 18px; }
.carousel__btn--next { right: 18px; }

.carousel__count {
  margin-top: 10px;
  font-size: var(--t-small); color: var(--slate);
  font-variant-numeric: tabular-nums;
}

/* Position is carried by the dots: quiet slate for the rest of the set,
   darker and larger for the one on screen.
   They sit over the artwork rather than below it — beneath the frame they
   fall outside the view unless the page is scrolled right down, which is
   the one moment they are no use. */
.carousel__dots {
  position: absolute; left: 0; right: 0; bottom: 5%;
  z-index: 2;
  display: flex; justify-content: center; align-items: center;
  gap: 11px;
}
.carousel__dot {
  width: 9px; height: 9px; padding: 0;
  border: 0; border-radius: 50%;
  background: var(--slate); opacity: .4;
  cursor: pointer;
  transition: opacity .2s ease, background .2s ease, transform .25s cubic-bezier(.22,.68,.24,1);
}
.carousel__dot:hover { opacity: .7; }
.carousel__dot.is-on {
  background: var(--ink); opacity: 1;
  transform: scale(1.45);
}
@media (prefers-reduced-motion: reduce) {
  .carousel__dot { transition: opacity .2s ease, background .2s ease; }
}

@media (prefers-reduced-motion: reduce) {
  .carousel__track { transition: none; }
}

/* ---------- lightbox ---------- */

.lightbox {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  padding: 6vmin;
  background: color-mix(in srgb, var(--ink) 86%, transparent);
  backdrop-filter: blur(6px);
}
.lightbox[hidden] { display: none; }
/* scrolls once the image is larger than the viewport */
.lightbox__stage {
  max-width: 100%; max-height: 100%;
  display: flex; align-items: center; justify-content: center;
}
.lightbox.is-zoomed .lightbox__stage {
  width: 100%; height: 100%;
  overflow: auto; align-items: flex-start; justify-content: flex-start;
  cursor: grab;
}
.lightbox.is-zoomed .lightbox__stage.is-dragging { cursor: grabbing; }

.lightbox__img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  object-fit: contain; border-radius: 6px;
  display: block;
}
/* a wide diagram has to exceed the viewport before its labels are legible */
.lightbox.is-zoomed .lightbox__img {
  max-width: none; max-height: none;
  width: auto; height: auto; border-radius: 0;
}
.lightbox__zoomable { cursor: zoom-in; }
.lightbox.is-zoomed .lightbox__zoomable { cursor: zoom-out; }

/* sits beside the close button */
.lightbox__hint {
  position: absolute; top: 34px; left: 26px;
  font-size: var(--t-small); color: rgb(255 255 255 / .7);
}
.lightbox__close {
  position: absolute; top: 26px; right: 26px;
  width: 46px; height: 46px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid rgb(255 255 255 / .45); border-radius: 50%;
  background: none; color: #fff; cursor: pointer;
  transition: border-color .18s ease, background .18s ease;
}
.lightbox__close:hover {
  border-color: #fff; background: rgb(255 255 255 / .12);
}
.lightbox__close svg { width: 18px; height: 18px; display: block; }

@media (prefers-reduced-motion: no-preference) {
  .lightbox { animation: lbFade .22s ease both; }
  .lightbox__img { animation: lbRise .3s cubic-bezier(.22,.68,.24,1) both; }
}
@keyframes lbFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes lbRise { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: none; } }

/* ---------- next project ---------- */

/* Naming the destination as well as labelling it: a reviewer decides
   whether to carry on based on what is next, not on the word "next". */
.next { border-top: 1px solid var(--rule); }
/* Right-aligned: it reads as forward motion, and the arrow ends on the
   same edge the eye is already travelling towards. */
.next__link {
  display: block; padding-block: var(--gap-block);
  text-decoration: none; color: inherit;
  text-align: right;
}
.next__label { display: block; font-size: var(--t-small); color: var(--slate); }
.next__title {
  display: inline-flex; align-items: center; gap: 16px;
  margin-top: 8px;
  font-size: clamp(26px, 3vw, 34px); font-weight: 500;
  letter-spacing: -.025em; line-height: 1.1;
  border-bottom: 1px solid transparent; padding-bottom: 3px;
  transition: border-color .2s ease;
}
.next__title svg { width: 26px; height: 26px; flex: none; }
.next__link:hover .next__title { border-bottom-color: var(--burgundy); }

@media (prefers-reduced-motion: no-preference) {
  .next__title svg { transition: transform .25s cubic-bezier(.22,.68,.24,1); }
  .next__link:hover .next__title svg { transform: translateX(7px); }
}

/* ---------- footer ---------- */

.foot {
  border-top: 1px solid var(--rule);
  padding-block: var(--gap-block) 60px;
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: 30px; flex-wrap: wrap;
}
.foot__links { display: flex; gap: 22px; font-size: var(--t-small); }
.foot__links a { color: var(--slate); text-decoration: none; border-bottom: 1px solid var(--rule); padding-bottom: 2px; }
.foot__links a:hover { color: var(--ink); }
.foot__note { font-size: var(--t-micro); color: var(--slate); }

/* ---------- responsive ---------- */

/* four across needs the full column — below this the thumbnails get
   too small to read as screens, so drop to two */
@media (max-width: 1080px) {
  .grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 860px) {
  /* no room to sit proud once the gutter tightens — it would overflow */
  :root { --gutter-nav: 24px; --mark-pull: 0px; }
  .featured { grid-template-columns: 1fr; gap: 34px; }
  .grid, .strip__grid { grid-template-columns: 1fr 1fr; }
  .nav__links { gap: 16px; font-size: var(--t-micro); }
  .hero { padding-block: 40px; }
  .bio { grid-template-columns: 200px 1fr; gap: 34px; }
  .case-row { grid-template-columns: 1fr; gap: 18px; padding: var(--gap-block) 0; }
  .case-meta { grid-template-columns: 1fr; gap: 4px; }
  .case-meta dd + dt { margin-top: 14px; }
  .contact { grid-template-columns: 1fr; gap: 34px; }
  /* half a phone width is far too small for a research board */
  .case-pair { grid-template-columns: 1fr; gap: 28px; }
  /* the flight path is sized in fixed pixels and would overhang a phone */
  .plane-stage { display: none; }
  .form__row { grid-template-columns: 1fr; gap: 0; }
  /* A full-height hero on a portrait phone shows a thin vertical slice
     of a landscape frame, which for the video pages means the title in
     the footage is cropped away. Video heroes take the frame's own
     16:9, so the whole shot is on screen; photo heroes, whose title is
     HTML, keep some height but not the entire viewport. */
  .case-hero { height: auto; aspect-ratio: 16 / 9; }
  .case-hero--image,
  .case-hero--titled { aspect-ratio: auto; height: 68svh; padding-bottom: 52px; }
  .skills { grid-template-columns: 1fr 1fr; }
  .traits { grid-template-columns: 1fr 1fr; gap: 34px; }
  /* once it wraps, space-between strands the last row — centre instead */
  .software { justify-content: center; gap: 28px 40px; }
}
/* two 132px discs and their gap no longer fit a 320px screen */
@media (max-width: 360px) {
  .traits { gap: 26px 16px; }
  .trait__disc { width: 108px; height: 108px; }
  .trait h3 { font-size: 18px; }
}
@media (max-width: 560px) {
  .dh-hero .wrap {
    min-height: auto;
    display: block;
    padding-block: 56px 42px;
  }
  .dh-hero h1 {
    max-width: none;
    font-size: clamp(38px, 12.5vw, 64px);
    line-height: .98;
  }
  .dh-hero__line--first { width: auto; }
  .dh-hero__intro {
    max-width: 32ch;
    font-size: clamp(20px, 5.7vw, 28px);
  }
  .dh-hero__rule { margin-top: 40px; }
  .dh-hero__foot { margin-top: 24px; }
  .grid, .strip__grid { grid-template-columns: 1fr; }
  /* the four links no longer fit beside the logo without wrapping mid-
     phrase, so they take a second row of their own under it */
  :root { --nav-h: 76px; } /* two rows: logo, then links */
  .nav__in { flex-wrap: wrap; padding-block: 6px 10px; row-gap: 2px; }
  .nav__social { order: 2; }
  .nav__links {
    order: 3; flex: 1 1 100%;
    gap: 18px; font-size: 13px; white-space: nowrap;
  }
  /* heading and note stack, rather than the heading breaking in two to
     make room beside the note */
  .sec__head { flex-direction: column; align-items: flex-start; gap: 4px; }
  .metrics { gap: 30px; }
  /* logo alone carries the header — the wordmark has no room beside the links */
  .mark__name { display: none; }
  /* one column, so there is no text beside it to take a height from */
  .bio { grid-template-columns: 1fr; gap: 26px; }
  .bio__portrait { max-width: 220px; height: auto; aspect-ratio: 1 / 1; }
  .cv__row { grid-template-columns: 1fr; gap: 6px; }
  .skills { grid-template-columns: 1fr; gap: 24px; }
  /* three columns is too narrow to judge a mark on a phone — one concept
     per row instead, with its pair back to side by side */
  .radial { grid-template-columns: 1fr; gap: 32px; justify-items: center; }
  .radial__chart { max-width: 300px; width: 100%; }
  .radial__legend { width: 100%; }
  .logo-set { grid-template-columns: 1fr; gap: 26px; }
  .logo-concept__pair { grid-auto-flow: column; grid-auto-columns: 1fr; gap: 12px; }
  /* Captioned slides: no room beside the device for a column of text,
     so the caption drops under the image and the dots under that, and
     the device sits centred and unscaled. */
  .carousel__slide { display: flex; flex-direction: column; }
  .carousel__caption {
    position: static; transform: none; width: auto;
    padding: 12px 0 0;
  }
  .carousel__caption h3 { font-size: 20px; }
  .carousel__caption p { font-size: var(--t-small); }
  /* the artwork is a wide frame with the device on its right and empty
     space where the caption used to sit; cropped to the device instead,
     and taller, so the screen is readable at phone size */
  .carousel--blend .carousel__track img {
    width: auto; max-width: none; align-self: flex-start; height: 62vw;
    /* the device’s centre is 74% of the way across the artwork; this
       puts that point at the middle of the slide whatever the scale */
    margin-left: 50%; transform: translateX(-74%);
  }
  .carousel--blend .carousel__dots { position: static; margin-top: 12px; }
  /* arrows centred on the image, not on image plus caption */
  .carousel--blend .carousel__btn { top: 31vw; }
  /* the frame is short at this width, so the label and play control go
     side by side along the bottom edge, clear of the centred device */
  .proto-cover__foot {
    flex-direction: row; justify-content: space-between;
    left: 14px; right: 14px; bottom: 10px;
  }
  .proto-cover__title { font-size: var(--t-small); }
}

@media (max-width: 360px) {
  :root { --nav-h: 100px; }
  .nav__links {
    display: grid;
    grid-template-columns: max-content max-content;
    gap: 2px 18px;
    white-space: normal;
  }
}

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

/* ---------- homepage, dark-hero edition ---------- */

/* Two colours join the palette:
     #1D2B2B  deep   hero band, buttons, caption bars
     #CCE0A1  lime   the accent line in the hero, nothing else yet */

:root { --deep: #1D2B2B; --lime: #CCE0A1; }

body.home { background: var(--ground); }   /* no gradient pools here */

/* ---------- hero ---------- */

.dh-hero {
  position: relative; isolation: isolate;
  min-height: calc(100vh - var(--nav-h));
  background: var(--deep); color: var(--ground);
}
.dh-hero::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(circle at 88% 20%, rgb(204 224 161 / .14), transparent 24%),
    radial-gradient(circle at 16% 80%, rgb(204 224 161 / .1), transparent 28%);
  pointer-events: none;
}
.dh-hero .wrap {
  position: relative; z-index: 1;
  min-height: calc(100vh - var(--nav-h));
  display: grid; grid-template-rows: 1fr auto auto;
  padding-block: 42px;
}
.dh-hero__message { align-self: center; }
.dh-hero__top {
  display: flex; justify-content: space-between; gap: 20px;
  font-size: var(--t-small); letter-spacing: .01em;
  color: rgb(242 241 239 / .72);
}
.dh-hero__top p { margin: 0; }
.dh-hero h1 {
  margin: 0; max-width: 14ch;
  font-size: clamp(52px, 8.4vw, 118px); font-weight: 500;
  letter-spacing: -.04em; line-height: .96;
  color: var(--ground);
}
.dh-hero__line {
  display: block;
}
/* Words, not letters, are what wraps: on a phone "design." drops to its
   own line instead of the whole line running off the screen. */
.dh-hero__line--first {
  display: flex; flex-wrap: wrap; align-items: baseline;
  column-gap: .24em;
  color: var(--ground);
}
.dh-hero__word { display: inline-flex; white-space: nowrap; }
.dh-hero__letter {
  display: inline-block;
  opacity: 0;
  transform: translateY(-38px) rotate(-8deg);
  animation: dh-head-letter-drop 2.5s cubic-bezier(.2,.8,.2,1) forwards;
}
.dh-hero__letter--space {
  width: .24em;
}
.dh-hero__letter:nth-child(1) { animation-delay: .04s; }
.dh-hero__letter:nth-child(2) { animation-delay: .10s; }
.dh-hero__letter:nth-child(3) { animation-delay: .16s; }
.dh-hero__letter:nth-child(4) { animation-delay: .22s; }
.dh-hero__letter:nth-child(5) { animation-delay: .28s; }
.dh-hero__letter:nth-child(6) { animation-delay: .34s; }
.dh-hero__letter:nth-child(7) { animation-delay: .40s; }
.dh-hero__letter:nth-child(8) { animation-delay: .46s; }
.dh-hero__letter:nth-child(9) { animation-delay: .52s; }
.dh-hero__letter:nth-child(10) { animation-delay: .58s; }
.dh-hero__letter:nth-child(11) { animation-delay: .64s; }
.dh-hero__letter:nth-child(12) { animation-delay: .70s; }
.dh-hero__letter:nth-child(13) { animation-delay: .76s; }
.dh-hero__letter:nth-child(14) { animation-delay: .82s; }
.dh-hero__letter:nth-child(15) { animation-delay: .88s; }
.dh-hero__letter:nth-child(16) { animation-delay: .94s; }
.dh-hero__letter:nth-child(17) { animation-delay: 1.00s; }
.dh-hero__letter:nth-child(18) { animation-delay: 1.06s; }
/* hero letters sit inside words, so nth-child restarts per word; each
   carries its position in the whole line as --i instead */
.dh-hero__word .dh-hero__letter { animation-delay: calc(.04s + var(--i) * .06s); }
.dh-hero__line--second {
  margin-top: 16px;
  color: var(--lime);
  opacity: 0;
  animation: dh-word-rise .8s cubic-bezier(.2,.75,.2,1) 2.1s forwards;
}

.dh-hero__intro {
  max-width: 31ch;
  margin: 32px 0 0;
  color: rgb(242 241 239 / .88);
  font-size: clamp(23px, 3vw, 36px);
  line-height: 1.18;
  letter-spacing: -.025em;
  opacity: 0;
  transform: translateY(14px);
  animation: dh-rise 1.05s cubic-bezier(.22,.68,.24,1) 3s forwards;
}

.dh-hero__rule { height: 1px; background: rgb(242 241 239 / .28); margin-top: 28px; }
.dh-hero__foot {
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: 40px; margin-top: 28px;
}
.dh-hero__cta {
  display: inline-flex; align-items: center; gap: 16px; flex: none;
  color: var(--ground); text-decoration: none; font-size: var(--t-body);
}
.dh-hero__cta span {
  display: grid; place-items: center; width: 44px; height: 44px;
  border: 1px solid rgb(242 241 239 / .5); border-radius: 50%;
  transition: transform .2s ease, border-color .2s ease, background .2s ease;
}
.dh-hero__cta svg { width: 16px; height: 16px; }
.dh-hero__cta:hover span {
  transform: translateY(-1px);
  border-color: var(--ground); background: rgb(242 241 239 / .08);
}

@keyframes dh-head-letter-drop {
  0% {
    opacity: 0;
    transform: translateY(-42px) rotate(-9deg);
    filter: blur(2px);
  }
  26% {
    opacity: 1;
    transform: translateY(12px) rotate(2deg);
    filter: blur(0);
  }
  48% {
    transform: translateY(-5px) rotate(-1deg);
  }
  70% {
    transform: translateY(2px) rotate(.6deg);
  }
  100% {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}
@keyframes dh-word-rise {
  0% {
    opacity: 0;
    transform: translateY(18px);
  }
  100% {
    opacity: 1;
    transform: none;
  }
}
@keyframes dh-rise {
  from {
    opacity: 0; transform: translateY(14px);
  }
  to {
    opacity: 1; transform: none;
  }
}
@keyframes dh-circle {
  0% {
    opacity: 0;
    transform: scale(.48) rotate(-28deg);
  }
  100% {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }
}
@keyframes dh-ring {
  0% {
    opacity: 0; transform: scale(.7);
  }
  65% {
    opacity: .9; transform: scale(1.02);
  }
  100% {
    opacity: 1; transform: scale(1);
  }
}
@keyframes dh-glow {
  0% { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes dh-text {
  from {
    opacity: 0; transform: translateY(8px);
  }
  to {
    opacity: 1; transform: none;
  }
}

/* ---------- section heads ---------- */

.dh-sec { padding-block: 0 var(--gap-section); }
.dh-sec--first { padding-block: 60px; }
.dh-sec--divider {
  position: relative; margin-top: 0; padding-top: 60px;
  border-top: 1px solid var(--rule);
  background: transparent;
}
.dh-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 20px; margin-bottom: 40px;
}
.dh-head--product { margin-bottom: 40px; }
.dh-head h2 {
  margin: 0; font-size: clamp(28px, 2.9vw, 38px); font-weight: 500;
  letter-spacing: -.03em; line-height: 1.06;
}
.dh-head--animated { overflow: visible; }
.about-title {
  margin: 0 0 40px;
  font-size: clamp(52px, 8.4vw, 118px);
  font-weight: 500; letter-spacing: -.04em; line-height: .96;
  color: var(--deep);
  -webkit-text-fill-color: var(--deep);
}
.about-title .dh-head__letters { display: inline-flex; }
.about-title .dh-head__letter { -webkit-text-fill-color: var(--deep); }
.sec--first:has(.about-title) { padding-block-start: 62px; }
@media (prefers-reduced-motion: no-preference) {
  .about-bio {
    opacity: 0;
    transform: translateY(18px);
    animation: dh-word-rise .8s cubic-bezier(.2,.75,.2,1) 2.1s forwards;
  }
}
.dh-sec--first .dh-head { margin-bottom: 60px; }
.dh-head__letters { display: inline-flex; }
/* headings split into words wrap between them, so a long one breaks
   onto a second line on a phone instead of running off the screen */
.dh-head__letters:has(> .dh-head__word) { flex-wrap: wrap; column-gap: .24em; }
.dh-head__word { display: inline-flex; white-space: nowrap; }
.dh-head__letter--space { width: .24em; }
.dh-head p {
  margin: 0; font-size: var(--t-small); color: var(--slate);
  letter-spacing: .01em;
}
.dh-eyebrow {
  margin: 0 0 14px; font-size: var(--t-small); color: var(--slate);
  letter-spacing: .01em;
}

@media (prefers-reduced-motion: no-preference) {
  .dh-head--animated .dh-head__letter {
    display: inline-block;
    opacity: 0;
    transform: translateY(-38px) rotate(-8deg);
    animation: none;
  }
  .dh-head--animated.is-visible .dh-head__letter {
    animation: dh-head-letter-drop 2.5s cubic-bezier(.2,.8,.2,1) forwards;
  }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(1) { animation-delay: .04s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(2) { animation-delay: .10s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(3) { animation-delay: .16s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(4) { animation-delay: .22s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(5) { animation-delay: .28s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(6) { animation-delay: .34s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(7) { animation-delay: .40s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(8) { animation-delay: .46s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(9) { animation-delay: .52s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(10) { animation-delay: .58s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(11) { animation-delay: .64s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(12) { animation-delay: .70s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(13) { animation-delay: .76s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(14) { animation-delay: .82s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(15) { animation-delay: .88s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(16) { animation-delay: .94s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(17) { animation-delay: 1.00s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(18) { animation-delay: 1.06s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(19) { animation-delay: 1.12s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(20) { animation-delay: 1.18s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(21) { animation-delay: 1.24s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(22) { animation-delay: 1.30s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(23) { animation-delay: 1.36s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(24) { animation-delay: 1.42s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(25) { animation-delay: 1.48s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(26) { animation-delay: 1.54s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(27) { animation-delay: 1.60s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(28) { animation-delay: 1.66s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(29) { animation-delay: 1.72s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(30) { animation-delay: 1.78s; }
  .dh-head--animated.is-visible .dh-head__letter:nth-child(31) { animation-delay: 1.84s; }
  /* letters grouped into words restart nth-child per word; --i carries
     each letter's place in the whole heading instead */
  .dh-head--animated.is-visible .dh-head__word .dh-head__letter { animation-delay: calc(.04s + var(--i) * .06s); }
}

/* ---------- featured ---------- */

.dh-feat { display: grid; grid-template-columns: 1.15fr 1fr; gap: 42px; align-items: center; }
.dh-feat__stage {
  display: block; text-decoration: none; color: inherit;
  border-radius: 8px; overflow: hidden; background: var(--band);
  box-shadow: inset 0 0 0 1px rgb(65 63 61 / .06);
}
.dh-feat__stage img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  box-sizing: border-box;
}
.dh-feat__bar {
  display: flex; justify-content: space-between; align-items: center; gap: 16px;
  padding: 14px 18px; background: var(--deep); color: var(--ground);
  font-size: var(--t-small); font-weight: 500;
}
.dh-feat__bar svg { width: 14px; height: 14px; }
.dh-feat__meta h3 {
  margin: 0; font-size: clamp(30px, 3.4vw, 42px); font-weight: 500;
  letter-spacing: -.03em; line-height: 1.04;
}
.dh-feat__meta h3 span { display: block; }
.dh-feat__meta .role { margin: 14px 0 0; font-size: var(--t-body); font-weight: 500; }
.dh-feat__meta .desc { margin: 12px 0 0; max-width: 42ch; font-size: var(--t-small); line-height: 1.55; }
.dh-feat__meta .metrics { border-top: 1px solid var(--rule); padding-top: 22px; margin-top: 24px; }
.dh-feat__meta .more { margin-top: 26px; }

/* ---------- cards ---------- */

.dh-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 42px 28px; }
.dh-card { display: block; text-decoration: none; color: inherit; }
.dh-card__stage {
  position: relative; overflow: hidden; border-radius: 8px;
  background: var(--band); aspect-ratio: 4 / 3;
  box-shadow: inset 0 0 0 1px rgb(65 63 61 / .06);
}
.dh-card__stage img,
.dh-card__stage video {
  display: block; width: 100%; height: 100%; object-fit: cover; object-position: center;
  transition: transform .5s cubic-bezier(.22,.68,.24,1);
  filter: saturate(.96) contrast(1.04);
}
.dh-card__stage img.is-contained { object-fit: contain; padding: 6%; box-sizing: border-box; }
.dh-card:hover .dh-card__stage img,
.dh-card:hover .dh-card__stage video { transform: scale(1.03); }
.dh-card h3 {
  margin: 16px 0 0; font-size: var(--t-title); font-weight: 500; letter-spacing: -.015em;
}
.dh-card .dh-tagline { margin: 2px 0 0; font-size: var(--t-body); }
.dh-card .tags { margin: 2px 0 0; font-size: var(--t-small); color: var(--slate); }
.dh-card .dh-eyebrow { margin-bottom: 10px; }

/* the renders are 3:2, and cropping them to 4:3 takes the logo off the
   Green Sabre shot */
.dh-grid--product .dh-card__stage { aspect-ratio: 3 / 2; }

/* one card that runs across the row, text beside the image */
.dh-wide { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: 28px; align-items: center; }
.dh-wide .dh-card__stage { aspect-ratio: 4 / 3; }
.dh-wide h3 { margin-top: 0; font-size: clamp(26px, 2.8vw, 34px); letter-spacing: -.025em; }
.dh-wide .tags { margin-top: 34px; }

/* the fifth project, as a line rather than a fifth tile */
.dh-more {
  display: flex; align-items: baseline; gap: 18px;
  padding: 20px 0; margin-top: 42px;
  border-block: 1px solid var(--rule);
  text-decoration: none; color: inherit;
}
.dh-more .dh-more__label { font-size: var(--t-small); color: var(--slate); }
.dh-more .dh-more__title { font-size: var(--t-body); font-weight: 500; }
.dh-more .dh-more__meta { margin-left: auto; font-size: var(--t-small); color: var(--slate); }
.dh-more:hover .dh-more__title { text-decoration: underline; text-underline-offset: 3px; }

/* ---------- footer ---------- */

.dh-foot {
  position: relative; isolation: isolate;
  overflow: hidden;
  background: var(--deep); color: var(--ground);
  border-top: 1px solid rgb(242 241 239 / .18);
}
.dh-foot::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background:
    radial-gradient(circle at 88% 20%, rgb(204 224 161 / .14), transparent 24%),
    radial-gradient(circle at 16% 80%, rgb(204 224 161 / .1), transparent 28%);
  pointer-events: none;
}
.dh-foot .wrap { padding-block: 40px 56px; }
.dh-foot__ask { margin: 0; font-size: var(--t-small); color: rgb(242 241 239 / .78); }
.dh-foot__title {
  display: flex; justify-content: space-between; align-items: center; gap: 24px;
  margin: 10px 0 0; text-decoration: none; color: var(--ground);
  font-size: clamp(40px, 6vw, 84px); font-weight: 500; letter-spacing: -.035em; line-height: 1;
}
/* wraps between words, never inside one: on a phone "design." drops
   to the next line whole */
.dh-foot__title-text { display: flex; flex-wrap: wrap; column-gap: .24em; }
.dh-foot__word { display: inline-flex; white-space: nowrap; }
.dh-foot__letter {
  display: inline-block;
  opacity: 1;
  transform: none;
}
.dh-foot__letter--space { width: .24em; }
.dh-foot__letter:nth-child(1) { animation-delay: .04s; }
.dh-foot__letter:nth-child(2) { animation-delay: .10s; }
.dh-foot__letter:nth-child(3) { animation-delay: .16s; }
.dh-foot__letter:nth-child(4) { animation-delay: .22s; }
.dh-foot__letter:nth-child(5) { animation-delay: .28s; }
.dh-foot__letter:nth-child(6) { animation-delay: .34s; }
.dh-foot__letter:nth-child(7) { animation-delay: .40s; }
.dh-foot__letter:nth-child(8) { animation-delay: .46s; }
.dh-foot__letter:nth-child(9) { animation-delay: .52s; }
.dh-foot__letter:nth-child(10) { animation-delay: .58s; }
.dh-foot__letter:nth-child(11) { animation-delay: .64s; }
.dh-foot__letter:nth-child(12) { animation-delay: .70s; }
.dh-foot__letter:nth-child(13) { animation-delay: .76s; }
.dh-foot__letter:nth-child(14) { animation-delay: .82s; }
.dh-foot__letter:nth-child(15) { animation-delay: .88s; }
.dh-foot__letter:nth-child(16) { animation-delay: .94s; }
.dh-foot__letter:nth-child(17) { animation-delay: 1s; }
.dh-foot__letter:nth-child(18) { animation-delay: 1.06s; }
/* nth-child restarts inside each word; --i is the letter's place in the line */
.dh-foot__word .dh-foot__letter { animation-delay: calc(.04s + var(--i) * .06s); }
@media (prefers-reduced-motion: no-preference) {
  .dh-foot:not(.is-visible) .dh-foot__letter {
    opacity: 0;
    transform: translateY(-42px) rotate(-9deg);
    animation: none;
  }
  .dh-foot.is-visible .dh-foot__letter {
    animation: dh-head-letter-drop 2.5s cubic-bezier(.2,.8,.2,1) forwards;
  }
}
.dh-foot__title svg {
  width: .5em; height: .5em; flex: none;
  transition: transform .25s cubic-bezier(.22,.68,.24,1);
}
.dh-foot__title:hover svg { transform: translate(4px, -4px); }
.dh-foot__row {
  display: flex; justify-content: space-between; align-items: flex-end; gap: 30px; flex-wrap: wrap;
  margin-top: 44px;
}
.dh-foot .foot__links a,
.dh-foot .foot__note { color: rgb(242 241 239 / .82); }
.dh-foot .foot__links a { border-bottom-color: rgb(242 241 239 / .38); }
.dh-foot .foot__links a:hover { color: var(--ground); border-bottom-color: var(--ground); }

/* ---------- responsive ---------- */

@media (max-width: 860px) {
  .dh-feat, .dh-wide { grid-template-columns: 1fr; }
  .dh-hero__foot { flex-direction: column; align-items: flex-start; gap: 24px; }
}
@media (max-width: 560px) {
  .dh-grid { grid-template-columns: 1fr; }
  .dh-hero__top { flex-direction: column; gap: 4px; }
  .dh-head { flex-direction: column; align-items: flex-start; gap: 4px; }
  .dh-more { flex-wrap: wrap; }
  .dh-more .dh-more__meta { margin-left: 0; flex-basis: 100%; }
}

