feat: wire landing page feature showcase

Replace the older feature bands with the new product-window tour, add the supporting showcase sections, and hook the scroll animations into the page.
This commit is contained in:
Matthew Meszaros
2026-06-06 11:55:40 +02:00
parent eaa201b5a1
commit fe0feeb1eb
+189 -399
View File
@@ -2,11 +2,18 @@
import Layout from '../layouts/Layout.astro';
import Icon from '../components/Icon.astro';
import DashboardMock from '../components/DashboardMock.astro';
import InboxMock from '../components/InboxMock.astro';
import WarmupMock from '../components/WarmupMock.astro';
import HeroAtmosphere from '../components/HeroAtmosphere.astro';
import SequenceMock from '../components/SequenceMock.astro';
import SectionSky from '../components/SectionSky.astro';
import FeatureSection from '../components/FeatureSection.astro';
import CampaignBuilderMock from '../components/CampaignBuilderMock.astro';
import ScheduleMock from '../components/ScheduleMock.astro';
import UniboxMock from '../components/UniboxMock.astro';
import AnalyticsMock from '../components/AnalyticsMock.astro';
import AIAssistantMock from '../components/AIAssistantMock.astro';
import IntegrationsMock from '../components/IntegrationsMock.astro';
import DeveloperMock from '../components/DeveloperMock.astro';
import CRMMock from '../components/CRMMock.astro';
import TemplatingShowcase from '../components/TemplatingShowcase.astro';
import ChangelogShowcase from '../components/ChangelogShowcase.astro';
const compare = [
['Per-mailbox cold cap', '50 / day default', '200 / day if you ask for it'],
@@ -214,406 +221,158 @@ const softwareJsonLd = {
</section>
<!-- ============================================================
WARMUP — dark sky panel · real-looking product detail screen
FEATURE SHOWCASE — one section per feature, each on its own
background, with a pixel-faithful product window + an auto-playing
demo cursor. (Replaces the old warmup + abstract inbox/campaign bands.)
============================================================ -->
<section class="relative isolate overflow-hidden text-white" style="background: radial-gradient(ellipse 130% 140% at 72% 28%, #0284c7 0%, #0369a1 25%, #075985 50%, #0c4a6e 80%, #0a3d5c 100%);">
<img src="/backdrops/cloud-3.webp" alt="" aria-hidden="true" class="absolute -top-10 -left-10 w-[460px] opacity-30 pointer-events-none select-none cloud-drift cloud-1" loading="lazy" decoding="async" style="mix-blend-mode: screen;"/>
<img src="/backdrops/cloud-5.webp" alt="" aria-hidden="true" class="absolute top-10 right-0 w-[520px] opacity-22 pointer-events-none select-none cloud-drift cloud-2" loading="lazy" decoding="async" style="mix-blend-mode: screen;"/>
<img src="/backdrops/cloud-4.webp" alt="" aria-hidden="true" class="absolute -bottom-12 left-1/4 w-[420px] opacity-18 pointer-events-none select-none cloud-drift cloud-3" loading="lazy" decoding="async" style="mix-blend-mode: screen;"/>
<div class="container-page py-20 md:py-28 relative">
<div class="grid lg:grid-cols-12 gap-10 items-end mb-12">
<div class="lg:col-span-7">
<div class="text-[11px] uppercase tracking-[0.18em] text-white/55 font-mono mb-3">Warmup</div>
<h2 class="text-[34px] md:text-[52px] font-semibold tracking-[-0.025em] leading-[1.04]">
Mailbox warmup, built&nbsp;in.
</h2>
</div>
<div class="lg:col-span-5">
<p class="text-[15.5px] text-white/75 leading-relaxed">
Pool placement, daily ramps, conversational replies, and per-mailbox health monitoring run automatically on every paid mailbox. The same code that warms also quarantines.
</p>
<a href="/warmup/" class="mt-5 inline-flex items-center gap-1.5 text-[13.5px] font-medium text-white border-b border-white/30 hover:border-white pb-px">
Read the full doc
<Icon name="arrowUpRight" size={13} />
</a>
</div>
</div>
<FeatureSection
eyebrow="Campaigns"
title="Sequences that branch on real behavior."
description="Build a multi-step flow on the canvas, then drag a connector from any step to fork on opens, clicks, and replies. It's the exact builder you'll use in the app."
side="right"
bg="sky"
url="app.warmbly.com/campaigns/q1-sales/sequences"
cursor="a"
callouts={[
{ label: 'Drag to branch', text: 'Pull a link from a step to add an “if opened / clicked / replied” path. No match just stops.' },
{ label: 'A/B at every step', text: 'Weighted subject + body variants; the stronger one keeps sending.' },
{ label: 'Threaded follow-ups', text: 'Follow-ups reply on the previous subject, so they land in the same conversation.' },
]}
cta={{ text: 'Explore campaigns', href: '/campaigns/' }}
>
<CampaignBuilderMock />
</FeatureSection>
<!-- Clean warmup visual: row of mailbox rings up top, live activity
stream below. No big chart, no panels, no white borders.
Everything sits directly on the sky background. -->
<div class="relative">
<FeatureSection
eyebrow="Scheduling"
title="Send only inside their working hours."
description="A calendar-style editor with full per-day control. Draw a sending window on any weekday, drag to move it, grab an edge to resize. Everything snaps to 30 minutes."
side="left"
bg="light"
url="app.warmbly.com/campaigns/q1-sales/schedule"
cursor="b"
callouts={[
{ label: 'Per-day windows', text: 'Independent windows for each day: mornings only, or split shifts. Several per day is fine.' },
{ label: 'Timezone-aware', text: 'Sends follow the chosen timezone; worker IPs spread the volume naturally.' },
{ label: 'One-click presets', text: 'MonFri 95 in a tap, then fine-tune by dragging the blocks.' },
]}
cta={{ text: 'See sending controls', href: '/sending/' }}
>
<ScheduleMock />
</FeatureSection>
<!-- Mailbox warmup rings · each mailbox shown as a circular ramp
with email + day count. Hovering brightens the ring. -->
<div class="flex items-baseline justify-between flex-wrap gap-3 mb-6">
<span class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-white/55">Mailboxes in warmup · acme.co</span>
<span class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-emerald-300/80 inline-flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-soft"></span>
pool active
</span>
</div>
<FeatureSection
eyebrow="Unified inbox"
title="Every reply, triaged the moment it lands."
description="Replies from every connected mailbox stream into one searchable queue. The classifier tags each one on arrival so you act on the labels that matter."
side="right"
bg="navy"
url="app.warmbly.com/unibox"
cursor="c"
callouts={[
{ label: 'One queue, every mailbox', text: 'Scope by inbox, tag, or status: unread, awaiting reply, snoozed, scheduled.' },
{ label: 'Classified on arrival', text: 'Positive, out-of-office, unsubscribe and bounce are labeled before you open them.' },
{ label: 'Reply in place', text: 'Snooze, schedule, and send from the same pane, keyboard-first.' },
]}
cta={{ text: 'Tour the inbox', href: '/inbox/' }}
>
<UniboxMock />
</FeatureSection>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-y-8 gap-x-6">
{[
{ e: 'founder', day: 28 },
{ e: 'hello', day: 22 },
{ e: 'team', day: 16 },
{ e: 'intro', day: 11 },
{ e: 'partnerships', day: 7 },
{ e: 'support', day: 3 },
].map((m) => {
const r = 26;
const C = 2 * Math.PI * r;
const pct = Math.min(1, m.day / 30);
const dash = (C * pct).toFixed(2);
const gap = (C - C * pct).toFixed(2);
return (
<div class="flex flex-col items-center text-center group">
<svg viewBox="0 0 64 64" class="w-[68px] h-[68px] -rotate-90 transition-transform duration-300 group-hover:scale-[1.05]" role="img" aria-label={`${m.e} warmup day ${m.day} of 30`}>
<circle cx="32" cy="32" r={r} fill="none" stroke="rgba(186,230,253,0.12)" stroke-width="3" />
<circle
cx="32" cy="32" r={r}
fill="none"
stroke="url(#ringStroke)"
stroke-width="3"
stroke-linecap="round"
stroke-dasharray={`${dash} ${gap}`}
/>
</svg>
<div class="-mt-[46px] mb-[20px] text-[15px] font-semibold text-white tabular-nums">{m.day}</div>
<div class="font-mono text-[12px] text-white/85">{m.e}<span class="text-white/40">@acme.co</span></div>
<div class="mt-1 font-mono text-[10.5px] text-white/40 uppercase tracking-[0.16em]">day {m.day} / 30</div>
</div>
);
})}
</div>
<FeatureSection
eyebrow="Analytics"
title="Proof of what actually landed."
description="Deliverability across the whole workspace: sent, opens, clicks, replies and bounces, with per-campaign breakdowns and account health on one screen."
side="left"
bg="wallpaper"
url="app.warmbly.com/analytics"
cursor="d"
callouts={[
{ label: 'Placement, not vanity', text: 'Track the metrics that move reputation, over 7, 30 or 90 days.' },
{ label: 'Best performers, surfaced', text: 'Top campaigns and account health rank themselves as sends roll in.' },
{ label: 'Shareable report card', text: 'Export a branded image of any range in a single tap.' },
]}
cta={{ text: 'View analytics', href: '/analytics/' }}
>
<AnalyticsMock />
</FeatureSection>
<!-- Shared gradient for all rings -->
<svg width="0" height="0" class="absolute" aria-hidden="true">
<defs>
<linearGradient id="ringStroke" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#bae6fd" />
<stop offset="100%" stop-color="#0ea5e9" />
</linearGradient>
</defs>
</svg>
<FeatureSection
eyebrow="AI assistant"
title="Draft the whole email from one line."
description="Describe what the email should do and the assistant writes it in your voice, merge fields and tone already in place. Edit inline, regenerate, or keep what works."
side="right"
bg="sky"
url="app.warmbly.com/campaigns/q1-sales/steps"
cursor="a"
callouts={[
{ label: 'One-line briefs', text: 'Tell it the goal; get back a ready-to-send draft with variables filled in.' },
{ label: 'Tone presets', text: 'Friendly, professional, concise, persuasive. Switch and regenerate in a click.' },
{ label: 'Credits, no surprises', text: 'Each draft uses one writing credit, shown before you generate.' },
]}
cta={{ text: 'Try the assistant', href: '/campaigns/' }}
>
<AIAssistantMock />
</FeatureSection>
<!-- Subtle horizontal divider made of the sky color, not white -->
<div class="mt-14 h-px" style="background: linear-gradient(90deg, transparent 0%, rgba(186,230,253,0.22) 50%, transparent 100%);"></div>
<FeatureSection
eyebrow="Integrations"
title="Plug into the stack you already run."
description="Connect Gmail, Microsoft 365, your CRM, Slack and Zapier over OAuth. Tokens are encrypted at rest and every webhook is HMAC-signed."
side="left"
bg="light"
url="app.warmbly.com/integrations"
cursor="b"
callouts={[
{ label: 'OAuth in two clicks', text: 'Authorize a provider and Warmbly handles the rest. Revoke anytime.' },
{ label: 'Signed webhooks', text: 'Every event is HMAC-signed and HTTPS by default, with SSRF protection.' },
{ label: 'Encrypted at rest', text: 'Secrets are sealed with your data key, never stored in plaintext.' },
]}
cta={{ text: 'Browse integrations', href: '/integrations/' }}
>
<IntegrationsMock />
</FeatureSection>
<!-- KPI row directly below divider, clean and short -->
<div class="mt-10 grid grid-cols-2 md:grid-cols-4 gap-y-6 gap-x-10">
{[
{ k: 'Peers', v: '142', sub: 'active last 24h' },
{ k: 'Replies', v: '11', sub: 'reply rate 34%' },
{ k: 'Inbox', v: '96.4%', sub: 'rolling 14 day' },
{ k: 'Spam', v: '1.8%', sub: 'well under 10%' },
].map((s) => (
<div>
<div class="text-[10.5px] uppercase tracking-[0.22em] text-white/45 font-mono">{s.k}</div>
<div class="mt-2 text-[32px] md:text-[36px] font-semibold tracking-[-0.025em] text-white leading-none tabular-nums">{s.v}</div>
<div class="mt-1.5 text-[11.5px] text-white/45 font-mono">{s.sub}</div>
</div>
))}
</div>
<FeatureSection
eyebrow="Developers"
title="An API you can build a product on."
description="Everything in the dashboard is a REST endpoint: scoped API keys, idempotent writes, opaque cursors, and HMAC-signed webhooks."
side="right"
bg="navy"
url="app.warmbly.com/api-keys"
cursor="c"
callouts={[
{ label: 'Scoped keys', text: 'Mint read, write, or send-only keys per environment and rotate without downtime.' },
{ label: 'Idempotent by default', text: 'Send an Idempotency-Key and retries are always safe.' },
{ label: 'Webhooks that verify', text: 'Subscribe to events like contact.replied with signed payloads.' },
]}
cta={{ text: 'Read the API docs', href: '/developers/' }}
>
<DeveloperMock />
</FeatureSection>
<!-- Pool composition strip, quiet, no box -->
<div class="mt-10 flex flex-wrap items-center gap-x-5 gap-y-2 text-[11.5px]">
<span class="text-[10.5px] uppercase tracking-[0.22em] text-white/45 font-mono mr-2">Pool</span>
<span class="inline-flex items-center gap-1.5 text-white/70"><span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span>Gmail <span class="font-mono text-white/45">46%</span></span>
<span class="inline-flex items-center gap-1.5 text-white/70"><span class="w-1.5 h-1.5 rounded-full bg-sky-400"></span>Outlook <span class="font-mono text-white/45">24%</span></span>
<span class="inline-flex items-center gap-1.5 text-white/70"><span class="w-1.5 h-1.5 rounded-full bg-violet-400"></span>Yahoo <span class="font-mono text-white/45">12%</span></span>
<span class="inline-flex items-center gap-1.5 text-white/70"><span class="w-1.5 h-1.5 rounded-full bg-zinc-300"></span>iCloud <span class="font-mono text-white/45">9%</span></span>
<span class="inline-flex items-center gap-1.5 text-white/70"><span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>Self-IMAP <span class="font-mono text-white/45">9%</span></span>
</div>
</div>
</div>
</section>
<FeatureSection
eyebrow="CRM"
title="Replies become a pipeline, not a pile."
description="A positive reply can open a deal. Drag it across stages, assign an owner, and keep tasks and notes right next to the conversation that started it."
side="left"
bg="wallpaper"
url="app.warmbly.com/crm/deals"
cursor="d"
callouts={[
{ label: 'Visual pipelines', text: 'Drag deals across stages; stage totals update as you go.' },
{ label: 'Owners and tasks', text: 'Assign teammates, set follow-up tasks, and never drop a thread.' },
{ label: 'Tied to the inbox', text: 'Each deal links back to the reply that created it.' },
]}
cta={{ text: 'See the CRM', href: '/crm/' }}
>
<CRMMock />
</FeatureSection>
<!-- ============================================================
INBOX — architecture flow diagram (many mailboxes → 1 triage)
============================================================ -->
<section class="relative bg-white border-t border-[color:var(--border)] overflow-hidden">
<div class="container-page py-20 md:py-28">
<div class="grid lg:grid-cols-12 gap-10 items-end">
<div class="lg:col-span-7">
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Inbox</div>
<h2 class="text-[34px] md:text-[48px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
Every reply, one queue, five labels.
</h2>
<p class="mt-5 text-[15.5px] text-foreground/75 leading-relaxed max-w-lg">
Replies stream in from every connected mailbox. The classifier tags each one before it lands so triage stays on the labels you care about.
</p>
</div>
<!-- Stats fills the empty top-right space -->
<div class="lg:col-span-5">
<div class="grid grid-cols-3 gap-px bg-[color:var(--border)] ring-1 ring-[color:var(--border)] rounded-[10px] overflow-hidden">
{[
{ v: '5', l: 'labels' },
{ v: '< 1s', l: 'tag latency' },
{ v: '∞', l: 'mailboxes per queue' },
].map((s) => (
<div class="bg-white px-4 py-4">
<div class="text-[26px] font-semibold tracking-[-0.025em] text-heading leading-none">{s.v}</div>
<div class="mt-1.5 text-[11px] uppercase tracking-[0.14em] font-mono text-muted-foreground">{s.l}</div>
</div>
))}
</div>
<div class="mt-3 flex items-center justify-between text-[12px] text-muted-foreground">
<span>Quoted-text aware. OOO + unsubscribe auto-handled.</span>
<a href="/inbox/" class="font-medium text-foreground border-b border-foreground/30 hover:border-foreground pb-px inline-flex items-center gap-1">See more <Icon name="arrowUpRight" size={11} /></a>
</div>
</div>
</div>
<!-- Flow diagram — pure SVG so dots track lines perfectly -->
<div class="mt-12 relative rounded-[14px] ring-1 ring-[color:var(--border)] bg-[color:var(--surface-2)]/30 px-4 py-6 md:px-10 md:py-10 overflow-hidden">
<!-- Subtle background grid -->
<div class="absolute inset-0 pointer-events-none opacity-[0.6]" style="background-image: linear-gradient(rgba(15,23,42,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(15,23,42,0.05) 1px, transparent 1px); background-size: 32px 32px;"></div>
<svg viewBox="0 0 1400 540" class="relative w-full h-auto" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="flow-in" x1="360" y1="0" x2="620" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#7dd3fc"/>
<stop offset="100%" stop-color="#0284c7"/>
</linearGradient>
<linearGradient id="flow-out" x1="840" y1="0" x2="1080" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#0284c7"/>
<stop offset="100%" stop-color="#7dd3fc"/>
</linearGradient>
<filter id="card-shadow" x="-10%" y="-30%" width="120%" height="160%">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.07"/>
</filter>
<filter id="classifier-shadow" x="-25%" y="-25%" width="150%" height="150%">
<feDropShadow dx="0" dy="20" stdDeviation="28" flood-color="#0f172a" flood-opacity="0.30"/>
</filter>
<filter id="dot-glow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Connector paths (IDs for animateMotion mpath) -->
<path id="in-1" d="M 360 60 C 500 60, 560 270, 620 270" stroke="url(#flow-in)" stroke-width="2" fill="none"/>
<path id="in-2" d="M 360 165 C 500 165, 560 270, 620 270" stroke="url(#flow-in)" stroke-width="2" fill="none"/>
<path id="in-4" d="M 360 375 C 500 375, 560 270, 620 270" stroke="url(#flow-in)" stroke-width="2" fill="none"/>
<path id="in-5" d="M 360 480 C 500 480, 560 270, 620 270" stroke="url(#flow-in)" stroke-width="2" fill="none"/>
<path id="in-3" d="M 360 270 L 620 270" stroke="url(#flow-in)" stroke-width="2" fill="none"/>
<path id="out-1" d="M 840 270 C 900 270, 940 60, 1080 60" stroke="url(#flow-out)" stroke-width="2" fill="none"/>
<path id="out-2" d="M 840 270 C 900 270, 940 165, 1080 165" stroke="url(#flow-out)" stroke-width="2" fill="none"/>
<path id="out-4" d="M 840 270 C 900 270, 940 375, 1080 375" stroke="url(#flow-out)" stroke-width="2" fill="none"/>
<path id="out-5" d="M 840 270 C 900 270, 940 480, 1080 480" stroke="url(#flow-out)" stroke-width="2" fill="none"/>
<path id="out-3" d="M 840 270 L 1080 270" stroke="url(#flow-out)" stroke-width="2" fill="none"/>
<!-- Mailbox pills (left) -->
{(() => {
const mb = [
{ e: 'founder@acme.co', y: 60 },
{ e: 'team@northwind.dev', y: 165 },
{ e: 'hello@spruce.bio', y: 270 },
{ e: 'intro@helio.gen', y: 375 },
{ e: 'maya@cinder.app', y: 480 },
];
return mb.map((m) => (
<g filter="url(#card-shadow)">
<rect x="20" y={m.y - 22} rx="22" ry="22" width="340" height="44" fill="#ffffff" stroke="#e2e8f0" stroke-width="1"/>
<rect x="32" y={m.y - 12} rx="6" ry="6" width="24" height="24" fill="#e0f2fe"/>
<text x="44" y={m.y + 4} font-family="ui-sans-serif, system-ui" font-size="11" font-weight="700" fill="#0369a1" text-anchor="middle">{m.e.slice(0, 2).toUpperCase()}</text>
<text x="68" y={m.y + 5} font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="14" fill="#0f172a">{m.e}</text>
</g>
));
})()}
<!-- Classifier card (center, larger + more prominent) -->
<g filter="url(#classifier-shadow)">
<rect x="620" y="150" rx="16" ry="16" width="220" height="240" fill="#0f172a"/>
<!-- Top status row -->
<circle cx="644" cy="184" r="5" fill="#34d399">
<animate attributeName="opacity" values="1;0.3;1" dur="2s" repeatCount="indefinite"/>
</circle>
<text x="660" y="189" font-family="ui-monospace" font-size="11" font-weight="600" letter-spacing="2" fill="#cbd5e1">CLASSIFIER</text>
<!-- Headline -->
<text x="644" y="232" font-family="ui-sans-serif, system-ui" font-size="18" font-weight="600" fill="#ffffff" letter-spacing="-0.4">Tags every reply</text>
<text x="644" y="254" font-family="ui-sans-serif, system-ui" font-size="18" font-weight="600" fill="#ffffff" letter-spacing="-0.4">on arrival.</text>
<!-- Divider line -->
<line x1="644" y1="280" x2="816" y2="280" stroke="#1e293b" stroke-width="1"/>
<!-- Capability list -->
<text x="644" y="304" font-family="ui-monospace" font-size="11.5" fill="#94a3b8">quoted-text aware</text>
<text x="644" y="324" font-family="ui-monospace" font-size="11.5" fill="#94a3b8">OOO detection</text>
<text x="644" y="344" font-family="ui-monospace" font-size="11.5" fill="#94a3b8">auto-suppress</text>
<text x="644" y="364" font-family="ui-monospace" font-size="11.5" fill="#94a3b8">round-robin assign</text>
</g>
<!-- Label pills (right) -->
{(() => {
const lb = [
{ t: 'Positive', dot: '#10b981', y: 60 },
{ t: 'Negative', dot: '#f43f5e', y: 165 },
{ t: 'Out of office',dot: '#f59e0b', y: 270 },
{ t: 'Unsubscribe', dot: '#64748b', y: 375 },
{ t: 'Bounce', dot: '#fb923c', y: 480 },
];
return lb.map((l) => (
<g filter="url(#card-shadow)">
<rect x="1080" y={l.y - 22} rx="22" ry="22" width="300" height="44" fill="#ffffff" stroke="#e2e8f0" stroke-width="1"/>
<circle cx="1100" cy={l.y} r="4.5" fill={l.dot}/>
<text x="1116" y={l.y + 5} font-family="ui-sans-serif, system-ui" font-size="14" font-weight="500" fill="#0f172a">{l.t}</text>
</g>
));
})()}
<!-- Traveling dots — bigger, glowing, on every path -->
{[
{ id: 'in-1', dur: '3.2s', begin: '0s', color: '#0ea5e9' },
{ id: 'in-2', dur: '3.6s', begin: '0.5s', color: '#0ea5e9' },
{ id: 'in-3', dur: '3.0s', begin: '1.0s', color: '#0ea5e9' },
{ id: 'in-4', dur: '3.8s', begin: '1.5s', color: '#0ea5e9' },
{ id: 'in-5', dur: '3.4s', begin: '2.0s', color: '#0ea5e9' },
{ id: 'out-1', dur: '3.2s', begin: '2.6s', color: '#10b981' },
{ id: 'out-2', dur: '3.6s', begin: '3.0s', color: '#f43f5e' },
{ id: 'out-3', dur: '3.0s', begin: '3.4s', color: '#f59e0b' },
{ id: 'out-4', dur: '3.8s', begin: '3.8s', color: '#64748b' },
{ id: 'out-5', dur: '3.4s', begin: '4.2s', color: '#fb923c' },
].map((d) => (
<circle r="6" fill={d.color} filter="url(#dot-glow)">
<animateMotion dur={d.dur} repeatCount="indefinite" begin={d.begin}><mpath href={`#${d.id}`}/></animateMotion>
</circle>
))}
</svg>
</div>
<div class="mt-12 flex items-center gap-3">
<a href="/inbox/" class="inline-flex items-center gap-1.5 text-[13.5px] font-medium text-foreground border-b border-foreground/30 hover:border-foreground pb-px">
See the inbox
<Icon name="arrowUpRight" size={13} />
</a>
<span class="text-[12.5px] text-muted-foreground">Cross-mailbox search, snooze with reminders, round-robin assignment.</span>
</div>
</div>
</section>
<!-- ============================================================
CAMPAIGNS — code config + horizontal timeline of steps
============================================================ -->
<section class="relative overflow-hidden border-t border-[color:var(--border)]" style="background: #fafafa;">
<div class="container-page py-20 md:py-28 relative">
<div class="grid lg:grid-cols-12 gap-12 lg:gap-16 mb-12">
<div class="lg:col-span-5">
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Campaigns</div>
<h2 class="text-[34px] md:text-[44px] font-semibold tracking-[-0.025em] leading-[1.08] text-heading">
Sequences as configuration, not click-and-drag prison.
</h2>
</div>
<div class="lg:col-span-7">
<p class="text-[15.5px] text-foreground/75 leading-relaxed">
Build a sequence in the UI or import one as YAML through the API. Up to 12 steps with A/B variants, business-hour delays, and conditions that branch on opens, replies and bounces.
</p>
</div>
</div>
<!-- Auto-scrolling step timeline (no scrollbar, loops forever) -->
<div class="relative marquee-mask overflow-hidden">
<!-- Faint connecting rail behind the cards -->
<div class="pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 h-px bg-gradient-to-r from-transparent via-[color:var(--border-strong)] to-transparent opacity-60"></div>
<ol class="relative flex items-stretch gap-0 w-max marquee-track-slow py-1">
{Array.from({ length: 2 }).map(() => (
<>
{[
{ n: 1, k: 'SEND', t: 'Intro · plain text', m: '2 variants', icon: 'send' },
{ n: 2, k: 'WAIT', t: '3 business days', m: 'if no reply', icon: 'clock' },
{ n: 3, k: 'SEND', t: 'Follow-up · context line', m: '4 variants', icon: 'send' },
{ n: 4, k: 'BRANCH', t: 'If opened twice', m: 'send earlier',icon: 'workflow' },
{ n: 5, k: 'SEND', t: 'Soft close · short ask', m: '2 variants', icon: 'send' },
{ n: 6, k: 'WAIT', t: '7 business days', m: 'silence', icon: 'clock' },
{ n: 7, k: 'SEND', t: 'Final · breakup note', m: '1 variant', icon: 'send' },
{ n: 8, k: 'END', t: 'Stop · mark contact done', m: 'sequence done', icon: 'check' },
].map((s) => {
const tone = s.k === 'SEND' ? { bg: 'bg-sky-50', text: 'text-sky-700', dot: 'bg-sky-500' } :
s.k === 'WAIT' ? { bg: 'bg-slate-50', text: 'text-slate-500', dot: 'bg-slate-400' } :
s.k === 'BRANCH' ? { bg: 'bg-violet-50', text: 'text-violet-700', dot: 'bg-violet-500' } :
{ bg: 'bg-emerald-50',text: 'text-emerald-700', dot: 'bg-emerald-500' };
return (
<li class="shrink-0 flex items-center">
<!-- step card -->
<div class="w-[260px] rounded-[12px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden" style="box-shadow: var(--shadow-sm);">
<div class={`flex items-center gap-2 px-4 h-8 ${tone.bg}`}>
<span class={`w-1.5 h-1.5 rounded-full ${tone.dot}`}></span>
<span class={`text-[10px] font-mono font-semibold uppercase tracking-[0.16em] ${tone.text}`}>{s.k}</span>
<span class="ml-auto text-[10px] font-mono text-muted-foreground">{String(s.n).padStart(2, '0')}</span>
</div>
<div class="px-4 py-3.5">
<div class="flex items-start gap-2.5">
<Icon name={s.icon} size={15} class="text-foreground/55 mt-0.5 shrink-0" />
<div class="min-w-0">
<div class="text-[13.5px] font-medium text-heading leading-snug">{s.t}</div>
<div class="mt-1 text-[11px] font-mono text-muted-foreground">{s.m}</div>
</div>
</div>
</div>
</div>
<!-- connector arrow -->
<div class="shrink-0 px-2.5 text-[color:var(--border-strong)]" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M3 7h8M7.5 3.5L11 7l-3.5 3.5"/></svg>
</div>
</li>
);
})}
</>
))}
</ol>
</div>
<!-- Config block -->
<div class="mt-12 grid lg:grid-cols-12 gap-8 items-start">
<div class="lg:col-span-7">
<div class="rounded-[10px] overflow-hidden ring-1 ring-[color:var(--border)] bg-[#0c1224]"
style="box-shadow: var(--shadow-md);">
<div class="flex items-center gap-2 px-4 h-9 border-b border-white/10 bg-[#0a0f1e]">
<div class="flex items-center gap-1.5">
<span class="w-2.5 h-2.5 rounded-full bg-[#ff5f57]"></span>
<span class="w-2.5 h-2.5 rounded-full bg-[#febc2e]"></span>
<span class="w-2.5 h-2.5 rounded-full bg-[#28c840]"></span>
</div>
<span class="ml-3 text-[11px] font-mono text-white/45">campaign.q2-outbound.yaml</span>
</div>
<pre class="text-[12.5px] leading-[1.7] font-mono text-white/85 p-5 overflow-x-auto"><code><span style="color:#7dd3fc">name</span>: <span style="color:#fcd34d">Q2 outbound · enterprise</span>
<span style="color:#7dd3fc">mailboxes</span>: <span style="color:#fcd34d">[founder@acme.co, team@acme.co]</span>
<span style="color:#7dd3fc">steps</span>:
<span style="color:#a78bfa">- type</span>: <span style="color:#86efac">send</span>
<span style="color:#a78bfa">subject</span>: <span style="color:#fcd34d">Re: {`{{news.summary}}`}</span>
<span style="color:#a78bfa">variants</span>: <span style="color:#fcd34d">2</span>
<span style="color:#a78bfa">- type</span>: <span style="color:#86efac">wait</span>
<span style="color:#a78bfa">days</span>: <span style="color:#fcd34d">3</span>
<span style="color:#a78bfa">unless</span>: <span style="color:#fcd34d">replied</span>
<span style="color:#a78bfa">- type</span>: <span style="color:#86efac">send</span>
<span style="color:#a78bfa">subject</span>: <span style="color:#fcd34d">Quick context on {`{{custom.pain}}`}</span>
<span style="color:#a78bfa">variants</span>: <span style="color:#fcd34d">4</span>
<span style="color:#a78bfa">- type</span>: <span style="color:#86efac">branch</span>
<span style="color:#a78bfa">if</span>: <span style="color:#fcd34d">opens &gt;= 2</span>
<span style="color:#a78bfa">jump</span>: <span style="color:#fcd34d">step.5</span></code></pre>
</div>
</div>
<div class="lg:col-span-5">
<h3 class="text-[18px] font-semibold tracking-[-0.015em] text-heading">Built around an API, not against one.</h3>
<p class="mt-3 text-[14px] text-foreground/75 leading-relaxed">
Every sequence built in the UI is the same object the API returns. Version-control campaigns, branch them in code, deploy with a webhook.
</p>
<a href="/developers/" class="mt-5 inline-flex items-center gap-1.5 text-[13.5px] font-medium text-foreground border-b border-foreground/30 hover:border-foreground pb-px">
API reference
<Icon name="arrowUpRight" size={13} />
</a>
</div>
</div>
</div>
</section>
<!-- Changelog + Go templating: useful content sections after the feature showcase -->
<ChangelogShowcase />
<TemplatingShowcase />
<!-- ============================================================
COMPARE — serious data table
@@ -1035,6 +794,36 @@ const softwareJsonLd = {
transition: none;
}
}
/* Schedule grid — window blocks grow up once their feature frame enters
view. JS-gated (html.js-ready) so blocks stay visible if JS never runs;
reduced-motion shows them in their final state. */
html.js-ready .showcase-frame .sched-block {
transform: scaleY(0);
transform-origin: top;
opacity: 0;
}
html.js-ready .showcase-frame.is-in .sched-block {
animation: sched-grow 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes sched-grow {
from { transform: scaleY(0); opacity: 0; }
to { transform: scaleY(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
html.js-ready .showcase-frame .sched-block,
html.js-ready .showcase-frame.is-in .sched-block {
transform: none;
opacity: 1;
animation: none;
}
}
/* Pause the demo cursor while a visitor is inspecting a feature window. */
.feature-window:hover .demo-cursor,
.feature-window:hover .demo-cursor-ripple {
animation-play-state: paused;
}
</style>
<script is:inline>
// Tag the document so the CSS above can hide fade-in targets only when JS is alive.
@@ -1076,11 +865,12 @@ const softwareJsonLd = {
}, { amount: 0.12 });
});
};
tag('.pricing-card', 0.10);
tag('.step-card', 0.10);
tag('.feature-card', 0.10);
tag('.metric-cell', 0.06);
tag('.bento-card', 0.06);
tag('.pricing-card', 0.10);
tag('.step-card', 0.10);
tag('.feature-card', 0.10);
tag('.metric-cell', 0.06);
tag('.bento-card', 0.06);
tag('.showcase-frame', 0.12);
// ── Bar grow on scroll ──────────────────────────────────────────
document.querySelectorAll('.bar-fill').forEach((el) => {