mirror of
https://gitlab.com/vyla-entertainment/home-frontend.git
synced 2026-11-09 20:57:25 -05:00
website rewrite
This commit is contained in:
@@ -86,34 +86,8 @@
|
|||||||
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/base/tokens.css">
|
<link rel="stylesheet" href="/src/css/main.css">
|
||||||
<link rel="stylesheet" href="/src/styles/base/reset.css">
|
<link rel="stylesheet" href="/src/css/other.css">
|
||||||
<link rel="stylesheet" href="/src/styles/base/typography.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/layout.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/reveal.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/layout/nav.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/hero-atmosphere.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/hero-content.css">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/layout/footer.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/buttons.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/pills.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/stats.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/script-card.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/marquee.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/testimonial-card.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/modal.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/partners.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/faq.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/section-base.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/testimonials.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/features.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/faq-section.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/cta-banner.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/responsive.css" media="print" onload="this.media='all'">
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/pages/other.css" />
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
@@ -241,13 +215,6 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header role="banner">
|
|
||||||
<nav class="nav" role="navigation" aria-label="Main navigation">
|
|
||||||
<div class="logo-image">
|
|
||||||
<img src="/assets/images/icons/logo_white.png" alt="Vyla Home">
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
@@ -303,14 +270,18 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="cta-footer-wrap">
|
<div class="cta-footer-wrap">
|
||||||
<div id="cta-rays" class="cta-rays-container" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<section class="cta-banner" aria-labelledby="cta-heading">
|
<section class="cta-banner" aria-labelledby="cta-heading">
|
||||||
<div class="container cta-banner-content reveal">
|
<div class="cta-atmosphere" aria-hidden="true">
|
||||||
|
<video class="cta-atmosphere-video" autoplay muted loop playsinline preload="auto">
|
||||||
|
<source src="https://www.pexels.com/download/video/38243003/" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div class="container cta-banner-content">
|
||||||
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
|
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
|
||||||
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
||||||
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
|
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
|
||||||
<button class="hero-cta hero-cta-light" id="cta-download-btn">
|
<button class="hero-cta hero-cta-light" id="cta-download-btn" type="button" aria-haspopup="true"
|
||||||
|
aria-controls="cta-download-options">
|
||||||
<span class="download-btn-text">Download Now</span>
|
<span class="download-btn-text">Download Now</span>
|
||||||
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
@@ -318,123 +289,44 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="download-options" id="cta-download-options" aria-hidden="true">
|
<div class="download-options" id="cta-download-options" aria-hidden="true">
|
||||||
<a href="#" class="download-option" id="cta-windows-download">
|
<a href="#" class="download-option" id="cta-windows-download" aria-label="Download for Windows">
|
||||||
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Windows v0.0.0</span>
|
<span>Windows</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="cta-mac-download">
|
<a href="#" class="download-option" id="cta-mac-download" aria-label="Download for macOS">
|
||||||
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Mac v0.0.0</span>
|
<span>macOS</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="cta-go-download">
|
<a href="#" class="download-option" id="cta-go-download" aria-label="Download for Android">
|
||||||
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Android v0.0.0</span>
|
<span>Android</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
async function setupExpandableDownload(expandableId, btnId, optionsId, windowsId, goId, macId) {
|
|
||||||
const expandable = document.getElementById(expandableId);
|
|
||||||
const downloadBtn = document.getElementById(btnId);
|
|
||||||
const downloadOptions = document.getElementById(optionsId);
|
|
||||||
const windowsDownload = document.getElementById(windowsId);
|
|
||||||
const goDownload = document.getElementById(goId);
|
|
||||||
const macDownload = document.getElementById(macId);
|
|
||||||
if (!expandable || !downloadBtn || !downloadOptions || !windowsDownload || !goDownload || !macDownload) return;
|
|
||||||
try {
|
|
||||||
const [homeResponse, goResponse] = await Promise.all([
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"),
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases/permalink/latest")
|
|
||||||
]);
|
|
||||||
if (!homeResponse.ok) throw new Error("Failed to fetch latest home release");
|
|
||||||
if (!goResponse.ok) throw new Error("Failed to fetch latest go release");
|
|
||||||
const homeRelease = await homeResponse.json();
|
|
||||||
const goRelease = await goResponse.json();
|
|
||||||
const version = homeRelease.tag_name;
|
|
||||||
const goVersion = goRelease.tag_name;
|
|
||||||
const fileVersion = version.replace(/^v/, "");
|
|
||||||
const goFileVersion = goVersion.replace(/^v/, "");
|
|
||||||
const windowsAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".exe"));
|
|
||||||
const macAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".dmg"));
|
|
||||||
const goAsset = goRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".apk"));
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = `Download ${version}`;
|
|
||||||
}
|
|
||||||
if (windowsAsset?.url) {
|
|
||||||
windowsDownload.href = windowsAsset.url;
|
|
||||||
} else {
|
|
||||||
windowsDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = `Windows ${version}`;
|
|
||||||
if (goAsset?.url) {
|
|
||||||
goDownload.href = goAsset.url;
|
|
||||||
} else {
|
|
||||||
goDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/packages/generic/vyla-go/${goVersion}/vyla-go-${goFileVersion}.apk`;
|
|
||||||
}
|
|
||||||
goDownload.querySelector("span").textContent = `Android ${goVersion}`;
|
|
||||||
if (macAsset?.url) {
|
|
||||||
macDownload.href = macAsset.url;
|
|
||||||
} else {
|
|
||||||
macDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_aarch64.dmg`;
|
|
||||||
}
|
|
||||||
macDownload.querySelector("span").textContent = `Mac ${version}`;
|
|
||||||
downloadBtn.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const isExpanded = expandable.getAttribute("aria-expanded") === "true";
|
|
||||||
expandable.setAttribute("aria-expanded", String(!isExpanded));
|
|
||||||
downloadOptions.setAttribute("aria-hidden", String(isExpanded));
|
|
||||||
downloadOptions.classList.toggle("show", !isExpanded);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = "Download Latest Release";
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = "Windows (Latest)";
|
|
||||||
goDownload.querySelector("span").textContent = "Android (Latest)";
|
|
||||||
macDownload.querySelector("span").textContent = "Mac (Latest)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setupExpandableDownload("cta-download-expandable", "cta-download-btn", "cta-download-options", "cta-windows-download", "cta-go-download", "cta-mac-download");
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
const expandables = document.querySelectorAll(".download-expandable");
|
|
||||||
expandables.forEach(expandable => {
|
|
||||||
const btn = expandable.querySelector("button");
|
|
||||||
const options = expandable.querySelector(".download-options");
|
|
||||||
if (btn && options && !expandable.contains(e.target)) {
|
|
||||||
expandable.setAttribute("aria-expanded", "false");
|
|
||||||
options.setAttribute("aria-hidden", "true");
|
|
||||||
options.classList.remove("show");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer" role="contentinfo">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Vyla</h2>
|
<h2>Vyla</h2>
|
||||||
<p>Vyla is a passion project born from a love for film and television.</p>
|
<p>Vyla is a passion project born from a love for film and television.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Backup Site:</h2>
|
<h2>Backup Site</h2>
|
||||||
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
|
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener noreferrer">https://vyla.qzz.io</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
@@ -442,33 +334,32 @@
|
|||||||
<a href="/terms-of-service">Terms of Service</a>
|
<a href="/terms-of-service">Terms of Service</a>
|
||||||
<a href="/privacy-policy">Privacy Policy</a>
|
<a href="/privacy-policy">Privacy Policy</a>
|
||||||
<a href="/about-us">About Us</a>
|
<a href="/about-us">About Us</a>
|
||||||
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener">DMCA</a>
|
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener noreferrer">DMCA</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Our API</h2>
|
<h2>Developers</h2>
|
||||||
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
|
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Join our Discord</h2>
|
<h2>Community</h2>
|
||||||
<div class="social-links">
|
<div class="social-links">
|
||||||
<a href="/discord" target="_blank" rel="noopener">
|
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
|
||||||
<i class="fab fa-discord"></i> Discord
|
aria-label="Official Discord Server">Discord</a>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
|
<div class="footer-section footer-badge">
|
||||||
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
|
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
|
||||||
height="95" loading="lazy" decoding="async">
|
alt="This website runs on green hosting verified by thegreenwebfoundation.org" width="200"
|
||||||
|
height="95" loading="lazy" decoding="async">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
|
<p>© 2025 Vyla Entertainment. All rights reserved.</p>
|
||||||
© 2025 Vyla Entertainment. All rights reserved.
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/scripts/main.js"></script>
|
<script type="module" src="/src/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
BIN
assets/images/testimonials/asherread.png
Normal file
BIN
assets/images/testimonials/asherread.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
assets/images/testimonials/ghomistty.png
Normal file
BIN
assets/images/testimonials/ghomistty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
BIN
assets/images/testimonials/saiyance0.png
Normal file
BIN
assets/images/testimonials/saiyance0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 562 KiB |
728
index.html
728
index.html
@@ -1,38 +1,47 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" dir="ltr">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="referrer" content="no-referrer-when-downgrade">
|
<meta name="referrer" content="strict-origin-when-cross-origin">
|
||||||
|
|
||||||
<title>Vyla Home - Self-Hosted Desktop Media Center | Serverless Streaming & Entertainment Platform</title>
|
<title>Vyla Home - Self-Hosted Desktop Media Center | Serverless Streaming & Entertainment Platform</title>
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="Vyla Home is a revolutionary standalone desktop media center that bypasses datacenter blocking, ISP throttling, and cloud downtime. Experience self-hosted streaming, 4K movies, TV shows, and anime with complete privacy and zero server dependencies.">
|
content="Vyla Home is an ultra-fast, self-hosted standalone desktop media center that bypasses datacenter blocks, ISP throttling, and cloud downtime. Stream 4K movies, TV, and anime privately.">
|
||||||
<meta name="keywords"
|
<meta name="keywords"
|
||||||
content="vyla home, desktop media center, self-hosted streaming, serverless entertainment, media player, movies, tv shows, anime, 4K streaming, bypass ISP throttling, datacenter blocking, local media vault, encrypted storage, desktop application, offline streaming, personal entertainment">
|
content="vyla home, desktop media center, self-hosted streaming, serverless entertainment, media player, 4K streaming, bypass ISP throttling, local media vault, encrypted storage, desktop application">
|
||||||
<meta name="author" content="Vyla Entertainment">
|
<meta name="author" content="Vyla Entertainment">
|
||||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
||||||
<meta name="googlebot" content="index, follow">
|
<meta name="googlebot" content="index, follow">
|
||||||
<meta name="language" content="English">
|
<meta name="theme-color" content="#000000">
|
||||||
<meta name="geo.region" content="US">
|
<meta name="color-scheme" content="dark">
|
||||||
<meta name="geo.placename" content="Global">
|
|
||||||
<meta name="coverage" content="Worldwide">
|
|
||||||
<meta name="distribution" content="Global">
|
|
||||||
<meta name="rating" content="General">
|
|
||||||
<meta name="revisit-after" content="7 days">
|
|
||||||
<link rel="canonical" href="https://vyla.cc/">
|
<link rel="canonical" href="https://vyla.cc/">
|
||||||
|
|
||||||
<meta name="application-name" content="Vyla Home">
|
<meta property="og:title" content="Vyla Home - Self-Hosted Desktop Media Center | Serverless Streaming">
|
||||||
<meta name="msapplication-tooltip" content="Self-hosted desktop media center for streaming entertainment">
|
<meta property="og:description"
|
||||||
<meta name="msapplication-starturl" content="/">
|
content="Experience high-performance self-hosted streaming with Vyla Home. Direct connection, zero tracking, hardware-accelerated 4K playback.">
|
||||||
<meta name="msapplication-window" content="width=device-width;height=device-height">
|
<meta property="og:image" content="https://vyla.cc/assets/images/thumbnail.png">
|
||||||
<meta name="msapplication-navbutton-color" content="#FFF">
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
<meta property="og:image:alt" content="Vyla Home Desktop Media Center Interface">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:site_name" content="Vyla Home">
|
||||||
|
<meta property="og:url" content="https://vyla.cc/">
|
||||||
|
<meta property="og:locale" content="en_US">
|
||||||
|
|
||||||
<link rel="icon" href="assets/images/favicon.ico" type="image/x-icon">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon.ico">
|
<meta name="twitter:site" content="@VylaEntertainment">
|
||||||
|
<meta name="twitter:creator" content="@VylaEntertainment">
|
||||||
|
<meta name="twitter:title" content="Vyla Home - Self-Hosted Desktop Media Center">
|
||||||
|
<meta name="twitter:description"
|
||||||
|
content="Experience high-performance self-hosted streaming with Vyla Home. Direct connection, zero tracking, hardware-accelerated 4K playback.">
|
||||||
|
<meta name="twitter:image" content="https://vyla.cc/assets/images/thumbnail.png">
|
||||||
|
<meta name="twitter:image:alt" content="Vyla Home Desktop Media Center Interface">
|
||||||
|
|
||||||
|
<link rel="icon" href="assets/images/favicon.ico" sizes="any">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-16x16.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/images/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="assets/images/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
@@ -41,200 +50,60 @@
|
|||||||
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
|
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
|
||||||
<link rel="dns-prefetch" href="https://gitlab.com">
|
<link rel="dns-prefetch" href="https://gitlab.com">
|
||||||
<link rel="dns-prefetch" href="https://discord.com">
|
<link rel="dns-prefetch" href="https://discord.com">
|
||||||
<link rel="dns-prefetch" href="https://videos.pexels.com">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link rel="preconnect" href="https://discord.com" crossorigin>
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval';">
|
|
||||||
|
|
||||||
<meta property="og:title" content="Vyla Home - Self-Hosted Desktop Media Center | Serverless Streaming">
|
|
||||||
<meta property="og:description"
|
|
||||||
content="Experience revolutionary self-hosted streaming with Vyla Home. Bypass datacenter blocking, ISP throttling, and cloud downtime. 4K movies, TV shows, anime with complete privacy and zero server dependencies.">
|
|
||||||
<meta property="og:image" content="/assets/images/thumbnail.png">
|
|
||||||
<meta property="og:image:width" content="1200">
|
|
||||||
<meta property="og:image:height" content="630">
|
|
||||||
<meta property="og:image:alt" content="Vyla Home Desktop Media Center Interface">
|
|
||||||
<meta property="og:type" content="website">
|
|
||||||
<meta property="og:site_name" content="Vyla Home">
|
|
||||||
<meta property="og:url" content="https://vyla.cc/">
|
|
||||||
<meta property="og:locale" content="en_US">
|
|
||||||
<meta property="og:locale:alternate" content="en_GB">
|
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
|
||||||
<meta name="twitter:site" content="@VylaEntertainment">
|
|
||||||
<meta name="twitter:creator" content="@VylaEntertainment">
|
|
||||||
<meta name="twitter:title" content="Vyla Home - Self-Hosted Desktop Media Center">
|
|
||||||
<meta name="twitter:description"
|
|
||||||
content="Experience revolutionary self-hosted streaming. Bypass datacenter blocking, ISP throttling, and cloud downtime. 4K movies, TV shows, anime with complete privacy.">
|
|
||||||
<meta name="twitter:image" content="/assets/images/thumbnail.png">
|
|
||||||
<meta name="twitter:image:alt" content="Vyla Home Desktop Media Center Interface">
|
|
||||||
|
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
|
||||||
|
|
||||||
<link rel="manifest" href="manifest.json">
|
|
||||||
|
|
||||||
<meta name="theme-color" content="#FFF">
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
||||||
<meta name="apple-mobile-web-app-title" content="Vyla Home">
|
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="application-name" content="Vyla Home">
|
|
||||||
<meta name="msapplication-TileColor" content="#FFF">
|
|
||||||
<meta name="msapplication-config" content="/browserconfig.xml">
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" href="favicon.ico">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap">
|
<link rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap">
|
||||||
<!-- Critical CSS - loaded immediately -->
|
<link rel="stylesheet" href="src/css/main.css">
|
||||||
<link rel="stylesheet" href="src/styles/base/tokens.css">
|
|
||||||
<link rel="stylesheet" href="src/styles/base/reset.css">
|
|
||||||
<link rel="stylesheet" href="src/styles/base/typography.css">
|
|
||||||
<link rel="stylesheet" href="src/styles/utilities/layout.css">
|
|
||||||
<link rel="stylesheet" href="src/styles/utilities/reveal.css">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/glass-surface.css">
|
|
||||||
<link rel="stylesheet" href="src/styles/layout/nav.css">
|
|
||||||
<link rel="stylesheet" href="src/styles/sections/hero-atmosphere.css">
|
|
||||||
<link rel="stylesheet" href="src/styles/sections/hero-content.css">
|
|
||||||
|
|
||||||
<!-- Non-critical CSS - deferred loading -->
|
|
||||||
<link rel="stylesheet" href="src/styles/layout/footer.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/buttons.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/pills.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/stats.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/script-card.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/marquee.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/testimonial-card.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/modal.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/partners.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/components/faq.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/sections/section-base.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/sections/testimonials.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/sections/features.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/sections/faq-section.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/sections/cta-banner.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="src/styles/utilities/responsive.css" media="print" onload="this.media='all'">
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "SoftwareApplication",
|
"@graph": [
|
||||||
"name": "Vyla Home",
|
|
||||||
"applicationCategory": "MultimediaApplication",
|
|
||||||
"operatingSystem": "Windows, macOS",
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"description": "Vyla Home is a revolutionary standalone desktop media center that bypasses datacenter blocking, ISP throttling, and cloud downtime. Experience self-hosted streaming, 4K movies, TV shows, and anime with complete privacy and zero server dependencies.",
|
|
||||||
"url": "https://vyla.cc/",
|
|
||||||
"author": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "Vyla Entertainment",
|
|
||||||
"url": "https://gitlab.com/vyla-entertainment/home"
|
|
||||||
},
|
|
||||||
"publisher": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "Vyla Entertainment",
|
|
||||||
"url": "https://gitlab.com/vyla-entertainment/home"
|
|
||||||
},
|
|
||||||
"aggregateRating": {
|
|
||||||
"@type": "AggregateRating",
|
|
||||||
"ratingValue": "4.8",
|
|
||||||
"ratingCount": "1250",
|
|
||||||
"bestRating": "5",
|
|
||||||
"worstRating": "1"
|
|
||||||
},
|
|
||||||
"featureList": [
|
|
||||||
"Self-hosted streaming platform",
|
|
||||||
"Bypass datacenter blocking",
|
|
||||||
"Encrypted local vault",
|
|
||||||
"4K movie streaming",
|
|
||||||
"TV show library",
|
|
||||||
"Anime collection",
|
|
||||||
"Automated OTA updates",
|
|
||||||
"Customizable interface",
|
|
||||||
"Offline playback capability"
|
|
||||||
],
|
|
||||||
"screenshot": "https://vyla.cc/assets/images/thumbnail.png",
|
|
||||||
"softwareVersion": "1.0.0",
|
|
||||||
"datePublished": "2025-01-01",
|
|
||||||
"downloadUrl": "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/v1.0.0/Vyla-Home_1.0.0_x64-setup.exe",
|
|
||||||
"license": "https://gitlab.com/vyla-entertainment/home/-/blob/main/LICENSE"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "WebSite",
|
|
||||||
"name": "Vyla Home",
|
|
||||||
"url": "https://vyla.cc/",
|
|
||||||
"description": "Self-hosted desktop media center for streaming entertainment",
|
|
||||||
"potentialAction": {
|
|
||||||
"@type": "SearchAction",
|
|
||||||
"target": "https://vyla.cc/?q={search_term_string}",
|
|
||||||
"query-input": "required name=search_term_string"
|
|
||||||
},
|
|
||||||
"publisher": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "Vyla Entertainment",
|
|
||||||
"url": "https://gitlab.com/vyla-entertainment/home",
|
|
||||||
"logo": "https://vyla.cc/assets/images/header_logo.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "Vyla Entertainment",
|
|
||||||
"url": "https://vyla.cc/",
|
|
||||||
"logo": "https://vyla.cc/assets/images/header_logo.png",
|
|
||||||
"description": "Developer of self-hosted entertainment software",
|
|
||||||
"sameAs": [
|
|
||||||
"https://gitlab.com/vyla-entertainment/home",
|
|
||||||
"https://discord.gg/5eqRhKeywG"
|
|
||||||
],
|
|
||||||
"contactPoint": {
|
|
||||||
"@type": "ContactPoint",
|
|
||||||
"contactType": "customer support",
|
|
||||||
"url": "https://discord.gg/5eqRhKeywG"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "FAQPage",
|
|
||||||
"mainEntity": [
|
|
||||||
{
|
{
|
||||||
"@type": "Question",
|
"@type": "WebSite",
|
||||||
"name": "What is Vyla Home?",
|
"@id": "https://vyla.cc/#website",
|
||||||
"acceptedAnswer": {
|
"url": "https://vyla.cc/",
|
||||||
"@type": "Answer",
|
"name": "Vyla Home",
|
||||||
"text": "Vyla Home is a standalone desktop media center designed to bypass datacenter blocking, ISP throttling, and cloud downtime. It runs locally on your computer, providing self-hosted streaming without server dependencies."
|
"description": "Self-Hosted Desktop Media Center",
|
||||||
|
"publisher": {
|
||||||
|
"@id": "https://vyla.cc/#organization"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "Question",
|
"@type": "Organization",
|
||||||
"name": "How does Vyla Home bypass datacenter blocking?",
|
"@id": "https://vyla.cc/#organization",
|
||||||
"acceptedAnswer": {
|
"name": "Vyla Entertainment",
|
||||||
"@type": "Answer",
|
"url": "https://vyla.cc/",
|
||||||
"text": "Vyla Home runs entirely on your local workstation using your residential network connection, eliminating datacenter IP addresses that are commonly blocked by streaming services."
|
"logo": "https://vyla.cc/assets/images/header_logo.png",
|
||||||
}
|
"sameAs": [
|
||||||
|
"https://gitlab.com/vyla-entertainment/home",
|
||||||
|
"https://discord.gg/5eqRhKeywG"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "Question",
|
"@type": "SoftwareApplication",
|
||||||
"name": "Is Vyla Home free to use?",
|
"@id": "https://vyla.cc/#application",
|
||||||
"acceptedAnswer": {
|
"name": "Vyla Home",
|
||||||
"@type": "Answer",
|
"applicationCategory": "MultimediaApplication",
|
||||||
"text": "Yes, Vyla Home is completely free and open-source. You can download it from our GitLab repository without any cost."
|
"operatingSystem": "Windows, macOS, Android",
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "USD"
|
||||||
|
},
|
||||||
|
"description": "Vyla Home is a standalone desktop media center that bypasses datacenter blocking, ISP throttling, and cloud downtime with complete privacy.",
|
||||||
|
"url": "https://vyla.cc/",
|
||||||
|
"author": {
|
||||||
|
"@id": "https://vyla.cc/#organization"
|
||||||
|
},
|
||||||
|
"aggregateRating": {
|
||||||
|
"@type": "AggregateRating",
|
||||||
|
"ratingValue": "4.9",
|
||||||
|
"ratingCount": "1250",
|
||||||
|
"bestRating": "5",
|
||||||
|
"worstRating": "1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -243,13 +112,23 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div class="body-ambient-bg" aria-hidden="true">
|
||||||
|
<video class="body-ambient-video" id="body-ambient-video" autoplay muted loop playsinline preload="auto">
|
||||||
|
<source src="https://www.pexels.com/download/video/38243003/" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
<header role="banner">
|
<header role="banner">
|
||||||
<nav class="nav" role="navigation" aria-label="Main navigation">
|
<nav class="nav" role="navigation" aria-label="Main navigation">
|
||||||
<div class="nav-container">
|
<div class="nav-container">
|
||||||
<div class="logo-image">
|
|
||||||
<img src="/assets/images/icons/logo_white.png" alt="Vyla Home">
|
|
||||||
</div>
|
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
|
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="nav-discord-link" aria-label="Join our Discord community">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.929 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.894.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.028zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
<a href="#features">Features</a>
|
<a href="#features">Features</a>
|
||||||
<a href="#testimonials">Testimonials</a>
|
<a href="#testimonials">Testimonials</a>
|
||||||
<a href="#faq">FAQ</a>
|
<a href="#faq">FAQ</a>
|
||||||
@@ -258,357 +137,196 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<svg aria-hidden="true" class="dot-grid" width="100%" height="100%">
|
|
||||||
<defs>
|
|
||||||
<pattern id="dot-pattern" width="20" height="20" patternUnits="userSpaceOnUse"
|
|
||||||
patternContentUnits="userSpaceOnUse" x="0" y="0">
|
|
||||||
<circle cx="1" cy="1" r="1"></circle>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect width="100%" height="100%" stroke-width="0" fill="url(#dot-pattern)"></rect>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<main role="main">
|
<main role="main">
|
||||||
<section class="hero" aria-labelledby="hero-title">
|
<section class="hero" aria-labelledby="hero-title">
|
||||||
<div class="hero-atmosphere" aria-hidden="true">
|
<div class="hero-atmosphere" aria-hidden="true">
|
||||||
<video class="hero-atmosphere-video" autoplay muted loop playsinline preload="auto">
|
<video class="hero-atmosphere-video" autoplay muted loop playsinline preload="auto">
|
||||||
<source src="https://www.pexels.com/download/video/39333459/" type="video/mp4">
|
<source src="https://www.pexels.com/download/video/38243003/" type="video/mp4">
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
<div class="container hero-container">
|
<div class="container hero-container">
|
||||||
<div class="logo-image">
|
<div class="logo-image">
|
||||||
<img src="assets/images/header_logo.png" alt="Vyla Home Logo" width="100" height="100"
|
<img src="assets/images/header_logo.png" alt="Vyla Home Official Logo" width="100" height="100"
|
||||||
fetchpriority="high" loading="eager" decoding="async"
|
fetchpriority="high" loading="eager" decoding="async">
|
||||||
srcset="assets/images/header_logo.png 100w, assets/images/header_logo.png 200w" sizes="100px">
|
|
||||||
</div>
|
</div>
|
||||||
<h1 id="hero-title" class="hero-title">The Local-First Desktop Media Center</h1>
|
<h1 id="hero-title" class="hero-title">Welcome to Vyla Entertainment</h1>
|
||||||
<p class="hero-description"> Vyla Home runs directly on your local workstation. By decentralizing stream
|
<p class="hero-description">Vyla runs directly on your local workstation. By decentralizing stream
|
||||||
resolution to your residential connection, it eliminates centralized cloud failures, datacenter IP
|
resolution to your residential connection, it eliminates centralized cloud failures, datacenter IP
|
||||||
blocks, and third-party tracking. </p>
|
blocks, and third-party tracking.</p>
|
||||||
<div class="discord-copy-bar reveal" id="copy-discord-btn" role="button" tabindex="0"
|
|
||||||
aria-label="Copy GitLab releases URL">
|
<div class="hero-cta-row">
|
||||||
<span class="copy-discord-label"
|
|
||||||
id="copy-discord-label">https://gitlab.com/vyla-entertainment/home/-/releases</span>
|
|
||||||
<span class="discord-copy-actions">
|
|
||||||
<span class="discord-copy-icon" id="copy-discord-icon" aria-hidden="true">
|
|
||||||
<svg class="copy-icon" width="20px" height="20px" viewBox="0 -0.5 25 25" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" stroke="#000000" aria-hidden="true">
|
|
||||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
|
||||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
|
||||||
<g id="SVGRepo_iconCarrier">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
||||||
d="M8.94605 4.99995L13.2541 4.99995C14.173 5.00498 15.0524 5.37487 15.6986 6.02825C16.3449 6.68163 16.7051 7.56497 16.7001 8.48395V12.716C16.7051 13.6349 16.3449 14.5183 15.6986 15.1717C15.0524 15.825 14.173 16.1949 13.2541 16.2H8.94605C8.02707 16.1949 7.14773 15.825 6.50148 15.1717C5.85522 14.5183 5.495 13.6349 5.50005 12.716L5.50005 8.48495C5.49473 7.5658 5.85484 6.6822 6.50112 6.0286C7.1474 5.375 8.0269 5.00498 8.94605 4.99995Z"
|
|
||||||
stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"></path>
|
|
||||||
<path d="M10.1671 19H14.9371C17.4857 18.9709 19.5284 16.8816 19.5001 14.333V9.666"
|
|
||||||
stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<svg class="check-icon" width="20px" height="20px" viewBox="0 -0.5 25 25" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
||||||
<g>
|
|
||||||
<path d="M5.5 12.5L10 17L19.5 7.5" stroke="#ffffff" stroke-width="2.5"
|
|
||||||
stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="hero-cta-row reveal">
|
|
||||||
<div class="download-expandable" id="hero-download-expandable" aria-expanded="false">
|
<div class="download-expandable" id="hero-download-expandable" aria-expanded="false">
|
||||||
<button class="hero-cta hero-cta-dark" id="hero-download-btn">
|
<button class="hero-cta hero-cta-dark" id="hero-download-btn" type="button" aria-haspopup="true"
|
||||||
<span class="download-btn-text">Download v0.0.0</span>
|
aria-controls="hero-download-options">
|
||||||
|
<span class="download-btn-text">Download Latest</span>
|
||||||
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path d="M7 10l5 5 5-5z" />
|
<path d="M7 10l5 5 5-5z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="download-options" id="hero-download-options" aria-hidden="true">
|
<div class="download-options" id="hero-download-options" aria-hidden="true">
|
||||||
<a href="#" class="download-option" id="hero-windows-download">
|
<a href="#" class="download-option" id="hero-windows-download"
|
||||||
|
aria-label="Download for Windows">
|
||||||
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Windows v0.0.0</span>
|
<span>Windows</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="hero-go-download">
|
<a href="#" class="download-option" id="hero-go-download" aria-label="Download for Android">
|
||||||
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Android v0.0.0</span>
|
<span>Android</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="hero-mac-download">
|
<a href="#" class="download-option" id="hero-mac-download" aria-label="Download for macOS">
|
||||||
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Mac v0.0.0</span>
|
<span>macOS</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="hero-cta hero-cta-unsure" href="https://gitlab.com/vyla-entertainment/home/-/releases"
|
<a class="hero-cta hero-cta-unsure" href="https://gitlab.com/vyla-entertainment/home/-/releases"
|
||||||
target="_blank" rel="noopener noreferrer"> View Releases </a>
|
target="_blank" rel="noopener noreferrer">View Releases</a>
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
async function setupExpandableDownload(expandableId, btnId, optionsId, windowsId, goId, macId) {
|
|
||||||
const expandable = document.getElementById(expandableId);
|
|
||||||
const downloadBtn = document.getElementById(btnId);
|
|
||||||
const downloadOptions = document.getElementById(optionsId);
|
|
||||||
const windowsDownload = document.getElementById(windowsId);
|
|
||||||
const goDownload = document.getElementById(goId);
|
|
||||||
const macDownload = document.getElementById(macId);
|
|
||||||
if (!expandable || !downloadBtn || !downloadOptions || !windowsDownload || !goDownload || !macDownload) return;
|
|
||||||
try {
|
|
||||||
const [homeResponse, goResponse] = await Promise.all([
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"),
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases/permalink/latest")
|
|
||||||
]);
|
|
||||||
if (!homeResponse.ok) throw new Error("Failed to fetch latest home release");
|
|
||||||
if (!goResponse.ok) throw new Error("Failed to fetch latest go release");
|
|
||||||
const homeRelease = await homeResponse.json();
|
|
||||||
const goRelease = await goResponse.json();
|
|
||||||
const version = homeRelease.tag_name;
|
|
||||||
const goVersion = goRelease.tag_name;
|
|
||||||
const fileVersion = version.replace(/^v/, "");
|
|
||||||
const goFileVersion = goVersion.replace(/^v/, "");
|
|
||||||
const windowsAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".exe"));
|
|
||||||
const macAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".dmg"));
|
|
||||||
const goAsset = goRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".apk"));
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = `Download ${version}`;
|
|
||||||
}
|
|
||||||
if (windowsAsset?.url) {
|
|
||||||
windowsDownload.href = windowsAsset.url;
|
|
||||||
} else {
|
|
||||||
windowsDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = `Windows ${version}`;
|
|
||||||
if (goAsset?.url) {
|
|
||||||
goDownload.href = goAsset.url;
|
|
||||||
} else {
|
|
||||||
goDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/packages/generic/vyla-go/${goVersion}/vyla-go-${goFileVersion}.apk`;
|
|
||||||
}
|
|
||||||
goDownload.querySelector("span").textContent = `Android ${goVersion}`;
|
|
||||||
if (macAsset?.url) {
|
|
||||||
macDownload.href = macAsset.url;
|
|
||||||
} else {
|
|
||||||
macDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_aarch64.dmg`;
|
|
||||||
}
|
|
||||||
macDownload.querySelector("span").textContent = `Mac ${version}`;
|
|
||||||
downloadBtn.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const isExpanded = expandable.getAttribute("aria-expanded") === "true";
|
|
||||||
expandable.setAttribute("aria-expanded", String(!isExpanded));
|
|
||||||
downloadOptions.setAttribute("aria-hidden", String(isExpanded));
|
|
||||||
downloadOptions.classList.toggle("show", !isExpanded);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = "Download Latest Release";
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = "Windows (Latest)";
|
|
||||||
goDownload.querySelector("span").textContent = "Android (Latest)";
|
|
||||||
macDownload.querySelector("span").textContent = "Mac (Latest)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setupExpandableDownload("hero-download-expandable", "hero-download-btn", "hero-download-options", "hero-windows-download", "hero-go-download", "hero-mac-download");
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
const expandables = document.querySelectorAll(".download-expandable");
|
|
||||||
expandables.forEach(expandable => {
|
|
||||||
const btn = expandable.querySelector("button");
|
|
||||||
const options = expandable.querySelector(".download-options");
|
|
||||||
if (btn && options && !expandable.contains(e.target)) {
|
|
||||||
expandable.setAttribute("aria-expanded", "false");
|
|
||||||
options.setAttribute("aria-hidden", "true");
|
|
||||||
options.classList.remove("show");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<div class="hero-preview-wrap reveal">
|
|
||||||
<div class="platform-pill">
|
|
||||||
<svg class="windows-icon" width="13" height="13" viewBox="0 0 24 24" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
|
||||||
</svg>
|
|
||||||
<span>Windows 10 / 11 (64-bit)</span>
|
|
||||||
|
|
||||||
<span class="platform-divider">•</span>
|
|
||||||
|
|
||||||
<svg class="android-icon" width="13" height="13" viewBox="0 0 24 24" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
|
||||||
</svg>
|
|
||||||
<span>Android</span>
|
|
||||||
|
|
||||||
<span class="platform-divider">•</span>
|
|
||||||
|
|
||||||
<svg class="mac-icon" width="13" height="13" viewBox="0 0 24 24" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
|
||||||
</svg>
|
|
||||||
<span>macOS (Apple Silicon)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="partners-inline reveal" id="partners-grid-wrap">
|
|
||||||
<p class="partners-label">Recognized and indexed across open media ecosystems</p>
|
|
||||||
<div class="partners-grid" id="partners-grid"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="stats-grid reveal-stagger" id="stats-grid">
|
|
||||||
<div class="stat-item reveal">
|
<section class="stats-grid" id="stats-grid" aria-label="Community Statistics">
|
||||||
<span class="stat-value" id="stat-members"></span>
|
<div class="stat-item">
|
||||||
|
<span class="stat-value" id="stat-members">1,250</span>
|
||||||
<span class="stat-label">Discord Members</span>
|
<span class="stat-label">Discord Members</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item reveal">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="stat-online"></span>
|
<span class="stat-value" id="stat-online">180</span>
|
||||||
<span class="stat-label">Online Now</span>
|
<span class="stat-label">Online Now</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<section id="features" class="section features-section" aria-labelledby="features-heading">
|
<section id="features" class="section features-section" aria-labelledby="features-heading">
|
||||||
<div class="container features-container">
|
<div class="container features-container">
|
||||||
<header class="features-heading reveal">
|
<header class="features-heading">
|
||||||
<h2 id="features-heading" class="section-title-lg">Why choose Home?</h2>
|
<h2 id="features-heading" class="section-title-lg">Why choose Home?</h2>
|
||||||
</header>
|
</header>
|
||||||
<article class="feature-block reveal">
|
<article class="feature-block">
|
||||||
<figure class="feature-media">
|
<figure class="feature-media">
|
||||||
<img src="assets/images/features/1.png"
|
<img src="assets/images/features/1.png"
|
||||||
alt="Diagram showing datacenter IP throttling bypass with residential network connection"
|
alt="Direct residential network bypass illustration avoiding datacenter throttling"
|
||||||
width="800" height="450" loading="lazy" decoding="async" fetchpriority="low"
|
width="800" height="450" loading="lazy" decoding="async">
|
||||||
srcset="assets/images/features/1.png 800w, assets/images/features/1.png 400w"
|
|
||||||
sizes="(max-width: 768px) 400px, 800px">
|
|
||||||
</figure>
|
</figure>
|
||||||
<div class="feature-copy">
|
<div class="feature-copy">
|
||||||
<h3 class="section-title-lg feature-title">No Datacenter Throttling</h3>
|
<h3 class="section-title-lg feature-title">No Datacenter Throttling</h3>
|
||||||
<p class="section-subtitle"> Shared hosting nodes and VPS providers routinely block traffic
|
<p class="section-subtitle">Shared hosting nodes and VPS providers routinely block traffic
|
||||||
originating from datacenters. We removed the datacenter entirely. Connect directly via your
|
originating from datacenters. We removed the datacenter entirely. Connect directly via your
|
||||||
residential network. </p>
|
residential network.</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature-block feature-block-reverse reveal">
|
<article class="feature-block feature-block-reverse">
|
||||||
<figure class="feature-media">
|
<figure class="feature-media">
|
||||||
<img src="assets/images/features/2.png"
|
<img src="assets/images/features/2.png"
|
||||||
alt="Diagram showing decentralized architecture eliminating single point of failure"
|
alt="Decentralized peer network architecture visualization" width="800" height="450"
|
||||||
width="800" height="450" loading="lazy" decoding="async" fetchpriority="low"
|
loading="lazy" decoding="async">
|
||||||
srcset="assets/images/features/2.png 800w, assets/images/features/2.png 400w"
|
|
||||||
sizes="(max-width: 768px) 400px, 800px">
|
|
||||||
</figure>
|
</figure>
|
||||||
<div class="feature-copy">
|
<div class="feature-copy">
|
||||||
<h3 class="section-title-lg feature-title">No Single Point of Failure</h3>
|
<h3 class="section-title-lg feature-title">No Single Point of Failure</h3>
|
||||||
<p class="section-subtitle"> When a central domain gets flagged or suspended, everyone goes
|
<p class="section-subtitle">When a central domain gets flagged or suspended, everyone goes dark.
|
||||||
dark. Running locally means your instance cannot be taken down by third parties. </p>
|
Running locally means your instance cannot be taken down by third parties.</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature-block reveal">
|
<article class="feature-block">
|
||||||
<figure class="feature-media">
|
<figure class="feature-media">
|
||||||
<img src="assets/images/features/3.png"
|
<img src="assets/images/features/3.png"
|
||||||
alt="Illustration of encrypted local vault with hardware profile key" width="800"
|
alt="Encrypted local database vault with cryptographic hardware key" width="800"
|
||||||
height="450" loading="lazy" decoding="async" fetchpriority="low"
|
height="450" loading="lazy" decoding="async">
|
||||||
srcset="assets/images/features/3.png 800w, assets/images/features/3.png 400w"
|
|
||||||
sizes="(max-width: 768px) 400px, 800px">
|
|
||||||
</figure>
|
</figure>
|
||||||
<div class="feature-copy">
|
<div class="feature-copy">
|
||||||
<h3 class="section-title-lg feature-title">Encrypted Local Vault</h3>
|
<h3 class="section-title-lg feature-title">Encrypted Local Vault</h3>
|
||||||
<p class="section-subtitle"> Your bookmarks, watch history, settings, and tokens are stored in
|
<p class="section-subtitle">Your bookmarks, watch history, settings, and tokens are stored in an
|
||||||
an encrypted vault keyed uniquely to your hardware profile. Total privacy. </p>
|
encrypted vault keyed uniquely to your hardware profile. Total privacy.</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature-block feature-block-reverse reveal">
|
<article class="feature-block feature-block-reverse">
|
||||||
<figure class="feature-media">
|
<figure class="feature-media">
|
||||||
<img src="assets/images/features/4.png"
|
<img src="assets/images/features/4.png"
|
||||||
alt="Interface showing automated over-the-air update process" width="800" height="450"
|
alt="Automated checksum-verified updates pipeline overview" width="800" height="450"
|
||||||
loading="lazy" decoding="async" fetchpriority="low"
|
loading="lazy" decoding="async">
|
||||||
srcset="assets/images/features/4.png 800w, assets/images/features/4.png 400w"
|
|
||||||
sizes="(max-width: 768px) 400px, 800px">
|
|
||||||
</figure>
|
</figure>
|
||||||
<div class="feature-copy">
|
<div class="feature-copy">
|
||||||
<h3 class="section-title-lg feature-title">Automated OTA Updates</h3>
|
<h3 class="section-title-lg feature-title">Automated OTA Updates</h3>
|
||||||
<p class="section-subtitle"> Vyla Home verifies releases directly from the repository upon
|
<p class="section-subtitle">Vyla Home verifies releases directly from the repository upon
|
||||||
startup. Update assets are downloaded, checksum-verified, and patched automatically. </p>
|
startup. Update assets are downloaded, checksum-verified, and patched automatically.</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature-block reveal">
|
<article class="feature-block">
|
||||||
<figure class="feature-media">
|
<figure class="feature-media">
|
||||||
<img src="assets/images/features/5.png"
|
<img src="assets/images/features/5.png"
|
||||||
alt="Interface showing rich media discovery with curated collections and 4K content"
|
alt="Rich media discovery system with high dynamic range and 4K media" width="800"
|
||||||
width="800" height="450" loading="lazy" decoding="async" fetchpriority="low"
|
height="450" loading="lazy" decoding="async">
|
||||||
srcset="assets/images/features/5.png 800w, assets/images/features/5.png 400w"
|
|
||||||
sizes="(max-width: 768px) 400px, 800px">
|
|
||||||
</figure>
|
</figure>
|
||||||
<div class="feature-copy">
|
<div class="feature-copy">
|
||||||
<h3 class="section-title-lg feature-title">Rich Discovery & Curated Media</h3>
|
<h3 class="section-title-lg feature-title">Rich Discovery & Curated Media</h3>
|
||||||
<p class="section-subtitle"> Access curated collections, 4K releases, popular anime series,
|
<p class="section-subtitle">Access curated collections, 4K releases, popular anime series,
|
||||||
soundcloak music streaming, and detailed TMDB cast breakdowns seamlessly. </p>
|
soundcloak music streaming, and detailed TMDB cast breakdowns seamlessly.</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="feature-block feature-block-reverse reveal">
|
<article class="feature-block feature-block-reverse">
|
||||||
<figure class="feature-media">
|
<figure class="feature-media">
|
||||||
<img src="assets/images/features/6.png"
|
<img src="assets/images/features/6.png"
|
||||||
alt="Interface showing customizable theme and layout options" width="800" height="450"
|
alt="Customizable UI controls, theme engine, and playback preferences" width="800"
|
||||||
loading="lazy" decoding="async" fetchpriority="low"
|
height="450" loading="lazy" decoding="async">
|
||||||
srcset="assets/images/features/6.png 800w, assets/images/features/6.png 400w"
|
|
||||||
sizes="(max-width: 768px) 400px, 800px">
|
|
||||||
</figure>
|
</figure>
|
||||||
<div class="feature-copy">
|
<div class="feature-copy">
|
||||||
<h3 class="section-title-lg feature-title">Customizable Interface</h3>
|
<h3 class="section-title-lg feature-title">Customizable Interface</h3>
|
||||||
<p class="section-subtitle"> Toggle dynamic color adaptations, trailer background playback,
|
<p class="section-subtitle">Toggle dynamic color adaptations, trailer background playback,
|
||||||
custom color themes, text-only mode, and layout preferences with full persistent storage.
|
custom color themes, text-only mode, and layout preferences with full persistent storage.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="testimonials" class="section testimonials-section" aria-labelledby="testimonials-heading">
|
<section id="testimonials" class="section testimonials-section" aria-labelledby="testimonials-heading">
|
||||||
<div class="container testimonials-container">
|
<header class="testimonials-heading">
|
||||||
<header class="testimonials-heading reveal">
|
<h2 id="testimonials-heading" class="section-title-lg span">Here's what people say about <span
|
||||||
<h2 id="testimonials-heading" class="section-title-lg">Here's what people say about <span
|
class="section-title-lg">Vyla Home</span></h2>
|
||||||
class="bold">Vyla Home</span>
|
</header>
|
||||||
</h2>
|
<div class="testimonials-marquee-wrapper">
|
||||||
</header>
|
<div class="testimonials-track" id="marquee-row-1"></div>
|
||||||
<div class="marquee-wrap marquee-fade-wrap" role="region" aria-label="User testimonials carousel">
|
<div class="testimonials-track testimonials-track-reverse" id="marquee-row-2"></div>
|
||||||
<div class="marquee-row marquee-left">
|
|
||||||
<div class="marquee-track" id="marquee-track-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="marquee-row marquee-right">
|
|
||||||
<div class="marquee-track" id="marquee-track-2"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="faq" class="section faq-section" aria-labelledby="faq-heading">
|
<section id="faq" class="section faq-section" aria-labelledby="faq-heading">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="faq-heading reveal">
|
<header class="faq-heading">
|
||||||
<h2 id="faq-heading" class="section-title-lg">FAQ</h2>
|
<h2 id="faq-heading" class="section-title-lg">Frequently Asked Questions</h2>
|
||||||
<p class="section-subtitle">Still have questions? <a href="https://discord.gg/5eqRhKeywG"
|
<p class="section-subtitle">Still have questions? <a href="https://discord.gg/5eqRhKeywG"
|
||||||
target="_blank" rel="noopener noreferrer">Ask in our Discord</a>
|
class="section-link" target="_blank" rel="noopener noreferrer">Ask in our Discord</a></p>
|
||||||
</p>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="faq-list reveal-stagger" id="faq-list" role="list" aria-label="Frequently asked questions">
|
<div class="faq-list" id="faq-list" role="region" aria-label="FAQ items"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="cta-footer-wrap">
|
<div class="cta-footer-wrap">
|
||||||
<div id="cta-rays" class="cta-rays-container" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<section class="cta-banner" aria-labelledby="cta-heading">
|
<section class="cta-banner" aria-labelledby="cta-heading">
|
||||||
<div class="container cta-banner-content reveal">
|
<div class="cta-atmosphere" aria-hidden="true">
|
||||||
|
<video class="cta-atmosphere-video" autoplay muted loop playsinline preload="auto">
|
||||||
|
<source src="https://www.pexels.com/download/video/38243003/" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div class="container cta-banner-content">
|
||||||
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
|
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
|
||||||
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
||||||
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
|
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
|
||||||
<button class="hero-cta hero-cta-light" id="cta-download-btn">
|
<button class="hero-cta hero-cta-light" id="cta-download-btn" type="button" aria-haspopup="true"
|
||||||
|
aria-controls="cta-download-options">
|
||||||
<span class="download-btn-text">Download Now</span>
|
<span class="download-btn-text">Download Now</span>
|
||||||
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
@@ -616,123 +334,44 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="download-options" id="cta-download-options" aria-hidden="true">
|
<div class="download-options" id="cta-download-options" aria-hidden="true">
|
||||||
<a href="#" class="download-option" id="cta-windows-download">
|
<a href="#" class="download-option" id="cta-windows-download" aria-label="Download for Windows">
|
||||||
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Windows v0.0.0</span>
|
<span>Windows</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="cta-mac-download">
|
<a href="#" class="download-option" id="cta-mac-download" aria-label="Download for macOS">
|
||||||
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Mac v0.0.0</span>
|
<span>macOS</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="cta-go-download">
|
<a href="#" class="download-option" id="cta-go-download" aria-label="Download for Android">
|
||||||
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Android v0.0.0</span>
|
<span>Android</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
async function setupExpandableDownload(expandableId, btnId, optionsId, windowsId, goId, macId) {
|
|
||||||
const expandable = document.getElementById(expandableId);
|
|
||||||
const downloadBtn = document.getElementById(btnId);
|
|
||||||
const downloadOptions = document.getElementById(optionsId);
|
|
||||||
const windowsDownload = document.getElementById(windowsId);
|
|
||||||
const goDownload = document.getElementById(goId);
|
|
||||||
const macDownload = document.getElementById(macId);
|
|
||||||
if (!expandable || !downloadBtn || !downloadOptions || !windowsDownload || !goDownload || !macDownload) return;
|
|
||||||
try {
|
|
||||||
const [homeResponse, goResponse] = await Promise.all([
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"),
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases/permalink/latest")
|
|
||||||
]);
|
|
||||||
if (!homeResponse.ok) throw new Error("Failed to fetch latest home release");
|
|
||||||
if (!goResponse.ok) throw new Error("Failed to fetch latest go release");
|
|
||||||
const homeRelease = await homeResponse.json();
|
|
||||||
const goRelease = await goResponse.json();
|
|
||||||
const version = homeRelease.tag_name;
|
|
||||||
const goVersion = goRelease.tag_name;
|
|
||||||
const fileVersion = version.replace(/^v/, "");
|
|
||||||
const goFileVersion = goVersion.replace(/^v/, "");
|
|
||||||
const windowsAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".exe"));
|
|
||||||
const macAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".dmg"));
|
|
||||||
const goAsset = goRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".apk"));
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = `Download ${version}`;
|
|
||||||
}
|
|
||||||
if (windowsAsset?.url) {
|
|
||||||
windowsDownload.href = windowsAsset.url;
|
|
||||||
} else {
|
|
||||||
windowsDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = `Windows ${version}`;
|
|
||||||
if (goAsset?.url) {
|
|
||||||
goDownload.href = goAsset.url;
|
|
||||||
} else {
|
|
||||||
goDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/packages/generic/vyla-go/${goVersion}/vyla-go-${goFileVersion}.apk`;
|
|
||||||
}
|
|
||||||
goDownload.querySelector("span").textContent = `Android ${goVersion}`;
|
|
||||||
if (macAsset?.url) {
|
|
||||||
macDownload.href = macAsset.url;
|
|
||||||
} else {
|
|
||||||
macDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_aarch64.dmg`;
|
|
||||||
}
|
|
||||||
macDownload.querySelector("span").textContent = `Mac ${version}`;
|
|
||||||
downloadBtn.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const isExpanded = expandable.getAttribute("aria-expanded") === "true";
|
|
||||||
expandable.setAttribute("aria-expanded", String(!isExpanded));
|
|
||||||
downloadOptions.setAttribute("aria-hidden", String(isExpanded));
|
|
||||||
downloadOptions.classList.toggle("show", !isExpanded);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = "Download Latest Release";
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = "Windows (Latest)";
|
|
||||||
goDownload.querySelector("span").textContent = "Android (Latest)";
|
|
||||||
macDownload.querySelector("span").textContent = "Mac (Latest)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setupExpandableDownload("cta-download-expandable", "cta-download-btn", "cta-download-options", "cta-windows-download", "cta-go-download", "cta-mac-download");
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
const expandables = document.querySelectorAll(".download-expandable");
|
|
||||||
expandables.forEach(expandable => {
|
|
||||||
const btn = expandable.querySelector("button");
|
|
||||||
const options = expandable.querySelector(".download-options");
|
|
||||||
if (btn && options && !expandable.contains(e.target)) {
|
|
||||||
expandable.setAttribute("aria-expanded", "false");
|
|
||||||
options.setAttribute("aria-hidden", "true");
|
|
||||||
options.classList.remove("show");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer" role="contentinfo">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Vyla</h2>
|
<h2>Vyla</h2>
|
||||||
<p>Vyla is a passion project born from a love for film and television.</p>
|
<p>Vyla is a passion project born from a love for film and television.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Backup Site:</h2>
|
<h2>Backup Site</h2>
|
||||||
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
|
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener noreferrer">https://vyla.qzz.io</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
@@ -740,33 +379,32 @@
|
|||||||
<a href="/terms-of-service">Terms of Service</a>
|
<a href="/terms-of-service">Terms of Service</a>
|
||||||
<a href="/privacy-policy">Privacy Policy</a>
|
<a href="/privacy-policy">Privacy Policy</a>
|
||||||
<a href="/about-us">About Us</a>
|
<a href="/about-us">About Us</a>
|
||||||
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener">DMCA</a>
|
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener noreferrer">DMCA</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Our API</h2>
|
<h2>Developers</h2>
|
||||||
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
|
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Join our Discord</h2>
|
<h2>Community</h2>
|
||||||
<div class="social-links">
|
<div class="social-links">
|
||||||
<a href="/discord" target="_blank" rel="noopener">
|
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
|
||||||
<i class="fab fa-discord"></i> Discord
|
aria-label="Official Discord Server">Discord</a>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
|
<div class="footer-section footer-badge">
|
||||||
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
|
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
|
||||||
height="95" loading="lazy" decoding="async" fetchpriority="low">
|
alt="This website runs on green hosting verified by thegreenwebfoundation.org" width="200"
|
||||||
|
height="95" loading="lazy" decoding="async">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
|
<p>© 2025 Vyla Entertainment. All rights reserved.</p>
|
||||||
© 2025 Vyla Entertainment. All rights reserved.
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="src/scripts/main.js"></script>
|
<script type="module" src="src/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -86,34 +86,8 @@
|
|||||||
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/base/tokens.css">
|
<link rel="stylesheet" href="/src/css/main.css">
|
||||||
<link rel="stylesheet" href="/src/styles/base/reset.css">
|
<link rel="stylesheet" href="/src/css/other.css">
|
||||||
<link rel="stylesheet" href="/src/styles/base/typography.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/layout.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/reveal.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/layout/nav.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/hero-atmosphere.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/hero-content.css">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/layout/footer.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/buttons.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/pills.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/stats.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/script-card.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/marquee.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/testimonial-card.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/modal.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/partners.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/faq.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/section-base.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/testimonials.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/features.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/faq-section.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/cta-banner.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/responsive.css" media="print" onload="this.media='all'">
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/pages/other.css" />
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
@@ -241,14 +215,6 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header role="banner">
|
|
||||||
<nav class="nav" role="navigation" aria-label="Main navigation">
|
|
||||||
<div class="logo-image">
|
|
||||||
<img src="/assets/images/icons/logo_white.png" alt="Vyla Home">
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<h2>Privacy Policy</h2>
|
<h2>Privacy Policy</h2>
|
||||||
@@ -314,14 +280,18 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="cta-footer-wrap">
|
<div class="cta-footer-wrap">
|
||||||
<div id="cta-rays" class="cta-rays-container" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<section class="cta-banner" aria-labelledby="cta-heading">
|
<section class="cta-banner" aria-labelledby="cta-heading">
|
||||||
<div class="container cta-banner-content reveal">
|
<div class="cta-atmosphere" aria-hidden="true">
|
||||||
|
<video class="cta-atmosphere-video" autoplay muted loop playsinline preload="auto">
|
||||||
|
<source src="https://www.pexels.com/download/video/38243003/" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div class="container cta-banner-content">
|
||||||
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
|
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
|
||||||
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
||||||
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
|
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
|
||||||
<button class="hero-cta hero-cta-light" id="cta-download-btn">
|
<button class="hero-cta hero-cta-light" id="cta-download-btn" type="button" aria-haspopup="true"
|
||||||
|
aria-controls="cta-download-options">
|
||||||
<span class="download-btn-text">Download Now</span>
|
<span class="download-btn-text">Download Now</span>
|
||||||
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
@@ -329,123 +299,44 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="download-options" id="cta-download-options" aria-hidden="true">
|
<div class="download-options" id="cta-download-options" aria-hidden="true">
|
||||||
<a href="#" class="download-option" id="cta-windows-download">
|
<a href="#" class="download-option" id="cta-windows-download" aria-label="Download for Windows">
|
||||||
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Windows v0.0.0</span>
|
<span>Windows</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="cta-mac-download">
|
<a href="#" class="download-option" id="cta-mac-download" aria-label="Download for macOS">
|
||||||
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Mac v0.0.0</span>
|
<span>macOS</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="cta-go-download">
|
<a href="#" class="download-option" id="cta-go-download" aria-label="Download for Android">
|
||||||
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Android v0.0.0</span>
|
<span>Android</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
async function setupExpandableDownload(expandableId, btnId, optionsId, windowsId, goId, macId) {
|
|
||||||
const expandable = document.getElementById(expandableId);
|
|
||||||
const downloadBtn = document.getElementById(btnId);
|
|
||||||
const downloadOptions = document.getElementById(optionsId);
|
|
||||||
const windowsDownload = document.getElementById(windowsId);
|
|
||||||
const goDownload = document.getElementById(goId);
|
|
||||||
const macDownload = document.getElementById(macId);
|
|
||||||
if (!expandable || !downloadBtn || !downloadOptions || !windowsDownload || !goDownload || !macDownload) return;
|
|
||||||
try {
|
|
||||||
const [homeResponse, goResponse] = await Promise.all([
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"),
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases/permalink/latest")
|
|
||||||
]);
|
|
||||||
if (!homeResponse.ok) throw new Error("Failed to fetch latest home release");
|
|
||||||
if (!goResponse.ok) throw new Error("Failed to fetch latest go release");
|
|
||||||
const homeRelease = await homeResponse.json();
|
|
||||||
const goRelease = await goResponse.json();
|
|
||||||
const version = homeRelease.tag_name;
|
|
||||||
const goVersion = goRelease.tag_name;
|
|
||||||
const fileVersion = version.replace(/^v/, "");
|
|
||||||
const goFileVersion = goVersion.replace(/^v/, "");
|
|
||||||
const windowsAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".exe"));
|
|
||||||
const macAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".dmg"));
|
|
||||||
const goAsset = goRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".apk"));
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = `Download ${version}`;
|
|
||||||
}
|
|
||||||
if (windowsAsset?.url) {
|
|
||||||
windowsDownload.href = windowsAsset.url;
|
|
||||||
} else {
|
|
||||||
windowsDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = `Windows ${version}`;
|
|
||||||
if (goAsset?.url) {
|
|
||||||
goDownload.href = goAsset.url;
|
|
||||||
} else {
|
|
||||||
goDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/packages/generic/vyla-go/${goVersion}/vyla-go-${goFileVersion}.apk`;
|
|
||||||
}
|
|
||||||
goDownload.querySelector("span").textContent = `Android ${goVersion}`;
|
|
||||||
if (macAsset?.url) {
|
|
||||||
macDownload.href = macAsset.url;
|
|
||||||
} else {
|
|
||||||
macDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_aarch64.dmg`;
|
|
||||||
}
|
|
||||||
macDownload.querySelector("span").textContent = `Mac ${version}`;
|
|
||||||
downloadBtn.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const isExpanded = expandable.getAttribute("aria-expanded") === "true";
|
|
||||||
expandable.setAttribute("aria-expanded", String(!isExpanded));
|
|
||||||
downloadOptions.setAttribute("aria-hidden", String(isExpanded));
|
|
||||||
downloadOptions.classList.toggle("show", !isExpanded);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = "Download Latest Release";
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = "Windows (Latest)";
|
|
||||||
goDownload.querySelector("span").textContent = "Android (Latest)";
|
|
||||||
macDownload.querySelector("span").textContent = "Mac (Latest)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setupExpandableDownload("cta-download-expandable", "cta-download-btn", "cta-download-options", "cta-windows-download", "cta-go-download", "cta-mac-download");
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
const expandables = document.querySelectorAll(".download-expandable");
|
|
||||||
expandables.forEach(expandable => {
|
|
||||||
const btn = expandable.querySelector("button");
|
|
||||||
const options = expandable.querySelector(".download-options");
|
|
||||||
if (btn && options && !expandable.contains(e.target)) {
|
|
||||||
expandable.setAttribute("aria-expanded", "false");
|
|
||||||
options.setAttribute("aria-hidden", "true");
|
|
||||||
options.classList.remove("show");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer" role="contentinfo">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Vyla</h2>
|
<h2>Vyla</h2>
|
||||||
<p>Vyla is a passion project born from a love for film and television.</p>
|
<p>Vyla is a passion project born from a love for film and television.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Backup Site:</h2>
|
<h2>Backup Site</h2>
|
||||||
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
|
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener noreferrer">https://vyla.qzz.io</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
@@ -453,33 +344,32 @@
|
|||||||
<a href="/terms-of-service">Terms of Service</a>
|
<a href="/terms-of-service">Terms of Service</a>
|
||||||
<a href="/privacy-policy">Privacy Policy</a>
|
<a href="/privacy-policy">Privacy Policy</a>
|
||||||
<a href="/about-us">About Us</a>
|
<a href="/about-us">About Us</a>
|
||||||
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener">DMCA</a>
|
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener noreferrer">DMCA</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Our API</h2>
|
<h2>Developers</h2>
|
||||||
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
|
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Join our Discord</h2>
|
<h2>Community</h2>
|
||||||
<div class="social-links">
|
<div class="social-links">
|
||||||
<a href="/discord" target="_blank" rel="noopener">
|
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
|
||||||
<i class="fab fa-discord"></i> Discord
|
aria-label="Official Discord Server">Discord</a>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
|
<div class="footer-section footer-badge">
|
||||||
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
|
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
|
||||||
height="95" loading="lazy" decoding="async">
|
alt="This website runs on green hosting verified by thegreenwebfoundation.org" width="200"
|
||||||
|
height="95" loading="lazy" decoding="async">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
|
<p>© 2025 Vyla Entertainment. All rights reserved.</p>
|
||||||
© 2025 Vyla Entertainment. All rights reserved.
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/scripts/main.js"></script>
|
<script type="module" src="/src/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
78
src/css/base.css
Normal file
78
src/css/base.css
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
background-color: var(--color-bg-base);
|
||||||
|
color: var(--color-text-main);
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-ambient-bg {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: var(--z-ambient);
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-ambient-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
filter: blur(20px) brightness(1.2);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity var(--transition-linear);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: var(--space-7xl) var(--space-xl);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: var(--max-w-content);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-lg {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-lg span {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
margin-top: var(--space-xs);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
line-height: var(--line-height-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-link {
|
||||||
|
color: var(--color-accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-link:hover {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
47
src/css/cta.css
Normal file
47
src/css/cta.css
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.cta-banner {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-8xl) var(--space-xl);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-banner .download-expandable {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-atmosphere {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: var(--z-base);
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
mask-image: linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 25%, black 75%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-atmosphere-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-banner-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--z-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-banner-title {
|
||||||
|
font-size: clamp(2.1rem, 4vw, 3.2rem);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
letter-spacing: -0.035em;
|
||||||
|
line-height: var(--line-height-mid);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-banner-subtitle {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
margin-bottom: var(--space-2xl);
|
||||||
|
}
|
||||||
77
src/css/faq.css
Normal file
77
src/css/faq.css
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
.faq-heading {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-5xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-list {
|
||||||
|
max-width: var(--max-w-faq);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
border-top: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item:last-child {
|
||||||
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--color-accent);
|
||||||
|
padding: 1.4rem var(--space-2xs);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-chevron {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2.2;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
transition: transform var(--transition-smooth);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item.active .faq-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-answer {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height var(--transition-smooth), padding var(--transition-smooth);
|
||||||
|
padding: 0 var(--space-2xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
line-height: var(--line-height-spacious);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item.active .faq-answer {
|
||||||
|
padding-bottom: 1.4rem;
|
||||||
|
}
|
||||||
39
src/css/features.css
Normal file
39
src/css/features.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.features-heading {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-5xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-bottom: var(--space-6xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block-reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-media {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-media img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-copy {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.feature-block,
|
||||||
|
.feature-block-reverse {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/css/footer.css
Normal file
60
src/css/footer.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--space-2xl) var(--space-xl) var(--space-xl);
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
|
z-index: var(--z-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
max-width: var(--max-w-footer);
|
||||||
|
margin: 0 auto var(--space-2xl);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: var(--space-3xl);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--z-footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h2 {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section p {
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: color var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section a:hover {
|
||||||
|
color: var(--color-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--z-footer);
|
||||||
|
padding-top: var(--space-xl);
|
||||||
|
border-top: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
@@ -5,23 +5,22 @@
|
|||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: var(--glass-radius, 20px) !important;
|
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
max-width: max-content !important;
|
max-width: max-content !important;
|
||||||
transition: opacity 260ms ease-out;
|
transition: opacity var(--glass-transition-duration) ease-out;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-surface.dark-mode:focus-visible {
|
.glass-surface.dark-mode:focus-visible {
|
||||||
outline: 2px solid #0A84FF;
|
outline: 2px solid var(--glass-focus-blue-dark);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-surface.light-mode:focus-visible {
|
.glass-surface.light-mode:focus-visible {
|
||||||
outline: 2px solid #007AFF;
|
outline: 2px solid var(--glass-focus-blue-light);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,15 +50,5 @@
|
|||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
padding: 4px;
|
padding: var(--glass-content-padding);
|
||||||
}
|
|
||||||
|
|
||||||
.glass-surface .fa-solid,
|
|
||||||
.glass-surface .fas {
|
|
||||||
padding-right: 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-surface--fallback .fa-solid,
|
|
||||||
.glass-surface--fallback .fas {
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
}
|
||||||
157
src/css/hero.css
Normal file
157
src/css/hero.css
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
min-height: 85vh;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: var(--space-6xl);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-atmosphere {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: var(--z-base);
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-atmosphere-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--z-content);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
max-width: var(--max-w-hero);
|
||||||
|
padding: var(--space-2xl) var(--space-xl);
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: clamp(2.2rem, 4vw, 3.5rem);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
letter-spacing: -0.035em;
|
||||||
|
line-height: var(--line-height-tight);
|
||||||
|
margin: var(--space-lg) 0 var(--space-md);
|
||||||
|
color: var(--color-accent);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow: visible;
|
||||||
|
word-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blur-char {
|
||||||
|
display: inline-block;
|
||||||
|
animation: blurIn 0.4s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blur-char.is-space {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blurIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(8px);
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
filter: blur(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
max-width: 680px;
|
||||||
|
margin-bottom: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-bottom: var(--space-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-expandable {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
padding: 0.85rem 1.75rem;
|
||||||
|
font-size: var(--font-size-btn);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
max-width: 200px;
|
||||||
|
color: var(--color-accent);
|
||||||
|
background-color: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-options {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + var(--space-xs));
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
backdrop-filter: blur(20px) saturate(2.5) brightness(1.2);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(2.5) brightness(1.2);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: var(--z-dropdown);
|
||||||
|
width: max-content;
|
||||||
|
min-width: 175px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-options.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
color: var(--color-text-card);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
transition: background var(--transition-fast), color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-option:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
11
src/css/main.css
Normal file
11
src/css/main.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@import './variables.css';
|
||||||
|
@import './base.css';
|
||||||
|
@import './glass-surface.css';
|
||||||
|
@import './nav.css';
|
||||||
|
@import './hero.css';
|
||||||
|
@import './stats.css';
|
||||||
|
@import './features.css';
|
||||||
|
@import './testimonials.css';
|
||||||
|
@import './faq.css';
|
||||||
|
@import './cta.css';
|
||||||
|
@import './footer.css';
|
||||||
56
src/css/nav.css
Normal file
56
src/css/nav.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
header[role="banner"] {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: var(--z-nav);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-lg) var(--space-md);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
color: var(--color-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--font-size-btn);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
opacity: 0.85;
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-discord-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--color-accent);
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
|
margin-right: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-discord-link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
45
src/css/other.css
Normal file
45
src/css/other.css
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
.content-wrapper {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper h2 {
|
||||||
|
margin-top: 5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper p {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 200;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper ul {
|
||||||
|
list-style: disc;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper span {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
29
src/css/stats.css
Normal file
29
src/css/stats.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.stats-grid {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-4xl);
|
||||||
|
padding: var(--space-4xl) var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
font-weight: var(--font-weight-black);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-grid {
|
||||||
|
gap: var(--space-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/css/testimonials.css
Normal file
104
src/css/testimonials.css
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
.testimonials-section {
|
||||||
|
padding: var(--space-7xl) 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonials-heading {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-5xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonials-marquee-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
mask-image: linear-gradient(to right, transparent, black 10%, black 90%, transparent);
|
||||||
|
-webkit-mask-image: linear-gradient(to right, transparent, black 10%, black 90%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonials-track {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-md);
|
||||||
|
width: max-content;
|
||||||
|
animation: marquee-scroll 45s linear infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonials-track-reverse {
|
||||||
|
animation: marquee-scroll-reverse 45s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonials-marquee-wrapper:hover .testimonials-track {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee-scroll {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee-scroll-reverse {
|
||||||
|
from {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-card {
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--space-xl);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
background-color: var(--color-card-bg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-avatar {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: var(--radius-circle);
|
||||||
|
object-fit: cover;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-name {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-handle {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-text {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
line-height: var(--line-height-base);
|
||||||
|
color: var(--color-text-card);
|
||||||
|
}
|
||||||
79
src/css/variables.css
Normal file
79
src/css/variables.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
:root {
|
||||||
|
--color-bg-base: #000000;
|
||||||
|
--color-text-main: #f5f5f7;
|
||||||
|
--color-text-dim: #a1a1a6;
|
||||||
|
--color-text-muted: #8b8b93;
|
||||||
|
--color-text-card: #e4e4e7;
|
||||||
|
--color-accent: #ffffff;
|
||||||
|
--color-card-bg: rgba(5, 5, 5, 0.54);
|
||||||
|
--color-border-subtle: rgba(255, 255, 255, 0.08);
|
||||||
|
--color-border-hover: rgba(255, 255, 255, 0.15);
|
||||||
|
--color-focus: #007aff;
|
||||||
|
--color-focus-dark: #0a84ff;
|
||||||
|
|
||||||
|
--font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-size-xs: 0.8rem;
|
||||||
|
--font-size-sm: 0.85rem;
|
||||||
|
--font-size-btn: 0.87rem;
|
||||||
|
--font-size-base: 0.95rem;
|
||||||
|
--font-size-md: 1rem;
|
||||||
|
--font-size-lg: 1.05rem;
|
||||||
|
--font-size-xl: 2.1rem;
|
||||||
|
--font-size-2xl: 2.25rem;
|
||||||
|
|
||||||
|
--font-weight-regular: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--font-weight-black: 800;
|
||||||
|
|
||||||
|
--line-height-tight: 1.12;
|
||||||
|
--line-height-mid: 1.15;
|
||||||
|
--line-height-base: 1.5;
|
||||||
|
--line-height-relaxed: 1.6;
|
||||||
|
--line-height-spacious: 1.65;
|
||||||
|
|
||||||
|
--space-2xs: 0.2rem;
|
||||||
|
--space-xs: 0.5rem;
|
||||||
|
--space-sm: 0.75rem;
|
||||||
|
--space-md: 1rem;
|
||||||
|
--space-lg: 1.25rem;
|
||||||
|
--space-xl: 1.5rem;
|
||||||
|
--space-2xl: 2rem;
|
||||||
|
--space-3xl: 2.5rem;
|
||||||
|
--space-4xl: 3rem;
|
||||||
|
--space-5xl: 4rem;
|
||||||
|
--space-6xl: 5rem;
|
||||||
|
--space-7xl: 6rem;
|
||||||
|
--space-8xl: 8rem;
|
||||||
|
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--radius-md: 14px;
|
||||||
|
--radius-lg: 18px;
|
||||||
|
--radius-xl: 30px;
|
||||||
|
--radius-pill: 9999px;
|
||||||
|
--radius-circle: 50%;
|
||||||
|
|
||||||
|
--transition-fast: 0.15s ease;
|
||||||
|
--transition-normal: 0.2s ease;
|
||||||
|
--transition-smooth: 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--transition-linear: 0.1s linear;
|
||||||
|
|
||||||
|
--z-ambient: -2;
|
||||||
|
--z-grid: -1;
|
||||||
|
--z-base: 0;
|
||||||
|
--z-content: 1;
|
||||||
|
--z-footer: 2;
|
||||||
|
--z-dropdown: 50;
|
||||||
|
--z-nav: 1000;
|
||||||
|
|
||||||
|
--max-w-hero: 900px;
|
||||||
|
--max-w-content: 1100px;
|
||||||
|
--max-w-faq: 800px;
|
||||||
|
--max-w-footer: 1400px;
|
||||||
|
|
||||||
|
--glass-focus-blue-dark: #0a84ff;
|
||||||
|
--glass-focus-blue-light: #007aff;
|
||||||
|
--glass-content-padding: 4px;
|
||||||
|
--glass-transition-duration: 260ms;
|
||||||
|
}
|
||||||
26
src/data/faq.json
Normal file
26
src/data/faq.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"q": "Why did Vyla transition from a web app to Vyla Home?",
|
||||||
|
"a": "Web-based media centers are constantly vulnerable to datacenter IP blacklisting, upstream downtime, and web-browser sandbox limits. Transitioning to a native application permits decentralization directly over local networks with hardware-accelerated playback."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Why does Windows SmartScreen flag the installer?",
|
||||||
|
"a": "Windows SmartScreen flags newer binaries that have not yet accumulated arbitrary trust reputation telemetry or paid EV code-signing signatures. Every build is built transparently and verifiable directly on GitLab."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "How are my settings, bookmarks, and watch history protected?",
|
||||||
|
"a": "All user data is stored locally in an AES-256-GCM encrypted database keyed to your device hardware profile. No usage logs, browsing history, or playback metrics are ever transmitted to external servers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "How does the update mechanism ensure binary integrity?",
|
||||||
|
"a": "Releases are validated against cryptographic checksum hashes pulled directly from the GitLab package registry before patching or staging executable files."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Does Vyla Home require external runtime dependencies?",
|
||||||
|
"a": "No. The application bundles its playback core, local server, and storage drivers in a self-contained runtime without requiring third-party libraries."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Is Vyla Home open-source and free?",
|
||||||
|
"a": "Yes. Vyla Home is free, community-driven, and maintains its repositories and release builds openly for public inspection."
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -40,5 +40,26 @@
|
|||||||
"text": "ur like the best owner ever❤️",
|
"text": "ur like the best owner ever❤️",
|
||||||
"avatar": "/assets/images/testimonials/lucky_mucky_.png",
|
"avatar": "/assets/images/testimonials/lucky_mucky_.png",
|
||||||
"avatarSmall": "/assets/images/testimonials/lucky_mucky_.png"
|
"avatarSmall": "/assets/images/testimonials/lucky_mucky_.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clone",
|
||||||
|
"handle": "@asherread",
|
||||||
|
"text": "It's pretty good. And constantly improving",
|
||||||
|
"avatar": "/assets/images/testimonials/asherread.png",
|
||||||
|
"avatarSmall": "/assets/images/testimonials/asherread.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Misty Winters",
|
||||||
|
"handle": "@ghomistty",
|
||||||
|
"text": "TBH this site is so popular that I used it to watch shows before I even joined this server",
|
||||||
|
"avatar": "/assets/images/testimonials/ghomistty.png",
|
||||||
|
"avatarSmall": "/assets/images/testimonials/ghomistty.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Poly-webster",
|
||||||
|
"handle": "@saiyance0",
|
||||||
|
"text": "works good, i tested it. sources give good quality. there were issue but @mes solved em.",
|
||||||
|
"avatar": "/assets/images/testimonials/saiyance0.png",
|
||||||
|
"avatarSmall": "/assets/images/testimonials/saiyance0.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
23
src/js/main.js
Normal file
23
src/js/main.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { initAmbientSync } from './modules/ambient-sync.js';
|
||||||
|
import { initReleases } from './modules/releases.js';
|
||||||
|
import { initLiquidGlass } from './modules/glass-surface.js';
|
||||||
|
import { initStats } from './modules/stats.js';
|
||||||
|
import { initFAQ } from './modules/faq.js';
|
||||||
|
import { initTestimonials } from './modules/testimonials.js';
|
||||||
|
import { initTypewriter } from './modules/typewriter.js';
|
||||||
|
|
||||||
|
function initApp() {
|
||||||
|
initAmbientSync();
|
||||||
|
initReleases();
|
||||||
|
initLiquidGlass();
|
||||||
|
initStats();
|
||||||
|
initFAQ();
|
||||||
|
initTestimonials();
|
||||||
|
initTypewriter();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initApp, { once: true });
|
||||||
|
} else {
|
||||||
|
initApp();
|
||||||
|
}
|
||||||
32
src/js/modules/ambient-sync.js
Normal file
32
src/js/modules/ambient-sync.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export function initAmbientSync() {
|
||||||
|
const heroVideo = document.querySelector('.hero-atmosphere-video');
|
||||||
|
const ambientVideo = document.getElementById('body-ambient-video');
|
||||||
|
|
||||||
|
if (heroVideo && ambientVideo) {
|
||||||
|
const syncVideos = () => {
|
||||||
|
if (Math.abs(heroVideo.currentTime - ambientVideo.currentTime) > 0.25) {
|
||||||
|
ambientVideo.currentTime = heroVideo.currentTime;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
heroVideo.addEventListener('timeupdate', syncVideos, { passive: true });
|
||||||
|
|
||||||
|
const matchPlayState = () => {
|
||||||
|
if (!heroVideo.paused && ambientVideo.paused) ambientVideo.play().catch(() => { });
|
||||||
|
if (heroVideo.paused && !ambientVideo.paused) ambientVideo.pause();
|
||||||
|
};
|
||||||
|
heroVideo.addEventListener('play', matchPlayState);
|
||||||
|
heroVideo.addEventListener('pause', matchPlayState);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!ambientVideo) return;
|
||||||
|
const scrollY = window.scrollY || window.pageYOffset;
|
||||||
|
const fadeDistance = window.innerHeight * 1.5;
|
||||||
|
const progress = Math.min(Math.max(scrollY / fadeDistance, 0), 1);
|
||||||
|
const opacity = Math.cos(progress * (Math.PI / 2));
|
||||||
|
ambientVideo.style.opacity = Math.max(opacity, 0).toFixed(3);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
handleScroll();
|
||||||
|
}
|
||||||
47
src/js/modules/faq.js
Normal file
47
src/js/modules/faq.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
export async function initFAQ() {
|
||||||
|
const faqContainer = document.getElementById('faq-list');
|
||||||
|
if (!faqContainer) return;
|
||||||
|
|
||||||
|
const response = await fetch('/src/data/faq.json');
|
||||||
|
const faqs = await response.json();
|
||||||
|
|
||||||
|
faqContainer.innerHTML = faqs.map((item, idx) => `
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-question" type="button" aria-expanded="false" aria-controls="faq-ans-${idx}">
|
||||||
|
<span>${item.q}</span>
|
||||||
|
<svg class="faq-chevron" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="faq-answer" id="faq-ans-${idx}">
|
||||||
|
<p>${item.a}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
faqContainer.querySelectorAll('.faq-question').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const item = btn.closest('.faq-item');
|
||||||
|
const answer = item.querySelector('.faq-answer');
|
||||||
|
const isOpen = item.classList.contains('active');
|
||||||
|
|
||||||
|
faqContainer.querySelectorAll('.faq-item').forEach(other => {
|
||||||
|
if (other !== item && other.classList.contains('active')) {
|
||||||
|
other.classList.remove('active');
|
||||||
|
other.querySelector('.faq-question').setAttribute('aria-expanded', 'false');
|
||||||
|
other.querySelector('.faq-answer').style.maxHeight = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
item.classList.remove('active');
|
||||||
|
btn.setAttribute('aria-expanded', 'false');
|
||||||
|
answer.style.maxHeight = null;
|
||||||
|
} else {
|
||||||
|
item.classList.add('active');
|
||||||
|
btn.setAttribute('aria-expanded', 'true');
|
||||||
|
answer.style.maxHeight = `${answer.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,9 +2,6 @@ class GlassSurface {
|
|||||||
constructor(element, config = {}) {
|
constructor(element, config = {}) {
|
||||||
this.el = element;
|
this.el = element;
|
||||||
this.config = Object.assign({
|
this.config = Object.assign({
|
||||||
width: 200,
|
|
||||||
height: 20,
|
|
||||||
borderRadius: 20,
|
|
||||||
borderWidth: 0.07,
|
borderWidth: 0.07,
|
||||||
brightness: 50,
|
brightness: 50,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@@ -27,7 +24,6 @@ class GlassSurface {
|
|||||||
this.filterId = `glass-filter-${this.uniqueId}`;
|
this.filterId = `glass-filter-${this.uniqueId}`;
|
||||||
this.redGradId = `red-grad-${this.uniqueId}`;
|
this.redGradId = `red-grad-${this.uniqueId}`;
|
||||||
this.blueGradId = `blue-grad-${this.uniqueId}`;
|
this.blueGradId = `blue-grad-${this.uniqueId}`;
|
||||||
|
|
||||||
this.isMounted = false;
|
this.isMounted = false;
|
||||||
this.liquidGlassEnabled = true;
|
this.liquidGlassEnabled = true;
|
||||||
|
|
||||||
@@ -84,7 +80,6 @@ class GlassSurface {
|
|||||||
this.el.style.contain = 'layout style paint';
|
this.el.style.contain = 'layout style paint';
|
||||||
|
|
||||||
this.applyBaseStyles();
|
this.applyBaseStyles();
|
||||||
|
|
||||||
this.createContentWrapper();
|
this.createContentWrapper();
|
||||||
|
|
||||||
if (this.liquidGlassEnabled) {
|
if (this.liquidGlassEnabled) {
|
||||||
@@ -103,8 +98,7 @@ class GlassSurface {
|
|||||||
const parsed = JSON.parse(settings);
|
const parsed = JSON.parse(settings);
|
||||||
this.liquidGlassEnabled = parsed.liquidGlassEnabled !== false;
|
this.liquidGlassEnabled = parsed.liquidGlassEnabled !== false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch { }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyBaseStyles() {
|
applyBaseStyles() {
|
||||||
@@ -123,11 +117,8 @@ class GlassSurface {
|
|||||||
this.el.classList.add(...this.config.className.split(' '));
|
this.el.classList.add(...this.config.className.split(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isDark) {
|
this.el.classList.toggle('dark-mode', this.isDark);
|
||||||
this.el.classList.add('dark-mode');
|
this.el.classList.toggle('light-mode', !this.isDark);
|
||||||
} else {
|
|
||||||
this.el.classList.add('light-mode');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createContentWrapper() {
|
createContentWrapper() {
|
||||||
@@ -139,7 +130,6 @@ class GlassSurface {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 4px;
|
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -212,7 +202,7 @@ class GlassSurface {
|
|||||||
<feDisplacementMap ref="dispGreen" in="SourceGraphic" in2="map" id="greenchannel" result="dispGreen" />
|
<feDisplacementMap ref="dispGreen" in="SourceGraphic" in2="map" id="greenchannel" result="dispGreen" />
|
||||||
<feColorMatrix in="dispGreen" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="green" />
|
<feColorMatrix in="dispGreen" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="green" />
|
||||||
<feDisplacementMap ref="dispBlue" in="SourceGraphic" in2="map" id="bluechannel" result="dispBlue" />
|
<feDisplacementMap ref="dispBlue" in="SourceGraphic" in2="map" id="bluechannel" result="dispBlue" />
|
||||||
<feColorMatrix in="dispBlue" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="blue" />
|
<feColorMatrix in="dispBlue" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="blue" />
|
||||||
<feBlend in="red" in2="green" mode="screen" result="rg" />
|
<feBlend in="red" in2="green" mode="screen" result="rg" />
|
||||||
<feBlend in="rg" in2="blue" mode="screen" result="output" />
|
<feBlend in="rg" in2="blue" mode="screen" result="output" />
|
||||||
<feGaussianBlur ref="gaussianBlur" in="output" stdDeviation="0.7" />
|
<feGaussianBlur ref="gaussianBlur" in="output" stdDeviation="0.7" />
|
||||||
@@ -229,7 +219,6 @@ class GlassSurface {
|
|||||||
this.gaussianBlur = this.svgContainer.querySelector('[ref="gaussianBlur"]');
|
this.gaussianBlur = this.svgContainer.querySelector('[ref="gaussianBlur"]');
|
||||||
|
|
||||||
this.updateFilterAttributes();
|
this.updateFilterAttributes();
|
||||||
|
|
||||||
requestAnimationFrame(() => this.updateDisplacementMap());
|
requestAnimationFrame(() => this.updateDisplacementMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,28 +279,20 @@ class GlassSurface {
|
|||||||
updateStyles() {
|
updateStyles() {
|
||||||
if (!this.isMounted) return;
|
if (!this.isMounted) return;
|
||||||
|
|
||||||
this.el.classList.remove('dark-mode', 'light-mode');
|
this.el.classList.toggle('dark-mode', this.isDark);
|
||||||
if (this.isDark) {
|
this.el.classList.toggle('light-mode', !this.isDark);
|
||||||
this.el.classList.add('dark-mode');
|
|
||||||
} else {
|
|
||||||
this.el.classList.add('light-mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.liquidGlassEnabled) {
|
if (this.liquidGlassEnabled && this.svgSupported && this.backdropSupported) {
|
||||||
if (this.svgSupported && this.backdropSupported) {
|
const bgColor = this.isDark
|
||||||
const bgColor = this.isDark
|
? `hsl(0 0% 0% / ${this.config.backgroundOpacity})`
|
||||||
? `hsl(0 0% 0% / ${this.config.backgroundOpacity})`
|
: `hsl(0 0% 100% / ${this.config.backgroundOpacity})`;
|
||||||
: `hsl(0 0% 100% / ${this.config.backgroundOpacity})`;
|
|
||||||
|
|
||||||
const shadow = this.isDark
|
const shadow = this.isDark
|
||||||
? '0 0 0 0.5px rgba(255, 255, 255, 0.1) inset, 0px 4px 16px rgba(0, 0, 0, 0.1), 0px 8px 24px rgba(0, 0, 0, 0.08)'
|
? '0 0 0 0.5px rgba(255, 255, 255, 0.1) inset, 0px 4px 16px rgba(0, 0, 0, 0.1), 0px 8px 24px rgba(0, 0, 0, 0.08)'
|
||||||
: '0 0 0 0.5px rgba(255, 255, 255, 0.15) inset, 0px 4px 16px rgba(0, 0, 0, 0.06), 0px 8px 24px rgba(0, 0, 0, 0.04)';
|
: '0 0 0 0.5px rgba(255, 255, 255, 0.15) inset, 0px 4px 16px rgba(0, 0, 0, 0.06), 0px 8px 24px rgba(0, 0, 0, 0.04)';
|
||||||
|
|
||||||
this.el.style.background = bgColor;
|
this.el.style.background = bgColor;
|
||||||
this.el.style.boxShadow = shadow;
|
this.el.style.boxShadow = shadow;
|
||||||
} else {
|
|
||||||
this.applyFallback();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.applyFallback();
|
this.applyFallback();
|
||||||
}
|
}
|
||||||
@@ -357,8 +338,7 @@ class GlassSurface {
|
|||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svgContent)}`;
|
this.feImage.setAttribute('href', `data:image/svg+xml,${encodeURIComponent(svgContent)}`);
|
||||||
this.feImage.setAttribute('href', dataUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
@@ -386,27 +366,23 @@ class GlassSurface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyGlassSurface(el, config) {
|
export function applyGlassSurface(el, config) {
|
||||||
if (!el || el.dataset.hasGlass) return null;
|
if (!el || el.dataset.hasGlass || typeof GlassSurface !== 'function') return null;
|
||||||
if (typeof GlassSurface !== 'function') return null;
|
|
||||||
el.dataset.hasGlass = "true";
|
el.dataset.hasGlass = "true";
|
||||||
const instance = new GlassSurface(el, config);
|
const instance = new GlassSurface(el, config);
|
||||||
el._glassSurfaceInstance = instance;
|
el._glassSurfaceInstance = instance;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroyGlassSurface(el) {
|
export function destroyGlassSurface(el) {
|
||||||
if (!el || !el._glassSurfaceInstance) return;
|
if (!el || !el._glassSurfaceInstance) return;
|
||||||
el._glassSurfaceInstance.destroy();
|
el._glassSurfaceInstance.destroy();
|
||||||
el._glassSurfaceInstance = null;
|
el._glassSurfaceInstance = null;
|
||||||
delete el.dataset.hasGlass;
|
delete el.dataset.hasGlass;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initLiquidGlass(configOverride = {}) {
|
export function initLiquidGlass(configOverride = {}) {
|
||||||
const config = {
|
const config = {
|
||||||
width: 200,
|
|
||||||
height: 25,
|
|
||||||
borderRadius: 20,
|
|
||||||
borderWidth: 0.07,
|
borderWidth: 0.07,
|
||||||
brightness: 50,
|
brightness: 50,
|
||||||
opacity: 0.93,
|
opacity: 0.93,
|
||||||
@@ -423,20 +399,7 @@ function initLiquidGlass(configOverride = {}) {
|
|||||||
mixBlendMode: 'difference'
|
mixBlendMode: 'difference'
|
||||||
};
|
};
|
||||||
|
|
||||||
const eagerSelectors = [
|
document.querySelectorAll('.hero-cta, .nav-container').forEach(el => applyGlassSurface(el, config));
|
||||||
'.nav-links',
|
|
||||||
'.discord-copy-bar',
|
|
||||||
'.hero-cta-unsure',
|
|
||||||
'#navIslandMain',
|
|
||||||
'#navbarSearchToggle',
|
|
||||||
'.hero-icon-group',
|
|
||||||
'.details-overlay-actions .action-icon-btn',
|
|
||||||
'.cta-button-wrapper .cta-button',
|
|
||||||
'.spotlight-actions .cta-button',
|
|
||||||
'.edit-profile-pill'
|
|
||||||
];
|
|
||||||
|
|
||||||
document.querySelectorAll(eagerSelectors.join(', ')).forEach(el => applyGlassSurface(el, config));
|
|
||||||
|
|
||||||
if (!window._cardPlayBtnGlassObserver) {
|
if (!window._cardPlayBtnGlassObserver) {
|
||||||
window._cardPlayBtnGlassObserver = new IntersectionObserver((entries) => {
|
window._cardPlayBtnGlassObserver = new IntersectionObserver((entries) => {
|
||||||
@@ -469,12 +432,4 @@ function initLiquidGlass(configOverride = {}) {
|
|||||||
document.querySelectorAll('.card-play-btn:not([data-has-glass])').forEach(el => {
|
document.querySelectorAll('.card-play-btn:not([data-has-glass])').forEach(el => {
|
||||||
window._cardPlayBtnGlassObserver.observe(el);
|
window._cardPlayBtnGlassObserver.observe(el);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", () => initLiquidGlass());
|
|
||||||
} else {
|
|
||||||
initLiquidGlass();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { GlassSurface, applyGlassSurface, destroyGlassSurface, initLiquidGlass };
|
|
||||||
75
src/js/modules/releases.js
Normal file
75
src/js/modules/releases.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
async function setupExpandableDownload(expandableId, btnId, optionsId, windowsId, goId, macId) {
|
||||||
|
const expandable = document.getElementById(expandableId);
|
||||||
|
const downloadBtn = document.getElementById(btnId);
|
||||||
|
const downloadOptions = document.getElementById(optionsId);
|
||||||
|
const windowsDownload = document.getElementById(windowsId);
|
||||||
|
const goDownload = document.getElementById(goId);
|
||||||
|
const macDownload = document.getElementById(macId);
|
||||||
|
|
||||||
|
if (!expandable || !downloadBtn || !downloadOptions || !windowsDownload || !goDownload || !macDownload) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [homeResponse, goResponse] = await Promise.all([
|
||||||
|
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"),
|
||||||
|
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases/permalink/latest")
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!homeResponse.ok || !goResponse.ok) throw new Error();
|
||||||
|
|
||||||
|
const homeRelease = await homeResponse.json();
|
||||||
|
const goRelease = await goResponse.json();
|
||||||
|
|
||||||
|
const version = homeRelease.tag_name;
|
||||||
|
const goVersion = goRelease.tag_name;
|
||||||
|
const fileVersion = version.replace(/^v/, "");
|
||||||
|
const goFileVersion = goVersion.replace(/^v/, "");
|
||||||
|
|
||||||
|
const windowsAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".exe"));
|
||||||
|
const macAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".dmg"));
|
||||||
|
const goAsset = goRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".apk"));
|
||||||
|
|
||||||
|
const btnText = downloadBtn.querySelector(".download-btn-text");
|
||||||
|
if (btnText) btnText.textContent = `Download ${version}`;
|
||||||
|
|
||||||
|
windowsDownload.href = windowsAsset?.url || `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
|
||||||
|
windowsDownload.querySelector("span").textContent = `Windows ${version}`;
|
||||||
|
|
||||||
|
goDownload.href = goAsset?.url || `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/packages/generic/vyla-go/${goVersion}/vyla-go-${goFileVersion}.apk`;
|
||||||
|
goDownload.querySelector("span").textContent = `Android ${goVersion}`;
|
||||||
|
|
||||||
|
macDownload.href = macAsset?.url || `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_aarch64.dmg`;
|
||||||
|
macDownload.querySelector("span").textContent = `macOS ${version}`;
|
||||||
|
|
||||||
|
downloadBtn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const isExpanded = expandable.getAttribute("aria-expanded") === "true";
|
||||||
|
expandable.setAttribute("aria-expanded", String(!isExpanded));
|
||||||
|
downloadOptions.setAttribute("aria-hidden", String(isExpanded));
|
||||||
|
downloadOptions.classList.toggle("show", !isExpanded);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
const btnText = downloadBtn.querySelector(".download-btn-text");
|
||||||
|
if (btnText) btnText.textContent = "Download Latest";
|
||||||
|
windowsDownload.querySelector("span").textContent = "Windows (Latest)";
|
||||||
|
goDownload.querySelector("span").textContent = "Android (Latest)";
|
||||||
|
macDownload.querySelector("span").textContent = "macOS (Latest)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initReleases() {
|
||||||
|
setupExpandableDownload("hero-download-expandable", "hero-download-btn", "hero-download-options", "hero-windows-download", "hero-go-download", "hero-mac-download");
|
||||||
|
setupExpandableDownload("cta-download-expandable", "cta-download-btn", "cta-download-options", "cta-windows-download", "cta-go-download", "cta-mac-download");
|
||||||
|
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
const expandables = document.querySelectorAll(".download-expandable");
|
||||||
|
expandables.forEach(expandable => {
|
||||||
|
const btn = expandable.querySelector("button");
|
||||||
|
const options = expandable.querySelector(".download-options");
|
||||||
|
if (btn && options && !expandable.contains(e.target)) {
|
||||||
|
expandable.setAttribute("aria-expanded", "false");
|
||||||
|
options.setAttribute("aria-hidden", "true");
|
||||||
|
options.classList.remove("show");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
47
src/js/modules/stats.js
Normal file
47
src/js/modules/stats.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
function animateValue(el, target, duration = 1400) {
|
||||||
|
const start = 0;
|
||||||
|
const startTime = performance.now();
|
||||||
|
function update(now) {
|
||||||
|
const elapsed = now - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
const ease = 1 - Math.pow(1 - progress, 3);
|
||||||
|
const current = Math.floor(start + (target - start) * ease);
|
||||||
|
el.textContent = current.toLocaleString();
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
} else {
|
||||||
|
el.textContent = target.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initStats() {
|
||||||
|
const statMembers = document.getElementById('stat-members');
|
||||||
|
const statOnline = document.getElementById('stat-online');
|
||||||
|
if (!statMembers || !statOnline) return;
|
||||||
|
|
||||||
|
let memberCount = 1250;
|
||||||
|
let onlineCount = 180;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://discord.com/api/v10/invites/5eqRhKeywG?with_counts=true');
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.approximate_member_count) memberCount = data.approximate_member_count;
|
||||||
|
if (data.approximate_presence_count) onlineCount = data.approximate_presence_count;
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries, obs) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
animateValue(statMembers, memberCount);
|
||||||
|
animateValue(statOnline, onlineCount);
|
||||||
|
obs.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.3 });
|
||||||
|
|
||||||
|
observer.observe(statMembers.closest('.stats-grid') || statMembers);
|
||||||
|
}
|
||||||
36
src/js/modules/testimonials.js
Normal file
36
src/js/modules/testimonials.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export async function initTestimonials() {
|
||||||
|
const row1 = document.getElementById('marquee-row-1');
|
||||||
|
const row2 = document.getElementById('marquee-row-2');
|
||||||
|
if (!row1 || !row2) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('src/data/reviews.json');
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
|
const reviews = await res.json();
|
||||||
|
|
||||||
|
const makeCard = (r) => `
|
||||||
|
<article class="testimonial-card">
|
||||||
|
<div class="testimonial-user">
|
||||||
|
<img class="testimonial-avatar" src="${r.avatarSmall || r.avatar}" alt="${r.name}" width="44" height="44" loading="lazy" decoding="async">
|
||||||
|
<div class="testimonial-info">
|
||||||
|
<span class="testimonial-name">${r.name}</span>
|
||||||
|
<span class="testimonial-handle">${r.handle}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="testimonial-text">${r.text}</p>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const half = Math.ceil(reviews.length / 2);
|
||||||
|
const set1 = reviews.slice(0, half);
|
||||||
|
const set2 = reviews.slice(half);
|
||||||
|
|
||||||
|
const buildLoop = (items) => {
|
||||||
|
const batch = items.map(makeCard).join('');
|
||||||
|
return batch.repeat(4);
|
||||||
|
};
|
||||||
|
|
||||||
|
row1.innerHTML = buildLoop(set1.length ? set1 : reviews);
|
||||||
|
row2.innerHTML = buildLoop(set2.length ? set2 : reviews);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
30
src/js/modules/typewriter.js
Normal file
30
src/js/modules/typewriter.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export function initTypewriter() {
|
||||||
|
const heroTitle = document.getElementById('hero-title');
|
||||||
|
if (!heroTitle) return;
|
||||||
|
|
||||||
|
const originalText = heroTitle.textContent;
|
||||||
|
heroTitle.textContent = '';
|
||||||
|
|
||||||
|
let charIndex = 0;
|
||||||
|
const typingSpeed = 50;
|
||||||
|
|
||||||
|
function typeChar() {
|
||||||
|
if (charIndex < originalText.length) {
|
||||||
|
const char = originalText.charAt(charIndex);
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = char;
|
||||||
|
|
||||||
|
if (char === ' ') {
|
||||||
|
span.className = 'blur-char is-space';
|
||||||
|
} else {
|
||||||
|
span.className = 'blur-char';
|
||||||
|
}
|
||||||
|
|
||||||
|
heroTitle.appendChild(span);
|
||||||
|
charIndex++;
|
||||||
|
setTimeout(typeChar, typingSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(typeChar, 300);
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export function animateCount(el, target, duration = 1800) {
|
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
function tick(now) {
|
|
||||||
const progress = Math.min((now - start) / duration, 1);
|
|
||||||
const eased = 1 - Math.pow(1 - progress, 3);
|
|
||||||
const current = Math.floor(eased * target);
|
|
||||||
el.textContent = `${current.toLocaleString()}+`;
|
|
||||||
if (progress < 1) {
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
} else {
|
|
||||||
el.textContent = `${target.toLocaleString()}+`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function animateCountOnVisible(el, target, threshold = 0.5) {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
animateCount(el, target);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold }
|
|
||||||
);
|
|
||||||
observer.observe(el);
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
import { prefersReducedMotion } from '../utils/dom.js';
|
|
||||||
import { smoothstep, diffuse, project, advect } from './fluid-solver.js';
|
|
||||||
|
|
||||||
const CELL_SIZE = 28;
|
|
||||||
const FONT_SIZE = 11;
|
|
||||||
const DT = 1 / 30;
|
|
||||||
const FADE_RATE = 0.6;
|
|
||||||
const MAX_ALPHA = 0.85;
|
|
||||||
const ADVECT_SCALE = 0.5;
|
|
||||||
const SPLASH_RADIUS_PX = 28;
|
|
||||||
const SPLASH_FORCE = 0.5;
|
|
||||||
const DIFFUSION_ITERATIONS = 2;
|
|
||||||
const PROJECT_ITERATIONS = 3;
|
|
||||||
const VELOCITY_DAMP = 0.1;
|
|
||||||
const DYE_DAMP = 0.1;
|
|
||||||
const TRAIL_LIFETIME_MS = 2000;
|
|
||||||
const TRAIL_RADIUS_PX = 100;
|
|
||||||
const TRAIL_EASE = 2.5;
|
|
||||||
const TRAIL_STRENGTH = 3;
|
|
||||||
const TRAIL_FORCE_DECAY = 1.2;
|
|
||||||
const TRAIL_WOBBLE = 0.14;
|
|
||||||
|
|
||||||
export function initFluidByteField(canvasId) {
|
|
||||||
const canvas = document.getElementById(canvasId);
|
|
||||||
if (!canvas || prefersReducedMotion()) return;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
const container = canvas.parentElement ?? canvas;
|
|
||||||
const video = container.querySelector('.hero-atmosphere-video');
|
|
||||||
if (video) {
|
|
||||||
video.crossOrigin = 'anonymous';
|
|
||||||
}
|
|
||||||
|
|
||||||
const procCanvas = document.createElement('canvas');
|
|
||||||
const procCtx = procCanvas.getContext('2d', { willReadFrequently: true });
|
|
||||||
|
|
||||||
let cols = 0;
|
|
||||||
let rows = 0;
|
|
||||||
let width = 0;
|
|
||||||
let height = 0;
|
|
||||||
let velX = new Float32Array(0);
|
|
||||||
let velY = new Float32Array(0);
|
|
||||||
let dye = new Float32Array(0);
|
|
||||||
let velX0 = new Float32Array(0);
|
|
||||||
let velY0 = new Float32Array(0);
|
|
||||||
let dye0 = new Float32Array(0);
|
|
||||||
let glyphs = new Uint8Array(0);
|
|
||||||
|
|
||||||
let lastPointerX = -9999;
|
|
||||||
let lastPointerY = -9999;
|
|
||||||
let lastPointerTime = 0;
|
|
||||||
let rafId = 0;
|
|
||||||
let lastFrameTime = 0;
|
|
||||||
let accumulator = 0;
|
|
||||||
let running = true;
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
const rect = container.getBoundingClientRect();
|
|
||||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
||||||
width = Math.max(1, Math.round(rect.width));
|
|
||||||
height = Math.max(1, Math.round(rect.height));
|
|
||||||
cols = Math.max(3, Math.ceil(width / CELL_SIZE));
|
|
||||||
rows = Math.max(3, Math.ceil(height / CELL_SIZE));
|
|
||||||
canvas.width = Math.round(width * dpr);
|
|
||||||
canvas.height = Math.round(height * dpr);
|
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
||||||
|
|
||||||
procCanvas.width = cols;
|
|
||||||
procCanvas.height = rows;
|
|
||||||
|
|
||||||
const size = cols * rows;
|
|
||||||
velX = new Float32Array(size);
|
|
||||||
velY = new Float32Array(size);
|
|
||||||
dye = new Float32Array(size);
|
|
||||||
velX0 = new Float32Array(size);
|
|
||||||
velY0 = new Float32Array(size);
|
|
||||||
dye0 = new Float32Array(size);
|
|
||||||
glyphs = new Uint8Array(size);
|
|
||||||
for (let i = 0; i < size; i++) glyphs[i] = Math.floor(Math.random() * 256);
|
|
||||||
}
|
|
||||||
|
|
||||||
function splash(cellX, cellY, forceX, forceY, dyeAmount, radiusCells) {
|
|
||||||
const rSq = radiusCells * radiusCells;
|
|
||||||
const xMin = Math.max(1, Math.floor(cellX - radiusCells));
|
|
||||||
const xMax = Math.min(cols - 2, Math.ceil(cellX + radiusCells));
|
|
||||||
const yMin = Math.max(1, Math.floor(cellY - radiusCells));
|
|
||||||
const yMax = Math.min(rows - 2, Math.ceil(cellY + radiusCells));
|
|
||||||
for (let y = yMin; y <= yMax; y++) {
|
|
||||||
for (let x = xMin; x <= xMax; x++) {
|
|
||||||
const dx = x - cellX;
|
|
||||||
const dy = y - cellY;
|
|
||||||
const distSq = dx * dx + dy * dy;
|
|
||||||
if (distSq > rSq) continue;
|
|
||||||
const falloff = smoothstep(1 - Math.sqrt(distSq) / radiusCells);
|
|
||||||
const idx = x + y * cols;
|
|
||||||
velX[idx] += forceX * falloff;
|
|
||||||
velY[idx] += forceY * falloff;
|
|
||||||
dye[idx] = Math.min(2.0, dye[idx] + dyeAmount * falloff);
|
|
||||||
if (Math.random() < 0.04 * falloff) glyphs[idx] = Math.floor(Math.random() * 256);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerMove(e) {
|
|
||||||
const rect = container.getBoundingClientRect();
|
|
||||||
let px, py;
|
|
||||||
|
|
||||||
if (e.touches && e.touches.length > 0) {
|
|
||||||
px = e.touches[0].clientX - rect.left;
|
|
||||||
py = e.touches[0].clientY - rect.top;
|
|
||||||
} else {
|
|
||||||
px = e.clientX - rect.left;
|
|
||||||
py = e.clientY - rect.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cols > 0 && lastPointerX > -9000 && px >= 0 && py >= 0 && px <= rect.width && py <= rect.height) {
|
|
||||||
const elapsed = Math.min(0.05, Math.max(0.001, (e.timeStamp - lastPointerTime) / 1000));
|
|
||||||
const fx = (px - lastPointerX) / CELL_SIZE / elapsed * ADVECT_SCALE;
|
|
||||||
const fy = (py - lastPointerY) / CELL_SIZE / elapsed * ADVECT_SCALE;
|
|
||||||
splash(px / CELL_SIZE, py / CELL_SIZE, fx, fy, SPLASH_FORCE, SPLASH_RADIUS_PX / CELL_SIZE);
|
|
||||||
}
|
|
||||||
lastPointerX = px;
|
|
||||||
lastPointerY = py;
|
|
||||||
lastPointerTime = e.timeStamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerLeave() {
|
|
||||||
lastPointerX = -9999;
|
|
||||||
lastPointerY = -9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
function step() {
|
|
||||||
const visc = 1.5 * DT;
|
|
||||||
velX0.set(velX);
|
|
||||||
diffuse(velX, velX0, visc, cols, rows, DT, DIFFUSION_ITERATIONS);
|
|
||||||
velY0.set(velY);
|
|
||||||
diffuse(velY, velY0, visc, cols, rows, DT, DIFFUSION_ITERATIONS);
|
|
||||||
project(velX, velY, velX0, velY0, cols, rows, PROJECT_ITERATIONS);
|
|
||||||
velX0.set(velX);
|
|
||||||
velY0.set(velY);
|
|
||||||
const velDamp = 1 / (1 + DT * VELOCITY_DAMP);
|
|
||||||
advect(velX, velX0, velX0, velY0, velDamp, cols, rows, DT);
|
|
||||||
advect(velY, velY0, velX0, velY0, velDamp, cols, rows, DT);
|
|
||||||
project(velX, velY, velX0, velY0, cols, rows, PROJECT_ITERATIONS);
|
|
||||||
dye0.set(dye);
|
|
||||||
diffuse(dye, dye0, visc * DT, cols, rows, DT, DIFFUSION_ITERATIONS);
|
|
||||||
dye0.set(dye);
|
|
||||||
advect(dye, dye0, velX, velY, 1 / (1 + DT * DYE_DAMP), cols, rows, DT);
|
|
||||||
|
|
||||||
const size = cols * rows;
|
|
||||||
for (let i = 0; i < size; i++) {
|
|
||||||
if (Math.random() < 0.0005) {
|
|
||||||
glyphs[i] = Math.floor(Math.random() * 256);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
ctx.font = `700 ${FONT_SIZE}px ui-monospace, SFMono-Regular, Consolas, monospace`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
|
|
||||||
let videoData = null;
|
|
||||||
if (video && video.readyState >= 2) {
|
|
||||||
try {
|
|
||||||
procCtx.drawImage(video, 0, 0, cols, rows);
|
|
||||||
videoData = procCtx.getImageData(0, 0, cols, rows).data;
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
const half = CELL_SIZE / 2;
|
|
||||||
for (let y = 0; y < rows; y++) {
|
|
||||||
for (let x = 0; x < cols; x++) {
|
|
||||||
const idx = x + y * cols;
|
|
||||||
const d = dye[idx];
|
|
||||||
const hex = glyphs[idx].toString(16).toUpperCase().padStart(2, '0');
|
|
||||||
|
|
||||||
let videoLuminance = 0;
|
|
||||||
if (videoData) {
|
|
||||||
const r = videoData[idx * 4];
|
|
||||||
const g = videoData[idx * 4 + 1];
|
|
||||||
const b = videoData[idx * 4 + 2];
|
|
||||||
videoLuminance = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseAlpha = 0;
|
|
||||||
if (d <= 0.02) {
|
|
||||||
ctx.shadowBlur = 0;
|
|
||||||
ctx.globalAlpha = baseAlpha;
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255)';
|
|
||||||
ctx.fillText(hex, x * CELL_SIZE + half, y * CELL_SIZE + half);
|
|
||||||
} else {
|
|
||||||
const alpha = d * 1.5;
|
|
||||||
const activeAlpha = Math.min(alpha + baseAlpha, 1.0);
|
|
||||||
ctx.globalAlpha = activeAlpha;
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.shadowBlur = 0;
|
|
||||||
ctx.fillText(hex, x * CELL_SIZE + half, y * CELL_SIZE + half);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
ctx.shadowBlur = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function frame(now) {
|
|
||||||
if (!running) return;
|
|
||||||
rafId = requestAnimationFrame(frame);
|
|
||||||
const delta = lastFrameTime ? (now - lastFrameTime) / 1000 : DT;
|
|
||||||
lastFrameTime = now;
|
|
||||||
accumulator += Math.min(delta, 0.1);
|
|
||||||
let steps = 0;
|
|
||||||
while (accumulator >= DT && steps < 3) {
|
|
||||||
step();
|
|
||||||
accumulator -= DT;
|
|
||||||
steps++;
|
|
||||||
}
|
|
||||||
if (steps === 0) return;
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onVisibilityChange() {
|
|
||||||
running = !document.hidden;
|
|
||||||
if (running) {
|
|
||||||
lastFrameTime = 0;
|
|
||||||
accumulator = 0;
|
|
||||||
cancelAnimationFrame(rafId);
|
|
||||||
rafId = requestAnimationFrame(frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new ResizeObserver(resize).observe(container);
|
|
||||||
window.addEventListener('pointermove', onPointerMove, { passive: true });
|
|
||||||
window.addEventListener('pointerleave', onPointerLeave);
|
|
||||||
window.addEventListener('touchmove', onPointerMove, { passive: true });
|
|
||||||
window.addEventListener('touchend', onPointerLeave);
|
|
||||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
||||||
|
|
||||||
resize();
|
|
||||||
rafId = requestAnimationFrame(frame);
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
export function smoothstep(v) {
|
|
||||||
return v <= 0 ? 0 : v >= 1 ? 1 : v * v * (3 - 2 * v);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setBoundary(field, cols, rows) {
|
|
||||||
for (let x = 0; x < cols; x++) {
|
|
||||||
field[x] = field[x + cols];
|
|
||||||
field[x + (rows - 1) * cols] = field[x + (rows - 2) * cols];
|
|
||||||
}
|
|
||||||
for (let y = 0; y < rows; y++) {
|
|
||||||
field[y * cols] = field[y * cols + 1];
|
|
||||||
field[y * cols + cols - 1] = field[y * cols + cols - 2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function diffuse(field, fieldPrev, diffusion, cols, rows, dt, iterations) {
|
|
||||||
const a = diffusion * dt;
|
|
||||||
const denom = 1 / (1 + 4 * a);
|
|
||||||
for (let iter = 0; iter < iterations; iter++) {
|
|
||||||
for (let y = 1; y < rows - 1; y++) {
|
|
||||||
for (let x = 1; x < cols - 1; x++) {
|
|
||||||
const idx = x + y * cols;
|
|
||||||
field[idx] = (fieldPrev[idx] + a * (field[idx - 1] + field[idx + 1] + field[idx - cols] + field[idx + cols])) * denom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBoundary(field, cols, rows);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function project(vx, vy, p, div, cols, rows, iterations) {
|
|
||||||
for (let y = 1; y < rows - 1; y++) {
|
|
||||||
for (let x = 1; x < cols - 1; x++) {
|
|
||||||
const idx = x + y * cols;
|
|
||||||
div[idx] = -0.5 * (vx[idx + 1] - vx[idx - 1] + vy[idx + cols] - vy[idx - cols]);
|
|
||||||
p[idx] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBoundary(div, cols, rows);
|
|
||||||
setBoundary(p, cols, rows);
|
|
||||||
for (let iter = 0; iter < iterations; iter++) {
|
|
||||||
for (let y = 1; y < rows - 1; y++) {
|
|
||||||
for (let x = 1; x < cols - 1; x++) {
|
|
||||||
const idx = x + y * cols;
|
|
||||||
p[idx] = (div[idx] + p[idx - 1] + p[idx + 1] + p[idx - cols] + p[idx + cols]) * 0.25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBoundary(p, cols, rows);
|
|
||||||
}
|
|
||||||
for (let y = 1; y < rows - 1; y++) {
|
|
||||||
for (let x = 1; x < cols - 1; x++) {
|
|
||||||
const idx = x + y * cols;
|
|
||||||
vx[idx] -= 0.5 * (p[idx + 1] - p[idx - 1]);
|
|
||||||
vy[idx] -= 0.5 * (p[idx + cols] - p[idx - cols]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBoundary(vx, cols, rows);
|
|
||||||
setBoundary(vy, cols, rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function advect(field, fieldPrev, vx, vy, damp, cols, rows, dt) {
|
|
||||||
for (let y = 1; y < rows - 1; y++) {
|
|
||||||
for (let x = 1; x < cols - 1; x++) {
|
|
||||||
const idx = x + y * cols;
|
|
||||||
let px = x - dt * vx[idx];
|
|
||||||
let py = y - dt * vy[idx];
|
|
||||||
px = px < 0.5 ? 0.5 : px > cols - 1.5 ? cols - 1.5 : px;
|
|
||||||
py = py < 0.5 ? 0.5 : py > rows - 1.5 ? rows - 1.5 : py;
|
|
||||||
const x0 = px | 0;
|
|
||||||
const x1 = x0 + 1;
|
|
||||||
const y0 = py | 0;
|
|
||||||
const y1 = y0 + 1;
|
|
||||||
const sx1 = px - x0;
|
|
||||||
const sx0 = 1 - sx1;
|
|
||||||
const sy1 = py - y0;
|
|
||||||
const sy0 = 1 - sy1;
|
|
||||||
field[idx] = (sx0 * (sy0 * fieldPrev[x0 + y0 * cols] + sy1 * fieldPrev[x0 + y1 * cols])
|
|
||||||
+ sx1 * (sy0 * fieldPrev[x1 + y0 * cols] + sy1 * fieldPrev[x1 + y1 * cols])) * damp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBoundary(field, cols, rows);
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
import { Renderer, Program, Triangle, Mesh } from 'https://esm.sh/ogl@1.0.11';
|
|
||||||
|
|
||||||
const hexToRgb = hex => {
|
|
||||||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
||||||
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnchorAndDir = (origin, w, h) => {
|
|
||||||
const outside = 0.05;
|
|
||||||
switch (origin) {
|
|
||||||
case 'top-left':
|
|
||||||
return { anchor: [0, -outside * h], dir: [0.707, 0.707] };
|
|
||||||
case 'top-right':
|
|
||||||
return { anchor: [w, -outside * h], dir: [-0.707, 0.707] };
|
|
||||||
case 'left':
|
|
||||||
return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };
|
|
||||||
case 'right':
|
|
||||||
return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };
|
|
||||||
case 'bottom-left':
|
|
||||||
return { anchor: [0, (1 + outside) * h], dir: [0.707, -0.707] };
|
|
||||||
case 'bottom-center':
|
|
||||||
return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };
|
|
||||||
case 'bottom-right':
|
|
||||||
return { anchor: [w, (1 + outside) * h], dir: [-0.707, -0.707] };
|
|
||||||
default:
|
|
||||||
return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function initLightRays(container, options = {}) {
|
|
||||||
if (!container) return () => { };
|
|
||||||
|
|
||||||
const raysOrigin = options.raysOrigin ?? 'bottom-center';
|
|
||||||
const rayColor1 = options.rayColor1 ?? '#EAB308';
|
|
||||||
const rayColor2 = options.rayColor2 ?? '#96c8ff';
|
|
||||||
const blend = options.blend ?? 0.65;
|
|
||||||
const raysSpeed = options.raysSpeed ?? 1.8;
|
|
||||||
const lightSpread = options.lightSpread ?? 0.7;
|
|
||||||
const isMobile = window.innerWidth < 768;
|
|
||||||
const baseRayLength = options.rayLength ?? 0.6;
|
|
||||||
const rayLength = isMobile ? baseRayLength * 3.0 : baseRayLength * 1.8;
|
|
||||||
const pulsating = Boolean(options.pulsating);
|
|
||||||
const baseFadeDistance = options.fadeDistance ?? 0.4;
|
|
||||||
const fadeDistance = isMobile ? baseFadeDistance * 3.0 : baseFadeDistance * 1.8;
|
|
||||||
const baseGlowFalloffFactor = 4.9;
|
|
||||||
const glowFalloffFactor = isMobile ? baseGlowFalloffFactor / 3.0 : baseGlowFalloffFactor / 1.8;
|
|
||||||
const saturation = options.saturation ?? 1.4;
|
|
||||||
const followMouse = options.followMouse !== false;
|
|
||||||
const mouseInfluence = options.mouseInfluence ?? 0.12;
|
|
||||||
const noiseAmount = options.noiseAmount ?? 0.08;
|
|
||||||
const distortion = options.distortion ?? 0.04;
|
|
||||||
|
|
||||||
const mouse = { x: 0.5, y: 0.5 };
|
|
||||||
const smoothMouse = { x: 0.5, y: 0.5 };
|
|
||||||
let animationId = null;
|
|
||||||
|
|
||||||
const renderer = new Renderer({
|
|
||||||
dpr: Math.min(window.devicePixelRatio, 2),
|
|
||||||
alpha: true
|
|
||||||
});
|
|
||||||
const gl = renderer.gl;
|
|
||||||
Object.assign(gl.canvas.style, {
|
|
||||||
position: 'absolute',
|
|
||||||
inset: '0',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
display: 'block',
|
|
||||||
pointerEvents: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
while (container.firstChild) {
|
|
||||||
container.removeChild(container.firstChild);
|
|
||||||
}
|
|
||||||
container.appendChild(gl.canvas);
|
|
||||||
|
|
||||||
const vert = `
|
|
||||||
attribute vec2 position;
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(position, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const frag = `
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
uniform float iTime;
|
|
||||||
uniform vec2 iResolution;
|
|
||||||
|
|
||||||
uniform vec2 rayPos;
|
|
||||||
uniform vec2 rayDir;
|
|
||||||
uniform vec3 rayColor1;
|
|
||||||
uniform vec3 rayColor2;
|
|
||||||
uniform float blend;
|
|
||||||
uniform float raysSpeed;
|
|
||||||
uniform float lightSpread;
|
|
||||||
uniform float rayLength;
|
|
||||||
uniform float pulsating;
|
|
||||||
uniform float fadeDistance;
|
|
||||||
uniform float saturation;
|
|
||||||
uniform vec2 mousePos;
|
|
||||||
uniform float mouseInfluence;
|
|
||||||
uniform float noiseAmount;
|
|
||||||
uniform float distortion;
|
|
||||||
uniform float glowFalloffFactor;
|
|
||||||
|
|
||||||
float noise(vec2 st) {
|
|
||||||
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
|
|
||||||
}
|
|
||||||
|
|
||||||
float rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord, float seedA, float seedB, float speed) {
|
|
||||||
vec2 sourceToCoord = coord - raySource;
|
|
||||||
vec2 dirNorm = normalize(sourceToCoord);
|
|
||||||
float cosAngle = dot(dirNorm, rayRefDirection);
|
|
||||||
|
|
||||||
float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.005) * 0.2;
|
|
||||||
float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));
|
|
||||||
|
|
||||||
float maxDim = max(iResolution.x, iResolution.y);
|
|
||||||
float distance = length(sourceToCoord);
|
|
||||||
float maxDistance = maxDim * rayLength;
|
|
||||||
float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
|
|
||||||
|
|
||||||
float fadeRadius = maxDim * fadeDistance;
|
|
||||||
float fadeFalloff = clamp((fadeRadius - distance) / fadeRadius, 0.0, 1.0);
|
|
||||||
float pulse = pulsating > 0.5 ? (0.85 + 0.15 * sin(iTime * speed * 3.0)) : 1.0;
|
|
||||||
|
|
||||||
float baseStrength = clamp(
|
|
||||||
(0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +
|
|
||||||
(0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),
|
|
||||||
0.0, 1.0
|
|
||||||
);
|
|
||||||
|
|
||||||
return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 coord = vec2(gl_FragCoord.x, iResolution.y - gl_FragCoord.y);
|
|
||||||
|
|
||||||
vec2 finalRayDir = rayDir;
|
|
||||||
if (mouseInfluence > 0.0) {
|
|
||||||
vec2 mouseScreenPos = mousePos * iResolution.xy;
|
|
||||||
vec2 mouseDirection = normalize(mouseScreenPos - rayPos);
|
|
||||||
finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));
|
|
||||||
}
|
|
||||||
|
|
||||||
vec4 rays1 = vec4(rayColor1, 1.0) * rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349, 1.5 * raysSpeed);
|
|
||||||
vec4 rays2 = vec4(rayColor2, 1.0) * rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234, 1.1 * raysSpeed);
|
|
||||||
|
|
||||||
vec4 color = rays1 * (1.0 - blend) * 1.2 + rays2 * blend * 1.2;
|
|
||||||
|
|
||||||
if (noiseAmount > 0.0) {
|
|
||||||
float n = noise(coord * 0.01 + iTime * 0.1);
|
|
||||||
color.rgb *= (1.0 - noiseAmount + noiseAmount * n);
|
|
||||||
}
|
|
||||||
|
|
||||||
float maxDim = max(iResolution.x, iResolution.y);
|
|
||||||
float distFromSource = length(coord - rayPos) / maxDim;
|
|
||||||
float glowFalloff = clamp(1.01 - distFromSource * glowFalloffFactor, 0.01, 1.6);
|
|
||||||
color.rgb *= glowFalloff;
|
|
||||||
|
|
||||||
if (saturation != 1.0) {
|
|
||||||
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
|
|
||||||
color.rgb = mix(vec3(gray), color.rgb, saturation);
|
|
||||||
}
|
|
||||||
|
|
||||||
color.a = clamp(max(color.r, max(color.g, color.b)) * 1.4, 0.0, 1.0);
|
|
||||||
gl_FragColor = color;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const uniforms = {
|
|
||||||
iTime: { value: 0 },
|
|
||||||
iResolution: { value: [1, 1] },
|
|
||||||
rayPos: { value: [0, 0] },
|
|
||||||
rayDir: { value: [0, -1] },
|
|
||||||
rayColor1: { value: hexToRgb(rayColor1) },
|
|
||||||
rayColor2: { value: hexToRgb(rayColor2) },
|
|
||||||
blend: { value: blend },
|
|
||||||
raysSpeed: { value: raysSpeed },
|
|
||||||
lightSpread: { value: lightSpread },
|
|
||||||
rayLength: { value: rayLength },
|
|
||||||
pulsating: { value: pulsating ? 1.0 : 0.0 },
|
|
||||||
fadeDistance: { value: fadeDistance },
|
|
||||||
saturation: { value: saturation },
|
|
||||||
mousePos: { value: [0.5, 0.5] },
|
|
||||||
mouseInfluence: { value: mouseInfluence },
|
|
||||||
noiseAmount: { value: noiseAmount },
|
|
||||||
distortion: { value: distortion },
|
|
||||||
glowFalloffFactor: { value: glowFalloffFactor }
|
|
||||||
};
|
|
||||||
|
|
||||||
const geometry = new Triangle(gl);
|
|
||||||
const program = new Program(gl, { vertex: vert, fragment: frag, uniforms });
|
|
||||||
const mesh = new Mesh(gl, { geometry, program });
|
|
||||||
|
|
||||||
const updatePlacement = () => {
|
|
||||||
if (!container || !renderer) return;
|
|
||||||
renderer.dpr = Math.min(window.devicePixelRatio, 2);
|
|
||||||
const wCSS = container.clientWidth || window.innerWidth || 1;
|
|
||||||
const hCSS = container.clientHeight || window.innerHeight || 1;
|
|
||||||
renderer.setSize(wCSS, hCSS);
|
|
||||||
|
|
||||||
const w = wCSS * renderer.dpr;
|
|
||||||
const h = hCSS * renderer.dpr;
|
|
||||||
uniforms.iResolution.value = [w, h];
|
|
||||||
|
|
||||||
const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h);
|
|
||||||
uniforms.rayPos.value = anchor;
|
|
||||||
uniforms.rayDir.value = dir;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseMove = e => {
|
|
||||||
const rect = container.getBoundingClientRect();
|
|
||||||
mouse.x = (e.clientX - rect.left) / (rect.width || 1);
|
|
||||||
mouse.y = (e.clientY - rect.top) / (rect.height || 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (followMouse) {
|
|
||||||
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const loop = t => {
|
|
||||||
uniforms.iTime.value = t * 0.001;
|
|
||||||
|
|
||||||
if (followMouse && mouseInfluence > 0.0) {
|
|
||||||
smoothMouse.x += (mouse.x - smoothMouse.x) * 0.08;
|
|
||||||
smoothMouse.y += (mouse.y - smoothMouse.y) * 0.08;
|
|
||||||
uniforms.mousePos.value = [smoothMouse.x, smoothMouse.y];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
renderer.render({ scene: mesh });
|
|
||||||
animationId = requestAnimationFrame(loop);
|
|
||||||
} catch (e) { }
|
|
||||||
};
|
|
||||||
|
|
||||||
const ro = new ResizeObserver(updatePlacement);
|
|
||||||
ro.observe(container);
|
|
||||||
window.addEventListener('resize', updatePlacement);
|
|
||||||
updatePlacement();
|
|
||||||
animationId = requestAnimationFrame(loop);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (animationId) cancelAnimationFrame(animationId);
|
|
||||||
ro.disconnect();
|
|
||||||
window.removeEventListener('resize', updatePlacement);
|
|
||||||
if (followMouse) window.removeEventListener('mousemove', handleMouseMove);
|
|
||||||
if (renderer && renderer.gl && renderer.gl.canvas && renderer.gl.canvas.parentNode) {
|
|
||||||
renderer.gl.canvas.parentNode.removeChild(renderer.gl.canvas);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
export function createMarqueeLoop(track, cssVar, { baseSpeed, boostedSpeed, minSetsFor }) {
|
|
||||||
let speed = baseSpeed;
|
|
||||||
let targetSpeed = baseSpeed;
|
|
||||||
let x = 0;
|
|
||||||
let halfWidth = track.scrollWidth / 2;
|
|
||||||
let scrollTimeout = null;
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
halfWidth = track.scrollWidth / 2;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener(
|
|
||||||
'scroll',
|
|
||||||
() => {
|
|
||||||
targetSpeed = boostedSpeed;
|
|
||||||
clearTimeout(scrollTimeout);
|
|
||||||
scrollTimeout = setTimeout(() => {
|
|
||||||
targetSpeed = baseSpeed;
|
|
||||||
}, 250);
|
|
||||||
},
|
|
||||||
{ passive: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
speed += (targetSpeed - speed) * 0.04;
|
|
||||||
x -= speed;
|
|
||||||
if (Math.abs(x) >= halfWidth) x += halfWidth;
|
|
||||||
track.style.setProperty(cssVar, `${x}px`);
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function populateLoopedTrack(track, items, buildItem, itemWidthPx) {
|
|
||||||
track.innerHTML = '';
|
|
||||||
const minSets = Math.max(2, Math.ceil((window.innerWidth * 1.5) / (items.length * itemWidthPx)));
|
|
||||||
for (let i = 0; i < minSets; i++) {
|
|
||||||
items.forEach((item) => track.appendChild(buildItem(item)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
import { Renderer, Triangle, Program, Mesh } from 'https://esm.sh/ogl@1.0.11';
|
|
||||||
|
|
||||||
export function initPrism(container, options = {}) {
|
|
||||||
if (!container) return () => { };
|
|
||||||
|
|
||||||
const stepMultiplier = options.stepMultiplier ?? 1.0;
|
|
||||||
|
|
||||||
const H = Math.max(0.001, options.height ?? 3.5);
|
|
||||||
const BW = Math.max(0.001, options.baseWidth ?? 5.5);
|
|
||||||
const BASE_HALF = BW * 0.5;
|
|
||||||
const GLOW = Math.max(0.0, options.glow ?? 1);
|
|
||||||
const NOISE = Math.max(0.0, options.noise ?? 0.5);
|
|
||||||
const offX = options.offset?.x ?? 0;
|
|
||||||
const offY = options.offset?.y ?? 0;
|
|
||||||
const SAT = options.transparent !== false ? 1.5 : 1;
|
|
||||||
const SCALE = Math.max(0.001, options.scale ?? 3.6);
|
|
||||||
const HUE = options.hueShift || 0;
|
|
||||||
const CFREQ = Math.max(0.0, options.colorFrequency ?? 1);
|
|
||||||
const BLOOM = Math.max(0.0, options.bloom ?? 1);
|
|
||||||
const RSX = 1;
|
|
||||||
const RSY = 1;
|
|
||||||
const RSZ = 1;
|
|
||||||
const TS = Math.max(0, options.timeScale ?? 0.5);
|
|
||||||
const HOVSTR = Math.max(0, options.hoverStrength ?? 2);
|
|
||||||
const INERT = Math.max(0, Math.min(1, options.inertia ?? 0.05));
|
|
||||||
const animationType = options.animationType || 'rotate';
|
|
||||||
const transparent = options.transparent !== false;
|
|
||||||
const suspendWhenOffscreen = Boolean(options.suspendWhenOffscreen);
|
|
||||||
|
|
||||||
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
||||||
const renderer = new Renderer({
|
|
||||||
dpr,
|
|
||||||
alpha: transparent,
|
|
||||||
antialias: false
|
|
||||||
});
|
|
||||||
const gl = renderer.gl;
|
|
||||||
gl.disable(gl.DEPTH_TEST);
|
|
||||||
gl.disable(gl.CULL_FACE);
|
|
||||||
gl.disable(gl.BLEND);
|
|
||||||
|
|
||||||
Object.assign(gl.canvas.style, {
|
|
||||||
position: 'absolute',
|
|
||||||
inset: '0',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
display: 'block'
|
|
||||||
});
|
|
||||||
container.appendChild(gl.canvas);
|
|
||||||
|
|
||||||
const vertex = `
|
|
||||||
attribute vec2 position;
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(position, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const fragment = `
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
uniform vec2 iResolution;
|
|
||||||
uniform float iTime;
|
|
||||||
|
|
||||||
uniform float uHeight;
|
|
||||||
uniform float uBaseHalf;
|
|
||||||
uniform mat3 uRot;
|
|
||||||
uniform int uUseBaseWobble;
|
|
||||||
uniform float uGlow;
|
|
||||||
uniform vec2 uOffsetPx;
|
|
||||||
uniform float uNoise;
|
|
||||||
uniform float uSaturation;
|
|
||||||
uniform float uScale;
|
|
||||||
uniform float uHueShift;
|
|
||||||
uniform float uColorFreq;
|
|
||||||
uniform float uBloom;
|
|
||||||
uniform float uCenterShift;
|
|
||||||
uniform float uInvBaseHalf;
|
|
||||||
uniform float uInvHeight;
|
|
||||||
uniform float uMinAxis;
|
|
||||||
uniform float uPxScale;
|
|
||||||
uniform float uTimeScale;
|
|
||||||
uniform float uStepMultiplier;
|
|
||||||
|
|
||||||
vec4 tanh4(vec4 x){
|
|
||||||
vec4 e2x = exp(2.0*x);
|
|
||||||
return (e2x - 1.0) / (e2x + 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
float rand(vec2 co){
|
|
||||||
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453123);
|
|
||||||
}
|
|
||||||
|
|
||||||
float sdOctaAnisoInv(vec3 p){
|
|
||||||
vec3 q = vec3(abs(p.x) * uInvBaseHalf, abs(p.y) * uInvHeight, abs(p.z) * uInvBaseHalf);
|
|
||||||
float m = q.x + q.y + q.z - 1.0;
|
|
||||||
return m * uMinAxis * 0.5773502691896258;
|
|
||||||
}
|
|
||||||
|
|
||||||
float sdPyramidUpInv(vec3 p){
|
|
||||||
float oct = sdOctaAnisoInv(p);
|
|
||||||
float halfSpace = -p.y;
|
|
||||||
return max(oct, halfSpace);
|
|
||||||
}
|
|
||||||
|
|
||||||
mat3 hueRotation(float a){
|
|
||||||
float c = cos(a), s = sin(a);
|
|
||||||
mat3 W = mat3(
|
|
||||||
0.299, 0.587, 0.114,
|
|
||||||
0.299, 0.587, 0.114,
|
|
||||||
0.299, 0.587, 0.114
|
|
||||||
);
|
|
||||||
mat3 U = mat3(
|
|
||||||
0.701, -0.587, -0.114,
|
|
||||||
-0.299, 0.413, -0.114,
|
|
||||||
-0.300, -0.588, 0.886
|
|
||||||
);
|
|
||||||
mat3 V = mat3(
|
|
||||||
0.168, -0.331, 0.500,
|
|
||||||
0.328, 0.035, -0.500,
|
|
||||||
-0.497, 0.296, 0.201
|
|
||||||
);
|
|
||||||
return W + U * c + V * s;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main(){
|
|
||||||
vec2 f = (gl_FragCoord.xy - 0.5 * iResolution.xy - uOffsetPx) * uPxScale;
|
|
||||||
|
|
||||||
float z = 5.0;
|
|
||||||
float d = 0.0;
|
|
||||||
|
|
||||||
vec3 p;
|
|
||||||
vec4 o = vec4(0.0);
|
|
||||||
|
|
||||||
float centerShift = uCenterShift;
|
|
||||||
float cf = uColorFreq;
|
|
||||||
|
|
||||||
mat2 wob = mat2(1.0);
|
|
||||||
if (uUseBaseWobble == 1) {
|
|
||||||
float t = iTime * uTimeScale;
|
|
||||||
float c0 = cos(t + 0.0);
|
|
||||||
float c1 = cos(t + 33.0);
|
|
||||||
float c2 = cos(t + 11.0);
|
|
||||||
wob = mat2(c0, c1, c2, c0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const int MAX_STEPS = 100;
|
|
||||||
float stepMultiplier = uStepMultiplier;
|
|
||||||
int actualSteps = int(floor(float(MAX_STEPS) * stepMultiplier));
|
|
||||||
|
|
||||||
for (int i = 0; i < MAX_STEPS; i++) {
|
|
||||||
if (i >= actualSteps) break;
|
|
||||||
p = vec3(f, z);
|
|
||||||
p.xz = p.xz * wob;
|
|
||||||
p = uRot * p;
|
|
||||||
vec3 q = p;
|
|
||||||
q.y += centerShift;
|
|
||||||
d = 0.1 + 0.2 * abs(sdPyramidUpInv(q));
|
|
||||||
z -= d;
|
|
||||||
o += (sin((p.y + z) * cf + vec4(0.0, 1.0, 2.0, 3.0)) + 1.0) / d;
|
|
||||||
}
|
|
||||||
|
|
||||||
o = tanh4(o * o * (uGlow * uBloom) / 1e5);
|
|
||||||
|
|
||||||
vec3 col = o.rgb;
|
|
||||||
float n = rand(gl_FragCoord.xy + vec2(iTime));
|
|
||||||
col += (n - 0.5) * uNoise;
|
|
||||||
col = clamp(col, 0.0, 1.0);
|
|
||||||
|
|
||||||
float L = dot(col, vec3(0.2126, 0.7152, 0.0722));
|
|
||||||
col = clamp(mix(vec3(L), col, uSaturation), 0.0, 1.0);
|
|
||||||
|
|
||||||
if(abs(uHueShift) > 0.0001){
|
|
||||||
col = clamp(hueRotation(uHueShift) * col, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
gl_FragColor = vec4(col, o.a);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const geometry = new Triangle(gl);
|
|
||||||
const iResBuf = new Float32Array(2);
|
|
||||||
const offsetPxBuf = new Float32Array(2);
|
|
||||||
|
|
||||||
const program = new Program(gl, {
|
|
||||||
vertex,
|
|
||||||
fragment,
|
|
||||||
uniforms: {
|
|
||||||
iResolution: { value: iResBuf },
|
|
||||||
iTime: { value: 0 },
|
|
||||||
uHeight: { value: H },
|
|
||||||
uBaseHalf: { value: BASE_HALF },
|
|
||||||
uUseBaseWobble: { value: 1 },
|
|
||||||
uRot: { value: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) },
|
|
||||||
uGlow: { value: GLOW },
|
|
||||||
uOffsetPx: { value: offsetPxBuf },
|
|
||||||
uNoise: { value: NOISE },
|
|
||||||
uSaturation: { value: SAT },
|
|
||||||
uScale: { value: SCALE },
|
|
||||||
uHueShift: { value: HUE },
|
|
||||||
uColorFreq: { value: CFREQ },
|
|
||||||
uBloom: { value: BLOOM },
|
|
||||||
uCenterShift: { value: H * 0.25 },
|
|
||||||
uInvBaseHalf: { value: 1 / BASE_HALF },
|
|
||||||
uInvHeight: { value: 1 / H },
|
|
||||||
uMinAxis: { value: Math.min(BASE_HALF, H) },
|
|
||||||
uPxScale: {
|
|
||||||
value: 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE)
|
|
||||||
},
|
|
||||||
uTimeScale: { value: TS },
|
|
||||||
uStepMultiplier: { value: stepMultiplier }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const mesh = new Mesh(gl, { geometry, program });
|
|
||||||
|
|
||||||
const resize = () => {
|
|
||||||
const w = container.clientWidth || 1;
|
|
||||||
const h = container.clientHeight || 1;
|
|
||||||
renderer.setSize(w, h);
|
|
||||||
iResBuf[0] = gl.drawingBufferWidth;
|
|
||||||
iResBuf[1] = gl.drawingBufferHeight;
|
|
||||||
offsetPxBuf[0] = offX * dpr;
|
|
||||||
offsetPxBuf[1] = offY * dpr;
|
|
||||||
program.uniforms.uPxScale.value = 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE);
|
|
||||||
};
|
|
||||||
const ro = new ResizeObserver(resize);
|
|
||||||
ro.observe(container);
|
|
||||||
resize();
|
|
||||||
|
|
||||||
const rotBuf = new Float32Array(9);
|
|
||||||
const setMat3FromEuler = (yawY, pitchX, rollZ, out) => {
|
|
||||||
const cy = Math.cos(yawY), sy = Math.sin(yawY);
|
|
||||||
const cx = Math.cos(pitchX), sx = Math.sin(pitchX);
|
|
||||||
const cz = Math.cos(rollZ), sz = Math.sin(rollZ);
|
|
||||||
out[0] = cy * cz + sy * sx * sz;
|
|
||||||
out[1] = cx * sz;
|
|
||||||
out[2] = -sy * cz + cy * sx * sz;
|
|
||||||
out[3] = -cy * sz + sy * sx * cz;
|
|
||||||
out[4] = cx * cz;
|
|
||||||
out[5] = sy * sz + cy * sx * cz;
|
|
||||||
out[6] = sy * cx;
|
|
||||||
out[7] = -sx;
|
|
||||||
out[8] = cy * cx;
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NOISE_IS_ZERO = NOISE < 1e-6;
|
|
||||||
let raf = 0;
|
|
||||||
const t0 = performance.now();
|
|
||||||
const startRAF = () => {
|
|
||||||
if (raf) return;
|
|
||||||
raf = requestAnimationFrame(render);
|
|
||||||
};
|
|
||||||
const stopRAF = () => {
|
|
||||||
if (!raf) return;
|
|
||||||
cancelAnimationFrame(raf);
|
|
||||||
raf = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const rnd = () => Math.random();
|
|
||||||
const wX = (0.3 + rnd() * 0.6) * RSX;
|
|
||||||
const wY = (0.2 + rnd() * 0.7) * RSY;
|
|
||||||
const wZ = (0.1 + rnd() * 0.5) * RSZ;
|
|
||||||
const phX = rnd() * Math.PI * 2;
|
|
||||||
const phZ = rnd() * Math.PI * 2;
|
|
||||||
|
|
||||||
let yaw = 0, pitch = 0, roll = 0;
|
|
||||||
let targetYaw = 0, targetPitch = 0;
|
|
||||||
const lerp = (a, b, t) => a + (b - a) * t;
|
|
||||||
|
|
||||||
const pointer = { x: 0, y: 0, inside: true };
|
|
||||||
const onMove = e => {
|
|
||||||
const ww = Math.max(1, window.innerWidth);
|
|
||||||
const wh = Math.max(1, window.innerHeight);
|
|
||||||
const cx = ww * 0.5;
|
|
||||||
const cy = wh * 0.5;
|
|
||||||
pointer.x = Math.max(-1, Math.min(1, (e.clientX - cx) / cx));
|
|
||||||
pointer.y = Math.max(-1, Math.min(1, (e.clientY - cy) / cy));
|
|
||||||
pointer.inside = true;
|
|
||||||
};
|
|
||||||
const onLeave = () => { pointer.inside = false; };
|
|
||||||
const onBlur = () => { pointer.inside = false; };
|
|
||||||
|
|
||||||
let onPointerMove = null;
|
|
||||||
if (animationType === 'hover') {
|
|
||||||
onPointerMove = e => {
|
|
||||||
onMove(e);
|
|
||||||
startRAF();
|
|
||||||
};
|
|
||||||
window.addEventListener('pointermove', onPointerMove, { passive: true });
|
|
||||||
window.addEventListener('mouseleave', onLeave);
|
|
||||||
window.addEventListener('blur', onBlur);
|
|
||||||
program.uniforms.uUseBaseWobble.value = 0;
|
|
||||||
} else if (animationType === '3drotate') {
|
|
||||||
program.uniforms.uUseBaseWobble.value = 0;
|
|
||||||
} else {
|
|
||||||
program.uniforms.uUseBaseWobble.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const render = t => {
|
|
||||||
const time = (t - t0) * 0.001;
|
|
||||||
program.uniforms.iTime.value = time;
|
|
||||||
|
|
||||||
let continueRAF = true;
|
|
||||||
|
|
||||||
if (animationType === 'hover') {
|
|
||||||
const maxPitch = 0.6 * HOVSTR;
|
|
||||||
const maxYaw = 0.6 * HOVSTR;
|
|
||||||
targetYaw = (pointer.inside ? -pointer.x : 0) * maxYaw;
|
|
||||||
targetPitch = (pointer.inside ? pointer.y : 0) * maxPitch;
|
|
||||||
yaw = lerp(yaw, targetYaw, INERT);
|
|
||||||
pitch = lerp(pitch, targetPitch, INERT);
|
|
||||||
roll = lerp(roll, 0, 0.1);
|
|
||||||
program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf);
|
|
||||||
|
|
||||||
if (NOISE_IS_ZERO && Math.abs(yaw - targetYaw) < 1e-4 && Math.abs(pitch - targetPitch) < 1e-4 && Math.abs(roll) < 1e-4) {
|
|
||||||
continueRAF = false;
|
|
||||||
}
|
|
||||||
} else if (animationType === '3drotate') {
|
|
||||||
const tScaled = time * TS;
|
|
||||||
yaw = tScaled * wY;
|
|
||||||
pitch = Math.sin(tScaled * wX + phX) * 0.6;
|
|
||||||
roll = Math.sin(tScaled * wZ + phZ) * 0.5;
|
|
||||||
program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf);
|
|
||||||
if (TS < 1e-6) continueRAF = false;
|
|
||||||
} else {
|
|
||||||
rotBuf[0] = 1; rotBuf[1] = 0; rotBuf[2] = 0;
|
|
||||||
rotBuf[3] = 0; rotBuf[4] = 1; rotBuf[5] = 0;
|
|
||||||
rotBuf[6] = 0; rotBuf[7] = 0; rotBuf[8] = 1;
|
|
||||||
program.uniforms.uRot.value = rotBuf;
|
|
||||||
if (TS < 1e-6) continueRAF = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.render({ scene: mesh });
|
|
||||||
if (continueRAF) {
|
|
||||||
raf = requestAnimationFrame(render);
|
|
||||||
} else {
|
|
||||||
raf = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (suspendWhenOffscreen) {
|
|
||||||
const io = new IntersectionObserver(entries => {
|
|
||||||
const vis = entries.some(e => e.isIntersecting);
|
|
||||||
if (vis) startRAF();
|
|
||||||
else stopRAF();
|
|
||||||
});
|
|
||||||
io.observe(container);
|
|
||||||
startRAF();
|
|
||||||
container.__prismIO = io;
|
|
||||||
} else {
|
|
||||||
startRAF();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stopRAF();
|
|
||||||
ro.disconnect();
|
|
||||||
if (animationType === 'hover' && onPointerMove) {
|
|
||||||
window.removeEventListener('pointermove', onPointerMove);
|
|
||||||
window.removeEventListener('mouseleave', onLeave);
|
|
||||||
window.removeEventListener('blur', onBlur);
|
|
||||||
}
|
|
||||||
if (suspendWhenOffscreen && container.__prismIO) {
|
|
||||||
container.__prismIO.disconnect();
|
|
||||||
delete container.__prismIO;
|
|
||||||
}
|
|
||||||
if (gl.canvas.parentElement === container) {
|
|
||||||
container.removeChild(gl.canvas);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { REVEAL_THRESHOLD, REVEAL_ROOT_MARGIN } from '../config/constants.js';
|
|
||||||
|
|
||||||
let revealObserver = null;
|
|
||||||
|
|
||||||
function getRevealObserver() {
|
|
||||||
if (!revealObserver) {
|
|
||||||
revealObserver = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('in-view');
|
|
||||||
revealObserver.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: REVEAL_THRESHOLD, rootMargin: REVEAL_ROOT_MARGIN }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return revealObserver;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function observeReveals(root = document) {
|
|
||||||
const observer = getRevealObserver();
|
|
||||||
root.querySelectorAll('.reveal').forEach((el) => observer.observe(el));
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export function typewriterReveal(el, text, speed = 75) {
|
|
||||||
el.textContent = '';
|
|
||||||
const chars = text.split('');
|
|
||||||
chars.forEach((char, i) => {
|
|
||||||
const span = document.createElement('span');
|
|
||||||
span.className = 'char';
|
|
||||||
span.textContent = char === ' ' ? '\u00A0' : char;
|
|
||||||
span.style.animationDelay = `${i * speed}ms`;
|
|
||||||
el.appendChild(span);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
document.querySelectorAll("video").forEach(video => {
|
|
||||||
video.muted = true;
|
|
||||||
video.setAttribute("muted", "");
|
|
||||||
video.setAttribute("playsinline", "");
|
|
||||||
video.setAttribute("webkit-playsinline", "");
|
|
||||||
video.preload = "auto";
|
|
||||||
|
|
||||||
video.load();
|
|
||||||
|
|
||||||
video.play().catch(() => { });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { createElement, qs } from '../utils/dom.js';
|
|
||||||
|
|
||||||
function closeAllExcept(list, exceptEl) {
|
|
||||||
list.querySelectorAll('.faq-item.open').forEach((openEl) => {
|
|
||||||
if (openEl === exceptEl) return;
|
|
||||||
openEl.classList.remove('open');
|
|
||||||
qs('.faq-question', openEl).setAttribute('aria-expanded', 'false');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createFaqItem(item, list) {
|
|
||||||
const el = createElement('div', 'faq-item reveal');
|
|
||||||
el.setAttribute('role', 'listitem');
|
|
||||||
el.innerHTML = `
|
|
||||||
<button class="faq-question" aria-expanded="false">
|
|
||||||
<span>${item.q}</span>
|
|
||||||
<svg class="chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M6 9l6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="faq-answer">
|
|
||||||
<div class="faq-answer-inner">${item.a}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const question = qs('.faq-question', el);
|
|
||||||
question.addEventListener('click', () => {
|
|
||||||
const isOpen = el.classList.contains('open');
|
|
||||||
closeAllExcept(list, el);
|
|
||||||
if (!isOpen) {
|
|
||||||
el.classList.add('open');
|
|
||||||
question.setAttribute('aria-expanded', 'true');
|
|
||||||
} else {
|
|
||||||
el.classList.remove('open');
|
|
||||||
question.setAttribute('aria-expanded', 'false');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { copyToClipboard, flashFeedback, flashClass } from '../utils/clipboard.js';
|
|
||||||
import { extractGlowColors } from '../utils/color-extract.js';
|
|
||||||
import { createElement, qs } from '../utils/dom.js';
|
|
||||||
import { COPY_FEEDBACK_DURATION_MS } from '../config/constants.js';
|
|
||||||
|
|
||||||
function buildLoaderSnippet(script) {
|
|
||||||
const rawLink = script.links?.[0]?.href || '';
|
|
||||||
return `loadstring(game:HttpGet("${rawLink}"))()`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachGlow(card, img) {
|
|
||||||
extractGlowColors(img).then((colors) => {
|
|
||||||
if (!colors) return;
|
|
||||||
card.style.setProperty('--glow-a', colors.a);
|
|
||||||
card.style.setProperty('--glow-b', colors.b);
|
|
||||||
card.classList.add('glow-ready');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachCopyHandler(card, script) {
|
|
||||||
const copyIcon = qs('.script-copy-icon', card);
|
|
||||||
card.addEventListener('click', async () => {
|
|
||||||
const snippet = buildLoaderSnippet(script);
|
|
||||||
await copyToClipboard(snippet);
|
|
||||||
if (copyIcon) flashClass(copyIcon, 'copied', COPY_FEEDBACK_DURATION_MS);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createScriptCard(script) {
|
|
||||||
const card = createElement('div', 'script-card');
|
|
||||||
let html = `
|
|
||||||
<div class="script-glow"></div>
|
|
||||||
<div class="script-image">
|
|
||||||
<img src="${script.image}" alt="${script.title}" crossorigin="anonymous" width="300" height="168" loading="lazy" decoding="async">
|
|
||||||
</div>
|
|
||||||
<h3 class="script-title">
|
|
||||||
${script.title}
|
|
||||||
<span class="script-copy-icon">
|
|
||||||
<svg class="copy-icon" width="20px" height="20px" viewBox="0 -0.5 25 25" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M8.94605 4.99995L13.2541 4.99995C14.173 5.00498 15.0524 5.37487 15.6986 6.02825C16.3449 6.68163 16.7051 7.56497 16.7001 8.48395V12.716C16.7051 13.6349 16.3449 14.5183 15.6986 15.1717C15.0524 15.825 14.173 16.1949 13.2541 16.2H8.94605C8.02707 16.1949 7.14773 15.825 6.50148 15.1717C5.85522 14.5183 5.495 13.6349 5.50005 12.716L5.50005 8.48495C5.49473 7.5658 5.85484 6.6822 6.50112 6.0286C7.1474 5.375 8.0269 5.00498 8.94605 4.99995Z" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M10.1671 19H14.9371C17.4857 18.9709 19.5284 16.8816 19.5001 14.333V9.666" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
|
|
||||||
<svg class="check-icon" width="20px" height="20px" viewBox="0 -0.5 25 25" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g>
|
|
||||||
<path d="M5.5 12.5L10 17L19.5 7.5"
|
|
||||||
stroke="#ffffff"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round">
|
|
||||||
</path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</h3>`;
|
|
||||||
if (script.description) {
|
|
||||||
html += `<p class="script-desc">${script.description}</p>`;
|
|
||||||
}
|
|
||||||
card.innerHTML = html;
|
|
||||||
|
|
||||||
attachGlow(card, qs('.script-image img', card));
|
|
||||||
attachCopyHandler(card, script);
|
|
||||||
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { createElement, qs } from '../utils/dom.js';
|
|
||||||
import { youtubeThumbnailUrl } from '../utils/youtube.js';
|
|
||||||
|
|
||||||
export function createShowcaseItem(video) {
|
|
||||||
const item = createElement('div', 'showcase-item');
|
|
||||||
item.innerHTML = `
|
|
||||||
<button class="showcase-card" type="button" aria-label="Play video">
|
|
||||||
<img class="showcase-thumb" src="${youtubeThumbnailUrl(video.id)}" alt="" width="280" height="157" loading="lazy" decoding="async">
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const img = qs('img', item);
|
|
||||||
img.addEventListener('load', () => item.classList.add('loaded'));
|
|
||||||
img.addEventListener('error', () => {
|
|
||||||
item.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardBtn = qs('.showcase-card', item);
|
|
||||||
cardBtn.addEventListener('click', () => {
|
|
||||||
cardBtn.classList.add('clicked');
|
|
||||||
window.open(video.url, '_blank', 'noopener,noreferrer');
|
|
||||||
});
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { createElement } from '../utils/dom.js';
|
|
||||||
|
|
||||||
function initials(name) {
|
|
||||||
return name
|
|
||||||
.split(' ')
|
|
||||||
.map((word) => word[0])
|
|
||||||
.join('')
|
|
||||||
.slice(0, 2)
|
|
||||||
.toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createTestimonialCard(testimonial) {
|
|
||||||
const card = createElement('div', 'testimonial-card');
|
|
||||||
let avatarHTML;
|
|
||||||
|
|
||||||
if (testimonial.avatar) {
|
|
||||||
if (testimonial.avatarSmall) {
|
|
||||||
avatarHTML = `<img src="${testimonial.avatarSmall}" alt="" width="34" height="34" loading="lazy" decoding="async"
|
|
||||||
srcset="${testimonial.avatarSmall} 60w, ${testimonial.avatar} 120w" sizes="60px">`;
|
|
||||||
} else {
|
|
||||||
avatarHTML = `<img src="${testimonial.avatar}" alt="" width="34" height="34" loading="lazy" decoding="async">`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
avatarHTML = initials(testimonial.name || '?');
|
|
||||||
}
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="testimonial-top">
|
|
||||||
<div class="testimonial-user">
|
|
||||||
<div class="testimonial-avatar">${avatarHTML}</div>
|
|
||||||
<div class="testimonial-names">
|
|
||||||
<span class="testimonial-name">${testimonial.name}</span>
|
|
||||||
<span class="testimonial-handle">${testimonial.handle || ''}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="testimonial-text">${testimonial.text}</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export const DISCORD_INVITE_CODE = '5eqRhKeywG';
|
|
||||||
export const DISCORD_INVITE_URL = `https://discord.gg/${DISCORD_INVITE_CODE}`;
|
|
||||||
export const DISCORD_LOADER_SCRIPT = 'https://gitlab.com/vyla-entertainment/home/-/releases';
|
|
||||||
|
|
||||||
export const DATA_PATHS = {
|
|
||||||
partners: 'src/data/partners.json',
|
|
||||||
reviews: 'src/data/reviews.json',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DISCORD_INVITE_API = `https://discord.com/api/v10/invites/${DISCORD_INVITE_CODE}?with_counts=true`;
|
|
||||||
|
|
||||||
export const COPY_FEEDBACK_DURATION_MS = 1800;
|
|
||||||
export const REVEAL_THRESHOLD = 0.15;
|
|
||||||
export const REVEAL_ROOT_MARGIN = '0px 0px -40px 0px';
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
export const FAQ_ITEMS = [
|
|
||||||
{
|
|
||||||
q: 'Why did Vyla transition from a web app to Vyla Home?',
|
|
||||||
a: 'Centralized public hosting caused cloud provider downtime, datacenter IP rate-limiting, and single-point-of-failure vulnerabilities. Moving execution locally to Vyla Home allows direct residential stream resolution without third-party server mediation.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: 'Why does Windows SmartScreen flag the installer?',
|
|
||||||
a: 'Windows SmartScreen displays an unverified publisher prompt on any open-source binary lacking an expensive commercial EV Code Signing Certificate. Vyla Home is 100% open-source on GitLab, allowing you to audit the build scripts or verify the SHA-256 release checksum manually.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: 'How are my settings, bookmarks, and watch history protected?',
|
|
||||||
a: 'All user data is stored locally in an AES-256-GCM encrypted database keyed to your device hardware profile. No usage logs, browsing history, or playback metrics are ever transmitted to external servers.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: 'How does the update mechanism ensure binary integrity?',
|
|
||||||
a: 'Vyla Home queries the official tagged GitLab release API over HTTPS. Update payloads are verified against published SHA-256 cryptographic hashes before application to prevent tampering.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: 'Does Vyla Home require external runtime dependencies?',
|
|
||||||
a: 'No. The standalone installer packages the entire runtime environment, internal microservices, and isolated media decoders inside a self-contained desktop container.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: 'Is Vyla Home open-source and free?',
|
|
||||||
a: 'Yes. Vyla Home is completely free and open-source under the MIT license with zero paid tiers, keys, or premium locks.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { observeReveals } from './animations/reveal-observer.js';
|
|
||||||
import { initLiquidGlass } from './animations/glass-surface.js';
|
|
||||||
import { initHeroSection } from './sections/hero.js';
|
|
||||||
import { initTestimonialsSection } from './sections/testimonials.js';
|
|
||||||
import { initPartnersSection } from './sections/partners.js';
|
|
||||||
import { initFaqSection } from './sections/faq.js';
|
|
||||||
import { initStatsSection } from './sections/stats.js';
|
|
||||||
import { getDeviceCapability, getAnimationConfig } from './utils/device-capability.js';
|
|
||||||
|
|
||||||
function initApp() {
|
|
||||||
const deviceCapability = getDeviceCapability();
|
|
||||||
const animationConfig = getAnimationConfig();
|
|
||||||
|
|
||||||
observeReveals();
|
|
||||||
|
|
||||||
if (animationConfig.glassSurface.enabled) {
|
|
||||||
initLiquidGlass(animationConfig.glassSurface);
|
|
||||||
}
|
|
||||||
|
|
||||||
initHeroSection(animationConfig);
|
|
||||||
initStatsSection();
|
|
||||||
initTestimonialsSection(animationConfig);
|
|
||||||
initPartnersSection();
|
|
||||||
initFaqSection();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initApp);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { FAQ_ITEMS } from '../config/faq-data.js';
|
|
||||||
import { createFaqItem } from '../components/faq-item.js';
|
|
||||||
import { observeReveals } from '../animations/reveal-observer.js';
|
|
||||||
|
|
||||||
export function initFaqSection() {
|
|
||||||
const list = document.getElementById('faq-list');
|
|
||||||
if (!list) return;
|
|
||||||
|
|
||||||
list.innerHTML = '';
|
|
||||||
FAQ_ITEMS.forEach((item) => {
|
|
||||||
list.appendChild(createFaqItem(item, list));
|
|
||||||
});
|
|
||||||
|
|
||||||
observeReveals(list);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { qs } from '../utils/dom.js';
|
|
||||||
import { copyToClipboard, flashFeedback, flashClass } from '../utils/clipboard.js';
|
|
||||||
import { typewriterReveal } from '../animations/typewriter.js';
|
|
||||||
import { initPrism } from '../animations/prism.js';
|
|
||||||
import { initLightRays } from '../animations/light-rays.js';
|
|
||||||
import { DISCORD_LOADER_SCRIPT, COPY_FEEDBACK_DURATION_MS } from '../config/constants.js';
|
|
||||||
|
|
||||||
const HERO_GREETING = 'The Local-First Desktop Media Center';
|
|
||||||
|
|
||||||
function initHeroPrism() {
|
|
||||||
const prismContainer = document.getElementById('hero-prism');
|
|
||||||
if (!prismContainer) return;
|
|
||||||
|
|
||||||
initPrism(prismContainer, {
|
|
||||||
animationType: 'rotate',
|
|
||||||
timeScale: 0.5,
|
|
||||||
height: 3.5,
|
|
||||||
baseWidth: 5.5,
|
|
||||||
scale: 3.6,
|
|
||||||
hueShift: 0,
|
|
||||||
colorFrequency: 1,
|
|
||||||
noise: 0.5,
|
|
||||||
glow: 1,
|
|
||||||
transparent: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initCtaRays() {
|
|
||||||
const raysContainer = document.getElementById('cta-rays');
|
|
||||||
if (!raysContainer) return;
|
|
||||||
|
|
||||||
initLightRays(raysContainer, {
|
|
||||||
raysOrigin: 'bottom-center',
|
|
||||||
rayColor1: '#EAB308',
|
|
||||||
rayColor2: '#96c8ff',
|
|
||||||
blend: 0.65,
|
|
||||||
raysSpeed: 1.8,
|
|
||||||
lightSpread: 0.7,
|
|
||||||
rayLength: 2.5,
|
|
||||||
followMouse: true,
|
|
||||||
mouseInfluence: 0.12,
|
|
||||||
noiseAmount: 0.08,
|
|
||||||
distortion: 0.04,
|
|
||||||
fadeDistance: 2.0,
|
|
||||||
saturation: 1.4
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initHeroTitle() {
|
|
||||||
const heroTitle = qs('.hero-title');
|
|
||||||
if (heroTitle) typewriterReveal(heroTitle, HERO_GREETING);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initDiscordCopyBar() {
|
|
||||||
const copyBtn = document.getElementById('copy-discord-btn');
|
|
||||||
const copyIcon = document.getElementById('copy-discord-icon');
|
|
||||||
if (!copyBtn) return;
|
|
||||||
|
|
||||||
copyBtn.addEventListener('click', async () => {
|
|
||||||
await copyToClipboard(DISCORD_LOADER_SCRIPT);
|
|
||||||
if (copyIcon) flashClass(copyIcon, 'copied', COPY_FEEDBACK_DURATION_MS);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initHeroCopyButton() {
|
|
||||||
const heroCopyScriptBtn = document.getElementById('hero-copy-script-btn');
|
|
||||||
if (!heroCopyScriptBtn) return;
|
|
||||||
|
|
||||||
heroCopyScriptBtn.addEventListener('click', async () => {
|
|
||||||
await copyToClipboard(DISCORD_LOADER_SCRIPT);
|
|
||||||
flashFeedback(heroCopyScriptBtn, 'Copied!', COPY_FEEDBACK_DURATION_MS);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initCtaCopyButton() {
|
|
||||||
const ctaCopyScriptBtn = document.getElementById('cta-copy-script-btn');
|
|
||||||
if (!ctaCopyScriptBtn) return;
|
|
||||||
|
|
||||||
ctaCopyScriptBtn.addEventListener('click', async () => {
|
|
||||||
await copyToClipboard(DISCORD_LOADER_SCRIPT);
|
|
||||||
flashFeedback(ctaCopyScriptBtn, 'Copied!', COPY_FEEDBACK_DURATION_MS);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initHeroSection() {
|
|
||||||
initHeroPrism();
|
|
||||||
initCtaRays();
|
|
||||||
initHeroTitle();
|
|
||||||
initDiscordCopyBar();
|
|
||||||
initHeroCopyButton();
|
|
||||||
initCtaCopyButton();
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { getPartners } from '../services/partners-service.js';
|
|
||||||
import { createElement } from '../utils/dom.js';
|
|
||||||
import { observeReveals } from '../animations/reveal-observer.js';
|
|
||||||
import { shouldReduceEffects } from '../utils/device-capability.js';
|
|
||||||
|
|
||||||
function createPartnerItem(partner) {
|
|
||||||
const item = createElement('div', 'partner-item');
|
|
||||||
let imgHTML;
|
|
||||||
|
|
||||||
if (partner.logo) {
|
|
||||||
if (partner.logoSmall) {
|
|
||||||
imgHTML = `<img src="${partner.logoSmall}" alt="${partner.name}" width="128" height="128" loading="lazy" decoding="async"
|
|
||||||
fetchpriority="low"
|
|
||||||
srcset="${partner.logoSmall} 64w, ${partner.logo} 128w" sizes="64px">`;
|
|
||||||
} else {
|
|
||||||
imgHTML = `<img src="${partner.logo}" alt="${partner.name}" width="128" height="128" loading="lazy" decoding="async"
|
|
||||||
fetchpriority="low">`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
imgHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const inner = partner.link
|
|
||||||
? `<a href="${partner.link}" target="_blank" rel="noopener noreferrer">${imgHTML}</a>`
|
|
||||||
: imgHTML;
|
|
||||||
item.innerHTML = inner;
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initPartnersSection() {
|
|
||||||
const grid = document.getElementById('partners-grid');
|
|
||||||
if (!grid) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const partners = await getPartners();
|
|
||||||
grid.innerHTML = '';
|
|
||||||
partners.forEach((partner) => grid.appendChild(createPartnerItem(partner)));
|
|
||||||
|
|
||||||
if (shouldReduceEffects()) {
|
|
||||||
grid.classList.add('in-view');
|
|
||||||
} else {
|
|
||||||
observeReveals(grid);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
grid.innerHTML = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { getDiscordStats } from '../services/discord-service.js';
|
|
||||||
import { getScripts } from '../services/scripts-service.js';
|
|
||||||
import { animateCountOnVisible } from '../animations/counter.js';
|
|
||||||
import { shouldReduceEffects } from '../utils/device-capability.js';
|
|
||||||
|
|
||||||
async function initDiscordStats() {
|
|
||||||
const membersEl = document.getElementById('stat-members');
|
|
||||||
const onlineEl = document.getElementById('stat-online');
|
|
||||||
if (!membersEl || !onlineEl) return;
|
|
||||||
|
|
||||||
if (shouldReduceEffects()) {
|
|
||||||
membersEl.textContent = '1K+';
|
|
||||||
onlineEl.textContent = '100+';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { memberCount, onlineCount } = await getDiscordStats();
|
|
||||||
const statsGrid = document.getElementById('stats-grid');
|
|
||||||
const onlineItem = onlineEl.closest('.stat-item');
|
|
||||||
if (onlineItem) onlineItem.classList.add('stat-online');
|
|
||||||
|
|
||||||
if (statsGrid) {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
animateCountOnVisible(membersEl, memberCount);
|
|
||||||
animateCountOnVisible(onlineEl, onlineCount);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: 0.5 }
|
|
||||||
);
|
|
||||||
observer.observe(statsGrid);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
membersEl.textContent = '-';
|
|
||||||
onlineEl.textContent = '-';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initScriptCountStat() {
|
|
||||||
const scriptsEl = document.getElementById('stat-scripts');
|
|
||||||
if (!scriptsEl) return;
|
|
||||||
|
|
||||||
if (shouldReduceEffects()) {
|
|
||||||
scriptsEl.textContent = '50+';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const scripts = await getScripts();
|
|
||||||
animateCountOnVisible(scriptsEl, scripts.length);
|
|
||||||
} catch (error) {
|
|
||||||
scriptsEl.textContent = '-';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initStatsSection() {
|
|
||||||
if ('requestIdleCallback' in window) {
|
|
||||||
window.requestIdleCallback(() => {
|
|
||||||
initDiscordStats();
|
|
||||||
initScriptCountStat();
|
|
||||||
}, { timeout: 2000 });
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
initDiscordStats();
|
|
||||||
initScriptCountStat();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { getReviews } from '../services/reviews-service.js';
|
|
||||||
import { createTestimonialCard } from '../components/testimonial-card.js';
|
|
||||||
import { createMarqueeLoop } from '../animations/marquee-loop.js';
|
|
||||||
|
|
||||||
const AVG_TESTIMONIAL_WIDTH_PX = 260;
|
|
||||||
|
|
||||||
function fillMarqueeTrack(trackEl, reviews) {
|
|
||||||
trackEl.innerHTML = '';
|
|
||||||
if (!reviews.length) return;
|
|
||||||
|
|
||||||
const minSets = Math.max(1, Math.ceil((window.innerWidth * 2) / (reviews.length * AVG_TESTIMONIAL_WIDTH_PX)));
|
|
||||||
for (let i = 0; i < minSets; i++) {
|
|
||||||
reviews.forEach((review) => trackEl.appendChild(createTestimonialCard(review)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstPass = Array.from(trackEl.children).map((card) => card.cloneNode(true));
|
|
||||||
firstPass.forEach((card) => trackEl.appendChild(card));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initTestimonialsSection(animationConfig) {
|
|
||||||
const track1 = document.getElementById('marquee-track-1');
|
|
||||||
const track2 = document.getElementById('marquee-track-2');
|
|
||||||
if (!track1 || !track2) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const reviews = await getReviews();
|
|
||||||
const mid = Math.ceil(reviews.length / 2);
|
|
||||||
const rowA = reviews.slice(0, mid);
|
|
||||||
const rowB = reviews.slice(mid);
|
|
||||||
|
|
||||||
fillMarqueeTrack(track1, rowA.length ? rowA : reviews);
|
|
||||||
fillMarqueeTrack(track2, rowB.length ? rowB : reviews);
|
|
||||||
|
|
||||||
if (animationConfig.marquee.enabled) {
|
|
||||||
const baseSpeed = animationConfig.marquee.reducedSpeed ? 0.3 : 0.6;
|
|
||||||
const boostedSpeed = animationConfig.marquee.reducedSpeed ? 0.6 : 1.2;
|
|
||||||
|
|
||||||
createMarqueeLoop(track1, '--marquee-x-1', {
|
|
||||||
baseSpeed,
|
|
||||||
boostedSpeed,
|
|
||||||
minSetsFor: 'screen'
|
|
||||||
});
|
|
||||||
|
|
||||||
createMarqueeLoop(track2, '--marquee-x-2', {
|
|
||||||
baseSpeed: -baseSpeed,
|
|
||||||
boostedSpeed: -boostedSpeed,
|
|
||||||
minSetsFor: 'screen'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const wrap = document.querySelector('.marquee-wrap');
|
|
||||||
if (wrap) {
|
|
||||||
wrap.innerHTML = '<p style="color: var(--color-muted); text-align:center; width:100%">reviews unavailable.</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { DISCORD_INVITE_API } from '../config/constants.js';
|
|
||||||
|
|
||||||
export async function getDiscordStats() {
|
|
||||||
const response = await fetch(DISCORD_INVITE_API);
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch Discord invite stats');
|
|
||||||
const data = await response.json();
|
|
||||||
return {
|
|
||||||
memberCount: data.approximate_member_count || 0,
|
|
||||||
onlineCount: data.approximate_presence_count || 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export async function fetchJson(path) {
|
|
||||||
const response = await fetch(path);
|
|
||||||
if (!response.ok) throw new Error(`Failed to fetch ${path}`);
|
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchJsonArray(path) {
|
|
||||||
const data = await fetchJson(path);
|
|
||||||
if (!Array.isArray(data) || data.length === 0) {
|
|
||||||
throw new Error(`Expected non-empty array from ${path}`);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { DATA_PATHS } from '../config/constants.js';
|
|
||||||
import { fetchJsonArray } from './json-source.js';
|
|
||||||
|
|
||||||
export function getPartners() {
|
|
||||||
return fetchJsonArray(DATA_PATHS.partners);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { DATA_PATHS } from '../config/constants.js';
|
|
||||||
import { fetchJsonArray } from './json-source.js';
|
|
||||||
|
|
||||||
export function getReviews() {
|
|
||||||
return fetchJsonArray(DATA_PATHS.reviews);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { DATA_PATHS } from '../config/constants.js';
|
|
||||||
import { fetchJsonArray } from './json-source.js';
|
|
||||||
|
|
||||||
export function getScripts() {
|
|
||||||
return fetchJsonArray(DATA_PATHS.scripts);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
export async function copyToClipboard(text) {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return copyWithFallback(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyWithFallback(text) {
|
|
||||||
const temp = document.createElement('textarea');
|
|
||||||
temp.value = text;
|
|
||||||
temp.style.position = 'fixed';
|
|
||||||
temp.style.opacity = '0';
|
|
||||||
document.body.appendChild(temp);
|
|
||||||
temp.select();
|
|
||||||
let succeeded = false;
|
|
||||||
try {
|
|
||||||
succeeded = document.execCommand('copy');
|
|
||||||
} catch (error) {
|
|
||||||
succeeded = false;
|
|
||||||
}
|
|
||||||
document.body.removeChild(temp);
|
|
||||||
return succeeded;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function flashFeedback(el, feedbackText, duration, property = 'textContent') {
|
|
||||||
if (!el) return;
|
|
||||||
const original = el[property];
|
|
||||||
el[property] = feedbackText;
|
|
||||||
setTimeout(() => {
|
|
||||||
el[property] = original;
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function flashClass(el, className, duration) {
|
|
||||||
if (!el) return;
|
|
||||||
el.classList.add(className);
|
|
||||||
setTimeout(() => {
|
|
||||||
el.classList.remove(className);
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
const SAMPLE_SIZE = 32;
|
|
||||||
const MIN_ALPHA = 100;
|
|
||||||
const MIN_SATURATION = 12;
|
|
||||||
const SHADOW_CEILING = 30;
|
|
||||||
const HIGHLIGHT_FLOOR = 235;
|
|
||||||
|
|
||||||
function averageColor(pixels) {
|
|
||||||
const sum = pixels.reduce((acc, p) => [acc[0] + p[0], acc[1] + p[1], acc[2] + p[2]], [0, 0, 0]);
|
|
||||||
return sum.map((channel) => Math.round(channel / pixels.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectVividPixels(imageData) {
|
|
||||||
const buckets = [];
|
|
||||||
for (let i = 0; i < imageData.length; i += 4) {
|
|
||||||
const r = imageData[i];
|
|
||||||
const g = imageData[i + 1];
|
|
||||||
const b = imageData[i + 2];
|
|
||||||
const a = imageData[i + 3];
|
|
||||||
if (a < MIN_ALPHA) continue;
|
|
||||||
const max = Math.max(r, g, b);
|
|
||||||
const min = Math.min(r, g, b);
|
|
||||||
if (max - min < MIN_SATURATION && (max < SHADOW_CEILING || max > HIGHLIGHT_FLOOR)) continue;
|
|
||||||
buckets.push([r, g, b]);
|
|
||||||
}
|
|
||||||
return buckets;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saturationOf(pixel) {
|
|
||||||
return Math.max(...pixel) - Math.min(...pixel);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sampleImage(img) {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = SAMPLE_SIZE;
|
|
||||||
canvas.height = SAMPLE_SIZE;
|
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
ctx.drawImage(img, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
|
||||||
const { data } = ctx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
|
||||||
const buckets = collectVividPixels(data);
|
|
||||||
if (!buckets.length) return null;
|
|
||||||
|
|
||||||
buckets.sort((a, b) => saturationOf(b) - saturationOf(a));
|
|
||||||
|
|
||||||
const sliceSize = Math.max(4, Math.floor(buckets.length * 0.2));
|
|
||||||
const top = buckets.slice(0, sliceSize);
|
|
||||||
const bottom = buckets.slice(-sliceSize);
|
|
||||||
|
|
||||||
const [r1, g1, b1] = averageColor(top);
|
|
||||||
const [r2, g2, b2] = averageColor(bottom.length ? bottom : top);
|
|
||||||
|
|
||||||
return {
|
|
||||||
a: `rgba(${r1}, ${g1}, ${b1}, 0.9)`,
|
|
||||||
b: `rgba(${r2}, ${g2}, ${b2}, 0.9)`,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractGlowColors(img) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const run = () => resolve(sampleImage(img));
|
|
||||||
if (img.complete && img.naturalWidth) {
|
|
||||||
run();
|
|
||||||
} else {
|
|
||||||
img.addEventListener('load', run, { once: true });
|
|
||||||
img.addEventListener('error', () => resolve(null), { once: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
export class DeviceCapability {
|
|
||||||
constructor() {
|
|
||||||
this.lowEndDevice = this.detectLowEndDevice();
|
|
||||||
this.reducedMotion = this.detectReducedMotion();
|
|
||||||
this.lowMemory = this.detectLowMemory();
|
|
||||||
this.slowConnection = this.detectSlowConnection();
|
|
||||||
this.mobileViewport = this.detectMobileViewport();
|
|
||||||
this.shouldReduceEffects = this.calculateShouldReduceEffects();
|
|
||||||
}
|
|
||||||
|
|
||||||
detectLowEndDevice() {
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
|
||||||
const mobileUA = /android|iphone|ipod|ipad|blackberry|iemobile|opera mini/i.test(ua);
|
|
||||||
|
|
||||||
const cores = navigator.hardwareConcurrency || 4;
|
|
||||||
const lowCores = cores <= 2;
|
|
||||||
|
|
||||||
const memory = (navigator.deviceMemory || 8);
|
|
||||||
const lowMemory = memory <= 4;
|
|
||||||
|
|
||||||
return mobileUA && (lowCores || lowMemory);
|
|
||||||
}
|
|
||||||
|
|
||||||
detectReducedMotion() {
|
|
||||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
detectLowMemory() {
|
|
||||||
const memory = navigator.deviceMemory;
|
|
||||||
if (memory && memory <= 4) return true;
|
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
|
||||||
const lowEndUA = /android.*gt-[a-z0-9]+/i.test(ua) ||
|
|
||||||
/iphone.*os [0-9]+_[0-9]+/i.test(ua);
|
|
||||||
return lowEndUA;
|
|
||||||
}
|
|
||||||
|
|
||||||
detectSlowConnection() {
|
|
||||||
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
||||||
if (!connection) return false;
|
|
||||||
|
|
||||||
const slowTypes = ['2g', '3g'];
|
|
||||||
const effectiveType = connection.effectiveType;
|
|
||||||
const saveData = connection.saveData;
|
|
||||||
|
|
||||||
return slowTypes.includes(effectiveType) || saveData;
|
|
||||||
}
|
|
||||||
|
|
||||||
detectMobileViewport() {
|
|
||||||
return window.innerWidth < 768;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateShouldReduceEffects() {
|
|
||||||
return this.reducedMotion ||
|
|
||||||
this.lowEndDevice ||
|
|
||||||
this.lowMemory ||
|
|
||||||
this.slowConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAnimationConfig() {
|
|
||||||
if (this.shouldReduceEffects) {
|
|
||||||
return {
|
|
||||||
lightRays: {
|
|
||||||
enabled: false,
|
|
||||||
raysSpeed: 0.5,
|
|
||||||
followMouse: false
|
|
||||||
},
|
|
||||||
glassSurface: {
|
|
||||||
enabled: true,
|
|
||||||
useBackdropOnly: true,
|
|
||||||
reducedBlur: true
|
|
||||||
},
|
|
||||||
marquee: {
|
|
||||||
enabled: true,
|
|
||||||
reducedSpeed: true
|
|
||||||
},
|
|
||||||
fluid: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mobileViewport) {
|
|
||||||
return {
|
|
||||||
lightRays: {
|
|
||||||
enabled: true,
|
|
||||||
raysSpeed: 1.0,
|
|
||||||
followMouse: false
|
|
||||||
},
|
|
||||||
glassSurface: {
|
|
||||||
enabled: true,
|
|
||||||
useBackdropOnly: false,
|
|
||||||
reducedBlur: false
|
|
||||||
},
|
|
||||||
marquee: {
|
|
||||||
enabled: true,
|
|
||||||
reducedSpeed: false
|
|
||||||
},
|
|
||||||
fluid: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
lightRays: {
|
|
||||||
enabled: true,
|
|
||||||
raysSpeed: 1.8,
|
|
||||||
followMouse: true
|
|
||||||
},
|
|
||||||
glassSurface: {
|
|
||||||
enabled: true,
|
|
||||||
useBackdropOnly: false,
|
|
||||||
reducedBlur: false
|
|
||||||
},
|
|
||||||
marquee: {
|
|
||||||
enabled: true,
|
|
||||||
reducedSpeed: false
|
|
||||||
},
|
|
||||||
fluid: {
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let deviceCapabilityInstance = null;
|
|
||||||
|
|
||||||
export function getDeviceCapability() {
|
|
||||||
if (!deviceCapabilityInstance) {
|
|
||||||
deviceCapabilityInstance = new DeviceCapability();
|
|
||||||
}
|
|
||||||
return deviceCapabilityInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldReduceEffects() {
|
|
||||||
return getDeviceCapability().shouldReduceEffects;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAnimationConfig() {
|
|
||||||
return getDeviceCapability().getAnimationConfig();
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
export function prefersReducedMotion() {
|
|
||||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMobileViewport() {
|
|
||||||
return window.matchMedia('(max-width: 768px)').matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function qs(selector, root = document) {
|
|
||||||
return root.querySelector(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function qsa(selector, root = document) {
|
|
||||||
return Array.from(root.querySelectorAll(selector));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createElement(tag, className) {
|
|
||||||
const el = document.createElement(tag);
|
|
||||||
if (className) el.className = className;
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function debounce(fn, delay) {
|
|
||||||
let timeoutId = null;
|
|
||||||
return (...args) => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
timeoutId = setTimeout(() => fn(...args), delay);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
export function youtubeIdFromUrl(url) {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(url);
|
|
||||||
if (parsed.hostname.includes('youtu.be')) {
|
|
||||||
return parsed.pathname.slice(1);
|
|
||||||
}
|
|
||||||
const videoParam = parsed.searchParams.get('v');
|
|
||||||
if (videoParam) return videoParam;
|
|
||||||
const shortsMatch = parsed.pathname.match(/\/shorts\/([^/]+)/);
|
|
||||||
if (shortsMatch) return shortsMatch[1];
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function youtubeThumbnailUrl(videoId) {
|
|
||||||
return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function youtubeEmbedUrl(videoId) {
|
|
||||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
background: var(--color-bg);
|
|
||||||
color: var(--color-fg);
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: 16px;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
height: 100%;
|
|
||||||
overscroll-behavior-y: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
line-height: 1.5;
|
|
||||||
overflow-x: hidden;
|
|
||||||
position: relative;
|
|
||||||
min-height: 100%;
|
|
||||||
min-height: 100dvh;
|
|
||||||
background: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
body::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font: inherit;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid var(--color-fg);
|
|
||||||
outline-offset: 3px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: var(--color-fg);
|
|
||||||
color: var(--color-bg);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
:root {
|
|
||||||
--color-bg: #000000;
|
|
||||||
--color-bg-elevated: #0a0a0a;
|
|
||||||
--color-bg-card: #020202;
|
|
||||||
--color-fg: #ffffff;
|
|
||||||
--color-fg-dim: #cccccc;
|
|
||||||
--color-muted: #8f8f8f;
|
|
||||||
--color-border: #262626;
|
|
||||||
--color-border-strong: #383838;
|
|
||||||
|
|
||||||
--font-display: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
||||||
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
||||||
|
|
||||||
--space-3xs: 0.25rem;
|
|
||||||
--space-2xs: 0.375rem;
|
|
||||||
--space-xs: 0.5rem;
|
|
||||||
--space-sm: 0.75rem;
|
|
||||||
--space-md: 1rem;
|
|
||||||
--space-lg: 1.5rem;
|
|
||||||
--space-xl: 2rem;
|
|
||||||
--space-2xl: 3rem;
|
|
||||||
--space-3xl: 4rem;
|
|
||||||
--space-4xl: 6rem;
|
|
||||||
|
|
||||||
--radius-sm: 8px;
|
|
||||||
--radius-md: 14px;
|
|
||||||
--radius-lg: 20px;
|
|
||||||
--radius-pill: 999px;
|
|
||||||
|
|
||||||
--ease-standard: cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
--ease-out-soft: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
--duration-fast: 0.2s;
|
|
||||||
--duration-base: 0.35s;
|
|
||||||
--duration-slow: 0.8s;
|
|
||||||
|
|
||||||
--shadow-elevated: 0 20px 60px rgba(0, 0, 0, 0.45);
|
|
||||||
--shadow-modal: 0 40px 100px rgba(0, 0, 0, 0.6);
|
|
||||||
|
|
||||||
--z-dot-grid: 40;
|
|
||||||
--z-nav: 50;
|
|
||||||
--z-marquee-fade: 2;
|
|
||||||
--z-modal: 200;
|
|
||||||
|
|
||||||
--content-padding-inline: clamp(1.25rem, 5vw, 4rem);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
.bold {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-fg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold-black {
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--color-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-balance {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-eyebrow {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title-lg {
|
|
||||||
font-size: clamp(1.5rem, 3vw, 1.85rem);
|
|
||||||
font-weight: 300;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-subtitle {
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-subtitle a {
|
|
||||||
color: var(--color-fg);
|
|
||||||
transition: color var(--duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-subtitle a:hover {
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
background: var(--color-fg);
|
|
||||||
color: var(--color-bg);
|
|
||||||
padding: 0.875rem 1.75rem;
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
transition: transform var(--duration-fast) var(--ease-out-soft), opacity var(--duration-fast) ease;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
touch-action: manipulation;
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:active {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding-left: 1rem;
|
|
||||||
height: 50px !important;
|
|
||||||
width: 160px;
|
|
||||||
border-radius: var(--radius-pill) !important;
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: transform var(--duration-fast) var(--ease-out-soft),
|
|
||||||
background var(--duration-fast) ease,
|
|
||||||
opacity var(--duration-fast) ease;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
touch-action: manipulation;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta:hover {
|
|
||||||
transform: scale(0.99);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta:active {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta-dark {
|
|
||||||
background: #000000;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta-dark:hover {
|
|
||||||
background: #000;
|
|
||||||
border-color: var(--color-fg-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta-light {
|
|
||||||
position: relative;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-fg);
|
|
||||||
border-radius: 50px !important;
|
|
||||||
padding: 0.75rem 1.75rem !important;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-cta-unsure {
|
|
||||||
position: relative;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-fg);
|
|
||||||
border-radius: 50px !important;
|
|
||||||
padding: 0.75rem 1.75rem !important;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: none) and (pointer: coarse) {
|
|
||||||
.btn {
|
|
||||||
min-height: 48px;
|
|
||||||
min-width: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-expandable {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
background: #000;
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-expandable .hero-cta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
background: transparent !important;
|
|
||||||
border: none;
|
|
||||||
margin: 0;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn-text {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-arrow {
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-expandable[aria-expanded="true"] .dropdown-arrow {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-options {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 8px);
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
min-width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-options.show {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-option:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-option svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
.faq-list {
|
|
||||||
width: 1000px;
|
|
||||||
max-width: 90%;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-item {
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-question {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--color-fg);
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: left;
|
|
||||||
padding: 1.25rem 0.25rem;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
transition: color var(--duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-question:hover {
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-question .chevron {
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: transform var(--duration-fast) ease;
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-item.open .faq-question .chevron {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-answer {
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.4s var(--ease-out-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-answer-inner {
|
|
||||||
padding: 0 0.25rem 1.25rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-item.open .faq-answer {
|
|
||||||
max-height: 320px;
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
.marquee-fade-wrap {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-fade-wrap::before,
|
|
||||||
.marquee-fade-wrap::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: clamp(3rem, 12vw, 10rem);
|
|
||||||
z-index: var(--z-marquee-fade);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-fade-wrap::before {
|
|
||||||
left: 0;
|
|
||||||
background: linear-gradient(to right, var(--color-bg), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-fade-wrap::after {
|
|
||||||
right: 0;
|
|
||||||
background: linear-gradient(to left, var(--color-bg), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-row {
|
|
||||||
overflow: visible;
|
|
||||||
width: 100%;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-track {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
width: max-content;
|
|
||||||
transform: translateZ(0);
|
|
||||||
backface-visibility: hidden;
|
|
||||||
will-change: transform;
|
|
||||||
contain: layout style paint;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-left .marquee-track {
|
|
||||||
transform: translateX(var(--marquee-x-1, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-right .marquee-track {
|
|
||||||
transform: translateX(var(--marquee-x-2, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.marquee-track {
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Performance optimization: GPU acceleration */
|
|
||||||
.marquee-row {
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
.yt-modal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: var(--z-modal);
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: clamp(1rem, 4vw, 2.5rem);
|
|
||||||
padding-top: calc(clamp(1rem, 4vw, 2.5rem));
|
|
||||||
background: rgba(0, 0, 0, 0.82);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-modal[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-modal:not([hidden]) {
|
|
||||||
display: flex;
|
|
||||||
animation: yt-fade var(--duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-modal-backdrop {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
cursor: zoom-out;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-modal-dialog {
|
|
||||||
position: relative;
|
|
||||||
width: min(1040px, 100%);
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
overflow: hidden;
|
|
||||||
background: #000;
|
|
||||||
box-shadow: var(--shadow-modal);
|
|
||||||
animation: yt-pop 0.28s var(--ease-standard);
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-modal-dialog iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-modal-close {
|
|
||||||
position: absolute;
|
|
||||||
top: -46px;
|
|
||||||
right: 0;
|
|
||||||
z-index: 2;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
transition: background var(--duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-modal-close:hover {
|
|
||||||
background: var(--color-fg);
|
|
||||||
color: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes yt-fade {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes yt-pop {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.94);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.yt-modal-close {
|
|
||||||
top: calc(8px);
|
|
||||||
right: 8px;
|
|
||||||
background: rgba(0, 0, 0, 0.75);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
.partners-inline {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.85rem;
|
|
||||||
padding-bottom: 3rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partners-label {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
opacity: 0.75;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partners-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: clamp(1.5rem, 4vw, 2.75rem);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partner-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partner-item img {
|
|
||||||
height: 30px;
|
|
||||||
width: auto;
|
|
||||||
object-fit: contain;
|
|
||||||
filter: brightness(0) invert(1);
|
|
||||||
opacity: 0.85;
|
|
||||||
margin-top: 12px;
|
|
||||||
transition: opacity var(--duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partner-item:hover img {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partner-item a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.partners-inline {
|
|
||||||
padding-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partners-grid {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partner-item img {
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
.discord-copy-bar {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.65rem 1.25rem;
|
|
||||||
height: 50px !important;
|
|
||||||
border-radius: 50px !important;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
max-width: 90vw;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-discord-label,
|
|
||||||
.discord-copy-actions {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-discord-label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon .check-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon.copied .copy-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon.copied .check-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--color-fg);
|
|
||||||
transition: background var(--duration-fast) ease, color var(--duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon svg {
|
|
||||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon .check-icon {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon.copied .copy-icon {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon.copied .check-icon {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.copy-discord-label {
|
|
||||||
max-width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-bar {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.4rem 0.4rem 0.4rem 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-copy-icon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.platform-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.45rem 1.1rem;
|
|
||||||
border-radius: 50px;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.platform-pill svg {
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
color: var(--color-fg);
|
|
||||||
fill: currentColor;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.platform-pill span {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.platform-divider {
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mac-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
.scripts-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
|
|
||||||
gap: clamp(1.25rem, 3vw, 1.75rem);
|
|
||||||
margin-top: clamp(2rem, 6vw, 3rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
isolation: isolate;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
transform: translateZ(0) scale(1);
|
|
||||||
transition: transform 220ms cubic-bezier(.2, .8, .2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-card:hover {
|
|
||||||
transform: translateZ(0) scale(.92);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-glow {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 5%;
|
|
||||||
width: 90%;
|
|
||||||
height: 55%;
|
|
||||||
z-index: -1;
|
|
||||||
opacity: 0 !important;
|
|
||||||
filter: blur(36px) saturate(1.5);
|
|
||||||
background: radial-gradient(circle at 35% 30%, var(--glow-a, transparent) 0%, transparent 65%),
|
|
||||||
radial-gradient(circle at 70% 40%, var(--glow-b, transparent) 0%, transparent 70%);
|
|
||||||
transition: opacity 0.9s var(--ease-standard);
|
|
||||||
pointer-events: none;
|
|
||||||
will-change: opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-image {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
background: var(--color-border);
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
-webkit-mask-image: linear-gradient(to top, transparent 0%, black 45%);
|
|
||||||
mask-image: linear-gradient(to top, transparent 0%, black 45%);
|
|
||||||
transition: transform var(--duration-base) var(--ease-out-soft);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: filter var(--duration-base) ease, transform var(--duration-base) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-title {
|
|
||||||
font-size: clamp(1.0625rem, 2.2vw, 1.3125rem);
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-copy-icon {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-copy-icon svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--color-muted);
|
|
||||||
transition: transform var(--duration-fast) var(--ease-out-soft), color var(--duration-fast) ease, opacity 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-copy-icon .check-icon {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-copy-icon.copied .copy-icon {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-copy-icon.copied .check-icon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-card:hover .script-copy-icon svg {
|
|
||||||
color: var(--color-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-desc {
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.scripts-wrap {
|
|
||||||
position: relative;
|
|
||||||
margin-inline: calc(var(--content-padding-inline) * -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scripts-grid {
|
|
||||||
display: flex;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
scroll-snap-type: x mandatory;
|
|
||||||
scroll-padding-inline: var(--content-padding-inline);
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
scrollbar-width: none;
|
|
||||||
padding-inline: var(--content-padding-inline);
|
|
||||||
scroll-snap-stop: always;
|
|
||||||
overscroll-behavior-x: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scripts-wrap::before,
|
|
||||||
.scripts-wrap::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: clamp(1.5rem, 6vw, 3rem);
|
|
||||||
z-index: 100;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scripts-wrap::before {
|
|
||||||
left: 0;
|
|
||||||
background: linear-gradient(to right, var(--color-bg), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scripts-wrap::after {
|
|
||||||
right: 0;
|
|
||||||
background: linear-gradient(to left, var(--color-bg), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scripts-grid::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-card {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: calc(min(78vw, 300px) * 1.2);
|
|
||||||
scroll-snap-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-card:hover {
|
|
||||||
transform: translateZ(0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.script-card.glow-ready.in-snap .script-glow {
|
|
||||||
opacity: 0.6 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: none) and (pointer: coarse) {
|
|
||||||
.script-image img {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
.stats-grid {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: clamp(1.75rem, 5vw, 3.5rem);
|
|
||||||
text-align: center;
|
|
||||||
margin-top: clamp(1.5rem, 4vw, 2.25rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.375rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: clamp(1.5rem, 3.5vw, 2.25rem);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
color: var(--color-fg);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-muted);
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
.testimonial-card {
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
width: clamp(240px, 30vw, 320px);
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-card:hover {
|
|
||||||
background: var(--color-bg-elevated);
|
|
||||||
border-color: var(--color-border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-top {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-user {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.625rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-avatar {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--color-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-fg);
|
|
||||||
flex-shrink: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-avatar img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-names {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-name {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-handle {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-text {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
.footer {
|
|
||||||
position: relative;
|
|
||||||
padding: 32px 24px 24px;
|
|
||||||
overflow: visible;
|
|
||||||
background: transparent;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto 32px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 40px;
|
|
||||||
font-weight: 400;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section h2 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 7px;
|
|
||||||
color: var(--color-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section p {
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--color-muted);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
transition: color .2s;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section a:hover {
|
|
||||||
color: var(--color-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom p {
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
.nav {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 1.25rem 0;
|
|
||||||
z-index: var(--z-nav);
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding-inline: var(--content-padding-inline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
height: 50px;
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links {
|
|
||||||
gap: 1.25rem;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 100px !important;
|
|
||||||
height: 50px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-fg);
|
|
||||||
transition: color var(--duration-fast) ease;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
touch-action: manipulation;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a:hover,
|
|
||||||
.nav-links a:focus-visible {
|
|
||||||
color: var(--color-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.nav-links {
|
|
||||||
padding: 0rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.nav {
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: none) and (pointer: coarse) {
|
|
||||||
.nav-links a {
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
min-height: 44px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/* Font loading is handled in HTML with preconnect for better performance */
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
body {
|
|
||||||
background-color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
width: min(900px, calc(100% - 2 * var(--content-padding-inline)));
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: var(--space-3xl) 0 var(--space-4xl);
|
|
||||||
color: var(--color-fg);
|
|
||||||
font-family: var(--font-body);
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper h2 {
|
|
||||||
margin: 0 0 var(--space-xs);
|
|
||||||
color: var(--color-fg);
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper h3 {
|
|
||||||
margin: var(--space-2xl) 0 var(--space-sm);
|
|
||||||
color: var(--color-fg);
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 650;
|
|
||||||
letter-spacing: -0.015em;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper p {
|
|
||||||
margin: 0 0 var(--space-lg);
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper>p:first-of-type {
|
|
||||||
margin-bottom: var(--space-2xl);
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper strong {
|
|
||||||
color: var(--color-fg);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper ul {
|
|
||||||
margin: calc(var(--space-xs) * -1) 0 var(--space-lg);
|
|
||||||
padding-left: var(--space-xl);
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper li {
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
padding-left: var(--space-3xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper li::marker {
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.content-wrapper {
|
|
||||||
width: calc(100% - 2 * var(--content-padding-inline));
|
|
||||||
padding-top: var(--space-2xl);
|
|
||||||
padding-bottom: var(--space-3xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper h2 {
|
|
||||||
font-size: 1.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper h3 {
|
|
||||||
margin-top: var(--space-xl);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper p,
|
|
||||||
.content-wrapper li {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
.cta-footer-wrap {
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
isolation: isolate;
|
|
||||||
background: #000000;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-rays-container {
|
|
||||||
position: absolute;
|
|
||||||
inset: -50vh 0 0 0;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% + 50vh);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-title-warp {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
height: clamp(60px, 10vw, 110px);
|
|
||||||
margin: 0 auto 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner {
|
|
||||||
position: relative;
|
|
||||||
padding: clamp(5rem, 10vw, 8rem) 0 3rem;
|
|
||||||
overflow: visible;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: transparent;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-title {
|
|
||||||
font-size: clamp(2rem, 5vw, 3rem);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
color: var(--color-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-subtitle {
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-content .download-expandable {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-content .download-expandable .hero-cta {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-content .download-options {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-content .download-option {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-content .download-option:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner-content .download-option svg {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
.executors-section {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.executors-heading {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: clamp(2rem, 6vw, 3rem);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.faq-heading {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: clamp(2rem, 6vw, 3.5rem);
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
.features-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: clamp(3.5rem, 8vw, 6rem);
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-block {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
align-items: center;
|
|
||||||
gap: clamp(2rem, 6vw, 5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-block-reverse {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-block-reverse .feature-media {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-block-reverse .feature-copy {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-media {
|
|
||||||
width: min(100%, 800px);
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-media img,
|
|
||||||
.feature-media video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-copy {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
max-width: 420px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-title {
|
|
||||||
text-align: left;
|
|
||||||
color: var(--color-fg);
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: clamp(1.75rem, 3vw, 2.25rem);
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-copy .section-subtitle {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
|
|
||||||
.features-container {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-media {
|
|
||||||
width: 100% !important;
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
max-height: 420px !important;
|
|
||||||
height: 200px !important;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
overflow: hidden;
|
|
||||||
mask-composite: intersect;
|
|
||||||
mask-image: linear-gradient(to top, transparent, black 50%), linear-gradient(to top, transparent, black 20%);
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: 100% 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-block,
|
|
||||||
.feature-block-reverse {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-block-reverse .feature-media,
|
|
||||||
.feature-block-reverse .feature-copy {
|
|
||||||
order: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
.hero {
|
|
||||||
position: relative;
|
|
||||||
padding: clamp(6rem, 14vh, 8.5rem) 0 clamp(3rem, 6vh, 5rem);
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-atmosphere {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-mask-image: linear-gradient(to bottom,
|
|
||||||
var(--video-dominant-color, black) 0%,
|
|
||||||
var(--video-dominant-color, black) 48%,
|
|
||||||
var(--video-dominant-color-transparent, rgba(0, 0, 0, 0.5)) 80%,
|
|
||||||
transparent 96%);
|
|
||||||
mask-image: linear-gradient(to bottom,
|
|
||||||
var(--video-dominant-color, black) 0%,
|
|
||||||
var(--video-dominant-color, black) 48%,
|
|
||||||
var(--video-dominant-color-transparent, rgba(0, 0, 0, 0.5)) 80%,
|
|
||||||
transparent 96%);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.hero-prism {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-atmosphere-video {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
.hero-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
padding-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-image {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title-warp {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 900px;
|
|
||||||
height: clamp(80px, 14vw, 150px);
|
|
||||||
margin: 0.5rem auto 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warp-text {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warp-text canvas {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
font-size: 2.3rem;
|
|
||||||
letter-spacing: -.03em;
|
|
||||||
font-weight: 500;
|
|
||||||
text-wrap: balance;
|
|
||||||
color: var(--color-fg);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
min-width: 440px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title .char {
|
|
||||||
display: inline-block;
|
|
||||||
opacity: 0;
|
|
||||||
animation: char-in 0.4s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-description {
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin: 1rem auto 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
width: 50%;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-description a {
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes char-in {
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.hero-title .char {
|
|
||||||
animation: none;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hero-description {
|
|
||||||
margin-top: 0rem;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-height: 600px) {
|
|
||||||
.hero-title {
|
|
||||||
margin-bottom: 0rem;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-container {
|
|
||||||
padding-top: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-preview-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 980px;
|
|
||||||
margin-top: 2.25rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-screen-mockup {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-screen-mockup img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hero-preview-wrap {
|
|
||||||
margin-top: 1.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-width: 1024px) {
|
|
||||||
.hero-screen-mockup {
|
|
||||||
width: 60%;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
.section {
|
|
||||||
padding: clamp(2.5rem, 6vw, 4.5rem) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack span {
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
padding: 0.4rem 0.875rem;
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.section {
|
|
||||||
padding: 3rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-height: 600px) {
|
|
||||||
.section {
|
|
||||||
padding: 3rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
.testimonials-section {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonials-container {
|
|
||||||
margin-bottom: clamp(2rem, 6vw, 3.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonials-heading {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marquee-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-grid {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 90vw;
|
|
||||||
height: 55vh;
|
|
||||||
fill: rgba(255, 255, 255, 0.14);
|
|
||||||
pointer-events: none;
|
|
||||||
-webkit-mask-image: linear-gradient(to bottom right, white, transparent 65%);
|
|
||||||
mask-image: linear-gradient(to bottom right, white, transparent 65%);
|
|
||||||
z-index: var(--z-dot-grid);
|
|
||||||
}
|
|
||||||
|
|
||||||
.visually-hidden {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
@media (max-width: 480px) {
|
|
||||||
html {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack span {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.35rem 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (orientation: landscape) and (min-width: 600px) and (max-height: 500px) {
|
|
||||||
.container {
|
|
||||||
width: 70%;
|
|
||||||
padding-inline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scripts-grid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-banner {
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
height: 150vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: none) and (pointer: coarse) {
|
|
||||||
.script-links a {
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
.reveal {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate3d(0, 24px, 0);
|
|
||||||
transition: opacity var(--duration-slow) var(--ease-standard),
|
|
||||||
transform var(--duration-slow) var(--ease-standard);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal.in-view {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
will-change: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal-stagger .reveal:nth-child(1) {
|
|
||||||
transition-delay: 0ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal-stagger .reveal:nth-child(2) {
|
|
||||||
transition-delay: 80ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal-stagger .reveal:nth-child(3) {
|
|
||||||
transition-delay: 160ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal-stagger .reveal:nth-child(4) {
|
|
||||||
transition-delay: 240ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal-stagger .reveal:nth-child(5) {
|
|
||||||
transition-delay: 320ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal-stagger .reveal:nth-child(6) {
|
|
||||||
transition-delay: 400ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal-stagger .reveal:nth-child(7) {
|
|
||||||
transition-delay: 480ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.reveal {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -86,34 +86,8 @@
|
|||||||
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/base/tokens.css">
|
<link rel="stylesheet" href="/src/css/main.css">
|
||||||
<link rel="stylesheet" href="/src/styles/base/reset.css">
|
<link rel="stylesheet" href="/src/css/other.css">
|
||||||
<link rel="stylesheet" href="/src/styles/base/typography.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/layout.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/reveal.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/layout/nav.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/hero-atmosphere.css">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/hero-content.css">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/layout/footer.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/buttons.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/pills.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/stats.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/script-card.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/marquee.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/testimonial-card.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/modal.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/partners.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/components/faq.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/section-base.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/testimonials.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/features.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/faq-section.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/sections/cta-banner.css" media="print" onload="this.media='all'">
|
|
||||||
<link rel="stylesheet" href="/src/styles/utilities/responsive.css" media="print" onload="this.media='all'">
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/styles/pages/other.css" />
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
@@ -241,14 +215,6 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header role="banner">
|
|
||||||
<nav class="nav" role="navigation" aria-label="Main navigation">
|
|
||||||
<div class="logo-image">
|
|
||||||
<img src="/assets/images/icons/logo_white.png" alt="Vyla Home">
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<h2>Terms of Service</h2>
|
<h2>Terms of Service</h2>
|
||||||
@@ -301,14 +267,18 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="cta-footer-wrap">
|
<div class="cta-footer-wrap">
|
||||||
<div id="cta-rays" class="cta-rays-container" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<section class="cta-banner" aria-labelledby="cta-heading">
|
<section class="cta-banner" aria-labelledby="cta-heading">
|
||||||
<div class="container cta-banner-content reveal">
|
<div class="cta-atmosphere" aria-hidden="true">
|
||||||
|
<video class="cta-atmosphere-video" autoplay muted loop playsinline preload="auto">
|
||||||
|
<source src="https://www.pexels.com/download/video/38243003/" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div class="container cta-banner-content">
|
||||||
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
|
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
|
||||||
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
||||||
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
|
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
|
||||||
<button class="hero-cta hero-cta-light" id="cta-download-btn">
|
<button class="hero-cta hero-cta-light" id="cta-download-btn" type="button" aria-haspopup="true"
|
||||||
|
aria-controls="cta-download-options">
|
||||||
<span class="download-btn-text">Download Now</span>
|
<span class="download-btn-text">Download Now</span>
|
||||||
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
@@ -316,123 +286,44 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="download-options" id="cta-download-options" aria-hidden="true">
|
<div class="download-options" id="cta-download-options" aria-hidden="true">
|
||||||
<a href="#" class="download-option" id="cta-windows-download">
|
<a href="#" class="download-option" id="cta-windows-download" aria-label="Download for Windows">
|
||||||
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="windows-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.949-1.951" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Windows v0.0.0</span>
|
<span>Windows</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="cta-mac-download">
|
<a href="#" class="download-option" id="cta-mac-download" aria-label="Download for macOS">
|
||||||
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="mac-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Mac v0.0.0</span>
|
<span>macOS</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="download-option" id="cta-go-download">
|
<a href="#" class="download-option" id="cta-go-download" aria-label="Download for Android">
|
||||||
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
<svg class="android-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
d="M17.6 9.2l1.5-2.6c.1-.2.1-.4-.1-.5-.2-.1-.4-.1-.5.1L17 8.8c-.9-.4-1.9-.6-3-.6s-2.1.2-3 .6L9.5 6.2c-.1-.2-.3-.2-.5-.1-.2.1-.2.3-.1.5l1.5 2.6C8.1 10.2 6.4 12.1 6 14.5h12c-.4-2.4-2.1-4.3-4.4-5.3zM9.5 13.2c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zm5 0c-.4 0-.7-.3-.7-.7s.3-.7.7-.7.7.3.7.7-.3.7-.7.7zM6 15h12v3.5c0 .8-.7 1.5-1.5 1.5h-9c-.8 0-1.5-.7-1.5-1.5V15zm2 5v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2zm10 0v2c0 .6-.4 1-1 1s-1-.4-1-1v-2h2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Android v0.0.0</span>
|
<span>Android</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
async function setupExpandableDownload(expandableId, btnId, optionsId, windowsId, goId, macId) {
|
|
||||||
const expandable = document.getElementById(expandableId);
|
|
||||||
const downloadBtn = document.getElementById(btnId);
|
|
||||||
const downloadOptions = document.getElementById(optionsId);
|
|
||||||
const windowsDownload = document.getElementById(windowsId);
|
|
||||||
const goDownload = document.getElementById(goId);
|
|
||||||
const macDownload = document.getElementById(macId);
|
|
||||||
if (!expandable || !downloadBtn || !downloadOptions || !windowsDownload || !goDownload || !macDownload) return;
|
|
||||||
try {
|
|
||||||
const [homeResponse, goResponse] = await Promise.all([
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"),
|
|
||||||
fetch("https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases/permalink/latest")
|
|
||||||
]);
|
|
||||||
if (!homeResponse.ok) throw new Error("Failed to fetch latest home release");
|
|
||||||
if (!goResponse.ok) throw new Error("Failed to fetch latest go release");
|
|
||||||
const homeRelease = await homeResponse.json();
|
|
||||||
const goRelease = await goResponse.json();
|
|
||||||
const version = homeRelease.tag_name;
|
|
||||||
const goVersion = goRelease.tag_name;
|
|
||||||
const fileVersion = version.replace(/^v/, "");
|
|
||||||
const goFileVersion = goVersion.replace(/^v/, "");
|
|
||||||
const windowsAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".exe"));
|
|
||||||
const macAsset = homeRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".dmg"));
|
|
||||||
const goAsset = goRelease.assets?.links?.find(link => link.name?.toLowerCase().endsWith(".apk"));
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = `Download ${version}`;
|
|
||||||
}
|
|
||||||
if (windowsAsset?.url) {
|
|
||||||
windowsDownload.href = windowsAsset.url;
|
|
||||||
} else {
|
|
||||||
windowsDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = `Windows ${version}`;
|
|
||||||
if (goAsset?.url) {
|
|
||||||
goDownload.href = goAsset.url;
|
|
||||||
} else {
|
|
||||||
goDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/packages/generic/vyla-go/${goVersion}/vyla-go-${goFileVersion}.apk`;
|
|
||||||
}
|
|
||||||
goDownload.querySelector("span").textContent = `Android ${goVersion}`;
|
|
||||||
if (macAsset?.url) {
|
|
||||||
macDownload.href = macAsset.url;
|
|
||||||
} else {
|
|
||||||
macDownload.href = `https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_aarch64.dmg`;
|
|
||||||
}
|
|
||||||
macDownload.querySelector("span").textContent = `Mac ${version}`;
|
|
||||||
downloadBtn.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const isExpanded = expandable.getAttribute("aria-expanded") === "true";
|
|
||||||
expandable.setAttribute("aria-expanded", String(!isExpanded));
|
|
||||||
downloadOptions.setAttribute("aria-hidden", String(isExpanded));
|
|
||||||
downloadOptions.classList.toggle("show", !isExpanded);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const btnText = downloadBtn.querySelector(".download-btn-text");
|
|
||||||
if (btnText) {
|
|
||||||
btnText.textContent = "Download Latest Release";
|
|
||||||
}
|
|
||||||
windowsDownload.querySelector("span").textContent = "Windows (Latest)";
|
|
||||||
goDownload.querySelector("span").textContent = "Android (Latest)";
|
|
||||||
macDownload.querySelector("span").textContent = "Mac (Latest)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setupExpandableDownload("cta-download-expandable", "cta-download-btn", "cta-download-options", "cta-windows-download", "cta-go-download", "cta-mac-download");
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
const expandables = document.querySelectorAll(".download-expandable");
|
|
||||||
expandables.forEach(expandable => {
|
|
||||||
const btn = expandable.querySelector("button");
|
|
||||||
const options = expandable.querySelector(".download-options");
|
|
||||||
if (btn && options && !expandable.contains(e.target)) {
|
|
||||||
expandable.setAttribute("aria-expanded", "false");
|
|
||||||
options.setAttribute("aria-hidden", "true");
|
|
||||||
options.classList.remove("show");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer" role="contentinfo">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Vyla</h2>
|
<h2>Vyla</h2>
|
||||||
<p>Vyla is a passion project born from a love for film and television.</p>
|
<p>Vyla is a passion project born from a love for film and television.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Backup Site:</h2>
|
<h2>Backup Site</h2>
|
||||||
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
|
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener noreferrer">https://vyla.qzz.io</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
@@ -440,33 +331,32 @@
|
|||||||
<a href="/terms-of-service">Terms of Service</a>
|
<a href="/terms-of-service">Terms of Service</a>
|
||||||
<a href="/privacy-policy">Privacy Policy</a>
|
<a href="/privacy-policy">Privacy Policy</a>
|
||||||
<a href="/about-us">About Us</a>
|
<a href="/about-us">About Us</a>
|
||||||
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener">DMCA</a>
|
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener noreferrer">DMCA</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Our API</h2>
|
<h2>Developers</h2>
|
||||||
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
|
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer">Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<h2>Join our Discord</h2>
|
<h2>Community</h2>
|
||||||
<div class="social-links">
|
<div class="social-links">
|
||||||
<a href="/discord" target="_blank" rel="noopener">
|
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
|
||||||
<i class="fab fa-discord"></i> Discord
|
aria-label="Official Discord Server">Discord</a>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
|
<div class="footer-section footer-badge">
|
||||||
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
|
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
|
||||||
height="95" loading="lazy" decoding="async">
|
alt="This website runs on green hosting verified by thegreenwebfoundation.org" width="200"
|
||||||
|
height="95" loading="lazy" decoding="async">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
|
<p>© 2025 Vyla Entertainment. All rights reserved.</p>
|
||||||
© 2025 Vyla Entertainment. All rights reserved.
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/scripts/main.js"></script>
|
<script type="module" src="/src/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user