Messe-Landingpage /her-career/ zur herCAREER Expo 2026

Ziel des QR-Codes auf der Rückwandgrafik. Der gedruckte Code zeigt auf
https://her-career.anelia-wolf.com – die Adresse ist damit unveränderlich.
Die Seite liegt als /her-career/ im normalen Export, die Subdomain liefert
dieselbe Datei aus demselben Docroot aus; DNS, Zertifikat, Vhosts für nginx
und Apache und die Prüfbefehle stehen in docs/SUBDOMAIN-HERCAREER.md.

Aufbau in beiden Sprachen: Hero (ink) → 01 Messe-Special → 02 Themen →
03 Auf der Messe → 04 Kontakt mit dem Formular. Die vier Themenblöcke der
Rückwand liegen auf den drei Leistungsseiten, wie in der Navigation; jede
Zeile verlinkt absolut auf die Hauptdomain.

Der Wortlaut des Messe-Specials fehlt noch und steht sichtbar als „[…]“ auf
der Seite. Der Build-Guard hält den Launch-Flip an, bis er da ist
(CONTENT_TODO in src/lib/legal.ts, docs/OFFENE-DATEN.md Punkte 2.7–2.9).
Deadline ist nicht der Livegang, sondern der 22.10.2026.

- Canonical auf anelia-wolf.com/her-career/, damit die Subdomain-Kopie nicht
  ein zweites Mal im Index steht
- flags.showHerCareer trägt Termine-Ledger, Sitemap-Eintrag und die
  Meta-Beschreibung mit Stand und Uhrzeit. Ab dem 24.10.2026 auf false
- ServiceHero: facts und cta sind optional – ohne beides und ohne Foto
  entfällt die rechte Spalte
- ProcessContact: optionaler Zähler, die Messeseite nummeriert ihre Abschnitte
- Kopf-CTA auf dieser Seite ausgeblendet. Das macht eine Regel in globals.css,
  weil der Kopf außerhalb von <main> liegt und auf der Subdomain der Pfad
  ohnehin „/“ ist

Enthält außerdem die bis dahin nicht committeten Änderungen an Header,
LangSwitch, OverlayMenu, Wordmark und globals.css.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 22:39:26 +02:00
parent b9e1d4d15c
commit 33ec0df7e3
21 changed files with 941 additions and 50 deletions

View File

@@ -129,10 +129,17 @@ export function Header({ mark, chrome = chromeDe, nav = navDe, legal = legalDe,
</nav>
<div className="col-span-6 flex items-center justify-end gap-6 xl:col-span-4 2xl:gap-8">
{/* Steht an der Stelle, an der bis zum 06.09.2026 „Kontakt“ stand. */}
{/* Steht an der Stelle, an der bis zum 06.09.2026 „Kontakt“ stand.
Zwei Formen, weil neben Wortmarke und „Menü“ auf einem Telefon kein Paar
Platz hat: unter 1024 px nur die Zielsprache als ein Knopf, darüber beide.
Im Overlay-Menü steht er nicht mehr dort fiel er auf kurzen Geräten
unter die Kante, ohne dass man ihm ansah, dass da noch etwas kommt. */}
{flags.showLangSwitch && <LangSwitch lang={lang} variant="compact" className="lg:hidden" />}
{flags.showLangSwitch && <LangSwitch lang={lang} className="hidden lg:flex" />}
<a
data-menu-inert=""
/* Haken für globals.css: Auf der Messeseite entfällt dieser Knopf. */
data-header-cta=""
href={chrome.header.cta.href}
className="hidden whitespace-nowrap border border-gold bg-[image:var(--gold-sheen)] bg-[length:250%_100%] bg-[position:0%_50%] px-[1.4em] py-[1.05em] font-sans text-[0.75rem] font-medium uppercase leading-none tracking-[0.16em] text-ink transition-[background-position] duration-[600ms] ease-[var(--ease-out-quart)] hover:bg-[position:100%_50%] focus-visible:bg-[position:100%_50%] xl:inline-block 2xl:px-[1.6em] 2xl:tracking-[0.2em]"
>
@@ -161,7 +168,7 @@ export function Header({ mark, chrome = chromeDe, nav = navDe, legal = legalDe,
</div>
</Container>
</header>
<OverlayMenu open={open} onClose={() => setOpen(false)} chrome={chrome} legal={legal} lang={lang} />
<OverlayMenu open={open} onClose={() => setOpen(false)} chrome={chrome} legal={legal} />
</>
);
}

View File

@@ -5,27 +5,57 @@ import { cn } from "@/lib/utils";
import { counterpart, langSwitch, otherLang, type Lang } from "@/lib/i18n";
import { TransitionLink } from "@/components/motion/TransitionLink";
export interface LangSwitchProps {
lang: Lang;
className?: string;
/**
* `pair` zeigt beide Sprachen nebeneinander (Platz ab 1024 px).
* `compact` zeigt nur die andere Sprache als einen Knopf ein Tipp, ein Wechsel.
* Auf dem Telefon ist das die einzige Form, die neben Wortmarke und „Menü“ passt.
*/
variant?: "pair" | "compact";
}
const BASE =
"inline-flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.14em] transition-colors duration-300";
/**
* Sprachumschalter im Kopf. Steht an der Stelle, an der vorher „Kontakt“ stand.
*
* Zeigt beide Sprachen: die aktive als stiller Zustand, die andere als Link auf das
* Gegenstück derselben Seite (`counterpart()`), nicht pauschal auf die Startseite.
* Zielt auf das Gegenstück derselben Seite (`counterpart()`), nicht pauschal auf die
* Startseite.
*
* Zur Flagge: Eine Flagge steht für ein Land, nicht für eine Sprache deshalb steht
* neben ihr immer das Sprachkürzel, und der zugängliche Name nennt die Sprache
* ausgeschrieben. Wer die Flaggen weglassen will, entfernt hier zwei Zeilen.
*/
export function LangSwitch({ lang, className }: { lang: Lang; className?: string }) {
export function LangSwitch({ lang, className, variant = "pair" }: LangSwitchProps) {
const pathname = usePathname() ?? "/";
const other = otherLang(lang);
const href = counterpart(pathname, other);
const base =
"inline-flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.14em] transition-colors duration-300";
/* Kompakt: Die aktive Sprache steht nicht da sie steht ja im Dokument. Gezeigt wird,
wohin es geht. Die negativen Ränder heben das Tippziel auf 54 × 42 px, ohne die
Grundlinie oder den Abstand zum Nachbarn zu verschieben (WCAG 2.5.8: 24 × 24 px
Minimum, 44 × 44 px ist die Empfehlung die Breite von 38 px lag darunter). */
if (variant === "compact") {
return (
<TransitionLink
href={href}
label={langSwitch[other].label}
hrefLang={other}
aria-label={langSwitch[other].title}
className={cn(BASE, "-mx-2 -my-3 shrink-0 px-2 py-3 text-fg", className)}
>
<Flag lang={other} />
{langSwitch[other].short}
</TransitionLink>
);
}
return (
<div className={cn("flex items-center gap-2", className)}>
<span className={cn(base, "text-accent")} aria-current="true" title={langSwitch[lang].title}>
<span className={cn(BASE, "text-accent")} aria-current="true" title={langSwitch[lang].title}>
<Flag lang={lang} />
{langSwitch[lang].short}
</span>
@@ -35,7 +65,7 @@ export function LangSwitch({ lang, className }: { lang: Lang; className?: string
label={langSwitch[other].label}
hrefLang={other}
aria-label={langSwitch[other].title}
className={cn(base, "-my-3 py-3 text-fg-muted hover:text-accent focus-visible:text-accent")}
className={cn(BASE, "-my-3 py-3 text-fg-muted hover:text-accent focus-visible:text-accent")}
>
<Flag lang={other} />
{langSwitch[other].short}

View File

@@ -2,9 +2,7 @@
import { usePathname } from "next/navigation";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { flags, legal as legalDe, site } from "@/lib/site";
import type { Lang } from "@/lib/i18n";
import { LangSwitch } from "./LangSwitch";
import { legal as legalDe, site } from "@/lib/site";
import { chrome as chromeDe, type Chrome } from "@/content/chrome";
import { gsap, EASE, MM, getLenis, refreshSoon } from "@/lib/gsap";
import { TransitionLink } from "@/components/motion/TransitionLink";
@@ -17,8 +15,6 @@ export interface OverlayMenuProps {
chrome?: Chrome;
/** Impressum/Datenschutz der jeweiligen Sprache. */
legal?: readonly { href: string; label: string }[];
/** Aktuelle Sprache für den Umschalter im Fuß des Menüs. */
lang?: Lang;
}
/** The header's „Menü“ button part of the dialog's operable set (see the trap below). */
@@ -34,7 +30,7 @@ export const MENU_TOGGLE_ID = "menue-toggle";
* as well as by mouse. Every other header control is `inert` while the sheet is
* open, so the mouse-operable and the keyboard-operable set are the same one.
*/
export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe, lang = "de" }: OverlayMenuProps) {
export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe }: OverlayMenuProps) {
const panel = useRef<HTMLDivElement>(null);
const pathname = usePathname();
const onCloseRef = useRef(onClose);
@@ -168,7 +164,9 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
data-lenis-prevent=""
className="fixed inset-0 z-40 flex flex-col overflow-y-auto overscroll-contain bg-ivory text-ink"
>
<Container className="flex min-h-full flex-1 flex-col pb-10 pt-[calc(var(--header-h,60px)+24px)]">
{/* short: = kurze Ansicht (≤ 760 px hoch). Ohne das Engersetzen fällt der Fuß
(Telefonnummer, E-Mail, Rechtslinks) auf einem iPhone SE unter die Kante. */}
<Container className="flex min-h-full flex-1 flex-col pb-10 pt-[calc(var(--header-h,60px)+24px)] short:pb-6 short:pt-[calc(var(--header-h,60px)+12px)]">
<ol className="border-t border-line-light">
{chrome.menu.items.map((item) => (
<li key={item.href} className="overflow-hidden border-b border-line-light">
@@ -177,7 +175,7 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
href={item.href}
label={item.label}
onClick={() => onCloseRef.current()}
className="group flex items-baseline gap-5 py-5 lg:gap-8 lg:py-7"
className="group flex items-baseline gap-5 py-5 short:py-3 lg:gap-8 lg:py-7"
>
<span className="font-mono text-xs tracking-[0.06em] tabular-nums text-gold-deep">{item.numeral}</span>
<span className="font-display text-[clamp(2rem,1.4rem+3vw,4rem)] leading-none text-ink transition-[font-style] group-hover:italic">
@@ -197,7 +195,7 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
<div
data-menu-foot=""
className="mt-auto flex flex-col gap-4 pt-12 lg:flex-row lg:items-end lg:justify-between"
className="mt-auto flex flex-col gap-4 pt-12 short:gap-3 short:pt-6 lg:flex-row lg:items-end lg:justify-between"
>
<div className="flex flex-col gap-2">
<a href={site.phoneHref} className="font-display text-[1.75rem] leading-none text-ink">
@@ -208,10 +206,6 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
</a>
</div>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 font-mono text-xs tracking-[0.06em] text-grey-deep">
{/* Unter 1024 px versteckt der Kopf den Umschalter (dort ist kein Platz neben
der Wortmarke). Ohne diese Zeile gäbe es auf dem Telefon gar keinen Weg in
die andere Sprache er steht deshalb genau dort, wo er im Kopf fehlt. */}
{flags.showLangSwitch && <LangSwitch lang={lang} className="lg:hidden" />}
<a href={site.linkedin} target="_blank" rel="noopener noreferrer" className="hover:text-gold-deep">
{chrome.menu.linkedin} <span aria-hidden="true"></span>
</a>