diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 6ca99d764a..9df8040d4d 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -1,4 +1,8 @@ -name: Build Docs +name: Build Site + +# Reusable build of the full site: the React landing (website/) overlaid onto the +# MkDocs docs + blog build, via scripts/docs/build_site.sh. Produces the `site` artifact +# consumed by docs.yaml (deploy) and build.yml (PR build check). on: workflow_call: @@ -11,13 +15,18 @@ jobs: - uses: astral-sh/setup-uv@v5 with: python-version: 3.11 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: website/package-lock.json - name: Install dstack run: | uv sync --extra server - name: Build run: | sudo apt-get update && sudo apt-get install -y libcairo2-dev libfreetype6-dev libffi-dev libjpeg-dev libpng-dev libz-dev - uv run mkdocs build -s + ./scripts/docs/build_site.sh - uses: actions/upload-artifact@v4 with: name: site diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 3e0d5f3a75..78212c6fda 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,4 +1,7 @@ -name: Build & Deploy Docs +name: Build & Deploy Site + +# Builds the full site (React landing + MkDocs docs + blog, see build-docs.yml) and +# cross-repo deploys it to the GitHub Pages repo serving dstack.ai. on: workflow_dispatch: diff --git a/.justfile b/.justfile index efa8c87f61..c419685edc 100644 --- a/.justfile +++ b/.justfile @@ -8,6 +8,7 @@ # * runner/.justfile – Building and uploading dstack runner and shim # * frontend/.justfile – Building and running the frontend # * mkdocs/.justfile – Building and previewing the docs site +# * website/.justfile – Building and previewing the React landing page default: @just --list @@ -19,3 +20,5 @@ import "runner/.justfile" import "frontend/.justfile" import "mkdocs/.justfile" + +import "website/.justfile" diff --git a/contributing/DOCS.md b/contributing/DOCS.md index 663e5c4c77..a0b034a181 100644 --- a/contributing/DOCS.md +++ b/contributing/DOCS.md @@ -1,5 +1,11 @@ # Documentation setup +> **The dstack.ai site has three parts on one origin:** the **landing** page (`/`) is a React +> app in [`website/`](../website); the **docs** (`/docs`) and **blog** (`/blog`) are built with +> MkDocs from `mkdocs/`. This guide covers the **docs and blog** (MkDocs). For the landing and +> for building everything together, see [The landing page](#the-landing-page-website) and +> [Building the whole site](#building-the-whole-site) below. + ## 1. Clone the repo: ```shell @@ -36,7 +42,7 @@ uv run pre-commit install ## 5. Preview documentation -To preview the documentation, run the follow command: +To preview the **docs and blog** (MkDocs), run the follow command: ```shell uv run mkdocs serve --livereload -s @@ -44,12 +50,44 @@ uv run mkdocs serve --livereload -s The `--livereload` flag is required to work around live-reload bugs in recent `mkdocs` versions. +This serves the docs and blog only. The landing page (`/`) is a separate React app — when you +run `mkdocs serve` on its own, `/` simply redirects to `/docs/`. To work on the landing, see +[The landing page](#the-landing-page-website) below. + If you want to build static files, you can use the following command: ```shell uv run mkdocs build -s ``` +## The landing page (website/) + +The landing page at `/` is a React (Vite) app in [`website/`](../website), not MkDocs. It has +its own `package.json`/`node_modules`. Preview it on its own (requires Node 20+): + +```shell +just website-dev # Vite dev server on http://127.0.0.1:5173 +``` + +Docs/blog links on the landing resolve same-origin (`/docs`, `/blog`), which 404 in standalone +dev. Point them at a live site while iterating: `just website-dev https://dstack.ai`. + +The `/old` route is kept as a template for building future product pages (reachable in dev; not +part of the production deploy). Google Analytics and the social/OG image reuse the same property +and MkDocs-generated card as the rest of the site. + +## Building the whole site + +CI builds the landing and the MkDocs docs/blog and overlays them into a single `site/`: + +```shell +just site-build # website/dist + `mkdocs build` -> ./site (scripts/docs/build_site.sh) +just site-serve # preview the combined site on http://127.0.0.1:8001 +``` + +In the combined build the React `index.html` owns `/`, while MkDocs serves `/docs`, `/blog`, and +the shared `/assets`. This is what the `Build & Deploy Site` workflow deploys. + ## Documentation build system The documentation uses a custom build system with MkDocs hooks to generate various files dynamically. @@ -141,7 +179,7 @@ we should not reintroduce per-tag OpenAPI files unless there is a concrete reaso ``` mkdocs/ # docs_dir for the mkdocs site -├── index.md # Homepage +├── index.md # Redirects to /docs/ (the landing "/" is the React app in website/) ├── docs/ # /docs/ URL section │ ├── index.md # Getting started │ ├── installation.md @@ -157,7 +195,13 @@ mkdocs/ # docs_dir for the mkdocs site ├── layouts/ # Social card layouts └── assets/ # Stylesheets, images, fonts +website/ # React (Vite) landing page — served at "/" +├── index.html # Entry; title, OG/meta, Google Analytics +├── src/ # App, pages (Home, Old), components, routes +└── public/static/ # Landing assets (namespaced to avoid clashing with /assets) + scripts/docs/ +├── build_site.sh # Build landing + docs/blog and overlay into ./site ├── hooks.py # MkDocs build hooks ├── gen_llms_files.py # llms.txt generation ├── gen_schema_reference.py # Schema expansion diff --git a/mkdocs.yml b/mkdocs.yml index 1ea8193b47..2fc74935aa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,7 +11,7 @@ repo_name: dstackai/dstack edit_uri: edit/master/mkdocs/ #Copyright -copyright: © 2026 dstack Inc. +copyright: © 2026, dstack Inc. All rights reserved. # Source directory for site content docs_dir: mkdocs @@ -32,21 +32,23 @@ theme: font: text: Source Sans Pro code: IBM Plex Mono + # Light (default) + dark (slate) schemes WITH toggles — Material needs the toggle radios so its + # palette JS honors the stored __palette and applies it during parse (flash-free). We HIDE the + # toggle buttons via CSS (cloudscape-docs.css) and drive the choice from the shared `dstack-theme` + # localStorage key (website/src/theme.ts), translated into __palette in main.html's extrahead. palette: - - media: "(prefers-color-scheme: light)" - scheme: default + - scheme: default primary: white accent: lilac - # toggle: - # icon: material/weather-night - # name: Switch to dark mode - # - media: "(prefers-color-scheme: dark)" - # scheme: slate - # primary: black - # accent: light blue - # toggle: - # icon: material/weather-sunny - # name: Switch to light mode + toggle: + icon: material/weather-night + name: Switch to dark mode + - scheme: slate + primary: white + accent: lilac + toggle: + icon: material/weather-sunny + name: Switch to light mode features: - content.tooltips - navigation.path @@ -60,7 +62,7 @@ theme: - navigation.sections # - navigation.expand - navigation.top - - announce.dismiss + # announce.dismiss removed — /old's banner has no dismiss (×) button - navigation.tracking - navigation.footer @@ -252,6 +254,7 @@ extra_css: - assets/stylesheets/termynal.css - assets/stylesheets/landing.css - assets/stylesheets/pricing.css + - assets/stylesheets/cloudscape-docs.css extra_javascript: - https://unpkg.com/swagger-ui-dist@5.32.0/swagger-ui-bundle.js - assets/javascripts/swagger.js diff --git a/mkdocs/assets/images/arch-logos/amd.webp b/mkdocs/assets/images/arch-logos/amd.webp new file mode 100644 index 0000000000..a53f88bfc5 Binary files /dev/null and b/mkdocs/assets/images/arch-logos/amd.webp differ diff --git a/mkdocs/assets/images/arch-logos/aws.svg b/mkdocs/assets/images/arch-logos/aws.svg new file mode 100644 index 0000000000..80192a12d8 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/aws.svg @@ -0,0 +1,38 @@ + + + diff --git a/mkdocs/assets/images/arch-logos/gcp.svg b/mkdocs/assets/images/arch-logos/gcp.svg new file mode 100644 index 0000000000..b8478f1f93 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/gcp.svg @@ -0,0 +1,9 @@ + + diff --git a/mkdocs/assets/images/arch-logos/huggingface.svg b/mkdocs/assets/images/arch-logos/huggingface.svg new file mode 100644 index 0000000000..cab717b750 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/huggingface.svg @@ -0,0 +1,7 @@ + diff --git a/mkdocs/assets/images/arch-logos/kubernetes.svg b/mkdocs/assets/images/arch-logos/kubernetes.svg new file mode 100644 index 0000000000..140c74aab4 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/kubernetes.svg @@ -0,0 +1,16 @@ + diff --git a/mkdocs/assets/images/arch-logos/lambda.svg b/mkdocs/assets/images/arch-logos/lambda.svg new file mode 100644 index 0000000000..6143f02db0 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/lambda.svg @@ -0,0 +1 @@ + diff --git a/mkdocs/assets/images/arch-logos/meta.svg b/mkdocs/assets/images/arch-logos/meta.svg new file mode 100644 index 0000000000..ac4cdd3f5a --- /dev/null +++ b/mkdocs/assets/images/arch-logos/meta.svg @@ -0,0 +1,19 @@ + + diff --git a/mkdocs/assets/images/arch-logos/nebius.svg b/mkdocs/assets/images/arch-logos/nebius.svg new file mode 100644 index 0000000000..55a25a6b2c --- /dev/null +++ b/mkdocs/assets/images/arch-logos/nebius.svg @@ -0,0 +1,14 @@ + diff --git a/mkdocs/assets/images/arch-logos/nvidia.svg b/mkdocs/assets/images/arch-logos/nvidia.svg new file mode 100644 index 0000000000..7514d0ed39 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/nvidia.svg @@ -0,0 +1 @@ + diff --git a/mkdocs/assets/images/arch-logos/pytorch.svg b/mkdocs/assets/images/arch-logos/pytorch.svg new file mode 100644 index 0000000000..9dcafc39af --- /dev/null +++ b/mkdocs/assets/images/arch-logos/pytorch.svg @@ -0,0 +1,12 @@ + + + + diff --git a/mkdocs/assets/images/arch-logos/runpod.svg b/mkdocs/assets/images/arch-logos/runpod.svg new file mode 100644 index 0000000000..f6fab58239 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/runpod.svg @@ -0,0 +1,3 @@ + diff --git a/mkdocs/assets/images/arch-logos/sglang.svg b/mkdocs/assets/images/arch-logos/sglang.svg new file mode 100644 index 0000000000..a82fa0aeb1 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/sglang.svg @@ -0,0 +1 @@ + diff --git a/mkdocs/assets/images/arch-logos/tenstorrent.svg b/mkdocs/assets/images/arch-logos/tenstorrent.svg new file mode 100644 index 0000000000..570923ae35 --- /dev/null +++ b/mkdocs/assets/images/arch-logos/tenstorrent.svg @@ -0,0 +1,23 @@ + + diff --git a/mkdocs/assets/images/arch-logos/vllm.svg b/mkdocs/assets/images/arch-logos/vllm.svg new file mode 100644 index 0000000000..07eaef09ca --- /dev/null +++ b/mkdocs/assets/images/arch-logos/vllm.svg @@ -0,0 +1 @@ + diff --git a/mkdocs/assets/javascripts/extra.js b/mkdocs/assets/javascripts/extra.js index e21b22e6e0..dbb999b558 100644 --- a/mkdocs/assets/javascripts/extra.js +++ b/mkdocs/assets/javascripts/extra.js @@ -31,12 +31,23 @@ function setupTermynal(root = document) { const singleInput = getTermynalOption(node, termynalRoot, "termynalSingleInput") === "true"; const copyEnabled = getTermynalOption(node, termynalRoot, "termynalCopy") === "true"; const instant = getTermynalOption(node, termynalRoot, "termynalInstant") === "true"; + const maxHeight = getTermynalOption(node, termynalRoot, "termynalMaxHeight"); + const lines = text.split(/(? { + if (line.startsWith(promptLiteralStart)) return line.slice(promptLiteralStart.length).trimEnd(); + if (line.startsWith(customPromptLiteralStart)) { + const p = line.indexOf(promptLiteralStart); + if (p !== -1) return line.slice(p + promptLiteralStart.length).trimEnd(); + } + return null; + }).filter(l => l !== null).join("\n"); const copyText = node.dstackTermynalCopyText || termynalRoot?.dstackTermynalCopyText || getTermynalOption(node, termynalRoot, "termynalCopyText") || - text.trimEnd(); - const maxHeight = getTermynalOption(node, termynalRoot, "termynalMaxHeight"); - const lines = text.split(/(? script in main.html reads dstack-theme on load and applies it flash-free). +function setupThemeToggle() { + var KEY = "data-md-color-scheme"; + function apply(dark) { + try { + localStorage.setItem("dstack-theme", dark ? "dark" : "light"); + } catch (e) {} + // __dstackApplyTheme (defined inline in main.html's extrahead) owns the dark→palette mapping + // and flips the
scheme. Fall back to the bare scheme flip if it somehow isn't loaded. + if (typeof window.__dstackApplyTheme === "function") { + window.__dstackApplyTheme(dark, true); + } else { + document.body.setAttribute(KEY, dark ? "slate" : "default"); + } + } + document.querySelectorAll("[data-cs-theme-toggle]").forEach(function (btn) { + btn.addEventListener("click", function () { + apply(document.body.getAttribute(KEY) !== "slate"); + }); + }); +} + +// Clicking a heading's ¶ permalink copies its full URL to the clipboard instead of +// scrolling/jumping (which landed under the sticky header). Delegated, so it survives +// instant navigation and re-rendered content. +function setupHeaderlinkCopy() { + document.addEventListener("click", function (event) { + var link = event.target.closest && event.target.closest("a.headerlink"); + if (!link) return; + event.preventDefault(); + event.stopPropagation(); + var url = location.origin + location.pathname + link.getAttribute("href"); + copyTextToClipboard(url); + flashCopied(event); + }, true); +} + +function copyTextToClipboard(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).catch(function () { legacyCopyText(text); }); + } else { + legacyCopyText(text); + } +} + +function legacyCopyText(text) { + var ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.top = "-1000px"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand("copy"); } catch (e) {} + document.body.removeChild(ta); +} + +// Small "Copied!" toast at the click point — fixed-positioned so it shows even after the +// ¶ (which is opacity:0 until hover) is no longer hovered. +function flashCopied(event) { + var tip = document.createElement("span"); + tip.className = "cs-copied-tip"; + tip.textContent = "Copied!"; + tip.style.left = event.clientX + "px"; + tip.style.top = event.clientY + "px"; + document.body.appendChild(tip); + setTimeout(function () { tip.remove(); }, 1200); +} + window.addEventListener("DOMContentLoaded", function() { - let tabs = document.querySelector(".md-tabs") - let header = document.querySelector(".md-header") - let search = document.querySelector(".md-search") - search.parentNode.insertBefore(tabs, search) - header.classList.add("ready") + // Tabs are now rendered directly inside the header (see header-2.html) instead of being + // relocated here from below the header — that move caused a visible flash on load. setupTermynal() setupCustomCodeTitles() setupSensitiveTocActiveState() + setupThemeToggle() + setupHeaderlinkCopy() }); (function () { diff --git a/mkdocs/assets/stylesheets/cloudscape-docs.css b/mkdocs/assets/stylesheets/cloudscape-docs.css new file mode 100644 index 0000000000..cabeb93b94 --- /dev/null +++ b/mkdocs/assets/stylesheets/cloudscape-docs.css @@ -0,0 +1,2129 @@ +/* ============================================================================ + Cloudscape docs theme + Overrides that make the MkDocs chrome match the /old page (the React/Cloudscape + reference in website/src/styles.css) pixel-to-pixel, in light and dark. + + Loaded LAST (after extra.css, landing.css, pricing.css) so these win on source + order — avoids per-property specificity fights with the existing theme CSS. + Design tokens (--cs-*) are defined in extra.css. Built up component by component. + ============================================================================ */ + +/* ===== Dark-mode foundation ================================================ + Material's slate scheme paints the page a lighter blue-gray (#1e2129); /old's dark page is + near-black (#0f141d = --cs-bg). Repoint Material's default bg token so the body and every + surface that inherits it match /old. Code blocks and admonitions keep their own tokens. */ +[data-md-color-scheme="slate"] { + --md-default-bg-color: var(--cs-bg); +} + +/* ===== Announce banner ===================================================== */ +/* /old's banner INVERTS with the theme: dark bar + white text in light, light bar + dark text in + dark. Driving bg from --cs-text and text from --cs-bg reproduces that for free (light: #16191f / + #fff; dark: #f2f3f3 / #0f141d) — and keeps light mode pixel-identical to before. */ +.md-banner { + background-color: var(--cs-text); + color: var(--cs-bg); + /* Match /old's .site-banner EXACTLY (website/src/styles.css): flex-centered, min-height 38px, + padding 9px 16px 7.5px — not the old padding:3px 0 + 26px line-height (which only happened + to be ~38px tall but positioned the text differently). */ + display: flex; + align-items: center; + justify-content: center; + min-height: 38px; + padding: 9px 16px 7.5px; + margin-bottom: 0; /* a base 1px bottom margin left a gap below the banner, pushing the whole + header (and logo/burger) 1px lower than /old, where banner+header touch */ +} + +.md-typeset.md-banner__inner { + font-size: 16.5px; + font-weight: 300; /* /old measures 16.5px / weight 300 (lighter); earlier pass wrongly used 16/400 */ + line-height: 1.15; /* /old's .site-banner__link line-height (18.975px) — was inheriting 26px */ + margin: 0; /* drop .md-grid's auto margins; the flex parent centers it */ + color: var(--cs-bg); /* inverts with the banner bg; in slate a stray rule otherwise made it #1e2129 (invisible on the light bar) */ + /* .md-typeset sets font-smoothing:auto (subpixel → heavier); override here (0,2,0 beats + .md-typeset's 0,1,0) so the banner renders antialiased like /old + the landing. */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* extra.css forces the link to 0.75rem (15px); restore /old's size/weight (the visible text is the link). */ +.md-typeset.md-banner__inner a { + font-size: 16.5px; + font-weight: 300; + color: var(--cs-bg) !important; /* the visible banner text is this link; invert it with the bar (beats the slate link color) */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* Light-on-dark text looks thinner than dark-on-light at the same weight; use a heavier weight in + dark mode so the banner reads with the same emphasis as in light (matches the landing). */ +[data-md-color-scheme="slate"] .md-typeset.md-banner__inner, +[data-md-color-scheme="slate"] .md-typeset.md-banner__inner a { + font-weight: 500; +} +/* The → arrow: replicate /old's .site-banner__arrow exactly — inline-flex (so it doesn't stretch + the line box past the text), vertical-align middle, and a 6px gap before it (was 0). */ +.md-banner__inner .icon { + display: inline-flex !important; + align-items: center; + justify-content: center; + vertical-align: middle; + margin-inline-start: 6px; + width: 17px !important; /* match /old's 17×17 arrow box so the 17px svg sits centered, not overflowing a 12.5px box */ + height: 17px !important; +} +/* Banner arrow is now /old's exact 24×24 stroked → (swapped in main.html); size it 17×17 like /old. + max-width:none lifts mkdocs's `.md-typeset svg{max-width:100%}`, which was capping it to ~12.5px. */ +.md-banner__inner .icon svg { + width: 17px !important; + height: 17px !important; + max-width: none !important; +} + +/* ===== Header ============================================================== */ +@media screen and (min-width: 76.1875em) { + /* White (dark in slate) bar with a 1px solid hairline, like /old .site-nav. */ + .md-header { + background-color: var(--cs-bg); + backdrop-filter: none; + border-bottom: 1px solid var(--cs-border); + padding-bottom: 0; + } + + /* Full-width nav, 78px tall, 24px gutters (vs the centered md-grid). */ + .md-header__inner.md-grid { + height: var(--cs-nav-height); + min-height: var(--cs-nav-height); + max-width: none; + margin: 0; + padding-left: 24px; + padding-right: 24px; + align-items: center; + } + + /* /old keeps the logo alone on the left and clusters the whole nav on the right. + Push the tab group (and the search + buttons that follow it in the DOM) to the + right edge, leaving an empty middle. */ + /* Search is FIRST in the right cluster (it ships before the tabs in the template), so it + carries the auto-margin that pushes search → tabs → buttons to the right edge as one group. + The tabs then sit a fixed gap to the search's right, just left of "Docs". */ + .md-header .md-search { + margin-left: auto; + } + /* The theme toggle is the first item in the buttons block (before GitHub); 15px sits it a touch + tighter than the 20px GitHub→Get-started gap. */ + .cs-theme-toggle--header { + margin-right: 15px; + } + .md-tabs { + display: block; /* was display:none until JS added .ready — tabs now ship in the header */ + flex-grow: 0; + margin-left: 10px; /* makes the search→Docs gap (~22px) match the inter-menu gaps */ + padding-left: 0; + } + + /* The buttons block also has margin-left:auto by default, which would split the free + space into two gaps. Zero it so the single auto-margin on .md-tabs pushes the whole + cluster (tabs → search → buttons) to the right as one group. */ + .md-header__inner .md-header__buttons { + margin-left: 0; + display: flex; + align-items: center; + } + + /* No Discord icon in the header. */ + .md-tabs__item:nth-child(6) { + display: none; + } +} + +/* Below desktop the header's right cluster (search → GitHub → Get started) loses the desktop flex + layout: the buttons fall back to display:block (inline), so search abuts GitHub (0 gap), the + GitHub↔Get-started gap is just github's margin (unequal), and the pills sit a px or two off. + Restore a flex cluster with equal 16px gaps, vertically centered. */ +@media screen and (max-width: 76.1875em) { + .md-header__inner .md-header__buttons { + display: flex; + align-items: center; + /* search↔GitHub gap = the GitHub↔Get-started gap (github's existing margin-right: 20px), + so all three sit at an equal 20px. No flex `gap` — that would double with the margin. */ + margin-left: 20px; + } +} +/* Below 60em the search box collapses to a 40px icon button (already padded). The 20px button + margin then reads as too much space before GitHub — trim it so the glyph→GitHub gap matches. */ +@media screen and (max-width: 59.9375em) { + .md-header__inner .md-header__buttons { + margin-left: 4px; + } +} + +/* Keep every header pill vertically centered and free of stray vertical margins so GitHub + and the Get started split button line up exactly. */ +.md-header__buttons .md-button { + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; +} + +/* Nav links (Docs / Case studies / …) → 16px / 700 / cs-text. Active stays cs-text + (the gradient underline indicator is kept). */ +[data-md-color-primary=white] .md-tabs__link, +[data-md-color-primary=white] .md-tabs__link.md-tabs__link--active, +[data-md-color-primary=white] .md-tabs__item--active .md-tabs__link { + font-size: 16px; + font-weight: 700; + color: var(--cs-text); +} + +[data-md-color-primary=white] .md-tabs__link:hover { + color: var(--cs-text); + text-decoration: underline; + text-underline-offset: 0.2em; /* matches /old's .site-menu-link:hover distance */ +} + +/* No current-page indicator on the top nav — /old's top nav has none. Removes the + purple gradient bar from extra.css. */ +.md-tabs__item--active .md-tabs__link::after { + display: none !important; +} + +/* Header action buttons → /old pills. Prefixed with the primary color attr to + out-specify landing.css's button rules. */ +[data-md-color-primary=white] .md-header__buttons .md-button--primary, +[data-md-color-primary=white] .md-header__buttons .md-button--primary:hover { + box-sizing: border-box; + height: 36px; /* /old buttons are all 36px tall */ + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 12px; + font-size: 16px; + font-weight: 700 !important; /* beats `.md-button { font-weight: 500 !important }` in extra.css */ + line-height: 1; + padding: 0 18px; + 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 { + background: transparent; + color: var(--cs-text); + border: 1px solid var(--cs-border); +} + +/* Gap between GitHub and Get started → 20px to match /old (was 5px). */ +.md-header__buttons .md-button.github { + margin-right: 20px; +} + +/* landing.css adds a GitHub octocat ::before; /old's button is just "GitHub ↗", so drop it. */ +.md-header__buttons .md-button--primary.github::before { + content: none !important; + display: none !important; +} + +.md-header__buttons .md-button.github::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; +} + +/* 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; +} + +/* 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 { + 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 .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; +} + +/* 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; +} + +.md-header__buttons .cs-get-started__toggle svg { + width: 18px; + height: 18px; + fill: currentColor; +} + +/* 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 { + position: absolute; + top: calc(100% + 6px); + right: 0; + min-width: 300px; + padding: 8px 0; + display: flex; + flex-direction: column; + 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; +} + +.md-header__buttons .cs-get-started__menu[hidden] { + display: 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); +} + +/* Items → 4px/16px padding; a 15/600 heading-color title with an optional description below. */ +.md-header__buttons .cs-get-started__menu a { + display: flex; + flex-direction: column; + padding: 4px 16px; + font-size: 15px; + font-weight: 600; + color: var(--cs-text); + text-decoration: none; +} +.md-header__buttons .cs-get-started__item-title { + color: var(--cs-nav-heading); +} +/* 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; +} +.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; +} +/* 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; + color: var(--cs-text); + white-space: normal; +} + +.md-header__buttons .cs-get-started__menu a:hover { + background: var(--cs-hover); +} + +/* Burger (desktop sidebar toggle) — far left, like /old. */ +.cs-nav-toggle { + order: -1; + margin-left: 5px; /* /old centers its 16px icon in a 26px button at the 24px gutter → icon at + x=29; ours had it at x=28. 5px from the gutter matches /old exactly. */ + margin-right: 8px; + padding: 0; + color: var(--cs-nav-link); /* /old's burger is #424650 (muted gray), not the near-black text color */ + background: transparent; + border: 0; + cursor: pointer; + align-items: center; + justify-content: center; +} + +.cs-nav-toggle svg { + width: 16px; /* match /old's 16px stroked hamburger */ + height: 16px; + fill: none; + display: block; /* inline svg sat in a 19px line-box (3px descender below), so the icon's + center landed 1.5px above the button center; block removes the line-box + so the icon centers in the header like /old (shared cy with the logo) */ +} + +/* Logo: a bit more gap from the burger so the icon sits at /old's x=76 (not ~63). 23px (not 24) + compensates for the burger's +1px left margin above, keeping the logo icon at exactly 76. */ +.md-header .md-logo { + margin-left: 23px; +} + +/* Header chrome (burger + search icons, the "dstack" wordmark, page title) → theme-aware so it + stays visible on the dark header in dark mode (it was hardcoded black). var(--cs-text) is the + existing dark color in light mode, so this is a no-op there. The colorful .md-logo IMG is a + multi-color SVG and isn't affected by color/fill. */ +.md-header__title, +.md-header__topic, +.md-header__ellipsis, +.md-header__button.md-icon { + color: var(--cs-text) !important; +} +.md-header__button.md-icon svg { + fill: currentColor !important; +} +.md-header .md-search__icon, +.md-header .md-search__icon svg { + color: var(--cs-muted) !important; + fill: currentColor !important; +} + +/* Hide Material's header palette toggle — the theme switcher lives in the FOOTER (.cs-theme-toggle). + display:none keeps the radio inputs in the DOM so Material's palette JS still honors __palette + (which we set from the shared dstack-theme key) and applies the scheme flash-free on load. */ +.md-header__option, +[data-md-component="palette"] { + display: none !important; +} + +@media screen and (min-width: 76.1875em) { + .cs-nav-toggle { display: inline-flex; } +} + +@media screen and (max-width: 76.1875em) { + .cs-nav-toggle { display: none; } + + /* Tighten the header on mobile so the cluster fits: drop the icon buttons' (burger/search) + margins and shrink the gaps (search→GitHub and GitHub→Get started) to 5px. */ + .md-header__button.md-icon { + margin: 0 !important; + } + .md-header__inner .md-header__buttons { + margin-left: 5px; + } + .md-header__buttons .md-button.github { + margin-right: 5px; + } + + /* The theme-aware nav colors below are scoped to the desktop media query, so the MOBILE drawer + fell back to extra.css's black text — invisible on the dark drawer. Apply them here too. */ + .md-sidebar--primary .md-nav__title, + .md-sidebar--primary .md-nav__item--section > .md-nav__link, + .md-sidebar--primary .md-nav__item--section > .md-nav__link[for] { + color: var(--cs-nav-heading) !important; + } + .md-sidebar--primary .md-nav__link, + .md-sidebar--primary .md-nav__link .md-typeset { + color: var(--cs-nav-link) !important; + } + .md-sidebar--primary .md-nav__link--active, + .md-sidebar--primary .md-nav__link--active .md-typeset { + color: var(--cs-nav-active) !important; + } + /* Material gives the drawer's section titles a light (#f6f9fc) background that stayed light in + dark mode (the light text then sat on a near-white bar). Make it transparent so the titles + read on the dark drawer. */ + .md-sidebar--primary .md-nav__title { + background-color: transparent !important; + } + /* The repo "source" facts (dstackai/dstack · stars · forks) + its icon at the bottom of the + drawer were black (rgba(0,0,0,.87)) → invisible on the dark drawer. Make them theme-aware. */ + .md-sidebar--primary .md-nav__source, + .md-sidebar--primary .md-source__repository, + .md-sidebar--primary .md-source__facts, + .md-sidebar--primary .md-source__fact, + .md-sidebar--primary .md-source__icon, + .md-sidebar--primary .md-source__icon svg { + color: var(--cs-nav-link) !important; + fill: currentColor !important; + } +} + +/* Search box → 36px tall, 12px radius, 1px border (consistent with the sidebar + components). + color-primary prefix beats extra.css's rules. */ +[data-md-color-primary=white] .md-search__form, +[data-md-color-primary=white] .md-search__form:hover { + border-radius: 12px; + height: 36px; + border: 1px solid var(--cs-border); /* stays 1px on hover (extra.css thinned it to 0.5px) */ +} + +.md-search__input { + border-radius: 12px; + color: var(--cs-text); /* theme-aware so the typed text is visible in dark mode */ +} +/* Placeholder was `color: inherit` (extra.css) → invisible on the dark search surface. Use a + theme-aware muted token so it shows in both light and dark. */ +.md-search__input::placeholder { + color: var(--cs-muted) !important; + opacity: 1; +} + +/* Magnifying-glass icon → 18px and vertically centered (was 24px, top-aligned). */ +.md-search__form .md-search__icon { + width: 18px; + height: 18px; + top: 50%; + transform: translateY(-50%); +} +.md-search__form .md-search__icon svg { + width: 18px; + height: 18px; +} + +/* ===== Left sidebar (Cloudscape SideNavigation) ============================ + Spec measured from /old: 28px left / 24px right gutter, section titles 18/700, + links 14/400 with a 12px gap (margin, not padding), full-bleed section dividers, + a 1px right border, no chevron icons. */ +@media screen and (min-width: 76.1875em) { + .md-sidebar--primary { + width: 280px; + /* mkdocs default padding-top is 26px; /old's first section sits ~20px below the header + (6px here + the inner's 14px). Kill the default 1.2rem bottom padding too. */ + padding-top: 6px; + padding-bottom: 0; + /* Sticky viewport-height rail (Material default position:sticky, top:79). Sticky — not fixed — + so it tracks the header as the (non-sticky) banner scrolls away: it sits below the header + at the top, stays pinned for the whole article, and only eases away as the footer scrolls + in. The 280px column stays in the flex row (it's still in flow). */ + height: calc(100vh - 79px); + overflow: hidden; + } + + /* The visible section groups (Getting started, Concepts, …) live inside the active tab + item's (Docs) nested nav. Zero the wrapper padding so the gutter lives on each section + item — that makes the item box full-bleed, so its divider border runs edge-to-edge + while the content stays inset 28/24. */ + .md-sidebar--primary .md-nav--primary > .md-nav__list, + .md-sidebar--primary .md-nav--primary > .md-nav__list > .md-nav__item, + .md-sidebar--primary .md-nav__item--section > .md-nav { + padding: 0; + margin: 0; + } + /* /old aligns a section's direct items (leaf links AND expandable labels) flush with their + header at the gutter — NO indent. Only items nested under an expandable get indented (handled + below). So the section's child list stays at padding 0. */ + .md-sidebar--primary .md-nav__item--section > .md-nav > .md-nav__list { + padding: 0; + margin: 0; + } + .md-sidebar--primary .md-nav__item--section > .md-nav > .md-nav__list > .md-nav__item--section { + padding-left: 24px; + padding-right: 24px; + margin: 0; + } + /* Section lists carry mkdocs' overflow:hidden (collapse animation) and clip two things that must + hang in the gutter: the full-bleed section separators (::before, left:-4 → 280) and the + expandable carets (left:-18). Sections never collapse (navigation.sections), so overflow is + safe to release. Must be ALL sections, not just the active one — else carets only show under + the currently-active section. Neutralize the nested-nav slide-surface bg that visible exposes. */ + .md-sidebar--primary .md-nav__item--section > .md-nav > .md-nav__list { + overflow: visible !important; + } + .md-sidebar--primary .md-nav__item--section > .md-nav { + background: transparent !important; + box-shadow: none !important; + } + + /* Top spacing: override extra.css's -25px scrollwrap pull so content clears the header. + Also span the full 0→280 width (margin 0) with a 4px left padding to preserve the gutter — + otherwise the scrollwrap's box is 4→276 and its overflow-x (forced to auto by overflow-y: + hidden) CLIPS the full-bleed section separators, leaving a gap at both edges. */ + .md-sidebar--primary .md-sidebar__scrollwrap { + margin-top: 0; + margin-left: 0; + margin-right: 0; + padding-left: 4px; + /* Internal scroll (nav stays sticky; the page scroll doesn't move it). Drop extra.css's fade. */ + height: 100% !important; + max-height: none !important; + overflow-y: auto !important; + overscroll-behavior: contain; + /* Don't let the scrollbar gutter reserve space (which would clip the full-bleed separators + on the right); thin/overlay scrollbar instead. */ + scrollbar-width: thin; + -webkit-mask-image: none !important; + mask-image: none !important; + } + .md-sidebar--primary .md-sidebar__inner { + padding-top: 14px; + /* Containing block for the collapse "<" button, which now lives inside this (scrolling) + element so it scrolls out with the nav. */ + position: relative; + } + /* Nav scrollbar → neutral gray like /old (mkdocs colours it with the lilac accent by default). */ + .md-sidebar--primary .md-sidebar__scrollwrap { + scrollbar-color: rgba(0, 0, 0, 0.28) transparent; + } + .md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.28) !important; + border-radius: 4px; + } + + /* Remove the lifted-nav negative margin (was pulling the gutter left to ~20px). */ + .md-sidebar--primary .md-nav__item--section > .md-nav { + margin-left: 0 !important; + } + + /* Align links with their section title (kill the per-item 10px left margin). */ + .md-sidebar--primary .md-nav__item { + margin-left: 0 !important; + } + + /* Divider + 24px gap between section groups. The section item is inset (4px left from the + scrollwrap margin, ~19px right), so a border-top leaves a gap before the 280px vertical + divider. Draw the separator as a full-bleed line (0→280, like /old) via a pseudo-element + anchored to the sidebar edges (left:-4 cancels the scrollwrap margin; width 280 reaches the + divider). */ + .md-sidebar--primary .md-nav__item--section > .md-nav > .md-nav__list > .md-nav__item--section + .md-nav__item--section { + position: relative; + margin-top: 24px; + padding-top: 24px; + border-top: 0; + } + .md-sidebar--primary .md-nav__item--section > .md-nav > .md-nav__list > .md-nav__item--section + .md-nav__item--section::before { + content: ""; + position: absolute; + top: 0; + left: -4px; + width: 280px; + height: 1px; + background: var(--cs-border); + pointer-events: none; + } + + /* Remove mkdocs' per-item left guide border (the stray vertical line on nested items). */ + .md-sidebar--primary .md-nav__item, + .md-sidebar--primary .md-nav__item--nested { + border-left: 0 !important; + } + + /* Zero every nested indent so a section's links align with its title at the gutter. */ + .md-sidebar--primary .md-nav__item--section .md-nav__list { + padding-left: 0; + margin-left: 0; + } + .md-sidebar--primary .md-nav__item--section .md-nav__item { + margin-left: 0; + } + + /* Section title → 18/700 heading at the gutter, with a gap before its links. */ + .md-sidebar--primary .md-nav__item--section > .md-nav__link, + .md-sidebar--primary .md-nav__item--section > .md-nav__link[for] { + font-size: 18px; + font-weight: 700; + color: var(--cs-nav-heading); + left: 0; + padding: 0; + margin: 0 0 12px 0; + } + + /* Links → 14/400 muted, compact, 12px apart via margin (no padding box). */ + .md-sidebar--primary .md-nav__link { + font-size: 16px; + font-weight: 400; + color: var(--cs-nav-link); + line-height: 20px; + padding: 0; + margin: 0 0 10px 0; + } + /* The blog left-nav items wrap their text in an inner `.md-typeset` span which extra.css sizes to + 0.75rem (15px), overriding the link's 14px (docs items have no such span → 14px). Force the + span to 14px so blog items match docs. The "Blog" section title has no .md-typeset, so its + 18px rule is unaffected. Also zero the 0.75rem leak on the