Greator-Zertifikat klebt rechts neben Über Anelia

Gegenstück zur klebenden „04“: gleiche Höhe (calc(50vh − .45em)),
gleiche Zeilen, nur am anderen Rand. Im goldenen Lorbeerkranz steht
das Greator-TheKey-Siegel, darunter Haarlinie, Beschreibung, Datum.

Das Siegel ist aus dem Zertifikat freigestellt – das PDF hat keine
eingebettete Logodatei, die ganze Seite ist ein JPEG. Farbe und Form
bleiben unangetastet: eine fremde Marke wird nicht ins Gold der Seite
umgefärbt. Größe 58 % der Kranzbreite; die Öffnung gäbe bis 70 % her,
an den SVG-Pfaden nachgemessen.

Der Klick öffnet eine Vorschau nach dem Muster des Overlay-Menüs:
Escape schließt, Klick auf den Grund schließt, Fokus bleibt drin,
Lenis hält an, der Rest der Seite wird inert. Bewusst ein Bild und
kein eingebettetes PDF – ein iframe stünde quer zur Zusage in der
Datenschutzerklärung. Ausgeliefert wird ein echter Link aufs PDF,
den erst das JS zum Overlay aufwertet.

Nebenbei: in der Foto-Variante der Sektion lagen Werdegang-Ledger
und Marginalie beide in Zeile 2 übereinander. Da kein Porträt in
public/photos/ liegt, ist das nie aufgefallen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
JUSNandClaude Opus 5 committed 2026-09-10 20:13:08 +02:00
1 parent c09f674653
commit 389b3b9d71
11 files changed
+418 -7

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.
+30 -5
View File
@@ -1,5 +1,6 @@
import { cn } from "@/lib/utils";
import { flags } from "@/lib/site";
import type { CertificateAsset } from "@/lib/assets";
import { about as aboutDe, type HomeContent } from "@/content/home";
import { Section } from "@/components/layout/Section";
import { Lines } from "@/components/motion/Lines";
@@ -9,12 +10,15 @@ import { Eyebrow } from "@/components/ui/Eyebrow";
import { Ledger } from "@/components/ui/Ledger";
import { Marginalia } from "@/components/ui/Marginalia";
import { Portrait } from "@/components/ui/Portrait";
import { Certificate } from "@/components/ui/Certificate";
export interface AboutProps {
/** `getPhotos().portrait` – `null` renders the documented variant B (no photo). */
photo: string | null;
/** Inhalt der Sektion. Ohne Angabe die deutsche Fassung – die englische reicht sie durch. */
content?: HomeContent["about"];
/** `getCertificate()` – `null` lässt die Sektion wie vorher rendern. */
certificate?: CertificateAsset | null;
}
/* H2 on ivory: Playfair 400, max 14ch (spec §3.2 / §6.5). */
@@ -29,11 +33,13 @@ const BODY = "max-w-[36rem] font-sans text-[1.125rem] leading-[1.6] text-fg [hyp
* in view while reading, the Werdegang as a ledger, marginalia with
* verifiable facts. No counters, no logos.
*
* With photo: Portrait cols 3–6 (sticky) · text + ledger cols 7–12 · marginalia cols 10–12.
* Without: text cols 3–8 · ledger cols 3–9 · marginalia under the ledger
* (no monogram vignette – see below).
* With photo: Portrait cols 3–6 (sticky) · text + ledger cols 7–12 · marginalia and
* certificate stacked under the ledger (cols 7–12) – the text already
* owns the outer columns there, so nothing can stick to the right edge.
* Without: text cols 3–8 · ledger cols 3–9 · marginalia under the ledger ·
* certificate sticky in cols 10–12 (no monogram vignette – see below).
*/
export function About({ photo, content: about = aboutDe }: AboutProps) {
export function About({ photo, content: about = aboutDe, certificate = null }: AboutProps) {
const paragraphs = about.paragraphs.map((p, i) =>
i === about.paragraphs.length - 1 && flags.showTwins ? `${p} ${about.twins}` : p,
);
@@ -92,11 +98,30 @@ export function About({ photo, content: about = aboutDe }: AboutProps) {
<Reveal
className={cn(
"col-span-12 mt-12",
hasPhoto ? "lg:col-span-3 lg:col-start-10 lg:row-start-2" : "lg:col-span-7 lg:col-start-3 lg:row-start-3",
hasPhoto ? "lg:col-span-6 lg:col-start-7 lg:row-start-3" : "lg:col-span-7 lg:col-start-3 lg:row-start-3",
)}
>
<Marginalia items={about.marginalia} />
</Reveal>
{/* Das Zertifikat ist das Gegenstück zur klebenden „04“: gleiche Höhe
(`calc(50vh − .45em)`), gleiche Zeilen, nur am anderen Rand. Mit Foto
reicht der Text bis Spalte 12 – dort gäbe es keine freie Gutter, also
steht es dann still unter dem Werdegang. */}
{certificate && (
<Reveal
className={cn(
"col-span-12 mt-14",
hasPhoto
? "lg:col-span-4 lg:col-start-7 lg:row-start-4"
: "lg:col-span-3 lg:col-start-10 lg:row-span-3 lg:row-start-1 lg:mt-0",
)}
>
<div className={cn(!hasPhoto && "lg:sticky lg:top-[calc(50vh_-_0.45em)]")}>
<Certificate content={about.certificate} asset={certificate} />
</div>
</Reveal>
)}
</Section>
</div>
);
+2 -2
View File
@@ -7,7 +7,7 @@ import { About } from "@/components/home/About";
import { Audience } from "@/components/home/Audience";
import { ProcessContact } from "@/components/home/ProcessContact";
import type { HomeContent } from "@/content/home";
import { getPhotos } from "@/lib/assets";
import { getCertificate, getPhotos } from "@/lib/assets";
import { routesByLang, type Lang } from "@/lib/i18n";
import { rootCopy } from "@/lib/metadata";
import { site } from "@/lib/site";
@@ -37,7 +37,7 @@ export function HomeBody({ lang, content }: { lang: Lang; content: HomeContent }
<Positioning content={content.positioning} />
<ServicesIndex content={content.services} />
<WolfPrinciple mark={<BrandMark idPrefix="prinzip" />} content={content.principle} />
<About photo={aboutPhoto} content={content.about} />
<About photo={aboutPhoto} content={content.about} certificate={getCertificate()} />
<Audience content={content.audience} />
<ProcessContact content={content.contact} privacyHref={routesByLang[lang].datenschutz} />
</>
+97
View File
@@ -0,0 +1,97 @@
"use client";
import { useState, type MouseEvent } from "react";
import { cn } from "@/lib/utils";
import type { CertificateAsset } from "@/lib/assets";
import type { CertificateCopy } from "@/content/home";
import { LaurelWreath } from "./LaurelWreath";
import { CertificateDialog } from "./CertificateDialog";
export interface CertificateProps {
content: CertificateCopy;
asset: CertificateAsset;
className?: string;
}
/**
* Das Zertifikats-Abzeichen der Sektion „Über Anelia“ – Lorbeerkranz in Gold,
* darunter Haarlinie, Beschreibung, Datum. Kein Fettdruck, Radius 0, kein
* Schatten; die Display-Schrift kommt hier gar nicht vor, weil sie unter 28 px
* nicht gesetzt werden darf und in diese Spalte nichts Größeres passt.
*
* Ausgeliefert wird ein echter Link auf das PDF: ohne JavaScript, beim
* Rechtsklick und beim Öffnen im neuen Tab landet man beim Dokument. Erst das
* JS wertet den Klick zum Overlay auf.
*/
export function Certificate({ content, asset, className }: CertificateProps) {
const [open, setOpen] = useState(false);
const onClick = (e: MouseEvent<HTMLAnchorElement>) => {
/* Neues Fenster/Tab bleibt neues Fenster/Tab – nur der einfache Klick
wird abgefangen. */
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
e.preventDefault();
setOpen(true);
};
return (
<div className={cn("max-w-[16rem]", className)}>
<a
href={asset.pdf}
target="_blank"
rel="noopener noreferrer"
onClick={onClick}
className="group block text-fg"
>
<span className="relative block aspect-[892.77/688.08] w-full">
<LaurelWreath className="absolute inset-0 h-full w-full text-gold-700 transition-colors duration-500 ease-[var(--ease-out-quart)] group-hover:text-gold-900 group-focus-visible:text-gold-900 [[data-theme=dark]_&]:text-gold" />
<span className="absolute inset-0 flex items-center justify-center">
{/* Aus dem Zertifikat freigestellt, unverändert in Farbe und Form: eine
fremde Marke wird nicht ins Gold der Seite umgefärbt. `alt=""`, weil
der Text direkt darunter dasselbe sagt. */}
{/* eslint-disable-next-line @next/next/no-img-element -- static export, no optimiser */}
<img
src={asset.seal}
alt=""
width={asset.sealSize}
height={asset.sealSize}
/* 58 % der Kranzbreite. Die Öffnung gäbe bis 70 % her (im Browser an den
Pfaden nachgemessen), der Rest ist Luft, damit das Siegel nicht an den
Blättern klebt. */
className="block h-auto w-[58%]"
/>
</span>
</span>
<span className="mt-6 block font-mono text-xs uppercase leading-none tracking-[0.18em] text-accent">
{content.title}
</span>
<span aria-hidden="true" className="rule-gold mt-5 block h-px w-full" />
<span className="mt-5 block font-sans text-[0.875rem] leading-[1.5] text-fg [hyphens:auto]">
{content.description}
</span>
<span className="mt-3 block font-mono text-[0.6875rem] tracking-[0.06em] tabular-nums text-fg-muted">
{content.date}
</span>
<span className="mt-5 inline-flex items-baseline gap-2 font-sans text-[0.8125rem] font-medium uppercase leading-none tracking-[0.2em] text-accent">
<span className="bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-1 transition-[background-size] duration-[400ms] ease-[var(--ease-out-quart)] group-hover:bg-[length:100%_1px] group-focus-visible:bg-[length:100%_1px]">
{content.cta}
</span>
<span
aria-hidden="true"
className="transition-transform duration-300 ease-[var(--ease-out-quart)] group-hover:translate-x-2"
>
→
</span>
</span>
</a>
<CertificateDialog open={open} onClose={() => setOpen(false)} content={content} asset={asset} />
</div>
);
}
export default Certificate;
+204
View File
@@ -0,0 +1,204 @@
"use client";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import type { CertificateAsset } from "@/lib/assets";
import type { CertificateCopy } from "@/content/home";
import { gsap, DUR, EASE, MM, getLenis, refreshSoon } from "@/lib/gsap";
/** Die Überschrift benennt den Dialog (`aria-labelledby`) – es gibt genau einen. */
const TITLE_ID = "zertifikat-titel";
/**
* Kopf, Blatt und Fußzeile teilen sich eine Breite, sonst stehen sie versetzt.
* Auf dem Desktop richtet sie sich nach der Höhe: das Blatt soll ganz ins Bild
* passen (0,706 = 989 ÷ 1400, das Seitenverhältnis des Scans).
*/
const COLUMN = "w-full max-w-[46rem] lg:max-w-[min(46rem,calc((100svh-16rem)*0.706))]";
export interface CertificateDialogProps {
open: boolean;
onClose: () => void;
content: CertificateCopy;
asset: CertificateAsset;
}
/**
* Vorschau des Zertifikats: dunkler Grund, Seite 1 als Bild, darunter der Weg
* zum PDF. Bewusst ein Bild und kein eingebettetes PDF – ein `<object>`/iframe
* hätte den Viewer des Browsers mitgebracht (auf dem Handy unzuverlässig) und
* stünde quer zur Zusage in der Datenschutzerklärung.
*
* Verhalten wie beim Overlay-Menü, dessen Muster diese Komponente übernimmt:
* Escape schließt, Klick auf den Grund schließt, Fokus bleibt im Dialog,
* Lenis hält an, der Rest der Seite wird `inert`, am Ende bekommt das
* auslösende Element seinen Fokus zurück.
*
* Gerendert wird in `document.body` – der Auslöser sitzt in `main`, und `main`
* ist ja gerade das, was hier `inert` gesetzt wird. Ohne den Portal läge der
* Dialog in seinem eigenen toten Bereich.
*/
export function CertificateDialog({ open, onClose, content, asset }: CertificateDialogProps) {
const panel = useRef<HTMLDivElement>(null);
const onCloseRef = useRef(onClose);
/* Der Dialog muss seinen Ausblend-Tween überleben – gleiche Lösung wie im
Overlay-Menü: `exiting` hält ihn im DOM, bis GSAP fertig ist. */
const [exiting, setExiting] = useState(false);
const [prevOpen, setPrevOpen] = useState(open);
if (prevOpen !== open) {
setPrevOpen(open);
setExiting(!open);
}
const mounted = open || exiting;
useEffect(() => {
onCloseRef.current = onClose;
});
/* Nur `opacity` – der Dialog enthält Links, und alles mit `visibility` oder
`autoAlpha` nähme sie aus der Tab-Reihenfolge (Projektregel). */
useLayoutEffect(() => {
const el = panel.current;
if (!mounted || !el) return;
const mm = gsap.matchMedia();
mm.add({ reduce: MM.reduce, ok: MM.ok }, (ctx) => {
const reduce = Boolean(ctx.conditions?.reduce);
if (open) {
gsap.set(el, { opacity: reduce ? 1 : 0 });
if (reduce) return;
gsap.to(el, { opacity: 1, duration: DUR.micro, ease: EASE.out });
return;
}
gsap.to(el, {
opacity: 0,
duration: reduce ? 0 : DUR.micro,
ease: EASE.inOut,
onComplete: () => setExiting(false),
});
});
return () => mm.revert();
}, [open, mounted]);
useEffect(() => {
if (!open) return;
const lenis = getLenis();
lenis?.stop();
const previous = document.activeElement as HTMLElement | null;
const others = Array.from(document.querySelectorAll<HTMLElement>("main, header, footer"));
others.forEach((el) => {
el.inert = true;
});
const focusables = () =>
Array.from(panel.current?.querySelectorAll<HTMLElement>("a[href], button:not([disabled])") ?? []);
const t = window.setTimeout(() => focusables()[0]?.focus(), 50);
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onCloseRef.current();
return;
}
if (e.key !== "Tab") return;
const items = focusables();
if (!items.length) return;
const first = items[0];
const last = items[items.length - 1];
const active = document.activeElement as HTMLElement | null;
const inside = active ? items.includes(active) : false;
if (e.shiftKey && (active === first || !inside)) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && (active === last || !inside)) {
e.preventDefault();
first.focus();
}
};
document.addEventListener("keydown", onKey);
return () => {
window.clearTimeout(t);
document.removeEventListener("keydown", onKey);
others.forEach((el) => {
el.inert = false;
});
lenis?.start();
previous?.focus?.();
refreshSoon();
};
}, [open]);
if (!mounted) return null;
return createPortal(
<div
ref={panel}
role="dialog"
aria-modal="true"
aria-labelledby={TITLE_ID}
data-theme="dark"
data-lenis-prevent=""
onClick={(e) => {
/* Nur der Grund schließt – ein Klick ins Blatt darf nicht durchschlagen. */
if (e.target === e.currentTarget) onCloseRef.current();
}}
className="fixed inset-0 z-50 flex flex-col items-center overflow-y-auto overscroll-contain bg-ink px-5 py-[calc(var(--header-h,60px)/2+16px)] text-fg lg:px-10"
>
<div className={`mb-8 flex items-start justify-between gap-6 ${COLUMN}`}>
<div>
<p className="font-mono text-xs uppercase tracking-[0.12em] text-accent">{content.eyebrow}</p>
{/* Playfair hat hier den Platz für seine 28-px-Untergrenze – im Abzeichen selbst nicht. */}
<h2
id={TITLE_ID}
className="mt-3 font-display text-[clamp(1.75rem,1.2rem+1vw,2.25rem)] font-normal leading-none tracking-[-0.01em] text-fg"
>
{content.title}
</h2>
</div>
<button
type="button"
onClick={() => onCloseRef.current()}
className="-my-3 shrink-0 py-3 font-mono text-xs uppercase tracking-[0.2em] text-fg-muted transition-colors duration-300 hover:text-gold focus-visible:text-gold"
>
{content.close} <span aria-hidden="true">✕</span>
</button>
</div>
<figure className={`m-0 flex flex-col ${COLUMN}`}>
{/* eslint-disable-next-line @next/next/no-img-element -- static export, no optimiser */}
<img
src={asset.image}
alt={content.imageAlt}
width={asset.width}
height={asset.height}
className="block h-auto w-full border border-line"
/>
<figcaption className="mt-6 flex w-full flex-col gap-4 sm:flex-row sm:items-baseline sm:justify-between">
<span className="font-mono text-xs tracking-[0.06em] text-fg-muted">{content.dialogMeta}</span>
<a
href={asset.pdf}
target="_blank"
rel="noopener noreferrer"
className="group/tl -my-4 inline-flex shrink-0 items-baseline gap-2 py-4 font-sans text-[0.8125rem] font-medium uppercase leading-none tracking-[0.2em] text-accent"
>
<span className="bg-[linear-gradient(currentColor,currentColor)] bg-[length:0%_1px] bg-left-bottom bg-no-repeat pb-1 transition-[background-size] duration-[400ms] ease-[var(--ease-out-quart)] group-hover/tl:bg-[length:100%_1px] group-focus-visible/tl:bg-[length:100%_1px]">
{content.pdfLabel}
</span>
<span
aria-hidden="true"
className="transition-transform duration-300 ease-[var(--ease-out-quart)] group-hover/tl:translate-x-2"
>
↗
</span>
</a>
</figcaption>
</figure>
</div>,
document.body,
);
}
export default CertificateDialog;
+32
View File
@@ -0,0 +1,32 @@
import { cn } from "@/lib/utils";
/**
* Lorbeerkranz, zwei Hälften, `fill: currentColor` – die Farbe kommt also von
* der Klasse am Element, nicht aus dem Pfad. Rein dekorativ (`aria-hidden`):
* was der Kranz bedeutet, steht als Text daneben.
*
* Inline und nicht als `<img src>`, weil das Projekt keine externen Requests
* macht und ein Inline-SVG die Goldfarbe des jeweiligen Grundes erbt.
*/
export function LaurelWreath({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 892.77 688.08"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
className={cn("fill-current", className)}
>
<path d={RIGHT} />
<path d={LEFT} />
</svg>
);
}
const RIGHT =
"M892.6,358.7c-2.45,21.69-26.03,75.09-50.59,78.41-2.61.35-8.8-.53-10.21.3-.39.23-4.32,6.79-4.75,7.74-6.88,15.27-11.04,42.49-17.43,60.57-2.66,7.51-9.87,21.35-11.03,27.98-.1.58-.26,1.65.51,1.5,6.6-6.88,8.6-17.3,13.33-25.67,13.94-24.66,43.3-43.5,72.17-42.32-2.33,14.6-10.5,29.43-19.18,41.31-12.47,17.06-30.45,32.62-53.23,29.6-7.56-1-6.85-5.3-14.59,1.58-6.35,5.64-9.71,15.53-14.19,22.81-9.91,16.12-20.19,32.01-32.8,46.2,4.66.89,8.76-7.66,11.97-11.03,11.14-11.72,21.81-19.57,38.02-22.98,15.67-3.3,37.45-3.14,52.02,4.03-12.24,25.25-57.46,51.32-84.7,41.67-7.61-2.7-12.94-10.13-22.06-6.43-18.34,18.87-38.7,35.14-61.25,48.75,4.97,1.57,9.28-3.53,13.32-5.68,20.73-11.01,32.74-14.63,56.5-9.15,13.85,3.19,26.55,9.91,38.17,17.82-19.09,21.89-75.53,31.43-97.49,11.5-3.79-3.44-5.4-10.28-10.95-11.04-9.07-1.24-22.07,6.8-31.04,10.07-3.69,1.34-27.01,9.51-28.52,7.99.06-2.6-.8-5.29,1.57-6.93,1.5-1.04,17.65-4.87,21.68-6.32,8.48-3.06,22.88-8.38,30.46-12.54,9.12-5.01,2.78-21.66,2.3-29.72-1.18-19.76,7.83-43.47,20.48-58.5,1.79-2.13,19.39-19.37,21.51-16.98-2.82,28.58.11,60.88-19.03,84.46-2.69,3.03-6.49,6.06-10.06,7.94-4.12,2.16-9.31.65-8.9,7.59,1.27,1.23,11.02-5.38,12.91-6.58,10.31-6.56,22.01-15.72,31.1-23.9,2.59-2.34,19.93-19.11,20.44-20.46.23-.63-.39-11.92-.68-13.33-1.18-5.83-6.95-13.96-8.5-21.5-3.96-19.28-2.28-43.2,4.96-61.49,3.45-8.71,9.01-17.74,15.27-24.73,2.9,17.5,8.39,36.08,10.37,53.62,1.87,16.52.69,35.79-10.9,48.86-4.62,5.21-9.96,5.2-7.97,14.52,13.17-15.31,24.53-32.58,34.16-50.33,1.99-3.66,9.61-17.17,9.7-20.13.33-10.18-12.65-21.57-17.05-30.87-11.72-24.78-14.73-58.69-5.28-84.63.71-1.96,1.42-5.12,3.44-6.04,12.22,32.23,38.76,63.84,24.79,100.27-2.31,6.03-7.68,8.74-4.76,15.73,1.54-.25,1.89-1.87,2.47-3.02,7.87-15.49,14.88-42.87,18.84-60.16,1.87-8.16,6.12-21.11,2.85-28.5-4.29-9.69-11.33-11.15-18.64-17.35-24.62-20.88-33.84-58.75-31.52-89.97,1.97-.51,1.93.81,2.81,1.68,20.75,20.47,53.85,55.45,49.12,86.75-.44,2.9-3.52,7.02-3.71,9.23s2.82,4.83,2.79,7.33c1.42,1.39,1.82-.07,1.99-1.48.8-6.86,1.5-14.15,1.99-21.03,1.05-14.96,1.76-33.12,1.05-48.03-.45-9.51-.5-18.45-7.5-25.5-4.23-4.26-9.74-3.95-14.91-6.09-28.89-12.01-48.06-49.12-49.88-79.13-.07-1.11-2.03-1.97.73-1.73,23.64,16.34,59.89,35.61,63.41,67.59.81,7.39-3.03,17.42,5.1,20.39-.84-15.5-4.12-31.76-7.48-47.01-1.91-8.66-8.25-37.76-12.65-43.35-6.39-8.12-14.14-3.87-22.25-4.75-21.82-2.37-43.12-23.21-53.78-41.22-3.02-5.1-7.72-13-7.34-18.66,29.71,10.05,66.39,12.92,75.07,48.93,1.39,5.76-1.51,11.75,7.44,11.05-9.45-25.34-21.53-49.48-34.15-73.34-9.49-11.1-18.29-1.46-29.31-.62-24.24,1.85-49.97-15.79-62.53-35.54,12.16-1.78,27.18-2.45,39.53-1.53,14.47,1.07,29.13,6.2,36.65,19.35,3.12,5.45,2.93,14.34,10.8,11.68-7.94-13.11-17.24-26.37-27.04-38.45-2.54-3.14-13.54-16.92-15.98-18.02-7.78-3.53-11.89,2.2-17.3,4.66-22.22,10.12-48.27,3.4-65.69-12.67-1.12-1.03-7.02-6.35-5.98-7.51,17.62-2.11,36.48-11.68,54.24-7.75,8.57,1.89,16.8,6.91,22.19,13.81,2.88,3.69.83,6.42,7.06,6.94,1.49.12,2.92.63,2.49-1.49-8.92-8.7-17.38-18.05-26.98-26.03-3.66-3.04-13.47-11.25-17.29-12.71-4.55-1.73-10.25-.33-15.11-.89-27.28-3.12-48.71-29.1-57.58-53.4,1.55-2.18,1.45-.91,2.6-.65,29.66,6.66,68.48,16.71,70.87,53.16,21.14,14.84,39.62,33.01,57,52,.62-8.81-9.38-14.38-14.48-20.52-19.56-23.59-25.5-58.58-24.02-88.47.68-.79,10.44,7.02,11.52,7.97,20.16,17.57,37.02,49.29,34.52,76.57-1,10.9-9.52,15.89-4.29,28.21,1.81,4.26,14.83,18.08,18.66,23.34,9.6,13.17,19.55,27.31,26.6,41.91,1.62.09,2.24.49,1.98-1.47-.48-3.57-9.92-18.62-12.17-23.85-11.51-26.81-14.45-60.89-2.88-88.23.37-.88.81-2.87,2.06-1.96,9.94,12.98,16.87,28.86,21.49,44.52,5.8,19.65,9.83,40.98.04,60.03-1.45,2.82-4.93,5.59-5.48,8.51-2.09,11.08,6.07,21.31,10.12,30.78,7.21,16.81,14.1,33.95,18.83,51.67,1.58-.05,1.97-4.44,1.92-5.42-.27-5.28-7.25-19.63-8.64-27.37-4.67-26.05-2.53-50.65,10.53-73.88,1.45-2.57,4.8-8.51,6.73-10.28,1.69-1.54,1.69-.41,2.66,1.23,12.28,20.79,17.41,81.23,2.78,101.21-2.35,3.21-9.25,7.51-10.45,9.55-.39.66-3.05,12.36-3.11,13.43-.35,5.98,7.73,33.4,9.25,42.34,2.02,11.93,3.48,24.11,4.32,36.18,2.19-.58,1.95-2.71,2.05-4.46.4-6.52-1.93-15.33-2.11-22-.88-33.13,13.53-64.28,38.54-85.52,1.65-.03,3.41,14.23,3.55,16.43,1.54,24.35-4.14,74.1-26.52,88.57-6.99,4.52-12.38,3.46-14.29,13.71-2.29,12.31,3.05,32.46.81,45.81l-4.03,40.46c4.58-3.93,3.21-12.03,4.24-17.25,6.58-33.49,28.21-64.47,60.75-76.75-.37,4.65.51,9.94,0,14.5Z";
const LEFT =
"M122.6,347.7c-1.7,17.7-14.14,47.55-26.48,60.52-5.79,6.09-14.83,9.27-20.05,15.95-7.2,9.23-5.11,14.49-3.18,25.23,4.49,25.1,12.63,50.47,22.72,73.79,1.68-.31,2.16-5.36,2.02-6.44-.24-1.76-4.97-6.9-6.22-9.86-8.94-21.17-.97-44.77,8.01-64.37l18.18-35.83c12.17,28.27,10.36,66.34-4.83,93.18-4.82,8.52-16.69,18.39-14.52,29.1.83,4.09,6.01,13.72,8.18,17.9,9.33,17.95,21.89,36.15,35.16,51.33,1.78-8.72-2.17-7.86-6.5-12.99-25.68-30.41-7.81-69.51-2-104.01,20.14,23.8,27.35,62.58,18.27,92.26-3.14,10.27-9.76,17.88-6.25,29.71,18.74,19.75,39.22,38.23,63.47,51.02.89-8.43-5.47-6.33-10.34-9.15-4.61-2.67-12.11-11.32-15.06-15.94-13.97-21.88-9.34-50.93-13.57-75.43,4.11-.29,10.26,4.82,13.45,7.55,18.34,15.69,31.07,45.29,29.51,69.46-.49,7.59-5.43,18.51-.47,25.5,1.35,1.9,21.37,10.31,25.22,11.78,9.94,3.8,20.08,6.73,30.28,9.72l.98,7.51c-7.54-2.18-15.1-4.36-22.51-6.98-7.88-2.79-24.11-11.27-30.95-12.04-11.89-1.35-11.52,5.99-19.05,12-20.85,16.65-61.75,9.86-83.5-2.46-2.07-1.18-13.19-7.73-11.54-10.53,21.13-14.47,48.69-24.76,74.34-17.26,10.95,3.2,20.18,11.91,29.19,14.81,1.02.33,5.04,1.53,5.02-.51-11.82-6.39-24.42-14.2-34.91-22.59-6.02-4.82-23.27-23.82-27.5-25.5-9.8-3.9-9.91.63-16.9,4.26-26.89,13.94-71.69-11.42-87.08-34.28-1.05-1.55-3.6-3.96-2.17-5.96,3.81-5.34,36.79-5.58,43.94-4.81,18.64,1.99,33.72,11.44,46.14,24.86,3.15,3.4,6.26,11.64,11.48,10.52-10.1-11.12-19.65-23.71-27.41-36.58-5-8.3-11.58-25.16-17.59-31.41-7.79-8.1-10.21-3.34-18.5-2.52-24.7,2.44-44.75-17.93-56.91-37.07-6.3-9.93-12.8-22.08-13.59-33.92,33.98-.41,60.36,19.86,75.52,48.98,3.34,6.42,4.42,14.83,10.47,19.02-6.28-16.37-13.32-32.34-18.26-49.23s-6.28-33.8-14.89-48.61c-33.08,1.59-49.47-37.67-57.35-64.16-2.91-9.81-5.26-18.73-3.5-28.99,26.83,9.2,52.19,40.65,58.53,67.96,1.93,8.33.98,19.55,6.46,26.03-5.71-27.36-4-55.47-3.25-83.24-.33-2.46-2.64-10.16-4.27-11.73-1.84-1.78-8.46-3.44-11.41-5.59-21.82-15.94-28.32-68.11-24.94-92.82.26-1.88,2.02-11.61,3.38-11.6,24.77,21.31,40.05,54.58,38.49,87.47-.37,7.9-3.74,17.25-1.49,24.51,1.87-1.18,1.21-2.88,1.51-4.49,2.56-13.9,3.69-28.54,6.31-42.69,1.26-6.78,7.5-26.71,7.24-31.33-.05-.87-2.77-11.88-3.12-12.42-.93-1.43-6.38-5.09-8.41-7.59-17.02-20.94-12.69-82.05.77-104.18.86-1.41,1.09-3.01,2.65-1.25,3.37,3.78,8.96,15.02,11.03,19.97,8.44,20.2,10.18,39.73,6.24,61.21-1.57,8.58-8.23,23.74-8.7,30.39-.09,1.31.19,5.98,1.96,6.38,2.2-9.93,5.59-20.68,9.26-30.24,4.82-12.57,15.43-28.92,18.49-40.51.76-2.89,1.78-9.95,1.23-12.72-.47-2.38-6.17-8.59-7.75-12.24-9.36-21.65.56-56.02,9.6-76.95.98-2.26,12.81-26.07,15.16-24.32,12.15,28.6,10.64,62.39-1.81,90.67-2.46,5.58-10.57,17.48-11.2,21.81-.14.96-.63,3.81,1.01,3.51,4.3-10.16,10.59-19.63,16.79-28.71,6.7-9.82,24.02-28.29,27.92-37.08,5.11-11.53.33-12.38-2.74-21.66-7.05-21.37,8.26-54.53,22.07-71.01,3.38-4.04,17.33-17.9,21.49-19.52,1.73-.68,1.35.43,1.49,1.47,3.89,29.17-8.37,67.53-27.03,89.99-4.6,5.54-11.32,8.61-11.97,16.52,1.97.46,2.37-1.01,3.41-2.08,16.97-17.57,33.36-36.17,54.38-49.11-.07-35.38,43.89-46.99,71.78-53.74,1.38-.33,1.84.04,1.42,1.42-5.53,18.05-25.93,42.44-43.72,49.28-12.29,4.72-24.31,2.03-34.57,8.43-14.24,8.89-27.32,24.04-38.69,36.31,6.82,2.69,8.09-6.05,11.95-10.05,18.99-19.63,50.96-6.74,73.53-2.45,1.16.29.47,1.31.03,2.01-2.75,4.35-16.7,13.95-21.64,16.36-13.41,6.54-31.14,9.11-45.24,3.5-7.37-2.94-11.36-9.47-20.93-6.66-4.55,1.34-25.58,28.93-29.52,34.48-3.29,4.64-12.58,17.34-14.17,21.83-1,2.82,5.58-.56,6-1,.9-.94,3.53-9.1,5.75-12.25,5.76-8.16,20.75-17.27,30.73-17.27h43l1.06,2.03c-13.36,19.94-41.02,38.11-65.81,34.71-10.24-1.41-16.8-10.17-26.17,1.35-8.68,10.66-28.7,55.97-32.13,69.87-.26,1.06-1.13,3.41.04,4.04,6.8-3.28,4.54-9.25,5.94-15.06,7.5-31,50.07-37.7,75.55-45.94-6.63,24.9-36.5,58.71-63.45,60.04-7.64.37-14.17-2.72-20.55,4.45-2.66,2.99-6.55,17.32-7.91,22.09-3.07,10.75-5.58,22.38-7.6,33.4-2.1,11.43-4.64,23.48-3.98,35.02,6.42-4.26,3.24-5.92,3.5-10.5,2.2-39.31,32.77-57.91,63.47-76.54.76-.46,1.75-1.99,2.53-.47-3.3,29.92-22.05,68.78-51.31,80.69-4.12,1.68-8.25,1.66-12.01,3.99-1.75,1.09-6.98,6.87-7.46,8.54-1.18,4.12-2.01,16.84-2.25,21.75-1.13,22.55,1.18,45.14,3.05,67.54,1.37,1.8,4.44-5.33,4.55-5.94.14-.79-4.39-11.57-4.45-15.45-.46-30.71,29.69-61.31,49.92-82.09.96-.98,1.3-1.88,2.98-1.52-.55,7.91.75,16.71,0,24.5Z";
export default LaurelWreath;
+12
View File
@@ -122,6 +122,18 @@ export const homeEn: HomeContent = {
{ value: "LinkedIn", href: site.linkedin, external: true },
{ value: "German · English" },
],
certificate: {
title: "Greator Speaker",
description: "Certified graduate of the Greator Speaker programme",
date: "May 2026",
cta: "View certificate",
eyebrow: "Certificate",
dialogMeta: "Greator TheKey · issued 16 May 2026",
imageAlt:
"Certificate: Anelia Wolf has successfully completed the certified Greator Speaker programme.",
pdfLabel: "Open PDF",
close: "Close",
},
folio: { current: "05", total: "06" },
},
+17
View File
@@ -208,6 +208,21 @@ export const about = {
{ value: "LinkedIn", href: site.linkedin, external: true },
{ value: "Deutsch · Englisch" },
] satisfies readonly MarginaliaItem[],
/* Das Abzeichen in der rechten Gutter – Gegenstück zur klebenden „04“ links.
`seal` steht IM Lorbeerkranz, alles darunter ist die Bildunterschrift. */
certificate: {
title: "Greator Speaker",
description: "Zertifizierte Absolventin der Greator Speaker Ausbildung",
date: "Mai 2026",
cta: "Zertifikat ansehen",
/* Overlay */
eyebrow: "Zertifikat",
dialogMeta: "Greator TheKey · Ausstellungsdatum 16. Mai 2026",
imageAlt:
"Zertifikat: Anelia Wolf hat die Ausbildung zum zertifizierten Greator Speaker erfolgreich abgeschlossen.",
pdfLabel: "PDF öffnen",
close: "Schließen",
},
folio: { current: "05", total: "06" },
} as const;
@@ -319,3 +334,5 @@ export const home = { hero, positioning, services, principle, about, audience, t
assertNoPlaceholders("src/content/home.ts", collectStrings(home));
/** Struktur, nicht Wortlaut – die englische Fassung erfüllt denselben Typ. */
export type HomeContent = Widen<typeof home>;
/** Texte des Zertifikat-Abzeichens – Abzeichen und Overlay teilen sie sich. */
export type CertificateCopy = HomeContent["about"]["certificate"];
+24
View File
@@ -56,3 +56,27 @@ export function getLogoSvg(): string | null {
export function getSpeakerPdf(): string | null {
return publicUrl("speaker-profil.pdf");
}
export interface CertificateAsset {
/** `/zertifikate/greator-speaker.pdf` – das Original zum Öffnen/Herunterladen. */
pdf: string;
/** Seite 1 als Bild für die Vorschau (kein eingebettetes PDF, kein iframe). */
image: string;
width: number;
height: number;
/** Das runde Greator-TheKey-Siegel, freigestellt – es steht im Lorbeerkranz. */
seal: string;
sealSize: number;
}
/**
* Greator-Speaker-Zertifikat für die Sektion „Über Anelia“. Alle drei Dateien müssen
* liegen – fehlt eine, rendert die Sektion wie vorher (Variante B, wie bei den Fotos).
*/
export function getCertificate(): CertificateAsset | null {
const pdf = publicUrl("zertifikate/greator-speaker.pdf");
const image = publicUrl("zertifikate/greator-speaker.jpg");
const seal = publicUrl("zertifikate/greator-siegel.webp");
if (!pdf || !image || !seal) return null;
return { pdf, image, width: 989, height: 1400, seal, sealSize: 372 };
}