Templates/Meeting Recaps SaaS
A calm SaaS page in white and one muted royal blue. Instead of describing the product it shows it: the hero card writes its summary word by word and ticks its action items off one at a time, then swaps in the next meeting, and further down a drop zone runs the same four steps a real upload would. Every illustration is drawn in the page itself, so there is not a single photograph to replace. Depth comes from hairline borders and a change of surface, never from a shadow.
The finished page to scroll through, switchable between desktop, tablet and mobile.
Source code
The complete page as a single file. Copy it into ./components and add the component to your route.
import {
useCallback,
useEffect,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import { AnimatePresence, motion, useInView, useReducedMotion } from "framer-motion";
import {
ArrowRight,
BellRing,
Check,
FileAudio,
Globe2,
Lock,
Mail,
MapPin,
Menu,
MonitorPlay,
ShieldCheck,
Sparkles,
Star,
Trash2,
UserCheck,
Users,
Video,
X,
} from "lucide-react";
/*
* Confer: a full page for a meeting-transcription product.
*
* White plus one muted royal blue, with two cool near-white surfaces for the
* section bands. Depth comes from hairline borders and a change of surface,
* never from a shadow. The only texture is a fine dot grid, masked so it fades
* out before it reaches the type.
*
* The page shows the product instead of describing it: the hero holds a live
* recap card that writes its summary word by word and ticks its action items
* off one at a time, then swaps in the next meeting. Further down a drop zone
* runs the same four steps a real upload would.
*/
const TINTE = "#0a0a0a";
const LEISE = "#545a6b";
const BLAU = "#3a5bc4";
const BLAU_2 = "#5069ce";
const BLAU_SANFT = "#ebeefb";
const VIOLETT = "#6b54ce";
const FLAECHE = "#f6f7fb";
const FLAECHE_2 = "#eceff6";
const STUMM = "#f2f4f9";
const KANTE = "#e3e6ef";
const GRUEN = "#2f8f5b";
const SANS = '"Albert Sans", ui-sans-serif, system-ui, sans-serif';
/** Karten gross gerundet, Knoepfe klein: die Form der Flaeche, nicht der Taste. */
const KARTE = 24;
const KNOPF = 8;
const MARKE = { name: "Confer", mail: "hello@confer.app" };
const PRODUKTE = {
capture: "Confer.Capture",
recap: "Confer.Recap",
automate: "Confer.Automate",
};
const NAV = [
{ href: "#journey", label: "Capture" },
{ href: "#journey", label: "Recap" },
{ href: "#journey", label: "Automate" },
{ href: "#pricing", label: "Pricing" },
{ href: "#security", label: "Security" },
];
type Aufgabe = { was: string; wer: string; wann: string; fertig: boolean };
type Termin = {
titel: string;
leute: string[];
dauer: string;
zusammenfassung: string[];
aufgaben: Aufgabe[];
};
const TERMINE: Termin[] = [
{
titel: "Weekly Sync",
leute: ["MK", "AR", "SL"],
dauer: "34 min",
zusammenfassung: [
"12%",
"discount",
"to",
"Q3",
"approved,",
"API",
"blocker",
"with",
"Mert",
"tomorrow.",
],
aufgaben: [
{ was: "Resolve API blocker", wer: "MK", wann: "tomorrow", fertig: true },
{ was: "Send timeline", wer: "SL", wann: "Fri", fertig: true },
{ was: "Log discount approval", wer: "AR", wann: "this wk", fertig: false },
],
},
{
titel: "Sales Call · Northwind",
leute: ["JD", "KE"],
dauer: "22 min",
zusammenfassung: [
"Sent",
"offer",
"with",
"12%",
"discount,",
"close",
"expected",
"by",
"Q3.",
],
aufgaben: [
{ was: "Send the offer", wer: "JD", wann: "today", fertig: true },
{ was: "Follow up", wer: "KE", wann: "Tue", fertig: false },
],
},
{
titel: "Client · Meyer",
leute: ["SL", "TN", "KU"],
dauer: "41 min",
zusammenfassung: ["Timeline", "slips,", "new", "plan", "promised", "by", "Friday."],
aufgaben: [
{ was: "Revise the timeline", wer: "SL", wann: "Fri", fertig: true },
{ was: "Book follow-up", wer: "TN", wann: "Wed", fertig: false },
],
},
];
const VERTRAUEN = [
{ label: "Used by teams in 20+ countries", Icon: Globe2 },
{ label: "GDPR-compliant", Icon: ShieldCheck },
{ label: "Encrypted transfer", Icon: Lock },
{ label: "For Zoom, Meet and Teams", Icon: Video },
{ label: "Participants are notified", Icon: BellRing },
];
const WEG = [
{
schritt: "01",
marke: "Capture",
name: PRODUKTE.capture,
frage: "“What was said?”",
text: "Records and transcribes your meetings, live in Zoom, Meet and Teams or from a file.",
farbe: BLAU,
},
{
schritt: "02",
marke: "Recap",
name: PRODUKTE.recap,
frage: "“What needs doing?”",
text: "Creates a summary, decisions and action items, including a draft for the follow-up.",
farbe: "#2c4699",
},
{
schritt: "03",
marke: "Automate",
name: PRODUKTE.automate,
frage: "“How does it all connect?”",
text: "Pushes results to your tools and makes every meeting searchable.",
farbe: BLAU_2,
},
];
const STIMMEN = [
{
text: "Nobody has to take notes during the meeting anymore. By the time the call ends, the recap is already in our team channel.",
name: "Amina",
rolle: "Product Lead, Berlin",
},
{
text: "The action-item list with owners is priceless, by the next meeting everything is actually done.",
name: "Kerem",
rolle: "Sales Manager, Munich",
},
{
text: "I now send clients a clean summary five minutes after the call. Very professional.",
name: "Sabine",
rolle: "Agency Owner, Hamburg",
},
];
const SCHRITTE = [
{
titel: "Connect your meeting or upload a recording",
text: "Live in Zoom, Meet and Teams, or an MP3 or video file.",
},
{ titel: "Confer transcribes and summarizes", text: "Calm, careful, in plain language." },
{
titel: "You get notes, decisions and action items",
text: "Including a follow-up draft and open questions.",
},
];
const ARCHIV_PUNKTE = [
"A searchable archive of every meeting",
"Results shared in a single click",
"Nothing important slips away",
];
const HOCHLADEN_SCHRITTE = [
"Transcribed audio",
"Identified speakers",
"Wrote the summary",
"Extracted action items",
];
const PREISE = [
{
name: PRODUKTE.capture,
marke: "Capture",
preis: "€0",
je: "free",
text: "For single meetings and trying things out.",
punkte: [
"Up to 5 meetings per month",
"Plain-text transcription",
"Automatic summary",
"Export as PDF",
],
cta: "Start for free",
hervor: false,
},
{
name: PRODUKTE.recap,
marke: "Recap",
preis: "€12",
je: "per user / month",
text: "For teams with a lot of meetings.",
punkte: [
`Everything in ${PRODUKTE.capture}`,
"Decisions and action items with owners",
"Follow-up drafts",
"Up to 30 meetings per month",
],
cta: "Start 30-day trial",
hervor: true,
},
{
name: PRODUKTE.automate,
marke: "Automate",
preis: "€29",
je: "per user / month",
text: "For teams that want to connect everything.",
punkte: [
`Everything in ${PRODUKTE.recap}`,
"Integrations (Slack, Notion, CRM)",
"Searchable meeting archive",
"Analytics across all meetings",
],
cta: "Contact sales",
hervor: false,
},
];
const SICHERHEIT = [
{
Icon: MapPin,
titel: "Hosted in the EU",
text: "Processed and stored exclusively on servers in the European Union.",
},
{
Icon: Lock,
titel: "Encrypted in transit and at rest",
text: "TLS in transit, encryption at rest.",
},
{
Icon: Trash2,
titel: "Audio is deleted",
text: "The original audio is removed after transcription, you keep the recap.",
},
{
Icon: UserCheck,
titel: "With consent",
text: "Participants are notified of the recording. You stay in control of your data.",
},
];
const FUSS_SPALTEN = [
{ titel: "Product", links: ["How it works", "Pricing", "Testimonials", "Upload a recording"] },
{ titel: "Company", links: ["About", "Contact", "Press", "Careers"] },
{ titel: "Legal", links: ["Privacy", "Legal notice", "Terms", "Cookie settings"] },
];
/* --------------------------------------------------------------- primitives */
function Auftauchen({
children,
delay = 0,
className = "",
style,
}: {
children: ReactNode;
delay?: number;
className?: string;
style?: CSSProperties;
}) {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const sichtbar = useInView(ref, { once: true, margin: "-70px" });
return (
<motion.div
ref={ref}
className={className}
style={style}
initial={reduce ? false : { opacity: 0, y: 20 }}
animate={sichtbar || reduce ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{
duration: reduce ? 0 : 0.7,
ease: [0.22, 1, 0.36, 1],
delay: reduce ? 0 : delay,
}}
>
{children}
</motion.div>
);
}
/** Die Marke: fuenf Balken wie ein Pegel. Es geht um Aufnehmen und Zuhoeren. */
function Pegel({ groesse = 40, farbe = BLAU }: { groesse?: number; farbe?: string }) {
const balken = [
{ x: 3, h: 7, o: 0.55 },
{ x: 8.8, h: 13, o: 1 },
{ x: 14.6, h: 20, o: 0.7 },
{ x: 20.4, h: 11, o: 1 },
{ x: 26.2, h: 6, o: 0.55 },
];
return (
<svg
width={groesse}
height={(groesse * 3) / 4}
viewBox="0 0 32 24"
fill="none"
aria-hidden
className="shrink-0"
>
{balken.map((b) => (
<rect
key={b.x}
x={b.x}
y={12 - b.h / 2}
width={2.8}
height={b.h}
rx={1.4}
fill={farbe}
fillOpacity={b.o}
/>
))}
</svg>
);
}
/** Ein Kicker: klein, in Grossbuchstaben, weit gesperrt. */
function Kicker({ children }: { children: ReactNode }) {
return (
<p
className="text-sm uppercase"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "0.16em", color: LEISE }}
>
{children}
</p>
);
}
function Titel({ children, id }: { children: ReactNode; id?: string }) {
return (
<h2
id={id}
className="text-4xl md:text-5xl"
style={{ fontFamily: SANS, fontWeight: 500, letterSpacing: "-0.02em", color: TINTE }}
>
{children}
</h2>
);
}
function Bahn({ children, className = "" }: { children: ReactNode; className?: string }) {
return (
<div className={`mx-auto w-full max-w-[1200px] px-6 md:px-12 ${className}`}>{children}</div>
);
}
/** Der Ring aus Anfangsbuchstaben, statt eines erfundenen Gesichts. */
function Monogramm({ name }: { name: string }) {
return (
<span
aria-hidden
className="grid size-11 shrink-0 place-items-center rounded-full text-sm"
style={{
background: `linear-gradient(135deg, ${BLAU} 0%, ${VIOLETT} 100%)`,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 700,
}}
>
{name.slice(0, 1)}
</span>
);
}
/* ------------------------------------------------------------------- header */
function Kopfzeile() {
const [gescrollt, setGescrollt] = useState(false);
const [offen, setOffen] = useState(false);
useEffect(() => {
const beim = () => setGescrollt(window.scrollY > 12);
beim();
window.addEventListener("scroll", beim, { passive: true });
return () => window.removeEventListener("scroll", beim);
}, []);
useEffect(() => {
if (!offen) return;
const vorher = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = vorher;
};
}, [offen]);
return (
<header
className="sticky top-0 z-40 w-full transition-colors"
style={{
background: gescrollt ? "rgba(255,255,255,0.92)" : "transparent",
borderBottom: gescrollt ? `1px solid ${KANTE}` : "1px solid transparent",
backdropFilter: gescrollt ? "blur(10px)" : undefined,
}}
>
<Bahn className="flex min-h-[72px] items-center justify-between gap-4">
<a href="#top" className="group flex items-center gap-2.5" aria-label={MARKE.name}>
<Pegel />
<span
className="text-lg md:text-xl"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "-0.02em", color: TINTE }}
>
{MARKE.name}
</span>
</a>
<nav aria-label="Main navigation" className="hidden lg:block">
<ul className="flex items-center gap-1">
{NAV.map((eintrag) => (
<li key={eintrag.label}>
<a
href={eintrag.href}
className="inline-flex min-h-11 items-center px-4 py-2 text-base transition-colors"
style={{
borderRadius: KNOPF,
fontFamily: SANS,
fontWeight: 500,
color: "#2c2f38",
}}
>
{eintrag.label}
</a>
</li>
))}
</ul>
</nav>
<div className="flex items-center gap-2">
<a
href="#upload"
className="hidden min-h-11 items-center px-5 py-2.5 text-base transition-colors md:inline-flex"
style={{
borderRadius: KNOPF,
background: BLAU,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 500,
}}
>
Upload a recording
</a>
<button
type="button"
onClick={() => setOffen((v) => !v)}
aria-expanded={offen}
aria-label={offen ? "Close menu" : "Open menu"}
className="inline-flex size-11 items-center justify-center lg:hidden"
style={{
borderRadius: KNOPF,
border: `1px solid ${KANTE}`,
background: "#ffffff",
color: TINTE,
}}
>
{offen ? <X className="size-5" /> : <Menu className="size-5" />}
</button>
</div>
</Bahn>
{offen ? (
<div
className="lg:hidden"
style={{ borderTop: `1px solid ${KANTE}`, background: "#ffffff" }}
>
<Bahn className="py-4">
<ul className="flex flex-col gap-1">
{NAV.map((eintrag) => (
<li key={eintrag.label}>
<a
href={eintrag.href}
onClick={() => setOffen(false)}
className="flex min-h-12 items-center px-4 py-3 text-lg"
style={{ borderRadius: KNOPF, fontFamily: SANS, fontWeight: 500, color: TINTE }}
>
{eintrag.label}
</a>
</li>
))}
<li className="mt-2">
<a
href="#upload"
onClick={() => setOffen(false)}
className="flex min-h-12 items-center justify-center px-4 py-3 text-lg"
style={{
borderRadius: KNOPF,
background: BLAU,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 500,
}}
>
Upload a recording
</a>
</li>
</ul>
</Bahn>
</div>
) : null}
</header>
);
}
/* --------------------------------------------------------------------- hero */
function Held() {
return (
<section id="top" className="relative overflow-hidden">
{/* Ein feines Punktraster, weich ausmaskiert. Es gibt der Flaeche
Struktur, ohne als Leuchten aufzutreten. */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: `radial-gradient(circle, rgba(58,91,196,0.13) 1px, transparent 1px)`,
backgroundSize: "28px 28px",
maskImage: "radial-gradient(ellipse 70% 60% at 70% 40%, black 0%, transparent 75%)",
WebkitMaskImage: "radial-gradient(ellipse 70% 60% at 70% 40%, black 0%, transparent 75%)",
}}
/>
<Bahn className="relative grid gap-10 py-12 md:py-28 lg:grid-cols-[1.05fr_1fr] lg:items-center lg:gap-20 lg:py-32">
<Auftauchen>
<p
className="mb-5 inline-flex items-center gap-2 py-1.5 pe-4 ps-1.5 text-sm md:mb-6"
style={{
borderRadius: KNOPF,
background: STUMM,
color: "#2c2f38",
fontFamily: SANS,
fontWeight: 500,
}}
>
<span
className="grid size-6 place-items-center rounded-full"
style={{ background: "#e6e9f2", color: LEISE }}
>
<Users className="size-3.5" strokeWidth={1.75} aria-hidden />
</span>
Built for teams
</p>
<h1
className="text-[clamp(2rem,5.5vw,4rem)]"
style={{ fontFamily: SANS, fontWeight: 500, lineHeight: 1.05, color: TINTE }}
>
Turn every <Unterstrichen>meeting</Unterstrichen> into a clear recap.
</h1>
<p
className="mt-5 max-w-xl text-lg md:mt-6 md:text-xl"
style={{ fontFamily: SANS, color: LEISE }}
>
{MARKE.name} listens in, transcribes and summarizes. You get decisions, action items and
a clean recap, minutes after the call ends.
</p>
<div className="mt-7 flex flex-wrap items-center gap-3 md:gap-4">
<a
href="#upload"
className="inline-flex min-h-14 items-center gap-2 px-7 py-4 text-lg transition-colors"
style={{
borderRadius: KNOPF,
background: BLAU,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 600,
}}
>
Upload a recording
<ArrowRight className="size-5" aria-hidden />
</a>
<a
href="#how-it-works"
className="inline-flex min-h-14 items-center gap-1.5 px-5 py-4 text-lg"
style={{
fontFamily: SANS,
fontWeight: 500,
color: TINTE,
textDecoration: "underline",
textDecorationColor: BLAU_2,
textDecorationThickness: 2,
textUnderlineOffset: 8,
}}
>
How it works
<ArrowRight className="size-4 opacity-70" aria-hidden />
</a>
</div>
<p
className="mt-6 inline-flex items-start gap-2 text-sm"
style={{ fontFamily: SANS, color: LEISE }}
>
<Lock className="mt-0.5 size-4 shrink-0" aria-hidden />
<span>
Encrypted in transit and at rest. The original audio is deleted after transcription,
you keep the recap.
</span>
</p>
</Auftauchen>
<Auftauchen delay={0.15} className="mx-auto w-full max-w-2xl lg:mx-0">
<Rueckblick />
</Auftauchen>
</Bahn>
</section>
);
}
/** Ein Wort mit einem gezeichneten Strich darunter, der sich einmal zieht. */
function Unterstrichen({ children }: { children: ReactNode }) {
const reduce = useReducedMotion();
const ref = useRef<HTMLSpanElement>(null);
const sichtbar = useInView(ref, { once: true, amount: 0.6 });
return (
<span ref={ref} className="relative inline-block whitespace-nowrap" style={{ color: BLAU }}>
{children}
<svg
aria-hidden
viewBox="0 0 220 12"
preserveAspectRatio="none"
className="pointer-events-none absolute inset-x-0 -bottom-2 h-[0.4em] w-full overflow-visible"
style={{ color: BLAU_2 }}
>
<motion.path
d="M2 7 C 40 2, 90 10, 140 5 S 210 8, 218 4"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.85"
initial={reduce ? false : { pathLength: 0 }}
animate={sichtbar || reduce ? { pathLength: 1 } : { pathLength: 0 }}
transition={{ duration: reduce ? 0 : 0.9, ease: "easeOut", delay: 0.4 }}
/>
</svg>
</span>
);
}
/**
* Die Karte im Hero. Sie zeigt, was das Produkt liefert: der Satz schreibt
* sich Wort fuer Wort, die Aufgaben haken sich nacheinander ab, dann kommt der
* naechste Termin. Beim Anfahren haelt sie an, damit man lesen kann.
*/
function Rueckblick() {
const reduce = useReducedMotion();
const [i, setI] = useState(0);
const pause = useRef(false);
const ref = useRef<HTMLElement>(null);
const sichtbar = useInView(ref, { amount: 0.3 });
useEffect(() => {
if (reduce || !sichtbar) return;
const uhr = window.setInterval(() => {
if (pause.current) return;
setI((v) => (v + 1) % TERMINE.length);
}, 6200);
return () => window.clearInterval(uhr);
}, [reduce, sichtbar]);
const jetzt = TERMINE[i];
return (
<article
ref={ref}
className="relative mx-auto w-full max-w-xl overflow-hidden p-6 sm:p-8"
style={{ borderRadius: 28, border: `1px solid ${KANTE}`, background: "#ffffff" }}
onMouseEnter={() => (pause.current = true)}
onMouseLeave={() => (pause.current = false)}
>
<header className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2.5">
<Pegel groesse={22} />
<p
className="truncate text-sm"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "-0.01em", color: TINTE }}
>
{jetzt.titel}
</p>
</div>
<span
className="inline-flex shrink-0 items-center gap-1.5 px-2.5 py-1 text-[10px]"
style={{
borderRadius: KNOPF,
background: STUMM,
color: "#3a3d47",
fontFamily: SANS,
fontWeight: 500,
}}
>
<motion.span
aria-hidden
className="size-1.5 rounded-full"
style={{ background: GRUEN }}
animate={reduce ? undefined : { opacity: [1, 0.35, 1] }}
transition={reduce ? undefined : { duration: 2, repeat: Infinity, ease: "easeInOut" }}
/>
Done
</span>
</header>
<div className="mt-4 flex items-center gap-3">
<div className="flex gap-1.5">
{jetzt.leute.map((kuerzel) => (
<span
key={kuerzel}
className="grid size-7 place-items-center rounded-full text-[10px]"
style={{ background: BLAU_SANFT, color: BLAU, fontFamily: SANS, fontWeight: 600 }}
>
{kuerzel}
</span>
))}
</div>
<span className="text-xs" style={{ fontFamily: SANS, color: LEISE }}>
{jetzt.dauer}
</span>
</div>
<div className="mt-6">
<p
className="text-[10px] uppercase"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "0.18em", color: LEISE }}
>
Summary
</p>
<p
className="mt-2 min-h-[3.4rem] text-[17px] leading-relaxed"
style={{ fontFamily: SANS, color: "#1a1c22" }}
>
<AnimatePresence mode="wait">
<motion.span
key={`s-${i}`}
exit={{ opacity: 0, y: -6 }}
transition={{ duration: 0.25 }}
>
{jetzt.zusammenfassung.map((wort, k) => (
<motion.span
key={`${i}-${k}`}
className="inline-block"
initial={reduce ? false : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: reduce ? 0 : 0.3, delay: reduce ? 0 : k * 0.055 }}
>
{wort}
{k < jetzt.zusammenfassung.length - 1 ? " " : ""}
</motion.span>
))}
</motion.span>
</AnimatePresence>
</p>
</div>
<div className="mt-5 pt-5" style={{ borderTop: `1px solid ${KANTE}` }}>
<p
className="text-[10px] uppercase"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "0.18em", color: LEISE }}
>
Action items
</p>
<ul className="mt-3 min-h-[5.75rem] space-y-2.5">
{jetzt.aufgaben.map((aufgabe, k) => (
<motion.li
key={`${i}-${k}`}
className="flex items-center gap-3"
initial={reduce ? false : { opacity: 0, x: -6 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: reduce ? 0 : 0.35, delay: reduce ? 0 : 0.5 + k * 0.13 }}
>
<span
className="grid size-5 shrink-0 place-items-center"
style={{
borderRadius: 6,
background: aufgabe.fertig ? BLAU : "#ffffff",
border: aufgabe.fertig ? "1px solid transparent" : `1px solid ${KANTE}`,
color: "#ffffff",
}}
>
{aufgabe.fertig ? <Check className="size-3.5" strokeWidth={3} aria-hidden /> : null}
</span>
<span
className="flex-1 truncate text-[15px]"
style={{ fontFamily: SANS, color: TINTE }}
>
{aufgabe.was}
</span>
<span
aria-hidden
className="grid size-6 shrink-0 place-items-center rounded-full text-[10px]"
style={{ background: STUMM, color: "#3a3d47", fontFamily: SANS, fontWeight: 600 }}
>
{aufgabe.wer}
</span>
<span
className="w-14 shrink-0 text-right text-xs"
style={{ fontFamily: SANS, color: LEISE }}
>
{aufgabe.wann}
</span>
</motion.li>
))}
</ul>
</div>
<footer
className="mt-6 flex flex-wrap items-center gap-x-4 gap-y-2 pt-5"
style={{ borderTop: `1px solid ${KANTE}` }}
>
<div
className="flex flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-[11px]"
style={{ fontFamily: SANS, color: LEISE }}
>
<MonitorPlay className="size-3.5 shrink-0" aria-hidden />
<span>Works with</span>
<span style={{ fontWeight: 500, color: "#2c2f38" }}>Zoom · Meet · Teams</span>
</div>
<span
className="inline-flex shrink-0 items-center gap-1.5 text-[11px]"
style={{ fontFamily: SANS, color: LEISE }}
>
<Sparkles className="size-3" aria-hidden />
42 seconds
</span>
</footer>
</article>
);
}
/* ------------------------------------------------------------------ vertrauen */
function Vertrauensleiste() {
return (
<section aria-label="Trust" style={{ background: FLAECHE }}>
<Bahn className="py-6">
<ul
className="flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm md:text-base"
style={{ fontFamily: SANS, color: LEISE }}
>
{VERTRAUEN.map(({ label, Icon }) => (
<li key={label} className="inline-flex items-center gap-2">
<Icon className="size-4" style={{ color: "#7a8090" }} aria-hidden strokeWidth={1.7} />
<span>{label}</span>
</li>
))}
</ul>
</Bahn>
</section>
);
}
/* --------------------------------------------------------------------- weg */
function Produktweg() {
return (
<section id="journey" className="scroll-mt-24 py-16 md:py-32">
<Bahn>
<Auftauchen className="mx-auto max-w-2xl text-center">
<Kicker>How {MARKE.name} works</Kicker>
<div className="mt-3">
<Titel>Capture. Recap. Automate.</Titel>
</div>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: LEISE }}>
Three steps that build on each other, from the recording to the automatic follow-up.
</p>
</Auftauchen>
<ol className="mt-10 grid gap-8 md:mt-16 lg:grid-cols-3">
{WEG.map((teil, i) => (
<li key={teil.name} className="h-full">
<Auftauchen delay={i * 0.12} className="h-full">
<a
href="#pricing"
className="group flex h-full w-full flex-col overflow-hidden p-8 transition-colors"
style={{ borderRadius: KARTE, background: FLAECHE_2 }}
>
<div className="mb-6 flex items-center justify-between">
<span
className="text-sm"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.16em",
color: LEISE,
}}
>
{teil.schritt}
</span>
<span
className="px-3 py-1 text-xs uppercase"
style={{
borderRadius: KNOPF,
background: "#e2e6f0",
color: "#3a3d47",
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.1em",
}}
>
{teil.marke}
</span>
</div>
<div className="mb-6 flex h-12 items-center transition-transform duration-500 ease-out group-hover:scale-105">
<Pegel groesse={56} farbe={teil.farbe} />
</div>
<h3
className="text-2xl"
style={{ fontFamily: SANS, fontWeight: 500, color: TINTE }}
>
{teil.name}
</h3>
<p
className="mt-2 text-base italic"
style={{ fontFamily: SANS, color: "#33363f" }}
>
{teil.frage}
</p>
<p className="mt-3 text-base" style={{ fontFamily: SANS, color: LEISE }}>
{teil.text}
</p>
<span
className="mt-6 inline-flex items-center gap-2 text-base transition-all duration-300 group-hover:gap-3"
style={{ fontFamily: SANS, fontWeight: 500, color: TINTE }}
>
See plans
<ArrowRight
className="size-4 transition-transform duration-300 group-hover:translate-x-1"
aria-hidden
/>
</span>
</a>
</Auftauchen>
</li>
))}
</ol>
</Bahn>
</section>
);
}
/* ----------------------------------------------------------------- stimmen */
function Stimmen() {
return (
<section
id="testimonials"
className="scroll-mt-24 py-16 md:py-32"
style={{ background: FLAECHE }}
>
<Bahn>
<Auftauchen className="mx-auto max-w-2xl text-center">
<Titel>No meeting slips through the cracks</Titel>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: LEISE }}>
Three of many teams that never forget anything important with {MARKE.name}.
</p>
</Auftauchen>
<ul className="mt-12 grid gap-8 md:mt-16 md:grid-cols-3">
{STIMMEN.map((stimme, i) => (
<li key={stimme.name} className="h-full">
<Auftauchen delay={i * 0.12} className="h-full">
<figure
className="flex h-full w-full flex-col p-8"
style={{ borderRadius: KARTE, background: "#ffffff" }}
>
<span
className="flex gap-1"
aria-label="5 out of 5 stars"
style={{ color: VIOLETT }}
>
{Array.from({ length: 5 }).map((_, k) => (
<Star
key={k}
className="size-4"
fill="currentColor"
strokeWidth={0}
aria-hidden
/>
))}
</span>
<blockquote
className="mt-5 flex-1 text-lg leading-relaxed"
style={{ fontFamily: SANS, color: TINTE }}
>
{stimme.text}
</blockquote>
<figcaption className="mt-6 flex items-center gap-3 pt-2">
<Monogramm name={stimme.name} />
<span className="min-w-0">
<span
className="block text-base"
style={{ fontFamily: SANS, fontWeight: 600, color: TINTE }}
>
{stimme.name}
</span>
<span className="block text-sm" style={{ fontFamily: SANS, color: LEISE }}>
{stimme.rolle}
</span>
</span>
</figcaption>
</figure>
</Auftauchen>
</li>
))}
</ul>
</Bahn>
</section>
);
}
/* -------------------------------------------------------------- so laeuft es */
function SoLaeuftEs() {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const sichtbar = useInView(ref, { once: true, margin: "-100px" });
return (
<section id="how-it-works" className="scroll-mt-24 py-16 md:py-32">
<Bahn>
<Auftauchen className="mx-auto max-w-2xl text-center">
<Titel>How it works</Titel>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: LEISE }}>
Ready in under a minute, try it without signing up.
</p>
</Auftauchen>
<div ref={ref} className="relative mt-12 md:mt-20">
{/* Der Verbinder laeuft hinter den drei Karten durch und zeichnet
sich einmal. Nur am Schreibtisch: darunter stehen die Karten
untereinander, und eine Linie quer waere dort sinnlos. */}
<svg
aria-hidden
viewBox="0 0 1000 400"
preserveAspectRatio="none"
className="pointer-events-none absolute inset-0 hidden h-full w-full overflow-visible lg:block"
style={{ color: BLAU_2 }}
>
<motion.path
d="M 157 400 L 157 414 Q 157 426 169 426 L 488 426 Q 500 426 500 414 L 500 -14 Q 500 -26 512 -26 L 831 -26 Q 843 -26 843 -14 L 843 0"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.4"
initial={reduce ? false : { pathLength: 0 }}
animate={sichtbar || reduce ? { pathLength: 1 } : { pathLength: 0 }}
transition={{ duration: reduce ? 0 : 1.6, ease: "easeInOut" }}
/>
</svg>
<ol className="relative grid gap-8 md:grid-cols-3">
{SCHRITTE.map((schritt, i) => (
<li key={schritt.titel}>
<Auftauchen delay={i * 0.12}>
<div
className="h-full p-6 md:p-8"
style={{ borderRadius: KARTE, background: FLAECHE_2 }}
>
<div className="flex items-center gap-3">
<span
className="grid size-10 shrink-0 place-items-center text-base"
style={{
borderRadius: 12,
background: BLAU_SANFT,
color: BLAU,
fontFamily: SANS,
fontWeight: 600,
}}
>
{String(i + 1).padStart(2, "0")}
</span>
<h3
className="text-xl"
style={{ fontFamily: SANS, fontWeight: 500, color: TINTE }}
>
{schritt.titel}
</h3>
</div>
<p className="mt-3 text-base" style={{ fontFamily: SANS, color: LEISE }}>
{schritt.text}
</p>
</div>
</Auftauchen>
</li>
))}
</ol>
</div>
<p
className="mt-16 text-center text-sm md:mt-20"
style={{ fontFamily: SANS, fontWeight: 500, color: LEISE }}
>
Takes under a minute.
</p>
</Bahn>
</section>
);
}
/* ------------------------------------------------------------------- archiv */
function Archiv() {
return (
<section id="archive" className="scroll-mt-24 py-16 md:py-28">
<Bahn>
<Auftauchen>
<div
className="relative overflow-hidden"
style={{
borderRadius: 32,
border: `1px solid ${KANTE}`,
backgroundImage: `linear-gradient(135deg, ${FLAECHE} 0%, #ffffff 46%, ${BLAU_SANFT} 100%)`,
}}
>
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage:
"radial-gradient(circle, rgba(58,91,196,0.14) 1px, transparent 1px)",
backgroundSize: "24px 24px",
maskImage: "radial-gradient(ellipse 75% 85% at 82% 55%, #000 0%, transparent 72%)",
WebkitMaskImage:
"radial-gradient(ellipse 75% 85% at 82% 55%, #000 0%, transparent 72%)",
}}
/>
<div className="relative grid items-center gap-10 p-8 md:gap-14 md:p-14 lg:grid-cols-2">
<div className="order-2 lg:order-1">
<Kicker>Your meeting memory</Kicker>
<h2
className="mt-3 text-3xl leading-[1.1] md:text-4xl lg:text-[2.75rem]"
style={{
fontFamily: SANS,
fontWeight: 500,
letterSpacing: "-0.02em",
color: TINTE,
}}
>
Every meeting. In one place.
</h2>
<p
className="mt-5 text-lg leading-relaxed"
style={{ fontFamily: SANS, color: LEISE }}
>
{MARKE.name} files every recap neatly, with summary, decisions and action items.
So you can find any point again, share results in one click, and nobody has to
scramble to take notes.
</p>
<ul className="mt-7 space-y-3">
{ARCHIV_PUNKTE.map((punkt) => (
<li key={punkt} className="flex items-start gap-3">
<span
className="mt-0.5 grid size-6 shrink-0 place-items-center rounded-full"
style={{ background: BLAU, color: "#ffffff" }}
>
<Check className="size-3.5" strokeWidth={3} aria-hidden />
</span>
<span className="text-base" style={{ fontFamily: SANS, color: TINTE }}>
{punkt}
</span>
</li>
))}
</ul>
<a
href="#upload"
className="mt-8 inline-flex items-center gap-1.5 text-base"
style={{ fontFamily: SANS, fontWeight: 500, color: TINTE }}
>
Try it for free
<ArrowRight className="size-4" aria-hidden />
</a>
</div>
<div className="order-1 lg:order-2">
<ArchivBild />
</div>
</div>
</div>
</Auftauchen>
</Bahn>
</section>
);
}
/**
* Ein Stapel Rueckblicke als Zeichnung. Tiefe kommt aus Kante und Versatz,
* nicht aus einem Schatten. Der Pegel atmet, die Haken zeichnen sich, sobald
* das Bild im Sichtfeld steht.
*/
function ArchivBild() {
const reduce = useReducedMotion();
const ref = useRef<SVGSVGElement>(null);
const sichtbar = useInView(ref, { once: true, amount: 0.3 });
const balken = [
{ x: 202, h: 10, f: BLAU },
{ x: 208, h: 18, f: BLAU_2 },
{ x: 214, h: 24, f: VIOLETT },
{ x: 220, h: 14, f: BLAU },
{ x: 226, h: 8, f: BLAU_2 },
];
const zeilen = [
{ y: 168, w: 200 },
{ y: 190, w: 232 },
{ y: 212, w: 168 },
];
const haken = [252, 292, 332];
return (
<svg
ref={ref}
viewBox="0 0 640 480"
fill="none"
role="img"
aria-label="Stacked meeting recaps with search and completed action items"
className="w-full"
>
<rect
x="215"
y="72"
width="210"
height="258"
rx="18"
fill="#e2e7f6"
stroke="#dfe3ee"
strokeWidth="1.5"
/>
<rect
x="196"
y="88"
width="248"
height="266"
rx="20"
fill={BLAU_SANFT}
stroke="#dfe3ee"
strokeWidth="1.5"
/>
<rect
x="176"
y="104"
width="288"
height="288"
rx="22"
fill="#ffffff"
stroke="#e4e7ef"
strokeWidth="1.5"
/>
{balken.map((b, i) => (
<motion.rect
key={b.x}
x={b.x}
y={137 - b.h}
width={3.4}
height={b.h}
rx={1.7}
fill={b.f}
style={{ originY: 1 }}
animate={reduce ? undefined : { scaleY: [1, 0.55, 1] }}
transition={
reduce
? undefined
: { duration: 1.8, repeat: Infinity, ease: "easeInOut", delay: i * 0.12 }
}
/>
))}
<rect x="240" y="124" width="110" height="11" rx="5.5" fill="#dfe3ee" />
<rect x="388" y="122" width="50" height="20" rx="10" fill="#e9f4ee" />
<circle cx="399" cy="132" r="3.4" fill={GRUEN} />
<rect x="407" y="128" width="24" height="8" rx="4" fill="#bcdcc9" />
{zeilen.map((z) => (
<rect key={z.y} x="202" y={z.y} width={z.w} height="9" rx="4.5" fill="#e8ebf3" />
))}
{haken.map((y, i) => (
<g key={y}>
<rect x="202" y={y} width="236" height="30" rx="10" fill={FLAECHE} />
<motion.path
d={`M 214 ${y + 15} l 5 5 l 9 -10`}
fill="none"
stroke={BLAU}
strokeWidth="2.6"
strokeLinecap="round"
strokeLinejoin="round"
initial={reduce ? false : { pathLength: 0 }}
animate={sichtbar || reduce ? { pathLength: 1 } : { pathLength: 0 }}
transition={{ duration: reduce ? 0 : 0.4, delay: reduce ? 0 : 0.3 + i * 0.22 }}
/>
<rect x="240" y={y + 11} width={150 - i * 26} height="8" rx="4" fill="#dfe3ee" />
</g>
))}
{/* Der Sucher links: eine Lupe auf einem Feld. */}
<rect
x="58"
y="196"
width="150"
height="44"
rx="14"
fill="#ffffff"
stroke="#e4e7ef"
strokeWidth="1.5"
/>
<circle cx="82" cy="218" r="7.5" fill="none" stroke={LEISE} strokeWidth="2.2" />
<path d="M 88 224 l 7 7" stroke={LEISE} strokeWidth="2.2" strokeLinecap="round" />
<rect x="102" y="214" width="86" height="8" rx="4" fill="#e8ebf3" />
{/* Der Schnipsel mit dem aufgenommenen Ton. */}
<rect
x="46"
y="292"
width="130"
height="60"
rx="16"
fill="#ffffff"
stroke="#e4e7ef"
strokeWidth="1.5"
/>
{[
{ x: 62, h: 8, f: BLAU_2 },
{ x: 70, h: 16, f: BLAU },
{ x: 78, h: 26, f: VIOLETT },
{ x: 86, h: 12, f: BLAU },
{ x: 94, h: 6, f: BLAU_2 },
{ x: 102, h: 20, f: BLAU },
{ x: 110, h: 10, f: VIOLETT },
].map((b, i) => (
<motion.rect
key={b.x}
x={b.x}
y={334 - b.h}
width={3.4}
height={b.h}
rx={1.7}
fill={b.f}
style={{ originY: 1 }}
animate={reduce ? undefined : { scaleY: [1, 0.5, 1] }}
transition={
reduce
? undefined
: { duration: 1.6, repeat: Infinity, ease: "easeInOut", delay: i * 0.09 }
}
/>
))}
<rect x="122" y="310" width="40" height="8" rx="4" fill="#e8ebf3" />
<rect x="122" y="324" width="26" height="8" rx="4" fill="#eef0f6" />
</svg>
);
}
/* --------------------------------------------------------------- hochladen */
function Hochladen() {
const reduce = useReducedMotion();
const [zustand, setZustand] = useState<"ruhe" | "laeuft" | "fertig">("ruhe");
const [schritt, setSchritt] = useState(0);
const [ueber, setUeber] = useState(false);
const starten = useCallback(() => {
if (zustand === "laeuft") return;
setZustand("laeuft");
setSchritt(0);
}, [zustand]);
useEffect(() => {
if (zustand !== "laeuft") return;
if (reduce) {
setSchritt(HOCHLADEN_SCHRITTE.length);
setZustand("fertig");
return;
}
if (schritt >= HOCHLADEN_SCHRITTE.length) {
const t = window.setTimeout(() => setZustand("fertig"), 500);
return () => window.clearTimeout(t);
}
const t = window.setTimeout(() => setSchritt((s) => s + 1), 850);
return () => window.clearTimeout(t);
}, [zustand, schritt, reduce]);
const anteil = Math.round(
(Math.min(schritt, HOCHLADEN_SCHRITTE.length) / HOCHLADEN_SCHRITTE.length) * 100,
);
return (
<section id="upload" className="scroll-mt-24 py-16 md:py-32" style={{ background: FLAECHE }}>
<Bahn>
<Auftauchen className="mx-auto max-w-2xl text-center">
<Kicker>Try it now</Kicker>
<div className="mt-3">
<Titel>Upload a recording</Titel>
</div>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: LEISE }}>
No account needed. You will see exactly what your recap will look like.
</p>
</Auftauchen>
<Auftauchen delay={0.1} className="mx-auto mt-10 max-w-3xl md:mt-14">
<div className="p-6 sm:p-10" style={{ borderRadius: 28, background: "#ffffff" }}>
{zustand === "ruhe" ? (
<button
type="button"
onClick={starten}
onDragOver={(e) => {
e.preventDefault();
setUeber(true);
}}
onDragLeave={() => setUeber(false)}
onDrop={(e) => {
e.preventDefault();
setUeber(false);
starten();
}}
className="flex w-full flex-col items-center justify-center px-6 py-14 text-center transition-colors"
style={{
borderRadius: KARTE,
border: `2px dashed ${ueber ? BLAU : "#ccd3e6"}`,
background: ueber ? BLAU_SANFT : FLAECHE,
}}
>
<span
className="grid size-14 place-items-center rounded-full"
style={{ background: BLAU_SANFT, color: BLAU }}
>
<FileAudio className="size-6" strokeWidth={1.7} aria-hidden />
</span>
<span
className="mt-5 text-xl"
style={{ fontFamily: SANS, fontWeight: 500, color: TINTE }}
>
{ueber ? `Release, ${MARKE.name} takes over` : "Drop your file here"}
</span>
<span className="mt-2 max-w-sm text-sm" style={{ fontFamily: SANS, color: LEISE }}>
or click to choose. MP3, WAV, M4A or video, even a Zoom recording.
</span>
</button>
) : (
<div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<span
className="grid size-10 shrink-0 place-items-center rounded-full"
style={{ background: BLAU_SANFT, color: BLAU }}
>
<FileAudio className="size-5" strokeWidth={1.7} aria-hidden />
</span>
<span className="min-w-0">
<span
className="block truncate text-base"
style={{ fontFamily: SANS, fontWeight: 500, color: TINTE }}
>
weekly-sync.mp3
</span>
<span className="block text-sm" style={{ fontFamily: SANS, color: LEISE }}>
{zustand === "fertig"
? "summarized in 42 seconds"
: `Step ${Math.min(schritt + 1, HOCHLADEN_SCHRITTE.length)} of ${HOCHLADEN_SCHRITTE.length}`}
</span>
</span>
</div>
<span
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs"
style={{
borderRadius: KNOPF,
background: zustand === "fertig" ? "#e9f4ee" : STUMM,
color: zustand === "fertig" ? GRUEN : "#3a3d47",
fontFamily: SANS,
fontWeight: 600,
}}
>
{zustand === "fertig" ? "Done" : `${anteil}%`}
</span>
</div>
{/* Der Balken laeuft mit den Schritten, nicht mit einer Uhr:
er zeigt, wo die Arbeit steht, nicht wie lange sie dauert. */}
<div
className="mt-6 h-1.5 w-full overflow-hidden"
style={{ borderRadius: 999, background: STUMM }}
>
<motion.div
className="h-full"
style={{ background: BLAU, borderRadius: 999 }}
initial={{ width: "0%" }}
animate={{ width: `${anteil}%` }}
transition={{ duration: reduce ? 0 : 0.6, ease: "easeOut" }}
/>
</div>
<ul className="mt-7 space-y-3">
{HOCHLADEN_SCHRITTE.map((text, i) => {
const fertig = i < schritt;
return (
<li key={text} className="flex items-center gap-3">
<span
className="grid size-6 shrink-0 place-items-center rounded-full transition-colors"
style={{
background: fertig ? BLAU : STUMM,
color: fertig ? "#ffffff" : "#aeb3c2",
}}
>
<Check className="size-3.5" strokeWidth={3} aria-hidden />
</span>
<span
className="text-base transition-colors"
style={{ fontFamily: SANS, color: fertig ? TINTE : LEISE }}
>
{text}
</span>
</li>
);
})}
</ul>
{zustand === "fertig" ? (
<div
className="mt-8 p-6"
style={{
borderRadius: KARTE,
background: FLAECHE,
border: `1px solid ${KANTE}`,
}}
>
<p
className="text-[10px] uppercase"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.18em",
color: LEISE,
}}
>
Summary
</p>
<p
className="mt-2 text-[17px] leading-relaxed"
style={{ fontFamily: SANS, color: TINTE }}
>
The team approved the 12% discount for deals closed by end of Q3. The API
blocker will be resolved with Mert tomorrow, and the revised timeline goes to
the client by Friday.
</p>
<button
type="button"
onClick={() => {
setZustand("ruhe");
setSchritt(0);
}}
className="mt-6 inline-flex items-center gap-2 px-5 py-2.5 text-sm"
style={{
borderRadius: KNOPF,
background: TINTE,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 500,
}}
>
New recording
</button>
</div>
) : null}
</div>
)}
<ul
className="mt-7 flex flex-wrap items-center gap-x-6 gap-y-2 text-xs"
style={{ fontFamily: SANS, color: LEISE }}
>
{[
"Encrypted in transit and at rest",
"GDPR-compliant, servers in the EU",
"Audio deleted after transcription",
].map((punkt) => (
<li key={punkt} className="inline-flex items-center gap-1.5">
<ShieldCheck
className="size-3.5"
style={{ color: BLAU }}
strokeWidth={1.8}
aria-hidden
/>
{punkt}
</li>
))}
</ul>
</div>
</Auftauchen>
</Bahn>
</section>
);
}
/* ------------------------------------------------------------------- preise */
function Preise() {
return (
<section id="pricing" className="scroll-mt-24 py-16 md:py-32">
<Bahn>
<Auftauchen className="mx-auto max-w-2xl text-center">
<Kicker>Pricing</Kicker>
<div className="mt-3">
<Titel>Fair pricing, no surprises</Titel>
</div>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: LEISE }}>
You only pay for what you use, and can cancel anytime.
</p>
</Auftauchen>
<ul className="mt-14 grid gap-8 lg:mt-16 lg:grid-cols-3">
{PREISE.map((plan, i) => (
<li key={plan.name} className="h-full">
<Auftauchen delay={i * 0.12} className="h-full">
<div className="relative h-full w-full pt-4">
{plan.hervor ? (
<span
className="pointer-events-none absolute left-1/2 top-4 z-10 inline-flex -translate-x-1/2 -translate-y-1/2 items-center gap-1.5 whitespace-nowrap px-3 py-1 text-xs"
style={{
borderRadius: KNOPF,
background: VIOLETT,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 600,
}}
>
<Sparkles className="size-3.5" aria-hidden />
Recommended
</span>
) : null}
<div
className="relative flex h-full flex-col p-6 sm:p-8"
style={{
borderRadius: KARTE,
background: plan.hervor ? BLAU : FLAECHE,
color: plan.hervor ? "#ffffff" : TINTE,
border: `1px solid ${plan.hervor ? BLAU : KANTE}`,
}}
>
<span
className="text-xs uppercase"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.16em",
color: plan.hervor ? "rgba(255,255,255,0.9)" : BLAU,
}}
>
{plan.marke}
</span>
<h3
className="mt-2 text-2xl"
style={{ fontFamily: SANS, fontWeight: 500, letterSpacing: "-0.02em" }}
>
{plan.name}
</h3>
<p
className="mt-2 text-base"
style={{
fontFamily: SANS,
color: plan.hervor ? "rgba(255,255,255,0.85)" : LEISE,
}}
>
{plan.text}
</p>
<div className="mt-8 flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span
className="text-4xl sm:text-5xl"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "-0.03em" }}
>
{plan.preis}
</span>
<span
className="text-sm"
style={{
fontFamily: SANS,
color: plan.hervor ? "rgba(255,255,255,0.9)" : LEISE,
}}
>
{plan.je}
</span>
</div>
<div
aria-hidden
className="my-8 h-px w-full"
style={{ background: plan.hervor ? "rgba(255,255,255,0.22)" : KANTE }}
/>
<ul className="space-y-3.5">
{plan.punkte.map((punkt) => (
<li key={punkt} className="flex items-start gap-3 text-base">
<span
className="mt-0.5 grid size-5 shrink-0 place-items-center rounded-full"
style={{
background: plan.hervor ? "rgba(255,255,255,0.18)" : BLAU_SANFT,
color: plan.hervor ? "#ffffff" : BLAU,
}}
>
<Check className="size-3" strokeWidth={3} aria-hidden />
</span>
<span
style={{
fontFamily: SANS,
color: plan.hervor ? "rgba(255,255,255,0.95)" : TINTE,
}}
>
{punkt}
</span>
</li>
))}
</ul>
<div className="mt-auto pt-10">
<a
href="#upload"
className="inline-flex min-h-12 w-full items-center justify-center px-6 py-3 text-base sm:w-auto"
style={{
borderRadius: KNOPF,
background: plan.hervor ? "#ffffff" : TINTE,
color: plan.hervor ? BLAU : "#ffffff",
fontFamily: SANS,
fontWeight: 600,
}}
>
{plan.cta}
</a>
</div>
</div>
</div>
</Auftauchen>
</li>
))}
</ul>
</Bahn>
</section>
);
}
/* -------------------------------------------------------------- sicherheit */
function Sicherheit() {
return (
<section id="security" className="scroll-mt-24 py-16 md:py-32" style={{ background: FLAECHE }}>
<Bahn>
<Auftauchen className="mx-auto max-w-2xl text-center">
<Kicker>Security and privacy</Kicker>
<div className="mt-3">
<Titel>Your meetings stay yours</Titel>
</div>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: LEISE }}>
Built for sensitive conversations, with clear rules on where your data goes and how long
it stays.
</p>
</Auftauchen>
<div className="mt-12 grid items-center gap-10 md:mt-16 lg:grid-cols-2 lg:gap-16">
<Auftauchen className="order-1">
<SchildBild />
</Auftauchen>
<ul className="order-2 space-y-7">
{SICHERHEIT.map((punkt, i) => (
<li key={punkt.titel}>
<Auftauchen delay={i * 0.1} className="flex items-start gap-4">
<span
className="grid size-11 shrink-0 place-items-center"
style={{ borderRadius: 12, background: BLAU_SANFT, color: BLAU }}
>
<punkt.Icon className="size-5" strokeWidth={1.75} aria-hidden />
</span>
<div>
<h3
className="text-lg"
style={{
fontFamily: SANS,
fontWeight: 500,
letterSpacing: "-0.01em",
color: TINTE,
}}
>
{punkt.titel}
</h3>
<p
className="mt-1.5 text-sm leading-relaxed"
style={{ fontFamily: SANS, color: LEISE }}
>
{punkt.text}
</p>
</div>
</Auftauchen>
</li>
))}
</ul>
</div>
</Bahn>
</section>
);
}
/** Ein Schild, das sich einmal zeichnet, mit einem Schloss darin. */
function SchildBild() {
const reduce = useReducedMotion();
const ref = useRef<SVGSVGElement>(null);
const sichtbar = useInView(ref, { once: true, amount: 0.4 });
return (
<svg
ref={ref}
viewBox="0 0 420 360"
fill="none"
role="img"
aria-label="A shield with a lock, standing on a base of stored recaps"
className="mx-auto w-full max-w-md lg:max-w-none"
>
<rect x="86" y="266" width="248" height="20" rx="10" fill="#e2e7f6" />
<rect x="112" y="296" width="196" height="16" rx="8" fill={BLAU_SANFT} />
<rect x="142" y="322" width="136" height="12" rx="6" fill="#eef0f6" />
<motion.path
d="M 210 36 L 322 74 V 158 C 322 214 274 250 210 268 C 146 250 98 214 98 158 V 74 Z"
fill="#ffffff"
stroke={BLAU}
strokeWidth="2.4"
strokeLinejoin="round"
initial={reduce ? false : { pathLength: 0 }}
animate={sichtbar || reduce ? { pathLength: 1 } : { pathLength: 0 }}
transition={{ duration: reduce ? 0 : 1.3, ease: "easeInOut" }}
/>
<path
d="M 210 52 L 306 84 V 156 C 306 204 266 236 210 252 C 154 236 114 204 114 156 V 84 Z"
fill={BLAU_SANFT}
opacity="0.55"
/>
<rect x="182" y="150" width="56" height="46" rx="10" fill={BLAU} />
<path
d="M 192 150 v -12 a 18 18 0 0 1 36 0 v 12"
fill="none"
stroke={BLAU}
strokeWidth="7"
strokeLinecap="round"
/>
<circle cx="210" cy="170" r="5" fill="#ffffff" />
<rect x="207" y="172" width="6" height="12" rx="3" fill="#ffffff" />
<motion.circle
cx="210"
cy="152"
r="108"
fill="none"
stroke={BLAU_2}
strokeWidth="1.2"
opacity="0.35"
strokeDasharray="4 10"
animate={reduce ? undefined : { rotate: 360 }}
transition={reduce ? undefined : { duration: 70, repeat: Infinity, ease: "linear" }}
style={{ originX: "210px", originY: "152px" }}
/>
</svg>
);
}
/* --------------------------------------------------------------- fusszeile */
function Fusszeile() {
return (
<footer id="about" className="scroll-mt-24" style={{ background: FLAECHE }}>
<div className="mx-auto w-full max-w-[1200px] px-6 pb-10 pt-2 md:px-12 md:pb-16 md:pt-4">
<div
className="px-6 py-12 sm:px-12 md:px-16 md:py-24 lg:px-20 lg:py-28"
style={{ borderRadius: 40, background: "#ffffff" }}
>
<div className="grid gap-12 md:grid-cols-12">
<div className="md:col-span-5">
<div className="flex items-center gap-2.5">
<Pegel />
<span
className="text-xl md:text-2xl"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "-0.02em",
color: TINTE,
}}
>
{MARKE.name}
</span>
</div>
<p
className="mt-5 max-w-md text-base leading-relaxed"
style={{ fontFamily: SANS, color: LEISE }}
>
{MARKE.name} turns every meeting into a clear recap, with decisions, action items
and follow-up.
</p>
<a
href={`mailto:${MARKE.mail}`}
className="mt-6 inline-flex items-center gap-2 text-base"
style={{ fontFamily: SANS, fontWeight: 500, color: TINTE }}
>
<Mail className="size-4" aria-hidden />
{MARKE.mail}
</a>
<ul className="mt-8 flex flex-wrap gap-2">
{[
{ label: "GDPR-compliant", Icon: ShieldCheck },
{ label: "Encrypted in transit and at rest", Icon: Lock },
{ label: "Servers in the EU", Icon: MapPin },
].map(({ label, Icon }) => (
<li
key={label}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs"
style={{
borderRadius: KNOPF,
background: BLAU_SANFT,
color: "#3a3d47",
fontFamily: SANS,
fontWeight: 500,
}}
>
<Icon
className="size-3.5"
style={{ color: BLAU }}
aria-hidden
strokeWidth={1.8}
/>
{label}
</li>
))}
</ul>
</div>
<nav
aria-label="Footer"
className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:col-span-7"
>
{FUSS_SPALTEN.map((spalte) => (
<div key={spalte.titel}>
<h3
className="text-xs uppercase"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.16em",
color: "#6d7280",
}}
>
{spalte.titel}
</h3>
<ul className="mt-3 space-y-1">
{spalte.links.map((link) => (
<li key={link}>
<a
href="#top"
className="inline-block py-1.5 text-base transition-colors"
style={{ fontFamily: SANS, color: "#2c2f38" }}
>
{link}
</a>
</li>
))}
</ul>
</div>
))}
</nav>
</div>
<div
className="mt-14 flex flex-col gap-3 pt-6 text-sm md:flex-row md:items-center md:justify-between"
style={{ borderTop: `1px solid ${KANTE}`, fontFamily: SANS, color: LEISE }}
>
<p>© 2026 {MARKE.name}. All rights reserved.</p>
<p>{MARKE.name} produces automatic summaries, please review for accuracy.</p>
</div>
</div>
</div>
</footer>
);
}
/* --------------------------------------------------------------------- page */
export function ConferMeetingsPage() {
return (
<div style={{ background: "#ffffff", color: TINTE, fontFamily: SANS }}>
<Kopfzeile />
<main>
<Held />
<Vertrauensleiste />
<Produktweg />
<Stimmen />
<SoLaeuftEs />
<Archiv />
<Hochladen />
<Preise />
<Sicherheit />
</main>
<Fusszeile />
</div>
);
}
export default ConferMeetingsPage;
Frequently asked questions
A curated landing page made of matching library sections in a sensible order from hero to footer. You copy the sections one by one and assemble them with the composition snippet.
Didn't find your question? We're happy to help.
Get in touch