diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..146ace2
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,50 @@
+name: Build MARS.COM
+
+on:
+ push:
+ branches: ["**"]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Cache the JWasm toolchain
+ uses: actions/cache@v4
+ with:
+ path: .toolchain
+ key: jwasm-${{ hashFiles('versions.env') }}
+
+ - name: Install mtools (needed to build the FreeDOS floppy image)
+ run: sudo apt-get update -qq && sudo apt-get install -y -qq mtools
+
+ - name: Assemble MARS.COM and verify reproducibility
+ run: bash scripts/build.sh --check
+
+ - name: Run the test suite
+ run: bash tests/run-tests.sh
+
+ - name: Report size and checksum
+ run: |
+ size=$(wc -c < build/MARS.COM | tr -d ' ')
+ sha=$(sha256sum build/MARS.COM | cut -d' ' -f1)
+ {
+ echo "### MARS.COM"
+ echo ""
+ echo "| | |"
+ echo "|---|---|"
+ echo "| Size | $size bytes |"
+ echo "| SHA-256 | \`$sha\` |"
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: MARS.COM
+ path: build/MARS.COM
+ if-no-files-found: error
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..fb5acb0
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,54 @@
+name: Deploy Pages
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Let a running deploy finish rather than cancelling it mid-flight.
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Cache the JWasm toolchain
+ uses: actions/cache@v4
+ with:
+ path: .toolchain
+ key: jwasm-${{ hashFiles('versions.env') }}
+
+ - name: Install mtools (needed to build the FreeDOS floppy image)
+ run: sudo apt-get update -qq && sudo apt-get install -y -qq mtools
+
+ - name: Build the site
+ run: bash scripts/build-site.sh
+
+ - uses: actions/configure-pages@v5
+
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: _site
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1c90d17
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+# Build outputs — MARS.ASM is the single source of truth
+build/
+_site/
+.toolchain/
+MARS.COM
+*.jsdos
+
+# Fetched dependencies
+node_modules/
+*.tgz
+
+# OS turds
+.DS_Store
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9b720c3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,31 @@
+# Thin wrappers over scripts/. Every target is also runnable directly.
+.PHONY: all build image v86 site serve test clean help
+
+all: site
+
+help: ## List the available targets
+ @grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) \
+ | sed -e 's/:.*##/\t/' \
+ | awk -F'\t' '{ printf " \033[1m%-8s\033[0m %s\n", $$1, $$2 }'
+
+build: ## Assemble MARS.ASM into build/MARS.COM
+ @bash scripts/build.sh
+
+image: build ## Build the bootable FreeDOS floppy (build/mars.img)
+ @bash scripts/make-image.sh
+
+v86: ## Fetch the pinned v86 runtime and BIOS blobs
+ @bash scripts/fetch-v86.sh
+
+site: ## Compose the deployable site into _site/
+ @bash scripts/build-site.sh
+
+serve: ## Build the site and serve it at http://localhost:8080
+ @bash scripts/serve.sh
+
+test: ## Run the test suite
+ @bash tests/run-tests.sh
+
+clean: ## Remove build outputs (keeps the cached toolchain)
+ @rm -rf build _site
+ @echo "Removed build/ and _site/ (run 'rm -rf .toolchain' to drop the assembler too)"
diff --git a/README.md b/README.md
index 712ee16..05606b4 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,89 @@
# MARS landscape
-A comprehensive study of the outstanding code example from 1993 - the martian landscape renderer by Tim J. Clarke.
+A comprehensive study of the outstanding code example from 1993 — the martian
+landscape renderer by Tim J. Clarke.
-Original code has been disassembled, rewritten and reduced from **5649** bytes to... **1517 bytes**! :)
+Original code has been disassembled, rewritten and reduced from **5649** bytes
+to about **1.5 kB**! :)
-**Tim, if you read this, please contact me! I was trying to reach you, but I couldn't...**
+**Tim, if you read this, please contact me! I was trying to reach you, but I
+couldn't...**
-
+
-It works on DOSBOX and also on genuine x86 machines (mouse is needed). More details can be found [here](https://chaos.if.uj.edu.pl/~wojtek/MARS.COM).
+## ▶ Run it in your browser
+
+****
+
+Click the canvas to capture the mouse — moving it pans the camera.
+
+## Building and running it here
+
+Alongside the annotated assembly, this repository can build the binary and
+publish a browser-playable page:
+
+- `scripts/build.sh` — assembles `MARS.ASM` into `MARS.COM`
+- `scripts/build-site.sh` — composes the GitHub Pages site
+- GitHub Actions workflows that verify the build and deploy the page
+
+`MARS.ASM` remains the single source of truth; the binary is never committed.
+
+## Building
+
+```sh
+make build # assemble build/MARS.COM
+make serve # build the site and preview at http://localhost:8080
+make test # run the test suite
+```
+
+`MARS.ASM` is MASM/TASM dialect (`.model tiny`, `org 100h`, `COMMENT #`
+blocks), so it needs a MASM-compatible assembler — NASM cannot build it. The
+build uses [JWasm](https://github.com/Baron-von-Riedesel/JWasm), pinned in
+`versions.env`. If no assembler is on your `PATH`, `build.sh` compiles JWasm
+from source into `.toolchain/` automatically. With no C compiler available,
+`bash scripts/build.sh --docker` runs the whole build in a container.
+
+On macOS the JWasm build needs two portability fixes (its makefile targets
+Linux/FreeBSD); `build.sh` applies them automatically.
+
+Building the site also needs `mtools` (`brew install mtools` /
+`apt-get install mtools`) to write `MARS.COM` into the floppy image.
+
+### How the browser player works
+
+The page boots a real emulated PC rather than emulating DOS. `make image`
+takes the 720 KB FreeDOS boot floppy used by v86's own demos and injects
+`MARS.COM`, the CuteMouse driver, and an `AUTOEXEC.BAT` that loads the driver
+and runs the program. Both downloads are checksum-pinned in `versions.env`.
+
+The mouse driver is not optional: MARS calls `int 33h`, which DOSBox-based
+players implement internally but hardware emulation does not. Without a
+resident driver the landscape still renders — MARS checks for the driver and
+degrades gracefully — but the camera never moves.
+
+### Build output
+
+The pinned toolchain produces a **1550-byte** `MARS.COM`
+(`sha256:10a1bb6c…`), byte-identical under JWasm v2.20 and v2.21 and across
+macOS/clang/ARM64 and Linux/gcc. Upstream's README reports **1517** bytes; the
+33-byte difference comes from the original author's toolchain and has not been
+reconciled. `scripts/build.sh --check` enforces the 1550-byte result so
+unintended changes to `MARS.ASM` are caught.
+
+## Running it natively
+
+It works on DOSBox and on genuine x86 machines (a mouse is needed). Under
+DOSBox, always use `-machine vgaonly` — the renderer drives VGA mode 13h and
+reprograms the palette DAC, and the default `svga_s3` emulation shows
+artifacts. More details are on the
+[author's page](https://chaos.if.uj.edu.pl/~wojtek/MARS.COM).
+
+## Credits and licence
+
+- Original martian landscape renderer — **Tim J. Clarke**, 1993
+- Disassembly, rewrite and size reduction — **Wojciech Bruzda**, 2021
+- Browser emulation — [v86](https://github.com/copy/v86) (BSD), booting
+ [FreeDOS](https://www.freedos.org/) with the
+ [CuteMouse](https://cutemouse.sourceforge.net/) driver (GPL)
+
+Released under GPL-3.0, as upstream.
diff --git a/scripts/build-site.sh b/scripts/build-site.sh
new file mode 100755
index 0000000..0c69079
--- /dev/null
+++ b/scripts/build-site.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# Composes the deployable site from the bootable image, the v86 runtime and
+# the static page sources.
+#
+# Everything the browser loads is copied into _site/, so the deployed page
+# makes no third-party requests at view time.
+. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
+
+log "Building MARS.COM"
+bash "$REPO_ROOT/scripts/build.sh"
+
+log "Building the bootable floppy image"
+bash "$REPO_ROOT/scripts/make-image.sh"
+
+log "Fetching v86 $V86_VERSION"
+bash "$REPO_ROOT/scripts/fetch-v86.sh"
+
+log "Composing $SITE_OUT"
+rm -rf "$SITE_OUT"
+mkdir -p "$SITE_OUT"
+
+cp "$REPO_ROOT/site/index.html" "$SITE_OUT/index.html"
+cp "$REPO_ROOT/site/style.css" "$SITE_OUT/style.css"
+cp "$REPO_ROOT/site/favicon.svg" "$SITE_OUT/favicon.svg"
+cp "$BUILD_DIR/mars.img" "$SITE_OUT/mars.img"
+cp "$BUILD_DIR/MARS.COM" "$SITE_OUT/MARS.COM"
+cp "$REPO_ROOT/MARS.ASM" "$SITE_OUT/MARS.ASM"
+cp "$REPO_ROOT/mars_4_3.png" "$SITE_OUT/mars_4_3.png"
+cp -R "$BUILD_DIR/v86" "$SITE_OUT/v86"
+
+# GitHub Pages runs Jekyll by default, which strips paths beginning with a
+# dot and can mangle asset directories. .nojekyll turns that off.
+touch "$SITE_OUT/.nojekyll"
+
+log "Site ready in $SITE_OUT ($(du -sh "$SITE_OUT" | cut -f1))"
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000..86d3ce5
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,145 @@
+#!/usr/bin/env bash
+# Assembles MARS.ASM into a runnable DOS .COM image.
+#
+# MARS.ASM is MASM/TASM dialect (.model tiny, org 100h, COMMENT # blocks),
+# so it needs a MASM-compatible assembler — NASM cannot build it. We use
+# JWasm, pinned in versions.env. Because the model is tiny, the assembler
+# emits the .COM memory image directly; there is no link step.
+. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/build.sh [--docker] [--check]
+
+ --docker Run the build inside a container (no host compiler needed).
+ --check Verify the output matches the pinned size and SHA-256.
+
+Assembler resolution order:
+ 1. $JWASM environment variable
+ 2. jwasm on PATH
+ 3. build JWasm from source into .toolchain/ (needs gcc, make, git)
+EOF
+}
+
+use_docker=0
+do_check=0
+for arg in "$@"; do
+ case "$arg" in
+ --docker) use_docker=1 ;;
+ --check) do_check=1 ;;
+ -h|--help) usage; exit 0 ;;
+ *) die "unknown option: $arg (try --help)" ;;
+ esac
+done
+
+if [ "$use_docker" -eq 1 ]; then
+ need docker
+ log "Building inside debian:bookworm-slim"
+ docker run --rm -v "$REPO_ROOT:/repo" -w /repo debian:bookworm-slim bash -c '
+ set -e
+ apt-get update -qq >/dev/null
+ apt-get install -y -qq build-essential git ca-certificates >/dev/null 2>&1
+ bash scripts/build.sh
+ '
+ exit $?
+fi
+
+# --- resolve an assembler -----------------------------------------------
+resolve_jwasm() {
+ if [ -n "${JWASM:-}" ]; then
+ [ -x "$JWASM" ] || die "\$JWASM is set to '$JWASM' but that is not executable"
+ printf '%s' "$JWASM"
+ return
+ fi
+
+ if command -v jwasm >/dev/null 2>&1; then
+ command -v jwasm
+ return
+ fi
+
+ local cached="$TOOLCHAIN_DIR/bin/jwasm"
+ if [ -x "$cached" ]; then
+ printf '%s' "$cached"
+ return
+ fi
+
+ log "No assembler found; building JWasm $JWASM_TAG from source"
+ need git
+ need make
+ command -v cc >/dev/null 2>&1 || command -v gcc >/dev/null 2>&1 \
+ || die "no C compiler found. Install gcc/clang, set \$JWASM to a jwasm binary, or rerun with --docker"
+
+ local src="$TOOLCHAIN_DIR/JWasm"
+ local buildlog="$TOOLCHAIN_DIR/jwasm-build.log"
+ rm -rf "$src"
+ mkdir -p "$TOOLCHAIN_DIR/bin"
+ git clone -q "$JWASM_REPO" "$src" >&2
+ ( cd "$src" && git checkout -q "$JWASM_TAG" ) \
+ || die "could not check out JWasm $JWASM_TAG"
+
+ # GccUnix.mak targets Linux/FreeBSD, so it needs two fixes on macOS:
+ #
+ # 1. It includes , a glibc location. macOS declares malloc in
+ # . Rather than editing 58 source files, we prepend an
+ # include directory holding a malloc.h that forwards to stdlib.h.
+ # 2. It links with -s and -Wl,-Map, both GNU ld flags that Apple's ld
+ # rejects. These are hardcoded in the recipe, not in a variable, so
+ # they have to be stripped from our throwaway checkout.
+ #
+ # Both are confined to .toolchain/, which is gitignored. Verified to yield
+ # a binary byte-identical to the Linux build.
+ local inc_dirs="-Isrc/H"
+ if [ "$(uname -s)" = "Darwin" ]; then
+ local shim="$TOOLCHAIN_DIR/shim"
+ mkdir -p "$shim"
+ printf '/* Darwin shim: malloc is declared in stdlib.h, not malloc.h */\n#include \n' \
+ > "$shim/malloc.h"
+ sed -i.bak -e 's/ -Wl,-Map,[^ ]*//' -e 's/ -s -o / -o /' "$src/GccUnix.mak" \
+ || die "could not patch GccUnix.mak for macOS"
+ inc_dirs="-Isrc/H -I$shim"
+ log "Applied macOS portability patches to the JWasm checkout"
+ fi
+
+ ( cd "$src" && make -f GccUnix.mak inc_dirs="$inc_dirs" ) >"$buildlog" 2>&1 \
+ || die "JWasm build failed; see $buildlog. Rerun with --docker, or set \$JWASM to a prebuilt binary."
+
+ local built
+ built="$(find "$src" -name jwasm -type f -perm -u+x | head -1)"
+ [ -n "$built" ] || die "JWasm built but no 'jwasm' binary was produced"
+ cp "$built" "$cached"
+ printf '%s' "$cached"
+}
+
+JWASM_BIN="$(resolve_jwasm)"
+log "Assembler: $JWASM_BIN"
+
+# --- assemble ------------------------------------------------------------
+mkdir -p "$BUILD_DIR"
+out="$BUILD_DIR/MARS.COM"
+rm -f "$out"
+
+# -bin: raw binary output, which for .model tiny is exactly a .COM image.
+"$JWASM_BIN" -bin -Fo="$out" "$REPO_ROOT/MARS.ASM" \
+ || die "assembly failed"
+
+[ -s "$out" ] || die "assembler produced an empty $out"
+
+size=$(wc -c < "$out" | tr -d ' ')
+[ "$size" -le "$MARS_COM_MAX_SIZE" ] \
+ || die "$out is $size bytes, which exceeds the $MARS_COM_MAX_SIZE byte .COM ceiling"
+
+if command -v shasum >/dev/null 2>&1; then
+ sha=$(shasum -a 256 "$out" | cut -d' ' -f1)
+else
+ sha=$(sha256sum "$out" | cut -d' ' -f1)
+fi
+
+log "Built $out — $size bytes, sha256 $sha"
+
+if [ "$do_check" -eq 1 ]; then
+ [ "$size" = "$MARS_COM_SIZE" ] \
+ || die "size mismatch: expected $MARS_COM_SIZE bytes, got $size. If MARS.ASM changed on purpose, update versions.env."
+ [ "$sha" = "$MARS_COM_SHA256" ] \
+ || die "sha256 mismatch: expected $MARS_COM_SHA256, got $sha. If MARS.ASM changed on purpose, update versions.env."
+ log "Reproducibility check passed"
+fi
diff --git a/scripts/fetch-v86.sh b/scripts/fetch-v86.sh
new file mode 100755
index 0000000..6ce1c75
--- /dev/null
+++ b/scripts/fetch-v86.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+# Downloads a pinned v86 release plus the BIOS blobs it needs.
+#
+# This runs at BUILD time, never at page-view time — the deployed site serves
+# everything from its own origin, so it depends on no CDN once published.
+#
+# v86's npm package ships only the runtime (libv86.js + v86.wasm); the BIOS
+# images live in the GitHub repo, so they are fetched separately.
+. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
+
+need npm
+need tar
+need curl
+
+dest="$BUILD_DIR/v86"
+work="$BUILD_DIR/.v86-download"
+
+if [ -f "$dest/libv86.js" ] && [ -f "$dest/bios/seabios.bin" ] && [ "${FORCE_FETCH:-0}" != "1" ]; then
+ log "v86 $V86_VERSION already present in $dest (set FORCE_FETCH=1 to refetch)"
+ exit 0
+fi
+
+rm -rf "$work" "$dest"
+mkdir -p "$work" "$dest/bios"
+
+log "Fetching v86 $V86_VERSION from npm"
+( cd "$work" && npm pack "v86@$V86_VERSION" >/dev/null ) \
+ || die "npm pack failed for v86@$V86_VERSION"
+
+tarball="$work/v86-$V86_VERSION.tgz"
+[ -f "$tarball" ] || die "expected tarball $tarball was not produced"
+verify_sha "$tarball" "$V86_TARBALL_SHA256" "v86 npm tarball"
+
+tar xzf "$tarball" -C "$work" \
+ package/build/libv86.js \
+ package/build/v86.wasm \
+ || die "v86 tarball did not contain the expected build layout"
+
+cp "$work/package/build/libv86.js" "$dest/libv86.js"
+cp "$work/package/build/v86.wasm" "$dest/v86.wasm"
+
+# SeaBIOS and the VGA BIOS are not in the npm package.
+log "Fetching BIOS blobs from $V86_BIOS_REPO@$V86_BIOS_REF"
+fetch_bios() {
+ local blob="$1" expected="$2"
+ curl -sSfL --max-time 120 \
+ "https://raw.githubusercontent.com/$V86_BIOS_REPO/$V86_BIOS_REF/bios/$blob" \
+ -o "$dest/bios/$blob" \
+ || die "could not fetch bios/$blob"
+ [ -s "$dest/bios/$blob" ] || die "bios/$blob came back empty"
+ verify_sha "$dest/bios/$blob" "$expected" "bios/$blob"
+}
+
+fetch_bios seabios.bin "$V86_SEABIOS_SHA256"
+fetch_bios vgabios.bin "$V86_VGABIOS_SHA256"
+
+rm -rf "$work"
+
+log "v86 $V86_VERSION staged in $dest ($(du -sh "$dest" | cut -f1))"
diff --git a/scripts/lib.sh b/scripts/lib.sh
new file mode 100644
index 0000000..bd82358
--- /dev/null
+++ b/scripts/lib.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# Shared helpers for every script in this repo. Source it, don't execute it.
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+BUILD_DIR="$REPO_ROOT/build"
+SITE_OUT="$REPO_ROOT/_site"
+TOOLCHAIN_DIR="$REPO_ROOT/.toolchain"
+
+# shellcheck source=/dev/null
+. "$REPO_ROOT/versions.env"
+
+log() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; }
+die() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; exit 1; }
+need() { command -v "$1" >/dev/null 2>&1 || die "required tool not found: $1"; }
+
+sha256_of() {
+ if command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$1" | cut -d' ' -f1
+ else
+ sha256sum "$1" | cut -d' ' -f1
+ fi
+}
+
+# Every third-party download must pass through this. Anything fetched over
+# the network and then executed — a BIOS blob, a DOS driver, a wasm runtime —
+# is pinned by content, not just by name or version.
+verify_sha() {
+ local file="$1" expected="$2" label="$3" actual
+ actual="$(sha256_of "$file")"
+ [ "$actual" = "$expected" ] \
+ || die "$label checksum mismatch: expected $expected, got $actual"
+}
diff --git a/scripts/make-image.sh b/scripts/make-image.sh
new file mode 100755
index 0000000..e54d0d1
--- /dev/null
+++ b/scripts/make-image.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+# Builds the bootable floppy image that v86 boots.
+#
+# Starts from the FreeDOS 720 KB boot floppy used by v86's own demos, then
+# injects three things with mtools:
+#
+# MARS.COM the program itself
+# CTMOUSE.EXE a resident int 33h mouse driver — DOSBox provided this
+# service internally, real hardware emulation does not
+# AUTOEXEC.BAT loads the driver, then runs MARS
+#
+# Both downloads are checksum-pinned, so a rebuild years from now either
+# produces the same image or fails loudly rather than drifting.
+. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
+
+need curl
+need unzip
+command -v mcopy >/dev/null 2>&1 \
+ || die "mtools not found — install it with 'brew install mtools' or 'apt-get install mtools'"
+
+# mtools is strict about geometry on these vintage images; this is expected.
+export MTOOLS_SKIP_CHECK=1
+
+work="$BUILD_DIR/.image-work"
+out="$BUILD_DIR/mars.img"
+
+com="$BUILD_DIR/MARS.COM"
+[ -f "$com" ] || die "$com not found — run scripts/build.sh first"
+
+rm -rf "$work"
+mkdir -p "$work"
+
+# --- FreeDOS boot floppy -------------------------------------------------
+log "Fetching the FreeDOS boot floppy"
+curl -sSfL --max-time 180 "$FREEDOS_IMG_URL" -o "$work/freedos.img" \
+ || die "could not download $FREEDOS_IMG_URL"
+verify_sha "$work/freedos.img" "$FREEDOS_IMG_SHA256" "FreeDOS image"
+
+# --- CuteMouse -----------------------------------------------------------
+log "Fetching the CuteMouse driver"
+curl -sSfL --max-time 180 "$CTMOUSE_URL" -o "$work/ctmouse.zip" \
+ || die "could not download $CTMOUSE_URL"
+verify_sha "$work/ctmouse.zip" "$CTMOUSE_ZIP_SHA256" "CuteMouse archive"
+
+unzip -o -q "$work/ctmouse.zip" -d "$work/ctmouse" \
+ || die "could not unpack the CuteMouse archive"
+ctmouse="$work/ctmouse/CTMOUSE.EXE"
+[ -f "$ctmouse" ] || die "CTMOUSE.EXE not found in the CuteMouse archive"
+
+# --- compose the image ---------------------------------------------------
+cp "$work/freedos.img" "$out"
+
+# The stock image ships demo programs that crowd a 720 KB floppy. Drop the
+# large ones so there is comfortable room, and so the boot is not cluttered.
+for junk in ::/VIM.EXE ::/NASM.EXE ::/DEBUG.COM ::/PRIMES.EXE ::/X86TEST.ASM \
+ ::/HELLO.ASM ::/CLOCK.COM ::/CAL.COM ::/FOO ::/README; do
+ mdel -i "$out" "$junk" >/dev/null 2>&1 || true
+done
+
+cat > "$work/AUTOEXEC.BAT" <<'EOF'
+@ECHO OFF
+CTMOUSE.EXE
+MARS.COM
+EOF
+# DOS expects CRLF line endings in batch files.
+awk 'BEGIN{RS="\n";ORS="\r\n"} {print}' "$work/AUTOEXEC.BAT" > "$work/AUTOEXEC.CRLF"
+mv "$work/AUTOEXEC.CRLF" "$work/AUTOEXEC.BAT"
+
+mcopy -i "$out" -o "$com" ::/MARS.COM || die "could not copy MARS.COM into the image"
+mcopy -i "$out" -o "$ctmouse" ::/CTMOUSE.EXE || die "could not copy CTMOUSE.EXE into the image"
+mcopy -i "$out" -o "$work/AUTOEXEC.BAT" ::/AUTOEXEC.BAT || die "could not write AUTOEXEC.BAT into the image"
+
+rm -rf "$work"
+
+log "Built $out ($(wc -c < "$out" | tr -d ' ') bytes)"
+mdir -i "$out" ::/ 2>/dev/null | grep -iE "MARS|CTMOUSE|AUTOEXEC|bytes free" >&2 || true
diff --git a/scripts/serve.sh b/scripts/serve.sh
new file mode 100755
index 0000000..d768dee
--- /dev/null
+++ b/scripts/serve.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Builds the site and serves it over HTTP.
+#
+# HTTP rather than file:// is required: js-dos loads mars.jsdos with fetch(),
+# and browsers block fetch on file:// origins.
+. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
+
+port="${PORT:-8080}"
+
+bash "$REPO_ROOT/scripts/build-site.sh"
+
+need python3
+log "Serving $SITE_OUT at http://localhost:$port (Ctrl-C to stop)"
+cd "$SITE_OUT"
+exec python3 -m http.server "$port"
diff --git a/site/favicon.svg b/site/favicon.svg
new file mode 100644
index 0000000..24e44eb
--- /dev/null
+++ b/site/favicon.svg
@@ -0,0 +1,5 @@
+
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..a225d01
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+MARS — a martian landscape in 1550 bytes
+
+
+
+
+
+
+
+
+
MARS
+
A martian landscape renderer from 1993, in 1550 bytes.
+
+
+
+
+
+
+
+
Booting FreeDOS…
+
+
+
+
+ Click the screen to capture your mouse — moving it pans the camera.
+ Press Esc to release.
+
+
+
+
What this is
+
+ In 1993 Tim J. Clarke wrote a real-time martian landscape
+ renderer that fit in a 5649-byte DOS executable. In 2021
+ Wojciech Bruzda disassembled it, rewrote it, and reduced it
+ to roughly a tenth of that size — the binary running above is
+ assembled from that annotated source.
+
+
+ It draws into VGA mode 13h, reprograms the palette DAC directly, and needs
+ 32-bit registers, so it requires a 386 or better. Rather than emulate DOS,
+ this page boots an actual FreeDOS floppy on an emulated PC.
+