diff --git a/.gitea/workflows/test-skins.yml b/.gitea/workflows/test-skins.yml
new file mode 100644
index 00000000..e214a719
--- /dev/null
+++ b/.gitea/workflows/test-skins.yml
@@ -0,0 +1,110 @@
+name: Test Skins
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ link-check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Validate links and assets
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ RED="\033[31m"
+ GREEN="\033[32m"
+ RESET="\033[0m"
+
+ ERRORS=()
+
+ urldecode() {
+ printf '%b' "${1//%/\\x}"
+ }
+
+ check_http() {
+ local url="$1"
+ # HEAD first
+ if curl -Is --max-time 10 "$url" | head -n 1 | grep -qE "HTTP/.* (200|30[0-9])"; then
+ return 0
+ fi
+ # GET fallback
+ if curl -Is --max-time 10 -X GET "$url" | head -n 1 | grep -qE "HTTP/.* (200|30[0-9])"; then
+ return 0
+ fi
+ return 1
+ }
+
+ check_local() {
+ local path="$1"
+ path="${path#/}" # strip leading slash
+ local decoded
+ decoded=$(urldecode "$path")
+
+ if [[ ! -e "$decoded" ]]; then
+ return 1
+ fi
+ return 0
+ }
+
+ extract_links() {
+ local file="$1"
+
+ # Markdown links
+ grep -oE '\[[^]]*\]\([^)]*\)' "$file" \
+ | sed -E 's/.*\((.*)\).*/\1/'
+
+ # Image markdown links
+ grep -oE '!\[[^]]*\]\([^)]*\)' "$file" \
+ | sed -E 's/.*\((.*)\).*/\1/'
+
+ # Raw URLs
+ grep -oE 'https?://[^ )"]+' "$file"
+
+ #
+ grep -oE '
]*src="[^"]+"' "$file" \
+ | sed -E 's/.*src="([^"]*)".*/\1/'
+
+ #