Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export default defineConfig({
],
},
}),
sitemap({ filter: (page) => !page.includes("/og-card") }),
// Both exclusions are pages that carry robots=noindex, and a sitemap that
// submits a noindex URL is a conflict Search Console reports rather than
// ignores. og-card is the screenshot source for the social card;
// download/started only means anything with a ?platform= the sitemap
// cannot carry, so an indexed copy would rank as a thank-you for a
// download the visitor never started.
sitemap({ filter: (page) => !page.includes("/og-card") && !page.includes("/download/started") }),
],
});
20 changes: 19 additions & 1 deletion site/src/components/Footer.astro
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
---
import { Icon } from "astro-icon/components";
import { links } from "../config";
import Mark from "./ui/Mark.astro";
import Wordmark from "./ui/Wordmark.astro";
---
<footer class="border-t border-edge-inner">
<div class="mx-auto max-w-shell px-6 pb-8 pt-12">
<div class="grid grid-cols-2 gap-8 md:grid-cols-[1.7fr_1fr_1fr_1fr]">
<div>
<div class="mb-2.5">
{/* The full lockup, as in the nav — the wordmark alone was the only
place on the site the mark was dropped, so the page opened and closed
on two different marques. h-8 against the wordmark's h-6 holds the
kit's proportion (the mark box runs 4/3 of the type size); scaled
down from the nav's 9/7 because the footer is a sign-off, not a
header. Keep the pair in step. */}
<div class="mb-2.5 flex items-center gap-2">
<Mark class="h-8 w-auto" />
<Wordmark class="h-6 w-auto" />
</div>
<p class="mb-3.5 max-w-[230px] text-meta leading-relaxed text-muted">
Expand Down Expand Up @@ -46,6 +54,16 @@ import Wordmark from "./ui/Wordmark.astro";
<div class="mt-7 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-edge-inner pt-4.5 font-mono text-marker text-faint">
<span>© 2026 antgrid</span>
<span aria-hidden="true">·</span>
{/* The repo, with no ask attached. /security already sends readers here to
check the handshake spec against the client that implements it, so the
link exists on the site either way — it just had nowhere a reader could
find it without being sold something first. The star ask lives on
/download/started, the one page where they already have what they came
for. */}
<a href={links.repo} class="inline-flex items-center gap-1.5 text-muted hover:text-ink">
<Icon name="tabler:brand-github" width="13" height="13" />GitHub
</a>
<span aria-hidden="true">·</span>
<a href={links.company} class="group inline-flex items-center gap-1.5 text-muted hover:text-ink">
<span>a product of</span>
<img src="/logo/radhaai-mark.svg" alt="" width="13" height="13" class="rounded-[2px]" />
Expand Down
114 changes: 98 additions & 16 deletions site/src/components/Nav.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,28 @@ import Button from "./ui/Button.astro";
import Mark from "./ui/Mark.astro";
import Wordmark from "./ui/Wordmark.astro";
import { BETA_FREE } from "../data/pricing";

// One list feeding both the bar and the drawer. Held apart, the two drifted —
// an item added to the desktop bar is invisible to every phone until someone
// notices, and nothing turns red.
//
// Down to the one destination. Features was a scroll link into the middle of one
// of six home sections: on the home page it did what scrolling already does, and
// from anywhere else it threw the reader back into the middle of another page.
// Sign in went with it — the paid path is closed and the beta is free, so it was
// a slot spent on the smallest audience the site has. Both are still in the
// footer, which is where an escape hatch belongs.
const navItems = [{ href: links.pricing, label: "Pricing" }];

// Guards for whatever is added next, not for the list above: an anchor is a
// scroll position rather than a location, and an off-origin href is never this
// page.
const here = Astro.url.pathname.replace(/\/+$/, "") || "/";
const isCurrent = (href: string) =>
href.startsWith("/") && !href.includes("#") && (href.replace(/\/+$/, "") || "/") === here;
---
<header class="sticky top-0 z-50 border-b border-edge-inner bg-page/90 backdrop-blur">
<nav class="mx-auto flex max-w-shell items-center justify-between px-6 py-3.5">
<nav aria-label="Main" class="mx-auto flex max-w-shell items-center justify-between px-6 py-3.5">
{/* Inside the home link, and a span rather than an anchor: nested anchors
are invalid, and the hero's beta pill is already the one that LINKS (to
pricing). This one is a status marker on the lockup, which is why it can
Expand All @@ -25,31 +44,94 @@ import { BETA_FREE } from "../data/pricing";
)}
</a>
<div class="hidden items-center gap-7 md:flex">
<a href={links.features} class="text-note text-muted transition-colors hover:text-ink">Features</a>
<a href={links.getStarted} class="text-note text-muted transition-colors hover:text-ink">Get started</a>
<a href={links.pricing} class="text-note text-muted transition-colors hover:text-ink">Pricing</a>
<a href={links.signIn} class="text-note text-muted transition-colors hover:text-ink">Sign in</a>
<Button href={links.startFree} variant="primary" class="px-4 py-2">Start free</Button>
{navItems.map((item) => (
<a
href={item.href}
aria-current={isCurrent(item.href) ? "page" : undefined}
class:list={[
"text-note transition-colors hover:text-ink",
isCurrent(item.href) ? "text-ink" : "text-muted",
]}
>{item.label}</a>
))}
{/* Mark only, and the one nav item with no label: the octocat is read
faster than the word next to it, and a nav that spells this one out
gives the repo the same weight as Pricing. The negative margin buys a
pointer target around a 19px glyph without moving it in the row —
padding alone would widen the gaps its neighbours are measured on. */}
<a
href={links.repo}
aria-label="antgrid on GitHub"
class="-m-1.5 inline-flex items-center rounded-md p-1.5 text-muted transition-colors hover:text-ink"
>
<Icon name="tabler:brand-github" width="19" height="19" />
</a>
{/* Names the action, not the offer: there is no account to open and no
trial to start, so "Start free" promised a signup the product does not
have. "free" stays on it because this is the highest-attention pixel on
the page and nothing else above the fold says the beta costs nothing.
The free PLAN's CTA keeps "Start free" (src/data/pricing.ts) — on a card
beside a paid tier the offer is the point, and the two labels are
deliberately different. */}
<Button href={links.startFree} variant="primary" class="px-4 py-2">Download free</Button>
</div>
<button id="navToggle" class="text-ink2 md:hidden" aria-label="Open menu" aria-expanded="false">
<Icon name="tabler:menu-2" width="22" height="22" />
<button
id="navToggle"
class="-m-1.5 inline-flex items-center rounded-md p-1.5 text-ink2 md:hidden"
aria-label="Open menu"
aria-controls="navMenu"
aria-expanded="false"
>
<Icon id="navIconOpen" name="tabler:menu-2" width="22" height="22" />
<Icon id="navIconClose" name="tabler:x" width="22" height="22" class="hidden" />
</button>
</nav>
<div id="navMenu" class="hidden border-t border-edge-inner px-6 py-4 md:hidden">
<div class="flex flex-col gap-4">
<a href={links.features} class="text-note text-muted">Features</a>
<a href={links.getStarted} class="text-note text-muted">Get started</a>
<a href={links.pricing} class="text-note text-muted">Pricing</a>
<a href={links.signIn} class="text-note text-muted">Sign in</a>
<Button href={links.startFree} variant="primary">Start free</Button>
{navItems.map((item) => (
<a
href={item.href}
aria-current={isCurrent(item.href) ? "page" : undefined}
class:list={["text-note", isCurrent(item.href) ? "text-ink" : "text-muted"]}
>{item.label}</a>
))}
{/* Labelled here, unlike the desktop bar: a lone icon in a stacked column
of words has nothing to be read against and reads as a stray glyph. */}
<a href={links.repo} class="inline-flex items-center gap-2 text-note text-muted">
<Icon name="tabler:brand-github" width="17" height="17" />GitHub
</a>
<Button href={links.startFree} variant="primary">Download free</Button>
</div>
</div>
</header>
<script>
const btn = document.getElementById("navToggle");
const menu = document.getElementById("navMenu");
btn?.addEventListener("click", () => {
const open = menu?.classList.toggle("hidden") === false;
btn.setAttribute("aria-expanded", String(open));
const iconOpen = document.getElementById("navIconOpen");
const iconClose = document.getElementById("navIconClose");

const setOpen = (open: boolean) => {
menu?.classList.toggle("hidden", !open);
iconOpen?.classList.toggle("hidden", open);
iconClose?.classList.toggle("hidden", !open);
btn?.setAttribute("aria-expanded", String(open));
// The label names the action, not the state, so it has to move with the
// icon — a screen reader on an open menu was being offered "Open menu".
btn?.setAttribute("aria-label", open ? "Close menu" : "Open menu");
};

btn?.addEventListener("click", () => setOpen(menu?.classList.contains("hidden") ?? false));

// Half this menu's items are same-page anchors, which navigate without a
// reload — so nothing tears the drawer down, and it stays parked over the
// section the reader just asked to see.
menu?.addEventListener("click", (e) => {
if ((e.target as HTMLElement).closest("a")) setOpen(false);
});

document.addEventListener("keydown", (e) => {
if (e.key !== "Escape" || menu?.classList.contains("hidden") !== false) return;
setOpen(false);
btn?.focus();
});
</script>
61 changes: 35 additions & 26 deletions site/src/components/sections/ClosingCta.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import { Icon } from "astro-icon/components";
import Eyebrow from "../ui/Eyebrow.astro";
import Button from "../ui/Button.astro";
import { links } from "../../config";
import { links, PLATFORMS, downloadUrlFor } from "../../config";
import { BETA_FREE, FREE_WORKERS, PRO_WORKERS, YEARLY_OFFER_USD } from "../../data/pricing";

const stores = [
{ icon: "tabler:brand-apple", label: "App Store" },
{ icon: "tabler:brand-google-play", label: "Google Play" },
];
// The same OS-matched button the hero leads with, through the same confirmation
// page — a closing CTA that ends on a doc link is a page that never asks for the
// thing it spent nine sections arguing for. The setup guide keeps its place in
// the line below, which is where a reader who wants to read first looks anyway.
---
<section id="download" class="reveal mx-auto max-w-shell px-6 py-20">
<div class="card-lit relative overflow-hidden rounded-xl border border-signaldeep2 bg-[#17171e] px-9 py-14 text-center">
Expand All @@ -26,29 +26,38 @@ const stores = [
)}
</p>
<div class="mb-3.5 flex flex-wrap items-center justify-center gap-3">
<Button href={links.getStarted} variant="primary">Setup guide<Icon name="tabler:arrow-right" width="16" height="16" /></Button>
<Button href={links.pricing} variant="outline">See pricing</Button>
</div>
<p class="mb-8 text-meta text-muted2">No card required · remote control on the free plan</p>

<div class="mx-auto mb-6 h-px max-w-sm bg-edge"></div>
<p class="mb-4 font-mono text-marker uppercase tracking-[0.16em] text-faint">Download the desktop app</p>
<div class="mb-3.5 flex flex-wrap items-center justify-center gap-3">
<Button href={links.downloadMacos} variant="outline"><Icon name="tabler:brand-apple" width="16" height="16" />Download for macOS</Button>
<Button href={links.downloadWindows} variant="outline"><Icon name="tabler:brand-windows" width="16" height="16" />Download for Windows</Button>
<Button href={links.downloadLinux} variant="outline"><Icon name="tabler:brand-open-source" width="16" height="16" />Download for Linux</Button>
</div>
<p class="mb-8 text-meta text-muted2">First time? The <a href={links.getStarted} class="text-signal2 hover:text-signal">setup guide</a> walks you from install to your phone.</p>

<p class="mb-4 font-mono text-marker uppercase tracking-[0.16em] text-faint">App stores coming soon</p>
<div class="flex flex-wrap justify-center gap-2">
{stores.map((s) => (
<span class="inline-flex items-center gap-1.5 rounded-md border border-edge bg-panel px-3 py-2 text-meta text-muted">
<Icon name={s.icon} class="text-muted2" width="16" height="16" />{s.label}
<span class="rounded border border-edge px-1.5 py-px font-mono text-pip text-faint">soon</span>
</span>
{PLATFORMS.map((p) => (
<Button href={downloadUrlFor(p.id)} variant="primary" class={`dl-os dl-${p.os}`}>
<Icon name={p.icon} width="16" height="16" />Download for {p.name}
</Button>
))}
</div>
<p class="mb-2 text-meta text-muted2">No card required · remote control on the free plan</p>
{/* The escape hatch for the OS match, and the reason the three-button row
could go: the hero's "Windows, macOS & Linux" link lands here, so this
band is where a reader on the wrong machine — or downloading for one they
are not sitting at — has to be able to name their own build. Each goes
through the confirmation page like every other download on the site; the
raw artifacts stay one layer down, in the guide. */}
{/* Its own line rather than trailing the build list: the same middot cannot
separate three peers AND then a different sentence — run together, "Linux ·
First time?" reads as a fourth build for one beat, and the whole thing
clumps into three wrapped lines on a phone. */}
<p class="mb-1.5 text-meta text-muted2">
All builds: {PLATFORMS.map((p, i) => (
<>{i > 0 && <span class="text-faint"> · </span>}<a href={downloadUrlFor(p.id)} class="text-signal2 hover:text-signal">{p.name}</a></>
))}
</p>
<p class="mb-8 text-meta text-muted2">
First time? The <a href={links.getStarted} class="text-signal2 hover:text-signal">setup guide</a> walks you from install to your phone.
</p>

{/* A sentence, not two dead store chips. Invites are open today — a page that
closes on "App Store — soon" tells a reader the phone half is unavailable,
which is both wrong and the half the product is for. */}
<p class="text-meta text-muted2">
iOS &amp; Android are in private beta — <a href={links.mobileInvite} class="text-signal2 hover:text-signal">request an invite</a>.
</p>
</div>
</div>
</section>
2 changes: 1 addition & 1 deletion site/src/components/sections/Compat.astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const agents = [
{ name: "Mistral Vibe", icon: "mistralai", color: "#FA520F", w: 20 },
];
---
<section id="agents" class="border-b border-edge-inner bg-well">
<section id="agents" class="reveal border-b border-edge-inner bg-well">
<div class="mx-auto max-w-shell px-6 py-8">
<Eyebrow class="mb-4">The agent you already run</Eyebrow>

Expand Down
Loading