/* Staub & Wirbel — main stylesheet.
 * Built to the Figma design (see CONTEXT.md → Visual design).
 * Loaded on the front end AND in the editor canvas (add_editor_style), so a
 * change here moves both — see tools/editor-canvas/ for the parity check.
 *
 * ============================================================================
 * COLOUR TOKENS — two namespaces, one source of truth. theme.json is JSON and
 * cannot hold comments, so the rationale lives here, next to the consumers.
 *
 *   --wp--preset--color--*   BRAND. The 8 real colours of the design, and the
 *                            only things the client can PICK in the editor
 *                            (theme.json settings.color.palette). Every hex in
 *                            the theme lives here and nowhere else.
 *
 *   --wp--custom--color--*   SEMANTIC ROLES — ink, surface, accent, line …
 *                            (theme.json settings.custom.color). These do not
 *                            appear in the colour picker, which is the point:
 *                            they are a code-facing layer.
 *
 * Why roles are `custom` and not extra palette entries: a `custom` value may
 * REFERENCE another token, a palette value may not (palette entries have to be
 * literal colours). So `ink` is `var(--wp--preset--color--petal-frost)` — an
 * alias, not a copy. Before this split the two layers shared one list, which
 * meant 20 palette entries for 12 distinct colours: #361418 was typed out four
 * times as mahagony / surface-dark / ink-inverse / accent-contrast, and the
 * picker showed every colour two to four times over.
 *
 * Adding a colour? If the client should be able to pick it, it goes in the
 * palette. If only CSS consumes it, it goes in custom, ideally as a var()
 * reference to a brand token rather than a fresh literal.
 *
 * Style against the ROLE, not the brand name: `var(--wp--custom--color--ink)`
 * says "body text on a dark surface" and survives a rebrand;
 * `var(--wp--preset--color--petal-frost)` just says "pink".
 *
 * The one deliberate literal left in this file is the zero-alpha Burnt Rose on
 * .site-header — see the comment there for why it cannot be derived.
 * ----------------------------------------------------------------------------
 * TOKEN FALLBACKS — `--wp--*` vars carry NONE, on purpose.
 *
 * Write `var(--wp--preset--spacing--60)`, never
 * `var(--wp--preset--spacing--60, 2.5rem)`. WordPress emits every theme.json
 * preset on every front-end and editor request (verified against
 * wp_get_global_stylesheet(), including --wp--style--root--padding-* and
 * --wp--style--global--content-size, which live in the styles scope rather than
 * the variables scope), so the fallback never fires. All it did was park a
 * second copy of the value next to each of 286 uses, to be kept in step with
 * theme.json by hand — and make a renamed or deleted token fail SILENTLY at the
 * right value instead of visibly. Prefer the loud break.
 *
 * Theme-local `--sw-*` vars are the opposite case and DO keep their fallbacks:
 * --sw-hero-apex, --sw-skew and friends are set per block instance from inline
 * style attributes, so an instance that doesn't set one is normal and the
 * fallback is the real default, not a duplicate.
 * ========================================================================= */

:root {
	box-sizing: border-box;

	/* Height of the fixed site header. ONE source: the header bar sizes itself
	 * from it, and .site-main reserves exactly this much on pages that do not
	 * open with a hero. Figma 493:3975 — 100px. */
	--sw-header-height: 6.25rem;
}

::selection {
  background: var(--wp--preset--color--bone-wheat);
  color: var(--wp--preset--color--dark-mint);
}

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

html {
	scroll-behavior: smooth;
}

body {
	margin: 0;
	font-family: system-ui, sans-serif;
	line-height: 1.5;
}

img {
	max-width: 100%;
	height: auto;
}

/* Narrow measure — legal + long-form prose pages (R01 token). Unified with the
 * body text tier (720): the old 680 near-duplicate was retired to keep a single
 * prose measure. Token: --wp--custom--measure--body (theme.json settings.custom.measure).
 * Apply to a constrained Group/wrapper to hold a comfortable reading line. */
.sw-measure-narrow {
	max-width: var(--wp--custom--measure--body);
	margin-inline: auto;
}

/* Projekte — Dienstleistungen chips.
 * Rendered as inline spans inside a paragraph by the `sw/dienstleistungen`
 * Block Bindings source. Never links (the taxonomy has no public archives).
 * Tokens come from theme.json presets. */
.sw-chips {
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--20);
	margin: 0;
}

.sw-chip {
	display: inline-block;
	padding: 0.15em 0.7em;
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.5;
	color: var(--wp--custom--color--ink-muted);
	background-color: var(--wp--custom--color--surface);
	border: 1px solid var(--wp--custom--color--line);
	border-radius: 999px;
}

/* Projekte index + card: see the ticket R04 section further down. */

/* ==========================================================================
 * Ticket 05 — bespoke content blocks (sw/hero, sw/pitch, sw/projekt-card).
 * All values are theme.json tokens (color/spacing/type presets) so the layout
 * stays on-brand. Loaded on the front end AND in the editor (add_editor_style),
 * so InnerBlocks previews match the front end.
 *
 * TODO: reconcile against the Figma file (CONTEXT.md → Visual design) once the
 * Figma MCP can authenticate. Confirm per block: exact paddings, image ratios,
 * type sizes/weights, and the wide/mobile breakpoints. The structure (blocks,
 * supports, styles, bindings) is final; only these visual constants are
 * provisional and marked "TODO: Figma" below.
 * ========================================================================== */

/* ==========================================================================
 * Ticket R06 — Home v3 (design-audit §4 HOME; Figma 493:3533).
 * The Dreiklang re-skins sw/pitch (scoped to the .sw-dreiklang-card class).
 *
 * The note that used to sit here — "the bespoke sw/hero block was removed …
 * there is nothing a locked template would add" — was true of the composed
 * hero and is FALSE under the new design, which puts the clipped shape on a
 * wrapper that no core-block composition contains and adds sidebar-controlled
 * polygon attributes that core blocks cannot carry. See `sw/hero` below.
 * ========================================================================== */

/* --- Dreiklang (Was? / Für wen? / Wer?) --------------------------------------
 * Three skewed Mahagony cards on the Burnt Rose page background, alternating
 * illustration/text sides. The skewed panel is a rotated ::before layer so the
 * content stays level (Figma: angled panels, horizontal text); alternation is
 * pure CSS (:nth-child), no markup state. */
.sw-dreiklang {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--80);
	max-width: var(--wp--style--global--content-size);
	margin-inline: auto;
	padding-block: var(--wp--preset--spacing--90);

	/* The rotated ::before panel sticks a couple of pixels past the card box on
	 * tall/narrow cards, which produced a horizontal scrollbar at ~390px.
	 * `clip` (not `hidden`) contains it without creating a scroll container. */
	overflow-x: clip;
}

.wp-block-sw-pitch.sw-dreiklang-card {
	position: relative;
	isolation: isolate;
	display: grid;
	gap: var(--wp--preset--spacing--40) var(--wp--preset--spacing--70);
	padding: clamp(3rem, 7vw, 6rem) clamp(1.75rem, 7vw, 6.5rem);
	color: var(--wp--preset--color--cream);
}

.wp-block-sw-pitch.sw-dreiklang-card::before {
	content: "";
	position: absolute;
	inset: clamp(0.75rem, 2.5vw, 2rem);
	z-index: -1;
	background-color: var(--wp--preset--color--mahagony);
	transform: rotate(calc(-1 * var(--sw-skew, 2.5deg)));
}

.sw-dreiklang-card .sw-pitch__title {
	margin: 0;
	color: var(--wp--preset--color--petal-frost);
}

.sw-dreiklang-card .sw-pitch__body {
	color: inherit;
	max-width: 34ch;
}

/* The illustration is always the card's FIRST child (locked template). Target
 * it positionally, not by class: on the front end it is the .sw-illustration
 * div itself, in the editor canvas it sits inside a block wrapper div — the
 * :first-child selector matches both, keeping editor/front-end parity. */
.sw-dreiklang-card > :first-child {
	align-self: center;
	justify-self: center;
}

@media (min-width: 782px) {
	.wp-block-sw-pitch.sw-dreiklang-card {
		grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
	}

	/* Odd cards: illustration left / text right (source order). */
	.sw-dreiklang-card > :first-child {
		grid-column: 1;
		grid-row: 1 / span 3;
	}

	.sw-dreiklang-card > :not(:first-child) {
		grid-column: 2;
	}

	/* Even cards swap sides and flip the panel skew. */
	.sw-dreiklang > .sw-dreiklang-card:nth-child(even)::before {
		transform: rotate(var(--sw-skew, 2.5deg));
	}

	.sw-dreiklang > .sw-dreiklang-card:nth-child(even) > :first-child {
		grid-column: 2;
	}

	.sw-dreiklang > .sw-dreiklang-card:nth-child(even) > :not(:first-child) {
		grid-column: 1;
	}
}

/* --- Testimonial band --------------------------------------------------------
 * Petal Frost Sektion with a big centered Voyage quote; the ” mark is drawn in
 * CSS (no extra block). Figma shows ◀▶ carousel arrows for multiple quotes —
 * deliberately omitted (single quote, dependency-free); revisit if the client
 * supplies several testimonials. */
.sw-testimonial {
	text-align: center;
	padding-block: var(--wp--preset--spacing--60);
}

.sw-testimonial::before {
	content: "\201d"; /* ” */
	display: block;
	font-family: var(--wp--preset--font-family--heading);
	font-weight: 900;
	font-size: clamp(3.5rem, 6vw, 5rem);
	line-height: 0.6;
	margin-bottom: var(--wp--preset--spacing--50);
}

.sw-testimonial__quote {
	margin: 0 auto var(--wp--preset--spacing--60);
	max-width: 40ch;
	font-family: var(--wp--preset--font-family--heading);
	font-weight: 900;
	font-size: clamp(1.625rem, 2.8vw, 2.5rem);
	line-height: 1.3;
}

.sw-testimonial__attribution {
	margin: 0;
	font-size: var(--wp--preset--font-size--small);
}

/* --- sw/pitch base -----------------------------------------------------------
 * The Home Dreiklang uses the v3 .sw-dreiklang-card skin above; this minimal
 * base only serves a fresh (card-class-less) insert. The pre-v3 tile skins
 * (.sw-pitch-grid, eyebrow, surface/ink/accent block styles) were removed in
 * R07 together with the old Angebot tiles that used them. */
.wp-block-sw-pitch {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--30);
	height: 100%;
}

.sw-pitch__title {
	margin: 0;
	font-size: var(--wp--preset--font-size--x-large);
}

.sw-pitch__body {
	margin: 0;
}

/* ==========================================================================
 * Ticket R04 — Projekte index + card, v3 (design-audit §4; Figma PROJEKTE
 * 493:3725). Burnt Rose page background (theme default); serif "Projekte"
 * hero + lead; then the alternating entries: skewed duotone photo on one
 * side, eyebrow (Kategorie) + serif title + Teaser + mint "ZUR CASE-STUDY →"
 * on the other. Alternation is pure CSS (:nth-child on the post-template
 * list items) — no markup state.
 * ========================================================================== */

/* --- Duotone / tint photo treatment (motif #4) -----------------------------
 * CSS-only, token overlays (no image processing): mahagony-30 multiplies the
 * shadows into maroon, petal-frost-30 screens the highlights pink; a slight
 * desaturation on the image keeps original colors from fighting the tint. */
.sw-duotone {
	position: relative;
	overflow: hidden;
}

/* .sw-duotone img {
	filter: saturate(0.55) contrast(1.05);
} */

.sw-duotone::before,
.sw-duotone::after {
	content: "";
	position: absolute;
	inset: 0;
	pointer-events: none;
}

.sw-duotone::before {
	background-color: var(--wp--custom--color--petal-frost-30);
	mix-blend-mode: screen;
}

.sw-duotone::after {
	background-color: var(--wp--custom--color--mahagony-30);
	mix-blend-mode: multiply;
}

/* ==========================================================================
 * sw/hero — the page head (hero rework, step 2).
 *
 * Placed AFTER .sw-duotone on purpose, not with the other hero styles further
 * up: the backdrop carries the `sw-duotone` class and must win the
 * `position: absolute` / `position: relative` conflict against it. Equal
 * specificity, so source order is the whole mechanism. Do not move this block
 * above .sw-duotone.
 *
 * The shape is a LAYER BEHIND the content (.sw-hero__backdrop), never a
 * clip-path on the content box: the title is meant to hang over the angled
 * edge, and clipping the content box would shear live text. Same technique as
 * the .sw-dreiklang-card panel.
 *
 * The three polygon numbers arrive as custom properties from render.php
 * (bounded there and in the editor's sliders), so the client can tune the edge
 * without touching CSS — and we can retune the CSS without invalidating their
 * saved content, because the block is server-rendered.
 * ========================================================================== */
.sw-hero {
	/* Distance from the top of the hero to the PEAK of the edge, and how far the
	 * title sits BELOW that peak. Both are ours, not the author's — the sliders
	 * only move the two corners.
	 *
	 * The title clearing the peak is the design: the overlap is meant to be
	 * minimal, happening only where the two corners hang down past the title,
	 * not across the whole width. An earlier cut pulled the title up OVER the
	 * peak with a negative margin, which buried it in the photo. */
	--sw-hero-band: clamp(24rem, 60vh, 44rem);
	--sw-hero-title-gap: 54px;

	/* The reference length the two drops are measured against. WIDTH-based, and
	 * that is the whole point:
	 *
	 * An angle is rise over run. The run is horizontal — a share of the width
	 * (the peak sits at --sw-hero-apex% across). So the rise has to scale with
	 * WIDTH for the angle to hold. Measured against --sw-hero-band (the old
	 * behaviour) the same slider value produced a far smaller drop on the
	 * image-less hero, whose band is a third of the height, and the edge came
	 * out visibly flatter. Measured against width, both variants draw the
	 * identical angle — the band height no longer enters the calculation.
	 *
	 * vw also gives mobile continuity for free, which absolute px would not:
	 * a fixed px rise over a shrinking run gets steeper as the screen narrows,
	 * so the edge would feel much pointier on a phone. In vw the rise and the
	 * run shrink together and the angle is the same at every width.
	 *
	 * The hero is inherently full-bleed (alignfull), so vw IS its own width.
	 * 28vw is calibrated to the image hero's band at desktop, so the values
	 * already tuned against the old height-relative scale keep their meaning —
	 * this is a change of reference, not a re-scaling of the sliders. */
	--sw-hero-edge-base: 28vw;

	/* The corners hang BELOW the peak by their own drop. Nothing here reads the
	 * band or the other slider, so neither can move the peak — that coupling
	 * (peak derived from max(left, right)) is what made the shape jump once.
	 *
	 * The 0.3 factor is SHAPE_BOUNDS.edgeLeft/edgeRight.max / 100 in
	 * src/hero/shape.js. It reserves room for the deepest drop the sliders can
	 * produce, so the layer's box is a FIXED height no matter where the sliders
	 * sit. Keep the two in step if the bounds ever change. */
	--sw-hero-drop-left: calc(var(--sw-hero-edge-base) * var(--sw-hero-edge-left, 20) / 100);
	--sw-hero-drop-right: calc(var(--sw-hero-edge-base) * var(--sw-hero-edge-right, 30) / 100);
	--sw-hero-drop-max: calc(var(--sw-hero-edge-base) * 0.3);

	position: relative;
	isolation: isolate;

	/* The surface the hero sits on lives HERE, not on the body: the corners hang
	 * past the peak into the body's territory, and an opaque body background
	 * would cut them off. A positioned child with z-index 0 still paints above
	 * this background, so the layer shows in full. */
	background-color: var(--wp--preset--color--burnt-rose);

	/* Reserve the band with PADDING, never with a top margin on the body.
	 * A margin there collapses straight through this element — `overflow: clip`
	 * does NOT establish a block formatting context (that is exactly why it is
	 * safe for the sticky header, and exactly why it does not contain margins) —
	 * so the hero would be pushed down bodily instead of growing, its top edge
	 * would land on the content wrapper, and the backdrop's `top: 0` would
	 * resolve to there, hiding the photo behind the body background. */
	padding-top: var(--sw-hero-band);

	/* The clipped layer can reach a fraction past the box on extreme edge
	 * values. `clip` (not `hidden`) contains it WITHOUT creating a scroll
	 * container — the same distinction the sticky header depends on globally. */
	overflow-x: clip;
}

/* No image: the band collapses to a short dark strip, still with the same
 * peaked edge (Figma PROJEKTE / ABOUT 493:3839 carry no hero photo — the strip
 * is what the transparent site header sits on).
 *
 * There is no overlap at all here, and that is not a detail: with no photo to
 * hang over, there is nothing for the title to overlap, and Figma (PROJEKTE)
 * shows it sitting cleanly BELOW the strip. The much larger gap is what buys
 * that — it also clears the corners hanging below the peak, which on this
 * short band would otherwise reach the H1. */
.sw-hero--plain {
	--sw-hero-band: clamp(7rem, 16vh, 11rem);
	--sw-hero-title-gap: 160px;
}

.sw-hero__backdrop {
	position: absolute;
	inset: 0 0 auto;

	/* Fixed: the peak sits at --sw-hero-band from the top, and the box reserves
	 * the deepest drop the sliders can reach below it. Neither number depends on
	 * the current slider values, so the geometry stays put while the corners
	 * move — which is the whole point. */
	height: calc(var(--sw-hero-band) + var(--sw-hero-drop-max));

	/* Positive, not negative: a z-index of -1 would paint the layer behind the
	 * hero's OWN background too. The body is lifted above it instead. */
	z-index: 0;
	background-color: var(--wp--preset--color--mahagony);

	/* A PEAK, not a notch: the middle point is the HIGHEST point of the bottom
	 * edge and the two outer corners hang below it. (Built the other way up
	 * first — a V with the middle lowest — which is the shape the pre-rework
	 * .sw-*-hero__image clip-paths used. Figma disagrees; this is the design.)
	 *
	 * Percentages throughout, so the polygon never mixes units: --sw-hero-apex
	 * is a share of the WIDTH (where the peak sits), the two edge values are
	 * shares of the HEIGHT (how far each outer corner drops below the peak).
	 *
	 * The peak is the FIXED anchor: --sw-hero-apex only moves it sideways, and
	 * the two drops only pull their own corner down from it. Nothing here reads
	 * the other slider's value, so no control can move the shape as a whole. */
	clip-path: polygon(
		0 0,
		100% 0,
		100% calc(var(--sw-hero-band) + var(--sw-hero-drop-right)),
		var(--sw-hero-apex, 33%) var(--sw-hero-band),
		0 calc(var(--sw-hero-band) + var(--sw-hero-drop-left))
	);
}

.sw-hero__backdrop img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	/* opacity: 0.75; */
	mix-blend-mode: overlay;
}

.sw-hero__body {
	position: relative;
	z-index: 1;

	/* flow-root so the title's top margin stays INSIDE this element and measures
	 * from the peak, instead of collapsing through and displacing the body box.
	 * It establishes that context without clipping overflow, which `hidden`
	 * would — and the title does overflow the box on the image variant. */
	display: flow-root;
	padding-bottom: var(--wp--preset--spacing--70);

	/* Deliberately NO background: the surface is on .sw-hero (see there), so the
	 * corners hanging below the peak stay visible instead of being cut off by
	 * this element's box. */
}

/* Shared content column (see the ".sw-*__inner content column" note near the
 * content-root rules): max-width ABSORBS the gutter so the content-box is exactly
 * the measure and its left edge lines up with bare content and every other
 * __inner, at all widths. */
.sw-hero__inner {
	max-width: calc(var(--wp--style--global--content-size) + 2 * var(--wp--custom--gutter));
	margin-inline: auto;
	padding-inline: var(--wp--custom--gutter);
}

/* The body starts at the peak, so this gap alone places the title relative to
 * it. The title still paints OVER the layer (z-index) where the corners hang
 * below it — that is the minimal overlap the design asks for. */
.sw-hero__title {
	margin: var(--sw-hero-title-gap) 0 var(--wp--preset--spacing--50);
	color: var(--wp--preset--color--petal-frost);
	font-size: var(--wp--preset--font-size--display);
	line-height: 0.95;
}

/* A manual line break in the title is a DESKTOP typographic decision — Home
 * reads "Gemeinsam / Menschen bewegen", because letting it wrap on its own puts
 * two words on the first line and one on the second. At narrow widths the
 * available measure is what should decide, and honouring the author's break
 * there would strand a single word. Dropping the <br> lets the text reflow
 * naturally; the author's break is kept in the content either way. */
@media (max-width: 767px) {
	.sw-hero__title br {
		display: none;
	}
}

.sw-hero__lead {
	margin: 0;
	max-width: 70ch;
}

.sw-hero__extra {
	margin-top: var(--wp--preset--spacing--40);
}

/* Editor-only helper text in the sidebar panel. */
.sw-hero__editor-help {
	margin: 0.75rem 0 0;
	font-size: 12px;
	font-style: normal;
	color: #757575;
}

/* The index runs to the wide 1440 measure — wider than the 1120 content column
 * the rest of the site uses — so the alternating cards have room to breathe.
 * Overrides the ambient content-root cap (`.sw-page > .entry-content > *`) on
 * this one top-level child — an explicit class beats that rule's universal
 * selector on specificity, so source order does not matter. The responsive side
 * gutter on .entry-content still keeps it off the screen edge.
 * The hero heading/lead and the Referenzen band stay at 1120 by design. */
.sw-page > .entry-content > .sw-projekte-index {
	max-width: var(--wp--custom--measure--wide);
}

/* --- Index list (core Query Loop, stacked entries) ------------------------ */
.sw-projekte-list.wp-block-post-template {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--90);
	margin: 0;
	padding: 0;
	list-style: none;
}

/* --- sw/projekt-card (dynamic; rendered inside the Query Loop) ------------ */
.sw-projekt-card {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--wp--preset--spacing--50) var(--wp--preset--spacing--70);
	align-items: center;
}

@media (min-width: 782px) {
	.sw-projekt-card {
		grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
	}

	/* Odd entries: image left / text right (source order). Even entries swap
	 * sides; the skew flips with them. Selector targets the post-template list
	 * items so the editor preview (a lone card) stays on the odd layout. */
	.sw-projekte-list > .wp-block-post:nth-child(even) .sw-projekt-card__media {
		order: 2;
	}

	.sw-projekte-list > .wp-block-post:nth-child(even) .sw-projekt-card__image {
		transform: rotate(var(--sw-skew, 3deg));
	}
}

/* Skewed photo frame (motif #2): the media link is the rotation-safe box, the
 * figure carries the skew + duotone. */
.sw-projekt-card__media {
	display: block;
}

.sw-projekt-card__image {
	margin: 0;
	transform: rotate(calc(-1 * var(--sw-skew, 3deg)));
}

.sw-projekt-card__image img {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: 5 / 4;
	object-fit: cover;
}

/* Editor placeholder image (edit.js preview only). */
.sw-projekt-card__image--placeholder {
	aspect-ratio: 5 / 4;
	background-color: var(--wp--custom--color--mahagony-30);
	border: 1px solid var(--wp--custom--color--line);
}

.sw-projekt-card__body {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--wp--preset--spacing--40);
}

/* Eyebrow — the Kategorie meta (e.g. "Crowdfunding-Kampagne"). */
.sw-projekt-card__eyebrow {
	margin: 0;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 500;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--wp--custom--color--ink-muted);
}

/* Serif title (Voyage), Petal Frost on the maroon page. */
.sw-projekt-card__title {
	margin: 0;
	font-family: var(--wp--preset--font-family--heading);
	font-size: var(--wp--preset--font-size--x-large);
	font-weight: 900;
	line-height: 1.05;
	color: var(--wp--custom--color--ink);
}

.sw-projekt-card__title-link {
	color: inherit;
	text-decoration: none;
}

.sw-projekt-card__title-link:hover,
.sw-projekt-card__title-link:focus {
	text-decoration: underline;
	text-underline-offset: 0.15em;
}

.sw-projekt-card__teaser {
	margin: 0;
	max-width: 48ch;
	color: var(--wp--custom--color--ink);
}

/* ==========================================================================
 * Ticket R03 — global chrome, v3 re-skin (design audit §3; Figma KONTAKT
 * 493:3923 header 493:3975 + footer group). Mahagony header bar with Petal
 * Frost uppercase nav + "DE▾" dropdown switcher; Mahagony footer with the big
 * chevron top edge, large logo, three editable columns and the legal row.
 * All colors/spacing/type via theme.json tokens; clip-path polygons are
 * geometry, not brand values.
 * ========================================================================== */

/* --- Accessibility helpers ---------------------------------------------- */
.screen-reader-text {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

.sw-skip-link:focus {
	position: fixed;
	top: var(--wp--preset--spacing--20);
	left: var(--wp--preset--spacing--20);
	z-index: 1000;
	width: auto;
	height: auto;
	padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--40);
	clip: auto;
	margin: 0;
	color: var(--wp--custom--color--accent-contrast);
	background-color: var(--wp--custom--color--ink);
}

/* --- Site header (Figma 493:3975: 100px tall, 60px gutters, logo 60px, nav
 * gap 40px, Clash Grotesk uppercase 20px Petal Frost) -----------------------
 *
 * FIXED, not sticky. The header overlays the hero — Figma puts the nav on top
 * of the hero photo — and a sticky element stays in flow, so it would push the
 * hero down instead of floating over it. (The hero-rework plan said "sticky",
 * but its own `.site-main` padding rule below only makes sense for an
 * out-of-flow header, so `fixed` is what it actually described.)
 *
 * `body { overflow-x: clip }` further down is load-bearing here and must stay
 * `clip`: unlike `hidden` it does not create a scroll container, so it neither
 * clips nor re-anchors the fixed header. If anyone ever "tidies" it back to
 * `hidden`, the header silently breaks.
 *
 * Transparent at rest so the hero shows through; solid once scrolled, switched
 * by .site-header--stuck from the sentinel observer in assets/js/sw-chrome.js.
 * Without JS it simply stays transparent — the page background behind it is
 * Burnt Rose and the ink is Petal Frost, so the nav stays legible either way. */
.site-header {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;

	/* Above all page content, below the skip link (1000) so keyboard users can
	 * still see it. Nothing else in the theme reaches this range — verified:
	 * the only other stacking contexts are .sw-hero and the Dreiklang card,
	 * both of which are page content and none of which set a z-index above 1. */
	z-index: 900;

	/* Burnt Rose at ZERO ALPHA, not the `transparent` keyword. Both look
	 * identical at rest, but they fade very differently: `transparent` is
	 * rgba(0,0,0,0), so the transition interpolates through semi-transparent
	 * BLACK and the bar visibly dips dark before reaching the brand colour —
	 * over a hero photo that reads as a flicker, not a fade. Matching the hue
	 * and moving only the alpha gives a clean cross-fade.
	 *
	 * The literal must stay in step with --wp--preset--color--burnt-rose
	 * (#69272B); it cannot be derived from the token without relative-color
	 * syntax, which is newer than this theme's baseline. */
	background-color: rgba(105, 39, 43, 0);
	color: var(--wp--custom--color--ink);
	transition: background-color 300ms ease;
}

/* Solid once scrolled — and also whenever the mobile nav is open, since an
 * expanded menu over a transparent bar would sit unreadably on the page
 * content behind it. */
.site-header--stuck,
.site-header:has( .site-nav--open ) {
	background-color: var(--wp--preset--color--burnt-rose);
}

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

/* The observed marker. Its HEIGHT is the trigger distance: the header turns
 * solid once the page has scrolled by its own height, rather than on the first
 * stray pixel of scroll. */
.sw-header-sentinel {
	position: absolute;
	top: 0;
	left: 0;
	width: 1px;
	height: var(--sw-header-height, 6.25rem);
	pointer-events: none;
}

/* The header is out of flow, so every page has to reserve its height —
 * EXCEPT when a hero opens the page, because the hero is designed to run up
 * behind the header. `:has()` reads the real markup: the hero is not a direct
 * child of .site-main (page.php nests .wrap > article > .entry-content), so
 * this matches the first block of the content, not a child of main. */
.site-main {
	padding-top: var(--sw-header-height, 6.25rem);
}

.site-main:has( .entry-content > .wp-block-sw-hero:first-child ) {
	padding-top: 0;
}

.site-header__inner {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--50);
	max-width: var(--wp--custom--measure--wide);
	min-height: var(--sw-header-height, 6.25rem);
	margin: 0 auto;
	padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--50);
}

.site-brand {
	font-family: var(--wp--preset--font-family--heading);
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 900;
	color: var(--wp--custom--color--ink);
	text-decoration: none;
}

.custom-logo-link {
	display: inline-flex;
	align-items: center;
}

.custom-logo {
	display: block;
	width: auto;
	max-height: 3.75rem; /* Figma: 60px in the 100px bar. */
	height: auto;
}

/* --- Primary navigation (uppercase letter-spaced Clash Grotesk) ---------- */
.site-nav {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--wp--preset--spacing--50);
}

/* DIRECT child only. As `.site-nav ul` this also matched the language
 * switcher's dropdown — which is a <ul> inside the nav — and at (0,1,1) it beat
 * the switcher's own (0,1,0) `display: none`, forcing the menu permanently
 * open. The nav's own list is a direct child (wp_nav_menu with
 * container => false), so the child combinator hits exactly what was meant. */
.site-nav > ul {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--wp--preset--spacing--50);
	margin: 0;
	padding: 0;
	list-style: none;
}

.site-nav a {
	color: var(--wp--custom--color--ink);
	font-family: var(--wp--preset--font-family--body);
	/* Figma: 20px at 1440w. */
	font-size: clamp(0.875rem, 0.55rem + 0.9vw, 1.25rem);
	font-weight: 700;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	text-decoration: none;
	transition: color .2s ease-in-out;
}

.site-nav .current-menu-item > a,
.site-nav a:hover,
.site-nav a:focus {
	color: var(--wp--custom--color--accent);
}

/* --- Language switcher — "DE▾" dropdown (ticket R03) ----------------------
 * No-JS: the menu opens via :focus-within (button focus reveals it, focus loss
 * closes it). sw-chrome.js stamps --js on the wrapper, which hands control to
 * the aria-expanded / .is-open state (click, Escape, focusout, outside click).
 *
 * ALIGNMENT — the wrapper IS the component's box. The toggle sizes it
 * (shrink-to-fit around "DE" + chevron) and the panel is pinned to it with
 * `left: 0; right: 0`, so the two fills are the same width by construction
 * rather than by arithmetic. Both use the same padding token for their inset,
 * so every language code sits on one left edge.
 *
 * The typography is declared ONCE here and inherited by the button and the
 * links. It used to be repeated on each, which is what broke the alignment:
 * the panel's <ul> never got the clamp, so an `em` padding on the panel and the
 * same `em` padding on the toggle resolved against different font sizes and the
 * boxes drifted apart. The insets are rem for the same reason — one length, no
 * dependence on where it is read.
 *
 * COLOUR — open, the component inverts to a solid Petal Frost chip with
 * Mahagony text. The earlier Mahagony fill disappeared whenever the header sat
 * over a Mahagony section; the inverse is the one pairing in the palette that
 * stays visible against every brand background and over imagery. */
.sw-language-switcher {
	--sw-language-pad-x: 0.6rem;
	--sw-language-pad-y: 0.35rem;

	position: relative;
	display: inline-block;
	/* Pulls the horizontal padding back out of the flow, so the label still
	 * sits optically flush with the header's right edge and the nav's gap looks
	 * the same as it did before the chip had a box. */
	margin-inline: calc(var(--sw-language-pad-x) * -1);
	color: var(--wp--custom--color--ink);
	font-family: var(--wp--preset--font-family--body);
	/* Figma: 20px at 1440w — matches .site-nav a. */
	font-size: clamp(0.875rem, 0.55rem + 0.9vw, 1.25rem);
	font-weight: 700;
	line-height: 1.2;
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

.sw-language-switcher__toggle {
	display: flex;
	align-items: center;
	/* Figma puts the chevron hard against the label: node 493:3616 is 32px wide
	 * for a 26px "DE" plus the 8px chevron, i.e. the two boxes touch. 0.4em read
	 * as a deliberate gap; 0.1em keeps them optically adjacent while leaving the
	 * uppercase letter-spacing on the E room to breathe. */
	gap: 0.1em;
	padding: var(--sw-language-pad-y) var(--sw-language-pad-x);
	background-color: transparent;
	border: 0;
	cursor: pointer;
	/* Buttons do not inherit the UA font; everything else cascades from the
	 * wrapper. */
	color: inherit;
	font: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
	transition: background-color .2s ease-in-out, color .2s ease-in-out;
}

.sw-language-switcher__toggle:hover,
.sw-language-switcher__toggle:focus-visible {
	color: var(--wp--custom--color--accent);
}

.sw-language-switcher__chevron {
	display: block;
	width: 0.5em;
	height: auto;
	flex: none;
	transition: transform .2s ease-in-out;
}

.sw-language-switcher__menu {
	display: none;
	position: absolute;
	/* Pinned to the wrapper on three sides: flush under the toggle and exactly
	 * as wide as it, with no gap for the page to show through. */
	top: 100%;
	left: 0;
	right: 0;
	z-index: 100;
	margin: 0;
	/* No padding of its own: each link carries the full inset, so the space
	 * below "EN" matches the space above "DE" instead of doubling up. */
	padding: 0;
	list-style: none;
	background-color: var(--wp--custom--color--ink);
}

.sw-language-switcher__item a {
	display: block;
	padding: var(--sw-language-pad-y) var(--sw-language-pad-x);
	color: var(--wp--custom--color--ink-inverse);
	font: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
	text-decoration: none;
	transition: background-color .2s ease-in-out;
}

/* The colour is restated because `.site-nav a:hover` matches these links at the
 * same specificity and would otherwise repaint them Mint on Petal Frost. */
.sw-language-switcher__item a:hover,
.sw-language-switcher__item a:focus {
	background-color: var(--wp--preset--color--petal-frost-shadow);
	color: var(--wp--custom--color--ink-inverse);
}

/* --- Open state ---------------------------------------------------------
 * Two selectors on every rule, and they must stay in step: the :focus-within
 * one is the no-JS path, the .is-open one is what sw-chrome.js drives. */
.sw-language-switcher:not(.sw-language-switcher--js):focus-within .sw-language-switcher__menu,
.sw-language-switcher.is-open .sw-language-switcher__menu {
	display: block;
}

.sw-language-switcher:not(.sw-language-switcher--js):focus-within .sw-language-switcher__toggle,
.sw-language-switcher.is-open .sw-language-switcher__toggle {
	background-color: var(--wp--custom--color--ink);
	color: var(--wp--custom--color--ink-inverse);
}

.sw-language-switcher:not(.sw-language-switcher--js):focus-within .sw-language-switcher__chevron,
.sw-language-switcher.is-open .sw-language-switcher__chevron {
	transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
	.sw-language-switcher__toggle,
	.sw-language-switcher__chevron,
	.sw-language-switcher__item a {
		transition: none;
	}
}

/* --- Mobile nav toggle (progressive enhancement) ------------------------ */
.site-nav__toggle {
	display: none;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	/* No box around the bars: the negative space in the header already reads as
	 * a target, and a hairline rule here competed with the header's own edges.
	 * The 2.75rem square keeps the 44px touch target the border used to imply. */
	padding: 0;
	background: transparent;
	border: 0;
	cursor: pointer;
	color: var(--wp--custom--color--ink);
}

.site-nav__toggle-bars,
.site-nav__toggle-bars::before,
.site-nav__toggle-bars::after {
	display: block;
	width: 1.25rem;
	height: 2px;
	background-color: var(--wp--custom--color--ink);
	transition: transform 0.2s ease, opacity 0.2s ease;
}

.site-nav__toggle-bars {
	position: relative;
}

.site-nav__toggle-bars::before,
.site-nav__toggle-bars::after {
	position: absolute;
	left: 0;
	content: "";
}

.site-nav__toggle-bars::before {
	top: -0.375rem;
}

.site-nav__toggle-bars::after {
	top: 0.375rem;
}

.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-bars {
	background-color: transparent;
}

.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-bars::before {
	transform: translateY(0.375rem) rotate(45deg);
}

.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-bars::after {
	transform: translateY(-0.375rem) rotate(-45deg);
}

/* TODO: Figma — confirm the mobile breakpoint (768px assumed). */
@media (max-width: 767px) {
	/* The header is fixed, so a long open menu on a short screen would have no
	 * way to be reached. Cap it to the viewport and let it scroll internally.
	 * dvh, not vh: mobile browser chrome shrinks the visual viewport. */
	.site-header {
		max-height: 100dvh;
		overflow-y: auto;
	}

	.site-nav__toggle {
		display: inline-flex;
	}

	.site-nav {
		display: none;
		flex-basis: 100%;
		flex-direction: column;
		align-items: flex-start;
		gap: var(--wp--preset--spacing--40);
		padding-top: var(--wp--preset--spacing--40);
	}

	.site-nav.site-nav--open {
		display: flex;
	}

	/* Direct child, for the same reason as the desktop rule above. */
	.site-nav > ul {
		flex-direction: column;
		align-items: flex-start;
		gap: var(--wp--preset--spacing--40);
	}

	/* Inside the stacked mobile nav the dropdown flows in place instead of
	 * floating. The header is a `max-height: 100dvh; overflow-y: auto` scroll
	 * box, and an absolutely positioned panel hanging off the last nav item was
	 * being clipped by it. In flow it just extends the menu's own height, which
	 * the header scrolls normally.
	 *
	 * The wrapper's optical outdent stays: in the stacked menu it is what puts
	 * "DE" on the same left edge as "PROJEKTE" above it. Cancelling it would
	 * align the chip's FILL with the nav links and push its text 0.6rem right of
	 * them — the fill is allowed to overhang, the text is not. */
	.sw-language-switcher__menu {
		position: static;
	}
}

/* --- Site footer (Mahagony band with the big chevron top edge) ------------
 * The clip cuts the footer's own box: everything above the polygon is
 * transparent, so the page background (Burnt Rose) shows through — matching
 * the Figma footer edge (apex ~27% from the left, left endpoint high, right
 * endpoint deep; ~110/275px at 1440w). The footer sizes its OWN edge (R02
 * note): larger than the Sektion default. */
.site-footer {
	--sw-footer-edge: clamp(5rem, 19vw, 17rem);
	margin-top: var(--wp--preset--spacing--70);
	padding-top: calc(var(--sw-footer-edge) + var(--wp--preset--spacing--70));
	background-color: var(--wp--custom--color--surface-dark);
	color: var(--wp--custom--color--ink);
	clip-path: polygon(
		0 calc(var(--sw-footer-edge) * 0.4),
		27% 0,
		100% var(--sw-footer-edge),
		100% 100%,
		0 100%
	);
}

.site-footer__inner {
	max-width: var(--wp--custom--measure--content);
	margin: 0 auto;
	padding: 0 var(--wp--preset--spacing--40) var(--wp--preset--spacing--60);
}

/* Large footer logo — the same Custom Logo as the header, rendered bigger
 * (Figma: ~80px vs 60px in the header). */
.site-footer__logo {
	display: block;
	margin-bottom: var(--wp--preset--spacing--70);
}

.site-footer__logo .custom-logo {
	max-height: 5rem;
}

a.site-footer__logo {
	font-size: var(--wp--preset--font-size--large);
}

/* Three editable columns (synced pattern; core/columns stacks on mobile). */
.sw-footer-cols p {
	margin: 0 0 var(--wp--preset--spacing--20);
	font-size: 1rem;
	line-height: 1.5;
}

.sw-footer-cols__title {
	font-weight: 700;
}

a.sw-footer-cols__link {
	display: block;
	margin-bottom: var(--wp--preset--spacing--20);
	font-size: 1rem;
	line-height: 1.5;
}

.site-footer a {
	color: var(--wp--custom--color--ink);
	text-decoration: none;
}

.site-footer a:hover,
.site-footer a:focus {
	color: var(--wp--custom--color--accent);
	text-decoration: underline;
	text-underline-offset: 0.25em;
}

/* Erstgespräch CTA — styled mailto button, bound to the accent token. Kept for
 * page-level CTA reuse (the footer now links it as a plain column link). */
/* The Erstgespräch mailto CTA in its button form. It carries `wp-element-button`
 * (see sw_erstgespraech_cta() in inc/chrome.php), so the brand button above
 * already skins it — colors, type, padding and the arrow. It used to repeat a
 * skin of its own here (Light Mint panel, Mahagony text), which was a THIRD
 * button style competing with theme.json's; that is the drift this consolidation
 * exists to stop, so the duplicate is gone rather than merely re-tuned.
 *
 * The class is kept as a hook for anything Erstgespräch-specific later. Note the
 * only current caller passes `sw-footer-cols__link` instead, so the button form
 * is currently unused — it is the DEFAULT argument, and the next caller to take
 * it must land on the brand button. */

/* Bottom row: copyright left, AGB · Datenschutz · Impressum right. */
.site-footer__meta {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--40);
	margin-top: var(--wp--preset--spacing--80);
	font-size: var(--wp--preset--font-size--small);
}

.site-footer__copyright {
	margin: 0;
}

.site-footer__legal-list {
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--50);
	margin: 0;
	padding: 0;
	list-style: none;
}

/* ==========================================================================
 * Ticket 07 — non-project pages (Home, Angebot, Über, Kontakt, legal).
 * Pages are composed from the ticket-05 blocks + core blocks; almost all styling
 * comes from theme.json tokens and block supports. Only the few page-specific
 * helpers below need CSS. Loaded on the front end AND in the editor.
 * ========================================================================== */

/* Page content wrapper: front-page.php + page.php render blocks here.
 *
 * Ambient content width (the "invert the default" model). This is a classic
 * theme — there is no block template to emit a constrained layout for the
 * content root — so we hand-write the constrained-layout rules here. Top-level
 * blocks get contentSize (1120, the 2/3 column) for free; full-bleed becomes the
 * opt-in via `align:full` (Sektion, sliders). Existing seeded sections are
 * `alignfull` constrained groups, so they escape to 100vw and re-constrain at
 * 1120 inside themselves exactly as before — this change is transparent to them.
 *
 * The editor canvas has a different root than the front end. In the post editor
 * the root block-list container carries BOTH `.wp-block-post-content` and
 * `.is-root-container`; matching either keeps the rule robust across WP versions
 * without a live check. add_editor_style() namespaces these under
 * `.editor-styles-wrapper`, so they apply in the canvas iframe too. */
.sw-page > .entry-content,
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) {
	margin: 0;
}

/* Side gutter so content never touches the screen edge below the 1120 column
 * (mobile + smaller desktops). FRONT-END ONLY: the editor canvas root carries
 * `.has-global-padding`, so WordPress applies this same gutter there natively
 * from theme.json `styles.spacing.padding` — adding it here too would double it.
 *
 * Both sides read the SAME token. theme.json defines `settings.custom.gutter`
 * and points its own `styles.spacing.padding` at it, so --wp--custom--gutter and
 * --wp--style--root--padding-* resolve to one value that exists in exactly one
 * place. This used to be a theme-local --sw-gutter that had to be kept in step
 * with theme.json by hand, and because the editor canvas never inherited it,
 * every use also carried a literal `max(1.25rem, 5vw)` fallback — eleven copies
 * of one number. The global token is emitted for the front end AND the canvas,
 * so neither the duplicate nor the fallback is needed.
 *
 * `vw` (not %) so the break-out margin below cancels it exactly; % padding and %
 * margin resolve against different bases. Tune it in theme.json. */
.sw-page > .entry-content {
	padding-inline: var(--wp--custom--gutter);
}

/* ONE horizontal system, so every content left-edge lines up at every width:
 *  - bare content: capped + flush-lefted to the centred `contentSize` column
 *    (the `> *` cap here + the tier-cap margin formula below);
 *  - full-bleed sections (.sw-hero__inner, .sw-sektion__inner, .sw-cta-band__inner):
 *    the shared "content column" — `max-width: calc(measure + 2*gutter)` with
 *    `padding-inline: gutter`, so the gutter is ABSORBED and the content-box is
 *    exactly `measure`, landing its edge at the same x as bare content.
 * The gutter (`--wp--custom--gutter`) is the single side-inset knob; it lives in
 * theme.json `settings.custom.gutter` and nowhere else. */
.sw-page > .entry-content > *,
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) > * {
	max-width: var(--wp--style--global--content-size);
	margin-inline: auto;
}

/* Full-bleed break-out: cancel the side gutter so alignfull bands reach the
 * viewport (front end) / canvas (editor) edge. */
.sw-page > .entry-content > .alignfull,
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) > .alignfull {
	max-width: none;
}

.sw-page > .entry-content > .alignfull {
	margin-inline: calc(-1 * var(--wp--custom--gutter));
}

/* The editor's break-out has to be spelled out rather than left to WordPress.
 * Core does emit one — `.has-global-padding > .alignfull { margin-inline: -root-padding }`
 * from `useRootPaddingAwareAlignments` — but it lands at the SAME specificity
 * (0,2,0) as our `> *` centring rule above, and add_editor_style() sheets are
 * injected after the global stylesheet, so `margin-inline: auto` won and every
 * full-bleed band sat inset by the root padding in the canvas while spanning the
 * viewport on the front end. Repeating the negative margin on the alignfull
 * selector (0,3,0) settles it in one direction for good. The values mirror
 * It cancels --wp--style--root--padding-*, which theme.json points at
 * `settings.custom.gutter` — so this stays exact if the gutter changes. */
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) > .alignfull {
	margin-inline: calc(-1 * var(--wp--style--root--padding-left))
		calc(-1 * var(--wp--style--root--padding-right));
}

/* Tier-2 text measure caps. Scoped to WITHIN the content root (any nesting
 * depth) — this is the "global" the design wants: a paragraph reads at 720
 * wherever it lands in the CONTENT (page canvas, Sektion, column), but the caps
 * never leak into site chrome (header, nav, footer). `max-width` only ever
 * shrinks, so a cap inside a narrower container clamps to the container for free.
 * Titles get 960, body prose 720. Zero-specificity :where() so block-specific
 * rules (e.g. .sw-tabelle h2) still win.
 *
 * Lists are matched as `.wp-block-list` (the List block), NOT bare `ul`/`ol`:
 * layout lists — the Query post-template (`.sw-projekte-list` is a <ul>),
 * navigation, social links — are also ul/ol, and capping those to 720 squeezed
 * the 2-column projekt teasers. Only genuine prose lists carry `.wp-block-list`. */
.sw-page > .entry-content :where(h1, h2),
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) :where(h1, h2) {
	max-width: var(--wp--custom--measure--title);
}

.sw-page > .entry-content :where(h3, h4, h5, h6, p, .wp-block-list, blockquote),
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) :where(h3, h4, h5, h6, p, .wp-block-list, blockquote) {
	max-width: var(--wp--custom--measure--body);
}

/* Direct-child text: same cap PLUS flush-left WITHIN the centred 1120 column (the
 * "scope alignment" half of the hybrid). Placed after the ambient `> *` rule and
 * at equal specificity so it wins on source order — otherwise the 1120 ambient cap
 * would beat the tier cap.
 *
 * The margin-inline-start is NOT 0: a 720/960 box pinned to the raw content edge
 * lands at the far-left gutter, way left of the centred 1120 column (and the
 * centred hero). `max(0px, (100% - contentSize)/2)` offsets it to exactly where a
 * centred 1120 column begins — so left-flushed text lines up with full-width
 * blocks and .sw-hero__inner. On viewports below 1120 the max() collapses to 0 and
 * it sits flush to the gutter. Left-pin is NOT forced on nested text, so content
 * inside columns/cards keeps its own alignment. */
.sw-page > .entry-content > :where(h1, h2),
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) > :where(h1, h2) {
	max-width: var(--wp--custom--measure--title);
	margin-inline-start: max(0px, calc((100% - var(--wp--style--global--content-size)) / 2));
	margin-inline-end: auto;
}

.sw-page > .entry-content > :where(h3, h4, h5, h6, p, .wp-block-list, blockquote),
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) > :where(h3, h4, h5, h6, p, .wp-block-list, blockquote) {
	max-width: var(--wp--custom--measure--body);
	margin-inline-start: max(0px, calc((100% - var(--wp--style--global--content-size)) / 2));
	margin-inline-end: auto;
}

/* The flush-left column pin above assumes default (left) alignment. When the
 * author explicitly centres or right-aligns a block, honour that instead: the
 * capped box centres (or flush-rights to the column edge) rather than pinning
 * left — otherwise a centred one-liner's box sits at the column's left edge and
 * its text looks off-centre. Cap and gutter are unchanged. Placed after the pin
 * rules so it wins at equal specificity on source order. */
.sw-page > .entry-content > :where(.has-text-align-center),
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) > :where(.has-text-align-center) {
	margin-inline: auto;
}

.sw-page > .entry-content > :where(.has-text-align-right),
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) > :where(.has-text-align-right) {
	margin-inline-start: auto;
	margin-inline-end: max(0px, calc((100% - var(--wp--style--global--content-size)) / 2));
}

/* Small accent label above section/page titles (mirrors the hero/pitch eyebrow). */
.sw-eyebrow {
	margin: 0 0 var(--wp--preset--spacing--20);
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--wp--custom--color--accent);
}

/* Introductory lead paragraph under a heading — the "lead" tier (960), flush
 * with the heading above it. Now that the content root is a real constrained
 * layout, this is a plain opt-in rule: the old margin hack (which fought the
 * constrained parent's auto-margins with !important) is gone. The .entry-content
 * descendant selector clears the constrained parent's auto-margins without
 * !important; a lead can sit inside an alignfull group, so it opts in by class
 * rather than relying on being a direct child of the content root. */
.entry-content .sw-lead,
.editor-styles-wrapper :is(.wp-block-post-content, .is-root-container) .sw-lead {
	max-width: var(--wp--custom--measure--title);
	/* Flush-left to the centred 1120 column, same as body text (see the tier-cap
	 * block above). Collapses to 0 in a ≤1120 container (e.g. the hero inner, which
	 * centres its own text), so hero leads are unaffected. */
	margin-inline-start: max(0px, calc((100% - var(--wp--style--global--content-size)) / 2));
	margin-inline-end: auto;
	color: var(--wp--custom--color--ink-muted);
}

/* Über — portrait placeholder keeps a sensible ratio before the client adds an
 * image (the image block renders nothing until then). */
.sw-portrait img {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: 4 / 5;
	object-fit: cover;
}

/* -----------------------------------------------------------------------------
 * Kontaktformular v3 (ticket R10) — sw/form, sw/form-step, sw/field.
 * The form sits directly on the maroon page background (Figma KONTAKT frames):
 * serif Petal-Frost labels, underline inputs, filled/unfilled radio + checkbox
 * dots, mint WEITER/arrow controls and the stepper across the top.
 * Token-driven; hardcoded values are fallbacks only.
 * -------------------------------------------------------------------------- */

.sw-form__form {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--60);
	max-width: 45rem;
}

/* Honeypot — off-screen, non-focusable, invisible to real users. */
.sw-form__hp {
	position: absolute !important;
	left: -9999px !important;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

/* --- Stepper -----------------------------------------------------------------
 * Numbered dots + labels with a connecting hairline running behind the row.
 * The items carry the page background so they visually sit ON the line. */
.sw-form__stepper {
	position: relative;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--wp--preset--spacing--50);
	margin: 0;
	padding: 0;
	list-style: none;
}

.sw-form__stepper::before {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	top: 50%;
	height: 1px;
	background-color: var(--wp--preset--color--mahagony);
}

.sw-form__stepper-item {
	position: relative;
	display: flex;
	align-items: center;
	gap: 0.25rem;
	margin: 0;
	background-color: var(--wp--custom--color--base);
	padding-right: 0.75rem;
	color: color-mix(in srgb, var(--wp--preset--color--petal-frost) 60%, transparent);
	font-size: var(--wp--preset--font-size--small);
}

.sw-form__stepper-dot {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: 0 0 auto;
	width: 1.25rem;
	height: 1.25rem;
	border-radius: 50%;
	background-color: color-mix(in srgb, var(--wp--preset--color--mahagony) 60%, transparent);
	font-weight: 700;
	line-height: 1;
}

.sw-form__stepper-item.is-current {
	color: var(--wp--preset--color--petal-frost);
}

.sw-form__stepper-item.is-current .sw-form__stepper-dot {
	background-color: var(--wp--preset--color--mahagony);
}

/* Steps — fieldset reset; two columns for the short step-1 inputs. The
 * display:grid would defeat the hidden attribute view.js toggles, so [hidden]
 * must win explicitly. */
.sw-form__step[hidden] {
	display: none;
}

.sw-form__step {
	margin: 0;
	padding: 0;
	border: 0;
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--wp--preset--spacing--60) var(--wp--preset--spacing--60);
}

@media (min-width: 782px) {
	.sw-form__step {
		grid-template-columns: 1fr 1fr;
	}

	.sw-form__step .sw-field--textarea,
	.sw-form__step .sw-field--radio,
	.sw-form__step .sw-field--checkbox,
	.sw-form__step .sw-field--checkboxes,
	.sw-form__step .sw-field--select {
		grid-column: 1 / -1;
	}
}

/* Fields. */
.sw-field {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--30);
}

/* Hairline separators between the optional step-2 questions (Figma). */
.sw-field--radio + .sw-field--radio {
	border-top: 1px solid var(--wp--custom--color--line);
	padding-top: var(--wp--preset--spacing--60);
}

/* Serif question labels (Voyage, Petal Frost). */
.sw-field__label {
	font-family: var(--wp--preset--font-family--heading);
	font-size: var(--wp--preset--font-size--large);
	font-weight: 900;
	line-height: 1.15;
	color: var(--wp--preset--color--petal-frost);
	padding: 0;
}

.sw-field__required {
	color: currentColor;
}

/* Small sans helper under the label. */
.sw-field__help {
	margin: 0;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--custom--color--ink);
	max-width: 60ch;
}

/* Underline inputs on the maroon band. */
.sw-field__control {
	width: 100%;
	padding: var(--wp--preset--spacing--40) 0.25rem;
	font: inherit;
	color: var(--wp--custom--color--ink);
	background-color: transparent;
	border: 0;
	border-bottom: 1px solid color-mix(in srgb, var(--wp--preset--color--mahagony) 60%, transparent);
	border-radius: 0;
}

.sw-field__control::placeholder {
	color: var(--wp--custom--color--petal-frost-30);
	opacity: 1;
}

.sw-field__control:focus {
	outline: none;
	border-bottom-color: var(--wp--preset--color--petal-frost);
}

.sw-field__control:focus-visible {
	outline: 2px solid var(--wp--custom--color--accent);
	outline-offset: 2px;
}

.sw-field__control[aria-invalid="true"] {
	border-bottom-color: var(--wp--custom--color--accent);
}

textarea.sw-field__control {
	resize: vertical;
	min-height: 6.5rem;
}

/* Grouped controls (radio / checkbox / checkboxes). */
.sw-field__group {
	margin: 0;
	padding: 0;
	border: 0;
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--30);
}

/* Option rows run horizontally and wrap (Figma). */
.sw-field__options {
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--30) var(--wp--preset--spacing--50);
}

.sw-field__option {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	font-weight: 500;
	color: var(--wp--custom--color--ink);
}

/* Custom dots: unfilled outline, filled Petal Frost when checked. */
.sw-field__option input[type="radio"],
.sw-field__option input[type="checkbox"] {
	appearance: none;
	-webkit-appearance: none;
	flex: 0 0 auto;
	width: 1rem;
	height: 1rem;
	margin: 0;
	background-color: transparent;
	border: 2px solid var(--wp--preset--color--petal-frost);
	cursor: pointer;
}

.sw-field__option input[type="radio"] {
	border-radius: 50%;
}

.sw-field__option input[type="checkbox"] {
	border-radius: 4px;
}

.sw-field__option input:checked {
	background-color: var(--wp--preset--color--petal-frost);
}

.sw-field__option input:focus-visible {
	outline: 2px solid var(--wp--custom--color--accent);
	outline-offset: 2px;
}

/* Validation messages. */
.sw-field__error {
	margin: 0;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	color: var(--wp--custom--color--accent);
}

/* Consent block (deliberate DSG addition near submit — not in Figma). JS shows
 * it on the last step only; [hidden] must beat the display:flex. */
.sw-form__consent[hidden] {
	display: none;
}

.sw-form__consent {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--20);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--custom--color--ink);
}

.sw-form__consent .sw-field__option {
	align-items: flex-start;
}

.sw-form__consent input[type="checkbox"] {
	margin-top: 0.2em;
}

.sw-form__consent a {
	color: var(--wp--custom--color--accent);
}

/* Navigation row: direct-mail note left, controls right (Figma). */
.sw-form__nav {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--40);
	border-top: 1px solid var(--wp--custom--color--line);
	padding-top: var(--wp--preset--spacing--50);
}

.sw-form__mail {
	margin: 0;
	color: var(--wp--custom--color--ink);
}

.sw-form__mail-link {
	color: var(--wp--custom--color--accent);
	text-decoration: none;
}

.sw-form__mail-link:hover,
.sw-form__mail-link:focus {
	text-decoration: underline;
}

.sw-form__buttons {
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--50);
}

.sw-form__nav button {
	padding: 0;
	font: inherit;
	cursor: pointer;
	background: transparent;
	border: 0;
	border-radius: 0;
}

/* Mint uppercase WEITER → (next + submit share the treatment). */
.sw-form__next,
.sw-form__submit {
	font-weight: 700;
	font-size: var(--wp--preset--font-size--medium);
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--wp--custom--color--accent);
}

.sw-form__next::after,
.sw-form__submit::after {
	content: "\00a0\2192";
}

.sw-form__back {
	font-size: var(--wp--preset--font-size--small);
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--wp--custom--color--ink-muted);
}

.sw-form__back::before {
	content: "\2190\00a0";
}

.sw-form__nav button:focus-visible {
	outline: 2px solid var(--wp--custom--color--accent);
	outline-offset: 3px;
}

.sw-form__nav button:disabled {
	opacity: 0.6;
	cursor: default;
}

/* Status + success state. */
.sw-form__status {
	margin: 0;
	color: var(--wp--custom--color--ink);
}

.sw-form__status.is-error {
	padding-left: var(--wp--preset--spacing--30);
	border-left: 3px solid var(--wp--custom--color--accent);
}

/* Success (view.js swaps the form for this): serif thank-you + subline + mint
 * ZURÜCK ZUR STARTSEITE → arrow link (Figma 493:4158). */
.sw-form__success {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--50);
}

.sw-form__success:focus {
	outline: none;
}

.sw-form__success-title {
	margin: 0;
	font-size: var(--wp--preset--font-size--x-large);
	color: var(--wp--preset--color--petal-frost);
}

.sw-form__success-sub {
	margin: 0;
	font-size: var(--wp--preset--font-size--medium);
	color: var(--wp--custom--color--ink);
}

.sw-form__success-link {
	font-weight: 700;
	font-size: var(--wp--preset--font-size--medium);
	text-transform: uppercase;
	letter-spacing: 0.04em;
	text-decoration: none;
	color: var(--wp--custom--color--accent);
}

.sw-form__success-link::after {
	content: "\00a0\2192";
}

.sw-form__success-link:hover,
.sw-form__success-link:focus {
	text-decoration: underline;
}

/* Editor hint (only in the block editor preview). */
.sw-form__editor-note {
	margin: var(--wp--preset--spacing--30) 0 0;
	font-size: var(--wp--preset--font-size--small);
	font-style: italic;
	color: var(--wp--custom--color--ink-muted);
}


/* ==========================================================================
 * Ticket R02 — v3 brand primitives (ADR 0003; design-audit §2 + §3).
 * sw/sektion (angled color bands), sw/illustration (brand SVG set),
 * sw/cta-band ("Zusammen Staub aufwirbeln?"), skew block styles.
 * All colors/typography via theme.json tokens; the clip-path polygons are
 * geometry (derived from the Figma band vectors), not brand values.
 * ========================================================================== */

/* Sektion bleeds to the viewport with the 50%–50vw trick; 100vw can include a
 * classic scrollbar gutter, so clip stray horizontal overflow at the root. */
body {
	overflow-x: clip;
}

/* --- sw/sektion -----------------------------------------------------------
 * Full-bleed colour band with editor-set angled edges — the site's signature
 * layout motif (ADR 0003, design-audit §2 motif #1).
 *
 * The shape is painted on LAYERS behind the content (.sw-sektion__layer), never
 * as a clip-path on the band itself. Two reasons: a child or pseudo-element of
 * a clipped element is clipped by its parent, so the shadow layer could not
 * exist at all while the band carried the polygon (which is exactly why the v1
 * block could only draw one shape); and clipping the band would shear live
 * text. Same technique as sw/hero's __backdrop.
 *
 * Geometry arrives as custom properties from src/sektion/render.php (via
 * inc/edge-geometry.php): an apex position per edge — unitless, a share of the
 * width — and a TANGENT per corner. CSS multiplies the tangent by a horizontal
 * run to get the drop, because an angle is rise over run and the run here is
 * horizontal; that is what makes one authored value draw the same angle at
 * every viewport. Percentages of the band's own HEIGHT are what made earlier
 * versions of this motif change shape on resize.
 *
 * Both layers read the SAME property names, so one polygon rule serves both:
 * the main layer inherits the numbers from the band, the shadow layer
 * re-declares them inline with its own.
 * ========================================================================== */

.wp-block-sw-sektion {
	/* The horizontal reference every drop is measured against.
	 *
	 * Clamped rather than a bare 100vw, which settles both ends of the viewport
	 * range in one expression:
	 *
	 *  - Below 480px the run stops shrinking, so a shape keeps its presence on a
	 *    phone rather than flattening to a hairline against a band that has
	 *    grown tall with wrapped text. The edge reads slightly steeper than
	 *    authored there — deliberately: staying faithful to the desktop drawing
	 *    matters more than the angle being literally true at a width the design
	 *    was never drawn at.
	 *  - Above 1440px the run freezes, so an ultrawide monitor cannot grow a
	 *    band into a triangle. Capping the run caps every drop derived from it,
	 *    which is why there is no separate maximum-drop rule anywhere.
	 *
	 * It also reproduces the v1 hard-coded edge — clamp(2.75rem, 9vw, 8rem) — to
	 * within ~2px at the 5.14° angle the old `diagonal` was. That is what lets
	 * legacy bands map onto this model without visibly moving; see
	 * LEGACY_EDGE_SHAPES in src/shared/edge-geometry.js. */
	--sw-sektion-run: clamp(30rem, 100vw, 90rem);

	/* The measure the inner column actually occupies: the narrower of the chosen
	 * tier and what the viewport leaves once the gutters are taken. The
	 * clearance maths needs it to know where the content's left and right edges
	 * sit relative to the cut. Overridden per tier by .sw-sektion--measure-*. */
	--sw-sektion-measure: var(--wp--custom--measure--content);
	--sw-sektion-col: min(
		var(--sw-sektion-measure),
		calc(100vw - 2 * var(--wp--custom--gutter))
	);

	position: relative;
	/* The layers paint at negative z-index so they sit behind the content
	 * without a stacking order having to be maintained between siblings;
	 * `isolation` keeps that negative index from escaping behind the page. */
	isolation: isolate;
	width: 100vw;
	max-width: 100vw;
	margin-left: calc(50% - 50vw);
	padding-block: var(--wp--preset--spacing--90);
}

/* Surfaces. The variant class carries the band colour, the PAIRED shadow colour
 * and the correct ink automatically (light surface → mahagony ink, dark surface
 * → petal-frost ink). There are no colour pickers, including for the shadow:
 * each surface has exactly one right shadow, and theme.json names them as
 * pairs.
 *
 * The shadows are NOT uniformly "the surface, darker". Old Mahagony's is
 * LIGHTER than the surface: the Figma draws it as Old Mahagony at 30% over the
 * Burnt Rose page background, i.e. 0.3 × #361418 + 0.7 × #69272B = #5A2125,
 * which is what --mahagony-shadow holds. It is stored flat (like every other
 * *-shadow token) rather than as `rgb(… / 30%)` so two overlapping shadows
 * cannot double-darken; the trade is that it assumes the Burnt Rose page
 * background, which is the only thing these shapes ever sit on. */
.sw-sektion--burnt-rose {
	--sw-sektion-surface: var(--wp--preset--color--burnt-rose);
	--sw-sektion-shadow: var(--wp--preset--color--burnt-rose-shadow);
	color: var(--wp--custom--color--ink);
}

.sw-sektion--mahagony {
	--sw-sektion-surface: var(--wp--preset--color--mahagony);
	--sw-sektion-shadow: var(--wp--preset--color--mahagony-shadow);
	color: var(--wp--custom--color--ink);
}

.sw-sektion--petal-frost {
	--sw-sektion-surface: var(--wp--preset--color--petal-frost);
	--sw-sektion-shadow: var(--wp--preset--color--petal-frost-shadow);
	color: var(--wp--custom--color--ink-inverse);
}

/* The mint BAND is Dark Mint with Bone Wheat ink — a dark surface. Light Mint
 * (--mint / --accent) is reserved for text buttons and arrow-links, including
 * the ones sitting on this band; the two greens are not interchangeable. */
.sw-sektion--mint {
	--sw-sektion-surface: var(--wp--custom--color--surface-mint);
	--sw-sektion-shadow: var(--wp--preset--color--dark-mint-shadow);
	color: var(--wp--preset--color--bone-wheat);
}

/* On the light pink surface the global mint link color has no contrast — links
 * follow the dark ink there. */
.sw-sektion--petal-frost a:where(:not(.wp-element-button)) {
	color: var(--wp--custom--color--ink-inverse);
}

/* On Dark Mint, links are the Light Mint text-button green. */
.sw-sektion--mint a:where(:not(.wp-element-button)) {
	color: var(--wp--preset--color--mint);
}

/* The painted layers. Both fill the band's box exactly; the shadow is never
 * allowed outside it. Because the shadow is a copy of the same shape that is
 * LESS cut away, it shows precisely in the area the main polygon carves off —
 * which is all the design asks for, and it means a band can be reordered next
 * to any other without a layer poking into its neighbour. */
.sw-sektion__layer {
	position: absolute;
	inset: 0;
	z-index: -1;
	background-color: var(--sw-sektion-surface);
}

.sw-sektion__layer--shadow {
	z-index: -2;
	background-color: var(--sw-sektion-shadow);
}

/* Drops: how far each corner falls away from its edge's apex.
 *
 * Plus the offsets: how far the whole edge sits inward from the box, which is
 * what lets the shadow's tip and the main shape's tip sit at DIFFERENT heights
 * (with three numbers per edge both apexes are pinned to the box and the tips
 * always coincide). One unit is 1% of the run, so an inset scales exactly like a
 * drop does. The `, 0` fallbacks keep the polygon valid if markup rendered
 * before this shipped is served from a page cache against the new stylesheet.
 *
 * Declared on the band (whose --sw-edge-* are the MAIN shape, inherited by the
 * main layer) and again on the shadow layer, which re-declares --sw-edge-* with
 * its own numbers and so recomputes these from them. */
.sw-sektion--shaped,
.sw-sektion--shaped .sw-sektion__layer--shadow {
	--sw-edge-top-offset: calc(
		var(--sw-edge-top-inset, 0) * var(--sw-sektion-run) / 100
	);
	--sw-edge-bottom-offset: calc(
		var(--sw-edge-bottom-inset, 0) * var(--sw-sektion-run) / 100
	);
	--sw-edge-top-left-drop: calc(
		var(--sw-edge-top-left-tan) * var(--sw-sektion-run) *
			var(--sw-edge-top-apex) / 100
	);
	--sw-edge-top-right-drop: calc(
		var(--sw-edge-top-right-tan) * var(--sw-sektion-run) *
			(100 - var(--sw-edge-top-apex)) / 100
	);
	--sw-edge-bottom-left-drop: calc(
		var(--sw-edge-bottom-left-tan) * var(--sw-sektion-run) *
			var(--sw-edge-bottom-apex) / 100
	);
	--sw-edge-bottom-right-drop: calc(
		var(--sw-edge-bottom-right-tan) * var(--sw-sektion-run) *
			(100 - var(--sw-edge-bottom-apex)) / 100
	);
}

/* The one polygon. Six points: the top edge's two corners and its apex, then
 * the bottom edge's, going clockwise. The offset shifts an edge's whole profile
 * inward without touching its angles — it is added to all three of that edge's
 * points. With every angle AND offset at zero this degenerates to the band's own
 * rectangle, which is why an unshaped band simply does not get the rule rather
 * than getting a no-op version of it. */
.sw-sektion--shaped .sw-sektion__layer {
	clip-path: polygon(
		0 calc(var(--sw-edge-top-offset) + var(--sw-edge-top-left-drop)),
		calc(var(--sw-edge-top-apex) * 1%) var(--sw-edge-top-offset),
		100% calc(var(--sw-edge-top-offset) + var(--sw-edge-top-right-drop)),
		100%
			calc(
				100% - var(--sw-edge-bottom-offset) -
					var(--sw-edge-bottom-right-drop)
			),
		calc(var(--sw-edge-bottom-apex) * 1%)
			calc(100% - var(--sw-edge-bottom-offset)),
		0
			calc(
				100% - var(--sw-edge-bottom-offset) -
					var(--sw-edge-bottom-left-drop)
			)
	);
}

/* Clearance: how much padding the cut costs on each side.
 *
 * NOT the deepest point of the cut — that is at the band's corners, and padding
 * by it would push a heading far below where the design puts it (the Wirbel
 * band's title clears its column, not the far corner, by some 125px). What
 * content actually has to clear is the cut's depth at the CONTENT COLUMN's own
 * left and right edges, so that is what is measured here.
 *
 * `reach` is the horizontal distance from the apex out to a column edge, floored
 * at 0 for the side the apex has already passed. Depth is then that reach times
 * the side's tangent — the same rise-over-run as the drops — capped at the drop
 * itself for the case where the column edge lies outside the band. Whichever
 * side reaches deeper wins.
 *
 * With every angle at zero every term collapses to 0, so an unshaped band's
 * padding is exactly var(--wp--preset--spacing--90): identical to what this
 * block rendered before the rework, to the byte. Angled bands grow their own
 * clearance; there is no fixed reserve anybody pays for. */
.sw-sektion--shaped {
	--sw-sektion-top-reach-left: max(
		0px,
		calc(
			var(--sw-sektion-run) * var(--sw-edge-top-apex) / 100 -
				(var(--sw-sektion-run) - var(--sw-sektion-col)) / 2
		)
	);
	--sw-sektion-top-reach-right: max(
		0px,
		calc(
			(var(--sw-sektion-run) + var(--sw-sektion-col)) / 2 -
				var(--sw-sektion-run) * var(--sw-edge-top-apex) / 100
		)
	);
	--sw-sektion-bottom-reach-left: max(
		0px,
		calc(
			var(--sw-sektion-run) * var(--sw-edge-bottom-apex) / 100 -
				(var(--sw-sektion-run) - var(--sw-sektion-col)) / 2
		)
	);
	--sw-sektion-bottom-reach-right: max(
		0px,
		calc(
			(var(--sw-sektion-run) + var(--sw-sektion-col)) / 2 -
				var(--sw-sektion-run) * var(--sw-edge-bottom-apex) / 100
		)
	);

	--sw-sektion-clear-top: max(
		min(
			var(--sw-edge-top-left-drop),
			calc(var(--sw-edge-top-left-tan) * var(--sw-sektion-top-reach-left))
		),
		min(
			var(--sw-edge-top-right-drop),
			calc(
				var(--sw-edge-top-right-tan) * var(--sw-sektion-top-reach-right)
			)
		)
	);
	--sw-sektion-clear-bottom: max(
		min(
			var(--sw-edge-bottom-left-drop),
			calc(
				var(--sw-edge-bottom-left-tan) *
					var(--sw-sektion-bottom-reach-left)
			)
		),
		min(
			var(--sw-edge-bottom-right-drop),
			calc(
				var(--sw-edge-bottom-right-tan) *
					var(--sw-sektion-bottom-reach-right)
			)
		)
	);

	/* The offset is added on top of the clearance, not folded into it: it moves
	 * the entire edge down (or up), so every point of the cut — including the
	 * depth measured at the content column — sits that much further into the box.
	 * Leave it out and content creeps into the wedge on any band with an inset. */
	padding-top: calc(
		var(--wp--preset--spacing--90) + var(--sw-edge-top-offset) +
			var(--sw-sektion-clear-top)
	);
	padding-bottom: calc(
		var(--wp--preset--spacing--90) + var(--sw-edge-bottom-offset) +
			var(--sw-sektion-clear-bottom)
	);
}

/* Inner content width — the R01 measure tokens. `--sw-sektion-measure` selects
 * the tier ON THE BAND (the clearance maths above needs it too, which is why it
 * no longer lives on this element); max-width absorbs the gutter so the
 * content-box is exactly the measure and its left edge aligns with bare content
 * and the hero. --wp--custom--gutter is a global token, so the editor canvas
 * resolves it too.
 *
 * `position: relative` puts the column into the same stacking context as the
 * negative-index layers, above both. */
.sw-sektion__inner {
	position: relative;
	max-width: calc(var(--sw-sektion-measure) + 2 * var(--wp--custom--gutter));
	margin-inline: auto;
	padding-inline: var(--wp--custom--gutter);
}

.sw-sektion--measure-content {
	--sw-sektion-measure: var(--wp--custom--measure--content);
}

.sw-sektion--measure-wide {
	--sw-sektion-measure: var(--wp--custom--measure--wide);
}

.sw-sektion--measure-narrow {
	--sw-sektion-measure: var(--wp--custom--measure--body);
}

/* --- sw/card ---------------------------------------------------------------
 * The shaped Dreiklang card. Replaces the class-driven `.sw-dreiklang-card`
 * skin above, which is kept only so content still holding `sw/pitch` renders.
 *
 * The shape is painted on LAYERS behind the content (.sw-card__layer), never as
 * a clip-path on the card itself — a child or pseudo-element of a clipped
 * element is clipped by its parent, so the shadow could not exist at all, and
 * clipping the card would shear live text. Same technique as sw/sektion.
 *
 * Geometry arrives as custom properties from src/card/render.php (via
 * inc/corner-geometry.php): per corner, an inward X as a share of the WIDTH and
 * an inward Y in units of 1% of the RUN. The properties are absolute
 * (tl/tr/br/bl) and already mirrored for the card's orientation, so this
 * stylesheet never has to know which way the card faces.
 *
 * Y is measured against a horizontal run rather than against the card's own
 * height on purpose: a card grows taller as its text wraps, and a
 * height-relative offset would redraw every angle in the shape when it did.
 * Same reasoning as sw/sektion's tangents and sw/hero's shape.
 *
 * This is a general QUADRILATERAL — all four sides may slant — which is why it
 * does not share sw/sektion's edge model. See
 * docs/adr/0007-corner-geometry-for-cards.md.
 *
 * KNOWN LIMITATION — this block is LTR-only. `orientation` is a physical
 * left/right choice and the polygon's corners are physical, but the grid places
 * the illustration with line-based `grid-column`, which mirrors in RTL. A
 * right-to-left language would therefore flip the columns while the painted
 * shape stayed put. The site is German with WPML across Latin-script languages,
 * so nothing here is wrong today; adding an RTL locale means mirroring the
 * polygon too, not just translating strings. */
.wp-block-sw-card {
	/* The horizontal reference every Y offset is measured against: the content
	 * measure the card occupies, floored so the shape keeps its presence on a
	 * phone instead of flattening to a hairline against a card that has grown
	 * tall with wrapped text. One unit is therefore ~11px at the 1120px measure. */
	--sw-card-run: clamp(
		20rem,
		calc(100vw - 2 * var(--wp--custom--gutter)),
		var(--wp--custom--measure--content)
	);

	/* The card's vertical rhythm: the ONE number that sets the distance between
	 * the heading, the body and the CTA. Children have their block margins
	 * zeroed so nothing adds to it, and the flexible third row (see the
	 * two-column rules) keeps the illustration's height from inflating it — so
	 * this value is the distance you actually get, at any icon size. */
	--sw-card-row-gap: var(--wp--preset--spacing--40);

	/* Base inner padding, before any clearance the shape adds. The OUTER side —
	 * the illustration's — is deliberately tight: the Figma puts the
	 * illustration at the content column's own edge (x 236 against a column
	 * starting at 240), outside the quad, which is what the handoff described as
	 * the illustration "breaking out of the card". It needs no gutter of its
	 * own. */
	--sw-card-pad-inline: clamp(1.75rem, 5vw, 4.5rem);
	--sw-card-pad-outer: clamp(1rem, 2vw, 1.5rem);
	--sw-card-pad-block: clamp(2rem, 4vw, 3.5rem);

	/* How far the shadow layer's box is inflated past the card's, in run units.
	 * Fixed rather than authorable: the card's shadow is allowed OUTSIDE its box
	 * (unlike sw/sektion's, which is confined so bands can be reordered without
	 * a layer poking into a neighbour) because a contained box on the page
	 * background has no neighbour to collide with. 9 units covers the ~86px of
	 * overhang the Figma draws with room to spare, and the corner offsets then
	 * decide which sides actually show: an offset equal to this puts a shadow
	 * corner exactly on the card's corner, a smaller one bleeds outside, a
	 * larger one pulls in. */
	--sw-card-shadow-bleed: 9;
	--sw-card-bleed: calc(
		var(--sw-card-shadow-bleed) * var(--sw-card-run) / 100
	);

	/* Clearance defaults, so an unshaped card resolves to plain padding. */
	--sw-card-clear-start: 0px;
	--sw-card-clear-end: 0px;
	--sw-card-clear-top: 0px;
	--sw-card-clear-bottom: 0px;

	position: relative;
	/* The layers paint at negative z-index so they sit behind the content
	 * without a stacking order having to be maintained between siblings;
	 * `isolation` keeps that negative index from escaping behind the page. */
	isolation: isolate;
}

/* Surfaces. The variant class carries the card colour, the PAIRED shadow colour
 * and the correct ink automatically. No colour pickers, including for the
 * shadow: each surface has exactly one right shadow and theme.json names them
 * as pairs. Mirrors .sw-sektion--* exactly. */
.sw-card--mahagony {
	--sw-card-surface: var(--wp--preset--color--mahagony);
	--sw-card-shadow: var(--wp--preset--color--mahagony-shadow);
	color: var(--wp--custom--color--ink);
}

.sw-card--burnt-rose {
	--sw-card-surface: var(--wp--preset--color--burnt-rose);
	--sw-card-shadow: var(--wp--preset--color--burnt-rose-shadow);
	color: var(--wp--custom--color--ink);
}

.sw-card--petal-frost {
	--sw-card-surface: var(--wp--preset--color--petal-frost);
	--sw-card-shadow: var(--wp--preset--color--petal-frost-shadow);
	color: var(--wp--custom--color--ink-inverse);
}

/* The mint CARD is Dark Mint with Bone Wheat ink — a dark surface. Light Mint
 * stays reserved for text buttons and arrow-links, including the ones on this
 * card; the two greens are not interchangeable. */
.sw-card--mint {
	--sw-card-surface: var(--wp--custom--color--surface-mint);
	--sw-card-shadow: var(--wp--preset--color--dark-mint-shadow);
	color: var(--wp--preset--color--bone-wheat);
}

/* On the light pink surface the global mint link colour has no contrast. */
.sw-card--petal-frost a:where(:not(.wp-element-button)) {
	color: var(--wp--custom--color--ink-inverse);
}

.sw-card--mint a:where(:not(.wp-element-button)) {
	color: var(--wp--preset--color--mint);
}

/* The painted layers. The main one fills the card's box; the shadow's box is
 * inflated so it can bleed outside. */
.sw-card__layer {
	position: absolute;
	inset: 0;
	z-index: -1;
	background-color: var(--sw-card-surface);
}

.sw-card__layer--shadow {
	z-index: -2;
	inset: calc(-1 * var(--sw-card-bleed));
	background-color: var(--sw-card-shadow);
}

/* Every offset — X as well as Y — is resolved against the RUN, never against a
 * percentage of the layer's own box.
 *
 * That is not a stylistic choice. The shadow's box is inflated by the bleed, so
 * a percentage would mean a different distance on the two layers and the same
 * authored number would draw a different corner on each. Against the run they
 * mean the same thing everywhere, and the bleed can be expressed in the same
 * units: an offset equal to --sw-card-shadow-bleed puts a shadow corner exactly
 * on the card's own corner, a smaller one bleeds outside, a larger one pulls in.
 * The card's width IS the run wherever it is a full-width child of the content
 * column, which is how the block is used.
 *
 * The `, 0` fallbacks keep the polygon valid if markup rendered before this
 * shipped is served from a page cache against the new stylesheet.
 *
 * Declared on the card (whose --sw-corner-* are the MAIN shape, inherited by
 * the main layer) and again on the shadow layer, which re-declares --sw-corner-*
 * with its own numbers and so recomputes these from them. */
.sw-card--shaped,
.sw-card--shaped .sw-card__layer--shadow {
	--sw-card-tl-x: calc(var(--sw-corner-tl-x, 0) * var(--sw-card-run) / 100);
	--sw-card-tl-y: calc(var(--sw-corner-tl-y, 0) * var(--sw-card-run) / 100);
	--sw-card-tr-x: calc(var(--sw-corner-tr-x, 0) * var(--sw-card-run) / 100);
	--sw-card-tr-y: calc(var(--sw-corner-tr-y, 0) * var(--sw-card-run) / 100);
	--sw-card-br-x: calc(var(--sw-corner-br-x, 0) * var(--sw-card-run) / 100);
	--sw-card-br-y: calc(var(--sw-corner-br-y, 0) * var(--sw-card-run) / 100);
	--sw-card-bl-x: calc(var(--sw-corner-bl-x, 0) * var(--sw-card-run) / 100);
	--sw-card-bl-y: calc(var(--sw-corner-bl-y, 0) * var(--sw-card-run) / 100);
}

/* The one polygon. Four points, clockwise from the top-left, each measured
 * inward from the corner of its own layer's box. With every offset at zero this
 * degenerates to the card's own rectangle, which is why an unshaped card simply
 * does not get the rule rather than getting a no-op version of it. */
.sw-card--shaped .sw-card__layer {
	clip-path: polygon(
		var(--sw-card-tl-x) var(--sw-card-tl-y),
		calc(100% - var(--sw-card-tr-x)) var(--sw-card-tr-y),
		calc(100% - var(--sw-card-br-x)) calc(100% - var(--sw-card-br-y)),
		var(--sw-card-bl-x) calc(100% - var(--sw-card-bl-y))
	);
}

/* Clearance: how much padding the cut costs on each side.
 *
 * NOT the deepest point of the cut. That is at the card's outer corners, and
 * padding by it would push the heading far below where the design puts it and
 * blow the card up to nearly twice its drawn height — the same mistake
 * .sw-sektion--shaped's comment warns about, made and measured here first.
 * What content has to clear is the cut's depth WHERE THE CONTENT IS.
 *
 * The default below is the safe, blunt version — the larger of a side's two
 * corner offsets — which is exact for a card whose text spans the full width.
 * The quadrilateral is convex, so a straight side's furthest intrusion is at one
 * of its two endpoints; there is no interior maximum to hunt for. This is what
 * an ICON-FREE card gets.
 *
 * It bounds where the CUT reaches, which is all clearance can do. It does not
 * bound where the TEXT reaches: a word wider than its column overflows whatever
 * padding is reserved. That is handled separately, on .sw-card__inner. */
.sw-card--shaped {
	--sw-card-clear-start: calc(
		max(var(--sw-corner-tl-x, 0), var(--sw-corner-bl-x, 0)) *
			var(--sw-card-run) / 100
	);
	--sw-card-clear-end: calc(
		max(var(--sw-corner-tr-x, 0), var(--sw-corner-br-x, 0)) *
			var(--sw-card-run) / 100
	);
	--sw-card-clear-top: calc(
		max(var(--sw-corner-tl-y, 0), var(--sw-corner-tr-y, 0)) *
			var(--sw-card-run) / 100
	);
	--sw-card-clear-bottom: calc(
		max(var(--sw-corner-bl-y, 0), var(--sw-corner-br-y, 0)) *
			var(--sw-card-run) / 100
	);
}

/* The content column. `position: relative` puts it into the same stacking
 * context as the negative-index layers, above both.
 *
 * `overflow-wrap` is load-bearing and the derived padding does NOT cover it: a
 * single word wider than its column overflows no matter how much clearance the
 * shape reserved. German makes that ordinary rather than exotic — an authored
 * "Kommunikationsberatungsdienstleistungsvertrag" escaped the card entirely and
 * landed on the page background at the sliders' maximum. Same fix, same reason,
 * as .sw-tabelle__label. See also the min-width on the children below. */
.sw-card__inner {
	position: relative;
	overflow-wrap: anywhere;
	display: grid;

	/* ONE control for the vertical rhythm — see the margin reset below.
	 * Grid gap and block margins both applied here before, so the visible
	 * distance was 16px of gap plus 24px of theme.json blockGap and could not be
	 * changed from a single place. */
	gap: var(--sw-card-row-gap) var(--wp--preset--spacing--70);
	padding-block: calc(var(--sw-card-pad-block) + var(--sw-card-clear-top))
		calc(var(--sw-card-pad-block) + var(--sw-card-clear-bottom));
	padding-inline: calc(
			var(--sw-card-pad-inline) + var(--sw-card-clear-start)
		)
		calc(var(--sw-card-pad-inline) + var(--sw-card-clear-end));
}

/* `min-width: 0`: a grid item's automatic minimum size is its min-content
 * width, so one long word widens the TRACK past the `minmax(0, …)` the columns
 * already ask for. Pairs with the overflow-wrap on .sw-card__inner.
 *
 * `margin-block: 0` is what makes --sw-card-row-gap the single source of the
 * card's vertical rhythm. Core gives the paragraph a blockGap margin
 * (theme.json → spacing 50), which in a grid container ADDS to the row gap
 * instead of collapsing into it, so the real distance was gap + margin and
 * neither number alone described it. */
.sw-card__inner > * {
	min-width: 0;
	margin-block: 0;
}

.sw-card__title {
	margin: 0;
}

/* On the dark surfaces the heading is Petal Frost, not the body ink. */
.sw-card--mahagony .sw-card__title,
.sw-card--burnt-rose .sw-card__title {
	color: var(--wp--preset--color--petal-frost);
}

.sw-card__body {
	max-width: 34ch;
	color: inherit;
}

/* The illustration is the card's FIRST child whenever there is one (locked
 * template). Target it positionally, not by class: on the front end it is the
 * .sw-illustration div itself, in the editor canvas it sits inside a block
 * wrapper div — :first-child matches both, keeping editor/front-end parity.
 * `--has-icon` is what makes the position meaningful; without an illustration
 * the first child is the heading. */
.sw-card--has-icon .sw-card__inner > :first-child {
	align-self: center;
	justify-self: center;
}

/* NOTE — do not try to cap the illustration's HEIGHT here.
 *
 * sw/illustration's sizes are widths, and the brand set holds both landscape and
 * portrait artwork, so at one size the raised fist renders far taller than the
 * megaphone. The fix is the SIZE the pattern asks for (the Figma draws the fist
 * at 231px against the megaphone's 398px), not a rule here: the inlined SVGs
 * carry a viewBox and no width/height attributes, so `width: auto` computes to
 * ZERO and the artwork disappears outright. Verified on the front end. */

@media (min-width: 782px) {
	/* The 0.9/1.1 split puts the text column's edge at 45% of the card.
	 *
	 * CHANGING THESE TRACKS MEANS CHECKING THE 0.65 BLEND BELOW: it is the
	 * conservative bound on where the cut is deepest across the text, and it
	 * holds only while the split stays at or under 65%. Push the illustration
	 * column past that and the heading can meet the slant. */
	.sw-card--has-icon .sw-card__inner {
		grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);

		/* Two FLEXIBLE spacer tracks, one above the text and one below, with the
		 * three text rows between them. This is what keeps the text's rhythm
		 * independent of the illustration's height AND keeps it centred.
		 *
		 * The illustration spans every row. When it is taller than the text —
		 * which it is on two of the three Dreiklang cards — grid has to put the
		 * difference somewhere. With three `auto` rows it shared it out between
		 * them: every row grew, each text box stretched, and the slack appeared
		 * as space under the heading and under the body. Measured: the "Was?"
		 * heading's box stretched from 48px to 80px, and the text column's height
		 * came out EXACTLY equal to the icon's (318px, and 363px on "Wer?") —
		 * the giveaway that the icon was setting it.
		 *
		 * Grid gives space to flexible tracks in a span before inflexible ones,
		 * so the two 1fr tracks take the whole difference and split it evenly:
		 * the text rows stay at their content height and the block stays
		 * vertically centred against the illustration. One flexible track at the
		 * end would also fix the rhythm, but it dumps all the slack below the CTA
		 * and the text rides up to the top — which is what this replaces. */
		grid-template-rows: 1fr auto auto auto 1fr;

		/* `row-gap` would apply to the spacer tracks too, adding two phantom gaps
		 * that would have to be subtracted back out of the padding. The gap is
		 * reinstated as a margin on the items that need it, just below — still
		 * driven by --sw-card-row-gap, so the rhythm stays one number. */
		row-gap: 0;
	}

	/* Children are: 1 illustration, 2 heading, 3 body, 4 CTA. Placed explicitly
	 * because auto-placement would otherwise drop the heading into the leading
	 * spacer track. */
	.sw-card--has-icon .sw-card__inner > :nth-child(2) {
		grid-row: 2;
	}

	.sw-card--has-icon .sw-card__inner > :nth-child(3) {
		grid-row: 3;
	}

	.sw-card--has-icon .sw-card__inner > :nth-child(4) {
		grid-row: 4;
	}

	/* The body and the CTA carry the gap; the heading leads and needs none. */
	.sw-card--has-icon .sw-card__inner > :nth-child(n + 3) {
		margin-top: var(--sw-card-row-gap);
	}

	/* With an illustration BESIDE the text, the clearance changes, and both
	 * halves of the change are what the Figma actually draws:
	 *
	 *  - The OUTER side takes no horizontal clearance. The illustration is meant
	 *    to sit at the card's own edge, outside the quad — that is the design,
	 *    not an accident, and reserving the cut's width there would pull it into
	 *    the middle of the card. It also gets the tighter base padding.
	 *  - The vertical clearance is measured where the TEXT is, at the inner
	 *    column. The top edge is a straight line between two corners, so over the
	 *    text's span its worst value is at the column boundary: a blend weighted
	 *    0.65 toward the outer corner, conservative for any split up to 65% (the
	 *    grid's is 45%). `max()` against the inner corner keeps it correct when
	 *    the inner corner is the deeper of the two.
	 *
	 * The weights are literals because CSS calc() cannot divide by a variable, so
	 * interpolating exactly at the column boundary is not expressible; a fixed
	 * conservative blend is.
	 *
	 * These live INSIDE the breakpoint deliberately. Below it the card is a
	 * single column and the text spans the full width, so it needs the same full
	 * clearance an icon-free card gets — the first version of this rule sat
	 * outside the query and let the heading hang off the shape's left edge on a
	 * phone. */
	.sw-card--shaped.sw-card--has-icon.sw-card--left {
		--sw-card-clear-start: 0px;
		--sw-card-clear-top: calc(
			max(
					var(--sw-corner-tr-y, 0),
					var(--sw-corner-tl-y, 0) * 0.65 + var(--sw-corner-tr-y, 0) *
						0.35
				) * var(--sw-card-run) / 100
		);
		--sw-card-clear-bottom: calc(
			max(
					var(--sw-corner-br-y, 0),
					var(--sw-corner-bl-y, 0) * 0.65 + var(--sw-corner-br-y, 0) *
						0.35
				) * var(--sw-card-run) / 100
		);
	}

	.sw-card--shaped.sw-card--has-icon.sw-card--right {
		--sw-card-clear-end: 0px;
		--sw-card-clear-top: calc(
			max(
					var(--sw-corner-tl-y, 0),
					var(--sw-corner-tr-y, 0) * 0.65 + var(--sw-corner-tl-y, 0) *
						0.35
				) * var(--sw-card-run) / 100
		);
		--sw-card-clear-bottom: calc(
			max(
					var(--sw-corner-bl-y, 0),
					var(--sw-corner-br-y, 0) * 0.65 + var(--sw-corner-bl-y, 0) *
						0.35
				) * var(--sw-card-run) / 100
		);
	}

	.sw-card--has-icon.sw-card--left .sw-card__inner {
		padding-left: calc(
			var(--sw-card-pad-outer) + var(--sw-card-clear-start)
		);
	}

	.sw-card--has-icon.sw-card--right .sw-card__inner {
		padding-right: calc(
			var(--sw-card-pad-outer) + var(--sw-card-clear-end)
		);
	}

	.sw-card--has-icon.sw-card--left .sw-card__inner > :first-child {
		grid-column: 1;
		grid-row: 1 / -1;
	}

	.sw-card--has-icon.sw-card--left .sw-card__inner > :not(:first-child) {
		grid-column: 2;
	}

	.sw-card--has-icon.sw-card--right .sw-card__inner > :first-child {
		grid-column: 2;
		grid-row: 1 / -1;
	}

	.sw-card--has-icon.sw-card--right .sw-card__inner > :not(:first-child) {
		grid-column: 1;
	}
}

/* Mobile: the card stacks with the illustration above the text, and the
 * illustration deliberately OVERHANGS the shape's top edge so roughly 40% of it
 * sits above the geometry. The shape keeps its angles — the run's floor above is
 * what stops it flattening at this width.
 *
 * The overhang is expressed as a fraction of an explicit icon size rather than
 * of the rendered height, because a fraction of an intrinsic height is not
 * something CSS can compute. The layers start that far down; the illustration
 * starts at the card's own top and therefore straddles the edge. */
@media (max-width: 781px) {
	.sw-card--has-icon {
		/* A HEIGHT — see the svg rule below for why that matters. */
		--sw-card-icon-size: min(11rem, 40vw);
		--sw-card-icon-overhang: calc(var(--sw-card-icon-size) * 0.4);
	}

	.sw-card--has-icon .sw-card__layer {
		top: var(--sw-card-icon-overhang);
	}

	/* The shadow's box is inflated, so its own top has to be offset from that
	 * inflated edge rather than from the card's. */
	.sw-card--has-icon .sw-card__layer--shadow {
		top: calc(var(--sw-card-icon-overhang) - var(--sw-card-bleed));
	}

	/* The illustration is meant to sit above the shape, so it does NOT take the
	 * top clearance — it starts at the card's own top edge. */
	.sw-card--has-icon .sw-card__inner {
		padding-top: 0;
	}

	/* …but the text after it still must clear the cut. The icon has already
	 * covered `icon-size` of the distance to the shape's deepest top point
	 * (overhang + clear-top), so only the remainder is owed here, and never less
	 * than the normal gap. */
	.sw-card--has-icon .sw-card__inner > :first-child {
		width: auto;
		max-width: 100%;
		margin-bottom: max(
			var(--wp--preset--spacing--40),
			calc(
				var(--sw-card-icon-overhang) + var(--sw-card-clear-top) -
					var(--sw-card-icon-size)
			)
		);
	}

	/* The icon is sized by HEIGHT here, not by width.
	 *
	 * The overhang above is a fraction of --sw-card-icon-size, so that fraction
	 * only means "this much of the icon sticks out" if the size IS the icon's
	 * height. Sized by width instead, the brand set's mix of landscape and
	 * portrait artwork overhangs by anything from 25% (raised fist) to 48%
	 * (dove) — measured, not guessed. By height all three land on 40%.
	 *
	 * `width: auto` is safe ONLY because the height is definite here: these
	 * inlined SVGs carry a viewBox and no width/height attributes, so with both
	 * dimensions auto the element collapses to zero and the artwork vanishes
	 * outright. See the note further up. */
	.sw-card--has-icon .sw-card__inner > :first-child svg {
		height: var(--sw-card-icon-size);
		width: auto;
		max-width: 100%;
	}
}

/* The stack the Dreiklang pattern places three of these in. Deliberately NOT
 * `.sw-dreiklang`, which carries `overflow-x: clip` for the rotated ::before
 * panel the old sw/pitch card drew — that clip would cut off this card's shadow
 * bleed. Stray horizontal overflow is already handled at the root (see `body`
 * in the sw/sektion section). */
.sw-card-stack {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--80);
	max-width: var(--wp--style--global--content-size);
	margin-inline: auto;
	padding-block: var(--wp--preset--spacing--90);
}

/* --- sw/illustration -------------------------------------------------------
 * Inlined brand SVG (assets/img/illustrations/); the artwork fills with
 * currentColor, so the color token class on the wrapper drives it. */
.sw-illustration {
	line-height: 0;
}

.sw-illustration svg {
	display: block;
	width: 100%;
	height: auto;
}

.sw-illustration--size-s {
	max-width: 9rem;
}

.sw-illustration--size-m {
	max-width: 14rem;
}

.sw-illustration--size-l {
	max-width: 22rem;
}

.sw-illustration--color-petal-frost {
	color: var(--wp--preset--color--petal-frost);
}

.sw-illustration--color-mint {
	color: var(--wp--preset--color--mint);
}

.sw-illustration--color-burnt-rose {
	color: var(--wp--preset--color--burnt-rose);
}

.sw-illustration--color-mahagony {
	color: var(--wp--preset--color--mahagony);
}

.sw-illustration--color-cream {
	color: var(--wp--preset--color--cream);
}

/* --- Mint arrow-link (motif #5) ---------------------------------------------
 * Uppercase Clash Grotesk label + arrow, bound to the accent token. Generic:
 * used by sw/cta-band now, reusable by later tickets. Inside light Sektionen
 * the surface link override above wins (mint has no contrast there).
 *
 * These rules are now also the skin for the sw/arrow-link BLOCK, which emits
 * this same class. The block additionally sets --arrow-<direction> and
 * --arrow-before/after modifiers; neither needs CSS, because render.php places
 * the glyph before or after the label in the MARKUP and the flex row follows.
 * The modifiers exist so page CSS can target a specific arrow if it ever has to.
 *
 * The three hand-built copies this block replaced — `.sw-home-hero__cta a`,
 * `.sw-pitch__cta a` and `.sw-ueber-cv a` — have all been deleted now that their
 * pages emit the block; the drift between them is why the block exists. */
.sw-arrow-link {
	display: inline-flex;

	/* Grid/flex parents blockify an inline-flex child, which would stretch the
	 * link — and therefore its click target and hover underline — across the
	 * whole column. The Dreiklang card (.sw-dreiklang-card) is exactly such a
	 * grid; `justify-self` is simply ignored where the parent is not one. */
	justify-self: start;
	align-items: center;
	gap: 0.65em;
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--small);

	/* Clash Grotesk BOLD — Figma uses the bold cut for every arrow-link, not the
	 * Medium that body copy uses. 700 is a real shipped face
	 * (ClashGrotesk-Bold.woff2); do not use 600, which would silently synthesise
	 * against the 700 face. */
	font-weight: 700;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--wp--custom--color--accent);
	text-decoration: none;
}

.sw-arrow-link:hover .sw-arrow-link__label,
.sw-arrow-link:focus .sw-arrow-link__label {
	text-decoration: underline;
	text-underline-offset: 0.3em;
}

.sw-arrow-link__arrow {
	width: 1.5em;
	height: 1.5em;
	flex: none;
}

/* Editor-only: the block renders as a bare link, so an unset target would be
 * invisible until someone clicked it on the front end. add_editor_style() loads
 * this file in the editor, hence the plain class rather than an editor.css. */
.sw-arrow-link__editor-hint {
	display: block;
	margin-top: 0.5em;
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.75rem;
	letter-spacing: 0;
	text-transform: none;
	opacity: 0.75;
}

/* --- core/button — the brand button ------------------------------------------
 * Figma ANGEBOT 493:3663: a SOLID Dark Mint block with Bone Wheat uppercase
 * Clash Grotesk Bold at 20px, 12px/28px padding, square corners, and the brand
 * arrow trailing the label. Sharp corners and the solid fill are the design —
 * the earlier hand-built `.sw-angebot-cta` was an OUTLINED box in `ink`, which
 * is where the drift this rule exists to end came from.
 *
 * The point of styling core/button rather than shipping another bespoke block:
 * an editor inserts a plain button and gets the on-brand one, so third-party or
 * ad-hoc button styles never accumulate. Colors, type, padding and radius are in
 * theme.json (`styles.elements.button`) so the editor's own panels show the
 * right defaults; only what theme.json cannot express lives here — the flex row
 * and the arrow, which needs a pseudo-element.
 *
 * The arrow is a MASK, not a background image or a text glyph: masking paints
 * `currentColor` through the shape, so the arrow follows the label's color into
 * the hover state for free — and it is the real brand vector rather than the
 * "\2192" approximation the pre-block CTAs used. The path is the same Figma
 * export as sw_arrow_glyph() in inc/blocks.php and src/arrow-link/arrows.js;
 * change one, change all three. Butt caps / miter joins (no stroke-linecap or
 * stroke-linejoin) are what give it sharp corners — do not "round" them. */
.wp-block-button__link,
.wp-element-button {
	display: inline-flex;
	align-items: center;
	gap: 0.5em; /* 10px at the 20px label */
	text-decoration: none;
}

.wp-block-button__link::after,
.wp-element-button::after {
	content: "";

	/* 22.5px against the 20px label, per the Figma frame. */
	width: 1.125em;
	height: 1.125em;
	flex: none;
	background-color: currentColor;
	mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 23 23' fill='none' stroke='%23000' stroke-width='3.75'%3E%3Cpath d='M0.937501 11.25L19.0625 11.25M12.5 17.8125L19.0625 11.25L12.5 4.6875'/%3E%3C/svg%3E");
	mask-repeat: no-repeat;
	mask-position: center;
	mask-size: contain;
}

/* --- sw/cta-band -------------------------------------------------------------
 * "Zusammen Staub aufwirbeln?" — sits on the page background (Burnt Rose in
 * Figma), so it stays transparent and inherits the surrounding ink; only the
 * arrow-link carries the accent. */
.sw-cta-band {
	position: relative;
	overflow: hidden;
	padding-block: var(--wp--preset--spacing--100);
}

/* Suspended dust haze (src/cta-band/view.js). The canvas element only exists
 * when JS runs, so nothing here affects a no-JS page. Pointer-transparent: the
 * stir listener sits on the section, the CTA link must stay clickable. */
.sw-cta-band__dust {
	position: absolute;
	inset: 0;
	z-index: 0;
	display: block;
	width: 100%;
	height: 100%;
	pointer-events: none;
}

.sw-cta-band__inner {
	position: relative;
	z-index: 1;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--wp--preset--spacing--60);
	/* Shared content column — absorbs the gutter so its left edge aligns with the
	 * hero, Sektionen and bare content at all widths. */
	max-width: calc(var(--wp--custom--measure--content) + 2 * var(--wp--custom--gutter));
	margin-inline: auto;
	padding-inline: var(--wp--custom--gutter);
}

.sw-cta-band__heading {
	margin: 0;
	font-family: var(--wp--preset--font-family--heading);
	font-size: var(--wp--preset--font-size--xx-large);
	font-weight: 900;
	line-height: 1;
	color: inherit;
}

/* --- Skew / rotation motif (motif #2) ----------------------------------------
 * Block styles (registered in inc/blocks.php) + raw utilities for patterns.
 * Tune per use via the --sw-skew custom property. */
.is-style-skew-left,
.sw-skew-left {
	transform: rotate(calc(-1 * var(--sw-skew, 3deg)));
}

.is-style-skew-right,
.sw-skew-right {
	transform: rotate(var(--sw-skew, 3deg));
}

/* ==========================================================================
 * Ticket R05 — Case Study (single projekt) block palette (design-audit §4
 * CASE STUDY; Figma 493:4345). v3 project hero, sw/kennzahlen metrics rows,
 * subsection heading+text pairs, and the horizontal media gallery. All values
 * are tokens or currentColor so every piece works on any Sektion surface.
 * ========================================================================== */

/* --- Projekt hero (v3) ------------------------------------------------------
 * Full-bleed duotone featured image with a chevron bottom edge; the serif
 * title overlaps the photo (Figma: title starts on the image, ends on the
 * Burnt Rose band); dot-separated Dienstleistungen line underneath. */
.sw-projekt-hero {
	padding-bottom: var(--wp--preset--spacing--70);
}

.sw-projekt-hero__image {
	--sw-hero-edge: clamp(3rem, 10vw, 9rem);
	margin-top: 0;
	margin-bottom: 0;
	clip-path: polygon(
		0 0,
		100% 0,
		100% calc(100% - var(--sw-hero-edge) * 0.4),
		28% 100%,
		0 calc(100% - var(--sw-hero-edge))
	);
}

.sw-projekt-hero__image img {
	width: 100%;
	height: clamp(24rem, 64vh, 46rem);
	object-fit: cover;
}

.sw-projekt-hero__title {
	position: relative;
	z-index: 1;
	margin: clamp(-9rem, -11vw, -3rem) 0 var(--wp--preset--spacing--50);
	color: var(--wp--preset--color--petal-frost);
}

/* The hero services line is dot-separated uppercase text (Figma), not pills:
 * neutralise the generic chip skin inside the hero only. */
.sw-projekt-hero__chips {
	gap: 0.75em;
	margin-bottom: var(--wp--preset--spacing--60);
}

.sw-projekt-hero__chips .sw-chip {
	padding: 0;
	border: 0;
	border-radius: 0;
	background: none;
	color: inherit;
	text-transform: uppercase;
	letter-spacing: 0.12em;
}

.sw-projekt-hero__chips .sw-chip + .sw-chip::before {
	content: "\00b7";
	margin-right: 0.75em;
}

/* --- Subsection (heading + text pair) — SUPERSEDED by .sw-liste ------------
 * Kept for Case-Studies authored before the switch to sw/liste. No block
 * identity to transform from (core/group + hand-typed classNames), so these are
 * converted by hand. Removal gated on .scratch/liste-block/issues/06-cleanup.md.
 * The definition-list rhythm inside the Case-Study panels (Imagepflege /
 * Service & Info / …): small serif heading, body indented to the right. */
.sw-subsection {
	margin-block: var(--wp--preset--spacing--60);
}

.sw-subsection__title {
	margin: 0 0 var(--wp--preset--spacing--30);
	font-size: var(--wp--preset--font-size--medium);
}

.sw-subsection__text {
	margin: 0;

	/* Its measure is narrower than the content width, so WP's constrained-layout
	 * `margin-left/right: auto !important` would centre it and double the indent;
	 * Figma 493:4372 indents it from the heading by the padding alone. */
	margin-inline: 0 auto !important;
	padding-left: clamp(2rem, 8vw, 7.5rem);
	max-width: 40rem;
}

/* --- Media gallery ("Social Media Videos") -----------------------------------
 * A styled CORE Gallery, no custom block and no JS: one row of portrait
 * (9:16) cards that scroll-snaps horizontally — the no-dependency stand-in
 * for the Figma arrow carousel. The width overrides need !important because
 * core's gallery stylesheet sets per-column widths with !important too. */
.wp-block-gallery.sw-gallery {
	flex-wrap: nowrap;
	overflow-x: auto;
	scroll-snap-type: x proximity;
	gap: var(--wp--preset--spacing--50);
	padding-bottom: var(--wp--preset--spacing--40);
	-webkit-overflow-scrolling: touch;
	scrollbar-width: thin;
}

.wp-block-gallery.sw-gallery .wp-block-image {
	flex: 0 0 clamp(15rem, 26vw, 25rem) !important;
	width: clamp(15rem, 26vw, 25rem) !important;
	margin: 0;
	scroll-snap-align: start;
}

.wp-block-gallery.sw-gallery .wp-block-image img {
	width: 100%;
	height: 100% !important;
	aspect-ratio: 9 / 16;
	object-fit: cover;
	border-radius: 4px;
}

/* ==========================================================================
 * Ticket R07 — Angebot v3 (design-audit §4 ANGEBOT; Figma 493:3619).
 * The page hero is now the sw/hero block; this section covers the three
 * alternating illustration/text service sections, the workshops line + CTA
 * button, the numbered process (sw/liste) and the "Richtpreise" table
 * (sw/tabelle). All values are tokens or currentColor.
 * ========================================================================== */

/* --- Service sections (illustration ⟷ text) ---------------------------------
 * Three alternating sections; #2 sits on the page background, #1/#3 inside
 * Mahagony chevron Sektionen. Alternation is an explicit modifier class
 * (sections are not siblings in one list, so :nth-child can't drive it). The
 * illustration is the group's FIRST child — addressed positionally, exactly
 * like the Dreiklang card, for editor/front-end parity. */
.sw-angebot-service {
	display: grid;
	gap: var(--wp--preset--spacing--50) var(--wp--preset--spacing--80);
	align-items: center;
}

.sw-angebot-service > :first-child {
	justify-self: center;
}

.sw-angebot-service__title {
	margin: 0 0 var(--wp--preset--spacing--50);
	font-size: var(--wp--preset--font-size--medium);
}

@media (min-width: 782px) {
	.sw-angebot-service {
		grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.25fr);
	}

	.sw-angebot-service > :first-child {
		grid-column: 1;
		grid-row: 1;
	}

	.sw-angebot-service > :not(:first-child) {
		grid-column: 2;
	}

	.sw-angebot-service--flip > :first-child {
		grid-column: 2;
	}

	.sw-angebot-service--flip > :not(:first-child) {
		grid-column: 1;
		grid-row: 1;
	}
}

/* --- Workshops line + "Erzähl mir von deinem Projekt" CTA --------------------
 * Content-authored button-link (same wrapper-class pattern as the arrow-links,
 * see R06): the anchor gets the bordered uppercase skin + arrow glyph via the
 * paragraph class, so the client can re-link it without losing the style. */
.sw-angebot-workshops {
	margin-block: var(--wp--preset--spacing--80) var(--wp--preset--spacing--60);
}

/* Standoff below the Workshops CTA (the core/button block that carries the
 * brand button skin from theme.json + .wp-block-button__link). */
.sw-angebot-cta {
	margin-block: 0 var(--wp--preset--spacing--80);
}

/* ==========================================================================
 * Ticket R08 — Über Staub & Wirbel v3 (design-audit §4 ABOUT; Figma 493:3839).
 * The "Staub – Olivia" portrait section with the mint CV arrow-link and the
 * sw/netzwerk member list on the mint Sektion. All values are tokens or
 * currentColor; the hero is the image-less (`--plain`) sw/hero block.
 * ========================================================================== */

/* --- Staub – Olivia (portrait ⟷ text) ----------------------------------------
 * Portrait left, serif heading + prose + CV link right (Figma: 418px photo,
 * 480px text). The portrait is the group's FIRST child — addressed
 * positionally, exactly like the service sections, for editor parity. */
.sw-ueber-staub {
	display: grid;
	gap: var(--wp--preset--spacing--50) var(--wp--preset--spacing--80);
	align-items: start;
}

@media (min-width: 782px) {
	.sw-ueber-staub {
		grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
	}
}

.sw-ueber-staub__text > h2 {
	margin: 0 0 var(--wp--preset--spacing--50);
}

/* CV arrow-link (motif #5). The seeder now emits the sw/arrow-link BLOCK here,
 * which brings its own skin — this rule only supplies the standoff from the
 * prose above, which is a layout concern of THIS section, not of the block. */
.sw-ueber-staub__text > .wp-block-sw-arrow-link {
	margin-top: var(--wp--preset--spacing--60);
}

/* ==========================================================================
 * Ticket R09 — Referenzen (design-audit §4 PROJEKTE; Figma 493:3725).
 * The client-logo strip under the project cards: centered uppercase
 * "REFERENZEN" heading, then skewed dark tiles forming the broken angled
 * stripe across the Burnt Rose page background, one logo per tile in a
 * monochrome brand tint. Progressive enhancement: the track is a plain
 * horizontal scroll-snap strip (the R05 gallery precedent); view.js injects
 * the ◀ ▶ arrows only when the tiles overflow. All values are tokens or
 * derived from them via color-mix/currentColor.
 * ========================================================================== */

/* --- Band wrapper (pattern-level group around heading + strip) ------------ */
/* The band is a genuine `alignfull` group (see sw_projekte_index_content() in
 * inc/projekte.php), so the width engine's `.alignfull` break-out — which has a
 * rule for both the front end and the editor canvas — takes it to the viewport
 * edges on its own. No band-specific break-out lives here; only the inner strip
 * opts back out of the group's 1120 content constraint (below). */
.sw-referenzen-band {
	padding-top: var(--wp--preset--spacing--90);
	padding-bottom: var(--wp--preset--spacing--80);
}

.sw-referenzen-band__title {
	margin: 0 0 var(--wp--preset--spacing--70);
	text-align: center;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 700;
	color: var(--wp--preset--color--petal-frost);
}

/* --- Strip ----------------------------------------------------------------
 * The wrapper is the positioning context for the injected arrows; the track
 * scrolls. Vertical padding gives the rotated tiles room so they are not
 * clipped by the scroll container. */
.sw-referenzen {
	position: relative;
}

/* The band is full-bleed, but it is a `constrained` group, so WordPress's own
 * group-layout style (`.wp-container-… > *`) still caps the band's direct
 * children at the 1120 content column. Release that cap on the strip block so
 * the scrolling track reaches the viewport edges left + right. Two classes beat
 * core's `.wp-container-… > *` (one class + universal) on specificity, so no
 * !important is needed. The heading stays capped/centered — only the slider
 * goes full width. */
.sw-referenzen-band .sw-referenzen {
	max-width: none;
}

.sw-referenzen__track {
	display: flex;
	flex-wrap: nowrap;
	overflow-x: auto;
	scroll-snap-type: x proximity;
	gap: var(--wp--preset--spacing--70);
	padding: var(--wp--preset--spacing--40) var(--wp--preset--spacing--30);
	-webkit-overflow-scrolling: touch;
	scrollbar-width: thin;
	scrollbar-color: color-mix(
		in srgb,
		var(--wp--preset--color--petal-frost) 35%,
		transparent
	) transparent;
}

/* Once JS confirms an overflowing strip, reserve room for the edge arrows. */
.sw-referenzen--enhanced .sw-referenzen__track {
	padding-left: clamp(2.5rem, 6vw, 4.5rem);
	padding-right: clamp(2.5rem, 6vw, 4.5rem);
}

/* --- Tile ------------------------------------------------------------------
 * One skewed dark quad per logo (the Figma stripe pieces): a surface mixed
 * from the two dark brand tokens, slightly rotated with alternating
 * direction via the skew motif (--sw-skew, see .sw-skew-left/right). */
.sw-referenz {
	position: relative;
	flex: 0 0 clamp(12rem, 22vw, 15rem);
	aspect-ratio: 48 / 25;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--wp--preset--spacing--40) var(--wp--preset--spacing--50);
	background-color: var(--wp--preset--color--mahagony);
	background-color: color-mix(
		in srgb,
		var(--wp--preset--color--mahagony) 45%,
		var(--wp--preset--color--burnt-rose)
	);
	scroll-snap-align: center;
	text-decoration: none;
	transform: rotate(calc(-1 * var(--sw-skew, 2deg)));
	transition: transform 0.15s ease;
}

.sw-referenzen__track > .sw-referenz:nth-child(even) {
	transform: rotate(var(--sw-skew, 2deg));
}

a.sw-referenz:hover,
a.sw-referenz:focus-visible {
	transform: rotate(0deg) scale(1.03);
}

a.sw-referenz:focus-visible {
	outline: 2px solid var(--wp--preset--color--mint);
	outline-offset: 3px;
}

/* Monochrome brand tint: grayscale the logo and screen-blend it onto the
 * dark tile, so dark logo areas melt into the token surface and light areas
 * read near-cream — one treatment for any supplied logo file, in pure CSS. */
.sw-referenz__logo {
	display: block;
	max-width: 100%;
	max-height: 4rem;
	object-fit: contain;
	filter: grayscale(1) contrast(1.05);
	mix-blend-mode: screen;
}

/* Glyph fallback when a referenz has no picture (server-rendered — see
 * src/referenz/render.php). Muted against the tile so a strip of fallbacks
 * never out-shouts the real logos next to it. */
.sw-referenz__fallback {
	display: block;
	width: 3rem;
	color: var(--wp--preset--color--cream);
	opacity: 0.55;
}

.sw-referenz__fallback svg {
	display: block;
	width: 100%;
	height: auto;
}

/* Text fallback: editor-only now (the front end renders the glyph instead). */
.sw-referenz__name {
	color: var(--wp--preset--color--cream);
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	text-align: center;
	font-size: var(--wp--preset--font-size--base);
}

/* Editor-only inline "Logo wählen" button under the name fallback. */
.sw-referenz .sw-referenz__upload {
	position: absolute;
	bottom: 0.25rem;
	font-size: 0.75rem;
	color: var(--wp--preset--color--mint);
}

/* --- Arrows (injected by view.js; never present without JS) ---------------- */
.sw-referenzen__nav {
	position: absolute;
	top: 50%;
	transform: translateY(-50%);
	z-index: 1;
	border: 0;
	padding: 0.5rem;
	background: transparent;
	color: var(--wp--preset--color--petal-frost);
	font-size: clamp(1.75rem, 3vw, 2.5rem);
	line-height: 1;
	cursor: pointer;
}

.sw-referenzen__nav--prev {
	left: 0;
}

.sw-referenzen__nav--next {
	right: 0;
}

.sw-referenzen__nav:hover {
	color: var(--wp--preset--color--cream);
}

.sw-referenzen__nav:focus-visible {
	outline: 2px solid var(--wp--preset--color--mint);
	outline-offset: 2px;
}

/* ==========================================================================
 * Legal pages (R11) — Impressum / Datenschutz / AGB
 * Shared template (Figma Datenschutz 493:4217): the header's mahagony band
 * runs out into a chevron and carries the serif display H1 + the small
 * "Version vom …" subline; the long-form prose follows in the narrow measure
 * (.sw-measure-narrow, token --wp--custom--measure--narrow) on the Burnt Rose
 * page background. All values are tokens or plain type metrics.
 * ========================================================================== */
.sw-legal__title {
	margin: var(--wp--preset--spacing--60) 0 0;
	line-height: 0.95;
}

.sw-legal__version {
	margin: var(--wp--preset--spacing--50) 0 0;
	font-size: 1rem;
}

/* Long-form prose reads at ~16–18px (Figma 16/22), below the site's display
 * body size — a reading column, not marketing copy. */
.sw-legal {
	font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
	line-height: 1.45;
}

/* Numbered section headings (Voyage serif, Figma 28px = medium preset) get a
 * larger gap above than the paragraph rhythm; the class rule outweighs the
 * :where() blockGap flow rule without !important. */
.sw-legal .sw-legal__heading {
	margin-block-start: var(--wp--preset--spacing--70);
	margin-block-end: var(--wp--preset--spacing--40);
}

/* ==========================================================================
 * sw/liste — «Abschnittsliste», the one repeatable «Titel + Text» list.
 * Spec: .scratch/liste-block/spec.md
 *
 * Supersedes three near-identical implementations: .sw-schritte/.sw-schritt
 * (the Angebot process), .sw-netzwerk/.sw-netzwerk-mitglied (the Über members)
 * and the hand-classed .sw-subsection groups of the Case-Studies. Those three
 * rule sets stay below for content authored before the switch — see
 * .scratch/liste-block/issues/06-cleanup.md for when they may go.
 *
 * The ROWS save identical markup; every difference is a modifier on the
 * container, because block context is not available in a static block's save().
 * All values are theme.json presets.
 * ========================================================================== */

.sw-liste {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--70);
	margin-top: var(--wp--preset--spacing--70);
}

/* Column flow by default so the header, an image and the body never sit flush
 * against each other on narrow screens; ≥782px the image variant takes over
 * with a grid. */
.sw-liste__eintrag {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--40);
}

.sw-liste__kopf {
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--20)
		var(--wp--preset--spacing--40);
}

/* Design (Figma): big title 40px, small title 28px. The scale has no 40px step
 * — `large` is 36px, `x-large` 46px — so «normal» takes `large` and lands 4px
 * under rather than inventing an off-scale value; «kompakt» takes `medium`,
 * which is 28px exactly. Add a 2.5rem preset to theme.json if the 40px has to
 * be exact. */
.sw-liste__titel {
	margin: 0;
	font-size: var(--wp--preset--font-size--large);
}

/* The number is typed ("1", "1a", "Phase A") but the separator is not: an
 * em-space/em-dash/em-space is not something an editor types reliably, so CSS
 * owns the glyph and the field stays a bare number. Carried from the CSS
 * counter this replaces (.sw-schritt__title::before). */
/* The number is a SIBLING of the title, not part of it (the superseded
 * .sw-schritt__title::before counter lived inside the heading and inherited its
 * type for free). `inherit` here would pick up the flex row's body type, so the
 * heading face and size are restated — kept in sync with .sw-liste__titel. */
.sw-liste__nummer {
	font-family: var(--wp--preset--font-family--heading);
	font-size: var(--wp--preset--font-size--large);
	font-weight: 900;
	line-height: 1.05;
}

.sw-liste__nummer::after {
	content: "\2002\2014\2002"; /* " — " */
}

/* While the field is empty the separator would be a dash with nothing in front
 * of it. Two selectors are needed because "empty" differs by context: on the
 * front end an empty number emits no span at all (so :empty is only a
 * belt-and-braces guard), while in the EDITOR RichText fills the span with a
 * zero-width space plus a placeholder element — never :empty — so the
 * placeholder itself has to be matched. */
.sw-liste__nummer:empty::after,
.sw-liste__nummer:has( [data-rich-text-placeholder] )::after {
	content: none;
}

.sw-liste__text > :where(p, ul, ol) {
	margin: 0 0 var(--wp--preset--spacing--40);
	max-width: 52ch;
}

.sw-liste__text > :last-child {
	margin-bottom: 0;
}

/* --- Untertitel (die Rolle eines Netzwerk-Mitglieds) -----------------------
 * Mobile-first: the subtitle sits ABOVE the title on its own line (order -1 +
 * a full-width basis forces the wrap), because a long role pushed onto a second
 * line after the name reads as an orphan. From 782px it returns to the baseline
 * row beside the title, where the source order already puts it. */
.sw-liste__zusatz {
	order: -1;
	flex-basis: 100%;
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 500;
	letter-spacing: 0.14em;
	text-transform: uppercase;
}

@media (min-width: 782px) {
	.sw-liste__zusatz {
		order: 0;
		flex-basis: auto;
	}
}

/* --- Text einrücken --------------------------------------------------------
 * Only meaningful WITHOUT the image column: there the image already provides
 * the horizontal offset, and adding the indent on top would double it. The
 * :not() keeps the two options independent instead of making the editor know
 * they interact. */
.sw-liste--eingerueckt:not(.sw-liste--mit-bild) .sw-liste__text {
	/* Its measure is narrower than the content width, so WP's constrained-layout
	 * `margin-left/right: auto !important` would centre it and double the indent;
	 * Figma 493:4372 indents it from the heading by the padding alone. */
	margin-inline: 0 auto !important;
	padding-left: clamp(2rem, 8vw, 7.5rem);
}

/* .sw-liste--eingerueckt {
	gap: var(--wp--preset--spacing--60);
} */

.sw-liste--eingerueckt .sw-liste__text > :where(p, ul, ol) {
	max-width: 40rem;
}

/* --- Grösse: kompakt (Listen innerhalb eines Fliesstexts) ------------------
 * 28px, the design's small title. NB the superseded .sw-subsection__title asked
 * for `--font-size--medium` with a 1.5rem FALLBACK that never applied (the
 * preset resolves to 1.75rem = 28px). The dead fallback was real, but the size
 * it rendered was RIGHT — the actual defect was the big title never reaching
 * 40px, which `.sw-liste__titel` above now addresses. */
.sw-liste--kompakt .sw-liste__titel,
.sw-liste--kompakt .sw-liste__nummer {
	font-size: var(--wp--preset--font-size--medium);
}

.sw-liste--kompakt .sw-liste__text > :where(p, ul, ol) {
	max-width: 40rem;
}

/* --- Bildspalte ------------------------------------------------------------
 * Reserved on the CONTAINER, not per row, so rows stay flush when some have no
 * photo. Targets the block's own figure — the superseded .sw-netzwerk-mitglied
 * rule addressed "the first .wp-block-image", which broke whenever an editor
 * reordered the inner blocks. Below 782px the row is the column flex above, so
 * the flex gap already separates photo from text. */
.sw-liste__bild {
	margin: .25rem 0 0 0;
	justify-self: flex-end;
}

.sw-liste__bild img {
	mix-blend-mode: overlay;
	display: block;
	width: 100%;
	max-width: 10rem;
	aspect-ratio: 1;
	object-fit: cover;
}

@media (min-width: 782px) {
	.sw-liste--mit-bild .sw-liste__eintrag {
		display: grid;
		grid-template-columns: 12.5rem minmax(0, 1fr);
		gap: var(--wp--preset--spacing--40)
			var(--wp--preset--spacing--60);
		align-items: start;
	}

	.sw-liste--mit-bild .sw-liste__kopf {
		grid-column: 1 / -1;
	}

	.sw-liste--mit-bild .sw-liste__bild {
		grid-column: 1;
	}

	/* Column 2 explicitly, so a row WITHOUT a photo still starts beside the
	 * reserved track instead of sliding under it. */
	.sw-liste--mit-bild .sw-liste__text {
		grid-column: 2;
	}
}

/* The editor-only "Bild wählen" affordance in an empty image slot. */
.sw-liste__bild-upload {
	justify-content: flex-start;
}

/* Editor only: the "+" that adds an Abschnitt.
 *
 * WordPress positions the appender ABSOLUTELY inside flex containers, with BOTH
 * `left` and `right` set. Overriding only `left` therefore stretches it across
 * the whole list, where it overlaps the last Abschnitt. Putting it back into
 * normal flow is the fix: as an ordinary flex item it lands after the last row,
 * and `align-self` stops it stretching. The button needs its own width reset or
 * it inherits the full-width box. */
.sw-liste > .block-list-appender {
	position: relative;
	inset: auto;
	align-self: flex-start;
	width: auto;
	margin: 0;
}

.sw-liste > .block-list-appender .block-editor-button-block-appender {
	width: auto;
}

/* --- sw/tabelle / sw/tabellen-zeile ------------------------------------------
 * The one two-column key/value table, superseding sw/preisliste and
 * sw/kennzahlen — those were the same component built twice (their row CSS
 * differed by exactly one property). Hairlines are currentColor at low alpha so
 * a table reads on any Sektion surface; the --line token is the
 * no-color-mix fallback.
 *
 * Both column labels are independently optional, so all four header states have
 * to look right: none / left only / right only / both. */
.sw-tabelle__kopf {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--50);
	padding-bottom: var(--wp--preset--spacing--40);
	border-bottom: 1px solid var(--wp--custom--color--line);
	border-bottom-color: color-mix(in srgb, currentColor 35%, transparent);
}

.sw-tabelle__kopf-links,
.sw-tabelle__kopf-rechts {
	font-family: var(--wp--preset--font-family--heading);
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 500;

	/* Figma 493:3706/3707 has the column headers in the surface ink (Petal Frost
	 * on Burnt Rose), not the mint accent — and inheriting keeps them legible on
	 * whichever Sektion surface the table is dropped onto. */
	color: inherit;
}

/* The left slot renders as span/h2/h3/h4 (the kopfTag attribute), and headings
 * arrive with theme margins. The span form never needed this; the heading form
 * would otherwise gain stray space above the hairline. */
.sw-tabelle__kopf-links {
	margin: 0;
}

/* THE SINGLE-LABEL FIX. With two labels, space-between does the work and this
 * is a no-op. With only the RIGHT label filled it is the only child, and
 * space-between would put it on the LEFT — silently wrong, and the one state in
 * the four-state matrix that does not fall out of the layout for free. */
.sw-tabelle__kopf-rechts {
	margin-inline-start: auto;
}

.sw-tabelle__zeile {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--50);
	padding-block: var(--wp--preset--spacing--50);
	border-bottom: 1px solid var(--wp--custom--color--line);
	border-bottom-color: color-mix(in srgb, currentColor 25%, transparent);
}

.sw-tabelle__label {
	flex: 1 1 auto;
	min-width: 0;

	/* Load-bearing: long unbroken labels ("Kommunikations-/Kampagnen-Konzept")
	 * otherwise overflow their column at ~390px and collide with the nowrap
	 * value. Short labels (the former Kennzahlen) never hit it, but the unified
	 * block must carry the fix for both. */
	overflow-wrap: anywhere;
}

.sw-tabelle__wert {
	flex: 0 0 auto;
	text-align: right;
	white-space: nowrap;
	font-weight: 500;
}

/* Editor only: the "+" that adds a row. Same trap as .sw-liste — WordPress
 * positions the appender absolutely with both `left` and `right` set, so it has
 * to go back into normal flow or it stretches across the table and overlaps the
 * last row. */
.sw-tabelle__zeilen > .block-list-appender {
	position: relative;
	inset: auto;
	width: auto;
	margin: 0;
}

.sw-tabelle__zeilen > .block-list-appender .block-editor-button-block-appender {
	width: auto;
}

/* ==========================================================================
 * Editor canvas only — the WordPress post-title field.
 *
 * main.css is shared with the block editor via add_editor_style(), so the
 * theme's H1 (big Voyage serif, pink) was landing on WP's built-in post-title
 * field too. Every page here authors its own hero H1 in the CONTENT (page.php
 * never prints the_title()), so that made the editor show the title TWICE — a
 * hero-styled duplicate above the real hero block.
 *
 * We can't drop the WP title: it's the admin/list label, the SEO <title> base,
 * the slug source and what archives render. So instead of hiding it (which
 * removes the only place to edit it), we present it as a small, distinct
 * FORM FIELD — a white input, centered at the top of the canvas, with a
 * disclaimer directly underneath — so it reads as chrome, not page content.
 * WP prefixes add_editor_style() rules with `.editor-styles-wrapper`, so none
 * of this reaches the front end. The !important beats compiled theme.json
 * heading styles. */
.edit-post-visual-editor__post-title-wrapper,
.editor-visual-editor__post-title-wrapper {
	display: flex;
	flex-direction: column;
	align-items: center;
	margin-block: 2rem !important;
}

.editor-post-title,
.editor-post-title__input,
.wp-block-post-title {
	box-sizing: border-box !important;
	width: min(100%, 32rem) !important;
	margin: 0 auto !important;
	padding: 0.5rem 0.875rem !important;
	background: #fff !important;
	border: 1px solid rgba(54, 20, 24, 0.25) !important;
	border-radius: 6px !important;
	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
	font-family: var(--wp--preset--font-family--body) !important;
	font-weight: 500 !important;
	font-size: 1rem !important;
	line-height: 1.4 !important;
	letter-spacing: 0 !important;
	text-transform: none !important;
	text-align: center !important;
	color: var(--wp--custom--color--ink-inverse) !important;
}

/* Disclaimer directly under the input. ::after on the wrapper places it below
 * the field (which is the wrapper's only real child). Both wrapper class names
 * are targeted to survive WP's post-editor refactor. */
.edit-post-visual-editor__post-title-wrapper::after,
.editor-visual-editor__post-title-wrapper::after {
	content: "Interner Seitentitel — im Frontend nicht sichtbar";
	display: block;
	max-width: 32rem;
	margin-top: 0.375rem;
	text-align: center;
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.75rem;
	font-weight: 500;
	letter-spacing: 0.01em;
	color: var(--wp--custom--color--ink-muted);
	opacity: 0.75;
}

/* --- Floating variant: pages that open with a hero -----------------------------
 * On the front end a hero starts at the very top of the page — there is no title
 * above it, because page.php never prints the_title(). In the canvas the title
 * field sat in the flow and pushed the hero down, so the editor showed a maroon
 * band the front end doesn't have and the hero's top edge never matched.
 *
 * When the first block IS a hero, the field is taken out of flow and floated
 * over it instead: the hero starts at the canvas top exactly as it does live,
 * and the field lands on the part of the hero that can carry it — the photo, or
 * the empty upper area of the image-less band (design-audit §4: the title hangs
 * LOW, over the angled edge, so the top is always free).
 *
 * The condition is a plain `:has()` on the canvas root: the title wrapper and
 * the block list are siblings in the iframe body, so the body is the nearest
 * element that can see both. `.wp-block-sw-hero` is the class useBlockProps()
 * puts on the block wrapper in the editor — the front end never has this
 * structure (no title field at all), so nothing here can leak.
 *
 * Positioning context is that same body: it scrolls with the canvas content, so
 * the field travels with the hero instead of pinning to the viewport. */
.editor-styles-wrapper:has(
		:is(.wp-block-post-content, .is-root-container) > .wp-block-sw-hero:first-child
	) {
	position: relative;
}

.editor-styles-wrapper:has(
		:is(.wp-block-post-content, .is-root-container) > .wp-block-sw-hero:first-child
	)
	:is(
		.edit-post-visual-editor__post-title-wrapper,
		.editor-visual-editor__post-title-wrapper
	) {
	position: absolute;
	inset-block-start: 0;
	inset-inline: 0;
	z-index: 10;
	margin-block: 1.5rem !important;
	/* The wrapper spans the full canvas width, so left in the flow of pointer
	 * events it would swallow clicks on the hero either side of the field. Only
	 * the field itself and its caption take the mouse back. */
	pointer-events: none;
}

.editor-styles-wrapper:has(
		:is(.wp-block-post-content, .is-root-container) > .wp-block-sw-hero:first-child
	)
	:is(.editor-post-title, .editor-post-title__input, .wp-block-post-title) {
	pointer-events: auto;
}

/* Floating, the caption sits on the hero photo rather than the flat page colour,
 * where muted pink on an arbitrary image is a coin toss. A dark pill keeps it
 * legible over photo and empty band alike. */
.editor-styles-wrapper:has(
		:is(.wp-block-post-content, .is-root-container) > .wp-block-sw-hero:first-child
	)
	:is(
		.edit-post-visual-editor__post-title-wrapper,
		.editor-visual-editor__post-title-wrapper
	)::after {
	width: fit-content;
	margin-inline: auto;
	padding: 0.25rem 0.625rem;
	border-radius: 999px;
	background: rgba(54, 20, 24, 0.72);
	color: var(--wp--preset--color--petal-frost);
	opacity: 1;
	pointer-events: auto;
}

/* Video embed styling (Youtube, iframes, etc.) */
[class*="wp-embed-aspect-"] iframe {
    display: block;
    width: 100%;
    height: auto;
    margin-inline: auto;
    aspect-ratio: 16 / 9; /* sensible fallback */
}
.wp-embed-aspect-16-9 iframe { aspect-ratio: 16 / 9; }
.wp-embed-aspect-9-16 iframe { aspect-ratio: 9 / 16; }
.wp-embed-aspect-4-3  iframe { aspect-ratio: 4 / 3; }
.wp-embed-aspect-1-1  iframe { aspect-ratio: 1 / 1; }
.wp-embed-aspect-21-9 iframe { aspect-ratio: 21 / 9; }
