diff --git a/app.js b/app.js
index 28e9085a..b9b298a4 100644
--- a/app.js
+++ b/app.js
@@ -4,6 +4,7 @@ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
import { SpatialMemory } from './nexus/components/spatial-memory.js';
+import { SessionRooms } from './nexus/components/session-rooms.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -705,6 +706,7 @@ async function init() {
createWorkshopTerminal();
createAshStorm();
SpatialMemory.init(scene);
+ SessionRooms.init(scene, camera, null);
updateLoad(90);
loadSession();
@@ -1915,8 +1917,19 @@ function setupControls() {
}
}
+ // Priority 3: Session rooms (Mnemosyne #1171)
+ const roomMeshes = SessionRooms.getClickableMeshes();
+ if (roomMeshes.length > 0) {
+ const roomHits = raycaster.intersectObjects(roomMeshes, false);
+ if (roomHits.length > 0) {
+ const session = SessionRooms.handleRoomClick(roomHits[0].object);
+ if (session) { _showSessionRoomPanel(session); return; }
+ }
+ }
+
// Clicked empty space — dismiss panel
dismissMemoryPanel();
+ _dismissSessionRoomPanel();
}
}
});
@@ -2734,6 +2747,62 @@ function _dismissMemoryPanelForce() {
}, 200);
}
+/**
+ * Show the session room HUD panel when a chamber is entered.
+ * @param {object} session — { id, timestamp, facts[] }
+ */
+function _showSessionRoomPanel(session) {
+ const panel = document.getElementById('session-room-panel');
+ if (!panel) return;
+
+ const dt = session.timestamp ? new Date(session.timestamp) : new Date();
+ const tsEl = document.getElementById('session-room-timestamp');
+ if (tsEl) tsEl.textContent = isNaN(dt.getTime()) ? session.id : dt.toLocaleString();
+
+ const countEl = document.getElementById('session-room-fact-count');
+ const facts = session.facts || [];
+ if (countEl) countEl.textContent = facts.length + (facts.length === 1 ? ' fact' : ' facts') + ' in this chamber';
+
+ const listEl = document.getElementById('session-room-facts');
+ if (listEl) {
+ listEl.innerHTML = '';
+ facts.slice(0, 8).forEach(f => {
+ const item = document.createElement('div');
+ item.className = 'session-room-fact-item';
+ item.textContent = f.content || f.id || '(unknown)';
+ item.title = f.content || '';
+ listEl.appendChild(item);
+ });
+ if (facts.length > 8) {
+ const more = document.createElement('div');
+ more.className = 'session-room-fact-item';
+ more.style.color = 'rgba(200,180,255,0.4)';
+ more.textContent = '\u2026 ' + (facts.length - 8) + ' more';
+ listEl.appendChild(more);
+ }
+ }
+
+ // Close button
+ const closeBtn = document.getElementById('session-room-close');
+ if (closeBtn) closeBtn.onclick = () => _dismissSessionRoomPanel();
+
+ panel.classList.remove('session-panel-fade-out');
+ panel.style.display = 'block';
+}
+
+/**
+ * Dismiss the session room panel.
+ */
+function _dismissSessionRoomPanel() {
+ const panel = document.getElementById('session-room-panel');
+ if (!panel || panel.style.display === 'none') return;
+ panel.classList.add('session-panel-fade-out');
+ setTimeout(() => {
+ panel.style.display = 'none';
+ panel.classList.remove('session-panel-fade-out');
+ }, 200);
+}
+
function gameLoop() {
requestAnimationFrame(gameLoop);
@@ -2765,6 +2834,9 @@ function gameLoop() {
animateMemoryOrbs(delta);
}
+ // Project Mnemosyne - Session Rooms (#1171)
+ SessionRooms.update(delta);
+
const mode = NAV_MODES[navModeIdx];
const chatActive = document.activeElement === document.getElementById('chat-input');
@@ -3294,6 +3366,37 @@ init().then(() => {
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
SpatialMemory.runGravityLayout();
+ // Project Mnemosyne — seed demo session rooms (#1171)
+ // Sessions group facts by conversation/work session with a timestamp.
+ const demoSessions = [
+ {
+ id: 'session_2026_03_01',
+ timestamp: '2026-03-01T10:00:00.000Z',
+ facts: [
+ { id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95 },
+ { id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9 },
+ ]
+ },
+ {
+ id: 'session_2026_03_15',
+ timestamp: '2026-03-15T14:30:00.000Z',
+ facts: [
+ { id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85 },
+ { id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7 },
+ { id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain homes', category: 'engineering', strength: 0.8 },
+ ]
+ },
+ {
+ id: 'session_2026_04_10',
+ timestamp: '2026-04-10T09:00:00.000Z',
+ facts: [
+ { id: 'mem_session_rooms', content: 'Session rooms introduced — holographic chambers per session', category: 'projects', strength: 0.88 },
+ { id: 'mem_gravity_wells', content: 'Gravity-well clustering bakes crystal positions on load', category: 'engineering', strength: 0.75 },
+ ]
+ }
+ ];
+ SessionRooms.updateSessions(demoSessions);
+
fetchGiteaData();
setInterval(fetchGiteaData, 30000);
runWeeklyAudit();
diff --git a/index.html b/index.html
index b7e3c0b1..54ada489 100644
--- a/index.html
+++ b/index.html
@@ -236,6 +236,21 @@
+
+
diff --git a/nexus/components/session-rooms.js b/nexus/components/session-rooms.js
new file mode 100644
index 00000000..bb8a470a
--- /dev/null
+++ b/nexus/components/session-rooms.js
@@ -0,0 +1,413 @@
+// ═══════════════════════════════════════════════════════
+// PROJECT MNEMOSYNE — SESSION ROOMS (Issue #1171)
+// ═══════════════════════════════════════════════════════
+//
+// Groups memories by session into holographic chambers.
+// Each session becomes a wireframe cube floating in space.
+// Rooms are arranged chronologically along a spiral.
+// Click a room to fly inside; distant rooms LOD to a point.
+//
+// Usage from app.js:
+// SessionRooms.init(scene, camera, controls);
+// SessionRooms.updateSessions(sessions); // [{id, timestamp, facts[]}]
+// SessionRooms.update(delta); // call each frame
+// SessionRooms.getClickableMeshes(); // for raycasting
+// SessionRooms.handleRoomClick(mesh); // trigger fly-in
+// ═══════════════════════════════════════════════════════
+
+const SessionRooms = (() => {
+
+ // ─── CONSTANTS ───────────────────────────────────────
+ const MAX_ROOMS = 20;
+ const ROOM_SIZE = 9; // wireframe cube edge length
+ const ROOM_HALF = ROOM_SIZE / 2;
+ const LOD_THRESHOLD = 55; // distance: full → point
+ const LOD_HYSTERESIS = 5; // buffer to avoid flicker
+ const SPIRAL_BASE_R = 20; // spiral inner radius
+ const SPIRAL_R_STEP = 5; // radius growth per room
+ const SPIRAL_ANGLE_INC = 2.399; // golden angle (radians)
+ const SPIRAL_Y_STEP = 1.5; // vertical rise per room
+ const FLY_DURATION = 1.5; // seconds for fly-in tween
+ const FLY_TARGET_DEPTH = ROOM_HALF - 1.5; // how deep inside to stop
+
+ const ROOM_COLOR = 0x7b5cff; // violet — mnemosyne accent
+ const POINT_COLOR = 0x9b7cff;
+ const LABEL_COLOR = '#c8b4ff';
+ const STORAGE_KEY = 'mnemosyne_sessions_v1';
+
+ // ─── STATE ────────────────────────────────────────────
+ let _scene = null;
+ let _camera = null;
+ let _controls = null;
+
+ let _rooms = []; // array of room objects
+ let _sessionIndex = {}; // id → room object
+
+ // Fly-in tween state
+ let _flyActive = false;
+ let _flyElapsed = 0;
+ let _flyFrom = null;
+ let _flyTo = null;
+ let _flyLookFrom = null;
+ let _flyLookTo = null;
+ let _flyActiveRoom = null;
+
+ // ─── SPIRAL POSITION ──────────────────────────────────
+ function _spiralPos(index) {
+ const angle = index * SPIRAL_ANGLE_INC;
+ const r = SPIRAL_BASE_R + index * SPIRAL_R_STEP;
+ const y = index * SPIRAL_Y_STEP;
+ return new THREE.Vector3(
+ Math.cos(angle) * r,
+ y,
+ Math.sin(angle) * r
+ );
+ }
+
+ // ─── CREATE ROOM ──────────────────────────────────────
+ function _createRoom(session, index) {
+ const pos = _spiralPos(index);
+ const group = new THREE.Group();
+ group.position.copy(pos);
+
+ // Wireframe cube
+ const boxGeo = new THREE.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE);
+ const edgesGeo = new THREE.EdgesGeometry(boxGeo);
+ const edgesMat = new THREE.LineBasicMaterial({
+ color: ROOM_COLOR,
+ transparent: true,
+ opacity: 0.55
+ });
+ const wireframe = new THREE.LineSegments(edgesGeo, edgesMat);
+ wireframe.userData = { type: 'session_room_wireframe', sessionId: session.id };
+ group.add(wireframe);
+
+ // Collision mesh (invisible, for raycasting)
+ const hitGeo = new THREE.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE);
+ const hitMat = new THREE.MeshBasicMaterial({
+ visible: false,
+ transparent: true,
+ opacity: 0,
+ side: THREE.FrontSide
+ });
+ const hitMesh = new THREE.Mesh(hitGeo, hitMat);
+ hitMesh.userData = { type: 'session_room', sessionId: session.id, roomIndex: index };
+ group.add(hitMesh);
+
+ // LOD point (small sphere shown at distance)
+ const pointGeo = new THREE.SphereGeometry(0.5, 6, 4);
+ const pointMat = new THREE.MeshBasicMaterial({
+ color: POINT_COLOR,
+ transparent: true,
+ opacity: 0.7
+ });
+ const pointMesh = new THREE.Mesh(pointGeo, pointMat);
+ pointMesh.userData = { type: 'session_room_point', sessionId: session.id };
+ pointMesh.visible = false; // starts hidden; shown only at LOD distance
+ group.add(pointMesh);
+
+ // Timestamp billboard sprite
+ const sprite = _makeTimestampSprite(session.timestamp, session.facts.length);
+ sprite.position.set(0, ROOM_HALF + 1.2, 0);
+ group.add(sprite);
+
+ // Inner ambient glow
+ const glow = new THREE.PointLight(ROOM_COLOR, 0.4, ROOM_SIZE * 1.2);
+ group.add(glow);
+
+ _scene.add(group);
+
+ const room = {
+ session,
+ group,
+ wireframe,
+ hitMesh,
+ pointMesh,
+ sprite,
+ glow,
+ pos: pos.clone(),
+ index,
+ lodActive: false,
+ pulsePhase: Math.random() * Math.PI * 2
+ };
+
+ _rooms.push(room);
+ _sessionIndex[session.id] = room;
+
+ console.info('[SessionRooms] Created room for session', session.id, 'at index', index);
+ return room;
+ }
+
+ // ─── TIMESTAMP SPRITE ────────────────────────────────
+ function _makeTimestampSprite(isoTimestamp, factCount) {
+ const canvas = document.createElement('canvas');
+ canvas.width = 320;
+ canvas.height = 72;
+ const ctx = canvas.getContext('2d');
+
+ // Background pill
+ ctx.clearRect(0, 0, 320, 72);
+ ctx.fillStyle = 'rgba(20, 10, 40, 0.82)';
+ _roundRect(ctx, 4, 4, 312, 64, 14);
+ ctx.fill();
+
+ // Border
+ ctx.strokeStyle = 'rgba(123, 92, 255, 0.6)';
+ ctx.lineWidth = 1.5;
+ _roundRect(ctx, 4, 4, 312, 64, 14);
+ ctx.stroke();
+
+ // Timestamp text
+ const dt = isoTimestamp ? new Date(isoTimestamp) : new Date();
+ const label = _formatDate(dt);
+ ctx.fillStyle = LABEL_COLOR;
+ ctx.font = 'bold 15px monospace';
+ ctx.textAlign = 'center';
+ ctx.fillText(label, 160, 30);
+
+ // Fact count
+ ctx.fillStyle = 'rgba(200, 180, 255, 0.65)';
+ ctx.font = '12px monospace';
+ ctx.fillText(factCount + (factCount === 1 ? ' fact' : ' facts'), 160, 52);
+
+ const tex = new THREE.CanvasTexture(canvas);
+ const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.88 });
+ const sprite = new THREE.Sprite(mat);
+ sprite.scale.set(5, 1.1, 1);
+ sprite.userData = { type: 'session_room_label' };
+ return sprite;
+ }
+
+ // ─── HELPERS ──────────────────────────────────────────
+ function _roundRect(ctx, x, y, w, h, r) {
+ ctx.beginPath();
+ ctx.moveTo(x + r, y);
+ ctx.lineTo(x + w - r, y);
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
+ ctx.lineTo(x + w, y + h - r);
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
+ ctx.lineTo(x + r, y + h);
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
+ ctx.lineTo(x, y + r);
+ ctx.quadraticCurveTo(x, y, x + r, y);
+ ctx.closePath();
+ }
+
+ function _formatDate(dt) {
+ if (isNaN(dt.getTime())) return 'Unknown session';
+ const pad = n => String(n).padStart(2, '0');
+ return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
+ }
+
+ // ─── DISPOSE ROOM ────────────────────────────────────
+ function _disposeRoom(room) {
+ room.wireframe.geometry.dispose();
+ room.wireframe.material.dispose();
+ room.hitMesh.geometry.dispose();
+ room.hitMesh.material.dispose();
+ room.pointMesh.geometry.dispose();
+ room.pointMesh.material.dispose();
+ if (room.sprite.material.map) room.sprite.material.map.dispose();
+ room.sprite.material.dispose();
+ if (room.group.parent) room.group.parent.remove(room.group);
+ delete _sessionIndex[room.session.id];
+ }
+
+ // ─── PUBLIC: UPDATE SESSIONS ─────────────────────────
+ // sessions: [{id, timestamp, facts:[{id,content,category,strength,...}]}]
+ // Sorted chronologically oldest→newest; max MAX_ROOMS shown.
+ function updateSessions(sessions) {
+ if (!_scene) return;
+
+ const sorted = [...sessions]
+ .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
+ .slice(-MAX_ROOMS); // keep most recent MAX_ROOMS
+
+ // Remove rooms no longer present
+ const incoming = new Set(sorted.map(s => s.id));
+ for (let i = _rooms.length - 1; i >= 0; i--) {
+ const room = _rooms[i];
+ if (!incoming.has(room.session.id)) {
+ _disposeRoom(room);
+ _rooms.splice(i, 1);
+ }
+ }
+
+ // Add / update
+ sorted.forEach((session, idx) => {
+ if (_sessionIndex[session.id]) {
+ // Update position if index changed
+ const room = _sessionIndex[session.id];
+ if (room.index !== idx) {
+ room.index = idx;
+ const newPos = _spiralPos(idx);
+ room.group.position.copy(newPos);
+ room.pos.copy(newPos);
+ }
+ } else {
+ _createRoom(session, idx);
+ }
+ });
+
+ saveToStorage(sorted);
+ console.info('[SessionRooms] Updated:', _rooms.length, 'session rooms');
+ }
+
+ // ─── PUBLIC: INIT ─────────────────────────────────────
+ function init(scene, camera, controls) {
+ _scene = scene;
+ _camera = camera;
+ _controls = controls;
+ console.info('[SessionRooms] Initialized');
+
+ // Restore persisted sessions
+ const saved = loadFromStorage();
+ if (saved && saved.length > 0) {
+ updateSessions(saved);
+ }
+ }
+
+ // ─── PUBLIC: UPDATE (per-frame) ───────────────────────
+ function update(delta) {
+ if (!_scene || !_camera) return;
+
+ const camPos = _camera.position;
+
+ _rooms.forEach(room => {
+ const dist = camPos.distanceTo(room.pos);
+
+ // LOD toggle
+ const threshold = room.lodActive
+ ? LOD_THRESHOLD + LOD_HYSTERESIS // must come closer to exit LOD
+ : LOD_THRESHOLD;
+
+ if (dist > threshold && !room.lodActive) {
+ room.lodActive = true;
+ room.wireframe.visible = false;
+ room.sprite.visible = false;
+ room.pointMesh.visible = true;
+ } else if (dist <= threshold && room.lodActive) {
+ room.lodActive = false;
+ room.wireframe.visible = true;
+ room.sprite.visible = true;
+ room.pointMesh.visible = false;
+ }
+
+ // Pulse wireframe opacity
+ room.pulsePhase += delta * 0.6;
+ if (!room.lodActive) {
+ room.wireframe.material.opacity = 0.3 + Math.sin(room.pulsePhase) * 0.2;
+ room.glow.intensity = 0.3 + Math.sin(room.pulsePhase * 1.4) * 0.15;
+ }
+
+ // Slowly rotate each room
+ room.group.rotation.y += delta * 0.04;
+ });
+
+ // Fly-in tween
+ if (_flyActive) {
+ _flyElapsed += delta;
+ const t = Math.min(_flyElapsed / FLY_DURATION, 1);
+ const ease = _easeInOut(t);
+
+ _camera.position.lerpVectors(_flyFrom, _flyTo, ease);
+
+ // Interpolate lookAt
+ const lookNow = new THREE.Vector3().lerpVectors(_flyLookFrom, _flyLookTo, ease);
+ _camera.lookAt(lookNow);
+ if (_controls && _controls.target) _controls.target.copy(lookNow);
+
+ if (t >= 1) {
+ _flyActive = false;
+ if (_controls && typeof _controls.update === 'function') _controls.update();
+ console.info('[SessionRooms] Fly-in complete for session', _flyActiveRoom && _flyActiveRoom.session.id);
+ _flyActiveRoom = null;
+ }
+ }
+ }
+
+ // ─── EASING ───────────────────────────────────────────
+ function _easeInOut(t) {
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
+ }
+
+ // ─── PUBLIC: GET CLICKABLE MESHES ─────────────────────
+ function getClickableMeshes() {
+ return _rooms.map(r => r.hitMesh);
+ }
+
+ // ─── PUBLIC: HANDLE ROOM CLICK ────────────────────────
+ function handleRoomClick(mesh) {
+ const { sessionId } = mesh.userData;
+ const room = _sessionIndex[sessionId];
+ if (!room || !_camera) return null;
+
+ // Fly into the room from the front face
+ _flyActive = true;
+ _flyElapsed = 0;
+ _flyActiveRoom = room;
+
+ _flyFrom = _camera.position.clone();
+
+ // Target: step inside the room toward its center
+ const dir = room.pos.clone().sub(_camera.position).normalize();
+ _flyTo = room.pos.clone().add(dir.multiplyScalar(FLY_TARGET_DEPTH));
+
+ _flyLookFrom = _controls && _controls.target
+ ? _controls.target.clone()
+ : _camera.position.clone().add(_camera.getWorldDirection(new THREE.Vector3()));
+ _flyLookTo = room.pos.clone();
+
+ console.info('[SessionRooms] Flying into session room:', sessionId);
+ return room.session;
+ }
+
+ // ─── PERSISTENCE ──────────────────────────────────────
+ function saveToStorage(sessions) {
+ if (typeof localStorage === 'undefined') return;
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({ v: 1, sessions }));
+ } catch (e) {
+ console.warn('[SessionRooms] Failed to save to localStorage:', e);
+ }
+ }
+
+ function loadFromStorage() {
+ if (typeof localStorage === 'undefined') return null;
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw);
+ if (!parsed || parsed.v !== 1 || !Array.isArray(parsed.sessions)) return null;
+ console.info('[SessionRooms] Restored', parsed.sessions.length, 'sessions from localStorage');
+ return parsed.sessions;
+ } catch (e) {
+ console.warn('[SessionRooms] Failed to load from localStorage:', e);
+ return null;
+ }
+ }
+
+ function clearStorage() {
+ if (typeof localStorage !== 'undefined') {
+ localStorage.removeItem(STORAGE_KEY);
+ console.info('[SessionRooms] Cleared localStorage');
+ }
+ }
+
+ // ─── PUBLIC API ───────────────────────────────────────
+ return {
+ init,
+ updateSessions,
+ update,
+ getClickableMeshes,
+ handleRoomClick,
+ clearStorage,
+ // For external inspection
+ getRooms: () => _rooms,
+ getSession: (id) => _sessionIndex[id] || null,
+ isFlyActive: () => _flyActive
+ };
+
+})();
+
+export { SessionRooms };
diff --git a/style.css b/style.css
index 4d01e4a8..2c3f70eb 100644
--- a/style.css
+++ b/style.css
@@ -1461,3 +1461,122 @@ canvas#nexus-canvas {
gap: 2px;
}
+/* ═══════════════════════════════════════════════════════
+ PROJECT MNEMOSYNE — SESSION ROOM HUD PANEL (#1171)
+═══════════════════════════════════════════════════════ */
+
+.session-room-panel {
+ position: fixed;
+ bottom: 24px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 125;
+ animation: sessionPanelIn 0.25s ease-out forwards;
+}
+
+.session-room-panel.session-panel-fade-out {
+ animation: sessionPanelOut 0.2s ease-in forwards !important;
+}
+
+@keyframes sessionPanelIn {
+ from { opacity: 0; transform: translateX(-50%) translateY(12px); }
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
+}
+
+@keyframes sessionPanelOut {
+ from { opacity: 1; }
+ to { opacity: 0; transform: translateX(-50%) translateY(10px); }
+}
+
+.session-room-panel-content {
+ min-width: 320px;
+ max-width: 480px;
+ background: rgba(8, 4, 28, 0.93);
+ backdrop-filter: blur(14px);
+ border: 1px solid rgba(123, 92, 255, 0.35);
+ border-radius: 12px;
+ padding: 14px 18px;
+ box-shadow: 0 0 32px rgba(123, 92, 255, 0.1), 0 8px 32px rgba(0, 0, 0, 0.45);
+}
+
+.session-room-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.07);
+}
+
+.session-room-icon {
+ font-size: 14px;
+ line-height: 1;
+}
+
+.session-room-title {
+ font-family: var(--font-display, monospace);
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ color: #9b7cff;
+ text-transform: uppercase;
+ flex: 1;
+}
+
+.session-room-close {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.35);
+ cursor: pointer;
+ font-size: 14px;
+ padding: 0 2px;
+ line-height: 1;
+ transition: color 0.15s;
+}
+
+.session-room-close:hover {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.session-room-timestamp {
+ font-family: var(--font-display, monospace);
+ font-size: 13px;
+ color: #c8b4ff;
+ margin-bottom: 6px;
+ letter-spacing: 0.08em;
+}
+
+.session-room-fact-count {
+ font-size: 11px;
+ color: rgba(200, 180, 255, 0.55);
+ margin-bottom: 10px;
+}
+
+.session-room-facts {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ max-height: 140px;
+ overflow-y: auto;
+}
+
+.session-room-fact-item {
+ font-size: 11px;
+ color: rgba(220, 210, 255, 0.75);
+ padding: 4px 8px;
+ background: rgba(123, 92, 255, 0.07);
+ border-left: 2px solid rgba(123, 92, 255, 0.4);
+ border-radius: 0 4px 4px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.session-room-hint {
+ margin-top: 10px;
+ font-size: 10px;
+ color: rgba(200, 180, 255, 0.35);
+ text-align: center;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+