/* ═══════════════════════════════════════════════════════════════
   FINALLY MONDAY — spatial & experience design studio, Berlin
   ───────────────────────────────────────────────────────────────
   All brand colour lives in the :root block below. To re-skin the
   entire site, edit those values and nothing else — the 3D hero
   object reads its colours from the same variables.
   ═══════════════════════════════════════════════════════════════ */

:root {
  /* ── BRAND COLOUR ─────────────────────────────────────────── */
  --c-paper:        #F4EEEE;  /* ground                           */
  --c-ink:          #574142;  /* plum-brown — body text, 8.2:1    */
  --c-blush:        #D19FA0;  /* dusty rose — decorative          */
  --c-cream:        #F1F2CF;  /* pale chartreuse — surface/accent */
  --c-accent:       #AD3537;  /* crimson                          */

  /* Derived tints. Everything below is mixed from the five above. */
  --c-paper-2:      #FAF6F6;  /* raised surface                   */
  --c-ink-2:        #7A6162;  /* secondary text — 4.9:1 on paper  */
  --c-blush-2:      #E8D2D3;  /* softened rose for washes         */
  --c-accent-deep:  #8E2A2C;  /* crimson, darkened for hovers     */
  --c-night:        #574142;  /* dark section ground              */
  --c-night-2:      #6A5051;  /* raised surface on dark           */
  --c-line:         #E0D2D2;  /* hairline on paper                */
  --c-line-dark:    #75595A;  /* hairline on night                */
  --c-on-accent:    #F4EEEE;  /* text on crimson — 5.5:1          */
  --c-on-night:     #F4EEEE;  /* text on plum — 8.2:1             */
  --c-accent-night: #F1F2CF;  /* accent text on plum — 8.3:1      */

  /* Contrast rules this palette obeys:
     · --c-accent  is 5.5:1 on paper → safe as text, including small
     · --c-accent  is 1.5:1 on night → NEVER used on the dark bands;
                   --c-accent-night carries the accent role there
     · --c-blush   is 1.9:1 on paper → decorative fills only, never text
     · --c-ink-2   is 4.9:1 → the lightest the scroll-highlight may go   */

  /* ── TYPE ─────────────────────────────────────────────────────
     Jost variable (wght 200–700) — geometric sans in the Futura
     lineage. The About highlight animates the weight axis, so the
     variable range matters.                                       */
  --f-display: "Jost", "Futura", "Century Gothic", "Avenir", sans-serif;
  --f-body:    "Jost", "Futura", "Century Gothic", "Avenir", sans-serif;

  /* ── SPACE ────────────────────────────────────────────────── */
  --s-1: 0.5rem;  --s-2: 1rem;   --s-3: 1.5rem;  --s-4: 2rem;
  --s-5: 3rem;    --s-6: 4rem;   --s-7: 6rem;    --s-8: 9rem;

  --gutter: clamp(1.25rem, 4vw, 4.5rem);
  --maxw: 1680px;

  /* ── MOTION ───────────────────────────────────────────────── */
  --e-out: cubic-bezier(0.22, 1, 0.36, 1);
  --e-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --t-fast: 160ms;
  --t-mid: 320ms;
  --t-slow: 620ms;

  --z-badge: 40;  --z-header: 60;  --z-menu: 80;  --z-skip: 100;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--c-paper);
  color: var(--c-ink);
  font-family: var(--f-body);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3, h4, p, ul, ol, figure { margin: 0; }
ul, ol { list-style: none; padding: 0; }
img, svg, canvas { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; }

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

.skip-link {
  position: absolute;
  left: var(--gutter);
  top: -100%;
  z-index: var(--z-skip);
  background: var(--c-ink);
  color: var(--c-paper);
  padding: 0.9rem 1.4rem;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  transition: top var(--t-fast) var(--e-out);
}
.skip-link:focus { top: var(--s-2); }

/* Shared micro-label voice. */
.proj__top,
.proj__scope,
.cta__key,
.nav-toggle__label,
.site-nav a,
.edge-badge span,
.site-footer__bar {
  font-weight: 500;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* ═══ HEADER ═══════════════════════════════════════════════════ */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-3);
  padding: var(--s-3) var(--gutter);
  /* No bar, no blur, no rule beneath. The words sit straight on whatever
     is behind them and the only thing that changes on scroll is their
     colour, inverting as they cross the dark sections. */
  transition: color var(--t-mid) var(--e-out);
}
/* Nothing sits behind the nav at all. Where it crosses a pale project
   shot in the work section the cream words fade into it — that is the
   intended behaviour, not an oversight. */
.site-header.is-dark { color: var(--c-on-night); }

.site-nav ul { display: flex; gap: var(--s-4); }
.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-decoration: none;
  position: relative;
}
.site-nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 12px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--t-mid) var(--e-out);
}
.site-nav a:hover::after,
.site-nav a:focus-visible::after { transform: scaleX(1); }

.nav-toggle {
  display: none;
  align-items: center;
  gap: 0.6rem;
  min-height: 44px;
  min-width: 44px;
  padding: 0 0.4rem;
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  font-family: inherit;
}
.nav-toggle__bars { display: grid; gap: 5px; width: 22px; }
.nav-toggle__bars i {
  display: block;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--t-mid) var(--e-out);
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars i:first-child { transform: translateY(3.25px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars i:last-child  { transform: translateY(-3.25px) rotate(-45deg); }

.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  background: var(--c-ink);
  color: var(--c-on-night);
  display: grid;
  align-content: center;
  gap: var(--s-5);
  padding: var(--gutter);
}
.mobile-nav[hidden] { display: none; }
.mobile-nav a {
  font-weight: 700;
  font-size: clamp(2.5rem, 12vw, 4.5rem);
  letter-spacing: -0.01em;
  line-height: 1.12;
  text-transform: uppercase;
  text-decoration: none;
  display: block;
  padding-block: 0.1em;
}
.mobile-nav__meta {
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.75;
}

/* ═══ EDGE BADGE ═══════════════════════════════════════════════ */
.edge-badge {
  position: fixed;
  right: 0;
  top: 50%;
  z-index: var(--z-badge);
  transform: translateY(-50%);
  background: var(--c-cream);
  color: var(--c-ink);          /* 8.3:1 on cream */
  padding: var(--s-3) 0.5rem;
  pointer-events: none;
}
.edge-badge span {
  writing-mode: vertical-rl;
  display: block;
  font-size: 0.63rem;
}

/* Section headings are not shown, but each section still carries one so
   the document outline stays intact for screen readers. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ═══ HERO ═════════════════════════════════════════════════════ */
.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding: calc(var(--s-8) + 2vw) 0 var(--s-7);
  isolation: isolate;
  /* The disc below is decoration and must never widen the document.
     `clip` rather than `hidden`: it crops without making this a scroll
     container. Horizontal only, so nothing vertical is affected. */
  overflow-x: clip;
}

/* The blush disc the portfolio drawings always sit on. */
.hero::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(96vw, 1100px);
  /* Capped to the viewport so the disc never runs past the top of the
     page, where its fade would be cut off. */
  height: min(96vw, 1100px, 100svh);
  transform: translate(-50%, -50%);
  /* `ellipse closest-side` measures the gradient against the nearer
     edge, so the fade reaches transparent inside the box on every side.
     A centred origin keeps the distance equal top and bottom — off-centre
     it completes on the long side and gets clipped on the short one. */
  background: radial-gradient(ellipse closest-side at 50% 50%,
              var(--c-blush-2) 0%,
              color-mix(in srgb, var(--c-blush-2) 48%, transparent) 58%,
              transparent 92%);
  z-index: 0;
  pointer-events: none;
}

.hero__type {
  display: grid;
  align-content: center;
  justify-items: center;
  padding-inline: var(--gutter);
  position: relative;
  z-index: 2;
}

.hero__title {
  font-weight: 700;
  font-size: clamp(3.1rem, 16.5vw, 15.5rem);
  line-height: 0.84;
  letter-spacing: -0.025em;
  text-transform: uppercase;
  display: grid;
  justify-items: center;
  text-align: center;
}
.hero__line { display: block; }
.hero__line--mid { position: relative; z-index: 1; }

.hero__slogan {
  margin-top: clamp(1.25rem, 2.6vw, 2.25rem);
  font-weight: 400;
  font-size: clamp(0.78rem, 1.5vw, 1.12rem);
  letter-spacing: 0.32em;
  text-transform: uppercase;
  text-align: center;
  color: var(--c-ink-2);
  position: relative;
  z-index: 3;
}

.hero__object {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(58vw, 780px);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  /* Behind the name: translucent, but never at the cost of legibility. */
  z-index: 1;
  pointer-events: none;
}
.hero__object canvas, .hero__fallback { width: 100%; height: 100%; }
.hero__fallback { display: none; opacity: 0.55; }
.no-webgl .hero__object canvas { display: none; }
.no-webgl .hero__fallback { display: block; }

/* ═══ 01 · ABOUT — full-screen scroll-driven highlight ═════════ */
.about { padding-bottom: 0; }

/* Tall stage: the statement pins for its whole length, and the
   capabilities section only arrives once every word is lit. */
.about__stage { height: 260vh; position: relative; }
.about__sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  align-content: center;
  justify-content: center;
  padding: var(--s-7) var(--gutter);
}

.about__statement {
  font-size: clamp(1.85rem, 5.6vw, 5.4rem);
  line-height: 1.1;
  letter-spacing: -0.015em;
  max-width: 19ch;
  text-wrap: pretty;
}
/* Words are wrapped by JS, which then writes a weight and colour per
   word every frame — a gradient front rather than a hard on/off edge.
   These are the unlit values, and the state the page holds with
   JavaScript disabled. */
.about__statement .w {
  font-weight: 250;
  color: var(--c-ink-2);
}

/* ═══ 02 · CAPABILITIES — annotated wave ═══════════════════════ */
.caps { padding-bottom: var(--s-8); }
.caps__inner {
  max-width: var(--maxw);
  margin-inline: auto;
  padding: var(--s-6) var(--gutter) 0;
}
.caps__display {
  font-weight: 300;
  font-size: clamp(1.5rem, 4vw, 3.4rem);
  line-height: 1.12;
  letter-spacing: -0.015em;
  text-align: center;
  text-wrap: balance;
  max-width: 26ch;
  margin-inline: auto;
}
.caps__sub {
  font-size: clamp(0.98rem, 1.6vw, 1.3rem);
  line-height: 1.5;
  color: var(--c-ink-2);
  text-align: center;
  text-wrap: balance;
  max-width: 46ch;
  margin-inline: auto;
}

/* The line and its annotations share one box, so percentage-placed
   labels track the curve exactly as it scales. */
.wave {
  position: relative;
  width: 100%;
  /* Matches the drawn curve's own proportions (2.915:1) so the line is
     never stretched out of shape. */
  aspect-ratio: 1600 / 549;
  /* Extra vertical room: the topmost and bottommost labels are centred
     on the curve's extremes and so overhang the box by half a line. */
  margin: clamp(var(--s-5), 5vw, var(--s-7)) auto;
  container-type: inline-size;
  isolation: isolate;
}

.wave__line {
  width: 100%;
  height: 100%;
  overflow: visible;
  position: relative;
  z-index: 1;
}

/* Drawn on as the section arrives. pathLength="1" normalises the
   curve so the dash maths is exact regardless of its real length. */
.wave__line path {
  /* No dash here on purpose. A pathLength of 1 normalises a dash only
     if the browser carries it through <use>, which Chromium does and
     Firefox has long got wrong — and a pathLength that does not apply
     turns a dasharray of 1 into a line broken into one-unit fragments.
     So the dash is measured in real units and set in JS, and with no
     JS at all the line simply rests solid. */
}
.js .wave.is-in .wave__line path {
  animation: wave-draw 1.9s var(--e-out) forwards;
}
@keyframes wave-draw { to { stroke-dashoffset: 0; } }

/* Without JS, or once drawn, the line simply sits there. */
html:not(.js) .wave__line path { stroke-dashoffset: 0; }

.wave__labels { margin: 0; }
.wave__labels li {
  position: absolute;
  left: var(--x);
  top: var(--y);
  transform: translate(-50%, -50%);
  z-index: 2;
  width: max-content;
  /* The labels carry the line breaks from the PSD, so they set their own
     width — an automatic wrap here would fight those breaks. */
  max-width: none;
  /* Sized against the wave box rather than the viewport, so the
     annotations keep their proportion to the curve as it narrows. */
  font-size: clamp(0.75rem, 1.18cqw, 1.12rem);
  line-height: 1.2;
  color: var(--c-accent);      /* 5.5:1 on paper */
  text-align: center;
  text-wrap: balance;
  /* A halo so the curve passes behind the words rather than striking
     through them — the labels sit within a few pixels of the line by
     design, and crimson-on-crimson would be unreadable. Mixed toward
     cream so it disappears into the glow instead of reading as a pink
     patch on it. */
  --halo: color-mix(in srgb, var(--c-cream) 70%, var(--c-paper));
  text-shadow:
     0 0 7px var(--halo),  0 0 7px var(--halo),
     0 0 7px var(--halo),  0 0 12px var(--halo),
     0 0 12px var(--halo);
}

/* Where each label sits, as a percentage of the wave box. Read out of
   the text layers in Untitled-1.psd and mapped through the same
   transform as the curve. They live here rather than on the elements
   because inline style attributes are blocked by the site’s CSP.
   The order is the list order in index.html, left to right. */
.wave__labels li:nth-child(1) { --x: 9.4%;  --y: 94.6%; }  /* architecture           */
.wave__labels li:nth-child(2) { --x: 19.1%; --y: 57%;   }  /* interior design        */
.wave__labels li:nth-child(3) { --x: 30.9%; --y: 1.1%;  }  /* spatial concepts       */
.wave__labels li:nth-child(4) { --x: 41.6%; --y: 29.4%; }  /* creative direction     */
.wave__labels li:nth-child(5) { --x: 49.9%; --y: 84%;   }  /* graphic & visual design*/
.wave__labels li:nth-child(6) { --x: 66.4%; --y: 24.3%; }  /* exhibition / set design*/
.wave__labels li:nth-child(7) { --x: 79.4%; --y: 61.6%; }  /* brand environments     */
.wave__labels li:nth-child(8) { --x: 96.6%; --y: 10.5%; }  /* installations          */

/* Each label carries its own soft cream glow, so it reads against a
   warmer ground wherever it happens to sit on the curve. Sized in em,
   so the glow scales with the label rather than the container.
   Cream and paper are within a hair of the same lightness, so this
   lifts the area without touching the labels' 5.5:1 contrast. */
.wave__labels li::before {
  content: "";
  position: absolute;
  inset: -1.2em -1.5em;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 50%,
    var(--c-cream) 0%,
    color-mix(in srgb, var(--c-cream) 72%, transparent) 34%,
    color-mix(in srgb, var(--c-cream) 28%, transparent) 60%,
    transparent 78%);
}

/* ═══ 03 · WORK ════════════════════════════════════════════════ */
.work {
  background: var(--c-night);
  color: var(--c-on-night);
  padding-block: calc(var(--s-8) * 1.35);
  overflow: hidden;
}
.work__inner {
  position: relative;
  max-width: var(--maxw);
  margin-inline: auto;
}

/* The thread: the drawn curve on its side, stretched down the section
   and drawn progressively as it is scrolled. `preserveAspectRatio="none"`
   lets it span any height; `vector-effect` keeps the stroke even. */
.work__thread {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: min(48vw, 640px);
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
}
.work__thread svg { width: 100%; height: 100%; }
.work__thread path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  opacity: 0.5;
}
/* Without JS there is nothing to scrub it, so it simply sits there. */
html:not(.js) .work__thread path { stroke-dashoffset: 0; }

/* ── The shots: one image per project, staggered down the page ─── */
.shots {
  position: relative;
  z-index: 1;
  display: grid;
  gap: clamp(var(--s-7), 16vh, 15rem);
  padding-inline: var(--gutter);
}

.shot__link { display: block; text-decoration: none; color: inherit; }

.shot__media {
  margin: 0;
  background: var(--c-paper);
  border: 1px solid var(--c-line-dark);
  overflow: hidden;
}
.shot__media img {
  width: 100%;
  transition: transform var(--t-slow) var(--e-out);
}
.shot__link:hover .shot__media img,
.shot__link:focus-visible .shot__media img { transform: scale(1.02); }

/* For images whose background has been cut away: no frame, no ground —
   the artwork sits directly on the section. */
.shot__media--bare,
.plate__media--bare {
  background: none;
  border: 0;
  overflow: visible;
}

/* ── Rhythm: single images placed off-axis, and one pair shown
      together at different heights. ───────────────────────────── */
.shot--right  { width: min(46%, 700px); margin-left: auto; }
.shot--centre { width: min(54%, 860px); margin-inline: auto; }
.shot--left   { width: min(42%, 640px); margin-right: auto; }

.shot--pair { width: min(92%, 1240px); margin-inline: auto; }
.shot--pair .shot__link {
  display: grid;
  grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
  gap: clamp(var(--s-4), 5vw, var(--s-7));
  align-items: start;
}
/* The second image drops, so the pair reads as two moments rather
   than one row. */
.shot--pair .shot__media:last-child { margin-top: clamp(var(--s-6), 11vw, 13rem); }

/* Reveal-on-scroll — visible by default, JS opts in. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity var(--t-slow) var(--e-out), transform var(--t-slow) var(--e-out);
}
.js [data-reveal].is-in { opacity: 1; transform: none; }

/* ═══ 04 · CONTACT ═════════════════════════════════════════════ */
.cta {
  position: relative;
  isolation: isolate;
  background: var(--c-night);
  color: var(--c-on-night);
  padding-block: var(--s-8) var(--s-7);
}

/* Studio photograph behind the closing section. The image is already
   duotoned dark; this scrim takes the brightest point down to roughly
   11:1 against the paper and cream text, so the photo reads as ground
   rather than competing with the words. */
.cta__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
}
/* <picture> is inline by default; without this the img has no definite
   box to fill in some engines. */
.cta__bg picture {
  display: block;
  width: 100%;
  height: 100%;
}
.cta__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Keeps the paper cityscape in frame however the section crops. */
  object-position: center 52%;
}
.cta__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--c-night) 62%, transparent);
}

/* The closing thought, set at the same quiet scale as the capabilities
   heading rather than as a slab of display type. */
.cta__closing {
  font-weight: 300;
  font-size: clamp(1.4rem, 3.7vw, 3.05rem);
  line-height: 1.14;
  letter-spacing: -0.015em;
  text-align: center;
  text-wrap: balance;
  max-width: 26ch;
  box-sizing: content-box;   /* gutters sit outside the measure */
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-bottom: var(--s-7);
}
.cta__closing span { display: block; }
.cta__closing-turn { color: var(--c-accent-night); }

.cta__details {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: var(--s-5);
  border-top: 1px solid var(--c-line-dark);
  padding-top: var(--s-5);
  margin-inline: var(--gutter);
}
.cta__key { color: var(--c-accent-night); margin-bottom: var(--s-2); }
.cta__val { opacity: 0.85; }
.cta__mail {
  font-weight: 500;
  font-size: clamp(1.05rem, 2.2vw, 1.6rem);
  text-decoration: none;
  border-bottom: 1px solid var(--c-accent-night);
  padding-bottom: 3px;
  display: inline-block;
  transition: color var(--t-fast) var(--e-out);
}
.cta__mail:hover { color: var(--c-accent-night); }
.cta__social { display: grid; }
.cta__social a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-decoration: none;
  opacity: 0.85;
  transition: opacity var(--t-fast) var(--e-out), color var(--t-fast) var(--e-out);
}
.cta__social a:hover { opacity: 1; color: var(--c-accent-night); }

.cta__signoff {
  margin-top: var(--s-7);
  padding-inline: var(--gutter);
  font-weight: 300;
  font-size: clamp(1.25rem, 3vw, 2.3rem);
  line-height: 1.1;
  letter-spacing: -0.01em;
  text-align: center;
  color: var(--c-accent-night);
}

/* ═══ PROJECT PAGES ════════════════════════════════════════════ */
.project {
  background: var(--c-night);
  color: var(--c-on-night);
  padding-top: calc(var(--s-8) + 2vw);
}
.project__inner {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.project__eyebrow {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s-3);
  font-weight: 500;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-accent-night);
}
.project__title {
  margin-top: var(--s-3);
  max-width: 17ch;
  font-weight: 700;
  font-size: clamp(2rem, 6vw, 5rem);
  line-height: 1;
  letter-spacing: -0.028em;
  text-transform: uppercase;
}

.project__intro {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 0.8fr);
  gap: var(--s-6);
  margin-top: var(--s-6);
  padding-top: var(--s-4);
  border-top: 1px solid var(--c-line-dark);
}
.project__lede p { max-width: 62ch; opacity: 0.88; }
.project__lede p + p { margin-top: var(--s-3); }

.project__facts { display: grid; gap: var(--s-3); align-content: start; }
.project__facts dt {
  margin-bottom: 0.3rem;
  font-weight: 500;
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-accent-night);
}
.project__facts dd { margin: 0; opacity: 0.88; }

/* Plates: the portfolio sheets, framed on the dark ground. */
/* `minmax(0, 1fr)` and the min-width below stop a grid item from being
   sized by its content — without them the oversized board pushes the
   column wide and the whole page scrolls sideways instead of the plate. */
.plates {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-6);
  margin-top: var(--s-8);
}
.plate { margin: 0; min-width: 0; }
.plate__media { min-width: 0; }
.plate__media {
  background: var(--c-paper);
  border: 1px solid var(--c-line-dark);
  overflow: hidden;
}
.plate__media img { width: 100%; }
.plate figcaption {
  margin-top: var(--s-2);
  font-weight: 500;
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0.62;
}

/* ── Wide plates: the diploma boards ───────────────────────────
   These are 2.83:1 and dense with drawing. Fitted to the page they
   are ~560px tall and nothing on them can be read, so instead they
   are shown at close to full height and scrolled sideways inside
   their own frame. The page itself never scrolls horizontally. */
.plate--wide .plate__media {
  overflow-x: auto;
  overflow-y: hidden;
  /* Stops a trackpad swipe from triggering browser back-navigation. */
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--c-accent-night) var(--c-night-2);
}
.plate--wide .plate__media img {
  height: min(78vh, 880px);
  width: auto;
  /* Beats both the global img reset and .plate__media img. */
  max-width: none;
}
.plate--wide .plate__media::-webkit-scrollbar { height: 10px; }
.plate--wide .plate__media::-webkit-scrollbar-track { background: var(--c-night-2); }
.plate--wide .plate__media::-webkit-scrollbar-thumb {
  background: var(--c-accent-night);
  border-radius: 5px;
}
/* The container is focusable so it can be scrolled from the keyboard;
   the caption says so, and each one carries a matching aria-label. */
.plate--wide figcaption::after {
  content: " — scroll sideways";
  color: var(--c-accent-night);
}

.project__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-5);
  justify-content: space-between;
  align-items: baseline;
  margin-top: var(--s-8);
  padding-top: var(--s-4);
  padding-bottom: var(--s-8);
  border-top: 1px solid var(--c-line-dark);
}
.project__nav a { text-decoration: none; }
.project__back {
  font-weight: 500;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.85;
}
.project__back:hover { opacity: 1; color: var(--c-accent-night); }
.project__next {
  font-weight: 700;
  font-size: clamp(1.15rem, 3vw, 2.2rem);
  letter-spacing: -0.02em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--c-accent-night);
  padding-bottom: 3px;
  transition: color var(--t-fast) var(--e-out);
}
.project__next:hover { color: var(--c-accent-night); }

@media (max-width: 860px) {
  .project__intro { grid-template-columns: 1fr; gap: var(--s-4); }
  /* A full-height board would swallow a phone screen whole. */
  .plate--wide .plate__media img { height: min(62vh, 560px); }
}

/* ═══ THE REEL ═════════════════════════════════════════════════
   Half a screen of details, then the boards at full screen height.
   The page is therefore one and a half screens tall and then ends;
   from there the boards run sideways in their own native scroller.
   No pinning, no hijacking — the browser does the work. */
.reel { position: relative; }

/* The reel's own head carries the top spacing, so the page wrapper
   must not add its usual padding on top of it. */
.project--reel { padding-top: 0; }

/* On a reel page the nav belongs to the details, not to the window: it
   scrolls away with them, leaving the boards the whole screen. */
.site-header--inline {
  position: absolute;
  top: 0;
}

/* The details: identity on the left, the written description on the
   right. Sized so that this plus the full-height strip below keeps the
   page a little over one and a half screens and then stops.

   Two inks alternate throughout — butter yellow (--c-accent-night) for
   the title, the disciplines and the field labels; pale pink
   (--c-on-night) for the values and the description. Both sit above
   8:1 on the plum. */
.reel__head {
  height: 66svh;
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(var(--s-4), 4vw, var(--s-7));
  align-content: center;
  /* The nav sits absolute in the first ~92px of the page, so the top
     padding has to clear it or the right-hand column runs underneath. */
  padding: calc(var(--s-7) + 1vw) var(--gutter) var(--s-4);
  max-width: var(--maxw);
  margin-inline: auto;
  width: 100%;
}

.reel__title {
  max-width: 19ch;
  font-weight: 300;
  font-size: clamp(1.6rem, 3.5vw, 3.1rem);
  line-height: 1.05;
  letter-spacing: -0.015em;
  color: var(--c-accent-night);
}
.reel__disciplines {
  margin-top: var(--s-2);
  font-size: clamp(0.85rem, 1.2vw, 1.05rem);
  line-height: 1.4;
  color: var(--c-accent-night);
}

.reel__meta-list {
  margin-top: clamp(var(--s-4), 3vw, var(--s-6));
  display: grid;
  gap: 0.1rem;
  font-size: clamp(0.8rem, 1.05vw, 0.98rem);
  line-height: 1.45;
  color: var(--c-on-night);
}
.reel__meta-list span { color: var(--c-accent-night); }

/* A second block of facts — who made what — sitting under the first
   with a line's worth of air between them, as on the title page. */
.reel__meta-list--credits { margin-top: var(--s-3); }

.reel__about {
  align-self: start;
  columns: 2;
  column-gap: clamp(var(--s-4), 3vw, var(--s-6));
  font-size: clamp(0.76rem, 0.92vw, 0.92rem);
  line-height: 1.48;
  color: var(--c-on-night);
}
.reel__about p { margin-bottom: var(--s-3); }
.reel__about p:last-child { margin-bottom: 0; }

/* Credits and the Impressum. The page carries no footer, so they sit
   in the spread's own white margin under the last drawing — the strip
   simply ends on the artwork, with no panel after it. Ink, not the
   butter yellow: that would vanish against the paper. */
.reel__track figure.reel__last { position: relative; }

.reel__end {
  position: absolute;
  right: clamp(var(--s-5), 7vw, var(--s-7));
  bottom: clamp(var(--s-4), 5vh, var(--s-6));
  display: grid;
  gap: var(--s-3);
  font-size: clamp(0.82rem, 1.05vw, 1rem);
  line-height: 1.5;
  color: var(--c-ink);
}
.reel__end a {
  text-decoration: none;
  border-bottom: 1px solid currentColor;
}

/* Project 01's spreads leave a wide paper margin under the drawings, so
   its credits sit in it. Photographs are full-bleed and have no such
   margin — the block on top of one would just be text over a picture —
   so there it takes a frame of its own at the end of the strip, on the
   plum, in the butter yellow. */
.reel__end--panel {
  position: static;
  width: min(70vw, 420px);
  flex: none;
  /* Low in the frame, level with where project 01's block sits. */
  align-content: end;
  padding: var(--s-6) clamp(var(--s-4), 4vw, var(--s-6));
  color: var(--c-accent-night);
}

/* ── Variant 1 (project-01-variant-1.html) ──────────────────────
   The earlier layout: a half-screen head with a single row of facts,
   and the description carried in a panel at the end of the strip.
   Kept so that page still works while variant 2 is the live one. */
.reel--v1 .reel__head {
  height: 50svh;
  grid-template-columns: 1fr;
  align-content: space-between;
  gap: var(--s-1);
  padding-bottom: var(--s-1);
}
.reel--v1 .reel__title {
  margin-top: var(--s-1);
  max-width: 22ch;
  font-weight: 700;
  font-size: clamp(1.35rem, 3.3vw, 2.9rem);
  line-height: 1.02;
  letter-spacing: -0.028em;
  text-transform: uppercase;
  color: var(--c-on-night);
}
.reel--v1 .reel__facts {
  display: flex;
  gap: var(--s-2) clamp(var(--s-3), 3vw, var(--s-5));
  padding-top: var(--s-2);
  border-top: 1px solid var(--c-line-dark);
}
.reel--v1 .reel__facts > div { flex: 1 1 0; min-width: 0; max-width: 28ch; }
.reel--v1 .reel__facts dt {
  margin-bottom: 0.15rem;
  font-weight: 500;
  font-size: 0.58rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-accent-night);
}
.reel--v1 .reel__facts dd { margin: 0; font-size: 0.78rem; line-height: 1.35; opacity: 0.86; }

.reel__panel {
  width: min(78vw, 720px);
  display: grid;
  align-content: center;
  gap: var(--s-5);
  padding: var(--s-6) clamp(var(--s-4), 4vw, var(--s-6));
  overflow-y: auto;
}
.reel__panel .project__nav { margin-top: 0; padding-bottom: 0; }
.reel__meta {
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--c-line-dark);
  font-weight: 500;
  font-size: 0.64rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0.7;
}
.reel__meta a { text-decoration: none; border-bottom: 1px solid currentColor; }

/* The filmstrip: a full-height strip that scrolls sideways. Each board
   stands a whole screen tall, so at 16:9 it is about 2.8 screens wide
   and reads at something close to its real scale. */
.reel__track {
  height: 100svh;
  display: flex;
  align-items: stretch;
  /* No gap: the boards butt together so no ground shows between them. */
  gap: 0;
  overflow-x: auto;
  overflow-y: hidden;
  /* Stops a sideways swipe from triggering browser back-navigation. */
  overscroll-behavior-x: contain;
  /* Scrolled by wheel, trackpad and arrow keys — no visible bar. */
  scrollbar-width: none;
}
.reel__track::-webkit-scrollbar { display: none; }
.reel__track > * { flex: none; height: 100%; margin: 0; }
/* A frame that has not loaded yet reads as a blank page rather than a
   plum gap in the strip. The pages' own grounds are near this colour,
   so it is invisible once the image paints. */
.reel__track figure { background: var(--c-paper); }
.reel__track img {
  height: 100%;
  width: auto;
  max-width: none;
}
/* The strip is focusable so it can be scrolled from the keyboard. The
   ring is cream and inset rather than the crimson default, which would
   draw a hard frame around the whole screen. */
.reel__track:focus-visible {
  outline: 2px solid var(--c-accent-night);
  outline-offset: -2px;
}


/* ── Narrow screens ─────────────────────────────────────────────
   A full-height board on a phone is a sliver of a drawing. The strip
   becomes a vertical stack instead, each board scrolling sideways on
   its own. */
@media (max-width: 860px) {
  /* One column, and the description in a single run of text. */
  .reel__head {
    height: auto;
    grid-template-columns: 1fr;
    gap: var(--s-5);
  }
  .reel__about { columns: 1; }
  .reel__track {
    height: auto;
    display: grid;
    gap: var(--s-5);
    overflow: visible;
    padding-inline: var(--gutter);
  }
  .reel__track > * { height: auto; }
  .reel__track figure {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .reel__track figure::-webkit-scrollbar { display: none; }
  .reel__track img { height: min(62svh, 520px); }
  /* Stacked, the spreads are only as tall as a phone allows and there
     is no margin left to sit in, so the credits drop back into flow
     under the last one. `left: 0` keeps them put while the figure is
     scrolled sideways. */
  .reel__end {
    position: sticky;
    left: 0;
    inset-block-end: auto;
    inset-inline-end: auto;
    width: 100%;
    padding-block: var(--s-4) var(--s-2);
  }



  /* variant 1 only */
  .reel__panel {
    width: 100%;
    padding-inline: 0;
    overflow: visible;
  }
  .reel--v1 .reel__facts { flex-wrap: wrap; }
  .reel--v1 .reel__facts > div { flex: 1 1 13rem; max-width: none; }
}

/* ═══ PROJECT 02 — THE AP88 STRIP ══════════════════════════════
   Laid out as one continuous field of 21200 × 1972 , the proportions of
   the Photoshop mockup, rather than as a row of separate frames. Every
   element is placed by percentage, so the whole composition scales with
   the screen and the drawn line can run behind all of it in one pass.
   Edit a position here; there are no inline styles, because the CSP
   blocks them. */
.ap88 {
  position: relative;
  container-type: inline-size;
  height: 100%;
  aspect-ratio: 21200 / 1972;
  flex: none;
  background: var(--c-paper);
}

/* The line sits behind everything and is stretched to the strip. The
   stroke keeps its weight because it is exempted from the transform. */
.ap88__thread {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  z-index: 0;
  /* Clipped to the field. The path is fitted inside it below, so this
     no longer cuts anything — it only stops a stroke edge bleeding out. */
  overflow: hidden;
  opacity: 0.85;
}
.ap88__thread--down { display: none; }

/* One path, two views. The stroke is set on the <use> and inherits into
   the referenced path, so both views draw from the same geometry. */
.ap88__stroke {
  fill: none;
  stroke: var(--c-accent);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
/* Without JS the line simply rests drawn. */

.reel__track .ap88__el {
  position: absolute;
  margin: 0;
  /* No ground behind these. `.reel__track figure` gives project 01's
     spreads a paper background, and inherited here it was an opaque
     rectangle that cut the line in a box far outside the drawing.
     The drawings carry their own alpha; the line reads through it. */
  background: none;
  /* Everything sits over the line, which passes behind all of it.
     What keeps it readable is that the drawings are transparent, so it
     shows through their empty ground rather than being blocked by a
     rectangle. */
  z-index: 1;
}
.ap88__el picture,
.ap88__el img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
}
.ap88__el img { object-fit: cover; }

/* The moodboard and the plans are drawings on the same paper as the
   page, so they must not be cropped the way a photograph can be. */
.ap88__mood img,
.ap88__plans img,
.ap88__plannew img { object-fit: contain; }

/* Captions are live text, sitting just under the picture — in the
   mockup they were baked in and brought a white box with them. */
.ap88__el figcaption {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 0.7em;
  font-size: clamp(0.62rem, 1.05cqw, 0.9rem);
  letter-spacing: 0.04em;
  line-height: 1.2;
  color: var(--c-ink-2);
  white-space: nowrap;
}

.ap88__living { left: 0.024%; top: 29.057%; width: 6.873%; height: 55.375%; }
.ap88__desk { left: 9.580%; top: 12.170%; width: 7.009%; height: 56.542%; }
.ap88__mood { left: 19.090%; top: 0.862%; width: 3.075%; height: 96.704%; }
.ap88__plans { left: 22.608%; top: 7.302%; width: 6.651%; height: 56.846%; }
.ap88__plannew { left: 26.627%; top: 56.034%; width: 5.741%; height: 42.901%; }
.ap88__s1a { left: 33.533%; top: 10.142%; width: 4.132%; height: 33.316%; }
.ap88__s1b { left: 38.278%; top: 10.142%; width: 4.132%; height: 33.316%; }
.ap88__s1c { left: 43.024%; top: 10.142%; width: 4.132%; height: 33.316%; }
.ap88__s2a { left: 41.651%; top: 58.519%; width: 4.132%; height: 33.316%; }
.ap88__s2b { left: 46.396%; top: 58.519%; width: 4.132%; height: 33.316%; }
.ap88__s2c { left: 51.142%; top: 58.519%; width: 4.132%; height: 33.316%; }
.ap88__s3a { left: 52.958%; top: 10.142%; width: 4.132%; height: 33.316%; }
.ap88__s3b { left: 57.703%; top: 10.142%; width: 4.132%; height: 33.316%; }
.ap88__s3c { left: 62.448%; top: 10.142%; width: 4.132%; height: 33.316%; }
.ap88__f1 { left: 72.052%; top: 34.077%; width: 4.132%; height: 33.316%; }
.ap88__f2 { left: 76.797%; top: 34.077%; width: 4.132%; height: 33.316%; }
.ap88__f3 { left: 81.542%; top: 34.077%; width: 4.132%; height: 33.316%; }
.ap88__f4 { left: 86.288%; top: 34.077%; width: 4.132%; height: 33.316%; }

/* Credits close the strip on the paper, low and to the left of the
   last picture. */
/* Credits close the strip, past the last pictures rather than under
   them, low on the paper. */
.ap88__end {
  position: absolute;
  left: 91.844%;
  bottom: 8.621%;
  width: 5.579%;
  z-index: 1;
  color: var(--c-ink);
}

/* ═══ LEGAL PAGES ══════════════════════════════════════════════ */
.legal { padding: calc(var(--s-8) + 2vw) 0 var(--s-8); }
.legal__inner {
  max-width: 68ch;
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.legal__title {
  font-weight: 700;
  font-size: clamp(2rem, 6vw, 3.6rem);
  line-height: 1;
  letter-spacing: -0.025em;
  text-transform: uppercase;
  margin-bottom: var(--s-5);
}
.legal h2 {
  font-weight: 500;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-accent);
  margin-top: var(--s-5);
  padding-bottom: var(--s-1);
  border-bottom: 1px solid var(--c-line);
}
.legal p { margin-top: var(--s-2); line-height: 1.75; }
.legal a { color: var(--c-accent); }
.legal__back { margin-top: var(--s-6); }
.legal__back a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
}

/* ═══ FOOTER ═══════════════════════════════════════════════════ */
/* Continues the contact section's ground, closing on a single rule. */
.site-footer {
  background: var(--c-night);
  color: var(--c-on-night);
}
.site-footer__bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-5);
  justify-content: space-between;
  align-items: center;
  padding: var(--s-2) var(--gutter);
  margin-inline: var(--gutter);
  border-top: 1px solid var(--c-line-dark);
  padding-inline: 0;
}
.site-footer__bar a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
}

/* ═══ RESPONSIVE ═══════════════════════════════════════════════ */
@media (max-width: 1024px) {
  .about__stage { height: 240vh; }
  .hero__object { width: min(64vw, 460px); }

  /* The off-axis placement needs room to read; below this it crowds. */
  .shot--right,
  .shot--centre,
  .shot--left {
    width: min(86%, 760px);
    margin-inline: auto;
  }
  .shot--pair { width: 100%; }
  .shot--pair .shot__media:last-child { margin-top: var(--s-5); }
  .work__thread { width: 92%; }
}

/* Narrow screens get the same drawn curve, turned 90° so it runs down
   the page instead of across it. Nothing is redrawn: the SVG is simply
   rotated, and the labels are remapped to match. */
@media (max-width: 700px) {
  .wave {
    /* The curve is 2.915:1 on its side. Letting it occupy the middle
       60% of the width leaves the outer 20% either side as room for
       the labels, which gives the box this ratio. */
    aspect-ratio: 1 / 1.75;
    margin-block: var(--s-6);
  }

  .wave__line {
    position: absolute;
    top: 50%;
    left: 50%;
    /* Before rotating, the SVG is wide: its width becomes the box's
       full height (175% of the width), and its height becomes the 60%
       band the curve occupies. 175/34.3 = 2.915, so the viewBox still
       fits exactly and the curve is never distorted. */
    width: 175%;
    height: 34.3%;
    /* The global svg{max-width:100%} reset would otherwise clamp that
       175% back to the box width and shrink the curve. */
    max-width: none;
    transform: translate(-50%, -50%) rotate(90deg);
  }

  /* Rotating clockwise sends a point (x, y) to (100 − y, x): the start
     of the curve moves from the left edge to the top, so it still
     reads in the direction of scroll. The 0.6 factor and 20% offset
     place each label on the same 60% band as the curve. */
  .wave__labels li {
    left: calc(20% + (100% - var(--y)) * 0.6);
    top: var(--x);
  }
}

@media (max-width: 720px) {
  .site-nav { display: none; }
  .nav-toggle { display: inline-flex; }
  .edge-badge { display: none; }

  .hero { padding-top: calc(var(--s-7) + 8vh); }
  .hero__title { font-size: clamp(2.7rem, 17vw, 5.5rem); }
  .hero__object { width: min(84vw, 340px); }
  /* Wider than the screen on purpose, so the disc bleeds off both
     edges. `.hero` clips horizontally, which is what stops that bleed
     widening the document. */
  .hero::before { width: 118vw; height: min(118vw, 92svh); }

  .about__stage { height: 215vh; }
  .about__sticky { padding-block: var(--s-5); }
  .about__statement { max-width: 100%; }

  .shot--right,
  .shot--centre,
  .shot--left,
  .shot--pair { width: 100%; }
  /* Side by side is too tight to read on a phone. */
  .shot--pair .shot__link { grid-template-columns: 1fr; gap: var(--s-4); }
  .shot--pair .shot__media:last-child { margin-top: 0; }
  .shots { gap: var(--s-7); }
}

/* ═══ REDUCED MOTION ═══════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .js [data-reveal] { opacity: 1; transform: none; }
  /* The statement rests fully lit rather than animating. */
  .about__statement .w { font-weight: 600; color: var(--c-ink); }
  /* The curve is simply present rather than drawing itself in. */
  .wave__line path { stroke-dashoffset: 0 !important; animation: none !important; }
}

/* ═══ PRINT ════════════════════════════════════════════════════ */
@media print {
  .site-header, .edge-badge, .hero__object, .mobile-nav { display: none !important; }
  body { background: #fff; color: #000; }
  .about__statement .w { font-weight: 600; color: #000; }
  .wave__line path { stroke-dashoffset: 0 !important; animation: none !important; }
}

/* ═══ PROJECT 02 ON A PHONE ════════════════════════════════════
   The strip is a placed composition and cannot survive being squeezed
   onto a phone, so it unpacks into a plain column: every element back
   in flow, in source order, full width. The drawn line goes with it —
   it only means anything across the width it was drawn for.

   This block sits at the end of the stylesheet on purpose. It shares
   its specificity with the strip rules above, so it only wins by
   coming later. */
@media (max-width: 860px) {
  .ap88 {
    height: auto;
    aspect-ratio: auto;
    display: grid;
    gap: var(--s-5);
    width: 100%;
  }
  /* The line stays, turned on its side and running down the column, as
     the capabilities wave does. The across view hides and the down view
     takes over: same path, a viewBox with the axes swapped. */
  .ap88__thread--across { display: none; }
  .ap88__thread--down {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
  }
  .ap88__el,
  .ap88__end {
    position: static;
    width: 100%;
    height: auto;
    inset: auto;
  }
  /* Pulled in a little and alternating side to side, so the line down
     the column is visible between them rather than covered end to end —
     full-width pictures left nothing of it showing but the gaps. */
  .ap88__el { width: 91%; }
  figure.ap88__el:nth-of-type(odd)  { justify-self: start; }
  figure.ap88__el:nth-of-type(even) { justify-self: end; }
  .ap88__el img { height: auto; object-fit: contain; }
  .ap88__el figcaption {
    position: static;
    margin-top: var(--s-1);
    font-size: 0.72rem;
  }
  .ap88__end { padding-block: var(--s-2); }
}
