Released New Website
BIN
assets/images/features/1.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/images/features/2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/features/3.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/images/features/4.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
assets/images/features/5.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
assets/images/features/6.png
Normal file
|
After Width: | Height: | Size: 1010 KiB |
BIN
assets/images/header_logo.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
BIN
assets/images/partners/fmhy.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
assets/images/partners/piracy.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
assets/images/testimonials/acideetdeforme.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/images/testimonials/block.tale.adict.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
assets/images/testimonials/chariachtus.png
Normal file
|
After Width: | Height: | Size: 693 KiB |
BIN
assets/images/testimonials/lucky_mucky_.png
Normal file
|
After Width: | Height: | Size: 789 KiB |
BIN
assets/images/testimonials/snorer.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/images/testimonials/zenithrifle.png
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
assets/images/videos/header_video.mp4
Normal file
590
index.html
@@ -1,219 +1,371 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Vyla</title>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="favicon.ico" />
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: black;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
background: radial-gradient(circle at center,
|
||||
rgba(0, 0, 0, 0) 40%,
|
||||
rgba(0, 0, 0, 1) 100%);
|
||||
}
|
||||
|
||||
.center-text {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
font-size: 5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
max-height: 70vh;
|
||||
min-width: 70vw;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
font-size: 1.1rem;
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.footer::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: rgb(182, 182, 182);
|
||||
text-decoration: none;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.center-text {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<canvas id="c"></canvas>
|
||||
|
||||
<div class="center-text">
|
||||
Vyla is no longer available as a hosted website. Use Vyla Home to access Vyla on desktop.
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<a href="https://gitlab.com/vyla-entertainment/home/-/releases" target="_blank"
|
||||
rel="noopener noreferrer">Download Vyla Home</a>
|
||||
<a href="https://docs.vyla.cc" target="_blank" rel="noopener noreferrer">Docs</a>
|
||||
<a href="https://vyla.cc/discord" target="_blank" rel="noopener noreferrer">Discord</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const CHARS = ' .:-=+*#%@';
|
||||
const FONT_SIZE = 50;
|
||||
|
||||
const canvas = document.getElementById('c');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let CW, CH;
|
||||
let cols, rows;
|
||||
let t = 0;
|
||||
|
||||
let mouseX = -9999;
|
||||
let mouseY = -9999;
|
||||
let lastMouseMove = 0;
|
||||
let mouseStrength = 0;
|
||||
|
||||
const FADE_TIME = 250;
|
||||
const SMOOTHING = 0.08;
|
||||
|
||||
function initFont() {
|
||||
ctx.font = `${FONT_SIZE}px monospace`;
|
||||
CW = Math.ceil(ctx.measureText('M').width);
|
||||
CH = FONT_SIZE + 2;
|
||||
ctx.textBaseline = 'top';
|
||||
}
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
initFont();
|
||||
cols = Math.ceil(canvas.width / CW) + 1;
|
||||
rows = Math.ceil(canvas.height / CH) + 1;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
mouseX = e.clientX - rect.left;
|
||||
mouseY = e.clientY - rect.top;
|
||||
lastMouseMove = performance.now();
|
||||
});
|
||||
|
||||
function plasma(cx, cy, t) {
|
||||
const x = cx + Math.sin(cy * 3 + t * 0.7) * 0.07;
|
||||
const y = cy + Math.cos(cx * 3 + t * 0.5) * 0.07;
|
||||
|
||||
let v =
|
||||
Math.sin(x * 10 + t) +
|
||||
Math.sin(y * 10 + t) +
|
||||
Math.sin((x + y) * 7 + t) +
|
||||
Math.sin(Math.sqrt(x * x + y * y) * 12 - t * 1.2) +
|
||||
Math.sin(x * 6 - t * 0.8) +
|
||||
Math.sin(y * 8 + t * 0.9);
|
||||
|
||||
return (v / 6 + 1) / 2;
|
||||
}
|
||||
|
||||
function render() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
t += 0.03;
|
||||
|
||||
const elapsed = performance.now() - lastMouseMove;
|
||||
const targetStrength = Math.max(0, 1 - elapsed / FADE_TIME);
|
||||
|
||||
mouseStrength += (targetStrength - mouseStrength) * SMOOTHING;
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const x = col * CW;
|
||||
const y = row * CH;
|
||||
|
||||
const dx = x - mouseX;
|
||||
const dy = y - mouseY;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
const force = (7000 / (dist * dist + 1500)) * mouseStrength;
|
||||
|
||||
const offsetX = dx * force;
|
||||
const offsetY = dy * force;
|
||||
|
||||
const v = plasma(col / cols, row / rows, t);
|
||||
const brightness = Math.round(v * 255);
|
||||
const charIdx = Math.floor(v * (CHARS.length - 0.001));
|
||||
const ch = CHARS[charIdx];
|
||||
|
||||
ctx.fillStyle = `rgb(${brightness},${brightness},${brightness})`;
|
||||
ctx.fillText(ch, x + offsetX, y + offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
resize();
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
|
||||
<title>Vyla Home</title>
|
||||
<meta name="description"
|
||||
content="Vyla Home is a standalone desktop media center designed to bypass datacenter blocking, ISP throttling, and cloud downtime.">
|
||||
<meta name="keywords"
|
||||
content="vyla, vyla home, media player, streaming, desktop app, self-hosted, movies, tv shows">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://vyla.cc/">
|
||||
|
||||
<meta property="og:title" content="Vyla Home">
|
||||
<meta property="og:description"
|
||||
content="A standalone desktop media center designed to bypass datacenter blocking, ISP throttling, and cloud downtime.">
|
||||
<meta property="og:image" content="assets/images/thumbnail.png">
|
||||
<meta property="og:image:width" content="1080">
|
||||
<meta property="og:image:height" content="607">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Vyla Home">
|
||||
<meta property="og:url" content="https://vyla.cc/">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Vyla Home">
|
||||
<meta name="twitter:description"
|
||||
content="A standalone desktop media center designed to bypass datacenter blocking, ISP throttling, and cloud downtime.">
|
||||
<meta name="twitter:image" content="assets/images/thumbnail.png">
|
||||
|
||||
<link rel="icon" href="assets/images/favicon.ico" type="image/x-icon">
|
||||
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<meta name="theme-color" content="transparent">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="transparent">
|
||||
<meta name="apple-mobile-web-app-title" content="Vyla Home">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
<link rel="apple-touch-icon" href="assets/images/icon.png">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<link rel="stylesheet" href="src/styles/main.css">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "Vyla Home",
|
||||
"url": "https://vyla.cc/",
|
||||
"description": "Vyla Home is a standalone desktop media center designed to bypass datacenter blocking, ISP throttling, and cloud downtime."
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<div class="nav-container">
|
||||
<span class="logo">Vyla Home</span>
|
||||
<div class="nav-links">
|
||||
<a href="#features">Features</a>
|
||||
<a href="#architecture">Architecture</a>
|
||||
<a href="#faq">FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<svg aria-hidden="true" class="dot-grid" width="100%" height="100%">
|
||||
<defs>
|
||||
<pattern id="dot-pattern" width="20" height="20" patternUnits="userSpaceOnUse"
|
||||
patternContentUnits="userSpaceOnUse" x="0" y="0">
|
||||
<circle cx="1" cy="1" r="1"></circle>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" stroke-width="0" fill="url(#dot-pattern)"></rect>
|
||||
</svg>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="hero-atmosphere" aria-hidden="true">
|
||||
<video class="hero-atmosphere-video" autoplay loop muted playsinline webkit-playsinline preload="auto">
|
||||
<source src="assets\images\videos\header_video.mp4" type="video/mp4">
|
||||
</video>
|
||||
<div class="hero-atmosphere-sheen"></div>
|
||||
<canvas id="hero-byte-overlay"></canvas>
|
||||
<div class="hero-atmosphere-vignette"></div>
|
||||
</div>
|
||||
<div class="container hero-container">
|
||||
|
||||
<div class="logo-image">
|
||||
<img src="assets/images/header_logo.png" alt="Vyla Home">
|
||||
</div>
|
||||
|
||||
<h1 class="hero-title">Vyla Home</h1>
|
||||
|
||||
<p class="hero-description">We've completely stopped hosting Vyla on a public VPS. Vyla Home is now the
|
||||
primary, self-contained way to experience the platform.
|
||||
</p>
|
||||
|
||||
<button class="discord-copy-bar reveal" id="copy-discord-btn" type="button">
|
||||
<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">
|
||||
<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>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="hero-cta-row reveal">
|
||||
<a class="hero-cta hero-cta-dark" id="hero-copy-script-btn"
|
||||
href="https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/v1.0.0/Vyla-Home_1.0.0_x64-setup.exe"
|
||||
target="_blank" rel="noopener noreferrer">Download Release v1.0.0</a>
|
||||
<a class="hero-cta hero-cta-light" href="https://gitlab.com/vyla-entertainment/home/-/releases"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
View Releases</a>
|
||||
</div>
|
||||
|
||||
<div class="partners-inline reveal" id="partners-grid-wrap">
|
||||
<p class="partners-label">Bypass datacenter blocks, ISP throttling, and cloud downtime.</p>
|
||||
<div class="partners-grid" id="partners-grid"></div>
|
||||
</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 class="stat-item reveal">
|
||||
<span class="stat-value" id="stat-scripts"></span>
|
||||
<span class="stat-label">Supported Sources</span>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<!--
|
||||
|
||||
<section id="scripts" class="section">
|
||||
<div class="container">
|
||||
<h2 class="section-eyebrow reveal">Desktop Advantages</h2>
|
||||
<div class="scripts-wrap">
|
||||
<div class="scripts-grid reveal-stagger" id="scripts-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="architecture" class="section executors-section">
|
||||
<div class="container executors-container">
|
||||
<div class="executors-heading reveal">
|
||||
<h2 class="section-title-lg">A self-contained orchestration stack</h2>
|
||||
<p class="section-subtitle">Running locally on your workstation</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="executors-marquee-wrap">
|
||||
<div class="executors-marquee-row">
|
||||
<div class="executors-marquee-track" id="executors-track"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
-->
|
||||
|
||||
<section id="features" class="section features-section">
|
||||
<div class="container features-container">
|
||||
<div class="features-heading reveal">
|
||||
<h2 class="section-title-lg">Why choose Home?</h2>
|
||||
</div>
|
||||
|
||||
<div class="feature-block reveal">
|
||||
<div class="feature-media">
|
||||
<img src="assets/images/features/1.png" alt="Datacenter IP Throttling">
|
||||
</div>
|
||||
<div class="feature-copy">
|
||||
<h2 class="section-title-lg feature-title">No Datacenter Throttling</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="feature-block feature-block-reverse reveal">
|
||||
<div class="feature-media">
|
||||
<img src="assets/images/features/2.png" alt="Single Point of Failure">
|
||||
</div>
|
||||
<div class="feature-copy">
|
||||
<h2 class="section-title-lg feature-title">No Single Point of Failure</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="feature-block reveal">
|
||||
<div class="feature-media">
|
||||
<img src="assets/images/features/3.png" alt="Encrypted Local Vault">
|
||||
</div>
|
||||
<div class="feature-copy">
|
||||
<h2 class="section-title-lg feature-title">Encrypted Local Vault</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="feature-block feature-block-reverse reveal">
|
||||
<div class="feature-media">
|
||||
<img src="assets/images/features/4.png" alt="Automated OTA Updates">
|
||||
</div>
|
||||
<div class="feature-copy">
|
||||
<h2 class="section-title-lg feature-title">Automated OTA Updates</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="feature-block reveal">
|
||||
<div class="feature-media">
|
||||
<img src="assets/images/features/5.png" alt="Rich Discovery">
|
||||
</div>
|
||||
<div class="feature-copy">
|
||||
<h2 class="section-title-lg feature-title">Rich Discovery & Curated Media</h2>
|
||||
<p class="section-subtitle">
|
||||
Access curated collections, 4K releases, popular anime series, soundcloak music streaming,
|
||||
and detailed TMDB cast breakdowns seamlessly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-block feature-block-reverse reveal">
|
||||
<div class="feature-media">
|
||||
<img src="assets/images/features/6.png" alt="Customizable Interface">
|
||||
</div>
|
||||
<div class="feature-copy">
|
||||
<h2 class="section-title-lg feature-title">Customizable Interface</h2>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
|
||||
<section id="showcases" class="showcases-section">
|
||||
<div class="container showcases-heading reveal">
|
||||
<h2 class="section-title-lg">Advanced Player Engine</h2>
|
||||
<p class="section-subtitle">Isolated playback sandbox with custom subtitle decoders and a responsive
|
||||
video pipeline.</p>
|
||||
</div>
|
||||
<div class="showcase-viewport">
|
||||
<div class="showcase-track" id="showcase-track"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
-->
|
||||
|
||||
<section id="testimonials" class="section testimonials-section">
|
||||
<div class="container testimonials-container">
|
||||
<div class="testimonials-heading reveal">
|
||||
<h2 class="section-title-lg">Here's what people say about <span class="bold">Vyla Home</span></h2>
|
||||
</div>
|
||||
<div class="marquee-wrap marquee-fade-wrap">
|
||||
<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">
|
||||
<div class="container">
|
||||
<div class="faq-heading reveal">
|
||||
<h2 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>
|
||||
</div>
|
||||
<div class="faq-list reveal-stagger" id="faq-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<section class="cta-banner">
|
||||
<div class="cta-banner-media" aria-hidden="true">
|
||||
<video class="cta-banner-video hero-atmosphere-video" src="assets\images\videos\header_video.mp4" autoplay
|
||||
loop muted playsinline webkit-playsinline preload="auto"></video>
|
||||
<canvas id="cta-byte-overlay"></canvas>
|
||||
<div class="cta-banner-overlay"></div>
|
||||
</div>
|
||||
<div class="container cta-banner-content reveal">
|
||||
<h2 class="cta-banner-title">Try Vyla Home today</h2>
|
||||
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
|
||||
<a class="hero-cta hero-cta-light" id="cta-copy-script-btn"
|
||||
href="https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/v1.0.0/Vyla-Home_1.0.0_x64-setup.exe"
|
||||
target="_blank" rel="noopener noreferrer">Download Now</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>Vyla Home</h4>
|
||||
<p>A solo passion project built for resilient, serverless, personal entertainment playback. Run locally,
|
||||
own your experience.</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Join our Discord</h4>
|
||||
<div class="social-links">
|
||||
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener">
|
||||
<i class="fab fa-discord"></i> Discord
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Our Gitlab</h4>
|
||||
<p><a href="https://gitlab.com/vyla-entertainment/home" target="_blank" rel="noopener">Vyla
|
||||
Entertainment</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
© 2025 Vyla Entertainment. Personal local desktop software suite.
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="src/scripts/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
57
manifest.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "Vyla - Stream Movies & TV Shows",
|
||||
"short_name": "Vyla",
|
||||
"description": "Your ultimate streaming platform for movies and TV shows. Discover trending content, 4K releases, and personalized recommendations all in one place.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display_override": [
|
||||
"standalone"
|
||||
],
|
||||
"display": "standalone",
|
||||
"background_color": "transparent",
|
||||
"theme_color": "transparent",
|
||||
"orientation": "any",
|
||||
"prefer_related_applications": false,
|
||||
"related_applications": [],
|
||||
"categories": [
|
||||
"entertainment",
|
||||
"video",
|
||||
"streaming"
|
||||
],
|
||||
"screenshots": [],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/favicon-16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "Vyla - Stream Movies & TV Shows",
|
||||
"short_name": "Vyla",
|
||||
"description": "Your ultimate streaming platform for movies and TV shows. Discover trending content, 4K releases, and personalized recommendations all in one place.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display_override": [
|
||||
"standalone"
|
||||
],
|
||||
"display": "standalone",
|
||||
"background_color": "transparent",
|
||||
"theme_color": "transparent",
|
||||
"orientation": "any",
|
||||
"prefer_related_applications": false,
|
||||
"related_applications": [],
|
||||
"categories": [
|
||||
"entertainment",
|
||||
"video",
|
||||
"streaming"
|
||||
],
|
||||
"screenshots": [],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/favicon-16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
src/data/partners.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"name": "FMHY",
|
||||
"logo": "assets/images/partners/fmhy.png",
|
||||
"link": "https://fmhy.net"
|
||||
},
|
||||
{
|
||||
"name": "r/Piracy",
|
||||
"logo": "assets/images/partners/piracy.png",
|
||||
"link": "https://reddit.com/r/Piracy"
|
||||
}
|
||||
]
|
||||
38
src/data/reviews.json
Normal file
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"name": "ZENITH The Great (Ash Fan)",
|
||||
"handle": "@zenithrifle",
|
||||
"text": "oh god the ui is so goood",
|
||||
"avatar": "/assets/images/testimonials/zenithrifle.png"
|
||||
},
|
||||
{
|
||||
"name": "Light",
|
||||
"handle": "@acideetdeforme",
|
||||
"text": "Youre highk a goat for doing ts without ads",
|
||||
"avatar": "/assets/images/testimonials/acideetdeforme.png"
|
||||
},
|
||||
{
|
||||
"name": "char",
|
||||
"handle": "@chariachtus",
|
||||
"text": "I find Vyla as the best by far",
|
||||
"avatar": "/assets/images/testimonials/chariachtus.png"
|
||||
},
|
||||
{
|
||||
"name": "amri",
|
||||
"handle": "@snorer",
|
||||
"text": "who is the owner of such greatness",
|
||||
"avatar": "/assets/images/testimonials/snorer.png"
|
||||
},
|
||||
{
|
||||
"name": "Bee",
|
||||
"handle": "@block.tale.adict",
|
||||
"text": "this is sickk",
|
||||
"avatar": "/assets/images/testimonials/block.tale.adict.png"
|
||||
},
|
||||
{
|
||||
"name": "lucky-mucky",
|
||||
"handle": "@lucky_mucky_",
|
||||
"text": "ur like the best owner ever❤️",
|
||||
"avatar": "/assets/images/testimonials/lucky_mucky_.png"
|
||||
}
|
||||
]
|
||||
132
src/data/scripts.json
Normal file
@@ -0,0 +1,132 @@
|
||||
[
|
||||
{
|
||||
"image": "assets/static/background-with-logo.png",
|
||||
"alt": "Soluna Script Loader Screenshot",
|
||||
"title": "Soluna Script Loader",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/Loader.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-15eb19b4816654a3270689171a3dbdef/768/432/Image/Webp/noFilter",
|
||||
"alt": "Rivals Classic Script Screenshot",
|
||||
"title": "Rivals Classic",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Rivals/Classic.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-27507ba164fae9b46c68047d34d0078b/768/432/Image/Webp/noFilter",
|
||||
"alt": "Rivals Modern Script Screenshot",
|
||||
"title": "Rivals Modern",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Rivals/Modern.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-4ca6e066ef51f10829f653ca849f350d/768/432/Image/Webp/noFilter",
|
||||
"alt": "Rivals Rewrite Script Screenshot",
|
||||
"title": "Rivals Rewrite",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Rivals/Rewrite.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-c66296c3f8c6e16eb6f6eecca67c4710/768/432/Image/Webp/noFilter",
|
||||
"alt": "Rivals Skin Changer Script Screenshot",
|
||||
"title": "Rivals Skin Changer",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Rivals/Skin-Changer.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-fd5d29ef7df403915891862d02ae09bb/768/432/Image/Webp/noFilter",
|
||||
"alt": "Arsenal Script Screenshot",
|
||||
"title": "Arsenal",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Arsenal.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-aa0679c96e6ce33f961087cebcc07ce6/768/432/Image/Webp/noFilter",
|
||||
"alt": "Blade Ball Script Screenshot",
|
||||
"title": "Blade Ball",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Bladeball.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-fe7335c3ad752e84323cd81ae38de69a/768/432/Image/Webp/noFilter",
|
||||
"alt": "Murder Mystery 2 Script Screenshot",
|
||||
"title": "Murder Mystery 2",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Murder-Mystery-2.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-643b9b9285f3bd9b63cf392db12a4d87/768/432/Image/Webp/noFilter",
|
||||
"alt": "99 Nights in the Forest Script Screenshot",
|
||||
"title": "99 Nights in the Forest",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/99-Nights-in-the-Forest.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-9fb820a5978cb6a5125861c3f0cbe131/768/432/Image/Webp/noFilter",
|
||||
"alt": "The Wild West Script Screenshot",
|
||||
"title": "The Wild West",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/The-Wild-West.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-09f0d9b97f35839568ec129016b18485/768/432/Image/Webp/noFilter",
|
||||
"alt": "Doors Script Screenshot",
|
||||
"title": "Doors",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Doors.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-20254f91c5043de87f6cc8adb218c624/768/432/Image/Webp/noFilter",
|
||||
"alt": "Fling Things and People Script Screenshot",
|
||||
"title": "Fling Things and People",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Fling-Things-and-People.lua"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"image": "https://tr.rbxcdn.com/180DAY-be85b5af1040324de15123ab4208cf84/768/432/Image/Webp/noFilter",
|
||||
"alt": "Murderers VS Sheriffs DUELS Script Screenshot",
|
||||
"title": "Murderers VS Sheriffs DUELS",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://gitlab.com/soluna-development/api/-/raw/main/src/scripts/Murderers-VS-Sheriffs-DUELS.lua"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
16
src/data/showcases.json
Normal file
@@ -0,0 +1,16 @@
|
||||
[
|
||||
"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"
|
||||
]
|
||||
32
src/scripts/animations/counter.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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);
|
||||
}
|
||||
313
src/scripts/animations/fluid-byte-field.js
Normal file
@@ -0,0 +1,313 @@
|
||||
import { prefersReducedMotion } from '../utils/dom.js';
|
||||
import { smoothstep, diffuse, project, advect } from './fluid-solver.js';
|
||||
|
||||
const CELL_SIZE = 24;
|
||||
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 = 3;
|
||||
const PROJECT_ITERATIONS = 4;
|
||||
const VELOCITY_DAMP = 0.1;
|
||||
const DYE_DAMP = 0.1;
|
||||
const TRAIL_LIFETIME_MS = 184;
|
||||
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;
|
||||
const DRAG_THRESHOLD_PX = 12;
|
||||
const MAX_TRAIL_POINTS = 24;
|
||||
|
||||
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;
|
||||
|
||||
const trails = [];
|
||||
let dragStartX = 0;
|
||||
let dragStartY = 0;
|
||||
let dragging = false;
|
||||
|
||||
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();
|
||||
const px = e.clientX - rect.left;
|
||||
const 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 onPointerDown(e) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const px = e.clientX - rect.left;
|
||||
const py = e.clientY - rect.top;
|
||||
dragStartX = px;
|
||||
dragStartY = py;
|
||||
dragging = px >= 0 && py >= 0 && px <= rect.width && py <= rect.height;
|
||||
}
|
||||
|
||||
function onPointerUp(e) {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
const rect = container.getBoundingClientRect();
|
||||
const px = e.clientX - rect.left;
|
||||
const py = e.clientY - rect.top;
|
||||
if (Math.hypot(px - dragStartX, py - dragStartY) > DRAG_THRESHOLD_PX) return;
|
||||
trails.push({ x: px, y: py, age: 0, seed: Math.random() * Math.PI * 2 });
|
||||
if (trails.length > MAX_TRAIL_POINTS) trails.shift();
|
||||
}
|
||||
|
||||
function applyTrails() {
|
||||
if (!trails.length) return;
|
||||
const lifetimeSec = TRAIL_LIFETIME_MS / 1000;
|
||||
const cellsPerRadius = TRAIL_RADIUS_PX / CELL_SIZE;
|
||||
for (let i = trails.length - 1; i >= 0; i--) {
|
||||
const trail = trails[i];
|
||||
trail.age += DT;
|
||||
const progress = trail.age / lifetimeSec;
|
||||
if (progress >= 1) {
|
||||
trails.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
const eased = 1 - Math.pow(1 - progress, TRAIL_EASE);
|
||||
const ringRadius = eased * TRAIL_RADIUS_PX / CELL_SIZE;
|
||||
const remaining = 1 - progress;
|
||||
const intensity = 0.35 + 0.65 * eased;
|
||||
const force = TRAIL_STRENGTH * Math.pow(remaining, TRAIL_FORCE_DECAY);
|
||||
const dyeAmount = 0.2 * Math.pow(remaining, 2);
|
||||
const cx = trail.x / CELL_SIZE;
|
||||
const cy = trail.y / CELL_SIZE;
|
||||
const seed = trail.seed;
|
||||
const band = ringRadius * 1.3 + cellsPerRadius;
|
||||
const xMin = Math.max(1, Math.floor(cx - band));
|
||||
const xMax = Math.min(cols - 2, Math.ceil(cx + band));
|
||||
const yMin = Math.max(1, Math.floor(cy - band));
|
||||
const yMax = Math.min(rows - 2, Math.ceil(cy + band));
|
||||
for (let y = yMin; y <= yMax; y++) {
|
||||
for (let x = xMin; x <= xMax; x++) {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const wobble = 0.5 * Math.sin(6 * angle + seed) + 0.25 * Math.sin(3 * angle + 0.05 * dist + 0.3 * seed) + 0.5;
|
||||
const wobbleAmount = TRAIL_WOBBLE * (2 * wobble - 1) * intensity;
|
||||
const targetRadius = ringRadius * Math.min(1.6, Math.max(0.6, 1 + wobbleAmount));
|
||||
const ringDist = Math.abs(dist - targetRadius) / cellsPerRadius;
|
||||
if (ringDist >= 1) continue;
|
||||
const falloff = smoothstep(1 - ringDist);
|
||||
const forceMul = Math.min(2.2, Math.max(0.4, 1 + wobbleAmount));
|
||||
const invDist = dist > 0.001 ? 1 / dist : 0;
|
||||
const idx = x + y * cols;
|
||||
velX[idx] += dx * invDist * force * forceMul * falloff;
|
||||
velY[idx] += dy * invDist * force * forceMul * falloff;
|
||||
dye[idx] = Math.min(1.5, dye[idx] + dyeAmount * forceMul * falloff);
|
||||
if (Math.random() < 0.03 * falloff) glyphs[idx] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function step() {
|
||||
applyTrails();
|
||||
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.04 + videoLuminance * 0.22;
|
||||
if (d <= 0.02) {
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.globalAlpha = baseAlpha;
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
|
||||
ctx.fillText(hex, x * CELL_SIZE + half, y * CELL_SIZE + half);
|
||||
} else {
|
||||
const alpha = d * FADE_RATE;
|
||||
const activeAlpha = Math.min(alpha + baseAlpha, MAX_ALPHA);
|
||||
ctx.globalAlpha = activeAlpha;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.shadowColor = '#ffffff';
|
||||
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('pointerdown', onPointerDown, { passive: true });
|
||||
window.addEventListener('pointerup', onPointerUp, { passive: true });
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
resize();
|
||||
rafId = requestAnimationFrame(frame);
|
||||
}
|
||||
81
src/scripts/animations/fluid-solver.js
Normal file
@@ -0,0 +1,81 @@
|
||||
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);
|
||||
}
|
||||
41
src/scripts/animations/marquee-loop.js
Normal file
@@ -0,0 +1,41 @@
|
||||
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 * 2.5) / (items.length * itemWidthPx)));
|
||||
for (let i = 0; i < minSets; i++) {
|
||||
items.forEach((item) => track.appendChild(buildItem(item)));
|
||||
}
|
||||
}
|
||||
25
src/scripts/animations/reveal-observer.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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));
|
||||
}
|
||||
11
src/scripts/animations/typewriter.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
13
src/scripts/animations/videos.js
Normal file
@@ -0,0 +1,13 @@
|
||||
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(() => { });
|
||||
});
|
||||
});
|
||||
39
src/scripts/components/faq-item.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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.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;
|
||||
}
|
||||
62
src/scripts/components/script-card.js
Normal file
@@ -0,0 +1,62 @@
|
||||
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">
|
||||
</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;
|
||||
}
|
||||
25
src/scripts/components/showcase-item.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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="">
|
||||
</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;
|
||||
}
|
||||
32
src/scripts/components/testimonial-card.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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');
|
||||
const avatar = testimonial.avatar
|
||||
? `<img src="${testimonial.avatar}" alt="">`
|
||||
: initials(testimonial.name || '?');
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="testimonial-top">
|
||||
<div class="testimonial-user">
|
||||
<div class="testimonial-avatar">${avatar}</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;
|
||||
}
|
||||
17
src/scripts/config/constants.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export const DISCORD_INVITE_CODE = '5eqRhKeywG';
|
||||
export const DISCORD_INVITE_URL = `https://discord.gg/${DISCORD_INVITE_CODE}`;
|
||||
export const DISCORD_LOADER_SCRIPT = 'loadstring(game:HttpGet("https://gitlab.com/soluna-development/api/-/raw/main/src/Loader.lua"))()';
|
||||
|
||||
export const DATA_PATHS = {
|
||||
scripts: 'src/data/scripts.json',
|
||||
partners: 'src/data/partners.json',
|
||||
reviews: 'src/data/reviews.json',
|
||||
showcases: 'src/data/showcases.json',
|
||||
};
|
||||
|
||||
export const DISCORD_INVITE_API = `https://discord.com/api/v10/invites/${DISCORD_INVITE_CODE}?with_counts=true`;
|
||||
export const WEAO_EXECUTORS_ENDPOINT = 'https://weao.xyz/api/status/exploits';
|
||||
|
||||
export const COPY_FEEDBACK_DURATION_MS = 1800;
|
||||
export const REVEAL_THRESHOLD = 0.15;
|
||||
export const REVEAL_ROOT_MARGIN = '0px 0px -40px 0px';
|
||||
30
src/scripts/config/faq-data.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export const FAQ_ITEMS = [
|
||||
{
|
||||
q: 'Why was the website version discontinued?',
|
||||
a: 'Hosting Vyla on a public VPS caused repeated Cloudflare DMCA reports, datacenter IP blocks, and hosting instability. Moving to Vyla Home allows the app to run locally on your own network with zero server dependencies or datacenter blocks.',
|
||||
},
|
||||
{
|
||||
q: 'How do I install and launch Vyla Home?',
|
||||
a: 'Download the latest setup executable (Vyla.Home.Setup.exe) from our official GitLab releases page. Run the installer, and the app will automatically configure all local microservices, player sandboxes, and runtimes.',
|
||||
},
|
||||
{
|
||||
q: 'How do updates work?',
|
||||
a: 'Vyla Home checks for updates directly from the official GitLab repository upon startup. When an update is available, it downloads the payload, verifies SHA-256 checksums, and applies patches automatically.',
|
||||
},
|
||||
{
|
||||
q: 'Where are my watch history, favorites, and settings stored?',
|
||||
a: 'All data is stored locally on your machine in an encrypted vault. Your credentials and settings are secured using AES-256-GCM encryption tied to your unique hardware profile.',
|
||||
},
|
||||
{
|
||||
q: 'Do I need to install Node.js, Python, or other dependencies?',
|
||||
a: 'No. The desktop setup bundles all required runtimes, backend microservices, and dependencies inside an isolated Electron package.',
|
||||
},
|
||||
{
|
||||
q: 'Where can I report bugs or suggest new features?',
|
||||
a: 'Join our official Discord community where you can report issues, get technical support, submit feature suggestions, and receive update announcements.',
|
||||
},
|
||||
{
|
||||
q: 'Is Vyla Home free to use?',
|
||||
a: 'Yes, Vyla Home is completely free for personal use with no keys, subscriptions, or paywalls.',
|
||||
},
|
||||
];
|
||||
34
src/scripts/main.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { observeReveals } from './animations/reveal-observer.js';
|
||||
import { initHeroSection } from './sections/hero.js';
|
||||
import { initStatsSection } from './sections/stats.js';
|
||||
import { initScriptsSection } from './sections/scripts.js';
|
||||
import { initTestimonialsSection } from './sections/testimonials.js';
|
||||
import { initExecutorsSection } from './sections/executors.js';
|
||||
import { initShowcasesSection } from './sections/showcases.js';
|
||||
import { initPartnersSection } from './sections/partners.js';
|
||||
import { initFaqSection } from './sections/faq.js';
|
||||
import { getExecutors } from './services/executors-service.js';
|
||||
|
||||
function initApp() {
|
||||
observeReveals();
|
||||
|
||||
initHeroSection();
|
||||
initStatsSection();
|
||||
initScriptsSection();
|
||||
initTestimonialsSection();
|
||||
initExecutorsSection();
|
||||
initShowcasesSection();
|
||||
initPartnersSection();
|
||||
initFaqSection();
|
||||
|
||||
window.debugExecutors = async () => {
|
||||
try {
|
||||
const executors = await getExecutors();
|
||||
return executors;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initApp);
|
||||
35
src/scripts/sections/executors.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { getExecutors } from '../services/executors-service.js';
|
||||
import { createElement } from '../utils/dom.js';
|
||||
import { createMarqueeLoop, populateLoopedTrack } from '../animations/marquee-loop.js';
|
||||
|
||||
const EXECUTOR_ITEM_WIDTH_PX = 180;
|
||||
const BASE_SPEED = 0.35;
|
||||
const BOOSTED_SPEED = 2.2;
|
||||
|
||||
function createExecutorItem(executor) {
|
||||
const el = createElement('div', 'executor-item');
|
||||
const avatar = executor.image
|
||||
? `<img src="${executor.image}" alt="" " onerror="this.parentElement.innerHTML=''">`
|
||||
: '';
|
||||
el.innerHTML = `
|
||||
<span class="executor-avatar">${avatar}</span>
|
||||
<span class="executor-name">${executor.name}</span>
|
||||
`;
|
||||
return el;
|
||||
}
|
||||
|
||||
export async function initExecutorsSection() {
|
||||
const track = document.getElementById('executors-track');
|
||||
if (!track) return;
|
||||
|
||||
try {
|
||||
const executors = await getExecutors();
|
||||
populateLoopedTrack(track, executors, createExecutorItem, EXECUTOR_ITEM_WIDTH_PX);
|
||||
createMarqueeLoop(track, '--marquee-x', { baseSpeed: BASE_SPEED, boostedSpeed: BOOSTED_SPEED });
|
||||
} catch (error) {
|
||||
const wrap = document.querySelector('.executors-marquee-wrap');
|
||||
if (wrap) {
|
||||
wrap.innerHTML = '<p style="color: var(--color-muted); text-align:center; width:100%">executors unavailable.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/scripts/sections/faq.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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);
|
||||
}
|
||||
55
src/scripts/sections/hero.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { qs, isMobileViewport } from '../utils/dom.js';
|
||||
import { copyToClipboard, flashFeedback, flashClass } from '../utils/clipboard.js';
|
||||
import { typewriterReveal } from '../animations/typewriter.js';
|
||||
import { initFluidByteField } from '../animations/fluid-byte-field.js';
|
||||
import { DISCORD_LOADER_SCRIPT, COPY_FEEDBACK_DURATION_MS } from '../config/constants.js';
|
||||
|
||||
const HERO_GREETING = 'Soluna Development';
|
||||
|
||||
function initHeroTitle() {
|
||||
const heroTitle = qs('.hero-title');
|
||||
if (heroTitle) typewriterReveal(heroTitle, HERO_GREETING);
|
||||
}
|
||||
|
||||
function initDiscordCopyBar() {
|
||||
const copyBtn = document.getElementById('copy-discord-btn');
|
||||
const copyLabel = document.getElementById('copy-discord-label');
|
||||
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() {
|
||||
initHeroTitle();
|
||||
initDiscordCopyBar();
|
||||
initHeroCopyButton();
|
||||
initCtaCopyButton();
|
||||
if (!isMobileViewport()) {
|
||||
initFluidByteField('hero-byte-overlay');
|
||||
initFluidByteField('cta-byte-overlay');
|
||||
}
|
||||
}
|
||||
26
src/scripts/sections/partners.js
Normal file
@@ -0,0 +1,26 @@
|
||||
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');
|
||||
const inner = partner.link
|
||||
? `<a href="${partner.link}" target="_blank" rel="noopener noreferrer"><img src="${partner.logo}" alt="${partner.name}" "></a>`
|
||||
: `<img src="${partner.logo}" alt="${partner.name}" ">`;
|
||||
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 = '';
|
||||
}
|
||||
}
|
||||
40
src/scripts/sections/scripts.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getScripts } from '../services/scripts-service.js';
|
||||
import { createScriptCard } from '../components/script-card.js';
|
||||
import { debounce, isMobileViewport } from '../utils/dom.js';
|
||||
|
||||
let snapObserver = null;
|
||||
|
||||
function setupSnapGlow(grid) {
|
||||
if (snapObserver) snapObserver.disconnect();
|
||||
if (!isMobileViewport()) return;
|
||||
|
||||
snapObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
entry.target.classList.toggle('in-snap', entry.intersectionRatio >= 0.7);
|
||||
});
|
||||
},
|
||||
{ root: grid, threshold: [0, 0.7, 1] }
|
||||
);
|
||||
|
||||
grid.querySelectorAll('.script-card').forEach((card) => snapObserver.observe(card));
|
||||
}
|
||||
|
||||
export async function initScriptsSection() {
|
||||
const grid = document.getElementById('scripts-grid');
|
||||
if (!grid) return;
|
||||
|
||||
try {
|
||||
const scripts = await getScripts();
|
||||
grid.innerHTML = '';
|
||||
scripts.forEach((script) => {
|
||||
const card = createScriptCard(script);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
setupSnapGlow(grid);
|
||||
|
||||
window.addEventListener('resize', debounce(() => setupSnapGlow(grid), 200));
|
||||
} catch (error) {
|
||||
grid.innerHTML = '<p style="color: var(--color-muted)">scripts unavailable.</p>';
|
||||
}
|
||||
}
|
||||
23
src/scripts/sections/showcases.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getShowcaseVideos } from '../services/showcases-service.js';
|
||||
import { createShowcaseItem } from '../components/showcase-item.js';
|
||||
import { createMarqueeLoop, populateLoopedTrack } from '../animations/marquee-loop.js';
|
||||
|
||||
const SHOWCASE_ITEM_WIDTH_PX = 280;
|
||||
const BASE_SPEED = 0.4;
|
||||
const BOOSTED_SPEED = 2.4;
|
||||
|
||||
export async function initShowcasesSection() {
|
||||
const track = document.getElementById('showcase-track');
|
||||
if (!track) return;
|
||||
|
||||
try {
|
||||
const videos = await getShowcaseVideos();
|
||||
populateLoopedTrack(track, videos, createShowcaseItem, SHOWCASE_ITEM_WIDTH_PX);
|
||||
createMarqueeLoop(track, '--showcase-x', { baseSpeed: BASE_SPEED, boostedSpeed: BOOSTED_SPEED });
|
||||
} catch (error) {
|
||||
const viewport = document.querySelector('.showcase-viewport');
|
||||
if (viewport) {
|
||||
viewport.innerHTML = '<p style="color: var(--color-muted); text-align:center; width:100%">showcases unavailable.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/scripts/sections/stats.js
Normal file
@@ -0,0 +1,52 @@
|
||||
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();
|
||||
}
|
||||
38
src/scripts/sections/testimonials.js
Normal file
@@ -0,0 +1,38 @@
|
||||
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 * 3) / (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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/scripts/services/discord-service.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
43
src/scripts/services/executors-service.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { WEAO_EXECUTORS_ENDPOINT } from '../config/constants.js';
|
||||
|
||||
function faviconFromUrl(websiteUrl) {
|
||||
try {
|
||||
const host = new URL(websiteUrl).hostname;
|
||||
return `https://favicone.com/${host}?s=64`;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function dedupeByName(executors) {
|
||||
return executors.filter(
|
||||
(executor, index, all) => all.findIndex((e) => e.name === executor.name) === index
|
||||
);
|
||||
}
|
||||
|
||||
export async function getExecutors() {
|
||||
const response = await fetch(WEAO_EXECUTORS_ENDPOINT, {
|
||||
headers: {
|
||||
'User-Agent': 'WEAO-3PService'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch executor status: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
throw new Error('Empty executor response');
|
||||
}
|
||||
|
||||
const executors = data
|
||||
.filter((entry) => !entry.hidden && entry.extype !== 'wexternal')
|
||||
.map((entry) => ({
|
||||
name: entry.title,
|
||||
image: entry.websitelink ? faviconFromUrl(entry.websitelink) : '',
|
||||
}));
|
||||
|
||||
return dedupeByName(executors);
|
||||
}
|
||||
14
src/scripts/services/json-source.js
Normal file
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
}
|
||||
6
src/scripts/services/partners-service.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { DATA_PATHS } from '../config/constants.js';
|
||||
import { fetchJsonArray } from './json-source.js';
|
||||
|
||||
export function getPartners() {
|
||||
return fetchJsonArray(DATA_PATHS.partners);
|
||||
}
|
||||
6
src/scripts/services/reviews-service.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { DATA_PATHS } from '../config/constants.js';
|
||||
import { fetchJsonArray } from './json-source.js';
|
||||
|
||||
export function getReviews() {
|
||||
return fetchJsonArray(DATA_PATHS.reviews);
|
||||
}
|
||||
6
src/scripts/services/scripts-service.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { DATA_PATHS } from '../config/constants.js';
|
||||
import { fetchJsonArray } from './json-source.js';
|
||||
|
||||
export function getScripts() {
|
||||
return fetchJsonArray(DATA_PATHS.scripts);
|
||||
}
|
||||
12
src/scripts/services/showcases-service.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { DATA_PATHS } from '../config/constants.js';
|
||||
import { fetchJsonArray } from './json-source.js';
|
||||
import { youtubeIdFromUrl } from '../utils/youtube.js';
|
||||
|
||||
export async function getShowcaseVideos() {
|
||||
const links = await fetchJsonArray(DATA_PATHS.showcases);
|
||||
const videos = links
|
||||
.map((url) => ({ url, id: youtubeIdFromUrl(url) }))
|
||||
.filter((video) => video.id);
|
||||
if (!videos.length) throw new Error('No valid showcase videos found');
|
||||
return videos;
|
||||
}
|
||||
42
src/scripts/utils/clipboard.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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);
|
||||
}
|
||||
72
src/scripts/utils/color-extract.js
Normal file
@@ -0,0 +1,72 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
29
src/scripts/utils/dom.js
Normal file
@@ -0,0 +1,29 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
23
src/scripts/utils/youtube.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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`;
|
||||
}
|
||||
69
src/styles/base/reset.css
Normal file
@@ -0,0 +1,69 @@
|
||||
*,
|
||||
*::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;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
min-height: 100%;
|
||||
min-height: 100dvh;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
46
src/styles/base/tokens.css
Normal file
@@ -0,0 +1,46 @@
|
||||
: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);
|
||||
}
|
||||
41
src/styles/base/typography.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.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);
|
||||
}
|
||||
82
src/styles/components/buttons.css
Normal file
@@ -0,0 +1,82 @@
|
||||
.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: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.7rem 1.35rem;
|
||||
border-radius: var(--radius-pill);
|
||||
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: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 {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #0f0f0f;
|
||||
}
|
||||
|
||||
.hero-cta-light:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.btn {
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
}
|
||||
}
|
||||
59
src/styles/components/executors.css
Normal file
@@ -0,0 +1,59 @@
|
||||
.executors-marquee-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.executors-marquee-row {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.executors-marquee-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(2rem, 5vw, 3.5rem);
|
||||
width: max-content;
|
||||
transform: translate3d(var(--marquee-x, 0px), 0, 0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.executor-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.45;
|
||||
filter: grayscale(0.3);
|
||||
transition: opacity var(--duration-base) ease, filter var(--duration-base) ease;
|
||||
}
|
||||
|
||||
.executor-item:hover {
|
||||
opacity: 1;
|
||||
filter: grayscale(0);
|
||||
}
|
||||
|
||||
.executor-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: var(--color-border);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.executor-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.executor-name {
|
||||
font-size: clamp(0.95rem, 2vw, 1.15rem);
|
||||
font-weight: 700;
|
||||
color: var(--color-fg);
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
59
src/styles/components/faq.css
Normal file
@@ -0,0 +1,59 @@
|
||||
.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;
|
||||
}
|
||||
77
src/styles/components/marquee.css
Normal file
@@ -0,0 +1,77 @@
|
||||
.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: hidden;
|
||||
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;
|
||||
}
|
||||
}
|
||||
101
src/styles/components/modal.css
Normal file
@@ -0,0 +1,101 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
49
src/styles/components/partners.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.partners-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
padding-top: 120px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
83
src/styles/components/pills.css
Normal file
@@ -0,0 +1,83 @@
|
||||
.discord-copy-bar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(140%);
|
||||
border: none;
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 0.5rem 0.5rem 0.5rem 0.9rem;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-fg);
|
||||
cursor: pointer;
|
||||
letter-spacing: -0.01em;
|
||||
margin-top: 1.2rem;
|
||||
transition: border-color var(--duration-fast) ease, background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.copy-discord-label {
|
||||
position: relative;
|
||||
max-width: 350px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
color: var(--color-fg);
|
||||
-webkit-mask-image: linear-gradient(to left, transparent, black 15%);
|
||||
mask-image: linear-gradient(to left, transparent, black 15%);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
170
src/styles/components/script-card.css
Normal file
@@ -0,0 +1,170 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
77
src/styles/components/showcase.css
Normal file
@@ -0,0 +1,77 @@
|
||||
.showcase-viewport {
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
|
||||
mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
|
||||
}
|
||||
|
||||
.showcase-track {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
gap: clamp(0.75rem, 1.5vw, 1.25rem);
|
||||
transform: translate3d(var(--showcase-x, 0px), 0, 0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.showcase-item {
|
||||
flex: 0 0 auto;
|
||||
width: clamp(220px, 22vw, 280px);
|
||||
opacity: 0;
|
||||
transition: opacity var(--duration-base) ease;
|
||||
}
|
||||
|
||||
.showcase-item.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.showcase-card {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--color-border);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0;
|
||||
transition: transform var(--duration-base) var(--ease-standard);
|
||||
}
|
||||
|
||||
.showcase-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: filter var(--duration-base) ease;
|
||||
}
|
||||
|
||||
.showcase-card:hover .showcase-thumb {
|
||||
filter: brightness(0.7);
|
||||
}
|
||||
|
||||
.showcase-play {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
transition: background var(--duration-fast) ease, transform var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.showcase-card:hover .showcase-play {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.showcase-item {
|
||||
width: clamp(200px, 62vw, 260px);
|
||||
}
|
||||
}
|
||||
32
src/styles/components/stats.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.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;
|
||||
}
|
||||
79
src/styles/components/testimonial-card.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.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;
|
||||
}
|
||||
62
src/styles/layout/footer.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.footer {
|
||||
position: relative;
|
||||
padding: 48px 24px 24px;
|
||||
z-index: 1000;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.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
|
||||
}
|
||||
|
||||
.footer-section h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 7px
|
||||
}
|
||||
|
||||
.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-muted)
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.footer-bottom p {
|
||||
color: var(--color-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400
|
||||
}
|
||||
67
src/styles/layout/nav.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.nav {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1.25rem 0;
|
||||
z-index: var(--z-nav);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: clamp(1.25rem, 3vw, 2.5rem);
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-fg-dim);
|
||||
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 {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
35
src/styles/main.css
Normal file
@@ -0,0 +1,35 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap');
|
||||
|
||||
@import 'base/tokens.css';
|
||||
@import 'base/reset.css';
|
||||
@import 'base/typography.css';
|
||||
|
||||
@import 'utilities/layout.css';
|
||||
@import 'utilities/reveal.css';
|
||||
|
||||
@import 'layout/nav.css';
|
||||
@import 'layout/footer.css';
|
||||
|
||||
@import 'components/buttons.css';
|
||||
@import 'components/pills.css';
|
||||
@import 'components/stats.css';
|
||||
@import 'components/script-card.css';
|
||||
@import 'components/marquee.css';
|
||||
@import 'components/testimonial-card.css';
|
||||
@import 'components/executors.css';
|
||||
@import 'components/showcase.css';
|
||||
@import 'components/modal.css';
|
||||
@import 'components/partners.css';
|
||||
@import 'components/faq.css';
|
||||
|
||||
@import 'sections/hero-atmosphere.css';
|
||||
@import 'sections/hero-content.css';
|
||||
@import 'sections/section-base.css';
|
||||
@import 'sections/testimonials.css';
|
||||
@import 'sections/executors.css';
|
||||
@import 'sections/features.css';
|
||||
@import 'sections/showcases.css';
|
||||
@import 'sections/faq-section.css';
|
||||
@import 'sections/cta-banner.css';
|
||||
|
||||
@import 'utilities/responsive.css';
|
||||
79
src/styles/sections/cta-banner.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.cta-banner {
|
||||
position: relative;
|
||||
padding: clamp(4rem, 10vw, 7rem) 0;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.cta-banner-media {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
-webkit-mask-image:
|
||||
linear-gradient(to bottom, transparent, black 50%),
|
||||
linear-gradient(to top, transparent, black 70%);
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-image:
|
||||
linear-gradient(to bottom, transparent, black 50%),
|
||||
linear-gradient(to top, transparent, black 65%);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
.cta-banner-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.cta-banner-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
#cta-byte-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#cta-byte-overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cta-banner-content {
|
||||
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;
|
||||
}
|
||||
12
src/styles/sections/executors.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.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);
|
||||
}
|
||||
8
src/styles/sections/faq-section.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.faq-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: clamp(2rem, 6vw, 3.5rem);
|
||||
}
|
||||
91
src/styles/sections/features.css
Normal file
@@ -0,0 +1,91 @@
|
||||
.features-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(3.5rem, 8vw, 6rem);
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.feature-block,
|
||||
.feature-block-reverse {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-block-reverse .feature-media,
|
||||
.feature-block-reverse .feature-copy {
|
||||
order: initial;
|
||||
}
|
||||
}
|
||||
109
src/styles/sections/hero-atmosphere.css
Normal file
@@ -0,0 +1,109 @@
|
||||
.hero {
|
||||
position: relative;
|
||||
padding: clamp(6rem, 14vh, 8.5rem) 0 clamp(3rem, 6vh, 5rem);
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-atmosphere {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
-webkit-mask-image: linear-gradient(to bottom,
|
||||
black 0%,
|
||||
black 48%,
|
||||
rgba(0, 0, 0, 0.5) 80%,
|
||||
transparent 96%);
|
||||
mask-image: linear-gradient(to bottom,
|
||||
black 0%,
|
||||
black 48%,
|
||||
rgba(0, 0, 0, 0.5) 80%,
|
||||
transparent 96%);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
.hero-atmosphere-video {
|
||||
position: absolute;
|
||||
inset: -6%;
|
||||
width: 112%;
|
||||
height: 112%;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
transform: translateZ(0) scale(1.04);
|
||||
}
|
||||
|
||||
.hero-atmosphere-sheen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at 50% 4%, rgba(255, 255, 255, 0.18), transparent 46%),
|
||||
linear-gradient(116deg, transparent 18%, rgba(255, 255, 255, 0.06) 37%, transparent 51%);
|
||||
mix-blend-mode: soft-light;
|
||||
opacity: 0.8;
|
||||
animation: hero-sheen 16s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.hero-atmosphere-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to bottom, transparent 15%, rgba(0, 0, 0, 0.15) 55%, rgba(0, 0, 0, 0.95) 100%),
|
||||
radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.2) 100%);
|
||||
}
|
||||
|
||||
#hero-byte-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.9;
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes hero-sheen {
|
||||
0% {
|
||||
transform: translate3d(-4%, 0, 0) scale(1.04);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(5%, 1%, 0) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#hero-byte-overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-atmosphere-sheen {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.hero-atmosphere-video,
|
||||
#hero-byte-overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hero {
|
||||
padding-top: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 600px) {
|
||||
.hero {
|
||||
height: auto;
|
||||
min-height: 100svh;
|
||||
padding-top: 5rem;
|
||||
}
|
||||
}
|
||||
84
src/styles/sections/hero-content.css
Normal file
@@ -0,0 +1,84 @@
|
||||
.hero-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.logo-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(2rem, calc(2rem + 2 * ((100vw - 23.4375rem) / 66.5625)), 4rem);
|
||||
line-height: clamp(2.28rem, calc(2.28rem + 1.72 * ((100vw - 23.4375rem) / 66.5625)), 4rem);
|
||||
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;
|
||||
filter: blur(10px);
|
||||
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: 80%;
|
||||
}
|
||||
|
||||
.hero-description a {
|
||||
color: var(--color-fg-dim);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
@keyframes char-in {
|
||||
to {
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-title .char {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-description {
|
||||
margin-top: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 600px) {
|
||||
.hero-title {
|
||||
margin-bottom: 0rem;
|
||||
font-size: 2.7rem;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
30
src/styles/sections/section-base.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
13
src/styles/sections/showcases.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.showcases-section {
|
||||
padding: clamp(3rem, 7vw, 5rem) 0 clamp(2rem, 6vw, 4rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.showcases-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: clamp(2rem, 5vw, 3rem);
|
||||
}
|
||||
23
src/styles/sections/testimonials.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.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;
|
||||
}
|
||||
27
src/styles/utilities/layout.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.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;
|
||||
}
|
||||
38
src/styles/utilities/responsive.css
Normal file
@@ -0,0 +1,38 @@
|
||||
@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: 95%;
|
||||
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: 70vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.script-links a {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
52
src/styles/utilities/reveal.css
Normal file
@@ -0,0 +1,52 @@
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
filter: blur(12px);
|
||||
transform: translate3d(0, 24px, 0);
|
||||
transition: opacity var(--duration-slow) var(--ease-standard),
|
||||
filter var(--duration-slow) var(--ease-standard),
|
||||
transform var(--duration-slow) var(--ease-standard);
|
||||
will-change: opacity, filter, transform;
|
||||
}
|
||||
|
||||
.reveal.in-view {
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.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;
|
||||
filter: none;
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||