diff --git a/public/zertifikate/greator-siegel.webp b/public/zertifikate/greator-siegel.webp new file mode 100644 index 0000000..1623100 Binary files /dev/null and b/public/zertifikate/greator-siegel.webp differ diff --git a/public/zertifikate/greator-speaker.jpg b/public/zertifikate/greator-speaker.jpg new file mode 100644 index 0000000..192acaa Binary files /dev/null and b/public/zertifikate/greator-speaker.jpg differ diff --git a/public/zertifikate/greator-speaker.pdf b/public/zertifikate/greator-speaker.pdf new file mode 100644 index 0000000..b3912ef Binary files /dev/null and b/public/zertifikate/greator-speaker.pdf differ diff --git a/src/components/home/About.tsx b/src/components/home/About.tsx index bcfe24d..e338ae5 100644 --- a/src/components/home/About.tsx +++ b/src/components/home/About.tsx @@ -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) { + + {/* 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 && ( + +
+ +
+
+ )} ); diff --git a/src/components/pages/HomeBody.tsx b/src/components/pages/HomeBody.tsx index 8794059..6f9177b 100644 --- a/src/components/pages/HomeBody.tsx +++ b/src/components/pages/HomeBody.tsx @@ -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 } } content={content.principle} /> - + diff --git a/src/components/ui/Certificate.tsx b/src/components/ui/Certificate.tsx new file mode 100644 index 0000000..2429c59 --- /dev/null +++ b/src/components/ui/Certificate.tsx @@ -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) => { + /* 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 ( + + ); +} + +export default Certificate; diff --git a/src/components/ui/CertificateDialog.tsx b/src/components/ui/CertificateDialog.tsx new file mode 100644 index 0000000..e1904aa --- /dev/null +++ b/src/components/ui/CertificateDialog.tsx @@ -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 ``/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(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("main, header, footer")); + others.forEach((el) => { + el.inert = true; + }); + + const focusables = () => + Array.from(panel.current?.querySelectorAll("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( +
{ + /* 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" + > +
+
+

{content.eyebrow}

+ {/* Playfair hat hier den Platz für seine 28-px-Untergrenze – im Abzeichen selbst nicht. */} +

+ {content.title} +

+
+ +
+ +
+ {/* eslint-disable-next-line @next/next/no-img-element -- static export, no optimiser */} + {content.imageAlt} +
+ {content.dialogMeta} + + + {content.pdfLabel} + + + +
+
+
, + document.body, + ); +} + +export default CertificateDialog; diff --git a/src/components/ui/LaurelWreath.tsx b/src/components/ui/LaurelWreath.tsx new file mode 100644 index 0000000..a5d6341 --- /dev/null +++ b/src/components/ui/LaurelWreath.tsx @@ -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 ``, weil das Projekt keine externen Requests + * macht und ein Inline-SVG die Goldfarbe des jeweiligen Grundes erbt. + */ +export function LaurelWreath({ className }: { className?: string }) { + return ( + + ); +} + +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; diff --git a/src/content/en/home.ts b/src/content/en/home.ts index 95129fe..6b3140a 100644 --- a/src/content/en/home.ts +++ b/src/content/en/home.ts @@ -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" }, }, diff --git a/src/content/home.ts b/src/content/home.ts index 4b3670d..d97e455 100644 --- a/src/content/home.ts +++ b/src/content/home.ts @@ -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; +/** Texte des Zertifikat-Abzeichens – Abzeichen und Overlay teilen sie sich. */ +export type CertificateCopy = HomeContent["about"]["certificate"]; diff --git a/src/lib/assets.ts b/src/lib/assets.ts index 31e469a..57f9e79 100644 --- a/src/lib/assets.ts +++ b/src/lib/assets.ts @@ -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 }; +}