diff --git a/.gitattributes b/.gitattributes
index 5b7fbba..be62f13 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,6 +2,8 @@
# zip. `bin/build-plugin-zip.sh` builds with `git archive`, which honours these,
# so this file is the single list of what is developer-only.
/.gitattributes export-ignore
+/.wordpress-org export-ignore
+/assets-src export-ignore
/.github export-ignore
/.gitignore export-ignore
/.phpcs.xml.dist export-ignore
diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
index 8206064..f52cc34 100644
--- a/.phpcs.xml.dist
+++ b/.phpcs.xml.dist
@@ -65,12 +65,25 @@
Test files are not shipped: they legitimately reassign WordPress
globals, declare unprefixed test classes, and carry docblocks that hold
only PHPUnit annotations (@covers, @dataProvider).
+
+ The local rig (`bin/rig/*`, driven by `bin/local-rig.sh`) is the same
+ category and then some. It is not shipped — `bin/` is export-ignored and
+ the zip guard refuses an archive containing it — and it is not plugin
+ code: these are standalone scripts that install a throwaway WordPress,
+ seed it and assert against rendered pages. Two of them do not run inside
+ WordPress at all. Prefixing a local `$url` in a test harness with
+ `citecue_` would be ceremony that makes the harness harder to read
+ without making anything safer. Everything the sniffs flag there that IS
+ worth fixing — missing docblocks, short ternaries — is fixed rather than
+ excused; only these four categories are waived.
-->
/tests/*
+ /bin/rig/*/tests/*
+ /bin/rig/*/tests/*
@@ -83,6 +96,7 @@
/tests/*
+ /bin/rig/*
diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png
new file mode 100644
index 0000000..a661f6e
Binary files /dev/null and b/.wordpress-org/banner-1544x500.png differ
diff --git a/.wordpress-org/banner-772x250.png b/.wordpress-org/banner-772x250.png
new file mode 100644
index 0000000..e356247
Binary files /dev/null and b/.wordpress-org/banner-772x250.png differ
diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png
new file mode 100644
index 0000000..c84446e
Binary files /dev/null and b/.wordpress-org/icon-128x128.png differ
diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png
new file mode 100644
index 0000000..930d3c8
Binary files /dev/null and b/.wordpress-org/icon-256x256.png differ
diff --git a/.wordpress-org/icon.svg b/.wordpress-org/icon.svg
new file mode 100644
index 0000000..392da32
--- /dev/null
+++ b/.wordpress-org/icon.svg
@@ -0,0 +1,20 @@
+
diff --git a/assets-src/banner.html b/assets-src/banner.html
new file mode 100644
index 0000000..a85383b
--- /dev/null
+++ b/assets-src/banner.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
diff --git a/assets-src/build-assets.sh b/assets-src/build-assets.sh
new file mode 100755
index 0000000..474ca65
--- /dev/null
+++ b/assets-src/build-assets.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+#
+# Regenerates the WordPress.org directory assets in .wordpress-org/.
+#
+# These are the banner and icon the plugin directory shows above the readme.
+# They are NOT part of the plugin: WordPress.org serves them from the `assets/`
+# folder of the SVN repository, which sits beside `trunk/` and `tags/` and is
+# never installed on a site. Both directories are export-ignored so they can
+# never reach the distributed zip.
+#
+# Rendering is done by headless Chrome rather than a dedicated rasterizer
+# because it is the one thing reliably present on a Mac that can lay out real
+# webfont text, and --window-size with --force-device-scale-factor gives exact
+# pixel dimensions. The 2x pass is a genuine re-render, not an upscale.
+#
+# Fonts come from the citecue2 site repository rather than being vendored
+# here: they are the same faces the brand uses everywhere, and a second copy
+# in a second repo is a second thing to update when they change.
+#
+# Usage: assets-src/build-assets.sh
+# CITECUE_FONT_DIR override the font source directory
+# CHROME override the Chrome binary
+
+set -euo pipefail
+
+ROOT=$(git rev-parse --show-toplevel)
+SRC="$ROOT/assets-src"
+OUT="$ROOT/.wordpress-org"
+
+FONT_DIR=${CITECUE_FONT_DIR:-"$HOME/Sites/citecue2/public/fonts"}
+CHROME=${CHROME:-"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"}
+
+if [ ! -x "$CHROME" ]; then
+ echo "error: Chrome not found at $CHROME (set CHROME=)" >&2
+ exit 1
+fi
+
+for face in schibsted-grotesk schibsted-grotesk-italic instrument-sans jetbrains-mono; do
+ if [ ! -f "$FONT_DIR/$face.woff2" ]; then
+ echo "error: $FONT_DIR/$face.woff2 not found (set CITECUE_FONT_DIR=)" >&2
+ exit 1
+ fi
+done
+
+WORK=$(mktemp -d)
+trap 'rm -rf "$WORK"' EXIT
+
+# Inline the faces. A relative @font-face would be a separate fetch that
+# --screenshot does not wait for, and the banner would silently render in a
+# fallback with different metrics.
+python3 - "$SRC/banner.html" "$FONT_DIR" "$WORK/banner.html" <<'PY'
+import base64, pathlib, sys
+
+template, font_dir, out = (pathlib.Path(p) for p in sys.argv[1:4])
+html = template.read_text()
+
+for token, face in (
+ ("__SG__", "schibsted-grotesk"),
+ ("__SGI__", "schibsted-grotesk-italic"),
+ ("__IS__", "instrument-sans"),
+ ("__JB__", "jetbrains-mono"),
+):
+ if token not in html:
+ sys.exit(f"error: {token} missing from {template}")
+ html = html.replace(token, base64.b64encode((font_dir / f"{face}.woff2").read_bytes()).decode())
+
+out.write_text(html)
+PY
+
+shot() { # shot