/* Scroll progress, 0 to 1. Registered so CSS can animate it directly on a
   scroll timeline; the JS fallback writes the same property. */
@property --p {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

:root {
  color-scheme: light dark;
  --surface: #ffffff;
  --surface-soft: #eeeeec;
  --surface-strong: #d7d7d4;
  --ink: #080908;
  --muted: #63645f;
  --line: rgba(8, 9, 8, 0.22);
  --accent: #254b9b;
  --scrim: rgba(10, 12, 11, 0.34);
  --header-height: 72px;
  --rail-width: clamp(176px, 14.5vw, 224px);
  --page-gutter: clamp(18px, 2.4vw, 38px);
  --max-width: 1720px;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --z-header: 20;
  --z-menu: 30;
  --z-dialog: 50;
}

html[data-theme="dark"] {
    color-scheme: dark;
    --surface: #151615;
    --surface-soft: #1e201e;
    --surface-strong: #2a2c29;
    --ink: #eeede9;
    --muted: #a7a8a2;
    --line: rgba(238, 237, 233, 0.2);
    --accent: #8ca9eb;
    --scrim: rgba(4, 5, 4, 0.46);
}

html[data-theme="light"] {
  color-scheme: light;
}

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

html {
  scroll-behavior: smooth;
  background: var(--surface);
}

body {
  margin: 0;
  min-width: 320px;
  background: var(--surface);
  color: var(--ink);
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.45;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

body.dialog-open,
body.menu-open {
  overflow: hidden;
}

button,
a {
  font: inherit;
}

button {
  color: inherit;
}

a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

img {
  display: block;
  max-width: 100%;
}

/* Every image on the site is wrapped in a <picture> so AVIF, WebP and the original can be
   offered in that order. <picture> is a format switch, not a box: without this it would
   become the grid item in .hero-media, the aspect-ratio child in .project-image and the
   positioned child in .puzzle-image, and every rule below that sizes an image would be
   measuring the wrapper instead. display: contents removes the box and leaves the <img>
   exactly where the layout already expected it. <picture> carries no ARIA role, so nothing
   is lost from the accessibility tree either. */
picture {
  display: contents;
}

figure,
h1,
h2,
h3,
p {
  margin: 0;
}

address {
  font-style: normal;
}

.skip-link {
  position: fixed;
  left: 12px;
  top: 12px;
  z-index: 100;
  padding: 10px 14px;
  background: var(--ink);
  color: var(--surface);
  transform: translateY(-160%);
}

.skip-link:focus {
  transform: translateY(0);
}

/* SC 4.1.3: hulpklasse voor live regions die wel voorgelezen maar niet getoond worden.
   Wordt gebruikt door de statusmelding bij een mislukte indexpreview. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

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

.site-header {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: var(--z-header);
  display: flex;
  flex-direction: column;
  align-items: center;
  width: var(--rail-width);
  height: 100dvh;
  padding: 18px 18px 24px;
  background: var(--surface);
  border-right: 1px solid var(--line);
  transition: background 280ms var(--ease);
}

.site-header.is-scrolled {
  background: color-mix(in srgb, var(--surface) 96%, transparent);
}

/* Fully opaque while the menu is open. Higher specificity than the translucent mobile
   header rule, and placed after .is-scrolled so it wins on source order too. */
.site-header.is-menu-open {
  background: var(--surface);
}

.brand,
.dialog-brand {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
}

.site-header .brand {
  align-self: flex-start;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

.brand-mark,
.dialog-brand img {
  width: 76px;
  height: 76px;
  object-fit: contain;
  mix-blend-mode: multiply;
}

/* The mark is a B that turns into an S and back: Broekhof and Schuit in one shape, the
   "two becoming one" idea behind the identity. It used to be a 522 KB GIF that looped for
   ever, which fails SC 2.2.2 Pause, Stop, Hide, and CSS cannot pause a GIF, so the
   reduced-motion block had no effect on it whatsoever.

   Rebuilt as an 11 KB sprite of the 31 frames that actually differ. Of the original 14.12
   seconds, ten were two dead five-second holds; what is left is the movement itself, played
   once, in 2.48 seconds. Under five seconds and not looping means 2.2.2 no longer applies,
   and the merger still reads. The url() sits in the stylesheet, so it resolves against this
   file rather than the page and works from /work/<slug>/ without any rewriting. */
.brand-mark {
  display: block;
  background-image: url("assets/logo-sprite.webp");
  background-repeat: no-repeat;
  background-size: 3100% 100%;
  background-position: 0% center;
  animation: brand-mark-play 2480ms steps(1, end) 1 both;
}

/* Replaying on hover is deliberately not done here. Declaring the same animation again on
   :hover attaches nothing new, because the animation is already on the element and has
   finished; the rule looks like it works and does nothing at all. site.js rewinds it through
   the Web Animations API instead. */

@keyframes brand-mark-play {
  0% { background-position-x: 0%; }
  20.968% { background-position-x: 3.3333%; }
  22.581% { background-position-x: 6.6667%; }
  24.194% { background-position-x: 10%; }
  25.806% { background-position-x: 13.3333%; }
  27.419% { background-position-x: 16.6667%; }
  29.032% { background-position-x: 20%; }
  30.645% { background-position-x: 23.3333%; }
  32.258% { background-position-x: 26.6667%; }
  33.871% { background-position-x: 30%; }
  35.484% { background-position-x: 33.3333%; }
  37.097% { background-position-x: 36.6667%; }
  38.71% { background-position-x: 40%; }
  40.323% { background-position-x: 43.3333%; }
  41.935% { background-position-x: 46.6667%; }
  43.548% { background-position-x: 50%; }
  45.161% { background-position-x: 53.3333%; }
  46.774% { background-position-x: 56.6667%; }
  48.387% { background-position-x: 60%; }
  50% { background-position-x: 63.3333%; }
  51.613% { background-position-x: 66.6667%; }
  53.226% { background-position-x: 70%; }
  54.839% { background-position-x: 73.3333%; }
  87.097% { background-position-x: 76.6667%; }
  88.71% { background-position-x: 80%; }
  90.323% { background-position-x: 83.3333%; }
  91.935% { background-position-x: 86.6667%; }
  93.548% { background-position-x: 90%; }
  95.161% { background-position-x: 93.3333%; }
  96.774% { background-position-x: 96.6667%; }
  98.387% { background-position-x: 100%; }
  100% { background-position-x: 100%; }
}

.brand-name {
  display: block;
  color: var(--ink);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.04;
  letter-spacing: -0.02em;
}

.mobile-menu a,
.footer-links a {
  text-decoration: none;
}

.rail-utility {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  width: 100%;
  margin-top: auto;
  color: var(--muted);
  font-size: 12px;
}

.rail-utility a {
  text-decoration: none;
}

.theme-toggle {
  padding: 0;
  background: transparent;
  border: 0;
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 4px;
}

.menu-toggle {
  position: relative;
  z-index: calc(var(--z-menu) + 1);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  align-self: flex-start;
  margin-top: 28px;
  padding: 6px 0;
  background: transparent;
  border: 0;
  cursor: pointer;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.09em;
  line-height: 1;
  text-transform: uppercase;
}

/* The primary navigation control, and at 55x33 on a phone it was the smallest button on the
   screen. It clears the 2.5.8 spacing exception, so this is comfort rather than conformance:
   Apple asks 44 and Material 48. Extending the hit area rather than padding the button keeps
   the uppercase label sitting exactly where the rail alignment puts it. */
.menu-toggle::before {
  content: "";
  position: absolute;
  inset: -8px -14px;
}

.menu-toggle-symbol {
  display: inline-block;
  font-size: 18px;
  font-weight: 400;
  line-height: 0.7;
  transform-origin: center;
  transition: transform 420ms var(--ease);
}

.menu-toggle:hover .menu-toggle-symbol {
  transform: scale(1.18);
}

.menu-toggle[aria-expanded="true"] .menu-toggle-symbol {
  transform: rotate(45deg);
}

.menu-toggle[aria-expanded="true"]:hover .menu-toggle-symbol {
  transform: rotate(45deg) scale(1.18);
}

.site-menu {
  position: fixed;
  z-index: var(--z-menu);
  inset: 0 0 0 var(--rail-width);
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  overflow: hidden;
  padding: clamp(22px, 2.8vw, 46px) var(--page-gutter) clamp(24px, 3vw, 48px);
  background: var(--ink);
  color: var(--surface);
  opacity: 0;
  pointer-events: none;
  transform: translateX(28px);
  transition:
    opacity 420ms var(--ease),
    transform 520ms var(--ease);
}

/* SC 1.4.11. De overlay keert surface en ink om, maar --accent draaide niet mee: de
   focusring haalde 2,43:1 in light en 1,99:1 in dark, tegen een eis van 3:1. Binnen het
   menu geldt daarom het accent van het andere thema: 8,54:1 respectievelijk 7,01:1.
   --accent wordt binnen de overlay nergens anders gebruikt dan voor de focusring. */
.site-menu {
  --accent: #8ca9eb;
}

html[data-theme="dark"] .site-menu {
  --accent: #254b9b;
}

.site-menu[hidden] {
  display: none;
}

.site-header.is-menu-open .site-menu {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(0);
}

.menu-brand-object {
  position: absolute;
  top: 50%;
  right: -11vw;
  width: min(67vw, 1040px);
  /* The img still carries width="1080" height="1080" for CLS elsewhere; with only the CSS
     width set, the browser used that height attribute literally and stretched the square
     mark to 460x1080. height: auto restores the aspect ratio. */
  height: auto;
  max-width: none;
  opacity: 0.09;
  filter: invert(1);
  transform: translateY(-50%) rotate(-4deg);
  pointer-events: none;
}

html[data-theme="dark"] .menu-brand-object {
  filter: none;
}

/* SC 1.4.10. Deze rij is minmax(0, 1fr) in .site-menu, dus op korte viewports krimpt hij
   tot onder zijn eigen inhoud: op 320x256 bleef 29 px over voor drie links met letters van
   61,7 px, 16 procent zichtbaar, en overflow: hidden op .site-menu verhinderde scrollen.
   De navigatie scrollt nu binnen deze rij in plaats van te worden afgeknipt.
   Bewust hier en niet op .site-menu: het decoratieve .menu-brand-object is absoluut
   gepositioneerd en steekt op elk formaat onder het menu uit (gemeten 1022 tegen 900 op
   1440x900, 1115 tegen 1080 op 1920x1080). Een scroller op .site-menu zou daardoor op
   iedere schermmaat een scrollbalk krijgen, ook op desktop, en het hele menu laten
   wegschuiven. Op deze rij verschijnt er alleen een scrollbalk zodra de links echt niet
   meer passen, en .menu-meta blijft altijd tegen de onderrand staan. */
.menu-primary {
  position: relative;
  z-index: 1;
  display: grid;
  align-content: start;
  min-height: 0;
  overflow: hidden auto;
  overscroll-behavior: contain;
}

.menu-primary > a {
  display: flex;
  /* SC 1.4.12: met flex: 0 1 auto op de small hieronder zakt de meta bij de
     tekstafstandtest naar de volgende regel in plaats van 124,9 px buiten de
     overflow: hidden te verdwijnen. Bij normale tekstafstand wordt er niets afgebroken. */
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  min-width: 0;
  /* overflow: hidden maskeert de translateY-onthulling, maar zet de automatische
     minimumhoogte van dit gridelement op 0. Zonder de expliciete min-height krompen de
     links op 320x256 tot 9,7 px terwijl de letters 61,7 px hoog zijn. */
  min-height: min-content;
  overflow: hidden;
  border-bottom: 1px solid color-mix(in srgb, currentColor 34%, transparent);
  opacity: 0;
  transform: translateY(42px);
  transition:
    opacity 440ms var(--ease),
    transform 620ms var(--ease);
}

.site-header.is-menu-open .menu-primary > a {
  opacity: 1;
  transform: translateY(0);
}

.site-header.is-menu-open .menu-primary > a:nth-child(1) {
  transition-delay: 70ms;
}

.site-header.is-menu-open .menu-primary > a:nth-child(2) {
  transition-delay: 120ms;
}

.site-header.is-menu-open .menu-primary > a:nth-child(3) {
  transition-delay: 170ms;
}

.menu-primary > a > span {
  font-size: clamp(82px, 12.7vw, 222px);
  font-weight: 400;
  letter-spacing: -0.082em;
  line-height: 0.82;
  transform: translateX(-0.04em);
  transition: transform 560ms var(--ease);
}

.menu-primary > a small {
  flex: 0 1 auto;
  min-width: 0;
  padding-bottom: clamp(7px, 1.1vw, 18px);
  color: color-mix(in srgb, currentColor 65%, transparent);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  transition: color 300ms ease;
}

.menu-primary > a:hover > span,
.menu-primary > a:focus-visible > span {
  transform: translateX(0.04em);
}

.menu-primary > a:hover small,
.menu-primary > a:focus-visible small {
  color: currentColor;
}

.menu-meta {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(2, minmax(150px, 0.4fr)) minmax(150px, 1fr);
  gap: clamp(28px, 4vw, 72px);
  align-items: end;
  padding-top: clamp(22px, 3vh, 42px);
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity 420ms var(--ease) 180ms,
    transform 520ms var(--ease) 180ms;
}

.site-header.is-menu-open .menu-meta {
  opacity: 1;
  transform: translateY(0);
}

.menu-meta address,
.menu-links {
  font-size: 11px;
  line-height: 1.45;
}

.menu-meta address {
  color: color-mix(in srgb, currentColor 68%, transparent);
}

.menu-meta address strong {
  display: block;
  margin-bottom: 5px;
  color: var(--surface);
  font-weight: 500;
}

.menu-links {
  display: flex;
  justify-content: flex-end;
  gap: 26px;
}

.menu-links a,
.menu-links button {
  text-decoration: underline;
  text-underline-offset: 4px;
}

main,
.site-footer {
  margin-left: var(--rail-width);
}

/* The copy column has to hold "with space." on one line. At 1280px the old 0.72fr gave it
   361px while the line needs roughly 416px, so the H1 broke into four lines instead of two.
   The column is widened and the type scale eased slightly; both keep about 10% headroom at
   1280, 1440 and 1920. Below 900px the single-column rules take over untouched. */
.hero {
  display: grid;
  grid-template-columns: minmax(320px, 0.86fr) minmax(0, 1.14fr);
  min-height: 100dvh;
  max-width: none;
  margin: 0;
  padding: var(--page-gutter);
  gap: var(--page-gutter);
}

.hero-copy {
  align-self: end;
  padding: 0 0 clamp(18px, 5vh, 64px);
}

.kicker {
  margin-bottom: clamp(28px, 7vh, 74px);
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.hero h1 {
  max-width: 9ch;
  font-size: clamp(58px, 6.8vw, 122px);
  font-weight: 500;
  letter-spacing: -0.072em;
  line-height: 0.84;
}

.hero-intro {
  max-width: 31ch;
  margin-top: clamp(30px, 5vh, 54px);
  color: var(--muted);
  font-size: clamp(17px, 1.35vw, 22px);
  letter-spacing: -0.025em;
  line-height: 1.28;
}

.text-link {
  display: inline-block;
  margin-top: 30px;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  transition: color 220ms ease, border-color 220ms ease;
}

.text-link:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* SC 1.4.3. Het bijschrift stond wit over de foto met een scrim van 0,34. Gemeten op de
   werkelijke pixels haalde het 2,99:1 op 1920 px en 3,06:1 op 1440 px, tegen een eis van
   4,5:1. Belangrijker: dat contrast hing volledig af van welke foto er toevallig stond. Het
   bijschrift staat nu onder het beeld, zoals overal elders op deze site (.index-preview,
   .portrait, .case-media), en is daarmee beeldonafhankelijk conform. De scrim blijft als
   beelddecoratie bestaan. */
.hero-media {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  min-height: calc(100dvh - 2 * var(--page-gutter));
  background: var(--surface-soft);
}

.hero-media img {
  grid-row: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 57% center;
}

.hero-media::after {
  content: "";
  grid-row: 1;
  position: relative;
  align-self: end;
  height: 22%;
  background: linear-gradient(to top, var(--scrim), transparent);
  pointer-events: none;
}

/* .image-loading::before ligt met inset: 0 over de hele figure. Nu het bijschrift een eigen
   rij heeft, zou de shimmer dat bijschrift meebedekken. Als abspos kind van een grid mag
   dit pseudo-element in een grid-area worden geplaatst; inset: 0 rekent dan tegen die area. */
.hero-media::before {
  grid-row: 1 / 2;
}

.hero-media figcaption {
  grid-row: 2;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding-top: 12px;
  color: var(--muted);
  font-size: 12px;
  font-weight: 500;
}

.image-loading {
  position: relative;
  background: var(--surface-soft);
}

.image-loading::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(100deg, var(--surface-soft) 22%, var(--surface-strong) 42%, var(--surface-soft) 62%);
  background-size: 240% 100%;
  animation: image-shimmer 1.3s ease-in-out infinite;
  transition: opacity 500ms var(--ease), visibility 500ms var(--ease);
}

.image-loading.is-loaded::before,
.image-loading.is-error::before {
  opacity: 0;
  visibility: hidden;
}

.image-loading.is-error {
  min-height: 220px;
}

.image-loading.is-error::after {
  content: "Image unavailable";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--muted);
  font-size: 13px;
}

@keyframes image-shimmer {
  to { background-position: -240% 0; }
}

.hero-enter {
  opacity: 0;
  transform: translateY(24px);
  animation: hero-enter 900ms var(--ease) forwards;
}

.hero-enter:nth-child(2) { animation-delay: 80ms; }
.hero-enter:nth-child(3) { animation-delay: 160ms; }
.hero-enter:nth-child(4) { animation-delay: 240ms; }

@keyframes hero-enter {
  to { opacity: 1; transform: translateY(0); }
}

.manifesto {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: clamp(150px, 20vw, 330px) var(--page-gutter);
}

.manifesto h2 {
  max-width: 14ch;
  font-size: clamp(46px, 7.8vw, 136px);
  font-weight: 500;
  letter-spacing: -0.065em;
  line-height: 0.92;
}

.manifesto p {
  max-width: 49ch;
  margin-top: 54px;
  margin-left: min(22vw, 340px);
  color: var(--muted);
  font-size: clamp(19px, 1.65vw, 27px);
  letter-spacing: -0.028em;
  line-height: 1.3;
}

.project-index-section {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: clamp(100px, 12vw, 190px) var(--page-gutter);
}

.index-heading {
  max-width: 680px;
  margin-bottom: clamp(64px, 9vw, 138px);
}

.index-heading h2 {
  font-size: clamp(64px, 10vw, 168px);
  font-weight: 400;
  letter-spacing: -0.075em;
  line-height: 0.82;
}

.index-heading p {
  max-width: 40ch;
  margin-top: 26px;
  color: var(--muted);
  font-size: clamp(16px, 1.2vw, 20px);
  letter-spacing: -0.02em;
}

.index-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.08fr) minmax(330px, 0.92fr);
  align-items: start;
  gap: clamp(42px, 7vw, 120px);
}

.project-index-list {
  border-bottom: 1px solid var(--line);
}

.index-row {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 24px;
  width: 100%;
  padding: 12px 12px 13px;
  background: transparent;
  border: 0;
  border-top: 1px solid var(--line);
  color: var(--ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.index-row::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 2px 0;
  background: var(--project-color, var(--surface-soft));
  opacity: 0;
  transform: scaleX(0.985);
  transition:
    opacity 300ms ease,
    transform 500ms var(--ease);
}

.index-row:hover::before,
.index-row:focus-visible::before,
.index-row.is-current::before {
  opacity: 1;
  transform: scaleX(1);
}

.index-row span {
  font-size: clamp(24px, 2.65vw, 46px);
  font-weight: 400;
  letter-spacing: -0.055em;
  line-height: 0.98;
  transition: color 240ms ease, transform 420ms var(--ease);
}

.index-row small {
  color: var(--muted);
  font-size: 11px;
  white-space: nowrap;
  transition: color 240ms ease;
}

.index-row:hover span,
.index-row:focus-visible span,
.index-row.is-current span {
  color: #080908;
  transform: translateX(10px);
}

.index-row:hover small,
.index-row:focus-visible small,
.index-row.is-current small {
  color: #080908;
}

.index-preview {
  position: sticky;
  top: var(--page-gutter);
  padding: clamp(10px, 1.2vw, 18px);
  background: var(--preview-color, var(--surface-soft));
  transition: background-color 420ms ease;
}

.index-preview::before {
  display: none;
}

.index-preview-media {
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--surface-soft);
}

.index-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 260ms ease, transform 760ms var(--ease);
}

.index-preview img.is-switching {
  opacity: 0;
  transform: scale(1.018);
}

/* A failed preview reports inside the media frame instead of covering the caption, so the
   caption keeps naming the project the visitor is actually pointing at. */
.index-preview.is-error {
  min-height: 0;
}

.index-preview.is-error::after {
  content: none;
}

.index-preview.is-error .index-preview-media {
  position: relative;
}

.index-preview.is-error .index-preview-media::after {
  content: "Image unavailable";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--surface-soft);
  color: var(--muted);
  font-size: 13px;
}

/* SC 1.4.3. --preview-color is in beide themes een lichte pastel, dus dit bijschrift mag de
   themakleuren niet erven: in dark theme leverde dat 1,01:1 op voor de titel en 1,49:1
   voor de meta. Vaste waarden, gemeten tegen de donkerste van de vijftien projectkleuren
   (#c5d8c6): titel 12,37:1, meta 4,67:1. Zelfde aanpak als .index-row.is-current span. */
.index-preview figcaption {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  margin-top: 12px;
  color: #080908;
  font-size: 12px;
}

.index-preview figcaption span {
  color: #54554f;
  text-align: right;
}

.work-section,
.studio-section {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: clamp(100px, 12vw, 190px) var(--page-gutter);
}

.section-heading {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.section-heading h2,
.studio-intro h2,
.puzzle-copy h2 {
  font-size: clamp(44px, 6.3vw, 104px);
  font-weight: 500;
  letter-spacing: -0.062em;
  line-height: 0.95;
}

.section-heading p,
.studio-intro p,
.puzzle-copy p {
  max-width: 47ch;
  margin-top: 28px;
  color: var(--muted);
  font-size: clamp(17px, 1.25vw, 21px);
  letter-spacing: -0.02em;
}

.work-section .section-heading {
  margin-bottom: clamp(58px, 8vw, 116px);
}

.menu-toggle:active,
.dialog-close:active {
  transform: translateY(1px);
}

.project-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  align-items: start;
  gap: clamp(46px, 7vw, 110px) var(--page-gutter);
}

.project-card {
  display: block;
  width: 100%;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--ink);
  text-align: left;
  /* The cards are <a href="work/<slug>/"> since the static project pages exist, so the
     underline has to be switched off here the way .index-row already does. */
  text-decoration: none;
  cursor: pointer;
}

.project-card[hidden] {
  display: none;
}

.project-wide { grid-column: 1 / span 7; }
.project-tall { grid-column: 9 / span 4; margin-top: clamp(80px, 12vw, 190px); }
.project-landscape { grid-column: 4 / span 8; }
.project-square { grid-column: 1 / span 4; margin-top: clamp(30px, 6vw, 90px); }
.project-offset { grid-column: 6 / span 7; }
.project-forest { grid-column: 2 / span 6; margin-top: clamp(34px, 7vw, 110px); }

.project-image {
  display: block;
  overflow: hidden;
  background: var(--surface-soft);
}

.project-wide .project-image { aspect-ratio: 1.42 / 1; }
.project-tall .project-image { aspect-ratio: 0.72 / 1; }
.project-landscape .project-image { aspect-ratio: 1.7 / 1; }
.project-square .project-image { aspect-ratio: 1 / 1.08; }
.project-offset .project-image { aspect-ratio: 1.5 / 1; }
.project-forest .project-image { aspect-ratio: 1.35 / 1; }

.project-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 900ms var(--ease), filter 350ms ease;
}

.project-wide img { object-position: center 62%; }
.project-tall img { object-position: center; }

.project-card:hover .project-image img,
.project-card:focus-visible .project-image img {
  transform: scale(1.025);
}

.project-info {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  margin-top: 14px;
}

.project-info > span:first-child {
  display: grid;
  gap: 3px;
}

.project-info strong {
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.025em;
}

.project-info small,
.project-action {
  color: var(--muted);
  font-size: 12px;
}

.project-action {
  white-space: nowrap;
}

.puzzle-section {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: clamp(110px, 14vw, 220px) var(--page-gutter);
}

.puzzle-copy {
  max-width: 950px;
  margin-bottom: clamp(60px, 9vw, 140px);
}

.puzzle-copy h2 {
  max-width: 12ch;
}

/* Sketch to model. The two sources are both 1672x941 and register pixel for pixel, so they
   are stacked and given the identical crop. At rest the drawing covers the model exactly;
   scrolling slides it straight up and out of the frame, uncovering the model beneath it.
   Nothing is offset or scaled at rest, so no edge of the model is ever visible before the
   movement starts. The aspect ratio stays 16 / 8.5 on purpose: both images take the same
   crop and stay aligned at every breakpoint.

   Both the scroll-timeline path and the JS fallback drive the same --p, so the movement is
   defined once. Only transform animates, so it stays on the compositor. */
.puzzle-image {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 8.5;
  overflow: hidden;
}

.puzzle-image img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.puzzle-sketch {
  z-index: 2;
  transform: translate3d(0, calc(var(--p, 0) * -100%), 0);
}

/* Pinning. The tall wrapper supplies the scroll distance; the inner block sticks to the
   viewport so the frame stays put while that distance is consumed. The slide therefore
   plays out in full, in place, instead of drifting past. 210vh gives roughly one viewport
   of pinned scrolling, which is enough to read the movement without holding the reader
   hostage. */
.puzzle-pin {
  height: 260vh;
}

.puzzle-sticky {
  position: sticky;
  top: 0;
  display: grid;
  align-content: center;
  min-height: 100vh;
  min-height: 100dvh;
}

/* Scroll-driven where supported, so the scrub runs off the main thread. The timeline is
   declared on the tall wrapper and consumed by the figure, because the figure itself no
   longer moves. `contain` is exactly the phase during which the wrapper covers the
   viewport, so the animation maps one to one onto the pinned stretch. */
@supports (animation-timeline: view()) {
  .puzzle-pin {
    view-timeline-name: --puzzle;
    view-timeline-axis: block;
  }

  .puzzle-image {
    animation: puzzle-scrub linear both;
    animation-timeline: --puzzle;
    animation-range: contain 0% contain 100%;
  }
}

/* Three beats rather than one constant movement: the drawing is held still long enough to
   be read, then eases across, then the model is held before the pin lets go. The middle
   segment uses linear() sampled from smoothstep, u * u * (3 - 2u), so it accelerates out of
   the first hold and decelerates into the second. The JS fallback computes that same curve,
   so both paths are identical rather than merely similar. */
@keyframes puzzle-scrub {
  0% {
    --p: 0;
  }

  18% {
    --p: 0;
    animation-timing-function: linear(
      0, 0.028 10%, 0.104 20%, 0.216 30%, 0.352 40%, 0.5 50%,
      0.648 60%, 0.784 70%, 0.896 80%, 0.972 90%, 1
    );
  }

  78% {
    --p: 1;
  }

  100% {
    --p: 1;
  }
}

.studio-intro {
  max-width: 920px;
}

.studio-portraits {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--page-gutter);
  margin-top: clamp(70px, 10vw, 150px);
}

.portrait {
  overflow: visible;
}

.portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(100%);
}

.portrait-bas {
  grid-column: 1 / span 5;
}

.portrait-steven {
  grid-column: 7 / span 6;
  margin-top: clamp(80px, 10vw, 155px);
}

.portrait-bas img,
.portrait-steven img {
  aspect-ratio: 4 / 3;
}

.portrait figcaption {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin-top: 12px;
  font-size: 13px;
}

.portrait figcaption span {
  color: var(--muted);
}

.studio-biographies {
  margin-top: clamp(120px, 16vw, 250px);
  border-top: 1px solid var(--line);
}

.studio-bio {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--page-gutter);
  padding: clamp(38px, 6vw, 88px) 0 clamp(70px, 9vw, 140px);
  border-bottom: 1px solid var(--line);
}

.studio-bio > span {
  grid-column: 1;
  color: var(--muted);
  font-size: 11px;
}

.studio-bio-copy {
  display: grid;
  grid-template-columns: repeat(8, minmax(0, 1fr));
  grid-column: 3 / span 10;
  gap: var(--page-gutter);
}

.studio-bio h3 {
  grid-column: 1 / -1;
  margin-bottom: clamp(24px, 4vw, 56px);
  font-size: clamp(46px, 7vw, 112px);
  font-weight: 400;
  letter-spacing: -0.065em;
  line-height: 0.9;
}

.studio-bio-copy > p {
  grid-column: span 4;
  max-width: 47ch;
  color: var(--muted);
  font-size: clamp(16px, 1.25vw, 20px);
  text-wrap: pretty;
}

/* An attributed quote, not another body paragraph. It carries the line that used to be
   presented as the office motto, so it has to read plainly as one person speaking. Sitting
   in the left column under the first paragraph, it also fills the void that opened up there
   once the education table moved to the right. */
.studio-bio-copy > .studio-bio-quote {
  grid-column: span 4;
  align-self: start;
  margin-top: clamp(28px, 4vw, 52px);
  padding-top: 18px;
  border-top: 1px solid var(--line);
  color: var(--ink);
  font-size: clamp(18px, 1.55vw, 25px);
  letter-spacing: -0.025em;
  line-height: 1.24;
  text-wrap: pretty;
}

.education {
  grid-column: 5 / -1;
  margin-top: clamp(42px, 6vw, 82px);
  font-size: 13px;
}

.education > strong {
  display: block;
  margin-bottom: 15px;
  font-weight: 600;
}

.education p {
  display: grid;
  grid-template-columns: 48px 1fr;
  gap: 14px;
  padding: 8px 0;
  border-top: 1px solid var(--line);
}

.education p > span {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.principles {
  margin-top: clamp(130px, 17vw, 270px);
}

.principle {
  display: grid;
  grid-template-columns: 0.75fr 1.25fr;
  align-items: baseline;
  gap: var(--page-gutter);
  padding: 28px 0;
  border-top: 1px solid var(--line);
}

.principle:last-child {
  border-bottom: 1px solid var(--line);
}

/* These headings used to be single words (Challenge, Connect, Compose) and were set at up
   to 84px to match. They now carry three to five words each, so the old size wrapped them
   into towers that dwarfed the sentence beside them. Scaled back to the size the copy
   actually needs, with balanced wrapping so a stray word cannot end up alone on a line. */
.principle h3 {
  max-width: 14ch;
  font-size: clamp(26px, 2.9vw, 46px);
  font-weight: 500;
  letter-spacing: -0.045em;
  line-height: 1.02;
  text-wrap: balance;
}

.principle p {
  max-width: 38ch;
  color: var(--muted);
  font-size: clamp(16px, 1.25vw, 21px);
}

.site-footer {
  max-width: var(--max-width);
  margin: 0 0 0 var(--rail-width);
  padding: clamp(130px, 18vw, 280px) var(--page-gutter) 36px;
}

.footer-heading p {
  color: var(--muted);
  font-size: 14px;
}

.footer-heading a {
  display: inline-block;
  margin-top: 16px;
  font-size: clamp(38px, 7.4vw, 126px);
  font-weight: 500;
  letter-spacing: -0.068em;
  line-height: 0.95;
  text-decoration: none;
  overflow-wrap: anywhere;
  transition: color 220ms ease;
}

.footer-heading a:hover {
  color: var(--accent);
}

.footer-invitation {
  max-width: 42ch;
  margin-top: 28px;
  color: var(--muted);
  font-size: 16px;
}

.footer-details {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--page-gutter);
  margin-top: clamp(100px, 13vw, 190px);
  padding-top: 20px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 13px;
}

.footer-details strong {
  display: block;
  margin-bottom: 8px;
  color: var(--ink);
}

.footer-links {
  grid-column: 4;
  display: grid;
  justify-items: end;
  align-content: start;
  gap: 8px;
}

.footer-note {
  margin-top: 110px;
  color: var(--muted);
  font-size: 11px;
}

.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 760ms var(--ease), transform 760ms var(--ease);
}

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

.project-dialog {
  --case-color: var(--surface);
  --case-color-strength: 82%;
  /* SC 1.4.3: op de gemengde achtergrond zakte --muted bij happy, diyrural en ensuite naar
     4,06:1 tot 4,30:1. Lokaal iets donkerder maakt het slechtste geval 4,88:1, zonder de
     tint van de case aan te tasten. In dark theme blijft de globale waarde staan, want
     daar voldoet die al (4,75:1 slechtste geval). */
  --muted: #575852;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  background: color-mix(in srgb, var(--case-color) var(--case-color-strength), var(--surface));
  border: 0;
  color: var(--ink);
}

.project-dialog::backdrop {
  background: rgba(9, 10, 9, 0.66);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}

.dialog-shell {
  min-height: 100%;
}

.dialog-header {
  position: sticky;
  z-index: 1;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--header-height);
  /* Sticky inside a full-screen dialog, so it meets the same notch the site header does.
     --header-height already carries the top inset on mobile; the padding centres the row's
     content below it and keeps Close clear of curved corners in landscape. */
  padding: env(safe-area-inset-top, 0px) max(var(--page-gutter), env(safe-area-inset-right, 0px)) 0
    max(var(--page-gutter), env(safe-area-inset-left, 0px));
  background: color-mix(in srgb, var(--case-color) var(--case-color-strength), var(--surface));
  border-bottom: 1px solid var(--line);
  -webkit-backdrop-filter: blur(18px);
  backdrop-filter: blur(18px);
}

.dialog-brand img {
  width: 38px;
  height: 38px;
}

.dialog-brand {
  padding: 8px 10px 8px 0;
  transition: opacity 220ms ease, transform 220ms var(--ease);
}

.dialog-brand:hover {
  opacity: 0.62;
  transform: translateX(-2px);
}

.dialog-brand:active {
  transform: scale(0.96);
}

.dialog-close {
  position: relative;
  padding: 8px 0;
  background: none;
  border: 0;
  border-bottom: 1px solid currentColor;
  cursor: pointer;
  font-size: 14px;
}

/* The only way out of a full-screen overlay, and it measured 36x37. Apple asks 44 and
   Material 48. The hit area is extended past the visual bounds instead of padding the
   button, so the underline stays tight to the word and the header keeps its proportions. */
.dialog-close::before {
  content: "";
  position: absolute;
  inset: -6px -14px;
}

.dialog-content {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: clamp(72px, 10vw, 150px) var(--page-gutter);
}

.case-header {
  display: grid;
  grid-template-columns: 1fr minmax(280px, 0.5fr);
  align-items: end;
  gap: var(--page-gutter);
  margin-bottom: clamp(70px, 10vw, 150px);
}

.case-header h2 {
  max-width: 10ch;
  font-size: clamp(58px, 10vw, 170px);
  font-weight: 500;
  letter-spacing: -0.075em;
  line-height: 0.82;
}

.case-meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px;
  color: var(--muted);
  font-size: 13px;
}

.case-meta strong {
  display: block;
  margin-bottom: 5px;
  color: var(--ink);
  font-weight: 600;
}

.case-hero {
  width: 100%;
  max-height: 86dvh;
  object-fit: cover;
  background: var(--surface-soft);
}

.case-story {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--page-gutter);
  padding: clamp(80px, 12vw, 180px) 0;
}

.case-lead {
  grid-column: 2 / span 5;
  font-size: clamp(27px, 3.7vw, 58px);
  letter-spacing: -0.045em;
  line-height: 1.04;
}

.case-body {
  grid-column: 8 / span 4;
  align-self: end;
  color: var(--muted);
  font-size: clamp(16px, 1.18vw, 19px);
}

.case-facts {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--page-gutter);
  margin-bottom: clamp(110px, 16vw, 250px);
}

.case-facts p {
  display: grid;
  gap: 8px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.case-facts strong {
  font-size: clamp(54px, 7vw, 112px);
  font-weight: 400;
  letter-spacing: -0.075em;
  line-height: 0.9;
}

.case-facts span {
  max-width: 20ch;
  color: var(--muted);
  font-size: 13px;
}

.case-details {
  margin-bottom: clamp(100px, 15vw, 230px);
  border-top: 1px solid var(--line);
}

.case-detail {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--page-gutter);
  padding: clamp(28px, 4vw, 58px) 0;
  border-bottom: 1px solid var(--line);
}

.case-detail > span {
  grid-column: 1;
  color: var(--muted);
  font-size: 11px;
}

.case-detail h3 {
  grid-column: 2 / span 5;
  max-width: 12ch;
  font-size: clamp(30px, 4vw, 62px);
  font-weight: 400;
  letter-spacing: -0.055em;
  line-height: 0.96;
}

.case-detail p {
  grid-column: 8 / span 4;
  color: var(--muted);
  font-size: clamp(15px, 1.12vw, 18px);
}

.case-credits {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--page-gutter);
  margin-top: clamp(100px, 14vw, 210px);
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

.case-credits h3 {
  grid-column: 1 / span 3;
  font-size: 14px;
  font-weight: 600;
}

.case-credits dl {
  grid-column: 6 / span 7;
  margin: 0;
}

.case-credits dl > div {
  display: grid;
  grid-template-columns: minmax(150px, 0.55fr) 1fr;
  gap: 24px;
  padding: 8px 0;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
}

.case-credits dt {
  color: var(--muted);
}

.case-credits dd {
  margin: 0;
}

.case-chapter {
  padding: clamp(90px, 13vw, 210px) 0;
  border-top: 1px solid var(--line);
}

.case-chapter-copy {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  align-items: start;
  gap: var(--page-gutter);
  margin-bottom: clamp(64px, 9vw, 140px);
}

.case-chapter-copy > span {
  grid-column: 1;
  color: var(--muted);
  font-size: 12px;
}

.case-chapter-copy h3 {
  grid-column: 2 / span 5;
  max-width: 10ch;
  font-size: clamp(42px, 6.4vw, 108px);
  font-weight: 400;
  letter-spacing: -0.065em;
  line-height: 0.92;
}

.case-chapter-copy p {
  grid-column: 9 / span 4;
  align-self: end;
  color: var(--muted);
  font-size: clamp(16px, 1.16vw, 19px);
}

.case-media-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  align-items: start;
  gap: var(--page-gutter);
}

.case-media img {
  width: 100%;
  height: auto;
  background: var(--surface-soft);
}

.case-media figcaption {
  margin-top: 10px;
  color: var(--muted);
  font-size: 11px;
}

.case-media-drawing {
  padding: clamp(18px, 2.5vw, 42px);
  background: #f0f0ed;
}

.case-media-drawing img {
  background: transparent;
  object-fit: contain;
}

.case-media-drawing figcaption {
  color: #5f605c;
}

/* SC 1.4.10. Deze plaatsingen horen bij het twaalfkoloms raster van .case-media-grid en
   gelden dus alleen zolang dat raster bestaat. Ze stonden eerder ongebonden in de cascade
   en zijn (0,3,0), even sterk als de mobiele reset in de max-width: 900px-query. Die reset
   wint nu alleen omdat hij verderop in het bestand staat: verschuif een van beide blokken
   en het raster maakt op 320 px weer elf impliciete kolommen (612 px inhoud in een viewport
   van 320 px). Door de plaatsingen bestaansafhankelijk te maken van de breedte kan die
   volgorde er niet meer toe doen. */
@media (min-width: 901px) {
  .case-layout-duo .case-media:first-child {
    grid-column: 1 / span 6;
  }

  .case-layout-duo .case-media:nth-child(2) {
    grid-column: 8 / span 5;
    margin-top: 10vw;
  }

  .case-layout-editorial .case-media:first-child {
    grid-column: 1 / span 4;
  }

  .case-layout-editorial .case-media:nth-child(2) {
    grid-column: 6 / span 7;
    margin-top: 6vw;
  }

  .case-layout-editorial .case-media:nth-child(3) {
    grid-column: 7 / span 5;
    margin-top: 4vw;
  }

  .case-layout-technical .case-media:first-child {
    grid-column: 1 / span 4;
  }

  .case-layout-technical .case-media:nth-child(2) {
    grid-column: 5 / span 4;
    margin-top: 8vw;
  }

  .case-layout-technical .case-media:nth-child(3) {
    grid-column: 9 / span 4;
    margin-top: 3vw;
  }

  .case-layout-colour .case-media:first-child {
    grid-column: 1 / span 8;
  }

  .case-layout-colour .case-media:nth-child(2) {
    grid-column: 9 / span 4;
    margin-top: 9vw;
  }

  .case-layout-colour .case-media:nth-child(3) {
    grid-column: 2 / span 4;
    margin-top: 4vw;
  }

  .case-layout-colour .case-media:nth-child(4) {
    grid-column: 6 / span 7;
    margin-top: 13vw;
  }
}

.case-gallery {
  display: grid;
  gap: var(--page-gutter);
}

.case-gallery.two {
  grid-template-columns: 0.78fr 1.22fr;
  align-items: start;
}

.case-gallery img {
  width: 100%;
  max-height: 90dvh;
  object-fit: cover;
  background: var(--surface-soft);
}

/* Was .case-gallery.two img:first-child. Each gallery item is a <picture> now, so every
   image is the first child of its own wrapper and the offset landed on both of them. The
   first selector targets the image inside the first grid item; the second keeps a bare
   <img> working for an image that has no derivatives and is emitted without a wrapper. */
.case-gallery.two > :first-child img,
.case-gallery.two > img:first-child {
  margin-top: 12vw;
}

.case-source {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  margin-top: clamp(90px, 12vw, 170px);
  padding-top: 20px;
  border-top: 1px solid var(--line);
  font-size: 14px;
}

.case-source a {
  font-weight: 600;
}

html[data-theme="dark"] .brand-mark,
html[data-theme="dark"] .dialog-brand img {
  filter: invert(1);
  mix-blend-mode: screen;
}

html[data-theme="dark"] .project-dialog {
  --case-color-strength: 16%;
  --muted: #a7a8a2;
}

html[data-theme="dark"] .project-image img,
/* Was .hero-media > img:first-child. The hero image is now the child of a <picture>, so the
   direct-child combinator no longer reached it. .hero-media holds exactly one image. */
html[data-theme="dark"] .hero-media img,
html[data-theme="dark"] .case-gallery img,
html[data-theme="dark"] .case-hero {
  filter: brightness(0.92);
}

@media (max-width: 900px) {
  /* The top inset lives inside the variable, so everything that positions itself against
     the header (the menu overlay, the hero padding) moves along with it for free. On a
     device without cutouts env() is 0px and this is exactly the old 64px. */
  :root {
    --header-height: calc(64px + env(safe-area-inset-top, 0px));
    --page-gutter: 18px;
  }

  /* Safe areas. With viewport-fit=cover the page draws into the notch and the gesture bar,
     so the fixed bar has to push its own content clear of them. The height grows by the top
     inset rather than being replaced by it, and the horizontal padding takes whichever is
     larger: the design gutter or the inset. In portrait every env() here is 0 and the
     numbers are identical to before; the change only bites in phone landscape, where the
     brand and the Index button used to sit partly behind the notch. */
  .site-header {
    inset: 0 0 auto;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    width: auto;
    height: var(--header-height);
    padding: env(safe-area-inset-top, 0px) max(var(--page-gutter), env(safe-area-inset-left, 0px)) 0
      max(var(--page-gutter), env(safe-area-inset-right, 0px));
    background: color-mix(in srgb, var(--surface) 94%, transparent);
    border-right: 0;
    border-bottom: 1px solid var(--line);
    -webkit-backdrop-filter: blur(18px);
    backdrop-filter: blur(18px);
  }

  .site-header.is-menu-open {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  .site-header .brand {
    align-self: auto;
    flex-direction: row;
    align-items: center;
    gap: 10px;
  }

  .brand-mark {
    width: 44px;
    height: 44px;
  }

  .rail-utility {
    display: none;
  }

  main,
  .site-footer {
    margin-left: 0;
  }

  .menu-toggle {
    align-self: auto;
    margin-top: 0;
    padding: 10px 0;
    font-size: 10px;
  }

  .site-menu {
    inset: var(--header-height) 0 0;
    grid-template-rows: minmax(0, 1fr) auto;
    /* The bottom edge holds the addresses and the theme toggle, exactly where the gesture
       bar sits on modern phones; the side maxes keep the huge links clear of curved edges
       in landscape. */
    padding: 20px max(var(--page-gutter), env(safe-area-inset-right, 0px))
      calc(24px + env(safe-area-inset-bottom, 0px))
      max(var(--page-gutter), env(safe-area-inset-left, 0px));
  }

  .menu-brand-object {
    top: 43%;
    right: -34vw;
    width: 118vw;
    opacity: 0.08;
  }

  /* safe: zodra de links niet meer passen, centreert deze rij niet langer maar lijnt hij
     uit op de scroll-oorsprong. Zonder het trefwoord staat de eerste link boven het
     scrolbereik en is hij onbereikbaar. */
  .menu-primary {
    align-content: safe center;
  }

  .menu-primary > a {
    align-items: baseline;
  }

  /* Sized by viewport width alone, the three links needed 198px of a 148px row in phone
     landscape, so the primary navigation had to be scrolled through. The min() adds a
     height ceiling that only engages when the viewport is short: in portrait 20.5vw is
     always the smaller of the two, so nothing about the upright layout changes. */
  .menu-primary > a > span {
    font-size: clamp(40px, min(20.5vw, 13vh), 104px);
    line-height: 0.94;
  }

  .menu-primary > a small {
    padding-bottom: 5px;
    font-size: 9px;
  }

  .menu-meta {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px;
    padding-top: 22px;
  }

  .menu-links {
    grid-column: 1 / -1;
    justify-content: space-between;
    padding-top: 10px;
  }

  .hero {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(54dvh, 1fr);
    min-height: 100dvh;
    padding-top: calc(var(--header-height) + 30px);
  }

  .hero-copy {
    padding: 0;
  }

  .kicker {
    margin-bottom: 30px;
  }

  .hero h1 {
    max-width: none;
    font-size: clamp(56px, 18vw, 112px);
  }

  .hero-intro {
    max-width: 34ch;
    margin-top: 22px;
    font-size: 16px;
  }

  .text-link {
    margin-top: 18px;
  }

  .hero-media {
    min-height: 54dvh;
  }

  .manifesto {
    padding-top: 150px;
    padding-bottom: 150px;
  }

  .manifesto h2 {
    font-size: clamp(48px, 13vw, 82px);
  }

  .manifesto p {
    margin-top: 38px;
    margin-left: 13vw;
    font-size: 18px;
  }

  .work-section,
  .studio-section,
  .puzzle-section,
  .project-index-section {
    padding-top: 90px;
    padding-bottom: 90px;
  }

  .index-layout {
    grid-template-columns: 1fr;
    gap: 44px;
  }

  .index-preview {
    position: relative;
    top: auto;
    grid-row: 1;
  }

  .index-preview-media {
    aspect-ratio: 1.22 / 1;
  }

  .index-row span {
    font-size: clamp(24px, 7.8vw, 38px);
  }

  .index-row small {
    font-size: 10px;
  }

  .project-grid {
    grid-template-columns: 1fr;
    gap: 64px;
  }

  .project-wide,
  .project-tall,
  .project-landscape,
  .project-square,
  .project-offset,
  .project-forest {
    grid-column: 1;
    margin-top: 0;
  }

  .project-wide .project-image,
  .project-tall .project-image,
  .project-landscape .project-image,
  .project-square .project-image,
  .project-offset .project-image,
  .project-forest .project-image {
    aspect-ratio: 1.25 / 1;
  }

  .project-tall .project-image {
    aspect-ratio: 0.86 / 1;
    width: 76%;
    margin-left: 24%;
  }

  .puzzle-image {
    aspect-ratio: 1 / 0.82;
  }

  .studio-portraits {
    grid-template-columns: 1fr;
    gap: 66px;
  }

  .portrait-bas,
  .portrait-steven {
    grid-column: 1;
    margin-top: 0;
  }

  .portrait-bas {
    width: 78%;
  }

  .portrait-steven {
    width: 82%;
    margin-left: 18%;
  }

  .studio-biographies {
    margin-top: 100px;
  }

  .studio-bio {
    grid-template-columns: 1fr;
    gap: 18px;
    padding: 34px 0 76px;
  }

  .studio-bio > span {
    grid-column: 1;
  }

  .studio-bio-copy {
    grid-template-columns: 1fr;
    grid-column: 1;
  }

  .studio-bio h3,
  .studio-bio-copy > p,
  .education {
    grid-column: 1;
  }

  .studio-bio-copy > p + p {
    margin-top: 18px;
  }

  .education {
    margin-top: 44px;
  }

  .principle {
    grid-template-columns: 1fr;
    gap: 8px;
    padding: 24px 0;
  }

  .footer-details {
    grid-template-columns: repeat(2, 1fr);
    gap: 34px 18px;
  }

  .footer-links {
    grid-column: 2;
    justify-items: start;
  }

  .case-header {
    grid-template-columns: 1fr;
    align-items: start;
  }

  .case-story {
    grid-template-columns: 1fr;
    gap: 38px;
  }

  .case-lead,
  .case-body {
    grid-column: 1;
  }

  .case-detail {
    grid-template-columns: 1fr;
    gap: 16px;
  }

  .case-detail > span,
  .case-detail h3,
  .case-detail p {
    grid-column: 1;
  }

  .case-detail p {
    max-width: 42ch;
    margin-top: 12px;
  }

  .case-credits {
    grid-template-columns: 1fr;
    gap: 30px;
  }

  .case-credits h3,
  .case-credits dl {
    grid-column: 1;
  }

  .case-credits dl > div {
    grid-template-columns: minmax(120px, 0.65fr) 1fr;
    gap: 16px;
  }

  .case-chapter-copy {
    grid-template-columns: 1fr;
    gap: 20px;
  }

  .case-chapter-copy > span,
  .case-chapter-copy h3,
  .case-chapter-copy p {
    grid-column: 1;
  }

  .case-chapter-copy p {
    max-width: 42ch;
    margin-top: 18px;
  }

  .case-media-grid {
    grid-template-columns: 1fr;
    gap: 54px;
  }

  /* The .case-layout-* spans are (0,3,0) and outranked this reset, so every case chapter
     kept its 12-column spans on mobile and scrolled sideways inside the dialog. Matching
     their specificity puts the media back into a single column. */
  .case-media-grid .case-media,
  .case-media-grid .case-media:first-child,
  .case-media-grid .case-media:nth-child(2),
  .case-media-grid .case-media:nth-child(3),
  .case-media-grid .case-media:nth-child(4) {
    grid-column: 1;
    margin-top: 0;
  }

  .case-gallery.two {
    grid-template-columns: 1fr;
  }

  .case-gallery.two > :first-child img,
  .case-gallery.two > img:first-child {
    margin-top: 0;
  }
}

@media (max-width: 540px) {
  .brand-mark {
    width: 38px;
    height: 38px;
  }

  .brand-name {
    font-size: 10px;
  }

  .index-row {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .index-row small {
    justify-self: start;
  }

  .hero {
    grid-template-rows: auto minmax(48dvh, 1fr);
  }

  .hero h1 {
    font-size: clamp(52px, 17vw, 78px);
  }

  .hero-media {
    min-height: 48dvh;
  }

  .hero-media figcaption {
    padding-top: 10px;
  }

  .section-heading h2,
  .studio-intro h2,
  .puzzle-copy h2 {
    font-size: 48px;
  }

  .manifesto p {
    margin-left: 0;
  }

  .project-info {
    align-items: flex-start;
  }

  .project-action {
    display: none;
  }

  .footer-heading a {
    font-size: clamp(32px, 10.5vw, 50px);
  }

  .footer-details {
    grid-template-columns: 1fr;
  }

  .footer-links {
    grid-column: 1;
  }

  /* SC 1.4.10, buiten de patch-spec gevonden. De spec mat alleen The Bow Tie, die past.
     "Frankenstein" en "Conservatory" passen niet: .case-header is hier een 1fr-kolom, dus
     de min-content van dat ene woord zet de bodem onder het hele raster en trok ook
     .case-meta mee. Gemeten op 320 px: Conservatory Room 340 tegen 320, Happy Frankenstein
     326 tegen 320. Boven 414 px trad het niet op, boven 480 px verandert deze regel niets.
     overflow-wrap: anywhere verlaagt wel de min-content-breedte (break-word niet) en is de
     behandeling die .footer-heading a op deze site al gebruikt. */
  .case-header h2 {
    font-size: clamp(54px, 19vw, 86px);
    overflow-wrap: anywhere;
  }

  .case-meta {
    grid-template-columns: 1fr;
    gap: 14px;
  }

  .case-facts {
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
  }

  .case-facts strong {
    font-size: 48px;
  }

  .case-facts span {
    font-size: 11px;
  }

  .case-source {
    flex-direction: column;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }

  .reveal,
  .hero-enter {
    opacity: 1;
    transform: none;
  }

  /* Explicit rather than relying on the blanket 0.01ms above: the mark rests on the B and
     never moves, on load or on hover. This is the thing a GIF could not be told to do. */
  .brand-mark,
  .brand:hover .brand-mark,
  .brand:focus-visible .brand-mark {
    animation: none !important;
    background-position-x: 0%;
  }

  /* No transition at all: the model is simply there. The pin collapses too, otherwise the
     reader is left scrolling through two viewports of nothing. */
  .puzzle-sketch {
    display: none;
  }

  .puzzle-pin {
    height: auto;
  }

  .puzzle-sticky {
    position: static;
    min-height: 0;
  }
}
