Section: {sectionInfo?.sectionTitle || 'Unknown section'}
diff --git a/src/components/focus-group-session/RoundTable3D.tsx b/src/components/focus-group-session/RoundTable3D.tsx
new file mode 100644
index 00000000..ef9cc392
--- /dev/null
+++ b/src/components/focus-group-session/RoundTable3D.tsx
@@ -0,0 +1,669 @@
+import { useEffect, useRef, useState, useCallback } from 'react';
+import * as THREE from 'three';
+import { Persona } from '@/types/persona';
+import { Message } from './types';
+
+interface RoundTable3DProps {
+ personas: Persona[];
+ activeSpeakerId: string | null;
+ messages: Message[];
+}
+
+interface AvatarObject {
+ mesh: THREE.Mesh;
+ ring: THREE.Mesh;
+ haloLight: THREE.PointLight;
+ inactiveLight: THREE.PointLight;
+ spotLight: THREE.SpotLight;
+ targetScale: number;
+ currentScale: number;
+}
+
+interface SpeechBubble {
+ personaId: string;
+ text: string;
+ screenX: number;
+ screenY: number;
+}
+
+interface NameTag {
+ personaId: string;
+ name: string;
+ screenX: number;
+ screenY: number;
+}
+
+function nameToColor(name: string): string {
+ let hash = 0;
+ for (let i = 0; i < name.length; i++) {
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
+ hash = hash & hash;
+ }
+ const hue = Math.abs(hash) % 360;
+ return `hsl(${hue}, 60%, 35%)`;
+}
+
+function nameToHex(name: string): number {
+ let hash = 0;
+ for (let i = 0; i < name.length; i++) {
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
+ hash = hash & hash;
+ }
+ const hue = Math.abs(hash) % 360;
+ const h = hue / 360;
+ const s = 0.6;
+ const l = 0.35;
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ const hue2rgb = (p2: number, q2: number, t: number) => {
+ let tt = t;
+ if (tt < 0) tt += 1;
+ if (tt > 1) tt -= 1;
+ if (tt < 1 / 6) return p2 + (q2 - p2) * 6 * tt;
+ if (tt < 1 / 2) return q2;
+ if (tt < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - tt) * 6;
+ return p2;
+ };
+ const r = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
+ const g = Math.round(hue2rgb(p, q, h) * 255);
+ const b = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
+ return (r << 16) | (g << 8) | b;
+}
+
+function makeAvatarTexture(initial: string, color: string): THREE.CanvasTexture {
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 256;
+ const ctx = canvas.getContext('2d')!;
+
+ // Background circle with gradient
+ const gradient = ctx.createRadialGradient(128, 100, 0, 128, 128, 128);
+ gradient.addColorStop(0, lightenColor(color, 20));
+ gradient.addColorStop(1, darkenColor(color, 20));
+ ctx.fillStyle = gradient;
+ ctx.beginPath();
+ ctx.arc(128, 128, 128, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Rim highlight
+ ctx.strokeStyle = 'rgba(255,255,255,0.15)';
+ ctx.lineWidth = 4;
+ ctx.beginPath();
+ ctx.arc(128, 128, 122, Math.PI * 1.1, Math.PI * 1.9);
+ ctx.stroke();
+
+ // Letter
+ ctx.fillStyle = 'rgba(255,255,255,0.92)';
+ ctx.font = 'bold 110px system-ui, sans-serif';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(initial, 128, 136);
+
+ return new THREE.CanvasTexture(canvas);
+}
+
+function lightenColor(hsl: string, amount: number): string {
+ return hsl.replace(/(\d+)%\)$/, (_, l) => `${Math.min(100, parseInt(l) + amount)}%)`);
+}
+
+function darkenColor(hsl: string, amount: number): string {
+ return hsl.replace(/(\d+)%\)$/, (_, l) => `${Math.max(0, parseInt(l) - amount)}%)`);
+}
+
+function makeTableTexture(): THREE.CanvasTexture {
+ const canvas = document.createElement('canvas');
+ canvas.width = 512;
+ canvas.height = 512;
+ const ctx = canvas.getContext('2d')!;
+
+ // Dark wood base
+ ctx.fillStyle = '#120d08';
+ ctx.fillRect(0, 0, 512, 512);
+
+ // Wood grain lines
+ for (let i = 0; i < 40; i++) {
+ const y = Math.random() * 512;
+ ctx.strokeStyle = `rgba(${60 + Math.random() * 20}, ${35 + Math.random() * 10}, ${15 + Math.random() * 8}, ${0.3 + Math.random() * 0.4})`;
+ ctx.lineWidth = 0.5 + Math.random() * 1.5;
+ ctx.beginPath();
+ ctx.moveTo(0, y + Math.sin(0) * 8);
+ for (let x = 0; x < 512; x += 10) {
+ ctx.lineTo(x, y + Math.sin(x * 0.02) * 12 + Math.random() * 3);
+ }
+ ctx.stroke();
+ }
+
+ // Subtle amber center glow
+ const radialGrad = ctx.createRadialGradient(256, 256, 0, 256, 256, 200);
+ radialGrad.addColorStop(0, 'rgba(251, 191, 36, 0.08)');
+ radialGrad.addColorStop(1, 'rgba(0,0,0,0)');
+ ctx.fillStyle = radialGrad;
+ ctx.fillRect(0, 0, 512, 512);
+
+ return new THREE.CanvasTexture(canvas);
+}
+
+function makeEmblemTexture(): THREE.CanvasTexture {
+ const canvas = document.createElement('canvas');
+ canvas.width = 128;
+ canvas.height = 128;
+ const ctx = canvas.getContext('2d')!;
+
+ ctx.clearRect(0, 0, 128, 128);
+
+ // Outer circle
+ ctx.strokeStyle = 'rgba(251, 191, 36, 0.7)';
+ ctx.lineWidth = 2.5;
+ ctx.beginPath();
+ ctx.arc(64, 64, 56, 0, Math.PI * 2);
+ ctx.stroke();
+
+ // Inner glow
+ const glow = ctx.createRadialGradient(64, 64, 0, 64, 64, 50);
+ glow.addColorStop(0, 'rgba(251, 191, 36, 0.18)');
+ glow.addColorStop(1, 'rgba(251, 191, 36, 0)');
+ ctx.fillStyle = glow;
+ ctx.beginPath();
+ ctx.arc(64, 64, 50, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Letter C
+ ctx.strokeStyle = 'rgba(251, 191, 36, 0.9)';
+ ctx.lineWidth = 5;
+ ctx.lineCap = 'round';
+ ctx.beginPath();
+ ctx.arc(64, 64, 28, Math.PI * 0.25, Math.PI * 1.75);
+ ctx.stroke();
+
+ return new THREE.CanvasTexture(canvas);
+}
+
+function makeFloorTexture(): THREE.CanvasTexture {
+ const canvas = document.createElement('canvas');
+ canvas.width = 512;
+ canvas.height = 512;
+ const ctx = canvas.getContext('2d')!;
+
+ ctx.fillStyle = '#0a0806';
+ ctx.fillRect(0, 0, 512, 512);
+
+ // Very subtle grid
+ ctx.strokeStyle = 'rgba(251, 191, 36, 0.04)';
+ ctx.lineWidth = 0.5;
+ const step = 512 / 20;
+ for (let i = 0; i <= 20; i++) {
+ ctx.beginPath();
+ ctx.moveTo(i * step, 0);
+ ctx.lineTo(i * step, 512);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(0, i * step);
+ ctx.lineTo(512, i * step);
+ ctx.stroke();
+ }
+
+ return new THREE.CanvasTexture(canvas);
+}
+
+export default function RoundTable3D({ personas, activeSpeakerId, messages }: RoundTable3DProps) {
+ const mountRef = useRef
(null);
+ const sceneRef = useRef(null);
+ const cameraRef = useRef(null);
+ const rendererRef = useRef(null);
+ const avatarsRef = useRef