Split landing CSS by render priority

This commit is contained in:
KoalaDev
2026-07-11 15:48:09 +02:00
parent 7fea11de0a
commit 72d4f04526
19 changed files with 13281 additions and 6583 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ KoalaSync is a specialized tool for **synchronized video playback** across multi
- `server/`: Node.js Relay Server using Socket.IO (WebSocket-only).
- `website/`: **Landing Page** & Invitation Bridge (Marketing, Tutorials, and Downloads).
- **`build.cjs`**: Zero-dependency static site compiler. Translates `template.html` + `locales/*.json``www/`. Also minifies CSS/JS automatically.
- **`www/` is auto-generated**: Never edit files in `www/` directly. Always edit source files (`template.html`, `style.css`, `app.js`, `lang-init.js`, `locales/*.json`) and run `node website/build.cjs` to regenerate. CSS/JS are output as `.min.*` files — a built-in cleanup step removes stale artifacts on each build.
- **`www/` is auto-generated**: Never edit files in `www/` directly. Always edit source files (`template.html`, `style.css`, `styles/*.css`, `app.js`, `lang-init.js`, `locales/*.json`) and run `node website/build.cjs` to regenerate. `style.css` is the development manifest; `style.legacy.css` is an unloaded, byte-identical migration reference. Production creates page-specific and render-priority CSS bundles from `styles/*.css`; CSS/JS are output as `.min.*` files and stale generated assets are removed on each build.
- `shared/`: **Single Source of Truth** for protocol constants, event names, blacklist data, and generated usernames.
- `scripts/`: Development utilities (e.g., `build-extension.cjs`).
- `docker-compose.yml`: Root-level orchestration for the relay server.
@@ -149,7 +149,7 @@ Before starting any task, committing, or pushing, you **MUST** run `git pull --r
3. Implement the handler in `server/index.js` and `background.js`.
### Making Website Changes
1. Edit source files in `website/` (`template.html`, `style.css`, `app.js`, `lang-init.js`, or `locales/*.json`).
1. Edit source files in `website/` (`template.html`, `style.css`, `styles/*.css`, `app.js`, `lang-init.js`, or `locales/*.json`).
2. Run the compiler: `node website/build.cjs`. This generates the multilingual pages in `www/` and minifies CSS/JS.
3. Verify the output: `node --check website/www/app.min.js && node --check website/www/lang-init.min.js`.
4. Test locally: `npx serve website/www` or `python3 -m http.server 8080 -d website/www`.
+2 -1
View File
@@ -4,7 +4,7 @@ This directory contains the static KoalaSync website. It is both the public mark
## Where You Are
- `template.html`, `style.css`, `app.js`, `lang-init.js`, and `locales/*.json` are source files.
- `template.html`, `style.css`, `styles/*.css`, `app.js`, `lang-init.js`, and `locales/*.json` are source files. `style.css` is the local-development manifest; production concatenates the modules in the order declared by `website/build.cjs`.
- `build.cjs` compiles the static site into `website/www/`.
- `website/www/` is generated output. Do not edit files there directly.
- `join.html` handles room-invite links and communicates with the extension through `bridge.js`.
@@ -33,6 +33,7 @@ The website is 100% static HTML, CSS, and JavaScript.
- **Static i18n compiler**: `build.cjs` combines `template.html` with dictionaries in `locales/`.
- **Build-time minification**: Source CSS/JS stays readable; generated output uses `.min.css` and `.min.js`.
- **Strangler CSS architecture**: `styles/*.css` supplies page/runtime bundles; `style.legacy.css` is the byte-identical, unloaded reference monolith. The landing ships a critical shell, a desktop/on-demand demo bundle, and idle-loaded below-fold styles. Legal, join, and alternatives rules stay out of landing bundles.
- **Zero backend**: The compiled site can be hosted by any static file server.
- **Zero external assets**: Fonts, icons, scripts, and images must remain self-hosted.
- **Generated SEO/runtime files**: `version.json`, sitemap, robots, clean URLs, localized pages, and minified assets are copied or generated into `www/`.
+23
View File
@@ -1,6 +1,27 @@
// KoalaSync Landing Page Logic
document.addEventListener('DOMContentLoaded', () => {
const deferredCss = document.body.dataset.deferredCss;
if (deferredCss) {
const loadDeferredCss = () => {
if (document.querySelector('link[data-landing-deferred]')) return;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = deferredCss;
link.dataset.landingDeferred = '';
const integrity = document.body.dataset.deferredCssIntegrity;
if (integrity) {
link.integrity = integrity;
link.crossOrigin = 'anonymous';
}
document.head.appendChild(link);
};
if ('requestIdleCallback' in window) {
window.requestIdleCallback(loadDeferredCss, { timeout: 1500 });
} else {
window.setTimeout(loadDeferredCss, 250);
}
}
// Mockup Video Title Randomization on Load
const SERIES_NAMES = [
'Stranger Things',
@@ -1311,6 +1332,8 @@ document.addEventListener('DOMContentLoaded', () => {
let previousFocus = null;
const openModal = () => {
const demoStyles = document.querySelector('link[data-landing-demo]');
if (demoStyles) demoStyles.media = 'all';
previousFocus = document.activeElement && typeof document.activeElement.focus === 'function'
? document.activeElement
: null;
+67 -1
View File
@@ -132,11 +132,62 @@ async function compile() {
// ── 1. Minify CSS/JS (must happen first so hashes go into HTML) ──
console.log('Minifying CSS/JS...');
const styleRaw = fs.readFileSync(path.join(websiteDir, 'style.css'), 'utf8');
const styleModules = [
'styles/foundation.css',
'styles/hero.css',
'styles/landing-primary.css',
'styles/legal.css',
'styles/landing-controls.css',
'styles/join-spinner.css',
'styles/landing-sections.css',
'styles/join.css',
'styles/landing-faq.css',
'styles/alternatives.css',
'styles/demo.css'
];
// Landing CSS is split by actual render dependency, not by the historical
// section comments in style.legacy.css. The initial bundle contains every
// selector used by the background, navigation, hero, interactive mockup,
// responsive shell, and accessibility states. Below-fold sections load
// after first paint from app.js. Utility-page rules never reach the landing.
const landingCriticalModules = [
'styles/foundation.css',
'styles/hero.css',
'styles/legal.css',
'styles/landing-controls.css',
'styles/landing-demo-gate.css'
];
const landingDemoModules = ['styles/demo.css'];
const landingDeferredModules = [
'styles/landing-primary.css',
'styles/landing-sections.css',
'styles/landing-faq.css'
];
const styleRaw = styleModules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const styleMin = minifyCSS(styleRaw);
const styleHash = sha8(styleMin);
const styleName = `style.${styleHash}.min.css`;
const styleSRI = sha384(styleMin);
const landingStyleRaw = landingCriticalModules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const landingStyleMin = minifyCSS(landingStyleRaw);
const landingStyleName = `landing.${sha8(landingStyleMin)}.min.css`;
const landingStyleSRI = sha384(landingStyleMin);
const landingDemoRaw = landingDemoModules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const landingDemoMin = minifyCSS(landingDemoRaw);
const landingDemoName = `landing-demo.${sha8(landingDemoMin)}.min.css`;
const landingDemoSRI = sha384(landingDemoMin);
const landingDeferredRaw = landingDeferredModules
.map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8'))
.join('');
const landingDeferredMin = minifyCSS(landingDeferredRaw);
const landingDeferredName = `landing-deferred.${sha8(landingDeferredMin)}.min.css`;
const landingDeferredSRI = sha384(landingDeferredMin);
const appRaw = fs.readFileSync(path.join(websiteDir, 'app.js'), 'utf8');
const appMin = await minifyJS(appRaw);
@@ -154,6 +205,9 @@ async function compile() {
const appPct = ((1 - appMin.length / appRaw.length) * 100).toFixed(0);
const langPct = ((1 - langMin.length / langRaw.length) * 100).toFixed(0);
console.log(` CSS: ${styleName} (${(styleMin.length/1024).toFixed(1)} KB, -${stylePct}%)`);
console.log(` Landing CSS: ${landingStyleName} (${(landingStyleMin.length/1024).toFixed(1)} KB)`);
console.log(` Desktop/modal demo CSS: ${landingDemoName} (${(landingDemoMin.length/1024).toFixed(1)} KB)`);
console.log(` Deferred landing CSS: ${landingDeferredName} (${(landingDeferredMin.length/1024).toFixed(1)} KB)`);
console.log(` App: ${appName} (${(appMin.length/1024).toFixed(1)} KB, -${appPct}%)`);
console.log(` Lang: ${langName} (${(langMin.length/1024).toFixed(1)} KB, -${langPct}%)`);
@@ -167,6 +221,9 @@ async function compile() {
// Write minified files
fs.writeFileSync(path.join(wwwDir, styleName), styleMin);
fs.writeFileSync(path.join(wwwDir, landingStyleName), landingStyleMin);
fs.writeFileSync(path.join(wwwDir, landingDemoName), landingDemoMin);
fs.writeFileSync(path.join(wwwDir, landingDeferredName), landingDeferredMin);
fs.writeFileSync(path.join(wwwDir, appName), appMin);
fs.writeFileSync(path.join(wwwDir, langName), langMin);
@@ -463,6 +520,15 @@ async function compile() {
html = html.replace(/(<link\b[^>]*?\bhref=")((?:\.\.\/)*\/?)style\.min\.css"/g, (m, before, prefix) => {
return `${before}${prefix}${styleName}" integrity="${styleSRI}" crossorigin="anonymous"`;
});
html = html.replace(/(<link\b[^>]*?\bhref=")((?:\.\.\/)*\/?)landing\.min\.css"/g, (m, before, prefix) => {
return `${before}${prefix}${landingStyleName}" integrity="${landingStyleSRI}" crossorigin="anonymous"`;
});
html = html.replace(/(<link\b[^>]*?\bhref=")((?:\.\.\/)*\/?)landing-demo\.min\.css"/g, (m, before, prefix) => {
return `${before}${prefix}${landingDemoName}" integrity="${landingDemoSRI}" crossorigin="anonymous"`;
});
html = html.replace(/data-deferred-css="((?:\.\.\/)*\/?)landing-deferred\.min\.css"/g, (m, prefix) => {
return `data-deferred-css="${prefix}${landingDeferredName}" data-deferred-css-integrity="${landingDeferredSRI}"`;
});
html = html.replace(/(<script\b[^>]*?\bsrc=")((?:\.\.\/)*\/?)app\.min\.js"/g, (m, before, prefix) => {
return `${before}${prefix}${appName}" integrity="${appSRI}" crossorigin="anonymous"`;
});
+18 -6576
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
/* --- Alternatives Hub Styles --- */
.guide-card {
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.25s ease, border-color 0.25s ease !important;
}
.guide-card:hover {
transform: translateY(-4px);
background-color: rgba(255, 255, 255, 0.07) !important;
border-color: rgba(255, 255, 255, 0.22) !important;
box-shadow: 0 12px 24px -10px rgba(0, 0, 0, 0.4);
}
.guide-card .btn-primary {
transition: background-color 0.2s ease, transform 0.1s ease;
}
.guide-card .btn-primary:active {
transform: scale(0.97);
}
@media (max-width: 640px) {
.guides-grid {
grid-template-columns: 1fr !important;
gap: 1.25rem !important;
}
}
File diff suppressed because it is too large Load Diff
+1418
View File
File diff suppressed because it is too large Load Diff
+911
View File
@@ -0,0 +1,911 @@
/* --- Hero Section --- */
.hero {
min-height: 100vh;
display: flex;
align-items: flex-start;
padding-top: clamp(8rem, 12vh, 10rem);
padding-bottom: clamp(2rem, 5vh, 4rem);
}
.hero-grid {
display: grid;
/* Single column until the interactive demo appears (1024px). Below that the
headline gets the full width and the install CTAs stay prominent while the
demo stacks underneath, matching the demo's own 1024px breakpoint. */
grid-template-columns: minmax(0, 1fr);
align-content: center;
/* Keep the whole hero vertically centered. Do not use grid alignment to fix
the demo top edge; that moves the headline and breaks the hero balance. */
align-items: center;
gap: 3rem;
text-align: left;
width: 100%;
min-height: calc(100svh - 12.5rem);
}
@media (min-width: 1024px) {
.container {
max-width: 1200px;
}
.hero-grid {
grid-template-columns: 1.35fr 1fr;
gap: 3.5rem;
}
.cta-group {
gap: 0.75rem;
}
.cta-group .btn {
padding: 0.8rem 1.35rem;
}
}
.hero-text h1 {
font-size: clamp(2.9rem, 4.1vw, 4rem);
font-weight: 800;
line-height: 1.12;
margin-bottom: 1.4rem;
padding-bottom: 0.1em;
color: var(--text);
letter-spacing: -0.02em;
}
.hero-text h1 .hero-highlight {
background: linear-gradient(90deg, var(--accent-green), var(--accent-terracotta), var(--accent-green));
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
white-space: nowrap;
animation: hero-gradient-pan 10s linear infinite;
/* background-clip: text only paints inside the layout box; with the
negative h1 letter-spacing the last glyph overhangs it and gets cut
off. Invisible padding extends the paint area without moving text. */
padding-right: 0.08em;
margin-right: -0.08em;
}
@keyframes hero-gradient-pan {
from { background-position: 0% 50%; }
to { background-position: 300% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.hero-text h1 .hero-highlight {
animation: none;
}
}
.hero-text p, .hero-subtitle {
font-size: 1.25rem;
color: var(--text-muted);
margin-bottom: 1.9rem;
max-width: 600px;
font-weight: 400;
line-height: 1.6;
}
.hero-mascot-container {
position: relative;
display: block;
margin-bottom: -0.5rem;
margin-left: 2.2rem;
}
.hero-lookdown-mascot {
display: block;
width: 90px;
height: auto;
}
/* Changelog link as the koala's comic speech bubble. Absolutely positioned
so it never moves the koala out of its spot above the install buttons. */
.koala-speech-bubble {
position: absolute;
left: 108px;
top: 0.2rem;
width: max-content;
max-width: min(240px, calc(100% - 120px));
display: block;
text-decoration: none;
text-align: left;
background: oklch(0.27 0.035 130 / 0.95);
border: 1px solid oklch(0.68 0.13 150 / 0.45);
border-radius: 14px;
border-bottom-left-radius: 4px;
padding: 0.6rem 0.9rem;
line-height: 1.35;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
animation: bubble-float 4s ease-in-out infinite;
transition: border-color 0.25s ease, box-shadow 0.25s ease;
}
/* Tail: outline triangle + fill triangle so it reads as part of the bubble */
.koala-speech-bubble::before,
.koala-speech-bubble::after {
content: '';
position: absolute;
width: 0;
height: 0;
}
.koala-speech-bubble::before {
left: -11px;
bottom: 10px;
border-top: 8px solid transparent;
border-bottom: 4px solid transparent;
border-right: 11px solid oklch(0.68 0.13 150 / 0.45);
}
.koala-speech-bubble::after {
left: -9px;
bottom: 11px;
border-top: 7px solid transparent;
border-bottom: 3px solid transparent;
border-right: 10px solid oklch(0.27 0.035 130 / 0.95);
}
.koala-speech-bubble:hover {
border-color: oklch(0.68 0.13 150 / 0.85);
box-shadow: 0 8px 28px oklch(0.68 0.13 150 / 0.35);
}
.koala-speech-bubble .version-text-en,
.koala-speech-bubble .version-text-de {
display: block;
}
.koala-speech-bubble .bubble-title {
display: block;
font-size: 0.85rem;
font-weight: 800;
color: var(--text);
letter-spacing: 0;
white-space: nowrap;
}
.koala-speech-bubble .bubble-sub {
display: block;
font-size: 0.72rem;
font-weight: 600;
color: var(--accent);
margin-top: 2px;
}
html.theme-light .koala-speech-bubble {
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 8px 24px oklch(0.20 0.025 140 / 0.12);
}
html.theme-light .koala-speech-bubble::after {
border-right-color: rgba(255, 255, 255, 0.96);
}
@keyframes bubble-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
@media (prefers-reduced-motion: reduce) {
.koala-speech-bubble {
animation: none;
}
}
@media (max-width: 768px) {
/* The koala keeps standing on the active install CTA; app.js updates the
offset from the real button geometry when the row wraps. */
.hero-mascot-container {
--hero-mascot-offset: 0px;
margin-left: 0;
display: flex;
justify-content: center;
}
.hero-lookdown-mascot {
transform: translateX(var(--hero-mascot-offset));
}
.koala-speech-bubble {
left: calc(50% + var(--hero-mascot-offset) + 50px);
right: 0.75rem;
top: 0.3rem;
max-width: 240px;
padding: 0.5rem 0.75rem;
}
.koala-speech-bubble .bubble-title {
font-size: 0.8rem;
}
.koala-speech-bubble .bubble-sub {
font-size: 0.68rem;
}
/* The relative release time is too much text for the small mobile bubble */
.koala-speech-bubble .bubble-ago {
display: none;
}
}
.cta-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
/* Quiet trust line under the install buttons; answers "is it free, is it
legit, do I need an account" right at the moment of the click decision. */
.hero-text .cta-trust {
margin-top: 0.85rem;
margin-bottom: 0;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-muted);
opacity: 0.75;
letter-spacing: 0.01em;
max-width: none;
}
.btn {
padding: 0.85rem 1.75rem;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1), color 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
}
.btn-primary {
background: linear-gradient(135deg, oklch(0.48 0.13 150), oklch(0.39 0.10 145));
color: #fff;
border: 1px solid oklch(0.72 0.11 95 / 0.24);
box-shadow: 0 12px 24px -8px oklch(0.38 0.11 148 / 0.65), 0 0 0 1px oklch(0.80 0.08 90 / 0.06) inset;
}
.btn-primary:hover {
background: linear-gradient(135deg, oklch(0.54 0.14 150), oklch(0.44 0.11 145));
transform: translateY(-2px);
box-shadow: 0 18px 32px -8px oklch(0.38 0.11 148 / 0.75), 0 0 18px oklch(0.68 0.13 150 / 0.18);
}
.btn-firefox {
background: oklch(0.62 0.14 45);
color: white;
box-shadow: 0 10px 15px -3px oklch(0.62 0.14 45 / 0.3);
}
.btn-firefox:hover {
background: oklch(0.68 0.14 45);
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px oklch(0.62 0.14 45 / 0.4);
}
.btn-secondary {
background: var(--card);
color: var(--text);
border: 1px solid var(--glass-border);
}
.btn-secondary:hover {
background: oklch(0.30 0.03 125);
transform: translateY(-2px);
}
.btn-demo-mobile {
display: none;
border: 1px solid oklch(0.68 0.13 150 / 0.34);
cursor: pointer;
font: inherit;
}
.btn-demo-mobile svg {
flex-shrink: 0;
}
/* Install buttons: compact by default (just the browser name) so all three
CTAs fit one line; the detected browser expands to the full call to action.
The GitHub button stays compact always. */
.btn-label-full { display: none; }
.btn-install:not(.is-detected),
.btn-compact {
padding: 0.62rem 1.05rem;
font-size: 0.9rem;
gap: 0.4rem;
}
.btn-install.is-detected .btn-label-short { display: none; }
.btn-install.is-detected .btn-label-full { display: inline; }
.version-badge {
display: inline-block;
background: oklch(0.68 0.13 150 / 0.1);
color: var(--accent);
padding: 0.4rem 1rem;
border-radius: 99px;
font-size: 0.8rem;
font-weight: 700;
margin-bottom: 1.5rem;
border: 1px solid oklch(0.68 0.13 150 / 0.3);
letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, background 0.2s;
}
.version-badge-below {
margin-bottom: 0;
margin-top: 1.5rem;
}
.version-badge:hover {
background: oklch(0.68 0.13 150 / 0.2);
box-shadow: 0 0 16px oklch(0.68 0.13 150 / 0.3);
transform: translateY(-1px);
}
/* --- Interactive CSS Extension Mockup --- */
.hero-mockup-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.extension-mockup {
width: 100%;
max-width: 320px;
height: 528px;
background: oklch(0.20 0.025 140);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 0 0 20px oklch(0.68 0.13 150 / 0.15);
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
overflow: hidden;
position: relative;
user-select: none;
padding: 14px;
padding-bottom: 0;
box-sizing: border-box;
}
.mock-header-row {
display: flex;
align-items: center;
margin-bottom: 12px;
flex-shrink: 0;
}
.mock-header-title {
font-size: 18px;
margin: 0;
color: var(--accent);
letter-spacing: 1px;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 800;
}
.mock-header-title picture {
display: contents;
}
.mock-header-title img {
width: 28px;
height: 28px;
object-fit: contain;
border-radius: 6px;
}
.mock-version-link {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--text-muted);
text-decoration: none;
opacity: 0.5;
transition: opacity 0.2s, color 0.2s;
cursor: pointer;
}
.mock-version-link:hover {
opacity: 1;
color: var(--accent);
}
/* Close button for the hero demo popup (hidden in the static mobile mockup) */
.mock-close-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
margin-left: 8px;
padding: 0;
flex-shrink: 0;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-muted);
cursor: pointer;
transition: color 0.2s, background 0.2s;
}
.mock-close-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
@media (max-width: 1024px) {
.mock-close-btn {
display: none;
}
}
.mock-tabs {
display: flex;
gap: 4px;
background: oklch(0.27 0.035 130);
padding: 4px;
border-radius: 12px;
margin-bottom: 14px;
flex-shrink: 0;
}
.mock-tab {
flex: 1;
background: none;
border: none;
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 600;
padding: 7px 4px;
cursor: pointer;
border-radius: 8px;
transition: color 0.2s, background-color 0.2s;
text-align: center;
}
.mock-tab:hover {
color: var(--text);
}
.mock-tab.active {
background: var(--accent);
color: var(--text-on-green);
}
.mock-body {
flex: 1;
overflow-y: auto;
padding: 0;
padding-bottom: 12px;
display: flex;
flex-direction: column;
font-size: 0.8rem;
scrollbar-width: none; /* Firefox */
}
.mock-body::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.mock-screen {
display: none;
flex-direction: column;
gap: 10px;
}
.mock-screen.active {
display: flex;
}
/* Common Mockup Components */
.mock-card {
background: oklch(0.27 0.035 130);
border: 1px solid oklch(0.32 0.03 125);
border-radius: 8px;
padding: 10px;
}
.mock-label {
font-size: 0.65rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.3rem;
}
.mock-section-label {
display: block;
font-size: 11px;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 4px;
font-weight: 700;
}
.mock-input {
background: oklch(0.27 0.035 130);
border: 1px solid oklch(0.32 0.03 125);
border-radius: 8px;
color: white;
padding: 8px 10px;
font-size: 0.75rem;
width: 100%;
outline: none;
}
.mock-btn {
background: var(--accent);
color: var(--text-on-green);
border: none;
border-radius: 6px;
padding: 0.5rem;
font-weight: 600;
font-size: 0.75rem;
cursor: pointer;
transition: background 0.2s;
text-align: center;
}
.mock-btn:hover {
background: oklch(0.60 0.12 150);
}
/* Room Tab Details */
.mock-joined-room {
display: flex;
justify-content: space-between;
align-items: center;
background: oklch(0.68 0.13 150 / 0.1);
border: 1px solid oklch(0.68 0.13 150 / 0.2);
border-radius: 8px;
padding: 0.6rem 0.75rem;
}
.mock-room-name {
font-weight: 700;
color: var(--accent);
font-size: 0.85rem;
}
.mock-server-badge {
background: oklch(0.68 0.13 150 / 0.2);
color: var(--accent);
font-size: 0.6rem;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
}
.mock-invite-box {
display: flex;
gap: 0.35rem;
}
.mock-invite-box input {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.demo-room-empty {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 24px 10px;
text-align: center;
}
.demo-room-empty img {
border-radius: 10px;
opacity: 0.85;
box-shadow: 0 4px 12px oklch(0.68 0.13 150 / 0.2);
}
.demo-room-empty-text {
font-size: 0.8rem;
color: var(--text-muted);
font-weight: 600;
}
.demo-create-room-btn {
background: var(--accent);
border: none;
color: var(--text-on-green);
font-weight: 700;
padding: 10px 18px;
border-radius: 10px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.demo-create-room-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px oklch(0.68 0.13 150 / 0.4);
}
.demo-create-room-btn:active {
transform: scale(0.96);
}
.mock-peer-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.mock-peer-item {
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.03);
border-radius: 8px;
padding: 0.45rem 0.55rem;
}
.mock-peer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.2rem;
}
.mock-peer-name {
font-weight: 700;
color: var(--text);
}
.mock-peer-meta {
font-size: 0.65rem;
color: var(--text-muted);
}
.mock-peer-tab {
font-size: 0.7rem;
color: var(--accent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mock-peer-status {
font-size: 0.65rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.25rem;
}
/* Sync Tab Details */
.mock-target-box {
display: flex;
align-items: center;
gap: 0.4rem;
background: oklch(0.62 0.14 45 / 0.1);
border: 1px solid oklch(0.62 0.14 45 / 0.2);
color: oklch(0.72 0.13 50);
padding: 0.5rem 0.6rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.7rem;
}
.mock-ctrl-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.mock-btn-play { background: oklch(0.68 0.13 150); }
.mock-btn-play:hover { background: oklch(0.60 0.12 150); }
.mock-btn-pause { background: oklch(0.55 0.15 25); color: #fff; }
.mock-btn-pause:hover { background: oklch(0.48 0.14 25); }
.mock-btn-force { background: var(--accent); grid-column: span 2; }
/* Contrast-safe palette for the decorative hero demo. Lighthouse/axe audits
sighted text contrast even inside aria-hidden, so every walkthrough state
must meet WCAG AA. Accent shown as text gets a lighter indigo; buttons that
put white text on a colour get a darker shade (these beat inline styles). */
.hero-demo-scene {
--accent: oklch(0.80 0.10 150);
--demo-sky-top: oklch(0.22 0.055 152);
--demo-sky-mid: oklch(0.145 0.035 150);
--demo-sky-bottom: oklch(0.075 0.018 145);
--demo-glow: oklch(0.76 0.13 150 / 0.34);
--demo-stars: oklch(0.94 0.025 95);
--demo-moon: oklch(0.97 0.025 92);
--demo-mountain: oklch(0.18 0.055 148 / 0.9);
--demo-midground: oklch(0.22 0.065 145 / 0.88);
--demo-foreground: oklch(0.065 0.025 145);
--demo-card: oklch(0.18 0.032 142);
--demo-toolbar: oklch(0.12 0.025 142);
--demo-control-fade: oklch(0.055 0.015 145 / 0.94);
--demo-track: oklch(0.92 0.02 95 / 0.2);
}
.hero-demo-scene .mock-tab.active,
.hero-demo-scene .mock-btn-force,
.hero-demo-scene #demo-force-sync,
.hero-demo-scene .demo-create-room-btn {
background: oklch(0.42 0.09 150) !important;
color: #fff;
}
.hero-demo-scene .mock-btn-play {
background: oklch(0.50 0.11 150) !important;
}
.hero-demo-scene .mock-btn-pause,
.hero-demo-scene #demo-room-joined > button.mock-btn {
background: oklch(0.45 0.14 25) !important;
}
.hero-demo-scene .mock-version-link {
opacity: 0.85;
}
html.theme-light .hero-demo-scene {
--accent: oklch(0.60 0.12 150);
--demo-sky-top: oklch(0.86 0.085 78);
--demo-sky-mid: oklch(0.76 0.095 112);
--demo-sky-bottom: oklch(0.54 0.10 150);
--demo-glow: oklch(0.92 0.13 72 / 0.44);
--demo-stars: oklch(0.98 0.025 90);
--demo-moon: oklch(0.99 0.025 88);
--demo-mountain: oklch(0.48 0.095 150 / 0.82);
--demo-midground: oklch(0.40 0.10 148 / 0.8);
--demo-foreground: oklch(0.24 0.075 145);
--demo-card: oklch(0.985 0.008 95);
--demo-toolbar: oklch(0.955 0.014 100);
--demo-control-fade: oklch(0.16 0.035 145 / 0.88);
--demo-track: oklch(0.98 0.01 95 / 0.34);
}
html.theme-light .hero-demo-scene .demo-tab-card {
background: var(--demo-card);
border-color: oklch(0.20 0.025 140 / 0.1);
box-shadow:
0 18px 38px oklch(0.20 0.025 140 / 0.16),
0 0 28px oklch(0.82 0.10 85 / 0.14),
inset 0 1px rgba(255, 255, 255, 0.9);
}
html.theme-light .hero-demo-scene .demo-tab-titlebar {
background: var(--demo-toolbar);
border-bottom-color: oklch(0.88 0.015 90);
color: oklch(0.45 0.02 110);
}
html.theme-light .hero-demo-scene .demo-tab-dots i {
background: oklch(0.78 0.02 90);
}
html.theme-light .hero-demo-scene .demo-ext-launcher {
background: #ffffff;
border-color: oklch(0.78 0.02 90);
}
html.theme-light .hero-demo-scene .demo-sync-chip {
background: oklch(0.55 0.12 150 / 0.1);
border-color: oklch(0.55 0.12 150 / 0.26);
color: oklch(0.40 0.10 150);
}
.mock-status-display {
display: flex;
align-items: center;
justify-content: space-between;
}
.mock-status-pill {
display: inline-flex;
align-items: center;
gap: 0.3rem;
background: oklch(0.68 0.13 150 / 0.1);
border: 1px solid oklch(0.68 0.13 150 / 0.2);
color: oklch(0.68 0.13 150);
padding: 2px 8px;
border-radius: 99px;
font-size: 0.65rem;
font-weight: 700;
}
.mock-log-item {
font-size: 0.7rem;
color: var(--text-muted);
border-left: 2px solid var(--accent);
padding-left: 0.4rem;
margin-top: 0.4rem;
}
/* Settings Tab Details */
.mock-settings-row {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin-bottom: 0.6rem;
}
.mock-settings-row input[type="checkbox"] {
margin-top: 0.2rem;
cursor: pointer;
}
.mock-settings-row label {
cursor: pointer;
font-weight: 600;
}
.mock-settings-row p {
font-size: 0.65rem;
color: var(--text-muted);
margin-top: 0.1rem;
}
/* Mock Toggle Switch Details */
.mock-toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
flex-shrink: 0;
}
.mock-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.mock-slider {
position: absolute;
cursor: default;
top: 0; left: 0; right: 0; bottom: 0;
background-color: oklch(0.32 0.03 125);
transition: .3s;
border-radius: 20px;
}
.mock-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: oklch(0.68 0.02 100);
transition: .3s;
border-radius: 50%;
}
input:checked + .mock-slider {
background-color: var(--accent);
}
input:checked + .mock-slider:before {
transform: translateX(16px);
background-color: white;
}
/* Dev Tab Details */
.mock-diagnostic-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.4rem;
}
.mock-diag-box {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 6px;
padding: 0.4rem;
}
.mock-diag-box .diag-val {
font-family: monospace;
font-weight: 700;
color: var(--accent);
font-size: 0.75rem;
}
+41
View File
@@ -0,0 +1,41 @@
/* --- Join Page Loading Spinner --- */
.join-spinner {
width: 36px;
height: 36px;
border: 3px solid oklch(0.68 0.13 150 / 0.2);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 0.75rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* --- Reduced Motion --- */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
[data-reveal] {
opacity: 1;
transform: none;
}
.illus-typing-text::after {
animation: none;
}
.illus-timeline-progress {
animation: none;
width: 60%;
}
.illus-sync-pulse {
animation: none;
}
.illus-extension-icon {
animation: none;
}
}
+141
View File
@@ -0,0 +1,141 @@
/* --- Join & Invitation Page Visuals --- */
.join-status-visual {
margin-bottom: 2rem;
position: relative;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.join-status-mascot {
display: block;
width: 140px;
height: auto;
z-index: 2;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));
}
.join-status-mascot.searching-mascot {
width: 175px; /* Slightly wider due to antenna to keep main body scale identical */
}
.join-status-pulse {
animation: mascot-gentle-pulse 2s ease-in-out infinite;
}
@keyframes mascot-gentle-pulse {
0% {
transform: scale(0.96);
filter: drop-shadow(0 4px 10px oklch(0.68 0.13 150 / 0.15));
}
50% {
transform: scale(1.04);
filter: drop-shadow(0 8px 20px oklch(0.68 0.13 150 / 0.35));
}
100% {
transform: scale(0.96);
filter: drop-shadow(0 4px 10px oklch(0.68 0.13 150 / 0.15));
}
}
.legal-mascot {
display: block;
width: 180px;
height: auto;
margin: -0.75rem auto 0.4rem auto;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.legal-mascot:hover {
transform: scale(1.06) translateY(-4px) rotate(1.5deg);
}
.selfhost-mascot {
display: block;
width: 300px;
height: auto;
margin: 0 auto;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.selfhost-mascot:hover {
transform: scale(1.05) translateY(-4px) rotate(-1deg);
}
.status-ring {
position: absolute;
width: 90px;
height: 90px;
border-radius: 50%;
background: oklch(0.68 0.13 150 / 0.05);
border: 2px dashed oklch(0.68 0.13 150 / 0.25);
z-index: 1;
}
.status-ring.active-pulse {
animation: radar-pulse 3s cubic-bezier(0.25, 0.8, 0.25, 1) infinite;
}
@keyframes radar-pulse {
0% {
transform: scale(0.85);
opacity: 0.9;
border-color: oklch(0.68 0.13 150 / 0.3);
box-shadow: 0 0 0 0px oklch(0.68 0.13 150 / 0.15);
}
50% {
transform: scale(1.18);
opacity: 0.35;
border-color: oklch(0.75 0.10 150 / 0.2);
box-shadow: 0 0 0 15px oklch(0.75 0.10 150 / 0);
}
100% {
transform: scale(0.85);
opacity: 0.9;
border-color: oklch(0.68 0.13 150 / 0.3);
box-shadow: 0 0 0 0px oklch(0.68 0.13 150 / 0.15);
}
}
.join-card-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
}
.join-card-actions .btn {
width: 100%;
justify-content: center;
padding: 1.1rem;
font-size: 0.88rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.join-card-actions .btn svg {
margin-right: 0.3rem;
flex-shrink: 0;
}
.join-card-actions .btn img {
margin-right: 0.3rem;
flex-shrink: 0;
}
.btn-success {
background: var(--accent-green);
color: var(--text-on-green) !important;
box-shadow: 0 10px 15px -3px oklch(0.68 0.13 150 / 0.3);
}
.btn-success:hover {
background: oklch(0.60 0.12 150);
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px oklch(0.68 0.13 150 / 0.4);
}
+332
View File
@@ -0,0 +1,332 @@
/* --- Language Toggle --- */
html.lang-de [lang="en"] {
display: none !important;
}
html:not(.lang-de) [lang="de"] {
display: none !important;
}
/* --- Modern Glassmorphic Language Selector --- */
.lang-select-container {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
background: oklch(0.27 0.035 130 / 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 9999px;
padding: 6px 14px;
transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--text-muted);
}
.lang-select-container:hover {
background: oklch(0.27 0.035 130 / 0.6);
border-color: oklch(0.68 0.13 150 / 0.3);
color: var(--text);
box-shadow: 0 0 15px oklch(0.68 0.13 150 / 0.1);
}
.lang-select-container .globe-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
opacity: 0.8;
transition: transform 0.5s ease;
}
.lang-select-container:hover .globe-icon {
transform: rotate(15deg);
opacity: 1;
}
.lang-select-container .chevron-icon {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
pointer-events: none;
opacity: 0.6;
}
.lang-select-container:hover .chevron-icon {
opacity: 1;
}
.lang-dropdown {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
border: none;
outline: none;
font-family: inherit;
font-size: 0.8rem;
font-weight: 600;
color: currentColor;
cursor: pointer;
padding: 0 28px 0 0;
margin: 0;
width: auto;
min-width: 110px;
overflow: hidden;
}
@supports (appearance: base-select) {
.lang-dropdown::picker(select) {
appearance: base-select;
background-color: oklch(0.20 0.025 140);
border: 1px solid oklch(0.32 0.03 125);
border-radius: 8px;
padding: 4px;
font-family: 'Twemoji Country Flags', inherit;
}
}
.lang-dropdown option {
background: oklch(0.20 0.025 140);
color: white;
font-family: inherit;
font-size: 0.9rem;
}
/* Light theme: the pill was designed on the dark glass recipe by day it
becomes frosted white so it sits in the same family as the buttons. */
html.theme-light .lang-select-container {
background: rgba(255, 255, 255, 0.6);
border-color: oklch(0.28 0.035 140 / 0.14);
}
html.theme-light .lang-select-container:hover {
background: rgba(255, 255, 255, 0.85);
border-color: oklch(0.56 0.13 150 / 0.35);
box-shadow: 0 0 15px oklch(0.56 0.13 150 / 0.12);
}
html.theme-light .lang-dropdown option {
background: #ffffff;
color: oklch(0.21 0.03 140);
}
@supports (appearance: base-select) {
html.theme-light .lang-dropdown::picker(select) {
background-color: #ffffff;
border-color: oklch(0.28 0.035 140 / 0.18);
}
}
/* --- Hamburger Menu --- */
.hamburger {
display: none;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text);
font-size: 1.5rem;
cursor: pointer;
padding: 0.25rem;
line-height: 1;
}
/* --- Feature Cards: subtle differentiation --- */
.feature-card:nth-child(1) { --card-accent: oklch(0.68 0.13 150 / 0.06); }
.feature-card:nth-child(2) { --card-accent: oklch(0.62 0.14 45 / 0.06); }
.feature-card:nth-child(3) { --card-accent: oklch(0.68 0.13 150 / 0.06); }
.feature-card:nth-child(4) { --card-accent: oklch(0.75 0.10 150 / 0.06); }
.feature-card:nth-child(5) { --card-accent: oklch(0.68 0.14 45 / 0.06); }
.feature-card:nth-child(6) { --card-accent: oklch(0.62 0.14 45 / 0.06); }
.feature-card:nth-child(7) { --card-accent: oklch(0.62 0.14 45 / 0.06); }
.feature-card:nth-child(8) { --card-accent: oklch(0.68 0.13 150 / 0.06); }
.feature-card {
background: linear-gradient(135deg, var(--card-accent, transparent), oklch(0.27 0.035 130 / 0.45));
}
/* --- Compatibility Section --- */
.compat-section {
padding: 0.5rem 0 2.5rem 0;
text-align: center;
background: transparent;
border-bottom: 0;
}
.compat-mascot {
display: block;
margin: 0 auto 0.3rem auto;
max-width: 270px;
width: 100%;
height: auto;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.compat-mascot:hover {
transform: scale(1.05) translateY(-5px) rotate(1deg);
}
.compat-heading {
font-size: 1rem;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 1.4rem;
}
.compat-row {
display: flex;
justify-content: center;
align-items: center;
gap: 2.4rem 3rem;
flex-wrap: wrap;
}
.compat-logo {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--text-muted);
transition: opacity 0.3s;
opacity: 0.55;
}
.compat-logo:hover {
opacity: 0.85;
}
.compat-logo img {
width: 28px;
height: 28px;
display: block;
}
.compat-logo img.compat-logo-wide {
width: 52px;
height: 28px;
object-fit: contain;
}
.compat-logo img.compat-logo-disneyplus-mark {
width: 60px;
height: 30px;
object-fit: contain;
filter: brightness(0) saturate(100%) invert(63%) sepia(12%) saturate(621%) hue-rotate(203deg) brightness(92%) contrast(88%);
}
html.theme-light .compat-logo img.compat-logo-disneyplus-mark {
filter: none;
}
@media (max-width: 700px) {
.compat-section {
padding: 0 0 1.6rem;
}
.compat-mascot {
max-width: 220px;
margin-bottom: 0.1rem;
}
.compat-heading {
font-size: 0.92rem;
line-height: 1.55;
margin-bottom: 1rem;
}
.compat-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1.55rem 0.75rem;
align-items: start;
}
.compat-logo {
gap: 0.35rem;
font-size: 0.72rem;
}
.compat-logo img {
width: 26px;
height: 26px;
}
.compat-logo img.compat-logo-wide {
width: 48px;
height: 26px;
}
.compat-logo img.compat-logo-disneyplus-mark {
width: 56px;
height: 28px;
}
.compat-logo-jellyfin {
display: none;
}
.compat-more {
grid-column: 1 / -1;
justify-self: center;
white-space: nowrap;
}
}
.compat-more {
opacity: 0.4;
cursor: help;
flex-direction: row;
gap: 0.35rem;
position: relative;
}
.compat-more:hover {
opacity: 0.6;
}
.compat-more-icon {
font-size: 1.1rem;
font-weight: 700;
color: var(--accent);
line-height: 1;
}
.compat-tooltip {
display: none;
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
background: var(--card);
color: var(--text-muted);
padding: 0.6rem 0.9rem;
border-radius: 8px;
font-size: 0.72rem;
font-weight: 500;
white-space: nowrap;
border: 1px solid var(--glass-border);
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
z-index: 10;
line-height: 1.4;
}
.compat-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--glass-border);
}
.compat-more:hover .compat-tooltip {
display: block;
}
+14
View File
@@ -0,0 +1,14 @@
/* Initial mobile shell for the deferred interactive hero demo. */
@media (max-width: 900px) {
.hero-grid > .hero-mockup-wrapper {
display: none !important;
}
.hero-github-cta {
display: none;
}
.btn-demo-mobile {
display: inline-flex;
}
}
+213
View File
@@ -0,0 +1,213 @@
/* --- Section collapse (FAQ, self-hosting): closed by default to keep the page short --- */
.section-collapse {
margin: 0 auto;
}
.faq-section-collapse {
max-width: 800px;
}
.collapse-summary {
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
list-style: none;
cursor: pointer;
font-weight: 700;
font-size: 1rem;
color: var(--accent);
background: oklch(0.68 0.13 150 / 0.08);
border: 1px solid oklch(0.68 0.13 150 / 0.3);
border-radius: 99px;
padding: 0.85rem 2rem;
max-width: 360px;
margin: 0 auto;
transition: background 0.3s, box-shadow 0.3s, transform 0.3s;
}
.collapse-summary::-webkit-details-marker {
display: none;
}
.collapse-summary:hover {
background: oklch(0.68 0.13 150 / 0.16);
box-shadow: 0 0 20px oklch(0.68 0.13 150 / 0.25);
transform: translateY(-2px);
}
.collapse-summary .collapse-chevron {
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.section-collapse[open] .collapse-chevron {
transform: rotate(180deg);
}
.collapse-label-hide { display: none; }
.section-collapse[open] .collapse-label-show { display: none; }
.section-collapse[open] .collapse-label-hide { display: inline; }
.section-collapse[open] .collapse-summary {
margin-bottom: 2rem;
}
.section-collapse[open] .collapse-content {
animation: collapse-content-in 0.4s ease-out;
}
@keyframes collapse-content-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.section-collapse[open] .collapse-content {
animation: none;
}
}
/* --- FAQ Section --- */
.faq-grid {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 800px;
margin: 0 auto;
}
.faq-item {
background: oklch(0.27 0.035 130 / 0.45);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 1.5rem 2rem;
cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
overflow: hidden;
}
.faq-item:hover {
border-color: oklch(0.68 0.13 150 / 0.3);
box-shadow: 0 8px 20px oklch(0.68 0.13 150 / 0.1);
}
.faq-item[open] {
border-color: oklch(0.68 0.13 150 / 0.4);
background: oklch(0.27 0.035 130 / 0.65);
}
.faq-item summary {
font-weight: 700;
font-size: 1.05rem;
color: var(--text);
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.faq-item summary::-webkit-details-marker {
display: none;
}
.faq-item summary::after {
content: '+';
font-size: 1.4rem;
color: var(--accent);
font-weight: 300;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
flex-shrink: 0;
transform-origin: center;
}
.faq-item[open] summary::after {
transform: rotate(45deg);
}
.faq-item p {
color: var(--text-muted);
font-size: 0.95rem;
line-height: 1.6;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
/* --- Focus states for interactive cards --- */
.feature-card:focus-visible,
.use-case-card:focus-visible,
.compat-logo:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
transform: translateY(-5px);
}
.feature-card,
.use-case-card,
.compat-logo,
section[id],
header[id] {
scroll-margin-top: 100px;
}
/* --- Screen-reader-only (also crawlable by search engines) --- */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* --- Skip-to-content accessibility link --- */
.skip-link {
position: absolute;
left: -9999px;
top: 0;
background: var(--accent);
color: var(--text-on-green);
padding: 0.75rem 1.25rem;
border-radius: 0 0 8px 0;
font-weight: 700;
z-index: 9999;
text-decoration: none;
}
.skip-link:focus {
left: 0;
}
/* --- Reduced motion support --- */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.join-status-pulse { animation: none; }
.status-ring.active-pulse { animation: none; }
}
/* --- will-change hints for animated elements --- */
.join-status-pulse,
.status-ring.active-pulse,
.hero-mockup-wrapper,
.cta-group .btn {
will-change: transform, opacity;
}
@media (prefers-reduced-motion: reduce) {
.join-status-pulse,
.status-ring.active-pulse,
.hero-mockup-wrapper,
.cta-group .btn {
}
}
File diff suppressed because it is too large Load Diff
+462
View File
@@ -0,0 +1,462 @@
/* --- Use Cases / Anwendungsszenarien Section --- */
.use-cases-section {
padding: 6rem 0;
position: relative;
border-bottom: 0;
}
.use-cases-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 2rem;
margin-top: 1rem;
}
.use-cases-feature-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 900px) {
.use-cases-feature-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.25rem;
}
}
@media (max-width: 600px) {
.use-cases-feature-grid {
grid-template-columns: minmax(0, 1fr);
gap: 1rem;
}
}
.use-case-card {
background: oklch(0.27 0.035 130 / 0.4);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 1.2rem 1.6rem 1.7rem 1.6rem;
text-align: left;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
gap: 0.8rem;
position: relative;
overflow: hidden;
}
.use-case-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, transparent, var(--card-accent-border, var(--accent)), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.use-case-card:nth-child(1) { --card-accent-border: oklch(0.68 0.13 150); }
.use-case-card:nth-child(2) { --card-accent-border: oklch(0.62 0.14 45); }
.use-case-card:nth-child(3) { --card-accent-border: oklch(0.75 0.10 150); }
.use-case-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
border-color: rgba(255, 255, 255, 0.15);
}
.use-case-card:hover::before {
opacity: 1;
}
.use-case-image {
display: block;
width: 100%;
max-width: 272px;
height: 150px;
object-fit: contain;
border-radius: 8px;
margin: 0 auto 0 auto;
}
.cta-github-mascot {
display: block;
width: 100%;
max-width: 250px;
height: auto;
margin: 1.5rem auto;
}
.features-questions-mascot {
display: block;
width: 100%;
max-width: 250px;
height: auto;
margin: -1.5rem auto 0.2rem auto;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.features-questions-mascot:hover {
transform: scale(1.05) translateY(-4px) rotate(-1.5deg);
}
.comparison-mascot {
display: block;
width: 100%;
max-width: 260px;
height: auto;
margin: -2.2rem auto 0.2rem auto;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.comparison-mascot:hover {
transform: scale(1.05) translateY(-4px) rotate(1deg);
}
.use-case-icon {
font-size: 1.6rem;
margin-right: 0.6rem;
display: inline-block;
vertical-align: middle;
filter: drop-shadow(0 2px 8px rgba(0,0,0,0.2));
}
.use-case-card h3 {
font-size: 1.25rem;
font-weight: 700;
margin: 0;
color: white;
}
.use-case-card p {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-muted);
margin: 0;
}
/* --- Comparison Section --- */
.comparison-section {
padding: 6rem 0;
background: transparent;
border-bottom: 0;
}
#self-hosting,
#faq,
#faq + section,
footer {
background: var(--section-tint-strong);
}
#self-hosting {
border-top: 1px solid var(--glass-border);
}
.comparison-table-wrapper {
overflow-x: auto;
background: oklch(0.27 0.035 130 / 0.35);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
margin-top: 1rem;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 0.95rem;
}
.comparison-table th,
.comparison-table td {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.comparison-table th {
background: oklch(0.20 0.025 140 / 0.6);
font-weight: 700;
color: white;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.08em;
}
.comparison-table th.highlight,
.comparison-table td.highlight {
background: oklch(0.68 0.13 150 / 0.08);
}
.comparison-table tbody tr:hover {
background: rgba(255, 255, 255, 0.02);
}
.comparison-table tbody tr:last-child td {
border-bottom: none;
}
.feat-name {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.feat-name strong {
color: white;
font-weight: 600;
}
.feat-desc {
font-size: 0.8rem;
color: var(--text-muted);
}
.check {
color: oklch(0.68 0.13 150);
font-weight: 700;
}
.cross {
color: oklch(0.68 0.17 25);
font-weight: 600;
opacity: 1;
}
@media (max-width: 768px) {
.comparison-table th,
.comparison-table td {
padding: 1rem;
font-size: 0.85rem;
}
}
/* On phones the 3-column table is turned into stacked cards so there is
no horizontal scrolling one card per feature, KoalaSync above Teleparty. */
@media (max-width: 600px) {
.comparison-table-wrapper {
overflow-x: visible;
background: transparent;
border: none;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
margin-top: 0;
}
.comparison-table {
display: block;
font-size: 0.95rem;
}
/* Visually hide the header row but keep it for screen readers. */
.comparison-table thead {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.comparison-table tbody,
.comparison-table tr,
.comparison-table td {
display: block;
width: 100%;
}
.comparison-table tbody tr {
background: oklch(0.27 0.035 130 / 0.35);
border: 1px solid var(--glass-border);
border-radius: 14px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
margin-bottom: 1rem;
overflow: hidden;
}
.comparison-table tbody tr:last-child {
margin-bottom: 0;
}
.comparison-table td {
padding: 0.85rem 1.1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.comparison-table tbody tr td:last-child {
border-bottom: none;
}
/* Feature name acts as the card header. */
.comparison-table td.feat-name {
background: oklch(0.20 0.025 140 / 0.55);
padding: 0.9rem 1.1rem;
}
/* Room is back on the card, so show the description again. */
.feat-desc {
display: block;
margin-top: 0.15rem;
}
/* Label each value cell with the product it belongs to. */
.comparison-table td.check,
.comparison-table td.cross {
display: flex;
align-items: baseline;
gap: 0.4rem;
}
.comparison-table td.check::before,
.comparison-table td.cross::before {
flex: 0 0 5.5rem;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
opacity: 0.85;
}
.comparison-table td.check::before,
.comparison-table td.cross::before {
content: attr(data-label);
}
/* Keep the KoalaSync row subtly highlighted, like on desktop. */
.comparison-table td.check.highlight {
background: oklch(0.68 0.13 150 / 0.08);
}
}
@media (max-width: 600px) {
html.theme-light .comparison-section {
background: transparent;
}
html.theme-light .comparison-table tbody tr {
background: #ffffff;
border-color: oklch(0.20 0.025 140 / 0.1);
box-shadow: 0 8px 20px oklch(0.20 0.025 140 / 0.08);
}
html.theme-light .comparison-table tbody tr:hover {
background: #ffffff;
}
html.theme-light .comparison-table td {
border-bottom-color: oklch(0.20 0.025 140 / 0.08);
}
html.theme-light .comparison-table td.feat-name {
background: #ffffff;
}
html.theme-light .comparison-table td.check.highlight {
background: oklch(0.52 0.12 150 / 0.07);
}
}
/* --- Footnotes & Source Links --- */
.source-link {
color: var(--accent);
text-decoration: none;
font-size: 0.72rem;
font-weight: 700;
margin-left: 0.25rem;
display: inline-flex;
align-items: center;
vertical-align: super;
opacity: 0.82;
transition: opacity 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.source-link:hover {
opacity: 1;
color: oklch(0.75 0.10 150);
transform: translateY(-1px);
}
.table-footnotes {
padding: 0.8rem 1.25rem;
background: oklch(0.20 0.025 140 / 0.45);
border-top: 1px solid rgba(255, 255, 255, 0.05);
font-size: 0.72rem;
color: var(--text-muted);
line-height: 1.5;
opacity: 0.65;
transition: opacity 0.2s ease;
}
/* Link from the comparison table to the fuller guides/alternatives hub */
.comparison-guides-link {
text-align: center;
margin: 2rem 0 0;
}
.comparison-guides-link a {
display: inline-flex;
align-items: center;
gap: 0.45rem;
color: var(--accent);
text-decoration: none;
font-weight: 600;
font-size: 0.95rem;
transition: gap 0.2s ease, opacity 0.2s ease;
}
.comparison-guides-link a:hover {
gap: 0.7rem;
opacity: 0.85;
}
.table-footnotes:hover {
opacity: 0.95;
}
.table-footnotes p {
margin: 0;
display: flex;
align-items: baseline;
gap: 0.4rem;
}
.table-footnotes p:not(:last-child) {
margin-bottom: 0.4rem;
}
.table-footnotes a {
color: var(--accent);
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease, text-decoration 0.2s ease;
}
.table-footnotes a:hover {
color: oklch(0.75 0.10 150);
text-decoration: underline;
}
.table-footnotes sup {
font-weight: 700;
color: var(--accent);
}
@media (max-width: 768px) {
.table-footnotes {
padding: 0.75rem 1rem;
font-size: 0.68rem;
}
}
+168
View File
@@ -0,0 +1,168 @@
/* --- 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: oklch(0.68 0.13 150 / 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 oklch(0.68 0.13 150 / 0.2);
}
@media (max-width: 768px) {
.hero {
align-items: center;
padding-top: 5.5rem;
padding-bottom: 3.5rem;
min-height: 100svh;
}
.hero-grid {
min-height: auto;
}
.hero-grid {
grid-template-columns: minmax(0, 1fr);
text-align: center;
}
.step {
grid-template-columns: minmax(0, 1fr);
text-align: left;
}
.hero-text h1 {
font-size: 2.5rem;
max-width: none;
}
.cta-group {
justify-content: center;
flex-wrap: wrap;
}
.nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
background: oklch(0.20 0.025 140 / 0.95);
backdrop-filter: blur(12px);
padding: 1rem 2rem;
gap: 1rem;
border-bottom: 1px solid var(--glass-border);
margin-left: 0;
}
.nav-links.open {
display: flex;
}
.nav-right {
margin-left: auto;
}
.hamburger {
display: inline-flex !important;
}
nav .container {
padding: 0 0.5rem;
}
.logo-area {
gap: 0.25rem;
font-size: 1.1rem;
}
.logo-area img {
height: 40px;
width: 40px;
margin: 0;
}
.lang-select-container {
padding: 4px 10px;
gap: 4px;
}
.lang-select-container .globe-icon {
width: 14px;
height: 14px;
}
.lang-dropdown {
font-size: 0.7rem;
padding: 0 6px 0 0;
}
.lang-select-container .chevron-icon {
width: 10px;
height: 10px;
}
.legal-card {
padding: 1.5rem;
}
}
@media (min-width: 769px) and (max-height: 920px) {
.hero {
padding-top: clamp(7.5rem, 10vh, 9rem);
padding-bottom: clamp(1.5rem, 3vh, 2.75rem);
}
.hero-grid {
min-height: calc(100svh - 9.5rem);
}
}
/* Extra-narrow phones: make sure logo, language picker and the menu button
never crowd each other in the header (kept comfortable down to ~320px). */
@media (max-width: 380px) {
nav .container {
padding: 0 0.4rem;
}
.nav-right {
gap: 0.35rem;
}
.logo-area {
font-size: 1rem;
gap: 0.4rem;
}
}
+3 -3
View File
@@ -10,9 +10,9 @@
<link rel="dns-prefetch" href="https://addons.mozilla.org">
<link rel="dns-prefetch" href="https://github.com">
<link rel="preload" href="{{ASSET_PATH}}style.min.css" as="style">
<link rel="stylesheet" href="{{ASSET_PATH}}landing.min.css">
<link rel="stylesheet" href="{{ASSET_PATH}}landing-demo.min.css" media="(min-width: 769px)" data-landing-demo>
<link rel="preload" as="image" href="{{ASSET_PATH}}assets/LookDownKoala.avif" imagesrcset="{{ASSET_PATH}}assets/LookDownKoala-1x.avif 90w, {{ASSET_PATH}}assets/LookDownKoala.avif 205w" imagesizes="90px" type="image/avif" fetchpriority="high">
<link rel="stylesheet" href="{{ASSET_PATH}}style.min.css">
<link rel="icon" type="image/png" sizes="16x16" href="{{ASSET_PATH}}assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ASSET_PATH}}assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="{{ASSET_PATH}}assets/icon-192x192.png">
@@ -209,7 +209,7 @@
</style>
</noscript>
</head>
<body>
<body data-deferred-css="{{ASSET_PATH}}landing-deferred.min.css">
<div class="scroll-progress-bar"></div>
<a class="skip-link" href="#main-content">Skip to main content</a>