/* Generated by build/generate-cta.js — do not edit by hand. */

/*
 * A full-bleed photo band. The photograph is an <img> rather than a container
 * background so it keeps its srcset; that means it has to be laid under the
 * content, which is the one thing Elementor's generated settings do not do
 * reliably. Shared by every photo band on the site.
 */
.v-band {
  position: relative;
  isolation: isolate;
}

.v-band > .v-band__img {
  position: absolute;
  inset: 0;
  z-index: 0;
  margin: 0;
}

.v-band > .v-band__img,
.v-band > .v-band__img .elementor-widget-container,
.v-band > .v-band__img figure,
.v-band > .v-band__img img {
  width: 100%;
  height: 100%;
}

.v-band > .v-band__img img {
  display: block;
  object-fit: cover;
}

/* The scrim: #000000 at 0.2, measured on every desktop copy that has one. */
.v-band::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: #000000;
  opacity: 0.2;
  pointer-events: none;
}

.v-band--voll::after {
  content: none;
}

.v-band > :not(.v-band__img) {
  position: relative;
  z-index: 2;
}

/*
 * ---------------------------------------------------------------------------
 * Everything below is a function of viewport width, interpolated between the
 * two widths the design draws — 360 and 1440 — and clamped outside them. Both
 * ends reproduce the file exactly; the middle no longer falls off a cliff.
 *
 * These are Elementor's own container custom properties, so the values travel
 * through its own `padding-block-start: var(--padding-top)` machinery. The
 * generated JSON deliberately sets no padding, gap, radius, background or
 * min-height on these two elements: with no element rule to fight,
 * .v-band.e-con (0,2,0) beats .e-con (0,1,0) and there is exactly one
 * declaration in play.
 *
 * No min-height either — the band's height IS the card plus its padding, and
 * that closure is gated on every variant and both breakpoints.
 *
 * The closing placement is the base; the others are deltas from it, generated
 * by comparing measurements rather than written out a second time.
 * ---------------------------------------------------------------------------
 */

/* The band's horizontal padding IS the card's inset: the card is full width
   inside it, so no max-width control is needed and none is fought over.
   1440 - 2x175 = 1090 and 360 - 2x20 = 320. */
.v-band.e-con {
  --padding-top: clamp(27px, calc(27px + 67 * (100vw - 360px) / 1080), 94px);
  --padding-bottom: clamp(27px, calc(27px + 67 * (100vw - 360px) / 1080), 94px);
  --padding-left: max(clamp(20px, calc(20px + 155 * (100vw - 360px) / 1080), 175px), calc((100% - 1090px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 155 * (100vw - 360px) / 1080), 175px), calc((100% - 1090px) / 2));
}

/* breit (Seitenmitte) */
.v-band.v-band--breit.e-con {
  --padding-top: clamp(59px, calc(59px + 65 * (100vw - 360px) / 1080), 124px);
  --padding-bottom: clamp(59px, calc(59px + 65 * (100vw - 360px) / 1080), 124px);
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/* voll (Startseite) */
.v-band.v-band--voll.e-con {
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
}

/*
 * The card. `--column-gap` is what Elementor's `gap: var(--row-gap)
 * var(--column-gap)` reads; the row gap only ever applies once the columns have
 * wrapped, and the design's stacked arrangement uses 24.
 */
.v-cta.e-con {
  --padding-top: clamp(16px, calc(16px + 64 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(16px, calc(16px + 64 * (100vw - 360px) / 1080), 80px);
  --padding-left: clamp(16px, calc(16px + 16 * (100vw - 360px) / 1080), 32px);
  --padding-right: clamp(16px, calc(16px + 16 * (100vw - 360px) / 1080), 32px);
  --row-gap: 24px;
  --column-gap: clamp(24px, calc(24px + 56 * (100vw - 360px) / 1080), 80px);
  --gap: var(--row-gap) var(--column-gap);
  --border-radius: 6px 6px 6px 6px;
  background-color: rgba(51, 51, 51, 0.06);
}

/* breit (Seitenmitte) */
.v-band--breit .v-cta.e-con {
  --padding-top: clamp(16px, calc(16px + 48 * (100vw - 360px) / 1080), 64px);
  --padding-bottom: clamp(16px, calc(16px + 48 * (100vw - 360px) / 1080), 64px);
  --padding-left: clamp(16px, calc(16px + 16 * (100vw - 360px) / 1080), 32px);
  --padding-right: clamp(16px, calc(16px + 16 * (100vw - 360px) / 1080), 32px);
  --column-gap: 24px;
}

/* voll (Startseite) */
.v-band--voll .v-cta.e-con {
  --padding-top: clamp(40px, calc(40px + 111 * (100vw - 360px) / 1080), 151px);
  --padding-bottom: clamp(40px, calc(40px + 111 * (100vw - 360px) / 1080), 151px);
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --border-radius: 0px 0px 0px 0px;
  background-color: rgba(51, 51, 51, 0.3);
}

/*
 * The two columns carry the design's ratio as grow FACTORS rather than as two
 * pixel widths, so it holds at every width.
 *
 * Grow factors only mean anything in the GROW regime. An Elementor container is
 * `width: 100%`, so two side by side start from a basis larger than the row and
 * are distributed by SHRINK instead, which is weighted by basis and therefore
 * always lands on equal halves. Measured: 473 / 473 where the design says
 * 566 / 377. Zeroing the basis puts them back where the factors apply.
 *
 * `min-width` then does two jobs at once: it decides where the row wraps — the
 * flex algorithm breaks lines on the hypothetical size, which is the basis
 * clamped by min-width — and it stops a column being squeezed past the point
 * where the heading or the button still fit. `min()` caps it at the container's
 * own width so that once wrapped, a 400px minimum cannot overflow a 288px card.
 */
.v-cta > .v-cta__col {
  flex-basis: 0;
}

.v-cta > .v-cta__col--a {
  flex-grow: 566;
  min-width: min(400px, 100%);
}

.v-cta > .v-cta__col--b {
  flex-grow: 377;
  min-width: min(320px, 100%);
}

.v-band--breit .v-cta > .v-cta__col--a { flex-grow: 1; }
.v-band--breit .v-cta > .v-cta__col--b { flex-grow: 1; }
.v-band--voll .v-cta > .v-cta__col--a { flex-grow: 566; }
.v-band--voll .v-cta > .v-cta__col--b { flex-grow: 520; }

/* The columns space their children with flex `gap` (32 on the wide one);
   a trailing paragraph margin would be added on top of it. */
.v-cta .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/*
 * ---------------------------------------------------------------------------
 * The hero (generated by build/generate-hero.js via generate-pages.js).
 * Loaded AFTER the CTA base above: the per-page scrims override .v-band::after
 * at equal specificity, so this order is load-bearing.
 * ---------------------------------------------------------------------------
 */

/* Every hero: content on the site's gutter ramp (D26/D60), photo heroes
   bottom-anchored (the drawn offsets close as bottom paddings, D57). The
   padding-top floor keeps content from sliding under the overlaying header at
   widths where the fluid min-height runs out before the copy does. */
.v-hero.e-con {
  /* The bar is position:fixed from the first paint (Pro sticky), but its
     spacer keeps the header's height in the flow — the drawing puts the
     photograph UNDER the glass. The negative margin pulls the hero up by the
     same variable the padding-top floor restores, so the copy never moves
     and the bar frosts the photo instead of the page ground. */
  margin-block-start: calc(-1 * var(--valera-header-h, 72px));
  --padding-top: var(--valera-header-h, 72px);
  --padding-bottom: 0px;
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --row-gap: 0px;
  --column-gap: 0px;
  --gap: var(--row-gap) var(--column-gap);
}

.v-hero--foto.e-con {
  /* The screen, not the artboard. The drawn 960/700 are the artboards'
     stand-ins for a viewport; shipped as fixed pixels they cut the slider strip
     off on any laptop shorter than 960 (found on review).

     There was a min(…, 1080px) cap here between 2026-08-18 and 2026-08-20, on
     the reading that past full-HD height a full-bleed hero stops being a frame
     and becomes a wall. It was never written into a decision, and the recorded
     one says plainly min-height: 100svh; on real hardware it is simply a white
     strip of the next section under a hero that is meant to be one screen —
     reported on a 24" iMac in both browsers, 2026-08-20. The cap is gone.

     min-height, not height, so content can still push past a short screen; svh
     so mobile URL bars do not pump the layout. */
  min-height: 100vh;
  min-height: 100svh;
}

/* The gap chain, one declaration per joint — measured 8/24/32 desktop and
   4/16/24 mobile on every page that draws the joint (D57). */
.v-hero__cols.e-con {
  --row-gap: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px);
  --column-gap: var(--row-gap);
  --gap: var(--row-gap) var(--column-gap);
}

.v-hero__col.e-con {
  --column-gap: var(--row-gap);
  --gap: var(--row-gap) var(--column-gap);
}

.v-hero__col--a.e-con { --row-gap: clamp(4px, calc(4px + 4 * (100vw - 360px) / 1080), 8px); }
.v-hero__col--b.e-con { --row-gap: clamp(24px, calc(24px + 8 * (100vw - 360px) / 1080), 32px); }

/* The anchor grid's flow children (the photographs are absolute and take no
   track): copy on row 2, the strip on row 4 behind its spacer track. */
.v-hero--foto .v-hero__cols {
  grid-row: 2;
}

.v-hero--foto .v-slider {
  grid-row: 4;
  justify-self: end;
}

/* Hero – Startseite */

.v-hero--startseite.e-con {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 140fr) auto minmax(0, 52fr);
}

@media (min-width: 768px) {
  .v-hero--startseite.e-con {
    grid-template-rows: minmax(0, 226fr) auto minmax(0, 82fr) auto;
    --padding-bottom: 32px;
  }
}

.v-hero--startseite .v-hero__cols .elementor-widget.v-hero__heading { max-width: 668px; }

.v-hero--startseite .v-hero__cols .elementor-widget.v-hero__text { max-width: 434px; }

.v-hero--startseite > .v-band__img img { object-position: 66.5% 50%; }

.v-hero--startseite::after {
  background: #000000;
  opacity: 0.4;
}

@media (min-width: 768px) {
  .v-hero--startseite::after {
    background: linear-gradient(90deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.2) 100%);
    opacity: 0.73;
  }
}


@media (min-width: 768px) {
  /*
   * THE FRAME CLIPS ITS GLOW, and the design says so: every photo-hero frame
   * carries clipsContent = true, measured as d.clips and gated.
   *
   * It has to. The gradient below stands in for a blur, so it carries
   * 2 x GLOW_SIGMA = 420 of falloff on EVERY side - which put its lower edge
   * 194px past the bottom of the hero, in the design's own units - and more
   * on screen, because the box is a PERCENTAGE of a frame the page sizes to
   * the viewport: measured at 385 on a 950-tall window. It washed the top of
   * the next section grey. Serhii reported it twice over: the section under the hero
   * looked semi-transparent, and it took a long scroll before it cleared.
   * Both are that overhang; the pinned section below (D213) simply held still
   * long enough to make it obvious. It predates the pin.
   *
   * Scoped to this media query rather than set on the hero at every width,
   * because the glow is: the homepage's MOBILE frame is the one root that
   * does NOT clip, and there is nothing there to clip.
   *
   * clip, not hidden: hidden makes a scroll container, which this project has
   * already paid for once - it silently disables sticky for every descendant.
   */
  .v-hero--startseite.e-con { overflow: clip; }

  .v-hero--startseite::before {
    content: "";
    position: absolute;
    z-index: 1;
    left: -32.361%;
    top: -15.208%;
    width: 118.403%;
    height: 135.417%;
    background: radial-gradient(50% 50% at 50% 50%, rgba(20, 18, 2, 0.451) 0%, rgba(20, 18, 2, 0.375) 30%, rgba(20, 18, 2, 0.24) 50%, rgba(20, 18, 2, 0.152) 62%, rgba(20, 18, 2, 0.076) 75%, rgba(20, 18, 2, 0.031) 88%, rgba(20, 18, 2, 0.011) 100%);
    pointer-events: none;
  }
}


/* Hero – Über uns */

.v-hero--ueber-uns.e-con {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 150fr) auto minmax(0, 54fr);
}

@media (min-width: 768px) {
  .v-hero--ueber-uns.e-con {
    grid-template-rows: minmax(0, 497fr) auto minmax(0, 63fr);
  }
}

.v-hero--ueber-uns .v-hero__cols .elementor-widget.v-hero__heading { max-width: 743px; }

.v-hero--ueber-uns .v-hero__cols .elementor-widget.v-hero__text { max-width: 551px; }

.v-hero--ueber-uns > .v-band__img img { object-position: 36.1% 50%; }

.v-hero--ueber-uns::after {
  background: #000000;
  opacity: 0.3;
}

@media (min-width: 768px) {
  .v-hero--ueber-uns::after {
    background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%);
    opacity: 0.65;
  }
}


/* Hero – Stützmauer & Hangsicherung */

.v-hero--stuetzmauer.e-con {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 147fr) auto minmax(0, 56fr);
}

@media (min-width: 768px) {
  .v-hero--stuetzmauer.e-con {
    grid-template-rows: minmax(0, 157.5fr) auto minmax(0, 157.5fr);
  }
}

.v-hero--stuetzmauer .v-hero__cols .elementor-widget.v-hero__heading { max-width: 756px; }

.v-hero--stuetzmauer .v-hero__cols .elementor-widget.v-hero__text { max-width: 646px; }

.v-hero--stuetzmauer::after {
  background: #000000;
  opacity: 0.5;
}

@media (min-width: 768px) {
  .v-hero--stuetzmauer::after {
    background: #000000;
    opacity: 0.32;
  }
}


@media (min-width: 768px) {
  /*
   * THE FRAME CLIPS ITS GLOW, and the design says so: every photo-hero frame
   * carries clipsContent = true, measured as d.clips and gated.
   *
   * It has to. The gradient below stands in for a blur, so it carries
   * 2 x GLOW_SIGMA = 420 of falloff on EVERY side - which put its lower edge
   * 324px past the bottom of the hero, in the design's own units - and more
   * on screen, because the box is a PERCENTAGE of a frame the page sizes to
   * the viewport: measured at 385 on a 950-tall window. It washed the top of
   * the next section grey. Serhii reported it twice over: the section under the hero
   * looked semi-transparent, and it took a long scroll before it cleared.
   * Both are that overhang; the pinned section below (D213) simply held still
   * long enough to make it obvious. It predates the pin.
   *
   * Scoped to this media query rather than set on the hero at every width,
   * because the glow is: the homepage's MOBILE frame is the one root that
   * does NOT clip, and there is nothing there to clip.
   *
   * clip, not hidden: hidden makes a scroll container, which this project has
   * already paid for once - it silently disables sticky for every descendant.
   */
  .v-hero--stuetzmauer.e-con { overflow: clip; }

  .v-hero--stuetzmauer::before {
    content: "";
    position: absolute;
    z-index: 1;
    left: -38.75%;
    top: -27.875%;
    width: 124.792%;
    height: 168.375%;
    background: radial-gradient(50% 50% at 50% 50%, rgba(17, 21, 2, 0.52) 0%, rgba(17, 21, 2, 0.447) 30%, rgba(17, 21, 2, 0.295) 50%, rgba(17, 21, 2, 0.188) 62%, rgba(17, 21, 2, 0.094) 75%, rgba(17, 21, 2, 0.036) 88%, rgba(17, 21, 2, 0.012) 100%);
    pointer-events: none;
  }
}


/* Hero – Kanalbau */

.v-hero--kanalbau.e-con {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 163fr) auto minmax(0, 58fr);
}

@media (min-width: 768px) {
  .v-hero--kanalbau.e-con {
    grid-template-rows: minmax(0, 177.5fr) auto minmax(0, 177.5fr);
  }
}

.v-hero--kanalbau .v-hero__cols .elementor-widget.v-hero__heading { max-width: 756px; }

.v-hero--kanalbau .v-hero__cols .elementor-widget.v-hero__text { max-width: 646px; }

.v-hero--kanalbau > .v-band__img img { object-position: 60.5% 50%; }

.v-hero--kanalbau::after {
  background: #000000;
  opacity: 0.5;
}

@media (min-width: 768px) {
  .v-hero--kanalbau::after {
    background: #000000;
    opacity: 0.2;
  }
}


@media (min-width: 768px) {
  /*
   * THE FRAME CLIPS ITS GLOW, and the design says so: every photo-hero frame
   * carries clipsContent = true, measured as d.clips and gated.
   *
   * It has to. The gradient below stands in for a blur, so it carries
   * 2 x GLOW_SIGMA = 420 of falloff on EVERY side - which put its lower edge
   * 344px past the bottom of the hero, in the design's own units - and more
   * on screen, because the box is a PERCENTAGE of a frame the page sizes to
   * the viewport: measured at 385 on a 950-tall window. It washed the top of
   * the next section grey. Serhii reported it twice over: the section under the hero
   * looked semi-transparent, and it took a long scroll before it cleared.
   * Both are that overhang; the pinned section below (D213) simply held still
   * long enough to make it obvious. It predates the pin.
   *
   * Scoped to this media query rather than set on the hero at every width,
   * because the glow is: the homepage's MOBILE frame is the one root that
   * does NOT clip, and there is nothing there to clip.
   *
   * clip, not hidden: hidden makes a scroll container, which this project has
   * already paid for once - it silently disables sticky for every descendant.
   */
  .v-hero--kanalbau.e-con { overflow: clip; }

  .v-hero--kanalbau::before {
    content: "";
    position: absolute;
    z-index: 1;
    left: -40.833%;
    top: -37.375%;
    width: 124.792%;
    height: 180.375%;
    background: radial-gradient(50% 50% at 50% 50%, rgba(21, 19, 2, 0.48) 0%, rgba(21, 19, 2, 0.412) 30%, rgba(21, 19, 2, 0.273) 50%, rgba(21, 19, 2, 0.174) 62%, rgba(21, 19, 2, 0.086) 75%, rgba(21, 19, 2, 0.034) 88%, rgba(21, 19, 2, 0.011) 100%);
    pointer-events: none;
  }
}


/* Über uns: the one split hero — heading left, text right, drawn 779/551 with
   a 32px gutter. Grow factors carry the ratio; the min-widths decide where the
   row wraps back to the stacked (mobile) arrangement — ours, D60. */
.v-hero--ueber-uns .v-hero__cols.e-con {
  flex-direction: row;
  flex-wrap: wrap;
  --column-gap: 32px;
}

.v-hero--ueber-uns .v-hero__cols > .v-hero__col {
  flex-basis: 0;
}

.v-hero--ueber-uns .v-hero__cols > .v-hero__col--a {
  flex-grow: 779;
  min-width: min(460px, 100%);
}

.v-hero--ueber-uns .v-hero__cols > .v-hero__col--b {
  flex-grow: 551;
  min-width: min(320px, 100%);
}

/* The text hero — Leistungsübersicht is the model (white ground, centred,
   dark text); the five undrawn pages reuse it minus the button (D59). The
   drawn 533 is content + 2x80; mobile breathes 40 above and 60 below the
   copy against the header's own 48 (both measured, D57). */
.v-hero--text.e-con {
  --padding-top: calc(var(--valera-header-h, 72px) + clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px));
  --padding-bottom: clamp(60px, calc(60px + 20 * (100vw - 360px) / 1080), 80px);
  text-align: center;
}

.v-hero--text .v-hero__cols.e-con,
.v-hero--text .v-hero__col.e-con {
  align-items: center;
}

.v-hero--text .v-hero__cols .elementor-widget.v-hero__heading { max-width: 974px; }
.v-hero--text .v-hero__cols .elementor-widget.v-hero__text { max-width: 650px; }

/* Its image band: no scrim (the drawn overlay is #000000 at 0), the height a
   function of width between the two drawn boxes. */
.v-band--still::after {
  content: none;
}

.v-hero-band.e-con {
  min-height: clamp(162px, calc(162px + 520 * (100vw - 360px) / 1080), 682px);
}

/*
 * ---------------------------------------------------------------------------
 * The slider (Startseite, D58). The strip's constants live in Elementor; here
 * live the state machine's clothes: the crossfade, the progress bar, pause.
 * One clock: the fill animation's end advances the slide (hero.js), so
 * :hover/:focus-within pausing the animation pauses the rotation, and
 * prefers-reduced-motion disabling it disables auto-advance — the bar rests
 * filled, which is the drawn state.
 * ---------------------------------------------------------------------------
 */
/*
 * The crossfade, and the two things that were wrong with it until 2026-08-19.
 *
 * FIRST, it never ran. .v-hero__bild is (0,1,0) and Elementor's own
 *   .elementor-element:where(:not(.e-con)):where(:not(.e-div-block-base)):not(:has(.elementor-widget-container))
 * is (0,2,0) — :where() contributes nothing, :not(:has(.class)) contributes
 * a class — and it sets transition: background, border, border-radius,
 * box-shadow, transform with no opacity in the list. Measured on the rendered
 * page: transition-property came back without opacity and a slide change
 * completed inside one frame. The generator said 600ms; the browser did 0. So
 * the selector below carries three of our own classes, (0,3,0), and beats it
 * outright rather than tying and hoping for source order.
 *
 * SECOND, the shape of the fade. Both images animated at once — the outgoing
 * one to 0 while the incoming one came up from 0 — so at the midpoint two
 * half-transparent photographs sat over the band and the ground showed through.
 * Two layers at 0.5 do not compose to one opaque layer; the whole hero dips in
 * brightness on every change, which is what reads as crude.
 *
 * The fix is order, not easing: the outgoing photograph STAYS opaque underneath
 * (is-leaving) while the incoming one fades in above it, and only once the
 * fade has ended does it drop out — invisibly, because it is covered. One layer
 * animates, nothing shows through, and the curve stops being load-bearing.
 *
 * The first frame is exempt from all of it: slide 1 ships with is-shown in
 * the markup, so its opacity is 1 at first paint and no transition runs on an
 * initial style. That matters beyond taste — it is the LCP element (D47/D61),
 * and an element fading in from 0 is not "contentful" until the fade ends.
 */
/*
 * THIRD, and this is why the transition sits on .is-shown and not on the base
 * rule. With it on the base rule EVERY image animates its opacity, in both
 * directions — including the leaver, when is-leaving is cleaned up at the end
 * of the fade. Cleaning it up restores opacity: 0 WITH a 400ms transition, so
 * the outgoing photograph does not drop out, it fades out over another 400ms.
 * On three of the four changes that is invisible: the leaver is earlier in DOM
 * order than the incoming, both sit at z-index 0 by then, so it fades away
 * underneath an opaque photograph. On the FOURTH — slide 4 back to slide 1, the
 * wrap — the leaver is the LAST child and the incoming is the FIRST, so it
 * fades out ON TOP: slide 1 arrives, slide 4 snaps back over it, and slide 1
 * arrives a second time. Reported as „the first slide appears twice, only when
 * it comes round again", and traced frame by frame on staging: at t+8426ms
 * bild 4 went z-index -1 -> 0 at opacity 1 and took 400ms to leave.
 *
 * Putting the transition on the target state fixes it without a line of
 * JavaScript, because a transition is decided by the style the element ends on:
 * gaining is-shown ends on a rule that has one, so the incoming fades in;
 * losing it ends on a rule that has none, so the leaver snaps. DOM order stops
 * mattering, which is what made this show on one change out of four.
 */
.v-hero .v-hero__bild.v-band__img {
  opacity: 0;
}

.v-hero .v-hero__bild.v-band__img.is-shown {
  opacity: 1;
  transition: opacity var(--valera-motion-media) var(--valera-motion-ease);
}

/*
 * The outgoing photograph: opaque, BELOW the incoming one, and not animated -
 * it is being covered, so animating it would only put the dip back.
 *
 * -1 and not a positive number, and that is the whole point of this comment.
 * The first attempt raised the incoming image to z-index 2 instead, which put
 * it above the scrim at z-index 1 and left the hero copy sitting on a bare
 * photograph - unreadable, and reported within minutes. The images have to stay
 * UNDER the scrim, so the layering happens below it rather than above.
 *
 * -1 is safe here for two measured reasons: .v-hero carries isolation: isolate,
 * so a negative child cannot escape the band, and the band has no background of
 * its own (rgba(0,0,0,0)), so there is nothing for the leaver to hide behind.
 */
.v-hero .v-hero__bild.v-band__img.is-leaving {
  opacity: 1;
  z-index: -1;
  transition: none;
}

.v-slider.e-con {
  /* Hug the four slides (drawn 656 = 4x153 + 3x8 + 2x10); an Elementor
     container is width:100% by default and the glass stretched edge to edge.
     Where the viewport is narrower than the strip (phones), max-width clamps
     it and the row scrolls by hand — the fourth thumbnail peeking past the
     edge is the affordance. */
  width: fit-content;
  max-width: 100%;
  align-self: flex-end;
  /* An Elementor container wraps by default; wrapped slides stacked the strip
     644px tall on phones instead of scrolling (measured). */
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}

.v-slider.e-con::-webkit-scrollbar {
  display: none;
}

/* Phones: the 360 artboard draws no strip and Serhii kept it that way — the
   backgrounds rotate on their own (hero.js's timer; display:none stops the
   CSS clock). From 768 up the strip is the drawn navigation. */
@media (max-width: 767px) {
  .v-slider.e-con {
    display: none;
  }
}

.v-slider {
  /* Drawn #FFF6F6@0.19 — a fifth near-white; ships as the kit's whitebg, the
     same unification as D39/D45 (D58). Blur via .valera-blur-10 beside it. */
  background: rgba(246, 246, 246, 0.19);
  border-radius: 4px;
}

.v-slider.e-con > .v-slide.e-con {
  width: 153px;
  min-width: 0;
  flex: 0 0 auto;
}

.v-slide__thumb img {
  width: 153px;
  height: 102px;
  object-fit: cover;
  border-radius: 4px;
  display: block;
}

.v-slide__label {
  position: relative;
  height: 42px;
  width: 100%;
}

/* The track and the fill are both DRAWN: #949597@0.4 on resting slides,
   #F6F6F6 full-width on the active one. Only the motion is ours. */
.v-slide__label::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: rgba(148, 149, 151, 0.4);
}

/* The fill animates TRANSFORM, not width: a width animation repaints every
   frame inside the strip's backdrop-filter layer — Safari pays for that in
   whole-page jank. scaleX composites. */
.v-slide__label::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 1px;
  width: 100%;
  background: #F6F6F6;
  transform: scaleX(0);
  transform-origin: 0 50%;
}

/*
 * The rotation runs everywhere (Serhii's call, 2026-08-18 — on phones the
 * backgrounds change with no strip at all). Pointer devices can pause it by
 * hovering; touch devices cannot pause it, which is a WCAG 2.2.2 exposure he
 * accepted, softened by prefers-reduced-motion switching the rotation off
 * entirely. On phones the strip is display:none, which also kills this
 * animation — hero.js carries a plain timer for that case.
 */
.v-slide.is-active .v-slide__label::after {
  animation: v-slide-fill var(--v-slider-interval, 8s) linear forwards;
}

@keyframes v-slide-fill {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/*
 * Pause while the pointer rests on the strip or while KEYBOARD focus is in it.
 * :focus-visible, not :focus-within — a mouse click also focuses the slide it
 * activated, and :focus-within kept the rotation paused forever after any
 * click (found on review). Hover is gated to devices that have one, so a tap
 * on a tablet cannot stick the pause either.
 */
@media (hover: hover) {
  .v-slider:hover .v-slide.is-active .v-slide__label::after {
    animation-play-state: paused;
  }
}

.v-slider:has(.v-slide:focus-visible) .v-slide.is-active .v-slide__label::after {
  animation-play-state: paused;
}

.v-slider[data-live] .v-slide {
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  /* .is-shown here too, and not by preference: that is where the transition
     now lives, at (0,4,0). A media query adds no specificity, so the old
     three-class selector would have lost to it and reduced motion would have
     stopped working, silently. */
  .v-hero .v-hero__bild.v-band__img.is-shown {
    transition: none;
  }

  .v-slide.is-active .v-slide__label::after {
    animation: none;
    transform: scaleX(1);
  }
}

/* Generated by build/lib/section-header.js — do not edit by hand. */

/*
 * The numbered section header (46 carriers, 23 placements).
 *
 * The eyebrow sits in columns 1-4 and the body in 5-12, so the
 * content column starts at 4 x 87.333333 + 4 x 24 = 445.3333 and is
 * 866.6667 wide — the two numbers the design draws — WITHOUT any gap being
 * named. The authored stackSpacing on those headers runs 48..317 and is a
 * consequence of that placement, never a parameter; on two of the eighteen it
 * is inert entirely, because the stack packs by distribution.
 *
 * `repeat(12, 1fr)` and not the column literal: the grid is STRETCH and
 * divides exactly at any width, while the literal is wrong the moment the
 * container is not 1312 (D7).
 *
 * The template is declared here but reaches the element through Elementor's own
 * grid control, which stores `var(--v-sechead-cols)`. Declaring
 * `--e-con-grid-template-columns` here directly does not work and does not
 * fail: the grid group renders its DEFAULTS at (0,3,0) whether or not you set
 * them, so a repeat(3, 1fr) it never asked for wins over a repeat(12, 1fr) it
 * did.
 */
.v-sechead.e-con {
  --v-sechead-cols: repeat(12, 1fr);
  --grid-align-content: start;
  --align-items: start;
  --column-gap: 24px;
  --row-gap: 32px;
}

.v-sechead > .v-sechead__eyebrow { grid-column: 1 / 5; }
.v-sechead > .v-sechead__body    { grid-column: 5 / -1; }

/*
 * The row becomes a stack at 894, which is derived and not chosen: below it the
 * 8-column body would be narrower than 532px, the narrowest body the design
 * itself stacks (`211:3263`). A layout change is the one thing a breakpoint is
 * the right tool for; every VALUE below is still a function of width.
 */
@media (max-width: 893px) {
  .v-sechead.e-con { --v-sechead-cols: 1fr; }
  .v-sechead > .v-sechead__eyebrow,
  .v-sechead > .v-sechead__body { grid-column: 1 / -1; }
}

/*
 * One placement only (`211:1830`): in the ROW state the body leads and the
 * eyebrow trails. It is done by placement rather than by DOM order, because the
 * same element has to stack the other way round: its mobile twin (`211:2050`)
 * puts the eyebrow first, like all 22 others. So the DOM keeps the one order
 * that reads correctly — the label, then the heading it labels — and the grid
 * mirrors it where the design does.
 *
 * `grid-row: 1` on both, and it is load-bearing: sparse auto-placement never
 * moves the cursor backwards, so an eyebrow placed at columns 9-12 pushes a
 * body with a definite column 1-8 onto row TWO.
 *
 * Inside `min-width` rather than beside the base rules, for the same reason:
 * these are (0,2,0) like the stack rule above, so written after it they won
 * inside its media query too and the reverse header kept an eight-column grid
 * at 893. Measured. Two mutually exclusive queries cannot do that.
 */
@media (min-width: 894px) {
  .v-sechead--reverse > .v-sechead__body {
    grid-column: 1 / 9;
    grid-row: 1;
  }
  .v-sechead--reverse > .v-sechead__eyebrow {
    grid-column: 9 / -1;
    grid-row: 1;
  }
  .v-sechead--reverse > .v-sechead__eyebrow.e-con { --justify-content: flex-end; }
}

/*
 * The narrow columns (532 / 600 / 762) stack at every width. Their gap is the
 * one value here with two drawn anchors — 48 in the narrow desktop columns and
 * 32 on their mobile counterparts — so it interpolates rather than steps (D51).
 */
.v-sechead--narrow.e-con,
.v-sechead--narrow-centred.e-con {
  --v-sechead-cols: 1fr;
  --row-gap: clamp(32px, calc(32px + 16 * (100vw - 360px) / 1080), 48px);
}

/* Named children, not `> *`: the two rules above are (0,2,0) and a
   `.v-sechead--narrow > *` is (0,1,0), so the universal selector loses and the
   narrow columns render as a four-column grid — measured, not predicted. */
.v-sechead--narrow > .v-sechead__eyebrow,
.v-sechead--narrow > .v-sechead__body,
.v-sechead--narrow-centred > .v-sechead__eyebrow,
.v-sechead--narrow-centred > .v-sechead__body { grid-column: 1 / -1; }

/* Three of the four narrow ones are centred, and only above the stack point:
   all 23 mobile carriers are left-aligned, their own counterparts included. */
@media (min-width: 894px) {
  .v-sechead--narrow-centred { justify-items: center; text-align: center; }
  .v-sechead--narrow-centred > .v-sechead__eyebrow.e-con { --justify-content: center; }
}

/*
 * The drawn text caps. 8 columns is 866.67; nine bodies are capped
 * inside it at 829 and two at 805 — a text measure inside the column, not the
 * column. Only in the row state: every one of the 23 mobile carriers fills its
 * 320, so a cap in the stack would be a number the design does not draw.
 */
@media (min-width: 894px) {
  .v-sechead--measure-829 > .v-sechead__body { max-width: 829px; }
  .v-sechead--measure-805 > .v-sechead__body { max-width: 805px; }
}

/* Three testimonial headers stack at 60 where the other twenty stack at 32.
   Above the stack point there is one grid row, so the row gap does nothing. */
.v-sechead--gap-60.e-con { --row-gap: 60px; }

/*
 * The body's own stacking: 24 desktop / 16 mobile, and 32 / 24 for the outer
 * column that holds the text block and the Actions frame. Two anchors each,
 * so both interpolate.
 */
.v-sechead__block.e-con {
  --row-gap: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px);
  --column-gap: 0px;
}

.v-sechead__body--acts.e-con {
  --row-gap: clamp(24px, calc(24px + 8 * (100vw - 360px) / 1080), 32px);
  --column-gap: 0px;
}

/*
 * The eyebrow's number and tagline are TOP aligned — the tagline's y is 0 on
 * all 46 carriers, so it is neither centred nor on a shared baseline. The
 * number's line box is 1.1 x its size and the tagline's 1.5 x its own, and
 * both start at the top of the row.
 */
.v-sechead__eyebrow.e-con { --align-items: flex-start; }

/*
 * The number never breaks. Measured at 894: the widest eyebrow does not fit its
 * four columns, the row shrank BOTH widgets, and the site's own
 * `overflow-wrap: break-word` backstop (D52) then split „04" into „0" and „4" —
 * a two-digit number on two lines, with no error anywhere. The tagline is the
 * part that may wrap; the number is atomic.
 *
 * Four classes on purpose. Elementor's widget-in-a-flex-container rules are
 * (0,4,0) and load after the child theme's sheets — the hero paid for that once
 * already.
 */
.v-sechead__eyebrow.e-con > .elementor-widget.v-sechead__num {
  flex-shrink: 0;
}

.v-sechead__num .elementor-heading-title {
  white-space: nowrap;
}

/*
 * Tracking is 0 on all 46 numbers and all 46 headings — measured, and the kit
 * says so too. It does not RENDER as 0: the kit's `primary` and `heading` rows
 * declare no letter-spacing at all, so both inherit the body's `text` role and
 * come out at -0.32px. Measured on the rendered page, not predicted.
 *
 * The real fix is one line in the kit's zero-tracking rows, which would put
 * every heading on the site right at once; it is on the launch checklist. This
 * rule is the atom's own scope in the meantime, and it fights nothing —
 * Elementor emits no letter-spacing declaration for these two at all.
 */
.v-sechead__num .elementor-heading-title,
.v-sechead__heading .elementor-heading-title {
  letter-spacing: 0;
}

.v-sechead .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* Generated by build/generate-accordion.js — do not edit by hand. */

/*
 * Block A — the accordion. Five sections, ten drawn placements.
 *
 * The section is the grid: 12 columns of the 1312 the design draws, the same
 * grid the section header divides, so the eyebrow and the photograph share
 * columns 1-4 and the heading, the accordion and the trailing card share
 * 5-12. Nothing here names 445.33 or 866.67; both fall out of the
 * placement, which is why the three line up at every width and not only at
 * 1312.
 *
 * The row's authored gap of 111 appears nowhere: the drawn stack is
 * SPACE_BETWEEN, so that number is inert and renders as 107 (or 109 on the
 * Startseite's 1312). A stored value is not a rendered value.
 *
 * SPECIFICITY NOTE, and it is load-bearing: `widget-nested-accordion.min.css`
 * is enqueued when the widget renders, i.e. AFTER the child theme's sheets. At
 * equal specificity the widget wins. Every rule below that touches a property
 * the widget also sets is written to out-specify it, and every value the widget
 * emits a DEFAULT for is set through its control instead — those land at (0,3,0)
 * and cannot be beaten from here.
 */
.v-acc.e-con {
  --v-acc-cols: repeat(12, 1fr);
  --grid-align-content: start;
  --align-items: start;
  --column-gap: 24px;
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /*
   * The design draws 1440 and 360 and nothing outside them. Below 1440 the
   * padding interpolates; above it the padding takes the surplus so the content
   * stays on the 1312 grid while the ground stays full-bleed.
   *
   * `100%` and not `100vw`: this element is the full-width band, so its own
   * containing block IS the page and the percentage resolves against the right
   * box. (100vw would also count the scrollbar and pull the content 8-17px off
   * centre on every desktop that shows one.)
   */
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/*
 * Explicit rows, not auto-placement. Sparse auto-placement never moves the
 * cursor backwards, so a photograph placed in columns 1-4 would push an
 * accordion with a definite column 5 onto the next row — the same trap the
 * section header's reverse layout hit.
 */
.v-acc > .v-sechead      { grid-column: 1 / -1;              grid-row: 1; }
.v-acc > .v-acc__photo { grid-column: 1 / 5;    grid-row: 2; align-self: start; }
.v-acc > .v-acc__list  { grid-column: 5 / -1;   grid-row: 2; }

/*
 * THE PHOTOGRAPH IS PLACED FROM THE TOP, and that is the whole fix for a jump
 * this block has now paid for twice.
 *
 * It used to be `align-self: end`, which is a faithful reading of the drawing —
 * the photograph's bottom edge does line up with the list's — but it makes the
 * photograph's position a FUNCTION OF THE LIST'S LIVE HEIGHT. Opening a panel
 * changes that height, so the photograph moved. The first repair gave the list
 * a min-height floor derived from the drawn answers; it took the jump from 49px
 * down to 3, and no further, because German copy renders a few pixels taller
 * than the .fig draws it and a floor is not a cap. Measured on staging:
 *   /             floor 342  list 342.0..345.6  photo top 404.0..406.6  2.6px
 *   /leistungen/  floor 464  list 464.0..467.2  photo top 365.0..368.2  3.2px
 *   /ueber-uns/   floor 442  list 442.0..444.8  photo top 343.0..345.8  2.8px
 *
 * THE OFFSET IS AGAINST THE FLOOR, NOT AGAINST THE DRAWN COLUMN, and getting
 * that wrong is worth writing down because it looked right and measured wrong.
 * The .fig draws EVERY panel expanded, so the drawn column
 * (startseite 342, ueber-uns 742, leistungen 830, stuetzmauer 926, kanalbau 902)
 * is not the column the page ever shows — the site opens one panel at a time,
 * which is the floor above (342, 442, 464, 538, 514).
 * Shipping the drawn y put the photograph 366px too low on /leistungen: it
 * reproduced a drawing of a state that does not exist. The offset is the floor
 * minus the drawn photograph instead, so the bottom edges line up in the state
 * the reader actually sees, and it is still a constant that no click can move.
 * A list that renders taller than its floor now shows that difference as a
 * static few pixels at its bottom edge instead of as a moving photograph.
 *
 * The rule this is an instance of: a value that must not move cannot be derived
 * from a live, variable quantity. Same shape as D51 and as the CTA's percentage
 * inside calc().
 */
@media (min-width: 894px) {
  .v-acc > .v-acc__photo--startseite { margin-block-start: 0px; }
  .v-acc > .v-acc__photo--ueber-uns { margin-block-start: 99px; }
  .v-acc > .v-acc__photo--leistungen { margin-block-start: 121px; }
  .v-acc > .v-acc__photo--stuetzmauer { margin-block-start: 195px; }
  .v-acc > .v-acc__photo--kanalbau { margin-block-start: 171px; }
}

/*
 * The list keeps a floor under it, and the floor is measured.
 *
 * Since 2026-08-19 one panel is open at a time, so opening a second closes the
 * first — and Elementor collapses the outgoing panel faster than it expands the
 * incoming one. Measured on /leistungen/: the section went 1003 -> 954 -> 981,
 * dipping 49px before settling 22px down. The photograph rides that, because it
 * is align-self: end against a column whose height was changing, so the whole
 * block jumped on every click.
 *
 * align-self: end is NOT the thing to change: it is measured on all four FAQ
 * pages (photo y = column height - 343). What was wrong is that the column's
 * height became variable when the open policy changed. Give it the height of
 * its tallest single-open state and the overshoot has nowhere to show.
 *
 * The floor is derived, not chosen: the drawn column minus every drawn answer
 * gives the closed list, and the closed list plus the TALLEST answer is the
 * tallest state one open panel can produce. It is a floor and not a cap, so an
 * answer that renders taller than drawn still grows the box — monotonically,
 * which was never the complaint.
 */
@media (min-width: 894px) {
  .v-acc > .v-acc__list--startseite { min-height: 342px; }
  .v-acc > .v-acc__list--ueber-uns { min-height: 442px; }
  .v-acc > .v-acc__list--leistungen { min-height: 464px; }
  .v-acc > .v-acc__list--stuetzmauer { min-height: 538px; }
  .v-acc > .v-acc__list--kanalbau { min-height: 514px; }
}
.v-acc > .v-acc__card  { grid-column: 5 / -1;   grid-row: 3; }

/*
 * The stack point is 894, imported from the section header rather than chosen
 * again: below it the 8-column body is narrower than the narrowest body the
 * design itself stacks. The header and the accordion must break at the SAME
 * width or the two columns stop agreeing halfway down the section.
 *
 * Rows go back to auto here on purpose. Four of the five placements have no
 * mobile photograph, and an explicit `grid-row: 3` on the accordion would keep
 * the empty photograph row — and its 32px row gap — in the stack.
 */
@media (max-width: 893px) {
  .v-acc.e-con { --v-acc-cols: 1fr; }
  .v-acc > .v-sechead,
  .v-acc > .v-acc__photo,
  .v-acc > .v-acc__list,
  .v-acc > .v-acc__card { grid-column: 1 / -1; grid-row: auto; }
  /* Four of five: the mobile artboard has no `Frame 28` at all. A
     `loading="lazy"` image inside `display: none` never enters the viewport,
     so this drops the request as well as the picture. */
  .v-acc--photo-desktop > .v-acc__photo { display: none; }
}

/* ---- the photograph ---------------------------------------------- *
 * 334 x 343 in the row, 320 x 369.701 in the stack — one file at two
 * aspect ratios, so the ratio steps at the stack point while the width stays
 * fluid. Its radius is 6 on every desktop copy and 6.4671 on the mobile one,
 * which is a scaled 6 (D7).
 */
.v-acc__photo { width: 100%; }

.v-acc__photo .elementor-widget-container,
.v-acc__photo figure,
.v-acc__photo img {
  display: block;
  width: 100%;
}

.v-acc__photo img {
  height: 100%;
  object-fit: cover;
  border-radius: var(--valera-radius-sm);
  aspect-ratio: 320 / 369.701;
}

/*
 * FIVE classes on the cap, and every one of them is needed. Elementor's own
 * `.elementor.elementor .e-con > .elementor-widget { max-width: 100% }` is
 * (0,4,0); a `.v-acc__photo` rule at (0,1,0) is not a close contest, and the
 * first build of this block rendered the photograph 464 wide — the full
 * four-column band — with nothing in any stylesheet looking wrong. This is the
 * same rule the hero step named, in a third place.
 */
@media (min-width: 894px) {
  .v-acc.e-con > .elementor-widget.v-acc__photo.elementor-widget-image {
    max-width: 334px;
  }
  .v-acc__photo img { aspect-ratio: 334 / 343; }
}

/* ---- the accordion ------------------------------------------------ *
 * What the widget's controls already hold is NOT repeated here: the item gap
 * (16), the question row's padding (0), the title/marker distribution, the
 * question's type and colour and the animation all live in the generated
 * element rule at (0,3,0), where they win. What is left is what no control
 * reaches.
 */

/* Elementor gives every panel a 1px #d5d8dc border via a (0,1,0) :where() rule;
   the design draws none on any of the fourteen placements. */
.v-acc > .v-acc__list { --n-accordion-border-width: 0px; }

/*
 * The answer, capped at the drawn 730. Its padding is NOT here: it is a real
 * setting on the container (see the payload), because the drawn 10px now travels
 * with the pixels the <details> used to carry and the widget's animation has to
 * be able to measure all of it in one `offsetHeight`.
 */
.v-acc__list .v-acc__answer.e-con {
  border-width: 0;
  max-width: 730px;
}

.v-acc__list .elementor-widget-text-editor p:last-of-type,
.v-acc__card .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/*
 * The rule, and the 8px above it.
 *
 * The file draws one line twice. On the Startseite it is a LINE node 8px below
 * the question row, inside the item; on the four FAQ pages it is the item's own
 * `borderBottomWeight: 1` with `stackPaddingBottom: 8`, below the answer. Those
 * two coincide exactly while an item is CLOSED, which is the state this
 * accordion ships in, and the second is what 20 of the 21 drawn OPEN items
 * show — so the item's bottom edge carries it. There is no control for this:
 * the widget's border control reaches `.e-n-accordion-item-title`, the
 * <summary>, and nothing else.
 *
 * THE 8px IS NO LONGER HERE. It used to be `padding-block-end` on this
 * element, and that put it outside every height the open/close animation
 * computes — the item landed 8 short and snapped, on every item, on every
 * click. It is carried by the summary and by the answer now, which is where the
 * widget can see it (nested-accordion boxFix). The 1px border stays: `offsetHeight`
 * is an integer, so 1px rounds away and measures 0.6px on the page.
 *
 * (0,3,0) because the widget sets `display`, `flex-direction` and `position`
 * on the same element from a later stylesheet; the border is not among them, but
 * the margin between items is, so the whole block is written to win.
 */
.v-acc > .v-acc__list .e-n-accordion-item:not(:last-child),
.v-acc > .v-acc__list--rule-last .e-n-accordion-item:last-child {
  border-block-end: 1px solid var(--e-global-color-secondary);
}

/* The question takes the width the marker leaves. Elementor's own stylesheet
/* Elementor's own typo: the title header is grown with a MENU variable
   (--n-menu-title-flex-grow) that does not exist on an accordion, so the title
   never takes the row and the marker collapses against the text. */
.v-acc > .v-acc__list .e-n-accordion-item-title-header { flex: 1 1 auto; min-width: 0; }

/*
 * The question's face, bound to the kit rather than restated — and it has to be
 * said HERE even though the widget already says it, because the widget says it
 * at (0,1,0):
 *
 *   :where( … > .e-n-accordion-item-title-header ) > .e-n-accordion-item-title-text
 *
 * Everything inside `:where()` contributes nothing, so that whole selector is
 * worth one class — and the kit's Theme Style writes `.elementor-kit-N h3` at
 * (0,1,1). The kit wins. Measured on the rendered page: the widget's
 * `title_typography` global was bound, present in `post-175.css`, and the
 * question came out in Inter because the site's own h3 rule out-specified it.
 * Its font-SIZE survived only because that one travels through
 * `--n-accordion-title-font-size` and lands in a (0,2,0) rule.
 *
 * Tracking is 0 on all 48 drawn questions and the kit's `cardhead` row declares
 * none, so it would otherwise inherit the body role's -2% — the same gap the
 * section header found on its headings; the kit fix is on the launch checklist.
 */
.v-acc > .v-acc__list .e-n-accordion-item-title-text {
  font-family: var(--e-global-typography-cardhead-font-family), sans-serif;
  font-weight: var(--e-global-typography-cardhead-font-weight);
  line-height: var(--e-global-typography-cardhead-line-height);
  letter-spacing: 0;
}

/*
 * The marker. `ic:baseline-plus` and `ic:baseline-minus` are a 32 x 32 frame
 * holding bars of 18.667 x 2.667 — two of them for the plus, one for the minus.
 * Drawn here rather than through the ICONS control because that control's only
 * built-in library is Font Awesome, whose stylesheet and webfont would be
 * enqueued to render two rectangles on a site that self-hosts every other
 * glyph. `render()` omits the icon markup entirely when the control is empty,
 * so nothing is left over.
 *
 * `currentColor`: 43 of the 48 drawn markers are #000000 and the five Kanalbau
 * desktop ones are #171717 — two blacks that normalise to the question's own
 * `black`, so the marker simply takes the colour the title already has, in
 * every state.
 */
.v-acc > .v-acc__list .e-n-accordion-item-title::after {
  content: "";
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  background-image:
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor);
  background-size: 18.667px 2.667px, 2.667px 18.667px;
  background-position: center, center;
  background-repeat: no-repeat;
}

.v-acc > .v-acc__list .e-n-accordion-item[open] > .e-n-accordion-item-title::after {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 18.667px 2.667px;
}

/*
 * Ours, and nothing in the file argues with it: the design draws no hover and
 * no focus state anywhere (0 of 29 interaction fields). A visible focus ring is
 * not decoration — the question row is the only control in the block and it is
 * reached with Tab and the arrow keys.
 */
.v-acc > .v-acc__list .e-n-accordion-item-title:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/* ---- the trailing card -------------------------------------------- *
 * Drawn on three of the five pages, on the same 8 columns as the accordion,
 * 867 x 152 desktop. Its two gaps are 24 and 16 at BOTH widths, so they are
 * Elementor's and not interpolated here; only the column placement is width's.
 */
.v-acc__card.e-con { --column-gap: 0px; }
.v-acc__card-text.e-con { --column-gap: 0px; }


/* Generated by build/generate-beforeafter.js — do not edit by hand. */

/*
 * The homepage before/after comparison, ONE placement (`211:1855` desktop,
 * `211:2176` mobile).
 *
 * The box is the only fluid thing: 1312 x 780.016 at 1440 and 320 x 213 at 360,
 * both drawn, interpolated between them (D51). The chrome — the 58px chips,
 * the 50px disc, the 4px divider — keeps its drawn desktop size at both
 * ends, because the mobile artboard's own chrome is a uniform shrink of the
 * desktop one (24.39% of it) and lands at 3.9px type and a 12.2px disc.
 */
.v-ba.e-con {
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /* `100%` and not `100vw`: this element IS the full-width band, so its own
     containing block is the page and the percentage resolves against the right
     box (D51's second half). */
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --align-items: stretch;
}

/* ---- the header ---------------------------------------------------- *
 * A row on the desktop, drawn SPACE_EVENLY with the column at x=0 and the link
 * ending at the right edge — i.e. space-between at 772px of content — and a
 * stack below 894. That number is the section-header atom's own and is imported
 * from it, not derived again: every other section on this page breaks there.
 *
 * `.e-con.e-flex` — three classes. Elementor's own `.e-con.e-flex` already
 * declares `--flex-direction: column` at (0,2,0), so a two-class rule only
 * TIES with it and is decided by stylesheet order.
 */
.v-ba__head.e-con.e-flex {
  --flex-direction: column;
  --row-gap: 24px;
  --column-gap: 16px;
}

/* The drawn measure of the heading column, from the CHILD frame: its wrapper is
   606x75 while the frame that was actually laid out is 628x82. */
.v-ba__intro.e-con {
  --row-gap: 16px;
  --width: min(100%, 628px);
}

@media (min-width: 894px) {
  .v-ba__head.e-con.e-flex {
    --flex-direction: row;
    --flex-wrap: nowrap;
    --justify-content: space-between;
  }
  /*
   * `align-self` on the CHILD and not `--align-items` on the row: Elementor's
   * alignment travels through a custom property, and custom properties
   * INHERIT — an `--align-items: flex-end` on the row would be inherited by
   * the intro column and shrink it to its content. The drawn row counter-aligns
   * MAX, i.e. the link sits on the column's bottom line.
   */
  .v-ba__head > .v-ba__more.elementor-widget { align-self: flex-end; }
}

/*
 * The link. The design's own arrow is a `solar:arrow-up-linear` vector
 * NETWORK — segments and regions, no `fillGeometry` — so it cannot be decoded,
 * which is the same class of asset D21 and D40 already record. The site's own
 * decoded chevron stands in, turned to point the way the drawn one does.
 */
.v-ba__more .elementor-heading-title { margin: 0; }

.v-ba__more a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  color: inherit;
}

.v-ba__more a:hover { text-decoration: underline; }

.v-ba__more a::after {
  content: '';
  flex: none;
  width: 24px;
  height: 24px;
  background-color: currentColor;
  -webkit-mask: url(img/icons/chevron-down.svg) center / contain no-repeat;
  mask: url(img/icons/chevron-down.svg) center / contain no-repeat;
  transform: rotate(-90deg);
}

.v-ba__more a:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/* ---- the comparison box -------------------------------------------- *
 * `--v-ba-pos` is the whole mechanism and it is declared HERE, at 50%, which is
 * the state the design draws — the top image starts at 50% of the box and
 * the disc's centre is at 50% of it, two independent readings of the same
 * number. With no JavaScript, and with the control stripped for any reason,
 * that declaration is the whole block and the section renders exactly as drawn.
 */
.v-ba__stage.e-con {
  --v-ba-pos: 50%;
  position: relative;
  height: clamp(213px, calc(213px + 567.016 * (100vw - 360px) / 1080), 780.016px);
  border-radius: var(--valera-radius-sm);
  overflow: hidden;
  /* Reclaim every dimension rather than the one in mind: Elementor's
     `.e-con::before` background overlay arrives with `inset: 0` and a
     `height: max(…)` of its own, and the footer lost a whole artwork layer to
     exactly that. */
  isolation: isolate;
}

/*
 * FIVE classes on the photograph's cap, and every one is needed: Elementor's
 * own `.elementor.elementor .e-con > .elementor-widget { max-width: 100% }` is
 * (0,4,0) and loads after the child theme's sheets. The hero paid for that
 * once, the accordion again and the mosaic a third time.
 */
.v-ba__stage.e-con > .elementor-widget.v-ba__photo.elementor-widget-image,
.v-ba__clip.e-con > .elementor-widget.v-ba__photo.elementor-widget-image {
  position: absolute;
  inset: 0;
  max-width: none;
  width: auto;
  height: auto;
  margin: 0;
}

.v-ba__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * The reveal. `clip-path: inset()` and not a width, because a width is layout
 * on every frame of a drag and this is a drag; an inset clip is paint only, and
 * D58's rule is that Safari pays per frame for what Blink absorbs.
 *
 * `--width: auto` and an explicit `height: auto`: an `.e-con` is
 * `width: var(--width)` and would otherwise fight `inset: 0`.
 */
.v-ba__clip.e-con {
  position: absolute;
  inset: 0;
  --width: auto;
  height: auto;
  clip-path: inset(0 0 0 var(--v-ba-pos));
}

/* ---- the two labels ------------------------------------------------- *
 * Drawn 32px in from their own edge and 32px down, 58 tall, radius 16,
 * `#171717` with `#F6F6F6` type. The drawn boxes are 125 and 117 wide — hand-sized,
 * with the master's own 24px horizontal padding centring the text inside them —
 * so the chip HUGS at the drawn padding here and the drawn widths are recorded
 * rather than reproduced (D279).
 */
.v-ba__stage.e-con > .elementor-widget.v-ba__pill {
  position: absolute;
  top: 32px;
  width: auto;
  max-width: none;
  margin: 0;
}

.v-ba__pill--before.elementor-widget { left: 32px; }
.v-ba__pill--after.elementor-widget { right: 32px; }

.v-ba__pill .elementor-heading-title {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 58px;
  padding: 12px 24px;
  border-radius: 16px;
  /* The 1px stroke the master carries. D5 drops it on the yellow buttons
     because there it is the same colour as the fill and invisible by
     construction; on these four instances the fill is overridden to
     `#171717` and the stroke is not, so it is visible and it is drawn. */
  border: 1px solid var(--e-global-color-accent);
  background-color: var(--e-global-color-primary);
  margin: 0;
}

/* ---- the control ---------------------------------------------------- */
.v-ba__control.elementor-widget {
  position: absolute;
  inset: 0;
  width: auto;
  max-width: none;
  margin: 0;
}

/*
 * The range covers the whole box, so a click anywhere sets the divider and a
 * drag from anywhere moves it. `touch-action: pan-y` keeps a vertical swipe
 * scrolling the page instead of being swallowed by a full-width control.
 */
.v-ba__range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  background: transparent;
  border: 0;
  -webkit-appearance: none;
  appearance: none;
  cursor: ew-resize;
  touch-action: pan-y;
}

/*
 * A ZERO-WIDTH thumb. Every engine insets a range thumb by half its own width
 * so it cannot leave the track, so a 50px thumb would put value 0 at 25px and
 * the drawn divider — positioned from the same variable — would part company
 * with it at both ends. At width 0 the value maps linearly to 0..100%.
 */
.v-ba__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 0;
  height: 50px;
  background: transparent;
  border: 0;
}

.v-ba__range::-moz-range-thumb {
  width: 0;
  height: 50px;
  background: transparent;
  border: 0;
}

.v-ba__range::-webkit-slider-runnable-track { background: transparent; }
.v-ba__range::-moz-range-track { background: transparent; }

/* The ring belongs on the disc, which is what a reader is looking at — not on
   the invisible full-box input. The adjacent sibling is exactly that disc. */
.v-ba__range:focus { outline: none; }

/*
 * The divider and the disc are ONE element: the line is the element, the disc
 * is its `::after`. `transform: translateX(-50%)` rather than a negative
 * margin so the offset follows the drawn width if that ever changes.
 */
.v-ba__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--v-ba-pos);
  width: 4px;
  transform: translateX(-50%);
  background-color: var(--e-global-color-accent);
  pointer-events: none;
}

.v-ba__handle::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 50px;
  height: 50px;
  margin: -25px 0 0 -25px;
  border-radius: 50%;
  /* The circle's fill and the arrow together, in the colours the file draws:
     the glyph is decoded out of `211:1870` (50 path commands, bbox
     27.0174 x 18.0174 — the node's own size to four decimals) with its #171717
     baked in, because a mask carries one colour and this disc has two. */
  background: var(--e-global-color-accent) url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNy4wMTc0IDE4LjAxNzQiIGZpbGw9IiMxNzE3MTciPjxwYXRoIGQ9Ik0xMC4wNzM3IDAuNDQzNyBDOS45MzQzIDAuMzAzMSA5Ljc2ODQgMC4xOTE1IDkuNTg1NiAwLjExNTQgQzkuNDAyOCAwLjAzOTIgOS4yMDY3IDAgOS4wMDg3IDAgQzguODEwNyAwIDguNjE0NiAwLjAzOTIgOC40MzE4IDAuMTE1NCBDOC4yNDkgMC4xOTE1IDguMDgzMSAwLjMwMzEgNy45NDM3IDAuNDQzNyBMMC40NDM3IDcuOTQzNyBDMC4zMDMxIDguMDgzMSAwLjE5MTUgOC4yNDkgMC4xMTU0IDguNDMxOCBDMC4wMzkyIDguNjE0NiAwIDguODEwNyAwIDkuMDA4NyBDMCA5LjIwNjcgMC4wMzkyIDkuNDAyOCAwLjExNTQgOS41ODU2IEMwLjE5MTUgOS43Njg0IDAuMzAzMSA5LjkzNDMgMC40NDM3IDEwLjA3MzcgTDcuOTQzNyAxNy41NzM3IEM4LjA4MzEgMTcuNzE0MyA4LjI0OSAxNy44MjU5IDguNDMxOCAxNy45MDIgQzguNjE0NiAxNy45NzgyIDguODEwNyAxOC4wMTc0IDkuMDA4NyAxOC4wMTc0IEM5LjIwNjcgMTguMDE3NCA5LjQwMjggMTcuOTc4MiA5LjU4NTYgMTcuOTAyIEM5Ljc2ODQgMTcuODI1OSA5LjkzNDMgMTcuNzE0MyAxMC4wNzM3IDE3LjU3MzcgQzEwLjIxNDMgMTcuNDM0MyAxMC4zMjU5IDE3LjI2ODQgMTAuNDAyIDE3LjA4NTYgQzEwLjQ3ODIgMTYuOTAyOCAxMC41MTc0IDE2LjcwNjcgMTAuNTE3NCAxNi41MDg3IEMxMC41MTc0IDE2LjMxMDcgMTAuNDc4MiAxNi4xMTQ2IDEwLjQwMiAxNS45MzE4IEMxMC4zMjU5IDE1Ljc0OSAxMC4yMTQzIDE1LjU4MzEgMTAuMDczNyAxNS40NDM3IEwzLjYyMzcgOS4wMDg3IEwxMC4wNzM3IDIuNTczNyBDMTAuMjE0MyAyLjQzNDMgMTAuMzI1OSAyLjI2ODQgMTAuNDAyIDIuMDg1NiBDMTAuNDc4MiAxLjkwMjggMTAuNTE3NCAxLjcwNjcgMTAuNTE3NCAxLjUwODcgQzEwLjUxNzQgMS4zMTA3IDEwLjQ3ODIgMS4xMTQ2IDEwLjQwMiAwLjkzMTggQzEwLjMyNTkgMC43NDkgMTAuMjE0MyAwLjU4MzEgMTAuMDczNyAwLjQ0MzcgWiBNMjYuNTczNyA3Ljk0MzcgTDE5LjA3MzcgMC40NDM3IEMxOC45MzM4IDAuMzAzOCAxOC43Njc4IDAuMTkyOSAxOC41ODUxIDAuMTE3MiBDMTguNDAyMyAwLjA0MTUgMTguMjA2NSAwLjAwMjYgMTguMDA4NyAwLjAwMjYgQzE3LjYwOTIgMC4wMDI2IDE3LjIyNjIgMC4xNjEyIDE2Ljk0MzcgMC40NDM3IEMxNi44MDM4IDAuNTgzNiAxNi42OTI5IDAuNzQ5NiAxNi42MTcyIDAuOTMyMyBDMTYuNTQxNSAxLjExNTEgMTYuNTAyNiAxLjMxMDkgMTYuNTAyNiAxLjUwODcgQzE2LjUwMjYgMS45MDgxIDE2LjY2MTIgMi4yOTEyIDE2Ljk0MzcgMi41NzM3IEwyMy4zOTM3IDkuMDA4NyBMMTYuOTQzNyAxNS40NDM3IEMxNi44MDMxIDE1LjU4MzEgMTYuNjkxNSAxNS43NDkgMTYuNjE1NCAxNS45MzE4IEMxNi41MzkyIDE2LjExNDYgMTYuNSAxNi4zMTA3IDE2LjUgMTYuNTA4NyBDMTYuNSAxNi43MDY3IDE2LjUzOTIgMTYuOTAyOCAxNi42MTU0IDE3LjA4NTYgQzE2LjY5MTUgMTcuMjY4NCAxNi44MDMxIDE3LjQzNDMgMTYuOTQzNyAxNy41NzM3IEMxNy4wODMxIDE3LjcxNDMgMTcuMjQ5IDE3LjgyNTkgMTcuNDMxOCAxNy45MDIgQzE3LjYxNDYgMTcuOTc4MiAxNy44MTA3IDE4LjAxNzQgMTguMDA4NyAxOC4wMTc0IEMxOC4yMDY3IDE4LjAxNzQgMTguNDAyOCAxNy45NzgyIDE4LjU4NTYgMTcuOTAyIEMxOC43Njg0IDE3LjgyNTkgMTguOTM0MyAxNy43MTQzIDE5LjA3MzcgMTcuNTczNyBMMjYuNTczNyAxMC4wNzM3IEMyNi43MTQzIDkuOTM0MyAyNi44MjU5IDkuNzY4NCAyNi45MDIgOS41ODU2IEMyNi45NzgyIDkuNDAyOCAyNy4wMTc0IDkuMjA2NyAyNy4wMTc0IDkuMDA4NyBDMjcuMDE3NCA4LjgxMDcgMjYuOTc4MiA4LjYxNDYgMjYuOTAyIDguNDMxOCBDMjYuODI1OSA4LjI0OSAyNi43MTQzIDguMDgzMSAyNi41NzM3IDcuOTQzNyBaIi8+PC9zdmc+") center / 27.0174px 18.0174px no-repeat;
}

.v-ba__range:focus-visible + .v-ba__handle::after {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

.v-ba .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* Generated by build/generate-bento.js — do not edit by hand. */

/*
 * The homepage services bento. One section, one page, EIGHT services.
 *
 * The parked strip `211:2392` is 2648 x 883: two 1312 x 883 panels at gap 24.
 * 1312 is the content column, so the drawn 1440 section is the strip's first
 * view — four cards on the column and four off to the right, which is what the
 * annotation „scroll horizontally" at `211:2372` is pointing at.
 *
 * The strip is a NATIVE horizontal scroll region with scroll-snap on the two
 * panels, not a pinned section (D258): the drawn section is 1239 tall and
 * `stepscroll.js` refuses a pin under about 1071px of viewport, which is more
 * than a 13-inch laptop has. No JavaScript is added by this block.
 *
 * Below 894 — the section header's own stack point, so the section breaks in one
 * place and not two — the two panels become the ONE COLUMN the mobile artboard
 * draws: eight cards, 430 tall, gap 16.
 */
.v-bento.e-con {
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /* `100%` and not `100vw`: this element IS the band, so its own containing
     block is the page and the percentage resolves against the right box.
     THE INSET IS 64 ON THE DESKTOP AND 20 ON THE MOBILE, and neither is the
     section's own padding control: the drawn 1440 section carries 0 and puts
     the 1312 column on its HEADER child instead (`211:1830`, padding 0/64),
     while the mobile artboard puts 20 on the section and none on its header.
     Reading only the section would have shipped a band that pads to ZERO at
     1440. Summed per breakpoint, and gated as a sum. */
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --align-items: stretch;
}

/* ---- the scroll region --------------------------------------------- *
 * Stacked below the stack point, so nothing here applies there: no overflow,
 * no snap, and the track is a single column.
 *
 * The 4px inset is the focus ring's reach (`--valera-focus-width` +
 * `--valera-focus-offset`, read from lib/focus.js): a scroll container clips
 * its children's outlines, so the room is given back as padding and taken off
 * again as margin. Plain px on both sides — a percentage here would resolve
 * against a different box on each of them.
 */
.v-bento__view.e-con {
  --row-gap: 0px;
  --column-gap: 0px;
  /*
   * `--width` and not `width`: Elementor's `.e-con { width: var(--width) }`
   * reads the variable, so feeding the variable keeps one declaration in play
   * instead of two fighting.
   *
   * AND IT HAS TO GROW BY THE INSET, not merely be offset by it. Elementor's
   * boxes are `border-box`, so a plain `width: 100%` with 4px of padding leaves a
   * CONTENT box of column − 8 — measured on the rendered page: the panel came out
   * 1304 instead of 1312 and every card 4 short, silently. The border box
   * grows by 8, the margin takes the same 8 back, and the content box is the
   * content column to the pixel.
   */
  --width: calc(100% + 8px);
  --padding-top: 4px;
  --padding-right: 4px;
  --padding-bottom: 4px;
  --padding-left: 4px;
  margin: -4px;
}

.v-bento__track.e-con {
  --v-bento-track-cols: 1fr;
  --row-gap: 16px;
  --column-gap: 0px;
}

.v-bento__panel.e-con {
  --v-bento-cols: 1fr;
  --v-bento-rows: none;
  --row-gap: 16px;
  --column-gap: 0px;
}

.v-pin.v-pin--startseite-bento.e-con {
  --v-pin-advance: 80vh;
  /* The zones are flow children, so the container's own gap would space them
     apart and add itself to the pin distance. */
  --row-gap: 0px;
  --column-gap: 0px;
  --padding-block-start: 0px;
  --padding-block-end: 0px;
  --padding-inline-start: 0px;
  --padding-inline-end: 0px;
}

/* Off below the stack point: stacked, a pinned section would hold the page
   still over a column that is now the page's own flow. */
@media (min-width: 894px) {
  .v-pin.v-pin--startseite-bento.is-pinned { position: relative; }

  .v-pin.v-pin--startseite-bento.is-pinned > .v-bento__view {
    position: sticky;
    /*
     * The offset is the SCRIPT's, because it depends on the window: normally
     * the bar's height, and on a short window as low as the content allows so
     * the section's own rows stay on screen. The fallback is the bar, so the
     * declaration is correct before the script runs and if it never does.
     */
    top: var(--v-pin-top, var(--valera-header-h));
  }

  /*
   * THE ZONES ARE THE PIN'S LENGTH. Flow children, on purpose: a sticky element
   * is constrained to its containing block's CONTENT box, and padding is
   * outside it — bought with padding, the pin does not happen at all, silently.
   *
   * visibility: hidden rather than display: none, because an
   * IntersectionObserver needs geometry and hidden boxes have it.
   */
  .v-pin.v-pin--startseite-bento.is-pinned > .v-pin__zone {
    height: var(--v-pin-advance);
    visibility: hidden;
    pointer-events: none;
    /*
     * NO SNAP ON THIS ONE, and the measurement is why (D318).
     *
     * The soft close is right for a STEPPED pin: the zone starts are the
     * positions the block actually rests at, so pulling to one lands the reader
     * where the design draws them. A consumer whose motion is CONTINUOUS has no
     * such positions — every scroll offset is a valid state — and the pull then
     * fights the reader instead of helping.
     *
     * Measured on the homepage bento, 1280x680, deployed: approaching the
     * section a 100px scroll was dragged **136px forward** onto a zone start,
     * and from there every further 100px request landed back on the same
     * pixel — the page could not be scrolled past the point at all in ordinary
     * increments. „Оно всё рывками", and it was this.
     */
  }
}



/* ---- the strip, driven by the page's own scroll --------------------- *
 *
 * Everywhere the block is two columns — the section header's own stack point,
 * 894 — and not from 1440 as it was until 2026-08-20. One page must not hold
 * two different blocks: a 1280 window got a mute horizontal scroller and a 1440
 * one got the pin, which is exactly the „скролит вообще не правильно" Serhii
 * reported (his window is 1280 wide, so nothing ever travelled for him). D314.
 *
 * NO SCALE ANY MORE. The pinned view used to be forced to
 * `883px * --v-pin-fit` and the whole strip scaled down to whatever was left
 * under the bar — which on a 1440x900 window meant the cards filled the screen
 * exactly, edge to edge, with no air at all. The panel's own ceiling
 * (`max-height`, below) now guarantees the fit before the script is consulted,
 * so the view keeps its natural height and `--v-pin-fit` has no job left.
 */
@media (min-width: 894px) {
  @media (prefers-reduced-motion: no-preference) {
    /* Pinned, the view stops being a scroller: the page's scroll is the only
       thing moving the strip, and two scrollers on one axis fight. */
    .v-pin.v-pin--startseite-bento.is-pinned .v-bento__view.e-con {
      overflow-x: clip;
      scroll-snap-type: none;
    }

    /*
     * THE PINNED STRIP SITS IN THE MIDDLE of the space under the bar.
     *
     * It used to park against the bar and leave its leftover hanging below —
     * measured on the live homepage, 74-90px of it depending on the window.
     * Serhii, 2026-08-24: „не очень аккуратно".
     *
     * The leftover is the pin's to know, not this block's: the strip's height
     * is the smaller of its aspect ratio and its ceiling, so any expression for
     * it here would be a second copy of the ceiling — the thing D314 was about.
     * The browser
     * half publishes it and the pin's own helper spends it, symmetrically, as
     * padding. band() excludes padding, so neither the fit test nor the ceiling
     * is touched by this.
     *
     * The floor is the strip's own inset, which is also the snap padding: with
     * no slack, or with no JavaScript, the strip keeps exactly what it had.
     */
    .v-pin--startseite-bento.is-pinned > .v-bento__view.e-con {
      --padding-top: max(4px, calc(var(--v-pin-slack, 0px) / 2));
      --padding-bottom: max(4px, calc(var(--v-pin-slack, 0px) / 2));
    }

    /*
     * THE TRAVEL IS THE WHOLE STRIP, from flush-left to flush-right, and it is
     * the same distance the native scroller below has.
     *
     * `100%` is the track's own border box, which is the view's content box,
     * which is the content column. The track holds two panels of
     * `100% - 64` with the drawn 24 between them, so its content runs
     * 104px past the column and that surplus IS the travel. Ending there puts
     * panel 2 flush right with 40px of panel 1 showing on the left — the mirror
     * of the resting state, so the reader can see where they came from.
     */
    .v-pin.v-pin--startseite-bento.is-pinned .v-bento__track.e-con {
      transform-origin: top left;
      transform: translateX(calc(var(--v-bento-step, 0) * -1 * var(--v-bento-travel)));
      transition: transform var(--valera-motion-media) var(--valera-motion-ease);
    }

    .v-pin.v-pin--startseite-bento.is-pinned[data-step="1"] .v-bento__track.e-con { --v-bento-step: 0; }
    .v-pin.v-pin--startseite-bento.is-pinned[data-step="2"] .v-bento__track.e-con { --v-bento-step: 1; }

    /*
     * AND THE TRAVEL IS CONTINUOUS WHERE THE BROWSER CAN DO IT (D317).
     *
     * The rules above are two states with a transition between them, and that
     * is what a step-based pin gives: the strip stands still, then slides all
     * at once when a threshold is crossed. Scroll quickly and the slide is over
     * before you have looked at it — Serhii, 2026-08-20: «если быстро
     * скролишь, то не понятно что оно вообще есть … как-то коряво».
     *
     * Tied to the scroll instead, the strip moves exactly as much as the reader
     * moves, at whatever speed they move, and stops when they stop. Same
     * mechanism the „Unsere Geschichte" spine already uses on this site, same
     * shape: the timeline is NAMED on the subject and referenced from the
     * element that moves.
     *
     * THE SUBJECT IS THE PIN TRACK AND IT HAS TO BE. The strip itself is inside
     * the sticky element, so its box does not move through the viewport during
     * the pin at all — a `view()` on it would be a timeline that never
     * advances. The track is the tall, unsticky box the zones make, so its
     * progress IS the pin's progress.
     *
     * `contain` is the range where the track covers the whole viewport, which
     * sits inside the sticky range with a beat at each end: the strip is
     * already still when the pin takes hold, pans, and is still again before it
     * lets go. Measured, 1280x680: the pin holds for 408px and the pan uses the
     * middle 262 of them.
     *
     * No support: the two states above stand, which is the arrangement this
     * block shipped with. Nothing is lost, only the smoothness.
     */
    @supports (animation-timeline: view()) {
      .v-pin.v-pin--startseite-bento.is-pinned.e-con {
        view-timeline: --v-bento-strip block;
        /*
         * THE PAN IS EXACTLY THE PIN (D318), and that is what „плавно заходить …
         * и так же плавно выходить" turned out to need.
         *
         * Untouched, the range is the track's own `contain`: trackTop at the
         * viewport top to trackBottom at its foot. But the section LOCKS one
         * header height earlier and RELEASES one band inset later, so those two
         * margins are scroll the page is held for and nothing moves in — you
         * arrive, the section stops dead, you push and nothing happens, then
         * the strip starts. The same again at the far end. That is the jerk at
         * each end: not the pin, the gap between the pin and the pan.
         *
         * The inset closes both gaps exactly, and it is written from THE PIN'S
         * OWN TWO NUMBERS rather than from a second copy of the ceiling: the
         * pin holds from the moment the track's head reaches `--v-pin-top` to
         * the moment its foot reaches `top + --v-pin-height`, both published by
         * the browser half, which is the only thing that can measure the pinned
         * geometry. Written out of the ceiling instead it was right only where
         * the ceiling BINDS: on an iMac the strip reaches its drawn height, the
         * view is shorter than the ceiling, and the pan finished 275px before
         * the section let go — held scroll with nothing moving in it, which is
         * the defect this whole decision is about.
         *
         * Nothing is added beyond the two gaps. Leading the pan by a step was
         * tried and measured, and it carries the first card 200px off the left
         * edge before the section even settles — a reader never sees the drawn
         * first view. The pace is bought with the pin's own length instead
         * (`advance` above), which costs nothing at either end.
         */
        view-timeline-inset: var(--v-pin-top)
                             calc(100svh - var(--v-pin-top) - var(--v-pin-height));
      }

      .v-pin.v-pin--startseite-bento.is-pinned .v-bento__track.e-con {
        transition: none;
        /* ONE DRIVER. The step above is the no-support arrangement and must not
           reach the transform here as well — see the keyframes. */
        --v-bento-step: 0;
        animation-name: v-bento-travel;
        animation-fill-mode: both;
        animation-timing-function: linear;
        animation-timeline: --v-bento-strip;
        animation-range: contain 0% contain 100%;
      }

      /*
       * BOTH FRAMES ARE WRITTEN OUT, and that is the whole of a bug this cost
       * an hour. With only a `to` frame the implicit `from` is the element's
       * OWN current value — which here is the step rule above. The instant the
       * observer flipped `data-step` to 2 the from-frame became the end
       * position, the animation interpolated from the end to the end, and the
       * strip snapped the remaining 40 % of its travel in one frame at 60 % of
       * the way through. Measured: linear to progress 0.609 (-399px), then
       * -656 at 0.638. Everything else about the animation was correct.
       */
      @keyframes v-bento-travel {
        from { transform: translateX(0); }
        62.5% { transform: translateX(calc(-1 * var(--v-bento-travel))); }
        to { transform: translateX(calc(-1 * var(--v-bento-travel))); }
      }
    }
  }
}

@media (min-width: 894px) {
  .v-bento__view.e-con {
    overflow-x: auto;
    /* A horizontal flick inside the strip must not walk the page or trigger a
       back gesture. */
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    /* The snapport is the CONTENT box, so panel 1 snaps to 0 and panel 2 to
       exactly one panel plus one gutter. */
    scroll-padding: 4px;
  }

  /*
   * `100%` of the TRACK's content box, which is the view's content box, which
   * is the content column. Each panel gives up 64 of it — one gutter and
   * the design's own 40px peek (D315) — so panel 2's edge is on screen at rest
   * and the strip says „there is more" without a control the design never drew.
   */
  .v-bento__track.e-con {
    --v-bento-panel-w: min(calc(100% - 64px), calc(calc(100svh - var(--valera-header-h) - clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px) - 8px) * 1.8573));
    /* The whole travel: two panels and a gutter, less the column that is
       already on screen. Both the pinned pan and the native scroller's end
       snap land on it, so the two arrangements cannot drift apart. */
    --v-bento-travel: calc(2 * var(--v-bento-panel-w) + 24px - 100%);
    --v-bento-track-cols: var(--v-bento-panel-w) var(--v-bento-panel-w);
    --row-gap: 0px;
    --column-gap: 24px;
  }

  /*
   * The panel keeps its drawn composition at every width: `aspect-ratio` sets
   * the height from the width, and the row tracks are `fr` so the four card
   * heights stay in their drawn proportion. The 24px gutters stay 24px, because a
   * gutter is not part of the picture. At 1312 this is the drawing to the pixel.
   */
  .v-bento__panel.e-con {
    --v-bento-cols: 1fr 24px 1fr;
    --row-gap: 0px;
    --column-gap: 0px;
    aspect-ratio: 1312 / 883;
    /*
     * THE CEILING (D314). The aspect ratio above is the preferred height; this
     * is the most there is room for. Whichever is smaller wins, so the four
     * cards are whole on every window and never larger than the screen.
     * `align-self: start` because the track stretches its items by default and
     * a stretched item would take the row's height back off the ceiling.
     */
    max-height: calc(100svh - var(--valera-header-h) - clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px) - 8px);
    align-self: start;
    /*
     * The scroll range ends 4px short of panel 2's own snap point, and that is
     * measured rather than tolerated blindly: a scroll container's scrollable
     * region ends at its last descendant's border box, so the 4px of trailing
     * inset the focus ring needs is not in it. Three ways of buying it back
     * were tried on the rendered page and all three failed — a trailing
     * `margin-inline-end` on the panel (region unchanged at 2652), a third
     * 4px grid track (unchanged), and `width: max-content` on the track (the
     * percentage tracks go circular and the panel blows out to 7914). So panel
     * 2 rests 4px right of the column at full scroll instead of on it. The
     * alternative is an empty spacer element in the client's editor for 4px
     * nobody can see.
     */
    scroll-snap-align: start;
  }

  /*
   * The LAST panel snaps by its trailing edge, and that is what finally pays
   * off D259's 4px: a scroll container's scrollable region ends at its last
   * descendant's border box, so a `start` snap on panel 2 asks for a position
   * the scroller cannot reach and it rested 4px off the column. `end` asks for
   * the position the travel actually has — panel 2 flush right, 40px of panel 1
   * showing — which is also exactly where the pinned travel above ends, so the
   * two arrangements agree instead of differing by a few pixels.
   */
  .v-bento__panel--2.e-con { scroll-snap-align: end; }

  .v-bento__panel--1.e-con { --v-bento-rows: 285fr 24px 128fr 24px 422fr; }
  .v-bento__panel--2.e-con { --v-bento-rows: 437fr 24px 113fr 24px 285fr; }

  .v-bento__card--erd-und-tiefbau { grid-column: 1 / 2; grid-row: 1 / 4; }
  .v-bento__card--abbruch { grid-column: 3 / 4; grid-row: 1 / 2; }
  .v-bento__card--strassenbau { grid-column: 3 / 4; grid-row: 3 / 6; }
  .v-bento__card--pflasterarbeiten { grid-column: 1 / 2; grid-row: 5 / 6; }
  .v-bento__card--erschliessung { grid-column: 1 / 2; grid-row: 1 / 2; }
  .v-bento__card--baggerarbeiten { grid-column: 1 / 2; grid-row: 3 / 6; }
  .v-bento__card--garten-und-landschaftsbau { grid-column: 3 / 4; grid-row: 1 / 4; }
  .v-bento__card--ihr-wunschvorhaben { grid-column: 3 / 4; grid-row: 5 / 6; }
}

/* ---- the card ------------------------------------------------------ *
 * The whole tile is the <a>, which is what „The cursor shape will change on hover to indicate that it is a clickable element."
 * asks for: the cursor changes over all of it, and one service is one tab stop.
 */
.v-bento__card.e-con {
  --padding-top: 24px;
  --padding-right: 24px;
  --padding-bottom: 24px;
  --padding-left: 24px;
  --justify-content: flex-end;
  min-height: 430px;
  position: relative;
  overflow: hidden;
  border-radius: var(--valera-radius-sm);
  text-decoration: none;
  /* Reclaim every dimension, not the one you were thinking about: an ancestor
     grid stretches its items, and a card that took its height from the track
     and its min-height from here would be whichever the browser resolved last.
     Both are stated. */
  height: auto;
}

@media (min-width: 894px) {
  .v-bento__card.e-con { min-height: 0; }
}

.v-bento__card.e-con:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/* ---- the photograph ------------------------------------------------ *
 * An <img> and not a container background, for the reason the CTA, the
 * accordion and the mosaic all give: a background emits one url with no
 * srcset, and this block has eight of them.
 *
 * FOUR classes on the cap and every one is needed: Elementor's own
 * `.elementor.elementor .e-con > .elementor-widget { max-width: 100% }` is
 * (0,4,0) and loads after the child theme's sheets. The hero paid for that
 * once and the accordion paid for it again.
 */
.v-bento__card.e-con > .elementor-widget.v-bento__photo.elementor-widget-image {
  position: absolute;
  inset: 0;
  max-width: none;
  width: auto;
  height: auto;
  margin: 0;
}

.v-bento__photo .elementor-widget-container,
.v-bento__photo figure {
  position: absolute;
  inset: 0;
  margin: 0;
}

.v-bento__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * TWO scrims, on the image WIDGET itself rather than on the card or on any
 * wrapper inside it. Two reasons, and the second one cost the mosaic a whole
 * staging round:
 *
 * 1. `.e-con::before` is Elementor's background-overlay pseudo-element and it
 *    arrives with `inset: 0` and `height: max(100% + …, 100%)` already set —
 *    the footer lost a whole artwork layer to exactly that. The widget carries
 *    no such rule, so these two own every dimension they use.
 * 2. `.elementor-widget-container` does not exist while `e_optimized_markup`
 *    is on, which it is in 4.2.2. The widget element itself exists under both
 *    markup modes.
 *
 * `::before` is the REST scrim: the drawn gradient at layer opacity 0.8,
 * folded into the stops (Figma multiplies the layer opacity into an alpha that
 * runs 0 -> 1; CSS interpolates a premultiplied 0 -> 0.8, and the
 * two are the same function of y).
 *
 * `::after` is the HOVER scrim and it is the diff: the same gradient at 1
 * plus a flat #000000 at 0.2 over it. It fades in ON TOP of the rest scrim, so
 * only its opacity animates and neither layer ever repaints.
 *
 * THE ORDER IS THE PSEUDO-ELEMENT ORDER, AND IT IS WHY THESE TWO ARE THIS WAY
 * ROUND. `::before` is the element's first child and `::after` its last, so
 * with both positioned at `z-index: auto` the later one paints on top and no
 * z-index is needed. Writing it the other way and reaching for `z-index: 1`
 * would lift the scrim out of the card's own painting order and over the text
 * and the arrow — both of which live in a `.e-con`, which Elementor positions,
 * so they sit at z-index auto and a 1 anywhere beats them.
 */
.v-bento__photo::before,
.v-bento__photo::after {
  content: '';
  position: absolute;
  inset: 0;
  width: auto;
  height: auto;
  pointer-events: none;
}

.v-bento__photo::before {
  background: linear-gradient(180deg, rgba(102, 102, 102, 0) 0%, rgba(0, 0, 0, 0.8) 100%);
}

.v-bento__photo::after {
  background: linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), linear-gradient(180deg, rgba(102, 102, 102, 0) 0%, rgba(0, 0, 0, 1) 100%);
  opacity: 0;
  transition: opacity var(--valera-motion-state) var(--valera-motion-ease);
}

/* ---- the foot row --------------------------------------------------- *
 * The drawn SPACE_BETWEEN row: text left, arrow right, both on the card's
 * bottom padding line. `flex-end` on the counter axis is the drawn MAX — it
 * reproduces every multi-line row exactly, and the single-line rows the file
 * centres instead differ from it by 3.5px.
 */
.v-bento__foot.e-con {
  --flex-direction: row;
  --justify-content: space-between;
  --align-items: flex-end;
  --row-gap: 0px;
  --column-gap: 24px;
  --flex-wrap: nowrap;
}

/*
 * THE SLOT IS ONE GRID CELL AND BOTH TEXTS ARE IN IT.
 *
 * The hover mock replaces the title with the description rather than adding to
 * it: the drawn row is 88 tall on `211:1973`, not 55 + 10 + 88. So the two
 * texts occupy the same cell, bottom-aligned, and swap opacity. The cell is as
 * tall as the taller of them, which is why the arrow beside it never moves and
 * why nothing in this block reflows on hover (D58).
 */
.v-bento__slot.e-con {
  --display: grid;
  display: grid;
  align-items: end;
  /* A flex item's floor is its content, so a long German title would push the
     arrow off the card without this. Explicit rather than inherited from
     Elementor's own `--width: 100%`, which shrinks by luck. */
  flex: 1 1 auto;
  min-width: 0;
}

.v-bento__slot > .v-bento__title,
.v-bento__slot > .v-bento__text {
  grid-area: 1 / 1;
  margin: 0;
  /* Nothing inside the card is interactive — the card IS the link — so neither
     text may swallow a click or start a selection over the other. */
  pointer-events: none;
}

.v-bento__text {
  opacity: 0;
  transition: opacity var(--valera-motion-state) var(--valera-motion-ease);
}

.v-bento__title {
  transition: opacity var(--valera-motion-state) var(--valera-motion-ease);
}

/*
 * The description's ink. #E9E9E9 binds no kit colour — it is a
 * deliberately dimmer grey for secondary copy on a photograph, drawn on 7 of
 * the 8 — so it is stated rather than bound, and the kit's `text` role carries
 * everything else about the face.
 */
.v-bento__text .elementor-widget-container,
.v-bento__text p {
  color: #E9E9E9;
}

/*
 * The reveal is scoped to a pointer that CAN hover, and that is the design's
 * own decision rather than ours: the mobile artboard draws all eight cards
 * with their title only. Where there is no hover the card stays exactly as
 * drawn, and the description is still in the DOM for assistive technology.
 */
@media (hover: hover) {
  .v-bento__card:hover .v-bento__photo::after,
  .v-bento__card:focus-visible .v-bento__photo::after { opacity: 1; }

  .v-bento__card:hover .v-bento__text,
  .v-bento__card:focus-visible .v-bento__text { opacity: 1; }

  .v-bento__card:hover .v-bento__title,
  .v-bento__card:focus-visible .v-bento__title { opacity: 0; }
}

/*
 * Reduced motion removes the ANIMATION, never the information: the swap still
 * happens on hover, it just arrives at once.
 */
@media (prefers-reduced-motion: reduce) {
  .v-bento__text,
  .v-bento__title,
  .v-bento__photo::after { transition: none; }
}

/* ---- the arrow ------------------------------------------------------ *
 * #FFFFFF at 0.19 over a 4px backdrop blur, on all sixteen drawn
 * arrows, with a 50px radius on a 48px box — the file's own way of saying
 * round. The blur comes from `valera-blur-4`, the token sheet's own utility
 * (71 carriers file-wide), so no backdrop-filter is authored here.
 */
.v-bento__foot.e-con > .elementor-widget.v-bento__arrow {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--valera-radius-pill);
  background-color: rgba(255, 255, 255, 0.19);
  /* The puck sits over the photograph and its two scrims. It must not become a
     backdrop root's child by accident (D56) — nothing between it and the card
     carries a filter, a transform or an opacity, and the two scrims are
     pseudo-elements of a sibling, so it samples them as intended. */
}

.v-bento__arrow .elementor-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  /* The drawn glyph is 24x24 inside the 48px puck. */
  font-size: 24px;
  color: #FAFAFA;
}

.v-bento .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* Generated by build/generate-contact-form.js — do not edit by hand. */

/*
 * ---------------------------------------------------------------------------
 * Block B — the contact form (8 drawn placements on 4 pages, plus /kontakt/).
 *
 * Elementor owns everything constant: the fields, the colours, the type roles,
 * the borders, the button, the aside's 32 / 16 / 12 / 8 / 6. What is here is
 * what is a function of width, plus the four things Pro's form widget has no
 * control for — field padding, the acceptance label's type, the field row gap
 * (its control cannot hold a clamp), and the focus ring.
 *
 * The intro block's own 24 / 16 is NOT here: it is exactly the numbered
 * section header's `.v-sechead__block`, and that atom keeps owning it.
 * ---------------------------------------------------------------------------
 */

/*
 * The band. Direction, wrap and both gaps go through Elementor's own container
 * custom properties rather than through its controls, because the stack below
 * 894 has to be able to override them: a control writes an element rule at
 * (0,3,0), which no media query written here could beat.
 *
 * THREE classes, not two. `.e-con.e-flex` — Elementor's own — already declares
 * `--flex-direction: column`, `--flex-basis: auto`, `--flex-grow: 0` and
 * `--flex-wrap`, and it is (0,2,0). A `.v-form-band.e-con` rule only TIES with
 * it and is decided by which stylesheet the browser read last. `.e-flex` on the
 * end makes it (0,3,0) and there is nothing left to argue about.
 *
 * Padding 80/64 at 1440 and 40/20 at 360, on the site's own gutter ramp.
 * The column gap is the LEFTOVER, not the stored 143: `Frame 58` packs
 * SPACE_BETWEEN, so 1312 - 540 - 708 = 64 is what renders, against 60 stacked.
 *
 * `--gap` is deliberately NOT set: the property Elementor actually writes is
 * `gap: var(--row-gap) var(--column-gap)`, and a `--gap` declaration would be
 * a line nothing reads.
 */
.v-form-band.e-con.e-flex {
  --flex-direction: row;
  --flex-wrap: nowrap;
  --align-items: flex-start;
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --row-gap: 60px;
  --column-gap: clamp(60px, calc(60px + 4 * (100vw - 360px) / 1080), 64px);
}

/*
 * The two columns carry the drawn 540 / 708 as grow FACTORS rather than as two
 * pixel widths, so the ratio holds at every width and reproduces 540 and 708
 * exactly at 1440. Zeroing the basis is what puts them in the grow regime at
 * all: an Elementor container is `width: 100%`, so two side by side would
 * otherwise be distributed by SHRINK, which is weighted by basis and always
 * lands on equal halves. Same mechanism as the CTA card's columns.
 *
 * Through `--flex-grow` / `--flex-basis` rather than the longhands, because
 * Elementor's own `flex: var(--flex-grow) var(--flex-shrink) var(--flex-basis)`
 * is a SHORTHAND: winning the tie against it with longhands depends on file
 * order, while feeding its variables cannot lose to it at all.
 */
.v-form-band > .v-form__aside.e-con { --flex-grow: 540; --flex-basis: 0; }
.v-form-band > .v-form__col.e-con   { --flex-grow: 708; --flex-basis: 0; }

/*
 * The stack, at the SAME width the numbered section header stacks at —
 * imported from build/lib/section-header.js rather than derived again. A page
 * whose header stacks at 894 and whose form stacks at 905 changes shape twice
 * on the way down for no reason a reader could name.
 */
@media (max-width: 893px) {
  .v-form-band.e-con.e-flex { --flex-direction: column; }
  .v-form-band > .v-form__aside.e-con,
  .v-form-band > .v-form__col.e-con { --flex-grow: 0; --flex-basis: auto; }
}

.v-form-band .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/*
 * The two Material glyphs. `line-height: 0` on the wrapper so the inline SVG
 * does not sit on a text baseline it has no text to share, and an explicit
 * colour because `currentColor` inherits — and what it inherited on the
 * rendered page was the kit's body `text` (#333333) on a #171717 band, i.e.
 * an invisible icon. Measured, not predicted.
 */
.v-form__icon {
  line-height: 0;
  color: var(--e-global-color-whitebg, #F6F6F6);
}

.v-form__icon svg { display: block; width: 32px; height: 32px; }

/*
 * The two contact links. The heading widget colours `.elementor-heading-title`,
 * but with a link set the text is inside an <a>, and the kit's Theme Style
 * `.elementor-kit-5 a { color: var(--e-global-color-primary) }` (0,2,0) then
 * wins over inheritance — the address rendered #171717 on #171717. `inherit`
 * hands it back to the widget's own accent, which is what the design draws.
 */
.v-form__link .elementor-heading-title a { color: inherit; }

/*
 * ---------------------------------------------------------------------------
 * The form itself.
 *
 * Selector depth is deliberate on every rule below. Elementor writes the
 * widget's own settings as `.elementor-{doc} .elementor-element-{id} <sel>`,
 * which is (0,3,0) before the selector's own classes, and core's form CSS
 * reaches (0,2,0) with `.elementor-field-group .elementor-field-textual`.
 * Three or four classes is what it takes to be the one declaration in play —
 * the hero paid for that lesson once already.
 * ---------------------------------------------------------------------------
 */

/*
 * The field rhythm: 24 at 1440, 16 at 360. It cannot go through `row_gap`,
 * whose second selector is `margin-bottom: -{{SIZE}}{{UNIT}}` and would
 * render `-clamp(...)`. The control is set to 0 and the wrapper — which core
 * already makes a wrapping flexbox — carries a real `gap` instead.
 */
.v-form .elementor-form .elementor-form-fields-wrapper {
  gap: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px) 0;
  margin: 0;
}

.v-form .elementor-form .elementor-form-fields-wrapper .elementor-field-group {
  margin: 0;
  padding: 0;
}

/*
 * The honeypot's own wrapper. Pro hides the INPUT with an inline
 * `display:none !important` but its `hide_label` handler REPLACES the group's
 * class list with a bare `elementor-field-type-text`, so the div itself is
 * still a flex item: zero high, full-width-wrapping, and worth one whole row
 * gap of empty space between the consent row and the button. Found on the
 * rendered markup, not predicted. It is the only field div without
 * `.elementor-field-group`, which is exactly how to name it.
 */
.v-form .elementor-form-fields-wrapper > .elementor-field-type-text:not(.elementor-field-group) {
  display: none;
}

/*
 * The fields' HEIGHTS, and nothing else about them.
 *
 * Border, radius, padding and type come from the kit's Theme Style, which
 * measured the same `input` component and already says 0/0/1/0 at #949597,
 * radius 0, padding 8/12, Inter 16/1.4/-2%. The four widget controls that
 * would override it default to empty, so they emit nothing and the kit wins —
 * one entity, and the client can still change it in Site Settings.
 *
 * The two heights are the design's own 40 and 121 and are set explicitly
 * rather than left to fall out of `min-height` plus line box plus padding,
 * which is an arithmetic coincidence rather than the number the design gives.
 */
.v-form .elementor-field-group .elementor-field-textual {
  height: 40px;
  min-height: 40px;
}

.v-form .elementor-field-group textarea.elementor-field-textual {
  height: 121px;
  min-height: 121px;
  resize: vertical;
}

/* The one drawn placeholder is #F6F6F6 at full strength; core dims every
   placeholder to 0.6 of the field's colour. */
.v-form .elementor-field-group .elementor-field-textual::placeholder {
  color: var(--e-global-color-whitebg, #F6F6F6);
  opacity: 1;
}

/*
 * Focus. Ours — the file carries 0 of 29 prototype/transition/state fields —
 * and it replaces core's own, which is an INSET 1px black shadow at 10% and is
 * invisible on this ground.
 *
 * The whole form stands on #171717: measured, and true of the fields, the
 * acceptance box AND the submit button, whose ring is drawn in the 2px gap
 * OUTSIDE its yellow face and therefore also lands on the dark. So the form is
 * one dark context and flips the ring's colour once, here. It used to state
 * three rings — accent, accent, paper — which was three answers to a question
 * the header and the footer had already answered.
 *
 * The accent yellow is deliberately not kept: it is this site's BUTTON colour,
 * and a focus ring that looks like a button is a worse signal than one that
 * looks like a focus ring.
 */
.v-form {
  --valera-focus-color: var(--valera-focus-paper);
}

.v-form .elementor-field-group .elementor-field-textual:focus,
.v-form .elementor-field-group .elementor-field-textual:focus-visible,
.v-form .elementor-field-type-acceptance .elementor-acceptance-field:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/* Core's inset shadow, off — it is the thing the ring replaces. */
.v-form .elementor-field-group .elementor-field-textual:focus {
  box-shadow: none;
}

/* Hover: the border comes up to the field text's own colour. The duration is
   the site's ui step — hover, focus, a colour answering the pointer. */
.v-form .elementor-field-group .elementor-field-textual {
  transition: border-color var(--valera-motion-ui) var(--valera-motion-ease), outline-color var(--valera-motion-ui) var(--valera-motion-ease);
}

/*
 * TIER B — the three rules that have to out-specify Elementor's OWN generated
 * CSS rather than only core's.
 *
 * `field_border_color` renders as
 *   .elementor-{doc} .elementor-element.elementor-element-{id}
 *   .elementor-field-group .elementor-field:not(.elementor-select-wrapper)
 * which is (0,6,0), and it sets `border-color` in the NORMAL state — so a
 * hover or invalid rule written at (0,4,0) never fires. Read off the CSS
 * Elementor actually generates for this exact payload, not predicted; the
 * first draft of this block lost both silently.
 *
 * `.v-form.elementor-widget.elementor-widget-form` is the widget's own wrapper spelled with three classes, matching the
 * depth of Elementor's document prefix, which puts these at (0,7,0).
 */
@media (hover: hover) {
  .v-form.elementor-widget.elementor-widget-form .elementor-form .elementor-field-group .elementor-field.elementor-field-textual:hover {
    border-color: var(--e-global-color-whitebg, #F6F6F6);
  }
}

.v-form.elementor-widget.elementor-widget-form .elementor-form .elementor-field-group .elementor-field.elementor-field-textual[aria-invalid="true"] {
  border-color: var(--e-global-color-accent, #F4EA64);
}

/*
 * The consent row. Pro renders it as
 *   .elementor-field-subgroup > .elementor-field-option > input + label
 * and `label_typography`'s selector is `.elementor-field-group > label`,
 * which that label is not — so the drawn 14/1.5 is here rather than in the
 * widget. `align-items: center` reproduces the drawn offset exactly: the
 * 18px box sits at y 1.5 in a 21px row, which is (21 - 18) / 2.
 */
.v-form .elementor-field-type-acceptance .elementor-field-option {
  display: flex;
  align-items: center;
  gap: 8px;
}

.v-form .elementor-field-type-acceptance .elementor-acceptance-field {
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--e-global-color-accent, #F4EA64);
}

/*
 * TIER B. `field_typography`'s selector list ends in
 * `{doc-prefix} .elementor-field-subgroup label`, which is (0,4,1) and sets
 * the consent label to the `text` role's 16px — the design draws 14. A rule
 * at (0,3,1) loses to it silently; this one is (0,5,1). Measured on the
 * rendered page: the label came out 16px before this.
 */
.v-form.elementor-widget.elementor-widget-form .elementor-form .elementor-field-subgroup label {
  margin: 0;
  cursor: pointer;
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: 0;
  color: var(--e-global-color-whitebg, #F6F6F6);
}

.v-form .elementor-field-subgroup label a {
  color: var(--e-global-color-accent, #F4EA64);
  text-underline-offset: 0.2em;
}


/*
 * The submit row. The design puts 36 between the last field and the button on
 * desktop and 28 on mobile — the field row gap plus the same 12 at both
 * widths, so 12 is the whole rule and the rest follows the gap above.
 *
 * Five classes, because the submit group ALSO carries `.elementor-field-group`
 * and the `margin: 0` two rules up is (0,4,0): the first draft wrote this at
 * (0,3,0) and lost to its own neighbour. Measured, not predicted.
 */
.v-form .elementor-form .elementor-form-fields-wrapper .elementor-field-group.e-form__buttons {
  margin-top: 12px;
}

/*
 * The button hugs its label above the stack point (drawn 105 x 48) and fills
 * the column below it (drawn 320 x 48). `button_align: start` gives the first;
 * this gives the second, at the block's one breakpoint rather than Elementor's
 * 767, and at (0,4,0) so nothing argues with it.
 */
@media (max-width: 893px) {
  .v-form .elementor-form .e-form__buttons .elementor-button { flex: 1 1 100%; }
}

/*
 * Hover on the button, TIER B again: `button_background_color` renders as
 * `{doc-prefix} .elementor-button[type="submit"]`, which is (0,5,0) in the
 * normal state. The colour is a mix of two kit colours rather than a
 * fifteenth literal, which is why it is here at all — a colour control holds
 * a value, not an expression. The duration comes from the widget's own
 * `hover_transition_duration`.
 */
.v-form.elementor-widget.elementor-widget-form .elementor-form .elementor-button[type="submit"]:hover {
  background-color: color-mix(in srgb, var(--e-global-color-accent, #F4EA64) 88%, var(--e-global-color-primary, #171717));
}

/* Kept as its own selector rather than folded into style.css's site-wide
   button ring: this one has to out-specify Elementor's generated widget CSS,
   which the (0,2,0) site rule would not. Same two declarations, one source. */
.v-form.elementor-widget.elementor-widget-form .elementor-form .elementor-button[type="submit"]:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/*
 * The messages Elementor's own JS injects. Their colour and type come from the
 * widget's four message controls, not from here — Elementor already gives them
 * `role="alert"` and prints an eicon glyph before the text, so the state is
 * announced and is not carried by colour alone. What is left is one layout
 * fact the controls have no say in: the per-field inline help is a <span>
 * inside a wrapping flex row and would otherwise sit BESIDE the input.
 */
.v-form .elementor-field-group .elementor-form-help-inline {
  flex-basis: 100%;
}

/* Generated by build/generate-introsplit.js — do not edit by hand. */

/*
 * Block 5 — the intro split. Two sections, four drawn placements.
 *
 * The section IS the grid: 12 columns of the 1312 the design draws, the same
 * grid the section header divides and the accordion re-uses. The wide element
 * takes columns 5-12 and the narrow one 1-4; which of the two is the
 * photograph is the only thing that differs between the pages. Nothing here
 * names 445.33 or 866.67 — both fall out of the placement, which is why
 * the header and the split line up at every width and not only at 1312.
 *
 * The drawn row gaps (27 on Stützmauer, 102 on Kanalbau) appear nowhere: both
 * are the leftover of a distributing stack, and gate() proves it by
 * re-deriving them.
 */
.v-isp.e-con {
  --v-isp-cols: repeat(12, 1fr);
  --grid-align-content: start;
  --column-gap: 24px;
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /*
   * `100%` and not `100vw`: this element is the full-width band, so its own
   * containing block IS the page and the percentage resolves against the right
   * box. Above 1312 + 2x64 the padding takes the surplus, so the content
   * stays on the grid while the ground stays full-bleed.
   */
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/*
 * No background. Only one of the four drawn frames carries a fill and it is
 * #F6F6F6 — the page ground the kit already sets (D121). A section that paints
 * the ground it stands on is a declaration that changes nothing and hides the
 * next seam, so this block paints none.
 */

/*
 * Explicit rows, not auto-placement. Sparse auto-placement never moves the
 * cursor backwards, so a photograph placed in columns 1-4 would push a column
 * with a definite 5 onto the next row — the trap the section header's reverse
 * layout and the accordion both hit.
 */
.v-isp > .v-sechead { grid-column: 1 / -1; grid-row: 1; }

.v-isp--photo-left > .v-isp__photo { grid-column: 1 / 5;  grid-row: 2; }
.v-isp--photo-left > .v-isp__col { grid-column: 5 / -1; grid-row: 2; }

.v-isp--photo-right > .v-isp__photo { grid-column: 5 / -1; grid-row: 2; }
.v-isp--photo-right > .v-isp__col { grid-column: 1 / 5;  grid-row: 2; }

/*
 * The counter axis, per placement, read off the drawn row: `Frame 35238`
 * counter-aligns MAX, so the Stützmauer text column sits on the photograph's
 * bottom edge of its 404; `Frame 35234` does not, so Kanalbau's photograph
 * starts at the top of its row.
 *
 * `align-self` on the CHILDREN and not `--align-items` on the section, and
 * that is not a style preference. Elementor's alignment travels through a
 * CUSTOM PROPERTY, and custom properties inherit: an `--align-items: end` on
 * the section would be inherited by `.v-isp__col`, `.v-isp__list` and
 * `.v-isp__item`, where it is the cross axis of a column flex and would
 * shrink every one of them to its content and push it right. Nothing would
 * error. Two classes on the section side because Elementor's own
 * `.e-con > .elementor-widget` rules are (0,2,0) and load later.
 */
.v-isp.v-isp--stuetzmauer > .v-isp__photo,
.v-isp.v-isp--stuetzmauer > .v-isp__col { align-self: end; }
.v-isp.v-isp--kanalbau > .v-isp__photo { align-self: start; }

/*
 * The stack point is 894, imported from the section header rather than chosen
 * again: below it the 8-column body would be narrower than the narrowest
 * body the design itself stacks. The header and the split must break at the
 * SAME width or the two columns stop agreeing halfway down the section.
 *
 * Rows go back to auto here on purpose: on Stützmauer the aside photograph
 * leaves the flow entirely below this point, and an explicit `grid-row: 2` on
 * the column would keep its empty row — and the 32px row gap with it.
 */
@media (max-width: 893px) {
  .v-isp.e-con { --v-isp-cols: 1fr; }
  .v-isp > .v-sechead,
  .v-isp > .v-isp__photo,
  .v-isp > .v-isp__col { grid-column: 1 / -1; grid-row: auto; }
  /*
   * Stützmauer only. Its mobile artboard interleaves the photographs INTO the
   * list — item 1 carries the same bytes at a taller crop, item 2 carries a
   * second photograph that has no desktop counterpart at all — so the aside
   * would be a third copy. A `loading="lazy"` image inside `display: none`
   * never enters the viewport, so this drops the request as well as the
   * picture, in both directions.
   */
  .v-isp--photo-inline > .v-isp__photo { display: none; }
}

@media (min-width: 894px) {
  /* The inline photographs are the mobile artboard's; above the stack point the
     aside is the only one drawn. */
  .v-isp__inline { display: none; }
}

/* ---- the photographs --------------------------------------------- *
 * One <img> per drawn rect, `object-fit: cover`, radius 6 — the site token,
 * and the two Kanalbau rects' 5.5792 is a scaled 6 (D7).
 */
.v-isp__photo { width: 100%; }

.v-isp__photo .elementor-widget-container,
.v-isp__photo figure,
.v-isp__photo img {
  display: block;
  width: 100%;
}

.v-isp__photo img {
  height: 100%;
  object-fit: cover;
  border-radius: var(--valera-radius-sm);
}

/*
 * Stützmauer's aside is 867x404 and never renders below the stack point,
 * so it carries ONE aspect. Its mobile counterpart is item 1's own inline
 * photograph — the same bytes at 320x404 — and the two authored crops
 * share a horizontal centre to within a pixel of the source (gate()), which is
 * what lets one file serve both without an `object-position`.
 */
.v-isp--stuetzmauer > .v-isp__photo img { aspect-ratio: 867 / 404; }

.v-isp--stuetzmauer .v-isp__inline--1 img { aspect-ratio: 320 / 404; }
.v-isp--stuetzmauer .v-isp__inline--2 img { aspect-ratio: 320 / 359.1111; }

/*
 * Kanalbau: 342x254 beside the column and 320x237.6608 above it — the SAME
 * aspect at both anchors, so nothing steps. The width does not fill the four
 * columns (421.33 at 1312) and it is not frozen either: 320 and 342 are
 * two drawn anchors, so it interpolates between them (D51).
 *
 * FIVE classes on the cap, and every one is needed: Elementor's own
 * `.elementor.elementor .e-con > .elementor-widget { max-width: 100% }` is
 * (0,4,0) and loads after the child theme's sheets. The accordion rendered its
 * photograph at the full four-column band once already, with nothing in any
 * stylesheet looking wrong.
 */
.v-isp--kanalbau .v-isp__photo img { aspect-ratio: 342 / 254; }

@media (min-width: 894px) {
  .v-isp--kanalbau.e-con > .elementor-widget.v-isp__photo.elementor-widget-image {
    max-width: clamp(320px, calc(320px + 22 * (100vw - 360px) / 1080), 342px);
  }
}

/* ---- the column, the list and one item --------------------------- *
 * The column stacks list over action at 32 desktop / 24 mobile; on
 * Stützmauer it has one child, so that gap is inert and correct.
 *
 * The list gap is per placement and both anchors are drawn: 24/32 on
 * Stützmauer, 24/16 on Kanalbau. They differ because Stützmauer's mobile
 * list holds item-and-photograph pairs where Kanalbau's holds bare headings —
 * two measurements, not drift, and neither is averaged into the other.
 */
.v-isp__col.e-con {
  --row-gap: clamp(24px, calc(24px + 8 * (100vw - 360px) / 1080), 32px);
  --column-gap: 0px;
}

.v-isp--stuetzmauer .v-isp__list.e-con { --row-gap: clamp(24px, calc(32px + -8 * (100vw - 360px) / 1080), 32px); }
.v-isp--kanalbau .v-isp__list.e-con    { --row-gap: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px); }

/*
 * One item: heading, optional paragraph, optional photograph, all at 16 —
 * the same 16 on all ten drawn items and at both breakpoints, so it is a
 * constant rather than a ramp. The heading carries 8px beneath it, which is
 * drawn on three different levels across the file and measured as one number:
 * with a paragraph under it that makes the drawn 24, and with nothing under
 * it, it is the trailing space Kanalbau's heading-only items are drawn with.
 */
.v-isp__item.e-con {
  --row-gap: 16px;
  --column-gap: 0px;
}

.v-isp__item > .v-isp__item-head { padding-block-end: 8px; }

/*
 * Tracking is 0 on all ten item headings — measured, and the build sheet
 * says so for the whole `secondary` role. It does not RENDER as 0: the kit's
 * zero-tracking rows declare no letter-spacing at all, so they inherit the
 * body's -0.32px. The real fix is one line in the kit; it is already on the
 * launch checklist, and this is the block's own scope in the meantime. It
 * fights nothing — Elementor emits no letter-spacing for this role.
 */
.v-isp__item-head .elementor-heading-title { letter-spacing: 0; }

.v-isp__item .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}


/* ---- the pinned „step scroll" section (D213) ---------------------- *
 *
 * The design draws this ONE section twice — 211:4256 beside the drawn
 * 211:3973, both 1438x808 — and the difference between the two
 * frames is the entire animation:
 *
 *   state 1   the text window is 417x192, counter-aligned MAX in the 404 row,
 *             holding 382 of content -> item 1 only, photograph 1.
 *   state 2   the same window is 417x404 at y 0 -> both items, photograph 2.
 *
 * SO THE MOVEMENT IS ONE NUMBER, and it is derived rather than chosen:
 * 404 - 192 = 212. Push the content down by that and the window's
 * lower edge lands on 192 — which is exactly where item 2 begins, measured off
 * its own y. State 1 therefore shows item 1 whole and nothing of item 2, with
 * no second measurement and nothing rounded.
 *
 * Nothing here animates a height or a position. The window is the full 404 in
 * both states and the CONTENT translates inside it, so the only animated
 * properties are transform and opacity — D58's Safari rule, where a
 * per-frame repaint inside a pinned section is precisely the case WebKit
 * charges for and Blink absorbs.
 *
 * EVERYTHING BELOW IS INERT UNTIL .is-pinned, which only stepscroll.js adds
 * and only when the section actually fits the viewport under the header. With
 * no script, with prefers-reduced-motion: reduce, on a short window, or below
 * the stack point, the section renders exactly as it does today: both items,
 * first photograph, no pin. That is the same contract the hero ships under.
 */
.v-pin.v-pin--stuetzmauer.e-con {
  --v-pin-advance: 30vh;
  /* The zones are flow children, so the container's own gap would space them
     apart and add itself to the pin distance. */
  --row-gap: 0px;
  --column-gap: 0px;
  --padding-block-start: 0px;
  --padding-block-end: 0px;
  --padding-inline-start: 0px;
  --padding-inline-end: 0px;
}

/* Off below the stack point: stacked, a pinned section would hold the page
   still over a column that is now the page's own flow. */
@media (min-width: 894px) {
  .v-pin.v-pin--stuetzmauer.is-pinned { position: relative; }

  .v-pin.v-pin--stuetzmauer.is-pinned > .v-isp {
    position: sticky;
    /*
     * The offset is the SCRIPT's, because it depends on the window: normally
     * the bar's height, and on a short window as low as the content allows so
     * the section's own rows stay on screen. The fallback is the bar, so the
     * declaration is correct before the script runs and if it never does.
     */
    top: var(--v-pin-top, var(--valera-header-h));
  }

  /*
   * THE ZONES ARE THE PIN'S LENGTH. Flow children, on purpose: a sticky element
   * is constrained to its containing block's CONTENT box, and padding is
   * outside it — bought with padding, the pin does not happen at all, silently.
   *
   * visibility: hidden rather than display: none, because an
   * IntersectionObserver needs geometry and hidden boxes have it.
   */
  .v-pin.v-pin--stuetzmauer.is-pinned > .v-pin__zone {
    height: var(--v-pin-advance);
    visibility: hidden;
    pointer-events: none;
    /*
     * THE SOFT CLOSE. Each zone is one step, so its start is the one position
     * a step wants to settle on — Serhii, 2026-08-19: „эффект как
     * примагничивания… как автодоводчик".
     *
     * `proximity` BY DEFAULT, and that is measured rather than preferred:
     * proved on the live page in „Leistungen im Detail" before it shipped, it
     * pulls from about 220px and leaves you alone past 300, which is a
     * soft-closer exactly.
     *
     * `snap: 'mandatory'` is the third mode, added 2026-08-24 for the services
     * deck. It makes the document scroller always resolve to a snap position,
     * so the PAGE lands on a service rather than merely being nudged toward
     * one — which is what „как автодоводчик" asks for. The cost is real and is
     * why it is not the default: while it is in force the reader cannot rest
     * between two services, so a block that wants it must be one the reader is
     * meant to step through rather than read past.
     *
     * The `:has()` scope is what makes it affordable at all — the mode applies
     * only while THIS track is held, never to the rest of the page.
     */
    scroll-snap-align: start;
  }
}


/* The snap TYPE goes on the document scroller, because that is what moves.
   `:has()` keeps it off every page that carries no such track, and scoping it
   to this consumer's own track keeps two pinned blocks from declaring the same
   selector in one assembled sheet — and, just as importantly, keeps the mode
   below off the reader's scroll everywhere except while this block is held. */
@media (min-width: 894px) {
  html:has(.v-pin.v-pin--stuetzmauer.is-pinned) { scroll-snap-type: y proximity; }
}

@media (min-width: 894px) {
  /*
   * WHAT MUST FIT IS THE COLUMN, NOT THE WHOLE SECTION (D338).
   *
   * The numbered section header lives inside this section and is 184 tall at
   * 1440, 203 at 1024 and 260 from 990 down. Demanding it as well is what made
   * this block refuse a 709px window by 11px and every narrower one by more —
   * silently, as a plain two-item column with no error anywhere.
   *
   * The column is the interaction: it is 404px at every pinned width
   * (the photograph beside it is always shorter — 403.8 at 1440 down to 248 at
   * the stack point), and it is the thing that steps. So the pin measures it,
   * and the header above is a bonus the window earns. With room for both the
   * section still sticks under the bar exactly as before; without it the header
   * goes off the top and the column is centred between the bar and the fold.
   *
   * Same shape as the services deck, D336/D337.
   */
  .v-pin.v-pin--stuetzmauer.is-pinned > .v-isp {
    --v-pin-fit: '.v-isp__col';
  }

  .v-pin.v-pin--stuetzmauer.is-pinned > .v-isp > .v-isp__col {
    height: 404px;
    overflow: clip;
    align-self: end;
  }

  /* No will-change. A transition promotes its own layer for the duration and
     gives it back; a standing hint keeps a compositor layer alive under a
     sticky section for the whole page, which is the memory case D58 says
     WebKit charges for and Blink absorbs. */
  .v-pin.v-pin--stuetzmauer.is-pinned .v-isp__list {
    transition: transform var(--valera-motion-media) var(--valera-motion-ease);
  }

  /* State 1 is the DRAWN section: the content pushed down so the fold lands on
     192. Every later step brings it back by one item. */
  .v-pin.v-pin--stuetzmauer.is-pinned[data-step="1"] .v-isp__list {
    transform: translateY(212px);
  }

  /*
   * The photographs share one grid cell - .v-isp__photo already carries it - so
   * they stack without a wrapper and cross-fade on the site's media step, the
   * same animation the hero and the step list use.
   *
   * Only the pin turns the extra ones on, and it turns them on with display
   * rather than opacity alone. That is not tidiness: two images in one cell
   * both at opacity 1 means the LAST one wins, so a section that never pins —
   * no script, reduced motion, a short window, below the stack point — would
   * have shown the second photograph instead of the drawn one. Written the
   * other way first; the gate that says every pinned rule needs .is-pinned is
   * what caught it. display: none also means the browser never fetches them
   * in any of those cases.
   */
  .v-pin.v-pin--stuetzmauer.is-pinned > .v-isp > .v-isp__photo {
    display: block;
    opacity: 0;
    transition: opacity var(--valera-motion-media) var(--valera-motion-ease);
  }

  .v-pin.v-pin--stuetzmauer.is-pinned[data-step="1"] > .v-isp > .v-isp__photo--1 { opacity: 1; }
  .v-pin.v-pin--stuetzmauer.is-pinned[data-step="2"] > .v-isp > .v-isp__photo--2 { opacity: 1; }
}

/*
 * The static state, at every width and with no media query around it: the
 * section shows the ONE photograph the design draws in the page, and the
 * step-scroll extras are not there at all. Below the stack point that is also
 * correct on its own terms — the mobile artboard places its own copies INSIDE
 * the list, so an aside copy would be a second one.
 */

.v-isp--stuetzmauer > .v-isp__photo--2 { display: none; }
/* Generated by build/generate-mosaic.js — do not edit by hand. */

/*
 * Block 1 — the photo-card mosaic. Three sections, six drawn placements.
 *
 * The cluster is the one FIXED thing in this design: `cards on hover` has no
 * auto-layout and its four children carry absolute transforms, identical on all
 * three pages. So it keeps its drawn 866 x 647 and is right-aligned in the
 * content column, which puts its left edge at 1312 - 866 = 446 — the grid's
 * column 5 (445.33) to within the drift the file itself carries (444 on
 * Über uns, 447 on the other two).
 *
 * Below 955 the cluster no longer fits the content box and the mosaic becomes
 * the ONE COLUMN the mobile artboard draws. That width is derived, not chosen:
 * content(V) = V - 2 x clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), and 955 is the first V where it reaches 866.
 */
.v-mos.e-con {
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /* `100%` and not `100vw`: this element IS the full-width band, so its own
     containing block is the page and the percentage resolves against the right
     box (100vw would also count the scrollbar). */
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --align-items: stretch;
}

/* ---- the cluster -------------------------------------------------- *
 * `--width` and not `width`: Elementor's `.e-con { width: var(--width) }`
 * reads the variable, so feeding the variable keeps one declaration in play
 * instead of two fighting.
 *
 * Row gap 0 and the gutters IN the track lists. The mosaic's two columns break
 * at different heights (233 on the right, 385 on the left), so a card that spans
 * the seam has to span the gutter with it; a `row-gap` would be added on top
 * of every span and the 647 would come out 695.
 */
.v-mos__grid.e-con {
  --v-mos-cols: 1fr;
  --v-mos-rows: none;
  --width: min(866px, 100%);
  --column-gap: 0px;
  --row-gap: 24px;
  /* `flex-end` and not `end`: the section is a FLEX column, and the flexbox
     keyword is the one every engine has had since flexbox shipped. */
  align-self: flex-end;
}

@media (min-width: 955px) {
  .v-mos__grid.e-con {
    --v-mos-cols: 421px 24px 421px;
    --v-mos-rows: 233px 24px 128px 24px 238px;
    --row-gap: 0px;
  }
  .v-mos__card--tl { grid-column: 1; grid-row: 1 / 4; }
  .v-mos__card--tr { grid-column: 3; grid-row: 1 / 2; }
  .v-mos__card--bl { grid-column: 1; grid-row: 5 / 6; }
  .v-mos__card--br { grid-column: 3; grid-row: 3 / 6; }
}

/* ---- the card ------------------------------------------------------ *
 * The padding is a property of the SHAPE and the design draws two shapes: the
 * 421-wide mosaic card at 24, and the full-width stacked card at 16. It steps
 * at the layout switch rather than interpolating, because between them there is
 * no third shape to interpolate towards.
 */
.v-mos__card.e-con {
  --padding-top: 16px;
  --padding-right: 16px;
  --padding-bottom: 16px;
  --padding-left: 16px;
  --justify-content: flex-end;
  min-height: 320px;
  position: relative;
  overflow: hidden;
  border-radius: var(--valera-radius-sm);
  /* Reclaim every dimension, not the one you were thinking about: an ancestor
     grid stretches its items, and a card that took its height from the track
     and its min-height from here would be whichever the browser resolved last.
     Both are stated. */
  height: auto;
}

@media (min-width: 955px) {
  .v-mos__card.e-con {
    --padding-top: 24px;
    --padding-right: 24px;
    --padding-bottom: 24px;
    --padding-left: 24px;
    min-height: 0;
  }
}

/* ---- the photograph ------------------------------------------------ *
 * An <img> and not a container background, for the reason the CTA and the
 * accordion both give: a background emits one url with no srcset, so every
 * phone pulls the master — and this block has eleven of them.
 *
 * FOUR classes on the cap and every one is needed: Elementor's own
 * `.elementor.elementor .e-con > .elementor-widget { max-width: 100% }` is
 * (0,4,0) and loads after the child theme's sheets. The hero paid for that
 * once and the accordion paid for it again.
 */
.v-mos__card.e-con > .elementor-widget.v-mos__photo.elementor-widget-image {
  position: absolute;
  inset: 0;
  max-width: none;
  width: auto;
  height: auto;
  margin: 0;
}

.v-mos__photo .elementor-widget-container,
.v-mos__photo figure {
  position: absolute;
  inset: 0;
  margin: 0;
}

.v-mos__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * The scrim, on the image WIDGET itself rather than on the card or on any
 * wrapper inside it. Two reasons, and the second one cost a whole staging
 * round:
 *
 * 1. `.e-con::before` is Elementor's background-overlay pseudo-element and it
 *    arrives with `inset: 0` and `height: max(100% + …, 100%)` already set —
 *    the footer lost a whole artwork layer to exactly that. The widget carries
 *    no such rule, so this one owns every dimension it uses.
 * 2. It was first written as `.${CLASS.photo} .elementor-widget-container::after`
 *    and rendered NOTHING, on all twelve cards, at every width. Elementor
 *    4.2.2 ships `e_optimized_markup` ON, and an optimized image widget has no
 *    `.elementor-widget-container` at all — the <img> is a direct child of
 *    `.elementor-widget`. The rule was valid CSS matching an element that does
 *    not exist, so #F6F6F6 text sat on the bare photograph at 1.05:1. The
 *    widget element itself exists under both markup modes, and is already the
 *    absolutely-positioned box that fills the card, so `::after` on IT is
 *    correct whether or not the experiment is ever switched off.
 *
 * TWO scrims, and which one applies follows the STATE rather than the width —
 * because that is what the design draws. The mobile artboards, where all four
 * descriptions are permanently visible, wash the photograph flat: #000000 at
 * 0.5, on all twelve. The desktop ones, where a description only appears on
 * hover, draw a gradient instead: 11 of the 12 carry the same one, folded
 * here from a 0.8 layer opacity over stops that run alpha 0 -> 1.
 *
 * So the wash is the default and the gradient is scoped to the branch that
 * hides the description — which also settles a measured contrast problem.
 * #F6F6F6 over the gradient scores 3.03:1 to 7.16:1 at the 95th brightest
 * pixel of the band the text fills (12 cards measured against their own
 * photographs; six are below the 4.5:1 that 16px body copy needs). Over the
 * flat wash every one of the twelve clears it, the worst at 4.89:1. Pinning the
 * gradient to the hover branch therefore leaves the shortfall only where the
 * text is transient and the design's own state is being reproduced; the full
 * ledger, and the one-line fix if Gaby wants it applied there too — delete the
 * `(hover: hover)` rule below — are in 02-decisions-mosaic.md (DM9).
 */
.v-mos__photo::after {
  content: '';
  position: absolute;
  inset: 0;
  width: auto;
  height: auto;
  background: rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

@media (min-width: 955px) and (hover: hover) {
  .v-mos__photo::after {
    background: linear-gradient(180deg, rgba(102, 102, 102, 0) 0%, rgba(0, 0, 0, 0.8) 100%);
  }
}

/* ---- the reveal ---------------------------------------------------- *
 * The stage is the card MINUS ITS BOTTOM PADDING, and its top edge is the
 * card's own so a long description can use the full height rather than being
 * cut at the top padding line. It cannot be the card itself: the card clips at
 * its own edge, and `padding (24) - gap (16) = 8px` of the description would
 * stay visible at rest.
 *
 * One transform does the whole movement. `translateY(calc(100% + 16px))` is the
 * reveal's OWN height plus the drawn gap, so its top lands below the stage's
 * clip line (invisible) and the heading — positioned 16px above the reveal and
 * carried with it — lands exactly on the padding line. Removing the transform
 * is the drawn open state. No height is named anywhere and nothing reflows,
 * which is what D58 asks for: Safari pays per frame for what Blink absorbs.
 */
/* No `--padding-*` here: both elements carry Elementor's own `padding` key,
   which lands at (0,3,0) and cannot be reached from a (0,2,0) rule anyway. A
   declaration that can never win is not a safety net, it is a line to explain
   later. Verified on the rig: the container's padding control emits
   `--padding-top: 0px` and the gap controls emit nothing at all unless set. */
.v-mos__stage.e-con,
.v-mos__reveal.e-con {
  --row-gap: 16px;
  --column-gap: 0px;
}

/*
 * The reveal is scoped to `(hover: hover)` and not merely to the width. A
 * pointer that cannot hover cannot reach a hover state, so on a tablet at 1024
 * the whole mechanism would hide two thirds of the copy with no way to get at
 * it. Where there is no hover the block stays in the state the design itself
 * draws for that case: everything visible, bottom-aligned, in flow — which is
 * also the default here, so that branch needs no rules at all. `gate()` proves
 * all twelve cards' full content fits inside its drawn padding box at the
 * drawn size, so it cannot clip.
 */
@media (min-width: 955px) and (hover: hover) {
  .v-mos__stage.e-con {
    position: absolute;
    top: 0;
    right: 24px;
    bottom: 24px;
    left: 24px;
    width: auto;
    height: auto;
    overflow: hidden;
  }

  .v-mos__reveal.e-con {
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    transform: translateY(calc(100% + 16px));
    transition: transform var(--valera-motion-state) var(--valera-motion-ease);
  }

  /*
   * `:focus-within` beside `:hover` costs one selector and covers the case a
   * later step creates rather than the one that exists now: nothing in these
   * cards is focusable today, because no link target exists anywhere in the
   * file (D13). The description is in the DOM either way, so assistive
   * technology reads it whether or not it is on screen.
   */
  .v-mos__card:hover > .v-mos__stage > .v-mos__reveal,
  .v-mos__card:focus-within > .v-mos__stage > .v-mos__reveal {
    transform: none;
  }

  .v-mos__title.elementor-widget {
    position: absolute;
    inset-inline: 0;
    bottom: calc(100% + 16px);
    margin: 0;
  }
}

/*
 * Reduced motion is honoured by removing the ANIMATION, never the information:
 * the reveal still happens on hover, it just arrives at once.
 */
@media (prefers-reduced-motion: reduce) {
  .v-mos__reveal.e-con { transition: none; }
}

/* ---- the two faces -------------------------------------------------- *
 * 24 / Inter/Semi Bold and 16 / Inter/Regular at BOTH breakpoints on all
 * twenty-four texts, which is the kit's `secondary` and `text` exactly. Only
 * the colour is stated here, because `#F6F6F6` on a photograph is not what
 * either role defaults to.
 */
.v-mos__title .elementor-heading-title {
  /* The kit's zero-tracking rows declare no letter-spacing at all, so the
     heading inherits the body role's -0.32px. The real fix is one line in the
     kit and is on the launch checklist; this is the same scoped repair the
     section header atom already carries.

     `/ 100` because Figma stores letterSpacing in PERCENT — `gate()` asserts
     the unit on all 24 texts. The drawn value is 0, where percent and em agree,
     so this divide changes no byte today; without it a heading that ever gains
     tracking would ship at 100x it. The description is NOT repaired: its drawn
     -2% IS the -0.32px it inherits. */
  letter-spacing: 0em;
}

.v-mos .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* Generated by build/generate-numbers.js — do not edit by hand. */

/*
 * The numbers. Four placements, two pages, two shapes.
 *
 * Both sections ARE the 12-column grid the section-header atom already
 * establishes: the header spans all twelve, and the second row is placed on the
 * columns the design draws it on. The header's own nested grid is the same
 * template at the same width, so its columns line up with these without
 * `subgrid` and without a second measurement.
 *
 * Both padding anchors are the file's: 40/20 at 360 and 80/64 at 1440,
 * on all four placements without exception (build sheet §4).
 *
 * Neither section carries a fill, on either breakpoint. That is D121 and it is
 * deliberate: they are meant to show the #F6F6F6 page ground.
 */
.v-numbers.e-con {
  --v-numbers-cols: repeat(12, 1fr);
  --column-gap: 24px;
  --grid-align-content: start;
  --align-items: start;
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/* Startseite: header, then the figures, 32 apart at BOTH anchors. */
.v-numbers--startseite.e-con { --row-gap: 32px; }

/*
 * Über uns: header, then the split, 60 apart at 1440 and 32 at 360 (D51).
 *
 * In the stack that same row gap also separates the card column from the
 * photograph, at 32 — which is what the design draws there too, give or
 * take the 34 it authored on that one frame. Normalised to 32: it is the
 * only 34 in the block, and 32 is what the same frame is drawn at on
 * desktop, what the card column stacks at, and what the section stacks at.
 */
.v-numbers--ueber-uns.e-con { --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px); }

/*
 * Placement. The header spans the grid; the figures take the body's 8 columns;
 * the card column takes the eyebrow's 4 and the photograph the body's 8.
 *
 * Named children and not `> *`: the atom learned that its own (0,2,0) rules
 * beat a universal selector, so a `.v-numbers > *` here would lose to
 * them and the header would be re-placed.
 */
.v-numbers > .v-sechead { grid-column: 1 / -1; }
.v-numbers--startseite > .v-numbers__figures { grid-column: 5 / -1; }
.v-numbers--ueber-uns > .v-numbers__cards { grid-column: 1 / 5; align-self: center; }
.v-numbers--ueber-uns > .v-numbers__photo { grid-column: 5 / -1; }

/*
 * The stack point is 894 and it is the ATOM's, imported rather than re-derived:
 * a second breakpoint here would stack this grid and the header inside it at
 * different widths, which is a seam nobody would look for.
 */
@media (max-width: 893px) {
  .v-numbers.e-con { --v-numbers-cols: 1fr; }
  .v-numbers--startseite > .v-numbers__figures,
  .v-numbers--ueber-uns > .v-numbers__cards,
  .v-numbers--ueber-uns > .v-numbers__photo { grid-column: 1 / -1; }
  .v-numbers--ueber-uns > .v-numbers__cards { align-self: stretch; }
}

/*
 * The four figures: 3 of the body's 8 columns, then 5.
 *
 * NOT two halves. Both drawn rows are 920 wide with the second figure starting
 * at x=335 exactly (`211:1573` and `211:1580`, two independent frames), and
 * 335 is 3 x 87.333333 + 3 x 24 = 334, one pixel off. Half of
 * 920 is 460, which is 125px further right.
 */
.v-numbers__figures.e-con {
  --v-numbers-fig-cols: repeat(8, 1fr);
  --column-gap: 24px;
  --row-gap: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px);
  --grid-align-content: start;
  --align-items: start;
}

.v-numbers__figures > .v-numbers__figure:nth-child(odd) { grid-column: span 3; }
.v-numbers__figures > .v-numbers__figure:nth-child(even) { grid-column: span 5; }

/*
 * The spans are RESET in the stack, not merely ignored. `span 3` inside a
 * one-column grid does not clamp — it creates two implicit columns and lays the
 * four figures out three-wide off the right edge of the page.
 *
 * The query is the ARTBOARD's, not the atom's stack point: see `twoUpFrom`.
 * One column is the design's own answer to a narrow page — it is what the 360
 * artboard draws — and it is used from here down, so every figure keeps the
 * drawn size, the drawn flush-left axis and one line (D313).
 */
@media (max-width: 1439px) {
  .v-numbers__figures.e-con { --v-numbers-fig-cols: 1fr; }
  .v-numbers__figures > .v-numbers__figure { grid-column: auto; }
}

.v-numbers__figure.e-con {
  --row-gap: clamp(0px, calc(0px + 8 * (100vw - 360px) / 1080), 8px);
  --column-gap: 0px;
}

/*
 * ...and the same measured gap INSIDE the counter, because since 2026-08-19 the
 * figure is one widget rather than two.
 *
 * The container gap above used to separate a heading from a heading. Elementor's
 * counter prints the number and the title as siblings of its own and stacks them
 * with flex-direction: column-reverse — which is how the number ends up above
 * the title with title_position: after, and why no CSS reorders anything here.
 * What it does not do is space them: measured 0 on the rendered page against the
 * drawn 8.
 *
 * IT WAS margin-block-end ON THE TITLE, AND THAT IS THE WRONG EDGE. The old
 * reasoning — "in a reversed column the title is the FIRST box, so its bottom
 * edge is the one facing the number" — has the geometry backwards: `column-
 * reverse` puts the first box at the BOTTOM, so the edge facing the number
 * above it is its TOP. The margin was spent below the label, outside the pair,
 * and the rendered gap between value and label stayed 0 while the figure grew
 * 8px taller than it should be. It measured as
 * 34 + 22 + 8 = 64 of content where the .fig draws 56 + 8.
 *
 * `row-gap` on the flex box instead: it is direction-agnostic, so no reasoning
 * about which edge faces what can be got wrong a second time.
 */
.v-numbers--startseite .v-numbers__value .elementor-counter {
  row-gap: clamp(0px, calc(0px + 8 * (100vw - 360px) / 1080), 8px);
}
/*
 * Über uns draws 16 between the same two boxes, not 8 — measured on all
 * six cards at both breakpoints. It was already measured and already emitted,
 * as `--row-gap` on `.v-numbers__card` — a container with ONE child, where it
 * separates nothing. Unscoping the rule above would have given both variants
 * the Startseite's 8 and the drawn 16 would still be nowhere.
 */
.v-numbers--ueber-uns .v-numbers__value .elementor-counter {
  row-gap: 16px;
}

/*
 * THE COUNTER AXIS ON THE STARTSEITE — taking back three declarations the
 * widget makes and the design contradicts.
 *
 * Elementor's counter is built to centre a statistic: `.elementor-counter-title`
 * is a flex row with `justify-content: center`, and inside
 * `.elementor-counter-number-wrapper` the prefix and suffix carry
 * `flex-grow: 1` so they expand equally and the NUMBER lands in the middle of
 * the column. Measured on the rendered page: prefix 69px, number 16px, suffix
 * 198px inside 283 — which is also why „12 Fahrzeuge im Fuhrpark" wrapped, the
 * suffix having 198px of a 303px box.
 *
 * The .fig draws all four flush left: every figure frame carries no
 * `stackCounterAlignItems` (= Figma's MIN) and no padding, and each hugs its own
 * text at 216 / 389 / 306 / 193 wide.
 * Über uns is the opposite and keeps its centring — its cards ARE drawn CENTER,
 * which is why this is scoped to the one variant and not to the block.
 */
.v-numbers--startseite .v-numbers__figures.e-con {
  --padding-top: 0px;
  --padding-right: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
}
.v-numbers--startseite .v-numbers__figure.e-con {
  --padding-top: 0px;
  --padding-right: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
}
.v-numbers--startseite .v-numbers__value .elementor-counter-title,
.v-numbers--startseite .v-numbers__value .elementor-counter-number-wrapper {
  justify-content: flex-start;
  text-align: left;
}
/*
 * The three spans size to their own words instead of sharing the column —
 * flex-grow 0, which is the centring taken back.
 *
 * SHRINK IS LEFT AT 1 ON PURPOSE (D313). It had been `0 0 auto`, and a flex
 * item that cannot shrink cannot wrap: the value ran over the figure beside it
 * rather than taking a second line. The two-up now only runs at the width it
 * was drawn for, so nothing wraps in the shipped copy — this is what a longer
 * edit lands on, and a wrapped line beats an overlap.
 */
.v-numbers--startseite .v-numbers__value .elementor-counter-number-wrapper > * {
  flex: 0 1 auto;
}

/* Über uns: three cards, 32 apart at 1440 and 16 at 360. */
.v-numbers__cards.e-con {
  --row-gap: clamp(16px, calc(16px + 16 * (100vw - 360px) / 1080), 32px);
  --column-gap: 0px;
}

/*
 * The card, and the hairline that is NOT a box.
 *
 * All six carry `strokeWeight: 1` and a #949597 paint, which reads as a
 * bordered card. It is not: `borderStrokeWeightsIndependent` is true and only
 * `borderBottomWeight` is set, on all six. (457 nodes file-wide are drawn that
 * way and 440 carry all four sides, so reading `strokeWeight` alone would have
 * been wrong on the larger half.)
 *
 * The paint's own opacity is 0.4, so `color-mix` against the kit swatch rather
 * than a baked rgba — a client edit to `secondary` still follows. The plain
 * declaration above it is the fallback for anything without `color-mix`.
 */
.v-numbers__card.e-con {
  --row-gap: 16px;
  --column-gap: 0px;
  --padding-top: clamp(16px, calc(16px + 16 * (100vw - 360px) / 1080), 32px);
  --padding-bottom: clamp(16px, calc(16px + 16 * (100vw - 360px) / 1080), 32px);
  --padding-left: 6px;
  --padding-right: 6px;
  --align-items: center;
  text-align: center;
  border-block-end: 1px solid var(--e-global-color-secondary, #949597);
  border-block-end: 1px solid color-mix(in srgb, var(--e-global-color-secondary, #949597) 40%, transparent);
}

/* 160 is drawn, and it is a floor rather than a height: German copy that
   wraps to a third line must grow the card, not spill out of it. Only above the
   stack point — all three mobile cards hug (108 / 128 / 130). */
@media (min-width: 894px) {
  .v-numbers__card.e-con { min-height: 160px; }
}

/*
 * Tracking is 0 on every figure in this block — measured, and the kit says so
 * too. It does not RENDER as 0: the kit's `heading` row declares no
 * letter-spacing at all, so it inherits the body's `text` role and comes out at
 * -0.32px. The atom carries the same rule in its own scope for the same reason;
 * the real fix is one line in the kit and is on the launch checklist.
 */
.v-numbers__value .elementor-counter-number-wrapper {
  letter-spacing: 0;
}

/*
 * The photograph. One node (`211:2692`), drawn 867x550 at 1440 and
 * 320x203 at 360 — the same aspect to three decimals, so one
 * ratio holds across the range and one file covers both.
 *
 * Its drawn radius is 6 on desktop and 2.2145 on mobile, which is a scaled 6 (D7).
 */
.v-numbers__photo { width: 100%; }

.v-numbers__photo .elementor-widget-container,
.v-numbers__photo figure,
.v-numbers__photo img {
  display: block;
  width: 100%;
}

.v-numbers__photo img {
  height: 100%;
  object-fit: cover;
  border-radius: var(--valera-radius-sm);
  aspect-ratio: 867 / 550;
}

/* Generated by build/generate-steps3.js — do not edit by hand. */

/*
 * Block 3 — the homepage process steps: a centred header, then three dots on
 * one dashed rule. ONE placement, `211:1691` / `211:2235`, and no motion:
 * all fifteen frames parked beside the homepage root were enumerated and none
 * is a state of this section.
 *
 * The section is drawn FULL BLEED (side padding 0 at both breakpoints), so the
 * content cap lives on the row inside it (D201) and the rule keeps the whole
 * width the design gives it.
 *
 * THE MOBILE IS A STRIP. The drawn mobile row is 850 wide inside a 360
 * artboard and its rule is 890.009 = 20 + 850 + 20. One mechanism serves both
 * drawings: the rail scrolls, the row is `max-content` with a 100% floor, and
 * a column may grow but may not shrink below its drawn 270. At 1440 that
 * gives (1258 - 2 x 68) / 3 = 374; at 360 it gives 270 and an overflow.
 * The strip begins to scroll below 999px, which is derived from those numbers
 * and is not a breakpoint anything keys on.
 */
.v-ablauf.e-con.e-flex {
  --flex-direction: column;
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --column-gap: 0px;
  --align-items: stretch;
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /* Drawn 0 on both artboards. The band is the artboard here. */
  --padding-left: 0px;
  --padding-right: 0px;
}

/* ---- the header --------------------------------------------------- *
 * Drawn 801 wide and centred at 1440, and 320 wide inside a 20 gutter at
 * 360. That is the site's own band idiom with the drawn 801 as its cap, so
 * the two drawn widths come out exactly and the ramp between them is the one
 * every other section uses.
 *
 * The text is CENTRED on the desktop artboard and LEFT on the mobile one. The
 * switch is derived rather than picked: 884px is the first width at which the
 * drawn 801 measure still fits the content box.
 */
.v-ablauf__head.e-con.e-flex {
  --flex-direction: column;
  --row-gap: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px);
  --column-gap: 0px;
  --align-items: stretch;
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 801px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 801px) / 2));
  text-align: start;
}

@media (min-width: 884px) {
  .v-ablauf__head.e-con.e-flex { text-align: center; }
}

/* ---- the rail: the scroll container -------------------------------- *
 * `overflow-x` and not a widget: an Elementor container has NO overflow
 * control — all 306 of its control names and labels were searched on staging
 * and none matches — and `nested-carousel`, the platform's strip, costs a
 * Swiper dependency for a scroll the browser does natively.
 *
 * `overflow-y: hidden` is stated because a scroll container with one axis on
 * `auto` computes the other to `auto` as well, and an `auto` y-axis on a box
 * whose contents exactly fit is a scrollbar that appears on a rounding error.
 */
.v-ablauf__rail.e-con.e-flex {
  --flex-direction: column;
  --align-items: flex-start;
  --row-gap: 0px;
  --column-gap: 0px;
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
  --width: 100%;
  position: relative;
  overflow-x: auto;
  overflow-y: hidden;
}

/* The scroller is a tab stop (D289), so it needs the site's one ring. */
.v-ablauf__rail:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/* ---- the row, and the rule it carries ------------------------------ *
 * `--width: max(100%, <the strip at its drawn minimum>)` is the whole
 * responsive behaviour: above 999px the 100% wins and the columns share the
 * content box; below it the strip's own width wins, the row is wider than the
 * rail, and the rail scrolls.
 *
 * The second term is spelled out — 3 columns at their drawn 270, both gaps and
 * both paddings — and NOT `max-content`. That was the first version and it is
 * wrong in a way no gate over the stylesheet could see: a flex item with
 * `flex-grow: 1` contributes its own max-content size to the container's
 * max-content size, NOT its `flex-basis`, so the row came out 4602px wide at
 * 1440 — every paragraph on one line. Measured in a real browser, which is the
 * only place that fact shows up.
 *
 * The percentage in the padding is MEANT to resolve against this element's own
 * containing block — the rail, which is the full-bleed band — because that is
 * what centres the 1258 content box. This is the opposite choice from
 * `fluid()`'s `100vw`, and both are deliberate.
 */
.v-ablauf__row.e-con.e-flex {
  --v-ablauf-gap: clamp(20px, calc(20px + 48 * (100vw - 360px) / 1080), 68px);
  --v-ablauf-pad: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1258px) / 2));
  --flex-direction: row;
  --flex-wrap: nowrap;
  --align-items: stretch;
  --row-gap: 0px;
  --column-gap: var(--v-ablauf-gap);
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: var(--v-ablauf-pad);
  --padding-right: var(--v-ablauf-pad);
  --width: max(100%, calc(3 * 270px + 2 * var(--v-ablauf-gap) + 2 * var(--v-ablauf-pad)));
  position: relative;
}

/*
 * The rule. It spans this element's PADDING box, which is the rail's scrollable
 * width — 1440 at the desktop artboard and 890.009 at the mobile one, both
 * drawn, from one declaration.
 *
 * `::after` and never `::before`: Elementor's own `.e-con:before` is the
 * background-overlay pseudo (`content: var(--background-overlay)`,
 * `position: absolute`, `height: max(100% + …, 100%)`), read off
 * frontend.min.css on the server. Nothing in Elementor matches `.e-con:after`.
 * Every property the rule depends on is stated here, because the lesson the
 * footer paid for is that a dimension you do not reclaim is one Elementor may
 * already own.
 *
 * The vertical position is the DOT'S CENTRE, composed rather than measured off
 * the line: 27 (the tagline's line box) + 16 (the marker's gap) + 18 (half the
 * dot) = 61. The drawn line is BELOW that on both artboards — by 2 on the
 * desktop, and by 4.6858 to 7.2522 across the mobile strip, where the line
 * is hand-dragged and FALLS left to right. The dot settles it: the marker is
 * yellow above its horizontal diameter and grey below it, so the diameter is
 * the line the rule continues, and any offset cuts the yellow half.
 *
 * The dash is the drawn `dashPattern: [7, 7]`. `border-style: dashed`
 * cannot express it: the browser picks that dash length, not the author.
 */
.v-ablauf__row.e-con::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-start: calc(61px - 0.5px);
  inset-block-end: auto;
  block-size: 1px;
  background-image: repeating-linear-gradient(
    90deg,
    var(--e-global-color-secondary, #949597) 0 7px,
    transparent 7px 14px);
  border: 0;
  border-radius: 0;
  opacity: 1;
  mix-blend-mode: normal;
  pointer-events: none;
  z-index: 0;
}

/* ---- one step ------------------------------------------------------ *
 * `--flex-grow` / `--flex-shrink` / `--flex-basis` and never `flex:`,
 * because Elementor's own rule is the SHORTHAND on those three variables and
 * feeding them cannot lose to it.
 *
 * `z-index` on the step and on the rule, both stated: the rule is a positioned
 * pseudo-element and the columns are in-flow, so without this the dashes would
 * paint over the dots they are supposed to run behind.
 */
.v-ablauf__step.e-con.e-flex {
  --flex-direction: column;
  --flex-grow: 1;
  --flex-shrink: 0;
  --flex-basis: 270px;
  --align-items: center;
  --row-gap: 48px;
  --column-gap: 0px;
  --width: auto;
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
  position: relative;
  z-index: 1;
  text-align: center;
}

/* ---- the marker, and its dot --------------------------------------- *
 * The dot is this element's `::after` — the drawn „Frame 35186", with no DOM
 * node of its own. Three drawn shapes, one pseudo-element: a 2px transparent
 * border carrying the ring as a `border-box` gradient layer, and the 32 core as
 * a `padding-box` layer over it. `border-top-color` cannot draw this — a
 * circular border splits its colours on 45-degree miters, so the top colour
 * would cover 90 degrees where the design draws 180.
 */
.v-ablauf__marker.e-con.e-flex {
  --flex-direction: column;
  --flex-grow: 0;
  --flex-shrink: 0;
  --flex-basis: auto;
  --align-items: center;
  --row-gap: 16px;
  --column-gap: 0px;
  --width: auto;
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
}

.v-ablauf__marker.e-con::after {
  content: '';
  flex: 0 0 auto;
  inline-size: 36px;
  block-size: 36px;
  border-radius: 50%;
  border: 2px solid transparent;
  background-image:
    linear-gradient(
      var(--e-global-color-primary, #171717),
      var(--e-global-color-primary, #171717)),
    linear-gradient(
      180deg,
      var(--e-global-color-accent, #F4EA64) 0 50%,
      var(--e-global-color-secondary, #949597) 50% 100%);
  background-clip: padding-box, border-box;
  background-origin: border-box;
  background-repeat: no-repeat;
  position: static;
  inset: auto;
  opacity: 1;
  mix-blend-mode: normal;
}

/*
 * The tagline's line box is the first term of the rule's position, so it may
 * not wrap and it may not carry a margin. 27px is 18 x 1.5 from the kit's
 * `tagline` role, and `gate()` reads kit.json to prove that rather than
 * trusting the name.
 */
.v-ablauf__tag.elementor-widget .elementor-heading-title,
.v-ablauf__tag.elementor-widget {
  margin-block: 0;
  white-space: nowrap;
}

/* ---- the body ------------------------------------------------------ *
 * „Content Bottom" (`211:1708` and its five siblings) is a VERTICAL frame with
 * gap 32 holding exactly ONE child, so it renders as its text and nothing
 * else. Not emitted; the paragraph is the body's second child directly.
 */
.v-ablauf__body.e-con.e-flex {
  --flex-direction: column;
  --align-items: stretch;
  --row-gap: 24px;
  --column-gap: 0px;
  --width: 100%;
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
}

/*
 * The theme gives every <p> a bottom margin, and the LAST one in a text-editor
 * has nothing to separate from: it is added on top of the flex gap the design
 * measured. BOTH scopes, and this was measured rather than assumed — the first
 * version scoped it to the body only, and the rendered header came out 220.8
 * against a drawn 206, all 14.4 of it that margin. One rule from
 * lib/elementor.js, never a second copy.
 */
.v-ablauf__head .elementor-widget-text-editor p:last-of-type,
.v-ablauf__body .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* Generated by build/generate-publicprojects.js — do not edit by hand. */

/*
 * Kanalbau „Öffentliche Projekte" (211:4959 / 211:5403), and its parked hover
 * mock 211:5030.
 *
 * The whole interaction is ONE measured number. The mock lays out
 * 309 + 25 + 309 = 643, which is the rest card's own width, so an
 * open card occupies exactly the box a closed one does and NOTHING outside the
 * hovered card moves. The row is 643 + 25 + 643 = 1311, which is the
 * section's own content box (1439 − 64 − 64).
 *
 * Both switches are DERIVED, never typed:
 *   736px  two columns — the content box first holds two drawn mobile cards
 *          (320, node 211:5405) plus the drawn 25 gap
 *   1043px  the hover split — one half first holds the drawn panel text
 *          (277 x 240, node 211:4963) inside the card's 382px of height
 *          with one 24px line reserved: 186 + 32 = 218px per half
 */

/* ---- the section --------------------------------------------------- *
 * The content column is capped at 1312 above 1440 (D197) by flooring the
 * padding ramp, exactly as every other band on the site does.
 */
.v-pub.e-con {
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --display: grid;
  --grid-template-columns: 1fr;
  grid-template-columns: var(--grid-template-columns);
  /*
   * THE ROWS, and leaving them out cost a 446px hole under the two cards.
   *
   * Elementor writes `--e-con-grid-template-rows` at (0,3,0) whether the
   * control is set or not, and its default is TWO rows. The columns above win
   * because this block declares them as a real property here; the rows had no
   * such declaration, so the section laid out 2 columns x 2 rows with both
   * cards in row 1 and row 2 empty — 414px of nothing plus the 32px gap,
   * measured on /leistungen/kanalbau/ at 1440.
   *
   * `auto` and not a count: one row above the stack point where the two cards
   * sit side by side, two below it where they stack, without either number
   * being written down twice.
   */
  grid-template-rows: auto;
  --column-gap: 25px;
  --row-gap: 32px;
}

@media (min-width: 736px) {
  .v-pub.e-con {
    --grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ---- one card, in flow --------------------------------------------- *
 * This is the state the MOBILE artboard draws (211:5404: the photograph,
 * then the description, gap 16), and it is the base state at every width:
 * nothing is hidden, no pointer is assumed, no script runs. The split below is
 * an enhancement on top of it.
 */
.v-pub__card.e-con {
  position: relative;
  --display: flex;
  --flex-direction: column;
  --row-gap: 16px;
  --column-gap: 0px;
}

.v-pub__card.e-con > .elementor-widget.v-pub__photo.elementor-widget-image {
  position: relative;
  width: 100%;
  height: 414px;
  flex: 0 0 auto;
}

/*
 * The photograph is clipped rather than resized, and the radius rides in the
 * clip so it is the SHAPE of the visible window at every moment. Both endpoints
 * are written in full four-value form so the interpolation is component-wise.
 */
.v-pub__photo {
  clip-path: inset(0px 0px 0px 0px round 6px);
  transition: clip-path var(--valera-motion-state) var(--valera-motion-ease);
}

.v-pub__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transition: transform var(--valera-motion-state) var(--valera-motion-ease);
}

/*
 * The scrim, drawn as a second fill on the card at layer opacity 0.8 with the
 * axis matrix [0 1 0], i.e. t = y. It lives on the image widget's
 * ::after so the clip takes it with the photograph.
 */
.v-pub__photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(102, 102, 102, 0) 0%, rgba(0, 0, 0, 0.8) 100%);
  pointer-events: none;
}

/*
 * The heading. The card's padding box is 314 from the top and 16 from the
 * bottom — an 84px band — and the drawn heading is CENTRED in it, which is why
 * one line lands at y 335 = 314 + (84 − 42) / 2 and two lines fill it.
 *
 * `calc(100% - 398px)` is the same expression in BOTH branches: in flow
 * the card is 414 + 16 + the panel tall and this lands 16 above the photograph's
 * bottom edge; split, the card is 414 tall and it resolves to 16.
 */
.v-pub__card.e-con > .elementor-widget.v-pub__title {
  position: absolute;
  inset-inline-start: 16px;
  inset-inline-end: 16px;
  bottom: calc(100% - 398px);
  z-index: 1;
  margin: 0;
  min-height: 84px;
  display: flex;
  align-items: center;
}

.v-pub__title .elementor-heading-title {
  /* The kit's zero-tracking rows declare no letter-spacing at all, so this
     heading would inherit the body role's -0.02em. The drawn value is 0%.
     `/ 100` because Figma stores letterSpacing in PERCENT — gate() asserts the
     unit on all 6 headings. The real fix is one line in the kit and is on
     the launch checklist; this is the same scoped repair the section-header
     atom already carries. */
  letter-spacing: 0em;
}

/* The description is drawn at 0% too, so it needs the same repair — unlike
   the mosaic's, whose drawn -2% IS the -0.32px it inherits. */
.v-pub__text {
  letter-spacing: 0em;
}

/* ---- the split ------------------------------------------------------ *
 * Scoped to `(hover: hover)` and not merely to a width: a pointer that cannot
 * hover cannot reach a hover state, so on a touch laptop at 1043 the mechanism
 * would hide the description with no way to get at it. Where there is no hover
 * the block stays in the state the design itself draws for that case — which is
 * also the base above, so that branch needs no rules and cannot break.
 */
@media (min-width: 1043px) and (hover: hover) {
  .v-pub__card.e-con {
    --display: block;
    height: 414px;
  }

  .v-pub__card.e-con > .elementor-widget.v-pub__photo.elementor-widget-image {
    position: absolute;
    inset: 0;
    width: auto;
    height: auto;
  }

  .v-pub__panel.e-con {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    width: calc(50% - 12.5px);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--valera-motion-state) var(--valera-motion-ease);
  }

  /*
   * `:focus-within` beside `:hover` costs one selector and covers the case a
   * later step creates rather than the one that exists now: nothing in these
   * cards is focusable today, because no link target exists anywhere in the
   * file (D13). The description is in the DOM at every width either way, so
   * assistive technology reads it whether or not it is on screen.
   *
   * calc(50% + 12.5px) is (P + 25) / 2 against the card's own width, so the visible
   * window is (P − 25) / 2 — the drawn half at EVERY width, not only at 1440.
   */
  .v-pub__card.e-con:hover > .v-pub__photo,
  .v-pub__card.e-con:focus-within > .v-pub__photo {
    clip-path: inset(0px calc(50% + 12.5px) 0px 0px round 6px);
  }

  /* Half the width the clip removed, so the same point of the photograph stays
     in the middle of the window. The image's left edge moves at half the speed
     of the clip's right edge, so the window is covered at every intermediate
     moment and no gap can open. */
  .v-pub__card.e-con:hover > .v-pub__photo img,
  .v-pub__card.e-con:focus-within > .v-pub__photo img {
    transform: translateX(calc(-25% - 6.25px));
  }

  /* The heading REWRAPS to the two lines the mock draws (278 x 84,
     node 211:5032) rather than being clipped. Deliberately NOT transitioned:
     transitioning a text box's width reflows it on every frame. */
  .v-pub__card.e-con:hover > .elementor-widget.v-pub__title,
  .v-pub__card.e-con:focus-within > .elementor-widget.v-pub__title {
    inset-inline-end: calc(50% + 28.5px);
  }

  .v-pub__card.e-con:hover > .v-pub__panel,
  .v-pub__card.e-con:focus-within > .v-pub__panel {
    opacity: 1;
    pointer-events: auto;
  }
}

/*
 * Reduced motion removes the ANIMATION, never the information: the split still
 * happens on hover, it just arrives at once.
 */
@media (prefers-reduced-motion: reduce) {
  .v-pub__photo,
  .v-pub__photo img,
  .v-pub__panel.e-con {
    transition: none;
  }
}

.v-pub .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}


/* ================================================================== *
 * Rechtstexte — Impressum, Datenschutz, Cookie-Richtlinie
 *
 * The client's own legal texts, set in the site's type. Every face and
 * colour below is a KIT ROLE, never a pixel: these pages must follow the
 * site, and a legal page is the last place that should own a type scale.
 *
 * The prose column is capped at 720px inside the site's undefined
 * band. That number is ours — the design draws no legal page — and it is
 * the width at which this text sets about 75 characters in Inter 16.
 * ================================================================== */
.v-legal.e-con {
  --align-items: flex-start;
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  /* The TOP is this block's own (D333): the drawing puts 72px of ink between
     the title and the first heading against the 167 that shipped, and half of
     that saving is here. The BOTTOM stays the site's common run — nothing in
     the drawing asks to move it, and it meets the footer like every other
     page. Written out rather than taken from bandEnvelopeCss(), which emits
     one value for both edges: two --padding-top declarations on one selector
     would leave a gate reading the first while the second wins. */
  --padding-top: clamp(28px, calc(28px + 4 * (100vw - 360px) / 1080), 32px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/* FOUR CLASSES ON PURPOSE. Elementor's own rule capping a widget inside a
   container — .elementor.elementor .e-con > .elementor-widget { max-width:
   100% } — is (0,4,0). A one-class cap loses to it silently, and did: the first
   version of this rule was a bare .v-legal__prose and the column rendered 1312
   wide on the live page with every gate green. Equal specificity plus a later
   load is what wins, the same shape generate-hero.js uses for its two content
   caps. */
.v-legal.e-con > .elementor-widget.v-legal__prose {
  width: 100%;
  max-width: 780px;
}

/* The rhythm. A legal text is almost entirely headings and paragraphs, so
   the space between them IS the design. Ratios of the body line-height
   rather than round numbers, so the block breathes with the type. */
.v-legal__prose h2 {
  font-family: var(--e-global-typography-subhead-font-family), sans-serif;
  font-size: var(--e-global-typography-subhead-font-size);
  font-weight: var(--e-global-typography-subhead-font-weight);
  line-height: var(--e-global-typography-subhead-line-height);
  color: var(--e-global-color-primary);
  margin-block: 2.5em 0.6em;
}
.v-legal__prose h3 {
  font-family: var(--e-global-typography-colhead-font-family), sans-serif;
  font-size: var(--e-global-typography-colhead-font-size);
  font-weight: var(--e-global-typography-colhead-font-weight);
  line-height: var(--e-global-typography-colhead-line-height);
  color: var(--e-global-color-primary);
  margin-block: 1.8em 0.5em;
}
.v-legal__prose h4 {
  font-family: var(--e-global-typography-lead-font-family), sans-serif;
  font-size: var(--e-global-typography-lead-font-size);
  font-weight: var(--e-global-typography-lead-font-weight);
  line-height: var(--e-global-typography-lead-line-height);
  color: var(--e-global-color-primary);
  margin-block: 1.4em 0.4em;
}
/* The first heading must not push the section's own padding further down. */
.v-legal__prose > :first-child { margin-block-start: 0; }

.v-legal__prose p { margin-block: 0 1em; }
.v-legal__prose ul,
.v-legal__prose ol { margin-block: 0 1em; padding-inline-start: 1.5em; }
.v-legal__prose li { margin-block-end: 0.35em; }
.v-legal__prose .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* A LINK MUST LOOK LIKE A LINK. The kit gives links the ink colour, which on
   this site is right everywhere else — links sit in buttons and navigation
   and are recognisable by their place. In a wall of legal prose there is no
   place to recognise, and an unmarked link is invisible: measured on the
   live page, the Impressum's link to gesetze-im-internet.de rendered
   rgb(23,23,23) with no underline, identical to the sentence around it. */
.v-legal__prose a {
  color: var(--e-global-color-primary);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-thickness: from-font;
  transition: color var(--valera-motion-ui) var(--valera-motion-ease);
}
.v-legal__prose a:hover { color: var(--e-global-color-text); }
.v-legal__prose a:focus-visible {
    outline: var(--valera-focus-width) solid var(--valera-focus-color);
    outline-offset: var(--valera-focus-offset);}

/* Complianz renders the cookie policy itself, so that markup is the plugin's
   and so is its type scale — measured on the live page it set h2 at 22px and
   body at 14px against this sheet's 28 and 16. Two legal pages in the site's
   type and a third in the plugin's is the disunity that gets noticed first, so
   the document is brought onto the same roles. Only the faces: the plugin keeps
   owning the content, and it keeps regenerating it.

   THE SELECTOR CARRIES AN ID, and that is not reaching further than necessary.
   Complianz styles this document as #cmplz-document h2 — (1,0,1) — so a
   class-only rule loses however many classes it has. Measured: a (0,3,1)
   version of these rules matched the element and lost, and the page kept the
   plugin's 22/14. The wrapper carries both the id and a class of the same name;
   this reaches exactly as far as the rule it is answering. */
.v-legal--cookies .v-legal__prose #cmplz-document { max-width: none; }
.v-legal--cookies .v-legal__prose #cmplz-document h2 {
  font-size: var(--e-global-typography-subhead-font-size);
  line-height: var(--e-global-typography-subhead-line-height);
  margin-block: 2.5em 0.6em;
}
.v-legal--cookies .v-legal__prose #cmplz-document h3 {
  font-size: var(--e-global-typography-colhead-font-size);
  line-height: var(--e-global-typography-colhead-line-height);
  margin-block: 1.8em 0.5em;
}
/*
 * THE COOKIE TABLE ON A PHONE. Complianz lays each cookie out as a two-track
 * grid and sizes the value track by its CONTENT, so a long value makes the
 * track wider than the phone: measured at 414, one instance computed
 * 100px 317.047px inside a 370px column and its cells reached x=441. The
 * page cannot scroll sideways to reveal them - overflow-x: clip on html and
 * body is deliberate — so the right 27px of every row on that card is simply
 * cut off. Found 2026-08-26, in a screenshot, after the geometry probe flagged
 * 64 cells past the viewport.
 *
 * No number of the plugin's is copied here: the label track keeps sizing to its
 * own content and only the value track is told it may shrink. min-width: 0
 * is the other half - a grid item's automatic minimum is its content, which is
 * what stops 1fr from shrinking at all.
 */
.v-legal--cookies .v-legal__prose #cmplz-document .cookies-per-purpose {
  grid-template-columns: auto minmax(0, 1fr);
}
.v-legal--cookies .v-legal__prose #cmplz-document .cookies-per-purpose > * {
  min-width: 0;
  overflow-wrap: anywhere;
}
.v-legal--cookies .v-legal__prose #cmplz-document p,
.v-legal--cookies .v-legal__prose #cmplz-document li,
.v-legal--cookies .v-legal__prose #cmplz-document td {
  font-size: var(--e-global-typography-text-font-size);
  line-height: var(--e-global-typography-text-line-height);
}
/* ================================================================== *
 * D333 — the layout the agency's designer drew for these pages.
 *
 * Two artboards came back on 2026-08-26 as 1440-wide exports. Against
 * what shipped, they change four things and nothing else: the title is
 * left-aligned instead of centred, it is the service role instead of
 * the h1's 56, the air moves from BELOW the title to ABOVE it, and the
 * prose column sits at the band's left edge instead of centred.
 *
 * Scoped by page key, so /leistungen/ — which shares .v-hero--text
 * and is not part of this — keeps its centred hero untouched. This
 * block is emitted after generate-hero.js's in the assembled sheet,
 * so equal specificity plus a later load is what wins; both are (0,2,0).
 *
 * ONE DEPARTURE FROM THE DRAWING, deliberate. The Impressum artboard
 * indents its body 16px from its own title while the Datenschutz one
 * sets both flush; the two drawings disagree about the same element.
 * Both pages ship flush, because shipping the difference would put two
 * legal pages on one site whose text starts at different x.
 * ================================================================== */
.v-hero--impressum.e-con,
.v-hero--datenschutz.e-con,
.v-hero--cookies.e-con {
  --padding-top: calc(var(--valera-header-h, 72px) + clamp(40px, calc(40px + 80 * (100vw - 360px) / 1080), 120px));
  --padding-bottom: clamp(28px, calc(28px + 4 * (100vw - 360px) / 1080), 32px);
  text-align: left;
}

.v-hero--impressum .v-hero__cols.e-con,
.v-hero--impressum .v-hero__col.e-con,
.v-hero--datenschutz .v-hero__cols.e-con,
.v-hero--datenschutz .v-hero__col.e-con,
.v-hero--cookies .v-hero__cols.e-con,
.v-hero--cookies .v-hero__col.e-con {
  /* BOTH spellings, and both are needed. Elementor's container rule takes its
     alignment from the custom property, so that is what the platform honours —
     but generate-hero.js's text hero writes the REAL property at (0,2,0), and a
     custom property cannot beat a real one. Measured with only the variable
     set: the variable resolved to flex-start on both containers while the real
     property still resolved to centre and the title stayed at x=593. */
  --align-items: flex-start;
  align-items: flex-start;
}

/* The cap the text hero puts on its heading is for a centred measure and
   has nothing to do with a left-aligned page title. */
.v-hero--impressum .v-hero__cols .elementor-widget.v-hero__heading,
.v-hero--datenschutz .v-hero__cols .elementor-widget.v-hero__heading,
.v-hero--cookies .v-hero__cols .elementor-widget.v-hero__heading {
  max-width: none;
}

/* DESKTOP ONLY, and the boundary is the site's own stack point.
   The drawing is a 1440 export and says nothing about a phone. Below the stack
   point the kit already steps the page title down to 36 — measured on the live
   pages at 893 and every width under it — and applying the drawn 42 there would
   make the title BIGGER on a phone than it is today, which nobody asked for. */
@media (min-width: 894px) {
  .v-hero--impressum .v-hero__cols .elementor-widget.v-hero__heading .elementor-heading-title,
  .v-hero--datenschutz .v-hero__cols .elementor-widget.v-hero__heading .elementor-heading-title,
  .v-hero--cookies .v-hero__cols .elementor-widget.v-hero__heading .elementor-heading-title {
    font-family: var(--e-global-typography-service-font-family), sans-serif;
    font-size: var(--e-global-typography-service-font-size);
    font-weight: var(--e-global-typography-service-font-weight);
    line-height: var(--e-global-typography-service-line-height);
  }
}

/* ================================================================== *
 * D344 — the drawing's TWO columns.
 *
 * The frame Gaby showed on 2026-08-24 (node 57-2134) puts the page's
 * name in a narrow left column and the heading with the whole text in
 * a wide right one. The two 1440 exports we were sent are a different,
 * single-column rendering of the same node, and D333 followed those.
 *
 * The split is not a new one: read off the recording the right column
 * begins where COLUMN 5 of the site's own 12-column grid begins, which
 * is what the numbered section header on every other page uses. So this
 * emits that grid rather than a geometry of its own, and the numbers
 * come from lib/section-header.js — 12 columns, gutter
 * 24, eyebrow across 4.
 *
 * DESKTOP ONLY. Below the site's stack point the eyebrow and the
 * heading go back to one column, exactly as the section header does.
 *
 * display: grid is a REAL property here, not --display. Elementor
 * writes --display: flex per element and its container rule reads the
 * variable, so a sheet that asks for a grid through the variable loses
 * silently and the tracks have nothing to lay out (D307).
 * ================================================================== */
/*
 * BELOW THE STACK POINT THE LABEL IS GONE. Serhii, 2026-08-26: „на мобильном,
 * когда одна колонка, Datenschutzerklärung вообще не нужно показывать."
 *
 * He is right, and it is not only taste: stacked, the label is a second line of
 * the same words directly under the page title, which reads as a mistake. It
 * exists to label a column, and below this width there is no column to label.
 *
 * display: none and not hidden-mobile: Elementor's class resolves against
 * the kit's mobile breakpoint, which is 893 and NOT the section stack point
 * (D311 — one moved constant, four live defects). This has to follow the point
 * at which the two columns become one, which is the atom's.
 */
@media (max-width: 893px) {
  .v-legal.e-con > .elementor-widget.v-legal__label { display: none; }
}

@media (min-width: 894px) {
  .v-hero--impressum .v-hero__cols .v-hero__col.e-con,
  .v-hero--datenschutz .v-hero__cols .v-hero__col.e-con,
  .v-hero--cookies .v-hero__cols .v-hero__col.e-con {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: 24px;
    row-gap: 0;
    align-items: start;
  }
  .v-hero--impressum .v-hero__cols .v-hero__col > .elementor-widget.v-hero__heading,
  .v-hero--datenschutz .v-hero__cols .v-hero__col > .elementor-widget.v-hero__heading,
  .v-hero--cookies .v-hero__cols .v-hero__col > .elementor-widget.v-hero__heading {
    grid-column: 5 / -1;
  }

  /* The text section carries the SAME grid, and both of the drawing's columns:
     the page's name on the left, the prose on the right. */
  .v-legal.e-con {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: 24px;
    align-items: start;
  }
  .v-legal.e-con > .elementor-widget.v-legal__label {
    grid-column: 1 / span 4;
    grid-row: 1;
    max-width: none;
    /*
     * AND IT STAYS IN VIEW WHILE THE TEXT SCROLLS — Serhii, 2026-08-26:
     * „слева надпись должна быть fixed и всегда видна при скроле".
     *
     * Sticky, not fixed. Fixed would take the label out of the flow and keep it
     * on screen over the footer and everything after it; sticky holds it exactly
     * as long as there is text beside it, which is the whole of what it labels.
     * The offset is the sticky bar plus the same air the site's anchor scrolling
     * already leaves, so the label comes to rest where an anchored heading does.
     *
     * This is also why the label is not in the hero, where D344 first put it: a
     * sticky element is held by its own container, and the hero's ends a line
     * below the title. It unstuck immediately and the page scrolled past it.
     */
    position: sticky;
    top: calc(var(--valera-header-h, 72px) + 16px);
    /*
     * AND IT STARTS LEVEL WITH THE TITLE — Serhii, 2026-08-26: „а почему она не
     * в один ряд начинаются".
     *
     * The title is in the hero, a different section, so the distance cannot be
     * inherited and is written instead. Every term is generated: the title's own
     * line box from the kit role that sets it (service, 54.6px),
     * and the two paddings that stand between the sections. Measured on the live
     * page before this existed, the label sat exactly 54.6 + 32 + 32 = 119px
     * below the title at 1440 — which is the sum below, and is why it is a sum
     * and not a number.
     *
     * Only the label moves. Its neighbour is a separate grid cell in the same
     * row, and with align-items: start a negative margin here lifts this cell
     * alone; the prose does not follow it up.
     */
    margin-block-start: calc(-1 * (54.6px + clamp(28px, calc(28px + 4 * (100vw - 360px) / 1080), 32px) + clamp(28px, calc(28px + 4 * (100vw - 360px) / 1080), 32px)));
  }
  .v-legal.e-con > .elementor-widget.v-legal__prose {
    grid-column: 5 / -1;
    grid-row: 1;
  }
}


/* Generated by build/generate-servicecards.js — do not edit by hand. */

/*
 * Block 4 — „Weitere Leistungen", two sections, four drawn artboards.
 *
 * The section is the band: full-bleed ground, content on the 1312 grid, padding
 * 80/64 at 1440 and 40/20 at 360. Above 1440 the padding takes the
 * surplus so the ground stays full-bleed while the content stays on the grid.
 */
.v-weitere.e-con {
  --column-gap: 0px;
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/*
 * The header is the atom's NARROW variant, drawn 600 wide and centred in the
 * 1312 row on both placements — (1312 - 600) / 2 = 356, which is the x the
 * file draws. A max-width and not a width: at 360 the column is 320 and the
 * cap is inert, which is exactly what the mobile artboard draws. One number,
 * both drawn anchors.
 */
.v-weitere > .v-sechead.e-con {
  max-width: 600px;
  margin-inline: auto;
  width: 100%;
}

/*
 * The row.
 *
 * The design draws a card at 421.333 (four columns of the 12-column grid) and at
 * 320 (mobile) and nothing between, so the rule is that range held: the card
 * is never narrower than 320 and never wider than 421.333, and the column count is
 * whatever keeps it there. Both thresholds below are SOLVED from the padding
 * curve above — at 1110 a three-column card is 320.3 and at 1109 it is
 * 319.99; at 735 a two-column card is 320.22 and at 734 it is 319.76.
 *
 * Only the THREE-column band carries a cap, and it is 1312 — the content
 * width the design draws, the same number every section on the page now stops
 * at (D197). The two narrower bands fill their column instead of centring at
 * the drawn maximum (D198, replacing D166's second half). D166 had capped all
 * three at N x 421.333 + (N-1) x 24 and named the reverse as the one
 * change Gaby could ask for; Serhii asked for it on 2026-08-19, having seen the
 * cap bind between ~460 and 734 and again between ~890 and 1109, where the card
 * stopped growing and sat centred with wide empty margins while the block above
 * it filled the width.
 *
 * The thresholds do not move: they were SOLVED from the padding curve on the
 * FILLED model in the first place (at 735 a two-column card is 320.22, at 734 it
 * is 319.76), so the card still crosses the drawn 320 exactly where the
 * column count changes. What the cap did was hold the card BELOW that model in
 * the middle of each band, which is the defect and not the rule.
 *
 * The cost is named rather than hidden: the master is cut at 2 x 421, so a card
 * wider than 421.333 is served below 2x. The widest state is the one-column band
 * at its top edge. On the launch checklist as a media re-cut, not silently
 * accepted.
 *
 * The authored `stackSpacing` on the drawn row is 66 and renders nowhere: the
 * frame is FIXED at 1312 with SPACE_EVENLY packing, so the rendered gap is
 * 24.5 — the grid's own gutter. A stored value is not a rendered value.
 */
.v-weitere__row.e-con {
  --v-weitere-cols: repeat(3, minmax(0, 1fr));
  --column-gap: 24px;
  --row-gap: 24px;
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
  max-width: 1312px;
  margin-inline: auto;
  width: 100%;
}

@media (max-width: 1109px) {
  .v-weitere__row.e-con {
    --v-weitere-cols: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 734px) {
  .v-weitere__row.e-con {
    --v-weitere-cols: minmax(0, 1fr);
  }
}

/* ---- the card ----------------------------------------------------- *
 * Everything the widget has a control for is in the payload, at (0,3,0): the
 * photograph's width, the 24/16 gap under it, the 16 under the heading, both
 * type roles, both colours, the tag and the alignment. What is left is the
 * photograph's shape, which no control expresses.
 *
 * The drawn ratio is 421 / 414 in the row states and 320 / 354 in the stack — the
 * two artboards' own boxes, both exact at their own width. The third card's
 * mobile frame is drawn 320x414 on BOTH placements, which is the DESKTOP
 * height left behind; it normalises to its two siblings.
 */
.v-weitere__card .elementor-image-box-img img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 421 / 414;
  object-fit: cover;
  border-radius: var(--valera-radius-sm);
}

@media (max-width: 734px) {
  .v-weitere__card .elementor-image-box-img img { aspect-ratio: 320 / 354; }
}

/*
 * Two repairs, both MEASURED on a rendered page (rig post 232) and neither
 * visible in any payload. Together they were 13.41px of extra air under every
 * photograph — the design draws 24 and the browser rendered 37.41.
 *
 * 1. `widget-image-box.min.css` sets `.elementor-widget-image-box
 *    .elementor-image-box-img { display: inline-block }` — so the <figure> sits
 *    on a text baseline and the line box adds its descender space, 5.41px at the
 *    inherited 16/1.4. That rule is (0,2,0) and its sheet is enqueued when the
 *    widget renders, i.e. AFTER the child theme's, so a (0,2,0) rule here would
 *    lose the tie. Hence the widget's own class in the selector: (0,3,0).
 *
 * 2. Hello Elementor's `reset.css` gives every h1-h6 `margin-block: 0.5rem 1rem`
 *    and the widget's `title_bottom_space` control only writes the BOTTOM half.
 *    The 8px top survives. (The bottom half is the same 16px the design draws,
 *    which is exactly the kind of coincidence that hides a missing control.)
 */
.v-weitere__card.elementor-widget-image-box .elementor-image-box-img {
  display: block;
}

.v-weitere__card .elementor-image-box-title {
  margin-top: 0;
}

/*
 * Tracking is 0 on all twelve card headings and all twelve card texts —
 * measured, and the kit's `cardhead` and `cardbody` rows say so too. It does
 * not RENDER as 0: those two rows declare no letter-spacing at all, so both
 * inherit the body's -0.02em. The real fix is one line in the kit, which is on
 * the launch checklist and would put every card on the site right at once; this
 * is the block's own scope until then. It fights nothing — Elementor emits no
 * letter-spacing declaration for either role.
 */
.v-weitere__card .elementor-image-box-title,
.v-weitere__card .elementor-image-box-description {
  letter-spacing: 0;
}

/*
 * Reserve TWO lines for the heading, so the three paragraphs start level.
 *
 * „Stützmauer und Hangabfangung" wraps onto a second line where „Kanalbau" and
 * „Hausanschlüsse" do not, and the heading's height is its content — so card 1's
 * paragraph began 41.59px below the other two. Gaby asked for the space to be
 * reserved (2026-08-25, „vielleicht kriegen wir das hier auf eine Höhe … dass
 * das auf einer Höhe dann erscheint"). The design does not ask for it: it draws
 * card 1's body 100px down against 58px on cards 2 and 3 (211:1600 / 211:1607).
 * This is her request, not build-as-drawn.
 *
 * The value is the design's own: 2 x the drawn line-height, in em units, so
 * it is right at both the 32px desktop and the 24px mobile type size without a
 * second query. Only in the THREE-column band — below 1110 the cards are
 * two-up or stacked, where a reserved blank line would be dead space under a
 * heading that did not wrap. Measured at 1210, 1109, 1054, 1024, 894, 893, 820,
 * 799, 768, 735, 734, 414 and 360: every number identical to the baseline.
 */
@media (min-width: 1110px) {
  .v-weitere__card .elementor-image-box-title {
    min-height: 2.6em;
  }
}

/* Generated by build/generate-servicesdetail.js — do not edit by hand. */

/*
 * Block 6 — „Unsere Leistungen im Detail" (`211:3262` / `211:3721`), one
 * placement, on /leistungen/.
 *
 * MOBILE FIRST, because the mobile artboard is the one with the unusual
 * control and the desktop is the override. Below 1061 the section is what
 * `211:3721` draws: the header, then a horizontal scroll strip of 8 cards
 * 320 wide at gap 14, running past the right edge. Above it the section is
 * what `211:3262` draws: a 532 header column and a 669 entry column, gap 111,
 * which is 532 + 111 + 669 = 1312 — the content column, exactly.
 *
 * 1061 IS DERIVED, NOT CHOSEN. The header keeps its drawn 532 (the atom's own
 * `narrowBody`, and the node it cites is this column); the entry column is the
 * elastic one, because the design draws it at 669 AND at 320. The row holds
 * while the content box can carry 532 + 111 + 320 = 963, and 1061 is the first
 * viewport where content(V) reaches it.
 */

/* ---- the band ------------------------------------------------------ *
 * Side padding 0 at every width, and that is measured rather than chosen: the
 * mobile section `211:3721` carries padding 0 on both sides while every other
 * mobile section on this page carries 20. The 20 was moved onto the header so
 * the strip could reach the edge, and this rule reproduces that.
 */
.v-detail.e-con {
  --flex-direction: column;
  --row-gap: 32px;
  --column-gap: 0px;
  --align-items: stretch;
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-left: 0px;
  --padding-right: 0px;
}

/* ---- the header column --------------------------------------------- *
 * A wrapper around `.v-sechead`, not the atom itself: the atom sets
 * `padding: 0` in its own Elementor settings, which lands at (0,3,0) and beats
 * anything a stylesheet says. So the padding lives one level out, where
 * Elementor emits nothing.
 */
.v-detail__head.e-con {
  --flex-direction: column;
  --row-gap: 0px;
  --column-gap: 0px;
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/* ---- the rail: the scroll container --------------------------------- *
 * `overflow-x` and not a widget. An Elementor container has no overflow
 * control — all 306 of its control names were read off the running site — and
 * `nested-carousel`, the platform's strip, declares `get_script_depends() =>
 * ['swiper']` and cannot also be the desktop column.
 *
 * `overflow-y: hidden` is stated because a scroll container with one axis on
 * `auto` computes the other to `auto` as well, and an `auto` y-axis on a box
 * whose contents exactly fit is a scrollbar that appears on a rounding error.
 */
.v-detail__rail.e-con {
  --flex-direction: column;
  --row-gap: 0px;
  --column-gap: 0px;
  --width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
}

/* The strip is a tab stop (D245), so it takes the site's one ring. */
.v-detail__rail:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/* ---- the row -------------------------------------------------------- *
 * `--width: max(100%, <the strip at its drawn card width>)` is the whole
 * behaviour: where the 8 cards fit, 100% wins and the row is the rail; where
 * they do not, the strip's own width wins and the rail scrolls.
 *
 * The second term is spelled out — 8 cards at their drawn 320, 7 gaps and
 * both paddings — and NOT `max-content`: a flex item that may grow contributes
 * its own max-content size to the container's, so `max-content` on a row of
 * paragraphs resolves to every paragraph on one line.
 */
.v-detail__row.e-con {
  --flex-direction: row;
  --flex-wrap: nowrap;
  --align-items: stretch;
  --row-gap: 0px;
  --column-gap: 14px;
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --width: max(100%, calc(8 * 320px + 7 * 14px + 2 * max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2))));
}

/* ---- one entry ------------------------------------------------------ *
 * Elementor's `flex` is a shorthand on three custom properties; a column is
 * sized through `--flex-grow` / `--flex-shrink` / `--flex-basis`, never
 * through `flex:`, because feeding its variables cannot lose to it.
 */
.v-detail__entry.e-con {
  --flex-direction: column;
  --row-gap: 24px;
  --column-gap: 0px;
  --flex-grow: 0;
  --flex-shrink: 0;
  --flex-basis: 320px;
}

.v-detail__body.e-con {
  --flex-direction: column;
  --row-gap: 16px;
  --column-gap: 0px;
}

/* ---- the photograph ------------------------------------------------- *
 * 414 is drawn at BOTH breakpoints — 669x414 and 320x414 — so the height is
 * constant and only the width moves. One `object-fit: cover` reproduces both
 * drawn boxes and everything between them, with no media query and no
 * aspect-ratio (D244). The 6px radius is the kit's `image_border_radius`
 * and is deliberately NOT restated here.
 */
.v-detail__photo img {
  display: block;
  width: 100%;
  height: 414px;
  object-fit: cover;
}

/* The theme gives every <p> a bottom margin; inside one text-editor the gap
   between the two drawn paragraphs is the design's 16, and the last one has
   nothing to separate from. */
.v-detail__text p {
  margin-block-end: 16px;
}
.v-detail .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* ---- the entry heading follows the CARD, not Elementor's breakpoint - *
 * D247. The design draws this heading at 32 in the 669 column and at 24 in the
 * 320 card. The kit's `cardhead` role carries both, but its 24 is bound to
 * Elementor's own mobile step (max-width 767, measured on the running site) —
 * while THIS block's card is 320 wide from 1060 down. So between 768 and
 * 1060 the 320 card was rendering the 32 face: four- and five-line headings
 * broken mid-word („Meis-terbetrieb"), which the design draws nowhere.
 *
 * The size therefore follows the arrangement. Mobile-first, so the strip's own
 * 24 is the base and the row restores the kit below.
 *
 * (0,5,0), and that is not decoration: a widget bound to a global typography
 * gets its own rule in the PAGE's stylesheet at
 * `.elementor-42 .elementor-element.elementor-element-XXXXXXX .elementor-heading-title`
 * = (0,4,0) — read off post-42.css on the running site, not assumed. A (0,3,0)
 * rule here would lose silently, and a (0,4,0) one would win only on load
 * order. Nothing else is restated: family, weight, line-height and colour all
 * stay the kit's.
 */
.elementor .v-detail .v-detail__entry .v-detail__title .elementor-heading-title {
  font-size: 24px;
}
/* ================================================================= *
 * Above 1061: the drawn two-column row
 * ================================================================= */
.v-pin.v-pin--leistungen-detail.e-con {
  --v-pin-advance: 30vh;
  /* The zones are flow children, so the container's own gap would space them
     apart and add itself to the pin distance. */
  --row-gap: 0px;
  --column-gap: 0px;
  --padding-block-start: 0px;
  --padding-block-end: 0px;
  --padding-inline-start: 0px;
  --padding-inline-end: 0px;
}

/* Off below the stack point: stacked, a pinned section would hold the page
   still over a column that is now the page's own flow. */
@media (min-width: 1061px) {
  .v-pin.v-pin--leistungen-detail.is-pinned { position: relative; }

  .v-pin.v-pin--leistungen-detail.is-pinned > .v-detail {
    position: sticky;
    /*
     * The offset is the SCRIPT's, because it depends on the window: normally
     * the bar's height, and on a short window as low as the content allows so
     * the section's own rows stay on screen. The fallback is the bar, so the
     * declaration is correct before the script runs and if it never does.
     */
    top: var(--v-pin-top, var(--valera-header-h));
  }

  /*
   * THE ZONES ARE THE PIN'S LENGTH. Flow children, on purpose: a sticky element
   * is constrained to its containing block's CONTENT box, and padding is
   * outside it — bought with padding, the pin does not happen at all, silently.
   *
   * visibility: hidden rather than display: none, because an
   * IntersectionObserver needs geometry and hidden boxes have it.
   */
  .v-pin.v-pin--leistungen-detail.is-pinned > .v-pin__zone {
    height: var(--v-pin-advance);
    visibility: hidden;
    pointer-events: none;
    /*
     * THE SOFT CLOSE. Each zone is one step, so its start is the one position
     * a step wants to settle on — Serhii, 2026-08-19: „эффект как
     * примагничивания… как автодоводчик".
     *
     * `proximity` BY DEFAULT, and that is measured rather than preferred:
     * proved on the live page in „Leistungen im Detail" before it shipped, it
     * pulls from about 220px and leaves you alone past 300, which is a
     * soft-closer exactly.
     *
     * `snap: 'mandatory'` is the third mode, added 2026-08-24 for the services
     * deck. It makes the document scroller always resolve to a snap position,
     * so the PAGE lands on a service rather than merely being nudged toward
     * one — which is what „как автодоводчик" asks for. The cost is real and is
     * why it is not the default: while it is in force the reader cannot rest
     * between two services, so a block that wants it must be one the reader is
     * meant to step through rather than read past.
     *
     * The `:has()` scope is what makes it affordable at all — the mode applies
     * only while THIS track is held, never to the rest of the page.
     */
    scroll-snap-align: start;
  }
}


/* The snap TYPE goes on the document scroller, because that is what moves.
   `:has()` keeps it off every page that carries no such track, and scoping it
   to this consumer's own track keeps two pinned blocks from declaring the same
   selector in one assembled sheet — and, just as importantly, keeps the mode
   below off the reader's scroll everywhere except while this block is held. */
@media (min-width: 1061px) {
  html:has(.v-pin.v-pin--leistungen-detail.is-pinned) { scroll-snap-type: y proximity; }
}

@media (min-width: 1061px) {
  /* The band takes the padding back, capped at 1312 (D197). `100%` and not
     `100vw`: this element IS the full-width band, so its own containing block
     is the page and the percentage resolves against the right box. */
  .v-detail.e-con {
    --flex-direction: row;
    --row-gap: 0px;
    --column-gap: 111px;
    /* flex-start and not stretch, because a stretched flex item cannot stick. */
    --align-items: flex-start;
    --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
    --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  }

  /*
   * D242 — the header column sticks. One declaration, and the reason it is a
   * declaration and not Elementor Pro's sticky module: that module is
   * `assets/lib/sticky/jquery.sticky.js` plus a cloned `__spacer` and
   * `position: fixed`, i.e. a scroll handler (D58: Safari pays per frame for
   * what Blink absorbs), and it does nothing at all with JavaScript off.
   *
   * The offset is `--valera-header-h` plus the 16 the site already uses for
   * every anchor jump — read out of style.css by gate(), never retyped.
   */
  .v-detail__head.e-con {
    --flex-grow: 0;
    --flex-shrink: 0;
    --flex-basis: 532px;
    --padding-left: 0px;
    --padding-right: 0px;
    position: sticky;
    top: calc(var(--valera-header-h) + 16px);
    align-self: flex-start;
  }

  /* `min-width: 0` because a flex item's automatic minimum size is its content,
     and a paragraph column without it refuses to shrink below its longest word
     plus its photograph. */
  .v-detail__rail.e-con {
    --flex-grow: 1;
    --flex-shrink: 1;
    --flex-basis: 0%;
    min-width: 0;
    overflow: visible;
  }

  /*
   * ONE ENTRY AT A TIME. The section is pinned and the entries step through it
   * — the same mechanism as the services list above, from the same file
   * (D215), with no second set of names.
   *
   * ALL EIGHT SHARE ONE GRID CELL rather than being translated by a running
   * offset, and that is the whole reason this is robust. The drawn entries are
   * 710 / 752 / 774 / 732 / 774 / 732 / 752 / 648 tall and they render two to three pixels taller
   * apiece, because German copy wraps further than the .fig draws it. An offset
   * built from the drawn tops would therefore be right at the first step and
   * twenty pixels out by the eighth — the accordion's floor bug wearing another
   * hat. Stacked in one cell there is no arithmetic to drift: the row is as tall
   * as its tallest entry, whatever that turns out to be, and a step names a
   * child rather than a distance.
   *
   * The incoming entry rises the drawn 24px — the gap the file draws
   * between two entries — so it arrives from below the way Serhii described,
   * rather than cross-fading in place.
   */
  .v-pin.v-pin--leistungen-detail.is-pinned .v-detail__row {
    display: grid;
  }

  .v-pin.v-pin--leistungen-detail.is-pinned .v-detail__row > * {
    grid-area: 1 / 1;
    opacity: 0;
    visibility: hidden;
    transform: translateY(24px);
    transition: opacity var(--valera-motion-media) var(--valera-motion-ease), transform var(--valera-motion-media) var(--valera-motion-ease);
  }

  .v-pin.v-pin--leistungen-detail.is-pinned[data-step="1"] .v-detail__row > *:nth-child(1) {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .v-pin.v-pin--leistungen-detail.is-pinned[data-step="2"] .v-detail__row > *:nth-child(2) {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .v-pin.v-pin--leistungen-detail.is-pinned[data-step="3"] .v-detail__row > *:nth-child(3) {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .v-pin.v-pin--leistungen-detail.is-pinned[data-step="4"] .v-detail__row > *:nth-child(4) {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .v-pin.v-pin--leistungen-detail.is-pinned[data-step="5"] .v-detail__row > *:nth-child(5) {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .v-pin.v-pin--leistungen-detail.is-pinned[data-step="6"] .v-detail__row > *:nth-child(6) {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .v-pin.v-pin--leistungen-detail.is-pinned[data-step="7"] .v-detail__row > *:nth-child(7) {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .v-pin.v-pin--leistungen-detail.is-pinned[data-step="8"] .v-detail__row > *:nth-child(8) {
    opacity: 1;
    visibility: visible;
    transform: none;
  }

  /*
   * AND THE ENTRY IS SIZED TO THE WINDOW, WHICH IS WHY THE PIN RUNS AT ALL
   * (D321). Everything above this was built on 2026-08-19 and had never once
   * been seen, because one entry is 774 drawn — 414 of photograph, the 24
   * gap and 336 of the longest body — and the pin refuses when that plus
   * the bar does not fit the visitor's window. It asked for 849px, measured;
   * a browser window on a 900px screen has about 780.
   *
   * The photograph is the one part with slack, so it is the part that gives
   * way: the drawn 414 whenever the window holds it, less below, never under
   * 240 — under that the pin refuses and the flow column is the right
   * answer again. ONE expression rather than a `flex-shrink` per entry, so all
   * eight photographs are the same height and the body always begins on the
   * same line; the eight bodies are 210..336 tall and shrinking against
   * them would have given eight different pictures.
   *
   * 464 = 80 of the section's own block padding + 24 left under the entry
   * + 360 that the drawn entry carries below its photograph. Only the 24 is
   * ours. `object-fit: cover` was already doing this for the two drawn boxes
   * (D244), so no second cut and no second file.
   */
  .v-pin.v-pin--leistungen-detail.is-pinned .v-detail__photo img {
    height: clamp(240px, calc(100vh - var(--valera-header-h) - 464px), 414px);
  }

  .v-detail__row.e-con {
    --flex-direction: column;
    --row-gap: 48px;
    --column-gap: 0px;
    --padding-left: 0px;
    --padding-right: 0px;
    --width: 100%;
  }

  .v-detail__entry.e-con {
    --flex-grow: 0;
    --flex-shrink: 1;
    --flex-basis: auto;
  }

  /* D247 — in the 669 column the heading is the kit's again. The VARIABLE and not
     the number, so the kit stays the one place the face is edited; it is the
     same variable Elementor's own per-element rule reads, proven to resolve on
     the running site (32 above 767, 24 below). */
  .elementor .v-detail .v-detail__entry .v-detail__title .elementor-heading-title {
    font-size: var(--e-global-typography-cardhead-font-size);
  }
}

/* Generated by build/generate-serviceslist.js — do not edit by hand. */

/*
 * Block 5 — Leistungsübersicht „Unsere Leistungen für Ihren Erfolg".
 * One page, one placement, eight rows: 211:3162 at 1440x2330 and
 * 211:3650 at 360x2114, plus the parked mock 211:3803
 * „services-cards on scroll", which is where the whole interaction is drawn.
 */
.v-svl.e-con {
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /* `100%` and not `100vw`: this element IS the band, so the percentage
     resolves against the page and centres the 1312 column (D197). */
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --align-items: stretch;
}

/* The list carries no gap of its own: the rows are drawn edge to edge, and the
   only thing between them is the hair rule below. */
/*
 * The card layer exists only while the section is pinned. At rest the artboard
 * draws ONE list with the first row black, and a second copy of it would be the
 * eight services printed twice.
 */
.v-svl__list--card { display: none; }

.v-svl__list.e-con {
  --row-gap: 0px;
  --column-gap: 0px;
  --align-items: stretch;
}

/* ---- one row ------------------------------------------------------ *
 *
 * Below 894 the mobile artboard's shape: title over paragraph, gap 24,
 * and NO photograph — 211:3650 draws none on any of the eight rows.
 *
 * The paint is the mock's, not an invention. 211:3821 — the row that
 * overlaps its predecessor — is the one row in the whole block that carries an
 * explicit fill, and it is #F6F6F6, the page ground. A row that overlaps
 * has to be opaque; a row that does not looks identical either way.
 */
.v-svl__row.e-con {
  --v-svl-cols: 1fr;
  --v-svl-rows: none;
  --v-svl-paper: var(--e-global-color-whitebg);
  --v-svl-ink: var(--e-global-color-primary);
  --v-svl-pad: 0px;
  --v-svl-photo-scale: 1;
  --row-gap: 24px;
  --column-gap: 0px;
  --padding-top: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px);
  --padding-bottom: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px);
  --padding-left: var(--v-svl-pad);
  --padding-right: var(--v-svl-pad);
  --align-items: start;
  background-color: var(--v-svl-paper);
  border-radius: var(--valera-radius-sm);
  transition: background-color var(--valera-motion-state) var(--valera-motion-ease), color var(--valera-motion-state) var(--valera-motion-ease);
}

/*
 * The hair rule as an INSET SHADOW and not a border, because a border is 1px
 * of layout: eight of them make the list 1992 instead of the drawn 1984,
 * and the stack arithmetic below is built on the rows being exactly 248.
 * Drawn 1px #949597 at 40% under rows 1..7, and NONE under the
 * last one — 211:3214 carries no stroke paint at all.
 */
.v-svl__row:not(:last-child) {
  box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--e-global-color-secondary) 40%, transparent);
}

/* Colour travels through one custom property so the card's state changes both
   halves at once. (0,3,0) beats Elementor's own widget defaults at (0,2,0)
   without emitting a colour into the payload, which would out-specify this. */
.v-svl__row .v-svl__title .elementor-heading-title {
  color: var(--v-svl-ink);
  font-size: clamp(28px, calc(28px + 14 * (100vw - 360px) / 1080), 42px);
  letter-spacing: 0;
  /*
   * Inert until the pile turns them on, and then they are what makes a
   * collapsed card show its NAME. One line, clipped: two of the eight titles
   * wrap at 1440 and a second line would hang 31px past the 103 strip and be
   * sliced by the card above it.
   */
  max-height: var(--v-svl-title-max, none);
  overflow: var(--v-svl-title-clip, visible);
}
/* The heading rises out of the row's middle to its top edge when the card is
   in the pile — `align-self`, so a one-line and a two-line title both land on
   the padding edge instead of one measured offset that only suits one of them. */
.v-svl__row > .v-svl__title { align-self: var(--v-svl-title-align, auto); }
/* And the rest of the card goes with it. Opacity only: the row must keep its
   248 or every landing below it moves. */
.v-svl__row > .v-svl__text,
.v-svl .v-svl__row > .v-svl__photo {
  opacity: var(--v-svl-body-opacity, 1);
  transition: opacity var(--valera-motion-state) var(--valera-motion-ease);
}
.v-svl__list .v-svl__row .v-svl__text {
  color: var(--v-svl-ink);
}
.v-svl__row .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* The photograph is a square with the drawn radius; `transform` and not
   `width` for the card's inset, so nothing reflows and Safari pays nothing
   per frame (D58). */
.v-svl__row .v-svl__photo img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--valera-radius-xs);
  transform: scale(var(--v-svl-photo-scale));
  transition: transform var(--valera-motion-state) var(--valera-motion-ease);
}
/* Below the stack point the mobile artboard draws no photograph at all.
   (0,3,0) so Elementor's `.elementor.elementor .e-con > .elementor-widget`
   cap cannot be the rule that decides this. */
.v-svl .v-svl__row > .v-svl__photo { display: none; }

/* ---- the drawn desktop row ---------------------------------------- *
 *
 * Five tracks and every one is a measured distance on the twelve-column grid:
 * the title runs to the grid's column 5 at 445.3333, the paragraph is drawn 400
 * on all eight rows, the gap to the photograph is 188 on the seven plain
 * ones, the photograph is a 200 square, and 78.6667 is what the drawn row
 * leaves over on the right. Fractions rather than pixels, so the whole row
 * narrows together and the photograph stays square.
 *
 * `min-content` on the FIRST track and 0 on the rest, and that too was
 * measured rather than reasoned: the proportional title track is 445.3333 at
 * 1440 and 244 at 894, where a 34.9px „Landschaftsbau" is 286 — so the heading
 * ran 42px past its column and over the paragraph. The paragraph and the gap
 * may squeeze, because a paragraph wraps; a single German compound cannot.
 *
 * The black card is not a sixth track. It takes its drawn 16px of inline
 * padding, the fractions narrow with it, and its photograph then lands within
 * a pixel of the 1031 the file draws it at — asserted, not assumed.
 */
@media (min-width: 894px) {
  .v-svl__row.e-con {
    --v-svl-cols: minmax(min-content, 445.3333fr) minmax(0, 400fr) minmax(0, 188fr) 200px minmax(0, 78.6667fr);
    --row-gap: 0px;
    --align-items: center;
  }
  /* The grid's own gutter, carried on the title rather than as a track: the
     title column is floored at its longest word, and without this the floor is
     the word EXACTLY — at 1024 „Wunschvorhaben" ended flush against the
     paragraph with nothing between them. */
  .v-svl__row > .v-svl__title { grid-column: 1; padding-inline-end: 24px; }
  .v-svl__row > .v-svl__text  { grid-column: 2; }
  .v-svl .v-svl__row > .v-svl__photo { display: block; grid-column: 4; }

  /*
   * The drawn static state: row 1 is the black card. The mobile artboard fills
   * no row at all, which is why this is inside the query and not above it.
   *
   * `.e-con` is not decoration. The rest state is declared on
   * `.v-svl__row.e-con` — (0,2,0) — because that is the only selector that
   * can beat Elementor's own container defaults, and a bare `.v-svl__row--1`
   * at (0,1,0) LOSES to it. Measured, not reasoned: the card rendered light at
   * 894, 1024 and 1210 while every gate here passed, and it looked right at
   * 1440 only because the animation was painting it. Every rule that changes
   * the card's state carries `.e-con` for that reason, and gateCss() counts
   * the classes rather than trusting this comment.
   */
  .v-svl__row--1.e-con {
    --v-svl-paper: var(--e-global-color-primary);
    --v-svl-ink: var(--e-global-color-whitebg);
    --v-svl-pad: 16px;
    --v-svl-photo-scale: 1;
  }
}

/* ---- the pile, and the card that moves down it -------------------- *
 *
 * Measured off 211:3803 „services-cards on scroll": the rows overlap by
 * 145 and each collapsed row shows 103, which is exactly 24 + 55 + 24 —
 * its own padding and one heading line. Nothing here was chosen except how
 * deep the pile is allowed to get, and the mock draws that too: undefined strips.
 *
 * From 894 up only, because the arithmetic is exact only while a row is
 * 248 tall — and the content column is locked at 1312 from 894 (D197), so
 * from there it is exact at every width. Below it the paragraph column narrows,
 * the rows grow, and the list is the plain one the artboard draws at rest.
 *
 * `prefers-reduced-motion: reduce` gets that same plain list. A pile that
 * assembles under the reader is a motion trigger, not a taste.
 */
.v-pin.v-pin--leistungen.e-con {
  --v-pin-advance: 30vh;
  /* The zones are flow children, so the container's own gap would space them
     apart and add itself to the pin distance. */
  --row-gap: 0px;
  --column-gap: 0px;
  --padding-block-start: 0px;
  --padding-block-end: 0px;
  --padding-inline-start: 0px;
  --padding-inline-end: 0px;
}

/* Off below the stack point: stacked, a pinned section would hold the page
   still over a column that is now the page's own flow. */
@media (min-width: 894px) {
  .v-pin.v-pin--leistungen.is-pinned { position: relative; }

  .v-pin.v-pin--leistungen.is-pinned > .v-svl {
    position: sticky;
    /*
     * The offset is the SCRIPT's, because it depends on the window: normally
     * the bar's height, and on a short window as low as the content allows so
     * the section's own rows stay on screen. The fallback is the bar, so the
     * declaration is correct before the script runs and if it never does.
     */
    top: var(--v-pin-top, var(--valera-header-h));
  }

  /*
   * THE ZONES ARE THE PIN'S LENGTH. Flow children, on purpose: a sticky element
   * is constrained to its containing block's CONTENT box, and padding is
   * outside it — bought with padding, the pin does not happen at all, silently.
   *
   * visibility: hidden rather than display: none, because an
   * IntersectionObserver needs geometry and hidden boxes have it.
   */
  .v-pin.v-pin--leistungen.is-pinned > .v-pin__zone {
    height: var(--v-pin-advance);
    visibility: hidden;
    pointer-events: none;
    /*
     * THE SOFT CLOSE. Each zone is one step, so its start is the one position
     * a step wants to settle on — Serhii, 2026-08-19: „эффект как
     * примагничивания… как автодоводчик".
     *
     * `proximity` BY DEFAULT, and that is measured rather than preferred:
     * proved on the live page in „Leistungen im Detail" before it shipped, it
     * pulls from about 220px and leaves you alone past 300, which is a
     * soft-closer exactly.
     *
     * `snap: 'mandatory'` is the third mode, added 2026-08-24 for the services
     * deck. It makes the document scroller always resolve to a snap position,
     * so the PAGE lands on a service rather than merely being nudged toward
     * one — which is what „как автодоводчик" asks for. The cost is real and is
     * why it is not the default: while it is in force the reader cannot rest
     * between two services, so a block that wants it must be one the reader is
     * meant to step through rather than read past.
     *
     * The `:has()` scope is what makes it affordable at all — the mode applies
     * only while THIS track is held, never to the rest of the page.
     */
    scroll-snap-align: start;
  }
}


/* The snap TYPE goes on the document scroller, because that is what moves.
   `:has()` keeps it off every page that carries no such track, and scoping it
   to this consumer's own track keeps two pinned blocks from declaring the same
   selector in one assembled sheet — and, just as importantly, keeps the mode
   below off the reader's scroll everywhere except while this block is held. */
@media (min-width: 894px) {
  html:has(.v-pin.v-pin--leistungen.is-pinned) { scroll-snap-type: y proximity; }
}

/* ---- the card that stays, and the list that travels through it ----- *
 *
 * The mock 211:3803 „services-cards on scroll" draws ONE moment — row 1 and
 * row 2 collapsed to a 103 strip each and row 3 black — and the arithmetic
 * behind that strip is real: 24 of padding + one 55px heading line + 24.
 * What the file does NOT draw is what happens after the third row, and the
 * first build guessed: eight sticky rows, a pile two deep, and rows 3…8 landing
 * on the same offset so each one painted over the card in front of it. Measured
 * on staging every 40px of scroll, the black card's visible height sawtoothed
 * 248 → 5…21 → 248 — full only at the eight landings, which are exactly the
 * offsets the generator itself computed and the verifier then sampled.
 *
 * So the guess is gone. The section is PINNED and the list runs through a
 * window one row tall: at step N the list is translated by (N-1) rows, so the
 * card never moves, never collapses, and shows one service at a time — the
 * arrangement Serhii drew.
 *
 * From 894 up only, because a row is 248 tall only while the content column is
 * locked at 1312 (D197). Below that the paragraph column narrows, the row
 * grows, and a fixed window would clip it — so below 894 the list is the plain
 * one the artboard draws at rest, with row 1 black.
 *
 * `prefers-reduced-motion: reduce` gets that same plain list: a section that
 * holds the page still is motion, not taste.
 */
@media (min-width: 894px) {
  @media (prefers-reduced-motion: no-preference) {
    /*
     * THE PINNED FRAME FILLS THE SCREEN, so nothing under the card is bare
     * ground. Serhii saw a whole viewport of it after the last service, and it
     * was there at every step — the frame was 512 tall on a window of 900 or
     * more, and the rest of the screen was the page showing through.
     *
     * `min-height` on the section and `flex-grow` on the list: the list takes
     * whatever the header leaves, so a tall window shows four cards and a short
     * one shows two. No number is invented — the browser does the arithmetic
     * the drawn header and padding imply.
     */

    /*
     * THE WINDOW. It clips, and it carries the row's own drawn radius: a clip
     * cuts at its padding box, so a card crossing that edge had its 6px corners
     * flashed square for as long as it moved. Rounding the window means the cut
     * follows the same curve the card has, at rest and in motion alike.
     */
    /*
     * THE FRAME IS TWO WHOLE CARDS.
     *
     * It held one card and the mock's 103px strip, which is a row's own
     * 24 of padding plus one 55px heading line plus 24 — the height at which a
     * row shows its NAME and nothing else. That is what the mock draws for a
     * PASSED row, and it is the wrong height for the one that is coming:
     * Serhii, „нижние карточки почему-то обрезанные, а должна быть целиком".
     *
     * The travel still runs to the LAST service rather than stopping a card
     * early. A service that never becomes the card is worse than 248px of
     * ground behind the last one — and behind the last one is exactly where a
     * deck that has been dealt out has nothing left to show.
     */
    .v-pin.v-pin--leistungen.is-pinned .v-svl__frame.e-con {
      /* `.e-con` is not decoration here either: without it Elementor's own
         container rule wins and the frame computes to height 0, which is
         exactly what it did — the whole travel stood still with every gate
         green and the deployed rule reading `height: 496px`. */
      position: relative;
      --v-svl-rowh: var(--v-pin-unit, calc(200px + 2 * clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px)));
      /* ONE SLOT IS THE DEFAULT and the second is an upgrade the window has to
         earn. The black card is one row at every size — the clip-path below
         cuts it to exactly that — so one slot shows the whole of it and loses
         only the peek at the service coming next. Serhii, 2026-08-26: „нам по
         факту нужно чтобы всегда влазило именно одно — чёрное. Второе белое оно
         не так важно." Two slots need up to 798px of window across this width
         range against 553 for one; refusing the pin outright rather than
         dropping a slot is what shipped, and it is why the deck never moved on
         a 709px window. */
      --v-svl-max: 7;
      /* `--height`, not `height`: an Elementor container takes its height from
         that variable, and a raw declaration here loses to its own rule. The
         frame computed 0 for exactly that reason, with the rule present in the
         sheet and matching the element. */
      --height: var(--v-svl-rowh);
      overflow: clip;
      border-radius: var(--valera-radius-sm);
    }

    /*
     * WHAT MUST FIT IS THE FRAME, NOT THE WHOLE SECTION (D336).
     *
     * The numbered header lives inside this section and is 104 tall at 1440,
     * 181 where it stacks. Demanding it too is what made the deck refuse a
     * 1016x409 window and show a plain list instead of the one thing the
     * section is about. Serhii, 2026-08-26: „нам же важно, чтобы чёрная
     * карточка вмещалась в доступное окно, а название раздела — это как
     * приятный бонус."
     *
     * So the pin is told to measure the frame. With room the section still
     * sticks under the bar and the header is in view exactly as before; without
     * it, the header scrolls off the top and the card stays whole.
     */
    .v-pin.v-pin--leistungen.is-pinned .v-svl.e-con {
      --v-pin-fit: '.v-svl__frame';
      /* And what one step travels: the tallest row as it actually renders. */
      --v-pin-unit-of: '.v-svl__row';
      /*
       * And which element each of the eight anchors means. The plain list only:
       * the black card holds a second copy of all eight rows and they carry no
       * ids, so the row class on its own matches sixteen elements and the
       * pin would refuse the mapping outright.
       */
      --v-pin-anchor-of: '.v-svl__list:not(.v-svl__list--card) > .v-svl__row';
    }

    /*
     * ALL EIGHT ROWS ARE THE SAME HEIGHT WHILE PINNED, and that is what lets
     * the deck run below 990. min-height, never height: the unit IS the
     * tallest row, so this only grows the shorter ones and can never cut the
     * one it was measured from.
     */
    .v-pin.v-pin--leistungen.is-pinned .v-svl__row.e-con {
      min-height: var(--v-pin-unit);
    }

    /*
     * ON A SHORT WINDOW THE GAP ABOVE THE CARD SHARES WITH THE ONE BELOW IT.
     *
     * Serhii, 2026-08-26, at 1440x485: „сверху более чем достаточно, а снизу
     * впритык". The space he meant is this gap — the site's common 60 between
     * the numbered header and the content.
     *
     * Its first reason is gone: once the pin measures the frame (D336) the air
     * underneath comes out of real slack and no longer needs this. Measured
     * after that change, it keeps a second one — at 485 the full 60 lifts the
     * numbered header up behind the fixed bar, and the halved one leaves it
     * clear. Halved to the band's own MOBILE value, so it is still not a number
     * invented for this rule.
     *
     * Nothing changes on an ordinary window.
     */
    @media (max-height: 620px) {
      .v-pin.v-pin--leistungen.is-pinned .v-svl.e-con {
        --row-gap: 32px;
      }
    }

    /*
     * THE SECOND SLOT, where the window can hold it. Both numbers move together
     * and they must: with two slots the rows stop one short and the last step
     * drops the window onto the eighth service (D320); with one slot there is
     * nowhere to drop it to, so the rows run all the way instead. The drop is
     * DERIVED from this max below rather than declared, so it follows the switch
     * on its own and cannot disagree with it.
     */
    @media (min-height: 800px) {
      .v-pin.v-pin--leistungen.is-pinned .v-svl__frame.e-con {
        --height: calc(2 * var(--v-svl-rowh));
        --v-svl-max: 6;
      }
    }

    .v-pin.v-pin--leistungen.is-pinned .v-svl__frame.e-con > .v-svl__list {
      position: absolute;
      inset: 0 0 auto 0;
    }

    /*
     * The copy, shown only here and clipped to the slot the card occupies.
     *
     * ON THE LAST STEP THE SLOT MOVES (D320). Everything above it stops: the
     * rows of the light list are already where they will end, the seventh
     * service in the upper slot and the eighth whole in the lower one. What
     * moves is this layer — down by exactly one slot, so the black lands on the
     * eighth service instead of the eighth service being dragged up over
     * nothing. `transform`, so the sweep costs no layout and no repaint of the
     * list beneath it (D58).
     */
    .v-pin.v-pin--leistungen.is-pinned .v-svl__list--card {
      display: block;
      clip-path: inset(0 0 calc(100% - var(--v-svl-rowh)) 0);
      transform: translateY(calc(var(--v-svl-drop, 0) * var(--v-svl-rowh)));
      transition: transform var(--valera-motion-media) var(--valera-motion-ease);
    }

    /*
     * NO RULE INSIDE THE FRAME. The artboard draws a hairline under rows 1..7,
     * and in a list at rest it separates them. In the pinned frame the rows are
     * moving, so the same 1px reads as a line crawling up the screen ahead of
     * every service — Serhii saw it before any measurement did. The card's own
     * edge is what separates them here.
     */
    .v-pin.v-pin--leistungen.is-pinned .v-svl__row { box-shadow: none; }

    .v-pin.v-pin--leistungen.is-pinned .v-svl__list--card .v-svl__row.e-con {
      --v-svl-paper: var(--e-global-color-primary);
      --v-svl-ink: var(--e-global-color-whitebg);
      --v-svl-pad: 16px;
      --v-svl-photo-scale: 1;
    }

    /*
     * Both layers travel by the same expression, so a row's black half and its
     * light half are always the same row in the same place.
     */
    .v-pin.v-pin--leistungen.is-pinned .v-svl__list > * {
      transform: translateY(calc(min(var(--v-svl-step, 0), var(--v-svl-max)) * -1 * var(--v-svl-rowh)));
      transition: transform var(--valera-motion-media) var(--valera-motion-ease);
    }

    /*
     * …and on the last step the card layer's rows go ONE ROW FURTHER, by
     * exactly the slot the layer above drops. The two cancel, which is the
     * point: the black copy stays registered over the light copy underneath it
     * to the pixel, so what the reader sees is a black window sliding down onto
     * the eighth service and not a second set of type sliding under it.
     *
     * Same specificity as the rule above (0,4,0) and it must stay BELOW it —
     * source order is what decides between them.
     */
    .v-pin.v-pin--leistungen.is-pinned .v-svl__list--card > * {
      transform: translateY(calc((min(var(--v-svl-step, 0), var(--v-svl-max)) + var(--v-svl-drop, 0)) * -1 * var(--v-svl-rowh)));
    }

    .v-pin.v-pin--leistungen.is-pinned[data-step="1"] .v-svl__frame.e-con { --v-svl-step: 0; }
    .v-pin.v-pin--leistungen.is-pinned[data-step="2"] .v-svl__frame.e-con { --v-svl-step: 1; }
    .v-pin.v-pin--leistungen.is-pinned[data-step="3"] .v-svl__frame.e-con { --v-svl-step: 2; }
    .v-pin.v-pin--leistungen.is-pinned[data-step="4"] .v-svl__frame.e-con { --v-svl-step: 3; }
    .v-pin.v-pin--leistungen.is-pinned[data-step="5"] .v-svl__frame.e-con { --v-svl-step: 4; }
    .v-pin.v-pin--leistungen.is-pinned[data-step="6"] .v-svl__frame.e-con { --v-svl-step: 5; }
    .v-pin.v-pin--leistungen.is-pinned[data-step="7"] .v-svl__frame.e-con { --v-svl-step: 6; }
    .v-pin.v-pin--leistungen.is-pinned[data-step="8"] .v-svl__frame.e-con { --v-svl-step: 7; --v-svl-drop: calc(7 - var(--v-svl-max)); }
  }
}

/* Generated by build/generate-steps.js — do not edit by hand. */

/*
 * Block 3 — the photo + step column. Two sections, four drawn placements.
 *
 * The section is the grid: 12 columns of the 1312 the design draws, the same
 * grid the section header divides. The photograph takes columns 1-4 and the
 * step column 5-12, which is what the drawn 421 and 867 ARE — the
 * two spans, to a third of a pixel. Nothing here names 445.33; it falls out
 * of the placement, which is why the three line up at every width and not only
 * at 1312.
 *
 * NO BACKGROUND, deliberately. Neither drawn frame carries a fill at either
 * width and both artboards are #F6F6F6, so this section shows the page ground
 * (D121). Painting it — white to be safe, or grey to match — hides the next
 * seam instead of revealing it.
 *
 * SPECIFICITY NOTE, load-bearing twice over. `widget-nested-accordion.min.css`
 * is enqueued when the widget renders, i.e. AFTER the child theme's sheets, so
 * at equal specificity the widget wins; and the kit's Theme Style writes
 * `.elementor-kit-N h3` at (0,1,1), which beats the widget's own
 * `:where( … ) > .e-n-accordion-item-title-text` at (0,1,0). Every rule below
 * that touches a property either of them also sets is written to out-specify
 * both, and every value the widget emits a DEFAULT for is set through its
 * control instead — those land at (0,3,0) and cannot be beaten from here.
 */
.v-steps.e-con {
  --v-steps-cols: repeat(12, 1fr);
  /* Read by the widget through `accordion_item_title_space_between`, which
     is the only way into a (0,3,0) declaration this sheet cannot reach. */
  --v-steps-item-gap: clamp(16px, calc(24px + -8 * (100vw - 360px) / 1080), 24px);
  /* THE SECTION IS A COLUMN NOW (D326): the numbered header, then the pin's
     track. The twelve-column grid moved down to the row, which is the element
     that gets pinned. align-items stretch, so the header and the track take the
     band's full content width instead of shrinking to their contents. */
  --flex-direction: column;
  --align-items: stretch;
  --column-gap: 0px;
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /*
   * Below 1440 the padding interpolates; above it the padding takes the surplus
   * so the content stays on the 1312 grid while the band stays full width.
   * `100%` and not `100vw`: this element IS the band, so its own containing
   * block is the page and the percentage resolves against the right box.
   */
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/*
 * THE ROW — the photograph and the step column, side by side, and the element
 * the pin makes sticky. It carries the twelve-column grid the section used to.
 *
 * Explicit columns, not auto-placement. Sparse auto-placement never moves the
 * cursor backwards, so a photograph placed in columns 1-4 would push a step
 * column with a definite column 5 onto the next row — the same trap the section
 * header's reverse layout hit.
 *
 * The column is TOP-aligned against the photograph, which is what the file
 * draws: the 867 x 550 column frame is FIXED height with its list at y = 0,
 * so the row's CENTER distribution has nothing left to centre. Top also keeps
 * the first step still while a panel opens below it.
 */
.v-steps__row.e-con {
  --grid-align-content: start;
  --align-items: start;
  --column-gap: 24px;
  --row-gap: 0px;
  /*
   * ZERO, and it is not tidiness. Elementor gives every container a default
   * 10px padding, and this container did not exist before the pin: measured on
   * the live page, it put the photograph 10px to the right of the numbered
   * header, so the header and the column stopped standing on the same grid —
   * the one thing this block's own comment says must hold. The pinned rule
   * below spends the vertical pair on centring; the inline pair stays 0 at
   * every width, because the band already owns the inset.
   */
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
}

.v-steps__row > .v-steps__photo { grid-column: 1 / 5;   grid-row: 1; }
.v-steps__row > .v-steps__list  { grid-column: 5 / -1;  grid-row: 1; }

/*
 * The stack point is 894, imported from the section header rather than chosen
 * again: below it the 8-column body is narrower than the narrowest body the
 * design itself stacks. The header and this block must break at the SAME width
 * or the two columns stop agreeing halfway down the section.
 */
@media (max-width: 893px) {
  .v-steps.e-con { --v-steps-cols: 1fr; }
  /* Stacked, the row's own gap is the section's — the photograph and the list
     become two rows of one column and need the band's rhythm between them. */
  .v-steps__row.e-con { --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px); }
  .v-steps__row > .v-steps__photo,
  .v-steps__row > .v-steps__list { grid-column: 1 / -1; grid-row: auto; }
}

/* ---- the photograph ---------------------------------------------- *
 * 421 x 550 in the row and 320 x 418.052 in the stack — ONE aspect
 * ratio, because 320 x 550 / 421 is 418.052 exactly. So unlike the
 * accordion's photograph this one needs no second ratio and no width cap: it is
 * the grid's four columns at every width, and the grid already says so.
 * Its radius is 6 desktop and 6 mobile, which is a scaled 6 (D7).
 */
.v-steps__photo { width: 100%; }

.v-steps__photo .elementor-widget-container,
.v-steps__photo figure,
.v-steps__photo img {
  display: block;
  width: 100%;
}

.v-steps__photo img {
  aspect-ratio: 421 / 550;
  object-fit: cover;
  border-radius: var(--valera-radius-sm);
}

/*
 * The four photographs, stacked, and the open step decides which one shows.
 *
 * They occupy ONE grid cell rather than four, so the column keeps the height it
 * was drawn at and nothing below moves when a step is opened - the same reason
 * the FAQ list got a measured floor on the same day.
 *
 * The binding is pure CSS. Elementor renders each item as a <details>, so
 * :has() can read which one carries the open attribute, and no script decides
 * anything. Without JavaScript the accordion still works - <details> is native -
 * so the photograph still follows it.
 *
 * The transition is the site's media step, the same one the hero cross-fades
 * its slides with, because this is the same job: one picture replacing another.
 * Only the incoming layer animates, over an opaque outgoing one, so the ground
 * never shows through the middle of the change (the dip the hero taught us).
 */
.v-steps__row > .v-steps__photo {
  grid-row: 1;
  grid-column: 1 / 5;
}

@media (min-width: 894px) {
  .v-steps__row > .v-steps__photo {
    /* One cell, four layers. */
    grid-area: 1 / 1 / 2 / 5;
    opacity: 0;
    transition: opacity var(--valera-motion-media) var(--valera-motion-ease);
  }

  /* step 1 */
  .v-steps:has(.v-steps__list .e-n-accordion-item:nth-of-type(1)[open]) .v-steps__photo--1 { opacity: 1; z-index: 1; }
  /* step 2 */
  .v-steps:has(.v-steps__list .e-n-accordion-item:nth-of-type(2)[open]) .v-steps__photo--2 { opacity: 1; z-index: 1; }
  /* step 3 */
  .v-steps:has(.v-steps__list .e-n-accordion-item:nth-of-type(3)[open]) .v-steps__photo--3 { opacity: 1; z-index: 1; }
  /* step 4 */
  .v-steps:has(.v-steps__list .e-n-accordion-item:nth-of-type(4)[open]) .v-steps__photo--4 { opacity: 1; z-index: 1; }

  /* Nothing open — the drawn state is step 1, and it is also what a visitor
     sees for the moment between closing one panel and opening the next. */
  .v-steps:not(:has(.e-n-accordion-item[open])) .v-steps__photo--1 { opacity: 1; }
}

/* Below the stack point the design draws ONE photograph and the list beneath
   it, so the other three are not in the layout at all. */
@media (max-width: 893px) {
  .v-steps__photo--2,
  .v-steps__photo--3,
  .v-steps__photo--4 { display: none; }
}

/* ---- the steps ---------------------------------------------------- *
 * What the widget's controls already hold is NOT repeated here: the item gap,
 * the summary row's padding, the distance from the panel, the animation, the
 * three title colours and the type role all live in the generated element rule
 * at (0,3,0), where they win. What is left is what no control reaches.
 */

/* Elementor gives every panel a 1px #d5d8dc border via a (0,1,0) :where() rule;
   the design draws none on any of the fourteen placements. */
.v-steps__row > .v-steps__list { --n-accordion-border-width: 0px; }

/* Elementor's own typo: the title header is grown with a MENU variable
   (--n-menu-title-flex-grow) that does not exist on an accordion, so the title
   never takes the row and the marker collapses against the text. */
.v-steps__row > .v-steps__list .e-n-accordion-item-title-header { flex: 1 1 auto; min-width: 0; }

/*
 * The summary's face, bound to the kit rather than restated — and it has to be
 * said here even though the widget already says it, because the widget says it
 * inside `:where( … )`, which contributes nothing, while the kit writes
 * `.elementor-kit-N h3` at (0,1,1). The kit would win. Its font-SIZE survives
 * on its own because that one travels through `--n-accordion-title-font-size`
 * into a (0,2,0) rule.
 *
 * Tracking is 0 on all 8 drawn summaries and the kit's `secondary` row declares
 * none, so it would otherwise inherit the body role's -2% — the same gap the
 * atom and the accordion each found; the kit fix is on the launch checklist.
 */
.v-steps__row > .v-steps__list .e-n-accordion-item-title-text {
  font-family: var(--e-global-typography-secondary-font-family), sans-serif;
  font-weight: var(--e-global-typography-secondary-font-weight);
  line-height: var(--e-global-typography-secondary-line-height);
  letter-spacing: 0;
  color: var(--n-accordion-title-normal-color);
}

/*
 * THE HANGING INDENT THE NUMBER ASKS FOR — D346.
 *
 * These summaries are an ORDERED LIST in the drawing: Figma prints „1." itself
 * and sets the text at a hanging indent behind it, which is why `characters`
 * carries no number and why the build shipped without one until 2026-08-27.
 * The number is back in the string (`listedText`); the indent is the half CSS
 * owns.
 *
 * Measured, not chosen: the drawing sets the text at 36 on a 24px face and
 * at 48 on the mobile artboard's 32px one — 1.5em both times. So the value
 * follows the face instead of being two pixel numbers that drift apart the
 * next time a breakpoint moves (D311 is what that costs).
 *
 * `text-indent` pulls the FIRST line back to the padding edge, so the number
 * sits where the drawing puts it and every wrapped line lines up under the
 * text rather than under the number. That is the whole point on a phone, where
 * all four of these titles wrap.
 */
.v-steps__row > .v-steps__list .e-n-accordion-item-title-text {
  padding-inline-start: 1.5em;
  text-indent: -1.5em;
}

/*
 * The open step's colour, and the same problem one level down: the widget puts
 * it on the <summary> and lets it inherit, but the title is an <h3> and the kit
 * colours <h3> directly at (0,1,1). So the colour is claimed on the text
 * itself, in both states, at a specificity neither can reach.
 */
.v-steps__row > .v-steps__list .e-n-accordion-item[open] > .e-n-accordion-item-title .e-n-accordion-item-title-text {
  color: var(--n-accordion-title-active-color);
}

.v-steps__row > .v-steps__list .e-n-accordion-item:not([open]):hover > .e-n-accordion-item-title .e-n-accordion-item-title-text {
  color: var(--n-accordion-title-hover-color);
}

/*
 * Ours, and nothing in the file argues with it: the design draws no focus state
 * anywhere (0 of 29 interaction fields). A visible focus ring is not decoration
 * — the summary row is the only control in the block, it carries NO marker, and
 * it is reached with Tab and the arrow keys.
 */
.v-steps__row > .v-steps__list .e-n-accordion-item-title:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/* ---- the panel ---------------------------------------------------- *
 * The panel's inset (45) and its gap (24) are constant across the drawn
 * range and live on the container's own controls, so they are not here. What is
 * here is the body's own measure: 599px inside an 822px content box, on all five
 * desktop copies including the three state mocks. It is a decision the design
 * took, not a wrap — and below the stack point the column is narrower than
 * 599 anyway, so one declaration serves both widths.
 *
 * FIVE classes, and every one is needed: Elementor's own
 * `.elementor.elementor .e-con > .elementor-widget { max-width: 100% }` is
 * (0,4,0), and this widget is a direct child of the panel container. The hero
 * step paid for this once already.
 */
.v-steps__panel.e-con > .elementor-widget.v-steps__text.elementor-widget-text-editor {
  max-width: 599px;
}

/* the open step's panel sits flush; the widget's own border is off above */
.v-steps__panel.e-con { --column-gap: 0px; }

.v-steps__panel .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* ---- the pinned scroll (D326) ------------------------------------- *
 * The section holds still while the reader scrolls, and each step opens in
 * turn. Gaby asked for it on 2026-08-24; the design does not draw it — its
 * three parked mocks per page are accordion STATES, one open item each (D152)
 * — so this is a departure, recorded as one.
 *
 * The mechanism is the shared one and nothing here reinvents it: the track,
 * the zones, the sticky offset and the scroll distance all come from
 * build/lib/stepscroll.js, and the pinned element is the ROW.
 *
 * WHAT A STEP DOES is the only per-block part, and here it is already written:
 * the four :has([open]) rules above swap the photograph and the widget's own
 * active colour turns the summary gold. So the pin has to produce [open] on
 * item N and nothing else — which is what assets/steps.js does, by clicking
 * the summary. Zero new per-step CSS.
 */
.v-pin.v-pin--steps.e-con {
  --v-pin-advance: 30vh;
  /* The zones are flow children, so the container's own gap would space them
     apart and add itself to the pin distance. */
  --row-gap: 0px;
  --column-gap: 0px;
  --padding-block-start: 0px;
  --padding-block-end: 0px;
  --padding-inline-start: 0px;
  --padding-inline-end: 0px;
}

/* Off below the stack point: stacked, a pinned section would hold the page
   still over a column that is now the page's own flow. */
@media (min-width: 894px) {
  .v-pin.v-pin--steps.is-pinned { position: relative; }

  .v-pin.v-pin--steps.is-pinned > .v-steps__row {
    position: sticky;
    /*
     * The offset is the SCRIPT's, because it depends on the window: normally
     * the bar's height, and on a short window as low as the content allows so
     * the section's own rows stay on screen. The fallback is the bar, so the
     * declaration is correct before the script runs and if it never does.
     */
    top: var(--v-pin-top, var(--valera-header-h));
  }

  /*
   * THE ZONES ARE THE PIN'S LENGTH. Flow children, on purpose: a sticky element
   * is constrained to its containing block's CONTENT box, and padding is
   * outside it — bought with padding, the pin does not happen at all, silently.
   *
   * visibility: hidden rather than display: none, because an
   * IntersectionObserver needs geometry and hidden boxes have it.
   */
  .v-pin.v-pin--steps.is-pinned > .v-pin__zone {
    height: var(--v-pin-advance);
    visibility: hidden;
    pointer-events: none;
    /*
     * THE SOFT CLOSE. Each zone is one step, so its start is the one position
     * a step wants to settle on — Serhii, 2026-08-19: „эффект как
     * примагничивания… как автодоводчик".
     *
     * `proximity` BY DEFAULT, and that is measured rather than preferred:
     * proved on the live page in „Leistungen im Detail" before it shipped, it
     * pulls from about 220px and leaves you alone past 300, which is a
     * soft-closer exactly.
     *
     * `snap: 'mandatory'` is the third mode, added 2026-08-24 for the services
     * deck. It makes the document scroller always resolve to a snap position,
     * so the PAGE lands on a service rather than merely being nudged toward
     * one — which is what „как автодоводчик" asks for. The cost is real and is
     * why it is not the default: while it is in force the reader cannot rest
     * between two services, so a block that wants it must be one the reader is
     * meant to step through rather than read past.
     *
     * The `:has()` scope is what makes it affordable at all — the mode applies
     * only while THIS track is held, never to the rest of the page.
     */
    scroll-snap-align: start;
  }
}


/* The snap TYPE goes on the document scroller, because that is what moves.
   `:has()` keeps it off every page that carries no such track, and scoping it
   to this consumer's own track keeps two pinned blocks from declaring the same
   selector in one assembled sheet — and, just as importantly, keeps the mode
   below off the reader's scroll everywhere except while this block is held. */
@media (min-width: 894px) {
  html:has(.v-pin.v-pin--steps.is-pinned) { scroll-snap-type: y proximity; }
}
@media (min-width: 894px) {
  @media (prefers-reduced-motion: no-preference) {
    /* The flag assets/steps.js reads, so the breakpoint lives in ONE place.
       A width typed into the script would be D311 exactly: one constant, one
       hand-written copy, and the copy does not fail loudly. */
    .v-pin--steps.is-pinned > .v-steps__row { --v-steps-live: 1; }

    /*
     * THE RESERVE, and it cannot be a number in this file.
     *
     * The design draws the step column as a fixed 867 x 550 box with its
     * list at y = 0 — as tall as the photograph, in every state, in all six
     * mocks. Rendered German does not honour that: measured on the live pages,
     * the column is 331/354/354/354 tall on Stützmauer and 410/429/541/432 on
     * Kanalbau at 1440, and 542-586 / 564-676 at 894. A pinned box that changes
     * height at every step shoves everything below it mid-scroll, which is the
     * defect stepscroll.js's own header warns about.
     *
     * The value is the tallest of the four AT THE READER'S WIDTH, which only
     * the browser knows, so assets/steps.js measures and publishes it — the
     * same reason stepscroll.js publishes --v-pin-height. The 0px fallback IS
     * the no-JS state: without the script this rule reserves nothing and the
     * block renders exactly as it did before.
     */
    .v-pin--steps.is-pinned .v-steps__list {
      min-height: var(--v-steps-reserve, 0px);
    }

    /*
     * THE PINNED ROW SITS IN THE MIDDLE of the space under the bar, not against
     * it. Parked at the top it left its leftover hanging below and read as
     * unfinished — Serhii, 2026-08-24.
     *
     * The mechanism is the pin's, not this block's: the browser half publishes
     * how much room is left over (only it can know — the row's height is a
     * function of the width, the copy and the photograph's clamp) and
     * centreCss() spends it as symmetric padding. The services bento wanted the
     * same thing the same afternoon, which is why it is there and not here.
     *
     * The floor is the site's one anchor clearance, so with no slack — or with
     * no JavaScript at all — the row keeps exactly the air it had.
     */
    .v-pin--steps.is-pinned > .v-steps__row.e-con {
      --padding-top: max(16px, calc(var(--v-pin-slack, 0px) / 2));
      --padding-bottom: max(16px, calc(var(--v-pin-slack, 0px) / 2));
    }

    /*
     * THE PHOTOGRAPH GIVES WAY, the same move D321 made for the services
     * detail. max-height and not height, so the drawn 421 / 550 stays the only
     * place this photograph's shape is written and the object-fit: cover above
     * does the crop — no second ratio, no second file.
     *
     * Above its floor this can never be why the pin refuses: if the clamp is
     * not at the floor then the photograph is already smaller than the room
     * left under the bar. Below the floor the pin refuses instead of shrinking
     * the picture into a stripe.
     */
    .v-pin--steps.is-pinned .v-steps__photo img {
      max-height: max(240px, calc(100svh - var(--valera-header-h) - 16px - clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px)));
    }
  }
}

/* The default the script tests against. Outside the query, so below the stack
   point and under reduced motion it reads 0 and the script stands down. */
.v-steps__row { --v-steps-live: 0; }

/* Generated by build/generate-subcontractor.js — do not edit by hand. */

/*
 * Über uns — „Wir sind auch als Nachunternehmer & Lohnunternehmen tätig"
 * (211:2763 / 211:3102).
 *
 * The fourth band of the CTA card's envelope. Only what DIFFERS from
 * generate-cta.js's base is written here; the card padding, the column gap,
 * the radius, the glass, the fill, the scrim and column A are all identical to
 * it and are inherited rather than restated.
 *
 * Band: 1312 inside 1440 (inset 64) and 320 inside 360 (inset 20);
 * 75 of breathing room above and below at 1440, 40 at 360.
 */
.v-band.v-band--nachu.e-con {
  --padding-top: clamp(40px, calc(40px + 35 * (100vw - 360px) / 1080), 75px);
  --padding-bottom: clamp(40px, calc(40px + 35 * (100vw - 360px) / 1080), 75px);
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/* The design draws 566 / 588 where the base draws 566 / 377. Column A
   is already right; only B moves, and as a grow FACTOR, never a pixel width. */
.v-band--nachu .v-cta > .v-cta__col--b { flex-grow: 588; }

/* The blank line the design draws between the two sentences (§3): one line
   box, taken from the kit's own `text` role rather than typed. The theme
   gives 0.9em; measured on the rendered page at 1440. */
.v-band--nachu .v-cta .elementor-widget-text-editor p:not(:last-of-type) {
  margin-block-end: 1.4em;
}

/* Generated by build/generate-timeline.js — do not edit by hand. */

/*
 * Block 4 — Über uns „Unsere Geschichte". One section, one page, three entries.
 *
 * Two forms of one layout. Above 729 the entries alternate about a centred
 * spine; below it they stack in one column with the spine on the left, which is
 * what the mobile artboard draws. That width is DERIVED, not chosen: a column
 * holds while it is at least as wide as the narrowest column the design draws
 * for this content anywhere — the mobile rail's own 285 (`211:3072`) —
 * and `729` is the first viewport where it is.
 *
 * ONE number places the spine at both ends: `--v-tl-clear`, the gap between the
 * spine and a column. The design draws 22 at 360 and 87 at 1440, so the two
 * forms are one measurement seen at two widths (D51).
 *
 * The drawn desktop spine is at 644.477 of 1312 — 11.523 left of centre — and the
 * three entries are 567.671 / 580 / 557 wide. Neither is a system; the body has no
 * auto-layout and every child carries an absolute transform. The symmetric
 * model ships (D216): spine at 50%, columns of (1312 - 2 x 87) / 2 = 569, which
 * is -1.329 / +11 / -12 against the three drawn widths.
 */
.v-tl.e-con {
  --row-gap: clamp(32px, calc(32px + 28 * (100vw - 360px) / 1080), 60px);
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  /* `100%` and not `100vw`: this element IS the band, so its own containing
     block is the page and the percentage resolves against the right box. */
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --align-items: stretch;
}

/* ---- the list ------------------------------------------------------ *
 * The rail is the BASE and the two columns are the media query, so the
 * narrowest layout needs no query at all.
 *
 * The left padding is the spine's own x plus the clear space: 13 + 22 = 35,
 * which is exactly where the mobile artboard puts the entry stack (x 55 in a
 * 360 artboard whose content box starts at 20).
 */
.v-tl__list.e-con {
  --v-tl-cols: 1fr;
  --v-tl-rows: none;
  --v-tl-clear: 22px;
  --row-gap: 32px;
  --column-gap: 0px;
  --padding-top: 0px;
  --padding-right: 0px;
  --padding-bottom: 0px;
  --padding-left: 35px;
  position: relative;
}

@media (min-width: 729px) {
  .v-tl__list.e-con {
    --v-tl-cols: 1fr 1fr;
    --v-tl-clear: clamp(22px, calc(22px + 65 * (100vw - 360px) / 1080), 87px);
    --row-gap: 40px;
    --column-gap: calc(2 * var(--v-tl-clear));
    --padding-left: 0px;
  }
  .v-tl__entry--1 { grid-column: 1; grid-row: 1; }
  .v-tl__entry--2 { grid-column: 2; grid-row: 2; }
  .v-tl__entry--3 { grid-column: 1; grid-row: 3; }
}

/* ---- the spine ----------------------------------------------------- *
 * A real, empty container rather than a pseudo-element on the list, because
 * Elementor's `.e-con::before` is its background-overlay layer and arrives
 * with `inset: 0` and a `height: max(…)` already set — the footer lost an
 * artwork layer to exactly that. `::after` is safe and is spent on the
 * progress overlay, which needs a second box: the track is 1px #949597 and the
 * progress is 1.6px #F6F6F6.
 *
 * Absolutely positioned, so it takes no grid track, and `--width` rather than
 * `width` because `.e-con { width: var(--width) }` reads the variable — one
 * declaration in play instead of two fighting. Every dimension is reclaimed,
 * for the reason the footer's artwork layer records.
 */
.v-tl__axis.e-con {
  --width: 1px;
  /* No `--padding-*` here: this element carries Elementor's own `padding` key,
     which lands at (0,3,0) and cannot be reached from a (0,2,0) rule anyway. A
     declaration that can never win is not a safety net, it is a line to explain
     later. Same for the list, in reverse: the list emits NO padding key, so its
     four are in the sheet and nowhere else. */
  --min-height: 0;
  position: absolute;
  inset-block: 0;
  left: 13px;
  height: auto;
  margin: 0;
  background-color: #949597;
  pointer-events: none;
}

@media (min-width: 729px) {
  .v-tl__axis.e-con {
    left: 50%;
    margin-inline-start: -0.5px;
  }
}

/*
 * The progress overlay. Wider and brighter than the track, anchored at the top,
 * and scaled — never resized: a height animation lays out every frame and D58
 * is explicit that the review browser pays for what Blink absorbs.
 *
 * The DRAWN state is 36.16% of the desktop track and 41.56% of the mobile one, and
 * in both artboards the bright line stops at the SECOND of the three dots. That
 * is the whole animation spec the file carries, and it is what says this is a
 * reader-progress line rather than decoration.
 */
.v-tl__axis::after {
  content: '';
  position: absolute;
  inset-block: 0;
  left: 50%;
  width: 1.6px;
  margin-inline-start: -0.8px;
  background-color: #F6F6F6;
  transform-origin: top center;
  transform: scaleY(1);
}

/*
 * Driven by a CSS scroll-driven view timeline and NO JavaScript.
 *
 * The timeline is NAMED on the list and referenced from the overlay rather than
 * declared on the overlay itself, and that is not style: `view()` measures its
 * subject's own box, and the overlay's box is the thing being scaled — a
 * `scaleY(0)` subject has no height and the timeline degenerates. The list is
 * never transformed, so it is the honest subject.
 *
 * `entry 100% exit 0%` is the range where the section fills the viewport, i.e.
 * exactly „how far through this section is the reader". Where the browser has
 * no scroll-driven animations the rule above stands and the whole spine is lit,
 * which is what the drawing looks like once the reader has reached the end.
 */
@supports (animation-timeline: view()) {
  .v-tl__list.e-con {
    view-timeline: --v-tl-track block;
  }

  .v-tl__axis::after {
    transform: scaleY(0);
    animation-name: v-tl-progress;
    animation-fill-mode: both;
    animation-timing-function: linear;
    animation-timeline: --v-tl-track;
    animation-range: entry 100% exit 0%;
  }

  @keyframes v-tl-progress {
    to { transform: scaleY(1); }
  }
}

/* Reduced motion removes the ANIMATION, never the information: the spine is
   simply lit end to end, which is the same state the no-support branch gets. */
@media (prefers-reduced-motion: reduce) {
  .v-tl__axis::after {
    animation-name: none;
    transform: scaleY(1);
  }
}

/* ---- the dot ------------------------------------------------------- *
 * 25px, a 1.389px ring — #F4EA64 above the midline and #949597 below it — over a
 * #F6F6F6 interior. All 6 drawn dots are identical, including the one the
 * progress has not reached, so the dot carries no state and none is invented.
 *
 * One box, using the gradient-border idiom: the interior is painted to the
 * padding box and the two-tone ring to the border box, which is crisper than a
 * radial-gradient and needs no second element.
 *
 * `top: 0` — the dot's top edge on its entry's top edge. The file gives no
 * rule: the six drawn offsets are 37.001 / 26 / 11 on desktop and
 * 30.903 / 60.903 / 69.903 on mobile, spanning 11…70 and drifting in
 * OPPOSITE directions between the two artboards. The entry's own top edge is
 * the one anchor that survives a rewrap (D218). The positioning context it needs
 * is declared once, with the entry's gaps, further down.
 */
.v-tl__entry::after {
  content: '';
  position: absolute;
  top: 0;
  left: calc(-1 * var(--v-tl-clear));
  width: 25px;
  height: 25px;
  box-sizing: border-box;
  border: 1.389px solid transparent;
  border-radius: 50%;
  background:
    linear-gradient(#F6F6F6 0 0) padding-box,
    linear-gradient(180deg, #F4EA64 0 50%, #949597 50% 100%) border-box;
  transform: translateX(-50%);
  pointer-events: none;
}

/* A right-hand entry's spine is on ITS left, which is the base rule already;
   only the left-hand ones have to reach across their own width. */
@media (min-width: 729px) {
  .v-tl__entry--left::after {
    left: calc(100% + var(--v-tl-clear));
  }
}

/* ---- the entry ----------------------------------------------------- *
 * Three gaps, all of them the same at both breakpoints on all three entries:
 * 24 between the picture and the text, 24 between the date and the content,
 * 16 between the heading and the paragraph. The fourth frame the design draws
 * („Content Top", gap 8) holds a single child, so its gap renders nothing
 * and is not emitted.
 *
 * `position: relative` lives here and not in a rule of its own: the dot above
 * is positioned against this box, and a second `.v-tl__entry.e-con` rule would
 * make `decl()` read whichever of the two it met first — which is exactly the
 * failure this gate caught on the first run.
 */
.v-tl__entry.e-con {
  --row-gap: 24px;
  --column-gap: 0px;
  position: relative;
}

.v-tl__body.e-con {
  --row-gap: 24px;
  --column-gap: 0px;
}

.v-tl__content.e-con {
  --row-gap: 16px;
  --column-gap: 0px;
}

/* ---- the picture cell ---------------------------------------------- *
 * Two layers: the paper frame fills the cell, the photograph is mounted on top
 * of it at the design's own fractions. The fractions are identical to four
 * decimals at both breakpoints on all three entries, which is the proof that
 * the pair is one scalable unit — see the gates.
 *
 * The cell keeps its own aspect ratio per entry, also identical across the two
 * breakpoints, so the frame never has to be told a height.
 */
.v-tl__frame.e-con {
  --width: 100%;
  position: relative;
  height: auto;
}

.v-tl__entry--1 > .v-tl__frame.e-con { aspect-ratio: 567.671 / 375.782; }
.v-tl__entry--2 > .v-tl__frame.e-con { aspect-ratio: 580 / 370; }
.v-tl__entry--3 > .v-tl__frame.e-con { aspect-ratio: 557 / 385; }

/*
 * FOUR classes on the cap and every one is needed: Elementor's own
 * `.elementor.elementor .e-con > .elementor-widget { max-width: 100% }` is
 * (0,4,0) and loads after the child theme's sheets. The hero paid for that
 * once and the accordion paid for it again.
 */
.v-tl__frame.e-con > .elementor-widget.v-tl__paper.elementor-widget-image {
  position: absolute;
  inset: 0;
  max-width: none;
  width: auto;
  height: auto;
  margin: 0;
}

/* Both markup modes. `e_optimized_markup` is ON in 4.2.2 and deletes
   `.elementor-widget-container`; a rule that names only that wrapper is valid
   CSS matching an element that does not exist, which is how twelve mosaic
   scrims once rendered on nothing. */
.v-tl__paper .elementor-widget-container,
.v-tl__paper figure,
.v-tl__photo .elementor-widget-container,
.v-tl__photo figure {
  position: absolute;
  inset: 0;
  margin: 0;
}

/* `contain` and not `cover`: the artwork's crop reproduces the cell's aspect
   to within 0.002 (gated), so there is nothing to letterbox — but if a master
   is ever recut slightly off, `contain` loses a sub-pixel edge where `cover`
   would crop the torn border that is the entire point of the artwork. */
.v-tl__paper img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.v-tl__frame.e-con > .elementor-widget.v-tl__photo.elementor-widget-image {
  position: absolute;
  max-width: none;
  margin: 0;
}

.v-tl__entry--1 > .v-tl__frame.e-con > .elementor-widget.v-tl__photo.elementor-widget-image {
  left: 7.2225%;
  top: 9.58%;
  width: 82.7944%;
  height: 81.1641%;
}

.v-tl__entry--2 > .v-tl__frame.e-con > .elementor-widget.v-tl__photo.elementor-widget-image {
  left: 5.5172%;
  top: 5.9459%;
  width: 69.8276%;
  height: 85.9459%;
}

.v-tl__entry--3 > .v-tl__frame.e-con > .elementor-widget.v-tl__photo.elementor-widget-image {
  left: 12.2083%;
  top: 9.6104%;
  width: 81.8671%;
  height: 81.8182%;
}

.v-tl__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Per-entry crop, for a photograph whose own aspect is nothing like its
   aperture's. See DECISIONS.photoPosition. */
.v-tl__entry--1 .v-tl__photo img { object-position: 50% 40%; }

/* ---- the two faces the kit does not own ---------------------------- *
 * The date is authored on the widget (D219). The heading binds the kit's
 * `cardhead` role, whose row declares no letter-spacing at all, so it would
 * inherit the body role's -0.02em. The drawn value is 0%; the same
 * scoped repair the section-header atom and the mosaic both carry.
 *
 * `/ 100` because Figma stores letterSpacing in PERCENT — `gate()` asserts the
 * unit on every text in the block. The drawn value is 0, where percent and em
 * agree, so this divide changes no byte today; without it a heading that ever
 * gained tracking would ship at 100x it.
 */
.v-tl__head .elementor-heading-title {
  letter-spacing: 0em;
}

.v-tl .elementor-widget-text-editor p:last-of-type {
  margin-block-end: 0;
}

/* Generated by build/generate-testimonials.js — do not edit by hand. */

/*
 * Block C — the testimonial slider. Six placements, three pages.
 *
 * Elementor holds everything it has a control for; this sheet holds the three
 * things it does not have one for at all — the order of the parts inside a
 * slide, where the arrows sit, and their glass — plus every value that is a
 * function of width (D51), plus the widget's own card, which the design does
 * not draw. Both padding anchors are the file's: 40/20 at 360 and
 * 80/64 at 1440, on all six placements without exception.
 */
.v-tstm.e-con {
  --padding-top: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-bottom: clamp(40px, calc(40px + 40 * (100vw - 360px) / 1080), 80px);
  --padding-left: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
  --padding-right: max(clamp(20px, calc(20px + 44 * (100vw - 360px) / 1080), 64px), calc((100% - 1312px) / 2));
}

/*
 * The widget draws a Twitter-style card: white ground, 1px #e1e8ed border, 5px
 * radius, on `.elementor-widget-reviews .swiper-slide` (0,2,0). The design
 * draws no card at all — the quote sits straight on the ground. The widget's
 * own 15px insets on the header and the content go with it.
 */
.v-tstm .v-tstm__slider .swiper-slide {
  background-color: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
}

.v-tstm .v-tstm__slider .elementor-testimonial__content {
  padding: 0;
}

/*
 * The slide, as the design draws it: stars, quote, attribution.
 *
 * The widget prints stars and attribution inside one <cite> in the header and
 * the quote in a sibling <div>, and has no control for that order.
 * `display: contents` lifts the three parts into one flex column so `order`
 * can place them; the header's own `display: flex` and padding go with it.
 */
.v-tstm .v-tstm__slider .elementor-testimonial {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(16px, calc(16px + 8 * (100vw - 360px) / 1080), 24px);
  text-align: start;
}

.v-tstm .v-tstm__slider .elementor-testimonial__header,
.v-tstm .v-tstm__slider .elementor-testimonial__cite {
  display: contents;
}

.v-tstm .v-tstm__slider .elementor-star-rating { order: 1; }
.v-tstm .v-tstm__slider .elementor-testimonial__content { order: 2; }
.v-tstm .v-tstm__slider .elementor-testimonial__title { order: 3; font-style: normal; }

/*
 * The stars, repaired 2026-08-19. They shipped as five empty squares.
 *
 * Pro prints each star as the character `&#xE934;` — once as the `<i>`'s own
 * text (the unmarked layer) and once through `i:before` (the marked layer,
 * clipped by width for fractions). Both resolve only against Elementor's
 * `eicons` webfont, which this site never enqueues: measured on the rendered
 * page, `eicons` appears 0 times while the stylesheet itself returns 200. So
 * the browser drew tofu and the widget's own colour made it yellow tofu, which
 * reads as a design decision rather than a missing font — that is why it
 * survived the verify pass.
 *
 * Loading the font was the alternative and is the same trade the accordion
 * refused for two rectangles. Instead the measured star (211:1744, 20x18.889,
 * one SHA-1 across all six placements) is decoded by generate-icons.js and used
 * here as a MASK over the widget's own elements. The widget keeps its markup,
 * its settings and its accessibility tree; only the glyph changes.
 *
 * `font-size: 0` removes the PUA character from the box without touching
 * `color`, and the explicit box comes from the file rather than from the
 * widget's `star_size`, so the geometry is the drawn one.
 *
 * FULL STARS ONLY, and that is gated: all six placements draw five identical
 * full stars (`starCount` 5, `uniform` true). A mask cannot express the
 * fractional clip `i:before` does, so if a rating ever becomes fractional this
 * rule has to grow a second, clipped layer — the gate is what will say so.
 */
.v-tstm .v-tstm__slider .elementor-star-rating i {
  width: 20px;
  height: 18.8889px;
  font-size: 0;
  background-color: currentColor;
  -webkit-mask: url(img/icons/stern.svg) center / contain no-repeat;
  mask: url(img/icons/stern.svg) center / contain no-repeat;
}

/* The marked layer would paint a second glyph on top in the same font, and it
   is the one Elementor colours `#f0ad4e`. With the mask carrying the shape and
   `stars_unmarked_color` carrying ours, it has nothing left to add. */
.v-tstm .v-tstm__slider .elementor-star-rating i::before { content: none; }

/* The slide's social icon. It has no glyph — the icon control is emptied in the
   payload — but the widget prints its wrapper unconditionally, with an
   `aria-label` on a <div> that has no role. `display: none` takes it out of the
   accessibility tree as well as the layout. */
.v-tstm .v-tstm__slider .elementor-testimonial__icon { display: none; }

/*
 * 829 is a text cap INSIDE the eight-column column, not the column itself.
 * The column is 866.67 at 1440 and the arrows sit at ITS right edge, outside
 * the cap — which is what proves the 829 belongs to the text. Only above the
 * stack point: every one of the three mobile quotes fills its 320.
 */
@media (min-width: 894px) {
  .v-tstm .v-tstm__slider .elementor-testimonial__content { max-width: 829px; }
}

/*
 * THE COLUMN STARTS WHERE EVERY OTHER SECTION'S DOES, and until 2026-08-20 it
 * did not.
 *
 * Elementor gives `.elementor-main-swiper` a symmetric margin of its own —
 * measured 21.67px each side on the rendered page — which CENTRES the slider
 * inside the column it was given. The column is right: 509 at 1440, the grid's
 * own `columnStart` plus the section's 64. The quote inside it was landing at
 * 531, so the reviews sat 22px right of the mosaic (510) and the accordion
 * (509) above them on the same page. Serhii saw it as a broken vertical line
 * before any measurement did.
 *
 * The .fig agrees with the neighbours and not with the widget: the drawn body
 * `211:4180` is at x=445 inside a block at 64, which is that same 509.
 *
 * Both margins go, and nothing replaces them — the cap above already holds the
 * text to its drawn 829, and the arrows keep sitting on the column's own
 * right edge, which is what that cap was proved against.
 */
.v-tstm .v-tstm__slider .elementor-main-swiper {
  margin-inline: 0;
}

/*
 * The arrows. Swiper positions them absolutely at the vertical middle of the
 * slider, one on each edge; the design draws them as a 120x48 pair at gap 24,
 * right-aligned under the quote. Putting them back IN FLOW is what does that:
 * the swiper becomes a wrapping flex row, the slide track takes the whole first
 * line and the two buttons fall onto the second, where `justify-content`
 * right-aligns them. They stay inside the swiper's box, so its `overflow:
 * hidden` — which is what makes the slider work — never clips them.
 */
.v-tstm .v-tstm__slider .elementor-main-swiper {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  column-gap: 24px;
  row-gap: clamp(0px, calc(32px + -32 * (100vw - 360px) / 1080), 32px);
}

/* stuetzmauer: the design draws its own gap here (32 / 32). */
.v-tstm--stuetzmauer .v-tstm__slider .elementor-main-swiper {
  row-gap: 32px;
}

.v-tstm .v-tstm__slider .elementor-main-swiper > .swiper-wrapper {
  flex: 0 0 100%;
  min-width: 0;
}

/* (0,4,0), because `.elementor-element .swiper .elementor-swiper-button` is
   (0,3,0) and sets position, top and transform — and `-prev`/`-next` set left
   and right at the same weight. */
.v-tstm .v-tstm__slider .elementor-main-swiper .elementor-swiper-button {
  position: static;
  inset: auto;
  transform: none;
  margin: 0;
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--valera-radius-pill);
  /* #FFFFFF at 0.19 over a 4px backdrop blur, on all twelve arrows. The
     radius token is 50px on a 48px box, which is the file's own way of saying
     round. */
  background-color: rgba(255, 255, 255, 0.19);
  backdrop-filter: blur(var(--valera-blur-4));
  -webkit-backdrop-filter: blur(var(--valera-blur-4));
}

/* The button is `role="button" tabindex="0"`, so it takes focus and must show
   it. The design draws no focus state — this is ours, in the kit's accent. */
.v-tstm .v-tstm__slider .elementor-main-swiper .elementor-swiper-button:focus-visible {
  outline: var(--valera-focus-width) solid var(--valera-focus-color);
  outline-offset: var(--valera-focus-offset);
}

/*
 * The transition is 400ms and it is ours — the file carries no timing anywhere,
 * across all 18,869 nodes. Swiper writes its duration inline on every slide, so
 * the reduced-motion override has to out-rank an inline style.
 */
@media (prefers-reduced-motion: reduce) {
  .v-tstm .v-tstm__slider .swiper-slide { transition-duration: 1ms !important; }
}

/*
 * ---------------------------------------------------------------------------
 * The homepage band: ONE photograph, two contents.
 *
 * 1412 = 430 + 552 + 430 on the desktop artboard and
 * 1416 = 518.8889 + 503.1111 + 394 on the mobile one — the middle term is
 * bare photograph, so it is a gap and not a box. No min-height: the band's
 * height is its contents', the same closure every band on this site keeps.
 * ---------------------------------------------------------------------------
 */
.v-tstm-band.e-con {
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
  --row-gap: clamp(503.1111px, calc(503.1111px + 48.8889 * (100vw - 360px) / 1080), 552px);
  --column-gap: 0px;
}

/*
 * The ramp the standalone CTA strip could not have had: it is a SECOND FILL on
 * the photo node, not a scrim rectangle — #000000 at alpha 1 at the top to
 * #666666 at alpha 0 at the bottom, the whole layer at 0.8. That is the
 * site's 55-carrier scrim ramp REVERSED, so `--valera-scrim` is the wrong
 * token for it and it is written out. The mobile artboard draws #020202 where
 * the desktop draws #000000; one value ships.
 *
 * It reuses `.v-band::after` rather than adding a layer — same box, same
 * z-index, two properties changed. Two classes so it cannot lose the tie.
 */
.v-band.v-tstm-band::after {
  background: linear-gradient(180deg, rgba(0, 0, 0, 1) 0%, rgba(102, 102, 102, 0) 100%);
  opacity: 0.8;
}

/*
 * The CTA half. Its wrapper carries `v-band--voll` so the card inside it takes
 * the geometry generate-cta.js already wrote for that variant — and NOT
 * `v-band`, whose `isolation: isolate` would put an isolated ancestor between
 * the photograph and the card's backdrop-filter (D56). The declarations
 * `.v-band.v-band--voll.e-con` would have brought are these — taken from
 * generate-cta.js's own helper, not retyped, so the 1440 content cap (D197)
 * reaches this placement too. It did not when these were a hand-written zero:
 * the strip rendered 1792 of content at 1920 while every other section on the
 * page rendered 1312.
 */
.v-tstm-band__cta.e-con {
  --padding-top: 0px;
  --padding-bottom: 0px;
  --padding-left: 0px;
  --padding-right: 0px;
}
