diff --git a/mkdocs.yml b/mkdocs.yml
index 2fc74935a..62d7feabf 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -397,10 +397,8 @@ nav:
- llms-full.txt: https://dstack.ai/llms-full.txt
- skill.md: https://dstack.ai/skill.md
- Case studies: blog/case-studies.md
- - Benchmarks: blog/benchmarks.md
- Blog:
- blog/index.md
- - Discord: https://discord.gg/u8SmfwPpMd" target="_blank
# - Changelog: https://github.com/dstackai/dstack/releases" target="_blank
# - GitHub: https://github.com/dstackai/dstack" target="_blank
# - Sign in: https://sky.dstack.ai" target="_blank
diff --git a/mkdocs/assets/stylesheets/cloudscape-docs.css b/mkdocs/assets/stylesheets/cloudscape-docs.css
index cabeb93b9..8f1c5c6c0 100644
--- a/mkdocs/assets/stylesheets/cloudscape-docs.css
+++ b/mkdocs/assets/stylesheets/cloudscape-docs.css
@@ -204,15 +204,16 @@
white-space: nowrap;
}
-/* GitHub → outlined "GitHub" with a trailing external-link glyph (no star count). */
-[data-md-color-primary=white] .md-header__buttons .md-button--primary.github,
-[data-md-color-primary=white] .md-header__buttons .md-button--primary.github:hover {
+/* GitHub → outlined (normal) "GitHub" with a trailing external-link glyph. Weight 700 so it matches
+ the filled "Get started" trigger beside it (the two now read as one type, different fills). */
+[data-md-color-primary=white] .md-header__buttons .md-button--primary.github {
background: transparent;
color: var(--cs-text);
border: 1px solid var(--cs-border);
+ font-weight: 700 !important;
}
-/* Gap between GitHub and Get started → 20px to match /old (was 5px). */
+/* Gap between GitHub and dstack Sky → 20px to match /old (was 5px). */
.md-header__buttons .md-button.github {
margin-right: 20px;
}
@@ -235,138 +236,206 @@
mask: var(--cs-ext-icon) center / contain no-repeat;
}
-/* Get started → /old's split button (dark primary main + caret) with a dropdown menu. */
-.md-header__buttons .cs-get-started {
- position: relative;
- display: inline-flex;
- align-items: stretch;
- margin-right: 5px;
-}
-
-[data-md-color-primary=white] .md-header__buttons .cs-get-started .md-button--primary,
-[data-md-color-primary=white] .md-header__buttons .cs-get-started .md-button--primary:hover {
- background: var(--cs-text);
- color: var(--cs-bg);
- border: 1px solid var(--cs-text);
- margin: 0;
- /* Primary (filled) button uses 500 weight per design — the GitHub outline button stays 700. */
- font-weight: 500 !important;
+/* Try dstack Sky → outlined (normal) pill with a trailing external-link glyph, sitting after
+ the now-filled GitHub button (the two swapped emphasis vs the old GitHub + Get started). */
+[data-md-color-primary=white] .md-header__buttons .md-button--primary.cs-try-sky {
+ background: transparent;
+ color: var(--cs-text);
+ border: 1px solid var(--cs-border);
}
-
-/* Hover states (Cloudscape): the outlined GitHub button fills subtly (NOT black — landing.css
- forces `background:black !important` on primary hover, so these need !important too); the dark
- Get-started button lightens to the button-hover token. */
-[data-md-color-primary=white] .md-header__buttons .md-button--primary.github:hover {
+[data-md-color-primary=white] .md-header__buttons .md-button--primary.cs-try-sky:hover {
background: var(--cs-hover) !important;
border-color: var(--cs-border) !important;
color: var(--cs-text) !important;
}
-/* Hovering EITHER segment lights up BOTH (the whole pill reads as one button that opens the menu). */
-[data-md-color-primary=white] .md-header__buttons .cs-get-started:hover .md-button--primary {
- background: var(--cs-btn-hover) !important;
- border-color: var(--cs-btn-hover) !important;
- color: var(--cs-bg) !important;
+.md-header__buttons .md-button.cs-try-sky::after {
+ content: "";
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin-left: 6px;
+ flex: 0 0 auto;
+ background-color: currentColor;
+ -webkit-mask: var(--cs-ext-icon) center / contain no-repeat;
+ mask: var(--cs-ext-icon) center / contain no-repeat;
}
-.md-header__buttons .cs-get-started__main {
- border-radius: 12px 0 0 12px !important;
- border-right: 0 !important;
- cursor: pointer;
- /* No separator now, so pull the caret in close to the label (was 18px + the toggle's 6px). */
- padding-right: 6px !important;
+/* "Get started" header dropdown — mirrors the landing's Products popup (featured open-source +
+ dstack Sky + Enterprise). Pure-CSS hover/focus-within (the docs are static); right-aligned so the
+ panel never overflows the header's right edge. Reuses the --cs-* tokens (defined in extra.css). */
+.cs-gs-menu {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
}
-
-/* No segment separator — the caret reads as part of the same button. */
-.md-header__buttons .cs-get-started__toggle {
- border-radius: 0 12px 12px 0 !important;
- padding: 0 6px !important;
- border-left: 0 !important;
- cursor: pointer;
+.cs-gs-menu__trigger {
+ display: inline-flex !important;
+ align-items: center;
+ gap: 5px;
+ /* the caret is lighter than a full glyph, so trim the trailing padding (was 18px). */
+ padding-right: 12px !important;
}
-
-.md-header__buttons .cs-get-started__toggle svg {
- width: 18px;
- height: 18px;
- fill: currentColor;
+.cs-gs-menu__caret {
+ transition: transform 0.15s ease;
}
-
-/* Matches the new landing / old Get-started popup (cloudscape-overrides.css): fixed 300px, flat
- (no shadow), a single 0.5px cs-text border, 12px radius clipped to rounded corners. */
-.md-header__buttons .cs-get-started__menu {
+.cs-gs-menu:hover .cs-gs-menu__caret,
+.cs-gs-menu:focus-within .cs-gs-menu__caret {
+ transform: rotate(180deg);
+}
+.cs-gs-menu__popup {
position: absolute;
- top: calc(100% + 6px);
+ top: calc(100% + 8px);
right: 0;
- min-width: 300px;
- padding: 8px 0;
- display: flex;
- flex-direction: column;
+ z-index: 1000;
+ width: 360px;
+ padding: 6px;
background: var(--cs-bg);
border: 0.5px solid var(--cs-text);
border-radius: 12px;
- box-shadow: none;
- overflow: hidden;
- z-index: 1000;
- /* Match the landing dropdown's subpixel smoothing (Material defaults to antialiased = thinner). */
- -webkit-font-smoothing: auto;
- -moz-osx-font-smoothing: auto;
+ box-shadow: 0 12px 34px rgba(0, 0, 0, 0.14);
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-4px);
+ transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
+}
+/* Invisible bridge over the 8px gap so the pointer can travel from trigger to popup. */
+.cs-gs-menu__popup::before {
+ content: "";
+ position: absolute;
+ inset: -8px 0 auto 0;
+ height: 8px;
}
-
-.md-header__buttons .cs-get-started__menu[hidden] {
- display: none;
+.cs-gs-menu:hover .cs-gs-menu__popup,
+.cs-gs-menu:focus-within .cs-gs-menu__popup {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+/* Text inside the popup must wrap — Material forces white-space:nowrap on header links/buttons. */
+.cs-gs-menu__popup,
+.cs-gs-menu__feat,
+.cs-gs-menu__row,
+.cs-gs-menu__feat *,
+.cs-gs-menu__row * {
+ white-space: normal !important;
+}
+.cs-gs-menu__feat {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 14px;
+ border-radius: 10px;
+ background: linear-gradient(135deg, #002aff, #002aff, #e165fe);
+ color: #fff;
+ text-decoration: none;
}
-
-/* Group headers ("Products" / "Login") → 15px / 300, ~5.5px vertical padding, like the landing. */
-.md-header__buttons .cs-get-started__group {
- padding: 5.5px 16px;
- font-size: 15px;
- font-weight: 300;
- color: var(--cs-text);
+.cs-gs-menu__feat-body {
+ flex: 1 1 auto;
+ min-width: 0;
}
-
-/* Items → 4px/16px padding; a 15/600 heading-color title with an optional description below. */
-.md-header__buttons .cs-get-started__menu a {
+/* Left column: icon tile with the live star count centered beneath it (mirrors the landing). */
+.cs-gs-menu__feat-iccol {
display: flex;
flex-direction: column;
- padding: 4px 16px;
- font-size: 15px;
+ align-items: center;
+ gap: 6px;
+ flex: 0 0 auto;
+}
+.cs-gs-menu__gh {
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 12px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+.cs-gs-menu__feat-ic {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
+ border: 0.5px solid rgba(255, 255, 255, 0.5);
+ color: #fff;
+}
+.cs-gs-menu__feat-ic svg {
+ width: 18px;
+ height: 18px;
+}
+.cs-gs-menu__feat-name {
+ display: block;
+ color: #fff;
+ font-size: 16px;
font-weight: 600;
+}
+.cs-gs-menu__feat-desc {
+ display: block;
+ margin-top: 8px;
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 13px;
+ line-height: 1.5;
+}
+.cs-gs-menu__list {
+ display: flex;
+ flex-direction: column;
+ margin-top: 6px;
+}
+.cs-gs-menu__row {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 11px 10px;
+ border-radius: 9px;
color: var(--cs-text);
text-decoration: none;
}
-.md-header__buttons .cs-get-started__item-title {
- color: var(--cs-nav-heading);
+.cs-gs-menu__row:hover {
+ background: var(--cs-hover);
}
-/* The generic external-link "↗" is an a::after rendered as a block, so the column flex dropped it
- onto its own line below the description. Suppress it and put the icon inline after the TITLE
- text instead (external items only), like the landing. */
-.md-header__buttons .cs-get-started__menu a::after {
- content: none !important;
- display: none !important;
+.cs-gs-menu__ic {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
+ border: 0.5px solid var(--cs-text);
+ color: var(--cs-text);
}
-.md-header__buttons .cs-get-started__menu a[target="_blank"] .cs-get-started__item-title::after {
- content: "";
- display: inline-block;
- width: 13px;
- height: 13px;
- margin-left: 5px;
- vertical-align: middle;
- background-color: currentColor;
- -webkit-mask: var(--cs-ext-icon) center / contain no-repeat;
- mask: var(--cs-ext-icon) center / contain no-repeat;
+.cs-gs-menu__ic svg {
+ width: 20px;
+ height: 20px;
}
-/* Description: 13px / 300 / full text color (light weight reads muted) / 16px line-height, like
- the landing's secondary text. */
-.md-header__buttons .cs-get-started__item-desc {
- margin-top: 1.5px;
- font-size: 13px;
- font-weight: 300;
- line-height: 16px;
+.cs-gs-menu__rbody {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+.cs-gs-menu__name {
+ display: block;
color: var(--cs-text);
- white-space: normal;
+ font-size: 15px;
+ font-weight: 600;
+}
+.cs-gs-menu__desc {
+ display: block;
+ margin-top: 3px;
+ color: var(--cs-muted);
+ font-size: 13px;
+ line-height: 1.5;
+}
+/* On tablet/mobile the header buttons collapse; keep the dropdown from overflowing tiny screens. */
+@media (max-width: 76.1875em) {
+ .cs-gs-menu__popup { width: min(360px, calc(100vw - 32px)); }
}
-.md-header__buttons .cs-get-started__menu a:hover {
- background: var(--cs-hover);
+/* GitHub hover: outlined button gets a faint tint (landing.css forces black !important on primary
+ hover, so these need !important to win). */
+[data-md-color-primary=white] .md-header__buttons .md-button--primary.github:hover {
+ background: var(--cs-hover) !important;
+ border-color: var(--cs-border) !important;
+ color: var(--cs-text) !important;
}
/* Burger (desktop sidebar toggle) — far left, like /old. */
diff --git a/mkdocs/overrides/header-2.html b/mkdocs/overrides/header-2.html
index f6887a7a3..fc6eb3e39 100644
--- a/mkdocs/overrides/header-2.html
+++ b/mkdocs/overrides/header-2.html
@@ -101,29 +101,47 @@
GitHub
-
-
Get started
-
- {% include ".icons/material/menu-down.svg" %}
+ {# "Get started" dropdown — mirrors the landing's Products popup (featured open-source +
+ dstack Sky + Enterprise). Pure-CSS hover/focus-within (the site is static), opens below,
+ right-aligned so it never overflows the header's right edge. #}
+
@@ -134,21 +152,25 @@
document.body.classList.toggle('cs-nav-collapsed');
// The content column reflows (792↔960), so re-align the right rail's TOC offset.
window.dispatchEvent(new Event('resize'));
- return;
- }
- var menu = document.querySelector('.cs-get-started__menu');
- if (!menu) return;
- var triggers = document.querySelectorAll('.cs-get-started__main, .cs-get-started__toggle');
- // Both segments open the popup (no default navigation).
- if (e.target.closest('.cs-get-started__main, .cs-get-started__toggle')) {
- var hidden = menu.hasAttribute('hidden');
- menu.toggleAttribute('hidden', !hidden);
- triggers.forEach(function (t) { t.setAttribute('aria-expanded', String(hidden)); });
- } else if (!e.target.closest('.cs-get-started__menu')) {
- menu.setAttribute('hidden', '');
- triggers.forEach(function (t) { t.setAttribute('aria-expanded', 'false'); });
}
});
+
+ // Live GitHub star count under the "Get started" popup's open-source icon (mirrors the
+ // landing). Best-effort: if the API is rate-limited or errors, the badge stays hidden.
+ var el = document.querySelector('[data-cs-gh-stars]');
+ if (el) {
+ fetch('https://api.github.com/repos/dstackai/dstack')
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (d) {
+ if (!d || typeof d.stargazers_count !== 'number') return;
+ var n = d.stargazers_count, label;
+ if (n < 1000) label = String(n);
+ else { var k = n / 1000; label = (k >= 10 ? Math.round(k) : Number(k.toFixed(1))) + 'k'; }
+ el.textContent = label;
+ el.hidden = false;
+ })
+ .catch(function () {});
+ }
})();
diff --git a/website/.gitignore b/website/.gitignore
index de4d1f007..311e030bb 100644
--- a/website/.gitignore
+++ b/website/.gitignore
@@ -1,2 +1,3 @@
dist
node_modules
+.vite
diff --git a/website/mkdocs/docs/reference/plugins/rest/rest_plugin_openapi.json b/website/mkdocs/docs/reference/plugins/rest/rest_plugin_openapi.json
new file mode 100644
index 000000000..d5955fcbe
--- /dev/null
+++ b/website/mkdocs/docs/reference/plugins/rest/rest_plugin_openapi.json
@@ -0,0 +1 @@
+{"openapi": "3.1.0", "info": {"title": "REST Plugin OpenAPI Spec", "version": "0.0.0"}, "servers": [{"url": "http://localhost:8000", "description": "Local server"}], "paths": {"/apply_policies/on_run_apply": {"post": {"summary": "On Run Apply", "operationId": "on_run_apply_apply_policies_on_run_apply_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpecApplyRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpecApplyResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/apply_policies/on_fleet_apply": {"post": {"summary": "On Fleet Apply", "operationId": "on_fleet_apply_apply_policies_on_fleet_apply_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpecApplyRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpecApplyResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/apply_policies/on_volume_apply": {"post": {"summary": "On Volume Apply", "operationId": "on_volume_apply_apply_policies_on_volume_apply_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpecApplyRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpecApplyResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/apply_policies/on_gateway_apply": {"post": {"summary": "On Gateway Apply", "operationId": "on_gateway_apply_apply_policies_on_gateway_apply_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpecApplyRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SpecApplyResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"ACMGatewayCertificateRequest": {"properties": {"type": {"type": "string", "enum": ["acm"], "title": "Type", "description": "Certificates by AWS Certificate Manager (ACM)", "default": "acm"}, "arn": {"type": "string", "title": "Arn", "description": "The ARN of the wildcard ACM certificate for the domain"}}, "additionalProperties": false, "type": "object", "required": ["arn"], "title": "ACMGatewayCertificateRequest"}, "AWSVolumeConfigurationRequest": {"properties": {"type": {"type": "string", "enum": ["volume"], "title": "Type", "default": "volume"}, "backend": {"type": "string", "enum": ["aws"], "title": "Backend", "description": "The volume backend", "default": "aws"}, "name": {"type": "string", "title": "Name", "description": "The volume name"}, "size": {"type": "number", "title": "Size", "description": "The volume size. Must be specified when creating new volumes"}, "auto_cleanup_duration": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Auto Cleanup Duration", "description": "Time to wait after volume is no longer used by any job before deleting it. Defaults to keep the volume indefinitely. Use the value `off` or `-1` to disable auto-cleanup"}, "tags": {"additionalProperties": {"type": "string"}, "type": "object", "title": "Tags", "description": "The custom tags to associate with the volume. The tags are also propagated to the underlying backend resources. If there is a conflict with backend-level tags, does not override them"}, "volume_id": {"type": "string", "title": "Volume Id", "description": "The volume ID. Must be specified when registering external volumes"}, "region": {"type": "string", "title": "Region", "description": "The volume region"}, "availability_zone": {"type": "string", "title": "Availability Zone", "description": "The volume availability zone"}}, "additionalProperties": false, "type": "object", "required": ["region"], "title": "AWSVolumeConfigurationRequest"}, "AcceleratorVendor": {"type": "string", "enum": ["nvidia", "amd", "google", "intel", "tenstorrent"], "title": "AcceleratorVendor", "description": "An enumeration."}, "BackendType": {"type": "string", "enum": ["amddevcloud", "aws", "azure", "cloudrift", "crusoe", "cudo", "datacrunch", "digitalocean", "dstack", "gcp", "hotaisle", "jarvislabs", "kubernetes", "lambda", "remote", "nebius", "oci", "runpod", "tensordock", "vastai", "verda", "vultr"], "title": "BackendType", "description": "Attributes:\n AMDDEVCLOUD (BackendType): AMD Developer Cloud\n AWS (BackendType): Amazon Web Services\n AZURE (BackendType): Microsoft Azure\n CLOUDRIFT (BackendType): CloudRift\n CRUSOE (BackendType): Crusoe\n CUDO (BackendType): Cudo\n DATACRUNCH (BackendType): DataCrunch (for backward compatibility)\n DIGITALOCEAN (BackendType): DigitalOcean\n DSTACK (BackendType): dstack Sky\n GCP (BackendType): Google Cloud Platform\n HOTAISLE (BackendType): Hot Aisle\n JARVISLABS (BackendType): JarvisLabs\n KUBERNETES (BackendType): Kubernetes\n LAMBDA (BackendType): Lambda Cloud\n NEBIUS (BackendType): Nebius AI Cloud\n OCI (BackendType): Oracle Cloud Infrastructure\n RUNPOD (BackendType): Runpod Cloud\n TENSORDOCK (BackendType): TensorDock Marketplace\n VASTAI (BackendType): Vast.ai Marketplace\n VERDA (BackendType): Verda Cloud\n VULTR (BackendType): Vultr"}, "CPUArchitecture": {"type": "string", "enum": ["x86", "arm"], "title": "CPUArchitecture", "description": "An enumeration."}, "CPUSpecRequest": {"properties": {"arch": {"allOf": [{"$ref": "#/components/schemas/CPUArchitecture"}], "description": "The CPU architecture, one of: `x86`, `arm`"}, "count": {"anyOf": [{"$ref": "#/components/schemas/Range_int_"}, {"type": "integer"}, {"type": "string"}], "title": "Count", "description": "The number of CPU cores", "default": {"min": 2}}}, "additionalProperties": false, "type": "object", "title": "CPUSpecRequest"}, "CreationPolicy": {"type": "string", "enum": ["reuse", "reuse-or-create"], "title": "CreationPolicy", "description": "An enumeration."}, "DevEnvironmentConfigurationRequest": {"properties": {"ide": {"anyOf": [{"type": "string", "enum": ["vscode"]}, {"type": "string", "enum": ["cursor"]}, {"type": "string", "enum": ["windsurf"]}, {"type": "string", "enum": ["zed"]}], "title": "Ide", "description": "The IDE to pre-install. Supported values include `vscode`, `cursor`, `windsurf`, and `zed`. Defaults to no IDE (SSH only)"}, "version": {"type": "string", "title": "Version", "description": "The version of the IDE. For `windsurf`, the version is in the format `version@commit`"}, "init": {"items": {"type": "string"}, "type": "array", "title": "Init", "description": "The shell commands to run on startup", "default": []}, "inactivity_duration": {"anyOf": [{"type": "string", "enum": ["off"]}, {"type": "integer"}, {"type": "boolean"}, {"type": "string"}], "title": "Inactivity Duration", "description": "The maximum amount of time the dev environment can be inactive (e.g., `2h`, `1d`, etc). After it elapses, the dev environment is automatically stopped. Inactivity is defined as the absence of SSH connections to the dev environment, including VS Code connections, `ssh ` shells, and attached `dstack apply` or `dstack attach` commands. Use `off` for unlimited duration. Can be updated in-place. Defaults to `off`"}, "ports": {"items": {"anyOf": [{"type": "integer", "maximum": 65536.0, "exclusiveMinimum": 0.0}, {"type": "string", "pattern": "^(?:[0-9]+|\\*):[0-9]+$"}, {"$ref": "#/components/schemas/PortMappingRequest"}]}, "type": "array", "title": "Ports", "description": "Port numbers/mapping to expose", "default": []}, "type": {"type": "string", "enum": ["dev-environment"], "title": "Type", "default": "dev-environment"}, "name": {"type": "string", "title": "Name", "description": "The run name. If not specified, a random name is generated"}, "image": {"type": "string", "title": "Image", "description": "The name of the Docker image to run"}, "user": {"type": "string", "title": "User", "description": "The user inside the container, `user_name_or_id[:group_name_or_id]` (e.g., `ubuntu`, `1000:1000`). Defaults to the default user from the `image`"}, "privileged": {"type": "boolean", "title": "Privileged", "description": "Run the container in privileged mode", "default": false}, "entrypoint": {"type": "string", "title": "Entrypoint", "description": "The Docker entrypoint"}, "working_dir": {"type": "string", "title": "Working Dir", "description": "The absolute path to the working directory inside the container. Defaults to the `image`'s default working directory"}, "home_dir": {"type": "string", "title": "Home Dir", "default": "/root"}, "registry_auth": {"allOf": [{"$ref": "#/components/schemas/RegistryAuthRequest"}], "title": "Registry Auth", "description": "Credentials for pulling a private Docker image"}, "python": {"allOf": [{"$ref": "#/components/schemas/PythonVersion"}], "description": "The major version of Python. Mutually exclusive with `image` and `docker`"}, "nvcc": {"type": "boolean", "title": "Nvcc", "description": "Use image with NVIDIA CUDA Compiler (NVCC) included. Mutually exclusive with `image` and `docker`"}, "single_branch": {"type": "boolean", "title": "Single Branch", "description": "Whether to clone and track only the current branch or all remote branches. Relevant only when using remote Git repos. Defaults to `false` for dev environments and to `true` for tasks and services"}, "env": {"allOf": [{"$ref": "#/components/schemas/Env"}], "title": "Env", "description": "The mapping or the list of environment variables", "default": {"__root__": {}}}, "shell": {"type": "string", "title": "Shell", "description": "The shell used to run commands. Allowed values are `sh`, `bash`, or an absolute path, e.g., `/usr/bin/zsh`. Defaults to `/bin/sh` if the `image` is specified, `/bin/bash` otherwise"}, "resources": {"allOf": [{"$ref": "#/components/schemas/ResourcesSpecRequest"}], "title": "Resources", "description": "The resources requirements to run the configuration", "default": {"cpu": {"min": 2}, "memory": {"min": 8.0}, "gpu": {"count": {"min": 0}}, "disk": {"size": {"min": 100.0}}}}, "priority": {"type": "integer", "maximum": 100.0, "minimum": 0.0, "title": "Priority", "description": "The priority of the run, an integer between `0` and `100`. `dstack` tries to provision runs with higher priority first. Defaults to `0`"}, "volumes": {"items": {"anyOf": [{"$ref": "#/components/schemas/VolumeMountPointRequest"}, {"$ref": "#/components/schemas/InstanceMountPointRequest"}, {"type": "string"}]}, "type": "array", "title": "Volumes", "description": "The volumes mount points", "default": []}, "docker": {"type": "boolean", "title": "Docker", "description": "Use Docker inside the container. Mutually exclusive with `image`, `python`, and `nvcc`. Overrides `privileged`"}, "repos": {"items": {"$ref": "#/components/schemas/RepoSpecRequest"}, "type": "array", "title": "Repos", "description": "The list of Git repos", "default": []}, "files": {"items": {"anyOf": [{"$ref": "#/components/schemas/FilePathMappingRequest"}, {"type": "string"}]}, "type": "array", "title": "Files", "description": "The local to container file path mappings", "default": []}, "setup": {"items": {"type": "string"}, "type": "array", "title": "Setup", "default": []}, "backends": {"items": {"$ref": "#/components/schemas/BackendType"}, "type": "array", "description": "The backends to consider for provisioning (e.g., `[aws, gcp]`)"}, "regions": {"items": {"type": "string"}, "type": "array", "title": "Regions", "description": "The regions to consider for provisioning (e.g., `[eu-west-1, us-west4, westeurope]`)"}, "availability_zones": {"items": {"type": "string"}, "type": "array", "title": "Availability Zones", "description": "The availability zones to consider for provisioning (e.g., `[eu-west-1a, us-west4-a]`)"}, "instance_types": {"items": {"type": "string"}, "type": "array", "title": "Instance Types", "description": "The cloud-specific instance types to consider for provisioning (e.g., `[g6e.24xlarge, n1-standard-4]`)"}, "reservation": {"type": "string", "title": "Reservation", "description": "The existing reservation to use for instance provisioning. Supports AWS Capacity Reservations, AWS Capacity Blocks, and GCP reservations"}, "spot_policy": {"allOf": [{"$ref": "#/components/schemas/SpotPolicy"}], "description": "The policy for provisioning spot or on-demand instances: `spot`, `on-demand`, `auto`. Defaults to `on-demand`"}, "retry": {"anyOf": [{"$ref": "#/components/schemas/ProfileRetryRequest"}, {"type": "boolean"}], "title": "Retry", "description": "The policy for resubmitting the run. Defaults to `false`"}, "max_duration": {"anyOf": [{"type": "string", "enum": ["off"]}, {"type": "integer"}, {"type": "boolean"}, {"type": "string"}], "title": "Max Duration", "description": "The maximum duration of a run (e.g., `2h`, `1d`, etc) in a running state, excluding provisioning and pulling. After it elapses, the run is automatically stopped. Use `off` for unlimited duration. Defaults to `off`"}, "stop_duration": {"anyOf": [{"type": "string", "enum": ["off"]}, {"type": "integer"}, {"type": "boolean"}, {"type": "string"}], "title": "Stop Duration", "description": "The maximum duration of a run graceful stopping. After it elapses, the run is automatically forced stopped. This includes force detaching volumes used by the run. Use `off` for unlimited duration. Defaults to `5m`"}, "max_price": {"type": "number", "exclusiveMinimum": 0.0, "title": "Max Price", "description": "The maximum instance price per hour, in dollars"}, "creation_policy": {"allOf": [{"$ref": "#/components/schemas/CreationPolicy"}], "description": "The policy for using instances from fleets: `reuse`, `reuse-or-create`. Defaults to `reuse-or-create`"}, "idle_duration": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Idle Duration", "description": "Time to wait before terminating idle instances. When the run reuses an existing fleet instance, the fleet's `idle_duration` applies. When the run provisions a new instance, the shorter of the fleet's and run's values is used. Defaults to `5m` for runs and `3d` for fleets. Use `off` for unlimited duration. Only applied for VM-based backends"}, "utilization_policy": {"allOf": [{"$ref": "#/components/schemas/UtilizationPolicyRequest"}], "title": "Utilization Policy", "description": "Run termination policy based on utilization"}, "startup_order": {"allOf": [{"$ref": "#/components/schemas/StartupOrder"}], "description": "The order in which master and workers jobs are started: `any`, `master-first`, `workers-first`. Defaults to `any`"}, "stop_criteria": {"allOf": [{"$ref": "#/components/schemas/StopCriteria"}], "description": "The criteria determining when a multi-node run should be considered finished: `all-done`, `master-done`. Defaults to `all-done`"}, "schedule": {"allOf": [{"$ref": "#/components/schemas/ScheduleRequest"}], "title": "Schedule", "description": "The schedule for starting the run at specified time"}, "fleets": {"items": {"anyOf": [{"$ref": "#/components/schemas/EntityReferenceRequest"}, {"type": "string"}]}, "type": "array", "title": "Fleets", "description": "The fleets considered for reuse. For fleets owned by the current project, specify fleet names. For imported fleets, specify `/`"}, "instances": {"items": {"anyOf": [{"$ref": "#/components/schemas/InstanceNameSelectorRequest"}, {"$ref": "#/components/schemas/InstanceHostnameSelectorRequest"}, {"$ref": "#/components/schemas/FleetInstanceSelectorRequest"}, {"type": "string", "minLength": 1}]}, "type": "array", "minItems": 1, "title": "Instances", "description": "The specific fleet instances to consider for reuse. Each value can be an instance name string, or an object with `name`, `hostname`, or `fleet` and `instance`. When set, the run is only placed on matching existing instances."}, "tags": {"additionalProperties": {"type": "string"}, "type": "object", "title": "Tags", "description": "The custom tags to associate with the resource. The tags are also propagated to the underlying backend resources. If there is a conflict with backend-level tags, does not override them"}, "backend_options": {"items": {"$ref": "#/components/schemas/VastAIProfileOptions"}, "type": "array", "title": "Backend Options", "description": "Backend-specific options, applied only to offers from that backend"}}, "additionalProperties": false, "type": "object", "title": "DevEnvironmentConfigurationRequest"}, "DiskSpecRequest": {"properties": {"size": {"anyOf": [{"$ref": "#/components/schemas/Range_Memory_"}, {"type": "integer"}, {"type": "string"}], "title": "Size", "description": "Disk size"}}, "additionalProperties": false, "type": "object", "required": ["size"], "title": "DiskSpecRequest"}, "EntityReferenceRequest": {"properties": {"project": {"type": "string", "title": "Project", "description": "The project name. If unspecified, refers to the current project"}, "name": {"type": "string", "title": "Name", "description": "The entity name"}}, "additionalProperties": false, "type": "object", "required": ["name"], "title": "EntityReferenceRequest", "description": "Cross-project entity reference."}, "Env": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"additionalProperties": {"anyOf": [{"type": "string"}, {"$ref": "#/components/schemas/EnvSentinelRequest"}]}, "type": "object"}], "title": "Env", "description": "Env represents a mapping of process environment variables, as in environ(7).\nEnvironment values may be omitted, in that case the :class:`EnvSentinel`\nobject is used as a placeholder.\n\nTo create an instance from a `dict[str, str]` or a `list[str]` use pydantic's\n:meth:`BaseModel.parse_obj(dict | list)` method.\n\nNB: this is *NOT* a CoreModel, pydantic-duality, which is used as a base\nfor the CoreModel, doesn't play well with custom root models.", "default": {}}, "EnvSentinelRequest": {"properties": {"key": {"type": "string", "title": "Key"}}, "additionalProperties": false, "type": "object", "required": ["key"], "title": "EnvSentinelRequest"}, "FileArchiveMappingRequest": {"properties": {"id": {"type": "string", "format": "uuid", "title": "Id", "description": "The File archive ID"}, "path": {"type": "string", "title": "Path", "description": "The path in the container"}}, "additionalProperties": false, "type": "object", "required": ["id", "path"], "title": "FileArchiveMappingRequest"}, "FilePathMappingRequest": {"properties": {"local_path": {"type": "string", "title": "Local Path", "description": "The path on the user's machine. Relative paths are resolved relative to the parent directory of the the configuration file"}, "path": {"type": "string", "title": "Path", "description": "The path in the container. Relative paths are resolved relative to the working directory"}}, "additionalProperties": false, "type": "object", "required": ["local_path", "path"], "title": "FilePathMappingRequest"}, "FleetConfigurationRequest": {"properties": {"type": {"type": "string", "enum": ["fleet"], "title": "Type", "default": "fleet"}, "name": {"type": "string", "title": "Name", "description": "The fleet name"}, "placement": {"allOf": [{"$ref": "#/components/schemas/InstanceGroupPlacement"}], "description": "The placement of instances: `any` or `cluster`"}, "blocks": {"anyOf": [{"type": "string", "enum": ["auto"]}, {"type": "integer", "minimum": 1.0}], "title": "Blocks", "description": "The amount of blocks to split the instance into, a number or `auto`. `auto` means as many as possible. The number of GPUs and CPUs must be divisible by the number of blocks. Defaults to `1`, i.e. do not split", "default": 1}, "nodes": {"anyOf": [{"$ref": "#/components/schemas/FleetNodesSpecRequest"}, {"type": "integer"}, {"type": "string"}], "title": "Nodes", "description": "The number of instances"}, "reservation": {"type": "string", "title": "Reservation", "description": "The existing reservation to use for instance provisioning. Supports AWS Capacity Reservations, AWS Capacity Blocks, and GCP reservations"}, "resources": {"allOf": [{"$ref": "#/components/schemas/ResourcesSpecRequest"}], "title": "Resources", "description": "The resources requirements"}, "backends": {"items": {"$ref": "#/components/schemas/BackendType"}, "type": "array", "description": "The backends to consider for provisioning (e.g., `[aws, gcp]`)"}, "regions": {"items": {"type": "string"}, "type": "array", "title": "Regions", "description": "The regions to consider for provisioning (e.g., `[eu-west-1, us-west4, westeurope]`)"}, "availability_zones": {"items": {"type": "string"}, "type": "array", "title": "Availability Zones", "description": "The availability zones to consider for provisioning (e.g., `[eu-west-1a, us-west4-a]`)"}, "instance_types": {"items": {"type": "string"}, "type": "array", "title": "Instance Types", "description": "The cloud-specific instance types to consider for provisioning (e.g., `[g6e.24xlarge, n1-standard-4]`)"}, "spot_policy": {"allOf": [{"$ref": "#/components/schemas/SpotPolicy"}], "description": "The policy for provisioning spot or on-demand instances: `spot`, `on-demand`, `auto`. Defaults to `on-demand`"}, "retry": {"anyOf": [{"$ref": "#/components/schemas/ProfileRetryRequest"}, {"type": "boolean"}], "title": "Retry", "description": "The policy for provisioning retry. Defaults to `false`"}, "max_price": {"type": "number", "exclusiveMinimum": 0.0, "title": "Max Price", "description": "The maximum instance price per hour, in dollars"}, "idle_duration": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Idle Duration", "description": "Time to wait before terminating idle instances. Instances are not terminated if the fleet is already at `nodes.min`. Defaults to `5m` for runs and `3d` for fleets. Use `off` for unlimited duration"}, "tags": {"additionalProperties": {"type": "string"}, "type": "object", "title": "Tags", "description": "The custom tags to associate with the resource. The tags are also propagated to the underlying backend resources. If there is a conflict with backend-level tags, does not override them"}, "backend_options": {"items": {"$ref": "#/components/schemas/VastAIProfileOptions"}, "type": "array", "title": "Backend Options", "description": "Backend-specific options, applied only to offers from that backend"}, "ssh_config": {"allOf": [{"$ref": "#/components/schemas/SSHParamsRequest"}], "title": "Ssh Config", "description": "The parameters for adding instances via SSH"}, "env": {"allOf": [{"$ref": "#/components/schemas/Env"}], "title": "Env", "description": "The mapping or the list of environment variables", "default": {"__root__": {}}}}, "additionalProperties": false, "type": "object", "title": "FleetConfigurationRequest"}, "FleetInstanceSelectorRequest": {"properties": {"fleet": {"anyOf": [{"$ref": "#/components/schemas/EntityReferenceRequest"}, {"type": "string", "minLength": 1}], "title": "Fleet", "description": "The fleet reference. For fleets owned by the current project, specify the fleet name. For a fleet from another project, specify `/` or an object with `project` and `name`."}, "instance": {"type": "integer", "minimum": 0.0, "title": "Instance", "description": "The fleet instance number"}}, "additionalProperties": false, "type": "object", "required": ["fleet", "instance"], "title": "FleetInstanceSelectorRequest"}, "FleetNodesSpecRequest": {"properties": {"min": {"type": "integer", "title": "Min", "description": "The minimum number of instances to maintain in the fleet"}, "target": {"type": "integer", "title": "Target", "description": "The number of instances to provision on fleet apply. `min` <= `target` <= `max` Defaults to `min`"}, "max": {"type": "integer", "title": "Max", "description": "The maximum number of instances allowed in the fleet. Unlimited if not specified"}}, "additionalProperties": false, "type": "object", "required": ["min", "target"], "title": "FleetNodesSpecRequest"}, "FleetSpecRequest": {"properties": {"configuration": {"$ref": "#/components/schemas/FleetConfigurationRequest"}, "configuration_path": {"type": "string", "title": "Configuration Path"}, "profile": {"$ref": "#/components/schemas/ProfileRequest"}, "autocreated": {"type": "boolean", "title": "Autocreated", "default": false}}, "additionalProperties": false, "type": "object", "required": ["configuration", "profile"], "title": "FleetSpecRequest"}, "GCPVolumeConfigurationRequest": {"properties": {"type": {"type": "string", "enum": ["volume"], "title": "Type", "default": "volume"}, "backend": {"type": "string", "enum": ["gcp"], "title": "Backend", "description": "The volume backend", "default": "gcp"}, "name": {"type": "string", "title": "Name", "description": "The volume name"}, "size": {"type": "number", "title": "Size", "description": "The volume size. Must be specified when creating new volumes"}, "auto_cleanup_duration": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Auto Cleanup Duration", "description": "Time to wait after volume is no longer used by any job before deleting it. Defaults to keep the volume indefinitely. Use the value `off` or `-1` to disable auto-cleanup"}, "tags": {"additionalProperties": {"type": "string"}, "type": "object", "title": "Tags", "description": "The custom tags to associate with the volume. The tags are also propagated to the underlying backend resources. If there is a conflict with backend-level tags, does not override them"}, "volume_id": {"type": "string", "title": "Volume Id", "description": "The volume ID. Must be specified when registering external volumes"}, "region": {"type": "string", "title": "Region", "description": "The volume region"}, "availability_zone": {"type": "string", "title": "Availability Zone", "description": "The volume availability zone"}}, "additionalProperties": false, "type": "object", "required": ["region"], "title": "GCPVolumeConfigurationRequest"}, "GPUSpecRequest": {"properties": {"vendor": {"allOf": [{"$ref": "#/components/schemas/AcceleratorVendor"}], "description": "The vendor of the GPU/accelerator, one of: `nvidia`, `amd`, `google` (alias: `tpu`), `intel`"}, "name": {"anyOf": [{"type": "array"}, {"type": "string"}], "items": {"type": "string"}, "title": "Name", "description": "The name of the GPU (e.g., `A100` or `H100`)"}, "count": {"anyOf": [{"$ref": "#/components/schemas/Range_int_"}, {"type": "integer"}, {"type": "string"}], "title": "Count", "description": "The number of GPUs", "default": {"min": 1}}, "memory": {"anyOf": [{"$ref": "#/components/schemas/Range_Memory_"}, {"type": "integer"}, {"type": "string"}], "title": "Memory", "description": "The RAM size (e.g., `16GB`). Can be set to a range (e.g. `16GB..`, or `16GB..80GB`)"}, "total_memory": {"anyOf": [{"$ref": "#/components/schemas/Range_Memory_"}, {"type": "integer"}, {"type": "string"}], "title": "Total Memory", "description": "The total RAM size (e.g., `32GB`). Can be set to a range (e.g. `16GB..`, or `16GB..80GB`)"}, "compute_capability": {"items": {}, "type": "array", "title": "Compute Capability", "description": "The minimum compute capability of the GPU (e.g., `7.5`)"}}, "additionalProperties": false, "type": "object", "title": "GPUSpecRequest"}, "GatewayConfigurationRequest": {"properties": {"type": {"type": "string", "enum": ["gateway"], "title": "Type", "default": "gateway"}, "name": {"type": "string", "title": "Name", "description": "The gateway name"}, "default": {"type": "boolean", "title": "Default", "description": "Make the gateway default", "default": false}, "backend": {"allOf": [{"$ref": "#/components/schemas/BackendType"}], "description": "The gateway backend"}, "region": {"type": "string", "title": "Region", "description": "The gateway region"}, "instance_type": {"type": "string", "minLength": 1, "title": "Instance Type", "description": "Backend-specific instance type to use for the gateway instance. Omit to use the backend's default, which is typically a small non-GPU instance"}, "router": {"allOf": [{"$ref": "#/components/schemas/SGLangGatewayRouterConfigRequest"}], "title": "Router", "description": "The router configuration for this gateway. E.g. `{ type: sglang, policy: round_robin }`."}, "domain": {"type": "string", "title": "Domain", "description": "The gateway wildcard domain name, e.g. `example.com`. Service domain names are constructed as `./`"}, "instances": {"items": {"anyOf": [{"$ref": "#/components/schemas/InstanceNameSelectorRequest"}, {"$ref": "#/components/schemas/InstanceHostnameSelectorRequest"}, {"$ref": "#/components/schemas/FleetInstanceSelectorRequest"}, {"type": "string", "minLength": 1}]}, "type": "array", "minItems": 1, "title": "Instances", "description": "The specific fleet instances to consider for reuse. Each value can be an instance name string, or an object with `name`, `hostname`, or `fleet` and `instance`. When set, the run is only placed on matching existing instances."}, "tags": {"additionalProperties": {"type": "string"}, "type": "object", "title": "Tags", "description": "The custom tags to associate with the resource. The tags are also propagated to the underlying backend resources. If there is a conflict with backend-level tags, does not override them"}, "backend_options": {"items": {"$ref": "#/components/schemas/VastAIProfileOptions"}, "type": "array", "title": "Backend Options", "description": "Backend-specific options, applied only to offers from that backend"}, "name": {"type": "string", "title": "Name", "description": "The name of the profile that can be passed as `--profile` to `dstack apply`", "default": ""}, "default": {"type": "boolean", "title": "Default", "description": "If set to true, `dstack apply` will use this profile by default.", "default": false}}, "additionalProperties": false, "type": "object", "title": "ProfileRequest"}, "ProfileRetryRequest": {"properties": {"on_events": {"items": {"$ref": "#/components/schemas/RetryEvent"}, "type": "array", "description": "The list of events that should be handled with retry. Supported events are `no-capacity`, `interruption`, `error`. Omit to retry on all events"}, "duration": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Duration", "description": "The maximum period of retrying the run, e.g., `4h` or `1d`. The period is calculated as a run age for `no-capacity` event and as a time passed since the last `interruption` and `error` for `interruption` and `error` events."}}, "additionalProperties": false, "type": "object", "title": "ProfileRetryRequest"}, "PythonVersion": {"type": "string", "enum": ["3.9", "3.10", "3.11", "3.12", "3.13"], "title": "PythonVersion", "description": "An enumeration."}, "Range_Memory_": {"properties": {"min": {"type": "number", "title": "Min"}, "max": {"type": "number", "title": "Max"}}, "additionalProperties": false, "type": "object", "title": "Range[Memory]"}, "Range_int_": {"properties": {"min": {"type": "integer", "title": "Min"}, "max": {"type": "integer", "title": "Max"}}, "additionalProperties": false, "type": "object", "title": "Range[int]"}, "RateLimitRequest": {"properties": {"prefix": {"type": "string", "maxLength": 4094, "pattern": "^/[^\\s\\\\{}]*$", "title": "Prefix", "description": "URL path prefix to which this limit is applied. If an incoming request matches several prefixes, the longest prefix is applied", "default": "/"}, "key": {"oneOf": [{"$ref": "#/components/schemas/IPAddressPartitioningKeyRequest"}, {"$ref": "#/components/schemas/HeaderPartitioningKeyRequest"}], "title": "Key", "description": "The partitioning key. Each incoming request belongs to a partition and rate limits are applied per partition. Defaults to partitioning by client IP address", "default": {"type": "ip_address"}, "discriminator": {"propertyName": "type", "mapping": {"ip_address": "#/components/schemas/IPAddressPartitioningKeyRequest", "header": "#/components/schemas/HeaderPartitioningKeyRequest"}}}, "rps": {"type": "number", "maximum": 1.5372286728091293e+17, "minimum": 0.016666666666666666, "title": "Rps", "description": "Max allowed number of requests per second. Requests are tracked at millisecond granularity. For example, `rps: 10` means at most 1 request per 100ms"}, "burst": {"type": "integer", "maximum": 9.223372036854776e+18, "minimum": 0.0, "title": "Burst", "description": "Max number of requests that can be passed to the service ahead of the rate limit", "default": 0}}, "additionalProperties": false, "type": "object", "required": ["rps"], "title": "RateLimitRequest"}, "RegistryAuthRequest": {"properties": {"username": {"type": "string", "title": "Username", "description": "The username"}, "password": {"type": "string", "title": "Password", "description": "The password or access token"}}, "additionalProperties": false, "type": "object", "required": ["username", "password"], "title": "RegistryAuthRequest", "description": "Credentials for pulling a private Docker image.\n\nAttributes:\n username (str): The username\n password (str): The password or access token"}, "RemoteRunRepoDataRequest": {"properties": {"repo_type": {"type": "string", "enum": ["remote"], "title": "Repo Type", "default": "remote"}, "repo_name": {"type": "string", "title": "Repo Name"}, "repo_branch": {"type": "string", "title": "Repo Branch"}, "repo_hash": {"type": "string", "title": "Repo Hash"}, "repo_diff": {"type": "string", "format": "binary", "title": "Repo Diff"}, "repo_config_name": {"type": "string", "title": "Repo Config Name"}, "repo_config_email": {"type": "string", "title": "Repo Config Email"}}, "additionalProperties": false, "type": "object", "required": ["repo_name"], "title": "RemoteRunRepoDataRequest"}, "ReplicaGroupRequest": {"properties": {"name": {"type": "string", "title": "Name", "description": "The name of the replica group. If not provided, defaults to '0', '1', etc. based on position."}, "count": {"allOf": [{"$ref": "#/components/schemas/Range_int_"}], "title": "Count", "description": "The number of replicas. Can be a number (e.g. `2`) or a range (`0..4` or `1..8`). If it's a range, the `scaling` property is required"}, "scaling": {"allOf": [{"$ref": "#/components/schemas/ScalingSpecRequest"}], "title": "Scaling", "description": "The auto-scaling rules. Required if `count` is set to a range"}, "resources": {"allOf": [{"$ref": "#/components/schemas/ResourcesSpecRequest"}], "title": "Resources", "description": "The resources requirements for replicas in this group", "default": {"cpu": {"min": 2}, "memory": {"min": 8.0}, "gpu": {"count": {"min": 0}}, "disk": {"size": {"min": 100.0}}}}, "spot_policy": {"allOf": [{"$ref": "#/components/schemas/SpotPolicy"}], "description": "The policy for provisioning spot or on-demand instances for replicas in this group: `spot`, `on-demand`, `auto`"}, "reservation": {"type": "string", "title": "Reservation", "description": "The existing reservation to use for replicas in this group. Supports AWS Capacity Reservations, AWS Capacity Blocks, and GCP reservations"}, "commands": {"items": {"type": "string"}, "type": "array", "title": "Commands", "description": "The shell commands to run for replicas in this group", "default": []}, "image": {"type": "string", "title": "Image", "description": "The name of the Docker image to run for replicas in this group. Mutually exclusive with group-level `docker` and `python`."}, "python": {"allOf": [{"$ref": "#/components/schemas/PythonVersion"}], "description": "The major version of Python for replicas in this group. Mutually exclusive with group-level `image` and `docker`."}, "nvcc": {"type": "boolean", "title": "Nvcc", "description": "Use the image with NVIDIA CUDA Compiler (NVCC) included for replicas in this group. Mutually exclusive with group-level `docker`."}, "docker": {"type": "boolean", "title": "Docker", "description": "Use the docker-in-docker image for this group (injects `start-dockerd` and runs privileged). Mutually exclusive with group-level `image`, `python`, and `nvcc`."}, "privileged": {"type": "boolean", "title": "Privileged", "description": "Run replicas in this group in privileged mode."}, "router": {"allOf": [{"$ref": "#/components/schemas/ReplicaGroupRouterConfigRequest"}], "title": "Router", "description": "When set, replicas in this group run the in-service HTTP router (e.g. SGLang)."}}, "additionalProperties": false, "type": "object", "required": ["count"], "title": "ReplicaGroupRequest"}, "ReplicaGroupRouterConfigRequest": {"properties": {"type": {"type": "string", "enum": ["sglang", "dynamo"], "title": "Type", "description": "The router implementation for this replica group. `sglang` runs the SGLang router and dstack syncs worker URLs to it. `dynamo` runs the NVIDIA Dynamo frontend, which discovers workers itself via etcd/NATS.", "default": "sglang"}}, "additionalProperties": false, "type": "object", "title": "ReplicaGroupRouterConfigRequest"}, "RepoExistsAction": {"type": "string", "enum": ["error", "skip"], "title": "RepoExistsAction", "description": "An enumeration."}, "RepoSpecRequest": {"properties": {"local_path": {"type": "string", "title": "Local Path", "description": "The path to the Git repo on the user's machine. Relative paths are resolved relative to the parent directory of the the configuration file. Mutually exclusive with `url`"}, "url": {"type": "string", "title": "Url", "description": "The Git repo URL. Mutually exclusive with `local_path`"}, "branch": {"type": "string", "title": "Branch", "description": "The repo branch. Defaults to the active branch for local paths and the default branch for URLs"}, "hash": {"type": "string", "title": "Hash", "description": "The commit hash"}, "path": {"type": "string", "title": "Path", "description": "The repo path inside the run container. Relative paths are resolved relative to the working directory", "default": "."}, "if_exists": {"allOf": [{"$ref": "#/components/schemas/RepoExistsAction"}], "description": "The action to be taken if `path` exists and is not empty. One of: `error`, `skip`", "default": "error"}}, "additionalProperties": false, "type": "object", "title": "RepoSpecRequest"}, "ResourcesSpecRequest": {"properties": {"cpu": {"anyOf": [{"$ref": "#/components/schemas/CPUSpecRequest"}, {"$ref": "#/components/schemas/Range_int_"}, {"type": "integer"}, {"type": "string"}], "title": "Cpu", "description": "The CPU requirements", "default": {"count": {"min": 2}}}, "memory": {"anyOf": [{"$ref": "#/components/schemas/Range_Memory_"}, {"type": "integer"}, {"type": "string"}], "title": "Memory", "description": "The RAM size (e.g., `8GB`)", "default": {"min": 8.0}}, "shm_size": {"anyOf": [{"type": "number"}, {"type": "integer"}, {"type": "string"}], "title": "Shm Size", "description": "The size of shared memory (e.g., `8GB`). If you are using parallel communicating processes (e.g., dataloaders in PyTorch), you may need to configure this"}, "gpu": {"anyOf": [{"$ref": "#/components/schemas/GPUSpecRequest"}, {"type": "integer"}, {"type": "string"}], "title": "Gpu", "description": "The GPU requirements", "default": {"count": {"min": 0}}}, "disk": {"anyOf": [{"$ref": "#/components/schemas/DiskSpecRequest"}, {"type": "integer"}, {"type": "string"}], "title": "Disk", "description": "The disk resources", "default": {"size": {"min": 100.0}}}}, "additionalProperties": false, "type": "object", "title": "ResourcesSpecRequest"}, "RetryEvent": {"type": "string", "enum": ["no-capacity", "interruption", "error"], "title": "RetryEvent", "description": "An enumeration."}, "RunSpecRequest": {"properties": {"run_name": {"type": "string", "title": "Run Name", "description": "The run name. If not set, the run name is generated automatically."}, "repo_id": {"type": "string", "title": "Repo Id", "description": "Same `repo_id` that is specified when initializing the repo by calling the `/api/project/{project_name}/repos/init` endpoint. If not specified, a default virtual repo is used."}, "repo_data": {"oneOf": [{"$ref": "#/components/schemas/RemoteRunRepoDataRequest"}, {"$ref": "#/components/schemas/LocalRunRepoDataRequest"}, {"$ref": "#/components/schemas/VirtualRunRepoDataRequest"}], "title": "Repo Data", "description": "The repo data such as the current branch and commit.", "discriminator": {"propertyName": "repo_type", "mapping": {"remote": "#/components/schemas/RemoteRunRepoDataRequest", "local": "#/components/schemas/LocalRunRepoDataRequest", "virtual": "#/components/schemas/VirtualRunRepoDataRequest"}}}, "repo_code_hash": {"type": "string", "title": "Repo Code Hash", "description": "The hash of the repo diff. Can be omitted if there is no repo diff."}, "repo_dir": {"type": "string", "title": "Repo Dir", "description": "The repo path inside the container. Relative paths are resolved relative to the working directory."}, "file_archives": {"items": {"$ref": "#/components/schemas/FileArchiveMappingRequest"}, "type": "array", "title": "File Archives", "description": "The list of file archive ID to container path mappings.", "default": []}, "working_dir": {"type": "string", "title": "Working Dir"}, "configuration_path": {"type": "string", "title": "Configuration Path", "description": "The path to the run configuration YAML file. It can be omitted when using the programmatic API."}, "configuration": {"oneOf": [{"$ref": "#/components/schemas/DevEnvironmentConfigurationRequest"}, {"$ref": "#/components/schemas/TaskConfigurationRequest"}, {"$ref": "#/components/schemas/ServiceConfigurationRequest"}], "title": "Configuration", "discriminator": {"propertyName": "type", "mapping": {"dev-environment": "#/components/schemas/DevEnvironmentConfigurationRequest", "task": "#/components/schemas/TaskConfigurationRequest", "service": "#/components/schemas/ServiceConfigurationRequest"}}}, "profile": {"allOf": [{"$ref": "#/components/schemas/ProfileRequest"}], "title": "Profile", "description": "The profile parameters"}, "ssh_key_pub": {"type": "string", "title": "Ssh Key Pub", "description": "The contents of the SSH public key that will be used to connect to the run. Can be empty only before the run is submitted."}}, "additionalProperties": false, "type": "object", "required": ["configuration"], "title": "RunSpecRequest"}, "RunpodVolumeConfigurationRequest": {"properties": {"type": {"type": "string", "enum": ["volume"], "title": "Type", "default": "volume"}, "backend": {"type": "string", "enum": ["runpod"], "title": "Backend", "description": "The volume backend", "default": "runpod"}, "name": {"type": "string", "title": "Name", "description": "The volume name"}, "size": {"type": "number", "title": "Size", "description": "The volume size. Must be specified when creating new volumes"}, "auto_cleanup_duration": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Auto Cleanup Duration", "description": "Time to wait after volume is no longer used by any job before deleting it. Defaults to keep the volume indefinitely. Use the value `off` or `-1` to disable auto-cleanup"}, "tags": {"additionalProperties": {"type": "string"}, "type": "object", "title": "Tags", "description": "The custom tags to associate with the volume. The tags are also propagated to the underlying backend resources. If there is a conflict with backend-level tags, does not override them"}, "volume_id": {"type": "string", "title": "Volume Id", "description": "The volume ID. Must be specified when registering external volumes"}, "region": {"type": "string", "title": "Region", "description": "The volume region"}, "availability_zone": {"type": "string", "title": "Availability Zone"}}, "additionalProperties": false, "type": "object", "required": ["region"], "title": "RunpodVolumeConfigurationRequest"}, "SGLangGatewayRouterConfigRequest": {"properties": {"type": {"type": "string", "enum": ["sglang"], "title": "Type", "description": "The router type enabled on this gateway.", "default": "sglang"}, "policy": {"type": "string", "enum": ["random", "round_robin", "cache_aware", "power_of_two"], "title": "Policy", "description": "The routing policy. Deprecated: prefer setting policy in the service's router config. Options: `random`, `round_robin`, `cache_aware`, `power_of_two`", "default": "cache_aware"}}, "additionalProperties": false, "type": "object", "title": "SGLangGatewayRouterConfigRequest", "description": "Gateway-level router configuration. type and policy only. pd_disaggregation is service-level."}, "SGLangServiceRouterConfigRequest": {"properties": {"type": {"type": "string", "enum": ["sglang"], "title": "Type", "description": "The router type", "default": "sglang"}, "policy": {"type": "string", "enum": ["random", "round_robin", "cache_aware", "power_of_two"], "title": "Policy", "description": "The routing policy. Options: `random`, `round_robin`, `cache_aware`, `power_of_two`", "default": "cache_aware"}, "pd_disaggregation": {"type": "boolean", "title": "Pd Disaggregation", "description": "Enable PD disaggregation mode for the SGLang router", "default": false}}, "additionalProperties": false, "type": "object", "title": "SGLangServiceRouterConfigRequest"}, "SSHHostParamsRequest": {"properties": {"hostname": {"type": "string", "title": "Hostname", "description": "The IP address or domain to connect to"}, "port": {"type": "integer", "title": "Port", "description": "The SSH port to connect to for this host"}, "user": {"type": "string", "title": "User", "description": "The user to log in with for this host"}, "identity_file": {"type": "string", "title": "Identity File", "description": "The private key to use for this host"}, "proxy_jump": {"allOf": [{"$ref": "#/components/schemas/SSHProxyParamsRequest"}], "title": "Proxy Jump", "description": "The SSH proxy configuration for this host"}, "internal_ip": {"type": "string", "title": "Internal Ip", "description": "The internal IP of the host used for communication inside the cluster. If not specified, `dstack` will use the IP address from `network` or from the first found internal network."}, "ssh_key": {"$ref": "#/components/schemas/SSHKeyRequest"}, "blocks": {"anyOf": [{"type": "string", "enum": ["auto"]}, {"type": "integer", "minimum": 1.0}], "title": "Blocks", "description": "The amount of blocks to split the instance into, a number or `auto`. `auto` means as many as possible. The number of GPUs and CPUs must be divisible by the number of blocks. Defaults to the top-level `blocks` value"}}, "additionalProperties": false, "type": "object", "required": ["hostname"], "title": "SSHHostParamsRequest"}, "SSHKeyRequest": {"properties": {"public": {"type": "string", "title": "Public"}, "private": {"type": "string", "title": "Private"}}, "additionalProperties": false, "type": "object", "required": ["public"], "title": "SSHKeyRequest"}, "SSHParamsRequest": {"properties": {"user": {"type": "string", "title": "User", "description": "The user to log in with on all hosts"}, "port": {"type": "integer", "title": "Port", "description": "The SSH port to connect to"}, "identity_file": {"type": "string", "title": "Identity File", "description": "The private key to use for all hosts"}, "ssh_key": {"$ref": "#/components/schemas/SSHKeyRequest"}, "proxy_jump": {"allOf": [{"$ref": "#/components/schemas/SSHProxyParamsRequest"}], "title": "Proxy Jump", "description": "The SSH proxy configuration for all hosts"}, "hosts": {"items": {"anyOf": [{"$ref": "#/components/schemas/SSHHostParamsRequest"}, {"type": "string"}]}, "type": "array", "title": "Hosts", "description": "The per host connection parameters: a hostname or an object that overrides default ssh parameters"}, "network": {"type": "string", "title": "Network", "description": "The network address for cluster setup in the format `/`. `dstack` will use IP addresses from this network for communication between hosts. If not specified, `dstack` will use IPs from the first found internal network."}}, "additionalProperties": false, "type": "object", "required": ["hosts"], "title": "SSHParamsRequest"}, "SSHProxyParamsRequest": {"properties": {"hostname": {"type": "string", "title": "Hostname", "description": "The IP address or domain of proxy host"}, "port": {"type": "integer", "title": "Port", "description": "The SSH port of proxy host"}, "user": {"type": "string", "title": "User", "description": "The user to log in with for proxy host"}, "identity_file": {"type": "string", "title": "Identity File", "description": "The private key to use for proxy host"}, "ssh_key": {"$ref": "#/components/schemas/SSHKeyRequest"}}, "additionalProperties": false, "type": "object", "required": ["hostname", "user", "identity_file"], "title": "SSHProxyParamsRequest"}, "ScalingSpecRequest": {"properties": {"metric": {"type": "string", "enum": ["rps"], "title": "Metric", "description": "The target metric to track. Currently, the only supported value is `rps` (meaning requests per second)"}, "target": {"type": "number", "exclusiveMinimum": 0.0, "title": "Target", "description": "The target value of the metric. The number of replicas is calculated based on this number and automatically adjusts (scales up or down) as this metric changes"}, "window": {"type": "integer", "title": "Window", "description": "The time window used to calculate requests per second. Allowed values: `30s`, `60s`, `300s`. Defaults to `60s`"}, "scale_up_delay": {"type": "integer", "title": "Scale Up Delay", "description": "The minimum time, in seconds, between a scaling event and the next scale-up decision. Used to prevent overly frequent scaling", "default": 300}, "scale_down_delay": {"type": "integer", "title": "Scale Down Delay", "description": "The minimum time, in seconds, between a scaling event and the next scale-down decision. Used to prevent overly frequent scaling", "default": 600}}, "additionalProperties": false, "type": "object", "required": ["metric", "target"], "title": "ScalingSpecRequest"}, "ScheduleRequest": {"properties": {"cron": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "string"}], "title": "Cron", "description": "A cron expression or a list of cron expressions specifying the UTC time when the run needs to be started"}}, "additionalProperties": false, "type": "object", "required": ["cron"], "title": "ScheduleRequest"}, "ServiceConfigurationRequest": {"properties": {"port": {"anyOf": [{"type": "integer", "maximum": 65536.0, "exclusiveMinimum": 0.0}, {"type": "string", "pattern": "^[0-9]+:[0-9]+$"}, {"$ref": "#/components/schemas/PortMappingRequest"}], "title": "Port", "description": "The port the application listens on"}, "gateway": {"anyOf": [{"type": "boolean"}, {"$ref": "#/components/schemas/EntityReferenceRequest"}, {"type": "string"}], "title": "Gateway", "description": "The name of the gateway. Specify boolean `false` to run without a gateway. Specify boolean `true` to run with the default gateway. Omit to run with the default gateway if there is one, or without a gateway otherwise"}, "strip_prefix": {"type": "boolean", "title": "Strip Prefix", "description": "Strip the `/proxy/services///` path prefix when forwarding requests to the service. Only takes effect when running the service without a gateway", "default": true}, "model": {"anyOf": [{"$ref": "#/components/schemas/TGIChatModelRequest"}, {"$ref": "#/components/schemas/OpenAIChatModelRequest"}, {"type": "string"}], "title": "Model", "description": "Mapping of the model for the OpenAI-compatible endpoint provided by `dstack`. Can be a full model format definition or just a model name. If it's a name, the service is expected to expose an OpenAI-compatible API at the `/v1` path"}, "https": {"anyOf": [{"type": "boolean"}, {"type": "string", "enum": ["auto"]}], "title": "Https", "description": "Enable HTTPS if running with a gateway. Set to `auto` to determine automatically based on gateway configuration. Defaults to `true`"}, "auth": {"type": "boolean", "title": "Auth", "description": "Enable the authorization", "default": true}, "scaling": {"allOf": [{"$ref": "#/components/schemas/ScalingSpecRequest"}], "title": "Scaling", "description": "The auto-scaling rules. Required if `replicas` is set to a range"}, "rate_limits": {"items": {"$ref": "#/components/schemas/RateLimitRequest"}, "type": "array", "title": "Rate Limits", "description": "Rate limiting rules", "default": []}, "probes": {"items": {"$ref": "#/components/schemas/ProbeConfigRequest"}, "type": "array", "title": "Probes", "description": "The list of probes to determine service health. If `model` is set, defaults to a `/v1/chat/completions` probe. Set explicitly to override"}, "replicas": {"anyOf": [{"items": {"$ref": "#/components/schemas/ReplicaGroupRequest"}, "type": "array"}, {"$ref": "#/components/schemas/Range_int_"}, {"type": "integer"}, {"type": "string"}], "title": "Replicas", "description": "The number of replicas or a list of replica groups. Can be an integer (e.g., `2`), a range (e.g., `0..4`), or a list of replica groups. Each replica group defines replicas with shared configuration (commands, resources, scaling). When `replicas` is a list of replica groups, top-level `scaling`, `commands`, and `resources` are not allowed and must be specified in each replica group instead. "}, "router": {"allOf": [{"$ref": "#/components/schemas/SGLangServiceRouterConfigRequest"}], "title": "Router", "description": "Router configuration for the service. Requires a gateway with matching router enabled. "}, "commands": {"items": {"type": "string"}, "type": "array", "title": "Commands", "description": "The shell commands to run", "default": []}, "type": {"type": "string", "enum": ["service"], "title": "Type", "default": "service"}, "name": {"type": "string", "title": "Name", "description": "The run name. If not specified, a random name is generated"}, "image": {"type": "string", "title": "Image", "description": "The name of the Docker image to run"}, "user": {"type": "string", "title": "User", "description": "The user inside the container, `user_name_or_id[:group_name_or_id]` (e.g., `ubuntu`, `1000:1000`). Defaults to the default user from the `image`"}, "privileged": {"type": "boolean", "title": "Privileged", "description": "Run the container in privileged mode", "default": false}, "entrypoint": {"type": "string", "title": "Entrypoint", "description": "The Docker entrypoint"}, "working_dir": {"type": "string", "title": "Working Dir", "description": "The absolute path to the working directory inside the container. Defaults to the `image`'s default working directory"}, "home_dir": {"type": "string", "title": "Home Dir", "default": "/root"}, "registry_auth": {"allOf": [{"$ref": "#/components/schemas/RegistryAuthRequest"}], "title": "Registry Auth", "description": "Credentials for pulling a private Docker image"}, "python": {"allOf": [{"$ref": "#/components/schemas/PythonVersion"}], "description": "The major version of Python. Mutually exclusive with `image` and `docker`"}, "nvcc": {"type": "boolean", "title": "Nvcc", "description": "Use image with NVIDIA CUDA Compiler (NVCC) included. Mutually exclusive with `image` and `docker`"}, "single_branch": {"type": "boolean", "title": "Single Branch", "description": "Whether to clone and track only the current branch or all remote branches. Relevant only when using remote Git repos. Defaults to `false` for dev environments and to `true` for tasks and services"}, "env": {"allOf": [{"$ref": "#/components/schemas/Env"}], "title": "Env", "description": "The mapping or the list of environment variables", "default": {"__root__": {}}}, "shell": {"type": "string", "title": "Shell", "description": "The shell used to run commands. Allowed values are `sh`, `bash`, or an absolute path, e.g., `/usr/bin/zsh`. Defaults to `/bin/sh` if the `image` is specified, `/bin/bash` otherwise"}, "resources": {"allOf": [{"$ref": "#/components/schemas/ResourcesSpecRequest"}], "title": "Resources", "description": "The resources requirements to run the configuration", "default": {"cpu": {"min": 2}, "memory": {"min": 8.0}, "gpu": {"count": {"min": 0}}, "disk": {"size": {"min": 100.0}}}}, "priority": {"type": "integer", "maximum": 100.0, "minimum": 0.0, "title": "Priority", "description": "The priority of the run, an integer between `0` and `100`. `dstack` tries to provision runs with higher priority first. Defaults to `0`"}, "volumes": {"items": {"anyOf": [{"$ref": "#/components/schemas/VolumeMountPointRequest"}, {"$ref": "#/components/schemas/InstanceMountPointRequest"}, {"type": "string"}]}, "type": "array", "title": "Volumes", "description": "The volumes mount points", "default": []}, "docker": {"type": "boolean", "title": "Docker", "description": "Use Docker inside the container. Mutually exclusive with `image`, `python`, and `nvcc`. Overrides `privileged`"}, "repos": {"items": {"$ref": "#/components/schemas/RepoSpecRequest"}, "type": "array", "title": "Repos", "description": "The list of Git repos", "default": []}, "files": {"items": {"anyOf": [{"$ref": "#/components/schemas/FilePathMappingRequest"}, {"type": "string"}]}, "type": "array", "title": "Files", "description": "The local to container file path mappings", "default": []}, "setup": {"items": {"type": "string"}, "type": "array", "title": "Setup", "default": []}, "backends": {"items": {"$ref": "#/components/schemas/BackendType"}, "type": "array", "description": "The backends to consider for provisioning (e.g., `[aws, gcp]`)"}, "regions": {"items": {"type": "string"}, "type": "array", "title": "Regions", "description": "The regions to consider for provisioning (e.g., `[eu-west-1, us-west4, westeurope]`)"}, "availability_zones": {"items": {"type": "string"}, "type": "array", "title": "Availability Zones", "description": "The availability zones to consider for provisioning (e.g., `[eu-west-1a, us-west4-a]`)"}, "instance_types": {"items": {"type": "string"}, "type": "array", "title": "Instance Types", "description": "The cloud-specific instance types to consider for provisioning (e.g., `[g6e.24xlarge, n1-standard-4]`)"}, "reservation": {"type": "string", "title": "Reservation", "description": "The existing reservation to use for instance provisioning. Supports AWS Capacity Reservations, AWS Capacity Blocks, and GCP reservations"}, "spot_policy": {"allOf": [{"$ref": "#/components/schemas/SpotPolicy"}], "description": "The policy for provisioning spot or on-demand instances: `spot`, `on-demand`, `auto`. Defaults to `on-demand`"}, "retry": {"anyOf": [{"$ref": "#/components/schemas/ProfileRetryRequest"}, {"type": "boolean"}], "title": "Retry", "description": "The policy for resubmitting the run. Defaults to `false`"}, "max_duration": {"anyOf": [{"type": "string", "enum": ["off"]}, {"type": "integer"}, {"type": "boolean"}, {"type": "string"}], "title": "Max Duration", "description": "The maximum duration of a run (e.g., `2h`, `1d`, etc) in a running state, excluding provisioning and pulling. After it elapses, the run is automatically stopped. Use `off` for unlimited duration. Defaults to `off`"}, "stop_duration": {"anyOf": [{"type": "string", "enum": ["off"]}, {"type": "integer"}, {"type": "boolean"}, {"type": "string"}], "title": "Stop Duration", "description": "The maximum duration of a run graceful stopping. After it elapses, the run is automatically forced stopped. This includes force detaching volumes used by the run. Use `off` for unlimited duration. Defaults to `5m`"}, "max_price": {"type": "number", "exclusiveMinimum": 0.0, "title": "Max Price", "description": "The maximum instance price per hour, in dollars"}, "creation_policy": {"allOf": [{"$ref": "#/components/schemas/CreationPolicy"}], "description": "The policy for using instances from fleets: `reuse`, `reuse-or-create`. Defaults to `reuse-or-create`"}, "idle_duration": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Idle Duration", "description": "Time to wait before terminating idle instances. When the run reuses an existing fleet instance, the fleet's `idle_duration` applies. When the run provisions a new instance, the shorter of the fleet's and run's values is used. Defaults to `5m` for runs and `3d` for fleets. Use `off` for unlimited duration. Only applied for VM-based backends"}, "utilization_policy": {"allOf": [{"$ref": "#/components/schemas/UtilizationPolicyRequest"}], "title": "Utilization Policy", "description": "Run termination policy based on utilization"}, "startup_order": {"allOf": [{"$ref": "#/components/schemas/StartupOrder"}], "description": "The order in which master and workers jobs are started: `any`, `master-first`, `workers-first`. Defaults to `any`"}, "stop_criteria": {"allOf": [{"$ref": "#/components/schemas/StopCriteria"}], "description": "The criteria determining when a multi-node run should be considered finished: `all-done`, `master-done`. Defaults to `all-done`"}, "schedule": {"allOf": [{"$ref": "#/components/schemas/ScheduleRequest"}], "title": "Schedule", "description": "The schedule for starting the run at specified time"}, "fleets": {"items": {"anyOf": [{"$ref": "#/components/schemas/EntityReferenceRequest"}, {"type": "string"}]}, "type": "array", "title": "Fleets", "description": "The fleets considered for reuse. For fleets owned by the current project, specify fleet names. For imported fleets, specify `/`"}, "instances": {"items": {"anyOf": [{"$ref": "#/components/schemas/InstanceNameSelectorRequest"}, {"$ref": "#/components/schemas/InstanceHostnameSelectorRequest"}, {"$ref": "#/components/schemas/FleetInstanceSelectorRequest"}, {"type": "string", "minLength": 1}]}, "type": "array", "minItems": 1, "title": "Instances", "description": "The specific fleet instances to consider for reuse. Each value can be an instance name string, or an object with `name`, `hostname`, or `fleet` and `instance`. When set, the run is only placed on matching existing instances."}, "tags": {"additionalProperties": {"type": "string"}, "type": "object", "title": "Tags", "description": "The custom tags to associate with the resource. The tags are also propagated to the underlying backend resources. If there is a conflict with backend-level tags, does not override them"}, "backend_options": {"items": {"$ref": "#/components/schemas/VastAIProfileOptions"}, "type": "array", "title": "Backend Options", "description": "Backend-specific options, applied only to offers from that backend"}}, "additionalProperties": false, "type": "object", "required": ["port"], "title": "ServiceConfigurationRequest"}, "SpecApplyRequest": {"properties": {"user": {"type": "string", "title": "User", "description": "The name of the user making the apply request"}, "project": {"type": "string", "title": "Project", "description": "The name of the project the request is for"}, "spec": {"anyOf": [{"$ref": "#/components/schemas/RunSpecRequest"}, {"$ref": "#/components/schemas/FleetSpecRequest"}, {"$ref": "#/components/schemas/VolumeSpecRequest"}, {"$ref": "#/components/schemas/GatewaySpecRequest"}], "title": "Spec", "description": "The spec to be applied"}}, "type": "object", "required": ["user", "project", "spec"], "title": "SpecApplyRequest"}, "SpecApplyResponse": {"properties": {"spec": {"anyOf": [{"$ref": "#/components/schemas/RunSpecRequest"}, {"$ref": "#/components/schemas/FleetSpecRequest"}, {"$ref": "#/components/schemas/VolumeSpecRequest"}, {"$ref": "#/components/schemas/GatewaySpecRequest"}], "title": "Spec", "description": "The spec to apply, original spec if error otherwise original or mutated by plugin service if approved"}, "error": {"type": "string", "minLength": 1, "title": "Error", "description": "Error message if request is rejected"}}, "type": "object", "required": ["spec"], "title": "SpecApplyResponse"}, "SpotPolicy": {"type": "string", "enum": ["spot", "on-demand", "auto"], "title": "SpotPolicy", "description": "An enumeration."}, "StartupOrder": {"type": "string", "enum": ["any", "master-first", "workers-first"], "title": "StartupOrder", "description": "An enumeration."}, "StopCriteria": {"type": "string", "enum": ["all-done", "master-done"], "title": "StopCriteria", "description": "An enumeration."}, "TGIChatModelRequest": {"properties": {"type": {"type": "string", "enum": ["chat"], "title": "Type", "description": "The type of the model", "default": "chat"}, "name": {"type": "string", "title": "Name", "description": "The name of the model"}, "format": {"type": "string", "enum": ["tgi"], "title": "Format", "description": "The serving format. Must be set to `tgi`"}, "chat_template": {"type": "string", "title": "Chat Template", "description": "The custom prompt template for the model. If not specified, the default prompt template from the HuggingFace Hub configuration will be used"}, "eos_token": {"type": "string", "title": "Eos Token", "description": "The custom end of sentence token. If not specified, the default end of sentence token from the HuggingFace Hub configuration will be used"}}, "additionalProperties": false, "type": "object", "required": ["name", "format"], "title": "TGIChatModelRequest", "description": "Mapping of the model for the OpenAI-compatible endpoint.\n\nAttributes:\n type (str): The type of the model, e.g. \"chat\"\n name (str): The name of the model. This name will be used both to load model configuration from the HuggingFace Hub and in the OpenAI-compatible endpoint.\n format (str): The format of the model, e.g. \"tgi\" if the model is served with HuggingFace's Text Generation Inference.\n chat_template (Optional[str]): The custom prompt template for the model. If not specified, the default prompt template from the HuggingFace Hub configuration will be used.\n eos_token (Optional[str]): The custom end of sentence token. If not specified, the default end of sentence token from the HuggingFace Hub configuration will be used."}, "TaskConfigurationRequest": {"properties": {"nodes": {"type": "integer", "minimum": 1.0, "title": "Nodes", "description": "Number of nodes", "default": 1}, "ports": {"items": {"anyOf": [{"type": "integer", "maximum": 65536.0, "exclusiveMinimum": 0.0}, {"type": "string", "pattern": "^(?:[0-9]+|\\*):[0-9]+$"}, {"$ref": "#/components/schemas/PortMappingRequest"}]}, "type": "array", "title": "Ports", "description": "Port numbers/mapping to expose", "default": []}, "commands": {"items": {"type": "string"}, "type": "array", "title": "Commands", "description": "The shell commands to run", "default": []}, "type": {"type": "string", "enum": ["task"], "title": "Type", "default": "task"}, "name": {"type": "string", "title": "Name", "description": "The run name. If not specified, a random name is generated"}, "image": {"type": "string", "title": "Image", "description": "The name of the Docker image to run"}, "user": {"type": "string", "title": "User", "description": "The user inside the container, `user_name_or_id[:group_name_or_id]` (e.g., `ubuntu`, `1000:1000`). Defaults to the default user from the `image`"}, "privileged": {"type": "boolean", "title": "Privileged", "description": "Run the container in privileged mode", "default": false}, "entrypoint": {"type": "string", "title": "Entrypoint", "description": "The Docker entrypoint"}, "working_dir": {"type": "string", "title": "Working Dir", "description": "The absolute path to the working directory inside the container. Defaults to the `image`'s default working directory"}, "home_dir": {"type": "string", "title": "Home Dir", "default": "/root"}, "registry_auth": {"allOf": [{"$ref": "#/components/schemas/RegistryAuthRequest"}], "title": "Registry Auth", "description": "Credentials for pulling a private Docker image"}, "python": {"allOf": [{"$ref": "#/components/schemas/PythonVersion"}], "description": "The major version of Python. Mutually exclusive with `image` and `docker`"}, "nvcc": {"type": "boolean", "title": "Nvcc", "description": "Use image with NVIDIA CUDA Compiler (NVCC) included. Mutually exclusive with `image` and `docker`"}, "single_branch": {"type": "boolean", "title": "Single Branch", "description": "Whether to clone and track only the current branch or all remote branches. Relevant only when using remote Git repos. Defaults to `false` for dev environments and to `true` for tasks and services"}, "env": {"allOf": [{"$ref": "#/components/schemas/Env"}], "title": "Env", "description": "The mapping or the list of environment variables", "default": {"__root__": {}}}, "shell": {"type": "string", "title": "Shell", "description": "The shell used to run commands. Allowed values are `sh`, `bash`, or an absolute path, e.g., `/usr/bin/zsh`. Defaults to `/bin/sh` if the `image` is specified, `/bin/bash` otherwise"}, "resources": {"allOf": [{"$ref": "#/components/schemas/ResourcesSpecRequest"}], "title": "Resources", "description": "The resources requirements to run the configuration", "default": {"cpu": {"min": 2}, "memory": {"min": 8.0}, "gpu": {"count": {"min": 0}}, "disk": {"size": {"min": 100.0}}}}, "priority": {"type": "integer", "maximum": 100.0, "minimum": 0.0, "title": "Priority", "description": "The priority of the run, an integer between `0` and `100`. `dstack` tries to provision runs with higher priority first. Defaults to `0`"}, "volumes": {"items": {"anyOf": [{"$ref": "#/components/schemas/VolumeMountPointRequest"}, {"$ref": "#/components/schemas/InstanceMountPointRequest"}, {"type": "string"}]}, "type": "array", "title": "Volumes", "description": "The volumes mount points", "default": []}, "docker": {"type": "boolean", "title": "Docker", "description": "Use Docker inside the container. Mutually exclusive with `image`, `python`, and `nvcc`. Overrides `privileged`"}, "repos": {"items": {"$ref": "#/components/schemas/RepoSpecRequest"}, "type": "array", "title": "Repos", "description": "The list of Git repos", "default": []}, "files": {"items": {"anyOf": [{"$ref": "#/components/schemas/FilePathMappingRequest"}, {"type": "string"}]}, "type": "array", "title": "Files", "description": "The local to container file path mappings", "default": []}, "setup": {"items": {"type": "string"}, "type": "array", "title": "Setup", "default": []}, "backends": {"items": {"$ref": "#/components/schemas/BackendType"}, "type": "array", "description": "The backends to consider for provisioning (e.g., `[aws, gcp]`)"}, "regions": {"items": {"type": "string"}, "type": "array", "title": "Regions", "description": "The regions to consider for provisioning (e.g., `[eu-west-1, us-west4, westeurope]`)"}, "availability_zones": {"items": {"type": "string"}, "type": "array", "title": "Availability Zones", "description": "The availability zones to consider for provisioning (e.g., `[eu-west-1a, us-west4-a]`)"}, "instance_types": {"items": {"type": "string"}, "type": "array", "title": "Instance Types", "description": "The cloud-specific instance types to consider for provisioning (e.g., `[g6e.24xlarge, n1-standard-4]`)"}, "reservation": {"type": "string", "title": "Reservation", "description": "The existing reservation to use for instance provisioning. Supports AWS Capacity Reservations, AWS Capacity Blocks, and GCP reservations"}, "spot_policy": {"allOf": [{"$ref": "#/components/schemas/SpotPolicy"}], "description": "The policy for provisioning spot or on-demand instances: `spot`, `on-demand`, `auto`. Defaults to `on-demand`"}, "retry": {"anyOf": [{"$ref": "#/components/schemas/ProfileRetryRequest"}, {"type": "boolean"}], "title": "Retry", "description": "The policy for resubmitting the run. Defaults to `false`"}, "max_duration": {"anyOf": [{"type": "string", "enum": ["off"]}, {"type": "integer"}, {"type": "boolean"}, {"type": "string"}], "title": "Max Duration", "description": "The maximum duration of a run (e.g., `2h`, `1d`, etc) in a running state, excluding provisioning and pulling. After it elapses, the run is automatically stopped. Use `off` for unlimited duration. Defaults to `off`"}, "stop_duration": {"anyOf": [{"type": "string", "enum": ["off"]}, {"type": "integer"}, {"type": "boolean"}, {"type": "string"}], "title": "Stop Duration", "description": "The maximum duration of a run graceful stopping. After it elapses, the run is automatically forced stopped. This includes force detaching volumes used by the run. Use `off` for unlimited duration. Defaults to `5m`"}, "max_price": {"type": "number", "exclusiveMinimum": 0.0, "title": "Max Price", "description": "The maximum instance price per hour, in dollars"}, "creation_policy": {"allOf": [{"$ref": "#/components/schemas/CreationPolicy"}], "description": "The policy for using instances from fleets: `reuse`, `reuse-or-create`. Defaults to `reuse-or-create`"}, "idle_duration": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Idle Duration", "description": "Time to wait before terminating idle instances. When the run reuses an existing fleet instance, the fleet's `idle_duration` applies. When the run provisions a new instance, the shorter of the fleet's and run's values is used. Defaults to `5m` for runs and `3d` for fleets. Use `off` for unlimited duration. Only applied for VM-based backends"}, "utilization_policy": {"allOf": [{"$ref": "#/components/schemas/UtilizationPolicyRequest"}], "title": "Utilization Policy", "description": "Run termination policy based on utilization"}, "startup_order": {"allOf": [{"$ref": "#/components/schemas/StartupOrder"}], "description": "The order in which master and workers jobs are started: `any`, `master-first`, `workers-first`. Defaults to `any`"}, "stop_criteria": {"allOf": [{"$ref": "#/components/schemas/StopCriteria"}], "description": "The criteria determining when a multi-node run should be considered finished: `all-done`, `master-done`. Defaults to `all-done`"}, "schedule": {"allOf": [{"$ref": "#/components/schemas/ScheduleRequest"}], "title": "Schedule", "description": "The schedule for starting the run at specified time"}, "fleets": {"items": {"anyOf": [{"$ref": "#/components/schemas/EntityReferenceRequest"}, {"type": "string"}]}, "type": "array", "title": "Fleets", "description": "The fleets considered for reuse. For fleets owned by the current project, specify fleet names. For imported fleets, specify `/`"}, "instances": {"items": {"anyOf": [{"$ref": "#/components/schemas/InstanceNameSelectorRequest"}, {"$ref": "#/components/schemas/InstanceHostnameSelectorRequest"}, {"$ref": "#/components/schemas/FleetInstanceSelectorRequest"}, {"type": "string", "minLength": 1}]}, "type": "array", "minItems": 1, "title": "Instances", "description": "The specific fleet instances to consider for reuse. Each value can be an instance name string, or an object with `name`, `hostname`, or `fleet` and `instance`. When set, the run is only placed on matching existing instances."}, "tags": {"additionalProperties": {"type": "string"}, "type": "object", "title": "Tags", "description": "The custom tags to associate with the resource. The tags are also propagated to the underlying backend resources. If there is a conflict with backend-level tags, does not override them"}, "backend_options": {"items": {"$ref": "#/components/schemas/VastAIProfileOptions"}, "type": "array", "title": "Backend Options", "description": "Backend-specific options, applied only to offers from that backend"}}, "additionalProperties": false, "type": "object", "title": "TaskConfigurationRequest"}, "UtilizationPolicyRequest": {"properties": {"min_gpu_utilization": {"type": "integer", "maximum": 100.0, "minimum": 0.0, "title": "Min Gpu Utilization", "description": "Minimum required GPU utilization, percent. If any GPU has utilization below specified value during the whole time window, the run is terminated"}, "time_window": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Time Window", "description": "The time window of metric samples taking into account to measure utilization (e.g., `30m`, `1h`). Minimum is `5m`"}}, "additionalProperties": false, "type": "object", "required": ["min_gpu_utilization", "time_window"], "title": "UtilizationPolicyRequest"}, "ValidationError": {"properties": {"loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "type": "array", "title": "Location"}, "msg": {"type": "string", "title": "Message"}, "type": {"type": "string", "title": "Error Type"}}, "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError"}, "VastAIOfferOrder": {"type": "string", "enum": ["score", "price"], "title": "VastAIOfferOrder", "description": "An enumeration."}, "VastAIProfileOptions": {"properties": {"type": {"type": "string", "enum": ["vastai"], "title": "Type", "default": "vastai"}, "offer_order": {"allOf": [{"$ref": "#/components/schemas/VastAIOfferOrder"}], "description": "Controls the order in which offers are considered for provisioning. Use `score` to prioritize the highest overall score first (the default order in the Vast.ai console), or `price` to prioritize the lowest-cost offers first. Lower-cost offers are often less reliable, so consider applying stricter filters when using `price`. Defaults to `score`"}, "min_reliability": {"type": "number", "maximum": 1.0, "minimum": 0.0, "title": "Min Reliability", "description": "The minimum reliability threshold for offers, on a scale from `0` to `1`. Defaults to `0.9`"}, "min_score": {"type": "integer", "minimum": 0.0, "title": "Min Score", "description": "The minimum overall score required for offers to be considered. The scoring scale varies and may require experimentation. Starting with a value in the low hundreds is generally recommended"}}, "additionalProperties": false, "type": "object", "title": "VastAIProfileOptions"}, "VirtualRunRepoDataRequest": {"properties": {"repo_type": {"type": "string", "enum": ["virtual"], "title": "Repo Type", "default": "virtual"}}, "additionalProperties": false, "type": "object", "title": "VirtualRunRepoDataRequest"}, "VolumeMountPointRequest": {"properties": {"name": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "title": "Name", "description": "The network volume name or the list of network volume names to mount. If a list is specified, one of the volumes in the list will be mounted. Specify volumes from different backends/regions to increase availability"}, "path": {"type": "string", "title": "Path", "description": "The absolute container path to mount the volume at"}}, "additionalProperties": false, "type": "object", "required": ["name", "path"], "title": "VolumeMountPointRequest"}, "VolumeSpecRequest": {"properties": {"configuration": {"oneOf": [{"$ref": "#/components/schemas/AWSVolumeConfigurationRequest"}, {"$ref": "#/components/schemas/GCPVolumeConfigurationRequest"}, {"$ref": "#/components/schemas/RunpodVolumeConfigurationRequest"}, {"$ref": "#/components/schemas/KubernetesVolumeConfigurationRequest"}], "title": "Configuration", "discriminator": {"propertyName": "backend", "mapping": {"aws": "#/components/schemas/AWSVolumeConfigurationRequest", "gcp": "#/components/schemas/GCPVolumeConfigurationRequest", "runpod": "#/components/schemas/RunpodVolumeConfigurationRequest", "kubernetes": "#/components/schemas/KubernetesVolumeConfigurationRequest"}}}, "configuration_path": {"type": "string", "title": "Configuration Path"}}, "additionalProperties": false, "type": "object", "required": ["configuration"], "title": "VolumeSpecRequest"}}}}
diff --git a/website/package-lock.json b/website/package-lock.json
index 4bbcd544b..feba06fc1 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -1,16 +1,17 @@
{
- "name": "cloudscape-reverse-engineering",
+ "name": "dstack-website",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "cloudscape-reverse-engineering",
+ "name": "dstack-website",
"version": "0.1.0",
"dependencies": {
"@cloudscape-design/code-view": "^3.0.142",
"@cloudscape-design/components": "^3.0.0",
"@cloudscape-design/global-styles": "^1.0.0",
+ "@dstackai/sqircle": "^0.1.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.30.4"
@@ -483,6 +484,16 @@
"react": ">=16.8.0"
}
},
+ "node_modules/@dstackai/sqircle": {
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/@dstackai/sqircle/-/sqircle-0.1.9.tgz",
+ "integrity": "sha512-/NrPXUILifHN8u8uGTZNVX6EK4BVut1fmhdHwXvOsvjJ9fU17ib6NkjAzp2PQSmCGUtnhin9crTxqV9iuZG/sg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
diff --git a/website/package.json b/website/package.json
index c0d772303..09cff3cbb 100644
--- a/website/package.json
+++ b/website/package.json
@@ -12,6 +12,7 @@
"@cloudscape-design/code-view": "^3.0.142",
"@cloudscape-design/components": "^3.0.0",
"@cloudscape-design/global-styles": "^1.0.0",
+ "@dstackai/sqircle": "^0.1.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.30.4"
diff --git a/website/public/static/dstack-gpu-artwork-dark.svg b/website/public/static/dstack-gpu-artwork-dark.svg
deleted file mode 100644
index eb5b68eeb..000000000
--- a/website/public/static/dstack-gpu-artwork-dark.svg
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/website/public/static/dstack-gpu-artwork.svg b/website/public/static/dstack-gpu-artwork.svg
deleted file mode 100644
index 1c06e970a..000000000
--- a/website/public/static/dstack-gpu-artwork.svg
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/website/src/cloudscape-overrides.css b/website/src/cloudscape-overrides.css
index fcc988664..42230d504 100644
--- a/website/src/cloudscape-overrides.css
+++ b/website/src/cloudscape-overrides.css
@@ -14,7 +14,6 @@
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
@@ -34,40 +33,18 @@
/* 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-divider-f5t9va: color-mix(in srgb, var(--cs-text) 14%, transparent);
--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;
+ a) Drop the vertical separators Cloudscape draws between tabs. Selection is shown by the
+ active tab's pill background (section 9), so the dividers just add noise — the tabs read
+ as a row of pills separated by the cells' own padding (matching the landing's tab design). */
+[class*='awsui_tabs-tab_'] > [class*='awsui_tabs-tab-header-container']::before {
+ display: none !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). */
@@ -79,87 +56,12 @@ body {
[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. */
+/* c) The scroll-arrow buttons carry 12px block margins, so they are taller than the tab row and
+ stretch the whole strip (and its tabs) ~10px taller than a chevron-free strip. Zero the block
+ margin so the arrows no longer inflate the tab height — the strip matches the others. */
+[class*='awsui_pagination-button'] {
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
@@ -199,62 +101,49 @@ body {
/* Primary (filled) buttons use 500 weight per design — outline/normal buttons keep their weight.
Applies app-wide (new landing + /old). */
-[class*='awsui_variant-primary'],
-.site-get-started [class*='awsui_trigger-button'] {
+[class*='awsui_variant-primary'] {
font-weight: 500 !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). */
+/* 9) Content tabs — "pill" selection, matching the landing's own tab design: the active tab is a
+ panel-grey rounded-top pill (a hovered tab gets the faint --cs-hover tint), inactive labels
+ are muted, and there are no separators (section 4a). The pill is painted on the inner link
+ so each cell's inline padding becomes the gap between pills; a little top padding on the
+ strip lets the pills float above the softened (section 2) under-tab divider. */
+/* a) Drop the per-tab active underline indicator — selection is shown by the pill background. */
[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;
+/* b) Float the pills: top padding above the strip, and tighter cell padding so neighbouring
+ pills sit close (the cell's inline padding is the inter-pill gap). */
+[class*='awsui_tabs-header-list'] {
+ padding: 6px 4px 0 !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;
+[class*='awsui_tabs-tab-header-container'] {
+ padding-inline: 3px !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;
+/* c) The pill: muted label at rest, body color + faint tint on hover, panel-grey when selected.
+ Rounded top only — the flat bottom sits on the under-tab divider. aria-selected lives on
+ the inner link. */
+[class*='awsui_tabs-tab-link'] {
+ color: var(--cs-muted) !important;
+ padding-block: 13px !important; /* with the label padding zeroed (d), this matches .gs-tab's 15px/14px content-tab height */
+ padding-inline: 14px !important;
+ border-radius: 8px 8px 0 0 !important;
}
-[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 */
+[class*='awsui_tabs-tab-link']:hover {
+ color: var(--cs-text) !important;
+ background: var(--cs-hover) !important;
}
-.gpu-scroll tr > :first-child {
- padding-inline-start: 20px !important;
+[class*='awsui_tabs-tab-link'][aria-selected='true'] {
+ color: var(--cs-text) !important;
+ background: var(--cs-panel) !important;
}
-.gpu-scroll tr > :last-child {
- padding-inline-end: 20px !important;
+/* d) The label span carries its own inner padding (4px 8px) on top of the link's padding, which
+ makes the Cloudscape pill ~12px taller than the landing's own tabs. Zero it so the pill height
+ is just link-padding + text — matching .gs-tab / .gs-skytab exactly. */
+[class*='awsui_tabs-tab-label'] {
+ padding: 0 !important;
}
/* 11) Thin (0.5px) dividers between stacked FAQ items. The block's outer corners
diff --git a/website/src/cloudscape-theme.ts b/website/src/cloudscape-theme.ts
index 18d98325f..f5da772ff 100644
--- a/website/src/cloudscape-theme.ts
+++ b/website/src/cloudscape-theme.ts
@@ -112,3 +112,8 @@ export const mainButtonStyle: ButtonProps.Style = {
export const menuButtonStyle: ButtonProps.Style = {
root: { paddingBlock: '7px', paddingInline: '18px' },
};
+// Header "Get started" — same as menuButtonStyle but with 1px extra on the right so the label
+// doesn't sit a hair close to the edge (inline-end 19px vs inline-start 18px).
+export const getStartedButtonStyle: ButtonProps.Style = {
+ root: { paddingBlock: '7px', paddingInline: '18px 19px' },
+};
diff --git a/website/src/components/HeroSquircle.tsx b/website/src/components/HeroSquircle.tsx
new file mode 100644
index 000000000..f6c5e9939
--- /dev/null
+++ b/website/src/components/HeroSquircle.tsx
@@ -0,0 +1,100 @@
+import {
+ SquircleScene,
+ type SquircleGeometryConfig,
+ type SquircleLayerConfig,
+ type SquircleLayerHoverContext,
+} from '@dstackai/sqircle';
+import '@dstackai/sqircle/style.css';
+import type { ThemeMode } from '../theme';
+
+// Live hero object, composed in the squircle constructor (@dstackai/sqircle) and pasted here.
+// Layers, back → front: wireframe slab (dashed inlay) → wireframe slab → solid
+// "GPU" face (dotted inlay). Layers cross-react on hover, and clicking any layer scrolls to
+// Get-started. `theme` stays driven by the app toggle (the snippet hardcodes "light").
+const HERO_GEOMETRY: SquircleGeometryConfig = {
+ angleDegrees: 20,
+};
+
+const HERO_LAYERS: SquircleLayerConfig[] = [
+ {
+ id: 'layer-1',
+ visible: true,
+ offset: { x: 0, y: 176 },
+ base: {
+ material: 'wireframe',
+ paletteId: '15',
+ line: 'dashed',
+ lineColor: 'auto',
+ effect: 'off',
+ },
+ stroke: { wire: 1.6, line: 2.2, face: 0, wireLine: 2.2 },
+ // Hovering this bottom slab itself turns it into a solid metal "dstack" face (no inlay line).
+ hover: (ctx: SquircleLayerHoverContext) => {
+ if (ctx.hoveredLayerId === 'layer-1')
+ return { material: 'solid', paletteId: '20', effect: 'metal', text: 'dstack', line: false }
+ return false
+ }
+ },
+ {
+ id: 'layer-2',
+ visible: true,
+ offset: { x: 0, y: 88 },
+ base: {
+ material: 'transparent',
+ paletteId: '20',
+ // line: false,
+ },
+ stroke: { face: 0 },
+ // Cross-layer hover (0.1.4 resolver API): hovering the top (layer-3) or bottom (layer-1)
+ // slab keeps this middle a wireframe; hovering the middle itself turns it transparent.
+ hover: (ctx: SquircleLayerHoverContext) => {
+ if (ctx.hoveredLayerId === 'layer-2') return { material: 'wireframe' };
+ if (ctx.hoveredLayerId === 'layer-1')
+ return { material: 'wireframe' };
+ return false;
+ },
+ },
+ {
+ id: 'layer-3',
+ visible: true,
+ offset: { x: 0, y: 0 },
+ base: {
+ material: 'solid',
+ paletteId: '20',
+ effect: 'metal',
+ text: 'GPU',
+ textColor: 'auto',
+ textStyle: 'solid',
+ // line: 'dotted',
+ lineColor: 'auto',
+ grain: true,
+ },
+ // Top "GPU" face hover behavior:
+ // - hovering itself (layer-3): becomes solid palette 20 with the metal effect;
+ // - hovering the bottom (layer-1): wireframe, "GPU" text outlined, inlay line removed;
+ // - hovering the middle (layer-2): wireframe, "GPU" text outlined (line kept).
+ hover: (ctx: SquircleLayerHoverContext) => {
+ if (ctx.hoveredLayerId === 'layer-1') return { material: 'wireframe', line: 'dotted', palletteId: 15, textStyle: 'wireframe' };
+ // if (ctx.hoveredLayerId === 'layer-2') return { material: 'wireframe', line: 'dotted', palletteId: 15 };
+ if (ctx.hoveredLayerId === 'layer-3') return { material: 'wireframe', line: 'dotted', palletteId: 15 };
+ return false;
+ },
+ },
+];
+
+export function HeroSquircle({ theme }: { theme: ThemeMode }) {
+ return (
+
+
+ document.getElementById('resources')?.scrollIntoView({ behavior: 'smooth' })
+ }
+ />
+
+ );
+}
diff --git a/website/src/components/SiteFooter.tsx b/website/src/components/SiteFooter.tsx
index c8625fe08..dbb8c83a3 100644
--- a/website/src/components/SiteFooter.tsx
+++ b/website/src/components/SiteFooter.tsx
@@ -66,7 +66,7 @@ const footerColumns: FooterColumn[] = [
heading: 'Company',
links: [
{ label: 'Blog', href: BLOG_URL },
- { label: 'Talk to us', href: 'https://calendly.com/dstackai/discovery-call', external: true },
+ { label: 'Get a demo', href: 'https://calendly.com/dstackai/discovery-call', external: true },
{ label: 'Terms of service', href: TERMS_URL },
{ label: 'Privacy policy', href: PRIVACY_URL },
],
diff --git a/website/src/components/SiteNavigation.tsx b/website/src/components/SiteNavigation.tsx
index 28b99d46d..4eac7b1bc 100644
--- a/website/src/components/SiteNavigation.tsx
+++ b/website/src/components/SiteNavigation.tsx
@@ -1,66 +1,190 @@
-import { useState } from 'react';
+import { ReactNode, useEffect, 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 Icon from '@cloudscape-design/components/icon';
import SideNavigation, { SideNavigationProps } from '@cloudscape-design/components/side-navigation';
import SpaceBetween from '@cloudscape-design/components/space-between';
-import { menuButtonStyle } from '../cloudscape-theme';
+import { getStartedButtonStyle, menuButtonStyle } from '../cloudscape-theme';
import { ThemeToggle } from './ThemeToggle';
import { asset } from '../asset';
-import { BLOG_URL, DOCS_URL, ROUTES, docsUrl } from '../routes';
+import { BLOG_URL, DOCS_URL, ROUTES } from '../routes';
import { ThemeMode } from '../theme';
const dstackGithubUrl = 'https://github.com/dstackai/dstack';
+const dstackGithubApiUrl = 'https://api.github.com/repos/dstackai/dstack';
const externalIconAriaLabel = 'External link icon';
+// Compact star count: 1340 → "1.3k", 12000 → "12k", 980 → "980".
+function formatStars(count: number): string {
+ if (count < 1000) return String(count);
+ const thousands = count / 1000;
+ return `${thousands >= 10 ? Math.round(thousands) : Number(thousands.toFixed(1))}k`;
+}
+
+// Monochrome product glyphs for the "Products" menu (GitHub mark also doubles as the star badge).
+const GithubGlyph = () => (
+
+
+
+);
+const CloudUploadGlyph = () => (
+
+
+
+
+
+);
+const FingerprintGlyph = () => (
+
+
+
+
+
+
+
+
+);
+
// Primary links in the desktop top navigation (plain same-origin MkDocs links). The blog
-// categories are listed individually (Case studies / Benchmarks / Blog) to mirror the docs
+// categories are listed individually (Case studies / Blog) to mirror the docs
// header tabs — no "Resources" dropdown.
const audienceNavItems: Array<{ label: string; href: string }> = [
{ label: 'Docs', href: DOCS_URL },
{ label: 'Case studies', href: `${BLOG_URL}/case-studies/` },
- { label: 'Benchmarks', href: `${BLOG_URL}/benchmarks/` },
{ label: 'Blog', href: BLOG_URL },
];
-// "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 },
- ],
- },
+type ProductLink = {
+ id: string;
+ text: string;
+ secondaryText: string;
+ href: string;
+ external?: boolean;
+ icon: ReactNode;
+};
+
+// The products. products[0] (open-source) is featured at the top of the "Products" menu; the rest
+// follow as rows. Reused by the standalone top-nav hover menu and the mobile nav's "Products"
+// section.
+const products: ProductLink[] = [
+ { id: 'open-source', text: 'dstack', secondaryText: 'The open-source control plane that works across clouds, Kubernetes, and on-prem.', href: DOCS_URL, icon: },
+ { id: 'sky-product', text: 'dstack Sky', secondaryText: 'Access GPU marketplace, or bring your own clouds. Hosted and managed by us.', href: 'https://sky.dstack.ai', external: true, icon: },
+ { id: 'enterprise', text: 'Enterprise', secondaryText: 'Self-hosted with SSO, air-gapped setup, dedicated support, and more.', href: 'https://calendly.com/dstackai/discovery-call', external: true, icon: },
];
// Items for the mobile slide-out navigation. The blog categories are top-level links (mirroring
// the flattened desktop nav), not a "Resources" section.
const mobileNavigationItems: SideNavigationProps.Item[] = [
- { type: 'link', text: 'Docs', href: DOCS_URL },
- { 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',
+ text: 'Products',
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 },
- ],
+ items: products.map((p): SideNavigationProps.Item => ({
+ type: 'link',
+ text: p.text,
+ href: p.href,
+ ...(p.external ? { external: true, externalIconAriaLabel } : {}),
+ })),
},
+ { type: 'link', text: 'Docs', href: DOCS_URL },
+ { type: 'link', text: 'Case studies', href: `${BLOG_URL}/case-studies/` },
+ { type: 'link', text: 'Blog', href: BLOG_URL },
+ { type: 'link', text: 'GitHub', href: dstackGithubUrl, external: true, externalIconAriaLabel },
];
+// Standalone "Products" top-nav menu. The popup opens on hover (and on keyboard focus) and
+// closes once the pointer/focus leaves both the trigger and the popup. The trigger reads like
+// the plain text links beside it — no dropdown caret. A short close delay (hover-intent, below)
+// lets the pointer cross the gap from trigger to popup without the menu dropping.
+function ProductsHoverMenu() {
+ const [open, setOpen] = useState(false);
+ // Hover-intent: leaving the trigger schedules a close, but re-entering the wrapper (e.g. moving
+ // onto the popup, which is a child) cancels it — so the small gap between trigger and popup
+ // doesn't drop the menu.
+ const closeTimer = useRef(undefined);
+ const openMenu = () => {
+ window.clearTimeout(closeTimer.current);
+ setOpen(true);
+ };
+ const scheduleClose = () => {
+ window.clearTimeout(closeTimer.current);
+ closeTimer.current = window.setTimeout(() => setOpen(false), 150);
+ };
+
+ // GitHub star count for the open-source repo, fetched once the menu first opens. Best-effort:
+ // if the API is rate-limited or errors, the badge simply doesn't render.
+ const [stars, setStars] = useState(null);
+ const starsFetched = useRef(false);
+ useEffect(() => {
+ if (!open || starsFetched.current) return;
+ starsFetched.current = true;
+ fetch(dstackGithubApiUrl)
+ .then(response => (response.ok ? response.json() : null))
+ .then(data => {
+ if (data && typeof data.stargazers_count === 'number') setStars(data.stargazers_count);
+ })
+ .catch(() => {});
+ }, [open]);
+
+ return (
+ {
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
+ setOpen(false);
+ }
+ }}
+ >
+
+ Products
+
+
+
+
+ {open && (
+
+ )}
+
+ );
+}
+
// 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({
@@ -86,20 +210,17 @@ export function SiteNavigation({
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;
+ // "Get started" scrolls to the Get-started section on the home page (id="resources"). From any
+ // other route, go home first, then scroll once it has rendered.
+ const goToGetStarted = (event: { preventDefault: () => void }) => {
+ event.preventDefault();
+ setMobileNavigationOpen(false);
+ if (pathname === ROUTES.HOME) {
+ document.getElementById('resources')?.scrollIntoView({ behavior: 'smooth' });
+ } else {
+ navigate(ROUTES.HOME);
+ window.setTimeout(() => document.getElementById('resources')?.scrollIntoView({ behavior: 'smooth' }), 120);
}
-
- go(ROUTES.HOME);
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => {
- document.getElementById('resources')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
- });
- });
};
return (
@@ -131,6 +252,8 @@ export function SiteNavigation({
+ {/* Standalone "Products" hover menu — a flat list of the three products. Sits before "Docs". */}
+
{audienceNavItems.map(item => (
{item.label}
@@ -148,13 +271,14 @@ export function SiteNavigation({
>
GitHub
-
+ href="#resources"
+ onClick={goToGetStarted}
+ style={getStartedButtonStyle}
+ >
+ Get started
+
diff --git a/website/src/data/gpus.ts b/website/src/data/gpus.ts
new file mode 100644
index 000000000..152625a9c
--- /dev/null
+++ b/website/src/data/gpus.ts
@@ -0,0 +1,14 @@
+// Rough per-GPU/hour price ranges across backends, in the spirit of `dstack offer --group-by gpu`.
+// Single source of truth, shared by the "Access marketplace GPUs" block (ExploreSection) and the
+// dstack Sky "GPU marketplace" pane (GetStartedSection). Prices use a compact en-dash range; a
+// "/hr" suffix is appended at render time.
+export 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' },
+];
diff --git a/website/src/data/images.ts b/website/src/data/images.ts
index c525c5b88..0201d8251 100644
--- a/website/src/data/images.ts
+++ b/website/src/data/images.ts
@@ -1,18 +1,10 @@
-// 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
+// Image assets used across the site. The home hero is now a live
component and
+// the "Vendor-agnostic, open-source" architecture diagram is an HTML/CSS component (see
+// components/ArchitectureDiagram.tsx) — neither is an image. The Old-page placeholders below 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'),
diff --git a/website/src/pages/Home/ExploreSection.tsx b/website/src/pages/Home/ExploreSection.tsx
index ed37bde82..d4de38682 100644
--- a/website/src/pages/Home/ExploreSection.tsx
+++ b/website/src/pages/Home/ExploreSection.tsx
@@ -1,16 +1,14 @@
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 { gpuOffers } from '../../data/gpus';
import { docsUrl } from '../../routes';
import {
backendConfigs,
@@ -22,22 +20,10 @@ import {
// 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' },
+ { name: 'Fleets', label: 'Cloud & on-prem', href: docsUrl('concepts/fleets'), description: 'Provision and manage clusters across clouds, Kubernetes, and on-prem.' },
+ { name: 'Dev environments', label: 'Development', href: docsUrl('concepts/dev-environments'), description: 'Launch dev environments to be accessed by agents or from your IDE.' },
+ { name: 'Tasks', label: 'Training and batch', href: docsUrl('concepts/tasks'), description: 'Run training and batch jobs across a single node or clusters.' },
+ { name: 'Services', label: 'Model inference', href: docsUrl('concepts/services'), description: 'Deploy model inference as secure and scalable endpoints.' },
];
// Read-only YAML snippet. Line wrapping is left off so one line maps to one row,
@@ -50,23 +36,20 @@ function YamlCode({ content }: { content: string }) {
);
}
-// 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.
+// GPU price list — a plain monospace name/price list in a bordered card, matching the dstack Sky
+// "GPU marketplace" pane in the Get started section (same .gs-mkt__row treatment, single source).
function GpuMarketplaceTable() {
return (
-
-
-
<>{offer.name} ({offer.memory})>, isRowHeader: true },
- { id: 'price', header: '$/hour', cell: offer => offer.price },
- ]}
- items={gpuOffers}
- />
-
-
+
+
+ {gpuOffers.map(offer => (
+
+ {offer.name} {' '}{offer.memory}
+ {offer.price}/hr
+
+ ))}
+
+
);
}
@@ -97,7 +80,6 @@ export function ExploreSection() {
}
title="Bring your own clouds"
imageFirst
- action={Backends }
>
dstack natively integrates with the major GPU clouds and automates provisioning of clusters.
@@ -119,12 +101,6 @@ export function ExploreSection() {
/>
}
title="Bring on-prem clusters"
- action={
-
- Kubernetes
- SSH fleets
-
- }
>
Have an existing Kubernetes cluster? Point dstack to the kubeconfig, and dstack
will schedule workloads on it as it was a cloud cluster.
@@ -150,8 +126,11 @@ function KeyConceptsBlock() {
// onClick-only ActionCard component.
-
- {concept.name}
+ {concept.label}
+
+ {concept.name}
+
+
{highlightTerms(concept.description)}
))}
@@ -173,7 +152,7 @@ function GpuMarketplaceBlock() {
visual={ }
title="Access marketplace GPUs"
imageFirst
- action={Sign up }
+ action={Try dstack Sky }
>
Don't have your own cloud accounts or on-prem clusters? No problem. You can access compute
through dstack Sky, our hosted GPU marketplace.
diff --git a/website/src/pages/Home/FaqSection.tsx b/website/src/pages/Home/FaqSection.tsx
index c8b094952..4a9e81d43 100644
--- a/website/src/pages/Home/FaqSection.tsx
+++ b/website/src/pages/Home/FaqSection.tsx
@@ -1,6 +1,7 @@
import { useState } from 'react';
import Button from '@cloudscape-design/components/button';
import ExpandableSection from '@cloudscape-design/components/expandable-section';
+import SpaceBetween from '@cloudscape-design/components/space-between';
import { mainButtonStyle } from '../../cloudscape-theme';
import { AlternatingDocBlock } from '../../components/AlternatingDocBlock';
import { highlightTerms } from '../../components/highlightTerms';
@@ -48,9 +49,14 @@ export function FaqSection() {
}
title="FAQ"
action={
-
- Discord
-
+
+
+ Discord
+
+
+ Contact us
+
+
}
>
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
index 92eafeb23..d388cf362 100644
--- a/website/src/pages/Home/GetStartedSection.tsx
+++ b/website/src/pages/Home/GetStartedSection.tsx
@@ -1,13 +1,79 @@
+import { KeyboardEvent, useEffect, useState } from 'react';
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 { gpuOffers } from '../../data/gpus';
import { installMethods, maxInstallLines, padYamlToLines } from '../../data/snippets';
import { docsUrl } from '../../routes';
+const GITHUB_API_URL = 'https://api.github.com/repos/dstackai/dstack';
+
+// Compact star count: 1340 → "1.3k", 12000 → "12k", 980 → "980" (mirrors the Products menu).
+function formatStars(count: number): string {
+ if (count < 1000) return String(count);
+ const thousands = count / 1000;
+ return `${thousands >= 10 ? Math.round(thousands) : Number(thousands.toFixed(1))}k`;
+}
+
+// Product glyphs. GitHub mark doubles as the open-source star badge; cloud / fingerprint mark the
+// hosted / self-hosted rows (thin-line, matching the Products menu).
+const GithubGlyph = () => (
+
+
+
+);
+const CloudUploadGlyph = () => (
+
+
+
+
+
+);
+const FingerprintGlyph = () => (
+
+
+
+
+
+
+
+
+);
+const CheckGlyph = () => (
+
+
+
+);
+// Enterprise capability glyphs (thin-line, matching the Products menu). Distinct from the
+// fingerprint product mark in the switcher — one icon per capability.
+const KeyGlyph = () => (
+
+
+
+
+);
+const ShieldGlyph = () => (
+
+
+
+
+);
+const SupportGlyph = () => (
+
+
+
+
+
+);
+const AuditGlyph = () => (
+
+
+
+
+
+);
+
// Read-only shell snippet. Line wrapping is left off so padded snippets stay
// equal height across tabs (see padYamlToLines).
function ShellCode({ content }: { content: string }) {
@@ -18,69 +84,200 @@ function ShellCode({ content }: { content: string }) {
);
}
-// Closing "Get started" section: the open-source install path, then the hosted/enterprise
-// options under "Looking for more?".
+type DeployTab = 'oss' | 'sky' | 'ent';
+
+// dstack Sky: a sample of the GPU marketplace (price ranges, no provider/region — the point is
+// "on-demand GPUs at a price"; same offers as the "Access marketplace GPUs" block) and the clouds
+// you can bring instead. Both lists scroll.
+const SKY_CLOUDS = ['AWS', 'GCP', 'Azure', 'Kubernetes', 'Lambda', 'RunPod', 'Nebius', 'OCI', 'Vast.ai', 'CloudRift', 'Crusoe', 'SSH fleets'];
+
+// Enterprise "Extra" capabilities (beyond open-source). One tab for now; more can be added later.
+const ENT_CAPS = [
+ { icon: , title: 'Single Sign-On (SSO)', sub: 'Okta, Microsoft Entra, Google Workspace' },
+ { icon: , title: 'Air-gapped deployment', sub: 'Run fully offline, in your own VPC' },
+ { icon: , title: 'Dedicated support', sub: 'Bug fixes, feature prioritization, SLAs' },
+ { icon: , title: 'Audit logs & RBAC', sub: 'Fine-grained roles and audit trails' },
+];
+
+// Closing "Get started" section. The Products-popup component is reused as a vertical switcher
+// (open-source featured + selected by default; dstack Sky / Enterprise as rows). Each tab's detail
+// is a bordered box with a footer-bar CTA: open-source shows the install code, Sky shows the GPU
+// marketplace alongside the clouds you can bring, Enterprise is a placeholder for now.
export function GetStartedSection() {
+ const [tab, setTab] = useState('oss');
+ const [method, setMethod] = useState<(typeof installMethods)[number]['id']>(installMethods[0].id);
+ const [stars, setStars] = useState(null);
+
+ // Live star count for the open-source tile, fetched once. Best-effort: if the API is rate-limited
+ // or errors, the badge simply doesn't render.
+ useEffect(() => {
+ let active = true;
+ fetch(GITHUB_API_URL)
+ .then(response => (response.ok ? response.json() : null))
+ .then(data => {
+ if (active && data && typeof data.stargazers_count === 'number') setStars(data.stargazers_count);
+ })
+ .catch(() => {});
+ return () => {
+ active = false;
+ };
+ }, []);
+
+ // Each option behaves like a tab: click or Enter/Space selects it.
+ const optionProps = (id: DeployTab) => ({
+ role: 'tab',
+ 'aria-selected': tab === id,
+ tabIndex: 0,
+ onClick: () => setTab(id),
+ onKeyDown: (event: KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ setTab(id);
+ }
+ },
+ });
+
+ const activeInstall = installMethods.find(m => m.id === method) ?? installMethods[0];
+
return (
Get started
- ({
- id: method.id,
- label: method.label,
- content: ,
- }))}
- />
- }
- title="dstack"
- imageFirst
- action={
-
- Quickstart
-
- }
- >
- 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.
-
- Sign up
-
+
+ {/* Left: the popup-style selector. */}
+
+
+
+
+ {stars !== null && (
+ {formatStars(stars)}
+ )}
+
+
+ dstack
+ The open-source control plane that works across clouds, Kubernetes, and on-prem.
+
+
+
+
+
+
+ dstack Sky
+ Access GPU marketplace, or bring your own clouds. Hosted and managed by us.
+
+
+
+
+
+
+ Enterprise
+ Self-hosted with SSO, air-gapped setup, dedicated support, and more.
+
+
+
+
+ {/* Open-source: install-method tabs + read-only code + footer CTA bar. */}
+ {tab === 'oss' && (
+
+
+
+ {installMethods.map(m => (
+ setMethod(m.id)}
+ >
+ {m.label}
+
+ ))}
+
+
+
+
+
+
+ Use with your own clouds, Kubernetes, and on-prem clusters
+ Use with your own clouds & clusters
+
+ Install open-source
+
+
+
+ )}
+
+ {/* dstack Sky: GPU marketplace + bring-your-own-clouds, equal columns; footer CTA. */}
+ {tab === 'sky' && (
+
+
+ {/* Two panes with tab-styled headers. Source order is header→list, header→list, so on
+ mobile (single column) each header sits directly above its own list; on desktop a
+ 2-row grid (auto-flow column) puts both headers in a row and both lists beneath. */}
+
+
GPU marketplace
+
+
+ {gpuOffers.map(gpu => (
+
+ {gpu.name} {' '}{gpu.memory}
+ {gpu.price}/hr
+
+ ))}
+
+
+
Bring your own clouds
+
+
+ {SKY_CLOUDS.map(cloud => (
+ {cloud}
+ ))}
+
+
+
+
+
+ Sign up to get $5 credit for on-demand and spot instances
+ Sign up to get $5 in credits
+
+ Sign up
+
+
+
+ )}
+
+ {/* Enterprise: the open-source box's tabbed shell, with a single "Extra" tab for now. */}
+ {tab === 'ent' && (
+
+
+
+ Self-managed
+
+
+
+ {ENT_CAPS.map(cap => (
+
+ {cap.icon}
+
+ {cap.title}
+ {cap.sub}
+
+
+ ))}
+
-
-
-
-
dstack Enterprise
-
Self-hosted with SSO, air-gapped setup, dedicated support, and more.
-
- Talk to us
-
+
+
+ Talk to our team to get answers and a free trial
+ Talk to our team for a free trial
+
+ Contact us
-
+
- }
- 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
index 5c03d02e4..96c414001 100644
--- a/website/src/pages/Home/HomePage.tsx
+++ b/website/src/pages/Home/HomePage.tsx
@@ -1,33 +1,22 @@
import Button from '@cloudscape-design/components/button';
+import { useLayoutContext } from '../../App';
import { heroButtonStyle } from '../../cloudscape-theme';
+import { HeroSquircle } from '../../components/HeroSquircle';
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' });
-
+ const { theme } = useLayoutContext();
return (
@@ -44,7 +33,15 @@ export function HomePage() {
)}
-
+ {
+ event.preventDefault();
+ document.getElementById('resources')?.scrollIntoView({ behavior: 'smooth' });
+ }}
+ style={heroButtonStyle}
+ >
Get started
@@ -64,12 +61,6 @@ export function HomePage() {
-
- {/* 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/styles.css b/website/src/styles.css
index f450216c9..e5335dc98 100644
--- a/website/src/styles.css
+++ b/website/src/styles.css
@@ -41,13 +41,6 @@
--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'] {
@@ -61,13 +54,6 @@
--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%
- );
}
* {
@@ -309,29 +295,171 @@ p {
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 {
+/* "Products" top-nav hover menu. The trigger (.site-hover-menu__trigger) inherits the plain
+ text-link look from .site-menu-button; the popup opens on hover and is anchored to the
+ trigger. Continuity across the trigger→popup gap is handled by a close delay in JS
+ (hover-intent), so no visual bridge is needed here. */
+.site-hover-menu {
+ position: relative;
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 */
+
+/* Dropdown caret on the Products trigger — rotates 180° while the menu is open. */
+.site-hover-menu__caret {
+ width: 12px;
+ height: 12px;
+ opacity: 1; /* full text colour — the caret reads as black like the menu label */
+ transition: transform 0.15s ease;
}
-.site-menu-dropdown [class*='awsui_button']:hover {
- background: transparent !important;
- text-decoration: underline;
- text-underline-offset: 0.2em;
+.site-hover-menu__trigger[aria-expanded='true'] .site-hover-menu__caret {
+ transform: rotate(180deg);
+}
+
+/* "Products" popup: the open-source product featured on the brand gradient (with its live
+ GitHub star count), then dstack Sky & Enterprise as rows. Anchored to the trigger; flat with a
+ single 0.5px cs-text border and 12px radius, matching the landing's popup geometry. */
+.site-products-menu {
+ position: absolute;
+ top: calc(100% + 6px);
+ left: 0;
+ z-index: 1000;
+ width: 360px;
+ padding: 6px;
+ background: var(--cs-bg);
+ border: 0.5px solid var(--cs-text);
+ border-radius: 12px;
+}
+
+/* Featured open-source panel — brand gradient; the whole panel links to the docs. Carries an icon
+ tile (like the Sky/Enterprise rows) with the star count beneath, so all three read consistently. */
+.site-products-menu__feat {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 14px;
+ border-radius: 10px;
+ background: linear-gradient(135deg, #002aff, #002aff, #e165fe);
+ color: #fff;
+ text-decoration: none;
+}
+/* Left column: the icon tile (same as the Sky/Enterprise rows) with the star count beneath it. */
+.site-products-menu__feat-iccol {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ flex: 0 0 auto;
+}
+.site-products-menu__feat-body {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+.site-products-menu__feat-ic {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
+ border: 0.5px solid rgba(255, 255, 255, 0.5);
+ color: #fff;
+}
+.site-products-menu__feat-ic svg {
+ width: 20px;
+ height: 20px;
+}
+.site-products-menu__feat-name {
+ display: block;
+ color: #fff;
+ font-size: 16px;
+ font-weight: 600;
+}
+.site-products-menu__feat-desc {
+ display: block;
+ margin: 8px 0 13px;
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 13px;
+ line-height: 1.5;
}
-/* 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;
+/* The "Documentation" button. Hovering anywhere on the featured panel changes the button's
+ background (only the button reacts, not the whole section). */
+.site-products-menu__feat-cta {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 7px 14px;
+ border-radius: 8px;
+ background: #fff;
+ color: #16191f;
+ font-size: 12.5px;
+ font-weight: 600;
+ transition: background-color 0.15s ease;
+}
+.site-products-menu__feat:hover .site-products-menu__feat-cta {
+ background: #e9ebef;
+}
+
+/* Live GitHub star count, centered beneath the icon tile (only shown once the API responds). */
+.site-products-menu__gh {
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 12px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+
+/* dstack Sky & Enterprise rows. */
+.site-products-menu__list {
+ display: flex;
+ flex-direction: column;
+ margin-top: 6px;
+}
+.site-products-menu__row {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 11px 10px;
+ border-radius: 9px;
+ color: var(--cs-text);
+ text-decoration: none;
+}
+.site-products-menu__row:hover {
+ background: var(--cs-hover);
+}
+.site-products-menu__ic {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
+ border: 0.5px solid var(--cs-text);
+ color: var(--cs-text);
+}
+.site-products-menu__ic svg {
+ width: 20px;
+ height: 20px;
+}
+.site-products-menu__rbody {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+.site-products-menu__name {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ color: var(--cs-text);
+ font-size: 15px;
+ font-weight: 600;
+}
+.site-products-menu__desc {
+ display: block;
+ margin-top: 3px;
+ color: color-mix(in srgb, var(--cs-text) 68%, var(--cs-muted));
+ font-size: 13px;
+ line-height: 1.5;
}
.home-hero {
@@ -340,25 +468,21 @@ p {
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);
+ /* The content column overlaps the absolutely-positioned art box on the right; let pointer
+ events fall through its empty areas so the live hero scene stays hoverable. The
+ interactive children (text + CTAs) opt back in below. */
+ pointer-events: none;
+}
+
+.home-hero h2,
+.home-hero p,
+.home-hero__actions {
+ pointer-events: auto;
}
.home-hero h2 {
@@ -401,34 +525,14 @@ p {
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 {
+/* Live squircle hero scene occupies the second content column: right edge at the frame
+ edge, width of one column, so it lines up with the second column of the blocks below.
+ The SVG inside is width:100%, so it scales to this box. */
+.hero-squircle {
position: absolute;
- top: 68px;
+ top: 40px;
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
@@ -517,11 +621,6 @@ p {
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 {
@@ -801,25 +900,32 @@ p {
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;
+/* "Access marketplace GPUs" price list — the same monospace row treatment as the dstack Sky
+ marketplace pane (.gs-mkt__row), inside a bordered card matching the landing's other boxes. */
+.gpu-mkt {
+ border: 1px solid var(--cs-border);
+ border-radius: 12px;
+ background: var(--cs-bg);
+ padding: 16px 20px;
+ overflow: hidden;
+}
+.gpu-mkt__list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
}
/* 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 {
+.docs-article .figma-card a,
+.docs-article .gs-deploy 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;
}
@@ -859,11 +965,14 @@ 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-card descriptions: muted, 14px (a step down from the standard small text) since they
+ sit in a dense card grid. Higher specificity than `.doc-alternating p` so it wins (the cards
+ sit inside an alternating block). */
.concept-grid .media-card p {
- font-size: var(--font-small);
+ padding: 0;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--cs-muted);
}
/* "Vendor-agnostic, open-source" architecture diagram (components/ArchitectureDiagram.tsx),
@@ -1142,27 +1251,62 @@ p {
.concept-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
+ grid-auto-rows: 1fr; /* equal-height cards across both rows */
gap: var(--card-gap);
+ /* No outer border/padding — each card carries its own dotted outline (DashedBorder / .arch-dash),
+ matching the architecture diagram's pills. */
}
-/* Each concept card is a whole-card link to the docs: text-colored (not link-blue),
- no underline, with a subtle background on hover to signal it's clickable. */
+/* Each concept card is a whole-card link to the docs: an uppercase label (with a small gradient
+ dot), a tight title, and muted copy inside the landing's dotted outline — the shared
+ / .arch-dash, matching the architecture-diagram pills above. Faint fill on
+ hover, and the arrow slides. */
.concept-grid .media-card {
display: block;
- position: relative; /* for the arrow + the dotted border overlay */
+ position: relative; /* for the arrow, label dot, and dotted overlay */
color: var(--cs-text);
text-decoration: none !important;
- transition: background 0.15s ease;
- border: none; /* dotted outline is drawn by the shared (.arch-dash) instead */
+ background: transparent;
+ border: none;
+ border-radius: 12px;
+ padding: 16px 18px 18px;
+ transition: background-color 0.15s ease;
}
-.concept-grid .media-card:hover {
- background: var(--cs-hover);
+/* ActionCard-style navigation arrow on the title row (aligned with Fleets / Dev environments /
+ …) — hidden until hover, then fades in and slides right. No background change on hover; the
+ arrow alone signals interactivity. */
+.concept-card__arrow {
+ margin-left: auto;
+ display: inline-flex;
+ color: var(--cs-text);
+ opacity: 0;
+ transition: opacity 0.18s ease, transform 0.18s ease;
+}
+.concept-grid .media-card:hover .concept-card__arrow {
+ opacity: 1;
+ transform: translateX(4px);
+}
+
+/* Uppercase category label above the title — black and light weight for a delicate look. */
+.concept-card__label {
+ display: block;
+ margin-bottom: 11px;
+ font-size: 11px;
+ font-weight: 400;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+ color: var(--cs-text);
}
.concept-grid .media-card h3 {
- font-size: var(--font-card-heading);
- padding-right: 44px; /* clear the arrow */
+ display: flex;
+ align-items: center;
+ margin: 0 0 6px;
+ padding: 0;
+ font-size: 17px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
}
/* Quote author name reuses the card-heading size. */
@@ -1170,15 +1314,6 @@ p {
font-size: var(--font-card-heading) !important;
}
-/* ActionCard-style navigation arrow, top-right. */
-.concept-card__arrow {
- position: absolute;
- top: 16px;
- right: 18px;
- display: inline-flex;
- color: var(--cs-text);
-}
-
/* Product cards reusing the figma-card style. Used inside an AlternatingDocBlock
visual slot, so it stays vertically centered (no top margin). */
.product-grid {
@@ -1187,7 +1322,7 @@ p {
gap: var(--card-gap);
}
-/* Two-up variant: dstack Sky and dstack Enterprise side by side. */
+/* Two-up variant: dstack Sky and Enterprise side by side. */
.product-grid--pair {
grid-template-columns: repeat(2, 1fr);
}
@@ -1229,6 +1364,358 @@ p {
margin-top: 0;
}
+/* "Get started" deployment switcher. The Products-popup component reused as a vertical switcher:
+ a bordered selector panel on the left whose options carry a caption + description, with the
+ selected option filled by the brand gradient. Open-source is featured (bigger, with a live star
+ count and a Documentation button) and selected by default; dstack Sky / Enterprise are compact
+ rows. The right column shows the selection's detail — install code for open-source, an included-
+ features list + CTA for the hosted / self-hosted tiers. align-items: stretch keeps the selector
+ and the detail equal height and top-aligned. */
+.gs-deploy {
+ display: grid;
+ grid-template-columns: 1fr 2fr; /* selector 1/3, detail 2/3 */
+ align-items: start; /* the box sets its own (taller) height; the rail sits at the top, not stretched */
+ gap: 36px;
+ margin-top: 8px;
+}
+
+/* Left: the selector — a bare column of individually-bordered option cards (no panel chrome). */
+.gs-rail {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.gs-opt {
+ position: relative;
+ display: block;
+ width: 100%;
+ border: 0.5px solid transparent; /* non-active: no visible border (space reserved so selecting never shifts) */
+ border-radius: 10px;
+ background: transparent;
+ color: var(--cs-text);
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+ transition: background-color 0.15s ease, border-color 0.15s ease;
+}
+.gs-opt:not(.gs-opt--on):hover {
+ background: var(--cs-hover);
+}
+.gs-opt--on,
+.gs-opt--on:hover {
+ background: linear-gradient(135deg, #002aff, #002aff, #e165fe);
+ color: #fff;
+}
+.gs-opt__name {
+ display: block;
+ font-size: 17px;
+ font-weight: 600;
+}
+.gs-opt__desc {
+ display: block;
+ margin-top: 4px;
+ font-size: 13px;
+ line-height: 1.5;
+ color: color-mix(in srgb, var(--cs-text) 68%, var(--cs-muted));
+}
+.gs-opt--on .gs-opt__desc {
+ color: rgba(255, 255, 255, 0.88);
+}
+
+/* Open-source: the featured card — bigger, but now with an icon tile (like the Sky/Enterprise rows)
+ and the star count moved beneath the name, so all three controls read consistently. */
+.gs-opt--feat {
+ padding: 20px;
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+}
+/* Left column: the icon tile with the star count centered beneath it (the icon itself lays out
+ exactly like the Sky / Enterprise icon tiles). */
+.gs-opt__icwrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ flex: 0 0 auto;
+}
+.gs-opt__stars {
+ font-size: 12px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ color: inherit;
+}
+
+/* dstack Sky / Enterprise: compact rows with an icon. */
+.gs-opt--row {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 13px 14px;
+}
+.gs-opt__ic {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
+ border: 0.5px solid var(--cs-border);
+ color: inherit;
+}
+.gs-opt__ic svg {
+ width: 20px;
+ height: 20px;
+}
+.gs-opt--on .gs-opt__ic {
+ border-color: rgba(255, 255, 255, 0.5);
+}
+.gs-opt__body {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+/* Right: the selection's detail. */
+.gs-detail {
+ min-width: 0;
+}
+/* The detail box — shared shell for all three tabs: a bordered container with a fixed height (so its
+ own scrollable lists never inflate it), holding a tab strip, content, and a footer CTA bar. The
+ height is tuned a touch taller than the rail, giving the content room to breathe and matching the
+ tabbed code cards higher up the page; all three tabs reuse this shell, so they stay equal height. */
+.gs-box {
+ height: 320px;
+ display: flex;
+ flex-direction: column;
+ background: var(--cs-bg);
+ border: 1px solid var(--cs-border); /* matches the landing's cards (.media-card / .figma-card) */
+ border-radius: 12px;
+ overflow: hidden;
+}
+.gs-boxfoot {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+ padding: 13px 16px 13px 20px;
+ border-top: 1px solid color-mix(in srgb, var(--cs-border) 14%, transparent);
+}
+.gs-foot__note {
+ font-size: 13px;
+ color: var(--cs-muted);
+ min-width: 0; /* allow the note to shrink/wrap so it shares the row with the CTA on narrow screens */
+}
+/* Footer note has a full (desktop) and a shorter (mobile) wording; the mobile breakpoint swaps them. */
+.gs-foot__short {
+ display: none;
+}
+/* The footer CTA(s) keep their width (never shrink/wrap mid-word); the note takes the rest. */
+.gs-boxfoot > [class*='awsui_button'] {
+ flex: 0 0 auto;
+}
+
+/* Tab strip — shared by the open-source code box (uv / pip / Docker) and dstack Sky (GPU
+ marketplace / bring your own clouds), so both boxes read the same: tabs on top, content, footer. */
+.gs-tabs {
+ display: flex;
+ gap: 2px;
+ padding: 6px 8px 0;
+ border-bottom: 1px solid color-mix(in srgb, var(--cs-border) 14%, transparent);
+}
+.gs-tab {
+ padding: 15px 14px;
+ border: 0;
+ border-radius: 8px 8px 0 0;
+ background: transparent;
+ color: var(--cs-muted);
+ font-family: inherit;
+ font-size: 16px; /* match the page's other (Cloudscape) tab groups: 16px / 700 */
+ font-weight: 700;
+ cursor: pointer;
+ transition: background-color 0.15s ease, color 0.15s ease;
+}
+.gs-tab:not(.gs-tab--on):hover {
+ color: var(--cs-text);
+ background: var(--cs-hover); /* match the Cloudscape tabs' hover tint */
+}
+.gs-tab--on {
+ color: var(--cs-text);
+ background: var(--cs-panel);
+}
+
+/* Open-source: read-only code fills the body. */
+.gs-codebody {
+ flex: 1 1 auto;
+ min-height: 0;
+ padding: 10px 12px;
+ overflow: hidden;
+}
+
+/* dstack Sky: two panes (GPU marketplace | bring your own clouds). Source order is header→list,
+ header→list. Desktop: a 2-row grid with column auto-flow, so the two headers land in row 1 (the
+ tab strip) and the two lists in row 2 (the content). Mobile (media query): a single column, so
+ each header sits directly above its own list. The chips reuse the open-source tab look. */
+.gs-sky {
+ display: grid;
+ grid-template-rows: auto 1fr;
+ grid-auto-flow: column;
+ grid-auto-columns: 1fr;
+ flex: 1 1 auto;
+ min-height: 0;
+}
+.gs-skyhalf {
+ min-width: 0;
+ padding: 6px 0 0 8px; /* chip text (chip pad 14) lands at 22px, matching the column content below */
+ border-bottom: 1px solid color-mix(in srgb, var(--cs-border) 14%, transparent); /* under-tab divider */
+}
+.gs-skytab {
+ display: inline-block;
+ padding: 15px 14px;
+ border-radius: 8px 8px 0 0;
+ background: var(--cs-panel);
+ color: var(--cs-text);
+ font-size: 16px; /* match the page's other tab groups: 16px / 700 */
+ font-weight: 700;
+}
+.gs-col {
+ min-width: 0;
+ min-height: 0; /* grid item: allow the 1fr row to cap it so the list scrolls instead of overflowing */
+ display: flex;
+ flex-direction: column;
+ padding: 14px 22px 0;
+}
+/* Vertical divider between the two lists only (the 2nd list pane), not under the headers. */
+.gs-sky .gs-col ~ .gs-col {
+ border-left: 1px solid color-mix(in srgb, var(--cs-border) 14%, transparent);
+}
+.gs-col__list {
+ list-style: none;
+ margin: 0;
+ padding: 0 2px 14px 0;
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ overflow-y: auto;
+ /* soften the cut-off row so the scroll reads cleanly into the footer */
+ -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
+ mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
+}
+.gs-mkt__row {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+}
+.gs-mkt__g {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 13px;
+ font-weight: 400; /* only the GPU name is emphasized (below); VRAM stays regular */
+}
+.gs-mkt__name {
+ font-weight: 600; /* emphasize the GPU name by weight, not size */
+}
+.gs-mkt__p {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 13px;
+ font-weight: 400; /* price is not emphasized — only the GPU name is */
+ white-space: nowrap;
+}
+.gs-cloud {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: var(--font-small);
+ line-height: 1.3;
+}
+
+/* Enterprise: the "Extra" capabilities — a 2×2 grid of capability rows (icon + title + one-line
+ detail), centered in the body. No card borders. */
+.gs-entbody {
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 20px 24px;
+}
+.gs-caps {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 18px 26px;
+}
+.gs-cap {
+ display: flex;
+ align-items: flex-start;
+ gap: 11px;
+}
+.gs-cap__ic {
+ flex: 0 0 auto;
+ margin-top: 1px;
+ color: #8b2fd0;
+}
+:root[data-theme='dark'] .gs-cap__ic {
+ color: #c08bff;
+}
+.gs-cap__ic svg {
+ width: 20px;
+ height: 20px;
+}
+.gs-cap__b {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+.gs-cap__t {
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 1.3;
+}
+.gs-cap__s {
+ font-size: 12.5px;
+ color: var(--cs-muted);
+ margin-top: 2px;
+ line-height: 1.4;
+}
+
+/* Brand-purple check (Sky clouds). */
+.gs-check__mark {
+ width: 17px;
+ height: 17px;
+ flex: 0 0 auto;
+ margin-top: 1px;
+ color: #8b2fd0;
+}
+:root[data-theme='dark'] .gs-check__mark {
+ color: #c08bff;
+}
+
+@media (max-width: 860px) {
+ .gs-deploy { grid-template-columns: 1fr; gap: 24px; }
+ /* Stacked: the box flows naturally to fit its content (no fixed-height scroll). */
+ .gs-box { height: auto; }
+ /* Sky: single column so each header sits directly above its own list (header→list→header→list). */
+ .gs-sky { display: flex; flex-direction: column; }
+ .gs-sky .gs-col ~ .gs-col { border-left: 0; }
+ /* Separate the second pane (its header follows the first list). */
+ .gs-sky .gs-col + .gs-skyhalf { margin-top: 12px; }
+ .gs-col__list { overflow-y: visible; -webkit-mask-image: none; mask-image: none; }
+ .gs-caps { grid-template-columns: 1fr; }
+ /* Read-only install code is wider than the phone; let it scroll instead of clipping. */
+ .gs-codebody { overflow-x: auto; }
+ /* Footer stays a single row (note + CTA, like desktop), never split onto separate rows. The note
+ uses its shorter wording and the CTAs use tighter padding so the note keeps room beside them. */
+ .gs-boxfoot { align-items: center; gap: 10px; }
+ .gs-boxfoot [class*='awsui_button'] { padding-inline: 14px !important; }
+ .gs-foot__full { display: none; }
+ .gs-foot__short { display: inline; }
+}
+
.right-rail-block {
margin-top: 32px;
padding-top: 28px;
@@ -1558,8 +2045,8 @@ p {
opacity: 0.85;
}
- .hero-slice {
- top: 24px;
+ .hero-squircle {
+ top: 12px;
}
.feature-card-grid,
@@ -1682,9 +2169,10 @@ p {
border-bottom: 0.5px solid var(--cs-border);
background: var(--cs-bg, #ffffff);
}
-}
-@media (max-width: 640px) {
+ /* Below 1024 (where the nav also collapses to a hamburger) the side-by-side hero would
+ overlap the headline, so stack it: headline + CTAs first, then the squircle below them
+ in normal flow, centered — on tablet and phone alike. */
.home-hero {
display: flex;
flex-direction: column;
@@ -1692,17 +2180,37 @@ p {
.home-hero__content {
order: 1;
- padding: 64px 0 24px;
+ padding-bottom: 8px;
}
- /* On phones the hero artwork is dropped entirely (not worth the vertical space): hide both the
- top instance and the relocated bottom copy. */
.home-hero__art {
- display: none;
+ order: 2;
+ position: static;
+ height: auto;
+ opacity: 1;
+ padding-bottom: 8px;
}
- .home-hero-mobile-art {
- display: none;
+ .home-hero__art-frame {
+ height: auto;
+ }
+
+ .hero-squircle {
+ position: static;
+ width: min(100%, 460px);
+ margin: 8px auto 0;
+ }
+}
+
+@media (max-width: 640px) {
+ .home-hero {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .home-hero__content {
+ order: 1;
+ padding: 64px 0 24px;
}
/* Hero CTAs share one full-width row on phones — two equal columns spanning the width. */