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
+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