landing page
This commit is contained in:
260
public/app/index.html
Normal file
260
public/app/index.html
Normal file
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BlindMaster App — Coming Soon</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--accent: #3B82F6;
|
||||
--accent-rgb: 59, 130, 246;
|
||||
--bg: #0A0B0F;
|
||||
--bg-card: #181B24;
|
||||
--text: #F1F5F9;
|
||||
--text-muted: #8892A4;
|
||||
--text-faint: #4B5563;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Background grid */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, black 30%, transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Glow */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 600px; height: 400px;
|
||||
background: radial-gradient(ellipse, rgba(var(--accent-rgb), 0.12) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid rgba(255,255,255,0.07);
|
||||
border-radius: 24px;
|
||||
padding: 56px 48px;
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
box-shadow: 0 32px 80px rgba(0,0,0,0.5);
|
||||
animation: fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(24px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.logo-slat {
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
background: rgba(var(--accent-rgb), 0.12);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.25);
|
||||
padding: 5px 14px;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 6px; height: 6px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.8s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.75); }
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
background: linear-gradient(135deg, var(--accent), #a78bfa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.92rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 4px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-radius: 100px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
width: 72%;
|
||||
background: linear-gradient(90deg, var(--accent), rgba(var(--accent-rgb), 0.4));
|
||||
border-radius: 100px;
|
||||
animation: shimmer 2s infinite;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% center; }
|
||||
100% { background-position: -200% center; }
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-faint);
|
||||
text-align: right;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.back-link:hover { color: var(--text); }
|
||||
|
||||
.back-link svg { transition: transform 0.2s; }
|
||||
.back-link:hover svg { transform: translateX(-3px); }
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.card { padding: 40px 24px; }
|
||||
h1 { font-size: 1.6rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="logo">
|
||||
<div class="logo-icon">
|
||||
<div class="logo-slat"></div>
|
||||
<div class="logo-slat"></div>
|
||||
<div class="logo-slat"></div>
|
||||
<div class="logo-slat"></div>
|
||||
</div>
|
||||
BlindMaster
|
||||
</div>
|
||||
|
||||
<div class="badge">
|
||||
<span class="badge-dot"></span>
|
||||
In Development
|
||||
</div>
|
||||
|
||||
<h1>The web app is<br /><span>coming soon.</span></h1>
|
||||
|
||||
<p>
|
||||
We're building the BlindMaster web experience so you can control
|
||||
your blinds from any browser — no install required. Stay tuned.
|
||||
</p>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill"></div>
|
||||
</div>
|
||||
<div class="progress-label">Development in progress</div>
|
||||
|
||||
<a href="/" class="back-link">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<path d="M19 12H5M12 5l-7 7 7 7"/>
|
||||
</svg>
|
||||
Back to homepage
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Match time-based accent color from main landing page
|
||||
const hour = new Date().getHours();
|
||||
let accent, accentRgb;
|
||||
if (hour >= 5 && hour < 10) {
|
||||
accent = '#F97316'; accentRgb = '249, 115, 22';
|
||||
} else if (hour >= 10 && hour < 18) {
|
||||
accent = '#3B82F6'; accentRgb = '59, 130, 246';
|
||||
} else {
|
||||
accent = '#7C3AED'; accentRgb = '124, 58, 237';
|
||||
}
|
||||
document.documentElement.style.setProperty('--accent', accent);
|
||||
document.documentElement.style.setProperty('--accent-rgb', accentRgb);
|
||||
// Update slats to match accent
|
||||
document.querySelectorAll('.logo-slat').forEach(s => s.style.background = accent);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
934
public/css/styles.css
Normal file
934
public/css/styles.css
Normal file
@@ -0,0 +1,934 @@
|
||||
/* ============================================================
|
||||
BlindMaster Landing Page — Styles
|
||||
Theme: time-based accent (orange / blue / purple)
|
||||
matching the Flutter app's time-of-day color system
|
||||
============================================================ */
|
||||
|
||||
/* ---------- CSS Custom Properties (set by JS at runtime) --- */
|
||||
:root {
|
||||
--accent: #3B82F6; /* default: day blue */
|
||||
--accent-rgb: 59, 130, 246;
|
||||
--accent-dark: #1D4ED8;
|
||||
--accent-glow: rgba(59, 130, 246, 0.25);
|
||||
|
||||
--bg: #0A0B0F;
|
||||
--bg-surface: #12141A;
|
||||
--bg-card: #181B24;
|
||||
--bg-card-border: rgba(255,255,255,0.07);
|
||||
|
||||
--text: #F1F5F9;
|
||||
--text-muted: #8892A4;
|
||||
--text-faint: #4B5563;
|
||||
|
||||
--slat-color: #6B4C2A;
|
||||
--slat-shadow: rgba(0,0,0,0.4);
|
||||
|
||||
--transition: 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--radius: 16px;
|
||||
--radius-sm: 10px;
|
||||
}
|
||||
|
||||
/* ---------- Reset & Base ----------------------------------- */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html { scroll-behavior: smooth; font-size: 16px; }
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
overflow-x: hidden;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
img { display: block; max-width: 100%; }
|
||||
button { font-family: inherit; cursor: pointer; border: none; }
|
||||
|
||||
/* ---------- Utility ---------------------------------------- */
|
||||
.section-inner {
|
||||
max-width: 1160px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.section-eyebrow {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(1.8rem, 4vw, 2.8rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-sub {
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.section-header { margin-bottom: 56px; }
|
||||
|
||||
/* ---------- Buttons ---------------------------------------- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 13px 28px;
|
||||
border-radius: 100px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.25s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 24px var(--accent-glow);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
filter: brightness(1.12);
|
||||
box-shadow: 0 0 36px var(--accent-glow);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1.5px solid var(--bg-card-border);
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
color: var(--text);
|
||||
border-color: rgba(255,255,255,0.18);
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
/* ---------- Scroll reveal ---------------------------------- */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
transition: opacity 0.7s ease, transform 0.7s ease;
|
||||
transition-delay: var(--delay, 0ms);
|
||||
}
|
||||
.reveal.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
NAV
|
||||
============================================================ */
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
z-index: 100;
|
||||
padding: 16px 0;
|
||||
transition: background 0.3s, backdrop-filter 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
.nav.scrolled {
|
||||
background: rgba(10, 11, 15, 0.85);
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: 0 1px 0 rgba(255,255,255,0.06);
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: 1160px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.nav-logo-icon {
|
||||
width: 26px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
.nav-logo-icon.small { width: 20px; height: 18px; gap: 3px; }
|
||||
.slat {
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
list-style: none;
|
||||
}
|
||||
.nav-links a {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.nav-links a:hover { color: var(--text); }
|
||||
.nav-cta {
|
||||
background: var(--accent) !important;
|
||||
color: #fff !important;
|
||||
padding: 8px 20px;
|
||||
border-radius: 100px;
|
||||
font-size: 0.85rem !important;
|
||||
transition: filter 0.2s, box-shadow 0.2s !important;
|
||||
}
|
||||
.nav-cta:hover { filter: brightness(1.1); }
|
||||
.nav-launch {
|
||||
background: transparent;
|
||||
color: var(--text) !important;
|
||||
border: 1.5px solid var(--bg-card-border);
|
||||
padding: 7px 18px;
|
||||
border-radius: 100px;
|
||||
font-size: 0.85rem !important;
|
||||
transition: border-color 0.2s, background 0.2s !important;
|
||||
}
|
||||
.nav-launch:hover {
|
||||
border-color: rgba(255,255,255,0.2) !important;
|
||||
background: rgba(255,255,255,0.05) !important;
|
||||
}
|
||||
.mobile-nav-launch {
|
||||
color: var(--accent) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
.nav-hamburger {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
background: transparent;
|
||||
padding: 4px;
|
||||
}
|
||||
.nav-hamburger span {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 2px;
|
||||
background: var(--text-muted);
|
||||
border-radius: 2px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 64px; left: 0; right: 0;
|
||||
background: rgba(10,11,15,0.97);
|
||||
backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid var(--bg-card-border);
|
||||
z-index: 99;
|
||||
padding: 16px 0;
|
||||
transform: translateY(-16px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s, transform 0.25s;
|
||||
}
|
||||
.mobile-nav.open {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
display: block;
|
||||
}
|
||||
.mobile-nav ul { list-style: none; }
|
||||
.mobile-nav-link {
|
||||
display: block;
|
||||
padding: 14px 24px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.mobile-nav-link:hover { color: var(--text); }
|
||||
|
||||
/* ============================================================
|
||||
HERO
|
||||
============================================================ */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 120px 24px 80px;
|
||||
max-width: 1160px;
|
||||
margin: 0 auto;
|
||||
gap: 48px;
|
||||
position: relative;
|
||||
}
|
||||
.hero-bg-grid {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
mask-image: radial-gradient(ellipse 80% 60% at 50% 40%, black 30%, transparent 100%);
|
||||
}
|
||||
.hero-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
background: rgba(var(--accent-rgb), 0.12);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.25);
|
||||
padding: 6px 14px;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.badge-dot {
|
||||
width: 6px; height: 6px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.8); }
|
||||
}
|
||||
.hero-title {
|
||||
font-size: clamp(2.4rem, 6vw, 4.2rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 20px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.hero-accent {
|
||||
background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 60%, #fff));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 500px;
|
||||
margin-bottom: 36px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
.hero-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.stat { text-align: center; }
|
||||
.stat-num {
|
||||
display: block;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-faint);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 36px;
|
||||
background: var(--bg-card-border);
|
||||
}
|
||||
|
||||
/* ---- Hero visual: window + blind ---- */
|
||||
.hero-visual {
|
||||
flex: 0 0 420px;
|
||||
z-index: 1;
|
||||
}
|
||||
.window-frame {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 32px 80px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04) inset;
|
||||
}
|
||||
.window-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-bottom: 1px solid var(--bg-card-border);
|
||||
}
|
||||
.window-dot {
|
||||
width: 12px; height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.window-dot.red { background: #FF5F57; }
|
||||
.window-dot.yellow { background: #FEBC2E; }
|
||||
.window-dot.green { background: #28C840; }
|
||||
.window-title {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.blind-container {
|
||||
height: 220px;
|
||||
background: linear-gradient(180deg, #87CEEB 0%, #B0D4F1 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.blind-slat {
|
||||
height: 18px;
|
||||
background: var(--slat-color);
|
||||
border-radius: 2px;
|
||||
margin: 0 12px;
|
||||
box-shadow: 0 2px 4px var(--slat-shadow);
|
||||
transform-origin: center;
|
||||
transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transform: rotateX(calc(var(--i, 0) * 0deg));
|
||||
}
|
||||
.app-ui-overlay {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
.app-slider-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.app-slider-track {
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-radius: 100px;
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.app-slider-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.3);
|
||||
transition: left 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), background var(--transition);
|
||||
cursor: pointer;
|
||||
}
|
||||
.app-slider-vals {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
FEATURES
|
||||
============================================================ */
|
||||
.features {
|
||||
padding: 120px 0;
|
||||
background: linear-gradient(180deg, var(--bg) 0%, var(--bg-surface) 100%);
|
||||
}
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.feature-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 28px;
|
||||
transition: border-color 0.25s, box-shadow 0.25s, transform 0.25s;
|
||||
}
|
||||
.feature-card:hover {
|
||||
border-color: rgba(var(--accent-rgb), 0.35);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 0 1px rgba(var(--accent-rgb), 0.1) inset;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.feature-icon {
|
||||
width: 48px; height: 48px;
|
||||
background: rgba(var(--accent-rgb), 0.12);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 18px;
|
||||
color: var(--accent);
|
||||
transition: background var(--transition), color var(--transition);
|
||||
}
|
||||
.feature-icon svg { width: 22px; height: 22px; }
|
||||
.feature-card h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.feature-card p {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
HOW IT WORKS
|
||||
============================================================ */
|
||||
.how-it-works {
|
||||
padding: 120px 0;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
/* Arch flow */
|
||||
.arch-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.arch-node {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px 20px;
|
||||
text-align: center;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
.arch-node-center {
|
||||
border-color: rgba(var(--accent-rgb), 0.35);
|
||||
box-shadow: 0 0 40px rgba(var(--accent-rgb), 0.08);
|
||||
}
|
||||
.arch-icon {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 14px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.app-icon { background: rgba(var(--accent-rgb), 0.12); }
|
||||
.server-icon { background: rgba(var(--accent-rgb), 0.18); }
|
||||
.device-icon { background: rgba(var(--accent-rgb), 0.12); }
|
||||
.arch-icon svg { width: 24px; height: 24px; }
|
||||
.arch-node h4 { font-size: 0.95rem; font-weight: 600; margin-bottom: 8px; }
|
||||
.arch-node p { font-size: 0.78rem; color: var(--text-muted); line-height: 1.6; }
|
||||
.arch-tag {
|
||||
margin-top: 12px;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--accent);
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
padding: 3px 10px;
|
||||
border-radius: 100px;
|
||||
display: inline-block;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
.arch-arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.arch-arrow-line {
|
||||
width: 40px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--bg-card-border), rgba(var(--accent-rgb), 0.4));
|
||||
}
|
||||
.arch-arrow-label {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-faint);
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.04em;
|
||||
writing-mode: horizontal-tb;
|
||||
}
|
||||
.arch-ble-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-faint);
|
||||
margin-bottom: 56px;
|
||||
padding: 10px 16px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Steps */
|
||||
.steps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.step {
|
||||
padding: 24px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.step-num {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
color: rgba(var(--accent-rgb), 0.2);
|
||||
line-height: 1;
|
||||
margin-bottom: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.step h4 { font-size: 0.95rem; font-weight: 600; margin-bottom: 10px; }
|
||||
.step p { font-size: 0.85rem; color: var(--text-muted); line-height: 1.65; }
|
||||
|
||||
/* ============================================================
|
||||
TECH / OPEN HARDWARE
|
||||
============================================================ */
|
||||
.tech {
|
||||
padding: 120px 0;
|
||||
background: linear-gradient(180deg, var(--bg-surface) 0%, var(--bg) 100%);
|
||||
}
|
||||
.tech-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 64px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tech-text { flex: 1; min-width: 280px; }
|
||||
.tech-text .section-title { margin-bottom: 20px; }
|
||||
.tech-text p {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.75;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.tech-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.tech-list li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tech-check {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* Chip visual */
|
||||
.tech-visual {
|
||||
flex: 0 0 280px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.chip-card {
|
||||
position: relative;
|
||||
width: 240px;
|
||||
}
|
||||
.chip-glow {
|
||||
position: absolute;
|
||||
inset: -40px;
|
||||
background: radial-gradient(ellipse at 50% 50%, rgba(var(--accent-rgb), 0.18) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
.chip-body {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: 20px;
|
||||
padding: 24px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.chip-label {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.chip-sub {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-faint);
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.chip-pins {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.chip-pin-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.chip-pin {
|
||||
width: 24px;
|
||||
height: 7px;
|
||||
background: rgba(var(--accent-rgb), 0.35);
|
||||
border-radius: 2px;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
.chip-core {
|
||||
flex: 1;
|
||||
aspect-ratio: 1;
|
||||
background: rgba(var(--accent-rgb), 0.08);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.chip-core-inner {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
background: rgba(var(--accent-rgb), 0.15);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.3);
|
||||
}
|
||||
.chip-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.chip-tags span {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--accent);
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
padding: 3px 8px;
|
||||
border-radius: 100px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
COMING SOON
|
||||
============================================================ */
|
||||
.coming-soon {
|
||||
padding: 120px 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
.coming-soon-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: 24px;
|
||||
padding: 64px 56px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 80px rgba(var(--accent-rgb), 0.06);
|
||||
}
|
||||
.coming-soon-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -80px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 400px; height: 200px;
|
||||
background: radial-gradient(ellipse, rgba(var(--accent-rgb), 0.15) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.cs-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
background: rgba(var(--accent-rgb), 0.12);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.25);
|
||||
padding: 6px 16px;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.cs-badge-dot {
|
||||
width: 6px; height: 6px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
.coming-soon-card h2 {
|
||||
font-size: clamp(1.6rem, 3.5vw, 2.4rem);
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.coming-soon-card > p {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 520px;
|
||||
margin: 0 auto 36px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.coming-soon-card strong { color: var(--text); }
|
||||
.cs-highlights {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.cs-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.cs-item svg { color: var(--accent); flex-shrink: 0; }
|
||||
.cs-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
max-width: 420px;
|
||||
margin: 0 auto 16px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.cs-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1.5px solid var(--bg-card-border);
|
||||
border-radius: 100px;
|
||||
padding: 13px 20px;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.cs-input::placeholder { color: var(--text-faint); }
|
||||
.cs-input:focus { border-color: var(--accent); }
|
||||
.cs-btn { flex-shrink: 0; }
|
||||
.cs-disclaimer {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-faint);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.cs-success {
|
||||
color: #4ADE80;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
FOOTER
|
||||
============================================================ */
|
||||
.footer {
|
||||
border-top: 1px solid var(--bg-card-border);
|
||||
padding: 48px 24px;
|
||||
}
|
||||
.footer-inner {
|
||||
max-width: 1160px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.footer-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.footer-tagline {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.footer-links a {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-faint);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.footer-links a:hover { color: var(--text-muted); }
|
||||
.footer-copy {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
RESPONSIVE
|
||||
============================================================ */
|
||||
@media (max-width: 900px) {
|
||||
.hero {
|
||||
flex-direction: column;
|
||||
padding-top: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
.hero-subtitle { margin-left: auto; margin-right: auto; }
|
||||
.hero-actions { justify-content: center; }
|
||||
.hero-stats { justify-content: center; }
|
||||
.hero-visual { width: 100%; flex: none; }
|
||||
.window-frame { max-width: 380px; margin: 0 auto; }
|
||||
.nav-links { display: none; }
|
||||
.nav-hamburger { display: flex; }
|
||||
.tech-content { flex-direction: column; }
|
||||
.tech-visual { flex: none; width: 100%; }
|
||||
.arch-flow { gap: 4px; }
|
||||
.arch-arrow-line { width: 24px; }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.coming-soon-card { padding: 40px 24px; }
|
||||
.arch-flow { flex-direction: column; }
|
||||
.arch-arrow { flex-direction: row; gap: 8px; }
|
||||
.arch-arrow-line { width: 20px; height: 1px; }
|
||||
}
|
||||
419
public/index.html
Normal file
419
public/index.html
Normal file
@@ -0,0 +1,419 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BlindMaster — Smart Blinds, Effortlessly Controlled</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav class="nav" id="nav">
|
||||
<div class="nav-inner">
|
||||
<div class="nav-logo">
|
||||
<div class="nav-logo-icon">
|
||||
<div class="slat"></div>
|
||||
<div class="slat"></div>
|
||||
<div class="slat"></div>
|
||||
<div class="slat"></div>
|
||||
</div>
|
||||
<span>BlindMaster</span>
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#how-it-works">How It Works</a></li>
|
||||
<li><a href="#tech">Open Hardware</a></li>
|
||||
<li><a href="#coming-soon" class="nav-cta">Get Early Access</a></li>
|
||||
<li><a href="/app" class="nav-launch">Launch App</a></li>
|
||||
</ul>
|
||||
<button class="nav-hamburger" id="hamburger" aria-label="Menu">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- MOBILE NAV -->
|
||||
<div class="mobile-nav" id="mobileNav">
|
||||
<ul>
|
||||
<li><a href="#features" class="mobile-nav-link">Features</a></li>
|
||||
<li><a href="#how-it-works" class="mobile-nav-link">How It Works</a></li>
|
||||
<li><a href="#tech" class="mobile-nav-link">Open Hardware</a></li>
|
||||
<li><a href="#coming-soon" class="mobile-nav-link">Get Early Access</a></li>
|
||||
<li><a href="/app" class="mobile-nav-link mobile-nav-launch">Launch App</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero" id="hero">
|
||||
<div class="hero-bg-grid"></div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">
|
||||
<span class="badge-dot"></span>
|
||||
Smart Home IoT
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
Your blinds,<br />
|
||||
<span class="hero-accent">on your schedule.</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
BlindMaster brings real-time remote control, intelligent scheduling, and
|
||||
seamless IoT integration to your motorized window blinds — from anywhere.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/app" class="btn btn-primary">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M12 18h.01"/></svg>
|
||||
Launch App
|
||||
</a>
|
||||
<a href="#how-it-works" class="btn btn-ghost">See How It Works</a>
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-num">11</span>
|
||||
<span class="stat-label">Positions</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat">
|
||||
<span class="stat-num">∞</span>
|
||||
<span class="stat-label">Schedules</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat">
|
||||
<span class="stat-num">Real-time</span>
|
||||
<span class="stat-label">Socket.IO Sync</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-visual">
|
||||
<div class="window-frame">
|
||||
<div class="window-header">
|
||||
<div class="window-dot red"></div>
|
||||
<div class="window-dot yellow"></div>
|
||||
<div class="window-dot green"></div>
|
||||
<span class="window-title">BlindMaster</span>
|
||||
</div>
|
||||
<div class="blind-container" id="heroBlind">
|
||||
<div class="blind-slat" style="--i:0"></div>
|
||||
<div class="blind-slat" style="--i:1"></div>
|
||||
<div class="blind-slat" style="--i:2"></div>
|
||||
<div class="blind-slat" style="--i:3"></div>
|
||||
<div class="blind-slat" style="--i:4"></div>
|
||||
<div class="blind-slat" style="--i:5"></div>
|
||||
<div class="blind-slat" style="--i:6"></div>
|
||||
<div class="blind-slat" style="--i:7"></div>
|
||||
<div class="blind-slat" style="--i:8"></div>
|
||||
<div class="blind-slat" style="--i:9"></div>
|
||||
</div>
|
||||
<div class="app-ui-overlay">
|
||||
<div class="app-slider-label">Position</div>
|
||||
<div class="app-slider-track">
|
||||
<div class="app-slider-thumb" id="sliderThumb"></div>
|
||||
</div>
|
||||
<div class="app-slider-vals">
|
||||
<span>Close ↓</span>
|
||||
<span>Open</span>
|
||||
<span>Close ↑</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<section class="features" id="features">
|
||||
<div class="section-inner">
|
||||
<div class="section-header reveal">
|
||||
<p class="section-eyebrow">Why BlindMaster</p>
|
||||
<h2 class="section-title">Everything you need to control your light.</h2>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
|
||||
<div class="feature-card reveal" style="--delay:0ms">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M12 18.5A6.5 6.5 0 1 0 12 5.5a6.5 6.5 0 0 0 0 13Z"/>
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Real-Time Control</h3>
|
||||
<p>Adjust any blind instantly via WebSocket — sub-second response from your phone to your window, anywhere in the world.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card reveal" style="--delay:80ms">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"/>
|
||||
<path d="M16 2v4M8 2v4M3 10h18"/>
|
||||
<path d="M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Smart Scheduling</h3>
|
||||
<p>Set automated cron-based schedules per blind or per group. Wake up to light gradually filling the room — automatically.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card reveal" style="--delay:160ms">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Groups & Rooms</h3>
|
||||
<p>Group multiple blinds and control them together. One tap to raise every blind in a room simultaneously.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card reveal" style="--delay:240ms">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Secure by Default</h3>
|
||||
<p>JWT authentication, Argon2 password hashing, TLS everywhere, and multi-layer rate limiting — your home stays yours.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card reveal" style="--delay:320ms">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M23 6l-9.5 9.5-5-5L1 18"/>
|
||||
<path d="M17 6h6v6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Auto-Calibration</h3>
|
||||
<p>A guided multi-stage calibration flow maps encoder ticks to your exact blind travel — precise positioning every time.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card reveal" style="--delay:400ms">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="2" y="7" width="20" height="14" rx="2"/>
|
||||
<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/>
|
||||
<line x1="12" y1="12" x2="12" y2="16"/>
|
||||
<line x1="10" y1="14" x2="14" y2="14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Battery-Aware</h3>
|
||||
<p>Built-in MAX17048 fuel gauge monitoring with low-battery alerts. Dynamic CPU scaling and servo power gating extend runtime.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section class="how-it-works" id="how-it-works">
|
||||
<div class="section-inner">
|
||||
<div class="section-header reveal">
|
||||
<p class="section-eyebrow">Architecture</p>
|
||||
<h2 class="section-title">Three components. One seamless system.</h2>
|
||||
<p class="section-sub">BlindMaster is a full-stack IoT platform — every layer is purpose-built to work together.</p>
|
||||
</div>
|
||||
<div class="arch-flow reveal">
|
||||
<div class="arch-node">
|
||||
<div class="arch-icon app-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2"/>
|
||||
<path d="M12 18h.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4>Flutter App</h4>
|
||||
<p>iOS & Android mobile app. Time-based theming. Real-time slider control. Schedule management.</p>
|
||||
<div class="arch-tag">blinds_flutter</div>
|
||||
</div>
|
||||
<div class="arch-arrow">
|
||||
<div class="arch-arrow-line"></div>
|
||||
<div class="arch-arrow-label">Socket.IO + JWT + TLS</div>
|
||||
<div class="arch-arrow-line"></div>
|
||||
</div>
|
||||
<div class="arch-node arch-node-center">
|
||||
<div class="arch-icon server-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2"/>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2"/>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4>Express Server</h4>
|
||||
<p>Central relay & API. PostgreSQL + MongoDB. Agenda scheduling. Email via AWS SES.</p>
|
||||
<div class="arch-tag">blinds_express</div>
|
||||
</div>
|
||||
<div class="arch-arrow">
|
||||
<div class="arch-arrow-line"></div>
|
||||
<div class="arch-arrow-label">Socket.IO + JWT + TLS</div>
|
||||
<div class="arch-arrow-line"></div>
|
||||
</div>
|
||||
<div class="arch-node">
|
||||
<div class="arch-icon device-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4>ESP32-C6</h4>
|
||||
<p>FreeRTOS firmware. BLE provisioning. Servo + encoder control. NVS persistent state.</p>
|
||||
<div class="arch-tag">Blinds_XIAO</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arch-ble-note reveal">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16">
|
||||
<path d="M7 7l10 10M17 7l-5 5 5 5M7 7l5 5-5 5"/>
|
||||
</svg>
|
||||
BLE provisioning (setup only) — phone pairs directly to device to deliver Wi-Fi credentials & auth token
|
||||
</div>
|
||||
<div class="steps-grid reveal">
|
||||
<div class="step">
|
||||
<div class="step-num">01</div>
|
||||
<h4>Provision Once</h4>
|
||||
<p>Pair your ESP32-C6 device over BLE from the app. Enter your Wi-Fi credentials and authenticate — stored securely on the device in NVS.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">02</div>
|
||||
<h4>Calibrate</h4>
|
||||
<p>A guided handshake walks the device through measuring your blind's full travel range. Precise 11-position control from that point on.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">03</div>
|
||||
<h4>Control & Schedule</h4>
|
||||
<p>Use the app slider for immediate control or set recurring schedules. Changes reach your blinds in real-time over Socket.IO.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- OPEN HARDWARE -->
|
||||
<section class="tech" id="tech">
|
||||
<div class="section-inner">
|
||||
<div class="tech-content reveal">
|
||||
<div class="tech-text">
|
||||
<p class="section-eyebrow">Open Hardware</p>
|
||||
<h2 class="section-title">Built on the Seeed XIAO ESP32-C6.</h2>
|
||||
<p>BlindMaster hardware is built around the Seeed XIAO ESP32-C6 — a compact, powerful RISC-V module with built-in Wi-Fi 6 and Bluetooth 5.3. The firmware is open-source ESP-IDF, and hardware schematics will be open-sourced at launch.</p>
|
||||
<ul class="tech-list">
|
||||
<li>
|
||||
<span class="tech-check">✓</span>
|
||||
LEDC PWM servo control at 50Hz with encoder-based position feedback
|
||||
</li>
|
||||
<li>
|
||||
<span class="tech-check">✓</span>
|
||||
MAX17048 I²C fuel gauge for accurate LiPo state-of-charge monitoring
|
||||
</li>
|
||||
<li>
|
||||
<span class="tech-check">✓</span>
|
||||
Dynamic CPU scaling (80–160MHz) + light sleep for extended battery life
|
||||
</li>
|
||||
<li>
|
||||
<span class="tech-check">✓</span>
|
||||
NimBLE secure provisioning — no cloud dependency during setup
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tech-visual">
|
||||
<div class="chip-card">
|
||||
<div class="chip-glow"></div>
|
||||
<div class="chip-body">
|
||||
<div class="chip-label">ESP32-C6</div>
|
||||
<div class="chip-sub">RISC-V · Wi-Fi 6 · BT 5.3</div>
|
||||
<div class="chip-pins">
|
||||
<div class="chip-pin-row left">
|
||||
<div class="chip-pin"></div>
|
||||
<div class="chip-pin"></div>
|
||||
<div class="chip-pin"></div>
|
||||
<div class="chip-pin"></div>
|
||||
<div class="chip-pin"></div>
|
||||
</div>
|
||||
<div class="chip-core">
|
||||
<div class="chip-core-inner"></div>
|
||||
</div>
|
||||
<div class="chip-pin-row right">
|
||||
<div class="chip-pin"></div>
|
||||
<div class="chip-pin"></div>
|
||||
<div class="chip-pin"></div>
|
||||
<div class="chip-pin"></div>
|
||||
<div class="chip-pin"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chip-tags">
|
||||
<span>FreeRTOS</span>
|
||||
<span>ESP-IDF</span>
|
||||
<span>NimBLE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- COMING SOON -->
|
||||
<section class="coming-soon" id="coming-soon">
|
||||
<div class="section-inner">
|
||||
<div class="coming-soon-card reveal">
|
||||
<div class="cs-badge">
|
||||
<span class="cs-badge-dot"></span>
|
||||
Coming Soon
|
||||
</div>
|
||||
<h2>Ready to take control of your light?</h2>
|
||||
<p>
|
||||
BlindMaster is launching on <strong>TestFlight</strong> for iOS beta testers,
|
||||
with hardware schematics open-sourced at the same time. Join the early access
|
||||
list to be first to know.
|
||||
</p>
|
||||
<div class="cs-highlights">
|
||||
<div class="cs-item">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.45 2 2 0 0 1 3.59 1h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.96a16 16 0 0 0 6.29 6.29l.62-.91a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||
iOS TestFlight Beta
|
||||
</div>
|
||||
<div class="cs-item">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||
Open-Source Hardware
|
||||
</div>
|
||||
<div class="cs-item">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M12 18h.01"/></svg>
|
||||
Android App (following)
|
||||
</div>
|
||||
</div>
|
||||
<form class="cs-form" id="earlyAccessForm">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
class="cs-input"
|
||||
id="emailInput"
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary cs-btn">Notify Me</button>
|
||||
</form>
|
||||
<p class="cs-disclaimer">No spam. Just a launch notification.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="footer">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-logo">
|
||||
<div class="nav-logo-icon small">
|
||||
<div class="slat"></div>
|
||||
<div class="slat"></div>
|
||||
<div class="slat"></div>
|
||||
<div class="slat"></div>
|
||||
</div>
|
||||
<span>BlindMaster</span>
|
||||
</div>
|
||||
<p class="footer-tagline">Smart blinds. Effortless control.</p>
|
||||
<div class="footer-links">
|
||||
<a href="#features">Features</a>
|
||||
<a href="#how-it-works">How It Works</a>
|
||||
<a href="#tech">Hardware</a>
|
||||
<a href="#coming-soon">Early Access</a>
|
||||
</div>
|
||||
<p class="footer-copy">© 2026 BlindMaster. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
201
public/js/main.js
Normal file
201
public/js/main.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/* ============================================================
|
||||
BlindMaster Landing Page — main.js
|
||||
- Time-based accent color (mirrors Flutter app theming)
|
||||
- Nav scroll behavior
|
||||
- Scroll reveal animations
|
||||
- Hero blind animation
|
||||
- Interactive demo slider
|
||||
- Early access form
|
||||
============================================================ */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 1. Time-based theme (matches Flutter app color system)
|
||||
// Orange 5–10am | Blue 10am–6pm | Purple 6pm+
|
||||
// ----------------------------------------------------------
|
||||
function getThemeForHour(hour) {
|
||||
if (hour >= 5 && hour < 10) {
|
||||
return {
|
||||
name: 'morning',
|
||||
accent: '#F97316',
|
||||
accentRgb: '249, 115, 22',
|
||||
accentDark: '#C2410C',
|
||||
accentGlow: 'rgba(249, 115, 22, 0.25)',
|
||||
};
|
||||
} else if (hour >= 10 && hour < 18) {
|
||||
return {
|
||||
name: 'day',
|
||||
accent: '#3B82F6',
|
||||
accentRgb: '59, 130, 246',
|
||||
accentDark: '#1D4ED8',
|
||||
accentGlow: 'rgba(59, 130, 246, 0.25)',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: 'evening',
|
||||
accent: '#7C3AED',
|
||||
accentRgb: '124, 58, 237',
|
||||
accentDark: '#5B21B6',
|
||||
accentGlow: 'rgba(124, 58, 237, 0.25)',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--accent', theme.accent);
|
||||
root.style.setProperty('--accent-rgb', theme.accentRgb);
|
||||
root.style.setProperty('--accent-dark', theme.accentDark);
|
||||
root.style.setProperty('--accent-glow', theme.accentGlow);
|
||||
}
|
||||
|
||||
const hour = new Date().getHours();
|
||||
applyTheme(getThemeForHour(hour));
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 2. Nav — scrolled state
|
||||
// ----------------------------------------------------------
|
||||
const nav = document.getElementById('nav');
|
||||
function updateNav() {
|
||||
if (window.scrollY > 40) {
|
||||
nav.classList.add('scrolled');
|
||||
} else {
|
||||
nav.classList.remove('scrolled');
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', updateNav, { passive: true });
|
||||
updateNav();
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 3. Mobile nav toggle
|
||||
// ----------------------------------------------------------
|
||||
const hamburger = document.getElementById('hamburger');
|
||||
const mobileNav = document.getElementById('mobileNav');
|
||||
let mobileOpen = false;
|
||||
|
||||
hamburger.addEventListener('click', () => {
|
||||
mobileOpen = !mobileOpen;
|
||||
mobileNav.classList.toggle('open', mobileOpen);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.mobile-nav-link').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
mobileOpen = false;
|
||||
mobileNav.classList.remove('open');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 4. Scroll reveal
|
||||
// ----------------------------------------------------------
|
||||
const revealEls = document.querySelectorAll('.reveal');
|
||||
const revealObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
revealObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.12, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
revealEls.forEach(el => revealObserver.observe(el));
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 5. Hero blind animation
|
||||
// Cycle through positions to demo the 0–10 position model
|
||||
// ----------------------------------------------------------
|
||||
const slats = document.querySelectorAll('.blind-slat');
|
||||
const sliderThumb = document.getElementById('sliderThumb');
|
||||
|
||||
// Position 0–10: map to slat rotation angle
|
||||
// 0 = fully closed (slats flat, blocking light)
|
||||
// 5 = fully open (slats perpendicular, maximum light)
|
||||
// 10 = closed from opposite direction
|
||||
function positionToAngle(pos) {
|
||||
if (pos <= 5) {
|
||||
return (pos / 5) * 75; // 0° → 75°
|
||||
} else {
|
||||
return 75 - ((pos - 5) / 5) * 75; // 75° → 0°
|
||||
}
|
||||
}
|
||||
|
||||
function positionToSliderPercent(pos) {
|
||||
return (pos / 10) * 100;
|
||||
}
|
||||
|
||||
function setBlindPosition(pos) {
|
||||
const angle = positionToAngle(pos);
|
||||
slats.forEach(slat => {
|
||||
slat.style.transform = `rotateX(${angle}deg)`;
|
||||
});
|
||||
sliderThumb.style.left = `${positionToSliderPercent(pos)}%`;
|
||||
}
|
||||
|
||||
// Animate through positions: 0 → 5 → 10 → 5 → 0, looping
|
||||
const demoPositions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
|
||||
let demoIndex = 0;
|
||||
|
||||
setBlindPosition(demoPositions[0]);
|
||||
|
||||
setInterval(() => {
|
||||
demoIndex = (demoIndex + 1) % demoPositions.length;
|
||||
setBlindPosition(demoPositions[demoIndex]);
|
||||
}, 700);
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 6. Interactive slider on demo window (drag/click)
|
||||
// ----------------------------------------------------------
|
||||
const sliderTrack = document.querySelector('.app-slider-track');
|
||||
|
||||
function handleSliderInteraction(clientX) {
|
||||
const rect = sliderTrack.getBoundingClientRect();
|
||||
const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
||||
const pos = Math.round(pct * 10);
|
||||
setBlindPosition(pos);
|
||||
}
|
||||
|
||||
sliderTrack.addEventListener('mousedown', (e) => {
|
||||
handleSliderInteraction(e.clientX);
|
||||
const onMove = (e) => handleSliderInteraction(e.clientX);
|
||||
const onUp = () => {
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
});
|
||||
|
||||
sliderTrack.addEventListener('touchstart', (e) => {
|
||||
handleSliderInteraction(e.touches[0].clientX);
|
||||
const onMove = (e) => handleSliderInteraction(e.touches[0].clientX);
|
||||
const onEnd = () => {
|
||||
window.removeEventListener('touchmove', onMove);
|
||||
window.removeEventListener('touchend', onEnd);
|
||||
};
|
||||
window.addEventListener('touchmove', onMove);
|
||||
window.addEventListener('touchend', onEnd);
|
||||
}, { passive: true });
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 7. Early access form
|
||||
// ----------------------------------------------------------
|
||||
const form = document.getElementById('earlyAccessForm');
|
||||
const emailInput = document.getElementById('emailInput');
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const email = emailInput.value.trim();
|
||||
if (!email) return;
|
||||
|
||||
// Replace form with success message
|
||||
const successMsg = document.createElement('p');
|
||||
successMsg.className = 'cs-success';
|
||||
successMsg.textContent = `You're on the list! We'll notify ${email} at launch.`;
|
||||
form.replaceWith(successMsg);
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user