diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/.github/workflows/rtl.yml b/.github/workflows/rtl.yml
new file mode 100644
index 0000000..e92718e
--- /dev/null
+++ b/.github/workflows/rtl.yml
@@ -0,0 +1,79 @@
+name: RTL CI
+
+on:
+ push:
+ paths:
+ - 'rtl/**'
+ - 'sim/**'
+ - '.github/workflows/rtl.yml'
+ pull_request:
+ paths:
+ - 'rtl/**'
+ - 'sim/**'
+ - '.github/workflows/rtl.yml'
+
+jobs:
+ rtl-ci:
+ runs-on: ubuntu-24.04
+
+ steps:
+ - uses: actions/checkout@v4
+
+ # cocotb 2.x requires Verilator >= 5.036; the ubuntu-24.04 apt package
+ # is only 5.020, so we build from source and cache by version tag.
+ - name: Cache Verilator build
+ id: cache-verilator
+ uses: actions/cache@v4
+ with:
+ path: ~/verilator-install
+ key: verilator-v5.036-ubuntu-24.04
+
+ - name: Build Verilator v5.036 from source
+ if: steps.cache-verilator.outputs.cache-hit != 'true'
+ run: |
+ sudo apt-get update -qq
+ sudo apt-get install -y autoconf flex bison libfl2 libfl-dev help2man perl
+ git clone --depth 1 --branch v5.036 https://github.com/verilator/verilator.git /tmp/verilator
+ cd /tmp/verilator
+ autoconf
+ ./configure --prefix="$HOME/verilator-install"
+ make -j$(nproc)
+ make install
+
+ - name: Add Verilator to PATH
+ run: echo "$HOME/verilator-install/bin" >> $GITHUB_PATH
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install Python deps
+ run: pip install cocotb numpy pytest
+
+ - name: Lint RTL
+ run: verilator --lint-only -Wall rtl/*.sv
+
+ - name: Golden model tests
+ run: pytest sim/golden.py -q
+
+ # Each suite gets its own SIM_BUILD directory. cocotb does not detect a
+ # TOPLEVEL change across runs sharing the same sim_build/ — it reuses the
+ # previous binary, which causes "root handle not found" at runtime.
+ - name: cocotb — PE
+ working-directory: sim
+ run: make MODULE=test_pe TOPLEVEL=pe WAVES=0 SIM_BUILD=sim_build/pe
+
+ - name: cocotb — Systolic Array
+ working-directory: sim
+ run: |
+ make MODULE=test_systolic_array TOPLEVEL=systolic_array \
+ VERILOG_SOURCES="../rtl/pe.sv ../rtl/systolic_array.sv" \
+ WAVES=0 SIM_BUILD=sim_build/systolic_array
+
+ - name: cocotb — Top (full matmul vs golden)
+ working-directory: sim
+ run: |
+ make MODULE=test_top TOPLEVEL=tiny_tpu_top \
+ VERILOG_SOURCES="../rtl/pe.sv ../rtl/systolic_array.sv \
+ ../rtl/controller.sv ../rtl/tiny_tpu_top.sv" \
+ WAVES=0 SIM_BUILD=sim_build/top
diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml
new file mode 100644
index 0000000..b0049dd
--- /dev/null
+++ b/.github/workflows/wasm.yml
@@ -0,0 +1,37 @@
+name: WASM Build
+
+on:
+ push:
+ paths:
+ - 'rtl/**'
+ - 'wasm/**'
+ - '.github/workflows/wasm.yml'
+ pull_request:
+ paths:
+ - 'rtl/**'
+ - 'wasm/**'
+ - '.github/workflows/wasm.yml'
+
+jobs:
+ wasm-build:
+ runs-on: ubuntu-24.04
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Verilator and build deps
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y verilator build-essential
+
+ - name: Set up Emscripten
+ uses: mymindstorm/setup-emsdk@v14
+
+ - name: Build WASM
+ run: bash wasm/build.sh
+
+ - name: Assert artifacts produced
+ run: |
+ test -f web/public/tiny_tpu.mjs || (echo "ERROR: tiny_tpu.mjs not produced" && exit 1)
+ test -f web/public/tiny_tpu.wasm || (echo "ERROR: tiny_tpu.wasm not produced" && exit 1)
+ echo "WASM artifacts verified: $(du -sh web/public/tiny_tpu.wasm | cut -f1) wasm"
diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml
new file mode 100644
index 0000000..6db9bce
--- /dev/null
+++ b/.github/workflows/web.yml
@@ -0,0 +1,44 @@
+name: Web CI
+
+on:
+ push:
+ paths:
+ - 'web/**'
+ - '.github/workflows/web.yml'
+ pull_request:
+ paths:
+ - 'web/**'
+ - '.github/workflows/web.yml'
+
+jobs:
+ web-ci:
+ runs-on: ubuntu-24.04
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ version: '11'
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'pnpm'
+ cache-dependency-path: web/pnpm-lock.yaml
+
+ - name: Install dependencies
+ working-directory: web
+ run: pnpm install --frozen-lockfile
+
+ - name: Lint
+ working-directory: web
+ run: pnpm lint
+
+ - name: Type check
+ working-directory: web
+ run: pnpm typecheck
+
+ - name: Build
+ working-directory: web
+ run: pnpm build
diff --git a/README.md b/README.md
index 825471c..b6177c2 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,12 @@