diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index 6ca99d764a..9df8040d4d 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -1,4 +1,8 @@
-name: Build Docs
+name: Build Site
+
+# Reusable build of the full site: the React landing (website/) overlaid onto the
+# MkDocs docs + blog build, via scripts/docs/build_site.sh. Produces the `site` artifact
+# consumed by docs.yaml (deploy) and build.yml (PR build check).
on:
workflow_call:
@@ -11,13 +15,18 @@ jobs:
- uses: astral-sh/setup-uv@v5
with:
python-version: 3.11
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ cache-dependency-path: website/package-lock.json
- name: Install dstack
run: |
uv sync --extra server
- name: Build
run: |
sudo apt-get update && sudo apt-get install -y libcairo2-dev libfreetype6-dev libffi-dev libjpeg-dev libpng-dev libz-dev
- uv run mkdocs build -s
+ ./scripts/docs/build_site.sh
- uses: actions/upload-artifact@v4
with:
name: site
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 3e0d5f3a75..78212c6fda 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -1,4 +1,7 @@
-name: Build & Deploy Docs
+name: Build & Deploy Site
+
+# Builds the full site (React landing + MkDocs docs + blog, see build-docs.yml) and
+# cross-repo deploys it to the GitHub Pages repo serving dstack.ai.
on:
workflow_dispatch:
diff --git a/.justfile b/.justfile
index efa8c87f61..c419685edc 100644
--- a/.justfile
+++ b/.justfile
@@ -8,6 +8,7 @@
# * runner/.justfile – Building and uploading dstack runner and shim
# * frontend/.justfile – Building and running the frontend
# * mkdocs/.justfile – Building and previewing the docs site
+# * website/.justfile – Building and previewing the React landing page
default:
@just --list
@@ -19,3 +20,5 @@ import "runner/.justfile"
import "frontend/.justfile"
import "mkdocs/.justfile"
+
+import "website/.justfile"
diff --git a/contributing/DOCS.md b/contributing/DOCS.md
index 663e5c4c77..a0b034a181 100644
--- a/contributing/DOCS.md
+++ b/contributing/DOCS.md
@@ -1,5 +1,11 @@
# Documentation setup
+> **The dstack.ai site has three parts on one origin:** the **landing** page (`/`) is a React
+> app in [`website/`](../website); the **docs** (`/docs`) and **blog** (`/blog`) are built with
+> MkDocs from `mkdocs/`. This guide covers the **docs and blog** (MkDocs). For the landing and
+> for building everything together, see [The landing page](#the-landing-page-website) and
+> [Building the whole site](#building-the-whole-site) below.
+
## 1. Clone the repo:
```shell
@@ -36,7 +42,7 @@ uv run pre-commit install
## 5. Preview documentation
-To preview the documentation, run the follow command:
+To preview the **docs and blog** (MkDocs), run the follow command:
```shell
uv run mkdocs serve --livereload -s
@@ -44,12 +50,44 @@ uv run mkdocs serve --livereload -s
The `--livereload` flag is required to work around live-reload bugs in recent `mkdocs` versions.
+This serves the docs and blog only. The landing page (`/`) is a separate React app — when you
+run `mkdocs serve` on its own, `/` simply redirects to `/docs/`. To work on the landing, see
+[The landing page](#the-landing-page-website) below.
+
If you want to build static files, you can use the following command:
```shell
uv run mkdocs build -s
```
+## The landing page (website/)
+
+The landing page at `/` is a React (Vite) app in [`website/`](../website), not MkDocs. It has
+its own `package.json`/`node_modules`. Preview it on its own (requires Node 20+):
+
+```shell
+just website-dev # Vite dev server on http://127.0.0.1:5173
+```
+
+Docs/blog links on the landing resolve same-origin (`/docs`, `/blog`), which 404 in standalone
+dev. Point them at a live site while iterating: `just website-dev https://dstack.ai`.
+
+The `/old` route is kept as a template for building future product pages (reachable in dev; not
+part of the production deploy). Google Analytics and the social/OG image reuse the same property
+and MkDocs-generated card as the rest of the site.
+
+## Building the whole site
+
+CI builds the landing and the MkDocs docs/blog and overlays them into a single `site/`:
+
+```shell
+just site-build # website/dist + `mkdocs build` -> ./site (scripts/docs/build_site.sh)
+just site-serve # preview the combined site on http://127.0.0.1:8001
+```
+
+In the combined build the React `index.html` owns `/`, while MkDocs serves `/docs`, `/blog`, and
+the shared `/assets`. This is what the `Build & Deploy Site` workflow deploys.
+
## Documentation build system
The documentation uses a custom build system with MkDocs hooks to generate various files dynamically.
@@ -141,7 +179,7 @@ we should not reintroduce per-tag OpenAPI files unless there is a concrete reaso
```
mkdocs/ # docs_dir for the mkdocs site
-├── index.md # Homepage
+├── index.md # Redirects to /docs/ (the landing "/" is the React app in website/)
├── docs/ # /docs/ URL section
│ ├── index.md # Getting started
│ ├── installation.md
@@ -157,7 +195,13 @@ mkdocs/ # docs_dir for the mkdocs site
├── layouts/ # Social card layouts
└── assets/ # Stylesheets, images, fonts
+website/ # React (Vite) landing page — served at "/"
+├── index.html # Entry; title, OG/meta, Google Analytics
+├── src/ # App, pages (Home, Old), components, routes
+└── public/static/ # Landing assets (namespaced to avoid clashing with /assets)
+
scripts/docs/
+├── build_site.sh # Build landing + docs/blog and overlay into ./site
├── hooks.py # MkDocs build hooks
├── gen_llms_files.py # llms.txt generation
├── gen_schema_reference.py # Schema expansion
diff --git a/mkdocs/index.md b/mkdocs/index.md
index 571c05ae6e..50475eafce 100644
--- a/mkdocs/index.md
+++ b/mkdocs/index.md
@@ -1,8 +1,11 @@
---
-template: home.html
+# The landing page ("/") is now the React app in website/. When the docs are served on
+# their own (e.g. `mkdocs serve`), this page redirects to /docs/. The title/description
+# below are only used to render the social card that the React landing reuses as its OG
+# image (assets/images/social/index.png).
+template: redirect.html
title: The orchestration stack for AI infrastructure
-hide:
- - navigation
- - toc
- - footer
+description: dstack is a unified control plane for GPU provisioning and orchestration that works with any GPU cloud, Kubernetes, or on-prem clusters.
+search:
+ exclude: true
---
diff --git a/mkdocs/overrides/assets/images/quotes/spott.jpg b/mkdocs/overrides/assets/images/quotes/spott.jpg
deleted file mode 100644
index cc9e0ae625..0000000000
Binary files a/mkdocs/overrides/assets/images/quotes/spott.jpg and /dev/null differ
diff --git a/mkdocs/overrides/home.html b/mkdocs/overrides/home.html
deleted file mode 100644
index c876938795..0000000000
--- a/mkdocs/overrides/home.html
+++ /dev/null
@@ -1,1011 +0,0 @@
-{% extends "landing.html" %}
-
-{% block scripts %}
-{{ super() }}
-
-
-
-{% endblock %}
-
-{% block content %}
-
-
-
-
-
The orchestration stack for AI infrastructure
-
-
- dstack is a unified control plane for GPU provisioning and orchestration that works with any GPU cloud, Kubernetes, or on-prem clusters.
- It streamlines development, training, and inference, and is compatible with any hardware, open-source tools, and frameworks.
-
- Finally, an orchestration stack that doesn’t suck.
-
-
-
-
-
-
-
One control plane for AI compute
-
- Managing AI infrastructure requires first-class primitives for accelerator provisioning,
- workload scheduling, and observability across clouds, clusters, and open-source frameworks.
-
-
-
- dstack unifies fleets, dev environments, tasks, services,
- volumes, and gateways in one control plane for AI workloads.
-
-
-
- It’s built for containerized AI workloads with a simple CLI, UI, and API.
- No Kubernetes or Slurm hassle required.
-
type: fleet
-name: my-fleet
-
-placement: cluster
-
-# Allow concurrent workloads on the same host
-blocks: auto
-
-ssh_config:
- user: ubuntu
- identity_file: ~/.ssh/id_rsa
- hosts:
- - 3.255.177.51
- - 3.255.177.52
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
type: dev-environment
-name: vscode
-
-python: "3.12"
-
-# Clones this repo to the working dir (from default image)
-repos:
- - .
-
-ide: vscode
-
-# Stop if inactive for 2 hours
-inactivity_duration: 2h
-
-resources:
- gpu: H100:1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Launch GPU dev environments
-
- If you need a remote development environment with a GPU, let dstack create you a dev environment.
-
-
-
If you plan to work with it yourself, you can access it using your desktop IDE such as VS
- Code, Cursor, and
- Windsurf. dstack apply prints both the IDE URL and SSH command.
-
- Run training or batch workloads on a single GPU, or scale to multi-GPU and multi-node clusters using simple task configurations.
- dstack automates cluster provisioning, resource allocation, and job scheduling.
-
-
-
- During execution, dstack reports GPU utilization, memory usage, and GPU health metrics for each job.
-
- With dstack, you can deploy models as secure,
- auto-scaling, OpenAI-compatible endpoints, integrating with top open-source serving frameworks
- such as SGLang, vLLM,
- TensorRT-LLM, or any other.
-
- Slurm is a battle-tested system with decades of production use in HPC environments.
- dstack by contrast, is built for modern ML/AI workloads with cloud-native provisioning and a container-first architecture.
- While both support distributed training and batch jobs, dstack
- also natively supports development and production-grade inference.
-
- Kubernetes is a general-purpose container orchestrator. dstack also
- orchestrates containers, but it provides a lightweight and streamlined interface that is purpose
- built for ML.
-
-
-
- You declare
- dev environments,
- tasks,
- services, and
- fleets
- with simple configuration. dstack provisions GPUs, manages clusters via fleets with fine-grained
- controls, and optimizes cost and utilization, while keeping a simple UI and CLI.
-
-
-
- If you already use Kubernetes, you can run dstack on it via the Kubernetes backend.
-
-
-
-
-
-
- Can I use dstack with Kubernetes?
-
-
-
-
-
-
- Yes. You can connect existing Kubernetes clusters using the Kubernetes backend and run
- dev environments,
- tasks, and
- services on it.
- Choose the Kubernetes backend if your GPUs already run on Kubernetes and your team depends on its
- ecosystem and tooling.
- See the
- Kubernetes guide for setup and best practices.
-
-
- If your priority is orchestrating cloud GPUs and Kubernetes isn’t a must, VM-based backends are a better fit
- thanks to their native cloud integration.
- For on-prem GPUs where Kubernetes is optional, SSH fleets provide a simpler and more lightweight alternative.
-
-
-
-
-
-
- When should I use dstack?
-
-
-
-
-
-
- dstack accelerates ML development with a simple, ML‑native interface.
- Spin up dev environments, run
- single‑node or distributed tasks, and deploy services without infrastructure overhead.
-
-
-
- It radically reduces GPU costs via smart orchestration and fine‑grained fleet controls, including efficient reuse,
- right‑sizing, and support for spot, on‑demand, and reserved capacity.
-
-
-
- It is 100% interoperable with your stack and works with any open‑source frameworks and tools, as
- well as your own Docker images and code, across GPU clouds, Kubernetes, and on‑prem GPUs.
-
Trusted by thousands of engineers across 100+ AI-first companies
-
-
-
-
-
-
-
Wah Loon Keng
-
-
Sr. AI Engineer @Electronic Arts
-
-
- With dstack, AI researchers at EA can spin up and scale experiments without touching
- infrastructure. It supports everything from quick prototyping to multi-node training on any cloud.
-
-
-
-
-
-
-
-
Aleksandr Movchan
-
-
ML Engineer @Mobius Labs
-
-
- Thanks to dstack, my team can quickly tap into affordable
- GPUs and streamline our workflows
- from testing and development to full-scale application deployment.
-
-
-
-
-
-
-
-
Alvaro Bartolome
-
-
ML Engineer @Argilla
-
-
- With dstack it's incredibly easy to define a configuration
- within a
- repository
- and run it without worrying about GPU availability. It lets you focus on
- data and your research.
-
-
-
-
-
-
-
-
Park Chansung
-
-
ML Researcher @ETRI
-
-
- Thanks to dstack, I can effortlessly access the top GPU
- options across
- different clouds,
- saving me time and money while pushing my AI work forward.
-
-
-
-
-
-
-
-
Eckart Burgwedel
-
-
CEO @Uberchord
-
-
- With dstack, running LLMs on a cloud GPU is as
- easy as running a local Docker container.
- It combines the ease of Docker with the auto-scaling capabilities of K8S.
-
-
-
-
-
-
-
-
Jon Stevens
-
-
CEO @Hot Aisle
-
-
- dstack 's advantages over Slurm are clear: it's a modern, ground-up approach to running workloads at scale. If you're choosing an orchestration platform, dstack is the place to start.
-
-
-
-
-
-
-
-
-
-
-
-
Get started in minutes
-
-
-
-
-
-
- Install dstack on your laptop with uv,
- or deploy it anywhere using the dstackai/dstack Docker image.
-
-
-
Bring your compute via backends or SSH fleets, then bring your team.
+
+ {/* The Old page renders its own footer inside the AppLayout content, so the side nav
+ runs full-height beside it; every other page uses the global footer here. */}
+ {pathname !== ROUTES.OLD && (
+
+ )}
+ >
+ );
+}
diff --git a/website/src/asset.ts b/website/src/asset.ts
new file mode 100644
index 0000000000..4019a7d512
--- /dev/null
+++ b/website/src/asset.ts
@@ -0,0 +1,3 @@
+// Resolve a path to a file in public/ against the configured base URL, so assets work
+// both at the site root (dev) and under the GitHub Pages project subpath in production.
+export const asset = (path: string) => import.meta.env.BASE_URL + path.replace(/^\//, '');
diff --git a/website/src/cloudscape-overrides.css b/website/src/cloudscape-overrides.css
new file mode 100644
index 0000000000..b59e691cb2
--- /dev/null
+++ b/website/src/cloudscape-overrides.css
@@ -0,0 +1,250 @@
+/*
+ Cloudscape internal overrides — the deliberate, isolated exception.
+
+ Everything Cloudscape exposes as a themeable design token lives in cloudscape-theme.ts
+ (the supported `applyTheme` API). This file covers only the few visual details that
+ Cloudscape does NOT expose as tokens, so they have to be set against its generated CSS
+ variables / scoped class names directly.
+
+ MAINTENANCE
+ - The hashed suffixes below (…-uwo8my, …-gudemr, awsui_…_sne0l) are pinned to the
+ installed @cloudscape-design/components version.
+ - After upgrading Cloudscape, re-verify each name. If one drifts, the rule simply stops
+ matching and the component falls back to its Cloudscape default (graceful degradation,
+ not a breakage). Re-derive with:
+ grep -roE '\-\-border-divider-section-width-[a-z0-9]+' node_modules/@cloudscape-design/components/container
+ grep -roE '\-\-color-border-tabs-[a-z-]+-[a-z0-9]+' node_modules/@cloudscape-design/components/tabs
+ grep -roE 'awsui_split-trigger-wrapper_[a-z0-9_]+' node_modules/@cloudscape-design/components/button-dropdown
+*/
+
+/* NB: Cloudscape defines these tokens on `body` inside `@layer awsui-base-theme`. Our
+ redeclarations are unlayered, so they win over Cloudscape's layered ones. The COLOR
+ tokens inherit, so `body` is enough. The border-WIDTH token does NOT inherit, so it
+ has to be declared on the elements that read it — hence the universal selector. */
+
+/* 1) Hairline (0.5px) borders on Container / Tabs / Table rows (and anything else using
+ these divider widths). Border color + radius are themeable (cloudscape-theme.ts); the
+ widths are not, and they don't inherit — so set them on every element via `*`.
+ (section-width: containers/tabs; list-width: table rows + list dividers.) */
+* {
+ --border-divider-section-width-uwo8my: 1px; /* 1px outer border on containers / tabs / tables */
+ --border-divider-list-width-tdfx1x: 0.5px; /* table rows + list dividers stay hairline (exception) */
+}
+
+/* 2) Tabs: the bottom divider and the active-tab underline use the text color.
+ Tabs use tab-specific border-color tokens that the theming API doesn't expose. */
+body {
+ --color-border-tabs-divider-f5t9va: var(--cs-text);
+ --color-border-tabs-underline-gudemr: var(--cs-text);
+ --color-border-dropdown-group-ylcnh8: transparent; /* no divider between dropdown groups */
+ --color-text-expandable-section-hover-ojzwhd: var(--cs-text); /* FAQ: don't turn the question blue on hover */
+}
+
+/* 3) Split ButtonDropdown ("Get started"): close the gap before the arrow (a 2px
+ margin on the main segment) and drop the main action's trailing padding so the
+ label and arrow sit flush. ButtonDropdown has no `style` prop, so this targets its
+ scoped classes by stable prefix (hash-independent). */
+/* !important is required: Cloudscape boosts its own rules' specificity with a :not(#\9)
+ trick, which an attribute selector can't otherwise outrank. */
+[class*='awsui_split-trigger-wrapper'] > [class*='awsui_trigger-item']:not(:last-child) > [class*='awsui_trigger-button'] {
+ margin-inline-end: 0 !important; /* close the 2px gap before the arrow */
+ padding-inline-end: 16px !important; /* breathing room between the label and the divider */
+}
+/* Match the other top-nav buttons' height (see menuButtonStyle, 7px block). */
+[class*='awsui_split-trigger-wrapper'] [class*='awsui_trigger-button'] {
+ padding-block: 7px !important;
+}
+/* Subtle 0.5px divider between the two segments — a hair lighter/darker than the fill
+ so the split reads, without breaking the single-button look. (Hover stays per-segment,
+ which is Cloudscape's default and the better affordance here.) */
+[class*='awsui_split-trigger-wrapper'] > [class*='awsui_trigger-item']:not(:first-child) > [class*='awsui_trigger-button'] {
+ border-inline-start: 0.5px solid var(--cs-seg-divider) !important;
+}
+
+/* 4) Content tabs.
+ a) Full-height vertical separators between tabs (default insets them 12px top/bottom).
+ `inset-block: 0` spans only the container's padding box, leaving a 1px gap at top/bottom
+ against the 1px transparent border; the negative inset extends them over that border so
+ they match the height of the edge separators (real borders on the border box). */
+[class*='awsui_tabs-tab_']:not(:last-child) > [class*='awsui_tabs-tab-header-container']::before {
+ inset-block: calc(-1 * var(--border-divider-section-width-uwo8my, 1px)) !important;
+}
+/* b) Drop the gray border + shadow that Cloudscape shows on the scroll arrows when the
+ tab strip overflows (the box-shadow renders both; border-inline is the divider). */
+[class*='awsui_pagination-button-left-scrollable'],
+[class*='awsui_pagination-button-right-scrollable'] {
+ box-shadow: none !important;
+}
+[class*='awsui_pagination-button-left'],
+[class*='awsui_pagination-button-right'] {
+ border-inline: 0 !important;
+}
+
+/* 5) Dropdown menu popups (the "Get started" and "Resources" menus). Targeted by stable
+ class prefix so the rules apply wherever the popup renders.
+ a) Fixed width, flat (no drop-shadow), and a single uniform 0.5px border on all four
+ sides. By default Cloudscape draws top/bottom on the wrapper (1px) and left/right on
+ a ::after — so we set the wrapper border ourselves and drop the ::after layer. */
+[class*='awsui_dropdown-content-wrapper'] {
+ inline-size: 300px !important;
+ box-shadow: none !important;
+ border: 0.5px solid var(--cs-text) !important;
+ border-radius: 12px !important; /* rounded popup */
+ overflow: hidden; /* clip the menu items to the rounded corners */
+}
+[class*='awsui_dropdown-content-wrapper']::after {
+ border: 0 !important;
+}
+/* b) Group headers ("Products" / "Login"). lighter (300) and a touch smaller
+ (15px) in the full text color (no longer muted). */
+[class*='awsui_header_16mm3'] {
+ font-weight: 300 !important;
+ font-size: 15px !important;
+ color: var(--cs-text) !important;
+ padding-inline: 16px !important;
+}
+/* c) Items: bold label (matching the group weight), tighter horizontal padding aligned
+ with the header. The description below keeps its normal/muted styling. */
+[class*='awsui_menu-item'] {
+ padding-inline: 16px !important;
+}
+[class*='awsui_menu-item'] [class*='awsui_main-row'] {
+ font-weight: 600;
+ font-size: 15px; /* (Cloudscape's default popup label is 14px) */
+}
+/* The hovered item still picked up a border in dark mode (the token override didn't hold
+ there), so force it off on the highlighted item itself (the cue is the bg tint). */
+[class*='awsui_item-element'][class*='awsui_highlighted'] {
+ border-color: transparent !important;
+}
+/* Popup item descriptions: normal text color (not muted), 13px / weight 300. A hair of
+ separation (1.5px) from the label above so the two lines don't read as one block. */
+[class*='awsui_secondary-text'] {
+ color: var(--cs-text) !important;
+ margin-block-start: 1.5px;
+ font-size: 13px !important;
+ font-weight: 300 !important;
+}
+/* A little breathing room at the top and bottom of the popup (inside the border). Placed on
+ the first/last rows rather than on the list itself, so a hovered first/last item's
+ background fills that space instead of leaving a thin un-highlighted strip against the
+ border (the hover tint is painted on the item-element, which is what carries the padding). */
+[class*='awsui_options-list'] {
+ padding-block: 0 !important;
+ /* Cloudscape pulls the list 1px into the wrapper border (decrease-block-margin) to overlap
+ its default 1px divider. With our single hairline border that just lets a hovered
+ first/last item's fill paint over the border (most visible in dark mode) — so sit the
+ list flush inside the border instead. */
+ margin-block: 0 !important;
+}
+[class*='awsui_options-list'] > :first-child {
+ padding-block-start: 6px !important;
+}
+/* Last item — flat list (ButtonDropdown without groups, e.g. the Resources menu). */
+[class*='awsui_options-list'] > [class*='awsui_item-element']:last-child {
+ padding-block-end: 6px !important;
+}
+/* Last item — grouped list: the last item inside the last category (e.g. "Get started"). */
+[class*='awsui_options-list'] > [class*='awsui_category']:last-child [class*='awsui_item-element']:last-child {
+ padding-block-end: 6px !important;
+}
+/* d) Push the external-link icon to the right edge of the item. It's rendered inline at
+ the end of the label, so make the label row fill the width and flex the icon out.
+ Scoped under `awsui_main-row` (dropdown-item only) so it doesn't affect the external
+ icons in SideNavigation, which share the `awsui_external-icon` class. */
+[class*='awsui_main-row'] > :first-child {
+ display: flex !important;
+ flex: 1 1 auto !important;
+ align-items: center;
+}
+[class*='awsui_main-row'] [class*='awsui_external-icon'] {
+ margin-inline-start: auto !important;
+}
+
+/* 6) FAQ accordion (ExpandableSection): faint background tint on hover that covers the
+ whole block (question + answer) uniformly. Tint the section root, then neutralize
+ every inner background (header + content both carry their own white bg) so only the
+ single root tint shows — otherwise an opaque child paints over it and the question
+ ends up with an extra highlight vs the answer. FAQ items are plain text, so blanking
+ inner backgrounds is safe. */
+.faq-list [class*='awsui_root']:hover {
+ background: var(--cs-hover) !important;
+}
+.faq-list [class*='awsui_root']:hover * {
+ background: transparent !important;
+}
+
+/* 7) Testimonial quote cards. solid 0.5px (was dotted 0.25px); radius comes from
+ the container token (12px). */
+.testimonial-grid [class*='awsui_fit-height'] {
+ border-style: solid !important;
+ border-width: 0.5px !important;
+}
+
+/* 8) Mobile slide-out navigation (SideNavigation): render the expandable section headers
+ ("Resources" / "Get started") in the same weight and size as their sub-links, instead
+ of bold, for a flatter menu. Scoped to the mobile drawer; `awsui_header-text` is a
+ stable, hash-independent prefix. */
+.site-mobile-navigation [class*='awsui_header-text'] {
+ font-weight: 400 !important;
+ font-size: 14px !important;
+}
+
+/* 9) Content tabs: highlight the selected (and hovered) tab with a background
+ tint instead of blue text + an underline indicator. The background uses the same tint as
+ the dropdown popup items (--cs-hover), so every hover surface matches. */
+/* a) The under-tab horizontal divider and the vertical tab separators keep the 1px outer
+ border width (they inherit --border-divider-section-width, no override needed here). */
+/* b) Drop the per-tab active underline indicator — selection is now shown by the background
+ (this also removes that indicator's rounded ends). */
+[class*='awsui_tabs-tab-header-container']::after {
+ display: none !important;
+}
+/* c) Keep tab labels in the body color at rest, on hover, and when selected (no blue accent). */
+[class*='awsui_tabs-tab-link'] {
+ color: var(--cs-text) !important;
+}
+/* d) Background highlight for the hovered and selected tab — painted on the whole tab cell
+ (the header container fills edge to edge; the inner link has padding-inline:0, so painting
+ the link alone leaves gaps). aria-selected lives on the inner link, hence :has(). */
+[class*='awsui_tabs-tab-header-container']:hover,
+[class*='awsui_tabs-tab-header-container']:has([aria-selected='true']) {
+ background: var(--cs-hover) !important;
+}
+/* e) 1px vertical separators bounding the strip (Cloudscape only draws them between tabs).
+ After the last tab: always. Before the first tab: ONLY when the strip overflows (a
+ scroll arrow is present). With no scrolling the first tab sits against the container's
+ left border, so a leading separator just reads as a doubled line — so we omit it. */
+[class*='awsui_tabs-tab_']:last-child > [class*='awsui_tabs-tab-header-container'] {
+ border-inline-end: 1px solid var(--color-border-tabs-divider-f5t9va) !important;
+}
+[class*='awsui_tab-header-scroll-container']:has([class*='pagination-button-left-scrollable'], [class*='pagination-button-right-scrollable'])
+ [class*='awsui_tabs-tab_']:first-child
+ > [class*='awsui_tabs-tab-header-container'] {
+ border-inline-start: 1px solid var(--color-border-tabs-divider-f5t9va) !important;
+}
+
+/* 10) GPU table: 0.5px row separators that run edge to edge, while keeping the
+ container's breathing room. The Container padding lives on `content-inner` (the
+ `with-paddings` element). We drop only its INLINE padding so the table — and therefore
+ the row separators — spans the full width; the top/bottom padding is kept. A 20px inset
+ is then restored on just the outer cells so the text isn't flush against the border. */
+.gpu-scroll * {
+ --border-divider-list-width-tdfx1x: 0.5px;
+}
+[class*='awsui_content-inner']:has(.gpu-scroll) {
+ padding-inline: 0 !important;
+ padding-block: 8px !important; /* a little top/bottom breathing room, trimmed from the default */
+}
+.gpu-scroll tr > :first-child {
+ padding-inline-start: 20px !important;
+}
+.gpu-scroll tr > :last-child {
+ padding-inline-end: 20px !important;
+}
+
+/* 11) Thin (0.5px) dividers between stacked FAQ items. The block's outer corners
+ are clipped to 12px by .faq-list; these are the internal separators. */
+.faq-list * {
+ --border-divider-section-width-uwo8my: 0.5px;
+}
diff --git a/website/src/cloudscape-theme.ts b/website/src/cloudscape-theme.ts
new file mode 100644
index 0000000000..18d98325f9
--- /dev/null
+++ b/website/src/cloudscape-theme.ts
@@ -0,0 +1,114 @@
+// dstack's visual customization of Cloudscape, expressed through the official theming
+// API (@cloudscape-design/components/theming). We only override design tokens here — no
+// CSS reaching into Cloudscape's internal (hashed) class/variable names — so this stays
+// on the supported path and survives Cloudscape upgrades. Applied once before the first
+// render (see index.tsx), which is the supported way to avoid a flash of the base theme.
+import { applyTheme, Theme } from '@cloudscape-design/components/theming';
+import type { ButtonProps } from '@cloudscape-design/components/button';
+
+// Mirrors the --cs-* palette in styles.css (light/dark). Token values accept either a
+// single string or a { light, dark } pair, which Cloudscape maps to its color modes.
+const TEXT = { light: '#16191f', dark: '#f2f3f3' }; // body text color
+const SURFACE = { light: '#ffffff', dark: '#0f141d' }; // page background — used as the label color on filled buttons
+// Hover states, light + theme-aware: filled (primary) buttons shift to a slightly softer
+// shade; outlined (normal) buttons, cards, and dropdown items all share one faint tint
+// (--cs-hover, defined per theme in styles.css) so every interactive surface hovers alike.
+const HOVER_FILL = 'var(--cs-btn-hover)'; // filled-button hover fill (defined per theme in styles.css; shared with the split-button override)
+const FONT = 'var(--font-base)'; // single source of truth: the Geist stack defined in styles.css
+
+const tokens = {
+ // Typography: render Cloudscape components in Geist (replaces the hashed-token CSS
+ // override that used to live in styles.css). Monospace is left as-is for code.
+ fontFamilyBase: FONT,
+ fontFamilyHeading: FONT,
+ fontFamilyDisplay: FONT,
+
+ // rounded corners on buttons + containers (tabs/tables/quotes). 12px.
+ borderRadiusButton: '12px',
+ borderRadiusContainer: '12px',
+ borderRadiusInput: '0px',
+ borderRadiusDropdown: '0px',
+ borderRadiusItem: '0px',
+ borderRadiusBadge: '0px',
+ borderRadiusAlert: '0px',
+ borderRadiusPopover: '0px',
+ borderRadiusTiles: '0px',
+ borderRadiusCardDefault: '12px',
+ borderRadiusCardEmbedded: '12px',
+ borderRadiusActionCardDefault: '12px',
+ borderRadiusActionCardEmbedded: '12px',
+ borderRadiusFlashbar: '0px',
+ borderRadiusDropzone: '0px',
+ borderRadiusTutorialPanelItem: '0px',
+ borderRadiusStatusIndicator: '0px',
+ borderRadiusToken: '0px',
+
+ // Hairline borders on structural controls. Icon stroke widths (borderWidthIcon*) are
+ // intentionally left alone — those drive icon rendering, not container borders.
+ borderWidthButton: '1px', /* 1px borders */
+ borderWidthField: '0.5px',
+ borderWidthDropdown: '0.5px',
+ borderWidthPopover: '0.5px',
+ borderWidthCard: '1px', /* 1px borders */
+
+ // Neutral borders/dividers take the text color instead of gray. Semantic borders
+ // (status, badges, selected/focused) keep their defaults so they still read as such.
+ colorBorderDividerDefault: TEXT,
+ colorBorderDividerSecondary: TEXT,
+ colorBorderInputDefault: TEXT,
+ colorBorderControlDefault: TEXT,
+ colorBorderContainerTop: TEXT,
+ colorBorderDropdownContainer: TEXT,
+ colorBorderPopover: TEXT,
+ colorBorderLayout: TEXT,
+ colorBorderCard: TEXT,
+ colorBorderDialog: TEXT,
+ colorBorderExpandableSectionDefault: TEXT,
+ // Dropdown menu items: no outline box on the hovered/focused row; the cue is a faint
+ // background tint (matching the cards/buttons) via --cs-hover.
+ colorBorderDropdownItemHover: 'transparent',
+ colorBorderDropdownItemFocused: 'transparent',
+ colorBackgroundDropdownItemHover: 'var(--cs-hover)',
+
+ // Primary button: filled in the text color, with the label in the surface color.
+ colorBackgroundButtonPrimaryDefault: TEXT,
+ colorBackgroundButtonPrimaryHover: HOVER_FILL,
+ colorBackgroundButtonPrimaryActive: HOVER_FILL,
+ colorBorderButtonPrimaryDefault: TEXT,
+ colorBorderButtonPrimaryHover: HOVER_FILL,
+ colorBorderButtonPrimaryActive: HOVER_FILL,
+ colorTextButtonPrimaryDefault: SURFACE,
+ colorTextButtonPrimaryHover: SURFACE,
+ colorTextButtonPrimaryActive: SURFACE,
+
+ // Normal (secondary) button: outlined, transparent by default; on hover/active a faint
+ // tint (not a solid fill). Border + label stay the text color.
+ colorBackgroundButtonNormalDefault: 'transparent',
+ colorBackgroundButtonNormalHover: 'var(--cs-hover)',
+ colorBackgroundButtonNormalActive: 'var(--cs-hover)',
+ colorBorderButtonNormalDefault: TEXT,
+ colorBorderButtonNormalHover: TEXT,
+ colorBorderButtonNormalActive: TEXT,
+ colorTextButtonNormalDefault: TEXT,
+ colorTextButtonNormalHover: TEXT,
+ colorTextButtonNormalActive: TEXT,
+} satisfies Theme['tokens'];
+
+// Apply on import. index.tsx imports this module for its side effect (alongside the CSS
+// imports) so the theme is in place before the first render — no flash of the base theme.
+applyTheme({ theme: { tokens } });
+
+// Context-specific button padding. Cloudscape has no global button-padding token, so this
+// uses the Button `style` prop (the supported per-instance route). Padding scales x and y
+// proportionally: hero buttons are the most generous, main-area buttons a step below.
+// (Transparent backgrounds for normal buttons are handled globally by the tokens above.)
+export const heroButtonStyle: ButtonProps.Style = {
+ root: { paddingBlock: '10px', paddingInline: '30px' }, // trimmed a touch from 12/34
+};
+export const mainButtonStyle: ButtonProps.Style = {
+ root: { paddingBlock: '8px', paddingInline: '26px' },
+};
+// Top-nav buttons — slightly roomier than default but compact.
+export const menuButtonStyle: ButtonProps.Style = {
+ root: { paddingBlock: '7px', paddingInline: '18px' },
+};
diff --git a/website/src/components/AlternatingDocBlock.tsx b/website/src/components/AlternatingDocBlock.tsx
new file mode 100644
index 0000000000..4fea813ff9
--- /dev/null
+++ b/website/src/components/AlternatingDocBlock.tsx
@@ -0,0 +1,47 @@
+import { ReactNode } from 'react';
+import { ThemedImage } from '../data/images';
+import { highlightTerms } from './highlightTerms';
+
+// In-content diagram: a plain string renders a single image, a themed pair swaps with the theme.
+function ThemedDocImage({ image }: { image: string | ThemedImage }) {
+ if (typeof image === 'string') {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+// A documentation block with a visual on one side and copy on the other. Pass either
+// `image` (rendered via ThemedDocImage) or an arbitrary `visual` node. `imageFirst`
+// places the visual on the left, otherwise it sits on the right.
+export function AlternatingDocBlock({
+ image,
+ visual,
+ title,
+ children,
+ action,
+ imageFirst = false,
+}: {
+ image?: string | ThemedImage;
+ visual?: ReactNode;
+ title: string;
+ children: ReactNode;
+ action?: ReactNode;
+ imageFirst?: boolean;
+}) {
+ return (
+
+
{visual ?? (image && )}
+
+
{title}
+
{highlightTerms(children)}
+ {action &&
{action}
}
+
+
+ );
+}
diff --git a/website/src/components/ArchitectureDiagram.tsx b/website/src/components/ArchitectureDiagram.tsx
new file mode 100644
index 0000000000..77e53d6853
--- /dev/null
+++ b/website/src/components/ArchitectureDiagram.tsx
@@ -0,0 +1,127 @@
+import { asset } from '../asset';
+import { DashedBorder } from './DashedBorder';
+
+// Layered "vendor-agnostic" architecture diagram, rebuilt as HTML/CSS (replaces the previous
+// static SVG). Logos are recolored to the current text color via CSS masking (see .arch-logo in
+// styles.css) so they read monochrome and flip with the light/dark theme. Per-logo size/aspect
+// lives in CSS (.arch-logo--); only the mask image URL is set inline, since it must carry
+// the runtime base path (asset()).
+
+type Logo = { key: string; label: string; src?: string; initials?: string };
+
+const logoSrc = (file: string) => asset(`/static/logos/${file}`);
+
+const FRAMEWORKS: Logo[] = [
+ { key: 'pytorch', label: 'PyTorch', src: logoSrc('pytorch.svg') },
+ { key: 'vllm', label: 'vLLM', src: logoSrc('vllm.svg') },
+ { key: 'sglang', label: 'SGLang', src: logoSrc('sglang.svg') },
+ { key: 'meta', label: 'Meta', src: logoSrc('meta.svg') },
+ { key: 'huggingface', label: 'Hugging Face', src: logoSrc('huggingface.svg') },
+];
+
+const GPU_CLOUDS: Logo[] = [
+ { key: 'aws', label: 'AWS', src: logoSrc('aws.svg') },
+ { key: 'gcp', label: 'Google Cloud', src: logoSrc('gcp.svg') },
+ { key: 'lambda', label: 'Lambda', src: logoSrc('lambda.svg') },
+ { key: 'nebius', label: 'Nebius', src: logoSrc('nebius.svg') },
+ { key: 'runpod', label: 'RunPod', src: logoSrc('runpod.svg') },
+];
+
+const KUBERNETES: Logo = { key: 'kubernetes', label: 'Kubernetes', src: logoSrc('kubernetes.svg') };
+
+const HARDWARE: Logo[] = [
+ { key: 'nvidia', label: 'NVIDIA', src: logoSrc('nvidia.svg') },
+ { key: 'amd', label: 'AMD', src: logoSrc('amd.webp') },
+ { key: 'tenstorrent', label: 'Tenstorrent', src: logoSrc('tenstorrent.svg') },
+ { key: 'tpu', label: 'Google TPU', src: logoSrc('gcp.svg') }, // TPU shares the GCP mark
+];
+
+function LogoMark({ logo }: { logo: Logo }) {
+ if (logo.src) {
+ return (
+
+ );
+ }
+ return (
+
+ {logo.initials}
+
+ );
+}
+
+function LogoRow({ logos }: { logos: Logo[] }) {
+ return (
+
+ {logos.map(logo => (
+
+ ))}
+
+ );
+}
+
+export function ArchitectureDiagram() {
+ return (
+
+
+ {/* Top: what plugs in on top of the orchestration layer */}
+
+
+
+ Any framework
+
+
+
+
+ Your data
+
+
+
+ Any models
+
+
+
+ {/* Middle: the orchestration layer itself */}
+
+ );
+}
diff --git a/website/src/components/DashedBorder.tsx b/website/src/components/DashedBorder.tsx
new file mode 100644
index 0000000000..8c1932c5b0
--- /dev/null
+++ b/website/src/components/DashedBorder.tsx
@@ -0,0 +1,11 @@
+// Dotted outline shared by the architecture diagram and the "AI-native orchestration" concept
+// cards: a single rounded rect stroked with the exact 2-on / 6-off dash. Geometry, color, and
+// radius live in CSS (.arch-dash / .arch-dash rect in styles.css), so it follows the element's
+// rounded corners, stays crisp at any size, and takes the current text color (theme-adaptive).
+export function DashedBorder() {
+ return (
+
+ );
+}
diff --git a/website/src/components/SiteBanner.tsx b/website/src/components/SiteBanner.tsx
new file mode 100644
index 0000000000..d092e8a4eb
--- /dev/null
+++ b/website/src/components/SiteBanner.tsx
@@ -0,0 +1,33 @@
+import { BLOG_URL } from '../routes';
+
+// Top announcement banner, mirroring the one on the MkDocs docs site. It sits above the top
+// nav; the two stick to the top together (see .site-header). Update the copy/href here when
+// the announcement changes.
+const BANNER_TEXT = 'Infrastructure orchestration is an agent skill';
+const BANNER_HREF = `${BLOG_URL}/agentic-orchestration/`;
+
+export function SiteBanner() {
+ return (
+
+ );
+}
diff --git a/website/src/components/SiteFooter.tsx b/website/src/components/SiteFooter.tsx
new file mode 100644
index 0000000000..a836819096
--- /dev/null
+++ b/website/src/components/SiteFooter.tsx
@@ -0,0 +1,145 @@
+import Button from '@cloudscape-design/components/button';
+import Link from '@cloudscape-design/components/link';
+import SpaceBetween from '@cloudscape-design/components/space-between';
+import { asset } from '../asset';
+import { BLOG_URL, docsUrl, PRIVACY_URL, TERMS_URL } from '../routes';
+import { ThemeMode } from '../theme';
+
+type FooterLinkItem = { label: string; href: string; external?: boolean };
+type FooterColumn = { heading: string; links: FooterLinkItem[] };
+
+// Brand-area social links (single-color SVG paths drawn in currentColor), matching the
+// icons configured for the MkDocs site footer.
+const socialLinks: { label: string; href: string; path: string }[] = [
+ {
+ label: 'GitHub',
+ href: 'https://github.com/dstackai/dstack',
+ path: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
+ },
+ {
+ label: 'Discord',
+ href: 'https://discord.gg/u8SmfwPpMd',
+ path: 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z',
+ },
+ {
+ label: 'X',
+ href: 'https://x.com/dstackai',
+ path: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z',
+ },
+ {
+ label: 'LinkedIn',
+ href: 'https://www.linkedin.com/company/dstackai',
+ path: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
+ },
+];
+
+// Link columns for the footer, mirroring the dstack.ai site footer. Doc/blog/legal links
+// resolve through the route table (same origin in production, VITE_DOCS_BASE in dev).
+const footerColumns: FooterColumn[] = [
+ {
+ heading: 'Documentation',
+ links: [
+ { label: 'Getting started', href: docsUrl('installation') },
+ { label: 'Concepts', href: docsUrl('concepts/backends/') },
+ { label: 'Guides', href: docsUrl('guides/protips/') },
+ { label: 'Reference', href: docsUrl('reference/dstack.yml/dev-environment/') },
+ ],
+ },
+ {
+ heading: 'Examples',
+ links: [
+ { label: 'Training', href: docsUrl('examples/training/trl/') },
+ { label: 'Clusters', href: docsUrl('examples/clusters/aws/') },
+ { label: 'Inference', href: docsUrl('examples/inference/sglang/') },
+ { label: 'Models', href: docsUrl('examples/models/deepseek-v4/') },
+ { label: 'Accelerators', href: docsUrl('examples/accelerators/amd/') },
+ ],
+ },
+ {
+ heading: 'Community',
+ links: [
+ { label: 'GitHub', href: 'https://github.com/dstackai/dstack', external: true },
+ { label: 'Discord', href: 'https://discord.gg/u8SmfwPpMd', external: true },
+ ],
+ },
+ {
+ heading: 'Company',
+ links: [
+ { label: 'Blog', href: BLOG_URL },
+ { label: 'Talk to us', href: 'https://calendly.com/dstackai/discovery-call', external: true },
+ { label: 'Terms of service', href: TERMS_URL },
+ { label: 'Privacy policy', href: PRIVACY_URL },
+ ],
+ },
+];
+
+// Global footer: a brand block (logo + social) reserving the leading ~1.5 columns,
+// then the multi-column link grid, with the theme toggle and copyright in a
+// divider-separated bottom bar. On the home page it carries an extra gradient
+// (.site-footer--home).
+export function SiteFooter({
+ home,
+ theme,
+ onToggleTheme,
+}: {
+ home: boolean;
+ theme: ThemeMode;
+ onToggleTheme: () => void;
+}) {
+ return (
+
+ );
+}
diff --git a/website/src/components/SiteNavigation.tsx b/website/src/components/SiteNavigation.tsx
new file mode 100644
index 0000000000..7c0dd18668
--- /dev/null
+++ b/website/src/components/SiteNavigation.tsx
@@ -0,0 +1,231 @@
+import { useRef, useState } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+import Button from '@cloudscape-design/components/button';
+import ButtonDropdown, { ButtonDropdownProps } from '@cloudscape-design/components/button-dropdown';
+import SideNavigation, { SideNavigationProps } from '@cloudscape-design/components/side-navigation';
+import SpaceBetween from '@cloudscape-design/components/space-between';
+import { menuButtonStyle } from '../cloudscape-theme';
+import { asset } from '../asset';
+import { BLOG_URL, DOCS_URL, ROUTES, docsUrl } from '../routes';
+
+const dstackGithubUrl = 'https://github.com/dstackai/dstack';
+const externalIconAriaLabel = 'External link icon';
+
+// Primary links in the desktop top navigation (plain same-origin MkDocs links).
+const audienceNavItems: Array<{ label: string; href: string }> = [
+ { label: 'Documentation', href: DOCS_URL },
+];
+
+// "Resources" top-nav dropdown: the blog landing plus its two main categories (all
+// same-origin MkDocs pages).
+const resourcesDropdownItems: ButtonDropdownProps.Items = [
+ {
+ id: 'case-studies',
+ text: 'Case studies',
+ secondaryText: 'How AI teams run training and inference with dstack.',
+ href: `${BLOG_URL}/case-studies/`,
+ },
+ {
+ id: 'benchmarks',
+ text: 'Benchmarks',
+ secondaryText: 'Comparing hardware, inference engines, and deployment setups for AI.',
+ href: `${BLOG_URL}/benchmarks/`,
+ },
+ {
+ id: 'blog',
+ text: 'Blog',
+ secondaryText: 'Major releases, industry reports, and product updates.',
+ href: BLOG_URL,
+ },
+];
+
+// "Resources" dropdown that opens on hover and stays open while the cursor is over the
+// trigger OR the popup (the popup renders inside this wrapper, so hovering it still counts as
+// hovering the wrapper). Cloudscape's ButtonDropdown is click-only, so we open/close it by
+// reading aria-expanded and synthesizing a click on the trigger — click and keyboard keep
+// working unchanged. A short close delay bridges the gap between trigger and popup so moving
+// the cursor across it doesn't dismiss the menu. Desktop top-nav only (mobile uses SideNavigation).
+function ResourcesHoverMenu() {
+ const wrapRef = useRef(null);
+ const closeTimer = useRef(undefined);
+
+ const trigger = () => wrapRef.current?.querySelector('button') ?? null;
+ const isOpen = () => trigger()?.getAttribute('aria-expanded') === 'true';
+ const cancelClose = () => {
+ if (closeTimer.current !== undefined) {
+ window.clearTimeout(closeTimer.current);
+ closeTimer.current = undefined;
+ }
+ };
+ const openNow = () => {
+ cancelClose();
+ if (!isOpen()) trigger()?.click();
+ };
+ const closeSoon = () => {
+ cancelClose();
+ closeTimer.current = window.setTimeout(() => {
+ if (isOpen()) trigger()?.click();
+ }, 140);
+ };
+
+ return (
+
+
+ Resources
+
+
+ );
+}
+
+// "Get started" dropdown items. secondaryText is shown under each label.
+const productDropdownItems: ButtonDropdownProps.Items = [
+ {
+ text: 'Products',
+ items: [
+ { id: 'open-source', text: 'dstack', secondaryText: 'The open-source control plane that works across clouds, Kubernetes, and on-prem.', href: docsUrl('installation') },
+ { id: 'sky-product', text: 'dstack Sky', secondaryText: 'Access GPU marketplace, or bring your own clouds. Hosted by us.', href: 'https://sky.dstack.ai', external: true, externalIconAriaLabel },
+ { id: 'enterprise', text: 'dstack Enterprise', secondaryText: 'Self-hosted with SSO, air-gapped setup, dedicated support, and more.', href: 'https://calendly.com/dstackai/discovery-call', external: true, externalIconAriaLabel },
+ ],
+ },
+ {
+ text: 'Login',
+ items: [
+ { id: 'sky-login', text: 'dstack Sky', href: 'https://sky.dstack.ai', external: true, externalIconAriaLabel },
+ ],
+ },
+];
+
+// Items for the mobile slide-out navigation.
+const mobileNavigationItems: SideNavigationProps.Item[] = [
+ { type: 'link', text: 'Documentation', href: DOCS_URL },
+ // The desktop "Resources" dropdown becomes an expandable section on mobile (SideNavigation
+ // has no popups), matching the "Get started" section pattern below.
+ {
+ type: 'section',
+ text: 'Resources',
+ defaultExpanded: true,
+ items: [
+ { type: 'link', text: 'Case studies', href: `${BLOG_URL}/case-studies/` },
+ { type: 'link', text: 'Benchmarks', href: `${BLOG_URL}/benchmarks/` },
+ { type: 'link', text: 'Blog', href: BLOG_URL },
+ ],
+ },
+ { type: 'link', text: 'GitHub', href: dstackGithubUrl, external: true, externalIconAriaLabel },
+ {
+ type: 'section',
+ text: 'Get started',
+ defaultExpanded: true,
+ items: [
+ { type: 'link', text: 'dstack', href: docsUrl('installation'), external: true, externalIconAriaLabel },
+ { type: 'link', text: 'dstack Sky', href: 'https://sky.dstack.ai', external: true, externalIconAriaLabel },
+ { type: 'link', text: 'dstack Enterprise', href: 'https://calendly.com/dstackai/discovery-call', external: true, externalIconAriaLabel },
+ ],
+ },
+];
+
+// Global top navigation. On the Old page it also renders the trigger that toggles
+// that page's side navigation drawer (state owned by the App layout).
+export function SiteNavigation({
+ oldNavigationOpen,
+ onToggleOldNavigation,
+}: {
+ oldNavigationOpen: boolean;
+ onToggleOldNavigation: () => void;
+}) {
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+ const [mobileNavigationOpen, setMobileNavigationOpen] = useState(false);
+
+ const isHome = pathname === ROUTES.HOME;
+ const isOldPage = pathname === ROUTES.OLD;
+
+ const go = (to: string) => {
+ navigate(to);
+ setMobileNavigationOpen(false);
+ };
+
+ // Scroll to the "Get started" section, navigating home first if we're on another page.
+ const scrollToResources = () => {
+ const target = document.getElementById('resources');
+ if (target) {
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ return;
+ }
+
+ go(ROUTES.HOME);
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ document.getElementById('resources')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ });
+ };
+
+ return (
+
+
+ {isOldPage && (
+
+
+
+ )}
+
+
+
+
+
+
+ {mobileNavigationOpen && (
+
+ setMobileNavigationOpen(false)}
+ />
+
+ )}
+
+ );
+}
diff --git a/website/src/components/highlightTerms.tsx b/website/src/components/highlightTerms.tsx
new file mode 100644
index 0000000000..94cf5c098f
--- /dev/null
+++ b/website/src/components/highlightTerms.tsx
@@ -0,0 +1,37 @@
+import { Fragment, ReactNode } from 'react';
+
+// Terms wrapped with the .highlight outline in body copy (never headers or menus — those just
+// don't call this). Order matters: longer / compound terms come first so e.g. "dstack Sky" and
+// "dstackai/dstack" win over a bare "dstack". Case-sensitive, each bounded by \b so we don't
+// match inside larger words (e.g. "uv" won't hit "uvicorn", "pip" won't hit "pipeline").
+const TERMS_RE =
+ /\bdstackai\/dstack\b|\bdstack Sky\b|\bdstack\b|\bKubernetes\b|\bTenstorrent\b|\bNVIDIA\b|\bSlurm\b|\bAMD\b|\bTPU\b|\buv\b|\bpip\b/g;
+
+function markString(text: string): ReactNode {
+ const parts: ReactNode[] = [];
+ let lastIndex = 0;
+ let match: RegExpExecArray | null;
+ TERMS_RE.lastIndex = 0;
+ while ((match = TERMS_RE.exec(text)) !== null) {
+ if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
+ parts.push(
+
+ {match[0]}
+ ,
+ );
+ lastIndex = match.index + match[0].length;
+ }
+ if (parts.length === 0) return text;
+ if (lastIndex < text.length) parts.push(text.slice(lastIndex));
+ return parts;
+}
+
+// Walks a node tree and outlines the known terms inside string leaves. Non-string nodes (e.g.
+// ) pass through untouched; element children aren't recursed into. Apply to body copy only.
+export function highlightTerms(node: ReactNode): ReactNode {
+ if (typeof node === 'string') return markString(node);
+ if (Array.isArray(node)) {
+ return node.map((child, i) => {highlightTerms(child)});
+ }
+ return node;
+}
diff --git a/website/src/data/images.ts b/website/src/data/images.ts
new file mode 100644
index 0000000000..c525c5b889
--- /dev/null
+++ b/website/src/data/images.ts
@@ -0,0 +1,26 @@
+// Image assets used across the site. Local SVGs live in /public/static; the architecture
+// diagram is served from the dstack static-assets host; the Old-page placeholders are
+// pulled from the Cloudscape foundation image set.
+import { asset } from '../asset';
+
+const img = (path: string) => `https://cloudscape.design${path}`;
+
+export const images = {
+ // Home hero artwork (light/dark variants).
+ hero: {
+ light: asset('/static/dstack-gpu-artwork.svg'),
+ dark: asset('/static/dstack-gpu-artwork-dark.svg'),
+ },
+ // (The "Vendor-agnostic, open-source" architecture diagram is now an HTML/CSS component —
+ // see components/ArchitectureDiagram.tsx — not an image.)
+ // Old page imagery (kept for comparison / as a template for future product pages).
+ meet: img('/__images/yvlrib0vb3vb/3RkANdWu0IRLpTcBJYSPg5/2397551327a83cfbddd1fe4db9f58188/homepage--meet-cloudscape--os-light.png'),
+ familiar: img('/__images/yvlrib0vb3vb/3CJGtMGSx07lhdtgwL8Ncb/0e33dc1bac3936239e2bc856ee268e80/homepage--get-familiar-with-system--os-light.png'),
+ github: img('/__images/yvlrib0vb3vb/5WFt79VY8lv19rULhh7kss/a47ddc4dceb91f020910445feeebc306/homepage--cloudscape-on-github--os-light.png'),
+ mode: img('/__images/yvlrib0vb3vb/6dN8hKQOPLKko1MdoXZWXc/b2a5390038d3a7e2604c2550a12c5641/foundation-overview--visual-modes--core-light.png'),
+ theming: img('/__images/yvlrib0vb3vb/2GUtUNLAIC2tfIePMVbM4z/f054770207e01200a6e03f140c838437/foundation-overview--theming--os-light.png'),
+ accessibility: img('/__images/yvlrib0vb3vb/1Bv6Ju9XwvoRt4YzZys5eI/cdc470297880845185849edb739cb5b6/foundation-overview--accessibility--vr-light.png'),
+ responsive: img('/__images/yvlrib0vb3vb/4AYh8LuIrJO3AN0S0jtTzB/906ff95fd94ec7b20891e7327d1fc53f/foundation-overview--responsive-design--vr-light.png'),
+} as const;
+
+export type ThemedImage = { light: string; dark: string };
diff --git a/website/src/data/snippets.ts b/website/src/data/snippets.ts
new file mode 100644
index 0000000000..23d10dff69
--- /dev/null
+++ b/website/src/data/snippets.ts
@@ -0,0 +1,228 @@
+// Code snippets shown in the home page tab groups: cloud backends and on-prem
+// clusters (YAML), plus the open-source install commands (shell). The first line
+// of each YAML snippet is a comment naming the file it represents.
+const BACKEND_HEADER = '# ~/.dstack/server/config.yml';
+const FLEET_HEADER = '# my-fleet.dstack.yml';
+
+export const backendConfigs = (
+ [
+ {
+ id: 'aws',
+ label: 'AWS',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: aws
+ creds:
+ type: access_key
+ access_key: KKAAUKLIZ5EHKICAOASV
+ secret_key: pn158lMqSBJiySwpQ9ubwmI6VUU3/W2fdJdFwfgO`,
+ },
+ {
+ id: 'gcp',
+ label: 'GCP',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: gcp
+ project_id: my-gcp-project
+ creds:
+ type: service_account
+ filename: ~/.dstack/server/gcp-024ed630eab5.json`,
+ },
+ {
+ id: 'lambda',
+ label: 'Lambda',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: lambda
+ creds:
+ type: api_key
+ api_key: eersct_yrpiey-naaeedst-tk-_cb6ba38e1128464aea9bcc619e4ba2a5.iijPMi07obgt6TZ87v5qAEj61RVxhd0p`,
+ },
+ {
+ id: 'nebius',
+ label: 'Nebius',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: nebius
+ creds:
+ type: service_account
+ service_account_id: serviceaccount-e00dhnv9ftgb3cqmej
+ public_key_id: publickey-e00ngaex668htswqy4
+ private_key_file: ~/path/to/key.pem`,
+ },
+ {
+ id: 'crusoe',
+ label: 'Crusoe',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: crusoe
+ project_id: my-crusoe-project
+ creds:
+ type: access_key
+ access_key: CRUSOE3X9PLANROAQ2KZ
+ secret_key: 8fJqV2mWcR7tY1nB6xD4eL0pS5gH3aZ9uK7iO2wQ`,
+ },
+ {
+ id: 'runpod',
+ label: 'RunPod',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: runpod
+ creds:
+ type: api_key
+ api_key: US9XTPDIV8AR42MMINY8TCKRB8S4E7LNRQ6CAUQ9`,
+ },
+ {
+ id: 'azure',
+ label: 'Azure',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: azure
+ subscription_id: 06c82ce3-28ff-4285-a146-c5e981a9d808
+ tenant_id: f84a7584-88e4-4fd2-8e97-623f0a715ee1
+ creds:
+ type: client
+ client_id: acf3f73a-597b-46b6-98d9-748d75018ed0
+ client_secret: 1Kb8Q~o3Q2hdEvrul9yaj5DJDFkuL3RG7lger2VQ`,
+ },
+ {
+ id: 'verda',
+ label: 'Verda',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: verda
+ creds:
+ type: api_key
+ client_id: xfaHBqYEsArqhKWX-e52x3HH7w8T
+ client_secret: B5ZU5Qx9Nt8oGMlmMhNI3iglK8bjMhagTbylZy4WzncZe39995f7Vxh8`,
+ },
+ {
+ id: 'digitalocean',
+ label: 'Digital Ocean',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: digitalocean
+ project_name: my-digital-ocean-project
+ creds:
+ type: api_key
+ api_key: dop_v1_examplekey3f8a1c9e2b7d6045a1c8e3f9b2d7a6c4e1f0b9d8`,
+ },
+ {
+ id: 'jarvislabs',
+ label: 'JarvisLabs',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: jarvislabs
+ creds:
+ type: api_key
+ api_key: jlab_8Kd2Pq7Vn4Rt9Wm3Xy6Zb1Lc5Fg0HsJ4Tn7Vr2Wq9`,
+ },
+ {
+ id: 'cloudrift',
+ label: 'CloudRift',
+ yaml: `projects:
+ - name: main
+ backends:
+ - type: cloudrift
+ creds:
+ type: api_key
+ api_key: rift_2prgY1d0laOrf2BblTwx2B2d1zcf1zIp4tZYpj5j88qmNgz38pxNlpX3vAo`,
+ },
+ ] as const
+).map(backend => ({ ...backend, yaml: `${BACKEND_HEADER}\n${backend.yaml}` }));
+
+export const clusterConfigs = [
+ {
+ id: 'kubernetes',
+ label: 'Kubernetes',
+ yaml: `${BACKEND_HEADER}
+projects:
+ - name: main
+ backends:
+ - type: kubernetes
+
+ kubeconfig:
+ filename: ~/.kube/config
+
+ contexts:
+ - name: gpu-cluster-a
+ - name: gpu-cluster-b`,
+ },
+ {
+ id: 'ssh',
+ label: 'SSH',
+ yaml: `${FLEET_HEADER}
+type: fleet
+name: my-fleet
+
+placement: cluster
+
+ssh_config:
+ user: ubuntu
+ identity_file: ~/.ssh/id_rsa
+ hosts:
+ - 3.255.177.51
+ - 3.255.177.52`,
+ },
+] as const;
+
+// Pad every snippet in a tab group to the same line count so all tabs are equal height. Filler
+// lines hold a single space rather than being empty: the highlighter omits the trailing newline
+// after the last line, so an empty final line would collapse to zero height. Requires wrapLines
+// off (1 line = 1 row), otherwise long values would wrap and break the line-count = height mapping.
+export const padYamlToLines = (yaml: string, maxLines: number) =>
+ yaml + '\n '.repeat(maxLines - yaml.split('\n').length);
+
+export const maxBackendYamlLines = Math.max(...backendConfigs.map(backend => backend.yaml.split('\n').length));
+export const maxClusterYamlLines = Math.max(...clusterConfigs.map(cluster => cluster.yaml.split('\n').length));
+
+// Open-source install commands (uv / pip / Docker), each followed by the
+// `dstack server` startup output. Rendered as shell in the "Get started" block.
+export const installMethods = [
+ {
+ id: 'uv',
+ label: 'uv',
+ code: `$ uv tool install "dstack[all]" -U
+$ dstack server
+
+Applying ~/.dstack/server/config.yml...
+
+The admin token is "bbae0f28-d3dd-4820-bf61-8f4bb40815da"
+The server is running at http://127.0.0.1:3000/`,
+ },
+ {
+ id: 'pip',
+ label: 'pip',
+ code: `$ pip install "dstack[all]" -U
+$ dstack server
+
+Applying ~/.dstack/server/config.yml...
+
+The admin token is "bbae0f28-d3dd-4820-bf61-8f4bb40815da"
+The server is running at http://127.0.0.1:3000/`,
+ },
+ {
+ id: 'docker',
+ label: 'Docker',
+ code: `$ docker run -p 3000:3000 \\
+ -v $HOME/.dstack/server/:/root/.dstack/server \\
+ dstackai/dstack
+
+Applying ~/.dstack/server/config.yml...
+
+The admin token is "bbae0f28-d3dd-4820-bf61-8f4bb40815da"
+The server is running at http://127.0.0.1:3000/`,
+ },
+] as const;
+
+export const maxInstallLines = Math.max(...installMethods.map(method => method.code.split('\n').length));
diff --git a/website/src/fonts/Geist-Variable.woff2 b/website/src/fonts/Geist-Variable.woff2
new file mode 100644
index 0000000000..b2f0121062
Binary files /dev/null and b/website/src/fonts/Geist-Variable.woff2 differ
diff --git a/website/src/fonts/GeistPixel-Square.woff2 b/website/src/fonts/GeistPixel-Square.woff2
new file mode 100644
index 0000000000..232cae2c11
Binary files /dev/null and b/website/src/fonts/GeistPixel-Square.woff2 differ
diff --git a/website/src/index.tsx b/website/src/index.tsx
new file mode 100644
index 0000000000..c03484046e
--- /dev/null
+++ b/website/src/index.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { RouterProvider } from 'react-router-dom';
+import '@cloudscape-design/global-styles/index.css';
+import './styles.css';
+import './cloudscape-overrides.css'; // non-themeable details (hairline borders, tab colors, split-button spacing)
+import './cloudscape-theme'; // applies dstack's Cloudscape design-token overrides (runs on import, before render)
+import { router } from './router';
+
+ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+ ,
+);
diff --git a/website/src/pages/Home/ExploreSection.tsx b/website/src/pages/Home/ExploreSection.tsx
new file mode 100644
index 0000000000..ed37bde82d
--- /dev/null
+++ b/website/src/pages/Home/ExploreSection.tsx
@@ -0,0 +1,185 @@
+import CodeView from '@cloudscape-design/code-view/code-view';
+import yamlHighlight from '@cloudscape-design/code-view/highlight/yaml';
+import Button from '@cloudscape-design/components/button';
+import Container from '@cloudscape-design/components/container';
+import Icon from '@cloudscape-design/components/icon';
+import SpaceBetween from '@cloudscape-design/components/space-between';
+import Table from '@cloudscape-design/components/table';
+import Tabs from '@cloudscape-design/components/tabs';
+import { mainButtonStyle } from '../../cloudscape-theme';
+import { AlternatingDocBlock } from '../../components/AlternatingDocBlock';
+import { ArchitectureDiagram } from '../../components/ArchitectureDiagram';
+import { DashedBorder } from '../../components/DashedBorder';
+import { highlightTerms } from '../../components/highlightTerms';
+import { docsUrl } from '../../routes';
+import {
+ backendConfigs,
+ clusterConfigs,
+ maxBackendYamlLines,
+ maxClusterYamlLines,
+ padYamlToLines,
+} from '../../data/snippets';
+
+// Core orchestration primitives shown in the "AI-native orchestration" block.
+const keyConcepts = [
+ { name: 'Fleets', href: docsUrl('concepts/fleets'), description: 'Provision and manage clusters across clouds, Kubernetes, and on-prem.' },
+ { name: 'Dev environments', href: docsUrl('concepts/dev-environments'), description: 'Launch dev environments to be accessed by agents or from your IDE.' },
+ { name: 'Tasks', href: docsUrl('concepts/tasks'), description: 'Run training and batch jobs across a single node or clusters.' },
+ { name: 'Services', href: docsUrl('concepts/services'), description: 'Deploy model inference as secure and scalable endpoints.' },
+];
+
+// Rough per-GPU/hour ranges across backends, in the spirit of `dstack offer --group-by gpu`.
+const gpuOffers = [
+ { name: 'B300', memory: '288GB', price: '$6.00 - $12.00' },
+ { name: 'B200', memory: '192GB', price: '$4.00 - $9.00' },
+ { name: 'H200', memory: '141GB', price: '$3.10 - $7.49' },
+ { name: 'H100', memory: '80GB', price: '$1.90 - $5.99' },
+ { name: 'RTX PRO 6000', memory: '96GB', price: '$1.79 - $3.50' },
+ { name: 'A100', memory: '80GB', price: '$1.20 - $3.40' },
+ { name: 'A100', memory: '40GB', price: '$0.83 - $2.30' },
+ { name: 'L40S', memory: '48GB', price: '$0.80 - $1.40' },
+];
+
+// Read-only YAML snippet. Line wrapping is left off so one line maps to one row,
+// which keeps padded snippets equal height across tabs (see padYamlToLines).
+function YamlCode({ content }: { content: string }) {
+ return (
+
+
+
+ );
+}
+
+// Scrollable GPU price list. The column header is hidden via CSS (.gpu-scroll thead)
+// and the table uses the embedded variant so it sits flush inside the container.
+function GpuMarketplaceTable() {
+ return (
+
+
+
<>{offer.name} ({offer.memory})>, isRowHeader: true },
+ { id: 'price', header: '$/hour', cell: offer => offer.price },
+ ]}
+ items={gpuOffers}
+ />
+
+
+ );
+}
+
+// The main marketing content: a sequence of alternating documentation blocks.
+export function ExploreSection() {
+ return (
+
+ } title="Vendor-agnostic, open-source" imageFirst>
+ dstack unifies fleets, dev environments, tasks, services, volumes, and gateways in one control plane for AI workloads.
+
+
+ It’s built for containerized AI workloads with a simple CLI, UI, and API. No Kubernetes or Slurm hassle required.
+
+
+
+
+ ({
+ id: backend.id,
+ label: backend.label,
+ content: ,
+ }))}
+ />
+ }
+ title="Bring your own clouds"
+ imageFirst
+ action={}
+ >
+ dstack natively integrates with the major GPU clouds and automates provisioning of clusters.
+
+
+ Authorize dstack by providing credentials, and dstack will provision compute and schedule workloads
+ in your own cloud account.
+
+
+ ({
+ id: cluster.id,
+ label: cluster.label,
+ content: ,
+ }))}
+ />
+ }
+ title="Bring on-prem clusters"
+ action={
+
+
+
+
+ }
+ >
+ Have an existing Kubernetes cluster? Point dstack to the kubeconfig, and dstack
+ will schedule workloads on it as it was a cloud cluster.
+
+
+ Have bare-metal servers or VMs with SSH access? Point dstack to those hosts and provide SSH credentials, and dstack will
+ schedule workloads on them alongside Kubernetes and cloud clusters.
+
+
+
+
+ );
+}
+
+function KeyConceptsBlock() {
+ return (
+
+ {keyConcepts.map(concept => (
+ // Whole card is the link so it reads as clickable, with an ActionCard-style
+ // arrow. Kept as a real (open-in-new-tab / SEO) rather than Cloudscape's
+ // onClick-only ActionCard component.
+
+
+
+
+
+ ))}
+
+ }
+ title="AI-native orchestration"
+ >
+ Managing AI infrastructure requires first-class primitives for accelerator provisioning, workload scheduling, and observability.
+
+
+ dstack offers a streamlined interface for development, training, and inference built for heterogeneous AI compute.
+
+ );
+}
+
+function GpuMarketplaceBlock() {
+ return (
+ }
+ title="Access marketplace GPUs"
+ imageFirst
+ action={}
+ >
+ Don't have your own cloud accounts or on-prem clusters? No problem. You can access compute
+ through dstack Sky, our hosted GPU marketplace.
+
+
+ It's possible to use dstack Sky alongside with your own cloud accounts or on-prem clusters.
+
+ );
+}
diff --git a/website/src/pages/Home/FaqSection.tsx b/website/src/pages/Home/FaqSection.tsx
new file mode 100644
index 0000000000..5e8f9de6c2
--- /dev/null
+++ b/website/src/pages/Home/FaqSection.tsx
@@ -0,0 +1,60 @@
+import { useState } from 'react';
+import Button from '@cloudscape-design/components/button';
+import ExpandableSection from '@cloudscape-design/components/expandable-section';
+import { mainButtonStyle } from '../../cloudscape-theme';
+import { AlternatingDocBlock } from '../../components/AlternatingDocBlock';
+import { highlightTerms } from '../../components/highlightTerms';
+
+const faqItems = [
+ {
+ q: 'How does dstack differ from Slurm?',
+ a: 'Slurm is a battle-tested system with decades of production use in HPC environments. dstack, by contrast, is built for modern ML/AI workloads with cloud-native provisioning and a container-first architecture. While both support distributed training and batch jobs, dstack also natively supports development and production-grade inference.',
+ },
+ {
+ q: 'How does dstack compare to Kubernetes?',
+ a: "Kubernetes is a general-purpose container orchestrator. dstack also orchestrates containers, but it provides a lightweight, streamlined interface that's purpose-built for ML. You declare dev environments, tasks, services, and fleets with simple configuration, and dstack provisions GPUs, manages clusters via fleets with fine-grained controls, and optimizes cost and utilization, all while keeping a simple CLI and UI.",
+ },
+ {
+ q: 'Can I use dstack with Kubernetes?',
+ a: 'Yes. You can connect existing Kubernetes clusters using the Kubernetes backend and run dev environments, tasks, and services on them. Choose the Kubernetes backend if your GPUs already run on Kubernetes and your team depends on its ecosystem and tooling. Otherwise, VM-based backends (for cloud GPUs) or SSH fleets (for on-prem) are often a better fit.',
+ },
+ {
+ q: 'When should I use dstack?',
+ a: "dstack accelerates ML development with a simple, ML-native interface: spin up dev environments, run single-node or distributed tasks, and deploy services without infrastructure overhead. It radically reduces GPU costs through smart orchestration and fine-grained fleet controls, including efficient reuse, right-sizing, and support for spot, on-demand, and reserved capacity. It's 100% interoperable with your stack, working with any open-source frameworks and tools and your own Docker images and code, across GPU clouds, Kubernetes, and on-prem GPUs.",
+ },
+];
+
+// FAQ block: a single-open accordion of questions beside contact actions.
+export function FaqSection() {
+ const [openQuestion, setOpenQuestion] = useState(null);
+
+ return (
+
+
+ {faqItems.map(item => (
+ setOpenQuestion(detail.expanded ? item.q : null)}
+ >
+ {highlightTerms(item.a)}
+
+ ))}
+
+ }
+ title="FAQ"
+ action={
+
+ }
+ >
+ Have questions, or need help? Reach out to us on Discord or directly.
+
+
+ );
+}
diff --git a/website/src/pages/Home/GetStartedSection.tsx b/website/src/pages/Home/GetStartedSection.tsx
new file mode 100644
index 0000000000..92eafeb239
--- /dev/null
+++ b/website/src/pages/Home/GetStartedSection.tsx
@@ -0,0 +1,86 @@
+import CodeView from '@cloudscape-design/code-view/code-view';
+import shHighlight from '@cloudscape-design/code-view/highlight/sh';
+import Button from '@cloudscape-design/components/button';
+import SpaceBetween from '@cloudscape-design/components/space-between';
+import Tabs from '@cloudscape-design/components/tabs';
+import { mainButtonStyle } from '../../cloudscape-theme';
+import { AlternatingDocBlock } from '../../components/AlternatingDocBlock';
+import { installMethods, maxInstallLines, padYamlToLines } from '../../data/snippets';
+import { docsUrl } from '../../routes';
+
+// Read-only shell snippet. Line wrapping is left off so padded snippets stay
+// equal height across tabs (see padYamlToLines).
+function ShellCode({ content }: { content: string }) {
+ return (
+
+
+
+ );
+}
+
+// Closing "Get started" section: the open-source install path, then the hosted/enterprise
+// options under "Looking for more?".
+export function GetStartedSection() {
+ return (
+
+
Get started
+
+ ({
+ id: method.id,
+ label: method.label,
+ content: ,
+ }))}
+ />
+ }
+ title="dstack"
+ imageFirst
+ action={
+
+
+
+ }
+ >
+ dstack is fully open-source. Install it on your laptop with uv, or deploy it anywhere using
+ the dstackai/dstack Docker image.
+
+
+
+
+ Once it's running, manage your workloads from the CLI, or let agents do it for you.
+
+
+
+
+
+
dstack Sky
+
Hosted by us. Bring your own clouds, or access marketplace GPUs.
+
+
+
+
+
+
dstack Enterprise
+
Self-hosted with SSO, air-gapped setup, dedicated support, and more.
+
+
+
+
+ }
+ title="Looking for more?"
+ >
+ We can host and operate dstack for you, or back your own self-hosted deployment with enterprise security and support.
+
+
+ );
+}
diff --git a/website/src/pages/Home/HomePage.tsx b/website/src/pages/Home/HomePage.tsx
new file mode 100644
index 0000000000..7b78ad83ba
--- /dev/null
+++ b/website/src/pages/Home/HomePage.tsx
@@ -0,0 +1,75 @@
+import Button from '@cloudscape-design/components/button';
+import { heroButtonStyle } from '../../cloudscape-theme';
+import { highlightTerms } from '../../components/highlightTerms';
+import { images, ThemedImage } from '../../data/images';
+import { DOCS_URL } from '../../routes';
+import { ExploreSection } from './ExploreSection';
+import { FaqSection } from './FaqSection';
+import { GetStartedSection } from './GetStartedSection';
+import { TrustedBySection } from './TrustedBySection';
+
+// Hero artwork: both variants are rendered and CSS shows the one matching the theme.
+function ThemedHeroImage({ image }: { image: ThemedImage }) {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export function HomePage() {
+ const scrollToResources = () =>
+ document.getElementById('resources')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+
+ return (
+
+
+
+
+
+
+
+
+
+ The orchestration stack
+
+ for AI infrastructure
+
+
+ {highlightTerms(
+ 'dstack is an open-source orchestration layer for heterogeneous AI compute. ' +
+ 'It standardizes how workloads run across GPU clouds, Kubernetes, and on-prem clusters, ' +
+ 'across NVIDIA, AMD, Tenstorrent, and TPU accelerators.',
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* On phones the hero artwork is relocated down here, just above the footer
+ (the top instance is hidden at the same breakpoint). */}
+
+
+
+
+ );
+}
diff --git a/website/src/pages/Home/TrustedBySection.tsx b/website/src/pages/Home/TrustedBySection.tsx
new file mode 100644
index 0000000000..a056d3df04
--- /dev/null
+++ b/website/src/pages/Home/TrustedBySection.tsx
@@ -0,0 +1,113 @@
+import Box from '@cloudscape-design/components/box';
+import Container from '@cloudscape-design/components/container';
+import SpaceBetween from '@cloudscape-design/components/space-between';
+import { asset } from '../../asset';
+import { highlightTerms } from '../../components/highlightTerms';
+
+const testimonials = [
+ {
+ name: 'Wah Loon Keng',
+ title: 'Sr. AI Engineer',
+ company: 'Electronic Arts',
+ photo: '/static/quotes/keng.png',
+ quote:
+ 'With dstack, AI researchers at EA can spin up and scale experiments without touching infrastructure. It supports everything from quick prototyping to multi-node training on any cloud.',
+ },
+ /* Replaced Aleksandr Movchan, kept hidden for reference:
+ {
+ name: 'Aleksandr Movchan',
+ title: 'ML Engineer',
+ company: 'Mobius Labs',
+ photo: '/static/quotes/movchan.jpg',
+ quote:
+ 'Thanks to dstack, my team can quickly tap into affordable GPUs and streamline our workflows from testing and development to full-scale application deployment.',
+ },
+ */
+ {
+ name: 'Konstantin Willeke',
+ title: 'AI Researcher',
+ company: 'Metamorphic',
+ photo: '/static/quotes/konstantin.png',
+ quote:
+ 'Fantastic tool if you have heterogeneous compute across different clusters or clouds. dstack ties it all together behind one interface, so we can run experiments without rethinking our setup.',
+ },
+ /* Replaced Alvaro Bartolome, kept hidden for reference:
+ {
+ name: 'Alvaro Bartolome',
+ title: 'ML Engineer',
+ company: 'Argilla',
+ photo: '/static/quotes/alvarobartt.jpg',
+ quote:
+ "With dstack it's incredibly easy to define a configuration within a repository and run it without worrying about GPU availability. It lets you focus on data and your research.",
+ },
+ */
+ {
+ name: 'Dmitry Melikyan',
+ title: 'CEO',
+ company: 'Graphsignal',
+ photo: '/static/quotes/dmitry.jpg',
+ quote:
+ "dstack gives us a unified layer for GPU development and inference across on-prem systems and GPU clouds. It's one workflow from local experiments to production — no custom orchestration to build or maintain for each environment.",
+ },
+ {
+ name: 'Park Chansung',
+ title: 'ML Researcher',
+ company: 'ETRI',
+ photo: '/static/quotes/chansung.jpg',
+ quote:
+ 'Thanks to dstack, I can effortlessly access the top GPU options across different clouds, saving me time and money while pushing my AI work forward.',
+ },
+ /* Replaced Eckart Burgwedel, kept hidden for reference:
+ {
+ name: 'Eckart Burgwedel',
+ title: 'CEO',
+ company: 'Uberchord',
+ photo: '/static/quotes/eckart.png',
+ quote:
+ 'With dstack, running LLMs on a cloud GPU is as easy as running a local Docker container. It combines the ease of Docker with the auto-scaling capabilities of K8S.',
+ },
+ */
+ {
+ name: 'Nikita Shupeyko',
+ title: 'AI Infra',
+ company: 'Toffee',
+ photo: '/static/quotes/nikita.jpeg',
+ quote:
+ "Since we switched to dstack, we've cut the overhead of GPU-cloud orchestration by more than 50%. Running across multiple GPU clouds from a single config also let us reduce our effective GPU spend by 2–3×.",
+ },
+ {
+ name: 'Jon Stevens',
+ title: 'CEO',
+ company: 'Hot Aisle',
+ photo: '/static/quotes/jon.jpeg',
+ quote:
+ "dstack's advantages over Slurm are clear: it's a modern, ground-up approach to running workloads at scale. If you're choosing an orchestration platform, dstack is the place to start.",
+ },
+];
+
+// Social proof: a grid of customer testimonials shown above the "Get started" section.
+export function TrustedBySection() {
+ return (
+
+
+ Cloudscape is an open source design system to create web applications. It was built for
+ and is used by Amazon Web Services (AWS) products and services.
+
+
+ We created it in 2016 to improve the user experience across web applications owned by AWS
+ services, and also to help teams implement those applications faster. Since then, we have
+ continued enhancing the system based on customer feedback and research. Learn more{' '}
+ about the system.
+
+
+
+
+ Each component has a playground where designers and
+ developers can see how the component behaves, along with sample code. To save you time and
+ effort when building, we offer extensive guidance on accessibility options and design
+ solutions. Head over to our demos for examples of Cloudscape in action.
+
+
+
+
+ We publish our source code on GitHub under the Apache 2.0 License{' '}
+ in the cloudscape-design organization. In our{' '}
+ main components repository you can find information about our support
+ and contribution model, versioning strategy, and change logs. You can also{' '}
+ open issues and ask a question.
+
+ Cloudscape supports various visual modes, accessibility, responsive design, and broad browser
+ coverage. Services built with Cloudscape are designed for all customers, regardless of browser,
+ screen size, or ability.
+
+ Get started{' '}
+ with designing accessible and intuitive interfaces. Use our{' '}
+ visual foundation, UX guidelines, and Figma{' '}
+ resources to reduce the time needed to get from project inception to
+ wireframe and prototype.
+
+
+
+
For developers
+
+ Integrate with our system to start developing. Use our accessible
+ and responsive React components to quickly create high quality interfaces.
+
+
+
+
+ );
+}
diff --git a/website/src/pages/Old/index.ts b/website/src/pages/Old/index.ts
new file mode 100644
index 0000000000..2456362322
--- /dev/null
+++ b/website/src/pages/Old/index.ts
@@ -0,0 +1 @@
+export { OldPage } from './OldPage';
diff --git a/website/src/router.tsx b/website/src/router.tsx
new file mode 100644
index 0000000000..6b63521522
--- /dev/null
+++ b/website/src/router.tsx
@@ -0,0 +1,20 @@
+import { Navigate, createBrowserRouter } from 'react-router-dom';
+import { App } from './App';
+import { HomePage } from './pages/Home';
+import { OldPage } from './pages/Old';
+import { ROUTES } from './routes';
+
+// Single data router. In production this app owns only `/` (the landing) — docs and blog
+// are served by MkDocs on the same origin. `/old` is kept as a template for future product
+// pages: reachable in dev, and harmless in production (MkDocs serves unknown paths). Stray
+// paths redirect home.
+export const router = createBrowserRouter([
+ {
+ element: ,
+ children: [
+ { index: true, element: },
+ { path: ROUTES.OLD, element: },
+ { path: '*', element: },
+ ],
+ },
+], { basename: import.meta.env.BASE_URL.replace(/\/$/, '') || '/' });
diff --git a/website/src/routes.ts b/website/src/routes.ts
new file mode 100644
index 0000000000..dd9c5fac47
--- /dev/null
+++ b/website/src/routes.ts
@@ -0,0 +1,26 @@
+// Central route table + cross-links to the MkDocs-served parts of the site.
+//
+// This app (the landing) owns only `/`. Docs and blog are served by MkDocs on the
+// SAME origin in production (`/docs`, `/blog`). For standalone landing development you
+// can point those links at the live site by setting VITE_DOCS_BASE, e.g.
+// VITE_DOCS_BASE=https://dstack.ai npm run dev
+const SITE_BASE = (import.meta.env.VITE_DOCS_BASE ?? '').replace(/\/+$/, '');
+
+export const ROUTES = {
+ HOME: '/',
+ // Kept as a template/reference for building future product pages. Reachable in dev
+ // (`npm run dev` at /old); not part of the integrated production deploy (where this app
+ // only owns `/` and MkDocs serves everything else).
+ OLD: '/old',
+} as const;
+
+export type Route = (typeof ROUTES)[keyof typeof ROUTES];
+
+// Cross-links into the MkDocs site (same origin unless VITE_DOCS_BASE is set).
+export const DOCS_URL = `${SITE_BASE}/docs`;
+export const BLOG_URL = `${SITE_BASE}/blog`;
+export const TERMS_URL = `${SITE_BASE}/terms`;
+export const PRIVACY_URL = `${SITE_BASE}/privacy`;
+
+// Deep link into the docs, e.g. docsUrl('concepts/fleets') -> `${SITE_BASE}/docs/concepts/fleets`.
+export const docsUrl = (path: string) => `${DOCS_URL}/${path.replace(/^\/+/, '')}`;
diff --git a/website/src/styles.css b/website/src/styles.css
new file mode 100644
index 0000000000..cc6950d184
--- /dev/null
+++ b/website/src/styles.css
@@ -0,0 +1,1697 @@
+@font-face {
+ font-family: 'Geist';
+ font-style: normal;
+ font-weight: 100 900;
+ font-display: swap;
+ src: url('./fonts/Geist-Variable.woff2') format('woff2');
+}
+
+@font-face {
+ font-family: 'Geist Pixel Square';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('./fonts/GeistPixel-Square.woff2') format('woff2');
+}
+
+:root {
+ color-scheme: light;
+ --cs-text: #16191f;
+ --font-base: Geist, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+ --cs-bg: #ffffff;
+ --cs-muted: #5f6b7a;
+ --cs-link: #0972d3;
+ --cs-border: #16191f;
+ --cs-panel: #f4f4f7;
+ --font-small: 15px; /* small body text: action-card descriptions, quotes, FAQ answers */
+ --font-card-heading: 16px; /* card titles: action cards, dstack Sky/Enterprise, quote names */
+ --cs-hover: rgba(22, 25, 31, 0.03); /* shared faint hover tint: normal buttons, cards, dropdown items (softened from 0.05) */
+ --cs-btn-hover: #272d38; /* filled (primary) button hover fill — shared with the split-button override */
+ --cs-seg-divider: rgba(255, 255, 255, 0.22); /* split-button segment divider (light line on the dark fill) */
+ --cs-dark: #0f141d;
+ --nav-height: 4.875rem;
+ --frame: 80rem;
+ --page-gutter: clamp(1.5rem, 5.5vw, 5rem);
+ --section-gap: clamp(2.5rem, 4vw, 4rem);
+ --card-gap: 1.25rem;
+ --radius: 0;
+ --media-column: minmax(18rem, 40rem);
+ --doc-article-width: 49.5rem;
+ --doc-rail-width: 17.5rem;
+ --doc-right-gap: 2.5rem;
+ --doc-gap: var(--doc-right-gap);
+ --doc-main-width: calc(var(--doc-article-width) + var(--doc-right-gap) + var(--doc-rail-width));
+ --hero-gradient: linear-gradient(
+ 145deg,
+ rgba(176, 65, 255, 0.38) 0%,
+ rgba(96, 106, 255, 0.33) 34%,
+ rgba(38, 59, 188, 0.06) 62%,
+ rgba(255, 255, 255, 0) 100%
+ );
+}
+
+:root[data-theme='dark'] {
+ color-scheme: dark;
+ --cs-text: #f2f3f3;
+ --cs-bg: #0f141d;
+ --cs-muted: #c6c6cd;
+ --cs-link: #58a6ff;
+ --cs-border: #f2f3f3;
+ --cs-panel: #1b212d;
+ --cs-hover: rgba(242, 243, 243, 0.05); /* softened from 0.085 */
+ --cs-btn-hover: #e2e5e8;
+ --cs-seg-divider: rgba(0, 0, 0, 0.18); /* split-button segment divider (dark line on the light fill) */
+ --hero-gradient: linear-gradient(
+ 145deg,
+ rgba(159, 98, 255, 0.2) 0%,
+ rgba(92, 105, 255, 0.13) 36%,
+ rgba(21, 36, 112, 0.12) 66%,
+ rgba(15, 20, 29, 0) 100%
+ );
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+ /* Offset anchor / scrollIntoView landings by the sticky header (banner + nav) so a targeted
+ section isn't hidden behind it — e.g. "Get started" scrolling to #resources. */
+ scroll-padding-top: calc(var(--nav-height) + 3.25rem);
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ color: var(--cs-text);
+ background: var(--cs-bg, #ffffff);
+ font-family: var(--font-base);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Cloudscape's own font family is set via the theming API (see cloudscape-theme.ts);
+ this base rule covers the non-Cloudscape markup. */
+
+img {
+ display: block;
+ max-width: 100%;
+}
+
+h1,
+h2,
+h3,
+p {
+ margin: 0;
+}
+
+p {
+ line-height: 1.5;
+}
+
+/* Inline highlight for key terms (e.g. "dstack"): a thin rounded outline with minor padding and
+ a transparent fill. Border takes a muted text color so it stays subtle and theme-adaptive. */
+.highlight {
+ padding: 0.06em 0.36em;
+ border: 0.25px solid currentColor;
+ border-radius: 6px;
+ background: transparent;
+ white-space: nowrap;
+}
+
+.site-frame {
+ width: min(var(--frame), calc(100vw - (2 * var(--page-gutter))));
+ margin: 0 auto;
+}
+
+/* Top announcement banner. Filled with the text color and an inverted label — the same
+ treatment as the primary buttons — so it reads as a deliberate strip in either theme. It
+ sticks to the top together with the nav (see .site-header). */
+.site-banner {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 38px;
+ padding: 9px 16px 7.5px;
+ background: var(--cs-text);
+ color: var(--cs-bg);
+ text-align: center;
+}
+
+/* Plain inline link so the label and arrow wrap together as one centered paragraph on
+ narrow screens (the banner grows to contain them). */
+.site-banner__link {
+ color: var(--cs-bg) !important;
+ font-size: 16.5px;
+ font-weight: 300;
+ text-decoration: none !important;
+}
+
+/* On dark, the banner is light-on-dark; nudge the weight up a hair for even visual weight. */
+:root[data-theme='dark'] .site-banner__link {
+ font-weight: 350;
+}
+
+/* Icons follow the banner's (inverted) text color in both themes. */
+.site-banner [class*='awsui_icon'] {
+ color: var(--cs-bg) !important;
+}
+
+.site-banner__arrow {
+ display: inline-flex;
+ vertical-align: middle;
+ margin-inline-start: 6px;
+ transition: transform 0.2s ease;
+}
+
+.site-banner__link:hover .site-banner__arrow {
+ transform: translateX(3px);
+}
+
+/* Banner + nav stick to the top together. Sticking the wrapper (rather than each child)
+ handles the banner's variable height when its text wraps on narrow screens, and keeps it
+ above the hero gradient (which can extend up into this area). */
+.site-header {
+ position: sticky;
+ top: 0;
+ z-index: 901;
+}
+
+.site-nav {
+ position: relative;
+ z-index: 900;
+ height: var(--nav-height);
+ border-bottom: 0.5px solid var(--cs-border);
+ background: var(--cs-bg, #ffffff);
+}
+
+.site-nav--home {
+ border-bottom: 0;
+ background: transparent;
+ -webkit-backdrop-filter: blur(5px);
+ backdrop-filter: blur(5px);
+}
+
+.site-nav::after {
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ right: 0;
+ height: 4px;
+ content: '';
+ background: linear-gradient(90deg, #8f41ff 0 29%, #5668ff 29% 61%, #2d49d8 61% 78%, #102f80 78% 100%);
+}
+
+.site-nav--home::after {
+ display: none;
+}
+
+/* On the Old page the nav drops the brand gradient accent for a plain 1px hairline in the
+ text color, matching the Cloudscape reference header. */
+.site-nav--old {
+ border-bottom: 1px solid var(--cs-text);
+}
+
+.site-nav--old::after {
+ display: none;
+}
+
+.site-nav__inner {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ height: 100%;
+ padding: 14px 24px;
+}
+
+.site-logo,
+.site-menu-button {
+ border: 0;
+ background: transparent;
+ color: var(--cs-text);
+ font: inherit;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.site-logo {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ flex: 0 0 auto;
+ font-size: 20px;
+ line-height: 1;
+}
+
+.site-logo img {
+ width: 28px;
+ height: 28px;
+}
+
+.site-logo span {
+ font-family: 'Geist Pixel Square', var(--font-base);
+ font-weight: 700;
+ font-size: 25px;
+}
+
+.site-menu {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.site-desktop-trigger {
+ flex: 0 0 auto;
+}
+
+.site-mobile-trigger,
+.site-mobile-spacer {
+ display: none;
+}
+
+.site-menu-button,
+.site-menu-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ min-height: 42px;
+ /* No horizontal padding: borderless text items are spaced purely by the nav's
+ SpaceBetween gap, so the whitespace to a neighboring text item matches the whitespace
+ to a bordered button (whose padding sits inside its border) — giving uniform gaps. */
+ padding: 10px 0;
+ border-radius: 21px;
+ color: var(--cs-text);
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.site-menu-button:hover,
+.site-menu-link:hover {
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+
+/* Keyboard focus only on the top-menu buttons. These are our own elements (not Cloudscape
+ components), so use plain values rather than Cloudscape's internal token variables. */
+.site-menu-button:focus-visible,
+.site-menu-link:focus-visible {
+ background: var(--cs-panel);
+ color: var(--cs-text);
+}
+
+.site-menu-button.active {
+ border: 2px solid #879596;
+}
+
+/* "Resources" top-nav dropdown (ResourcesHoverMenu): make the ButtonDropdown trigger read
+ like the plain text menu links (.site-menu-link) — no border/background, bold 16px label,
+ underline on hover — rather than the bordered "normal" button look. */
+.site-menu-dropdown-wrap {
+ display: inline-flex;
+ align-items: center;
+}
+.site-menu-dropdown [class*='awsui_button'] {
+ border-color: transparent !important;
+ background: transparent !important;
+ color: var(--cs-text) !important;
+ font-size: 16px !important; /* match the .site-menu-link items (e.g. Documentation) */
+ font-weight: 700 !important;
+ padding: 10px 0 !important; /* no horizontal padding — matches the text links */
+}
+.site-menu-dropdown [class*='awsui_button']:hover {
+ background: transparent !important;
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+/* The menu opens on hover, so the dropdown caret is redundant — hide it. */
+.site-menu-dropdown [class*='awsui_button'] [class*='awsui_icon'] {
+ display: none !important;
+}
+
+.home-hero {
+ position: relative;
+ overflow: visible;
+ background: var(--cs-bg);
+}
+
+.home-hero::before {
+ position: absolute;
+ top: calc(-1 * var(--nav-height));
+ right: 0;
+ bottom: -16rem;
+ left: 0;
+ z-index: 0;
+ content: '';
+ pointer-events: none;
+ background: var(--hero-gradient);
+ -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 46%, transparent 88%);
+ mask-image: linear-gradient(180deg, #000 0%, #000 46%, transparent 88%);
+}
+
+.home-hero__content {
+ position: relative;
+ z-index: 1;
+ padding: 96px 0 64px;
+ color: var(--cs-text);
+}
+
+.home-hero h2 {
+ margin-top: 0;
+ max-width: 720px;
+ color: var(--cs-text);
+ font-size: 42px;
+ line-height: 1.16;
+ font-weight: 700;
+}
+
+.home-hero p {
+ margin-top: 16px;
+ max-width: 600px;
+ font-size: 17px;
+}
+
+/* Hero CTAs sit side by side, each sized to its own label (not forced to equal width). */
+.home-hero__actions {
+ margin-top: 28px;
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.home-hero__art {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ opacity: 1;
+}
+
+:root[data-theme='dark'] .home-hero__art {
+ opacity: 0.92;
+}
+
+/* Centered content frame for the art, so it aligns with the columns of the blocks below. */
+.home-hero__art-frame {
+ position: relative;
+ height: 100%;
+}
+
+/* Art occupies the second content column: right edge at the frame edge, width of one
+ column, so its left edge lines up with the second column of the blocks below. */
+.hero-slice {
+ position: absolute;
+ top: 68px;
+ right: 0;
+ width: calc((100% - var(--doc-gap)) / 2);
+ height: auto;
+ pointer-events: none;
+ user-select: none;
+}
+
+.hero-slice--dark {
+ display: none;
+}
+
+:root[data-theme='dark'] .hero-slice--light {
+ display: none;
+}
+
+:root[data-theme='dark'] .hero-slice--dark {
+ display: block;
+}
+
+/* Phone-only copy of the hero artwork, placed just above the footer (see HomePage). The
+ light/dark swap is handled by the shared .hero-slice--light/--dark rules above. */
+.home-hero-mobile-art {
+ display: none;
+}
+
+/* Lifted above the footer gradient so the content/cards stay clean while the gradient
+ shows through behind them. */
+.home-main {
+ position: relative;
+ z-index: 1;
+}
+
+.home-stack {
+ padding: 0 0 96px;
+}
+
+.home-with-rail .docs-body {
+ grid-template-columns:
+ minmax(0, calc(100% - var(--doc-right-gap) - var(--doc-rail-width)))
+ var(--doc-right-gap)
+ var(--doc-rail-width);
+}
+
+.home-with-rail .image-text-row,
+.home-with-rail .image-text-row:not(.image-first) {
+ grid-template-columns: minmax(16rem, 1fr) minmax(0, 1fr);
+ gap: var(--doc-gap);
+}
+
+.media-card h3,
+.doc-media-card h3 {
+ margin-bottom: 8px;
+ font-size: 20px;
+ line-height: 1.2;
+}
+
+.image-text-row {
+ display: grid;
+ grid-template-columns: var(--media-column) minmax(0, 1fr);
+ gap: var(--section-gap);
+ align-items: center;
+ margin-top: 40px;
+}
+
+.image-text-row:not(.image-first) {
+ grid-template-columns: minmax(0, 1fr) var(--media-column);
+}
+
+.image-text-row.image-first .landing-image {
+ order: -1;
+}
+
+.docs-shell--old-page .image-text-row,
+.docs-shell--old-page .image-text-row:not(.image-first) {
+ grid-template-columns: minmax(14rem, 24rem) minmax(0, 1fr);
+ gap: var(--doc-gap);
+}
+
+.docs-shell--old-page .image-text-row:not(.image-first) {
+ grid-template-columns: minmax(0, 1fr) minmax(14rem, 24rem);
+}
+
+.landing-image {
+ width: 100%;
+ aspect-ratio: 876 / 528;
+ height: auto;
+ object-fit: cover;
+ border-radius: var(--radius);
+}
+
+.landing-copy {
+ max-width: 604px;
+}
+
+.landing-copy h2,
+.overview-section h2,
+.start-building h2,
+.docs-section > h2 {
+ margin-bottom: 12px;
+ font-size: 28px;
+ line-height: 1.25;
+}
+
+/* "Get started" and "Trusted by" section headings: larger than the in-flow block
+ titles, closer to the hero size. */
+#resources > h2,
+#trusted-by > h2 {
+ margin-bottom: 32px;
+ font-size: 36px;
+}
+
+/* Extra breathing room before "Looking for more?" (the second block in this section). */
+#resources .doc-alternating + .doc-alternating {
+ margin-top: 105px;
+}
+
+/* Keep the lower sections flowing as one continuous sequence (no dividers between them). */
+#faq,
+#trusted-by {
+ border-bottom: 0;
+}
+
+/* 1px outer frame on the FAQ block with rounded (12px) corners. ExpandableSection
+ has no radius token, so we border + clip the wrapper; overflow:hidden trims the items' square
+ corners to the radius. Internal dividers between questions stay 0.5px (see cloudscape-overrides). */
+.faq-list {
+ border: 1px solid var(--cs-border);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+/* FAQ question headers. */
+.faq-list [class*="awsui_header-text"] {
+ font-size: 16px;
+}
+
+.landing-copy p + p {
+ margin-top: 18px;
+}
+
+.overview-section {
+ margin-top: 56px;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--card-gap);
+ margin-top: 20px;
+}
+
+.stat-card {
+ display: grid;
+ grid-template-columns: 72px minmax(0, 1fr);
+ gap: var(--card-gap);
+ align-items: center;
+ min-height: 160px;
+ padding: 24px 40px;
+ border-radius: 0;
+ background: var(--cs-panel);
+}
+
+.stat-card span {
+ color: #3867ff;
+ font-size: 44px;
+ line-height: 1;
+ font-weight: 400;
+}
+
+.stat-card p {
+ max-width: 14ch;
+ font-size: 24px;
+ line-height: 1.15;
+}
+
+.core-features {
+ margin-top: 36px;
+}
+
+.core-features > h3 {
+ margin-bottom: 4px;
+ font-size: 20px;
+}
+
+.core-features > p {
+ max-width: 1230px;
+}
+
+.feature-card-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20px;
+ margin-top: 32px;
+}
+
+.media-card {
+ border: 1px solid var(--cs-border); /* 1px / 12px */
+ border-radius: 12px;
+ overflow: hidden;
+ background: var(--cs-bg, #ffffff);
+}
+
+.media-card img {
+ width: 100%;
+ height: 120px;
+ object-fit: cover;
+ background: #f3f5f9;
+}
+
+.media-card h3,
+.media-card p {
+ padding-inline: 20px;
+}
+
+.media-card h3 {
+ padding-top: 16px;
+}
+
+.media-card p {
+ padding-bottom: 22px;
+ font-size: var(--font-small);
+}
+
+.start-building {
+ margin-top: 64px;
+ padding-top: 48px;
+ border-top: 0.5px solid var(--cs-border);
+}
+
+.start-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--card-gap);
+ margin-top: 20px;
+}
+
+.start-grid article {
+ min-height: 152px;
+ padding: 24px 20px;
+ border: 0.5px solid var(--cs-border);
+ border-radius: 0;
+}
+
+.start-grid h3 {
+ margin-bottom: 18px;
+ font-size: 20px;
+}
+
+.start-grid p {
+ font-size: 14px;
+}
+
+.docs-shell {
+ min-height: calc(100vh - var(--nav-height));
+}
+
+.docs-main {
+ width: min(var(--doc-main-width), calc(100vw - (2 * var(--page-gutter))));
+ padding: 1.625rem 0 6rem;
+}
+
+.docs-shell--navigation-collapsed .docs-main {
+ width: min(var(--frame), calc(100vw - (2 * var(--page-gutter))));
+ margin-inline: auto;
+}
+
+.docs-body {
+ display: grid;
+ grid-template-columns: minmax(0, var(--doc-article-width)) var(--doc-right-gap) var(--doc-rail-width);
+ align-items: start;
+}
+
+.docs-body--no-rail {
+ grid-template-columns: minmax(0, 1fr);
+}
+
+.docs-shell--navigation-collapsed .docs-body {
+ grid-template-columns:
+ minmax(0, calc(100% - var(--doc-right-gap) - var(--doc-rail-width)))
+ var(--doc-right-gap)
+ var(--doc-rail-width);
+}
+
+.docs-shell--old-page .docs-main {
+ width: min(var(--frame), 100%);
+ margin-inline: auto;
+ /* The footer is the last child here; sit it flush at the bottom of the content
+ column instead of leaving the article's bottom padding below it. */
+ padding-bottom: 0;
+}
+
+/* Cloudscape gives the AppLayout content region a 40px bottom margin. With the footer living
+ inside it, that leaves a 40px strip below the footer; drop it so the footer sits flush at the
+ page bottom, matching the global site footer. */
+.docs-shell--old-page [class*='awsui_main_'] {
+ margin-bottom: 0 !important;
+}
+
+.docs-shell--old-page .docs-body {
+ grid-template-columns:
+ minmax(0, calc(100% - var(--doc-right-gap) - var(--doc-rail-width)))
+ var(--doc-right-gap)
+ var(--doc-rail-width);
+}
+
+.docs-article {
+ grid-column: 1;
+}
+
+.docs-right {
+ grid-column: 3;
+ position: sticky;
+ /* Clear the sticky header (banner + nav), matching scroll-padding-top above. */
+ top: calc(var(--nav-height) + 3.25rem);
+ margin-top: 1.5rem;
+}
+
+/* The section divider in the docs side nav is inset by default (unlike the header
+ divider); pull it out to the drawer edges so it spans the full sidebar width. */
+.docs-shell [class*='awsui_divider-default'] {
+ margin-left: -28px !important;
+ margin-right: -24px !important;
+}
+
+/* Cloudscape's expandable nav group renders the caret before the label, indenting the parent
+ past its sibling links, and indents the children 40px. Pull the caret into the left gutter so
+ the parent label lines up with its siblings, and tighten the children to one indent level. */
+.docs-shell [class*='awsui_expandable-link-group'] [class*='awsui_expand-button'] {
+ margin-left: -22px !important;
+}
+
+.docs-shell [class*='awsui_list-variant-expandable-link-group'] {
+ padding-left: 20px !important;
+}
+
+.docs-title {
+ margin-top: 1rem;
+ padding-bottom: 1.125rem;
+ border-bottom: 0.5px solid var(--cs-border);
+}
+
+.docs-title h1 {
+ font-size: 36px;
+ line-height: 1.2;
+}
+
+.docs-title p {
+ margin-top: 8px;
+ color: var(--cs-muted);
+ font-size: 18px;
+}
+
+.docs-section {
+ padding: 1.5rem 0 3rem;
+ border-bottom: 0.5px solid var(--cs-border);
+}
+
+.docs-section:last-child {
+ border-bottom: 0;
+}
+
+/* No divider between the explore section and Access GPU marketplace below it. */
+.docs-section.explore-section {
+ border-bottom: 0;
+}
+
+.doc-alternating {
+ display: grid;
+ grid-template-columns: minmax(16rem, 1fr) minmax(0, 1fr);
+ gap: var(--doc-gap);
+ align-items: center;
+ margin-top: 18px;
+}
+
+.doc-alternating + .doc-alternating {
+ margin-top: 48px;
+}
+
+.doc-alternating:not(.image-first) {
+ grid-template-columns: minmax(0, 1fr) minmax(16rem, 1fr);
+}
+
+.doc-alternating:not(.image-first) .doc-visual {
+ order: 2;
+}
+
+.doc-visual {
+ min-width: 0;
+}
+
+/* CodeView has no prop to disable its built-in fill; let the Container surface show through. */
+.code-snippet > div {
+ background-color: transparent !important;
+}
+
+/* Cap the marketplace table height so the rows scroll inside the container.
+ Tuned so the block matches the height of the tab blocks above. */
+.gpu-scroll {
+ max-height: 307px;
+ overflow-y: auto;
+}
+
+/* Cloudscape buttons render as when given href; exclude them from the docs underline rule.
+ Higher specificity than `.docs-article a` so the reset wins regardless of source order. */
+.docs-article .doc-action a,
+.docs-article .figma-card a {
+ text-decoration: none !important;
+}
+
+/* No column header for the marketplace list (Table has no prop to omit it). */
+.gpu-scroll thead {
+ display: none;
+}
+
+.doc-action {
+ margin-top: 20px;
+}
+
+
+.doc-alternating img {
+ width: 100%;
+ aspect-ratio: 980 / 640;
+ height: auto;
+ object-fit: cover;
+ border-radius: var(--radius);
+}
+
+.doc-alternating img.doc-diagram {
+ object-fit: contain;
+}
+
+.doc-diagram--dark {
+ display: none;
+}
+
+:root[data-theme='dark'] .doc-diagram--light {
+ display: none;
+}
+
+:root[data-theme='dark'] .doc-diagram--dark {
+ display: block;
+}
+
+.doc-alternating h2 {
+ margin-bottom: 12px;
+ font-size: 28px;
+}
+
+/* Body copy in the alternating marketing blocks (normal page text). */
+.doc-alternating p {
+ font-size: 17px;
+}
+
+/* Action-card (concept) descriptions are a step smaller than normal text. Higher specificity
+ than `.doc-alternating p` so it wins (the cards sit inside an alternating block). This is the
+ shared small-text size (--font-small), reused for quotes, FAQ answers, and popup descriptions. */
+.concept-grid .media-card p {
+ font-size: var(--font-small);
+}
+
+/* "Vendor-agnostic, open-source" architecture diagram (components/ArchitectureDiagram.tsx),
+ rebuilt from a static SVG into HTML/CSS and sized 1:1 with it. Every dimension is in cqw
+ (% of the diagram's own width, resolved against the .arch-diagram-wrap container) so the
+ whole thing scales proportionally like the original SVG. Source design width is 901.8
+ units, so 1 unit ≈ 0.111cqw. Logos are monochrome + theme-adaptive (see .arch-logo). */
+.arch-diagram-wrap {
+ container-type: inline-size;
+ width: 100%;
+}
+
+.arch-diagram {
+ display: flex;
+ flex-direction: column;
+ gap: 2.66cqw; /* 24u between the four layers */
+ width: 100%;
+ color: var(--cs-text);
+ --arch-border-width: max(1px, 0.22cqw); /* placeholder badge stroke */
+ --arch-gradient: linear-gradient(45deg, #002aff, #002aff, #e165fe);
+}
+
+/* Dotted outline: an inline