From 690d137ea34667675f1c7f1da61faf74d3ad2c17 Mon Sep 17 00:00:00 2001 From: Lumerel Deploy Date: Fri, 20 Feb 2026 21:01:40 +0000 Subject: [PATCH] Deploy from Lumerel --- .dockerignore | 3 + Dockerfile | 23 +++ index.html | 185 ++++++++++++++++++++ main.js | 176 +++++++++++++++++++ styles.css | 456 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 843 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 index.html create mode 100644 main.js create mode 100644 styles.css diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..17896fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aba1cbf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM webdevops/php-nginx:8.3-alpine +ENV WEB_DOCUMENT_ROOT=/app +ARG CACHE_BUST=1771621300 +COPY . /app +RUN echo "index index.php index.html index.htm;" > /opt/docker/etc/nginx/vhost.common.d/01-index.conf \ + && echo "add_header Cache-Control 'no-cache, no-store, must-revalidate';" > /opt/docker/etc/nginx/vhost.common.d/02-no-cache.conf +RUN set -e; if [ -f /app/composer.json ]; then \ + echo ">>> composer.json found, installing dependencies..."; \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \ + cd /app && composer install --no-dev --no-interaction --optimize-autoloader; \ + ls -la /app/vendor/autoload.php; \ + else \ + echo ">>> No composer.json found, skipping composer install"; \ + fi +RUN set -e; if [ -f /app/package.json ]; then \ + echo ">>> package.json found, installing node dependencies..."; \ + apk add --no-cache nodejs npm; \ + cd /app && npm install --production; \ + else \ + echo ">>> No package.json found, skipping npm install"; \ + fi + +RUN chown -R application:application /app \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..e552937 --- /dev/null +++ b/index.html @@ -0,0 +1,185 @@ + + + + + + Lumerel — Web3 at the Speed of Experience + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + +
+ +
+
+
+
+ 3D Spline Scene Placeholder +
+
+ + + + + +
+ + +
+ + + Early access available from + May 1, 2026 + +
+ + +

+ Web3 at the Speed of Experience +

+ + +

+ Powering seamless experiences and real-time connections, EOS is the base for creators who move with purpose, leveraging resilience, speed, and scale to shape the future. +

+ + + + +
+ +
+ + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..c6d3c2b --- /dev/null +++ b/main.js @@ -0,0 +1,176 @@ +/** + * Lumerel — 3D Homepage + * main.js + * + * Handles: + * - Spline 3D scene loading & pointer-events toggle + * - Navbar scroll behaviour (frosted glass on scroll) + * - Video autoplay fallback (iOS / autoplay-blocked browsers) + * - Waitlist button click handlers + * - Global helpers exposed on window.Lumerel + */ + +/* ── Spline Integration ──────────────────────────────────────────────── + * + * WHEN YOUR SPLINE SCENE IS READY: + * + * Option A — HTML (simplest): + * Open index.html, find #spline-layer, delete .spline-placeholder, + * and paste in: + * + * + * + * Option B — JavaScript: + * Call window.Lumerel.loadSplineScene('https://prod.spline.design/…'); + * This removes the placeholder and injects for you. + * + * Option C — @splinetool/runtime (advanced): + * import { Application } from '@splinetool/runtime'; + * const app = new Application(canvas); + * app.load('https://prod.spline.design/YOUR_SCENE_ID/scene.splinecode'); + * + * For interactive scenes (mouse/touch on the 3D layer), call: + * window.Lumerel.enableSpline() + * + * ──────────────────────────────────────────────────────────────────────── */ + +const splineLayer = document.getElementById('spline-layer'); + +/** + * Enable pointer events on the 3D layer so Spline scenes + * can receive mouse/touch input. + * Call this AFTER your Spline scene has loaded. + */ +function enableSpline() { + if (splineLayer) { + splineLayer.style.pointerEvents = 'auto'; + console.log('[Lumerel] Spline pointer events ENABLED.'); + } +} + +/** + * Disable pointer events on the 3D layer (default state). + * Ensures all UI elements (buttons, links) remain fully clickable. + */ +function disableSpline() { + if (splineLayer) { + splineLayer.style.pointerEvents = 'none'; + console.log('[Lumerel] Spline pointer events DISABLED.'); + } +} + +/** + * Programmatically load a Spline scene by URL. + * Removes the placeholder orbs and injects a . + * @param {string} sceneUrl - The Spline scene URL (.splinecode) + * @param {boolean} [enableInteraction=false] - Auto-enable pointer events + */ +function loadSplineScene(sceneUrl, enableInteraction = false) { + if (!splineLayer) return; + + // Remove placeholder + const placeholder = splineLayer.querySelector('.spline-placeholder'); + if (placeholder) placeholder.remove(); + + // Inject spline-viewer + const viewer = document.createElement('spline-viewer'); + viewer.setAttribute('url', sceneUrl); + viewer.setAttribute('events-target', 'global'); + viewer.style.width = '100%'; + viewer.style.height = '100%'; + viewer.style.display = 'block'; + + viewer.addEventListener('load', () => { + console.log('[Lumerel] Spline scene loaded:', sceneUrl); + if (enableInteraction) enableSpline(); + }); + + splineLayer.appendChild(viewer); + console.log('[Lumerel] Spline scene injected:', sceneUrl); +} + +// Listen for spline-viewer ready event if already in HTML +if (splineLayer) { + const existingViewer = splineLayer.querySelector('spline-viewer'); + if (existingViewer) { + existingViewer.addEventListener('load', () => { + console.log('[Lumerel] Spline scene loaded.'); + // Uncomment to auto-enable 3D interactivity: + // enableSpline(); + }); + } +} + +/* ── Navbar Scroll Effect ────────────────────────────────────────────── + Adds a frosted-glass background when the user scrolls past 24px. +──────────────────────────────────────────────────────────────────────── */ +const navbar = document.getElementById('navbar'); + +function handleNavbarScroll() { + if (!navbar) return; + if (window.scrollY > 24) { + navbar.style.background = 'rgba(0, 0, 0, 0.45)'; + navbar.style.backdropFilter = 'blur(16px)'; + navbar.style.webkitBackdropFilter = 'blur(16px)'; + } else { + navbar.style.background = 'transparent'; + navbar.style.backdropFilter = 'none'; + navbar.style.webkitBackdropFilter = 'none'; + } +} + +window.addEventListener('scroll', handleNavbarScroll, { passive: true }); +handleNavbarScroll(); // run once on load + +/* ── Video Autoplay Fallback ─────────────────────────────────────────── + Some browsers / iOS block autoplay. This attempts a manual play + on the first user interaction as a fallback. +──────────────────────────────────────────────────────────────────────── */ +const heroVideo = document.querySelector('.hero__video'); + +if (heroVideo) { + heroVideo.play().catch(() => { + // Autoplay blocked — wait for first interaction + const resumeVideo = () => { + heroVideo.play().catch(() => {}); + document.removeEventListener('pointerdown', resumeVideo); + document.removeEventListener('keydown', resumeVideo); + }; + document.addEventListener('pointerdown', resumeVideo, { once: true }); + document.addEventListener('keydown', resumeVideo, { once: true }); + }); +} + +/* ── Waitlist Button Handlers ───────────────────────────────────────── + Replace the console.log with your modal, form, or redirect logic. +──────────────────────────────────────────────────────────────────────── */ +const navbarWaitlistBtn = document.getElementById('navbar-waitlist-btn'); +const heroWaitlistBtn = document.getElementById('hero-waitlist-btn'); + +function handleWaitlistClick(source) { + console.log('[Lumerel] Join Waitlist clicked —', source); + // TODO: open modal, redirect to /waitlist, or trigger form + // Example: window.location.href = '/waitlist'; +} + +if (navbarWaitlistBtn) { + navbarWaitlistBtn.addEventListener('click', () => handleWaitlistClick('navbar')); +} + +if (heroWaitlistBtn) { + heroWaitlistBtn.addEventListener('click', () => handleWaitlistClick('hero-cta')); +} + +/* ── Global API ──────────────────────────────────────────────────────── + Expose helpers on window.Lumerel for easy console/runtime access. +──────────────────────────────────────────────────────────────────────── */ +window.Lumerel = { + loadSplineScene, + enableSpline, + disableSpline, +}; + +console.log('[Lumerel] Ready. Use window.Lumerel.loadSplineScene(url) to add a 3D scene.'); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..434c772 --- /dev/null +++ b/styles.css @@ -0,0 +1,456 @@ +/* + ═══════════════════════════════════════════════════════════════════ + LUMEREL — 3D Hero Page + Font : General Sans (Fontshare) + Theme : Pure black · Web3 · Spline-ready + ═══════════════════════════════════════════════════════════════════ +*/ + +/* ── Reset & Base ─────────────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + width: 100%; + height: 100%; + background: #000000; + color: #ffffff; + font-family: 'General Sans', system-ui, -apple-system, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; +} + +a { + text-decoration: none; + color: inherit; +} + +ul { + list-style: none; +} + +button { + cursor: pointer; + border: none; + background: none; + font-family: inherit; +} + +/* + ═══════════════════════════════════════════════════════════════════ + HERO SECTION + Full-screen · layers: video → 3D → navbar → content + ═══════════════════════════════════════════════════════════════════ +*/ +.hero { + position: relative; + width: 100%; + min-height: 100dvh; + background: #000000; + overflow: hidden; +} + +/* ── Layer 0 · Background Video ───────────────────────────────────── */ +.hero__video-wrap { + position: absolute; + inset: 0; + z-index: 0; +} + +.hero__video { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +/* Exactly 50% black overlay */ +.hero__overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.50); +} + +/* ── Layer 1 · 3D / Spline ──────────────────────────────────────────── + Above video (z-index: 1), below all UI (z-index: 2+). + pointer-events: none keeps buttons & links fully clickable. + Call window.Lumerel.enableSpline() for interactive scenes. +──────────────────────────────────────────────────────────────────────── */ +.hero__3d-layer { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; +} + +/* Spline viewer fills the full layer */ +.hero__3d-layer spline-viewer { + width: 100%; + height: 100%; + display: block; +} + +/* ── 3D Placeholder Orbs ────────────────────────────────────────────── + Animated ambient orbs simulate depth until Spline assets arrive. + Delete .spline-placeholder when adding . +──────────────────────────────────────────────────────────────────────── */ +.spline-placeholder { + position: relative; + width: 100%; + height: 100%; +} + +.orb { + position: absolute; + border-radius: 50%; + filter: blur(80px); + opacity: 0.18; +} + +.orb--violet { + width: 520px; + height: 520px; + background: radial-gradient(circle, #7c3aed 0%, #4f46e5 55%, transparent 100%); + top: 8%; + right: 5%; + animation: orbDrift 11s ease-in-out infinite; +} + +.orb--cyan { + width: 360px; + height: 360px; + background: radial-gradient(circle, #06b6d4 0%, #3b82f6 55%, transparent 100%); + bottom: 14%; + left: 4%; + animation: orbDrift 13s ease-in-out infinite reverse; + animation-delay: -5s; +} + +.orb--pink { + width: 260px; + height: 260px; + background: radial-gradient(circle, #f0abfc 0%, #a78bfa 55%, transparent 100%); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + animation: orbDriftCenter 9s ease-in-out infinite; + animation-delay: -3s; +} + +@keyframes orbDrift { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(-20px, -30px) scale(1.06); } + 66% { transform: translate(16px, 22px) scale(0.95); } +} + +@keyframes orbDriftCenter { + 0%, 100% { transform: translate(-50%, -50%) scale(1); } + 40% { transform: translate(-50%, -60%) scale(1.08); } + 75% { transform: translate(-50%, -44%) scale(0.94); } +} + +/* Placeholder label */ +.spline-placeholder__label { + position: absolute; + bottom: 32px; + left: 50%; + transform: translateX(-50%); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.10em; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.20); + white-space: nowrap; + border: 1px solid rgba(255, 255, 255, 0.08); + padding: 6px 14px; + border-radius: 100px; + pointer-events: none; +} + +/* + ═══════════════════════════════════════════════════════════════════ + NAVBAR (z-index: 10) + 120px h-padding desktop · 20px v-padding · frosted on scroll + ═══════════════════════════════════════════════════════════════════ +*/ +.navbar { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 120px; + transition: background 0.25s ease, backdrop-filter 0.25s ease; +} + +.navbar__left { + display: flex; + align-items: center; + gap: 40px; +} + +/* Logo — exactly 187×25px */ +.navbar__logo { + display: flex; + align-items: center; + flex-shrink: 0; + width: 187px; + height: 25px; +} + +.navbar__logo svg { + display: block; + width: 187px; + height: 25px; +} + +/* Nav links list — hidden below 768px */ +.navbar__links { + display: flex; + align-items: center; + gap: 30px; +} + +/* Each nav link */ +.navbar__link { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + color: #ffffff; + white-space: nowrap; + transition: opacity 0.15s ease; +} + +.navbar__link:hover { + opacity: 0.75; +} + +.navbar__chevron { + display: block; + flex-shrink: 0; + width: 14px; + height: 14px; +} + +/* + ═══════════════════════════════════════════════════════════════════ + PILL BUTTON — Layered construction + ───────────────────────────────────────────────────────────────── + Outer ring : box-shadow 0 0 0 0.6px #fff (exact 0.6px border) + Glow streak: ::before pseudo — blurred radial-gradient at top + Inner pill : background + text + ═══════════════════════════════════════════════════════════════════ +*/ +.btn-pill { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 100px; + padding: 11px 29px; + font-size: 14px; + font-weight: 500; + font-family: 'General Sans', system-ui, -apple-system, sans-serif; + cursor: pointer; + overflow: hidden; + transition: opacity 0.15s ease, transform 0.15s ease; + /* 0.6px white outer border via box-shadow */ + box-shadow: 0 0 0 0.6px rgba(255, 255, 255, 1); +} + +.btn-pill:hover { + opacity: 0.88; + transform: translateY(-1px); +} + +.btn-pill:active { + opacity: 1; + transform: translateY(0); +} + +/* Top glow streak — blurred white radial blob at the top edge */ +.btn-pill__glow { + position: absolute; + top: -8px; + left: 50%; + transform: translateX(-50%); + width: 60%; + height: 18px; + background: radial-gradient(ellipse at center top, rgba(255, 255, 255, 0.55) 0%, transparent 70%); + filter: blur(4px); + pointer-events: none; + z-index: 1; + border-radius: 50%; +} + +/* Label sits above glow */ +.btn-pill__label { + position: relative; + z-index: 2; + white-space: nowrap; +} + +/* Dark variant — navbar button: black bg, white text */ +.btn-pill--dark { + background: #000000; + color: #ffffff; +} + +/* Light variant — CTA button: white bg, black text */ +.btn-pill--light { + background: #ffffff; + color: #000000; +} + +/* Light variant glow is slightly darker to show on white */ +.btn-pill--light .btn-pill__glow { + background: radial-gradient(ellipse at center top, rgba(255, 255, 255, 0.90) 0%, transparent 70%); +} + +/* + ═══════════════════════════════════════════════════════════════════ + HERO CONTENT (z-index: 3) + Centered · 280px top padding desktop · 200px mobile · 102px bottom + ═══════════════════════════════════════════════════════════════════ +*/ +.hero__content { + position: relative; + z-index: 3; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding-top: 280px; + padding-bottom: 102px; + padding-left: 24px; + padding-right: 24px; + gap: 40px; +} + +/* ── Badge / Early access pill ────────────────────────────────────── */ +.hero__badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 14px; + border-radius: 20px; + background: rgba(255, 255, 255, 0.10); + border: 1px solid rgba(255, 255, 255, 0.20); + font-size: 13px; + font-weight: 500; +} + +/* 4px white dot */ +.hero__badge-dot { + display: inline-block; + width: 4px; + height: 4px; + border-radius: 50%; + background: #ffffff; + flex-shrink: 0; +} + +.hero__badge-text { + display: inline; + line-height: 1; +} + +/* "Early access available from" — 60% opacity */ +.hero__badge-muted { + color: rgba(255, 255, 255, 0.60); +} + +/* " May 1, 2026" — solid white */ +.hero__badge-date { + color: #ffffff; +} + +/* ── Heading ──────────────────────────────────────────────────────── */ +.hero__heading { + max-width: 613px; + font-size: 56px; + font-weight: 500; + line-height: 1.28; + letter-spacing: -0.02em; + /* Gradient text: 144.5deg · solid white at 28% → transparent at 115% */ + background: linear-gradient(144.5deg, #ffffff 28%, rgba(0, 0, 0, 0) 115%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: transparent; +} + +/* ── Subtitle ─────────────────────────────────────────────────────── */ +.hero__subtitle { + max-width: 680px; + font-size: 15px; + font-weight: 400; + line-height: 1.65; + color: rgba(255, 255, 255, 0.70); + /* Pull up 16px to achieve 24px visual gap from heading (40px gap - 16px = 24px) */ + margin-top: -16px; +} + +/* + ═══════════════════════════════════════════════════════════════════ + RESPONSIVE + ═══════════════════════════════════════════════════════════════════ +*/ + +/* Tablet — ≤ 1024px */ +@media (max-width: 1024px) { + .navbar { + padding: 20px 48px; + } +} + +/* Mobile — < 768px */ +@media (max-width: 767px) { + /* Hide nav links */ + .navbar__links { + display: none; + } + + .navbar { + padding: 20px 24px; + } + + .navbar__left { + gap: 0; + } + + /* Heading scales down to 36px */ + .hero__heading { + font-size: 36px; + } + + /* Top padding reduces to 200px */ + .hero__content { + padding-top: 200px; + gap: 32px; + } + + .hero__subtitle { + margin-top: -8px; + } +} + +/* Very small screens */ +@media (max-width: 380px) { + .hero__heading { + font-size: 30px; + } + + .navbar { + padding: 20px 16px; + } +}