/* ==========================================================================
   DJ Tim Matthiä — Stylesheet
   Design system: 1970s record sleeve. A cream paper ground, a warm
   near-black, and one ordered 12-step ramp that runs yellow → orange →
   red → rose and deliberately stops before the cold half of the wheel.
   Structure comes from a Studio 54 poster (stacked concentric arcs);
   the colour range comes from the Soulway sheets the client chose, which
   never go blue. Every colour on this page is a position on that ramp; nothing sits
   outside it. The brand gradient (#f58634 → #a8518a, straight off the
   client's logo) is not an exception to the system — it IS the system,
   compressed: those two hues fall at ramp positions 3 and 7.

   Retro is the handwriting here, not the announcement. This page has to
   win weddings and corporate parties, not club nights: full colour system,
   full arcs, but the tone stays composed. Where a choice is between louder
   and more assured, assured wins.

   The ramp reads as depth, not decoration: the poster's arcs are parallax
   layers standing still, and the arc-stack block near the bottom of this
   file sets them in motion.
   ========================================================================== */

/* ---- Fonts — self-hosted (no request to Google's servers, no personal
   data / IP address shared with a third party on page load) ---- */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../assets/fonts/inter-variable-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Archivo Black";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/archivo-black-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* The same Didone at a sturdier optical size, for display type set
   REVERSED — light on the dark sections. Measured, because the reason is
   not obvious: at opsz 96 this family's hairline is 0.20% of the em,
   which at 72px works out to 0.14 device pixels. Dark on cream that still
   renders as a faint grey and reads as elegance; light on near-black the
   antialiasing simply eats it and the word falls apart. That is the
   classic failure of a Didone in negative setting, not a rendering bug.

   opsz 32 triples the hairline to 0.60% (0.44px at the same size) and
   pulls stroke contrast from 1:75 to 1:25 — still unmistakably a Didone,
   but one that survives reversal. Kept as its own family so the hero,
   which sits dark on cream and wants the thinnest hairlines the family
   has, is untouched. Used by --font-display-dark. */
@font-face {
  font-family: "Bodoni Moda Deck";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/bodoni-moda-deck-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Bodoni Moda Deck";
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/bodoni-moda-deck-italic-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Lora";
  font-style: italic;
  font-weight: 500;
  font-display: swap;
  src: url("../assets/fonts/lora-italic-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Display face — Bodoni Moda (OFL), pinned to the 96pt optical size.
   A Didone: the thinnest hairlines and the highest stroke contrast the
   family offers, at weight 700 so the name carries real mass. That
   pairing — heaviest strokes, thinnest hairlines — is the whole point of
   a Didone, and it is what the reference is set in. The variable font
   ships an opsz axis; instantiating it at 96 rather than shipping the
   axis keeps the file at 7 KB and removes any chance of a small optical
   size being picked by accident.

   Replaces a fat rounded 70s face that was too playful for a page that
   has to win weddings and corporate bookings. The colour system still
   carries the Studio 54 reference; the typography no longer shouts it.
   Set in caps — a Didone's whole argument is its capitals.

   Subset to 89 glyphs. Note ★ is deliberately NOT included: no text face
   carries it, so the star rating falls back per-glyph, as it should. */
@font-face {
  font-family: "Bodoni Moda";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/bodoni-moda-display-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Hero headline face — Quicksand Bold (OFL), chosen 21 Oct 2026 after a
   side-by-side of 15 faces on the live first screen (Rebranding 2026).
   Picked because it is the "TIM" wordmark's own idea as a typeface: one
   even stroke, round terminals, a circular O — the same line the logo is
   drawn with, so headline and mark in the navbar read as one voice. The
   house font Le Havre Rounded was tried on the whole site and rejected,
   so the site keeps its own typography and is re-set text by text; this
   is the first of those texts. Only the hero H1 uses it (see .hero h1) —
   --font-display stays the Bodoni for the other display settings until
   each of those has been looked at on its own. Google's latin subset,
   self-hosted like the rest (no third-party request; font-src 'self' in
   the CSP). Licence: assets/fonts/OFL-Quicksand.txt. */
@font-face {
  font-family: "Quicksand";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/quicksand-bold-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Hero slogan face — Le Havre Rounded Light, the brand's house font
   (Rebranding 2026), chosen 21 Oct 2026 from ten candidates on the live
   first screen. The Markenhandbuch sets its headlines in exactly this
   cut, so the line under the name is the first place the house font
   appears on the site; the fat Quicksand above it is the same drawing
   idea one weight up, which is why the two sit together.

   LICENCE — read before deploying. Two licences, two file types:
   the MyFonts desktop licence covers the client's own Mac (the .otf in
   ~/Documents/DJ/Branding/myfonts_order_18951524122794/), the Fontspring
   web-font licence (order 136129416, 23 Oct 2026: whole family,
   perpetual, up to 10,000 pageviews/month) covers this site. The site
   loads only the Fontspring .woff2 — the EULA forbids embedding .otf/.ttf
   on a website. The EULA also forbids distributing the font files with
   the site's source code, so they stay out of git
   (.gitignore: assets/fonts/LeHavreRounded*): copy the six woff2 from
   ~/Documents/DJ/Branding/My_Fontspring_Fonts_Order_136129416/Web Fonts/
   into assets/fonts/ (renamed LeHavreRounded<Cut>.woff2) and upload
   them with the site. The web kit's line metrics differ from the .otf;
   the baseline correction right after these rules compensates. */
@font-face {
  font-family: "Le Havre Rounded";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("../assets/fonts/LeHavreRoundedLight.woff2") format("woff2");
}
/* Kicker face — Le Havre Rounded Black, the house font's heaviest cut,
   chosen 21 Oct 2026 on the tuning page from ten candidates for the
   section labels ("Mein Service", "Über mich" …). Same LICENCE note as
   the Light cut above. */
@font-face {
  font-family: "Le Havre Rounded";
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url("../assets/fonts/LeHavreRoundedBlack.woff2") format("woff2");
}
/* Column-title face of "Mein Service" — Le Havre Rounded Bold, chosen
   22 Oct 2026 on the tuning page. Same LICENCE note as the Light cut
   above. */
@font-face {
  font-family: "Le Havre Rounded";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/LeHavreRoundedBold.woff2") format("woff2");
}
/* Statement face — Le Havre Rounded Thin and Thin Italic, chosen 22 Oct
   2026 on the tuning page from ten candidates for the statement in
   "Mein Service" ("Der richtige Song / im richtigen Moment."). Same
   LICENCE note as the Light cut above. */
@font-face {
  font-family: "Le Havre Rounded";
  font-style: normal;
  font-weight: 100;
  font-display: swap;
  src: url("../assets/fonts/LeHavreRoundedThin.woff2") format("woff2");
}
@font-face {
  font-family: "Le Havre Rounded";
  font-style: italic;
  font-weight: 100;
  font-display: swap;
  src: url("../assets/fonts/LeHavreRoundedThinItalic.woff2") format("woff2");
}
/* Regular — the contact intro (23 Oct 2026), one step heavier than the
   Light body copy elsewhere. Same LICENCE note as the Light cut above. */
@font-face {
  font-family: "Le Havre Rounded";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/LeHavreRoundedRegular.woff2") format("woff2");
}

/* ---- Le Havre Rounded: web-font baseline correction ----
   Every placement on this page was tuned with the desktop .otf on a Mac.
   The Fontspring web kit (Oct 2026) has the same glyphs and widths,
   but its generator copied the Windows line metrics into hhea: ascent/
   descent 1221/415 (Thin) instead of the .otf's 630/370. Browsers centre
   the glyphs in the line box by those numbers, so with the web kit every
   line sat 0.15–0.27em lower — 21px under the big Thin headings,
   measured in WebKit.

   Why not fix it at the font level: ascent-override/descent-override
   would, but Safari (Mac and iPhone) ignores them, and rewriting the
   woff2's metrics would be a modification the Fontspring EULA (§2)
   doesn't clearly allow. So each element is lifted back by exactly the
   difference, (ascent − descent − 260) / 2 per mille of the font size:
   Thin 273, Light 206, Regular 146, Bold 162, Black 203. position:
   relative + top only moves the painted text, not the layout, and none
   of these elements has its own background. Inline children (the em in
   the statement, strong in copy) share the parent's baseline, so only
   the element that sets the font gets a shift. The .kicker's rule
   (::before) hangs off the text baseline and moves along with it.

   Adding a new Le Havre element: put its selector in the list for its
   weight, or its text will sit low. Swapping the kit for one with
   matching hhea values: delete this block. */
:root {
  --lhr-lift-100: -0.273em;
  --lhr-lift-300: -0.206em;
  --lhr-lift-400: -0.146em;
  --lhr-lift-700: -0.162em;
  --lhr-lift-900: -0.203em;
}
.statement-head,
.about-head,
#stimmen .text-heading,
#faq .text-heading,
#impressionen .text-heading,
#kontakt .text-heading { position: relative; top: var(--lhr-lift-100); }
.hero-slogan,
.statement-lead,
.feature-col p,
.service-proof strong,
.about-copy p,
#ablauf .text-heading,
.step-body p,
.testimonial-quote,
.faq-item .faq-answer p { position: relative; top: var(--lhr-lift-300); }
.contact-intro p { position: relative; top: var(--lhr-lift-400); }
.feature-col h3,
.service-proof span:not(.feature-rule),
.step-body h3,
.testimonial-name,
.testimonial-meta,
.info-list .label strong,
.form-field label { position: relative; top: var(--lhr-lift-700); }
/* Inline-level boxes need a second half. Their box is aligned to the
   surrounding line by its baseline, and with the web kit the baseline
   sits lower inside the box — so the box itself rose and the parent
   line grew (2.6px under every kicker, more per row in the contact
   list). vertical-align lowers the box back to where the .otf put it,
   top then lifts the glyphs. On a block-level .kicker (flex, in
   "Mein Service") vertical-align does nothing, top alone is right.
   .faq-q has its own optical top further down and adds the lift there. */
.info-list .label span { position: relative; top: var(--lhr-lift-700); vertical-align: var(--lhr-lift-700); }
.kicker { position: relative; top: var(--lhr-lift-900); vertical-align: var(--lhr-lift-900); }

/* ---- Design tokens ---- */
:root {
  /* Ground. Never pure #fff / #000 — the poster is printed paper and the
     dark state is a dimmed room, so both carry a little warmth. The
     light↔dark scroll handoff tweens between --cream and --ink, and
     because both are warm the crossfade passes through brown rather than
     grey: it reads as a dimmer coming down. That is intended. */
  --cream: #f7f1e6;
  --cream-2: #efe6d6;
  /* Two darks, and only two. --ink is the page's dark ground AND the
     fill of a primary button: same value on purpose, so a filled CTA
     reads as a piece of the ground standing up rather than as a third
     shade. --ink-2 is the one step above it, reserved for a surface that
     genuinely sits in front of the ground (the Ablauf cards). Anything
     wanting a fourth black is a sign the hierarchy is wrong, not that
     the palette is short.

     Every one of them is then lifted identically by the grain layer,
     which is why that layer has to sit above the navbar — see .grain. */
  --ink: #1a1512;
  --ink-2: #2a2320;

  /* Ink scale on cream. Contrast ratios measured against --cream, not
     guessed: anything below 4.5:1 cannot carry body copy. */
  --ink-70: #4a413c; /* 9.6:1 — body copy */
  --ink-55: #6d625b; /* 5.3:1 — secondary labels, captions */
  --ink-40: #8f847c; /* 3.2:1 — DECORATIVE ONLY, never text */
  --rule: #ddd0ba;
  --rule-dark: #2e2722;

  /* The ramp. Warm only: yellow through orange and red into rose, and it
     stops there. The Studio 54 poster runs on into violet and indigo, but
     every Soulway sheet the client picked stays on the warm half of the
     wheel, and that is what gives those designs their heat — a cold end
     drains it. Band 7 is the red sampled straight off the client's own
     swatch; the arcs run pink on the outside to yellow on the inside,
     exactly as the Soulway rings do.

     Bands 1–6 are light on cream (under 3:1) and are fill colours only.
     Bands 7–12 can carry a glyph on cream; check the pair before you do.
     Order matters — the arcs, the step numbers and the ribbon all walk
     this list in sequence. */
  --band-1: #f2c14e;  /* yellow   */
  --band-2: #ecac41;
  --band-3: #e5953c;  /* orange   */
  --band-4: #de7f38;
  --band-5: #d66435;  /* vermilion */
  --band-6: #cf4d33;
  --band-7: #c9393a;  /* red — the client's swatch */
  --band-8: #bf3448;
  --band-9: #b5355e;  /* raspberry */
  --band-10: #b94b7c;
  --band-11: #c4699a; /* rose     */
  --band-12: #d38fb2; /* blush    */

  /* Brand, straight from the client's logo SVG. Untouched: these are
     printed elsewhere. --brand-orange sits between bands 2 and 3,
     --brand-magenta between 7 and 8, which is why the logo and the arcs
     belong on the same page. The -rgb pairs exist because rgba() cannot
     take a var() hex, and the CTA's hover shadow needs alpha. */
  --brand-orange: #f58634;
  --brand-magenta: #a8518a;
  --cream-rgb: 247, 241, 230;
  --ink-rgb: 26, 21, 18;
  --brand-orange-rgb: 245, 134, 52;
  --brand-magenta-rgb: 168, 81, 138;

  /* Composed once here so the CTA, the loader bar and the footer rule all
     stretch the same spectrum instead of each inventing their own. */
  --grad-brand: linear-gradient(120deg, var(--band-1), var(--brand-orange), var(--brand-magenta), var(--band-9), var(--brand-orange));
  --grad-ramp: linear-gradient(90deg, var(--band-1), var(--band-3), var(--band-5), var(--band-7), var(--band-9), var(--band-11));

  /* Confetti — sampled from the moodboard's dark canvas. Softer and
     chalkier than the ramp on purpose: these are scattered accents on
     near-black, not structural colour. */
  --confetti-coral: #dc684b;
  --confetti-yellow: #f6cb75;
  --confetti-green: #80cea1;
  --confetti-pink: #df80ba;
  --confetti-blush: #db8b86;
  --confetti-blue: #274f99;
  --confetti-peach: #f09a56;

  /* Success state. Was a neon green that belonged to no system; now the
     confetti green, so the one moment of confirmation still reads as part
     of the same world. */
  --success: #80cea1;

  /* Radii */
  --radius-tag: 100px;
  --radius-card: 8px;
  --radius-image: 8px;
  --radius-btn: 40px;
  --radius-nav: 20px;
  --radius-sm: 4px;
  --radius-frame: 40px;

  /* Layout */
  --container: 1200px;
  --gutter: clamp(1.25rem, 4vw, 2.5rem);
  --section-pad: clamp(3.75rem, 8vw, 6.5rem);

  /* Fonts. Two display voices with a strict division of labour:
     --font-display is the editorial headline voice (the H1 and the big
     numerals), --font-heading carries every section heading and label.
     Archivo Black exists in one weight, so anything using it must not
     ask for a bolder one or the browser fakes it. */
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-display: "Bodoni Moda", "Didot", "Times New Roman", serif;
  --font-heading: "Archivo Black", "Arial Black", Impact, sans-serif;
  /* Display type set light-on-dark uses this instead of --font-display.
     Same family, sturdier optical size — see the @font-face note. */
  --font-display-dark: "Bodoni Moda Deck", "Bodoni Moda", "Didot", serif;

  /* Tracking — universal on UI/body type */
  --tracking: 0.06em;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

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

/* scroll-behavior stays "auto" — smoothness is now Lenis's job (see
   main.js), not the browser's; the two fighting over the same scroll
   position causes stutter. Lenis's own required base CSS (inlined from
   lenis/dist/lenis.css so it isn't a second render-blocking request). */
html { scroll-behavior: auto; }

/* Scrollbar hidden outright — same exact rule pair as austoentertainment
   .com (verified in their live stylesheet: `scrollbar-width: none` +
   `::-webkit-scrollbar { display: none }`), not something Lenis does on
   its own. Two reasons to want this here specifically: it matches that
   reference's look, and it removes a real stutter source — .lenis-
   stopped below switches html to overflow:clip while Lenis is paused
   (during the intro loader, the mobile menu, etc.), which momentarily
   removes a classic (space-reserving) scrollbar and snaps the content
   area's width back the instant it's restored. With no scrollbar ever
   reserving space in the first place, that width never moves at all —
   most noticeable for anyone whose OS/browser is set to always show a
   scrollbar, as ours was when this was reported. Scrolling itself is
   completely unaffected; only the visual bar is gone. */
html { scrollbar-width: none; }
html::-webkit-scrollbar { display: none; }

html.lenis, html.lenis body { height: auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch],
.lenis [data-lenis-prevent-vertical],
.lenis [data-lenis-prevent-horizontal] { overscroll-behavior: contain; }
.lenis.lenis-smooth iframe { pointer-events: none; }
.lenis.lenis-autoToggle { transition-property: overflow; transition-duration: 1ms; transition-behavior: allow-discrete; }

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.4;
  letter-spacing: var(--tracking);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* clip, not hidden. `overflow-x: hidden` makes <body> a scroll
     container, and a scroll-container ancestor silently kills
     `position: sticky` on everything inside it — which is what the
     Ablauf card stack is built on. `clip` does the same visual job
     (no sideways scrollbar from full-bleed elements) without creating
     that scroll container. */
  overflow-x: clip;
}

/* ---- Phone: no sideways drag ----
   On the iPhone the whole page could be dragged a few dozen pixels to
   the side and would spring back, the fixed navbar standing still — the
   gesture Safari gives a document that is wider than the screen. The
   document is not wider: measured in WebKit with iPhone emulation,
   documentElement.scrollWidth stays at the viewport width through the
   loader, the hero intro and every section, and each box that does poke
   past the edge (arc bands, gallery row, honeypot field) sits inside its
   own clipping ancestor. What is left is where the clip lives. With html
   at overflow:visible, body's overflow-x:clip above is handed to the
   viewport and body itself is laid out as visible — and a clip on the
   viewport is the one iOS Safari's scroll view treats as advisory: the
   rubber-band drag stays available. Two phone-only measures, so desktop
   keeps the rule above exactly as it is:
   1. html takes overflow-x:clip. Any non-visible value on the root stops
      body's value from propagating, so body's clip now applies to body's
      own box — a real layout clip, and the document cannot be wider than
      the screen whatever a descendant does. Still clip rather than
      hidden on both elements: neither becomes a scroll container, so the
      position:sticky card stack in Ablauf keeps working.
   2. The sections and the footer refuse the horizontal pan gesture
      outright; vertical scrolling and pinch-zoom stay. Not on body or
      html: touch-action is intersected up the ancestor chain, and that
      would reach the photo strip in Impressionen, whose own horizontal
      swipe must survive — so it is set per section and skips that one. */
@media (max-width: 960px) {
  html { overflow-x: clip; }
  main > section:not(#impressionen),
  footer { touch-action: pan-y pinch-zoom; }
}

img { max-width: 100%; display: block; }
/* Jedes Foto steckt in <picture> (WebP mit JPG-Fallback). display:contents
   lässt die Hülle im Layout verschwinden, damit das <img> weiterhin das
   direkte Kind von .hero-slide / .gallery-card / .media-frame ist — sonst
   greifen height:100% und object-fit nicht mehr, weil sich die Prozente
   auf ein inline-<picture> ohne eigene Höhe beziehen würden. */
picture { display: contents; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; margin: 0; padding: 0; }
button { font: inherit; cursor: pointer; }
p { margin: 0; }

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

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}

/* ---- Type scale (Minor Third, 16px base) ---- */
.text-caption { font-size: 12px; line-height: 1.21; letter-spacing: 0.06em; }
.text-body-sm { font-size: 14px; line-height: 1.33; letter-spacing: 0.06em; }
.text-body { font-size: 16px; line-height: 1.4; letter-spacing: 0.06em; }
.text-subheading { font-size: 18px; line-height: 1.4; letter-spacing: 0.06em; }
.text-heading-sm { font-size: 24px; line-height: 1.25; letter-spacing: 0.06em; }
.text-heading { font-size: 28px; line-height: 1.22; letter-spacing: 0.06em; }

h1, h2, h3, h4 { margin: 0; font-weight: 700; letter-spacing: 0.06em; }

/* Display face — wide, bold, all-caps. Stand-in for the reference's
   condensed "Foggy"; chosen wide per the client's own reference (Annonce,
   Cleveland Orchestra album art) instead of a condensed face. */
.display {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  /* A Didone at display size sets tightest in caps; a touch of positive
     tracking stops the hairlines from clotting at large sizes. */
  letter-spacing: 0.015em;
  line-height: 0.98;
  /* Explicit at the source, not just inherited from body — makes sure
     nothing between here and body can accidentally lose the smoothing
     on this very large, very bold headline text. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Serif italic accent — nods to the Century Oldstyle Italic pairing from
   the client's reference; used sparingly for taglines only. */
.accent-serif {
  font-family: "Lora", Georgia, serif;
  font-style: italic;
  font-weight: 500;
  letter-spacing: 0;
}

/* ---- Reveal-on-scroll ---- */
.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* ---- Buttons — pill shape is mandatory, 40px radius ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 12px 22px;
  border-radius: var(--radius-btn);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: var(--tracking);
  border: 1.5px solid transparent;
  white-space: nowrap;
  transition: background 0.3s var(--ease), color 0.3s var(--ease), border-color 0.3s var(--ease), opacity 0.3s var(--ease);
}

.btn-filled {
  background: var(--ink);
  color: var(--cream);
}
.btn-filled:hover { opacity: 0.8; }

.btn-outline {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink);
}
.btn-outline:hover { background: var(--ink); color: var(--cream); }

/* On black sections, outline buttons invert */
.on-dark .btn-outline {
  color: var(--cream);
  border-color: var(--cream);
}
.on-dark .btn-outline:hover { background: var(--cream); color: var(--ink); }
.on-dark .btn-filled { background: var(--cream); color: var(--ink); }

.btn-lg { padding: 16px 30px; font-size: 13px; }

/* ---- CTA gradient hover — reserved for primary "Anfragen" actions.
   Apply alongside .btn-filled or .btn-outline on every button that drives
   toward a booking enquiry, now or in the future: a colour panel wipes in
   from the left using the exact logo gradient (#f58634 → #a8518a), which
   keeps flowing/shifting for as long as the button is hovered, plus a
   quick diagonal shine sweep. This is the one deliberate colour exception
   in the interaction layer — base look stays flat black/white, only the
   hover moment goes brand-colour. */
.btn-cta {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition: color 0.4s var(--ease), border-color 0.4s var(--ease), transform 0.3s var(--ease), box-shadow 0.4s var(--ease);
}
.btn-cta::before {
  content: "";
  position: absolute;
  inset: -2px;
  background: var(--grad-brand);
  background-size: 300% 220%;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
  animation: dtmGradientFlow 4s ease-in-out infinite;
  animation-play-state: paused;
  z-index: 0;
}
.btn-cta::after {
  content: "";
  position: absolute;
  top: 0;
  left: -60%;
  width: 35%;
  height: 100%;
  background: linear-gradient(115deg, transparent, rgba(var(--cream-rgb), 0.55), transparent);
  transform: skewX(-18deg);
  opacity: 0;
  z-index: 1;
  pointer-events: none;
}
.btn-cta > * { position: relative; z-index: 2; }

.btn-cta:hover,
.btn-cta:focus-visible {
  color: var(--cream);
  border-color: transparent;
  transform: translateY(-2px);
  box-shadow: 0 16px 32px -12px rgba(var(--brand-magenta-rgb), 0.55), 0 10px 24px -14px rgba(var(--brand-orange-rgb), 0.5);
}
.btn-cta.btn-outline:hover,
.btn-cta.btn-outline:focus-visible,
.on-dark .btn-cta.btn-outline:hover,
.on-dark .btn-cta.btn-outline:focus-visible {
  background: transparent;
}
.btn-cta:hover::before,
.btn-cta:focus-visible::before {
  transform: scaleX(1);
  animation-play-state: running;
}
.btn-cta:hover::after {
  animation: dtmShine 1.1s ease-out;
}

@keyframes dtmGradientFlow {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}
@keyframes dtmShine {
  from { left: -60%; opacity: 1; }
  to { left: 130%; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .btn-cta::before { animation: none; transition: opacity 0.3s; transform: none; opacity: 0; }
  .btn-cta:hover::before, .btn-cta:focus-visible::before { opacity: 1; }
  .btn-cta::after { display: none; }
  .btn-cta:hover, .btn-cta:focus-visible { transform: none; }
}

/* ---- Loader — full-viewport intro, plays once on every page load.
   Visible by default via plain CSS below (no flash of the real page
   before JS runs); main.js holds the GSAP timeline that plays it back
   out — see the loader block near the top of that file. Two stacked
   copies of the TIM wordmark (Leinen on Nacht, per the brand rules) sit
   centered: a faint permanent copy (.is--base) and a full-strength copy
   (.is--top) that starts fully clipped away and gets revealed upward via
   clip-path. The black "page" — what
   actually covers the real site underneath — is an SVG path rather than
   a plain background-color, so it can morph and peel off upward instead
   of just fading; see .loader__wave's path in index.html and the three
   d-string keyframes in main.js — anchored to the TOP edge, so it
   retreats off the top of the screen, same up-and-away direction as the
   logo fill. */
.loader {
  position: fixed;
  inset: 0;
  z-index: 1000; /* above .grain (900), which is above everything else */
  width: 100%;
  height: 100dvh;
  overflow: hidden;
}
.loader__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.loader__wave {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--ink);
}
.loader__bar {
  position: absolute;
  inset: auto 0 0;
  height: 3px;
  background: var(--cream);
  transform-origin: 0% center;
  transform: scaleX(0);
}
.loader__container {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.loader__logo {
  position: relative;
  width: clamp(150px, 26vw, 250px);
  aspect-ratio: 630 / 238; /* the wordmark SVG's own box — both layers fill it */
  /* No transition here on purpose — the fly-up-to-navbar handoff in
     main.js's playLoaderOpen() drives transform/opacity on this element
     directly via GSAP, and a competing CSS transition would fight its
     own easing curve. */
}
.loader__logo-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.loader__logo-layer.is--base { opacity: 0.2; } /* faint copy, always visible */
.loader__logo-layer.is--top { clip-path: inset(100% 0% 0% 0%); } /* fully hidden — clip-path animates open from the bottom up */

/* Bottom-right percentage counter — same spot and idea as djmalike.com's
   own loader. Positioned off .loader__container (which is already
   position:relative and spans the full viewport). Fades on its own
   autoAlpha tween in main.js, independent of the logo — the logo no
   longer just fades with it, it flies up into the navbar instead (see
   playLoaderOpen() there). */
.loader__counter {
  position: absolute;
  right: var(--gutter);
  bottom: var(--gutter);
  width: 44px;
  height: 44px;
  border: 1.5px solid var(--cream);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--cream);
}

@media (prefers-reduced-motion: reduce) {
  .loader { display: none; }
}

/* ---- Navbar ---- */
.navbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  background: var(--cream);
}
.navbar .container {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding-block: 29px; /* matches austoentertainment.com's ~102px-tall nav */
}

/* The "TIM" wordmark (Rebranding Oktober 2026 — see the Branding note
   in Tims Obsidian vault and the Logo-Pack it points to) sits dead-center
   in the bar, independent of how wide the nav links or the right-side
   actions are — .brand is pulled out of the flex row (position:absolute)
   so nav-links and nav-cta simply divide the remaining space between
   themselves via the container's space-between, sliding nav-links over
   to the left where .brand used to sit. Centering shifts left by exactly
   half of --nav-icon-size (the mark's WIDTH, not 50%) so the mark's own
   midpoint lands on the container's true center.
   --nav-icon-size is the rendered width; the SVG is 630x238, so the
   height follows from that. Brand rule: never below 80px wide on screen
   (the 520px breakpoint below bottoms out exactly there). */
.brand {
  --nav-icon-size: 96px;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(calc(var(--nav-icon-size) / -2), -50%);
  display: flex;
  align-items: center;
  gap: 16px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: var(--tracking);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink);
}
.brand img { width: var(--nav-icon-size); height: auto; flex-shrink: 0; }

/* No hover effect on the mark beyond a quiet dim — the Markenhandbuch
   forbids rotating the logo, recolouring it and adding effects, which is
   exactly what the previous spin + gradient-flow hover did. The dim is a
   plain opacity change on the whole mark, so the mark itself stays the
   untouched original file. */
.brand-mark {
  position: relative;
  display: block;
  width: var(--nav-icon-size);
  flex-shrink: 0;
  transition: opacity 0.35s var(--ease);
}
.brand:hover .brand-mark,
.brand:focus-visible .brand-mark {
  opacity: 0.6;
}
.brand-mark img { display: block; width: 100%; height: auto; }

@media (max-width: 520px) {
  .brand { --nav-icon-size: 80px; } /* brand minimum for digital use */
  /* :not(.brand-mark) matters here — .brand-mark is itself a <span>, so
     the old plain ".brand span" selector was hiding the logo icon too,
     not just the wordmark. */
  .brand > span:not(.brand-mark) { display: none; }
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 2rem;
}
.nav-links a {
  font-size: 14px;
  font-weight: 400;
  letter-spacing: var(--tracking);
  color: var(--ink);
  position: relative;
  padding-block: 4px;
}
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0; right: 100%;
  bottom: 0;
  height: 1.5px;
  background: var(--ink);
  transition: right 0.3s var(--ease);
}
.nav-links a:hover::after { right: 0; }

.nav-cta { display: flex; align-items: center; gap: 1rem; }

/* The "Anfragen" button used to be yanked out of this flex row and
   absolutely positioned over the hero photo's horizontal centre, with two
   further rules re-stating the hover lift and the reduced-motion variant
   just to stop the centring transform being overwritten. The rebuilt hero
   puts the photo on the left, so that centring would now drag the button
   away from the navbar entirely. All of it is gone: the button sits in
   the flex row where it belongs, and three workaround blocks plus a
   resize listener went with it. */

/* ---- Music toggle — 8-bar "now playing" icon: each bar is scaled
   independently via JS/GSAP ("Freies Tanzen" — own random rhythm/height
   per bar), this just sizes/colours the resting icon. Free-standing (no
   box/border) — just the bars floating on their own. Always
   position:fixed — never part of .nav-cta's flex flow — so its X (the
   "longitude": centered in the lane between the content column's right
   edge and the viewport's right edge) is set once by main.js and never has to
   jump; only its Y animates, between the navbar's row and near the
   bottom of the viewport, scrubbed to scroll position (see main.js).
   Colour is driven by GSAP in sync with the light↔dark page-
   background handoff (also in main.js), not by CSS, since it needs to
   stay legible over whatever section it's currently floating above. */
.music-toggle {
  position: fixed;
  z-index: 102; /* above .navbar (100) and .mobile-panel (101) — it sits inside the navbar's own row while docked, and its opaque white background would otherwise hide it */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--ink);
}
.music-bars { width: 36px; height: 14px; overflow: visible; }
.music-bars rect { fill: currentColor; transform-box: fill-box; }

/* Menu toggle: just the three lines, no box around them. The button itself stays a 44px tap
   target; the lines sit flush left in it so they line up with the
   gutter exactly where the old boxed button's edge was. */
.nav-toggle {
  display: none;
  background: none;
  border: none;
  padding: 0;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: flex-start;
  color: var(--ink);
  flex-shrink: 0;
}

/* Three lines that fold into an X. Shared by the navbar toggle (always
   lines) and the mobile panel's close button (lines → X once the panel
   is open, see .mobile-panel.is-open below). Spans instead of an SVG so
   the transforms rotate around each line's own centre without relying
   on transform-box support for SVG in older Safari.

   Proportions taken from fabianbaroud.com's mobile header, which the
   client prefers: thin, square-ended, about as wide as a thumbnail,
   6px pitch. The first version (1.8px, round ends, middle line at
   calc(50% - 0.9px) = 6.1px) read as three DIFFERENT weights on the
   iPhone: each line started at another sub-pixel offset, so each got
   antialiased differently. Now every line sits on a whole-pixel step
   (0 / 6 / 12) of a whole-pixel box, so all three render identically. */
.burger {
  position: relative;
  display: block;
  width: 20px;
  height: 14px;
}
.burger span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.5px;
  background: currentColor;
  transition: transform 0.4s var(--ease), opacity 0.2s var(--ease);
}
.burger span:nth-child(1) { top: 0; }
.burger span:nth-child(2) { top: 6px; }
.burger span:nth-child(3) { top: 12px; }

/* 960px, not the more typical 860px: once the fixed music-toggle button
   claims a permanent right-hand column (see positionMusicToggleX() in
   main.js), the full desktop nav (links + centered logo + Anfragen) no
   longer fits in the 860–960px range without the wordmark colliding with
   Anfragen and nav-links colliding with the logo icon — verified via
   direct measurement at several widths in that band. */
@media (max-width: 960px) {
  .nav-links { display: none; }
  .nav-cta .btn { display: none; }
  .nav-toggle { display: flex; }

  /* ---- Touch targets (mobile only) ----
     Everything in the bar is a thumb target below 960px, and 44×44 CSS px
     is the floor a finger can hit reliably (Apple HIG / WCAG 2.5.5).
     Measured before this block: hamburger 40×40, music bars 36×14, the
     wordmark link 80×30. The bars themselves stay 36×14 — the brand mark
     and the icon are not redrawn, only the invisible hit area grows
     around them via padding, so the docked/floating geometry in main.js
     (which measures getBoundingClientRect) simply picks up the larger
     box and keeps centring it in the same lane. */
  .nav-toggle { width: 44px; height: 44px; }
  .music-toggle { padding: 15px 8px; } /* 36×14 bars → 52×44 hit area */
  /* The wordmark is absolutely centred via translate(-w/2, -50%), so
     vertical padding grows the target symmetrically without moving the
     mark. Brand rule (never below 80px wide) is untouched. */
  .brand { padding-block: 7px; }

  /* Slim bar on mobile: 102px (the austoentertainment.com desktop height)
     ate an eighth of a phone screen. 6px above and below the 44px touch
     row gives ~56px, the height of fabianbaroud.com's mobile header,
     which the client picked as the reference. The hero's top padding
     shrinks to match (see the .hero rule). */
  .navbar .container { padding-block: 6px; }

  /* Every tappable control: no 300ms double-tap-zoom delay on the tap,
     and the grey tap flash iOS paints over links replaced by nothing —
     the controls carry their own pressed/hover states. Page-level pinch
     zoom is untouched (that would be `touch-action: none`). */
  a, button, summary, input, textarea { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
  /* Pill buttons measure 43px (12px type + 12px padding + border). One
     pixel under the floor is still under it. */
  .btn { min-height: 44px; }
}

/* ---- Mobile panel — full-bleed black takeover ---- */
.mobile-panel {
  position: fixed;
  inset: 0;
  z-index: 101;
  background: var(--ink);
  color: var(--cream);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2rem;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s var(--ease), visibility 0.35s;
}
.mobile-panel.is-open { opacity: 1; visibility: visible; }
/* The small drop-in slide lives on the links, not on the panel: the
   close button has to stay pinned exactly over the navbar toggle (see
   below), and a transform on the panel would drag it 8px along. */
.mobile-panel a {
  font-family: var(--font-heading);
  text-transform: uppercase;
  font-size: 2rem;
  letter-spacing: 0;
  transform: translateY(-8px);
  transition: transform 0.35s var(--ease);
}
.mobile-panel.is-open a { transform: translateY(0); }

/* Sits on exactly the same spot as .nav-toggle (main.js copies the
   toggle's position over on open; top/left here are only the fallback),
   so to the eye it is one button: the panel fades in around the three
   lines and they fold into an X. Closing plays it backwards — the X
   unfolds while the panel fades out. The delay only exists on the way
   in (transition-delay is taken from the state being entered), so the
   fold starts once the black is mostly there. */
.mobile-panel .close-btn {
  position: absolute;
  top: 6px; left: var(--gutter);
  background: none;
  border: none;
  padding: 0;
  width: 44px; height: 44px;
  color: var(--cream);
  display: flex; align-items: center; justify-content: flex-start;
}
.mobile-panel.is-open .close-btn .burger span { transition-delay: 0.12s; }
.mobile-panel.is-open .close-btn .burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.mobile-panel.is-open .close-btn .burger span:nth-child(2) { opacity: 0; transform: scaleX(0); }
.mobile-panel.is-open .close-btn .burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

@media (prefers-reduced-motion: reduce) {
  .burger span,
  .mobile-panel a { transition: none; }
}

/* The panel only ever exists below the 960px nav breakpoint, so this is
   mobile-only by construction. Close button up to the 44px touch floor
   (same as .nav-toggle above, which it mirrors in position); the links
   get block padding so the gap between them is tappable too, not just
   the glyphs — a 2rem Archivo line is ~45px tall, but a finger that
   lands in the 2rem gap between two items currently hits nothing. The
   gap shrinks by the same amount so the panel's rhythm is unchanged.
   Safe-area padding keeps the bottom "Anfragen" clear of the home
   indicator on notched phones. */
@media (max-width: 960px) {
  /* LEFT, not right — and this one is a bug fix, not a preference. The
     music toggle is position:fixed at z-index 102, docked in the nav row
     at the right gutter, and it stays there above the open panel (101)
     with ink-coloured bars on the panel's ink background: invisible, but
     still the top-most hit target. Measured on the live site at 375px:
     the toggle's 36×14 box sat dead centre over the 40×40 close button,
     so a tap on the X started the music instead of closing the menu.
     Moving the X to exactly where the hamburger was (same gutter, same
     centre line: 6px nav padding + 22px) also means open and close
     happen under the same thumb. */
  .mobile-panel .close-btn { width: 44px; height: 44px; top: 6px; left: var(--gutter); right: auto; }
  /* And the toggle itself steps out while the panel is open — it is
     unreadable there anyway (ink on ink) and only ever intercepted taps.
     :has() is fine here: Safari 15.4+/Chrome 105+, and the fallback is
     just the pre-existing behaviour minus the overlap, since the X has
     moved away from it. */
  body:has(.mobile-panel.is-open) .music-toggle { visibility: hidden; }
  .mobile-panel { gap: 1rem; padding-bottom: env(safe-area-inset-bottom, 0px); }
  .mobile-panel a:not(.btn) { padding-block: 8px; }

  /* ---- Menu open/close choreography — the technique on raphaelnuevo.de
     (read from that site's own stylesheet, "V27 fullscreen menu"; the
     client sent a recording of it and asked for exactly this feel).
     Three moving parts, each with its own clock:

     1. The panel does not fade, it WIPES: a clip-path inset that opens
        from the top edge downward, 0.6s on a symmetric in-out curve
        (.76,0,.24,1 — slow off the mark, fast through the middle, soft
        landing), so the dark sheet reads as a blind coming down over the
        page rather than a layer appearing. Closing runs the same curve
        in reverse — the sheet lifts back up and the page is revealed from
        the bottom. visibility flips instantly on open and only after the
        wipe on close, so the links stay tappable while visible and never
        catch a tap while the panel is already gone. clip-path, not
        height/top: it is composited, nothing re-lays-out per frame.

     2. The links do not ride the blind down; they are already in place
        and SURFACE one after another: opacity 0 → 1 and a 14px lift, each
        0.45s on an expo-out curve (.22,1,.36,1), the first starting 180ms
        in (once the sheet has cleared the top of the list) and every
        further one 45ms later. The base rule carries the un-delayed
        transition, so on close they all drop out together, at once, a
        beat ahead of the lifting sheet — same ordering as the reference.
        No --i on the markup (index.html stays untouched); the stagger is
        written out per child, which is fine for seven of them.

     3. The X. The hamburger sits in the navbar UNDER the panel (z 100 vs
        101) and is covered within the first ~100ms of the wipe; the
        panel's own close button stands at the same spot (see .close-btn
        above). Its glyph turns in from -90° as the sheet passes, timed
        so it lands as the first link surfaces — reads as the three bars
        becoming the cross, without a second icon in the markup.

     Reduced motion: the global rule at the top of this file collapses
     every transition-duration to 0.01ms, so all three stages become an
     instant switch; the delays are zeroed here too so the links do not
     pop in a quarter-second after the sheet. */
  .mobile-panel {
    opacity: 1;
    transform: none;
    clip-path: inset(0 0 100% 0);
    transition: clip-path 0.6s cubic-bezier(0.76, 0, 0.24, 1), visibility 0s 0.6s;
  }
  .mobile-panel.is-open {
    clip-path: inset(0);
    transition-delay: 0s;
  }

  .mobile-panel > a {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1), transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .mobile-panel.is-open > a { opacity: 1; transform: none; }
  /* Children 2–8: the close button is child 1 and gets its own stage. */
  .mobile-panel.is-open > a:nth-child(2) { transition-delay: 0.18s; }
  .mobile-panel.is-open > a:nth-child(3) { transition-delay: 0.225s; }
  .mobile-panel.is-open > a:nth-child(4) { transition-delay: 0.27s; }
  .mobile-panel.is-open > a:nth-child(5) { transition-delay: 0.315s; }
  .mobile-panel.is-open > a:nth-child(6) { transition-delay: 0.36s; }
  .mobile-panel.is-open > a:nth-child(7) { transition-delay: 0.405s; }
  .mobile-panel.is-open > a:nth-child(8) { transition-delay: 0.45s; }
  /* The button's own .btn transition (background/colour hovers) is
     replaced for the entrance — opacity/transform only, same curve. */
  .mobile-panel.is-open > a.btn { transition-property: opacity, transform; }

  .mobile-panel .close-btn svg {
    opacity: 0;
    transform: rotate(-90deg) scale(0.6);
    transition: opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1), transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .mobile-panel.is-open .close-btn svg {
    opacity: 1;
    transform: none;
    transition-delay: 0.12s;
  }

  @media (prefers-reduced-motion: reduce) {
    .mobile-panel,
    .mobile-panel.is-open > a,
    .mobile-panel.is-open .close-btn svg { transition-delay: 0s !important; }
    .mobile-panel { clip-path: none; opacity: 0; }
    .mobile-panel.is-open { opacity: 1; }
  }
}

/* ---- Section shell ---- */
.section {
  position: relative;
  padding-block: var(--section-pad);
}

/* "Mein Service" runs at least a full viewport tall (not just its natural
   content height) so the dark-band takeover actually gets a moment to
   hold at full black before the next (light) section starts pulling it
   back — the whole point of the light↔dark handoff in main.js. Content
   stays the same, just given more room to breathe, centered in it. */
#service {
  min-height: 100vh;
  display: flex;
  align-items: center;
}
.section.on-dark { background: var(--ink); color: var(--cream); }
.section.on-dark .charcoal-text { color: var(--rule); }
.charcoal-text { color: var(--ink-70); }

/* ---- Phone: the light↔dark handoff on a sheet, not on body ----
   Desktop tweens body's background-color (see the handoff block in
   main.js). That is a JS write of a new colour on every frame for 0.6s,
   and each write repaints every visible tile of the page on the main
   thread — on an iPhone that is the stutter Tim recorded right where
   this handoff fires, at the Ablauf→Trennbild→Stimmen seam, and it fires
   again on every direction change across that line. On a phone the
   colour therefore lives on a fixed ink sheet behind the content, and
   only the sheet's opacity changes: a CSS transition on opacity runs on
   the compositor, no repaint, no JS per frame. z-index -1 puts it under
   the in-flow content but over the canvas, which stays cream from body's
   own background; the sections keep the transparent background main.js
   gives them, so the sheet shows through exactly where the body colour
   did. Same 0.6s as the desktop tween; reduced motion snaps, like the
   desktop's duration 0. */
@media (max-width: 960px) {
  body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    background: var(--ink);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.6s;
  }
  body.is-dark::before { opacity: 1; }
  @media (prefers-reduced-motion: reduce) {
    body::before { transition: none; }
  }
}

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
}
.section-head .heading-block { max-width: 640px; }
.section-head .text-heading { margin-top: 8px; text-transform: uppercase; font-family: var(--font-heading); font-weight: 400; }
.section-head p { margin-top: 12px; }
.section-head.center { justify-content: center; text-align: center; }
.section-head.center .heading-block { max-width: 640px; margin-inline: auto; }

.kicker {
  display: inline-flex;
  /* baseline, not center: see the vertical placement note on ::before. */
  align-items: baseline;
  gap: 14px; /* a rule needs more air beside it than a dot did */
  /* Le Havre Rounded Black instead of Archivo Black (--font-heading),
     see the @font-face note at the top. Size and tracking are the
     client's own values from the tuning page: 14px (Archivo sat at
     11.5px, but its caps are much wider) and 0.125em, open enough for
     a label in caps. The fallbacks are heavy rounded/grotesque system
     faces, so a missing font stays a bold label. */
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", "Arial Black", sans-serif;
  font-size: 14px;
  font-weight: 900;
  letter-spacing: 0.125em;
  text-transform: uppercase;
}
/* A rule, not a dot. A coloured dot in front of every label is one of the
   most reliable tells of a page that was assembled from defaults, and it
   claims a meaning it does not have — a dot reads as a status light. A
   short line is plain typographic punctuation: it separates the label
   from the margin without pretending to signal anything.

   1px at 60% strength rather than a solid hairline: at full currentColor
   the bar out-weighs the caps beside it and the eye lands on the line
   instead of the word. 40px since the switch to Le Havre (the client's
   value): the larger label carries a slightly longer rule. Round ends
   as the client chose them — at 1px they barely show, but they follow
   the house font's round terminals if the rule is ever made thicker. */
.kicker::before {
  content: "";
  width: 40px;
  height: 1px;
  border-radius: 999px;
  background: currentColor;
  opacity: 0.6;
  flex: none; /* never let a long label squeeze the rule */
  /* Vertical placement: the rule's centre sits at half the cap height,
     i.e. the optical middle of the uppercase label, whatever the face.
     align-items:center centred it on the line box instead, which is set
     by each font's own ascent/descent metrics: Archivo Black happens to
     land within 0.5px of the cap middle, but Le Havre Rounded (tested
     for the kickers, 21 Oct 2026) carries a deep descent and put the
     rule 2.7–4.5px low — visibly under the word. With baseline
     alignment an empty box sits ON the baseline; lifting it by half a
     cap height and dropping it by half its own height centres it on the
     caps. `cap` measures the label's own font; the em line before it is
     the fallback for browsers without the unit (Safari < 17.2), where
     0.35em is a typical half cap height. */
  transform: translateY(calc(50% - 0.35em));
  transform: translateY(calc(50% - 0.5cap));
}

/* ---- Paper grain ----
   The wear the client recognised from Lightroom, done the web way. An
   feTurbulence noise tile, desaturated, laid over the entire page at very
   low opacity.

   Two things make this cheap enough to ship:

   1. It is `position: fixed`. A grain layer inside the scrolling content
      has to be repainted every single scroll frame across its full area,
      which is the classic way to destroy scrolling performance on a
      phone. Fixed, the compositor paints it once and never touches it
      again. Never move this into a scrolling container.

   2. `pointer-events: none`, so it is invisible to every click, hover and
      focus on the page beneath it.

   Three things the first attempt got wrong, all measured rather than
   eyeballed:

   - feTurbulence output is semi-transparent by default (mean alpha came
     out at 127/255), so half the layer opacity was being spent before
     anything reached the screen. feFuncA discrete/1 forces the tile
     opaque, which makes the `opacity` line below the single honest knob.
   - Four octaves of fractalNoise is soft cloud, not grain. Two octaves
     at a high base frequency gives per-pixel speckle you can actually
     read as paper.
   - The contrast ramp (slope 1.5) widens the tonal spread so the
     speckle survives being laid on at a tenth of full strength.

   Effective on-screen contrast before: 3.55 levels out of 255, i.e.
   below the threshold where a screen can show it at all. The client
   could not see it, and was right not to.

   The tile's mean sits light, so the effect is asymmetric on purpose: on
   cream it barely registers, while on the near-black sections it lifts
   the blacks noticeably — the "faded" look that was asked for.

   z-index 900: over EVERYTHING except the loader, and that placement is
   load-bearing rather than arbitrary. At 60 the layer sat below the
   navbar (100), the mobile panel (101) and the music toggle (102), so
   those four elements were the only things on the page the grain never
   touched. The result was measurable and visible: --ink is #1a1512, the
   grain lifts it to roughly #2a2420 everywhere it applies, and the
   "Anfragen" button in the navbar therefore rendered 16 levels darker
   than every other black on the page. The client spotted it before the
   maths did.

   Real grain lives in the emulsion, across the whole frame — so the fix
   is the film going over everything, not a hand-tuned second black for
   the chrome. Keeping it under the loader is deliberate: that panel is
   its own moment before the page exists. pointer-events:none means
   sitting above the navbar costs no interactivity. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 900;
  pointer-events: none;
  opacity: 0.10;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='1.5' intercept='-0.22'/%3E%3CfeFuncG type='linear' slope='1.5' intercept='-0.22'/%3E%3CfeFuncB type='linear' slope='1.5' intercept='-0.22'/%3E%3CfeFuncA type='discrete' tableValues='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
}

/* Print has real paper; it does not need a picture of one. */
@media print { .grain { display: none; } }

/* The grain stays on phones too, and that is load-bearing, not an
   oversight. It was switched off below 960px once as a scroll-stutter
   suspect, and the intro broke: iOS Safari paints under its translucent
   bottom bar a stretched copy of the topmost screen-filling fixed layer
   that has no clipping of its own — on this page that is this grain
   layer (a tenth-opacity noise over the cream canvas, so the bar reads
   as cream). Take it away and Safari falls through to the page itself,
   since the loader is overflow:hidden and gets skipped: the hero showed
   through the bar while the loader's wave was still up. Bisected on
   Tim's iPhone (iOS 18.7, 21 Oct 2026) against four one-change variants;
   an empty fixed sheet without the image did NOT restore the cover, nor
   did a taller loader or an ink background on it. So the real layer with
   its background-image it is. Measured in Chromium and WebKit, the layer
   costs nothing on the main thread; the stutter had other causes (see
   the address-bar notes at the Ablauf stack and in main.js). */

/* ---- Arc stacks — the signature device ----
   The Studio 54 poster's stacked rainbow arcs, set in motion. Each band is
   the SAME arch (#dtmArc, defined once in index.html) at a slightly
   smaller size, drawn opaque and stacked front-to-back; the visible
   "stripe" is just the part of a bigger arch the next one doesn't cover.
   That is why there is one path and not seven.

   Two structural decisions worth keeping:

   1. Each band is a <div> wrapping the SVG, and GSAP animates the DIV.
      Transforms on an SVG <g> are not reliably promoted to their own
      compositor layer on iOS Safari; transforms on a block element are.

   2. Static positioning uses NO transform at all — not `transform`, and
      not the independent `translate` property either. GSAP writes
      `transform`, so CSS obviously cannot use it; but `translate` is out
      too, and that one is a trap worth recording: the moment GSAP takes
      over an element's transforms it sets `translate`, `rotate` and
      `scale` to `none` defensively, to stop them stacking on top of the
      matrix it manages. Measured here, not assumed — the bands sat in a
      pile at the left edge until this was found. So the fan is built
      purely from `left`, `bottom` and `width`, and `transform` belongs
      to GSAP alone. That also means the CSS-only layout is correct on its
      own, which is exactly what reduced-motion and no-JS visitors get.

      `left` is the centring maths written out: a band is
      `152% - i*11%` wide, so centring it needs
      `(100% - width) / 2` = `-26% + i*5.5%`.

   `contain: paint` plus the clipping on .arc-stack means the browser
   never has to consider these when painting anything outside the section.
   will-change is deliberately NOT set here — see .is-active below. */
.arc-stack {
  position: absolute;
  inset: auto 0 0;
  /* --flip lets one stack hang from the top instead of standing on the
     bottom. scaleY on the STACK, not the bands: the bands' own transforms
     belong to GSAP (see note 2), and flipping the container leaves their
     parallax maths untouched — it just runs upside down, which is what
     an inverted arch wants anyway. */
  height: clamp(18rem, 52vh, 34rem);
  overflow: hidden;
  contain: paint;
  pointer-events: none;
  z-index: 0;
}

.arc-band {
  position: absolute;
  bottom: calc(var(--i) * 1.2%); /* front bands ride slightly higher — the fan */
  left: calc(-26% + var(--i) * 5.5%); /* = (100% - width) / 2 */
  width: calc(152% - var(--i) * 11%);
  aspect-ratio: 2 / 1;
}
.arc-band svg { display: block; width: 100%; height: 100%; }

/* Layer promotion is switched on by the ScrollTrigger's onToggle only
   while a stack is actually on screen. Three stacks x seven bands
   permanently promoted would exhaust the compositor's layer budget on a
   mid-range phone before the gallery and the confetti even get a turn. */
.arc-stack.is-active .arc-band { will-change: transform; }

/* Hangs from the top of its section. */
.arc-stack--top {
  inset: 0 0 auto;
  transform: scaleY(-1);
}

/* The Ablauf stack is the only one that opens rather than drifts, and an
   opening fan needs stage depth the way a closed one does not: the bands
   grow out of a shared base at the top of the screen, so everything the
   effect does happens BELOW that line and gets cut off by the stack's own
   clip. At 52vh the bloom finished outside the box. Taller is safe here —
   #ablauf is over two viewports deep and the arcs sit behind its heading
   either way. */
.arc-stack--bloom { height: clamp(20rem, 64vh, 42rem); }

/* Seven overlapping arches on a phone read as mud, and cost seven layers
   to say it. Dropping to four widens the remaining bands so the fan still
   reads. Reuses the existing 860px breakpoint rather than adding one. */
@media (max-width: 860px) {
  .arc-stack { height: clamp(14rem, 38vh, 22rem); }
  .arc-band {
    width: calc(168% - var(--i) * 19%);
    left: calc(-34% + var(--i) * 9.5%); /* same centring maths, wider bands */
  }
  .arc-band:nth-child(2),
  .arc-band:nth-child(4),
  .arc-band:nth-child(6) { display: none; }

  /* Re-state the bloom height AFTER the .arc-stack rule above, which would
     otherwise win on equal specificity and take the opening fan's stage
     back to 22rem. Smaller than the desktop figure on purpose: four bands
     need less room than seven, and a phone has none to spare. */
  .arc-stack--bloom { height: clamp(16rem, 46vh, 26rem); }
}

/* ---- Hero ----
   Rebuilt to the client's own reference: a minimal editorial screen. Three
   things only — the name in Didone capitals, one line of promise, and the
   portrait. Everything that used to live here (an eyebrow, two buttons, a
   three-up stat row) either moved into Mein Service below or was cut as a
   duplicate. The single "Anfragen" button on this screen is the one in the
   navbar; nothing on this screen competes with it.

   The arc stack moved out of this section entirely. This is the quiet
   entrance; one flat colour block reads as the same palette without the
   fanfare, and the arcs do their work further down the page. */
.hero {
  position: relative;
  overflow: hidden;
  padding-top: clamp(7rem, 12vw, 9.5rem);
  padding-bottom: var(--section-pad);
  background: var(--cream);
}
/* Mobile: the navbar is only ~56px tall below 960px (see the navbar
   block), so the hero starts right under it — 56px bar + the same 10px
   gap above the portrait the 102px bar had. Fixed px, not the clamp: the
   bar's height is fixed px too. */
@media (max-width: 960px) {
  .hero { padding-top: 66px; }
}
.hero .container {
  position: relative;
  z-index: 1;
  display: grid;
  /* Portrait first, headline second — mirroring the reference, and the
     reverse of the previous layout. */
  grid-template-columns: 0.85fr 1.15fr;
  gap: clamp(2rem, 6vw, 5.5rem);
  align-items: center;
}

/* The 3D stage. perspective sits on the parent so both children share one
   vanishing point and read as two depths of a single object rather than
   two cards tilting independently. */
.hero-stage {
  position: relative;
  perspective: 1100px;
  perspective-origin: 50% 50%;
}
.hero-stage > * { transform-style: preserve-3d; }

/* The flat colour block behind the portrait — the reference's pink
   square, in the ramp's softest band, offset up and left so the portrait
   sits proud of it instead of centred on it. */
/* Warm ochre off the poster rather than the magenta that was here before:
   at low opacity that magenta went dusty pink on cream, which read as a
   colour that belonged to no system. Ochre keeps the block reading as
   printed paper stock and sits beside the brand orange instead of
   fighting it. */
.hero-block {
  position: absolute;
  left: -6%;
  top: -7%;
  width: 86%;
  height: 84%;
  background: var(--band-2);
  opacity: 0.22;
  border-radius: var(--radius-card);
}

/* The poster motif, brought back to the first screen as an accent rather
   than a full-width statement: a compact arc fan cropped by the right
   edge, sitting behind the headline. Same markup and same parallax as the
   big stacks — it just answers the colour block on the left instead of
   spanning the page. */
/* Anchored to the section, not the content column, so its overhang is
   cropped by the hero's own edge rather than pushing the page wider. */
.arc-stack--corner {
  inset: auto -7% -14% auto;
  width: min(42vw, 28rem);
  height: min(42vw, 28rem);
  border-radius: 50%;
  z-index: 0;
}
.arc-stack--corner .arc-band {
  left: calc(-26% + var(--i) * 5.5%);
  bottom: calc(var(--i) * 2.4%);
}
@media (max-width: 900px) {
  .arc-stack--corner { display: none; }
}

/* No ch-based cap here: `ch` resolves against this box's own inherited
   font (Inter at body size), not against the 100px+ display face inside
   it, so a "15ch" column came out ~150px wide and the headline ran
   straight out of it. The grid column already does the constraining; the
   slogan caps itself in its own rule, where ch does measure the right
   font. */
.hero-content { min-width: 0; }

.hero h1 {
  /* Quicksand Bold instead of the Bodoni that .display sets — see the
     @font-face note at the top. The fallbacks are the rounded sans faces
     most systems ship, so a failed font load still lands in the family. */
  font-family: "Quicksand", "Nunito", "Arial Rounded MT Bold", "Varela Round", sans-serif;
  font-weight: 700;
  /* The old clamp (3.25rem, 7vw + 0.5rem, 9rem) was sized for the
     Bodoni; every term is multiplied by 1.207 so the two-line block
     keeps exactly the width the client signed off on. Measured, not
     eyeballed: at the same size Quicksand's caps run narrower than the
     Didone's, and the client liked the block's dimensions, not the
     number. Tops out at 174px on wide screens. */
  font-size: clamp(3.92rem, 8.45vw + 0.6rem, 10.865rem);
  /* .display adds positive tracking to keep a Didone's hairlines apart;
     a monoline rounded face wants the opposite. The client set this
     value himself on the comparison page: slightly negative, which
     closes the gaps around J and T that Quicksand's wide round caps
     leave open, and he preferred it to the mixed-case setting. */
  letter-spacing: -0.02em;
}
/* "DJ TIM" on one line, the surname under it — the client's own wording.
   nowrap rather than text-wrap:balance, which was free to break the two
   words of the first line apart once the column got narrow. */
.hero h1 span { display: block; white-space: nowrap; }

.hero-slogan {
  margin-top: clamp(1.25rem, 2.5vw, 2rem);
  /* Le Havre Rounded Light instead of the Lora italic accent, see the
     @font-face note. The fallbacks are light system sans faces, so a
     missing font (no webfont kit yet) degrades to a thin sans line, not
     to a serif. accent-serif was removed from the markup for this. */
  font-family: "Le Havre Rounded", "Avenir Next", "Helvetica Neue", Arial, sans-serif;
  font-weight: 300;
  font-style: normal;
  /* 1.5625 × the old Lora clamp (1.05rem, 0.6vw + 0.9rem, 1.4rem): the
     client sized it on the comparison page to 35px at full width, where
     the Light cut fills the same line the Lora italic did. Lands at 26px
     on phones, which the thin stroke still carries on cream. */
  font-size: clamp(1.64rem, 0.94vw + 1.41rem, 2.19rem);
  line-height: 1.3;
  /* The client's own value from the tracking slider: a touch open, so
     the light strokes get air between them at this size. */
  letter-spacing: 0.02em;
  color: var(--ink-70);
  /* Three lines on purpose: "Wedding & Event DJ aus Köln:", then the
     occasions split by the cap. The one-liner it replaced ran into the
     corner arc fan on MacBook-sized windows (1280–1512px), and a plain
     balanced two-line wrap still did, by 4–85px, measured per width on
     the tuning page. The <br> in the markup fixes the first line; 23ch
     breaks the rest after "Firmenevent," — it has to stay wide enough
     for the first line, which is the longest. Gives 49–150px of air to
     the fan from 1220px up; between 901 and ~1200px the fan still
     overlaps (it doesn't shrink until 1067px). ch here measures Le Havre,
     the slogan's own face. balance keeps the occasions from stranding a
     single word on phones. */
  max-width: 23ch;
  text-wrap: balance;
}

.hero-frame {
  position: relative;
  background: var(--ink);
  border-radius: var(--radius-card);
  padding: 12px;
}
.hero-frame .inner {
  border-radius: var(--radius-sm);
  overflow: hidden;
  aspect-ratio: 4 / 5;
}
.hero-frame .inner img { object-position: center 32%; }
.hero-frame .inner img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Hero image slider — hovering the frame loops through the 3 photos,
   each one pushed up from below over the last. The strip is 4 slides tall
   (400%) — the 3 real photos plus slide 1 duplicated at the end — so the
   loop back to the start keeps sliding up instead of snapping backwards;
   see the hero slider block in main.js for the invisible reset once the
   duplicate is reached. Only one moving part (the strip itself), nothing
   to keep in sync. */
.hero-slider-track {
  height: 400%;
  display: flex;
  flex-direction: column;
}
.hero-slide { flex: 1 1 0; min-height: 0; }

@media (max-width: 900px) {
  .hero .container { grid-template-columns: 1fr; gap: 2.5rem; }
  .hero-stage { max-width: 400px; margin-inline: auto; width: 100%; }
  .hero-content { max-width: none; text-align: center; margin-inline: auto; }
  .hero-slogan { margin-inline: auto; }
  /* Centred under the name, same three lines as on desktop. balance is
     already set above; it stays repeated here because a lone last word
     ("… oder / Club") under a centred headline reads as a mistake. */
  .hero-slogan { text-wrap: balance; }
}

/* ---- Mein Service — the page's first real answer ----
   Position two has one job: tell the visitor whether this is for their
   evening. So it is the offering — not a greeting, not a gallery, not
   references, all of which land better once there is a reason to care.

   Set as an editorial statement rather than a feature grid, following the
   reference the client sent (austoentertainment.com, which this codebase
   already cites elsewhere). Its whole effect is air: one large sentence,
   a small label far to one side, a short paragraph offset from both, and
   a great deal of nothing in between. Cutting the ornament — icon chips,
   coloured discs, card boxes — is the point, not a saving.

   Motion here is restrained on purpose. The scroll reveal that walks the
   three columns in one at a time stays (the client asked for it
   specifically). The pointer tilt is dialled down to a third of the
   hero's: tilting a block of body copy in 3D fights legibility, and text
   that leans as your mouse passes reads as a gimmick. The hero, which has
   an image to tilt, keeps the full effect. */

.statement {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 3.1fr);
  column-gap: clamp(1.5rem, 4vw, 4rem);
  align-items: start;
  padding-block: clamp(1rem, 5vw, 4rem);
}
.statement-kicker { grid-column: 1; margin-top: 0.9em; }

.statement-head {
  grid-column: 2;
  /* The statement in the house font's finest cut, Le Havre Rounded
     Thin (was the Bodoni Moda Deck Bold until 22 Oct 2026). All values
     are the client's own from the tuning page. Thin carries on the dark
     ground only because it is set large: 120% of the old Bodoni size.
     The fallbacks are system faces with a hairline weight, so a missing
     font stays a light sentence rather than turning bold. */
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 100;
  /* Mixed case, not the caps the hero uses. A statement is a sentence and
     should read like one; the caps treatment belongs to the name. */
  text-transform: none;
  font-size: clamp(2.52rem, 5.52vw + 0.48rem, 6.48rem);
  /* Tight on purpose: two short lines read as one block. The <br> in the
     markup fixes the break after "Song", so the two halves of the
     sentence mirror each other. */
  line-height: 0.92;
  letter-spacing: 0.005em;
  text-wrap: balance;
}
/* The second line, "im richtigen Moment.", in the Thin Italic: a true
   italic of the same cut, so the emphasis is a change of voice, not of
   weight. */
.statement-head em {
  font-style: italic;
  font-weight: 100;
}

/* Offset from both the label and the headline, low and to the right —
   the reference's arrangement, and the reason the section reads as
   composed rather than stacked. */
.statement-lead {
  grid-column: 2;
  justify-self: end;
  margin-top: clamp(2rem, 6vw, 5rem);
  /* 36ch measured in this face. */
  max-width: 36ch;
}

/* The section's reading text — the lead above and the copy of the three
   columns below — in one setting: Le Havre Rounded Light (was Inter at
   15–16.5px until 22 Oct 2026). The client set it on the lead on the
   tuning page and on 23 Oct 2026 asked for the columns to match it
   exactly, so it lives in one rule and the two can't drift apart. One
   size at every width, far below the statement, in the muted --rule.
   The fallbacks match the statement's. */
.statement-lead,
.feature-col p {
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 300;
  font-size: 20px;
  line-height: 1.6;
  letter-spacing: 0.02em;
  color: var(--rule);
}

.feature-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 5vw, 3.25rem);
  margin-top: clamp(4rem, 10vw, 8rem);
  perspective: 1200px; /* one vanishing point, so the three read as depths */
}

/* The step-down. Breaks the flat three-across rhythm and gives the depth
   order something to agree with: the column that tilts least sits lowest.
   Layout only — the scroll reveal owns each column's transform. */
@media (min-width: 901px) {
  .feature-col { margin-top: calc(var(--i) * 2.2rem); }
}

.feature-col-inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  transform-style: preserve-3d;
}

/* The entire ornament: one hairline in this facet's ramp colour. */
.feature-rule {
  width: 48px;
  height: 2px;
  background: var(--col-band);
}

/* Column titles in Le Havre Rounded Bold caps (was Archivo Black until
   22 Oct 2026); the copy below them shares the lead's setting, see
   above. Values are the client's own from the tuning page; the titles
   reach 20px at a 1470px window and
   shrink with it, never under 16px. The fallbacks are rounded/sans
   system faces of the same weight. */
.feature-col h3 {
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", Helvetica, sans-serif;
  font-weight: 700;
  font-size: clamp(16px, 1.361vw, 20px);
  line-height: 1.15;
  text-transform: uppercase;
  letter-spacing: 0.065em;
}

/* Proof, sitting under the claim it supports. Evidence, not a feature —
   it should not compete with the statement above it.

   Rebuilt to actually behave like that: same 3-up grid and the same gap
   as .feature-grid above (not an auto-sized row hugging the left edge
   with whatever's left over as dead air on a wide screen), and the same
   single ornament that grid uses — one .feature-rule hairline, in the
   same --col-band sequence (band-1 / band-5 / band-9) the feature columns
   already walk. Colour lives in that hairline now, not in the numerals
   themselves; a full rainbow of Bodoni numerals was louder than anything
   else in a section whose own point, stated above, is cutting ornament
   rather than adding it. Quiet cream numbers read as this section's
   voice continuing, not a separate, showier widget bolted on after it. */
.service-proof {
  list-style: none;
  margin: clamp(3.5rem, 8vw, 6rem) 0 0;
  padding: clamp(1.75rem, 3vw, 2.5rem) 0 0;
  border-top: 1px solid var(--rule-dark);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 5vw, 3.25rem);
}
.service-proof li,
.service-proof a {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
}
.service-proof a { color: inherit; text-decoration: none; }
/* The numerals in Le Havre Rounded Light, the labels in Bold caps (was
   Archivo Black over Inter until 23 Oct 2026). Values are the client's
   own from the tuning page: 46px at a 1470px window, shrinking with it,
   never under 28px. A light cut works for numbers here where the old
   Didone did not: Le Havre has no hairline contrast to lose, one even
   stroke throughout. Below 900px the phone rule further down keeps its
   own sizes, tuned so three figures fit side by side. */
.service-proof strong {
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 300;
  font-size: clamp(28px, 3.129vw, 46px);
  line-height: 1;
  letter-spacing: 0.035em;
}
/* ★ is in no text face, so it would fall back mid-word at a wildly
   different size. Setting the stars in the body face makes that
   deliberate rather than accidental. */
.service-proof strong em {
  font-family: var(--font-body);
  font-style: normal;
  font-size: 0.58em;
  letter-spacing: -0.04em;
  vertical-align: 0.14em;
}
.service-proof span {
  /* Same secondary-on-ink colour .feature-col p and .statement-lead use in this
     section — not --ink-40, which the token scale
     above documents as calibrated for --cream and decorative-only, never
     text, i.e. the wrong pairing on this dark ground. */
  color: var(--rule);
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", Helvetica, sans-serif;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
}
.service-proof a strong { text-decoration: underline; text-decoration-color: transparent; transition: text-decoration-color 0.25s var(--ease); }
.service-proof a:hover strong,
.service-proof a:focus-visible strong { text-decoration-color: currentColor; }

@media (max-width: 900px) {
  .statement { grid-template-columns: 1fr; row-gap: 1.25rem; }
  .statement-kicker, .statement-head, .statement-lead { grid-column: 1; }
  .statement-kicker { margin-top: 0; }
  .statement-lead { justify-self: start; margin-top: 1.75rem; }
  .feature-grid { grid-template-columns: 1fr; gap: 2.5rem; perspective: none; }

  /* The three figures stay side by side on a phone — the stacked version
     cost almost a full screen of this dark section for three numbers and
     left the right half empty. One row, three equal columns that may
     shrink below their content's natural width (minmax(0, 1fr); a plain
     1fr floors at max-content and the longest label would widen its
     column and push the others off the edge). Everything inside scales
     down to the column: numerals at 26–32px instead of 28–44px, set
     nowrap with the tracking pulled in a touch (the widest, "24 Std.",
     measures 96px at 26px and a column at 360px is 101px — at the body's
     0.06em tracking it would not fit and broke into "24 / Std."), the
     label a point smaller and allowed to wrap — "Gespielte Gigs" and
     "Google-Bewertung" each break into two short lines at 375px — and
     the stars drop to a line of their own under the 5,0, since "5,0" and
     five stars side by side need ~125px and a column here is ~100px.
     The coloured hairline keeps marking each column; the stagger from
     main.js (12px per item) keeps working on a row exactly as it does on
     desktop. The Google column stays a link: the <a> wraps rule, number,
     stars and label, so its box is the whole column, well over 44px. */
  .service-proof {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(0.5rem, 2.5vw, 1.5rem);
    margin-top: clamp(2.5rem, 6vw, 4rem);
    padding-top: 1.5rem;
  }
  .service-proof li,
  .service-proof a { gap: 10px; }
  .service-proof strong {
    font-size: clamp(1.5rem, 4.6vw + 0.6rem, 2rem);
    line-height: 1;
    letter-spacing: -0.02em;
    white-space: nowrap;
  }
  .service-proof strong em {
    display: block;
    margin-top: 6px;
    font-size: 0.5em;
    letter-spacing: 0;
    vertical-align: baseline;
  }
  .service-proof span {
    font-size: 10px;
    line-height: 1.35;
    letter-spacing: 0.04em;
  }
}

/* ---- Über mich ----
   Rebuilt as a plain editorial spread. Copy first in the DOM so a phone
   reads text before portrait (the client's requirement) with no reordering
   trickery; on desktop the same order puts the portrait on the RIGHT,
   mirroring the hero so the two most photographic screens do not repeat
   one composition.

   The heading is set in the statement's face, Le Havre Rounded Thin
   with the emphasis in Thin Italic (was the Didone until 23 Oct 2026),
   rather than the grotesk the other section headings use. That splits
   the page into two tiers on purpose: Mein Service and Über mich are the
   two personal claims and get the display voice; Ablauf, FAQ,
   Impressionen and Kontakt are functional and keep Archivo Black.

   The stat row that used to close this section is gone. Every number in
   it was either already said elsewhere or wrong: "300+" now lives in the
   proof row under Mein Service, "18" is in the first paragraph two lines
   above where the tile repeated it, and "150 Gäste je Set-up"
   contradicted the "bis zu 120 Personen" in the Technik copy. Removing it
   loses no information and settles the contradiction. */
.about-grid {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(2.5rem, 7vw, 6rem);
  align-items: center;
}

/* Same face and tuning as .statement-head in Mein Service, at 90% of its
   size — the client's own values from the tuning page (23 Oct 2026). The
   clamp is the statement's clamp times 0.9. */
/* Gap to the kicker matches "Stimmen", FAQ, Impressionen and Kontakt
   (one-liners at line-height 1.35, 23 Oct 2026). This heading runs to two
   lines, so it keeps its tight 0.92 between them; the extra 0.215em
   (half of 1.35 − 0.92) puts its cap top exactly where a 1.35 line would. */
.about-head {
  margin-top: calc(8px + 0.215em);
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 100;
  font-size: clamp(2.268rem, 4.968vw + 0.432rem, 5.832rem);
  line-height: 0.92;
  letter-spacing: 0.005em;
  text-wrap: balance;
}
.about-head em {
  font-style: italic;
  font-weight: 100;
  padding-bottom: 0.06em; /* room for an italic descender at this size */
}

/* Four short paragraphs at one size, in the reading face of Mein Service
   (Le Havre Rounded Light, 20px), slightly tighter leading for the
   narrower column. All values are the client's own from the tuning page
   (23 Oct 2026); the old larger "lead" paragraph is gone — "Hi, ich bin
   Tim." opens the text by itself. */
.about-head + p,
.about-copy p + p { margin-top: 1.15rem; }
.about-copy p {
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 300;
  font-size: 20px;
  line-height: 1.45;
  letter-spacing: 0.02em;
  color: var(--ink-70);
  max-width: 54ch;
}

.about-media { position: relative; perspective: 1000px; }
.media-frame {
  border-radius: var(--radius-card);
  overflow: hidden;
  aspect-ratio: 4 / 5;
  transform-style: preserve-3d;
}
.media-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Phone-only helpers inside the frame; see the block below. */
.about-shade, .about-veil { display: none; }

/* ---- Über mich on a phone: the photo stays, the text drives through it.
   Stacked copy-then-photo (the layout before 23 Oct 2026) meant a phone
   showed heading plus four paragraphs and no picture for most of a
   screen and a half — Tim's complaint. Of six mockups he picked the mix
   of "tap to reveal" and "scroll story": the portrait with kicker and
   heading on it fills the screen and sticks, then the whole text (no
   cards, no chapters) scrolls up over it as one block while the photo
   darkens underneath. Once the whole text is on screen the section is
   over: photo and text scroll away together, the portrait is never
   shown bare a second time.

   Same markup as desktop. .about-copy and .split-copy-inner become
   display: contents, which promotes .about-intro, .about-text and
   .about-media to direct grid items so `order` can put the photo
   between heading and text. .about-intro is sticky with zero height:
   it holds no space, so the stage starts exactly where the section
   does, and sticks at the frame's inner padding over the photo. Both
   sticky boxes share the grid as their containing block, so they stay
   aligned for the whole run.

   Heights are plain vh (iOS's LARGE viewport), not dvh, on purpose.
   dvh re-measures every time Safari's address bar collapses, which
   resized the frame in steps mid-scroll — the "stuttering zoom" Tim saw
   on the mockup. With vh the stage's bottom simply sits under the
   toolbar while it is shown; nothing re-lays out. The zoom he liked is
   kept as a deliberate, scrubbed transform on the img in main.js.

   The veil and the heading fade are driven from main.js (scrubbed, so
   they follow the finger); the opacity set here is only the resting
   state for no-JS / reduced motion, where the photo stays lightly
   dimmed so the cream copy is readable throughout. */
@media (max-width: 900px) {
  .about-grid { grid-template-columns: 1fr; gap: 0; }
  .about-copy, .split-copy-inner { display: contents; }
  .about-copy p { max-width: none; }

  .about-intro {
    order: 1;
    position: sticky;
    top: 0;
    z-index: 3;
    height: 0;
    padding: 0 22px;
    color: var(--cream);
    pointer-events: none;
  }
  /* The 36px (stage padding 14px + 22px inside the frame) sit on the
     kicker, not on `top`: the zero-height box starts level with the
     stage before either sticks, so an offset on the content keeps the
     label 22px inside the photo both before and while it is pinned. */
  .about-intro .kicker { margin-top: 36px; }
  .about-intro .about-head { color: var(--cream); }

  .about-media {
    order: 2;
    position: sticky;
    top: 0;
    height: 100vh;
    max-width: none;
    padding-block: 14px;
    perspective: none;
    z-index: 1;
  }
  .about-media .media-frame { aspect-ratio: auto; height: 100%; }
  /* Scaled by main.js; the hint keeps the scaled bitmap on its own
     layer so the scrub is a composite, not a repaint. */
  .about-media .media-frame img { will-change: transform; transform-origin: 50% 40%; }

  .about-shade, .about-veil { display: block; position: absolute; pointer-events: none; }
  .about-shade-top { inset: 0 0 auto; height: 52%;
    background: linear-gradient(180deg, rgba(var(--ink-rgb), 0.72), rgba(var(--ink-rgb), 0.35) 45%, rgba(var(--ink-rgb), 0)); }
  .about-shade-bottom { inset: auto 0 0; height: 60%;
    background: linear-gradient(0deg, rgba(var(--ink-rgb), 0.86), rgba(var(--ink-rgb), 0.55) 45%, rgba(var(--ink-rgb), 0)); }
  .about-veil { inset: 0; background: rgba(var(--ink-rgb), 0.78); opacity: 0.6;
    -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }

  .about-text {
    order: 3;
    position: relative;
    z-index: 2;
    margin-top: -100vh;   /* start on top of the stage … */
    padding: 92vh 22px 56px; /* … but enter from just below the screen. No
                                tail: the moment the last line is on
                                screen the grid ends, the stage unpins and
                                photo and text leave together — Tim did not
                                want the portrait shown a second time
                                without text (24 Oct 2026) */
  }
  .about-text p {
    color: rgba(var(--cream-rgb), 0.92);
    font-size: 19px;
    line-height: 1.5;
    text-shadow: 0 1px 12px rgba(var(--ink-rgb), 0.5);
  }
  .about-text p + p { margin-top: 1.1rem; }
}
/* ---- Ablauf — a scroll-told card stack ----
   Four steps that are genuinely sequential, so they are staged as a
   sequence rather than laid out as four equal boxes in a row (which is
   what was here, and which said nothing about order).

   Each card sticks to the top of the viewport; the next one scrolls up
   over it while the covered one scales down and tips backwards into
   depth. The stack is pure CSS `position: sticky` — no ScrollTrigger pin.
   Pinning is the fragile half of this pattern (it rewrites the document's
   height and fights Lenis); sticky costs nothing and cannot desync.

   Consequence worth knowing: NO ancestor of .step-card may have
   `overflow` other than visible, or sticky silently stops working. That
   is why #ablauf below does not clip — the arc stack clips itself. */
#ablauf { position: relative; }
#ablauf > .container { position: relative; z-index: 1; }

/* The bloom's opening arcs cycle through the ramp's lightest colours right
   where the section head sits (see the "arcs sit behind its heading either
   way" note on .arc-stack--bloom above) — cream text and a cream outline
   button lose all contrast the moment a yellow or orange band is behind
   them. A rectangular ink plate behind the row fixed that but read as a
   floating card sitting on top of the arcs — the wrong kind of shape for
   a device that is supposed to bloom freely behind the heading. A stacked
   drop-shadow instead hugs the actual glyphs and the button's own outline
   (drop-shadow, unlike box-shadow/text-shadow, follows the alpha shape of
   the element and everything inside it — kicker rule, heading letterforms,
   pill border, all at once), so it reads as ink pooling around the type
   rather than a box laid over the artwork. Three passes: a tight one for
   a crisp edge, a wider blur for the actual contrast lift, and a soft long
   one so it still holds up against the brightest band (#f2c14e). */
/* "So einfach geht's." in Le Havre Rounded Light, 73 % of the "Mein
   Service" statement clamp (23 Oct 2026; replaced the longer two-line
   "Vom ersten Hallo / bis zum letzten Song." the same day). Light, not
   the Thin of the other big headings: it sits on the bright arcs and
   needs the extra stroke. Upright, because only Thin has an italic cut
   on the site. Mixed case, so the shared uppercase rule for
   .section-head .text-heading is undone here; the 640px heading block
   stays widened so the line never breaks. */
#ablauf .section-head .heading-block { max-width: 1010px; }
#ablauf .section-head .text-heading {
  /* Same kicker gap as "Stimmen" (see .about-head): two lines at
     line-height 1, so the gap is made up in margin. 0.198em was measured
     23 Oct 2026; it holds at every width because this size and the
     "Stimmen" size are fixed fractions (73 % and 90 %) of one clamp. */
  margin-top: calc(8px + 0.198em);
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 300;
  font-size: clamp(1.8396rem, 4.0296vw + 0.3504rem, 4.7304rem);
  line-height: 1;
  letter-spacing: 0;
  text-transform: none;
  text-wrap: balance;
}

#ablauf .section-head {
  position: relative;
  z-index: 1;
  filter:
    drop-shadow(0 0 2px rgba(26, 21, 18, 0.85))
    drop-shadow(0 0 10px rgba(26, 21, 18, 0.7))
    drop-shadow(0 0 22px rgba(26, 21, 18, 0.5));
}

.steps-stack {
  list-style: none;
  margin: clamp(2.5rem, 6vw, 4.5rem) 0 0;
  /* Pulls back the empty slack under the last card, measured in main.js
     ("Ablauf → Trennbild"), so the photo below sits centred between the
     last card and the "Rezensionen" kicker. 0 without JS. The 7px match
     what sits above the kicker's letters inside #stimmen on top of its
     padding (the kicker's own line box and the cap's side bearing), so
     both cream gaps measure the same to the eye, not just to the box. */
  margin-bottom: calc(7px - var(--steps-tail, 0px));
  padding: 0;
  position: relative;
  z-index: 1;
  /* Where the deck docks. Kept as a token because the recede tween in
     main.js has to end at the same line — see the note there. */
  --stack-top: 22dvh;
}

/* The card's own height IS its scroll cost: a sticky element holds for
   exactly as long as its box takes to pass. At full viewport height that
   made four steps cost four screens of scrolling, which read as a toll
   rather than a sequence — the first build genuinely held people up.
   At 54dvh the whole process passes in about two screens, and because a
   card is now shorter than the viewport you can see the next one waiting
   below the current one instead of it arriving out of nowhere. */
.step-card {
  position: sticky;
  top: var(--stack-top);
  min-height: 54dvh; /* dvh, never vh — the mobile address bar makes vh jump */
  display: grid;
  place-items: center;
  padding-inline: var(--gutter);
}

/* Perspective sits on a wrapper INSIDE the sticky element, never on the
   sticky element itself or any ancestor: `perspective` makes an element a
   containing block for its descendants, which breaks sticky's frame of
   reference. GSAP transforms .step-card-face; nothing ever transforms
   .step-card. */
.step-card-3d {
  perspective: 1200px;
  width: min(100%, 62rem);
}

.step-card-face {
  transform-origin: 50% 100%; /* tips away from its base, like a lid closing */
  display: grid;
  grid-template-columns: auto 1fr;
  gap: clamp(1.5rem, 5vw, 4rem);
  align-items: start;
  padding: clamp(2rem, 5vw, 3.5rem);
  background: var(--ink-2);
  border-radius: var(--radius-card);
  /* A uniform hairline, not a thick coloured top edge. A one-sided 3px
     accent on a rounded box tapers into two wedges where it meets the
     corner radii, which reads as a rendering artefact rather than a
     decision. The step's colour is already carried — at 9rem — by the
     numeral; the card only needs enough edge to lift off the dark
     section behind it. */
  border: 1px solid var(--rule-dark);
}

/* Archivo Black (--font-heading). The step title beside it moved to Le
   Havre Rounded on 23 Oct 2026; the numeral stayed, Tim compared it with
   five Le Havre cuts on the tuning panel and kept the loud grotesque.
   Before that, after Bodoni (too austere for a number this loud) and Fraunces at a soft,
   ink-trap optical size (too round-shouldered) both missed, the plainest
   answer won: the grotesque already carrying every heading on the page is
   also the one with no roundedness to object to. Single weight (400) —
   Archivo Black only ships the one cut; asking for 900 here would fake-bold
   it, same rule as --font-heading everywhere else. */
.step-num {
  font-family: var(--font-heading);
  font-weight: 400;
  font-size: clamp(3.5rem, 11vw, 9rem);
  line-height: 0.8;
  color: var(--step-band);
}

/* Step title and copy in the house font (23 Oct 2026, tuned on the
   panel): title Bold in caps like the "Mein Service" column titles, one
   size up because it sits next to a 144px numeral; copy Light like the
   lead and column copy there, but in --cream, since the card is dark. */
.step-body h3 {
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", Helvetica, sans-serif;
  font-weight: 700;
  font-size: clamp(32.64px, 3.265vw, 48px);
  line-height: 1.15;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}
.step-body p {
  margin-top: clamp(0.75rem, 1.5vw, 1.25rem);
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 300;
  color: var(--cream);
  font-size: 22px;
  line-height: 1.42;
  letter-spacing: 0.035em;
  max-width: 56ch;
}

@media (max-width: 640px) {
  .step-card-face { grid-template-columns: 1fr; gap: 1rem; }
  /* The deck docks much closer to the top on a phone. 22dvh of the
     viewport was 180px of empty ink above every card at 812px tall, on
     top of the centring inside a 54dvh box — the first card arrived a
     third of the way down the screen with nothing above it. The navbar
     hides itself on scroll-down (see main.js), so there is nothing up
     there to clear. 10dvh keeps a breath of space and still leaves the
     tilted, dimmed top edge of the covered card peeking out above the
     one in front, which is the whole stacking cue. The recede tween in
     main.js reads this token (stackTopPercent), so it follows.
     svh, not the dvh the desktop rules use: dvh tracks the address bar,
     so on iOS every collapse and return of that bar re-resolves these
     heights, relayouts the whole stack and shifts everything below it
     mid-scroll — a hitch at the start of nearly every scroll. svh is the
     small (bar visible) height and never moves once the page is loaded;
     the box is a little shorter than the bar-hidden viewport, which is
     fine for a top-aligned card. */
  .steps-stack { --stack-top: 10svh; }
  /* Top-aligned inside its box instead of centred: a 250px card centred
     in a 54dvh box put another ~80px of nothing above it. The box keeps
     its height (that is the sticky hold, i.e. how long a step stays
     before the next climbs over it); the slack now sits BELOW the card,
     where it shows the next card waiting instead of empty ink above. */
  .step-card { min-height: 46svh; place-items: start center; }
}

/* With motion off the cards must not stack invisibly on top of each
   other — nothing would ever move them apart. Fall back to a plain
   readable list. */
@media (prefers-reduced-motion: reduce) {
  .step-card { position: static; min-height: 0; margin-bottom: 1.5rem; }
  .step-card-3d { perspective: none; }
}

/* Short viewports (laptops at 100% zoom, phones in landscape) would leave
   a 54dvh card with almost no breathing room around the text. svh for
   the same reason as the phone block above: this rule is what an iPhone
   in portrait actually gets (its inner height sits under 720px), and
   dvh would relayout the stack on every address-bar move. On a laptop
   svh and dvh are the same number. */
@media (max-height: 720px) {
  .steps-stack { --stack-top: 14svh; }
  .step-card { min-height: 62svh; }
}

/* ---- Trennbild — one full-bleed photo band between Ablauf and Stimmen,
   the same device the reference site (fabianbaroud.com) uses between its
   own sections. <main> carries no horizontal padding of its own (only
   .container inside each section does), so a plain block-level <figure>
   sibling already spans the full viewport width — no 100vw/negative-
   margin trick needed, and body's overflow-x: clip (see the .lenis rule
   above) is already the safety net a full-bleed element would need
   anyway. Full colour, straight out of the source photo — no filter.
   aspect-ratio locks it to the source crop's own proportions (a ~4:1
   strip matching the client's own reference crop of this photo — wide
   enough to keep the room and the crowd around Tim in frame, rather
   than the earlier, tighter attempts that read as an over-zoomed
   close-up) so it stays a consistent thin band at any viewport width
   instead of cropping tighter on wide screens; no caption/credit,
   matching the Impressionen gallery just above having none either. */
/* Browsers give <figure> a default 1em/40px margin — unreset, that's
   exactly the cream gutter-on-all-sides look that turns a full-bleed
   band into a framed inset photo. */
.photo-divider { margin: 0; line-height: 0; }
.photo-divider img {
  display: block;
  width: 100%;
  /* The HTML width/height attributes map straight to the CSS width/height
     properties (not to aspect-ratio) — without this override, the
     height="605" attribute pins a fixed 605px box at every viewport
     width, aspect-ratio never gets a chance to run, and object-fit:cover
     crops the sides away on anything wider than 2400px÷(605/…)… in
     practice, on every real screen. height: auto hands sizing back to
     aspect-ratio, so the band actually gets thinner on narrow screens and
     wider on big ones instead of staying stuck at one fixed pixel height. */
  height: auto;
  aspect-ratio: 2400 / 605;
  object-fit: cover;
}
/* The source crop is ~4:1. At 375px wide that is a 94px strip — the
   crowd and Tim's raised arm become a smear between two sections, and
   the band reads as a rendering glitch rather than a photo. Phones get
   their own file instead (<source media> in index.html): a 4:3 cut from
   the full original ("Bilder/1 Ich/3 get down.jpg"), reaching from the
   fairy lights down past Tim's waist. Framed to the left of him (23 Oct
   2026): the laughing guests crouching on his left are the point of the
   photo, so they stay recognisable and his outstretched hand is cut at
   the wrist instead. An earlier 16:10 window onto the strip stopped at his chest, because
   the strip has nothing below that; Tim asked for more of the photo
   rather than less cream around it, and the taller band also fills the
   gap under the Ablauf cards. Graded to match the strip (its colours
   come from the client's darker reference export). Above 640px the
   strip is wide enough to read as the full-bleed band it is meant to
   be. */
@media (max-width: 640px) {
  .photo-divider img { aspect-ratio: 4 / 3; }
}

/* "Stimmen" breaks from the shared uppercase Archivo Black voice
   (.section-head .text-heading above): it reads as an editorial statement
   like "Über mich", so since 23 Oct 2026 it is set exactly like
   "Musik ist mein Handwerk": Le Havre Rounded Thin at 90 % of the
   "Mein Service" statement clamp. The looser line-height (1.35, tuned on
   the panel) gives the single word more air above the quotes. Scoped to
   #stimmen, #faq ("Fragen und Antworten."), #impressionen ("Von der
   Tanzfläche") and #kontakt ("Erzähl mir von deiner Veranstaltung"):
   same values, chosen on the panel the same day. Since then every
   section heading on the page is in Le Havre. The two longer ones need
   the heading block wider than the shared 640px, or they would break
   into three lines at full size. */
#impressionen .section-head .heading-block,
#kontakt .section-head .heading-block { max-width: 1200px; }
#stimmen .text-heading,
#faq .text-heading,
#impressionen .text-heading,
#kontakt .text-heading {
  text-transform: none;
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 100;
  font-size: clamp(2.268rem, 4.968vw + 0.432rem, 5.832rem);
  line-height: 1.35;
  letter-spacing: 0.005em;
}

/* ---- Stimmen — three real Google reviews, quiet and monochrome.
   Deliberately not the Ablauf/Impressionen dark-band treatment: this is
   proof, not spectacle, so it reads as a calm continuation of FAQ's flat
   register rather than another set piece. A vertical hairline stands
   between columns (the .service-proof stat row above uses the same
   "rule, not ornament" idea) instead of boxed/shadowed cards, which the
   monochrome system reserves nowhere else. Grid items stretch to the
   row's full height by default, so each .testimonial can be a column
   flexbox with margin-top: auto on the attribution — quotes of different
   lengths still land with every name/date/location on the same
   baseline. */
.testimonial-grid {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 4vw, 3rem);
}
.testimonial {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
  padding-left: clamp(2rem, 4vw, 3rem);
  border-left: 1px solid var(--rule);
}
.testimonial:first-child { padding-left: 0; border-left: none; }
/* ★ falls back mid-word at a wildly different size in the display/heading
   faces — same reasoning as .service-proof strong em above — so these are
   set in the body face and pulled in slightly tighter than body text. */
.testimonial-stars {
  font-family: var(--font-body);
  font-size: 15px;
  letter-spacing: -0.04em;
}
/* Quote, name and date in the house font (23 Oct 2026): the quote in
   Light like the "Über mich" copy, one pixel larger because the columns
   are narrow; name and date both Bold caps, like the stat labels. */
.testimonial-quote {
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 300;
  color: var(--ink-70);
  font-size: 21px;
  line-height: 1.45;
  letter-spacing: 0.02em;
}
.testimonial-attr { display: flex; flex-direction: column; gap: 4px; margin-top: auto; }
.testimonial-name {
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", Helvetica, sans-serif;
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.testimonial-meta {
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", Helvetica, sans-serif;
  font-weight: 700;
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-55);
}

.testimonial-cta { margin-top: clamp(2.5rem, 5vw, 4rem); }
.testimonial-cta .btn svg { flex-shrink: 0; }
.testimonial-nav { display: none; }

/* ---- Stimmen on a phone: the three columns become a swipe track.
   Stacked, the three reviews were a long scroll of similar grey text;
   Tim wanted them side by side with a swipe to the next one (24 Oct
   2026, "Anschnitt" of four mockups). Same markup: the grid turns into
   a horizontally scrolling flex row with scroll-snap. Each review is
   82% wide so the next one shows cut off at the right edge — that
   peek is the whole affordance, no arrows needed. The hairline between
   columns stays, the same rule as on desktop, just one at a time.

   The track bleeds to the screen edges with negative margins and gets
   the gutter back as padding so the first review lines up with the
   heading. Safari ignores end padding of a scroll container, so the
   trailing room that lets the LAST review snap to the start like the
   others is a flex pseudo-element, not padding-right — without it the
   third review could never come fully left, which is also why the
   mockup's dots stuck on "2" while the third was being read. */
@media (max-width: 900px) {
  .testimonial-grid {
    display: flex;
    gap: 0;
    margin-left: calc(-1 * var(--gutter));
    margin-right: calc(-1 * var(--gutter));
    padding-left: var(--gutter);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-left: var(--gutter);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }
  .testimonial-grid::-webkit-scrollbar { display: none; }
  /* 18% of the track's content box = exactly what is left beside an
     82% item, so the last item's start position is the maximum scroll
     position, no more and no less. */
  .testimonial-grid::after { content: ""; flex: 0 0 18%; }
  .testimonial {
    flex: 0 0 82%;
    scroll-snap-align: start;
    padding-left: 0;
    padding-right: 20px;
    border-left: none;
  }
  .testimonial + .testimonial { padding-left: 20px; border-left: 1px solid var(--rule); }
  /* Reviews off to the right are outside the viewport until swiped in;
     the one-time .reveal fade would then play mid-read, so on the
     phone the three are simply visible. */
  .testimonial.reveal { opacity: 1; transform: none; transition: none; }

  .testimonial-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 22px;
  }
  .testimonial-dots { display: flex; gap: 6px; }
  .testimonial-dots i {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--rule);
    transition: background 0.3s var(--ease);
  }
  .testimonial-dots i.is-on { background: var(--ink); }
  .testimonial-counter {
    font-family: var(--font-body);
    font-size: 12px; font-weight: 600; letter-spacing: 0.08em;
    font-variant-numeric: tabular-nums;
    color: var(--ink-55);
  }
}

/* ---- FAQ — native accordion, flat, hairline dividers ---- */
/* The page turns dark as soon as Impressionen's top crosses the viewport
   centre (handoff in main.js), while the last questions are still on
   screen above it. Ink on ink made them vanish (Tim, 23 Oct 2026), so the
   FAQ's text follows the background to cream, with the same 0.6s, and
   back on the way up. Everything inside inherits (kicker rule and + icon
   use currentColor); the hairlines are --rule, readable on both. */
#faq { transition: color 0.6s; }
body.is-dark #faq { color: var(--cream); }
@media (prefers-reduced-motion: reduce) { #faq { transition: none; } }
.faq-list { border-top: 1px solid var(--rule); }
.faq-item { border-bottom: 1px solid var(--rule); }
.faq-item summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding-block: 22px;
  /* Le Havre Rounded Light like the other questions-as-headlines on the
     page, tuned on the panel (23 Oct 2026). */
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-size: 27.5px;
  font-weight: 300;
  line-height: 1;
  letter-spacing: 0.005em;
}
.faq-item summary::-webkit-details-marker { display: none; }
/* Optical centring between the hairlines. The flex row centres the line
   box, and Le Havre Rounded's metrics (shallow ascent, deep descent, the
   same quirk the kicker rule works around) put the baseline at 17.5 of a
   27.5px line: the capitals then sat 6px above the middle and the
   questions looked like they were drifting up towards the rule above.
   Measured 23 Oct 2026: moving the text down by 0.22em puts the centre of
   the cap height exactly on the row's centre, where the + icon already
   sits. `top` shifts the glyphs only, so row height and icon stay put.
   Tied to this font: re-measure if the question face ever changes. */
.faq-q { position: relative; top: calc(0.22em + var(--lhr-lift-300)); } /* lift: web-font correction, see top of file */
.faq-icon { position: relative; width: 20px; height: 20px; flex-shrink: 0; }
.faq-icon::before,
.faq-icon::after {
  content: "";
  position: absolute;
  background: currentColor;
  transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
}
.faq-icon::before { left: 0; top: 50%; width: 100%; height: 1.5px; transform: translateY(-50%); }
.faq-icon::after { top: 0; left: 50%; width: 1.5px; height: 100%; transform: translateX(-50%); }
/* [open] covers the no-JS/reduced-motion path (native, instant toggle);
   .is-open covers the animated path in main.js, which flips this class
   immediately on click but can only remove the `open` attribute itself
   once the close animation has actually finished — see the FAQ accordion
   block there for why. */
.faq-item[open] .faq-icon::after,
.faq-item.is-open .faq-icon::after { opacity: 0; transform: translateX(-50%) rotate(90deg); }

.faq-item .faq-answer {
  max-width: 68ch;
  color: var(--ink-70);
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 300;
  font-size: 21px;
  line-height: 1.45;
  letter-spacing: 0.02em;
  overflow: hidden; /* clips the GSAP height tween in main.js mid-animation */
}
/* The bottom gap lives on the content (margin), not on .faq-answer itself
   (padding) — box-sizing:border-box means a border-box element's own
   padding can never be animated away by tweening `height` toward 0 (the
   padding alone would floor it at 22px, leaving a residual sliver that
   never fully closes). A margin on the last child has no such floor and
   is already included in whatever height GSAP measures for "auto". */
.faq-item .faq-answer > :last-child { margin-bottom: 22px; }
.faq-item .faq-answer p + p { margin-top: 0.75rem; }

/* ---- Impressionen — fixed viewport window (clips, never moves) with a
   photo-row track inside it that GSAP slides sideways (see main.js), tied
   1:1 to the page's vertical scroll. Splitting window and track into two
   elements is what actually reveals new photos as you scroll — putting
   both the clip and the transform on one element just drags the whole
   window off-page instead. No manual swipe of its own, so nothing fights
   the scroll-driven slide. */
.gallery-viewport {
  overflow-x: hidden;
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  padding-bottom: 12px;
}
.gallery-row {
  display: flex;
  gap: 16px;
  width: max-content;
  will-change: transform;
}

.gallery-card {
  flex: 0 0 auto;
  width: min(72vw, 280px);
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-image);
  overflow: hidden;
}
.gallery-card img { width: 100%; height: 100%; object-fit: cover; }

/* ---- Impressionen, desktop: two rows (djmalike.com) ----
   main.js splits the cards into two rows and adds .is-dual (desktop
   with motion only). Full-bleed, no gutter: the rows run edge to edge
   like theirs. Square tiles at roughly a quarter of the window, as
   there. No upper cap on purpose: with 10 photos each row has five,
   and only a width tied to the window keeps it wider than the
   window on any screen (5 × 24vw + gaps ≈ 130vw), so neither row ever
   shows an empty edge. */
.gallery-viewport.is-dual {
  --tile: max(220px, 24vw);
  margin-inline: 0;
  padding: 0;
  overflow: hidden;
}
.gallery-viewport.is-dual .gallery-row { gap: clamp(20px, 2.4vw, 40px); }
/* Wider than the gap inside a row, as on djmalike.com (80px there):
   at the in-row gap the two rows read as one cramped grid. */
.gallery-viewport.is-dual .gallery-row + .gallery-row { margin-top: clamp(48px, 5.5vw, 100px); }
.gallery-viewport.is-dual .gallery-card { width: var(--tile); aspect-ratio: 1 / 1; }
/* Most photos are portrait with the face in the upper third. */
.gallery-viewport.is-dual .gallery-card img { object-position: 50% 30%; }

/* ---- Impressionen, mobile ----
   .gallery-viewport sits OUTSIDE .container (a direct child of the
   section), so its negative gutter margin does not undo a container
   padding — it pulls the box 20px past the viewport on both sides. On
   desktop that overhang is harmless (body's overflow-x: clip hides it)
   and stays as it is. On a phone it is the one thing making
   documentElement.scrollWidth 395px at a 375px screen, which is exactly
   what lets a mobile browser grow its layout viewport past the glass
   and lets position:fixed chrome (navbar, menu panel, music toggle) land
   20px off-screen. Zero margin, same gutter padding: the first card
   still sits flush with the text column, nothing overhangs. */
@media (max-width: 960px) {
  .gallery-viewport { margin-inline: 0; }

  /* Swipe, not scrub. The desktop ride ties the row's X to the page's Y
     (see main.js): the section is 686px tall at 375px wide, so the 13
     cards — 3.6 m of row — fly through in a screen and a half of
     thumb-scrolling, with no way to stop on one. A phone already has
     the right gesture for a photo strip. The viewport becomes a native
     horizontal scroller with snap points; main.js skips its ScrollTrigger
     below this same breakpoint, so nothing fights the finger. Lenis
     leaves native touch scrolling alone by default (syncTouch is off),
     so the nested scroller needs no data-lenis-prevent. Scrollbar hidden
     the same way the page's own is (see html at the top of this file);
     the 72vw card width already shows the next card's edge as the cue
     that there is more. */
  .gallery-viewport {
    overflow-x: auto;
    overscroll-behavior-x: contain; /* a swipe past the last card must not become Safari's back/forward gesture */
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--gutter);
    scrollbar-width: none;
  }
  .gallery-viewport::-webkit-scrollbar { display: none; }
  .gallery-card { scroll-snap-align: start; }
  .gallery-row { will-change: auto; } /* no transform tween here — no layer to keep warm */
}

/* ---- Kontakt ---- */
.contact-layout {
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: start;
}
/* Contact copy in Le Havre Rounded (23 Oct 2026, tuned on the panel):
   intro in Regular, one step heavier than the Light body copy elsewhere;
   contact ways in Bold, their sub line and the form labels in small Bold
   caps like the dates under the reviews. */
.contact-intro p {
  color: var(--ink-70);
  font-family: "Le Havre Rounded", "Helvetica Neue", Helvetica, sans-serif;
  font-weight: 400;
  font-size: 20px;
  line-height: 1.45;
  letter-spacing: 0.03em;
}

.info-list { margin-top: 2rem; border-top: 1px solid var(--rule); }
.info-list a {
  display: flex;
  align-items: center;
  gap: 16px;
  padding-block: 18px;
  border-bottom: 1px solid var(--rule);
  transition: gap 0.3s var(--ease);
}
.info-list a:hover { gap: 22px; }
.info-icon {
  width: 24px; height: 24px;
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.info-icon svg { width: 100%; height: 100%; stroke: var(--ink); }
/* Same optical fix as .faq-q: Le Havre's deep descent leaves the two
   lines (cap top of the address to the baseline of the caps line under
   it) 4.5px above the row's centre, so they looked pushed up against
   the rule above while the icon beside them sat in the middle. Measured
   23 Oct 2026; `top` moves the glyphs only, row height stays. */
.info-list .label { position: relative; top: 4.5px; }
.info-list .label strong {
  display: block;
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", Helvetica, sans-serif;
  font-size: 20px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.015em;
}
.info-list .label span {
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", Helvetica, sans-serif;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.28;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-55);
}

/* Form */
.form-field { margin-bottom: 20px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-field label {
  display: block;
  font-family: "Le Havre Rounded", "Arial Rounded MT Bold", Helvetica, sans-serif;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-bottom: 8px;
}
.form-field input,
.form-field textarea {
  width: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--radius-nav);
  padding: 14px 20px;
  font-family: var(--font-body);
  font-size: 15px;
  letter-spacing: var(--tracking);
  color: var(--ink);
  background: var(--cream);
  transition: border-color 0.25s var(--ease);
}
.form-field textarea { min-height: 140px; resize: vertical; }
.form-field input:focus,
.form-field textarea:focus { outline: none; border-color: var(--ink); }
.form-field .field-error {
  display: none;
  margin-top: 8px;
  font-size: 12.5px;
  color: var(--ink);
}
.form-field.has-error input,
.form-field.has-error textarea { border-color: var(--ink); border-width: 1.5px; }
.form-field.has-error .field-error { display: block; }

.form-honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

.form-note { margin-top: 20px; font-size: 12.5px; color: var(--ink-55); }

/* The one permitted color moment: success confirmation */
.form-panel {
  display: none;
  border-radius: var(--radius-card);
  padding: clamp(1.75rem, 4vw, 2.5rem);
}
.form-panel.is-visible { display: block; }
.form-panel.success { background: var(--success); color: var(--ink); }
.form-panel.success h3 { text-transform: uppercase; }
.form-panel.success p { margin-top: 8px; font-size: 15px; }
.form-panel.fail { border: 1.5px solid var(--ink); }
.form-panel.fail p { margin-top: 8px; font-size: 15px; color: var(--ink-70); }
.form-panel.fail a { text-decoration: underline; }

@media (max-width: 900px) {
  .contact-layout { grid-template-columns: 1fr; }
  /* Form first on a phone, Tim's own channels after it. In the single
     column the DOM order put the paragraph, then mail / phone / WhatsApp /
     Instagram, and only then the form — a screen of links before the
     thing the heading asks for. The paragraph still belongs above the
     form (it says "use the form"), so .contact-intro gives up its box
     (display: contents) and its two children become grid items in their
     own right, which lets the form slot between them: intro text, form,
     channels. No markup change, desktop keeps its two columns. Side
     effects, both deliberate: the intro's one-time .reveal fade cannot
     run on a box-less element, so text and links simply stand there
     (the form's own fade still runs), and .info-list's top margin goes,
     because the grid gap now spaces it from the form. Keyboard order
     stays the DOM order (links before fields); for a phone that trade
     was taken knowingly. */
  .contact-intro { display: contents; }
  .contact-form-wrap { order: 1; }
  .info-list { order: 2; margin-top: 0; }
}
@media (max-width: 560px) {
  .form-row { grid-template-columns: 1fr; }
}

/* ---- Kontakt & Footer, mobile ----
   16px on the fields is not taste: iOS Safari auto-zooms the whole page
   into any focused input whose font-size is under 16px, and the visitor
   then has to pinch back out to find the submit button. Desktop keeps
   15px. The pill submit goes full width in the single-column form — the
   natural landing spot for a thumb after the textarea, and it no longer
   reads as a small button floating left of a wide field. */
@media (max-width: 960px) {
  .form-field input,
  .form-field textarea { font-size: 16px; }
  .form-note { font-size: 13px; line-height: 1.5; }
  /* Inline link inside the consent note: 15px tall as pure text. Padding
     on an inline element extends its hit area without moving the line
     box, which is exactly what we want here. */
  .form-note a { padding-block: 14px; }
  /* Footer legal links were 19px tall. inline-block so the padding
     counts toward the line box and the two don't overlap each other. */
  footer nav a { display: inline-block; padding-block: 13px; } /* 19px line + 26px = 45px */
  footer nav { gap: 1.25rem; }
  /* Notched phones: keep the legal links clear of the home indicator. (The
     music toggle no longer floats over this row on mobile — it stays
     docked in the navbar, see musicToggleDocked in main.js.) */
  footer .container { padding-bottom: env(safe-area-inset-bottom, 0px); }
}
@media (max-width: 560px) {
  #formSubmit { width: 100%; }
  .info-list a { padding-block: 16px; }
}

/* ---- Legal pages (Impressum / Datenschutz) ---- */
/* The legal pages' bar holds only the centred logo (it already links
   home, so there is no separate "Zur Startseite" button). The logo is
   position:absolute, so without an in-flow item the bar would collapse
   to its 12px padding and push the logo off the top. This empty item
   stands in for the 44px control row the homepage bar has, keeping the
   same height (56px mobile, ~102px desktop) and the logo in the same spot. */
.navbar-legal .container::before { content: ""; height: 44px; }
.legal-page { padding-top: clamp(7.5rem, 14vw, 10rem); }
.legal-body { max-width: 74ch; margin-top: 2.5rem; }
.legal-body h2 { margin-top: 2.25rem; margin-bottom: 0.75rem; text-transform: uppercase; }
.legal-body h2:first-child { margin-top: 0; }
.legal-body p { color: var(--ink-70); margin-bottom: 1rem; }
.legal-body a { text-decoration: underline; color: var(--ink); }
.legal-body ul { list-style: disc; padding-left: 1.4rem; margin-bottom: 1rem; }
.legal-body li { color: var(--ink-70); margin-bottom: 0.4rem; }
