:root {
  --ink: #262523;
  --paper: #ffffff;
  --paper-2: #f4f2ee;
  --muted: #8a857c;
  --line: rgba(38, 37, 35, 0.12);
  --dark: #121110;
  --font-display: "Montserrat", "Proxima Nova", "Century Gothic", -apple-system, "Segoe UI", sans-serif;
  --font-body: "Inter", "Helvetica Neue", Arial, sans-serif;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  /* Slower, smoother deceleration than --ease above — that curve is tuned
     for small, snappy UI feedback (hovers, the nav dropdown); applied to
     a large-distance/large-scale transition like the project reveal
     (script.js) it read as abrupt. Used there instead: a gentler
     "decelerate" curve for motion covering a lot of screen space. */
  --ease-expand: cubic-bezier(0.2, 0, 0, 1);
  --header-clear: clamp(78px, 11vw, 104px);
  --footer-clear: 68px;
  --edge-clear: clamp(20px, 4vw, 48px);
  --header-side-pad: clamp(16px, 4vw, 48px);
  --hero-gap: clamp(64px, 14vh, 200px);
  /* Recomputed in JS (updateWorkTeamGap, script.js) right after load, to
     the exact margin that lands team's content centered in the viewport
     the moment work's last row is 50px above the top edge — this value is
     only the fallback used for the very first paint, before that runs. */
  --work-team-gap: calc(100svh + 60px);
  /* Same principle, one section later (updateTeamContactGap, script.js):
     contact's content lands centered the moment team's last card is 50px
     above the top edge. Same first-paint-only fallback as above. */
  --team-contact-gap: calc(100svh + 60px);
  /* Header text has no shadow/glow — just a flat color switch. While the
     hero is still on screen it's always this near-white, regardless of
     theme (script.js adds .is-past-hero to the header once an
     IntersectionObserver reports the hero has fully scrolled out); once
     past it, --header-ink/--header-muted are overridden below to the
     normal theme-adaptive colors. */
  --header-ink: #fdfbf6;
  --header-muted: rgba(253, 251, 246, 0.65);
  --header-bg: rgba(255, 255, 255, 0.78);
}

:root[data-theme="dark"] {
  --ink: #f2f0ec;
  --paper: #121110;
  --paper-2: #1c1b18;
  --muted: #9b968c;
  --line: rgba(242, 240, 236, 0.14);
  --dark: #050504;
  --header-bg: rgba(18, 17, 16, 0.72);
}

.site-header.is-past-hero {
  --header-ink: var(--ink);
  --header-muted: var(--muted);
}

/* While a project is open, the veil (--paper) covers the hero, so the header no
   longer sits over a photo. Switch its text to the theme's ink colour — dark on
   the light theme's white veil, light on the dark theme's near-black veil — so
   it stays readable instead of near-white-on-white (light mode). project-open
   lasts through the whole close (dropped in finishClose), so the header stays
   legible while the veil retracts, then reverts as the hero is revealed. */
body.project-open .site-header {
  --header-ink: var(--ink);
  --header-muted: var(--muted);
}

* { box-sizing: border-box; }

/* The native scrollbar (track + system thumb) is hidden entirely — scrolling
   still works by wheel/trackpad/keyboard — and replaced by a slim custom thumb
   (.scroll-thumb, built in script.js) that acts purely as a scroll-position
   symbol. No scrollbar-gutter is needed anymore since nothing reserves width. */
html { scroll-behavior: smooth; scrollbar-width: none; -ms-overflow-style: none; }
::-webkit-scrollbar { width: 0; height: 0; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  overscroll-behavior-y: none;
  transition: background 0.3s var(--ease), color 0.3s var(--ease);
}

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

a { color: inherit; text-decoration: none; }

/* Buttons don't inherit the page font by default in browser UA
   stylesheets — force them onto the same two type families declared in
   the header (and used everywhere else on the site) instead of falling
   back to the system font. */
button { font-family: inherit; }

/* ---------- Header ---------- */
/* Fixed at all times, fixed size at all times — no scroll-linked padding
   or shadow changes, since those were what made it visibly shift while
   scrolling. Background stays translucent + blurred so work-grid images,
   which now start at the very top of the page, read through underneath. */

/* Horizontal padding is --edge-clear (the work grid's own left/right
   inset), not --header-side-pad, so the wordmark and "contact" line up
   flush with the work images' edges rather than just approximately. */
/* Off-screen until keyboard-focused (Tab from page load) — the very
   first focusable element, for jumping straight past the header/hero to
   .main-nav's actual content. Sits above the header (z-index 1100) once
   visible, since it needs to be reachable/legible over it, not behind it. */
.skip-link {
  position: absolute;
  top: -100px;
  left: var(--edge-clear);
  z-index: 1200;
  background: var(--ink);
  color: var(--paper);
  padding: 12px 20px;
  border-radius: 8px;
  font-size: 14px;
  font-family: var(--font-body);
  transition: top 0.2s var(--ease);
}

.skip-link:focus {
  top: 16px;
}

.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 1100;
  padding: 20px var(--edge-clear);
  background: transparent;
  transition: background 0.35s var(--ease);
}

/* Subtle top-down dark scrim, kept as a separate taller layer rather than
   the header's own background — over the hero, header text has no
   control over how bright any given slideshow photo is, so a permanent
   light darkening right where the text sits keeps it readable regardless.
   Sized well past the header's own height and faded very gently so it
   reads as part of the photo rather than a visible box: negative z-index
   places it behind the header's own content but above its (transparent,
   in this state) background. Scoped to :not(.is-past-hero) — the same
   state that keeps header text white — so it only exists while there's
   actually a photo behind it; past the hero (light theme's plain white
   page background, or the --hero-gap/--work-team-gap in between) it's
   gone entirely instead of showing up as a visible gray smudge over a
   surface it was never meant to tint. Deliberately theme-independent
   (plain black) since it's darkening photographic image content, not
   page chrome. */
.site-header:not(.is-past-hero)::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: clamp(160px, 24vh, 280px);
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0));
  pointer-events: none;
  z-index: -1;
}

/* Added once the work-section images actually reach the header's bottom
   edge (script.js, scroll-driven) — not at the same moment text color
   switches past the hero, since --hero-gap leaves empty space between the
   two that should stay see-through. */
.site-header.has-bg {
  background: var(--header-bg);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* position:relative anchors the mobile dropdown panel (see .main-nav in
   the max-width:700px block below) to this group rather than the whole
   header-inner row. */
.header-controls {
  display: flex;
  align-items: center;
  gap: clamp(6px, 2vw, 12px);
  position: relative;
  flex-shrink: 0;
}

/* Scaled 2x on first paint (script.js), top-left anchored so the text
   grows down and to the right rather than around its center, then
   shrinks back to 1x shortly before the hero scrolls out of view — same
   scroll-driven transform pattern as the hero tagline/images, batched
   into the one rAF-throttled scroll handler so it can't reintroduce the
   layout-thrashing jank that scroll effect had before. */
.wordmark {
  position: relative;
  display: inline-block;
  transform-origin: top left;
  will-change: transform;
  font-family: var(--font-display);
  font-size: clamp(13px, 3.6vw, 19px);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--header-ink);
  transition: color 0.3s var(--ease);
}

.main-nav {
  display: flex;
  align-items: center;
  gap: clamp(6px, 2vw, 12px);
  font-size: clamp(11px, 3vw, 14px);
  font-weight: 500;
  color: var(--header-ink);
  transition: color 0.3s var(--ease);
  flex-shrink: 0;
}

.main-nav a { position: relative; padding: 4px 2px; }

.main-nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 100%;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transition: right 0.35s var(--ease);
}

.main-nav a:hover::after,
.main-nav a.is-current::after { right: 0; }

.main-nav a.is-current { color: var(--header-muted); }

.main-nav .dot { opacity: 0.45; font-size: 12px; }

/* Sits to the left of work/team/contact now (see index.html), with its
   own extra margin so it reads as a separate control rather than
   crowding the nav — the other inter-item spacing (.header-controls'
   gap) is deliberately tighter, it's meant for closely related items
   like the nav links themselves. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin-right: clamp(16px, 3vw, 28px);
  border-radius: 50%;
  border: 1px solid var(--line);
  background: none;
  color: var(--header-ink);
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
  padding: 0;
  transition: border-color 0.3s var(--ease), transform 0.3s var(--ease), color 0.3s var(--ease);
}

.theme-toggle:hover { border-color: var(--ink); transform: scale(1.08); }

/* Hidden on desktop — work/team/contact show inline via .main-nav as
   normal. Below 700px it takes over: .main-nav becomes an absolutely
   positioned dropdown anchored to .header-controls, opening downward. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-left: 2px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: none;
  color: var(--header-ink);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 0;
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
}

.nav-toggle:hover { border-color: var(--ink); }

@media (max-width: 700px) {
  .nav-toggle { display: inline-flex; }

  /* No background, no border — deliberately just floating letters, so
     whatever's behind the header (hero photo, work image, page
     background) shows straight through with nothing framing it. Link
     color stays on the normal --header-ink/--header-muted (the base
     .main-nav rule already sets this), same light-over-photos logic as
     the rest of the header; the text-shadow below is what lifts the
     letters off arbitrary image content instead of a panel surface. */
  .main-nav {
    position: absolute;
    top: calc(100% + 14px);
    right: 0;
    flex-direction: column;
    align-items: flex-end;
    gap: 14px;
    border-radius: 14px;
    padding: 14px;
    min-width: 108px;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0s linear 0.25s;
  }

  .main-nav.is-open {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0s linear 0s;
  }

  .main-nav a {
    font-size: 15px;
    padding: 2px 0;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.45), 0 1px 4px rgba(0, 0, 0, 0.35);
  }
  .main-nav .dot { display: none; }
}

/* ---------- One-pager sections ---------- */
/* work/team/contact are normal, always-in-flow sections now — reachable
   either by scrolling past them or by clicking the header nav (which
   smooth-scrolls via the browser's native anchor jump, since html has
   scroll-behavior:smooth). Team/contact reserve top clearance for the
   fixed header; work doesn't — its images run edge to edge and sit behind
   the translucent header, same as the hero above it. */

main { display: block; }

/* margin-top is --team-contact-gap, recomputed in JS (updateTeamContactGap,
   script.js) — same principle as --work-team-gap on .team-section below:
   just enough distance that .contact-columns lands vertically centered the
   moment team's last card clears 50px above the top edge, not a full extra
   viewport of blank scroll.
   min-height:100svh + centering the columns vertically inside it also
   means a plain scrollIntoView to the section's top (see the nav click
   handler, script.js) already lands the content vertically centered in
   the viewport, without needing its own scroll-centering math. */
.contact-section {
  /* One viewport minus the footer's height, so contact + footer together fill
     exactly one screenful and a nav jump to contact lands at the page bottom
     (--footer-height set by script.js; 0px fallback until it runs). */
  min-height: calc(100svh - var(--footer-height, 0px));
  margin-top: var(--team-contact-gap);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Team is a "business card" screen, at least one viewport tall — on
   screens roomy enough for the intro text + both cards it still reads as
   a single, non-scrolling screen (min-height lets flex:1 below still
   stretch team-grid to fill it exactly like a fixed height would), but on
   shorter viewports or with longer copy it's now allowed to grow instead
   of clipping content, and the page scrolls past the rest.
   margin-top is --work-team-gap, recomputed in JS (updateWorkTeamGap,
   script.js) to the exact distance that lands this section's content
   vertically centered in the viewport the moment work's last row is 50px
   above the top edge — deliberately not a full extra viewport of blank
   scroll, just enough that the two never visibly overlap. */
.team-section {
  min-height: 100svh;
  margin-top: var(--work-team-gap);
  /* Just enough to clear the fixed header, not the 3x multiplier this
     used to carry — that much fixed top offset was eating so much of the
     viewport that updateWorkTeamGap's centering math could never actually
     land content in the middle (there wasn't room left to center into),
     only ever hit its floor. */
  padding-top: var(--header-clear);
  display: flex;
  flex-direction: column;
}

/* ---------- Hero ---------- */
/* Fullscreen slideshow (images cross-fade every 6s, orientation-specific
   sets swapped in by script.js — portrait uses images/hero/hoch, landscape
   uses images/hero/quer) filling the entire first viewport. script.js also
   drifts the whole slide stack upward at half scroll speed (a translateY
   counter-offset on top of its normal in-flow scroll motion), so the
   imagery visibly lags behind as the hero scrolls away.

   The tagline sits over the hero from the very first paint (unchanged
   from before) and keeps its own parallax-drift + letter-flicker fade —
   only the distance that fade is measured over is now derived from where
   the work section actually starts (script.js), instead of a guessed
   fixed number of pixels, so it's always fully gone by the time work
   arrives regardless of --hero-gap or viewport height. */

.hero {
  position: relative;
  height: 100svh;
  min-height: 480px;
  overflow: hidden;
  background: var(--dark);
}

.hero-slides {
  position: absolute;
  inset: 0;
  will-change: transform;
}

.hero-slide {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transition: opacity 1.8s ease;
}

.hero-slide.is-active { opacity: 1; z-index: 1; }

.hero-tagline {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  margin: 0;
  padding: 0 var(--header-side-pad) clamp(28px, 6vh, 64px);
  display: flex;
  flex-direction: column;
  text-align: left;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(22px, 5vw, 64px);
  will-change: transform;
  transition: opacity 0.5s var(--ease);
}

/* 5vw bottoms out at the clamp's 22px floor on most phones (375px width
   is only 18.75px, well under it) — a dedicated, steeper mobile value
   instead of just relying on that floor. */
@media (max-width: 700px) {
  .hero-tagline { font-size: clamp(28px, 9vw, 42px); }
}

/* ---------- Compositor parallax (scroll-driven) ----------
   When the browser supports scroll-progress timelines, the hero image and
   tagline parallax runs entirely on the compositor via
   animation-timeline: scroll() instead of the per-frame JS transform writes
   in updateHeroOnScroll — so it stays in perfect lockstep with the page
   scroll, with none of the ~1-frame main-thread lag the JS path has. script.js
   adds .hero-parallax-css to <html> ONLY when
   CSS.supports("animation-timeline: scroll()") is true, and in that case stops
   writing the .hero-slides/.hero-tagline transforms itself (the wordmark scale
   and the tagline letter-flicker deliberately stay in JS). Without support the
   class is never added and the unchanged JS transform path remains the
   fallback.

   Motion is identical to the JS version: over the first viewport of scroll the
   image drifts +50vh (half scroll speed, factor 0.5) and the tagline -10vh
   (factor -0.1), mapped linearly by animation-range. */
@keyframes hero-parallax { from { transform: translateY(0); } to { transform: translateY(50vh); } }
@keyframes hero-tagline-parallax { from { transform: translateY(0); } to { transform: translateY(-10vh); } }

.hero-parallax-css .hero-slides {
  animation: hero-parallax linear both;
  animation-timeline: scroll(root block);
  animation-range: 0 100vh;
}
.hero-parallax-css .hero-tagline {
  animation: hero-tagline-parallax linear both;
  animation-timeline: scroll(root block);
  animation-range: 0 100vh;
}

/* Both languages stay on screen together (see script.js) in the same
   interleaved order as _ROCOCOON.ai's "tagline_2" layer — German,
   English, German, English. Line-height matches that layer's own
   baseline-to-baseline spacing (measured off its text-matrix Td values,
   ~0.52x the font size — deliberately tight, not the usual paragraph
   line-height) rather than any gap-based approximation, per how precisely
   that layer's rhythm needed to be followed. Letter-spacing is no longer
   set here either — each line carries its own value (script.js, straight
   off that same layer's own per-line Tc), since the source doesn't use
   one blanket tracking value across the whole block. */
.tagline-line {
  display: flex;
  align-items: baseline;
  line-height: 0.52;
}

/* That source layer colors German lines black and English a muted tone;
   here German is white instead (needs to read against a photo, not
   paper) and English is a dimmer translucent white rather than the
   source's literal gray, so the two keep a clear brightness gap while
   English still stays legible over dark image areas. English lines are
   also indented relative to German — matching that layer's own
   text-matrix offset between the two languages — instead of both flush
   left. */
.tagline-de { color: var(--header-ink); }
.tagline-en { color: rgba(253, 251, 246, 0.52); margin-left: 0.29em; }

.tagline-letter { display: inline-block; }

/* ---------- Work grid (flowing / justified gallery) ---------- */
/* Row composition (how many projects per row) and row height are computed
   in script.js per breakpoint/orientation — see justifyWorkGrid(). The grid
   still starts flush with the top of the page (behind the header) — only
   the horizontal spacing (between images and to the left/right edge) is
   restored here. work-section is pushed down by --hero-gap so it never
   butts straight up against the hero. */

.work-section {
  position: relative;
  margin-top: var(--hero-gap);
  padding-bottom: 0;
  --gap: 6px;
}

@media (min-width: 900px) { .work-section { --gap: 8px; } }
@media (min-width: 1400px) { .work-section { --gap: 10px; } }

/* Hero cover: a --paper panel sitting just above the work-section (bottom:100%
   anchors its bottom edge to the section's top). Normally zero-height and
   invisible; while a project is open (body.project-open) its top edge grows
   upward to blanket the whole hero, so the hero never shows through behind the
   centred project. z-index 3 puts it above the hero AND the hero tagline
   (z-index 2), so the tagline is covered too, but below the work tiles
   (z-index 4), the expanded project (z-index 5) and the header, so only the
   hero is hidden. On close, project-open
   is dropped in finishClose and it retracts — by then the project has already
   shrunk back into the grid, so the hero is only re-revealed once nothing is
   over it. When the hero isn't on screen (scrolled down) this all happens
   off-screen above the viewport, unseen. */
.work-section::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;   /* bottom edge pinned to the work-section's top edge */
  height: var(--veil-height, 100svh);  /* full hero-covering height (JS: expandProject) */
  background: var(--paper);
  z-index: 3;     /* below work tiles (4) and the open project (5), above hero (1) + tagline (2) */
  pointer-events: none;
  /* Revealed by a hard CLIP edge (clip-path), NOT by animating height. The panel
     stays full-height; clip-path insets it from the top — closed it's clipped to
     nothing (its bottom sits on the work-section's top edge, so nothing peeks into
     the header on load), open it's fully shown and covers the hero. The visible
     top edge of the clip travels UP on open / DOWN on close, exactly like a moving
     veil edge — but clip-path is composited (no layout reflow, no sub-pixel seam),
     so it draws smoothly. This base rule governs the close; it WAITS 0.35s so the
     shrinking project clears the hero area first and the downward travel is seen
     instead of finishing hidden behind the card. */
  clip-path: inset(100% 0 0 0);
  transition: clip-path 1.2s var(--ease) 0.35s;
}

body.hero-covered .work-section::before {
  /* Fully revealed (clip removed) — the panel, sized by --veil-height, covers the
     whole visible hero. Reveals immediately (no delay, unlike the base/close rule):
     the card is still small on open, so the upward travel is visible from frame one.
     hero-covered is dropped at the START of the close (collapseProject), so the clip
     edge starts travelling back down immediately with the close. */
  clip-path: inset(0 0 0 0);
  transition: clip-path 0.6s var(--ease);
}

.work-grid {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  padding: 0 var(--edge-clear);
}

/* Each row is its own flex line (built by justifyWorkGrid in script.js) so
   a height-capped row — narrower than the container — can never let the
   next row's item flow up into its leftover space, the way a flat
   flex-wrap container would.

   is-expanding-row (script.js): the row containing the clicked project
   grows toward the header/bottom edge in normal document flow — rows
   before it are simply left behind as the page grows underneath them
   (already scrolled to sit just below the header, see expandProject),
   rows after get pushed down by the extra height, same as any other
   height change in normal flow. margin-top/bottom grow right along with
   it rather than staying pinned to the grid's own tight row gap, so the
   breathing room around the expanded row scales with it too. position:
   relative gives its now-absolutely-positioned row-mates (.project.
   is-pushed, further down) something to measure themselves against. */
.work-row {
  display: flex;
  gap: var(--gap);
  /* transform is the rows above/below sliding away when a project opens —
     matched to the project's OWN grow (1.2s open / 0.8s close, same ease-expand)
     so the growing project can never outpace and overlap them: they travel a
     longer distance (110vh) than the project's edge, so with identical timing
     the gap between them only ever grows, never shrinks below the original --gap
     (see .project.is-pushed for the same reasoning). height is held, not grown,
     during an expand now, so its transition is effectively unused. */
  transition: margin-top 0.6s var(--ease-expand), margin-bottom 0.6s var(--ease-expand), height 1.2s var(--ease-expand), transform 1.2s var(--ease-expand);
}

/* No margin-top: adding one would push the whole row (and the clicked card
   with it) downward on open, so the project would visibly drop away from the
   exact spot it was clicked — the opposite of growing in place. Only
   margin-bottom is kept, purely as separation to the row below; it doesn't
   move the card itself. */
.work-row.is-expanding-row {
  position: relative;
  margin-bottom: clamp(24px, 4vh, 64px);
}

/* JS sets an exact pixel width/height on each .project once measured; this
   is just the pre-JS fallback so nothing collapses to 0. Width/height get
   their own slower, gentler transition (--ease-expand, see above) than
   opacity/transform's 0.9s scroll-reveal-in — this is also what
   .project.is-expanded rides to grow into its row (script.js sets its
   width/height directly, .project-media/img/video underneath just track
   it at 100%/100%, no separate expanded-state sizing rules needed there
   at all). */
.project {
  position: relative;
  /* Image + caption sit above the hero-cover veil (z-index 3); the expanded
     project overrides this to 5, pushed-away row-mates to 1 (both below). */
  z-index: 4;
  cursor: pointer;
  flex: 0 0 auto;
  width: 100%;
  aspect-ratio: var(--ar, 1.4);
  opacity: 0;
  /* Pre-reveal offset: a hidden project sits this far below its final slot, so
     the larger the value the bigger the gap to the last visible project above
     before it slides up into place. */
  transform: translateY(100px);
  /* The open/close grow — width/height/left/top — runs at 1.2s (half the
     earlier 0.6s speed); the same transition drives both directions, so the
     close is exactly as slow as the open. opacity/transform here are the
     unrelated scroll-reveal slide-in, now at 1.8s (half the earlier 0.9s
     speed). */
  transition: opacity 1.8s var(--ease), transform 1.8s var(--ease), width 1.2s var(--ease-expand), height 1.2s var(--ease-expand), left 1.2s var(--ease-expand), top 1.2s var(--ease-expand);
}

.project.is-expanded {
  /* Clickable as a pager (left half = previous image, right half = next). */
  cursor: pointer;
  /* Above the work tiles (z-index 4) and the hero-cover veil (z-index 3,
     .work-section::before) so the open project always sits on top. */
  z-index: 5;
}

/* The clicked project's row-mates (script.js) — pulled out of the row's
   flex flow via inline position:absolute/left/top/width/height (measured
   at the moment of the click, so they hold their exact in-flow appearance
   as the starting frame) so the clicked project can grow to fill the row
   on its own without fighting them for flex space, then slid off-screen
   sideways — left for the ones before the clicked project in the row,
   right for the ones after — rather than just shrinking away in place. */
.project.is-pushed {
  /* The row-mates sliding off sideways — matched to the project's own grow
     (1.2s open / 0.8s close, same ease-expand curve, see .work-row) so the
     growing project can never catch up and cover them. They travel 110vw, more
     than the project's side edge, so the free space between them only grows —
     the "pushed out of the way" feel, never a shrinking/overlapping gap. */
  transition: transform 1.2s var(--ease-expand);
  pointer-events: none;
  z-index: 4;
}

/* Closing runs 1.5× faster than opening. These overrides apply only while a
   collapse is in progress (script.js adds body.project-closing in
   collapseProject and removes it in finishClose), so the OPEN keeps its own
   pace: card shrink 1.2s → 0.8s, and everything sliding back stays MATCHED to
   that card motion (also 0.8s) so the shrinking project can never overtake and
   cover the returning rows; the caption pop 0.3s → 0.2s. Higher specificity than
   the base rules above, so the shorter durations win during the close. */
body.project-closing .project.is-expanded { transition-duration: 0.8s; }
body.project-closing .project.is-pushed { transition-duration: 0.8s; }
body.project-closing .work-row {
  transition: margin-top 0.4s var(--ease-expand), margin-bottom 0.4s var(--ease-expand), height 0.8s var(--ease-expand), transform 0.8s var(--ease-expand);
}
body.project-closing .expand-caption { transition-duration: 0.2s; }

/* The team + contact sections sit below the whole work grid. When a project
   opens, script.js slides them down and off (translateY) in the same gesture
   as the work rows below the clicked one, so the expanding project clears
   everything beneath it — then clears the transform again on close. Same pace
   as .work-row's own transform slide: 1.2s opening, 0.8s closing (matched to the
   project's grow, see .work-row). */
.team-section,
.contact-section {
  transition: transform 1.2s var(--ease-expand);
}
body.project-closing .team-section,
body.project-closing .contact-section {
  transition: transform 0.8s var(--ease-expand);
}

/* One-shot reveal: once a project has scrolled into view it stays visible,
   even when scrolling back up past it again (see projectObserver in
   script.js — it no longer toggles the class off). */
.project.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.project.is-expanded .project-media::after { display: none; }

.project-media {
  position: relative;
  overflow: hidden;
  width: 100%;
  height: 100%;
  background: var(--paper-2);
}

.project-media img,
.project-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* No hover-zoom, and no permanent scale on the thumbnail either: the image
   rests at its natural scale (1.0) in BOTH the grid thumbnail and the
   expanded view, so closing a project no longer leaves it settling from the
   expanded 1.0 back onto a 1.03 thumbnail — that 3% jump was the "zoom" seen
   at the very end of the close. (The earlier subtle 1.03 crop is gone as a
   result; if it's ever wanted back it must be done without a transform, e.g.
   a slightly zoomed background, so the discontinuity can't return.) */

/* Expanded, the image is shown in full — object-fit:contain instead of the
   thumbnail's cover — so every part the thumbnail cropped away is now
   revealed inside the grown box rather than stay cropped. No opacity here at
   all: stepping between a project's images just swaps the src (script.js), it
   never crossfades. The cropped pixels were only ever hidden by the box's own
   overflow, never removed — growing the box is what brings them back. */
.project.is-expanded .project-media img,
.project.is-expanded .project-media video {
  object-fit: contain;
}

/* Close crossfade: on close a project dissolves in a single step from the slide
   the viewer left it on straight to its first image (crossfadeTo in script.js).
   This overlay is that incoming first image, absolutely stacked over the base
   <img> and faded in via opacity (a pure opacity crossfade — the current image
   sits fully opaque underneath). Cleaned up when the fade ends; finishClose's
   innerHTML restore clears it if still mid-fade. */
.rewind-fade {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 1.4s var(--ease);
  z-index: 1;
  pointer-events: none;
}

/* Opened, the letterbox around a contained image is filled with the page's
   OWN background colour (--paper), not the dark-gray thumbnail placeholder
   (--paper-2): still no visible "box" framing the image — it reads as the
   page surface continuing behind it — but being opaque it cleanly hides the
   neighbouring rows the card glides over on its way to the centre of the
   viewport, instead of letting them show through the transparent bars. */
.project.is-expanded .project-media {
  background: var(--paper);
}

/* ---------- Before/after comparison slider ---------- */
/* Built in script.js (renderCompare) for "compare" slides. The "after" image
   sits full underneath, the "before" image is clipped to the left of --split on
   top, and a draggable handle marks the split. Both images are object-fit:
   contain at the same box size, so they align exactly and the handle wipes
   cleanly between them. --split (0–100%) is set by the drag. */
.compare {
  position: absolute;
  inset: 0;
  overflow: hidden;
  cursor: ew-resize;
  touch-action: none;
  --split: 50%;
}
.compare-after,
.compare-before {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}
.compare-before {
  clip-path: inset(0 calc(100% - var(--split)) 0 0);
  transition: clip-path 0.45s var(--ease);
}
/* The handle only appears once the project is fully open (body.project-ready,
   set ~1.2s after open in script.js) and is hidden the instant a close starts. */
.compare-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--split);
  width: 2px;
  margin-left: -1px;
  background: rgb(232, 230, 226);
  box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s var(--ease), left 0.45s var(--ease);
}
body.project-ready .compare-handle { opacity: 1; }
body.project-closing .compare-handle { transition: none; opacity: 0; }
/* While dragging, the divider must track the pointer instantly, not glide. */
.compare.dragging .compare-before,
.compare.dragging .compare-handle { transition: none; }
.compare-handle::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 34px;
  height: 34px;
  transform: translate(-50%, -50%);
  border: 2px solid rgb(232, 230, 226);
  border-radius: 50%;
  box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
}

.project-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(15,13,11,0) 55%, rgba(15,13,11,0.5) 100%);
  opacity: 0;
  transition: opacity 0.35s var(--ease);
}

.project:not(.is-expanded):hover .project-media::after { opacity: 1; }

/* Title + client name only appear on mouse hover, in a fixed spot at the
   bottom of the image — no longer tracking the cursor, since following the
   mouse turned out to be more distracting than useful. */
.project-caption {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 14px;
  z-index: 2;
  /* Uniform light tone in both themes (not theme-adaptive var(--ink), which
     went dark on the light theme) — these labels sit over the project images,
     so they should read light regardless of theme, and match the em below.
     Deliberately a touch lighter than the open-project caption (which is
     rgb(222,220,216)). */
  color: rgb(240, 238, 234);
  font-size: 21px;
  font-weight: 500;
  letter-spacing: 0.01em;
  pointer-events: none;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}

.project:not(.is-expanded):hover .project-caption {
  opacity: 1;
  transform: translateY(0);
}

.project-caption em {
  display: block;
  font-style: italic;
  font-weight: 400;
  font-size: 16.5px;
  letter-spacing: 0.06em;
  color: rgb(240, 238, 234);
  margin-top: 2px;
}

/* Superseded by .expand-caption (script.js appends it into the card)
   while expanded — this smaller hover caption would otherwise sit
   underneath/behind it, visible through any gap. */
.project.is-expanded .project-caption { display: none; }

/* No hover on touch — the caption would otherwise get stuck showing after
   a tap, with no pointer to move it or trigger mouseleave. */
@media (hover: none) {
  .project-caption { display: none; }
}

/* Reused from an earlier version of this that dimmed the whole grid — this
   design (script.js) only ever touches the clicked project's own row now:
   the row itself grows in-flow (see .work-row.is-expanding-row above),
   and only its row-mates get pushed out (.project.is-pushed, also above).
   Everything else in the grid is untouched, left exactly where normal
   document flow puts it as the expanding row shoves rows after it down. */

/* ---------- Team: two business cards ---------- */
/* Same background as the work page (no override — both inherit body's
   --paper) so switching between the two reads as one continuous surface.
   Cards sit side by side in landscape, stacked in portrait — splitting
   the available height 50/50. Type sizes are clamped against vh/vw so
   dense content (career history, contact, address) stays comfortable as
   the available space shrinks. */

/* Full text always shows, uncropped — the section grows past one
   viewport to fit it if it has to (see .team-section). Bigger and bolder
   on screens tall enough to spare the room; the media query below pulls
   it back down to a denser, more compact style on short viewports where
   the cards need every pixel more. German runs in the left half, its
   English translation in the right — a plain two-column split rather than
   a max-width block, so the translation actually lands in the right half
   of the screen instead of just trailing after the German. Narrow
   viewports stack them instead (see max-width media query below). */
.team-intro {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 clamp(24px, 4vw, 64px);
  margin: 0;
  padding: 0 clamp(20px, 4vw, 56px) clamp(6px, 1vh, 14px);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(16px, 2.2vh, 24px);
  line-height: 1.4;
  color: var(--ink);
}

.team-intro-text { margin: 0; }

.team-intro-en { color: var(--muted); }

@media (max-width: 700px) {
  /* Narrow phones stack German under English (already) *and* get a
     smaller, denser style — at this width the intro is two full
     paragraphs one above the other, and left at the display-font size it
     ate most of the screen, squeezing the cards below into a sliver (see
     .team-grid/.team-card in this query). */
  .team-intro {
    grid-template-columns: 1fr;
    gap: 0.6em 0;
    font-family: var(--font-body);
    font-weight: 400;
    font-size: clamp(13px, 3.6vw, 16px);
    line-height: 1.55;
  }
  .team-intro-de { color: var(--muted); }
}

@media (max-height: 700px) {
  .team-intro {
    font-family: var(--font-body);
    font-weight: 400;
    font-size: clamp(10.5px, 1.3vh, 13.5px);
    line-height: 1.5;
  }
  .team-intro-de { color: var(--muted); }
}

/* Desktop landscape only (excludes phone landscape, which stays as tight
   as the rest of the compact mobile treatment) — nearly double the gap
   down to the cards, pushing them further down the screen. */
@media (orientation: landscape) and (min-width: 900px) {
  .team-intro { padding-bottom: clamp(11px, 1.9vh, 27px); }
}

.team-grid {
  flex: 1;
  min-height: 0;
  display: flex;
}

@media (orientation: landscape) {
  /* flex-start rather than the stretch default: without it each card
     stretches to fill the whole remaining height and centers its content
     inside that, which reads as a big empty gap under the intro text on
     tall screens. Top-aligning the cards instead closes that gap and
     lets any leftover space collect harmlessly below them. */
  .team-grid { flex-direction: row; align-items: flex-start; }
}

@media (orientation: portrait) {
  .team-grid { flex-direction: column; }
}

.team-card {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: var(--paper);
  padding: clamp(10px, 3.4vh, 44px) clamp(20px, 4vw, 56px);
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: clamp(12px, 3vw, 32px);
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}

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

/* A photo works well on most screens, but on the shortest viewports
   (phone landscape) there just isn't room for it alongside the full text
   block without risking overflow — the one hard rule for this page is
   that it never scrolls, so the photo is the first thing to go there. */
.team-card-photo {
  flex: 0 0 auto;
  width: clamp(48px, min(11vh, 20vw), 160px);
  aspect-ratio: 1;
  overflow: hidden;
  background: var(--paper-2);
  border: 1px solid var(--line);
}

.team-card-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1);
}

@media (max-height: 420px) {
  .team-card-photo { display: none; }
}

.team-card-body {
  flex: 1 1 0;
  min-width: 0;
  max-width: 460px;
  display: flex;
  flex-direction: column;
  gap: clamp(2px, 1vh, 10px);
}

.team-card-studio {
  font-size: clamp(9.5px, 1.3vh, 12px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

.team-card-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(14.5px, 2.8vh, 26px);
  letter-spacing: -0.01em;
  margin: 0;
  line-height: 1.15;
}

.team-card-role {
  font-size: clamp(10px, 1.4vh, 13px);
  color: var(--muted);
}

.team-card-history {
  list-style: none;
  margin: clamp(1px, 0.6vh, 8px) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(1px, 0.6vh, 6px);
}

.team-card-history li {
  font-size: clamp(9px, 1.15vh, 12.5px);
  line-height: 1.3;
  color: var(--muted);
}

.team-card-contact,
.team-card-address {
  margin: 0;
  font-size: clamp(10.5px, 1.25vh, 13px);
  color: var(--ink);
  line-height: 1.4;
}

.team-card-contact a:hover,
.team-card-address a:hover { opacity: 0.6; }

/* On narrow phones the two-language intro can run tall, and .team-grid's
   flex:1 was forcing the cards to squeeze into whatever sliver of a
   viewport was left over — sometimes under 60px tall, clipping photo and
   text into an illegible overlap. Auto height instead: each card takes
   only what its own content needs, the section (already allowed to grow
   past one viewport, see .team-section) simply extends and the page
   scrolls the rest into view. Photo-above-text also reads better than a
   cramped side-by-side row at this width. Placed after the base rules
   above (rather than up near .team-grid's other media queries) so it
   actually wins the cascade — an earlier same-specificity override here
   would just get overwritten by those unconditional base rules. */
@media (max-width: 700px) {
  .team-grid { flex: 0 1 auto; min-height: auto; }

  .team-card {
    flex: 0 1 auto;
    min-height: auto;
    overflow: visible;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    padding: 24px clamp(20px, 6vw, 32px);
    gap: 12px;
  }

  .team-card-photo { width: clamp(56px, 20vw, 88px); }
}

/* ---------- Contact ---------- */
/* Deliberately flat and plain — four lowercase text columns, no cards, no
   borders, no background boxes, closely following _contact.ai's own
   minimal layout (bold label, then a short stack of regular-weight lines
   underneath, nothing else). Typography reuses .team-card's own type
   system rather than the source file's literal Proxima Nova/black-only
   styling: the bold uppercase label matches .team-card-studio's eyebrow
   treatment, the lines beneath match .team-card-contact/-address (ink) —
   except the intro column, which reads as descriptive rather than actual
   contact data, so it takes .team-card-role's muted tone instead. */

.contact-columns {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(28px, 4vw, 64px);
  max-width: 1100px;
  width: 100%;
  margin: 0 auto;
  padding: 0 clamp(20px, 4vw, 48px) clamp(60px, 8vw, 100px);
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}

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

.contact-col-label {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(10px, 1.3vh, 12px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 10px;
}

.contact-col p {
  margin: 0;
  font-size: clamp(13px, 1.4vw, 15px);
  line-height: 1.65;
  color: var(--ink);
}

.contact-col-intro p { color: var(--muted); }

.contact-col a:hover { opacity: 0.6; }

@media (max-width: 700px) {
  .contact-columns { grid-template-columns: 1fr 1fr; gap: 32px clamp(20px, 6vw, 32px); }
}

@media (max-width: 460px) {
  .contact-columns { grid-template-columns: 1fr; gap: 26px; }
}

/* ---------- Footer ---------- */

.site-footer {
  position: relative;
  z-index: 2;
  color: var(--muted);
  padding: 20px clamp(20px, 4vw, 48px) 30px;
  display: flex;
  justify-content: space-between;
  /* Wrap on very narrow phones instead of squashing/overlapping the two lines.
     --footer-height (updateFooterHeight in script.js) measures the resulting
     height, so contact-section's reservation stays correct either way. */
  flex-wrap: wrap;
  gap: 4px 16px;
  font-size: 12px;
}

/* ---------- Expand caption ---------- */
/* Reusable text+controls piece, appended straight into whichever project
   is currently expanded (script.js) and removed again on close. Bottom-
   left corner of that project's own box (position:absolute — .project is
   already position:relative as its base style) — the project already
   starts at the grid's own --edge-clear inset from the viewport, so left:0
   here lines up with it (and the wordmark above) without double-counting
   that inset. No background — just text over the image, deliberately
   allowed to overlap it.

   "poppt auf" (pops up) rather than fades/slides in — a quick scale off
   its own bottom-left corner, on the snappy site-wide --ease rather than
   the slower --ease-expand the row/project grow itself uses (see below):
   this is a small, discrete UI element arriving on top of an
   already-moving scene, not part of the large-distance motion, so it
   reads better with a snap than a match to that slower pace. */
.expand-caption {
  position: absolute;
  left: clamp(16px, 3vw, 32px);
  bottom: clamp(16px, 3vh, 32px);
  z-index: 2;
  text-align: left;
  /* Fixed off-white text (same in both themes — it sits over the project
     image, not the page surface) with a small dark drop shadow (inherited by
     the title, category and the control symbols below) lifting it off the
     image. The text colour itself stays fully opaque. */
  color: rgb(232, 230, 226);
  text-shadow: -1px 1px 1px rgba(0, 0, 0, 0.8);
  pointer-events: none;
  /* Hidden until a project is actually open. This element lives in the <body>
     by default (script.js only moves it into the expanded card), and being
     position:absolute it would otherwise render its × and ‹ › symbols at the
     bottom-left of the first viewport — i.e. over the hero. visibility (not
     opacity) keeps it out of sight there without reintroducing a transparency
     effect, and still lets the scale pop below animate when it is shown. */
  visibility: hidden;
  transform: scale(0.9);
  transform-origin: bottom left;
  transition: transform 0.3s var(--ease);
}

.expand-caption.is-shown {
  visibility: visible;
  transform: scale(1);
}

.expand-caption h4 {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 32px;
}

.expand-caption em {
  display: block;
  font-style: italic;
  font-weight: 400;
  font-size: 24px;
  letter-spacing: 0.02em;
  color: rgb(232, 230, 226);
  margin-top: 4px;
}

/* Close, above the title and flush with its own left edge — absolutely
   positioned against the caption block (its nearest positioned ancestor)
   rather than stacked in normal flow, so it doesn't push the title/
   category/nav down to make room for itself. */
.expand-close {
  position: absolute;
  left: 0;
  bottom: 100%;
  margin-bottom: 10px;
  background: none;
  border: 0;
  color: rgb(232, 230, 226);
  /* <button> doesn't inherit text-shadow reliably, so pull the caption's own
     shadow onto the × explicitly. */
  text-shadow: inherit;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 30px;
  line-height: 1;
  pointer-events: auto;
  transition: color 0.3s var(--ease);
}

/* Prev/count/next, stacked below the client line as one small muted row —
   reads as a continuation of the caption text instead of separate,
   much larger side-of-screen controls. Part of the caption's own normal
   flow (unlike .expand-close above), so it does contribute to what the
   reveal animation scales — appropriate here, since "part of the caption
   text" is exactly the point. */
.expand-nav {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 10px;
  pointer-events: auto;
}

.expand-count {
  font-size: 12px;
  color: rgb(232, 230, 226);
}

.expand-arrow {
  background: none;
  border: 0;
  color: rgb(232, 230, 226);
  text-shadow: inherit;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 18px;
  line-height: 1;
  padding: 4px;
  transition: color 0.3s var(--ease);
}

/* ---------- Custom scroll thumb ---------- */
/* Slim scroll-position indicator replacing the hidden native scrollbar. Its
   height + vertical position are set by script.js (updateScrollThumb). Colour
   is var(--ink), which already flips per theme (dark thumb on the light theme,
   light thumb on the dark one). It pulses gently via a slow opacity keyframe.
   It can be dragged up/down with the RIGHT mouse button to scroll (script.js);
   a transparent ::before widens the otherwise very thin grab area. Kept
   intentionally small/thin to sit quietly within the overall design. */
.scroll-thumb {
  position: fixed;
  top: 0;
  right: 3px;
  width: 3px;
  height: 40px;
  border-radius: 3px;
  background: var(--ink);
  z-index: 1150;
  pointer-events: auto;
  cursor: ns-resize;
  will-change: transform;
  animation: scroll-thumb-pulse 2.6s ease-in-out infinite;
  transition: opacity 0.4s var(--ease);
}

/* Invisible, wider grab zone so the 3px bar is realistically hittable with a
   right-click without changing how it looks. Events over it are attributed to
   the .scroll-thumb host, so the drag handlers fire. */
.scroll-thumb::before {
  content: "";
  position: absolute;
  top: -4px;
  bottom: -4px;
  right: -6px;
  left: -12px;
}

/* Gentle pulse — deliberately shallow so it's barely noticeable. */
@keyframes scroll-thumb-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 0.52; }
}

/* Hidden while a project is open — the wheel drives the image stepping then,
   not the page scroll, so a scroll symbol would be misleading. animation:none
   lets the flat opacity:0 win (a running keyframe would otherwise override it)
   and the transition fades it out. */
body.project-open .scroll-thumb {
  animation: none;
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .project, .team-card, .contact-columns, .hero-slide { transition: none; animation: none; opacity: 1; transform: none; }
  .hero-tagline, .hero-slides { transform: none !important; animation: none !important; }
  /* No pulse, but the thumb stays visible at a steady, legible opacity. */
  .scroll-thumb { animation: none; opacity: 0.5; }
}
