diff --git a/docs/concepts.md b/docs/concepts.md
new file mode 100755
index 0000000..cc6b905
--- /dev/null
+++ b/docs/concepts.md
@@ -0,0 +1,246 @@
+# Stack Concepts
+
+Most of this documentation explains how to *use* `stack`: which files to write, which
+commands to run, which flags to pass. This page explains the ideas underneath — what kind
+of thing a stack is, why the tool is shaped the way it is, and which problems it is
+deliberately choosing to solve (and not solve). If the reference pages are the map, this
+is the geography.
+
+## The missing tool
+
+A single program has enjoyed good tooling for decades. Its source is compiled by a build
+system that knows what depends on what. Its libraries come from a package manager that
+resolves versions and records them in a lock file. Installing it puts a known artifact in
+a known place, and the package manager can later upgrade, verify, or remove it. The
+program is treated, throughout, as *one thing* with well-defined inputs and a well-defined
+identity.
+
+A *system* of programs — a web front end, an API service, a database, perhaps a worker or
+two — has never had the equivalent. The pieces are individually well tooled, but the whole
+is customarily held together by convention and glue: a compose file here, some shell
+scripts there, a wiki page explaining which container tags go together, and a deployment
+procedure that lives partly in CI configuration and partly in someone's head. The system
+exists as a concept in its developers' minds, but nowhere is it an artifact that a tool
+can build, version, install, and manage.
+
+Stack's premise is that this is a tooling gap, not a fact of nature. The system deserves
+to be a concrete, first-class entity — something with a name, a definition, a build, a
+version, and an installed form — and the tool for it should feel like the build systems
+and package managers we already know. `stack fetch` and `stack prepare` are the build
+system. Lock files and image publishing are the package manager. `stack deploy` is the
+installer, and `stack manage` operates the installed result.
+
+## What a stack is made of
+
+A stack is described by a small set of declarative files, committed alongside (or near)
+the code they describe. The top of the hierarchy is `stack.yml`, which gives the system
+its name and enumerates its parts. Those parts fall into a handful of well-formed
+abstractions, and it is worth being precise about each.
+
+**Containers** are the units of code. Each entry in the `containers` list names an image
+and says where its source lives (`ref`, a git repository reference) and where its build
+recipe lives (`path`, plus optionally `content-root` to narrow what actually gets built).
+The recipe can be a `Dockerfile`, a `build.sh`, or a `container.yml` — and notably, the
+recipe and the source need not live in the same repository, which makes it practical to
+build customized images from upstream code you don't control. For common shapes of
+application there need be no recipe at all: a *wrapper* such as `static-content`,
+`nextjs`, or `node-service` supplies the entire containerization, so a repository of
+pages or a Next.js app becomes deployable without containing a single Docker artifact.
+
+**Pods** are the units of composition. A pod groups containers that deploy together and
+is described by a `composefile.yml` — deliberately compatible with `docker-compose.yml`
+syntax, because that format is widely understood and already expresses the right things:
+services, volumes, environment, ports. A pod may also declare `pre_start_command` and
+`post_start_command` hooks for initialization that belongs to the system rather than to
+any one container.
+
+**Configuration** is layered with defined precedence: values supplied at deployment time
+in `config.env` override `env_file:` entries, which override a service's inline
+`environment:` block. This is the same order Docker Compose uses, and Stack applies it
+identically on Kubernetes, so a pod file hands its containers the same values whichever
+target it lands on.
+
+**Secrets** are configuration whose values must never appear in the committed files or
+the deployment artifacts. A stack declares that a secret *exists*; the value is injected
+at deploy time — generated automatically by default, or supplied by reference when the
+secret's counterpart lives outside the deployment (an SMTP password, say).
+
+**Data** lives in named volumes declared in the pod files, and the deployment machinery
+knows about it: backing up and restoring state is a `stack manage` operation, not an
+exercise left to the reader.
+
+**Connectivity** inside a deployment follows one simple rule: every service can reach
+every other service by its service name, across pod boundaries, on every deployment
+target. On Kubernetes each deployment gets its own namespace so the unqualified names
+resolve; on Compose the network does the same job. Code never needs to know which world
+it is running in.
+
+The point of enumerating these is not the individual features but the claim they add up
+to: components, code, config, data, and connectivity are each *somewhere definite* in the
+model. When you wonder "where does X belong?", there is an answer, and it is the same
+answer for every stack.
+
+## Definition, specification, deployment
+
+Stack keeps three layers strictly apart, and most of its ergonomics flow from this
+separation.
+
+The **definition** is the stack itself — `stack.yml`, the pod files, the container
+recipes. It is committed to a repository, it is portable, and it says nothing about where
+the system will run. This is the analogue of source code plus its build description.
+
+The **specification** is what `stack init` produces: the definition combined with a set
+of choices for one intended deployment — the target (`--deploy-to compose` or `k8s`),
+port mappings, hostnames, an image registry, configuration values. It is a single YAML
+file you can inspect, edit, commit, and reuse. This is the analogue of a build
+configuration: same source, different `./configure` flags.
+
+The **deployment** is what `stack deploy` creates from a specification: a self-contained
+directory holding everything the running instance needs — its copies of the pod files,
+its configuration, its data volume locations. From then on the deployment directory *is*
+the installed system, and `stack manage --dir
` starts it, stops it, shows its
+status and logs, executes commands inside its services, and backs it up. Two deployments
+of the same stack are simply two directories; they don't share mutable state and can't
+interfere with each other.
+
+```mermaid
+flowchart LR
+ subgraph DEF["Definition (committed, portable)"]
+ SY["stack.yml
pod files
container recipes"]
+ end
+ subgraph SPEC["Specification (one intended deployment)"]
+ SP["spec file
target, ports, hostnames,
registry, config"]
+ end
+ subgraph DEP["Deployment (the installed system)"]
+ DD["deployment directory
pod files, config, data"]
+ end
+ SY -- "stack init" --> SP
+ SP -- "stack deploy" --> DD
+ DD -- "stack manage
start · stop · status · logs
exec · backup" --> DD
+```
+
+The payoff of the layering is that moving a system between worlds never touches the
+definition. The [README quick start](../README.md) deploys the same todo stack to local
+Docker and then to a Kubernetes cluster, and the diff between the two is confined to the
+`stack init` invocation. Which brings us to the next idea.
+
+## The place to deploy is an abstraction
+
+"Where the system runs" is, in Stack's model, a *parameter* — chosen at init time, not
+woven through the definition. Today the parameter takes three values: Docker/Podman
+Compose (the development loop, and equally a small production server), Kubernetes (real
+clusters, when scale or multi-tenancy genuinely calls for one), and `k8s-kind`
+(Kubernetes-in-Docker, for developing the k8s shape of a deployment without renting a
+cluster).
+
+```mermaid
+flowchart TD
+ DEF["one stack definition"]
+ DEF -- "init: --deploy-to compose
--map-ports-to-host" --> LOCAL["laptop
Docker / Podman Compose"]
+ DEF -- "same, plus a
reverse proxy" --> VM["rented VM
Compose + public hostname"]
+ DEF -- "init: --deploy-to k8s
--image-registry, --http-proxy-fqdn" --> K8S["Kubernetes cluster"]
+ DEF -- "init: --deploy-to k8s-kind" --> KIND["k8s-kind
rehearse the k8s shape locally"]
+```
+
+Abstracting the target is only honest if the semantics hold steady across it, so Stack
+works to make them hold: service-name resolution behaves identically, environment
+precedence behaves identically, the manage lifecycle (start, stop, status, logs, exec,
+backup) is the same set of verbs everywhere. What changes per target is confined to
+things that genuinely differ — a Kubernetes deployment needs an image registry to pull
+from and a public hostname for its ingress, so those appear as init flags and a
+`push-images` step, and nothing else changes.
+
+One consequence deserves emphasis, because it cuts against the prevailing current: this
+design makes ordinary infrastructure sufficient. A definition that deploys unchanged to a
+laptop, a rented VM, or a cluster means the decision between them is reversible, and the
+sensible default for a system with real users is often the humblest option — one small VM
+running the Compose target, described at more length in
+[From Laptop to Production](./from-laptop-to-production.md). Stack is in this sense the
+PaaS-free alternative: the "define once, deploy with one command" convenience, without
+the pricing curve, the platform's opinions about what you may run, or the proprietary
+configuration that makes leaving expensive.
+
+## Versioning without the tag maze
+
+Container tags are the weakest link in most multi-container workflows. They are mutable,
+they are invented by hand or by CI convention, and nothing ties a tag to the source that
+produced the image. Multiply by several images per system and you get the familiar maze:
+which `api:2024-11-eks-fix2` goes with which `frontend:latest`?
+
+Stack removes the maze by refusing to let humans (or CI scripts) make up image identities
+at all. An image's identity *is* the commit hash of its recipe repository — the repo
+carrying its `container.yml`, or failing that the repo carrying the `stack.yml` that
+declares it. In the common case where recipe and source are the same repository, the
+image identity is simply that repo's commit hash: check out a commit, and the images that
+belong with it are fully determined.
+
+Where the inputs span repositories — a recipe building someone else's source, or a
+wrapper supplying the containerization — lock files (`container.lock`, `stack.lock`)
+committed in the recipe repo pin the other repositories' commits, so that a recipe commit
+still fully determines image content, exactly as a `Cargo.lock` or `package-lock.json`
+makes a dependency tree reproducible. Third-party images named in pod files (`postgres:14`
+and the like) are pinned too, by manifest digest, recorded at prepare time and applied at
+deploy time; an image can opt out with an explicit `# @stack unpinned` annotation, making
+floating versions a visible, deliberate choice rather than the silent default.
+
+And when the tree is dirty — uncommitted changes, including a not-yet-committed lock
+file — the build gets a synthetic `stackdev-` tag that is never published and never
+matched remotely. The invariant this protects is worth stating plainly: **any image
+identity that can circulate corresponds to committed code.** Committing is what
+stabilizes identity, in containers just as in source control.
+
+The package-manager half of the analogy follows directly. Because identities are
+deterministic, prepared images can be published to a registry and later *matched* instead
+of rebuilt — a colleague (or a production host) that fetches the same commits can pull
+the same images, byte for byte, the way a package manager pulls a prebuilt artifact
+rather than compiling from source. The developer never names a tag in any of this; tags
+have become an implementation detail of the tool, which is where they belong.
+
+## Batteries included, least surprise, low weeds
+
+Three phrases recur when we explain Stack's design temperament, and they are meant
+seriously enough to define here.
+
+**Batteries included** means the normally encountered needs of a web system are covered
+in the box, not left as integration exercises: building images (or wrapping repos that
+have none), secrets generation, cross-service networking, ingress and HTTPS on public
+targets, backup and restore, log access, and the integrity tooling — `stack validate` to
+check that the stack's files agree with each other, `stack check` to dry-run a prepare,
+`stack chart` to draw the system. You should be able to go from a cloned repository to a
+running, reachable, backed-up system without leaving the tool.
+
+**Least surprise** means behavior transfers. The compose syntax is the one you already
+know. Environment precedence is the one Docker Compose already defined. What works
+locally works the same way on a VM and the same way on a cluster, because a promise of
+target abstraction is only as good as the uniformity behind it. Defaults are chosen so
+that the naive first command does the reasonable thing, and deviations from defaults
+(an unpinned image, an external secret) are explicit marks in the files rather than
+ambient state.
+
+**Low weeds** means the tool keeps you at the level of the system. You reason about
+stacks, containers, pods, and deployments; you do not hand-manage image tags, wire up
+per-target networking, or template YAML for two orchestrators. The weeds still exist —
+they always do — but visiting them becomes optional. And when you need to: every
+abstraction here has an escape hatch. A `build.sh` may do anything a shell can do; a pod
+file is real compose syntax, not a subset; hooks run arbitrary commands at defined
+moments; and a deployment directory is ordinary files you can read.
+
+## What Stack is not
+
+A concepts page should be honest about boundaries. Stack does not aim to address all
+aspects of application development and hosting, and saying so is part of the design.
+
+It is not a PaaS: there is no hosted control plane, no billing meter, and nobody carrying
+the pager for you. It is not an orchestrator: scheduling and supervising containers is
+Docker's and Kubernetes' job, and Stack drives them rather than replacing them. It is not
+a CI system, an infrastructure provisioner (companion tooling exists for creating hosts,
+but it is deliberately separate), or a monitoring suite. And it will not cover every
+exotic topology — the aim is a decent proportion of all normally encountered systems,
+handled well, rather than every conceivable system handled via configuration sprawl.
+
+What it is, is the tool that makes the *system* the unit of thought: one definition, built
+reproducibly, versioned without ceremony, deployable to an abstracted "somewhere" with
+one command, and manageable afterward as the single entity it always conceptually was.
+The rest of the documentation shows how; start with the
+[README quick start](../README.md), then [stack-files.md](./stack-files.md) for the file
+formats and [commands.md](./commands.md) for the verbs.