diff --git a/apps/site/src/app/stack/bun-mark.tsx b/apps/site/src/app/stack/bun-mark.tsx
new file mode 100644
index 0000000000..37c0b71e41
--- /dev/null
+++ b/apps/site/src/app/stack/bun-mark.tsx
@@ -0,0 +1,17 @@
+/**
+ * The Bun mark, drawn as a single-colour silhouette (with eyes + smile cut out
+ * via even-odd) in currentColor so it matches the Prisma mark's treatment.
+ */
+export function BunMark({ className }: { className?: string }) {
+ const silhouette =
+ "M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75Z";
+ const leftEye = "M20.19,33.29a5.51,5.51 0 1,0 11.02,0a5.51,5.51 0 1,0 -11.02,0Z";
+ const rightEye = "M44.96,33.29a5.51,5.51 0 1,0 11.02,0a5.51,5.51 0 1,0 -11.02,0Z";
+ const mouth =
+ "M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z";
+ return (
+
+
+
+ );
+}
diff --git a/apps/site/src/app/stack/framework-carousel.tsx b/apps/site/src/app/stack/framework-carousel.tsx
new file mode 100644
index 0000000000..74d6d99f06
--- /dev/null
+++ b/apps/site/src/app/stack/framework-carousel.tsx
@@ -0,0 +1,60 @@
+import { Marquee } from "@/components/marquee";
+import { Action, Card } from "@prisma/eclipse";
+import type { CSSProperties } from "react";
+import { frameworks } from "./stack-data";
+
+/**
+ * Vertical stack: a scrolling marquee of the frameworks Prisma Compute runs,
+ * with a Prisma Compute card beneath it. Both are full-width so the pairing
+ * reads cleanly on mobile. The compute icon matches the diagram node.
+ */
+export function FrameworkCarousel() {
+ return (
+
+
+
+ {frameworks.map((f) => (
+
+
+ {f.mono}
+
+
+ {f.name}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Prisma Compute
+
+
+ Same runtime, same database, same deploy for any framework.
+
+
+
+
+ );
+}
diff --git a/apps/site/src/app/stack/page.tsx b/apps/site/src/app/stack/page.tsx
index e0a8540e1e..6f092787bb 100644
--- a/apps/site/src/app/stack/page.tsx
+++ b/apps/site/src/app/stack/page.tsx
@@ -1,18 +1,17 @@
import { createPageMetadata } from "@/lib/page-metadata";
-import { Card } from "@prisma/eclipse";
-import { Layers } from "lucide-react";
-import Image from "next/image";
-import Link from "next/link";
-import { cn } from "@/lib/cn";
-import {
- stackCategories,
- type StackCategory,
- type StackLinkItem,
-} from "./stack-data";
+import { prisma_highlighter } from "@/lib/shiki_prisma";
+import { Action, Button, Card, CodeBlock } from "@prisma/eclipse";
+import type { ReactNode } from "react";
+import { FrameworkCarousel } from "./framework-carousel";
+import { PrismaMark } from "./prisma-mark";
+import { Reveal } from "./reveal";
+import { StackDiagram } from "./stack-diagram";
+import { bunApis, postgresExtensions } from "./stack-data";
+import styles from "./stack.module.css";
-const title = "Prisma in your stack | Prisma";
+const title = "Prisma Stack: a complete TypeScript foundation | Prisma";
const description =
- "Prisma is a Node.js and TypeScript ORM that integrates easily with popular databases, and frameworks.";
+ "Prisma Compute, Prisma Postgres, Bun and TypeScript. A complete, opinionated stack with room for your frontend.";
export const metadata = createPageMetadata({
title,
@@ -21,60 +20,219 @@ export const metadata = createPageMetadata({
ogImage: "/og/og-stack.png",
});
+const ORM_CODE = `const posts = await prisma.post.findMany({
+ where: { published: true },
+ include: { author: true }, // fully typed
+})`;
+
+const EXT_CODE = `-- semantic search, no extra service
+CREATE EXTENSION vector;
+
+SELECT id, content
+FROM docs
+ORDER BY embedding <-> $query
+LIMIT 5;`;
+
+async function Code({ code, lang }: { code: string; lang: "typescript" | "sql" }) {
+ const html = await (await prisma_highlighter()).codeToHtml(code, { lang, theme: "prisma-dark" });
+ return (
+
+ );
+}
+
+function SectionHead({
+ eyebrow,
+ title,
+ children,
+}: {
+ eyebrow?: string;
+ title: string;
+ children?: ReactNode;
+}) {
+ return (
+
+ {eyebrow ? {eyebrow} : null}
+ {title}
+ {children ? {children}
: null}
+
+ );
+}
+
export default function StackPage() {
return (
-
-
-
-
-
-
-
- Prisma Stack
-
-
- Build with
- your favorite tools
-
-
+
+ {/* ===== HERO ===== */}
+
+
+
+
+
+ Prisma Stack
+
+
+ Stop configuring. Start shipping.
+
+
+ Most apps have the same recurring problems. Prisma Stack solves them with the fewest
+ dependencies running on one platform. Great performance without learning and configuring
+ multiple frameworks or providers.
+
-
-
- {stackCategories.map((category: StackCategory) => (
-
-
- {category.title}
-
-
- {category.description}
-
-
- {category.items.map((item: StackLinkItem) => (
-
-
-
-
- {item.name}
+
+ {/* ===== THE STACK, SIMPLIFIED + DIAGRAM ===== */}
+
+
+
+
+ You can still deploy your app or database wherever you'd like. Use any dependency
+ you're familiar with. Prisma Stack still works.
+
+
+
+
+ {/* ===== FRONTEND CAROUSEL ===== */}
+
+
+
+ Prisma Compute runs every major frontend framework. Same database, same runtime, same
+ deploy. Your call on the view layer.
+
+
+
+
+
+
+
+ {/* ===== BUN ===== */}
+
+
+
Bun simplifies your dependencies.
+
+ {bunApis.map((b) => (
+
+
+
+ {b.api}
+
+ {b.role}
+
+
+ ))}
+
+
+
+
+ {/* ===== DATA ===== */}
+
+
+
+ A real Postgres database with a type-safe ORM on top and the extensions you actually
+ use, one line away.
+
+
+
+
+
+
+ Model your schema once. Get autocompletion, compile-time safety and migrations
+ for free.
+
+
+
+
+ Type-safe queries, end to end
+
+
+
+ Declarative schema and versioned migrations
+
+
+
+ Agent guardrails against unsafe writes
+
+
+
+
+
+
+
+
+
+
+
+
+ Postgres extensions
+
+
+
+ Opinionated defaults you can swap. Turn on vector search, full-text and cron
+ without leaving Postgres.
+
+
+ {postgresExtensions.map((ext) => (
+
+ {ext}
-
-
- ))}
+ ))}
+
+
+
+
+
+
+
+
+ {/* ===== FINAL CTA ===== */}
+
+
+
+
+
+ Stop configuring. Start shipping.
+
+
+ The database, the runtime and the compute layer are decided and connected. You
+ write features.
+
+
+
+ npx create-prisma
+
- ))}
+
-
+
);
}
diff --git a/apps/site/src/app/stack/prisma-mark.tsx b/apps/site/src/app/stack/prisma-mark.tsx
new file mode 100644
index 0000000000..3e85a65827
--- /dev/null
+++ b/apps/site/src/app/stack/prisma-mark.tsx
@@ -0,0 +1,8 @@
+/** The Prisma prism mark, drawn in currentColor so it inherits its tile's colour. */
+export function PrismaMark({ className }: { className?: string }) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/site/src/app/stack/reveal.tsx b/apps/site/src/app/stack/reveal.tsx
new file mode 100644
index 0000000000..20aec40ac9
--- /dev/null
+++ b/apps/site/src/app/stack/reveal.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { useEffect, useRef, type ReactNode } from "react";
+import styles from "./stack.module.css";
+
+/** Fades + lifts its children into view the first time they intersect. */
+export function Reveal({ children, className }: { children: ReactNode; className?: string }) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const io = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((e) => {
+ if (e.isIntersecting) {
+ e.target.classList.add(styles.in);
+ io.unobserve(e.target);
+ }
+ });
+ },
+ { threshold: 0.12 },
+ );
+ io.observe(el);
+ return () => io.disconnect();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/site/src/app/stack/stack-data.ts b/apps/site/src/app/stack/stack-data.ts
index 0c993cf1eb..9544fc334d 100644
--- a/apps/site/src/app/stack/stack-data.ts
+++ b/apps/site/src/app/stack/stack-data.ts
@@ -1,89 +1,60 @@
-export type StackLinkItem = {
- id: string;
+export type Framework = {
name: string;
- href: string;
- icon: string;
- /** Black/monochrome marks: invert in dark mode so they stay visible on dark UI */
- invertInDark?: boolean;
+ /** Short monogram shown in the badge tile */
+ mono: string;
+ /** Accent color for the badge glyph */
+ color: string;
+ /** Badge tile background */
+ bg: string;
};
-export type StackCategory = {
- id: string;
- title: string;
- description: string;
- items: StackLinkItem[];
-};
-
-/**
- * Twelve stack routes only — grouped like the legacy /stack page:
- * Languages → Databases → Frameworks.
- */
-export const stackCategories: StackCategory[] = [
- {
- id: "languages",
- title: "Languages",
- description: "Prisma can be used in any Node.js or TypeScript backend application.",
- items: [
- {
- id: "typescript",
- name: "TypeScript",
- href: "/typescript",
- icon: "/icons/technologies/ts.svg",
- },
- ],
- },
- {
- id: "databases",
- title: "Databases",
- description: "Prisma works seamlessly across most popular databases and service providers.",
- items: [
- {
- id: "mongodb",
- name: "MongoDB",
- href: "/mongodb",
- icon: "/icons/technologies/mongo.svg",
- },
- {
- id: "cockroachdb",
- name: "CockroachDB",
- href: "/cockroachdb",
- icon: "/icons/technologies/cockroach.svg",
- },
- {
- id: "planetscale",
- name: "PlanetScale",
- href: "/planetscale",
- icon: "/icons/technologies/planetscale.svg",
- invertInDark: true,
- },
- ],
- },
+/** Frameworks that Prisma Compute runs, cycled through the diagram + marquee. */
+export const frameworks: Framework[] = [
{
- id: "frameworks",
- title: "Frameworks",
- description:
- "Here is a non-exhaustive list of libraries and frameworks you can use with Prisma.",
- items: [
- { id: "nextjs", name: "Next.js", href: "/nextjs", icon: "/icons/technologies/nextjs.svg" },
- { id: "nestjs", name: "NestJS", href: "/nestjs", icon: "/icons/technologies/nestjs.svg" },
- { id: "react", name: "React", href: "/react", icon: "/icons/technologies/react.svg" },
- {
- id: "express",
- name: "Express",
- href: "/express",
- icon: "/icons/technologies/express.svg",
- invertInDark: true,
- },
- { id: "hapi", name: "Hapi", href: "/hapi", icon: "/icons/technologies/hapi.svg" },
- { id: "graphql", name: "GraphQL", href: "/graphql", icon: "/icons/technologies/graphql.svg" },
- {
- id: "fastify",
- name: "Fastify",
- href: "/fastify",
- icon: "/icons/technologies/fastify.svg",
- invertInDark: true,
- },
- { id: "apollo", name: "Apollo", href: "/apollo", icon: "/icons/technologies/apollo.svg" },
- ],
+ name: "Next.js",
+ mono: "N",
+ color: "var(--color-foreground-neutral)",
+ bg: "var(--color-background-neutral-weak)",
},
+ { name: "React Router", mono: "RR", color: "#f44250", bg: "rgba(244,66,80,0.14)" },
+ { name: "TanStack Start", mono: "TS", color: "#ff6b35", bg: "rgba(255,107,53,0.14)" },
+ { name: "Astro", mono: "A", color: "#ff5d01", bg: "rgba(255,93,1,0.14)" },
+ { name: "SvelteKit", mono: "S", color: "#ff3e00", bg: "rgba(255,62,0,0.14)" },
+ { name: "Nuxt", mono: "V", color: "#00dc82", bg: "rgba(0,220,130,0.14)" },
+ { name: "SolidStart", mono: "So", color: "#4c86c6", bg: "rgba(70,130,200,0.18)" },
+ { name: "Qwik City", mono: "Q", color: "#18b6f6", bg: "rgba(24,182,246,0.14)" },
+ { name: "Remix", mono: "Rx", color: "#3992ff", bg: "rgba(57,146,255,0.16)" },
+ { name: "Vite + React", mono: "Vt", color: "#bd34fe", bg: "rgba(189,52,254,0.16)" },
+];
+
+export type BunApi = {
+ api: string;
+ role: string;
+};
+
+/** Bun capabilities that ship with Prisma Compute. */
+export const bunApis: BunApi[] = [
+ { api: "Bun.serve()", role: "HTTP + WebSocket server, built in." },
+ { api: "Bun.image", role: "Resize, convert and optimise images." },
+ { api: "bun test", role: "Jest-compatible test runner." },
+ { api: "Bun.password", role: "Argon2 / bcrypt hashing." },
+ { api: "bun install", role: "Package manager, fast lockfile." },
+ { api: "Bun bundler", role: "Bundle and transpile, no config." },
+ { api: "Bun.$``", role: "Shell scripting in TypeScript." },
+ { api: "Bun.s3", role: "S3-compatible object storage client." },
+ { api: "Bun.redis", role: "Built-in Redis / cache client." },
+ { api: "Bun.file", role: "Fast file reads, writes and streams." },
+ { api: ".env (native)", role: "Auto-loads environment files." },
+ { api: "TS + JSX", role: "Runs TypeScript directly, no build." },
+];
+
+/** Postgres extensions surfaced in the data section. */
+export const postgresExtensions: string[] = [
+ "pgvector",
+ "pg_search",
+ "Vector type",
+ "pg_cron",
+ "pg_trgm",
+ "pg_stat_statements",
+ "uuid-ossp",
];
diff --git a/apps/site/src/app/stack/stack-diagram.tsx b/apps/site/src/app/stack/stack-diagram.tsx
new file mode 100644
index 0000000000..ddc621cc19
--- /dev/null
+++ b/apps/site/src/app/stack/stack-diagram.tsx
@@ -0,0 +1,323 @@
+"use client";
+
+import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";
+import { Button } from "@prisma/eclipse";
+import { BunMark } from "./bun-mark";
+import { PrismaMark } from "./prisma-mark";
+import styles from "./stack.module.css";
+import { frameworks } from "./stack-data";
+
+const accentPPG = {
+ "--accent": "var(--color-foreground-ppg)",
+} as CSSProperties;
+const accentViolet = {
+ "--accent": "var(--color-foreground-violet)",
+} as CSSProperties;
+
+/** Orthogonal connectors drawn between diagram nodes. */
+const FLOW_LINKS: [string, string][] = [
+ ["n-frontend", "n-compute"],
+ ["n-backend", "n-compute"],
+ ["n-orm", "n-postgres"],
+ ["n-compute", "n-fimage"],
+ ["n-compute", "n-ffile"],
+ ["n-compute", "n-fredis"],
+ ["n-compute", "n-fmore"],
+ ["n-postgres", "n-pgvector"],
+ ["n-postgres", "n-pgsearch"],
+ ["n-postgres", "n-pgstat"],
+ ["n-postgres", "n-pgmore"],
+];
+
+function Leaf({ id, name, role }: { id: string; name: string; role: string }) {
+ return (
+
+ );
+}
+
+export function StackDiagram() {
+ const rootRef = useRef(null);
+ const flowRef = useRef(null);
+ const svgRef = useRef(null);
+ const badgeRef = useRef(null);
+ const [idx, setIdx] = useState(0);
+ const [swapping, setSwapping] = useState(false);
+
+ const fw = frameworks[idx];
+
+ const drawFlow = useCallback(() => {
+ const flow = flowRef.current;
+ const svg = svgRef.current;
+ const root = rootRef.current;
+ if (!flow || !svg || !root) return;
+
+ // measure in the diagram's natural (unscaled) coordinate space
+ flow.style.transform = "none";
+ flow.style.marginBottom = "";
+ const W = flow.offsetWidth;
+ const H = flow.offsetHeight;
+
+ // offset geometry (relative to #flow) is immune to the scale transform
+ // applied below, unlike getBoundingClientRect
+ const rel = (id: string) => {
+ const el = root.querySelector(`[data-node="${id}"]`);
+ if (!el) return null;
+ let x = 0;
+ let y = 0;
+ let n: HTMLElement | null = el;
+ while (n && n !== flow) {
+ x += n.offsetLeft;
+ y += n.offsetTop;
+ n = n.offsetParent as HTMLElement | null;
+ }
+ const w = el.offsetWidth;
+ const h = el.offsetHeight;
+ return {
+ left: x,
+ right: x + w,
+ top: y,
+ bottom: y + h,
+ cx: x + w / 2,
+ cy: y + h / 2,
+ };
+ };
+ const elbow = (a: string, b: string) => {
+ const s = rel(a);
+ const t = rel(b);
+ if (!s || !t) return "";
+ const mx = (s.right + t.left) / 2;
+ return `M ${s.right} ${s.cy} H ${mx} V ${t.cy} H ${t.left}`;
+ };
+
+ let paths = FLOW_LINKS.map((l) => {
+ const d = elbow(l[0], l[1]);
+ return d
+ ? ` `
+ : "";
+ }).join("");
+
+ // dashed sub-ms latency link between Compute and Postgres
+ const c = rel("n-compute");
+ const p = rel("n-postgres");
+ if (c && p) {
+ const lx = c.cx;
+ paths += ` `;
+ const badge = badgeRef.current;
+ if (badge) {
+ badge.style.left = `${lx}px`;
+ badge.style.top = `${(c.bottom + p.top) / 2}px`;
+ }
+ }
+
+ svg.setAttribute("width", String(W));
+ svg.setAttribute("height", String(H));
+ svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
+ svg.innerHTML =
+ ` ` +
+ paths;
+
+ // scale the diagram down to fit its container so it never needs
+ // horizontal scrolling on narrower viewports
+ const scroller = flow.parentElement;
+ if (!scroller) return;
+ const cs = getComputedStyle(scroller);
+ const avail = scroller.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
+ const scale = Math.min(1, avail / W);
+ if (scale < 1) {
+ const tx = (avail - W * scale) / 2;
+ flow.style.transformOrigin = "top left";
+ flow.style.transform = `translateX(${tx}px) scale(${scale})`;
+ flow.style.marginBottom = `${-(1 - scale) * H}px`;
+ }
+ }, []);
+
+ // cycle the frontend framework in the header + diagram node
+ useEffect(() => {
+ let swapTimeout: ReturnType;
+ const interval = setInterval(() => {
+ setSwapping(true);
+ swapTimeout = setTimeout(() => {
+ setIdx((i) => (i + 1) % frameworks.length);
+ setSwapping(false);
+ }, 240);
+ }, 2000);
+ return () => {
+ clearInterval(interval);
+ clearTimeout(swapTimeout);
+ };
+ }, []);
+
+ // redraw connectors after the frontend node width changes
+ useEffect(() => {
+ drawFlow();
+ }, [idx, drawFlow]);
+
+ // draw on mount, on resize, and once fonts are ready
+ useEffect(() => {
+ // rAF-batch resize so a drag-resize doesn't thrash layout on every event
+ let raf = 0;
+ const onResize = () => {
+ cancelAnimationFrame(raf);
+ raf = requestAnimationFrame(drawFlow);
+ };
+ drawFlow();
+ window.addEventListener("resize", onResize);
+ if (document.fonts?.ready) document.fonts.ready.then(drawFlow);
+ const t1 = setTimeout(drawFlow, 300);
+ const t2 = setTimeout(drawFlow, 900);
+ return () => {
+ window.removeEventListener("resize", onResize);
+ cancelAnimationFrame(raf);
+ clearTimeout(t1);
+ clearTimeout(t2);
+ };
+ }, [drawFlow]);
+
+ return (
+
+
+ The stack simplified:
+
+
+ Prisma
+
+ Bun
+ Postgres
+
+
+ +
+ {fw.name}
+
+
+
+
+
+
+ Start building
+
+
+
+
+ npx create-prisma and you
+ have compute, a database, and a runtime.
+
+
+
+
+
+ {/* Column 1: everything you write, grouped under TypeScript */}
+
+
+
+
Frontend
+
+ {fw.mono}
+ {fw.name}
+
+
Fully open
+
+
+
Backend
+
+
+
+
+ Bun
+
+
test · build
+
+
+
+
+
Data access
+
+
type-safety · query · migrations
+
+
+
TypeScript
+
+
+ {/* Column 2: managed platform */}
+
+
+
+
+
+ Prisma Compute
+
+
+ Bun runtime
+
+
+
+
+
+
+
+ Prisma Postgres
+
+
+ Managed Postgres
+
+
+
+ {/* Column 3: Compute's Bun features + Postgres extensions */}
+
+
+
+
+
+ sub-ms latency
+
+
+
+
+ );
+}
diff --git a/apps/site/src/app/stack/stack.module.css b/apps/site/src/app/stack/stack.module.css
new file mode 100644
index 0000000000..8cc20f34eb
--- /dev/null
+++ b/apps/site/src/app/stack/stack.module.css
@@ -0,0 +1,403 @@
+/*
+ * Prisma Stack — flow-diagram + marquee geometry only.
+ *
+ * The page chrome (hero, sections, cards, code, CTA) is built with Tailwind
+ * and @prisma/eclipse components, matching the compute/postgres pages. This
+ * module holds only what those primitives can't express: the subgrid diagram,
+ * its SVG connectors, the "stack simplified" bracket, and the framework
+ * marquee. All colours come from semantic design tokens so it themes in both
+ * modes. Class names are referenced from JSX via styles["kebab-name"].
+ */
+
+.page {
+ --panel-from: var(--color-background-default);
+ --panel-to: var(--color-background-ppg);
+ --border: var(--color-stroke-neutral);
+ --border-ppg: var(--color-stroke-ppg);
+ --line: var(--color-stroke-neutral-strong);
+ --ink: var(--color-foreground-neutral);
+ --ink-dim: var(--color-foreground-neutral-weak);
+ --ink-faint: var(--color-foreground-neutral-weaker);
+ --teal: var(--color-foreground-ppg);
+ --teal-bright: var(--color-foreground-ppg-weak);
+ --violet: var(--color-foreground-violet);
+ --font-display: var(--font-mona-sans), "Mona Sans VF", "Inter", sans-serif;
+
+ position: relative;
+ width: 100%;
+ /* the diagram is 820px wide before it scales to fit; clip so it can never
+ widen the page and cause horizontal overflow on narrow viewports */
+ overflow-x: clip;
+}
+.content {
+ position: relative;
+ z-index: 1;
+}
+
+/* ===== "stack simplified" one-liner + include bracket ===== */
+.stack-simplified {
+ text-align: center;
+ max-width: 940px;
+ margin: 0 auto;
+ font-weight: 800;
+ font-family: var(--font-display);
+}
+.stack-simplified .ss-eq {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: baseline;
+ column-gap: 0.28em;
+ margin-top: 10px;
+ text-align: left;
+}
+.stack-simplified .ss-leftcol {
+ grid-column: 1;
+ justify-self: end;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+.stack-simplified .ss-brand {
+ white-space: nowrap;
+ background: linear-gradient(100deg, var(--teal-bright), var(--color-foreground-orm));
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+.stack-simplified .ss-plus {
+ grid-column: 2;
+ white-space: nowrap;
+ color: var(--ink-faint);
+}
+.stack-simplified .ss-fw {
+ grid-column: 3;
+ justify-self: start;
+ min-width: 0;
+ white-space: nowrap;
+ color: var(--ink);
+ transition: opacity 0.35s ease;
+}
+.stack-simplified.swapping .ss-fw {
+ opacity: 0;
+}
+/* the include bracket: one vertical rail spanning the two branch centres,
+ each branch a horizontal stub that meets the rail with no gap */
+.stack-simplified .ss-includes {
+ position: relative;
+ width: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin: 4px 0 0 0.3em;
+ font-size: 0.68em;
+ font-weight: 600;
+}
+/* vertical rail descends from just below "Prisma" down to the last branch */
+.stack-simplified .ss-includes::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0.9em;
+ width: 2px;
+ background: var(--line);
+}
+.stack-simplified .ss-inc {
+ position: relative;
+ padding-left: 1.05em;
+ line-height: 1.8;
+ white-space: nowrap;
+ color: var(--ink-dim);
+}
+.stack-simplified .ss-inc::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0.9em;
+ transform: translateY(-50%);
+ width: 0.8em;
+ height: 2px;
+ background: var(--line);
+}
+
+/* ===== flow diagram ===== */
+.flow-scroll {
+ overflow: hidden;
+ padding: 12px 6px 52px;
+}
+.flow {
+ position: relative;
+ display: grid;
+ grid-template-columns: auto max-content max-content;
+ grid-template-areas:
+ "tsg compute feat"
+ "tsg postgres ext";
+ align-items: center;
+ column-gap: 70px;
+ row-gap: 48px;
+ min-width: 820px;
+ width: max-content;
+ margin: 0 auto;
+}
+.ts-group {
+ grid-area: tsg;
+ position: relative;
+ display: grid;
+ grid-template-rows: subgrid;
+ align-items: center;
+ padding: 0 16px;
+}
+/* container box; a pseudo-element so its height is independent of the subgrid */
+.ts-group::before {
+ content: "";
+ position: absolute;
+ inset: -6px 0 -34px 0;
+ border-radius: 20px;
+ background: color-mix(in srgb, var(--teal) 6%, transparent);
+ border: 1px solid color-mix(in srgb, var(--teal) 26%, transparent);
+ z-index: -1;
+}
+.ts-sub {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+.ts-label {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: -26px;
+ z-index: 1;
+ text-align: center;
+ font-family: var(--font-display);
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ font-size: 13px;
+ color: var(--teal);
+}
+.n-compute {
+ grid-area: compute;
+ justify-self: start;
+}
+.n-postgres {
+ grid-area: postgres;
+ justify-self: start;
+}
+.feat-stack {
+ grid-area: feat;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+.ext-stack {
+ grid-area: ext;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+.flow-svg {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ overflow: visible;
+}
+.flow-path {
+ fill: none;
+ stroke: var(--line);
+ stroke-width: 1.5;
+}
+.flow-path.dashed {
+ stroke: var(--teal);
+ stroke-dasharray: 5 5;
+}
+.flow-badge {
+ position: absolute;
+ z-index: 5;
+ transform: translate(-50%, -50%);
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ background: var(--color-background-default);
+ border: 1px solid color-mix(in srgb, var(--teal) 45%, transparent);
+ color: var(--teal);
+ font-size: 11px;
+ font-weight: 700;
+ padding: 5px 10px;
+ border-radius: 999px;
+ white-space: nowrap;
+ box-shadow: var(--shadow-box-low);
+}
+
+/* ===== node ===== */
+.node {
+ position: relative;
+ z-index: 1;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ background: linear-gradient(180deg, var(--panel-from) 0%, var(--panel-to) 240%);
+ padding: 12px 15px;
+ min-width: 168px;
+ white-space: nowrap;
+ transition:
+ transform 0.18s,
+ border-color 0.18s,
+ box-shadow 0.18s;
+}
+.node:hover {
+ transform: translateY(-2px);
+ border-color: var(--border-ppg);
+ box-shadow: var(--shadow-box);
+}
+.node::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 10px;
+ bottom: 10px;
+ width: 3px;
+ border-radius: 3px;
+ background: var(--accent, var(--teal));
+}
+.node-eyebrow {
+ font-size: 10px;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--ink-faint);
+ font-weight: 700;
+}
+.node-title {
+ font-family: var(--font-display);
+ font-weight: 800;
+ font-size: 15px;
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ color: var(--ink);
+}
+.node-sub {
+ font-size: 11.5px;
+ color: var(--ink-dim);
+ margin-top: 3px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+/* Action-style icon tile (mirrors eclipse Action color="ppg") */
+.node-icon {
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ display: grid;
+ place-items: center;
+ flex: 0 0 auto;
+ font-size: 12px;
+ background: var(--color-background-ppg);
+ color: var(--color-foreground-ppg);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--teal)) 30%, transparent);
+}
+.node.root .node-icon {
+ width: 30px;
+ height: 30px;
+ font-size: 14px;
+}
+/* framework monogram tile (violet "open" layer) */
+.mono-badge {
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ display: grid;
+ place-items: center;
+ font-family: var(--font-display);
+ font-weight: 900;
+ font-size: 12px;
+ flex: 0 0 auto;
+ background: color-mix(in srgb, var(--violet) 16%, transparent);
+ color: var(--violet);
+ border: 1px solid color-mix(in srgb, var(--violet) 34%, transparent);
+}
+.pill {
+ display: inline-block;
+ margin-top: 8px;
+ font-size: 9.5px;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ padding: 3px 8px;
+ border-radius: 6px;
+}
+.pill.open {
+ color: var(--violet);
+ background: color-mix(in srgb, var(--violet) 14%, transparent);
+ border: 1px solid color-mix(in srgb, var(--violet) 30%, transparent);
+}
+.node.leaf {
+ min-width: 150px;
+ padding: 10px 14px;
+ background: color-mix(in srgb, var(--ink) 3%, transparent);
+}
+.node.leaf .node-title {
+ font-size: 13px;
+ font-weight: 600;
+ font-family: inherit;
+}
+.node.leaf::before {
+ top: 8px;
+ bottom: 8px;
+}
+.node.leaf.dots-leaf {
+ min-width: 60px;
+ text-align: center;
+}
+.node.leaf.dots-leaf .node-title {
+ justify-content: center;
+ color: var(--ink-faint);
+ font-weight: 800;
+ letter-spacing: 3px;
+}
+.node.root {
+ min-width: 208px;
+ border-color: color-mix(in srgb, var(--teal) 40%, transparent);
+}
+.node.root .node-title {
+ font-size: 16px;
+}
+.n-frontend .node-title {
+ transition: opacity 0.35s ease;
+}
+.n-frontend.swapping .node-title {
+ opacity: 0;
+}
+.leaf-dim {
+ color: var(--ink-faint);
+ font-weight: 400;
+}
+
+/* ===== scroll reveal ===== */
+.reveal {
+ opacity: 0;
+ transform: translateY(24px);
+ transition:
+ opacity 0.7s ease,
+ transform 0.7s ease;
+}
+.reveal.in {
+ opacity: 1;
+ transform: none;
+}
+@media (prefers-reduced-motion: reduce) {
+ .reveal {
+ opacity: 1;
+ transform: none;
+ transition: none;
+ }
+ /* the diagram auto-cycles frameworks, so silence its swap + hover motion too */
+ .stack-simplified .ss-fw,
+ .n-frontend .node-title,
+ .node {
+ transition: none;
+ }
+ .node:hover {
+ transform: none;
+ }
+}