Compare commits

9 Commits
main ... main

83 changed files with 636 additions and 5279 deletions

View File

@@ -1,49 +0,0 @@
# SEO and Performance Optimization for Vyla Home
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>
# Set cache expiration for static assets
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/x-javascript "access plus 1 month"
ExpiresByType application/json "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/ttf "access plus 1 year"
ExpiresByType font/eot "access plus 1 year"
</IfModule>
# Security headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# Force HTTPS (uncomment if SSL is available)
# <IfModule mod_rewrite.c>
# RewriteEngine On
# RewriteCond %{HTTPS} off
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# </IfModule>
# Redirect www to non-www (or vice versa)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.vyla\.cc [NC]
RewriteRule ^(.*)$ https://vyla.cc/$1 [L,R=301]
</IfModule>
# Custom error pages
ErrorDocument 404 /index.html

View File

@@ -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="/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/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" />
<link rel="stylesheet" href="/src/css/main.css">
<link rel="stylesheet" href="/src/css/other.css">
<script type="application/ld+json">
{
@@ -241,13 +215,6 @@
</head>
<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">
<div class="content-wrapper">
@@ -303,14 +270,18 @@
</main>
<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">
<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>
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
<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>
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
aria-hidden="true">
@@ -318,123 +289,44 @@
</svg>
</button>
<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"
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 v0.0.0</span>
<span>Windows</span>
</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"
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>Mac v0.0.0</span>
<span>macOS</span>
</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"
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 v0.0.0</span>
<span>Android</span>
</a>
</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>
<footer class="footer">
<footer class="footer" role="contentinfo">
<div class="footer-content">
<div class="footer-section">
<h2>Vyla</h2>
<p>Vyla is a passion project born from a love for film and television.</p>
</div>
<div class="footer-section">
<h2>Backup Site:</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
<h2>Backup Site</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener noreferrer">https://vyla.qzz.io</a>
</p>
</div>
<div class="footer-section">
@@ -442,33 +334,32 @@
<a href="/terms-of-service">Terms of Service</a>
<a href="/privacy-policy">Privacy Policy</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 class="footer-section">
<h2>Our API</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
<h2>Developers</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer">Documentation</a>
</div>
<div class="footer-section">
<h2>Join our Discord</h2>
<h2>Community</h2>
<div class="social-links">
<a href="/discord" target="_blank" rel="noopener">
<i class="fab fa-discord"></i> Discord
</a>
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
aria-label="Official Discord Server">Discord</a>
</div>
</div>
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
<div class="footer-section footer-badge">
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
alt="This website runs on green hosting verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
</div>
</div>
<div class="footer-bottom">
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
&copy; 2025 Vyla Entertainment. All rights reserved.
</a>
<p>&copy; 2025 Vyla Entertainment. All rights reserved.</p>
</div>
</footer>
</div>
<script type="module" src="/src/scripts/main.js"></script>
<script type="module" src="/src/js/main.js"></script>
</body>
</html>

18
ai-catalog.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"name": "Vyla Home",
"description": "Self-Hosted Desktop Media Center | Serverless Streaming & Entertainment Platform",
"url": "https://vyla.cc",
"ecosystem": {
"repository": "https://gitlab.com/vyla-entertainment",
"documentation": "https://docs.vyla.cc"
},
"contact": {
"discord": "https://discord.gg/5eqRhKeywG"
},
"data_exposure": "public",
"ai_permissions": {
"crawling": "allowed",
"indexing": "allowed"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

View File

@@ -1,56 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<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 &amp; Entertainment Platform</title>
<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"
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="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
<meta name="googlebot" content="index, follow">
<meta name="language" content="English">
<meta name="geo.region" content="US">
<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">
<meta name="theme-color" content="#000000">
<meta name="color-scheme" content="dark">
<link rel="canonical" href="https://vyla.cc/">
<meta name="application-name" content="Vyla Home">
<meta name="msapplication-tooltip" content="Self-hosted desktop media center for streaming entertainment">
<meta name="msapplication-starturl" content="/">
<meta name="msapplication-window" content="width=device-width;height=device-height">
<meta name="msapplication-navbutton-color" content="#FFF">
<link rel="icon" href="assets/images/favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="32x32" href="favicon.ico">
<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="manifest" href="manifest.json">
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link rel="dns-prefetch" href="https://gitlab.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">
content="Experience high-performance self-hosted streaming with Vyla Home. Direct connection, zero tracking, hardware-accelerated 4K playback.">
<meta property="og:image" content="https://vyla.cc/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">
@@ -58,179 +31,82 @@
<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">
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="favicon.ico" type="image/x-icon">
<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="apple-touch-icon" sizes="180x180" href="assets/images/apple-touch-icon.png">
<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="preconnect" href="https://gitlab.com" crossorigin>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
media="print" onload="this.media='all'">
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
</noscript>
<link rel="apple-touch-icon" href="favicon.ico">
<link rel="stylesheet" href="src/styling/main.css">
<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/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">
<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 defer src="src/javascript/fetch.js"></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"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": [
"@graph": [
{
"@type": "Question",
"name": "What is Vyla Home?",
"acceptedAnswer": {
"@type": "Answer",
"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."
"@type": "WebSite",
"@id": "https://vyla.cc/#website",
"url": "https://vyla.cc/",
"name": "Vyla Home",
"description": "Self-Hosted Desktop Media Center",
"publisher": {
"@id": "https://vyla.cc/#organization"
}
},
{
"@type": "Question",
"name": "How does Vyla Home bypass datacenter blocking?",
"acceptedAnswer": {
"@type": "Answer",
"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."
}
"@type": "Organization",
"@id": "https://vyla.cc/#organization",
"name": "Vyla Entertainment",
"url": "https://vyla.cc/",
"logo": "https://vyla.cc/assets/images/header_logo.png",
"sameAs": [
"https://gitlab.com/vyla-entertainment/home",
"https://discord.gg/5eqRhKeywG"
]
},
{
"@type": "Question",
"name": "Is Vyla Home free to use?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, Vyla Home is completely free and open-source. You can download it from our GitLab repository without any cost."
"@type": "SoftwareApplication",
"@id": "https://vyla.cc/#application",
"name": "Vyla Home",
"applicationCategory": "MultimediaApplication",
"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"
}
}
]
@@ -239,528 +115,95 @@
</head>
<body>
<header role="banner">
<nav class="nav" role="navigation" aria-label="Main navigation">
<div class="nav-container">
<div class="logo-image">
<img src="/assets/images/icons/logo_white.png" alt="Vyla Home">
</div>
<div class="nav-links">
<a href="#features">Features</a>
<a href="#testimonials">Testimonials</a>
<a href="#faq">FAQ</a>
</div>
</div>
</nav>
</header>
<video src="/" autoplay muted loop></video>
<main>
<pre>
Welcome to Vyla! I really hope you enjoy it!
</pre>
<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>
<pre>
<main role="main">
<section class="hero" aria-labelledby="hero-title">
<div class="hero-atmosphere" aria-hidden="true">
<div id="hero-prism" class="hero-prism"></div>
</div>
<div class="container hero-container">
<div class="logo-image">
<img src="assets/images/header_logo.png" alt="Vyla Home Logo" width="100" height="100"
fetchpriority="high"
srcset="assets/images/header_logo.png 100w, assets/images/header_logo.png 200w" sizes="100px">
</div>
<h1 id="hero-title" class="hero-title">The Local-First Desktop Media Center</h1>
<p class="hero-description"> Vyla Home runs directly on your local workstation. By decentralizing stream
resolution to your residential connection, it eliminates centralized cloud failures, datacenter IP
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">
<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">
<button class="hero-cta hero-cta-dark" id="hero-download-btn">
<span class="download-btn-text">Download v0.0.0</span>
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
aria-hidden="true">
<path d="M7 10l5 5 5-5z" />
</svg>
</button>
<div class="download-options" id="hero-download-options" aria-hidden="true">
<a href="#" class="download-option" id="hero-windows-download">
<svg class="windows-icon" width="16" height="16" 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 v0.0.0</span>
</a>
<a href="#" class="download-option" id="hero-go-download">
<svg class="android-icon" width="16" height="16" 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 v0.0.0</span>
</a>
<a href="#" class="download-option" id="hero-mac-download">
<svg class="mac-icon" width="16" height="16" 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>Mac v0.0.0</span>
</a>
</div>
</div>
<a class="hero-cta hero-cta-unsure" href="https://gitlab.com/vyla-entertainment/home/-/releases"
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>
Here's a little explanation of Vyla:
<span class="platform-divider">•</span>
Vyla started from the web project which initially intended to create a clean environment where one could stream movies
and TV shows free from all the unnecessary elements. Originally, it was developed mainly as a frontend which embedded
other players. With time I got involved in the process of developing the infrastructure.
<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>
Another core component of Vyla is Vyla API which dealt with searching streams from various providers, verifying their
availability and playing them. Later on, it evolved into being able to cope with many kinds of streams, deal with
possible issues related to their quality and reliability.
<span class="platform-divider">•</span>
With constant changing and breakage of providers, I had to develop Vyla SDK. The purpose of it was to isolate the source
implementations from the rest of Vyla so that they could be independently managed and used in other projects. I made
each source the provider modules which had the same interface. Vyla SDK remains updated as sources change.
<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>
Among the main challenges associated with the development of Vyla was hosting. Initially I did self-hosting of frontend,
API, player, Nginx, PM2 and other infrastructure on a VPS. Managing them was a struggle with constant downtime, provider
issues, Cloudflare problems and, lastly the DMCA complains.
However, after receiving a DMCA strike and experiencing problems due to frequent reports in Cloudflare, I figured out
that it wouldn’t be efficient to self host the streaming infrastructure. Therefore, the choice was made to focus more on
SDK and the application rather than using a remote backend.
Thanks for Reading ❤️
</pre>
<div class="download-box">
<div class="download-header">Download Vyla</div>
<div class="download-grid">
<div class="download-item">
<span class="platform-name">Windows</span>
<a id="windows-download" href="#" aria-label="Download Vyla for Windows">loading...</a>
</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 class="download-item">
<span class="platform-name">macOS</span>
<a id="mac-download" href="#" aria-label="Download Vyla for macOS">loading...</a>
</div>
<div class="download-item">
<span class="platform-name">Android</span>
<a id="android-download" href="#" aria-label="Download Vyla for Android">loading...</a>
</div>
</div>
</section>
<div class="stats-grid reveal-stagger" id="stats-grid">
<div class="stat-item reveal">
<span class="stat-value" id="stat-members"></span>
<span class="stat-label">Discord Members</span>
</div>
<div class="stat-item reveal">
<span class="stat-value" id="stat-online"></span>
<span class="stat-label">Online Now</span>
</div>
</div>
<section id="features" class="section features-section" aria-labelledby="features-heading">
<div class="container features-container">
<header class="features-heading reveal">
<h2 id="features-heading" class="section-title-lg">Why choose Home?</h2>
</header>
<article class="feature-block reveal">
<figure class="feature-media">
<img src="assets/images/features/1.png"
alt="Diagram showing datacenter IP throttling bypass with residential network connection"
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>
<div class="feature-copy">
<h3 class="section-title-lg feature-title">No Datacenter Throttling</h3>
<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
residential network. </p>
</div>
</article>
<article class="feature-block feature-block-reverse reveal">
<figure class="feature-media">
<img src="assets/images/features/2.png"
alt="Diagram showing decentralized architecture eliminating single point of failure"
width="800" height="450" loading="lazy" decoding="async"
srcset="assets/images/features/2.png 800w, assets/images/features/2.png 400w"
sizes="(max-width: 768px) 400px, 800px">
</figure>
<div class="feature-copy">
<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
dark. Running locally means your instance cannot be taken down by third parties. </p>
</div>
</article>
<article class="feature-block reveal">
<figure class="feature-media">
<img src="assets/images/features/3.png"
alt="Illustration of encrypted local vault with hardware profile key" width="800"
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>
<div class="feature-copy">
<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
an encrypted vault keyed uniquely to your hardware profile. Total privacy. </p>
</div>
</article>
<article class="feature-block feature-block-reverse reveal">
<figure class="feature-media">
<img src="assets/images/features/4.png"
alt="Interface showing automated over-the-air update process" width="800" height="450"
loading="lazy" decoding="async"
srcset="assets/images/features/4.png 800w, assets/images/features/4.png 400w"
sizes="(max-width: 768px) 400px, 800px">
</figure>
<div class="feature-copy">
<h3 class="section-title-lg feature-title">Automated OTA Updates</h3>
<p class="section-subtitle"> Vyla Home verifies releases directly from the repository upon
startup. Update assets are downloaded, checksum-verified, and patched automatically. </p>
</div>
</article>
<article class="feature-block reveal">
<figure class="feature-media">
<img src="assets/images/features/5.png"
alt="Interface showing rich media discovery with curated collections and 4K content"
width="800" 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>
<div class="feature-copy">
<h3 class="section-title-lg feature-title">Rich Discovery & Curated Media</h3>
<p class="section-subtitle"> Access curated collections, 4K releases, popular anime series,
soundcloak music streaming, and detailed TMDB cast breakdowns seamlessly. </p>
</div>
</article>
<article class="feature-block feature-block-reverse reveal">
<figure class="feature-media">
<img src="assets/images/features/6.png"
alt="Interface showing customizable theme and layout options" width="800" 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>
<div class="feature-copy">
<h3 class="section-title-lg feature-title">Customizable Interface</h3>
<p class="section-subtitle"> Toggle dynamic color adaptations, trailer background playback,
custom color themes, text-only mode, and layout preferences with full persistent storage.
</p>
</div>
</article>
</div>
</section>
<section id="testimonials" class="section testimonials-section" aria-labelledby="testimonials-heading">
<div class="container testimonials-container">
<header class="testimonials-heading reveal">
<h2 id="testimonials-heading" class="section-title-lg">Here's what people say about <span
class="bold">Vyla Home</span>
</h2>
</header>
<div class="marquee-wrap marquee-fade-wrap" role="region" aria-label="User testimonials carousel">
<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>
</section>
<section id="faq" class="section faq-section" aria-labelledby="faq-heading">
<div class="container">
<header class="faq-heading reveal">
<h2 id="faq-heading" class="section-title-lg">FAQ</h2>
<p class="section-subtitle">Still have questions? <a href="https://discord.gg/5eqRhKeywG"
target="_blank" rel="noopener noreferrer">Ask in our Discord</a>
</p>
</header>
<div class="faq-list reveal-stagger" id="faq-list" role="list" aria-label="Frequently asked questions">
</div>
</div>
</section>
</main>
<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">
<div class="container cta-banner-content reveal">
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
<div class="download-expandable" id="cta-download-expandable" aria-expanded="false">
<button class="hero-cta hero-cta-light" id="cta-download-btn">
<span class="download-btn-text">Download Now</span>
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
aria-hidden="true">
<path d="M7 10l5 5 5-5z" />
</svg>
</button>
<div class="download-options" id="cta-download-options" aria-hidden="true">
<a href="#" class="download-option" id="cta-windows-download">
<svg class="windows-icon" width="16" height="16" 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 v0.0.0</span>
</a>
<a href="#" class="download-option" id="cta-mac-download">
<svg class="mac-icon" width="16" height="16" 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>Mac v0.0.0</span>
</a>
<a href="#" class="download-option" id="cta-go-download">
<svg class="android-icon" width="16" height="16" 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 v0.0.0</span>
</a>
</div>
</div>
<!---
<pre>Screenshots</pre>
<div class="gallery">
<img src="assets/images/features/1.webp" alt="Feature 1" width="1920" height="1080" fetchpriority="high"
decoding="async">
<img src="assets/images/features/2.webp" alt="Feature 2" width="2075" height="1205" loading="lazy"
decoding="async">
<img src="assets/images/features/3.webp" alt="Feature 3" width="1805" height="1185" loading="lazy"
decoding="async">
<img src="assets/images/features/4.webp" alt="Feature 4" width="1679" height="1066" loading="lazy"
decoding="async">
<img src="assets/images/features/5.webp" alt="Feature 5" width="2078" height="1207" loading="lazy"
decoding="async">
<img src="assets/images/features/6.webp" alt="Feature 6" width="2071" height="1195" loading="lazy"
decoding="async">
</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>
<pre id="reviews-output"></pre>
<pre id="partners-output"></pre>
<pre id="faq-output"></pre>
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h2>Vyla</h2>
<p>Vyla is a passion project born from a love for film and television.</p>
</div>
<div class="footer-section">
<h2>Backup Site:</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
</p>
</div>
<div class="footer-section">
<h2>Quick Links</h2>
<a href="/terms-of-service">Terms of Service</a>
<a href="/privacy-policy">Privacy Policy</a>
<a href="/about-us">About Us</a>
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener">DMCA</a>
</div>
<div class="footer-section">
<h2>Our API</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
</div>
<div class="footer-section">
<h2>Join our Discord</h2>
<div class="social-links">
<a href="/discord" target="_blank" rel="noopener">
<i class="fab fa-discord"></i> Discord
</a>
</div>
</div>
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
</div>
<div class="footer-bottom">
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
&copy; 2025 Vyla Entertainment. All rights reserved.
</a>
<pre>Thanks for reading :-P</pre>
<div class="links">
<pre></pre>
<a href="https://gitlab.com/vyla-entertainment" target="_blank" rel="noopener noreferrer"
aria-label="GitLab project repository">Check out our Ecosystem on Gitlab</a>
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
aria-label="Vyla Discord community server">Discord</a>
<pre></pre>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer"
aria-label="Vyla documentation website">Docs</a>
<pre></pre>
We have a nice API so you can create your own Vyla.
</div>
</footer>
</div>
<script type="module" src="src/scripts/main.js"></script>
</main>
</body>
</html>

View File

@@ -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="/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/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" />
<link rel="stylesheet" href="/src/css/main.css">
<link rel="stylesheet" href="/src/css/other.css">
<script type="application/ld+json">
{
@@ -241,14 +215,6 @@
</head>
<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">
<div class="content-wrapper">
<h2>Privacy Policy</h2>
@@ -314,14 +280,18 @@
</main>
<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">
<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>
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
<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>
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
aria-hidden="true">
@@ -329,123 +299,44 @@
</svg>
</button>
<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"
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 v0.0.0</span>
<span>Windows</span>
</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"
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>Mac v0.0.0</span>
<span>macOS</span>
</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"
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 v0.0.0</span>
<span>Android</span>
</a>
</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>
<footer class="footer">
<footer class="footer" role="contentinfo">
<div class="footer-content">
<div class="footer-section">
<h2>Vyla</h2>
<p>Vyla is a passion project born from a love for film and television.</p>
</div>
<div class="footer-section">
<h2>Backup Site:</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
<h2>Backup Site</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener noreferrer">https://vyla.qzz.io</a>
</p>
</div>
<div class="footer-section">
@@ -453,33 +344,32 @@
<a href="/terms-of-service">Terms of Service</a>
<a href="/privacy-policy">Privacy Policy</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 class="footer-section">
<h2>Our API</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
<h2>Developers</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer">Documentation</a>
</div>
<div class="footer-section">
<h2>Join our Discord</h2>
<h2>Community</h2>
<div class="social-links">
<a href="/discord" target="_blank" rel="noopener">
<i class="fab fa-discord"></i> Discord
</a>
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
aria-label="Official Discord Server">Discord</a>
</div>
</div>
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
<div class="footer-section footer-badge">
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
alt="This website runs on green hosting verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
</div>
</div>
<div class="footer-bottom">
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
&copy; 2025 Vyla Entertainment. All rights reserved.
</a>
<p>&copy; 2025 Vyla Entertainment. All rights reserved.</p>
</div>
</footer>
</div>
<script type="module" src="/src/scripts/main.js"></script>
<script type="module" src="/src/js/main.js"></script>
</body>
</html>

26
src/data/faq.json Normal file
View 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."
}
]

View File

@@ -40,5 +40,26 @@
"text": "ur like the best owner ever❤️",
"avatar": "/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"
}
]

View File

@@ -1,16 +0,0 @@
[
"https://www.youtube.com/watch?v=vpBXODI5kus",
"https://www.youtube.com/watch?v=HBfYn84lctI",
"https://www.youtube.com/watch?v=5pKF3K3QC8g",
"https://www.youtube.com/watch?v=qCcsW2LT324",
"https://www.youtube.com/watch?v=Ww5U6JD_OZ8",
"https://www.youtube.com/watch?v=CQv6dzqLI-w",
"https://www.youtube.com/watch?v=RpfPlfCa158",
"https://www.youtube.com/watch?v=YM2Z3-L2DK8",
"https://www.youtube.com/watch?v=gr0u8UHA_tU",
"https://www.youtube.com/watch?v=vx_SfuS75rM",
"https://www.youtube.com/watch?v=TOMWYVA_iNE",
"https://www.youtube.com/watch?v=tjAbSxEcq_A",
"https://www.youtube.com/watch?v=Dwzb2iRf8_k",
"https://www.youtube.com/watch?v=ZavHJRW4yWg"
]

139
src/javascript/fetch.js Normal file
View File

@@ -0,0 +1,139 @@
const homeReleases = "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases?per_page=100";
const goReleases = "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases?per_page=100";
async function loadDownloads() {
try {
const [homeResponse, goResponse] = await Promise.all([
fetch(homeReleases),
fetch(goReleases)
]);
if (!homeResponse.ok || !goResponse.ok) {
throw new Error("Failed to fetch GitLab releases");
}
const [homeReleasesData, goReleasesData] = await Promise.all([
homeResponse.json(),
goResponse.json()
]);
let windowsUrl = null;
let macUrl = null;
let androidUrl = null;
for (const release of homeReleasesData) {
const assets = release.assets?.links || [];
const windows = assets.find(asset =>
/\.exe$/i.test(asset.name) ||
/\.exe$/i.test(asset.url)
);
const mac = assets.find(asset =>
/\.dmg$/i.test(asset.name) ||
/\.dmg$/i.test(asset.url)
);
if (!windowsUrl && windows) {
windowsUrl = windows.url;
}
if (!macUrl && mac) {
macUrl = mac.url;
}
if (windowsUrl && macUrl) {
break;
}
}
for (const release of goReleasesData) {
const assets = release.assets?.links || [];
const android = assets.find(asset =>
/\.apk$/i.test(asset.name) ||
/\.apk$/i.test(asset.url)
);
if (android) {
androidUrl = android.url;
break;
}
}
setDownload("windows-download", windowsUrl, "download windows (.exe)");
setDownload("mac-download", macUrl, "download mac (.dmg)");
setDownload("android-download", androidUrl, "download android (.apk)");
} catch {
setDownload("windows-download", null, "unavailable");
setDownload("mac-download", null, "unavailable");
setDownload("android-download", null, "unavailable");
}
}
function setDownload(id, url, label) {
const link = document.getElementById(id);
if (!link) return;
if (url) {
link.href = url;
link.textContent = label;
} else {
link.removeAttribute("href");
link.textContent = label;
}
}
async function fetchJSON(path) {
try {
const res = await fetch(path);
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
async function loadData() {
const [partners, reviews, faq] = await Promise.all([
fetchJSON("src/data/partners.json"),
fetchJSON("src/data/reviews.json"),
fetchJSON("src/data/faq.json")
]);
const revContainer = document.getElementById("reviews-output");
if (revContainer && Array.isArray(reviews)) {
let revText = "\nReviews:\n";
reviews.forEach(r => {
revText += `"${r.text}" - ${r.name} (${r.handle})\n`;
});
revContainer.textContent = revText;
}
const partContainer = document.getElementById("partners-output");
if (partContainer && Array.isArray(partners)) {
partContainer.textContent = "\nPartners:\n";
partners.forEach(p => {
const a = document.createElement("a");
a.href = p.link;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.textContent = `[${p.name}] ${p.link}`;
a.setAttribute("aria-label", `Partner website: ${p.name}`);
partContainer.appendChild(a);
partContainer.appendChild(document.createTextNode("\n"));
});
}
const faqContainer = document.getElementById("faq-output");
if (faqContainer && Array.isArray(faq)) {
let faqText = "\nFAQ:\n";
faq.forEach(f => {
faqText += `Q: ${f.q}\nA: ${f.a}\n\n`;
});
faqContainer.textContent = faqText;
}
}
loadDownloads();
loadData();

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -1,473 +0,0 @@
class GlassSurface {
constructor(element, config = {}) {
this.el = element;
this.config = Object.assign({
width: 200,
height: 20,
borderRadius: 20,
borderWidth: 0.07,
brightness: 50,
opacity: 0,
blur: 11,
displace: 0,
backgroundOpacity: 0,
saturation: 1,
distortionScale: -180,
redOffset: 0,
greenOffset: 0,
blueOffset: 0,
xChannel: 'R',
yChannel: 'G',
mixBlendMode: 'difference',
className: '',
style: {}
}, config);
this.uniqueId = Math.random().toString(36).substring(2, 9).replace(/:/g, '-');
this.filterId = `glass-filter-${this.uniqueId}`;
this.redGradId = `red-grad-${this.uniqueId}`;
this.blueGradId = `blue-grad-${this.uniqueId}`;
this.isMounted = false;
this.liquidGlassEnabled = true;
this.init();
}
supportsSVGFilters() {
if (typeof window === 'undefined' || typeof document === 'undefined') return false;
const ua = navigator.userAgent.toLowerCase();
if (ua.includes('firefox')) return false;
const isIOS = /iphone|ipad|ipod/.test(ua);
const isSafari = ua.includes('safari') && !ua.includes('chrome');
const isWebkitOnly = ua.includes('webkit') && !ua.includes('chrome');
if (isIOS || isSafari || isWebkitOnly) return false;
const div = document.createElement('div');
div.style.backdropFilter = `url(#${this.filterId})`;
const supported = div.style.backdropFilter !== '';
if (!supported) {
div.style.cssText = `-webkit-backdrop-filter: url(#${this.filterId})`;
return div.style.webkitBackdropFilter !== '' && div.style.webkitBackdropFilter !== undefined;
}
return supported;
}
supportsBackdropFilter() {
return CSS.supports('backdrop-filter', 'blur(10px)');
}
getIsDark() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
init() {
this.isMounted = true;
this.isDark = this.getIsDark();
this.svgSupported = this.supportsSVGFilters();
this.backdropSupported = this.supportsBackdropFilter();
this.darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.darkModeListener = (e) => {
this.isDark = e.matches;
this.updateStyles();
};
this.darkModeQuery.addEventListener('change', this.darkModeListener);
this.checkSettings();
this.el.classList.add('glass-surface');
this.el.style.position = 'relative';
this.el.style.transform = 'translateZ(0)';
this.el.style.backfaceVisibility = 'hidden';
this.el.style.perspective = '1000';
this.el.style.contain = 'layout style paint';
this.applyBaseStyles();
this.createContentWrapper();
if (this.liquidGlassEnabled) {
this.applyGlassEffect();
} else {
this.applyFallback();
}
this.setupResizeObserver();
}
checkSettings() {
try {
const settings = localStorage.getItem('glassSettings');
if (settings) {
const parsed = JSON.parse(settings);
this.liquidGlassEnabled = parsed.liquidGlassEnabled !== false;
}
} catch (e) {
}
}
applyBaseStyles() {
const width = typeof this.config.width === 'number' ? `${this.config.width}px` : this.config.width;
const height = typeof this.config.height === 'number' ? `${this.config.height}px` : this.config.height;
this.el.style.width = width || 'auto';
this.el.style.height = height || 'auto';
this.el.style.borderRadius = `${this.config.borderRadius}px`;
this.el.style.setProperty('--glass-frost', this.config.backgroundOpacity);
this.el.style.setProperty('--glass-saturation', this.config.saturation);
Object.assign(this.el.style, this.config.style);
if (this.config.className) {
this.el.classList.add(...this.config.className.split(' '));
}
if (this.isDark) {
this.el.classList.add('dark-mode');
} else {
this.el.classList.add('light-mode');
}
}
createContentWrapper() {
this.contentWrapper = document.createElement('div');
this.contentWrapper.className = 'glass-surface__content';
this.contentWrapper.style.cssText = `
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: inherit;
position: relative;
z-index: 10;
`;
while (this.el.firstChild) {
this.contentWrapper.appendChild(this.el.firstChild);
}
this.el.appendChild(this.contentWrapper);
}
applyGlassEffect() {
if (this.svgSupported && this.backdropSupported) {
this.applySVGFilter();
} else if (this.backdropSupported) {
this.applyBackdropFallback();
} else {
this.applyBasicFallback();
}
}
applySVGFilter() {
this.el.style.willChange = 'backdrop-filter, transform';
const bgColor = this.isDark
? `hsl(0 0% 0% / ${this.config.backgroundOpacity})`
: `hsl(0 0% 100% / ${this.config.backgroundOpacity})`;
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.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.backdropFilter = `url(#${this.filterId}) saturate(${this.config.saturation})`;
this.el.style.webkitBackdropFilter = `url(#${this.filterId}) saturate(${this.config.saturation})`;
this.el.style.boxShadow = shadow;
this.createSVGFilter();
}
createSVGFilter() {
this.svgContainer = document.createElementNS("http://www.w3.org/2000/svg", "svg");
this.svgContainer.setAttribute("class", "glass-surface__filter");
this.svgContainer.style.cssText = `
width: 100%;
height: 100%;
position: absolute;
inset: 0;
opacity: 0;
z-index: -10;
pointer-events: none;
transform: translateZ(0);
`;
this.svgContainer.innerHTML = `
<defs>
<filter id="${this.filterId}" color-interpolation-filters="sRGB" x="0%" y="0%" width="100%" height="100%">
<feImage ref="feImage" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map" />
<feDisplacementMap ref="dispRed" in="SourceGraphic" in2="map" id="redchannel" result="dispRed" />
<feColorMatrix in="dispRed" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="red" />
<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" />
<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" />
<feBlend in="red" in2="green" mode="screen" result="rg" />
<feBlend in="rg" in2="blue" mode="screen" result="output" />
<feGaussianBlur ref="gaussianBlur" in="output" stdDeviation="0.7" />
</filter>
</defs>
`;
this.el.appendChild(this.svgContainer);
this.feImage = this.svgContainer.querySelector('[ref="feImage"]');
this.dispRed = this.svgContainer.querySelector('[ref="dispRed"]');
this.dispGreen = this.svgContainer.querySelector('[ref="dispGreen"]');
this.dispBlue = this.svgContainer.querySelector('[ref="dispBlue"]');
this.gaussianBlur = this.svgContainer.querySelector('[ref="gaussianBlur"]');
this.updateFilterAttributes();
requestAnimationFrame(() => this.updateDisplacementMap());
}
updateFilterAttributes() {
if (!this.dispRed || !this.dispGreen || !this.dispBlue || !this.gaussianBlur) return;
this.dispRed.setAttribute('scale', (this.config.distortionScale + this.config.redOffset).toString());
this.dispRed.setAttribute('xChannelSelector', this.config.xChannel);
this.dispRed.setAttribute('yChannelSelector', this.config.yChannel);
this.dispGreen.setAttribute('scale', (this.config.distortionScale + this.config.greenOffset).toString());
this.dispGreen.setAttribute('xChannelSelector', this.config.xChannel);
this.dispGreen.setAttribute('yChannelSelector', this.config.yChannel);
this.dispBlue.setAttribute('scale', (this.config.distortionScale + this.config.blueOffset).toString());
this.dispBlue.setAttribute('xChannelSelector', this.config.xChannel);
this.dispBlue.setAttribute('yChannelSelector', this.config.yChannel);
this.gaussianBlur.setAttribute('stdDeviation', this.config.displace.toString());
}
applyBackdropFallback() {
this.el.style.willChange = 'transform';
if (this.isDark) {
this.el.style.background = 'rgba(0, 0, 0, 0.3)';
this.el.style.backdropFilter = 'blur(12px) saturate(1.8) brightness(1.1)';
this.el.style.webkitBackdropFilter = 'blur(12px) saturate(1.8) brightness(1.1)';
this.el.style.boxShadow = '0 0 0 0.5px rgba(255, 255, 255, 0.1) inset';
} else {
this.el.style.background = 'rgba(255, 255, 255, 0.1)';
this.el.style.backdropFilter = 'blur(12px) saturate(1.8) brightness(1.05)';
this.el.style.webkitBackdropFilter = 'blur(12px) saturate(1.8) brightness(1.05)';
this.el.style.boxShadow = '0 0 0 0.5px rgba(255, 255, 255, 0.15) inset, 0px 4px 16px rgba(0, 0, 0, 0.04)';
}
}
applyBasicFallback() {
this.el.style.willChange = 'transform';
if (this.isDark) {
this.el.style.background = 'rgba(0, 0, 0, 0.5)';
this.el.style.boxShadow = '0 0 0 0.5px rgba(255, 255, 255, 0.1) inset';
} else {
this.el.style.background = 'rgba(255, 255, 255, 0.2)';
this.el.style.boxShadow = '0 0 0 0.5px rgba(255, 255, 255, 0.15) inset';
}
}
applyFallback() {
if (this.backdropSupported) {
this.applyBackdropFallback();
} else {
this.applyBasicFallback();
}
}
updateStyles() {
if (!this.isMounted) return;
this.el.classList.remove('dark-mode', 'light-mode');
if (this.isDark) {
this.el.classList.add('dark-mode');
} else {
this.el.classList.add('light-mode');
}
if (this.liquidGlassEnabled) {
if (this.svgSupported && this.backdropSupported) {
const bgColor = this.isDark
? `hsl(0 0% 0% / ${this.config.backgroundOpacity})`
: `hsl(0 0% 100% / ${this.config.backgroundOpacity})`;
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.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.boxShadow = shadow;
} else {
this.applyFallback();
}
} else {
this.applyFallback();
}
}
setupResizeObserver() {
if (!this.svgSupported) return;
this.resizeObserver = new ResizeObserver(() => {
if ('requestIdleCallback' in window) {
window.requestIdleCallback(() => this.updateDisplacementMap(), { timeout: 100 });
} else {
setTimeout(() => this.updateDisplacementMap(), 0);
}
});
this.resizeObserver.observe(this.el);
}
updateDisplacementMap() {
if (!this.svgSupported || !this.feImage) return;
const rect = this.el.getBoundingClientRect();
const actualWidth = Math.max(Math.round(rect.width) || 400, 10);
const actualHeight = Math.max(Math.round(rect.height) || 200, 10);
const edgeSize = Math.min(actualWidth, actualHeight) * (this.config.borderWidth * 0.5);
const svgContent = `
<svg viewBox="0 0 ${actualWidth} ${actualHeight}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="${this.redGradId}" x1="100%" y1="0%" x2="0%" y2="0%">
<stop offset="0%" stop-color="#0000"/>
<stop offset="100%" stop-color="red"/>
</linearGradient>
<linearGradient id="${this.blueGradId}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0000"/>
<stop offset="100%" stop-color="blue"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" fill="black"></rect>
<rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" rx="${this.config.borderRadius}" fill="url(#${this.redGradId})" />
<rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" rx="${this.config.borderRadius}" fill="url(#${this.blueGradId})" style="mix-blend-mode: ${this.config.mixBlendMode}" />
<rect x="${edgeSize}" y="${edgeSize}" width="${actualWidth - edgeSize * 2}" height="${actualHeight - edgeSize * 2}" rx="${this.config.borderRadius}" fill="hsl(0 0% ${this.config.brightness}% / ${this.config.opacity})" style="filter:blur(${this.config.blur}px)" />
</svg>
`;
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svgContent)}`;
this.feImage.setAttribute('href', dataUrl);
}
destroy() {
this.isMounted = false;
if (this.darkModeQuery && this.darkModeListener) {
this.darkModeQuery.removeEventListener('change', this.darkModeListener);
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
if (this.svgContainer && this.svgContainer.parentNode) {
this.svgContainer.remove();
}
if (this.contentWrapper && this.contentWrapper.parentNode) {
while (this.contentWrapper.firstChild) {
this.el.appendChild(this.contentWrapper.firstChild);
}
this.contentWrapper.remove();
}
}
}
function applyGlassSurface(el, config) {
if (!el || el.dataset.hasGlass) return null;
if (typeof GlassSurface !== 'function') return null;
el.dataset.hasGlass = "true";
const instance = new GlassSurface(el, config);
el._glassSurfaceInstance = instance;
return instance;
}
function destroyGlassSurface(el) {
if (!el || !el._glassSurfaceInstance) return;
el._glassSurfaceInstance.destroy();
el._glassSurfaceInstance = null;
delete el.dataset.hasGlass;
}
function initLiquidGlass() {
const config = {
width: 200,
height: 25,
borderRadius: 20,
borderWidth: 0.07,
brightness: 50,
opacity: 0.93,
blur: 11,
displace: 0,
backgroundOpacity: 0,
saturation: 1,
distortionScale: -180,
redOffset: 0,
greenOffset: 10,
blueOffset: 20,
xChannel: 'R',
yChannel: 'G',
mixBlendMode: 'difference'
};
const eagerSelectors = [
'.nav-links',
'.discord-copy-bar',
'.platform-pill',
'.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) {
window._cardPlayBtnGlassObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
applyGlassSurface(entry.target, config);
window._cardPlayBtnGlassObserver.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
}
if (!window._sharedDomObserver) {
window._sharedDomObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
m.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
if (node.matches?.('.card-play-btn') && !node.dataset.hasGlass) {
window._cardPlayBtnGlassObserver.observe(node);
}
node.querySelectorAll?.('.card-play-btn:not([data-has-glass])').forEach(el => {
window._cardPlayBtnGlassObserver.observe(el);
});
});
}
});
window._sharedDomObserver.observe(document.body, { childList: true, subtree: true });
}
document.querySelectorAll('.card-play-btn:not([data-has-glass])').forEach(el => {
window._cardPlayBtnGlassObserver.observe(el);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initLiquidGlass());
} else {
initLiquidGlass();
}
export { GlassSurface, applyGlassSurface, destroyGlassSurface, initLiquidGlass };

View File

@@ -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);
}
};
}

View File

@@ -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)));
}
}

View File

@@ -1,361 +0,0 @@
import { Renderer, Triangle, Program, Mesh } from 'https://esm.sh/ogl@1.0.11';
export function initPrism(container, options = {}) {
if (!container) return () => { };
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;
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 STEPS = 100;
for (int i = 0; i < STEPS; i++) {
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 }
}
});
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);
}
};
}

View File

@@ -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));
}

View File

@@ -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);
});
}

View File

@@ -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(() => { });
});
});

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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.',
},
];

View File

@@ -1,21 +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';
function initApp() {
observeReveals();
initLiquidGlass();
initHeroSection();
initStatsSection();
initTestimonialsSection();
initPartnersSection();
initFaqSection();
}
document.addEventListener('DOMContentLoaded', initApp);

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -1,39 +0,0 @@
import { getPartners } from '../services/partners-service.js';
import { createElement } from '../utils/dom.js';
import { observeReveals } from '../animations/reveal-observer.js';
function createPartnerItem(partner) {
const item = createElement('div', 'partner-item reveal');
let imgHTML;
if (partner.logo) {
if (partner.logoSmall) {
imgHTML = `<img src="${partner.logoSmall}" alt="${partner.name}" width="128" height="128" loading="lazy" decoding="async"
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">`;
}
} 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)));
observeReveals(grid);
} catch (error) {
grid.innerHTML = '';
}
}

View File

@@ -1,52 +0,0 @@
import { getDiscordStats } from '../services/discord-service.js';
import { getScripts } from '../services/scripts-service.js';
import { animateCountOnVisible } from '../animations/counter.js';
async function initDiscordStats() {
const membersEl = document.getElementById('stat-members');
const onlineEl = document.getElementById('stat-online');
if (!membersEl || !onlineEl) 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;
try {
const scripts = await getScripts();
animateCountOnVisible(scriptsEl, scripts.length);
} catch (error) {
scriptsEl.textContent = '-';
}
}
export function initStatsSection() {
initDiscordStats();
initScriptCountStat();
}

View File

@@ -1,38 +0,0 @@
import { getReviews } from '../services/reviews-service.js';
import { createTestimonialCard } from '../components/testimonial-card.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() {
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);
} 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>';
}
}
}

View File

@@ -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,
};
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 });
}
});
}

View File

@@ -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);
};
}

View File

@@ -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`;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -1,175 +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: 0.7rem 1.35rem;
height: 50px !important;
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);
overflow: hidden;
}
.download-expandable[aria-expanded="true"] {
border-radius: 10px;
}
.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 {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.3s ease;
opacity: 0;
background: #000;
}
.download-options.show {
max-height: 200px;
opacity: 1;
}
.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;
}

View File

@@ -1,59 +0,0 @@
.faq-list {
max-width: 1000px;
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;
}

View File

@@ -1,65 +0,0 @@
.glass-surface {
position: relative !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
background: transparent !important;
border: none !important;
border-radius: var(--glass-radius, 20px) !important;
overflow: hidden !important;
text-decoration: none !important;
box-sizing: border-box !important;
width: auto !important;
max-width: max-content !important;
transition: opacity 260ms ease-out;
box-shadow: none !important;
}
.glass-surface.dark-mode:focus-visible {
outline: 2px solid #0A84FF;
outline-offset: 2px;
}
.glass-surface.light-mode:focus-visible {
outline: 2px solid #007AFF;
outline-offset: 2px;
}
.glass-surface__filter {
width: 100%;
height: 100%;
pointer-events: none;
position: absolute;
inset: 0;
opacity: 0;
z-index: -10;
box-shadow: none !important;
}
.glass-surface__content {
position: relative;
z-index: 10;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: inherit;
width: 100%;
height: 100%;
color: inherit;
white-space: nowrap;
border-radius: inherit;
pointer-events: auto;
box-shadow: none !important;
padding: 4px;
}
.glass-surface .fa-solid,
.glass-surface .fas {
padding-right: 10px !important;
}
.glass-surface--fallback .fa-solid,
.glass-surface--fallback .fas {
padding-right: 5px;
}

View File

@@ -1,77 +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;
}
.marquee-left .marquee-track {
animation: marquee-scroll-left 60s linear infinite;
}
.marquee-right .marquee-track {
animation: marquee-scroll-right 60s linear infinite;
}
@keyframes marquee-scroll-left {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(-50%, 0, 0);
}
}
@keyframes marquee-scroll-right {
from {
transform: translate3d(-50%, 0, 0);
}
to {
transform: translate3d(0, 0, 0);
}
}
@media (prefers-reduced-motion: reduce) {
.marquee-track,
.executors-marquee-track,
.showcase-track {
animation: none !important;
transform: none !important;
}
}

View File

@@ -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);
}
}

View File

@@ -1,49 +0,0 @@
.partners-inline {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.85rem;
padding-bottom: 3rem;
}
.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;
}

View File

@@ -1,124 +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;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: var(--radius-pill);
font-size: 0.8125rem;
color: var(--color-fg-dim);
}
.platform-pill svg {
flex-shrink: 0;
}
.platform-divider {
color: var(--color-fg-dim);
opacity: 0.5;
}
.mac-icon {
display: block;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,68 +0,0 @@
.footer {
position: relative;
padding: 32px 24px 24px;
overflow: visible;
background: transparent;
z-index: 2;
}
.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;
}

View File

@@ -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;
}
}

View File

@@ -1 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap');

View File

@@ -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;
}
}

View File

@@ -1,85 +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;
}
.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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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: 100%;
}
.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;
}
}

View File

@@ -1,35 +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%;
}

View File

@@ -1,173 +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%;
}
.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;
}
.platform-pill {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.45rem 1.1rem;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 500;
color: #ffffff;
margin-bottom: 1.25rem;
white-space: nowrap;
width: auto;
max-width: max-content;
}
.platform-pill svg {
width: 13px;
height: 13px;
color: #ffffff;
fill: currentColor;
flex-shrink: 0;
}
.platform-pill span {
white-space: nowrap;
}
.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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -1,27 +0,0 @@
.container {
width: 100%;
margin: 0 auto;
padding-inline: var(--content-padding-inline);
}
.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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

52
src/styling/download.css Normal file
View File

@@ -0,0 +1,52 @@
.download-box {
display: flex;
flex-direction: column;
gap: 1rem;
border: 1px dashed #333;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 10px;
padding: 1.25rem 1.5rem;
margin: 0 1.5rem 2rem 1.5rem;
width: calc(100% - 3rem);
max-width: 900px;
box-sizing: border-box;
}
.download-header {
color: #ffffff;
}
.download-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
width: 100%;
}
.download-item {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.platform-name {
color: #888888;
}
.download-item a {
text-decoration: none;
word-break: break-word;
}
.download-item a:hover,
.download-item a:focus {
text-decoration: underline;
}
@media (max-width: 600px) {
.download-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}

View File

@@ -0,0 +1,14 @@
.explanation {
padding-top: 50px;
padding-bottom: 50px;
font-size: 16px;
line-height: 1.6;
width: 50%;
margin: 0 auto;
}
@media (max-width: 768px) {
.explanation {
width: 90%;
}
}

35
src/styling/footer.css Normal file
View File

@@ -0,0 +1,35 @@
footer {
width: 100%;
box-sizing: border-box;
margin-top: 3.5rem;
border-top: 1px dashed #333;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding: 2.5rem 1.5rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
}
footer pre,
footer .links {
width: 100%;
max-width: 900px;
padding: 0;
margin: 0;
}
footer .links {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
margin-top: 1rem;
}
footer .links a {
display: flex;
align-items: center;
}

33
src/styling/grid.css Normal file
View File

@@ -0,0 +1,33 @@
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
width: 100%;
max-width: 900px;
padding: 0 1.5rem;
margin-bottom: 2.5rem;
box-sizing: border-box;
}
.gallery img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
object-fit: cover;
display: block;
border-radius: 30px;
}
@media (max-width: 900px) {
.gallery {
grid-template-columns: repeat(2, 1fr);
width: 100%;
}
}
@media (max-width: 500px) {
.gallery {
grid-template-columns: 1fr;
width: 100%;
}
}

67
src/styling/main.css Normal file
View File

@@ -0,0 +1,67 @@
@import url('./footer.css');
@import url('./grid.css');
@import url('./download.css');
@import url('./explanation.css');
body {
background-color: #000000;
color: #ffffff;
overflow-x: hidden;
margin-top: 50px;
margin-bottom: 0;
margin-left: 0;
margin-right: 0;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
}
body video {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: -1;
}
main {
margin: 0;
width: 100%;
min-height: calc(100vh - 50px);
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
}
body::before,
body::after {
content: "";
position: fixed;
left: 0;
right: 0;
height: 36px;
pointer-events: none;
z-index: 9999;
}
body::before {
top: 0;
background: linear-gradient(to bottom, #000000 15%, transparent);
}
body::after {
bottom: 0;
background: linear-gradient(to top, #000000 15%, transparent);
}
pre {
white-space: pre-wrap;
word-break: break-word;
margin: 0 0 1.5rem 0;
padding: 0 1.5rem;
width: 100%;
max-width: 900px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
}

View File

@@ -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="/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/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" />
<link rel="stylesheet" href="/src/css/main.css">
<link rel="stylesheet" href="/src/css/other.css">
<script type="application/ld+json">
{
@@ -241,14 +215,6 @@
</head>
<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">
<div class="content-wrapper">
<h2>Terms of Service</h2>
@@ -301,14 +267,18 @@
</main>
<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">
<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>
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
<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>
<svg class="dropdown-arrow" width="17" height="17" viewBox="0 0 24 24" fill="currentColor"
aria-hidden="true">
@@ -316,123 +286,44 @@
</svg>
</button>
<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"
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 v0.0.0</span>
<span>Windows</span>
</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"
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>Mac v0.0.0</span>
<span>macOS</span>
</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"
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 v0.0.0</span>
<span>Android</span>
</a>
</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>
<footer class="footer">
<footer class="footer" role="contentinfo">
<div class="footer-content">
<div class="footer-section">
<h2>Vyla</h2>
<p>Vyla is a passion project born from a love for film and television.</p>
</div>
<div class="footer-section">
<h2>Backup Site:</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
<h2>Backup Site</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener noreferrer">https://vyla.qzz.io</a>
</p>
</div>
<div class="footer-section">
@@ -440,33 +331,32 @@
<a href="/terms-of-service">Terms of Service</a>
<a href="/privacy-policy">Privacy Policy</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 class="footer-section">
<h2>Our API</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
<h2>Developers</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer">Documentation</a>
</div>
<div class="footer-section">
<h2>Join our Discord</h2>
<h2>Community</h2>
<div class="social-links">
<a href="/discord" target="_blank" rel="noopener">
<i class="fab fa-discord"></i> Discord
</a>
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener noreferrer"
aria-label="Official Discord Server">Discord</a>
</div>
</div>
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
<div class="footer-section footer-badge">
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc"
alt="This website runs on green hosting verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
</div>
</div>
<div class="footer-bottom">
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
&copy; 2025 Vyla Entertainment. All rights reserved.
</a>
<p>&copy; 2025 Vyla Entertainment. All rights reserved.</p>
</div>
</footer>
</div>
<script type="module" src="/src/scripts/main.js"></script>
<script type="module" src="/src/js/main.js"></script>
</body>
</html>