/* ==========================================================================
   Jaco Schilp — minimal gallery portfolio
   Design system
   ========================================================================== */

/* The dark room is the default: the work is light in a dark space, and the
   photography carries it. A visitor can switch to paper from the bar, and
   that choice is remembered. Light lives in [data-theme="light"] below. */
:root {
  --bg: #0e0f11;
  --fg: #f2f1ee;
  --muted: #a3a39d; /* 7.5:1 on the dark ground */
  --line: #2c2d30;
  --accent: #f2f1ee;
  /* the empty frame a picture loads into, and the cloud's own two colours,
     which main.js reads so the canvas follows the theme */
  --tile: #17181b;
  --cloud-bg: #0e0f11;
  --cloud-ink: #e8e7e3;

  color-scheme: dark;

  --maxw: calc(3000px * var(--x-width, 1));
  --gutter: calc(var(--x-gutter, 1) * var(--ts, 1) * clamp(1.25rem, 4vw, 4rem));
  --bar-h: calc(72px * var(--x-bar, 1.7) * var(--ui, 1));

  --font-sans: "Open Sans", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  --font-serif: "Open Sans", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;

  --t-fast: 0.25s ease;
  --t-med: 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

/* The paper room, one click away. Only the tokens change, never a size. */
:root[data-theme="light"] {
  --bg: #f7f6f4;
  --fg: #141414;
  /* the quiet grey still has to be readable: #8a8a86 measured 3.21:1 on the
     paper, under the 4.5 minimum. This is the lightest grey that passes
     (4.75:1), so secondary text stays secondary but legible in daylight. */
  --muted: #6e6e69;
  --line: #e3e1dd;
  --accent: #141414;
  --tile: #e9e7e3;
  --cloud-bg: #f7f6f4;
  --cloud-ink: #20201f;

  color-scheme: light;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The scrollbar's lane is always reserved. Without this, a page that just
     fits flips between having a scrollbar and not, the width changes 15px,
     text rewraps, the height crosses the threshold back, and the whole page
     jitters left and right in a loop. */
  scrollbar-gutter: stable;
  /* and for browsers without scrollbar-gutter, the scrollbar simply always
     exists; either way the width can never change under the layout */
  overflow-y: scroll;
}

/* The tune dials: every --x-* is a multiplier on the shipped design, 1 by
   default so nothing changes. The tune panel (?tune on any URL) turns them
   live; chosen values get baked back here as new defaults. */
html { font-size: calc(100% * var(--x-base, 1)); }

/* Proportional presence: the design IS the 4K wall, and every other screen
   shows the same picture scaled down — the whitespace fraction matches the
   wall everywhere. main.js sets --ts/--ui exactly (width / 3840, floored at
   0.35, phones excluded); these media steps are only the fallback for the
   first paint and for no-JS, so they approximate the same line. */
:root { --ts: 1; --ui: 1; }
@media (max-width: 3400px) { :root { --ts: 0.85; --ui: 0.85; } }
@media (max-width: 2600px) { :root { --ts: 0.65; --ui: 0.65; } }
@media (max-width: 2100px) { :root { --ts: 0.5; --ui: 0.5; } }
@media (max-width: 1500px) { :root { --ts: 0.39; --ui: 0.39; } }
@media (max-width: 600px) { :root { --ts: 0.66; --ui: 0.55; } }

/* THE READABLE FLOOR. Pure proportion is right until it makes text nobody
   can read: at 1512 the tagline fell to 8.6px and the footer to 5.1px.
   So every piece of type is written as max(floor, proportional) — the wall
   and any wide screen keep exactly the tuned sizes, and only where the
   proportional value would drop below legibility does the floor take over.
   Whitespace stays fully proportional; only the letters stop shrinking.
   --fl is 0 on phones, where max() then yields the phone-tuned size. */
:root { --fl: 1; }
@media (max-width: 600px) { :root { --fl: 0; } }
:root {
  --fx-title: calc(var(--x-title, 1.02) * var(--ts));
  --fx-text: calc(var(--x-text, 1.58) * var(--ts));
  --fx-label: calc(var(--x-label, 1.36) * var(--ts));
  --fx-credits: calc(var(--x-credits, 1.6) * var(--ts));
  --fx-space: calc(var(--x-space, 1.36) * var(--ts));
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: max(calc(15px * var(--fl, 1)), calc(17px * var(--fx-text)));
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }

/* ---- Layout ---- */
.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

section { padding-block: calc(var(--fx-space) * clamp(2.25rem, 5.5vw, 4.5rem)); }

/* ---- Type ---- */
.eyebrow {
  font-size: max(calc(11px * var(--fl, 1)), calc(var(--fx-label) * 0.72rem));
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 calc(1.5rem * var(--ts, 1));
}

h1, h2, h3 { font-weight: 400; margin: 0; line-height: 1.05; }

.display {
  font-family: var(--font-serif);
  font-size: max(calc(30px * var(--fl, 1)), calc(var(--fx-title) * clamp(2.6rem, 8vw, 6.5rem)));
  letter-spacing: -0.03em;
  font-weight: 700;
  line-height: 1.02;
}

.h-section {
  font-family: var(--font-serif);
  font-size: max(calc(21px * var(--fl, 1)), calc(var(--fx-title) * clamp(1.8rem, 4vw, 3rem)));
  letter-spacing: -0.02em;
  font-weight: 700;
}

.lead {
  font-size: max(calc(16px * var(--fl, 1)), calc(var(--fx-text) * clamp(1.05rem, 2vw, 1.4rem)));
  line-height: 1.5;
  max-width: 32ch;
  color: var(--fg);
}

p { margin: 0 0 1.2em; }

/* ==========================================================================
   Header / nav
   ========================================================================== */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: transparent; /* lets the point cloud flow underneath at the top */
  border-bottom: 1px solid transparent;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.site-header.scrolled {
  border-bottom-color: var(--line);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.2) blur(10px);
}

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--bar-h);
}
.brand {
  font-family: var(--font-serif);
  font-size: max(calc(19px * var(--fl, 1)), calc(var(--x-brand, 2.2) * 1.35rem * var(--ui, 1)));
  font-weight: 600;
  letter-spacing: 0.01em;
}
.nav-links {
  display: flex;
  gap: calc(var(--ui, 1) * clamp(1rem, 3vw, 2.5rem));
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: max(calc(12.5px * var(--fl, 1)), calc(var(--x-nav, 2) * 0.82rem * var(--ui, 1)));
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.nav-links a {
  position: relative;
  color: var(--muted);
  transition: color var(--t-fast);
}
.nav-links a:hover,
.nav-links a[aria-current="page"] { color: var(--fg); }
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0; bottom: -4px;
  width: 100%; height: 1px;
  background: var(--fg);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--t-med);
}
.nav-links a:hover::after,
.nav-links a[aria-current="page"]::after { transform: scaleX(1); }

/* The light switch: a named control, not a glyph to be discovered. It reads
   as the door out of the room you are in, so it carries the destination's
   icon and word, on the same pill language as the filters. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: max(calc(6px * var(--fl, 1)), calc(8px * var(--ui, 1)));
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  color: var(--fg);
  font: inherit;
  font-size: max(calc(11.5px * var(--fl, 1)), calc(var(--x-nav, 2) * 0.72rem * var(--ui, 1)));
  letter-spacing: 0.09em;
  text-transform: uppercase;
  line-height: 1;
  padding: max(calc(8px * var(--fl, 1)), calc(11px * var(--ui, 1)))
    max(calc(13px * var(--fl, 1)), calc(18px * var(--ui, 1)));
  margin-left: calc(var(--ui, 1) * clamp(1rem, 3vw, 2.5rem));
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
.theme-toggle:hover {
  background: var(--fg);
  color: var(--bg);
  border-color: var(--fg);
}
.theme-toggle svg {
  width: max(calc(15px * var(--fl, 1)), calc(20px * var(--ui, 1)));
  height: auto;
  display: block;
}
.theme-label { display: block; white-space: nowrap; }
/* each room shows the door to the other one: the dark room offers the sun */
.theme-toggle .theme-moon { display: none; }
:root[data-theme="light"] .theme-toggle .theme-sun { display: none; }
:root[data-theme="light"] .theme-toggle .theme-moon { display: block; }

.nav-toggle { display: none; background: none; border: 0; cursor: pointer; padding: 8px; }
.nav-toggle span { display: block; width: 24px; height: 1.5px; background: var(--fg); margin: 5px 0; transition: var(--t-fast); }

/* ==========================================================================
   Hero
   ========================================================================== */
.hero {
  position: relative;
  min-height: 56vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-bottom: calc(var(--ts, 1) * clamp(2rem, 5vw, 3.5rem));
  padding-top: calc(var(--ts, 1) * clamp(1.5rem, 4vw, 2.5rem));
}
.hero .display { max-width: 16ch; text-shadow: 0 0 14px var(--bg); }
/* mixed from the theme's own ink and paper, so it follows the room */
.hero .eyebrow { color: color-mix(in srgb, var(--fg) 74%, var(--bg)); text-shadow: 0 0 10px var(--bg), 0 0 4px var(--bg); }
/* the home statement leads on every layout, not only the one-view desktop:
   between phone and desktop it still has to out-weigh everything around it */
.home-hero .lead {
  font-weight: 600;
  line-height: 1.3;
  font-size: max(calc(20px * var(--fl, 1)), calc(var(--fx-text) * clamp(1.05rem, 2vw, 1.4rem)));
}
.hero .lead {
  margin-top: calc(1.75rem * var(--ts, 1));
  color: var(--fg); /* the statement carries full ink, not a grey */
  text-shadow: 0 0 12px var(--bg), 0 0 5px var(--bg), 0 0 2px var(--bg);
}

/* interactive point-cloud behind hero + top bar, vignetted top and bottom */
.hero-canvas {
  position: absolute;
  top: calc(-1 * var(--bar-h)); /* reaches up underneath the sticky header */
  left: 0;
  width: 100%;
  height: calc(100% + var(--bar-h));
  pointer-events: none;
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0, black 16%, black 70%, transparent 100%),
    linear-gradient(to right, transparent 0, black 12%, black 88%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to bottom, transparent 0, black 16%, black 70%, transparent 100%),
    linear-gradient(to right, transparent 0, black 12%, black 88%, transparent 100%);
  mask-composite: intersect;
}
.hero > :not(.hero-canvas) { position: relative; }

/* On the home the cloud is not a hero decoration but the GROUND: fixed
   behind the whole page, works row and about band floating over it. It
   paints its own paper color, so it simply is the background. */
.page-home .hero-canvas {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100dvh;
  z-index: -1;
  /* The cloud RESOLVES by losing POINTS, not by losing contrast. There used
     to be a vertical opacity ramp here (black 34% -> transparent 62%), and it
     was a mistake: its midpoint sat at 48% while the cloud's own centre is
     0.47H, so the ramp lay straight across the densest band and threw away
     half the ink. Dimming a dense field uniformly reads as a grey gradient,
     not as dust thinning out. draw() in main.js now drops dots as a function
     of their y instead, so every surviving dot keeps full ink. Only the
     horizontal mask stays, to keep the cloud off both margins. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 8%, black 92%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, black 8%, black 92%, transparent 100%);
}

/* ==========================================================================
   Work grid
   ========================================================================== */
.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: calc(var(--x-gap, 1) * var(--ts, 1) * clamp(1.25rem, 3.5vw, 3.5rem)) calc(var(--x-gap, 1) * var(--ts, 1) * clamp(1.25rem, 3vw, 2.5rem));
}
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }

.card { display: block; min-width: 0; }
.card-media {
  position: relative;
  overflow: hidden;
  background: var(--tile);
  aspect-ratio: 1 / 1;
}
.card-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity var(--t-med);
}
.card:hover .card-media img { transform: scale(1.04); }
/* scroll drives appearance: media rises and settles with the scroll position
   itself, forward and back, in browsers that speak scroll-driven animations.
   Everyone else keeps the reveal transitions. The HOME is exempt: there the
   pictures stand full ink from the first frame over the cloud. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* NOT the work wall. A scroll-driven animation on a page that cannot
       scroll never finishes: the wall is built to fit one screen, so its lower
       rows sat forever partway through their entry, at 95% opacity in the third
       row and 71% in the last, and the grid looked like it faded out towards
       the bottom. The cards keep the .reveal fade, which the observer in
       main.js drives and which does not need a scroll to complete. The press
       page does scroll and Jaco likes the reveal there, so it keeps it. */
    .press-media,
    .project-gallery img,
    .project-hero img,
    .project-videos.is-main .project-video-file {
      animation: scroll-rise linear both;
      animation-timeline: view();
      /* Finish once the thing is fully on screen, rather than 42% into the
         cover phase. "cover" needs scroll that the END of a page does not
         have, so the last row could never complete: it stayed scaled down and
         shifted 22px, which is how it came to sit over the related work at the
         bottom of flip-mode. "entry 100%" is reached the moment the element is
         wholly visible, which the last row does reach. */
      animation-range: entry 0% entry 100%;
    }
  }
}
/* the one-screen wall does not scroll, so nothing there may wait for scroll */
/* NOT the about portrait: it is floated, with the bio text wrapped around a
   hole that does not move. Animating the picture's transform slid and grew it
   inside that fixed hole for the whole length of the page, which is Jaco's
   "images at about is moving all the time". A picture set into running text
   stays put; only pictures that stand on their own line get the scroll rise. */
/* Framer-style variety: pictures grow in as the scroll carries them, but
   not all the same way. In a gallery row the left one leans in from the
   left, the right one from the right, the middle rises, at slightly
   different distances, so the page has depth instead of one gesture. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .project-gallery img:nth-child(3n + 1) { animation-name: scroll-slide-l; }
    .project-gallery img:nth-child(3n) { animation-name: scroll-slide-r; }
    .press-media:nth-child(odd of .press-item) { animation-name: scroll-rise; }

  }
}
@keyframes scroll-rise {
  from { opacity: 0.3; transform: translateY(34px) scale(0.8); }
  to { opacity: 1; transform: none; }
}
@keyframes scroll-slide-l {
  from { opacity: 0.3; transform: translate(-56px, 22px) scale(0.84); }
  to { opacity: 1; transform: none; }
}
@keyframes scroll-slide-r {
  from { opacity: 0.3; transform: translate(56px, 22px) scale(0.84); }
  to { opacity: 1; transform: none; }
}

/* Continuous parallax: whole gallery rows drift gently at alternating
   speeds the entire time they cross the screen, under the entrances,
   so the page has layers, not just arrivals. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .project-gallery .grow {
      animation: parallax-a linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
    .project-gallery .grow:nth-child(even) { animation-name: parallax-b; }
  }
}
@keyframes parallax-a {
  from { transform: translateY(22px); }
  to { transform: translateY(-22px); }
}
/* the words of a statement come into ink as the scroll reaches them */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .statement .w,
    .pillar-insight .w {
      animation: word-ink linear both;
      animation-timeline: view();
      animation-range: entry 70% entry 110%;
    }
  }
}
@keyframes word-ink {
  from { opacity: 0.22; }
  to { opacity: 1; }
}

@keyframes parallax-b {
  from { transform: translateY(38px); }
  to { transform: translateY(-38px); }
}
.card-media.tall { aspect-ratio: 1 / 1; }

.card-meta {
  display: flex;
  flex-direction: column;
  gap: calc(0.2rem * var(--ts, 1));
  margin-top: calc(0.85rem * var(--ts, 1));
}
.card-title {
  font-family: var(--font-serif);
  font-size: max(calc(14px * var(--fl, 1)), calc(var(--fx-label) * clamp(1.15rem, 1.7vw, 1.45rem)));
  font-weight: 600;
  letter-spacing: -0.01em;
  text-wrap: pretty; /* avoid stranded runt words in titles */
}
.card-sub { font-size: max(calc(11.5px * var(--fl, 1)), calc(var(--fx-label) * 0.78rem)); letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.card-cat { font-size: max(calc(11.5px * var(--fl, 1)), calc(var(--fx-label) * 0.78rem)); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }

/* placeholder shimmer for missing images */
.ph {
  width: 100%; height: 100%;
  min-height: calc(var(--ts, 1) * clamp(180px, 30vw, 360px));
  display: grid; place-items: center;
  background:
    linear-gradient(135deg, var(--tile) 0%, color-mix(in srgb, var(--tile) 82%, var(--fg)) 100%);
  color: var(--muted);
  font-family: var(--font-serif);
  font-size: calc(1.4rem * var(--ts, 1));
  letter-spacing: 0.04em;
}

/* ==========================================================================
   Filters
   ========================================================================== */
/* All projects head: bold statement left, quiet intro right, then one toolbar
   line with search, pill filters and the shown count */
.work-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: calc(var(--ts, 1) * clamp(2rem, 6vw, 8rem));
  margin-bottom: calc(var(--ts, 1) * clamp(1.5rem, 3vw, 2.5rem));
}
.work-title {
  font-size: max(calc(23px * var(--fl, 1)), calc(var(--ts, 1) * clamp(1.9rem, 3vw, 2.9rem)));
  line-height: 1.15;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0;
  max-width: 22ch;
}
.work-intro {
  margin: calc(0.5rem * var(--ts, 1)) 0 0;
  color: var(--muted);
  /* Floor 12px, not 14: at 1920 the honest proportional value is 11.9px, so
     12 lets the true value win there and the ratio to the 4K size stays 0.50.
     A 14px floor rendered this line 17% larger than proportional, which is
     visible next to everything around it. */
  font-size: max(calc(12px * var(--fl, 1)), calc(var(--fx-text) * 0.95rem));
  /* One line per sentence (the <br> in work.html), two lines on every screen.
     The measure is in vw, NOT ch: a ch measure tracks the font that is
     actually rendered, so the moment the floor engages the column stops being
     a share of the screen. It was 26rem * --ts before, which shrank with --ts
     while the type stopped at its floor and left a 206px column of five
     ragged four-word lines at 1920.

     No magic width. The <br> already says where the lines break, so
     max-content asks for exactly the longer of the two sentences at whatever
     size the type is actually rendering -- no number to guess, and it cannot
     starve the text the way a 26rem, a 27vw or a 420px cap each did in turn.
     max-width keeps it inside its column on a narrow window. The result is
     still a constant share of the screen where it matters: 20.4% at 1920 and
     at 3840, because above the floor the font itself is proportional. */
  width: max-content;
  max-width: 100%;
  line-height: 1.5;
}
.work-toolbar {
  display: flex;
  align-items: center;
  gap: calc(var(--ts, 1) * clamp(1rem, 2.5vw, 2.5rem));
  border-top: 1px solid var(--fg);
  border-bottom: 1px solid var(--line);
  padding: calc(0.7rem * var(--ts, 1)) 0;
  margin-bottom: calc(var(--ts, 1) * clamp(1rem, 2vw, 1.5rem));
}
.work-search {
  display: flex;
  align-items: center;
  gap: calc(0.5rem * var(--ts, 1));
  color: var(--muted);
  /* 260px * --ts left a 129px field at 1920 holding 13px type. The basis
     carries the same floor as the text inside it, so the box and its
     contents stop shrinking together. */
  flex: 0 1 max(calc(190px * var(--fl, 1)), calc(380px * var(--ts, 1)));
  border-bottom: 1px solid var(--line);
  padding-bottom: 3px;
}
.work-search input {
  border: 0;
  outline: none;
  background: none;
  font: inherit;
  font-size: max(calc(13px * var(--fl, 1)), calc(var(--fx-label) * 0.9rem));
  color: var(--fg);
  width: 100%;
}
.work-search input::placeholder { color: var(--muted); }
.pill,
.filter {
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  font: inherit;
  /* This was max(11.5px, 0.72rem * --ts). At 4K the proportional half comes
     to 11.47px, so the FLOOR won even on the wall and these controls were
     11.5px on every screen that has ever rendered them -- a ratio of exactly
     1.0, the thing the proportion law exists to prevent. On the label dial
     they read 15.6px on the wall and fall back to the floor on a laptop.
     The padding is in em, so the chip grows with its own text. */
  font-size: max(calc(11.5px * var(--fl, 1)), calc(var(--fx-label) * 0.72rem));
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0.42em 1em;
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
.pill:hover,
.filter:hover { color: var(--fg); border-color: var(--muted); }
.pill[aria-pressed="true"],
.filter[aria-pressed="true"] {
  background: var(--fg);
  color: var(--bg);
  border-color: var(--fg);
}
.work-tools {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: calc(1.2rem * var(--ts, 1));
  white-space: nowrap;
  /* the count is the one thing in this bar that must never be squeezed or
     clipped: the chips give way first */
  flex: 0 0 auto;
}
/* the chips are what yields when the bar runs out of room */
.filters { min-width: 0; }

.work-empty {
  grid-column: 1 / -1;
  margin: calc(2rem * var(--ts, 1)) 0 0;
  color: var(--muted);
  font-size: max(calc(13px * var(--fl, 1)), calc(var(--fx-text) * 0.95rem));
}

/* The work page holds its frame. Filter the wall down to five performances
   and the grid gets short, but the toolbar must stay where it is and the
   footer must stay on the floor -- otherwise the whole page jumps every time
   a chip is pressed. Only the pictures change size, which is the point. */
@media (min-width: 1000px) {
  .page-press { min-height: 100dvh; display: flex; flex-direction: column; }
  .page-press .site-footer { margin-top: auto; }
}

@media (min-width: 1000px) {
  /* The footer holds the floor. height:100% on the grid was tried here and
     was wrong: the grid took 100% of a section that carries its own bottom
     padding, so the wall overran the page and the 4K filtered view scrolled.
     fit() gives the grid an explicit pixel box instead. */
  .page-work { min-height: 100dvh; display: flex; flex-direction: column; }
  .page-work .site-footer { margin-top: auto; }
}
.work-count {
  font-size: max(calc(11.5px * var(--fl, 1)), calc(var(--fx-label) * 0.72rem));
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.filters { display: flex; flex-wrap: wrap; gap: calc(0.5rem * var(--ts, 1)); }
@media (max-width: 900px) {
  .work-head { flex-direction: column; gap: 0.75rem; }
  .work-toolbar { flex-wrap: wrap; row-gap: 0.7rem; }
  .work-search { flex-basis: 100%; padding-block: 0.4rem 0.5rem; }
  /* 16px keeps iOS from zooming the page when the field is focused */
  .work-search input { font-size: 16px; }
  .pill, .filter { padding: 0.6em 1.1em; }
  .work-tools { margin-left: 0; }
}

/* ==========================================================================
   Project detail
   ========================================================================== */
.project-head { padding-top: calc(var(--ts, 1) * clamp(2rem, 4.5vw, 3.5rem)); padding-bottom: calc(var(--ts, 1) * clamp(1.2rem, 2.5vw, 2rem)); }
.project-head .display { font-size: max(calc(28px * var(--fl, 1)), calc(var(--fx-title) * clamp(2.4rem, 7vw, 5.5rem))); max-width: 18ch; text-wrap: balance; }
/* a co-authored work is told from both makers: the byline under the title */
.project-by {
  margin: calc(1rem * var(--ts, 1)) 0 0;
  font-family: var(--font-serif);
  font-size: max(calc(16px * var(--fl, 1)), calc(var(--fx-text) * clamp(1.15rem, 1.6vw, 1.55rem)));
  font-weight: 600;
}
.project-by .by-x { color: var(--muted); font-weight: 400; padding-inline: 0.3em; }
.project-facts {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  justify-content: space-between;
  gap: calc(1.5rem * var(--ts, 1)) calc(var(--ts, 1) * clamp(2rem, 4vw, 5rem));
  margin-top: calc(1.4rem * var(--ts, 1));
  padding-top: calc(1.2rem * var(--ts, 1));
  border-top: 1px solid var(--line);
}
.fact-label { font-size: max(calc(11px * var(--fl, 1)), calc(var(--fx-label) * 0.72rem)); letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: calc(0.4rem * var(--ts, 1)); }
.fact-value { font-size: max(calc(14px * var(--fl, 1)), calc(var(--fx-label) * 0.95rem)); }
.fact-locations { line-height: 1.75; }

/* under the page: the press that wrote about the work and its nearest
   projects, one native-scrolling strip, cards snapping gently */
.project-related { margin-top: calc(var(--fx-space) * clamp(1.8rem, 3.5vw, 3rem)); }
.carousel {
  display: flex;
  gap: calc(var(--x-gap, 1) * var(--ts, 1) * clamp(0.8rem, 1.6vw, 1.4rem));
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-bottom: calc(0.6rem * var(--ts, 1));
  scrollbar-width: thin;
}
.rel-card { flex: 0 0 calc(var(--ts, 1) * clamp(240px, 22vw, 440px)); scroll-snap-align: start; display: block; }
.rel-media {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--tile);
  margin-bottom: calc(0.6rem * var(--ts, 1));
}
.rel-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.rel-card:hover .rel-media img { transform: scale(1.04); }
.rel-kicker {
  display: block;
  font-size: max(calc(11px * var(--fl, 1)), calc(var(--fx-label) * 0.68rem));
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.3rem;
}
.rel-title {
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: max(calc(14px * var(--fl, 1)), calc(var(--fx-label) * 0.95rem));
  line-height: 1.35;
}
/* A press card on a PROJECT page quotes the paper rather than repeating a
   headline about something else. Set like the press page's own pull quote. */
.rel-quote {
  display: block;
  font-family: var(--font-serif);
  font-style: italic;
  color: var(--muted);
  line-height: 1.45;
  font-size: max(calc(13px * var(--fl, 1)), calc(var(--fx-label) * 0.88rem));
}
/* the facts spread evenly; the link BLOCK closes the row at the right,
   its text left-aligned like every other fact */
.fact-link { margin-left: auto; }
/* the credits under the project text: each role a labeled credit with the
   name in full ink, set like the facts row. The credits dial sizes the names. */
/* Credits divide the width evenly instead of packing left and wrapping. As a
   flex-wrap row inside the half-width text column they came out ragged, with
   the last pair stranded alone on a second line under nothing. The column
   floor is in vw so the count is the same on every screen: about seven at
   most, four when there are four. */
/* A heading that continues a column, rather than opening one. The page label
   "About" and the column label "Selected CV" used to sit 30px apart, because
   one was outside the two-column block and one inside it, which is the
   "weird distance" Jaco kept seeing. Everything is a column label now, so
   both tops line up, and the blocks that used to sit in a section of their
   own carry on down the columns instead: the bio ended at 1717 while the CV
   ran to 2001, leaving a 333px void under his own text. */
/* the footer's Instagram is the same control as the one on About, so it is
   the same pill; the separator beside it goes, since a button needs no dot */
.footer-inner .pill { display: inline-block; vertical-align: middle; }

/* The Instagram link was an INLINE font-size of 0.85rem * --ts and nothing
   else: 6.7px at 1920, too small to read and far too small to click, on the
   one line of the site that asks a stranger to get in touch. Inline sizes
   need the floor as much as the stylesheet does. It is a pill now, the same
   control the filters use, so it looks like something you press. */
.contact-social { margin-top: calc(1.75rem * var(--ts, 1)); }
.contact-social .pill { display: inline-block; }

/* Every block on About opens with the same white line. It existed on exactly
   one thing, the CV's Work / Curation / Education heads, so half the page was
   ruled and half was not: Residencies, Live shows, Teaching, Tools, Services
   and Contact all opened with a bare label. And the lists closed with a line
   while the two paragraph blocks just stopped, so blocks ended two different
   ways as well. One rule to open, one to close, everywhere. */
#about .eyebrow {
  border-bottom: 1px solid var(--fg);
  padding-bottom: calc(0.5rem * var(--ts, 1));
  margin-bottom: calc(0.9rem * var(--ts, 1));
}
#about .about-line { border-bottom: 1px solid var(--line); padding-bottom: calc(0.85rem * var(--ts, 1)); }
/* whatever follows the bio starts on a clean line, not beside the portrait */
#about .about-sub { clear: left; }

.about-sub { margin-top: calc(2.6rem * var(--ts, 1)); }

/* The notes are their own line under the credits, not cells inside them. */
.credit-notes {
  margin: calc(0.9rem * var(--ts, 1)) 0 0;
  color: var(--muted);
  font-size: max(calc(12px * var(--fl, 1)), calc(var(--fx-label) * 0.8rem));
}
.project-credits {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, max(150px, 13vw)), 1fr));
  gap: calc(1.4rem * var(--ts, 1)) calc(var(--ts, 1) * clamp(1.5rem, 3vw, 3rem));
  margin-top: 1.6em;
  padding-top: 1.1em;
  border-top: 1px solid var(--line);
  align-items: start;
}
.credit-role {
  display: block;
  font-size: max(calc(11px * var(--fl, 1)), calc(var(--fx-label) * 0.72rem));
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.35rem;
}
.credit-name { font-size: max(calc(14px * var(--fl, 1)), calc(var(--fx-credits) * 1.05rem)); color: var(--fg); }
/* a credits sentence that is not a Role: Name pair reads as a quiet note */
.credit-note {
  flex-basis: 100%;
  margin: 0;
  max-width: 70ch;
  color: var(--muted);
  font-size: max(calc(13px * var(--fl, 1)), calc(var(--fx-label) * 0.85rem));
  line-height: 1.6;
}

.project-body {
  display: grid;
  grid-template-columns: 1fr;
  gap: calc(var(--ts, 1) * clamp(1rem, 3vw, 2rem));
  max-width: 60ch;
  /* below the text sits the gallery: that white marge has its own dial
     and ships small */
  margin-block: calc(var(--fx-space) * clamp(1.2rem, 3vw, 2.2rem))
    calc(var(--x-textgap, 0) * clamp(2rem, 5vw, 3.5rem));
  font-size: 1.05rem;
  line-height: 1.7;
}
/* photo gallery — justified rows: nothing cropped, every row flush left and right.
   Widths inside a row are set by flex-grow = aspect ratio, which makes all heights
   in the row equal automatically. */
.project-gallery .grow {
  display: flex;
  align-items: flex-start;
  gap: calc(var(--x-gap, 1) * var(--ts, 1) * clamp(0.6rem, 1.6vw, 1.1rem));
  margin-bottom: calc(var(--x-gap, 1) * var(--ts, 1) * clamp(0.6rem, 1.6vw, 1.1rem));
}
/* Rows are sized by HEIGHT now, set by layoutGallery, so a picture can never
   be taller than the cap however tall it is. The two dials live here:
   --gal-row is the height a row aims for and --gal-except the height it may
   never pass, both as a share of the window, so they hold on every screen. */
.project-gallery { --gal-row: 0.42; --gal-max: 0.66; }
.project-gallery .grow { justify-content: center; }
/* Films are laid out by the same justified rows as the pictures, so they take
   the same rule. They used to sit in a two-column band of their own, where a
   pair of 9:16 phone films each got half the page to stand in and came out as
   two narrow strips with a hole between them. */
.project-gallery .grow img,
.project-gallery .grow video {
  flex: 0 0 auto;
  height: 100%;
  width: auto;
  max-width: 100%;
  min-width: 0;
  margin: 0;
  background: var(--tile);
  display: block;
  object-fit: contain;
}

/* video embeds (Vimeo/YouTube iframe or local <video>) — paired grid */
.project-videos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
  gap: calc(var(--x-gap, 1) * var(--ts, 1) * clamp(0.6rem, 1.6vw, 1.1rem));
  margin-bottom: calc(var(--x-gap, 1) * var(--ts, 1) * clamp(0.6rem, 1.6vw, 1.1rem));
  align-items: start;
}
.project-video {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  overflow: hidden;
}
.project-video iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
/* Instagram reel embed (card) */
.project-video-ig { width: 100%; max-width: 480px; margin: 0 auto; }
.project-video-ig iframe { width: 100%; min-height: 600px; border: 0; background: #fff; }

/* The film and what it is have to be readable together. It used to be full
   page width: 1841x1036 at 1920, 96% of the window, so the description began
   227px BELOW the fold and you could not watch the film and read what it was
   at the same time. Capping the HEIGHT and letting the width follow puts the
   first lines of text on screen with it. Credits stay below the fold, which
   Jaco is happy with. */
.project-videos.is-main { grid-template-columns: 1fr; justify-items: center; }

/* further clips sit under the gallery at picture size, never full bleed */
/* further clips use the same grid, so they line up with the pictures under them */
.project-clips {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: calc(var(--x-gap, 1) * var(--ts, 1) * clamp(0.6rem, 1.6vw, 1.1rem));
  margin-bottom: calc(var(--x-gap, 1) * var(--ts, 1) * clamp(0.6rem, 1.6vw, 1.1rem));
}
/* A clip is shown at its OWN shape. It used to be poured into a fixed box,
   3/2 for a pair and 3/1 for a lone one, and then letterboxed to fit: a 16:9
   clip alone on the page became 1841 wide with the picture filling the middle
   1091 and 375px of black down either side, the largest and emptiest thing on
   the page. There is no box now, so there is nothing to letterbox against.
   The clip scales to the column and stops at the height cap, whichever comes
   first, and it can neither be cropped nor distorted. */
.project-clips { --clip: 0.46; }
.project-clips > * {
  overflow: hidden;
  margin: 0;
  min-width: 0;
  display: flex;
  justify-content: center;
}
.project-clips > *:last-child:nth-child(odd) { grid-column: 1 / -1; }
.project-clips video {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: calc(var(--clip) * 100dvh);
  display: block;
  margin-inline: auto;
  background: #000;
}

/* local video files fill their grid cell, natural height */
.project-video-file {
  display: block;
  width: 100%;
  height: auto;
  background: #000;
}

@media (max-width: 600px) {
  .project-clips { grid-template-columns: 1fr; }
  .project-clips > *:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

.project-nav {
  display: flex;
  justify-content: space-between;
  border-top: 1px solid var(--line);
  margin-top: calc(var(--fx-space) * clamp(1.8rem, 3.5vw, 3rem));
  padding-top: calc(2rem * var(--ts, 1));
  font-size: max(calc(12px * var(--fl, 1)), calc(0.85rem * var(--ts, 1)));
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.project-nav a { color: var(--muted); transition: color var(--t-fast); }
.project-nav a:hover { color: var(--fg); }

/* ==========================================================================
   About / Contact
   ========================================================================== */
.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: calc(var(--ts, 1) * clamp(2rem, 6vw, 6rem));
  align-items: start;
}
.about-portrait {
  /* The text runs BESIDE the portrait. At 42% of the column and 581px tall it
     left an empty 377x581 block to its right, under a rule that spans the
     whole column: a hole at the top of the page, next to Jaco's face. Floating
     it lets the bio fill that space and close up beneath. */
  float: left;
  margin-right: calc(var(--ts, 1) * clamp(1.25rem, 2.5vw, 2.25rem));
  shape-margin: 1rem;
  width: 100%;
  /* The portrait is a head-and-shoulders shot, 2:3 upright. At the full
     column width that is 897px across and 1346 tall at 1920, a poster that
     pushes the whole bio below the fold -- which is why a landscape venue
     photo had taken its place. A share of the COLUMN keeps it a portrait and
     stays proportional on its own, since the column already scales: 377px at
     1920, 756 on the 4K wall, 19.6% of the screen at both. */
  max-width: 42%;
  display: block;
  margin-bottom: calc(var(--ts, 1) * clamp(1.25rem, 3vw, 2rem));
  background: var(--tile);
}
.about-text { font-size: max(calc(16px * var(--fl, 1)), calc(var(--fx-text) * clamp(1.1rem, 1.8vw, 1.35rem))); line-height: 1.6; font-family: var(--font-serif); }
.about-text p { margin-bottom: 1em; }

/* The reading text on a project page runs larger than the site's base: the
   pages went wide and 17px drowned next to the pictures. */
.project-body { font-size: max(calc(16px * var(--fl, 1)), calc(var(--fx-text) * clamp(1.1rem, 1.15vw, 1.3rem))); line-height: 1.65; }

/* artist statement inside a project description — set apart from the info text */
.project-body .statement {
  font-style: italic;
  color: color-mix(in srgb, var(--fg) 78%, var(--bg));
  border-left: 2px solid var(--line);
  padding-left: 1.25em;
  font-size: 1.05em;
}

/* when a statement is present: info left, statement right */
.project-body.has-statement {
  grid-template-columns: 1fr 1fr;
  max-width: none;
  gap: calc(var(--ts, 1) * clamp(2rem, 5vw, 4.5rem));
  align-items: start;
}
.project-info p { margin-bottom: 1.1em; }
.project-info p:last-child { margin-bottom: 0; }
.project-statement .statement { margin-bottom: 1.1em; }
.project-statement .statement:last-child { margin-bottom: 0; }
@media (max-width: 820px) {
  .project-body.has-statement { grid-template-columns: 1fr; }
}

/* ---- press ---- */
#press .lead { color: var(--muted); margin-top: calc(1.25rem * var(--ts, 1)); }
/* Press as cards: each article carries its own picture, pulled from the
   article itself, with a black hairline on top like the approved board. */
.press-list {
  list-style: none;
  margin: calc(var(--ts, 1) * clamp(2rem, 5vw, 3.5rem)) 0 0;
  padding: 0;
  display: grid;
  /* The column COUNT is solved by fitPress() so the page never scrolls, and
     the cards then scale with the screen like everything else.

     This used to read minmax(max(250px * --fl, 300px * --ts), 1fr), which is
     a fixed pixel card: 294px at 1920 and 307px at 3840, so the card was 15%
     of a laptop screen and 8% of the 4K wall -- eleven columns of confetti,
     the exact ratio-of-1.0 the proportion law exists to stop. It broke the
     titles too: the card stayed ~300px while the title type scaled to 28px,
     so a long Dutch headline could not fit its own card on the wall.

     The fallback below is only for first paint and no-JS. */
  grid-template-columns: repeat(auto-fill, minmax(calc(300px * var(--ts, 1)), 1fr));
  gap: calc(var(--ts, 1) * clamp(1.2rem, 2.5vw, 2rem));
}
@media (max-width: 600px) { .press-list { grid-template-columns: 1fr; } }
.press-item { border-top: 1px solid var(--fg); }
.press-link { display: block; padding: calc(0.9rem * var(--ts, 1)) 0 calc(0.4rem * var(--ts, 1)); text-decoration: none; color: var(--fg); }
.press-media {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--tile);
  margin-bottom: calc(0.9rem * var(--ts, 1));
}
/* A cutting we do not have yet still holds its place, so one missing picture
   cannot make the whole row read as broken. */
.press-media--empty { background: var(--tile); }
.press-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.press-link:hover .press-media img { transform: scale(1.04); }
.press-head { display: flex; align-items: baseline; justify-content: space-between; gap: calc(1rem * var(--ts, 1)); }
.press-outlet { font-size: max(calc(11.5px * var(--fl, 1)), calc(var(--fx-label) * 0.8rem)); letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg); font-weight: 600; }
.press-date { font-size: max(calc(11.5px * var(--fl, 1)), calc(var(--fx-label) * 0.78rem)); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.press-title { font-size: max(calc(15px * var(--fl, 1)), calc(var(--fx-text) * 1.12rem)); line-height: 1.35; margin: calc(0.5rem * var(--ts, 1)) 0 0; font-weight: 600; }
.press-quote { color: var(--muted); font-family: var(--font-serif); font-style: italic; margin: calc(0.7rem * var(--ts, 1)) 0 0; line-height: 1.5; font-size: max(calc(13.5px * var(--fl, 1)), calc(var(--fx-text) * 0.92rem)); }
.press-cta { display: inline-block; margin-top: calc(0.9rem * var(--ts, 1)); font-size: max(calc(11.5px * var(--fl, 1)), calc(var(--fx-label) * 0.8rem)); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.press-link:hover .press-cta { color: var(--fg); }

.press-empty { color: var(--muted); padding: 1.5rem 0; }

/* The year column sizes to the widest YEAR IN THE LIST, not per row. It was
   a fixed 4.5rem, which is 35.7px at 1920: enough for "2025" and not for a
   RANGE, so the moment work spanning "2024-2026" went in every range broke
   over two lines. Making each row its own max-content grid fixed the wrap and
   broke the alignment instead -- single years got a 32px column and ranges
   71px, so no two entries started at the same place. The LIST owns the grid
   and the rows are display:contents, which is the only way one track can
   measure every row at once. Proportional on its own, since the type it
   measures is. */
.detail-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* year, kind, entry, link: the entry takes the slack so the arrows line up
     hard right, in a column of their own */
  grid-template-columns: max-content max-content 1fr max-content;
}
/* what a row IS: quiet, so the entry still leads */
.detail-list .k {
  color: var(--muted);
  white-space: nowrap;
  padding-right: calc(1.2rem * var(--ts, 1));
  font-size: max(calc(11px * var(--fl, 1)), calc(var(--fx-label) * 0.72rem));
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
#cv-filters { margin-bottom: calc(1rem * var(--ts, 1)); }
.cv-link {
  justify-self: end;
  padding-left: calc(1.2rem * var(--ts, 1));
  color: var(--muted);
  text-decoration: none;
  font-size: max(calc(12px * var(--fl, 1)), calc(var(--fx-label) * 0.8rem));
  transition: color var(--t-fast);
}
a.cv-link:hover { color: var(--fg); }
.detail-list li[hidden] { display: none; }
.detail-list li { display: contents; }
/* a group heading inside the CV: spans both tracks, and carries the rule so
   the section reads as a break rather than another row */
.detail-list .detail-head > * {
  grid-column: 1 / -1;
  padding-top: calc(2rem * var(--ts, 1));
  padding-bottom: calc(0.5rem * var(--ts, 1));
  border-bottom: 1px solid var(--fg);
  font-size: max(calc(11px * var(--fl, 1)), calc(var(--fx-label) * 0.72rem));
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg);
}
.detail-list li:first-child.detail-head > * { padding-top: 0; }
/* a one-line list: the live shows and the toolset */
.about-line {
  color: var(--muted);
  line-height: 1.7;
  margin: calc(0.6rem * var(--ts, 1)) 0 0;
  font-size: max(calc(14px * var(--fl, 1)), calc(var(--fx-text) * 0.95rem));
}
/* the rule under a row is drawn by its two cells; they touch, because the
   space between the columns is the year's padding rather than a grid gap,
   which would leave a gap in the line */
.detail-list li > * {
  padding-block: calc(0.85rem * var(--ts, 1));
  border-bottom: 1px solid var(--line);
  font-size: max(calc(14px * var(--fl, 1)), calc(var(--fx-text) * 0.95rem));
}
.detail-list .y {
  color: var(--muted);
  white-space: nowrap;
  padding-right: calc(1.5rem * var(--ts, 1));
}

.services-list { list-style: none; margin: 0; padding: 0; }
.services-list li { padding: calc(0.6rem * var(--ts, 1)) 0; border-bottom: 1px solid var(--line); }

.contact-big {
  font-family: var(--font-serif);
  font-size: max(calc(24px * var(--fl, 1)), calc(var(--ts, 1) * clamp(1.5rem, 4vw, 2.8rem)));
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
}
.contact-big a { border-bottom: 1px solid var(--line); transition: border-color var(--t-fast); }
.contact-big a:hover { border-color: var(--fg); }

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer {
  border-top: 1px solid var(--line);
  padding-block: calc(3rem * var(--ts, 1));
  margin-top: calc(var(--ts, 1) * clamp(2rem, 5vw, 4rem));
}
.footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: calc(1rem * var(--ts, 1)) calc(2rem * var(--ts, 1));
  justify-content: space-between;
  align-items: center;
  font-size: max(calc(12px * var(--fl, 1)), calc(0.82rem * var(--ts, 1)));
  color: var(--muted);
  letter-spacing: 0.04em;
}
.footer-inner a:hover { color: var(--fg); }

/* ==========================================================================
   Reveal animation
   ========================================================================== */
/* Pages cross-fade into each other; browsers without view transitions
   simply navigate as before. Reduced motion turns it off entirely. */
@view-transition { navigation: auto; }
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 0.32s; animation-timing-function: ease; }

/* Pictures drift in a touch softer and shallower than before; main.js adds a
   small stagger per element so a row settles like a hand of cards, not a slab */
.reveal { opacity: 0; transform: translateY(26px) scale(0.972); transition: opacity 1s ease, transform 1s cubic-bezier(0.22,1,0.36,1); }
.reveal.in { opacity: 1; transform: none; }
/* what is on screen at load arrives at once: a first impression may not
   spend a second and a half being invisible */
.reveal.at-once { transition-duration: 0.32s; transition-delay: 0s !important; }
/* if the script is slow, blocked or broken, the words still show up */
@media (scripting: none) { .reveal { opacity: 1; transform: none; } }
/* on the HOME anything carrying a picture skips the fade entirely: the
   tiles stand at full ink over the cloud from the first frame */
.page-home .reveal:has(img, video, canvas),
.page-home img.reveal,
.page-home video.reveal { opacity: 1; transform: none; transition: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
  html { scroll-behavior: auto; }
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 820px) {
  .grid.cols-3 { grid-template-columns: repeat(2, 1fr); }
  .two-col { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
  .nav-links {
    position: fixed;
    inset: var(--bar-h) 0 auto 0;
    background: var(--bg);
    flex-direction: column;
    gap: 0;
    padding: 1rem var(--gutter) 2rem;
    border-bottom: 1px solid var(--line);
    transform: translateY(-130%);
    /* hidden means hidden: the closed menu leaves the tab order too */
    visibility: hidden;
    transition: transform var(--t-med), visibility 0s var(--t-med);
    font-size: 1rem;
  }
  .nav-links.open { transform: none; visibility: visible; transition: transform var(--t-med); }
  .nav-links li { padding: 0.7rem 0; border-bottom: 1px solid var(--line); }
  .nav-toggle { display: block; }
  /* a finger needs a real target, and --fl is 0 here so the desktop floor
     does not apply: give the switch its own phone size. The word is dropped
     on a phone bar, where there is no room for it beside the hamburger. */
  .theme-toggle { padding: 0; width: 44px; height: 44px; justify-content: center; margin-left: auto; margin-right: 6px; border-color: transparent; }
  .theme-toggle svg { width: 20px; }
  .theme-label { display: none; }
  /* 3 squares across on phones — compact, legible one-line captions */
  .grid, .grid.cols-3 { grid-template-columns: repeat(3, 1fr); gap: 0.7rem 0.45rem; }
  .card-meta { gap: 0.1rem; margin-top: 0.4rem; }
  .card-title { font-size: 0.72rem; line-height: 1.25; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
  .card-cat { font-size: 0.62rem; letter-spacing: 0; text-transform: none; color: color-mix(in srgb, var(--fg) 74%, var(--bg)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

/* --------------------------------------------------------------------------
   Insight: an interview about the work, under the pictures
   -------------------------------------------------------------------------- */
.project-insight {
  border-top: 1px solid var(--line);
  margin-top: calc(var(--fx-space) * clamp(1.8rem, 3.5vw, 3rem));
  padding-top: calc(var(--ts, 1) * clamp(2rem, 4vw, 3.5rem));
}
.insight-head { max-width: calc(46rem * var(--ts, 1)); }
.insight-head h2 {
  font-size: max(calc(20px * var(--fl, 1)), calc(var(--ts, 1) * clamp(1.5rem, 3.2vw, 2.4rem)));
  line-height: 1.2;
  font-weight: 400;
  margin: 0.4rem 0 0;
  text-wrap: balance;
}
.insight-intro {
  color: var(--muted);
  margin: calc(1rem * var(--ts, 1)) 0 0;
  max-width: calc(38rem * var(--ts, 1));
}
.insight-qa {
  margin-top: calc(var(--ts, 1) * clamp(2rem, 4vw, 3rem));
  display: grid;
  gap: calc(var(--ts, 1) * clamp(1.75rem, 3vw, 2.75rem));
}
.insight-qa .qa {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: calc(0.75rem * var(--ts, 1));
  padding-top: calc(1.5rem * var(--ts, 1));
  border-top: 1px solid var(--line);
}
.insight-qa .qa:first-child { padding-top: 0; border-top: 0; }
.insight-qa .q {
  margin: 0;
  color: var(--muted);
  font-size: max(calc(12px * var(--fl, 1)), calc(0.82rem * var(--ts, 1)));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
}
.insight-qa blockquote {
  margin: 0;
  max-width: calc(42rem * var(--ts, 1));
}
.insight-qa blockquote + blockquote { margin-top: calc(1.1rem * var(--ts, 1)); }
.insight-qa blockquote p {
  margin: 0;
  font-size: max(calc(15px * var(--fl, 1)), calc(var(--fx-text) * clamp(1.05rem, 1.6vw, 1.25rem)));
  line-height: 1.55;
}
.insight-qa cite {
  display: block;
  margin-top: 0.5rem;
  font-style: normal;
  font-size: max(calc(12px * var(--fl, 1)), calc(0.8rem * var(--ts, 1)));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.insight-source {
  margin: calc(var(--ts, 1) * clamp(2rem, 4vw, 3rem)) 0 0;
  font-size: max(calc(13px * var(--fl, 1)), calc(0.85rem * var(--ts, 1)));
  color: var(--muted);
}
.insight-source a { border-bottom: 1px solid var(--line); }
.insight-source a:hover { color: var(--fg); }

@media (min-width: 900px) {
  .insight-qa .qa {
    grid-template-columns: calc(15rem * var(--ts, 1)) minmax(0, 1fr);
    gap: 0 calc(var(--ts, 1) * clamp(2rem, 4vw, 4rem));
  }
  .insight-qa .q { padding-top: 0.35rem; }
}

/* On a wide screen the 1500px text column leaves the pictures small with a lot
   of empty paper either side, so everything visual in a project steps out of the
   column and runs nearly the full window, keeping only the page gutter. Words
   stay in the column, where a line stays short enough to read.

   The width comes from --vw, which main.js sets from clientWidth: that excludes
   the scrollbar, where 100vw would include it and push the page sideways. The
   100vw fallback below is deliberately short by the gutter, so it is safe even
   before the script runs. */
.project-head > *,
.project-hero,
.project-videos.is-main,
.project-clips,
.project-gallery,
.project-body,
.project-credits,
.credit-notes,
.project-insight,
.project-nav {
  --gw: min(calc(var(--vw, 100vw) - 2 * var(--gutter)), var(--page-max));
  width: var(--gw);
  margin-inline: calc((100% - var(--gw)) / 2);
}
/* The film used to run exactly as wide as the text block under it: one
   width, one straight edge down the page. That is a real idea and it was
   Jaco's, but it cannot survive his other one. At full page width a 16:9 film
   is 1036px tall at 1920 and there are only 1018px under the header, so the
   description began 227px below the fold: you could watch the film or read
   what it was, never both. He asked for both, so the film is capped by HEIGHT
   and the width follows. It is narrower than the text now and centred, which
   is the price of the description being on screen with it.

   --film is the dial, a share of the window, so it holds on every screen. */
.project-videos.is-main { --film: 0.6; }
.project-videos.is-main .project-video-file {
  /* driven from the WIDTH, not the height, so the film still grows to the cap
     on a big screen. With width:auto it stopped at its own 1400px and sat at
     36% of the 4K wall looking lost; Jaco has already said he would rather
     have a slightly soft upscale than a small picture. */
  width: 100%;
  /* the cap is a HEIGHT, converted to a width through the film's own
     proportions. --vid-ar is set from the poster; 16:9 until it is known, and
     max-height holds the line in the meantime. */
  max-width: calc(var(--film) * 100dvh * var(--vid-ar, 1.7778));
  max-height: calc(var(--film) * 100dvh);
  height: auto;
  margin-inline: auto;
}
/* an embedded player keeps its 16:9 box, so its height is capped through the width */
.project-videos.is-main .project-video {
  max-width: calc(var(--film) * 100dvh * 16 / 9);
  margin-inline: auto;
}
/* An upright film obeys the same cap as a wide one. It had max-height:80vh of
   its own, which beat the cap and put flip-mode, schiedam-750 and sonaural at
   79-80% of the window with the description below the fold: exactly the
   problem the cap exists to prevent, on the three pages nobody thought to
   check. Width auto so it is never stretched. */
.project-videos.is-main .project-video-file.is-portrait {
  width: auto;
  max-width: 100%;
  max-height: calc(var(--film) * 100dvh);
  margin-inline: auto;
}
/* The cover fills the page width like every other block, so the page keeps
   one straight edge from top to bottom. A slightly soft upscale is the price,
   and Jaco chose the straight edge. */
.project-hero img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--tile);
}

/* A wide page: every column on it, bar and footer included, runs the full
   window less the gutter, so everything sits on the same two lines. The gutter
   is taken out of the width rather than added as padding, which is what puts
   the brand on the same line as the pictures below it. */
.page-wide .wrap {
  max-width: min(calc(var(--vw, 100vw) - 2 * var(--gutter)), var(--page-max));
  padding-inline: 0;
}
/* The project list has no cap: its cards stay one size and a wider screen buys
   more of them, so there is nothing to stop growing. A project page does stop,
   or a film and a line of text run the whole wall. */
.page-wide { --page-max: calc(6800px * var(--x-width, 1)); }  /* doubled on request: the studio screen carries it */
/* The bar and the footer sit at the window edge on every page, whatever the
   page's own column does: a header that moves between pages reads as a jump. */
.page-wide .site-header .wrap,
.page-wide .site-footer .wrap {
  max-width: calc(var(--vw, 100vw) - 2 * var(--gutter));
}
/* A project page is narrower: the film at the top sets the page width, and at
   full window it was simply too big. Everything on the page shares this width,
   so the one-straight-edge grid holds. */
.page-project { --page-max: calc(3600px * var(--x-width, 1)); }

/* The project list keeps its cards the size they have always been and fits as
   many across as the window allows, rather than stretching three of them over
   the whole screen. Desktop only: phones and tablets keep their own columns. */
@media (min-width: 1000px) {
  .page-wide #work-grid.grid {
    grid-template-columns: repeat(auto-fill, minmax(calc(540px * var(--ts, 1)), 1fr));
  }
}


/* The home selection: one row of six on desktop, and on a phone all nine as
   three tight square rows, the way an Instagram profile opens. */
@media (min-width: 1000px) {
  #featured-grid.grid { grid-template-columns: repeat(6, 1fr); }
  #featured-grid .card:nth-child(n + 7) { display: none; }
}
/* the home row sits tight: Jaco tuned its gaps to a fifth of the site's.
   The dial still works on top, everywhere else keeps the normal gap. */
#featured-grid.grid {
  gap: calc(var(--x-gap, 1) * var(--ts, 1) * 0.2 * clamp(1.25rem, 3.5vw, 3.5rem)) calc(var(--x-gap, 1) * var(--ts, 1) * 0.2 * clamp(1.25rem, 3vw, 2.5rem));
}

/* The home is one view, like the old site: point cloud hero, one row of
   works, the about band, all aiming to sit inside a single screen on
   desktop. Small screens simply scroll; nothing is hard-clamped. */
@media (min-width: 1000px) {
  /* the one-view home keeps its own vh-tuned sizes: whatever the site-wide
     text dials say, this page must fit the screen without scrolling.
     The footer counts too: the whole page is one flex column of exactly
     one viewport, the hero absorbing the slack. */
  .page-home { min-height: 100dvh; display: flex; flex-direction: column; }
  .page-home main { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
  .home-view { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; font-size: 17px; }
  .page-home .site-footer { margin-top: 0; padding-block: calc(1rem * var(--ts, 1)); }
  /* The statement is anchored to the VIEWPORT, not centred in what is left
     over. Leftover space shrinks faster than the screen does, so a centred
     block drifts upward on every smaller laptop -- it sat at 25% of the
     screen at 1920 against 30% at 4K, which put it in the middle of the
     cloud. Pinned at 22dvh the eyebrow holds 22.0-22.1% from 3840 down to
     1366. The slack now collects in the gap ABOVE the works row, which also
     keeps that row late enough (61-63%) that the cloud has resolved by the
     time it arrives. */
  .home-hero { flex: 0 0 auto; min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; padding-top: calc(22dvh - var(--bar-h)); padding-bottom: 0; }
  .home-works { margin-top: auto; }
  .home-hero .lead { margin-top: 0.6rem; }
  .home-hero .display { font-size: max(calc(30px * var(--fl, 1)), calc(var(--x-home-title, 1) * clamp(2rem, 4.2vh, 4.4rem))); margin-block: 0.4rem; }
  /* The tagline is the h1 and the only real statement on the home, so it is
     the biggest text on the page, in full ink, on a short measure. */
  .home-hero .lead {
    /* 34ch follows the FLOORED font, so on a laptop the block grew to 24.3%
       of the screen width against 19.9% on the wall -- that is the "too big"
       Jaco kept reporting. Capping the measure by screen share as well holds
       it at ~20% everywhere. The floor is 20px, not 24: at 1920 the honest
       proportional value is 19.6px and it wins, while 1512 and below still
       get a readable 20. */
    max-width: min(34ch, 22vw);
    font-size: max(calc(20px * var(--fl, 1)), calc(var(--x-home-lead, 2.35) * var(--ts, 1) * clamp(0.9rem, 1.5vh, 1.05rem)));
    line-height: 1.3;
    font-weight: 600;
    margin-block: calc(0.6rem * var(--ts, 1)) 0;
  }
  .home-works { padding-block: 0 clamp(0.6rem, 1.5vh, 1.4rem); }
  .home-works .card-cat { display: none; }
  .home-works .card-meta { margin-top: calc(0.45rem * var(--ts, 1)); }
  .home-works .card-title {
    font-size: max(calc(14px * var(--fl, 1)), calc(var(--x-home-cards, 1.5) * 0.85rem * var(--ts, 1)));
    /* one line, always: a wrapped title makes the row read ragged and the
       text look misaligned with the pictures */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    display: block;
  }
  /* the title's serif glyphs carry side bearing: a small negative pull
     puts the INK, not the glyph box, on the picture's left edge */
  .home-works .card-meta { margin-top: calc(0.55rem * var(--ts, 1)); margin-left: -0.09em; }
  .home-about { padding-block: 0 clamp(0.6rem, 1.5vh, 1.4rem); grid-template-columns: minmax(calc(140px * var(--ts, 1)), calc(180px * var(--ts, 1))) minmax(0, 1fr); }
  .home-about img { aspect-ratio: 3 / 2; }
  .home-about-text { font-size: max(calc(14.5px * var(--fl, 1)), calc(var(--x-home-about, 0.91) * var(--ts, 1) * clamp(0.9rem, 1.5vh, 1rem))); }
}
/* every text element on the home listens to its own dial; the line under
   the title is BIG and has its own dial, the small labels stay small */
.page-home .home-more,
.page-home .home-works-head { font-size: max(calc(12px * var(--fl, 1)), calc(var(--x-home-cards, 1.5) * var(--ts, 1) * 0.8rem)); }
.page-home .home-about .h-section,
.page-home .home-about h2 { font-size: max(calc(19px * var(--fl, 1)), calc(var(--x-home-about, 0.91) * var(--ts, 1) * clamp(1.3rem, 2.6vh, 1.9rem))); }
.home-works-head { display: flex; justify-content: space-between; align-items: baseline; border-top: 1px solid var(--line); padding-top: calc(0.6rem * var(--ts, 1)); margin-bottom: calc(0.6rem * var(--ts, 1)); }
.home-more { font-size: max(calc(12px * var(--fl, 1)), calc(0.8rem * var(--ts, 1))); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); border-bottom: 1px solid var(--line); padding-bottom: 2px; }
.home-more:hover { color: var(--fg); }
.home-about {
  display: grid;
  grid-template-columns: minmax(calc(140px * var(--ts, 1)), calc(180px * var(--ts, 1))) minmax(0, 1fr);
  gap: calc(var(--ts, 1) * clamp(1.5rem, 3vw, 3rem));
  align-items: center;
  border-top: 1px solid var(--line);
}
.home-about > div { padding-block: clamp(0.6rem, 1.5vh, 1.2rem); }
.home-about img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; display: block; background: var(--tile); }
.home-about-text { margin: calc(0.4rem * var(--ts, 1)) 0 calc(0.8rem * var(--ts, 1)); max-width: 62ch; font-size: max(calc(14.5px * var(--fl, 1)), calc(var(--x-home-about, 0.91) * var(--ts, 1) * clamp(0.95rem, 1.6vh, 1.08rem))); }
@media (max-width: 999px) { .home-about { grid-template-columns: 1fr; } }
@media (max-width: 600px) {
  #featured-grid.grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 3px;
  }
  #featured-grid .card-meta { display: none; }
}

/* The one-screen wall without captions: covers alone, tight, and the title
   comes back as an overlay when the cursor rests on a tile */
/* The whole work grid keeps the clean look Jaco chose: no captions under
   the tiles, the title lives INSIDE the image, appearing over a dark
   gradient on hover, in the one-screen wall and the browse grid alike. */
/* The wall's gaps as a share of the TILE, so the rhythm is the same whether
   the wall holds 43 works or 200 -- fit() solves the tile and writes the two
   gaps back in pixels. Taken from the wall as tuned at 43 works. */
#work-grid { --wall-gap-x: 0.133; --wall-gap-y: 0.186; }
#work-grid .card { position: relative; }
#work-grid .card-meta { display: none; }
#work-grid .card:hover .card-meta,
#work-grid .card:focus-visible .card-meta {
  display: flex;
  position: absolute;
  left: 0; right: 0; bottom: 0;
  margin: 0;
  padding: 0.5rem 0.6rem 0.45rem;
  background: linear-gradient(transparent, rgba(20, 20, 20, 0.78));
  z-index: 1;
}
#work-grid .card:hover .card-title,
#work-grid .card:focus-visible .card-title { color: #f7f6f4; font-size: calc(var(--fx-label) * 0.72rem); line-height: 1.25; }
#work-grid .card:hover .card-cat,
#work-grid .card:focus-visible .card-cat { color: rgba(247, 246, 244, 0.75); font-size: calc(var(--fx-label) * 0.6rem); }
#work-grid .card-media { background: var(--tile); }

/* Pages that fit the screen have no scroll to drive them, so the pictures
   breathe on their own: a very slow drift inside their crop, always moving,
   never arriving. Off for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .card-media img,
  .press-media img,
  .home-about img {
    animation: breathe 14s ease-in-out infinite alternate;
  }
  /* NOT the about portrait. The drift is meant for a picture sitting inside a
     crop frame, where it moves behind a hole that holds still and reads as
     life. The portrait has no frame: it IS the element, floated with the bio
     wrapped around it, so the same drift grew and slid the whole picture
     against text that never moves. That is Jaco's "images at about is moving
     all the time". A picture set into running text stays still. */
  .card-media img { animation-duration: 18s; }
}
@keyframes breathe {
  from { transform: scale(1); }
  to { transform: scale(1.07) translate(0.6%, -0.6%); }
}
/* the hover zoom must survive on top of the drift */
.card:hover .card-media img { animation-play-state: paused; transform: scale(1.07); }

/* The hover film sits over the cover and fades in once it is playing */
.card-media .card-preview {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.35s ease;
  pointer-events: none;
}
.card-media .card-preview.on { opacity: 1; }

/* Every tile is the same square, panorama covers included: the uneven
   double-width letterboxed tiles read as broken rows in the clean wall.
   A strip cover simply crops; the project page shows it whole. */

/* the point cloud player: a dark room for the scan, page-wide like the rest */
.project-pointcloud { margin-block: calc(var(--ts, 1) * clamp(1.5rem, 3vw, 2.5rem)); }
.pc-canvas {
  display: block;
  width: 100%;
  height: min(72vh, 780px);
  background: #0d0d0f;
  cursor: grab;
}
.pc-canvas:active { cursor: grabbing; }
.pc-label { margin: calc(0.6rem * var(--ts, 1)) 0 0; font-size: max(calc(13px * var(--fl, 1)), calc(0.85rem * var(--ts, 1))); color: var(--muted); }

/* press kit under the pictures, in the insight section's voice */
.project-presskit {
  border-top: 1px solid var(--line);
  margin-top: calc(var(--fx-space) * clamp(1.8rem, 3.5vw, 3rem));
  padding-top: calc(var(--ts, 1) * clamp(2rem, 4vw, 3.5rem));
}
.kit-quote { margin: calc(1.4rem * var(--ts, 1)) 0 0; max-width: calc(46rem * var(--ts, 1)); }
.kit-quote p { margin: 0; font-style: italic; font-size: max(calc(15px * var(--fl, 1)), calc(var(--fx-text) * 1.05rem)); line-height: 1.55; }
.kit-quote cite {
  display: block;
  margin-top: 0.4rem;
  font-style: normal;
  font-size: max(calc(11.5px * var(--fl, 1)), calc(0.78rem * var(--ts, 1)));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.kit-actions { display: flex; flex-wrap: wrap; gap: calc(0.7rem * var(--ts, 1)); margin-top: calc(1.8rem * var(--ts, 1)); }
.kit-actions .pill { text-decoration: none; display: inline-block; }

/* the expertise pillars: title left, thinking right, projects a quiet line */
.pillar {
  border-top: 2px solid var(--fg);
  padding-top: calc(1.2rem * var(--ts, 1));
  margin-bottom: calc(var(--ts, 1) * clamp(2.4rem, 4vw, 4rem));
  display: grid;
  grid-template-columns: minmax(200px, 1fr) minmax(0, 2fr);
  gap: calc(1rem * var(--ts, 1)) calc(var(--ts, 1) * clamp(2rem, 5vw, 6rem));
}
.pillar h2 { margin: 0; font-size: max(calc(19px * var(--fl, 1)), calc(var(--ts, 1) * clamp(1.3rem, 2vw, 1.8rem))); font-weight: 700; max-width: 14ch; }
.pillar-insight { font-size: max(calc(16px * var(--fl, 1)), calc(var(--fx-text) * clamp(1.05rem, 1.1vw, 1.2rem))); line-height: 1.65; max-width: 62ch; }
.pillar-insight p { margin: 0 0 1em; }
.pillar-insight p:last-child { margin-bottom: 0; }
.pillar-links { grid-column: 2; margin: 0.4rem 0 0; font-size: max(calc(13px * var(--fl, 1)), calc(0.85rem * var(--ts, 1))); color: var(--muted); }
.pillar-links a { color: var(--fg); border-bottom: 1px solid var(--line); }
.pillar-links a:hover { border-bottom-color: var(--fg); }
@media (max-width: 800px) {
  .pillar { grid-template-columns: 1fr; }
  .pillar-links { grid-column: 1; }
}


/* Regie: the whole project on one sticky full-bleed stage. Every picture
   is an absolutely placed item; the driver hands each a rect per beat and
   this one slow ease performs the recomposition. Nothing is scrubbed. */
.regie {
  position: relative;
  width: var(--vw, 100vw);
  margin-inline: calc((100% - var(--vw, 100vw)) / 2);
}
.regie-stage {
  position: sticky;
  top: calc(var(--bar-h) + 1px);
  height: calc(100dvh - var(--bar-h) - 1px);
  overflow: hidden;
}
.regie-item {
  position: absolute;
  left: 0;
  top: 0;
  will-change: transform, opacity;
}
.regie-stage.is-on .regie-item {
  transition:
    transform 1.15s cubic-bezier(0.22, 1, 0.36, 1),
    width 1.15s cubic-bezier(0.22, 1, 0.36, 1),
    height 1.15s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 1.15s cubic-bezier(0.22, 1, 0.36, 1);
}
.regie-item img,
.regie-item video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  animation: none;
}
/* the words arrive over the ghosted sheet, cascading in */
.regie-text {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.7s ease;
  padding-inline: clamp(1.2rem, 4vw, 4rem);
}
.regie-text.on {
  opacity: 1;
  pointer-events: auto;
}
.regie-text > div {
  max-width: 64ch;
  max-height: 100%;
  overflow-y: auto;
  font-size: max(calc(16px * var(--fl, 1)), calc(var(--fx-text) * clamp(1.05rem, 1.3vw, 1.35rem)));
  line-height: 1.7;
}
.regie-text .statement { margin-top: 1.2em; }
.regie-text .w { opacity: 0; transition: opacity 0.5s ease; animation: none !important; }
.regie-text.on .w { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .regie-stage.is-on .regie-item,
  .regie-text,
  .regie-text .w { transition: none; }
}
.story-counter {
  position: absolute;
  right: 0;
  bottom: 1.2rem;
  margin: 0;
  font-size: calc(0.75rem * var(--ts, 1));
  letter-spacing: 0.12em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.story-hint {
  position: absolute;
  left: 50%;
  bottom: 1.1rem;
  transform: translateX(-50%);
  margin: 0;
  font-size: calc(0.7rem * var(--ts, 1));
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The first Studio Schilp, closing the bio. It clears the floated portrait so
   it starts on its own line, and its width is capped as a share of the text
   column rather than in pixels, so it scales with everything else. */
.bio-sign {
  clear: both;
  margin: calc(2.2rem * var(--ts, 1)) 0 0;
}
.bio-sign img {
  display: block;
  /* The full width of the text column. A rem cap was wrong here: 30rem scales
     to 240px on a 1920 laptop against a column nearly 900px wide, so the sign
     came out as a stamp. The column is already proportional, so following it
     keeps the sign proportional too. */
  width: 100%;
  height: auto;
  background: var(--tile);
}
.bio-sign figcaption {
  margin-top: calc(0.6rem * var(--ts, 1));
  color: var(--muted);
  font-size: max(calc(12px * var(--fl, 1)), calc(0.78rem * var(--ts, 1)));
  line-height: 1.5;
}
@media (max-width: 600px) {
  .bio-sign { margin-top: 1.6rem; }
  .bio-sign figcaption { font-size: 0.8rem; }
}
