Files

879 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Synapse — Living Intelligence</title>
<style>
:root {
--bg-deep: #0a1a14;
--bg-teal: #061a1a;
--accent-emerald:#00e676;
--accent-cyan: #00bcd4;
--accent-mid: #2cba74;
--text-primary: #e8f5f0;
--text-secondary:#8fb8a8;
--text-dim: #4a7060;
--border-dim: rgba(0, 230, 118, 0.12);
--glow-emerald: rgba(0, 230, 118, 0.18);
--glow-cyan: rgba(0, 188, 212, 0.15);
--font-body: 'Georgia', 'Times New Roman', serif;
--font-ui: system-ui, -apple-system, 'Segoe UI', sans-serif;
--max-w: 860px;
--section-gap: clamp(5rem, 10vw, 9rem);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg-deep);
color: var(--text-primary);
font-family: var(--font-body);
font-size: clamp(1rem, 1.1vw + 0.6rem, 1.15rem);
line-height: 1.75;
overflow-x: hidden;
}
/* ── Canvas ── */
#neural-canvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
/* ── Layout shell ── */
.page {
position: relative;
z-index: 1;
}
.container {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 clamp(1.25rem, 5vw, 3rem);
}
/* ── NAV ── */
nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
padding: 1.4rem clamp(1.25rem, 5vw, 3rem);
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(to bottom, rgba(10,26,20,0.9) 0%, transparent 100%);
backdrop-filter: blur(2px);
}
.nav-wordmark {
font-family: var(--font-ui);
font-size: 1.05rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent-emerald);
text-shadow: 0 0 18px rgba(0,230,118,0.5);
}
.nav-cta {
font-family: var(--font-ui);
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent-emerald);
border: 1px solid rgba(0,230,118,0.35);
padding: 0.55rem 1.2rem;
border-radius: 2px;
text-decoration: none;
transition: background 0.25s, box-shadow 0.25s, color 0.25s;
min-height: 44px;
display: flex;
align-items: center;
}
.nav-cta:hover, .nav-cta:focus-visible {
background: rgba(0,230,118,0.1);
box-shadow: 0 0 20px rgba(0,230,118,0.25);
outline: none;
}
/* ── HERO ── */
#hero {
min-height: 100svh;
display: grid;
place-items: center;
text-align: center;
padding: 8rem clamp(1.25rem, 5vw, 3rem) 5rem;
}
.hero-inner {
max-width: 680px;
}
.hero-label {
font-family: var(--font-ui);
font-size: 0.72rem;
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(--accent-cyan);
margin-bottom: 2rem;
opacity: 0.85;
}
.hero-title {
font-family: var(--font-ui);
font-size: clamp(3.8rem, 10vw, 7.5rem);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 0.95;
margin-bottom: 1.8rem;
background: linear-gradient(135deg, #fff 0%, var(--accent-emerald) 55%, var(--accent-cyan) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
filter: drop-shadow(0 0 40px rgba(0,230,118,0.3));
}
.hero-tagline {
font-size: clamp(1.05rem, 1.5vw + 0.6rem, 1.3rem);
color: var(--text-secondary);
line-height: 1.65;
margin-bottom: 3.2rem;
max-width: 520px;
margin-left: auto;
margin-right: auto;
}
.hero-cta {
display: inline-flex;
align-items: center;
gap: 0.6rem;
font-family: var(--font-ui);
font-size: 0.9rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #021a10;
background: var(--accent-emerald);
padding: 1rem 2.4rem;
border-radius: 3px;
text-decoration: none;
min-height: 54px;
position: relative;
transition: transform 0.2s, box-shadow 0.3s;
box-shadow: 0 0 30px rgba(0,230,118,0.35), 0 0 60px rgba(0,230,118,0.12);
animation: cta-breathe 3s ease-in-out infinite;
}
@keyframes cta-breathe {
0%, 100% { box-shadow: 0 0 30px rgba(0,230,118,0.35), 0 0 60px rgba(0,230,118,0.12); }
50% { box-shadow: 0 0 45px rgba(0,230,118,0.55), 0 0 90px rgba(0,230,118,0.22); }
}
.hero-cta:hover, .hero-cta:focus-visible {
transform: translateY(-2px);
box-shadow: 0 6px 50px rgba(0,230,118,0.5), 0 0 100px rgba(0,230,118,0.25);
outline: none;
}
.hero-cta:active { transform: translateY(0); }
/* ── SECTION SHARED ── */
section {
padding: var(--section-gap) 0;
}
.section-label {
font-family: var(--font-ui);
font-size: 0.68rem;
letter-spacing: 0.38em;
text-transform: uppercase;
color: var(--accent-cyan);
margin-bottom: 2.4rem;
opacity: 0.75;
}
.section-title {
font-family: var(--font-ui);
font-size: clamp(1.7rem, 3vw + 0.8rem, 2.6rem);
font-weight: 600;
letter-spacing: -0.015em;
line-height: 1.15;
color: var(--text-primary);
margin-bottom: 2rem;
}
.section-divider {
width: 40px;
height: 1px;
background: linear-gradient(to right, var(--accent-emerald), transparent);
margin-bottom: 2.8rem;
}
/* ── WHAT IT IS ── */
#what {
border-top: 1px solid var(--border-dim);
}
.what-body p {
color: var(--text-secondary);
margin-bottom: 1.4rem;
}
.what-body p:last-child { margin-bottom: 0; }
.what-body strong {
color: var(--text-primary);
font-weight: normal;
font-style: italic;
}
/* ── CAPABILITIES ── */
#capabilities {
background: linear-gradient(180deg, transparent 0%, rgba(6,26,26,0.6) 40%, rgba(6,26,26,0.6) 60%, transparent 100%);
border-top: 1px solid var(--border-dim);
border-bottom: 1px solid var(--border-dim);
}
.cap-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0;
}
.cap-item {
display: grid;
grid-template-columns: 3rem 1fr;
gap: 0 1.6rem;
padding: 2.2rem 0;
border-bottom: 1px solid var(--border-dim);
align-items: start;
transition: background 0.2s;
}
.cap-item:last-child { border-bottom: none; }
.cap-item:hover {
background: rgba(0,230,118,0.03);
}
.cap-number {
font-family: var(--font-ui);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--accent-emerald);
padding-top: 0.25rem;
opacity: 0.7;
}
.cap-content {}
.cap-title {
font-family: var(--font-ui);
font-size: 1.05rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.55rem;
letter-spacing: -0.01em;
}
.cap-desc {
font-size: 0.95rem;
color: var(--text-secondary);
line-height: 1.65;
}
/* ── PHILOSOPHY ── */
#philosophy {
border-top: 1px solid var(--border-dim);
}
.philosophy-quote {
border-left: 3px solid transparent;
border-image: linear-gradient(to bottom, var(--accent-emerald), var(--accent-cyan), transparent) 1;
padding: 1.6rem 0 1.6rem 2.2rem;
margin: 2.8rem 0 0;
position: relative;
}
.philosophy-quote::before {
content: '';
position: absolute;
left: -1px;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(to bottom, var(--accent-emerald) 0%, var(--accent-cyan) 60%, transparent 100%);
border-radius: 2px;
box-shadow: 0 0 12px rgba(0,230,118,0.4);
}
.philosophy-quote blockquote {
font-size: clamp(1.1rem, 1.5vw + 0.6rem, 1.35rem);
font-style: italic;
color: var(--text-primary);
line-height: 1.7;
margin-bottom: 1.2rem;
}
.philosophy-quote cite {
font-family: var(--font-ui);
font-size: 0.75rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--accent-mid);
font-style: normal;
}
.philosophy-body {
color: var(--text-secondary);
margin-top: 2.4rem;
}
/* ── CTA SECTION ── */
#cta-section {
border-top: 1px solid var(--border-dim);
text-align: center;
}
.cta-tagline {
color: var(--text-secondary);
margin-bottom: 2.6rem;
max-width: 460px;
margin-left: auto;
margin-right: auto;
}
.email-form {
display: flex;
gap: 0;
max-width: 420px;
margin: 0 auto 1.2rem;
border: 1px solid rgba(0,230,118,0.25);
border-radius: 3px;
overflow: hidden;
transition: border-color 0.25s, box-shadow 0.25s;
}
.email-form:focus-within {
border-color: rgba(0,230,118,0.6);
box-shadow: 0 0 24px rgba(0,230,118,0.15);
}
.email-input {
flex: 1;
background: rgba(0,230,118,0.04);
border: none;
padding: 0.9rem 1.2rem;
font-family: var(--font-ui);
font-size: 0.9rem;
color: var(--text-primary);
outline: none;
min-height: 54px;
}
.email-input::placeholder { color: var(--text-dim); }
.email-submit {
background: var(--accent-emerald);
border: none;
padding: 0.9rem 1.4rem;
font-family: var(--font-ui);
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #021a10;
cursor: pointer;
min-height: 54px;
min-width: 44px;
transition: background 0.2s;
}
.email-submit:hover, .email-submit:focus-visible {
background: #33ff99;
outline: none;
}
.form-note {
font-family: var(--font-ui);
font-size: 0.72rem;
color: var(--text-dim);
letter-spacing: 0.05em;
}
/* ── FOOTER ── */
footer {
padding: 2.8rem 0;
border-top: 1px solid var(--border-dim);
}
.footer-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.footer-wordmark {
font-family: var(--font-ui);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent-emerald);
opacity: 0.6;
}
.footer-copy {
font-family: var(--font-ui);
font-size: 0.72rem;
color: var(--text-dim);
letter-spacing: 0.04em;
}
/* ── REDUCED MOTION ── */
@media (prefers-reduced-motion: reduce) {
.hero-cta { animation: none; }
html { scroll-behavior: auto; }
}
/* ── MOBILE ── */
@media (max-width: 600px) {
.footer-inner { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
.cap-item { grid-template-columns: 2.2rem 1fr; gap: 0 1rem; }
}
</style>
</head>
<body>
<canvas id="neural-canvas" aria-hidden="true"></canvas>
<div class="page">
<!-- NAV -->
<nav aria-label="Primary navigation">
<span class="nav-wordmark">Synapse</span>
<a class="nav-cta" href="#cta-section">Early Access</a>
</nav>
<!-- HERO -->
<section id="hero" aria-labelledby="hero-heading">
<div class="hero-inner">
<p class="hero-label">Living Intelligence</p>
<h1 class="hero-title" id="hero-heading">Synapse</h1>
<p class="hero-tagline">
Not a tool that answers. An intelligence that grows — branching, learning, and adapting the way living neural tissue does.
</p>
<a class="hero-cta" href="#cta-section">Request Early Access</a>
</div>
</section>
<!-- WHAT IT IS -->
<section id="what" aria-labelledby="what-heading">
<div class="container">
<p class="section-label">What It Is</p>
<h2 class="section-title" id="what-heading">Intelligence that grows<br>like living tissue</h2>
<div class="section-divider"></div>
<div class="what-body">
<p>
Synapse is not a model you prompt and receive from. It is a <strong>continuously evolving cognitive system</strong> — one that forms associations the way neurons do, strengthening pathways through use, letting dormant ones decay, always reshaping the structure of its own understanding.
</p>
<p>
Where conventional AI retrieves patterns, Synapse <strong>develops them</strong>. It builds internal representations of context over time, treats every interaction as growth rather than transaction, and maintains a living model of the world that changes as the world changes. The result is an intelligence that does not merely know — it comprehends.
</p>
</div>
</div>
</section>
<!-- CAPABILITIES -->
<section id="capabilities" aria-labelledby="cap-heading">
<div class="container">
<p class="section-label">Capabilities</p>
<h2 class="section-title" id="cap-heading">Four properties<br>no static model has</h2>
<div class="section-divider"></div>
<div class="cap-grid">
<article class="cap-item">
<span class="cap-number" aria-hidden="true">01</span>
<div class="cap-content">
<h3 class="cap-title">Adaptive Pathway Reinforcement</h3>
<p class="cap-desc">
Frequently used reasoning paths strengthen over time. Rare ones thin and recede. Synapse builds a weighted graph of thought that reflects real experience, not a frozen training snapshot.
</p>
</div>
</article>
<article class="cap-item">
<span class="cap-number" aria-hidden="true">02</span>
<div class="cap-content">
<h3 class="cap-title">Contextual Memory Architecture</h3>
<p class="cap-desc">
Rather than holding a flat context window, Synapse maintains layered memory — immediate, associative, and deep — letting it reference the texture of long conversations without losing the thread.
</p>
</div>
</article>
<article class="cap-item">
<span class="cap-number" aria-hidden="true">03</span>
<div class="cap-content">
<h3 class="cap-title">Organic Uncertainty Modeling</h3>
<p class="cap-desc">
Synapse knows the difference between what it knows well and what it holds loosely. It communicates that gradient — not as a disclaimer, but as a natural part of how it reasons aloud.
</p>
</div>
</article>
<article class="cap-item">
<span class="cap-number" aria-hidden="true">04</span>
<div class="cap-content">
<h3 class="cap-title">Relational World Modeling</h3>
<p class="cap-desc">
Concepts inside Synapse do not exist in isolation. They are connected, weighted, and alive to each other. Ask it about one thing and it draws on a whole web of adjacent understanding without being told to.
</p>
</div>
</article>
</div>
</div>
</section>
<!-- PHILOSOPHY -->
<section id="philosophy" aria-labelledby="phil-heading">
<div class="container">
<p class="section-label">Philosophy</p>
<h2 class="section-title" id="phil-heading">Intelligence should live,<br>not just execute</h2>
<div class="section-divider"></div>
<div class="philosophy-quote">
<blockquote>
"The brain does not process information; it grows it. Every thought leaves a physical trace — a pathway slightly more conductive than it was before. Understanding is not retrieved. It is cultivated."
</blockquote>
<cite>On the architecture of Synapse</cite>
</div>
<p class="philosophy-body">
We built Synapse from a conviction that the dominant metaphor for AI — the lookup table, the search engine, the statistical mouth — is not just limiting but wrong. Intelligence in nature is not a function from input to output. It is a living structure that changes shape as it encounters the world. We wanted to build something closer to that.
</p>
</div>
</section>
<!-- CTA -->
<section id="cta-section" aria-labelledby="cta-heading">
<div class="container">
<p class="section-label">Early Access</p>
<h2 class="section-title" id="cta-heading">Join the first wave</h2>
<div class="section-divider"></div>
<p class="cta-tagline">
Synapse is in closed development. Leave your address and we will reach out when a place opens.
</p>
<form class="email-form" onsubmit="handleSubmit(event)" novalidate aria-label="Early access request">
<label for="email-input" class="sr-only">Email address</label>
<input
id="email-input"
class="email-input"
type="email"
name="email"
placeholder="your@address.com"
autocomplete="email"
required
aria-required="true"
/>
<button class="email-submit" type="submit">Request</button>
</form>
<p class="form-note" role="status" id="form-status">No noise. One message when your place is ready.</p>
</div>
</section>
<!-- FOOTER -->
<footer>
<div class="container">
<div class="footer-inner">
<span class="footer-wordmark">Synapse</span>
<span class="footer-copy">2026 Synapse. All rights reserved.</span>
</div>
</div>
</footer>
</div><!-- /.page -->
<style>
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
</style>
<script>
/* ─────────────────────────────────────────────
Neural Canvas Animation
───────────────────────────────────────────── */
(function() {
const canvas = document.getElementById('neural-canvas');
const ctx = canvas.getContext('2d');
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/* palette */
const C = {
bg: '#0a1a14',
emerald: [0, 230, 118],
cyan: [0, 188, 212],
mid: [44, 186, 116],
};
/* ── resize ── */
let W, H;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
/* ── Node ── */
class Node {
constructor() { this.reset(true); }
reset(init) {
this.x = Math.random() * W;
this.y = Math.random() * H;
/* organic drift — slow, curved, non-uniform */
this.vx = (Math.random() - 0.5) * 0.28;
this.vy = (Math.random() - 0.5) * 0.28;
/* base radius and breathing phase */
this.r = 1.8 + Math.random() * 3.2;
this.phase = Math.random() * Math.PI * 2;
this.speed = 0.008 + Math.random() * 0.014;
/* colour — mostly emerald, occasional cyan */
this.col = Math.random() < 0.72 ? C.emerald : C.cyan;
/* pulse brightness */
this.bright = 0.4 + Math.random() * 0.6;
/* organic wander */
this.wx = Math.random() * 1000;
this.wy = Math.random() * 1000 + 500;
}
update(t) {
/* subtle Perlin-like wander via sin sums */
const ox = Math.sin(this.wx + t * 0.0003) * 0.12 + Math.sin(this.wx * 1.7 + t * 0.00017) * 0.07;
const oy = Math.cos(this.wy + t * 0.00025) * 0.12 + Math.cos(this.wy * 1.4 + t * 0.0002) * 0.07;
this.x += this.vx + ox;
this.y += this.vy + oy;
/* wrap with soft margin */
const m = 60;
if (this.x < -m) this.x = W + m;
if (this.x > W + m) this.x = -m;
if (this.y < -m) this.y = H + m;
if (this.y > H + m) this.y = -m;
}
currentR(t) {
/* breathing: radius gently oscillates */
return this.r * (0.82 + 0.18 * Math.sin(this.phase + t * this.speed));
}
currentAlpha(t) {
return this.bright * (0.55 + 0.45 * Math.sin(this.phase * 1.3 + t * this.speed * 0.8));
}
draw(t) {
const r = this.currentR(t);
const a = this.currentAlpha(t);
const [R, G, B] = this.col;
/* outer glow */
const grd = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, r * 4.5);
grd.addColorStop(0, `rgba(${R},${G},${B},${(a * 0.9).toFixed(3)})`);
grd.addColorStop(0.4, `rgba(${R},${G},${B},${(a * 0.22).toFixed(3)})`);
grd.addColorStop(1, `rgba(${R},${G},${B},0)`);
ctx.beginPath();
ctx.arc(this.x, this.y, r * 4.5, 0, Math.PI * 2);
ctx.fillStyle = grd;
ctx.fill();
/* core */
ctx.beginPath();
ctx.arc(this.x, this.y, r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${R},${G},${B},${Math.min(a * 1.4, 1).toFixed(3)})`;
ctx.fill();
}
}
/* ── Connection pulse ── */
class Pulse {
constructor(ax, ay, bx, by, col) {
this.ax = ax; this.ay = ay;
this.bx = bx; this.by = by;
this.col = col;
this.t = 0; /* 0→1 travel progress */
this.spd = 0.004 + Math.random() * 0.006;
this.r = 1.2 + Math.random() * 1.6;
this.done = false;
}
update() {
this.t += this.spd;
if (this.t >= 1) this.done = true;
}
draw() {
const px = this.ax + (this.bx - this.ax) * this.t;
const py = this.ay + (this.by - this.ay) * this.t;
const alpha = Math.sin(this.t * Math.PI) * 0.9;
const [R,G,B] = this.col;
const grd = ctx.createRadialGradient(px, py, 0, px, py, this.r * 3);
grd.addColorStop(0, `rgba(${R},${G},${B},${alpha.toFixed(3)})`);
grd.addColorStop(0.5,`rgba(${R},${G},${B},${(alpha*0.3).toFixed(3)})`);
grd.addColorStop(1, `rgba(${R},${G},${B},0)`);
ctx.beginPath();
ctx.arc(px, py, this.r * 3, 0, Math.PI * 2);
ctx.fillStyle = grd;
ctx.fill();
}
}
/* ── populate ── */
const NODE_COUNT = prefersReduced ? 0 : Math.min(Math.floor(W * H / 14000), 80);
const nodes = Array.from({ length: NODE_COUNT }, () => new Node());
const pulses = [];
const CONNECT_DIST = 180;
const CONNECT_DIST2 = CONNECT_DIST * CONNECT_DIST;
let lastPulseTime = 0;
/* ── main loop ── */
let animId;
function frame(t) {
animId = requestAnimationFrame(frame);
/* clear */
ctx.clearRect(0, 0, W, H);
/* background gradient — subtle depth */
const bg = ctx.createRadialGradient(W * 0.5, H * 0.38, 0, W * 0.5, H * 0.5, Math.max(W, H) * 0.75);
bg.addColorStop(0, 'rgba(10,28,20,0.96)');
bg.addColorStop(0.55,'rgba(8,22,18,0.98)');
bg.addColorStop(1, 'rgba(6,18,18,1)');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
/* update nodes */
nodes.forEach(n => n.update(t));
/* draw connections */
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j];
const dx = b.x - a.x;
const dy = b.y - a.y;
const d2 = dx * dx + dy * dy;
if (d2 > CONNECT_DIST2) continue;
const dist = Math.sqrt(d2);
const frac = 1 - dist / CONNECT_DIST;
/* blend colours toward average of the two nodes */
const [R1,G1,B1] = a.col;
const [R2,G2,B2] = b.col;
const R = Math.round((R1+R2)/2);
const G = Math.round((G1+G2)/2);
const B = Math.round((B1+B2)/2);
const aA = a.currentAlpha(t);
const aB = b.currentAlpha(t);
const lineAlpha = frac * frac * 0.35 * Math.min(aA, aB) * 1.6;
/* organic thickness — thicker near midpoint */
const midPulse = 0.5 + 0.5 * Math.sin(t * 0.0008 + i * 0.4 + j * 0.17);
const lw = (0.4 + frac * 1.6) * (0.7 + 0.3 * midPulse);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
/* slight organic curve via quadratic — control point displaced perpendicularly */
const mx = (a.x + b.x) * 0.5;
const my = (a.y + b.y) * 0.5;
const perp = Math.sin(t * 0.0005 + i * 0.7 + j * 0.3) * dist * 0.08;
const nx = -dy / dist;
const ny = dx / dist;
ctx.quadraticCurveTo(mx + nx * perp, my + ny * perp, b.x, b.y);
ctx.strokeStyle = `rgba(${R},${G},${B},${lineAlpha.toFixed(3)})`;
ctx.lineWidth = lw;
ctx.stroke();
}
}
/* spawn pulses */
if (t - lastPulseTime > 220 && nodes.length > 1) {
lastPulseTime = t;
/* pick two nearby nodes */
const a = nodes[Math.floor(Math.random() * nodes.length)];
const candidates = nodes.filter(n => {
if (n === a) return false;
const dx = n.x - a.x, dy = n.y - a.y;
return dx*dx + dy*dy < CONNECT_DIST2;
});
if (candidates.length) {
const b = candidates[Math.floor(Math.random() * candidates.length)];
pulses.push(new Pulse(a.x, a.y, b.x, b.y, Math.random() < 0.6 ? C.emerald : C.cyan));
}
}
/* update + draw pulses */
for (let i = pulses.length - 1; i >= 0; i--) {
pulses[i].update();
pulses[i].draw();
if (pulses[i].done) pulses.splice(i, 1);
}
/* draw nodes on top */
nodes.forEach(n => n.draw(t));
}
if (!prefersReduced) {
requestAnimationFrame(frame);
} else {
/* static dim background only */
ctx.fillStyle = C.bg;
ctx.fillRect(0, 0, W, H);
}
/* pause when tab hidden */
document.addEventListener('visibilitychange', () => {
if (document.hidden) cancelAnimationFrame(animId);
else requestAnimationFrame(frame);
});
})();
/* ── Form ── */
function handleSubmit(e) {
e.preventDefault();
const input = document.getElementById('email-input');
const status = document.getElementById('form-status');
const val = input.value.trim();
if (!val || !val.includes('@')) {
status.textContent = 'Please enter a valid email address.';
input.focus();
return;
}
status.textContent = 'Received. We will be in touch when your place opens.';
input.value = '';
input.disabled = true;
document.querySelector('.email-submit').disabled = true;
}
</script>
</body>
</html>