diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2ead01d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+dist/
+__pycache__/
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..8d59cb1
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,42 @@
+
+ ExpiresActive On
+
+ # Fonts (1 year - they never change)
+ ExpiresByType font/woff2 "access plus 1 year"
+ ExpiresByType application/font-woff2 "access plus 1 year"
+
+ # Images (1 year)
+ ExpiresByType image/svg+xml "access plus 1 year"
+ ExpiresByType image/png "access plus 1 year"
+ ExpiresByType image/jpeg "access plus 1 year"
+ ExpiresByType image/webp "access plus 1 year"
+ ExpiresByType image/x-icon "access plus 1 year"
+
+ # CSS & JS (1 week - updated via deploy)
+ ExpiresByType text/css "access plus 1 week"
+ ExpiresByType application/javascript "access plus 1 week"
+ ExpiresByType text/javascript "access plus 1 week"
+
+ # JSON (1 day - locales may change)
+ ExpiresByType application/json "access plus 1 day"
+
+ # HTML (no cache - always fresh)
+ ExpiresByType text/html "access plus 0 seconds"
+
+ # Manifest & Service Worker (no cache)
+
+ ExpiresDefault "access plus 0 seconds"
+
+
+
+
+ # Service Worker must not be cached
+
+ Header set Cache-Control "no-cache, no-store, must-revalidate"
+
+
+ # Fonts: immutable
+
+ Header set Cache-Control "public, max-age=31536000, immutable"
+
+
diff --git a/AGENTS.md b/AGENTS.md
index d77376b..29352ec 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -31,6 +31,13 @@ python3 -m http.server 8080
# Oder mit Live-Reload
npx live-server
+# Build (JS/CSS minifizieren → dist/)
+pip3 install rjsmin rcssmin # einmalig
+python3 build.py
+
+# Deploy (baut automatisch, dann rsync von dist/)
+./deploy.sh
+
# Tests ausführen (im Browser)
# Server starten, dann http://localhost:8080/tests/ öffnen
diff --git a/README.md b/README.md
index 2a0e974..599dddb 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,18 @@ Für Produktion werden nur diese Dateien benötigt:
**Nicht deployen:** `tests/`, `docs/`, `AGENTS.md`, `README.md`, `.git/`, `deploy.sh`
+#### Build (Minifizierung)
+
+```bash
+# Einmalig: Dependencies installieren
+pip3 install rjsmin rcssmin
+
+# Build ausführen (erstellt dist/ mit minifizierten Dateien)
+python3 build.py
+```
+
+Das Build-Script minifiziert alle JS- und CSS-Dateien (~111 KiB Ersparnis) und kopiert alles nach `dist/`.
+
#### Deploy via Script
```bash
@@ -126,10 +138,10 @@ Für Produktion werden nur diese Dateien benötigt:
./deploy.sh
```
-Das Script nutzt `rsync` über SSH und synchronisiert nur geänderte Dateien.
-Ausgeschlossene Dateien/Ordner (tests, docs, .git, etc.) werden automatisch übersprungen.
+Das Script führt automatisch `python3 build.py` aus, dann `rsync` von `dist/` zum Server.
**Voraussetzungen:**
+- Python 3 + `rjsmin` + `rcssmin` (für Build)
- SSH-Key-Authentifizierung zum Server
- `rsync` lokal und auf dem Server installiert
diff --git a/build.py b/build.py
new file mode 100644
index 0000000..802e62d
--- /dev/null
+++ b/build.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+"""
+Build script for dgray.io
+Minifies JS and CSS files into dist/ directory.
+
+Usage: python3 build.py
+Requirements: pip3 install rjsmin rcssmin
+"""
+
+import os
+import shutil
+import sys
+
+try:
+ import rjsmin
+ import rcssmin
+except ImportError:
+ print("Missing dependencies. Install with:")
+ print(" pip3 install rjsmin rcssmin")
+ sys.exit(1)
+
+SRC_DIR = os.path.dirname(os.path.abspath(__file__))
+DIST_DIR = os.path.join(SRC_DIR, 'dist')
+
+EXCLUDE_DIRS = {'.git', 'dist', 'node_modules', 'docs', 'tests', '.vscode', '.idea', '__pycache__'}
+EXCLUDE_FILES = {'AGENTS.md', 'deploy.sh', 'build.py', 'README.md', '.gitignore'}
+
+def should_exclude(path):
+ parts = os.path.relpath(path, SRC_DIR).split(os.sep)
+ return any(p in EXCLUDE_DIRS for p in parts) or os.path.basename(path) in EXCLUDE_FILES
+
+def build():
+ if os.path.exists(DIST_DIR):
+ shutil.rmtree(DIST_DIR)
+
+ js_saved = 0
+ css_saved = 0
+ files_copied = 0
+
+ for root, dirs, files in os.walk(SRC_DIR):
+ dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
+
+ for filename in files:
+ src_path = os.path.join(root, filename)
+ if should_exclude(src_path):
+ continue
+
+ rel_path = os.path.relpath(src_path, SRC_DIR)
+ dest_path = os.path.join(DIST_DIR, rel_path)
+ os.makedirs(os.path.dirname(dest_path), exist_ok=True)
+
+ if filename.endswith('.js') and not filename.endswith('.min.js'):
+ with open(src_path, 'r', encoding='utf-8') as f:
+ original = f.read()
+ minified = rjsmin.jsmin(original)
+ with open(dest_path, 'w', encoding='utf-8') as f:
+ f.write(minified)
+ saved = len(original.encode('utf-8')) - len(minified.encode('utf-8'))
+ js_saved += saved
+ files_copied += 1
+
+ elif filename.endswith('.css') and not filename.endswith('.min.css'):
+ with open(src_path, 'r', encoding='utf-8') as f:
+ original = f.read()
+ minified = rcssmin.cssmin(original)
+ with open(dest_path, 'w', encoding='utf-8') as f:
+ f.write(minified)
+ saved = len(original.encode('utf-8')) - len(minified.encode('utf-8'))
+ css_saved += saved
+ files_copied += 1
+
+ else:
+ shutil.copy2(src_path, dest_path)
+ files_copied += 1
+
+ print(f"Build complete: {files_copied} files -> dist/")
+ print(f" JS savings: {js_saved / 1024:.1f} KiB")
+ print(f" CSS savings: {css_saved / 1024:.1f} KiB")
+ print(f" Total saved: {(js_saved + css_saved) / 1024:.1f} KiB")
+
+if __name__ == '__main__':
+ build()
diff --git a/css/animate.custom.css b/css/animate.custom.css
new file mode 100644
index 0000000..7fc8786
--- /dev/null
+++ b/css/animate.custom.css
@@ -0,0 +1,44 @@
+:root {
+ --animate-duration: 1s;
+}
+
+.animate__animated {
+ animation-duration: var(--animate-duration);
+ animation-fill-mode: both;
+}
+
+.animate__faster {
+ animation-duration: calc(var(--animate-duration) / 2);
+}
+
+@keyframes fadeIn {
+ 0% { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.animate__fadeIn {
+ animation-name: fadeIn;
+}
+
+@keyframes fadeOut {
+ 0% { opacity: 1; }
+ to { opacity: 0; }
+}
+
+.animate__fadeOut {
+ animation-name: fadeOut;
+}
+
+@keyframes heartBeat {
+ 0% { transform: scale(1); }
+ 14% { transform: scale(1.3); }
+ 28% { transform: scale(1); }
+ 42% { transform: scale(1.3); }
+ 70% { transform: scale(1); }
+}
+
+.animate__heartBeat {
+ animation-name: heartBeat;
+ animation-duration: calc(var(--animate-duration) * 1.3);
+ animation-timing-function: ease-in-out;
+}
diff --git a/css/components.css b/css/components.css
index 2ed20fe..d1385cd 100644
--- a/css/components.css
+++ b/css/components.css
@@ -215,7 +215,7 @@ app-header .logo {
app-header .logo-img {
height: 28px;
- width: auto;
+ width: 100px;
}
app-header .logo-dark {
@@ -299,6 +299,8 @@ app-footer {
border-top: 1px solid var(--color-border);
padding: var(--space-lg) 0;
margin-top: auto;
+ min-height: 5.5rem;
+ contain: layout style;
}
app-footer .footer-inner {
diff --git a/css/fonts.css b/css/fonts.css
index 63c9385..1e70001 100644
--- a/css/fonts.css
+++ b/css/fonts.css
@@ -13,7 +13,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 400;
- font-display: swap;
+ font-display: optional;
src: url('../assets/fonts/Inter-Regular.woff2') format('woff2');
}
@@ -21,7 +21,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 500;
- font-display: swap;
+ font-display: optional;
src: url('../assets/fonts/Inter-Medium.woff2') format('woff2');
}
@@ -29,7 +29,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 600;
- font-display: swap;
+ font-display: optional;
src: url('../assets/fonts/Inter-SemiBold.woff2') format('woff2');
}
@@ -37,7 +37,7 @@
font-family: 'Inter';
font-style: normal;
font-weight: 700;
- font-display: swap;
+ font-display: optional;
src: url('../assets/fonts/Inter-Bold.woff2') format('woff2');
}
diff --git a/css/variables.css b/css/variables.css
index 346250f..c90f360 100644
--- a/css/variables.css
+++ b/css/variables.css
@@ -26,7 +26,7 @@
--color-text: #1A1A1A;
--color-text-secondary: #4A4A4A;
- --color-text-muted: #8A8A8A;
+ --color-text-muted: #737373;
--color-border: #D0D0D0;
--color-border-focus: #555555;
@@ -118,7 +118,7 @@
--color-text: #F0F0F0;
--color-text-secondary: #C0C0C0;
- --color-text-muted: #808080;
+ --color-text-muted: #949494;
--color-border: #3A3A3A;
--color-border-focus: #AAAAAA;
@@ -151,7 +151,7 @@
--color-text: #F0F0F0;
--color-text-secondary: #C0C0C0;
- --color-text-muted: #808080;
+ --color-text-muted: #949494;
--color-border: #3A3A3A;
--color-border-focus: #AAAAAA;
@@ -182,7 +182,7 @@
--color-text: #1A1A1A;
--color-text-secondary: #4A4A4A;
- --color-text-muted: #8A8A8A;
+ --color-text-muted: #737373;
--color-border: #D0D0D0;
--color-border-focus: #555555;
diff --git a/deploy.sh b/deploy.sh
index 855570f..2ef8fb6 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -14,35 +14,21 @@ set -e
REMOTE_USER_HOST="${1:-admin@dgray.io}"
REMOTE_PATH="${2:-/home/admin/web/dgray.io/public_html}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+DIST_DIR="$SCRIPT_DIR/dist"
-# Files/directories to exclude from deployment
-EXCLUDES=(
- ".git"
- ".gitignore"
- "AGENTS.md"
- "deploy.sh"
- "docs/"
- "tests/"
- "README.md"
- ".vscode"
- ".idea"
-)
+# --- Build (minify JS + CSS) ---
+echo "Building..."
+python3 "$SCRIPT_DIR/build.py"
+echo ""
-# --- Build exclude arguments ---
-EXCLUDE_ARGS=""
-for item in "${EXCLUDES[@]}"; do
- EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$item"
-done
-
-# --- Deploy ---
+# --- Deploy from dist/ ---
echo "Deploying dgray.io"
-echo " From: $SCRIPT_DIR"
+echo " From: $DIST_DIR"
echo " To: $REMOTE_USER_HOST:$REMOTE_PATH"
echo ""
rsync -avz --delete \
- $EXCLUDE_ARGS \
- "$SCRIPT_DIR/" \
+ "$DIST_DIR/" \
"$REMOTE_USER_HOST:$REMOTE_PATH/"
echo ""
diff --git a/index.html b/index.html
index 9c8c273..9318846 100644
--- a/index.html
+++ b/index.html
@@ -33,19 +33,152 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
-
+