diff --git a/src/app/globals.css b/src/app/globals.css
index 3586774..245db33 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -30,6 +30,13 @@
--gold-700: #a37a2c;
--gold-900: #6e501a;
+ /* Rabatt-Rot. Die einzige Farbe der Seite außerhalb von Tinte, Elfenbein, Bein und
+ Gold – und zwar, weil Rot „reduziert“ bedeutet und Gold das nicht sagen kann.
+ Warm und tief gewählt, damit sie neben dem Gold nicht kreischt. Sie hat genau
+ eine Aufgabe: das Reduziert-Fähnchen am Scroll-Fortschritt ().
+ Elfenbein darauf: 5,5:1. */
+ --sale: #b3321f;
+
/* Gradients (fixed light direction: top-left) */
--gold-gradient: linear-gradient(135deg, #f4e5b8 0%, #e6c97a 22%, #c9a24d 48%, #a37a2c 74%, #6e501a 100%);
--gold-gradient-text: linear-gradient(100deg, #f4e5b8 0%, #e6c97a 32%, #c9a24d 62%, #a37a2c 100%);
@@ -99,6 +106,7 @@
--color-gold-200: var(--gold-200);
--color-gold-700: var(--gold-700);
--color-gold-900: var(--gold-900);
+ --color-sale: var(--sale);
--color-grey-warm: var(--grey-warm);
--color-grey-deep: var(--grey-deep);
--color-line-dark: var(--line-dark);
@@ -199,6 +207,37 @@ body:has(main #messe-special) header [data-header-cta] {
display: none;
}
+/* Derselbe Erkennungsweg für den Kollaborationszusatz „× herCAREER“ neben der
+ Wortmarke – nur umgekehrt: Er ist überall aus und geht auf der Messeseite an.
+ Anelia arbeitet FÜR die herCAREER; auf dem Impressum oder den Leistungsseiten
+ hätte eine fremde Marke im Kopf nichts zu suchen. */
+header [data-collab] {
+ display: none;
+}
+body:has(main #messe-special) header [data-collab] {
+ display: inline-flex;
+}
+
+/* Unter 640 px steht der Zusatz unter der Wortmarke, und dort trägt der Kopf kein
+ Monogramm mehr: Mit ihm begann „ANELIA WOLF“ um 48 px eingerückt, während das
+ Malzeichen darunter an der Seitenkante saß – zwei Zeilen, die an keiner Kante
+ fluchteten. Ohne Monogramm beginnen beide links auf derselben Linie und lesen
+ sich als eine Marke. Betrifft nur den Kopf der Messeseite auf dem Telefon: Der
+ Fuß behält sein Monogramm, alle anderen Seiten auch. */
+@media (max-width: 639px) {
+ body:has(main #messe-special) header .wordmark__mark {
+ display: none;
+ }
+
+ /* Und die Laufweite von 0.22em auf 0.15em. „ANELIA WOLF“ hat zehn Buchstabenlücken;
+ bei 15 px sind das 33 px allein an Sperrung, und die fehlten, damit die Zeile
+ „ANELIA WOLF × herCAREER“ neben Sprachumschalter und Menü auf 335 px passt.
+ Nur hier – auf allen anderen Seiten und ab 640 px bleibt die Wortmarke, wie sie ist. */
+ body:has(main #messe-special) header .wordmark__name {
+ letter-spacing: 0.15em;
+ }
+}
+
/* Legal pages (Spec A/D6): „Datenschutzerklärung“ is one 20-character word and
at 44 px it is wider than the 375 px column – it widened the whole document.
Scoped here because the rule belongs to src/components/legal/LegalPage.tsx,
diff --git a/src/components/brand/Collab.tsx b/src/components/brand/Collab.tsx
new file mode 100644
index 0000000..4619a1f
--- /dev/null
+++ b/src/components/brand/Collab.tsx
@@ -0,0 +1,45 @@
+import { cn } from "@/lib/utils";
+import { HerCareerMark } from "./HerCareerMark";
+
+export interface CollabProps {
+ /** Was ein Screenreader hört – der Zusatz ist eine Angabe, kein Bild ohne Inhalt. */
+ label: string;
+ className?: string;
+}
+
+/**
+ * Kollaborationszusatz neben der Wortmarke: `× herCAREER`.
+ *
+ * Das Malzeichen ist die übliche Kurzschrift für „zusammen mit“ (Marke × Marke)
+ * und trägt hier die ganze Aussage – deshalb ist es dekorativ und der Sinn steht
+ * im `aria-label` der Gruppe, nicht in einem Zeichen, das ein Screenreader als
+ * „mal“ vorlesen würde.
+ *
+ * Sichtbar ist der Zusatz nur auf der Messeseite; eingeschaltet wird er über
+ * `[data-collab]` in globals.css, weil der Kopf global in hängt und von
+ * dort die Route nicht kennt. Anelia arbeitet FÜR die herCAREER – sie ist nicht
+ * dauerhaft mit ihr verbunden, und auf dem Impressum hätte die fremde Marke
+ * nichts zu suchen.
+ */
+export function Collab({ label, className }: CollabProps) {
+ return (
+
+ {/* Etwas kleiner als die Marke daneben und gedämpft: Das Malzeichen verbindet,
+ es soll nicht mit einer der beiden Marken um Aufmerksamkeit streiten. */}
+
+ ×
+
+ {/* Zweizeilige Marke: Die Gesamthöhe muss über der Versalhöhe von „ANELIA WOLF“
+ daneben liegen, sonst ist jede der beiden Zeilen nur halb so groß wie die
+ Wortmarke und der Zusatz wird unleserlich. */}
+
+
+ );
+}
+
+export default Collab;
diff --git a/src/components/brand/HerCareerMark.tsx b/src/components/brand/HerCareerMark.tsx
new file mode 100644
index 0000000..1acef54
--- /dev/null
+++ b/src/components/brand/HerCareerMark.tsx
@@ -0,0 +1,43 @@
+/**
+ * Wortmarke der herCAREER Expo – Kollaborationszusatz im Kopf der Messeseite.
+ *
+ * Aus `herCAREER_Logo-1.svg` (Illustrator-Export) übernommen, mit drei
+ * Eingriffen, ohne die sie hier nicht funktioniert:
+ *
+ * 1. Die Ebene „Hilfslinien“ ist raus. Sie enthielt ein Rechteck über die
+ * ganze Artboard-Fläche mit `stroke: #fff` – auf dem tintenfarbenen Kopf
+ * hätte die Marke in einem weißen Kasten gestanden.
+ * 2. `fill="currentColor"` statt der Illustrator-Vorgabe Schwarz: Auf dunklem
+ * Grund war die Marke sonst unsichtbar, und so nimmt sie die Schriftfarbe
+ * ihrer Umgebung an – hell im Kopf, tinte im geöffneten Menü.
+ * 3. Die viewBox ist auf die Zeichnung beschnitten (vorher 0 0 1424.7 702.5
+ * mit rund 12 % Luft oben und unten). Nur so lässt sich die Höhe optisch
+ * auf die Wortmarke daneben abstimmen; der Abstand ringsum kommt aus dem
+ * Layout, nicht aus der Datei.
+ *
+ * Die Pfade selbst sind unverändert – es ist eine fremde Marke.
+ */
+export function HerCareerMark({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+export default HerCareerMark;
diff --git a/src/components/home/ProcessContact.tsx b/src/components/home/ProcessContact.tsx
index d631b79..d7c2378 100644
--- a/src/components/home/ProcessContact.tsx
+++ b/src/components/home/ProcessContact.tsx
@@ -5,7 +5,7 @@ import { Reveal } from "@/components/motion/Reveal";
import { Rule } from "@/components/motion/Rule";
import { ContactLinks } from "@/components/ui/ContactLinks";
import { Eyebrow, EYEBROW_TEXT } from "@/components/ui/Eyebrow";
-import { ContactForm } from "@/components/ui/ContactForm";
+import { ContactForm, type ContactCoupon } from "@/components/ui/ContactForm";
/* H2 on ink: Bodoni, `clamp(2.75rem, 1.6rem + 4.4vw, 6rem)`, ivory (spec §6.7). */
const H2 =
@@ -27,11 +27,21 @@ export interface ProcessContactProps {
privacyHref?: string;
/** Nenner des Abschnittszählers („04 / 04“). Die Startseite zählt ohne Nenner. */
total?: string;
+ /** Vorteil, der schon an der Anfrage hängt (Messeseite). Ohne Angabe entfällt er. */
+ coupon?: ContactCoupon;
}
-export function ProcessContact({ content: contact = contactDe, privacyHref, total }: ProcessContactProps = {}) {
+export function ProcessContact({ content: contact = contactDe, privacyHref, total, coupon }: ProcessContactProps = {}) {
return (
-
+ /* `data-sale` ist der Haken, an dem das rote Reduziert-Fähnchen
+ aufhängt – die Komponente hängt global in und kann von dort nicht
+ wissen, ob diese Seite einen Gutschein trägt. */
+
-
+
{/* Contact block: under the Ablauf up to 1280 (the CTA button is 354 px
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index 986b9a1..77049e2 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -7,6 +7,7 @@ import { flags, legal as legalDe, nav as navDe } from "@/lib/site";
import { routesByLang, type Lang } from "@/lib/i18n";
import { chrome as chromeDe, type Chrome } from "@/content/chrome";
import { Wordmark } from "@/components/brand/Wordmark";
+import { Collab } from "@/components/brand/Collab";
import { TransitionLink } from "@/components/motion/TransitionLink";
import { subscribe } from "@/components/motion/transition-store";
import { Container } from "./Container";
@@ -108,9 +109,25 @@ export function Header({ mark, chrome = chromeDe, nav = navDe, legal = legalDe,
>
-
-
+ {/* Unter 640 px eine Flex-Zeile statt des 12-Spalten-Rasters. Grund ist Arithmetik:
+ Bei 375 px stehen 335 px zur Verfügung, und elf Spaltenlücken à 16 px fressen
+ davon 176 – für „ANELIA WOLF × herCAREER“ neben Sprachumschalter und Menü
+ blieb in zwei Sechstel-Zellen kein Platz. Ohne Raster bekommt jede Gruppe
+ ihre natürliche Breite, `justify-between` schiebt sie auseinander. Für die
+ anderen Seiten ändert das nichts: Dort stehen unter 640 px ohnehin nur diese
+ zwei Gruppen in der Zeile, links und rechts.
+ `min-h` statt `h`, damit die Zeile notfalls wachsen darf; `--header-h` misst
+ der ResizeObserver weiter unten am Element. */}
+
+ {/* Wortmarke und Kollaborationszusatz stehen auf jeder Breite nebeneinander.
+ Der Zusatz steht AUSSERHALB des Wortmarken-Links: Er führt nicht zur
+ Startseite, er ist eine Angabe. */}
+
+
-
- {/* 03 Auf der Messe – derselbe Termine-Ledger wie auf der Startseite, eine Quelle. */}
- {showFair && (
-
-
-
-
-
-
- )}
{/* Kontakt: derselbe Block wie Sektion 06 der Startseite, mit dem Formular. */}
-
+
>
);
}
diff --git a/src/components/services/ServiceHero.tsx b/src/components/services/ServiceHero.tsx
index 15ca74c..35d65ca 100644
--- a/src/components/services/ServiceHero.tsx
+++ b/src/components/services/ServiceHero.tsx
@@ -1,3 +1,4 @@
+import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
import { Container } from "@/components/layout/Container";
import { Folio } from "@/components/layout/Folio";
@@ -18,7 +19,8 @@ export interface ServiceHeroContent {
h1: Headline;
/** The italic word may also carry `.gold-text` (keynotes – the page's one gradient word). */
h1GoldEm?: boolean;
- subline: string;
+ /** Fließtext unter der Headline. Ohne Angabe trägt die Headline den Hero allein. */
+ subline?: string;
/** Haupt-Knopf. Ohne Angabe trägt der Hero nur Headline und Subline. */
cta?: Cta;
/** Fakten-Stapel oben rechts. Ohne Angabe (und ohne Foto) entfällt die Spalte. */
@@ -34,6 +36,13 @@ export interface ServiceHeroProps {
content: ServiceHeroContent;
/** Stage photo (keynotes) – `null`/undefined = no photo. */
photo?: string | null;
+ /**
+ * Steht in derselben Spalte direkt unter Headline, Subline und Knopf.
+ * Auf der Messeseite hängt hier der Gutschein: Wer den Code gescannt hat,
+ * soll den Nachlass sehen, ohne erst an einer Sektionsüberschrift vorbei
+ * zu scrollen.
+ */
+ children?: ReactNode;
className?: string;
}
@@ -41,8 +50,12 @@ export interface ServiceHeroProps {
* Service-page hero (ink, ≥ 60svh): mono numeral, H1 with one italic word,
* subline, the page's primary CTA, facts stack top-right, folio with the next
* section. Header monogram only – no hero draw.
+ *
+ * Ohne `subline` steht die Headline allein: sie erbt dann den Platz, den der
+ * Fließtext gehalten hat – eine Stufe größer und über die Spalten 3–11 statt
+ * 3–9. So trägt die Messeseite ihren Hero mit einem einzigen Satz.
*/
-export function ServiceHero({ content, photo, className }: ServiceHeroProps) {
+export function ServiceHero({ content, photo, children, className }: ServiceHeroProps) {
/* `masthead` und `eyebrow` stehen weiter in den Inhaltsdateien, werden aber seit
dem 06.09.2026 nicht mehr gerendert (Kundenwunsch): die Kopfzeile trug dieselben
Angaben doppelt, und über der H1 stand faktisch die Überschrift ein zweites Mal. */
@@ -51,6 +64,11 @@ export function ServiceHero({ content, photo, className }: ServiceHeroProps) {
/* Die Messeseite trägt weder Fakten noch Foto – dann gibt es die rechte Spalte
nicht, statt eine leere Reveal-Hülle in die Spalten 10–12 zu stellen. */
const hasAside = hasPhoto || Boolean(facts?.length);
+ /* Ohne Fließtext steht die Headline allein im Hero – dann bekommt sie dessen
+ Platz, statt dass der Hero um die fehlende Zeile zusammenfällt. Der Slot
+ darunter ändert daran nichts: Auf der Messeseite ist die große Begrüßung
+ ausdrücklich gewollt, der Gutschein hängt sich unter sie. */
+ const solo = !subline;
return (
-
{hasAside && (
diff --git a/src/components/ui/ContactForm.tsx b/src/components/ui/ContactForm.tsx
index 923e112..4b94e98 100644
--- a/src/components/ui/ContactForm.tsx
+++ b/src/components/ui/ContactForm.tsx
@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils";
import { site, routes } from "@/lib/site";
import { contact as contactDe, type HomeContent } from "@/content/home";
import { BUTTON_BASE, BUTTON_PRIMARY } from "@/components/ui/Button";
+import { COUPON_SURFACE, CouponPerforation } from "@/components/ui/Coupon";
import { TextLink } from "@/components/ui/TextLink";
/**
@@ -26,18 +27,54 @@ import { TextLink } from "@/components/ui/TextLink";
/** Endpunkt beim Hoster – gleiche Domain, deshalb ohne CORS-Vorspiel. */
const ENDPOINT = "/api/contact";
+/**
+ * Ein Vorteil, der schon an der Anfrage hängt (Messeseite: der gescannte Gutschein).
+ * Das Formular zeigt ihn als bereits ausgefülltes, gesperrtes Feld über den anderen –
+ * es gibt nichts abzutippen, kein Stichwort einzutragen.
+ */
+export interface ContactCoupon {
+ /** Mono-Caps über dem Wert, z. B. „Messe-Special“. */
+ eyebrow: string;
+ /** Der Vorteil in einer Zeile, z. B. „−20 % · Stichwort herCAREER“. */
+ value: string;
+ /** Zustand rechts oben, z. B. „Hinterlegt“. */
+ state: string;
+ /**
+ * Das rote Reduziert-Fähnchen am Scroll-Fortschritt, zweigeteilt wie ein Ticket:
+ * links der Betrag, rechts hinter der Perforation der Abriss („−20“ | „%“).
+ * schreibt beides als `data-sale` und `data-sale-stub` an den
+ * Abschnitt; liest es von dort. Wenige Zeichen je Hälfte – das
+ * Fähnchen ist rund 20 px hoch.
+ */
+ tag: { value: string; stub: string };
+ /** Ein Satz darunter: was der Besucher jetzt noch tun muss. */
+ hint: string;
+ /**
+ * Diese Zeile wird der Nachricht angehängt.
+ *
+ * Ohne sie wäre der Hinweis eine Behauptung, die nur auf dem Bildschirm gilt:
+ * `/api/contact` liegt beim Hoster und nicht in diesem Repo, also ist nicht
+ * nachprüfbar, ob ein zusätzliches Feld im Postfach ankommt. Die Nachricht kommt
+ * sicher an – deshalb reist der Vorteil in ihr mit.
+ */
+ line: string;
+}
+
type Status = "idle" | "sending" | "sent" | "error";
export function ContactForm({
className,
content: form = contactDe.form,
privacyHref = routes.datenschutz,
+ coupon,
}: {
className?: string;
/** Ziel des Datenschutz-Links – in der englischen Fassung /en/privacy/. */
privacyHref?: string;
/** Inhalt. Ohne Angabe die deutsche Fassung – die englische reicht sie durch. */
content?: HomeContent["contact"]["form"];
+ /** Vorteil, der schon an der Anfrage hängt. Ohne Angabe entfällt das Feld. */
+ coupon?: ContactCoupon;
}) {
const uid = useId();
const [status, setStatus] = useState("idle");
@@ -53,11 +90,15 @@ export function ContactForm({
const data = new FormData(formEl);
const value = (key: string) => String(data.get(key) ?? "").trim();
+ /* Der Vorteil hängt unter der Nachricht, durch eine Leerzeile abgesetzt – so steht
+ er im Postfach unter dem, was der Absender geschrieben hat, und nicht davor. */
+ const written = value("message");
+
const payload = {
name: value("name"),
email: value("email"),
company: value("company"),
- message: value("message"),
+ message: coupon ? `${written}\n\n${coupon.line}` : written,
page: typeof window === "undefined" ? "" : window.location.pathname,
// Honeypot: Menschen sehen das Feld nicht, Bots füllen es aus.
website: value("website"),
@@ -113,6 +154,28 @@ export function ContactForm({
return (