diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/workspace.yml
@@ -0,0 +1,75 @@
+name: Workspace
+
+on:
+  push:
+    branches: [main]
+    tags: ["v*"]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  native:
+    name: Workspace (${{ matrix.os }})
+    timeout-minutes: 30
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, windows-latest, macos-latest]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v4
+      - name: Install software Vulkan renderer
+        if: runner.os == 'Linux'
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y mesa-vulkan-drivers libvulkan1
+          echo 'WGPU_BACKEND=vulkan' >> "$GITHUB_ENV"
+      - uses: dtolnay/rust-toolchain@stable
+        with:
+          components: rustfmt, clippy
+      - uses: Swatinem/rust-cache@v2
+        with:
+          key: workspace
+      - name: Prepare the Windows test driver
+        if: runner.os == 'Windows'
+        shell: powershell
+        run: |
+          $ErrorActionPreference = 'Stop'
+          Start-Service Spooler
+          Add-PrinterDriver -Name 'Generic / Text Only'
+      - run: cargo fmt --check
+      - run: cargo clippy --workspace --all-targets --locked -- -D warnings
+      - run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
+      - run: cargo test --workspace --locked
+        env:
+          CRAFT_PRINT_REQUIRE_QUEUE: ${{ runner.os == 'Windows' && '1' || '0' }}
+      - run: cargo test --workspace --all-features --locked
+        env:
+          CRAFT_PRINT_REQUIRE_QUEUE: ${{ runner.os == 'Windows' && '1' || '0' }}
+  msrv:
+    name: Rust 1.95 (Linux libraries)
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@1.95.0
+      - uses: Swatinem/rust-cache@v2
+        with:
+          key: workspace-msrv
+      - run: cargo check --workspace --lib --all-features --locked
+  wasm:
+    name: UI and settings WASM
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: wasm32-unknown-unknown
+      - uses: Swatinem/rust-cache@v2
+        with:
+          key: workspace-wasm
+      - run: cargo check -p craft-ui -p craft-settings --all-features --locked --target wasm32-unknown-unknown
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,65 @@
+# Changelog
+
+All crates share the workspace version. See [the versioning policy](docs/versioning.md).
+
+## v0.1.0 — 2026-10-12
+
+First coordinated Git release of craft-libs. Applications can pin a named baseline instead of
+following development commits. This is an initial 0.x release: compatibility is maintained
+within `0.1.x`; changes requiring consumer migration use a new minor version.
+
+### Included crates
+
+- **craft-atomic:** same-directory staged file replacement, exclusive creation, streaming
+  writes, transient rename retries and fault-injection support, extracted from PhotoCraft.
+- **craft-settings:** app-owned TOML settings with platform/portable paths, atomic saves,
+  backups, damaged-file recovery, schema migrations and preservation of unknown fields.
+- **craft-quit:** native macOS termination, optional egui close handling and an optional
+  application menu. Save decisions and prompts stay with the app.
+- **craft-print:** CUPS and native Windows PDF submission, printer discovery, copies, duplex,
+  colour and scaling options. Includes bounded PDF sheet handling and the pinned Windows
+  completion fix. PDF generation stays with the app.
+- **craft-ui:** egui icon buttons, tabs, splits, resize handles, toolbar overflow, transactional
+  docking, theme helpers and print-settings controls. Commands, content, localization and
+  application state stay with the consumer. Docking serialization is opt-in through `serde`.
+
+### Requirements and adoption
+
+- Rust **1.95+**, edition 2024. Public UI integrations use **egui 0.36**.
+- Use `git = "https://github.com/storytold/craft-libs", tag = "v0.1.0"` for each required crate;
+  use the same tag everywhere. The [README](README.md#use-a-released-version) has a complete
+  manifest example and a commit-pin alternative. Commit application lockfiles.
+- No automatic migration from an arbitrary earlier Git revision is implied. Compare the API
+  you used with this release and test affected workflows before changing pins.
+- For `craft-quit`, run the close hook last in `eframe::App::logic`, after the app's unsaved-work
+  decision. Finish asynchronous saves before requesting close; termination does not execute
+  code after `run_native`. See the crate's API documentation before adoption.
+- This release is distributed through Git only. No crates.io packages are published by it.
+
+### Platform coverage and known limits
+
+The release gate checks the workspace on Linux, macOS and Windows with default and all features,
+plus Linux minimum-compiler and UI/settings WASM checks. UI tests include offscreen visual
+regressions. Windows print tests exercise a temporary paused spooler queue; those checks do
+not establish physical printer fidelity or every application's native GUI behavior.
+
+- `craft-ui` supports native and WASM use. Font coverage, bidirectional text and screen-reader
+  behavior still depend on the application and upstream stack; see [#17](https://github.com/storytold/craft-libs/issues/17).
+- `craft-settings` persistence is native-only. WASM exposes settings traits, document helpers
+  and error types. Concurrent writers are best-effort, last-writer-wins; strict coordination
+  needs an application-owned lock.
+- `craft-atomic` does not preserve all filesystem metadata during replacement. Directory sync
+  is best effort; tests do not prove physical power-loss durability. See its README's contract.
+- `craft-quit`'s native behavior is macOS-specific; other platforms keep their normal close
+  path. Earlier hardware testing of the close hook is recorded in
+  [#3](https://github.com/storytold/craft-libs/issues/3); it does not qualify every consuming app.
+- `craft-print` requires CUPS command-line tools on Linux/macOS or the native Windows spooler.
+  Browser submission is unsupported. Windows accepts a bounded subset of print-ready PDFs;
+  unsupported inputs and driver paper/scaling mismatches return errors. Printing is synchronous,
+  so call it from a worker. Physical output needs hardware testing.
+- Windows printing retains a pinned Git dependency on the maintained `winprint` fork. Its
+  build dependencies require the license/advisory policy exceptions documented in
+  [craft-print's README](crates/print/README.md). Registry publication is deferred.
+
+Planned crates and open feature PRs are not part of this release. Existing commit pins remain
+valid, and consumers do not need to update simply because `main` changes.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -15,12 +15,41 @@ A Cargo workspace (edition 2024, resolver 3). Each library is a crate under `cra
 (`license.workspace = true`, and so on) and `[lints] workspace = true`. Register each crate in
 `[workspace.dependencies]` in the root `Cargo.toml`.
 
-Apps depend on a crate by git:
+## Use a released version
+
+The first coordinated release is **[v0.1.0](https://github.com/storytold/craft-libs/releases/tag/v0.1.0)**.
+It gives consumers a fixed baseline while development continues on `main`. All five crates
+share this version. They require Rust 1.95 or later; the UI integrations use egui 0.36.
+
+Add only the crates you need, using the same tag for every craft-libs dependency:
 
 ```toml
-craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit>" }
+[dependencies]
+craft-ui = { git = "https://github.com/storytold/craft-libs", tag = "v0.1.0" }
+craft-settings = { git = "https://github.com/storytold/craft-libs", tag = "v0.1.0" }
+# Optional, for egui close handling and the macOS application menu:
+craft-quit = { git = "https://github.com/storytold/craft-libs", tag = "v0.1.0", features = ["menu"] }
 ```
 
+`craft-atomic` and `craft-print` use the same dependency syntax. Enable `craft-ui`'s
+`serde` feature if you need to serialize its docking layout. These are Git releases;
+crates.io publication is deferred.
+
+Release tags are never moved or reused. For a commit pin, resolve the tag with
+`git ls-remote https://github.com/storytold/craft-libs 'refs/tags/v0.1.0^{}'` and replace
+`tag = "v0.1.0"` with `rev = "<full commit SHA>"` on every craft-libs dependency.
+Use either `tag` or `rev`, not both. An existing commit pin keeps working; switching to this
+release may require adapting to changes since that commit.
+
+Commit your application's `Cargo.lock` and build with `cargo build --locked` for reproducible
+Rust dependency resolution. A Git pin fixes craft-libs source, not every transitive dependency.
+A library's own lockfile does not constrain its downstream consumers. Upgrade deliberately:
+read the release notes, update all craft-libs pins together, refresh the lockfile with
+`cargo check`, and run your application's tests and affected workflows before committing.
+
+See the [release notes](CHANGELOG.md) for included functionality and known limits, and the
+[versioning policy](docs/versioning.md) for compatibility guarantees and release checks.
+
 ## Libraries
 
 - [`craft-atomic`](crates/atomic/README.md): Crash-safe atomic file writing and replacement with same-directory staging, directory fsync, Apple `F_FULLFSYNC` fallback, transient lock backoff, and fault injection for the Craft suite.
diff --git a/docs/versioning.md b/docs/versioning.md
new file mode 100644
--- /dev/null
+++ b/docs/versioning.md
@@ -0,0 +1,81 @@
+# Versioning and releases
+
+craft-libs releases give applications a fixed dependency baseline. Consumers decide when to
+upgrade; development on `main` is not a reason to refresh a working application's pins.
+
+## One workspace, one release
+
+All crates inherit `workspace.package.version` and ship together under an annotated `vX.Y.Z`
+Git tag. A release contains the crates listed in that tag's README, not proposed crates in
+open pull requests. A crate can keep the same implementation across several workspace releases.
+Use the same tag or commit for every craft-libs dependency in a consumer's dependency graph;
+mixing Git sources can create duplicate crate instances and incompatible Rust types.
+
+Tags identify reviewed commits and are never moved, deleted or reused to fix a release.
+Corrections get a new version. The release notes and policy are stored with the source so a
+consumer can read the contract that applied to its version. GitHub Releases carry the same notes.
+Publishing to crates.io will be a separate step; these tags do not imply registry availability.
+
+## Compatibility while versions are 0.x
+
+The initial release is `0.1.0`. APIs are still developing, but compatibility within a minor
+release line is intentional:
+
+- **Patch (`0.1.0` → `0.1.1`):** compatible fixes, documentation and additive APIs that preserve
+  existing callers. Keep public signatures, feature defaults, supported targets, minimum Rust
+  version and documented behavior compatible. Check that additions do not break exhaustive
+  matches, struct literals, trait implementations or other existing source patterns.
+- **Minor (`0.1.x` → `0.2.0`):** changes requiring consumer action. This includes removing or
+  changing public APIs, incompatible persisted representations, feature/default changes,
+  dropping a supported target, raising the minimum Rust version, or upgrading a dependency
+  such as egui when its exposed types become incompatible. Explain the before/after and migration.
+- **`1.0.0`:** a separate decision that the public contracts are ready for long-term stability.
+  After that, breaking changes require a major release; compatible additions use minor releases.
+
+Documented invalid input can be rejected more strictly in a patch to fix a defect or security
+issue. Call out any observable change. Do not use “0.x” or “bug fix” to silently break a valid,
+documented workflow. App-owned settings schemas and migration policy remain the app's
+responsibility; library releases do not automatically migrate consumer data.
+
+For public egui APIs in `0.1.x`, keep the egui 0.36 compatibility line. The minimum Rust version
+is 1.95. Compatible transitive updates still need validation and release-note coverage when
+observable. Cargo's version-range rules do not select newer Git tags: changing a release pin
+is always an explicit manifest edit.
+
+## Support and scope
+
+The newest minor line receives routine fixes. Important fixes to older lines are considered
+case by case; there is no promise of maintaining every 0.x line or an LTS schedule. Existing
+tags remain available so consumers can upgrade on their own schedule. Release notes identify
+known limitations; a tag is not a claim that every driver, OS version or consuming app was tested.
+
+Platform boundaries are crate-specific. See each crate's API/README and the release notes.
+In particular, native printing needs the OS spooler, settings persistence is native-only, and
+browser UI compatibility does not imply native services work in a browser. Consumers must test
+their own save/close flows, accessibility, rendering and printing where they use those services.
+
+## Preparing a release
+
+1. Choose a reviewed commit on `main`. Inspect open defects, changes since the previous tag,
+   crate metadata and any changed platform/dependency contracts. Update the shared version
+   and lockfile when needed; the first release already uses `0.1.0`.
+2. Add a dated `CHANGELOG.md` entry with the included crates, changes, compatibility requirements,
+   migration steps and known limits. Update the README's recommended pin. Keep fixes and
+   unrelated pending features out of the release preparation unless they block the baseline.
+3. Run formatting, workspace Clippy with warnings denied, and workspace tests, both default
+   and all features. The `Workspace` workflow runs these on Linux, macOS and Windows, requires
+   Windows spooler regression tests, checks the minimum compiler on Linux, and checks the
+   documented UI/settings WASM surface. All release jobs must pass for the candidate source.
+   Repository-local build commands must also follow the host's storage/allocation rules.
+4. Review consumer impact and existing platform evidence. Exercise affected native workflows
+   when behavior changes; unit tests and cross-compilation cannot establish GUI or physical
+   printer acceptance. Record limitations instead of extending the support claim beyond evidence.
+5. Merge the preparation PR and verify the checks for the actual release commit. Create an
+   annotated tag on that exact commit (`git tag -a vX.Y.Z FULL_SHA -m "craft-libs vX.Y.Z"`),
+   push only that tag, and create a GitHub Release using that version's changelog entry.
+   Tag-triggered CI should also pass. Do not publish crates as part of this Git-only release.
+6. Verify that the remote tag resolves to the intended commit, the release notes are visible,
+   and a small external consumer resolves and checks with the documented pin. Preserve the
+   source identity, check results and release URL. Never repair a published tag by moving it.
+
+The workflow validates source; it does not automatically tag or publish on every push.
