/* PP Telegraf — the heavy grotesk used for the homepage hero headline */
@font-face{
  font-family:'Telegraf';
  src:url("assets/font%20/telegraf-cdnfonts/Telegraf%20UltraBold%20800.otf") format("opentype");
  font-weight:800; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Telegraf';
  src:url("assets/font%20/telegraf-cdnfonts/TelegrafRegular_272984568a25d8528fe2de8b20b29011.otf") format("opentype");
  font-weight:400; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Telegraf';
  src:url("assets/font%20/telegraf-cdnfonts/Telegraf%20UltraLight%20200.otf") format("opentype");
  font-weight:200; font-style:normal; font-display:swap;
}

/* SF Metric — the San Francisco stand-in for every machine that is not an
   Apple one. -apple-system resolves to San Francisco on macOS/iOS and NOWHERE
   else: on Windows it is skipped, so the stack used to fall straight through
   to system-ui = Segoe UI. Different typeface, different advance widths, and
   a line box ~13% taller — so every paragraph rewrapped and the vertical
   rhythm drifted on every section. That is why the site did not hold together
   on a PC while looking right here.

   This is Inter (variable, opsz axis) re-declared with size-adjust and metric
   overrides measured against San Francisco itself. Inter is the closest free
   stand-in for SF in both letterform and metrics, and its optical-size axis
   tracks SF's own size-dependent tracking better than any static font.

   What this buys, honestly:
     • line box at line-height:normal matches SF EXACTLY at every size — so
       vertical rhythm, baseline grid and section heights are identical;
     • advance widths match within ~1% around 16px, where ~93% of the copy
       using this stack lives, drifting to a few percent at the extremes.
   The residual is unavoidable: SF is an optical-size family whose width per
   em swings ~11% between 10px and 200px, and no single static substitute can
   track that exactly. It is a small fraction of the ~10% error plus broken
   line box that Segoe UI produced.

   Per-weight-range size-adjust (same file, cached once) because SF's width
   ratio drifts ~3.4% across weights 300–700.

   Apple devices never reach this face — -apple-system always wins first — so
   the rendering on Geert's own Mac is completely unchanged. */
@font-face{
  font-family:'SF Metric';
  src:url("https://fonts.gstatic.com/s/inter/v20/UcCo3FwrK3iLTcviYwY.woff2") format("woff2");
  font-weight:100 449; font-style:normal; font-display:swap;
  size-adjust:96.54%; ascent-override:100.22%; descent-override:21.75%; line-gap-override:0%;
}
@font-face{
  font-family:'SF Metric';
  src:url("https://fonts.gstatic.com/s/inter/v20/UcCo3FwrK3iLTcviYwY.woff2") format("woff2");
  font-weight:450 549; font-style:normal; font-display:swap;
  size-adjust:97.45%; ascent-override:99.28%; descent-override:21.55%; line-gap-override:0%;
}
@font-face{
  font-family:'SF Metric';
  src:url("https://fonts.gstatic.com/s/inter/v20/UcCo3FwrK3iLTcviYwY.woff2") format("woff2");
  font-weight:550 649; font-style:normal; font-display:swap;
  size-adjust:98.56%; ascent-override:98.16%; descent-override:21.31%; line-gap-override:0%;
}
@font-face{
  font-family:'SF Metric';
  src:url("https://fonts.gstatic.com/s/inter/v20/UcCo3FwrK3iLTcviYwY.woff2") format("woff2");
  font-weight:650 900; font-style:normal; font-display:swap;
  size-adjust:99.66%; ascent-override:97.08%; descent-override:21.07%; line-gap-override:0%;
}

:root {
  /* fine static film grain — one shared tile for every portrait surface
     (landing intro, about hero, match-cut clone) so the texture never
     changes across the transition; opacity lives in the SVG rect */
  --film-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='.055'/%3E%3C/svg%3E");
  --ivory: #f7f3ec;
  --linen: #ede8de;
  --stone: #e3dcd2;
  --dust: #ddd6c8;
  --ink: #211a14;
  --hero-ink: #000; /* pure black — the dark/unlit state is pitch black everywhere (no colour difference between the dipped showreel and the unlit lamp room); warmth appears only when the lamp turns on (the bloom), and screen-over-#000 is perfectly seamless */
  --walnut: #6e5f52;
  --taupe: #a8998a;
  --ember: #c4893a;
  --ember-lite: #d4a66e;
  --lamp: #9b6b3a;
  /* OOAK-style type system: SUSE display · Outfit sub-heads · Inter body */
  /* 'SF Metric' er ook hier tussen, zoals bij --font-body en --v4-gro. Zolang
     SUSE/Outfit laden verandert dit NIETS — een browser pakt de eerste familie
     die er is, dus een latere regel kan per definitie niet meespreken. Het gaat
     om het geval dat Google Fonts hapert: dan viel dit terug op system-ui, en
     dat is Segoe UI op Windows — fors breder, waardoor koppen en zinnen daar
     anders afbreken dan hier. Nu vangt SF Metric dat op. */
  --font-display: 'SUSE', 'SF Metric', system-ui, sans-serif;
  --font-head: 'Outfit', 'SF Metric', system-ui, sans-serif;
  /* -apple-system STAAT HIER BEWUST VOOROP en moet vooraan blijven: op Mac/iOS
     is dat San Francisco, en dat is de look van de site. Nagemeten tegen de
     productie-deploy van 6 aug (slag181), die had `-apple-system, system-ui,
     'Inter'`. Ik heb dit één keer "teruggedraaid" naar Inter op basis van de
     laatste git-commit — maar die commit liep ver achter op wat er live stond,
     dus dat veranderde juist de hero op de Mac. Referentie is de deploy, niet
     git.
     'SF Metric' staat er sinds de Windows-sessie tussen: Apple-toestellen
     bereiken het nooit (-apple-system wint eerder), Windows krijgt er Inter mee
     op SF-metrics i.p.v. Segoe UI. Mac-neutraal, dus dat blijft staan. */
  --font-body: -apple-system, 'SF Metric', system-ui, 'Inter', sans-serif;
  /* the name logotype: Cormorant — a display-cut Garamond, drawn for big sizes */
  --font-garamond: 'Cormorant', 'EB Garamond', Georgia, serif;
}
/* PILOT — projects page + every case page: the accent shifts from ember to the
   gold family of the Get-in-touch pill (lightened per Geert's eye: #e6bb52 /
   #f6d98a), so accents and pill read as ONE material. #case-overlay makes the
   cases gold WHEREVER they open (landing included — the close-X lives inside
   the overlay). .bio-pop is the landing's only other warm accent (its own
   close-X) — gold too now, so it's not the one leftover amber dot on an
   otherwise monochrome landing. The ember originals stay in :root; approve
   the pilot → move these two values into :root and delete this rule. */
body.projects-page, #case-overlay, .bio-pop { --ember: #e6bb52; --ember-lite: #f6d98a; }

* { margin: 0; padding: 0; box-sizing: border-box; }
a:focus-visible, button:focus-visible { outline: 2px solid var(--ember); outline-offset: 3px; border-radius: 4px; }

/* .sr-only — uit beeld, maar NIET uit de pagina. Draagt de <h1> met de naam op
   elke pagina: screenreaders lezen hem, crawlers indexeren hem, en visueel is er
   niets van te zien. Bewust GEEN display:none / visibility:hidden — die halen de
   tekst uit de accessibility tree én laten zoekmachines hem negeren. De 1px-clip
   is de standaardtechniek die dat allebei voorkomt. */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

::selection { background: var(--ember); color: var(--ivory); }

/* Scrollbar hidden site-wide: Lenis drives the smooth scroll, and a 0-width
   scrollbar means the content width never changes when the intro toggles
   body overflow — so the hero no longer shifts when scroll is handed back. */
html { scrollbar-width: none; -ms-overflow-style: none; }
::-webkit-scrollbar { width: 0; height: 0; display: none; }

html { scroll-behavior: auto; }
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto; }

body {
  background: var(--ivory);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

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

.sec-label {
  font-family: var(--font-body);
  font-size: clamp(0.85rem, 1.3vw, 1.05rem);
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--ember);
}
.sec-label.center { text-align: center; }
.sec-label, .ms-year { transition: color 0.5s, text-shadow 0.5s; }
.sec-label.lit, .ms-year.lit { color: #c08a4a; text-shadow: 0 0 18px rgba(212, 166, 110, 0.4); }

.mw { display: inline-block; overflow: hidden; vertical-align: bottom; }
/* after the slide-in reveal finishes, stop clipping so a lit word's soft glow
   can bleed past its box (otherwise the text-shadow is cut into hard rectangles) */
.mw-revealed .mw { overflow: visible; }
.mwi { display: inline-block; }

.flip-clone {
  position: fixed;
  object-fit: cover;
  z-index: 80;
  border-radius: 8px;
  margin: 0;
  will-change: top, left, width, height;
  pointer-events: none;
}


.finale-meta a, .case-link {
  border-bottom: none !important;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 100% 100%;
  background-size: 0% 1px;
  padding-bottom: 3px;
  transition: background-size 0.45s cubic-bezier(0.22, 1, 0.36, 1), color 0.3s;
}
.finale-meta a:hover, .case-link:hover {
  background-position: 0% 100%;
  background-size: 100% 1px;
}

/* ---------- LOADER ---------- */

.loader {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--hero-ink);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.6rem;
  pointer-events: none; /* purely a visual veil — never blocks interaction */
}
.loader-mote { display: none; }
.loader-gd {
  position: relative;
  font-family: var(--font-garamond);
  font-weight: 600;
  font-size: clamp(2.9rem, 6.6vw, 7.2rem);
  line-height: 1.0;
  letter-spacing: 0.01em;
  color: var(--ivory);
  opacity: 0;
  display: inline-flex;
  align-items: baseline;
  white-space: nowrap;
}
.loader-gd .lg-x, .loader-gd .lg-y {
  overflow: hidden;
  white-space: nowrap;
  flex: 0 0 auto;
}
html.reduced .loader { display: none; }

/* ---------- WISP ---------- */

.wisp {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 40;
}

/* ---------- PILL NAV ---------- */

.pill-nav {
  position: fixed;
  bottom: 1.4rem;
  left: 50%;
  display: flex;
  align-items: center;
  gap: 1.6rem;
  background: rgba(33, 26, 20, 0.82);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(247, 243, 236, 0.08);
  border-radius: 100px;
  padding: 0.7rem 1.6rem;
  z-index: 50;
}
.pill-nav.visible { translate: -50% 0; }

.pill-gd {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 0.95rem;
  color: var(--ember);
  letter-spacing: 0.05em;
  text-decoration: none;
  cursor: pointer;
  transition: color 0.25s;
}
.pill-gd:hover { color: var(--ivory); }

.pill-nav a {
  font-family: var(--font-body);
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--taupe);
  text-decoration: none;
  position: relative;
  padding: 0.2rem 0;
  transition: color 0.25s;
  cursor: pointer;
}
.pill-nav a:hover { color: var(--ivory); }
.pill-nav a.active { color: var(--ivory); }
/* actief-stipje verwijderd op verzoek — de regel stond er sinds gisteravond
   weer in (deploy mc8qirxys had hem niet) en zat op bottom:-4px, dus net onder
   de knop: precies waar het "streepje" opdook. Weg, zoals afgesproken. */

/* ---------- LANGUAGE SWITCH ---------- */

.lang-switch {
  position: fixed;
  top: 1.3rem;
  right: 1.5rem;
  z-index: 56;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  background: rgba(33, 26, 20, 0.55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(247, 243, 236, 0.08);
  border-radius: 100px;
  padding: 0.5rem 0.9rem;
}
.lang-switch button {
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  color: var(--taupe);
  padding: 0.3rem 0.1rem;
  transition: color 0.25s;
}
.lang-switch button:hover { color: var(--ivory); }
.lang-switch button.active { color: var(--ember-lite); }
.lang-switch span { color: rgba(168, 153, 138, 0.45); font-size: 0.55rem; }

/* ---------- SCROLL PROGRESS (minimal hairline) ---------- */

.scroll-progress {
  display: none; /* scroll-voortgangsbalk uitgezet op verzoek */
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, rgba(155, 107, 58, 0.5), var(--ember-lite));
  z-index: 58;
  pointer-events: none;
  will-change: transform;
}


/* ---------- HERO ---------- */

.hero {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  position: relative;
  overflow: hidden;
  padding: 0 6vw;
  background: var(--hero-ink);
}

.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 55% 60% at 42% 45%, rgba(212, 166, 110, 0.13), transparent 70%);
  /* the breathing glow may never touch the lower edge */
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent 90%);
  mask-image: linear-gradient(to bottom, #000 55%, transparent 90%);
  pointer-events: none;
  animation: breathe 9s ease-in-out infinite alternate;
}
@keyframes breathe { from { opacity: 0.65; } to { opacity: 1; } }

.hero-atmos { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; }

.hero-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  z-index: 1;
}

.hero-label {
  font-family: var(--font-body);
  font-size: clamp(0.72rem, 0.95vw, 0.84rem);
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  text-transform: uppercase;
  color: var(--taupe);
  margin-bottom: 3.6vh;
}

.hero-title {
  font-family: var(--font-garamond);
  font-weight: 500;
  font-optical-sizing: auto;
  font-size: clamp(2.9rem, 6.6vw, 7.2rem);
  line-height: 0.98;
  letter-spacing: 0.005em;
  color: var(--ivory);
  white-space: nowrap;
}
.hero-title .ch { display: inline-block; will-change: transform, opacity; }
.hero-title .chi { display: inline-block; will-change: transform; }

.hero-rule {
  width: min(360px, 26vw);
  height: 1px;
  background: var(--ember);
  margin: 3vh auto 2.6vh;
}

.hero-tagline {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(0.8rem, 1.1vw, 0.95rem);
  letter-spacing: 0.45em;
  text-indent: 0.45em;
  text-transform: uppercase;
  color: var(--taupe);
}

.hero-scroll {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  text-align: center;
  font-family: var(--font-body);
  font-size: 0.8rem;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: var(--taupe);
  z-index: 1;
  animation: scrollcue-in 0.9s ease-out 1.5s backwards, cuepulse 2.2s ease-in-out 2.4s infinite;
}
@keyframes cuepulse {
  0%, 100% { color: var(--taupe); text-shadow: none; }
  50% { color: #d4a66e; text-shadow: 0 0 14px rgba(212, 166, 110, 0.45); }
}
.scroll-track {
  display: block;
  position: relative;
  width: 2px;
  height: 7vh;
  background: rgba(247, 243, 236, 0.18);
  margin: 14px auto 0;
  overflow: hidden;
}
.beam {
  position: absolute;
  left: 0;
  width: 2px;
  height: 48px;
  background: linear-gradient(to bottom, transparent, #d4a66e 28%, #f3e2c4 50%, #d4a66e 72%, transparent);
  box-shadow: 0 0 14px 2px rgba(212, 166, 110, 0.85);
  display: none;
}
@keyframes scrollcue-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ---------- POSITIONING ---------- */

.positioning {
  min-height: 74vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  text-align: center;
  padding: 0 8vw 2vh;
  position: relative;
  z-index: 2; /* sits above .work so .pos-dark can extend over Selected Work */
  background-color: var(--ivory);
}

/* the dark room: one overlay (flat dark + dust canvas). When the bulb
   ignites, JS grows --mr so the light spreads radially from the bulb. */
.pos-dark {
  position: absolute;
  /* extends well past the lamp section, down over the top of Selected Work, so a
     single bloom from the bulb lights both as one continuous dark→light page */
  inset: -2px 0 -150vh 0;
  z-index: 0;
  /* flat, identical to the hero base — the hero fades its glow + dust + grain
     to nothing at its bottom and pos-atmos fades them in below the seam, so
     at the border both sides are the exact same solid ink: no break possible */
  background: var(--hero-ink);
  pointer-events: none;
  -webkit-mask-image: radial-gradient(circle at var(--mx, 50%) var(--my, 20%), transparent calc(var(--mr, 0px) * 0.1), #000 calc(var(--mr, 0px) * 1.7));
  mask-image: radial-gradient(circle at var(--mx, 50%) var(--my, 20%), transparent calc(var(--mr, 0px) * 0.1), #000 calc(var(--mr, 0px) * 1.7));
}
html.reduced .pos-dark { display: none; }

.pos-atmos {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.lamp {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 2vh;
  position: relative;
  z-index: 1;
}
.lamp-cord {
  position: relative;
  width: 2px;
  height: clamp(150px, 24vh, 260px);
  /* identical width + colour to the cue's .scroll-track so the two read as one
     continuous, evenly-weighted line (no thinner-below look at the seam) */
  background: rgba(247, 243, 236, 0.22);
  transition: background-color 1.1s ease;
  overflow: hidden;
}
/* the cord keeps ONE constant colour — it must not flash to a lighter tone when
   the lamp turns on (it stays the same neutral line at all times) */
.lamp-ferrule {
  width: 7px;
  height: 9px;
  background: linear-gradient(180deg, #d4a66e 0%, #6e5535 45%, #2a2014 100%);
  border-radius: 2px 2px 1px 1px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.lamp-shade {
  width: 58px;
  height: 30px;
  /* a more refined pendant cone: brushed-metal gradient, a soft top highlight,
     an inner shadow for depth and a warm brass rim where the light escapes */
  background: linear-gradient(180deg, #6a5436 0%, #4a3a25 48%, #2b2114 100%);
  clip-path: polygon(34% 0, 66% 0, 100% 100%, 0 100%);
  border-radius: 2px 2px 0 0;
  box-shadow:
    inset 0 1px 0 rgba(247, 238, 220, 0.18),
    inset 0 -3px 7px rgba(0, 0, 0, 0.5),
    inset 0 -1px 0 rgba(212, 166, 110, 0.6);
  position: relative;
  z-index: 2;
}
.lamp-bulb {
  position: relative;
  width: 24px;
  height: 34px;
  margin-top: -3px;
  z-index: 1;
}
.bulb-svg { display: block; width: 100%; height: 100%; overflow: visible; }
.bulb-cap { fill: #5a4a38; transition: fill 1.1s ease; }
.positioning.lit .bulb-cap, html.reduced .bulb-cap { fill: #3c3023; }
.bulb-thread { stroke: rgba(18, 13, 8, 0.55); stroke-width: 1; fill: none; }
.bulb-glass {
  fill: rgba(247, 238, 220, 0.07);
  stroke: rgba(226, 214, 196, 0.65);
  stroke-width: 1;
  transition: fill 1.1s ease, stroke 1.1s ease;
}
.positioning.lit .bulb-glass, html.reduced .bulb-glass {
  fill: rgba(255, 251, 242, 0.4);
  stroke: rgba(33, 26, 20, 0.38);
}
.bulb-filament {
  fill: none;
  stroke: rgba(216, 196, 172, 0.55);
  stroke-width: 0.8;
  stroke-linecap: round;
  transition: stroke 1.1s ease;
}
.positioning.lit .bulb-filament, html.reduced .bulb-filament { stroke: rgba(155, 107, 58, 0.55); }
.lamp-glow {
  position: absolute;
  left: 2.5px;
  top: 14px;
  width: 19px;
  height: 19px;
  border-radius: 50%;
  background: radial-gradient(circle at 45% 38%, #fff6e4 0%, #f3d3a4 50%, #d9a86f 100%);
  box-shadow:
    0 0 8px 3px rgba(255, 235, 200, 0.6),
    0 0 28px 12px rgba(224, 180, 124, 0.32),
    0 0 80px 40px rgba(212, 166, 110, 0.14);
  opacity: 0;
}
html.reduced .lamp-glow { opacity: 0.9; }

.pos-quote {
  font-family: var(--font-display);
  font-weight: 300;
  font-style: italic;
  font-size: clamp(1.9rem, 3.6vw, 3.4rem);
  line-height: 1.3;
  letter-spacing: 0;
  max-width: 24ch;
  text-wrap: balance;
  position: relative;
  z-index: 1;
  /* auto top + auto bottom on the cue centres the quote+cue group in the lit
     space below the lamp, so the quote sits between the lamp and Selected Work */
  margin-top: auto;
  /* hidden in the dark; JS fades the whole quote in as the light front sweeps
     over it, so the words appear exactly when that part of the page lights up */
  opacity: 0;
}
.pos-quote span {
  color: var(--ink);
  transition: text-shadow 0.5s;
}
.pos-quote span.lit { text-shadow: 0 0 24px rgba(212, 166, 110, 0.55); }
html.reduced .pos-quote { opacity: 1; }

.pos-cue {
  margin-top: 6vh;
  margin-bottom: auto;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  font-family: var(--font-body);
  font-size: 0.72rem;
  letter-spacing: 0.32em;
  text-indent: 0.32em;
  text-transform: uppercase;
  color: var(--taupe);
  text-decoration: none;
  position: relative;
  z-index: 1;
  opacity: 0;
  transition: opacity 1s ease, color 0.35s ease;
}
.positioning.lit .pos-cue { opacity: 1; transition-delay: 1.6s, 0s; }
.pos-cue:hover { color: var(--ember); }
.cue-arrow {
  font-size: 0.9rem;
  text-indent: 0;
  animation: cuebob 2.4s ease-in-out infinite;
}
@keyframes cuebob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(5px); }
}
html.reduced .pos-cue { opacity: 1; transition: none; }
html.reduced .cue-arrow { animation: none; }

/* ---------- SELECTED WORK ---------- */

/* z-index:1 so the dark overlay (which lives in .positioning, z-index:2) can
   extend down and cover the top of Selected Work — the single radial bloom from
   the lamp then reveals the lamp space AND Selected Work as one continuous page. */
.work { --wk-gap: clamp(12px, 1.25vw, 20px); padding: 2vh 6vw 3vh; background: var(--ivory); position: relative; z-index: 1; }

/* fan transform is built from these per-card custom props so GSAP can animate the
   deal (--fx sideways nudge, --fy rise-from-below, --fr tilt) while a hover lift
   (--hy/--hs) composes on top. Registered with @property so they animate smoothly. */
@property --fx { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
@property --fy { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --fr { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --hy { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --hs { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --ds { syntax: '<number>'; inherits: false; initial-value: 1; }

.card-stack {
  max-width: min(128rem, 92vw);
  margin: 12vh auto 0;
  display: flex;
  flex-direction: column;
  /* original spacing between cards */
  gap: 12vh;
}

.work-card {
  position: sticky;
  border-radius: 16px;
  overflow: hidden;
  isolation: isolate;
  min-height: 72vh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  box-shadow: 0 40px 80px -50px rgba(33, 26, 20, 0.45);
  cursor: pointer;
  will-change: transform;
  background: var(--ink);
  /* fan: tilt + sideways nudge (driven by GSAP on deal); hover lift composes via --hy/--hs */
  transform-origin: 50% 50%;
  transform:
    translateX(var(--fx, 0%))
    translateY(calc(var(--fy, 0px) + var(--hy, 0px)))
    rotate(calc(var(--fr, 0) * 1deg))
    scale(var(--hs, 1))
    scale(var(--ds, 1));
  transition: box-shadow 0.7s cubic-bezier(0.22, 1, 0.36, 1),
              --hy 0.5s cubic-bezier(0.22, 1, 0.36, 1),
              --hs 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.work-card, .more-panel {
  box-shadow: 0 40px 80px -50px rgba(33, 26, 20, 0.45);
}
/* depth between stacked cases: each card casts a soft shadow UP onto the sliver of
   the case behind it (the seam), so the stack reads as thick, physically layered
   cards with air between them instead of a hard cut. No border — a stroke would
   flatten the photographic cards. Work cards only; hover swaps to the glow below. */
.work-card {
  /* keep ONLY the black seam shadow onto the case behind. The warm ambient
     ground glow (rgba(33,26,20) bloom) read as a soft halo/stripe around each
     card, and with 4 cards docked at the bottom that stacked into ~4 glows. */
  box-shadow:
    0 -12px 26px -6px rgba(0, 0, 0, 0.5);       /* seam shadow onto the case behind */
}
/* old-CRT-screen glow: the lit card radiates a warm phosphor bloom into the dark
   room — a tight inner ring, a soft mid bloom, and a far halo — so it feels like a
   thick tube TV switched on, not a flat card */
/* NB: no clip-path here. overflow:hidden + border-radius already keep the card AND
   its scaled image rounded (verified, incl. the 3D hover-tilt) — and crucially a
   clip-path would CLIP the hover box-shadow, killing the backlight glow below. */
/* hover: a strong, glowy grey-white BACKLIGHT — a smooth blurred halo that bleeds
   well past the card edge into the dark for real depth (layered, no hard ring) */
.work-card:hover, .more-panel:hover {
  /* ONE soft white box-shadow, not a stack: every extra layer adds its own visible
     soft edge, and on the near-black room those edges read as concentric stripes.
     A single low-spread, wide-blur shadow is the smoothest a box-shadow can be —
     one continuous gaussian falloff that blooms out and fades cleanly into the dark. */
  box-shadow:
    0 46px 95px -50px rgba(33, 26, 20, 0.55),
    0 0 150px 6px rgba(236, 234, 230, 0.22);
}
.more-panel { transition: box-shadow 0.7s cubic-bezier(0.22, 1, 0.36, 1); }
/* loosely-placed deck: every card stays perfectly LEVEL (no horizontal tilt),
   but each drifts up on its own diagonal — different vertical rhythm + a side-shift
   that alternates left/right with varying reach, so they clearly lie over one
   another (never a perfect pile) and the stack climbs dynamically bottom→top */
/* original even stagger — each card docks 2vh lower than the one before.
   shifted +3vh down so the docked card clears the fixed pill-nav (bottom ~8vh)
   instead of sitting right under it; the 2vh inter-card rhythm is preserved */
.work-card:nth-child(1) { top: 12vh; }
.work-card:nth-child(2) { top: 14vh; }
.work-card:nth-child(3) { top: 16vh; }
.work-card:nth-child(4) { top: 18vh; }
/* subtle hover lift — composes on top of the fan tilt via custom props (so the
   card keeps its rotation while lifting + growing slightly) */
.work-card:hover { --hy: -0.6vh; --hs: 1.012; }

.work-card, .more-panel { --lit: 0; --mx: 50%; --my: 50%; }
.work-card > img, .more-panel > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  /* sits ~85% saturated at rest; only a real HOVER on the thumbnail pops it to
     full colour (0.85 → 1.0) + a touch of over-brightness */
  filter: saturate(0.85);
  transition: transform 0.9s cubic-bezier(0.22, 1, 0.36, 1), filter 0.55s ease;
}
.work-card:hover > img, .more-panel:hover > img { transform: scale(1.035); filter: saturate(1.08) brightness(1.05); }
/* the per-card "torch" wash is removed: the cursor already carries its own light
   (the curio glow), so a second card-level wash read as an extra haze on hover.
   The card now reveals cleanly via the ::after dim lifting + the image saturating. */
.work-card::after, .more-panel::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: rgba(22, 16, 11, 0.55);
  transition: opacity 1.4s cubic-bezier(0.33, 0, 0.2, 1);
  z-index: 0;
}
.work-card:hover::after, .more-panel:hover::after { opacity: 0; }

.card-shade {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* soft wash at the bottom, just strong (and tall) enough that the whole
     text block sits on dark — it has to carry readability on HOVER too,
     when the resting ::after dim lifts and the photo goes full colour */
  background: linear-gradient(to top, rgba(22, 16, 11, 0.82) 0%, rgba(22, 16, 11, 0.44) 38%, rgba(22, 16, 11, 0) 76%);
  z-index: 1;
}

.card-info {
  position: relative;
  z-index: 2;
  width: 100%;
  padding: clamp(1.8rem, 4vw, 3.5rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

/* .card-tag lives on only in the More-work panels; the work cards moved
   their meta to the film-frame corners below (.card-no / .card-cat) */
.card-tag {
  font-family: var(--v4-gro);
  font-size: 1.32rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  /* ivory, not amber — small amber type over a colour photo was the
     readability killer on these cards */
  color: rgba(247, 243, 236, 0.92);
  margin-bottom: 1.05rem;
  text-shadow: 0 1px 18px rgba(0, 0, 0, 0.75);
}

/* FILM-FRAME CORNERS — number top-left, category top-right, both in the
   site's mono chrome voice (same pattern as the journey filmstrip's FR.01 /
   year). The centre keeps only title + client + button. */
.card-no, .card-cat {
  position: absolute; top: 2.4rem; z-index: 2; margin: 0;
  font-family: var(--v4-mono), 'Space Mono', monospace;
  font-size: 1.5rem; letter-spacing: 0.22em; text-transform: uppercase;
  color: rgba(247, 243, 236, 0.68);
  text-shadow: 0 1px 14px rgba(0, 0, 0, 0.8);
}
.card-no { left: 2.6rem; }
.card-cat { right: 2.6rem; text-align: right; }
/* subtle top scrim so the corner meta stays readable on bright footage
   (the bottom text block already has .card-shade) */
.work-card::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 20%;
  background: linear-gradient(to bottom, rgba(22, 16, 11, 0.5), transparent);
  border-radius: inherit; pointer-events: none; z-index: 1;
}

/* ---------- IG-REEL — the exact Instagram Reels chrome over the Dafi card:
   action rail stacked VERTICALLY on the right edge (heart/comment/share,
   count under each icon, ⋮ menu, spinning-audio tile at the foot) and the
   username + Follow + caption + audio row bottom-left — the real reel
   anatomy, not the feed-post layout. Decorative only (aria-hidden,
   pointer-events:none); child order (after <img>, before ::after) makes it
   dim together with the photo on sibling-hover like everything else. */
/* AT REST the chrome sits dimmed — only the Dafi card carries it, so at full
   strength it pulled the eye and unbalanced the three-card row. Hovering the
   card brings it to full so it reads clearly the moment you engage. */
.ig-reel{
  position:absolute; inset:0; pointer-events:none; color:#fff; z-index:1;
  opacity:.42; transition:opacity .5s ease;
}
.work-card:hover .ig-reel{ opacity:1; }
/* reels put a readability scrim along the whole lower third */
.ig-reel::before{
  content:''; position:absolute; left:0; right:0; bottom:0; height:42%;
  background:linear-gradient(to top, rgba(8,6,5,.62), transparent);
}
.igr-rail{
  position:absolute; right:1.3rem; bottom:2.6rem;
  display:flex; flex-direction:column; align-items:center; gap:2.55rem;
}
.igr-act{ display:flex; flex-direction:column; align-items:center; gap:.62rem; }
.igr-act svg{ width:2.75rem; height:2.75rem; filter:drop-shadow(0 1px 5px rgba(0,0,0,.5)); }
.igr-act b{
  font-family:var(--font-body); font-size:1.12rem; font-weight:600; line-height:1;
  text-shadow:0 1px 8px rgba(0,0,0,.6);
}
.igr-more svg{ width:2.2rem; height:2.2rem; }
/* the audio tile: album art in a rounded square with the white keyline,
   exactly the reel's bottom-right "spinning record" slot */
.igr-audio{
  width:3.1rem; height:3.1rem; border-radius:9px; overflow:hidden;
  border:2px solid rgba(255,255,255,.95);
  box-shadow:0 1px 6px rgba(0,0,0,.45);
}
.igr-audio img{ width:100%; height:100%; object-fit:cover; }
.igr-meta{
  position:absolute; left:1.4rem; right:6.4rem; bottom:2.6rem;
  display:flex; flex-direction:column; gap:1.05rem;
}
.igr-user{
  display:flex; align-items:center; gap:1rem; margin:0;
  font-family:var(--font-body); font-size:1.5rem;
}
.igr-user b{ font-weight:600; text-shadow:0 1px 8px rgba(0,0,0,.6); }
/* the verified seal: white scalloped badge, dark check knocked out */
.igr-verified{
  width:1.5rem; height:1.5rem; flex:none; margin-left:-.25rem;
  filter:drop-shadow(0 1px 4px rgba(0,0,0,.5));
}
.igr-avatar{
  flex:none; width:3.4rem; height:3.4rem; border-radius:50%;
  display:grid; place-items:center; overflow:hidden;
  /* the Dafi wordmark is a transparent png — a white disc behind it, like a
     real profile picture with a light backdrop */
  background:#fff;
  box-shadow:0 0 0 1.5px rgba(255,255,255,.9);
}
.igr-avatar img{ width:82%; height:82%; object-fit:contain; }
.igr-caption{
  margin:0; font-family:var(--font-body); font-size:1.42rem; font-weight:400;
  line-height:1.35; color:rgba(255,255,255,.95);
  text-shadow:0 1px 8px rgba(0,0,0,.6);
}
.igr-sound{
  display:flex; align-items:center; gap:.7rem; margin:0;
  font-family:var(--font-body); font-size:1.3rem; color:rgba(255,255,255,.92);
  text-shadow:0 1px 8px rgba(0,0,0,.6);
}
.igr-sound svg{ width:1.5rem; height:1.5rem; flex:none; filter:drop-shadow(0 1px 4px rgba(0,0,0,.5)); }
.igr-sound span{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

.card-title {
  font-family: var(--v4-gro);
  font-weight: 500;
  font-size: clamp(3.8rem, 6.9vw, 6.9rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  color: var(--ivory);
  margin-bottom: 1.2rem;
  max-width: 26ch;
  text-wrap: balance;
  text-shadow: 0 2px 30px rgba(0, 0, 0, 0.5);
}

.card-client {
  font-family: var(--v4-gro);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #fff;
  margin-top: -0.2rem;
  text-shadow: 0 1px 18px rgba(0, 0, 0, 0.75);
}
.card-client::before {
  content: '';
  display: block;
  width: 30px;
  height: 1.5px;
  background: var(--ember-lite);
  margin: 0 auto 0.95rem;               /* centred again */
}

.card-cta {
  display: inline-block;
  margin-top: 1.5rem;
  font-family: var(--v4-gro);
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ivory);
  background: transparent;
  border: 1.5px solid rgba(231, 191, 140, 0.95);
  border-radius: 100px;
  padding: 0.72rem 1.55rem;
  /* transform must be in the transition for the grow to animate */
  transition: background 0.35s ease, color 0.35s ease, border-color 0.35s ease,
              transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
  transform-origin: center;
}
/* hover anywhere on the CARD lights up its button (fill + grow) and animates back
   to the empty outline on mouse-leave. Card-level so the small button is easy to
   trigger — the whole card is the hot zone. */
.work-card:hover .card-cta, .more-panel:hover .panel-cta {
  background: var(--ember);
  border-color: var(--ember);
  color: var(--ivory);
  transform: scale(1.08);
}
.card-cta::after { content: '  →'; white-space: pre; }

/* ---------- SELECTED WORK — 3-up hover row (reference: chriskalafatis.com) ----------
   header row = round "view all" button (left) + intro paragraph (right), then a
   single dark stage holding 3 full-height columns. Resting state: all 3 fully lit,
   no text. Hover one → its siblings dim (image desaturates + darkens), the hovered
   column reveals a bottom meta pair (title/client left, category/year right), and a
   single shared oversized title (fed by JS from data-feature) fades in low across
   the stage — same layer for all 3, so it doesn't jump size/position between cards. */

.work-head {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  /* same escape from the section's 6vw padding as .work-row below, so the
     button's LEFT edge and the intro line's RIGHT edge sit exactly on the
     outer edges of the first and last thumbnails */
  margin: 1.5vh calc(3 * var(--wk-gap) - 6vw) 0;
  /* the head keeps the height of the ORIGINAL (larger) button so shrinking
     the circle doesn't pull the thumbnail row up — the row stays put */
  min-height: clamp(9rem, 9.3vw, 12.1rem);
}
.work-head .sec-intro { text-align: left; margin-top: 0; flex: 1; transform: translateY(-0.9rem); }

/* round View-all button — the Learn more circle's twin: 2px ring, ivory fill
   that wipes up from the base on hover while the label slides out and a dark
   ↗ arrow rises in (the arrow also drifts toward the cursor, wired in JS) */
/* same motion system as .intro-more (the About "Learn more" circle): detached
   ring that breathes ahead of the button, compositor-friendly scale wipe from
   the base, label out / arrow in, press + sonar ping on click */
.work-viewall {
  position: relative;
  flex: none;
  width: clamp(8.1rem, 8.4vw, 10.9rem);
  height: clamp(8.1rem, 8.4vw, 10.9rem);
  border-radius: 50%;
  display: grid;
  place-items: center;
  text-align: center;
  color: var(--ivory);
  text-decoration: none;
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}
.work-viewall .wv-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid rgba(247, 243, 236, 0.5);
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.45s ease;
  will-change: transform;
}
.work-viewall::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  z-index: 0;
  background: #f4ece0;
  transform: scale(0);
  transform-origin: 50% 100%;
  transition: transform 0.65s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}
.work-viewall .wv-label {
  grid-area: 1 / 1;
  position: relative;
  z-index: 1;
  font-family: var(--v4-mono), 'Space Mono', monospace;
  font-size: clamp(1.05rem, 1.15vw, 1.4rem);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.2;
  transition: opacity 0.35s ease, transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.work-viewall .wv-arrow {
  grid-area: 1 / 1;
  position: relative;
  z-index: 2;
  width: 32%;
  height: 32%;
  color: #0a0a0a;
  opacity: 0;
  transform: translateY(72%);                 /* rises in from the base, like Learn more */
  transition: opacity 0.35s ease, transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
  overflow: visible;
}
.work-viewall:hover { transform: scale(1.06); }
.work-viewall:hover::before { transform: scale(1); }
.work-viewall:hover .wv-ring { transform: scale(1.1); border-color: #f4ece0; }
.work-viewall:hover .wv-label { opacity: 0; transform: translateY(-72%); }
.work-viewall:hover .wv-arrow { opacity: 1; transform: translateY(0); }
/* click = press + sonar ping, same gesture as the Learn-more circle; the
   page-transition curtain picks it up mid-ping */
.work-viewall.go { transform: scale(0.93); }
.work-viewall.go .wv-ring { animation: morePing 0.55s cubic-bezier(0.2, 0.7, 0.3, 1) forwards; }

/* three SEPARATE portrait cards — rounded rectangles with a breath of air between
   them, filling the row (reference frame 1: all lit, no text). The dark stage is
   simply the darkened section behind them. */
.work-row {
  position: relative;
  display: flex;
  gap: var(--wk-gap);
  /* nearly full-bleed: the row escapes the section's 6vw side padding so the
     outer margins are exactly 3× the gap between the cards (reference frame 1) */
  margin: 4.2vh calc(3 * var(--wk-gap) - 6vw) 0;
}
.work-row .work-card {
  position: relative;
  flex: 1;
  min-height: 0;
  height: auto;
  aspect-ratio: 0.74;          /* tall standing rectangle, a touch shorter than the reference's 600×860 */
  max-height: 88vh;
  top: auto;
  border-radius: clamp(14px, 1.6vw, 22px);
  overflow: hidden;
  box-shadow: none;
  background: var(--ink);
  transition: box-shadow 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}
/* hover: pop to full colour + a subtle white-grey BACKGLOW blooming past the
   frame into the dark (same soft halo as the earlier work-card version) */
.work-row .work-card:hover {
  --hy: 0px; --hs: 1;
  box-shadow:
    0 46px 95px -50px rgba(33, 26, 20, 0.55),
    0 0 120px 4px rgba(236, 234, 230, 0.20);
}
/* resting = muted (85% sat + brightness); hover one → the OTHERS drop into the dark */
.work-row .work-card::after {
  opacity: 0;
  background: rgba(8, 6, 4, 0.78);
  transition: opacity 0.55s ease;
}
.work-row.is-hovering .work-card:not(:hover)::after { opacity: 1; }
/* cards sit at 85% saturation + brightness at rest; a real hover pops the
   thumbnail to full colour (0.85 → 1.0) */
.work-row .work-card > img { filter: saturate(0.85) brightness(0.85); }
.work-row .work-card:hover > img { filter: saturate(1) brightness(1); transform: scale(1.04); }
.work-row.is-hovering .work-card:not(:hover) > img {
  filter: saturate(0.12) brightness(0.6);
  transform: scale(1);
}
/* IKEA sits further away in its frame than the other two (a wide plaza shot), so
   it reads smaller in the row. Push it in a little and shift the billboard left.
   The hover delta is restated at the same +0.04 step so the card still "lifts"
   by exactly as much as its neighbours — a bare base scale here would be
   overridden by the hover rule above and the thumbnail would SHRINK on hover.
   (Safe for the open flip: it measures the img's real painted box and clips that
   to the card frame, so any base scale is carried correctly.) */
.work-row .work-card[data-case="ikea"] > img { transform: scale(1.14); object-position: 13% 58%; }
.work-row .work-card[data-case="ikea"]:hover > img { transform: scale(1.18); }
.work-row.is-hovering .work-card[data-case="ikea"]:not(:hover) > img { transform: scale(1.14); }

/* VOLGORDE VERSCHILT PER LAYOUT — bewust, en alleen hier.
   De HTML-volgorde is Spoorwegmuseum → IKEA → Dafi, want dat is wat de mobiele
   carousel (≤760px) moet tonen: Spoorwegmuseum als eerste kaart links. Op de
   desktoprij hoort Spoorwegmuseum juist in het MIDDEN, zoals het eerder stond.
   Flex `order` regelt dat zonder de HTML aan te raken, dus mobiel blijft exact
   zoals het nu is en er verandert niets aan de carousel, de snap-punten of de
   flip (die zoeken op data-case, niet op positie).
   861px, niet 1024: tussen 761 en 860 staat de rij als verticale kolom
   (@media max-width:860px) — daar is "midden" betekenisloos en houdt hij de
   leesvolgorde van de HTML aan. */
@media (min-width:861px){
  .work-row .work-card[data-case="ikea"]{ order:-1; }
}

/* per-card data lives in hidden spans (so i18n keeps translating them);
   JS copies them into the shared meta bar on hover */
.wk-d { display: none; }

/* FEATURED PROJECT + oversized title — one shared centred layer over the row,
   exactly like the reference: eyebrow centred, huge uppercase name under it,
   sitting just below the middle of the stage */
.wk-feature {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* the flex group centres eyebrow+title together, which would drop the title
     below the true middle; this padding equals the eyebrow's rendered height
     (1.2 × font-size + its 2.4rem margin) so the TITLE lands dead-centre */
  padding-bottom: calc(1.2 * clamp(1.25rem, 1.8vw, 1.8rem) + 2.4rem);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s ease;
}
.work-row.is-hovering .wk-feature { opacity: 1; }
.wk-feature-eyebrow {
  margin: 0 0 2.4rem;
  font-family: var(--v4-mono), 'Space Mono', monospace;
  font-size: clamp(1.25rem, 1.8vw, 1.8rem);
  letter-spacing: 0.36em;
  text-transform: uppercase;
  color: #fff;
  /* twee ruim geblurde lagen i.p.v. één: draagt de dunne mono-letters over een
     felle lucht zonder dat er een rand om de glyphs komt te staan */
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.62),
               0 2px 24px rgba(0, 0, 0, 0.78);
}
/* the big name sits dead-centre of the stage; the eyebrow rides above it */
.wk-feature-title {
  margin: 0;
  font-family: var(--v4-gro);
  font-weight: 500;
  font-size: clamp(4.4rem, 9.7vw, 11.4rem);
  line-height: 1;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  /* pure white, matching the sec-intro line — ivory read as dingy next to it */
  color: #fff;
  white-space: nowrap;
  /* gelaagd van kort naar wijd: de eerste laag geeft de letters direct houvast,
     de wijdste dooft de foto eronder. Kleinste blur is 12px — alles daaronder
     tekent een zichtbaar randje langs de glyph i.p.v. schaduw */
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.34),
               0 8px 34px rgba(0, 0, 0, 0.42),
               0 18px 80px rgba(0, 0, 0, 0.5);
}

/* meta line BELOW the cards, left edge snapping to the hovered card's left
   edge — ONE quiet row "[project title] · [discipline]", the .pc-meta
   content model from the projects page (was two stacked twin-columns with
   four values: title/client + category/year). left is UNtransitioned on
   purpose: switching hover between cards must pop the line straight up
   under the new frame, not glide sideways past the ones in between */
.wk-meta {
  position: absolute;
  top: calc(100% + 0.7rem);   /* hugs the row so it stays clear of the proof tiles below */
  left: 0;
  z-index: 4;
  margin: 0;
  font-family: var(--v4-mono), 'Space Mono', monospace;
  font-size: 1.05rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  color: #fff;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.45s ease, transform 0.45s ease;
  pointer-events: none;
}
.work-row.is-hovering .wk-meta { opacity: 1; transform: translateY(0); }
.wk-meta .wk-m-cat { color: rgba(255, 255, 255, 0.72); }
.wk-meta-dot { margin: 0 0.85em; color: rgba(255, 255, 255, 0.4); }


/* ---------- MORE WORK ---------- */

.more { background: var(--linen); min-height: 100vh; display: flex; flex-direction: column; justify-content: center; padding: 10vh 0; }

.marquee {
  overflow: hidden;
  border-top: 1px solid var(--dust);
  border-bottom: 1px solid var(--dust);
  padding: 2.2rem 0;
  background: var(--linen);
}
.marquee-track { display: flex; width: max-content; will-change: transform; }
.mq {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(2rem, 3.8vw, 3.2rem);
  white-space: nowrap;
  display: flex;
  align-items: center;
  color: var(--ink);
}
.mq::after {
  content: '';
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--ember);
  margin: 0 3rem;
  flex-shrink: 0;
}

.sec-intro, .more-sub {
  text-align: center;
  color: var(--walnut);
  margin-top: 1.2rem;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.1rem, 1.6vw, 1.35rem);
}

/* editorial proof strip — a quiet metrics row (not a SaaS stats block): mono small
   caps, hairline dividers, warm-accent numbers, the result of following the light */
.proof-strip{
  list-style:none; margin:2.4rem auto 0.2rem; padding:0;
  display:flex; flex-wrap:wrap; justify-content:center; align-items:center;
  gap:.7rem 1.55rem; max-width:min(900px,90vw);
  font-family:var(--v4-mono); font-size:clamp(.6rem,.8vw,.72rem);
  letter-spacing:.14em; text-transform:uppercase; color:rgba(247,243,236,.5);
}
.proof-strip li{ position:relative; display:inline-flex; align-items:baseline; gap:.45em; white-space:nowrap; }
.proof-strip li:not(:last-child)::after{
  content:''; position:absolute; right:-.8rem; top:50%; height:.85em; width:1px;
  background:rgba(247,243,236,.18); transform:translateY(-50%);
}
.proof-strip strong{ font-weight:700; color:var(--ember-lite); letter-spacing:.04em; }
@media (max-width:600px){
  .proof-strip{ gap:.5rem 1rem; font-size:.58rem; letter-spacing:.1em; }
  .proof-strip li:not(:last-child)::after{ display:none; }
}

/* Beyond-the-Brief reach row — the stat-tile bar from the AI Animation case page,
   promoted to the section head (the case page itself no longer carries it).
   Same dark translucent tiles, sized up a notch. The numbers glow in the
   site's ivory light (they were amber — the only warm tint left on the page,
   and it read as a different site). */
/* scope label above the tiles — the sentence itself does the connecting
   ("what my work adds up to" points straight at the row above); it shares the
   row's left edge so eye-tracking groups label, tiles and thumbnails as one. */
.proof-scope{
  margin: clamp(3rem,6vh,5rem) calc(3 * var(--wk-gap) - 6vw) 0;
  font-family:var(--v4-mono),'Space Mono',monospace;
  font-size:1.5rem; letter-spacing:.24em; text-transform:uppercase;
  text-align:center; color:rgba(247,243,236,.62);
}
.more-proof{
  list-style:none; padding:0;
  margin: clamp(3rem,6vh,5rem) auto clamp(2.8rem,5vh,4.2rem);
  display:grid; grid-template-columns:repeat(auto-fit, minmax(200px,1fr));
  gap:1.3rem;
  /* .more is a flex column, so `margin:auto` alone shrinks the grid to
     fit-content — force the full width so the bar lines up with the two
     project panels below it */
  width:min(1280px,92vw); max-width:none;
}
/* when the kicker precedes the tiles it owns the top spacing; the tiles tuck in
   close and inherit the thumbnail row's exact width, edges and gap */
.proof-scope + .more-proof{
  margin:1.6rem calc(3 * var(--wk-gap) - 6vw) clamp(2.8rem,5vh,4.2rem);
  width:auto; gap:var(--wk-gap);
}
.more-proof li{
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.85rem;
  text-align:center;
  /* soft light pooling at the tile's foot — ivory, like every other glow */
  background:
    radial-gradient(130% 95% at 50% 118%, rgba(247,243,236,.09), transparent 62%),
    linear-gradient(180deg, rgba(27,25,29,.58), rgba(17,17,21,.42));
  border:1px solid rgba(247,243,236,.18);
  border-radius:14px;
  padding:3.1rem 1rem;
  box-shadow: 0 26px 60px -30px rgba(0,0,0,.75);
}
.more-proof strong{
  font-family:'Telegraf', var(--v4-gro); font-weight:700; line-height:1;
  font-size:clamp(2.5rem,4.4vw,3.9rem); color:var(--ivory); letter-spacing:-.01em;
  font-variant-numeric: tabular-nums;
  /* numbers carry their own light at rest */
  text-shadow: 0 0 22px rgba(247,243,236,.26), 0 0 8px rgba(247,243,236,.12);
}
.more-proof span{
  font-family:var(--font-body); font-weight:600;
  font-size:1.02rem; letter-spacing:.18em; text-transform:uppercase;
  color:rgba(247,243,236,.55);
}
/* scroll-in reveal: JS puts .pre on the list at init (motion allowed only), then
   lifts each tile with .in on a stagger while its number counts up — tile rises,
   number sharpens out of a soft blur. Order matters: .in rules must win over .pre. */
.more-proof.pre li{ opacity:0; transform:translateY(22px) scale(.97); }
.more-proof.pre li strong{ filter:blur(8px); }
/* the original unhurried fade-in — tiles rise and sharpen while the count is
   ALREADY ticking underneath (the count runs long enough to still be going
   well after the blur clears), so the reveal and the spin overlap */
.more-proof li{ transition:opacity .8s ease, transform .9s cubic-bezier(.16,1,.3,1); }
.more-proof li strong{ transition:filter .9s ease; }
.more-proof li.in{ opacity:1; transform:none; }
.more-proof li.in strong{ filter:blur(0); }
/* THE LANDING — the slot-machine "hit", but unhurried: as the count locks in,
   the number swells gently and its glow breathes up warm-bright, the tile's
   border + inner glow rising with it, then everything eases back down to the
   resting light. A slow warm pulse, not a camera flash. */
.more-proof strong.hit{ animation: proofHit 1.6s cubic-bezier(.33,1,.45,1) both; }
@keyframes proofHit{
  0%   { transform:scale(1); }
  38%  { transform:scale(1.03);
         text-shadow: 0 0 12px rgba(255,255,255,.38), 0 0 36px rgba(247,243,236,.32), 0 0 62px rgba(247,243,236,.14);
         color:#fff; }
  100% { transform:scale(1);
         text-shadow: 0 0 22px rgba(247,243,236,.26), 0 0 8px rgba(247,243,236,.12);
         color:var(--ivory); }
}
.more-proof li.hit{ animation: tileHit 1.8s ease-out both; }
@keyframes tileHit{
  0%   { border-color:rgba(247,243,236,.18); }
  36%  { border-color:rgba(247,243,236,.38);
         box-shadow: 0 0 46px -12px rgba(247,243,236,.20), 0 26px 60px -30px rgba(0,0,0,.75),
                     inset 0 0 30px rgba(247,243,236,.06); }
  100% { border-color:rgba(247,243,236,.18);
         box-shadow: 0 26px 60px -30px rgba(0,0,0,.75); }
}
@media (max-width:600px){
  .more-proof{ grid-template-columns:1fr 1fr; gap:.8rem; }
  .more-proof li{ padding:2rem .6rem; }
  .more-proof strong{ font-size:clamp(2rem,10vw,2.7rem); }
  .more-proof span{ font-size:.72rem; }
}

/* Beyond-the-Brief reach line — sits where .card-client sits on the work cards,
   so it takes that voice: brighter, a touch bigger, and the same ember dash above */
.panel-desc{
  font-family:var(--v4-gro); font-style:normal; font-weight:400;
  font-size:clamp(1.15rem,1.4vw,1.35rem); line-height:1.45;
  color:rgba(247,243,236,.92); margin:.1rem auto .9rem; max-width:40ch; letter-spacing:-.005em;
  text-shadow:0 1px 18px rgba(0,0,0,.75);
}
.panel-desc::before{
  content:''; display:block; width:26px; height:1.5px;
  background:var(--ember-lite); margin:0 auto .85rem;   /* = .card-client::before, scaled */
}

.more-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;
  gap: clamp(2rem, 4vw, 4.5rem);   /* white space between them so they breathe */
  /* same RENDERED width as the Selected Work cards: the .work section has 6vw side
     padding, so its card-stack tops out at ~88vw — match that here */
  max-width: min(128rem, 88vw);
  margin: 0 auto;
  padding: 0;
  width: 100%;
}
/* both panels sit level, side by side, centred on the page */
.more-grid .more-panel:nth-child(2) { margin-top: 0; }

.more-panel {
  position: relative;
  border-radius: 16px;
  overflow: hidden;
  isolation: isolate;
  aspect-ratio: 3 / 2;      /* landscape thumbnail (was a tall portrait) */
  min-height: 0;
  display: flex;
  align-items: flex-end;
  cursor: pointer;
  background: var(--ink);
  will-change: transform;
}
.more-panel > img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.9s cubic-bezier(0.22, 1, 0.36, 1), filter 0.55s ease;
}
.more-panel:hover > img { transform: scale(1.04); }
.panel-shade {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(to top, rgba(22, 16, 11, 0.92) 0%, rgba(22, 16, 11, 0.55) 26%, rgba(22, 16, 11, 0) 58%);
  z-index: 1;
}
/* panel typography = the .card-title/.card-tag/.card-cta system of the Selected
   Work cards, scaled to the smaller frame. A panel is ~58% of a work-card's
   height, so the display title takes that factor; the small caps (tag/CTA) scale
   more gently (~0.9) so they stay legible — standard optical scaling. */
.panel-info {
  position: relative;
  z-index: 2;
  width: 100%;
  padding: 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
/* the tag moves to the FILM-FRAME CORNER, same mono voice as .card-no/.card-cat
   on the work cards — scaled a notch down for the smaller frame */
.more-panel::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 22%;
  background: linear-gradient(to bottom, rgba(22, 16, 11, 0.5), transparent);
  border-radius: inherit; pointer-events: none; z-index: 1;
}
.more-panel > .card-tag {
  position: absolute; top: 1.9rem; right: 2rem; z-index: 2; margin: 0;
  font-family: var(--v4-mono), 'Space Mono', monospace;
  font-size: 1.02rem; font-weight: 400; letter-spacing: 0.2em; text-transform: uppercase;
  color: rgba(247, 243, 236, 0.62);
  text-shadow: 0 1px 14px rgba(0, 0, 0, 0.8);
}
.panel-info h3 {
  font-family: var(--v4-gro);
  font-weight: 400;                /* = .card-title as RENDERED (the SUSE-display group rule) */
  font-size: clamp(2.6rem, 4.6vw, 4.6rem);   /* card-title 6.9rem × ~0.66 — legible, not shrunken */
  line-height: 1.02;
  letter-spacing: -0.02em;
  color: var(--ivory);
  margin: 0 0 0.8rem;
  text-shadow: 0 2px 30px rgba(0, 0, 0, 0.5);   /* = .card-title */
}
.panel-cta {
  display: inline-block;
  margin-top: 1.2rem;
  font-family: var(--v4-gro);
  font-size: 0.88rem;              /* card-cta 0.92rem × ~0.95 */
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ivory);
  background: transparent;
  border: 1.5px solid rgba(231, 191, 140, 0.95);
  border-radius: 100px;
  padding: 0.68rem 1.5rem;
  transition: background 0.35s ease, color 0.35s ease, border-color 0.35s ease,
              transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
  transform-origin: center;
}
.panel-cta::after { content: '  →'; white-space: pre; }

/* ---------- JOURNEY ---------- */

.journey { background: var(--linen); padding: 8.5vh 6vw 9vh; }

.journey-grid {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  gap: 7vw;
  max-width: min(1480px, 92vw);
  margin: 0 auto;
}
.journey-left { position: sticky; top: 16vh; align-self: start; }
.journey-left h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.2rem, 4.4vw, 3.8rem);
  line-height: 1.05;
  margin: 1.6rem 0 1.4rem;
}
.journey-lore { color: var(--walnut); line-height: 1.85; max-width: 38ch; }

.journey-right { position: relative; }
.journey-line {
  position: absolute;
  left: -2.6rem;
  top: 1vh;
  bottom: 1vh;
  width: 1px;
  background: var(--dust);
}
.journey-line-fill {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, var(--ember-lite), var(--ember));
  transform-origin: top;
  transform: scaleY(0);
  box-shadow: 0 0 8px rgba(212, 166, 110, 0.5);
}

.milestone {
  padding: 2.2vh 0;
  border-bottom: 1px solid var(--dust);
  cursor: pointer;
  transition: padding-left 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.milestone:hover { padding-left: 0.8rem; }
.ms-plus { opacity: 0.55; }
.milestone:hover .ms-plus { opacity: 1; }
.milestone:last-child { border-bottom: none; }
.ms-head { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; }
.ms-plus {
  font-family: var(--font-display);
  font-size: 1.7rem;
  font-weight: 300;
  color: var(--ember);
  line-height: 1;
  flex-shrink: 0;
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.milestone.open .ms-plus { transform: rotate(45deg); }
.ms-body { overflow: hidden; }
.ms-body p { padding-top: 1.1rem; }
.ms-year {
  font-family: var(--font-body);
  font-size: 0.65rem;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  color: var(--ember);
}
.milestone h3 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.5rem, 2.5vw, 2.1rem);
  margin: 0.5rem 0 0;
  line-height: 1.1;
}
.milestone p:last-child { color: var(--walnut); line-height: 1.85; max-width: 52ch; }

/* ---------- ABOUT ---------- */

.about { background: var(--ivory); padding: 20vh 6vw; }

.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 40.9rem) 1fr;
  gap: 5.5vw;
  max-width: min(115.5rem, 92vw);
  margin: 0 auto;
  align-items: center;
}
/* fixed aspect ratio so the box always has a real height to crop against —
   without it, height:100% on the img has nothing to resolve against and the
   "crop" silently does nothing, letting the photo's own shape dictate the box. */
.about-photo {
  border-radius: 16px; overflow: hidden; aspect-ratio: 4 / 5;
  /* studio backdrop behind the transparent portrait cut-out — neutral silver,
     same family as the intro/about-hero wall */
  background: radial-gradient(120% 90% at 40% 24%, #cfcecb 0%, #b3b2af 48%, #8d8d8b 100%);
}
.about-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 22%;
  /* same size step as .abh-person (png6 export — carries its own grade, no CSS lift) */
  transform: scale(1.07) translateX(-.1%);
  transform-origin: 50% 100%;
  will-change: transform;
}

.about-text h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 3.8vw, 3.2rem);
  line-height: 1.08;
  margin: 1.6rem 0 1.8rem;
}
.about-text p { color: var(--walnut); line-height: 1.9; max-width: 58ch; margin-bottom: 1.4rem; }

/* ---------- FINALE ---------- */

.finale {
  position: relative;
  /* a normal viewport-height section — NO pin / no dead-scroll. The square forms
     during the natural scroll-in (driven from the fixed frame in main.js) and is
     settled exactly when the section reaches the bottom of the page. */
  min-height: 100vh;
  background: var(--hero-ink);
}
/* the pinned stage: holds the viewport while you scroll the extra height, so the
   text stays dead-centre while the frame shrinks around it (as in the reference) */
/* dvh as a live-updating second declaration (vh stays as the fallback for
   browsers that don't parse dvh — they just ignore the second line): on an
   iPhone, forced overscroll past the end of the page can bring Safari's
   chrome back on screen mid-gesture, which shrinks the REAL viewport. A
   static 100vh doesn't know that happened, so this sticky frame stayed the
   old (taller) size and the centred content inside it drifted off-centre.
   100dvh recalculates continuously with the real visible height, so the
   frame — and everything centred inside it — tracks the chrome instead of
   ignoring it. Doesn't touch the normal "scroll to the end, chrome already
   settled" case at all: there dvh and vh already agree. */
.finale-stick { position: sticky; top: 0; height: 100vh; height: 100dvh; overflow: hidden; }
/* FIXED to the viewport (not just the finale) so the dim covers the whole screen —
   the section above (the brief) darkens together with the finale, no hard cut at the
   seam. It only paints while the square exists (glow > 0), so it's inert elsewhere. */
.finale-atmos { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
.finale-inner { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; padding: 12vh 3.4vw 4.5vh; }
/* the pull-cord easter-egg is hidden on this page (it left a stray cord top-right) */
.finale .pull-cord { display: none; }
.finale .page-head { display: none; }
/* left-aligned to the ONE shared column: the connect-frame hairline (4vw),
   which the wordmark top-left shares too. Vertically CENTRED on the LinkedIn
   button bottom-right: same 3vh baseline + half the button's extra height
   (its .5em padding + bigger type ≈ 2rem taller than this line). */
.finale .site-credit { position: absolute; left: 4vw; bottom: calc(3vh + 1rem); margin: 0; z-index: 3; }
/* ---- the viewfinder FRAME — scrubbed by --p: 0 = full-screen hairline,
   1 = settled inset square. main.js writes --p from scroll. ---- */
.connect-frame {
  /* FIXED to the viewport (not the section) so it forms in place during the natural
     scroll-in — no pin needed. Wide landscape frame; top/left/right stay put, only
     the bottom reaches a little lower so the square is a touch taller. */
  position: fixed; z-index: 1; pointer-events: none;
  /* dvh, same reason as .finale-stick/.finale: this frame's top/bottom
     insets are a % of viewport height, so if the real viewport shrinks
     (Safari's chrome reappearing on a forced overscroll) a static vh value
     leaves it inset wrong relative to the NEW visible edges */
  top: calc(var(--p, 1) * 14dvh);
  bottom: calc(var(--p, 1) * 9dvh);
  left: calc(var(--p, 1) * 4vw);
  right: calc(var(--p, 1) * 4vw);
  /* fixed hairline colour; the FADE is driven by the element's opacity from main.js
     (calc-in-rgba-alpha isn't reliably evaluated here). opacity 0 at rest → 1 when
     settled, so the line fades cleanly in/out as the square forms and dissolves —
     never a hard line sweeping across the sections above on the way back up. */
  border: 1px solid rgba(247, 243, 236, 0.32);
  border-radius: calc(var(--p, 1) * 34px);
}
/* ---- contact = framed "let's connect" composition (ref) ---- */
.connect-card {
  flex: 1; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
}
.connect-kicker {
  font-family: var(--v4-mono), 'Space Mono', monospace;
  font-size: clamp(1.08rem, 1.45vw, 1.42rem); letter-spacing: .2em; text-transform: uppercase;
  color: rgba(247, 243, 236, 0.6); margin-bottom: 2.8rem;
  display: inline-flex; align-items: center; gap: .6em;
}
.finale .connect-title {
  font-family: var(--font-display); font-weight: 400; font-style: normal; text-transform: uppercase;
  font-size: clamp(3.6rem, 11.4vw, 11.85rem); line-height: .92; letter-spacing: -.012em;
  color: #fff; margin: 0;
}
/* the e-mail pill = the same round button as the HERO page pill ("Get in touch"):
   bordered pill, gold gradient fill that wipes in on hover with a diagonal highlight
   sweep. Click COPIES the address: the mail row rolls up, "Copied ✓" rolls in and the
   gold fill flashes as the receipt (.copied, set from main.js), then it rolls back. */
.connect-write {
  margin-top: 3.1vh;
  display: inline-flex; flex-direction: row; gap: .4em; align-items: center;
  font-family: var(--v4-gro); font-weight: 500;
  font-size: clamp(1.35rem, 1.9vw, 1.9rem); letter-spacing: .05em; text-transform: none;
  color: rgba(244, 236, 224, .9); text-decoration: none;
  background: none; cursor: pointer;
  border: 1px solid rgba(244, 236, 224, .38); border-radius: 100px; padding: 1.05rem 2.3rem;
  position: relative; overflow: hidden; isolation: isolate;
  transition: color .36s ease, border-color .36s ease, box-shadow .5s ease;
  /* permanent own compositor layer: the first hover promoted the button (the
     gold ::before starts animating) and the icon re-rasterised half a pixel
     lower — the hover raster IS the right position, so render there always */
  will-change: transform;
}
/* the copy glyph in front of the address — the always-visible "click = copy" cue.
   It lives OUTSIDE .cw-swap (one static icon, text-only rows) so it holds
   perfectly still while the words roll; same -.07em optical lift as the swap.
   On .copied it fades out — the check in the rolled-in row is the receipt. */
.connect-write .cw-copy {
  width: .8em; height: .8em; flex: none; opacity: .7; margin-right: .1em;
  /* no optical lift here — the flex centring is already right for the icon
     (the -.07em lift is a TEXT correction and pushed the glyph too high) */
  transition: opacity .3s ease;
  /* OWN compositor layer: the icon sits on a fractional y; when hover started
     animating the gold fill + its colour, it re-rasterised and pixel-snapped
     a hair lower. On its own layer its raster position is decided once and
     never changes, whatever the rest of the pill is doing. */
  will-change: transform;
}
.connect-write.copied .cw-copy { opacity: 0; }
/* the two rows roll through a clipped window (same move as the nav pills);
   the tiny lift is optical centring — the grotesk sits low in its line box and
   the all-lowercase address hangs its g-descenders below, so dead-centre reads
   as "too low" inside the pill */
.connect-write .cw-swap { display: block; position: relative; overflow: hidden; transform: translateY(-.07em); }
.connect-write .cw-row {
  display: inline-flex; align-items: center; justify-content: center; gap: .45em;
  width: 100%; white-space: nowrap;
  transition: transform .55s cubic-bezier(.22, 1, .36, 1);
  /* pre-promoted layer: without it the first hover promoted the rows on the
     fly and the text re-rasterised with a tiny visible hop */
  will-change: transform;
}
/* the rolled-in rows centre on the BUTTON, not on the swap: the swap sits
   right of the static icon (.8em + .1em margin + .4em gap = 1.3em), so true
   button-centre is half that (.65em) left of swap-centre */
.connect-write .cw-hover, .connect-write .cw-done { position: absolute; left: -.65em; top: 100%; }
.connect-write .cw-done svg { width: .9em; height: .9em; }
/* hover rolls the address away and "Copy e-mail" in — the pill explains its
   own click, no caption needed; a click rolls to "Copied ✓" instead (.copied
   outranks hover: the receipt must not flip back while the cursor rests) */
.connect-write .cw-done { visibility: hidden; }
.connect-write.copied .cw-done { visibility: visible; }
.connect-write.copied .cw-hover { visibility: hidden; }
.connect-write:hover .cw-row, .connect-write.copied .cw-row { transform: translateY(-100%); }
/* copied = the gold receipt, no hover needed */
.connect-write.copied { color: #241704; border-color: #f3d27a; }
.connect-write.copied::before { transform: translateX(0); }
/* the line under the pill: the mailto link as the quiet escape hatch */
.connect-meta {
  margin: 1.2rem 0 0;
  font-family: var(--v4-mono), 'Space Mono', monospace;
  font-size: clamp(.85rem, 1.05vw, 1.15rem); letter-spacing: .14em; text-transform: uppercase;
  color: rgba(247, 243, 236, .55);
}
.connect-alt {
  font-size: inherit; letter-spacing: inherit;
  color: rgba(247, 243, 236, .5); text-decoration: none;
  border-bottom: 1px solid transparent; padding-bottom: 2px;
  transition: color .3s ease, border-color .3s ease;
}
.connect-alt:hover { color: rgba(247, 243, 236, .92); border-color: rgba(247, 243, 236, .45); }
.connect-write::before {
  content: ''; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
  background: linear-gradient(100deg, #f6d985 0%, #d9ab43 38%, #b8862f 60%, #f1cd70 100%);
  transform: translateX(-101%); transition: transform .5s cubic-bezier(.45,.05,.2,1);
}
.connect-write::after {
  content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 55%; z-index: -1; border-radius: inherit;
  background: linear-gradient(100deg, transparent 0%, rgba(255,255,255,.7) 50%, transparent 100%);
  transform: translateX(-220%) skewX(-16deg);
  /* keep this sheen on its own compositor layer permanently — when the ambient
     glint animation started, the layer got promoted on the fly and the icon +
     address re-rasterised with a visible sub-pixel stutter */
  will-change: transform, opacity;
}
.connect-write:hover {
  color: #241704; border-color: #f3d27a;
  box-shadow: 0 0 0 1px rgba(243,210,122,.55), 0 10px 34px rgba(214,160,55,.5), 0 0 46px rgba(246,217,133,.4);
}
.connect-write:hover::before { transform: translateX(0); }
.connect-write:hover::after { transform: translateX(320%) skewX(-16deg); transition: transform .85s cubic-bezier(.45,.05,.2,1) .12s; }
/* ambient glint: the same diagonal highlight the hover uses drifts softly
   across the pill by itself — but only once you've actually ARRIVED: main.js
   adds .glint after you've been parked at the very bottom of the page for ~1s
   (not the instant the finale edge scrolls in). Slow glide (~2.4s) on an 8s
   cycle, dimmer than the hover sweep: it should read as the room's light
   catching the button, not as a call-to-action flash. Hover/copied suspend
   it — their own brighter sweep takes over. */
.connect-card.lit.glint .connect-write:not(:hover):not(.copied)::after {
  animation: cwGlint 8s cubic-bezier(.33,.02,.25,1) .35s infinite both;
}
@keyframes cwGlint {
  0%        { transform: translateX(-220%) skewX(-16deg); opacity: 0; }
  8%        { opacity: .3; }
  22%       { opacity: .3; }
  30%       { transform: translateX(320%) skewX(-16deg); opacity: 0; }
  100%      { transform: translateX(320%) skewX(-16deg); opacity: 0; }
}
html.reduced .connect-card .connect-write::after { animation: none; }

/* the "currently seeking a graduation internship" dot now lives in the kicker
   line itself (replacing "Got a project in mind?") instead of a separate line */
.ca-dot{
  width:8px; height:8px; border-radius:50%; background:#8fd49a; flex-shrink:0;
  box-shadow:0 0 10px rgba(143,212,154,.7); animation: caPulse 2.4s ease-in-out infinite;
}
@keyframes caPulse{ 0%,100%{ opacity:.45; transform:scale(.82); } 50%{ opacity:1; transform:scale(1); } }
html.reduced .ca-dot{ animation:none; opacity:.9; }
/* footer is now a single LinkedIn button, centred, styled exactly like the
   Projects / About nav buttons (rolling ivory fill on hover) */
/* LinkedIn sits BOTTOM-right — below the square — right-aligned to the "Get in touch"
   pill (same 5.4vw column), in the gap under the rectangle */
.connect-foot { position: absolute; bottom: 3vh; right: 5.4vw; z-index: 4; }
.connect-linkedin {
  display: inline-flex; align-items: center;
  font-family: var(--v4-gro); font-size: 1.46rem; font-weight: 400;
  letter-spacing: .04em; text-transform: uppercase; color: #fff; padding: .5em 1em;
  position: relative; overflow: hidden; isolation: isolate; border-radius: 100px;
  background: transparent; text-decoration: none; transition: color .3s ease;
}
.connect-linkedin::before {
  content: ''; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: #f4ece0;
  transform: translateY(101%); transition: transform .4s cubic-bezier(.5,0,.12,1);
}
.connect-linkedin:hover { color: #141414; }
.connect-linkedin:hover::before { transform: translateY(0); }
.connect-linkedin:hover .np-roll { transform: translateY(-100%); }

/* ---- LIGHT PAYOFF: the curiosity-light you carried the whole site is poured
   into the contact card, which ignites with the same cool grey-white light as the
   project-card backlight. The bloom itself is drawn on the .finale-atmos canvas
   (grain-dithered, so it never bands) and ramps up when JS adds .lit on arrival
   — see main.js finale atmosphere + hero-v4.js spark. ---- */
.connect-card > *:not(.connect-frame) { position: relative; z-index: 1; }
.connect-frame { transition: border-color 1s ease; }
/* no glow box-shadow on the frame — the bloom was casting soft rounded lines
   around the square that read as an artefact. Just brighten the hairline. */
.connect-card.lit .connect-frame {
  border-color: rgba(236, 234, 230, .42);
}
.finale .connect-title { transition: color .8s ease, text-shadow .8s ease; }
.connect-card.lit .connect-title {
  color: #f6f5f2;
  /* softer glow — gentler bloom on the headline */
  text-shadow: 0 0 18px rgba(236, 234, 230, .26), 0 0 46px rgba(236, 234, 230, .15);
}
.connect-kicker { transition: color .8s ease; }
.connect-card.lit .connect-kicker { color: rgba(244, 243, 240, .85); }

.finale h2 {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(2.2rem, 5.2vw, 4.6rem);
  line-height: 1.08;
  color: var(--ivory);
  margin: 2.4rem 0 2rem;
}
.finale-text { color: var(--taupe); line-height: 1.9; max-width: 56ch; margin-bottom: 5vh; }

.finale-mail {
  font-family: var(--font-display);
  font-weight: 400;
  font-style: italic;
  font-size: clamp(1.5rem, 3.4vw, 2.8rem);
  color: var(--ivory);
  text-decoration: none;
  border-bottom: 1px solid var(--ember);
  padding-bottom: 6px;
  transition: color 0.3s;
}
.finale-mail:hover { color: var(--ember-lite); }

.finale-meta {
  margin-top: 5vh;
  display: flex;
  align-items: center;
  gap: 2.4rem;
  font-family: var(--font-body);
  font-size: 0.62rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.finale-meta a { color: var(--taupe); text-decoration: none; border-bottom: 1px solid rgba(168, 153, 138, 0.4); padding-bottom: 3px; transition: color 0.3s; }
.finale-meta a:hover { color: var(--ivory); }
.avail { color: var(--taupe); display: flex; align-items: center; gap: 0.6rem; }
.avail-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: #7da068;
  animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* the pull cord: switch the source off and travel back to the beginning */
.pull-cord {
  position: relative;
  z-index: 1;
  margin-top: 10vh;
  background: none;
  border: none;
  cursor: pointer;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  padding: 0;
}
.pc-cord {
  display: flex;
  flex-direction: column;
  align-items: center;
  transform-origin: top center;
  will-change: transform;
}
.pc-line {
  display: block;
  width: 2px;
  height: 74px;
  background: linear-gradient(to bottom, transparent, rgba(212, 166, 110, 0.7));
}
.pc-knob {
  width: 12px;
  height: 17px;
  border-radius: 5px 5px 7px 7px;
  background: linear-gradient(180deg, #d4a66e, #9b6b3a);
  box-shadow: 0 0 14px rgba(212, 166, 110, 0.35);
  margin-top: -1px;
  transition: box-shadow 0.35s;
}
.pc-label {
  margin-top: 18px;
  font-family: var(--font-body);
  font-size: 0.62rem;
  letter-spacing: 0.35em;
  text-indent: 0.35em;
  text-transform: uppercase;
  color: var(--taupe);
  transition: color 0.3s;
}
.pull-cord:hover .pc-label { color: #d4a66e; }
.pull-cord:hover .pc-knob { box-shadow: 0 0 22px rgba(212, 166, 110, 0.6); }

/* ---------- LIGHTS OUT (pull-cord easter egg) ---------- */
.blackout {
  position: fixed;
  inset: 0;
  z-index: 88;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 1.15s cubic-bezier(0.4, 0, 0.2, 1), visibility 1.15s cubic-bezier(0.4, 0, 0.2, 1);
  background: #080605;            /* near-pitch-black, a hair of warmth */
}
.blackout.on { opacity: 1; visibility: visible; pointer-events: auto; cursor: none; }
.blackout.on * { cursor: none; }
/* the light you carry — the only thing that lifts the dark */
.blackout-lamp {
  position: absolute;
  top: 0; left: 0;
  width: 440px; height: 440px;
  margin: -220px 0 0 -220px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(231, 196, 142, 0.17), rgba(184, 132, 74, 0.05) 45%, transparent 70%);
  transform: translate(var(--gx, 50vw), var(--gy, 32vh));
  pointer-events: none;
  will-change: transform;
}
/* the note hides in the dark — only what's under your light becomes readable */
.blackout-msg {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 1.7rem;
  padding: 24vh 8vw 0;
  text-align: center;
  pointer-events: none;
  -webkit-mask-image: radial-gradient(circle 200px at var(--gx, 50vw) var(--gy, 32vh), #000 0%, #000 32%, transparent 70%);
  mask-image: radial-gradient(circle 200px at var(--gx, 50vw) var(--gy, 32vh), #000 0%, #000 32%, transparent 70%);
}
.bo-line {
  font-family: var(--font-garamond);
  font-weight: 600;
  font-size: clamp(2rem, 4.6vw, 3.1rem);
  line-height: 1.1;
  letter-spacing: 0.01em;
  color: var(--ivory);
  margin: 0;
}
.bo-sub {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(0.98rem, 1.4vw, 1.12rem);
  line-height: 1.7;
  color: #e7d6ba;
  max-width: 32rem;
  margin: 0;
}
.bo-end {
  font-family: var(--font-body);
  font-style: italic;
  font-size: clamp(1rem, 1.5vw, 1.2rem);
  color: #efe2cb;
  margin: 0.2rem 0 0;
}
/* the cord in the dark — identical pull to the finale cord, glowing so it can
   be found; pulling it carries you back up to the top */
.blackout-cord {
  position: absolute;
  left: 50%; top: 0;
  transform: translateX(-50%);
  background: none; border: none; padding: 0;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  pointer-events: auto;
}
.boc-cord {
  display: flex;
  flex-direction: column;
  align-items: center;
  transform-origin: top center;
  will-change: transform;
}
.boc-line {
  display: block;
  width: 2px; height: 74px;
  background: linear-gradient(to bottom, transparent, rgba(212, 166, 110, 0.7));
}
.boc-knob {
  width: 12px; height: 17px;
  border-radius: 5px 5px 7px 7px;
  background: linear-gradient(180deg, #d4a66e, #9b6b3a);
  box-shadow: 0 0 18px rgba(212, 166, 110, 0.55);
  margin-top: -1px;
}
.boc-label {
  margin-top: 18px;
  font-family: var(--font-body);
  font-size: 0.62rem;
  letter-spacing: 0.35em;
  text-indent: 0.35em;
  text-transform: uppercase;
  color: var(--taupe);
}
@media (prefers-reduced-motion: reduce) and (min-width:99999px){
  .blackout { transition: opacity 0.25s ease, visibility 0.25s ease; }
}

.site-credit {
  position: relative;
  z-index: 1;
  margin-top: 7vh;
  font-family: var(--font-body);
  /* px-clamp, NOT rem: on ≥1024px screens the root font scales down to ~10px
     (the canvas-scale rule), which silently shrank this line to ~8px */
  font-size: clamp(9px, 0.72vw, 12px);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(168, 153, 138, 0.55);
}

/* ---------- CASE OVERLAY ---------- */

.case-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--ivory);
  opacity: 0;
}
.case-overlay[hidden] { display: none; }

/* ===== CASE BACKGROUND =====
   The same domain-warped flow as the landing hero, but INVERTED: a light ivory
   field is dominant and soft grey waves move through it (rendered in case-bg.js).
   Each project gets its own wave shape via a per-case seed. The linen colour is
   only a fallback for the instant before the WebGL canvas paints. */
.case-atmos {
  position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
  background: #26262a; /* dark-grey fallback (matches the flow's base) until the canvas paints */
}
.case-atmos-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
}
.case-scroll { position: relative; z-index: 1; }

.case-close {
  position: fixed;
  bottom: 1.7rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 65;
  width: 64px;
  height: 64px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
/* the dot is a clear target and grows on hover — it never needs a focus ring
   (it's auto-focused when a case opens, which is what drew the box) */
.case-close:focus, .case-close:focus-visible { outline: none; }
.cc-dot {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: linear-gradient(140deg, var(--ember), var(--lamp));
  display: grid;
  place-items: center;
  /* at rest: NO glow — the drop shadow is neutral black (not amber-tinted,
     which read as a glow of its own and invited the click too loudly); the
     halo appears only on hover. Transparent stop keeps the shadow list
     shape identical so the hover glow FADES in via the transition */
  box-shadow: 0 14px 34px -12px rgba(0, 0, 0, 0.75),
              0 0 26px color-mix(in srgb, var(--ember) 0%, transparent),
              inset 0 0 0 1px rgba(255, 240, 214, 0.18);
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.4s;
}
.cc-x {
  position: relative;
  width: 19px;
  height: 19px;
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.cc-x::before, .cc-x::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 1.6px;
  background: var(--ivory);
  border-radius: 2px;
}
.cc-x::before { transform: translateY(-50%) rotate(45deg); }
.cc-x::after { transform: translateY(-50%) rotate(-45deg); }
/* "Close" set like inbuildamsterdam: sentence-case, vertical, reading
   bottom-to-top — but parked top-right of the dot (between 12 and 3 o'clock)
   rather than straight right */
.cc-label {
  position: absolute;
  left: 70px;
  top: 16px;
  transform: translate(-50%, -50%) rotate(-90deg);
  transform-origin: center;
  white-space: nowrap;
  font-family: var(--font-body);
  font-size: 0.66rem;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: none;
  color: var(--ember);
  transition: color 0.3s;
}
/* interactive but static — the dot grows, the word warms; nothing drifts */
.case-close:hover .cc-label { color: var(--ember); }
.case-close:hover .cc-dot {
  transform: scale(1.08);
  box-shadow: 0 18px 42px -12px rgba(196, 137, 58, 0.85),
              0 0 38px color-mix(in srgb, var(--ember) 58%, transparent),
              inset 0 0 0 1px rgba(255, 240, 214, 0.3);
}
.case-close:hover .cc-x { transform: rotate(90deg); }

/* EVERY CASE — the close dot starts UNFILLED: just a hairline ring and the
   gold ×, so it barely veils the cover and you see more of the page. The amber
   fill (a ::before that fades in), the glow and the scale only bloom on hover —
   the exact look the solid dot has at rest elsewhere. Was piloted on treinsafari
   via :has(); now scoped to the whole overlay so every case shares it. */
#case-overlay .cc-dot {
  position: relative;
  background: transparent;
  /* the ring is white (not the warm ivory) and a touch stronger so the unfilled
     dot reads clearly against the cover — still soft, not a hard solid white */
  box-shadow: 0 10px 26px -14px rgba(0, 0, 0, 0.5),
              inset 0 0 0 1px rgba(255, 255, 255, 0.52);
}
#case-overlay .cc-dot::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: linear-gradient(140deg, var(--ember), var(--lamp));
  opacity: 0;
  transition: opacity 0.45s ease;
}
/* keep the × above the fading fill layer */
#case-overlay .cc-x { position: relative; z-index: 1; }
/* hover → bloom to full colour + the amber glow (scoped so it outranks the
   unfilled rest state above); scale + × rotate come from the global hover rules */
#case-overlay .case-close:hover .cc-dot::before { opacity: 1; }
#case-overlay .case-close:hover .cc-dot {
  box-shadow: 0 18px 42px -12px rgba(196, 137, 58, 0.85),
              0 0 38px color-mix(in srgb, var(--ember) 58%, transparent),
              inset 0 0 0 1px rgba(255, 240, 214, 0.3);
}
/* the × itself: gold-yellow at rest (the accent reads through the empty ring),
   turning ivory-white on hover once the gold fill blooms in behind it */
#case-overlay .cc-x::before,
#case-overlay .cc-x::after {
  /* exact match to the accent copy's gold (#f6d98a): --ember-lite is redefined
     brighter inside the case scope, but the close button is a sibling of the
     case and only sees the darker root value — so hardcode the literal here */
  background: #f6d98a;
  transition: background 0.4s ease;
}
#case-overlay .case-close:hover .cc-x::before,
#case-overlay .case-close:hover .cc-x::after {
  background: var(--ivory);
}

.case-scroll { height: 100%; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; }

.case-head {
  position: relative;
  /* cap the cover in px (tuned to the ~1800×1130 laptop) so a taller external
     monitor doesn't balloon it — the proportions match the laptop reference */
  height: min(88vh, 1000px);
  /* overflow VISIBLE so the title can drop below the cover's bottom line and sit
     on the foreground (over the body's dip-to-black) without being clipped. The
     photo + shade are clipped instead by their own .case-head-media wrapper. */
  overflow: visible;
  display: flex;
  align-items: flex-end;
}
/* clips the photo/shade to the cover box; the title lives OUTSIDE this, free to
   overhang the bottom edge. inset:0 = exactly the old .case-head clip box. */
.case-head-media {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
}
.case-head img {
  position: absolute;
  top: -10%;
  left: 0;
  width: 100%;
  height: 120%;
  object-fit: cover;
  will-change: transform;
  /* the open animation scales this from 1.06 → 1. Scaling from the CENTRE pushed
     the bottom of the mask below the header's clip for the length of that tween,
     so the photo showed a hard cut and the dissolve only appeared once the zoom
     settled. Anchored at the bottom, the fade stays put while the photo eases in. */
  transform-origin: bottom center;
  /* NO cover-melt on any case, at any width (the ikea look, rolled out to all):
     the photo ends on a clean straight edge — clipped by the header's own
     overflow:hidden — and the .case-head-shade dip-to-black provides the fade
     over the bottom. The old soft dissolve-to-transparent used to leave the
     moving atmos field bleed through the photo's edge (the "smear" Geert hated).
     The ≥861px override that also killed the mask is now redundant but harmless. */
  -webkit-mask-image: none;
          mask-image: none;
}
.case-head-shade {
  position: absolute;
  inset: 0;
  /* neutral dark (matches the grey flow, not warm brown): backs the title in the
     lower third, then fades to nothing at the very bottom edge so there's no hard
     line where the photo dissolves into the field */
  background: linear-gradient(to top,
    rgba(16, 16, 19, 0.92) 0%, rgba(16, 16, 19, 0.78) 14%, rgba(16, 16, 19, 0.45) 34%,
    rgba(16, 16, 19, 0.14) 58%, transparent 78%);
}
/* scroll-driven dim: flat black wash whose opacity main.js drives 0→1 as the
   cover scrolls past (see caseScroll's scroll handler) — near-black by the
   time it leaves the top, on top of the shade's fixed title-backing gradient */
.case-head-dim {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0;
  pointer-events: none;
}
.case-head-info {
  position: relative;
  width: 100%;
  max-width: 1200px;
  /* flush to the RIGHT edge of the cover, not centred in a column: margin-left
     auto pushes the whole block right; the padding-right below is the only
     gutter left to the screen edge (mobile block re-centres it) */
  margin: 0 0 0 auto;
  padding: 0 6vw 4.3vh;
  text-align: right;
}
.case-head-info .case-meta { color: var(--ember-lite); }
.case-head-info h2 {
  font-family: var(--font-display);
  font-weight: 500;
  /* bigger overall (was 9vw / cap 7.9rem): the two-line cover title now fills
     the width with real presence. Measured safe — the widest lines ("Dafi Time
     & Bags", the bold "Photography & Film" kicker) still clear the gutter. */
  font-size: clamp(3.8rem, 11vw, 9.6rem);
  line-height: 1.0;
  letter-spacing: -0.015em;
  color: var(--ivory);
  text-shadow: 0 2px 36px rgba(0, 0, 0, 0.5);
}

.case-body { padding: 7vh 0 0; }

.case-section {
  display: grid;
  grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 6rem);
  max-width: 1440px;
  margin: 0 auto 6vh;
  padding: 0 5vw;
  align-items: start;
}
.case-section h3 { margin: 0 !important; }
.cs-text p:last-child { margin-bottom: 0; }
/* a second paragraph needs a real break — the inherited .85rem reads as a loose
   line at this body size (44px/50px), not as a new thought */
.cs-text p + p { margin-top: 1.8rem; }

.case-divider-label {
  font-family: var(--font-body);
  font-size: 0.92rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ember);
  text-align: center;
  margin: 8vh auto 4vh;
  max-width: 1280px;
  padding: 0 6vw;
}

.case-duo {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2.5vw, 2.5rem);
  max-width: 1560px;
  margin: 0 auto 6vh;
  padding: 0 6vw;
}
.case-duo figure, .case-trio figure { margin: 0; }
.case-duo img, .case-trio img { width: 100%; border-radius: 10px; }
.case-duo figcaption, .case-trio figcaption {
  font-family: var(--font-body);
  font-size: 0.82rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--taupe);
  margin-top: 1rem;
  text-align: center;
  line-height: 1.45;
}

.case-trio {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  align-items: start;
  gap: clamp(1rem, 2vw, 2rem);
  max-width: 1680px;
  margin: 0 auto 6vh;
  padding: 0 5vw;
}

/* Amstel Gold Race: an editorial block — the tall portrait stands as a feature
   beside the two landscapes stacked. The landscapes hold their native 3:2, and
   the portrait spans both rows so it lands at ~3:4, near its own ratio: large,
   balanced, almost no crop. */
.case-agr {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2.4vw, 2.4rem);
  max-width: 1500px;
  margin: 0 auto 6vh;
  padding: 0 5vw;
}
.case-agr figure { margin: 0; }
.case-agr figure:not(.agr-tall) { aspect-ratio: 3 / 2; }
.case-agr .agr-tall { grid-column: 2; grid-row: 1 / span 2; }
.case-agr img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 10px;
}

/* Klassiekers: full-bleed split — two videos on facing colour fields */
.kl-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  margin: 9vh 0;
}
.kl-half {
  min-height: 96vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2.4rem;
  padding: 9vh 6vw;
}
/* no colour fields — the clips float directly on the site's dark, amber-lit
   background; the cinematic weight comes from the frame's shadow instead */
.kl-warm, .df-warm,
.kl-cool, .df-cool { background: transparent; }
/* wrapper clips the video to clean rounded corners (Safari won't clip a bare
   <video>'s own corners, so the square edges poke through the radius) */
.kl-frame {
  display: inline-block;
  line-height: 0;
  /* lock the frame to the clips' own 9:16 so nothing gets letterboxed. Sized to
     carry the panel on any screen: width-driven with a firm px cap, so it stays
     large and identical whether on the laptop or an external monitor (no vh
     shrink on the shorter laptop viewport). */
  width: min(84%, 62vh, 540px);
  aspect-ratio: 9 / 16;
  border-radius: 14px;
  overflow: hidden;
  background: #000;
  /* cinematic lift on the dark background: hairline edge so the black frame
     doesn't dissolve into the page, two shadow layers for depth, and a faint
     warm halo underneath — like screen-light spilling off a cinema screen */
  box-shadow:
    0 0 0 1px rgba(247, 243, 236, 0.08),
    0 30px 60px -18px rgba(0, 0, 0, 0.8),
    0 70px 150px -40px rgba(0, 0, 0, 0.9),
    0 25px 110px -25px rgba(196, 137, 58, 0.22);
}
.kl-frame video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* a hair of zoom swallows the thin black edge baked into the source clips
     (the right side of the bloedvlek) — no top/bottom bars, perfectly clean */
  transform: scale(1.025);
  border-radius: 0;
}
.kl-half .kl-caption {
  font-family: var(--font-body);
  font-size: 0.62rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(247, 243, 236, 0.7);
  max-width: 40ch;
  text-align: center;
  line-height: 1.9;
}
.kl-half .kl-caption strong { color: var(--ivory); font-weight: 700; }

/* full-bleed screening room: the film centred on the page's own light flow
   (transparent so the inverted background shows through, no dark band) */
.case-cinema {
  position: relative;
  background: transparent;
  margin: 9vh 0;
  padding: clamp(3.5rem, 9vh, 7rem) 6vw;
  text-align: center;
}
.case-cinema::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 62% 85% at 50% 42%, rgba(33, 26, 20, 0.05), transparent 70%);
  pointer-events: none;
}
/* wrapper clips the video to clean rounded corners — a bare <video> ignores
   its own border-radius in Safari (same fix as .kl-frame), the square edges
   poke straight through unless something with overflow:hidden clips it */
.cinema-frame {
  position: relative;
  display: inline-block;
  line-height: 0;
  max-height: min(84vh, 960px);
  margin: 0 auto;
  border-radius: 32px;
  overflow: hidden;
  box-shadow: 0 40px 110px -35px rgba(0, 0, 0, 0.75);
}
.case-cinema video {
  position: relative;
  /* px cap (laptop reference) so it doesn't grow bigger on a taller monitor */
  max-height: min(84vh, 960px);
  width: auto;
  max-width: 100%;
  display: block;
}
.cinema-title{ margin-bottom:1.6rem; text-align:center; }
.cinema-caption {
  position: relative;
  font-family: var(--font-body);
  font-size: 0.55rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--taupe);
  margin-top: 2rem;
}
/* the Instagram link rides inside the caption, on the caption's scale */
.cinema-caption .case-link { font-size: inherit; letter-spacing: inherit; }
.cinema-kicker {
  position: relative;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.5rem, 2.4vw, 2.2rem);
  color: var(--ivory);
  text-align: center;
  max-width: none;
  margin: 3.5rem auto 0;
}
.cinema-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: clamp(1rem, 2.2vw, 2.4rem);
}
.cinema-row figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.1rem;
  max-width: 30%;
}
.cinema-row figure .single-frame { background: #000; box-shadow: 0 35px 90px -30px rgba(0, 0, 0, 0.7); }
.cinema-row figure video {
  height: min(62vh, 600px);
  width: auto;
  max-width: 100%;
}
.cinema-row figcaption {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-family: var(--font-body);
  font-size: 0.6rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--taupe);
  line-height: 1.7;
}
.cinema-row figcaption strong { color: var(--ivory); font-weight: 700; letter-spacing: 0.16em; }

.case-chapter { margin-bottom: 15vh; }

.case-meta {
  font-family: var(--font-body);
  font-size: 0.68rem;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  color: var(--ember);
  margin-bottom: 1.6rem;
}
.case-body h3 {
  font-family: var(--font-display);
  font-weight: 500;
  font-style: italic;
  font-size: clamp(2.3rem, 3.4vw, 3.5rem);
  color: var(--ink);
  line-height: 1.08;
}
.case-body p {
  color: var(--walnut);
  line-height: 1.24;
  margin-bottom: 0.85rem;
  max-width: 15em;
  font-size: clamp(1.9rem, 2.7vw, 2.9rem);
}
.case-body p strong { color: var(--ink); font-weight: 500; }

.case-media-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2.5vw, 2.5rem);
  margin: 6vh auto;
  max-width: 1440px;
  padding: 0 5vw;
}
.case-media-row.full { max-width: none; padding: 0; gap: 0.6vw; margin: 9vh 0; will-change: transform; }
.case-media-row.full img { border-radius: 0; height: auto; }
.case-media-row.wide { max-width: 1320px; }
.case-media-row.wide img { border-radius: 10px; }
.case-media-row.three { grid-template-columns: repeat(3, 1fr); }
.case-media-row.single { grid-template-columns: 1fr; text-align: center; justify-items: center; }
/* wrapper clips the rounded corners cleanly, including the native control
   strip, so no grey square pokes out past the radius */
.single-frame {
  display: inline-block;
  /* .case-media-row.single is a grid column, and grid items stretch to fill
     it by default regardless of display:inline-block — the video sized itself
     correctly to its own aspect ratio, but this wrapper still stretched to
     the full column width, showing as black bars either side of the video.
     fit-content stops the stretch; the video's own size is unchanged, only
     the wrapper now hugs it. */
  width: fit-content;
  line-height: 0;
  border-radius: 14px;
  overflow: hidden;
  max-width: 100%;
  background: #000;
  box-shadow: 0 36px 80px -38px rgba(33, 26, 20, 0.45);
}
.single-frame video {
  display: block;
  border-radius: 0;
  max-height: min(82vh, 940px);
  max-width: 100%;
  width: auto;
}
.case-media-row.vertical {
  grid-template-columns: repeat(auto-fit, minmax(240px, 400px));
  justify-content: center;
  max-width: none;
  background: var(--linen);
  padding: 10vh 6vw;
  gap: clamp(1.5rem, 3.5vw, 4rem);
  margin: 9vh 0;
}
.case-media-row.vertical video {
  border-radius: 16px;
  box-shadow: 0 36px 70px -34px rgba(33, 26, 20, 0.4);
}
.case-media-row.vertical video:nth-child(2) { transform: translateY(4vh); }
.case-media-row img, .case-media-row video { width: 100%; border-radius: 10px; }
img.reveal {
  clip-path: inset(100% 0 0 0);
  transform: scale(1.1);
  transition: clip-path 1.2s cubic-bezier(0.22, 1, 0.36, 1), transform 1.7s cubic-bezier(0.22, 1, 0.36, 1);
}
img.reveal.in { clip-path: inset(0 0 0 0); transform: none; }
.case-media-row.vertical video { aspect-ratio: 9 / 16; object-fit: cover; }
.case-media-row.single video { max-height: min(82vh, 940px); margin: 0 auto; width: auto; max-width: 100%; }

.case-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1.2rem;
  margin: 8vh auto;
  max-width: 1380px;
  padding: 0 5vw;
}
.case-stats div {
  background: var(--linen);
  border: 1px solid var(--dust);
  border-radius: 12px;
  padding: 2.2rem 1rem;
  text-align: center;
}
.case-stats strong {
  display: block;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: clamp(2rem, 3.8vw, 3.2rem);
  color: var(--ember);
  margin-bottom: 0.5rem;
}
.case-stats span {
  font-family: var(--font-body);
  font-size: 0.58rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--taupe);
}

.learn-label {
  display: block;
  font-family: var(--font-body);
  font-style: normal;
  font-size: 0.66rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--ember);
  margin-bottom: 1.4rem;
}
/* case-page testimonial — a large editorial pull-quote on the dark case body */
.case-quote{
  width:min(900px,88vw); margin:11vh auto 0; padding:0 6vw; text-align:center;
}
/* testimonial pull-quote: Cormorant italic (the logotype serif — the only
   loaded face with a REAL italic; SUSE/Telegraf would faux-slant) reads as a
   spoken aside instead of another body paragraph */
.case-quote p{
  font-family:var(--font-garamond); font-weight:600; font-style:italic;
  font-size:clamp(2.6rem,4.5vw,4.6rem); line-height:1.18; letter-spacing:.002em;
  color:var(--ivory); margin:0;
}
.case-quote em{ font-style:italic; color:var(--ember-lite); }
.case-quote cite{
  display:block; margin-top:1.7rem; font-style:normal;
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:.95rem; letter-spacing:.18em;
  text-transform:uppercase; color:rgba(247,243,236,.8);
}
.case-quote cite span{ color:rgba(247,243,236,.6); text-transform:uppercase; letter-spacing:.18em; font-size:1em; }

.case-learn {
  margin: 8vh auto 0 !important;
  width: min(1100px, 88vw);
  max-width: 1100px;
  padding: clamp(2.6rem, 5vw, 4.2rem) 8vw;
  background: var(--linen);
  border-radius: 12px;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.6rem, 2.4vw, 2.3rem);
  line-height: 1.4;
  color: var(--ink) !important;
  text-align: center;
}


.case-link {
  font-family: var(--font-body);
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ember);
  text-decoration: none;
  border-bottom: 1px solid var(--ember);
  padding-bottom: 3px;
}

/* ---------- RESPONSIVE ---------- */

@media (max-width: 860px) {
  .pill-nav { gap: 1.1rem; padding: 0.65rem 1.2rem; }
  .positioning { min-height: 70vh; padding-bottom: 10vh; }
  .pos-cue { margin-top: 4vh; }
  .work { padding: 6vh 5vw 12vh; }
  .card-stack { gap: 7vh; margin-top: 5vh; }
  .work-card { min-height: 54vh; border-radius: 6px; }
  .card-info { padding: 1.6rem; }
  .work-head { flex-direction: column; align-items: flex-start; gap: 1.6rem; }
  .work-head .sec-intro { text-align: left; }
  .work-viewall { width: 8rem; height: 8rem; }
  .work-viewall .wv-label { font-size: 1.05rem; }
  .work-row { flex-direction: column; gap: 14px; margin: 6vh 0 0; }
  .work-row .work-card { aspect-ratio: 0.82; max-height: none; }
  /* no hover on touch: keep a soft bottom scrim + always-visible name/client */
  .work-row .work-card::after {
    opacity: 1;
    background: linear-gradient(to top, rgba(8, 6, 4, 0.72), transparent 42%);
  }
  .wk-d-title, .wk-d-client {
    display: block;
    position: absolute;
    left: 1.3rem;
    z-index: 3;
    color: #fff;
    text-shadow: 0 1px 12px rgba(0, 0, 0, 0.8);
  }
  .wk-d-title { bottom: 2.7rem; font-family: var(--v4-gro); font-weight: 600; font-size: 1.05rem; }
  .wk-d-client {
    bottom: 1.3rem;
    font-family: var(--v4-mono), 'Space Mono', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.8);
  }
  .wk-feature, .wk-meta { display: none; }
  /* the Dafi card's IG-reel chrome is a hover reveal (opacity .42 → 1) — on
     touch that hover never fires, so it sat permanently half-visible right
     under wk-d-title/wk-d-client (both anchored ~2.6-2.7rem from the bottom)
     and the two text layers collided. Decorative only, so just hide it. */
  .ig-reel { display: none; }
  .more { padding-bottom: 10vh; }
  .marquee { padding: 1.4rem 0; }
  .more-panel { min-height: 38vh; }
  .journey { padding: 10vh 5vw; }
  .about { padding: 10vh 5vw; }
  .finale { padding: 12vh 6vw 14vh; }
  .case-body { padding: 6vh 0 0; }
  .case-divider-label { margin: 7vh auto 3.5vh; }
  .case-learn { font-size: 1.5rem; padding: 2.6rem 7vw; }
  .case-next { margin-top: 6vh; }
  .book-wrap { padding: 0 3vw; }
  .case-stats { gap: 0.7rem; }
  .case-stats div { padding: 1.4rem 0.6rem; }
  .case-media-row.vertical { padding: 6vh 5vw; }
  .case-split, .case-split.media-left { grid-template-columns: 1fr; }
  .split-text { position: static; }
  .split-media.duo-vert video:nth-child(2) { transform: none; }
  .case-lede { margin-bottom: 6vh; }
  .case-media-row.vertical video:nth-child(2) { transform: none; }
  .case-head { height: 56vh; }
  .journey-line { display: none; }
  .cn-img { height: 20vh; }
  .case-head { height: 48vh; }
  .case-head-info { padding-bottom: 4vh; }
  .case-head-info .case-meta { font-size: 0.56rem; letter-spacing: 0.16em; line-height: 1.9; margin-bottom: 0.9rem; }
  .case-head-info h2 { font-size: clamp(2.1rem, 10vw, 3rem); }
  .case-lede { font-size: 1.4rem; padding: 0 9vw; line-height: 1.35; }
  .case-body h3, .case-cols h3, .split-text h3 { font-size: 1.9rem; }
  .case-cols { gap: 2.2rem; margin-bottom: 6vh; }
  .case-cols h3 { margin-bottom: 0.7rem !important; }
  .case-cols p, .cs-text p, .split-text p { font-size: 1.7rem; line-height: 1.3; max-width: 22ch; }
  .case-section { margin-bottom: 4vh; }

  /* MOBILE ONLY: project-page copy reads centred (desktop untouched) */
  .case-head-info { text-align: center; }
  .case-section { grid-template-columns: 1fr; gap: 1.3rem; text-align: center; }
  .case-section h3, .case-cols h3, .case-cols > div { text-align: center; }
  .cs-text, .case-cols p, .cs-text p, .split-text, .split-text p { text-align: center; }
  .cs-text p, .case-cols p, .split-text p, .case-body > .case-section .cs-text p {
    margin-left: auto; margin-right: auto;
  }
  .cinema-kicker { text-align: center; }
  .cinema-row figcaption { align-items: center; text-align: center; }
  .case-split { gap: 2.2rem; margin-bottom: 6vh; }
  .split-text h3:not(:first-child) { margin-top: 2rem !important; }
  .split-media.duo-vert { grid-template-columns: 1fr; }
  .split-media.duo-vert video, .case-media-row.vertical video { max-width: 320px; margin-left: auto; margin-right: auto; }
  .case-media-row { gap: 1rem; margin: 4vh auto; }
  .case-stats { grid-template-columns: 1fr 1fr; gap: 0.7rem; margin: 5vh auto; }
  .case-stats strong { font-size: 1.65rem; }
  .case-stats div { padding: 1.3rem 0.5rem; }
  .case-divider-label { font-size: 0.8rem; letter-spacing: 0.22em; margin: 5vh auto 2.6vh; }
  .case-duo { gap: 1rem; margin-bottom: 4vh; }
  .case-duo figcaption { margin-top: 0.6rem; }
  .book-controls { margin-top: 1.4rem; }
  .book-controls button { width: 42px; height: 42px; }
  .book-count { min-width: 130px; font-size: 0.56rem; }
  .case-next { padding: 3rem 6vw 3.4rem; margin-top: 7vh; }
  .case-next strong { font-size: 1.6rem; }
  .case-next span { font-size: 0.68rem; margin-bottom: 0.8rem; }
  .cn-img { height: 18vh; margin-top: 1.6rem; }
  .case-learn { margin-top: 7vh !important; }
  .case-close { bottom: 1.2rem; gap: 0.6rem; }
  .cc-dot { width: 54px; height: 54px; }
  .cc-label { font-size: 0.56rem; letter-spacing: 0.24em; }
  .card-info { padding: 1.6rem 1.2rem 2rem; }
  .card-tag { font-size: 1.1rem; letter-spacing: 0.12em; margin-bottom: 0.8rem; }
  .card-no, .card-cat { top: 1.5rem; font-size: 1.15rem; letter-spacing: 0.16em; }
  .card-no { left: 1.6rem; }
  .card-cat { right: 1.6rem; }
  .card-title { font-size: clamp(2rem, 8.2vw, 2.7rem); margin-bottom: 1.1rem; }
  .card-client { font-size: 1.2rem; letter-spacing: 0.08em; }
  .card-client::before { margin-bottom: 0.75rem; }
  .case-trio { grid-template-columns: 1fr; gap: 2rem; }
  .case-agr { grid-template-columns: 1fr; gap: 1rem; max-width: 440px; }
  .case-agr .agr-tall { grid-column: auto; grid-row: auto; }
  .case-agr figure:not(.agr-tall) { aspect-ratio: auto; }
  .case-agr img { height: auto; }
  .kl-split { grid-template-columns: 1fr; }
  .kl-half { min-height: 0; padding: 8vh 8vw; }
  .kl-frame { width: min(68vw, 360px); }
  .cinema-row { flex-direction: column; align-items: center; gap: 2.6rem; }
  .cinema-row figure { max-width: 100%; }
  .cinema-row figure video { height: auto; width: min(78vw, 420px); max-width: 100%; }
  .book-chapters button { padding: 0.4rem 0.7rem; letter-spacing: 0.12em; }
  .card-cta { font-size: 0.7rem; letter-spacing: 0.14em; padding: 0.6rem 1.25rem; margin-top: 1rem; }
  .panel-info { padding: 1.5rem 1.2rem 1.9rem; }
  .more-panel > .card-tag { top: 1.3rem; right: 1.4rem; font-size: 0.88rem; letter-spacing: 0.16em; }
  .panel-info h3 { font-size: 2.1rem; margin: 0 0 0.9rem; }
  .panel-desc { font-size: 1.05rem; }
  .panel-cta { font-size: 0.72rem; letter-spacing: 0.14em; padding: 0.58rem 1.2rem; margin-top: 0.2rem; }
  .hero-title { font-size: clamp(2.1rem, 9.5vw, 4.5rem); }
  .hero-label { font-size: 0.6rem; letter-spacing: 0.32em; }
  .hero-tagline { font-size: 0.78rem; letter-spacing: 0.42em; }
  .hero-rule { width: 46vw; }
  .milestone { padding: 2.6vh 0; }
  .work-card { min-height: 60vh; }
  /* gentler tilt on narrow screens so the rotated corners never cause overflow */
  .work-card:nth-child(1) { top: 6vh; }
  .work-card:nth-child(2) { top: 8vh; }
  .work-card:nth-child(3) { top: 10vh; }
  .work-card:nth-child(4) { top: 12vh; }
  .sec-intro, .more-sub {
  text-align: center;
  color: var(--walnut);
  margin-top: 1.2rem;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.1rem, 1.6vw, 1.35rem);
}

.more-grid { grid-template-columns: 1fr; }
  .more-grid .more-panel:nth-child(2) { margin-top: 0; }
  .journey-grid { grid-template-columns: 1fr; gap: 0; }
  .journey-left { position: static; margin-bottom: 4vh; }
  .milestone { padding: 6vh 0; }
  .about-grid { grid-template-columns: 1fr; gap: 3rem; }
  .about-photo { max-width: 420px; }
  .case-media-row, .case-media-row.three { grid-template-columns: 1fr; }
  .case-hero { height: 38vh; }
  .finale-meta { flex-direction: column; gap: 1.4rem; }
  .wisp { display: none; }
}

/* ---------- REDUCED MOTION ---------- */

@media (prefers-reduced-motion: reduce) and (min-width:99999px){
  .hero::before { animation: none; }
  .wisp { display: none; }
  .scroll-runner { animation: none; }
  .avail-dot { animation: none; }
}
html.reduced .hero::before { animation: none; }
html.reduced .pill-nav { transform: none; }

/* ---------- CASE COMPOSITIONS ---------- */

.case-lede {
  font-family: var(--font-display);
  font-weight: 400;
  font-style: italic;
  font-size: clamp(1.9rem, 3.4vw, 3.2rem);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-align: center;
  max-width: 26ch;
  margin: 0 auto 10vh;
  padding: 0 6vw;
}

.case-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(2.5rem, 5vw, 6rem);
  max-width: 1480px;
  margin: 0 auto 9vh;
  padding: 0 5vw;
}
.case-cols h3 { margin-bottom: 1.2rem !important; font-size: clamp(1.5rem, 2.2vw, 2rem); }
.case-cols p { font-size: 1rem; max-width: none; }

.case-split {
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: clamp(2.5rem, 5vw, 6rem);
  max-width: 1380px;
  margin: 0 auto 9vh;
  padding: 0 6vw;
  align-items: start;
}
.case-split.media-left { grid-template-columns: 1fr 1fr; }
.split-text { position: sticky; top: 9vh; }
.split-text h3 { margin: 0 0 1rem !important; font-size: clamp(1.5rem, 2.2vw, 2rem); }
.split-text h3:not(:first-child) { margin-top: 3rem !important; }
.split-media { display: grid; gap: 1.5rem; }
.split-media.duo-vert { grid-template-columns: 1fr 1fr; align-items: start; }
.split-media video {
  width: 100%;
  border-radius: 14px;
  box-shadow: 0 36px 70px -34px rgba(33, 26, 20, 0.4);
}
.split-media.duo-vert video { aspect-ratio: 9 / 16; object-fit: cover; }
.split-media.duo-vert video:nth-child(2) { transform: translateY(4vh); }
.split-media video.v45 { aspect-ratio: 4 / 5; object-fit: cover; }

/* ---------- CASE NEXT ---------- */

.case-next {
  display: block;
  width: 100%;
  margin-top: 12vh;
  margin-bottom: 0;
  padding: clamp(4rem, 9vh, 7rem) 6vw;
  /* transparent — the fixed flow background shows straight through, so the
     bottom of the page never breaks into a separate block or colour */
  background: transparent;
  border: none;
  cursor: pointer;
  text-align: center;
}
.case-next span {
  display: block;
  font-family: var(--font-body);
  font-size: 0.66rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--taupe);
  margin-bottom: 1.2rem;
}
.case-next strong {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(2.2rem, 4.5vw, 3.8rem);
  color: var(--ivory);
  transition: color 0.3s;
}
.case-next:hover strong { color: var(--ember); }
.cn-img {
  display: block;
  height: 26vh;
  max-width: 940px;
  margin: 2.6rem auto 0;
  overflow: hidden;
  border-radius: 10px;
  will-change: height;
}
.cn-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1); }
.case-next:hover .cn-img img { transform: scale(1.04); }
/* the "connect" next-button has no thumbnail — it gets a DRAWN frame instead,
   in the site's card voice: thin ivory line, rounded like the photo frames,
   warming to ember on hover (same cue as the pill CTAs) */
.case-next .cn-box {
  display: grid;
  place-content: center;           /* text stays centred while the frame grows */
  max-width: 940px;                /* = .cn-img, so it reads as the same card slot */
  margin: 0 auto;
  padding: clamp(2.8rem, 6vh, 4.4rem) 2rem;
  /* same scroll-growth as .cn-img: JS drives min-height 24vh → 56vh on scroll */
  min-height: 24vh;
  will-change: min-height;
  font-size: 1rem; letter-spacing: normal; text-transform: none;   /* undo the kicker styling .case-next span applies */
  border: 1px solid rgba(247, 243, 236, 0.18);
  border-radius: 16px;
  background: rgba(247, 243, 236, 0.02);
  transition: border-color 0.35s ease, background 0.35s ease,
              transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.case-next:hover .cn-box {
  border-color: rgba(231, 191, 140, 0.6);
  background: rgba(247, 243, 236, 0.04);
  transform: translateY(-4px);
}
/* interior of the drawn frame: a short ember rule above the kicker, the big
   line, then a quiet subline — same voice as the section heads */
.cn-box::before {
  content: ""; width: 38px; height: 1px;
  background: var(--ember); opacity: 0.85;
  justify-self: center; margin-bottom: 1.6rem;
  transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.case-next .cn-sub {
  font-size: 0.95rem; letter-spacing: 0.01em; text-transform: none;
  color: rgba(247, 243, 236, 0.55);
  margin: 1.5rem auto 0; max-width: 36ch; line-height: 1.55;
}
/* the framed "Continue" card: a pale studio-grey slab — the one bright object
   at the end of a dark case, so the way out is unmistakable. The dark-theme
   !important colours above are beaten with .cn-framed specificity. */
.case-next.cn-framed .cn-box {
  position: relative; overflow: hidden;
  border: none;
  background: radial-gradient(120% 110% at 50% 18%, #f4f1ea 0%, #e6e3db 55%, #d2cfc7 100%);
  box-shadow: 0 34px 90px -20px rgba(0, 0, 0, 0.55);
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 0.5s ease, filter 0.35s ease;
}
.case-next.cn-framed strong {
  color: #1b1815 !important;
  font-size: clamp(2.8rem, 6vw, 5rem);
  letter-spacing: -0.02em; line-height: 1.05;
}
.case-next.cn-framed .cn-k { color: rgba(27, 24, 21, 0.5) !important; font-size: 0.72rem; }
.case-next.cn-framed .cn-sub { color: rgba(27, 24, 21, 0.62) !important; }
/* the arrow lives in its own span so it can travel; undo the kicker styling
   .case-next span applies and inherit the big display type instead */
.case-next.cn-framed .cn-arrow {
  display: inline-block; margin: 0;
  font-family: inherit; font-size: inherit; letter-spacing: inherit;
  text-transform: none; color: inherit !important;
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
/* soft light sweep that glides across the pale card on hover */
.case-next.cn-framed .cn-box::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: -40%; width: 34%;
  background: linear-gradient(100deg, transparent 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%);
  transform: skewX(-14deg) translateX(-140%);
  opacity: 0; pointer-events: none;
}
.case-next.cn-framed:hover .cn-box {
  transform: translateY(-6px);
  box-shadow: 0 48px 110px -18px rgba(0, 0, 0, 0.6);
  filter: brightness(1.045);
  background: radial-gradient(120% 110% at 50% 18%, #f4f1ea 0%, #e6e3db 55%, #d2cfc7 100%);
  border: none;
}
.case-next.cn-framed:hover .cn-box::before { width: 68px; }
.case-next.cn-framed:hover .cn-box::after {
  opacity: 1; transform: skewX(-14deg) translateX(520%);
  transition: transform 0.9s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
}
.case-next.cn-framed:hover strong { color: var(--ember) !important; }
.case-next.cn-framed:hover .cn-arrow { transform: translateX(0.32em); }

/* ---------- BOOK ---------- */

.book-wrap { max-width: 1780px; margin: 6vh auto 10vh; padding: 0 3vw; }
.book { position: relative; max-width: min(1640px, 100%); margin: 0 auto; }
.book-spread {
  position: relative;
  width: 100%;
  aspect-ratio: 2 / 1;
  perspective: 2800px;
  /* lift-schaduw op de HELE spread i.p.v. per pagina — anders wierpen de twee
     pagina's hun slagschaduw naar elkaar toe en stapelden die op de naad tot
     een donkere lijn in het midden. Nu alleen de buitenrand-lift, naad blijft
     schoon. */
  box-shadow: 0 24px 60px -28px rgba(33, 26, 20, 0.45);
}
.book-page {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
  overflow: hidden;
  background: #fdfbf7;
  cursor: pointer;
}
.book-page.left { left: 0; border-radius: 6px 0 0 6px; }
.book-page.right { right: 0; border-radius: 0 6px 6px 0; }
/* a solid fill (tried first) still read as an obvious grey block next to the
   cover, and transparent let the atmos canvas's cloud noise show through as a
   distinct streaky square — both drew attention to a shape that shouldn't be
   there at all. display:none removes it from paint entirely: no background,
   no box-shadow, no edge, just whatever sits behind the page naturally. Real
   page images (once you flip in) occupy this exact spot anyway. */
.book-page.left.empty { display: none; }
.book-page.left.empty img { display: none; }
.book-page img { width: 100%; height: 100%; object-fit: cover; user-select: none; }
/* rug: ÉÉN zachte vouwschaduw gecentreerd op de naad, buiten de 3D-laag (child
   van .book, niet van .book-spread) zodat hij áltijd bovenop ligt en tijdens de
   flip continu op zijn plek blijft. JS (fadeSpineWithFlip) laat 'm zacht mee-
   ademen met het omslaan — dip weg als de bladzijde opkomt, vol op de landing —
   dus geen harde cut. Aan de cover (linkerpagina leeg) valt de linkerhelft weg. */
.book-spine {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  /* realistische vouw-slagschaduw: het diepst precies op de naad, met een
     duidelijke zachte fade naar LINKS (de linkerpagina buigt de binding in) en
     een klein, korter fadetje naar RECHTS. Asymmetrisch — zwaartepunt links —
     maar niet te heftig. */
  background: linear-gradient(to right,
    transparent 42%,
    rgba(33, 26, 20, 0.02) 44.5%,
    rgba(33, 26, 20, 0.05) 46.8%,
    rgba(33, 26, 20, 0.10) 48.6%,
    rgba(33, 26, 20, 0.18) 50%,
    rgba(33, 26, 20, 0.05) 50.7%,
    rgba(33, 26, 20, 0.012) 51.3%,
    transparent 52%);
}
/* aan de cover NIET verbergen: anders klapt de schaduw ineens op vol zodra de
   eerste pagina landt (harde cut bij het openen). Laat 'm gewoon bestaan zodat
   hij net als elke andere bladzijde mee-dipt. De lange linker-fade valt over de
   lege (donkere) linkerhelft en is daar praktisch onzichtbaar, dus de gesloten
   cover blijft schoon — alleen het korte fadetje op de cover-rand blijft. */
.book-flipper {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
  left: 50%;
  transform-origin: left center;
  transform-style: preserve-3d;
  display: none;
  z-index: 5;
  pointer-events: none;
}
.book-flipper.from-left { left: 0; transform-origin: right center; }
.book-flipper .ff, .book-flipper .fb {
  position: absolute;
  inset: 0;
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  background: #fdfbf7;
  /* GEEN box-shadow op de flap: die halo popte in/uit op het moment dat de
     flap verschijnt (display none→block) en verdwijnt bij de landing — dat gaf
     de flikker. De vaste .book-spine (boven de flap) doet de vouwschaduw, dus
     de flap hoeft zelf geen schaduw te dragen. */
}
.book-flipper .fb { transform: rotateY(180deg); }
.book-flipper img { width: 100%; height: 100%; object-fit: cover; }
/* GEEN naadgradiënt meer op de flap-faces: de vaste .book-spine ligt bóven de
   flap en dekt de vouw continu af, dus de flap hoeft de naadschaduw niet zelf
   te dragen (dat gaf juist de hard-cut bij de landing). De ambient box-shadow
   hierboven geeft de draaiende flap nog wel zijn eigen diepte. */

/* mobile: the spread is too small to actually read a photo on a phone —
   this opens the current right page full-screen instead of turning the
   flip into a single-page mode (that would need its own gesture logic) */
.book-expand{ display:none; }
.book-lightbox{
  display:none; position:fixed; inset:0; z-index:9000;
  align-items:center; justify-content:center;
  background:rgba(6,6,7,.96);
}
.book-lightbox.on{ display:flex; }
.book-lightbox img{ max-width:94vw; max-height:88vh; object-fit:contain; border-radius:6px; }
.book-lightbox-close{
  position:absolute; top:1.2rem; right:1.2rem; width:44px; height:44px;
  border-radius:50%; background:rgba(255,255,255,.08);
  border:1px solid rgba(255,255,255,.5); color:#fff; font-size:1.1rem; cursor:pointer;
}
@media (max-width:760px){
  .book-expand{
    display:flex; align-items:center; justify-content:center;
    position:absolute; top:.6rem; right:.6rem; z-index:5;
    width:34px; height:34px; border-radius:50%;
    background:rgba(6,6,7,.55); border:1px solid rgba(255,255,255,.4);
    color:#fff; font-size:1rem; cursor:pointer;
  }
}

.book-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2.2rem;
  margin-top: 2.6rem;
}
.book-controls button {
  width: 84px;
  height: 84px;
  border-radius: 50%;
  border: 1px solid var(--dust);
  background: none;
  color: var(--ink);
  font-size: 1.8rem;
  cursor: pointer;
  transition: all 0.3s;
}
.book-controls button:hover { background: var(--ember); border-color: var(--ember); color: var(--ivory); }

.book-chapters {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.9rem;
  margin-top: 2rem;
}
.book-chapters button {
  font-family: var(--font-body);
  font-size: 1.28rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--walnut);
  background: none;
  border: 1px solid var(--dust);
  border-radius: 100px;
  padding: 1.15rem 2.4rem;
  cursor: pointer;
  transition: all 0.3s;
}
.book-chapters button:hover { border-color: var(--ember); color: var(--ember); }
.book-chapters button.active { background: var(--ember); border-color: var(--ember); color: var(--ivory); }
.book-count {
  font-family: var(--font-body);
  font-size: 1.22rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--walnut);
  min-width: 250px;
  text-align: center;
}

.toast { position: fixed; bottom: 5.4rem; left: 50%; transform: translateX(-50%) translateY(20px); background: rgba(33,26,20,0.92); color: var(--ivory); font-family: var(--font-body); font-size: 0.66rem; letter-spacing: 0.18em; text-transform: uppercase; padding: 0.8rem 1.5rem; border-radius: 100px; border: 1px solid rgba(212,166,110,0.4); opacity: 0; pointer-events: none; z-index: 95; }

/* ============================================================
   OOAK-STYLE TYPE SYSTEM — overrides (wins over earlier rules)
   SUSE display · Outfit sub-heads · Inter body & labels
   ============================================================ */

/* SUSE display: light weight, tight tracking, no faux-italic */
.hero-title { font-weight: 600; letter-spacing: 0.01em; line-height: 1.0; }
.pos-quote, .case-lede, .case-learn, .cinema-kicker, .kl-caption {
  font-style: normal;
  font-weight: 300;
  letter-spacing: -0.015em;
}
/* (.more-panel h3 removed from this group — the Beyond-the-Brief panel titles
   mirror the RENDERED .card-title style, weight 400, in .panel-info h3 itself) */
.card-title, .case-head-info h2, .case-next strong,
.journey-left h2, .about-text h2, .finale h2 {
  font-weight: 400;
  letter-spacing: -0.02em;
}

/* Outfit sub-heads + descriptive intros */
.case-body h3, .case-cols h3, .split-text h3,
.sec-intro, .more-sub, .finale-mail {
  font-family: var(--font-head);
  font-style: normal;
  font-weight: 400;
  letter-spacing: -0.01em;
}

/* =========================================================
   SITE V4 — KODE SHOWREEL HERO (merge layer, appended last)
   ========================================================= */
/* v4-gro: system font first (SF Pro op Mac) — de CDN-fonts (General Sans/Archivo)
   laadden niet in de Claude-preview en die render bleek mooier; nu overal gelijk */
/* Zie de noot bij --font-body: -apple-system hoort hier VOOROP. De deploy van
   6 aug (slag181) had `-apple-system,system-ui,'General Sans','Archivo'`, dus op
   de Mac is dit San Francisco — General Sans is de val-terug voor de rest. Ik
   heb dat één keer weggehaald in de veronderstelling dat het een Windows-
   toevoeging was; dat was het niet, en het veranderde de hero, de kaartlabels,
   de knoppen en de skills-wand op de Mac. Niet nog eens doen.
   'SF Metric' ertussen = Windows-only (Apple bereikt het nooit). */
:root{ --v4-mono:'Space Mono',monospace; --v4-gro:-apple-system,'SF Metric',system-ui,'General Sans','Archivo',sans-serif; }

/* intro veil: black over the showreel; lifts to reveal it behind the wordmark */
.v4-veil{ position:fixed; inset:0; z-index:6000; background:#070708; pointer-events:none; }

/* HUGE wordmark, top-left → shrinks to the small logo on scroll (KODE) */
.v4-logo{
  /* 4vw = the site's shared left column (connect-frame hairline, footer credit) */
  position:fixed; top:3vh; left:4vw; z-index:8000;
  font-family:'Telegraf',var(--v4-gro); font-weight:400; letter-spacing:-.02em; line-height:.82;
  color:#fff; mix-blend-mode:difference; text-decoration:none;
  transform-origin:left top; will-change:transform;
}
/* TERUG UIT DE WINDOWS-SESSIE (deploy mc8qirxys, 9 aug 01:06). De naam staat op
   line-height:.82 in een .v4-row met overflow:hidden — die regelbox is dus
   KORTER dan de letters zelf. Op de Mac past het net; op Windows valt de font-
   stack anders uit, staan de accenten/stokken hoger in de em-box en snijdt de
   overflow de bovenkant van de naam af. padding-top geeft die ruimte terug en
   de even grote negatieve marge haalt hem weer uit de layout, dus netto nul:
   het element blijft op exact dezelfde plek staan, ook op de Mac. */
.v4-row{ display:block; overflow:hidden; padding-top:.16em; margin-top:-.16em; }
.v4-row .ch{ display:inline-block; }
.v4-logo .v4-row{ font-size:clamp(1.5rem,2.5vw,2.3rem); }

/* nav → top-right row that scrolls with you (KODE) */
.pill-nav{
  position:fixed!important; top:4.6vh!important; left:50%!important; right:auto!important; bottom:auto!important;
  /* centring handled by the V4 override below (auto margins); transform stays free for gsap */
  background:none!important; backdrop-filter:none!important; -webkit-backdrop-filter:none!important; border:none!important; box-shadow:none!important;
  padding:0!important; display:flex!important; gap:clamp(14px,1.7vw,28px)!important; align-items:center;
  opacity:1!important; visibility:visible!important; mix-blend-mode:normal; z-index:8000;
}
.pill-nav .pill-gd{ display:none!important; }
.pill-nav a{
  font-family:var(--v4-gro)!important; font-size:1.46rem!important; font-weight:400!important;
  /* NIET 'normal' laten: die wordt per browser uit de fontmetrics berekend —
     Safari komt lager uit dan Blink, en omdat de pill zijn hoogte uit deze
     regelbox + padding opbouwt, viel de hele nav daar verticaal compacter uit.
     1.336 is exact wat Blink hier van 'normal' maakte (19.5px bij 14.6px), dus
     de look blijft zoals hij was en Safari volgt nu. */
  line-height:1.336!important;
  letter-spacing:.04em!important; text-transform:uppercase; color:#fff!important; padding:.5em 1em!important;
  position:relative; overflow:hidden; isolation:isolate; border-radius:100px; background:transparent!important;
  transition:color .3s ease;
  /* pure white + a faint shadow so it stays legible over the bright footage at the top.
     drop-shadow, NOT text-shadow: the overflow:hidden (roll hover) clips a text-shadow
     into a hard grey slab over light covers — drop-shadow is computed after the clip
     and hugs the letterforms only */
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.38)) drop-shadow(0 2px 10px rgba(0,0,0,.34));
}
/* hover → the ivory bar RISES up from the bottom while the label rolls up
   from beneath in sync (one smooth motion, button itself never moves) */
.pill-nav a::before{
  content:''; position:absolute; inset:0; z-index:-1; border-radius:inherit; background:#f4ece0;
  /* TERUG UIT DE WINDOWS-SESSIE (deploy mc8qirxys). 101% is precies de onderrand,
     en bij een niet-hele device-pixel-ratio rondt Windows die rand de verkeerde
     kant op: er blijft een streepje ivoor van de balk staan onder elke nav-knop.
     De extra 2px duwt hem er volledig uit. Mac-neutraal: de balk zit daar toch
     al buiten de overflow:hidden van de knop, 2px verder weg verandert daar
     niets aan. De +1px in .np-roll hieronder hoort bij dezelfde fix. */
  transform:translateY(calc(101% + 2px)); transition:transform .4s cubic-bezier(.5,0,.12,1);
}
.np-clip{ display:block; position:relative; overflow:hidden; }
.np-roll{ display:block; position:relative; transition:transform .4s cubic-bezier(.5,0,.12,1); }
.np-roll::after{ content:attr(data-t); position:absolute; left:0; top:calc(100% + 1px); width:100%; }
.pill-nav a:hover{ color:#141414!important; text-shadow:none!important; }
.pill-nav a:hover::before{ transform:translateY(0); }
.pill-nav a:hover .np-roll{ transform:translateY(calc(-100% - 1px)); }
/* language switch → bottom-left, out of the way */
.lang-switch{
  display:none; /* EN/NL-knop tijdelijk verborgen op verzoek */
  position:fixed; top:auto; bottom:3.2vh; left:5.4vw; right:auto; z-index:8000;
  mix-blend-mode:difference; color:#fff; font-family:var(--v4-mono); font-size:.7rem; letter-spacing:.08em;
}
.lang-switch button{ color:#fff; }

/* the quote moved onto the showreel — empty the lamp-section quote (kept for JS) */
.pos-quote--moved{ opacity:0!important; min-height:0; margin:0; height:0; }

/* showreel = two pinned pages */
/* ===== HERO — editorial, warm-brown noise + slow drifting light ===== */
/* SITE V4: the hero composition RIDES UP and away as you scroll into About
   (wordmark, nav, "Get in touch" + headline all move together, a touch slow for
   a cooler reveal). Extra height gives the ride-up room to play out gently. */
.showreel{ position:relative; height:100vh;
  background:radial-gradient(135% 105% at 62% 40%, #241a12 0%, #160f0a 50%, #0a0705 100%); }
/* page-2 quote + scroll cue removed from the hero (they live below About now) */
.showreel-headline{ display:none !important; }
.showreel-cue{ display:none !important; }
.showreel-sticky{ position:sticky; top:0; height:100vh; overflow:hidden; }
.showreel-media{ position:absolute; inset:0; z-index:0; }
/* the showreel video is replaced by the warm textured background for this hero */
.showreel-video{ display:none !important; }

/* soft warm light slowly drifting across the dark textured surface (ref video) */
.hero-glow{ position:absolute; inset:-25%; z-index:0; pointer-events:none;
  background:radial-gradient(40% 40% at 32% 42%, rgba(255,224,178,.16), transparent 62%);
  mix-blend-mode:screen; filter:blur(24px); will-change:transform;
  animation:heroDrift 24s ease-in-out infinite alternate; }
.hero-glow--2{ background:radial-gradient(36% 36% at 70% 60%, rgba(228,178,120,.10), transparent 64%);
  animation:heroDrift2 31s ease-in-out infinite alternate; }
@keyframes heroDrift{ 0%{transform:translate(0,0)} 50%{transform:translate(12%,7%)} 100%{transform:translate(-7%,-5%)} }
@keyframes heroDrift2{ 0%{transform:translate(0,0)} 50%{transform:translate(-10%,-6%)} 100%{transform:translate(8%,5%)} }
html.reduced .hero-glow{ animation:none; }

/* availability pill — top-right */
.hero-pill{ position:fixed; top:4.1vh; right:5.4vw; z-index:8000;
  font-family:var(--v4-mono); font-size:.66rem; letter-spacing:.13em; text-transform:uppercase;
  color:rgba(247,243,236,.86); text-decoration:none; line-height:1;
  padding:.66em 1.15em; border:1px solid rgba(247,243,236,.26); border-radius:999px;
  transition:border-color .4s ease, color .4s ease, background .4s ease; }
.hero-pill::before{ content:''; display:inline-block; width:6px; height:6px; border-radius:50%;
  background:var(--ember-lite); margin-right:.7em; vertical-align:middle;
  box-shadow:0 0 8px color-mix(in srgb, var(--ember-lite) 85%, transparent); }
.hero-pill:hover{ border-color:rgba(247,243,236,.5); color:#fff; background:rgba(247,243,236,.05); }

/* hero stage — large editorial headline (centre) + compact side text (right) */
.hero-stage{ position:absolute; inset:0; z-index:5; display:flex; flex-direction:column;
  justify-content:center; align-items:center; padding:0 6vw; pointer-events:none; }
.hero-headline{ margin:0; text-align:center; pointer-events:auto;
  font-family:var(--v4-gro); color:#f7f3ec; text-shadow:0 2px 34px rgba(0,0,0,.5); }
.hero-headline .hl-1{ display:block; font-weight:700; text-transform:uppercase;
  font-size:clamp(2.6rem,8.4vw,7rem); line-height:.92; letter-spacing:-.02em; }
.hero-headline .hl-2{ display:block; font-weight:500; margin-top:.2em;
  font-size:clamp(1.25rem,3.3vw,2.7rem); line-height:1; letter-spacing:-.005em; color:rgba(247,243,236,.8); }
.hero-side{ position:absolute; right:7vw; top:57%; max-width:30ch; margin:0; pointer-events:auto;
  font-family:var(--v4-mono); font-size:clamp(.66rem,.86vw,.82rem); line-height:1.7;
  letter-spacing:.05em; text-transform:uppercase; color:rgba(247,243,236,.58); text-align:left; }
@media (max-width:820px){
  .hero-headline .hl-1{ font-size:clamp(2.2rem,11vw,4rem); }
  .hero-headline .hl-2{ font-size:clamp(1.1rem,4.6vw,1.6rem); }
  .hero-side{ position:static; margin:2.4rem auto 0; max-width:34ch; text-align:center; right:auto; top:auto; font-size:.66rem; }
  .hero-pill{ font-size:.56rem; padding:.55em 1em; }
}
/* scroll-scrubbed black wash: 0 at the top (footage as-is), darkening as you
   scroll down to the quote so the white quote reads cleanly. Driven from hero-v4.js */
.showreel-darken{ position:absolute; inset:0; background:#000; opacity:0; pointer-events:none; }
.showreel-grade{ position:absolute; inset:0; background:
  /* About (warm shader) now follows the hero, not the black lamp room — so the
     hero bottom must NOT settle to black (that made a hard seam). Keep only the
     faint top vignette; the bottom stays transparent → continuous shader into About. */
  linear-gradient(180deg, rgba(0,0,0,.18) 0%, transparent 22%, transparent 100%); }
.showreel-grain{ position:absolute; inset:0; opacity:.16; mix-blend-mode:overlay; pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }

/* page-2 headline (bottom-left, KODE bold uppercase) */
.showreel-headline{
  /* bigger + higher: sits in the upper-left, between the top SCROLL row and the
     centred "see where it leads" cue. No reveal animation — JS just shows it on
     page 2. White + difference blend so it reacts to the footage behind it. */
  position:absolute; left:5.4vw; top:54%; bottom:auto; z-index:6; max-width:16ch;
  font-family:'Helvetica Neue','Helvetica','Arial',sans-serif; font-weight:300; text-transform:uppercase;
  font-size:clamp(2.3rem,5.6vw,5.2rem); line-height:1.04; letter-spacing:.005em;
  color:#fff; /* pure white — NOT mix-blend, so it stays clean white over the footage */
}
.showreel-headline em{ font-style:italic; font-weight:300; color:inherit; } /* dynamic accent, still white */
/* "light" literally glows — warm lamp colour + soft halo. This is the seed of the
   light system: the word the cursor/lamp light is born from. */
.showreel-headline .lit-word{ font-style:normal; color:#ffe7bd;
  text-shadow:0 0 18px rgba(255,206,140,.55), 0 0 42px rgba(255,188,110,.32);
  animation:litword 3.6s ease-in-out infinite; }
@keyframes litword{ 0%,100%{ text-shadow:0 0 16px rgba(255,206,140,.45), 0 0 38px rgba(255,188,110,.26); }
  50%{ text-shadow:0 0 22px rgba(255,212,150,.7), 0 0 56px rgba(255,190,112,.42); } }

/* page-2 thumbnail preview (bottom-right) */
.showreel-thumb{ position:absolute; right:5.4vw; bottom:7vh; z-index:4; display:flex; flex-direction:column; gap:8px; text-decoration:none; }
.showreel-thumb img{ width:clamp(140px,13vw,200px); aspect-ratio:16/10; object-fit:cover; filter:grayscale(.2); border-radius:2px; }
.showreel-thumb .st-label{ font-family:var(--v4-mono); font-size:.66rem; letter-spacing:.12em; text-transform:uppercase; color:rgba(247,243,236,.7); }

/* meta chrome (SCROLL centre + //©2026 right; JS moves it bottom→top) */
.showreel-chrome{ position:absolute; left:0; right:0; top:8.8vh; z-index:5; padding:0 5.4vw;
  display:flex; justify-content:center; align-items:center;
  font-family:var(--v4-gro); font-size:.72rem; font-weight:500; letter-spacing:.22em; text-transform:uppercase;
  color:#fff; mix-blend-mode:difference; pointer-events:none; will-change:transform; }
.showreel-chrome .sc-end{ position:absolute; right:5.4vw; display:flex; gap:10px; }
.showreel-chrome .sc-mark{ opacity:.8; }

/* light line → hands the hero line into the lamp cord (light system) */
.showreel-line{ position:absolute; left:50%; bottom:0; transform:translateX(-50%); z-index:3; visibility:hidden; }
.showreel-line .scroll-track{ height:14vh; margin:0; }

@media (max-width:760px){
  .v4-logo .v4-row{ font-size:clamp(2.2rem,15vw,5rem); }
  .pill-nav a[data-section]{ display:none!important; }
  .showreel-headline{ font-size:clamp(1.1rem,4.6vw,1.7rem); bottom:32vh; }
  .showreel-thumb{ display:none; }
}
@media (prefers-reduced-motion:reduce) and (min-width:99999px){
  .v4-veil{ display:none; }
  .showreel{ height:100vh; }
  .showreel-headline,.showreel-thumb{ position:static; opacity:1; }
}

/* hide the global wordmark + nav while a case study overlay is open */
body:has(.case-overlay:not([hidden])) .v4-logo,
body:has(.case-overlay:not([hidden])) .pill-nav,
body:has(.case-overlay:not([hidden])) .lang-switch{ opacity:0!important; pointer-events:none; transition:opacity .3s; }


/* page-2 cue: "see where it leads" at the bottom of the showreel, just above the lamp */
/* FIXED to the viewport bottom so it never drifts with scroll — it just sits at
   the bottom of the page. Text on top, a softly glowing line below it (scroll-to-
   explore feel). The line IS the light system's track, so the scroll beam runs
   through it and on into the lamp cord rising from below = one continuous line. */
.showreel-cue{ position:absolute; left:0; right:0; bottom:0; z-index:6; text-align:center;
  font-family:var(--v4-gro); font-weight:500; font-size:.78rem; letter-spacing:.26em; text-transform:uppercase;
  color:rgba(247,243,236,.82); pointer-events:none;
  animation:cueglow 2.8s ease-in-out infinite; }
.showreel-cue .cue-track{ display:block; }
.showreel-cue .scroll-track{ width:2px; height:3.5vh; margin:18px auto 0; overflow:hidden;
  background:linear-gradient(to bottom, rgba(247,243,236,.06), rgba(247,243,236,.34));
  box-shadow:0 0 12px rgba(243,226,196,.45); }
@keyframes cueglow{ 0%,100%{ text-shadow:0 0 0 rgba(243,226,196,0); color:rgba(247,243,236,.66); }
  50%{ text-shadow:0 0 18px rgba(243,226,196,.6); color:rgba(255,255,255,.98); } }

/* the cue moved onto the showreel — drop the old lamp-section cue */
.pos-cue{ display:none!important; }

/* tighten the lamp section so the lamp sits just above Selected Work and lights it */
.positioning{ min-height:42vh; padding-bottom:0; }
.work{ padding-top:clamp(10rem,19vh,13rem); }


/* showreel cue: "see where it leads" strictly ABOVE a short cord (flex column) */
.showreel-cue{ position:absolute!important; left:50%!important; right:auto!important; bottom:0!important;
  top:auto!important; transform:translateX(-50%)!important; width:auto!important;
  display:flex!important; flex-direction:column; align-items:center; gap:16px; z-index:6; pointer-events:none; }
/* bigger + brighter so it reads, and a taller cord so the text sits higher (a real
   reading moment) while the line still descends toward the lamp-cord seam */
/* same warm glow as the word "light" in the quote, a touch bigger */
.showreel-cue .cue-text{ font-family:var(--v4-gro); font-weight:500; font-size:1.34rem; letter-spacing:.3em;
  text-transform:uppercase; color:#fff; text-shadow:0 0 18px rgba(255,255,255,.38); animation:none;
  transition:color 1.1s ease, text-shadow 1.1s ease; }
/* the cue carries the light: once the beam departs down the cord, its own glow
   goes OUT (no warm colour, no breathing) — it lights back up on scroll-up */
/* the cue stays white + bright even after the beam sets off (Geert) */
.showreel-cue .cue-track{ display:block; }
.showreel-cue .scroll-track{ width:2px; height:18vh; margin:0; overflow:hidden;
  /* taller track pushes the "see where it leads" text higher (more reading room)
     while the line still descends to meet the lamp-cord. Identical width + solid
     colour to .lamp-cord so cue-track + cord read as ONE seamless, evenly-lit line
     (no brightness step and no thick/thin difference at the join) */
  background:rgba(247,243,236,.22); box-shadow:none; }

/* ============================================================
   CINEMATIC DARK COMMIT — the lamp never floods to white; the room
   stays dark and a warm cone pools the light onto the work.
   (one reversible block — delete to return to the light theme)
   ============================================================ */
:root{ --dk:#000000; --dk-2:#000000; --dk-fg:#ece8e0; --dk-mut:#9b9384; }

/* keep the room dark even when "lit": no white flood, just a warm glow */
.positioning{ background-color:var(--dk) !important; }
.positioning.lit{ background-color:var(--dk) !important; }
/* screen blend gives the luminous room-fill glow. The seam it used to make at the
   Selected Work boundary is fixed structurally below (positioning no longer
   isolates the blend, and .work is transparent) so screen now blends against one
   continuous #0a0706 backdrop across both sections. */
/* longer, gentler falloff so the lit lamp room AND the top of Selected Work read
   as ONE continuously-lit page — the glow tails off softly over the work area
   instead of cutting to hard black. (Unlit state stays pure #000.) */
.pos-dark{ background:radial-gradient(ellipse 104% 64% at 50% 15%, rgba(255,212,152,.30), rgba(240,182,120,.13) 26%, rgba(230,170,110,.06) 46%, rgba(220,162,106,.02) 66%, transparent 88%) !important; mix-blend-mode:screen; -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 30vh) !important; mask-image:linear-gradient(to bottom, transparent 0, #000 30vh) !important; opacity:0 !important; transition:opacity 1.6s ease !important; }
.positioning.lit .lamp-cue-text, .positioning.lit .sec-label{ color:var(--lamp) !important; }

/* sections → dark */
.work, .more, .about{ background:var(--dk) !important; }
.journey{ background:var(--dk-2) !important; }
body{ background:var(--dk) !important; }

/* the lamp's warm cone pooling down onto Selected Work */
.work{ position:relative; overflow:visible; }
.work::before{ display:none; }
.work > *{ position:relative; z-index:1; }

/* section text → light (was dark-on-ivory) */
.sec-label{ color:var(--dk-fg) !important; }
.sec-intro, .more-sub{ color:var(--dk-mut) !important; }
.journey-left h2, .about-text h2{ color:var(--dk-fg) !important; }
.journey-lore, .about-text p, .finale-text{ color:var(--dk-mut) !important; }

/* journey milestones → clean rows, no boxed fill (just a faint divider line);
   the .04 surface fill read as ugly stacked squares */
.milestone{ background:transparent !important; border-color:rgba(247,243,236,.10) !important; }
.milestone h3{ color:var(--dk-fg) !important; }
.milestone p{ color:var(--dk-mut) !important; }
.ms-year{ color:var(--ember) !important; }
.ms-plus{ color:var(--dk-mut) !important; }
.journey-line{ background:rgba(247,243,236,.12) !important; }

/* about portrait frame + photo sits fine on dark; quote rule stays ember */
.about-photo{ box-shadow:0 30px 80px rgba(0,0,0,.5); }

/* scrollbar track to match */
::-webkit-scrollbar-track{ background:var(--dk) !important; }


/* stacked cards: the frontmost lights up like a hover, the rest stay dimmed */
/* the front/active card lifts to ~85% brightness (not a full 100%) — a touch of
   shade stays so it reads as "almost full" rather than flat-lit */
.work-card.is-active::after{ opacity:0.27 !important; }


/* the warm lamp glow fades on smoothly when the bulb ignites — one continuous source */
.positioning.lit .pos-dark{ opacity:1 !important; }


/* ============ CONCEPT LAYER: curiosity light, film world, dark cases ============ */

/* the warm light you carry */
.curio{ position:fixed; left:0; top:0; width:280px; height:280px; margin:-140px 0 0 -140px; z-index:9994;
  pointer-events:none; mix-blend-mode:screen; opacity:0; will-change:transform;
  background:radial-gradient(circle, rgba(236,234,230,.16), rgba(236,234,230,.05) 36%, transparent 66%); }
.curio.hot{ background:radial-gradient(circle, rgba(236,234,230,.24), rgba(236,234,230,.08) 36%, transparent 66%); }
/* was display:none on touch (a separate wandering blob took its place). The
   phone now carries this same light — the spark really does leave the word
   "light" and come back to it — just a little smaller and softer, since it
   sits much closer to the eye on a phone screen. */
@media (hover:none){
  /* the CARRIED light — the state you live with for the rest of the page. It
     was inherited from the old mobile blob (.13/.04) and was so faint you could
     scroll past without noticing you were carrying anything. Up a step, while
     the spark that delivers it comes DOWN a step (see hero-v4.js) — the arrival
     should be the quiet part and the thing you keep should be the visible one. */
  .curio{
    width:230px; height:230px; margin:-115px 0 0 -115px;
    background:radial-gradient(circle, rgba(236,234,230,.19), rgba(236,234,230,.065) 36%, transparent 66%);
  }
}

/* (.curio-mobile is retired — touch carries .curio itself now, see above. The
   class name still appears in the "hide the light" groups further down; those
   selectors simply never match anything, and cost nothing.) */

/* film grain + vignette over the whole world */
.cine{ position:fixed; inset:0; z-index:9990; pointer-events:none; }
/* vignette removed — the inset box-shadow banded into visible rectangular
   "rainbow" edges on the near-black background. The warm base tone + grain carry
   the cinematic feel without any hard framed edge. */
.cine-vig{ display:none; }
.cine-grain{ position:absolute; inset:-50%; width:200%; height:200%; opacity:.09; mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:180px 180px;
  animation:cinegrain .55s steps(5) infinite; will-change:transform;
  }
/* "boiling" film grain — the layer jumps between a few offsets so the grain
   re-randomises like real film, instead of sitting as a static texture */
@keyframes cinegrain{0%{transform:translate(0,0)}20%{transform:translate(-2.5%,1.5%)}40%{transform:translate(2%,-2%)}60%{transform:translate(-1.5%,2.5%)}80%{transform:translate(2.5%,1%)}100%{transform:translate(0,0)}}
@media (prefers-reduced-motion:reduce) and (min-width:99999px){ .cine-grain{ animation:none; } }

/* page-1 value line (KODE-style, bottom-left) */
/* hero meta: a confident role line + a refined, letter-spaced discipline row
   (mono, like film credits) — reads instantly as who he is, agency-grade */
/* role sits DIRECTLY under the big name (top-left), read together as one identity
   block. Fixed, anchored just below the two name rows (1.56 = 2 rows × .78 line-height). */
.showreel-sub{ position:fixed; left:5.4vw; bottom:auto;
  top:calc(3.2vh + 1.56 * clamp(2.2rem,10.5vw,11.5rem) + 0.9rem); z-index:8000;
  display:flex; flex-direction:column; gap:4px; }
/* professional positioning: role (lead) → supporting line → calm value sentence,
   each a clear step quieter, so it reads in one glance without crowding the hero */
.showreel-sub .sub-role{ font-family:var(--v4-gro); font-weight:600;
  font-size:clamp(1.15rem,1.95vw,1.72rem); letter-spacing:-.015em; line-height:1.04;
  color:#fff; text-shadow:0 1px 12px rgba(0,0,0,.45); }
.showreel-sub .sub-support{ font-family:var(--v4-gro); font-weight:400;
  font-size:clamp(.92rem,1.28vw,1.1rem); letter-spacing:-.006em; line-height:1.05;
  color:rgba(247,243,236,.72); text-shadow:0 1px 10px rgba(0,0,0,.4); }
.showreel-sub .sub-value{ font-family:var(--v4-gro); font-weight:400;
  font-size:clamp(.76rem,.92vw,.9rem); line-height:1.5; letter-spacing:.004em;
  color:rgba(247,243,236,.5); max-width:36ch; margin-top:10px;
  text-shadow:0 1px 10px rgba(0,0,0,.35); }
@media (max-width:760px){ .showreel-sub{ top:calc(3.2vh + 1.56 * clamp(2.2rem,15vw,5rem) + 0.7rem); bottom:auto; gap:3px; }
  .showreel-sub .sub-role{ font-size:1.08rem; }
  .showreel-sub .sub-support{ font-size:.9rem; }
  .showreel-sub .sub-value{ font-size:.74rem; max-width:30ch; margin-top:8px; } }

/* (1) CASES → a darker, greyer version of the landing's flowing light (case-bg.js):
   a dark-grey field is dominant with light grey/white waves, self-animating like the
   hero. Text is LIGHT so it reads on the dark flow; the stat/learn cards are dark
   translucent panels so the whole case stays one cohesive dark theme. */
.case-overlay{ background:var(--case-bg-2, #26262a) !important; }
.case-section h3, .case-body h3, .case-cols h3, .case-head-info h2{ color:#f4efe6 !important; }
.case-body p, .cs-text p, .case-cols p, .case-row-text p{ color:#d4ccbe !important; }
.case-body p strong, .case-row-text strong{ color:#fbf7f0 !important; }
.case-body p em, .case-row-text em{ color:#e7c08a !important; }
.case-lede{ color:#ece5d8 !important; }
.case-divider-label, .case-stats span, .case-duo figcaption, .case-trio figcaption,
.case-agr figcaption{ color:#a89e8f !important; }
/* cinema caption: quiet credit line, not a heading — but bright enough to
   read. Extra-specific (+p, +.case-body) so it outranks the white/oversized
   ts-style body-p rules */
.ts-style .case-body p.cinema-caption{
  color:rgba(247,243,236,.72) !important;
  /* same weight as the Dafi/Klassiekers kl-caption stat lines, not a quiet
     credit line — this is the only caption on the page, it should carry */
  font-size:1.6rem !important;
  letter-spacing:.06em;
}
.case-duo figcaption, .case-trio figcaption{ color:#c9bda9 !important; }
.case-stats strong{ color:#e7c08a !important; }
.case-learn{ color:#f4efe6 !important; }
.case-next strong{ color:#f4efe6 !important; }
.case-next .cn-k, .case-next span{ color:#a89e8f !important; }
.case-quote p{ color:#f4efe6 !important; }
.case-quote em{ color:#e7c08a !important; }
.case-quote cite{ color:rgba(247,243,236,.92) !important; text-shadow: 0 1px 18px rgba(8,8,10,.55), 0 1px 3px rgba(8,8,10,.4); }
.case-quote cite span{ color:#e7c08a !important; }
/* text sits directly on the flowing field, which has bright crests — a soft shadow
   keeps it readable wherever a lighter wave passes behind it */
.case-head-info h2, .case-lede, .case-section h3, .case-body h3, .case-cols h3,
.case-body p, .cs-text p, .case-cols p, .case-row-text p, .case-quote p,
.case-divider-label, .case-next strong, .case-next span{
  text-shadow: 0 1px 18px rgba(8,8,10,.55), 0 1px 3px rgba(8,8,10,.4);
}
/* …but not inside the pale Continue card: dark text on light needs no glow */
.case-next.cn-framed strong, .case-next.cn-framed span{ text-shadow: none; }
/* stat + learn cards: dark translucent panels (not light linen) so light text reads */
.case-stats div{ background:rgba(20,20,24,.40) !important; border-color:rgba(247,243,236,.14) !important; }
.case-learn{ background:rgba(20,20,24,.40) !important; }
.case-section{ border-color:rgba(247,243,236,.12) !important; }

/* Scavenger Hunt case — generous vertical rhythm: the copy gets real air beneath
   it so the design sections don't crowd in straight under the text, and every
   block breathes instead of feeling cramped. */
.ts-style .case-section { margin-bottom: 16vh; }
/* Minor case: each case-section sits directly above its own chapter's media,
   not a fresh standalone block like Challenge/Approach/Solution — the big gap
   belongs between chapters (.case-chapter), not between a chapter's text and
   its own images/video. */
#case-minor .case-section { margin-bottom: 5vh; }
.ts-style .case-divider-label { margin-top: 14vh; margin-bottom: 7vh; }
.ts-style .case-trio,
.ts-style .case-duo { margin-bottom: 16vh; }
.ts-style .case-next { margin-top: 17vh; }
.ts-style .case-next > span { font-size: 1.4rem; letter-spacing: 0.34em; }

/* a case is open → hide the fixed top chrome (logo, nav, Get-in-touch pill) so it
   never overlaps the case content; the case has its own close button. */
body.case-open .v4-logo,
body.case-open .pill-nav,
body.case-open .hero2-pill,
/* the mobile hamburger was added after this list and never joined it, so on a
   phone it sat right on top of the case's close circle — two overlapping
   controls in the same corner, and the burger won the taps */
body.case-open .m-burger {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
}
/* …and that class flip is INSTANT. On close it is dropped the moment the overlay
   finishes dissolving, while the cover is still flying home for another ~0.2s —
   so the whole top chrome snapped back to full strength in a single frame: the
   hard black pop across the top in the last milliseconds of the close.
   Give them a fade so the chrome resolves *with* the transition instead of
   cutting in. (.pill-nav had transition:none and .hero2-pill had no opacity
   transition at all — those two were the pop; .v4-logo already eased at 0.3s.)
   visibility can't tween, but with opacity easing underneath it the flip is
   invisible.
   NOTE: .pill-nav and .hero2-pill are handled further down the file instead —
   both are re-declared later (the V4 overrides), and .pill-nav even carries
   `transition:none !important` there on purpose, so they have to be restored
   AFTER that point. See "top chrome — restore the opacity fade" below. */
.v4-logo { transition: opacity 0.5s ease; }

/* Scavenger Hunt case — white body copy (not grey) with the brand amber reserved
   for the key insight in each block, an index number per section for rhythm, and
   extra air between the header and the first section. */
.ts-style .case-body p { color: #ffffff !important; }
.ts-style .case-body p strong { color: var(--ember-lite) !important; font-weight: 500; }
.ts-style .case-body { padding-top: 12vh; }
.ts-style .case-body { counter-reset: cs; }
.ts-style .case-section { counter-increment: cs; }
.ts-style .case-section h3::before {
  content: counter(cs, decimal-leading-zero);
  display: block;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 1.2rem;
  letter-spacing: 0.1em;
  color: var(--ember-lite);
  margin-bottom: 0.7rem;
}
/* the cases drop the 01/02/03 numbers on their section headings (Challenge,
   Approach, Solution) — just the words, cleaner; the 01/ … /02 page-head rules
   carry the numbering for the page as a whole now */
.ts-style .case-section h3::before { content: none; }
/* restructured header meta: role first (prominent), org/year second (muted) */
.ts-style .case-head-info .case-meta {
  display: flex; flex-direction: column; gap: 0.45rem;
  letter-spacing: 0.2em;
  /* the block is right-aligned now, so this flex column must pack to the END or
     its lines would still sit left inside a full-width box. The old left optical
     nudge (which compensated for the title's round "S" ink inset at the LEFT
     edge) no longer applies — the shared edge is the right one. Letter-spacing
     adds a trailing gap after the last glyph though, so pull that back off the
     right so the meta's visible edge meets the title's. */
  align-items: flex-end;
  padding-left: 0;
  margin-right: -0.2em;
}
.ts-style .cm-role {
  color: var(--ember-lite); font-weight: 500;
  font-size: clamp(1.08rem, 1.35vw, 1.55rem);
}
.ts-style .cm-org { color: rgba(247,243,236,.62); font-size: 0.85em; letter-spacing: 0.2em; }
/* photo captions a touch larger + tighter tracking now the copy is shorter */
.ts-style .case-trio figcaption,
.ts-style .case-duo figcaption {
  font-size: 1.28rem; letter-spacing: 0.1em;
}

/* Scavenger Hunt case — carry the hero's Telegraf wordmark font through this page
   (title, labels, big answer copy, the learn card) so it matches the landing. */
.ts-style .case-head-info h2,
.ts-style .case-body h3,
.ts-style .case-body p,
.ts-style .case-learn {
  font-family: 'Telegraf', var(--v4-gro);
}
.ts-style .case-learn { font-style: normal; }
/* de testimonial-quote valt BUITEN de Telegraf-sweep hierboven: die deelt de
   Cormorant-italic stijl van .case-quote (Telegraf heeft geen echte italic) */
/* blockquote in de selector: moet de latere `.ts-style .case-body p`
   font-size-sweep (media query onderin) outranken, anders zakt de quote
   terug naar bodytekst-formaat */
.ts-style blockquote.case-quote p {
  font-family: var(--font-garamond);
  font-size: clamp(2.6rem, 4.5vw, 4.6rem);
  line-height: 1.18;
}

/* Beyond-the-Brief case pages (AI Animation / Dafi) carried a few leftover
   mini-styles from the old ivory cases — align every text element with what the
   Selected Work case pages use (Telegraf; captions/labels at the 1–1.3rem meta
   scale) so all six pages read as one system. */
/* three classes: must outrank `.ts-style .case-body p` (the captions are <p>'s
   inside .case-body, so the big Telegraf body-copy rule was winning) */
.ts-style .kl-half .kl-caption{
  font-family:var(--font-body);
  font-size:1.28rem; letter-spacing:0.1em; line-height:1.55; max-width:52ch;
  font-weight:400; color:#c9bda9 !important;   /* = ts-style figcaption exactly (body-p rule is !important too) */
}
/* Dafi + Klassiekers captions: same size, name + tactical move + the number
   that proves it — only weight/colour set the number apart. Dafi's lines are
   short enough to force onto one line; Klassiekers' are longer (English title
   + full sentence) and overflow the frame at nowrap, so those wrap normally. */
.df-warm .kl-caption, .df-cool .kl-caption,
.kl-warm .kl-caption, .kl-cool .kl-caption{
  font-size:1.6rem !important;
}
.df-warm .kl-caption, .df-cool .kl-caption{
  max-width:none; white-space:nowrap;
}
.kl-warm .kl-caption, .kl-cool .kl-caption{
  max-width:36ch;
}
.kl-num{
  font-family:var(--font-body);
  font-size:inherit; letter-spacing:0.04em;
  font-weight:700; color:var(--ivory) !important;
}
.ts-style .case-stats strong{ font-family:'Telegraf', var(--v4-gro); }
.ts-style .case-stats span{ font-size:1rem; letter-spacing:0.14em; }
/* (the old Telegraf swap for this case is gone: every case-quote now shares
   the Cormorant-italic testimonial style defined at .case-quote) */
.ts-style .case-quote cite{ font-size:1.45rem; }

/* ---- SOLUTION + TESTIMONIAL CARD (treinsafari) ----------------------------
   Solution drops a touch lower, and its quote gets real breathing room and a
   frosted, rounded card so it reads as a distinct testimonial 'moment' with its
   own hierarchy — not just one more paragraph. A soft gold quotation mark sits
   on the card's top edge as the quiet signal that someone is speaking. */
/* ---- TESTIMONIAL (treinsafari) — no card, just a clean pull-quote -----------
   Deliberately un-decorated: no panel, no glow, no border. Same language as the
   rest of the page (type floating on the atmosphere). Hierarchy is carried by
   size + weight alone: a large calm italic quote, then a small quiet source. */
/* a bit more air between the media above and the testimonial */
.ts-style .case-section:has(+ .case-quote){ margin-top: 14vh; }
/* clean and simple: a calm centred pull-quote (Cormorant italic), no card, no
   grid, no labels — just the quote and, quietly beneath it, the source. */
.ts-style .case-quote{
  width: min(1000px, 90vw);
  margin: 15vh auto 0;
  padding: 0 4vw;
  text-align: center;
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
}
.ts-style .case-quote::before{ content: none; }
.ts-style .case-quote p{
  font-size: clamp(1.55rem, 2.3vw, 2.3rem);
  line-height: 1.5;
  text-align: center;
  max-width: none;
}
.ts-style .case-quote cite{
  display: block;
  text-align: center;
  margin-top: 2.4rem;
  padding-top: 0;
}
.ts-style .case-quote cite::before{ content: none; }

/* dark-on-light controls (photo book) need their own palette on the
   ts-style dark wave background — the default walnut/dust pair was made for the
   light ivory cases and was nearly invisible here. */
.ts-style .book-chapters button {
  color: rgba(247,243,236,.78);
  border-color: rgba(247,243,236,.3);
}
.ts-style .book-chapters button:hover { border-color: var(--ember); color: var(--ivory); background: rgba(196,137,58,.35); }
.ts-style .book-chapters button.active { background: var(--ember); border-color: var(--ember); color: var(--ivory); }
.ts-style .book-controls button {
  color: var(--ivory);
  border-color: rgba(247,243,236,.3);
}
.ts-style .book-controls button:hover { background: var(--ember); border-color: var(--ember); color: var(--ivory); }
.ts-style .book-count { color: rgba(247,243,236,.62); }

#case-minor .case-body { padding-top: 4vh; }

/* Scavenger Hunt case — editorial layout: a small left-pinned label (Challenge /
   Approach / Solution) and a large, wide answer block that takes ~3/5 of the page,
   matching the reference. Desktop only; the ≤860 rules keep the mobile stack. */
@media (min-width: 861px) {
  .ts-style .case-section {
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 3.7fr);
    gap: clamp(1.5rem, 3.5vw, 5rem);
  }
  .ts-style .case-body h3 {
    font-family: var(--font-body);
    font-style: normal;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    font-size: clamp(1.15rem, 1.45vw, 1.65rem);
    line-height: 1.2;
  }
  .ts-style .case-body p {
    max-width: none;
    line-height: 1.22;
    font-size: clamp(1.85rem, 3vw, 3.35rem);
  }
  /* Pull the index number out of the flow so it sits ABOVE the label: now the top
     of CHALLENGE / APPROACH / SOLUTION lines up with the top of the paragraph next
     to it, and the number is the only thing that rises above that line. */
  .ts-style .case-section h3 { position: relative; top: 0.3rem; }
  .ts-style .case-section h3::before {
    position: absolute;
    bottom: 100%;
    left: 0;
    margin-bottom: 0.3rem;
  }
}

/* ============================================================
   ALL CASE PAGES — edge-to-edge editorial layout (ref: Four Sigmatic case
   study). Piloted on treinsafari, now the shared treatment for every case:
   the selectors below are .ts-style-wide, only crops and labels stay per-case.
   Desktop only, mirrors the breakpoint above — mobile keeps the existing
   centred stack untouched.
   ============================================================ */
.ts-style .case-page-head {
  max-width: none;
  padding: 0 4.4vw;
  /* top gap tightened (6vh→2vh): the Overview rule sat too low under the cover */
  margin: 2vh 0 13vh;
}
/* the divider after Approach needs its own breathing room above it too —
   the first one (Overview) sits right under the cover photo and needs none */
.ts-style .case-page-head-2 { margin-top: 9vh; }
.ts-style .case-page-head-rule { height: 3px; background: rgba(255,255,255,.55); }
.ts-style .case-page-head-row {
  position: relative;
  margin-top: 1.25rem;
  display: flex; align-items: baseline; justify-content: space-between;
  font-family: var(--v4-mono), 'Space Mono', monospace;
  font-size: 1.5rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(255,255,255,.85);
}
.ts-style .case-page-head-row .cph-name {
  position: absolute; left: 29%; transform: translateX(-50%);
  color: #fff; white-space: nowrap;
}

/* ---- mirrored dip-to-black under the cover (every case) -------------------
   The cover's bottom carries a dip-to-black (.case-head-shade, black at the
   very edge fading UP into the photo). Here we mirror that dip on the OTHER
   side of the cover's bottom line: a second dip-to-black that is black AT THE
   TOP (against the cover edge) and fades DOWN into the body field, so the hard
   cover line dissolves symmetrically toward the thin Overview rule below.
   Trialled on IKEA, now rolled out to every case. */
.ts-style .case-body { position: relative; }
.ts-style .case-body::before {
  content: "";
  position: absolute;
  /* start 2.5vh ABOVE the body top (i.e. up into the cover's own black dip). On
     open the body slides up from y:18px, so for a moment its top sits below the
     fixed cover edge — that gap used to flash the light field / white rule through
     ("the fade hadn't reached the edge yet"). The extra black cap always bridges
     that gap; once settled it overlaps the cover's #000 dip, so it's invisible. */
  top: -2.5vh; left: 0; right: 0;
  height: 17.5vh;               /* 2.5vh cap + the ~15vh fade down to the Overview rule */
  pointer-events: none;
  z-index: 0;
  /* solid black through the cap + cover edge, then ONE long even ramp that stays
     visible through the middle and only releases to transparent right at the white
     rule below — a full, graceful fade the whole distance (not a short black blob) */
  background: linear-gradient(to bottom,
    #000 0%, #000 14%, rgba(0,0,0,0.72) 42%,
    rgba(0,0,0,0.44) 61%, rgba(0,0,0,0.18) 81%, transparent 100%);
}

@media (min-width: 861px) {
  /* full page width: the base 1440–1680px max-widths left dead space on
     either side on anything wider than a laptop — drop the caps, keep a
     small viewport gutter instead. .case-page-head is NOT in this list — it
     keeps its own 4.4vw gutter, matching the landing header exactly. */
  .ts-style .case-section,
  .ts-style .case-trio,
  .ts-style .case-media-row,
  .ts-style .case-divider-label {
    max-width: none;
    padding: 0 3vw;
  }
  /* less dead air above the Overview divider: the base 12vh push was tuned
     for the old centred layout; the whole page-head + everything under it
     shifts up together since padding-top only moves the FIRST child down */
  .ts-style .case-body { padding-top: 4vh; }
  /* label left, answer block pushed toward the right two-thirds via a much
     wider gap (not a narrower label column) — text itself stays left-aligned
     (ragged right), it's the BLOCK that sits right, per the reference */
  .ts-style .case-section {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2.6fr);
    gap: clamp(3rem, 9vw, 11rem);
    /* more air between the Overview sections (Challenge → Approach) so the page
       breathes — was 8vh, felt cramped */
    margin-bottom: 12vh;
  }
  .ts-style .case-body h3 { font-size: clamp(1.3rem, 1.7vw, 1.9rem); }
  /* the :not()s are all <p>'s too, so without the exclusions they inherited this
     giant answer-copy size ("THE DESIGN" rendered as a full headline instead of a
     small label; the klassiekers/dafi video captions and the aftermovie credit
     line blew up the same way once this stopped being treinsafari-only) */
  .ts-style .case-body p:not(.case-divider-label):not(.kl-caption):not(.cinema-caption) {
    font-size: clamp(2.3rem, 4vw, 4.4rem);
    line-height: 1.14;
  }
  /* small, centred label — not a headline: just a quiet marker before the next
     image group */
  .ts-style .case-body p.case-divider-label {
    font-size: clamp(1.4rem, 2.1vw, 2.3rem) !important;
    text-align: center;
    letter-spacing: 0.22em;
    max-width: none;
    padding: 0 4.4vw;
    /* compact: sit close to its images (3vh below) and to whatever is above
       (top collapses with the tightened page-head / trio margins below) */
    margin: 5vh 0 3vh;
  }
  /* pull the group labels in tight to the divider / trio above them (were 13vh
     and 16vh of dead air) */
  .ts-style .case-page-head-2 { margin-bottom: 7vh; }
  .ts-style .case-trio { margin-bottom: 8vh; }
  /* every image group shares ONE rail: 84% of the page, centred. The trio is
     pulled in from full-bleed so the images read calmer, and the Amstel Gold
     grid (minor) sits on exactly the same edges so the six cases line up. */
  .ts-style .case-trio,
  .ts-style .case-agr {
    max-width: 84%;
    margin-left: auto;
    margin-right: auto;
    padding: 0;
  }
  /* "from design to museum floor" duo: match the trio's PER-IMAGE size instead
     of stretching 2 images across the full width (they'd balloon past what fits
     on screen) — 2 in 58% ≈ 3 in 84%. Extra bottom air before Solution follows:
     that transition felt cramped. */
  .ts-style .case-duo {
    max-width: 58%;
    margin: 0 auto 11vh;
    padding: 0;
  }
  /* IKEA is the exception: its duo carries no captions and IS the case's main
     work (two campaign posters), so it takes the 84% image rail instead of the
     58% one — same edges as every other group, without shrinking the posters. */
  #case-ikea .case-duo { max-width: 84%; }
  .ts-style .case-trio figcaption,
  .ts-style .case-duo figcaption {
    font-size: 1.65rem;
  }
  /* ---- COVER → PAGE colour-melt --------------------------------------
     Goal: the photo doesn't fade to black, it DISSOLVES — its own colours
     smear, diffuse and cool down until they become the dark page field.
     Three co-operating layers, all inside the overflow:hidden header so
     everything is fully resolved to #26262a by the box's bottom edge (=
     seamless with the body, which sits on that same colour):

     1. .case-head img  — the sharp photo, dissolved over a LONG, gentle
        mask so it thins out gradually instead of hitting an edge.
     2. .case-head::before — a heavily blurred duplicate sampling the LOWER
        (platform) band of the same photo, so the colours it bleeds are a
        natural continuation of what's directly above, not the bright sky.
        It stays colourful further down than the sharp layer, then fades.
     3. .case-head-shade — only a SOFT scrim now (was a near-black wall that
        crushed the colour before it could melt); just enough to seat the
        title, concentrated high, gone well before the box bottom. */
  /* COVER FADE — three phases so it never reads abrupt: the photo stays fully
     BRIGHT up top, eases through a DIM/muddy band, then the last strip fades to
     TRANSPARENT. The stops follow a smoothstep S-curve (alpha = 1 − (3t²−2t³))
     rather than a few hand-set points: the slope changes only gradually, so there
     are no kinks for the eye to read as horizontal "Mach band" lines at the stop
     boundaries. Measured against the IMAGE (top:-10%, height:120%); the box clips
     at image-fraction (100-(-10))/120 = 91.7%, so it MUST be transparent by ~91%
     (a hair before the clip) or the box slices the photo → seam line.

     A mask lives in IMAGE space, but three covers carry their own inline
     top/height (their crops were hand-tuned), so the same literal stops would
     land at different heights on the page — or worse, past the clip. So the two
     endpoints are variables (--mkA = fade starts, --mkB = fully gone, both in
     image fractions) and the nine intermediate stops are derived from them, keeping
     the exact smoothstep shape. Per-case values below solve for the SAME box
     positions treinsafari lands on: fade 71.6% → 99.2% of the header's height. */
  /* .flip-clone rides along: the thumbnail that flies from the card to the header
     shares this exact mask so the cover→page dissolve can ease in DURING the
     flight (main.js parks its two stops at 100% = no fade, then travels them to
     the header's own values). Without it the clone flew as a hard-edged photo and
     the dissolve popped on at touchdown. */
  /* COVERS ON EVERY CASE — no dissolve-to-page (that transparent fade let the
     moving atmos shader bleed through the photo's edge = the "smear" Geert hated).
     Instead: a hard bottom edge, clipped clean by the header's own overflow:hidden,
     plus a SOLID dip-to-black below. The flip-clone shares the maskless state;
     main.js/case-embed.js also detect the missing mask and keep the flying clone
     maskless through the whole flight, so nothing flickers between a soft clone and
     a hard-edged header. (Rolled out from the ikea trial — the old per-case
     --mkA/--mkB fade tunings are gone with the fade.) */
  .ts-style .case-head img,
  .flip-clone {
    -webkit-mask-image: none;
            mask-image: none;
  }
  /* the anti-banding grain existed only to hide banding in the old dimmed fade —
     there is no fade to band now, so it is off on every case */
  .ts-style .case-head::after { display: none; }
  /* DIP TO BLACK on every cover — a SOLID black gradient over the bottom of the
     photo (replaces the old title-scrim). The photo darkens to true, stable black
     at its lower edge: cinematic dip, nothing showing through, and it backs the
     title cleanly. */
  /* the title sits at the very bottom of the cover and then OVERHANGS it: the
     block is pushed down past the cover's bottom line with `top` (not transform —
     the open flight animates the info's transform and would reset it) so the type
     rests on the foreground, over the body's dip-to-black, never clipped. z-index
     lifts it above the .case-head-media photo layer. */
  .ts-style .case-head-info { padding-bottom: 0; top: 2.2vh; z-index: 1; }
  /* two-line cover title: same SIZE on both lines, weight does the hierarchy.
     Designer-forward split — the project-type line (.ch-type) is Telegraf Regular
     400 (was Bold 800, dialled back on Geert's call); the client / subject on
     line 2 drops to Thin 200 and reads as a quiet credit below it. Weight
     contrast, not scale. */
  .ts-style .case-head-info h2 { font-weight: 200; }
  .ts-style .case-head-info h2 .ch-type { font-weight: 400; }
  /* treinsafari's type line ("Scavenger Hunt Design") is the longest — at the
     full shared size (9.6rem) it wrapped to two lines. Instead of shrinking it
     hard, this is a SCALED TWIN of the default (clamp(3.8rem,11vw,9.6rem) × ~0.85):
     as large as fits on ONE line at every desktop width (measured safe 861→2560px,
     ~35-47px gutter margin), so the cover reads close to the minor/dafi covers. */
  #case-treinsafari .case-head-info h2 { font-size: clamp(3.2rem, 8vw, 8.1rem); }
  #case-treinsafari .case-head-info h2 .ch-type { white-space: nowrap; }
  /* Two layers on every cover: the bottom dip PLUS the mirrored bottom-left
     corner dip, so the fade closes the frame from two sides instead of one.
     The other covers carry no hard edge to erase (that was IKEA's billboard
     frame), so they run the corner layer at the LIGHTER setting — just enough
     to seat the corner, without the heavier pull IKEA needs. */
  .ts-style .case-head-shade {
    background:
      linear-gradient(to top right,
        rgba(0,0,0,0.82) 3%, rgba(0,0,0,0.38) 15%, rgba(0,0,0,0.13) 26%, transparent 38%),
      linear-gradient(to top,
        #000 0%, rgba(0,0,0,0.92) 6%, rgba(0,0,0,0.58) 14%,
        rgba(0,0,0,0.24) 23%, transparent 34%);
  }
  /* IKEA only: the photo has a hard vertical edge — the black JCDecaux billboard
     frame — that lands just LEFT of the title and read as a "streep" beside the P.
     Same two-layer structure as above, but the corner layer runs heavier (0.9 →
     42% instead of 0.82 → 38%) because it has to dissolve that edge, not just
     seat the corner. Poster + station stay bright. */
  #case-ikea .case-head-shade {
    background:
      linear-gradient(to top right,
        rgba(0,0,0,0.9) 4%, rgba(0,0,0,0.5) 16%, rgba(0,0,0,0.2) 28%, transparent 42%),
      linear-gradient(to top,
        #000 0%, rgba(0,0,0,0.92) 6%, rgba(0,0,0,0.58) 14%,
        rgba(0,0,0,0.24) 23%, transparent 34%);
  }
}


/* the spark that flies between the lamp and your cursor */
.curio-spark{ position:fixed; left:0; top:0; width:64px; height:64px; margin:-32px 0 0 -32px; border-radius:50%;
  z-index:9996; pointer-events:none; mix-blend-mode:screen; opacity:0; will-change:transform;
  background:radial-gradient(circle, rgba(248,247,245,.95), rgba(236,234,230,.5) 38%, transparent 70%); }
/* comet trail — many faint ghosts sampled close together along the spark's OWN
   path, so the tail reads as one smooth tapering streak (not distinct blobs) */
.curio-trail{ position:fixed; left:0; top:0; width:52px; height:52px; margin:-26px 0 0 -26px; border-radius:50%;
  z-index:9995; pointer-events:none; mix-blend-mode:screen; opacity:0; will-change:transform,opacity;
  background:radial-gradient(circle, rgba(246,245,242,.6), rgba(236,234,230,.24) 44%, transparent 72%); }
/* arrival bloom — one quick pop at the cursor the instant the spark lands there */
.curio-bloom{ position:fixed; left:0; top:0; width:200px; height:200px; margin:-100px 0 0 -100px; border-radius:50%;
  z-index:9995; pointer-events:none; mix-blend-mode:screen; opacity:0; will-change:transform,opacity;
  background:radial-gradient(circle, rgba(255,255,255,.55), rgba(238,236,231,.2) 40%, transparent 68%); }


/* the old wisp (light trailing down the right side) is replaced by the curiosity cursor light */
.wisp{ display:none !important; }


/* when the light has moved to the cursor, the lamp itself + its warm ambient go dark */
/* when the light is carried to the cursor the bulb's own glow leaves, but a soft
   warm ambient lingers in the room (floor 0.22) instead of snapping to black */
body.light-carried .positioning .pos-dark{ opacity:0.22 !important; transition:opacity 1s ease !important; }
body.light-carried .positioning .lamp-glow{ opacity:0 !important; transition:opacity .6s ease !important; }


/* Selected Work stays invisible until the beam lights the lamp + the room becomes visible */
.work{ opacity:0; transition:opacity 1.2s ease; }
.positioning.lit ~ .work{ opacity:1; }
html.reduced .work{ opacity:1; }

/* ============================================================
   HOMEPAGE HERO v2 — Chris-Kalafatis hierarchy, warm brand world.
   An overlay on the opening that FADES OUT on scroll (hero-v4.js),
   revealing the untouched showreel → quote → lamp below.
   (PP Telegraf isn't licensed here → Archivo, closest legal grotesk.)
   Reversible: delete this block + the .hero2* markup + the JS block.
   ============================================================ */
.hero2-bg{ position:absolute; inset:0; z-index:3; pointer-events:none; overflow:hidden;
  background:transparent; }
.hero2-canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
/* grain + cloud field are now painted on .hero2-canvas (see hero-bg.js) */

/* ============================================================
   GLOBAL LIVING "AERIAL" BACKGROUND (step 2)
   One fixed WebGL canvas behind the whole site; the dark sections go
   transparent so the living light flows continuously through everything.
   ============================================================ */
html{ background:#070708; }
/* threshold/lamp interstitial removed — intro flows straight into Selected Work */
.positioning{ display:none !important; }
/* work was revealed by the (now-hidden) lamp's .lit class — keep it always visible */
.work{ opacity:1 !important; }
/* Selected Work header pared back to a single big quote (Chris-K style):
   drop the section label + the metrics strip, blow up the intro line in white */
.work .sec-label.center, .work .proof-strip{ display:none !important; }
.work .sec-intro, .more .more-sub{
  font-family:var(--font-display); font-style:normal; font-weight:400;
  font-size:clamp(2.5rem,4.6vw,4.25rem); line-height:1.12; letter-spacing:-0.015em;
  /* 74vw → 88vw. De <br> in de HTML legt regel 1 vast; regel 2 moet dus in één
     keer passen, anders valt "content." er als wees af — precies wat Geert op
     Windows zag. Nagemeten hoeveel ruimte regel 2 overhoudt (speling t.o.v. de
     benodigde tekstbreedte):
     benodigde 1016,5px — die is op beide breedtes gelijk, want de root-fontsize
     zit hier op zijn ondergrens van 10px: de zin krimpt niet mee met het scherm
     terwijl zijn kader dat wél doet. Dáár zat de fout.
         breedte   74vw (oud)   88vw (nu)   werkelijke box
         1280       −6,8%        +2,5%      1041,5  ← hier begrenst niet
                                                      max-width maar de sectie
         1440       +4,8%       +16,1%      1180    ← hier wint de 1180px-cap
     Bij −6,8% past regel 2 domweg niet: met 74vw brak hij bij 1280 dus OOK op
     de Mac al naar drie regels.
     Waarom dit de Mac niet verplaatst waar hij al goed was: het blok staat op
     margin:0 0 0 auto met text-align:right, dus de RECHTERrand ligt vast —
     breder worden groeit alleen naar links. Bij 1440 nagemeten: regelboxen
     exact gelijk (zelfde left, top én breedte). Bij 1280 verandert er wél iets,
     en dat is de bedoeling: daar gaat hij van drie regels terug naar twee. */
  color:#fff !important; max-width:min(1180px,88vw); margin:0 0 0 auto; text-align:right;
}
/* …en op Windows daarbovenop de strakkere tracking van deploy mc8qirxys terug.
   Bij 1280 blijft er ook met 88vw maar 2,5% speling over (de sectie begrenst
   daar, niet max-width), en dat is te krap voor een platform dat dezelfde SUSE
   een fractie breder zet. -0.024em scheelt ~0,9% breedte op regel 2.
   Alleen achter .is-win, dus de Mac houdt de tracking die jij nu goed vindt. */
html.is-win .work .sec-intro, html.is-win .more .more-sub{ letter-spacing:-0.024em; }
/* .more has no horizontal padding of its own, so inset the line to line up with the
   "BEYOND THE BRIEF" head-rule above it (same 4.4vw) instead of hitting the edge */
.more .more-sub{ padding:0 4.4vw; margin:0 0 clamp(3vh,6vh,7vh) auto; max-width:none; }

/* when the curiosity-light is carried off on the cursor (scrolled down), the
   about/quote surroundings dim a touch so the lit word + carried glow read
   clearly. It eases back to full on the way up (body.light-carried removed). */
.intro::after{
  content:''; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(to bottom,
    rgba(6,4,3,0) 6%,
    rgba(6,4,3,.30) 40%,
    rgba(6,4,3,.55) 68%,
    rgba(6,4,3,.45) 82%,
    rgba(6,4,3,0) 99%);
  opacity:0; transition:opacity 1s ease;
}
body.light-carried .intro::after{ opacity:1; }
body{ background:transparent !important; }
#aerial-bg{ position:fixed; inset:0; width:100vw; height:100vh; z-index:-1; display:block; pointer-events:none; }
/* the hero shows the living warm background */
.showreel{ background:transparent !important; background-color:transparent !important; }
/* the global aerial canvas replaces footage, so the old film-grain overlays now
   sit ON TOP of it as visible noise → switch them off */
.showreel-grain, .cine-grain{ display:none !important; }
/* The old lamp gated every section to pitch-black until the bulb "lit" them.
   That lamp is gone, so the sections are now always visible on the living B&W
   background — no hidden content, no dependency on a reveal that no longer fires.
   (concept: light is a reward layered on top, never a gate to see the page) */
.positioning, .work, .more, .journey, .about, .finale{
  background-color:transparent !important; }

.hero2{ position:absolute; inset:0; z-index:4; pointer-events:none; }
/* centred headline block, like the reference: DISCIPLINARY (widest) sits symmetric
   about ~48vw; MULTI- on top nudged a touch left; DESIGNER beneath */
.hero2-title{ position:absolute; left:48.7%; top:51%; transform:translate(-50%,-50%); text-align:center; }
.hero2-h1{ font-family:'Telegraf','Archivo',sans-serif; font-weight:400; text-transform:uppercase;
  /* Telegraf ships only 200/400/800 — no real medium. A hair of same-colour
     stroke fakes a ~550 weight off the Regular without jumping to UltraBold. */
  -webkit-text-stroke:1.05px #ffffff;
  font-size:clamp(3.1rem,8.3vw,14.9rem); line-height:.8; letter-spacing:-.02em; color:#ffffff;
  /* no kerning → the intro's per-letter spans (which can't kern across separate
     boxes) lay out identically to this restored text, so the headline doesn't
     shift horizontally under text-align:center when the intro hands back. */
  font-kerning:none; font-feature-settings:"kern" 0;
  /* no stroke = cleaner letters; a soft dark shadow keeps it legible over the
     flowing light without looking like a glow */
  text-shadow:0 2px 38px rgba(0,0,0,.5), 0 1px 6px rgba(0,0,0,.38); }
/* `>` so these only style the LINE spans — not the per-letter .h2c spans the
   intro injects (otherwise the first letter of every line inherits the shift). */
.hero2-h1 > span{ display:block; white-space:nowrap; }
.hero2-h1 > span:first-child{ transform:translateX(-6.5vw); }
/* opening reveal: per-letter clip-mask "roll". Each glyph sits in an overflow
   clip that holds TWO stacked copies — the outgoing one rolls up and out of the
   top while an identical incoming one rises from beneath, so you see both swap
   (a wave of per-letter refreshes). A little headroom so caps aren't clipped. */
/* line-height MUST match the native .hero2-h1 (.8) so the masked reveal lays out
   at the exact final metrics — otherwise the headline "jumps" when the intro
   hands back the plain text at the end. The clip box is sized to EXACTLY the line
   box (no top headroom) so each mask clips in the gap between the lines — the
   rolling glyphs disappear/appear between the words instead of bleeding over the
   neighbouring line. The uppercase caps fit inside the .8 line box, so they are
   not clipped at rest. */
.hero2-h1 .hl-mask{ position:relative; display:inline-block; overflow:hidden; vertical-align:bottom; line-height:.8; }
.hero2-h1 .hl-ch{ display:block; will-change:transform; }
.hero2-h1 .hl-ghost{ position:absolute; left:0; top:0; will-change:transform; }
/* while the per-letter wave plays, drop the big 38px blur shadow: a blurred
   shadow inflates each glyph's GPU layer bounds, which makes compositing 50+
   moving layers per frame expensive. A small crisp shadow keeps legibility for
   the few hundred ms of the roll; the full shadow is restored when it settles. */
.hero2-h1.hl-rolling, .hero2-h1.hl-rolling .hl-ch{ text-shadow:0 1px 4px rgba(0,0,0,.4); }
/* single-line rise-mask for the "01//04 Scroll" label */
.hero2-scroll .rise-mask{ display:inline-block; overflow:hidden; vertical-align:bottom; }
.hero2-scroll .rise-inner{ display:inline-block; will-change:transform; }
/* side text plastered into the corner right of DISCIPLINARY/DESIGNER, level with the
   DESIGNER line — bigger, longer lines; first line indented like the reference */
.hero2-side{ position:absolute; left:70.45vw; top:57.4%; right:auto; bottom:auto; max-width:none; white-space:nowrap; text-align:left; text-indent:2.4em;
  /* anchored on its TOP-LEFT corner (left:70vw / top:57.4%), so bumping the size
     grows the block to the right + downward while its distance to the headline
     stays put. ~+18% over the old clamp(1.02,1.28vw,1.42). */
  font-family:'Telegraf','Archivo',sans-serif; font-weight:400; font-size:clamp(1.14rem,1.42vw,1.59rem);
  line-height:1.22; letter-spacing:.065em; text-transform:uppercase; color:#fff;
  text-shadow:0 1px 16px rgba(0,0,0,.42); }
/* WINDOWS-ONLY FIJNSTELLING van deze zin — de scheiding waar Geert om vroeg.
   Waarom dit veilig is: `.is-win` komt alleen op <html> te staan op een Windows-
   machine (zie de inline head-script in index.html), dus op de Mac bestaat deze
   regel domweg niet. De Mac-positie (left:70.45vw / top:57.4%) blijft exact.
   Waarom `translate` en niet left/top: `top` wordt verderop in de
   canvas-schaal-mediaquery nog eens overschreven (calc(50% + 4.416rem)), dus
   left/top hier zou ik op twee plekken moeten bijhouden. De losse
   translate-eigenschap stapelt bovenop wat er ook staat, en botst niet met
   `transform` — dat gebruikt intro-v4.js voor de intro-animatie van deze zin.
   BEIDE waarden komen nu letterlijk uit deploy mc8qirxys (9 aug 01:06), de
   versie die jij goed noemde — geen schatting meer:
       horizontaal  68.6vw  (toen)  −  70.45vw (nu)  = −1.85vw
       verticaal    5.1rem  (toen)  −  4.416rem (nu) = +0.684rem
   Let op de richting: die zin stond gisteravond dus iets naar LINKS, niet naar
   rechts. Op Windows staat hij hiermee weer exact waar hij stond.
   Bijstellen kan zonder deploy: zet ?winside=<x>,<y> achter de URL op de
   Windows-machine (bijv. ?winside=-2.2vw,0.9rem) en lees af wat klopt. */
@media (min-width:1024px){
  html.is-win .hero2-side{ translate:var(--win-side-x, -1.85vw) var(--win-side-y, .684rem); }
}
.hero2-scroll{ position:absolute; left:5.4vw; top:34%; transform:none;
  font-family:var(--v4-gro),'Telegraf',sans-serif; font-weight:400; font-size:1.02rem; letter-spacing:.04em;
  text-transform:uppercase; color:rgba(255,255,255,.68); }
/* bottom-centre scroll cue — crosshair + circled down-arrow, like the reference */
.hero2-down{ position:absolute; left:50%; bottom:7.5vh; transform:translateX(-50%);
  display:flex; align-items:center; gap:3.2vw; }
.hero2-cross{ position:relative; width:14px; height:14px; opacity:.6; }
.hero2-cross::before,.hero2-cross::after{ content:''; position:absolute; background:rgba(255,255,255,.76); }
.hero2-cross::before{ left:50%; top:0; width:1px; height:100%; transform:translateX(-50%); }
.hero2-cross::after{ top:50%; left:0; height:1px; width:100%; transform:translateY(-50%); }
.hero2-circle{ display:grid; place-items:center; width:clamp(64px,7vw,104px); height:clamp(64px,7vw,104px);
  border:1px solid rgba(255,255,255,.6); border-radius:50%; color:rgba(255,255,255,.96);
  font-size:clamp(1.1rem,1.6vw,1.6rem); line-height:1; }
.hero2-pill{ position:fixed; top:5.35vh; right:5.4vw; z-index:8001; white-space:nowrap; text-decoration:none; pointer-events:auto;
  font-family:var(--v4-gro); font-size:clamp(1.11rem,1.24vw,1.32rem); font-weight:500; letter-spacing:.07em; text-transform:uppercase;
  color:rgba(244,236,224,.9); border:1px solid rgba(244,236,224,.38); border-radius:100px; padding:.73rem 1.5rem;
  overflow:hidden; isolation:isolate;
  /* no transform-transition: it would lag the scroll-scrubbed ride-up out of sync
     with the wordmark/nav/headline */
  transition:color .36s ease, border-color .36s ease, box-shadow .5s ease; }
/* hover → a real metallic GOLD fill wipes in, the button glows like a golden ticket,
   and a bright highlight sweeps diagonally across it */
.hero2-pill::before{ content:''; position:absolute; inset:0; z-index:-1; border-radius:inherit;
  background:linear-gradient(100deg,#f6d985 0%,#d9ab43 38%,#b8862f 60%,#f1cd70 100%); transform:translateX(-101%);
  transition:transform .5s cubic-bezier(.45,.05,.2,1); }
.hero2-pill::after{ content:''; position:absolute; top:0; bottom:0; left:0; width:55%; z-index:-1; border-radius:inherit;
  background:linear-gradient(100deg,transparent 0%,rgba(255,255,255,.7) 50%,transparent 100%);
  transform:translateX(-220%) skewX(-16deg); }
.hero2-pill:hover{ color:#241704; border-color:#f3d27a; translate:0 -1px;
  box-shadow:0 0 0 1px rgba(243,210,122,.55), 0 10px 34px rgba(214,160,55,.5), 0 0 46px rgba(246,217,133,.4); }
.hero2-pill:hover::before{ transform:translateX(0); }
.hero2-pill:hover::after{ transform:translateX(320%) skewX(-16deg); transition:transform .85s cubic-bezier(.45,.05,.2,1) .12s; }
/* hero chrome adjustments: small static wordmark + nav centred; the old sub line is hidden */
.v4-logo .v4-row{ font-size:clamp(2.15rem,3.1vw,3.1rem) !important; }
/* drop the wordmark down so its vertical centre lines up with the nav row;
   tighten the tracking a hair for a more premium, KODE-like set */
.v4-logo{ top:4.8vh !important; letter-spacing:-.035em !important; }
.showreel-sub{ display:none !important; }
.pill-nav{ left:0 !important; right:0 !important; top:5.4vh !important; bottom:auto !important;
  margin:0 auto !important; width:max-content !important;
  /* centred via auto margins (no transform/translate) so `transform` is fully
     free for gsap to drive the vertical travel — in lock-step with the name +
     Get-in-touch, with no CSS transition smoothing it differently */
  translate:none !important; transition:none !important; }

/* top chrome — restore the opacity fade (see the body.case-open note earlier).
   OPACITY ONLY, deliberately: the rule directly above kills every transition on
   .pill-nav so gsap can drive `transform` for the scroll-linked ride-up without
   CSS smoothing it — that must stay true, so re-enabling the whole shorthand
   here would reintroduce exactly the lag that rule exists to prevent. Naming
   just `opacity` leaves transform unsmoothed while giving the case-close its
   fade back, instead of the top chrome hard-cutting in at full strength.
   .hero2-pill likewise keeps its colour/border/shadow easing and no transform. */
.pill-nav{ transition:opacity .5s ease !important; }
.hero2-pill{ transition:color .36s, border-color .36s, box-shadow .5s, opacity .5s ease; }

/* hero scroll-down arrow — a bold chevron inside a ring that DRAWS itself in
   sync with the intro reveal; the chevron drifts gently toward the cursor when
   it's near (no idle bob), click eases down to the next section */
.hero-arrow{
  position:absolute; left:50%; bottom:15vh; transform:translateX(-50%); z-index:7;
  width:122px; height:122px; display:grid; place-items:center;
  background:none; border:none; padding:0; cursor:pointer; color:#fff;
}
.hero-arrow .ha-svg{
  width:118px; height:118px; opacity:.92; overflow:visible;
  transition:opacity .3s ease;
}
.hero-arrow:hover .ha-svg{ opacity:1; }
/* the svg grew 104→128px but the LINES keep their old rendered thickness:
   stroke-width is in viewBox units, so it scales down by 104/128 */
.ha-arrow{ stroke-width:1.79; stroke-linecap:round; stroke-linejoin:round; }
.ha-ring{ stroke-width:1.22; }
.ha-ring{
  opacity:.55;
  /* drawn from the top; the intro animates stroke-dashoffset C -> 0 */
  transform-box:fill-box; transform-origin:center; transform:rotate(-90deg);
  stroke-dasharray:169.65; stroke-dashoffset:169.65;   /* hidden until the intro draws it */
  transition:opacity .3s ease;
}
.hero-arrow:hover .ha-ring{ opacity:.85; }
/* reduced motion / no-intro fallback: show the full ring, no follow */
html.reduced .ha-ring{ stroke-dashoffset:0; }
@media (prefers-reduced-motion:reduce) and (min-width:99999px){ .ha-ring{ stroke-dashoffset:0; } }
@media (max-width:760px){
  .hero2-h1{ font-size:clamp(2.2rem,12vw,4.4rem); }
  .hero2-h2{ font-size:1rem; }
  .hero2-side{ left:50%; top:auto; bottom:12vh; transform:translateX(-50%); text-align:center; max-width:32ch; }
  .hero2-scroll{ display:none; }
  .hero2-pill{ font-size:.48rem; padding:.4rem .75rem; }
}

/* NARROW DESKTOP-PATH WIDTHS (761–1360px) — iPad portrait (768–834), iPad
   landscape (1024) and smaller laptop windows all land here. `.hero2-side`
   is `white-space:nowrap` at `left:70.45vw`, which only clears the right
   edge once the viewport is roughly ≥1333px wide (394px of nowrap text at
   that left offset) — anything narrower runs the sentence off-screen (verified
   at 768px: right edge landed at 935px, 167px past the viewport). Letting it
   wrap and pulling it in from the right edge instead fixes every width in
   this band without touching the ≥1360px desktop rule below it or the
   ≤760px portrait-phone rule above it. */
@media (min-width:761px) and (max-width:1360px){
  .hero2-side{
    /* also moved off `top:57.4%` — that anchor assumes the base 14.9rem-cap
       headline, which at these narrower widths still runs tall enough for
       its own last line to reach past 57% and collide with the now-taller
       (wrapped) paragraph. Anchoring from the bottom instead, well above the
       arrow at bottom:15vh, keeps it clear regardless of exactly how tall
       the headline renders. */
    white-space:normal; max-width:34ch; text-indent:0;
    left:auto; top:auto; right:5vw; bottom:23vh;
    font-size:clamp(.86rem,1.5vw,1rem);
  }
}

/* PHONE LANDSCAPE (short viewport height, still wider than the 760px portrait
   breakpoint — this is the exact gap the portrait rule above doesn't cover).
   Same desktop hero, just fitted to the aspect ratio: the headline shrinks
   enough to clear the down-arrow, and the side paragraph is allowed to WRAP
   instead of running off the right edge (it was nowrap + left:70.45vw, tuned
   for wide desktop widths — never meant to sit in an 812px-wide viewport).
   Scoped tight (max-height:520px) so real desktop/iPad-landscape windows,
   which are always taller than this, never see it. */
@media (orientation:landscape) and (max-height:520px) and (min-width:761px){
  .hero2-h1{ font-size:clamp(1.9rem,8.2vw,3.4rem); line-height:.86; }
  .hero2-side{
    white-space:normal; max-width:30ch; text-indent:0;
    left:auto; right:4vw; top:18%;
    font-size:clamp(.82rem,1.7vw,1rem); line-height:1.3;
  }
  .hero2-scroll{ top:14%; }
  .hero-arrow{ bottom:4vh; width:82px; height:82px; }
  .hero-arrow .ha-svg{ width:78px; height:78px; }
}


/* ============================================================
   02 INTRO — about-teaser (Kalani-style intro page)
   sits right after the hero; pushes Positioning/Work down.
   Background is TRANSPARENT so the fixed #aerial-bg "curiosity of
   light" shader shows through — same atmosphere as the hero.
   ============================================================ */
.intro{
  position:relative; min-height:100vh; z-index:1;
  display:flex; flex-direction:column; justify-content:center;
  padding:14vh 4.4vw 6.5vh;
  background:transparent; overflow:hidden;
}
.intro > *{ position:relative; z-index:1; }

/* kill the drifting hero glow blobs — they flattened the depth */
.hero-glow{ display:none !important; }

/* top-right availability pill */
.intro-pill{
  position:absolute; top:3.4vh; right:4.4vw; z-index:3;
  display:inline-flex; align-items:center; gap:.55rem;
  font-family:var(--v4-mono); font-size:.62rem; letter-spacing:.12em; text-transform:uppercase;
  color:var(--dk-fg); padding:.55rem 1rem; border-radius:999px;
  border:1px solid rgba(247,243,236,.18); background:rgba(247,243,236,.02);
  backdrop-filter:blur(4px);
}

/* the 3-column stage: lead | square photo | learn-more.
   nudged up a little so the photo + text sit higher on the screen. */
.intro-stage{
  flex:1; display:grid; align-items:center;
  grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  gap:clamp(1.5rem,4vw,4.5rem);
  transform:translateY(-3vh);
}

/* left column — a short, wide, quiet line about me, dropped to ~1/3 of the
   portrait's height (ref: Chris K), anchored hard left on the page-head margin */
.intro-lead{ max-width:56ch; align-self:center; justify-self:start; margin-left:2.6vw; transform:translateY(calc(min(62vh,45vw,58rem) * -0.55 + 5.33rem)); }
/* index — sized like the hero's "01//04" scroll marker */
.intro-idx{
  font-family:var(--v4-gro),'Telegraf',sans-serif; font-weight:400;
  font-size:1.5rem; letter-spacing:.04em; text-transform:uppercase;
  color:rgba(244,236,224,.62); margin-bottom:1.6rem;
}
/* about copy — short, wide, calm (hero-side type), a touch bigger */
.intro-copy{
  font-family:'Telegraf','Archivo',sans-serif; font-weight:400;
  /* font scales with the viewport so the left column and the type shrink
     together — that keeps the three hand-set line breaks intact across the
     desktop range instead of wrapping to 4–5 lines on narrower screens.
     0.90vw, niet 1.0: de langste regel ("visual storytelling and AI
     animation.") moet nog passen in de linkerkolom op 1440x900, waar het
     portret (min(62vh,45vw,58rem)) de kolom tot ~295px terugdringt */
  font-size:clamp(1.05rem,0.90vw,1.5rem); line-height:1.4;
  letter-spacing:.03em; text-transform:uppercase; text-indent:2.4em;
  color:rgba(255,255,255,.92); text-shadow:0 1px 16px rgba(0,0,0,.42);
}
/* ---------- BIO POPUP — the Learn more second layer ----------
   The landing dims a step toward black; a clean dark text block (same
   background family as the page, white text) rises with the two bio
   paragraphs. The case-file cross sits under the text to go back. */
/* zijpadding 4vw → 2vw: de pane mag breder, de hoogte (3vh) blijft zoals hij was */
.bio-pop{ position:fixed; inset:0; z-index:9500; display:grid; place-items:center; padding:3vh 2vw; }
/* display:grid would silently beat the UA's [hidden]{display:none} — keep the
   closed popup truly gone, or it sits invisibly over the page eating clicks */
.bio-pop[hidden]{ display:none; }
/* while the popup is open the landing stays visible but recedes — dimmed,
   softened and pulled back a touch, so the panel reads as a block floating
   ABOVE the page (a real 3D layer) instead of the page vanishing behind it */
body.bio-open > :not(#aerial-bg):not(.bio-pop){
  opacity:.14 !important; filter:blur(5px) saturate(.7);
  transform:scale(.975);
  transition:opacity .5s ease, filter .5s ease, transform .5s cubic-bezier(.4,0,.2,1);
}
/* SLUITEN: zodra .bio-open eraf gaat matcht de regel hierboven niet meer, en
   dus verdwijnt óók zijn transition. Filter en transform sprongen daardoor
   terug terwijl opacity aan een andere regel (1.2s) bleef hangen — drie
   eigenschappen die los van elkaar terugkwamen. .bio-closing houdt ze tijdens
   de terugweg op één curve en één duur. */
body.bio-closing > :not(#aerial-bg):not(.bio-pop){
  transition:opacity .34s ease, filter .34s ease, transform .34s cubic-bezier(.4,0,.2,1);
}
/* the giant scrolling "I follow the light of curiosity" line reads as a loud
   moving shape even dimmed — it competes with the panel far more than any
   static content behind it, so it goes fully invisible instead of just dim */
body.bio-open .intro-name{ opacity:0 !important; transition:opacity .35s ease; }
/* zelfde val als hierboven: zonder dit knalt de reuzenregel bij het sluiten in
   één frame terug op vol — de luidste beweging op de pagina */
body.bio-closing .intro-name{ transition:opacity .34s ease; }
/* a light dim for depth only — the motion must stay clearly visible */
.bio-pop-backdrop{
  /* .45 → .72: het portret achter het paneel scheen door de tekst heen;
     dieper zwart laat de foto wegvallen terwijl de shader-beweging net leeft */
  position:absolute; inset:0; background:rgba(4,3,3,.72);
  /* .45s → .3s: de scrim bleef anders nog hangen nadat de pane al weg was */
  opacity:0; transition:opacity .3s ease;
}
/* a real second layer, not a small dialog: near full-bleed and genuinely
   roomy — the journey strip inside gets real space to breathe (bigger
   frames, more of the strip visible at once) and the panel reads as its
   own page, not a cramped modal */
.bio-pop-panel{
  /* 132rem was de bindende cap (niet de 93vw) — beide mee omhoog, anders bijt
     de volgende meteen en wordt de pane geen millimeter breder */
  position:relative; width:min(96vw, 140rem); padding:7.6rem 7.4rem 5.2rem;
  /* GEEN eigen vulling: het frame is een venster op dezelfde achtergrond als de
     landing. #aerial-bg is uitgezonderd van de bio-open dim (zie daar), dus de
     shader draait gewoon door achter dit paneel en levert hier de sfeer —
     precies wat de landing zelf ook doet (haar secties zijn transparant).
     Geen backdrop-filter: de landing blurt haar shader ook niet, dus dat zou
     het frame juist weer anders maken dan de pagina eromheen.
     De .18 is bewust klein: net genoeg dat het frame zich van de scrim eromheen
     aftekent, te weinig om de shader te smoren. Vanaf ~.30 valt die merkbaar
     weg en kruip je terug naar de vlakke plaat die het hiervoor was. */
  background:rgba(9,7,6,.18);
  border:1px solid rgba(247,243,236,.24); border-radius:26px;
  /* de hairline blijft dun — een dikkere lijn leest zwaarder, niet duurder —
     een zachte neutrale glow net buiten de rand geeft de edge presence */
  box-shadow:0 70px 180px -30px rgba(0,0,0,.85),
             0 0 60px -10px rgba(247,243,236,.35);
  /* ASYMMETRIC motion: de staat die betreden wordt bezit de transition, dus
     deze base-state is de EXIT en .on de entree (.65s expo-out).
     De exit was .5/.56s op cubic-bezier(.4,0,1,1) — een harde ease-in, dus het
     paneel bleef eerst hangen en schoot er dan versneld uit; met 24px + blur 6
     las dat als wegvallen i.p.v. wegleggen. Nu: kortere reis (14px), lichtere
     focus-pull (3px) en de standaardcurve die uitloopt i.p.v. accelereert —
     sneller op de klok (.34s) én rustiger voor het oog. */
  opacity:0; transform:translateY(14px) scale(.988); filter:blur(3px);
  transition:opacity .3s cubic-bezier(.4,0,.2,1), transform .34s cubic-bezier(.4,0,.2,1), filter .3s ease;
  /* the journey strip bleeds to the panel edges (negative margins) — clip it
     on the rounded corners; y stays auto so short screens can scroll the panel
     (bottom padding keeps content clear of the floating cross) */
  max-height:91vh; overflow-x:hidden; overflow-y:auto; scrollbar-width:none;
}
.bio-pop-panel::-webkit-scrollbar{ display:none; }
.bio-pop.on .bio-pop-backdrop{ opacity:1; transition-duration:.5s; }
.bio-pop.on .bio-pop-panel{
  opacity:1; transform:translateY(0) scale(1); filter:blur(0);
  transition:opacity .45s ease, transform .65s cubic-bezier(.16,1,.3,1), filter .45s ease;
}
/* the CONTENT arrives as a cascade, not as one flat slab: the head draws in
   first, then the facts rail, the two paragraphs, and finally the journey
   strip — each on the same expo-out curve as the panel, a beat apart (tight,
   so the cascade reads as one clean sweep, not a slow drip). The base state
   has NO delay and a short duration, so closing collapses everything
   together instantly instead of playing the stagger backwards. */
.bio-head, .bio-me-grid .ab-facts, .bio-me-body .bio-body, .bio-jr{
  opacity:0; transform:translateY(26px);
  /* Dit is óók de exit-staat. Voorheen zakte elk blok bij het sluiten zelf nog
     eens 26px terwijl de pane al wegzakte: beweging op beweging, precies wat
     het sluiten rommelig maakte. Nu 0s met .34s delay — de inhoud blijft
     gewoon staan, de pane vertrekt als één object, en pas als die onzichtbaar
     is springen de blokken terug naar hun startpositie voor de volgende keer. */
  transition:opacity 0s linear .34s, transform 0s linear .34s;
}
.bio-pop.on .bio-head, .bio-pop.on .ab-facts, .bio-pop.on .bio-body, .bio-pop.on .bio-jr{
  opacity:1; transform:none;
  transition:opacity .5s ease, transform .6s cubic-bezier(.16,1,.3,1);
}
.bio-pop.on .bio-head{ transition-delay:.05s; }
.bio-pop.on .ab-facts{ transition-delay:.12s; }
.bio-pop.on .bio-body:nth-of-type(1){ transition-delay:.17s; }
.bio-pop.on .bio-body:nth-of-type(2){ transition-delay:.24s; }
.bio-pop.on .bio-jr{ transition-delay:.31s; }
@media (prefers-reduced-motion:reduce) and (min-width:99999px){
  .bio-pop-panel, .bio-head, .bio-me-grid .ab-facts, .bio-me-body .bio-body, .bio-jr{
    transition:opacity .3s ease; transform:none; filter:none;
  }
}
/* the section head, same hairline-rule + tracked-label language as the rest
   of the site's page-heads (see .page-head-rule) — ties the popup into the
   established visual system instead of floating a bare label */
.bio-head{ margin:0 0 3.7rem; }
.bio-head-rule{ height:2px; width:100%; background:rgba(247,243,236,.85); }
.bio-label{
  display:block; margin:1.8rem 0 0;
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:1.58rem;
  letter-spacing:.34em; text-transform:uppercase; color:rgba(247,243,236,.95);
}
/* two equal paragraphs, left-aligned like the rest of the site's body copy */
.bio-pop-panel p.bio-body{
  max-width:56ch; margin:0 0 4.85rem; text-align:left;
  font-family:'Telegraf','Archivo',sans-serif; font-weight:400;
  font-size:clamp(2.05rem,2.85vw,2.7rem); line-height:1.5; letter-spacing:.005em;
  color:rgba(247,243,236,.88);
  text-wrap:pretty;   /* browsers that support it: no single-word orphan lines */
}
.bio-pop-panel p.bio-body:last-of-type{ margin-bottom:0; }
/* the quick-facts rail (ab-facts, shared with the old about page) sits left
   of the two paragraphs — same quiet-rail / story axis the about page used.
   The rail centres itself vertically (align-self on .ab-facts). */
.bio-me-grid{
  display:grid;
  /* the rail only needs to fit its own values (not a % of the panel) — at
     27% it ate enough width that the first paragraph wrapped to 4 lines
     instead of 3. Sized to content instead, it frees that width back to the
     story; "Focus" (the longest value) wraps to a second line in exchange —
     a far smaller cost than the story running long. */
  grid-template-columns:clamp(15.5rem,15.5vw,19rem) 1fr;
  column-gap:clamp(3.45rem,4.6vw,4rem); align-items:start;
}
/* the rail in the POPUP sits a touch tighter than the about page's — smaller
   type, closer rows — so five facts read as one quiet block, not a column */
.bio-me-grid .ab-facts{ row-gap:clamp(.7rem,1.2vh,1rem); }
.bio-me-grid .abf-k{ font-size:clamp(1rem,.85vw,1.17rem); }
.bio-me-grid .abf-v{ font-size:clamp(1.23rem,1.06vw,1.46rem); }
/* the story hugs the panel's right edge, ragged-left — the mirror of the
   left rail, so the two columns read as one spread */
.bio-me-body{ justify-self:end; }
.bio-me-body p.bio-body{ text-align:right; margin-left:auto; }
/* the first paragraph balances its three lines to near-equal length (the last
   line was landing much shorter than the first two) — balance keeps the 3-line
   count while evening the ragged edge */
.bio-me-body p.bio-body:nth-of-type(1){ text-wrap:balance; }
/* the second paragraph is pinned NARROWER than p1 so its exact line breaks are
   preserved when the text column widens for p1 (which needs the room to fit 3
   lines). 44ch reproduces p2's three original lines at the reduced font size —
   ch-based so the wrap holds as the type scales. */
.bio-me-body p.bio-body:nth-of-type(2){ max-width:44ch; }
@media (max-width:860px){
  .bio-me-grid{ grid-template-columns:1fr; row-gap:2.6rem; }
  /* one column again: back to left-aligned reading text */
  .bio-me-body{ justify-self:stretch; }
  .bio-me-body p.bio-body{ text-align:left; margin-left:0; }
}
/* the case-file cross — floats over the panel's bottom edge as a direct
   child of the overlay, so it's always visible and never scrolls away */
.bio-pop .case-close{
  position:absolute; bottom:6.5vh; left:50%; transform:translateX(-50%);
  z-index:3; opacity:0; transition:opacity .45s ease .15s;
}
.bio-pop.on .case-close{ opacity:1; }
/* ---- JOURNEY STRIP IN THE POPUP — the filmstrip re-homed from the landing.
   Geometry only: every light behaviour (frame opacity, exposure, year/title
   alpha, the colour clone) is inherited from the .abfs-* rules above; this
   block shrinks the 100vh-viewport frames to panel scale and swaps the
   scroll-scrub for a native horizontal scroller (main.js drives --lit from
   the scroll position). The track's side padding centres the first and last
   frame in the strip, so the light band can reach both ends. ---- */
.bio-jr{ margin-top:7.9rem; }
/* same hairline-rule head as the About me block — one visual system */
.bio-jr-head{ margin-bottom:3.45rem; }
.bio-jr-row{
  display:flex; justify-content:space-between; align-items:baseline;
  margin-top:1.8rem;
}
.bio-jr-label{
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:1.58rem;
  letter-spacing:.34em; text-transform:uppercase; color:rgba(247,243,236,.95);
}
.bio-pop .abfs-count{
  position:static; font-size:1rem; letter-spacing:.3em;
  color:rgba(247,243,236,.4);
  font-family:var(--v4-mono),'Space Mono',monospace;
}
/* natively scrollable strip: vertical wheel ANYWHERE in the popup scrubs it
   (main.js), mouse-drag scrubs it, touch pans it natively */
/* ---- VOORTGANG + AANWIJZING BIJ DE JOURNEY ----
   Het probleem was niet dat de strip slecht werkte, maar dat hij niets liet
   zien: één frame in beeld, geen randje van het volgende, geen maat waaraan je
   afleest dat er negen zijn. Dan is er geen reden om door te scrollen.
   De balk staat direct onder de kop-regel, op dezelfde hairline-breedte als de
   rest van het blok, en vult zich met de scrub mee. --p wordt vanuit main.js
   gezet (glow(), die toch al bij elke scrollbeweging draait). ---- */
.bio-jr-prog{
  position:relative; height:2px; margin-top:1.1rem; border-radius:2px;
  background:rgba(247,243,236,.13);
}
.bio-jr-prog i{
  position:absolute; left:0; top:0; bottom:0; border-radius:inherit;
  width:calc(var(--p, 0) * 100%); background:rgba(247,243,236,.62);
  /* geen transition: de balk moet exact op de scrub liggen, niet erachteraan */
}
.bio-jr-hint{
  display:flex; align-items:center; justify-content:center; gap:.6em;
  margin-top:1.1rem;
  font-family:var(--v4-mono),'Space Mono',monospace;
  font-size:.82rem; letter-spacing:.2em; text-transform:uppercase;
  color:rgba(247,243,236,.5);
  transition:opacity .5s ease, transform .5s ease;
}
.bio-jr-hint svg{ width:1.1em; height:1.1em; }
/* de pijl wipt rustig op en neer — genoeg om op te vallen, niet genoeg om te
   irriteren; staat stil zodra iemand liever geen beweging ziet */
.bio-jr-hint svg{ animation:jrHintBob 1.9s ease-in-out infinite; }
@keyframes jrHintBob{ 0%,100%{ transform:translateY(-2px); } 50%{ transform:translateY(2px); } }
@media (prefers-reduced-motion:reduce){ .bio-jr-hint svg{ animation:none; } }
/* zodra de strip in beweging is: weg, en blijvend weg */
.bio-jr.has-moved .bio-jr-hint{ opacity:0; transform:translateY(-4px); pointer-events:none; }

.bio-strip{
  overflow-x:auto; overflow-y:hidden; scrollbar-width:none; cursor:grab;
  margin:0 -10.4rem;                    /* bleed to the panel edges */
  touch-action:pan-x pan-y;
}
.bio-strip::-webkit-scrollbar{ display:none; }
.bio-strip.dragging{ cursor:grabbing; }
.bio-strip img{ -webkit-user-drag:none; user-select:none; }
.bio-strip .abfs-track{
  /* big frames — the strip fills the panel down to the bottom edge instead
     of leaving a dead band of empty space under it */
  --bjf:clamp(32rem, 38vw, 49.5rem);
  padding:2rem calc(50% - var(--bjf) / 2);
  column-gap:2.2rem;
}
.bio-strip .abfs-track::before{ top:.6rem; height:.75rem; }
.bio-strip .abfs-track::after{ bottom:.6rem; height:.75rem; }
.bio-strip .abfs-frame{
  width:var(--bjf); min-height:0; padding:1.9rem 2rem 2rem; gap:1.2rem;
  border-radius:8px;
}
.bio-strip .abfs-meta{ top:1.6rem; left:2rem; right:2rem; }
.bio-strip .abfs-no{ font-size:.95rem; letter-spacing:.2em; }
.bio-strip .abfs-year{ font-size:2.9rem; }
.bio-strip .abfs-img{ margin-top:3.5rem; }
.bio-strip .abfs-title{ font-size:2.3rem; line-height:1.22; }
.bio-strip .abfs-note{ font-size:1.44rem; line-height:1.6; min-height:3.6em; }
.bio-strip .abfs-load{ font-size:1.4rem; letter-spacing:.26em; gap:1.1rem; }
.bio-strip .abfs-dots{ gap:.7rem; }
.bio-strip .abfs-dots i{ width:.68rem; height:.68rem; }
@media (max-width:760px){
  .bio-pop{ padding:2.5vh 3.5vw; }
  .bio-pop-panel{ width:100%; padding:2.8rem 2rem 7rem; }
  .bio-head{ margin-bottom:2rem; }
  .bio-label{ font-size:.85rem; letter-spacing:.28em; margin-top:1.2rem; }
  .bio-pop-panel p.bio-body{ font-size:1.15rem; max-width:none; margin-bottom:1.6rem; }
  /* the quick-facts rail: the ≤860 rule turns it into a wrapping ROW, which on
     a phone put "Age" and "Based in" side by side while the two long facts got
     a line each — a ragged mix. One fact per line reads as one clean column. */
  .bio-me-grid .ab-facts{ flex-flow:column nowrap; gap:1.1rem; }
  .bio-jr{ margin-top:3.4rem; position:relative; }
  .bio-jr-label{ font-size:.85rem; letter-spacing:.28em; }
  /* De voortgangsbalk plakt bovenaan zolang je door de journey scrubt. Op de
     telefoon is dat het verschil tussen wel en niet weten dat er meer komt: de
     strip staat gepind in het midden van het paneel en de kop is dan allang
     weggescrold, dus zonder dit had je tijdens het scrubben geen enkel houvast
     meer. De strip zelf pint lager (main.js zet zijn top), dus ze zitten elkaar
     niet in de weg; z-index houdt de balk erboven. */
  .bio-jr-prog{ position:sticky; top:0; z-index:3; margin-top:.9rem; }
  .bio-jr-hint{ font-size:.7rem; letter-spacing:.16em; margin-top:.9rem; }
  /* PHONE RAIL: the strip pins itself while the panel's own vertical scroll
     scrubs it sideways (main.js sizes the spacer under it + drives scrollLeft),
     so scrolling down runs through the journey exactly like the desktop wheel
     rail does. Native sideways panning is off — it would fight that. */
  .bio-strip{
    margin:0 -2rem; position:sticky; top:0; z-index:1;
    overflow-x:hidden; touch-action:pan-y; cursor:default;
  }
  /* frame proportions for a PORTRAIT screen: the 3/2 photo left a tall dead
     band inside a frame whose min-height was 54vh, and the year was set at
     desktop scale (2.9rem) so "2012–18" wrapped to two lines. Square photo,
     content-height frame, year back to label scale. */
  .bio-strip .abfs-track{ --bjf:80vw; column-gap:1.1rem; padding-top:1.3rem; padding-bottom:1.3rem; }
  .bio-strip .abfs-track::before{ top:.45rem; height:.55rem; }
  .bio-strip .abfs-track::after{ bottom:.45rem; height:.55rem; }
  .bio-strip .abfs-frame{ min-height:0; padding:1.2rem 1.2rem 1.4rem; gap:.7rem; }
  .bio-strip .abfs-meta{ top:1.15rem; left:1.2rem; right:1.2rem; }
  .bio-strip .abfs-no{ font-size:.68rem; letter-spacing:.16em; }
  .bio-strip .abfs-year{ font-size:1.35rem; }
  .bio-strip .abfs-img{ margin-top:2.4rem; aspect-ratio:1; }
  .bio-strip .abfs-unexp{ aspect-ratio:1; }
  .bio-strip .abfs-load{ font-size:.72rem; letter-spacing:.18em; gap:.6rem; }
  .bio-strip .abfs-dots{ gap:.4rem; }
  .bio-strip .abfs-dots i{ width:.34rem; height:.34rem; }
  .bio-strip .abfs-title{ font-size:1.15rem; }
  .bio-strip .abfs-note{ font-size:.82rem; line-height:1.55; min-height:4.65em; }
}
.intro-social{ margin-top:2.2rem; display:flex; flex-wrap:wrap; gap:1.6rem; }
.intro-social a{
  font-family:var(--v4-mono); font-size:.66rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--taupe); text-decoration:none; position:relative; transition:color .25s ease;
}
.intro-social a::after{
  content:""; position:absolute; left:0; bottom:-4px; width:100%; height:1px;
  background:currentColor; transform:scaleX(0); transform-origin:left; transition:transform .3s ease;
}
.intro-social a:hover{ color:var(--dk-fg); }
.intro-social a:hover::after{ transform:scaleX(1); }

/* center photo — SQUARE, sized by viewport height so it stays big but always
   fits on a single screen (good on 1440p), pale panel, rounded, soft lift */
.intro-photo{
  margin:0; aspect-ratio:auto; width:min(62vh,45vw,58rem); height:calc(min(62vh,45vw,58rem) * 1.1);
  justify-self:center; border-radius:18px; overflow:hidden;
  /* the real scene: the empty-background photo (achtergrond v10) sits behind
     the person cut-out as an <img> layer; this is only a loading fallback */
  background:#141414;
  position:relative;
  box-shadow:0 40px 90px rgba(0,0,0,.55);
  transform:translateX(0); will-change:transform;
}
/* transparent cut-out of the person on a studio backdrop. On hover the person
   rises + grows → he steps forward while the backdrop stays put. */
.intro-photo img{
  position:absolute; inset:0;
  width:100%; height:100%; object-fit:cover; object-position:50% 30%;
  filter:saturate(.96) contrast(1.02);
  transform:scale(1.1); transform-origin:50% 42%;
}
/* light vignette on the studio backdrop (below the person) so the corners fall
   off softly and the person reads forward. Subtle. */
/* inner-border vignette on the landscape (sits UNDER the person, so he stays clean) */
.intro-photo::before{
  content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  border-radius:inherit;
  /* three layers: film grain (tooth on the blurred wall) · diagonal shade
     (the key light comes from top-left, so bottom-right falls away — the
     directionality that keeps the wall from reading flat) · edge vignette */
  background:
    var(--film-grain),
    linear-gradient(135deg, transparent 50%, rgba(0,0,0,.10) 80%, rgba(0,0,0,.18) 100%),
    linear-gradient(to bottom, rgba(0,0,0,.22) 0%, rgba(0,0,0,.08) 9%, transparent 20%),
    radial-gradient(ellipse 70% 88% at 50% 46%,
      transparent 22%, rgba(0,0,0,.04) 38%, rgba(0,0,0,.10) 52%, rgba(0,0,0,.19) 65%,
      rgba(0,0,0,.30) 78%, rgba(0,0,0,.42) 89%, rgba(0,0,0,.55) 100%);
  background-size:160px 160px, 100% 100%, 100% 100%, 100% 100%;
}
/* the background layer of the scene — identical crop rules to the person via
   the shared `.intro-photo img` block; only stacking differs.
   Heavy blur + desaturation melts the landscape into one tonal wash, so it
   reads as a seamless studio backdrop; scaled up to hide blurred edge fringe.
   Colours stay sourced from the real plate, so the cut-out edges keep matching. */
.intro-photo .ip-bg{
  z-index:0;
  /* whole grade (blur 28 + saturate 0 + brightness .82 + contrast 1.05) is
     baked into studio-wand-1280-baked82.jpg — live CSS blur was GPU-tiled and
     intermittently showed its tile seams as light bands. filter:none keeps the
     `.intro-photo img` base grade off the baked bitmap */
  filter:none;
  transform:scale(1.25);
}
/* soft key-light pool behind the head — the "studio" glow; under the person.
   OFF-AXIS (top-left) so the wall has a light direction instead of a flat
   symmetric halo; pairs with the diagonal shade on ::before */
.intro-photo .ip-light{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  /* staande ovaal die de FIGUUR volgt (hoofd + torso), niet alleen het hoofd —
     iets links van het midden zodat de lichtrichting behouden blijft */
  background:radial-gradient(ellipse 44% 72% at 45% 40%,
    rgba(255,255,255,.36) 0%, rgba(255,255,255,.12) 48%, transparent 75%);
}
/* cast shadow of the person on the wall — same recipe as .abh-cast on the
   about page: a gradient layer down-right of the figure (light is top-left),
   over the light pool, under the person. No drop-shadow filter (GPU seams). */
.intro-photo .ip-cast{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:radial-gradient(ellipse 34% 62% at 57% 64%,
    rgba(0,0,0,.34) 0%, rgba(0,0,0,.15) 46%, transparent 72%);
}
.intro-photo .ip-fg{
  z-index:2;
  /* Vertical placement is SOLVED, not eyeballed. With transform-origin 50% 42%
     the head lands at:  y = .42 + s * (headTop + t - .42)
     where headTop is the first non-transparent row of the cut-out, as a
     fraction of the bitmap (canvas-measurable — see the notes in memory).
     The old t=-7.5% was tuned for a 3504x2729 cut-out; on the current file it
     put the head ABOVE the frame and cropped it. t=+6% lands the head at ~8%
     with the body overshooting the bottom edge (~116%), so no transparent gap
     can appear. Re-measure headTop and re-solve if the portrait is swapped. */
  /* NB: scaling up also lifts the head (the origin sits at 42%), so t must grow
     with s to keep — or lower — the head position. s 1.17->1.24 with t 6%->8.2%
     nets a slightly larger figure sitting slightly lower (head 8.8% -> 9.5%). */
  transform:scale(1.30) translateY(7%) translateX(1.3%);
  /* de JPEG-uitsnede draagt GEEN gebakken schaduw — daarom een live drop-shadow
     die het silhouet volgt (key light links-boven → val rechts-onder). Twee
     lagen: strakke contactschaduw dicht op de figuur + bredere zachte wandval.
     Ruime blur houdt de rand vloeiend zodat er geen tegelnaad-banden achter het
     hoofd ontstaan (de oude reden voor filter:none). */
  filter:
    drop-shadow(4px 5px 8px rgba(0,0,0,.30))
    drop-shadow(13px 17px 28px rgba(0,0,0,.32));
  /* GEEN will-change:transform — dat pinde de GPU-textuur op de onvergrote
     grootte, waarna de scale(1.30) 'm opschaalde = onscherp (de "online blur").
     De match-cut is van de landing af, dus de laag-promotie hoeft niet meer;
     zonder promotie rastert de browser op de geschaalde resolutie → scherp,
     schaduw incluis. */
}
/* lower scrim — a soft dark gradient that fades the photo's bottom into the dark
   page exactly where the marquee crosses it, so the white quote always reads and
   the line clearly sits in front of the portrait. Sits ABOVE the person so his
   lower body fades too; photo + person stay bright up top. */
.intro-photo::after{
  content:""; position:absolute; left:0; right:0; bottom:0; height:46%;
  background:linear-gradient(to top,
    rgba(15,13,11,.82) 0%, rgba(15,13,11,.5) 38%, rgba(15,13,11,.18) 70%, transparent 100%);
  pointer-events:none; z-index:2;
}

/* right: clean learn-more circle — no orbit animation, bigger label */
.intro-more{
  justify-self:start; position:relative; transform:translate(5.92rem, 1.105rem);
  width:clamp(9rem,9.3vw,12.1rem); aspect-ratio:1; border-radius:50%;
  display:grid; place-items:center; text-decoration:none;
  transition:transform .6s cubic-bezier(.22,1,.36,1);
  will-change:transform;
}
.intro-more:hover{ transform:translate(5.92rem, 1.105rem) scale(1.06); }
.intro-more-ring{
  position:absolute; inset:0; border-radius:50%;
  border:2px solid rgba(247,243,236,.5);
  transition:transform .6s cubic-bezier(.22,1,.36,1), border-color .45s ease;
  will-change:transform;
}
/* hover = a content SWAP: the ivory fill wipes up from the base as a circular
   reveal, the "learn / more" label slides up and out, and an arrow rises in to
   take its place — crisp and directional, not a plain invert.
   The fill is a transform SCALE from the base, not an animated clip-path:
   clip-path repaints on the main thread every frame, so it stuttered whenever
   the shader/scroll had a heavy frame — scale runs on the compositor. */
.intro-more::before{
  content:''; position:absolute; inset:0; border-radius:50%; z-index:0;
  background:#f4ece0; transform:scale(0); transform-origin:50% 100%;
  transition:transform .65s cubic-bezier(.22,1,.36,1);
  will-change:transform;
}
.intro-more:hover::before{ transform:scale(1); }
.intro-more::after{
  content:''; position:absolute; inset:0; z-index:2; pointer-events:none;
  background:no-repeat center / 32% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230a0a0a' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 18 L18 6'/%3E%3Cpath d='M8 6 H18 V16'/%3E%3C/svg%3E");
  transform:translateY(72%); opacity:0;
  transition:transform .6s cubic-bezier(.22,1,.36,1), opacity .35s ease;
}
.intro-more:hover::after{ transform:translateY(0); opacity:1; }
.intro-more:hover .intro-more-ring{ transform:scale(1.1); border-color:#f4ece0; }
.intro-more:hover .intro-more-label{ transform:translateY(-72%); opacity:0; }
.intro-more-label{
  position:relative; z-index:1;
  display:flex; flex-direction:column; align-items:center;
  font-family:var(--v4-mono); font-size:1.85rem; font-weight:400; letter-spacing:.04em; text-transform:uppercase;
  line-height:1; color:var(--dk-fg);
  transition:transform .6s cubic-bezier(.22,1,.36,1), opacity .35s ease, color .45s ease;
}
/* symmetric stagger around the button centre: "learn" sits up-left, "more"
   drops down-right, but the diagonal is balanced on the centre */
.intro-more-label .iml-1{ transform:translateX(-0.5em); }
.intro-more-label .iml-2{ transform:translateX(0.5em); margin-top:.16em; }
/* click = a press + sonar ping: the button compresses while the ring detaches
   and expands outward, fading — then main.js hands off to the curtain wipe
   toward the full About page. One gesture: press, ping, sweep. */
.intro-more.go{ transform:translate(5.92rem,1.105rem) scale(.93); }
.intro-more.go::after{ transform:translateY(0) scale(1.12); opacity:1; }
.intro-more.go .intro-more-ring{ animation:morePing .55s cubic-bezier(.2,.7,.3,1) forwards; }
@keyframes morePing{
  0%   { transform:scale(1);   opacity:1; border-color:#f4ece0; }
  100% { transform:scale(1.9); opacity:0; border-color:#f4ece0; }
}

/* bottom: oversized name on a seamless right→left marquee loop,
   in the site's display face (SUSE) */
.intro-name{
  /* sits BEHIND the portrait, anchored to its lower edge: ~2/3 of the line is
     hidden behind the photo, only the bottom ~1/3 peeks out below + bleeds out
     the sides (ref: Chris K big type behind image). Anchored to centre + offset
     so it tracks the photo regardless of section height. */
  position:absolute; left:0; right:0; top:calc(50% + 25.5vh); bottom:auto; z-index:2;
  pointer-events:none; overflow:visible;
  /* roomy line-height + bottom padding so descenders (the "g" in light, the
     "y" in curiosity) are NOT clipped */
  line-height:1.12; padding-bottom:.16em;
}
.intro-name-track{
  display:inline-flex; align-items:flex-end; width:max-content; will-change:transform;
  animation:introMarquee 16s linear infinite;
  /* houd de bewegende laag op ÉÉN stabiele GPU-textuur: translate3d in de
     keyframes + backface-hidden voorkomt dat de browser 'm per scroll-frame
     her-tilet (dat gaf de lichte dubbelbeeld-flikkering / lag rond de marquee). */
  backface-visibility:hidden; -webkit-backface-visibility:hidden;
}
.intro-name-track span{
  flex:none; white-space:nowrap; padding-right:.18em;
  font-family:'Telegraf','Archivo',sans-serif; font-weight:200; font-style:normal;
  font-size:clamp(2.85rem,12vw,17rem); letter-spacing:-.01em; line-height:1.04;
  /* crisp solid white with a faint STATIC veil in the letters. The veil used
     to drift (nameSheen, background-position animation): animating a
     background-clip:text fill re-rasters these giant glyph textures (4 spans
     × ~3500px, 55px-blurred shadows) EVERY frame on the CPU — the standing
     lag of the whole intro section. The marquee itself (transform on the
     track) is the motion; the letters now raster once and just slide. */
  background:linear-gradient(105deg,
    #ffffff 0%, #ffffff 44%,
    rgba(255,255,255,.82) 50%,
    #ffffff 56%, #ffffff 100%);
  background-size:240% 100%;
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent; color:transparent;
  /* DEPTH — a soft, diffuse dark cloud behind the words; invisible on the black
     section, it only pools where the line crosses the bright portrait. Blur
     bewust kleiner (55→26px): een 55px-blur over de ~11000px-brede bewegende
     laag was zó duur om te rasteren dat de voorste tegels tijdens het schuiven
     verouderd in beeld kwamen = de lichte dubbelbeeld-flikkering. 26px pool ziet
     er nagenoeg gelijk uit maar rastert veel goedkoper. */
  text-shadow:0 6px 26px rgba(0,0,0,.5), 0 2px 12px rgba(0,0,0,.4);
}
/* "light" — same crisp white + the same faint in-letter veil, plus a soft cool
   white glow (the lit word). JS adds .is-spent → it dims when the spark leaves. */
.intro-name-track .qlight{
  font-style:normal;
  /* DE AFGEKNIPTE STAART VAN DE "g". Dit woord is een <em>, dus een INLINE
     element, en zijn achtergrond wordt geschilderd op de inline-box — die loopt
     van de bovenkant naar de onderkant van de regelbox, niet tot onder de
     staarten. Met background-clip:text + text-fill-color:transparent komt álle
     kleur uit die achtergrond, dus wat eronder uitsteekt krijgt simpelweg geen
     vulling en valt weg. Niets knipt hem af; hij wordt nooit geschilderd.
     Verticale padding op een inline element telt niet mee voor de regelhoogte
     (en verticale marges gelden er niet), dus dit vergroot alleen het
     schildervlak: de layout blijft tot op de pixel gelijk. Zelfde patroon als
     de padding-right-truc die hier eerder voor de rechterrand nodig was. */
  padding-bottom:.2em;
  background:linear-gradient(105deg,
    #ffffff 0%, #ffffff 44%,
    rgba(255,255,255,.86) 50%,
    #ffffff 56%, #ffffff 100%);
  background-size:240% 100%;
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent; color:transparent;
  text-shadow:0 10px 55px rgba(0,0,0,.46), 0 0 22px rgba(240,238,233,.55), 0 0 64px rgba(226,223,216,.34);
  /* ALLEEN opacity — een text-shadow-transition kan niet op de compositor en
     dwong bij elke scroll-passage (is-spent aan/uit) 1.1s lang per frame een
     CPU-herraster van de reuzenlaag af: dat was de lag én het dubbelbeeld
     (verse naast verouderde tiles) rond de quote. Element-opacity dimt de
     glow-schaduw net zo goed mee, dus visueel verandert er niets. */
  transition:opacity 1.1s ease;
  /* eigen kleine compositorlaag per woord: de opacity-fade en de curioCharge-
     flare raken dan alleen deze ~woordgrote textuur, nooit meer de volle
     11000px-brede marquee-laag */
  will-change:opacity;
}
/* (the old nameSheen background-position drift was removed here: per-frame
   re-raster of the background-clip:text fills = the intro-section lag) */
/* after the spark leaves, the word truly goes OUT — a burnt-out ghost of
   itself: the light is no longer in the word, it's on your cursor.
   NB: dim via OPACITY — a translucent text-fill-color does nothing here,
   the white background-clip gradient just shines straight through it. */
.intro-name.is-spent .qlight{
  /* GEEN text-shadow:none meer: die valt buiten de (bewust versmalde)
     transition en zou in één frame wegknippen; opacity .07 dimt de glow
     toch al mee, want element-opacity geldt óók voor de text-shadow */
  opacity:.07;
  animation:none;
}
/* #2 anticipation — the word flares once (JS adds .light-charge ~0.2s before the
   spark launches) so your eye is already on it when the light detaches */
@keyframes curioCharge {
  0%   { text-shadow:0 10px 55px rgba(0,0,0,.46), 0 0 22px rgba(240,238,233,.55), 0 0 64px rgba(226,223,216,.34); }
  55%  { text-shadow:0 0 12px rgba(255,255,255,.98), 0 0 42px rgba(255,255,255,.92), 0 0 100px rgba(244,242,238,.72); }
  100% { text-shadow:0 10px 55px rgba(0,0,0,.46), 0 0 22px rgba(240,238,233,.55), 0 0 64px rgba(226,223,216,.34); }
}
.intro-name.light-charge .qlight{ animation:curioCharge .24s ease-out; }
@media (prefers-reduced-motion:reduce) and (min-width:99999px){ .intro-name-track span, .intro-name-track .qlight, .intro-name.light-charge .qlight{ animation:none; } }
/* CENTRE SPOTLIGHT — a bright, glowing copy of the marquee, revealed only
   through a soft radial mask fixed at the middle of the screen. As the line
   scrolls, each word lights up warmly as it crosses the centre, then fades —
   the whole sentence is "read" on a loop. Cinematic, soft, never harsh. */
.intro-name-spot{
  position:absolute; inset:0; overflow:hidden; pointer-events:none;
  -webkit-mask-image:radial-gradient(40% 150% at 50% 50%, #000 0%, rgba(0,0,0,.92) 20%, rgba(0,0,0,.45) 42%, transparent 64%);
          mask-image:radial-gradient(40% 150% at 50% 50%, #000 0%, rgba(0,0,0,.92) 20%, rgba(0,0,0,.45) 42%, transparent 64%);
}
.intro-name-glow span{
  color:#ffe8c2;
  text-shadow:
    0 0 22px rgba(255,206,150,.55),
    0 0 60px rgba(255,182,112,.34),
    0 0 130px rgba(214,150,82,.22);
}
/* 4 identical copies → -50% shift lands on an identical frame → seamless.
   0 → -50% makes the name travel right→left (enters from the right). */
@keyframes introMarquee{ from{ transform:translate3d(0,0,0); } to{ transform:translate3d(-50%,0,0); } }

/* tablet / mobile */
@media (max-width:960px){
  .intro{ padding-top:16vh; }
  .intro-stage{
    grid-template-columns:1fr; justify-items:center; text-align:center;
    gap:2.4rem; padding-bottom:16vh;
  }
  .intro-lead{ order:2; max-width:44ch; }
  .intro-photo{ order:1; width:min(86vw,520px); }
  .intro-more{ order:3; }
  .intro-social{ justify-content:center; }
  .intro-name-track span{ font-size:clamp(2rem,12vw,5rem); }
}
@media (max-width:600px){
  .intro-pill{ font-size:.52rem; padding:.45rem .8rem; }
  .intro-idx{ font-size:1.15rem; }
  .intro-copy{ font-size:1rem; }
}
@media (prefers-reduced-motion:reduce) and (min-width:99999px){
  .intro-name-track{ animation:none; transform:none; }
  .intro-more::after{ animation:none; }
}

/* ============================================================
   PER-PAGE HEADER BAR (ref: Chris K) — a thin hairline at the top of
   every full-screen page, with a small row underneath telling you which
   page you're on: index left · page name centre · count right.
   ============================================================ */
.intro, .work, .more, .journey, .about, .finale{ position:relative; }
.page-head{
  position:absolute; left:0; right:0; top:clamp(4.5rem,7.5vh,6.5rem); z-index:6;
  padding:0 4.4vw; pointer-events:none;
}
.page-head-rule{ height:3px; width:100%; background:rgba(255,255,255,.55); }
.page-head-row{
  position:relative; margin-top:1.25rem; display:flex; align-items:baseline; justify-content:space-between;
  gap:1.5rem;
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:1.5rem; letter-spacing:.14em; text-transform:uppercase;
  color:rgba(255,255,255,.85);
}
.page-head-row .ph-idx{ flex:0 0 auto; }
.page-head-row .ph-total{ flex:0 0 auto; }
/* name + links are spread by space-between, never overlapping */
/* all section names sit at the same left-of-centre spot (where "About" used to
   sit before its social links were removed) — not centred */
.page-head-row .ph-name{ position:absolute; left:29%; transform:translateX(-50%); color:#fff; white-space:nowrap; }
.page-head-row .ph-links{ white-space:nowrap; }
/* on the light About panel the rule + text need to flip to dark */
.about .page-head-rule{ background:rgba(20,16,12,.16); }
.about .page-head-row{ color:rgba(20,16,12,.55); }
.about .page-head-row .ph-name{ color:rgba(20,16,12,.9); }
@media (max-width:760px){
  .page-head-row{ gap:.6rem; font-size:.8rem; }
  .page-head-row .ph-links{ display:none; }
}

/* ============================================================
   QUOTE PAGE — restored below the About section
   The "I follow the light of curiosity" quote moved out of the
   hero; the lamp section (Positioning) now reveals it after About.
   ============================================================ */
/* compact lamp beat: the lamp + its glow, then Selected Work begins right
   beneath it (no big empty gap) — the lamp lights the start of the work */
.positioning{ min-height:62vh; padding-bottom:0; }
.pos-quote{ margin-top:auto; margin-bottom:1.2rem; }
/* light words on the dark room (base colour was --ink, invisible on black) */
.pos-quote span{ color:rgba(247,243,236,.94); }
.pos-quote .lit-word{ color:#ffe7bd;
  text-shadow:0 0 18px rgba(255,206,140,.50), 0 0 42px rgba(255,188,110,.30); }
/* "see where it leads" cue removed here — Selected Work now begins right under
   the lamp, so the cue is redundant */
.pos-cue{ display:none !important; }

/* override the later `.showreel-cue{ display:flex!important }` rule: the cue must
   NOT live in the hero anymore — it belongs to the lamp page below About. */
.showreel .showreel-cue{ display:none !important; }

/* ============================================================
   ABOUT + JOURNEY page — opens from the intro "Learn more" button.
   A full-screen overlay with its own scroll, holding the About +
   Journey sections that used to live on the landing scroll.
   ============================================================ */
.about-overlay{
  position:fixed; inset:0; z-index:9000;
  overflow-y:auto; -webkit-overflow-scrolling:touch;
  background:
    radial-gradient(120% 80% at 82% -8%, rgba(214,160,96,.10), transparent 55%),
    #060504;
}
.about-overlay[hidden]{ display:none; }
.about-overlay.opening{ animation:aboutFade .5s ease both; }
@keyframes aboutFade{ from{ opacity:0 } to{ opacity:1 } }

.about-back{
  position:fixed; top:3vh; left:4.4vw; z-index:2;
  display:inline-flex; align-items:center; gap:.55rem;
  font-family:var(--v4-mono); font-size:.76rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--dk-fg); background:rgba(247,243,236,.04); cursor:pointer;
  border:1px solid rgba(247,243,236,.2); border-radius:999px; padding:.6rem 1.15rem;
  transition:background .3s ease, border-color .3s ease, transform .3s ease;
}
.about-back:hover{ background:rgba(214,160,96,.12); border-color:rgba(214,160,96,.6); transform:translateX(-2px); }
.about-back .ab-arrow{ font-size:1.1em; }

/* inside the overlay the sections are static + always visible (no scroll reveals) */
.about-overlay .journey,
.about-overlay .about{ min-height:auto; opacity:1; }
.about-overlay .milestone,
.about-overlay .journey-left > *,
.about-overlay .about-photo,
.about-overlay .about-text > *{ opacity:1 !important; transform:none !important; }
.about-overlay .journey-line-fill{ transform:scaleY(1); }
.about-overlay .journey{ padding-top:15vh; }
.about-overlay .about{ padding-top:8vh; }

body.about-open{ overflow:hidden; }

/* ============================================================
   QUOTE → LAMP → WORK : one continuous warm beat (no black gap).
   "Follow the light into the lamp." The lamp now hangs in the SAME
   warm shader as the About page (section made transparent), so there
   is no black wall between the quote and the lamp. The cord drops
   from the top edge — reading as a continuation of the "I follow the
   light of curiosity" quote just above. The bottom softly settles to
   black to hand off into the (dark) Selected Work gallery.
   ============================================================ */
.positioning{ background-color:transparent !important; position:relative; }
/* (the black fade bar was removed — Selected Work begins right under the lamp) */
/* cord begins right at the top edge → reads as dropping from the quote above */
.positioning .lamp{ margin-top:0; }

/* NO LAMP FIXTURE: hide the physical lamp (cord/ferrule/shade/bulb glass) so the
   warm reveal now reads as light coming from the "light of curiosity" quote
   itself rather than a hanging bulb. The soft glow is kept as the light source,
   and the bloom/reveal that lights Selected Work stays fully intact. */
.lamp-cord, .lamp-ferrule, .lamp-shade, .bulb-svg, .lamp-glow{ display:none !important; }
.lamp{ pointer-events:none; }
.lamp-bulb{ width:2px; height:2px; }            /* keep a tiny origin point for the spark hand-off */
/* kill the leftover amber reveal-overlay (it flashed a warm wash in the now-empty
   transition section — the "light bug"). The section is plain dark now. */
.pos-dark{ display:none !important; }

/* ============================================================
   Selected Work: drop the hard black block. The section becomes
   transparent so the warm shader continues out of the lamp into the
   work — the lamp's light "leads into" the gallery (theme/cards tuned later).
   ============================================================ */
.work{ background-color:transparent !important; }

/* ============================================================
   VIEWPORT FRAME — cinematic viewfinder. A faint hairline inset from
   the edges with small "+" ticks at the corners. Decorative + fixed;
   sits below the wordmark/nav (z 8000+) and below the overlays (z 9000).
   Easy to remove: delete this block + the .viewport-frame markup.
   ============================================================ */
.viewport-frame{ display:none !important; }
.viewport-frame--off{
  position:fixed; inset:18px; z-index:7000; pointer-events:none;
  border:1px solid rgba(247,243,236,.085); border-radius:3px;
}
.vf-corner{ position:absolute; width:13px; height:13px; }
.vf-corner::before,.vf-corner::after{ content:""; position:absolute; background:rgba(247,243,236,.3); }
.vf-corner::before{ left:50%; top:0; width:1px; height:100%; transform:translateX(-50%); }
.vf-corner::after{ top:50%; left:0; height:1px; width:100%; transform:translateY(-50%); }
.vf-tl{ left:-6.5px; top:-6.5px; }
.vf-tr{ right:-6.5px; top:-6.5px; }
.vf-bl{ left:-6.5px; bottom:-6.5px; }
.vf-br{ right:-6.5px; bottom:-6.5px; }
@media (max-width:600px){ .viewport-frame{ inset:10px; } }

/* ============================================================
   INTERACTIVE SKILLS FIELD — draggable physics chips (skills.js)
   A bounded play area: chips drift, repel from the cursor, can be
   grabbed and thrown with momentum, and softly collide. Black & white.
   ============================================================ */
.skills{ position:relative; z-index:2; padding:14vh 4.4vw 8vh; }
.skills-head{ text-align:center; margin-bottom:3vh; pointer-events:none; }
.skills-kicker{
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:.74rem; letter-spacing:.22em;
  text-transform:uppercase; color:rgba(247,243,236,.45); margin-bottom:1rem;
}
.skills-title{
  font-family:'Telegraf','Archivo',sans-serif; font-weight:400; text-transform:uppercase;
  font-size:clamp(2rem,5vw,4.6rem); line-height:.95; letter-spacing:-.02em; color:#fff;
}
.skills-hint{
  margin-top:1.4rem; font-family:var(--v4-mono),'Space Mono',monospace; font-size:.72rem;
  letter-spacing:.14em; text-transform:uppercase; color:rgba(247,243,236,.4);
}
.skills-hint span{ color:rgba(247,243,236,.78); }
.skills-field{
  position:relative; width:100%; height:clamp(420px,62vh,720px);
  margin:0 auto; touch-action:none; overflow:hidden;
  border:1px solid rgba(247,243,236,.08); border-radius:18px;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(247,243,236,.035), transparent 60%),
    rgba(247,243,236,.012);
  -webkit-mask-image:radial-gradient(130% 120% at 50% 45%, #000 70%, transparent 100%);
          mask-image:radial-gradient(130% 120% at 50% 45%, #000 70%, transparent 100%);
}
.skill-chip{
  position:absolute; top:0; left:0; will-change:transform;
  display:inline-flex; align-items:center; white-space:nowrap;
  font-family:var(--v4-mono),'Space Mono',monospace; font-weight:500;
  font-size:calc(.74rem * var(--cw,1)); letter-spacing:.08em; text-transform:uppercase;
  color:rgba(247,243,236,.9); background:rgba(20,18,16,.55); backdrop-filter:blur(6px);
  border:1px solid rgba(247,243,236,.22); border-radius:100px;
  padding:calc(.6rem * var(--cw,1)) calc(1.15rem * var(--cw,1));
  cursor:grab; user-select:none; -webkit-user-select:none;
  transition:background .25s ease, border-color .25s ease, color .25s ease, box-shadow .3s ease;
}
.skill-chip:hover{
  background:rgba(247,243,236,.92); color:#111; border-color:#fff;
  box-shadow:0 0 0 1px rgba(255,255,255,.4), 0 12px 40px rgba(255,255,255,.14);
}
.skill-chip.is-grabbing{ cursor:grabbing; background:#fff; color:#111; border-color:#fff;
  box-shadow:0 0 0 1px #fff, 0 18px 60px rgba(255,255,255,.22); z-index:5; }
.skill-chip.is-poked{ border-color:rgba(247,243,236,.55); }
.skill-chip.is-poked::after{
  content:""; position:absolute; inset:-1px; border-radius:inherit;
  box-shadow:0 0 0 1px rgba(255,255,255,.25); opacity:0; animation:chipPulse .5s ease;
}
@keyframes chipPulse{ from{ opacity:1; transform:scale(.9); } to{ opacity:0; transform:scale(1.25); } }
.skills-counter{
  text-align:center; margin-top:2.4vh; font-family:var(--v4-mono),'Space Mono',monospace;
  font-size:.72rem; letter-spacing:.16em; text-transform:uppercase; color:rgba(247,243,236,.4);
}
.skills-counter #skills-poked{ color:#fff; }
@media (prefers-reduced-motion:reduce) and (min-width:99999px){
  .skills-field{ height:auto; padding:2rem; display:flex; flex-wrap:wrap; gap:.8rem; justify-content:center; mask-image:none; -webkit-mask-image:none; }
  .skill-chip{ position:static !important; transform:none !important; }
}

/* ============================================================
   THRESHOLD — "From here, you carry the light."
   The old empty lamp section becomes a short, readable beat that
   teaches the cursor-light interaction. Always legible; the glow is
   a reward, never a gate. Reduced-motion + touch: fully static.
   ============================================================ */
.positioning{ justify-content:center !important; min-height:72vh !important; }
.pos-threshold{
  position:relative; z-index:3; margin:auto; text-align:center;
  display:flex; flex-direction:column; align-items:center; gap:3.2vh;
  padding:6vh 0;
}
.pt-line{
  font-family:'Telegraf','Archivo',sans-serif; font-weight:400; text-transform:uppercase;
  font-size:clamp(1.5rem,3.4vw,3.2rem); line-height:1.08; letter-spacing:-.01em;
  color:#f7f3ec;
  /* a soft inner light on the word, so the line itself glows faintly */
  text-shadow:0 0 26px rgba(247,243,236,.18), 0 2px 30px rgba(0,0,0,.5);
}
.pt-cue{
  display:inline-flex; flex-direction:column; align-items:center; gap:1.1rem;
  text-decoration:none;
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:.74rem; letter-spacing:.2em;
  text-transform:uppercase; color:rgba(247,243,236,.55);
  transition:color .3s ease;
}
.pt-cue:hover{ color:#f7f3ec; }
.pt-cue-arrow{
  width:1px; height:6vh; background:linear-gradient(to bottom, rgba(247,243,236,.5), transparent);
  position:relative; overflow:hidden;
}
/* a bright dot falls down the line on a loop — a living "scroll down" cue */
.pt-cue-arrow::after{
  content:""; position:absolute; left:50%; top:0; transform:translateX(-50%);
  width:3px; height:3px; border-radius:50%; background:#f7f3ec;
  box-shadow:0 0 8px 2px rgba(247,243,236,.6);
  animation:ptFall 2s cubic-bezier(.5,0,.5,1) infinite;
}
@keyframes ptFall{ 0%{ top:-10%; opacity:0; } 20%{ opacity:1; } 80%{ opacity:1; } 100%{ top:110%; opacity:0; } }
@media (prefers-reduced-motion:reduce) and (min-width:99999px){ .pt-cue-arrow::after{ animation:none; top:40%; } }

/* ============================================================
   OPENING PRELOADER (intro-v4.js) — warm-white screen, centred name +
   a count-up percentage; the name disperses, then a dark curtain rises
   with a curved top to reveal the hero, whose headline scrambles in.
   ============================================================ */
/* the opening sheet was #f4f0e7 — a warm off-white, half a step yellower than
   the marquee headline it hands over to. Side by side that read as two
   different whites; both are pure #fff now, so the loader and the type it
   reveals are the same light. */
.pl{
  position:fixed; inset:0; z-index:9500; background:#fff;
  display:grid; place-items:center; will-change:transform;
}
/* a curved white lip hanging just ABOVE the fold: as the sheet slides DOWN and
   out, this becomes the leading top edge, sweeping down to reveal the hero from
   the top (reference-style) with a soft convex arc instead of a flat line. */
.pl::before{
  content:''; position:absolute; left:0; right:0; bottom:100%; height:16vh;
  background:#fff; border-top-left-radius:50% 14vh; border-top-right-radius:50% 14vh;
}
.pl-name{
  font-family:'Telegraf','Archivo',sans-serif; font-weight:400; color:#0e0b08;
  font-size:clamp(2.7rem,11.5vw,11rem); letter-spacing:-.024em; white-space:nowrap; line-height:1;
  font-kerning:none;            /* split-spans match native → no reflow on split */
  visibility:hidden;            /* shown by intro-v4.js once the real font is ready (kills the swap "jump") */
}
.pl-name span{ display:inline-block; will-change:transform,opacity; }
/* the full stop is the one brand-gold moment in the loader — same gold as the
   close-X / Get-in-touch pill (#e6bb52) */
.pl-name span:last-child{ color:#e6bb52; }
.pl-count{
  position:absolute; right:5vw; bottom:5.2vh;
  font-family:'Telegraf','Archivo',sans-serif; font-weight:400; color:#0e0b08;
  font-size:clamp(2.4rem,6.4vw,5.6rem); letter-spacing:.01em; line-height:1;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
  visibility:hidden;            /* shown by intro-v4.js once the real font is ready */
}
/* dark curtain that rises over the white with a gentle convex-up top edge.
   Double-height + a wide top border-radius gives a persistent arc as it sweeps
   up (the arc is the leading edge throughout the rise, not just a flat box top). */
.pl-curtain{
  position:fixed; left:0; bottom:0; width:100%; height:200vh; z-index:9600;
  background:#0a0705; pointer-events:none; will-change:transform,opacity;
  border-top-left-radius:50% 11vh; border-top-right-radius:50% 11vh;
  transform:translateY(100%);
}
/* per-letter spans of the hero headline (split at runtime for the scramble-in) */
.hero2-h1 .h2c{ display:inline-block; will-change:transform,opacity; }
html.reduced .pl, html.reduced .pl-curtain{ display:none !important; }

/* ---------- PAGE→PAGE TRANSITION CURTAIN (see page-transition.js) ----------
   One tall black panel, taller than the viewport, that travels DOWNWARD for the
   whole transition — one continuous gesture:
     • LEAVE  (.pt-cover)  descends from above; its curved BOTTOM edge leads down
                           over the page to cover it → then navigate.
     • ENTER  (.pt-reveal) starts covering, then continues down and out; its
                           curved TOP edge reveals the new page from the top.
   The curve depth lives in --pt-arc so gsap can flex it subtly mid-sweep (the
   arc breathes instead of being a rigid shape).
   `html.pt-entering` (set by an inline <head> script before first paint) shows
   it already covering, so an incoming page never flashes before the reveal. */
.pt-curtain{
  --pt-arc:12vh;
  position:fixed; left:0; top:0; width:100%; height:115vh; z-index:9700;
  background:#0a0908; pointer-events:none; visibility:hidden;
  transform:translateY(-128vh); will-change:transform; backface-visibility:hidden;
}
/* LEAVE — the panel descends from above to cover; its curved BOTTOM edge leads.
   Pure CSS transform → runs on the compositor thread, so the wipe stays smooth
   even while the page is busy. Cubic ease-in-out (the reference feel). */
/* was .48s cubic-bezier(.55,.06,.28,1) — that curve sits still for its first
   third and then covers 128vh in the remainder, so the panel arrived as a
   slam. A gentler in-out over a slightly longer beat reads as one deliberate
   sweep instead, and gives the phone a few more frames to spend per pixel. */
.pt-curtain.pt-cover{
  border-bottom-left-radius:50% var(--pt-arc); border-bottom-right-radius:50% var(--pt-arc);
  animation:ptCover .55s cubic-bezier(.42,0,.22,1) forwards;
}
@keyframes ptCover{ from{ transform:translateY(-128vh); } to{ transform:translateY(0); } }
/* LEAVE, but for a real NAVIGATION (page-transition.js go()) rather than the
   in-page wipe. Same panel, same keyframes, longer and softer — because the
   two are not the same thing to watch.
   Measured: the panel travels 128vh but only 100 of those are on screen, and
   with the wipe's ease-in it spends its first fifth still above the fold. Net
   visible sweep = 0.237s. That is the whole "screen just goes black" problem:
   a quarter of a second, and unlike the reveal (which uncovers a bright page,
   so its edge is obvious) this one is near-black falling over an already dark
   site — the least legible half of the gesture given the least time.
   .85s with less hold at the top puts 0.394s of it on screen, and the extra
   beat is not dead time either: go() spends it fetching the destination, so
   the pause between the two halves shrinks by roughly what the fall gains.
   The in-page wipe deliberately keeps the original .55s — closing a case with
   the X is the version that already reads right, so it is left alone. */
.pt-curtain.pt-cover-slow{
  border-bottom-left-radius:50% var(--pt-arc); border-bottom-right-radius:50% var(--pt-arc);
  animation:ptCover .85s cubic-bezier(.3,0,.22,1) forwards;
}
/* ENTER — the panel continues down and out; its curved TOP edge reveals the page.
   Cubic ease-out → decelerates gently as the page settles into view. */
/* was .62s cubic-bezier(.18,.6,.22,1) — a touch longer with a softer landing,
   so the page settles in under the panel rather than being uncovered at speed */
.pt-curtain.pt-reveal{
  border-top-left-radius:50% var(--pt-arc); border-top-right-radius:50% var(--pt-arc);
  animation:ptReveal .72s cubic-bezier(.16,.72,.22,1) forwards;
}
@keyframes ptReveal{ from{ transform:translateY(-13vh); } to{ transform:translateY(115vh); } }
/* arriving page: paint the covering state instantly (curved top just off the fold
   → full black), then the CSS reveal slides it out. */
html.pt-entering .pt-curtain{
  visibility:visible; transform:translateY(-13vh);
  border-top-left-radius:50% var(--pt-arc); border-top-right-radius:50% var(--pt-arc);
}
html.reduced .pt-curtain{ display:none !important; }
/* the cursor-carried light sits above the curtain (z-index 9994/9996, above the
   curtain's 9700) so it stays visible over the wipe — flickering at the top as it
   tracks the cursor near a clicked nav link. Hide it for the whole transition. */
html.pt-leaving .curio, html.pt-leaving .curio-spark, html.pt-leaving .curio-trail, html.pt-leaving .curio-bloom, html.pt-leaving .curio-mobile,
html.pt-entering .curio, html.pt-entering .curio-spark, html.pt-entering .curio-trail, html.pt-entering .curio-bloom, html.pt-entering .curio-mobile{ opacity:0 !important; visibility:hidden !important; }

/* ============ ABOUT EMBED — the about page, preloaded like a case ============
   The REAL about.html boots invisibly in this full-viewport iframe while the
   visitor is still on the landing page — the same idea as the case overlays:
   the destination already lives here, so "navigating" to it loads nothing.
   z-index sits below the match-cut stage (9600) and the curtain (9700), so
   both transitions can play on top of it; above everything else. */
.about-embed{
  position:fixed; inset:0; width:100%; height:100%; border:0; z-index:9550;
  visibility:hidden; pointer-events:none; background:#0a0908;
}
/* WARM (hover on Learn more): composited at invisible opacity so the page's
   layers raster + upload BEFORE the flight — the reveal is then free */
html.about-embed-warm .about-embed{ visibility:visible; opacity:.0001; }
html.about-embed-open .about-embed{ visibility:visible; opacity:1; pointer-events:auto; }
/* the index page's cursor-carried light (z 9994) would hang frozen above the
   overlay (the iframe swallows the mousemoves) — hide it while open */
html.about-embed-open .curio, html.about-embed-open .curio-spark, html.about-embed-open .curio-trail, html.about-embed-open .curio-bloom, html.about-embed-open .curio-mobile{
  opacity:0 !important; visibility:hidden !important;
}
/* same cursor-light layer (z 9994-9996) sits ABOVE the bio popup (z 9500) —
   without a real mousemove since open it hangs stuck near its last/default
   spot, reading as a stray glowing ball over the panel. Hide it while open. */
body.bio-open .curio, body.bio-open .curio-spark, body.bio-open .curio-trail, body.bio-open .curio-bloom, body.bio-open .curio-mobile{
  opacity:0 !important; visibility:hidden !important;
}
/* the mobile light tracks window scroll, which is locked while a case is
   open (the case has its own internal scroller) — without this it would
   just hang frozen over the case content, same stray-glow problem as above.
   (.curio is the light on touch now; on a cursor device it keeps following
   the mouse over the case, which is the point, so this stays touch-only) */
@media (hover:none){
  body.case-open .curio, body.case-open .curio-spark,
  body.case-open .curio-trail, body.case-open .curio-bloom{
    opacity:0 !important; visibility:hidden !important;
  }
}

/* ============ MATCH-CUT — Learn more → about (see match-cut.js) ============
   A fixed clone of the intro portrait grows to the about hero's geometry while
   the page behind dims to the curtain black. The grade racks focus mid-flight:
   the blurred studio backdrop sharpens into the real location, the key light
   and bottom scrim fade, and the vignette crossfades to the about hero's. */
/* The whole flight is compositor-thread only (the same lesson as the curtain:
   see page-transition.js). NOTHING animates layout — the rounded card is a
   clip-path inset() window over a fixed full-viewport layer, and every layer
   inside is painted once at a fixed size and flown by a transform transition.
   All geometry shares ONE duration + easing so the ensemble stays in lockstep;
   only opacities (the light script below) run on their own clock. */
.mc-stage{
  position:fixed; inset:0; z-index:9600; pointer-events:none;
  --mc-dur:.95s; --mc-ease:cubic-bezier(.66,0,.34,1);   /* a touch longer = calmer glide, same character */
}
/* PRE-WARMED (hover): the fully-built stage composited at invisible opacity —
   Chrome rasters the blurred walls + uploads every texture NOW, so the click
   only flips it live (see match-cut.js prewarm) */
.mc-stage.mc-warm{ opacity:.0001; }
.mc-dim{ position:absolute; inset:0; background:#0a0908; opacity:0; transition:opacity .5s ease; }
.mc-run .mc-dim{ opacity:1; }
/* MID-FLIGHT HANDOFF (match-cut.js): once the dim is fully opaque the live
   about embed is revealed beneath the stage and the dim fades back out — the
   background under the flying card IS the destination page from here on, its
   texts/chrome fading in while the card lands. */
.mc-stage.mc-handoff .mc-dim{ opacity:0; transition:opacity .3s ease; }
/* the card's shadow + dark fill, as its own transformed layer under the clip.
   PAGE-black, not #141414: the clip rides 1px inside this plate, and that 1px
   ring of plate against the #0a0908 page read as a second faint grey line
   along the frame edge (with the AA leak above it: "two thin lines"). The
   walls fully cover the plate inside the window, so only the ring shows. */
.mc-shadow{
  position:absolute; left:0; top:0; transform-origin:0 0;
  border-radius:18px; background:#0a0908;
  box-shadow:0 40px 90px rgba(0,0,0,.55);
  will-change:transform; transition:transform var(--mc-dur) var(--mc-ease);
}
/* the rounded window: inset() components are linear in progress, so a single
   clip-path transition tracks the exact card path; radius stays 18px */
.mc-clip{
  position:absolute; inset:0;
  will-change:clip-path; transition:clip-path var(--mc-dur) var(--mc-ease);
}
.mc-clip > *{
  position:absolute; left:0; top:0; transform-origin:0 0; pointer-events:none;
  will-change:transform,opacity;
  transition:transform var(--mc-dur) var(--mc-ease), opacity .65s ease;
}
/* the flown layers are home-sized by JS — the global `img{max-width:100%}`
   must not clamp them to the viewport (that shrinks + distorts the wall) */
.mc-clip > img{ max-width:none; max-height:none; }
/* THE CONTINUOUS WALL + LOOK-CROSSFADE — the wall never leaves: the two
   blur-matched copies of the SAME full backdrop fly in perfect registration
   with the person (see match-cut.js), the top (start-blur) copy dissolving
   into the bottom (end-blur) copy over the whole flight. Meanwhile the
   portrait's dressing (key light, vignette, bottom scrim) fades out and the
   about-hero's dressing (light2, vig2) fades in, so the clone lands exactly
   on the real page's look. Selectors ride .mc-clip for specificity over the
   `.mc-clip > *` base rule. */
.mc-clip > .mc-bgc-start{
  transition-duration:var(--mc-dur), var(--mc-dur);
}
.mc-run .mc-bgc-start{ opacity:0; }
.mc-clip > .mc-vig, .mc-clip > .mc-scrim{
  transition-duration:var(--mc-dur), .55s;
}
.mc-run .mc-vig, .mc-run .mc-scrim{ opacity:0; }
.mc-clip > .mc-light, .mc-clip > .mc-light2{
  transition-duration:var(--mc-dur), .75s;
}
/* vig2 fades IN faster than vig fades OUT (.5s in vs .55s out), so the
   about-hero's edge darkening is at full strength before the start look's
   has gone — the frame edge never brightens mid-flight */
.mc-clip > .mc-vig2{
  transition-duration:var(--mc-dur), .5s;
}
.mc-run .mc-light{ opacity:0; }
.mc-run .mc-light2, .mc-run .mc-vig2{ opacity:1; }
/* the cast shadows crossfade on the light pools' clock */
.mc-clip > .mc-cast, .mc-clip > .mc-cast2{
  transition-duration:var(--mc-dur), .75s;
}
.mc-run .mc-cast{ opacity:0; }
.mc-run .mc-cast2{ opacity:1; }
.mc-cast{
  z-index:1;
  background:radial-gradient(ellipse 34% 62% at 57% 64%,
    rgba(0,0,0,.34) 0%, rgba(0,0,0,.15) 46%, transparent 72%);
}
.mc-cast2{
  z-index:1; opacity:0;
  /* EXACT copy of .abh-cast — the flight must land pixel-equal on the real
     hero, or the shadow visibly jumps at the stage drop */
  background:radial-gradient(ellipse 22% 52% at 34% 70%,
    rgba(0,0,0,.26) 0%, rgba(0,0,0,.12) 44%,
    rgba(0,0,0,.04) 66%, transparent 82%);
}
/* the person: fixed at his about-hero bitmap size, flown uniformly; his grade
   (brightness + drop-shadow) is baked into the png itself — both ends of the
   flight wear the identical bitmap, so the clone carries no filter at all */
.mc-fg{
  z-index:2;
  transition:transform var(--mc-dur) var(--mc-ease), opacity .65s ease;
}
/* overlays: the landing look (light/vig/scrim, sized to the portrait) fades
   out mid-flight while the about-hero look (light2/vig2) fades in */
.mc-light{
  z-index:1;
  background:radial-gradient(ellipse 44% 72% at 45% 40%,
    rgba(255,255,255,.36) 0%, rgba(255,255,255,.12) 48%, transparent 75%);
}
.mc-light2{
  z-index:1; opacity:0;
  /* EXACT copy of .abh-light — same reason as .mc-cast2 */
  background:radial-gradient(ellipse 34% 72% at 28% 40%,
    rgba(255,255,255,.34) 0%, rgba(255,255,255,.18) 36%,
    rgba(255,255,255,.07) 60%, transparent 82%);
}
.mc-vig{
  z-index:1;
  background:
    var(--film-grain),
    linear-gradient(135deg, transparent 50%, rgba(0,0,0,.10) 80%, rgba(0,0,0,.18) 100%),
    linear-gradient(to bottom, rgba(0,0,0,.22) 0%, rgba(0,0,0,.08) 9%, transparent 20%),
    radial-gradient(ellipse 70% 88% at 50% 46%,
      transparent 22%, rgba(0,0,0,.04) 38%, rgba(0,0,0,.10) 52%, rgba(0,0,0,.19) 65%,
      rgba(0,0,0,.30) 78%, rgba(0,0,0,.42) 89%, rgba(0,0,0,.55) 100%);
  background-size:160px 160px, 100% 100%, 100% 100%, 100% 100%;
}
/* the about-hero's edge vignette (= .abh-frame::after), fading in en route. */
.mc-vig2{
  z-index:3; opacity:0; border-radius:18px;
  background:
    var(--film-grain),
    linear-gradient(135deg, transparent 52%, rgba(0,0,0,.08) 82%, rgba(0,0,0,.14) 100%),
    linear-gradient(to bottom, rgba(0,0,0,.26) 0%, rgba(0,0,0,.10) 9%, transparent 20%),
    radial-gradient(82% 100% at 38% 46%,
      transparent 22%, rgba(0,0,0,.05) 38%, rgba(0,0,0,.12) 52%, rgba(0,0,0,.22) 65%,
      rgba(0,0,0,.35) 78%, rgba(0,0,0,.50) 89%, rgba(0,0,0,.65) 100%);
  background-size:160px 160px, 100% 100%, 100% 100%, 100% 100%;
}
/* THE EDGE GUARD — a dark hairline riding the window edge, now INSIDE the
   clip as its topmost layer (see match-cut.js buildStage). It used to be an
   unclipped sibling above .mc-clip: that sealed the per-layer AA leak (the
   thin white wall line at the bottom edge), but it flew on the compositor
   while the clip-path transition can lag a frame behind on the main thread —
   on an off-centre start (longer vertical travel, higher edge speed) the
   hairline visibly detached from the card's top/bottom edge mid-flight.
   Clipped by the window itself it can never detach, and the OUTSET spread
   overhangs the window edge by 12px: the clip always cuts through solid
   dark ring pixels (never between two layers' AA edges), so the white-line
   leak stays sealed and up to ~2 dropped frames of clip lag are covered.
   The overhang is outside the window, so the landed look is unchanged —
   the inner 2.5px still reads as the frame vignette's own edge-darkening
   and vanishes with the stage at the drop. */
.mc-edge{
  position:absolute; left:0; top:0; transform-origin:0 0; pointer-events:none;
  border-radius:18px; z-index:3;
  /* OPAQUE: the leak shows through exactly (1 - alpha) of this ring — at .85
     a faint 15% of the bright wall still measured along the bottom edge */
  box-shadow:inset 0 0 0 2.5px rgb(8,7,6), 0 0 0 12px rgb(8,7,6);
  will-change:transform; transition:transform var(--mc-dur) var(--mc-ease);
}
/* the landing's bottom scrim, expressed on a full-size overlay: the gradient
   occupies the bottom 46% (stops = the original 0/38/70/100% of that band) */
.mc-scrim{
  z-index:2;
  background:linear-gradient(to top,
    rgba(15,13,11,.82) 0%, rgba(15,13,11,.5) 17.5%, rgba(15,13,11,.18) 32.2%, transparent 46%);
}
/* arriving on about via the match-cut: the clone landed pixel-equal to the
   real hero frame (wall, light, vignette and all), so the frame paints in
   place at first paint — only the name/tagline and the chrome still enter.
   The entrance is a CSS transition on the compositor (about.js adds .mc-go
   once the fonts are ready), NOT a JS tween: the landing page is still
   booting (shader, images) and a main-thread tween stutters through that —
   that was the name's "jump". `translate` composes with each element's own
   `transform`, so the base transforms stay untouched; releasing both classes
   afterwards lands on these exact base values (no snap). */
/* ONE gesture, not a load sequence: name, tagline and chrome move together
   (no delays, one shared duration) over a short distance — the page reads as
   "settling into place", never as parts loading in one after another.
   In the EMBED route the page is revealed mid-flight (mc-arm → mc-go →
   mc-land, see about.html): these fade in VISIBLY under the flying card
   while it lands — hand in hand, never queued. `.mc-hold` withholds the
   hero frame itself until the clone has landed exactly onto it. */
/* the frame is WITHHELD with opacity, NOT visibility: with will-change the
   layer is painted + rastered while still unseen (the 28px-blurred wall is
   expensive to raster), so committing it at the landing is a pure compositor
   flip — no decode/blur-raster hitch right under the landing. will-change on
   the texts pre-promotes their layers the same way (GEERT is a huge glyph
   raster); it lifts when mc-entering is released. */
html.mc-entering.mc-hold .abh-frame{ opacity:0; }
html.mc-entering .abh-frame{ will-change:opacity; }
html.mc-entering .abh-name, html.mc-entering .abh-tagline{ will-change:opacity, translate; }
html.mc-entering .abh-name{ opacity:0; translate:0 .7rem; }
html.mc-entering .abh-tagline{ opacity:0; translate:0 .9rem; }
/* tagline + chrome ride mc-go (mid-flight, no overlap with the card); the
   NAME overlaps the frame — fading it earlier would hide half the word
   behind the flying card and pop it in at the drop. It starts the instant
   the card lands (mc-hold released). Kept SHORT and light (short duration,
   small offset) so it lands close on the heels of the tagline/chrome's .6s
   settle instead of trailing on its own afterwards — that lag was reading
   as a hard cut, GEERT arriving separately from the rest. */
html.mc-entering.mc-go .abh-tagline{
  opacity:1; translate:0 0;
  transition:opacity .6s ease-out, translate .6s cubic-bezier(.22,1,.36,1);
}
html.mc-entering.mc-go:not(.mc-hold) .abh-name{
  opacity:1; translate:0 0;
  transition:opacity .4s ease-out, translate .4s cubic-bezier(.22,1,.36,1);
}
html.mc-entering .v4-logo, html.mc-entering .pill-nav,
html.mc-entering .hero2-pill, html.mc-entering .lang-switch{ opacity:0; }
html.mc-entering.mc-go .v4-logo, html.mc-entering.mc-go .pill-nav,
html.mc-entering.mc-go .hero2-pill, html.mc-entering.mc-go .lang-switch{
  opacity:1; transition:opacity .6s ease-out;
}

/* ============================================================
   PROPORTIONAL SCALING — design reference: 1800×1130 (laptop).
   Goal: layout(W,H) = layout(1800,1130) × min(W/1800, H/1130).

   How: root font-size = min(100vw/160, 100vh/100.44).
   At 1800×1130 this equals 11.25px — identical to before, so all
   existing rem values remain correct. On larger screens the scale is
   capped by whichever axis fills first (height at typical 16:9), so
   the 1800×1130 composition zooms proportionally instead of reflow.
   Hero positions use rem (not vw) so they follow canvas scale.
   Derivation: 100.44 = 1130 × 160 / 1800. At 1800×1130 → 11.25px.
   At 2560×1440 → min(16, 14.34) = 14.34px = 1.274× the 1800 base.

   Scoped to desktop (≥1024px); the mobile layout keeps its own design.
   Fully reversible: delete this block.
   ============================================================ */
@media (min-width:1024px){
  html{ font-size: max(10px, min(calc(100vw / 160), calc(100vh / 100.44))); }
  /* Hero positions in rem (= 1.6× the old vw coefficient at 1800px reference).
     At 1800×1130: 1vw = 18px = 1.6rem → vw×1.6 = same pixel value. On larger
     screens rem respects the canvas scale; vw would not. */
  .hero2-scroll{ top:calc(50% - 14.4rem); }
  .hero2-title{ top:calc(50% - 1.328rem); }
  .hero2-side{ top:calc(50% + 4.416rem); }
  .hero2-pill{ top:4.688rem; }
  .showreel-chrome{ top:7.92rem; }

  /* ---- CASE PAGES: px caps → rem so they follow the canvas scale ----
     The fixed px max-widths/caps below were tuned to the 1800×1130 laptop. On a
     larger external monitor rem (text) grows with the canvas scale but the px
     containers did NOT → big type crammed into laptop-width columns and images
     that looked relatively small. Same values, expressed in rem (px ÷ 11.25):
     byte-identical at the reference, proportional on every other screen.
     Horizontal 5vw/6vw/8vw paddings become rem too (5vw@1800 = 8rem, 6vw = 9.6rem,
     8vw = 12.8rem) so gutters scale with the composition, not with raw width. */
  .case-head{ height:min(88vh, 88.9rem); }
  .case-head-info{ max-width:106.7rem; padding:0 9.6rem 4.3vh; }
  .case-section{ grid-template-columns:minmax(0, 33.8rem) minmax(0, 1fr); max-width:128rem; padding:0 8rem; }
  .case-divider-label{ max-width:113.8rem; padding:0 9.6rem; }
  .case-duo{ max-width:138.7rem; padding:0 9.6rem; }
  .case-trio{ max-width:149.3rem; padding:0 8rem; }
  .kl-half{ padding:9vh 9.6rem; }
  .kl-frame{ width:min(84%, 62vh, 48rem); }
  .case-cinema{ padding:clamp(3.5rem, 9vh, 7rem) 9.6rem; }
  .cinema-frame{ max-height:min(84vh, 85.3rem); }
  .case-cinema video{ max-height:min(84vh, 85.3rem); }
  .case-media-row{ max-width:128rem; padding:0 8rem; }
  .case-media-row.wide{ max-width:117.3rem; }
  .case-media-row.vertical{ grid-template-columns:repeat(auto-fit, minmax(21.3rem, 35.6rem)); padding:10vh 9.6rem; }
  .single-frame video, .case-media-row.single video{ max-height:min(82vh, 83.6rem); }
  .case-stats{ max-width:122.7rem; grid-template-columns:repeat(auto-fit, minmax(14.2rem, 1fr)); padding:0 8rem; }
  .case-quote{ width:min(80rem, 88vw); padding:0 9.6rem; }
  .case-learn{ width:min(97.8rem, 88vw); max-width:97.8rem; padding:clamp(2.6rem, 5vw, 4.2rem) 12.8rem; }
  .case-close, .cc-dot{ width:5.7rem; height:5.7rem; }
  .cc-x{ width:1.7rem; height:1.7rem; }
  .cc-label{ left:6.2rem; top:1.4rem; }
}

/* ============================================================
   ABOUT PAGE (about.html) — Kalafatis-style dedicated page.
   Reuses the global #aerial-bg shader, the top chrome (logo /
   pill-nav / get-in-touch) and the finale ("Let's connect").
   Only the photo-hero, ABOUT ME and SKILLS sections are new.
   Scoped under body.about-page so nothing here touches the
   landing page.
   ============================================================ */
.about-page{ background:transparent; }
.about-page-inner{ position:relative; }

/* ---------- 01 · PHOTO HERO — near full-screen; name straddles the fold ---------- */
.abh{
  position:relative;
  height:100vh;                         /* exactly one viewport on landing */
}
.abh-frame{
  position:absolute; left:4.4vw; right:5.4vw; top:12vh;   /* clears the nav bar above; edges tied to the logo (left:4.4vw) and the Get in touch pill's right edge (right:5.4vw) */
  height:71vh;                          /* shorter → more whitespace below for the name + tagline */
  border-radius:18px; overflow:hidden;
  border:none;
  /* the real scene: the empty-background photo (achtergrond v10) sits behind
     the person cut-out as an <img> layer; this is only a loading fallback */
  background:#141414;
  box-shadow:0 40px 120px -40px rgba(0,0,0,.7);
  isolation:isolate;
}
/* the background layer of the scene — IDENTICAL crop rules to .abh-person so
   the cut-out falls exactly back into place (same photo, person removed) */
.abh-bg{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:center 19%;
  /* the wall's whole grade (blur 28 + saturate 0 + brightness .86 + contrast
     1.05) is BAKED into studio-wand-2560-baked.jpg — a live CSS blur this
     size is rendered in GPU tiles and intermittently showed the tile seams
     as light bands behind the head. No runtime filter = no seams, ever. */
  transform:scale(1.2); transform-origin:50% 32%;
}
/* soft key-light pool behind the head — under the person, over the backdrop.
   OFF-AXIS (top-left), same light direction as the landing portrait */
.abh-light{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  /* staande ovaal die de figuur volgt (hoofd + torso), zelfde idee als .ip-light;
     x volgt de persoon (hoofd op ~27.5% van de framebreedte sinds de
     kleinere/hogere stand); extra tussenstops = geen zichtbare ringrand */
  background:radial-gradient(ellipse 34% 72% at 28% 40%,
    rgba(255,255,255,.34) 0%, rgba(255,255,255,.18) 36%,
    rgba(255,255,255,.07) 60%, transparent 82%);
}
/* cast shadow of the person on the wall — down-right of the figure (key light
   is top-left), painted OVER the light pool, under the person. A gradient
   layer, not a drop-shadow filter: that filter drew GPU tile seams here. */
.abh-cast{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  /* de png draagt zelf al een gebakken slagschaduw — dit is alleen de zachte
     wandval rechtsonder de figuur; dichter op de persoon (die staat nu op
     ~27.5% breedte) en lichter, anders leest hij als losse donkere vlek */
  background:radial-gradient(ellipse 22% 52% at 34% 70%,
    rgba(0,0,0,.26) 0%, rgba(0,0,0,.12) 44%,
    rgba(0,0,0,.04) 66%, transparent 82%);
}
/* vignette — darkens from the edges inward so the eye lands on the person */
/* inner-border vignette over the scene — the darker falloff along the frame edges */
.abh-frame::after{
  content:''; position:absolute; inset:0; z-index:3; pointer-events:none;
  border-radius:inherit;
  /* grain + diagonal shade (light from top-left) + edge vignette — the same
     three-layer dressing as the landing portrait (.intro-photo::before) */
  background:
    var(--film-grain),
    linear-gradient(135deg, transparent 52%, rgba(0,0,0,.08) 82%, rgba(0,0,0,.14) 100%),
    linear-gradient(to bottom, rgba(0,0,0,.26) 0%, rgba(0,0,0,.10) 9%, transparent 20%),
    radial-gradient(82% 100% at 38% 46%,
      transparent 22%, rgba(0,0,0,.05) 38%, rgba(0,0,0,.12) 52%, rgba(0,0,0,.22) 65%,
      rgba(0,0,0,.35) 78%, rgba(0,0,0,.50) 89%, rgba(0,0,0,.65) 100%);
  background-size:160px 160px, 100% 100%, 100% 100%, 100% 100%;
}
/* transparent cut-out of the person on the studio backdrop; biased a touch
   upward so the face stays in frame when cover crops top/bottom */
.abh-person{
  /* the box runs 4% PAST the frame bottom: the small translateY-up would
     otherwise expose the img box's bottom clip edge as a bar of wall under
     the shirt — now real bitmap (the photo has more shirt below) fills down
     past the frame edge and overflow:hidden does the cropping */
  position:absolute; top:0; left:0; width:100%; height:104%; z-index:2;
  object-fit:cover; object-position:center 58%;   /* no-JS fallback; about.js (fitPerson) overschrijft dit per viewport: zo hoog mogelijk zonder het haar te knippen */
  transform:scale(1.01) translate(-25.5%, -1.7%); transform-origin:50% 100%;   /* scale/translate hier wijzigen? dan ook de match-cut-fallback (match-cut.js) bijwerken */   /* persoon op ~1/3 van de framebreedte; iets kleiner, haar net onder de framerand (fitPerson pint de haarlijn) */ /* origin op de ONDERRAND; de kleine translateY omhoog verdwijnt onder de bodemvignette */
  /* JPEG-cutout draagt GEEN gebakken schaduw meer — daarom nu een live
     drop-shadow die het silhouet volgt (key light links-boven → val rechts-
     onder). Twee lagen: een strakke contactschaduw dicht op de figuur + een
     bredere, zachte wandval eronder. Ruime blur/offset houdt de rand vloeiend
     zodat er geen tegelnaad-banden achter het hoofd ontstaan. */
  filter:
    drop-shadow(6px 8px 10px rgba(0,0,0,.30))
    drop-shadow(20px 26px 40px rgba(0,0,0,.34));
}
/* the name straddles the frame's LOWER EDGE 50/50 (half on the photo, half on the
   dark below it), shifted a touch right of centre. bottom:14vh == 100 - 12(top) -
   74(height), so it tracks the frame's bottom edge. */
.abh-name{
  position:absolute; left:74.9%; bottom:calc(18.2vh + 16px); transform:translate(-50%,50%); z-index:2;   /* tracks the shorter frame's lower edge; a touch further right + a hair lower */
  font-family:var(--v4-gro); font-weight:500; color:#fff;
  font-size:clamp(4.6rem, 14.6vw, 16.4rem); line-height:.82; letter-spacing:-.03em;
  white-space:nowrap; pointer-events:none; text-shadow:0 2px 34px rgba(0,0,0,.5);
}
/* tagline sits in the whitespace BELOW the frame (not on the photo) — RIGHT-
   aligned against the frame's right edge (the oversized GEERT is retired, so
   this line is the hero's only voice; right:5.4vw = the frame's right edge) */
.abh-tagline{
  position:absolute; right:5.4vw; bottom:5.8vh; margin:0; max-width:40ch; z-index:2;
  text-align:right;
  font-family:var(--v4-mono); font-size:clamp(1.55rem, 1.85vw, 2.45rem); line-height:1.45;
  letter-spacing:.02em; text-transform:uppercase; color:rgba(247,243,236,.82);
}
/* scroll cue — "01 / 03 — SCROLL" in mono, LEFT-aligned on the frame's left
   edge (left:4.4vw = the logo/frame axis) and bottom-aligned with the right-
   aligned tagline (bottom:5.8vh): the hero's two quiet mono voices share one
   baseline band. Same numbering language as the page-heads (01/ … /03).
   Opacity of the whole cue is driven per-frame by about.js (rides the scroll). */
.abh-cue{
  position:absolute; left:4.4vw; bottom:5.8vh; z-index:2;
  text-decoration:none;
}
.abh-cue-txt{
  font-family:var(--v4-mono); font-size:clamp(.8rem, .95vw, 1.1rem);
  letter-spacing:.26em; line-height:1.45;
  text-transform:uppercase; color:rgba(247,243,236,.5); white-space:nowrap;
  transition:color .3s ease;
}
.abh-cue:hover .abh-cue-txt{ color:rgba(247,243,236,.85); }
/* filmstrip cue — just the word SCROLL, slowly glowing in and out */
.abfs-cue-word{
  display:block;
  font-family:var(--v4-mono); font-size:clamp(.8rem, .95vw, 1.1rem);
  letter-spacing:.26em; text-transform:uppercase;
  color:rgba(247,243,236,.9); white-space:nowrap;
  animation:abfs-glow 3.4s ease-in-out infinite;
}
@keyframes abfs-glow{
  0%, 100% { opacity:.15; }
  50%      { opacity:.8; }
}
html.reduced .abfs-cue-word{ animation:none; opacity:.5; }
@media (prefers-reduced-motion:reduce) and (min-width:99999px){
  .abfs-cue-word{ animation:none; opacity:.5; }
}
.abh-cue.gone{ opacity:0; pointer-events:none; }
/* landing filmstrip: frames with a case behind them are clickable */
.abfs-frame[data-case]{ cursor:pointer; }
/* the retired GEERT name — keep the element rules harmless if it ever returns */
.abh-name{ display:none !important; }

/* ---------- ABOUT ME / SKILLS — same head system as the hero page ----------
   thin rule + (01/ · name · /02), body pushed to the right two-thirds. */
/* rhythm: content sits CLOSE to its own section head (they belong together) and
   far from the next section's rule — clear grouping, one rhythm for every block.
   All content lives on the same axis: left rail to 36%, body from 36% → right. */
/* every block is the same room: the head rides the top, the content sits at
   the SAME optical centre of each section — one vertical rhythm, three times */
.ab-block{
  position:relative; display:flex; flex-direction:column; justify-content:center;
  min-height:86vh;
  padding:clamp(11rem,16vh,14rem) 4.4vw clamp(8rem,12vh,10rem);
}
/* about-me shares the exact two-column grid of journey + skills */
.ab-me-grid{
  display:grid; grid-template-columns:minmax(0,36%) 1fr;
  align-items:start;
}
/* the quiet mono rail: four facts, keys dim, values lit.
   align-self:center = vertically centred against the story column,
   whatever its height — no magic offsets. */
.ab-facts{
  list-style:none; margin:0; padding:0; align-self:center;
  display:flex; flex-direction:column; row-gap:clamp(.9rem,1.5vh,1.3rem);
}
.ab-facts li{ display:flex; flex-direction:column; gap:.25rem; }
.abf-k{
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:clamp(.95rem,.8vw,1.1rem);
  letter-spacing:.22em; text-transform:uppercase; color:rgba(247,243,236,.34);
}
.abf-v{
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:clamp(1.15rem,1vw,1.35rem);
  letter-spacing:.03em; color:rgba(247,243,236,.78);
}
.ab-body{ min-width:0; }
.ab-body p{
  font-family:var(--v4-gro); font-weight:400; color:#fff;
  font-size:clamp(2.4rem, 3.1vw, 3.3rem); line-height:1.28; letter-spacing:-.014em;   /* bigger than the case body; cap 3.3 → "forgotten." haalt regel 3 */
  margin:0 0 1.05em;
}
.ab-body p:last-child{ margin-bottom:0; }

/* ---------- 02 · JOURNEY = THE FILMSTRIP ----------
   His years as one strip of 35mm film. The section is tall (300vh); a sticky
   100vh viewport holds the strip, and scrolling slides it horizontally so
   each frame passes dead-centre in turn. The centred frame is the exposed
   one — warm, sharp, note visible; the others are dark negatives. */
.ab-journey{ display:block; height:300vh; min-height:0; padding:0; }
.abfs-stick{
  position:sticky; top:0; height:100vh; overflow:hidden;
  display:flex; flex-direction:column; justify-content:center;
}
/* the strip: continuous film base with perforation rows top + bottom */
.abfs-strip{ position:relative; }
.abfs-track{
  display:inline-flex; width:max-content; column-gap:clamp(1.4rem,1.2vw,2.2rem);
  padding:clamp(3.4rem,4vh,4.4rem) clamp(1.8rem,1.6vw,2.6rem);
  background:rgba(10,10,11,.78);
  border-top:1px solid rgba(247,243,236,.14);
  border-bottom:1px solid rgba(247,243,236,.14);
  position:relative; will-change:transform;
}
.abfs-track::before, .abfs-track::after{
  content:''; position:absolute; left:0; right:0; height:clamp(1rem,1.2vh,1.3rem);
  background:repeating-linear-gradient(90deg,
    transparent 0 12px, rgba(247,243,236,.13) 12px 30px, transparent 30px 46px);
  pointer-events:none;
}
.abfs-track::before{ top:clamp(1rem,1.2vh,1.3rem); }
.abfs-track::after{ bottom:clamp(1rem,1.2vh,1.3rem); }
/* one frame — a dark negative until the light reaches it */
.abfs-frame{
  margin:0; position:relative; flex:0 0 auto;
  width:clamp(40rem,32vw,54rem); min-height:clamp(36rem,54vh,56rem);
  padding:2rem 1.9rem 1.9rem;
  background:#0a0a0b;
  border:1px solid rgba(247,243,236,.10); border-radius:6px;
  display:flex; flex-direction:column; justify-content:flex-end; gap:1.1rem;
  overflow:hidden; isolation:isolate;
  /* the roaming light band (same idea as the skills wall): main.js feeds each
     frame its distance-to-centre as --lit (gaussian: 1 = centred, ~.45 =
     neighbour, 0 further out). Scroll drives it directly and EVERY lit
     property below rides it — opacity, border, exposure, year/title alpha —
     so no transitions, no class-snap: the glow is exactly as smooth as the
     scrub. The .lit class only marks the exposed frame (note reveal, counter). */
  opacity:calc(.5 + .5 * var(--lit, 0));
  border-color:rgba(247,243,236,calc(.10 + .26 * var(--lit, 0)));
}
/* the soft exposure that develops inside the lit frame — ivory, like the
   rest of the site's light (no warm amber; that tint lives nowhere else) */
.abfs-frame::before{
  content:''; position:absolute; inset:0; z-index:-1; opacity:var(--lit, 0);
  background:radial-gradient(95% 72% at 50% 20%,
    rgba(247,243,236,.12) 0%, rgba(247,243,236,.035) 55%, transparent 82%);
}
/* the photo in the frame: a dark negative until the light develops it */
.abfs-img{
  position:relative; width:100%; aspect-ratio:3/2; margin:3.4rem 0 0;
  border-radius:3px; overflow:hidden; background:rgba(247,243,236,.04);
}
.abfs-img img{
  width:100%; height:100%; object-fit:cover; display:block;
  filter:grayscale(1) brightness(.42) contrast(1.05);
}
/* the light band must never animate `filter` (that re-rasterises every image
   each scroll tick = jank). Instead main.js stacks a colour CLONE on top of
   the static grey negative; only its opacity rides --lit — pure compositor
   work, exactly as smooth as the scrub. */
.abfs-img{ position:relative; }
.abfs-img .abfs-img-lit{
  position:absolute; inset:0; filter:none;
  opacity:var(--lit, 0); will-change:opacity;
}
/* FR.07: the next frame is still loading — dashed empty film + mono
   "loading next frame" with three softly pulsing dots */
.abfs-unexp{
  aspect-ratio:3/2; display:flex; align-items:center; justify-content:center;
  border:1px dashed rgba(247,243,236,calc(.18 + .22 * var(--lit, 0)));
  background:transparent;
}
.abfs-load{
  display:inline-flex; align-items:baseline; gap:1rem;
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:1.05rem;
  letter-spacing:.3em; text-transform:uppercase;
  color:rgba(247,243,236,calc(.28 + .60 * var(--lit, 0)));
}
.abfs-dots{ display:inline-flex; gap:.55rem; }
.abfs-dots i{
  width:.45rem; height:.45rem; border-radius:50%; background:currentColor;
  animation:abfsdot 1.3s infinite ease-in-out;
}
.abfs-dots i:nth-child(2){ animation-delay:.22s; }
.abfs-dots i:nth-child(3){ animation-delay:.44s; }
@keyframes abfsdot{
  0%, 60%, 100%{ opacity:.25; transform:none; }
  30%{ opacity:1; transform:translateY(-.35rem); }
}
@media (prefers-reduced-motion:reduce) and (min-width:99999px){ .abfs-dots i{ animation:none; } }
.abfs-meta{
  position:absolute; top:1.7rem; left:1.9rem; right:1.9rem;
  display:flex; justify-content:space-between; align-items:baseline; gap:1rem;
}
.abfs-no{
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:1.05rem;
  letter-spacing:.24em; color:rgba(247,243,236,.30);
}
.abfs-year{
  font-family:var(--v4-gro); font-weight:500;
  font-size:clamp(2.5rem,2.8vw,3.6rem); line-height:1; letter-spacing:-.02em;
  color:rgba(247,243,236,calc(.30 + .70 * var(--lit, 0)));
}
.abfs-title{
  margin:0; font-family:var(--v4-gro); font-weight:500;
  font-size:clamp(2.1rem,2.1vw,2.7rem); line-height:1.14; letter-spacing:-.016em;
  color:rgba(247,243,236,calc(.42 + .58 * var(--lit, 0)));
}
.abfs-note{
  margin:0; font-family:var(--v4-mono),'Space Mono',monospace;
  font-size:clamp(1.15rem,1vw,1.4rem); line-height:1.6; letter-spacing:.01em;
  color:rgba(247,243,236,.6);
  opacity:0; transform:translateY(10px);
  transition:opacity .6s ease .1s, transform .6s cubic-bezier(.2,.7,.2,1) .1s;
}
/* — LIT: this frame is the exposed one. Only the note reveal stays on the
   class (a discrete "developed" moment with its own soft transition); all
   light/colour rides --lit continuously above. — */
.abfs-frame.lit .abfs-note{ opacity:1; transform:none; }
.abfs-now{ box-shadow:0 0 44px -8px rgba(247,243,236,calc(.18 * var(--lit, 0))); }
/* frame counter, quiet, lower-left of the sticky viewport */
.abfs-count{
  position:absolute; left:4.4vw; bottom:5vh;
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:1.1rem;
  letter-spacing:.3em; color:rgba(247,243,236,.34);
}
/* THE VOID — fixed black sheet behind all content but above the aerial
   shader (same z-index, later in the DOM). about.js fades it in as the
   strip pins and out as it unpins: the filmstrip floats in endless dark. */
.abfs-void{
  position:fixed; inset:0; background:#000;
  opacity:0; pointer-events:none; z-index:-1;
}
/* scroll cue — centred under the strip: the word SCROLL breathing slowly
   (see .abfs-cue-word above). Surfaces once the void has settled, gone on
   scrub, back when the visitor returns to frame one (about.js show/hide). */
.abfs-cue{
  position:absolute; left:50%; bottom:2.6vh; transform:translateX(-50%);
  opacity:0; transition:opacity .7s ease;
}
.abfs-cue.show{ opacity:1; }
.abfs-cue.hide{ opacity:0; }

/* the shared .ab-block rhythm (min-height:86vh, vertical-centred) leaves a
   huge empty band above AND below the wall on the landing — there's no
   next section pulling it taut like on the standalone about page. Trim it
   so the proof-strip above and the finale below both sit close. */
.ab-skills{ min-height:auto; padding:clamp(14rem,18vh,17rem) 4.4vw clamp(3.5rem,6vh,5rem); }

/* ---------- 03 · SKILLS = THE WALL OF LIGHT ----------
   Six oversized hollow (stroked) skill lines, edge to edge under hairlines.
   ONE soft band of light drifts over the wall, driven directly by scroll
   (about.js sets --wa/--wb per word): the centred word is solid white, the
   band spills into the tail of the previous word and the head of the next,
   and its front/back edges fade cleanly inside the glyphs — a haze passing
   over, not word-for-word steps. Click a line → it opens like a drawer:
   chips with what the skill concretely covers + the receipt (project +
   result). One open at a time. Reduced motion: the wall arrives fully lit. */
.absw{ position:relative; }
.absw-item{ border-top:1px solid rgba(247,243,236,.13); }
.absw-item:last-child{ border-bottom:1px solid rgba(247,243,236,.13); }
.absw-line{
  position:relative; display:flex; align-items:center; gap:2.2vw;
  /* niet langer klikbaar (zie main.js, wallOfLight): geen handje meer, want een
     cursor die klikbaarheid belooft waar niets gebeurt is erger dan geen cursor */
  padding:.4rem 1rem; cursor:default; outline-offset:-3px;
}
/* de lade en het plusje horen bij de uitvouw-interactie die eruit is. HTML +
   vertalingen blijven staan; dit zet ze alleen uit. */
.absw-drawer, .absw-plus{ display:none !important; }
.absw-item:nth-child(even) .absw-line{ justify-content:flex-end; }
.absw-no{
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:1.1rem;
  letter-spacing:.14em; color:rgba(247,243,236,.24); flex:0 0 auto;
  transition:color .6s ease;
}
.absw-item.open .absw-no{ color:var(--ivory); }
.absw-word{
  /* NIET het systeemfont: SF Pro heeft overlappende glyph-paden die als naden
     zichtbaar worden in de holle stroke-letters — General Sans strookt schoon */
  margin:0; font-family:'General Sans','Archivo',sans-serif; font-weight:700; text-transform:uppercase;
  font-size:clamp(4.2rem,7vw,12.5rem); line-height:1.16; letter-spacing:-.02em;
  white-space:nowrap;
  color:transparent; -webkit-text-stroke:1.5px rgba(247,243,236,.30);
  /* THE LIGHT BAND: about.js feeds each word the band's edges in its own
     horizontal space (--wa/--wb, % of the word). Between the edges the
     glyphs are solid white; an 18%-wide feather before and after each edge
     is the clean front/back fade. No transition — the band is driven
     directly by scroll, so the motion is exactly as smooth as the scroll. */
  background-image:linear-gradient(90deg,
    rgba(247,243,236,0) calc(var(--wa,-500%) - 18%),
    #fff var(--wa,-500%),
    #fff var(--wb,-500%),
    rgba(247,243,236,0) calc(var(--wb,-500%) + 18%));
  -webkit-background-clip:text; background-clip:text;
}
/* open drawer (or reduced motion): the whole word holds the light */
.absw-item.open .absw-word, .absw.lit-all .absw-word{
  background-image:linear-gradient(100deg, #fff 8%, var(--ivory) 46%, rgba(247,243,236,.78) 92%);
}
.absw-plus{
  font-family:var(--v4-gro); font-weight:400; font-size:clamp(2rem,1.8vw,3rem);
  color:rgba(247,243,236,.22); flex:0 0 auto;
  transition:transform .5s ease, color .5s ease;
}
.absw-item.open .absw-plus{ transform:rotate(45deg); color:var(--ivory); }
/* the drawer: chips (what it covers) + the receipt (project + result).
   Height is animated by about.js (gsap, to the REAL content height) — a
   max-height transition overshoots and reads jerky. */
.absw-drawer{ height:0; overflow:hidden; }
.absw-drawer-in{
  display:grid; grid-template-columns:1fr auto; gap:2rem; align-items:end;
  padding:.6rem 1rem 4rem;
}
.absw-chips{ display:flex; flex-wrap:wrap; gap:1rem; align-self:end; }
.absw-chips span{
  font-family:var(--v4-mono),'Space Mono',monospace; font-size:1.1rem;
  letter-spacing:.1em; color:rgba(247,243,236,.72);
  border:1px solid rgba(247,243,236,.22); border-radius:999px; padding:.8rem 1.4rem;
  white-space:nowrap;
}
.absw-rcpt{
  margin:0; font-family:var(--v4-mono),'Space Mono',monospace;
  font-size:1.15rem; line-height:1.7; letter-spacing:.02em;
  color:rgba(247,243,236,.82); text-align:right; max-width:42ch;
}
.absw-rcpt b{ display:block; color:#fff; font-weight:700; }

/* ---------- viewfinder crosshair tick (photo hero, like the reference) ---------- */
.ab-tick{ position:absolute; width:15px; height:15px; opacity:.55; pointer-events:none; }
.ab-tick::before, .ab-tick::after{ content:''; position:absolute; background:rgba(247,243,236,.65); }
.ab-tick::before{ left:50%; top:0; width:1px; height:100%; transform:translateX(-50%); }
.ab-tick::after{ top:50%; left:0; height:1px; width:100%; transform:translateY(-50%); }
.abh-tick{ left:16%; top:40vh; }       /* on the photo, clear of the name */

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

/* MOBILE-ONLY chrome elements are hidden by default (desktop) and turned on
   inside the phone media queries below. This guard sits BEFORE those queries
   so the mobile show-rules (later in source) always win. */
.m-burger, .m-menu, .hero2-marquee, .hero2-tag, .connect-circle, .connect-socials{ display:none; }

@media (max-width:860px){
  .abh-frame{ left:50%; right:auto; transform:translateX(-50%); width:96vw; height:58vh; top:15vh; }
  .abh-name{ font-size:clamp(3rem, 18vw, 8rem); bottom:27vh; left:58%; }   /* 100 - 15 - 58 */
  .abh-tagline{ font-size:1.3rem; bottom:3vh; }
  .abh-tick{ display:none; }

  /* one column: the index becomes a wrapping row above its panel */
  .ab-block{ min-height:0; padding:8rem 4.4vw 6rem; }
  .ab-me-grid{ grid-template-columns:1fr; row-gap:3rem; }
  .ab-facts{ padding-top:0; flex-direction:row; flex-wrap:wrap; gap:1.3rem 2.6rem; }

  /* WALL OF LIGHT -> Geert wants the desktop alternating left/right pattern
     back on mobile too (was flattened to all-left here — see the even-row
     override this used to carry). Base rule now wins again:
     .absw-item:nth-child(even) .absw-line{ justify-content:flex-end } */
  .absw-line{ gap:2.6vw; padding:1rem .2rem; align-items:flex-start; }
  .absw-item:nth-child(even) .absw-line{ text-align:right; }
  .absw-no{ padding-top:1.2rem; }
  .absw-word{ font-size:7.2vw; line-height:1.04; white-space:normal; -webkit-text-stroke-width:1.2px; flex:1 1 auto; min-width:0; }
  .absw-plus{ margin-left:auto; padding-top:.6rem; }
  .absw-drawer-in{ grid-template-columns:1fr; gap:1.6rem; padding:.4rem .2rem 3rem; }
  .absw-rcpt{ text-align:left; max-width:none; }

  /* FILMSTRIP: the desktop scroll-scrub (sticky + horizontal drive) now runs
     on mobile too — the old static-swipe fallback is retired (Geert wil
     exact het desktop-gedrag). Frame sizing lives in the MOBILE PASS layer. */
}

/* ============================================================
   MOBILE PASS (3 jul 2026) — phone polish layer, appended last so it
   wins every earlier mobile rule. Desktop is untouched: everything
   lives inside max-width / hover:none media queries.
   Design intent: the desktop composition translated to one calm
   column — same voice (mono chrome, oversized display type, one lit
   thing at a time), sized for a hand instead of shrunken desktop.
   ============================================================ */

@media (max-width:860px){

  /* ---- per-page header bar: tighter, name truly centred ---- */
  .page-head{ top:clamp(3.4rem,6vh,5rem); }
  .page-head-rule{ height:2px; }
  .page-head-row{ margin-top:.95rem; font-size:.72rem; letter-spacing:.12em; }
  .page-head-row .ph-name{ left:50%; }

  /* ---- SELECTED WORK ---- */
  /* top padding must clear the absolute page-head (~90px tall incl. rule):
     content was starting 8px under the head row */
  /* top clamp trimmed ~18px (was 8.5/16vh/10.5rem): the rule→text gap here
     ran wider than the same gap in About (91.8px vs 73.7px) — matched */
  .work{ padding:clamp(7.4rem,13.8vh,9.4rem) 5vw 14vh; }
  /* the big section quote: left-set single column instead of a cramped
     right-aligned block; the desktop <br>'s force bad breaks here */
  .work .sec-intro, .more .more-sub{
    font-size:clamp(1.5rem,6.4vw,2.1rem); line-height:1.25; letter-spacing:-.01em;
    max-width:24ch; margin:0; padding:0; text-align:left;
  }
  .work .sec-intro br, .more .more-sub br{ display:none; }
  /* the centred flex column started the sub line UNDER the page-head row
     (gap measured -8px) — give the section head-clearing top padding */
  .more{ padding:clamp(9rem,17vh,11.5rem) 0 10vh; }
  .more .more-sub{ padding:0 5vw; margin:0 0 4.5vh; }
  /* film-frame corner meta back on the mono chrome scale */
  .card-no, .card-cat{ font-size:.68rem; letter-spacing:.16em; top:1.3rem; }
  .card-no{ left:1.4rem; }
  .card-cat{ right:1.4rem; max-width:62%; }
  .card-title{ text-wrap:balance; }
  .card-client{ font-size:1.02rem; }

  /* ---- BEYOND THE BRIEF ---- */
  /* FIX: aspect-ratio(3/2) × min-height(38vh) forced each panel to 451px wide
     on a 375px phone (cut off right). Width rules; height follows the ratio. */
  .more-panel{ min-height:0; width:100%; }
  .panel-info{ padding:1.4rem 1rem 1.7rem; }
  .panel-info h3{ font-size:clamp(1.7rem,7vw,2.1rem); }
  .panel-desc{ font-size:.95rem; line-height:1.5; max-width:32ch; }
  .more-panel > .card-tag{ font-size:.68rem; letter-spacing:.16em; top:1.2rem; right:1.3rem; }
  .panel-cta{ font-size:.7rem; padding:.6rem 1.25rem; }
  .more-proof{ gap:.7rem; }
  .more-proof li{ padding:1.7rem .5rem; gap:.6rem; border-radius:10px; }
  .more-proof strong{ font-size:clamp(1.9rem,9vw,2.4rem); }
  .more-proof span{ font-size:.62rem; letter-spacing:.15em; }

  /* ---- FINALE ----
     Used to rebuild to Chris's composition here (circular "write a message"
     button instead of the desktop pill) — Geert wants mobile to actually
     match desktop instead, so the circle is gone and the real copy-email
     pill + mail-app line are shown here too, same component, same click
     behaviour (main.js's copy handler is device-agnostic already). Only the
     LinkedIn foot + credit line stay swapped for the bottom social row —
     that pairing already works and isn't what was asked to change. */
  /* The section was 100vh + 12vh/14vh padding, so at the document bottom the
     sticky stage had scrolled 114px PAST the viewport while .connect-frame
     (position:fixed) stayed put — content and frame were misaligned by exactly
     that, which read as "everything sits too high with a hole above the socials".
     Pinning the section to one screen puts the content back inside its frame. */
  /* dvh second declaration, same reasoning as .finale-stick above — keeps
     this frame matching the real viewport if Safari's chrome reappears on
     a forced overscroll past the end */
  .finale{ height:100vh; height:100dvh; min-height:100vh; min-height:100dvh; padding:0; }
  .finale-inner{ padding:12vh 6vw 6vh; }
  /* LinkedIn now uses the DESKTOP footer (.connect-foot), not the old mobile
     social row: it's already positioned outside the frame by design (bottom:
     3vh, vs. the frame's own inset of bottom:9vh — see the component comment
     above .connect-foot) so un-hiding it does exactly what Geert asked
     without any new CSS. Email is gone — Geert doesn't want it, and there's
     no longer a row for it to sit in. .connect-socials (the old LinkedIn+
     Email row) is retired outright: no mobile show-rule left for it, so it
     falls back to its default display:none. */
  .finale .site-credit{ display:none !important; }

  /* the whole group (kicker → title → pill) is optically centred in the
     frame as one column; connect-foot is pinned outside/below it and is NOT
     part of the centring. */
  .connect-card{ justify-content:center; padding-bottom:0; gap:0; }
  .connect-kicker{ font-size:.74rem; letter-spacing:.16em; margin-bottom:1.5rem; max-width:28ch; }
  .finale .connect-title{ font-size:clamp(3.4rem,15.5vw,5rem); margin-bottom:0; }
  /* the desktop pill's font-size floor (1.35rem) + padding (2.3rem sides) was
     tuned for a wide button with room to spare — on a 330px-wide mobile card
     the full email address at that size doesn't fit and the pill overflowed
     the frame on both sides. Sized down again per Geert's feedback: the pill
     (and the mail-app line under it) still read as too big against the
     title. Smaller pill also pulls the whole group's visual weight up
     toward the title, which was reading off-centre in the frame. */
  .connect-write{
    margin-top:2.4rem; max-width:100%; box-sizing:border-box;
    font-size:clamp(.82rem, 3.4vw, 1rem); padding:.7rem 1.2rem;
  }
  .connect-meta{ font-size:.72rem; margin-top:.9rem; }
  /* the footer LinkedIn button inherits the desktop 1.46rem — next to a
     mobile pill sized at ~.9rem it was the loudest thing in the section, and
     it is a footer link, not a call to action. Down to the same register as
     the rest of the mobile chrome, with padding to match so the hover fill
     still reads as a pill. */
  .connect-foot{ bottom:2.4vh; right:5.4vw; }
  /* TAP TARGET, not just a label. At .55em of vertical padding this rendered
     as a 95×30px hit area tucked into the bottom-right corner — under the
     ~44px minimum a fingertip can reliably land on, which is why it read as
     "the LinkedIn link doesn't do anything" on a phone while working fine
     under a mouse. The padding is what grows (to ~44px tall); the type size
     and letterspacing are untouched, so it looks the same and only becomes
     easier to hit. touch-action:manipulation drops the legacy double-tap
     delay so the first tap registers immediately. */
  .connect-linkedin{
    font-size:.82rem; letter-spacing:.1em; padding:1.1em 1.15em;
    touch-action:manipulation; -webkit-tap-highlight-color:transparent;
  }

  /* ---- CASE PAGES (overlays) ---- */
  /* the divider labels are <p>'s, so the big Telegraf body-copy clamp was
     winning (30px tracked uppercase wrapping 3 lines) — pin the meta scale */
  .ts-style .case-body p.case-divider-label{
    font-size:clamp(1.05rem,4.6vw,1.3rem) !important; letter-spacing:.2em; line-height:1.55;
  }
  /* was 1.1rem, left-aligned — smaller + centred, same treatment as the
     pull-quote source line */
  .ts-style .case-body p.cinema-caption{
    font-size:.85rem !important; letter-spacing:.06em; line-height:1.6;
    text-align:center;
  }
  .ts-style .case-trio figcaption, .ts-style .case-duo figcaption{ font-size:.9rem; letter-spacing:.08em; }
  .ts-style .kl-half .kl-caption{ font-size:.8rem; line-height:1.6; }
  .df-warm .kl-caption, .df-cool .kl-caption,
  .kl-warm .kl-caption, .kl-cool .kl-caption{ font-size:.95rem !important; white-space:normal; max-width:30ch; }
  .kl-num{ font-size:inherit; }
  .ts-style .case-stats span{ font-size:.7rem; }
  .ts-style .case-quote cite{ font-size:1.1rem; }
  /* Treinsafari + Dafi testimonials: the quote (.case-quote p) was actually
     losing to the general mobile body-copy rule above (1.12rem, higher
     specificity via its three :not()s) instead of its own intended clamp —
     it rendered SMALLER than the cite line below it, backwards from a
     pull-quote's hierarchy. #id scope wins outright. Sized compact per
     Geert's feedback: smaller, tighter leading, less air before the source
     line — both cases share the exact same numbers. */
  #case-treinsafari .case-quote p, #case-dafitime .case-quote p, #case-aftermovie .case-quote p{
    /* text-align-last:left leaks in from the general mobile body-copy rule
       (.ts-style .case-body p:not()... further up) — it doesn't fight
       text-align on most lines, but it DOES win for the paragraph's very
       last line specifically (that's what text-align-last targets), so the
       final line of the quote hung left while every line above it centred
       correctly. That's the "not really centred" Geert saw. */
    /* line-height 1.25 → 1.3: Geert wanted the lines a hair further apart —
       deliberately tiny, the quote is meant to stay compact. */
    font-size:1.42rem; line-height:1.3; text-align:center; text-align-last:center;
  }
  #case-treinsafari .case-quote cite, #case-dafitime .case-quote cite, #case-aftermovie .case-quote cite{
    font-size:.7rem; margin-top:1.2rem;
  }
  .book-controls button{ width:44px; height:44px; font-size:1.1rem; }
  .book-chapters{ gap:.5rem; }
  .book-chapters button{ font-size:.68rem; padding:.55rem 1rem; }
  .book-count{ font-size:.58rem; min-width:110px; }
  /* was .85rem — Geert: the dot may sit a touch lower still (repeatedly, in
     very small steps each time) */
  .case-close{ bottom:calc(.7rem + env(safe-area-inset-bottom)); }
  .case-next{ padding-bottom:calc(3.4rem + env(safe-area-inset-bottom)); }
  /* "NEXT PROJECT" kicker: was .9rem then 1rem, up another step */
  .ts-style .case-next > span{ font-size:1.1rem; letter-spacing:.3em; }

  /* Challenge / Approach / Solution: was centred (see the ~860px "MOBILE
     ONLY: project-page copy reads centred" block) — Geert wants this copy
     left-aligned like a normal editorial column instead. Same specificity as
     what it's overriding, so it just needs to come later in the file to win.
     Scoped to .case-section/.cs-text only — .case-cols and .split-text (used
     elsewhere) are untouched, not part of what was asked. */
  .case-section h3{ text-align:left; }
  .cs-text, .cs-text p{ text-align:left; }
  .cs-text p{ margin-left:0; margin-right:0; }

  /* ---- ABOUT PAGE ---- */
  .abh-frame{ left:50%; right:auto; transform:translateX(-50%); top:12vh; height:62vh; width:94vw; border-radius:14px; }
  .abh-name{ bottom:26vh; left:61%; font-size:clamp(3rem,19vw,7rem); }   /* 100 - 12 - 62; iets naar rechts (Geert) */
  .abh-tagline{ font-size:.9rem; line-height:1.65; max-width:30ch; bottom:4vh; }
  .ab-block{ min-height:0; padding:9rem 5.5vw 5rem; }   /* top clears the page-head; content sat 15px under the row */
  .ab-facts{ gap:1.1rem 2rem; }
  .abf-k{ font-size:.64rem; letter-spacing:.2em; }
  .abf-v{ font-size:.85rem; }
  .ab-body p{ font-size:clamp(1.35rem,5.9vw,1.7rem); line-height:1.42; }
  /* filmstrip: keep the desktop 300vh scrub geometry (guard against the
     generic .ab-block padding above), shrink only the frames + type */
  .ab-journey{ padding:0; height:300vh; }
  .abfs-frame{ width:87vw; min-height:30rem; padding:1.8rem 1.5rem 1.6rem; gap:.9rem; }
  .abfs-meta{ top:1.25rem; left:1.4rem; right:1.4rem; }
  .abfs-no{ font-size:.64rem; letter-spacing:.2em; }
  .abfs-year{ font-size:1.4rem; }
  .abfs-img{ margin-top:2.4rem; }
  .abfs-title{ font-size:1.28rem; line-height:1.2; }
  .abfs-note{ font-size:.82rem; line-height:1.55; }
  .abfs-load{ font-size:.92rem; letter-spacing:.24em; }
  .abfs-dots i{ width:.56rem; height:.56rem; }
  /* wall of light: bigger presence, meta chrome smaller */
  .absw-word{ font-size:13vw; line-height:1.05; }
  .absw-no{ font-size:.7rem; padding-top:.9rem; }
  .absw-plus{ font-size:1.5rem; }
  .absw-chips{ gap:.7rem; }
  .absw-chips span{ font-size:.72rem; padding:.55rem .95rem; }
  .absw-rcpt{ font-size:.8rem; line-height:1.6; }
}

@media (max-width:760px){

  /* ---- TOP CHROME: one compact row — wordmark · nav · contact pill ---- */
  .v4-logo{ top:1.9rem !important; left:6.2vw; }
  .v4-logo .v4-row{ font-size:1.18rem !important; }
  .pill-nav{ top:2.1rem !important; gap:2px !important; }
  .pill-nav a{ font-size:.6rem !important; letter-spacing:.09em !important; padding:1em .55em !important; }   /* tall padding = honest tap target, invisible until hover */
  .pill-nav a[data-section]{ display:inline-block !important; }   /* nav links back — they were gone on mobile */
  .hero2-pill{ top:1.9rem; right:4.6vw; font-size:.6rem; letter-spacing:.08em; padding:.6em .95em; }

  /* ---- HERO ---- */
  .hero2-title{ top:46%; }
  .hero2-h1{ font-size:clamp(2.6rem,13vw,4.6rem); }
  .hero2-h1 > span:first-child{ transform:translateX(-3vw); }
  /* side text: real paragraph under the headline, no more nowrap overflow */
  .hero2-side{
    left:50%; right:auto; top:auto; bottom:21vh; transform:translateX(-50%);
    white-space:normal; text-indent:0; text-align:center; width:86vw; max-width:30ch;
    font-size:.72rem; line-height:1.75; letter-spacing:.14em;
  }
  .hero2-side br{ display:none; }   /* desktop line breaks read ragged when centred */
  .hero-arrow{ bottom:calc(3.5vh + env(safe-area-inset-bottom)); width:64px; height:64px; }
  .hero-arrow .ha-svg{ width:60px; height:60px; }
  /* mobile keeps the 60px svg — restore the original viewBox stroke widths */
  .ha-arrow{ stroke-width:2.2; }
  .ha-ring{ stroke-width:1.5; }

}

/* =========================================================
   MOBILE-ONLY (≤760px) — Kalafatis-style hero chrome + marquee + hamburger.
   Everything here is scoped to phones; the burger, menu and marquee are
   display:none everywhere else so the desktop layout is untouched.
   Reversible: delete this whole block + the .m-burger/.m-menu/.hero2-marquee
   markup + mobile-nav.js.
   ========================================================= */

@media (max-width:760px){
  /* --- swap the pill-nav + "Get in touch" pill for a single hamburger --- */
  .pill-nav{ display:none !important; }
  .hero2-pill{ display:none !important; }

  .m-burger{
    display:flex; position:fixed; top:1.7rem; right:6.2vw; z-index:8002;
    width:40px; height:34px; align-items:center; justify-content:flex-end;
    background:none; border:0; padding:0; cursor:pointer; pointer-events:auto;
    -webkit-tap-highlight-color:transparent;
  }
  .m-burger-box{ position:relative; display:block; width:34px; height:16px; }
  /* the burger→X morph stays exactly as it was (Geert likes it) — the only
     change is HOW it eases: the bars used to arrive on one curve while the
     middle one blinked out on a shorter, different one, so the cross snapped
     together a beat before the panel had opened. Now the two outer bars
     travel to the middle first and only THEN rotate (two steps, one gesture),
     and the middle bar fades on the same clock as that first step. */
  .m-burger-box span{
    position:absolute; left:0; width:100%; height:2px; border-radius:2px;
    background:rgba(255,255,255,.96);
    transition:transform .5s cubic-bezier(.22,1,.36,1), opacity .2s ease .16s;
  }
  .m-burger-box span:nth-child(1){ top:0; }
  .m-burger-box span:nth-child(2){ top:50%; transform:translateY(-50%); }
  .m-burger-box span:nth-child(3){ bottom:0; }
  body.m-menu-open .m-burger-box span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
  body.m-menu-open .m-burger-box span:nth-child(2){ opacity:0; transition-delay:0s; }
  body.m-menu-open .m-burger-box span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }

  /* --- full-screen menu overlay ---
     It used to be ONE cross-fade: the whole panel, links and all, blinked
     from 0 to 1 in 0.42s linear-ish ease. Everything arrived at once, at the
     same speed, from nowhere — which is exactly what reads as cheap.
     Now it opens the way a good sheet opens:
       1. the ground goes first — the scrim fades and the blur builds under
          it, so the page behind recedes BEFORE anything new appears;
       2. the links follow one by one, each rising out of its own mask
          (clip-path, negative on the sides so no glyph edge is shaved) on a
          long ease-out — they arrive, they don't pop;
       3. the social row lands last, quietly.
     Closing is not the reverse: the delays live only in the open state, so
     on the way out everything leaves together on one short curve. An exit
     that staggers reads as hesitation; an exit that sweeps reads as intent. */
  .m-menu{
    display:flex; position:fixed; inset:0; z-index:8001;
    flex-direction:column; justify-content:center; padding:0 8vw;
    background:rgba(9,7,5,.97);
    -webkit-backdrop-filter:blur(0px); backdrop-filter:blur(0px);
    opacity:0; visibility:hidden; pointer-events:none;
    transition:opacity .34s cubic-bezier(.4,0,.2,1),
               visibility .34s,
               backdrop-filter .34s cubic-bezier(.4,0,.2,1),
               -webkit-backdrop-filter .34s cubic-bezier(.4,0,.2,1);
  }
  body.m-menu-open .m-menu{
    opacity:1; visibility:visible; pointer-events:auto;
    -webkit-backdrop-filter:blur(14px); backdrop-filter:blur(14px);
    transition-duration:.44s;
  }
  .m-menu-links{ display:flex; flex-direction:column; gap:1.35rem; }
  .m-menu-links a{
    display:block;
    font-family:var(--v4-gro); font-weight:500; text-transform:uppercase;
    font-size:2.7rem; line-height:1; letter-spacing:-.015em;
    color:#f4ece0; text-decoration:none;
    /* -18% top/bottom bleed keeps caps and any overshoot out of the mask edge */
    opacity:0; transform:translateY(34px); clip-path:inset(-18% -6% 100% -6%);
    transition:opacity .34s ease,
               transform .34s cubic-bezier(.4,0,.2,1),
               clip-path .34s cubic-bezier(.4,0,.2,1);
  }
  body.m-menu-open .m-menu-links a{
    opacity:1; transform:translateY(0); clip-path:inset(-18% -6% -18% -6%);
    transition-duration:.72s, .78s, .78s;
    transition-timing-function:ease, cubic-bezier(.16,1,.3,1), cubic-bezier(.16,1,.3,1);
  }
  body.m-menu-open .m-menu-links a:nth-child(1){ transition-delay:.10s; }
  body.m-menu-open .m-menu-links a:nth-child(2){ transition-delay:.17s; }
  body.m-menu-open .m-menu-links a:nth-child(3){ transition-delay:.24s; }
  body.m-menu-open .m-menu-links a:nth-child(4){ transition-delay:.31s; }
  .m-menu-social{
    position:absolute; left:8vw; right:8vw; bottom:calc(7vh + env(safe-area-inset-bottom));
    display:flex; gap:1.7rem;
    opacity:0; transform:translateY(12px);
    transition:opacity .3s ease, transform .3s cubic-bezier(.4,0,.2,1);
  }
  body.m-menu-open .m-menu-social{
    opacity:1; transform:translateY(0);
    transition-duration:.6s; transition-delay:.36s;
  }
  .m-menu-social a{
    font-family:var(--v4-mono); font-size:.72rem; letter-spacing:.14em; text-transform:uppercase;
    color:rgba(244,236,224,.55); text-decoration:none;
  }
  @media (prefers-reduced-motion:reduce) and (min-width:99999px){
    .m-menu-links a, .m-menu-social,
    body.m-menu-open .m-menu-links a, body.m-menu-open .m-menu-social{
      transition:none; transform:none; clip-path:none;
    }
  }

  /* --- HERO: replace the stacked static headline with a scrolling marquee.
         The real <h1> stays in the DOM (sr-only) for a11y/SEO. --- */
  .hero2-title{
    position:absolute !important; width:1px; height:1px; padding:0; margin:-1px;
    overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
  }
  /* 48.5% (was 55%), now 49.2%: the marquee moves up to free the lower
     third, so the paragraph → scroll-arrow → section hairline can breathe
     as one rhythm — nudged back down a touch since */
  .hero2-marquee{
    display:block; position:absolute; left:0; right:0; top:49.2%;
    transform:translateY(-50%); z-index:4; pointer-events:none;
  }
  /* top-left tag: chapter count + "Scroll" cue, sitting well clear of the
     marquee above it — same mono-chrome language as the case pages'
     01/…/02 chapter row and the projects carousel's index number */
  .hero2-tag{
    display:flex; align-items:center; gap:.55em;
    position:absolute; left:6vw; top:23%; z-index:4; pointer-events:none;
    font-family:var(--v4-mono),'Space Mono',monospace; font-size:.72rem;
    letter-spacing:.16em; text-transform:uppercase; color:#fff;
  }
  .hero2-tag .ht-dash{ opacity:.7; }
  /* pulled closer to "Scroll" specifically, without touching the other
     gaps in the row (flex gap is uniform otherwise) */
  .hero2-tag .ht-arrow{ width:14px; height:14px; margin-left:-.3em; }
  .h2m-row{ overflow:hidden; }
  .h2m-track{
    display:inline-flex; flex-wrap:nowrap; white-space:nowrap; will-change:transform;
    animation:h2m-scroll 18s linear infinite;
  }
  .h2m-rev .h2m-track{ animation-direction:reverse; }
  .h2m-track span{
    display:inline-block; padding-right:.32em;
    font-family:'Telegraf','Archivo',sans-serif; font-weight:400; text-transform:uppercase;
    font-size:25.5vw; line-height:.92; letter-spacing:-.015em; color:#fff;
  }
  /* middot between each repeat in the loop — otherwise back-to-back words
     (esp. "Disciplinary", which has no trailing dash of its own) run together */
  .h2m-track span::after{ content:'–'; padding-left:.32em; color:#fff; }
  @keyframes h2m-scroll{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }

  /* the intro paragraph, tucked close under the marquee. Centred with WIDE
     lines so the block reads as a rectangle, not a funnel; the first line is
     indented so it starts a touch in from the left, as in the reference. */
  /* was bottom:14.45vh — Geert wants the paragraph (and the scroll cue that
     follows it, see .hero-arrow below) sitting a touch lower */
  .hero2-side{
    /* was 10.8vh — down another small step */
    bottom:9.8vh; width:92vw; max-width:none;
    font-size:.92rem; line-height:1.62; letter-spacing:.05em;
    text-align:center; text-indent:2.6em; text-wrap:initial;
  }

  /* the scroll cue is a real target now, not a token. The painted ring is only
     54/64 of the svg box, so a 104px button paints an ~88px circle — the same
     diameter as the About "learn more" ring, and the two now read as one
     button scale. Sizes chosen so the ring lands optically halfway between the
     paragraph above and the section hairline below (≈62px of air either side);
     stroke widths are scaled back down so the line weight is unchanged. */
  /* NOTE — no env(safe-area-inset-bottom) here (it used to be in this offset).
     On a notched iPhone that term is up to 34px, so the button rendered that
     much HIGHER on the phone than in any desktop preview — which is why it kept
     reading as "too close to the text" on device while the measurements looked
     balanced. The button is not pinned to the physical screen edge (it lives
     inside a 100vh section and is judged mid-scroll), so it does not need the
     home-indicator inset. The negative offset carries the ring's optical centre
     to the midpoint between the paragraph and the ABOUT hairline; the ring's
     lowest ~8px therefore crosses the hero's bottom edge. */
  /* was -38px — moved less than the paragraph above, on purpose: that opens
     extra air between the paragraph and the ring instead of just following
     it 1:1 */
  /* dropped further still, on its own — more air above AND below the ring,
     without moving the paragraph or the hairline's own extra offset again */
  /* was -58px — Geert kept not seeing this move, so this time it's a real
     20px jump, not another 4-6px nudge */
  .hero-arrow{ width:104px; height:104px; bottom:-78px; }

  /* The air around the scroll button is bounded by two fixed things: the
     paragraph above it and the ABOUT hairline below it. The paragraph is meant
     to sit LOWER, which by itself eats that air — so the hairline of this one
     section is dropped 16px to pay for it. Scoped to #intro: every other
     section head keeps the shared offset, and the lead paragraph below still
     clears the row by ~50px. */
  /* pushed further than a 1:1 follow of the paragraph, so the ring also
     gains air on its lower side, not just its upper one */
  /* was +52px — same reasoning, a real 20px jump this time */
  #intro .page-head{ top:calc(clamp(3.4rem,6vh,5rem) + 72px); }
  .hero-arrow .ha-svg{ width:104px; height:104px; }
  /* the chevron itself shrinks inside the (unchanged) ring so the button reads
     as a ring with a mark in it, not a ring stuffed with an arrow. stroke-width
     is raised to 1.55 because the scale() thins the line by the same factor —
     net painted weight stays where it was. Safe to transform: the gsap
     cursor-drift that also writes .ha-arrow's transform is touch-gated off. */
  .hero-arrow .ha-arrow{
    stroke-width:1.55;
    transform-box:fill-box; transform-origin:50% 50%; transform:scale(.75);
  }
  .hero-arrow .ha-ring{ stroke-width:.8; }

  /* ---- SELECTED WORK head: the intro line gets the same treatment as the
     hero paragraph and the About copy — centred, running the full column
     width instead of stopping at 24ch, first line indented in. The head's
     own horizontal margin (which insets it to the thumbnail edges on
     desktop) is zeroed here, otherwise "full width" is 25px short. ---- */
  .work-head{ margin-left:0; margin-right:0; }
  .work-head .sec-intro{
    width:100%; max-width:none; margin:0;
    transform:none;
    /* TYPOGRAPHY REVERTED. This block briefly pinned the line to small
       uppercase Telegraf at .92rem and justified it, to make it match the
       hero paragraph and the About copy. Geert wants the display face and the
       big size back — it is a section opener, not run-of-copy, and at that
       size justify only opens ugly word-gaps anyway. So nothing typographic
       is set here any more: the face comes from `.work .sec-intro`
       (var(--font-display), normal) and the size from the mobile rule higher
       up in this block (clamp(1.5rem,6.4vw,2.1rem)). Only the LAYOUT stays —
       full column width instead of the 24ch cap. */
    text-align:left;
  }
  /* the view-all circle comes back down a step. margin-top drops it a hair
     further from the intro sentence above it (column-reverse head, so the
     sentence reads first) — a nudge, not a new gap */
  .work-viewall{ width:112px; height:112px; margin-right:5vw; margin-top:10px; }
  .work-viewall .wv-ring{ border-width:1.3px; }
  /* was .95rem — the label is one size too loud for a circle this size */
  .work-viewall .wv-label{ font-size:.85rem; }
  /* "The numbers behind the work" — was 1.5rem, way too loud for a kicker */
  /* was .85rem (a fix for it being too loud at 1.5rem) — Geert now wants it
     bigger again AND white, not the previous size or the previous tone */
  .proof-scope{ font-size:1.15rem; letter-spacing:.18em; color:#fff; }

  /* the section closed on 163px of dead space under the numbers (2.8rem on the
     strip + 14vh of section padding) before the next section added its own
     top padding on top of that — the gap to the BEYOND hairline was 217px */
  .work{ padding-bottom:4vh; }
  .work .more-proof{ margin-bottom:0; }
}

@media (max-width:760px) and (prefers-reduced-motion:reduce) and (min-width:99999px){
  .h2m-track{ animation:none; }
}

/* PHONE LANDSCAPE, narrow width (still the ≤760px "phone" path, e.g. an
   iPhone SE turned sideways at 667×375) — the marquee word-size (25.5vw) and
   every vh-based offset above assume a tall portrait viewport with plenty of
   vertical room. At a short height they all stack on top of each other:
   marquee, tag, paragraph and arrow overlap into an unreadable pile. Sized
   down and re-spaced by VH instead of VW here so it scales with the actual
   available height, same hero, same elements, just fitted to the shape of
   the screen. Portrait phones (the tuned, approved version) are untouched —
   this only fires when the height drops below what portrait ever reaches. */
@media (max-width:760px) and (max-height:520px){
  .hero2-marquee{ top:42%; }
  .h2m-track span{ font-size:13vh; }
  .hero2-tag{ top:26%; }
  .v4-logo .v4-row{ font-size:.92rem !important; }
  .hero2-side{ bottom:auto; top:64%; font-size:.72rem; line-height:1.4; }
  .hero-arrow{ bottom:2vh; width:64px; height:64px; }
  .hero-arrow .ha-svg{ width:60px; height:60px; }
}

/* =========================================================
   MOBILE-ONLY (≤760px) — Selected Work as a swipe carousel (Kalafatis-style).
   Overrides the ≤860px vertical stack (style.css ~2559) into a horizontal
   scroll-snap row with a peek of the next card. Phones only; tablets
   (761–860px) keep the stacked column. Reversible: delete this block.
   ========================================================= */
@media (max-width:760px){
  .work-row{
    flex-direction:row; flex-wrap:nowrap;
    /* asymmetric padding (was 0 11vw): nudges the row's resting position
       right, echoing the desktop head where the button sits left and the
       column of content sits right of it */
    gap:14px; margin:6vh 0 0; padding:0 8vw 0 14vw;
    overflow-x:auto; overflow-y:visible;
    scroll-snap-type:x mandatory;
    scroll-padding:0 8vw 0 14vw;
    -webkit-overflow-scrolling:touch;
    scrollbar-width:none;
  }
  .work-row::-webkit-scrollbar{ display:none; }
  /* bigger standing frame, properly rounded, and the caption moves OUT of the
     photo to sit under it (brand name, then what it is) — as in the reference.
     The card stops being the frame: the <img> is, so the card can grow past it
     to carry the two caption lines. */
  /* start-aligned snap with a narrower card, so a ~58px slice of the NEXT
     project always sits at the right edge — the row reads as "there is more
     here, swipe" instead of one card that happens to be centred */
  .work-row{
    align-items:flex-start; gap:12px;
    /* the row breaks out of .work's 5vw padding and scrolls edge to edge —
       inside that padding its viewport was only 351px, which left just 19px of
       the next card showing however narrow the cards got */
    /* 5vw gutter = the same left edge as the section copy and the view-all
       circle, so the cards line up with the rest of the column */
    /* THE GLOW NEEDS ROOM INSIDE THIS BOX. overflow-x:auto forces overflow-y to
       auto as well, so this row CLIPS — and it had exactly 0px of space above
       the photos. The backglow reaches ~49px past the frame, so its whole top
       half was being sliced off dead straight against the row's edge: a hard
       horizontal line across the top of every card, which is what makes a soft
       halo read as a square with a visible border.
       Room needed is NOT blur + spread. Measured against the engine's own blur
       (canvas shadowBlur uses the same Skia kernel as box-shadow): a blur of 92
       still paints alpha 6/255 at 92px out and only reaches a true zero at
       134px. So "blur + spread = 95px of room" was always ~40px short, which is
       why 6rem (96px) STILL left a faint band. Rather than open 9rem here — the
       negative top margin that pays for it already reaches up over the view-all
       circle — the fix is on the other side: the glow itself is tightened to a
       blur that dies inside this room (see the box-shadow below). Empirical
       rule for anything added here: a box-shadow fades out at ~1.46 × blur.
       6rem above and 1.8rem extra below, pulled back off the margins: the glow
       gets its room, nothing on the page moves.
       Left and right are deliberately left alone — the row spans the full
       viewport, so the clip there lands exactly on the screen edge, where
       there is nothing to see. */
    margin:calc(6vh - 6rem) -5vw -1.8rem; padding:6rem 5vw calc(.6rem + 1.8rem); scroll-padding:0 0 0 5vw;
  }
  .work-row .work-card{
    flex:0 0 86vw; width:86vw; max-width:none;
    aspect-ratio:auto; height:auto; max-height:none; min-height:0;
    /* the stagger offset (position:relative + top:6vh) pushed the card down out
       of the row's clip box — and overflow-x:auto forces overflow-y to auto, so
       the captions below the frame were being cut off. Sit flush instead. */
    top:0;
    display:flex; flex-direction:column;
    overflow:visible; background:none; border-radius:0; box-shadow:none;
    scroll-snap-align:start;
  }
  .work-row .work-card > img{
    /* the base rule pins this absolute+inset:0 to fill the card frame; here the
       image IS the frame and must be a real flex item, or the card collapses to
       the height of its captions and the photo escapes the layout */
    position:static; inset:auto;
    order:1; display:block; width:100%; height:auto;
    aspect-ratio:0.76; object-fit:cover;
    border-radius:28px;
    filter:saturate(.95) brightness(.95);
    /* THE BACKGLOW, as on desktop. There it is a hover state — the soft
       white-grey halo blooming past the frame into the dark when you point at
       a card. A phone has no hover, so it sits on permanently instead, and
       because it is always on it is dialled back from the desktop values
       (95/120px blur at .55/.20) — at this card size the full-strength halo
       spills onto the neighbouring card in the scroll row and the row starts
       to read as fog rather than as three lit frames. Lives on the IMG, not
       the card: here the image IS the frame (the card is overflow:visible
       with no radius), so the glow has to follow the 28px corners.
       Blur 58 (was 92 + 3 spread), so it fades to a real zero at ~85px — inside
       the 96px the scroll row can give it, with ~11px to spare. That is what
       removes the straight edge: the halo now ENDS before the clip instead of
       being cut off at 2% opacity. Alpha .18 → .23 keeps the same read close to
       the frame now that it carries less area. */
    box-shadow:
      0 26px 58px -38px rgba(33,26,20,.55),
      0 0 58px 0 rgba(236,234,230,.23);
  }
  /* NO transform on these images, in any state. On desktop the card clips its
     photo (overflow:hidden), so the hover zoom and the IKEA framing scale
     (`[data-case="ikea"] > img{ transform:scale(1.14) }`) stay inside the frame.
     Here the IMAGE IS the frame and the card is overflow:visible, so any scale
     spilled outside it: the IKEA thumbnail rendered 45px wider than its
     neighbours, lost its rounded corners off-screen, and covered the caption
     underneath it. iOS also keeps :hover stuck after a tap, so the hover zoom
     could freeze a card in that broken state. object-position keeps the IKEA
     crop instead — object-fit:cover already zooms it into the portrait frame. */
  .work-row .work-card > img,
  .work-row .work-card:hover > img,
  .work-row.is-hovering .work-card:not(:hover) > img{
    transform:none !important;
    filter:saturate(.95) brightness(.95);
  }
  .work-row .work-card[data-case="ikea"] > img{ object-position:13% 58%; }
  /* the in-frame scrim + hover chrome existed only to carry the overlay
     caption — with the caption outside the frame they'd just dirty the photo */
  .work-row .work-card::after, .work-row .work-card::before{ display:none !important; }
  .wk-d-cat{ display:none !important; }

  .wk-d-client, .wk-d-title{
    position:static; left:auto; bottom:auto; z-index:auto;
    display:block; text-align:left; text-shadow:none; max-width:none;
    /* inset from the photo's left edge instead of flush against it: dead-flush
       against a 28px rounded corner reads as a mistake, because the corner
       curves away above the text and the alignment never lands anywhere */
    padding-left:1.1rem;
  }
  /* was 1.05rem/.71rem — both up a step, Geert wants the caption block
     under the three landing thumbnails bigger as a whole */
  .wk-d-client{
    /* tucked up under the photo: was 1rem, which read as a stray line floating
       between two thumbnails rather than as this one's caption */
    order:2; margin-top:.55rem;
    font-family:var(--v4-gro); font-weight:500; text-transform:uppercase;
    font-size:1.3rem; line-height:1.2; letter-spacing:.01em; color:#f4ece0;
  }
  .wk-d-title{
    order:3; margin-top:.3rem;
    font-family:var(--v4-mono); font-weight:400; text-transform:uppercase;
    font-size:.88rem; line-height:1.4; letter-spacing:.13em; color:rgba(244,236,224,.55);
  }
}

/* INTRO (about teaser) — scoped to the same 960px point where the section
   stacks to one column: the desktop transforms/absolute marquee overlap the
   stacked layout anywhere below that, not just on phones */
@media (max-width:960px){
  .intro{ padding:clamp(9rem,17vh,11.5rem) 5vw 5vh; }   /* clears the page-head (photo sat 8px under it) */
  .intro-stage{ gap:2.1rem; padding-bottom:4vh; transform:none; }
  /* portrait again (the square-by-45vw math left a 323×186 letterbox strip) */
  .intro-photo{ width:min(78vw,340px); height:calc(min(78vw,340px) * 1.2); }
  .intro-lead{ transform:none; margin-left:0; max-width:32ch; }
  .intro-copy{ text-indent:0; font-size:.98rem; line-height:1.65; letter-spacing:.1em; }
  /* the desktop <br>s are hand-placed for a wide line — on a phone they leave
     an orphan "focused on" line; let it wrap naturally instead */
  .intro-copy br{ display:none; }
  .intro-more{ width:8.4rem; transform:none; justify-self:center; }
  .intro-more:hover{ transform:scale(1.06); }
  .intro-more.go{ transform:scale(.93); }
  /* the quote-marquee joins the flow below the circle (it floated over it) */
  .intro-name{ position:static; margin-top:5vh; line-height:1.1; padding-bottom:.14em; }
  .intro-name-track span{ font-size:clamp(2.6rem,14vw,4.2rem); }

  /* ---- THE QUOTE MARQUEE, MADE CHEAP ENOUGH FOR A PHONE ----
     It stuttered here, and every so often the line simply wasn't painted at
     all. The reason is layer size, not the animation: four copies of the
     sentence at 19vw is a ~4950px-wide promoted layer (will-change:transform
     + backface-hidden), and at a phone's DPR3 that is ~15000 device px of
     texture for ONE element — past what mobile GPUs will hand out, so the
     compositor either tiles it badly (the stutter) or drops it (the blank).
     On top of that every glyph is a background-clip:text gradient with two
     blurred shadows, i.e. a CPU raster of that whole strip.
     Three cuts, all invisible at phone scale:
       • two copies instead of four — the layer halves, and 0→-50% is still
         seamless (one copy already overruns the screen ~4x over);
       • solid white instead of the clipped gradient veil — the veil is a
         44%–56% band you cannot see at 74px anyway;
       • one modest shadow instead of two deep ones.
     Duration halves with the travel distance (-50% of two copies = one copy,
     where it used to be two), so the line moves at exactly the same speed. */
  .intro-name-track span:nth-child(n+3){ display:none; }
  .intro-name-track{ animation-duration:8s; }
  .intro-name-track span,
  .intro-name-track .qlight{
    background:none;
    -webkit-text-fill-color:#fff; color:#fff;
    text-shadow:0 3px 14px rgba(0,0,0,.5);
  }
  /* the lit word keeps its glow — it is the whole point of the line — but at
     a radius the phone can raster without breaking a sweat */
  .intro-name-track .qlight{
    text-shadow:0 3px 14px rgba(0,0,0,.5), 0 0 20px rgba(240,238,233,.55);
  }
  /* the anticipation flare pushed the glow to a 100px radius for 0.24s — on a
     phone that is a full CPU re-raster of the giant glyphs at exactly the
     moment the spark launches, i.e. a hitch on the one beat that has to land.
     Same gesture, radius the phone can afford. */
  /* the flare's peak was .95 white — on a phone that whited the word out for a
     beat, so the hand-over read as a flash rather than as light letting go.
     Pulled back to a lift above the word's own glow, not a blow-out. */
  @keyframes curioChargeM{
    0%,100%{ text-shadow:0 3px 14px rgba(0,0,0,.5), 0 0 20px rgba(240,238,233,.55); }
    55%    { text-shadow:0 3px 14px rgba(0,0,0,.5), 0 0 26px rgba(255,255,255,.7); }
  }
  .intro-name.light-charge .qlight{ animation:curioChargeM .24s ease-out; }
}

/* =========================================================
   MOBILE-ONLY (≤760px) — ABOUT/INTRO teaser recomposed:
   copy ABOVE the portrait, the quote-marquee riding over the photo's lower
   edge, and the Learn-more circle last. display:contents lifts the stage's
   children into .intro so all four can be ordered against each other.
   Must sit AFTER the ≤960px block above, which sets competing values.
   ========================================================= */
@media (max-width:760px){
  /* padding-bottom drops to 0 (the ≤960 block sets 5vh): the section is
     min-height:100vh anyway, and the freed room is spent on the air around
     the learn-more circle instead of dead space under it */
  /* padding-top up from the inherited clamp(9rem,17vh,11.5rem): more air
     between the "About" chapter head and "Hi, I'm Geert" below it */
  /* one more step (was clamp(10.6rem,19.4vh,13.1rem)): the head row + its
     hairline sit absolute at the section top, so this padding IS the gap
     down to the copy — ~42px read too tight, this puts it near 68px */
  .intro{ display:flex; flex-direction:column; align-items:center; gap:2.4rem; padding-top:clamp(12.2rem,22.3vh,14.7rem); padding-bottom:0; }
  /* the base rule's overflow:hidden clipped the Learn-more circle's BOTTOM: the
     button ends flush with the section edge, so the ivory fill lost its last
     pixel row (a flat cut) and the hover ring + click ping were sliced off.
     overflow-x:clip keeps the marquee's sideways bleed contained while letting
     the y axis paint freely — hidden would have forced y to clip as well. */
  .intro{ overflow:visible; overflow-x:clip; }
  .intro-stage{ display:contents; }
  .intro-lead{ order:1; max-width:none; width:100%; }
  .intro-photo{ order:2; }
  .intro-name{ order:3; }
  .intro-more{ order:4; }

  /* the portrait runs near-full-bleed (78vw → 90vw, ~20px gutters) so it has
     the same presence as the work cards; the ratio flattens from 1.2 to 1.06
     so the extra width doesn't push the marquee + button out of the screen */
  .intro-photo{ width:min(90vw,420px); height:calc(min(90vw,420px) * 1.06); }

  /* was centred — Geert wants an actual rectangle: every line but the last
     stretches edge to edge (justify), first line indented in like a
     paragraph opener, last line left so it doesn't stretch three words
     across the column */
  /* was matched to .sec-intro's own size — Geert actually wants it matched
     to the HERO paragraph (.hero2-side) instead: same font-size/line-height/
     letter-spacing as "I shape research…", so all three of these run-of-copy
     blocks read as one family. Width is 100% (was 92vw) so the block truly
     runs edge to edge; margin opens up on both sides for real air above and
     below, not just above. */
  .intro-copy{
    /* was .92rem (14.7px), matched to the hero paragraph. Sized by measurement
       instead, because ragged-right only looks "full" at the size where the
       words happen to land near the edge. Every half-step from 13 to 20px was
       measured line by line at this column width; 17px is the winner by a
       clear margin — its lines fill 94% / 100% / 91% of the column, against
       81% / 83% at .92rem. That is the "almost hits the right border" without
       stretching a single space.
       MAAR: 1.06rem is een VASTE maat in een kolom die met de vw meebeweegt, dus
       hij klopt maar op één schermbreedte. Op een echte telefoon viel "animation"
       door naar een vierde regel — een losse staart van 31% onder een volle
       alinea. Het omslagpunt is gemeten (binair gezocht) op 364/377/391/427 px
       breed en ligt verrassend stabiel op 3.84–3.91vw: precies omdat de kolom
       zelf ~88% van de vw is. Dus vw i.p.v. rem, met 3.7vw net onder de laagst
       gemeten drempel (3.84) zodat font-metrics-verschillen tussen Safari en
       Chrome er niet doorheen breken. Drie regels op elke telefoonbreedte, en de
       bovengrens 1.06rem houdt de oude maat zodra de kolom breed genoeg is dat
       vw hem zou laten doorgroeien. */
    font-size:clamp(.74rem, 3.7vw, 1.06rem); line-height:1.62; letter-spacing:.05em;
    /* NO LONGER JUSTIFIED. Justify buys a straight right edge by paying for it
       in word-gaps — and at this measure (~40 characters) it was paying a lot:
       the spaces stretched far enough that the line read as spaced-out words
       rather than a sentence. Left-aligned instead, but hyphens:auto stays,
       and that is what keeps the block looking full: the browser may break
       inside a word, so lines still land within a few percent of the right
       edge instead of ending wherever the last whole word happened to fit.
       Same straight-ish rectangle, normal spacing between the words. */
    text-align:left; text-indent:2.6em; hyphens:auto;
    /* top/bottom no longer equal: was 4vh both — the gap down to the photo
       needs to be tighter than the gap up to the hairline */
    width:100%; max-width:none; margin:4vh 0 2vh;
  }

  /* the marquee grows and rides up over the photo's lower edge */
  /* was -8vh / 0: the line moves up another 4vh so it overlaps the portrait
     deeper. The bottom margin was 4vh (put there to hold the Learn-more circle
     in place while the line rose) — halved to 2vh, which is the "less air
     between the marquee and the button" step: the whole gap is this margin +
     the section's 2.4rem flex gap + the button's own top margin. */
  .intro-name{
    position:relative; z-index:2; width:100%;
    margin-top:-12vh; margin-bottom:2vh;
  }
  /* was clamp(3.4rem,19vw,6rem) — Geert wants the quote marquee louder */
  .intro-name-track span{ font-size:clamp(4.1rem,23vw,7.2rem); }

  /* same painted diameter as the hero scroll-arrow ring (88px) — one circle-
     button scale across the page. The 1rem that used to drop it further from
     the marquee is gone: that gap is what Geert wanted tighter. */
  .intro-more{ width:108px; margin-top:0; }
  /* the ring is exactly the hero arrow's painted diameter (88px), but a 2px
     border against the arrow's ~1.3px stroke made the same circle read heavier
     and therefore differently sized — match the weight and they read as one
     button. */
  .intro-more-ring{ border-width:1.4px; }
  /* the desktop label is a deliberate diagonal (learn up-left, more down-right);
     at phone scale that just reads as a mis-set label — stack it dead centre.
     Smaller too: the chevron opposite it is now a small mark in a big ring, so
     a label filling its ring broke the pair. margin-left compensates the
     trailing letter-space, which otherwise pulls each line a hair off-centre. */
  .intro-more-label{
    font-size:.89rem; letter-spacing:.1em; line-height:1.22;
    text-align:center; margin-left:.05em;
  }
  .intro-more-label .iml-1{ transform:none; }
  .intro-more-label .iml-2{ transform:none; margin-top:.1em; }
}

@media (max-width:600px){
  /* the 3:2 landscape frame can't hold title + line + button on a phone —
     go portrait (4:5) so the photo keeps real presence above the text block */
  .more-panel{ aspect-ratio:4/5; }
}

/* touch: the blackout note can't follow a cursor — show it in full */
@media (hover:none){
  .blackout-msg{ -webkit-mask-image:none; mask-image:none; }
  .blackout.on, .blackout.on *{ cursor:auto; }
}

/* =========================================================
   TOUCH: no hover chrome on the project cards.
   Dragging a finger through the Selected Work row fires the emulated
   mouseenter/:hover on whatever card it passes, so that card lit up with the
   desktop backlight halo (a 150px white bloom) while the row dimmed the
   others — mid-swipe it read as if you had selected a project. iOS also keeps
   :hover latched on the last touched element until you tap elsewhere, so it
   stayed lit. main.js already skips the .is-hovering wiring on coarse
   pointers; this kills the pure-CSS half, which JS cannot prevent.
   Last in the file so it wins over the earlier :hover rules.
   ========================================================= */
@media (hover:none){
  .work-card:hover, .more-panel:hover, .work-row .work-card:hover{
    box-shadow:none; --hy:0; --hs:1;
  }
  .work-card:hover > img, .more-panel:hover > img{
    transform:none; filter:saturate(.85);
  }
  /* the row's own thumbnails keep the muted mobile grade at all times */
  .work-row .work-card:hover > img{ filter:saturate(.95) brightness(.95); }
  .work-row.is-hovering .wk-feature,
  .work-row.is-hovering .wk-meta{ opacity:0 !important; }
  .work-row.is-hovering .work-card:not(:hover)::after{ opacity:0 !important; }
  .work-card:hover .card-cta, .more-panel:hover .panel-cta,
  .work-card:hover .ig-reel{ opacity:inherit; }
}

/* TOUCH: "next project" needed two taps. iOS Safari fakes a :hover on the
   first tap of anything with hover styles (mouseenter fires, click doesn't)
   and only fires the real click on the second — same root cause as the
   work-card glow above, just on .case-next instead. Resetting every hover
   rule back to its base value here makes hover a no-op on touch, so the
   very first tap registers as the click. */
@media (hover:none){
  .case-next:hover strong{ color:var(--ivory); }
  .case-next:hover .cn-img img{ transform:none; }
  .case-next:hover .cn-box{ border-color:rgba(247, 243, 236, 0.18); background:rgba(247, 243, 236, 0.02); transform:none; }
  .case-next.cn-framed:hover .cn-box{
    transform:none; box-shadow:0 34px 90px -20px rgba(0, 0, 0, 0.55); filter:none;
    background:radial-gradient(120% 110% at 50% 18%, #f4f1ea 0%, #e6e3db 55%, #d2cfc7 100%);
  }
  .case-next.cn-framed:hover .cn-box::before{ width:38px; }
  .case-next.cn-framed:hover .cn-box::after{ opacity:0; transform:skewX(-14deg) translateX(-140%); }
  .case-next.cn-framed:hover strong{ color:#1b1815 !important; }
  .case-next.cn-framed:hover .cn-arrow{ transform:none; }
}


/* =========================================================
   PROJECTS PAGE — the vertical project carousel (projects.html).
   Layout modelled shot-for-shot on the reference recording: a fixed
   stage, one project per screen. Centred rounded media card with a
   faint reflection pooling beneath it; the huge title crossing the
   card's lower edge; twin mono meta columns under the card; index +
   hairline on the left; a tick ruler on the right. All placement
   maths and the organic motion live in projects.js — the CSS only
   sets the resting stage.
   ========================================================= */
body.projects-page{
  background:#070708; height:100dvh; overflow:hidden;
  overscroll-behavior:none;
}
body.projects-page #aerial-bg{ position:fixed; inset:0; z-index:0; }

.pc-stage{ position:fixed; inset:0; z-index:2; }
/* every item is a full-screen layer and they all STACK — the layers must
   let clicks fall through, or the last one (dafi) eats every click no
   matter which frame you aim at; only the card itself is a click target */
.pc-item{ position:absolute; inset:0; pointer-events:none; }

/* the media card — big like the reference (~56vw wide, over half the
   screen tall), soft corners, a quiet hairline edge so dark covers
   still read against the dark. projects.js adds the travel to THIS
   element (not the item), so the title clip below can stay put. */
.pc-card{
  position:absolute; left:50%; top:48%;
  width:min(58vw,106rem); aspect-ratio:1.68;
  transform:translate(-50%,-50%);
  border-radius:20px; overflow:hidden;   /* fallback frame; JS lifts overflow and clips via path() */
  /* drop-shadow, NOT box-shadow: the filter follows the live clip-path,
     so the shadow bows along with the frame */
  filter:drop-shadow(0 34px 40px rgba(0,0,0,.5));
  cursor:pointer; pointer-events:auto;   /* re-enable under the transparent item layer */
  will-change:transform, clip-path, filter;
  /* NO -webkit-box-reflect here: reflect + a live clip-path breaks the
     clipping in Blink — the card hard-cuts to its unclipped box */
}
/* hairline ring as a CHILD (a box-shadow ring would be clipped away, a
   straight one would contradict the bow) — JS fades it out in flight */
.pc-ring{
  position:absolute; inset:0; z-index:2; pointer-events:none;
  border:1px solid rgba(247,243,236,.07); border-radius:inherit;
}
/* the photo BLEEDS 30px past the frame top and bottom: when the frame's
   edge bows upward mid-scroll, real pixels are revealed, not emptiness.
   The clip-path crops the bleed back at rest. The counter-scale against
   the squash (projects.js) keeps the photo itself undeformed always. */
.pc-card img{
  position:absolute; left:0; top:-30px; width:100%; height:calc(100% + 60px);
  object-fit:cover; display:block; will-change:transform;
}
/* reduced-motion column: no JS clip-path runs, so no bleed either */
.pc-static .pc-card img{ position:static; height:100%; }
/* the phone title's scrim — a REAL element (projects.js builds it) rather
   than a ::after, so it can take the photo's own counter-scale and stay
   welded to the picture through the squash. Desktop has no scrim at all. */
.pc-scrim{ display:none; }

/* the title — HUGE, left-aligned off the screen edge (right after the
   index number), overlapping the card's lower-left like the reference.
   The clip box never moves; the h2 inside ROLLS vertically through it
   (the same move as the nav pills), scrubbed by the spring. */
.pc-tclip{
  /* the max() keeps the fixed-size index number clear of the title
     when the window narrows (mono text doesn't scale with vw).
     px floors, NOT rem: below 1024px the rem canvas snaps to 16px
     and rem floors would balloon the title off the screen. */
  position:absolute; left:max(12.8vw,150px); top:calc(48% + 7.9vw);
  transform:translateY(-50%);
  overflow:hidden; height:1.04em;
  /* dialled down from the original 9.2vw/15rem — short titles (IKEA, Dafi)
     read as huge without swallowing the whole screen in white type */
  font-size:clamp(40px,8.2vw,12.5rem);
  z-index:3; pointer-events:none;
  /* readability shadow lives HERE, not on the h2: drop-shadow is
     computed AFTER the overflow clip, so it hugs exactly the visible
     letterforms at all times — a text-shadow on the h2 would paint
     into the clip box and get sheared off at its straight edges,
     reading as an ugly shadow block while the title rolls */
  filter:drop-shadow(0 4px 22px rgba(0,0,0,.42));
}
.pc-title{
  margin:0;
  font-family:var(--v4-gro); font-weight:500;
  font-size:inherit; line-height:1.04; letter-spacing:-.02em;
  text-transform:uppercase; white-space:nowrap; color:#fff;
  will-change:transform, filter;
}

/* single-line mono meta under the card — tiny, tracked, quiet. Was two
   stacked columns (name+tag / title+"View case"); the whole card is
   already the click target, so the CTA text is gone and what's left
   reads as one row: [project title] · [discipline tag]. */
.pc-meta{
  /* right under the card's bottom edge (48% + half of 58vw/1.68) */
  position:absolute; left:50%; top:calc(48% + 17.3vw + 1rem);
  transform:translateX(-50%); margin:0; max-width:90vw;
  font-family:var(--v4-mono),'Space Mono',monospace;
  /* reference size: ~0.7% of the viewport width, never mini
     (px floor — see the .pc-tclip note on the sub-1024 rem snap) */
  font-size:clamp(10px,.75vw,1.35rem); line-height:1.5; letter-spacing:.12em;
  text-transform:uppercase; color:#fff;
  text-align:center; white-space:nowrap;
  z-index:3; pointer-events:none;
}
.pc-dot{ margin:0 .85em; color:rgba(255,255,255,.4); }

/* ---- carousel-hover: trage lens-zoom op de foto (de titel rolt al door de
   scroll-spring, dus verder niets). Alleen op echte hover-devices en zonder
   reduced-motion; de kaart is het hovervlak. Zoom via de aparte
   scale-property: componeert met de per-frame transform (scaleY-counter)
   die projects.js op de img schrijft. ---- */
@media (hover:hover){
  .pc-card img{ transition:scale .7s cubic-bezier(.22,.61,.21,1); }
  .pc-card:hover img{ scale:1.03; }
  /* backglow als op de landing-workcards, maar via een tweede drop-shadow:
     een box-shadow zou door de live clip-path worden weggeknipt. De
     filter-transition laat hem bloeien; tijdens de vlucht parkeert
     projects.js de filter inline ('none'), dat wint van dit alles. */
  .pc-card{ transition:filter .55s cubic-bezier(.22,1,.36,1); }
  .pc-card:hover{
    filter:drop-shadow(0 34px 40px rgba(0,0,0,.5))
           drop-shadow(0 0 70px rgba(236,234,230,.18));
  }
}

/* left chrome: the rolling index sitting ON a hairline that runs in
   from the screen edge — number above, line below (reference layout) */
.pc-index{
  position:fixed; left:0; top:calc(48% + 7.9vw); transform:translateY(-50%);
  display:flex; flex-direction:column; align-items:flex-start; gap:1.1rem;
  z-index:4; pointer-events:none;
}
/* the rule STOPS short of the title's left edge (12.8vw) — it must
   never run under the letters */
.pc-rule{ width:9.5vw; height:2px; background:#fff; }
.pc-num{
  display:block; overflow:hidden; padding-left:3.2vw;
  font-family:var(--v4-mono),'Space Mono',monospace;
  /* scales with the viewport so it always clears the title's left edge.
     was clamp(1.1rem,1.35vw,1.7rem) — one small step down */
  font-size:clamp(1rem,1.22vw,1.55rem); letter-spacing:.2em; color:#fff;
}
.pc-num-roll{ display:block; }

/* right chrome: one fine tick per project, the active one stretched */
.pc-ticks{
  /* well clear of the screen edge, like the reference — the group
     floats between the card and the edge, not glued to the frame */
  position:fixed; right:9vw; top:50%; transform:translateY(-50%);
  display:flex; flex-direction:column; align-items:flex-end; gap:9px;
  z-index:4;
}
.pc-tick{
  width:26px; height:3px; border-radius:99px;
  background:rgba(247,243,236,.26);
  cursor:pointer; border:0;
  /* NO transition: projects.js scrubs width + colour per frame — a
     continuous light band that travels with the scroll, not a toggle */
}
.pc-tick.on{ width:48px; background-color:rgba(247,243,236,.9); }

/* ---------- reduced motion: a plain scrollable column ---------- */
body.pc-static{ overflow:auto; height:auto; }
body.pc-static .pc-stage{ position:static; }
body.pc-static .pc-item{ position:relative; inset:auto; height:100vh; }
body.pc-static .pc-card{ cursor:pointer; }
body.pc-static .pc-tclip{ transform:translateY(-50%); height:auto; overflow:visible; }
body.pc-static .pc-title{ transform:none !important; }
body.pc-static .pc-ticks{ display:none; }

/* ---------- PHONES (≤760px): the carousel, recomposed vertically ----------
   The desktop carousel is a LANDSCAPE frame (1.68) with its chrome parked in
   the margins around it: the rolling index to its left, the tick rail to its
   right, the huge title lying across its lower-left corner. A phone has no
   margins — so at 88vw that frame was a 343×204 letterbox stranded in the
   middle of a black screen, with the index number, the title and the tick
   rail all sitting ON the photo, and the next project poking up into the
   caption from below.

   Same page, restacked — the frame stands up and the chrome moves ONTO the
   picture (where there is room on a phone) instead of into margins that do
   not exist:

       ┌──────────────────┐
       │ 00.01 ———        │        ← index, top-left, on the photo
       │                  │  ▬▬    ← ticks, right-aligned, mid-height,
       │                  │   ─      on the photo
       │ SPOORWEGMUSEUM   │        ← the client title lies on the photo
       └──────────────────┘
       SCAVENGER HUNT DESIGN · RESEARCH & DESIGN   ← under it, left-aligned

       ┌──────────────────┐        ← and the next project's top edge shows,
                                     which is what invites the scroll

   Everything the desktop page carries is still here and still animated the
   same way (the title still rolls through its clip, the ticks are still a
   travelling light band, the frame still bows and squashes) — only the
   geometry changed. Every piece of chrome hangs off the SAME two variables,
   so the whole composition moves as one.
   --pc-half must always equal (card width ÷ aspect-ratio) ÷ 2. */
@media (max-width:760px){
  :root{
    /* was 45dvh: Geert wants the card you land on, at rest, sitting a bit
       lower on the screen — every other piece of chrome (index, ticks,
       title, caption) is positioned off this one variable, so raising it
       carries the whole composition down together, not just the card. */
    --pc-top:48dvh;      /* the card's centre line */
    --pc-half:47.4vw;    /* = 90vw / .95 / 2 */
    --pc-gut:5vw;        /* the page gutter the frame's edges sit on */
    --pc-inset:.9rem;    /* how far the on-photo chrome sits inside the frame */
  }
  /* 86vw → 90vw. The frame is the only thing on the screen, and every px of
     width it gains goes straight into the SHARED title size (projects.js
     fits all six to the longest one), which is the scarce resource here.
     border-radius is deliberately not set — projects.js writes it every
     frame (it blooms with the squash) and the clip-path follows it. */
  .pc-card{ width:90vw; aspect-ratio:.95; top:var(--pc-top); }

  /* THE TITLE'S FLOOR — see the .pc-scrim note in projects.js for why this is
     a real element that rides the PHOTO's transform rather than a ::after on
     the card. Its box is the photo's box, bleed included: the frame is inset
     30px top and bottom inside it, which is what the 6%/94% shoulders below
     are compensating for, so the stops land where the visible edges are. */
  .pc-scrim{
    display:block; position:absolute; left:0; top:-30px;
    width:100%; height:calc(100% + 60px);
    z-index:1; pointer-events:none;
    background:
      linear-gradient(to top,
        rgba(6,6,7,.9) 0%, rgba(6,6,7,.86) 7%, rgba(6,6,7,.55) 24%,
        rgba(6,6,7,.18) 38%, transparent 52%),
      linear-gradient(to bottom,
        rgba(6,6,7,.55) 0%, rgba(6,6,7,.48) 7%, transparent 18%);
  }
  /* reduced motion: no bleed on the photo either, so the scrim loses it too */
  .pc-static .pc-scrim{ top:0; height:100%; }

  /* the ticks sit mid-height, where no scrim reaches them, and half of each
     bar hangs off the frame onto the page — a cover can be bright white right
     there (the IKEA key visual, the Treinsafari sky). A hairline of shade
     under the painted bar keeps every mark legible against either. */
  .pc-index, .pc-ticks{ filter:drop-shadow(0 1px 4px rgba(0,0,0,.8)); }

  /* index: top-left corner of the photo, number then rule on one line. The
     number carries the count and had shrunk to a footnote — back up to a
     size that holds its own against the title in the opposite corner. */
  .pc-index{
    left:0; top:calc(var(--pc-top) - var(--pc-half) + var(--pc-inset));
    transform:none; flex-direction:row; align-items:center; gap:.75rem;
  }
  /* was .95rem, then 1.2rem (up a step) — back down a hair, it was shouting */
  .pc-num{ padding-left:calc(var(--pc-gut) + var(--pc-inset)); font-size:1.08rem; letter-spacing:.16em; }
  .pc-rule{ width:12vw; }

  /* ticks: short rounded BARS straddling the frame's right edge — most of
     each one on the photo, a consistent 6px hanging off it into the page. The
     overhang is what makes them read as attached to the thumbnail instead of
     floating on it. align-items:flex-end pins the right end, so the active
     bar grows leftward INTO the picture and the overhang never changes. */
  .pc-ticks{
    left:auto; bottom:auto;
    right:calc(var(--pc-gut) - 6px); top:var(--pc-top);
    transform:translateY(-50%);
    flex-direction:column; align-items:flex-end; gap:9px;
  }
  .pc-tick{ width:13px; }
  .pc-tick.on{ width:26px; }

  /* the client title sits on the photo's lower edge. translateY(-100%) — NOT
     the desktop -50% — anchors the clip by its BOTTOM, so the title always
     rests on the same line above the frame's edge whatever its own height.
     The bottom inset is deliberately larger than the side inset (1.5 vs 1rem):
     optically, type reads as centred in a corner when it sits slightly
     further from the edge it stands on than from the one it starts at. */
  .pc-tclip{
    left:calc(var(--pc-gut) + var(--pc-inset));
    top:calc(var(--pc-top) + var(--pc-half) - 1.5rem);
    transform:translateY(-100%);
    font-size:min(13.5vw,4.8rem); letter-spacing:-.04em;
    filter:drop-shadow(0 4px 18px rgba(0,0,0,.65));
    /* TWO LINES HIGH — this is what buys the size. One shared size for all six
       (see fitTitles in projects.js) was being dictated by the longest title
       fitting on ONE line, so "Spoorwegmuseum" held every other title down to
       ~30px. Let the two long ones break instead and the shared size is set by
       the widest WORD, not the widest string: ~43px, and they stay uniform.
       The box is always two lines, whatever a title's own line count — see the
       .pc-title height below for why that matters. */
    height:2.08em;
  }
  .pc-title{
    /* wrap, but only where the markup allows it: an ordinary space, or the
       &shy; written into "Spoorweg&shy;museum". "& Film" is welded with an
       nbsp, so it travels as one piece. */
    white-space:normal; hyphens:manual;
    /* EVERY h2 is exactly two lines tall, with a short one (IKEA, Dafi) resting
       on the BOTTOM line. Not cosmetic: the roll in projects.js translates each
       title by a percentage of its OWN height, so a one-line title next to a
       two-line one would travel half as far and the two would drift apart
       mid-scroll. Equal heights keep the roll uniform by construction. */
    height:2.08em; display:flex; flex-direction:column; justify-content:flex-end;
  }

  /* meta: under the photo, but on the TITLE's rail rather than the frame's
     outer edge — it belongs to the title, not to the picture — and close
     enough under the frame to read as its caption instead of a stray line.
     (projects.js reads --pc-mx for the horizontal half of its per-frame
     transform, so the stylesheet can move it without fighting the loop.) */
  .pc-meta{
    --pc-mx:0;
    left:calc(var(--pc-gut) + var(--pc-inset));
    /* was + .85rem: sat as a stray line under the frame rather than reading
       as the thumbnail's caption. Tucked to .4rem and bumped a step bigger
       (.63rem → .72rem) so it holds its own against the title above it. */
    top:calc(var(--pc-top) + var(--pc-half) + .4rem);
    max-width:90vw; text-align:left; white-space:normal;
    font-size:.72rem; letter-spacing:.12em;
  }
  .pc-dot{ margin:0 .6em; }
  .pc-cursor{ display:none; }
}

/* taller phones have room for a properly standing frame — the caption is a
   single line whatever the height, so everything the extra viewport gives
   goes straight into the picture. Same variables, so the stack follows. */
@media (max-width:760px) and (min-height:780px){
  :root{ --pc-half:57.7vw; }        /* = 90vw / .78 / 2 */
  .pc-card{ aspect-ratio:.78; }
}

/* ============================================================
   MOBILE PASS v2 (13 jul 2026) — senior-UX audit layer, appended
   last so it wins every earlier mobile rule. Desktop untouched:
   everything lives inside max-width media queries.
   ============================================================ */

@media (max-width:860px){

  /* ---- OVERLAY CLOSE = BOTTOM-CENTRE, ALWAYS ON ----
     Used to live top-right permanently (it sat ON the running text
     otherwise) — moved bottom-centre like desktop. It no longer scrubs with
     scroll (hidden on the cover, revealed once you scrolled in) — it's
     permanently visible once a case is open, cover included. But "open" is
     not instant either: it fades + rises in on a short delay after the case
     opens (body.case-open, set by main.js openCase), and fades + settles
     back out immediately when it closes — so it never just pops on screen
     the moment you tap a project. */
  .case-overlay .case-close{
    opacity: 0;
    transform: translate(-50%, 10px) scale(.9);
    transition: opacity .3s ease, transform .3s cubic-bezier(.4,0,.2,1);
  }
  body.case-open .case-overlay .case-close{
    opacity: 1;
    transform: translate(-50%, 0) scale(1);
    transition: opacity .5s ease .3s, transform .5s cubic-bezier(.22,1,.36,1) .3s;
  }
  /* was pinned top-right as a solid gold disc — that read as a second accent
     floating over the panel. Same seat and same treatment as the case pages'
     close now: bottom-centre, thumb height, and an UNFILLED dot (hairline ring
     + gold ×) so it veils as little of the journey as possible. */
  .bio-pop .case-close{
    position:fixed; top:auto; left:50%; right:auto;
    bottom:calc(.7rem + env(safe-area-inset-bottom));
    transform:translateX(-50%);
  }
  /* the dot can sit over a bright cover photo now — a touch of edge so
     it always reads as a control, whatever is behind it */
  .case-overlay .cc-dot{
    box-shadow:0 10px 28px -10px rgba(0,0,0,.75), inset 0 0 0 1px rgba(255,240,214,.22);
  }
  .bio-pop .cc-dot{
    background:transparent;
    box-shadow:0 10px 26px -14px rgba(0,0,0,.5), inset 0 0 0 1px rgba(255,255,255,.52);
  }
  .bio-pop .cc-x::before, .bio-pop .cc-x::after{ background:#f6d98a; }

  /* ---- CASE HEAD: the title block earns its scrim ----
     At 48vh the info block rides up into the bright middle of the cover;
     the desktop gradient stopped too early and the gold meta drowned. */
  .case-head-shade{
    background:linear-gradient(to top,
      rgba(16,16,19,.96) 0%, rgba(16,16,19,.86) 16%, rgba(16,16,19,.58) 38%,
      rgba(16,16,19,.22) 62%, transparent 84%);
  }
  /* balanced line breaks for the wrapped cover titles (no lone "Design —") */
  .case-head-info h2{ text-wrap:balance; }

  /* ---- SELECTED WORK: read first, act second ----
     DOM puts the "View all projects" circle before the section intro, so
     the stacked column led with a CTA before any context — flip the
     column so the sentence lands first and the circle follows it. */
  .work-head{ flex-direction:column-reverse; }

  /* ---- FINALE ----
     The seeking-pill wrapped to two centred lines while the green dot
     stayed glued to line one's far left — let the dot travel inline with
     the first word instead of hanging detached. */
  .connect-kicker{ display:block; text-align:center; text-wrap:balance; }
  .connect-kicker .ca-dot{
    display:inline-block; vertical-align:middle; margin-right:.55em;
    margin-top:-2px;
  }

}

/* projects carousel — phone ergonomics */
@media (max-width:760px){
  /* the tick ruler: 18×3px was an impossible touch target — grow the hit
     area to ~15px rows while the painted line stays 3px (content-box) */
  .pc-tick{
    height:15px; padding:6px 0; box-sizing:border-box;
    background-clip:content-box;
  }
  .pc-tick.on{ background-clip:content-box; }
}

/* ============================================================
   CURSOR SAFETY NET — every clickable thing shows the hand.
   Many clickables here are non-semantic (an <article class="work-card">,
   a <span> skill line, the logo <div>) wired up in JS, so it's easy for one
   to ship without cursor:pointer. This guarantees the pointer on all of them
   in one place. Scoped off the projects page, which deliberately runs its own
   custom ✦ cursor (cursor:none on .pc-card). Deliberate non-pointer cursors —
   grab on the draggable filmstrip / bio-strip, default on an empty book page —
   are simply not listed, so they keep their own cursor.
   ============================================================ */
body:not(.projects-page) a[href],
body:not(.projects-page) button,
body:not(.projects-page) [role="button"],
body:not(.projects-page) [data-case],
body:not(.projects-page) [data-section],
body:not(.projects-page) [data-next],
body:not(.projects-page) [data-target],
body:not(.projects-page) .work-card,
body:not(.projects-page) .more-panel,
body:not(.projects-page) .absw-word,
body:not(.projects-page) .v4-logo,
body:not(.projects-page) .pill-gd,
body:not(.projects-page) .hero2-pill,
body:not(.projects-page) .wk-viewall {
  cursor: pointer;
}
/* SAFARI, DE ROL-KNOPPEN. Gemeten in Safari: de knop krijgt de hover wél (de
   balk rolt op, de tekst verkleurt), dus de hit-test bereikt de <a> gewoon —
   alleen de cursor-vorm blijft de pijl. Het is dus geen kwestie van iets dat
   eroverheen ligt, en ook niet van de np-spans (die uit de hit-test halen
   veranderde niets, dat is teruggedraaid).
   Wat overblijft is de cascade: als er in WebKit ergens op deze knop of op een
   kind alsnog `cursor:auto` wint, zie je precies dit. Deze regel sluit dat af
   voor de knop én al zijn kinderen tegelijk. In Blink verandert er niets — daar
   was de computed waarde al pointer, dus dit is puur een dichttimmering. */
.pill-nav a, .pill-nav a *,
.connect-linkedin, .connect-linkedin * { cursor: pointer !important; }

/* Minor case: this used to carry a whole phone-only exception — the flanking
   01/ and /03 hidden, the row at 1.05rem instead of .72rem, the name centred
   and wrapping — because its chapter names read "Chapter 01 · The Photo Book"
   and were twice as long as every other case's ("Overview", "Klassiekers"),
   with the number stated twice on one line.
   The names are plain now ("The Photo Book", "In the Field", "Film"), so the
   exception is gone with them: minor runs the exact same chapter row as every
   other case — number left, name centred, total right, one size. */

/* =========================================================
   MOBILE (≤760px) — CASE PAGES: phone typography + rhythm.
   The case pages were still running desktop editorial values on a phone:
   body copy at 27px (10 lines for one paragraph), section gaps of 16vh,
   and a chapter row whose centred name was pinned at left:29% — on a 390px
   screen that put "OVERVIEW" straight on top of the "01/" beside it
   ("010VERVIEW"). Everything here is scale + rhythm; no structure changes,
   so desktop is untouched.
   ========================================================= */
@media (max-width:760px){
  /* ---- COVER ----
     .cm-role sits outside any media query at clamp(1.08rem,…), so the gold
     kicker rendered at 17px + .2em tracking and wrapped to THREE lines across
     the middle of the photo. It is a credit line, not a headline. The flex
     column also still packed to flex-end (a desktop right-alignment) inside a
     block the mobile pass had centred, so its lines hung off-axis. */
  /* was align-items:center — Geert wants the credit line right-aligned like
     the rest of the header block (the title above it, the desktop base rule)
     instead of centred on its own axis */
  .ts-style .case-head-info .case-meta{ align-items:flex-end; gap:.35rem; margin-right:0; }
  /* SMALLER AND TIGHTER (Geert, aug): at .74rem/.16em the credit line
     ("Spoorwegmuseum · Visual Designer · Research & Experience Design", the
     longest of the six) still ran to three loose lines above the title and
     read as a second headline rather than a credit. Down a step and pulled
     in: the tracking is what was really eating the width, and a max-width in
     characters makes every case break on the dots instead of mid-phrase, so
     the block is a compact two lines on all six. */
  .ts-style .cm-role{ font-size:.58rem; letter-spacing:.1em; line-height:1.5; }
  .ts-style .cm-org{ font-size:.55rem; letter-spacing:.1em; }

  /* ---- THE COVER IS RESTRUCTURED ON A PHONE ----
     Desktop lays the title over a tall cover, where there is room for type to
     sit on photo. At 48vh the title band landed on the middle of the picture
     and needed a heavy gradient to stay legible — which is what made it read
     as shouting over the image, and it buried the photo at the same time.
     Here the photo is the photo and the type sits UNDER it: .case-head stops
     being a flex box with the title pinned to its bottom, .case-head-media
     joins the flow with a real height (56vh — bigger than before), and the
     info block follows it on black. Nothing is stacked on anything. */
  .ts-style .case-head{ height:auto; display:block; }
  .ts-style .case-head-media{
    position:relative; inset:auto; height:56vh;
    /* the fade the desktop cover has and mobile never got: the photo dissolves
       into the page instead of stopping on a straight cut. Masking the wrapper
       (not the img) takes the shade down with it, so they fade as one. */
    -webkit-mask-image:linear-gradient(to bottom, #000 56%, rgba(0,0,0,.55) 82%, transparent 100%);
            mask-image:linear-gradient(to bottom, #000 56%, rgba(0,0,0,.55) 82%, transparent 100%);
  }
  /* with no type on the photo the shade only has to seat the bottom edge */
  .ts-style .case-head-shade{
    background:linear-gradient(to top,
      rgba(16,16,19,.72) 0%, rgba(16,16,19,.34) 18%, rgba(16,16,19,.08) 34%, transparent 52%);
  }
  .ts-style .case-head-info{
    position:static; text-align:right;
    /* photo + fade untouched (Geert: leave those alone) — only the text moves,
       by pulling it up over the photo's already-fading bottom edge (the mask
       above dips to 55%/transparent right where this now sits). Everything
       after it in the flow (the chapter head, body copy) follows for free.
       text-align:right matches desktop (.case-head-info's own base rule) —
       was force-centred here, now right-aligned like the thumbnail titles
       everywhere else. */
    margin:-9vh 0 0; padding:1.6rem 6vw 0;
  }
  .ts-style .case-head-info h2{ text-shadow:none; }
  /* the black cap that bridges the cover edge on desktop would now sit over
     the title, and the mask above already does that job */
  .ts-style .case-body::before{ display:none; }
  /* ---- THE COVER TITLE IS THE TWO-LINE TITLE AGAIN ----
     A previous pass shrank this to 6.6vw AND hid the category line
     (".ch-type", e.g. "Photography & Film") plus its <br>, leaving only the
     short client name. Geert wants the pair back, at the size it had before:
     the kicker and the name together ARE the title, and one word on its own
     ("Minor", "Dafi Time & Bags") does not say what the case is.
     9.2vw, not the old 10vw: at 10vw the longest name ("Spoorwegmuseum")
     measured 332 of 390px and sat hard against both gutters. 9.2vw keeps
     every one of the six inside the 6vw-padded column with real margin, and
     the two lines stay a tight block at line-height 1.08. */
  .case-head-info h2, .ts-style .case-head-info h2{
    font-size:clamp(1.9rem,9.2vw,2.9rem); line-height:1.08;
  }
  .case-head-info h2 .ch-type, .case-head-info h2 br{ display:revert; }

  /* ---- CLOSE THE GAP UNDER THE TITLE ----
     46px sat between the title's last line and the hairline above the
     chapter row — two separate spaces stacked on each other: .case-body's
     own 33.8px top padding plus the page-head's 16.9px margin. On a phone
     that read as a hole, and it pushed everything after it (chapter row,
     Challenge, the whole body) a screen-fraction lower than it needed to be.
     Both are cut; the rule lands ~18px higher and, because the rest of the
     case is normal flow, the entire page follows it up. */
  /* nudged up again (1.4rem/.55rem → 1.1rem/.35rem): Geert wants the hairline
     and everything under it a bit higher still */
  .ts-style .case-body{ padding-top:1.1rem; }
  .ts-style .case-body > .case-page-head{ margin-top:.35rem; }
  /* minor carries its own #id padding (4vh) from the desktop sheet, which
     outranks the class rule above — it has to be restated here or that one
     case keeps a gap 10px looser than the other five */
  #case-minor .case-body{ padding-top:1.1rem; }
  /* the desktop nowrap on treinsafari's kicker ("Scavenger Hunt Design", the
     longest of the six) exists so it holds ONE line on a wide cover — at
     phone width that same rule would push it straight off the screen */
  #case-treinsafari .case-head-info h2 .ch-type{ white-space:normal; }
  /* and the same scaled twin the desktop rule uses, for the same reason: at
     the shared 9.2vw "Scavenger Hunt Design" measured ~375px against a 343px
     column and broke after "Hunt", giving this one cover three lines where
     the other five have two. 8.1vw holds it on one line with room to spare,
     and both the type and the column scale with vw — so the fit is identical
     on a 360px Android and a 430px Pro Max. */
  #case-treinsafari .case-head-info h2{ font-size:clamp(1.7rem,8.1vw,2.55rem); }

  /* ---- CHAPTER HEAD ROW (01 / OVERVIEW / 02) ----
     Same treatment as the landing's section heads: mono chrome scale, and the
     name centred on the row (left:50%) instead of at the desktop's 29% column
     line, which is what drove it into the number. */
  .ts-style .case-page-head-rule{ height:2px; }
  .ts-style .case-page-head-row{ font-size:.72rem; letter-spacing:.12em; margin-top:.95rem; }
  .ts-style .case-page-head-row .cph-name{ left:50%; }

  /* ---- BODY COPY ----
     .case-cols/.cs-text/.split-text p was 1.7rem = 27px, which fit ~25
     characters per line: every paragraph became a 10-line ribbon. At 1.12rem
     it runs ~40 characters, the same measure as the hero and About copy. */
  .ts-style .case-body h3, .case-body h3, .case-cols h3, .split-text h3{
    font-size:1.42rem; line-height:1.24;
  }
  .ts-style .case-body p:not(.case-divider-label):not(.kl-caption):not(.cinema-caption),
  .case-cols p, .cs-text p, .split-text p{
    font-size:1.12rem; line-height:1.58;
    /* full column, both edges straight. The 36ch cap and the centring left
       every line a different length; justified type squares the block off.
       hyphens:auto is what keeps that honest at this measure — without it the
       browser opens word-gaps instead of breaking a long word. */
    max-width:none; text-align:justify; text-align-last:left;
    -webkit-hyphens:auto; hyphens:auto;
  }

  /* ---- VERTICAL RHYTHM ----
     16vh between sections and 13vh under each chapter head is a desktop
     measure; on a phone it read as the page having fallen apart. */
  /* 12vh of padding under a cover that already fades to black left a ~120px
     void before the first chapter rule */
  /* title -> thin line gap: set once, further up in this same block
     ("CLOSE THE GAP UNDER THE TITLE", padding-top + the page-head's own
     margin). A second `.case-body{padding-top:4vh}` used to sit here
     and, being later in the sheet, silently won — putting 10px of the gap
     back. Removed; that earlier pair is now the only place it is set. */
  .ts-style .case-section, .case-section{ margin-bottom:8vh; }
  /* was 3vh: the white space above "Challenge" (and above every later
     chapter's first line) closes up, so the body copy sits nearer the rule
     it belongs to instead of floating under it */
  .ts-style .case-page-head{ margin-bottom:.4vh; }
  .ts-style .case-page-head-2{ margin-top:9vh; }
  .ts-style .case-duo, .ts-style .case-trio{ margin-bottom:8vh; }

  /* ---- MEDIA RUNS EDGE TO EDGE ----
     Was also applied to .single (video) and .case-duo/.case-trio (stacked
     photos) — Geert: both read as pasted flush against the screen edges
     instead of framed content, so both are now EXCLUDED and keep their own
     padded, rounded frame like desktop (.single-frame card / the base
     10px photo radius). Left in place only for genuine full-bleed rows
     (.case-media-row.full — already edge-to-edge at the base, unrelated to
     this override). */
  .ts-style .case-duo{ grid-template-columns:1fr; gap:14px; }
  .ts-style .case-media-row:not(.single):not(.case-duo):not(.case-trio){
    padding-left:0; padding-right:0;
    /* img.reveal parks at scale(1.1) until it scrolls into view. Inside a
       padded column that stayed within the screen; full-bleed it stuck 20px
       out on each side and the page could be dragged sideways until the
       reveal fired. Clipping the frame keeps the zoom-out inside it. */
    overflow:hidden;
  }
  .ts-style .case-duo, .ts-style .case-trio{ overflow:hidden; }
  .ts-style .case-media-row:not(.single):not(.case-duo):not(.case-trio) img,
  .ts-style .case-media-row:not(.single):not(.case-duo):not(.case-trio) video{ border-radius:0; }
  .ts-style .case-duo figcaption, .ts-style .case-trio figcaption{ padding:0; }
  /* the run-ins before a divider label, a pull-quote or a late section were
     14–15vh — on a phone that is most of a screen of nothing */
  .ts-style .case-body p.case-divider-label{ margin-top:8vh; margin-bottom:4vh; }
  .ts-style .case-quote{ margin-top:9vh; }
  .ts-style .case-body > .case-section{ margin-top:8vh; }
  /* the FIRST section under any chapter head (Challenge under Overview,
     the next chapter's opener under case-page-head-2, etc.) was still
     picking up that same 8vh via the rule above — on top of the head's own
     margin-bottom, that's what kept the white space above "Challenge" wide
     even after the head's margin was cut. It only belongs BETWEEN sections
     within a chapter, not between the head and the first one. */
  .ts-style .case-page-head + .case-section{ margin-top:2.4vh; }
  .ts-style .case-next, .case-next{ margin-top:10vh; }
}

/* =========================================================
   TOUCH: THUMBNAILS AT FULL COLOUR.
   On desktop a thumbnail rests muted (saturate .85, brightness .85 in the
   work row) and only a real HOVER pops it to 100/100 — the mute is the
   "before" half of a hover reveal. A touch device has no hover, so that
   before-state is the ONLY state it ever shows: every cover sat permanently
   at 95% exposure/saturation with nothing to lift it. Full colour here, in
   every state (base, the emulated :hover iOS latches after a tap, and the
   is-hovering sibling dim). Scoped to (hover:none) rather than a width, so
   this follows touch capability, not screen size — a wide-screen iPad gets
   the same always-on full colour as a phone, while any device with a real
   mouse/trackpad (laptop, desktop, an iPad with a pointer attached) keeps
   the muted hover-reveal untouched. The projects page needs no counterpart:
   .pc-card carries only a drop-shadow, its covers were already at 100%.
   ========================================================= */
@media (hover:none){
  .work-card > img, .more-panel > img,
  .work-card:hover > img, .more-panel:hover > img,
  .work-row .work-card > img,
  .work-row .work-card:hover > img,
  .work-row.is-hovering .work-card:not(:hover) > img{
    filter:none;
  }
}

/* PHONE LANDSCAPE — ABOUT PAGE. `.abh-frame{ top:12vh; height:62vh }` (the
   MOBILE PASS override, ~line 6327) assumes a tall portrait viewport; at
   375px height that 12vh top (45px) lands UNDER the logo (bottom ~74px),
   so the photo frame ran up under "GEERT DEBETS" instead of clearing it.
   Placed last so it wins over that portrait rule; scoped tight enough
   (max-height:520px, min-width so ≤760 portrait phones never match) that
   real desktop/iPad-landscape windows — always taller — never see it. */
@media (orientation:landscape) and (max-height:520px) and (min-width:761px){
  .abh-frame{ top:22vh; height:56vh; }
  .abh-name{ font-size:clamp(2.4rem,10vw,4.5rem); bottom:14vh; }
  .abh-tagline{ font-size:1rem; bottom:2vh; }
}



/* ---- MOBIEL: laatste skill-woord vulde nooit helemaal ----
   "WORKFLOWS" is één token dat niet kan afbreken en op 13vw ~43px breder is dan
   zijn box (gemeten 42–44px, van 320 t/m 440 viewport — de box groeit mee met de
   vw, de staart blijft dus even lang). De lichtband is een gradient met
   background-clip:text, en dáár zit het: de achtergrond wordt NOOIT buiten de
   elementbox geschilderd, hoe je hem ook schaalt. De vorige poging hier
   (background-size:118%) kon dus per definitie niets doen — gemeten met een
   vaste band op 99%: de vulling stopte nog steeds exact op de rand van de box.
   Het enige dat werkt is de BOX groter maken. padding-right verruimt het
   schilder­gebied naar rechts, de even grote negatieve marge geeft die ruimte
   weer terug aan de flex-regel: de content-box, de afbreking en de rechts­
   uitlijning van "AI" blijven op de pixel gelijk (geverifieerd op 320/390/440,
   EN én NL "AI-workflows" dat op het koppelteken afbreekt).
   56px = de 44px staart + marge voor de stroke. Op alle woorden i.p.v. alleen
   het laatste, zodat de gradient-box van alle zes even veel groeit en de band
   in hetzelfde tempo door de wand blijft lopen. */
@media (max-width:760px){
  .absw-word{ padding-right:56px; margin-right:-56px; }
}

/* ---- IKEA-thumbnail, mobiel ----
   TERUGGEDRAAID naar de basis. Er zijn drie technieken geprobeerd om de foto
   BINNEN het kader groter te maken, en alle drie zijn gesneuveld:
     1. `scale:1.15`      — op mobiel is de <img> zelf het kader (de kaart is
                            overflow:visible zonder radius), dus dit blies het
                            hele frame op i.p.v. de foto erin.
     2. `object-view-box` — de juiste eigenschap hiervoor, maar Safari kent hem
                            pas vanaf 18; op het testtoestel gebeurde er niets.
     3. `clip-path` + `transform` — werkt in Chrome, maar leunt er wéér op dat
                            één eigenschap pakt: valt de clip weg, dan blijft een
                            524px breed beeld over dat de rij uit haar oevers duwt.
   Les: zolang de <img> het kader IS, is elke CSS-zoom een schaal op het kader
   zelf die met een tweede eigenschap teruggeknipt moet worden — te fragiel.
   DIT IS DE OPLOSSING GEWORDEN: de foto is bijgesneden in een apart bestand.
   `ikea-cover2-zoom.jpg` is `ikea-cover2.jpg` uitgesneden op 1850x1235 (uit
   2460x1643) = 1.33x ingezoomd — de zoom zit dus in de PIXELS, niet in een
   CSS-eigenschap. De uitsnede is NIET gecentreerd: hij begint 192px van links
   i.p.v. 305px, dus 113px naar links over de bron geschoven. Dat duwt de
   billboard naar RECHTS in het kader (van 0–71% van de kaderbreedte, met de
   linkerrand net aangesneden, naar 16–84% met marge aan beide kanten).
   Reken je dit ooit opnieuw uit: het frame is 335x441, dus cover schaalt op
   hoogte en je ziet maar 1235x0.76 = 939 van de 1850 bronpixels breed. De
   inline object-position (10% 58%) legt dat venster op 91px vanaf de linkerrand
   van de uitsnede. Zichtbaar bronvenster = 283..1222; de billboard zit op
   430..1075. Bestandsnaam draagt ?v=2 omdat de vorige versie (1.45x,
   gecentreerd) al gecachet kon zijn.
   Waarom dit niet kan breken zoals de drie pogingen hierboven: `content` wisselt
   alléén het geschilderde beeld om. De doos van het element, zijn aspect-ratio,
   zijn 28px hoeken en zijn backglow blijven exact zoals bij de buurkaarten, dus
   het kader KAN niet groter worden en er kan niets buiten de rij steken. Kent
   een browser `content` op een <img> niet, dan zie je gewoon de oude foto —
   geen zoom, verder niets stuk. Beide uitkomsten zijn veilig, en dat was bij
   transform/clip-path juist niet zo.
   Geen <picture>-wrapper gebruikt: dat verandert de DOM en alle regels hierboven
   selecteren op `.work-card > img` als DIRECT kind.
   Uitsnede blijft uit de inline object-position (10% 58%) in index.html; die
   werkt gewoon door op het bijgesneden bestand.
   NB: main.js geeft de vlucht-kloon ditzelfde beeld mee (zoek op `content`),
   anders springt de uitsnede op het moment van aantikken.
   Projectenpagina: 4% is de waarde van vóór deze sessie, onaangeroerd. */
@media (max-width:760px){
  .work-row .work-card[data-case="ikea"] > img{ content:url("assets/img/ikea-cover2-zoom.jpg?v=3"); }
  /* DE CASE-KOP KRIJGT HETZELFDE BESTAND — anders hapert de overgang.
     Bij het aantikken vliegt een kloon van de thumbnail naar de kop en geeft
     daar het stokje over aan de <img> van de kop. Toonde de thumbnail de
     bijgesneden versie en de kop het volledige beeld, dan morphte de vlucht
     tussen TWEE verschillende beelden en klapte de uitsnede op het laatste
     frame om. Zelfde bestand aan beide kanten = de vlucht is nog puur een
     doos-en-uitsnede-morph, precies waar de animatie voor gemaakt is.
     object-position 10%: de kop toont 1085 van de 1984 bronpixels breed, de
     billboard zit daarin op 309–954, dus 10% zet hem netjes midden in beeld —
     en het is dezelfde 10% als de thumbnail, dus horizontaal staat de uitsnede
     bij vertrek en aankomst gelijk. Verticaal past de uitsnede exact, dus de
     Y-waarde doet hier niets. */
  #case-ikea .case-head-media img{
    content:url("assets/img/ikea-cover2-zoom.jpg?v=3");
    object-position:10% 50% !important;
  }
  .pc-item[data-case="ikea"] .pc-card img{ object-position:4% 50% !important; }
}

