diff --git a/.well-known/matrix/client b/.well-known/matrix/client
new file mode 100644
index 0000000..de450b7
--- /dev/null
+++ b/.well-known/matrix/client
@@ -0,0 +1 @@
+{"m.homeserver":{"base_url":"https://matrix.rastus01.com"}}
diff --git a/.well-known/matrix/server b/.well-known/matrix/server
new file mode 100644
index 0000000..d9f036a
--- /dev/null
+++ b/.well-known/matrix/server
@@ -0,0 +1 @@
+{"m.server":"matrix.rastus01.com:443"}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a0417e5
--- /dev/null
+++ b/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Personal Node
+ rastus01.com
+ Home Lab · Matrix · Selfhosting
+
+ Dies ist meine persönliche Selfhosting-Domain. Einige Dienste laufen
+ hier privat oder experimentell.
+
+
+
+
+
+
+
+
+ Eigene Projekte und Experimente folgen später.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..65cd58f
--- /dev/null
+++ b/script.js
@@ -0,0 +1,90 @@
+const yearElement = document.querySelector("#current-year");
+const canvas = document.querySelector("#network-canvas");
+
+if (yearElement) {
+ yearElement.textContent = new Date().getFullYear().toString();
+}
+
+if (canvas) {
+ const context = canvas.getContext("2d");
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
+ let animationFrame = null;
+ const nodes = Array.from({ length: 42 }, (_, index) => ({
+ x: (index * 97) % 100,
+ y: (index * 53) % 100,
+ speed: 0.08 + (index % 5) * 0.015,
+ phase: index * 0.7,
+ }));
+
+ function resizeCanvas() {
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
+ const { width, height } = canvas.getBoundingClientRect();
+
+ canvas.width = Math.max(1, Math.floor(width * ratio));
+ canvas.height = Math.max(1, Math.floor(height * ratio));
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
+ }
+
+ function drawNetwork(time = 0) {
+ const { width, height } = canvas.getBoundingClientRect();
+ const points = nodes.map((node) => {
+ const drift = prefersReducedMotion.matches ? 0 : time * 0.001 * node.speed;
+
+ return {
+ x: ((node.x + Math.sin(node.phase + drift) * 5 + 100) % 100) * width / 100,
+ y: ((node.y + Math.cos(node.phase + drift) * 5 + 100) % 100) * height / 100,
+ };
+ });
+
+ context.clearRect(0, 0, width, height);
+ context.fillStyle = "#070a0f";
+ context.fillRect(0, 0, width, height);
+
+ context.lineWidth = 1;
+ for (let index = 0; index < points.length; index += 1) {
+ for (let nextIndex = index + 1; nextIndex < points.length; nextIndex += 1) {
+ const first = points[index];
+ const second = points[nextIndex];
+ const distance = Math.hypot(first.x - second.x, first.y - second.y);
+
+ if (distance < 170) {
+ const alpha = (1 - distance / 170) * 0.22;
+ context.strokeStyle = `rgba(103, 232, 249, ${alpha})`;
+ context.beginPath();
+ context.moveTo(first.x, first.y);
+ context.lineTo(second.x, second.y);
+ context.stroke();
+ }
+ }
+ }
+
+ for (const point of points) {
+ context.fillStyle = "rgba(124, 247, 182, 0.88)";
+ context.beginPath();
+ context.arc(point.x, point.y, 2.2, 0, Math.PI * 2);
+ context.fill();
+ }
+
+ if (!prefersReducedMotion.matches) {
+ animationFrame = window.requestAnimationFrame(drawNetwork);
+ }
+ }
+
+ resizeCanvas();
+ drawNetwork();
+ window.addEventListener("resize", () => {
+ resizeCanvas();
+
+ if (prefersReducedMotion.matches) {
+ drawNetwork();
+ }
+ });
+ prefersReducedMotion.addEventListener("change", () => {
+ if (animationFrame) {
+ window.cancelAnimationFrame(animationFrame);
+ animationFrame = null;
+ }
+
+ drawNetwork();
+ });
+}
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..863afdb
--- /dev/null
+++ b/style.css
@@ -0,0 +1,328 @@
+:root {
+ color-scheme: dark;
+ --bg: #070a0f;
+ --bg-soft: #0d141d;
+ --panel: rgba(16, 24, 34, 0.78);
+ --panel-strong: rgba(22, 33, 46, 0.92);
+ --text: #ecf4ff;
+ --muted: #9badc0;
+ --line: rgba(142, 245, 255, 0.18);
+ --cyan: #67e8f9;
+ --green: #7cf7b6;
+ --pink: #ff5ea8;
+ --amber: #ffd166;
+ --shadow: 0 24px 80px rgba(0, 0, 0, 0.38);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ min-height: 100%;
+ background: var(--bg);
+}
+
+body {
+ min-height: 100vh;
+ margin: 0;
+ color: var(--text);
+ font-family:
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
+ "Segoe UI", sans-serif;
+ line-height: 1.6;
+ background:
+ linear-gradient(145deg, #070a0f 0%, #0b1018 44%, #101018 100%);
+}
+
+body::before {
+ position: fixed;
+ inset: 0;
+ z-index: -1;
+ pointer-events: none;
+ content: "";
+ background-image:
+ linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
+ background-size: 48px 48px;
+ mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.9), transparent 82%);
+}
+
+a {
+ color: inherit;
+}
+
+.page-shell {
+ min-height: 100vh;
+ padding: 0 0 28px;
+ display: flex;
+ flex-direction: column;
+ gap: 28px;
+}
+
+.hero {
+ position: relative;
+ overflow: hidden;
+ min-height: 66svh;
+ padding: clamp(32px, 7vw, 76px) max(20px, calc((100vw - 1120px) / 2));
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ background: linear-gradient(180deg, rgba(7, 10, 15, 0.1), rgba(7, 10, 15, 0.82));
+}
+
+.hero::before,
+.hero::after {
+ position: absolute;
+ pointer-events: none;
+ content: "";
+}
+
+.hero::before {
+ inset: 0;
+ background:
+ repeating-linear-gradient(
+ 0deg,
+ rgba(255, 255, 255, 0.045) 0,
+ rgba(255, 255, 255, 0.045) 1px,
+ transparent 1px,
+ transparent 14px
+ );
+ opacity: 0.42;
+}
+
+.hero::after {
+ right: -12%;
+ bottom: -28%;
+ width: 520px;
+ aspect-ratio: 1;
+ border: 1px solid rgba(103, 232, 249, 0.22);
+ border-radius: 50%;
+ background:
+ linear-gradient(90deg, transparent 49%, rgba(103, 232, 249, 0.24) 50%, transparent 51%),
+ linear-gradient(0deg, transparent 49%, rgba(103, 232, 249, 0.24) 50%, transparent 51%);
+ opacity: 0.34;
+}
+
+.network-canvas {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.signal-grid {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ display: grid;
+ grid-template-columns: repeat(2, 12px);
+ gap: 8px;
+}
+
+.signal-grid span {
+ width: 12px;
+ height: 12px;
+ border: 1px solid rgba(103, 232, 249, 0.5);
+ background: rgba(103, 232, 249, 0.12);
+}
+
+.eyebrow,
+.subtitle,
+.intro,
+h1 {
+ position: relative;
+ z-index: 2;
+}
+
+.eyebrow {
+ margin: 0 0 14px;
+ color: var(--green);
+ font-size: 0.78rem;
+ font-weight: 700;
+ letter-spacing: 0;
+ text-transform: uppercase;
+}
+
+h1 {
+ margin: 0;
+ font-size: clamp(3rem, 8vw, 7rem);
+ line-height: 0.92;
+ letter-spacing: 0;
+}
+
+.subtitle {
+ margin: 18px 0 0;
+ color: var(--cyan);
+ font-size: clamp(1rem, 2vw, 1.35rem);
+ font-weight: 650;
+}
+
+.intro {
+ max-width: 680px;
+ margin: 22px 0 0;
+ color: var(--muted);
+ font-size: 1.03rem;
+}
+
+.service-grid {
+ width: min(1120px, calc(100% - 32px));
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 18px;
+}
+
+.service-card {
+ min-height: 220px;
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ border: 1px solid rgba(148, 163, 184, 0.18);
+ border-radius: 8px;
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent),
+ var(--panel-strong);
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22);
+}
+
+.card-header {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.card-header h2 {
+ margin: 0;
+ font-size: 1.08rem;
+ line-height: 1.25;
+ letter-spacing: 0;
+}
+
+.status-dot {
+ width: 10px;
+ height: 10px;
+ flex: 0 0 auto;
+ border-radius: 50%;
+ box-shadow: 0 0 22px currentColor;
+}
+
+.status-live {
+ color: var(--green);
+ background: var(--green);
+}
+
+.status-idle {
+ color: var(--cyan);
+ background: var(--cyan);
+}
+
+.status-build {
+ color: var(--amber);
+ background: var(--amber);
+}
+
+.service-card p {
+ margin: 18px 0 0;
+ color: var(--muted);
+}
+
+.button-link {
+ margin-top: auto;
+ min-height: 44px;
+ padding: 10px 16px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid rgba(103, 232, 249, 0.45);
+ border-radius: 6px;
+ color: var(--text);
+ font-weight: 700;
+ text-decoration: none;
+ background: rgba(103, 232, 249, 0.09);
+ transition:
+ border-color 180ms ease,
+ background 180ms ease,
+ transform 180ms ease;
+}
+
+.button-link:hover,
+.button-link:focus-visible {
+ border-color: var(--cyan);
+ background: rgba(103, 232, 249, 0.17);
+ transform: translateY(-1px);
+}
+
+.button-link:focus-visible {
+ outline: 3px solid rgba(103, 232, 249, 0.3);
+ outline-offset: 3px;
+}
+
+.site-footer {
+ width: min(1120px, calc(100% - 32px));
+ margin: 0 auto;
+ padding: 2px 4px 0;
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ color: rgba(236, 244, 255, 0.58);
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ font-size: 0.82rem;
+}
+
+@media (max-width: 820px) {
+ .page-shell {
+ padding-top: 0;
+ }
+
+ .hero {
+ min-height: 62svh;
+ padding: 32px 24px;
+ }
+
+ .service-grid {
+ width: min(100% - 24px, 680px);
+ grid-template-columns: 1fr;
+ }
+
+ .site-footer {
+ width: min(100% - 24px, 680px);
+ }
+
+ .service-card {
+ min-height: 180px;
+ }
+}
+
+@media (max-width: 520px) {
+ .page-shell {
+ gap: 18px;
+ }
+
+ .hero {
+ min-height: 58svh;
+ }
+
+ .signal-grid {
+ top: 18px;
+ right: 18px;
+ }
+
+ .site-footer {
+ flex-direction: column;
+ gap: 4px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ }
+}