diff --git a/.gitignore b/.gitignore
index 80dd31e0a..aa986afda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,10 @@ deps/src/
# Build artifacts for creating documentation generated by the Documenter package
docs/build/
docs/site/
+# docs/src/public/ is populated by docs/make.jl (TOML snapshots + logo) for VitePress
+docs/src/public/
+# logo-dark.svg is generated from logo.svg by docs/make.jl
+docs/src/assets/logo-dark.svg
# --------------------
# Julia Environment Files
diff --git a/docs/make.jl b/docs/make.jl
index 67e85ea63..995c807e7 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -112,15 +112,32 @@ end
# ═══════════════════════════════════════════════════════════════════════════════
# Assets for reproducibility
# ═══════════════════════════════════════════════════════════════════════════════
+# Each TOML is copied twice: into `src/assets/` so Documenter's link checker
+# resolves the `assets/…` download links (no `warnonly` since #952), and into
+# `src/public/assets/` so VitePress actually serves the file — it does not bundle
+# `.toml`, and DocumenterVitepress's assets→public copy only handles logo/favicon.
+# Creating `src/public/` also makes DVP skip that logo copy, so the logo is staged
+# into `src/public/` here as well. `src/public/` is git-ignored (build output);
+# `src/assets/Manifest.toml` stays tracked (the reviewable snapshot).
mkpath(joinpath(@__DIR__, "src", "assets"))
-cp(
- joinpath(@__DIR__, "Manifest.toml"),
- joinpath(@__DIR__, "src", "assets", "Manifest.toml");
- force=true,
+mkpath(joinpath(@__DIR__, "src", "public", "assets"))
+for f in ("Manifest.toml", "Project.toml")
+ cp(joinpath(@__DIR__, f), joinpath(@__DIR__, "src", "assets", f); force=true)
+ cp(joinpath(@__DIR__, f), joinpath(@__DIR__, "src", "public", "assets", f); force=true)
+end
+
+# Logo: `logo.svg` is the tracked light artwork; `logo-dark.svg` is the same file with
+# the white backdrop swapped for the VitePress dark surface (#1b1b1f), Julia mark colours
+# unchanged — generated here (git-ignored) and Vite-bundled from `src/assets/` for the
+# home-page pair. `logo.svg` is also staged into `src/public/` for the navbar (DVP
+# fills `logo:` with the single `/logo.svg`; a `.dark` filter recolours it there).
+write(
+ joinpath(@__DIR__, "src", "assets", "logo-dark.svg"),
+ replace(read(joinpath(@__DIR__, "src", "assets", "logo.svg"), String), "#ffffff" => "#1b1b1f"),
)
cp(
- joinpath(@__DIR__, "Project.toml"),
- joinpath(@__DIR__, "src", "assets", "Project.toml");
+ joinpath(@__DIR__, "src", "assets", "logo.svg"),
+ joinpath(@__DIR__, "src", "public", "logo.svg");
force=true,
)
diff --git a/docs/src/.vitepress/theme/style.css b/docs/src/.vitepress/theme/style.css
index 0a33c33cf..a434faee3 100644
--- a/docs/src/.vitepress/theme/style.css
+++ b/docs/src/.vitepress/theme/style.css
@@ -414,3 +414,32 @@ mjx-container svg path {
flex: 1 1 100%;
}
}
+
+/* ============================================================================
+ Dark mode: navbar logo. The home page swaps an exact light/dark SVG pair, but
+ the navbar logo is a single `` (DocumenterVitepress fills `logo:` with one
+ src) at 24px, where the filter's ~10% desaturation is imperceptible. Same
+ `invert(0.894) hue-rotate(180deg)` recipe as the figures below.
+ ============================================================================ */
+.dark .VPNavBarTitle .VPImage {
+ filter: invert(0.894) hue-rotate(180deg);
+}
+
+/* ============================================================================
+ Dark mode: in-content images rendered on a white ground — figures that are
+ outputs of Julia `@example` / `@repl` blocks (Plots.jl and CairoMakie, both
+ forced to SVG in docs/make.jl) and the `examples/logo.md` hero. DocumenterVitepress
+ renders every *local* image (`` and captured plot output alike) with an
+ empty `alt`, so `img[alt=""]` catches them all; an image that must NOT be flipped
+ (a photo, a screenshot) is written as `` in a `@raw html` block —
+ the `rocket-def.png` pattern in modelling/with-ai.md — and keeps its real `alt`.
+ `invert(a)` maps white to `255·(1−a)`; a = 1 − 27/255 ≈ 0.894 lands it on the
+ VitePress dark surface (#1b1b1f). `hue-rotate(180deg)` leaves that neutral grey
+ untouched (its matrix rows sum to 1) and only rotates the coloured marks back
+ toward their hue — approximately; data colours come out ~10% flatter, an accepted
+ trade-off (see .reports/documentation-review-2026-09.md option A). The logo uses an
+ exact light/dark SVG pair instead (docs/make.jl, config.mts) and is not touched here.
+ ============================================================================ */
+.dark .vp-doc img[alt=""] {
+ filter: invert(0.894) hue-rotate(180deg);
+}
diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png
deleted file mode 100644
index 706dde78b..000000000
Binary files a/docs/src/assets/logo.png and /dev/null differ
diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg
new file mode 100644
index 000000000..e0ad3488d
--- /dev/null
+++ b/docs/src/assets/logo.svg
@@ -0,0 +1,113 @@
+
+
diff --git a/docs/src/examples/logo.md b/docs/src/examples/logo.md
index 49f9c7f3e..5f90192f0 100644
--- a/docs/src/examples/logo.md
+++ b/docs/src/examples/logo.md
@@ -4,7 +4,7 @@ The OptimalControl.jl logo is not a drawing. It is the solution of an optimal co
problem — an **energy-minimal low-thrust orbit transfer** — solved once and repeated by
symmetry.
-
+
A spacecraft spirals from a low circular orbit out to a higher one under continuous weak
thrust. Two-body dynamics do not care which way is up, so the same optimal transfer, rotated
@@ -168,7 +168,7 @@ scatter!(ax, Point2f(0, 0); color = jl.blue, markersize = 100)
fig
```
-That is the figure the site uses as its logo (`docs/src/assets/logo.png`).
+That is the figure the site uses as its logo (`docs/src/assets/logo.svg`).
The published asset is produced by a slightly more elaborate script — crisper strokes, a few
tuning knobs — kept in the repository at `.extras/logos/logo-gagnant/`. The optimal control
diff --git a/docs/src/index.md b/docs/src/index.md
index 02c216f58..60ecc7045 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -2,9 +2,37 @@
The OptimalControl.jl package is the root package of the [control-toolbox ecosystem](https://github.com/control-toolbox). The control-toolbox ecosystem gathers Julia packages for mathematical control and applications. It aims to provide tools to model and solve optimal control problems with ordinary differential equations by direct and indirect methods, both on CPU and GPU.
+```@raw html
+
+
+
+```
+
## Motivation
-The guiding philosophy of OptimalControl.jl is to offer, to our knowledge, the only Julia package that unifies both direct and indirect methods for optimal control within a single, coherent framework. This fills a gap in a landscape where existing tools are fragmented across programming languages and paradigms, and are usually restricted to a single family of methods. The package provides a domain-specific language that closely matches mathematical notation, together with multiple discretization schemes and shooting methods, and planned support for homotopy continuation methods. Its modeler–solver separation makes it agnostic to the underlying NLP modeling backend and optimization solver, and enables seamless execution on both CPU and GPU with minimal user intervention. Combined with an ecosystem of domain-specific applications, tutorials, and benchmarking tools, this design targets researchers and engineers working in optimal control, control theorists developing new algorithms, and students learning the field through interactive tutorials.
+To our knowledge, OptimalControl.jl is the only Julia package that unifies both direct
+and indirect methods for optimal control within a single, coherent framework. This fills
+a gap in a landscape where existing tools are fragmented across programming languages and
+paradigms, and are usually restricted to a single family of methods.
+
+The package provides a domain-specific language that closely matches mathematical
+notation, together with multiple discretization schemes and shooting methods, and planned
+support for homotopy continuation methods.
+
+Its modeler–solver separation makes it agnostic to the underlying NLP modeling backend
+and optimization solver, and enables seamless execution on both CPU and GPU with minimal
+user intervention.
+
+Combined with an ecosystem of domain-specific applications, tutorials, and benchmarking
+tools, this design targets researchers and engineers working in optimal control, control
+theorists developing new algorithms, and students learning the field through interactive
+tutorials.
## Installation
@@ -34,7 +62,9 @@ sol = solve(ocp)
plot(sol)
```
-- For more details, see the [energy minimisation example](@ref examples-double-integrator-energy).
+That is the whole program — model, solve, plot. Each step has its own guide:
+
+- For more details about the example, see the [energy minimisation page](@ref examples-double-integrator-energy).
- The `@def` macro defines the problem. See the [abstract syntax guide](@ref modelling-abstract-syntax).
- The `solve` function has many options. See the [solve overview](@ref solve-overview).
- The `plot` function is flexible. See the [plot guide](@ref results-plot).
@@ -49,15 +79,17 @@ plot(sol)
| [Results](@ref results-solution) | Read a `Solution` — trajectories, costate, duals, status — plot it, save it, reload it. |
| [Flows (indirect)](@ref flows-overview) | The Pontryagin Maximum Principle as code: build, integrate, and shoot with Hamiltonian flows. |
| [Geometry](@ref geometry-overview) | The Lie-theoretic tools (`Lift`, `ad`, `Poisson`, `@Lie`) behind singular-control problems. |
-| [Examples](@ref examples-gallery) | Six worked problems, direct and indirect, from energy minimisation to state constraints. |
+| [Examples](@ref examples-gallery) | A gallery of complete problems worked end to end, direct and indirect, from energy minimisation to state constraints. |
| [API reference](@ref api-modelling) | Every re-exported symbol, organised by theme. |
| [Migrating to v2.1](@ref migration) | What changed since v2.0 and how to update your code. |
## Mathematical formulation
-Optimal control problems are stated in Bolza form — a cost functional combining a terminal
-(Mayer) and an integral (Lagrange) term, subject to dynamics and box/path/boundary
-constraints, with optionally free times and extra optimisation variables. See
+Optimal control problems are stated in Bolza form — a cost functional combining a boundary
+term (Mayer), a pointwise cost evaluated on the initial and final times and states and on
+the optimisation variables, and an integral term (Lagrange) accumulated along the
+trajectory. The problem is subject to dynamics and box/path/boundary constraints, with
+optionally free times and extra optimisation variables. See
[Formulation](@ref modelling-formulation) for the full mathematical setting.
## Citing us
@@ -92,63 +124,57 @@ If you want to ask a question, feel free to start a discussion [here](https://gi
## Testing
-OptimalControl.jl is the umbrella package of a multi-repository ecosystem, and testing is organized in layers. Each sub-package (`CTBase`, `CTParser`, `CTModels`, `CTDirect`, `CTSolvers`, `CTFlows`) has its own test suite combining unit tests, integration tests, and code-quality checks (e.g. with [Aqua.jl](https://github.com/JuliaTesting/Aqua.jl)). At the umbrella level, OptimalControl.jl adds strong end-to-end integration tests that solve complete optimal control problems with both direct and indirect methods. Continuous integration runs on Linux, macOS, and Windows, on both CPU and GPU (via a self-hosted CUDA runner), through reusable workflows centralized in [CTActions](https://github.com/control-toolbox/CTActions). Code coverage is tracked on [Codecov](https://codecov.io), downstream packages are guarded against regressions through dedicated breakage tests, and beta versions are distributed during development via a local registry, [ct-registry](https://github.com/control-toolbox/ct-registry). Part of the test code is written with the help of AI agents, always under human review.
+OptimalControl.jl is the umbrella package of a multi-repository ecosystem, and testing is
+organized in layers. Each sub-package (`CTBase`, `CTDirect`, `CTFlows`, `CTModels`, `CTLie`,
+`CTSolvers`, `CTParser`) has its own test suite combining unit tests, integration
+tests, and code-quality checks (e.g. with [Aqua.jl](https://github.com/JuliaTesting/Aqua.jl)).
+At the umbrella level, OptimalControl.jl adds strong end-to-end integration tests that
+solve complete optimal control problems with both direct and indirect methods.
+
+Continuous integration runs on Linux, macOS, and Windows, on both CPU and GPU (via a
+self-hosted CUDA runner), through reusable workflows centralized in
+[CTActions](https://github.com/control-toolbox/CTActions). Code coverage is tracked on
+[Codecov](https://codecov.io), and downstream packages are guarded against regressions
+through dedicated breakage tests.
+
+Beta versions are distributed during development via a local registry,
+[ct-registry](https://github.com/control-toolbox/ct-registry). Part of the test code is
+written with the help of AI agents, always under human review.
## Reproducibility
```@setup main
using Pkg
using InteractiveUtils
-using Markdown
-
-# Download links for the benchmark environment
-function _downloads_toml(DIR)
- link_manifest = joinpath("assets", DIR, "Manifest.toml")
- link_project = joinpath("assets", DIR, "Project.toml")
- return Markdown.parse("""
- Download the exact environment used to build these docs:
- - 📦 [Project.toml]($link_project) - Package dependencies
- - 📋 [Manifest.toml]($link_manifest) - Full dependency tree
- """)
-end
```
-```@example main
-_downloads_toml(".") # hide
-```
+Every page on this site executes its code when the documentation is built, against a
+single pinned environment. You can inspect that environment below, or download it and
+rebuild it locally.
-```@raw html
-ℹ️ Version info
-```
+| Download | Contents |
+| --- | --- |
+| [`Project.toml`](assets/Project.toml) | the packages this documentation depends on directly |
+| [`Manifest.toml`](assets/Manifest.toml) | the exact version of every package in the resolved dependency tree |
+
+::: details Environment used to build this documentation
+
+**Julia version and operating system**
```@example main
versioninfo() # hide
```
-```@raw html
-
-```
-
-```@raw html
-📦 Package status
-```
+**Direct dependencies**
```@example main
Pkg.status() # hide
```
-```@raw html
-
-```
-
-```@raw html
-📚 Complete manifest
-```
+**Full dependency tree**
```@example main
Pkg.status(; mode = PKGMODE_MANIFEST) # hide
```
-```@raw html
-
-```
+:::