Auf her-career.anelia-wolf.com liegt die Seite unter „/“. usePathname() meldet dort „/“, und counterpart() leitete daraus folgerichtig die englische STARTSEITE ab – wer am Stand gescannt und dann auf „EN“ getippt hat, landete auf anelia-wolf.com/en/. Der Umschalter fragt jetzt den hreflang-Verweis im Kopf des Dokuments: Der hängt an der Seite, nicht an der Adresse, unter der sie ausgeliefert wird. Am selben Wurzelproblem: Der Sprachwechsel ist wegen der zwei Wurzel-Layouts ohnehin ein neues Dokument. Den macht jetzt der Browser statt router.push(), und über die Sprachgrenze wird nicht mehr vorgeladen – auf der Subdomain scheiterte der Vorabruf an CORS, mehrfach pro Seitenaufbau. Dazu zwei Sachen am Kopf der Messeseite: Das Monogramm steht hinter dem Schlusspunkt der Begrüßung, 0,783 em hoch (die Oberlänge der Playfair Display, im Browser gemessen) und auf der Grundlinie – und dafür nicht mehr in der Kopfzeile, wo es sonst zweimal auf einem Bildschirm stünde. Es steht dort dekorativ, sonst wandert der Text seines <title> über SplitText ins aria-label der Überschrift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
7.7 KiB
TypeScript
189 lines
7.7 KiB
TypeScript
import type { ReactNode } from "react";
|
||
import { cn } from "@/lib/utils";
|
||
import { Container } from "@/components/layout/Container";
|
||
import { Folio } from "@/components/layout/Folio";
|
||
import { GRID } from "@/components/layout/Section";
|
||
import { Lines } from "@/components/motion/Lines";
|
||
import { Reveal } from "@/components/motion/Reveal";
|
||
import type { Cta, FactItem, Headline } from "@/content/home";
|
||
import { Button } from "@/components/ui/Button";
|
||
import { Facts } from "@/components/ui/Facts";
|
||
import { Portrait } from "@/components/ui/Portrait";
|
||
|
||
/** The slice of `coaching` / `trainings` / `keynotes` the hero needs (structural). */
|
||
export interface ServiceHeroContent {
|
||
/** „01“ / „02“ / „03“ – mono, cols 1–2. */
|
||
numeral: string;
|
||
masthead: { center: string };
|
||
eyebrow: string;
|
||
h1: Headline;
|
||
/** The italic word may also carry `.gold-text` (keynotes – the page's one gradient word). */
|
||
h1GoldEm?: boolean;
|
||
/** 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. */
|
||
facts?: readonly FactItem[];
|
||
/** No fraction here – the page's own section counter is the only counter (see <Folio/>). */
|
||
folio: { next?: { label: string; href: string } };
|
||
/** Photo alt / caption (keynote stage photo). */
|
||
alt?: string;
|
||
caption?: string;
|
||
}
|
||
|
||
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;
|
||
/**
|
||
* Steht als letztes Zeichen in der H1, hinter dem Schlusspunkt. Auf der Messeseite
|
||
* hängt hier das Monogramm – die Begrüßung endet mit der Marke, zu der sie gehört.
|
||
*/
|
||
h1Mark?: ReactNode;
|
||
className?: string;
|
||
}
|
||
|
||
/**
|
||
* 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, children, h1Mark, 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. */
|
||
const { numeral, masthead, h1, h1GoldEm, subline, cta, facts, folio, alt, caption } = content;
|
||
const hasPhoto = Boolean(photo);
|
||
/* 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 (
|
||
<section
|
||
data-theme="dark"
|
||
className={cn(
|
||
"relative flex min-h-[60svh] flex-col bg-transparent pt-[calc(var(--header-h,7.5rem)+1.5rem)] text-fg",
|
||
className,
|
||
)}
|
||
>
|
||
<Container className="flex flex-1 flex-col">
|
||
<div className={cn(GRID, "flex-1 gap-y-12 py-14 lg:py-20")}>
|
||
<Reveal
|
||
play="load"
|
||
delay={0.3}
|
||
as="p"
|
||
className="hidden font-mono text-xs tracking-[0.06em] tabular-nums text-fg-muted lg:col-span-2 lg:block lg:pt-1"
|
||
>
|
||
{numeral}
|
||
</Reveal>
|
||
|
||
<div
|
||
className={cn(
|
||
"col-span-12 flex flex-col lg:col-start-3",
|
||
hasPhoto ? "lg:col-span-5" : solo ? "lg:col-span-9" : "lg:col-span-7",
|
||
)}
|
||
>
|
||
<Lines
|
||
as="h1"
|
||
play="load"
|
||
delay={0.35}
|
||
className={cn(
|
||
"max-w-[14ch] font-display font-normal leading-[0.96] tracking-[-0.015em] text-ivory [text-wrap:balance]",
|
||
solo ? "text-[clamp(3.25rem,1.6rem+7vw,9rem)]" : "text-[clamp(2.75rem,1.4rem+5.4vw,7rem)]",
|
||
)}
|
||
>
|
||
{h1.before}
|
||
{h1.em && <em className={cn("italic", h1GoldEm && "gold-text")}>{h1.em}</em>}
|
||
{h1.after}
|
||
{h1Mark && (
|
||
/* Auf der Grundlinie, hinter dem Punkt, 0,783 em hoch: Das ist die
|
||
Oberlänge der Playfair Display (im Browser an „h“ und „b“ gemessen;
|
||
die Versalhöhe liegt bei 0,719 em) – die Marke wird damit genau so
|
||
hoch, wie die Schrift dieser Zeile maximal wird, und keinen Punkt
|
||
höher. Die viewBox der Marke sitzt eng auf der Zeichnung, deshalb
|
||
ist Kastenhöhe gleich sichtbare Höhe.
|
||
|
||
Zwischen Punkt und Marke steht KEIN Leerzeichen im Markup – der
|
||
Abstand kommt aus `ml`, damit an dieser Stelle keine Umbruchstelle
|
||
entsteht und die Marke nicht allein in eine letzte Zeile rutscht.
|
||
|
||
Dekorativ (`aria-hidden`): Die Marke steht im Kopf schon mit Namen.
|
||
In der Überschrift stünde er ein zweites Mal – und zwar mitten im
|
||
Satz, den ein Screenreader vorliest. */
|
||
<span
|
||
aria-hidden="true"
|
||
className="ml-[0.22em] inline-block align-baseline [&_svg]:h-[0.783em] [&_svg]:w-auto"
|
||
>
|
||
{h1Mark}
|
||
</span>
|
||
)}
|
||
</Lines>
|
||
{subline && (
|
||
<Reveal play="load" delay={1.0} as="p" className="mt-10 max-w-[40ch] font-sans text-xl font-light leading-[1.45] text-fg-muted">
|
||
{subline}
|
||
</Reveal>
|
||
)}
|
||
{cta && (
|
||
<Reveal play="load" delay={1.2} className="mt-8">
|
||
<Button {...cta} />
|
||
</Reveal>
|
||
)}
|
||
{children && (
|
||
<Reveal play="load" delay={1.0} className="mt-12 lg:mt-14">
|
||
{children}
|
||
</Reveal>
|
||
)}
|
||
</div>
|
||
|
||
{hasAside && (
|
||
<div
|
||
className={cn(
|
||
"col-span-12 flex flex-col gap-10",
|
||
hasPhoto ? "lg:col-span-5 lg:col-start-8" : "lg:col-span-3 lg:col-start-10",
|
||
)}
|
||
>
|
||
{facts && facts.length > 0 && (
|
||
<Reveal play="load" delay={0.6} className={cn(hasPhoto && "lg:self-end")}>
|
||
<Facts items={facts} />
|
||
</Reveal>
|
||
)}
|
||
{photo && (
|
||
<Portrait
|
||
src={photo}
|
||
alt={alt ?? masthead.center}
|
||
aspect="16/10"
|
||
caption={caption}
|
||
priority
|
||
frame={false}
|
||
sizes="(min-width: 1024px) 40vw, 100vw"
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<Folio next={folio.next} play="load" delay={1.3} />
|
||
</Container>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
export default ServiceHero;
|