/* ==========================================================================
   Blitz Räumung & Umzüge 
   Layout is written in logical properties throughout, so the Arabic mirror
   is a `dir="rtl"` attribute, not a second stylesheet.
   ========================================================================== */

@font-face {
  font-family: Manrope;
  src: url('../fonts/manrope-latin.woff2') format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: Manrope;
  src: url('../fonts/manrope-latin-ext.woff2') format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: Inter;
  src: url('../fonts/inter-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: Inter;
  src: url('../fonts/inter-latin-ext.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --- Tokens -------------------------------------------------------------- */

:root {
  /* Colour */
  --orange: #eb642d;       /* graphics, large text, numerals — never small text on light */
  --ember: #c24d19;        /* the accessible rendition: buttons, small orange text, focus */
  --ink: #1f1f23;
  --green: #3d5b3f;        /* WhatsApp affordance + location badge. Nothing else. */
  --green-deep: #314a34;
  --paper: #fff;
  --surface: #f7f8fa;
  --muted: #6b7280;        /* 16px minimum, always */
  --line: #e5e7eb;
  --line-control: #8a92a0; /* control boundaries: 3.13:1 on paper, WCAG 1.4.11 */
  --line-inv: #3a3a42;
  --paper-dim: #c9cad0;    /* secondary text on ink grounds only */

  /* Radius */
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 16px;
  --r-pill: 999px;

  /* Space */
  --s-xs: 4px;
  --s-sm: 8px;
  --s-md: 16px;
  --s-lg: 24px;
  --s-xl: 32px;
  --s-2xl: 48px;
  --s-3xl: 64px;
  --s-4xl: 96px;
  --s-section: clamp(64px, 9vw, 128px);

  /* Structure */
  --container: 1200px;
  --gutter: 24px;
  --header-h: 107px;
  --header-h-compact: 64px;
  --bar-h: 60px;
  --bar-meta-h: 36px;

  /* Shadow vocabulary — four jobs. The fourth is new with the Leistungen menu:
     The system permits a shadow for "an element that genuinely floats above the
     page", and a panel hanging off the sticky header is that. It is deliberately
     NOT the Overlay shadow, which is drawn for the full-screen mobile menu and
     the prototype notice — at 0.28 alpha and 64px of blur it turns a nav panel into a
     modal. This one is a hairline's worth of separation plus enough depth to
     read as above the photograph it lands on. */
  --sh-hover: 0 8px 24px rgba(31, 31, 35, 0.1);
  --sh-header: 0 1px 0 rgba(31, 31, 35, 0.08);
  --sh-bar: 0 -2px 16px rgba(31, 31, 35, 0.12);
  --sh-overlay: 0 24px 64px rgba(31, 31, 35, 0.28);
  --sh-menu: 0 16px 40px rgba(31, 31, 35, 0.14);

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  /* One step added to the semantic ramp: the hero subline and section ledes
     sit between Body and Title. Everything else snaps to the documented ramp
     — and Muted never goes below 16px. */
  --fs-lede: 1.0625rem;   /* hero subline + section ledes */
  --fs-micro: 0.875rem;   /* form error message, 14px */
  --fs-phone: clamp(1.5rem, 4vw, 1.75rem); /* tap target, not a card title */
}

@media (max-width: 479px) {
  :root { --gutter: 16px; }
}

/* --- Reset --------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-block-start: calc(var(--header-h-compact) + var(--s-md));
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: Inter, system-ui, sans-serif;
  font-size: 1rem;
  line-height: 1.65;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizelegibility;
}

img, svg { display: block; max-width: 100%; }
img { height: auto; }

h1, h2, h3, h4, p, figure, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }

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

/* A tint of the brand orange rather than the orange itself: white on #EB642D
   is 3.29:1, and a selection the visitor cannot read is a worse detail than no
   detail. Ink on a 22% wash clears every threshold and still says Blitz. */
::selection { background: color-mix(in srgb, var(--orange) 22%, transparent); color: var(--ink); }

/* The default bar is a wide grey slab against a near-white page. Thin, in the
   palette's own hairline family, so the page edge stays quiet. */
html { scrollbar-width: thin; scrollbar-color: var(--paper-dim) transparent; }

/* One focus treatment, everywhere. The comp shows none, so this is designed. */
:focus-visible {
  outline: 2px solid var(--ember);
  outline-offset: 2px;
}
.on-dark :focus-visible,
.footer :focus-visible,
.trust :focus-visible,
.bar :focus-visible {
  outline-color: var(--paper);
}
/* The one ground the two-colour scheme above cannot answer: a photograph.
   From 1024 the hero's action row runs its natural width and crosses onto the
   hero image — measured at 1440, the row ends at x=607 against a photograph
   starting at x=432 — so the ring is drawn on whatever pixels happen to be
   there. Ember on bright sky and white on bright sky are both weak, and which
   one you get depends on the crop, not on the design.
   So the ring becomes two-tone: the documented Ember outline stays, and a
   Paper halo sits outside it. Whatever the photograph does, one of the two
   edges contrasts against it. The outline is kept as an outline rather than
   folded into the shadow so forced-colors mode still renders it.
   On the paper part of the row the halo is invisible against the page, so this
   costs nothing where the ground is already known. */
@media (min-width: 1024px) {
  .hero__cta .btn:focus-visible {
    box-shadow: 0 0 0 6px var(--paper);
  }
}

.skip {
  position: absolute;
  inset-block-start: -100px;
  inset-inline-start: var(--gutter);
  z-index: 200;
  background: var(--ink);
  color: var(--paper);
  padding: 12px 20px;
  border-radius: var(--r-sm);
  font-size: 1rem;
  font-weight: 600;
}
.skip:focus { inset-block-start: 12px; }

bdi[dir='ltr'] { white-space: nowrap; }

.vh {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Type ---------------------------------------------------------------- */

.display, .headline, .title, .numeral {
  font-family: Manrope, system-ui, sans-serif;
  text-wrap: balance;
  hyphens: auto;
  overflow-wrap: break-word;
}

/* Display is the hero H1 and nothing else, so it carries the hero's sizing.
   The size tracks the copy column, which is not a constant fraction of the
   viewport, so it steps at 1024 and 1280. */
.display {
  font-size: clamp(1.75rem, 8.2vw, 4rem);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
}
.headline {
  font-size: clamp(1.875rem, 3.5vw, 2.75rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.015em;
}
.title {
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
}
.numeral {
  font-size: clamp(2rem, 4vw, 2.75rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.label {
  font-family: Inter, system-ui, sans-serif;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.lede {
  font-size: var(--fs-lede);
  color: var(--muted);
  max-width: 38ch;
}
.muted { color: var(--muted); }

/* Arabic loses uppercase — its distinction comes from weight and colour, and
   letter-spacing breaks the letter joins. */
[dir='rtl'] .label {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 700;
}

/* --- Layout -------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--s-section); }
.section--paper { background: var(--paper); }
.section--surface { background: var(--surface); }

/* More space above a heading than below it — 2:1. */
.section__head { margin-block-end: var(--s-2xl); max-width: 60ch; }

@media (min-width: 1024px) {
  .section__cols {
    display: grid;
    grid-template-columns: minmax(20rem, 26rem) minmax(0, 1fr);
    gap: var(--s-3xl);
    align-items: start;
  }
  .section__cols .section__head { margin-block-end: 0; }
  .section__cols .section__head .headline { hyphens: manual; }
}
.section__head .headline { margin-block-start: var(--s-lg); }
.section__head .lede { margin-block-start: var(--s-md); }

/* The eyebrow reads as a field name, not a whisper. At the plain Label step
   (13px / 600 / 0.08em) a two-word string above a 44px Manrope 800 headline
   has no presence at all — it looked like a caption that had drifted upward.
   Three changes, none of which touch the layout: size up, weight to 700, and
   tracking opened from 0.08em to 0.12em — the tracking is what actually does
   the work, because a short uppercase string gains authority from interval
   rather than from mass. Uppercase plus tracking is what marks this as a
   label, not its size, so the Uppercase Is a Label Rule still holds at this
   scale. The gap beneath goes 16px → 24px (above) so the label owns a beat of
   its own instead of sitting on the headline's shoulder.

   **The size below is off the documented ramp and was set deliberately by
   hand.** The documented type ramp has five steps — Label 0.8125 / Micro 0.875 /
   Body 1 / Lede 1.0625 / Title 1.25 — and the value here is not one of them, so
   the design detector flags this line on every edit to this file, correctly.
   Two ways to close it: snap to the nearest ramp step (Lede at 1.0625rem is
   within a pixel of where this has been sitting), or add the step to the ramp
   so it stops describing a scale the build does not use. Until one of
   those happens the flag is a true finding, not noise — do not silence it.

   Tracking is the axis carrying the emphasis here rather than size, which is
   why it has been pushed well past the 0.08em of the base Label step. The one
   thing to watch as it opens further is the longest string in the set,
   `Kostenloses Angebot` (19 characters), against the narrowest supported
   viewport: at 360px the container is 328px wide, and every 0.01em of tracking
   adds roughly 3px to that string. Measure it there before opening the value
   further; it is the first thing that will wrap, and it wraps on the CTA band
   where a two-line eyebrow reads as a mistake.

   Contrast holds on every ground it appears on: Ember on Surface 4.52:1, on
   Paper 4.80:1, and the CTA band's Signal Orange on Ink 5.00:1 — all AA, with
   more headroom at any of these sizes than at the original 13px. The RTL reset
   at `[dir='rtl'] .label` still wins on tracking (0,2,0 against this rule's
   0,1,0), so Arabic keeps `letter-spacing: normal` and its letter joins
   survive however wide this gets. */
.eyebrow {
  color: var(--ember);
  display: block;
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: 0.24em;
}

/* Running body copy on the two pages that are documents rather than stacks of
   components — Über uns and the Messie page. Every other block on the site
   sets its own measure (.lede 46ch, .step__body 62ch, .faq p 68ch); these two
   had nothing to inherit one from, and a paragraph left to the 1200px
   container runs to roughly 150 characters a line. The reset zeroes p margins,
   so the interval between paragraphs belongs here too. */

/* --- Buttons ------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 48px;
  padding: 16px 22px;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
  font-family: Inter, system-ui, sans-serif;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: background-color 180ms var(--ease), color 180ms var(--ease),
    border-color 180ms var(--ease), transform 140ms var(--ease);
}
[dir='rtl'] .btn { text-transform: none; letter-spacing: normal; font-weight: 700; }

.btn__arrow { flex: none; transition: transform 180ms var(--ease); }
[dir='rtl'] .btn__arrow { transform: scaleX(-1); }

/* Ember, not Signal Orange: white 13px on #EB642D is 3.29:1 and fails AA.
   Ember reads as the same orange and passes. */
.btn--primary { background: var(--ember); color: var(--paper); }
.btn--primary:hover { background: var(--ink); }
.btn--primary:hover .btn__arrow { transform: translateX(3px); }
[dir='rtl'] .btn--primary:hover .btn__arrow { transform: scaleX(-1) translateX(3px); }

.btn--secondary {
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  padding: 15px 21px;
}
.btn--secondary:hover { background: var(--ink); color: var(--paper); }

.btn--whatsapp { background: var(--green); color: var(--paper); }
.btn--whatsapp:hover { background: var(--green-deep); }

.btn--block { width: 100%; }
.btn:active { transform: scale(0.98); }
.btn:disabled { opacity: 0.72; cursor: default; }
.btn:disabled[data-busy='true'] { cursor: progress; }
.btn:disabled:hover { background: var(--ember); }
.btn--whatsapp:disabled:hover { background: var(--green); }
.btn:disabled:active { transform: none; }

/* --- Header -------------------------------------------------------------- */

.header {
  position: sticky;
  inset-block-start: 0;
  z-index: 100;
  background: var(--paper);
  transition: box-shadow 180ms var(--ease);
}
.header.is-compact { box-shadow: var(--sh-header); }

/* No rule between the header and the hero: the separation is the resting
   height itself, which is why that height is generous. The bar compacts
   107 → 64px. The height change is NOT transitioned: .header is in flow, so
   animating its height reflows the whole document for the length of the
   transition and drags the content the reader is looking at. The shadow and
   the logo scale carry the gesture instead — both composite. */
/* The phone number left this row (its channels are the Sofortkontakt bar on
   mobile and the Kontakt section on desktop), and the ~180px it held went to
   the gaps rather than to nothing: at 1.2 the five nav links stop reading as
   one run-on string. One multiplier, so the row's proportions retune together. */
.header__inner {
  --bar-rhythm: 1.2;
  display: flex;
  align-items: center;
  gap: calc(var(--s-2xl) * var(--bar-rhythm));
  height: var(--header-h);
}
.header.is-compact .header__inner { height: var(--header-h-compact); }
.header.is-compact .nav,
.header.is-compact .header__aside { gap: calc(var(--s-lg) * var(--bar-rhythm) * 0.85); }

.header__logo { flex: none;   min-height: 44px;
  display: inline-flex;
  align-items: center;
}
/* Scaled, not resized, so compaction costs no layout work. `inline-start` is
   not in the transform-origin grammar — it is silently dropped and the logo
   pulls in from both sides — so the axis is mirrored explicitly instead. */
.header__logo img {
  width: 190px;
  height: 62px;
  object-fit: contain;
  transform-origin: left center;
  transition: transform 180ms var(--ease);
}
[dir='rtl'] .header__logo img { transform-origin: right center; }
.header.is-compact .header__logo img { transform: scale(0.8); }

/* `gap` is transitioned here and on the aside, and nowhere else in the file:
   the bar loses 43px of height on scroll, and with only the logo and a shadow
   acknowledging it the row squashed rather than compacted. Closing the two
   intervals in step makes it read as one object tightening. Gap animation is
   layout work, but it is confined to a 5-item flex row that is already being
   re-laid-out by the height change in the same frame. */
.nav { display: flex; gap: calc(var(--s-lg) * var(--bar-rhythm)); margin-inline-end: auto; transition: gap 180ms var(--ease); }
/* Direct children only. The Leistungen panel hangs inside this nav, and its
   six links are prose in Manrope with sentence-case names — an unscoped
   descendant selector set the whole panel in 13px tracked uppercase and blew
   its second column out past the panel's own edge. */
.nav > a {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  white-space: nowrap;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: color 150ms var(--ease);
}
[dir='rtl'] .nav > a { text-transform: none; letter-spacing: normal; }
.nav > a:hover { color: var(--ember); }
.nav > a[aria-current='page'] { color: var(--ink); }
.nav > a[aria-current='page']::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: 7px;
  height: 2px;
  background: var(--ember);
}

/* --- Leistungen menu ----------------------------------------------------- */
/* A disclosure in the nav bar, not a link. There is no Leistungen overview
   page to navigate to, so the control that used to point at one now opens the
   six it would have listed. The trigger is a <button> because that is what it
   is: it changes the state of this page rather than requesting another one,
   and a screen reader is told so by the element, not by a label. */

.nav__has-menu { position: relative; }

.nav__trigger {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 48px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
  font-family: Inter, system-ui, sans-serif;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: color 150ms var(--ease);
}
[dir='rtl'] .nav__trigger { text-transform: none; letter-spacing: normal; }
.nav__trigger:hover,
.nav__trigger[aria-expanded='true'] { color: var(--ember); }

/* On a Leistung page the current item is inside this menu, so the bar needs a
   way to say so. It takes the same Ember underline the nav's links use for
   aria-current — but as a class, because the trigger is a button that opens a
   panel, not a link to the page you are on, and marking it aria-current would
   claim otherwise. The page itself is marked inside the panel. */
.nav__trigger.is-current { color: var(--ink); }
.nav__trigger.is-current::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: 7px;
  height: 2px;
  background: var(--ember);
}
.nav__trigger { position: relative; }
/* The chevron sits outside the label, so the underline would run under it and
   read as a wider word than it is. */
.nav__trigger.is-current::after { inset-inline-end: 21px; }

.menu__item[aria-current='page'] { background: var(--surface); }
.menu__item[aria-current='page'] .menu__name { color: var(--ember); }

.nav__chev { flex: none; transition: transform 220ms var(--ease); }
[aria-expanded='true'] .nav__chev { transform: rotate(180deg); }

/* The panel hangs from the header's own bottom edge, not from the 48px nav
   item that sits centred inside it — so the drop is the leftover half of the
   header's height, and it retunes when the header compacts rather than
   leaving the panel floating in the middle of the bar. */
.header { --menu-drop: 30px; }
.header.is-compact { --menu-drop: 8px; }

.menu {
  position: absolute;
  inset-block-start: calc(100% + var(--menu-drop));
  inset-inline-start: -12px;
  z-index: 110;
  /* Measured against the longest pair: "Entrümpelung & Haushaltsauflösung"
     over "Haushaltsgeräte anschließen und installieren." At 680px both wrapped
     and the six rows came out on three different heights; at 760 the grid
     settles. The nav only exists from 1260px up, where 760 always fits inside
     the container with the trigger's own offset subtracted. */
  inline-size: min(760px, calc(100vw - var(--gutter) * 2));
  padding: 12px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-menu);
  display: none;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 160ms var(--ease), transform 160ms var(--ease),
    display 160ms allow-discrete;
}
.menu[data-open='true'] {
  display: block;
  opacity: 1;
  transform: none;
  transition: opacity 200ms var(--ease), transform 200ms var(--ease),
    display 200ms allow-discrete;
}
@starting-style {
  .menu[data-open='true'] { opacity: 0; transform: translateY(-6px); }
}

.menu__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; }
/* A grid item's default min-width is auto, so a long Leistung name refuses to
   wrap and pushes its column past the panel's edge instead. */
.menu__grid > li { min-width: 0; }

.menu__item {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 14px;
  padding: 12px;
  border-radius: var(--r-sm);
  transition: background-color 150ms var(--ease);
}
.menu__item:hover { background: var(--surface); }
.menu__item:hover .menu__name { color: var(--ember); }
.menu__item svg { flex: none; color: var(--ink); margin-block-start: 1px; }

.menu__text { display: grid; gap: 3px; min-width: 0; }
.menu__name {
  font-family: Manrope, system-ui, sans-serif;
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
  transition: color 150ms var(--ease);
}
/* 16px, on the Muted floor — a nav panel is not licence to go smaller. */
.menu__desc { color: var(--muted); font-size: 1rem; line-height: 1.45; }

/* Someone who cannot tell whether their job is an Entrümpelung or a
   Haushaltsauflösung should not have to decide before they may ask. */
.menu__foot {
  margin-block-start: 12px;
  padding: 14px 12px 4px;
  border-block-start: 1px solid var(--line);
  color: var(--muted);
  font-size: 1rem;
}
.menu__foot a {
  font-weight: 700;
  color: var(--ink);
  margin-inline-start: 6px;
  transition: color 150ms var(--ease);
}
.menu__foot a:hover { color: var(--ember); }
.menu__hours { display: block; font-size: 1rem; }

/* --- Leistungen menu, inline (mobile overlay) ---------------------------- */
/* Same six, same icons, no floating panel: there is nowhere on a phone for one
   to float to, so the group opens in place and pushes the rest of the menu
   down. Height is animated rather than faded, because the thing the visitor
   needs to understand is that the list belongs to the row above it. */

.overlay__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-md);
  width: 100%;
  min-height: 48px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  text-align: start;
  font-family: Manrope, system-ui, sans-serif;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
  transition: color 150ms var(--ease);
}
.overlay__trigger[aria-expanded='true'] { color: var(--ember); }

.menu--inline {
  position: static;
  inline-size: auto;
  padding: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  display: grid;
  grid-template-rows: minmax(0, 0fr);
  opacity: 0;
  transform: none;
  overflow: hidden;
  /* visibility, not just height. The panel collapses with grid-template-rows
     and overflow:hidden while keeping display:grid, so without this its six
     links stay in the tab order as 49x342 boxes clipped out of paint: focus
     lands on them and no ring is drawn anywhere on screen. WCAG 2.4.7 is
     Level A. visibility transitions discretely and so animates cleanly
     alongside the rows. */
  visibility: hidden;
  transition: grid-template-rows 240ms var(--ease), opacity 200ms var(--ease),
    visibility 240ms allow-discrete, display 240ms allow-discrete;
}
.menu--inline[data-open='true'] {
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  opacity: 1;
  transform: none;
  visibility: visible;
}
/* The panel starts from display:none, so there is no previous computed style
   for the transition to leave from — these are it. Without the row entry the
   list would snap open at full height and only the opacity would animate. */
@starting-style {
  .menu--inline[data-open='true'] { grid-template-rows: minmax(0, 0fr); opacity: 0; }
}
/* The 0fr → 1fr transition needs the grid to hold exactly one child, which is
   why the six items are wrapped rather than being the grid's own rows: with
   six rows declared, the first collapses and the other five stay open. */
.menu--inline > .menu__list { min-height: 0; padding-block: 6px 10px; }
.menu--inline .menu__item {
  padding-inline-start: 2px;
  min-height: 48px;
  align-items: center;
}
.menu--inline .menu__name { font-size: 1rem; font-weight: 600; }

.header__aside { display: flex; align-items: center; gap: calc(var(--s-lg) * var(--bar-rhythm)); flex: none; transition: gap 180ms var(--ease); }

/* Ink, not Muted: at 13px this sits below the Sixteen-Pixel Floor, where Muted
   has no contrast headroom left. The current locale is marked the way the nav
   marks its current page — an Ember underline — not by colour alone. */
/* The pair is set in Manrope, not Inter: two two-letter codes are a headline
   in miniature, and the display face gives them the weight to hold their own
   beside the logo without growing. Tracking is nearly closed — the Label step's
   0.08em was drawn for words, and it pulls a two-letter code apart. */
.locale {
  display: flex;
  align-items: center;
  gap: 2px;
  font-family: Manrope, system-ui, sans-serif;
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.02em;
}
.locale a {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  min-width: 44px;
  padding-inline: 4px;
  color: var(--ink);
  transition: color 150ms var(--ease);
}
.locale a:hover { color: var(--ember); }
.locale a[aria-current='page']::after {
  content: '';
  position: absolute;
  inset-inline: 4px;
  inset-block-end: 12px;
  height: 2px;
  background: var(--ember);
}
/* The Arabic endonym is no longer set in type — the visible label is the code
   `AR`, and العربية lives in the accessibility tree so a screen reader still
   announces the language in its own script. That retires the
   provisional Arabic font stack that used to sit here: no Arabic face is
   loaded, and none is needed until /ar/ itself is built. */
/* Direct child only: the separator. The AR link now carries spans of its own
   (the visible code and the screen-reader endonym), and a descendant selector
   here painted the visible label in hairline grey. */
.locale > span { color: var(--line); }

.header .btn--whatsapp { min-height: 48px; padding: 14px 18px; }

.header__tel {
  display: none;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  white-space: nowrap;
  font-family: Manrope, system-ui, sans-serif;
  font-size: 1.0625rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  transition: color 150ms var(--ease);
}
.header__tel svg { flex: none; color: var(--ember); }
.header__tel:hover { color: var(--ember); }

@media (min-width: 1260px) {
  .header__tel { display: inline-flex; }
}

.burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 48px; height: 48px;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
}

/* --- Mobile nav overlay -------------------------------------------------- */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 150;
  background: var(--paper);
  padding: var(--s-lg);
  display: none;
  flex-direction: column;
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 180ms var(--ease), transform 180ms var(--ease),
    display 180ms allow-discrete;
}
.overlay[data-open='true'] {
  display: flex;
  opacity: 1;
  transform: none;
  transition: opacity 220ms var(--ease), transform 220ms var(--ease),
    display 220ms allow-discrete;
}
@starting-style {
  .overlay[data-open='true'] { opacity: 0; transform: translateY(-8px); }
}
.overlay__top { display: flex; align-items: center; justify-content: space-between; }
.overlay__close {
  width: 48px; height: 48px;
  display: grid; place-items: center;
  background: none; border: 1px solid var(--line); border-radius: var(--r-sm);
  cursor: pointer;
  transition: transform 140ms var(--ease);
}
.overlay__close:active { transform: scale(0.98); }
.overlay__nav {
  display: flex;
  flex-direction: column;
  gap: var(--s-sm);
  margin-block-start: var(--s-3xl);
}
/* Title scale, as documented. This read `var(--fs-phone-sm)`, a token that was
   never defined anywhere — the declaration was invalid, so every link silently
   inherited 16px and each tap target came out 26px tall. */
.overlay__nav a {
  display: flex;
  align-items: center;
  min-height: 48px;
  font-family: Manrope, system-ui, sans-serif;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

/* Pinned to the foot of the menu so the two contact channels are reachable
   from the one screen that used to have neither. Same colours and height as
   the Sofortkontakt bar, because it is the same promise. */
.overlay__contact {
  margin-block: auto calc(var(--s-lg) * -1);
  margin-inline: calc(var(--s-lg) * -1);
  display: flex;
}
.overlay__contact a {
  flex: 1;
  height: var(--bar-h);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  color: var(--paper);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: background-color 180ms var(--ease), transform 140ms var(--ease);
}
[dir='rtl'] .overlay__contact a { text-transform: none; letter-spacing: normal; font-weight: 700; }
.overlay__call { background: var(--ink); }
.overlay__wa { background: var(--green); }
.overlay__wa:hover { background: var(--green-deep); }
.overlay__contact a:active { transform: scale(0.98); }
.overlay__contact :focus-visible { outline-color: var(--paper); outline-offset: -4px; }

/* --- Hero ---------------------------------------------------------------- */
/* Full-bleed photograph. The copy sits on an opaque Paper plane laid over it,
   dissolving into the page ground behind the copy, exactly as the comp draws
   it: no panel, no edge, no corner, no boundary of any kind between the work
   and the words. The photograph and the page are one surface.

   The system's "no gradients anywhere" rule contradicts the approved hero,
   which has always been a feathered image. The mask below is that feather,
   and it is the one gradient the system carries. */

.hero {
  position: relative;
  background: var(--paper);
  isolation: isolate;
}

.hero__media { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
/* Each source is already cut to its slot, so object-position only nudges. */
.hero__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
/* Photographs never mirror in the RTL locale — see the Mirror, Don't Flip Rule. */

/* The feather. A mask, not an overlay: the photograph itself thins to nothing
   rather than being covered by a wash of white, so nothing sits on top of the
   image and the page ground shows through as itself. Physical directions with
   an RTL flip, since gradients take no logical keywords.
   The opaque stop is `--ink` only because a mask reads the alpha channel and
   ignores the colour entirely — any fully opaque value behaves identically, so
   it uses a documented token rather than introducing a literal.

   The photograph is CUT rather than full-bleed: it starts at 30% instead of
   running the whole width under the copy. That is the decision that made this
   solvable. Full-bleed, object-fit:cover left only 44px of horizontal travel,
   so every earlier attempt could only argue about where to erase the image —
   and the livery at 27.3% and the cartons at the far left were always on
   opposite sides of whatever line was drawn. In a 70% box there is real
   travel, so the frame can simply be placed.
   Four stops, weighted so the falloff is slow at the start and resolves by
   20% of the box. A straight two-stop ramp over the same distance reads as a
   visible diagonal seam, which is why the curve is weighted rather than linear.

   The ramp was 14% until 2026-08-16. The comment here used to claim "the copy
   ends at ~27% and the photograph begins at 30%, so the two never overlap" —
   that was true at 1440 and above and false where it mattered. At 1024 the
   copy column comes closest to the image, and the H1's right edge sat on a
   photograph that was **54% opaque**; the checks sat at 50%. It survived only
   because that region of this particular frame happens to be bright sky, which
   is not a property of the design, it is a property of one photograph.
   Stretching the same four stops over 20% instead of 14% drops the H1 to 26%
   and the checks to 24%, and costs four points of solid image (60% of the
   viewport to 56%).

   Chosen over two alternatives, both built and compared at 1024/1280/1440/1920
   before deciding. The full-bleed feather the system describes
   (transparent 34% -> 18% at 50% -> solid 62%) measured cleanest of all — 2%
   behind the H1 — but showed only 38% solid photograph, giving up 22 points of
   the one asset a competitor could not copy. It also does not deliver what
   those documents claim it does: they say the image "crosses behind the copy
   as a faint wash", and it cannot, because the mask stays fully transparent
   until 34% of the viewport while the copy column is capped at 352px and ends
   at 24% of 1440. No implementation satisfies that sentence at desktop widths;
   the sentence is what needs correcting.

   Retune this together with the crop; the two are one decision. */
.hero__media {
  -webkit-mask-image: linear-gradient(to right,
    transparent 0%,
    color-mix(in srgb, var(--ink) 14%, transparent) 7%,
    color-mix(in srgb, var(--ink) 58%, transparent) 13%,
    var(--ink) 20%);
  mask-image: linear-gradient(to right,
    transparent 0%,
    color-mix(in srgb, var(--ink) 14%, transparent) 7%,
    color-mix(in srgb, var(--ink) 58%, transparent) 13%,
    var(--ink) 20%);
}
[dir='rtl'] .hero__media {
  -webkit-mask-image: linear-gradient(to left,
    transparent 0%,
    color-mix(in srgb, var(--ink) 14%, transparent) 7%,
    color-mix(in srgb, var(--ink) 58%, transparent) 13%,
    var(--ink) 20%);
  mask-image: linear-gradient(to left,
    transparent 0%,
    color-mix(in srgb, var(--ink) 14%, transparent) 7%,
    color-mix(in srgb, var(--ink) 58%, transparent) 13%,
    var(--ink) 20%);
}

.hero__plane {
  position: relative;
  z-index: 1;
  padding-block: clamp(40px, 6vw, 72px) clamp(44px, 6vw, 76px);
  padding-inline: 0 clamp(32px, 4vw, 64px);
  /* The floor rises with width so the band does not flatten into a letterbox:
     a flat 720 left 2560 at 3.53:1 and cropped the photograph to 64% of its own
     height, which is where the boxes, plant and carpet sit. 37vw is the largest
     step that still leaves 1024-1920 untouched — it only overtakes 720 past
     1946. 78vh stays the outer guard so a short viewport never loses the fold. */
  min-height: min(78vh, max(720px, 37vw));
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.hero__grid {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
/* The hero's vertical intervals are deliberately NOT uniform. The badge sits
   hard against the H1 so the two read as one address line; the lede breathes;
   then a wide interval sets the four promises and the two actions apart as
   their own blocks. One multiplier scales that cadence, so the relationships
   hold if it is ever retuned — opened from 0.85 to 1.19 when the column was
   quieted, because air was doing the work that dimming would otherwise do. */
/* Cadence chosen in live mode (typeset, compressed ramp). The measure lives
   with the desktop rule below, which is the one that binds at ≥1024. */
.hero__inner { --rhythm: 1.06; max-width: 34rem; }

/* The pin is what makes this a place rather than a category label — the words
   alone read as a section tag. It is decorative: the text carries the meaning,
   so the glyph is aria-hidden and never the only thing saying "Dortmund". */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  background: var(--green);
  color: var(--paper);
  border-radius: var(--r-pill);
  padding: 8px 18px;
}
.badge svg { flex: none; inline-size: 16px; block-size: 16px; }
/* Tighter than the base badge: this one carries a glyph, and the stock 8/18
   padding left the pin swimming. */
.hero__inner > .badge {
  margin-block-end: calc(var(--s-sm) * var(--rhythm));
  padding: 6px 14px;
  /* Opened from the Label default: against a tighter column and a heavier
     check list the badge needs to read as the quietest thing in the stack,
     and tracking does that without leaving the documented size. */
  letter-spacing: 0.1em;
}
/* Every other tracked component in this file carries its own RTL reset next to
   it — .btn, .nav > a, .nav__trigger, .overlay__contact a, .field > label,
   .bar a. This one was missed, and the global `[dir='rtl'] .label` reset could
   not cover for it: both selectors weigh (0,2,0), and the badge rule above is
   ~660 lines later in the source, so it won on order. The 0.1em survived into
   Arabic on the first element an Arabic reader sees. The system calls tracking
   on Arabic "the fastest way to make a page look machine-made to a native
   reader" and lists it under Don't; it is also invisible to anyone who does
   not read the script, which is why it lasted. */
[dir='rtl'] .hero__inner > .badge { letter-spacing: normal; }

/* Two blocks, so the H1 is exactly the two authored lines. `balance` is
   off here: it re-flowed the authored break into three even lines and turned a
   statement plus its orange answer into a list of three adjectives. */
.hero .display { margin-block: 0; text-wrap: wrap; hyphens: manual; }
/* The two lines are no longer the same size. The setup steps back to Headline
   and turns Muted; the payoff keeps Display. Colour is no longer doing the
   separating on its own — structure is, and the promise is the largest thing
   in the viewport because it is the most important, not because it is orange.
   Both steps are the documented ones, copied from .headline and .display. */
.hero .display .line,
.hero .display .accent { display: block; }
.hero .display .line {
  /* Floors raised from 1.25rem/2rem now that the H1 copy is short. The old
     floors were set against "Schnell. Zuverlässig." — 21 characters that had
     to survive a 358px phone, which forced the setup down to 20px, three
     pixels off the subline beneath it. A heading three pixels above body copy
     is not a heading, and at 390px that is what most visitors saw.
     "Wir räumen aus." is 15 characters and renders 138px at 22px, so the
     constraint that bought the low floor is gone. 22/36 keeps a real step at
     390 and both lines still clear the 358px column with room to spare.

     The slope is 5.6vw, not the Headline step's 3.5vw, because the payoff
     climbs at 8.2vw and a shallower setup falls away from it in the middle of
     the range: at 600px the two rendered 49.2 and 22, a ratio of 2.24, with
     the setup pinned to its phone floor while the payoff was nearly at desktop
     size. 5.6vw holds the pair between 1.45 and 1.64 from 390 all the way up,
     which is the ratio the capped sizes (64/44) settle at anyway. */
  font-size: clamp(1.375rem, 5.6vw, 2.75rem);
  line-height: 1.15;
  letter-spacing: -0.015em;
  /* Ink, not Muted. Muted is the secondary-text colour — captions,
     hints, card descriptions — and this line now carries the service the
     company is found for, which is not secondary to anything. The system is
     explicit that Ink is the colour of headings on light grounds; the Muted
     H1 half was the deviation, not this. The payoff still leads on size and
     on being the only orange thing in the viewport. */
  color: var(--ink);
}
.hero .display .accent {
  font-size: clamp(2.25rem, 8.2vw, 4rem);
  color: var(--orange);
}

.hero__sub {
  margin-block-start: calc(var(--s-lg) * var(--rhythm));
  /* Ink, not Muted. The sub now sits partly over the photograph, where Muted
     has no contrast headroom at all; and with the compressed ramp it is doing
     more of the hierarchy's work, so it reads as copy rather than as caption.
     Measure in ch rather than rem so it tracks the size if that is retuned. */
  color: var(--ink);
  max-width: 30ch;
  line-height: 1.55;
  font-size: var(--fs-lede);
}

/* The checks are not orange. Checkmarks are listed among Signal Orange's
   uses, but the One Loud Thing rule says the colour stops working the
   moment it appears three times in one viewport — and the hero had it in six
   places. The orange is spent on the H1's payoff line and the Primary button,
   and nowhere else above the fold. Concentrated, not sprinkled. */
.checks { margin-block-start: var(--s-lg); display: grid; gap: 10px; }
/* Muted, not Ink: the four promises are support for the H1, not peers of it.
   16px is the Muted floor and these sit exactly on it. */
.hero__inner > .checks {
  margin-block-start: calc(var(--s-2xl) * var(--rhythm));
  gap: calc(10px * var(--rhythm));
  /* Capped for the same reason as .hero__sub, and to the same target: the
     livery begins at 27.3% of the frame, so the copy has to end before it.
     The H1 is deliberately not capped — it sits at y 13.5-34.5%, entirely
     above the livery's band at y 38.6-69.9%, so its width costs nothing. */
  max-width: 21rem;
}
.hero__inner > .checks li { color: var(--muted); }
/* Weight 500, not a size change. The accepted variant carried scale 0.95,
   which would have put these at 15.2px; 1rem is the body floor
   and this is running copy, not a caption, so the ramp's emphasis comes from
   weight instead. */
.checks li {
  display: flex;
  align-items: start;
  gap: 10px;
  font-size: 1rem;
  font-weight: 500;
  /* "Faire Festpreise – keine versteckten Kosten" needs 361px on one line and
     a 390px phone affords 358, so it wraps — and the default break dropped the
     single word "Kosten" onto the second line, orphaning the noun the promise
     turns on. balance splits it evenly instead. Inherited, so it reaches the
     anonymous block the text forms inside this flex row. */
  text-wrap: balance;
}

/* A filled disc with the tick knocked out in Paper. The disc shape was chosen
   live over an ink hairline and two lighter fills, and that decision stands —
   only the fill has changed, from WhatsApp Green to Ink.

   The green was a deliberate amendment to the "Green Is Borrowed, Not
   Owned" rule, on the argument that green could carry two jobs above the fold:
   the WhatsApp affordance and the confirmed promise. Measured, it could not.
   At 1440 green appeared SEVEN times in the first viewport — badge, four
   discs, hero WhatsApp, header WhatsApp — against orange's two. A colour the
   system reserves as a borrowed platform signal had become the most repeated
   thing on the screen, which is the precise failure the rule describes: the
   WhatsApp button stops reading as tap-to-chat and starts reading as a second
   brand colour. The rule also names its own two previous breaches, and a code
   comment cannot unilaterally amend a settled document.

   Ink is not a fallback here, it is what the system specifies: Signal Orange's
   entry ends "Not the hero checkmarks — those are Ink." White on Ink measures
   16.4:1. Green in the hero is now the badge and the WhatsApp button, which is
   all the rule ever allowed.

   The disc is centred on the first line box arithmetically, so it holds if the
   type size or leading moves — the old hard 3px nudge did not. */
.checks svg {
  flex: none;
  inline-size: 20px;
  block-size: 20px;
  padding: 2px;
  color: var(--paper);
  background: var(--ink);
  border-radius: var(--r-pill);
  margin-block-start: calc((1lh - 20px) / 2);
}

.hero__cta { margin-block-start: var(--s-xl); display: flex; flex-wrap: wrap; gap: var(--s-md); }
.hero__inner > .hero__cta { margin-block-start: calc(var(--s-2xl) * var(--rhythm)); }

/* Shown only below 768px — see the responsive block. */
.hero__tel { display: none; }

/* --- Hero entrance ------------------------------------------------------- */
/* There isn't one, and that is the decision.
   Removed 2026-08-16: a `hero-rise` stagger, 520ms with delays from 40ms to
   410ms, settling roughly 930ms after paint.
   It contradicted the system that documents it. The system, under Buttons and
   Easing: "There is no second curve and no entrance animation anywhere: the
   visitor may be having the worst week of their life, and a page that performs
   at them is a page that costs them time." The surface brief lists "no scroll-reveal on
   the hero" among its anti-goals. Both were written before the stagger, and
   neither was amended when it arrived.
   It was also self-defeating. The two elements it choreographed toward — the
   actions at 360ms and 410ms — are below the fold at 390, 768 and 1024, so it
   spent most of a second performing toward a payoff nobody could see, on a
   page whose entire job is a phone call from someone having a bad week.
   The page now paints its hero and stops. If motion is ever wanted here, the
   thing to animate is a state change, not an arrival.
   The `.js` flag existed only to scope this and is now unused. */

/* --- Cards --------------------------------------------------------------- */

.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s-lg);
}
.grid-4 {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--s-lg);
}

.card {
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  transition: transform 180ms var(--ease), box-shadow 180ms var(--ease);
}
.card:hover { transform: translateY(-2px); box-shadow: var(--sh-hover); }
.card:hover .card__more svg { transform: translateX(4px); }
[dir='rtl'] .card:hover .card__more svg { transform: scaleX(-1) translateX(4px); }

.card__media { position: relative; aspect-ratio: 16 / 10; background: var(--surface); }
/* Slow, and slower than every other transition in the file on purpose: a
   photograph that answers a pointer at button speed reads as a UI element,
   and at this speed it reads as the card leaning in. The card's own
   overflow:hidden does the cropping. */
.card__media img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 640ms var(--ease);
}
.card:hover .card__media img { transform: scale(1.04); }

.card__body { padding: var(--s-lg); display: flex; flex-direction: column; flex: 1; gap: 10px; }
.card__desc { color: var(--muted); font-size: 1rem; }
.card__more {
  margin-block-start: auto;
  padding-block-start: var(--s-sm);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  font-weight: 600;
  font-size: 1rem;
  transition: color 180ms var(--ease);
}
.card:hover .card__more { color: var(--ember); }
.card__more svg { transition: transform 180ms var(--ease); }
[dir='rtl'] .card__more svg { transform: scaleX(-1); }

/* --- Warum Blitz --------------------------------------------------------- */

.why {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--s-lg);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.why svg { color: var(--ink); margin-block-end: 6px; }
.why p { color: var(--muted); font-size: 1rem; }

.why--action { position: relative; transition: border-color 180ms var(--ease); }
.why--action .title { transition: color 180ms var(--ease); }
.why--action .title a { color: inherit; }
.why--action .title a::after { content: ''; position: absolute; inset: 0; z-index: 1; }
.why--action:hover { border-color: var(--ink); }
.why--action:hover .title { color: var(--ember); }

/* --- Vertrauensleiste ---------------------------------------------------- */

.trust { background: var(--ink); color: var(--paper); padding-block: clamp(48px, 6vw, 72px); }
.trust__grid { display: grid; grid-template-columns: repeat(4, 1fr); }
.trust__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  padding-inline: var(--s-md);
  border-inline-start: 1px solid var(--line-inv);
}
.trust__item:first-child { border-inline-start: 0; }
.trust__item svg { color: var(--orange); }
.trust__item .numeral { color: var(--orange); }
.trust__item .label { color: var(--paper); }

.trust__text {
  font-family: Manrope, system-ui, sans-serif;
  font-size: clamp(1.0625rem, 1.7vw, 1.25rem);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--paper);
  text-wrap: balance;
}

/* Licence + insurance slots. They render nothing until content is supplied —
   no placeholder, no outline, no reserved space that collapses visibly. */
.trust__badges:empty { display: none; }

/* --- So funktioniert's --------------------------------------------------- */

.steps { border-block-start: 1px solid var(--line); }
.step {
  display: grid;
  grid-template-columns: auto auto 1fr;
  align-items: start;
  gap: var(--s-lg);
  padding-block: var(--s-xl);
  border-block-end: 1px solid var(--line);
}
.step__n {
  display: grid;
  place-items: center;
  width: 40px; height: 40px;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: var(--paper);
  font-family: Manrope, sans-serif;
  font-weight: 800;
  font-size: 1rem;
  flex: none;
}
.step__icon { color: var(--ink); flex: none; margin-block-start: -6px; }
.step__body { max-width: 50ch; display: flex; flex-direction: column; gap: 8px; }
.step__body p { color: var(--muted); }

.steps__foot { padding-block-start: var(--s-xl); }
.textlink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--ember);
  font-weight: 600;
  border-block-end: 1px solid transparent;
  transition: border-color 150ms var(--ease);
}
.textlink:hover { border-block-end-color: var(--ember); }
[dir='rtl'] .textlink svg { transform: scaleX(-1); }

/* --- Schluss-CTA --------------------------------------------------------- */
/* The second and last ink band on the page. It closes the homepage where the
   Anfrage form used to, and it is a decision point rather than a task: one
   Primary that leaves for /kontakt, and the number underneath for the visitor
   who was never going to type anything. */

.cta {
  background: var(--ink);
  color: var(--paper);
  padding-block: clamp(64px, 8vw, 104px);
  border-block-end: 1px solid var(--line-inv);
}
.cta__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(240px, 18rem);
  gap: var(--s-2xl);
  align-items: start;
}
.cta__copy { max-width: 40rem; }
/* Signal Orange, not Ember: on ink the darkened rendition is the wrong one.
   #EB642D on #1F1F23 measures 5.0:1 — the Ember Rule is a light-ground rule. */
.cta .eyebrow { color: var(--orange); }
.cta .headline { margin-block-start: var(--s-md); }
.cta__lede {
  margin-block-start: var(--s-md);
  color: var(--paper-dim);
  font-size: var(--fs-lede);
  max-width: 42ch;
}

.cta__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-md);
  text-align: start;
}
.cta__tel a {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: Manrope, system-ui, sans-serif;
  font-size: var(--fs-phone);
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.01em;
  transition: color 150ms var(--ease);
  min-height: 44px;
}
.cta__tel a:hover { color: var(--orange); }
.cta__tel svg { flex: none; color: var(--orange); }
.cta__hours { display: block; color: var(--paper-dim); font-size: 1rem; }

.cta .btn--primary:hover { background: var(--paper); color: var(--ink); }

.cta :focus-visible { outline-color: var(--paper); }

/* The band and the footer are both ink, so the footer gives back the padding
   it would otherwise stack on top of the band's own — 104 + 96 of dead ink
   between the closing sentence and the first footer link. */
main:has(.cta) + .footer { padding-block-start: var(--s-3xl); }

/* --- Page hero (inner pages) --------------------------------------------- */
/* Every page that is not the homepage opens on this instead of the
   photographic hero: eyebrow, H1, one line of lede, on Surface.
   The H1 takes the documented Headline step, not a new one. An earlier pass
   gave it an invented step between Headline and Display, on the reasoning that
   a page title should outrank a section heading — but the pages that carry
   this have no section H2 for it to outrank, so the step bought nothing and
   cost the type ramp its authority. What separates this from a section
   heading is the Surface ground, the eyebrow above it, and the padding
   around it. Display stays what the system says it is: the homepage hero,
   once. */

.pagehero { background: var(--surface); padding-block: clamp(48px, 7vw, 88px); }
.pagehero__inner { max-width: 46rem; }
.pagehero h1 { margin-block-start: var(--s-md); }
.pagehero .lede { margin-block-start: var(--s-md); max-width: 42ch; }

/* --- Leistung page ------------------------------------------------------- */
/* One template, six instances. Its architecture is deliberately
   not the homepage's: the homepage is a stack of full-width bands a visitor
   scrolls through once, and a Leistung page is a document somebody reads with
   a decision in mind. So the middle of the page is a two-column read — the
   scope, the situations and the Ablauf in the main column, and a rail beside
   them that stays put while they scroll.
   The rail carries no Primary button. Its job is the Sofortkontakt promise —
   the number and the tap-to-chat, always in reach — and a second orange
   button competing with the hero's would spend the One Loud Thing twice. */

/* --- The sheet: masthead + document ------------------------------------- */
/* The document opens in the first viewport. There is no hero band and no
   photograph above the fold: the masthead and the first line items share one
   screen, because this page is a job sheet rather than a brochure spread. */

.sheet { padding-block: clamp(32px, 4vw, 56px) clamp(56px, 7vw, 96px); }
.sheet__cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
}
.sheet__main { min-width: 0; }
/* Display, not Headline. This is the page title on the page that IS this
   Leistung; at Headline it tied with three section H2s and the service name
   had no rank of its own. */
.sheet h1 { margin-block-start: var(--s-lg); }
.sheet__promise {
  margin-block-start: var(--s-md);
  font-size: var(--fs-lede);
  line-height: 1.6;
  color: var(--muted);
  max-width: 50ch;
}
.sheet__cta { margin-block-start: var(--s-xl); display: flex; flex-wrap: wrap; gap: var(--s-md); }

.sheet__where {
  display: none;
  margin-block-start: var(--s-md);
  align-items: start;
  gap: 10px;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.5;
}
.sheet__where svg { flex: none; color: var(--muted); margin-block-start: calc((1lh - 16px) / 2); }

@media (max-width: 1023px) {
  .sheet__where { display: flex; }
}

@media (max-width: 479px) {
  .sheet__cta { display: grid; }
  .sheet__cta .btn { justify-content: center; }
}

/* Breadcrumb. Two levels only — there is no Leistungen overview page to sit
   between the homepage and here, so inventing a middle crumb would point at
   something that does not exist. */
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 1rem; overflow-wrap: break-word; }
/* Separator and leaf wrap as one unit, so the slash never dangles at a line end. */
.crumbs__leaf { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.crumbs a { color: var(--muted); transition: color 150ms var(--ease); }
.crumbs a:hover { color: var(--ember); }
.crumbs span[aria-hidden] { color: var(--line); }
.crumbs [aria-current] { color: var(--ink); }

/* The printed form's field header: a label, and what the document counts.
   These are field labels, not decorative kickers — they name the value under
   them the way a job sheet does. The section eyebrows that sat above the
   Ablauf and FAQ headlines are gone; those headings carry their own weight. */
.doc__h {
  display: flex;
  align-items: baseline;
  gap: var(--s-md);
  margin-block-start: var(--s-2xl);
  padding-block-end: var(--s-sm);
  border-block-end: 1px solid var(--ink);
}
.doc__h--no-border { border-block-end: 0; }
.doc__h h2 { color: var(--ember); font-size: 1.25rem; }
/* Positions — line items on a quotation. The numbers carry information here
   (this is what the document counts), so they are not decorative step chrome.
   A hairline between rows is all the structure a scanned list needs. */
.pos li {
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr);
  gap: var(--s-md);
  padding-block: 16px;
  border-block-end: 1px solid var(--line);
  align-items: baseline;
}
.pos__n {
  font-family: Manrope, system-ui, sans-serif;
  font-weight: 800;
  font-size: 1rem;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}
/* Measure held at 54ch, which renders 68–73 characters. `ch` is the width of
   the digit zero and runs ~1.3× wider than an average lowercase character, so
   the earlier 68ch cap was still delivering ~90 against a 65–75 target. */
.pos li:last-child { border-block-end: 0; }
.pos li > span:last-child,
.pos li > div { max-width: 54ch; overflow-wrap: break-word; }
.pos--body li { padding-block: var(--s-lg); align-items: start; }
.pos--body p { margin-block-start: 4px; color: var(--muted); max-width: 54ch; }
.pos--wide {
  margin-block-start: var(--s-2xl);
  border-block-start: 1px solid var(--ink);
  max-width: calc(3rem + var(--s-md) + 54ch);
}

/* The document's one set-apart row: the sentence written for the reader who
   is having the worst week of their life. It used to be item 2 of a four-row
   definition list in Muted at 16px. It is separated by rules and by scale —
   never by a coloured left border, which this system bans above 1px. */
.note {
  margin-block: var(--s-2xl);
  padding-block: var(--s-lg);
  border-block-start: 1px solid var(--ink);
}
.note__k {
  font-family: Manrope, system-ui, sans-serif;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
}
.note__t {
  margin-block-start: 12px;
  font-size: 1.25rem;
  line-height: 1.5;
  max-width: 50ch;
}

/* The scope boundary. Kleinabbruch is deliberately not structural demolition
   and Elektro-Service is deliberately not Elektroinstallation — saying so
   plainly is the single most credible thing either page does, so it gets a
   block of its own rather than a footnote. Surface and a hairline; no orange,
   no warning colour. It is a fact, not an alarm. */
/* A ruled row, not a box. Surface fill plus a border plus a radius made this
   the only card on a page whose whole thesis is that it has none — and the same
   content shipped as a plain paragraph on Entrümpelung, so the six siblings
   disagreed with each other about what a boundary statement looks like. */
.limit {
  margin-block: var(--s-2xl);
  padding-block: var(--s-lg);
  border-block: 1px solid var(--line);
}
.limit .title { color: var(--ink); margin-block-end: 4px; }
.limit p { color: var(--muted); max-width: 54ch; }

/* --- The tear-off stub --------------------------------------------------- */
/* Part of the document rather than a card floating beside it: a perforated
   rule separates it, and it sticks from 1024 where there are two columns to
   be sticky inside of. Below 1024 the Sofortkontakt bar carries the same two
   channels, so there is no width at which the number is out of reach — the
   previous build lost it entirely between 768 and 1023.
   The stub carries no Primary button: its job is the Sofortkontakt promise,
   and a second orange button would spend the One Loud Thing twice. */
.stub {
  border-inline-start: 1px dashed var(--line);
  padding-inline-start: var(--s-lg);
}
@media (min-width: 1024px) {
  .stub { position: sticky; inset-block-start: calc(var(--header-h-compact) + var(--s-lg)); }
}
.stub__phone {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  margin-block-start: var(--s-sm);
  min-height: 44px;
  font-family: Manrope, system-ui, sans-serif;
  font-size: var(--fs-phone);
  font-weight: 800;
  letter-spacing: -0.01em;
  transition: color 150ms var(--ease);
}
.stub__phone:hover { color: var(--ember); }
.stub__phone svg { flex: none; color: var(--ember); }
.stub__promises {
  display: grid;
  gap: 10px;
  margin-block-start: var(--s-lg);
  padding-block-start: var(--s-lg);
  border-block-start: 1px solid var(--line);
}
.stub__promises li { display: flex; align-items: start; gap: 10px; color: var(--muted); }
/* Ink, not green. The system names checkmarks Ink explicitly, and green here is
   neither a WhatsApp affordance nor the location badge — the Green Is Borrowed,
   Not Owned Rule. The borrowed colour was spreading a mark at a time. */
.stub__promises svg {
  flex: none;
  inline-size: 20px; block-size: 20px;
  color: var(--ink);
  margin-block-start: calc((1lh - 20px) / 2);
}
.stub__where {
  display: flex;
  align-items: start;
  gap: 10px;
  margin-block-start: var(--s-lg);
  padding-block-start: var(--s-lg);
  border-block-start: 1px solid var(--line);
  color: var(--muted);
}
/* Muted, not green. Green is the WhatsApp affordance and the hero's location
   badge; a pin glyph in the stub is neither, and the borrowed colour was
   spreading across the page one small mark at a time. */
.stub__where svg { flex: none; color: var(--muted); margin-block-start: calc((1lh - 16px) / 2); }

/* --- Das Ergebnis -------------------------------------------------------- */
/* One photograph per page, and it sits below the read rather than above the
   H1. The visitor arriving from a flat full of a dead relative's belongings
   already knows what the mess looks like. */
/* Hairline, because this band shares Paper with the sheet above it and the
   Alternating Ground Rule allows a rule where it cannot alternate a ground. */
.result { padding-block: clamp(48px, 6vw, 80px); border-block-start: 1px solid var(--line); }
/* Capped at the source's natural width. Full-container it rendered 1152px
   from an 880px file at 1440 — a 1.31x upscale on the page's only photograph.
   When real derivatives exist this cap lifts and a srcset replaces it. */
.result__fig { margin: 0 auto; max-width: 880px; }
/* No aspect-ratio override. The sources are 880x550 (8:5) and the width/height
   attributes reserve exactly that, so the box before load is the box after it.
   A 16/9 rule here reserved ~55px more than the band finally occupied — a
   layout shift on a page whose budget names CLS < 0.1 as a ranking factor. */
.result__img {
  inline-size: 100%;
  block-size: auto;
  border-radius: var(--r-md);
}
/* The caption describes what is in frame. These are stand-in photographs, and
   a caption that claimed a swept room over a picture of a wrecked one would be
   the same defect as the alt text that used to. */
.result__cap {
  margin-block-start: var(--s-md);
  color: var(--muted);
  max-width: 54ch;
}

.result__fig--pair { max-width: 1152px; }

.result__pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-lg);
}

.result__shot {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.result__tag { color: var(--ink); }

@media (max-width: 767px) {
  .result__pair { grid-template-columns: 1fr; gap: var(--s-xl); }
}

/* --- Preise -------------------------------------------------------------- */
/* Never a number. The section exists to answer "what will this
   cost me" with the mechanism instead of a figure nobody could honour. */

/* --- FAQ ----------------------------------------------------------------- */
/* Native <details>. It is keyboard-operable, screen-reader-announced and
   open-by-default-printable without a line of script, and the FAQPage
   structured data on the page describes exactly what is in the markup. */

.faq { border-block-start: 1px solid var(--line); max-width: 54ch; }
.faq details { border-block-end: 1px solid var(--line); }
.faq summary {
  padding-block: var(--s-md);
  min-height: 48px;
  cursor: pointer;
  list-style: none;
}
/* Title. The system names FAQ questions as a Title-step role explicitly. */
.faq summary h3 {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--s-md);
  margin: 0;
  font-family: Manrope, system-ui, sans-serif;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: -0.01em;
  transition: color 150ms var(--ease);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover h3 { color: var(--ember); }
.faq summary svg { flex: none; margin-block-start: 3px; transition: transform 220ms var(--ease); }
.faq details[open] summary svg { transform: rotate(180deg); }
.faq details p { padding-block-end: var(--s-md); color: var(--muted); max-width: 54ch; }

/* --- Weitere Leistungen -------------------------------------------------- */
/* With no Leistungen overview page, this row is the only lateral move between
   the six. It was a row of five tiles with no border, no ground and no radius
   at rest, hovering to a Surface 3% off the Paper behind it — five navigation
   targets that did not look like targets. It is now a ruled index in the
   document's own grammar, with a trailing arrow doing the affordance. */

.siblings {
  display: grid;
  grid-template-columns: 1fr;
  margin-block-start: var(--s-xl);
  max-width: 54ch;
}
.siblings li { border-block-end: 1px solid var(--line); }
.siblings .menu__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  padding-block: 16px;
  min-height: 56px;
  transition: color 150ms var(--ease);
}
.siblings .menu__item .siblings__go {
  color: var(--ember);
  margin-block-start: 0;
  transition: transform 150ms var(--ease);
}
[dir='rtl'] .siblings__go { transform: scaleX(-1); }
.siblings .menu__item:hover { color: var(--ember); }
.siblings .menu__item:hover .siblings__go { transform: translateX(4px); }
[dir='rtl'] .siblings .menu__item:hover .siblings__go { transform: scaleX(-1) translateX(4px); }
.siblings .menu__name { font-size: 1rem; }

/* --- Inhaltsseite -------------------------------------------------------- */
/* The one page type whose body is written in the editor rather than composed
   from fields, so nothing below can assume which tags arrive or in what order.
   There is no panel, no second ground and no aside: the column is the page and
   is sized to its measure rather than to the container. Space is what separates
   it from the header, which is the only separator a page of plain text needs. */

.folio { padding-block: clamp(32px, 4vw, 56px) clamp(56px, 7vw, 96px); }

.folio__inner { max-inline-size: 50ch; }

.folio__title {
  margin-block-start: var(--s-lg);
  text-wrap: balance;
  overflow-wrap: break-word;
}

/* --- Inhaltsverzeichnis -------------------------------------------------- */
/* Derived from the H2s the editor wrote, so there is nothing to keep in sync.
   It renders from three headings up: below that the reader can see the whole
   document without it, and a two-item index is furniture. */

.toc {
  margin-block-start: var(--s-xl);
  padding-block: var(--s-lg);
  border-block: 1px solid var(--line);
}

.toc__list {
  margin-block-start: var(--s-sm);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 var(--s-lg);
}

/* A jump link is a navigation target, so it carries a real tap area — 26px of
   line box plus 9px either side clears 44px without opening the block up. */
.toc__list a {
  display: block;
  padding-block: 9px;
  color: var(--ink);
  border-block-end: 1px solid transparent;
  transition: color 150ms var(--ease);
}
.toc__list a:hover { color: var(--ember); }

@media (max-width: 639px) {
  .toc__list { grid-template-columns: 1fr; }
}

/* --- Fliesstext ---------------------------------------------------------- */
/* The reset at the top of this file strips margins and list markers from every
   element, which is right for composed pages and wrong for authored HTML. This
   layer gives them back — inside `.prose` only, so nothing else on the site
   inherits a browser default by accident. Every step here is one of the eight
   on the documented ramp: H2 is Title, H3 is Manrope at the Lede size, H4 is a
   Label. Nothing new was added to the scale. */

.prose {
  max-inline-size: 50ch;
  margin-block-start: var(--s-xl);
  overflow-wrap: break-word;
  hyphens: auto;
}

.prose > * + * { margin-block-start: var(--s-md); }
.prose > :first-child { margin-block-start: 0; }

/* Without an index between them the title lands straight on the first section
   heading, which needs the wider interval the index otherwise supplies. */
.folio__title + .prose { margin-block-start: var(--s-2xl); }

/* A rule opens each section, in the same grammar as the ledger rows on a
   Leistung page. The first heading inherits the index's own bottom rule. */
.prose h2 {
  position: relative;
  margin-block-start: var(--s-2xl);
  padding-block-start: var(--s-xl);
  border-block-start: 1px solid var(--line);
  font-family: Manrope, system-ui, sans-serif;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.prose > h2:first-child {
  margin-block-start: 0;
  padding-block-start: 0;
  border-block-start: 0;
}

/* The page's one authored moment. Arriving from the index leaves a mark on the
   section rule so the reader can see which of them they landed on; it is drawn
   over the hairline rather than beside it, and it stays. */
.prose h2::before {
  content: '';
  position: absolute;
  inset-block-start: -1px;
  inset-inline-start: 0;
  inline-size: 40px;
  block-size: 2px;
  background: var(--ember);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 320ms var(--ease);
}
[dir='rtl'] .prose h2::before { transform-origin: right center; }
.prose h2:target::before { transform: scaleX(1); }
.prose > h2:first-child::before { content: none; }

.prose h3 {
  margin-block-start: var(--s-xl);
  font-family: Manrope, system-ui, sans-serif;
  font-size: var(--fs-lede);
  font-weight: 700;
  line-height: 1.4;
}

.prose h4 {
  margin-block-start: var(--s-lg);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
[dir='rtl'] .prose h4 { text-transform: none; letter-spacing: normal; font-weight: 700; }

.prose h2 + *,
.prose h3 + *,
.prose h4 + * { margin-block-start: var(--s-sm); }

.prose strong, .prose b { font-weight: 600; }

.prose a {
  color: var(--ember);
  font-weight: 600;
  border-block-end: 1px solid var(--line);
  transition: border-color 150ms var(--ease);
}
.prose a:hover { border-block-end-color: var(--ember); }

.prose ul, .prose ol { padding-inline-start: 1.4em; }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li::marker { color: var(--muted); font-variant-numeric: tabular-nums; }
.prose li + li { margin-block-start: var(--s-sm); }
.prose li > ul, .prose li > ol { margin-block-start: var(--s-sm); }

.prose blockquote {
  padding-inline-start: var(--s-lg);
  border-inline-start: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-lede);
}

.prose hr {
  border: 0;
  border-block-start: 1px solid var(--line);
  margin-block: var(--s-xl);
}

/* Every table the editor inserts arrives wrapped, including the ones pasted as
   bare HTML — the wrapper is what lets a table wider than the measure scroll
   instead of pushing the sheet open. */
.prose figure.table {
  max-inline-size: 100%;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
}

.prose table {
  inline-size: 100%;
  border-collapse: collapse;
  text-align: start;
}

.prose th, .prose td {
  padding-block: 12px;
  padding-inline: 0 var(--s-md);
  border-block-end: 1px solid var(--line);
  text-align: start;
  vertical-align: top;
}
.prose th:last-child, .prose td:last-child { padding-inline-end: 0; }

.prose th {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
[dir='rtl'] .prose th { text-transform: none; letter-spacing: normal; font-weight: 700; }

.prose figure.image { margin-block-start: var(--s-lg); }
.prose img { border-radius: var(--r-md); }
.prose figcaption {
  margin-block-start: var(--s-sm);
  color: var(--muted);
  font-size: 1rem;
}

.prose code {
  font-family: ui-monospace, monospace;
  font-size: 0.9375em;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 1px 5px;
}

.prose sub, .prose sup { line-height: 0; }

@media (prefers-reduced-motion: reduce) {
  .prose h2::before { transition-duration: 0s; }
}

/* --- Kontakt ------------------------------------------------------------- */

.contact__cols {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--s-2xl);
  align-items: start;
}

.field { display: flex; flex-direction: column; gap: 6px; margin-block-end: var(--s-md); }
.field > .fieldname,
.field > label {
  color: var(--ink);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
[dir="rtl"] .field > .fieldname,
[dir="rtl"] .field > label { text-transform: none; letter-spacing: normal; font-weight: 700; }
.field > .fieldname .muted,
.field > label .muted { text-transform: none; letter-spacing: normal; }
.field .req { color: var(--ember); }

/* What the asterisk means, said once, where it is first seen. */
.formnote {
  color: var(--muted);
  font-size: 1rem;
  margin-block-end: var(--s-lg);
}
.formnote .req { color: var(--ember); }

/* Guidance that has to survive the first keystroke, so not a placeholder. */
.hint {
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.5;
  max-inline-size: 54ch;
}

/* Service questions. The server renders every group hidden and disabled except
   the one already chosen, so with scripting off nothing the visitor never saw
   reaches the mailbox; the script un-disables the group the select points at. */
.qgroup { border: 0; padding: 0; margin: 0; min-inline-size: 0; }
.qgroup .field:last-child { margin-block-end: var(--s-md); }
.qgroup [hidden] { display: none; }
.qgroup[hidden] { display: none; }
.qgroup > legend { display: block; width: 100%; }

.qgroup .field > label,
.qgroup .field--radio > legend {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 600;
  line-height: 1.4;
}

.formgroup + .formgroup {
  margin-block-start: var(--s-2xl);
  padding-block-start: var(--s-xl);
  border-block-start: 1px solid var(--line);
}
.formgroup__head {
  font-family: Manrope, system-ui, sans-serif;
  font-size: 1.125rem;
  font-weight: 800;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin-block-end: var(--s-lg);
}
.formgroup__head--sub {
  font-size: 1rem;
  margin-block: var(--s-xl) var(--s-md);
  padding: 0;
}

.formerrors {
  border: 1px solid var(--ember);
  border-radius: var(--r-md);
  background: var(--paper);
  padding: var(--s-lg);
  margin-block-end: var(--s-lg);
}
.formerrors__count {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ember);
  font-size: 1rem;
  font-weight: 700;
}
.formerrors__count svg { flex: none; }
.formerrors__list {
  list-style: none;
  display: grid;
  gap: 8px;
  margin-block-start: 10px;
  padding-inline-start: 28px;
}
.formerrors__list a {
  display: inline-block;
  padding-block: 2px;
  color: var(--ember);
  border-block-end: 1px solid var(--line-control);
}
.formerrors__list a:hover { border-block-end-color: var(--ember); }

.assure {
  list-style: none;
  display: grid;
  gap: 10px;
  margin-block: var(--s-lg);
}
.assure li {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--ink);
  font-size: 1rem;
  line-height: 1.4;
}
.assure svg { flex: none; }

.fieldnote {
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.5;
  max-inline-size: 54ch;
}

/* A yes/no question is a radio group, so it needs a legend rather than a label —
   which then has to be dressed as one. */
.field--radio { border: 0; padding: 0; min-inline-size: 0; }
.field--radio > legend {
  padding: 0;
  margin-block-end: 6px;
  color: var(--ink);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
[dir="rtl"] .field--radio > legend { text-transform: none; letter-spacing: normal; font-weight: 700; }
.field--radio > legend .muted { text-transform: none; letter-spacing: normal; }

.radios { display: flex; flex-wrap: wrap; gap: 8px 22px; }
.radio {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-block: 12px;
  cursor: pointer;
}
.radio:hover input { border-color: var(--ink); }
.radio input {
  appearance: none;
  flex: none;
  width: 20px; height: 20px;
  border: 1px solid var(--line-control);
  border-radius: 50%;
  background: var(--paper);
  cursor: pointer;
  transition: background-color 150ms var(--ease), border-color 150ms var(--ease), box-shadow 150ms var(--ease);
}
.radio input:checked {
  border-color: var(--ember);
  background: var(--ember);
  box-shadow: inset 0 0 0 4px var(--paper);
}
.field--error .radio input { border-color: var(--ember); }

.input, .select, .textarea {
  width: 100%;
  background: var(--paper);
  border: 1px solid var(--line-control);
  border-radius: var(--r-sm);
  padding: 14px 16px;
  /* 16px minimum: iOS Safari zooms the viewport on focus below this. */
  font-family: Inter, system-ui, sans-serif;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink);
  transition: border-color 150ms var(--ease);
}
.input, .select { height: 52px; }
.textarea { min-height: 132px; resize: vertical; }
.input:focus, .select:focus, .textarea:focus { border-color: var(--ember); }
.input::placeholder, .textarea::placeholder { color: var(--muted); }
.input--zahl { max-inline-size: 8rem; appearance: textfield; }
.input--zahl::-webkit-outer-spin-button,
.input--zahl::-webkit-inner-spin-button { appearance: none; margin: 0; }

.select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  padding-inline-end: 44px;
}
[dir='rtl'] .select { background-position: left 16px center; padding-inline-end: 44px; }

.field--error .input,
.field--error .select,
.field--error .textarea { border-color: var(--ember); }

.err {
  display: none;
  opacity: 0;
  transform: translateY(-2px);
  align-items: center;
  gap: 6px;
  color: var(--ember);
  font-size: var(--fs-micro);
  line-height: 1.4;
  transition: opacity 160ms var(--ease), transform 160ms var(--ease),
    display 160ms allow-discrete;
}
.field--error .err {
  display: flex;
  opacity: 1;
  transform: none;
}
@starting-style {
  .field--error .err { opacity: 0; transform: translateY(-2px); }
}
.err svg { flex: none; }

.filefield {
  border: 1px dashed var(--line-control);
  border-radius: var(--r-sm);
  padding: var(--s-md);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  background: var(--paper);
}
.filefield__btn {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  border: 1px solid var(--ink);
  background: var(--paper);
  border-radius: var(--r-sm);
  padding: 9px 16px;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color 150ms var(--ease), color 150ms var(--ease),
    transform 140ms var(--ease);
}
[dir='rtl'] .filefield__btn { text-transform: none; letter-spacing: normal; font-weight: 700; }
.filefield__btn:hover { background: var(--ink); color: var(--paper); }
.filefield__btn:active { transform: scale(0.98); }

/* The real input is .vh, so its focus ring lands on a 1px box clipped to
   nothing. The visible button is a <label> and cannot take focus itself. */
.filefield:has(#f-fotos:focus-visible) .filefield__btn {
  outline: 2px solid var(--ember);
  outline-offset: 2px;
}

.filefield__hint {
  color: var(--muted);
  font-size: 1rem;
  max-inline-size: 54ch;
}

/* Dragging photos onto the field is the shorter path for anyone who already
   has them in a folder. The dashed edge it already had is the affordance;
   under a drag it simply becomes the one the system means. */
.filefield[data-drop='true'] {
  border-color: var(--ember);
  border-style: solid;
  background: color-mix(in srgb, var(--ember) 4%, var(--paper));
}

/* --- Photo tiles --------------------------------------------------------- */
/* A filename is not proof. Someone photographing a relative's cellar needs to
   see that they photographed the right cellar, which is what the thumbnail is
   for; the two file sizes beside it are what tells them the 8 MB photo their
   phone produced is not the thing being sent. */

.filelist {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: var(--s-md);
  margin-block-start: var(--s-md);
}

.shot {
  position: relative;
  display: grid;
  gap: 8px;
  animation: file-in 200ms var(--ease) both;
  animation-delay: calc(var(--i, 0) * 45ms);
}

.shot__frame {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--surface);
  overflow: hidden;
}
.shot__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  animation: shot-in 220ms var(--ease) both;
}
@keyframes shot-in { from { opacity: 0; transform: scale(1.02); } }

/* Visibility comes from the tile's state, never from the `hidden` attribute:
   the reset sets `img, svg { display: block }`, which outranks the user-agent
   rule for `[hidden]`, so a hidden SVG in this file is not hidden at all. */
.shot__na { display: none; color: var(--muted); }
.shot[data-state='nopreview'] .shot__na { display: block; }

/* The progress ring is the file's own state, so it lives on the frame rather
   than floating over the form. `--p` is written by the script at real stage
   boundaries — decode, thumbnail, encode — not on a timer, so a stall in the
   pipeline shows as a stalled ring instead of a lie that keeps moving. */
.shot__spinner {
  position: absolute;
  inline-size: 30px;
  block-size: 30px;
  border-radius: var(--r-pill);
  background:
    conic-gradient(var(--ember) calc(var(--p, 0) * 360deg), var(--line) 0);
  transition: background 180ms var(--ease);
}
.shot__spinner::after {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: var(--r-pill);
  background: var(--surface);
}
.shot[data-state='ready'] .shot__spinner,
.shot[data-state='nopreview'] .shot__spinner { display: none; }

.shot__meta { display: grid; gap: 2px; min-width: 0; }
.shot__name {
  font-size: 1rem;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shot__size { font-size: var(--fs-micro); color: var(--muted); }
.shot[data-sendable='false'] .shot__size { color: var(--ember); }
.shot[data-sendable='false'] .shot__frame { border-color: var(--ember); }

/* 32px is under the 44px target, so the hit area is extended past the visible
   button rather than the button being drawn larger than it needs to be. */
.shot__x {
  position: absolute;
  inset-block-start: 6px;
  inset-inline-end: 6px;
  inline-size: 32px;
  block-size: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 72%, transparent);
  color: var(--paper);
  cursor: pointer;
  opacity: 0;
  transition: opacity 150ms var(--ease), background-color 150ms var(--ease);
}
.shot__x::before {
  content: '';
  position: absolute;
  inset: -6px;
}
.shot:hover .shot__x,
.shot__x:focus-visible { opacity: 1; }
.shot__x:hover { background: var(--ink); }
/* No hover on touch, so the control cannot be hover-gated there. */
@media (hover: none) {
  .shot__x { opacity: 1; }
}

@keyframes file-in { from { opacity: 0; transform: translateY(6px); } }

.consent { display: flex; align-items: flex-start; gap: 12px; margin-block: var(--s-lg); }
.consent input {
  appearance: none;
  flex: none;
  width: 20px; height: 20px;
  margin-block-start: 2px;
  border: 1px solid var(--line-control);
  border-radius: var(--r-sm);
  background: var(--paper);
  cursor: pointer;
  position: relative;
  transition: background-color 150ms var(--ease), border-color 150ms var(--ease);
}
.consent input::after { content: ''; position: absolute; inset: -12px; }
.consent input:checked {
  background: var(--ember) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / 14px no-repeat;
  border-color: var(--ember);
}
.consent label { font-size: 1rem; color: var(--muted); max-inline-size: 54ch; }
.consent a {
  color: var(--ember);
  border-block-end: 1px solid var(--line-control);
  padding-block: 4px;
}
.field--error .consent input { border-color: var(--ember); }

/* overflow:hidden so the input inside is clipped to the 1px wrapper. Without it
   the field still laid out at its natural 22x147 off-screen — harmless, since
   the wrapper is at -9999px with opacity 0 and the input is aria-hidden and
   tabindex="-1", but it reads as a stray control to anything measuring the page. */
.hp {
  position: absolute;
  inset-inline-start: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
  opacity: 0;
}

.formstate {
  display: none;
  opacity: 0;
  transform: translateY(8px);
  border-radius: var(--r-md);
  padding: var(--s-lg);
  gap: 12px;
  transition: opacity 260ms var(--ease), transform 260ms var(--ease),
    display 260ms allow-discrete;
}
.formstate[data-on='true'] {
  display: flex;
  flex-direction: column;
  opacity: 1;
  transform: none;
}
@starting-style {
  .formstate[data-on='true'] { opacity: 0; transform: translateY(8px); }
}
.formstate--ok { background: var(--surface); border: 1px solid var(--green); }
.formstate__head { display: flex; align-items: center; gap: 10px; }
.formstate__mark { flex: none; color: var(--green); }
.formstate--fail { background: var(--surface); border: 1px solid var(--ember); }
.formstate p, .formstate .muted { color: var(--muted); font-size: 1rem; max-inline-size: 54ch; }
.formstate__actions { display: flex; flex-wrap: wrap; gap: 12px; margin-block-start: 6px; }

.aside {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--s-xl);
}
/* The three ways to reach Blitz travel with the form. Most visitors will
   phone rather than type, and the number scrolling away the moment someone
   starts filling in the long field is the one place it must not be. Offset
   clears the compacted header. */
@media (min-width: 1024px) {
  .aside {
    position: sticky;
    inset-block-start: calc(var(--header-h-compact) + var(--s-lg));
  }
}
.aside__phone {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-family: Manrope, sans-serif;
  font-size: var(--fs-phone);
  font-weight: 800;
  letter-spacing: -0.01em;
  transition: color 150ms var(--ease);
}
.aside__phone:hover { color: var(--ember); }
.aside__phone svg { color: var(--ember); flex: none; }
.aside__wa { margin-block-start: var(--s-md); }
.aside hr { border: 0; border-block-start: 1px solid var(--line); margin-block: var(--s-lg); }
.aside dl { display: grid; gap: 10px; margin: 0; font-size: 1rem; }
.aside dt { color: var(--muted); }
.aside dd { margin: 0; }

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

.footer { background: var(--ink); color: var(--paper); padding-block: var(--s-4xl) var(--s-xl); }
.footer__cols {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr 1fr;
  gap: var(--s-2xl);
}
.footer h2 { margin-block-end: var(--s-md); font-size: 1rem; }
.footer ul { display: grid; gap: 10px; }
.footer a, .footer p, .footer li { color: var(--paper-dim); font-size: 1rem; }
.footer a { transition: color 150ms var(--ease); }
/* Measured at 390: every footer link was a 19.5px-tall target, the phone,
   WhatsApp and email links among them. Inline-flex plus a minimum height
   gives each one a real tap area without changing how the column reads. */
.footer__cols a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* --- Promise list --------------------------------------------------------- */
/* Ruled rows with a check glyph. This was `.scope` on the old service-page
   template and lost its rules when that template was rebuilt as the numbered
   ledger, which silently unstyled the list on Über uns. The ledger numbers
   line items on a quotation; this marks promises, so the two stay distinct.
   Named `.pledges`, not `.checks`: the hero's own checklist has owned
   `.checks` since the homepage was built, and reusing it here re-styled the
   hero — top and bottom hairlines struck across the photograph. */
.pledges { border-block-start: 1px solid var(--line); }
.pledges li {
  display: flex;
  align-items: start;
  gap: 14px;
  padding-block: 14px;
  border-block-end: 1px solid var(--line);
  max-width: 54ch;
}
.pledges svg {
  flex: none;
  inline-size: 20px;
  block-size: 20px;
  color: var(--ink);
  margin-block-start: calc((1lh - 20px) / 2);
}

/* --- Touch targets, everywhere else -------------------------------------- */
/* WCAG 2.5.8 exempts a link sitting inline inside a sentence; every other
   target owes 44px. Two techniques, chosen by whether the visual size is
   allowed to move:
     · a minimum height where the link owns its own line;
     · a pseudo-element extending the hit area where the type must stay put,
       so a breadcrumb still looks like a breadcrumb.
   Fixed pixel offsets rather than `1lh` here: this has to hold on iOS Safari
   15, which the browser matrix still includes and which has no `lh` unit. A
   dropped declaration on a hit area is a silently small target. */

/* The whole card is the link target — the system has always said so, and the
   CSS never did it. Only the 28px title text and the image were tappable, so
   on a phone a ~300px-tall card answered to a 28px strip. The media link is
   aria-hidden and tabindex="-1", so covering it removes a duplicate target
   rather than hiding a real one. */
.card { position: relative; }
.card__body .title a::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
}

/* Standalone text links: the only thing in their paragraph. The inline
   rendition is deliberately untouched — giving it a minimum height would push
   apart the lines of the sentence it sits in, and it is exempt anyway. */
p > .textlink:only-child { min-height: 44px; }

.aside__phone { min-height: 44px; }

/* Breadcrumb and the Kontakt aside's contact values: 26px and 20px measured.
   The hit area grows, the type does not move. Nothing else is stacked within
   9px of these, so the expanded areas cannot overlap a neighbouring target. */
.crumbs a,
.aside dd a { position: relative; }
.crumbs a::after,
.aside dd a::after {
  content: '';
  position: absolute;
  inset-inline: 0;
}
/* Different offsets because the two sit on different line heights: the crumb
   renders 26px, the aside's contact values 20px. One shared value would leave
   the smaller of the two short, which is how the email link stayed a 38px
   target after the first pass at this. */
.crumbs a::after { inset-block: -9px; }
.aside dd a::after { inset-block: -13px; }
.footer a:hover { color: var(--paper); }
.footer__logo {
  width: 190px; height: 62px;
  object-fit: contain;
  background: var(--paper);
  border-radius: var(--r-sm);
  padding: 6px 10px;
  margin-block-end: var(--s-md);
}
.footer__bottom {
  margin-block-start: var(--s-4xl);
  padding-block-start: var(--s-lg);
  border-block-start: 1px solid var(--line-inv);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-md) var(--s-lg);
  justify-content: space-between;
  font-size: var(--fs-micro);
  color: var(--paper-dim);
}

/* --- Sofortkontakt bar (mobile only) ------------------------------------- */

.bar {
  display: none;
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 120;
  box-shadow: var(--sh-bar);
}
.bar__meta {
  padding: 7px var(--gutter);
  background: var(--ink);
  border-block-end: 1px solid var(--line-inv);
  color: var(--paper-dim);
  line-height: 1.25;
  text-align: center;
}
.bar__actions { display: flex; }
.bar a {
  flex: 1;
  height: var(--bar-h);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  color: var(--paper);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: background-color 180ms var(--ease), transform 140ms var(--ease);
}
[dir='rtl'] .bar a { text-transform: none; letter-spacing: normal; font-weight: 700; }
.bar__call { background: var(--ink); }
.bar__wa { background: var(--green); }
.bar a:active { transform: scale(0.98); }

/* --- Responsive ---------------------------------------------------------- */

@media (min-width: 1024px) {
  /* The copy sits on the page's own axis — the left edge every .wrap section
     below the hero opens from — not on a fixed inset from the viewport. The
     inset bought width for the photograph at the cost of the words: at 1440 it
     opened the H1 at x=43 while the sections beneath it opened at x=144, so the
     hero read as a caption pinned to the edge rather than the page's first
     statement. The cut and the plane are derived from that axis and one
     measure, so the clearance the feather needs is a consequence of the
     geometry instead of a percentage re-measured against each crop. */
  .hero {
    display: grid;
    --hero-axis: max(var(--gutter), calc((100% - var(--container)) / 2 + var(--gutter)));
    --hero-copy: 26rem;
    --hero-paper: calc(var(--hero-axis) + var(--hero-copy) + 40px);
  }
  .hero__plane {
    inline-size: var(--hero-paper);
    padding-inline: var(--hero-axis) 0;
  }
  .hero__grid { max-width: none; margin-inline: 0; padding-inline: 0; }
  .hero__media { inset-inline: var(--hero-paper) 0; }
  /* Seated right of centre: the narrower box crops harder from the sides, and
     at 50% the feather ate the cab while the second carrier fell off the right
     edge. 60% puts the loading action in the middle and spends the feather on
     foliage. */
  .hero__media img { object-position: 60% 50%; }

  .hero__inner { max-width: var(--hero-copy); }
  /* The lede and the promises were capped under the column to hold them off
     the van livery. The cut now follows the column, so the column is the only
     cap left to argue: the lede keeps its documented Lede measure and the
     promises run the column instead of breaking one of four across two lines. */
  .hero__sub { max-width: 38ch; }
  .hero__inner > .checks { max-width: none; }
  /* The actions break out of the column. Everything above them is type that
     needs paper behind it; these two are solid fills with their own grounds,
     so they can run their natural width and sit over the photograph. Capped
     with the rest they stacked, which read as a phone layout on a desktop. */
  .hero__cta { max-width: none; width: max-content; }
  /* The H1 sits above the livery's band, so it may run past the column — as
     far as the feather's 14% line and no further. */
  .hero .display {
    font-size: min(4.4vw, 4rem);
    width: max-content;
    max-width: calc(var(--hero-copy) + 3rem);
  }
}

/* The container goes fluid here, so the axis the hero shares with .wrap keeps
   moving right; the copy widens with the type rather than holding a measure
   set against smaller steps. */
@media (min-width: 1600px) {
  :root { --container: min(calc(1200px + (100% - 1600px) * 0.42), 1560px); }
  .hero { --hero-copy: 28rem; }

  .hero .display .line   { font-size: clamp(2.75rem, 2.6vw, 3.75rem); }
  .hero .display .accent { font-size: clamp(4rem, 3.8vw, 5.5rem); }
  .hero__sub { font-size: clamp(1.0625rem, 0.91vw, 1.3125rem); }
  .hero__inner > .checks li { font-size: clamp(1rem, 0.82vw, 1.1875rem); }

  /* Biased down, because the two halves of the crop are not worth the same:
     the top of this frame is sky and rooflines, the bottom is the boxes, the
     plant, the rolled carpet and the toolbox — the only part of the photograph
     that says what the business does. Whatever height leaves on the table now
     comes off the sky. */
  .hero__media img { object-position: 50% 62%; }
}

.hero--nomedia .hero__plane {
  inline-size: 100%;
  min-height: 0;
  padding-block: clamp(48px, 7vw, 88px);
  padding-inline: 0;
}
.hero--nomedia .hero__grid {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.hero--nomedia .hero__inner { max-width: 40rem; }
.hero--nomedia .display { max-width: none; }

/* Below 1024 the approved arrangement holds: copy first on Paper, then the photo
   full-bleed beneath it. German plus four checkmarks does not fit a 380px column. */
@media (max-width: 1023px) {
  .hero { display: flex; flex-direction: column; }
  /* Stacked — but not a hard white block meeting a hard photographic one.
     The photograph is taller here than it was and rises into the copy block on
     a vertical feather, so the hero warms downward into the work instead of
     ending on an abrupt edge. This is the same move as the desktop feather,
     rotated: the only thing in this palette that can fill white space is the
     photograph. There is no warm neutral (Surface is 3% off Paper) and ink is
     punctuation, capped at two bands per page. */
  /* The photograph leads on small screens and dissolves downward into the copy.
     Stacked below, it landed at y=719 on a 390×844 phone — under the fold — so
     the entire first viewport was unbroken white and no amount of feathering
     changed what the visitor actually saw. This is a deliberate divergence from
     the mobile mockup, which draws the image beneath the copy. */
  .hero__media {
    position: relative; inset: auto; order: 1;
    aspect-ratio: 16 / 10;
    /* Capped, or 4:3 swells with the viewport — at 1023px it took 94% of the
       fold and pushed the H1 out of sight.
       44vh, not the brief's 46: the vh half of this only binds on short, wide
       stacked viewports, which are exactly the ones where the copy column is
       squeezed. At 600x900 the taller H1 left the phone number's bottom at 844
       against an 840 fold — four pixels under. 44vh returns 18px there and
       changes nothing at the widths the brief quotes its own figures for: 390
       is governed by the 4:3 aspect (292px) and 768 by the 430px ceiling, so
       "42% of the fold at 390 and 46% at 768" both still hold. */
    max-block-size: min(44vh, 430px);
    margin-block-end: -48px;
    -webkit-mask-image: linear-gradient(to bottom,
      var(--ink) 62%,
      color-mix(in srgb, var(--ink) 30%, transparent) 88%,
      transparent 100%);
    mask-image: linear-gradient(to bottom,
      var(--ink) 62%,
      color-mix(in srgb, var(--ink) 30%, transparent) 88%,
      transparent 100%);
  }
  /* Held high in the frame: the phone slot is shallower than the crop, and
     centring it pushed the carriers below the fold. */
  .hero__media img { object-position: 60% 32%; }
  .hero__plane {
    inline-size: 100%;
    min-height: 0;
    border-radius: 0;
    padding-inline: 0;
    /* The top padding is the feather's landing zone: the photo's transparent
       tail overlaps only empty space, never a line of type. */
    padding-block: var(--s-lg) var(--s-2xl);
    order: 2;
  }
  .hero__grid { max-width: var(--container); }
  .hero__inner { max-width: 40rem; --rhythm: 0.9; }

  /* The number is carried by the whole stacked layout, not just below 768.
     Its reason was always "the header phone link is gone here" — and the
     header collapses to a burger at 1259, so 768-1023 had the identical hole
     the 767 line was drawn to close: no header phone, and a hero CTA row that
     sits at y1024 against a 964 fold. The Sofortkontakt bar covers the tap;
     this covers the number you can read aloud, write down, or show to someone.
     Layout rules stay at 767 — this is the stacked range, not the phone one.

     The intervals run 25 / 34 / 25 down the stack, so the number is bracketed
     by the two widest gaps and reads as the one thing to act on, without
     either neighbour merging into it. The 34 is measured, not chosen: "Faire
     Festpreise – keine versteckten Kosten" needs 361px on one line and a 390px
     phone affords 358, so it wraps to two lines at the primary width no matter
     how the column is capped — and at the old intervals its second line landed
     under the Sofortkontakt bar. These put its full 53px box at 714-767
     against a 784 fold, so the one promise required above the fold is
     whole. What the fold cuts instead is check two: one line, lower stakes,
     and a cut item is how a page says it continues. */
  .hero__tel { display: block; margin-block-start: calc(var(--s-xl) * var(--rhythm)); }
  .hero__inner > .checks { margin-block-start: calc(var(--s-lg) * var(--rhythm)); }
  .hero__tel a {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-height: 48px;
    font-family: Manrope, system-ui, sans-serif;
    font-size: var(--fs-phone);
    font-weight: 800;
    line-height: 1.2;
    letter-spacing: -0.01em;
  }
  .hero__tel a:hover { color: var(--ember); }
  .hero__tel svg { flex: none; color: var(--ember); }
  .hero__tel-hours { display: block; color: var(--muted); }

  .grid-3 { grid-template-columns: repeat(2, 1fr); }
  .contact__cols { grid-template-columns: 1fr; gap: var(--s-xl); }

  /* One column, and the stub stops being a stub. It moves to the foot of the
     read rather than the head of it: above the line items it would push the
     first thing the visitor came for off the screen. The perforation turns
     horizontal, so it still reads as part of the same document. */
  .sheet__cols { grid-template-columns: 1fr; gap: var(--s-2xl); }
  .stub {
    border-inline-start: 0;
    border-block-start: 1px dashed var(--line);
    padding-inline-start: 0;
    padding-block-start: var(--s-xl);
  }
}

@media (max-width: 900px) {
  .grid-4 { grid-template-columns: repeat(2, 1fr); }
  .footer__cols { grid-template-columns: 1fr 1fr; gap: var(--s-xl); }

  /* The two halves stop being a statement beside its answer and become one
     column: statement, then answer. The end-alignment goes with the second
     column it was written for — stacked, it left the phone number hanging off
     the right margin while every other line on the page started at the left. */
  .cta__inner { grid-template-columns: 1fr; gap: var(--s-xl); }
}

/* Measured: the desktop header needs ~1220px before the nav, the phone, the
   locale and the WhatsApp button stop fitting. Below that it overflowed the
   viewport and the page scrolled sideways, with the WhatsApp button off-screen
   entirely. The burger takes over here — a full breakpoint earlier than the
   bar, the hero stack and the grids, which keep their own 767px line.
   The locale switcher is deliberately NOT hidden: it is the only route to the
   Arabic site, and half the audience reads Arabic. */
@media (max-width: 1259px) {
  .nav, .header .btn--whatsapp { display: none; }
  .burger { display: flex; }
  .header__inner { justify-content: space-between; gap: var(--s-md); }
  .header__logo img { width: 153px; height: 50px; }
}

/* The Sofortkontakt bar runs to 1259px, not 1023 and not 767. It is pinned to
   the burger's own breakpoint because they answer the same question: below
   1259 the desktop nav and the header WhatsApp are gone and the tel: link is
   inside a collapsed dropdown, so the bar is the only contact affordance left
   on screen. The previous 1023 line reproduced the exact defect the earlier
   767 line did, one band further out — from 1024 to 1258 the homepage had no
   sticky stub (that is a Leistung-page component), no header phone, and a hero
   CTA row sliced by the fold: measured at 1024x768, the WhatsApp button spanned
   724-778 against a 768px fold and no phone number was visible at all.
   The "Number Is Always Reachable" rule says there is no viewport
   width at which a visitor cannot see a way to call. Now there isn't.
   Bottom clearance moves with it. */
@media (max-width: 1259px) {
  body { padding-block-end: calc(var(--bar-h) + var(--bar-meta-h) + 16px); }
  .bar { display: block; }

  /* One WhatsApp per screen. The hero's copy of it sat 43px above the bar's,
     pointing at the same deep link, so neither told the reader anything. The
     Kontakt aside's copy sits 20px above it and answers the same way. */
  .hero__cta .btn--whatsapp { display: none; }
  .aside__wa { display: none; }


  /* Anything focused near the end of the document would otherwise come to
     rest underneath the fixed bar: two of the 35 focus stops at 390px did. */
  a, button, summary, input, select, textarea { scroll-margin-block-end: calc(var(--bar-h) + var(--bar-meta-h) + 16px); }
}

@media (max-width: 767px) {
  .hero__cta { flex-direction: column; align-items: stretch; }
  .hero__cta .btn { width: 100%; }

  .cta__actions { flex-direction: column; align-items: stretch; }
  .cta__actions .btn { width: 100%; }
  .cta__tel { margin-block-start: var(--s-sm); }

  .grid-3, .grid-4 { grid-template-columns: 1fr; }
  .trust__grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-xl) 0; }
  .trust__item:nth-child(odd) { border-inline-start: 0; }

  .step { grid-template-columns: auto 1fr; gap: var(--s-md); }
  .step__icon { display: none; }


  .footer__cols { grid-template-columns: 1fr; }
  .aside { padding: var(--s-lg); }
}

@media (max-width: 479px) {
  /* Logo, locale switcher and burger have to share ~288px on a 320px phone.
     The separator is the one thing here that carries no meaning, and the logo
     is allowed to shrink to fit rather than push the row off-screen.
     Direct child only: the AR link carries spans of its own now, and a
     descendant selector here emptied it — a 44px blank tap target that was
     the only route to /ar/ on the phones half the audience is reading on. */
  .locale > span { display: none; }
  .locale { gap: 2px; }
  .header__logo { flex: 0 1 auto; min-width: 0; }
  .header__logo img { width: auto; max-width: 100%; height: 43px; }

  .hero__plane { padding-block-start: var(--s-md); }
  .hero__inner { --rhythm: 0.82; }
}

/* --- Reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    /* Zeroing the duration alone is not enough for anything staggered: a
       backwards-filling animation with a 400ms delay against a 0.01ms duration
       leaves its element sitting at opacity 0 for those 400ms, so a sequence
       nobody asked to see becomes a blank column instead. Kept as a standing
       guard — the hero stagger this was written for is gone, but the next
       delayed animation should not have to rediscover it. */
    animation-delay: 0ms !important;
  }

  /* Zeroing the duration is not enough here: the header compaction is a 43px
     displacement of everything below it, and an instantaneous 43px jump is
     precisely what this preference exists to prevent. So it does not happen —
     the header keeps its resting height and only the edge shadow appears. */
  .header.is-compact .header__inner { height: var(--header-h); }
  .header.is-compact .header__logo img { transform: none; }
  /* The gaps tighten to mirror the height change. No height change here, so
     no tightening either — otherwise the row shifts for no stated reason. */
  .header.is-compact .nav,
  .header.is-compact .header__aside { gap: calc(var(--s-lg) * var(--bar-rhythm)); }
  .card:hover { transform: none; }
  .card:hover .card__media img { transform: none; }
  .card:hover .card__more svg,
  .btn--primary:hover .btn__arrow { transform: none; }
  /* The chevron still turns — it is the control's state, not decoration, and
     a disclosure marker that never moves says the menu never opened. Only the
     travel goes: the panel arrives on a fade. */
  .menu { transform: none !important; transition-duration: 160ms !important; }
  [dir='rtl'] .card__more svg,
  [dir='rtl'] .btn__arrow { transform: scaleX(-1); }

  /* The fade is kept deliberately because it is what prevents the jarring
     appearance of the form success/failure panel; only the travel is dropped. */
  .formstate { transform: none !important; transition-duration: 260ms !important; }

  /* The fade is kept because the instant full-screen replacement of the
     mobile overlay is itself the jarring thing; only the travel is dropped. */
  .overlay { transform: none !important; transition-duration: 180ms !important; }

  /* The fade is kept so the validation error's arrival stays legible; only
     the travel is dropped. */
  .err { transform: none !important; transition-duration: 160ms !important; }
  /* The header stays at its resting height here, so the stub's sticky offset
     has to follow it — measured against the compacted height it slid 19px
     under the header for every reduced-motion reader at 1024 and above. */
  .stub { inset-block-start: calc(var(--header-h) + var(--s-lg)); }
}

[dir='rtl'] .header,
[dir='rtl'] .overlay {
  direction: ltr;
}

[dir='rtl'] .display,
[dir='rtl'] .headline,
[dir='rtl'] .title,
[dir='rtl'] .numeral,
[dir='rtl'] .note__k,
[dir='rtl'] .menu__name,
[dir='rtl'] .overlay__trigger,
[dir='rtl'] .overlay__nav a,
[dir='rtl'] .hero .display .line,
[dir='rtl'] .trust__text,
[dir='rtl'] .cta__tel a,
[dir='rtl'] .stub__phone,
[dir='rtl'] .aside__phone,
[dir='rtl'] .header__tel,
[dir='rtl'] .faq summary h3,
[dir='rtl'] .hero__tel a,
[dir='rtl'] .errorpage__code { letter-spacing: normal; }

[dir='rtl'] .display { line-height: 1.3; }
[dir='rtl'] .headline { line-height: 1.35; }
[dir='rtl'] .errorpage__code { line-height: 1.1; }

[dir='rtl'] .header__logo img { transform-origin: left center; }

/* --- Error page ---------------------------------------------------------- */

.errorbody { background: var(--surface); }

.errorpage { text-align: center; }

.errorpage--full {
  display: flex;
  align-items: center;
  min-block-size: 100dvh;
  padding-block: var(--s-3xl);
  background: var(--surface);
}

.errorpage__col { max-width: 40rem; margin-inline: auto; }

.errorpage__code {
  margin: 0;
  font-size: clamp(4rem, 13vw, 6rem);
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--orange);
}

.errorpage__headline {
  margin-block: var(--s-lg) 0;
  margin-inline: auto;
  max-width: 20ch;
}

.errorpage__lede {
  margin-block-start: var(--s-md);
  margin-inline: auto;
  text-wrap: pretty;
}

.errorpage__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-md);
  margin-block-start: var(--s-2xl);
}

@media (prefers-reduced-motion: no-preference) {
  .errorpage__code { animation: error-settle 620ms var(--ease) both; }
}

@keyframes error-settle {
  from { transform: translateY(0.14em); }
  to { transform: none; }
}
