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 (
-
+
{h1.before} {h1.em && {h1.em}} {h1.after} - - {subline} - + {subline && ( + + {subline} + + )} {cta && (
{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 (
+ {coupon && ( + /* Kein : Ein gesperrtes Feld ist für Screenreader ein Feld, das + man nicht bedienen kann – hier gibt es aber gar nichts zu bedienen. Deshalb + steht der Vorteil als Text da, in der Form eines ausgefüllten Feldes. */ +
+
+
+

{coupon.eyebrow}

+

+ + {coupon.state} +

+
+

{coupon.value}

+
+ +

+ {coupon.hint} +

+
+ )} +