Add the illustrated user guide

A LaTeX field guide covering install, keys, everyday commands, the
decoy passphrase, scripting, locking and sync, internals, troubleshooting
and a cheat sheet. build-pdf.sh compiles it with a pinned Tectonic,
downloaded on first use. shots.py regenerates every screenshot from the
real vault binary in a sandbox home.
This commit is contained in:
Andy Dixon
2026-09-26 15:01:35 +01:00
parent 5aa60d7bc7
commit 81dc29097f
23 changed files with 1896 additions and 0 deletions
+4
View File
@@ -2,3 +2,7 @@
/vault.exe /vault.exe
.claude/ .claude/
/dist /dist
/docs/guide/.tools/
/docs/guide/version.tex
/docs/guide/vault-guide.pdf
/docs/guide/*.log
+2
View File
@@ -17,6 +17,8 @@ root that builds a single `vault` binary. Module `github.com/andydixon/vault`.
- `lock*.go` - vault lock on `DIR/.lock`: fcntl on unix, LockFileEx on - `lock*.go` - vault lock on `DIR/.lock`: fcntl on unix, LockFileEx on
Windows, an exclusive lock file elsewhere. Writers lock exclusively, Windows, an exclusive lock file elsewhere. Writers lock exclusively,
readers shared, taken after the passphrase prompt. readers shared, taken after the passphrase prompt.
- `docs/guide` - LaTeX user guide (`build-pdf.sh` via Tectonic; `shots.py`
re-shoots screenshots from the real binary). Keep it in step with the CLI.
- `build.sh` - cross-compiles every `go tool dist list` target except - `build.sh` - cross-compiles every `go tool dist list` target except
iOS and android/386, amd64, arm (cgo-only). iOS and android/386, amd64, arm (cgo-only).
- `internal/pqcrypt` - file encryption (header + STREAM ChaCha20-Poly1305), - `internal/pqcrypt` - file encryption (header + STREAM ChaCha20-Poly1305),
+6
View File
@@ -21,6 +21,12 @@ go test ./...
both armv6 and armv7, and writes `dist/SHA256SUMS`. Set `TARGETS` to build a both armv6 and armv7, and writes `dist/SHA256SUMS`. Set `TARGETS` to build a
subset, for example `TARGETS="linux/amd64 darwin/arm64" ./build.sh`. subset, for example `TARGETS="linux/amd64 darwin/arm64" ./build.sh`.
## User guide
A full illustrated guide lives in `docs/guide`. Build the PDF with
`docs/guide/build-pdf.sh`, which fetches Tectonic if it isn't installed.
`docs/guide/shots.py` regenerates its screenshots from the real binary.
## Use ## Use
```sh ```sh
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Build the vault user guide PDF: docs/guide/vault-guide.pdf
#
# ./build-pdf.sh
#
# Uses Tectonic (a self-contained XeTeX engine). If `tectonic` is not on PATH,
# a pinned release is downloaded into docs/guide/.tools on first run. LaTeX
# packages and fonts are fetched by Tectonic and cached in your user cache.
# Screenshots are committed in img/; regenerate them with ./shots.py.
set -euo pipefail
cd "$(dirname "$0")"
TECTONIC_VERSION=0.17.0
find_tectonic() {
if command -v tectonic >/dev/null; then command -v tectonic; return; fi
local local_bin=.tools/tectonic
if [[ -x $local_bin ]]; then echo "$local_bin"; return; fi
local asset
case "$(uname -s)/$(uname -m)" in
Linux/x86_64) asset=x86_64-unknown-linux-musl ;;
Linux/aarch64 | Linux/arm64) asset=aarch64-unknown-linux-musl ;;
Linux/armv7l) asset=arm-unknown-linux-musleabihf ;;
Darwin/x86_64) asset=x86_64-apple-darwin ;;
Darwin/arm64) asset=aarch64-apple-darwin ;;
*) echo "build-pdf: no Tectonic download for $(uname -s)/$(uname -m); install tectonic and retry" >&2; exit 1 ;;
esac
echo "build-pdf: fetching tectonic $TECTONIC_VERSION ($asset)" >&2
mkdir -p .tools
curl -fsSL "https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%40$TECTONIC_VERSION/tectonic-$TECTONIC_VERSION-$asset.tar.gz" |
tar -xz -C .tools tectonic
echo "$local_bin"
}
tectonic=$(find_tectonic)
# The cover shows the git version and today's date.
version=$(git describe --tags --always --dirty 2>/dev/null || echo dev)
printf '\\newcommand\\vaultversion{%s}\n\\newcommand\\vaultdate{%s}\n' \
"$version" "$(date '+%B %Y')" >version.tex
"$tectonic" --keep-logs --chatter minimal vault-guide.tex
echo "build-pdf: wrote $(pwd)/vault-guide.pdf"
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+331
View File
@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""Regenerate the user guide's terminal screenshots from real vault runs.
Every screenshot is the genuine output of the vault binary built from this
checkout, run in a throwaway home directory. The cosmetic touches are: the
sandbox path and file owner are shown as /home/sam and "sam sam", key lines
are highlighted in amber for the reader, and "moment" frames show a prompt
with a cursor, exactly as vault prints it before you type.
Needs: go, python3, and Chrome or Chromium (set CHROME=/path if not found).
Output: docs/guide/img/*.png
"""
import getpass
import glob
import grp
import html
import os
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
import zipfile
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.abspath(os.path.join(HERE, "..", ".."))
IMG = os.path.join(HERE, "img")
TOOLS = os.path.join(HERE, ".tools")
FONT_URL = "https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip"
SHOWN_HOME = "/home/sam"
PROMPT_USER = "sam@laptop"
def find_chrome():
if os.environ.get("CHROME"):
return os.environ["CHROME"]
for name in ("chromium", "chromium-browser", "google-chrome", "google-chrome-stable"):
if shutil.which(name):
return shutil.which(name)
found = sorted(glob.glob(os.path.expanduser("~/.cache/ms-playwright/chromium-*/chrome-linux*/chrome")))
mac = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
if found:
return found[-1]
if os.path.exists(mac):
return mac
sys.exit("shots: no Chrome or Chromium found; set CHROME=/path/to/chrome")
def font_dir():
target = os.path.join(TOOLS, "jetbrains-mono")
if not os.path.exists(os.path.join(target, "JetBrainsMono-Regular.ttf")):
os.makedirs(target, exist_ok=True)
zpath = os.path.join(TOOLS, "jbm.zip")
print("shots: fetching JetBrains Mono")
urllib.request.urlretrieve(FONT_URL, zpath)
with zipfile.ZipFile(zpath) as z:
for n in z.namelist():
base = os.path.basename(n)
if n.startswith("fonts/ttf/") and base in ("JetBrainsMono-Regular.ttf", "JetBrainsMono-Bold.ttf"):
with open(os.path.join(target, base), "wb") as f:
f.write(z.read(n))
os.remove(zpath)
return target
class Sandbox:
"""A fake home with vault on PATH. Commands run through bash."""
def __init__(self):
self.root = tempfile.mkdtemp(prefix="vault-shots-")
self.home = os.path.join(self.root, "sam")
self.bin = os.path.join(self.root, "bin")
os.makedirs(self.home)
os.makedirs(self.bin)
subprocess.run(["go", "build", "-o", os.path.join(self.bin, "vault"), "."], cwd=REPO, check=True)
def env(self, extra=None):
e = {
"HOME": self.home,
"PATH": self.bin + os.pathsep + os.environ["PATH"],
"LANG": "C.UTF-8",
"TZ": "Europe/London",
}
e.update(extra or {})
return e
def run(self, command, passphrase=None, extra_env=None):
env = self.env(extra_env)
if passphrase is not None:
env["VAULT_PASSPHRASE"] = passphrase
p = subprocess.run(["bash", "-c", command], cwd=self.home, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
return self.tidy(p.stdout)
def tidy(self, text):
owner = " %s %s " % (getpass.getuser(), grp.getgrgid(os.getgid()).gr_name)
return text.replace(self.home, SHOWN_HOME).replace(owner, " sam sam ").rstrip("\n")
def write(self, rel, content):
path = os.path.join(self.home, rel)
os.makedirs(os.path.dirname(path), exist_ok=True)
mode = "wb" if isinstance(content, bytes) else "w"
with open(path, mode) as f:
f.write(content)
# ---- rendering -------------------------------------------------------------
CSS = """
@font-face { font-family: JBM; src: url('file://%(fonts)s/JetBrainsMono-Regular.ttf'); }
@font-face { font-family: JBM; font-weight: bold; src: url('file://%(fonts)s/JetBrainsMono-Bold.ttf'); }
html, body { margin: 0; background: transparent; }
body { padding: 22px 48px 64px; display: flex; gap: 28px; align-items: flex-start; }
.win { width: %(width)dpx; border-radius: 12px; overflow: hidden; background: #0d1326;
box-shadow: 0 18px 40px rgba(6, 10, 30, .45), 0 0 0 1px rgba(255,255,255,.06); }
.bar { height: 34px; background: linear-gradient(#1b2340, #151c35); display: flex;
align-items: center; padding: 0 14px; position: relative; }
.dot { width: 12px; height: 12px; border-radius: 50%%; margin-right: 8px; }
.r { background: #ff5f57; } .y { background: #febc2e; } .g { background: #28c840; }
.title { position: absolute; left: 0; right: 0; text-align: center; color: #8b95b7;
font: 12.5px JBM, monospace; pointer-events: none; }
pre { margin: 0; padding: 16px 20px 18px; font: 14px/21px JBM, monospace; color: #d6ddf2;
white-space: pre; }
.u { color: #3ee0c5; font-weight: bold; } .p { color: #7aa2ff; font-weight: bold; }
.c { color: #ffffff; font-weight: bold; } .dim { color: #6f7a9c; }
.cur { background: #3ee0c5; color: #0d1326; }
.hl { color: #ffcc66; }
"""
def prompt(cwd="~"):
return ('<span class="u">%s</span> <span class="p">%s</span> <span class="dim">$</span> '
% (PROMPT_USER, html.escape(cwd)))
def window(lines, title, width):
body = "\n".join(lines)
return ('<div class="win"><div class="bar"><span class="dot r"></span><span class="dot y"></span>'
'<span class="dot g"></span><div class="title">%s</div></div><pre>%s</pre></div>'
% (html.escape(title), body))
class Term:
"""Accumulates one terminal window's contents."""
def __init__(self, box, title="sam@laptop: ~", width=860):
self.box, self.title, self.width, self.lines = box, title, width, []
def cmd(self, command, passphrase=None, extra_env=None, shown=None, cwd="~", highlight=()):
self.lines.append(prompt(cwd) + '<span class="c">%s</span>' % html.escape(shown or command))
out = self.box.run(command, passphrase, extra_env)
for line in out.split("\n") if out else []:
esc = html.escape(line)
if any(h in line for h in highlight):
esc = '<span class="hl">%s</span>' % esc
self.lines.append(esc)
return out
def moment(self, command, prompt_text, cwd="~"):
"""A frame captured while vault waits for typed input (no echo)."""
self.lines.append(prompt(cwd) + '<span class="c">%s</span>' % html.escape(command))
self.lines.append(html.escape(prompt_text) + ' <span class="cur"> </span>')
def raw(self, text):
self.lines.append(text)
def idle(self, cwd="~"):
self.lines.append(prompt(cwd) + '<span class="cur"> </span>')
def html(self):
return window(self.lines, self.title, self.width)
def height(self):
return 34 + 16 + 18 + 21 * max(1, len(self.lines))
def shoot(name, *terms):
chrome = find_chrome()
fonts = font_dir()
total_w = sum(t.width for t in terms) + 28 * (len(terms) - 1) + 96
total_h = max(t.height() for t in terms) + 22 + 64
page = "<!doctype html><meta charset=utf-8><style>%s</style><body>%s</body>" % (
CSS % {"fonts": fonts, "width": terms[0].width}, "".join(t.html() for t in terms))
if len({t.width for t in terms}) > 1:
sys.exit("shots: side-by-side windows must share a width")
with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) as f:
f.write(page)
out = os.path.join(IMG, name + ".png")
subprocess.run([chrome, "--headless=new", "--disable-gpu", "--hide-scrollbars", "--no-sandbox",
"--force-device-scale-factor=2", "--default-background-color=00000000",
"--window-size=%d,%d" % (total_w, total_h), "--screenshot=" + out,
"file://" + f.name], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
os.remove(f.name)
print("shots: wrote img/%s.png" % name)
# ---- scenes ----------------------------------------------------------------
def main():
os.makedirs(IMG, exist_ok=True)
box = Sandbox()
P = "correct horse battery staple"
D = "shopping list 42"
box.write("Documents/tax-return-2025.pdf", os.urandom(183_402))
box.write("Documents/passport-scan.jpg", os.urandom(1_204_551))
box.write("Documents/letters/landlord.txt", "Dear landlord,\n")
box.write("Documents/letters/bank.txt", "Dear bank,\n")
box.write("notes.md", "# ideas\n")
box.write("recipes/pancakes.txt", "flour, eggs, milk\n")
box.write("recipes/curry.txt", "spices\n")
t = Term(box)
t.cmd("vault help")
shoot("help", t)
t = Term(box)
t.moment("vault genkey", "Enter passphrase:")
shoot("genkey-prompt", t)
t = Term(box)
t.moment("vault genkey", "Enter decoy passphrase (empty for none):")
shoot("genkey-decoy-prompt", t)
t = Term(box)
t.cmd("vault genkey", passphrase=P, extra_env={"VAULT_DECOY_PASSPHRASE": D})
t.cmd("ls -l ~/.vault/")
shoot("genkey", t)
t = Term(box)
t.cmd("vault init")
t.cmd("ls -A ~/.vault/store/")
t.cmd("ls ~/.vault/store/records | wc -l")
shoot("init", t)
t = Term(box)
t.cmd("vault add Documents/tax-return-2025.pdf", P)
t.cmd("vault add Documents/passport-scan.jpg id", P)
t.cmd("vault add Documents/letters", P)
t.cmd("vault add notes.md", P)
shoot("add", t)
t = Term(box)
t.cmd("vault list", P)
t.cmd("vault list letters", P)
shoot("list", t)
t = Term(box)
t.cmd("mkdir -p restored && cd restored && vault get id/passport-scan.jpg", P)
t.cmd("vault get tax-return-2025.pdf -o restored/tax.pdf", P)
t.cmd("vault get notes.md", P, highlight=("already exists",))
shoot("get", t)
t = Term(box)
t.cmd("echo '# more ideas' >> notes.md && vault add notes.md", P, highlight=("replaced",))
t.cmd("vault mv notes.md ideas/notes.md", P)
t.cmd("vault rm letters/bank.txt", P)
t.cmd("vault list", P)
shoot("mv-rm", t)
t = Term(box, title="sam@laptop: ~/.vault/store")
t.cmd("cd .vault/store && ls -l records | head -5", cwd="~/.vault/store", shown="ls -l records | head -5")
shoot("on-disk", t)
# decoy: fill the decoy side, then show both views side by side
box.run("vault add recipes", D)
box.run("vault add notes.md shopping", D)
real = Term(box, title="real passphrase", width=600)
real.cmd("vault list", P)
decoy = Term(box, title="decoy passphrase", width=600)
decoy.cmd("vault list", D)
shoot("decoy-views", real, decoy)
t = Term(box)
t.cmd("vault get ideas/notes.md -o /tmp/x.md", D, highlight=("not in the vault",))
t.cmd("ls ~/.vault/store/records | wc -l")
shoot("decoy-hidden", t)
# errors
t = Term(box)
t.cmd("vault list", "hunter2", highlight=("incorrect",))
t.cmd("vault get does-not-exist.txt", P, highlight=("not in",))
t.cmd("vault add notes.md ../escape", P, highlight=("invalid",))
t.cmd("vault list --vault ~/Desktop", P, highlight=("not a vault",))
t.cmd("vault init", highlight=("already",))
shoot("errors", t)
# locking: start a big add, then a second writer while it runs
box.write("big-video.mkv", os.urandom(600 << 20))
first = Term(box, title="terminal 1", width=600)
second = Term(box, title="terminal 2", width=600)
env = box.env({"VAULT_PASSPHRASE": P})
a = subprocess.Popen(["bash", "-c", "vault add big-video.mkv"], cwd=box.home, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
time.sleep(1.5)
second.cmd("vault add recipes/curry.txt", P, highlight=("waiting",))
first.lines.append(prompt() + '<span class="c">vault add big-video.mkv</span>')
first.lines += [html.escape(box.tidy(l)) for l in a.communicate()[0].rstrip("\n").split("\n")]
shoot("locking", first, second)
os.remove(os.path.join(box.home, "big-video.mkv"))
# automation
t = Term(box)
t.cmd("export VAULT_PASSPHRASE='correct horse battery staple'", shown="export VAULT_PASSPHRASE=…")
t.cmd("vault list ideas", P)
t.cmd("vault get nope 2>/dev/null; echo \"exit code: $?\"", P)
shoot("scripting", t)
# building, into a temporary OUT so an existing dist/ is left alone
rel = tempfile.mkdtemp(prefix="vault-release-")
targets = "linux/amd64 linux/arm darwin/arm64 windows/amd64"
t = Term(box, title="sam@laptop: ~/src/vault")
out = subprocess.run(["./build.sh"], cwd=REPO, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=dict(os.environ, TARGETS=targets, OUT=rel)).stdout
sums = subprocess.run(["sha256sum", "-c", "SHA256SUMS"], cwd=rel,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
t.lines.append(prompt("~/src/vault") + '<span class="c">export TARGETS="%s"</span>' % targets)
t.lines.append(prompt("~/src/vault") + '<span class="c">OUT=/tmp/release ./build.sh</span>')
t.lines += [html.escape(l.replace(rel, "/tmp/release")) for l in out.rstrip("\n").split("\n")]
t.lines.append(prompt("~/src/vault") + '<span class="c">cd /tmp/release &amp;&amp; sha256sum -c SHA256SUMS</span>')
t.lines += [html.escape(l) for l in sums.rstrip("\n").split("\n")]
shoot("build", t)
shutil.rmtree(rel, ignore_errors=True)
shutil.rmtree(box.root, ignore_errors=True)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+293
View File
@@ -0,0 +1,293 @@
% vaultguide.sty — the look of the vault field guide.
% Compiled with Tectonic (XeTeX). All fonts come from the TeX bundle.
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{vaultguide}
\PassOptionsToPackage{table}{xcolor}
\RequirePackage[a4paper,top=27mm,bottom=26mm,left=26mm,right=26mm,
headheight=16pt,headsep=9mm,footskip=13mm]{geometry}
\RequirePackage{fontspec}
\RequirePackage{xcolor}
\RequirePackage{tikz}
\usetikzlibrary{calc,positioning,arrows.meta,shadows.blur,backgrounds,fit,shapes.misc}
\RequirePackage[most]{tcolorbox}
\RequirePackage{fontawesome5}
\RequirePackage{graphicx}
\RequirePackage{titlesec}
\RequirePackage{titletoc}
\RequirePackage{fancyhdr}
\RequirePackage{enumitem}
\RequirePackage{booktabs,tabularx,array}
\RequirePackage{listings}
\RequirePackage{xstring}
\RequirePackage{microtype}
\RequirePackage[hidelinks]{hyperref}
\RequirePackage{bookmark}
% ---------------------------------------------------------------- colours
\definecolor{vnavy}{HTML}{0B1020}
\definecolor{vink}{HTML}{1A2244}
\definecolor{vteal}{HTML}{19C3A6}
\definecolor{vtealdeep}{HTML}{0B8C77}
\definecolor{vamber}{HTML}{FFB020}
\definecolor{vamberdeep}{HTML}{B87400}
\definecolor{vcoral}{HTML}{FF5D73}
\definecolor{vcoraldeep}{HTML}{C8243E}
\definecolor{vviolet}{HTML}{7C5CFF}
\definecolor{vvioletdeep}{HTML}{5337D6}
\definecolor{vsky}{HTML}{4F8CFF}
\definecolor{vskydeep}{HTML}{2A62D6}
\definecolor{vgreen}{HTML}{2BB673}
\definecolor{vgreendeep}{HTML}{178453}
\definecolor{vmist}{HTML}{F2F5FB}
\definecolor{vline}{HTML}{DCE3F0}
\definecolor{vslate}{HTML}{5B6788}
\definecolor{vtext}{HTML}{1E2433}
% ---------------------------------------------------------------- fonts
\defaultfontfeatures{Ligatures=TeX}
\setmainfont{Inter-Regular.otf}[
BoldFont=Inter-Bold.otf, ItalicFont=Inter-Italic.otf,
BoldItalicFont=Inter-BoldItalic.otf]
\setmonofont{FiraMono-Regular.otf}[BoldFont=FiraMono-Bold.otf, Scale=0.92]
\newfontfamily\semibold{Inter-SemiBold.otf}
\newfontfamily\lightfont{Inter-Light.otf}[ItalicFont=Inter-Italic.otf]
\newfontfamily\display{Montserrat-ExtraBold.otf}
\newfontfamily\displayblack{Montserrat-Black.otf}
\newfontfamily\displaybold{Montserrat-Bold.otf}
\newfontfamily\displaysemi{Montserrat-SemiBold.otf}
\newfontfamily\displaylight{Montserrat-Light.otf}
\newfontfamily\displaythin{Montserrat-Thin.otf}
\newfontfamily\labelfont{Montserrat-Bold.otf}[LetterSpace=14]
\newfontfamily\labelfontwide{Montserrat-SemiBold.otf}[LetterSpace=30]
\color{vtext}
\linespread{1.14}
\setlength\parindent{0pt}
\setlength\parskip{0.62em plus 0.15em minus 0.05em}
\setlength\emergencystretch{2em}
\raggedbottom
% ---------------------------------------------------------------- small helpers
\newcommand\cmd[1]{\leavevmode{\ttfamily\color{vtealdeep}#1}}
\newcommand\tld{\textasciitilde}
\newcommand\env[1]{\leavevmode{\ttfamily\color{vvioletdeep}#1}}
\newcommand\file[1]{\leavevmode{\ttfamily\color{vskydeep}#1}}
\newcommand\hl[1]{\leavevmode{\semibold\color{vink}#1}}
\newcommand\keycap[1]{\tikz[baseline=(k.base)]\node[draw=vline,fill=white,rounded corners=2pt,
inner xsep=4pt,inner ysep=2pt,font=\footnotesize\semibold,
blur shadow={shadow xshift=0pt,shadow yshift=-0.6pt,shadow blur steps=3,shadow opacity=25}](k){#1};}
\newcommand\pill[2][vteal]{\tikz[baseline=(p.base)]\node[fill=#1!14,text=#1!70!black,rounded corners=5pt,
inner xsep=5pt,inner ysep=2pt,font=\scriptsize\labelfont](p){#2};}
\newcommand\stepdot[1]{\tikz[baseline=(c.base)]\node[circle,fill=vteal,text=white,
font=\scriptsize\displaybold,inner sep=0pt,minimum size=15pt](c){#1};}
% A screenshot. Optional width fraction, file name in img/, caption.
\newcommand\shot[3][1.1]{%
\par\smallskip
\begin{center}
\makebox[\linewidth]{\includegraphics[width=#1\linewidth]{img/#2}}\par\vspace{-6pt}
{\footnotesize\color{vslate}\faIcon[regular]{camera}\enspace #3}
\end{center}\par\smallskip}
% ---------------------------------------------------------------- lists
\setlist{itemsep=2pt,topsep=4pt,parsep=1pt}
\setlist[itemize,1]{label={\color{vteal}\footnotesize\faIcon{chevron-right}},leftmargin=1.6em}
\setlist[itemize,2]{label={\color{vslate}\scriptsize\faIcon[regular]{circle}},leftmargin=1.4em}
\setlist[enumerate,1]{label={\color{vtealdeep}\displaybold\arabic*.},leftmargin=1.8em}
\newlist{steps}{enumerate}{1}
\setlist[steps]{label=\protect\stepdot{\arabic*},leftmargin=2.3em,itemsep=5pt,labelsep=0.6em}
\newlist{checks}{itemize}{1}
\setlist[checks]{label={\color{vtealdeep}\faIcon[regular]{check-square}},leftmargin=1.9em,itemsep=3pt}
\newlist{nopes}{itemize}{1}
\setlist[nopes]{label={\color{vcoraldeep}\faIcon[regular]{times-circle}},leftmargin=1.9em,itemsep=3pt}
% ---------------------------------------------------------------- callout boxes
\tcbset{
vbox/.style 2 args={enhanced,breakable,colback=#1!7!white,colframe=#1,boxrule=0pt,
leftrule=3.2pt,arc=3pt,outer arc=3pt,left=11pt,right=11pt,top=8pt,bottom=8pt,
before skip=10pt,after skip=12pt,
before upper={{\labelfont\scriptsize\color{#1!75!black}#2}\par\vspace{3pt}}},
}
\newtcolorbox{tldr}{vbox={vteal}{\faIcon{bolt}\enspace TL;DR},colback=vteal!9!white,fontupper=\semibold}
\newtcolorbox{quickwin}{vbox={vgreen}{\faIcon{rocket}\enspace QUICK WIN}}
\newtcolorbox{headsup}{vbox={vamber}{\faIcon{exclamation-triangle}\enspace HEADS UP}}
\newtcolorbox{danger}{vbox={vcoral}{\faIcon{skull-crossbones}\enspace HERE BE DRAGONS}}
\newtcolorbox{squirrel}{vbox={vviolet}{\faIcon{paw}\enspace SQUIRREL! \normalfont\itshape\color{vslate}(a tangent. skip it if you like)}}
\newtcolorbox{whyso}{vbox={vsky}{\faIcon[regular]{lightbulb}\enspace WHY THOUGH?}}
\newtcolorbox{pocket}{enhanced,breakable,colback=vnavy,colframe=vnavy,arc=5pt,boxrule=0pt,
left=14pt,right=14pt,top=11pt,bottom=11pt,before skip=16pt,after skip=10pt,
colupper=white,
before upper={{\labelfont\scriptsize\color{vteal}\faIcon{brain}\enspace POCKET THIS}\par\vspace{4pt}},
overlay={\node[anchor=north east,font=\fontsize{46}{46}\selectfont,text=vteal,opacity=0.13]
at ([xshift=-8pt,yshift=-4pt]frame.north east) {\faIcon{brain}};}}
\newenvironment{pocketlist}{\begin{itemize}[label={\color{vteal}\faIcon{check}},leftmargin=1.7em,itemsep=3pt]}{\end{itemize}}
% ---------------------------------------------------------------- shell listings
\lstdefinestyle{shell}{
basicstyle=\ttfamily\small\color{white!90!vmist},
columns=fullflexible,keepspaces=true,showstringspaces=false,
breaklines=true,breakatwhitespace=true,
postbreak=\mbox{\textcolor{vslate}{$\hookrightarrow$}\space},
morecomment=[l]{\#\ },commentstyle=\color{vslate!55!white},
literate={\$\ }{{\textcolor{vteal}{\$}\ }}2 {~}{{\textasciitilde}}1,
upquote=true}
\newtcblisting{shell}{enhanced,breakable,listing only,listing options={style=shell},
colback=vnavy,colframe=vnavy,boxrule=0pt,arc=5pt,left=12pt,right=12pt,top=13pt,bottom=7pt,
before skip=8pt,after skip=10pt,
overlay={\foreach \c [count=\i] in {vcoral,vamber,vgreen}
{\fill[\c] ([xshift=6pt+\i*8pt,yshift=-7pt]frame.north west) circle (2.1pt);}}}
\newtcblisting{plain}{enhanced,breakable,listing only,
listing options={basicstyle=\ttfamily\small\color{vtext},columns=fullflexible,keepspaces,
upquote=true,literate={~}{{\textasciitilde}}1},
colback=vmist,colframe=vline,boxrule=0.4pt,arc=4pt,left=10pt,right=10pt,top=6pt,bottom=6pt}
% ---------------------------------------------------------------- tables
\newcolumntype{Y}{>{\raggedright\arraybackslash}X}
\newcommand\thead[1]{\leavevmode{\labelfont\scriptsize\color{vslate}#1}}
\newenvironment{vtable}[1]
{\par\smallskip\rowcolors{2}{vmist}{white}\renewcommand\arraystretch{1.45}%
\small\tabularx{\linewidth}{#1}\toprule}
{\bottomrule\endtabularx\par\smallskip}
% ---------------------------------------------------------------- headers & footers
\newcommand\pagepill{\tikz[baseline=(n.base)]\node[fill=vnavy,text=white,rounded corners=7pt,
inner xsep=8pt,inner ysep=3.5pt,font=\footnotesize\displaybold](n){\thepage};}
\pagestyle{fancy}
\fancyhf{}
\renewcommand\headrulewidth{0pt}
\fancyhead[L]{\footnotesize\displaysemi\color{vslate}\leftmark}
\fancyhead[R]{\footnotesize\color{vslate}{\displaybold\color{vteal}vault}\enspace field guide}
\fancyfoot[C]{\pagepill}
\fancypagestyle{plain}{\fancyhf{}\fancyfoot[C]{\pagepill}}
\renewcommand\chaptermark[1]{\markboth{#1}{}}
\renewcommand\sectionmark[1]{}
% ---------------------------------------------------------------- chapter banners
\newif\ifvappendix
\newcommand\vaultchaptercount{11}
\newcommand\vbanner[3]{% #1 label, #2 big mark, #3 title
\begin{tikzpicture}[remember picture,overlay]
\shade[left color=vnavy,right color=vink]
(current page.north west) rectangle ([yshift=-72mm]current page.north east);
\begin{scope}
\clip (current page.north west) rectangle ([yshift=-72mm]current page.north east);
\foreach \r in {14,24,34,44,54,64}
\draw[vteal,opacity=0.13,line width=0.7pt]
([xshift=-34mm,yshift=-38mm]current page.north east) circle (\r mm);
\foreach \a in {0,6,...,354}
\draw[vteal,opacity=0.22,line width=0.5pt]
([xshift=-34mm,yshift=-38mm]current page.north east) ++(\a:44mm) -- ++(\a:3mm);
\node[anchor=center,font=\displayblack\fontsize{96}{96}\selectfont,text=vteal]
at ([xshift=-34mm,yshift=-38mm]current page.north east) {#2};
\end{scope}
\fill[vamber] ([yshift=-72mm]current page.north west) rectangle
([yshift=-73.4mm,xshift=62mm]current page.north west);
\node[anchor=south west,font=\labelfont\scriptsize,text=vteal]
at ([xshift=26mm,yshift=-27mm]current page.north west) {#1};
\node[anchor=north west,text width=118mm,align=left,text=white,
font={\display\fontsize{27}{31}\selectfont\hyphenpenalty=10000\exhyphenpenalty=10000}]
at ([xshift=25.4mm,yshift=-30mm]current page.north west) {#3};
\ifvappendix\else\ifnum\value{chapter}>0
\foreach \i in {1,...,\vaultchaptercount}{
\ifnum\i<\numexpr\value{chapter}+1\relax
\fill[vteal] ([xshift=26mm+\i*5.2mm-5.2mm,yshift=-63mm]current page.north west) circle (1.5pt);
\else
\draw[vteal!60,line width=0.5pt] ([xshift=26mm+\i*5.2mm-5.2mm,yshift=-63mm]current page.north west) circle (1.5pt);
\fi}
\node[anchor=west,font=\scriptsize\color{white!60!vslate}]
at ([xshift=26mm+\vaultchaptercount*5.2mm,yshift=-63mm]current page.north west)
{you are here};
\fi\fi
\end{tikzpicture}%
\par\vspace*{40mm}}
\newcommand\vchapterhead[1]{\ifvappendix\vbanner{APPENDIX \thechapter}{\thechapter}{#1}\else
\vbanner{CHAPTER \thechapter\ \ /\ \ \vaultchaptercount}{\thechapter}{#1}\fi}
\titleformat{\chapter}[block]{}{}{0pt}{\vchapterhead}
\titleformat{name=\chapter,numberless}[block]{}{}{0pt}{\vbanner{THE VAULT FIELD GUIDE}{\faIcon{key}}}
\titlespacing*{\chapter}{0pt}{-14mm}{6mm}
\titleformat{\section}[hang]{\display\fontsize{16.5}{20}\selectfont\color{vink}}
{\llap{\color{vteal}\thesection\hspace{0.65em}}}{0pt}{}
\titlespacing*{\section}{0pt}{18pt plus 4pt minus 2pt}{6pt}
\newcommand\vsubhead[1]{\llap{\color{vteal}\scriptsize\faIcon{square-full}\hspace{0.6em}}#1}
\titleformat{\subsection}[block]{\displaybold\fontsize{12}{15}\selectfont\color{vink}}
{}{0pt}{\vsubhead}
\titlespacing*{\subsection}{0pt}{12pt plus 3pt}{3pt}
\setcounter{secnumdepth}{1}
\setcounter{tocdepth}{1}
% ---------------------------------------------------------------- contents
\titlecontents{chapter}[0pt]{\addvspace{9pt}\display\fontsize{12.5}{16}\selectfont\color{vink}}
{\makebox[11mm][l]{\color{vteal}\thecontentslabel}}{}
{\hfill\color{vtealdeep}\contentspage}
\titlecontents{section}[11mm]{\addvspace{1pt}\small\color{vtext}}
{\makebox[10mm][l]{\color{vslate}\thecontentslabel}}{}
{\color{vline}\titlerule*[0.55pc]{.}\color{vslate}\contentspage}
\newcommand\tagline[1]{\addtocontents{toc}{\protect\vspace{-1pt}\protect\hspace*{11mm}%
{\protect\footnotesize\protect\itshape\protect\color{vslate}#1}\protect\par}}
% ---------------------------------------------------------------- cover
\newcommand\vaultdoor[2]{% #1 centre coordinate, #2 radius in mm
\begin{scope}[shift={#1}]
\foreach \g/\o in {1.42/0.035,1.3/0.05,1.2/0.07,1.12/0.09}
\fill[vteal,opacity=\o] (0,0) circle (#2*\g mm);
\fill[vink] (0,0) circle (#2 mm);
\draw[vteal,line width=1.6pt] (0,0) circle (#2 mm);
\draw[vteal!55,line width=0.5pt] (0,0) circle (#2*0.93 mm);
\foreach \a in {0,3,...,357}
\draw[vteal!70,line width=0.45pt] (\a:#2*0.93 mm) -- (\a:#2*0.885 mm);
\foreach \a in {0,30,...,330}
\draw[vteal,line width=1.1pt] (\a:#2*0.93 mm) -- (\a:#2*0.85 mm);
\fill[vnavy] (0,0) circle (#2*0.76 mm);
\draw[vteal!40,line width=0.6pt] (0,0) circle (#2*0.76 mm);
\foreach \a in {22.5,67.5,...,337.5}
{\fill[vink] (\a:#2*0.66 mm) circle (#2*0.055 mm);
\draw[vteal!80,line width=0.6pt] (\a:#2*0.66 mm) circle (#2*0.055 mm);}
\draw[vamber,line width=2.2pt,line cap=round] (0,0) circle (#2*0.34 mm);
\foreach \a in {90,210,330}
{\draw[white,line width=3.6pt,line cap=round] (\a:#2*0.1 mm) -- (\a:#2*0.5 mm);
\fill[white] (\a:#2*0.5 mm) circle (#2*0.045 mm);}
\fill[white] (0,0) circle (#2*0.12 mm);
\fill[vamber] (0,0) circle (#2*0.055 mm);
\end{scope}}
\newcommand\makecover{%
\thispagestyle{empty}
\begin{tikzpicture}[remember picture,overlay]
\shade[top color=vink,bottom color=vnavy]
(current page.north west) rectangle (current page.south east);
% ciphertext rain
\pgfmathsetseed{1789}
\begin{scope}
\clip (current page.north west) rectangle (current page.south east);
\foreach \y in {0,...,58}{
\pgfmathrandominteger{\off}{0}{60}
\node[anchor=west,font=\ttfamily\fontsize{6.5}{6.5}\selectfont,text=vteal,opacity=0.075]
at ([xshift=-\off mm,yshift=-\y*5.1mm-3mm]current page.north west)
{9f3a c01d e55b 7a90 b4f2 0e6c d318 a7e4 52cb 11f0 8d6e 4ab9 e203 7c5f 96d1 3be8 f40a 6d27 c9b3 0a5e 71fd 2c84 be16 5903 d7aa 18e9 f36b 4c20 a95d};}
\end{scope}
\vaultdoor{($(current page.center)+(38mm,42mm)$)}{62}
% title block
\node[anchor=south west,font=\labelfontwide\small,text=vteal]
at ([xshift=24mm,yshift=112mm]current page.south west) {THE FIELD GUIDE};
\node[anchor=north west,font=\displayblack\fontsize{104}{104}\selectfont,text=white]
at ([xshift=21mm,yshift=110mm]current page.south west) {vault};
\fill[vamber] ([xshift=25mm,yshift=72mm]current page.south west) rectangle ++(34mm,1.6mm);
\node[anchor=north west,text width=130mm,align=left,
font=\lightfont\fontsize{17}{23}\selectfont,text=white!85!vteal]
at ([xshift=24mm,yshift=66mm]current page.south west)
{Post-quantum file encryption.\\ With a secret trapdoor.};
\node[anchor=south west] at ([xshift=24mm,yshift=26mm]current page.south west)
{\coverchip{\faIcon{shield-alt}}{FrodoKEM + ML-KEM}\hspace{2.5mm}%
\coverchip{\faIcon{user-secret}}{decoy passphrase}\hspace{2.5mm}%
\coverchip{\faIcon{microchip}}{one binary, 43 platforms}};
\node[anchor=south east,font=\scriptsize\labelfont,text=white!55!vslate]
at ([xshift=-22mm,yshift=14mm]current page.south east) {VERSION \vaultversion\ \ \textbullet\ \ \vaultdate};
\node[anchor=south west,font=\footnotesize\itshape,text=white!55!vslate]
at ([xshift=24mm,yshift=14mm]current page.south west) {for humans who skim};
\end{tikzpicture}}
\newcommand\coverchip[2]{\tikz[baseline=(c.base)]\node[draw=vteal!70,line width=0.6pt,rounded corners=8pt,
text=white,font=\footnotesize\semibold,inner xsep=7pt,inner ysep=4pt](c){{\color{vteal}#1}\enspace #2};}
\endinput