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:
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.
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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" },
|
||||
},
|
||||
|
||||
|
||||
@@ -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"];
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user