diff --git a/assets/images/features/1.png b/assets/images/features/1.png new file mode 100644 index 0000000..6f552d4 Binary files /dev/null and b/assets/images/features/1.png differ diff --git a/assets/images/features/2.png b/assets/images/features/2.png new file mode 100644 index 0000000..5f5ef1c Binary files /dev/null and b/assets/images/features/2.png differ diff --git a/assets/images/features/3.png b/assets/images/features/3.png new file mode 100644 index 0000000..ed65f13 Binary files /dev/null and b/assets/images/features/3.png differ diff --git a/assets/images/features/4.png b/assets/images/features/4.png new file mode 100644 index 0000000..22d4d05 Binary files /dev/null and b/assets/images/features/4.png differ diff --git a/assets/images/features/5.png b/assets/images/features/5.png new file mode 100644 index 0000000..ffa792d Binary files /dev/null and b/assets/images/features/5.png differ diff --git a/assets/images/features/6.png b/assets/images/features/6.png new file mode 100644 index 0000000..ce67ec4 Binary files /dev/null and b/assets/images/features/6.png differ diff --git a/assets/images/header_logo.png b/assets/images/header_logo.png new file mode 100644 index 0000000..a80a11d Binary files /dev/null and b/assets/images/header_logo.png differ diff --git a/favicon.ico b/assets/images/logo.png similarity index 100% rename from favicon.ico rename to assets/images/logo.png diff --git a/assets/images/partners/fmhy.png b/assets/images/partners/fmhy.png new file mode 100644 index 0000000..eb21dc4 Binary files /dev/null and b/assets/images/partners/fmhy.png differ diff --git a/assets/images/partners/piracy.png b/assets/images/partners/piracy.png new file mode 100644 index 0000000..abbb555 Binary files /dev/null and b/assets/images/partners/piracy.png differ diff --git a/assets/images/testimonials/acideetdeforme.png b/assets/images/testimonials/acideetdeforme.png new file mode 100644 index 0000000..71bab81 Binary files /dev/null and b/assets/images/testimonials/acideetdeforme.png differ diff --git a/assets/images/testimonials/block.tale.adict.png b/assets/images/testimonials/block.tale.adict.png new file mode 100644 index 0000000..8093000 Binary files /dev/null and b/assets/images/testimonials/block.tale.adict.png differ diff --git a/assets/images/testimonials/chariachtus.png b/assets/images/testimonials/chariachtus.png new file mode 100644 index 0000000..04cd686 Binary files /dev/null and b/assets/images/testimonials/chariachtus.png differ diff --git a/assets/images/testimonials/lucky_mucky_.png b/assets/images/testimonials/lucky_mucky_.png new file mode 100644 index 0000000..f96a15d Binary files /dev/null and b/assets/images/testimonials/lucky_mucky_.png differ diff --git a/assets/images/testimonials/snorer.png b/assets/images/testimonials/snorer.png new file mode 100644 index 0000000..df41d20 Binary files /dev/null and b/assets/images/testimonials/snorer.png differ diff --git a/assets/images/testimonials/zenithrifle.png b/assets/images/testimonials/zenithrifle.png new file mode 100644 index 0000000..4f05231 Binary files /dev/null and b/assets/images/testimonials/zenithrifle.png differ diff --git a/assets/images/videos/header_video.mp4 b/assets/images/videos/header_video.mp4 new file mode 100644 index 0000000..62f5eed Binary files /dev/null and b/assets/images/videos/header_video.mp4 differ diff --git a/index.html b/index.html index 87f6d30..60548e6 100644 --- a/index.html +++ b/index.html @@ -1,219 +1,371 @@ - - - Vyla - - - - - - - - - - - - - - - - - - - - - - - - - -
- Vyla is no longer available as a hosted website. Use Vyla Home to access Vyla on desktop. -
- - - - - \ No newline at end of file + + + + + + + + Vyla Home + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+ Vyla Home +
+ +

Vyla Home

+ +

We've completely stopped hosting Vyla on a public VPS. Vyla Home is now the + primary, self-contained way to experience the platform. +

+ + + + + +
+

Bypass datacenter blocks, ISP throttling, and cloud downtime.

+
+
+
+
+ +
+
+ + Discord Members +
+
+ + Online Now +
+ +
+ + + +
+
+
+

Why choose Home?

+
+ +
+
+ Datacenter IP Throttling +
+
+

No Datacenter Throttling

+

+ Shared hosting nodes and VPS providers routinely block traffic originating from datacenters. + We removed the datacenter entirely. Connect directly via your residential network. +

+
+
+ +
+
+ Single Point of Failure +
+
+

No Single Point of Failure

+

+ When a central domain gets flagged or suspended, everyone goes dark. Running locally means + your instance cannot be taken down by third parties. +

+
+
+ +
+
+ Encrypted Local Vault +
+
+

Encrypted Local Vault

+

+ Your bookmarks, watch history, settings, and tokens are stored in an encrypted vault keyed + uniquely to your hardware profile. Total privacy. +

+
+
+ +
+
+ Automated OTA Updates +
+
+

Automated OTA Updates

+

+ Vyla Home verifies releases directly from the repository upon startup. Update assets are + downloaded, checksum-verified, and patched automatically. +

+
+
+ +
+
+ Rich Discovery +
+
+

Rich Discovery & Curated Media

+

+ Access curated collections, 4K releases, popular anime series, soundcloak music streaming, + and detailed TMDB cast breakdowns seamlessly. +

+
+
+ +
+
+ Customizable Interface +
+
+

Customizable Interface

+

+ Toggle dynamic color adaptations, trailer background playback, custom color themes, + text-only mode, and layout preferences with full persistent storage. +

+
+
+
+
+ + + +
+
+
+

Here's what people say about Vyla Home

+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+

FAQ

+

Still have questions? Ask in our Discord

+
+
+
+
+
+ +
+ +
+

Try Vyla Home today

+

Your Entertainment. Zero Servers Attached.

+ Download Now +
+
+ + + + + + + \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..2112aed --- /dev/null +++ b/manifest.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/manifest.webmanifest b/manifest.webmanifest deleted file mode 100644 index 5b15df5..0000000 --- a/manifest.webmanifest +++ /dev/null @@ -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" - } - ] - } \ No newline at end of file diff --git a/src/data/partners.json b/src/data/partners.json new file mode 100644 index 0000000..e113bb8 --- /dev/null +++ b/src/data/partners.json @@ -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" + } +] \ No newline at end of file diff --git a/src/data/reviews.json b/src/data/reviews.json new file mode 100644 index 0000000..96fcb6c --- /dev/null +++ b/src/data/reviews.json @@ -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" + } +] \ No newline at end of file diff --git a/src/data/scripts.json b/src/data/scripts.json new file mode 100644 index 0000000..315acc5 --- /dev/null +++ b/src/data/scripts.json @@ -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" + } + ] + } +] \ No newline at end of file diff --git a/src/data/showcases.json b/src/data/showcases.json new file mode 100644 index 0000000..2cf9ba3 --- /dev/null +++ b/src/data/showcases.json @@ -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" +] \ No newline at end of file diff --git a/src/scripts/animations/counter.js b/src/scripts/animations/counter.js new file mode 100644 index 0000000..41a9fea --- /dev/null +++ b/src/scripts/animations/counter.js @@ -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); +} diff --git a/src/scripts/animations/fluid-byte-field.js b/src/scripts/animations/fluid-byte-field.js new file mode 100644 index 0000000..8e9b401 --- /dev/null +++ b/src/scripts/animations/fluid-byte-field.js @@ -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); +} diff --git a/src/scripts/animations/fluid-solver.js b/src/scripts/animations/fluid-solver.js new file mode 100644 index 0000000..cede280 --- /dev/null +++ b/src/scripts/animations/fluid-solver.js @@ -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); +} diff --git a/src/scripts/animations/marquee-loop.js b/src/scripts/animations/marquee-loop.js new file mode 100644 index 0000000..b32ed61 --- /dev/null +++ b/src/scripts/animations/marquee-loop.js @@ -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))); + } +} diff --git a/src/scripts/animations/reveal-observer.js b/src/scripts/animations/reveal-observer.js new file mode 100644 index 0000000..30b5314 --- /dev/null +++ b/src/scripts/animations/reveal-observer.js @@ -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)); +} diff --git a/src/scripts/animations/typewriter.js b/src/scripts/animations/typewriter.js new file mode 100644 index 0000000..1f61768 --- /dev/null +++ b/src/scripts/animations/typewriter.js @@ -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); + }); +} diff --git a/src/scripts/animations/videos.js b/src/scripts/animations/videos.js new file mode 100644 index 0000000..24a008f --- /dev/null +++ b/src/scripts/animations/videos.js @@ -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(() => { }); + }); +}); \ No newline at end of file diff --git a/src/scripts/components/faq-item.js b/src/scripts/components/faq-item.js new file mode 100644 index 0000000..a30ac20 --- /dev/null +++ b/src/scripts/components/faq-item.js @@ -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 = ` + +
+
${item.a}
+
+ `; + + 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; +} diff --git a/src/scripts/components/script-card.js b/src/scripts/components/script-card.js new file mode 100644 index 0000000..fc361b4 --- /dev/null +++ b/src/scripts/components/script-card.js @@ -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 = ` +
+
+ ${script.title} +
+

+ ${script.title} + + + + + + + + + +

`; + if (script.description) { + html += `

${script.description}

`; + } + card.innerHTML = html; + + attachGlow(card, qs('.script-image img', card)); + attachCopyHandler(card, script); + + return card; +} diff --git a/src/scripts/components/showcase-item.js b/src/scripts/components/showcase-item.js new file mode 100644 index 0000000..72bd13a --- /dev/null +++ b/src/scripts/components/showcase-item.js @@ -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 = ` + + `; + + 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; +} \ No newline at end of file diff --git a/src/scripts/components/testimonial-card.js b/src/scripts/components/testimonial-card.js new file mode 100644 index 0000000..d92f08c --- /dev/null +++ b/src/scripts/components/testimonial-card.js @@ -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 + ? `` + : initials(testimonial.name || '?'); + + card.innerHTML = ` +
+
+
${avatar}
+
+ ${testimonial.name} + ${testimonial.handle || ''} +
+
+
+

${testimonial.text}

+ `; + + return card; +} diff --git a/src/scripts/config/constants.js b/src/scripts/config/constants.js new file mode 100644 index 0000000..12f2881 --- /dev/null +++ b/src/scripts/config/constants.js @@ -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'; diff --git a/src/scripts/config/faq-data.js b/src/scripts/config/faq-data.js new file mode 100644 index 0000000..b4f0c59 --- /dev/null +++ b/src/scripts/config/faq-data.js @@ -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.', + }, +]; \ No newline at end of file diff --git a/src/scripts/main.js b/src/scripts/main.js new file mode 100644 index 0000000..628cf21 --- /dev/null +++ b/src/scripts/main.js @@ -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); \ No newline at end of file diff --git a/src/scripts/sections/executors.js b/src/scripts/sections/executors.js new file mode 100644 index 0000000..15de839 --- /dev/null +++ b/src/scripts/sections/executors.js @@ -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 + ? `` + : ''; + el.innerHTML = ` + ${avatar} + ${executor.name} + `; + 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 = '

executors unavailable.

'; + } + } +} diff --git a/src/scripts/sections/faq.js b/src/scripts/sections/faq.js new file mode 100644 index 0000000..63463f6 --- /dev/null +++ b/src/scripts/sections/faq.js @@ -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); +} diff --git a/src/scripts/sections/hero.js b/src/scripts/sections/hero.js new file mode 100644 index 0000000..5e095e6 --- /dev/null +++ b/src/scripts/sections/hero.js @@ -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'); + } +} \ No newline at end of file diff --git a/src/scripts/sections/partners.js b/src/scripts/sections/partners.js new file mode 100644 index 0000000..62842de --- /dev/null +++ b/src/scripts/sections/partners.js @@ -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 + ? `${partner.name}` + : `${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 = ''; + } +} diff --git a/src/scripts/sections/scripts.js b/src/scripts/sections/scripts.js new file mode 100644 index 0000000..709e4f4 --- /dev/null +++ b/src/scripts/sections/scripts.js @@ -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 = '

scripts unavailable.

'; + } +} diff --git a/src/scripts/sections/showcases.js b/src/scripts/sections/showcases.js new file mode 100644 index 0000000..bd52730 --- /dev/null +++ b/src/scripts/sections/showcases.js @@ -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 = '

showcases unavailable.

'; + } + } +} diff --git a/src/scripts/sections/stats.js b/src/scripts/sections/stats.js new file mode 100644 index 0000000..0b85e66 --- /dev/null +++ b/src/scripts/sections/stats.js @@ -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(); +} diff --git a/src/scripts/sections/testimonials.js b/src/scripts/sections/testimonials.js new file mode 100644 index 0000000..08ae150 --- /dev/null +++ b/src/scripts/sections/testimonials.js @@ -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 = '

reviews unavailable.

'; + } + } +} diff --git a/src/scripts/services/discord-service.js b/src/scripts/services/discord-service.js new file mode 100644 index 0000000..d24de6a --- /dev/null +++ b/src/scripts/services/discord-service.js @@ -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, + }; +} diff --git a/src/scripts/services/executors-service.js b/src/scripts/services/executors-service.js new file mode 100644 index 0000000..0ede078 --- /dev/null +++ b/src/scripts/services/executors-service.js @@ -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); +} \ No newline at end of file diff --git a/src/scripts/services/json-source.js b/src/scripts/services/json-source.js new file mode 100644 index 0000000..96026fc --- /dev/null +++ b/src/scripts/services/json-source.js @@ -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; +} diff --git a/src/scripts/services/partners-service.js b/src/scripts/services/partners-service.js new file mode 100644 index 0000000..f0ad219 --- /dev/null +++ b/src/scripts/services/partners-service.js @@ -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); +} diff --git a/src/scripts/services/reviews-service.js b/src/scripts/services/reviews-service.js new file mode 100644 index 0000000..17500dc --- /dev/null +++ b/src/scripts/services/reviews-service.js @@ -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); +} diff --git a/src/scripts/services/scripts-service.js b/src/scripts/services/scripts-service.js new file mode 100644 index 0000000..4b7b740 --- /dev/null +++ b/src/scripts/services/scripts-service.js @@ -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); +} diff --git a/src/scripts/services/showcases-service.js b/src/scripts/services/showcases-service.js new file mode 100644 index 0000000..3367f61 --- /dev/null +++ b/src/scripts/services/showcases-service.js @@ -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; +} diff --git a/src/scripts/utils/clipboard.js b/src/scripts/utils/clipboard.js new file mode 100644 index 0000000..f250b53 --- /dev/null +++ b/src/scripts/utils/clipboard.js @@ -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); +} diff --git a/src/scripts/utils/color-extract.js b/src/scripts/utils/color-extract.js new file mode 100644 index 0000000..f199f3e --- /dev/null +++ b/src/scripts/utils/color-extract.js @@ -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 }); + } + }); +} diff --git a/src/scripts/utils/dom.js b/src/scripts/utils/dom.js new file mode 100644 index 0000000..42a4a87 --- /dev/null +++ b/src/scripts/utils/dom.js @@ -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); + }; +} diff --git a/src/scripts/utils/youtube.js b/src/scripts/utils/youtube.js new file mode 100644 index 0000000..32c2fd0 --- /dev/null +++ b/src/scripts/utils/youtube.js @@ -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`; +} diff --git a/src/styles/base/reset.css b/src/styles/base/reset.css new file mode 100644 index 0000000..a1dd49c --- /dev/null +++ b/src/styles/base/reset.css @@ -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); +} diff --git a/src/styles/base/tokens.css b/src/styles/base/tokens.css new file mode 100644 index 0000000..847ca6f --- /dev/null +++ b/src/styles/base/tokens.css @@ -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); +} \ No newline at end of file diff --git a/src/styles/base/typography.css b/src/styles/base/typography.css new file mode 100644 index 0000000..9484c60 --- /dev/null +++ b/src/styles/base/typography.css @@ -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); +} \ No newline at end of file diff --git a/src/styles/components/buttons.css b/src/styles/components/buttons.css new file mode 100644 index 0000000..f598715 --- /dev/null +++ b/src/styles/components/buttons.css @@ -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; + } +} \ No newline at end of file diff --git a/src/styles/components/executors.css b/src/styles/components/executors.css new file mode 100644 index 0000000..4021507 --- /dev/null +++ b/src/styles/components/executors.css @@ -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; +} diff --git a/src/styles/components/faq.css b/src/styles/components/faq.css new file mode 100644 index 0000000..a44d6eb --- /dev/null +++ b/src/styles/components/faq.css @@ -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; +} diff --git a/src/styles/components/marquee.css b/src/styles/components/marquee.css new file mode 100644 index 0000000..fb715c1 --- /dev/null +++ b/src/styles/components/marquee.css @@ -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; + } +} diff --git a/src/styles/components/modal.css b/src/styles/components/modal.css new file mode 100644 index 0000000..d3395ca --- /dev/null +++ b/src/styles/components/modal.css @@ -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); + } +} diff --git a/src/styles/components/partners.css b/src/styles/components/partners.css new file mode 100644 index 0000000..1b87dc0 --- /dev/null +++ b/src/styles/components/partners.css @@ -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; +} \ No newline at end of file diff --git a/src/styles/components/pills.css b/src/styles/components/pills.css new file mode 100644 index 0000000..7b3af75 --- /dev/null +++ b/src/styles/components/pills.css @@ -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; + } +} \ No newline at end of file diff --git a/src/styles/components/script-card.css b/src/styles/components/script-card.css new file mode 100644 index 0000000..a6fba6f --- /dev/null +++ b/src/styles/components/script-card.css @@ -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; + } +} \ No newline at end of file diff --git a/src/styles/components/showcase.css b/src/styles/components/showcase.css new file mode 100644 index 0000000..1fbc136 --- /dev/null +++ b/src/styles/components/showcase.css @@ -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); + } +} \ No newline at end of file diff --git a/src/styles/components/stats.css b/src/styles/components/stats.css new file mode 100644 index 0000000..b2d9d88 --- /dev/null +++ b/src/styles/components/stats.css @@ -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; +} \ No newline at end of file diff --git a/src/styles/components/testimonial-card.css b/src/styles/components/testimonial-card.css new file mode 100644 index 0000000..ffd535a --- /dev/null +++ b/src/styles/components/testimonial-card.css @@ -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; +} \ No newline at end of file diff --git a/src/styles/layout/footer.css b/src/styles/layout/footer.css new file mode 100644 index 0000000..fdc9e4b --- /dev/null +++ b/src/styles/layout/footer.css @@ -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 +} \ No newline at end of file diff --git a/src/styles/layout/nav.css b/src/styles/layout/nav.css new file mode 100644 index 0000000..4fb182a --- /dev/null +++ b/src/styles/layout/nav.css @@ -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; + } +} diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..2553f30 --- /dev/null +++ b/src/styles/main.css @@ -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'; \ No newline at end of file diff --git a/src/styles/sections/cta-banner.css b/src/styles/sections/cta-banner.css new file mode 100644 index 0000000..7981915 --- /dev/null +++ b/src/styles/sections/cta-banner.css @@ -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; +} \ No newline at end of file diff --git a/src/styles/sections/executors.css b/src/styles/sections/executors.css new file mode 100644 index 0000000..989a12b --- /dev/null +++ b/src/styles/sections/executors.css @@ -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); +} diff --git a/src/styles/sections/faq-section.css b/src/styles/sections/faq-section.css new file mode 100644 index 0000000..4552f19 --- /dev/null +++ b/src/styles/sections/faq-section.css @@ -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); +} diff --git a/src/styles/sections/features.css b/src/styles/sections/features.css new file mode 100644 index 0000000..f142a7d --- /dev/null +++ b/src/styles/sections/features.css @@ -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; + } +} \ No newline at end of file diff --git a/src/styles/sections/hero-atmosphere.css b/src/styles/sections/hero-atmosphere.css new file mode 100644 index 0000000..e864635 --- /dev/null +++ b/src/styles/sections/hero-atmosphere.css @@ -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; + } +} \ No newline at end of file diff --git a/src/styles/sections/hero-content.css b/src/styles/sections/hero-content.css new file mode 100644 index 0000000..62673af --- /dev/null +++ b/src/styles/sections/hero-content.css @@ -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; + } +} \ No newline at end of file diff --git a/src/styles/sections/section-base.css b/src/styles/sections/section-base.css new file mode 100644 index 0000000..05d915e --- /dev/null +++ b/src/styles/sections/section-base.css @@ -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; + } +} diff --git a/src/styles/sections/showcases.css b/src/styles/sections/showcases.css new file mode 100644 index 0000000..a9c6448 --- /dev/null +++ b/src/styles/sections/showcases.css @@ -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); +} diff --git a/src/styles/sections/testimonials.css b/src/styles/sections/testimonials.css new file mode 100644 index 0000000..379fc8e --- /dev/null +++ b/src/styles/sections/testimonials.css @@ -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; +} diff --git a/src/styles/utilities/layout.css b/src/styles/utilities/layout.css new file mode 100644 index 0000000..1919650 --- /dev/null +++ b/src/styles/utilities/layout.css @@ -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; +} diff --git a/src/styles/utilities/responsive.css b/src/styles/utilities/responsive.css new file mode 100644 index 0000000..969764c --- /dev/null +++ b/src/styles/utilities/responsive.css @@ -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; + } +} \ No newline at end of file diff --git a/src/styles/utilities/reveal.css b/src/styles/utilities/reveal.css new file mode 100644 index 0000000..c162077 --- /dev/null +++ b/src/styles/utilities/reveal.css @@ -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; + } +} \ No newline at end of file