diff --git a/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx b/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx
index fa64288..e9508bb 100644
--- a/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx
+++ b/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx
@@ -1,22 +1,23 @@
-import React, { useEffect, useRef } from 'react';
+import React, { useEffect } from 'react';
import {
ArrowRight,
BrainCircuit,
CheckCircle2,
ChevronDown,
+ Dna,
FlaskConical,
Gauge,
Layers3,
- LockKeyhole,
Microscope,
Play,
Share2,
Sparkles,
WandSparkles,
+ Waves,
Zap,
} from 'lucide-react';
import { Button } from '../components/ui/Button.jsx';
-import { AttractorPlayground } from '../features/gaugegap/AttractorPlayground.jsx';
+import { ExperimentArcade } from '../features/gaugegap/ExperimentArcade.jsx';
import { track } from '../lib/analytics.js';
import '../styles/gaugegap.css';
@@ -25,11 +26,35 @@ const CONTENT_SAMPLE = 'Everyone says this breakthrough changes everything. Here
const LABS = [
{
id: 'attractor',
- eyebrow: 'Live now',
+ eyebrow: 'Live experiment 001',
title: 'Butterfly Effect Lab',
- description: 'Change the rules of a chaotic system, capture the result and challenge someone to beat your discovery score.',
+ description: 'Tune a chaotic system until order and turbulence balance on the same orbit.',
icon: Gauge,
- action: 'Play now',
+ action: 'Shape chaos',
+ },
+ {
+ id: 'fireflies',
+ eyebrow: 'Live experiment 002',
+ title: 'Firefly Sync Lab',
+ description: 'Connect hundreds of independent clocks and watch collective rhythm emerge without a leader.',
+ icon: Sparkles,
+ action: 'Sync the swarm',
+ },
+ {
+ id: 'waves',
+ eyebrow: 'Live experiment 003',
+ title: 'Wave Eraser',
+ description: 'Move a detector through overlapping waves and find the exact places where energy disappears.',
+ icon: Waves,
+ action: 'Find silence',
+ },
+ {
+ id: 'reaction',
+ eyebrow: 'Live experiment 004',
+ title: 'Living Chemistry',
+ description: 'Draw into a reaction-diffusion field and grow coral, cells, worms and mazes from two chemicals.',
+ icon: Dna,
+ action: 'Grow a species',
},
{
id: 'soliton',
@@ -47,25 +72,17 @@ const LABS = [
icon: BrainCircuit,
action: 'Scan a viral claim',
},
- {
- id: 'claim',
- eyebrow: 'Coming next',
- title: 'Claim Boundary Game',
- description: 'Build the strongest claim the evidence allows. Push too far and the model breaks your argument in public.',
- icon: LockKeyhole,
- action: 'Join the first run',
- },
];
const LOOP = [
{ number: '01', title: 'Play', text: 'Touch the variables before reading the lesson. The system teaches through response.' },
{ number: '02', title: 'Discover', text: 'Find an unusual state, score it and preserve the exact parameters that produced it.' },
- { number: '03', title: 'Publish', text: 'Export the visual, the short clip, the challenge link and the explanation from one run.' },
+ { number: '03', title: 'Publish', text: 'Export the visual, the challenge link and the explanation from one run.' },
];
-export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
- const playgroundRef = useRef(null);
+const ARCADE_IDS = new Set(['attractor', 'fireflies', 'waves', 'reaction']);
+export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
useEffect(() => {
document.title = 'GaugeGap Foundry | Play with the impossible';
track('gaugegap_landing_viewed');
@@ -78,10 +95,18 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
function openLab(id) {
track('gaugegap_lab_clicked', { labId: id });
- if (id === 'attractor') scrollToPlayground();
+ if (ARCADE_IDS.has(id)) {
+ const url = new URL(window.location.href);
+ url.searchParams.set('lab', id);
+ url.searchParams.delete('run');
+ url.searchParams.delete('state');
+ url.hash = 'playground';
+ window.history.replaceState({}, '', url);
+ window.dispatchEvent(new CustomEvent('gaugegap:lab', { detail: { lab: id } }));
+ scrollToPlayground();
+ }
if (id === 'soliton') onNavigate?.('research');
if (id === 'content') onStart?.(CONTENT_SAMPLE);
- if (id === 'claim') onOpenReconstruct?.();
}
return (
@@ -96,7 +121,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
Alpha
- Playground
+ Arcade
document.getElementById('labs')?.scrollIntoView({ behavior: 'smooth' })}>Experiments
onNavigate?.('research')}>Research
@@ -115,18 +140,18 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
Science you can touch, remix and publish
Don’t just learn the universe. Play with it.
- GaugeGap turns difficult research into live experiments, visual challenges and shareable discoveries.
- Change one rule. Watch reality respond. Publish what you find.
+ Four live experiments turn difficult science into visual games and shareable challenges.
+ Shape chaos, synchronize a crowd, erase a wave or draw a new species.
-
Enter the live experiment
+
Enter the science arcade
onNavigate?.('research')}>See the research layer
-
Live Browser simulations
-
1-click Posters and clips
+
4 live Distinct experiments
+
1-click Challenge links
Exact Shareable run states
-
Open Assumptions and limits
+
Open Equations and limits
@@ -157,20 +182,18 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
Find the most beautiful edge of chaos.
-
Current community score to beat
-
90+
+
Then try three completely different systems
+
4 labs
Accept challenge
-
- Scroll to experiment
+
+ Scroll to arcade
-
+
@@ -190,9 +213,9 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
One run creates
Interactive link
- 6-second clip
- Poster
+ Visual loop
Score challenge
+ Lesson hook
Research state
@@ -209,7 +232,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
{LABS.map((lab, index) => {
const Icon = lab.icon;
return (
-
+
@@ -257,10 +280,10 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
- Your first discovery is already running
- Change one variable. Make something nobody has seen.
- Then turn the result into the clip, challenge and explanation that brings the next person into the lab.
- Play with the butterfly
+ Four worlds are already running
+ Pick a system. Break its rules. Share what appears.
+ The next person should arrive through your challenge—not through another explanation page.
+ Enter the science arcade
@@ -270,7 +293,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
GaugeGap Foundry Playable science and publishing infrastructure by BrainSNN.
- Playground
+ Arcade
onNavigate?.('research')}>Research
onStart?.('')}>BrainSNN scanner
onOpenReconstruct?.()}>Reconstruct
diff --git a/brainsnn-r3f-app/src/features/gaugegap/CollectivePlaygrounds.jsx b/brainsnn-r3f-app/src/features/gaugegap/CollectivePlaygrounds.jsx
new file mode 100644
index 0000000..7d6b328
--- /dev/null
+++ b/brainsnn-r3f-app/src/features/gaugegap/CollectivePlaygrounds.jsx
@@ -0,0 +1,630 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { Copy, MousePointer2, Pause, Play, RotateCcw, Share2, Sparkles } from 'lucide-react';
+
+function clamp(value, min, max) {
+ return Math.min(max, Math.max(min, value));
+}
+
+function round(value, places = 2) {
+ const factor = 10 ** places;
+ return Math.round(value * factor) / factor;
+}
+
+async function shareExperiment({ lab, title, text, state, onNotice }) {
+ const url = new URL(window.location.href);
+ url.searchParams.set('lab', lab);
+ url.searchParams.delete('run');
+ url.searchParams.set('state', state);
+ url.hash = 'playground';
+ try {
+ if (navigator.share) {
+ await navigator.share({ title, text, url: url.toString() });
+ } else {
+ await navigator.clipboard.writeText(url.toString());
+ onNotice('Challenge link copied. The exact settings are inside it.');
+ }
+ } catch (error) {
+ if (error?.name !== 'AbortError') onNotice('Sharing was blocked. Use Copy challenge instead.');
+ }
+}
+
+function copyExperiment({ lab, state, onNotice }) {
+ const url = new URL(window.location.href);
+ url.searchParams.set('lab', lab);
+ url.searchParams.delete('run');
+ url.searchParams.set('state', state);
+ url.hash = 'playground';
+ navigator.clipboard?.writeText(url.toString())
+ .then(() => onNotice('Challenge copied. Send it to someone who thinks they can do better.'))
+ .catch(() => onNotice('Copy was blocked by the browser. Use Share challenge.'));
+}
+
+function parseState(lab, count) {
+ if (typeof window === 'undefined') return null;
+ const params = new URLSearchParams(window.location.search);
+ if (params.get('lab') !== lab) return null;
+ const raw = params.get('state');
+ if (!raw) return null;
+ const values = raw.split(',').map(Number);
+ if (values.length !== count || !values.every(Number.isFinite)) return null;
+ return values;
+}
+
+function LabToolbar({ paused, onToggle, onReset, onShare, onCopy, notice }) {
+ return (
+
+
+
{paused ? : }{paused ? 'Resume' : 'Pause'}
+
Reset
+
Share challenge
+
Copy link
+
+
{notice}
+
+ );
+}
+
+function RangeControl({ label, value, min, max, step, onChange, suffix = '' }) {
+ return (
+
+ {label} {round(value, step < 0.01 ? 3 : 2)}{suffix}
+ onChange(Number(event.target.value))} />
+
+ );
+}
+
+const FIREFLY_PRESETS = [
+ { id: 'festival', label: 'Festival sync', coupling: 1.65, noise: 0.08, population: 120, speed: 1 },
+ { id: 'wild', label: 'Wild meadow', coupling: 0.32, noise: 0.52, population: 170, speed: 1.1 },
+ { id: 'whisper', label: 'Slow whisper', coupling: 0.82, noise: 0.04, population: 85, speed: 0.62 },
+ { id: 'storm', label: 'Electric storm', coupling: 2.5, noise: 0.2, population: 210, speed: 1.55 },
+];
+
+export function FireflySyncLab() {
+ const shared = useMemo(() => parseState('fireflies', 4), []);
+ const [params, setParams] = useState(() => shared ? {
+ coupling: clamp(shared[0], 0, 3),
+ noise: clamp(shared[1], 0, 1),
+ population: Math.round(clamp(shared[2], 30, 240)),
+ speed: clamp(shared[3], 0.3, 2),
+ } : FIREFLY_PRESETS[0]);
+ const [paused, setPaused] = useState(false);
+ const [sync, setSync] = useState(0);
+ const [notice, setNotice] = useState(shared ? 'Shared swarm loaded. Can you drive it into perfect sync?' : 'Raise coupling and watch a crowd discover one rhythm.');
+ const canvasRef = useRef(null);
+ const paramsRef = useRef(params);
+ const pausedRef = useRef(paused);
+ const swarmRef = useRef([]);
+ const resetRef = useRef(0);
+
+ useEffect(() => { paramsRef.current = params; }, [params]);
+ useEffect(() => { pausedRef.current = paused; }, [paused]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return undefined;
+ const context = canvas.getContext('2d', { alpha: false });
+ let width = 0;
+ let height = 0;
+ let frameId = 0;
+ let lastReset = -1;
+ let lastTime = performance.now();
+ let lastScore = 0;
+
+ function resize() {
+ const rect = canvas.getBoundingClientRect();
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
+ width = Math.max(320, rect.width);
+ height = Math.max(360, rect.height);
+ canvas.width = Math.floor(width * ratio);
+ canvas.height = Math.floor(height * ratio);
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
+ }
+
+ function buildSwarm() {
+ const count = paramsRef.current.population;
+ swarmRef.current = Array.from({ length: count }, (_, index) => ({
+ x: Math.random(),
+ y: Math.random(),
+ phase: Math.random() * Math.PI * 2,
+ omega: 0.76 + Math.random() * 0.48,
+ size: 1.5 + Math.random() * 2.7,
+ drift: (index % 2 ? 1 : -1) * (0.00004 + Math.random() * 0.00014),
+ }));
+ }
+
+ const observer = new ResizeObserver(resize);
+ observer.observe(canvas);
+ resize();
+ buildSwarm();
+
+ function render(now) {
+ if (lastReset !== resetRef.current || swarmRef.current.length !== paramsRef.current.population) {
+ lastReset = resetRef.current;
+ buildSwarm();
+ }
+ const dt = Math.min(0.04, (now - lastTime) / 1000);
+ lastTime = now;
+ const active = paramsRef.current;
+ const swarm = swarmRef.current;
+ let meanSin = 0;
+ let meanCos = 0;
+ for (const fly of swarm) {
+ meanSin += Math.sin(fly.phase);
+ meanCos += Math.cos(fly.phase);
+ }
+ meanSin /= Math.max(1, swarm.length);
+ meanCos /= Math.max(1, swarm.length);
+ const order = Math.hypot(meanSin, meanCos);
+ const meanPhase = Math.atan2(meanSin, meanCos);
+
+ if (!pausedRef.current) {
+ for (const fly of swarm) {
+ const couplingForce = active.coupling * order * Math.sin(meanPhase - fly.phase);
+ const noiseForce = (Math.random() - 0.5) * active.noise * 2.4;
+ fly.phase += (fly.omega + couplingForce + noiseForce) * dt * active.speed * 3.1;
+ fly.x = (fly.x + fly.drift * active.speed + 1) % 1;
+ fly.y = clamp(fly.y + Math.sin(now * 0.00025 + fly.x * 8) * 0.000035, 0.03, 0.97);
+ }
+ }
+
+ context.fillStyle = '#02040a';
+ context.fillRect(0, 0, width, height);
+ const sky = context.createRadialGradient(width * 0.5, height * 0.55, 20, width * 0.5, height * 0.55, Math.max(width, height) * 0.7);
+ sky.addColorStop(0, `rgba(91, 33, 182, ${0.04 + order * 0.1})`);
+ sky.addColorStop(0.55, 'rgba(3, 17, 32, 0.25)');
+ sky.addColorStop(1, 'rgba(1, 2, 6, 0)');
+ context.fillStyle = sky;
+ context.fillRect(0, 0, width, height);
+
+ for (const fly of swarm) {
+ const flash = Math.pow(Math.max(0, Math.cos(fly.phase)), 18);
+ const x = fly.x * width;
+ const y = fly.y * height;
+ if (flash > 0.025) {
+ const glow = context.createRadialGradient(x, y, 0, x, y, 18 + flash * 28);
+ glow.addColorStop(0, `rgba(236, 253, 120, ${0.92 * flash})`);
+ glow.addColorStop(0.2, `rgba(125, 249, 255, ${0.7 * flash})`);
+ glow.addColorStop(1, 'rgba(125, 249, 255, 0)');
+ context.fillStyle = glow;
+ context.beginPath();
+ context.arc(x, y, 18 + flash * 28, 0, Math.PI * 2);
+ context.fill();
+ }
+ context.fillStyle = `rgba(224, 255, 170, ${0.15 + flash * 0.85})`;
+ context.beginPath();
+ context.arc(x, y, fly.size + flash * 2.2, 0, Math.PI * 2);
+ context.fill();
+ }
+
+ if (now - lastScore > 220) {
+ lastScore = now;
+ setSync(Math.round(order * 100));
+ }
+ frameId = requestAnimationFrame(render);
+ }
+
+ frameId = requestAnimationFrame(render);
+ return () => {
+ observer.disconnect();
+ cancelAnimationFrame(frameId);
+ };
+ }, []);
+
+ function patch(key, value) {
+ setParams((current) => ({ ...current, [key]: value }));
+ setNotice('The swarm changed. Find the smallest coupling that still creates one flash.');
+ }
+
+ function reset() {
+ resetRef.current += 1;
+ setNotice('New fireflies, same rules. Collective order has to emerge again.');
+ }
+
+ function nudge() {
+ const target = Math.random() * Math.PI * 2;
+ for (const fly of swarmRef.current) fly.phase = target + (Math.random() - 0.5) * 1.5;
+ setNotice('The swarm received a pulse. Watch whether the shared rhythm survives.');
+ }
+
+ const state = [params.coupling, params.noise, params.population, params.speed].map((value) => round(value, 3)).join(',');
+
+ return (
+
+
+
+
Live experiment 002
+
Can a crowd agree without a leader?
+
Each firefly follows only its own clock and the faint rhythm of its neighbours. Synchrony emerges—or collapses—from those local rules.
+
+
Synchrony {sync} {sync > 88 ? 'One living pulse' : sync > 55 ? 'Locking together' : 'Independent clocks'}
+
+
+
+
+
+
dθᵢ/dt = ωᵢ + K r sin(ψ−θᵢ) + noise
+
Pulse the swarm
+
+
+
+ {FIREFLY_PRESETS.map((preset) => { setParams(preset); resetRef.current += 1; }}>{preset.label} )}
+
+ patch('coupling', value)} />
+ patch('noise', value)} />
+ patch('population', Math.round(value))} />
+ patch('speed', value)} />
+ Challenge Reach 90 synchrony with the highest noise you can keep alive.
+
+
+ setPaused((value) => !value)} onReset={reset}
+ onShare={() => shareExperiment({ lab: 'fireflies', state, title: `My firefly sync score: ${sync}`, text: `I reached ${sync}% synchrony in GaugeGap. Can you keep the swarm together with more noise?`, onNotice: setNotice })}
+ onCopy={() => copyExperiment({ lab: 'fireflies', state, onNotice: setNotice })} notice={notice} />
+
+ );
+}
+
+const WAVE_PRESETS = [
+ { id: 'constructive', label: 'Perfect harmony', wavelength: 34, spacing: 120, phase: 0, speed: 1 },
+ { id: 'cancel', label: 'Total cancellation', wavelength: 34, spacing: 120, phase: Math.PI, speed: 1 },
+ { id: 'tight', label: 'Fine ripples', wavelength: 18, spacing: 88, phase: 0.7, speed: 1.25 },
+ { id: 'ocean', label: 'Deep ocean', wavelength: 52, spacing: 160, phase: 1.4, speed: 0.62 },
+];
+
+export function WaveInterferenceLab() {
+ const shared = useMemo(() => parseState('waves', 4), []);
+ const [params, setParams] = useState(() => shared ? {
+ wavelength: clamp(shared[0], 12, 64),
+ spacing: clamp(shared[1], 30, 190),
+ phase: clamp(shared[2], 0, Math.PI * 2),
+ speed: clamp(shared[3], 0.2, 2),
+ } : WAVE_PRESETS[0]);
+ const [paused, setPaused] = useState(false);
+ const [detector, setDetector] = useState({ x: 0.5, y: 0.5, intensity: 0 });
+ const detectorRef = useRef({ x: 0.5, y: 0.5, intensity: 0 });
+ const [notice, setNotice] = useState(shared ? 'Shared wave field loaded. Move the detector and remix it.' : 'Move through the field and find where two waves disappear.');
+ const canvasRef = useRef(null);
+ const paramsRef = useRef(params);
+ const pausedRef = useRef(paused);
+ const timeRef = useRef(0);
+ const resetRef = useRef(0);
+
+ useEffect(() => { paramsRef.current = params; }, [params]);
+ useEffect(() => { pausedRef.current = paused; }, [paused]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return undefined;
+ const context = canvas.getContext('2d', { alpha: false });
+ const width = 180;
+ const height = 112;
+ canvas.width = width;
+ canvas.height = height;
+ const image = context.createImageData(width, height);
+ let frameId = 0;
+ let last = performance.now();
+ let lastReset = -1;
+
+ function intensityAt(nx, ny, active) {
+ const px = nx * width;
+ const py = ny * height;
+ const s1x = width * 0.5 - active.spacing * 0.22;
+ const s2x = width * 0.5 + active.spacing * 0.22;
+ const sy = height * 0.5;
+ const r1 = Math.hypot(px - s1x, py - sy);
+ const r2 = Math.hypot(px - s2x, py - sy);
+ const k = (Math.PI * 2) / active.wavelength;
+ return (Math.sin(k * r1 - timeRef.current) + Math.sin(k * r2 - timeRef.current + active.phase)) * 0.5;
+ }
+
+ function render(now) {
+ if (lastReset !== resetRef.current) {
+ lastReset = resetRef.current;
+ timeRef.current = 0;
+ }
+ const dt = Math.min(0.05, (now - last) / 1000);
+ last = now;
+ const active = paramsRef.current;
+ if (!pausedRef.current) timeRef.current += dt * active.speed * 4.2;
+
+ const data = image.data;
+ let offset = 0;
+ for (let y = 0; y < height; y += 1) {
+ for (let x = 0; x < width; x += 1) {
+ const value = intensityAt(x / width, y / height, active);
+ const power = Math.abs(value);
+ const positive = value > 0;
+ data[offset] = positive ? 40 + power * 100 : 16 + power * 210;
+ data[offset + 1] = positive ? 110 + power * 145 : 20 + power * 45;
+ data[offset + 2] = positive ? 150 + power * 105 : 100 + power * 155;
+ data[offset + 3] = 255;
+ offset += 4;
+ }
+ }
+ context.putImageData(image, 0, 0);
+
+ const activeDetector = detectorRef.current;
+ const dx = activeDetector.x * width;
+ const dy = activeDetector.y * height;
+ context.strokeStyle = '#ffffff';
+ context.lineWidth = 0.8;
+ context.beginPath();
+ context.arc(dx, dy, 4.5, 0, Math.PI * 2);
+ context.stroke();
+ context.beginPath();
+ context.moveTo(dx - 7, dy);
+ context.lineTo(dx + 7, dy);
+ context.moveTo(dx, dy - 7);
+ context.lineTo(dx, dy + 7);
+ context.stroke();
+
+ frameId = requestAnimationFrame(render);
+ }
+
+ frameId = requestAnimationFrame(render);
+ return () => cancelAnimationFrame(frameId);
+ }, []);
+
+ function moveDetector(event) {
+ const rect = event.currentTarget.getBoundingClientRect();
+ const x = clamp((event.clientX - rect.left) / rect.width, 0, 1);
+ const y = clamp((event.clientY - rect.top) / rect.height, 0, 1);
+ const active = paramsRef.current;
+ const width = 180;
+ const height = 112;
+ const px = x * width;
+ const py = y * height;
+ const s1x = width * 0.5 - active.spacing * 0.22;
+ const s2x = width * 0.5 + active.spacing * 0.22;
+ const sy = height * 0.5;
+ const k = (Math.PI * 2) / active.wavelength;
+ const value = (Math.sin(k * Math.hypot(px - s1x, py - sy) - timeRef.current)
+ + Math.sin(k * Math.hypot(px - s2x, py - sy) - timeRef.current + active.phase)) * 0.5;
+ const nextDetector = { x, y, intensity: value };
+ detectorRef.current = nextDetector;
+ setDetector(nextDetector);
+ }
+
+ function patch(key, value) {
+ setParams((current) => ({ ...current, [key]: value }));
+ setNotice('The interference map changed instantly. Hunt for a silent node.');
+ }
+
+ const visibility = Math.round(Math.abs(detector.intensity) * 100);
+ const state = [params.wavelength, params.spacing, params.phase, params.speed].map((value) => round(value, 3)).join(',');
+
+ return (
+
+
+
+
Live experiment 003
+
Make two waves erase each other.
+
Two sources fill the same space. Their peaks can amplify into brilliance—or meet a trough and vanish.
+
+
Detector energy {visibility} {visibility < 8 ? 'Near-perfect silence' : visibility > 85 ? 'Maximum reinforcement' : 'Mixed interference'}
+
+
+
+
+
+
I = sin(kr₁−ωt) + sin(kr₂−ωt+φ)
+
Move the detector through the field
+
+
+
+ setPaused((value) => !value)} onReset={() => { resetRef.current += 1; setNotice('Time reset. The geometry stays the same.'); }}
+ onShare={() => shareExperiment({ lab: 'waves', state, title: `I found a ${visibility}% wave node`, text: 'I built a two-source interference field in GaugeGap. Can you find a quieter point?', onNotice: setNotice })}
+ onCopy={() => copyExperiment({ lab: 'waves', state, onNotice: setNotice })} notice={notice} />
+
+ );
+}
+
+const REACTION_PRESETS = [
+ { id: 'coral', label: 'Electric coral', feed: 0.0545, kill: 0.062, speed: 4, brush: 6 },
+ { id: 'mitosis', label: 'Cell division', feed: 0.0367, kill: 0.0649, speed: 5, brush: 5 },
+ { id: 'maze', label: 'Living maze', feed: 0.029, kill: 0.057, speed: 5, brush: 7 },
+ { id: 'worms', label: 'Neon worms', feed: 0.078, kill: 0.061, speed: 4, brush: 4 },
+];
+
+export function ReactionDiffusionLab() {
+ const shared = useMemo(() => parseState('reaction', 4), []);
+ const [params, setParams] = useState(() => shared ? {
+ feed: clamp(shared[0], 0.01, 0.09),
+ kill: clamp(shared[1], 0.045, 0.075),
+ speed: Math.round(clamp(shared[2], 1, 8)),
+ brush: Math.round(clamp(shared[3], 2, 12)),
+ } : REACTION_PRESETS[0]);
+ const [paused, setPaused] = useState(false);
+ const [complexity, setComplexity] = useState(0);
+ const [notice, setNotice] = useState(shared ? 'Shared chemistry loaded. Draw into it and force a new species.' : 'Drag across the field. A tiny chemical seed becomes a living pattern.');
+ const canvasRef = useRef(null);
+ const paramsRef = useRef(params);
+ const pausedRef = useRef(paused);
+ const fieldRef = useRef(null);
+ const resetRef = useRef(0);
+ const drawingRef = useRef(false);
+
+ useEffect(() => { paramsRef.current = params; }, [params]);
+ useEffect(() => { pausedRef.current = paused; }, [paused]);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return undefined;
+ const context = canvas.getContext('2d', { alpha: false });
+ const width = 144;
+ const height = 92;
+ canvas.width = width;
+ canvas.height = height;
+ let frameId = 0;
+ let lastReset = -1;
+ let lastScore = 0;
+ const image = context.createImageData(width, height);
+
+ function initialize() {
+ const size = width * height;
+ const a = new Float32Array(size);
+ const b = new Float32Array(size);
+ const nextA = new Float32Array(size);
+ const nextB = new Float32Array(size);
+ a.fill(1);
+ for (let seed = 0; seed < 8; seed += 1) {
+ const cx = Math.floor(width * (0.2 + Math.random() * 0.6));
+ const cy = Math.floor(height * (0.2 + Math.random() * 0.6));
+ for (let oy = -4; oy <= 4; oy += 1) {
+ for (let ox = -4; ox <= 4; ox += 1) {
+ const x = (cx + ox + width) % width;
+ const y = (cy + oy + height) % height;
+ b[x + y * width] = 0.82 + Math.random() * 0.18;
+ }
+ }
+ }
+ fieldRef.current = { a, b, nextA, nextB };
+ }
+
+ function laplacian(field, x, y) {
+ const left = (x - 1 + width) % width;
+ const right = (x + 1) % width;
+ const up = (y - 1 + height) % height;
+ const down = (y + 1) % height;
+ return field[left + y * width] * 0.2
+ + field[right + y * width] * 0.2
+ + field[x + up * width] * 0.2
+ + field[x + down * width] * 0.2
+ + field[left + up * width] * 0.05
+ + field[right + up * width] * 0.05
+ + field[left + down * width] * 0.05
+ + field[right + down * width] * 0.05
+ - field[x + y * width];
+ }
+
+ function step() {
+ const field = fieldRef.current;
+ if (!field) return;
+ const active = paramsRef.current;
+ const da = 1;
+ const db = 0.5;
+ for (let y = 0; y < height; y += 1) {
+ for (let x = 0; x < width; x += 1) {
+ const index = x + y * width;
+ const av = field.a[index];
+ const bv = field.b[index];
+ const reaction = av * bv * bv;
+ field.nextA[index] = clamp(av + (da * laplacian(field.a, x, y) - reaction + active.feed * (1 - av)), 0, 1);
+ field.nextB[index] = clamp(bv + (db * laplacian(field.b, x, y) + reaction - (active.kill + active.feed) * bv), 0, 1);
+ }
+ }
+ [field.a, field.nextA] = [field.nextA, field.a];
+ [field.b, field.nextB] = [field.nextB, field.b];
+ }
+
+ initialize();
+
+ function render(now) {
+ if (lastReset !== resetRef.current) {
+ lastReset = resetRef.current;
+ initialize();
+ }
+ if (!pausedRef.current) {
+ for (let pass = 0; pass < paramsRef.current.speed; pass += 1) step();
+ }
+ const field = fieldRef.current;
+ const data = image.data;
+ let activeCells = 0;
+ for (let index = 0; index < field.b.length; index += 1) {
+ const value = clamp(field.a[index] - field.b[index], 0, 1);
+ const chemical = field.b[index];
+ if (chemical > 0.18 && chemical < 0.82) activeCells += 1;
+ const offset = index * 4;
+ data[offset] = 10 + (1 - value) * 210;
+ data[offset + 1] = 18 + chemical * 235;
+ data[offset + 2] = 42 + value * 195;
+ data[offset + 3] = 255;
+ }
+ context.putImageData(image, 0, 0);
+ if (now - lastScore > 260) {
+ lastScore = now;
+ setComplexity(Math.round((activeCells / field.b.length) * 180));
+ }
+ frameId = requestAnimationFrame(render);
+ }
+
+ frameId = requestAnimationFrame(render);
+ return () => cancelAnimationFrame(frameId);
+ }, []);
+
+ function paint(event) {
+ if (!drawingRef.current && event.type === 'pointermove') return;
+ const rect = event.currentTarget.getBoundingClientRect();
+ const width = 144;
+ const height = 92;
+ const cx = Math.floor(clamp((event.clientX - rect.left) / rect.width, 0, 0.999) * width);
+ const cy = Math.floor(clamp((event.clientY - rect.top) / rect.height, 0, 0.999) * height);
+ const radius = paramsRef.current.brush;
+ const field = fieldRef.current;
+ if (!field) return;
+ for (let oy = -radius; oy <= radius; oy += 1) {
+ for (let ox = -radius; ox <= radius; ox += 1) {
+ if (ox * ox + oy * oy > radius * radius) continue;
+ const x = (cx + ox + width) % width;
+ const y = (cy + oy + height) % height;
+ field.b[x + y * width] = 1;
+ field.a[x + y * width] = 0.15;
+ }
+ }
+ }
+
+ function patch(key, value) {
+ setParams((current) => ({ ...current, [key]: value }));
+ setNotice(key === 'brush' ? 'Brush resized. Draw a new chemical seed.' : 'Reaction rules changed. The old pattern must adapt or die.');
+ }
+
+ const state = [params.feed, params.kill, params.speed, params.brush].map((value) => round(value, 4)).join(',');
+
+ return (
+
+
+
+
Live experiment 004
+
Draw a creature from two chemicals.
+
No image is stored here. Spots, worms, cells and coral continuously build themselves from reaction and diffusion.
+
+
Pattern complexity {complexity} {complexity > 70 ? 'Dense living texture' : complexity > 35 ? 'Structures emerging' : 'Chemistry settling'}
+
+
+
+
+
{ drawingRef.current = true; event.currentTarget.setPointerCapture?.(event.pointerId); paint(event); }}
+ onPointerMove={paint}
+ onPointerUp={(event) => { drawingRef.current = false; event.currentTarget.releasePointerCapture?.(event.pointerId); }}
+ onPointerCancel={() => { drawingRef.current = false; }}
+ aria-label="Interactive Gray-Scott reaction diffusion simulation" />
+ ∂A/∂t = Dₐ∇²A − AB² + f(1−A)
+ Draw directly into the chemistry
+
+
+
+ {REACTION_PRESETS.map((preset) => { setParams(preset); resetRef.current += 1; }}>{preset.label} )}
+
+ patch('feed', value)} />
+ patch('kill', value)} />
+ patch('speed', Math.round(value))} />
+ patch('brush', Math.round(value))} />
+ Challenge Create a stable pattern above 65 complexity without using a preset.
+
+
+ setPaused((value) => !value)} onReset={() => { resetRef.current += 1; setNotice('Fresh chemistry seeded. The pattern starts from nothing again.'); }}
+ onShare={() => shareExperiment({ lab: 'reaction', state, title: `My living chemistry scored ${complexity}`, text: 'I grew a reaction-diffusion pattern in GaugeGap. Can you make a stranger species?', onNotice: setNotice })}
+ onCopy={() => copyExperiment({ lab: 'reaction', state, onNotice: setNotice })} notice={notice} />
+
+ );
+}
diff --git a/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx b/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx
new file mode 100644
index 0000000..713bee1
--- /dev/null
+++ b/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx
@@ -0,0 +1,143 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Activity, Dna, Gauge, Shuffle, Sparkles, Waves } from 'lucide-react';
+import { AttractorPlayground } from './AttractorPlayground.jsx';
+import { FireflySyncLab, ReactionDiffusionLab, WaveInterferenceLab } from './CollectivePlaygrounds.jsx';
+import '../../styles/arcade.css';
+
+const EXPERIMENTS = [
+ {
+ id: 'attractor',
+ number: '001',
+ title: 'Butterfly Effect',
+ short: 'Shape chaos',
+ description: 'Tune a Lorenz system until order and turbulence balance on the same orbit.',
+ icon: Gauge,
+ accent: 'cyan',
+ mechanic: 'Tune',
+ },
+ {
+ id: 'fireflies',
+ number: '002',
+ title: 'Firefly Sync',
+ short: 'Create collective rhythm',
+ description: 'Give hundreds of independent clocks one weak connection and watch a shared pulse emerge.',
+ icon: Sparkles,
+ accent: 'lime',
+ mechanic: 'Synchronize',
+ },
+ {
+ id: 'waves',
+ number: '003',
+ title: 'Wave Eraser',
+ short: 'Find perfect silence',
+ description: 'Move through two overlapping waves and hunt for the places where energy disappears.',
+ icon: Waves,
+ accent: 'violet',
+ mechanic: 'Explore',
+ },
+ {
+ id: 'reaction',
+ number: '004',
+ title: 'Living Chemistry',
+ short: 'Draw a new species',
+ description: 'Paint two virtual chemicals and grow coral, cells, worms and mazes from local reactions.',
+ icon: Dna,
+ accent: 'pink',
+ mechanic: 'Create',
+ },
+];
+
+function initialExperiment() {
+ if (typeof window === 'undefined') return 'attractor';
+ const requested = new URLSearchParams(window.location.search).get('lab');
+ return EXPERIMENTS.some((experiment) => experiment.id === requested) ? requested : 'attractor';
+}
+
+export function ExperimentArcade() {
+ const [active, setActive] = useState(initialExperiment);
+ const activeExperiment = useMemo(() => EXPERIMENTS.find((experiment) => experiment.id === active) || EXPERIMENTS[0], [active]);
+
+ useEffect(() => {
+ function selectFromEvent(event) {
+ const next = event.detail?.lab;
+ if (EXPERIMENTS.some((experiment) => experiment.id === next)) setActive(next);
+ }
+ window.addEventListener('gaugegap:lab', selectFromEvent);
+ return () => window.removeEventListener('gaugegap:lab', selectFromEvent);
+ }, []);
+
+ function selectExperiment(id, scroll = true) {
+ setActive(id);
+ const url = new URL(window.location.href);
+ url.searchParams.set('lab', id);
+ if (id !== 'attractor') url.searchParams.delete('run');
+ url.searchParams.delete('state');
+ url.hash = 'playground';
+ window.history.replaceState({}, '', url);
+ if (scroll) {
+ window.requestAnimationFrame(() => document.getElementById('playground')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
+ }
+ }
+
+ function surpriseMe() {
+ const available = EXPERIMENTS.filter((experiment) => experiment.id !== active);
+ selectExperiment(available[Math.floor(Math.random() * available.length)].id);
+ }
+
+ return (
+
+
+
+
GaugeGap science arcade
+
Four experiments. Four different ways to play.
+
Tune chaos, synchronize a crowd, erase a wave or draw living chemistry. Every lab starts in seconds and hides the real model one layer underneath.
+
+
Surprise me
+
+
+
+ {EXPERIMENTS.map((experiment) => {
+ const Icon = experiment.icon;
+ const selected = experiment.id === active;
+ return (
+
selectExperiment(experiment.id, false)}
+ >
+ {experiment.number}
+
+ {experiment.title}
+ {experiment.short}
+ {experiment.description}
+ {experiment.mechanic}
+
+ );
+ })}
+
+
+
+
+ Experiment {activeExperiment.number} loaded
+ {activeExperiment.title}
+
+ {active === 'attractor' ?
: null}
+ {active === 'fireflies' ?
: null}
+ {active === 'waves' ?
: null}
+ {active === 'reaction' ?
: null}
+
+
+
+ What we borrowed from the best interactive-science sites
+ Immediate motion
+ Recognizable presets
+ One clear challenge
+ Different interaction styles
+ A shareable result
+
+
+ );
+}
diff --git a/brainsnn-r3f-app/src/styles/arcade.css b/brainsnn-r3f-app/src/styles/arcade.css
new file mode 100644
index 0000000..b364dbb
--- /dev/null
+++ b/brainsnn-r3f-app/src/styles/arcade.css
@@ -0,0 +1,530 @@
+.gg-arcade {
+ position: relative;
+ z-index: 1;
+ width: min(100% - 36px, 1320px);
+ margin: 0 auto;
+ padding: clamp(76px, 9vw, 128px) 0 40px;
+ scroll-margin-top: 20px;
+}
+
+.gg-arcade-intro,
+.gg-sim-heading,
+.gg-arcade-toolbar,
+.gg-arcade-stage-topline {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+}
+
+.gg-arcade-intro { margin-bottom: 28px; }
+
+.gg-arcade-intro h2,
+.gg-sim-heading h2 {
+ margin: 10px 0 10px;
+ font-family: var(--bsn-font-display);
+ letter-spacing: -0.035em;
+ line-height: 1;
+}
+
+.gg-arcade-intro h2 {
+ max-width: 760px;
+ font-size: clamp(2.45rem, 5vw, 4.9rem);
+}
+
+.gg-arcade-intro > div > p:last-child,
+.gg-sim-heading > div > p:last-child {
+ max-width: 760px;
+ margin: 0;
+ color: var(--bsn-text-secondary);
+ font-size: 1.04rem;
+ line-height: 1.65;
+}
+
+.gg-surprise-button {
+ flex: 0 0 auto;
+ min-height: 48px;
+ display: inline-flex;
+ align-items: center;
+ gap: 9px;
+ padding: 0 18px;
+ border: 1px solid rgba(125, 249, 255, 0.24);
+ border-radius: 999px;
+ color: #dffcff;
+ background: rgba(125, 249, 255, 0.055);
+ font-weight: 850;
+ cursor: pointer;
+}
+
+.gg-surprise-button:hover {
+ border-color: rgba(125, 249, 255, 0.65);
+ background: rgba(125, 249, 255, 0.11);
+}
+
+.gg-arcade-selector {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 13px;
+ margin-bottom: 22px;
+}
+
+.gg-arcade-card {
+ --arcade-accent: #7df9ff;
+ position: relative;
+ min-width: 0;
+ min-height: 224px;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-template-rows: auto auto auto 1fr auto;
+ column-gap: 12px;
+ padding: 18px;
+ border: 1px solid rgba(255, 255, 255, 0.09);
+ border-radius: 20px;
+ overflow: hidden;
+ color: var(--bsn-text);
+ background: radial-gradient(circle at 95% 0%, rgba(125, 249, 255, 0.12), transparent 38%), rgba(10, 10, 20, 0.78);
+ text-align: left;
+ cursor: pointer;
+ transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
+}
+
+.gg-arcade-card::after {
+ content: "";
+ position: absolute;
+ inset: auto 14px 0;
+ height: 2px;
+ background: linear-gradient(90deg, transparent, var(--arcade-accent), transparent);
+ opacity: 0;
+ transition: opacity 180ms ease;
+}
+
+.gg-arcade-card:hover,
+.gg-arcade-card.active {
+ transform: translateY(-3px);
+ border-color: var(--arcade-accent);
+ background: radial-gradient(circle at 95% 0%, rgba(125, 249, 255, 0.2), transparent 42%), rgba(12, 12, 25, 0.94);
+}
+
+.gg-arcade-card.active::after { opacity: 1; }
+.gg-arcade-card-cyan { --arcade-accent: #7df9ff; }
+.gg-arcade-card-lime { --arcade-accent: #d9ff65; }
+.gg-arcade-card-violet { --arcade-accent: #a78bfa; }
+.gg-arcade-card-pink { --arcade-accent: #ff5edb; }
+
+.gg-arcade-card-number {
+ position: absolute;
+ top: 14px;
+ right: 15px;
+ color: rgba(255,255,255,0.32);
+ font-family: var(--bsn-font-mono);
+ font-size: 0.68rem;
+}
+
+.gg-arcade-card-icon {
+ grid-row: 1 / span 2;
+ width: 42px;
+ height: 42px;
+ display: grid;
+ place-items: center;
+ border: 1px solid var(--arcade-accent);
+ border-radius: 13px;
+ color: var(--arcade-accent);
+ background: rgba(125, 249, 255, 0.05);
+ box-shadow: 0 0 24px rgba(125, 249, 255, 0.08);
+}
+
+.gg-arcade-card strong {
+ align-self: end;
+ padding-right: 26px;
+ font-family: var(--bsn-font-display);
+ font-size: 1.12rem;
+}
+
+.gg-arcade-card small {
+ color: var(--arcade-accent);
+ font-family: var(--bsn-font-mono);
+ font-size: 0.69rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.gg-arcade-card p {
+ grid-column: 1 / -1;
+ margin: 18px 0 12px;
+ color: var(--bsn-text-secondary);
+ font-size: 0.88rem;
+ line-height: 1.5;
+}
+
+.gg-arcade-card em {
+ grid-column: 1 / -1;
+ align-self: end;
+ width: fit-content;
+ padding: 5px 9px;
+ border-radius: 999px;
+ color: #050509;
+ background: var(--arcade-accent);
+ font-family: var(--bsn-font-mono);
+ font-size: 0.67rem;
+ font-style: normal;
+ font-weight: 900;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.gg-arcade-stage {
+ position: relative;
+ padding: 12px;
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 28px;
+ background: radial-gradient(circle at 50% 0%, rgba(125, 249, 255, 0.06), transparent 38%), rgba(2, 3, 9, 0.86);
+ box-shadow: 0 32px 100px rgba(0,0,0,0.38);
+}
+
+.gg-arcade-stage-topline {
+ min-height: 42px;
+ padding: 0 10px 8px;
+ color: var(--bsn-text-muted);
+ font-family: var(--bsn-font-mono);
+ font-size: 0.7rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.gg-arcade-stage-topline span,
+.gg-arcade-stage-topline strong {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.gg-arcade-stage-topline i {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: #7df9ff;
+ box-shadow: 0 0 14px rgba(125,249,255,0.9);
+}
+
+.gg-arcade-stage-topline strong { color: #e7fbff; }
+
+.gg-arcade-stage .gg-playground {
+ width: 100%;
+ margin: 0;
+ padding: 18px 0 0;
+}
+
+.gg-arcade-stage .gg-playground-head,
+.gg-arcade-stage .gg-lab-frame,
+.gg-arcade-stage .gg-playground-actions { width: 100%; }
+
+.gg-arcade-lesson {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ flex-wrap: wrap;
+ padding: 24px 20px 0;
+ color: var(--bsn-text-muted);
+ font-size: 0.8rem;
+}
+
+.gg-arcade-lesson span {
+ color: var(--bsn-cyan);
+ font-family: var(--bsn-font-mono);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.gg-arcade-lesson strong {
+ padding: 7px 11px;
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 999px;
+ color: var(--bsn-text-secondary);
+ background: rgba(255,255,255,0.025);
+ font-weight: 750;
+}
+
+.gg-sim-lab {
+ padding: clamp(18px, 3vw, 34px);
+ border-radius: 22px;
+ background: radial-gradient(circle at 75% 0%, rgba(139, 92, 246, 0.08), transparent 34%), linear-gradient(180deg, rgba(10, 10, 22, 0.96), rgba(4, 5, 12, 0.98));
+}
+
+.gg-sim-heading {
+ align-items: flex-end;
+ margin-bottom: 24px;
+}
+
+.gg-sim-heading h2 { font-size: clamp(2rem, 4vw, 3.75rem); }
+
+.gg-sim-score {
+ flex: 0 0 180px;
+ padding: 16px 18px;
+ border: 1px solid rgba(125,249,255,0.16);
+ border-radius: 17px;
+ background: rgba(125,249,255,0.035);
+}
+
+.gg-sim-score span,
+.gg-sim-score small {
+ display: block;
+ color: var(--bsn-text-muted);
+ font-family: var(--bsn-font-mono);
+ font-size: 0.66rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.gg-sim-score strong {
+ display: block;
+ margin: 6px 0;
+ color: #e8ffff;
+ font-family: var(--bsn-font-mono);
+ font-size: 2.5rem;
+ line-height: 1;
+}
+
+.gg-sim-score small {
+ color: var(--bsn-cyan);
+ letter-spacing: 0;
+ text-transform: none;
+}
+
+.gg-sim-frame {
+ display: grid;
+ grid-template-columns: minmax(0, 1.55fr) minmax(270px, 0.55fr);
+ gap: 18px;
+}
+
+.gg-sim-canvas-wrap {
+ position: relative;
+ min-width: 0;
+ min-height: 560px;
+ overflow: hidden;
+ border: 1px solid rgba(125,249,255,0.13);
+ border-radius: 20px;
+ background: #02040a;
+ box-shadow: inset 0 0 65px rgba(0,0,0,0.75);
+}
+
+.gg-sim-canvas {
+ width: 100%;
+ height: 100%;
+ min-height: 560px;
+ display: block;
+ touch-action: none;
+}
+
+.gg-wave-canvas,
+.gg-pixel-canvas {
+ image-rendering: auto;
+ cursor: crosshair;
+}
+
+.gg-pixel-canvas { image-rendering: pixelated; }
+
+.gg-sim-canvas-label {
+ position: absolute;
+ left: 14px;
+ bottom: 13px;
+ max-width: calc(100% - 28px);
+ padding: 6px 9px;
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 8px;
+ color: rgba(220, 252, 255, 0.7);
+ background: rgba(2, 4, 10, 0.72);
+ backdrop-filter: blur(8px);
+ font-family: var(--bsn-font-mono);
+ font-size: 0.68rem;
+}
+
+.gg-sim-float-action,
+.gg-wave-hint {
+ position: absolute;
+ top: 14px;
+ right: 14px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 39px;
+ padding: 0 12px;
+ border: 1px solid rgba(217, 255, 101, 0.28);
+ border-radius: 999px;
+ color: #edffb3;
+ background: rgba(4, 9, 12, 0.72);
+ backdrop-filter: blur(8px);
+ font-size: 0.74rem;
+ font-weight: 850;
+}
+
+.gg-sim-float-action { cursor: pointer; }
+
+.gg-wave-hint {
+ left: 14px;
+ right: auto;
+ border-color: rgba(167, 139, 250, 0.3);
+ color: #ddd6fe;
+ pointer-events: none;
+}
+
+.gg-sim-controls {
+ display: grid;
+ align-content: start;
+ gap: 15px;
+ padding: 17px;
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 20px;
+ background: rgba(255,255,255,0.025);
+}
+
+.gg-sim-presets {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0,1fr));
+ gap: 8px;
+ padding-bottom: 14px;
+ border-bottom: 1px solid rgba(255,255,255,0.07);
+}
+
+.gg-sim-presets button {
+ min-height: 38px;
+ padding: 7px 9px;
+ border: 1px solid rgba(255,255,255,0.09);
+ border-radius: 9px;
+ color: var(--bsn-text-secondary);
+ background: rgba(255,255,255,0.025);
+ font-size: 0.72rem;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+.gg-sim-presets button:hover {
+ border-color: rgba(125,249,255,0.32);
+ color: #e7ffff;
+ background: rgba(125,249,255,0.06);
+}
+
+.gg-arcade-range { display: grid; gap: 9px; }
+
+.gg-arcade-range > span {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.gg-arcade-range strong,
+.gg-arcade-range output { font-size: 0.75rem; }
+.gg-arcade-range strong { color: var(--bsn-text-secondary); }
+.gg-arcade-range output { color: var(--bsn-cyan); font-family: var(--bsn-font-mono); }
+.gg-arcade-range input { width: 100%; accent-color: #7df9ff; }
+
+.gg-sim-mission {
+ margin-top: 4px;
+ padding: 13px;
+ border: 1px solid rgba(255, 94, 219, 0.16);
+ border-radius: 13px;
+ background: rgba(255, 94, 219, 0.035);
+}
+
+.gg-sim-mission strong {
+ color: #ff9ee9;
+ font-family: var(--bsn-font-mono);
+ font-size: 0.68rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.gg-sim-mission p {
+ margin: 7px 0 0;
+ color: var(--bsn-text-secondary);
+ font-size: 0.8rem;
+ line-height: 1.45;
+}
+
+.gg-arcade-toolbar {
+ margin-top: 17px;
+ padding-top: 16px;
+ border-top: 1px solid rgba(255,255,255,0.07);
+}
+
+.gg-arcade-toolbar-actions { display: flex; gap: 8px; flex-wrap: wrap; }
+
+.gg-arcade-toolbar-actions button {
+ min-height: 39px;
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 0 12px;
+ border: 1px solid rgba(255,255,255,0.09);
+ border-radius: 9px;
+ color: var(--bsn-text-secondary);
+ background: rgba(255,255,255,0.025);
+ font-size: 0.73rem;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+.gg-arcade-toolbar-actions button:hover {
+ border-color: rgba(125,249,255,0.3);
+ color: #e7ffff;
+}
+
+.gg-arcade-toolbar > p {
+ margin: 0;
+ max-width: 490px;
+ color: var(--bsn-text-muted);
+ font-size: 0.78rem;
+ line-height: 1.45;
+ text-align: right;
+}
+
+@media (max-width: 1050px) {
+ .gg-arcade-selector { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .gg-sim-frame { grid-template-columns: 1fr; }
+ .gg-sim-canvas-wrap,
+ .gg-sim-canvas { min-height: 500px; }
+ .gg-sim-controls { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .gg-sim-presets,
+ .gg-sim-mission { grid-column: 1 / -1; }
+}
+
+@media (max-width: 720px) {
+ .gg-arcade {
+ width: min(100% - 22px, 1320px);
+ padding-top: 68px;
+ }
+
+ .gg-arcade-intro,
+ .gg-sim-heading,
+ .gg-arcade-toolbar {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .gg-surprise-button { width: fit-content; }
+
+ .gg-arcade-selector {
+ display: flex;
+ overflow-x: auto;
+ scroll-snap-type: x mandatory;
+ padding: 0 1px 8px;
+ }
+
+ .gg-arcade-card {
+ flex: 0 0 min(78vw, 300px);
+ scroll-snap-align: start;
+ }
+
+ .gg-arcade-stage { padding: 7px; border-radius: 20px; }
+ .gg-arcade-stage-topline { padding-inline: 7px; }
+ .gg-sim-lab { padding: 18px 12px; }
+ .gg-sim-score { flex-basis: auto; }
+ .gg-sim-canvas-wrap,
+ .gg-sim-canvas { min-height: 410px; }
+ .gg-sim-controls { grid-template-columns: 1fr; padding: 14px; }
+ .gg-sim-presets,
+ .gg-sim-mission { grid-column: auto; }
+ .gg-arcade-toolbar > p { text-align: left; }
+ .gg-arcade-lesson { justify-content: flex-start; padding-inline: 6px; }
+}