/* ============================================================
   COMPONENTS — every block on the homepage.
   Rules: no hard-coded colours, no hard-coded spacing.
   If a value isn't in tokens.css it doesn't belong here.
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;   /* a word longer than its box breaks rather than spills (German compounds) */
}

/* Full-width: no max-width, no centring. Fluid gutters hold the
   content in; --measure keeps paragraphs readable. */
.wrap {
  width: 100%;
  padding-inline: var(--gutter);
}

section { padding-block: var(--section-pad); }
section + section { border-top: 1px solid var(--hairline); }

/* ---------- grounds: the ground mode and its bands ----------
   A site has two palettes (tokens.css, SKIN: --dark-* and --light-*). How
   it uses them is ONE setting, chosen with the user per site, on
   <html data-ground="…"> (house-rules.md, *Grounds*):

     dark        just dark: every section and the footer
     light       just light: every section and the footer
     dark-led    mostly dark: the page is dark; the sections the plan
                 marks data-band="lift" turn light
     light-led   mostly light: the page is light; the frame (the hero,
                 the CTA band or final CTA, the footer:
                 data-band="frame") is dark, and so is any "lift" section

   Sections say what they ARE, never which colour they take: frame, lift,
   or nothing (the argument, on the page's own ground). Switching the mode
   therefore recolours the whole site consistently. data-tone="dark|light"
   still forces one element's ground: behaviours.js sets it on the header
   and the sticky bar so they follow the band beneath them, and it outranks
   a band's role. Each remap points the colour tokens at one palette, so
   everything inside reads correctly with no variant of its own. --head-1
   is re-derived, since a token set on :root from --fg is computed there,
   and the logo's two versions (.wordmark [data-on]) swap with the ground.
   A page without data-ground keeps the SKIN's own pointer (tokens.css).
   Where the ground changes, the change is the edge: no hairline. */
[data-ground="dark"], [data-ground="dark-led"], [data-ground="light-led"] [data-band]:not([data-band="shade"]) {
  --bg: var(--dark-bg); --bg-2: var(--dark-bg-2); --bg-3: var(--dark-bg-3); --surface: var(--dark-surface);
  --bg-shade: var(--dark-bg-shade);
  --fg: var(--dark-fg); --fg-muted: var(--dark-fg-muted); --fg-dim: var(--dark-fg-dim);
  --hairline: var(--dark-hairline); --err: var(--dark-err); --field-edge: var(--dark-field-edge);
  --img-filter: var(--dark-img-filter);
  --head-1: var(--fg); --logo-on-dark: block; --logo-on-light: none; --glow-a: 13%;
}
[data-ground="light"], [data-ground="light-led"], [data-ground="dark-led"] [data-band="lift"] {
  --bg: var(--light-bg); --bg-2: var(--light-bg-2); --bg-3: var(--light-bg-3); --surface: var(--light-surface);
  --bg-shade: var(--light-bg-shade);
  --fg: var(--light-fg); --fg-muted: var(--light-fg-muted); --fg-dim: var(--light-fg-dim);
  --hairline: var(--light-hairline); --err: var(--light-err); --field-edge: var(--light-field-edge);
  --img-filter: var(--light-img-filter);
  --head-1: var(--fg); --logo-on-dark: none; --logo-on-light: block; --glow-a: 6%;
}
/* A forced ground: after the modes, and as specific as a band's role, so it wins. */
[data-tone="dark"], [data-band][data-tone="dark"] {
  --bg: var(--dark-bg); --bg-2: var(--dark-bg-2); --bg-3: var(--dark-bg-3); --surface: var(--dark-surface);
  --bg-shade: var(--dark-bg-shade);
  --fg: var(--dark-fg); --fg-muted: var(--dark-fg-muted); --fg-dim: var(--dark-fg-dim);
  --hairline: var(--dark-hairline); --err: var(--dark-err); --field-edge: var(--dark-field-edge);
  --img-filter: var(--dark-img-filter);
  --head-1: var(--fg); --logo-on-dark: block; --logo-on-light: none; --glow-a: 13%;
}
[data-tone="light"], [data-band][data-tone="light"] {
  --bg: var(--light-bg); --bg-2: var(--light-bg-2); --bg-3: var(--light-bg-3); --surface: var(--light-surface);
  --bg-shade: var(--light-bg-shade);
  --fg: var(--light-fg); --fg-muted: var(--light-fg-muted); --fg-dim: var(--light-fg-dim);
  --hairline: var(--light-hairline); --err: var(--light-err); --field-edge: var(--light-field-edge);
  --img-filter: var(--light-img-filter);
  --head-1: var(--fg); --logo-on-dark: none; --logo-on-light: block; --glow-a: 6%;
}
/* Shade (2026-09-11): a section on the page's own palette, a step off its
   ground (--bg-shade, tokens.css), so the argument between the frame
   doesn't sit on one flat colour. It keeps the palette, so nothing inside
   changes but the ground. The plan marks it: after two or three sections
   on the ground, one or two on the shade (house-rules.md, *Grounds*). */
[data-band="shade"] { --bg: var(--bg-shade); }
:where(section, footer):is([data-tone], [data-band]) { background-color: var(--bg); color: var(--fg); }
/* No hairline where the ground changes. */
:not([data-tone]) + section[data-tone],
[data-tone="dark"] + section[data-tone="light"],
[data-tone="light"] + section[data-tone="dark"],
[data-tone] + section:not([data-tone]),
section[data-band="shade"] + section:not([data-band="shade"]),
section:not([data-band="shade"]) + section[data-band="shade"],
[data-ground="light-led"] :is(section:not([data-band]) + section[data-band], section[data-band] + section:not([data-band])),
[data-ground="dark-led"] :is(section:not([data-band="lift"]) + section[data-band="lift"], section[data-band="lift"] + section:not([data-band="lift"])) { border-top: 0; }

/* ---------- type ----------
   Every var() below carries a fallback, so a project whose tokens.css
   predates the typography tokens renders exactly as it did before. */
h1, h2, h3 {
  font-family: var(--font-head);
  font-weight: var(--fw-head, 700);
  font-stretch: var(--head-stretch, normal);
  color: var(--head-1, var(--fg));
  margin: 0;
}
h1 {
  font-size: var(--fs-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-head, var(--ls-display));
}
h2 {
  font-size: var(--fs-h2);
  line-height: var(--lh-head);
  letter-spacing: var(--ls-head, var(--ls-display));
}
h1, h2 { text-transform: var(--head-case, none); }
h3 { font-size: var(--fs-h3); line-height: 1.3; }
/* A word longer than its line never clips: a German compound on a phone
   ("Betriebsstruktur" at 40px is 334px; the column at 375 is 327px).
   behaviours.js marks such a heading .hyph, which lets it, and only it,
   hyphenate; headings never hyphenate otherwise. overflow-wrap is the
   last resort where the browser has no dictionary for the language. */
h1, h2, h3, h4 { overflow-wrap: break-word; }
.hyph { -webkit-hyphens: auto; hyphens: auto; hyphenate-limit-chars: 10 4 4; }
p  { margin: 0; color: var(--fg-muted); max-width: var(--measure); }
.lead { font-size: var(--fs-lead); }

/* Two-voice headings. The .accent phrase is the one place a second
   colour — and, inside a heading, a second typeface — enters a line.
   Outside headings .accent only changes colour: a second face in body
   copy reads as a mistake, not a voice.
   font-synthesis is off so a missing italic cut shows upright instead
   of being faked by the browser: the failure stays visible. */
.accent { color: var(--head-2, var(--brand)); }
:is(h1, h2, h3) .accent {
  font-family:    var(--font-accent, inherit);
  font-weight:    var(--fw-accent, inherit);
  font-style:     var(--accent-style, normal);
  font-size:      calc(1em * var(--accent-scale, 1));
  text-transform: var(--accent-case, none);
  letter-spacing: var(--ls-accent, inherit);
  font-synthesis: none;
}

/* Eyebrow — note there is deliberately no ::before or ::after
   rule here. The house forbids a decorative line around it, and its
   colour stays the neutral --fg-dim: never --brand, never .accent. */
.eyebrow {
  font-family: var(--font-label, inherit);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--fg-dim);
  margin: 0 0 var(--sp-4);
}

/* ---------- stack: the rhythm of composed content ----------
   Library sections and kit components carry their own spacing. Anything
   composed for one site (a new section, a project block, the glue around
   a library section) stacks its content in a .stack, so nothing touches
   and every gap is the house's rather than picked by hand:
     <div class="stack">
       <p class="eyebrow">…</p> <h2>…</h2> <p>…</p> <div class="actions">…</div>
     </div>
   Eyebrow to heading and text to text: --sp-4. Heading to what follows:
   --sp-5. To actions, a form, a figure or a grid: --block-gap.
   .stack--tight for a small block (a card's title, meta and line). */
.stack > * { margin-block: 0; }
.stack > * + * { margin-block-start: var(--sp-4); }
.stack > :is(h1, h2, h3) + * { margin-block-start: var(--sp-5); }
.stack > * + :is(.actions, form, figure, .photo, .stats, .grid) { margin-block-start: var(--block-gap); }
.stack.stack--tight > * + * { margin-block-start: var(--sp-2); }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3) var(--sp-5); }

/* ============================================================
   INTERACTION — the canonical UI set, chosen in labs/ui-lab.html,
   labs/header-lab.html and labs/icons-forms-lab.html (2026-09-10):
   B03 A03 L01 N02 S01 H02 M01 C02 K01; icons GS01 GT01 GH01 GC01;
   forms FI01 FD01 FS04 FC04 FG02 FV01 FU01 FL01.
   references/interaction.md lists it, plus any favourites a site may
   swap in. Shared rules:
   · hover only where a pointer can really hover, so a tap never leaves
     a control stuck in its hover state;
   · keyboard focus always gets the ring below, on every control but
     text fields, whose accent edge is their focus (FI01, below);
   · colour eases fast (.3s), movement gets the long house ease-out;
   · no custom cursor (K01) — the native pointer keeps OS size/contrast.
   ============================================================ */
:where(a, button, input, textarea, select, summary):focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 3px;
}

/* The arrow: two copies in one clipped box. On hover the first leaves
   right as its twin arrives from the left — on buttons, action links and
   card chips alike, so one gesture means "go" across the site.
   Markup is one empty span:  <span class="arrow" aria-hidden="true"></span>
   Only on actions that lead somewhere; a Submit button carries none. */
:root {
  --arrow-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 8h11M9.5 4l4 4-4 4'/%3E%3C/svg%3E");
}
.arrow { position: relative; display: inline-block; flex: none; width: 1em; height: 1em; overflow: hidden; }
.arrow::before, .arrow::after {
  content: ""; position: absolute; inset: 0; background: currentColor;
  -webkit-mask: var(--arrow-icon) center / contain no-repeat;
          mask: var(--arrow-icon) center / contain no-repeat;
  transition: transform .55s var(--ease-out);
}
.arrow::after { transform: translateX(-170%); }
:is(.btn, .link, .card):focus-visible .arrow::before { transform: translateX(170%); }
:is(.btn, .link, .card):focus-visible .arrow::after  { transform: none; }
@media (hover: hover) {
  :is(.btn, .link, .card):hover .arrow::before { transform: translateX(170%); }
  :is(.btn, .link, .card):hover .arrow::after  { transform: none; }
}

/* ---------- actions ---------- */
/* B03 · fill + arrow cycle. The accent darkens; the ghost fills with the
   text colour and inverts. No lift — the arrow is the movement. Sizes are
   the HOUSE control tokens ("House refined", chosen in labs/scale-lab.html);
   the fallbacks are the previous 46px button, for older tokens files. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  justify-content: center;
  font-family: var(--font-label, var(--font-body));
  font-size: var(--fs-btn, 15px);
  font-weight: var(--fw-btn, 700);
  text-transform: var(--label-case, none);
  letter-spacing: var(--ls-label, normal);
  line-height: 20px;
  min-height: var(--btn-h, 46px);
  padding: 0 var(--btn-px, 24px);
  border: 0;
  border-radius: var(--radius-btn);
  text-decoration: none;
  cursor: pointer;
  transition: background-color .3s var(--ease-out), color .3s var(--ease-out),
              border-color .3s var(--ease-out), scale .12s ease;
}
.btn:active { scale: .97; }                  /* press feedback, every button */
.btn--accent { background: var(--brand); color: var(--on-brand, var(--fg)); }
.btn--ghost {
  background: transparent;
  color: var(--fg);
  border: 1.5px solid color-mix(in srgb, var(--fg) 35%, transparent);
  padding: 0 calc(var(--btn-px, 24px) - 1.5px);   /* the border sits inside the same box */
}
@media (hover: hover) {
  .btn--accent:hover { background: var(--brand-dark); }
  .btn--ghost:hover  { background: var(--fg); color: var(--bg); border-color: var(--fg); }
}

/* A03 · standalone action link: label + cycling arrow. Front-load the
   label ("Leistungen ansehen"), never a bare "Mehr erfahren". */
.link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  color: var(--fg);
  font-weight: 600;
  text-decoration: none;
  padding-block: 4px;
}

/* L01 · links inside running text. Underlined at rest — colour alone
   fails WCAG 1.4.1 — and the underline thickens into the accent on hover. */
:where(p, label, figcaption, blockquote, .prose) a:not(.btn, .link) {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .22em;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
  transition: text-decoration-color .3s, text-decoration-thickness .3s var(--ease-out);
}
@media (hover: hover) {
  :where(p, label, figcaption, blockquote, .prose) a:not(.btn, .link):hover {
    text-decoration-thickness: 2px;
    text-decoration-color: var(--brand);
  }
}

/* ---------- header ---------- */
.header {
  height: var(--header-h);
  display: flex;
  align-items: center;
  border-bottom: 1px solid transparent;
  position: sticky;
  top: 0;
  color: var(--fg);
  background: transparent;
  z-index: 10;
  transition: transform .35s cubic-bezier(.25,.8,.3,1), background-color .3s, border-color .3s;
}
/* At rest, at the top of the page, the header has no ground and no bottom
   edge: it sits on the hero, and a hairline there would rule a line through
   the first screen. Ground, blur and hairline arrive together once the page
   has scrolled and the header floats over content (data-stuck, set by
   behaviours.js). 2026-09-11, after the ERP v4 reference. While the mobile
   menu is open it takes the menu's ground. */
.header[data-stuck="true"], .menu-open .header {
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom-color: var(--hairline);
}
.menu-open .header { background: var(--bg); }
/* Retreats on downward scroll, returns on any upward scroll.
   Driven by data-hidden so the rule stays declarative. */
.header[data-hidden="true"] { transform: translateY(-100%); }

/* Reduced motion removes the slide, not the behaviour: the header
   still gets out of the way, it just does so instantly. */
@media (prefers-reduced-motion: reduce) {
  .header { transition: none; }
}

/* H02 · centred nav (labs/header-lab.html, chosen 2026-09-10): wordmark
   left, nav centred, .header__end right (EN/DE, ghost Kontakt, and the menu
   button on phones). The sides are at least their content, so the nav is
   centred while there is room and shifts, rather than colliding, when one
   side is wider. The row fills the header's height: it is the box the N02
   bar and a mega panel are placed in. */
.header .wrap {
  position: relative;
  align-self: stretch;
  display: grid;
  grid-template-columns: minmax(max-content, 1fr) auto minmax(max-content, 1fr);
  align-items: center;
  gap: var(--sp-5);
}
.wordmark {
  margin-right: auto;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 19px;
  letter-spacing: -0.01em;
}
.header__end { justify-self: end; display: flex; align-items: center; gap: var(--sp-4); }
/* An image logo carries one version per ground, marked data-on="dark" and
   data-on="light"; the other ground's version hides. Every ground remap
   (*grounds*, above) sets the two switches, so the right version shows on
   the page, in a band and in the header, whatever set the ground. */
/* LOGO-01 (references/rules.json): at least 40px tall, at most 200px wide
   (32px and 160px on phones). A wide logo stops at the cap, letterboxed. */
.wordmark img { display: block; height: 40px; width: auto; max-width: 200px; object-fit: contain; }
@media (max-width: 767px) { .wordmark img { max-width: 160px; } }
.wordmark [data-on="dark"]  { display: var(--logo-on-dark, block); }
.wordmark [data-on="light"] { display: var(--logo-on-light, none); }

/* N02 · one underline travels between the top-level items and rests under
   the current page (aria-current="page"), or under the item whose panel is
   open, so it stays put while the pointer moves down into the panel.
   behaviours.js adds the bar and sets .has-ind; without it the current page
   keeps a static underline. .nav itself is not positioned, so a mega panel
   can span the whole header row. */
.nav { display: flex; align-items: center; gap: var(--sp-5); }
.nav > a:not(.btn), .nav__trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: var(--sp-1) 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-family: var(--font-label, inherit);
  font-size: var(--fs-nav, var(--fs-small));
  text-transform: var(--label-case, none);
  letter-spacing: var(--ls-label, normal);
  color: var(--fg-muted);
  text-decoration: none;
  transition: color .2s;
}
.nav > a[aria-current="page"], .nav__trigger[aria-expanded="true"] { color: var(--fg); }
.nav:not(.has-ind) > a[aria-current="page"]::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--fg);
}
@media (hover: hover) { .nav > a:not(.btn):hover, .nav__trigger:hover { color: var(--fg); } }
.nav__ind {
  position: absolute; left: 0; top: 0; height: 2px; border-radius: 2px;
  background: var(--fg); pointer-events: none; opacity: 0;
  transition: transform .5s var(--ease-out), width .5s var(--ease-out), opacity .2s;
}
.nav__chev {
  width: 12px; height: 12px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
  transition: transform .3s var(--ease-out);
}
.nav__trigger[aria-expanded="true"] .nav__chev { transform: rotate(180deg); }

/* ---------- header panels: H07 mega or H09 dropdown ----------
   A top-level item with children is a button, and its panel follows it in
   the DOM, so Tab walks straight into it:
     <div class="nav__item">
       <button class="nav__trigger" type="button" aria-expanded="false"
               aria-controls="nav-x">Leistungen<svg class="nav__chev">…</svg></button>
       <div class="mega" id="nav-x" inert>…</div>   or   <div class="dropdown" id="nav-x" inert>…</div>
     </div>
   Mega or dropdown is a per-site choice, made in markup
   (references/interaction.md): the mega for five or more children that each
   carry a one-line promise, the dropdown for a short list of titles.
   behaviours.js opens and closes both. */
.mega, .dropdown { visibility: hidden; }

/* H07 · the mega: full width under the bar, the children in three columns,
   nothing beside them. */
.mega {
  position: absolute; left: 0; right: 0; top: calc(100% + 1px);
  padding: var(--sp-5) var(--gutter) var(--sp-6);
  background: var(--bg-2);
  border-bottom: 1px solid var(--hairline);
  clip-path: inset(0 0 100% 0);
  transition: clip-path .5s var(--ease-out), visibility 0s .5s;
}
.mega[data-open="true"] { visibility: visible; clip-path: inset(0 0 -2px 0); transition: clip-path .5s var(--ease-out); }
.mega__grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 var(--sp-5); }
.mega__item { display: block; height: 100%; padding: var(--sp-3) 0; border-top: 1px solid var(--hairline); text-decoration: none; }
.mega__t { display: block; color: var(--fg); font-weight: 600; font-size: var(--fs-body); line-height: 1.35; }
.mega__p { display: block; margin-top: 4px; max-width: var(--measure); color: var(--fg-muted); font-size: var(--fs-small); line-height: 1.5; }
@media (hover: hover) { .mega__item:hover .mega__t { text-decoration: underline 1px; text-underline-offset: .22em; } }

/* H09 · the dropdown: a floating card under its trigger, after the client's
   own (bdg-new.web-bakery.de) but neutral: no accent bar, no red arrow. One
   nested level shows indented and dash-marked, as in the mobile menu, not as
   a flyout. On the rightmost item, add .dropdown--end. */
.nav__item:has(> .dropdown) { position: relative; }
.dropdown {
  position: absolute; left: calc(-1 * var(--sp-4)); top: calc(100% + var(--sp-4));
  min-width: 240px; padding: var(--sp-2);
  border: 1px solid var(--hairline);
  border-radius: calc(var(--radius-card) + 4px);
  background: radial-gradient(120% 90% at 0% 0%, color-mix(in srgb, var(--brand) 11%, transparent), transparent 60%),
              color-mix(in srgb, var(--bg-2) 97%, transparent);
  box-shadow: 0 28px 60px -12px rgb(0 0 0 / .55), inset 0 1px 0 color-mix(in srgb, var(--fg) 6%, transparent);
  opacity: 0; transform: translateY(10px) scale(.985); transform-origin: 34px top;
  transition: opacity .22s var(--ease-out), transform .28s var(--ease-out), visibility 0s .28s;
}
.dropdown--end { left: auto; right: calc(-1 * var(--sp-4)); transform-origin: calc(100% - 34px) top; }
.dropdown[data-open="true"] { visibility: visible; opacity: 1; transform: none; transition: opacity .22s var(--ease-out), transform .28s var(--ease-out); }
/* The gap above belongs to the panel, so a pointer crossing it stays inside. */
.dropdown::before { content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: var(--sp-4); }
.dropdown ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; }
.dropdown a {
  position: relative; display: flex; align-items: center; gap: 10px;
  padding: 10px 40px 10px 14px;
  border-radius: calc(var(--radius-card) - 2px);
  color: color-mix(in srgb, var(--fg) 80%, transparent);
  font-size: var(--fs-small); font-weight: 500; line-height: 1.45; white-space: nowrap;
  text-decoration: none;
  transition: color .18s, background-color .22s, padding-left .22s var(--ease-out);
}
/* The house arrow slides in on hover: this row leads somewhere. */
.dropdown a::after {
  content: ""; position: absolute; right: 14px; top: 50%; width: 14px; height: 14px; margin-top: -7px;
  background: currentColor;
  -webkit-mask: var(--arrow-icon) center / contain no-repeat;
          mask: var(--arrow-icon) center / contain no-repeat;
  opacity: 0; transform: translateX(-6px);
  transition: opacity .2s, transform .24s var(--ease-out);
}
.dropdown a[aria-current="page"] { color: var(--fg); background: color-mix(in srgb, var(--fg) 5%, transparent); }
.dropdown a:focus-visible { outline-offset: -2px; color: var(--fg); background: color-mix(in srgb, var(--fg) 6%, transparent); padding-left: 20px; }
.dropdown a:focus-visible::after { opacity: 1; transform: none; }
@media (hover: hover) {
  .dropdown a:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 6%, transparent); padding-left: 20px; }
  .dropdown a:hover::after { opacity: 1; transform: none; }
}
.dropdown ul ul { padding: 0 0 4px 14px; }
.dropdown ul ul a { padding-block: 7px; color: var(--fg-dim); }
.dropdown ul ul a::before { content: ""; width: 8px; height: 1px; flex: none; background: color-mix(in srgb, var(--fg) 25%, transparent); }

/* ---------- menu button + mobile menu (≤900px) ----------
   After the client's own mobile menu (bdg-new.web-bakery.de, taken on
   2026-09-10), in tokens and without the dots before sub-items: a boxed
   button whose lines turn into ×, and a panel under the header. Top-level
   items are rows; a group is an accordion with at most one nested level; the
   foot holds the page's main action, two or three contact tiles and the
   hours. It replaced T02's "Menü" label, which made the phone header 24px
   too wide at 320px in German. */
.menu-toggle {
  display: none; align-items: center; justify-content: center; flex: none;
  width: 44px; height: 44px; padding: 0;
  border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
  border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--fg) 4%, transparent);
  color: var(--fg); cursor: pointer;
  transition: background-color .24s, border-color .24s;
}
@media (hover: hover) {
  .menu-toggle:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); border-color: color-mix(in srgb, var(--fg) 24%, transparent); }
}
.menu-toggle__lines { position: relative; width: 17px; height: 13.5px; }
.menu-toggle__lines i {
  position: absolute; left: 0; width: 100%; height: 1.5px; border-radius: 2px; background: currentColor;
  transition: transform .34s cubic-bezier(.3, 1.4, .4, 1), opacity .2s;
}
.menu-toggle__lines i:nth-child(1) { top: 0; }
.menu-toggle__lines i:nth-child(2) { top: 6px; }
.menu-toggle__lines i:nth-child(3) { top: 12px; }
.menu-toggle[aria-expanded="true"] .menu-toggle__lines i:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.menu-toggle[aria-expanded="true"] .menu-toggle__lines i:nth-child(2) { opacity: 0; }
.menu-toggle[aria-expanded="true"] .menu-toggle__lines i:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

/* The panel. Inert while closed, so its links are never tabbed into;
   data-lenis-prevent lets it scroll while the page is held still. */
.menu {
  position: fixed; inset: var(--header-h) 0 0 0; z-index: 9;
  display: flex; flex-direction: column;
  background: radial-gradient(420px 300px at 88% 4%, color-mix(in srgb, var(--brand) 16%, transparent), transparent 70%), var(--bg);
  overflow-y: auto; overscroll-behavior: contain;
  opacity: 0; visibility: hidden; transform: translateY(-10px);
  transition: opacity .32s, transform .42s var(--ease-out), visibility 0s .42s;
}
.menu[data-open="true"] { opacity: 1; visibility: visible; transform: none; transition: opacity .32s, transform .42s var(--ease-out); }
.menu-open .sticky-cta { visibility: hidden; }   /* the foot's action is the one accent in view */
.menu__list { list-style: none; margin: 0; padding: 10px 0 6px; }
.menu__list > li { border-bottom: 1px solid color-mix(in srgb, var(--fg) 7%, transparent); }
/* Rows rise in one after another, from visible: a failed animation leaves an
   unanimated menu, never an empty one. */
.menu[data-open="true"] .menu__list > li { animation: menu-in .5s var(--ease-out) backwards; }
.menu[data-open="true"] .menu__list > li:nth-child(1) { animation-delay: .06s; }
.menu[data-open="true"] .menu__list > li:nth-child(2) { animation-delay: .12s; }
.menu[data-open="true"] .menu__list > li:nth-child(3) { animation-delay: .18s; }
.menu[data-open="true"] .menu__list > li:nth-child(4) { animation-delay: .24s; }
.menu[data-open="true"] .menu__list > li:nth-child(5) { animation-delay: .30s; }
.menu[data-open="true"] .menu__list > li:nth-child(n+6) { animation-delay: .36s; }
@keyframes menu-in { from { opacity: 0; transform: translateY(10px); } }

.menu__link {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  width: 100%; padding: 18px var(--gutter);
  border: 0; background: none; cursor: pointer; text-align: left;
  font: inherit;
  font-family: var(--font-head); font-stretch: var(--head-stretch, normal); font-weight: 600;
  font-size: var(--fs-lead); line-height: 1.5; letter-spacing: var(--ls-head, normal);
  color: var(--fg); text-decoration: none;
  transition: color .24s, background-color .24s;
}
.menu__link:focus-visible { outline-offset: -2px; }
.menu__link[aria-current="page"] { text-decoration: underline 2px; text-underline-offset: .2em; }
/* While a group is open the other rows step back, keeping the eye on it. */
.menu__list:has(> li > [aria-expanded="true"]) > li > .menu__link:not([aria-expanded="true"]) { color: var(--fg-muted); }
@media (hover: hover) {
  .menu__list > li > .menu__link:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 3%, transparent); }
}
.menu__chev {
  width: 20px; height: 20px; flex: none; color: var(--fg-dim);
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  transition: transform .36s var(--ease-out), color .24s;
}
.menu__link[aria-expanded="true"] .menu__chev { transform: rotate(180deg); color: var(--brand); }

/* The accordion: 0fr to 1fr animates to the content's own height, which no
   max-height guess can. Hidden while closed, so Tab skips its links. */
.menu__sub {
  display: grid; grid-template-rows: 0fr; visibility: hidden;
  transition: grid-template-rows .42s var(--ease-out), visibility 0s .42s;
}
.menu__link[aria-expanded="true"] + .menu__sub { grid-template-rows: 1fr; visibility: visible; transition: grid-template-rows .42s var(--ease-out); }
.menu__sub > ul { list-style: none; margin: 0; padding: 0 var(--gutter); min-height: 0; overflow: hidden; }
.menu__sub a {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 0 12px 14px;
  color: var(--fg-muted); font-size: var(--fs-body); line-height: 1.6; text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 5%, transparent);
  transition: color .22s, padding-left .28s var(--ease-out);
}
.menu__sub li:last-child > a { border-bottom: 0; padding-bottom: 18px; }
.menu__sub a:focus-visible { outline-offset: -2px; }
.menu__sub2 { list-style: none; margin: 0 0 4px; padding: 0 0 0 14px; }
.menu__sub .menu__sub2 a { padding: 9px 0; color: var(--fg-dim); font-size: var(--fs-small); border-bottom: 0; }
.menu__sub2 a::before { content: ""; width: 8px; height: 1px; flex: none; background: color-mix(in srgb, var(--fg) 25%, transparent); }
.menu__sub .menu__sub2 li:last-child > a { padding-bottom: 18px; }
@media (hover: hover) {
  .menu__sub a:hover { color: var(--fg); padding-left: 20px; }
  .menu__sub .menu__sub2 a:hover { color: var(--fg); padding-left: 4px; }
}

.menu__foot {
  margin-top: auto; padding: 20px var(--gutter) 26px;
  display: flex; flex-direction: column; gap: var(--sp-3);
  border-top: 1px solid color-mix(in srgb, var(--fg) 8%, transparent);
}
.menu__foot .btn { width: 100%; }
.menu__contact { list-style: none; margin: 0; padding: 0; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 10px; }
.menu__contact a {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 10px 8px;
  border: 1px solid var(--hairline); border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--fg) 3%, transparent);
  color: color-mix(in srgb, var(--fg) 82%, transparent);
  font-size: var(--fs-eyebrow); letter-spacing: .03em; text-decoration: none;
  transition: border-color .24s, background-color .24s, color .24s;
}
@media (hover: hover) {
  .menu__contact a:hover { border-color: color-mix(in srgb, var(--fg) 26%, transparent); background: color-mix(in srgb, var(--fg) 7%, transparent); color: var(--fg); }
}
.menu__contact svg {
  width: var(--icon-inline, 20px); height: var(--icon-inline, 20px);
  fill: none; stroke: currentColor; stroke-width: var(--icon-stroke, 1.5); stroke-linecap: round; stroke-linejoin: round;
}
.menu__meta { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); color: var(--fg-dim); font-size: var(--fs-eyebrow); }

@media (min-width: 901px) { .menu { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .nav__ind, .nav__chev, .mega, .dropdown, .dropdown a, .dropdown a::after,
  .menu, .menu-toggle__lines i, .menu__link, .menu__chev, .menu__sub, .menu__sub a { transition: none; }
  .menu[data-open="true"] .menu__list > li { animation: none; }
}

/* ---------- hero ---------- */
.hero { padding-block: var(--sp-10) var(--sp-9); }
.hero .wrap {
  display: grid;
  grid-template-columns: 56fr 44fr;
  gap: var(--sp-8);
  align-items: center;
}
/* lighten makes any pixel darker than the page ground vanish, so a
   near-black render sits on the page with no visible edge. */
.hero-visual { width: 100%; height: auto; mix-blend-mode: lighten; }
.hero h1 { margin-bottom: var(--sp-5); text-wrap: balance; }
.hero .lead { margin-bottom: var(--block-gap); }
.trust {
  margin-top: var(--sp-7);
  font-size: var(--fs-small);
  color: var(--fg-dim);
  letter-spacing: .01em;
  max-width: none;
}

/* ---------- service hero ----------
   The canonical hero of a service page (2026-09-11, from the ERP v4
   reference). A dark band that reaches up under the resting header, the
   copy on the left and centred in the first screen: eyebrow, the two-voice
   h1, a sentence or two hung on a hairline, the main action beside a ghost
   second route. Behind it, light in layers: a brand glow, a cool glow and
   three faint seams, drifting at their own speeds as the page scrolls and,
   with a mouse, leaning away from the pointer. The copy itself never moves or
   fades on scroll (removed 2026-09-11 at the user's request). The band is complete without an image; a rendered object or a
   3D motif may take the calm right side as .hero__object, and is left out
   on phones. Motion: behaviours.js, *Depth*. The layers are the hero's own
   light, not image depth, so they don't count toward the 04 or 12 budgets.
     <section class="hero hero--service" data-band="frame">
       <div class="depth" aria-hidden="true">
         <span class="depth__glow depth__glow--brand" data-depth="-.46" data-depth-max="177" data-depth-mouse="26"></span>
         <span class="depth__glow depth__glow--cool" data-depth="-.34" data-depth-max="129" data-depth-mouse="19"></span>
         <span class="depth__seam" style="--seam-y: 26%; --seam-o: .45" data-depth=".1" data-depth-max="39" data-depth-mouse="4"></span>
         <span class="depth__seam" style="--seam-y: 46%" data-depth=".14" data-depth-max="51" data-depth-mouse="5"></span>
         <span class="depth__seam" style="--seam-y: 68%; --seam-o: .6" data-depth=".18" data-depth-max="63" data-depth-mouse="7"></span>
       </div>
       <div class="wrap"><div class="hero__copy">
         <p class="eyebrow">…</p>
         <h1 class="reveal" data-reveal="lines">Statement <span class="accent">turn.</span></h1>
         <p class="hero__sub">…</p>
         <div class="hero__acts"> .btn--accent · .btn--ghost </div>
       </div></div>
     </section>
   It is the first section after the header: the negative margin is what
   puts it under the resting header. */
.hero--service {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  align-items: center;
  min-height: min(90svh, 920px);
  margin-top: calc(-1 * var(--header-h));
  padding-block: calc(var(--header-h) + var(--sp-9)) var(--sp-9);
}
.hero--service .wrap { display: block; position: relative; }
.hero--service .eyebrow { margin-bottom: var(--sp-4); }
.hero--service h1 { max-width: 17ch; margin: 0; }
.hero__sub {
  max-width: 52ch;
  margin-top: var(--sp-5);
  padding-left: var(--sp-4);
  border-left: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
  font-size: var(--fs-lead);
}
.hero--service .hero__acts { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--block-gap); }
.hero__object {
  position: absolute; z-index: -1; top: 50%; right: clamp(-180px, -7vw, -48px);
  width: min(60vw, 900px, 76svh); height: auto; aspect-ratio: 1; object-fit: contain;
  translate: 0 -50%; pointer-events: none;
}

/* Light layers, for the service hero and the CTA band: inside a band with
   isolation, under its content. Brand and --fg at low strength only, so a
   layer reads as light on the ground, never as a new colour. */
.depth { position: absolute; inset: -18% -6%; z-index: -1; overflow: hidden; pointer-events: none; }
.depth > * { position: absolute; display: block; }
.depth__glow { aspect-ratio: 1; border-radius: 50%; }
.depth__glow--brand { left: 4%; top: 14%; width: min(52vw, 780px); background: radial-gradient(circle, color-mix(in srgb, var(--brand) 17%, transparent), transparent 68%); }
.depth__glow--cool  { right: -6%; top: -6%; width: min(46vw, 660px); background: radial-gradient(circle, color-mix(in srgb, var(--fg) 7%, transparent), transparent 66%); }
.depth__glow--deep  { right: 18%; bottom: -14%; width: min(38vw, 540px); background: radial-gradient(circle, color-mix(in srgb, var(--brand) 11%, transparent), transparent 70%); }
.depth__seam {
  left: -12%; right: -12%; top: var(--seam-y, 46%); height: 1px;
  opacity: var(--seam-o, 1); rotate: -4.5deg;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--fg) 18%, transparent) 38%, color-mix(in srgb, var(--fg) 5%, transparent) 72%, transparent);
}
/* Glow touch (2026-09-11, the user: "here and there across some of the
   sections"): the CTA band's brand light (.cta--band::before), weaker,
   from one side of an argument section. data-glow="left" | "right" on the
   section, nothing inside. Its strength follows the ground (--glow-a in the
   remaps at the top), so on a light ground it is a faint blush, never a pink
   patch. One or two sections a page, never two in a row, never next to the
   hero or the CTA band, which carry their own light (drafts/rules.md). */
section[data-glow] { position: relative; isolation: isolate; overflow: hidden; }
section[data-glow]::before {
  /* the section's own height, so the light fades out before its edges:
     a short band never shows the glow cut off at the seam */
  content: ""; position: absolute; z-index: -1; inset-block: 0;
  width: min(1000px, 100vw); pointer-events: none;
  background: radial-gradient(ellipse 50% 46% at 50% 50%, color-mix(in srgb, var(--brand) var(--glow-a, 13%), transparent), color-mix(in srgb, var(--brand) calc(var(--glow-a, 13%) * .3), transparent) 55%, transparent 78%);
}
section[data-glow="right"]::before { right: -300px; }
section[data-glow="left"]::before { left: -300px; }
@media (max-width: 900px) {
  .hero--service { min-height: 0; padding-block: calc(var(--header-h) + var(--sp-8)) var(--sp-8); }
  .hero__object { display: none; }
  .depth__glow--brand { width: 84vw; }
  .depth__glow--cool  { width: 72vw; }
}

/* ---------- statement band ---------- */
.statement { background: var(--bg-2); text-align: center; }
.statement h2 { max-width: 26ch; margin-inline: auto; text-wrap: balance; }
.statement .link { margin-top: var(--sp-7); }

/* ---------- split (approach) ---------- */
.split .wrap {
  display: grid;
  grid-template-columns: 42fr 58fr;
  gap: var(--sp-8);
}
.split .body > * + * { margin-top: var(--sp-4); }
.split .link { margin-top: var(--sp-6); }

/* ---------- tile grid ---------- */
.tiles h2 { margin-bottom: var(--sp-8); }
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-4);
}
.tile {
  display: block;
  padding: var(--sp-6);
  min-height: 232px;
  background: var(--bg-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-card);
  text-decoration: none;
  color: var(--fg);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: relative;
  transition: border-color .25s, background .25s;
}
/* C02 on a tile: no image to zoom, so the arrow chip is the whole state. */
.tile::before, .tile::after {
  content: ""; position: absolute; right: var(--sp-5); bottom: var(--sp-5);
  width: 36px; height: 36px; border-radius: 50%;
  opacity: 0; transform: scale(.6); transition: opacity .3s, transform .5s var(--ease-out);
}
.tile::before { background: var(--fg); }
.tile::after  { background: var(--bg); -webkit-mask: var(--arrow-icon) center / 16px no-repeat; mask: var(--arrow-icon) center / 16px no-repeat; }
.tile:focus-visible::before, .tile:focus-visible::after { opacity: 1; transform: none; }
@media (hover: hover) {
  .tile:hover { border-color: color-mix(in srgb, var(--fg) 28%, transparent); background: var(--surface); }
  .tile:hover::before, .tile:hover::after { opacity: 1; transform: none; }
}
/* Icons stay monochrome. Six crimson icons would spend the
   accent six times — the exact failure the PNG pass had. */
.tile svg { width: var(--icon-tile, 30px); height: var(--icon-tile, 30px); stroke: var(--fg-muted); fill: none;
            stroke-width: var(--icon-stroke, 1.5); transition: stroke .25s; }
.tile:hover svg { stroke: var(--brand); }
.tile h3 { text-wrap: balance; padding-right: 48px; }

/* ---------- image card — C02 ----------
   <div class="cards">
     <a class="card" href="…">
       <div class="card__media"><img src="…" alt=""></div>
       <p class="card__meta">Branche · 2026</p>
       <h3 class="card__title">What was built, for whom</h3>
       <span class="card__go" aria-hidden="true"><span class="arrow"></span></span>
     </a> …
   The whole card is one link. The image zooms a little inside its frame
   and an arrow chip arrives; without hover (touch) the chip simply shows. */
.cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4); }
.card {
  position: relative; display: flex; flex-direction: column;
  background: var(--bg-3); border: 1px solid var(--hairline); border-radius: var(--radius-card);
  overflow: hidden; color: var(--fg); text-decoration: none;
}
.card__media { position: relative; aspect-ratio: var(--ratio-teaser, 4 / 3); overflow: hidden; background: var(--bg-2); }
.card__media img { display: block; width: 100%; height: 100%; object-fit: cover; filter: var(--img-filter, none); transition: transform 1s var(--ease-out); }
.card__meta {
  margin: var(--sp-4) var(--sp-4) 0;
  font-family: var(--font-label, inherit); font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow); text-transform: uppercase; color: var(--fg-dim);
}
.card__title { margin: var(--sp-2) var(--sp-4) var(--sp-5); padding-right: 44px; text-wrap: balance; }
.card__go {
  position: absolute; right: var(--sp-4); bottom: var(--sp-5);
  display: grid; place-items: center; width: 36px; height: 36px; border-radius: 50%;
  background: var(--fg); color: var(--bg); font-size: 16px;
  opacity: 0; transform: scale(.6); transition: opacity .3s, transform .5s var(--ease-out);
}
.card:focus-visible .card__go { opacity: 1; transform: none; }
@media (hover: hover) {
  .card:hover .card__media img { transform: scale(1.06); }
  .card:hover .card__go { opacity: 1; transform: none; }
}
@media (hover: none) { .card__go { opacity: 1; transform: none; } }

/* ---------- team ---------- */
.team h2 { margin-bottom: var(--sp-8); }
.team .wrap > .cols {
  display: grid;
  grid-template-columns: 55fr 45fr;
  gap: var(--sp-8);
  align-items: center;
}
/* Every photograph sits in a .photo frame, filled by codex-imagegen or
   the client's shoot. The frame owns the crop, the ratio and the corner,
   and has no border (the user, 2026-09-12: the image-frames lab, FR02);
   the image inside is free to move for parallax. The ratio
   is a role from the image system (tokens.css): a photo beside text takes
   --ratio-inline, and a modifier switches the role. The image patterns
   (references/images.md) are further down, under IMAGE PATTERNS. */
.photo {
  position: relative;
  overflow: hidden;
  width: 100%;
  aspect-ratio: var(--ratio-inline, 4 / 3);
  border-radius: var(--img-radius, var(--radius-card));
  background: var(--bg-2);
}
.photo img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The site's grade (S3a). On a dark ground the knock-down keeps a
     daylight photograph from glaring against the page. */
  filter: var(--img-filter, saturate(.85));
}
.photo--people { aspect-ratio: var(--ratio-people, 3 / 4); }
.photo--teaser { aspect-ratio: var(--ratio-teaser, 16 / 9); }
.photo--band   { aspect-ratio: var(--ratio-band, 3 / 1); }
/* Per-image crop bias goes here, e.g.
   .team .photo { --parallax-focus: 15% 50%; }
   Set it whenever a photograph subject sits off-centre. */

/* ---------- parallax ----------
   The image is deliberately taller than its frame; that overscan IS the
   travel distance, so the frame can never reveal an edge. JS only ever
   writes --py, never a layout property. */
.parallax {
  position: relative;
  overflow: hidden;
  /* The one knob. Travel is this value, so raising it makes the effect
     more pronounced — at the cost of horizontal crop: `cover` has to
     scale the image up to fill a taller box, trimming roughly twice this
     figure off the width. Past ~15% that crop starts eating composition.
     Set --parallax-focus on an element whose subject sits off-centre. */
  --parallax-overscan: 12%;
  --parallax-focus: 50% 50%;
}
.parallax img {
  position: absolute;
  inset: 0 0 auto 0;
  top: 50%;
  width: 100%;
  height: calc(100% + var(--parallax-overscan) * 2);
  object-fit: cover;
  object-position: var(--parallax-focus);
  display: block;
  transform: translate3d(0, calc(-50% + var(--py, 0px)), 0);
  will-change: transform;
}
/* Parallax is a motion-sickness trigger, so reduced motion gets none of
   it: the overscan collapses and the image simply fills its frame. */
@media (prefers-reduced-motion: reduce) {
  .parallax { --parallax-overscan: 0%; }
  .parallax img { transform: translate3d(0, -50%, 0); will-change: auto; }
}
.team .body > p + p { margin-top: var(--sp-4); }
.facts {
  list-style: none;
  margin: var(--sp-7) 0 0;
  padding: 0;
  display: grid;
  gap: var(--sp-3);
}
.facts li {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  color: var(--fg-muted);
  font-size: var(--fs-small);
}
.facts svg { width: var(--icon-inline, 17px); height: var(--icon-inline, 17px); stroke: var(--fg-dim); fill: none; stroke-width: var(--icon-stroke, 1.5); }

/* ---------- 6 · reference logo band ---------- */
/* Narrow by design — the spec calls this "schmal". */
.logoband { padding-block: var(--sp-8); }
.logoband .eyebrow { text-align: center; margin-inline: auto; }   /* a p keeps --measure: centre the box too */
.logos {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--sp-5) var(--sp-8);
}
/* Slots, not logos. Real client marks drop in as <img>; these keep the
   band looking intentional until a case is approved for naming. */
.logo-slot {
  width: 192px;
  height: 64px;
  border-radius: 6px;
  border: 1px dashed var(--hairline);
  display: grid;
  place-items: center;
  color: var(--fg-dim);
  font-size: 11px;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  text-decoration: none;
  transition: border-color .25s, color .25s;
}
.logo-slot:hover { border-color: rgba(255,255,255,.3); color: var(--fg-muted); }
/* Logo sizes are rules (references/rules.json, LOGO-03 and -04; qa.mjs
   measures them). A client's mark 64px tall, at most 240px wide (56px and
   180px on phones); a partner, technology or certification mark 28px, at
   most 160px (24px and 120px on phones). A wide mark stops at its cap, or
   at the column if that is narrower, letterboxed in its box; the rule
   accepts it there. Outside the band a client logo takes
   data-logo="client", a partner's data-logo="partner". What shows is what
   counts, so the file is cropped tight. The slots take the size a real
   logo will. The caps are plain px on purpose: a percentage in them is
   dropped when the wrapping link sizes itself, which leaves the link at
   the logo's uncapped width and the logo off centre inside it. */
.logos > * { max-width: 100%; }
.logos img, .logos svg, img[data-logo="client"], svg[data-logo="client"] { display: block; height: 64px; width: auto; max-width: 240px; object-fit: contain; }
img[data-logo="partner"], svg[data-logo="partner"] { height: 28px; width: auto; max-width: 160px; object-fit: contain; }
@media (max-width: 767px) {
  .logos img, .logos svg, img[data-logo="client"], svg[data-logo="client"] { height: 56px; max-width: 180px; }
  img[data-logo="partner"], svg[data-logo="partner"] { height: 24px; max-width: 120px; }
  .logo-slot { width: 150px; height: 56px; }
}

/* ---------- 7 · six phases ---------- */
.phases { background: var(--bg-2); }
.phases h2 { margin-bottom: var(--sp-8); }
.phase-list { list-style: none; margin: 0; padding: 0; }
.phase {
  display: grid;
  grid-template-columns: 64px minmax(180px, 22%) 1fr;
  gap: var(--sp-5);
  align-items: baseline;
  padding-block: var(--sp-5);
  border-top: 1px solid var(--hairline);
}
.phase:last-child { border-bottom: 1px solid var(--hairline); }
.phase-num {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-h3);
  color: var(--brand);
  font-variant-numeric: tabular-nums;
}
.phase-name { font-family: var(--font-head); font-weight: 600; font-size: var(--fs-h3); }
.phase p { color: var(--fg-muted); }
.phases-note {
  margin-top: var(--sp-7);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3) var(--sp-6);
}
.phases-note p { max-width: 52ch; }

/* ---------- 8 · final CTA ---------- */
.cta .wrap { display: grid; grid-template-columns: 45fr 55fr; gap: var(--sp-8); }
.cta h2 { margin-bottom: var(--sp-4); }
.contact-methods { list-style: none; margin: var(--sp-7) 0 0; padding: 0; display: grid; gap: var(--sp-3); }
.contact-methods a {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  color: var(--fg-muted);
  text-decoration: none;
  font-size: var(--fs-body);
  transition: color .2s;
}
.contact-methods a:hover { color: var(--fg); }
.contact-methods svg { width: var(--icon-inline, 18px); height: var(--icon-inline, 18px); stroke: var(--fg-dim); fill: none; stroke-width: var(--icon-stroke, 1.5); }

/* ---------- CTA band ----------
   The canonical final CTA of every inner page (2026-09-11, from the ERP v4
   reference); the homepage keeps the .cta with its form. A dark band before
   the footer, everything centred: eyebrow, the closing question, one line,
   the page's main action beside a ghost second route, and the contact
   routes as a quiet line with their GC01 icons. Behind it a brand light
   from the upper right, two soft glows, and two bands of hairlines drifting
   slowly across (behaviours.js, *CTA lines*: canvas 2D, not WebGL). Reduced
   motion leaves the lines out. It carries its own light, so it never also
   takes the W04 neon line.
     <section class="cta cta--band" id="kontakt" data-band="frame">
       <div class="depth" aria-hidden="true">
         <span class="depth__glow depth__glow--deep" data-depth="-.28" data-depth-max="114"></span>
         <span class="depth__glow depth__glow--cool" data-depth="-.12" data-depth-max="66"></span>
       </div>
       <canvas class="cta__lines" aria-hidden="true"></canvas>
       <div class="wrap">
         <p class="eyebrow">…</p>
         <h2 class="reveal" data-reveal="lines">Question <span class="accent">turn?</span></h2>
         <p class="lead">…</p>
         <div class="cta__acts"> .btn--accent · .btn--ghost </div>
         <ul class="cta__contacts"> GC01 phone · chat · mail, each with its text </ul>
       </div>
     </section> */
.cta--band {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: var(--sp-9);
  text-align: center;
}
.cta--band::before {
  content: ""; position: absolute; z-index: -1; right: -220px; top: 0;
  width: min(1050px, 105vw); height: 760px; pointer-events: none;
  background: radial-gradient(ellipse 50% 44% at 62% 42%, color-mix(in srgb, var(--brand) 24%, transparent), color-mix(in srgb, var(--brand) 7%, transparent) 55%, transparent 78%);
  -webkit-mask-image: linear-gradient(transparent, #000 34%);
          mask-image: linear-gradient(transparent, #000 34%);
}
.cta--band .depth { inset: 0 -6% -18%; }
.cta--band .depth__glow--cool { right: -14%; top: 6%; width: min(40vw, 560px); opacity: .8; }
.cta__lines { position: absolute; inset: 0; z-index: -1; display: block; width: 100%; height: 100%; pointer-events: none; }
.cta--band .wrap { display: block; position: relative; }
.cta--band .eyebrow { margin: 0 auto var(--sp-4); }   /* a p keeps --measure: centre the box, not only its text */
.cta--band h2 { max-width: 24ch; margin: 0 auto; text-wrap: balance; }
.cta--band .lead { max-width: 46ch; margin: var(--sp-4) auto 0; }
.cta__acts { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-3) var(--sp-4); margin-top: var(--block-gap); }
.cta__contacts { list-style: none; margin: var(--sp-6) 0 0; padding: 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 0 var(--sp-5); }
.cta__contacts a {
  display: inline-flex; align-items: center; gap: 11px; min-height: 44px;
  color: var(--fg-muted); text-decoration: none;
  font-family: var(--font-label, inherit); font-size: var(--fs-small); letter-spacing: .02em;
  transition: color .3s;
}
.cta__contacts .ct { width: var(--icon-inline, 20px); height: var(--icon-inline, 20px); flex: none; }
@media (hover: hover) { .cta__contacts a:hover { color: var(--fg); } }

/* GC01 · contact icons — BDG's phone, WhatsApp and email glyphs
   (bdg-new.web-bakery.de), chosen on 2026-09-10 in labs/icons-forms-lab.html.
   A contact link with an icon takes one of these, in place of the set's
   phone, message and mail; plain text links stay plain. Each acts out its
   route once on hover (a pointer only) or keyboard focus: the handset
   rings and two waves draw out; the bubble
   springs, a tick draws in and a ring ripples; the flap folds back and a
   letter rises. Inline SVG only, since <use> can't reach the parts; the
   markup is in references/icons.md. A glyph takes the stroke of the line it
   sits in (.contact-methods, .menu__contact). Reduced motion keeps it still. */
.ct { overflow: visible; fill: none; stroke: currentColor; stroke-width: var(--icon-stroke, 1.5); stroke-linecap: round; stroke-linejoin: round; transform-style: preserve-3d; }
.ct > * { transform-box: fill-box; }
.ct--phone .ct-handset { transform-origin: 30% 70%; transition: transform .5s cubic-bezier(.34, 1.56, .5, 1); }
.ct--phone .ct-wave { stroke-dasharray: 14; stroke-dashoffset: 14; opacity: 0; transform-origin: 0 100%; }
.ct--chat .ct-bubble { transform-origin: 50% 62%; }
.ct--chat .ct-check { stroke-dasharray: 10.2; stroke-dashoffset: 10.2; opacity: 0; }
.ct--chat .ct-ring { opacity: 0; transform-origin: 50% 50%; transform: scale(.62); }
.ct--mail { perspective: 60px; }
.ct--mail > * { transform-style: flat; }
.ct--mail .ct-flap { transform-origin: 50% 0; transition: transform .52s cubic-bezier(.32, .94, .22, 1), opacity .3s; }
.ct--mail .ct-body { transform-origin: 50% 100%; transition: transform .5s cubic-bezier(.25, .8, .3, 1); }
.ct--mail .ct-sheet { opacity: 0; transform-origin: 50% 100%; transform: translateY(4px) scale(.72); transition: transform .56s cubic-bezier(.18, 1.3, .32, 1) .08s, opacity .26s .1s; }
.ct--mail .ct-sheet path { stroke-dasharray: 9; stroke-dashoffset: 9; transition: stroke-dashoffset .5s .3s; }
/* played once on keyboard focus … */
a:focus-visible .ct-handset { animation: ct-handset 1.1s cubic-bezier(.36, .07, .19, .97) both; }
a:focus-visible .ct-wave-1 { animation: ct-wave 1.1s ease-out .12s both; }
a:focus-visible .ct-wave-2 { animation: ct-wave 1.1s ease-out .26s both; }
a:focus-visible .ct-bubble { animation: ct-send .78s cubic-bezier(.34, 1.5, .5, 1) both; }
a:focus-visible .ct-check { animation: ct-check .62s cubic-bezier(.2, .9, .25, 1) .16s both; }
a:focus-visible .ct-ring { animation: ct-ripple 1.05s cubic-bezier(.22, .7, .3, 1) .1s both; }
a:focus-visible .ct-flap { transform: rotateX(-162deg) translateY(-.4px); opacity: .85; }
a:focus-visible .ct-sheet { opacity: 1; transform: translateY(-5.4px) scale(1) rotate(-2.5deg); }
a:focus-visible .ct-sheet path { stroke-dashoffset: 0; }
a:focus-visible .ct-body { transform: translateY(.8px) scaleY(1.03); }
/* … and on hover, where a pointer can hover */
@media (hover: hover) {
  a:hover .ct-handset { animation: ct-handset 1.1s cubic-bezier(.36, .07, .19, .97) both; }
  a:hover .ct-wave-1 { animation: ct-wave 1.1s ease-out .12s both; }
  a:hover .ct-wave-2 { animation: ct-wave 1.1s ease-out .26s both; }
  a:hover .ct-bubble { animation: ct-send .78s cubic-bezier(.34, 1.5, .5, 1) both; }
  a:hover .ct-check { animation: ct-check .62s cubic-bezier(.2, .9, .25, 1) .16s both; }
  a:hover .ct-ring { animation: ct-ripple 1.05s cubic-bezier(.22, .7, .3, 1) .1s both; }
  a:hover .ct-flap { transform: rotateX(-162deg) translateY(-.4px); opacity: .85; }
  a:hover .ct-sheet { opacity: 1; transform: translateY(-5.4px) scale(1) rotate(-2.5deg); }
  a:hover .ct-sheet path { stroke-dashoffset: 0; }
  a:hover .ct-body { transform: translateY(.8px) scaleY(1.03); }
}
@keyframes ct-handset { 0% { transform: rotate(0); } 12% { transform: rotate(-13deg); } 26% { transform: rotate(10deg); } 40% { transform: rotate(-7deg); } 54% { transform: rotate(4deg); } 70% { transform: rotate(-2deg); } 100% { transform: rotate(0); } }
@keyframes ct-wave { 0% { stroke-dashoffset: 14; opacity: 0; transform: scale(.82); } 35% { stroke-dashoffset: 0; opacity: 1; transform: scale(1); } 70% { opacity: 1; transform: scale(1.06); } 100% { stroke-dashoffset: 0; opacity: 0; transform: scale(1.14); } }
@keyframes ct-send { 0% { transform: scale(1) rotate(0); } 22% { transform: scale(.9) rotate(-5deg); } 52% { transform: scale(1.07) rotate(3deg); } 78% { transform: scale(.98) rotate(-1deg); } 100% { transform: scale(1) rotate(0); } }
@keyframes ct-check { 0% { stroke-dashoffset: 10.2; opacity: 0; } 30% { opacity: 1; } 100% { stroke-dashoffset: 0; opacity: 1; } }
@keyframes ct-ripple { 0% { opacity: 0; transform: scale(.62); } 25% { opacity: .5; } 100% { opacity: 0; transform: scale(1.32); } }
@media (prefers-reduced-motion: reduce) {
  .ct * { animation: none !important; transition: none !important; transform: none !important; }
  .ct :is(.ct-wave, .ct-check, .ct-ring, .ct-sheet) { opacity: 0 !important; }
  .ct .ct-flap { opacity: 1 !important; }
}

/* FI01 · BSL's field (demo-wp.web-bakery.de/bsl), chosen on 2026-09-10 in
   labs/icons-forms-lab.html in place of F01. Square, a 1px edge, the page
   ground as fill, labels and entries in the label face; no hover. Where the
   kit differs from BSL as it is live:
   · the edge is --field-edge, 3:1 against the page (BSL's own is 1.5:1);
   · focus is BSL's, a pixel thinner at the user's request: the 1px edge
     turns accent, with a faint glow (BSL live adds a 1px accent ring). It
     meets WCAG 2.4.7, not 2.4.13's 2px. Text fields are the one control
     without the house ring: they match :focus-visible on every click, so
     the ring would double the edge. The outline turns transparent rather
     than none, so forced-colours mode still draws it;
   · labels and entries take the site's --font-label, so only a skin with a
     mono label face (BSL's Plex Mono) gets mono fields;
   · placeholders are --fg-dim (4.5:1), not BSL's faint 1.9:1.
   The label never disappears, and an error says what is wrong with an icon
   and a sentence — never colour alone. Mark the optional fields, not the
   required ones:
     <div class="field">
       <label for="e">E-Mail</label>
       <input id="e" type="email" required aria-describedby="e-err">
       <small class="field__error" id="e-err">Diese Adresse scheint unvollständig.</small>
     </div>
   The error shows only after the visitor has touched the field (:user-invalid). */
/* align-content: start, because a form beside a taller column is stretched
   to its height, and a grid shares that height out to its rows: the fields
   grew with it (a 67px field in BDG's contact section). */
.form { display: grid; gap: var(--sp-4); align-content: start; }
.field, .consent, .check, .upload {
  --field-fill: color-mix(in srgb, var(--bg) 60%, transparent);
  --field-glow: 0 0 24px color-mix(in srgb, var(--brand) 15%, transparent);
}
.field { display: grid; gap: 9px; align-content: start; }
.field > label {
  font-family: var(--font-label, var(--font-body));
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.field__opt { font-weight: 400; color: var(--fg-dim); }
.field input, .field textarea, .field select {
  font-family: var(--font-label, var(--font-body));
  font-size: 14px;
  letter-spacing: .06em;
  line-height: 1.3;   /* BSL's 48px field in any face; "normal" is per face (45px in Archivo) */
  color: var(--fg);
  background: var(--field-fill);
  border: 1px solid var(--field-edge, var(--fg-dim));
  border-radius: 0;
  padding: 14px 16px;
  width: 100%;
  transition: border-color .3s, background-color .3s, box-shadow .3s;
}
/* the request is prose: the body face, untracked, body leading */
.field textarea { min-height: 110px; resize: vertical; font-family: var(--font-body); letter-spacing: 0; line-height: var(--lh-body); }
.field :is(input, textarea)::placeholder { color: var(--fg-dim); opacity: 1; }
/* 14px entries make iOS Safari zoom in on focus; 16px on touch avoids it */
@media (pointer: coarse) { .field :is(input, textarea, select) { font-size: 16px; } }
.field :is(input, textarea, select):focus { outline-color: transparent; border-color: var(--brand); box-shadow: var(--field-glow); }
.field :is(input, textarea, select):user-invalid { border-color: var(--err, var(--brand)); }
/* Autofill: the browser paints its own fill (blue in Chrome, yellow in
   Safari). Cover it with the page ground, opaque, as BDG's live theme does. */
.field :is(input, textarea):is(:autofill, :-webkit-autofill) {
  -webkit-text-fill-color: var(--fg);
  caret-color: var(--fg);
  box-shadow: 0 0 0 1000px var(--bg) inset;
}
.field :is(input, textarea):is(:autofill, :-webkit-autofill):focus { box-shadow: 0 0 0 1000px var(--bg) inset, var(--field-glow); }
/* An error must never move the page. The sentence is always in the flow and only
   hidden, so a field is exactly as tall whether it is right or wrong, and nothing
   below it jumps the moment a check fails. Only fields that carry an error element
   pay for the line, so a form without checks is unchanged. (the user, 2026-09-16;
   it was display: none, and each message pushed the form down by a line) */
.field__error {
  display: flex; align-items: center; gap: var(--sp-1);
  font-family: var(--font-label, var(--font-body)); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--err, var(--brand));
  visibility: hidden; opacity: 0; transition: opacity .2s;
}
.field__error::before {
  content: "!"; display: inline-grid; place-items: center; flex: none; width: 16px; height: 16px;
  background: var(--err, var(--brand)); color: var(--bg); font: 700 11px/1 var(--font-body);
}
.field :is(input, textarea):user-invalid ~ .field__error,
.field:has(select:user-invalid) .field__error { visibility: visible; opacity: 1; }
/* Consent: BSL's box, square in the field's edge and fill; ticked, an 8px
   accent square in an accent edge. Its error sentence wraps under the text. */
.consent { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); align-items: flex-start; }
.consent input {
  appearance: none; display: grid; place-items: center; flex: none;
  width: 18px; height: 18px; margin: 2px 0 0;
  border: 1px solid var(--field-edge, var(--fg-dim)); border-radius: 0;
  background: var(--field-fill);
  transition: border-color .3s, background-color .3s;
}
.consent input:checked { border-color: var(--brand); background: color-mix(in srgb, var(--brand) 12%, transparent); }
.consent input:checked::after { content: ""; width: 8px; height: 8px; background: var(--brand); }
.consent input:user-invalid { border-color: var(--err, var(--brand)); }
.consent input:user-invalid ~ .field__error { visibility: visible; opacity: 1; }
.consent .field__error { flex-basis: 100%; }
/* forced-colours mode drops backgrounds, so the tick takes a system colour */
@media (forced-colors: active) { .consent input:checked::after { background: CanvasText; } }
/* FV02 · error-float (the user, 2026-09-16): the reserved error line cost every
   checked field a second gap, 49px between two fields instead of 24px. Here
   the sentence leaves the flow and sits in the form's own gap, under its
   field. It still never moves the page (the rule above), but it takes no room
   while the field is right. Opt in with <html data-ui="error-float">, or on
   any wrapper.
   · the gap must hold one line: the error is 16px tall, 6px under the field,
     and 8px clear of the next label, so rows are --err-room (30px) apart.
     A layout with its own row gap inside the form uses the same token;
   · a sentence too long for its field (a phone, a half row) wraps; the
     behaviour marks it .is-tall and it takes room only while it shows: that
     one shift is better than the sentence lying on the next label;
   · it may not be cut off with an ellipsis, since the sentence says what to fix;
   · keep the sentences short, as the kit's are ("Bitte ausfüllen.").
   Where the pattern comes from: errors under the field, in words and never
   colour alone (WebAIM, TetraLogical); no layout shift as errors come and go
   (Smashing, "Designing Better Error Messages UX"). */
[data-ui~="error-float"] { --err-room: calc(var(--sp-4) + 6px); }   /* 6px, the line, 8px to the next label */
[data-ui~="error-float"] .form { row-gap: var(--err-room); }
[data-ui~="error-float"] :is(.field, .consent) { position: relative; }
[data-ui~="error-float"] :is(.field, .consent) .field__error {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  align-items: flex-start; line-height: 16px;
  translate: 0 -3px; transition: opacity .2s, translate .2s var(--ease-out, ease);
  pointer-events: none;
}
[data-ui~="error-float"] .field__error::before { margin-top: 0; }
[data-ui~="error-float"] .field :is(input, textarea):user-invalid ~ .field__error,
[data-ui~="error-float"] .field:has(select:user-invalid) .field__error,
[data-ui~="error-float"] .consent input:user-invalid ~ .field__error { translate: 0 0; }
/* the consent's sentence starts under its text, not under the box */
[data-ui~="error-float"] .consent .field__error { left: calc(18px + var(--sp-3)); }
/* a sentence that wraps (kit/behaviours.js marks it) joins the flow while it shows */
[data-ui~="error-float"] .field__error.is-tall:is(:user-invalid ~ *, .field:has(select:user-invalid) *) { position: static; translate: none; }
[data-ui~="error-float"] .consent .field__error.is-tall:is(:user-invalid ~ *) { padding-left: calc(18px + var(--sp-3)); }
@media (prefers-reduced-motion: reduce) { [data-ui~="error-float"] .field__error { translate: none; transition: none; } }
.consent label { flex: 1; min-width: 0; max-width: 56ch; font-size: 13px; color: var(--fg-dim); }
.consent a { color: var(--fg-muted); }

/* The tick and the chevron the form controls draw, as masks, so they take
   any colour: the house tick, and the nav's 16-unit chevron. */
:root {
  --tick-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E");
  --chev-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
}

/* FS04 · select (2026-09-10) — a native <select> in the field's frame
   (FS01), and where the browser has customizable selects (appearance:
   base-select, Chromium 135+) its open list in the same square frame, a
   tick beside the chosen option. Elsewhere the system list opens, which on
   phones is the better picker anyway. Focus is the field's. A <select>
   takes no ::after, so a wrapper draws the chevron:
     <div class="field">
       <label for="t">Thema</label>
       <div class="select"><select id="t" name="t" required aria-describedby="t-err">
         <option value="">Bitte wählen</option>
         <option>ERP-Beratung</option>
       </select></div>
       <small class="field__error" id="t-err">Bitte wählen Sie ein Thema.</small>
     </div> */
.select { position: relative; display: grid; }
.select::after {
  content: ""; position: absolute; right: 16px; top: 50%; width: 16px; height: 16px; margin-top: -8px;
  background: var(--fg-dim); pointer-events: none;
  -webkit-mask: var(--chev-icon) center / contain no-repeat;
          mask: var(--chev-icon) center / contain no-repeat;
}
.field select { appearance: none; padding-right: 44px; cursor: pointer; }
.field select:has(option[value=""]:checked) { color: var(--fg-dim); }
.field select option { color: var(--fg); background: var(--surface); }
@media (forced-colors: active) { .select::after { background: CanvasText; } }
@supports (appearance: base-select) {
  .field select, .field select::picker(select) { appearance: base-select; }
  .select::after { display: none; }
  .field select { display: flex; align-items: center; padding-right: 16px; }
  .field select::picker-icon { color: var(--fg-dim); transition: rotate .3s var(--ease-out); }
  .field select:open::picker-icon { rotate: 180deg; }
  .field select::picker(select) {
    margin-top: 6px; padding: 6px;
    border: 1px solid var(--field-edge, var(--fg-dim)); border-radius: 0;
    background: var(--surface);
    box-shadow: 0 28px 60px -12px rgb(0 0 0 / .55);
  }
  .field select option { min-height: 44px; padding: 10px 12px; gap: 12px; color: var(--fg); background: none; }
  .field select option:is(:hover, :focus-visible) { background: color-mix(in srgb, var(--fg) 6%, transparent); outline: none; }
  .field select option:checked { font-weight: 600; }
  .field select option::checkmark { order: 1; margin-left: auto; content: "✓"; }
}

/* FC04 · checkboxes and radios (2026-09-10): round, in ink (FC03's ink,
   made round). At rest the field's edge and fill; a ticked checkbox fills
   with the text colour while its tick wipes in, in the ground's colour; a
   chosen radio takes a thick ring. The control carries the house ring on
   keyboard focus. The consent box stays FI01's square for now: the user is
   deciding whether it follows.
     <fieldset class="checks">
       <legend>Wie sollen wir uns melden?</legend>
       <label class="check"><input type="radio" name="weg" value="telefon"> Telefon</label>
       <label class="check"><input type="radio" name="weg" value="email"> E-Mail</label>
     </fieldset> */
.checks, .pills { margin: 0; padding: 0; border: 0; min-width: 0; }
/* floated, so the legend lays out as the group's first row, like a field label */
.checks legend, .pills legend {
  float: left; width: 100%; margin: 0; padding: 0;
  font-family: var(--font-label, var(--font-body)); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--fg-dim);
}
.checks { display: grid; gap: 14px; }
.check { display: flex; align-items: flex-start; gap: var(--sp-2); font-size: var(--fs-body); line-height: 1.5; color: var(--fg); cursor: pointer; }
.check input {
  appearance: none; flex: none; display: grid; place-items: center;
  width: 22px; height: 22px; margin: 1px 0 0; border-radius: 50%;
  border: 1px solid var(--field-edge, var(--fg-dim)); background: var(--field-fill);
  cursor: pointer; transition: background-color .3s, border-color .3s, border-width .3s var(--ease-out);
}
.check input:checked { border-color: var(--fg); }
.check input[type="checkbox"]:checked { background: var(--fg); }
.check input[type="checkbox"]::after {
  content: ""; width: 14px; height: 14px; background: var(--bg);
  -webkit-mask: var(--tick-icon) center / contain no-repeat;
          mask: var(--tick-icon) center / contain no-repeat;
  clip-path: inset(0 100% 0 0); transition: clip-path .35s var(--ease-out) .04s;
}
.check input[type="checkbox"]:checked::after { clip-path: inset(0); }
.check input[type="radio"]:checked { border-width: 6.5px; }
.check input:user-invalid { border-color: var(--err, var(--brand)); }
@media (hover: hover) { .check:hover input:not(:checked) { border-color: var(--fg-dim); } }
@media (prefers-reduced-motion: reduce) { .check input, .check input::after { transition: none; } }
/* forced-colours mode drops fills, so the tick takes a system colour */
@media (forced-colors: active) { .check input[type="checkbox"]:checked::after { background: CanvasText; } }

/* FG02 · pills (2026-09-10) — a question with short answers: each is a
   round pill that fills with the text colour when chosen, as a tick slides
   in. Checkboxes for "choose any", radios for "choose one". The input
   covers its pill, so the whole pill is the target.
     <fieldset class="pills">
       <legend>Wobei können wir helfen? <span class="field__opt">(Mehrfachauswahl)</span></legend>
       <label class="pill"><input type="checkbox" name="thema" value="erp"><span>ERP-Beratung</span></label>
       <label class="pill"><input type="checkbox" name="thema" value="shop"><span>E-Commerce</span></label>
     </fieldset> */
.pills { display: flex; flex-wrap: wrap; gap: 10px; }
.pill {
  position: relative; display: inline-flex; align-items: center; min-height: 44px; padding: 0 18px;
  border: 1px solid var(--field-edge, var(--fg-dim)); border-radius: 999px;
  color: var(--fg); font-size: 15px; font-weight: 600; line-height: 1.2; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color .3s, color .3s, border-color .3s;
}
.pill input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.pill span::before {
  content: ""; display: inline-block; width: 0; height: 16px; vertical-align: -3px;
  background: currentColor;
  -webkit-mask: var(--tick-icon) left center / 16px no-repeat;
          mask: var(--tick-icon) left center / 16px no-repeat;
  transition: width .3s var(--ease-out), margin .3s var(--ease-out);
}
.pill:has(:checked) { background: var(--fg); border-color: var(--fg); color: var(--bg); }
.pill:has(:checked) span::before { width: 16px; margin-right: 8px; }
.pill:has(:focus-visible) { outline: 2px solid var(--fg); outline-offset: 3px; }
@media (hover: hover) { .pill:not(:has(:checked)):hover { border-color: var(--fg-dim); } }
@media (prefers-reduced-motion: reduce) { .pill span::before { transition: none; } }
@media (forced-colors: active) { .pill:has(:checked) { background: Highlight; border-color: Highlight; color: HighlightText; } }

/* FU01 · upload (2026-09-10) — the native file input, hidden but
   focusable; its label is a ghost button, with no arrow since it leads
   nowhere. The chosen files list below it, each with its size and a remove
   button: the behaviours.js upload module keeps the list and the input in
   step, and data-max (MB) marks files that are too large and leaves them
   out. Nothing is sent until the form is.
     <div class="upload" data-upload data-max="10">
       <span class="upload__label" id="u-l">Unterlagen <span class="field__opt">(optional)</span></span>
       <div class="upload__row">
         <input id="u" name="u" type="file" multiple accept=".pdf,.docx,.jpg,.jpeg,.png" aria-labelledby="u-l u-b" aria-describedby="u-h">
         <label class="btn btn--ghost" id="u-b" for="u">Dateien auswählen</label>
         <span class="upload__none">Keine Datei ausgewählt</span>
       </div>
       <small class="upload__hint" id="u-h">PDF, DOCX, JPG oder PNG, bis 10 MB je Datei.</small>
       <ul class="upload__list"></ul>
     </div> */
.upload { display: grid; gap: 9px; align-content: start; }
.upload__label { font-family: var(--font-label, var(--font-body)); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--fg-dim); }
.upload__row { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; }
.upload input[type="file"] { position: absolute; left: 0; top: 0; width: 1px; height: 1px; opacity: 0; overflow: hidden; clip-path: inset(50%); }
.upload input[type="file"]:focus-visible + label { outline: 2px solid var(--fg); outline-offset: 3px; }
.upload__none, .upload__hint { font-size: var(--fs-small); color: var(--fg-dim); }
.upload__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.upload__list:empty { display: none; }
.upload__item {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: 4px 12px;
  padding: 6px 6px 6px 14px; border: 1px solid var(--hairline); background: var(--field-fill);
  font-size: 15px; color: var(--fg);
}
.upload__item svg { width: 20px; height: 20px; fill: none; stroke: var(--fg-dim); stroke-width: var(--icon-stroke, 1.5); stroke-linecap: round; stroke-linejoin: round; }
.upload__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload__meta { font-size: var(--fs-small); color: var(--fg-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.upload__item.is-bad { border-color: var(--err, var(--brand)); }
.upload__item.is-bad > svg { stroke: var(--err, var(--brand)); }
.upload__item.is-bad .upload__meta { color: var(--err, var(--brand)); }
.upload__rm { display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0; background: none; color: var(--fg-muted); cursor: pointer; transition: background-color .3s, color .3s; }
.upload__rm svg { width: 18px; height: 18px; stroke: currentColor; }
@media (hover: hover) { .upload__rm:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); color: var(--fg); } }
.form .btn { justify-self: start; }

/* ---------- sticky CTA bar ----------
   Canonical since 2026-09-11 (ERP v4 reference): a small bar floating
   centred above the bottom edge. The page's main action as an outlined
   button, then the contact routes as GC01 icon links. It holds no filled
   accent, so it never competes with the hero's or the CTA's. behaviours.js
   shows it once the hero has left and hides it (inert) when the final CTA
   or the footer arrives, so it never covers the page's end; over a light
   band it takes the light palette, with a soft shadow to lift it off the
   ground. On phones it becomes a full-width bar on the bottom edge.
     <div class="sticky-cta" role="region" aria-label="Schnellkontakt" aria-hidden="true" inert>
       <a class="btn btn--ghost" href="#kontakt">Main action<span class="arrow" aria-hidden="true"></span></a>
       <a class="sticky-cta__mini" href="tel:…" aria-label="Anrufen"><svg class="ct ct--phone" …></a>
       <a class="sticky-cta__mini" href="https://wa.me/…" aria-label="WhatsApp"><svg class="ct ct--chat" …></a>
       <a class="sticky-cta__mini" href="mailto:…" aria-label="E-Mail"><svg class="ct ct--mail" …></a>
     </div> */
.sticky-cta {
  position: fixed; left: 50%; bottom: var(--sp-4); z-index: 9;
  display: flex; align-items: center; gap: 5px;
  padding: var(--sp-1) 10px;
  color: var(--fg);
  background: color-mix(in srgb, var(--bg-2) 74%, transparent);
  backdrop-filter: blur(14px);
  border: 1px solid color-mix(in srgb, var(--fg) 7%, transparent);
  border-radius: calc(var(--radius-btn) + min(var(--radius-btn), var(--sp-1)));
  transform: translate(-50%, calc(100% + var(--sp-5)));
  transition: transform .55s var(--ease-out), background-color .3s, border-color .3s, box-shadow .3s;
}
.sticky-cta[data-shown="true"] { transform: translate(-50%, 0); }
.sticky-cta[data-tone="light"] {
  background: color-mix(in srgb, var(--bg) 84%, transparent);
  border-color: var(--hairline);
}
/* Only while shown: parked below the fold, the shadow's blur would still
   reach up into the first screen as a faint smudge. */
.sticky-cta[data-tone="light"][data-shown="true"] { box-shadow: 0 18px 46px color-mix(in srgb, var(--fg) 13%, transparent); }
.sticky-cta .btn {
  --btn-h: 40px;
  padding: 0 17px;
  font-size: var(--fs-small);
  color: color-mix(in srgb, var(--fg) 82%, transparent);
  border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
}
.sticky-cta__mini {
  display: grid; place-items: center; flex: none; width: 38px; height: 38px;
  border-radius: var(--radius-btn);
  color: var(--fg-dim);
  transition: color .25s, background-color .25s;
}
.sticky-cta__mini .ct { width: 21px; height: 21px; }
@media (hover: hover) {
  .sticky-cta .btn:hover { background: color-mix(in srgb, var(--fg) 5%, transparent); border-color: color-mix(in srgb, var(--fg) 26%, transparent); color: var(--fg); }
  .sticky-cta__mini:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 6%, transparent); }
}
@media (max-width: 900px) {
  .sticky-cta {
    left: 0; right: 0; bottom: 0; gap: var(--sp-1);
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
    border: 0; border-top: 1px solid var(--hairline); border-radius: 0;
    background: color-mix(in srgb, var(--bg-2) 96%, transparent);
    transform: translateY(120%);
  }
  .sticky-cta[data-shown="true"] { transform: translateY(0); }
  .sticky-cta .btn { --btn-h: 48px; flex: 1 1 auto; }
  .sticky-cta__mini { width: 48px; height: 48px; }
}
@media (prefers-reduced-motion: reduce) { .sticky-cta { transition: none; } }

/* ---------- footer ----------
   Canonical since 2026-09-11 (ERP v4 reference), a frame band
   (data-band="frame"): dark in every ground mode but "light". Four columns: the brand (logo or wordmark, one line in
   the client's words, the social profiles it really has), two link lists,
   the contact address; under a hairline the copyright and the legal links,
   which a German site never omits. Plain text links, no contact icons. A
   column the client has nothing for is left out, not padded.
     <footer class="footer" data-band="frame"><div class="wrap">
       <div class="footer__grid">
         <div class="footer__brand"> .wordmark · <p> · <ul class="footer__social"> </div>
         <nav class="footer__col" aria-label="Footer"><h3 class="footer__h">Navigation</h3><ul>…</ul></nav>
         <div class="footer__col"><h3 class="footer__h">Leistungen</h3><ul>…</ul></div>
         <div class="footer__col"><h3 class="footer__h">Kontakt</h3><address>…</address></div>
       </div>
       <div class="footer__bottom"><small>© …</small><nav aria-label="Rechtliches">Impressum · Datenschutz</nav></div>
     </div></footer> */
.footer { padding-block: var(--sp-8) var(--sp-5); border-top: 1px solid var(--hairline); }
.footer__grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1.2fr; gap: var(--sp-6); }
.footer__brand .wordmark { display: inline-block; margin-bottom: var(--sp-4); color: var(--fg); text-decoration: none; }
/* LOGO-02: at least 44px tall, at most 240px wide (36px and 200px on phones). */
.footer__brand .wordmark img { height: 44px; max-width: 240px; }
@media (max-width: 767px) { .footer__brand .wordmark img { max-width: 200px; } }
.footer__brand p { max-width: 34ch; font-size: var(--fs-small); }
.footer__social { list-style: none; margin: var(--sp-4) 0 0; padding: 0; display: flex; gap: 14px; }
.footer__social a {
  display: grid; place-items: center; width: 38px; height: 38px;
  border: 1px solid var(--hairline); border-radius: var(--radius-btn);
  color: var(--fg-muted);
  transition: color .3s, background-color .3s, border-color .3s;
}
.footer__social svg { width: 16px; height: 16px; }
.footer__h {
  margin: 0 0 var(--sp-4);
  font-family: var(--font-label, var(--font-body)); font-size: var(--fs-eyebrow); font-weight: 500;
  font-stretch: normal; letter-spacing: var(--ls-eyebrow); text-transform: uppercase; line-height: 1.4;
  color: var(--fg-dim);
}
.footer__col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.footer__col a { color: var(--fg-muted); text-decoration: none; font-size: var(--fs-small); transition: color .3s; }
.footer address { font-style: normal; font-size: var(--fs-small); line-height: 1.8; color: var(--fg-muted); }
.footer__bottom {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--sp-3);
  margin-top: var(--sp-8); padding-top: var(--sp-4); border-top: 1px solid var(--hairline);
  font-family: var(--font-label, var(--font-body)); font-size: var(--fs-eyebrow); color: var(--fg-dim);
}
.footer__bottom small { font-size: inherit; }
.footer__bottom nav { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.footer__bottom a { color: inherit; text-decoration: none; transition: color .3s; }
@media (hover: hover) {
  .footer__col a:hover, .footer__bottom a:hover { color: var(--fg); }
  .footer__social a:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 6%, transparent); border-color: color-mix(in srgb, var(--fg) 30%, transparent); }
}
@media (max-width: 1080px) { .footer__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 640px)  { .footer__grid { grid-template-columns: 1fr; } }

/* ============================================================
   REVEAL ON ENTER

   By default there is nothing to write (2026-09-11): behaviours.js arms
   every section's content itself (the eyebrow, the text, the actions,
   each card, list item and figure) with the soft rise below, siblings
   one after another. Headings still take the line mask by markup.
     data-reveal="none"                              on a section or block: leave it still

   Usage by hand, for a piece that wants a different entrance:

     <div class="reveal">…</div>                     rise + fade
     <div class="reveal" data-reveal="left">…</div>  variants: left right scale fade soft
     <ul data-reveal-group> <li class="reveal">…     children stagger in DOM order

   The hidden state is gated behind html.reveal-ready, which behaviours.js
   adds only once it can guarantee a reveal. Ungate it and a JS failure, an
   old browser or a blocked script leaves the page permanently blank — the
   most common way this effect goes wrong in production.
   ============================================================ */
:root {
  --ease-out:        cubic-bezier(.16, 1, .3, 1);  /* expo-out: fast start, long settle */
  --reveal-distance: 34px;
  --reveal-duration: .9s;
  --reveal-stagger:  90ms;

  /* House heading reveal */
  --heading-duration: 1.15s;
  --heading-stagger:  100ms;
  --heading-lift:     26px;   /* how far the clipped frame travels */
  --illuminate-dim:   .16;    /* resting opacity of an unlit word */
}

/* An ANIMATION, not a transition — deliberately. `transition` is a
   shorthand, so a reveal written that way silently clobbers any
   transition the element already declares: put .reveal on a .tile and
   its hover transition dies. Animations occupy a separate channel and
   compose cleanly with whatever the component already does. */
html.reveal-ready .reveal { opacity: 0; }

html.reveal-ready .reveal.is-in {
  animation: reveal-up var(--reveal-duration) var(--ease-out) forwards;
  /* --reveal-i is set by behaviours.js, and only inside a group. */
  animation-delay: calc(var(--reveal-i, 0) * var(--reveal-stagger));
}
html.reveal-ready .reveal[data-reveal="left"].is-in  { animation-name: reveal-left; }
html.reveal-ready .reveal[data-reveal="right"].is-in { animation-name: reveal-right; }
html.reveal-ready .reveal[data-reveal="scale"].is-in { animation-name: reveal-scale; }
html.reveal-ready .reveal[data-reveal="fade"].is-in  { animation-name: reveal-fade; }
html.reveal-ready .reveal[data-reveal="soft"].is-in  { animation-name: reveal-soft; }
/* The default for content: half the distance, so a section's text arrives
   calmly under its heading instead of every block making an entrance. */
@keyframes reveal-soft {
  from { opacity: 0; transform: translateY(calc(var(--reveal-distance) * .45)); }
  to   { opacity: 1; transform: none; }
}

/* To add a variant: one @keyframes here plus one selector above. */
@keyframes reveal-up {
  from { opacity: 0; transform: translateY(var(--reveal-distance)); }
  to   { opacity: 1; transform: none; }
}
@keyframes reveal-left {
  from { opacity: 0; transform: translateX(calc(var(--reveal-distance) * -1)); }
  to   { opacity: 1; transform: none; }
}
@keyframes reveal-right {
  from { opacity: 0; transform: translateX(var(--reveal-distance)); }
  to   { opacity: 1; transform: none; }
}
@keyframes reveal-scale {
  from { opacity: 0; transform: scale(.965); }
  to   { opacity: 1; transform: none; }
}
@keyframes reveal-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Printed pages never run the observer — never print a blank sheet. */
@media print {
  html.reveal-ready .reveal { opacity: 1 !important; animation: none !important; }
}

/* Safety net if the preference changes after load. behaviours.js also
   declines to arm the effect at all when reduced motion is already set. */
@media (prefers-reduced-motion: reduce) {
  html.reveal-ready .reveal { opacity: 1; animation: none; }
}

/* ============================================================
   CANONICAL HEADING TREATMENTS

   Two, and only two. Which one a heading gets is a rhythm decision,
   not a taste one — the counts are in house-rules.md.

     <h2 class="reveal" data-reveal="lines">   the default, hero included
     <h2 class="illuminate">                    a pivot statement, rarely

   Never both on one heading. (Focus pull was tried for the hero and
   dropped on 2026-09-10 — don't bring it back.)
   ============================================================ */

/* ---- Line mask with counter-move (scroll-triggered, the default) ----
   Frame and text travel different distances, so the heading arrives with
   depth rather than sliding as one plane. The .ln structure is built by
   behaviours.js, so without JS the heading is simply plain text. */
html.reveal-ready .reveal[data-reveal="lines"]{opacity:1}
/* Descender room. The mask is the line box, and at --lh-tight an italic
   accent hangs its g, p and q about 1px below it. Pad the mask and pull
   the layout back by the same amount: the clip grows, nothing moves.
   The hidden state stays clipped: 112% travel on a line box of at least
   1.08em clears the extra .12em. */
.reveal[data-reveal="lines"] .ln{display:block;overflow:hidden;padding-bottom:.12em;margin-bottom:-.12em}
.reveal[data-reveal="lines"] .ln__i{display:block;transform:translateY(112%);opacity:.001}
.reveal[data-reveal="lines"].is-in .ln{
  animation:heading-frame var(--heading-duration) var(--ease-out) forwards;
  animation-delay:calc(var(--reveal-i, 0) * var(--heading-stagger));
}
.reveal[data-reveal="lines"].is-in .ln__i{
  animation:heading-text var(--heading-duration) var(--ease-out) forwards;
  animation-delay:calc(var(--reveal-i, 0) * var(--heading-stagger));
}
@keyframes heading-frame{
  from{transform:translateY(var(--heading-lift))}
  to{transform:none}
}
@keyframes heading-text{
  from{transform:translateY(112%);opacity:.001}
  to{transform:none;opacity:1}
}

/* ---- Word illumination (scroll-linked, sparing) ----
   Bound to scroll position, so it un-lights on the way back up. No gate
   class needed: without JS the .word spans never exist and the heading
   renders normally. */
.illuminate .word{opacity:var(--illuminate-dim);transition:opacity .5s var(--ease-out)}
.illuminate .word.lit{opacity:1}

@media (prefers-reduced-motion: reduce){
  .reveal[data-reveal="lines"] .ln__i{transform:none;opacity:1;animation:none}
  .reveal[data-reveal="lines"] .ln{animation:none}
  .illuminate .word{opacity:1;transition:none}
}

/* ============================================================
   CONTENT MOTION — the adopted set (house-rules.md, type-lab 10/11/12/16)

     .stats / .odometer   figures that count in
     .hover-list          list rows + a trailing cursor card
     .marquee             a calm endless strip — rare
     .drum-section        one claim, one rotating word — max once per page

   Each is progressive: without JS (or under reduced motion) the markup
   renders as plain, complete content.
   ============================================================ */
:root {
  --odo-duration: 1.8s;
  --odo-stagger:  90ms;
}

/* ---- Odometer stats ----
   <div class="stats">
     <div class="stat">
       <p class="stat__value"><span class="odometer">500</span><span class="accent">+</span></p>
       <p class="stat__label">Projekte umgesetzt</p>
     </div> …
   Real, verifiable figures only. */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); border-top: 1px solid var(--hairline); }
.stat { padding: var(--sp-5) var(--sp-5) 0 0; }
.stat + .stat { border-left: 1px solid var(--hairline); padding-left: var(--sp-5); }
.stat__value {
  display: flex; max-width: none;
  font-family: var(--font-head); font-weight: var(--fw-head, 700);
  font-size: calc(var(--fs-display) * 1.25); line-height: 1;
  letter-spacing: var(--ls-head, var(--ls-display));
  color: var(--head-1, var(--fg)); font-variant-numeric: tabular-nums;
}
.stat__label { margin-top: var(--sp-2); font-size: var(--fs-small); line-height: 1.45; color: var(--fg-muted); max-width: 24ch; }
.odometer { display: inline-flex; }
.odo { display: inline-block; height: 1em; overflow: hidden; }
.odo__s { display: flex; flex-direction: column; }
.odo__s > span { height: 1em; line-height: 1; }
.odometer.is-in .odo__s {
  transition: transform var(--odo-duration) cubic-bezier(.19, 1, .22, 1);
  transition-delay: calc(var(--odo-i, 0) * var(--odo-stagger));
  transform: translateY(calc(var(--odo-d, 0) * -1em));
}

/* ---- Hover list ----
   <ul class="hover-list">
     <li><a href="…" data-tip="One-line promise">
       <span class="hl-n">01</span><span class="hl-t">Title</span><span class="hl-k">Meta · Meta</span>
     </a></li> …
   Rows are real links; the card is decoration for a fine pointer only. */
.hover-list { list-style: none; margin: var(--sp-7) 0 0; padding: 0; }
.hover-list a {
  position: relative; display: grid; grid-template-columns: 64px minmax(0, 1fr) auto;
  gap: var(--sp-5); align-items: center; padding-block: var(--sp-5);
  color: var(--fg); text-decoration: none;
}
.hover-list a::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: var(--hairline); transform-origin: left;
}
/* Dividers draw in once — gated on .hl-armed, which JS adds, so a failed
   script never leaves the list without its rules. */
.hover-list.hl-armed:not(.is-in) a::before { transform: scaleX(0); }
.hover-list.hl-armed.is-in a::before {
  transition: transform 1.2s var(--ease-out);
  transition-delay: calc(var(--hl-i, 0) * 80ms);
}
.hl-n { font-family: var(--font-label, inherit); font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow); color: var(--fg-dim); font-variant-numeric: tabular-nums; }
.hl-t {
  font-family: var(--font-head); font-weight: var(--fw-head, 700);
  font-size: calc(var(--fs-h2) * .8); line-height: 1.1;
  letter-spacing: var(--ls-head, var(--ls-display));
  transition: transform .5s var(--ease-out), color .3s, opacity .3s;
}
.hl-k { font-size: var(--fs-small); color: var(--fg-dim); text-align: right; }
.hover-list:hover .hl-t { opacity: .28; }
.hover-list a:hover .hl-t, .hover-list a:focus-visible .hl-t {
  opacity: 1; color: var(--head-2, var(--brand)); transform: translateX(var(--sp-3));
}
.hl-card {
  position: fixed; left: 0; top: 0; z-index: 20; width: 300px;
  padding: var(--sp-4); border-radius: var(--radius-card);
  background: var(--brand); color: var(--on-brand, var(--fg));
  font-size: var(--fs-body); line-height: 1.45; pointer-events: none;
  opacity: 0; transition: opacity .25s;
  transform: translate3d(var(--x, 0px), var(--y, 0px), 0) rotate(var(--rot, 0deg)) scale(var(--s, .8));
}
.hl-card[data-on="true"] { opacity: 1; }
.hl-card small { display: block; margin-bottom: var(--sp-2); font-family: var(--font-label, inherit); font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow); text-transform: uppercase; opacity: .8; }

/* ---- Marquee — rare, and calm ----
   <div class="marquee" aria-label="A, B, C">
     <div class="marquee__track" aria-hidden="true"><span>A</span><span>B</span>…</div>
   </div>
   Add .marquee--small for a quieter second row running the other way. */
.marquee { overflow: clip; display: grid; gap: var(--sp-3); }
.marquee__track { display: flex; width: max-content; will-change: transform; }
.marquee__track > * {
  font-family: var(--font-head); font-weight: var(--fw-head, 700);
  font-size: calc(var(--fs-display) * 1.6); line-height: 1.05;
  letter-spacing: var(--ls-head, var(--ls-display));
  color: var(--fg); padding-right: .45em; white-space: nowrap;
}
.marquee__track > :nth-child(even) { color: transparent; -webkit-text-stroke: 1.5px var(--fg-dim); }
.marquee--small .marquee__track > * { font-size: var(--fs-h2); color: var(--fg-dim); -webkit-text-stroke: 0; }

/* ---- Word drum — at most once per page ----
   <section class="drum-section">
     <div class="drum-stage wrap">
       <p class="drum-line" aria-label="Wir kennen A, B und C in- und auswendig.">
         <span aria-hidden="true">Wir kennen</span>
         <span class="drum" aria-hidden="true">
           <span data-note="ERP · …">A</span><span data-note="…">B</span>…
         </span>
         <span aria-hidden="true">in- und auswendig.</span>
       </p>
       <p class="drum-note" aria-hidden="true"></p>
     </div>
   </section>
   4–8 names of similar length. JS builds the cylinder and sets the pin
   length; without it, the first name simply stands in the sentence. */
.drum-section { position: relative; padding-block: 0; }
.drum-stage {
  position: sticky; top: 0; min-height: 100vh;
  display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center;
}
.drum-line {
  display: flex; flex-direction: column; align-items: center; gap: .1em; max-width: none;
  font-family: var(--font-head); font-weight: var(--fw-head, 700);
  font-size: var(--fs-display); line-height: 1.1;
  letter-spacing: var(--ls-head, var(--ls-display)); color: var(--head-1, var(--fg));
}
/* The turning word is the sentence's accent phrase: accent face + colour. */
.drum {
  display: inline-grid; height: 1.1em; margin-block: .3em; perspective: 900px;
  color: var(--head-2, var(--brand));
  font-family: var(--font-accent, inherit); font-style: var(--accent-style, normal);
  font-weight: var(--fw-accent, inherit); font-synthesis: none;
}
.drum:not(.is-built) > :not(:first-child) { display: none; }
.drum > * { grid-area: 1 / 1; }
.drum__sizer { visibility: hidden; display: grid; }
.drum__sizer > span { grid-area: 1 / 1; }
.drum__cyl { position: relative; transform-style: preserve-3d; transform: rotateX(var(--drum-a, 0deg)); }
.drum__cyl > span {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  backface-visibility: hidden; opacity: var(--o, 1);
  transform: rotateX(var(--face-a, 0deg)) translateZ(var(--drum-r, .95em));
}
.drum-note { margin-top: var(--sp-6); min-height: 1.65em; color: var(--fg-muted); text-align: center; }
.drum-dots { display: flex; gap: var(--sp-1); justify-content: center; margin-top: var(--sp-4); }
.drum-dots i { width: 28px; height: 2px; background: var(--hairline); transition: background .3s; }
.drum-dots i[data-on="true"] { background: var(--brand); }

@media (prefers-reduced-motion: reduce) {
  .drum-stage { position: relative; min-height: 0; padding-block: var(--section-pad); }
  .hl-card { display: none; }
}

/* ============================================================
   PARALLAX SET — favourites (references/parallax.md, parallax-lab)

     .depth-stage       04  layered depth — a hero or chapter opener
     .stagger-cols      05  staggered columns — galleries, photo walls
     .expand-media      06  expanding media — pinned; never the first section
     .pinned-backdrop   07  pinned backdrop — three short statements
     .h-track           08  horizontal track — 4–8 projects; pinned
     .type-rows         09  counter-scrolling type — decoration only
     .pointer-depth     12  pointer depth — tier C, one per SITE

   Tier B (04–09): each at most once per page, never two back to back.
   The canonical image drift (01) is .parallax, above — not part of
   this set. Everything here is opt-in by class: no page changes until
   it uses one. JS only writes custom properties (and 08's measured
   height); CSS turns them into transforms, so each knob is a custom
   property declared on the component, with its default.
   Without JS, and under reduced motion, every one of them is plain,
   complete content: planes at rest, 06 and 08 un-pinned, 12 static.
   Sticky needs every ancestor free of overflow:hidden — use clip.
   ============================================================ */

/* ---- Layered depth (04) ----
   <section class="depth-stage">
     <div class="depth-stage__bg"><img src="…" alt=""></div>
     <div class="wrap depth-stage__copy">
       <p class="eyebrow">…</p>
       <h2 class="reveal" data-reveal="lines">…</h2>
       <p class="lead">…</p>
     </div>
     <figure class="depth-stage__card depth-stage__card--a"><img src="…" alt="…"></figure>
     <figure class="depth-stage__card depth-stage__card--b"><img src="…" alt="…"></figure>
   </section>
   Speed per plane is --depth-speed, a fraction of the stage height.
   Text rides the slow plane only. JS caps the background's travel at
   its overscan, so raising its speed can never show an edge. */
.depth-stage {
  --depth-overscan: 18%;    /* background bleed above and below; its travel is capped to this */
  --depth-scrim: 55%;       /* how far the photo is tinted toward the page ground */
  --depth-shadow: 0 var(--sp-6) var(--sp-8) color-mix(in srgb, var(--bg) 70%, transparent);
  position: relative;
  height: 120vh;
  min-height: 720px;
  padding-block: 0;
  overflow: clip;
}
.depth-stage__bg   { position: absolute; inset: calc(var(--depth-overscan) * -1) 0; --depth-speed: .15; }
.depth-stage__copy { position: absolute; left: 0; right: 0; top: 30%; z-index: 2; --depth-speed: .05; }
.depth-stage__card {
  position: absolute; z-index: 3; margin: 0; overflow: hidden;
  border-radius: var(--radius-card); box-shadow: var(--depth-shadow);
  --depth-speed: -.28;
}
.depth-stage__card--a { width: clamp(180px, 21vw, 360px); aspect-ratio: 4 / 5; right: var(--gutter); top: 40%; }
.depth-stage__card--b {
  width: clamp(130px, 12vw, 210px); aspect-ratio: 3 / 4; top: 72%;
  right: calc(var(--gutter) + clamp(180px, 21vw, 360px) - var(--sp-6));
  --depth-speed: -.55;
}
.depth-stage :is(.depth-stage__bg, .depth-stage__card) img { display: block; width: 100%; height: 100%; object-fit: cover; }
.depth-stage__bg img { filter: saturate(.85); }
.depth-stage::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, var(--bg) 0%,
    color-mix(in srgb, var(--bg) var(--depth-scrim), transparent) 22%,
    color-mix(in srgb, var(--bg) var(--depth-scrim), transparent) 72%, var(--bg) 100%);
}
.depth-stage__copy :is(h1, h2) { max-width: 16ch; text-wrap: balance; }
.depth-stage__copy :is(h1, h2) + p { margin-top: var(--sp-5); }
.depth-stage :is(.depth-stage__bg, .depth-stage__copy, .depth-stage__card) {
  transform: translate3d(0, var(--depth-y, 0px), 0);
  will-change: transform;
}

/* ---- Staggered columns (05) ----
   <div class="stagger-cols">
     <div class="wrap stagger-cols__grid">
       <div class="stagger-cols__col">
         <div class="stagger-cols__item"><img src="…" alt="…"></div> …
       </div> … three columns
     </div>
   </div>
   Photographs only, never text cards. The column is the motion, so the
   items are plain frames: no .parallax inside. --stagger-travel is the
   fastest column's travel AND the block padding that hides it, so the
   clip can never cut into a frame; --stagger-speed (−1…1) is each
   column's share of it. */
.stagger-cols {
  --stagger-travel: var(--sp-9);
  padding-block: var(--stagger-travel);
  overflow: clip;
}
.stagger-cols__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-5); }
.stagger-cols__col {
  display: flex; flex-direction: column; gap: var(--sp-5);
  --stagger-speed: .375;
  transform: translate3d(0, var(--stagger-y, 0px), 0);
  will-change: transform;
}
.stagger-cols__col:nth-child(2) { margin-top: var(--sp-9); --stagger-speed: -.625; }
.stagger-cols__col:nth-child(3) { --stagger-speed: 1; }
.stagger-cols__item { aspect-ratio: 4 / 5; overflow: hidden; border-radius: var(--radius-card); background: var(--bg-2); }
.stagger-cols__item img { display: block; width: 100%; height: 100%; object-fit: cover; filter: saturate(.85); }

/* ---- Expanding media (06) — pinned ----
   <section class="expand-media">
     <div class="expand-media__stage">
       <div class="wrap expand-media__intro">
         <p class="eyebrow">Showreel</p>
         <h2 class="reveal" data-reveal="lines">…</h2>
       </div>
       <div class="expand-media__frame"><img src="…" alt="…"></div>
       <div class="wrap expand-media__outro">
         <p class="eyebrow">Kapitel 02</p>
         <h2 class="reveal" data-reveal="lines">…</h2>
       </div>
     </div>
   </section>
   Unpinned (no JS, reduced motion) it reads top to bottom: the intro,
   then the photograph at full bleed with the chapter headline on it.
   JS adds .is-pinned: the stage holds for --expand-pin while the window
   opens to full bleed and the outro lands. The scrim tints toward the
   page ground, so the headline reads on either skin. */
.expand-media {
  --expand-pin: 280vh;      /* scroll spent while pinned; ~180vh on mobile, below */
  --expand-inset-y: 30%;    /* the starting window */
  --expand-inset-x: 34%;
  --expand-scale: 1.3;      /* the photo's counter-scale while the window opens */
  --expand-scrim: .45;
  position: relative;
  padding-block: 0;
}
.expand-media__stage { position: relative; overflow: clip; }
.expand-media__intro {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding-block: var(--section-pad) var(--sp-7);
}
.expand-media__intro :is(h2, p) { max-width: 24ch; text-wrap: balance; }
.expand-media__intro .eyebrow { max-width: none; }
.expand-media__frame { position: relative; height: 100vh; overflow: clip; }
.expand-media__frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
.expand-media__frame::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: var(--bg); opacity: var(--expand-scrim);
}
.expand-media__outro { position: absolute; left: 0; right: 0; bottom: var(--sp-8); z-index: 1; }
.expand-media__outro :is(h2, p) { max-width: 18ch; text-wrap: balance; }
.expand-media__outro .eyebrow { max-width: none; }
@media (prefers-reduced-motion: no-preference) {
  .expand-media.is-pinned { height: var(--expand-pin); }
  .expand-media.is-pinned .expand-media__stage { position: sticky; top: 0; height: 100vh; }
  .expand-media.is-pinned .expand-media__frame {
    position: absolute; inset: 0; height: auto;
    clip-path: inset(calc(var(--expand-inset-y) * (1 - var(--expand-p, 0)))
                     calc(var(--expand-inset-x) * (1 - var(--expand-p, 0)))
                     round calc(var(--radius-card) * (1 - var(--expand-p, 0))));
  }
  .expand-media.is-pinned .expand-media__frame img {
    transform: scale(calc(var(--expand-scale) - (var(--expand-scale) - 1) * var(--expand-p, 0)));
    will-change: transform;
  }
  .expand-media.is-pinned .expand-media__frame::after { opacity: calc(var(--expand-scrim) * var(--expand-dim, 0)); }
  .expand-media.is-pinned .expand-media__intro {
    position: absolute; left: 0; right: 0; top: calc(var(--header-h) + var(--sp-4)); z-index: 1;
    padding-block: 0; pointer-events: none;
    opacity: calc(1 - var(--expand-in, 0));
    transform: translateY(calc(var(--expand-in, 0) * var(--sp-6) * -1));
  }
  .expand-media.is-pinned .expand-media__intro h2 { font-size: calc(var(--fs-h2) * .75); }
  .expand-media.is-pinned .expand-media__outro {
    opacity: var(--expand-out, 0);
    transform: translateY(calc((1 - var(--expand-out, 0)) * var(--sp-6)));
  }
}

/* ---- Pinned backdrop (07) ----
   <section class="pinned-backdrop">
     <div class="pinned-backdrop__bg"><img src="…" alt=""></div>
     <div class="wrap pinned-backdrop__panels">
       <article class="pinned-backdrop__panel">
         <p class="eyebrow">Haltung · 01</p>
         <h3>…</h3>
         <p>Two sentences at most.</p>
       </article> … three panels
     </div>
   </section>
   Pure CSS sticky; JS only adds the slow settle (--backdrop-p). Under
   reduced motion it stays as it is — sticky is not motion — and only
   the settle goes (references/parallax.md). */
.pinned-backdrop {
  --backdrop-scale: 1.12;   /* the held photo settles from this to 1 */
  --backdrop-scrim: .3;
  --backdrop-lead: 75vh;    /* scroll before the first panel arrives */
  --backdrop-gap: 45vh;     /* between panels */
  --backdrop-tail: 35vh;    /* after the last */
  position: relative;
  padding-block: 0;
}
.pinned-backdrop__bg { position: sticky; top: 0; height: 100vh; overflow: clip; }
.pinned-backdrop__bg img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  transform: scale(calc(var(--backdrop-scale) - (var(--backdrop-scale) - 1) * var(--backdrop-p, 0)));
  will-change: transform;
}
.pinned-backdrop__bg::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: var(--bg); opacity: calc(var(--backdrop-scrim) + var(--backdrop-p, 0) * .2);
}
.pinned-backdrop__panels {
  position: relative; margin-top: -100vh;
  display: flex; flex-direction: column; gap: var(--backdrop-gap);
  padding-block: var(--backdrop-lead) var(--backdrop-tail);
}
.pinned-backdrop__panel {
  max-width: var(--measure); padding: var(--sp-6);
  border: 1px solid var(--hairline); border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
          backdrop-filter: blur(16px) saturate(1.2);
}
.pinned-backdrop__panel:nth-child(even) { margin-left: auto; }
.pinned-backdrop__panel :is(h2, h3) {
  font-size: calc(var(--fs-h2) * .7); line-height: var(--lh-head);
  letter-spacing: var(--ls-head, var(--ls-display)); text-wrap: balance;
}
.pinned-backdrop__panel :is(h2, h3) + p { margin-top: var(--sp-4); }

/* ---- Horizontal track (08) — pinned ----
   <section class="h-track">
     <div class="h-track__stage">
       <div class="wrap h-track__head">
         <div><p class="eyebrow">Arbeiten</p><h2 class="reveal" data-reveal="lines">…</h2></div>
       </div>
       <div class="h-track__rail">
         <article class="h-track__card">
           <div class="h-track__media"><img src="…" alt="…"></div>
           <p class="h-track__meta"><b>01 · Infrastruktur</b><span>Plattform</span></p>
         </article> … 4–8 cards
       </div>
     </div>
   </section>
   Unpinned it is a native horizontal swipe row: without JS, under reduced
   motion, and at ≤900px. On wider screens JS adds .is-pinned and sets the
   section's height from the measured rail (rail width − viewport + 100vh);
   never set that by hand. Each photo drifts sideways by --track-drift.
   Never the only route to what the cards show. */
.h-track {
  --track-drift: 10%;       /* sideways overscan of each photo — its travel */
  --track-card: min(clamp(260px, 28vw, 460px), 44vh);
  position: relative;
  padding-block: 0;
}
.h-track__stage {
  display: flex; flex-direction: column; justify-content: center; gap: var(--sp-6);
  padding-block: var(--section-pad);
}
.h-track__head { display: flex; align-items: end; justify-content: space-between; gap: var(--sp-5); }
.h-track__rail {
  display: flex; gap: var(--sp-5); padding-inline: var(--gutter);
  overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin;
  scroll-snap-type: x mandatory; scroll-padding-inline: var(--gutter);
}
.h-track__card { flex: none; width: var(--track-card); scroll-snap-align: start; }
.h-track__media { position: relative; aspect-ratio: 4 / 5; overflow: hidden; border-radius: var(--radius-card); background: var(--bg-2); }
.h-track__media img {
  position: absolute; top: 0; left: 50%; width: 100%; height: 100%;
  max-width: none;          /* a global img { max-width: 100% } would silently cancel the drift */
  object-fit: cover; filter: saturate(.85);
  transform: translate3d(calc(-50% + var(--px, 0px)), 0, 0);
}
.h-track__meta {
  display: flex; justify-content: space-between; gap: var(--sp-3); max-width: none;
  margin-top: var(--sp-3); font-size: var(--fs-small); color: var(--fg-dim);
}
.h-track__meta b { color: var(--fg); font-weight: 600; }
@media (prefers-reduced-motion: no-preference) and (min-width: 901px) {
  .h-track.is-pinned .h-track__stage {
    position: sticky; top: 0; height: 100vh; overflow: clip; padding-block: var(--sp-6);
  }
  .h-track.is-pinned .h-track__rail {
    width: max-content; overflow: visible; scroll-snap-type: none;
    transform: translate3d(var(--track-x, 0px), 0, 0); will-change: transform;
  }
  .h-track.is-pinned .h-track__media img { width: calc(100% + var(--track-drift) * 2); will-change: transform; }
}
/* The measured height is inline; drop it wherever the row isn't pinned. */
@media (prefers-reduced-motion: reduce), (max-width: 900px) { .h-track { height: auto !important; } }

/* ---- Counter-scrolling type (09) ----
   <section class="type-rows">
     <div class="type-rows__row" aria-hidden="true">
       <span>Strategie</span><i>·</i><span>Design</span><i>·</i> …
     </div>
     <div class="wrap type-rows__band"><div class="parallax"><img src="…" alt="…"></div></div>
     <div class="type-rows__row type-rows__row--accent" aria-hidden="true">…</div>
   </section>
   Decoration only: the rows are aria-hidden, so every word must also be
   on the page as real text. Rows alternate direction. The travel needs
   rows wider than 150vw, so JS repeats each row's words until it is twice
   the viewport; the section clips them. The band is a plain .parallax (01). */
.type-rows {
  --rows-size: clamp(var(--fs-display) * 1.6, 10vw, var(--fs-display) * 2.5);
  --rows-shift: -25vw;      /* resting offset, so a row can travel both ways */
  --rows-travel: 22vw;      /* either side of it */
  overflow: clip;
}
.type-rows__row {
  display: flex; width: max-content; white-space: nowrap;
  font-family: var(--font-head); font-weight: var(--fw-head, 700); font-stretch: var(--head-stretch, normal);
  font-size: var(--rows-size); line-height: var(--lh-tight);
  letter-spacing: var(--ls-head, var(--ls-display)); color: var(--head-1, var(--fg));
  transform: translate3d(calc(var(--rows-shift) + var(--rows-p, 0) * var(--rows-travel)), 0, 0);
  will-change: transform;
}
.type-rows__row > * { padding-right: .35em; }
.type-rows__row > i { font-style: normal; color: var(--fg-dim); }
.type-rows__row--accent {
  font-family: var(--font-accent, inherit); font-weight: var(--fw-accent, inherit);
  font-style: var(--accent-style, normal); font-stretch: normal;
  letter-spacing: var(--ls-accent, inherit); color: var(--head-2, var(--brand)); font-synthesis: none;
}
.type-rows__band { margin-block: var(--sp-6); }
.type-rows__band .parallax { aspect-ratio: 21 / 9; border-radius: var(--radius-card); }

/* ---- Pointer depth (12) — tier C, at most one per SITE ----
   <section class="pointer-depth">
     <div class="pointer-depth__bg"><img src="…" alt=""></div>
     <div class="wrap pointer-depth__copy">
       <p class="eyebrow">Kontakt</p>
       <h2 class="reveal" data-reveal="lines">…</h2>
       <p class="lead">…</p>
     </div>
     <figure class="pointer-depth__card pointer-depth__card--a"><img src="…" alt="…"></figure>
     <figure class="pointer-depth__card pointer-depth__card--b"><img src="…" alt="…"></figure>
   </section>
   A hero or the final CTA. The layers follow a mouse only: static on
   touch, at ≤900px and under reduced motion. Per plane, --ptr-depth is its
   travel in px (negative moves against the pointer) and --ptr-tilt, in
   degrees, makes it tilt. Keep --ptr-overscan larger than the background's
   depth. JS writes only --ptr-x / --ptr-y (−1…1) on the section. */
.pointer-depth {
  --ptr-overscan: 5%;
  --ptr-scrim: 55%;
  --ptr-shadow: 0 var(--sp-6) var(--sp-8) color-mix(in srgb, var(--bg) 70%, transparent);
  position: relative;
  height: 100vh;
  min-height: 640px;
  padding-block: 0;
  overflow: clip;
}
.pointer-depth__bg   { position: absolute; inset: calc(var(--ptr-overscan) * -1); --ptr-depth: -16; }
.pointer-depth__copy { position: absolute; left: 0; right: 0; top: 28%; z-index: 2; --ptr-depth: 8; }
.pointer-depth__card {
  position: absolute; z-index: 3; margin: 0; overflow: hidden;
  border-radius: var(--radius-card); box-shadow: var(--ptr-shadow);
  --ptr-depth: 28;
}
.pointer-depth__card--a { width: clamp(200px, 22vw, 360px); aspect-ratio: 4 / 5; right: calc(var(--gutter) + 5vw); top: 20%; --ptr-tilt: 7; }
.pointer-depth__card--b { width: clamp(120px, 11vw, 190px); aspect-ratio: 1; right: var(--gutter); top: 60%; --ptr-depth: 46; }
.pointer-depth :is(.pointer-depth__bg, .pointer-depth__card) img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pointer-depth__bg img { filter: saturate(.85); }
.pointer-depth::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, var(--bg) 0%,
    color-mix(in srgb, var(--bg) var(--ptr-scrim), transparent) 25%,
    color-mix(in srgb, var(--bg) var(--ptr-scrim), transparent) 70%, var(--bg) 100%);
}
.pointer-depth__copy :is(h1, h2) { max-width: 16ch; text-wrap: balance; }
.pointer-depth__copy :is(h1, h2) + p { margin-top: var(--sp-5); }
.pointer-depth :is(.pointer-depth__bg, .pointer-depth__copy, .pointer-depth__card) {
  transform: perspective(900px)
    translate3d(calc(var(--ptr-x, 0) * var(--ptr-depth, 0) * 1px), calc(var(--ptr-y, 0) * var(--ptr-depth, 0) * 1px), 0)
    rotateY(calc(var(--ptr-x, 0) * var(--ptr-tilt, 0) * 1deg))
    rotateX(calc(var(--ptr-y, 0) * var(--ptr-tilt, 0) * -1deg));
  will-change: transform;
}

/* ---- the set under reduced motion: every plane at rest ----
   (06 and 08 pin only under no-preference, above; 07's sticky stays.) */
@media (prefers-reduced-motion: reduce) {
  .depth-stage :is(.depth-stage__bg, .depth-stage__copy, .depth-stage__card),
  .stagger-cols__col,
  .pinned-backdrop__bg img,
  .pointer-depth :is(.pointer-depth__bg, .pointer-depth__copy, .pointer-depth__card) { transform: none; will-change: auto; }
  .h-track__media img { transform: translate3d(-50%, 0, 0); }
  .type-rows__row { transform: translate3d(var(--rows-shift), 0, 0); will-change: auto; }
}

/* ---- the set on mobile ---- */
@media (max-width: 900px) {
  .depth-stage__copy { top: var(--sp-9); }
  .depth-stage__card--a { top: auto; bottom: var(--sp-7); width: 42vw; }
  .depth-stage__card--b, .pointer-depth__card--b { display: none; }
  .stagger-cols__grid, .stagger-cols__col { gap: var(--sp-2); }
  .expand-media { --expand-pin: 180vh; }
  .h-track { --track-card: min(78vw, 360px); }
  /* 12 is static here, so it becomes an ordinary stacked section. */
  .pointer-depth {
    height: auto; min-height: 100vh; padding-block: var(--section-pad);
    display: flex; flex-direction: column; justify-content: center; gap: var(--sp-7);
  }
  .pointer-depth__copy, .pointer-depth__card--a { position: relative; top: auto; right: auto; }
  .pointer-depth__card--a { align-self: flex-end; width: 56vw; margin-right: var(--gutter); }
  .pointer-depth :is(.pointer-depth__bg, .pointer-depth__copy, .pointer-depth__card) { transform: none; }
}

/* ============================================================
   IMAGE PATTERNS (references/images.md, labs/image-lab.html)
   The thirteen patterns chosen on 2026-09-10. Each photograph is a
   .photo frame, so ratio, corner, border and grade come from the
   image system in tokens.css; a pattern only places frames.
   No captions (S4d): a photo states its fact through what it shows
   and the words beside it, and its alt text still describes it.
   Drift (01, .parallax) goes on the large frames. The small ones (a
   trio, a team grid, the portrait beside a quote, the contact card,
   the overlap's detail) stay still: at that size drift reads as jitter.
   Budgets (section-recipes.md): tier B once per page and never two
   back to back; I21 once per site; one full-bleed per page.
   ============================================================ */

/* I01 · framed split: one photo beside the text (the .team layout,
   for any section).
     <div class="wrap split-media">
       <div class="photo parallax"><img …></div>
       <div class="body">…</div>
     </div>
   Frame first = photo left. For a photo on the right, put the frame
   second and add .split-media--end. Keep one side for a run of
   sections; alternate at most two or three times. */
.split-media { display: grid; grid-template-columns: 55fr 45fr; gap: var(--sp-8); align-items: center; }
.split-media--end { grid-template-columns: 45fr 55fr; }
:is(.split-media, .overlap) .body > * + * { margin-top: var(--sp-4); }
:is(.split-media, .overlap, .team) .body > .link { margin-top: var(--sp-6); }

/* I02 · edge-bleed split: the photo runs to the viewport's right edge,
   the text keeps the gutter. Straight in the section, not in .wrap:
     <section class="split-media split-media--bleed">
       <div class="body">…</div>
       <div class="photo parallax" style="--parallax-focus: 30% 50%"><img …></div>
     </section>
   Never 100vw: it overflows by the scrollbar's width. */
.split-media--bleed { grid-template-columns: minmax(0, 1fr) minmax(0, .92fr); }
.split-media--bleed > .body { padding-left: var(--gutter); }
.split-media--bleed > .photo {
  aspect-ratio: var(--ratio-people, 3 / 4);
  border-right: 0;
  border-radius: var(--img-radius, var(--radius-card)) 0 0 var(--img-radius, var(--radius-card));
}

/* I03 · detail trio: three close crops of the work, from one shoot in
   one light, under the section's heading.
     <div class="trio">
       <figure><div class="photo photo--people"><img …></div><figcaption>Abläufe</figcaption></figure> …
     </div>
   The optional word under each names what it shows; it is content from
   the copy, not a photo caption. */
.trio { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4); margin-top: var(--sp-8); }
.trio figure, .team-grid figure { margin: 0; }
.trio .photo { aspect-ratio: var(--ratio-people, 3 / 4); }
.trio figcaption { margin-top: var(--sp-2); font-size: var(--fs-small); font-weight: 600; color: var(--fg-muted); }

/* I05 · case card with the software in context: a real, anonymised
   screenshot set into the card's photo and cropped by its lower edge.
     <div class="card__media"><img src="context.jpg" alt="">
       <img class="card__screen" src="screen.png" alt="" width="…" height="…"></div>
   Never a drawn or generated UI: without a real screen, leave it out. */
.card__media .card__screen {
  position: absolute; right: 7%; bottom: -10%;
  width: 58%; height: auto;
  border: 1px solid var(--hairline); border-radius: var(--img-radius-sm, 6px);
  box-shadow: 0 30px 60px -28px rgba(0, 0, 0, .75);
}
/* The screen holds still while the photo moves, and it is a UI, not a
   photograph, so it takes no grade. */
.card__media .card__screen,
.card:hover .card__media .card__screen,
[data-ui~="card-frame"] .card__media .card__screen { transform: none; filter: none; }

/* I06 · testimonial with portrait: the customer's own words beside a
   real portrait, the face turned toward the quote. Quote, name, role
   and company are required: without an approved quote, no block.
     <div class="wrap quote">
       <div>
         <p class="eyebrow">Referenzen</p>
         <blockquote>
           <p class="quote__text">„…“</p>
           <footer class="quote__who"><b>Name</b><span>Rolle, Firma</span></footer>
         </blockquote>
       </div>
       <div class="photo photo--people"><img …></div>
     </div> */
.quote { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: var(--sp-8); align-items: center; }
.quote blockquote { margin: 0; }
.quote__text {
  max-width: 30ch;
  font-family: var(--font-head); font-weight: 500; font-stretch: 100%;
  font-size: calc(var(--fs-h2) * .56); line-height: 1.3; letter-spacing: -.012em;
  color: var(--fg); text-wrap: pretty;
}
.quote__who { display: flex; flex-direction: column; gap: calc(var(--sp-1) / 2); margin-top: var(--sp-6); font-size: var(--fs-small); }
.quote__who b { font-size: var(--fs-body); font-weight: 600; color: var(--fg); }
.quote__who span { color: var(--fg-dim); }
.quote .photo { aspect-ratio: var(--ratio-people, 3 / 4); }

/* I07 · contact person beside the final CTA: "Ihr Ansprechpartner" with
   a real portrait and a direct line, once the client names the person.
   Phone and e-mail stay text links, so the section's one accent is still
   its button. In .cta it sits under the copy, or takes the column beside
   it when there is no form.
     <div class="person">
       <div class="photo photo--people"><img …></div>
       <div>
         <p class="eyebrow">Ihr Ansprechpartner</p>
         <p class="person__name">Name</p>
         <p class="person__role">Rolle</p>
         <ul class="prose"><li><a href="tel:…">…</a></li><li><a href="mailto:…">…</a></li></ul>
       </div>
     </div> */
.person {
  display: grid; grid-template-columns: clamp(128px, 12vw, 184px) minmax(0, 1fr);
  gap: var(--sp-5); align-items: center;
  padding: var(--sp-5); background: var(--bg-3);
  border: 1px solid var(--hairline); border-radius: var(--radius-card);
}
* + .person { margin-top: var(--sp-7); }
.person .photo { aspect-ratio: var(--ratio-people, 3 / 4); border: 0; border-radius: var(--img-radius-sm, 6px); }
.person .eyebrow { margin-bottom: var(--sp-2); }
.person__name {
  font-family: var(--font-head); font-weight: var(--fw-head, 700); font-stretch: var(--head-stretch, normal);
  font-size: var(--fs-h3); line-height: 1.3; color: var(--fg);
}
.person__role { margin-top: calc(var(--sp-1) / 2); font-size: var(--fs-small); color: var(--fg-dim); }
.person ul { list-style: none; margin: var(--sp-4) 0 0; padding: 0; display: grid; gap: var(--sp-1); }

/* I09 · hero stage: the offer in words first, then one wide photo or a
   real product screen that peeks into the first screen.
     <section class="hero hero--stage">
       <div class="wrap">
         <h1 …>…</h1>
         <div><p class="lead">…</p><a class="btn btn--accent" …>…</a><p class="trust">…</p></div>
         <div class="photo photo--band parallax hero__stage">
           <img … width="…" height="…" loading="eager" fetchpriority="high"></div>
       </div>
     </section>
   The stage image is the page's LCP element: never lazy. */
.hero--stage .wrap { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: end; }
.hero--stage h1 { margin-bottom: 0; }
.hero--stage .trust { margin-top: var(--sp-5); }
.hero__stage { grid-column: 1 / -1; }

/* I10 · overlap pair: a large scene with a small detail from the same
   shoot over its corner. The detail never drifts on its own (that would
   be parallax 04) and never covers a face.
     <div class="wrap overlap">
       <div class="overlap__media">
         <div class="photo photo--people parallax"><img …></div>
         <div class="photo overlap__detail"><img …></div>
       </div>
       <div class="body">…</div>
     </div> */
.overlap { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-8); align-items: center; }
.overlap__media { position: relative; padding: 0 0 12% 14%; }
.overlap__detail {
  position: absolute; left: 0; bottom: 0; width: 42%;
  border: 6px solid var(--bg);
  border-radius: calc(var(--img-radius, var(--radius-card)) + 6px);
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, .6);
}

/* I12 · figures docked to a photo: two or three real figures on a solid
   panel over the photo's lower edge, never on the picture itself.
     <div class="docked">
       <div class="photo photo--band parallax"><img …></div>
       <div class="docked__panel stats"><div class="stat">…</div> …</div>
     </div> */
.docked { position: relative; margin: var(--sp-8) 0; }
.docked__panel.stats {
  position: absolute; left: var(--sp-6); bottom: calc(var(--sp-8) * -1);
  grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: max-content;
  padding: var(--sp-2) var(--sp-6) var(--sp-6);
  background: var(--bg-3); border: 1px solid var(--hairline); border-radius: var(--radius-card);
}

/* I13 · portrait team grid: real people in one crop, with the same
   ratio, distance and grade, and the eye line on the upper third (set
   --eye per photo where it isn't). Name and role under each are content.
     <div class="team-grid">
       <figure><div class="photo photo--people"><img … style="--eye: 50% 25%"></div>
         <figcaption><b>Name</b><span>Rolle</span></figcaption></figure> …
     </div> */
.team-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-5) var(--sp-4); margin-top: var(--sp-8); }
.team-grid .photo { aspect-ratio: var(--ratio-people, 3 / 4); }
.team-grid .photo img { object-position: var(--eye, 50% 30%); }
.team-grid figcaption { margin-top: var(--sp-3); font-size: var(--fs-small); line-height: 1.5; }
.team-grid figcaption b { display: block; font-size: var(--fs-body); font-weight: 600; color: var(--fg); }
.team-grid figcaption span { color: var(--fg-dim); }

/* I17 · band: one photograph edge to edge, where the page takes a
   breath between two text-heavy sections. Outside .wrap, between the
   sections or after a section's heading:
     <div class="photo photo--band parallax band"><img …></div>
   Under 70vh, so the next section always peeks in. */
.band { max-height: 70vh; border: 0; border-radius: 0; }

/* I18 · CTA over a photograph: the final ask over people at work, with a
   scrim that keeps the text side solid. The alternative to I07, for a
   CTA without a form; never with the neon line.
     <section class="cta cta--image" id="kontakt">
       <div class="photo parallax cta__backdrop" style="--parallax-focus: 72% 50%"><img … alt=""></div>
       <div class="wrap"><div>… heading, one line, one accent button …</div></div>
     </section> */
.cta--image {
  position: relative; isolation: isolate; overflow: hidden;
  display: flex; align-items: center; min-height: min(78vh, 760px);
}
.cta__backdrop { position: absolute; inset: 0; z-index: -2; aspect-ratio: auto; border: 0; border-radius: 0; }
.cta--image::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--bg) 96%, transparent) 0%,
    color-mix(in srgb, var(--bg) 86%, transparent) 38%,
    color-mix(in srgb, var(--bg) 25%, transparent) 72%, transparent 100%);
}
.cta--image .btn { margin-top: var(--block-gap); }

/* I21 · diptych: the person and the thing they make, two photos edge to
   edge. Once per site; it is the page's full-bleed. Outside .wrap,
   after the section's heading:
     <div class="diptych">
       <div class="photo parallax"><img …></div>
       <div class="photo parallax"><img …></div>
     </div> */
.diptych { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: calc(var(--sp-1) / 2); margin-top: var(--sp-8); }
.diptych .photo { aspect-ratio: var(--ratio-people, 3 / 4); max-height: 92vh; border: 0; border-radius: 0; }

/* ---- the patterns on smaller screens ---- */
@media (max-width: 900px) {
  .split-media, .split-media--end, .split-media--bleed,
  .quote, .overlap { grid-template-columns: minmax(0, 1fr); gap: var(--sp-7); }
  .split-media--bleed > .body { padding-right: var(--gutter); }
  .split-media--bleed > .photo { aspect-ratio: var(--ratio-inline, 3 / 2); border-inline: 0; border-radius: 0; }
  .quote .photo { max-width: 26rem; }
  .team-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  .photo--band { aspect-ratio: var(--ratio-teaser, 16 / 9); }
  .trio { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .trio figure:nth-child(3) { grid-column: 1 / -1; }
  .trio figure:nth-child(3) .photo { aspect-ratio: var(--ratio-inline, 3 / 2); }
  .person { grid-template-columns: 96px minmax(0, 1fr); gap: var(--sp-4); padding: var(--sp-4); }
  .docked { margin-bottom: 0; }
  .docked__panel.stats {
    position: relative; left: auto; bottom: auto; margin: calc(var(--sp-7) * -1) var(--sp-4) 0;
    grid-auto-flow: row; grid-template-columns: 1fr 1fr; padding: var(--sp-2) var(--sp-5) var(--sp-5);
  }
  .cta--image::after {
    background: linear-gradient(180deg,
      color-mix(in srgb, var(--bg) 55%, transparent) 0%,
      color-mix(in srgb, var(--bg) 92%, transparent) 55%,
      color-mix(in srgb, var(--bg) 97%, transparent) 100%);
  }
  .diptych { grid-template-columns: minmax(0, 1fr); }
  .diptych .photo { aspect-ratio: var(--ratio-inline, 3 / 2); }
}

/* ============================================================
   WEBGL — one accent (references/webgl.md)
   Off until a section carries data-gl and the page loads
   webgl/wb-gl.js, the effect file and WBGL.auto() after
   behaviours.js. The runtime puts an aria-hidden canvas behind the
   host's content (z-index -1) and sets .gl-on while it runs, or
   .gl-off when the device can't; then the poster below stands in,
   so the section always looks finished.

     <section class="cta" id="kontakt" data-gl="neon-line"> … </section>

   Dark grounds only, and at most one in view.
   ============================================================ */
[data-gl] { position: relative; isolation: isolate; }

/* W04 poster: one still, soft line where the tube would run. */
[data-gl="neon-line"].gl-off::before,
[data-gl="neon-line"].gl-off::after {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  left: 6%; right: 6%; top: 50%; transform: translateY(-50%) rotate(-3deg);
}
[data-gl="neon-line"].gl-off::before {   /* halo */
  height: 48px; filter: blur(6px);
  background: radial-gradient(closest-side, color-mix(in srgb, var(--gl-1) 45%, transparent), transparent);
}
[data-gl="neon-line"].gl-off::after {    /* core */
  height: 2px;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--gl-1) 40%, #fff) 30%, color-mix(in srgb, var(--gl-1) 40%, #fff) 70%, transparent);
}

/* ============================================================
   FAVOURITES — opt-in alternatives (references/interaction.md).
   Off by default. A site switches one on with a token on <html>:
     <html lang="de" data-ui="btn-roll card-frame">
   That token is the site's whole configuration; these rules never
   change per site. behaviours.js wraps the labels the roll needs.
   ============================================================ */

/* B05 · label roll (btn-roll). A pill; label and clone roll up together,
   the fill stays canonical. The arrow steps aside — the roll is the motion. */
[data-ui~="btn-roll"] .btn { border-radius: 999px; }
[data-ui~="btn-roll"] .btn .arrow { display: none; }
.roll-label { position: relative; display: inline-flex; overflow: hidden; vertical-align: top; }
.roll-label__t { position: relative; display: inline-block; transition: transform .5s cubic-bezier(.4, 0, .1, 1); }
.roll-label__t::after { content: attr(data-text); position: absolute; left: 0; right: 0; top: 100%; }
:is(.btn, .card):focus-visible .roll-label__t { transform: translateY(-100%); }
@media (hover: hover) { :is(.btn, .card):hover .roll-label__t { transform: translateY(-100%); } }

/* C06 · frame tightens (card-frame). The frame insets with rounded corners
   while the image settles from 1.12 to 1 and the title rolls: counter-motion
   instead of a zoom. The arrow chip remains for touch only. */
[data-ui~="card-frame"] .card__media { clip-path: inset(0 round 0); transition: clip-path .75s var(--ease-out); }
[data-ui~="card-frame"] .card__media img { transform: scale(1.12); }
[data-ui~="card-frame"] .card:focus-visible .card__media { clip-path: inset(10px round 10px); }
[data-ui~="card-frame"] .card:focus-visible .card__media img { transform: none; }
@media (hover: hover) {
  [data-ui~="card-frame"] .card__go { display: none; }
  [data-ui~="card-frame"] .card:hover .card__media { clip-path: inset(10px round 10px); }
  [data-ui~="card-frame"] .card:hover .card__media img { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .roll-label__t, [data-ui~="card-frame"] .card__media { transition: none; }
}

/* FI02 · BDG's live field (field-soft), the secondary to FI01: 10px
   corners, a faint fill in the text colour, labels 14.5px/600 in the body
   face, entries 15px/1.5, a native checkbox. Focus lifts the fill and adds
   a 4px halo. The edge keeps --field-edge and the focus edge takes the full
   accent, both at 3:1 (live they are 1.5:1 and 2:1). */
[data-ui~="field-soft"] :is(.field, .consent, .check, .upload) {
  --field-fill: color-mix(in srgb, var(--fg) 4%, transparent);
  --field-glow: 0 0 0 4px var(--brand-soft);
}
[data-ui~="field-soft"] .field { gap: 8px; }
[data-ui~="field-soft"] :is(.field > label, .checks legend, .pills legend, .upload__label) { font-family: var(--font-body); font-size: 14.5px; font-weight: 600; letter-spacing: normal; text-transform: none; color: var(--fg); }
[data-ui~="field-soft"] .field :is(input, textarea, select) { font-family: var(--font-body); font-size: 15px; letter-spacing: normal; line-height: 1.5; border-radius: 10px; padding: 13px 15px; }
[data-ui~="field-soft"] .field textarea { min-height: 150px; }
[data-ui~="field-soft"] .field :is(input, textarea, select):focus { background: color-mix(in srgb, var(--fg) 6%, transparent); }
[data-ui~="field-soft"] .field__error { font-family: var(--font-body); font-size: var(--fs-small); letter-spacing: normal; text-transform: none; }
[data-ui~="field-soft"] .field__error::before { border-radius: 50%; }
[data-ui~="field-soft"] .consent input { appearance: auto; display: block; margin: 3px 0 0; border: 0; background: none; accent-color: var(--brand); }
[data-ui~="field-soft"] .consent input::after { content: none; }
[data-ui~="field-soft"] .consent label { font-size: var(--fs-small); line-height: 1.6; color: color-mix(in srgb, var(--fg) 85%, transparent); }
@media (pointer: coarse) { [data-ui~="field-soft"] .field :is(input, textarea, select) { font-size: 16px; } }

/* ---------- responsive ---------- */
@media (max-width: 900px) {
  :root { --fs-display: 40px; --fs-h2: 32px; --section-pad: var(--sp-8); }
  .hero .wrap, .split .wrap, .team .wrap > .cols,
  .cta .wrap { grid-template-columns: 1fr; gap: var(--sp-7); }
  .grid { grid-template-columns: 1fr; }
  .nav { display: none; }
  .menu-toggle { display: inline-flex; }
  .header .wrap { grid-template-columns: minmax(0, 1fr) auto; }
  .header__end > :not(.menu-toggle) { display: none; }   /* EN/DE and Kontakt live in the menu */
  .cards { grid-template-columns: 1fr; }
  .phase { grid-template-columns: 40px 1fr; }
  .phase p { grid-column: 2; }
  .stats { grid-template-columns: 1fr 1fr; }
  .stat:nth-child(odd) { border-left: 0; padding-left: 0; }
  .hover-list a { grid-template-columns: 40px minmax(0, 1fr); }
  .hl-k { display: none; }
}
/* Phones: the large steps shrink with the screen. At 96px a side, sections
   sat ~190px apart on a 390px screen, which read heavier than the desktop
   (the user, 2026-09-14). Scaling the steps, not each section, reaches every
   padding written as --sp-8/9/10, the CTA bands and footers included. The
   steps may tie on phones: sections padded with --sp-9 stay close to the
   kit's 56px instead of opening wider gaps than it. */
@media (max-width: 640px) {
  :root { --sp-8: 64px; --sp-9: 64px; --sp-10: 72px; --section-pad: 56px; }
}
/* An inline Lucide icon anywhere else (library sections emit .ico; the
   components above style their own icons). Was the library's core.css. */
.ico {
  width: var(--icon-inline, 20px); height: var(--icon-inline, 20px); flex: none;
  fill: none; stroke: currentColor; stroke-width: var(--icon-stroke, 1.5);
  stroke-linecap: round; stroke-linejoin: round;
}
