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 @@ + + + AgentOS coalesced readiness state machine + Dark-mode state machine demonstrating flags, per-capability revisions, per-VM wake epochs, acknowledgement races, and JavaScript backpressure. + + + + + + + + 4 · Coalescing — revisions are counters, not queued messages + Example for capability 42. One per-handle map entry survives races; one per-VM epoch identifies the currently outstanding doorbell. + + + Timeline: three readiness publications become at most two wakes + + + + 0 + Idle + queue = [] + flags = ∅ + revision = 7 + wake = Idle + No task or thread is spinning. + + + 1 + First Data event + queue = [chunk A] + flags = READABLE + revision = 8 + wake = Outstanding(11) + Enqueue ReadyWake(epoch 11). + + + 2 + More Data before drain + queue = [chunk A, chunk B] + flags = READABLE + revision = 9 + wake = Outstanding(11) + No second wake: already outstanding. + + + 3 + V8 takes batch + epoch = 11 + cap = 42 + flags = READABLE + observedRevision = 9 + This is a snapshot, not removal. + + + 4 + Race: chunk C + queue += chunk C + flags = READABLE + revision = 10 + wake = Outstanding(11) + Still no extra wake yet. + + + + + + + + + 5 + V8 acknowledges what it observed + complete_wake(epoch=11, observedRevision=9) + Current revision is 10, so flags are not cleared. + An old acknowledgement cannot erase newer work. + + + 6 + Broker atomically rearms + wake: Outstanding(11) → Idle + work remains → Outstanding(12) + Exactly one replacement wake is enqueued. + + + 7 + Next drain catches up + batch(epoch=12, revision=10) + ack revision=10 → clear READABLE + If no other work exists, wake returns to Idle. + + + + + + What is bounded? + • ReadyState has at most one entry per admitted capability. + • The wake mailbox has capacity one and state permits one queued/in-flight epoch. + • The socket completion queue is bounded by event count and charged bytes. + • ready_batch copies at most the configured work quantum. + + + What is not a queue? + • Revision 8 → 9 → 10 is one integer field being updated, not three queued revisions. + • READABLE is one level bit, not one event per packet or byte chunk. + • Epoch 11 identifies one wake cycle; it does not enumerate the socket events. + • The wake never owns payload bytes, so wake coalescing cannot lose data. + + + + Backpressure state: JavaScript controls whether the Tokio reader is allowed to read again + + Guest Readable buffer + reaches its high-water mark + Readable.push(bytes) === false + applicationReadDemand = false + bridge: SetReadInterest(false) + This is stream backpressure, not unref(). + + + Broker + socket capability pause + clear/suppress READABLE delivery + applicationReadInterest = false + reader waits on Notify + No repeated level-ready spin or wake storm. + + + Where bytes wait + bounded already-read completion state + plus OS receive buffer + plus sender-side TCP flow control + No additional readiness messages accumulate. + + + When JS needs more + _read() + SetReadInterest(true) + wake reader + republish if known + The bounded cycle begins again. + + + + + + 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 @@ + + + AgentOS socket readiness to V8 sequence + Dark-mode sequence diagram showing how a Tokio socket task stores data, publishes readiness, wakes the separate V8 executor, and causes JavaScript to drain the data. + + + + + + + + + 2 · One TCP read — exact wake-up and call-into-V8 sequence + The wake is only a doorbell. The actual bytes take a separate path and are fetched by JavaScript after V8 is awake. + + + + OS kernelTCP + epoll + + Tokio socket taskshared runtime worker + + Completion queuebounded, contains bytes + + ReadyState brokerflags + revisions + + V8 executor threadowns isolate + selector + + Guest JS NetSocketreadiness target + Duplex + + + + + + + + + + A. Tokio discovers work and stores it + 1 + + fd readable → Tokio Waker schedules socket future + 2 + + bounded try_read() turn + + bytes: [48 65 6c 6c 6f] + 3 + + send Data(bytes, accounting reservation) + If full, this task stops reading; the bytes remain in the kernel. + + + B. The same publisher rings one coalesced doorbell + 4 + + publish(cap=42, gen=3, READABLE) + + ReadyState[42].flags |= READABLE + ReadyState[42].revision += 1 + Repeated publications overwrite/merge this entry. + 5 + + ReadyWake { generation, epoch } + Internally: Tokio MPSC[1] staging → immediate crossbeam[1] transfer. + No bytes, socket event, packet count, or revision list is in the wake. + + + C. The V8 thread notices the wake and enters JavaScript itself + + crossbeam Select unblocks + Tokio did not enter V8 + 6 + + take_batch(epoch) + + [(cap=42, gen=3, flags=R, revision=9)] + 7 + + V8 Function::call(_agentOSReadyDispatch, 42, 3, R) + + JS Map lookup → wakeSocketBridgeReads() + Queues a JS microtask; still on the V8 thread. + + + D. JavaScript fetches bytes over a call-specific bridge response + 8 + + _netSocketReadRaw.applySync(socketId) + V8 executor blocks only itself while the registered call waiter settles. + + dequeue Data → direct response → actual bytes + 9 + Readable.push(bytes) + 10 + + complete_batch(epoch, observed revisions) + Clears only unchanged observations; newer revisions cause a replacement wake. + + + Important: the readiness path wakes V8; the bridge read path transports bytes. They are separate so millions of readiness signals cannot become millions of queued payload messages. + 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 @@ + + + + + + + +OS + + + + + 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. */}