diff --git a/public/nexus/app.js b/public/nexus/app.js
new file mode 100644
index 0000000..b30690f
--- /dev/null
+++ b/public/nexus/app.js
@@ -0,0 +1,2550 @@
+import * as THREE from 'three';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
+import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
+
+// ═══════════════════════════════════════════
+// NEXUS v1.1 — Portal System Update
+// ═══════════════════════════════════════════
+
+const NEXUS = {
+ colors: {
+ primary: 0x4af0c0,
+ secondary: 0x7b5cff,
+ bg: 0x050510,
+ panelBg: 0x0a0f28,
+ nebula1: 0x1a0a3e,
+ nebula2: 0x0a1a3e,
+ gold: 0xffd700,
+ danger: 0xff4466,
+ gridLine: 0x1a2a4a,
+ }
+};
+
+// ═══ STATE ═══
+let camera, scene, renderer, composer;
+let clock, playerPos, playerRot;
+let keys = {};
+let mouseDown = false;
+let batcaveTerminals = [];
+let portals = []; // Registry of active portals
+let visionPoints = []; // Registry of vision points
+let agents = []; // Registry of agent presences
+let activePortal = null; // Portal currently in proximity
+let activeVisionPoint = null; // Vision point currently in proximity
+let portalOverlayActive = false;
+let visionOverlayActive = false;
+let atlasOverlayActive = false;
+let thoughtStreamMesh;
+let harnessPulseMesh;
+let powerMeterBars = [];
+let particles, dustParticles, ambientLight;
+let debugOverlay;
+let frameCount = 0, lastFPSTime = 0, fps = 0;
+let chatOpen = true;
+let loadProgress = 0;
+let performanceTier = 'high';
+
+// ═══ HERMES WS STATE ═══
+let hermesWs = null;
+let wsReconnectTimer = null;
+let wsConnected = false;
+let recentToolOutputs = [];
+let workshopPanelCtx = null;
+let workshopPanelTexture = null;
+let workshopPanelCanvas = null;
+let workshopScanMat = null;
+let workshopPanelRefreshTimer = 0;
+let lastFocusedPortal = null;
+
+// ═══ NAVIGATION SYSTEM ═══
+const NAV_MODES = ['walk', 'orbit', 'fly'];
+let navModeIdx = 0;
+
+const orbitState = {
+ target: new THREE.Vector3(0, 2, 0),
+ radius: 14,
+ theta: Math.PI,
+ phi: Math.PI / 6,
+ minR: 3,
+ maxR: 40,
+ lastX: 0,
+ lastY: 0,
+};
+
+let flyY = 2;
+
+// ═══ SOVEREIGN SYMBOLIC ENGINE (GOFAI) ═══
+class SymbolicEngine {
+ constructor() {
+ this.facts = new Map();
+ this.rules = [];
+ this.reasoningLog = [];
+ }
+
+ addFact(key, value) {
+ this.facts.set(key, value);
+ }
+
+ addRule(condition, action, description) {
+ this.rules.push({ condition, action, description });
+ }
+
+ reason() {
+ this.rules.forEach(rule => {
+ if (rule.condition(this.facts)) {
+ const result = rule.action(this.facts);
+ if (result) {
+ this.logReasoning(rule.description, result);
+ }
+ }
+ });
+ }
+
+ logReasoning(ruleDesc, outcome) {
+ const entry = {
+ timestamp: Date.now(),
+ rule: ruleDesc,
+ outcome: outcome
+ };
+ this.reasoningLog.unshift(entry);
+ if (this.reasoningLog.length > 5) this.reasoningLog.pop();
+
+ // Update HUD if available
+ const container = document.getElementById('symbolic-log-content');
+ if (container) {
+ const logDiv = document.createElement('div');
+ logDiv.className = 'symbolic-log-entry';
+ logDiv.innerHTML = `[RULE] ${ruleDesc}→ ${outcome}`;
+ container.prepend(logDiv);
+ if (container.children.length > 5) container.lastElementChild.remove();
+ }
+ }
+}
+
+class AgentFSM {
+ constructor(agentId, initialState) {
+ this.agentId = agentId;
+ this.state = initialState;
+ this.transitions = {};
+ }
+
+ addTransition(fromState, toState, condition) {
+ if (!this.transitions[fromState]) this.transitions[fromState] = [];
+ this.transitions[fromState].push({ toState, condition });
+ }
+
+ update(facts) {
+ const possibleTransitions = this.transitions[this.state] || [];
+ for (const transition of possibleTransitions) {
+ if (transition.condition(facts)) {
+ console.log(`[FSM] Agent ${this.agentId} transitioning: ${this.state} -> ${transition.toState}`);
+ this.state = transition.toState;
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+// ═══ SOVEREIGN KNOWLEDGE GRAPH (SEMANTIC MEMORY) ═══
+class KnowledgeGraph {
+ constructor() {
+ this.nodes = new Map(); // id -> { data }
+ this.edges = []; // { from, to, relation }
+ }
+
+ addNode(id, type, metadata = {}) {
+ this.nodes.set(id, { id, type, ...metadata });
+ }
+
+ addEdge(from, to, relation) {
+ this.edges.push({ from, to, relation });
+ }
+
+ query(from, relation) {
+ return this.edges
+ .filter(e => e.from === from && e.relation === relation)
+ .map(e => this.nodes.get(e.to));
+ }
+
+ getRelated(id) {
+ return this.edges.filter(e => e.from === id || e.to === id);
+ }
+}
+
+// ═══ BLACKBOARD ARCHITECTURE (COLLABORATIVE INTELLIGENCE) ═══
+class Blackboard {
+ constructor() {
+ this.data = {};
+ this.subscribers = [];
+ }
+
+ write(key, value, source) {
+ const oldValue = this.data[key];
+ this.data[key] = value;
+ this.notify(key, value, oldValue, source);
+ }
+
+ read(key) {
+ return this.data[key];
+ }
+
+ subscribe(callback) {
+ this.subscribers.push(callback);
+ }
+
+ notify(key, value, oldValue, source) {
+ this.subscribers.forEach(sub => sub(key, value, oldValue, source));
+
+ // Log to HUD
+ const container = document.getElementById('blackboard-log-content');
+ if (container) {
+ const entry = document.createElement('div');
+ entry.className = 'blackboard-entry';
+ entry.innerHTML = `[${source}] ${key}: ${JSON.stringify(value)}`;
+ container.prepend(entry);
+ if (container.children.length > 8) container.lastElementChild.remove();
+ }
+ }
+}
+
+// ═══ SYMBOLIC PLANNER (STRIPS-LIKE) ═══
+class SymbolicPlanner {
+ constructor() {
+ this.actions = [];
+ this.currentPlan = [];
+ }
+
+ addAction(name, preconditions, effects) {
+ this.actions.push({ name, preconditions, effects });
+ }
+
+ findPlan(initialState, goalState) {
+ // Simple BFS for planning (for small state spaces)
+ let queue = [[initialState, []]];
+ let visited = new Set([JSON.stringify(initialState)]);
+
+ while (queue.length > 0) {
+ let [state, plan] = queue.shift();
+
+ if (this.isGoalReached(state, goalState)) {
+ return plan;
+ }
+
+ for (let action of this.actions) {
+ if (this.arePreconditionsMet(state, action.preconditions)) {
+ let nextState = { ...state, ...action.effects };
+ let stateStr = JSON.stringify(nextState);
+ if (!visited.has(stateStr)) {
+ visited.add(stateStr);
+ queue.push([nextState, [...plan, action.name]]);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ isGoalReached(state, goal) {
+ for (let key in goal) {
+ if (state[key] !== goal[key]) return false;
+ }
+ return true;
+ }
+
+ arePreconditionsMet(state, preconditions) {
+ for (let key in preconditions) {
+ if (state[key] < preconditions[key]) return false;
+ }
+ return true;
+ }
+
+ logPlan(plan) {
+ this.currentPlan = plan;
+ const container = document.getElementById('planner-log-content');
+ if (container) {
+ container.innerHTML = '';
+ if (!plan || plan.length === 0) {
+ container.innerHTML = '
NO ACTIVE PLAN
';
+ return;
+ }
+ plan.forEach((step, i) => {
+ const div = document.createElement('div');
+ div.className = 'planner-step';
+ div.innerHTML = `${i+1}. ${step}`;
+ container.appendChild(div);
+ });
+ }
+ }
+}
+
+// ═══ FUZZY LOGIC (HANDLING UNCERTAINTY) ═══
+class FuzzyLogic {
+ static getMembership(value, low, mid, high) {
+ if (value <= low) return 0;
+ if (value >= high) return 1;
+ if (value <= mid) return (value - low) / (mid - low);
+ return 1 - (value - mid) / (high - mid);
+ }
+
+ static isLow(value) { return this.getMembership(value, 0, 20, 40); }
+ static isMedium(value) { return this.getMembership(value, 30, 50, 70); }
+ static isHigh(value) { return this.getMembership(value, 60, 80, 100); }
+}
+
+// ═══ CASE-BASED REASONER (LEARNING FROM EXPERIENCE) ═══
+class CaseBasedReasoner {
+ constructor() {
+ this.caseLibrary = [];
+ }
+
+ addCase(situation, action, outcome) {
+ this.caseLibrary.push({ situation, action, outcome, timestamp: Date.now() });
+ }
+
+ findSimilarCase(currentSituation) {
+ let bestMatch = null;
+ let maxSimilarity = -1;
+
+ this.caseLibrary.forEach(c => {
+ let similarity = this.calculateSimilarity(currentSituation, c.situation);
+ if (similarity > maxSimilarity) {
+ maxSimilarity = similarity;
+ bestMatch = c;
+ }
+ });
+
+ return maxSimilarity > 0.7 ? bestMatch : null;
+ }
+
+ calculateSimilarity(s1, s2) {
+ let score = 0;
+ let total = 0;
+ for (let key in s1) {
+ if (s2[key] !== undefined) {
+ score += 1 - Math.abs(s1[key] - s2[key]);
+ total += 1;
+ }
+ }
+ return total > 0 ? score / total : 0;
+ }
+
+ logCase(c) {
+ const container = document.getElementById('cbr-log-content');
+ if (container) {
+ const div = document.createElement('div');
+ div.className = 'cbr-entry';
+ div.innerHTML = `
+ SIMILAR CASE FOUND (${(this.calculateSimilarity(symbolicEngine.facts, c.situation) * 100).toFixed(0)}%)
+ SUGGESTED: ${c.action}
+ PREVIOUS OUTCOME: ${c.outcome}
+ `;
+ container.prepend(div);
+ if (container.children.length > 3) container.lastElementChild.remove();
+ }
+ }
+}
+
+let cbr;
+let symbolicPlanner;
+let knowledgeGraph;
+let blackboard;
+let symbolicEngine;
+let agentFSMs = {};
+
+// ═══ INIT ═══
+async function init() {
+ clock = new THREE.Clock();
+ playerPos = new THREE.Vector3(0, 2, 12);
+ playerRot = new THREE.Euler(0, 0, 0, 'YXZ');
+
+ // Initialize GOFAI Stack
+ knowledgeGraph = new KnowledgeGraph();
+ blackboard = new Blackboard();
+ symbolicEngine = new SymbolicEngine();
+ symbolicPlanner = new SymbolicPlanner();
+ cbr = new CaseBasedReasoner();
+
+ setupKnowledgeBase();
+ setupSymbolicRules();
+ setupPlannerActions();
+ setupCaseLibrary();
+
+ const canvas = document.getElementById('nexus-canvas');
+ renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
+ renderer.toneMappingExposure = 1.2;
+ renderer.shadowMap.enabled = true;
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+
+ performanceTier = detectPerformanceTier();
+ updateLoad(10);
+
+ scene = new THREE.Scene();
+ scene.fog = new THREE.FogExp2(0x050510, 0.012);
+
+ camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
+ camera.position.copy(playerPos);
+
+ updateLoad(20);
+
+ createSkybox();
+ updateLoad(30);
+ createLighting();
+ updateLoad(40);
+ createFloor();
+ updateLoad(50);
+ createBatcaveTerminal();
+ updateLoad(60);
+
+ // Load Portals from Registry
+ try {
+ const response = await fetch('./portals.json');
+ const portalData = await response.json();
+ createPortals(portalData);
+ } catch (e) {
+ console.error('Failed to load portals.json:', e);
+ addChatMessage('error', 'Portal registry offline. Check logs.');
+ }
+
+ // Load Vision Points
+ try {
+ const response = await fetch('./vision.json');
+ const visionData = await response.json();
+ createVisionPoints(visionData);
+ } catch (e) {
+ console.error('Failed to load vision.json:', e);
+ }
+
+ updateLoad(80);
+ createParticles();
+ createDustParticles();
+ updateLoad(85);
+ createAmbientStructures();
+ createAgentPresences();
+ createThoughtStream();
+ createHarnessPulse();
+ createSessionPowerMeter();
+ createWorkshopTerminal();
+ createAshStorm();
+ updateLoad(90);
+
+ loadSession();
+ connectHermes();
+ fetchGiteaData();
+ setInterval(fetchGiteaData, 30000); // Refresh every 30s
+
+ composer = new EffectComposer(renderer);
+ composer.addPass(new RenderPass(scene, camera));
+ const bloom = new UnrealBloomPass(
+ new THREE.Vector2(window.innerWidth, window.innerHeight),
+ 0.6, 0.4, 0.85
+ );
+ composer.addPass(bloom);
+ composer.addPass(new SMAAPass(window.innerWidth, window.innerHeight));
+
+ updateLoad(95);
+
+ setupControls();
+ window.addEventListener('resize', onResize);
+ debugOverlay = document.getElementById('debug-overlay');
+
+ updateLoad(100);
+
+ setTimeout(() => {
+ document.getElementById('loading-screen').classList.add('fade-out');
+ const enterPrompt = document.getElementById('enter-prompt');
+ enterPrompt.style.display = 'flex';
+
+ enterPrompt.addEventListener('click', () => {
+ enterPrompt.classList.add('fade-out');
+ document.getElementById('hud').style.display = 'block';
+ setTimeout(() => { enterPrompt.remove(); }, 600);
+ }, { once: true });
+
+ setTimeout(() => { document.getElementById('loading-screen').remove(); }, 900);
+ }, 600);
+
+ requestAnimationFrame(gameLoop);
+}
+
+function updateLoad(pct) {
+ loadProgress = pct;
+ const fill = document.getElementById('load-progress');
+ if (fill) fill.style.width = pct + '%';
+}
+
+function setupKnowledgeBase() {
+ // Define Core Entities
+ knowledgeGraph.addNode('nexus', 'location', { name: 'The Nexus Core' });
+ knowledgeGraph.addNode('timmy', 'agent', { name: 'Timmy' });
+ knowledgeGraph.addNode('evonia', 'layer', { name: 'Evonia System' });
+
+ // Define Relationships
+ knowledgeGraph.addEdge('timmy', 'nexus', 'is_at');
+ knowledgeGraph.addEdge('nexus', 'evonia', 'monitored_by');
+
+ // Initialize Blackboard with system defaults
+ blackboard.write('system_status', 'INITIALIZING', 'SYSTEM');
+ blackboard.write('threat_level', 0, 'SYSTEM');
+}
+
+function setupSymbolicRules() {
+ // Facts: Energy, Stability, Portal Status
+ symbolicEngine.addFact('energy', 100);
+ symbolicEngine.addFact('stability', 1.0);
+ symbolicEngine.addFact('activePortals', 0);
+
+ // Rule: Low Energy Recovery
+ symbolicEngine.addRule(
+ (facts) => facts.get('energy') < 20,
+ (facts) => {
+ facts.set('mode', 'RECOVERY');
+ return 'Diverting power to core systems';
+ },
+ 'Low Energy Protocol'
+ );
+
+ // Rule: Stability Alert
+ symbolicEngine.addRule(
+ (facts) => facts.get('stability') < 0.5,
+ (facts) => {
+ facts.set('alert', 'CRITICAL');
+ return 'Initiating matrix stabilization';
+ },
+ 'Stability Safeguard'
+ );
+
+ // FSMs for Agents
+ agentFSMs['timmy'] = new AgentFSM('timmy', 'IDLE');
+ agentFSMs['timmy'].addTransition('IDLE', 'ANALYZING', (facts) => facts.get('activePortals') > 0);
+ agentFSMs['timmy'].addTransition('ANALYZING', 'IDLE', (facts) => facts.get('activePortals') === 0);
+ agentFSMs['timmy'].addTransition('IDLE', 'ALERT', (facts) => facts.get('stability') < 0.7);
+ agentFSMs['timmy'].addTransition('ALERT', 'IDLE', (facts) => facts.get('stability') >= 0.7);
+}
+
+function setupPlannerActions() {
+ symbolicPlanner.addAction('Divert Power', { energy: 20 }, { mode: 'RECOVERY' });
+ symbolicPlanner.addAction('Stabilize Matrix', { energy: 50, mode: 'RECOVERY' }, { stability: 1.0 });
+ symbolicPlanner.addAction('Open Portal', { energy: 80, stability: 1.0 }, { portals: 'online' });
+}
+
+function setupCaseLibrary() {
+ cbr.addCase({ energy: 15, stability: 0.4 }, 'Divert Power', 'SUCCESS');
+ cbr.addCase({ energy: 85, stability: 0.9 }, 'Open Portal', 'SUCCESS');
+}
+
+function updateSymbolicAI(delta, elapsed) {
+ // Sync facts from world state
+ const terminal = batcaveTerminals.find(t => t.title === 'NEXUS COMMAND');
+ if (terminal && terminal.lastState) {
+ const state = terminal.lastState;
+ symbolicEngine.addFact('energy', state.tower.energy);
+ symbolicEngine.addFact('stability', state.matrix.stability);
+ symbolicEngine.addFact('activePortals', portals.filter(p => p.config.status === 'online').length);
+
+ // Update Blackboard
+ blackboard.write('nexus_energy', state.tower.energy, 'NEXUS_COMMAND');
+ blackboard.write('nexus_stability', state.matrix.stability, 'NEXUS_COMMAND');
+
+ // Run CBR for suggestions
+ const currentSituation = { energy: state.tower.energy, stability: state.matrix.stability };
+ const matchedCase = cbr.findSimilarCase(currentSituation);
+ if (matchedCase) cbr.logCase(matchedCase);
+
+ // Run Planner if stability is low
+ if (state.matrix.stability < 0.5 && (!symbolicPlanner.currentPlan || symbolicPlanner.currentPlan.length === 0)) {
+ const initialState = { energy: state.tower.energy, stability: state.matrix.stability, mode: 'NORMAL' };
+ const goalState = { stability: 1.0 };
+ const plan = symbolicPlanner.findPlan(initialState, goalState);
+ symbolicPlanner.logPlan(plan);
+ }
+ }
+
+ // Run reasoning engine
+ if (Math.floor(elapsed * 2) > Math.floor((elapsed - delta) * 2)) { // Every 0.5s
+ // Use Fuzzy Logic in rules
+ const energy = symbolicEngine.facts.get('energy');
+ if (FuzzyLogic.isLow(energy) > 0.8) {
+ symbolicEngine.logReasoning('Fuzzy Energy Check', `Energy is VERY LOW (${(FuzzyLogic.isLow(energy)*100).toFixed(0)}%)`);
+ }
+
+ symbolicEngine.reason();
+ Object.values(agentFSMs).forEach(fsm => fsm.update(symbolicEngine.facts));
+
+ // Update Knowledge Graph based on portal status
+ portals.forEach(p => {
+ const nodeId = `portal_${p.config.id}`;
+ if (!knowledgeGraph.nodes.has(nodeId)) {
+ knowledgeGraph.addNode(nodeId, 'portal', { name: p.config.name });
+ }
+ knowledgeGraph.addEdge(nodeId, 'nexus', p.config.status === 'online' ? 'connected_to' : 'disconnected_from');
+ });
+ }
+}
+
+// ═══ PERFORMANCE BUDGET ═══
+function detectPerformanceTier() {
+ const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || window.innerWidth < 768;
+ const cores = navigator.hardwareConcurrency || 4;
+
+ if (isMobile) {
+ renderer.setPixelRatio(1);
+ renderer.shadowMap.enabled = false;
+ return 'low';
+ } else if (cores < 8) {
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
+ renderer.shadowMap.type = THREE.BasicShadowMap;
+ return 'medium';
+ } else {
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ return 'high';
+ }
+}
+
+function particleCount(base) {
+ if (performanceTier === 'low') return Math.floor(base * 0.25);
+ if (performanceTier === 'medium') return Math.floor(base * 0.6);
+ return base;
+}
+
+// ═══ SKYBOX ═══
+function createSkybox() {
+ const skyGeo = new THREE.SphereGeometry(400, 64, 64);
+ const skyMat = new THREE.ShaderMaterial({
+ uniforms: {
+ uTime: { value: 0 },
+ uColor1: { value: new THREE.Color(0x0a0520) },
+ uColor2: { value: new THREE.Color(0x1a0a3e) },
+ uColor3: { value: new THREE.Color(0x0a1a3e) },
+ uStarDensity: { value: 0.97 },
+ },
+ vertexShader: `
+ varying vec3 vPos;
+ void main() {
+ vPos = position;
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+ }
+ `,
+ fragmentShader: `
+ uniform float uTime;
+ uniform vec3 uColor1;
+ uniform vec3 uColor2;
+ uniform vec3 uColor3;
+ uniform float uStarDensity;
+ varying vec3 vPos;
+
+ float hash(vec3 p) {
+ p = fract(p * vec3(443.897, 441.423, 437.195));
+ p += dot(p, p.yzx + 19.19);
+ return fract((p.x + p.y) * p.z);
+ }
+
+ float noise(vec3 p) {
+ vec3 i = floor(p);
+ vec3 f = fract(p);
+ f = f * f * (3.0 - 2.0 * f);
+ return mix(
+ mix(mix(hash(i), hash(i + vec3(1,0,0)), f.x),
+ mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y),
+ mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),
+ mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y),
+ f.z
+ );
+ }
+
+ float fbm(vec3 p) {
+ float v = 0.0;
+ float a = 0.5;
+ for (int i = 0; i < 5; i++) {
+ v += a * noise(p);
+ p *= 2.0;
+ a *= 0.5;
+ }
+ return v;
+ }
+
+ void main() {
+ vec3 dir = normalize(vPos);
+ float n1 = fbm(dir * 3.0 + uTime * 0.02);
+ float n2 = fbm(dir * 5.0 - uTime * 0.015 + 100.0);
+ float n3 = fbm(dir * 2.0 + uTime * 0.01 + 200.0);
+
+ vec3 col = uColor1;
+ col = mix(col, uColor2, smoothstep(0.3, 0.7, n1));
+ col = mix(col, uColor3, smoothstep(0.4, 0.8, n2) * 0.5);
+
+ float glow = pow(n1 * n2, 2.0) * 1.5;
+ col += vec3(0.15, 0.05, 0.25) * glow;
+ col += vec3(0.05, 0.15, 0.25) * pow(n3, 3.0);
+
+ float starField = hash(dir * 800.0);
+ float stars = step(uStarDensity, starField) * (0.5 + 0.5 * hash(dir * 1600.0));
+ float twinkle = 0.7 + 0.3 * sin(uTime * 2.0 + hash(dir * 400.0) * 6.28);
+ col += vec3(stars * twinkle);
+
+ float bigStar = step(0.998, starField);
+ col += vec3(0.8, 0.9, 1.0) * bigStar * twinkle;
+
+ gl_FragColor = vec4(col, 1.0);
+ }
+ `,
+ side: THREE.BackSide,
+ });
+ const sky = new THREE.Mesh(skyGeo, skyMat);
+ sky.name = 'skybox';
+ scene.add(sky);
+}
+
+// ═══ LIGHTING ═══
+function createLighting() {
+ ambientLight = new THREE.AmbientLight(0x1a1a3a, 0.4);
+ scene.add(ambientLight);
+
+ const dirLight = new THREE.DirectionalLight(0x4466aa, 0.6);
+ dirLight.position.set(10, 20, 10);
+ dirLight.castShadow = renderer.shadowMap.enabled;
+ const shadowRes = performanceTier === 'high' ? 2048 : performanceTier === 'medium' ? 1024 : 512;
+ dirLight.shadow.mapSize.set(shadowRes, shadowRes);
+ scene.add(dirLight);
+
+ const tealLight = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5);
+ tealLight.position.set(0, 1, -5);
+ scene.add(tealLight);
+
+ const purpleLight = new THREE.PointLight(NEXUS.colors.secondary, 1.5, 25, 1.5);
+ purpleLight.position.set(-8, 3, -8);
+ scene.add(purpleLight);
+}
+
+// ═══ FLOOR ═══
+function createFloor() {
+ const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6);
+ const platMat = new THREE.MeshStandardMaterial({
+ color: 0x0a0f1a,
+ roughness: 0.8,
+ metalness: 0.3,
+ });
+ const platform = new THREE.Mesh(platGeo, platMat);
+ platform.position.y = -0.15;
+ platform.receiveShadow = true;
+ scene.add(platform);
+
+ const gridHelper = new THREE.GridHelper(50, 50, NEXUS.colors.gridLine, NEXUS.colors.gridLine);
+ gridHelper.material.opacity = 0.15;
+ gridHelper.material.transparent = true;
+ gridHelper.position.y = 0.02;
+ scene.add(gridHelper);
+
+ const ringGeo = new THREE.RingGeometry(24.5, 25.2, 6);
+ const ringMat = new THREE.MeshBasicMaterial({
+ color: NEXUS.colors.primary,
+ transparent: true,
+ opacity: 0.4,
+ side: THREE.DoubleSide,
+ });
+ const ring = new THREE.Mesh(ringGeo, ringMat);
+ ring.rotation.x = Math.PI / 2;
+ ring.position.y = 0.05;
+ scene.add(ring);
+}
+
+// ═══ BATCAVE TERMINAL ═══
+function createBatcaveTerminal() {
+ const terminalGroup = new THREE.Group();
+ terminalGroup.position.set(0, 0, -8);
+
+ const panelData = [
+ { title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3, lines: ['> STATUS: NOMINAL', '> UPTIME: 142.4h', '> HARNESS: STABLE', '> MODE: SOVEREIGN'] },
+ { title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3, lines: ['> ISSUE #4: CORE', '> ISSUE #5: PORTAL', '> ISSUE #6: TERMINAL', '> ISSUE #7: TIMMY'] },
+ { title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3, lines: ['> CPU: 12% [||....]', '> MEM: 4.2GB', '> COMMITS: 842', '> ACTIVE LOOPS: 5'] },
+ { title: 'SOVEREIGNTY', color: NEXUS.colors.gold, rot: 0.2, x: 3, y: 3, lines: ['REPLIT: GRADE: A', 'PERPLEXITY: GRADE: A-', 'HERMES: GRADE: B+', 'KIMI: GRADE: B', 'CLAUDE: GRADE: B+'] },
+ { title: 'AGENT STATUS', color: NEXUS.colors.primary, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] },
+ ];
+
+ panelData.forEach(data => {
+ const terminal = createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines);
+ batcaveTerminals.push(terminal);
+ });
+
+ scene.add(terminalGroup);
+}
+
+// ═══ WORKSHOP TERMINAL ═══
+function createWorkshopTerminal() {
+ const w = 6, h = 4;
+ const group = new THREE.Group();
+ group.position.set(-14, 3, 0);
+ group.rotation.y = Math.PI / 4;
+
+ workshopPanelCanvas = document.createElement('canvas');
+ workshopPanelCanvas.width = 1024;
+ workshopPanelCanvas.height = 512;
+ workshopPanelCtx = workshopPanelCanvas.getContext('2d');
+
+ workshopPanelTexture = new THREE.CanvasTexture(workshopPanelCanvas);
+ workshopPanelTexture.minFilter = THREE.LinearFilter;
+
+ const panelGeo = new THREE.PlaneGeometry(w, h);
+ const panelMat = new THREE.MeshBasicMaterial({
+ map: workshopPanelTexture,
+ transparent: true,
+ opacity: 0.9,
+ side: THREE.DoubleSide
+ });
+ const panel = new THREE.Mesh(panelGeo, panelMat);
+ group.add(panel);
+
+ const scanGeo = new THREE.PlaneGeometry(w + 0.1, h + 0.1);
+ workshopScanMat = new THREE.ShaderMaterial({
+ transparent: true,
+ uniforms: { uTime: { value: 0 } },
+ vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
+ fragmentShader: `
+ uniform float uTime;
+ varying vec2 vUv;
+ void main() {
+ float scan = sin(vUv.y * 200.0 + uTime * 10.0) * 0.05;
+ float noise = fract(sin(dot(vUv, vec2(12.9898, 78.233))) * 43758.5453) * 0.05;
+ gl_FragColor = vec4(0.0, 0.1, 0.2, scan + noise);
+ }
+ `
+ });
+ const scan = new THREE.Mesh(scanGeo, workshopScanMat);
+ scan.position.z = 0.01;
+ group.add(scan);
+
+ scene.add(group);
+ refreshWorkshopPanel();
+}
+
+function refreshWorkshopPanel() {
+ if (!workshopPanelCtx) return;
+ const ctx = workshopPanelCtx;
+ const w = 1024, h = 512;
+
+ ctx.clearRect(0, 0, w, h);
+ ctx.fillStyle = 'rgba(10, 15, 40, 0.8)';
+ ctx.fillRect(0, 0, w, h);
+
+ ctx.fillStyle = '#4af0c0';
+ ctx.font = 'bold 40px "Orbitron", sans-serif';
+ ctx.fillText('WORKSHOP TERMINAL v1.0', 40, 60);
+ ctx.fillRect(40, 80, 944, 4);
+
+ ctx.font = '24px "JetBrains Mono", monospace';
+ ctx.fillStyle = wsConnected ? '#4af0c0' : '#ff4466';
+ ctx.fillText(`HERMES STATUS: ${wsConnected ? 'ONLINE' : 'OFFLINE'}`, 40, 120);
+
+ ctx.fillStyle = '#7b5cff';
+ const contextName = activePortal ? activePortal.name.toUpperCase() : 'NEXUS CORE';
+ ctx.fillText(`CONTEXT: ${contextName}`, 40, 160);
+
+ ctx.fillStyle = '#a0b8d0';
+ ctx.font = 'bold 20px "Orbitron", sans-serif';
+ ctx.fillText('TOOL OUTPUT STREAM', 40, 220);
+ ctx.fillRect(40, 230, 400, 2);
+
+ ctx.font = '16px "JetBrains Mono", monospace';
+ recentToolOutputs.slice(-10).forEach((out, i) => {
+ ctx.fillStyle = out.type === 'call' ? '#ffd700' : '#4af0c0';
+ const text = `[${out.agent}] ${out.content.substring(0, 80)}${out.content.length > 80 ? '...' : ''}`;
+ ctx.fillText(text, 40, 260 + i * 24);
+ });
+
+ workshopPanelTexture.needsUpdate = true;
+}
+
+function createTerminalPanel(parent, x, y, rot, title, color, lines) {
+ const w = 2.8, h = 3.5;
+ const group = new THREE.Group();
+ group.position.set(x, y, 0);
+ group.rotation.y = rot;
+
+ const bgGeo = new THREE.PlaneGeometry(w, h);
+ const bgMat = new THREE.MeshPhysicalMaterial({
+ color: NEXUS.colors.panelBg,
+ transparent: true,
+ opacity: 0.6,
+ roughness: 0.1,
+ metalness: 0.5,
+ side: THREE.DoubleSide,
+ });
+ const bg = new THREE.Mesh(bgGeo, bgMat);
+ group.add(bg);
+
+ const borderMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
+ const border = new THREE.Mesh(new THREE.PlaneGeometry(w + 0.05, h + 0.05), borderMat);
+ border.position.z = -0.01;
+ group.add(border);
+
+ const textCanvas = document.createElement('canvas');
+ textCanvas.width = 512;
+ textCanvas.height = 640;
+ const ctx = textCanvas.getContext('2d');
+
+ const textTexture = new THREE.CanvasTexture(textCanvas);
+ textTexture.minFilter = THREE.LinearFilter;
+
+ function updatePanelText(newLines) {
+ ctx.clearRect(0, 0, 512, 640);
+ ctx.fillStyle = '#' + new THREE.Color(color).getHexString();
+ ctx.font = 'bold 32px "Orbitron", sans-serif';
+ ctx.fillText(title, 20, 45);
+ ctx.fillRect(20, 55, 472, 2);
+ ctx.font = '20px "JetBrains Mono", monospace';
+ ctx.fillStyle = '#a0b8d0';
+ const displayLines = newLines || lines;
+ displayLines.forEach((line, i) => {
+ let fillColor = '#a0b8d0';
+ if (line.includes('● RUNNING') || line.includes('● ACTIVE') || line.includes('ONLINE')) fillColor = '#4af0c0';
+ else if (line.includes('○ STANDBY') || line.includes('OFFLINE')) fillColor = '#5a6a8a';
+ else if (line.includes('NOMINAL')) fillColor = '#4af0c0';
+ ctx.fillStyle = fillColor;
+ ctx.fillText(line, 20, 100 + i * 40);
+ });
+ textTexture.needsUpdate = true;
+ }
+
+ updatePanelText();
+
+ const textMat = new THREE.MeshBasicMaterial({
+ map: textTexture,
+ transparent: true,
+ side: THREE.DoubleSide,
+ depthWrite: false,
+ });
+ const textMesh = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.95, h * 0.95), textMat);
+ textMesh.position.z = 0.01;
+ group.add(textMesh);
+
+ const scanGeo = new THREE.PlaneGeometry(w, h);
+ const scanMat = new THREE.ShaderMaterial({
+ transparent: true,
+ depthWrite: false,
+ uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color(color) } },
+ vertexShader: `
+ varying vec2 vUv;
+ void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
+ `,
+ fragmentShader: `
+ uniform float uTime;
+ uniform vec3 uColor;
+ varying vec2 vUv;
+ void main() {
+ float scanline = sin(vUv.y * 200.0 + uTime * 2.0) * 0.5 + 0.5;
+ scanline = pow(scanline, 8.0);
+ float sweep = smoothstep(0.0, 0.02, abs(fract(vUv.y - uTime * 0.1) - 0.5));
+ sweep = 1.0 - (1.0 - sweep) * 0.3;
+ float alpha = scanline * 0.04 + (1.0 - sweep) * 0.08;
+ gl_FragColor = vec4(uColor, alpha);
+ }
+ `,
+ side: THREE.DoubleSide,
+ });
+ const scanMesh = new THREE.Mesh(scanGeo, scanMat);
+ scanMesh.position.z = 0.02;
+ group.add(scanMesh);
+
+ parent.add(group);
+ return { group, scanMat, borderMat, updatePanelText, title };
+}
+
+// ═══ GITEA DATA INTEGRATION ═══
+async function fetchGiteaData() {
+ try {
+ const [issuesRes, stateRes] = await Promise.all([
+ fetch('/api/gitea/repos/admin/timmy-tower/issues?state=all'),
+ fetch('/api/gitea/repos/admin/timmy-tower/contents/world_state.json')
+ ]);
+
+ if (issuesRes.ok) {
+ const issues = await issuesRes.json();
+ updateDevQueue(issues);
+ updateAgentStatus(issues);
+ }
+
+ if (stateRes.ok) {
+ const content = await stateRes.json();
+ const worldState = JSON.parse(atob(content.content));
+ updateNexusCommand(worldState);
+ }
+ } catch (e) {
+ console.error('Failed to fetch Gitea data:', e);
+ }
+}
+
+function updateAgentStatus(issues) {
+ const terminal = batcaveTerminals.find(t => t.title === 'AGENT STATUS');
+ if (!terminal) return;
+
+ // Check for Morrowind issues
+ const morrowindIssues = issues.filter(i => i.title.toLowerCase().includes('morrowind') && i.state === 'open');
+ const perplexityStatus = morrowindIssues.length > 0 ? '● MORROWIND' : '○ STANDBY';
+
+ const lines = [
+ '> TIMMY: ● RUNNING',
+ '> KIMI: ○ STANDBY',
+ '> CLAUDE: ● ACTIVE',
+ `> PERPLEXITY: ${perplexityStatus}`
+ ];
+ terminal.updatePanelText(lines);
+}
+
+function updateDevQueue(issues) {
+ const terminal = batcaveTerminals.find(t => t.title === 'DEV QUEUE');
+ if (!terminal) return;
+
+ const lines = issues.slice(0, 4).map(issue => `> #${issue.number}: ${issue.title.substring(0, 15)}...`);
+ while (lines.length < 4) lines.push('> [EMPTY SLOT]');
+ terminal.updatePanelText(lines);
+}
+
+function updateNexusCommand(state) {
+ const terminal = batcaveTerminals.find(t => t.title === 'NEXUS COMMAND');
+ if (!terminal) return;
+
+ terminal.lastState = state; // Store for symbolic engine
+
+ const lines = [
+ `> STATUS: ${state.tower.status.toUpperCase()}`,
+ `> ENERGY: ${state.tower.energy}%`,
+ `> STABILITY: ${(state.matrix.stability * 100).toFixed(1)}%`,
+ `> AGENTS: ${state.matrix.active_agents.length}`
+ ];
+ terminal.updatePanelText(lines);
+}
+
+// ═══ AGENT PRESENCE SYSTEM ═══
+function createAgentPresences() {
+ const agentData = [
+ { id: 'timmy', name: 'TIMMY', color: NEXUS.colors.primary, pos: { x: -4, z: -4 }, station: { x: -4, z: -4 } },
+ { id: 'kimi', name: 'KIMI', color: NEXUS.colors.secondary, pos: { x: 4, z: -4 }, station: { x: 4, z: -4 } },
+ { id: 'claude', name: 'CLAUDE', color: NEXUS.colors.gold, pos: { x: 0, z: -6 }, station: { x: 0, z: -6 } },
+ { id: 'perplexity', name: 'PERPLEXITY', color: 0x4488ff, pos: { x: -6, z: -2 }, station: { x: -6, z: -2 } },
+ ];
+
+ agentData.forEach(data => {
+ const group = new THREE.Group();
+ group.position.set(data.pos.x, 0, data.pos.z);
+
+ const color = new THREE.Color(data.color);
+
+ // Agent Orb
+ const orbGeo = new THREE.SphereGeometry(0.4, 32, 32);
+ const orbMat = new THREE.MeshPhysicalMaterial({
+ color: color,
+ emissive: color,
+ emissiveIntensity: 2,
+ roughness: 0,
+ metalness: 1,
+ transmission: 0.8,
+ thickness: 0.5,
+ });
+ const orb = new THREE.Mesh(orbGeo, orbMat);
+ orb.position.y = 3;
+ group.add(orb);
+
+ // Halo
+ const haloGeo = new THREE.TorusGeometry(0.6, 0.02, 16, 64);
+ const haloMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.4 });
+ const halo = new THREE.Mesh(haloGeo, haloMat);
+ halo.position.y = 3;
+ halo.rotation.x = Math.PI / 2;
+ group.add(halo);
+
+ // Label
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 64;
+ const ctx = canvas.getContext('2d');
+ ctx.font = 'bold 24px "Orbitron", sans-serif';
+ ctx.fillStyle = '#' + color.getHexString();
+ ctx.textAlign = 'center';
+ ctx.fillText(data.name, 128, 40);
+ const tex = new THREE.CanvasTexture(canvas);
+ const mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
+ const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), mat);
+ label.position.y = 3.8;
+ group.add(label);
+
+ scene.add(group);
+ agents.push({
+ id: data.id,
+ group,
+ orb,
+ halo,
+ color,
+ station: data.station,
+ targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z),
+ wanderTimer: 0
+ });
+ });
+}
+
+function createThoughtStream() {
+ const geo = new THREE.CylinderGeometry(8, 8, 12, 32, 1, true);
+ const mat = new THREE.ShaderMaterial({
+ transparent: true,
+ side: THREE.BackSide,
+ depthWrite: false,
+ uniforms: {
+ uTime: { value: 0 },
+ uColor: { value: new THREE.Color(NEXUS.colors.primary) },
+ },
+ vertexShader: `
+ varying vec2 vUv;
+ void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
+ `,
+ fragmentShader: `
+ uniform float uTime;
+ uniform vec3 uColor;
+ varying vec2 vUv;
+
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
+
+ void main() {
+ float flow = fract(vUv.y - uTime * 0.1);
+ float lines = step(0.98, fract(vUv.x * 20.0 + uTime * 0.05));
+ float dots = step(0.99, hash(vUv * 50.0 + floor(uTime * 10.0) * 0.01));
+
+ float alpha = (lines * 0.1 + dots * 0.5) * smoothstep(0.0, 0.2, vUv.y) * smoothstep(1.0, 0.8, vUv.y);
+ gl_FragColor = vec4(uColor, alpha * 0.3);
+ }
+ `,
+ });
+ thoughtStreamMesh = new THREE.Mesh(geo, mat);
+ thoughtStreamMesh.position.y = 6;
+ scene.add(thoughtStreamMesh);
+}
+
+function createHarnessPulse() {
+ const geo = new THREE.RingGeometry(0.1, 0.2, 64);
+ const mat = new THREE.MeshBasicMaterial({
+ color: NEXUS.colors.primary,
+ transparent: true,
+ opacity: 0,
+ side: THREE.DoubleSide,
+ });
+ harnessPulseMesh = new THREE.Mesh(geo, mat);
+ harnessPulseMesh.rotation.x = -Math.PI / 2;
+ harnessPulseMesh.position.y = 0.1;
+ scene.add(harnessPulseMesh);
+}
+
+function createSessionPowerMeter() {
+ const group = new THREE.Group();
+ group.position.set(0, 0, 3);
+
+ const barCount = 12;
+ const barGeo = new THREE.BoxGeometry(0.2, 0.1, 0.1);
+
+ for (let i = 0; i < barCount; i++) {
+ const mat = new THREE.MeshStandardMaterial({
+ color: NEXUS.colors.primary,
+ emissive: NEXUS.colors.primary,
+ emissiveIntensity: 0.2,
+ transparent: true,
+ opacity: 0.6
+ });
+ const bar = new THREE.Mesh(barGeo, mat);
+ bar.position.y = 0.2 + i * 0.2;
+ group.add(bar);
+ powerMeterBars.push(bar);
+ }
+
+ const labelCanvas = document.createElement('canvas');
+ labelCanvas.width = 256;
+ labelCanvas.height = 64;
+ const ctx = labelCanvas.getContext('2d');
+ ctx.font = 'bold 24px "Orbitron", sans-serif';
+ ctx.fillStyle = '#4af0c0';
+ ctx.textAlign = 'center';
+ ctx.fillText('POWER LEVEL', 128, 40);
+ const tex = new THREE.CanvasTexture(labelCanvas);
+ const labelMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
+ const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), labelMat);
+ label.position.y = 3;
+ group.add(label);
+
+ scene.add(group);
+}
+
+// ═══ VISION SYSTEM ═══
+function createVisionPoints(data) {
+ data.forEach(config => {
+ const vp = createVisionPoint(config);
+ visionPoints.push(vp);
+ });
+}
+
+function createVisionPoint(config) {
+ const group = new THREE.Group();
+ group.position.set(config.position.x, config.position.y, config.position.z);
+
+ const color = new THREE.Color(config.color);
+
+ // Floating Crystal
+ const crystalGeo = new THREE.OctahedronGeometry(0.6, 0);
+ const crystalMat = new THREE.MeshPhysicalMaterial({
+ color: color,
+ emissive: color,
+ emissiveIntensity: 1,
+ roughness: 0,
+ metalness: 1,
+ transmission: 0.5,
+ thickness: 1,
+ });
+ const crystal = new THREE.Mesh(crystalGeo, crystalMat);
+ crystal.position.y = 2.5;
+ group.add(crystal);
+
+ // Glow Ring
+ const ringGeo = new THREE.TorusGeometry(0.8, 0.02, 16, 64);
+ const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 });
+ const ring = new THREE.Mesh(ringGeo, ringMat);
+ ring.position.y = 2.5;
+ ring.rotation.x = Math.PI / 2;
+ group.add(ring);
+
+ // Light
+ const light = new THREE.PointLight(color, 1, 10);
+ light.position.set(0, 2.5, 0);
+ group.add(light);
+
+ scene.add(group);
+
+ return { config, group, crystal, ring, light };
+}
+
+// ═══ PORTAL SYSTEM ═══
+function createPortals(data) {
+ data.forEach(config => {
+ const portal = createPortal(config);
+ portals.push(portal);
+ });
+}
+
+function createPortal(config) {
+ const group = new THREE.Group();
+ group.position.set(config.position.x, config.position.y, config.position.z);
+ if (config.rotation) {
+ group.rotation.y = config.rotation.y;
+ }
+
+ const portalColor = new THREE.Color(config.color);
+
+ // Torus Ring
+ const torusGeo = new THREE.TorusGeometry(3, 0.15, 16, 64);
+ const torusMat = new THREE.MeshStandardMaterial({
+ color: portalColor,
+ emissive: portalColor,
+ emissiveIntensity: 1.5,
+ roughness: 0.2,
+ metalness: 0.8,
+ });
+ const ring = new THREE.Mesh(torusGeo, torusMat);
+ ring.position.y = 3.5;
+ ring.name = `portal_ring_${config.id}`;
+ group.add(ring);
+
+ // Swirl Disc
+ const swirlGeo = new THREE.CircleGeometry(2.8, 64);
+ const swirlMat = new THREE.ShaderMaterial({
+ transparent: true,
+ side: THREE.DoubleSide,
+ uniforms: {
+ uTime: { value: 0 },
+ uColor: { value: portalColor },
+ },
+ vertexShader: `
+ varying vec2 vUv;
+ void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
+ `,
+ fragmentShader: `
+ uniform float uTime;
+ uniform vec3 uColor;
+ varying vec2 vUv;
+ void main() {
+ vec2 c = vUv - 0.5;
+ float r = length(c);
+ float a = atan(c.y, c.x);
+ float swirl = sin(a * 3.0 + r * 10.0 - uTime * 3.0) * 0.5 + 0.5;
+ float swirl2 = sin(a * 5.0 - r * 8.0 + uTime * 2.0) * 0.5 + 0.5;
+ float mask = smoothstep(0.5, 0.1, r);
+ vec3 col = mix(uColor, vec3(1.0, 1.0, 1.0), swirl * 0.3);
+ col = mix(col, vec3(1.0, 1.0, 1.0), swirl2 * 0.2);
+ float alpha = mask * (0.5 + 0.3 * swirl);
+ gl_FragColor = vec4(col, alpha);
+ }
+ `,
+ });
+ const swirl = new THREE.Mesh(swirlGeo, swirlMat);
+ swirl.position.y = 3.5;
+ group.add(swirl);
+
+ // Orbital Particles
+ const pCount = 120;
+ const pGeo = new THREE.BufferGeometry();
+ const pPos = new Float32Array(pCount * 3);
+ const pSizes = new Float32Array(pCount);
+ for (let i = 0; i < pCount; i++) {
+ const angle = Math.random() * Math.PI * 2;
+ const r = 3.2 + Math.random() * 0.5;
+ pPos[i * 3] = Math.cos(angle) * r;
+ pPos[i * 3 + 1] = 3.5 + (Math.random() - 0.5) * 6;
+ pPos[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
+ pSizes[i] = 0.05 + Math.random() * 0.1;
+ }
+ pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
+ pGeo.setAttribute('size', new THREE.BufferAttribute(pSizes, 1));
+ const pMat = new THREE.PointsMaterial({
+ color: portalColor,
+ size: 0.08,
+ transparent: true,
+ opacity: 0.6,
+ blending: THREE.AdditiveBlending,
+ depthWrite: false,
+ });
+ const pSystem = new THREE.Points(pGeo, pMat);
+ group.add(pSystem);
+
+ // Pulsing Point Light
+ const light = new THREE.PointLight(portalColor, 2, 15, 1.5);
+ light.position.set(0, 3.5, 1);
+ group.add(light);
+
+ // Label
+ const labelCanvas = document.createElement('canvas');
+ labelCanvas.width = 512;
+ labelCanvas.height = 64;
+ const lctx = labelCanvas.getContext('2d');
+ lctx.font = 'bold 32px "Orbitron", sans-serif';
+ lctx.fillStyle = '#' + portalColor.getHexString();
+ lctx.textAlign = 'center';
+ lctx.fillText(`◈ ${config.name.toUpperCase()}`, 256, 42);
+ const labelTex = new THREE.CanvasTexture(labelCanvas);
+ const labelMat = new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide });
+ const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.5), labelMat);
+ labelMesh.position.y = 7.5;
+ group.add(labelMesh);
+
+ // Base Pillars
+ for (let side of [-1, 1]) {
+ const pillarGeo = new THREE.CylinderGeometry(0.2, 0.3, 7, 8);
+ const pillarMat = new THREE.MeshStandardMaterial({
+ color: 0x1a1a2e,
+ roughness: 0.5,
+ metalness: 0.7,
+ emissive: portalColor,
+ emissiveIntensity: 0.1,
+ });
+ const pillar = new THREE.Mesh(pillarGeo, pillarMat);
+ pillar.position.set(side * 3, 3.5, 0);
+ pillar.castShadow = true;
+ group.add(pillar);
+ }
+
+ scene.add(group);
+
+ const portalObj = {
+ config,
+ group,
+ ring,
+ swirl,
+ pSystem,
+ light,
+ customElements: {}
+ };
+
+ // ═══ DISTINCT VISUAL IDENTITIES ═══
+ if (config.id === 'archive') {
+ // Floating Data Cubes
+ const cubes = [];
+ for (let i = 0; i < 6; i++) {
+ const cubeGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4);
+ const cubeMat = new THREE.MeshStandardMaterial({
+ color: portalColor,
+ emissive: portalColor,
+ emissiveIntensity: 1.5,
+ transparent: true,
+ opacity: 0.8
+ });
+ const cube = new THREE.Mesh(cubeGeo, cubeMat);
+ group.add(cube);
+ cubes.push(cube);
+ }
+ portalObj.customElements.cubes = cubes;
+ } else if (config.id === 'chapel') {
+ // Glowing Core + Halo
+ const coreGeo = new THREE.SphereGeometry(1.2, 32, 32);
+ const coreMat = new THREE.MeshPhysicalMaterial({
+ color: 0xffffff,
+ emissive: portalColor,
+ emissiveIntensity: 2,
+ transparent: true,
+ opacity: 0.4,
+ transmission: 0.9,
+ thickness: 2
+ });
+ const core = new THREE.Mesh(coreGeo, coreMat);
+ core.position.y = 3.5;
+ group.add(core);
+ portalObj.customElements.core = core;
+
+ const haloGeo = new THREE.TorusGeometry(3.5, 0.05, 16, 100);
+ const haloMat = new THREE.MeshBasicMaterial({ color: portalColor, transparent: true, opacity: 0.3 });
+ const halo = new THREE.Mesh(haloGeo, haloMat);
+ halo.position.y = 3.5;
+ group.add(halo);
+ portalObj.customElements.halo = halo;
+ } else if (config.id === 'courtyard') {
+ // Double Rotating Rings
+ const outerRingGeo = new THREE.TorusGeometry(4.2, 0.1, 16, 80);
+ const outerRingMat = new THREE.MeshStandardMaterial({
+ color: portalColor,
+ emissive: portalColor,
+ emissiveIntensity: 0.8,
+ transparent: true,
+ opacity: 0.5
+ });
+ const outerRing = new THREE.Mesh(outerRingGeo, outerRingMat);
+ outerRing.position.y = 3.5;
+ group.add(outerRing);
+ portalObj.customElements.outerRing = outerRing;
+ } else if (config.id === 'gate') {
+ // Spiky Monoliths
+ const spikes = [];
+ for (let i = 0; i < 8; i++) {
+ const spikeGeo = new THREE.ConeGeometry(0.2, 1.5, 4);
+ const spikeMat = new THREE.MeshStandardMaterial({ color: 0x111111, emissive: portalColor, emissiveIntensity: 0.5 });
+ const spike = new THREE.Mesh(spikeGeo, spikeMat);
+ const angle = (i / 8) * Math.PI * 2;
+ spike.position.set(Math.cos(angle) * 3.5, 3.5 + Math.sin(angle) * 3.5, 0);
+ spike.rotation.z = angle + Math.PI / 2;
+ group.add(spike);
+ spikes.push(spike);
+ }
+ portalObj.customElements.spikes = spikes;
+
+ // Darker Swirl
+ swirl.material.uniforms.uColor.value = new THREE.Color(0x220000);
+ }
+
+ return portalObj;
+}
+
+// ═══ PARTICLES ═══
+function createParticles() {
+ const count = particleCount(1500);
+ const geo = new THREE.BufferGeometry();
+ const positions = new Float32Array(count * 3);
+ const colors = new Float32Array(count * 3);
+ const sizes = new Float32Array(count);
+
+ const c1 = new THREE.Color(NEXUS.colors.primary);
+ const c2 = new THREE.Color(NEXUS.colors.secondary);
+ const c3 = new THREE.Color(NEXUS.colors.gold);
+
+ for (let i = 0; i < count; i++) {
+ positions[i * 3] = (Math.random() - 0.5) * 60;
+ positions[i * 3 + 1] = Math.random() * 20;
+ positions[i * 3 + 2] = (Math.random() - 0.5) * 60;
+
+ const t = Math.random();
+ const col = t < 0.5 ? c1.clone().lerp(c2, t * 2) : c2.clone().lerp(c3, (t - 0.5) * 2);
+ colors[i * 3] = col.r;
+ colors[i * 3 + 1] = col.g;
+ colors[i * 3 + 2] = col.b;
+
+ sizes[i] = 0.02 + Math.random() * 0.06;
+ }
+
+ geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+ geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
+ geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
+
+ const mat = new THREE.ShaderMaterial({
+ uniforms: { uTime: { value: 0 } },
+ vertexShader: `
+ attribute float size;
+ attribute vec3 color;
+ varying vec3 vColor;
+ uniform float uTime;
+ void main() {
+ vColor = color;
+ vec3 pos = position;
+ pos.y += sin(uTime * 0.5 + position.x * 0.5) * 0.3;
+ pos.x += sin(uTime * 0.3 + position.z * 0.4) * 0.2;
+ vec4 mv = modelViewMatrix * vec4(pos, 1.0);
+ gl_PointSize = size * 300.0 / -mv.z;
+ gl_Position = projectionMatrix * mv;
+ }
+ `,
+ fragmentShader: `
+ varying vec3 vColor;
+ void main() {
+ float d = length(gl_PointCoord - 0.5);
+ if (d > 0.5) discard;
+ float alpha = smoothstep(0.5, 0.1, d);
+ gl_FragColor = vec4(vColor, alpha * 0.7);
+ }
+ `,
+ transparent: true,
+ depthWrite: false,
+ blending: THREE.AdditiveBlending,
+ });
+
+ particles = new THREE.Points(geo, mat);
+ scene.add(particles);
+}
+
+function createDustParticles() {
+ const count = particleCount(500);
+ const geo = new THREE.BufferGeometry();
+ const positions = new Float32Array(count * 3);
+
+ for (let i = 0; i < count; i++) {
+ positions[i * 3] = (Math.random() - 0.5) * 40;
+ positions[i * 3 + 1] = Math.random() * 15;
+ positions[i * 3 + 2] = (Math.random() - 0.5) * 40;
+ }
+
+ geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+
+ const mat = new THREE.PointsMaterial({
+ color: 0x8899bb,
+ size: 0.03,
+ transparent: true,
+ opacity: 0.3,
+ depthWrite: false,
+ });
+
+ dustParticles = new THREE.Points(geo, mat);
+ scene.add(dustParticles);
+}
+
+// ═══ AMBIENT STRUCTURES ═══
+function createAmbientStructures() {
+ const crystalMat = new THREE.MeshPhysicalMaterial({
+ color: 0x3355aa,
+ roughness: 0.1,
+ metalness: 0.2,
+ transmission: 0.6,
+ thickness: 2,
+ emissive: 0x1122aa,
+ emissiveIntensity: 0.3,
+ });
+
+ const positions = [
+ { x: -18, z: -15, s: 1.5, ry: 0.3 },
+ { x: -20, z: -10, s: 1, ry: 0.8 },
+ { x: -15, z: -18, s: 2, ry: 1.2 },
+ { x: 18, z: -15, s: 1.8, ry: 2.1 },
+ { x: 20, z: -12, s: 1.2, ry: 0.5 },
+ { x: -12, z: 18, s: 1.3, ry: 1.8 },
+ { x: 14, z: 16, s: 1.6, ry: 0.9 },
+ ];
+
+ positions.forEach(p => {
+ const geo = new THREE.ConeGeometry(0.4 * p.s, 2.5 * p.s, 5);
+ const crystal = new THREE.Mesh(geo, crystalMat.clone());
+ crystal.position.set(p.x, 1.25 * p.s, p.z);
+ crystal.rotation.y = p.ry;
+ crystal.rotation.z = (Math.random() - 0.5) * 0.3;
+ crystal.castShadow = true;
+ scene.add(crystal);
+ });
+
+ for (let i = 0; i < 5; i++) {
+ const angle = (i / 5) * Math.PI * 2;
+ const r = 10;
+ const geo = new THREE.OctahedronGeometry(0.4, 0);
+ const mat = new THREE.MeshStandardMaterial({
+ color: NEXUS.colors.primary,
+ emissive: NEXUS.colors.primary,
+ emissiveIntensity: 0.5,
+ });
+ const stone = new THREE.Mesh(geo, mat);
+ stone.position.set(Math.cos(angle) * r, 5 + Math.sin(i * 1.3) * 1.5, Math.sin(angle) * r);
+ stone.name = 'runestone_' + i;
+ scene.add(stone);
+ }
+
+ const coreGeo = new THREE.IcosahedronGeometry(0.6, 2);
+ const coreMat = new THREE.MeshPhysicalMaterial({
+ color: 0x4af0c0,
+ emissive: 0x4af0c0,
+ emissiveIntensity: 2,
+ roughness: 0,
+ metalness: 1,
+ transmission: 0.3,
+ thickness: 1,
+ });
+ const core = new THREE.Mesh(coreGeo, coreMat);
+ core.position.set(0, 2.5, 0);
+ core.name = 'nexus-core';
+ scene.add(core);
+
+ const pedGeo = new THREE.CylinderGeometry(0.8, 1.2, 1.5, 8);
+ const pedMat = new THREE.MeshStandardMaterial({
+ color: 0x0a0f1a,
+ roughness: 0.4,
+ metalness: 0.8,
+ emissive: 0x1a2a4a,
+ emissiveIntensity: 0.3,
+ });
+ const pedestal = new THREE.Mesh(pedGeo, pedMat);
+ pedestal.position.set(0, 0.75, 0);
+ pedestal.castShadow = true;
+ scene.add(pedestal);
+}
+
+// ═══ NAVIGATION MODE ═══
+function cycleNavMode() {
+ navModeIdx = (navModeIdx + 1) % NAV_MODES.length;
+ const mode = NAV_MODES[navModeIdx];
+ if (mode === 'orbit') {
+ const dir = new THREE.Vector3(0, 0, -1).applyEuler(playerRot);
+ orbitState.target.copy(playerPos).addScaledVector(dir, orbitState.radius);
+ orbitState.target.y = Math.max(0, orbitState.target.y);
+ const toCamera = new THREE.Vector3().subVectors(playerPos, orbitState.target);
+ orbitState.radius = toCamera.length();
+ orbitState.theta = Math.atan2(toCamera.x, toCamera.z);
+ orbitState.phi = Math.acos(Math.max(-1, Math.min(1, toCamera.y / orbitState.radius)));
+ }
+ if (mode === 'fly') flyY = playerPos.y;
+ updateNavModeUI(mode);
+}
+
+function updateNavModeUI(mode) {
+ const el = document.getElementById('nav-mode-label');
+ if (el) el.textContent = mode.toUpperCase();
+}
+
+// ═══ CONTROLS ═══
+function setupControls() {
+ document.addEventListener('keydown', (e) => {
+ keys[e.key.toLowerCase()] = true;
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ const input = document.getElementById('chat-input');
+ if (document.activeElement === input) {
+ sendChatMessage();
+ } else {
+ input.focus();
+ }
+ }
+ if (e.key.toLowerCase() === 'm' && document.activeElement !== document.getElementById('chat-input')) {
+ openPortalAtlas();
+ }
+ if (e.key === 'Escape') {
+ document.getElementById('chat-input').blur();
+ if (portalOverlayActive) closePortalOverlay();
+ if (visionOverlayActive) closeVisionOverlay();
+ if (atlasOverlayActive) closePortalAtlas();
+ }
+ if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) {
+ cycleNavMode();
+ }
+ if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) {
+ activatePortal(activePortal);
+ }
+ if (e.key.toLowerCase() === 'e' && activeVisionPoint && !visionOverlayActive) {
+ activateVisionPoint(activeVisionPoint);
+ }
+ });
+ document.addEventListener('keyup', (e) => {
+ keys[e.key.toLowerCase()] = false;
+ });
+
+ const canvas = document.getElementById('nexus-canvas');
+ canvas.addEventListener('mousedown', (e) => {
+ if (e.target === canvas) {
+ mouseDown = true;
+ orbitState.lastX = e.clientX;
+ orbitState.lastY = e.clientY;
+
+ // Raycasting for portals
+ if (!portalOverlayActive) {
+ const mouse = new THREE.Vector2(
+ (e.clientX / window.innerWidth) * 2 - 1,
+ -(e.clientY / window.innerHeight) * 2 + 1
+ );
+ const raycaster = new THREE.Raycaster();
+ raycaster.setFromCamera(mouse, camera);
+ const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
+ if (intersects.length > 0) {
+ const clickedRing = intersects[0].object;
+ const portal = portals.find(p => p.ring === clickedRing);
+ if (portal) activatePortal(portal);
+ }
+ }
+ }
+ });
+ document.addEventListener('mouseup', () => { mouseDown = false; });
+ document.addEventListener('mousemove', (e) => {
+ if (!mouseDown) return;
+ if (document.activeElement === document.getElementById('chat-input')) return;
+ const mode = NAV_MODES[navModeIdx];
+ if (mode === 'orbit') {
+ const dx = e.clientX - orbitState.lastX;
+ const dy = e.clientY - orbitState.lastY;
+ orbitState.lastX = e.clientX;
+ orbitState.lastY = e.clientY;
+ orbitState.theta -= dx * 0.005;
+ orbitState.phi = Math.max(0.05, Math.min(Math.PI * 0.85, orbitState.phi + dy * 0.005));
+ } else {
+ playerRot.y -= e.movementX * 0.003;
+ playerRot.x -= e.movementY * 0.003;
+ playerRot.x = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, playerRot.x));
+ }
+ });
+
+ canvas.addEventListener('wheel', (e) => {
+ if (NAV_MODES[navModeIdx] === 'orbit') {
+ orbitState.radius = Math.max(orbitState.minR, Math.min(orbitState.maxR, orbitState.radius + e.deltaY * 0.02));
+ }
+ }, { passive: true });
+
+ document.getElementById('chat-toggle').addEventListener('click', () => {
+ chatOpen = !chatOpen;
+ document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen);
+ });
+ document.getElementById('chat-send').addEventListener('click', () => sendChatMessage());
+
+ // Chat quick actions
+ document.getElementById('chat-quick-actions').addEventListener('click', (e) => {
+ const btn = e.target.closest('.quick-action-btn');
+ if (!btn) return;
+
+ const action = btn.dataset.action;
+
+ switch(action) {
+ case 'status':
+ sendChatMessage("Timmy, what is the current system status?");
+ break;
+ case 'agents':
+ sendChatMessage("Timmy, check on all active agents.");
+ break;
+ case 'portals':
+ openPortalAtlas();
+ break;
+ case 'help':
+ sendChatMessage("Timmy, I need assistance with Nexus navigation.");
+ break;
+ }
+ });
+
+ document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
+ document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
+
+ document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas);
+ document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas);
+}
+
+function sendChatMessage(overrideText = null) {
+ const input = document.getElementById('chat-input');
+ const text = overrideText || input.value.trim();
+ if (!text) return;
+ addChatMessage('user', text);
+ if (!overrideText) input.value = '';
+ setTimeout(() => {
+ const responses = [
+ 'Processing your request through the harness...',
+ 'I have noted this in my thought stream.',
+ 'Acknowledged. Routing to appropriate agent loop.',
+ 'The sovereign space recognizes your command.',
+ 'Running analysis. Results will appear on the main terminal.',
+ 'My crystal ball says... yes. Implementing.',
+ 'Understood, Alexander. Adjusting priorities.',
+ ];
+ const resp = responses[Math.floor(Math.random() * responses.length)];
+ addChatMessage('timmy', resp);
+ }, 500 + Math.random() * 1000);
+ input.blur();
+}
+
+// ═══ HERMES WEBSOCKET ═══
+function connectHermes() {
+ if (hermesWs) return;
+
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsUrl = `${protocol}//${window.location.host}/api/world/ws`;
+
+ console.log(`Connecting to Hermes at ${wsUrl}...`);
+ hermesWs = new WebSocket(wsUrl);
+
+ hermesWs.onopen = () => {
+ console.log('Hermes connected.');
+ wsConnected = true;
+ addChatMessage('system', 'Hermes link established.');
+ updateWsHudStatus(true);
+ refreshWorkshopPanel();
+ };
+
+ hermesWs.onmessage = (evt) => {
+ try {
+ const data = JSON.parse(evt.data);
+ handleHermesMessage(data);
+ } catch (e) {
+ console.error('Failed to parse Hermes message:', e);
+ }
+ };
+
+ hermesWs.onclose = () => {
+ console.warn('Hermes disconnected. Retrying in 5s...');
+ wsConnected = false;
+ hermesWs = null;
+ updateWsHudStatus(false);
+ refreshWorkshopPanel();
+ if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
+ wsReconnectTimer = setTimeout(connectHermes, 5000);
+ };
+
+ hermesWs.onerror = (err) => {
+ console.error('Hermes WS error:', err);
+ };
+}
+
+let lastHeartbeatTime = 0;
+let heartbeatFrequency = 0.6; // Default 0.6Hz
+let heartbeatIntensity = 1.0;
+let heartbeatSource = 'local';
+
+function handleHermesMessage(data) {
+ if (data.type === 'chat') {
+ addChatMessage(data.agent || 'timmy', data.text);
+ } else if (data.type === 'heartbeat') {
+ heartbeatFrequency = data.frequency || 0.6;
+ heartbeatIntensity = data.intensity || 1.0;
+ heartbeatSource = data.source || 'evonia-layer';
+ lastHeartbeatTime = Date.now();
+
+ // Visual feedback for heartbeat sync
+ const pulseEl = document.querySelector('.heartbeat-pulse');
+ if (pulseEl) {
+ pulseEl.style.borderColor = '#4af0c0';
+ pulseEl.style.boxShadow = '0 0 15px #4af0c0';
+ setTimeout(() => {
+ pulseEl.style.borderColor = '';
+ pulseEl.style.boxShadow = '';
+ }, 100);
+ }
+ } else if (data.type === 'tool_call') {
+ const content = `Calling ${data.tool}(${JSON.stringify(data.args)})`;
+ recentToolOutputs.push({ type: 'call', agent: data.agent || 'SYSTEM', content });
+ addToolMessage(data.agent || 'SYSTEM', 'call', content);
+ refreshWorkshopPanel();
+ } else if (data.type === 'tool_result') {
+ const content = `Result: ${JSON.stringify(data.result)}`;
+ recentToolOutputs.push({ type: 'result', agent: data.agent || 'SYSTEM', content });
+ addToolMessage(data.agent || 'SYSTEM', 'result', content);
+ refreshWorkshopPanel();
+ } else if (data.type === 'history') {
+ const container = document.getElementById('chat-messages');
+ container.innerHTML = '';
+ data.messages.forEach(msg => {
+ if (msg.type === 'tool_call') addToolMessage(msg.agent, 'call', msg.content, false);
+ else if (msg.type === 'tool_result') addToolMessage(msg.agent, 'result', msg.content, false);
+ else addChatMessage(msg.agent, msg.text, false);
+ });
+ }
+}
+
+function updateWsHudStatus(connected) {
+ const dot = document.querySelector('.chat-status-dot');
+ if (dot) {
+ dot.style.background = connected ? '#4af0c0' : '#ff4466';
+ dot.style.boxShadow = connected ? '0 0 10px #4af0c0' : '0 0 10px #ff4466';
+ }
+}
+
+// ═══ SESSION PERSISTENCE ═══
+function saveSession() {
+ const msgs = Array.from(document.querySelectorAll('.chat-msg')).slice(-60).map(el => ({
+ html: el.innerHTML,
+ className: el.className
+ }));
+ localStorage.setItem('nexus_chat_history', JSON.stringify(msgs));
+}
+
+function loadSession() {
+ const saved = localStorage.getItem('nexus_chat_history');
+ if (saved) {
+ const msgs = JSON.parse(saved);
+ const container = document.getElementById('chat-messages');
+ container.innerHTML = '';
+ msgs.forEach(m => {
+ const div = document.createElement('div');
+ div.className = m.className;
+ div.innerHTML = m.html;
+ container.appendChild(div);
+ });
+ container.scrollTop = container.scrollHeight;
+ }
+}
+
+function addChatMessage(agent, text, shouldSave = true) {
+ const container = document.getElementById('chat-messages');
+ const div = document.createElement('div');
+ div.className = `chat-msg chat-msg-${agent}`;
+
+ const prefixes = {
+ user: '[ALEXANDER]',
+ timmy: '[TIMMY]',
+ system: '[NEXUS]',
+ error: '[ERROR]',
+ kimi: '[KIMI]',
+ claude: '[CLAUDE]',
+ perplexity: '[PERPLEXITY]'
+ };
+
+ const prefix = document.createElement('span');
+ prefix.className = 'chat-msg-prefix';
+ prefix.textContent = `${prefixes[agent] || '[' + agent.toUpperCase() + ']'} `;
+
+ div.appendChild(prefix);
+ div.appendChild(document.createTextNode(text));
+
+ container.appendChild(div);
+ container.scrollTop = container.scrollHeight;
+
+ if (shouldSave) saveSession();
+}
+
+function addToolMessage(agent, type, content, shouldSave = true) {
+ const container = document.getElementById('chat-messages');
+ const div = document.createElement('div');
+ div.className = `chat-msg chat-msg-tool tool-${type}`;
+
+ const prefix = document.createElement('div');
+ prefix.className = 'chat-msg-prefix';
+ prefix.textContent = `[${agent.toUpperCase()} TOOL ${type.toUpperCase()}]`;
+
+ const pre = document.createElement('pre');
+ pre.className = 'tool-content';
+ pre.textContent = content;
+
+ div.appendChild(prefix);
+ div.appendChild(pre);
+
+ container.appendChild(div);
+ container.scrollTop = container.scrollHeight;
+
+ if (shouldSave) saveSession();
+}
+
+// ═══ PORTAL INTERACTION ═══
+function checkPortalProximity() {
+ if (portalOverlayActive) return;
+
+ let closest = null;
+ let minDist = Infinity;
+
+ portals.forEach(portal => {
+ const dist = playerPos.distanceTo(portal.group.position);
+ if (dist < 4.5 && dist < minDist) {
+ minDist = dist;
+ closest = portal;
+ }
+ });
+
+ activePortal = closest;
+ const hint = document.getElementById('portal-hint');
+ if (activePortal) {
+ document.getElementById('portal-hint-name').textContent = activePortal.config.name;
+ hint.style.display = 'flex';
+ } else {
+ hint.style.display = 'none';
+ }
+}
+
+function activatePortal(portal) {
+ portalOverlayActive = true;
+ const overlay = document.getElementById('portal-overlay');
+ const nameDisplay = document.getElementById('portal-name-display');
+ const descDisplay = document.getElementById('portal-desc-display');
+ const redirectBox = document.getElementById('portal-redirect-box');
+ const errorBox = document.getElementById('portal-error-box');
+ const timerDisplay = document.getElementById('portal-timer');
+ const statusDot = document.getElementById('portal-status-dot');
+
+ nameDisplay.textContent = portal.config.name.toUpperCase();
+ descDisplay.textContent = portal.config.description;
+ statusDot.style.background = portal.config.color;
+ statusDot.style.boxShadow = `0 0 10px ${portal.config.color}`;
+
+ overlay.style.display = 'flex';
+
+ if (portal.config.destination && portal.config.destination.url) {
+ redirectBox.style.display = 'block';
+ errorBox.style.display = 'none';
+
+ let count = 5;
+ timerDisplay.textContent = count;
+ const interval = setInterval(() => {
+ count--;
+ timerDisplay.textContent = count;
+ if (count <= 0) {
+ clearInterval(interval);
+ if (portalOverlayActive) window.location.href = portal.config.destination.url;
+ }
+ if (!portalOverlayActive) clearInterval(interval);
+ }, 1000);
+ } else {
+ redirectBox.style.display = 'none';
+ errorBox.style.display = 'block';
+ }
+}
+
+function closePortalOverlay() {
+ portalOverlayActive = false;
+ document.getElementById('portal-overlay').style.display = 'none';
+}
+
+// ═══ VISION INTERACTION ═══
+function checkVisionProximity() {
+ if (visionOverlayActive) return;
+
+ let closest = null;
+ let minDist = Infinity;
+
+ visionPoints.forEach(vp => {
+ const dist = playerPos.distanceTo(vp.group.position);
+ if (dist < 3.5 && dist < minDist) {
+ minDist = dist;
+ closest = vp;
+ }
+ });
+
+ activeVisionPoint = closest;
+ const hint = document.getElementById('vision-hint');
+ if (activeVisionPoint) {
+ document.getElementById('vision-hint-title').textContent = activeVisionPoint.config.title;
+ hint.style.display = 'flex';
+ } else {
+ hint.style.display = 'none';
+ }
+}
+
+function activateVisionPoint(vp) {
+ visionOverlayActive = true;
+ const overlay = document.getElementById('vision-overlay');
+ const titleDisplay = document.getElementById('vision-title-display');
+ const contentDisplay = document.getElementById('vision-content-display');
+ const statusDot = document.getElementById('vision-status-dot');
+
+ titleDisplay.textContent = vp.config.title.toUpperCase();
+ contentDisplay.textContent = vp.config.content;
+ statusDot.style.background = vp.config.color;
+ statusDot.style.boxShadow = `0 0 10px ${vp.config.color}`;
+
+ overlay.style.display = 'flex';
+}
+
+function closeVisionOverlay() {
+ visionOverlayActive = false;
+ document.getElementById('vision-overlay').style.display = 'none';
+}
+
+// ═══ PORTAL ATLAS ═══
+function openPortalAtlas() {
+ atlasOverlayActive = true;
+ document.getElementById('atlas-overlay').style.display = 'flex';
+ populateAtlas();
+}
+
+function closePortalAtlas() {
+ atlasOverlayActive = false;
+ document.getElementById('atlas-overlay').style.display = 'none';
+}
+
+function populateAtlas() {
+ const grid = document.getElementById('atlas-grid');
+ grid.innerHTML = '';
+
+ let onlineCount = 0;
+ let standbyCount = 0;
+
+ portals.forEach(portal => {
+ const config = portal.config;
+ if (config.status === 'online') onlineCount++;
+ if (config.status === 'standby') standbyCount++;
+
+ const card = document.createElement('div');
+ card.className = 'atlas-card';
+ card.style.setProperty('--portal-color', config.color);
+
+ const statusClass = `status-${config.status || 'online'}`;
+
+ card.innerHTML = `
+
+ ${config.description}
+
+ `;
+
+ card.addEventListener('click', () => {
+ focusPortal(portal);
+ closePortalAtlas();
+ });
+
+ grid.appendChild(card);
+ });
+
+ document.getElementById('atlas-online-count').textContent = onlineCount;
+ document.getElementById('atlas-standby-count').textContent = standbyCount;
+
+ // Update Bannerlord HUD status
+ const bannerlord = portals.find(p => p.config.id === 'bannerlord');
+ if (bannerlord) {
+ const statusEl = document.getElementById('bannerlord-status');
+ statusEl.className = 'hud-status-item ' + (bannerlord.config.status || 'offline');
+ }
+}
+
+function focusPortal(portal) {
+ // Teleport player to a position in front of the portal
+ const offset = new THREE.Vector3(0, 0, 6).applyEuler(new THREE.Euler(0, portal.config.rotation?.y || 0, 0));
+ playerPos.copy(portal.group.position).add(offset);
+ playerPos.y = 2; // Keep at eye level
+
+ // Rotate player to face the portal
+ playerRot.y = (portal.config.rotation?.y || 0) + Math.PI;
+ playerRot.x = 0;
+
+ addChatMessage('system', `Navigation focus: ${portal.config.name}`);
+
+ // If in orbit mode, reset target
+ if (NAV_MODES[navModeIdx] === 'orbit') {
+ orbitState.target.copy(portal.group.position);
+ orbitState.target.y = 3.5;
+ }
+}
+
+// ═══ GAME LOOP ═══
+let lastThoughtTime = 0;
+let pulseTimer = 0;
+
+function gameLoop() {
+ requestAnimationFrame(gameLoop);
+ const delta = Math.min(clock.getDelta(), 0.1);
+ const elapsed = clock.elapsedTime;
+
+ // Agent Thought Simulation
+ if (elapsed - lastThoughtTime > 4) {
+ lastThoughtTime = elapsed;
+ simulateAgentThought();
+ }
+
+ // Harness Pulse
+ pulseTimer += delta;
+ if (pulseTimer > 8) {
+ pulseTimer = 0;
+ triggerHarnessPulse();
+ }
+ if (harnessPulseMesh) {
+ harnessPulseMesh.scale.addScalar(delta * 15);
+ harnessPulseMesh.material.opacity = Math.max(0, harnessPulseMesh.material.opacity - delta * 0.5);
+ }
+
+ updateAshStorm(delta, elapsed);
+ updateNexusHeartbeat(delta, elapsed);
+ updateSymbolicAI(delta, elapsed);
+
+ const mode = NAV_MODES[navModeIdx];
+ const chatActive = document.activeElement === document.getElementById('chat-input');
+
+ if (mode === 'walk') {
+ if (!chatActive && !portalOverlayActive) {
+ const speed = 6 * delta;
+ const dir = new THREE.Vector3();
+ if (keys['w']) dir.z -= 1;
+ if (keys['s']) dir.z += 1;
+ if (keys['a']) dir.x -= 1;
+ if (keys['d']) dir.x += 1;
+ if (dir.length() > 0) {
+ dir.normalize().multiplyScalar(speed);
+ dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), playerRot.y);
+ playerPos.add(dir);
+ const maxR = 24;
+ const dist = Math.sqrt(playerPos.x * playerPos.x + playerPos.z * playerPos.z);
+ if (dist > maxR) { playerPos.x *= maxR / dist; playerPos.z *= maxR / dist; }
+ }
+ }
+ playerPos.y = 2;
+ camera.position.copy(playerPos);
+ camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
+
+ } else if (mode === 'orbit') {
+ if (!chatActive && !portalOverlayActive) {
+ const speed = 8 * delta;
+ const pan = new THREE.Vector3();
+ if (keys['w']) pan.z -= 1;
+ if (keys['s']) pan.z += 1;
+ if (keys['a']) pan.x -= 1;
+ if (keys['d']) pan.x += 1;
+ if (pan.length() > 0) {
+ pan.normalize().multiplyScalar(speed);
+ pan.applyAxisAngle(new THREE.Vector3(0, 1, 0), orbitState.theta);
+ orbitState.target.add(pan);
+ orbitState.target.y = Math.max(0, Math.min(20, orbitState.target.y));
+ }
+ }
+ const r = orbitState.radius;
+ camera.position.set(
+ orbitState.target.x + r * Math.sin(orbitState.phi) * Math.sin(orbitState.theta),
+ orbitState.target.y + r * Math.cos(orbitState.phi),
+ orbitState.target.z + r * Math.sin(orbitState.phi) * Math.cos(orbitState.theta)
+ );
+ camera.lookAt(orbitState.target);
+ playerPos.copy(camera.position);
+ playerRot.y = orbitState.theta;
+
+ } else if (mode === 'fly') {
+ if (!chatActive && !portalOverlayActive) {
+ const speed = 8 * delta;
+ const forward = new THREE.Vector3(-Math.sin(playerRot.y), 0, -Math.cos(playerRot.y));
+ const right = new THREE.Vector3( Math.cos(playerRot.y), 0, -Math.sin(playerRot.y));
+ if (keys['w']) playerPos.addScaledVector(forward, speed);
+ if (keys['s']) playerPos.addScaledVector(forward, -speed);
+ if (keys['a']) playerPos.addScaledVector(right, -speed);
+ if (keys['d']) playerPos.addScaledVector(right, speed);
+ if (keys['q'] || keys[' ']) flyY += speed;
+ if (keys['e'] || keys['shift']) flyY -= speed;
+ flyY = Math.max(0.5, Math.min(30, flyY));
+ playerPos.y = flyY;
+ }
+ camera.position.copy(playerPos);
+ camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
+ }
+
+ // Proximity check
+ checkPortalProximity();
+ checkVisionProximity();
+
+ const sky = scene.getObjectByName('skybox');
+ if (sky) sky.material.uniforms.uTime.value = elapsed;
+
+ batcaveTerminals.forEach(t => {
+ if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed;
+ });
+
+ // Animate Portals
+ portals.forEach(portal => {
+ portal.ring.rotation.z = elapsed * 0.3;
+ portal.ring.rotation.x = Math.sin(elapsed * 0.5) * 0.1;
+ if (portal.swirl.material.uniforms) {
+ portal.swirl.material.uniforms.uTime.value = elapsed;
+ }
+ // Pulse light
+ portal.light.intensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
+
+ // Custom animations for distinct identities
+ if (portal.config.id === 'archive' && portal.customElements.cubes) {
+ portal.customElements.cubes.forEach((cube, i) => {
+ cube.rotation.x += delta * (0.5 + i * 0.1);
+ cube.rotation.y += delta * (0.3 + i * 0.1);
+ const orbitSpeed = 0.5 + i * 0.2;
+ const orbitRadius = 4 + Math.sin(elapsed * 0.5 + i) * 0.5;
+ cube.position.x = Math.cos(elapsed * orbitSpeed + i) * orbitRadius;
+ cube.position.z = Math.sin(elapsed * orbitSpeed + i) * orbitRadius;
+ cube.position.y = 3.5 + Math.sin(elapsed * 1.2 + i) * 1.5;
+ });
+ }
+
+ if (portal.config.id === 'chapel' && portal.customElements.halo) {
+ portal.customElements.halo.rotation.z -= delta * 0.2;
+ portal.customElements.halo.scale.setScalar(1 + Math.sin(elapsed * 0.8) * 0.05);
+ portal.customElements.core.material.emissiveIntensity = 2 + Math.sin(elapsed * 3) * 1;
+ }
+
+ if (portal.config.id === 'courtyard' && portal.customElements.outerRing) {
+ portal.customElements.outerRing.rotation.z -= delta * 0.5;
+ portal.customElements.outerRing.rotation.y = Math.cos(elapsed * 0.4) * 0.2;
+ }
+
+ if (portal.config.id === 'gate' && portal.customElements.spikes) {
+ portal.customElements.spikes.forEach((spike, i) => {
+ const s = 1 + Math.sin(elapsed * 2 + i) * 0.2;
+ spike.scale.set(s, s, s);
+ });
+ }
+
+ // Animate particles
+ const positions = portal.pSystem.geometry.attributes.position.array;
+ for (let i = 0; i < positions.length / 3; i++) {
+ positions[i * 3 + 1] += Math.sin(elapsed + i) * 0.002;
+ }
+ portal.pSystem.geometry.attributes.position.needsUpdate = true;
+ });
+
+ // Animate Vision Points
+ visionPoints.forEach(vp => {
+ vp.crystal.rotation.y = elapsed * 0.8;
+ vp.crystal.rotation.x = Math.sin(elapsed * 0.5) * 0.2;
+ vp.crystal.position.y = 2.5 + Math.sin(elapsed * 1.5) * 0.2;
+ vp.ring.rotation.z = elapsed * 0.5;
+ vp.ring.scale.setScalar(1 + Math.sin(elapsed * 2) * 0.05);
+ vp.light.intensity = 1 + Math.sin(elapsed * 3) * 0.3;
+ });
+
+ // Animate Agents
+ agents.forEach((agent, i) => {
+ // Wander logic
+ agent.wanderTimer -= delta;
+ if (agent.wanderTimer <= 0) {
+ agent.wanderTimer = 3 + Math.random() * 5;
+ agent.targetPos.set(
+ agent.station.x + (Math.random() - 0.5) * 4,
+ 0,
+ agent.station.z + (Math.random() - 0.5) * 4
+ );
+ }
+ agent.group.position.lerp(agent.targetPos, delta * 0.5);
+
+ agent.orb.position.y = 3 + Math.sin(elapsed * 2 + i) * 0.15;
+ agent.halo.rotation.z = elapsed * 0.5;
+ agent.halo.scale.setScalar(1 + Math.sin(elapsed * 3 + i) * 0.1);
+ agent.orb.material.emissiveIntensity = 2 + Math.sin(elapsed * 4 + i) * 1;
+ });
+
+ // Animate Power Meter
+ powerMeterBars.forEach((bar, i) => {
+ const level = (Math.sin(elapsed * 2 + i * 0.5) * 0.5 + 0.5);
+ const active = level > (i / powerMeterBars.length);
+ bar.material.emissiveIntensity = active ? 2 : 0.2;
+ bar.material.opacity = active ? 0.9 : 0.3;
+ bar.scale.x = active ? 1.2 : 1.0;
+ });
+
+ if (thoughtStreamMesh) {
+ thoughtStreamMesh.material.uniforms.uTime.value = elapsed;
+ thoughtStreamMesh.rotation.y = elapsed * 0.05;
+ }
+
+ if (particles?.material?.uniforms) {
+ particles.material.uniforms.uTime.value = elapsed;
+ }
+ if (dustParticles) {
+ dustParticles.rotation.y = elapsed * 0.01;
+ }
+
+ for (let i = 0; i < 5; i++) {
+ const stone = scene.getObjectByName('runestone_' + i);
+ if (stone) {
+ stone.position.y = 5 + Math.sin(elapsed * 0.8 + i * 1.3) * 0.8;
+ stone.rotation.y = elapsed * 0.5 + i;
+ stone.rotation.x = elapsed * 0.3 + i * 0.7;
+ }
+ }
+
+ const core = scene.getObjectByName('nexus-core');
+ if (core) {
+ core.position.y = 2.5 + Math.sin(elapsed * 1.2) * 0.3;
+ core.rotation.y = elapsed * 0.4;
+ core.rotation.x = elapsed * 0.2;
+ core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
+ }
+
+ composer.render();
+
+ updateAshStorm(delta, elapsed);
+ updatePortalTunnel(delta, elapsed);
+
+ if (workshopScanMat) workshopScanMat.uniforms.uTime.value = clock.getElapsedTime();
+ if (activePortal !== lastFocusedPortal) {
+ lastFocusedPortal = activePortal;
+ refreshWorkshopPanel();
+ }
+
+ frameCount++;
+ const now = performance.now();
+ if (now - lastFPSTime >= 1000) {
+ fps = frameCount;
+ frameCount = 0;
+ lastFPSTime = now;
+ }
+ if (debugOverlay) {
+ const info = renderer.info;
+ debugOverlay.textContent =
+ `FPS: ${fps} Draw: ${info.render?.calls} Tri: ${info.render?.triangles} [${performanceTier}]\n` +
+ `Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`;
+ }
+ renderer.info.reset();
+}
+
+function onResize() {
+ const w = window.innerWidth;
+ const h = window.innerHeight;
+ camera.aspect = w / h;
+ camera.updateProjectionMatrix();
+ renderer.setSize(w, h);
+ composer.setSize(w, h);
+}
+
+// ═══ AGENT SIMULATION ═══
+function simulateAgentThought() {
+ const agentIds = ['timmy', 'kimi', 'claude', 'perplexity'];
+ const agentId = agentIds[Math.floor(Math.random() * agentIds.length)];
+ const thoughts = {
+ timmy: [
+ 'Analyzing portal stability...',
+ 'Sovereign nodes synchronized.',
+ 'Memory stream optimization complete.',
+ 'Scanning for external interference...',
+ 'The harness is humming beautifully.',
+ ],
+ kimi: [
+ 'Processing linguistic patterns...',
+ 'Context window expanded.',
+ 'Synthesizing creative output...',
+ 'Awaiting user prompt sequence.',
+ 'Neural weights adjusted.',
+ ],
+ claude: [
+ 'Reasoning through complex logic...',
+ 'Ethical guardrails verified.',
+ 'Refining thought architecture...',
+ 'Connecting disparate data points.',
+ 'Deep analysis in progress.',
+ ],
+ perplexity: [
+ 'Searching global knowledge graph...',
+ 'Verifying source citations...',
+ 'Synthesizing real-time data...',
+ 'Mapping information topology...',
+ 'Fact-checking active streams.',
+ ]
+ };
+
+ const thought = thoughts[agentId][Math.floor(Math.random() * thoughts[agentId].length)];
+ addAgentLog(agentId, thought);
+}
+
+function addAgentLog(agentId, text) {
+ const container = document.getElementById('agent-log-content');
+ if (!container) return;
+
+ const entry = document.createElement('div');
+ entry.className = 'agent-log-entry';
+ entry.innerHTML = `[${agentId.toUpperCase()}]${text}`;
+
+ container.prepend(entry);
+ if (container.children.length > 6) {
+ container.lastElementChild.remove();
+ }
+}
+
+function triggerHarnessPulse() {
+ if (!harnessPulseMesh) return;
+ harnessPulseMesh.scale.setScalar(0.1);
+ harnessPulseMesh.material.opacity = 0.8;
+
+ // Flash the core
+ const core = scene.getObjectByName('nexus-core');
+ if (core) {
+ core.material.emissiveIntensity = 10;
+ setTimeout(() => { if (core) core.material.emissiveIntensity = 2; }, 200);
+ }
+}
+
+// ═══ ASH STORM (MORROWIND) ═══
+let ashStormParticles;
+function createAshStorm() {
+ const count = 1000;
+ const geo = new THREE.BufferGeometry();
+ const pos = new Float32Array(count * 3);
+ const vel = new Float32Array(count * 3);
+
+ for (let i = 0; i < count; i++) {
+ pos[i * 3] = (Math.random() - 0.5) * 20;
+ pos[i * 3 + 1] = Math.random() * 10;
+ pos[i * 3 + 2] = (Math.random() - 0.5) * 20;
+
+ vel[i * 3] = -0.05 - Math.random() * 0.1;
+ vel[i * 3 + 1] = -0.02 - Math.random() * 0.05;
+ vel[i * 3 + 2] = (Math.random() - 0.5) * 0.05;
+ }
+
+ geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
+ geo.setAttribute('velocity', new THREE.BufferAttribute(vel, 3));
+
+ const mat = new THREE.PointsMaterial({
+ color: 0x886644,
+ size: 0.05,
+ transparent: true,
+ opacity: 0,
+ depthWrite: false,
+ blending: THREE.AdditiveBlending
+ });
+
+ ashStormParticles = new THREE.Points(geo, mat);
+ ashStormParticles.position.set(15, 0, -10); // Center on Morrowind portal
+ scene.add(ashStormParticles);
+}
+
+function updateAshStorm(delta, elapsed) {
+ if (!ashStormParticles) return;
+
+ const morrowindPortalPos = new THREE.Vector3(15, 0, -10);
+ const dist = playerPos.distanceTo(morrowindPortalPos);
+ const intensity = Math.max(0, 1 - (dist / 12));
+
+ ashStormParticles.material.opacity = intensity * 0.4;
+
+ if (intensity > 0) {
+ const pos = ashStormParticles.geometry.attributes.position.array;
+ const vel = ashStormParticles.geometry.attributes.velocity.array;
+
+ for (let i = 0; i < pos.length / 3; i++) {
+ pos[i * 3] += vel[i * 3];
+ pos[i * 3 + 1] += vel[i * 3 + 1];
+ pos[i * 3 + 2] += vel[i * 3 + 2];
+
+ if (pos[i * 3 + 1] < 0 || Math.abs(pos[i * 3]) > 10 || Math.abs(pos[i * 3 + 2]) > 10) {
+ pos[i * 3] = (Math.random() - 0.5) * 20;
+ pos[i * 3 + 1] = 10;
+ pos[i * 3 + 2] = (Math.random() - 0.5) * 20;
+ }
+ }
+ ashStormParticles.geometry.attributes.position.needsUpdate = true;
+ }
+}
+
+function updateNexusHeartbeat(delta, elapsed) {
+ const heartbeatVal = document.getElementById('heartbeat-value');
+ if (heartbeatVal) {
+ heartbeatVal.textContent = `${heartbeatFrequency.toFixed(2)} Hz`;
+ heartbeatVal.style.color = heartbeatSource === 'evonia-layer' ? '#4af0c0' : '#7b5cff';
+ }
+
+ // Breathing effect for ambient light
+ if (ambientLight) {
+ const intensity = 0.3 + Math.sin(elapsed * heartbeatFrequency * Math.PI) * 0.1 * heartbeatIntensity;
+ ambientLight.intensity = intensity;
+ }
+
+ // Update heartbeat pulse animation speed
+ const pulseEl = document.querySelector('.heartbeat-pulse');
+ if (pulseEl) {
+ pulseEl.style.animationDuration = `${1 / heartbeatFrequency}s`;
+ pulseEl.style.opacity = 0.4 + Math.sin(elapsed * heartbeatFrequency * Math.PI) * 0.4 * heartbeatIntensity;
+ }
+}
+
+init().then(() => {
+ createAshStorm();
+ createPortalTunnel();
+ fetchGiteaData();
+ setInterval(fetchGiteaData, 30000);
+});
diff --git a/public/nexus/index.html b/public/nexus/index.html
new file mode 100644
index 0000000..b6ce465
--- /dev/null
+++ b/public/nexus/index.html
@@ -0,0 +1,289 @@
+
+
+
+
+
+
+
+
+
+
+
+The Nexus — Timmy's Sovereign Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
THE NEXUS
+
Initializing Sovereign Space...
+
+
+
+
+
+
+
+
+
+
+
+
NEXUS PULSE
+
0.00 Hz
+
+
+
+
+
+ ◈
+ The Nexus
+
+
+
+
+
+
+
+ BANNERLORD
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [NEXUS] Sovereign space initialized. Timmy is observing.
+
+
+ [TIMMY] Welcome to the Nexus, Alexander. All systems nominal.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WASD move Mouse look Enter chat
+ V mode: WALK
+
+ HERMES:
+
+
+
+
+
+
+
+
+
+
+
+
+
SOVEREIGNTY
+
The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness.
+
+
+
+
+
+
+
+
+
MORROWIND
+
The Vvardenfell harness. Ash storms and ancient mysteries.
+
+
+
DESTINATION NOT YET LINKED
+
+
+
+
+
+
+
+
+
+
+
+
+
Enter The Nexus
+
Click anywhere to begin
+
+
+
+
+
+
+
+
+
+
+⚡ NEW DEPLOYMENT DETECTED — Reloading in 5s…
+
+
+
+
diff --git a/public/nexus/style.css b/public/nexus/style.css
new file mode 100644
index 0000000..d6e67a0
--- /dev/null
+++ b/public/nexus/style.css
@@ -0,0 +1,1063 @@
+/* === NEXUS DESIGN SYSTEM === */
+:root {
+ --font-display: 'Orbitron', sans-serif;
+ --font-body: 'JetBrains Mono', monospace;
+
+ --color-bg: #050510;
+ --color-surface: rgba(10, 15, 40, 0.85);
+ --color-border: rgba(74, 240, 192, 0.2);
+ --color-border-bright: rgba(74, 240, 192, 0.5);
+
+ --color-text: #e0f0ff;
+ --color-text-muted: #8a9ab8;
+ --color-text-bright: #ffffff;
+
+ --color-primary: #4af0c0;
+ --color-primary-dim: rgba(74, 240, 192, 0.3);
+ --color-secondary: #7b5cff;
+ --color-danger: #ff4466;
+ --color-warning: #ffaa22;
+ --color-gold: #ffd700;
+
+ --text-xs: 11px;
+ --text-sm: 13px;
+ --text-base: 15px;
+ --text-lg: 18px;
+ --text-xl: 24px;
+ --text-2xl: 36px;
+
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-6: 24px;
+ --space-8: 32px;
+
+ --panel-blur: 16px;
+ --panel-radius: 8px;
+ --transition-ui: 200ms cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+html, body {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background: var(--color-bg);
+ font-family: var(--font-body);
+ color: var(--color-text);
+ -webkit-font-smoothing: antialiased;
+}
+
+canvas#nexus-canvas {
+ display: block;
+ width: 100vw;
+ height: 100vh;
+ position: fixed;
+ top: 0;
+ left: 0;
+}
+
+/* === LOADING SCREEN === */
+#loading-screen {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ background: var(--color-bg);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.8s ease;
+}
+#loading-screen.fade-out {
+ opacity: 0;
+ pointer-events: none;
+}
+.loader-content {
+ text-align: center;
+}
+.loader-sigil {
+ margin-bottom: var(--space-6);
+}
+.loader-title {
+ font-family: var(--font-display);
+ font-size: var(--text-2xl);
+ font-weight: 700;
+ letter-spacing: 0.3em;
+ color: var(--color-primary);
+ text-shadow: 0 0 30px rgba(74, 240, 192, 0.4);
+ margin-bottom: var(--space-2);
+}
+.loader-subtitle {
+ font-size: var(--text-sm);
+ color: var(--color-text-muted);
+ letter-spacing: 0.1em;
+ margin-bottom: var(--space-6);
+}
+.loader-bar {
+ width: 200px;
+ height: 2px;
+ background: rgba(74, 240, 192, 0.15);
+ border-radius: 1px;
+ margin: 0 auto;
+ overflow: hidden;
+}
+.loader-fill {
+ height: 100%;
+ width: 0%;
+ background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
+ border-radius: 1px;
+ transition: width 0.3s ease;
+}
+
+/* === ENTER PROMPT === */
+#enter-prompt {
+ position: fixed;
+ inset: 0;
+ z-index: 500;
+ background: rgba(5, 5, 16, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: opacity 0.5s ease;
+}
+#enter-prompt.fade-out {
+ opacity: 0;
+ pointer-events: none;
+}
+.enter-content {
+ text-align: center;
+}
+.enter-content h2 {
+ font-family: var(--font-display);
+ font-size: var(--text-xl);
+ color: var(--color-primary);
+ letter-spacing: 0.2em;
+ text-shadow: 0 0 20px rgba(74, 240, 192, 0.3);
+ margin-bottom: var(--space-2);
+}
+.enter-content p {
+ font-size: var(--text-sm);
+ color: var(--color-text-muted);
+ animation: pulse-text 2s ease-in-out infinite;
+}
+@keyframes pulse-text {
+ 0%, 100% { opacity: 0.5; }
+ 50% { opacity: 1; }
+}
+
+/* === GAME UI (HUD) === */
+.game-ui {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 10;
+ font-family: var(--font-body);
+ color: var(--color-text);
+}
+.game-ui button, .game-ui input, .game-ui [data-interactive] {
+ pointer-events: auto;
+}
+
+/* Top Right Container */
+.hud-top-right {
+ position: absolute;
+ top: var(--space-3);
+ right: var(--space-3);
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: var(--space-3);
+ pointer-events: none;
+}
+.hud-top-right > * {
+ pointer-events: auto;
+}
+
+.hud-icon-btn {
+ background: rgba(10, 15, 40, 0.7);
+ border: 1px solid var(--color-primary);
+ color: var(--color-primary);
+ padding: 8px 12px;
+ font-family: var(--font-display);
+ font-size: 11px;
+ font-weight: 600;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ transition: all var(--transition-ui);
+ backdrop-filter: blur(5px);
+ box-shadow: 0 0 10px rgba(74, 240, 192, 0.2);
+ letter-spacing: 0.1em;
+}
+
+.hud-icon-btn:hover {
+ background: var(--color-primary);
+ color: var(--color-bg);
+ box-shadow: 0 0 20px var(--color-primary);
+}
+
+.hud-status-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: rgba(0, 0, 0, 0.5);
+ padding: 4px 12px;
+ border-radius: 20px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ font-family: var(--font-body);
+ font-size: 10px;
+ letter-spacing: 0.1em;
+ color: var(--color-text-muted);
+ margin-bottom: 8px;
+ pointer-events: auto;
+}
+
+.hud-status-item .status-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--color-danger);
+}
+
+.hud-status-item.online .status-dot {
+ background: var(--color-primary);
+ box-shadow: 0 0 5px var(--color-primary);
+}
+
+.hud-status-item.standby .status-dot {
+ background: var(--color-gold);
+ box-shadow: 0 0 5px var(--color-gold);
+}
+
+.hud-status-item.online .status-label {
+ color: #fff;
+}
+
+.hud-icon {
+ font-size: 16px;
+}
+
+/* Portal Atlas Overlay */
+.atlas-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(5, 5, 16, 0.9);
+ backdrop-filter: blur(15px);
+ z-index: 2000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px;
+ pointer-events: auto;
+ animation: fadeIn 0.3s ease;
+}
+
+.atlas-content {
+ width: 100%;
+ max-width: 1000px;
+ max-height: 80vh;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
+}
+
+.atlas-header {
+ padding: 20px 30px;
+ border-bottom: 1px solid var(--color-border);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.atlas-title {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.atlas-title h2 {
+ margin: 0;
+ font-family: var(--font-display);
+ letter-spacing: 2px;
+ color: var(--color-primary);
+ font-size: var(--text-lg);
+}
+
+.atlas-close-btn {
+ background: transparent;
+ border: 1px solid var(--color-danger);
+ color: var(--color-danger);
+ padding: 6px 15px;
+ font-family: var(--font-display);
+ font-size: 11px;
+ cursor: pointer;
+ transition: all var(--transition-ui);
+}
+
+.atlas-close-btn:hover {
+ background: var(--color-danger);
+ color: white;
+}
+
+.atlas-grid {
+ flex: 1;
+ overflow-y: auto;
+ padding: 30px;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 20px;
+}
+
+.atlas-card {
+ background: rgba(20, 30, 60, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 20px;
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+ overflow: hidden;
+}
+
+.atlas-card:hover {
+ background: rgba(30, 45, 90, 0.6);
+ border-color: var(--color-primary);
+ transform: translateY(-5px);
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+}
+
+.atlas-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 4px;
+ height: 100%;
+ background: var(--portal-color, var(--color-primary));
+ opacity: 0.6;
+}
+
+.atlas-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 12px;
+}
+
+.atlas-card-name {
+ font-family: var(--font-display);
+ font-size: 16px;
+ font-weight: 700;
+ color: #fff;
+}
+
+.atlas-card-status {
+ font-family: var(--font-body);
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 2px;
+ text-transform: uppercase;
+}
+
+.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
+.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
+.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); }
+
+.atlas-card-desc {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ line-height: 1.5;
+ margin-bottom: 15px;
+}
+
+.atlas-card-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-family: var(--font-body);
+ font-size: 10px;
+ color: rgba(160, 184, 208, 0.6);
+}
+
+.atlas-footer {
+ padding: 15px 30px;
+ border-top: 1px solid var(--color-border);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-family: var(--font-body);
+ font-size: 11px;
+}
+
+.status-indicator {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: 4px;
+}
+
+.status-indicator.online { background: var(--color-primary); box-shadow: 0 0 5px var(--color-primary); }
+.status-indicator.standby { background: var(--color-gold); box-shadow: 0 0 5px var(--color-gold); }
+
+.atlas-hint {
+ color: rgba(160, 184, 208, 0.5);
+ font-style: italic;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+/* Responsive Atlas */
+@media (max-width: 768px) {
+ .atlas-grid {
+ grid-template-columns: 1fr;
+ }
+ .atlas-content {
+ max-height: 90vh;
+ }
+}
+
+/* Debug overlay */
+.hud-top-left {
+ position: absolute;
+ top: var(--space-3);
+ left: var(--space-3);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ pointer-events: none;
+}
+
+.hud-debug {
+ background: rgba(0, 0, 0, 0.7);
+ color: #0f0;
+ font-size: var(--text-xs);
+ line-height: 1.5;
+ padding: var(--space-2) var(--space-3);
+ border-radius: 4px;
+ white-space: pre;
+ font-variant-numeric: tabular-nums lining-nums;
+}
+
+.hud-heartbeat {
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(4px);
+ border: 1px solid rgba(74, 240, 192, 0.2);
+ border-radius: 4px;
+ padding: 8px 12px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ pointer-events: auto;
+}
+
+.heartbeat-pulse {
+ width: 10px;
+ height: 10px;
+ background: var(--color-primary);
+ border-radius: 50%;
+ box-shadow: 0 0 10px var(--color-primary);
+ animation: pulse-heartbeat 2s infinite ease-in-out;
+}
+
+@keyframes pulse-heartbeat {
+ 0%, 100% { transform: scale(1); opacity: 0.8; }
+ 50% { transform: scale(1.4); opacity: 1; box-shadow: 0 0 15px var(--color-primary); }
+}
+
+.heartbeat-label {
+ font-family: var(--font-display);
+ font-size: 9px;
+ letter-spacing: 0.1em;
+ color: var(--color-text-muted);
+}
+
+.heartbeat-value {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--color-primary);
+ min-width: 45px;
+}
+
+/* Location indicator */
+.hud-location {
+ position: absolute;
+ top: var(--space-3);
+ left: 50%;
+ transform: translateX(-50%);
+ font-family: var(--font-display);
+ font-size: var(--text-sm);
+ font-weight: 500;
+ letter-spacing: 0.15em;
+ color: var(--color-primary);
+ text-shadow: 0 0 10px rgba(74, 240, 192, 0.3);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+.hud-location-icon {
+ font-size: 16px;
+ animation: spin-slow 10s linear infinite;
+}
+@keyframes spin-slow {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* Controls hint */
+.hud-controls {
+ position: absolute;
+ bottom: var(--space-3);
+ left: var(--space-3);
+ font-size: var(--text-xs);
+ color: var(--color-text-muted);
+ pointer-events: none;
+}
+.hud-controls span {
+ color: var(--color-primary);
+ font-weight: 600;
+}
+#nav-mode-label {
+ color: var(--color-gold);
+ font-weight: 700;
+ letter-spacing: 0.05em;
+}
+
+/* Portal Hint */
+.portal-hint {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, 100px);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ background: rgba(0, 0, 0, 0.8);
+ padding: var(--space-2) var(--space-4);
+ border: 1px solid var(--color-primary);
+ border-radius: 4px;
+ animation: hint-float 2s ease-in-out infinite;
+}
+@keyframes hint-float {
+ 0%, 100% { transform: translate(-50%, 100px); }
+ 50% { transform: translate(-50%, 90px); }
+}
+.portal-hint-key {
+ background: var(--color-primary);
+ color: var(--color-bg);
+ font-weight: 700;
+ padding: 2px 8px;
+ border-radius: 2px;
+}
+.portal-hint-text {
+ font-size: var(--text-sm);
+ font-weight: 500;
+ letter-spacing: 0.05em;
+}
+#portal-hint-name {
+ color: var(--color-primary);
+ font-weight: 700;
+}
+
+/* Agent Log HUD */
+.hud-agent-log, .hud-symbolic-log, .hud-blackboard-log, .hud-planner-log, .hud-cbr-log {
+ position: absolute;
+ right: var(--space-3);
+ width: 280px;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(8px);
+ border-left: 2px solid var(--color-primary);
+ padding: var(--space-3);
+ font-size: 10px;
+ pointer-events: none;
+}
+
+.hud-agent-log { top: var(--space-3); }
+.hud-symbolic-log { top: 160px; border-left-color: var(--color-gold); }
+.hud-blackboard-log { top: 320px; border-left-color: #7b5cff; }
+.hud-planner-log { top: 480px; border-left-color: #ff4a4a; }
+.hud-cbr-log { top: 640px; border-left-color: #4af0f0; }
+
+.agent-log-header, .symbolic-log-header, .blackboard-log-header, .planner-log-header, .cbr-log-header {
+ font-family: var(--font-display);
+ color: var(--color-primary);
+ letter-spacing: 0.1em;
+ margin-bottom: var(--space-2);
+ opacity: 0.8;
+}
+
+.symbolic-log-header { color: var(--color-gold); }
+.blackboard-log-header { color: #7b5cff; }
+.planner-log-header { color: #ff4a4a; }
+.cbr-log-header { color: #4af0f0; }
+
+.agent-log-content, .symbolic-log-content, .blackboard-log-content, .planner-log-content, .cbr-log-content {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.symbolic-log-entry, .blackboard-entry, .planner-step, .cbr-entry {
+ animation: log-fade-in 0.5s ease-out forwards;
+ opacity: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ padding: 4px;
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: 2px;
+}
+
+.symbolic-rule { color: var(--color-gold); font-weight: bold; }
+.symbolic-outcome { color: var(--color-primary); opacity: 0.8; }
+
+.bb-source { color: #7b5cff; font-weight: bold; font-size: 9px; }
+.bb-key { color: #fff; opacity: 0.6; }
+.bb-value { color: var(--color-primary); }
+
+.planner-step { flex-direction: row; gap: 8px; align-items: center; }
+.step-num { color: #ff4a4a; font-weight: bold; }
+.planner-empty { color: rgba(255, 255, 255, 0.3); font-style: italic; text-align: center; padding: 10px; }
+
+.cbr-match { color: #4af0f0; font-weight: bold; font-size: 9px; }
+.cbr-action { color: #fff; }
+.cbr-outcome { color: var(--color-primary); opacity: 0.6; font-style: italic; }
+.agent-log-entry {
+ animation: log-fade-in 0.5s ease-out forwards;
+ opacity: 0;
+}
+@keyframes log-fade-in {
+ from { opacity: 0; transform: translateX(10px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+.agent-log-tag {
+ font-weight: 700;
+ margin-right: 4px;
+}
+.tag-timmy { color: var(--color-primary); }
+.tag-kimi { color: var(--color-secondary); }
+.tag-claude { color: var(--color-gold); }
+.tag-perplexity { color: #4488ff; }
+.agent-log-text {
+ color: var(--color-text-muted);
+}
+
+/* Vision Hint */
+.vision-hint {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, 140px);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ background: rgba(0, 0, 0, 0.8);
+ padding: var(--space-2) var(--space-4);
+ border: 1px solid var(--color-gold);
+ border-radius: 4px;
+ animation: hint-float-vision 2s ease-in-out infinite;
+}
+@keyframes hint-float-vision {
+ 0%, 100% { transform: translate(-50%, 140px); }
+ 50% { transform: translate(-50%, 130px); }
+}
+.vision-hint-key {
+ background: var(--color-gold);
+ color: var(--color-bg);
+ font-weight: 700;
+ padding: 2px 8px;
+ border-radius: 2px;
+}
+.vision-hint-text {
+ font-size: var(--text-sm);
+ font-weight: 500;
+ letter-spacing: 0.05em;
+}
+#vision-hint-title {
+ color: var(--color-gold);
+ font-weight: 700;
+}
+
+/* Vision Overlay */
+.vision-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(5, 5, 16, 0.9);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: auto;
+ z-index: 1000;
+}
+.vision-overlay-content {
+ width: 100%;
+ max-width: 600px;
+ text-align: center;
+ padding: var(--space-8);
+ border: 1px solid var(--color-gold);
+ border-radius: var(--panel-radius);
+ background: var(--color-surface);
+ backdrop-filter: blur(var(--panel-blur));
+}
+.vision-overlay-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-3);
+ margin-bottom: var(--space-4);
+}
+.vision-overlay-status {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--color-gold);
+ box-shadow: 0 0 10px var(--color-gold);
+}
+.vision-overlay-title {
+ font-family: var(--font-display);
+ font-size: var(--text-sm);
+ letter-spacing: 0.2em;
+ color: var(--color-gold);
+}
+.vision-overlay-content h2 {
+ font-family: var(--font-display);
+ font-size: var(--text-2xl);
+ margin-bottom: var(--space-4);
+ letter-spacing: 0.1em;
+ color: var(--color-text-bright);
+}
+.vision-overlay-content p {
+ color: var(--color-text);
+ font-size: var(--text-lg);
+ line-height: 1.8;
+ margin-bottom: var(--space-8);
+ font-style: italic;
+}
+.vision-close-btn {
+ background: var(--color-gold);
+ color: var(--color-bg);
+ border: none;
+ padding: var(--space-2) var(--space-8);
+ border-radius: 4px;
+ font-family: var(--font-display);
+ font-weight: 700;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+.vision-close-btn:hover {
+ transform: scale(1.05);
+}
+
+/* Portal Activation Overlay */
+.portal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(5, 5, 16, 0.95);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: auto;
+ z-index: 1000;
+}
+.portal-overlay-content {
+ width: 100%;
+ max-width: 500px;
+ text-align: center;
+ padding: var(--space-8);
+}
+.portal-overlay-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-3);
+ margin-bottom: var(--space-4);
+}
+.portal-overlay-status {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--color-primary);
+ box-shadow: 0 0 10px var(--color-primary);
+}
+.portal-overlay-title {
+ font-family: var(--font-display);
+ font-size: var(--text-sm);
+ letter-spacing: 0.2em;
+ color: var(--color-primary);
+}
+.portal-overlay-content h2 {
+ font-family: var(--font-display);
+ font-size: var(--text-2xl);
+ margin-bottom: var(--space-4);
+ letter-spacing: 0.1em;
+}
+.portal-overlay-content p {
+ color: var(--color-text-muted);
+ font-size: var(--text-base);
+ line-height: 1.6;
+ margin-bottom: var(--space-8);
+}
+.portal-redirect-box {
+ border: 1px solid var(--color-primary-dim);
+ padding: var(--space-6);
+ border-radius: var(--panel-radius);
+}
+.portal-redirect-label {
+ font-size: var(--text-xs);
+ letter-spacing: 0.2em;
+ margin-bottom: var(--space-2);
+}
+.portal-redirect-timer {
+ font-family: var(--font-display);
+ font-size: 48px;
+ font-weight: 700;
+ color: var(--color-primary);
+}
+.portal-error-box {
+ border: 1px solid var(--color-danger);
+ padding: var(--space-6);
+ border-radius: var(--panel-radius);
+}
+.portal-error-msg {
+ color: var(--color-danger);
+ font-weight: 700;
+ margin-bottom: var(--space-4);
+}
+.portal-close-btn {
+ background: var(--color-danger);
+ color: white;
+ border: none;
+ padding: var(--space-2) var(--space-6);
+ border-radius: 4px;
+ font-family: var(--font-display);
+ cursor: pointer;
+}
+
+/* === CHAT PANEL === */
+.chat-panel {
+ position: absolute;
+ bottom: var(--space-4);
+ right: var(--space-4);
+ width: 380px;
+ max-height: 400px;
+ background: var(--color-surface);
+ backdrop-filter: blur(var(--panel-blur));
+ border: 1px solid var(--color-border);
+ border-radius: var(--panel-radius);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ pointer-events: auto;
+ transition: max-height var(--transition-ui);
+}
+.chat-panel.collapsed {
+ max-height: 42px;
+}
+.chat-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-3) var(--space-4);
+ border-bottom: 1px solid var(--color-border);
+ font-family: var(--font-display);
+ font-size: var(--text-xs);
+ letter-spacing: 0.1em;
+ font-weight: 500;
+ color: var(--color-text-bright);
+ cursor: pointer;
+ flex-shrink: 0;
+}
+.chat-status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--color-primary);
+ box-shadow: 0 0 6px var(--color-primary);
+ transition: all 0.3s ease;
+}
+@keyframes dot-pulse {
+ 0%, 100% { opacity: 0.6; }
+ 50% { opacity: 1; }
+}
+.chat-toggle-btn {
+ margin-left: auto;
+ background: none;
+ border: none;
+ color: var(--color-text-muted);
+ font-size: 14px;
+ cursor: pointer;
+ transition: transform var(--transition-ui);
+}
+.chat-panel.collapsed .chat-toggle-btn {
+ transform: rotate(180deg);
+}
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-3) var(--space-4);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ max-height: 280px;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(74,240,192,0.2) transparent;
+}
+
+.chat-quick-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ padding: 8px 12px;
+ border-top: 1px solid var(--color-border);
+ background: rgba(0, 0, 0, 0.3);
+ pointer-events: auto;
+}
+
+.quick-action-btn {
+ background: rgba(74, 240, 192, 0.1);
+ border: 1px solid var(--color-primary-dim);
+ color: var(--color-primary);
+ font-family: var(--font-body);
+ font-size: 10px;
+ padding: 4px 8px;
+ cursor: pointer;
+ transition: all var(--transition-ui);
+ white-space: nowrap;
+}
+
+.quick-action-btn:hover {
+ background: var(--color-primary-dim);
+ border-color: var(--color-primary);
+ color: #fff;
+}
+.chat-msg {
+ font-size: var(--text-xs);
+ line-height: 1.6;
+ padding: var(--space-1) 0;
+}
+.chat-msg-prefix {
+ font-weight: 700;
+}
+.chat-msg-kimi .chat-msg-prefix { color: var(--color-secondary); }
+.chat-msg-claude .chat-msg-prefix { color: var(--color-gold); }
+.chat-msg-perplexity .chat-msg-prefix { color: #4488ff; }
+
+/* Tool Output Styling */
+.chat-msg-tool {
+ background: rgba(0, 0, 0, 0.3);
+ border-left: 2px solid #ffd700;
+ font-size: 11px;
+ padding: 8px;
+ margin: 4px 0;
+ border-radius: 4px;
+}
+.tool-call { border-left-color: #ffd700; }
+.tool-result { border-left-color: #4af0c0; }
+.tool-content {
+ font-family: 'JetBrains Mono', monospace;
+ white-space: pre-wrap;
+ word-break: break-all;
+ opacity: 0.8;
+ margin: 4px 0 0 0;
+ color: #a0b8d0;
+}
+.chat-msg-system .chat-msg-prefix { color: var(--color-text-muted); }
+.chat-msg-timmy .chat-msg-prefix { color: var(--color-primary); }
+.chat-msg-user .chat-msg-prefix { color: var(--color-gold); }
+.chat-msg-error .chat-msg-prefix { color: var(--color-danger); }
+
+.chat-input-row {
+ display: flex;
+ border-top: 1px solid var(--color-border);
+ flex-shrink: 0;
+}
+.chat-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ padding: var(--space-3) var(--space-4);
+ font-family: var(--font-body);
+ font-size: var(--text-xs);
+ color: var(--color-text-bright);
+ outline: none;
+}
+.chat-input::placeholder {
+ color: var(--color-text-muted);
+}
+.chat-send-btn {
+ background: none;
+ border: none;
+ border-left: 1px solid var(--color-border);
+ padding: var(--space-3) var(--space-4);
+ color: var(--color-primary);
+ font-size: 16px;
+ cursor: pointer;
+ transition: background var(--transition-ui);
+}
+.chat-send-btn:hover {
+ background: rgba(74, 240, 192, 0.1);
+}
+
+/* === FOOTER === */
+.nexus-footer {
+ position: fixed;
+ bottom: var(--space-1);
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 5;
+ font-size: 10px;
+ opacity: 0.3;
+}
+.nexus-footer a {
+ color: var(--color-text-muted);
+ text-decoration: none;
+}
+.nexus-footer a:hover {
+ color: var(--color-primary);
+}
+
+/* Mobile adjustments */
+@media (max-width: 1024px) {
+ .chat-panel {
+ width: 320px;
+ }
+ .hud-agent-log {
+ width: 220px;
+ }
+}
+
+@media (max-width: 768px) {
+ .chat-panel {
+ width: 300px;
+ bottom: var(--space-2);
+ right: var(--space-2);
+ }
+ .hud-agent-log {
+ display: none;
+ }
+ .hud-location {
+ font-size: var(--text-xs);
+ }
+}
+
+@media (max-width: 480px) {
+ .chat-panel {
+ width: calc(100vw - 24px);
+ right: 12px;
+ bottom: 12px;
+ }
+ .hud-controls {
+ display: none;
+ }
+ .loader-title {
+ font-size: var(--text-xl);
+ }
+}