site(header): cooler mega-menu + trim to 6 meaningful products

- Removed /product, /security, /workers (Footer, 404, privacy, dpa, trust
  references updated).
- Mega-menu now 620px wide with a sky-tinted header strip showing
  'The platform · 6 systems', a 2-col grid of products where each item
  has a 40x40 icon tile that goes from quiet surface ring to a sky
  gradient on hover (with a sky drop-shadow), current page is shown
  with the gradient tile active + a sky dot next to the name. Footer
  band kept (GitHub link + pricing CTA).
This commit is contained in:
Matthew Meszaros
2026-05-26 13:59:39 +00:00
parent 9ccfb519ba
commit 561ed7b874
9 changed files with 51 additions and 682 deletions
-2
View File
@@ -5,7 +5,6 @@ const groups: { title: string; links: { label: string; href: string }[] }[] = [
{
title: 'Product',
links: [
{ label: 'Overview', href: '/product/' },
{ label: 'Warmup', href: '/warmup/' },
{ label: 'Sending engine', href: '/sending/' },
{ label: 'Unified inbox', href: '/inbox/' },
@@ -47,7 +46,6 @@ const groups: { title: string; links: { label: string; href: string }[] }[] = [
{ label: 'About', href: '/about/' },
{ label: 'Contact', href: '/contact/' },
{ label: 'Brand', href: '/brand/' },
{ label: 'Security', href: '/security/' },
{ label: 'Trust', href: '/trust/' },
],
},
+46 -79
View File
@@ -10,30 +10,20 @@ const { onSky = false } = Astro.props;
const path = Astro.url.pathname;
// Product mega-menu items grouped into two columns
const productGroups = [
{
label: 'Send & deliver',
items: [
{ icon: 'flame', href: '/warmup/', title: 'Warmup', desc: 'Reputation-safe pools and gradual ramps.' },
{ icon: 'send', href: '/sending/', title: 'Sending engine', desc: 'Distributed across mailboxes, workers, IPs.' },
{ icon: 'cpu', href: '/workers/', title: 'Workers', desc: 'One worker per machine, never NAT-shared.' },
{ icon: 'shield', href: '/security/', title: 'Security', desc: 'Envelope encryption with AWS KMS.' },
],
},
{
label: 'Engage & analyse',
items: [
{ icon: 'inbox', href: '/inbox/', title: 'Unified inbox', desc: 'Every reply across every mailbox, classified.' },
{ icon: 'workflow', href: '/campaigns/', title: 'Campaigns', desc: 'Sequences with branching and A/B variants.' },
{ icon: 'contact', href: '/crm/', title: 'CRM', desc: 'Pipelines, deals and tasks next to sending.' },
{ icon: 'chart', href: '/analytics/', title: 'Analytics', desc: 'Placement, complaints, bounces, by mailbox.' },
],
},
// Product mega-menu items
const products = [
{ icon: 'flame', href: '/warmup/', title: 'Warmup', desc: 'Reputation-safe pools, gradual ramps.' },
{ icon: 'send', href: '/sending/', title: 'Sending engine', desc: 'Distributed across mailboxes and IPs.' },
{ icon: 'inbox', href: '/inbox/', title: 'Unified inbox', desc: 'Every reply, classified on arrival.' },
{ icon: 'workflow', href: '/campaigns/', title: 'Campaigns', desc: 'Sequences with branching and variants.' },
{ icon: 'chart', href: '/analytics/', title: 'Analytics', desc: 'Placement, complaints, bounces per mailbox.' },
{ icon: 'contact', href: '/crm/', title: 'CRM', desc: 'Pipelines, deals and tasks next to sending.' },
{ icon: 'cpu', href: '/workers/', title: 'Workers', desc: 'One worker per machine, isolated IPs.' },
{ icon: 'shield', href: '/security/', title: 'Security', desc: 'KMS envelope encryption, audit log.' },
];
const nav: { label: string; href: string; match?: string; mega?: boolean }[] = [
{ label: 'Product', href: '/product/', match: '/product', mega: true },
{ label: 'Product', href: '/warmup/', match: '/warmup|/sending|/inbox|/campaigns|/analytics|/crm', mega: true },
{ label: 'Deliverability',href: '/deliverability/',match: '/deliverability' },
{ label: 'Pricing', href: '/pricing/', match: '/pricing' },
{ label: 'Learn', href: '/learn/', match: '/learn' },
@@ -41,7 +31,9 @@ const nav: { label: string; href: string; match?: string; mega?: boolean }[] = [
function active(match?: string) {
if (!match) return false;
return path === match || path.startsWith(match + '/');
// Support pipe-delimited multi-match for the Product mega item
const parts = match.split('|');
return parts.some((m) => path === m || path.startsWith(m + '/'));
}
---
<header
@@ -75,71 +67,46 @@ function active(match?: string) {
</a>
<!-- Mega-menu panel -->
<div class="mega-panel absolute left-0 top-full pt-2 pointer-events-none opacity-0 transition-all duration-200 ease-out" style="width: 720px; max-width: 90vw;">
<div class="rounded-[16px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden" style="box-shadow: 0 24px 60px -20px rgba(15,23,42,0.25), 0 8px 24px -8px rgba(15,23,42,0.08);">
<!-- Top: overview + columns -->
<div class="grid grid-cols-[1.4fr_1fr] gap-px bg-[color:var(--border)]">
<!-- Featured: product overview -->
<a href="/product/" class="group bg-white p-5 hover:bg-[color:var(--sky-1)]/40 transition-colors">
<div class="flex items-start gap-3">
<div class="w-9 h-9 rounded-[10px] bg-gradient-to-br from-[#0284c7] to-[#075985] inline-flex items-center justify-center shrink-0">
<Icon name="layers" size={17} class="text-white" />
</div>
<div>
<div class="text-[13.5px] font-semibold text-heading group-hover:text-[#0369a1] transition-colors">Product overview</div>
<div class="mt-0.5 text-[11.5px] text-foreground/65 leading-snug">Nine systems, one platform.</div>
</div>
</div>
<div class="mt-3 ml-12 inline-flex items-center gap-1 text-[11.5px] font-medium text-[#0369a1] opacity-0 group-hover:opacity-100 transition-opacity">
See all <Icon name="arrowRight" size={11} />
</div>
</a>
<!-- Quick links -->
<div class="bg-[color:var(--surface-1)] p-5">
<div class="text-[10px] uppercase tracking-[0.18em] font-mono text-muted-foreground mb-3">Resources</div>
<div class="space-y-2">
<a href="/deliverability/" class="block text-[12.5px] text-foreground/75 hover:text-[#0369a1] transition-colors">Deliverability →</a>
<a href="/developers/" class="block text-[12.5px] text-foreground/75 hover:text-[#0369a1] transition-colors">API & webhooks →</a>
<a href="/integrations/" class="block text-[12.5px] text-foreground/75 hover:text-[#0369a1] transition-colors">Integrations →</a>
<a href="/changelog/" class="block text-[12.5px] text-foreground/75 hover:text-[#0369a1] transition-colors">Changelog →</a>
</div>
<div class="mega-panel absolute left-0 top-full pt-2 pointer-events-none opacity-0 transition-all duration-200 ease-out" style="width: 620px; max-width: 92vw;">
<div class="rounded-[16px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden" style="box-shadow: 0 28px 70px -22px rgba(2,32,71,0.32), 0 10px 28px -10px rgba(15,23,42,0.10);">
<!-- Subtle sky gradient header strip -->
<div class="relative px-4 py-2.5 border-b border-[color:var(--border)] overflow-hidden">
<div class="absolute inset-0 pointer-events-none" style="background: linear-gradient(180deg, rgba(186,230,253,0.55) 0%, rgba(186,230,253,0) 100%);"></div>
<div class="relative flex items-center justify-between">
<div class="text-[10px] uppercase tracking-[0.22em] font-mono text-[#075985]">The platform</div>
<div class="text-[10px] uppercase tracking-[0.22em] font-mono text-muted-foreground">{products.length} systems</div>
</div>
</div>
<!-- Product groups grid -->
<div class="grid grid-cols-2 gap-px bg-[color:var(--border)] border-t border-[color:var(--border)]">
{productGroups.map((g) => (
<div class="bg-white p-5">
<div class="text-[10px] uppercase tracking-[0.18em] font-mono text-muted-foreground mb-3 flex items-center gap-2">
<span>{g.label}</span>
<span class="flex-1 h-px bg-[color:var(--border)]"></span>
</div>
<div class="space-y-1">
{g.items.map((it) => (
<a href={it.href} class="mega-item group flex items-start gap-3 p-2 -mx-2 rounded-[8px] hover:bg-[color:var(--sky-1)]/50 transition-colors">
<div class="w-8 h-8 rounded-[8px] bg-[color:var(--surface-1)] text-[#0284c7] ring-1 ring-[color:var(--border)] group-hover:bg-white group-hover:ring-[color:var(--sky-3)] inline-flex items-center justify-center shrink-0 transition-all">
<Icon name={it.icon} size={15} />
</div>
<div class="min-w-0 flex-1">
<div class="text-[13px] font-semibold text-heading group-hover:text-[#0369a1] transition-colors">{it.title}</div>
<div class="text-[11.5px] text-foreground/60 leading-snug truncate">{it.desc}</div>
</div>
<Icon name="arrowRight" size={11} class="text-foreground/30 group-hover:text-[#0369a1] group-hover:translate-x-0.5 transition-all mt-2 shrink-0" />
</a>
))}
</div>
</div>
))}
<!-- 2-column product grid -->
<div class="p-3 grid grid-cols-2 gap-1.5">
{products.map((it) => {
const isCurrent = path === it.href || path.startsWith(it.href);
return (
<a href={it.href} aria-current={isCurrent ? 'page' : undefined} class={`mega-item group relative flex items-start gap-3.5 p-3.5 rounded-[12px] transition-all ${isCurrent ? 'bg-[color:var(--sky-1)]/70 ring-1 ring-[color:var(--sky-3)]' : 'hover:bg-[color:var(--sky-1)]/60 ring-1 ring-transparent hover:ring-[color:var(--border)]'}`}>
<div class={`w-10 h-10 rounded-[10px] inline-flex items-center justify-center shrink-0 transition-all ${isCurrent ? 'bg-gradient-to-br from-[#0284c7] to-[#075985] text-white shadow-[0_4px_12px_-2px_rgba(2,132,199,0.4)]' : 'bg-[color:var(--surface-1)] text-[#0284c7] ring-1 ring-[color:var(--border)] group-hover:bg-gradient-to-br group-hover:from-[#0284c7] group-hover:to-[#075985] group-hover:text-white group-hover:ring-transparent group-hover:shadow-[0_4px_12px_-2px_rgba(2,132,199,0.4)]'}`}>
<Icon name={it.icon} size={17} />
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
<span class={`text-[13.5px] font-semibold transition-colors ${isCurrent ? 'text-[#0369a1]' : 'text-heading group-hover:text-[#0369a1]'}`}>{it.title}</span>
{isCurrent && <span class="w-1.5 h-1.5 rounded-full bg-[#0284c7]"></span>}
</div>
<div class="mt-0.5 text-[11.5px] text-foreground/60 leading-snug">{it.desc}</div>
</div>
<Icon name="arrowRight" size={11} class={`absolute right-3 top-1/2 -translate-y-1/2 transition-all ${isCurrent ? 'text-[#0369a1]' : 'text-foreground/0 group-hover:text-[#0369a1] group-hover:translate-x-0'}`} />
</a>
);
})}
</div>
<!-- Footer -->
<div class="bg-[color:var(--surface-1)] px-5 py-3 border-t border-[color:var(--border)] flex items-center justify-between text-[11.5px]">
<a href="https://github.com/warmbly/warmbly" class="inline-flex items-center gap-1.5 font-mono text-foreground/70 hover:text-foreground transition-colors">
<!-- Footer band -->
<div class="bg-[color:var(--surface-1)] px-4 py-3 border-t border-[color:var(--border)] flex items-center justify-between text-[11.5px]">
<a href="https://github.com/warmbly/warmbly" target="_blank" rel="noopener" class="inline-flex items-center gap-1.5 font-mono text-foreground/70 hover:text-foreground transition-colors">
<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.8 10.9.6.1.8-.2.8-.5v-2c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.4-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.3 1.2 1-.3 2-.4 3-.4s2 .1 3 .4c2.3-1.5 3.3-1.2 3.3-1.2.7 1.6.2 2.9.1 3.1.8.8 1.2 1.9 1.2 3.2 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.5 4.6-1.5 7.8-5.8 7.8-10.9C23.5 5.7 18.3.5 12 .5z"/></svg>
Open source · Apache 2.0
</a>
<a href="/pricing/" class="inline-flex items-center gap-1 font-medium text-[#0369a1] hover:text-[#075985]">
<a href="/pricing/" class="inline-flex items-center gap-1 font-semibold text-[#0369a1] hover:text-[#075985]">
See pricing <Icon name="arrowRight" size={11} />
</a>
</div>
+1 -1
View File
@@ -36,7 +36,7 @@ import Icon from '../components/Icon.astro';
<div class="mt-12 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-[12px] font-mono text-white/55">
{[
['Product', '/product/'],
['Warmup', '/warmup/'],
['Pricing', '/pricing/'],
['Changelog', '/changelog/'],
['Contact', '/contact/'],
+1 -1
View File
@@ -41,7 +41,7 @@ import Legal from '../layouts/Legal.astro';
<h2>6. Security</h2>
<p>
Warmbly implements appropriate technical and organisational measures, including envelope encryption with AWS KMS, AES-256-GCM at rest, TLS in transit, role-based access control, audit logging, and the security controls described at <a href="/security/">/security</a>.
Warmbly implements appropriate technical and organisational measures, including envelope encryption with AWS KMS, AES-256-GCM at rest, TLS in transit, role-based access control, audit logging, and the security controls described at <a href="/trust/">/security</a>.
</p>
<h2>7. Sub-processors</h2>
+1 -1
View File
@@ -87,7 +87,7 @@ import Legal from '../layouts/Legal.astro';
<h2>8. Security</h2>
<p>
We protect customer data with envelope encryption (AES-256-GCM with KMS-derived data keys), TLS in transit, strict access controls, audit logging, and ongoing security review. Details at <a href="/security/">/security</a>.
We protect customer data with envelope encryption (AES-256-GCM with KMS-derived data keys), TLS in transit, strict access controls, audit logging, and ongoing security review. Details at <a href="/trust/">/security</a>.
</p>
<h2>9. Children</h2>
-254
View File
@@ -1,254 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Cloud from '../components/Cloud.astro';
import Icon from '../components/Icon.astro';
import CTA from '../components/CTA.astro';
const pillars = [
{
n: '01',
icon: 'flame',
title: 'Warmup',
href: '/warmup/',
body: 'Reputation-safe pools, gradual ramps, automatic quarantine. Free and premium pools never mix.',
bullets: [
'Start at 10 / day · ramp +1 / day · ceiling 40',
'Spam-score tracking + invalid token detection',
'Auto-block at 3 invalid tokens / 24h or score > 50',
'Recovery pool for cooldown senders',
],
},
{
n: '02',
icon: 'send',
title: 'Sending engine',
href: '/sending/',
body: 'Workers distribute campaign mail across machines. Per-mailbox caps, per-worker concentration limits.',
bullets: [
'Default 50 / day cold cap per mailbox',
'Minimum 10-minute gap between sends',
'Worker volume = sum of mailbox budgets',
'Native Gmail, Outlook, SMTP / IMAP',
],
},
{
n: '03',
icon: 'inbox',
title: 'Unified inbox',
href: '/inbox/',
body: 'Every reply, OOO, bounce and click across every mailbox in one console. Assign, snooze, label, search.',
bullets: [
'Reply detection that survives quoted text',
'OOO classification with auto-suppress',
'Thread-level assignment to teammates',
'Search across all connected mailboxes',
],
},
{
n: '04',
icon: 'workflow',
title: 'Sequences',
href: '/campaigns/',
body: 'Multi-step sequences with conditions, A/B variants and time-zone aware sending.',
bullets: [
'Step delays in business hours by recipient TZ',
'A/B variants on subject and body',
'If-replied / if-opened / if-clicked branching',
'Plain-text first. HTML is opt-in.',
],
},
{
n: '05',
icon: 'chart',
title: 'Analytics',
href: '/analytics/',
body: 'Placement, complaint rate, bounce rate, warmup health by mailbox, pool and worker. Not just opens.',
bullets: [
'Inbox vs spam placement per mailbox',
"Complaint rate vs Google's 0.1% / 0.3% bands",
"Bounce rate vs SES's 5% / 10% bands",
'Health: healthy → watch → quarantined',
],
},
{
n: '06',
icon: 'contact',
title: 'Contacts & CRM',
href: '/crm/',
body: 'Light CRM with pipelines, deals and tasks. Imports, dedup, categories, suppression-aware.',
bullets: [
'CSV / XLSX / JSON import with column mapping',
'Dedup by email, domain or custom key',
'Categories instead of tag soup',
'Suppression list shared across campaigns',
],
},
{
n: '07',
icon: 'cpu',
title: 'Workers',
href: '/workers/',
body: 'Distributed execution plane. Shared free, shared premium, or dedicated to your org.',
bullets: [
'Kafka-driven command / result topics',
'Per-worker IP identity, no shared NAT',
'Live rebalance and migration',
'Lightweight: no Postgres on workers',
],
},
{
n: '08',
icon: 'shield',
title: 'Security',
href: '/security/',
body: 'Envelope encryption with AWS KMS, per-user data keys, cached decryption, audit log.',
bullets: [
'AES-256-GCM via KMS-generated DEKs',
'Encrypted DEK stored, plaintext cached briefly',
'Workers never touch Postgres',
'Admin actions audited',
],
},
{
n: '09',
icon: 'webhook',
title: 'API & webhooks',
href: '/developers/',
body: 'Programmatic control over mailboxes, campaigns, contacts. Signed webhooks for every event.',
bullets: [
'REST with idempotency keys',
'HMAC-signed webhooks',
'Per-key scopes and rotation',
'Rate limits surfaced in headers',
],
},
];
const flow = [
{ label: 'Connect mailbox', sub: 'OAuth · SMTP · IMAP' },
{ label: 'Warm reputation', sub: 'Premium pool · gradual ramp' },
{ label: 'Send sequences', sub: 'Distributed across workers' },
{ label: 'Triage replies', sub: 'Unified inbox · classification' },
{ label: 'Score health', sub: 'Placement · complaints · quarantine' },
];
const principles = [
['Mailbox first, not worker first', "A worker's safe send volume is the sum of its mailbox budgets. We don't reward forcing one inbox to do the work of ten."],
['Quarantine before providers do', 'Shared pool thresholds are stricter than Google or Microsoft enforcement, so reputation never gets the chance to slip publicly.'],
['Plain-text by default', "Cold mail is a person talking to a person. HTML is opt-in. We don't add tracking pixels or branded footers without permission."],
['No black boxes', 'Every score, suppression and quarantine has a reason you can read. No "the algorithm decided."'],
['Reputation is shared infrastructure', 'A bad sender in a shared pool taxes everyone. We act on warning bands, not catastrophic bands.'],
['Workers are replaceable', 'Worker state is disposable. We migrate a mailbox between workers without you noticing.'],
];
---
<Layout
title="Product · Warmbly"
description="Warmbly is warmup, sending, inbox, analytics and CRM in one platform. Open source, self-hostable, mailbox-first."
>
<!-- HERO: sky band with the system flow rendered inline -->
<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%);">
<div class="absolute top-0 right-0 w-[440px] opacity-30 pointer-events-none cloud-drift cloud-1" style="mix-blend-mode: screen;">
<Cloud variant={5} opacity={1} />
</div>
<div class="absolute -top-8 left-[-60px] w-[300px] opacity-25 pointer-events-none cloud-drift cloud-2" style="mix-blend-mode: screen;">
<Cloud variant={2} opacity={1} />
</div>
<div class="container-page relative pt-16 md:pt-20 pb-20 md:pb-28">
<div class="text-[11px] uppercase tracking-[0.18em] text-white/55 font-mono mb-4">Product</div>
<h1 class="text-[36px] sm:text-[52px] md:text-[64px] font-semibold tracking-[-0.03em] leading-[1.02] text-white max-w-3xl">
Nine systems.<br/>One platform.
</h1>
<p class="mt-5 text-[15.5px] md:text-[17px] text-white/75 max-w-2xl leading-relaxed">
Each piece feeds the next. Warmup gathers the signal sending throttles on. Inbox classifies the replies analytics scores on. Nothing exists in a vacuum.
</p>
<!-- Flow strip -->
<div class="mt-14 grid grid-cols-2 md:grid-cols-5 gap-y-6">
{flow.map((f, i) => (
<div class="relative flex flex-col items-start md:items-center text-left md:text-center">
<div class="flex items-center w-full">
<div class="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-full bg-white/12 ring-1 ring-white/25 text-[12px] font-mono text-white/90">
{String(i + 1).padStart(2, '0')}
</div>
{i < flow.length - 1 && (
<div class="hidden md:block flex-1 h-px bg-gradient-to-r from-white/30 to-white/5 mx-2"></div>
)}
</div>
<div class="mt-3 text-[13.5px] font-medium text-white">{f.label}</div>
<div class="mt-0.5 text-[11.5px] text-white/55 font-mono">{f.sub}</div>
</div>
))}
</div>
</div>
</section>
<!-- PILLARS: numbered horizontal rows, alternating accent side -->
<section class="py-16 md:py-24">
<div class="container-page">
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">The platform</div>
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading max-w-2xl">
Built so each part makes the next one safer.
</h2>
<div class="mt-14 space-y-px bg-[color:var(--border)] rounded-[14px] overflow-hidden ring-1 ring-[color:var(--border)]" style="box-shadow: var(--shadow-sm);">
{pillars.map((p, i) => (
<a href={p.href} class="group block bg-white hover:bg-[color:var(--sky-1)]/40 transition-colors">
<div class="grid grid-cols-12 gap-4 lg:gap-8 p-6 md:p-8 items-start">
<div class="col-span-12 md:col-span-2 flex md:flex-col items-center md:items-start gap-3 md:gap-2">
<div class="text-[11px] font-mono text-muted-foreground">{p.n}</div>
<div class="inline-flex items-center justify-center w-10 h-10 rounded-[10px] bg-[color:var(--sky-1)] text-[color:var(--sky-6)] ring-1 ring-[color:var(--sky-2)]">
<Icon name={p.icon} size={18} />
</div>
</div>
<div class="col-span-12 md:col-span-4">
<h3 class="text-[18px] font-semibold tracking-[-0.01em] text-heading">{p.title}</h3>
<p class="mt-2 text-[14px] text-foreground/75 leading-relaxed">{p.body}</p>
<div class="mt-3 inline-flex items-center gap-1.5 text-[13px] font-medium text-[color:var(--brand)] group-hover:translate-x-0.5 transition-transform">
Read more <Icon name="arrowRight" size={12} />
</div>
</div>
<div class="col-span-12 md:col-span-6">
<ul class="grid sm:grid-cols-2 gap-x-6 gap-y-2">
{p.bullets.map((b) => (
<li class="flex items-start gap-2 text-[13px] text-foreground/80">
<span class="mt-1.5 w-1 h-1 rounded-full bg-[color:var(--sky-5)] shrink-0"></span>
<span>{b}</span>
</li>
))}
</ul>
</div>
</div>
</a>
))}
</div>
</div>
</section>
<!-- PRINCIPLES: mono-style annotated list -->
<section class="bg-[color:var(--surface-1)] border-y border-[color:var(--border)] py-16 md:py-24">
<div class="container-page">
<div class="grid lg:grid-cols-[1fr_2fr] gap-12 lg:gap-20">
<div>
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Design principles</div>
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
Opinionated where it matters. Quiet everywhere else.
</h2>
</div>
<div class="divide-y divide-[color:var(--border)]">
{principles.map(([title, body], i) => (
<div class="py-5 grid grid-cols-[40px_1fr] gap-4 first:pt-0">
<div class="text-[11px] font-mono text-muted-foreground pt-1">{String(i + 1).padStart(2, '0')}</div>
<div>
<div class="text-[15.5px] font-semibold text-heading tracking-[-0.01em]">{title}</div>
<p class="mt-1.5 text-[14px] text-foreground/75 leading-relaxed">{body}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
<CTA />
</Layout>
-200
View File
@@ -1,200 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Cloud from '../components/Cloud.astro';
import Icon from '../components/Icon.astro';
import CTA from '../components/CTA.astro';
const layers = [
['Edge', 'Cloudflare · WAF · DDoS · Turnstile'],
['Auth', 'OAuth · CAPTCHA · session pinning'],
['App', 'Per-user rate limits · idempotency · webhook HMAC'],
['Encryption', 'AWS KMS · per-user DEKs · AES-256-GCM · brief Redis cache'],
['Storage', 'Postgres (control) · DynamoDB (DEKs) · S3 (payloads)'],
['Execution', 'Worker plane · no SQL · per-machine IP · Kafka topics'],
];
const compliance = {
today: [
'GDPR-aligned data handling',
'DPA signature on request · 48h SLA',
'Subprocessor list published + versioned',
'Per-org audit log of admin actions',
'Configurable data residency (US / EU) on Business',
'TLS 1.2+ for all in-transit traffic',
'Per-user data encryption keys backed by AWS KMS',
],
progress: [
'SOC 2 Type II audit',
'ISO 27001 gap assessment',
'HIPAA covered-entity BAA pilot',
'Customer-managed KMS keys (BYOK)',
'Tenant-scoped IP allowlists for the API',
'Hardware-key SSO enforcement',
],
};
const controls = [
['CAPTCHA at auth', 'Cloudflare Turnstile on login, registration, password reset and confirmation flows.'],
['API rate limiting', 'Per-user category-aware rate limiting backed by Redis and plan limits.'],
['Realtime throttling', 'WebSocket joins, messages and events rate-limited as an anti-abuse boundary.'],
['Event dedupe', 'Tracking events deduped at edge and again in the consumer. Idempotency keys everywhere.'],
['Suppression', 'Bounce, complaint and unsubscribe signals suppress recipients across all campaigns.'],
['Mailbox-sync limits', '100 new emails / 5min burst, 500 / hour ceiling. Exceeded mailboxes are rate-limited.'],
['Admin enforcement', 'Manual ban / unban / rate-limit override with full audit history.'],
['Webhook signatures', 'HMAC-signed webhooks with rotation primitives in the dashboard.'],
];
---
<Layout
title="Security · Warmbly"
description="Envelope encryption with AWS KMS, per-user data keys, audit logs, and a worker plane that never touches your relational database."
>
<!-- HERO with envelope-encryption flow diagram inline -->
<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%);">
<div class="absolute top-0 right-0 w-[420px] opacity-30 pointer-events-none cloud-drift cloud-1" style="mix-blend-mode: screen;">
<Cloud variant={5} opacity={1} />
</div>
<div class="container-page relative pt-16 md:pt-20 pb-16 md:pb-20 grid lg:grid-cols-[1fr_1.1fr] gap-12 items-center">
<div>
<div class="text-[11px] uppercase tracking-[0.18em] text-white/55 font-mono mb-4">Security</div>
<h1 class="text-[36px] sm:text-[52px] md:text-[60px] font-semibold tracking-[-0.03em] leading-[1.02] text-white">
Encrypted at rest.<br/>Boundary-respecting<br/>by design.
</h1>
<p class="mt-5 text-[15.5px] md:text-[17px] text-white/75 max-w-xl leading-relaxed">
Mailbox credentials, refresh tokens and message payloads are envelope-encrypted with per-user data keys. Workers never open a Postgres connection.
</p>
</div>
<!-- Envelope encryption diagram -->
<div>
<svg viewBox="0 0 420 340" class="w-full h-auto">
<defs>
<linearGradient id="secStroke" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#7dd3fc" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#7dd3fc" stop-opacity="0.1"/>
</linearGradient>
</defs>
<!-- KMS -->
<rect x="40" y="40" width="120" height="56" rx="10" fill="rgba(255,255,255,0.08)" stroke="#7dd3fc" stroke-opacity="0.55"/>
<text x="100" y="62" text-anchor="middle" fill="#7dd3fc" font-family="ui-monospace,monospace" font-size="9" letter-spacing="1.5">ROOT KEY</text>
<text x="100" y="80" text-anchor="middle" fill="#fff" font-size="13" font-weight="600">AWS KMS</text>
<!-- DEK -->
<rect x="40" y="142" width="120" height="56" rx="10" fill="rgba(255,255,255,0.08)" stroke="#7dd3fc" stroke-opacity="0.45"/>
<text x="100" y="164" text-anchor="middle" fill="#7dd3fc" font-family="ui-monospace,monospace" font-size="9" letter-spacing="1.5">PER-USER DEK</text>
<text x="100" y="182" text-anchor="middle" fill="#fff" font-size="13" font-weight="600">AES-256-GCM</text>
<!-- Encrypted DEK in DDB -->
<rect x="240" y="142" width="140" height="56" rx="10" fill="rgba(255,255,255,0.04)" stroke="#7dd3fc" stroke-opacity="0.3" stroke-dasharray="4 4"/>
<text x="310" y="164" text-anchor="middle" fill="#7dd3fc" font-family="ui-monospace,monospace" font-size="9" letter-spacing="1.5">ENCRYPTED · STORED</text>
<text x="310" y="182" text-anchor="middle" fill="#fff" font-size="13" font-weight="600">DynamoDB</text>
<!-- Payload -->
<rect x="40" y="244" width="120" height="56" rx="10" fill="rgba(255,255,255,0.08)" stroke="#7dd3fc" stroke-opacity="0.45"/>
<text x="100" y="266" text-anchor="middle" fill="#7dd3fc" font-family="ui-monospace,monospace" font-size="9" letter-spacing="1.5">PAYLOAD</text>
<text x="100" y="284" text-anchor="middle" fill="#fff" font-size="13" font-weight="600">Sealed at rest</text>
<!-- Cache -->
<rect x="240" y="244" width="140" height="56" rx="10" fill="rgba(255,255,255,0.04)" stroke="#7dd3fc" stroke-opacity="0.3" stroke-dasharray="4 4"/>
<text x="310" y="266" text-anchor="middle" fill="#7dd3fc" font-family="ui-monospace,monospace" font-size="9" letter-spacing="1.5">SHORT TTL · CACHED</text>
<text x="310" y="284" text-anchor="middle" fill="#fff" font-size="13" font-weight="600">Redis</text>
<!-- Arrows -->
<line x1="100" y1="96" x2="100" y2="142" stroke="url(#secStroke)" stroke-width="1.5"/>
<line x1="100" y1="198" x2="100" y2="244" stroke="url(#secStroke)" stroke-width="1.5"/>
<line x1="160" y1="170" x2="240" y2="170" stroke="url(#secStroke)" stroke-width="1.5"/>
<line x1="160" y1="272" x2="240" y2="272" stroke="url(#secStroke)" stroke-width="1.5"/>
<!-- moving dot -->
<circle r="3" fill="#fff" opacity="0.95">
<animateMotion dur="6s" repeatCount="indefinite" path="M 100 96 L 100 142 L 100 198 L 100 244 L 160 272 L 240 272"/>
</circle>
</svg>
</div>
</div>
</section>
<!-- LAYER STACK -->
<section class="py-16 md:py-20">
<div class="container-page">
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Layered architecture</div>
<h2 class="text-[26px] md:text-[34px] font-semibold tracking-[-0.025em] text-heading max-w-2xl">Six layers, each with its own boundary.</h2>
<div class="mt-10 space-y-px rounded-[14px] overflow-hidden ring-1 ring-[color:var(--border)]" style="box-shadow: var(--shadow-sm);">
{layers.map(([l, b], i) => (
<div class="grid grid-cols-[64px_1fr] md:grid-cols-[80px_180px_1fr] gap-4 items-center px-5 py-4 bg-white border-t border-[color:var(--border)] first:border-t-0">
<div class="text-[11px] font-mono text-muted-foreground">{String(i + 1).padStart(2, '0')}</div>
<div class="text-[15px] font-semibold text-heading">{l}</div>
<div class="col-span-2 md:col-span-1 text-[13px] font-mono text-foreground/70 break-words">{b}</div>
</div>
))}
</div>
</div>
</section>
<!-- ANTI-ABUSE CONTROLS -->
<section class="bg-[color:var(--surface-1)] border-y border-[color:var(--border)] py-16 md:py-20">
<div class="container-page">
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Anti-abuse</div>
<h2 class="text-[26px] md:text-[34px] font-semibold tracking-[-0.025em] text-heading max-w-2xl">Layered controls, not one brittle gate.</h2>
<div class="mt-10 grid sm:grid-cols-2 lg:grid-cols-4 gap-px bg-[color:var(--border)] rounded-[14px] overflow-hidden ring-1 ring-[color:var(--border)]" style="box-shadow: var(--shadow-sm);">
{controls.map(([k, v]) => (
<div class="bg-white p-5">
<div class="text-[14px] font-semibold text-heading">{k}</div>
<p class="mt-1.5 text-[12.5px] text-foreground/70 leading-relaxed">{v}</p>
</div>
))}
</div>
</div>
</section>
<!-- COMPLIANCE: split today vs in-progress -->
<section class="py-16 md:py-20">
<div class="container-page">
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Compliance</div>
<h2 class="text-[26px] md:text-[34px] font-semibold tracking-[-0.025em] text-heading max-w-2xl">Where we are, honestly.</h2>
<div class="mt-10 grid md:grid-cols-2 gap-5">
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] p-6" style="box-shadow: var(--shadow-sm);">
<div class="flex items-center gap-2 mb-4">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<div class="text-[15.5px] font-semibold text-heading">Today</div>
</div>
<ul class="space-y-2.5">
{compliance.today.map((l) => (
<li class="flex items-start gap-2 text-[13.5px] text-foreground/85">
<Icon name="check" size={13} class="text-emerald-600 mt-1 shrink-0" />
<span>{l}</span>
</li>
))}
</ul>
</div>
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] p-6" style="box-shadow: var(--shadow-sm);">
<div class="flex items-center gap-2 mb-4">
<span class="w-2 h-2 rounded-full bg-amber-500"></span>
<div class="text-[15.5px] font-semibold text-heading">In progress</div>
</div>
<ul class="space-y-2.5">
{compliance.progress.map((l) => (
<li class="flex items-start gap-2 text-[13.5px] text-foreground/85">
<Icon name="clock" size={13} class="text-amber-600 mt-1 shrink-0" />
<span>{l}</span>
</li>
))}
</ul>
</div>
</div>
</div>
</section>
<CTA
title="Need our security package?"
description="Pen test summaries, subprocessor list, DPA, SOC 2 progress letter and architecture overview, on request."
primaryLabel="Request security pack"
primaryHref="/contact/?topic=security"
secondaryLabel="Read our trust page"
secondaryHref="/trust/"
/>
</Layout>
+2 -2
View File
@@ -113,8 +113,8 @@ const subs = [
<p class="mt-4 text-[14.5px] text-foreground/75 leading-relaxed max-w-md">
Per-user data encryption keys. Encrypted DEK in DynamoDB, plaintext briefly cached. Workers never touch Postgres.
</p>
<a href="/security/" class="mt-5 inline-flex items-center gap-1.5 text-[13.5px] font-medium text-[color:var(--brand)] hover:text-[color:var(--brand-strong)]">
Read the full security posture <Icon name="arrowRight" size={12} />
<a href="/contact/?topic=security" class="mt-5 inline-flex items-center gap-1.5 text-[13.5px] font-medium text-[color:var(--brand)] hover:text-[color:var(--brand-strong)]">
Request the security pack <Icon name="arrowRight" size={12} />
</a>
</div>
<div class="space-y-2">
-142
View File
@@ -1,142 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Cloud from '../components/Cloud.astro';
import Icon from '../components/Icon.astro';
import CTA from '../components/CTA.astro';
const tiers = [
{
tier: 'Shared free',
plan: 'Free trial',
body: 'Pooled across free senders. Lower per-mailbox caps and conservative ramps.',
bullets: ['Free warmup pool', 'Strict per-mailbox caps', 'Shared IPs'],
},
{
tier: 'Shared premium',
plan: 'Starter · Grow',
body: 'Pooled across paid senders. Stricter quarantine policy than free.',
bullets: ['Premium warmup pool', 'Default 50 / day cap', 'Shared paid IPs'],
featured: true,
},
{
tier: 'Dedicated',
plan: 'Business · Enterprise',
body: 'A worker process and IP allocated to your organisation. Still mailbox-first.',
bullets: ['Dedicated worker process', 'Dedicated IP(s) · warmed', 'Custom data residency'],
},
];
const props = [
{ icon: 'cpu', title: 'Per-machine identity', body: 'One worker process per machine. Each has its own IP. No shared NAT.' },
{ icon: 'database', title: 'Disposable state', body: 'Workers store nothing locally that cannot be rebuilt from a Kafka replay.' },
{ icon: 'workflow', title: 'Kafka-driven', body: 'Commands and results flow through worker-specific topics for traceability.' },
{ icon: 'shield', title: 'No SQL on workers', body: 'Workers do not open Postgres. They talk to Kafka, KMS, DynamoDB, S3, Redis.' },
{ icon: 'send', title: 'Migrable mailboxes', body: 'A mailbox can be reassigned between workers transparently.' },
{ icon: 'layers', title: 'Concentration caps', body: 'A shared worker should not be a concentration point. Add more before each does more.' },
];
---
<Layout
title="Workers · Warmbly"
description="Distributed worker plane that sends, syncs and tracks. Shared free, shared premium, or dedicated to your organisation."
>
<!-- HERO: split with worker fleet visualization -->
<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%);">
<div class="absolute top-0 right-0 w-[440px] opacity-30 pointer-events-none cloud-drift cloud-1" style="mix-blend-mode: screen;">
<Cloud variant={5} opacity={1} />
</div>
<div class="container-page relative pt-16 md:pt-20 pb-16 md:pb-20 grid lg:grid-cols-[1.1fr_1fr] gap-12 items-center">
<div>
<div class="text-[11px] uppercase tracking-[0.18em] text-white/55 font-mono mb-4">Workers</div>
<h1 class="text-[36px] sm:text-[52px] md:text-[60px] font-semibold tracking-[-0.03em] leading-[1.02] text-white">
Many workers.<br/>Many IPs.<br/>By design.
</h1>
<p class="mt-5 text-[15.5px] md:text-[17px] text-white/75 leading-relaxed max-w-xl">
Cold mail concentrated through one big sender is asking for trouble. Warmbly fans out: one worker per machine, each with its own network identity.
</p>
</div>
<!-- Worker fleet grid mock -->
<div class="relative">
<div class="grid grid-cols-5 gap-2 max-w-md mx-auto">
{Array.from({ length: 20 }).map((_, i) => {
const isYours = [6, 7, 11, 12].includes(i);
const isPremium = [0, 1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 15, 16, 17, 18, 19].includes(i);
return (
<div class={`relative rounded-[8px] p-2.5 ${isYours ? 'bg-white/15 ring-1 ring-white/45' : 'bg-white/[0.06] ring-1 ring-white/15'}`}>
<div class="text-[9.5px] font-mono text-white/75">w-{String(i + 1).padStart(2, '0')}</div>
<div class="mt-1.5 flex gap-0.5">
{Array.from({ length: 6 }).map((_, j) => (
<span class={`w-1 h-1 rounded-full ${isYours ? 'bg-emerald-300' : 'bg-sky-300/60'}`} style={`opacity: ${0.4 + (j % 3) * 0.2}`}></span>
))}
</div>
</div>
);
})}
</div>
<div class="mt-4 flex justify-center gap-4 text-[11px] text-white/70">
<span class="inline-flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-emerald-300"></span> Your dedicated</span>
<span class="inline-flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-sky-300/60"></span> Shared premium</span>
</div>
</div>
</div>
</section>
<!-- TIERS: 3-up with featured middle -->
<section class="py-16 md:py-24">
<div class="container-page">
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Three tiers</div>
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading max-w-2xl">
Three ways your mail moves.
</h2>
<div class="mt-12 grid md:grid-cols-3 gap-5">
{tiers.map((t) => (
<div class={`relative rounded-[14px] bg-white p-6 ${t.featured ? 'ring-1 ring-[color:var(--sky-6)]/35 shadow-[0_30px_60px_-30px_rgba(2,132,199,0.4)]' : 'ring-1 ring-[color:var(--border)]'}`} style={t.featured ? '' : 'box-shadow: var(--shadow-sm);'}>
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-[color:var(--sky-6)] mb-2">{t.plan}</div>
<div class="text-[20px] font-semibold tracking-[-0.02em] text-heading">{t.tier}</div>
<p class="mt-2 text-[13.5px] text-foreground/70 leading-relaxed">{t.body}</p>
<ul class="mt-5 pt-5 border-t border-[color:var(--border)] space-y-2">
{t.bullets.map((b) => (
<li class="flex items-start gap-2 text-[13px] text-foreground/80">
<Icon name="check" size={12} class="text-[color:var(--sky-6)] mt-1 shrink-0" />
<span>{b}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
<!-- WORKER PROPERTIES: dense feature grid -->
<section class="bg-[color:var(--surface-1)] border-y border-[color:var(--border)] py-16 md:py-24">
<div class="container-page">
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Why workers, not one big sender</div>
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading max-w-2xl">
We treat workers as cattle, not pets.
</h2>
<div class="mt-12 grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-[color:var(--border)] rounded-[14px] overflow-hidden ring-1 ring-[color:var(--border)]" style="box-shadow: var(--shadow-sm);">
{props.map((s) => (
<div class="bg-white p-5">
<div class="w-9 h-9 rounded-[8px] bg-[color:var(--sky-1)] text-[color:var(--sky-6)] inline-flex items-center justify-center mb-3">
<Icon name={s.icon} size={16} />
</div>
<div class="text-[14.5px] font-semibold text-heading">{s.title}</div>
<p class="mt-1.5 text-[13px] text-foreground/70 leading-relaxed">{s.body}</p>
</div>
))}
</div>
</div>
</section>
<CTA
title="Want dedicated workers for your org?"
description="Dedicated worker process and IP, plus monthly deliverability review."
primaryLabel="Talk to sales"
primaryHref="/contact/?topic=dedicated"
secondaryLabel="See pricing"
/>
</Layout>