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 INK = "#0a0a0a";
const QUIET = "#545a6b";
const BLUE = "#3a5bc4";
const BLUE_2 = "#5069ce";
const BLUE_GENTLE = "#ebeefb";
const VIOLET = "#6b54ce";
const AREA = "#f6f7fb";
const AREA_2 = "#eceff6";
const MUTED = "#f2f4f9";
const EDGE = "#e3e6ef";
const GREEN = "#2f8f5b";
const SANS = '"Albert Sans", ui-sans-serif, system-ui, sans-serif';
// Cards rounded generously, buttons tightly: the shape of the surface, not of the key.
const CARD = 24;
const BUTTON = 8;
const MARK = { name: "Confer", mail: "hello@confer.app" };
const PRODUCTS = {
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 Task = { what: string; who: string; when: string; done: boolean };
type Appointment = {
title: string;
leute: string[];
duration: string;
zusammenfassung: string[];
aufgaben: Task[];
};
const APPOINTMENTS: Appointment[] = [
{
title: "Weekly Sync",
leute: ["MK", "AR", "SL"],
duration: "34 min",
zusammenfassung: [
"12%",
"discount",
"to",
"Q3",
"approved,",
"API",
"blocker",
"with",
"Mert",
"tomorrow.",
],
aufgaben: [
{ what: "Resolve API blocker", who: "MK", when: "tomorrow", done: true },
{ what: "Send timeline", who: "SL", when: "Fri", done: true },
{ what: "Log discount approval", who: "AR", when: "this wk", done: false },
],
},
{
title: "Sales Call · Northwind",
leute: ["JD", "KE"],
duration: "22 min",
zusammenfassung: [
"Sent",
"offer",
"with",
"12%",
"discount,",
"close",
"expected",
"by",
"Q3.",
],
aufgaben: [
{ what: "Send the offer", who: "JD", when: "today", done: true },
{ what: "Follow up", who: "KE", when: "Tue", done: false },
],
},
{
title: "Client · Meyer",
leute: ["SL", "TN", "KU"],
duration: "41 min",
zusammenfassung: ["Timeline", "slips,", "new", "plan", "promised", "by", "Friday."],
aufgaben: [
{ what: "Revise the timeline", who: "SL", when: "Fri", done: true },
{ what: "Book follow-up", who: "TN", when: "Wed", done: false },
],
},
];
const TRUST = [
{ label: "Used by teams in 20+ countries", Icon: Globe2 },
{ label: "GDPR-compliant", Icon: ShieldCheck },
{ label: "Encrypted transfer", Icon: Lock },
{ label: "Works with your video calls", Icon: Video },
{ label: "Participants are notified", Icon: BellRing },
];
const WEG = [
{
step: "01",
marke: "Capture",
name: PRODUCTS.capture,
frage: "“What was said?”",
text: "Records and transcribes your meetings, live in your video call or from a file.",
color: BLUE,
},
{
step: "02",
marke: "Recap",
name: PRODUCTS.recap,
frage: "“What needs doing?”",
text: "Creates a summary, decisions and action items, including a draft for the follow-up.",
color: "#2c4699",
},
{
step: "03",
marke: "Automate",
name: PRODUCTS.automate,
frage: "“How does it all connect?”",
text: "Pushes results to your tools and makes every meeting searchable.",
color: BLUE_2,
},
];
const VOICES = [
{
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 STEPS = [
{
title: "Connect your meeting or upload a recording",
text: "Live in your video call, or an MP3 or video file.",
},
{ title: "Confer transcribes and summarizes", text: "Calm, careful, in plain language." },
{
title: "You get notes, decisions and action items",
text: "Including a follow-up draft and open questions.",
},
];
const ARCHIV_DOTS = [
"A searchable archive of every meeting",
"Results shared in a single click",
"Nothing important slips away",
];
const UPLOAD_STEPS = [
"Transcribed audio",
"Identified speakers",
"Wrote the summary",
"Extracted action items",
];
const PRICES = [
{
name: PRODUCTS.capture,
marke: "Capture",
preis: "€0",
je: "free",
text: "For single meetings and trying things out.",
points: [
"Up to 5 meetings per month",
"Plain-text transcription",
"Automatic summary",
"Export as PDF",
],
cta: "Start for free",
hervor: false,
},
{
name: PRODUCTS.recap,
marke: "Recap",
preis: "€12",
je: "per user / month",
text: "For teams with a lot of meetings.",
points: [
`Everything in ${PRODUCTS.capture}`,
"Decisions and action items with owners",
"Follow-up drafts",
"Up to 30 meetings per month",
],
cta: "Start 30-day trial",
hervor: true,
},
{
name: PRODUCTS.automate,
marke: "Automate",
preis: "€29",
je: "per user / month",
text: "For teams that want to connect everything.",
points: [
`Everything in ${PRODUCTS.recap}`,
"Integrations (chat, notes and CRM)",
"Searchable meeting archive",
"Analytics across all meetings",
],
cta: "Contact sales",
hervor: false,
},
];
const SAFETY = [
{
Icon: MapPin,
title: "Hosted in the EU",
text: "Processed and stored exclusively on servers in the European Union.",
},
{
Icon: Lock,
title: "Encrypted in transit and at rest",
text: "TLS in transit, encryption at rest.",
},
{
Icon: Trash2,
title: "Audio is deleted",
text: "The original audio is removed after transcription, you keep the recap.",
},
{
Icon: UserCheck,
title: "With consent",
text: "Participants are notified of the recording. You stay in control of your data.",
},
];
const FOOT_COLUMNS = [
{ title: "Product", links: ["How it works", "Pricing", "Testimonials", "Upload a recording"] },
{ title: "Company", links: ["About", "Contact", "Press", "Careers"] },
{ title: "Legal", links: ["Privacy", "Legal notice", "Terms", "Cookie settings"] },
];
/* --------------------------------------------------------------- primitives */
function Appear({
children,
delay = 0,
className = "",
style,
}: {
children: ReactNode;
delay?: number;
className?: string;
style?: CSSProperties;
}) {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const visible = useInView(ref, { once: true, margin: "-70px" });
return (
<motion.div
ref={ref}
className={className}
style={style}
initial={reduce ? false : { opacity: 0, y: 20 }}
animate={visible || 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>
);
}
// The brand: five bars like a level meter. It is about recording and listening.
function Level({ groesse = 40, color = BLUE }: { groesse?: number; color?: 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={color}
fillOpacity={b.o}
/>
))}
</svg>
);
}
/** A kicker: small, in capitals, widely tracked. */
function Kicker({ children }: { children: ReactNode }) {
return (
<p
className="text-sm uppercase"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "0.16em", color: QUIET }}
>
{children}
</p>
);
}
function Title({ 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: INK }}
>
{children}
</h2>
);
}
function Track({ children, className = "" }: { children: ReactNode; className?: string }) {
return (
<div className={`mx-auto w-full max-w-[1200px] px-6 md:px-12 ${className}`}>{children}</div>
);
}
// The ring of initials, instead of an invented face.
function Monogram({ 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, ${BLUE} 0%, ${VIOLET} 100%)`,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 700,
}}
>
{name.slice(0, 1)}
</span>
);
}
/* ------------------------------------------------------------------- header */
function Header() {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const onHover = () => setScrolled(window.scrollY > 12);
onHover();
window.addEventListener("scroll", onHover, { passive: true });
return () => window.removeEventListener("scroll", onHover);
}, []);
useEffect(() => {
if (!open) return;
const before = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = before;
};
}, [open]);
return (
<header
className="sticky top-0 z-40 w-full transition-colors"
style={{
background: scrolled ? "rgba(255,255,255,0.92)" : "transparent",
borderBottom: scrolled ? `1px solid ${EDGE}` : "1px solid transparent",
backdropFilter: scrolled ? "blur(10px)" : undefined,
}}
>
<Track className="flex min-h-[72px] items-center justify-between gap-4">
<a href="#top" className="group flex items-center gap-2.5" aria-label={MARK.name}>
<Level />
<span
className="text-lg md:text-xl"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "-0.02em", color: INK }}
>
{MARK.name}
</span>
</a>
<nav aria-label="Main navigation" className="hidden lg:block">
<ul className="flex items-center gap-1">
{NAV.map((entry) => (
<li key={entry.label}>
<a
href={entry.href}
className="inline-flex min-h-11 items-center px-4 py-2 text-base transition-colors"
style={{
borderRadius: BUTTON,
fontFamily: SANS,
fontWeight: 500,
color: "#2c2f38",
}}
>
{entry.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: BUTTON,
background: BLUE,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 500,
}}
>
Upload a recording
</a>
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-label={open ? "Close menu" : "Open menu"}
className="inline-flex size-11 items-center justify-center lg:hidden"
style={{
borderRadius: BUTTON,
border: `1px solid ${EDGE}`,
background: "#ffffff",
color: INK,
}}
>
{open ? <X className="size-5" /> : <Menu className="size-5" />}
</button>
</div>
</Track>
{open ? (
<div
className="lg:hidden"
style={{ borderTop: `1px solid ${EDGE}`, background: "#ffffff" }}
>
<Track className="py-4">
<ul className="flex flex-col gap-1">
{NAV.map((entry) => (
<li key={entry.label}>
<a
href={entry.href}
onClick={() => setOpen(false)}
className="flex min-h-12 items-center px-4 py-3 text-lg"
style={{ borderRadius: BUTTON, fontFamily: SANS, fontWeight: 500, color: INK }}
>
{entry.label}
</a>
</li>
))}
<li className="mt-2">
<a
href="#upload"
onClick={() => setOpen(false)}
className="flex min-h-12 items-center justify-center px-4 py-3 text-lg"
style={{
borderRadius: BUTTON,
background: BLUE,
color: "#ffffff",
fontFamily: SANS,
fontWeight: 500,
}}
>
Upload a recording
</a>
</li>
</ul>
</Track>
</div>
) : null}
</header>
);
}
/* --------------------------------------------------------------------- hero */
function Held() {
return (
<section id="top" className="relative overflow-hidden">
{/* A fine dot grid, softly masked out. It gives the surface structure without
appearing as a glow. */}
<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%)",
}}
/>
<Track 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">
<Appear>
<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: BUTTON,
background: MUTED,
color: "#2c2f38",
fontFamily: SANS,
fontWeight: 500,
}}
>
<span
className="grid size-6 place-items-center rounded-full"
style={{ background: "#e6e9f2", color: QUIET }}
>
<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: INK }}
>
Turn every <Underlined>meeting</Underlined> into a clear recap.
</h1>
<p
className="mt-5 max-w-xl text-lg md:mt-6 md:text-xl"
style={{ fontFamily: SANS, color: QUIET }}
>
{MARK.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: BUTTON,
background: BLUE,
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: INK,
textDecoration: "underline",
textDecorationColor: BLUE_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: QUIET }}
>
<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>
</Appear>
<Appear delay={0.15} className="mx-auto w-full max-w-2xl lg:mx-0">
<Recap />
</Appear>
</Track>
</section>
);
}
// A word with a drawn stroke under it that draws itself once.
function Underlined({ children }: { children: ReactNode }) {
const reduce = useReducedMotion();
const ref = useRef<HTMLSpanElement>(null);
const visible = useInView(ref, { once: true, amount: 0.6 });
return (
<span ref={ref} className="relative inline-block whitespace-nowrap" style={{ color: BLUE }}>
{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: BLUE_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={visible || reduce ? { pathLength: 1 } : { pathLength: 0 }}
transition={{ duration: reduce ? 0 : 0.9, ease: "easeOut", delay: 0.4 }}
/>
</svg>
</span>
);
}
/* The card in the hero. It shows what the product delivers: the sentence writes itself word
by word, the tasks tick off one after another, then the next meeting comes. On hover it
stops so it can be read. */
function Recap() {
const reduce = useReducedMotion();
const [i, setI] = useState(0);
const pause = useRef(false);
const ref = useRef<HTMLElement>(null);
const visible = useInView(ref, { amount: 0.3 });
useEffect(() => {
if (reduce || !visible) return;
const clock = window.setInterval(() => {
if (pause.current) return;
setI((v) => (v + 1) % APPOINTMENTS.length);
}, 6200);
return () => window.clearInterval(clock);
}, [reduce, visible]);
const jetzt = APPOINTMENTS[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 ${EDGE}`, 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">
<Level groesse={22} />
<p
className="truncate text-sm"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "-0.01em", color: INK }}
>
{jetzt.title}
</p>
</div>
<span
className="inline-flex shrink-0 items-center gap-1.5 px-2.5 py-1 text-[10px]"
style={{
borderRadius: BUTTON,
background: MUTED,
color: "#3a3d47",
fontFamily: SANS,
fontWeight: 500,
}}
>
<motion.span
aria-hidden
className="size-1.5 rounded-full"
style={{ background: GREEN }}
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((short) => (
<span
key={short}
className="grid size-7 place-items-center rounded-full text-[10px]"
style={{ background: BLUE_GENTLE, color: BLUE, fontFamily: SANS, fontWeight: 600 }}
>
{short}
</span>
))}
</div>
<span className="text-xs" style={{ fontFamily: SANS, color: QUIET }}>
{jetzt.duration}
</span>
</div>
<div className="mt-6">
<p
className="text-[10px] uppercase"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "0.18em", color: QUIET }}
>
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((word, 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 }}
>
{word}
{k < jetzt.zusammenfassung.length - 1 ? " " : ""}
</motion.span>
))}
</motion.span>
</AnimatePresence>
</p>
</div>
<div className="mt-5 pt-5" style={{ borderTop: `1px solid ${EDGE}` }}>
<p
className="text-[10px] uppercase"
style={{ fontFamily: SANS, fontWeight: 600, letterSpacing: "0.18em", color: QUIET }}
>
Action items
</p>
<ul className="mt-3 min-h-[5.75rem] space-y-2.5">
{jetzt.aufgaben.map((task, 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: task.done ? BLUE : "#ffffff",
border: task.done ? "1px solid transparent" : `1px solid ${EDGE}`,
color: "#ffffff",
}}
>
{task.done ? <Check className="size-3.5" strokeWidth={3} aria-hidden /> : null}
</span>
<span
className="flex-1 truncate text-[15px]"
style={{ fontFamily: SANS, color: INK }}
>
{task.what}
</span>
<span
aria-hidden
className="grid size-6 shrink-0 place-items-center rounded-full text-[10px]"
style={{ background: MUTED, color: "#3a3d47", fontFamily: SANS, fontWeight: 600 }}
>
{task.who}
</span>
<span
className="w-14 shrink-0 text-right text-xs"
style={{ fontFamily: SANS, color: QUIET }}
>
{task.when}
</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 ${EDGE}` }}
>
<div
className="flex flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-[11px]"
style={{ fontFamily: SANS, color: QUIET }}
>
<MonitorPlay className="size-3.5 shrink-0" aria-hidden />
<span>Works with</span>
<span style={{ fontWeight: 500, color: "#2c2f38" }}>
Video calls · Webinars · Recordings
</span>
</div>
<span
className="inline-flex shrink-0 items-center gap-1.5 text-[11px]"
style={{ fontFamily: SANS, color: QUIET }}
>
<Sparkles className="size-3" aria-hidden />
42 seconds
</span>
</footer>
</article>
);
}
/* ---------------------------------------------------------------------- trust */
function TrustBar() {
return (
<section aria-label="Trust" style={{ background: AREA }}>
<Track 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: QUIET }}
>
{TRUST.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>
</Track>
</section>
);
}
/* -------------------------------------------------------------------- path */
function ProductPath() {
return (
<section id="journey" className="scroll-mt-24 py-16 md:py-32">
<Track>
<Appear className="mx-auto max-w-2xl text-center">
<Kicker>How {MARK.name} works</Kicker>
<div className="mt-3">
<Title>Capture. Recap. Automate.</Title>
</div>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: QUIET }}>
Three steps that build on each other, from the recording to the automatic follow-up.
</p>
</Appear>
<ol className="mt-10 grid gap-8 md:mt-16 lg:grid-cols-3">
{WEG.map((part, i) => (
<li key={part.name} className="h-full">
<Appear 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: CARD, background: AREA_2 }}
>
<div className="mb-6 flex items-center justify-between">
<span
className="text-sm"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.16em",
color: QUIET,
}}
>
{part.step}
</span>
<span
className="px-3 py-1 text-xs uppercase"
style={{
borderRadius: BUTTON,
background: "#e2e6f0",
color: "#3a3d47",
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.1em",
}}
>
{part.marke}
</span>
</div>
<div className="mb-6 flex h-12 items-center transition-transform duration-500 ease-out group-hover:scale-105">
<Level groesse={56} color={part.color} />
</div>
<h3
className="text-2xl"
style={{ fontFamily: SANS, fontWeight: 500, color: INK }}
>
{part.name}
</h3>
<p
className="mt-2 text-base italic"
style={{ fontFamily: SANS, color: "#33363f" }}
>
{part.frage}
</p>
<p className="mt-3 text-base" style={{ fontFamily: SANS, color: QUIET }}>
{part.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: INK }}
>
See plans
<ArrowRight
className="size-4 transition-transform duration-300 group-hover:translate-x-1"
aria-hidden
/>
</span>
</a>
</Appear>
</li>
))}
</ol>
</Track>
</section>
);
}
/* ------------------------------------------------------------------ voices */
function Voices() {
return (
<section id="testimonials" className="scroll-mt-24 py-16 md:py-32" style={{ background: AREA }}>
<Track>
<Appear className="mx-auto max-w-2xl text-center">
<Title>No meeting slips through the cracks</Title>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: QUIET }}>
Three of many teams that never forget anything important with {MARK.name}.
</p>
</Appear>
<ul className="mt-12 grid gap-8 md:mt-16 md:grid-cols-3">
{VOICES.map((voice, i) => (
<li key={voice.name} className="h-full">
<Appear delay={i * 0.12} className="h-full">
<figure
className="flex h-full w-full flex-col p-8"
style={{ borderRadius: CARD, background: "#ffffff" }}
>
<span
className="flex gap-1"
aria-label="5 out of 5 stars"
style={{ color: VIOLET }}
>
{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: INK }}
>
{voice.text}
</blockquote>
<figcaption className="mt-6 flex items-center gap-3 pt-2">
<Monogram name={voice.name} />
<span className="min-w-0">
<span
className="block text-base"
style={{ fontFamily: SANS, fontWeight: 600, color: INK }}
>
{voice.name}
</span>
<span className="block text-sm" style={{ fontFamily: SANS, color: QUIET }}>
{voice.rolle}
</span>
</span>
</figcaption>
</figure>
</Appear>
</li>
))}
</ul>
</Track>
</section>
);
}
/* ------------------------------------------------------------ how it works */
function SoRunsEs() {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const visible = useInView(ref, { once: true, margin: "-100px" });
return (
<section id="how-it-works" className="scroll-mt-24 py-16 md:py-32">
<Track>
<Appear className="mx-auto max-w-2xl text-center">
<Title>How it works</Title>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: QUIET }}>
Ready in under a minute, try it without signing up.
</p>
</Appear>
<div ref={ref} className="relative mt-12 md:mt-20">
{/* The connector runs behind the three cards and draws itself once. Only on the
desktop: below that the cards stand under each other, and a line across
would make no sense there. */}
<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: BLUE_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={visible || reduce ? { pathLength: 1 } : { pathLength: 0 }}
transition={{ duration: reduce ? 0 : 1.6, ease: "easeInOut" }}
/>
</svg>
<ol className="relative grid gap-8 md:grid-cols-3">
{STEPS.map((step, i) => (
<li key={step.title}>
<Appear delay={i * 0.12}>
<div
className="h-full p-6 md:p-8"
style={{ borderRadius: CARD, background: AREA_2 }}
>
<div className="flex items-center gap-3">
<span
className="grid size-10 shrink-0 place-items-center text-base"
style={{
borderRadius: 12,
background: BLUE_GENTLE,
color: BLUE,
fontFamily: SANS,
fontWeight: 600,
}}
>
{String(i + 1).padStart(2, "0")}
</span>
<h3
className="text-xl"
style={{ fontFamily: SANS, fontWeight: 500, color: INK }}
>
{step.title}
</h3>
</div>
<p className="mt-3 text-base" style={{ fontFamily: SANS, color: QUIET }}>
{step.text}
</p>
</div>
</Appear>
</li>
))}
</ol>
</div>
<p
className="mt-16 text-center text-sm md:mt-20"
style={{ fontFamily: SANS, fontWeight: 500, color: QUIET }}
>
Takes under a minute.
</p>
</Track>
</section>
);
}
/* ------------------------------------------------------------------ archive */
function Archiv() {
return (
<section id="archive" className="scroll-mt-24 py-16 md:py-28">
<Track>
<Appear>
<div
className="relative overflow-hidden"
style={{
borderRadius: 32,
border: `1px solid ${EDGE}`,
backgroundImage: `linear-gradient(135deg, ${AREA} 0%, #ffffff 46%, ${BLUE_GENTLE} 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: INK,
}}
>
Every meeting. In one place.
</h2>
<p
className="mt-5 text-lg leading-relaxed"
style={{ fontFamily: SANS, color: QUIET }}
>
{MARK.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_DOTS.map((dot) => (
<li key={dot} className="flex items-start gap-3">
<span
className="mt-0.5 grid size-6 shrink-0 place-items-center rounded-full"
style={{ background: BLUE, color: "#ffffff" }}
>
<Check className="size-3.5" strokeWidth={3} aria-hidden />
</span>
<span className="text-base" style={{ fontFamily: SANS, color: INK }}>
{dot}
</span>
</li>
))}
</ul>
<a
href="#upload"
className="mt-8 inline-flex items-center gap-1.5 text-base"
style={{ fontFamily: SANS, fontWeight: 500, color: INK }}
>
Try it for free
<ArrowRight className="size-4" aria-hidden />
</a>
</div>
<div className="order-1 lg:order-2">
<ArchivImage />
</div>
</div>
</div>
</Appear>
</Track>
</section>
);
}
/* A stack of recaps as a drawing. Depth comes from edge and offset, not from a shadow. The
level meter breathes, the ticks draw themselves as soon as the picture is in view. */
function ArchivImage() {
const reduce = useReducedMotion();
const ref = useRef<SVGSVGElement>(null);
const visible = useInView(ref, { once: true, amount: 0.3 });
const balken = [
{ x: 202, h: 10, f: BLUE },
{ x: 208, h: 18, f: BLUE_2 },
{ x: 214, h: 24, f: VIOLET },
{ x: 220, h: 14, f: BLUE },
{ x: 226, h: 8, f: BLUE_2 },
];
const lines = [
{ y: 168, w: 200 },
{ y: 190, w: 232 },
{ y: 212, w: 168 },
];
const check = [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={BLUE_GENTLE}
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={GREEN} />
<rect x="407" y="128" width="24" height="8" rx="4" fill="#bcdcc9" />
{lines.map((z) => (
<rect key={z.y} x="202" y={z.y} width={z.w} height="9" rx="4.5" fill="#e8ebf3" />
))}
{check.map((y, i) => (
<g key={y}>
<rect x="202" y={y} width="236" height="30" rx="10" fill={AREA} />
<motion.path
d={`M 214 ${y + 15} l 5 5 l 9 -10`}
fill="none"
stroke={BLUE}
strokeWidth="2.6"
strokeLinecap="round"
strokeLinejoin="round"
initial={reduce ? false : { pathLength: 0 }}
animate={visible || 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>
))}
{/* The finder on the left: a magnifier on a field. */}
<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={QUIET} strokeWidth="2.2" />
<path d="M 88 224 l 7 7" stroke={QUIET} strokeWidth="2.2" strokeLinecap="round" />
<rect x="102" y="214" width="86" height="8" rx="4" fill="#e8ebf3" />
{/* The clip with the recorded sound. */}
<rect
x="46"
y="292"
width="130"
height="60"
rx="16"
fill="#ffffff"
stroke="#e4e7ef"
strokeWidth="1.5"
/>
{[
{ x: 62, h: 8, f: BLUE_2 },
{ x: 70, h: 16, f: BLUE },
{ x: 78, h: 26, f: VIOLET },
{ x: 86, h: 12, f: BLUE },
{ x: 94, h: 6, f: BLUE_2 },
{ x: 102, h: 20, f: BLUE },
{ x: 110, h: 10, f: VIOLET },
].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>
);
}
/* ------------------------------------------------------------------ upload */
function Hochladen() {
const reduce = useReducedMotion();
const [state, setState] = useState<"idle" | "running" | "done">("idle");
const [step, setStep] = useState(0);
const [ueber, setUeber] = useState(false);
const starten = useCallback(() => {
if (state === "running") return;
setState("running");
setStep(0);
}, [state]);
useEffect(() => {
if (state !== "running") return;
if (reduce) {
setStep(UPLOAD_STEPS.length);
setState("done");
return;
}
if (step >= UPLOAD_STEPS.length) {
const t = window.setTimeout(() => setState("done"), 500);
return () => window.clearTimeout(t);
}
const t = window.setTimeout(() => setStep((s) => s + 1), 850);
return () => window.clearTimeout(t);
}, [state, step, reduce]);
const portion = Math.round((Math.min(step, UPLOAD_STEPS.length) / UPLOAD_STEPS.length) * 100);
return (
<section id="upload" className="scroll-mt-24 py-16 md:py-32" style={{ background: AREA }}>
<Track>
<Appear className="mx-auto max-w-2xl text-center">
<Kicker>Try it now</Kicker>
<div className="mt-3">
<Title>Upload a recording</Title>
</div>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: QUIET }}>
No account needed. You will see exactly what your recap will look like.
</p>
</Appear>
<Appear 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" }}>
{state === "idle" ? (
<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: CARD,
border: `2px dashed ${ueber ? BLUE : "#ccd3e6"}`,
background: ueber ? BLUE_GENTLE : AREA,
}}
>
<span
className="grid size-14 place-items-center rounded-full"
style={{ background: BLUE_GENTLE, color: BLUE }}
>
<FileAudio className="size-6" strokeWidth={1.7} aria-hidden />
</span>
<span
className="mt-5 text-xl"
style={{ fontFamily: SANS, fontWeight: 500, color: INK }}
>
{ueber ? `Release, ${MARK.name} takes over` : "Drop your file here"}
</span>
<span className="mt-2 max-w-sm text-sm" style={{ fontFamily: SANS, color: QUIET }}>
or click to choose. MP3, WAV, M4A or video, even a call 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: BLUE_GENTLE, color: BLUE }}
>
<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: INK }}
>
weekly-sync.mp3
</span>
<span className="block text-sm" style={{ fontFamily: SANS, color: QUIET }}>
{state === "done"
? "summarized in 42 seconds"
: `Step ${Math.min(step + 1, UPLOAD_STEPS.length)} of ${UPLOAD_STEPS.length}`}
</span>
</span>
</div>
<span
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs"
style={{
borderRadius: BUTTON,
background: state === "done" ? "#e9f4ee" : MUTED,
color: state === "done" ? GREEN : "#3a3d47",
fontFamily: SANS,
fontWeight: 600,
}}
>
{state === "done" ? "Done" : `${portion}%`}
</span>
</div>
{/* The bar runs with the steps, not with a clock: it shows where the work
stands, not how long it takes. */}
<div
className="mt-6 h-1.5 w-full overflow-hidden"
style={{ borderRadius: 999, background: MUTED }}
>
<motion.div
className="h-full"
style={{ background: BLUE, borderRadius: 999 }}
initial={{ width: "0%" }}
animate={{ width: `${portion}%` }}
transition={{ duration: reduce ? 0 : 0.6, ease: "easeOut" }}
/>
</div>
<ul className="mt-7 space-y-3">
{UPLOAD_STEPS.map((text, i) => {
const done = i < step;
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: done ? BLUE : MUTED,
color: done ? "#ffffff" : "#aeb3c2",
}}
>
<Check className="size-3.5" strokeWidth={3} aria-hidden />
</span>
<span
className="text-base transition-colors"
style={{ fontFamily: SANS, color: done ? INK : QUIET }}
>
{text}
</span>
</li>
);
})}
</ul>
{state === "done" ? (
<div
className="mt-8 p-6"
style={{
borderRadius: CARD,
background: AREA,
border: `1px solid ${EDGE}`,
}}
>
<p
className="text-[10px] uppercase"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.18em",
color: QUIET,
}}
>
Summary
</p>
<p
className="mt-2 text-[17px] leading-relaxed"
style={{ fontFamily: SANS, color: INK }}
>
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={() => {
setState("idle");
setStep(0);
}}
className="mt-6 inline-flex items-center gap-2 px-5 py-2.5 text-sm"
style={{
borderRadius: BUTTON,
background: INK,
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: QUIET }}
>
{[
"Encrypted in transit and at rest",
"GDPR-compliant, servers in the EU",
"Audio deleted after transcription",
].map((dot) => (
<li key={dot} className="inline-flex items-center gap-1.5">
<ShieldCheck
className="size-3.5"
style={{ color: BLUE }}
strokeWidth={1.8}
aria-hidden
/>
{dot}
</li>
))}
</ul>
</div>
</Appear>
</Track>
</section>
);
}
/* ------------------------------------------------------------------ pricing */
function Prices() {
return (
<section id="pricing" className="scroll-mt-24 py-16 md:py-32">
<Track>
<Appear className="mx-auto max-w-2xl text-center">
<Kicker>Pricing</Kicker>
<div className="mt-3">
<Title>Fair pricing, no surprises</Title>
</div>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: QUIET }}>
You only pay for what you use, and can cancel anytime.
</p>
</Appear>
<ul className="mt-14 grid gap-8 lg:mt-16 lg:grid-cols-3">
{PRICES.map((plan, i) => (
<li key={plan.name} className="h-full">
<Appear 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: BUTTON,
background: VIOLET,
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: CARD,
background: plan.hervor ? BLUE : AREA,
color: plan.hervor ? "#ffffff" : INK,
border: `1px solid ${plan.hervor ? BLUE : EDGE}`,
}}
>
<span
className="text-xs uppercase"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.16em",
color: plan.hervor ? "rgba(255,255,255,0.9)" : BLUE,
}}
>
{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)" : QUIET,
}}
>
{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)" : QUIET,
}}
>
{plan.je}
</span>
</div>
<div
aria-hidden
className="my-8 h-px w-full"
style={{ background: plan.hervor ? "rgba(255,255,255,0.22)" : EDGE }}
/>
<ul className="space-y-3.5">
{plan.points.map((dot) => (
<li key={dot} 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)" : BLUE_GENTLE,
color: plan.hervor ? "#ffffff" : BLUE,
}}
>
<Check className="size-3" strokeWidth={3} aria-hidden />
</span>
<span
style={{
fontFamily: SANS,
color: plan.hervor ? "rgba(255,255,255,0.95)" : INK,
}}
>
{dot}
</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: BUTTON,
background: plan.hervor ? "#ffffff" : INK,
color: plan.hervor ? BLUE : "#ffffff",
fontFamily: SANS,
fontWeight: 600,
}}
>
{plan.cta}
</a>
</div>
</div>
</div>
</Appear>
</li>
))}
</ul>
</Track>
</section>
);
}
/* ---------------------------------------------------------------- security */
function Safety() {
return (
<section id="security" className="scroll-mt-24 py-16 md:py-32" style={{ background: AREA }}>
<Track>
<Appear className="mx-auto max-w-2xl text-center">
<Kicker>Security and privacy</Kicker>
<div className="mt-3">
<Title>Your meetings stay yours</Title>
</div>
<p className="mt-5 text-lg" style={{ fontFamily: SANS, color: QUIET }}>
Built for sensitive conversations, with clear rules on where your data goes and how long
it stays.
</p>
</Appear>
<div className="mt-12 grid items-center gap-10 md:mt-16 lg:grid-cols-2 lg:gap-16">
<Appear className="order-1">
<PlateImage />
</Appear>
<ul className="order-2 space-y-7">
{SAFETY.map((dot, i) => (
<li key={dot.title}>
<Appear delay={i * 0.1} className="flex items-start gap-4">
<span
className="grid size-11 shrink-0 place-items-center"
style={{ borderRadius: 12, background: BLUE_GENTLE, color: BLUE }}
>
<dot.Icon className="size-5" strokeWidth={1.75} aria-hidden />
</span>
<div>
<h3
className="text-lg"
style={{
fontFamily: SANS,
fontWeight: 500,
letterSpacing: "-0.01em",
color: INK,
}}
>
{dot.title}
</h3>
<p
className="mt-1.5 text-sm leading-relaxed"
style={{ fontFamily: SANS, color: QUIET }}
>
{dot.text}
</p>
</div>
</Appear>
</li>
))}
</ul>
</div>
</Track>
</section>
);
}
// A shield that draws itself once, with a lock inside it.
function PlateImage() {
const reduce = useReducedMotion();
const ref = useRef<SVGSVGElement>(null);
const visible = 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={BLUE_GENTLE} />
<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={BLUE}
strokeWidth="2.4"
strokeLinejoin="round"
initial={reduce ? false : { pathLength: 0 }}
animate={visible || 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={BLUE_GENTLE}
opacity="0.55"
/>
<rect x="182" y="150" width="56" height="46" rx="10" fill={BLUE} />
<path
d="M 192 150 v -12 a 18 18 0 0 1 36 0 v 12"
fill="none"
stroke={BLUE}
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={BLUE_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>
);
}
/* ------------------------------------------------------------------ footer */
function Footer() {
return (
<footer id="about" className="scroll-mt-24" style={{ background: AREA }}>
<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">
<Level />
<span
className="text-xl md:text-2xl"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "-0.02em",
color: INK,
}}
>
{MARK.name}
</span>
</div>
<p
className="mt-5 max-w-md text-base leading-relaxed"
style={{ fontFamily: SANS, color: QUIET }}
>
{MARK.name} turns every meeting into a clear recap, with decisions, action items and
follow-up.
</p>
<a
href={`mailto:${MARK.mail}`}
className="mt-6 inline-flex items-center gap-2 text-base"
style={{ fontFamily: SANS, fontWeight: 500, color: INK }}
>
<Mail className="size-4" aria-hidden />
{MARK.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: BUTTON,
background: BLUE_GENTLE,
color: "#3a3d47",
fontFamily: SANS,
fontWeight: 500,
}}
>
<Icon
className="size-3.5"
style={{ color: BLUE }}
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"
>
{FOOT_COLUMNS.map((column) => (
<div key={column.title}>
<h3
className="text-xs uppercase"
style={{
fontFamily: SANS,
fontWeight: 600,
letterSpacing: "0.16em",
color: "#6d7280",
}}
>
{column.title}
</h3>
<ul className="mt-3 space-y-1">
{column.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 ${EDGE}`, fontFamily: SANS, color: QUIET }}
>
<p>© 2026 {MARK.name}. All rights reserved.</p>
<p>{MARK.name} produces automatic summaries, please review for accuracy.</p>
</div>
</div>
</div>
</footer>
);
}
/* --------------------------------------------------------------------- page */
export function ConferMeetingsPage() {
return (
<div style={{ background: "#ffffff", color: INK, fontFamily: SANS }}>
<Header />
<main>
<Held />
<TrustBar />
<ProductPath />
<Voices />
<SoRunsEs />
<Archiv />
<Hochladen />
<Prices />
<Safety />
</main>
<Footer />
</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