diff --git a/CLAUDE.md b/CLAUDE.md
index 2ec4045a..2b373e0a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -35,6 +35,17 @@ Applies to all user-facing writing on the website (docs, marketing, blog). Inter
To add or update icons, see `frontend/packages/icons/CLAUDE.md`.
+## Product Marks
+
+The product wordmarks in `src/images/products/*-logo.svg` are white-on-transparent: the alpha channel is the mark, so the file carries no color of its own. They are always painted as a masked element via `wordmarkMaskStyle` (`src/lib/product-accent.ts`), never as an ``.
+
+- **Never render a naked mark.** A wordmark always rides inside its tile: `rounded-[34.375%]` at the SVG's inset-ring geometry, filled ink or the product accent. A bare mark dropped on a light surface — an `` with `brightness-0`, an invert filter, or a raw mask on `paper` — is the failure mode this rule exists to prevent. It reads as a black outline box and matches nothing else on the site.
+- **The mark inside the tile is white**, never cream. Cream is for text and fills inside ink panels; on a mark it reads as a dirty off-white against the pure-white Rivet badge in the header.
+- **Ink tile** (`bg-ink`) is the default: product-page hero lockups (`ProductLockup`), `/docs` cards (`DocsLanding`), inline product mentions, and any product list on a light surface.
+- **Accent tile** (`ProductBadge` in `src/components/ProductBar.tsx`) is the colored variant: header dropdowns, mobile sheet, product bar label, talk-to-an-engineer. Every place that lists products renders `ProductBadge` rather than hand-rolling the geometry, so the lockup cannot drift.
+- **One sanctioned tile-less case:** on the homepage stack plates (`StackSection`) the mark is painted white directly on the product-accent field. The plate *is* the background — it is not a naked mark.
+- **Rivet Cloud has no wordmark.** It takes the `faCloud` glyph — white inside an accent tile where `ProductBadge` supplies one, otherwise bare in ink, sized down so it sits level with the neighboring tiles rather than outweighing them (its own hero, and the careers grid). Do not invent a wordmark for it.
+
## Registry Integration Icons
Integration entries in `website/src/data/registry.ts` display icons on the registry page and detail pages. Each entry uses either an `image` (SVG file path) or an `icon` (Font Awesome icon).
@@ -103,7 +114,7 @@ Import from `@rivet-gg/icons`. The full Font Awesome Pro library is available. C
## Theme
-- Marketing pages and docs are light: cool porcelain (`paper`, `#EFEFEF`) with a `paper-deep` radial pooling bottom-left (`.depth-wash`) and a fine grain (`.paper-grain`). Warmth comes from warm-black `ink` text, classical imagery, and oil-paint textures, never from synthetic color gradients. Do not use the cream `mat` token as a surface/background design element on light surfaces (panels, dropdowns, badges, plate frames); use `paper`/`paper-mid`/`white` or `ink`-tint neutrals instead. `cream` stays valid only as the off-white text/fill inside dark `ink` panels. Docs paint the same porcelain field, render prose via `Prose surface="paper"`, and use `pine` for the active sidebar/TOC selected state; only the Learn section keeps a dark shell, and no other page may introduce a dark base.
+- Marketing pages and docs are light: cool porcelain (`paper`, `#EFEFEF`) with a `paper-deep` radial pooling bottom-left (`.depth-wash`) and a fine grain (`.paper-grain`). Warmth comes from warm-black `ink` text, classical imagery, and oil-paint textures, never from synthetic color gradients. Do not use the cream `mat` token as a surface/background design element on light surfaces (panels, dropdowns, badges, plate frames); use `paper`/`paper-mid`/`white` or `ink`-tint neutrals instead. `cream` stays valid only as the off-white text/fill inside dark `ink` panels, and never on a product mark — those are white (see Product Marks). Docs paint the same porcelain field, render prose via `Prose surface="paper"`, and use `pine` for the active sidebar/TOC selected state; only the Learn section keeps a dark shell, and no other page may introduce a dark base.
- Dark `ink` panels (`editorial/InkPanel`) are reserved for code, terminal, screenshot, and data moments. Code and data plates stay flat ink; the oil-texture backdrop (`textureSrc`) is for editorial moments only (CTA colophon, 404).
- Orange is the spark: at most one `accent`/ember CTA per page. White text sits only on `accent-deep` (`#D63E00`) or `ink` fills, never on `accent`. Pine (`#2E4034`) is the structural color for links, eyebrows, diagrams, and selected states; sage (`#93A286`) replaces it inside ink panels.
- No drop shadows on marketing cards or imagery; depth comes from `border-ink/10..25` hairlines, `bg-white/55` card fills, and `paper-mid`/`paper-deep` layering (inside ink panels: `border-cream/10..15`). Shadows stay acceptable on functional overlays (dropdowns, tooltips, modals) and the header's glass inset highlight.
diff --git a/public/images/agent-os/agentos-wordmark.svg b/public/images/agent-os/agentos-wordmark.svg
new file mode 100644
index 00000000..c51ce8b4
--- /dev/null
+++ b/public/images/agent-os/agentos-wordmark.svg
@@ -0,0 +1,73 @@
+
+
+
diff --git a/public/images/architecture/javascript-executor-readiness-state-dark.svg b/public/images/architecture/javascript-executor-readiness-state-dark.svg
new file mode 100644
index 00000000..c9c0e9ac
--- /dev/null
+++ b/public/images/architecture/javascript-executor-readiness-state-dark.svg
@@ -0,0 +1,146 @@
+
+
diff --git a/public/images/architecture/javascript-executor-wakeup-sequence-dark.svg b/public/images/architecture/javascript-executor-wakeup-sequence-dark.svg
new file mode 100644
index 00000000..fad63f61
--- /dev/null
+++ b/public/images/architecture/javascript-executor-wakeup-sequence-dark.svg
@@ -0,0 +1,103 @@
+
+
diff --git a/public/images/brand/actors-mark.svg b/public/images/brand/actors-mark.svg
new file mode 100644
index 00000000..3a94d564
--- /dev/null
+++ b/public/images/brand/actors-mark.svg
@@ -0,0 +1,21 @@
+
diff --git a/public/images/brand/agentos-mark.svg b/public/images/brand/agentos-mark.svg
new file mode 100644
index 00000000..d5e05f02
--- /dev/null
+++ b/public/images/brand/agentos-mark.svg
@@ -0,0 +1,13 @@
+
diff --git a/public/images/brand/cloud-mark.svg b/public/images/brand/cloud-mark.svg
new file mode 100644
index 00000000..a3637185
--- /dev/null
+++ b/public/images/brand/cloud-mark.svg
@@ -0,0 +1,6 @@
+
diff --git a/public/images/brand/dynamic-apps-mark.svg b/public/images/brand/dynamic-apps-mark.svg
new file mode 100644
index 00000000..c89025cc
--- /dev/null
+++ b/public/images/brand/dynamic-apps-mark.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/images/brand/workflows-mark.svg b/public/images/brand/workflows-mark.svg
new file mode 100644
index 00000000..8f4f7b22
--- /dev/null
+++ b/public/images/brand/workflows-mark.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/images/world-map.svg b/public/images/world-map.svg
new file mode 100644
index 00000000..453ad383
--- /dev/null
+++ b/public/images/world-map.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/public/rivet-brand-kit.zip b/public/rivet-brand-kit.zip
new file mode 100644
index 00000000..820145d5
Binary files /dev/null and b/public/rivet-brand-kit.zip differ
diff --git a/redirects.mjs b/redirects.mjs
index 00c41296..234315c0 100644
--- a/redirects.mjs
+++ b/redirects.mjs
@@ -136,6 +136,9 @@ const explicitRedirects = {
// Astro's static redirect, which serves HTTP 200 with a meta refresh.
'/pricing': '/cloud/',
'/meme/wired-in': '/',
+ // The Open-Source Friends page was retired. It had no equivalent and was
+ // part of a reciprocal-linking network, so preserve inbound links to home.
+ '/oss-friends': '/',
// Deployment folded into the shared Self-Host section.
'/agentos/docs/deployment': '/agentos/self-host/',
// Air-gapped deployment is a self-hosting topic, not an actors cookbook.
diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx
index 6b084de5..6ad153c1 100644
--- a/src/components/Footer.jsx
+++ b/src/components/Footer.jsx
@@ -38,8 +38,7 @@ const footer = {
],
resources: [
{ name: "Blog", href: "/blog" },
- { name: "Open-Source Friends", href: "/oss-friends" },
- { name: "Press Kit", href: "https://releases.rivet.dev/press-kit.zip" },
+ { name: "Brand", href: "/brand" },
],
compare: [
{
diff --git a/src/components/ProductBar.tsx b/src/components/ProductBar.tsx
index f1a218e2..2bf1134d 100644
--- a/src/components/ProductBar.tsx
+++ b/src/components/ProductBar.tsx
@@ -19,14 +19,14 @@ import { canonicalizeInternalHref } from "@/lib/internalHref";
* The wordmarks are white-on-transparent SVGs, so they are painted as a masked
* element rather than an ``: the SVG supplies the silhouette and a
* Tailwind background class supplies the hue. Glyph fallbacks take the same
- * accent as a text color. `tone="cream"` is for dark grounds: the accent tile
+ * accent as a text color. `tone="white"` is for dark grounds: the accent tile
* inside `ProductBadge` and the dark Learn shell.
*/
export function ProductMark({
product,
className,
tone = "accent",
-}: { product: Product; className?: string; tone?: "accent" | "cream" }) {
+}: { product: Product; className?: string; tone?: "accent" | "white" }) {
const accent = productAccent(product.id);
const logo = productLogos[product.id];
if (logo) {
@@ -36,7 +36,7 @@ export function ProductMark({
style={wordmarkMaskStyle(logo.src)}
className={cn(
"inline-block shrink-0",
- tone === "cream" ? "bg-cream" : (accent?.fill ?? "bg-ink"),
+ tone === "white" ? "bg-white" : (accent?.fill ?? "bg-ink"),
className,
)}
/>
@@ -49,7 +49,7 @@ export function ProductMark({
aria-hidden="true"
className={cn(
"shrink-0",
- tone === "cream" ? "text-cream" : (accent?.text ?? "text-ink-soft"),
+ tone === "white" ? "text-white" : (accent?.text ?? "text-ink-soft"),
className,
)}
/>
@@ -60,7 +60,7 @@ export function ProductMark({
/**
* The colored product lockup: a solid tile in the product accent with the
- * cream mark spanning it. The tile radius (34.375%) and the SVG's inset ring
+ * white mark spanning it. The tile radius (34.375%) and the SVG's inset ring
* reproduce the Rivet badge geometry, so this is the same treatment as the
* product bar on each product page. Size comes from the caller (`size-N`).
*
@@ -82,39 +82,54 @@ export function ProductBadge({
className,
)}
>
-
+
);
}
-/**
- * Which product the subnav belongs to.
- *
- * Not a switcher: `Products` in the top row is the one place products are
- * chosen, so a second control here was the same menu twice, one line apart.
- */
+/** Which product the subnav belongs to, linking back to its overview. */
function ProductLabel({
current,
label,
+ active = false,
}: {
current?: Product;
/** Used by docs sections that are not a product vertical (integrations). */
label?: string;
+ active?: boolean;
}) {
// Same treatment as the Products dropdown: the color is the tile behind the
// mark, and the mark and name are left uncolored.
- return (
-
+ const content = (
+ <>
{current && }
{current?.name ?? label}
-
+ >
+ );
+
+ if (!current) {
+ return {content};
+ }
+
+ const accent = productAccent(current.id);
+ return (
+
+ {content}
+
);
}
/**
- * Second header row inside a product vertical: which product you are in, a
- * switcher to jump to the same tab of another product, and that product's three
- * docs tabs.
+ * Second header row inside a product vertical: the product overview link and
+ * that product's remaining tabs.
*/
export function ProductBar({
initialPathname = "",
@@ -144,7 +159,7 @@ export function ProductBar({
// band fill, no underline — the active tab is ink, the rest ink-faint,
// and the product accent lives only in the label tile.
-
+
{product && visibleTabs(product).map((tab) => {
const accent = productAccent(product.id);
diff --git a/src/components/ScrollObserver.tsx b/src/components/ScrollObserver.tsx
index d0634988..5accaba8 100644
--- a/src/components/ScrollObserver.tsx
+++ b/src/components/ScrollObserver.tsx
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
const REVEAL_SELECTOR = '[data-site-reveal], [data-site-reveal-group], [data-site-hero-reveal]';
const VISIBLE_ATTRIBUTE = 'data-site-reveal-visible';
+const PENDING_ATTRIBUTE = 'data-site-reveal-pending';
const OBSERVER_READY_ATTRIBUTE = 'data-site-motion-observer-ready';
const findHydratingIsland = (element: HTMLElement) => {
@@ -40,6 +41,10 @@ export function ScrollObserver() {
useEffect(() => {
let observer: IntersectionObserver | undefined;
const pendingHydrationListeners = new Map();
+ const reveal = (element: HTMLElement) => {
+ element.removeAttribute(PENDING_ATTRIBUTE);
+ element.setAttribute(VISIBLE_ATTRIBUTE, '');
+ };
const clearPendingHydrationListeners = () => {
pendingHydrationListeners.forEach((listener, island) => {
@@ -49,9 +54,7 @@ export function ScrollObserver() {
};
const revealAll = (elements: HTMLElement[]) => {
- elements.forEach((element) => {
- element.setAttribute(VISIBLE_ATTRIBUTE, '');
- });
+ elements.forEach(reveal);
};
const observe = () => {
@@ -93,6 +96,21 @@ export function ScrollObserver() {
});
});
+ // Content that is already on screen should be settled on first paint.
+ // Only off-screen blocks enter the pending state that CSS animates out
+ // when the observer reveals them later.
+ observableElements.forEach((element) => {
+ if (element.hasAttribute(VISIBLE_ATTRIBUTE)) {
+ element.removeAttribute(PENDING_ATTRIBUTE);
+ return;
+ }
+
+ const bounds = element.getBoundingClientRect();
+ const isInViewport = bounds.bottom >= 0 && bounds.top <= window.innerHeight;
+ if (isInViewport) reveal(element);
+ else element.setAttribute(PENDING_ATTRIBUTE, '');
+ });
+
if (!('IntersectionObserver' in window)) {
revealAll(observableElements);
return;
@@ -102,7 +120,7 @@ export function ScrollObserver() {
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
- entry.target.setAttribute(VISIBLE_ATTRIBUTE, '');
+ reveal(entry.target as HTMLElement);
observer?.unobserve(entry.target);
});
},
diff --git a/src/components/docs/DocsLanding.tsx b/src/components/docs/DocsLanding.tsx
index 7e09dcd4..fff55592 100644
--- a/src/components/docs/DocsLanding.tsx
+++ b/src/components/docs/DocsLanding.tsx
@@ -46,13 +46,15 @@ function HeroTitle({
if (logo === "actors") {
return (
- {/* The mark is a solid-white SVG, so it is masked and refilled with
- ink to read on the porcelain field. */}
+ className="flex size-9 shrink-0 items-center justify-center rounded-[34.375%] bg-ink md:size-10"
+ >
+
+
{title}
@@ -88,14 +90,14 @@ function LandingCard({ item }: { item: DocsLandingItem }) {
{item.logoSrc ? (
// The same badge treatment as the product pages' lockups: an ink
- // squircle tile with the white-on-transparent mark masked in cream.
+ // squircle tile with the mark masked in white.
) : (
diff --git a/src/components/marketing/BrandKit.tsx b/src/components/marketing/BrandKit.tsx
new file mode 100644
index 00000000..6c9ed9c7
--- /dev/null
+++ b/src/components/marketing/BrandKit.tsx
@@ -0,0 +1,313 @@
+"use client";
+
+// Interactive pieces of the brand page. The surrounding headings, section
+// rules, and copy live in `src/pages/brand.astro`; only the bits that need a
+// browser (copy-to-clipboard, the canonical `ProductBadge`) are hydrated here.
+import { toast } from "@rivet-gg/components";
+import { Icon, faCopy, faDownload } from "@rivet-gg/icons";
+import { products } from "@/sitemap/products";
+import { productAccent } from "@/lib/product-accent";
+import { ProductBadge } from "@/components/ProductBar";
+
+// The corporate marks. White-on-transparent badge + wordmark SVGs; imported so
+// Vite resolves the hashed URLs (`.src`) the same way the header and footer do.
+import iconTextBlack from "@/images/rivet-logos/icon-text-black.svg";
+import iconTextWhite from "@/images/rivet-logos/icon-text-white.svg";
+import iconMark from "@/images/rivet-logos/icon-white.svg";
+
+async function copyText(text: string, label: string) {
+ try {
+ await navigator.clipboard.writeText(text);
+ toast.success(`Copied ${label}`);
+ } catch {
+ toast.error("Couldn’t copy to clipboard.");
+ }
+}
+
+async function copySvg(src: string, name: string) {
+ try {
+ const svg = await (await fetch(src)).text();
+ await navigator.clipboard.writeText(svg);
+ toast.success(`Copied ${name} as SVG`);
+ } catch {
+ toast.error("Couldn’t copy the SVG.");
+ }
+}
+
+function hexToRgb(hex: string): string {
+ const [r, g, b] = [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16));
+ return `rgb(${r}, ${g}, ${b})`;
+}
+
+const ASSET_BUTTON_CLASS =
+ "inline-flex items-center gap-1.5 rounded-md border border-ink/15 bg-white/55 px-2.5 py-1.5 text-xs font-medium text-ink-soft transition-colors hover:border-ink/30 hover:bg-white hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-pine focus-visible:ring-offset-2 focus-visible:ring-offset-paper";
+
+// --- Logos -----------------------------------------------------------------
+
+interface LogoAsset {
+ title: string;
+ usage: string;
+ src: string;
+ file: string;
+ /** Which ground the preview sits on. */
+ ground: "light" | "dark";
+ /** Height utility for the preview image. */
+ imgClass: string;
+}
+
+const LOGOS: LogoAsset[] = [
+ {
+ title: "Wordmark",
+ usage: "Primary lockup for light backgrounds",
+ src: iconTextBlack.src,
+ file: "rivet-wordmark-dark.svg",
+ ground: "light",
+ imgClass: "h-9 w-auto md:h-10",
+ },
+ {
+ title: "Wordmark",
+ usage: "Primary lockup for dark backgrounds",
+ src: iconTextWhite.src,
+ file: "rivet-wordmark-light.svg",
+ ground: "dark",
+ imgClass: "h-9 w-auto md:h-10",
+ },
+ {
+ title: "Icon",
+ usage: "The mark alone, for constrained space",
+ src: iconMark.src,
+ file: "rivet-icon.svg",
+ ground: "light",
+ imgClass: "h-16 w-16",
+ },
+];
+
+// agentOS is the one product with a full script wordmark (beyond its badge),
+// so it rides in the product-marks section rather than the Rivet logo gallery.
+const AGENTOS_WORDMARK_SRC = "/images/agent-os/agentos-wordmark.svg";
+
+export function LogoGallery() {
+ return (
+
+ );
+}
+
+// --- Product marks ---------------------------------------------------------
+
+// Display order matches the sitemap (Rivet Cloud is hidden from the switcher
+// but still carried, so it is pulled in explicitly).
+const PRODUCT_ORDER = ["actors", "agentos", "workflows", "dynamic-apps", "cloud"];
+
+// Standalone, accent-colored versions of each badge (tile baked in), generated
+// under public/images/brand/ so the marks are downloadable exactly as shown.
+const markSrc = (id: string) => `/images/brand/${id}-mark.svg`;
+
+export function ProductMarkGallery() {
+ const ordered = PRODUCT_ORDER.map((id) => products.find((product) => product.id === id)).filter(
+ (product): product is NonNullable => Boolean(product),
+ );
+
+ return (
+
+ Manrope sets everything on the marketing site and docs. JetBrains Mono is reserved for
+ code, terminal, keyboard keys, and tabular numbers.
+
+
+
+
+
+
Manrope
+
Variable · 200–800
+
+
+ Ag
+
+
+ Infrastructure for the agentic era.
+
+
+ {
+ specimenWeights.map((weight) => (
+
+
{weight.label}
+
Aa Bb Cc 0123
+
+ ))
+ }
+
+
Headings, body, and labels.
+
+
+
+
JetBrains Mono
+
Variable · 100–800
+
+
Ag
+
$ npx rivet deploy --prod
+
+
+
Regular · 400
+
Aa Bb Cc 0123
+
+
+
Medium · 500
+
Aa Bb Cc 0123
+
+
+
Code, terminal, keys, and tabular numbers only.
+
+
+
+
+
+
+
+
+
+
+
+
About Rivet
+
+
+ Rivet is composable, open-source infrastructure for the agentic era. At its core is
+ Actors — a primitive for stateful workloads that gives every agent a durable,
+ addressable process to live in, with SQLite-per-actor, WebSockets, and hibernation.
+
+
+ That foundation carries agentOS,
+ Workflows, and
+ Dynamic Apps, and
+ Rivet Cloud runs the whole stack for you. Most of
+ Rivet is Apache 2.0, self-hostable, and can be fully air-gapped in your VPC.
+
+
+ Orchestrate agents. Operate their environment. Automate their work. Deploy what they
+ build.
+