Use Podman for container builds

ober

08e60d4cd40e10bae8bbd6ef8b8e685a413cbe6e

diff --git a/.containerignore b/.containerignore
new file mode 100644
index 0000000..9d7bd06
--- /dev/null
+++ b/.containerignore
@@ -0,0 +1,17 @@
+vendor/jerboa/
+vendor/jerboa-shell/
+vendor/jerboa-aws/
+vendor/jerboa-pcre2/
+vendor/jerboa-scintilla/
+vendor/jerboa-qt/
+vendor/jerboa-ssl/
+vendor/jerboa-https/
+
+.git/
+*.o
+*.so
+*.dylib
+*.wpo
+*.boot
+jemacs
+jemacs-qt
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 9d7bd06..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,17 +0,0 @@
-vendor/jerboa/
-vendor/jerboa-shell/
-vendor/jerboa-aws/
-vendor/jerboa-pcre2/
-vendor/jerboa-scintilla/
-vendor/jerboa-qt/
-vendor/jerboa-ssl/
-vendor/jerboa-https/
-
-.git/
-*.o
-*.so
-*.dylib
-*.wpo
-*.boot
-jemacs
-jemacs-qt
diff --git a/AGENTS.md b/AGENTS.md
index 0f5b749..4ab785f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -371,7 +371,6 @@ improved versions of the above.
 When working in a Jerboa project, **ONLY modify files in the current repo** unless the user explicitly names another path.
 
 Common sibling repos that exist but must NOT be touched without explicit instruction:
-- `~/mine/jerboa-emacs` — **NEVER touch**. Another model owns it.
 - `~/mine/jerboa-mcp` — Legacy node MCP, superseded. The active MCP server now lives in THIS repo at `mcp/` + `data/`. Don't modify the legacy repo unless told.
 - `~/mine/jerboa-shell` — Only modify when user explicitly says to work there.
 - `~/mine/gerbil-mcp` — **NEVER touch**. Deprecated.
@@ -414,8 +413,8 @@ Run `jerboa_stale_static` to detect stale `.so` files before debugging "why does
 
 **ALWAYS** run a clean build **before** committing any code to this repository. Pick the right target for the *current* platform:
 
-- **Linux**: run `make docker-build` — the Docker image must build cleanly against the full musl-static release pipeline.
-- **macOS / FreeBSD / other**: run `make binary` — the native local build must succeed. Do **not** run `make docker-build` here; Docker on non-Linux hosts is slow and not the canonical pipeline for those platforms.
+- **Linux**: run `make static-qt` — the Podman image must build cleanly against the full musl-static release pipeline.
+- **macOS / FreeBSD / other**: run `make binary` — the native local build must succeed. Do **not** run `make static-qt` here; Podman on non-Linux hosts is slow and not the canonical pipeline for those platforms.
 
 Do not commit if the build fails.
 
diff --git a/Containerfile b/Containerfile
new file mode 100644
index 0000000..3b4f957
--- /dev/null
+++ b/Containerfile
@@ -0,0 +1,337 @@
+# Intermediate image for jemacs-qt static builds.
+# Bakes static Qt6 + QScintilla from source, plus all Chez Scheme
+# dependencies (jerboa, jsh, jerboa-pcre2, jerboa-scintilla,
+# jerboa-qt) so that subsequent jemacs-qt builds only compile jemacs
+# itself (~5-10 min instead of ~30 min).
+#
+ARG ARCH=x86_64
+FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d
+
+# ── Phase 1: Alpine build deps ──────────────────────────────────────────
+RUN apk add --no-cache \
+    su-exec \
+    cmake samurai perl python3 linux-headers patchelf \
+    libxcb-dev xcb-util-dev xcb-util-image-dev \
+    xcb-util-keysyms-dev xcb-util-renderutil-dev \
+    xcb-util-wm-dev xcb-util-cursor-dev \
+    libx11-dev libxkbcommon-dev \
+    fontconfig-dev freetype-dev harfbuzz-dev \
+    libpng-dev zlib-dev mesa-dev \
+    pcre2-dev \
+    at-spi2-core-dev libdrm-dev \
+    zlib-static libxcb-static \
+    fontconfig-static freetype-static harfbuzz-static \
+    libpng-static bzip2-static expat-static brotli-static \
+    libx11-static graphite2-static libxkbcommon-static \
+    ncurses-dev ncurses-static \
+    util-linux-dev util-linux-static \
+    libvterm-dev libvterm-static \
+    gcc g++ binutils make git curl wget
+
+# Build static libXau (no Alpine -static package available)
+ARG LIBXAU_SHA256=74d0e4dfa3d39ad8939e99bda37f5967aba528211076828464d2777d477fc0fb
+RUN apk add --no-cache libxau-dev && \
+    cd /tmp && \
+    wget -q https://xorg.freedesktop.org/releases/individual/lib/libXau-1.0.12.tar.xz && \
+    echo "${LIBXAU_SHA256}  libXau-1.0.12.tar.xz" | sha256sum -c - && \
+    tar xf libXau-1.0.12.tar.xz && \
+    cd libXau-1.0.12 && \
+    ./configure --prefix=/usr --enable-static --disable-shared && \
+    make -j$(nproc) && make install && \
+    cd / && rm -rf /tmp/libXau-1.0.12*
+
+# Build static libxcb-util (no Alpine -static package; needed by xcb-image)
+ARG XCB_UTIL_SHA256=5abe3bbbd8e54f0fa3ec945291b7e8fa8cfd3cccc43718f8758430f94126e512
+RUN cd /tmp && \
+    wget -q https://xcb.freedesktop.org/dist/xcb-util-0.4.1.tar.xz && \
+    echo "${XCB_UTIL_SHA256}  xcb-util-0.4.1.tar.xz" | sha256sum -c - && \
+    tar xf xcb-util-0.4.1.tar.xz && \
+    cd xcb-util-0.4.1 && \
+    ./configure --prefix=/usr --enable-static --disable-shared && \
+    make -j$(nproc) && make install && \
+    cd / && rm -rf /tmp/xcb-util-0.4.1*
+
+# Build static OpenSSL (needed by jerboa-ssl for AWS API calls)
+# Alpine's openssl-dev only ships shared libs; we need .a files for the static binary.
+ARG OPENSSL_VERSION=3.5.7
+ARG OPENSSL_SHA256=a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8
+RUN apk add --no-cache openssl-dev perl && \
+    wget -q "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" && \
+    echo "${OPENSSL_SHA256}  openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c - && \
+    tar xf "openssl-${OPENSSL_VERSION}.tar.gz" && \
+    cd "openssl-${OPENSSL_VERSION}" && \
+    ./Configure no-shared no-tests -O2 --prefix=/usr && \
+    make -j$(nproc) && \
+    ./apps/openssl version | grep -F "OpenSSL ${OPENSSL_VERSION}" && \
+    ./apps/openssl version -a > /usr/lib/jemacs-openssl-build.txt && \
+    cp libssl.a libcrypto.a /usr/lib/ && \
+    cd / && rm -rf "openssl-${OPENSSL_VERSION}" "openssl-${OPENSSL_VERSION}.tar.gz"
+
+# ── Phase 2: Build Qt6 qtbase static ────────────────────────────────────
+ARG QT6_VERSION=6.8.3
+ARG QTBASE_SHA256=56001b905601bb9023d399f3ba780d7fa940f3e4861e496a7c490331f49e0b80
+RUN wget -q https://download.qt.io/official_releases/qt/6.8/${QT6_VERSION}/submodules/qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
+    echo "${QTBASE_SHA256}  qtbase-everywhere-src-${QT6_VERSION}.tar.xz" | sha256sum -c - && \
+    tar xf qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
+    rm qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
+    cmake -S qtbase-everywhere-src-${QT6_VERSION} -B qt6-build -G Ninja \
+      -DCMAKE_BUILD_TYPE=Release \
+      -DCMAKE_INSTALL_PREFIX=/opt/qt6-static \
+      -DBUILD_SHARED_LIBS=OFF \
+      -DQT_BUILD_EXAMPLES=OFF \
+      -DQT_BUILD_TESTS=OFF \
+      -DQT_BUILD_BENCHMARKS=OFF \
+      -DFEATURE_xcb=ON \
+      -DFEATURE_sql=OFF \
+      -DFEATURE_network=OFF \
+      -DFEATURE_testlib=OFF \
+      -DFEATURE_printsupport=ON \
+      -DFEATURE_dbus=OFF \
+      -DFEATURE_opengl=OFF \
+      -DFEATURE_vulkan=OFF \
+      -DFEATURE_glib=OFF \
+      -DFEATURE_icu=OFF && \
+    cmake --build qt6-build --parallel && \
+    cmake --install qt6-build && \
+    rm -rf qtbase-everywhere-src-${QT6_VERSION} qt6-build
+
+# ── Phase 3: Build QScintilla static ────────────────────────────────────
+ARG QSCI_VERSION=2.14.1
+ARG QSCINTILLA_SHA256=dfe13c6acc9d85dfcba76ccc8061e71a223957a6c02f3c343b30a9d43a4cdd4d
+RUN wget -q https://www.riverbankcomputing.com/static/Downloads/QScintilla/${QSCI_VERSION}/QScintilla_src-${QSCI_VERSION}.tar.gz && \
+    echo "${QSCINTILLA_SHA256}  QScintilla_src-${QSCI_VERSION}.tar.gz" | sha256sum -c - && \
+    tar xf QScintilla_src-${QSCI_VERSION}.tar.gz && \
+    rm QScintilla_src-${QSCI_VERSION}.tar.gz && \
+    cd QScintilla_src-${QSCI_VERSION}/src && \
+    /opt/qt6-static/bin/qmake CONFIG+=staticlib && \
+    make -j$(nproc) && \
+    make install && \
+    cd / && rm -rf QScintilla_src-${QSCI_VERSION}
+
+# ── Phase 4: Generate pkg-config files with full transitive deps ─────────
+# Static Qt6 cmake doesn't generate .pc files.  We extract direct deps
+# from .prl files, then add known transitive deps manually.
+RUN mkdir -p /opt/qt6-static/lib/pkgconfig && \
+    prl_libs() { \
+      grep '^QMAKE_PRL_LIBS ' "$1" | \
+        sed 's/^QMAKE_PRL_LIBS *= *//' | \
+        tr ' ' '\n' | grep '^-l' | \
+        grep -v '^-lQt6' | tr '\n' ' '; \
+    } && \
+    CORE_PRIVATE=$(prl_libs /opt/qt6-static/lib/libQt6Core.prl) && \
+    GUI_PRL=$(prl_libs /opt/qt6-static/lib/libQt6Gui.prl) && \
+    XCB_PRL=$(prl_libs /opt/qt6-static/plugins/platforms/libqxcb.prl) && \
+    TRANSITIVE="-lgraphite2 -lbz2 -lbrotlidec -lbrotlicommon -lexpat -lXau -lXdmcp" && \
+    GUI_PRIVATE="$GUI_PRL $TRANSITIVE" && \
+    XCB_TRANSITIVE="-lxcb-util -lxcb -lXau -lXdmcp" && \
+    XCB_PRIVATE="$XCB_PRL $TRANSITIVE $XCB_TRANSITIVE" && \
+    echo "Core deps: $CORE_PRIVATE" && \
+    echo "Gui deps: $GUI_PRIVATE" && \
+    echo "XCB deps: $XCB_PRIVATE" && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: Qt6Core' 'Description: Qt6 Core' 'Version: 6.8.3' \
+      'Cflags: -I${includedir} -I${includedir}/QtCore' \
+      "Libs: -L\${libdir} -lQt6Core" \
+      "Libs.private: $CORE_PRIVATE" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6Core.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: Qt6Gui' 'Description: Qt6 Gui' 'Version: 6.8.3' \
+      'Requires: Qt6Core' \
+      'Cflags: -I${includedir} -I${includedir}/QtGui' \
+      "Libs: -L\${libdir} -lQt6Gui" \
+      "Libs.private: $GUI_PRIVATE" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6Gui.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: Qt6Widgets' 'Description: Qt6 Widgets' 'Version: 6.8.3' \
+      'Requires: Qt6Gui' \
+      'Cflags: -I${includedir} -I${includedir}/QtWidgets -I${includedir}/QtGui -I${includedir}/QtCore' \
+      "Libs: -L\${libdir} -lQt6Widgets" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6Widgets.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: Qt6PrintSupport' 'Description: Qt6 PrintSupport' 'Version: 6.8.3' \
+      'Requires: Qt6Widgets' \
+      'Cflags: -I${includedir} -I${includedir}/QtPrintSupport' \
+      "Libs: -L\${libdir} -lQt6PrintSupport" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6PrintSupport.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      'plugindir=${prefix}/plugins' \
+      '' \
+      'Name: Qt6XcbPlugin' 'Description: Qt6 XCB platform plugin' 'Version: 6.8.3' \
+      'Requires: Qt6Gui' \
+      "Libs: -L\${libdir} -L\${plugindir}/platforms -lqxcb -lQt6XcbQpa" \
+      "Libs.private: $XCB_PRIVATE" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6XcbPlugin.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: QScintilla' 'Description: QScintilla for Qt6' 'Version: 2.14.1' \
+      'Requires: Qt6Widgets Qt6PrintSupport' \
+      'Cflags: -I${includedir} -I${includedir}/Qsci' \
+      "Libs: -L\${libdir} -lqscintilla2_qt6" \
+      > /opt/qt6-static/lib/pkgconfig/QScintilla.pc && \
+    echo "Generated .pc files:" && ls /opt/qt6-static/lib/pkgconfig/
+
+# ── Phase 5: Build Chez Scheme from source for musl ─────────────────────
+# Build WITHOUT --static so libkernel.a retains full dlopen support.
+# This is needed because jemacs embeds the program as a .so and loads it
+# at runtime via Sscheme_script (which calls dlopen internally).
+# musl's static libdl.a provides dlopen in the final static binary.
+ARG CHEZ_COMMIT=fdf6b3f5d069bf53082bb827f46714f2de8f11f5
+RUN git init -q /tmp/ChezScheme && \
+    git -C /tmp/ChezScheme remote add origin https://github.com/cisco/ChezScheme && \
+    git -C /tmp/ChezScheme fetch -q --depth 1 origin ${CHEZ_COMMIT} && \
+    test "$(git -C /tmp/ChezScheme rev-parse FETCH_HEAD)" = "${CHEZ_COMMIT}" && \
+    git -C /tmp/ChezScheme checkout -q --detach ${CHEZ_COMMIT} && \
+    cd /tmp/ChezScheme && \
+    ./configure --threads --installprefix=/opt/chez && \
+    make -j$(nproc) && \
+    make install && \
+    rm -rf /tmp/ChezScheme
+
+# ── Phase 5.5: Build tree-sitter + grammars (static) ──────────────────
+ARG TS_VERSION=0.24.7
+ARG TS_COMMIT=5e8760bf462ce7b19b3d2396d5b7860f3906a297
+RUN git init -q /tmp/tree-sitter && \
+    git -C /tmp/tree-sitter remote add origin https://github.com/tree-sitter/tree-sitter && \
+    git -C /tmp/tree-sitter fetch -q --depth 1 origin ${TS_COMMIT} && \
+    test "$(git -C /tmp/tree-sitter rev-parse FETCH_HEAD)" = "${TS_COMMIT}" && \
+    git -C /tmp/tree-sitter checkout -q --detach ${TS_COMMIT} && \
+    cd /tmp/tree-sitter && \
+    cc -c -O2 -Ilib/include lib/src/lib.c -o lib/src/lib.o && \
+    mkdir -p /opt/tree-sitter-lib && \
+    ar rcs /opt/tree-sitter-lib/libtree-sitter.a lib/src/lib.o && \
+    mkdir -p /opt/tree-sitter-include/tree_sitter && \
+    cp lib/include/tree_sitter/api.h /opt/tree-sitter-include/tree_sitter/ && \
+    rm -rf /tmp/tree-sitter
+
+# Build tree-sitter grammar static archives
+RUN mkdir -p /opt/tree-sitter-grammars && \
+    build_grammar() { \
+      name=$1; repo=$2; commit=$3; subdir=${4:-.}; \
+      git init -q /tmp/ts-$name && \
+      git -C /tmp/ts-$name remote add origin "https://github.com/$repo" && \
+      git -C /tmp/ts-$name fetch -q --depth 1 origin "$commit" && \
+      test "$(git -C /tmp/ts-$name rev-parse FETCH_HEAD)" = "$commit" && \
+      git -C /tmp/ts-$name checkout -q --detach "$commit" && \
+      cd /tmp/ts-$name/$subdir && \
+      SRC_DIR=src && \
+      gcc -c -O2 -I/opt/tree-sitter-include -I$SRC_DIR \
+        $SRC_DIR/parser.c -o parser.o && \
+      if [ -f $SRC_DIR/scanner.c ]; then \
+        gcc -c -O2 -I/opt/tree-sitter-include -I$SRC_DIR \
+          $SRC_DIR/scanner.c -o scanner.o && \
+        ar rcs /opt/tree-sitter-grammars/libtree-sitter-$name.a parser.o scanner.o; \
+      else \
+        ar rcs /opt/tree-sitter-grammars/libtree-sitter-$name.a parser.o; \
+      fi && \
+      cd / && rm -rf /tmp/ts-$name; \
+    } && \
+    build_grammar c          tree-sitter/tree-sitter-c          2a265d69a4caf57108a73ad2ed1e6922dd2f998c && \
+    build_grammar cpp        tree-sitter/tree-sitter-cpp        f41e1a044c8a84ea9fa8577fdd2eab92ec96de02 && \
+    build_grammar python     tree-sitter/tree-sitter-python     bffb65a8cfe4e46290331dfef0dbf0ef3679de11 && \
+    build_grammar javascript tree-sitter/tree-sitter-javascript 3a837b6f3658ca3618f2022f8707e29739c91364 && \
+    build_grammar rust       tree-sitter/tree-sitter-rust       3d087c3df25286140393ddecc339208fae107149 && \
+    build_grammar go         tree-sitter/tree-sitter-go         3c3775faa968158a8b4ac190a7fda867fd5fb748 && \
+    build_grammar bash       tree-sitter/tree-sitter-bash       487734f87fd87118028a65a4599352fa99c9cde8 && \
+    build_grammar json       tree-sitter/tree-sitter-json       ee35a6ebefcef0c5c416c0d1ccec7370cfca5a24 && \
+    build_grammar ruby       tree-sitter/tree-sitter-ruby       71bd32fb7607035768799732addba884a37a6210 && \
+    build_grammar java       tree-sitter/tree-sitter-java       94703d5a6bed02b98e438d7cad1136c01a60ba2c && \
+    build_grammar css        tree-sitter/tree-sitter-css        c0d581e32d183a536731ed6c3a72758b27e20411 && \
+    build_grammar html       tree-sitter/tree-sitter-html       5a5ca8551a179998360b4a4ca2c0f366a35acc03 && \
+    build_grammar lua        tree-sitter-grammars/tree-sitter-lua 534c461d2b75b0887ec968ef9635f4460b0878b7 && \
+    build_grammar scheme     6cdh/tree-sitter-scheme            c6cb7c7d7a04b3f5d999c28e2e9c0c31b2d50ece && \
+    echo "Grammars built:" && ls /opt/tree-sitter-grammars/
+
+# ── Phase 6: Jerboa dependencies ───────────────────────────────────────
+ENV PKG_CONFIG_PATH=/opt/qt6-static/lib/pkgconfig
+ENV SCHEME=/opt/chez/bin/scheme
+
+# Copy dependency sources from build contexts
+COPY --from=jerboa-src . /deps/jerboa
+COPY --from=jsh-src . /deps/jsh
+COPY --from=pcre2-src . /deps/jerboa-pcre2
+COPY --from=sci-src . /deps/jerboa-scintilla
+COPY --from=qt-src . /deps/jerboa-qt
+RUN mkdir -p /deps/qt-shim
+COPY support/vendor-overrides/qt_shim.cpp support/vendor-overrides/qt_shim.h /deps/qt-shim/
+# qt_chez_shim.c is maintained in support/vendor-overrides/ (not in jerboa-qt).
+COPY support/vendor-overrides/qt_chez_shim.c support/vendor-overrides/qt_shim.h /deps/jerboa-qt/
+
+# jerboa-shell keeps these support libraries as jerbuild input files.
+# Transpile only the libraries imported by support/static-precompile-deps.ss.
+RUN mkdir -p /tmp/jsh-jerbuild-src/compat /tmp/jsh-jerbuild-src/jsh && \
+    cp /deps/jsh/jerboa-src/src/compat/gambit.ss /tmp/jsh-jerbuild-src/compat/ && \
+    cp /deps/jsh/jerboa-src/src/jsh/ffi.ss /tmp/jsh-jerbuild-src/jsh/ && \
+    cp /deps/jsh/jerboa-src/src/jsh/static-compat.ss /tmp/jsh-jerbuild-src/jsh/ && \
+    /opt/chez/bin/scheme --libdirs /deps/jerboa/lib \
+      --script /deps/jsh/support/jerbuild.ss /tmp/jsh-jerbuild-src /deps/jsh/src --force && \
+    rm -rf /tmp/jsh-jerbuild-src
+
+# Pre-compile all Jerboa library dependencies.
+# These .so files are baked into the image so jemacs builds only
+# need to compile jemacs-specific modules.
+COPY support/static-precompile-deps.ss /tmp/static-precompile-deps.ss
+RUN /opt/chez/bin/scheme \
+      --libdirs /deps/jerboa/lib:/deps/jsh/src:/deps/jsh/vendor/jerboa-coreutils/lib:/deps/jsh/vendor/jerboa-awk/lib:/deps/jsh/vendor/jerboa-sed/lib:/deps/jerboa-pcre2:/deps/jerboa-scintilla/lib:/deps/jerboa-qt/lib \
+      --compile-imported-libraries \
+      --script /tmp/static-precompile-deps.ss
+
+# Build static pcre2 shim object
+RUN PCRE2_CFLAGS=$(pkg-config --cflags libpcre2-8 2>/dev/null || echo "") && \
+    gcc -c -O2 -o /deps/jerboa-pcre2/pcre2_shim.o \
+        /deps/jerboa-pcre2/jerboa_pcre2_shim.c $PCRE2_CFLAGS -Wall
+
+# Build static jsh FFI shim object
+RUN gcc -c -O2 -o /deps/jsh/jsh_ffi_shim.o /deps/jsh/ffi-shim.c -Wall
+
+# Build static qt_chez_shim object
+RUN QT_CFLAGS=$(pkg-config --cflags Qt6Widgets 2>/dev/null || \
+        echo "-I/opt/qt6-static/include -I/opt/qt6-static/include/QtCore \
+              -I/opt/qt6-static/include/QtGui -I/opt/qt6-static/include/QtWidgets") && \
+    gcc -c -O2 -fPIC -o /deps/jerboa-qt/qt_chez_shim.o \
+        /deps/jerboa-qt/qt_chez_shim.c \
+        -I/deps/qt-shim $QT_CFLAGS -Wall
+
+# Build static libqt_shim.a from qt_shim.cpp
+# qt_static_plugins.o must be separate (NOT in archive) — it contains
+# Q_IMPORT_PLUGIN static constructors that the linker drops from archives.
+RUN QT_CFLAGS=$(pkg-config --cflags Qt6Widgets 2>/dev/null || \
+        echo "-I/opt/qt6-static/include -I/opt/qt6-static/include/QtCore \
+              -I/opt/qt6-static/include/QtGui -I/opt/qt6-static/include/QtWidgets") && \
+    QSCI_FLAGS="-DQT_SCINTILLA_AVAILABLE \
+        $(pkg-config --cflags QScintilla 2>/dev/null || \
+          echo "-I/opt/qt6-static/include -I/opt/qt6-static/include/Qsci")" && \
+    g++ -c -fPIC -std=c++17 \
+        $QT_CFLAGS $QSCI_FLAGS \
+        /deps/qt-shim/qt_shim.cpp \
+        -o /deps/qt-shim/qt_shim_static.o && \
+    ar rcs /deps/qt-shim/libqt_shim.a \
+        /deps/qt-shim/qt_shim_static.o && \
+    printf '#include <QtPlugin>\nQ_IMPORT_PLUGIN(QXcbIntegrationPlugin)\n' \
+        > /deps/qt-shim/qt_static_plugins.cpp && \
+    g++ -c -fPIC -std=c++17 $QT_CFLAGS \
+        /deps/qt-shim/qt_static_plugins.cpp \
+        -o /deps/qt-shim/qt_static_plugins.o
+
+WORKDIR /src
diff --git a/Containerfile.qt b/Containerfile.qt
new file mode 100644
index 0000000..43a9067
--- /dev/null
+++ b/Containerfile.qt
@@ -0,0 +1,333 @@
+# Podman-compatible recipe for building a fully static jemacs-qt with Qt6.
+#
+# Multi-stage build:
+#   Stage 1 (Alpine):          Build Qt6 static, QScintilla, tree-sitter, libvterm (musl)
+#   Stage 1.5 (Alpine+shim):   Compile libqt_shim.a with musl-compatible g++
+#   Stage 2 (jerboa21/jerboa): Copy Qt artifacts + Jerboa deps, build jemacs-qt
+#
+# Usage:
+#   podman build -f Containerfile.qt -t jemacs-qt-builder .
+#   id=$(podman create jemacs-qt-builder)
+#   podman cp $id:/out/jemacs-qt ./jemacs-qt && podman rm $id
+#   chmod +x jemacs-qt
+
+ARG JERBOA_IMAGE=jerboa21/jerboa@sha256:9c4034f1296601d5a9f7b219a0decdd5d62862a2be4347976b3d96b13ed7701e
+
+FROM ${JERBOA_IMAGE} AS jerboa-runtime
+
+# ── Stage 1: Build Qt6 + QScintilla + tree-sitter + libvterm on Alpine ───────
+FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d AS qt-alpine
+
+ARG QT6_VERSION=6.8.3
+ARG QSCI_VERSION=2.14.1
+ARG TS_VERSION=0.24.7
+ARG TS_COMMIT=5e8760bf462ce7b19b3d2396d5b7860f3906a297
+ARG LIBXAU_SHA256=74d0e4dfa3d39ad8939e99bda37f5967aba528211076828464d2777d477fc0fb
+ARG XCB_UTIL_SHA256=5abe3bbbd8e54f0fa3ec945291b7e8fa8cfd3cccc43718f8758430f94126e512
+ARG QTBASE_SHA256=56001b905601bb9023d399f3ba780d7fa940f3e4861e496a7c490331f49e0b80
+ARG QSCINTILLA_SHA256=dfe13c6acc9d85dfcba76ccc8061e71a223957a6c02f3c343b30a9d43a4cdd4d
+
+# ── Phase 1: Alpine build deps ──────────────────────────────────────────
+RUN apk add --no-cache \
+    cmake samurai perl python3 linux-headers patchelf file \
+    libxcb-dev xcb-util-dev xcb-util-image-dev \
+    xcb-util-keysyms-dev xcb-util-renderutil-dev \
+    xcb-util-wm-dev xcb-util-cursor-dev \
+    libx11-dev libxkbcommon-dev \
+    fontconfig-dev freetype-dev harfbuzz-dev \
+    mesa-dev \
+    libpng-dev zlib-dev \
+    pcre2-dev \
+    at-spi2-core-dev libdrm-dev \
+    zlib-static libxcb-static \
+    fontconfig-static freetype-static harfbuzz-static \
+    libpng-static bzip2-static expat-static brotli-static \
+    libx11-static graphite2-static libxkbcommon-static \
+    ncurses-dev ncurses-static \
+    util-linux-dev util-linux-static \
+    libvterm-dev libvterm-static \
+    gcc g++ binutils make git curl wget
+
+# Build static libXau (no Alpine -static package available)
+RUN apk add --no-cache libxau-dev && \
+    cd /tmp && \
+    wget -q https://xorg.freedesktop.org/releases/individual/lib/libXau-1.0.12.tar.xz && \
+    echo "${LIBXAU_SHA256}  libXau-1.0.12.tar.xz" | sha256sum -c - && \
+    tar xf libXau-1.0.12.tar.xz && \
+    cd libXau-1.0.12 && \
+    ./configure --prefix=/usr --enable-static --disable-shared && \
+    make -j$(nproc) && make install && \
+    cd / && rm -rf /tmp/libXau-1.0.12*
+
+# Build static libxcb-util (no Alpine -static package; needed by xcb-image)
+RUN cd /tmp && \
+    wget -q https://xcb.freedesktop.org/dist/xcb-util-0.4.1.tar.xz && \
+    echo "${XCB_UTIL_SHA256}  xcb-util-0.4.1.tar.xz" | sha256sum -c - && \
+    tar xf xcb-util-0.4.1.tar.xz && \
+    cd xcb-util-0.4.1 && \
+    ./configure --prefix=/usr --enable-static --disable-shared && \
+    make -j$(nproc) && make install && \
+    cd / && rm -rf /tmp/xcb-util-0.4.1*
+
+# ── Phase 2: Build Qt6 qtbase static ────────────────────────────────────
+RUN wget -q https://download.qt.io/official_releases/qt/6.8/${QT6_VERSION}/submodules/qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
+    echo "${QTBASE_SHA256}  qtbase-everywhere-src-${QT6_VERSION}.tar.xz" | sha256sum -c - && \
+    tar xf qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
+    rm qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
+    cmake -S qtbase-everywhere-src-${QT6_VERSION} -B qt6-build -G Ninja \
+      -DCMAKE_BUILD_TYPE=Release \
+      -DCMAKE_INSTALL_PREFIX=/opt/qt6-static \
+      -DBUILD_SHARED_LIBS=OFF \
+      -DQT_BUILD_EXAMPLES=OFF \
+      -DQT_BUILD_TESTS=OFF \
+      -DQT_BUILD_BENCHMARKS=OFF \
+      -DFEATURE_xcb=ON \
+      -DFEATURE_sql=OFF \
+      -DFEATURE_network=OFF \
+      -DFEATURE_testlib=OFF \
+      -DFEATURE_printsupport=ON \
+      -DFEATURE_dbus=OFF \
+      -DINPUT_opengl=no \
+      -DFEATURE_opengl=OFF \
+      -DFEATURE_opengl_desktop=OFF \
+      -DFEATURE_opengles2=OFF \
+      -DFEATURE_egl=OFF \
+      -DFEATURE_vulkan=OFF \
+      -DFEATURE_glib=OFF \
+      -DFEATURE_icu=OFF && \
+    cmake --build qt6-build --parallel && \
+    cmake --install qt6-build && \
+    rm -rf qtbase-everywhere-src-${QT6_VERSION} qt6-build
+
+# ── Phase 3: Build QScintilla static ────────────────────────────────────
+RUN wget -q https://www.riverbankcomputing.com/static/Downloads/QScintilla/${QSCI_VERSION}/QScintilla_src-${QSCI_VERSION}.tar.gz && \
+    echo "${QSCINTILLA_SHA256}  QScintilla_src-${QSCI_VERSION}.tar.gz" | sha256sum -c - && \
+    tar xf QScintilla_src-${QSCI_VERSION}.tar.gz && \
+    rm QScintilla_src-${QSCI_VERSION}.tar.gz && \
+    cd QScintilla_src-${QSCI_VERSION}/src && \
+    /opt/qt6-static/bin/qmake CONFIG+=staticlib && \
+    make -j$(nproc) && \
+    make install && \
+    cd / && rm -rf QScintilla_src-${QSCI_VERSION}
+
+# ── Phase 4: Generate pkg-config files with full transitive deps ─────────
+RUN mkdir -p /opt/qt6-static/lib/pkgconfig && \
+    prl_libs() { \
+      grep '^QMAKE_PRL_LIBS ' "$1" | \
+        sed 's/^QMAKE_PRL_LIBS *= *//' | \
+        tr ' ' '\n' | grep '^-l' | \
+        grep -v '^-lQt6' | tr '\n' ' '; \
+    } && \
+    CORE_PRIVATE=$(prl_libs /opt/qt6-static/lib/libQt6Core.prl) && \
+    GUI_PRL=$(prl_libs /opt/qt6-static/lib/libQt6Gui.prl) && \
+    XCB_PRL=$(prl_libs /opt/qt6-static/plugins/platforms/libqxcb.prl) && \
+    TRANSITIVE="-lgraphite2 -lbz2 -lbrotlidec -lbrotlicommon -lexpat -lXau -lXdmcp" && \
+    GUI_PRIVATE="$GUI_PRL $TRANSITIVE" && \
+    XCB_TRANSITIVE="-lxcb-util -lxcb -lXau -lXdmcp" && \
+    XCB_PRIVATE="$XCB_PRL $TRANSITIVE $XCB_TRANSITIVE" && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: Qt6Core' "Description: Qt6 Core" "Version: ${QT6_VERSION}" \
+      'Cflags: -I${includedir} -I${includedir}/QtCore' \
+      "Libs: -L\${libdir} -lQt6Core" \
+      "Libs.private: $CORE_PRIVATE" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6Core.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: Qt6Gui' "Description: Qt6 Gui" "Version: ${QT6_VERSION}" \
+      'Requires: Qt6Core' \
+      'Cflags: -I${includedir} -I${includedir}/QtGui' \
+      "Libs: -L\${libdir} -lQt6Gui" \
+      "Libs.private: $GUI_PRIVATE" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6Gui.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: Qt6Widgets' "Description: Qt6 Widgets" "Version: ${QT6_VERSION}" \
+      'Requires: Qt6Gui' \
+      'Cflags: -I${includedir} -I${includedir}/QtWidgets -I${includedir}/QtGui -I${includedir}/QtCore' \
+      "Libs: -L\${libdir} -lQt6Widgets" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6Widgets.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: Qt6PrintSupport' "Description: Qt6 PrintSupport" "Version: ${QT6_VERSION}" \
+      'Requires: Qt6Widgets' \
+      'Cflags: -I${includedir} -I${includedir}/QtPrintSupport' \
+      "Libs: -L\${libdir} -lQt6PrintSupport" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6PrintSupport.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      'plugindir=${prefix}/plugins' \
+      '' \
+      'Name: Qt6XcbPlugin' "Description: Qt6 XCB platform plugin" "Version: ${QT6_VERSION}" \
+      'Requires: Qt6Gui' \
+      "Libs: -L\${libdir} -L\${plugindir}/platforms -lqxcb -lQt6XcbQpa" \
+      "Libs.private: $XCB_PRIVATE" \
+      > /opt/qt6-static/lib/pkgconfig/Qt6XcbPlugin.pc && \
+    printf '%s\n' \
+      'prefix=/opt/qt6-static' \
+      'includedir=${prefix}/include' \
+      'libdir=${prefix}/lib' \
+      '' \
+      'Name: QScintilla' "Description: QScintilla for Qt6" "Version: ${QSCI_VERSION}" \
+      'Requires: Qt6Widgets Qt6PrintSupport' \
+      'Cflags: -I${includedir} -I${includedir}/Qsci' \
+      "Libs: -L\${libdir} -lqscintilla2_qt6" \
+      > /opt/qt6-static/lib/pkgconfig/QScintilla.pc && \
+    echo "Generated .pc files:" && ls /opt/qt6-static/lib/pkgconfig/
+
+# ── Phase 5: Build tree-sitter + grammars (static) ──────────────────────
+RUN git init -q /tmp/tree-sitter && \
+    git -C /tmp/tree-sitter remote add origin https://github.com/tree-sitter/tree-sitter && \
+    git -C /tmp/tree-sitter fetch -q --depth 1 origin ${TS_COMMIT} && \
+    test "$(git -C /tmp/tree-sitter rev-parse FETCH_HEAD)" = "${TS_COMMIT}" && \
+    git -C /tmp/tree-sitter checkout -q --detach ${TS_COMMIT} && \
+    cd /tmp/tree-sitter && \
+    cc -c -O2 -Ilib/include lib/src/lib.c -o lib/src/lib.o && \
+    mkdir -p /opt/tree-sitter-lib && \
+    ar rcs /opt/tree-sitter-lib/libtree-sitter.a lib/src/lib.o && \
+    mkdir -p /opt/tree-sitter-include/tree_sitter && \
+    cp lib/include/tree_sitter/api.h /opt/tree-sitter-include/tree_sitter/ && \
+    rm -rf /tmp/tree-sitter
+
+RUN mkdir -p /opt/tree-sitter-grammars && \
+    build_grammar() { \
+      name=$1; repo=$2; commit=$3; subdir=${4:-.}; \
+      git init -q /tmp/ts-$name && \
+      git -C /tmp/ts-$name remote add origin "https://github.com/$repo" && \
+      git -C /tmp/ts-$name fetch -q --depth 1 origin "$commit" && \
+      test "$(git -C /tmp/ts-$name rev-parse FETCH_HEAD)" = "$commit" && \
+      git -C /tmp/ts-$name checkout -q --detach "$commit" && \
+      cd /tmp/ts-$name/$subdir && \
+      SRC_DIR=src && \
+      gcc -c -O2 -I/opt/tree-sitter-include -I$SRC_DIR \
+        $SRC_DIR/parser.c -o parser.o && \
+      if [ -f $SRC_DIR/scanner.c ]; then \
+        gcc -c -O2 -I/opt/tree-sitter-include -I$SRC_DIR \
+          $SRC_DIR/scanner.c -o scanner.o && \
+        ar rcs /opt/tree-sitter-grammars/libtree-sitter-$name.a parser.o scanner.o; \
+      else \
+        ar rcs /opt/tree-sitter-grammars/libtree-sitter-$name.a parser.o; \
+      fi && \
+      cd / && rm -rf /tmp/ts-$name; \
+    } && \
+    build_grammar c          tree-sitter/tree-sitter-c          2a265d69a4caf57108a73ad2ed1e6922dd2f998c && \
+    build_grammar cpp        tree-sitter/tree-sitter-cpp        f41e1a044c8a84ea9fa8577fdd2eab92ec96de02 && \
+    build_grammar python     tree-sitter/tree-sitter-python     bffb65a8cfe4e46290331dfef0dbf0ef3679de11 && \
+    build_grammar javascript tree-sitter/tree-sitter-javascript 3a837b6f3658ca3618f2022f8707e29739c91364 && \
+    build_grammar rust       tree-sitter/tree-sitter-rust       3d087c3df25286140393ddecc339208fae107149 && \
+    build_grammar go         tree-sitter/tree-sitter-go         3c3775faa968158a8b4ac190a7fda867fd5fb748 && \
+    build_grammar bash       tree-sitter/tree-sitter-bash       487734f87fd87118028a65a4599352fa99c9cde8 && \
+    build_grammar json       tree-sitter/tree-sitter-json       ee35a6ebefcef0c5c416c0d1ccec7370cfca5a24 && \
+    build_grammar ruby       tree-sitter/tree-sitter-ruby       71bd32fb7607035768799732addba884a37a6210 && \
+    build_grammar java       tree-sitter/tree-sitter-java       94703d5a6bed02b98e438d7cad1136c01a60ba2c && \
+    build_grammar css        tree-sitter/tree-sitter-css        c0d581e32d183a536731ed6c3a72758b27e20411 && \
+    build_grammar html       tree-sitter/tree-sitter-html       5a5ca8551a179998360b4a4ca2c0f366a35acc03 && \
+    build_grammar lua        tree-sitter-grammars/tree-sitter-lua 534c461d2b75b0887ec968ef9635f4460b0878b7 && \
+    build_grammar scheme     6cdh/tree-sitter-scheme            c6cb7c7d7a04b3f5d999c28e2e9c0c31b2d50ece && \
+    echo "Grammars built:" && ls /opt/tree-sitter-grammars/
+
+# ── Phase 6: Stage Qt transitive static deps for the final link ─────────
+RUN mkdir -p /opt/qt-static-deps/lib && \
+    for lib in \
+      libpng.a libharfbuzz.a libfreetype.a libfontconfig.a libxkbcommon.a \
+      libxkbcommon-x11.a libgraphite2.a libbz2.a libbrotlidec.a \
+      libbrotlicommon.a libexpat.a libXau.a libXdmcp.a libxcb-util.a \
+      libxcb-cursor.a libxcb-icccm.a libxcb-image.a libxcb-keysyms.a \
+      libxcb-randr.a libxcb-render-util.a libxcb-shm.a libxcb-sync.a \
+      libxcb-xfixes.a libxcb-render.a libxcb-shape.a libxcb-xkb.a \
+      libxcb.a libX11-xcb.a libX11.a; do \
+        test -f "/usr/lib/$lib" || { echo "missing static Qt dependency: $lib" >&2; exit 1; }; \
+        cp -L "/usr/lib/$lib" "/opt/qt-static-deps/lib/$lib"; \
+    done
+
+# ── Stage 1.5: Compile libqt_shim.a with musl-compatible Alpine g++ ──────────
+# Separate stage so Qt6 cache is preserved when qt_shim.cpp changes
+FROM qt-alpine AS qt-shim-alpine
+
+COPY support/vendor-overrides/qt_shim.cpp support/vendor-overrides/qt_shim.h /tmp/qt-shim-src/
+
+RUN mkdir -p /tmp/qt-shim-out && \
+    QT_CFLAGS="-I/opt/qt6-static/include \
+      -I/opt/qt6-static/include/QtCore \
+      -I/opt/qt6-static/include/QtGui \
+      -I/opt/qt6-static/include/QtWidgets" && \
+    QSCI_FLAGS="-DQT_SCINTILLA_AVAILABLE \
+      -I/opt/qt6-static/include \
+      -I/opt/qt6-static/include/Qsci" && \
+    g++ -c -fPIC -std=c++17 -DJEMACS_CHEZ_SMP \
+      $QT_CFLAGS $QSCI_FLAGS \
+      /tmp/qt-shim-src/qt_shim.cpp \
+      -o /tmp/qt-shim-out/qt_shim_static.o && \
+    ar rcs /tmp/qt-shim-out/libqt_shim.a \
+      /tmp/qt-shim-out/qt_shim_static.o && \
+    printf '#include <QtPlugin>\nQ_IMPORT_PLUGIN(QXcbIntegrationPlugin)\n' \
+      > /tmp/qt-shim-out/qt_static_plugins.cpp && \
+    g++ -c -fPIC -std=c++17 $QT_CFLAGS \
+      /tmp/qt-shim-out/qt_static_plugins.cpp \
+      -o /tmp/qt-shim-out/qt_static_plugins.o && \
+    echo "libqt_shim.a built successfully" && \
+    ls -lh /tmp/qt-shim-out/
+
+# ── Stage 2: Alpine/musl builder + Chez from jerboa21/jerboa ─────────────────
+FROM qt-shim-alpine AS builder
+
+COPY --from=jerboa-runtime /build/chez-musl /opt/chez
+COPY --from=jerboa-runtime /usr/local /opt/chez-dynamic
+COPY --from=jerboa-runtime /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6
+COPY --from=jerboa-runtime /lib/x86_64-linux-gnu/libtinfo.so.6 /lib/x86_64-linux-gnu/libtinfo.so.6
+COPY --from=jerboa-runtime /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6
+COPY --from=jerboa-runtime /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2
+
+RUN mkdir -p /opt/qt-shim && \
+    cp -a /tmp/qt-shim-out/. /opt/qt-shim/ && \
+    cp -L /opt/qt-static-deps/lib/*.a /usr/local/lib/ && \
+    if [ -e /usr/lib/ld-musl-x86_64.so.1 ]; then \
+      ln -sf /usr/lib/ld-musl-x86_64.so.1 /lib/libc.musl-x86_64.so.1; \
+    fi && \
+    if [ -e /usr/lib/ld-musl-aarch64.so.1 ]; then \
+      ln -sf /usr/lib/ld-musl-aarch64.so.1 /lib/libc.musl-aarch64.so.1; \
+    fi && \
+    chez_dir="$(ls -d /opt/chez/lib/csv*/ta6le | head -1)" && \
+    printf '%s\n' \
+      '#!/bin/sh' \
+      "exec \"$chez_dir/scheme\" --boot \"$chez_dir/petite.boot\" --boot \"$chez_dir/scheme.boot\" \"\$@\"" \
+      > /opt/chez/bin/scheme-wrapper && \
+    chmod +x /opt/chez/bin/scheme-wrapper && \
+    /opt/chez/bin/scheme-wrapper --version
+
+# Copy source and run the in-container build
+COPY . /src
+WORKDIR /src
+
+ARG JEMACS_FEATURES=""
+ENV JEMACS_FEATURES=${JEMACS_FEATURES}
+RUN musl_chez_dir="$(ls -d /opt/chez/lib/csv*/ta6le | head -1)" && \
+    make linux-qt-local \
+      MUSL_CHEZ_ROOT=/opt/chez \
+      MUSL_CHEZ_DIR="$musl_chez_dir" \
+      MUSL_SCHEME=/opt/chez-dynamic/bin/scheme
+
+# Verify
+RUN ./jemacs-qt --version && \
+    file jemacs-qt && \
+    echo "--- Binary info ---" && \
+    ls -lh jemacs-qt
+
+# ── Final stage: extract binary ───────────────────────────────────────────────
+FROM ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
+COPY --from=builder /src/jemacs-qt /out/jemacs-qt
+CMD ["cat", "/out/jemacs-qt"]
diff --git a/Containerfile.tui b/Containerfile.tui
new file mode 100644
index 0000000..c098c8c
--- /dev/null
+++ b/Containerfile.tui
@@ -0,0 +1,29 @@
+# Podman-compatible container recipe for building static jemacs (TUI).
+#
+# Produces a fully static jemacs binary with zero runtime dependencies.
+#
+# Usage:
+#   podman build -f Containerfile.tui -t jemacs-tui-builder .
+#   id=$(podman create jemacs-tui-builder)
+#   podman cp $id:/out/jemacs ./jemacs && podman rm $id
+#   chmod +x jemacs
+
+FROM jerboa21/jerboa@sha256:9c4034f1296601d5a9f7b219a0decdd5d62862a2be4347976b3d96b13ed7701e AS builder
+
+# ── Copy jerboa-emacs source ────────────────────────────────────────────────
+COPY . /src
+
+# ── Build jemacs-musl ────────────────────────────────────────────────────────
+WORKDIR /src
+RUN make linux-tui-local
+
+# ── Verify ───────────────────────────────────────────────────────────────────
+RUN ./jemacs --version && \
+    file jemacs && \
+    echo "--- Binary info ---" && \
+    ls -lh jemacs
+
+# ── Output ───────────────────────────────────────────────────────────────────
+FROM ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
+COPY --from=builder /src/jemacs /out/jemacs
+CMD ["cat", "/out/jemacs"]
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 3b4f957..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,337 +0,0 @@
-# Intermediate image for jemacs-qt static builds.
-# Bakes static Qt6 + QScintilla from source, plus all Chez Scheme
-# dependencies (jerboa, jsh, jerboa-pcre2, jerboa-scintilla,
-# jerboa-qt) so that subsequent jemacs-qt builds only compile jemacs
-# itself (~5-10 min instead of ~30 min).
-#
-ARG ARCH=x86_64
-FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d
-
-# ── Phase 1: Alpine build deps ──────────────────────────────────────────
-RUN apk add --no-cache \
-    su-exec \
-    cmake samurai perl python3 linux-headers patchelf \
-    libxcb-dev xcb-util-dev xcb-util-image-dev \
-    xcb-util-keysyms-dev xcb-util-renderutil-dev \
-    xcb-util-wm-dev xcb-util-cursor-dev \
-    libx11-dev libxkbcommon-dev \
-    fontconfig-dev freetype-dev harfbuzz-dev \
-    libpng-dev zlib-dev mesa-dev \
-    pcre2-dev \
-    at-spi2-core-dev libdrm-dev \
-    zlib-static libxcb-static \
-    fontconfig-static freetype-static harfbuzz-static \
-    libpng-static bzip2-static expat-static brotli-static \
-    libx11-static graphite2-static libxkbcommon-static \
-    ncurses-dev ncurses-static \
-    util-linux-dev util-linux-static \
-    libvterm-dev libvterm-static \
-    gcc g++ binutils make git curl wget
-
-# Build static libXau (no Alpine -static package available)
-ARG LIBXAU_SHA256=74d0e4dfa3d39ad8939e99bda37f5967aba528211076828464d2777d477fc0fb
-RUN apk add --no-cache libxau-dev && \
-    cd /tmp && \
-    wget -q https://xorg.freedesktop.org/releases/individual/lib/libXau-1.0.12.tar.xz && \
-    echo "${LIBXAU_SHA256}  libXau-1.0.12.tar.xz" | sha256sum -c - && \
-    tar xf libXau-1.0.12.tar.xz && \
-    cd libXau-1.0.12 && \
-    ./configure --prefix=/usr --enable-static --disable-shared && \
-    make -j$(nproc) && make install && \
-    cd / && rm -rf /tmp/libXau-1.0.12*
-
-# Build static libxcb-util (no Alpine -static package; needed by xcb-image)
-ARG XCB_UTIL_SHA256=5abe3bbbd8e54f0fa3ec945291b7e8fa8cfd3cccc43718f8758430f94126e512
-RUN cd /tmp && \
-    wget -q https://xcb.freedesktop.org/dist/xcb-util-0.4.1.tar.xz && \
-    echo "${XCB_UTIL_SHA256}  xcb-util-0.4.1.tar.xz" | sha256sum -c - && \
-    tar xf xcb-util-0.4.1.tar.xz && \
-    cd xcb-util-0.4.1 && \
-    ./configure --prefix=/usr --enable-static --disable-shared && \
-    make -j$(nproc) && make install && \
-    cd / && rm -rf /tmp/xcb-util-0.4.1*
-
-# Build static OpenSSL (needed by jerboa-ssl for AWS API calls)
-# Alpine's openssl-dev only ships shared libs; we need .a files for the static binary.
-ARG OPENSSL_VERSION=3.5.7
-ARG OPENSSL_SHA256=a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8
-RUN apk add --no-cache openssl-dev perl && \
-    wget -q "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" && \
-    echo "${OPENSSL_SHA256}  openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c - && \
-    tar xf "openssl-${OPENSSL_VERSION}.tar.gz" && \
-    cd "openssl-${OPENSSL_VERSION}" && \
-    ./Configure no-shared no-tests -O2 --prefix=/usr && \
-    make -j$(nproc) && \
-    ./apps/openssl version | grep -F "OpenSSL ${OPENSSL_VERSION}" && \
-    ./apps/openssl version -a > /usr/lib/jemacs-openssl-build.txt && \
-    cp libssl.a libcrypto.a /usr/lib/ && \
-    cd / && rm -rf "openssl-${OPENSSL_VERSION}" "openssl-${OPENSSL_VERSION}.tar.gz"
-
-# ── Phase 2: Build Qt6 qtbase static ────────────────────────────────────
-ARG QT6_VERSION=6.8.3
-ARG QTBASE_SHA256=56001b905601bb9023d399f3ba780d7fa940f3e4861e496a7c490331f49e0b80
-RUN wget -q https://download.qt.io/official_releases/qt/6.8/${QT6_VERSION}/submodules/qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
-    echo "${QTBASE_SHA256}  qtbase-everywhere-src-${QT6_VERSION}.tar.xz" | sha256sum -c - && \
-    tar xf qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
-    rm qtbase-everywhere-src-${QT6_VERSION}.tar.xz && \
-    cmake -S qtbase-everywhere-src-${QT6_VERSION} -B qt6-build -G Ninja \
-      -DCMAKE_BUILD_TYPE=Release \
-      -DCMAKE_INSTALL_PREFIX=/opt/qt6-static \
-      -DBUILD_SHARED_LIBS=OFF \
-      -DQT_BUILD_EXAMPLES=OFF \
-      -DQT_BUILD_TESTS=OFF \
-      -DQT_BUILD_BENCHMARKS=OFF \
-      -DFEATURE_xcb=ON \
-      -DFEATURE_sql=OFF \
-      -DFEATURE_network=OFF \
-      -DFEATURE_testlib=OFF \
-      -DFEATURE_printsupport=ON \
-      -DFEATURE_dbus=OFF \
-      -DFEATURE_opengl=OFF \
-      -DFEATURE_vulkan=OFF \
-      -DFEATURE_glib=OFF \
-      -DFEATURE_icu=OFF && \
-    cmake --build qt6-build --parallel && \
-    cmake --install qt6-build && \
-    rm -rf qtbase-everywhere-src-${QT6_VERSION} qt6-build
-
-# ── Phase 3: Build QScintilla static ────────────────────────────────────
-ARG QSCI_VERSION=2.14.1
-ARG QSCINTILLA_SHA256=dfe13c6acc9d85dfcba76ccc8061e71a223957a6c02f3c343b30a9d43a4cdd4d
-RUN wget -q https://www.riverbankcomputing.com/static/Downloads/QScintilla/${QSCI_VERSION}/QScintilla_src-${QSCI_VERSION}.tar.gz && \
-    echo "${QSCINTILLA_SHA256}  QScintilla_src-${QSCI_VERSION}.tar.gz" | sha256sum -c - && \
-    tar xf QScintilla_src-${QSCI_VERSION}.tar.gz && \
-    rm QScintilla_src-${QSCI_VERSION}.tar.gz && \
-    cd QScintilla_src-${QSCI_VERSION}/src && \
-    /opt/qt6-static/bin/qmake CONFIG+=staticlib && \
-    make -j$(nproc) && \
-    make install && \
-    cd / && rm -rf QScintilla_src-${QSCI_VERSION}
-
-# ── Phase 4: Generate pkg-config files with full transitive deps ─────────
-# Static Qt6 cmake doesn't generate .pc files.  We extract direct deps
-# from .prl files, then add known transitive deps manually.
-RUN mkdir -p /opt/qt6-static/lib/pkgconfig && \
-    prl_libs() { \
-      grep '^QMAKE_PRL_LIBS ' "$1" | \
-        sed 's/^QMAKE_PRL_LIBS *= *//' | \
-        tr ' ' '\n' | grep '^-l' | \
-        grep -v '^-lQt6' | tr '\n' ' '; \
-    } && \
-    CORE_PRIVATE=$(prl_libs /opt/qt6-static/lib/libQt6Core.prl) && \
-    GUI_PRL=$(prl_libs /opt/qt6-static/lib/libQt6Gui.prl) && \
-    XCB_PRL=$(prl_libs /opt/qt6-static/plugins/platforms/libqxcb.prl) && \
-    TRANSITIVE="-lgraphite2 -lbz2 -lbrotlidec -lbrotlicommon -lexpat -lXau -lXdmcp" && \
-    GUI_PRIVATE="$GUI_PRL $TRANSITIVE" && \
-    XCB_TRANSITIVE="-lxcb-util -lxcb -lXau -lXdmcp" && \
-    XCB_PRIVATE="$XCB_PRL $TRANSITIVE $XCB_TRANSITIVE" && \
-    echo "Core deps: $CORE_PRIVATE" && \
-    echo "Gui deps: $GUI_PRIVATE" && \
-    echo "XCB deps: $XCB_PRIVATE" && \
-    printf '%s\n' \
-      'prefix=/opt/qt6-static' \
-      'includedir=${prefix}/include' \
-      'libdir=${prefix}/lib' \
-      '' \
-      'Name: Qt6Core' 'Description: Qt6 Core' 'Version: 6.8.3' \
-      'Cflags: -I${includedir} -I${includedir}/QtCore' \
-      "Libs: -L\${libdir} -lQt6Core" \
-      "Libs.private: $CORE_PRIVATE" \
-      > /opt/qt6-static/lib/pkgconfig/Qt6Core.pc && \
-    printf '%s\n' \
-      'prefix=/opt/qt6-static' \
-      'includedir=${prefix}/include' \
-      'libdir=${prefix}/lib' \
-      '' \
-      'Name: Qt6Gui' 'Description: Qt6 Gui' 'Version: 6.8.3' \
-      'Requires: Qt6Core' \
-      'Cflags: -I${includedir} -I${includedir}/QtGui' \
-      "Libs: -L\${libdir} -lQt6Gui" \
-      "Libs.private: $GUI_PRIVATE" \
-      > /opt/qt6-static/lib/pkgconfig/Qt6Gui.pc && \
-    printf '%s\n' \
-      'prefix=/opt/qt6-static' \
-      'includedir=${prefix}/include' \
-      'libdir=${prefix}/lib' \
-      '' \
-      'Name: Qt6Widgets' 'Description: Qt6 Widgets' 'Version: 6.8.3' \
-      'Requires: Qt6Gui' \
-      'Cflags: -I${includedir} -I${includedir}/QtWidgets -I${includedir}/QtGui -I${includedir}/QtCore' \
-      "Libs: -L\${libdir} -lQt6Widgets" \
-      > /opt/qt6-static/lib/pkgconfig/Qt6Widgets.pc && \
-    printf '%s\n' \
-      'prefix=/opt/qt6-static' \
-      'includedir=${prefix}/include' \
-      'libdir=${prefix}/lib' \
-      '' \
-      'Name: Qt6PrintSupport' 'Description: Qt6 PrintSupport' 'Version: 6.8.3' \
-      'Requires: Qt6Widgets' \
-      'Cflags: -I${includedir} -I${includedir}/QtPrintSupport' \
-      "Libs: -L\${libdir} -lQt6PrintSupport" \
-      > /opt/qt6-static/lib/pkgconfig/Qt6PrintSupport.pc && \
-    printf '%s\n' \
-      'prefix=/opt/qt6-static' \
-      'includedir=${prefix}/include' \
-      'libdir=${prefix}/lib' \
-      'plugindir=${prefix}/plugins' \
-      '' \
-      'Name: Qt6XcbPlugin' 'Description: Qt6 XCB platform plugin' 'Version: 6.8.3' \
-      'Requires: Qt6Gui' \
-      "Libs: -L\${libdir} -L\${plugindir}/platforms -lqxcb -lQt6XcbQpa" \
-      "Libs.private: $XCB_PRIVATE" \
-      > /opt/qt6-static/lib/pkgconfig/Qt6XcbPlugin.pc && \
-    printf '%s\n' \
-      'prefix=/opt/qt6-static' \
-      'includedir=${prefix}/include' \
-      'libdir=${prefix}/lib' \
-      '' \
-      'Name: QScintilla' 'Description: QScintilla for Qt6' 'Version: 2.14.1' \
-      'Requires: Qt6Widgets Qt6PrintSupport' \
-      'Cflags: -I${includedir} -I${includedir}/Qsci' \
-      "Libs: -L\${libdir} -lqscintilla2_qt6" \
-      > /opt/qt6-static/lib/pkgconfig/QScintilla.pc && \
-    echo "Generated .pc files:" && ls /opt/qt6-static/lib/pkgconfig/
-
-# ── Phase 5: Build Chez Scheme from source for musl ─────────────────────
-# Build WITHOUT --static so libkernel.a retains full dlopen support.
-# This is needed because jemacs embeds the program as a .so and loads it
-# at runtime via Sscheme_script (which calls dlopen internally).
-# musl's static libdl.a provides dlopen in the final static binary.
-ARG CHEZ_COMMIT=fdf6b3f5d069bf53082bb827f46714f2de8f11f5
-RUN git init -q /tmp/ChezScheme && \
-    git -C /tmp/ChezScheme remote add origin https://github.com/cisco/ChezScheme && \
-    git -C /tmp/ChezScheme fetch -q --depth 1 origin ${CHEZ_COMMIT} && \
-    test "$(git -C /tmp/ChezScheme rev-parse FETCH_HEAD)" = "${CHEZ_COMMIT}" && \
-    git -C /tmp/ChezScheme checkout -q --detach ${CHEZ_COMMIT} && \
-    cd /tmp/ChezScheme && \
-    ./configure --threads --installprefix=/opt/chez && \
-    make -j$(nproc) && \
-    make install && \
-    rm -rf /tmp/ChezScheme
-
-# ── Phase 5.5: Build tree-sitter + grammars (static) ──────────────────
-ARG TS_VERSION=0.24.7
-ARG TS_COMMIT=5e8760bf462ce7b19b3d2396d5b7860f3906a297
-RUN git init -q /tmp/tree-sitter && \
-    git -C /tmp/tree-sitter remote add origin https://github.com/tree-sitter/tree-sitter && \
-    git -C /tmp/tree-sitter fetch -q --depth 1 origin ${TS_COMMIT} && \
-    test "$(git -C /tmp/tree-sitter rev-parse FETCH_HEAD)" = "${TS_COMMIT}" && \
-    git -C /tmp/tree-sitter checkout -q --detach ${TS_COMMIT} && \
-    cd /tmp/tree-sitter && \
-    cc -c -O2 -Ilib/include lib/src/lib.c -o lib/src/lib.o && \
-    mkdir -p /opt/tree-sitter-lib && \
-    ar rcs /opt/tree-sitter-lib/libtree-sitter.a lib/src/lib.o && \
-    mkdir -p /opt/tree-sitter-include/tree_sitter && \
-    cp lib/include/tree_sitter/api.h /opt/tree-sitter-include/tree_sitter/ && \
-    rm -rf /tmp/tree-sitter
-
-# Build tree-sitter grammar static archives
-RUN mkdir -p /opt/tree-sitter-grammars && \
-    build_grammar() { \
-      name=$1; repo=$2; commit=$3; subdir=${4:-.}; \
-      git init -q /tmp/ts-$name && \
-      git -C /tmp/ts-$name remote add origin "https://github.com/$repo" && \
-      git -C /tmp/ts-$name fetch -q --depth 1 origin "$commit" && \