/* ============================================================
   So wie du bist Fotografie — main stylesheet
   Token base: KSWD tokens/primitives, re-skinned 1:1 per branding
   (Creme/Salbei/Warmweiß system, Jost + Cormorant Garamond + Great Vibes)
   ============================================================ */

/* ---------- Fonts (self-hosted, variable) ---------- */
@font-face {
  font-family: "Jost";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/jost-latin-variable.woff2") format("woff2");
}
@font-face {
  font-family: "Cormorant Garamond";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("../fonts/cormorant-garamond-latin-variable.woff2") format("woff2");
}
@font-face {
  font-family: "Cormorant Garamond";
  font-style: italic;
  font-weight: 300 700;
  font-display: swap;
  src: url("../fonts/cormorant-garamond-italic-latin-variable.woff2") format("woff2");
}
@font-face {
  font-family: "Great Vibes";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/great-vibes-latin-regular.woff2") format("woff2");
}

/* ---------- Tokens ---------- */
:root {
  /* Surfaces (branding §1) */
  --color-cream: #F2EDE4;        /* Creme — page background */
  --color-white: #FBFAF7;        /* Warmweiß — cards/forms (Reinweiß only as rare highlight) */
  --color-pure-white: #FFFFFF;   /* Reinweiß — rare small highlight only */
  --color-cream-deep: #E9E0D2;   /* Creme-Tief — alternate soft surface */

  /* Accent family (swapped by .theme-rose) */
  --accent: #A8B49A;             /* Salbei — primary buttons/accent */
  /* Salbei-Dunkel, darkened from the deck's #6B7A5E (WCAG deviation, logged in
     DESIGN.md): eyebrows/links are small text on Creme/Salbei-Fläche/Creme-Tief,
     where #6B7A5E lands at 3.5-3.9:1. #5C6850 is the same hue/saturation at the
     minimal darkness that clears 4.5:1 on every surface it renders on. */
  --accent-dark: #5C6850;        /* Salbei-Dunkel (a11y-deep) — text links, focus */
  --accent-soft: #D6DCCB;        /* Salbei-Hell — tags/hover surfaces */
  --surface-deep: #E6EADD;       /* Salbei-Fläche — calm sections */

  /* Text */
  --color-ink: #3A3F36;          /* Text — headings + body */
  --color-ink-secondary: #3A3F36;
  /* Sanft, darkened from the deck's #6E7266 (WCAG deviation, logged in DESIGN.md):
     captions/meta sit on Creme (4.23:1) and Creme-Tief footer (3.77:1). #62655B
     keeps the grey-green character and clears 4.5:1 on all caption surfaces. */
  --color-ink-muted: #62655B;    /* Sanft (a11y-deep) — captions/meta only */
  --color-ink-subtle: #62655B;

  /* Lines */
  --color-border: #DED7C8;       /* Linien */
  /* Form-field borders: the deck's Sanft #6E7266 in a border role (replaces the
     off-palette #C9C0AE, which sat at 1.7:1 — invisible as a field boundary).
     Sanft on Warmweiß is 4.7:1, comfortably past the 3:1 non-text minimum. */
  --color-border-strong: #6E7266;

  /* Compat aliases for library components */
  --color-violet: var(--accent);
  --color-violet-hover: var(--accent-dark);
  --color-violet-deep: var(--color-ink);
  --color-surface: var(--color-white);
  --color-surface-soft: var(--accent-soft);

  --shadow-soft: 0 18px 44px rgba(58, 63, 54, 0.10);
  --shadow-card: 0 14px 36px rgba(58, 63, 54, 0.12);
  --shadow-deep: 0 24px 60px rgba(58, 63, 54, 0.18);

  --radius-xs: 4px;              /* buttons, tags */
  --radius-sm: 6px;              /* cards max */
  --radius-md: 6px;
  --radius-lg: 6px;

  --shell-width: 1240px;
  --shell-gutter: clamp(20px, 4vw, 40px);
  --section-gap: clamp(72px, 9vw, 110px);

  /* Fluid spacing scale */
  --flow-2xs: clamp(8px, 1vw, 12px);
  --flow-xs: clamp(12px, 1.5vw, 18px);
  --flow-sm: clamp(18px, 2.2vw, 26px);
  --flow-md: clamp(26px, 3.2vw, 40px);
  --flow-lg: clamp(40px, 5vw, 64px);
  --flow-xl: clamp(56px, 7vw, 92px);

  /* Grid-gap tokens (ledger pass: promoted from recurring bespoke clamps —
     values identical to the previous literals, zero visual change) */
  --gap-grid: clamp(18px, 2.4vw, 30px);   /* card/tile grid gaps */
  --gap-tight: clamp(14px, 1.8vw, 22px);  /* masonry + image-band gaps */
  --gap-split: clamp(28px, 4.5vw, 64px);  /* split/teaser two-column gaps */

  /* Lockup rhythm — the ONE spacing sequence for every
     eyebrow → heading → lead → actions stack on the site. All three steps are
     existing scale tokens (no new values); the lockup block near the bottom of
     the layout primitives owns every gap in these stacks, so no context may
     add its own. See DESIGN.md "Lockup rhythm". */
  --lockup-eyebrow: var(--flow-xs);   /* 12-18px — eyebrow/script → heading */
  --lockup-lead: var(--flow-xs);      /* 12-18px — heading → body/lead */
  --lockup-actions: var(--flow-md);   /* 26-40px — body → actions/CTA */

  --font-body: "Jost", "Segoe UI", Helvetica, Arial, sans-serif;
  --font-display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --font-script: "Great Vibes", cursive;

  --halo-dark: 0 1px 2px rgba(26, 29, 24, 0.62), 0 2px 12px rgba(26, 29, 24, 0.45), 0 10px 44px rgba(26, 29, 24, 0.38);
  --header-height: 84px;

  /* Rosé family as named tokens (used by .theme-rose and the scoped rose accents) */
  --rose-accent: #D8BCB0;
  /* Rosé-Dunkel, darkened from the deck's #96705F (WCAG deviation, logged in
     DESIGN.md): wedding eyebrows/links on Puder sat at 3.6:1. #7D5D4F keeps the
     hue/saturation and clears 4.5:1 on Puder, Creme and the Creme-Tief footer. */
  --rose-accent-dark: #7D5D4F;
  --rose-accent-soft: #EAD9D0;
  --rose-surface-deep: #F1E7DE;

  /* Form error state (functional colour, warm red fitted to the palette;
     kept over the branding §8 dark-text recipe as a logged deviation — 5.8:1 on
     Warmweiß for error text, 5.8:1 as the invalid-border cue) */
  --color-error: #9C4A38;
}

/* Hochzeit variant: ONLY the accent family swaps (branding §1) */
.theme-rose {
  --accent: var(--rose-accent);            /* Rosé */
  --accent-dark: var(--rose-accent-dark);  /* Rosé-Dunkel */
  --accent-soft: var(--rose-accent-soft);  /* Rosé-Hell */
  --surface-deep: var(--rose-surface-deep);/* Puder */
}

/* ---------- Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; }
/* No scroll-behavior: smooth here: Chromium silently aborts smooth fragment scrolls
   in some environments, leaving anchor CTAs (#welten, /preise/#...) doing nothing.
   Instant jumps are reliable; a JS-driven glide may be added by a later pass if verified. */
/* Tap highlight: warm-ink tint instead of the engine-grey default flash
   (same rgba family as the shadow voice; the nav toggle keeps its own
   transparent value because its lines animate as the feedback). */
html { -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(58, 63, 54, 0.08); }

/* Text selection in the brand's tag tone (Salbei-Hell; Rosé-Hell on wedding
   pages via the same token) with ink text on top. */
::selection { background: var(--accent-soft); color: var(--color-ink); }

body {
  margin: 0;
  min-width: 320px;
  /* clip, not hidden: hidden makes body a scroll container and silently breaks
     anchor navigation (#welten, /preise/#familien, skip-link) in Chromium.
     hidden stays as the fallback for engines without clip support (old WebKit),
     which drop the second declaration. */
  overflow-x: hidden;
  overflow-x: clip;
  background: var(--color-cream);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.6;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}
body[data-nav-open="true"] { overflow: hidden; }
body[data-lightbox-open="true"] { overflow: hidden; }

img { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
button, input, textarea, select { font: inherit; }
button { cursor: pointer; }
ul, ol { margin: 0; padding: 0; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
[hidden] { display: none !important; }

:focus-visible { outline: 3px solid var(--accent-dark); outline-offset: 3px; }

.skip-link {
  position: absolute; top: -60px; left: 16px; z-index: 200;
  padding: 0.7rem 1.1rem; border-radius: var(--radius-xs);
  background: var(--color-ink); color: var(--color-white);
  font-weight: 500; transition: top 160ms ease;
}
.skip-link:focus-visible { top: 12px; }

/* ---------- Typography ---------- */
h1, h2, h3, h4 { margin: 0; font-family: var(--font-display); font-weight: 550; color: var(--color-ink); line-height: 1.08; letter-spacing: 0.002em; }
/* Cormorant's default figures are oldstyle (x-height 1/2, descending 3/9): fine in
   running serif text, but weak as display values. All display/value numerals use
   lining figures so prices, voucher values and step numerals sit at full height. */
h1, h2, h3, h4,
.step__num, .price-card__price { font-variant-numeric: lining-nums; }
h1 { font-size: clamp(2.7rem, 6.4vw, 4.6rem); }
h2 { font-size: clamp(2rem, 3.9vw, 3rem); }
h3 { font-size: clamp(1.3rem, 2vw, 1.65rem); }
p { margin: 0; }

/* Balanced heading wrap: no single-word widow lines on display type.
   Progressive enhancement (Chromium 114+, Safari 17.5+, Firefox 121+);
   older engines keep the plain greedy wrap, nothing breaks. */
h1, h2, h3, h4,
.quote-single__text, .statement__text { text-wrap: balance; }
/* Orphan control for running copy in engines that support `pretty`; no-op elsewhere. */
p, li, summary, figcaption { text-wrap: pretty; }

.section-lead { max-width: 62ch; font-size: clamp(1.02rem, 1.25vw, 1.15rem); color: var(--color-ink); }

/* Eyebrow and script accent carry NO own margin: the distance to the heading is
   a lockup step, owned by the lockup block below. (Before: the eyebrow's own
   bottom margin stacked on top of every container's gap, so the same pair sat
   18px apart in the hero and 36px apart in a media row.) */
.eyebrow {
  display: block; margin: 0;
  font-family: var(--font-body); font-weight: 500;
  font-size: 0.82rem; line-height: 1.2; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--accent-dark);
}
.script-accent {
  display: block; margin: 0;
  font-family: var(--font-script); font-weight: 400;
  font-size: clamp(1.7rem, 2.6vw, 2.3rem); line-height: 1.1;
  color: var(--accent-dark);
}

/* ---------- Layout primitives ---------- */
.shell { width: min(var(--shell-width), calc(100vw - (2 * var(--shell-gutter)))); margin: 0 auto; }
/* library variant, unused on current pages (kept per optimize ledger) */
.shell--narrow { width: min(820px, calc(100vw - (2 * var(--shell-gutter)))); margin: 0 auto; }

.section { position: relative; padding: var(--section-gap) 0; }
.section--cream { background: var(--color-cream); }
.section--sage { background: var(--surface-deep); }
.section--warm { background: var(--color-white); }
/* No .section--deepcream: Creme-Tief stays reserved for the footer ground and
   image-loading fallbacks (logged deviation) — not a section surface. */
/* No .section--flush-top: it existed only to cancel the trust strip's pull-up
   over the hero. The trust band no longer overlaps anything, so the section
   after it keeps its own full --section-gap. */

/* The heading block's own bottom margin is the SECTION step (heading block →
   section body), one uniform value everywhere; the steps INSIDE it come from
   the lockup below. When the block is the section's whole content (heading +
   lead only) there is no body to separate from, so the step drops. */
.section-heading { display: grid; margin-bottom: clamp(32px, 4.5vw, 52px); max-width: 780px; }
.section-heading:last-child { margin-bottom: 0; }
.section-heading--center { margin-inline: auto; text-align: center; }
.section-heading--center .section-lead { margin-inline: auto; }

/* ============================================================
   Lockup rhythm (BINDING, sitewide — one rule, no per-context one-offs)
   ------------------------------------------------------------
   Every stack of eyebrow → heading → lead/body → actions uses the SAME three
   steps, from the token scale: --lockup-eyebrow / --lockup-lead /
   --lockup-actions. The listed containers therefore declare NO gap of their
   own and their children carry no margins; add `.lockup` (or join this list)
   when a new context stacks the same parts.
   Out of scope on purpose: card-internal rhythms (.price-card, .step,
   .quote-card) — those are component spacing, not the lockup.
   ============================================================ */
:is(.lockup, .section-heading, .photo-hero__stack, .page-hero__stack,
    .split__body, .about-panel__body,
    .media-row__body, .cta-closer__stack, .cta-band__stack, .error-page,
    .care-note){ display: grid; gap: 0; }

/* step 2 (default): heading → body/lead, and any further body element */
:is(.lockup, .section-heading, .photo-hero__stack, .page-hero__stack,
    .split__body, .about-panel__body,
    .media-row__body, .cta-closer__stack, .cta-band__stack, .error-page,
    .care-note)> * + * { margin-top: var(--lockup-lead); }

/* step 1: eyebrow (or its script-accent flourish) → heading */
:is(.lockup, .section-heading, .photo-hero__stack, .page-hero__stack,
    .split__body, .about-panel__body,
    .media-row__body, .cta-closer__stack, .cta-band__stack, .error-page,
    .care-note)> .eyebrow + * { margin-top: var(--lockup-eyebrow); }
/* the script word belongs to the heading it introduces, so it sits tighter */
:is(.lockup, .section-heading, .photo-hero__stack, .page-hero__stack,
    .split__body, .about-panel__body,
    .media-row__body, .cta-closer__stack, .cta-band__stack, .error-page,
    .care-note)> .script-accent + * { margin-top: var(--flow-2xs); }

/* step 3 (last, so it wins over the steps above): body → actions/CTA */
:is(.lockup, .section-heading, .photo-hero__stack, .page-hero__stack,
    .split__body, .about-panel__body,
    .media-row__body, .cta-closer__stack, .cta-band__stack, .error-page,
    .care-note)> * + :is(.actions-row, .button, .text-link, .cta-closer__actions) { margin-top: var(--lockup-actions); }

/* ---------- Buttons (branding §3: hover lifts, NEVER recolors) ---------- */
.button {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  min-height: 48px; padding: 0.82rem 1.5rem;
  border: 1px solid transparent; border-radius: var(--radius-xs);
  font-family: var(--font-body); font-weight: 550; font-size: 0.92rem;
  letter-spacing: 0.1em; text-transform: uppercase; line-height: 1.15;
  text-align: center; white-space: nowrap;
  color: var(--color-ink);
  transition: transform 180ms ease, box-shadow 180ms ease, opacity 180ms ease;
}
.button:hover, .button:focus-visible { transform: translateY(-2px); box-shadow: var(--shadow-soft); }
.button:active { transform: translateY(0); box-shadow: none; }
.button--primary { background: var(--accent); color: var(--color-ink); }
.button--secondary { border-color: var(--color-ink); background: transparent; color: var(--color-ink); }
.button--light { background: var(--color-white); color: var(--color-ink); }
.button--ghost { border-color: rgba(251, 250, 247, 0.9); background: transparent; color: var(--color-white); text-shadow: var(--halo-dark); }
.button--small { min-height: 42px; padding: 0.62rem 1.15rem; font-size: 0.85rem; }
.button[disabled] { opacity: 0.6; cursor: default; }
.button[disabled]:hover { transform: none; box-shadow: none; }

/* Small-phone CTA tier (matches the worlds-grid 1-col line at 479px).
   German CTA labels run to 26+ uppercase characters ("Babybauchshooting
   anfragen") and buttons never wrap (branding §3): at the base spec they
   outgrow a 320-419px shell and every card that holds them. One systemic
   tier instead of per-page tweaks: the CTA becomes a full-width block
   (thumb-first, label centered), type compacts slightly (13.6px still
   reads as the letterspaced Jost button), inline padding drops since the
   block edge now comes from the container, and the shell gutter eases to
   16px on the narrowest phones. Tap height stays 48px (small 42px).
   Worst case measured: "NEWBORN-SHOOTING ANFRAGEN" inside a price card at
   320px = 245px min-content vs 250px available. */
@media (max-width: 479px) {
  :root { --shell-gutter: clamp(16px, 4vw, 20px); }
  .button {
    width: 100%;
    padding-inline: 0.6rem;
    font-size: 0.8rem;
    letter-spacing: 0.04em;
  }
  /* A1 — equal-width stacked buttons: because every button becomes a full-width
     block in this tier, any stacked pair (hero actions, closer actions, any
     wrapped .actions-row) reads at one shared width automatically. Side-by-side
     pairs ABOVE this tier keep each label's intrinsic width, as intended.
     Do NOT switch .actions-row to flex-direction:column here: the hero stack is
     a justify-items:start grid, so a column row shrinks to its widest label and
     the full-width block is lost (verified regression 352->185px). */
}

.text-link {
  color: var(--accent-dark); text-decoration: underline;
  text-decoration-color: var(--accent-dark);
  text-underline-offset: 3px; text-decoration-thickness: 1px;
  transition: color 220ms cubic-bezier(0.22, 1, 0.36, 1), text-decoration-color 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.text-link:hover, .text-link:focus-visible { color: var(--color-ink); text-decoration-color: var(--color-ink); }

.actions-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--flow-xs) var(--flow-sm); }

/* Sibling CTAs that stack must match width (Per, round 2).
   Measured stacking thresholds, not guesses: the home hero pair needs
   210+256+26 = 492px to share a line, so it stacks below 560px viewport and
   used to do so at two different widths (210/256). The closer pair needs
   210+178+26 = 414px. Where a pair stacks, both buttons go full width.
   `flex: 0 0 auto` is required alongside `width`: the closer's pairing rule
   sets `flex-basis: 0`, which otherwise wins over width on the main axis and
   left those buttons shrunken (127/104 at 360px).
   NOTE: do NOT reach for `container-type: inline-size` here — these rows are
   shrink-to-fit inside `justify-items` grids, and the size containment it adds
   collapses them to ~21px. */
@media (max-width: 559px) {
  .actions-row { width: 100%; }
  .actions-row > .button { flex: 0 0 auto; width: 100%; }
}
@media (max-width: 479px) {
  .cta-closer__actions { width: 100%; }
  .cta-closer__actions > .button { flex: 0 0 auto; width: 100%; }
}

/* ---------- Reveal system (hero never reveal-hidden) ---------- */
.has-js .reveal {
  opacity: 0;
  transform: translate3d(0, 12px, 0);
  transition: opacity 560ms cubic-bezier(0.22, 1, 0.36, 1), transform 560ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}
.has-js .reveal.is-visible { opacity: 1; transform: translate3d(0, 0, 0); }
.has-js .reveal.reveal--instant { transition-delay: 0ms; transition-duration: 1ms; }
/* Hoverable reveal elements: the .reveal transition shorthand would otherwise
   permanently override their box-shadow hover ease (shadow would snap while the
   image zoom runs 240ms). Merge both so the reveal rise AND the hover shadow
   each keep their own timing; only the reveal pair carries the stagger delay. */
.has-js .world-tile.reveal {
  transition:
    opacity 560ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 560ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 240ms ease;
  transition-delay: var(--reveal-delay, 0ms), var(--reveal-delay, 0ms), 0ms;
}
.has-js .world-tile.reveal--instant {
  transition-delay: 0ms;
  transition-duration: 1ms, 1ms, 240ms;
}

/* Reveal failsafe: the inline has-js script cannot fail, but the un-hiding lives
   in deferred site.js. site.js disarms this by setting .js-ready on <html> as its
   first act; if its fetch is blocked or aborted, every armed reveal forces itself
   visible after ~3s — content can never stay blank. */
@keyframes reveal-failsafe {
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}
.has-js:not(.js-ready) .reveal { animation: reveal-failsafe 0.5s ease 3s forwards; }

/* ---------- Entrance choreography (ledger split: hero replays, chrome once) ----------
   Hero copy stacks play a calm rise on EVERY load, in the exact reveal voice
   (12px, 560ms, same curve, stagger capped at 180ms). The persistent chrome
   (header) drops in only ONCE per session: an inline head script sets
   html[data-entrance-chrome] on the first page of a session, so page-to-page
   navigation never re-runs the nav entrance. Keyframes fill backwards and end
   at the natural state: they always complete (nothing can stay hidden), and
   the header's own hide-on-scroll transform transition takes over afterwards.
   Without JS there is no .has-js class and nothing is ever hidden. */
@keyframes entrance-rise {
  from { opacity: 0; transform: translate3d(0, 12px, 0); }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes entrance-drop {
  from { opacity: 0; transform: translate3d(0, -8px, 0); }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}
.has-js .photo-hero__stack > *,
.has-js .page-hero__stack > *,
.has-js .error-page > * {
  animation: entrance-rise 560ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.has-js .photo-hero__stack > :nth-child(2),
.has-js .page-hero__stack > :nth-child(2),
.has-js .error-page > :nth-child(2) { animation-delay: 60ms; }
.has-js .photo-hero__stack > :nth-child(3),
.has-js .page-hero__stack > :nth-child(3),
.has-js .error-page > :nth-child(3) { animation-delay: 120ms; }
.has-js .photo-hero__stack > :nth-child(n+4),
.has-js .page-hero__stack > :nth-child(n+4),
.has-js .error-page > :nth-child(n+4) { animation-delay: 180ms; }
html[data-entrance-chrome] .site-header {
  animation: entrance-drop 480ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}

/* ---------- Header ---------- */
.site-header {
  position: sticky; top: 0; z-index: 60;
  background: var(--color-cream);
  border-bottom: 1px solid var(--color-border);
  transform: translate3d(0, 0, 0);
  will-change: transform;
  transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 220ms ease;
}
.site-header.site-header--hidden { transform: translateY(calc(-100% - 6px)); }
.site-header.is-scrolled { box-shadow: 0 10px 30px rgba(58, 63, 54, 0.07); }

.site-header__inner { display: flex; align-items: center; justify-content: space-between; gap: 1.4rem; min-height: var(--header-height); }

.site-brand { display: inline-flex; align-items: center; flex: 0 0 auto; transition: opacity 180ms ease, transform 180ms ease; }
.site-brand:hover, .site-brand:focus-visible { opacity: 0.82; transform: translateY(-1px); }
.site-brand img { width: 208px; max-width: 52vw; height: auto; }

.site-nav { display: flex; align-items: center; gap: 1.5rem; }
.site-nav__list { display: flex; align-items: center; gap: 1.4rem; list-style: none; }
.site-nav__item { position: relative; }

.site-nav__link {
  position: relative; display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.35rem 0; border: 0; background: transparent;
  color: var(--color-ink); font-family: var(--font-body); font-weight: 450; font-size: 0.98rem;
}
.site-nav__link::after {
  content: ""; position: absolute; left: 0; bottom: -0.28rem; width: 100%; height: 2px;
  border-radius: 999px; background: var(--accent-dark);
  transform: scaleX(0); transform-origin: center; transition: transform 180ms ease;
}
.site-nav__link:hover::after, .site-nav__link:focus-visible::after, .site-nav__link.is-current::after { transform: scaleX(1); }
.site-nav__link.is-current { font-weight: 550; }
.site-nav__caret { display: inline-block; width: 0.5em; height: 0.5em; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg) translateY(-2px); transition: transform 180ms ease; }
.site-nav__item.is-open .site-nav__caret { transform: rotate(225deg) translateY(-1px); }

.site-nav__panel {
  position: absolute; top: calc(100% + 0.9rem); left: 50%; transform: translateX(-50%) translateY(-6px);
  min-width: 220px; padding: 0.55rem;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-white); box-shadow: var(--shadow-card);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 180ms;
}
.site-nav__item.is-open .site-nav__panel {
  opacity: 1; visibility: visible; pointer-events: auto;
  transform: translateX(-50%) translateY(0);
  transition: opacity 180ms ease, transform 180ms ease;
}
.site-nav__panel a {
  display: block; padding: 0.6rem 0.85rem; border-radius: var(--radius-xs);
  color: var(--color-ink); font-size: 0.96rem;
  transition: background-color 150ms ease;
}
.site-nav__panel a:hover, .site-nav__panel a:focus-visible { background: var(--accent-soft); }
.site-nav__panel a.is-current { font-weight: 550; background: var(--surface-deep); }

.site-nav__cta { white-space: nowrap; }

/* Toggle (mobile) */
.site-header__toggle {
  display: none; position: relative;
  width: 48px; height: 48px; padding: 0;
  border: 1px solid var(--color-border); border-radius: var(--radius-xs);
  background: var(--color-white); color: var(--color-ink);
  -webkit-tap-highlight-color: transparent;
}
.site-header__toggle-line {
  display: block; position: absolute; top: 50%; left: 50%; width: 20px; height: 2px;
  border-radius: 999px; background: currentColor;
  transform: translate(-50%, -50%); transform-origin: center;
  transition: transform 180ms ease, opacity 180ms ease;
}
.site-header__toggle-line:nth-child(1) { transform: translate(-50%, -50%) translateY(-6px); }
.site-header__toggle-line:nth-child(3) { transform: translate(-50%, -50%) translateY(6px); }
.site-header.is-nav-open .site-header__toggle-line:nth-child(1) { transform: translate(-50%, -50%) rotate(45deg); }
.site-header.is-nav-open .site-header__toggle-line:nth-child(2) { opacity: 0; }
.site-header.is-nav-open .site-header__toggle-line:nth-child(3) { transform: translate(-50%, -50%) rotate(-45deg); }

@media (max-width: 1020px) {
  .site-header__toggle { display: inline-flex; }
  /* Drawer keeps display:flex and animates via the visibility pattern (same as
     the desktop subnav panel): a short drop+fade on open, a quicker fade on
     close. visibility:hidden keeps the closed drawer out of the tab order and
     the a11y tree exactly like display:none did. */
  .site-nav {
    position: absolute; top: calc(100% + 1px); left: var(--shell-gutter); right: var(--shell-gutter);
    z-index: 50; display: flex; flex-direction: column; align-items: stretch; gap: 0;
    max-height: calc(100svh - var(--header-height) - 24px); overflow-y: auto;
    padding: 0.9rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm);
    background: var(--color-white); box-shadow: var(--shadow-card);
    opacity: 0; visibility: hidden; pointer-events: none;
    transform: translateY(-8px);
    transition: opacity 170ms ease, transform 170ms cubic-bezier(0.22, 1, 0.36, 1), visibility 0s linear 170ms;
  }
  body[data-nav-open="true"] .site-nav {
    opacity: 1; visibility: visible; pointer-events: auto; transform: none;
    transition: opacity 220ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .site-nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .site-nav__item + .site-nav__item { border-top: 1px solid var(--color-border); }
  .site-nav__link { display: flex; width: 100%; justify-content: space-between; padding: 0.85rem 0.35rem; font-size: 1.02rem; }
  .site-nav__link::after { display: none; }
  .site-nav__panel {
    position: static; min-width: 0; transform: none; opacity: 1; visibility: hidden; pointer-events: auto;
    display: none; padding: 0 0 0.6rem; border: 0; border-radius: 0; background: transparent; box-shadow: none;
    transition: none;
  }
  .site-nav__item.is-open .site-nav__panel { display: block; visibility: visible; transform: none; }
  .site-nav__panel a { padding: 0.6rem 1rem; }
  .site-nav__cta { width: 100%; margin-top: 0.8rem; }
}

/* ============================================================
   Directional veil (art direction, wave 2)
   ------------------------------------------------------------
   ONE mechanism for every photo hero and photo closer: a gradient bed that
   sits UNDER the letter-halo and is scoped to the side the copy sits on.
   It is never a flat scrim across the photo (ledger: "protect hero text with
   a letter-halo, not a grey image scrim") — --veil-extent stops it well
   before the subject, so the photograph keeps full strength exactly where
   the eye should go. Per's homepage brief is the model: strong where the
   words are, almost nothing on the side that carries the picture.

   Configured per instance from the include (veil / veil_strength /
   veil_extent, plus _m variants for the mobile composition):
     --veil-dir       gradient direction, aimed AWAY from the copy
                      ("to left" = strongest at the right edge)
     --veil-strength  peak alpha at the copy edge (default 0 = no veil)
     --veil-extent    how far across the frame it reaches before hitting 0
   Polarity follows the component's own text colour, so it never needs a
   param: warm-white copy gets a near-ink veil (same core as --halo-dark),
   ink copy on bright imagery (.cta-closer--dark) gets a warm-white one.
   The falloff is a 4-stop ease so the veil never shows a hard gradient
   edge over open sky.
   ============================================================ */
.photo-hero, .cta-closer {
  --veil-dir-x: var(--veil-dir, to top);
  --veil-strength-x: var(--veil-strength, 0);
  --veil-extent-x: var(--veil-extent, 62%);
  --veil-rgb: 26, 29, 24;
  /* B1 full-image wash alpha (dark closers only); 0 = off. See the wash note
     below. Resolves per-viewport like the veil so a side-anchored dark closer
     can stay wash-free on desktop and pick a wash up only where it centres. */
  --closer-wash-x: var(--wash, 0);
}
/* The mobile composition is a different crop, so it carries its own tuning
   and falls back to the desktop values when a page does not override. */
@media (max-width: 1020px) {
  .photo-hero, .cta-closer {
    --veil-dir-x: var(--veil-dir-m, var(--veil-dir, to top));
    --veil-strength-x: var(--veil-strength-m, var(--veil-strength, 0));
    --veil-extent-x: var(--veil-extent-m, var(--veil-extent, 62%));
    --closer-wash-x: var(--wash-m, var(--wash, 0));
  }
}
.cta-closer--dark { --veil-rgb: 251, 250, 247; }
.photo-hero__media::after,
.cta-closer__media::after {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  /* Shape: a long plateau across the copy zone, then a soft tail. An even
     ramp looked fine at the headline and left the eyebrow — the smallest,
     highest element in the stack — sitting on bare photo (measured 1.2-1.5:1
     in the first pass). The tail still lands on 0 at --veil-extent, so the
     picture side is untouched. */
  background: linear-gradient(
    var(--veil-dir-x),
    rgba(var(--veil-rgb), var(--veil-strength-x)) 0%,
    rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.92)) calc(var(--veil-extent-x) * 0.34),
    rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.66)) calc(var(--veil-extent-x) * 0.62),
    rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.28)) calc(var(--veil-extent-x) * 0.83),
    rgba(var(--veil-rgb), 0) var(--veil-extent-x)
  );
}
/* Centre-weighted veil (desktop only). For a CENTRED stack over a photo that
   fills the frame edge to edge, a directional veil lightens one whole side for
   no reason and a flat --wash lightens the picture everywhere. This peaks
   behind the copy and falls to nothing toward BOTH edges, mirroring the 4-stop
   falloff above on each side; here --veil-extent is the half-width.
   Below 1020px the mobile crop is narrow enough that the flat --wash-m beds
   the copy on its own, so this stays off there. */
@media (min-width: 1021px) {
  .cta-closer--veil-center .cta-closer__media::after {
    background: linear-gradient(
      to right,
      rgba(var(--veil-rgb), 0) calc(50% - var(--veil-extent-x)),
      rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.28)) calc(50% - var(--veil-extent-x) * 0.83),
      rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.66)) calc(50% - var(--veil-extent-x) * 0.62),
      rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.92)) calc(50% - var(--veil-extent-x) * 0.34),
      rgba(var(--veil-rgb), var(--veil-strength-x)) 50%,
      rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.92)) calc(50% + var(--veil-extent-x) * 0.34),
      rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.66)) calc(50% + var(--veil-extent-x) * 0.62),
      rgba(var(--veil-rgb), calc(var(--veil-strength-x) * 0.28)) calc(50% + var(--veil-extent-x) * 0.83),
      rgba(var(--veil-rgb), 0) calc(50% + var(--veil-extent-x))
    );
  }
}

/* ---------- Photo hero (full-bleed) ---------- */
.photo-hero {
  position: relative;
  display: flex; align-items: flex-end;
  min-height: max(640px, calc(100svh - var(--header-height)));
  overflow: hidden;
  background: var(--color-cream-deep);
}
.photo-hero__media { position: absolute; inset: 0; }
.photo-hero__media picture { display: block; width: 100%; height: 100%; }
.photo-hero__media img { width: 100%; height: 100%; object-fit: cover; object-position: var(--hero-pos, 50% 38%); }
/* Overlay text on photos is Warmweiß, not Reinweiß: branding §1 keeps #FFFFFF
   for rare small highlights only, and the deck's light tone is #FBFAF7 (the
   ghost buttons and lightbox already use it). Legibility comes from --halo-dark. */
/* Bottom inset is deliberately deeper than the top one (wave 2, client note
   "a bit too low, too close to end"): the stack now sits ON the lower third
   rather than against the frame edge, which also keeps it clear of the
   subjects' faces on every source. */
.photo-hero__content {
  position: relative; z-index: 2;
  width: 100%;
  padding: clamp(48px, 8vh, 96px) 0 clamp(64px, 12vh, 148px);
  color: var(--color-white);
}
.photo-hero__stack { max-width: 560px; display: grid; justify-items: start; }
.photo-hero--right .photo-hero__stack { margin-left: auto; text-align: right; justify-items: end; }
.photo-hero__content .eyebrow { color: var(--color-white); text-shadow: var(--halo-dark); }
.photo-hero__title { color: var(--color-white); font-size: clamp(3rem, 7vw, 5.2rem); font-weight: 520; text-shadow: var(--halo-dark); }
.photo-hero__sub { max-width: 46ch; font-size: clamp(1.05rem, 1.4vw, 1.22rem); text-shadow: var(--halo-dark); }
.photo-hero--right .photo-hero__actions { justify-content: flex-end; }

/* ---- Mobile + tablet-portrait hero (wave 2, client-directed) ----
   The hero owns the FIRST VIEWPORT ALONE on every width now, not just on
   desktop: nav + image, nothing else. Below 1020px the art-directed 4:5 -m-
   source fills the frame and the copy overlays its lower third, made legible
   by the directional veil above rather than by dropping onto a cream block.
   This replaces the wave-1 stacked composition, which ended at ~758px in an
   844px viewport and let the next section peek into the first screen.
   The stacked composition survives as an explicit per-page opt-in
   (.photo-hero--stacked) for sources whose 4:5 crop is filled edge to edge by
   faces — see the hero table in DESIGN.md. */
@media (max-width: 1020px) {
  .photo-hero { min-height: max(560px, calc(100svh - var(--header-height))); }
  .photo-hero__media img { object-position: var(--hero-pos-m, var(--hero-pos, 50% 38%)); }
  /* One reading direction on narrow screens: a right-aligned ragged edge over
     a photo is hard to scan at 390px, so every mobile hero reads from the
     left, whatever the desktop side is. */
  .photo-hero__stack, .photo-hero--right .photo-hero__stack { margin-left: 0; text-align: left; justify-items: start; max-width: none; }
  .photo-hero--right .photo-hero__actions { justify-content: flex-start; }
  .photo-hero__title { font-size: clamp(2.6rem, 11vw, 3.4rem); }
  .photo-hero__content { padding-bottom: clamp(40px, 7vh, 76px); }
}

/* Short landscape phones (~740x360): a 560px floor would push the image past
   a ~296px stage, so the hero drops to exactly the available height and the
   stack compacts instead of scrolling. */
@media (max-width: 1020px) and (max-height: 520px) and (orientation: landscape) {
  .photo-hero { min-height: calc(100svh - var(--header-height)); }
  .photo-hero__content { padding-top: clamp(20px, 4vh, 40px); padding-bottom: clamp(20px, 5vh, 40px); }
  .photo-hero__title { font-size: clamp(1.9rem, 6.4vw, 2.5rem); }
  .photo-hero__sub { display: none; }
}

/* ---- Stacked hero (per-page opt-in, ≤1020px) ----
   For sources whose 4:5 mobile crop carries faces edge to edge, so no overlay
   zone exists that clears them (currently /newborn/ only). Photo on top, copy
   overlapping onto cream below. */
@media (max-width: 1020px) {
  .photo-hero--stacked { display: block; min-height: 0; }
  /* width: 100% stops aspect-ratio from transferring width off max-height when
     74svh caps the box (tablet portrait): the frame stays full-bleed and the
     portrait source crops via object-fit instead. */
  .photo-hero--stacked .photo-hero__media { position: relative; inset: auto; width: 100%; aspect-ratio: 4 / 5; max-height: 74svh; overflow: hidden; }
  .photo-hero--stacked .photo-hero__media img { position: absolute; inset: 0; }
  .photo-hero--stacked .photo-hero__content {
    margin-top: calc(-1 * clamp(120px, 30vw, 200px));
    padding: 0 0 clamp(26px, 6vw, 44px);
  }
  /* Gentle pull tier: sources whose subjects reach into the standard 200px
     overlap zone (newborn: the baby's face fills the lower crop) keep the
     signature overlap at eyebrow/title depth only — sub + CTAs land on cream. */
  .photo-hero--stacked.photo-hero--gentle .photo-hero__content { margin-top: calc(-1 * clamp(56px, 14vw, 96px)); }
  /* In the stacked hero the actions row lands on the photo's bright bottom
     edge, straddles the photo/cream boundary, or sits fully on cream
     (width-dependent): the overlay-ghost recipe (warm-white border/label +
     dark halo) reads muddy on the light grounds. Reuse the secondary
     treatment here: ink outline + ink label, no halo. The overlay heroes keep
     the true over-photo ghost at every width. */
  .photo-hero--stacked .button--ghost { border-color: var(--color-ink); color: var(--color-ink); text-shadow: none; }
  /* Position-aware stack (classes set by site.js): any element that fully
     clears the photo and sits on bare cream reads as an on-cream element:
     ink, no halo — the same ground rule as the ghost button above. */
  .photo-hero--stacked .photo-hero__content .eyebrow.is-on-cream { color: var(--accent-dark); text-shadow: none; }
  .photo-hero--stacked .photo-hero__title.is-on-cream,
  .photo-hero--stacked .photo-hero__sub.is-on-cream { color: var(--color-ink); text-shadow: none; }
  /* The stacked hero still overlaps the photo's bottom strip with the eyebrow
     and title, so it keeps the veil — just a short one (small --veil-extent-m),
     anchored to that strip. The subjects above it stay completely clean. */
}

/* Short landscape phones (~740x360): the stacked hero's 4/5 media caps at 74svh
   (~266px) while the standard pull-up is 200px, so the text block lands on the
   subjects' faces. A gentle overlap keeps the signature layering without
   covering the photo. */
@media (max-width: 1020px) and (max-height: 480px) {
  .photo-hero--stacked .photo-hero__content { margin-top: calc(-1 * clamp(72px, 24vh, 120px)); }
}

/* Utility hero (no photo) */
.page-hero { padding: clamp(56px, 8vw, 96px) 0 clamp(30px, 4.5vw, 52px); background: var(--color-cream); }
.page-hero__stack { max-width: 760px; display: grid; }
.page-hero__sub { max-width: 58ch; font-size: clamp(1.02rem, 1.3vw, 1.18rem); }

/* ============================================================
   Trust band (full-bleed utility rail directly under the hero)
   ------------------------------------------------------------
   Replaces the old .trust-strip card, which pulled itself up over the hero's
   bottom edge (negative margin) and so put a boxed Warmweiss card inside the
   first viewport. The hero now owns the first screen alone (nav + image), and
   the band reads as the deliberate base rail under it: edge to edge, no side
   borders, no radius, no shadow, hairline top and bottom, sitting flush on the
   hero above (tight, they belong together) with the next section's full
   --section-gap below (double-padding trap).
   Content aligns to the shell so the outer points line up with the logo and
   the nav CTA. Below 1280px the four points become a marquee, before any label
   would need to wrap.
   ============================================================ */
.trust-band {
  position: relative; z-index: 3;
  background: var(--color-white);
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}
.trust-band__viewport { overflow: hidden; }
.trust-band__marquee { display: block; }
.trust-band__track {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  width: min(var(--shell-width), calc(100vw - (2 * var(--shell-gutter))));
  margin: 0 auto; padding: 0; list-style: none;
}
/* the clone only exists for the seamless mobile loop */
.trust-band__track--clone { display: none; }
/* Trust points are always a single line. The component changes to its marquee
   layout before the four-column rail becomes too narrow. */
.trust-band__item {
  display: flex; align-items: center; align-self: center;
  justify-content: center; text-align: center;
  gap: 0.45rem;
  padding: var(--flow-sm) clamp(8px, 1.2vw, 14px);
  font-size: 0.92rem; line-height: 1.45; color: var(--color-ink);
  white-space: nowrap;
}
.trust-band__item + .trust-band__item { border-left: 1px solid var(--color-border); }
/* tick styling/alignment is the shared check-marker rule (see .inline-icon--check) */

/* ---- Marquee (<1280px, before four single-line points stop fitting) ---- */
@media (max-width: 1279px) {
  .trust-band__marquee {
    display: flex; width: max-content;
    animation: trust-band-scroll 29.6s linear infinite;
  }
  .trust-band:hover .trust-band__marquee,
  .trust-band:focus-within .trust-band__marquee { animation-play-state: paused; }
  .trust-band__track {
    display: flex; flex: 0 0 auto; width: auto; margin: 0;
    grid-template-columns: none;
  }
  .trust-band__track--clone { display: flex; }
  /* Duplicated DOM breaks :first-child/:last-child edge rules (ledger), so the
     divider is re-asserted uniformly on EVERY item: the loop seam between the
     visible set and its clone then carries the same hairline as every other
     join, and no item depends on its structural position. */
  .trust-band__item,
  .trust-band__item + .trust-band__item {
    border-left: 0;
    border-right: 1px solid var(--color-border);
  }
  .trust-band__item { padding-inline: clamp(16px, 5vw, 26px); }
}
/* two identical tracks: -50% of the wrapper is exactly one track width */
@keyframes trust-band-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
/* Reduced motion: no automatic movement. The clone drops out and the real set
   remains a single-line rail that can be scrolled horizontally. */
@media (max-width: 1279px) and (prefers-reduced-motion: reduce) {
  .trust-band__viewport {
    overflow-x: auto;
    scrollbar-width: none;
  }
  .trust-band__viewport::-webkit-scrollbar { display: none; }
  .trust-band__marquee { display: block; width: max-content; animation: none; }
  .trust-band__track {
    display: flex; width: max-content; margin: 0;
  }
  .trust-band__track--clone { display: none; }
  .trust-band__item,
  .trust-band__item + .trust-band__item {
    border-left: 0;
    border-right: 1px solid var(--color-border);
  }
}

/* ---------- Worlds tile grid ---------- */
.worlds-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap-grid); }
.world-tile {
  position: relative; display: flex; flex-direction: column;
  border-radius: 0; overflow: hidden;
  background: var(--color-white);
  border: 1px solid var(--color-border);
  transition: box-shadow 240ms ease;
}
.world-tile:hover, .world-tile:focus-visible { box-shadow: var(--shadow-card); }
.world-tile__media { overflow: hidden; aspect-ratio: 3 / 4; }
.world-tile__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 240ms ease; }
.world-tile:hover .world-tile__media img, .world-tile:focus-visible .world-tile__media img { transform: scale(1.035); }
/* flex: 1 so the label fills the tile's height when the grid stretches rows to
   equal height — otherwise a shorter (one-line-whisper) label leaves the tile's
   own background showing below it, which made the rose Hochzeiten fill only part
   of the height (Per). align-content centres the copy in that filled block so
   every tile reads the same. */
.world-tile__label { display: grid; gap: 0.15rem; padding: 1rem 1.2rem 1.15rem; background: var(--color-white); flex: 1 0 auto; align-content: center; }
.world-tile__whisper { font-family: var(--font-display); font-style: italic; font-size: 1.06rem; color: var(--color-ink-muted); }
.world-tile__name { font-family: var(--font-body); font-weight: 550; font-size: 0.95rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-ink); }
.world-tile--rose .world-tile__label { background: var(--rose-surface-deep); }
.world-tile--rose .world-tile__name { color: var(--rose-accent-dark); }
@media (max-width: 1020px) { .worlds-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Phones keep the 2-col scan (review prototype): six full-width 3:4 tiles were a
   ~3400px single-column stretch at 390; at ~172px tiles every whisper line wraps
   at a natural phrase boundary and the grid reads in two thumb-lengths. */
@media (max-width: 700px) { .worlds-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } }

/* Cross-link band variant (2-3 tiles) */
.worlds-grid--cross { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 780px; }
.worlds-grid--cross .world-tile__media { aspect-ratio: 4 / 3; }
@media (max-width: 640px) { .worlds-grid--cross { grid-template-columns: 1fr; } }

/* ---------- Split rows ---------- */
/* Equal-height rule (Per, global): the media column matches the text column's
   height. Photos cover-fill the matched height (straight edges); the voucher
   CARD is a designed graphic with text + URLs that must not be cropped, so it
   uses --card (contained, centred in the matched-height column). */
.split { display: grid; grid-template-columns: minmax(0, 45fr) minmax(0, 55fr); gap: var(--gap-split); align-items: stretch; }
.split__media { overflow: hidden; }
/* Photo variant (not the voucher card): cover-fills the matched content height
   and lifts off the surface like the KW teaser, straight-edged. */
.split__media:not(.split__media--card) { box-shadow: var(--shadow-card); }
.split__media img { width: 100%; height: 100%; object-fit: cover; }
.split__media--card { display: flex; align-items: center; justify-content: center; overflow: visible; }
.split__media--card img { height: auto; max-height: 100%; object-fit: contain; }
/* Media beside copy is CSS-forced to the COPY's height (Per, round 2): the copy
   column sets the row height and the photo adapts to it. The image is taken out
   of flow so its intrinsic aspect can never make the row taller than the copy
   block — that was the defect where the photo visibly overran the text. Only
   side-by-side widths; stacked mobile keeps the natural image height below.
   The wedding page's media rows are exempt (Per allows taller images there). */
@media (min-width: 861px) {
  .split__media, .about-panel__media { position: relative; }
  .split__media > img,
  .about-panel__media > img,
  /* The voucher fills its box like every other media (contain left empty
     stripes above and below it). Its 2:1 artwork and the copy-driven box are
     close in aspect, so cover trims only a few percent of the outer frame and
     the centred lettering stays whole. */
  .split__media--card > img { object-fit: cover; }
}

.split__body { display: grid; justify-items: start; align-content: center; }
.split__body > p { max-width: 56ch; }
/* Side by side, the copy fills its column so the image-to-text distance is the
   grid gap in BOTH orders. With a ch cap the flipped copy
   stopped short and left a much wider void before the image. The column tops
   out at ~66ch, still inside the comfortable measure. Stacked mobile keeps its
   caps for readability. */
@media (min-width: 861px) {
  .split__body > p { max-width: none; }
}
/* library variant, unused on current pages (kept per optimize ledger) */
/* Mirrored variant: copy left, media right. */
.split--flip { grid-template-columns: minmax(0, 55fr) minmax(0, 45fr); }
.split--flip .split__media { order: 2; }
.split--flip .split__body { order: 1; }
@media (max-width: 860px) {
  .split, .split--flip { grid-template-columns: 1fr; }
  .split--flip .split__media { order: 0; }
  .split--flip .split__body { order: 1; }
  /* Stacked: no text column to match, so media returns to its natural height. */
  .split__media img { height: auto; object-fit: contain; margin-inline: auto; }
  .split__media--card { padding: 0; }
  /* Opt-in square crop on mobile (Per, about-page story): the tall portrait
     reads too big stacked, so this variant fixes a 1:1 box and cover-fills it
     (object-position on the img keeps her face). */
  .split__media--square-m { aspect-ratio: 1 / 1; }
  .split__media--square-m img { width: 100%; height: 100%; object-fit: cover; margin-inline: 0; }
}

/* ---------- About band (home "Über mich") ----------
   Was a lone Warmweiss card with a floating script word (client: "reads super
   lieblos, just a white box"). Now a composed portrait-beside-text band on the
   sage surface: the photo column matches the text column's height (Per's global
   equal-height rule), straight edges, and "von Herzen" reads as a signed close
   under the paragraph instead of a floating box. Portrait-swap-ready — the media
   is one <img>, swap the source when a portrait of Bianca exists. */
.about-panel {
  display: grid;
  grid-template-columns: minmax(0, 42fr) minmax(0, 58fr);
  gap: var(--gap-split);
  align-items: stretch;
  max-width: 1040px; margin-inline: auto;
}
.about-panel__media { overflow: hidden; background: var(--color-cream-deep); }
.about-panel__media img { width: 100%; height: 100%; object-fit: cover; object-position: var(--about-pos, 50% 40%); }
/* justify-items: start — without it the grid stretches its items and the
   "Mehr über mich" button ran the full column width instead of sitting at its
   natural button width. */
.about-panel__body { align-content: center; justify-items: start; }
.about-panel__body > p { max-width: 52ch; }
/* Signed close: "von Herzen," in the plain Jost body voice, the name below it
   on its own line in the Great Vibes script (branding §2: Great Vibes = single
   word, here "Bianca" — the one script word on the home page). */
/* Signed close, fully in Great Vibes (Per, round 2 — the one sanctioned
   multi-word use of the script face; branding §2 otherwise limits it to single
   words, so do not "correct" it back). "von Herzen," sits smaller above the
   name, and the name is tilted like a real handwritten signature. */
.about-panel__sign { margin-top: var(--flow-md); }
.about-panel__sign-phrase { display: block; font-size: 0.7em; line-height: 1.1; }
.about-panel__sign-name {
  display: inline-block;
  /* Offset so the name reads as one flowing hand, not a second stacked line
     (Per, round 3 — "shouldn't sit right below, more to the side and up").
     margin-left carries it off the tail of "von Herzen,"; the small margin-top
     lifts it so the two feel connected. Both in em, so they track the script
     clamp and hold on every screen (verified desktop/tablet/mobile). With the
     left-origin tilt below, the indent makes the name rise to the right — the
     natural upstroke of a signature. */
  margin-top: 0.55em;
  margin-left: 1.8em;
  transform: rotate(-13deg);
  transform-origin: left center;
}
@media (max-width: 860px) {
  .about-panel { grid-template-columns: 1fr; gap: var(--flow-md); }
  /* Stacked: the portrait becomes a warm banner rather than a full-height
     column, so it never dominates the phone screen. */
  .about-panel__media { aspect-ratio: 3 / 2; }
}

/* ---------- Process steps (editorial numeral columns) ----------
   One design reused everywhere (newborn timing, kita, hochzeiten Ablauf,
   ueber-mich, gutscheine, kontakt). Round 2 F1, client-directed: the earlier
   connected-rail "timeline" read as off — "doesn't work at all", "feels off
   compared to the amazing feeling of the rest of the site". Rebuilt on evidence
   (3 sandbox candidates rendered + judged at 3-up/4-up/mobile): each step is now
   an open editorial column — a short Salbei accent rule tops the column (a
   per-item masthead mark, NOT a connecting timeline), then a size-emphasised
   serif numeral (no box, ledger), a serif title (matches .price-card__name /
   section subheads), then the text. Airy and calm like the about band; the
   accent bar is the scarce, deliberate pop (swaps to Rosé under .theme-rose
   automatically). Explicit symmetric grid for the known item count (ledger);
   equal-height siblings via align-items: stretch. No markup change — the rule
   is a ::before, so the six pages keep their exact step markup + copy. */
.steps {
  --steps-gap: clamp(28px, 3.2vw, 48px);
  display: grid; grid-template-columns: repeat(var(--steps-count, 3), minmax(0, 1fr));
  gap: var(--steps-gap); align-items: stretch;
}
.step {
  display: flex; flex-direction: column; position: relative;
  padding-top: calc(var(--flow-sm) + 4px);   /* clears the top accent rule */
}
.step::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 40px; height: 2px; background: var(--accent);
}
.step__num {
  font-family: var(--font-display); font-weight: 550;
  font-size: clamp(2.4rem, 3.1vw, 3rem); line-height: 0.9;
  color: var(--accent-dark); font-variant-numeric: lining-nums;
  margin: 0 0 var(--flow-xs);
}
.step__title {
  font-family: var(--font-display); font-weight: 600;
  font-size: 1.4rem; line-height: 1.2; margin: 0 0 var(--flow-2xs);
}
.step__text { font-size: 0.99rem; line-height: 1.55; color: var(--color-ink); max-width: 36ch; margin: 0; }
/* Four-up (hochzeiten Ablauf) gets cramped on tablets before the 1-col line:
   drop it to a symmetric 2x2 (mirrors the four wedding price cards). */
@media (max-width: 1080px) and (min-width: 861px) {
  .steps[style*="--steps-count: 4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 860px) {
  .steps, .steps[style] { grid-template-columns: 1fr !important; gap: var(--flow-md); }
  .step__text { max-width: 56ch; }
}

/* ---------- Testimonials ---------- */
.quotes-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap-grid); align-items: stretch; }
.quote-card {
  display: flex; flex-direction: column; gap: 1.1rem;
  padding: var(--flow-md);
  background: var(--color-cream); border: 1px solid var(--color-border); border-radius: var(--radius-sm);
}
.section--cream .quote-card, .section--sage .quote-card { background: var(--color-white); }
.quote-card__text { font-family: var(--font-display); font-style: italic; font-size: clamp(1.18rem, 1.6vw, 1.38rem); line-height: 1.42; flex: 1 0 auto; }
.quote-card__name { font-size: 0.9rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-ink-muted); }
@media (max-width: 860px) { .quotes-grid { grid-template-columns: 1fr; } .quote-card__text { flex: 0 0 auto; } }

/* ---------- Review slider (testimonials, reused on / and /ueber-mich/) ----------
   Client: the static quote grid read "loveless"; this is a real slider. One
   quote in focus, crossfading in ONE stacked grid cell (never a sliding track —
   ledger), a decorative quote mark, and five filled stars that are a warmth cue
   with a text alternative (NOT an aggregate rating — there is no review count,
   so no numeric badge). Prev/next + pagination dots that emphasise in place, an
   aria-live counter for screen readers, no autoplay. Because every slide shares
   the one cell, the viewport is naturally as tall as the tallest quote — no jump
   and no JS height measuring. Inactive slides fade out then go visibility:hidden
   (out of the a11y tree and tab order); no-JS shows the first quote and the
   controls stay harmless. */
/* minmax(0,1fr) on both grids: the single implicit `auto` column otherwise
   grows to the cards' min-content (a long German word) and blows the slider
   past the shell at 320px. */
/* Narrower + shorter box (Per: too tall/empty). All three cards stack in the
   one viewport cell (grid-area 1/1) and reserve layout while hidden, so the
   viewport is always as tall as the LONGEST quote — the height never changes as
   quotes crossfade. Card interior is a centred flex column so the quote-mark can
   be lifted out of flow into the corner. */
.review-slider { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--flow-sm); max-width: 600px; margin-inline: auto; }
.review-slider__viewport { display: grid; grid-template-columns: minmax(0, 1fr); }
.review-card {
  grid-area: 1 / 1; min-width: 0; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  padding: var(--flow-md);
  background: var(--color-white); border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  opacity: 0; visibility: hidden;
  transition: opacity 460ms cubic-bezier(0.22, 1, 0.36, 1), visibility 0s linear 460ms;
}
.review-card.is-active {
  opacity: 1; visibility: visible;
  transition: opacity 460ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Decorative open-quote lifted to the top-left corner, near the edge, a touch
   bigger. Absolutely positioned so it adds no row to the card's locked height. */
.review-card__mark {
  position: absolute; top: var(--flow-2xs); left: var(--flow-sm);
  font-family: var(--font-display); font-weight: 600; font-size: 4.4rem; line-height: 1;
  color: var(--accent-soft); pointer-events: none;
}
/* On narrow cards the centred quote reaches the top-left flourish and the two
   collide (Per). Drop the mark into the flow as a centred lead-in above the
   quote instead of overlapping it. */
@media (max-width: 620px) {
  .review-card__mark { position: static; font-size: 3.2rem; line-height: 0.5; margin-bottom: 0.2rem; }
}
.review-card__quote {
  margin: 0; max-width: 34ch; min-width: 0;
  font-family: var(--font-display); font-style: italic; font-size: clamp(1.32rem, 2vw, 1.72rem); line-height: 1.4;
  color: var(--color-ink); text-wrap: balance; overflow-wrap: break-word;
}
/* Stars sit directly above the name: small gap to the name (one attribution
   unit), a larger gap up to the quote. Sage stars, text alternative on the span. */
.review-card__stars { display: inline-flex; gap: 0.22rem; margin-top: var(--flow-sm); color: var(--accent-dark); font-size: 1rem; letter-spacing: 0.06em; }
.review-card__cite { margin-top: var(--flow-2xs); font-style: normal; font-size: 0.9rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-ink-muted); }

.review-slider__controls { display: flex; align-items: center; justify-content: center; gap: var(--flow-sm); }
.review-slider__arrow {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 44px; min-width: 44px; max-width: 44px;
  height: 44px; min-height: 44px; max-height: 44px;
  padding: 0; line-height: 0;
  border: 1px solid var(--color-border-strong); border-radius: 999px;
  background: var(--color-white); color: var(--color-ink);
  -webkit-appearance: none; appearance: none;
  transition: transform 180ms ease, box-shadow 240ms ease, border-color 180ms ease;
}
.review-slider__arrow:hover, .review-slider__arrow:focus-visible { transform: translateY(-2px); box-shadow: var(--shadow-soft); border-color: var(--accent-dark); }
.review-slider__arrow:active { transform: translateY(0); box-shadow: none; }
.review-slider__arrow svg {
  display: block; flex: 0 0 18px;
  width: 18px; min-width: 18px;
  height: 18px; min-height: 18px;
  overflow: visible;
}
/* Dots: a 44px tap target with a small centred dot; the active dot emphasises in
   place (colour + scale around its own centre), never leaving the row (ledger). */
.review-slider__dots { display: flex; align-items: center; gap: 2px; }
.review-slider__dot {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 30px; height: 44px; min-height: 44px; padding: 0;
  border: 0; background: transparent; -webkit-tap-highlight-color: transparent;
  -webkit-appearance: none; appearance: none;
}
.review-slider__dot::before {
  content: ""; width: 8px; height: 8px; border-radius: 999px;
  background: var(--color-border-strong);
  transition: background-color 200ms ease, transform 200ms ease;
}
.review-slider__dot[aria-current="true"]::before { background: var(--accent-dark); transform: scale(1.5); }
.review-slider__dot:hover::before, .review-slider__dot:focus-visible::before { background: var(--accent-dark); }

.quote-single { max-width: 780px; margin-inline: auto; text-align: center; display: grid; gap: 1.2rem; }
.quote-single__text { font-family: var(--font-display); font-style: italic; font-size: clamp(1.5rem, 2.6vw, 2.1rem); line-height: 1.4; }
.quote-single__name { font-size: 0.9rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-ink-muted); }

/* Statement band */
.statement { max-width: 900px; margin-inline: auto; text-align: center; }
.statement__text { font-family: var(--font-display); font-style: italic; font-weight: 500; font-size: clamp(1.7rem, 3.2vw, 2.6rem); line-height: 1.35; }
.statement__signature { margin-top: var(--flow-xs); font-family: var(--font-display); font-style: italic; font-size: 1.3rem; color: var(--accent-dark); }

/* ---------- Gallery (justified rows: flush top AND bottom, no empty cells) ----
   Client-directed (wave 4): the gallery must ALWAYS start and end on one clean
   line (flush top AND bottom) while tiles still vary in size. This is a
   Flickr-style JUSTIFIED-ROWS layout: site.js groups the visible tiles into rows
   that each fill 100% of the width, and gives every tile in a row one shared
   height (width = rowHeight x aspect-ratio). Because every row is a full-width
   flex line, there is NO empty-cell failure mode at any tile count or breakpoint
   (the old uniform grid + grid-auto-flow: dense could leave interior holes and
   an empty column when a 2-span tile landed off the left edge). Tiles keep their
   TRUE aspect ratio, so object-fit: cover does not crop and faces are never cut,
   and both width AND height vary row to row for genuine size variety. The block
   is flush at top (first row) and bottom (last row) by construction, not tuning.
   --jl-target is the per-breakpoint target row height JS aims for; below 560px it
   goes huge so every tile is its own full-width row (true aspect, still varied by
   height). object-fit: cover per tile; the lightbox still shows every photo. */
.masonry {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: var(--gap-tight);
  --jl-target: 440px;
}
.masonry__item {
  margin: 0; min-width: 0; overflow: hidden;
  /* No-JS / pre-layout fallback: a fixed-height flex row that grows each tile by
     its aspect ratio, so it is already flush and gap-free before site.js refines
     it to true-aspect justified rows. JS then sets an explicit width + height. */
  height: var(--gallery-row, clamp(300px, 32vw, 468px));
  flex: var(--ar, 1) 1 calc(var(--ar, 1) * var(--gallery-row, clamp(300px, 32vw, 468px)));
}
/* .masonry__item--wide is now layout-neutral: justified rows size every tile by
   its real aspect ratio, so landscape photos become wide on their own. The class
   and the CMS `weit` flag are kept so existing data/markup stay valid. */
.masonry__link { display: block; position: relative; overflow: hidden; height: 100%; width: 100%; }
.masonry__link img {
  width: 100%; height: 100%; object-fit: cover; object-position: 50% 40%;
  transition: transform 240ms ease, opacity 240ms ease;
}
.masonry__link:hover img, .masonry__link:focus-visible img { transform: scale(1.02); opacity: 0.94; }
.has-js .masonry__item.is-hidden { display: none; }
/* Load-more entrance: newly shown items rise in with the reveal voice, stagger
   capped at 8 steps (~360ms). display:none -> block restarts the animation. */
@keyframes masonry-in {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}
.has-js .masonry__item.is-entering {
  animation: masonry-in 420ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
  animation-delay: calc(var(--enter-i, 0) * 45ms);
}
.gallery-more { display: flex; justify-content: center; margin-top: var(--flow-md); }
html.no-js .gallery-more { display: none; }
@media (max-width: 1020px) {
  .masonry { --jl-target: 360px; }
  .masonry__item { --gallery-row: clamp(300px, 42vw, 430px); }
}
@media (max-width: 560px) {
  /* One tile per row, full width, at its true aspect ratio (height still varies
     tile to tile). Works with and without JS; JS overrides with explicit px. */
  .masonry { --jl-target: 100000px; }
  .masonry__item { flex-basis: 100%; height: auto; aspect-ratio: var(--ar, 0.75); }
}

/* Lightbox */
/* Open/close rides the visibility pattern so the dark ground can fade calmly
   (240ms in, 180ms out); prev/next image swaps stay instant on purpose so
   arrow-key browsing never flickers. */
.lightbox {
  position: fixed; inset: 0; z-index: 120;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(12px, 3vw, 40px);
  background: rgba(38, 41, 36, 0.92);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 180ms ease, visibility 0s linear 180ms;
}
.lightbox.is-open {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity 240ms ease;
}
.lightbox__figure { position: relative; max-width: min(1500px, 94vw); max-height: 88svh; margin: 0; }
.lightbox__figure img { max-width: 100%; max-height: 82svh; width: auto; height: auto; margin-inline: auto; box-shadow: var(--shadow-deep); }
.lightbox__caption { margin-top: 0.7rem; text-align: center; color: rgba(251, 250, 247, 0.85); font-size: 0.92rem; }
.lightbox__btn {
  position: absolute; z-index: 2;
  display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; border: 1px solid rgba(251, 250, 247, 0.4); border-radius: 999px;
  background: rgba(38, 41, 36, 0.55); color: var(--color-white);
  font-size: 1.15rem; line-height: 1;
  transition: transform 160ms ease, background-color 160ms ease;
}
.lightbox__btn:hover, .lightbox__btn:focus-visible { background: rgba(38, 41, 36, 0.8); }
.lightbox__btn--close { position: fixed; top: max(10px, env(safe-area-inset-top)); right: 10px; }
.lightbox__btn--prev { position: fixed; left: 8px; top: 50%; transform: translateY(-50%); }
.lightbox__btn--next { position: fixed; right: 8px; top: 50%; transform: translateY(-50%); }
/* Focus rings on dark photo grounds: the global Salbei-Dunkel ring sinks into
   the lightbox's near-ink backdrop (~2.3:1) and the light-variant closer
   imagery. Warm-white keeps the same 3px geometry and stays on palette.
   Bright-imagery closers (--dark, ink text) keep an ink ring instead: a light
   ring would sink into their pale grass/petal grounds. */
.lightbox :focus-visible,
.cta-closer :focus-visible { outline-color: var(--color-white); }
.cta-closer--dark :focus-visible { outline-color: var(--color-ink); }

/* ---------- Price cards ---------- */
.price-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: var(--gap-grid); align-items: stretch; max-width: 980px; }
/* Four known cards: deliberate symmetric 2x2, never a lopsided 3+1 wrap */
.price-grid--wide { max-width: none; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 700px) { .price-grid--wide { grid-template-columns: 1fr; } }
/* A single package no longer stretches to the full 980px grid: lone cards match
   the visual width of a card in the two-card bands and sit on the left rail. */
.price-grid > .price-card:only-child { max-width: 620px; }
.price-card {
  display: flex; flex-direction: column; gap: 1rem;
  padding: var(--flow-md);
  background: var(--color-white); border: 1px solid var(--color-border); border-radius: var(--radius-sm);
}
.price-card__name { font-family: var(--font-display); font-weight: 550; font-size: 1.45rem; line-height: 1.2; }
.price-card__price { font-family: var(--font-display); font-weight: 550; font-size: clamp(2rem, 3vw, 2.5rem); line-height: 1; }
.price-card__meta { font-size: 0.9rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-ink-muted); }
.price-card__description { margin: 0.4rem 0 0; flex: 1 0 auto; font-size: 0.99rem; line-height: 1.55; }
.price-card__list { display: grid; gap: 0.55rem; list-style: none; margin: 0.4rem 0 0; padding: 0; flex: 1 0 auto; }
.price-card__list li { display: flex; gap: 0.65rem; align-items: flex-start; line-height: 1.5; font-size: 0.99rem; }
.price-card__note { font-size: 0.92rem; color: var(--color-ink-muted); }
.price-card__cta { margin-top: 0.6rem; }
.voucher-purchase { grid-template-columns: minmax(0, 620px); justify-content: center; max-width: none; }
.price-inklusiv { margin-top: var(--flow-md); }
.price-note { margin-top: var(--flow-sm); font-size: 0.98rem; color: var(--color-ink); }
/* Small-phone CTA tier: the card gives up inline padding (block padding stays)
   so the full-width CTA's long label clears 320px. See the .button tier note. */
@media (max-width: 479px) { .price-card { padding-inline: var(--flow-sm); } }

.inline-icon { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; line-height: 1; }
.inline-icon--check { width: 1rem; font-size: 0.88rem; font-weight: 600; }
.inline-icon svg { display: block; }

/* Shared check-marker treatment (A6 alignment + A7 one style sitewide): an
   inline SVG replaces the old font glyph, whose negative side bearing could be
   clipped by real iOS Safari. The slightly wider box gives the stroke explicit
   clearance on both sides and centres it against the FIRST text line. No
   per-context variants, no disc. */
.trust-band__item .inline-icon--check,
.price-card__list .inline-icon--check,
.check-list .inline-icon--check {
  color: var(--accent-dark);
  align-self: flex-start;
  font-size: inherit; line-height: 1;
  width: 1.15em; min-width: 1.15em;
  height: 1.5em;
  overflow: visible;
}
.inline-icon--check svg {
  width: 0.92em;
  height: 0.92em;
  overflow: visible;
}

.check-list { display: grid; gap: 0.7rem; list-style: none; padding: 0; }
.check-list li { display: flex; gap: 0.7rem; align-items: flex-start; line-height: 1.55; }
.check-list--two-col { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.8rem 2.2rem; }
@media (max-width: 760px) { .check-list--two-col { grid-template-columns: 1fr; } }

/* ---------- Composed check panel (kita USP, "in jedem Shooting/Paket enthalten") ----------
   Client: the bare two-column list read "lost and unintentional" / "randomly
   added". Framing it in a Warmweiss panel with generous spacing makes it a
   deliberate block. One treatment reused everywhere. */
.check-panel {
  display: grid; gap: var(--flow-sm);
  padding: var(--flow-md);
  background: var(--color-white); border: 1px solid var(--color-border); border-radius: var(--radius-sm);
}
.check-panel__head { display: grid; gap: var(--flow-2xs); }
.check-panel__title { font-family: var(--font-display); font-weight: 550; font-size: 1.4rem; }
.check-panel .check-list--two-col { gap: 0.95rem 2.6rem; }
.check-panel .check-list li { gap: 0.8rem; font-size: 0.99rem; }
/* The panel's ticks use the shared plain check-marker (A7: one checkmark style
   everywhere) — no circle disc. The panel reads as a deliberate block from its
   frame and spacing, not from a different tick treatment. */

/* G1 — the wedding "In jedem Paket enthalten" inclusions must read as
   ADDITIONAL INFO, not another package to buy. So it steps DOWN from the
   Warmweiss price cards it follows: a recessed Creme-Tief tint (deeper than the
   cream section, where the lifted cards are lighter than it) reads as an inset
   note rather than a card, and a "Gut zu wissen" info eyebrow names it as
   context. Subtle on purpose — the tint + eyebrow do it, nothing louder.
   (Applies wherever show_inklusiv renders: /preise/ and /hochzeiten/; under
   .theme-rose the eyebrow follows the wedding accent automatically.) */
.check-panel--info { background: var(--color-cream-deep); }
/* Lighter Creme, only for the /preise/ "Immer dabei" box — the Creme-Tief read
   too reddish there on the sage band (Per). The hochzeit inclusions box keeps
   the deeper tone. Placed after --info so it wins at equal specificity. */
.check-panel--cream { background: var(--color-cream); }

/* ---------- Preise filter (G2) ----------
   Ported from the KSWD portfolio filter (details/summary drawer, per-dimension
   selects, reset, empty-state, mobile collapse, a11y), reskinned to the Salbei
   palette and this site's flat editorial field style (4px radius, the same
   fields the contact form uses — not KSWD's pills). The chevron reuses the nav
   caret's CSS-drawn glyph approach. JS (site.js) shows/hides whole category
   sections, re-derives a clean warm/cream alternation among the visible set so
   hiding never leaves two same-surface bands adjacent, and drives reset/empty.
   No-JS: the details is inert, nothing is hidden, every category stays visible. */
/* Filter band: its own band under the full-bleed photo hero (client-directed),
   so it needs real air above it — the old flow-sm top belonged to the cream
   .page-hero it used to hug. */
.price-filter-band { padding-block: var(--flow-md) var(--flow-md); }
.price-filters { display: grid; gap: var(--flow-sm); }
.price-filters__summary { display: none; }
.price-filters__summary::-webkit-details-marker { display: none; }
.price-filters__body { display: grid; gap: var(--flow-sm); }
.price-filters__toolbar { display: flex; align-items: center; justify-content: space-between; gap: 0.9rem; }
.price-filters__label {
  margin: 0; font-family: var(--font-body); font-weight: 500;
  font-size: 0.82rem; line-height: 1.2; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--accent-dark);
}
.price-filters__controls {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--flow-xs); max-width: 620px;
}
.price-filter { position: relative; display: block; min-width: 0; }
.price-filter__field {
  width: 100%; min-width: 0; min-height: 48px;
  padding: 0.8rem 2.6rem 0.8rem 0.95rem;
  border: 1px solid var(--color-border-strong); border-radius: var(--radius-xs);
  background: var(--color-white); color: var(--color-ink);
  font: inherit; -webkit-appearance: none; appearance: none; cursor: pointer;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.price-filter__field:hover { border-color: var(--accent-dark); }
.price-filter__field:focus-visible { outline: none; border-color: var(--accent-dark); box-shadow: 0 0 0 3px rgba(92, 104, 80, 0.22); }
.price-filter__field.is-active { border-color: var(--accent-dark); background: var(--accent-soft); }
/* CSS-drawn caret (matches the nav caret glyph approach) overlaid on each field
   and on the mobile summary; pure decoration, so it never steals pointer events. */
.price-filter__caret {
  position: absolute; top: 50%; right: 1.05rem;
  width: 0.46em; height: 0.46em;
  border-right: 1.6px solid var(--accent-dark); border-bottom: 1.6px solid var(--accent-dark);
  transform: translateY(-68%) rotate(45deg);
  pointer-events: none;
}
.price-filters__meta { display: flex; align-items: center; gap: 0.9rem; }
.price-filters__reset { flex: 0 0 auto; }

.price-filters__empty { padding-block: var(--flow-xl); }
.price-filters__empty .shell { display: grid; justify-items: start; gap: var(--flow-sm); }
.price-filters__empty-text { max-width: 52ch; font-size: clamp(1.02rem, 1.25vw, 1.15rem); color: var(--color-ink); }

/* Desktop: no summary chrome, the controls are always visible (the details'
   [open] state only matters on mobile). Author rule overrides the UA
   details-collapse so a closed details still shows its body here. */
@media (min-width: 641px) {
  .price-filters:not([open]) > .price-filters__body { display: grid; }
}

@media (max-width: 640px) {
  .price-filters { gap: var(--flow-xs); }
  .price-filters__summary {
    position: relative;
    display: flex; align-items: center; justify-content: space-between; gap: 0.9rem;
    min-height: 48px; padding: 0.75rem 1rem;
    border: 1px solid var(--color-border-strong); border-radius: var(--radius-xs);
    background: var(--color-white); color: var(--accent-dark);
    cursor: pointer; list-style: none;
  }
  .price-filters__summary-label {
    font-size: 0.82rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase;
  }
  .price-filters__summary-icon { position: static; transform: rotate(45deg); transition: transform 180ms ease; }
  .price-filters[open] .price-filters__summary-icon { transform: translateY(3px) rotate(225deg); }
  .price-filters__toolbar { display: none; }
  .price-filters:not([open]) .price-filters__body { display: none; }
  .price-filters__controls { grid-template-columns: 1fr; }
  .price-filters__meta { width: 100%; }
}

/* ---------- FAQ accordion ---------- */
.faq { display: grid; gap: 0.9rem; max-width: 820px; }
.faq-item { border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-white); overflow: hidden; }
.faq-item summary {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 1.05rem 1.3rem; cursor: pointer; list-style: none;
  font-family: var(--font-body); font-weight: 550; font-size: 1.02rem;
  transition: background-color 160ms ease;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary:hover { background: var(--color-cream); }
.faq-item__icon { position: relative; flex: 0 0 auto; width: 20px; height: 20px; }
.faq-item__icon::before, .faq-item__icon::after {
  content: ""; position: absolute; left: 50%; top: 50%; width: 12px; height: 1.6px;
  background: var(--accent-dark); transform: translate(-50%, -50%);
  transition: transform 200ms ease;
}
.faq-item__icon::after { transform: translate(-50%, -50%) rotate(90deg); }
.faq-item[open] .faq-item__icon::after { transform: translate(-50%, -50%) rotate(0deg); }
.faq-item__body { padding: 0 1.3rem 1.2rem; }
.faq-item__body p { max-width: 64ch; }
/* Native <details> pops its content: give the answer a short bloom on open
   (opacity + 4px, no layout animation). Closing stays native-instant (exit
   faster than entrance). Replays naturally because display toggles. */
@keyframes faq-bloom {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
.faq-item[open] .faq-item__body { animation: faq-bloom 240ms ease backwards; }

/* ---------- Reassurance callout (care-note) ----------
   Client: the old left-parked box "looks totally lost, not intentionally placed,
   just a random box on an empty section". Now a centred, composed moment: the
   note sits in the middle of its section, centre-aligned, opened by a small
   heart medallion (Bianca's "von Herzen" motif, tied to her camera-heart logo).
   Reads as a warm aside from Bianca, one design across all six instances. The
   heart is a ::before so no per-page markup changes are needed. */
.care-note {
  display: grid; justify-items: center; text-align: center;
  max-width: 660px; margin-inline: auto;
  padding: var(--flow-md) var(--flow-md) calc(var(--flow-md) + 2px);
  background: var(--surface-deep); border: 1px solid var(--color-border); border-radius: var(--radius-sm);
}
.care-note::before {
  content: ""; flex: 0 0 auto;
  width: 2.9rem; height: 2.9rem; margin-bottom: var(--flow-xs);
  border-radius: 999px; background-color: var(--accent-soft);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235C6850' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: center; background-size: 1.4rem;
}
/* Rosé pages swap the medallion heart to the wedding accent so it stays on-theme
   (the ::before colour is baked into the data URI, so re-declare it there). */
.theme-rose .care-note::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237D5D4F' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/%3E%3C/svg%3E");
}
.care-note__title { font-family: var(--font-display); font-weight: 550; font-size: 1.4rem; }
.care-note__text { max-width: 52ch; }
.care-note .actions-row { justify-content: center; }

/* ---------- Full-bleed CTA closer ---------- */
.cta-closer { position: relative; overflow: hidden; display: flex; align-items: center; min-height: clamp(460px, 62vh, 640px); background: var(--color-cream-deep); }
.cta-closer__media { position: absolute; inset: 0; }
.cta-closer__media img { width: 100%; height: 100%; object-fit: cover; object-position: var(--closer-pos, 50% 45%); }
/* Art-directed closers (slug_m): a mobile-composed source takes over below the
   art-direction line — 1020px (like the hero) by default, 760px for closers
   whose desktop frame still works on tablets (--art-sm, home). Two lazy
   pictures, display-toggled: the hidden one never intersects, so it is never
   fetched. The mobile image reads --closer-pos-m across its whole range. */
.cta-closer__media--mobile { display: none; }
.cta-closer__media--mobile img { object-position: var(--closer-pos-m, 50% 50%); }
@media (max-width: 1020px) {
  .cta-closer--art .cta-closer__media--single img { object-position: var(--closer-pos-m, 50% 50%); }
}
@media (max-width: 760px) {
  .cta-closer--art-sm .cta-closer__media--single img { object-position: var(--closer-pos-m, 50% 50%); }
}
@media (max-width: 1020px) {
  .cta-closer--art .cta-closer__media--desktop { display: none; }
  .cta-closer--art .cta-closer__media--mobile { display: block; }
}
@media (max-width: 760px) {
  .cta-closer--art-sm .cta-closer__media--desktop { display: none; }
  .cta-closer--art-sm .cta-closer__media--mobile { display: block; }
}
/* Tablet-tall art closer (home): in the 761-1020 band the width-bound 4:5 crop
   would push the family into the centered stack; a taller stage keeps the
   subjects above the text like on phones. */
@media (min-width: 761px) and (max-width: 1020px) {
  .cta-closer--tall-t { min-height: 900px; }
}
.cta-closer__content { position: relative; z-index: 2; width: 100%; padding: var(--section-gap) 0; text-align: center; color: var(--color-white); }
.cta-closer__stack { max-width: 640px; margin-inline: auto; display: grid; justify-items: center; }
.cta-closer__title { color: var(--color-white); text-shadow: var(--halo-dark); font-size: clamp(2.1rem, 4.4vw, 3.2rem); }
.cta-closer__line { max-width: 46ch; text-shadow: var(--halo-dark); font-size: clamp(1rem, 1.3vw, 1.15rem); }
/* B2 — primary + WhatsApp secondary share a row on desktop. Flex-wrap (not the
   old stacked grid) so a pair that fits the stack sits side by side; a pair too
   wide for the 640/560px stack wraps and each button then grows to one shared
   full width — equal-size stacked, the same outcome A1 gives the hero. Buttons
   grow from a 0 basis so a fitting pair is two equal halves and a wrapped pair
   is two equal full-width blocks; nowrap + auto min-width means a long label
   wraps the pair rather than the label. Alignment follows the stack side. */
.cta-closer__actions { display: flex; flex-wrap: wrap; align-items: stretch; justify-content: center; gap: var(--flow-xs) var(--flow-sm); }
/* Grow both to a shared width only when the WhatsApp secondary is present; a
   lone primary (no-WhatsApp closers, e.g. kita) keeps its natural width. */
.cta-closer__actions:has(.cta-closer__whatsapp) > .button { flex: 1 1 0; }
/* Desktop: keep the pair on ONE row even when the TITLE (not the buttons) is the
   widest thing in the stack — that was making the row wrap to stacked full-width
   on every shooting page (Per). Sizing the row to its own content widens the
   stack column just enough for both to sit side by side, and flex:1 1 0 above
   then splits that row into two equal halves. Narrow screens keep the
   wrap-to-equal-stack behavior untouched. */
@media (min-width: 620px) {
  .cta-closer__actions:has(.cta-closer__whatsapp) { flex-wrap: nowrap; width: max-content; max-width: 100%; }
}
/* Narrowest phones: the WhatsApp secondary's 37-char sentence label, as a
   nowrap button, outgrows the ~288px shell at 320px and would clip. Ease the
   closer button PAIR's type just here (letter-spacing off, tighter inline
   padding, a hair smaller) so both fit one line, matched, no wrap, no overflow
   — measured 270px in a 288px shell (18px headroom, Chromium+WebKit). The lone
   primary (kita) is out of scope and keeps the standard tier. */
@media (max-width: 360px) {
  .cta-closer__actions:has(.cta-closer__whatsapp) > .button {
    font-size: 0.76rem; letter-spacing: 0; padding-inline: 0.35rem;
  }
}
/* The WhatsApp secondary is the hero's on-photo ghost button (.button--ghost):
   warm-white outline + halo over the photo, no hover recolor (branding §3). */

/* Closer variant: content in the upper third (when subjects sit mid-frame) */
.cta-closer--top { align-items: flex-start; }
.cta-closer--top .cta-closer__content { padding-top: clamp(48px, 7vh, 84px); padding-bottom: clamp(220px, 34vh, 380px); }
/* Close-up variant: a taller stage keeps a bottom-anchored CTA below faces
   without shrinking the photograph into a banner. */
.cta-closer--bottom { align-items: flex-end; min-height: 860px; }
.cta-closer--bottom .cta-closer__content { padding-top: 0; padding-bottom: clamp(40px, 6vh, 72px); }

/* Mobile-center override: an art-directed closer whose mobile composition wants
   the standard centered stack while the DESKTOP frame runs --top (preise: family
   sits low in the wide frame, couple sits high in the mobile crop). */
@media (max-width: 1020px) {
  .cta-closer--center-m { align-items: center; }
  .cta-closer--center-m .cta-closer__content { padding-top: var(--section-gap); padding-bottom: var(--section-gap); }
}

/* Reserve variant (/ueber-mich/): on stacked widths the bench group spans the
   frame's middle, so the closer grows tall enough that the whole stack sits
   above the subjects — text on the house/garden top, family fully visible
   below. Photo-forward on purpose (pages end in photography). */
@media (max-width: 1020px) {
  .cta-closer--reserve { align-items: flex-start; }
  .cta-closer--reserve .cta-closer__content { padding-top: clamp(40px, 6vh, 64px); padding-bottom: clamp(580px, 70vh, 660px); }
}

/* Closer variant: dark ink text with warm-white halo (for bright imagery).
   The halo is denser than the light variant's (ink over mid-tone grass/petals
   needs a real light bed). A CENTRED ink stack is bedded by a full-image
   warm-white WASH (B1) — a uniform, barely-there lift of the whole frame so the
   ink reads without a bloom/box in the middle. It replaces the old centred
   radial bloom (Per: "either gradient or full overlay, not a box on image").
   Side-anchored dark closers (hochzeiten, preise on desktop) keep the
   directional linear veil and set no desktop wash — --wash/--wash-m stay 0
   there — so the veil alone beds the copy; they add a small wash only at the
   widths where the stack centres. The wash rides the same media layer as the
   linear veil, under the copy, and stays gentle (photo is still the star). */
.cta-closer--dark .cta-closer__content { color: var(--color-ink); }
.cta-closer--dark .cta-closer__media::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: rgba(var(--veil-rgb), var(--closer-wash-x, 0));
}
.cta-closer--dark .cta-closer__title,
.cta-closer--dark .cta-closer__line {
  color: var(--color-ink);
  text-shadow: 0 0 2px rgba(251, 250, 247, 0.95), 0 1px 3px rgba(251, 250, 247, 0.9), 0 3px 18px rgba(251, 250, 247, 0.8), 0 10px 44px rgba(251, 250, 247, 0.65);
}
/* Dark-closer WhatsApp ghost: the ink twin of the hero's on-photo ghost —
   ink outline + ink label + warm-white halo, no hover recolor (branding §3). */
.cta-closer--dark .button--ghost {
  border-color: rgba(26, 29, 24, 0.5); color: var(--color-ink);
  text-shadow: 0 0 2px rgba(251, 250, 247, 0.95), 0 1px 3px rgba(251, 250, 247, 0.9), 0 3px 16px rgba(251, 250, 247, 0.75);
}

/* Closer variant: content left, subjects visible centre/right */
.cta-closer--left .cta-closer__content { text-align: left; }
.cta-closer--left .cta-closer__stack { margin-inline: 0; justify-items: start; max-width: 560px; }
.cta-closer--left .cta-closer__actions { justify-content: flex-start; }

/* Closer variant: content right, subjects visible left/centre (wave 2).
   The mirror of --left, for frames whose open ground is on the right — the
   homepage closer is the client's own example: copy right over the tree and
   sky, veil right, the family on the left untouched. */
.cta-closer--right .cta-closer__content { text-align: right; }
.cta-closer--right .cta-closer__stack { margin-inline: 0 0; margin-left: auto; justify-items: end; max-width: 560px; }
.cta-closer--right .cta-closer__actions { justify-content: flex-end; }

/* Below 1020px every side-anchored closer returns to the centred stack. The
   switch sits on the SAME line as the art-direction source swap and the veil's
   _m tier (wave 2): before, the stack centred at 760 while the portrait source
   and the mobile veil arrived at 1020, so the 761-1020 band got a side-anchored
   stack lit by a bottom-anchored veil. One composition line, one veil. */
@media (max-width: 1020px) {
  .cta-closer__media img { object-position: var(--closer-pos-m, var(--closer-pos, 50% 45%)); }
  .cta-closer--bottom { min-height: 900px; }
  .cta-closer--left, .cta-closer--right { align-items: flex-end; }
  .cta-closer--left .cta-closer__content, .cta-closer--right .cta-closer__content { text-align: center; }
  .cta-closer--left .cta-closer__stack, .cta-closer--right .cta-closer__stack { margin-inline: auto; justify-items: center; }
  .cta-closer--left .cta-closer__actions, .cta-closer--right .cta-closer__actions { justify-content: center; }
}

/* Phone closer contract (client-directed): every pre-footer photo uses the
   Lifestyle closer's responsive height, regardless of desktop art-direction
   modifiers. Fixed `height` rather than `min-height` keeps every page equal;
   the shared content padding keeps the complete CTA stack inside even when a
   desktop composition uses --top, --bottom or --reserve. Desktop and tablet
   compositions remain untouched. */
@media (max-width: 760px) {
  .cta-closer {
    height: clamp(460px, 62vh, 640px);
    height: clamp(460px, 62svh, 640px);
    min-height: 0;
  }
  .cta-closer--top .cta-closer__content,
  .cta-closer--reserve .cta-closer__content {
    padding-top: var(--section-gap);
    padding-bottom: var(--section-gap);
  }
  /* Home keeps its bottom-anchored reading order inside the shared stage: the
     shorter top inset moves the copy off the eyes and toward the shoulders. */
  .cta-closer--bottom .cta-closer__content {
    padding-top: 0;
    padding-bottom: clamp(32px, 5svh, 48px);
  }
}

/* Sage closing band (Kita, B2B tone) */
.cta-band { text-align: center; }
.cta-band__stack { max-width: 640px; margin-inline: auto; display: grid; justify-items: center; text-align: center; }
.cta-band__stack .actions-row { justify-content: center; }
/* left-aligned variant (preise note band) — was an inline style override */
.cta-band__stack--start { margin-inline: 0; text-align: left; justify-items: start; }
.cta-band__stack--start .actions-row { justify-content: flex-start; }

/* ---------- Seasonal strip (home, CMS-toggled) ----------
   A small full-bleed band below the shootings that surfaces the current seasonal
   offer. Deeper Creme-Tief so it reads as its own note between the cream worlds
   grid and the sage About band. Centred stack (eyebrow, offer, button), normal
   type, a little air so it doesn't read cramped — but still a compact strip. */
.season-strip { background: var(--color-cream-deep); }
.season-strip__inner {
  display: grid; justify-items: center; gap: var(--flow-xs);
  text-align: center; padding-block: clamp(1.5rem, 3.4vw, 2.4rem);
}
.season-strip__text { display: grid; justify-items: center; gap: 0.35rem; margin: 0; }
.season-strip__text .eyebrow { margin: 0; }
.season-strip__offer { font-family: var(--font-display); font-weight: 550; font-size: 1.4rem; color: var(--color-ink); }

/* Voucher visual on /gutscheine/: the near-white printed card needs an edge on
   Warmweiss (print material, not photography — photos stay borderless). */
.voucher-visual img { border: 1px solid var(--color-border); box-shadow: var(--shadow-soft); }

/* ---------- Contact ---------- */
/* Channels left (42fr), form right (58fr); the channels card is first in the
   DOM so it also lands on top when this stacks on mobile. */
.contact-layout { display: grid; grid-template-columns: minmax(0, 42fr) minmax(0, 58fr); gap: clamp(24px, 3.5vw, 48px); align-items: start; }
/* The channels card rides down with the page and comes to rest when the form
   beside it ends — sticky inside the grid row does exactly that, no JS. */
@media (min-width: 961px) {
  .contact-card--channels { position: sticky; top: calc(var(--header-height) + var(--flow-sm)); }
}
/* minmax(0,1fr), not 1fr: a bare 1fr track floors at min-content, so one wide
   intrinsic child (nowrap button, select) pushes BOTH stacked cards past the
   shell on phones. Zero-floor lets the cards stay honest and children shrink. */
@media (max-width: 960px) { .contact-layout { grid-template-columns: minmax(0, 1fr); } }

.contact-card {
  display: grid; gap: 1.3rem;
  padding: var(--flow-md);
  background: var(--color-white); border: 1px solid var(--color-border); border-radius: var(--radius-sm);
}
/* Small-phone CTA tier: same inline-padding ease as .price-card. */
@media (max-width: 479px) { .contact-card { padding-inline: var(--flow-sm); } }
.contact-card h2, .contact-card h3 { font-size: 1.4rem; }
.contact-card__block { display: grid; gap: var(--flow-xs); justify-items: start; }
.contact-card__hint { font-size: 0.95rem; color: var(--color-ink-muted); max-width: 40ch; }

/* Channel rows: one consistent treatment for phone, mail and the radius note,
   each with a quiet uppercase label above its value. Previously the phone sat
   as unclickable muted text under the button while the mail was a text-link,
   so the same kind of information was styled three different ways. */
.contact-channels { list-style: none; display: grid; gap: var(--flow-sm); padding: 0; margin: 0; }
.contact-channel { display: grid; gap: 0.25rem; justify-items: start; }
.contact-channel + .contact-channel { padding-top: var(--flow-sm); border-top: 1px solid var(--color-border); }
.contact-channel__label {
  font-family: var(--font-body); font-weight: 500;
  font-size: 0.74rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--color-ink-muted);
}
.contact-channel__value {
  font-size: 1.05rem; color: var(--color-ink);
  text-decoration: underline; text-decoration-color: var(--color-border);
  text-underline-offset: 4px;
  transition: color 220ms cubic-bezier(0.22, 1, 0.36, 1), text-decoration-color 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.contact-channel__value:hover,
.contact-channel__value:focus-visible { color: var(--accent-dark); text-decoration-color: currentColor; }
.contact-channel__note { font-size: 0.95rem; color: var(--color-ink-muted); max-width: 40ch; }

.whatsapp-button { gap: 0.6rem; }
.whatsapp-button .inline-icon svg { width: 1.15em; height: 1.15em; }

/* Form */
.contact-form { display: grid; gap: 1.15rem; }
.contact-form__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem 1.1rem; }
.contact-form__field--full { grid-column: 1 / -1; }
.form-field { display: grid; gap: 0.4rem; }
.form-field label { font-size: 0.92rem; font-weight: 550; color: var(--color-ink); }
.form-field label .form-field__optional { font-weight: 400; color: var(--color-ink-muted); }
/* Every single-line control gets ONE explicit height. Sizing them from padding
   plus line-height alone let the browser drift individual types: the tel input
   rendered 58px against 54.8px for the rest, which read as fields being a few
   pixels off from column to column. */
.form-field input:not([type="hidden"]), .form-field select { height: 3.425rem; }
.form-field input, .form-field textarea, .form-field select {
  width: 100%; min-width: 0; padding: 0.8rem 0.95rem; line-height: 1.6;
  border: 1px solid var(--color-border-strong); border-radius: var(--radius-xs);
  background: var(--color-white); color: var(--color-ink);
  -webkit-appearance: none; appearance: none;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.form-field select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%233A3F36' stroke-width='1.6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0.9rem center;
  padding-right: 2.4rem;
}
.form-field textarea { resize: vertical; min-height: 8rem; }
.form-field input:focus-visible, .form-field textarea:focus-visible, .form-field select:focus-visible {
  outline: none; border-color: var(--accent-dark); box-shadow: 0 0 0 3px rgba(92, 104, 80, 0.22);
}
/* Autofill: browsers paint their own yellow/blue field tint; the inset-shadow
   trick holds Warmweiß + ink so autofilled fields match the calm form voice.
   (WebKit and standard :autofill stay separate rules: one unknown selector
   would invalidate the whole list in the other engine.) */
.form-field input:-webkit-autofill,
.form-field textarea:-webkit-autofill,
.form-field select:-webkit-autofill {
  -webkit-text-fill-color: var(--color-ink);
  -webkit-box-shadow: 0 0 0 1000px var(--color-white) inset;
  caret-color: var(--color-ink);
}
.form-field input:-webkit-autofill:focus-visible,
.form-field textarea:-webkit-autofill:focus-visible,
.form-field select:-webkit-autofill:focus-visible {
  -webkit-box-shadow: 0 0 0 3px rgba(92, 104, 80, 0.22), 0 0 0 1000px var(--color-white) inset;
}
.form-field input:autofill,
.form-field textarea:autofill,
.form-field select:autofill {
  box-shadow: 0 0 0 1000px var(--color-white) inset;
  caret-color: var(--color-ink);
}
.form-field input:autofill:focus-visible,
.form-field textarea:autofill:focus-visible,
.form-field select:autofill:focus-visible {
  box-shadow: 0 0 0 3px rgba(92, 104, 80, 0.22), 0 0 0 1000px var(--color-white) inset;
}
.form-field input.is-invalid, .form-field textarea.is-invalid, .form-field select.is-invalid { border-color: var(--color-error); }
.form-field__error { min-height: 1rem; color: var(--color-error); font-size: 0.87rem; }
.form-field__error:empty { min-height: 0; }
.form-honeypot {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* Status + success enter with a small settle; both toggle display (via :empty /
   [hidden]) so the animation replays per state change. Inline field errors stay
   instant on purpose: validation feedback must not lag the submit press. */
@keyframes note-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
.contact-form__status {
  color: var(--color-ink-muted); font-size: 0.95rem;
  animation: note-in 280ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.contact-form__status:empty { display: none; }
.contact-form__success {
  padding: 0.95rem 1.15rem;
  border: 1px solid var(--accent); border-radius: var(--radius-xs);
  background: var(--surface-deep); color: var(--color-ink); font-weight: 500;
  animation: note-in 320ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.contact-form__privacy { font-size: 0.88rem; color: var(--color-ink-muted); max-width: 60ch; }
@media (max-width: 640px) { .contact-form__grid { grid-template-columns: minmax(0, 1fr); } }

/* Mini image band */
.image-band { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap-tight); }
.image-band--four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Eight reads as 4x2 rather than one long row, so each frame keeps the same
   width as the four-up band instead of shrinking to a thumbnail. */
.image-band--eight { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.image-band img { width: 100%; height: 100%; object-fit: cover; aspect-ratio: 3 / 4; }
@media (max-width: 760px) {
  .image-band, .image-band--four, .image-band--eight { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .image-band > :nth-child(3):last-child { grid-column: 1 / -1; }
  .image-band > :nth-child(3):last-child img { aspect-ratio: 16 / 10; }
}

/* ---------- Alternating media rows (Hochzeit Momente) ---------- */
.media-rows { display: grid; gap: clamp(40px, 6vw, 80px); }
.media-row { display: grid; grid-template-columns: minmax(0, 48fr) minmax(0, 52fr); gap: clamp(24px, 4vw, 60px); align-items: center; }
.media-row__media { overflow: hidden; }
.media-row__media img { width: 100%; height: auto; }
.media-row__body { display: grid; justify-items: start; }
.media-row__body p { max-width: 52ch; }
.media-row--flip .media-row__media { order: 2; }
.media-row--flip .media-row__body { order: 1; }
@media (max-width: 860px) {
  .media-row, .media-row--flip { grid-template-columns: 1fr; }
  .media-row--flip .media-row__media { order: 0; }
  .media-row--flip .media-row__body { order: 1; }
}

/* ---------- Legal pages ---------- */
.legal-body { display: grid; gap: 1.1rem; max-width: 760px; }
.legal-body h2 { font-size: clamp(1.5rem, 2.2vw, 1.9rem); margin-top: var(--flow-sm); }
.legal-body h3 { font-size: 1.15rem; font-family: var(--font-body); font-weight: 600; margin-top: 0.4rem; }
.legal-body p, .legal-body li { max-width: 72ch; }
.legal-body ul { display: grid; gap: 0.4rem; padding-left: 1.2rem; }
.legal-body a {
  color: var(--accent-dark); text-decoration: underline; text-decoration-color: var(--accent-dark);
  text-underline-offset: 3px;
  transition: color 220ms cubic-bezier(0.22, 1, 0.36, 1), text-decoration-color 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.legal-body a:hover, .legal-body a:focus-visible { color: var(--color-ink); text-decoration-color: var(--color-ink); }

/* ---------- Footer ---------- */
.site-footer { background: var(--color-cream-deep); border-top: 1px solid var(--color-border); }
.site-footer__main {
  display: grid; grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 4vw, 56px);
  padding: clamp(48px, 6vw, 72px) 0 clamp(32px, 4vw, 48px);
}
.site-footer__brand { display: grid; gap: 0.9rem; align-content: start; justify-items: start; }
.site-footer__brand a { display: inline-block; transition: opacity 180ms ease, transform 180ms ease; }
.site-footer__brand a:hover, .site-footer__brand a:focus-visible { opacity: 0.82; transform: translateY(-1px); }
.site-footer__brand img { width: 190px; height: auto; }
.site-footer__tagline { font-family: var(--font-display); font-style: italic; font-size: 1.12rem; color: var(--color-ink); }
.site-footer__col { display: grid; gap: 0.6rem; align-content: start; }
.site-footer__col-title { font-size: 0.82rem; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 550; color: var(--color-ink-muted); margin-bottom: 0.3rem; }
.site-footer__col a {
  display: inline-block; width: fit-content; padding: 0.1rem 0; color: var(--color-ink); font-size: 0.98rem;
  text-decoration: underline; text-decoration-color: transparent; text-decoration-thickness: 1px; text-underline-offset: 3px;
  transition: color 220ms cubic-bezier(0.22, 1, 0.36, 1), text-decoration-color 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.site-footer__col a:hover, .site-footer__col a:focus-visible { color: var(--accent-dark); text-decoration-color: var(--accent-dark); }
.site-footer__contact { display: grid; gap: 0.6rem; }
/* Single-colour footer (Per, round 2): the bottom meta row shares the
   Creme-Tief footer ground; a hairline divider (not a colour change) separates
   it from the columns. The badge no longer reads as a lighter chip because its
   own orbit fill was tuned to this ground (see the badge block). */
.site-footer__bottom {
  border-top: 1px solid var(--color-border);
  font-size: 0.9rem; color: var(--color-ink-muted);
}
.site-footer__bottom-inner {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 1.1rem 0 calc(1.1rem + env(safe-area-inset-bottom));
}
.site-footer__legal { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 1.2rem; }
/* padding-block + negative margin: tap box reaches the 24px WCAG 2.2 floor
   (rendered line box was 23px) without shifting the legal row's visual rhythm */
.site-footer__legal a {
  color: var(--color-ink); padding-block: 0.35rem; margin-block: -0.35rem;
  text-decoration: underline; text-decoration-color: transparent; text-decoration-thickness: 1px; text-underline-offset: 3px;
  transition: color 220ms cubic-bezier(0.22, 1, 0.36, 1), text-decoration-color 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.site-footer__legal a:hover, .site-footer__legal a:focus-visible { color: var(--accent-dark); text-decoration-color: var(--accent-dark); }
/* Tablet: the brand introduces the footer across the full rail. The two link
   columns then align as a pair, while contact gets the full width it needs.
   This avoids a short brand block inheriting the height of the long Shootings
   column and leaving an accidental empty quadrant beneath the tagline. */
@media (max-width: 960px) {
  .site-footer__main { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .site-footer__brand, .site-footer__contact { grid-column: 1 / -1; }
}
@media (max-width: 560px) {
  /* Phone: one calm reading column. The footer is longer, but every label and
     contact detail has room to breathe and the scan order stays unambiguous. */
  .site-footer__main {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--flow-md);
  }
  .site-footer__brand, .site-footer__contact { grid-column: 1; }
  .site-footer__bottom-inner { display: grid; justify-items: start; }
}

/* ---------- KSWD badge (fixed asset — do not restyle) ---------- */
.site-footer__badge {
  --badge-orbit-duration: 6.8s;
  /* KSWD BRAND ASSET: the badge carries KSWD's own cream and violet, never this
     site's re-skinned tokens and never the footer colour. `var(--color-cream)`
     would resolve to Bianca's #F2EDE4 here, so the KSWD cream is a literal.
     Rail = that same cream nudged 10% toward ink, at a 1px hairline (1px or 2px
     are both valid; 1px suits this calm light footer). */
  --badge-cream: #f8f6f2;
  --badge-orbit-thickness: 1px;
  --badge-trace-length: 8;
  --badge-trace-opacity: 0.67;
  --badge-rail: color-mix(in srgb, var(--badge-cream) 90%, #231b2d 10%);
  position: relative;
  display: inline-flex;
  align-items: center;
  width: max-content;
  max-width: 100%;
  padding: 0.24rem;
  border: 0;
  border-radius: 0.4rem;
  background: transparent;
  box-shadow: 0 0.45rem 1.3rem -0.65rem rgba(0, 0, 0, 0.22);
  isolation: isolate;
  overflow: visible;
  transition: transform 180ms ease;
}
.site-footer__badge-orbit {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.site-footer__badge-orbit-fill { fill: var(--badge-cream); }
.site-footer__badge-orbit-rail,
.site-footer__badge-orbit-trace {
  fill: none;
  vector-effect: non-scaling-stroke;
}
.site-footer__badge-orbit-rail {
  stroke: var(--badge-rail);
  stroke-width: 1;
}
.site-footer__badge-orbit-trace {
  stroke: rgb(124, 58, 237);
  stroke-width: var(--badge-orbit-thickness);
  stroke-linecap: round;
  opacity: 0;
  filter: drop-shadow(0 0 1px rgba(124, 58, 237, 0.64));
}
.site-footer__badge-orbit.is-ready .site-footer__badge-orbit-trace {
  opacity: var(--badge-trace-opacity);
}
.site-footer__badge-core {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.24em;
  min-height: 1.32rem;
  padding: 0.24rem 0.88rem;
  border-radius: 0.16rem;
  background: transparent;
  color: #231b2d;
  font-size: 0.66rem;
  line-height: 1;
  letter-spacing: 0.035em;
  font-weight: 500;
  white-space: nowrap;
}
.site-footer__badge-core strong { font-weight: 600; }
.site-footer__badge:hover,
.site-footer__badge:focus-visible { transform: translateY(-2px); }

/* Anchor targets land below the sticky header (e.g. /#welten, /preise/#familien) */
section[id] { scroll-margin-top: 96px; }

/* ---------- 404 ---------- */
.error-page { text-align: center; display: grid; justify-items: center; }
.error-page__media { max-width: min(560px, 100%); overflow: hidden; }

/* ---------- Print ---------- */
/* Sanity pass for paper (legal pages are the real case, e.g. printing the
   Datenschutzerklärung): un-hide scroll reveals, pin the header (a scrolled-away
   header would otherwise print translated off-canvas), drop purely interactive
   chrome, and lift the photo-overlay halos (heavy grey blurs on paper). */
@media print {
  .has-js .reveal { opacity: 1; transform: none; transition: none; }
  .site-header { position: static; transform: none; animation: none; box-shadow: none; }
  .skip-link, .site-header__toggle, .site-nav__cta, .lightbox, .gallery-more { display: none !important; }
  .photo-hero__content .eyebrow, .photo-hero__title, .photo-hero__sub,
  .cta-closer__title, .cta-closer__line, .cta-closer__whatsapp, .button--ghost { text-shadow: none; }
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal, .has-js .reveal { opacity: 1; transform: none; transition: none; will-change: auto; }
  /* Entrance choreography flattens completely: no rise, no stagger, content
     simply stands (the global kill below already collapses durations; zeroing
     the animations keeps even the first frame at full opacity). */
  .has-js .photo-hero__stack > *,
  .has-js .page-hero__stack > *,
  .has-js .error-page > *,
  html[data-entrance-chrome] .site-header,
  .has-js .masonry__item.is-entering,
  .faq-item[open] .faq-item__body,
  .contact-form__status,
  .contact-form__success { animation: none; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
}
