diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ba83bcc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Shik3i
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/extension/background.js b/extension/background.js
index 366b741..eac563b 100644
--- a/extension/background.js
+++ b/extension/background.js
@@ -191,9 +191,30 @@ function handleServerEvent(event, data) {
currentRoom = data;
addLog(`Joined Room: ${data.roomId}`, 'success');
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: data.peers }).catch(() => {});
+ // Inform Website Bridge
+ chrome.tabs.query({}, (tabs) => {
+ tabs.forEach(tab => {
+ chrome.tabs.sendMessage(tab.id, { type: 'JOIN_STATUS', success: true, message: 'Joined' }).catch(() => {});
+ });
+ });
+ break;
+ case EVENTS.ROOM_LIST:
+ chrome.runtime.sendMessage({ type: 'ROOM_LIST', rooms: data.rooms }).catch(() => {});
break;
case EVENTS.ERROR:
addLog(`Server Error: ${data.message}`, 'error');
+ chrome.notifications.create({
+ type: 'basic',
+ iconUrl: 'icons/icon128.png',
+ title: 'KoalaSync Error',
+ message: data.message
+ });
+ // Inform Website Bridge
+ chrome.tabs.query({}, (tabs) => {
+ tabs.forEach(tab => {
+ chrome.tabs.sendMessage(tab.id, { type: 'JOIN_STATUS', success: false, message: data.message }).catch(() => {});
+ });
+ });
break;
case EVENTS.PLAY:
case EVENTS.PAUSE:
@@ -327,6 +348,16 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(logs);
} else if (message.type === 'GET_HISTORY') {
sendResponse(history);
+ } else if (message.type === 'GET_ROOM_LIST') {
+ if (socket && socket.readyState === WebSocket.OPEN) {
+ socket.send(`42${JSON.stringify([EVENTS.GET_ROOMS])}`);
+ }
+ } else if (message.type === 'WEB_JOIN_REQUEST') {
+ const { roomId, password } = message;
+ chrome.storage.sync.set({ roomId, password }, () => {
+ connect();
+ // We wait for status update in handleServerEvent
+ });
} else if (message.type === 'CONTENT_EVENT') {
if (sender.tab) {
currentTabId = sender.tab.id;
diff --git a/extension/bridge.js b/extension/bridge.js
new file mode 100644
index 0000000..ca8a8ef
--- /dev/null
+++ b/extension/bridge.js
@@ -0,0 +1,31 @@
+/**
+ * KoalaSync Bridge Script
+ * Injected into koalasync.shik3i.net to facilitate communication between
+ * the landing page and the extension.
+ */
+
+// 1. Signal presence to the website
+document.documentElement.dataset.koalasyncInstalled = 'true';
+
+// 2. Listen for Join Requests from the Website
+window.addEventListener('KOALASYNC_JOIN_REQUEST', (e) => {
+ const { roomId, password } = e.detail;
+ chrome.runtime.sendMessage({
+ type: 'WEB_JOIN_REQUEST',
+ roomId,
+ password
+ });
+});
+
+// 3. Listen for Status Updates from the Extension and relay to Website
+chrome.runtime.onMessage.addListener((msg) => {
+ if (msg.type === 'JOIN_STATUS') {
+ const event = new CustomEvent('KOALASYNC_STATUS', {
+ detail: {
+ success: msg.success,
+ message: msg.message
+ }
+ });
+ window.dispatchEvent(event);
+ }
+});
diff --git a/extension/manifest.json b/extension/manifest.json
index a733c7d..302c6be 100644
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -35,6 +35,11 @@
],
"run_at": "document_idle",
"all_frames": false
+ },
+ {
+ "matches": ["https://koalasync.shik3i.net/*"],
+ "js": ["bridge.js"],
+ "run_at": "document_start"
}
],
"icons": {
diff --git a/extension/popup.html b/extension/popup.html
index ab47434..20c97fe 100644
--- a/extension/popup.html
+++ b/extension/popup.html
@@ -222,7 +222,17 @@
Password (Optional)
- Join / Create Room
+
+ Join Room
+ Create Random Room
+
+
+ Public Rooms
+ REFRESH
+
+
Invite Link
diff --git a/extension/popup.js b/extension/popup.js
index 9664043..b9d0f47 100644
--- a/extension/popup.js
+++ b/extension/popup.js
@@ -1,4 +1,4 @@
-import { EVENTS } from './shared/constants.js';
+import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
const elements = {
@@ -23,7 +23,11 @@ const elements = {
inviteLink: document.getElementById('inviteLink'),
filterNoise: document.getElementById('filterNoise'),
historyList: document.getElementById('historyList'),
- copyLogs: document.getElementById('copyLogs')
+ copyLogs: document.getElementById('copyLogs'),
+ createRoomBtn: document.getElementById('createRoomBtn'),
+ publicRooms: document.getElementById('publicRooms'),
+ refreshRooms: document.getElementById('refreshRooms'),
+ roomError: document.getElementById('roomError')
};
// --- Initialization ---
@@ -55,6 +59,12 @@ async function init() {
updatePeerList(res.peers);
}
});
+
+ // Check for invite link on landing page
+ checkInviteLink();
+
+ // Initial room list fetch
+ chrome.runtime.sendMessage({ type: 'GET_ROOM_LIST' });
}
// --- UI Logic ---
@@ -62,7 +72,8 @@ function updateUI(roomId, password) {
const inRoom = !!roomId;
elements.roomInfo.style.display = inRoom ? 'block' : 'none';
if (inRoom) {
- elements.inviteLink.value = `${roomId}${password ? '#' + password : ''}`;
+ const invite = `${OFFICIAL_LANDING_PAGE_URL}/join.html#join:${roomId}:${password}`;
+ elements.inviteLink.value = invite;
}
}
@@ -168,6 +179,43 @@ function refreshHistory() {
});
}
+function updateRoomList(rooms) {
+ if (!rooms || rooms.length === 0) {
+ elements.publicRooms.innerHTML = '
No active rooms
';
+ return;
+ }
+ elements.publicRooms.innerHTML = rooms.map(r => `
+
+ ${r.id}
+ ${r.peerCount} peers
+
+ `).join('');
+
+ elements.publicRooms.querySelectorAll('.room-item').forEach(item => {
+ item.addEventListener('click', () => {
+ elements.roomId.value = item.dataset.id;
+ elements.password.value = '';
+ elements.password.focus();
+ });
+ });
+}
+
+function checkInviteLink() {
+ chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
+ const tab = tabs[0];
+ if (tab && tab.url && tab.url.includes(OFFICIAL_LANDING_PAGE_URL) && tab.url.includes('#join:')) {
+ const parts = tab.url.split('#join:')[1].split(':');
+ if (parts.length >= 2) {
+ elements.roomId.value = parts[0];
+ elements.password.value = parts[1];
+ // Visual feedback
+ elements.joinBtn.style.boxShadow = '0 0 15px var(--accent)';
+ setTimeout(() => elements.joinBtn.style.boxShadow = '', 2000);
+ }
+ }
+ });
+}
+
function setServerMode(custom) {
elements.serverOfficial.classList.toggle('active', !custom);
elements.serverCustom.classList.toggle('active', custom);
@@ -194,6 +242,28 @@ elements.tabs.forEach(btn => {
});
});
+function showError(msg) {
+ if (!elements.roomError) return;
+ elements.roomError.textContent = msg;
+ elements.roomError.style.display = 'block';
+ elements.roomId.style.borderColor = 'var(--error)';
+ elements.password.style.borderColor = 'var(--error)';
+
+ // Shake effect
+ document.getElementById('tab-room').animate([
+ { transform: 'translateX(0)' },
+ { transform: 'translateX(-5px)' },
+ { transform: 'translateX(5px)' },
+ { transform: 'translateX(0)' }
+ ], { duration: 200, iterations: 2 });
+
+ setTimeout(() => {
+ if (elements.roomError) elements.roomError.style.display = 'none';
+ elements.roomId.style.borderColor = '';
+ elements.password.style.borderColor = '';
+ }, 5000);
+}
+
// --- Action Handlers ---
elements.joinBtn.addEventListener('click', async () => {
const serverUrl = elements.serverUrl.value;
@@ -216,6 +286,22 @@ elements.leaveBtn.addEventListener('click', async () => {
updateUI(null, null);
});
+elements.createRoomBtn.addEventListener('click', () => {
+ const animals = ['koala', 'panda', 'tiger', 'eagle', 'fox', 'bear'];
+ const adj = ['happy', 'cool', 'fast', 'smart', 'brave', 'calm'];
+ const id = `${adj[Math.floor(Math.random() * adj.length)]}-${animals[Math.floor(Math.random() * animals.length)]}-${Math.floor(Math.random() * 100)}`;
+ const pass = Math.random().toString(36).substring(2, 8);
+
+ elements.roomId.value = id;
+ elements.password.value = pass;
+ elements.joinBtn.click();
+});
+
+elements.refreshRooms.addEventListener('click', () => {
+ elements.publicRooms.innerHTML = '
Refreshing...
';
+ chrome.runtime.sendMessage({ type: 'GET_ROOM_LIST' });
+});
+
elements.targetTab.addEventListener('change', async () => {
await chrome.storage.sync.set({ targetTabId: elements.targetTab.value });
});
@@ -270,6 +356,10 @@ chrome.runtime.onMessage.addListener((msg) => {
applyConnectionStatus(msg.status);
} else if (msg.type === 'HISTORY_UPDATE') {
updateHistory(msg.history);
+ } else if (msg.type === 'ROOM_LIST') {
+ updateRoomList(msg.rooms);
+ } else if (msg.type === 'LOG_UPDATE' && msg.log && msg.log.type === 'error') {
+ showError(msg.log.message);
}
});
diff --git a/server/index.js b/server/index.js
index 110bc12..c6d8dad 100644
--- a/server/index.js
+++ b/server/index.js
@@ -42,8 +42,46 @@ function log(type, message, details = '') {
console.log(`[${timestamp}] [${type}] ${message}`, details);
}
-// Rate Limiting Storage
+// Rate Limiting & Security
const connectionCounts = new Map(); // ip -> { count, resetTime }
+const failedAuthAttempts = new Map(); // Map
+
+function checkAuthRate(ip, roomId) {
+ const key = `${ip}:${roomId}`;
+ const now = Date.now();
+ const record = failedAuthAttempts.get(key) || { count: 0, lastAttempt: 0 };
+
+ // Block for 15 mins if 5 fails in 2 mins
+ if (record.count >= 5 && (now - record.lastAttempt) < 15 * 60 * 1000) {
+ return false;
+ }
+
+ // Reset if last attempt was long ago
+ if ((now - record.lastAttempt) > 2 * 60 * 1000) {
+ record.count = 0;
+ }
+
+ return true;
+}
+
+function recordAuthFailure(ip, roomId) {
+ const key = `${ip}:${roomId}`;
+ const record = failedAuthAttempts.get(key) || { count: 0, lastAttempt: 0 };
+ record.count++;
+ record.lastAttempt = Date.now();
+ failedAuthAttempts.set(key, record);
+}
+
+// Periodically clean up old auth failure records (every hour)
+setInterval(() => {
+ const now = Date.now();
+ for (const [key, record] of failedAuthAttempts.entries()) {
+ if (now - record.lastAttempt > 60 * 60 * 1000) {
+ failedAuthAttempts.delete(key);
+ }
+ }
+}, 60 * 60 * 1000);
+
const eventCounts = new Map(); // socketId -> { count, resetTime }
function checkConnectionRate(ip) {
@@ -121,6 +159,12 @@ io.on('connection', (socket) => {
}
}
+ const ip = socket.handshake.address;
+ if (!checkAuthRate(ip, roomId)) {
+ socket.emit(EVENTS.ERROR, { message: "Too many failed attempts. Try again later." });
+ return;
+ }
+
let room = rooms.get(roomId);
if (!room) {
@@ -141,6 +185,7 @@ io.on('connection', (socket) => {
} else {
if (room.passwordHash) {
if (!password || !(await bcrypt.compare(password, room.passwordHash))) {
+ recordAuthFailure(ip, roomId);
socket.emit(EVENTS.ERROR, { message: "Invalid password" });
return;
}
@@ -192,6 +237,14 @@ io.on('connection', (socket) => {
});
});
+ socket.on(EVENTS.GET_ROOMS, () => {
+ const list = Array.from(rooms.entries()).map(([id, r]) => ({
+ id,
+ peerCount: r.peers.size
+ }));
+ socket.emit(EVENTS.ROOM_LIST, { rooms: list });
+ });
+
socket.on(EVENTS.LEAVE_ROOM, () => {
const mapping = socketToRoom.get(socket.id);
if (mapping) {
diff --git a/shared/constants.js b/shared/constants.js
index 0d67cc7..7e7c1f1 100644
--- a/shared/constants.js
+++ b/shared/constants.js
@@ -6,7 +6,8 @@ export const PROTOCOL_VERSION = "1.0.0";
export const APP_VERSION = "1.0.0";
export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net';
-export const OFFICIAL_SERVER_TOKEN = '39ce48b42aa442a34b80cfe2d7314177d4c725c4316b1f83aa3a1e2f0f5c5bfd';
+export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net';
+export const OFFICIAL_SERVER_TOKEN = 'koala_secure_access_2026';
export const EVENTS = {
// Connection & Room
@@ -24,7 +25,9 @@ export const EVENTS = {
PEER_STATUS: "peer_status", // Heartbeat from peers
FORCE_SYNC_PREPARE: "force_sync_prepare",
FORCE_SYNC_ACK: "force_sync_ack",
- FORCE_SYNC_EXECUTE: "force_sync_execute"
+ FORCE_SYNC_EXECUTE: "force_sync_execute",
+ GET_ROOMS: "get_rooms",
+ ROOM_LIST: "room_list"
};
export const HEARTBEAT_INTERVAL = 15000; // 15s
diff --git a/website/README.md b/website/README.md
new file mode 100644
index 0000000..1cd52b7
--- /dev/null
+++ b/website/README.md
@@ -0,0 +1,51 @@
+# KoalaSync Landing Page
+
+This directory contains the static marketing website for KoalaSync. It is built using vanilla HTML, CSS, and JavaScript to ensure maximum performance, zero tracking, and easy hosting.
+
+## Features
+- **Privacy First**: No external fonts, scripts, or trackers.
+- **Modern Tech Aesthetic**: Pure CSS animated gradients and glassmorphism.
+- **Smart Join**: Integrated bridge for communication with the KoalaSync browser extension.
+
+## Hosting with Caddy
+
+Caddy is the recommended web server for KoalaSync due to its automatic HTTPS and simple configuration.
+
+### Example Caddyfile
+
+To host the website on `koalasync.shik3i.net`, you can use the following configuration:
+
+```caddy
+koalasync.shik3i.net {
+ # Path to the website directory
+ root * /var/www/koalasync/website
+
+ # Enable static file serving
+ file_server
+
+ # Enable Gzip/Zstd compression
+ encode zstd gzip
+
+ # Security Headers
+ header {
+ # Prevent FLoC tracking
+ Permissions-Policy interest-cohort=()
+ # Security best practices
+ Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+ X-Content-Type-Options nosniff
+ X-Frame-Options DENY
+ Referrer-Policy no-referrer-when-downgrade
+ }
+
+ # Custom 404 page
+ handle_errors {
+ rewrite * /{err.status_code}.html
+ file_server
+ }
+}
+```
+
+### Deployment Steps
+1. Copy the contents of this folder to `/var/www/koalasync/website` on your server.
+2. Update the path in your `Caddyfile`.
+3. Reload Caddy: `caddy reload`.
diff --git a/website/app.js b/website/app.js
index dbfaa46..678fa06 100644
--- a/website/app.js
+++ b/website/app.js
@@ -47,4 +47,120 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
});
+
+ // Invite Detection & Bridge
+ const checkInvite = () => {
+ const isJoinPage = window.location.pathname.includes('join.html');
+
+ // Use a short timeout to let the bridge script initialize its dataset attribute
+ setTimeout(() => {
+ const isInstalled = document.documentElement.dataset.koalasyncInstalled === 'true';
+
+ if (window.location.hash.startsWith('#join:')) {
+ const parts = window.location.hash.split(':');
+ if (parts.length >= 3) {
+ const roomId = parts[1];
+ const password = parts[2];
+
+ if (isJoinPage) {
+ const displayRoom = document.getElementById('display-room-id');
+ const actions = document.getElementById('join-actions');
+ if (displayRoom) displayRoom.textContent = roomId;
+
+ if (actions) {
+ if (!isInstalled) {
+ actions.innerHTML = `
+ GET IT ON CHROME WEBSTORE
+ Download via GitHub
+ The extension is required to join and sync videos.
+ `;
+ } else {
+ actions.innerHTML = `
+ JOIN ROOM NOW
+ ✅ Extension detected and ready.
+ `;
+ }
+ }
+ } else {
+ // Fallback banner for index.html
+ if (!document.getElementById('koala-banner')) {
+ const banner = document.createElement('div');
+ banner.className = 'invite-banner';
+ banner.id = 'koala-banner';
+ banner.innerHTML = `
+
+ `;
+ document.body.prepend(banner);
+ }
+ }
+
+ // Global listener for Join Button
+ document.addEventListener('click', (e) => {
+ if (e.target && e.target.id === 'webJoinBtn') {
+ e.target.textContent = 'JOINING...';
+ e.target.disabled = true;
+ window.dispatchEvent(new CustomEvent('KOALASYNC_JOIN_REQUEST', {
+ detail: { roomId, password }
+ }));
+ }
+ });
+ }
+ }
+ }, 600); // 600ms delay to ensure bridge.js has set the dataset
+ };
+
+ // Listen for status from Extension
+ window.addEventListener('KOALASYNC_STATUS', (e) => {
+ const { success, message } = e.detail;
+ const isJoinPage = window.location.pathname.includes('join.html');
+
+ if (isJoinPage) {
+ const icon = document.getElementById('join-status-icon');
+ const title = document.getElementById('join-title');
+ const actions = document.getElementById('join-actions');
+ const desc = document.getElementById('join-desc');
+
+ if (success) {
+ if (icon) icon.textContent = '✅';
+ title.textContent = 'Erfolgreich!';
+
+ let count = 5;
+ const updateCountdown = () => {
+ desc.textContent = `Du bist dem Raum beigetreten. Dieser Tab schließt sich in ${count} Sekunden...`;
+ if (count <= 0) {
+ window.close();
+ desc.textContent = 'Beitritt erfolgreich! Du kannst diesen Tab jetzt manuell schließen.';
+ } else {
+ count--;
+ setTimeout(updateCountdown, 1000);
+ }
+ };
+ updateCountdown();
+
+ actions.innerHTML = 'TAB JETZT SCHLIESSEN ';
+ } else {
+ if (icon) icon.textContent = '❌';
+ title.textContent = 'Fehler';
+ desc.textContent = `Beitritt fehlgeschlagen: ${message}`;
+ actions.innerHTML = 'ERNEUT VERSUCHEN ';
+ }
+ } else {
+ const banner = document.getElementById('koala-banner');
+ if (banner) {
+ if (success) {
+ banner.style.background = 'var(--success)';
+ banner.innerHTML = '✅ Joined! This tab will close in 3s...
';
+ setTimeout(() => window.close(), 3000);
+ } else {
+ banner.style.background = 'var(--error)';
+ banner.innerHTML = `❌ Error: ${message}
`;
+ }
+ }
+ }
+ });
+
+ checkInvite();
});
diff --git a/website/datenschutz.html b/website/datenschutz.html
new file mode 100644
index 0000000..1e0e4cf
--- /dev/null
+++ b/website/datenschutz.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+ Datenschutz | KoalaSync
+
+
+
+
+
+
+
+
+
+
+
+
+
Datenschutz
+
+ Sicherheit & Privatsphäre
+
+
+
+ 1. Hosting & Logfiles
+
+ Diese Seite wird auf einem privaten Server gehostet. Zur Gewährleistung der Stabilität werden standardmäßige Server-Logs (IP, Browser, Zeit) erhoben, aber nicht mit Personen verknüpft und nach 7 Tagen automatisch gelöscht.
+
+
+
+
+ 2. Keine Drittanbieter & Tracking
+
+ KoalaSync verzichtet bewusst auf Analyse-Tools, Tracking-Cookies oder Werbenetzwerke. Wir laden keine Ressourcen von Drittanbietern (wie Google Fonts) nach, um Ihre Privatsphäre maximal zu schützen.
+
+
+
+
+ 3. Relay-Server Architektur
+
+ Unser Relay-Server arbeitet ausschließlich im Arbeitsspeicher (RAM). Nachrichten zwischen Teilnehmern werden nicht auf Festplatten gespeichert und sind flüchtig. Sobald ein Raum geschlossen wird, werden alle zugehörigen Metadaten sofort gelöscht.
+
+
+
+
+ 4. Brute-Force Schutz
+
+ Zur Sicherheit unserer Nutzer speichern wir fehlgeschlagene Login-Versuche (IP-Adresse und Raum-ID) für maximal 15 Minuten im RAM, um automatisierte Angriffe zu verhindern. Diese Daten werden danach rückstandslos gelöscht.
+
+
+
+
+ 5. Ihre Rechte
+
+ Sie haben das Recht auf Auskunft, Berichtigung oder Löschung Ihrer Daten. Da wir jedoch keine personenbezogenen Daten dauerhaft speichern, ist eine Zuordnung zu Ihrer Person in der Regel technisch nicht möglich.
+
+ Kontakt bei Fragen: [E-Mail anzeigen]
+
+
+
+
+
+
+
+
+
diff --git a/website/impressum.html b/website/impressum.html
new file mode 100644
index 0000000..e74c101
--- /dev/null
+++ b/website/impressum.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+ Impressum | KoalaSync
+
+
+
+
+
+
+
+
+
+
+
+
+
Impressum
+
+ Transparenz & Identifikation
+
+
+
+ Betreiber & Kontakt
+ Administrator KoalaSync (Privatperson)
+ E-Mail: [E-Mail anzeigen]
+
+
+
+ Privatprojekt-Hinweis
+
+ Diese Website ist ein rein privates Hobby-Projekt und dient keinen geschäftsmäßigen Zwecken.
+ Eine Impressumspflicht nach § 5 DDG (ehemals TMG) besteht daher nicht.
+ Diese Angaben erfolgen rein freiwillig zur Transparenz gegenüber der Community.
+
+
+
+
+ Haftung für Inhalte
+
+ Gemäß § 7 Abs.1 DDG sind wir für eigene Inhalte verantwortlich. Nach §§ 8 bis 10 DDG sind wir jedoch nicht verpflichtet,
+ übermittelte oder gespeicherte fremde Informationen zu überwachen.
+
+
+
+
+ Haftung für Links
+
+ Unser Angebot enthält Links zu externen Websites Dritter. Auf deren Inhalte haben wir keinen Einfluss und
+ können daher keine Gewähr für diese fremden Inhalte übernehmen.
+
+
+
+
+ Urheberrecht
+
+ Die durch die Seitenbetreiber erstellten Inhalte auf diesen Seiten unterliegen dem deutschen Urheberrecht.
+ Vervielfältigung, Bearbeitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung.
+
+
+
+
+
+
+
+
+
+
diff --git a/website/index.html b/website/index.html
index e1044f4..e4ccde5 100644
--- a/website/index.html
+++ b/website/index.html
@@ -8,8 +8,6 @@
-
-
@@ -124,7 +122,11 @@
diff --git a/website/join.html b/website/join.html
new file mode 100644
index 0000000..f375379
--- /dev/null
+++ b/website/join.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+ Beitreten | KoalaSync
+
+
+
+
+
+
+
+
+
+
+
+
+
INVITATION DETECTED
+
Ready to sync?
+
+ You've been invited to join a synchronized session.
+
+
+
+
+
+
Detecting extension...
+
+
+
+
+
+
+
+
+
diff --git a/website/style.css b/website/style.css
index 9ddb1bf..4755e77 100644
--- a/website/style.css
+++ b/website/style.css
@@ -1,4 +1,4 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');
+/* KoalaSync Design System - Privacy-Focused (No External Assets) */
:root {
--bg: #0f172a;
@@ -12,6 +12,36 @@
--glass-border: rgba(255, 255, 255, 0.05);
}
+.invite-banner {
+ background: var(--accent);
+ color: white;
+ padding: 0.75rem 0;
+ text-align: center;
+ font-weight: 600;
+ font-size: 0.9rem;
+ position: relative;
+ z-index: 2000;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+}
+
+.btn-banner {
+ background: white;
+ color: var(--accent);
+ padding: 4px 12px;
+ border-radius: 6px;
+ text-decoration: none;
+ font-size: 0.8rem;
+ font-weight: 700;
+ margin-left: 12px;
+ cursor: pointer;
+ border: none;
+ transition: transform 0.2s;
+}
+
+.btn-banner:hover {
+ transform: scale(1.05);
+}
+
* {
margin: 0;
padding: 0;
@@ -19,7 +49,7 @@
}
body {
- font-family: 'Outfit', sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
background-color: var(--bg);
color: var(--text);
line-height: 1.6;
@@ -285,6 +315,64 @@ footer {
transform: translateY(0);
}
+/* --- Legal Pages --- */
+.legal-content {
+ max-width: 800px;
+ margin: 8rem auto 4rem auto;
+ padding: 0 2rem;
+}
+
+.legal-card {
+ background: var(--card);
+ padding: 3rem;
+ border-radius: 24px;
+ border: 1px solid var(--glass-border);
+}
+
+.legal-card h1 {
+ font-size: 2.5rem;
+ margin-bottom: 2rem;
+ text-align: center;
+}
+
+.legal-card h2 {
+ font-size: 1.25rem;
+ margin-top: 1rem;
+ margin-bottom: 0.5rem;
+ color: var(--accent);
+}
+
+.legal-card p {
+ color: var(--text-muted);
+ margin-bottom: 0.5rem;
+ font-size: 0.95rem;
+}
+
+/* Reset global section padding for legal sections specifically */
+.legal-card section {
+ padding: 0 !important;
+ margin-bottom: 0.75rem;
+}
+
+/* --- Join Page Specifics --- */
+.join-card {
+ max-width: 450px;
+ margin: 6rem auto;
+ text-align: center;
+}
+
+.room-badge {
+ display: inline-block;
+ background: rgba(99, 102, 241, 0.1);
+ color: var(--accent);
+ padding: 0.5rem 1rem;
+ border-radius: 99px;
+ font-size: 0.8rem;
+ font-weight: 700;
+ margin-bottom: 1rem;
+ border: 1px solid rgba(99, 102, 241, 0.2);
+}
+
@media (max-width: 768px) {
.hero-grid, .step {
grid-template-columns: 1fr;
@@ -299,4 +387,7 @@ footer {
.nav-links {
display: none;
}
+ .legal-card {
+ padding: 1.5rem;
+ }
}