mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-09 16:04:41 +00:00
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:
+189
-399
@@ -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 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: 'Mon–Fri 9–5 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 >= 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) => {
|
||||
|
||||
Reference in New Issue
Block a user