diff --git a/website/README.md b/website/README.md index b982136..af7a637 100644 --- a/website/README.md +++ b/website/README.md @@ -33,7 +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. +- **Strangler CSS architecture**: `styles/*.css` supplies page-specific production bundles; `style.legacy.css` is the byte-identical, unloaded reference monolith. The landing bundle preserves the legacy cascade while excluding Join- and Alternatives-only rules. Structural CSS is not activated after paint because that causes layout shifts. - **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/`. diff --git a/website/app.js b/website/app.js index 6de2394..f11d24a 100644 --- a/website/app.js +++ b/website/app.js @@ -1,27 +1,6 @@ // 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', @@ -1332,8 +1311,6 @@ 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; diff --git a/website/build.cjs b/website/build.cjs index cc09301..d881d91 100644 --- a/website/build.cjs +++ b/website/build.cjs @@ -145,23 +145,24 @@ async function compile() { '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' + // Keep the landing cascade equivalent to styleModules while excluding + // utility-page-only rules. A single blocking file avoids both request + // fan-out and layout shifts from activating structural CSS after paint. + const landingBundles = [ + { + key: 'landing', + modules: [ + 'styles/foundation.css', + 'styles/hero.css', + 'styles/landing-primary.css', + 'styles/legal.css', + 'styles/landing-controls.css', + 'styles/landing-sections.css', + 'styles/landing-selfhost.css', + 'styles/landing-faq.css', + 'styles/demo.css' + ] + } ]; const styleRaw = styleModules .map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8')) @@ -170,24 +171,18 @@ async function compile() { 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 compiledLandingBundles = landingBundles.map(bundle => { + const raw = bundle.modules + .map(relativePath => fs.readFileSync(path.join(websiteDir, relativePath), 'utf8')) + .join(''); + const min = minifyCSS(raw); + return { + ...bundle, + min, + name: `${bundle.key === 'landing' ? 'landing' : `landing-${bundle.key}`}.${sha8(min)}.min.css`, + sri: sha384(min) + }; + }); const appRaw = fs.readFileSync(path.join(websiteDir, 'app.js'), 'utf8'); const appMin = await minifyJS(appRaw); @@ -205,9 +200,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)`); + for (const bundle of compiledLandingBundles) { + console.log(` Landing ${bundle.key}: ${bundle.name} (${(bundle.min.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}%)`); @@ -221,9 +216,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); + for (const bundle of compiledLandingBundles) { + fs.writeFileSync(path.join(wwwDir, bundle.name), bundle.min); + } fs.writeFileSync(path.join(wwwDir, appName), appMin); fs.writeFileSync(path.join(wwwDir, langName), langMin); @@ -520,15 +515,12 @@ async function compile() { html = html.replace(/(]*?\bhref=")((?:\.\.\/)*\/?)style\.min\.css"/g, (m, before, prefix) => { return `${before}${prefix}${styleName}" integrity="${styleSRI}" crossorigin="anonymous"`; }); - html = html.replace(/(]*?\bhref=")((?:\.\.\/)*\/?)landing\.min\.css"/g, (m, before, prefix) => { - return `${before}${prefix}${landingStyleName}" integrity="${landingStyleSRI}" crossorigin="anonymous"`; - }); - html = html.replace(/(]*?\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}"`; - }); + for (const bundle of compiledLandingBundles) { + const placeholder = bundle.key === 'landing' ? 'landing.min.css' : `landing-${bundle.key}.min.css`; + html = html.replace(new RegExp(`(]*?\\bhref=")((?:\\.\\.\\/)*\\/?)${placeholder.replaceAll('.', '\\.') }"`, 'g'), (m, before, prefix) => { + return `${before}${prefix}${bundle.name}" integrity="${bundle.sri}" crossorigin="anonymous"`; + }); + } html = html.replace(/(]*?\bsrc=")((?:\.\.\/)*\/?)app\.min\.js"/g, (m, before, prefix) => { return `${before}${prefix}${appName}" integrity="${appSRI}" crossorigin="anonymous"`; }); diff --git a/website/styles/landing-demo-gate.css b/website/styles/landing-demo-gate.css deleted file mode 100644 index 2e569d7..0000000 --- a/website/styles/landing-demo-gate.css +++ /dev/null @@ -1,14 +0,0 @@ -/* 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; - } -} diff --git a/website/styles/landing-selfhost.css b/website/styles/landing-selfhost.css new file mode 100644 index 0000000..a24f780 --- /dev/null +++ b/website/styles/landing-selfhost.css @@ -0,0 +1,12 @@ +.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); +} diff --git a/website/template.html b/website/template.html index 9a3460f..648c15f 100644 --- a/website/template.html +++ b/website/template.html @@ -11,7 +11,6 @@ - @@ -209,7 +208,7 @@ - +