diff --git a/README.md b/README.md
index 0b499b4..f7911d3 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,19 @@ Stats are collected every 2 seconds in a background thread with minimal performa
+## Screenshots
+
+
+
+ Overview Connections table with live stats and sparklines
 |
+ Details Per-connection SNI, cipher, GeoIP, DPI
 |
+
+
+ Graph Traffic chart, app distribution, top processes
 |
+ Interfaces Per-interface RX/TX history with errors and drops
 |
+
+
+
## Quick Start
### Installation
diff --git a/assets/rustnet.gif b/assets/rustnet.gif
index 61bbf92..b211771 100644
Binary files a/assets/rustnet.gif and b/assets/rustnet.gif differ
diff --git a/assets/screenshots/details.png b/assets/screenshots/details.png
new file mode 100644
index 0000000..0a595c7
Binary files /dev/null and b/assets/screenshots/details.png differ
diff --git a/assets/screenshots/filter.png b/assets/screenshots/filter.png
new file mode 100644
index 0000000..cd4c0b1
Binary files /dev/null and b/assets/screenshots/filter.png differ
diff --git a/assets/screenshots/graph.png b/assets/screenshots/graph.png
new file mode 100644
index 0000000..09a28c4
Binary files /dev/null and b/assets/screenshots/graph.png differ
diff --git a/assets/screenshots/grouping.png b/assets/screenshots/grouping.png
new file mode 100644
index 0000000..2af6643
Binary files /dev/null and b/assets/screenshots/grouping.png differ
diff --git a/assets/screenshots/interfaces.png b/assets/screenshots/interfaces.png
new file mode 100644
index 0000000..275bbe1
Binary files /dev/null and b/assets/screenshots/interfaces.png differ
diff --git a/assets/screenshots/overview.png b/assets/screenshots/overview.png
new file mode 100644
index 0000000..e7fdc47
Binary files /dev/null and b/assets/screenshots/overview.png differ
diff --git a/demo.tape b/demo.tape
new file mode 100644
index 0000000..ff6a503
--- /dev/null
+++ b/demo.tape
@@ -0,0 +1,86 @@
+# vhs tape for the rustnet showcase demo.
+#
+# Render with: scripts/record-rustnet-demo.sh
+# (wraps `vhs demo.tape` with sudo pre-cache, background traffic,
+# and gifsicle optimization).
+#
+# Manual render: vhs demo.tape (writes assets/rustnet.gif).
+# Pre-build the release binary first: cargo build --release
+# Capture needs root: the tape runs `sudo -A`, which reads the password
+# from the askpass helper pointed to by $SUDO_ASKPASS. The wrapper script
+# above sets that automatically. For manual runs, export SUDO_ASKPASS
+# yourself (or on Linux set CAP_NET_RAW on the binary and drop the
+# `sudo -A` from the tape).
+#
+# Storyboard (~30s, 7 beats):
+# 1. Land on Overview, let traffic populate the table and sparkline.
+# 2. Walk three rows so the right-side panels update (continuity).
+# 3. Filter mode: /, type "443", briefly settle, Esc to clear.
+# 4. Process grouping: a, Down, Space to expand a node, a to flatten.
+# 5. Enter zooms into Details on the selected connection.
+# 6. Esc back to Overview, Tab x3 to land on Graph.
+# 7. Esc back to Overview to close the loop.
+#
+# Sized for the ratatui showcase: 1200x720 fits the 800-1200px guidance and
+# stays readable on phone-width viewports.
+
+Output assets/rustnet.gif
+
+Set Theme "Catppuccin Mocha"
+Set FontSize 14
+Set Width 1200
+Set Height 720
+Set Padding 20
+Set TypingSpeed 50ms
+Set Framerate 24
+Set PlaybackSpeed 1.0
+
+Hide
+Type "clear"
+Enter
+Type "sudo -A target/release/rustnet"
+Enter
+Sleep 2500ms
+Show
+
+Sleep 3s
+
+Down
+Sleep 1s
+Down
+Sleep 1s
+Down
+Sleep 1500ms
+
+Type "/"
+Sleep 400ms
+Type "443"
+Sleep 1500ms
+Escape
+Sleep 800ms
+
+Type "a"
+Sleep 1500ms
+Down
+Sleep 600ms
+Space
+Sleep 1500ms
+Type "a"
+Sleep 800ms
+
+Enter
+Sleep 3s
+
+Escape
+Sleep 600ms
+
+Tab@400ms 3
+Sleep 3s
+
+Escape
+Sleep 1500ms
+
+Hide
+Type "q"
+Sleep 200ms
+Type "q"
diff --git a/screenshots.tape b/screenshots.tape
new file mode 100644
index 0000000..e1042a3
--- /dev/null
+++ b/screenshots.tape
@@ -0,0 +1,96 @@
+# vhs tape for the rustnet README screenshots.
+#
+# Render with: scripts/record-rustnet-demo.sh
+# (which runs both demo.tape and this tape, then cleans the throwaway GIF).
+#
+# Manual render: vhs screenshots.tape
+# Pre-build the release binary first: cargo build --release
+#
+# Walks every tab and emits PNGs under assets/screenshots/.
+# Sized larger than demo.tape (1600x1000, FontSize 16) for crisper README
+# rendering. The GIF Output is a throwaway and is deleted by the script.
+#
+# Beats and outputs:
+# 1. Overview -> assets/screenshots/overview.png
+# 2. Filter mode (/port:443) -> assets/screenshots/filter.png
+# 3. Process grouping (a, Space) -> assets/screenshots/grouping.png
+# 4. Details (Enter) -> assets/screenshots/details.png
+# 5. Graph (Tab x3) -> assets/screenshots/graph.png
+# 6. Interfaces (i) -> assets/screenshots/interfaces.png
+
+Output assets/screenshots/_throwaway.gif
+
+Set Theme "Catppuccin Mocha"
+Set FontSize 16
+Set Width 1600
+Set Height 1000
+Set Padding 20
+Set TypingSpeed 50ms
+Set Framerate 24
+Set PlaybackSpeed 1.0
+
+Hide
+Type "clear"
+Enter
+Type "sudo -A target/release/rustnet"
+Enter
+Sleep 2500ms
+Show
+
+# 1. Overview
+Sleep 3s
+Down
+Sleep 500ms
+Down
+Sleep 500ms
+Screenshot "assets/screenshots/overview.png"
+Sleep 500ms
+
+# 2. Filter mode
+Type "/"
+Sleep 400ms
+Type "port:443"
+Sleep 1500ms
+Screenshot "assets/screenshots/filter.png"
+Sleep 500ms
+Escape
+Sleep 800ms
+
+# 3. Process grouping
+Type "a"
+Sleep 1500ms
+Down
+Sleep 400ms
+Space
+Sleep 1500ms
+Screenshot "assets/screenshots/grouping.png"
+Sleep 500ms
+Type "a"
+Sleep 800ms
+
+# 4. Details
+Enter
+Sleep 2500ms
+Screenshot "assets/screenshots/details.png"
+Sleep 500ms
+Escape
+Sleep 800ms
+
+# 5. Graph
+Tab@400ms 3
+Sleep 2500ms
+Screenshot "assets/screenshots/graph.png"
+Sleep 500ms
+Escape
+Sleep 800ms
+
+# 6. Interfaces
+Type "i"
+Sleep 2500ms
+Screenshot "assets/screenshots/interfaces.png"
+Sleep 500ms
+
+Hide
+Type "q"
+Sleep 200ms
+Type "q"
diff --git a/scripts/record-rustnet-demo.sh b/scripts/record-rustnet-demo.sh
new file mode 100755
index 0000000..a7e3c73
--- /dev/null
+++ b/scripts/record-rustnet-demo.sh
@@ -0,0 +1,217 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Automate the rustnet VHS recording: produces assets/rustnet.gif (showcase)
+# and assets/screenshots/*.png (README) in one pass.
+#
+# Prerequisites:
+# brew install vhs # required
+# brew install gifsicle # optional, for GIF size optimization
+# cargo build --release # auto-run if target/release/rustnet missing
+#
+# Usage:
+# scripts/record-rustnet-demo.sh
+#
+# What it does:
+# 1. Verifies vhs and cargo are installed.
+# 2. Builds target/release/rustnet if missing or stale vs Cargo.lock.
+# 3. Pre-caches sudo (PKTAP capture on macOS always needs root).
+# 4. Spawns a sudo keepalive loop so a long render does not lose creds.
+# 5. Spawns a background traffic generator (curl/dig/ping) so the
+# connection table is busy and sparklines move.
+# 6. Runs `vhs demo.tape` -> assets/rustnet.gif.
+# 7. Runs `vhs screenshots.tape` -> assets/screenshots/*.png.
+# 8. Optimizes the GIF with gifsicle -O3 if available.
+# 9. Cleans up the throwaway GIF and background processes.
+# 10. Prints a summary of the produced assets.
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+RUSTNET_DIR="$(dirname "$SCRIPT_DIR")"
+DEMO_TAPE="${RUSTNET_DIR}/demo.tape"
+SCREENSHOTS_TAPE="${RUSTNET_DIR}/screenshots.tape"
+GIF_FILE="${RUSTNET_DIR}/assets/rustnet.gif"
+SCREENSHOTS_DIR="${RUSTNET_DIR}/assets/screenshots"
+THROWAWAY_GIF="${SCREENSHOTS_DIR}/_throwaway.gif"
+RELEASE_BIN="${RUSTNET_DIR}/target/release/rustnet"
+
+TRAFFIC_PID=""
+ASKPASS_SCRIPT=""
+PW_FILE=""
+
+cleanup() {
+ if [[ -n "${TRAFFIC_PID}" ]] && kill -0 "${TRAFFIC_PID}" 2>/dev/null; then
+ kill "${TRAFFIC_PID}" 2>/dev/null || true
+ wait "${TRAFFIC_PID}" 2>/dev/null || true
+ fi
+ [[ -n "${ASKPASS_SCRIPT}" ]] && rm -f "${ASKPASS_SCRIPT}"
+ [[ -n "${PW_FILE}" ]] && rm -f "${PW_FILE}"
+ rm -f "${THROWAWAY_GIF}"
+}
+trap cleanup EXIT INT TERM
+
+# Kill stale ttyd / headless-Chrome processes left over from prior failed
+# vhs runs. VHS uses go-rod, which writes its Chrome profile to a
+# `rod/user-data` tmpdir; if a prior run died, the locked profile causes
+# the next run to fail with `could not open ttyd: ERR_CONNECTION_REFUSED`.
+preflight_cleanup_vhs() {
+ local stale=0
+ if pgrep -f "ttyd --port" >/dev/null 2>&1; then
+ pkill -f "ttyd --port" 2>/dev/null || true
+ stale=1
+ fi
+ if pgrep -f "rod/user-data" >/dev/null 2>&1; then
+ pkill -f "rod/user-data" 2>/dev/null || true
+ stale=1
+ fi
+ if [[ "${stale}" -eq 1 ]]; then
+ echo "Cleaned up stale ttyd / Chrome processes from prior vhs run."
+ sleep 1
+ fi
+ rm -rf "${TMPDIR:-/tmp}/rod" 2>/dev/null || true
+}
+preflight_cleanup_vhs
+
+# Dependency checks
+for cmd in vhs cargo; do
+ if ! command -v "$cmd" &> /dev/null; then
+ echo "Error: $cmd is not installed."
+ [[ "$cmd" == "vhs" ]] && echo "Install: brew install vhs"
+ [[ "$cmd" == "cargo" ]] && echo "Install: https://rustup.rs"
+ exit 1
+ fi
+done
+
+if ! command -v gifsicle &> /dev/null; then
+ echo "Note: gifsicle not found; skipping GIF optimization."
+ echo " Install with: brew install gifsicle"
+ HAS_GIFSICLE=0
+else
+ HAS_GIFSICLE=1
+fi
+
+# Tape files must exist
+[[ -f "${DEMO_TAPE}" ]] || { echo "Error: ${DEMO_TAPE} missing."; exit 1; }
+[[ -f "${SCREENSHOTS_TAPE}" ]] || { echo "Error: ${SCREENSHOTS_TAPE} missing."; exit 1; }
+
+# Build release binary if missing or stale
+if [[ ! -x "${RELEASE_BIN}" ]] || [[ "${RUSTNET_DIR}/Cargo.lock" -nt "${RELEASE_BIN}" ]]; then
+ echo "Building release binary..."
+ (cd "${RUSTNET_DIR}" && cargo build --release)
+fi
+
+# Read sudo password and validate it. macOS sudo uses tty_tickets by default,
+# so a `sudo -v` cached in this shell does NOT carry over to the new pty
+# that vhs/ttyd spawns. Instead, store the validated password in a private
+# file and expose it via SUDO_ASKPASS so the in-tape `sudo -A target/release/rustnet`
+# can re-authenticate non-interactively.
+echo "Capture requires root. Enter your sudo password (3 attempts)."
+SUDO_PW=""
+for attempt in 1 2 3; do
+ printf 'Password: '
+ IFS= read -rs SUDO_PW
+ printf '\n'
+ if [[ -z "${SUDO_PW}" ]]; then
+ echo " empty password, try again ($attempt/3)"
+ continue
+ fi
+ if printf '%s\n' "${SUDO_PW}" | sudo -S -v 2>/dev/null; then
+ break
+ fi
+ echo " wrong password ($attempt/3)"
+ SUDO_PW=""
+done
+if [[ -z "${SUDO_PW}" ]]; then
+ echo "Error: sudo authentication failed after 3 attempts."
+ exit 1
+fi
+
+# Stash the password in a 0600 temp file and write a tiny askpass helper
+# that cats it. SUDO_ASKPASS in the env makes `sudo -A` use this helper.
+PW_FILE="$(mktemp -t rustnet-pw.XXXXXX)"
+chmod 600 "${PW_FILE}"
+printf '%s' "${SUDO_PW}" > "${PW_FILE}"
+unset SUDO_PW
+
+ASKPASS_SCRIPT="$(mktemp -t rustnet-askpass.XXXXXX)"
+chmod 700 "${ASKPASS_SCRIPT}"
+cat > "${ASKPASS_SCRIPT}" </dev/null 2>&1 || true
+ dig +short +time=2 +tries=1 github.com >/dev/null 2>&1 || true
+ ping -c 1 -W 1 1.1.1.1 >/dev/null 2>&1 || true
+ sleep 1
+ done
+) &
+TRAFFIC_PID=$!
+
+# Render the demo GIF.
+# VHS does not propagate the parent shell's env to the recording shell.
+# Inject an `Env SUDO_ASKPASS ` directive into each tape so the in-tape
+# `sudo -A` can find the askpass helper. VHS requires Env to come AFTER all
+# `Set` directives but BEFORE the first command (Hide/Type/Sleep/etc.), so
+# we use awk to insert it at the first command line.
+render_tape_with_askpass() {
+ local tape="$1"
+ local tmp_tape
+ tmp_tape="$(mktemp -t "$(basename "${tape}").XXXXXX")"
+ awk -v askpass="${ASKPASS_SCRIPT}" '
+ BEGIN { injected = 0 }
+ /^(Output|Set|Env|Require|Source|#|[[:space:]]*$)/ { print; next }
+ {
+ if (!injected) {
+ print "Env SUDO_ASKPASS \"" askpass "\""
+ injected = 1
+ }
+ print
+ }
+ ' "${RUSTNET_DIR}/${tape}" > "${tmp_tape}"
+ (cd "${RUSTNET_DIR}" && vhs "${tmp_tape}")
+ rm -f "${tmp_tape}"
+}
+
+echo ""
+echo "Rendering demo GIF (vhs demo.tape)..."
+render_tape_with_askpass demo.tape
+
+# Render the screenshots.
+echo ""
+echo "Rendering screenshots (vhs screenshots.tape)..."
+render_tape_with_askpass screenshots.tape
+
+# Optimize the GIF.
+if [[ "${HAS_GIFSICLE}" -eq 1 ]] && [[ -f "${GIF_FILE}" ]]; then
+ echo ""
+ echo "Optimizing GIF with gifsicle -O3..."
+ BEFORE_SIZE=$(stat -f%z "${GIF_FILE}" 2>/dev/null || stat -c%s "${GIF_FILE}")
+ gifsicle -O3 --batch "${GIF_FILE}"
+ AFTER_SIZE=$(stat -f%z "${GIF_FILE}" 2>/dev/null || stat -c%s "${GIF_FILE}")
+ echo " ${BEFORE_SIZE} -> ${AFTER_SIZE} bytes"
+fi
+
+# Summary.
+echo ""
+echo "Done."
+echo ""
+echo "GIF:"
+ls -lh "${GIF_FILE}" 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
+echo ""
+echo "Screenshots:"
+for png in "${SCREENSHOTS_DIR}"/*.png; do
+ [[ -f "${png}" ]] && ls -lh "${png}" | awk '{print " " $9 " (" $5 ")"}'
+done
+echo ""
+echo "Preview:"
+echo " open ${GIF_FILE}"
+echo " open ${SCREENSHOTS_DIR}"