<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VERSEVA Design · Example: 3D hero</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@500..700&family=Manrope:wght@400..700&family=JetBrains+Mono:wght@400..600&display=swap">
<link rel="stylesheet" href="../../tokens.css?v=1p3">
<link rel="stylesheet" href="../../components.css?v=1p3">
<style>
* { margin: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text-1); font-family: var(--font-sans); font-size: 15px; line-height: 1.6; }
.crumb { position: fixed; top: 0; left: 0; right: 0; z-index: 5; display: flex; align-items: center; gap: var(--s-3); padding: var(--s-3) var(--s-5); font-size: 12.5px; color: var(--text-3); }
.crumb a { color: var(--text-2); text-decoration: none; font-family: var(--font-display); font-weight: var(--display-weight); }
.stage { position: relative; height: 100vh; overflow: hidden; }
#scene { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
.overlay { position: relative; z-index: 2; height: 100%; display: flex; flex-direction: column; justify-content: center; max-width: var(--container-content); margin: 0 auto; padding: 0 var(--s-6); pointer-events: none; }
.overlay .kick { font-family: var(--font-mono); font-size: 12px; color: var(--text-3); padding-bottom: var(--s-3); }
.overlay h1 { font-family: var(--font-display); font-weight: var(--display-weight); font-size: clamp(40px, 7vw, 84px); letter-spacing: -0.03em; line-height: 1.02; max-width: 14ch; }
.overlay p { color: var(--text-2); font-size: 17px; max-width: 44ch; padding-top: var(--s-4); }
.overlay .cta { display: flex; gap: var(--s-3); padding-top: var(--s-5); pointer-events: auto; }
.hint { position: absolute; z-index: 2; left: 50%; bottom: var(--s-5); transform: translateX(-50%); font-family: var(--font-mono); font-size: 11px; color: var(--text-3); white-space: nowrap; padding: var(--s-1) var(--s-3); border-radius: var(--r-pill); background: color-mix(in srgb, var(--bg) 90%, transparent); }
/* A directional scrim between the scene and the copy: the ground colour holds behind the text
and fades out over the knot, so the copy keeps its contrast in both themes. */
.stage::before { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; background: linear-gradient(90deg, color-mix(in srgb, var(--bg) 90%, transparent) 0%, color-mix(in srgb, var(--bg) 90%, transparent) 58%, transparent 84%); }
@media (max-width: 760px) {
.stage::before { background: linear-gradient(180deg, transparent 8%, color-mix(in srgb, var(--bg) 90%, transparent) 22%, color-mix(in srgb, var(--bg) 90%, transparent) 78%, transparent 92%); }
}
.below { max-width: var(--container-content); margin: 0 auto; padding: var(--s-9) var(--s-6); display: grid; gap: var(--s-5); }
.below h2 { font-family: var(--font-display); font-weight: var(--heading-weight); font-size: 24px; letter-spacing: -0.015em; }
.trio { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--s-4); }
footer { max-width: var(--container-content); margin: 0 auto; padding: 0 var(--s-6) var(--s-7); display: flex; justify-content: space-between; flex-wrap: wrap; gap: var(--s-3); color: var(--text-3); font-size: 12.5px; }
.badge-line { font-family: var(--font-display); font-weight: var(--display-weight); font-size: 13px; color: var(--text-2); }
.crumb a { display: inline-flex; align-items: center; gap: 8px; }
.crumb .vmark { width: 16px; height: 16px; flex: none; }
.crumb .codelink { margin-left: auto; }
</style>
</head>
<body>
<div class="crumb"><a href="../index.html"><svg class="vmark" viewBox="0 0 768 775.92" aria-hidden="true"><defs><clipPath id="vmk-a"><path d="M400 277h358v396.4H400z"/></clipPath><clipPath id="vmk-b"><path d="M10 97h550v576.4H10z"/></clipPath><clipPath id="vmk-c"><path d="M10 94.5h358V491H10z"/></clipPath><clipPath id="vmk-d"><path d="M208 94.5h550V671H208z"/></clipPath></defs><g clip-path="url(#vmk-a)"><path fill="currentColor" opacity=".55" d="M 558.605469 278.128906 C 555.410156 277.984375 552.164062 277.90625 548.96875 277.90625 C 466.261719 277.90625 407.433594 327.675781 401.222656 333.144531 L 400.800781 333.59375 C 400.800781 333.59375 587.386719 406.570312 718.046875 673.386719 C 718.046875 673.386719 767.757812 550.011719 756.207031 509.574219 C 740.042969 453 663.917969 362.328125 584.683594 279.359375 L 558.605469 278.128906"/></g><g clip-path="url(#vmk-b)"><path fill="currentColor" d="M 49.953125 673.386719 C 49.953125 673.386719 262.128906 253.59375 559.761719 253.59375 C 497.894531 190.457031 437.765625 134.679688 406.402344 106.136719 C 393.699219 94.574219 374.308594 94.574219 361.601562 106.136719 C 285.859375 175.0625 42.257812 402.917969 11.796875 509.574219 C 0.242188 550.011719 49.953125 673.386719 49.953125 673.386719"/></g><g clip-path="url(#vmk-c)"><path fill="currentColor" opacity=".55" d="M 209.394531 489.785156 C 212.589844 489.929688 215.835938 490.007812 219.03125 490.007812 C 301.738281 490.007812 360.566406 440.238281 366.777344 434.765625 L 367.199219 434.316406 C 367.199219 434.316406 180.613281 361.34375 49.953125 94.523438 C 49.953125 94.523438 0.242188 217.902344 11.792969 258.335938 C 27.957031 314.914062 104.082031 405.585938 183.316406 488.554688 L 209.394531 489.785156"/></g><g clip-path="url(#vmk-d)"><path fill="currentColor" d="M 718.046875 94.523438 C 718.046875 94.523438 505.871094 514.316406 208.238281 514.316406 C 270.105469 577.453125 330.234375 633.230469 361.597656 661.773438 C 374.300781 673.335938 393.691406 673.335938 406.398438 661.773438 C 482.140625 592.847656 725.742188 364.992188 756.203125 258.335938 C 767.757812 217.902344 718.046875 94.523438 718.046875 94.523438"/></g></svg><span>VERSEVA Design</span></a><span>examples</span><span>3D hero · Three.js</span><a class="btn btn-secondary sm codelink" href="../code.html?f=examples/three-hero.html">Code</a></div>
<section class="stage">
<canvas id="scene"></canvas>
<div class="overlay">
<div class="kick">SIGNAL FOUNDRY · WEBGL ON THE SIGNATURE</div>
<h1>Depth is a material too</h1>
<p>The scene reads every color from the tokens at runtime: ground, glass, and one energetic red rim. Drag to orbit; the signature holds in 3D.</p>
<div class="cta">
<button class="btn btn-primary lg" type="button" id="cta-toast">Request access</button>
<a class="btn btn-secondary lg" href="#how">How it's built</a>
</div>
</div>
<div class="hint">drag to orbit · scroll for the doctrine</div>
</section>
<section class="below" id="how">
<h2>How the scene stays on-signature</h2>
<div class="trio">
<div class="card"><div class="hd"><h4>Tokens at runtime</h4></div><div class="bd">Materials read <span style="font-family: var(--font-mono); font-size: 12.5px">--bg</span>, <span style="font-family: var(--font-mono); font-size: 12.5px">--surface-2</span> and <span style="font-family: var(--font-mono); font-size: 12.5px">--accent</span> with getComputedStyle. No hex ever enters the scene file.</div></div>
<div class="card"><div class="hd"><h4>One accent in 3D</h4></div><div class="bd">The red budget is one rim light and the particle edge. The mesh itself stays matte dark glass.</div></div>
<div class="card"><div class="hd"><h4>Reduced motion honored</h4></div><div class="bd">With reduced motion set, the idle rotation stops and the scene becomes a still composition. Orbit stays manual.</div></div>
</div>
</section>
<footer>
<span>Signal Foundry is fictional; this page is a VERSEVA Design example.</span>
<span class="badge-line">Built on VERSEVA Design</span>
</footer>
<div id="toastwrap"></div>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
const css = getComputedStyle(document.documentElement);
const tok = (name) => new THREE.Color(css.getPropertyValue(name).trim());
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('scene');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0, 8, 22);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
camera.position.set(0, 0.4, 9);
const knot = new THREE.Mesh(
new THREE.TorusKnotGeometry(2.1, 0.62, 220, 36),
new THREE.MeshStandardMaterial({ metalness: 0.55, roughness: 0.32 })
);
knot.position.x = 2.2;
scene.add(knot);
const pts = new THREE.BufferGeometry();
const N = 900, arr = new Float32Array(N * 3);
for (let i = 0; i < N; i++) {
const r = 5.4 + Math.random() * 7, t = Math.random() * Math.PI * 2, p = Math.acos(2 * Math.random() - 1);
arr[i * 3] = r * Math.sin(p) * Math.cos(t); arr[i * 3 + 1] = r * Math.cos(p) * 0.6; arr[i * 3 + 2] = r * Math.sin(p) * Math.sin(t) - 4;
}
pts.setAttribute('position', new THREE.BufferAttribute(arr, 3));
const dots = new THREE.Points(pts, new THREE.PointsMaterial({ size: 0.035, transparent: true, opacity: 0.7 }));
scene.add(dots);
const ambient = new THREE.AmbientLight(0, 0.5);
scene.add(ambient);
const key = new THREE.DirectionalLight(0, 1.1);
key.position.set(-4, 6, 6);
scene.add(key);
const rim = new THREE.PointLight(0, 260, 40);
rim.position.set(6.5, -1.5, 3.5);
scene.add(rim);
/* The scene takes its colours from the tokens, and takes them again whenever the theme or brand
changes (the studio flips a frame after load) or a theme sheet lands. In light the glass is
lit from the surface tones, so the knot stays light behind dark copy; in dark it stays dark. */
function retheme() {
const light = document.documentElement.dataset.theme === 'light';
scene.background = tok('--bg');
scene.fog.color = tok('--bg');
knot.material.color = tok('--surface-2');
knot.material.metalness = light ? 0.15 : 0.55;
dots.material.color = tok('--text-3');
ambient.color = light ? tok('--surface') : tok('--text-3');
ambient.intensity = light ? 1.6 : 0.5;
key.color = light ? tok('--surface') : tok('--text-1');
rim.color = tok('--accent');
rim.intensity = light ? 90 : 260;
renderer.render(scene, camera);
}
new MutationObserver(retheme).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'data-brand'] });
document.addEventListener('load', (e) => { if (e.target instanceof HTMLLinkElement) retheme(); }, true);
let dragX = 0, dragY = 0, tx = 0, ty = 0, dragging = false, px = 0, py = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; px = e.clientX; py = e.clientY; });
addEventListener('pointerup', () => { dragging = false; });
addEventListener('pointermove', (e) => {
if (!dragging) return;
tx += (e.clientX - px) * 0.005; ty += (e.clientY - py) * 0.004;
ty = Math.max(-0.9, Math.min(0.9, ty));
px = e.clientX; py = e.clientY;
});
function size() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
addEventListener('resize', size);
size();
retheme();
renderer.setAnimationLoop((t) => {
if (!reduced) { knot.rotation.y = t * 0.00012; dots.rotation.y = t * 0.00003; }
dragX += (tx - dragX) * 0.08; dragY += (ty - dragY) * 0.08;
knot.rotation.x = dragY; knot.rotation.z = dragX * 0.6;
renderer.render(scene, camera);
});
</script>
<script>
document.getElementById('cta-toast').addEventListener('click', () => {
const wrap = document.getElementById('toastwrap');
wrap.replaceChildren();
const toast = document.createElement('div');
toast.className = 'toast ok';
toast.style.position = 'fixed';
toast.style.right = 'var(--s-5)';
toast.style.bottom = 'var(--s-5)';
toast.style.zIndex = '9';
toast.innerHTML = '<span class="dot"></span>Request received. The fictional team will be in touch.';
wrap.append(toast);
setTimeout(() => toast.remove(), 2600);
});
</script>
</body>
</html>