/**
 * Sticky Card Stack Component
 * ============================================================
 * Scroll-triggered stacking effect for image showcase.
 * 
 * Inspired by Skiper UI Card Stack component
 * Original concept: @gurvinder-singh02 (https://gxuri.in)
 * Attribution: Skiper UI
 * License: Free to use with attribution; no attribution required with Skiper UI Pro
 * 
 * This is a custom implementation adapted for vanilla JS/CSS
 * (not a direct port of the React/Framer Motion original)
 * ============================================================ */

/* ── Portfolio Showcase Section ──────────────────────────── */
.portfolio-showcase {
  position: relative;
  display: flex;
  width: 100%;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-top: 4vh;
  padding-right: 0;
  /* Scroll-Auslauf NACH der letzten Karte: haelt sie kurz pinned, dann
     Uebergabe an die naechste Sektion. War 80vh (zu viel Leerraum), dann 25vh
     (Owner-Feedback: jetzt zu wenig Halt/Abstand unterhalb) — 40vh als
     Mittelweg: spuerbare Pause, kein langer Leerlauf. */
  padding-bottom: 40vh;
  padding-left: 0;
  background: var(--color-bg);
}

/* ── Scroll Hint ─────────────────────────────────────────── */
.showcase-scroll-hint {
  position: absolute;
  top: 15%;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  z-index: 1;
  opacity: 0.3;
  pointer-events: none;
}

.showcase-hint-text {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-widest);
  font-weight: var(--font-weight-semibold);
  text-align: center;
  max-width: 14ch;
  line-height: 1.3;
  color: var(--color-text);
}

.showcase-hint-line {
  width: 1px;
  height: 3rem;
  background: linear-gradient(to bottom, 
    transparent 0%, 
    var(--color-text) 50%,
    transparent 100%
  );
  animation: scrollPulse 2s ease-in-out infinite;
}

@keyframes scrollPulse {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 0.6; }
}

/* ── Individual Sticky Card ──────────────────────────────── */
.sticky-card {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  /* Die volle Breite der (transparenten, überlappenden) Stapel-Reihe darf KEINE
     Pointer-Events fangen — sonst schnappt sich immer die im DOM oberste Stapel-
     Karte jeden Hover/Klick, egal wo die Maus wirklich ist. Nur die sichtbare
     .sticky-card-content ist interaktiv (unten wieder auf auto gesetzt). */
  pointer-events: none;
}

/* Karte unter der Maus nach vorn holen (überschreibt die DOM-Paint-Reihenfolge),
   damit EXAKT die gehoverte/fokussierte Karte vorne liegt, glüht und klickbar ist.
   ABER nur solange NICHT gescrollt wird: .is-scrolling setzt das Stack-JS während
   des Scrollens — „entweder pinnen ODER scrollen", nie beides, sonst überdeckt die
   gepinnte Karte den Scroll-Stack-Effekt.
 *
 * ── WARUM DAS AUF TOUCH NICHTS ZU SUCHEN HAT ────────────────────────────────
 * Auf einem Touchscreen gibt es kein „Verweilen ohne Absicht": der FINGER IST DER
 * SCROLL. Ungated wurde diese Hervorhebung deshalb auf Mobil vom Scrollen selbst
 * ausgelöst, aus zwei unabhängigen Quellen:
 *   1. EMULIERTER HOVER — iOS/Android setzen :hover auf das Element unter dem
 *      Finger (iOS hält ihn nach dem Tap sogar „klebrig", bis woanders getippt
 *      wird). Zusammen mit dem Velocity-Gate oben ergab das ein Zappeln, das an
 *      der FINGERGESCHWINDIGKEIT hing statt an einer Absicht: langsam ziehen
 *      (< V_SLOW) → .is-scrolling fällt weg → Karte springt auf z-index 5 und
 *      zoomt; minimal schneller ziehen → .is-scrolling zurück → Karte schnappt
 *      zurück. Genau das „überempfindliche", komische Verhalten.
 *   2. :focus-within — ein Tap fokussiert den Stretched-Link. Der Fokus überlebt
 *      die Geste, die Karte blieb also dauerhaft vorne + gezoomt hängen.
 * Fix, ohne Desktop anzufassen: Hover-Zweig hinter (hover: hover) and
 * (pointer: fine) — Haus-Konvention, vgl. buttons.css — und für die Tastatur
 * :focus-within → :has(a[href]:focus-visible). :focus-visible feuert bei echter
 * Tastatur-Navigation, NICHT beim Tap; die Karte enthält genau einen fokussier-
 * baren Knoten (a.sticky-card-link), die Reichweite bleibt also identisch.
 * Auf Touch bleibt als Feedback der native Tap-Highlight — der ordnet nichts um. */
.portfolio-showcase:not(.is-scrolling) .sticky-card:has(a[href]:focus-visible) {
  z-index: 5;
}

.portfolio-showcase:not(.is-scrolling) .sticky-card:has(a[href]:focus-visible) .sticky-card-content {
  box-shadow: var(--sticky-card-shadow-hover);
}

@media (hover: hover) and (pointer: fine) {
  .portfolio-showcase:not(.is-scrolling) .sticky-card:has(a[href]):hover {
    z-index: 5;
  }

  .portfolio-showcase:not(.is-scrolling) .sticky-card:has(a[href]):hover .sticky-card-content {
    box-shadow: var(--sticky-card-shadow-hover);
  }
}

/* ── Card Content ────────────────────────────────────────── */
.sticky-card-content {
  position: relative;
  top: calc(-0.5vh + 50px);
  width: 500px;
  height: 300px;
  max-width: min(500px, 88vw);
  border-radius: 2rem;
  overflow: hidden;
  /* Wieder interaktiv: Parent .sticky-card ist pointer-events:none (Anti-Capture),
     und pointer-events vererbt — hier also explizit zurück auf auto, damit exakt
     diese sichtbare Box Hover/Klick empfängt. */
  pointer-events: auto;
  /* Nicht-transparenter Hintergrund als Fallback für Bilder */
  background: var(--color-bg-dark);
  box-shadow: var(--sticky-card-shadow);
  /* Original: transform-origin top für korrektes Scaling */
  transform-origin: top center;
  transition: box-shadow var(--glow-speed) var(--glow-ease);
  will-change: transform;
}

.sticky-card-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--card-img-zoom-speed) var(--card-img-zoom-ease);
}

/* Hover/Fokus: Bild leicht heranzoomen (zentrale Card-Tokens; vom overflow:hidden der
   .sticky-card-content sauber beschnitten). Der scroll-getriebene Stack-Scale sitzt per JS
   auf .sticky-card-content, der Zoom auf dem <img> — getrennte Elemente, kein Konflikt.
   Gating wie beim z-index/Glow oben: Tastatur überall, Hover nur auf echten Zeigern —
   sonst zoomt auf Mobil das Bild unter dem scrollenden Finger. */
.portfolio-showcase:not(.is-scrolling) .sticky-card:has(a[href]:focus-visible) .sticky-card-img {
  transform: scale(var(--card-img-zoom));
}

@media (hover: hover) and (pointer: fine) {
  .portfolio-showcase:not(.is-scrolling) .sticky-card:has(a[href]):hover .sticky-card-img {
    transform: scale(var(--card-img-zoom));
  }
}

/* Claim-Kernwert (Maß/Norm) nicht umbrechen. */
.sticky-card-claim {
  white-space: nowrap;
}

/* ── Card Overlay ────────────────────────────────────────── */
.sticky-card-overlay {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: var(--space-6);
  background: var(--media-bottom-scrim);
  color: var(--on-dark-text);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  pointer-events: none;
}

.sticky-card-badge {
  display: inline-block;
  width: fit-content;
  font-size: var(--text-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  padding: var(--space-1) var(--space-3);
  background: var(--on-dark-surface-solid);
  backdrop-filter: blur(var(--blur-md));
  border-radius: var(--radius-sm);
  color: var(--on-dark-text);
}

.sticky-card-title {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--font-weight-bold);
  line-height: 1.2;
  margin: 0;
  color: var(--on-dark-text);
}

/* Zweite Beweis-Zeile unter dem Headline-Claim (z. B. Walzen-Bereiche neben der
   Abkant-Presskraft). Dezenter als der Titel — Mono, kleiner, leicht gedämpft —
   damit die Karte zwei Fähigkeiten trägt, ohne die Headline zu überladen. Die
   Ziffern (.sticky-card-claim) brechen weiter nicht um. */
.sticky-card-spec {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--tracking-snug);
  line-height: 1.3;
  color: var(--on-dark-text-soft);
}

/* Untergeordnete zweite Fähigkeit (z. B. Abkanten unter der Walzen-Headline): noch
   kleiner + stärker gedämpft als die Spec-Zeile — klare 3-Stufen-Hierarchie
   Headline › Detail › Unterzeile. Der Kernwert bleibt akzentuiert (.sticky-card-claim). */
.sticky-card-sub {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  line-height: 1.3;
  color: var(--on-dark-text-muted, var(--on-dark-text-soft));
  opacity: 0.85;
}

/* ── Responsive Adjustments ──────────────────────────────── */
@media (min-width: 768px) {
  .sticky-card-content {
    width: 500px;
    height: 300px;
    max-width: min(500px, 88vw);
  }

  .sticky-card-title {
    font-size: var(--text-2xl);
  }
}

/* ── Capability Parallax Gallery ──────────────────────────────
 *
 * ORIGIN / ATTRIBUTION
 * Standalone vanilla adaptation of "Oliver parallax" / Skiper30 by Skiper UI
 * (https://skiper-ui.com/v1/skiper30, author @gurvinder-singh02 / gxuri.in).
 * Deliberately NOT a code copy and no Skiper asset: the original is driven by an
 * animation library, this one by plain rAF + transform. Own Bock photos.
 *
 * THE THREE SOURCES OF THIS COMPONENT
 *   Look      HERE (.capability-parallax__*)
 *   Behaviour assets/js/parallax.js  — byte-identical with the Bock Strength shop
 *   Markup    templates/partials/page_sections.html.jinja (type: capability_parallax)
 * Guard: tests/test_parallax_ssot.py
 */
.capability-parallax {
  --capability-gap: 2vw;
  /* Theme-flippend: --color-gray-100 IST im Hellen der rohe --c-gray-100, kippt aber
     im Dark auf --c-scrim. Ein roher --c-gray-100 hier bliebe hell und stünde unter
     hellem --capability-ink (dark-on-hell → unsichtbar). */
  --capability-surface: var(--color-gray-100);
  --capability-ink: var(--color-text);
  position: relative;
  overflow: hidden;
  background: var(--capability-surface);
  color: var(--capability-ink);
}

.capability-parallax__screen {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-24) 0;
}

.capability-parallax__screen--outro {
  min-height: 100svh;
  align-items: start;
  padding-top: var(--space-24);
}

.capability-parallax__hint {
  position: absolute;
  left: 50%;
  top: 10%;
  z-index: 2;
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  gap: var(--space-6);
  text-align: center;
  color: var(--color-text-light);
  pointer-events: none;
}

.capability-parallax__hint span {
  position: relative;
  max-width: 12ch;
  font-size: var(--text-xs);
  font-weight: var(--font-weight-semibold);
  line-height: 1.2;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
}

.capability-parallax__hint span::after {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(100% + var(--space-5));
  width: 1px;
  height: 4rem;
  background: linear-gradient(180deg, transparent, var(--color-text-muted));
}

.capability-parallax__hint--up span::after {
  background: linear-gradient(180deg, var(--color-text-muted), transparent);
}

.capability-parallax__statement,
.capability-parallax__outro {
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: center;
  text-align: center;
}

.capability-parallax__statement {
  padding-top: var(--space-16);
}

.capability-parallax__statement h2 {
  max-width: 12ch;
  margin: 0;
  color: var(--color-text);
  font-family: var(--font-display);
  font-size: var(--text-5xl);
  font-weight: var(--font-weight-bold);
  line-height: 0.98;
}

.capability-parallax__statement p,
.capability-parallax__outro p {
  max-width: 720px;
  margin: var(--space-6) auto 0;
  color: var(--color-text-muted);
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
}

.capability-parallax__proofline {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4) var(--space-8);
  margin-top: var(--space-10);
  padding-top: var(--space-6);
  border-top: 1px solid var(--color-border);
}

.capability-parallax__proofline li {
  display: grid;
  justify-items: center;
  gap: var(--space-1);
}

.capability-parallax__proofline strong {
  display: block;
  color: var(--color-accent-text);
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  line-height: 1;
}

.capability-parallax__proofline span {
  color: var(--color-gray-600);
  font-size: var(--text-xs);
  font-weight: var(--font-weight-semibold);
  line-height: 1.2;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}

.capability-parallax__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3) var(--space-6);
  margin-top: var(--space-8);
}

.capability-parallax__links a {
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: var(--font-weight-semibold);
  text-decoration: none;
  border-bottom: 1px solid rgb(var(--c-red) / 0.55);
  padding-bottom: 0.18rem;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.capability-parallax__links a:hover,
.capability-parallax__links a:focus-visible {
  color: var(--color-accent-text);
  border-color: currentColor;
}

/* A real four-track parallax — images drift past each other at different speeds
   — and GAP-PROOF by construction. Each column stacks fixed-aspect images and is
   therefore naturally TALLER than the visible stage window; JS moves each column
   by at most the overflow it actually has (measured live), so a track reveals its
   lower images at its own speed but can NEVER expose a void. This replaced the
   old `top:-45%/-95%` pre-offset approach, which left big white gaps at rest.

   Tablet and mobile are the SAME rules with other numbers — the media queries
   below only remap the tokens declared here (and mobile drops tracks 3+4). There
   is no second layout to keep in sync. */
.capability-parallax__stage {
  /* ── GEOMETRIE-SSOT ────────────────────────────────────────────────────────
     Diese elf Zahlen sind die EINZIGE Quelle für das Layout, aus dem sich der
     Weg jeder Spur ergibt. Zwei Verbraucher lesen sie beim Bauen — der Renderer
     (build_site.py → seo_engine/media/parallax_geometry.py) berechnet daraus das Fenster,
     der Guard (tests/test_parallax_ssot.py) rechnet mit denselben Zahlen nach.
     Wer hier dreht, dreht damit automatisch auch am Fenster und am Guard; es
     gibt keine zweite Stelle, an der dieselben Maße noch einmal stünden.

     Das war vorher anders und genau deshalb kaputt: das Fenster stand hier als
     `clamp(560px, 72vh, 780px)` — geraten, an der Viewport-HÖHE hängend —, die
     Spurhöhe folgt aber der Viewport-BREITE (Kachelbreite × Bildformat). Auf
     einem schmal-hohen Schirm lief das auseinander; gemessen bei 1280×1200
     hatten zwei von vier Spuren exakt 0 px Weg, bei 1440×900 lief die
     schwächste 53 px und die stärkste 358 px. Das Fenster ist deshalb kein
     Geschmackswert mehr, sondern eine Ableitung aus dem Bildbestand. */
  --px-pad-desktop: 2vw;    --px-gap-desktop: 2vw;    --px-cols-desktop: 4;
  --px-pad-tablet: 2.5vw;   --px-gap-tablet: 2.5vw;   --px-cols-tablet: 4;
  --px-pad-mobile: 3vw;     --px-gap-mobile: 3vw;     --px-cols-mobile: 2;

  /* Der Design-Regler: welchen Anteil der Fensterhöhe die SCHNELLSTE Spur über
     den ganzen Scroll zurücklegt. Die übrigen Spuren folgen exakt im Verhältnis
     ihrer `data-speed` (siehe assets/js/parallax.js). Nur Python liest ihn —
     er steht trotzdem hier, damit alles, was die Bewegung bestimmt, an EINER
     Stelle steht und nicht die Hälfte in einem Skript. */
  --px-amplitude: 0.55;

  /* Deckel gegen eine Sektion, die höher wird als der Schirm. Nur ein `min()`:
     ein kleineres Fenster vergrößert jeden Überhang, kann die Bewegung also
     nie kleiner machen — der garantierte Weg bleibt garantiert. Ein px-BODEN
     hätte genau die umgekehrte Richtung und ist deshalb weg. */
  --px-vh-cap: 88vh;

  --px-pad: var(--px-pad-desktop);
  --px-gap: var(--px-gap-desktop);

  /* Das sichtbare Fenster; die Spuren überragen es, ihr Überhang IST der Weg.
     `--pxh-desktop` schreibt build_site.py inline an dieses Element, berechnet
     aus den echten Bildmaßen dieser einen Strecke. Der Rückfallwert greift nur,
     wenn das Markup ohne den Renderer entsteht — er ist bewusst in vw notiert,
     also in derselben Einheit wie die Spurhöhen. */
  --parallax-h: min(var(--pxh-desktop, 52vw), var(--px-vh-cap));

  /* Weiche Ober-/Unterkante, damit die Strecke in die Sektionsfläche läuft
     statt hart abzuschneiden. Alpha-only, deshalb in beiden Themes korrekt.
     Bewusst knapp: jeder Prozentpunkt Verlauf ist Bildfläche, die niemand mehr
     sieht. UNTEN kürzer als oben (2.5 % statt 5 %): oben laufen die Kacheln
     erst ins Bild, unten stehen die fertig sichtbaren. Abschalten = `none`. */
  --parallax-mask: linear-gradient(180deg,
                     rgb(var(--c-black) / 0) 0%,
                     rgb(var(--c-black)) 5%,
                     rgb(var(--c-black)) 97.5%,
                     rgb(var(--c-black) / 0) 100%);

  position: relative;
  box-sizing: border-box;
  height: var(--parallax-h);
  min-height: 0;
  overflow: hidden;
  background: var(--color-bg);
  padding: var(--px-pad);
  display: flex;
  gap: var(--px-gap);
  align-items: start;          /* keep columns at their natural (taller) height */
  -webkit-mask-image: var(--parallax-mask);
          mask-image: var(--parallax-mask);
}

/* Ein `.capability-parallax__columns`-Zwischenelement gab es hier einmal; das
   Markup (page_sections.html.jinja) hängt die Spuren seit Langem direkt in die
   Stage. Die Regeln dafür standen trotzdem noch — inklusive einer, die bei
   reduzierter Bewegung wie das dortige Layout aussah und keines war. Wer sie
   wieder braucht, braucht zuerst das Element. */
.capability-parallax__column {
  position: relative;
  flex: 1 1 0%;
  min-width: 0;
  height: auto;
  /* A floor, NOT a cap: 100% is the stage window (its content box). Richly
     stocked, a track stays taller than the window — the rule is inert, and the
     overflow (i.e. the motion) is untouched. Thinly stocked it can no longer be
     shorter than the window, and the last tile fills the rest (flex-grow below).

     This became load-bearing when the tiles started carrying their own aspect
     ratio: with a single 4/5 for everything, four equally stocked tracks were
     equally tall by construction. With real ratios one track can end well above
     the window's lower edge, and that is exactly where a void would open. */
  min-height: 100%;
  display: flex;
  flex-direction: column;
  /* Geerbt von der Stage — der Abstand zwischen den Kacheln IST der Abstand
     zwischen den Spuren, und die Spurhöhen-Rechnung setzt genau das voraus.
     Zwei getrennte Werte wären eine zweite Wahrheit über dieselbe Lücke. */
  gap: var(--px-gap);
  will-change: transform;
  transform: translate3d(0, 0, 0);
  backface-visibility: hidden;
}

/* Last resort against the gap under a thinly stocked track (see min-height
   above): if a track's content does not reach the window's lower edge, the LAST
   tile grows into the remainder — object-fit:cover keeps the motif intact. With
   sufficient stock the track is longer than the window, there is no free space,
   and the rule is inert. */
.capability-parallax__column > .capability-parallax__item:last-child {
  flex-grow: 1;
}

/* The tile carries the aspect ratio of ITS OWN image: `--parallax-item-ratio`
   is written inline by `image_ratio_style()` (build_site.py) from the real file
   dimensions in the V2 manifest, clamped to a band. Tile and cover are then
   congruent — object-fit:cover crops nothing.

   The 4/5 below is the FALLBACK, not the normal case any more. It applies when
   an image ships no dimensions. Until 2026-07-31 it applied to EVERY tile on
   EVERY breakpoint, and the stock spans roughly 0.6 … 1.6: a square motif lost
   about a fifth, a landscape one more. The sibling project (Bock Strength) had
   already fixed this; the fix now lives in both. */
.capability-parallax__item {
  position: relative;
  width: 100%;
  height: auto;
  aspect-ratio: var(--parallax-item-ratio, 4 / 5);
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--color-gray-100);
}

.capability-parallax__picture {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.capability-parallax__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

@media (min-width: 768px) {
  .capability-parallax__statement h2 {
    font-size: var(--text-7xl);
  }
}

@media (min-width: 1280px) {
  .capability-parallax__statement h2 {
    font-size: 5.75rem;
  }
}

@media (max-width: 720px) {
  .capability-parallax {
    --capability-gap: 3vw;
  }

  .capability-parallax__screen {
    min-height: 100svh;
    padding: var(--space-20) 0;
  }

  .capability-parallax__screen--outro {
    min-height: 100svh;
  }

  .capability-parallax__statement h2 {
    max-width: 11ch;
    font-size: var(--text-4xl);
    line-height: 1.02;
  }

  .capability-parallax__statement p,
  .capability-parallax__outro p {
    font-size: var(--text-base);
  }

  .capability-parallax__proofline {
    gap: var(--space-4) var(--space-6);
  }

  /* Mobile: TWO REAL TRACKS — the same physics as everywhere else, just half
     as many of them.
     -------------------------------------------------------------------------
     What stood here before (grid + `display: contents` + one aspect ratio for
     all) was the short circuit of a correct observation: FOUR tracks are too
     narrow at 390px. But measured, the jump from "four is too narrow" to "no
     motion at all" is not covered — there is exactly one state in between:

       390px viewport, gap/padding 3vw = 11.7px
         4 tracks -> (390 - 23.4 - 35.1) / 4 =  82.9px per tile   (a sliver)
         2 tracks -> (390 - 23.4 - 11.7) / 2 = 177.5px per tile   (= the old grid cell)

     At two tracks the tile is EXACTLY as wide as the former grid cell, so the
     reason for switching the effect off no longer holds. `display: contents`
     also made the columns box-less, which silently disabled every transform —
     the JS looked healthy while the effect was dead.

     Only a token swap plus one rule that drops tracks 3+4 remains here. Flex
     stage, per-image aspect ratio and the JS transforms are inherited
     unchanged. A separate mobile layout no longer exists — which is the point:
     what does not exist cannot drift. Same change in the sibling project
     (Bock Strength, `.bs-pxgal__*`). */
  .capability-parallax__stage {
    --px-pad: var(--px-pad-mobile);
    --px-gap: var(--px-gap-mobile);
    --parallax-h: min(var(--pxh-mobile, 88vw), var(--px-vh-cap));
  }

  /* Zwei Spuren statt vier. Diese Regel und `--px-cols-mobile: 2` oben sind
     dieselbe Aussage — der Guard prüft, dass sie es bleiben. */
  .capability-parallax__column:nth-child(n+3) {
    display: none;
  }
}

@media (min-width: 721px) and (max-width: 1180px) {
  /* Tablet: a real four-track parallax — images slide past each other at
     different speeds, like on desktop — and GAP-PROOF by construction. Columns
     stack into tracks that are naturally TALLER than the visible window; JS
     then slides each column only within the exact overflow it has (measured
     live), so a track can reveal its lower images at its own speed but can
     never expose a void.

     The tile ratio is NOT re-fixed here any more. It used to be forced back to
     4/5 in this block, which quietly undid the per-image ratio on exactly the
     breakpoint that shows four tracks side by side — the widest tiles, i.e.
     where a wrong ratio crops the most. The floor on the column plus the
     growing last tile (base rules) carry the gap-proofness instead.

     Was hier ausserdem stand und weg ist: eine Wiederholung der Stage-Regeln
     (height/overflow/align-items) und ein `top: 0` auf den ersten vier Spuren,
     das einen Startversatz zuruecksetzte, den es seit dem gemessenen Umbau
     nicht mehr gibt. Beides war wirkungsgleich mit der Grundregel — und genau
     solche Wiederholungen sind es, die spaeter EINSEITIG geaendert werden.
     Uebrig bleibt, was diesen Breakpoint wirklich ausmacht: andere Zahlen. */
  .capability-parallax__stage {
    --px-pad: var(--px-pad-tablet);
    --px-gap: var(--px-gap-tablet);
    --parallax-h: min(var(--pxh-tablet, 46vw), var(--px-vh-cap));
  }
}

@media (prefers-reduced-motion: reduce) {
  .capability-parallax__screen {
    min-height: auto;
  }

  .capability-parallax__hint {
    display: none;
  }

  /* Das Fenster wächst auf die volle Spurhöhe — es geht kein Motiv verloren.
     Die Maske geht mit aus: sie ist die weiche Kante EINER Bewegung; ohne
     Bewegung blendet sie nur noch Bildfläche weg. */
  .capability-parallax__stage {
    height: auto;
    min-height: 0;
    overflow: visible;
    -webkit-mask-image: none;
            mask-image: none;
  }

  .capability-parallax__column {
    min-width: 0;
    height: auto;
    transform: none !important;
  }

  /* No aspect-ratio override here: the tile keeps the ratio of its own image
     (`--parallax-item-ratio`). Reduced motion means no transforms and no fixed
     window — it does not mean cropping every motif back into 4/5. Forcing the
     ratio here used to be the third place that quietly undid the per-image
     ratio, and the one hardest to notice, because it only shows up for users
     who asked for less motion. */
  .capability-parallax__item {
    flex-basis: auto;
    height: auto;
  }
}

@media (min-width: 1024px) {
  .sticky-card-content {
    width: 500px;
    height: 300px;
    border-radius: 2.5rem;
  }

  .sticky-card-overlay {
    padding: var(--space-8);
  }
}

/* ── Accessibility: Reduced Motion ───────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .sticky-card-content {
    transition: none;
  }
  
  .portfolio-showcase {
    padding: var(--section-py) 0;
  }

  .sticky-card {
    position: relative;
    height: auto;
    padding: var(--space-4) 0;
  }

  .sticky-card-content {
    transform: none !important;
  }

  .showcase-scroll-hint {
    display: none;
  }
}

/* ════════════════════════════════════════════════════════════════
   LEISTUNGEN — Prozesskette / Value-Stream layout (/leistungen/)
   The narrative spine = the journey of ONE part through the works:
   01 Zuschnitt → 02 Umformen → 03 Schweißen → 04 Zerspanen → 05 Oberfläche.
   The number now MEANS the production step (replaces "Leistungsbereich 0X").
   All values reference variables.css tokens (styling SSOT).
   ════════════════════════════════════════════════════════════════ */

/* ── Einstieg: one statement + proof bar + clickable process index ── */
.proc-intro { max-width: 760px; margin: 0 auto; text-align: center; }
.proc-intro__lead {
  margin: var(--space-5) auto 0;
  max-width: 660px;
  color: var(--color-text-muted);
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
}

/* Proof bar — the hard numbers, immediately visible (no longer buried) */
.proc-proofbar {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-5) var(--space-12);
  max-width: 820px;
  margin: var(--space-10) auto 0;
  padding: var(--space-8) 0 0;
  border-top: 1px solid var(--color-border);
  list-style: none;
}
.proc-proofbar li { display: grid; justify-items: center; gap: var(--space-1); }
.proc-proofbar strong {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  line-height: 1;
  color: var(--color-accent-text);
}
.proc-proofbar span {
  font-size: var(--text-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* Process index — the clickable value stream; doubles as the page's nav map */
.proc-index {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin: var(--space-12) auto 0;
}
.proc-index__step {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  text-decoration: none;
  transition: var(--transition-fast);
}
.proc-index__step:hover,
.proc-index__step:focus-visible {
  border-color: var(--color-accent);
  box-shadow: var(--card-shadow-hover-soft);
  transform: var(--card-hover-transform);
}
.proc-index__num {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--font-weight-bold);
  color: var(--color-accent-text);
}
.proc-index__name {
  font-size: var(--text-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.proc-index__arrow {
  display: inline-flex;
  align-items: center;
  color: var(--color-text-light);
  font-size: var(--text-xs);
}
.proc-index__step--special {
  border-style: dashed;
  background: transparent;
}
/* Akzent-Icon der Sonder-Schritte (Rohr/Sondermaschinenbau) — zentral statt
   inline style="color:…;font-size:…" auf dem <i> in den Templates. */
.proc-index__step--special i {
  color: var(--color-accent-text);
  font-size: var(--text-xs);
}
@media (max-width: 720px) {
  .proc-index__arrow { display: none; }
}

/* ── Prozess-Schiene — die Sprung-Navi als sticky Fortschrittsleiste ──────
   Variante von .proc-index für den Fall „Navi steht ALLEIN in ihrer eigenen
   Sektion" (Karriere-Hub). Der Basisfall bleibt unverändert: dort folgt die
   Navi auf Inhalt (Envelope-Kacheln, Proofbar), weshalb dessen
   `margin-top: var(--space-12)` dort RICHTIG ist — hier trennt es nichts und
   war reine Luft.

   Drei Dinge, die die Variante besser macht als das Pillen-Band:
   1. STICKY: sie klebt unter dem Header, statt nach 600 px Scrollen nutzlos
      oben liegenzubleiben. Der Container ist <main>, nicht die kurze Sektion
      — `position: sticky` auf der Sektion selbst hält damit über die ganze
      Seite (auf der Navi selbst würde es nach 56 px enden).
   2. SCHIENE statt Chevrons: EINE durchgehende Linie hinter den Schritten;
      die Schritte tragen die Seitenhintergrundfarbe und stanzen sich frei,
      sodass die Linie nur in den Lücken sichtbar wird. Eine zweite Linie
      (--rail-progress, aus main.js) füllt sie bis zum aktiven Schritt.
   3. SCROLL-SPY: der aktive Abschnitt leuchtet + trägt aria-current
      (main.js, nav[data-rail-spy]) — die Navi sagt nicht nur wohin, sondern
      auch wo man IST. Ohne JS bleibt es eine korrekte, statische Sprungnavi.  */
.section--rail {
  position: sticky;
  top: var(--header-offset);
  z-index: var(--rail-z);
  padding: var(--rail-py) 0;
  border-bottom: 1px solid var(--color-border);
  transition: box-shadow var(--transition-base);
}
/* Erst wenn die Leiste wirklich klebt, hebt sie sich vom Inhalt ab (main.js). */
.section--rail.is-stuck { box-shadow: var(--shadow-md); }

/* Ankersprünge müssen unter Header UND Schiene landen, sonst verdeckt die
   Leiste genau die Überschrift, zu der man gesprungen ist.
   Diese Deklaration ist der Landepunkt der Seite — Klick-Handler und Scroll-Spy
   in main.js LESEN sie (bmAnchorOffset), statt sie nachzurechnen. Beide Summanden
   sind zur Laufzeit gepflegt: --header-offset über --ls-banner-h (lang-suggest.js),
   --rail-h über die gemessene Leistenhöhe (Scroll-Spy). */
html:has(.section--rail) {
  scroll-padding-top: calc(var(--header-offset) + var(--rail-h));
}

.proc-index--rail {
  --rail-progress: 0;
  position: relative;
  /* `0 auto`, nicht `0`: der Basisfall zentriert über `margin: … auto 0`, und
     ein blankes `margin: 0` nimmt der Schiene genau diese Zentrierung — sie
     klebte dann an der linken Containerkante (gemessen 2026-08-04). */
  margin: 0 auto;
  gap: 0;
  flex-wrap: nowrap;
  justify-content: space-between;
  /* So breit wie NÖTIG, mit Untergrenze und Obergrenze — statt fixem Deckel:
       min  --rail-max  damit wenige Schritte nicht gedrängt beieinanderstehen
       max  100 %       der Container ist die harte Grenze
     Der feste Deckel liess Sprachen, die nur knapp darüber lagen, unnötig
     scrollen UND dabei die Linie verlieren, obwohl im Container noch Platz war
     (gemessen: ro 1004 px, el 1011 px bei 940 px Deckel und 1184 px Container).
     Jetzt wächst die Schiene in den vorhandenen Platz und scrollt erst, wenn
     auch der Container nicht mehr reicht. */
  width: max-content;
  min-width: min(var(--rail-max), 100%);
  max-width: 100%;
  /* Die Schiene scrollt, SOBALD sie nicht mehr passt — nicht erst ab einer
     Viewportbreite. Das faengt beides mit derselben Regel ab: das Handy UND
     eine lange Prozesskette (8 Schritte auf /leistungen/) in einer Sprache mit
     langen Woertern. Ohne das verbreitert sie die Seite, und `body{overflow-x:
     hidden}` schnitte die letzten Schritte lautlos ab — ein Fehler, den man
     nicht sieht, sondern nur vermisst. */
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.proc-index--rail::-webkit-scrollbar { display: none; }

/* Ab 7 Schritten wird es auf --rail-max zu eng. Die Komponente entscheidet das
   SELBST per Mengen-Query (sie zaehlt ihre eigenen Schritte) statt ueber eine
   gepflegte Ausnahme pro Seite: eine neue Prozesskette bekommt die dichtere
   Variante automatisch, egal wer sie anlegt. */
/* Ab 5 Schritten ist --rail-max als UNTERgrenze zu klein: die Schritte fuellen
   sie fast aus, und die Verbindungsstuecke schrumpfen zu Strichen (gemessen an
   der 8-gliedrigen Prozesskette). Dann ist der volle Container die richtige
   Grundbreite — die Schiene atmet wieder. Bis 4 Schritte bleibt es bei
   --rail-max, sonst stuenden sie unnoetig weit auseinander. */
.proc-index--rail:has(.proc-index__step:nth-child(5)) {
  --rail-max: 100%;
}
.proc-index--rail:has(.proc-index__step:nth-child(7)) {
  --rail-node: 26px;
}
.proc-index--rail:has(.proc-index__step:nth-child(7)) .proc-index__step {
  gap: var(--space-2);
  padding: 0 var(--space-2);
}
.proc-index--rail:has(.proc-index__step:nth-child(7)) .proc-index__name {
  font-size: var(--text-xs);
}
/* Die Schiene + ihre Füllung. Beide liegen HINTER den Schritten (z-index 0
   gegen 1), laufen von Kante zu Kante und werden von den Schritten selbst
   überdeckt — dadurch braucht keine Lücke eine eigene Breite. */
.proc-index--rail::before,
.proc-index--rail::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  height: var(--rail-line);
  margin-top: calc(var(--rail-line) / -2);
  border-radius: var(--radius-full);
  z-index: 0;
}
.proc-index--rail::before {
  right: 0;
  background: var(--color-border);
}
/* Die Fuellung. BEWUSST ohne width-Transition: sie haengt jetzt direkt am
   Scrollweg (main.js aktualisiert sie pro Frame), und eine Transition liefe
   dem Finger sichtbar hinterher, statt ihm zu folgen. Beim Klick-Sprung
   animiert sie trotzdem weich — weil der Scroll selbst weich ist. */
.proc-index--rail::after {
  width: calc(var(--rail-progress) * 100%);
  background: var(--color-accent);
}
.proc-index--rail .proc-index__step {
  position: relative;
  z-index: 1;
  /* Nicht schrumpfen: sonst quetschen sich 8 Schritte in die Breite, statt zu
     scrollen — die Schiene sieht dann "passend" aus, waehrend die Labels
     abgeschnitten sind. */
  flex: 0 0 auto;
  scroll-snap-align: center;
  gap: var(--space-3);
  padding: 0 var(--space-4);
  border: 0;
  border-radius: 0;
  /* Die Hintergrundfarbe der Sektion — sie stanzt den Schritt aus der Linie. */
  background: var(--color-bg);
}
.proc-index--rail .proc-index__step:hover,
.proc-index--rail .proc-index__step:focus-visible {
  box-shadow: none;
  transform: none;
}
/* Der nummerierte Knoten sitzt AUF der Schiene. */
.proc-index--rail .proc-index__num {
  display: inline-grid;
  place-items: center;
  width: var(--rail-node);
  height: var(--rail-node);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  background: var(--color-bg);
  color: var(--color-text-muted);
  transition: var(--transition-fast);
}
.proc-index--rail .proc-index__name {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  transition: var(--transition-fast);
  white-space: nowrap;
}

/* Aktiver Abschnitt (Scroll-Spy). */
.proc-index--rail .proc-index__step[aria-current] .proc-index__num {
  border-color: var(--color-accent);
  background: var(--color-accent);
  color: var(--color-text-inverse);
}
.proc-index--rail .proc-index__step[aria-current] .proc-index__name {
  color: var(--color-text);
  font-weight: var(--font-weight-bold);
}

/* Hover NUR auf echten Zeigergeräten — auf Touch ist der Hover eine
   Nebenwirkung des Scrollens und würde einen falschen Abschnitt markieren
   (siehe die Hover-Gating-Ratsche weiter oben in dieser Datei). */
@media (hover: hover) and (pointer: fine) {
  .proc-index--rail .proc-index__step:hover .proc-index__num {
    border-color: var(--color-accent);
    color: var(--color-accent-text);
  }
  .proc-index--rail .proc-index__step:hover .proc-index__name {
    color: var(--color-text);
  }
  .proc-index--rail .proc-index__step[aria-current]:hover .proc-index__num {
    color: var(--color-text-inverse);
  }
}
/* Tastatur-Fokus muss auch ohne Hover sichtbar sein. */
.proc-index--rail .proc-index__step:focus-visible .proc-index__num {
  border-color: var(--color-accent);
}

/* Wird WIRKLICH gescrollt, verschwindet die durchgehende Linie: sie spannt die
   sichtbare Breite, nicht die Scrollbreite, und endete sonst mitten im Nichts.
   `is-scrollable` setzt die generische assets/js/scroll-affordance.js über
   [data-scroll-x] — inklusive Font-Load und Resize, also auch dann, wenn ein
   spät geladener Webfont die Labels erst breiter macht. Bewusst KEINE
   Breakpoint-Abfrage: ob gescrollt wird, hängt an Schrittzahl UND Sprache,
   nicht an der Viewportbreite. */
.proc-index--rail.is-scrollable {
  justify-content: flex-start;
  gap: var(--space-2);
}
.proc-index--rail.is-scrollable::before,
.proc-index--rail.is-scrollable::after { display: none; }

@media (max-width: 767px) {
  .proc-index--rail .proc-index__step {
    padding: 0 var(--space-2);
  }
}

@media (prefers-reduced-motion: reduce) {
  .section--rail { transition: none; }
}

/* Small kicker line above the cinematic parallax band */
.proc-band-kicker {
  margin: 0;
  padding: var(--space-12) var(--space-4) var(--space-6);
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}

/* ── Process-stage header — big step number + identity ──────────────
   Replaces the generic "Leistungsbereich 0X" label with the part's
   position in the production flow. */
.proc-stage--muted { background: var(--color-bg-alt); }
.proc-stage__head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--space-5) var(--space-8);
  width: 100%;
  max-width: 900px;
}
.proc-stage__head > :not(.proc-stage__num) {
  min-width: 0;
}
.proc-stage__num {
  font-family: var(--font-display);
  font-size: clamp(3rem, 7vw, 5.5rem);
  font-weight: var(--font-weight-extrabold);
  line-height: 0.85;
  letter-spacing: var(--tracking-tight);
  color: var(--color-accent-text);
}
/* Plus-Icon in der großen Schrittnummer — zentral statt inline
   style="font-size:0.6em" auf dem <i> in den Templates. */
.proc-stage__num i { font-size: 0.6em; } /* ssot-ok: relatives Icon-Verhältnis (em), kein Typ-Skala-Wert */
.proc-stage__text {
  margin-top: var(--space-3);
  max-width: 660px;
  color: var(--color-text-muted);
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
}
.proc-stage__spec {
  margin-top: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text);
}
.dark-section .proc-stage__text { color: var(--color-text-muted); }
.dark-section .proc-stage__spec { color: var(--color-accent-text); }
@media (max-width: 600px) {
  .proc-stage__head { grid-template-columns: 1fr; gap: var(--space-2); }
  .proc-stage__num { font-size: clamp(2.5rem, 16vw, 3.75rem); }
}
/* Abschluss-Stufe (Komplettbau): Icon statt Nummer → signalisiert „kein Schritt N,
   sondern die Zusammenführung". Etwas kleiner als die Zahlen, gleiche Akzentfarbe. */
.proc-stage__num--icon { font-size: clamp(2.25rem, 5vw, 3.25rem); }

/* Sub-Bereich innerhalb eines Prozess-Schritts (z. B. Rohr-Zuschnitt in Schritt 01):
   klar als eigener, gleichrangiger Unterbereich abgesetzt — kein Extra-„Highlight". */
.proc-substage {
  padding-top: var(--space-8);
  border-top: 1px dashed var(--color-border);
}
.proc-substage__head { max-width: 720px; }
.proc-substage__title {
  margin: var(--space-2) 0 0;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--font-weight-bold);
  line-height: var(--leading-tight);
  color: var(--color-text);
}
.proc-substage__text {
  margin-top: var(--space-3);
  color: var(--color-text-muted);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
}


/* ── Numbered image-process chain (3 steps with photo + badge) ──────
   Reuses proc-stage__num font tokens so a single token change
   updates all numbered components sitewide. Dark-section native.     */
.proc-chain {
  display: grid;
  grid-template-columns: repeat(3, var(--grid-track-fluid));
  gap: var(--space-4);
  margin-top: var(--space-8);
}
.proc-chain__item {
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--on-dark-surface-soft);
  border: 1px solid rgba(148, 163, 184, 0.36); /* ssot-ok: Steel-Hairline (Frosted-Card auf Dunkel) — off-scale, Token-Kandidat */
  box-shadow: 0 10px 24px rgb(var(--c-black) / 0.18);
  display: flex;
  flex-direction: column;
}
.proc-chain__media { position: relative; flex-shrink: 0; }
.proc-chain__img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
}
.proc-chain__num {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 3.5vw, 2.25rem);
  font-weight: var(--font-weight-extrabold);
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--color-accent-text);
  background: rgba(10, 14, 23, 0.74); /* ssot-ok: dunkler Badge-Scrim über Foto — off-scale, Token-Kandidat */
  backdrop-filter: blur(6px); /* ssot-ok: Frosted-Badge, 6px liegt zwischen --blur-sm/-md */
  -webkit-backdrop-filter: blur(6px); /* ssot-ok: s. o. */
  padding: 0.18em 0.42em;
  border-radius: var(--radius-md);
}
.proc-chain__body { padding: var(--space-4); flex: 1; }
.proc-chain__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-inverse);
}
.proc-chain__text {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--on-dark-text-soft);
  line-height: var(--leading-relaxed);
}
.proc-chain__text a { color: var(--color-accent-text); }
.proc-chain__text a:hover { text-decoration: underline; }
@media (max-width: 860px) {
  .proc-chain { grid-template-columns: 1fr; }
}
