mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-10 16:04:55 +00:00
site(use-cases): full redesign of recruiters page, drops shared UseCasePage component
This commit is contained in:
@@ -112,8 +112,8 @@ const faq: [string, string][] = [
|
||||
|
||||
// Astro JSX caveat: hoist `<` and `<=` comparisons to the frontmatter where possible.
|
||||
const tipsForRecruiters = [
|
||||
'No image-heavy job description — link to a hosted JD page instead.',
|
||||
'No HTML signature with logo, social links, or banner — plain text only.',
|
||||
'No image-heavy job description. Link to a hosted JD page instead.',
|
||||
'No HTML signature with logo, social links, or banner. Plain text only.',
|
||||
'No tracking pixel on candidate outreach.',
|
||||
'Use the recruiter\'s individual mailbox, not careers@ or talent@.',
|
||||
'Keep the first message under 90 words.',
|
||||
@@ -349,7 +349,8 @@ We were both at {{ custom.connection }} <span style="color:#
|
||||
|
||||
Open to a 20-minute call next week?
|
||||
|
||||
— {{ sender.first_name }}
|
||||
Thanks,
|
||||
{{ sender.first_name }}
|
||||
{{ sender.title | fallback: <span style="color:#fde68a">"Recruiter"</span> }} · {{ sender.agency }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,628 @@
|
||||
---
|
||||
import UseCasePage from '../../components/UseCasePage.astro';
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import HeroAtmosphere from '../../components/HeroAtmosphere.astro';
|
||||
import Icon from '../../components/Icon.astro';
|
||||
import CTA from '../../components/CTA.astro';
|
||||
|
||||
// Platform defaults referenced throughout, all from CLAUDE.md / config:
|
||||
// internal/config/constants.go · 100 / day cold cap, 600s gap
|
||||
// internal/scheduler/warmup_scheduler.go · 40 / day warmup ceiling
|
||||
// internal/repository/pg_worker.go · sender pool round-robin, health-aware
|
||||
|
||||
// ===================== HERO MOCK · "Rep board" =====================
|
||||
// 8 BDRs/AEs, 2 mailboxes each, today's send count vs cap, reply spark, status.
|
||||
// Numbers stay below the 100/day platform cold cap, with realistic spread.
|
||||
type Status = 'healthy' | 'watch' | 'capped';
|
||||
type Mailbox = { addr: string; today: number; cap: number; status: Status };
|
||||
type Rep = { initials: string; tone: string; name: string; role: 'BDR' | 'AE'; territory: string; reply: number[]; mailboxes: [Mailbox, Mailbox] };
|
||||
|
||||
const reps: Rep[] = [
|
||||
{ initials: 'BL', tone: 'bg-sky-100 text-sky-700', name: 'Ben Lee', role: 'BDR', territory: 'NA-East', reply: [2, 3, 2, 4, 5, 3, 4],
|
||||
mailboxes: [
|
||||
{ addr: 'ben@acme.com', today: 87, cap: 100, status: 'healthy' },
|
||||
{ addr: 'ben@acmeinc.io', today: 64, cap: 100, status: 'healthy' },
|
||||
] },
|
||||
{ initials: 'SR', tone: 'bg-emerald-100 text-emerald-700', name: 'Sara Reyes', role: 'BDR', territory: 'NA-West', reply: [3, 4, 4, 5, 6, 5, 7],
|
||||
mailboxes: [
|
||||
{ addr: 'sara@acme.com', today: 93, cap: 100, status: 'healthy' },
|
||||
{ addr: 'sara@acmeinc.io', today: 51, cap: 100, status: 'healthy' },
|
||||
] },
|
||||
{ initials: 'MO', tone: 'bg-amber-100 text-amber-700', name: 'Mark Okafor', role: 'BDR', territory: 'EMEA', reply: [1, 2, 2, 3, 2, 3, 3],
|
||||
mailboxes: [
|
||||
{ addr: 'mark@acme.com', today: 100, cap: 100, status: 'capped' },
|
||||
{ addr: 'mark@acmeinc.io', today: 42, cap: 100, status: 'healthy' },
|
||||
] },
|
||||
{ initials: 'KN', tone: 'bg-violet-100 text-violet-700', name: 'Kai Nakamura', role: 'BDR', territory: 'APAC', reply: [4, 5, 6, 5, 7, 6, 8],
|
||||
mailboxes: [
|
||||
{ addr: 'kai@acme.com', today: 71, cap: 100, status: 'healthy' },
|
||||
{ addr: 'kai@acmeinc.io', today: 56, cap: 100, status: 'healthy' },
|
||||
] },
|
||||
{ initials: 'JP', tone: 'bg-rose-100 text-rose-700', name: 'Jess Park', role: 'BDR', territory: 'NA-East', reply: [2, 2, 1, 2, 3, 2, 2],
|
||||
mailboxes: [
|
||||
{ addr: 'jess@acme.com', today: 38, cap: 100, status: 'watch' },
|
||||
{ addr: 'jess@acmeinc.io', today: 47, cap: 100, status: 'healthy' },
|
||||
] },
|
||||
{ initials: 'AN', tone: 'bg-teal-100 text-teal-700', name: 'Amir Naseri', role: 'BDR', territory: 'EMEA', reply: [3, 3, 4, 4, 5, 4, 5],
|
||||
mailboxes: [
|
||||
{ addr: 'amir@acme.com', today: 81, cap: 100, status: 'healthy' },
|
||||
{ addr: 'amir@acmeinc.io', today: 60, cap: 100, status: 'healthy' },
|
||||
] },
|
||||
{ initials: 'DV', tone: 'bg-indigo-100 text-indigo-700', name: 'Dana Vasquez', role: 'AE', territory: 'NA-West', reply: [5, 6, 5, 7, 6, 8, 7],
|
||||
mailboxes: [
|
||||
{ addr: 'dana@acme.com', today: 22, cap: 100, status: 'healthy' },
|
||||
{ addr: 'dana@acmeinc.io', today: 14, cap: 100, status: 'healthy' },
|
||||
] },
|
||||
{ initials: 'TM', tone: 'bg-fuchsia-100 text-fuchsia-700', name: 'Tom Miller', role: 'AE', territory: 'NA-East', reply: [4, 5, 4, 6, 5, 7, 6],
|
||||
mailboxes: [
|
||||
{ addr: 'tom@acme.com', today: 18, cap: 100, status: 'healthy' },
|
||||
{ addr: 'tom@acmeinc.io', today: 9, cap: 100, status: 'healthy' },
|
||||
] },
|
||||
];
|
||||
|
||||
// Pre-compute aggregates for the rep-board summary strip.
|
||||
let aggToday = 0;
|
||||
let aggCap = 0;
|
||||
let mboxCount = 0;
|
||||
let healthyCount = 0;
|
||||
for (const r of reps) {
|
||||
for (const m of r.mailboxes) {
|
||||
aggToday += m.today;
|
||||
aggCap += m.cap;
|
||||
mboxCount += 1;
|
||||
if (m.status === 'healthy') healthyCount += 1;
|
||||
}
|
||||
}
|
||||
const aggPct = Math.round((aggToday / aggCap) * 100);
|
||||
|
||||
// Status chip styling (frontmatter-only comparisons).
|
||||
const statusChip = (s: Status) => {
|
||||
if (s === 'healthy') return { dot: 'bg-emerald-500', text: 'text-emerald-700', bg: 'bg-emerald-50', label: 'healthy' };
|
||||
if (s === 'watch') return { dot: 'bg-amber-500', text: 'text-amber-700', bg: 'bg-amber-50', label: 'watch' };
|
||||
return { dot: 'bg-rose-500', text: 'text-rose-700', bg: 'bg-rose-50', label: 'capped' };
|
||||
};
|
||||
|
||||
// Sparkline geometry — pre-compute polyline points to avoid JSX math noise.
|
||||
const sparkPoints = (vals: number[]) => {
|
||||
const w = 56, h = 16, max = Math.max(...vals, 1);
|
||||
return vals.map((v, i) => {
|
||||
const x = (i / (vals.length - 1)) * w;
|
||||
const y = h - (v / max) * (h - 2) - 1;
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
}).join(' ');
|
||||
};
|
||||
|
||||
// Cap utilisation tint for the per-mailbox bar.
|
||||
const capTint = (today: number, cap: number) => {
|
||||
const pct = (today / cap) * 100;
|
||||
if (pct >= 95) return 'bg-rose-500';
|
||||
if (pct >= 80) return 'bg-amber-500';
|
||||
return 'bg-[#0284c7]';
|
||||
};
|
||||
|
||||
// ===================== ROUND-ROBIN FAN-OUT (8 mailboxes) =====================
|
||||
// Used by the diagram. Sorted by territory then by role so the visual lines up.
|
||||
const fanMailboxes = reps.flatMap((r) => r.mailboxes.map((m) => ({
|
||||
addr: m.addr, role: r.role, today: m.today, cap: m.cap, initials: r.initials, tone: r.tone, status: m.status,
|
||||
})));
|
||||
|
||||
// ===================== REPLY ROUTING POLICIES =====================
|
||||
const routingPolicies = [
|
||||
{ name: 'Round-robin', body: 'Reply assigned to the next AE in the rotation, skipping those at quota.', code: 'route: round_robin(team: ae_us)', tone: 'bg-sky-50 text-sky-700' },
|
||||
{ name: 'By territory', body: 'Reply assigned by the recipient domain or contact territory field on file.', code: 'route: by_territory(field: account.region)', tone: 'bg-violet-50 text-violet-700' },
|
||||
{ name: 'By account owner', body: 'Reply assigned to the named owner in HubSpot or Salesforce.', code: 'route: crm.account_owner', tone: 'bg-emerald-50 text-emerald-700' },
|
||||
{ name: 'Manual', body: 'Reply enters the team inbox triage queue. Manager assigns from there.', code: 'route: manual(queue: triage)', tone: 'bg-slate-100 text-slate-700' },
|
||||
];
|
||||
|
||||
const autoActions = [
|
||||
{ label: 'Pause sequence', detail: 'Sequence halts for this contact across every step.' },
|
||||
{ label: 'Slack notify', detail: 'Channel ping to the assignee with reply preview and 1-click claim.' },
|
||||
{ label: 'Stage move', detail: 'CRM deal stage advances from Sequenced to Replied · Awaiting AE.' },
|
||||
{ label: 'Calendar offer', detail: 'Optional reply-handler step inserts the assignee\'s booking link.' },
|
||||
];
|
||||
|
||||
// ===================== CRM SYNC SPEC =====================
|
||||
type SyncMode = 'two-way' | 'inbound' | 'outbound';
|
||||
type SyncRow = { object: string; hubspot: SyncMode; salesforce: SyncMode; pipedrive: SyncMode; note: string };
|
||||
const crmSync: SyncRow[] = [
|
||||
{ object: 'Contact', hubspot: 'two-way', salesforce: 'two-way', pipedrive: 'two-way', note: 'Email, name, title, custom fields. Dedup on email.' },
|
||||
{ object: 'Deal', hubspot: 'two-way', salesforce: 'two-way', pipedrive: 'two-way', note: 'Created on positive reply. Stage transitions sync both ways.' },
|
||||
{ object: 'Activity', hubspot: 'outbound', salesforce: 'outbound', pipedrive: 'outbound', note: 'Every send, open, click, reply logged on the contact timeline.' },
|
||||
{ object: 'Stage', hubspot: 'two-way', salesforce: 'two-way', pipedrive: 'two-way', note: 'CRM stage moves out of CRM pause the sequence here.' },
|
||||
{ object: 'Owner', hubspot: 'two-way', salesforce: 'two-way', pipedrive: 'two-way', note: 'Reply routing reads CRM owner. Owner changes sync back.' },
|
||||
];
|
||||
|
||||
const modeBadge = (m: SyncMode) => {
|
||||
if (m === 'two-way') return { txt: 'two-way', bg: 'bg-sky-50', fg: 'text-[#0369a1]', dot: 'bg-[#0284c7]' };
|
||||
if (m === 'inbound') return { txt: 'inbound', bg: 'bg-violet-50', fg: 'text-violet-700', dot: 'bg-violet-500' };
|
||||
return { txt: 'outbound', bg: 'bg-emerald-50', fg: 'text-emerald-700', dot: 'bg-emerald-500' };
|
||||
};
|
||||
|
||||
// ===================== FAQ =====================
|
||||
const faq: [string, string][] = [
|
||||
['How do we distribute volume across reps without crushing one mailbox?', 'Add the rep mailboxes to a sender pool. The scheduler round-robins sequence sends across the pool with awareness of each mailbox\'s 100 / day cold cap, the 600s spacing rule, and the current health band. A mailbox that hits its cap is skipped for the rest of the day. The campaign does not stall.'],
|
||||
['Where do positive replies go?', 'Replies are classified, routed by the rule on the sequence (round-robin, territory, CRM account owner, or manual), and an assignment event fires. The assignment lands as a Slack notification, a CRM stage move, and an inbox claim for the named AE. The originating sequence is paused for that contact at the same moment.'],
|
||||
['Can a rep\'s mailbox issue affect the rest of the team?', 'Per-mailbox reputation is isolated. A mailbox that drifts into the watch band has its sends throttled and spacing widened. If it crosses into quarantine it leaves the sender pool entirely until it requalifies. The rest of the pool keeps sending at full pace.'],
|
||||
];
|
||||
|
||||
// Frontmatter helper to avoid `<` and `<=` in JSX expressions.
|
||||
const capRatio = (today: number, cap: number) => Math.min(100, Math.round((today / cap) * 100));
|
||||
---
|
||||
<UseCasePage
|
||||
pageTitle="Sales teams — Warmbly"
|
||||
description="Distribute sends across reps, surface positive replies fast."
|
||||
title="Sales outbound that keeps reps and reputation healthy."
|
||||
intro="When the team grows past a handful of BDRs, the math of cold mail changes. Volume should come from more healthy mailboxes, not forcing each rep’s inbox to send more."
|
||||
challenges={[
|
||||
{ title: 'BDRs hit the per-mailbox ceiling', body: 'A single rep crammed against the 50/day ceiling will burn out their mailbox by quarter end.' },
|
||||
{ title: 'Replies lost between consoles', body: 'Reps switch between their mailbox, the SDR tool, the CRM and Slack. Positive replies sit unanswered for hours.' },
|
||||
{ title: 'No team-level deliverability view', body: 'Without per-rep mailbox health, ops only knows there’s a problem when the pipeline slows.' },
|
||||
{ title: 'Manual round-robin assignment', body: 'Spreadsheets to assign replies to AEs go stale within a week.' },
|
||||
]}
|
||||
recipe={[
|
||||
{ title: 'Two mailboxes per BDR', body: 'Distribute cold volume across two mailboxes per BDR, both warmed. Cap each at 40/day to leave headroom.' },
|
||||
{ title: 'Sequences send across the pool', body: 'A single sequence assigned to a group of mailboxes round-robins sends, never overloads one rep’s inbox.' },
|
||||
{ title: 'Positive-reply routing', body: 'Webhook fires on positive replies into Slack. AE assignment happens by territory rule.' },
|
||||
{ title: 'Inbox roles', body: 'BDR sees their replies. AE sees the meeting-booked threads they own. Ops sees everything.' },
|
||||
{ title: 'Quarterly deliverability review', body: 'Growth and Business plans include a quarterly review with a Warmbly customer engineer.' },
|
||||
]}
|
||||
caps={[
|
||||
{ value: '2 / BDR', label: 'Recommended mailboxes per rep' },
|
||||
{ value: '40 / day', label: 'Suggested cold cap per mailbox' },
|
||||
{ value: 'Round-robin', label: 'Sender pool distribution' },
|
||||
{ value: 'Two-way', label: 'CRM sync · HubSpot · Salesforce' },
|
||||
]}
|
||||
/>
|
||||
<Layout
|
||||
title="Sales teams · Warmbly"
|
||||
description="Distribute outbound across rep mailboxes with round-robin sender pools, route positive replies to the right AE, and keep CRM in sync."
|
||||
>
|
||||
<!-- ============================================================
|
||||
HERO · HeroAtmosphere, identical pattern to warmup / campaigns
|
||||
============================================================ -->
|
||||
<section class="relative isolate overflow-hidden">
|
||||
<HeroAtmosphere />
|
||||
|
||||
<div class="container-page relative pt-16 md:pt-24 pb-44 md:pb-56 text-center">
|
||||
<div class="inline-flex items-center gap-2 h-7 pl-1 pr-3 rounded-full bg-white/15 backdrop-blur ring-1 ring-white/25 text-[12px] text-white/95 shadow-[0_4px_14px_-4px_rgba(0,0,0,0.25)]">
|
||||
<span class="inline-flex items-center h-5 px-1.5 rounded-full text-[10.5px] font-semibold uppercase tracking-[0.06em] bg-white" style="color:#0369a1;">
|
||||
Sales teams
|
||||
</span>
|
||||
Built for teams of BDRs and AEs working a shared pipeline
|
||||
</div>
|
||||
|
||||
<h1 class="mt-8 md:mt-10 text-[44px] sm:text-6xl md:text-[72px] lg:text-[80px] font-semibold tracking-[-0.04em] leading-[0.98] text-white max-w-4xl mx-auto">
|
||||
More reps. More mailboxes.<br/>Volume follows.
|
||||
</h1>
|
||||
<p class="mt-6 text-[17px] md:text-[19px] text-white/80 max-w-2xl mx-auto leading-relaxed">
|
||||
A sales team should not get outbound volume by squeezing each rep’s inbox. It should get volume by spreading sends across many healthy mailboxes and by routing every positive reply to the right AE in seconds.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="https://app.warmbly.com/register" class="inline-flex h-11 px-5 items-center gap-2 rounded-[10px] text-[14.5px] font-semibold bg-white hover:bg-white hover:-translate-y-0.5 hover:shadow-[0_12px_28px_-6px_rgba(0,0,0,0.3)] transition-all duration-200 ease-out shadow-[0_4px_14px_-2px_rgba(0,0,0,0.18)]">
|
||||
<span style="color:#075985;">Start free trial</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#075985" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/use-cases/" class="inline-flex h-11 px-5 items-center rounded-[10px] text-[14.5px] font-medium bg-white/10 backdrop-blur ring-1 ring-white/40 text-white hover:bg-white/20 hover:-translate-y-0.5 transition-all duration-200 ease-out">
|
||||
See more use cases
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
FLOATING "REP BOARD" · floats up into the hero (-mt-36 md:-mt-44)
|
||||
============================================================ -->
|
||||
<section class="relative -mt-36 md:-mt-44 pb-16 md:pb-24 z-10">
|
||||
<div class="container-page">
|
||||
<div class="rounded-[18px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden shadow-[0_40px_90px_-30px_rgba(2,32,71,0.35),0_12px_30px_-12px_rgba(15,23,42,0.12)]">
|
||||
|
||||
<!-- Header strip -->
|
||||
<div class="px-6 md:px-7 py-4 border-b border-[color:var(--border)] bg-[color:var(--surface-1)]/70 flex flex-wrap items-baseline justify-between gap-3">
|
||||
<div class="flex items-baseline gap-3">
|
||||
<div class="text-[13px] font-semibold text-heading">Rep board · acme</div>
|
||||
<span class="inline-flex items-center gap-1.5 h-5 px-2 rounded-full bg-emerald-50 text-emerald-700 text-[10.5px] font-medium font-mono uppercase tracking-[0.08em]">
|
||||
<span class="w-1 h-1 rounded-full bg-emerald-500"></span>
|
||||
Live · today
|
||||
</span>
|
||||
<span class="text-[10.5px] font-mono uppercase tracking-[0.14em] text-muted-foreground">{reps.length} reps · {mboxCount} mailboxes</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-[10.5px] font-mono text-muted-foreground">
|
||||
<span>{aggToday} sent today</span>
|
||||
<span class="w-1 h-1 rounded-full bg-[color:var(--border)]"></span>
|
||||
<span>{aggCap - aggToday} headroom</span>
|
||||
<span class="w-1 h-1 rounded-full bg-[color:var(--border)]"></span>
|
||||
<span>{aggPct}% pool util.</span>
|
||||
<span class="w-1 h-1 rounded-full bg-[color:var(--border)]"></span>
|
||||
<span>{healthyCount}/{mboxCount} healthy</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table head -->
|
||||
<div class="hidden md:grid grid-cols-[1.4fr_1.6fr_0.7fr_0.6fr] px-7 py-2.5 bg-white border-b border-[color:var(--border)] text-[10px] uppercase tracking-[0.16em] font-mono text-muted-foreground">
|
||||
<div>Rep · Territory</div>
|
||||
<div>Mailboxes · today / cap</div>
|
||||
<div>Reply rate · 7d</div>
|
||||
<div class="text-right">Status</div>
|
||||
</div>
|
||||
|
||||
<!-- Rep rows -->
|
||||
<div class="divide-y divide-[color:var(--border)]">
|
||||
{reps.map((r) => {
|
||||
const worst = r.mailboxes.reduce((acc: Status, m) => {
|
||||
if (m.status === 'capped' || acc === 'capped') return 'capped';
|
||||
if (m.status === 'watch' || acc === 'watch') return 'watch';
|
||||
return 'healthy';
|
||||
}, 'healthy' as Status);
|
||||
const chip = statusChip(worst);
|
||||
return (
|
||||
<div class="grid grid-cols-[1fr] md:grid-cols-[1.4fr_1.6fr_0.7fr_0.6fr] gap-2 md:gap-0 px-6 md:px-7 py-3.5 items-center hover:bg-[color:var(--surface-1)]/60 transition-colors">
|
||||
<!-- Rep identity -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span class={`w-8 h-8 rounded-full inline-flex items-center justify-center text-[11px] font-semibold font-mono ${r.tone}`}>{r.initials}</span>
|
||||
<div>
|
||||
<div class="text-[13.5px] font-medium text-heading leading-tight">{r.name}</div>
|
||||
<div class="mt-0.5 text-[10.5px] font-mono uppercase tracking-[0.14em] text-muted-foreground">{r.role} · {r.territory}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mailboxes -->
|
||||
<div class="space-y-1.5">
|
||||
{r.mailboxes.map((m) => {
|
||||
const pct = capRatio(m.today, m.cap);
|
||||
return (
|
||||
<div class="grid grid-cols-[1fr_70px_40px] items-center gap-3">
|
||||
<div class="font-mono text-[11.5px] text-foreground/85 truncate">{m.addr}</div>
|
||||
<div class="h-1.5 rounded-full overflow-hidden bg-[color:var(--surface-1)]">
|
||||
<div class={`h-full ${capTint(m.today, m.cap)}`} style={`width: ${pct}%`}></div>
|
||||
</div>
|
||||
<div class="text-right font-mono text-[11px] text-foreground/70 tabular-nums">{m.today}/{m.cap}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!-- Sparkline -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg viewBox="0 0 56 16" class="w-14 h-4">
|
||||
<polyline points={sparkPoints(r.reply)} fill="none" stroke="#0284c7" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span class="text-[11px] font-mono text-foreground/65 tabular-nums">{r.reply[r.reply.length - 1]}%</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="md:text-right">
|
||||
<span class={`inline-flex items-center gap-1.5 h-5 px-2 rounded-full text-[10px] font-mono font-semibold uppercase tracking-[0.08em] ${chip.bg} ${chip.text}`}>
|
||||
<span class={`w-1 h-1 rounded-full ${chip.dot}`}></span>
|
||||
{chip.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!-- Footer hint -->
|
||||
<div class="px-7 py-3 border-t border-[color:var(--border)] bg-[color:var(--surface-1)]/60 flex items-center justify-between text-[11px] font-mono text-muted-foreground">
|
||||
<span>Per-mailbox cap: 100 / day · spacing 600s · enforced server-side</span>
|
||||
<span class="hidden md:inline">Capped mailboxes are skipped, not stalled</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
ROUND-ROBIN DISTRIBUTION · 1 campaign -> 8 mailboxes
|
||||
============================================================ -->
|
||||
<section class="border-y border-[color:var(--border)] py-20 md:py-28">
|
||||
<div class="container-page">
|
||||
<div class="grid lg:grid-cols-[1fr_1.5fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-muted-foreground mb-3">Round-robin pool</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
One campaign.<br/>Eight rep mailboxes.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
A sequence is bound to a sender pool, not a mailbox. The scheduler distributes recipients across every mailbox in the pool, respects the per-mailbox 100 / day cap, holds to the 600s spacing rule, and skips any mailbox that has dropped out of the healthy band.
|
||||
</p>
|
||||
<ul class="mt-6 space-y-2.5 text-[14px] text-foreground/85">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0"/><span>Cap-aware: a 100 / 100 mailbox is skipped for the day.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0"/><span>Health-aware: a mailbox in the watch band gets fewer sends.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0"/><span>Spacing-aware: sends from one mailbox sit ~10 minutes apart.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0"/><span>Pool grows when you add reps, not when you raise a cap.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Fan-out diagram -->
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="px-6 py-3 border-b border-[color:var(--border)] flex items-center justify-between">
|
||||
<div class="text-[11.5px] font-mono text-muted-foreground">sequence: acme-q2-outbound</div>
|
||||
<div class="text-[11.5px] font-mono text-muted-foreground">pool size: {fanMailboxes.length}</div>
|
||||
</div>
|
||||
<div class="relative p-6 md:p-8">
|
||||
<svg viewBox="0 0 900 460" class="w-full h-auto max-h-[520px]" aria-label="One sequence distributing sends round-robin to eight rep mailboxes">
|
||||
<defs>
|
||||
<linearGradient id="flowLineSales" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#0284c7" stop-opacity="0.5"/>
|
||||
<stop offset="100%" stop-color="#0284c7" stop-opacity="0.12"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Campaign source on the left -->
|
||||
<g>
|
||||
<rect x="20" y="200" width="190" height="64" rx="12" fill="#0c4a6e"/>
|
||||
<text x="115" y="225" text-anchor="middle" fill="#7dd3fc" font-size="9" font-family="ui-monospace,monospace" letter-spacing="2">SEQUENCE</text>
|
||||
<text x="115" y="245" text-anchor="middle" fill="#ffffff" font-size="13" font-weight="600">acme-q2-outbound</text>
|
||||
<text x="115" y="259" text-anchor="middle" fill="#7dd3fc" font-size="9" font-family="ui-monospace,monospace">412 recipients</text>
|
||||
</g>
|
||||
|
||||
<!-- Center scheduler glyph -->
|
||||
<g>
|
||||
<circle cx="380" cy="232" r="36" fill="#ffffff" stroke="#0284c7" stroke-width="1.5"/>
|
||||
<text x="380" y="229" text-anchor="middle" fill="#0369a1" font-size="9" font-family="ui-monospace,monospace" letter-spacing="2">RR</text>
|
||||
<text x="380" y="243" text-anchor="middle" fill="#0c4a6e" font-size="9" font-family="ui-monospace,monospace">cap+health</text>
|
||||
<line x1="210" y1="232" x2="344" y2="232" stroke="#0284c7" stroke-width="1.5" stroke-dasharray="3 3" opacity="0.6"/>
|
||||
</g>
|
||||
|
||||
<!-- 8 mailbox cards on the right -->
|
||||
{fanMailboxes.map((m, i) => {
|
||||
const yTop = 18 + i * 53;
|
||||
const dur = 4 + (i % 4) * 0.4;
|
||||
const begin = (i * 0.45).toFixed(2);
|
||||
const capColor = m.status === 'capped' ? '#f43f5e' : m.status === 'watch' ? '#f59e0b' : '#0284c7';
|
||||
return (
|
||||
<g>
|
||||
<rect x="540" y={yTop} width="340" height="42" rx="10" fill="#f8fafc" stroke="#e6ebf2"/>
|
||||
<circle cx="560" cy={yTop + 21} r="11" fill="#e0f2fe"/>
|
||||
<text x="560" y={yTop + 24} text-anchor="middle" fill="#0369a1" font-size="8.5" font-family="ui-monospace,monospace" font-weight="700">{m.initials}</text>
|
||||
<text x="580" y={yTop + 18} fill="#0f172a" font-size="11.5" font-family="ui-monospace,monospace" font-weight="600">{m.addr}</text>
|
||||
<text x="580" y={yTop + 33} fill="#64748b" font-size="9.5" font-family="ui-monospace,monospace">{m.role} · {m.today}/{m.cap}</text>
|
||||
<rect x="780" y={yTop + 14} width="86" height="14" rx="4" fill="#ffffff" stroke="#e6ebf2"/>
|
||||
<rect x="781" y={yTop + 15} width={84 * (m.today / m.cap)} height="12" rx="3" fill={capColor} opacity="0.85"/>
|
||||
<line x1="416" y1="232" x2="540" y2={yTop + 21} stroke="url(#flowLineSales)" stroke-width="1.5"/>
|
||||
{m.status !== 'capped' && (
|
||||
<circle r="3" fill="#0284c7" opacity="0.95">
|
||||
<animateMotion dur={`${dur}s`} repeatCount="indefinite" begin={`${begin}s`}
|
||||
path={`M 416 232 L 540 ${yTop + 21}`}/>
|
||||
</circle>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="px-6 py-3 border-t border-[color:var(--border)] bg-[color:var(--surface-1)]/60 flex items-center justify-between text-[11.5px] font-mono text-muted-foreground">
|
||||
<span>Round-robin · cap-aware · health-aware</span>
|
||||
<span>Capped lanes paused, sends rebalance to healthy mailboxes</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
REPLY ROUTING · 3-step diagram + policy chips + auto-actions
|
||||
============================================================ -->
|
||||
<section class="bg-[color:var(--surface-1)]/40 border-b border-[color:var(--border)] py-20 md:py-28">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-muted-foreground mb-3">Reply routing</div>
|
||||
<h2 class="text-[32px] md:text-[44px] font-semibold tracking-[-0.03em] leading-[1.05] text-heading">
|
||||
A positive reply lands in the right AE’s lap in seconds.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/70 leading-relaxed">
|
||||
Every reply runs through classification, then the routing rule on the sequence picks the assignee. The platform fires a fixed set of actions on the same event so nothing relies on a human noticing first.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 3-step diagram -->
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] p-7 md:p-10 mb-6">
|
||||
<div class="grid lg:grid-cols-[1fr_auto_1.05fr_auto_1.05fr] gap-4 lg:gap-2 items-stretch">
|
||||
<!-- Step 1 · Reply lands -->
|
||||
<div class="rounded-[12px] bg-[color:var(--surface-1)]/70 ring-1 ring-[color:var(--border)] p-5">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="w-6 h-6 rounded-full bg-[#0c4a6e] text-white text-[10px] font-mono font-semibold inline-flex items-center justify-center">01</span>
|
||||
<span class="text-[10.5px] font-mono uppercase tracking-[0.16em] text-muted-foreground">Reply lands</span>
|
||||
</div>
|
||||
<div class="text-[14px] font-semibold text-heading">Inbound to rep mailbox</div>
|
||||
<div class="mt-3 rounded-[8px] bg-white ring-1 ring-[color:var(--border)] p-3">
|
||||
<div class="text-[11.5px] font-mono text-muted-foreground">from: <span class="text-heading">dana.li@northwind.io</span></div>
|
||||
<div class="text-[11.5px] font-mono text-muted-foreground">to: <span class="text-heading">ben@acme.com</span></div>
|
||||
<div class="mt-2 text-[12.5px] text-foreground/80 leading-snug">“Yes, this is timely. Could we put 20 minutes on the calendar next week?”</div>
|
||||
</div>
|
||||
<div class="mt-3 text-[11px] font-mono text-muted-foreground">in-reply-to: seq:acme-q2-outbound · step 03</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="hidden lg:flex items-center justify-center">
|
||||
<svg width="22" height="14" viewBox="0 0 22 14"><path d="M0 7h18m-4-5 5 5-5 5" fill="none" stroke="#0284c7" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 · Classified -->
|
||||
<div class="rounded-[12px] bg-[color:var(--surface-1)]/70 ring-1 ring-[color:var(--border)] p-5">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="w-6 h-6 rounded-full bg-[#0c4a6e] text-white text-[10px] font-mono font-semibold inline-flex items-center justify-center">02</span>
|
||||
<span class="text-[10.5px] font-mono uppercase tracking-[0.16em] text-muted-foreground">Classified</span>
|
||||
</div>
|
||||
<div class="text-[14px] font-semibold text-heading">Intent + sentiment</div>
|
||||
<ul class="mt-3 space-y-1.5 text-[12.5px]">
|
||||
<li class="flex items-center justify-between"><span class="text-foreground/70">Intent</span><span class="font-mono text-emerald-700 bg-emerald-50 px-2 py-0.5 rounded">meeting_request</span></li>
|
||||
<li class="flex items-center justify-between"><span class="text-foreground/70">Sentiment</span><span class="font-mono text-emerald-700 bg-emerald-50 px-2 py-0.5 rounded">positive</span></li>
|
||||
<li class="flex items-center justify-between"><span class="text-foreground/70">OOO</span><span class="font-mono text-slate-600 bg-slate-100 px-2 py-0.5 rounded">false</span></li>
|
||||
<li class="flex items-center justify-between"><span class="text-foreground/70">Unsubscribe</span><span class="font-mono text-slate-600 bg-slate-100 px-2 py-0.5 rounded">false</span></li>
|
||||
</ul>
|
||||
<div class="mt-3 text-[11px] font-mono text-muted-foreground">routing: by_territory(field: account.region)</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="hidden lg:flex items-center justify-center">
|
||||
<svg width="22" height="14" viewBox="0 0 22 14"><path d="M0 7h18m-4-5 5 5-5 5" fill="none" stroke="#0284c7" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 · Routed -->
|
||||
<div class="rounded-[12px] bg-white ring-1 ring-[#0284c7]/35 p-5 shadow-[0_20px_40px_-20px_rgba(2,132,199,0.3)]">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="w-6 h-6 rounded-full bg-[#0284c7] text-white text-[10px] font-mono font-semibold inline-flex items-center justify-center">03</span>
|
||||
<span class="text-[10.5px] font-mono uppercase tracking-[0.16em] text-[#0369a1]">Routed</span>
|
||||
</div>
|
||||
<div class="text-[14px] font-semibold text-[#0369a1]">Assigned to AE</div>
|
||||
<div class="mt-3 flex items-center gap-3">
|
||||
<span class="w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 text-[11px] font-mono font-semibold inline-flex items-center justify-center">DV</span>
|
||||
<div>
|
||||
<div class="text-[13px] font-semibold text-heading leading-tight">Dana Vasquez</div>
|
||||
<div class="text-[10.5px] font-mono uppercase tracking-[0.14em] text-muted-foreground">AE · NA-West</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 space-y-1.5">
|
||||
<div class="flex items-center justify-between text-[11.5px] font-mono">
|
||||
<span class="text-muted-foreground">sequence</span>
|
||||
<span class="text-rose-700">paused</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[11.5px] font-mono">
|
||||
<span class="text-muted-foreground">slack</span>
|
||||
<span class="text-emerald-700">#ae-na-west pinged</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[11.5px] font-mono">
|
||||
<span class="text-muted-foreground">crm stage</span>
|
||||
<span class="text-[#0369a1]">replied → awaiting ae</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Policy chips + auto-actions -->
|
||||
<div class="grid lg:grid-cols-[1.1fr_1fr] gap-6">
|
||||
<!-- Routing policies -->
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="px-6 py-3 border-b border-[color:var(--border)] text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Routing policies</div>
|
||||
<div class="divide-y divide-[color:var(--border)]">
|
||||
{routingPolicies.map((p) => (
|
||||
<div class="px-6 py-4 grid grid-cols-[140px_1fr] gap-4 items-start">
|
||||
<span class={`inline-flex items-center h-6 px-2.5 rounded-full text-[11px] font-mono font-semibold ${p.tone}`}>{p.name}</span>
|
||||
<div>
|
||||
<p class="text-[13px] text-foreground/80 leading-snug">{p.body}</p>
|
||||
<div class="mt-1.5 font-mono text-[11.5px] text-[#0369a1] bg-[color:var(--sky-1)]/60 px-2 py-1 rounded inline-block">{p.code}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto-actions -->
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="px-6 py-3 border-b border-[color:var(--border)] text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Auto-actions fired on assignment</div>
|
||||
<ul class="divide-y divide-[color:var(--border)]">
|
||||
{autoActions.map((a, i) => (
|
||||
<li class="px-6 py-4 grid grid-cols-[36px_1fr] gap-3 items-start">
|
||||
<span class="font-mono text-[11px] text-[#0284c7] font-semibold pt-1">{String(i + 1).padStart(2, '0')}</span>
|
||||
<div>
|
||||
<div class="text-[13.5px] font-semibold text-heading">{a.label}</div>
|
||||
<p class="mt-1 text-[12.5px] text-foreground/65 leading-snug">{a.detail}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div class="px-6 py-3 border-t border-[color:var(--border)] bg-[color:var(--surface-1)]/60 text-[11px] font-mono text-muted-foreground">
|
||||
All four fire on the same event. No human-in-the-loop delay between them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
CRM SYNC SPEC SHEET · 5 rows, HubSpot / Salesforce / Pipedrive
|
||||
============================================================ -->
|
||||
<section class="py-20 md:py-28">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-muted-foreground mb-3">CRM sync</div>
|
||||
<h2 class="text-[32px] md:text-[44px] font-semibold tracking-[-0.03em] leading-[1.05] text-heading">
|
||||
CRM of record stays the source of truth.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/70 leading-relaxed">
|
||||
The sales team’s CRM does not get a separate copy of pipeline. Warmbly syncs contacts, deals, activity, stage, and ownership both ways with HubSpot, Salesforce, and Pipedrive. A stage moved in the CRM is honoured here on the next pass.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<!-- Head -->
|
||||
<div class="grid grid-cols-[1.1fr_1fr_1fr_1fr_1.6fr] px-6 md:px-7 py-3 border-b border-[color:var(--border)] bg-[color:var(--surface-1)]/70 text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">
|
||||
<div>Object</div>
|
||||
<div>HubSpot</div>
|
||||
<div>Salesforce</div>
|
||||
<div>Pipedrive</div>
|
||||
<div>Notes</div>
|
||||
</div>
|
||||
<div class="divide-y divide-[color:var(--border)]">
|
||||
{crmSync.map((row) => {
|
||||
const h = modeBadge(row.hubspot);
|
||||
const s = modeBadge(row.salesforce);
|
||||
const p = modeBadge(row.pipedrive);
|
||||
return (
|
||||
<div class="grid grid-cols-[1.1fr_1fr_1fr_1fr_1.6fr] px-6 md:px-7 py-4 items-center text-[13px]">
|
||||
<div class="font-semibold text-heading">{row.object}</div>
|
||||
<div>
|
||||
<span class={`inline-flex items-center gap-1.5 h-5 px-2 rounded-full text-[10.5px] font-mono font-semibold ${h.bg} ${h.fg}`}>
|
||||
<span class={`w-1 h-1 rounded-full ${h.dot}`}></span>{h.txt}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class={`inline-flex items-center gap-1.5 h-5 px-2 rounded-full text-[10.5px] font-mono font-semibold ${s.bg} ${s.fg}`}>
|
||||
<span class={`w-1 h-1 rounded-full ${s.dot}`}></span>{s.txt}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class={`inline-flex items-center gap-1.5 h-5 px-2 rounded-full text-[10.5px] font-mono font-semibold ${p.bg} ${p.fg}`}>
|
||||
<span class={`w-1 h-1 rounded-full ${p.dot}`}></span>{p.txt}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-foreground/70 text-[12.5px] leading-snug">{row.note}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div class="px-6 md:px-7 py-3 border-t border-[color:var(--border)] bg-[color:var(--surface-1)]/60 flex items-center justify-between text-[11px] font-mono text-muted-foreground">
|
||||
<span>Dedup on email. Last-writer-wins on stage and owner.</span>
|
||||
<span>Webhook events for every sync transition.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
FAQ · grid-rows accordion, same as warmup / campaigns
|
||||
============================================================ -->
|
||||
<section class="bg-[color:var(--surface-1)]/40 border-t border-[color:var(--border)] py-20 md:py-24">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.8fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-muted-foreground mb-3">Sales team FAQ</div>
|
||||
<h2 class="text-[28px] md:text-[36px] font-semibold tracking-[-0.025em] leading-[1.08] text-heading">
|
||||
Three questions sales ops asks first.
|
||||
</h2>
|
||||
<p class="mt-4 text-[14.5px] text-foreground/70 leading-relaxed">
|
||||
More detail in the <a class="text-[#0369a1] hover:text-[#075985] font-medium" href="/campaigns/">campaigns</a> and <a class="text-[#0369a1] hover:text-[#075985] font-medium" href="/inbox/">inbox</a> pages.
|
||||
</p>
|
||||
</div>
|
||||
<div class="divide-y divide-[color:var(--border)]" data-faq>
|
||||
{faq.map(([q, a]) => (
|
||||
<div class="faq-row py-2">
|
||||
<button type="button" class="faq-trigger group w-full flex items-start justify-between gap-4 py-3 text-left" aria-expanded="false">
|
||||
<span class="text-[15.5px] font-medium text-heading group-hover:text-[#0369a1] transition-colors">{q}</span>
|
||||
<span class="faq-icon shrink-0 inline-flex items-center justify-center w-7 h-7 rounded-full bg-white ring-1 ring-[color:var(--border)] text-foreground/60 transition-transform duration-300 ease-out">
|
||||
<Icon name="plus" size={12} />
|
||||
</span>
|
||||
</button>
|
||||
<div class="faq-panel grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 ease-out">
|
||||
<div class="overflow-hidden">
|
||||
<p class="pb-4 pr-12 text-[14px] text-foreground/75 leading-relaxed max-w-2xl">{a}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const faq = document.querySelector('[data-faq]');
|
||||
if (!faq) return;
|
||||
faq.querySelectorAll('.faq-trigger').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const row = btn.closest('.faq-row');
|
||||
const panel = row.querySelector('.faq-panel');
|
||||
const icon = btn.querySelector('.faq-icon');
|
||||
const open = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', String(!open));
|
||||
panel.style.gridTemplateRows = open ? '0fr' : '1fr';
|
||||
if (icon) icon.style.transform = open ? 'rotate(0deg)' : 'rotate(45deg)';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTA
|
||||
title="Add reps. Add mailboxes. Volume follows."
|
||||
description="Connect mailboxes, build a sender pool, route every positive reply to the right AE."
|
||||
primaryLabel="Start free trial"
|
||||
primaryHref="https://app.warmbly.com/register"
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user