diff --git a/AGENTS.md b/AGENTS.md index 078a9f5..e20941e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,10 @@ schreibt den Block neu, dieser Teil bleibt). - Betonungswort einer Headline: genau ein ``. Kursiv, und auf `data-theme="dark"` zusätzlich golden – das macht eine Regel in `globals.css`, nicht die Komponente. Auf hellem Grund bleibt es rein typografisch (Gold-Verlauf auf Elfenbein = 2,4:1). + Einzige Ausnahme: eine Headline, die eine Aufzählung IST – die Trias der Startseite + („Persönlichkeit … Wirkung … Erfolg“), weil sie den Claim unter dem Monogramm + wiederholt. Dann trägt jedes Wort ein ``, den Gold-Verlauf aber weiterhin nur + eines; die übrigen bekommen `em-plain` (`Headline.parts[].gold` im Inhalt steuert das). - Goldene Schrift nur auf `[data-theme="dark"]`; auf Ivory gilt `--gold-deep`. - Sichtbare Copy lebt ausschließlich in `src/content/*.ts`, nie im JSX. diff --git a/src/app/globals.css b/src/app/globals.css index 245db33..d6deca8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -285,6 +285,17 @@ svg { -webkit-text-fill-color: transparent; } +/* Ausnahme für eine Headline, die eine Aufzählung IST – die Trias der Startseite + („Persönlichkeit … Wirkung … Erfolg“) betont drei Wörter, weil sie den Claim unter + dem Monogramm wiederholt. Gold bleibt trotzdem rationiert: den Verlauf trägt genau + das letzte Wort, die übrigen bleiben rein kursiv. Gesetzt über `parts[].gold` im + Inhalt, nicht in der Komponente. */ +[data-theme="dark"] .font-display em.em-plain { + color: inherit; + background: none; + -webkit-text-fill-color: currentColor; +} + /* Hairlines */ .rule-gold { height: 1px; background: var(--gold-hairline); } [data-theme="light"] .rule-gold { background: var(--gold-hairline-light); } diff --git a/src/components/home/Hero.tsx b/src/components/home/Hero.tsx index 5d4cf06..5a37626 100644 --- a/src/components/home/Hero.tsx +++ b/src/components/home/Hero.tsx @@ -1,6 +1,6 @@ "use client"; -import { useLayoutEffect, useRef, useState, type ReactNode } from "react"; +import { Fragment, useLayoutEffect, useRef, useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; import { flags } from "@/lib/site"; import { gsap, MM } from "@/lib/gsap"; @@ -15,6 +15,7 @@ import { Magnet } from "@/components/motion/Magnet"; import { DrawMark } from "@/components/brand/DrawMark"; import { Button } from "@/components/ui/Button"; import { PlaceholderText } from "@/components/ui/Facts"; +import { EYEBROW_TEXT } from "@/components/ui/Eyebrow"; export interface HeroProps { /** `` – Server Component, rendered by page.tsx. */ @@ -26,8 +27,10 @@ export interface HeroProps { } /* Load choreography (spec §6.1), seconds from mount. Masthead hairlines 0 / 0.15 are - Masthead's own; the folio hairline waits for the neck stroke (draw 0.7 + handoff 1.2–1.4). */ -const T = { lines: 0.3, door: 0.5, draw: 0.7, cta: 1.2, folio: 2.0 } as const; + Masthead's own; the folio hairline waits for the neck stroke (draw 0.7 + handoff 1.2–1.4). + Der Claim liegt dazwischen: erst zeichnet der Strich zu Ende, dann steht das Wort da, + dann läuft die Haarlinie aus. */ +const T = { lines: 0.3, door: 0.5, draw: 0.7, cta: 1.2, claim: 1.5, folio: 2.0 } as const; /** Fallback length of the handoff hairline until it is measured (px). */ const HANDOFF = 40; @@ -36,6 +39,8 @@ const MONO_11 = "font-mono text-[11px] tracking-[0.06em]"; /** * Handoff x for ``, measured from the folio's left edge. + * Unverändert vom Claim: der Ursprung der Folio-Haarlinie bleibt der Punkt + * direkt unter dem V, auch wenn der Strich jetzt am Claim endet. * DrawMark writes `--handoff-x` on its wrapper (fallback WolfMark); a client * SVG with `path#handoff` is measured at the path's end point. */ @@ -105,14 +110,18 @@ function markBottomY(markWrap: HTMLElement, pts: { x: number; y: number }[]): nu * Variant A (photo): portrait cols 8–12 bleeding to the page edge with scrims, * monogram 28vh in cols 6–8 overlapping its left edge, neck handoff into the * folio hairline, which draws outward from that x. Variant B (no photo): - * monogram 56vh cols 7–12, right-aligned, same handoff. Mobile: masthead, - * monogram 14svh, H1, CTAs, portrait 3:4 (≤ 48svh) – no folio row (it costs - * 69 px the phone cover does not have; Positionierung carries „01 / 06“). + * monogram 40vh cols 7–12, right-aligned, same handoff. Unter dem Monogramm + * steht der Claim („Persönlichkeit · Wirkung · Erfolg“) – dafür ist das Zeichen + * von 45vh auf 40vh geschrumpft, damit das Cover nicht wächst; der Nackenstrich + * endet an seiner Oberkante. Mobile: masthead, monogram 12svh, Claim, H1, CTAs, + * portrait 3:4 (≤ 48svh) – no folio row (it costs 69 px the phone cover does not + * have; Positionierung carries „01 / 06“). */ export function Hero({ mark, photo, content: hero = heroDe }: HeroProps) { const root = useRef(null); const h1 = useRef(null); const markWrap = useRef(null); + const claim = useRef(null); const folio = useRef(null); const [origin, setOrigin] = useState(null); const [handoffLength, setHandoffLength] = useState(HANDOFF); @@ -133,7 +142,13 @@ export function Hero({ mark, photo, content: hero = heroDe }: HeroProps) { if (!pts) pts = sampleMarkPoints(m); const bottom = markBottomY(m, pts); if (bottom !== null) { - const gap = f.getBoundingClientRect().top - bottom; + /* Der Strich endet an der Oberkante des Claims, wenn einer da steht – + sonst wie bisher an der Folio-Haarlinie. Die Vertikale liest sich damit + Nacken → Strich → Claim → Haarlinie, und der Ursprung der Haarlinie + bleibt derselbe x. Der Claim animiert `y`, sein Wrapper nicht: dessen + Kasten steht schon zur Messzeit an der richtigen Stelle. */ + const target = claim.current ?? f; + const gap = target.getBoundingClientRect().top - bottom; setHandoffLength(Math.round(Math.min(72, Math.max(16, gap)))); } }; @@ -226,30 +241,47 @@ export function Hero({ mark, photo, content: hero = heroDe }: HeroProps) { )} - {/* Monogram: bottom-aligned to the folio hairline; the neck hands off into it. */} + {/* Monogramm + Claim: als Block unten an der Folio-Haarlinie ausgerichtet. + Der Nackenstrich endet am Claim, der Ursprung der Haarlinie bleibt der + Punkt direkt unter dem V. `items-end`/`items-start` statt `justify-self`, + weil der Kasten jetzt zwei Kinder trägt und die Spalte füllen muss. */}
- - {mark} - +
+ + {mark} + +
+ + {/* Der Wrapper trägt das Ref und wird NICHT animiert – `` verschiebt + nur sein eigenes Element, sodass die Messung oben den ruhenden Kasten trifft. + Trennzeichen wie im Ledger von der Komponente gesetzt, nicht aus dem Inhalt. */} +
+ + {hero.claim.map((word, i) => ( + + {i > 0 && ·} + {word} + + ))} + +
{/* H1 */} @@ -257,20 +289,48 @@ export function Hero({ mark, photo, content: hero = heroDe }: HeroProps) { ref={h1} className="relative z-[2] col-span-12 row-start-3 mt-6 lg:col-span-8 lg:col-start-1 lg:row-start-3 lg:mt-0 lg:self-end" > - {/* The H1 is capped by viewport HEIGHT as well as width: `26vh - 108px` - is neutral at 1440×900 (the cover's reference frame) and pulls the - three Bodoni lines back on 800/768 px laptops, where the vw term - alone grows faster than the fold shrinks. */} + {/* Zwei Maße, beide gemessen statt geschätzt (Playfair bei 100 px: + „Mit deiner Persönlichkeit“ = 1033 px, „entwickelst du deine Wirkung –“ + = 1315 px, ganze Zeile = 3314 px): + + `max-w-[10.9em]` liegt zwischen diesen beiden Werten. Damit bricht die + Headline auf JEDER Schriftgröße in dieselben vier Zeilen – der Umbruch + hängt nicht mehr an der Fenstergröße. Ab lg begrenzt die Spalte (854 px + bei 1440) auf 10.95 em; auch das liegt noch über 1033. + + Der Clamp bleibt zusätzlich von der Fenster-HÖHE gedeckelt: `22vh - 120px` + ist bei 1440×900 neutral und zieht die vier Zeilen auf 800/768-px-Laptops + zurück, wo der vw-Term schneller wächst als der Bildschirm schrumpft. */} - {hero.h1.before} - {hero.h1.em} - {hero.h1.after} + {hero.h1.parts ? ( + /* Mehr als eine Betonung: kursiv trägt jedes Teil, den Gold-Verlauf + genau eines – `em-plain` nimmt ihn den übrigen wieder ab. */ + hero.h1.parts.map((part, i) => { + if (!part.em) return {part.text}; + const word = {part.text}; + /* `tail` klammert Wort und Satzzeichen zusammen – siehe `HeadlinePart.tail`. */ + return part.tail ? ( + + {word} + {part.tail} + + ) : ( + {word} + ); + }) + ) : ( + <> + {hero.h1.before} + {hero.h1.em} + {hero.h1.after} + + )} diff --git a/src/content/en/home.ts b/src/content/en/home.ts index 6b3140a..204e424 100644 --- a/src/content/en/home.ts +++ b/src/content/en/home.ts @@ -21,7 +21,21 @@ export const homeEn: HomeContent = { center: "Sales Mentoring & Potential Development", lang: "German & English", }, - h1: { before: "Who you are decides how you ", em: "land", after: "." }, + h1: { + before: "With your personality you shape your impact – and your ", + em: "success", + after: ".", + parts: [ + { text: "With your " }, + { text: "personality", em: true }, + { text: " you shape your " }, + { text: "impact", em: true, tail: " –" }, + { text: " and your " }, + { text: "success", em: true, gold: true }, + { text: "." }, + ], + }, + claim: ["Personality", "Impact", "Success"], subline: "Welcome. Whether as a speaker at your company event, in one-to-one coaching or as a trainer for your team – I am glad to hear what you need.", cta: { @@ -67,8 +81,8 @@ export const homeEn: HomeContent = { { numeral: "02", title: "Training & Workshops", - oneLiner: "Sales with personality – for teams and their leaders.", - keywords: ["B2B sales training", "New and existing customers", "Leadership & team dynamics"], + oneLiner: "Leadership & HR with personality – for leaders and their teams.", + keywords: ["B2B sales training", "HR team building & conflict resolution", "Communication & team spirit"], link: { label: "See training", href: r.trainings }, }, { diff --git a/src/content/home.ts b/src/content/home.ts index d97e455..7086e0e 100644 --- a/src/content/home.ts +++ b/src/content/home.ts @@ -8,10 +8,37 @@ import { assertNoPlaceholders, collectStrings, ph } from "@/lib/legal"; /* ---------- gemeinsame Typen (werden von allen content/*.ts genutzt) ---------- */ +/** + * Ein Stück einer Headline. `em` setzt kursiv, `gold` zusätzlich den Verlauf – + * der greift ohnehin nur auf `data-theme="dark"` (Regel in globals.css). + */ +export interface HeadlinePart { + text: string; + em?: boolean; + gold?: boolean; + /** + * Satzzeichen, das untrennbar an `text` hängt – Gedankenstrich, Punkt. Wird mit + * dem Wort in ein `white-space: nowrap` gesetzt, inklusive führendem Leerzeichen. + * + * Nötig, weil SplitText die Zeilen selbst neu aufbaut und dabei an jeder + * Elementgrenze umbrechen darf: ohne die Klammer landete „–“ bzw. „.“ hinter + * einem `` am Zeilenanfang. Ein geschütztes Leerzeichen hilft nicht, + * SplitText normalisiert es zu einem gewöhnlichen; ein Margin auch nicht, der + * verschiebt den Umbruch nur. `nowrap` nimmt dem Browser die Stelle wirklich weg. + */ + tail?: string; +} + export interface Headline { before: string; em: string; after: string; + /** + * Headline mit mehr als einer Betonung – nur für Aufzählungen wie die Trias + * der Startseite. Wird bevorzugt gerendert; `before`/`em`/`after` bleiben als + * einfache Fassung stehen (Schrift-Test, Fallback). Gold trägt genau ein Teil. + */ + parts?: readonly HeadlinePart[]; } export interface Cta { @@ -100,7 +127,22 @@ export const hero = { center: "Sales Mentoring & Potential Development", lang: "Deutsch & Englisch", }, - h1: { before: "Wer du bist, entscheidet, wie du ", em: "wirkst", after: "." } satisfies Headline, + h1: { + before: "Mit deiner Persönlichkeit entwickelst du deine Wirkung – und deinen ", + em: "Erfolg", + after: ".", + parts: [ + { text: "Mit deiner " }, + { text: "Persönlichkeit", em: true }, + { text: " entwickelst du deine " }, + { text: "Wirkung", em: true, tail: " –" }, + { text: " und deinen " }, + { text: "Erfolg", em: true, gold: true }, + { text: "." }, + ], + } satisfies Headline, + /** Claim unter dem Monogramm. Das Trennzeichen setzt der Hero, wie im Ledger. */ + claim: ["Persönlichkeit", "Wirkung", "Erfolg"], subline: "Herzlich willkommen. Ob als Speakerin für dein Firmenevent, im 1:1 Coaching oder als Trainerin für dein Team – gerne bin ich für dein Anliegen da.", cta: { @@ -148,8 +190,8 @@ export const services = { { numeral: "02", title: "Trainings & Workshops", - oneLiner: "Vertrieb mit Persönlichkeit – für Teams und Führungskräfte.", - keywords: ["B2B-Vertriebstraining", "Neukunden & Bestandskunden", "Leadership & Teamdynamik"], + oneLiner: "Leadership & HR mit Persönlichkeit – für Führungskräfte und Teams.", + keywords: ["B2B-Vertriebstraining", "HR-Teambuilding & Konfliktlösung", "Kommunikation & Teamgeist"], link: { label: "Zu den Trainings", href: routes.trainings }, }, {