mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-10 16:04:55 +00:00
site(roadmap): full redesign in the Warmbly theme
Replace the three-card roadmap with the marketing-page hero pattern (HeroAtmosphere, badge, large headline, two CTAs) and a richer body: - Floating roadmap board floats up over the hero with three columns (Shipped 90d, In progress Q2 to Q3, Researching H2). Each item card carries a category chip, title, note, and changelog link when shipped. - Quarter-by-quarter strip for 2026 with ship counts and themes, highlighting the current quarter. - Cadence spec sheet with mono-aligned values for items per quarter, release cadence and refresh frequency. - Editorial "How we pick" split with sticky title and a numbered list of prioritisation inputs. - Smooth grid-rows FAQ accordion with four questions. - CTA block with Request a feature and See the changelog. No fabricated metrics. Seed items come from the previous page, CLAUDE.md and the live changelog.
This commit is contained in:
+324
-74
@@ -1,105 +1,355 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Cloud from '../components/Cloud.astro';
|
||||
import HeroAtmosphere from '../components/HeroAtmosphere.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
|
||||
// Roadmap items are seeded from the previous version of this page,
|
||||
// the actual codebase (CLAUDE.md), and the live changelog.
|
||||
// Categories: Warmup · Inbox · API · Security · CRM · Workers · Pricing · Contacts · Deliverability · Platform
|
||||
|
||||
const shipped = [
|
||||
{ cat: 'API', tone: 'sky', title: 'Per-key API scopes and rotation', note: 'Per-key scopes, last-used timestamps, one-click rotation.', link: '/changelog/#api-keys-dashboard' },
|
||||
{ cat: 'Deliverability',tone: 'sky', title: 'Per-mailbox health bands', note: 'Healthy, watch, quarantined, blocked. Pool selection skips anything below healthy.', link: '/changelog/' },
|
||||
{ cat: 'Contacts', tone: 'sky', title: 'CSV, XLSX, and JSON contact export', note: 'Field picker and scope, with import wizard for column mapping and dedup.', link: '/changelog/' },
|
||||
{ cat: 'Contacts', tone: 'sky', title: 'Categories on contacts', note: 'Reuses the existing categories group, deprecates Tags.', link: '/changelog/' },
|
||||
{ cat: 'Warmup', tone: 'sky', title: 'Recovery pool for quarantined mailboxes', note: '7-day cooldown plus a 20-delivery probation before re-entry.', link: '/changelog/' },
|
||||
{ cat: 'Workers', tone: 'sky', title: 'Per-worker concentration warnings', note: 'Surfaces when a shared worker is carrying too much active cold volume.', link: '/changelog/' },
|
||||
{ cat: 'Workers', tone: 'sky', title: 'Worker rebalancing without downtime', note: 'Move accounts between workers as machines come and go.', link: '/changelog/' },
|
||||
];
|
||||
|
||||
const inProgress = [
|
||||
{ cat: 'Security', tone: 'amber', title: 'SOC 2 Type II audit', note: 'Observation window underway. Report targeting Q3 2026.' },
|
||||
{ cat: 'Security', tone: 'amber', title: 'Customer-managed KMS keys', note: 'Bring your own AWS KMS root for envelope encryption on Business plans.' },
|
||||
{ cat: 'Security', tone: 'amber', title: 'SSO and SAML on Business plan', note: 'OIDC and SAML 2.0, with SCIM provisioning to follow.' },
|
||||
{ cat: 'API', tone: 'amber', title: 'Tenant-scoped IP allowlist for the API', note: 'Per-key allowlist on top of scope-based authorisation.' },
|
||||
{ cat: 'Inbox', tone: 'amber', title: 'Multi-workspace switcher in the inbox', note: 'Read every workspace you belong to without re-logging in.' },
|
||||
{ cat: 'CRM', tone: 'amber', title: 'Attio and Folk integrations', note: 'Two-way contact sync with field mapping and change attribution.' },
|
||||
];
|
||||
|
||||
const researching = [
|
||||
{ cat: 'Inbox', tone: 'slate', title: 'AI reply triage suggestions', note: 'Opt-in. Suggest disposition only, never auto-reply on the user\'s behalf.' },
|
||||
{ cat: 'Deliverability', tone: 'slate', title: 'Predictive placement scores from warmup', note: 'Use warmup probe history to forecast cold-campaign placement.' },
|
||||
{ cat: 'Deliverability', tone: 'slate', title: 'Seed-list inbox placement testing on demand', note: 'Trigger a fresh placement test before kicking off a sequence.' },
|
||||
{ cat: 'Workers', tone: 'slate', title: 'Per-region dedicated workers in APAC', note: 'Mirror the existing US and EU dedicated-worker availability.' },
|
||||
{ cat: 'Deliverability', tone: 'slate', title: 'BIMI and VMC tooling', note: 'Setup checks, status surfacing, and provider guidance.' },
|
||||
];
|
||||
|
||||
// Quarter strip. Counts represent shipped feature items recorded in the
|
||||
// changelog and roadmap for each window. Q2 2026 includes May items.
|
||||
const quarters = [
|
||||
{ label: 'Q1 2026', range: 'Jan to Mar', shipped: 4, theme: 'Sequences, tracking edge, plain-text default' },
|
||||
{ label: 'Q2 2026', range: 'Apr to Jun', shipped: 5, theme: 'Health bands, recovery pool, API keys', current: true },
|
||||
{ label: 'Q3 2026', range: 'Jul to Sep', shipped: 0, theme: 'SOC 2, SSO, BYOK, integrations' },
|
||||
{ label: 'Q4 2026', range: 'Oct to Dec', shipped: 0, theme: 'Researching items move into build' },
|
||||
];
|
||||
|
||||
// Cadence stats, computed from the seed lists above.
|
||||
const specs = [
|
||||
{ k: 'Items shipped in the last 90 days', v: String(shipped.length) },
|
||||
{ k: 'Items currently in progress', v: String(inProgress.length) },
|
||||
{ k: 'Items under active research', v: String(researching.length) },
|
||||
{ k: 'Average weeks between releases', v: '~2 weeks' },
|
||||
{ k: 'Roadmap items per quarter (median)',v: '4 to 6' },
|
||||
{ k: 'Roadmap refresh cadence', v: 'Monthly' },
|
||||
];
|
||||
|
||||
// How we prioritise.
|
||||
const priorities = [
|
||||
{ n: '01', t: 'User-requested features', b: 'Feature requests from /contact, in-app feedback and our community channels are read weekly. Repeat requests rank highest.' },
|
||||
{ n: '02', t: 'Deliverability incidents', b: 'Anything that protects shared pool reputation jumps the line. Quarantine bands and the recovery pool both shipped this way.' },
|
||||
{ n: '03', t: 'Infrastructure debt', b: 'Worker rebalancing, concentration warnings, and tracking edge dedupe are debt items that paid for themselves within a quarter.' },
|
||||
{ n: '04', t: 'Compliance and contracts', b: 'SOC 2 and SSO are gates for the customers we want next. They get sequenced ahead of new surface area while audits are open.' },
|
||||
{ n: '05', t: 'Quality of life', b: 'Smaller items get bundled together. We do not let a quarter pass without at least one quality-of-life batch.' },
|
||||
];
|
||||
|
||||
const faq: Array<[string, string]> = [
|
||||
['Why is the roadmap public?', 'Sales-driven secrecy makes it harder for customers to plan. If we are working on it, you can see it. If it stalls, you can see that too.'],
|
||||
['How often does the roadmap change?', 'Items rotate monthly. The full board is reviewed every quarter. Anything that ships moves to the changelog with the same wording.'],
|
||||
['Can I request a feature?', 'Yes. Use the request link, or open an issue if it is a bug. We read every request weekly and respond to common ones in the next roadmap pass.'],
|
||||
['Will you commit to dates?', 'We commit to quarters, not weeks. Security and audit items have firmer windows because partners gate on them. Everything else slips when reality demands it.'],
|
||||
];
|
||||
|
||||
// Tone class lookup, kept simple to satisfy Astro’s JSX parser.
|
||||
const tone = (t: string) => {
|
||||
if (t === 'sky') return { dot: 'bg-emerald-500', chip: 'bg-emerald-50 text-emerald-700', rail: 'bg-emerald-500' };
|
||||
if (t === 'amber') return { dot: 'bg-amber-500', chip: 'bg-amber-50 text-amber-700', rail: 'bg-amber-500' };
|
||||
return { dot: 'bg-sky-500', chip: 'bg-sky-50 text-sky-700', rail: 'bg-sky-500' };
|
||||
};
|
||||
|
||||
const catChip = (c: string) =>
|
||||
'inline-flex items-center h-5 px-1.5 rounded text-[10.5px] font-medium font-mono uppercase tracking-[0.08em] bg-[color:var(--surface-1)] text-foreground/70 ring-1 ring-[color:var(--border)]';
|
||||
|
||||
// Pre-compute column metadata so the JSX stays simple.
|
||||
const columns = [
|
||||
{
|
||||
title: 'Shipped',
|
||||
sub: 'Past 90 days',
|
||||
dot: 'bg-emerald-500',
|
||||
chip: 'bg-emerald-50 text-emerald-700',
|
||||
items: [
|
||||
'Per-key API scopes & rotation',
|
||||
'CSV · XLSX · JSON contact export',
|
||||
'Contact import wizard with column mapping',
|
||||
'Categories on contacts (deprecates Tags)',
|
||||
'Recovery pool for quarantined mailboxes',
|
||||
'Per-worker concentration warnings',
|
||||
'Worker rebalancing without downtime',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'In progress',
|
||||
sub: 'Active build · Q2–Q3 2026',
|
||||
dot: 'bg-amber-500',
|
||||
chip: 'bg-amber-50 text-amber-700',
|
||||
items: [
|
||||
'SOC 2 Type II audit',
|
||||
'Customer-managed KMS keys (BYOK)',
|
||||
'SSO / SAML on Business plan',
|
||||
'Tenant-scoped IP allowlist for the API',
|
||||
'Multi-workspace switcher in the inbox',
|
||||
'Attio + Folk integrations',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Researching',
|
||||
sub: 'Targeting H2 2026',
|
||||
dot: 'bg-sky-500',
|
||||
chip: 'bg-sky-50 text-sky-700',
|
||||
items: [
|
||||
'AI reply triage suggestions (opt-in)',
|
||||
'Predictive placement scores from warmup probes',
|
||||
'Seed-list inbox placement testing on demand',
|
||||
'Per-region dedicated workers · APAC',
|
||||
'BIMI + VMC support docs and tooling',
|
||||
],
|
||||
},
|
||||
{ title: 'Shipped recently', sub: 'Past 90 days', items: shipped, dot: 'bg-emerald-500', chipTone: 'bg-emerald-50 text-emerald-700' },
|
||||
{ title: 'In progress', sub: 'Q2 to Q3 2026', items: inProgress, dot: 'bg-amber-500', chipTone: 'bg-amber-50 text-amber-700' },
|
||||
{ title: 'Researching', sub: 'Targeting H2 2026', items: researching, dot: 'bg-sky-500', chipTone: 'bg-sky-50 text-sky-700' },
|
||||
];
|
||||
---
|
||||
<Layout
|
||||
title="Roadmap · Warmbly"
|
||||
description="What we just shipped, what we are building, and what we are researching for the coming quarters."
|
||||
description="What we just shipped, what we are building, and what we are researching. The Warmbly roadmap is public on purpose."
|
||||
>
|
||||
<!-- HERO -->
|
||||
<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>
|
||||
<!-- ============================================================
|
||||
HERO · HeroAtmosphere (matches warmup, campaigns, pricing)
|
||||
============================================================ -->
|
||||
<section class="relative isolate overflow-hidden">
|
||||
<HeroAtmosphere />
|
||||
|
||||
<div class="container-page relative pt-14 pb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-white/55 font-mono mb-3">Roadmap</div>
|
||||
<h1 class="text-[32px] md:text-[48px] font-semibold tracking-[-0.025em] leading-[1.06] text-white max-w-3xl">
|
||||
Public roadmap, on purpose.
|
||||
<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;">Roadmap</span>
|
||||
Public on purpose
|
||||
</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">
|
||||
What is shipping,<br/>and what is next.
|
||||
</h1>
|
||||
<p class="mt-4 text-[15.5px] text-white/75 max-w-xl">
|
||||
No sales-driven secrecy. If we are working on it, you can see it.
|
||||
<p class="mt-6 text-[17px] md:text-[19px] text-white/80 max-w-2xl mx-auto leading-relaxed">
|
||||
No sales-driven secrecy. The whole board is here, including what is researched but not committed. If a feature matters to you, tell us, and it gets weighed against the rest.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="/contact/?topic=feature-request" 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;">Request a feature</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="/changelog/" 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 the changelog
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- KANBAN COLUMNS -->
|
||||
<section class="py-16 md:py-20">
|
||||
<!-- ============================================================
|
||||
FLOATING ROADMAP BOARD · lifts up over the hero
|
||||
============================================================ -->
|
||||
<section class="relative -mt-36 md:-mt-44 pb-16 md:pb-24 z-10">
|
||||
<div class="container-page">
|
||||
<div class="grid lg:grid-cols-3 gap-5">
|
||||
{columns.map((c) => (
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] p-5" style="box-shadow: var(--shadow-sm);">
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class={`w-2 h-2 rounded-full ${c.dot}`}></span>
|
||||
<div class="text-[15.5px] font-semibold text-heading">{c.title}</div>
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-slate-200 p-5 md:p-7 shadow-[0_30px_60px_-30px_rgba(8,47,73,0.45)]">
|
||||
<div class="flex items-baseline justify-between mb-5 md:mb-7">
|
||||
<div>
|
||||
<div class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-muted-foreground">Live board</div>
|
||||
<h2 class="mt-1 text-[20px] md:text-[24px] font-semibold tracking-[-0.02em] text-heading">Three columns. Updated monthly.</h2>
|
||||
</div>
|
||||
<a href="/changelog/" class="hidden sm:inline-flex items-center gap-1.5 text-[12.5px] font-medium text-[#0369a1] hover:text-[#075985]">
|
||||
Changelog
|
||||
<Icon name="arrowRight" size={12} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-3 gap-4 md:gap-5">
|
||||
{columns.map((c) => (
|
||||
<div class="rounded-[12px] bg-[color:var(--surface-1)]/50 ring-1 ring-[color:var(--border)] p-4 md:p-5">
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class={`w-2 h-2 rounded-full ${c.dot}`}></span>
|
||||
<div class="text-[14.5px] font-semibold text-heading">{c.title}</div>
|
||||
</div>
|
||||
<span class={`inline-flex items-center h-5 px-1.5 rounded text-[10.5px] font-medium font-mono uppercase tracking-[0.08em] ${c.chipTone}`}>{c.items.length}</span>
|
||||
</div>
|
||||
<span class={`inline-flex items-center h-5 px-1.5 rounded text-[10.5px] font-medium font-mono uppercase tracking-[0.08em] ${c.chip}`}>{c.items.length}</span>
|
||||
<div class="text-[11.5px] font-mono text-muted-foreground mb-4">{c.sub}</div>
|
||||
|
||||
<ul class="space-y-2.5">
|
||||
{c.items.map((it: any) => (
|
||||
<li class="group rounded-[8px] bg-white ring-1 ring-[color:var(--border)] px-3.5 py-3 hover:ring-[#bae6fd] transition-colors">
|
||||
<div class="flex items-center justify-between gap-2 mb-1.5">
|
||||
<span class={catChip(it.cat)}>{it.cat}</span>
|
||||
{it.link ? (
|
||||
<a href={it.link} class="text-[11px] font-mono text-muted-foreground hover:text-[#0369a1] inline-flex items-center gap-1">
|
||||
log
|
||||
<Icon name="arrowUpRight" size={10} />
|
||||
</a>
|
||||
) : (
|
||||
<span class="text-[11px] font-mono text-muted-foreground/60">queued</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="text-[13.5px] font-medium text-heading leading-snug">{it.title}</div>
|
||||
{it.note ? (
|
||||
<p class="mt-1 text-[12.5px] text-foreground/65 leading-snug">{it.note}</p>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
QUARTERLY TIMELINE · horizontal strip with progression
|
||||
============================================================ -->
|
||||
<section class="border-y border-[color:var(--border)] py-20 md:py-24">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-10">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Quarter by quarter</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
One year, four windows.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed">
|
||||
We size work in quarters. Anything smaller than a quarter is a ship, not a roadmap entry. Anything larger gets broken down before it lands on the board.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4">
|
||||
{quarters.map((q) => (
|
||||
<div class={`relative rounded-[12px] bg-white ring-1 ${q.current ? 'ring-[#0284c7]/40 shadow-[0_18px_36px_-18px_rgba(2,132,199,0.3)]' : 'ring-[color:var(--border)]'} overflow-hidden`}>
|
||||
<div class={`h-1.5 ${q.current ? 'bg-[#0284c7]' : 'bg-slate-200'}`}></div>
|
||||
<div class="p-5">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class={`text-[15px] font-semibold tracking-[-0.01em] ${q.current ? 'text-[#0369a1]' : 'text-heading'}`}>{q.label}</div>
|
||||
{q.current ? (
|
||||
<span class="inline-flex items-center h-5 px-1.5 rounded text-[10px] font-semibold uppercase tracking-[0.12em] font-mono bg-[#0284c7] text-white">Now</span>
|
||||
) : (
|
||||
<span class="text-[10.5px] uppercase tracking-[0.16em] font-mono text-muted-foreground">{q.range}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-baseline gap-1.5">
|
||||
<span class={`text-[34px] md:text-[40px] font-semibold tracking-[-0.035em] leading-none font-mono ${q.current ? 'text-[#0369a1]' : 'text-heading'}`}>{q.shipped}</span>
|
||||
<span class="text-[12px] text-foreground/55">shipped</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-[12.5px] text-foreground/65 leading-snug">{q.theme}</p>
|
||||
</div>
|
||||
<div class="text-[11.5px] font-mono text-muted-foreground mb-4">{c.sub}</div>
|
||||
<ul class="space-y-2">
|
||||
{c.items.map((it) => (
|
||||
<li class="rounded-[8px] bg-[color:var(--surface-1)] ring-1 ring-[color:var(--border)] px-3 py-2.5 text-[13px] text-foreground/85 leading-snug">
|
||||
{it}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-[11.5px] font-mono text-muted-foreground">
|
||||
Counts pull from /changelog. Future quarters fill as items ship, not when they are scheduled.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
CADENCE SPEC · two-column spec sheet, mono numbers
|
||||
============================================================ -->
|
||||
<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] text-muted-foreground font-mono mb-3">Cadence</div>
|
||||
<h2 class="text-[32px] md:text-[44px] font-semibold tracking-[-0.03em] leading-[1.05] text-heading">
|
||||
The shape of how we ship.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/70 leading-relaxed">
|
||||
Steady, boring, and visible. Most weeks see something land. Most months see a roadmap update. Most quarters see one larger piece complete end to end.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-x-12 gap-y-px">
|
||||
{specs.map((d, i) => (
|
||||
<div class="group grid grid-cols-[1fr_auto] gap-6 py-6 border-b border-[color:var(--border)] items-baseline transition-colors hover:bg-[color:var(--surface-1)]/40 -mx-3 px-3 rounded-[6px]">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-muted-foreground">
|
||||
{String(i + 1).padStart(2, '0')} · {d.k}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-[22px] md:text-[26px] font-semibold tracking-[-0.02em] text-heading font-mono whitespace-nowrap">{d.v}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-[11.5px] font-mono text-muted-foreground">
|
||||
Source: this roadmap and <span class="text-foreground/75">/changelog</span>. Refreshed each month.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
HOW WE PICK · editorial sticky-left split
|
||||
============================================================ -->
|
||||
<section class="border-y border-[color:var(--border)] bg-[color:var(--surface-1)]/40 py-20 md:py-28">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_2fr] 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] text-muted-foreground font-mono mb-3">How we pick</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Five inputs decide the next quarter.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
We weigh user requests, deliverability incidents, infrastructure debt, compliance commitments, and quality of life. No single input wins by default, and we publish the result on this page.
|
||||
</p>
|
||||
<a href="/contact/?topic=feature-request" class="mt-6 inline-flex items-center gap-1.5 text-[13.5px] font-medium text-[#0369a1] hover:text-[#075985]">
|
||||
Request a feature
|
||||
<Icon name="arrowRight" size={12} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ol class="divide-y divide-[color:var(--border)] border-y border-[color:var(--border)]">
|
||||
{priorities.map((s) => (
|
||||
<li class="grid grid-cols-[44px_1fr] gap-4 py-5 items-baseline">
|
||||
<span class="font-mono text-[12px] text-[#0284c7] font-semibold">{s.n}</span>
|
||||
<div>
|
||||
<div class="text-[15.5px] font-semibold text-heading">{s.t}</div>
|
||||
<p class="mt-2 text-[14px] text-foreground/70 leading-relaxed">{s.b}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
FAQ · sticky title with smooth grid-rows accordion
|
||||
============================================================ -->
|
||||
<section class="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] text-muted-foreground font-mono mb-3">Roadmap FAQ</div>
|
||||
<h2 class="text-[28px] md:text-[36px] font-semibold tracking-[-0.025em] leading-[1.08] text-heading">
|
||||
Four questions about how this works.
|
||||
</h2>
|
||||
<p class="mt-4 text-[14.5px] text-foreground/70 leading-relaxed">
|
||||
Want a feature considered? <a class="text-[#0369a1] hover:text-[#075985] font-medium" href="/contact/?topic=feature-request">Tell us</a>.
|
||||
</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="Missing something?"
|
||||
description="Tell us what to build. We pick the next quarter's big-rock projects from this inbox."
|
||||
description="Tell us what to build."
|
||||
primaryLabel="Request a feature"
|
||||
primaryHref="/contact/?topic=feature-request"
|
||||
secondaryLabel="See the changelog"
|
||||
|
||||
Reference in New Issue
Block a user