@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 200 800;          /* variable wght axis */
  font-stretch: 75% 100%;        /* variable wdth axis (we use normal 100%) */
  font-optical-sizing: auto;     /* drives the opsz axis automatically */
  font-display: swap;
  src: url('/assets/fonts/BricolageGrotesque[opsz,wdth,wght].woff2') format('woff2');
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;          /* variable wght axis */
  font-display: swap;
  src: url('/assets/fonts/SpaceGrotesk[wght].woff2') format('woff2');
}
@font-face {
  font-family: 'Fira Code';
  font-style: normal;
  font-weight: 400;              /* static — mono is 400 everywhere */
  font-display: swap;
  src: url('/assets/fonts/FiraCode-Regular.woff2') format('woff2');
}

:root {
  --font-display: 'Bricolage Grotesque', ui-sans-serif, system-ui, sans-serif;
  --font-body:    'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
  --font-mono:    'Fira Code', ui-monospace, monospace;
}

.site-footer, .contrib-head, .detail-more-head {
  font-variant-ligatures: none;
  font-feature-settings: "calt" 0, "liga" 0;
}

/* ---- Base ---- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--accent-text); }

h1, h2, h3 { margin: 0; font-family: var(--font-display); font-weight: 700; letter-spacing: 0.01em; line-height: 1.2; }
img, video { display: block; max-width: 100%; height: auto; }
.icon { width: 1.2rem; height: 1.2rem; }
:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; border-radius: 2px; }

/* ---- Header: full-bleed, sticky, quiet ---- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.7rem var(--gutter);
  background: color-mix(in srgb, var(--bg) 68%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--hairline);
}
.brand { display: flex; flex-direction: row; align-items: center; min-width: 0; }
.brand-text { display: flex; flex-direction: row; align-items: baseline; min-width: 0; line-height: 1.2; }
.brand-logo { height: 2rem; width: auto; margin-right: 1.05rem; }
.brand:hover { color: inherit; }
.brand-mark { font-family: var(--font-display); font-weight: 700; font-size: 1.38rem; letter-spacing: 0.03em; }
.brand-desc { font-size: 0.9rem; color: var(--text-muted); white-space: nowrap; }
.brand-desc::before { content: "/"; margin: 0 0.5rem; opacity: 0.6; }
.site-nav { display: flex; align-items: center; gap: 0.25rem; flex-shrink: 0; }
.nav-link {
  padding: 0.35rem 0.8rem;
  font-size: 0.9rem;
  color: var(--text-muted);
  border-radius: 999px;
  transition: color 120ms ease, box-shadow 120ms ease;
}
.nav-link:hover {
  color: var(--text);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--hairline);
}
/* Scroll-spy active state (Phase 4, refined) — same pill shape/sizing as
   the hover state (inset:0 = identical box to the hover box-shadow above),
   but clipped down to just the bottom third of that shape, so it reads as
   a small accent-colored "cup" under the label rather than a duplicate of
   the hover pill. */
.nav-link.is-active {
  position: relative;
  color: var(--accent-text);
}
.nav-link.is-active::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 999px;
  border: 1.5px solid var(--accent-text);
  clip-path: inset(66% 0 0 0);
  pointer-events: none;
}

.site-main {
  flex: 1 0 auto;
  width: 100%;
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--gutter);
}

.site-footer {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  flex-wrap: wrap;
  padding: 1rem var(--gutter);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  border-top: 1px solid var(--hairline);
}
.footer-left,
.footer-right { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.site-footer .sep { opacity: 0.5; }
.footer-social { display: inline-flex; align-items: center; color: inherit; }
.footer-social:hover { color: var(--accent-text); }
.case-exempt { text-transform: none; }

.hero {
  max-width: 46rem;
  margin: 0 auto;
  padding: 3.5rem 0 2rem;
  text-align: center;
}

.hero-logo {
  display: block;
  width: min(16rem, 45%);
  aspect-ratio: 35 / 20;
  height: auto;
  margin: 0 auto 1.25rem;
}

@media (max-width: 40rem) {
  .hero-logo { width: min(8rem, 55%); margin-bottom: 1rem; }
}

.hero-title {
  font-size: 4.5rem;
  line-height: 1.08;
}
.hero-line {
  font-size: 3.7rem;
  line-height: 1.08;
}
.hero-title .hero-name,
.hero-title .hero-line { display: block; }
.hero-tagline {
  margin: 0 0 0;
  font-weight: 500;
  font-size: 2.3rem;
  color: var(--accent-text);
}
.hero-intro {
  margin: 0.5rem auto 0;
  max-width: 38rem;
  color: var(--text-muted);
  font-size: 1rem;
  line-height: 1.65;
}
.hero-cta {
  display: flex;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 1.8rem;
  flex-wrap: wrap;
}

/* Shared pill buttons — .btn-fill (lime) / .btn-outline (white, the header
   nav-hover style reuses this same look, see .nav-link:hover above). */
.btn {
  display: inline-flex;
  align-items: center;
  padding: 0.7rem 1.4rem;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.92rem;
  border-radius: 999px;
  border: 1px solid transparent;
  transition: filter 120ms ease, border-color 120ms ease, background-color 120ms ease;
}
.btn-fill {
  background: var(--accent);
  color: var(--text);
}
.btn-fill:hover { filter: brightness(1.05); color: var(--text); }
.btn-outline {
  background: var(--surface);
  border-color: var(--hairline);
  color: var(--text);
}
.btn-outline:hover { border-color: var(--text-muted); color: var(--text); }

@media (max-width: 40rem) {
  .hero-title { font-size: 2.3rem; }
  .hero-line { font-size: 2rem; }
  .hero-tagline { font-size: 1.15rem; }
}

.services {
  max-width: 68rem;
  margin: 0 auto;
  padding: 2.5rem 0 3.5rem;
}
.services-head,
.projects-head {
  text-align: center;
  font-size: 2.5rem;
  margin-bottom: 1.8rem;
}
/* Extra inset for the gallery specifically (not hero/services/contact,
   which keep the base .site-main gutter). Total left/right margin here is
   the site-main gutter plus this one — double the margin, gallery only. */
.projects-section {
  padding-inline: var(--gutter);
}
@media (max-width: 39.99rem) {
  /* Mobile only: cancel .site-main's own gutter and replace with a much
     smaller inset, so gallery cards sit close to the screen edges. */
  .projects-section {
    margin-inline: calc(-1 * var(--gutter));
    padding-inline: 0.5rem;
  }
}
.contact-head {
  text-align: center;
  font-size: 3rem;
  margin-bottom: 1.8rem;
}
.services-grid {
  display: grid;
  grid-template-columns: 1fr;   /* mobile: 1 */
  gap: var(--gutter);
}
@media (min-width: 40rem) {
  .services-grid { grid-template-columns: repeat(3, 1fr); }
}
.service-card {
  position: relative;
  padding: 1.3rem 1.2rem;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  transition: transform 220ms ease-in-out, box-shadow 220ms ease-in-out;
}
.service-link {
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
@media (hover: hover) and (pointer: fine) {
  .service-card:hover,
  .service-card:focus-within {
    transform: translateY(-8px) scale(1.016);
    box-shadow: 0 16px 28px rgba(0, 0, 0, 0.14);
  }
}
.service-ico {
  display: inline-flex;
  color: var(--accent-text);
  margin-bottom: 0.6rem;
}
.service-ico .icon { width: 1.6rem; height: 1.6rem; }
.service-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1rem;
  margin-bottom: 0.4rem;
}
.service-desc {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.85rem;
  line-height: 1.55;
}

/* =====================================================================
   GALLERY
   ===================================================================== */

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin-bottom: var(--gutter);
}
.filter-btn,
.chip {
  display: inline-flex;
  align-items: center;
  padding: 0.3rem 0.7rem;
  font-size: 0.8rem;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  transition: color 120ms ease, border-color 120ms ease, background-color 120ms ease;
}
.filter-btn:hover,
.chip:hover { color: var(--text); border-color: var(--text-muted); }
.filter-btn.is-active {
  color: var(--text);            /* dark ink on lime-500 fill (#113) */
  background: var(--accent);
  border-color: var(--accent);
}

.gallery-grid {
  --gallery-gap: 0.7rem;                    /* mobile: tighter than --gutter */
  display: grid;
  grid-template-columns: repeat(2, 1fr);   /* mobile: 2 */
  gap: var(--gallery-gap);
  align-items: start;                       /* natural heights -> ragged bottoms */
}
@media (min-width: 40rem) {
  .gallery-grid {
    grid-template-columns: repeat(3, 1fr);   /* mid: 3 */
    --gallery-gap: var(--gutter);
  }
}
@media (min-width: 72rem) {
  .gallery-grid {
    grid-template-columns: repeat(4, 1fr);   /* large: 4 max */
    --gallery-gap: calc(var(--gutter) + 0.5rem);  /* desktop: a bit more breathing room */
  }
}
/* True staggered masonry (Phase 4, JS-applied only — .js-masonry is added by
   site.js). Fine-grained row-span technique: each card's row-end span is
   computed from its own rendered height, so cards keep normal grid reading
   order (order.json order) instead of being packed into shortest-column like
   classic column-masonry, which would reorder them. No-JS visitors keep the
   plain grid above (ragged bottoms, no gap-filling — that's fine). */
.gallery-grid.js-masonry {
  grid-auto-rows: 1px;
  row-gap: 0;                          /* real vertical gap moves to card margin below (#161) */
  margin-bottom: calc(-1 * var(--gallery-gap)); /* cancel trailing margin under the last row */
}
.gallery-grid.js-masonry > .card {
  margin-bottom: var(--gallery-gap);
}

.gallery-empty { color: var(--text-muted); }
.gallery-notice {
  margin: 0 auto var(--gutter);
  max-width: 32rem;
  padding: 0.7rem 1rem;
  text-align: center;
  color: var(--text);            /* dark ink on lime-500 fill (#113) */
  background: var(--accent);
  border-radius: var(--radius);
}

/* ---- Card ---- */
.card {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--hairline);
}
.card-media { position: relative; display: block; }
.card-media img,
.card-media picture { display: block; width: 100%; height: auto; }

.card-vid {
  position: absolute;
  top: 0.55rem;
  left: 0.55rem;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 999px;
  background: var(--accent);       /* lime circle */
  color: var(--text);              /* near-black triangle (#118) */
  pointer-events: none;
}
.card-vid svg { width: 1.02rem; height: 1.02rem; margin-left: 1px; }  /* no crop (#92) */

.card-link { position: absolute; inset: 0; }
.card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; }

.card-label {
  padding: 0.4rem 0.6rem;
  text-align: center;
  font-size: 0.85rem;
  color: color-mix(in srgb, var(--text) calc(var(--card-label) * 100%), transparent);
}

.card-overlay { display: none; }

@media (hover: hover) and (pointer: fine) {
  .card {
    transition: transform 220ms ease-in-out, box-shadow 220ms ease-in-out;
  }
  .card:hover,
  .card:focus-within {
    transform: translateY(-8px) scale(1.016);
    box-shadow: 0 16px 28px rgba(0, 0, 0, 0.14);
    /* Masonry packs cards tightly (minimal gap between rows), so a lifted
       card now sits close enough to its neighbors that, without this, a
       later-DOM-order sibling paints over the translated card and hides
       the lift — only the box-shadow peeking out around the edges was
       visible. Explicit z-index puts the hovered card above them. */
    z-index: 5;
  }

  .card-overlay {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: 0.5rem;
    position: absolute;
    inset: 0;
    z-index: 2;
    padding: 0.7rem;
    opacity: 0;
    transition: opacity 220ms ease-in-out;  /* fade in on hover; no image darkening */
    pointer-events: none;
  }
  .card:hover .card-overlay,
  .card:focus-within .card-overlay { opacity: 1; }


  /* Year: its own little box, top-right (reveals on hover). */
  .card-year {
    position: absolute;
    top: 0.6rem;
    right: 0.6rem;
    padding: 0.15rem 0.5rem;
    font-size: 0.78rem;
    color: var(--text);
    border: 1px solid var(--hairline);
    border-radius: 999px;
    background: color-mix(in srgb, var(--bg) calc(var(--card-overlay) * 100%), transparent);
  }

  /* Categories above the card-text rectangle; informational (not links). */
  .card-cats { display: flex; flex-wrap: wrap; gap: 0.35rem; }
  .card-cats .chip {
    color: var(--text);                   /* white labels */
    border: 1px solid var(--hairline);
    background: color-mix(in srgb, var(--bg) calc(var(--card-overlay) * 100%), transparent);
  }

  /* Card text — the only thing beneath the categories — on a rounded rect. */
  .ov-text {
    padding: 0.5rem 0.6rem;
    text-align: left;
    font-size: 0.8rem;
    color: var(--text);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: color-mix(in srgb, var(--bg) calc(var(--card-overlay) * 100%), transparent);
  }
}

.contact-section {
  max-width: 33.8rem;
  margin: 0 auto;
  padding: 2.5rem 0 1.5rem;
}
.contact-reply,
.contact-intro {
  text-align: center;
  color: var(--text-muted);
  font-size: 0.9rem;
}
.contact-reply { margin-bottom: 0.15rem; }
.contact-intro { margin-bottom: 1.5rem; }
.contact-intro a { color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px; }
.contact-form { display: flex; flex-direction: column; gap: 0.3rem; }
.contact-form label { font-size: 0.95rem; color: var(--text-muted); margin-top: 0.45rem; }
.contact-form label:first-of-type { margin-top: 0; }
.contact-form input,
.contact-form textarea {
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 0.5rem 0.6rem;
}
.contact-form input::placeholder,
.contact-form textarea::placeholder { color: var(--text-muted); opacity: 0.5; }
.contact-form input:focus,
.contact-form textarea:focus { outline: none; border-color: var(--accent-text); }
.contact-form textarea { resize: vertical; }
.contact-form button {
  margin-top: 0.8rem;
  padding: 0.55rem 0.9rem;
  font: inherit;
  cursor: pointer;
  color: var(--text);            /* dark ink on lime-500 fill (#113) */
  background: var(--accent);
  border: 0;
  border-radius: var(--radius);
}
.contact-form button:hover { filter: brightness(1.08); }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* JS-enhanced result banner (3R.4a) — sits in-flow directly above the fields
   (not a page-corner toast, so it can't be missed) and fades down into place.
   [hidden] covers the no-JS case; the script only clears it once it has real
   content to show, and re-sets it after the fade-out completes. */
.contact-toast {
  margin-bottom: 1rem;
  padding: 0.7rem 1rem;
  text-align: center;
  font-size: 0.88rem;
  color: var(--text);             /* dark ink on lime-500 fill, same as .btn-fill */
  background: var(--accent);
  border-radius: var(--radius);
  opacity: 0;
  transform: translateY(-0.5rem);
  transition: opacity 200ms ease, transform 200ms ease;
}
.contact-toast.is-visible {
  opacity: 1;
  transform: translateY(0);
}
.contact-toast.is-error {
  color: var(--accent-text);
  background: var(--surface);
  border: 1px solid var(--accent-text);
}

.contact-result { max-width: 42rem; margin-inline: auto; padding: 2.5rem 0; text-align: center; }
/* Failure state: the form comes back pre-filled, so it needs form layout. */
.contact-retry { text-align: left; }
.contact-retry h1 { text-align: center; }
.contact-error {
  color: var(--accent-text);
  text-align: center;
  margin-bottom: 1.2rem;
}
.contact-retry .contact-form { margin-bottom: 1.2rem; }
.contact-retry > p:last-child { text-align: center; }
.contact-result h1 { font-weight: 700; margin-bottom: 0.6rem; }
.contact-result p { color: var(--text-muted); margin-bottom: 0.5rem; }
.contact-result a { color: var(--accent-text); }

/* ---- Stub helper (temporary; removed as templates land) ---- */
.stub { color: var(--text-muted); }
.stub strong { color: var(--text); font-weight: 400; }

/* ---- Responsive ---- */
@media (max-width: 30rem) { .brand-text { display: none; } }

/* ---- Accessibility floor ---- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* Scroll reveal (Phase 4). `.motion-ready` is added by site.js right before
   it starts observing cards — never in CSS — so a no-JS visitor's cards are
   never given the hidden starting state in the first place. Reduced-motion
   visitors still get `.in-view` applied by the observer; the accessibility
   floor above strips the transition, so it snaps instantly instead of
   animating. Scoped to `.gallery-grid` only — `.more-projects-grid` sits at
   the very bottom of a detail page, right before the footer, where reveal
   was producing an intermittent flicker and adds little value anyway (a
   visitor scrolling that far is already deliberately reading the page). */
.motion-ready .gallery-grid > .card {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 420ms ease, transform 420ms ease;
}
.motion-ready .gallery-grid > .card.in-view {
  opacity: 1;
  transform: none;
}
/* Once a card has `.in-view`, the rule above (4 class-level selectors,
   specificity 0,4,0) otherwise permanently outranks the plain `.card:hover`
   rule (0,2,0) and pins transform: none regardless of hover state — the
   card's box-shadow still showed on hover since that property isn't
   touched by the reveal rule, but the lift/scale never could. Matching
   the specificity here restores it without needing !important. */
@media (hover: hover) and (pointer: fine) {
  .motion-ready .gallery-grid > .card.in-view:hover,
  .motion-ready .gallery-grid > .card.in-view:focus-within {
    transform: translateY(-8px) scale(1.016);
  }
}

/* Deferred particles (Phase 4, refined) — a fixed full-viewport backdrop
   (`.hero-particles-bg`, injected as the first child of <body> by site.js),
   not scoped to the hero box, so it reads as depth behind the whole page
   instead of scrolling with hero content. `body` is given its own stacking
   context so the negative z-index canvas paints behind body's normal
   in-flow content (header/main/footer) but above the plain --bg fill. */
body { position: relative; z-index: 0; }
.hero-particles-bg { position: fixed; inset: 0; z-index: -1; pointer-events: none; }

/* Cross-document view transitions (native, zero-JS) — makes real page
   navigations (e.g. landing -> /impressum) slide rather than hard-cut, so
   the fixed particle backdrop reads as one continuous surface even though
   it's technically a fresh canvas on every page. Chrome/Edge 126+ and
   Safari 18.2+ as of mid-2026; unsupported browsers (Firefox, at time of
   writing) just get today's instant navigation — this is pure progressive
   enhancement, nothing to fall back to manually. Explicitly gated on
   reduced-motion here rather than relying on the `*` floor above, since
   ::view-transition-* are generated pseudo-elements outside the normal DOM
   tree and aren't reliably matched by a universal selector in every engine. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.5s;
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  }
  ::view-transition-old(root) {
    animation-name: site-slide-out;
  }
  ::view-transition-new(root) {
    animation-name: site-slide-in;
  }
  @keyframes site-slide-out {
    to { transform: translateX(-9%); opacity: 0; }
  }
  @keyframes site-slide-in {
    from { transform: translateX(9%); opacity: 0; }
  }

  /* Smooth in-page scroll for the header nav links (Services/Projects/
     Contact) instead of an instant jump. Scoped inside this same
     no-preference block, so reduced-motion visitors keep the plain
     instant jump — no separate override needed since the rule below
     simply won't apply for them. */
  /* Smooth in-page scroll for the header nav links (Services/Projects/
     Contact). Gated behind `.smooth-scroll-ready`, added by site.js only
     after the `load` event — NOT a blanket `html{scroll-behavior:smooth}`.
     A blanket rule also smooth-animates the browser's automatic
     scroll-to-fragment on page load (e.g. landing on /#services from
     another page), which then visibly races the view-transition slide
     above and stops short. Deferring it past `load` keeps the initial
     fragment jump instant and only smooths later, click-triggered scrolls. */
  html.smooth-scroll-ready {
    scroll-behavior: smooth;
  }
}

.detail {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "hero" "text" "media";   /* text above media */
  gap: var(--gutter);
  align-items: start;
}
.detail.has-contribs { grid-template-areas: "hero" "aside" "media"; }
/* No additional media: drop the "media" row entirely rather than leaving an
   empty grid track — `gap` still applies between template rows even when a
   row has no content, which otherwise doubled the space before the footer. */
.detail.no-media { grid-template-areas: "hero" "text"; }
.detail.no-media.has-contribs { grid-template-areas: "hero" "aside"; }

.detail-hero  { grid-area: hero; }
.detail-aside { grid-area: aside; }
.detail-text  { grid-area: text; }   /* used only when NO contributions */
.detail-more  { grid-area: media; }

/* Right-column stack: text ABOVE contributions, tight gap (not the page gutter). */
.detail-aside { display: flex; flex-direction: column; gap: var(--gutter); }

/* Desktop: fixed contribution rail beside the hero; text still above media. */
@media (min-width: 48rem) {
  .detail.has-contribs {
    grid-template-columns: 1fr 260px;
    grid-template-areas:
      "hero  aside"
      "media media";
    align-items: start;
  }
  .detail.no-media.has-contribs {
    grid-template-areas: "hero aside";
  }
}

/* ---- Hero ---- */
.detail-hero img,
.detail-hero picture { display: block; width: 100%; height: auto; }
.detail-hero .media-item { border-radius: var(--radius); overflow: hidden; margin-inline: auto; }
/* Rounded framing on the media itself — parent clipping alone can be defeated
   by the <a>/<picture> wrappers, so round the rendered element directly. */
.detail-hero img,
.detail-hero picture,
.media-item img,
.media-item picture { border-radius: var(--radius); }
/* Returning from the lightbox jumps to the image's id; keep it clear of the
   sticky header instead of tucking it underneath. */
.media-item { scroll-margin-top: 6rem; }
/* Anchor-jump targets from the header nav — without this the sticky header
   covers the top of the heading on landing. */
#services, #projects, #contact { scroll-margin-top: 6rem; }
/* Portrait IMAGE hero: cap height (~80vh), centered — no crop */
.detail.hero-portrait .detail-hero img {
  width: auto; max-width: 100%; max-height: 80vh; margin-inline: auto;
}

.detail-contribs {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.9rem 1rem;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}
@media (max-width: 48rem) {
  .detail-contribs { flex-direction: row; flex-wrap: wrap; gap: 0.6rem 1.1rem; }
}
.contrib { display: inline-flex; align-items: center; gap: 0.55rem; font-size: 0.85rem; color: var(--text); }
.contrib-ico { flex: 0 0 auto; display: inline-flex; color: var(--contrib-did); }
.contrib-ico svg { display: block; }
.contrib-head {
  flex-basis: 100%;
  margin: 0 0 0.15rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.contrib.is-didnt .contrib-label { color: var(--text-muted); text-decoration: line-through; opacity: 0.7; }
.contrib.is-didnt .contrib-ico   { color: var(--contrib-did); opacity: var(--contrib-muted); }

/* ---- Project details — boxed ---- */
.detail-text {
  padding: 1rem 1.15rem;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}
.detail-title { font-size: 1.25rem; font-weight: 400; color: var(--accent-text); }
.detail-meta {
  margin-top: 0.45rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}
.detail-meta strong { font-weight: 700; color: var(--text); }
.detail-desc { margin-top: 0.7rem; max-width: 62ch; font-weight: 400; font-size: 0.85rem; }
.detail-desc a { color: var(--accent-text); }
.detail-desc strong, .detail-desc b { font-weight: 700; color: var(--text); }

/* ---- Additional media grid (max 3/row, ragged, no crop) ---- */
.detail-more-head {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 0.7rem;
}
.detail-media {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gutter);
  align-items: start;
}
@media (max-width: 40rem) { .detail-media { grid-template-columns: 1fr; } }
/* True staggered masonry (JS-applied only, same technique/rationale as
   .gallery-grid.js-masonry above). No-JS visitors keep the plain grid
   above (ragged bottoms, no gap-filling — that's fine). */
.detail-media.js-masonry {
  grid-auto-rows: 1px;
  row-gap: 0;                          /* same row-span quantization fix as .gallery-grid (#161) */
  margin-bottom: calc(-1 * var(--gutter));
}
.detail-media.js-masonry > .media-item {
  margin-bottom: var(--gutter);
}
.media-item { border-radius: var(--radius); overflow: hidden; }
.media-item img,
.media-item picture { display: block; width: 100%; height: auto; }

/* Additional-media items get the same lift+shadow hover as gallery/more-
   projects cards (.card:hover above), reusing identical timing/values so
   hover feels consistent across the site. Hero media-item is excluded via
   the .detail-media scope — only the additional-media grid gets this. */
@media (hover: hover) and (pointer: fine) {
  .detail-media .media-item {
    transition: transform 220ms ease-in-out, box-shadow 220ms ease-in-out;
  }
  .detail-media .media-item:hover,
  .detail-media .media-item:focus-within {
    transform: translateY(-8px) scale(1.016);
    box-shadow: 0 16px 28px rgba(0, 0, 0, 0.14);
    z-index: 5;
  }
}
.media-video { /* sits in the normal grid cell now, same as images — was previously forced full-width via grid-column:1/-1, which degenerated the layout into a single stacked column whenever a project had multiple videos */ }

/* ---- Video sizing: landscape fills width (natural height, no bars);
       square/portrait capped at 80vh + centered ---- */
.detail-video {
  display: block;
  background: #000;
  margin-inline: auto;
  object-fit: contain;
  border-radius: var(--radius);
}
.media-video--wide .detail-video { width: 100%; }
.media-video--tall .detail-video { width: auto; height: auto; max-height: 80vh; max-width: 100%; }

/* Additional-media video tile: poster + centered play affordance. The real
   player only exists inside the lightbox (see .lightbox--video below) — no
   inline <video> in the grid itself. */
.media-video-tile {
  position: relative;
  display: block;
}

/* Lightbox video: same "as large as the viewport allows" sizing as the
   image lightbox, just targeting the <video> instead of an <img>. */
.lightbox-fig--video .detail-video {
  display: block;
  max-width: 90vw;
  max-height: 90vh;
  width: auto;
  height: auto;
  border-radius: var(--radius);
}

/* ---- Bookending: More projects + Back link (3R.5) ---- */
.detail-more-projects {
  margin: 3rem auto 0;
}
.more-projects-head {
  text-align: center;
  font-size: 1.8rem;
  margin-bottom: 1.5rem;
}
/* Fixed row height instead of uniform grid columns — width is driven by each
   card's own aspect-ratio (already set inline on the <img> by
   gallery_picture()), so portrait thumbnails come out narrower rather than
   being stretched to match landscape ones. */
.more-projects-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gutter);
}
.more-projects-grid .card { flex: 0 0 auto; }
/* Long client names (e.g. "Vienna International Airport") were forcing the
   whole card wider than its own image — a flex:0 0 auto item sizes to the
   widest of its children, and nothing constrained the label's width. That
   visibly mismatched the card's border/background against the image
   inside it whenever the label was wider than an unusually narrow/tall
   image. Capping width to the image keeps the card's rendered width always
   equal to its image's width; wrapping (instead of nowrap+ellipsis) is used
   so overlong labels break onto a second centered line rather than being
   truncated off-center — text-align:center + nowrap + ellipsis clips
   silently on the left and visibly on the right, which reads as the label
   being shoved left (#more-projects-label-center). */
.more-projects-grid .card-label {
  max-width: 100%;
  overflow-wrap: break-word;
  word-break: break-word;
}
.more-projects-grid .card-media {
  width: auto;
  height: 198px;
  display: flex;
  justify-content: center;
}
.more-projects-grid .card-media picture,
.more-projects-grid .card-media img {
  width: auto;
  height: 198px;
  max-width: 100%;
}
@media (max-width: 40rem) {
  .more-projects-grid .card-media,
  .more-projects-grid .card-media picture,
  .more-projects-grid .card-media img { height: 144px; }
}
/* Always a single row (JS-applied only — .js-single-row is added by
   site.js). No scaling: cards keep the fixed 198px/144px height above;
   site.js hides whichever cards don't fit (from the end) and this centers
   whatever's left. No-JS visitors keep the plain wrapping grid above. */
.more-projects-grid.js-single-row {
  flex-wrap: nowrap;
  justify-content: center;
}
.detail-back {
  text-align: center;
  margin: 2rem 0 1rem;
  font-size: 0.9rem;
}
.detail-back a { color: var(--accent-text); }

/* Extra side clearance so the fixed prev/next arrows below don't sit
   directly on top of the hero, contributions rail, or the More-projects
   strip — same breakpoint the arrows themselves appear at. */
@media (min-width: 60rem) {
  .detail,
  .detail-more-projects {
    padding-inline: 3.5rem;
  }
}

/* ---- Prev/next project navigation (fixed to the viewport edges) ---- */
.detail-nav {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--text);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.detail-nav:hover { border-color: var(--text-muted); color: var(--accent-text); }
.detail-nav .icon { width: 1.3rem; height: 1.3rem; }
.detail-nav--prev { left: 1rem; }
.detail-nav--next { right: 1rem; }
/* No room for side arrows below this width — they'd sit on top of content
   rather than beside it. */
@media (max-width: 60rem) {
  .detail-nav { display: none; }
}

/* ---- Lightbox (CSS-only :target; full-resolution image) ---- */
.zoom { display: block; cursor: zoom-in; }
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--gutter);
}
.lightbox:target,
.lightbox.is-open { display: flex; }
.lightbox-bg {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--bg) 90%, transparent);
  /* No cursor: zoom-out anymore — background click no longer closes
     (X / Escape only, see site.js). Left as a plain dimmed backdrop. */
}
.lightbox-fig {
  position: relative;
  z-index: 1;
  display: flex;               /* centers the image within the canvas below */
  align-items: center;
  justify-content: center;
  overflow: hidden;            /* clips the zoomed image to this canvas */
  /* Always the full available canvas, not shrink-wrapped to the image's
     own size — previously this box matched the image exactly, so zooming
     was clipped to that original (often much smaller) crop area instead
     of using the whole screen. site.js's pan math reads this box's live
     size (fig.getBoundingClientRect()) to know how much room is actually
     available, so it stays in sync with whatever this resolves to. */
  width: 100%;
  height: 90vh;
}
.lightbox-fig img {
  display: block;
  max-width: 100%;
  max-height: 90vh;
  width: auto;
  height: auto;
  border-radius: var(--radius);
  /* transform-origin:0 0 keeps the wheel-zoom math in site.js (which
     assumes scaling happens from the image's own top-left corner) simple —
     default center-origin would need a different, messier formula.
     touch-action:none stops the browser treating a one-finger drag on the
     zoomed image as a page-scroll gesture, so our own drag-to-pan gets a
     clean pointer sequence instead of fighting native scrolling. */
  transform-origin: 0 0;
  touch-action: none;
}
.lightbox-fig.is-zoomed img { cursor: grab; }
.lightbox-fig.is-zoomed img.is-dragging { cursor: grabbing; }
.lightbox-fig:not(.is-zoomed) img { cursor: zoom-in; } /* hints scroll/click-to-zoom now that click no longer closes */

/* Desktop-only hint, next to the controls — touch devices have no wheel,
   so this would be misleading advice there. Gated the same way the site's
   existing hover-only affordances are (see .card hover block above). */
.lightbox-hint {
  display: none;
  font-size: 0.78rem;
  color: color-mix(in srgb, var(--text) 70%, transparent);
  white-space: nowrap;
  align-self: center;
  margin-right: 0.25rem;
}
@media (hover: hover) and (pointer: fine) {
  .lightbox-hint { display: inline; }
}

/* Minimap (indicator-only): small scaled-down copy of the same image
   (same URL the main lightbox already loaded — browser cache serves this
   second <img> for free, no extra request) with an overlay rectangle
   showing the currently-visible region. Only shown once zoomed past fit;
   toggled via the `hidden` attribute in site.js rather than a CSS class,
   since `hidden` is the correct semantic default-off state here. */
.lightbox-minimap {
  position: absolute;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 3;
  width: 8.5rem;
  overflow: hidden;
  border-radius: calc(var(--radius) * 0.6);
  border: 1px solid var(--hairline);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
}
.lightbox-minimap img { display: block; width: 100%; height: auto; }
.lightbox-minimap-view {
  position: absolute;
  border: 2px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  pointer-events: none; /* indicator-only, per 3R.6 scope decision — not click-to-navigate */
}

.lightbox-controls {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.lightbox-controls button,
.lightbox-controls a {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: color-mix(in srgb, var(--bg) 85%, transparent);
  color: var(--text);
  font-size: 1.25rem;
  line-height: 1;
  font-family: inherit;
  cursor: pointer;
}
.lightbox-controls button svg { width: 1.15rem; height: 1.15rem; }
@media (hover: hover) and (pointer: fine) {
  .lightbox-controls button:hover,
  .lightbox-controls a:hover { background: var(--accent); }
}

.legal {
  max-width: 44rem;
  margin-inline: auto;
  padding: 1.25rem 0 2.5rem;
  font-size: 0.82rem;      /* main text: smaller */
  line-height: 1.65;
}
.legal h1 {
  color: var(--accent-text);    /* "Impressum" / "Legal Notice" */
  font-weight: 400;
  font-size: 1.2rem;       /* headline */
  letter-spacing: 0.02em;
  margin-bottom: 0.4rem;
}
.legal h2 {
  font-weight: 400;
  font-size: 0.9rem;
  margin: 1.5rem 0 0.4rem;
}
.legal-intro { color: var(--text-muted); font-size: 0.78rem; margin-bottom: 1.2rem; }
.legal-name { margin-bottom: 1.2rem; }
.legal-name strong { font-weight: 700; }

/* Inline "Label: value" rows — no line break between label and value. */
.legal-rows { margin: 0; }
.legal-row { display: block; margin: 0 0 0.15rem; }
.legal-row dt { display: inline; color: var(--text-muted); }
.legal-row dt::after { content: ":"; }
.legal-row dd { display: inline; margin: 0 0 0 0.35rem; }

.legal-note { color: var(--text-muted); }
.legal a { color: var(--accent-text); }
.legal-sep { border: 0; border-top: 1px solid var(--hairline); margin: 2.25rem 0; }

.mx-b {
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  padding: 0.4rem 1.1rem;
  border-radius: var(--radius);
  border: 1px solid var(--hairline);
  background: transparent;
  color: var(--text);
  transition: border-color 120ms ease, filter 120ms ease;
}
.mx-no:hover { border-color: var(--text-muted); }
.mx-ok {
  background: var(--accent);       /* lime fill, dark ink */
  border-color: var(--accent);
  color: var(--text);
  font-weight: 500;
}
.mx-ok:hover { filter: brightness(1.05); }
/* Footer reopen link (injected) — inherits footer link styling. */
.mx-re {
  font: inherit;
  cursor: pointer;
  padding: 0;
  margin: 0;
  background: none;
  border: 0;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
}
.mx-re:hover { color: var(--accent-text); }