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:
Matthew Meszaros
2026-05-27 04:08:00 +00:00
parent dba5ead5fe
commit 3beeea2ca5
+324 -74
View File
@@ -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 Astros 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 · Q2Q3 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"