/**
 * Bare Radio Video Chat — Client
 * WebRTC mesh (camera-only, no audio)
 * Signaling via Socket.io
 * Floating draggable/resizable video tiles
 */

(function () {
  'use strict';

  // ── CONFIG ───────────────────────────────────────────────
  const ICE_SERVERS = [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' }
  ];

  const TILE_W = 240;
  const TILE_H = 180;
  const TILE_DRAGBAR_H = 28;

  // ── STATE ────────────────────────────────────────────────
  // i18n — expose socket and role for lang toggle (set after connect)
  // window.LANG and window.t() are provided by /languages/i18n.js
  let lastUserList   = null;
  let socket        = null;
  let localStream   = null;
  let mySocketId    = null;
  let myDisplayName = '';
  let myRole        = 'member';
  let myAvatarUrl   = null;
  let myRoom        = '';
  let camEnabled    = false;       // camera off until user turns it on
  let zTop          = 100;         // z-index counter for tiles
  let tileCount     = 0;           // for default position cascade
  const pendingViewRequests = new Map(); // socketId -> displayName (awaiting owner approval)
  let selectedCamId = localStorage.getItem('camDeviceId') || null; // chosen video input (ManyCam/OBS/etc.)

  // ── ROLLE-VISNINGSNAVN ───────────────────────────────────
  const ROLE_LABELS = {
    admin:     'Admin',
    dj:        'DJ',
    moderator: 'Mod',
    vip:       'VIP',
    member:    'Medlem',
    guest:     'Gjest'
  };
  function roleLabel(role) { return t('roles.' + role) || role; }

  // Role colours loaded from server settings
  let ROLE_COLORS = {
    admin:     '#CC1C2E',
    dj:        '#A855F7',
    moderator: '#22C55E',
    vip:       '#EAB308',
    member:    '#FFFFFF',
    guest:     '#9CA3AF'
  };

  // White-label branding loaded from server settings (/api/settings)
  let BRANDING = {
    siteName:      'Pulse Chat',
    tagline:       'Pulse Radio',
    welcomeMessage:'Vi ønsker {name} velkommen til Pulse Chat! 🎙️',
    creditText:    'Designed By Dahl Total',
    logo:          '/pulse_chat_logo.png',
    themeColor:    '#CC1C2E',
    radioEnabled:  true,
    streamUrl:     'https://artemis.streamerr.co/listen/bare_radio/radio.mp3',
    nowPlayingUrl: 'https://artemis.streamerr.co/api/nowplaying/bare_radio'
  };

  // Map<socketId, { pc, stream, displayName, role }>
  const peers = new Map();

  // ── DOM REFS ─────────────────────────────────────────────
  const joinScreen       = document.getElementById('join-screen');
  const chatRoom         = document.getElementById('chat-room');

  const userList         = document.getElementById('user-list');
  const vcNum            = document.getElementById('vc-num');
  const watchersMe       = document.getElementById('watchers-me');
  const wmNum            = document.getElementById('wm-num');
  const clearChatBar     = document.getElementById('clear-chat-bar');
  const clearChatBtn     = document.getElementById('clear-chat-btn');
  const topbarRoomLabel  = document.getElementById('topbar-room-label');
  const sidebarYouName   = document.getElementById('sidebar-you-name');
  const localCamSidebar  = document.getElementById('local-cam-sidebar');
  const localCamVideo    = document.getElementById('local-cam-video');
  const leaveBtn         = document.getElementById('leave-btn');
  const camToggle        = document.getElementById('cam-toggle');
  const camSelect        = document.getElementById('cam-select');
  const emojiBtn         = document.getElementById('emoji-btn');
  const emojiPicker      = document.getElementById('emoji-picker');
  const chatMessages     = document.getElementById('chat-messages');
  const chatInput        = document.getElementById('chat-input');
  const chatSend         = document.getElementById('chat-send');
  const videoContainer   = document.getElementById('video-container');

  // Formatting toolbar refs
  const fmtBold          = document.getElementById('fmt-bold');
  const fmtItalic        = document.getElementById('fmt-italic');
  const fmtUnderline     = document.getElementById('fmt-underline');
  const fmtColorBtn      = document.getElementById('fmt-color-btn');
  const fmtColorInput    = document.getElementById('fmt-color-input');
  const fmtColorSwatch   = document.getElementById('fmt-color-swatch');
  const fmtGifBtn        = document.getElementById('fmt-gif');
  const gifPicker        = document.getElementById('gif-picker');
  const gifSearchInput   = document.getElementById('gif-search-input');
  const gifSearchBtn     = document.getElementById('gif-search-btn');
  const gifResults       = document.getElementById('gif-results');

  // Profile modal refs
  const profileModal      = document.getElementById('profile-modal');
  const profileBtn        = document.getElementById('profile-btn');
  const profileClose      = document.getElementById('profile-close');
  const profileAvatarImg  = document.getElementById('profile-avatar-img');
  const profileAvatarPh   = document.getElementById('profile-avatar-placeholder');
  const profileInitials   = document.getElementById('profile-avatar-initials');
  const avatarFileInput   = document.getElementById('avatar-file-input');
  const avatarUploadStatus= document.getElementById('avatar-upload-status');
  const pwCurrent         = document.getElementById('pw-current');
  const pwNew             = document.getElementById('pw-new');
  const pwConfirm         = document.getElementById('pw-confirm');
  const pwSaveBtn         = document.getElementById('pw-save-btn');
  const pwStatus          = document.getElementById('pw-status');

  // ── FETCH SESSION USERNAME + ROLE COLORS ─────────────────
  fetch('/api/me')
    .then(r => r.ok ? r.json() : Promise.reject())
    .then(data => {
      myDisplayName = data.username;
      myRole        = data.role || 'member';
      window._myRole = myRole;
      myAvatarUrl   = data.avatarUrl || null;
      if (sidebarYouName) sidebarYouName.textContent = myDisplayName.toUpperCase();
      updateProfileAvatar(myAvatarUrl);
    })
    .catch(() => { window.location.href = '/login'; });

  fetch('/api/settings')
    .then(r => r.ok ? r.json() : Promise.reject())
    .then(data => {
      if (data.roleColors) ROLE_COLORS = { ...ROLE_COLORS, ...data.roleColors };
      if (data.branding)   BRANDING    = { ...BRANDING, ...data.branding };
      applyBranding();
    })
    .catch(() => { applyBranding(); });

  // Apply white-label branding across the UI
  function applyBranding() {
    try {
      // Topbar logo + favicon
      document.querySelectorAll('.topbar-logo').forEach(img => {
        img.src = BRANDING.logo; img.alt = BRANDING.siteName;
      });
      let fav = document.querySelector('link[rel="icon"]');
      if (!fav) { fav = document.createElement('link'); fav.rel = 'icon'; document.head.appendChild(fav); }
      fav.href = BRANDING.logo;
      // Theme accent colour
      if (BRANDING.themeColor) {
        document.documentElement.style.setProperty('--red', BRANDING.themeColor);
        document.documentElement.style.setProperty('--red-bright', BRANDING.themeColor);
      }
      // Credit text
      document.querySelectorAll('.sidebar-credit').forEach(el => {
        el.textContent = BRANDING.creditText;
      });
      // Topbar room label + document title (if already set, refresh)
      if (topbarRoomLabel) topbarRoomLabel.textContent = BRANDING.siteName;
      document.title = BRANDING.siteName;
      // Radio player: hide entirely if disabled
      const playerEl = document.getElementById('radio-player') ||
                       document.querySelector('.radio-player');
      if (BRANDING.radioEnabled === false && playerEl) {
        playerEl.style.display = 'none';
      }
      // On-air label + default artist from tagline
      const onAir = document.getElementById('radio-onair-label');
      if (onAir) onAir.textContent = (BRANDING.tagline || BRANDING.siteName).toUpperCase() + ' — ON AIR';
      const artistDefault = document.getElementById('player-artist');
      if (artistDefault) {
        artistDefault.textContent = BRANDING.tagline || BRANDING.siteName;
      }
      // Init radio player (deferred until branding known)
      if (BRANDING.radioEnabled !== false && window._initRadioPlayer) {
        window._initRadioPlayer();
      }
    } catch (e) { /* non-fatal */ }
  }

  // ── AUTO-JOIN BareChat ────────────────────────────────────
  myRoom = 'barechat';
  connectAndJoin();

  // ── SOCKET.IO CONNECTION ──────────────────────────────────
  function connectAndJoin() {
    socket = io({ transports: ['websocket'] });
    window._socket = socket;

    socket.on('connect_error', () => {
      showJoinError(t('system.connError'));
      localStream?.getTracks().forEach((t) => t.stop());
      localStream = null;
    });

    socket.on('connect', () => {
      mySocketId = socket.id;
      socket.emit('join-room', { roomId: myRoom });
    });

    socket.on('room-full', () => {
      showJoinError(t('system.roomFull'));
      socket.disconnect();
    });

    socket.on('room-joined', async ({ room, peers: existingPeers, you, history }) => {
      mySocketId = you;
      enterRoom(room);
      if (typeof applyI18n === 'function') applyI18n();
      addLocalTile();
      // Render chat history
      if (history && history.length) {
        addChatMessage({ system: true, text: t('system.historyDivider') });
        history.forEach(msg => addChatMessage({ ...msg, historical: true }));
        addChatMessage({ system: true, text: '— Nå —' });
      }
      for (const peer of existingPeers) {
        await createOffer(peer.socketId, peer.displayName, peer.role, peer.avatarUrl);
      }
    });

    socket.on('peer-joined', ({ socketId, displayName, role, avatarUrl }) => {
      peers.set(socketId, { displayName, role: role || 'member', avatarUrl: avatarUrl || null, pc: null, stream: null });
      addChatMessage({ system: true, text: `${displayName} har ramla inn på Bare Chat` });
    });

    socket.on('offer', async ({ from, offer }) => {
      await handleOffer(from, offer);
    });

    socket.on('answer', async ({ from, answer }) => {
      const peer = peers.get(from);
      if (!peer?.pc) return;
      try { await peer.pc.setRemoteDescription(new RTCSessionDescription(answer)); }
      catch (err) { console.warn('answer error', err); }
    });

    socket.on('ice-candidate', async ({ from, candidate }) => {
      const peer = peers.get(from);
      if (!peer?.pc || !candidate) return;
      try { await peer.pc.addIceCandidate(new RTCIceCandidate(candidate)); }
      catch (err) { console.warn('ice error', err); }
    });

    socket.on('peer-left', ({ socketId }) => {
      removePeer(socketId);
    });

    socket.on('user-list', (list) => {
      renderUserList(list);
    });

    socket.on('role-changed', ({ role }) => {
      myRole = role;
      window._myRole = myRole;
      addChatMessage({ system: true, text: t('system.roleChanged', { role: roleLabel(role) }) });
      syncAdminControls();
    });

    socket.on('chat-cleared', ({ by }) => {
      document.getElementById('chat-messages').innerHTML = '';
      addChatMessage({ system: true, text: t('system.chatCleared', { name: by }) });
    });

    socket.on('lang-changed', function ({ lang }) {
      loadLang(lang, function () {
        if (typeof window.onLangChanged === 'function') window.onLangChanged();
      });
    });

    // Someone wants to watch MY camera — show approve/deny popup
    socket.on('view-request', ({ from, displayName, role }) => {
      showViewRequest(from, displayName, role);
    });

    // The camera owner answered MY request to watch them
    socket.on('view-response', ({ from, approved, displayName }) => {
      const name = pendingViewRequests.get(from) || displayName || '';
      pendingViewRequests.delete(from);
      const btn = userList.querySelector('.cam-view-pending');
      if (btn) btn.classList.remove('cam-view-pending');
      if (approved) {
        openCameraTile(from, name);
      } else {
        showToast(t('view.denied', { name }), 'red');
      }
    });

    socket.on('chat-message', (msg) => {
      addChatMessage(msg);
    });

    socket.on('system-message', ({ text }) => {
      addChatMessage({ system: true, text });
    });

    socket.on('kicked', ({ reason }) => {
      showRoomAlert('⚡ You have been kicked' + (reason ? ': ' + reason : ''), 'red');
      setTimeout(() => { cleanup(); window.location.href = '/'; }, 2500);
    });

    socket.on('banned', ({ reason }) => {
      showRoomAlert('🔨 You have been banned' + (reason ? ': ' + reason : ''), 'red');
      setTimeout(() => { cleanup(); window.location.href = '/login?error=banned'; }, 2500);
    });

    socket.on('disconnect', () => {
      console.log('Disconnected');
    });
  }

  // ── WebRTC ────────────────────────────────────────────────

  // Safari/iOS compat: play video element robustly
  function safePlay(video) {
    // Re-attach srcObject so Safari re-initialises decoder
    const src = video.srcObject;
    if (src) { video.srcObject = null; video.srcObject = src; }
    video.load();
    const p = video.play();
    if (p && typeof p.catch === 'function') p.catch(() => {});
  }

  // Attach a stream to a video and play — deferred one tick for Safari DOM timing
  function attachStream(video, stream) {
    video.srcObject = stream;
    // Defer slightly so Safari has flushed layout before play()
    setTimeout(() => safePlay(video), 0);
  }

  async function createOffer(peerId, displayName, role, avatarUrl) {
    const existing = peers.get(peerId) || {};
    let pc = existing.pc;
    if (!pc || pc.connectionState === 'closed' || pc.connectionState === 'failed') {
      pc = createPeerConnection(peerId, displayName, role);
    }
    peers.set(peerId, { displayName, role: role || 'member', avatarUrl: avatarUrl || null, pc, stream: existing.stream || null });

    // Always include a video slot in SDP — use addTrack if camera is on, else addTransceiver
    const hasVideoSender = pc.getSenders().some(s => s.track?.kind === 'video') ||
                           pc.getTransceivers().some(t => t.direction !== 'stopped');
    if (!hasVideoSender) {
      if (localStream && camEnabled) {
        localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
      } else {
        // sendrecv: no track yet, but sender slot must exist so replaceTrack works later
        if (typeof pc.addTransceiver === 'function') {
          pc.addTransceiver('video', { direction: 'sendrecv' });
        }
      }
    }

    try {
      // Always include offerToReceiveVideo for maximum Safari compat
      const offer = await pc.createOffer({ offerToReceiveVideo: true });
      await pc.setLocalDescription(offer);
      socket.emit('offer', { to: peerId, offer: pc.localDescription });
    } catch (err) { console.error('createOffer', err); }
  }

  async function handleOffer(peerId, offer) {
    const existing = peers.get(peerId) || {};
    const displayName = existing.displayName || 'Unknown';
    const role = existing.role || 'member';
    const avatarUrl = existing.avatarUrl || null;

    let pc = existing.pc;
    if (!pc || pc.connectionState === 'closed' || pc.connectionState === 'failed') {
      pc = createPeerConnection(peerId, displayName, role);
      peers.set(peerId, { ...existing, displayName, role, avatarUrl, pc, stream: null });
    }

    try {
      await pc.setRemoteDescription(new RTCSessionDescription(offer));

      // Force sendrecv so we can send video later without renegotiating
      // Wrapped in try/catch: Safari throws if direction is set before ice gathering
      try {
        for (const t of pc.getTransceivers()) {
          if (t.direction === 'recvonly') t.direction = 'sendrecv';
          else if (t.direction === 'inactive') t.direction = 'recvonly'; // at minimum receive
        }
      } catch (_) {}

      // If camera is already on, inject our track into the video sender
      if (localStream && camEnabled) {
        const videoTrack = localStream.getVideoTracks()[0];
        for (const t of pc.getTransceivers()) {
          if (!t.sender.track) { await t.sender.replaceTrack(videoTrack).catch(() => {}); break; }
        }
      }

      const answer = await pc.createAnswer({ offerToReceiveVideo: true });
      await pc.setLocalDescription(answer);
      socket.emit('answer', { to: peerId, answer: pc.localDescription });
    } catch (err) { console.error('handleOffer', err); }
  }

  function createPeerConnection(peerId, displayName, role) {
    const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
    pc.onicecandidate = ({ candidate }) => {
      if (candidate) socket.emit('ice-candidate', { to: peerId, candidate });
    };
    pc.ontrack = (event) => {
      // event.streams[0] is undefined when addTransceiver is used without a stream (no MSID).
      // Build a stream from the track as fallback, but only if track is live.
      const track = event.track;
      let stream = event.streams && event.streams[0];
      if (!stream) {
        stream = new MediaStream();
        stream.addTrack(track);
      }

      const existing = peers.get(peerId);
      if (existing) existing.stream = stream;

      // If the tile is already open, update its srcObject now
      const tile = document.getElementById(`tile-${peerId}`);
      if (tile) {
        const v = tile.querySelector('video');
        if (v) attachStream(v, stream);
      }

      // Safari: track.onmute fires when sender calls replaceTrack(null)
      track.onmute = () => {
        const t = document.getElementById(`tile-${peerId}`);
        if (t) { t.remove(); tileCount = Math.max(0, tileCount - 1); }
      };
      // track.onunmute fires when track comes back (replaceTrack with real track)
      track.onunmute = () => {
        const existing2 = peers.get(peerId);
        if (existing2 && existing2.stream) {
          const t = document.getElementById(`tile-${peerId}`);
          if (t) {
            const v = t.querySelector('video');
            if (v) attachStream(v, existing2.stream);
          }
        }
      };
    };
    pc.onconnectionstatechange = () => {
      if (pc.connectionState === 'failed') removePeer(peerId);
    };
    return pc;
  }

  // ── TILES ─────────────────────────────────────────────────
  function getDefaultTilePos() {
    const topbarH = parseInt(getComputedStyle(document.documentElement)
      .getPropertyValue('--topbar-h')) || 52;
    const idx = tileCount;
    const left = Math.max(8, window.innerWidth - TILE_W - 16 - idx * 20);
    const top  = topbarH + 8 + idx * 32;
    tileCount++;
    return { left, top };
  }

  function addLocalTile() {
    // Local camera lives in the sidebar, not as a floating tile
    localCamSidebar.classList.add('cam-off'); // off by default
  }

  function renderRemoteTile(peerId, displayName, stream) {
    const existing = document.getElementById(`tile-${peerId}`);
    if (existing) {
      const v = existing.querySelector('video');
      if (v) attachStream(v, stream);
      return;
    }
    const pos = getDefaultTilePos();
    const tile = makeTile(`tile-${peerId}`, displayName, false, pos);
    const video = tile.querySelector('video');
    attachStream(video, stream);
  }

  function makeTile(id, name, isLocal, pos) {
    const tile = document.createElement('div');
    tile.className = 'video-tile' + (isLocal ? ' local' : '');
    tile.id = id;
    tile.style.left   = pos.left + 'px';
    tile.style.top    = pos.top  + 'px';
    tile.style.width  = TILE_W  + 'px';
    tile.style.height = TILE_H  + 'px';
    tile.style.zIndex = ++zTop;

    tile.innerHTML = `
      <div class="tile-dragbar">
        <span class="tile-drag-icon">⠿</span>
        <span class="tile-dragbar-name">${escHtml(name)}</span>
        <div class="tile-actions">
          <button class="tile-btn min-btn" data-i18n-title="tile.minimise" title="Minimer">−</button>
          <button class="tile-btn close-btn" data-i18n-title="tile.close" title="Lukk">×</button>
        </div>
      </div>
      <div class="tile-body">
        <video autoplay muted playsinline></video>
        <div class="tile-cam-off">
          <span class="cam-off-icon">📷</span>
          <span class="cam-off-text" data-i18n="tile.camOff">CAM AV</span>
        </div>
      </div>
      <div class="tile-resize" data-i18n-title="tile.resize" title="Endre størrelse"></div>
    `;

    // Bring to front on click
    tile.addEventListener('pointerdown', () => { tile.style.zIndex = ++zTop; });

    // Drag
    const dragBar = tile.querySelector('.tile-dragbar');
    makeDraggable(tile, dragBar);

    // Resize
    const resizeHandle = tile.querySelector('.tile-resize');
    makeResizable(tile, resizeHandle);

    // Minimise
    const minBtn = tile.querySelector('.min-btn');
    let savedH = TILE_H;
    minBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      const minimised = tile.classList.toggle('minimised');
      if (minimised) {
        savedH = tile.offsetHeight;
        minBtn.textContent = '□';
        minBtn.title = 'Restore';
      } else {
        tile.style.height = savedH + 'px';
        minBtn.textContent = '−';
        minBtn.title = 'Minimise';
      }
    });

    // Close — removes tile from view; peer stays connected
    // Re-open by clicking the user's name in the sidebar
    const closeBtn = tile.querySelector('.close-btn');
    closeBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      // Notify server we stopped watching (extract peerId from tile id)
      const peerId = tile.id.replace('tile-', '');
      if (peerId && peerId !== 'local') {
        socket.emit('camera-view', { targetSocketId: peerId, viewing: false });
      }
      tileCount = Math.max(0, tileCount - 1);
      tile.style.transition = 'opacity 0.2s, transform 0.2s';
      tile.style.opacity = '0';
      tile.style.transform = 'scale(0.88)';
      setTimeout(() => tile.remove(), 200);
    });

    videoContainer.appendChild(tile);
    return tile;
  }

  // ── DRAG ─────────────────────────────────────────────────
  function makeDraggable(tile, bar) {
    let ox = 0, oy = 0;

    bar.addEventListener('pointerdown', (e) => {
      if (e.target.closest('.tile-actions')) return;
      e.preventDefault();
      ox = e.clientX - tile.offsetLeft;
      oy = e.clientY - tile.offsetTop;
      bar.setPointerCapture(e.pointerId);
    });

    bar.addEventListener('pointermove', (e) => {
      if (!bar.hasPointerCapture(e.pointerId)) return;
      const x = Math.max(0, Math.min(window.innerWidth  - tile.offsetWidth,  e.clientX - ox));
      const y = Math.max(0, Math.min(window.innerHeight - TILE_DRAGBAR_H,    e.clientY - oy));
      tile.style.left = x + 'px';
      tile.style.top  = y + 'px';
    });
  }

  // ── RESIZE ───────────────────────────────────────────────
  function makeResizable(tile, handle) {
    let sx, sy, sw, sh;

    handle.addEventListener('pointerdown', (e) => {
      e.preventDefault();
      e.stopPropagation();
      sx = e.clientX; sy = e.clientY;
      sw = tile.offsetWidth; sh = tile.offsetHeight;
      handle.setPointerCapture(e.pointerId);
    });

    handle.addEventListener('pointermove', (e) => {
      if (!handle.hasPointerCapture(e.pointerId)) return;
      const w = Math.max(140, sw + e.clientX - sx);
      const h = Math.max(TILE_DRAGBAR_H + 60, sh + e.clientY - sy);
      tile.style.width  = w + 'px';
      tile.style.height = h + 'px';
    });
  }

  // ── REMOVE PEER ───────────────────────────────────────────
  function removePeer(peerId) {
    const peer = peers.get(peerId);
    if (peer?.pc) peer.pc.close();
    const name = peer?.displayName || 'Someone';
    peers.delete(peerId);

    addChatMessage({ system: true, text: `${name} har forlatt Bare Chat` });

    const tile = document.getElementById(`tile-${peerId}`);
    if (tile) {
      tile.style.transition = 'opacity 0.3s, transform 0.3s';
      tile.style.opacity = '0';
      tile.style.transform = 'scale(0.88)';
      setTimeout(() => tile.remove(), 300);
    }
    tileCount = Math.max(0, tileCount - 1);
  }

  // ── CHAT ─────────────────────────────────────────────────
  let activeFmtColor = null; // null = no color override

  // Bold / italic / underline are state-based toggles — NO BBCode is ever shown
  // in the input. Toggling one keeps it on (and live-previews the style in the
  // input) until you click it off; the message is wrapped with the tags on send.
  const activeFmt = { b: false, i: false, u: false };

  function applyFmtPreview() {
    chatInput.style.fontWeight     = activeFmt.b ? '700' : '';
    chatInput.style.fontStyle      = activeFmt.i ? 'italic' : '';
    chatInput.style.textDecoration = activeFmt.u ? 'underline' : '';
  }

  function toggleFormat(tag, btn) {
    activeFmt[tag] = !activeFmt[tag];
    btn.classList.toggle('active', activeFmt[tag]);
    applyFmtPreview();
    chatInput.focus();
  }

  fmtBold.addEventListener('click',      () => toggleFormat('b', fmtBold));
  fmtItalic.addEventListener('click',    () => toggleFormat('i', fmtItalic));
  fmtUnderline.addEventListener('click', () => toggleFormat('u', fmtUnderline));

  // ── FONT SIZE +/− ────────────────────────────────────────
  const CHAT_SIZES = [13, 14, 15, 16, 17, 18, 20, 22, 24];
  let chatSizeIdx = Math.min(Math.max(parseInt(localStorage.getItem('chatSizeIdx') ?? '4', 10), 0), CHAT_SIZES.length - 1);
  function applyChatSize() {
    const sz = CHAT_SIZES[chatSizeIdx] + 'px';
    document.documentElement.style.setProperty('--chat-font-size', sz);
    localStorage.setItem('chatSizeIdx', String(chatSizeIdx));
  }
  applyChatSize();
  const btnSizeUp   = document.getElementById('fmt-size-up');
  const btnSizeDown = document.getElementById('fmt-size-down');
  if (btnSizeUp)   btnSizeUp.addEventListener('click',   () => { if (chatSizeIdx < CHAT_SIZES.length - 1) { chatSizeIdx++; applyChatSize(); } });
  if (btnSizeDown) btnSizeDown.addEventListener('click', () => { if (chatSizeIdx > 0) { chatSizeIdx--; applyChatSize(); } });

  // Active text colour is stored as state — NO BBCode is ever shown in the input.
  // Picking a colour live-previews it in the input; the message is wrapped on send.
  function setActiveColor(color) {
    activeFmtColor = color || null;
    if (activeFmtColor) {
      fmtColorSwatch.style.background = activeFmtColor;
      fmtColorBtn.style.boxShadow = `0 0 0 2px ${activeFmtColor}`;
      fmtColorBtn.title = 'Tekstfarge på — høyreklikk for å nullstille';
      chatInput.style.color = activeFmtColor;
    } else {
      fmtColorSwatch.style.background = '';
      fmtColorBtn.style.boxShadow = '';
      fmtColorBtn.title = 'Velg tekstfarge';
      chatInput.style.color = '';
    }
  }

  // Clicking the swatch ALWAYS opens the colour picker, so you can pick or change
  // the colour at any time. Right-click clears it (back to the default colour).
  fmtColorBtn.addEventListener('click', () => fmtColorInput.click());
  fmtColorBtn.addEventListener('contextmenu', (e) => { e.preventDefault(); setActiveColor(null); chatInput.focus(); });
  fmtColorInput.addEventListener('input',  () => setActiveColor(fmtColorInput.value));
  fmtColorInput.addEventListener('change', () => { setActiveColor(fmtColorInput.value); chatInput.focus(); });

  // ── GIF PICKER ───────────────────────────────────────────
  fmtGifBtn.addEventListener('click', () => {
    const open = gifPicker.style.display !== 'none';
    gifPicker.style.display = open ? 'none' : 'block';
    if (!open) gifSearchInput.focus();
  });

  const gifCloseBtn = document.getElementById('gif-close');
  if (gifCloseBtn) gifCloseBtn.addEventListener('click', () => {
    gifPicker.style.display = 'none';
  });

  // Klikk utenfor popup lukker den
  document.addEventListener('click', (e) => {
    if (gifPicker.style.display !== 'none' &&
        !gifPicker.contains(e.target) &&
        e.target !== fmtGifBtn) {
      gifPicker.style.display = 'none';
    }
  });

  async function searchGifs() {
    const q = gifSearchInput.value.trim();
    if (!q) return;
    gifResults.innerHTML = '<p class="gif-hint">' + t('gif.loading') + '</p>';
    try {
      const res  = await fetch(`/api/gifs?q=${encodeURIComponent(q)}`);
      const data = await res.json();
      if (!data.results?.length) {
        gifResults.innerHTML = '<p class="gif-hint">' + t('gif.noResults') + '</p>';
        return;
      }
      gifResults.innerHTML = '';
      data.results.forEach(gif => {
        const img = document.createElement('img');
        img.src   = gif.preview || gif.url;
        img.title = gif.title || '';
        img.loading = 'lazy';
        img.addEventListener('click', () => {
          sendGif(gif.url);
          gifPicker.style.display = 'none';
          gifSearchInput.value = '';
          gifResults.innerHTML = '<p class="gif-hint">' + t('chat.gifHint') + '</p>';
        });
        gifResults.appendChild(img);
      });
    } catch {
      gifResults.innerHTML = '<p class="gif-hint">' + t('gif.error') + '</p>';
    }
  }

  gifSearchBtn.addEventListener('click', searchGifs);
  gifSearchInput.addEventListener('keydown', e => { if (e.key === 'Enter') searchGifs(); });

  function sendGif(gifUrl) {
    if (!socket) return;
    addChatMessage({ from: mySocketId, displayName: myDisplayName, role: myRole, avatarUrl: myAvatarUrl, text: '', gifUrl });
    socket.emit('chat-message', { gifUrl });
  }

  function sendChat() {
    let text = chatInput.value.trim();
    // Drop any empty formatting tags (legacy / [b][i][u] left empty).
    text = text
      .replace(/\[c=#[0-9a-fA-F]{3,6}\]\s*\[\/c\]/g, '')
      .replace(/\[(b|i|u)\]\s*\[\/\1\]/g, '')
      .trim();
    if (!text || !socket) return;
    // Apply the active formatting automatically — the user never types codes.
    if (activeFmt.b && !/\[b\]/.test(text)) text = `[b]${text}[/b]`;
    if (activeFmt.i && !/\[i\]/.test(text)) text = `[i]${text}[/i]`;
    if (activeFmt.u && !/\[u\]/.test(text)) text = `[u]${text}[/u]`;
    if (activeFmtColor && !/\[c=#[0-9a-fA-F]{3,6}\]/.test(text)) {
      text = `[c=${activeFmtColor}]${text}[/c]`;
    }
    addChatMessage({ from: mySocketId, displayName: myDisplayName, role: myRole, avatarUrl: myAvatarUrl, text });
    socket.emit('chat-message', { text });
    chatInput.value = '';
    chatInput.focus();
  }
  chatSend.addEventListener('click', sendChat);
  chatInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); }
  });

  function addChatMessage({ from, displayName, role, avatarUrl, text, gifUrl, system = false, welcome = false }) {
    const li = document.createElement('li');

    if (system) {
      li.className = 'chat-msg system' + (welcome ? ' welcome' : '');
      li.innerHTML = `<span class="msg-text">${escHtml(text)}</span>`;
    } else {
      const isMe = from === mySocketId;
      const msgRole = role || (isMe ? myRole : 'member');
      const roleColor = ROLE_COLORS[msgRole] || '#ffffff';
      const roleBadge = msgRole !== 'member'
        ? `<span class="role-badge" style="--badge-color:${escHtml(roleColor)}">${escHtml(roleLabel(msgRole))}</span>`
        : '';
      const avatarHtml = avatarUrl
        ? `<img class="avatar sm" src="${escHtml(avatarUrl)}" alt="" loading="lazy"/>`
        : `<span class="avatar-placeholder sm">${escHtml((displayName || '?')[0].toUpperCase())}</span>`;

      // Build body: GIF or formatted text
      let bodyHtml = '';
      if (gifUrl) {
        bodyHtml = `<img class="msg-gif" src="${escHtml(gifUrl)}" alt="GIF" loading="lazy"/>`;
      } else {
        bodyHtml = `<span class="msg-text">${renderFormatted(text || '')}</span>`;
      }

      li.className = 'chat-msg' + (isMe ? ' mine' : '');
      li.innerHTML = `
        <div class="msg-header">
          ${avatarHtml}
          <span class="msg-name" style="color:${escHtml(isMe ? (ROLE_COLORS[myRole] || 'var(--red-bright)') : roleColor)}">${escHtml(displayName)}</span>
          ${roleBadge}
          <span class="msg-cam-icon" title="Click to view camera">📷</span>
          <span class="msg-time">${formatTime()}</span>
        </div>
        ${bodyHtml}
      `;

      // Click the 📷 to flash/highlight that user's tile (or toggle own cam)
      const camIcon = li.querySelector('.msg-cam-icon');
      camIcon.addEventListener('click', () => {
        if (isMe) {
          localCamSidebar.style.outline = '2px solid var(--red-bright)';
          setTimeout(() => { localCamSidebar.style.outline = ''; }, 1200);
          return;
        }
        const tile = document.getElementById(`tile-${from}`);
        if (!tile) return;
        tile.style.zIndex = ++zTop;
        if (tile.classList.contains('minimised')) tile.querySelector('.min-btn').click();
        tile.style.outline = '2px solid var(--red-bright)';
        setTimeout(() => { tile.style.outline = ''; }, 1200);
      });
    }

    chatMessages.appendChild(li);
    chatMessages.scrollTop = chatMessages.scrollHeight;
  }

  // Parse [b], [i], [u], [c=#hex] BBCode tags into HTML + @mentions
  function renderFormatted(text) {
    let s = escHtml(text);
    s = s.replace(/\[b\]([\s\S]*?)\[\/b\]/g,  '<strong>$1</strong>');
    s = s.replace(/\[i\]([\s\S]*?)\[\/i\]/g,  '<em>$1</em>');
    s = s.replace(/\[u\]([\s\S]*?)\[\/u\]/g,  '<u>$1</u>');
    s = s.replace(/\[c=(#[0-9a-fA-F]{3,6})\]([\s\S]*?)\[\/c\]/g,
      '<span style="color:$1">$2</span>');
    // @mention highlight — bold + accent colour, flash if it's you
    s = s.replace(/@([\w\æøåÆØÅ]+)/g, (_, name) => {
      const isYouMention = name.toLowerCase() === myDisplayName.toLowerCase();
      return `<span class="mention${isYouMention ? ' mention-me' : ''}">@${name}</span>`;
    });
    return s;
  }

  // ── @MENTION AUTOCOMPLETE ─────────────────────────────────
  let mentionDropdown = null;

  function getMentionContext(input) {
    const pos = input.selectionStart;
    const before = input.value.slice(0, pos);
    const match = before.match(/@([\w\æøåÆØÅ]*)$/);
    if (!match) return null;
    return { query: match[1], start: pos - match[0].length, end: pos };
  }

  function buildMentionDropdown(names, ctx) {
    closeMentionDropdown();
    if (!names.length) return;
    mentionDropdown = document.createElement('ul');
    mentionDropdown.className = 'mention-dropdown';
    names.forEach(name => {
      const li = document.createElement('li');
      li.textContent = '@' + name;
      li.addEventListener('mousedown', (e) => {
        e.preventDefault();
        const val = chatInput.value;
        const inserted = '@' + name + ' ';
        chatInput.value = val.slice(0, ctx.start) + inserted + val.slice(ctx.end);
        chatInput.selectionStart = chatInput.selectionEnd = ctx.start + inserted.length;
        chatInput.focus();
        closeMentionDropdown();
      });
      mentionDropdown.appendChild(li);
    });
    chatInput.parentElement.style.position = 'relative';
    chatInput.parentElement.appendChild(mentionDropdown);
  }

  function closeMentionDropdown() {
    if (mentionDropdown) { mentionDropdown.remove(); mentionDropdown = null; }
  }

  chatInput.addEventListener('input', () => {
    const ctx = getMentionContext(chatInput);
    if (!ctx) { closeMentionDropdown(); return; }
    // Collect current names from user-list
    const allNames = [...document.querySelectorAll('#user-list .ul-name')]
      .map(el => el.textContent.replace(/\s*\(deg\)/, '').trim())
      .filter(n => n.toLowerCase().startsWith(ctx.query.toLowerCase()) &&
                   n.toLowerCase() !== myDisplayName.toLowerCase());
    buildMentionDropdown(allNames, ctx);
  });

  chatInput.addEventListener('keydown', (e) => {
    if (!mentionDropdown) return;
    const items = mentionDropdown.querySelectorAll('li');
    const active = mentionDropdown.querySelector('li.active');
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      const next = active ? active.nextElementSibling || items[0] : items[0];
      active?.classList.remove('active'); next?.classList.add('active');
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      const prev = active ? active.previousElementSibling || items[items.length-1] : items[items.length-1];
      active?.classList.remove('active'); prev?.classList.add('active');
    } else if (e.key === 'Enter' || e.key === 'Tab') {
      if (active) { e.preventDefault(); active.dispatchEvent(new MouseEvent('mousedown')); }
    } else if (e.key === 'Escape') {
      closeMentionDropdown();
    }
  });

  document.addEventListener('click', (e) => {
    if (mentionDropdown && !mentionDropdown.contains(e.target)) closeMentionDropdown();
  });

  // ── USER LIST ─────────────────────────────────────────────
  function renderUserList(list) {
    vcNum.textContent = list.length;
    lastUserList = list;
    // Update "Ser På Meg" counter from my own entry
    const me = list.find(u => u.socketId === mySocketId);
    const myWatchers = (me && me.viewerCount) ? me.viewerCount : 0;
    wmNum.textContent = myWatchers;
    watchersMe.style.display = myWatchers > 0 ? '' : 'none';
    userList.innerHTML = '';
    list.forEach(({ socketId, displayName, role, avatarUrl, camOn, viewerCount = 0 }) => {
      const isYou   = socketId === mySocketId;
      const isAdmin = myRole === 'admin' || myRole === 'dj';
      const uRole   = role || 'member';
      const uColor  = ROLE_COLORS[uRole] || '#ffffff';
      const roleBadge = uRole !== 'member'
        ? `<span class="role-badge small" style="--badge-color:${escHtml(uColor)}">${escHtml(roleLabel(uRole))}</span>`
        : '';
      const avUrl = isYou ? myAvatarUrl : (avatarUrl || null);
      const avatarHtml = avUrl
        ? `<img class="avatar sm" src="${escHtml(avUrl)}" alt="" loading="lazy"/>`
        : `<span class="avatar-placeholder sm">${escHtml((displayName || '?')[0].toUpperCase())}</span>`;

      const li = document.createElement('li');
      if (isYou) li.classList.add('you');

      let modBtns = '';
      if (isAdmin && !isYou) {
        modBtns = `
          <button class="mod-btn kick-btn" data-sid="${escHtml(socketId)}" title="Kick">👢</button>
          <button class="mod-btn ban-btn"  data-sid="${escHtml(socketId)}" title="Ban">🔨</button>
        `;
      }

      // Webcam SVG icon — shown only when user has camera on
      const camIconHtml = camOn
        ? `<button class="cam-view-btn${isYou ? ' cam-view-you' : ''}" title="${isYou ? t('userList.yourCamera') : t('userList.viewCamera', { name: displayName })}">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="white" width="22" height="22">
              <rect x="1" y="7" width="21" height="16" rx="3"/>
              <polygon points="22,13 31,8 31,24 22,19"/>
              <circle cx="11.5" cy="15" r="4.5" fill="none" stroke="black" stroke-width="2"/>
              <circle cx="11.5" cy="15" r="1.8" fill="black"/>
            </svg>
            ${viewerCount > 0 ? `<span class="viewer-badge">${viewerCount}</span>` : ''}
          </button>`
        : '';

      li.innerHTML = `
        ${avatarHtml}
        <span class="ul-name">${escHtml(displayName)}${isYou ? ` <em>${t('userList.you')}</em>` : ''}</span>
        ${roleBadge}
        ${camIconHtml}
        ${modBtns}
      `;

      li.style.cursor = 'pointer';

      // Webcam icon click — opt-in view
      const camBtn = li.querySelector('.cam-view-btn');
      if (camBtn) {
        camBtn.addEventListener('click', (e) => {
          e.stopPropagation();
          if (isYou) { toggleCam(); return; }
          const tileId = `tile-${socketId}`;
          let tile = document.getElementById(tileId);
          if (tile) {
            // Toggle: if visible, close it
            tile.remove(); tileCount = Math.max(0, tileCount - 1);
            socket.emit('camera-view', { targetSocketId: socketId, viewing: false });
            return;
          }
          // Don't double-request if one is already pending for this user
          if (pendingViewRequests.has(socketId)) return;
          pendingViewRequests.set(socketId, displayName);
          camBtn.classList.add('cam-view-pending');
          socket.emit('view-request', { targetSocketId: socketId });
          showToast(t('view.requestSent', { name: displayName }));
        });
      }

      li.addEventListener('click', (e) => {
        if (e.target.closest('.mod-btn')) return;
        if (e.target.closest('.cam-view-btn')) return;
        if (isYou) { toggleCam(); return; }
        const tileId = `tile-${socketId}`;
        let tile = document.getElementById(tileId);
        if (!tile) return;
        tile.style.zIndex = ++zTop;
        if (tile.classList.contains('minimised')) tile.querySelector('.min-btn').click();
        tile.style.outline = '2px solid var(--red-bright)';
        setTimeout(() => { tile.style.outline = ''; }, 1200);
      });

      const kickBtn = li.querySelector('.kick-btn');
      if (kickBtn) {
        kickBtn.addEventListener('click', (e) => {
          e.stopPropagation();
          if (confirm(t('system.kickConfirm', { name: displayName }))) socket.emit('kick-user', { targetSocketId: socketId });
        });
      }
      const banBtn = li.querySelector('.ban-btn');
      if (banBtn) {
        banBtn.addEventListener('click', (e) => {
          e.stopPropagation();
          if (confirm(t('system.banConfirm', { name: displayName })))
            socket.emit('ban-user', { targetSocketId: socketId });
        });
      }

      userList.appendChild(li);
    });
  }

  // ── ENTER ROOM ────────────────────────────────────────────
  function enterRoom(room) {
    joinScreen.style.display = 'none';
    chatRoom.style.display = 'flex';
    topbarRoomLabel.textContent = BRANDING.siteName;
    if (sidebarYouName) sidebarYouName.textContent = myDisplayName.toUpperCase();
    document.title = BRANDING.siteName;
    const welcomeTpl = (BRANDING.welcomeMessage && BRANDING.welcomeMessage.trim())
      ? BRANDING.welcomeMessage
      : t('system.welcome', { name: '{name}' });
    const welcomeText = welcomeTpl.replace(/\{name\}/g, myDisplayName);
    addChatMessage({ system: true, welcome: true, text: welcomeText });
    chatInput.focus();
    syncCamButtons();
    syncAdminControls();
  }

  // ── BACKGROUND PICKER ─────────────────────────────────────
  const bgBtn    = document.getElementById('bg-btn');
  const bgPicker = document.getElementById('bg-picker');

  const BG_OPTIONS = [
    { label: 'Default',     value: '#0C0D14' },
    { label: 'Pitch Black', value: '#000000' },
    { label: 'Navy',        value: '#040C1E' },
    { label: 'Deep Blue',   value: '#05102A' },
    { label: 'Midnight',    value: '#080818' },
    { label: 'Dark Red',    value: '#160408' },
    { label: 'Forest',      value: '#060E09' },
    { label: 'Graphite',    value: '#131316' },
    { label: 'Warm Dark',   value: '#13100C' },
    { label: 'Purple',      value: '#0E0814' },
    { label: 'Steel',       value: '#0A0E14' },
    { label: 'Slate',       value: '#0B0C12' },
    { label: 'Red Glow',    value: 'radial-gradient(ellipse at 50% 100%, #1a0306 0%, #0C0D14 65%)' },
    { label: 'Blue Glow',   value: 'radial-gradient(ellipse at 50% 100%, #020A20 0%, #0C0D14 65%)' },
    { label: 'Signal',      value: 'radial-gradient(ellipse at 20% 50%, #130308 0%, #040915 60%, #0C0D14 100%)' },
  ];

  let bgPickerOpen = false;

  // Apply a background to the chat main area. type: 'color' | 'image'
  function applyChatBg(value, type) {
    const chatMain = document.querySelector('.chat-main');
    if (!chatMain) return;
    if (type === 'image') {
      chatMain.style.backgroundImage    = `url("${value}")`;
      chatMain.style.backgroundSize      = 'cover';
      chatMain.style.backgroundPosition  = 'center';
      chatMain.style.backgroundRepeat    = 'no-repeat';
    } else {
      // color or gradient
      chatMain.style.backgroundImage = value.startsWith('radial') || value.startsWith('linear') ? value : 'none';
      chatMain.style.background      = value;
      chatMain.style.backgroundSize  = '';
      chatMain.style.backgroundPosition = '';
    }
    try { localStorage.setItem('chatBg', JSON.stringify({ value, type })); } catch (_) {}
  }

  function markActiveSwatch(container, activeEl) {
    container.querySelectorAll('.bg-swatch').forEach(s => s.classList.remove('active'));
    if (activeEl) activeEl.classList.add('active');
  }

  function buildBgPicker() {
    bgPicker.innerHTML = '';

    // Tabs
    const tabs = document.createElement('div');
    tabs.className = 'bg-tabs';
    const tabColors = document.createElement('button');
    tabColors.className = 'bg-tab active'; tabColors.type = 'button';
    tabColors.setAttribute('data-i18n', 'bg.tabColors');
    tabColors.textContent = t('bg.tabColors');
    const tabImages = document.createElement('button');
    tabImages.className = 'bg-tab'; tabImages.type = 'button';
    tabImages.setAttribute('data-i18n', 'bg.tabImages');
    tabImages.textContent = t('bg.tabImages');
    tabs.appendChild(tabColors);
    tabs.appendChild(tabImages);
    bgPicker.appendChild(tabs);

    // ── Colours panel ──
    const colorPanel = document.createElement('div');
    colorPanel.className = 'bg-panel';
    const colorGrid = document.createElement('div');
    colorGrid.className = 'bg-swatches';
    BG_OPTIONS.forEach((opt, i) => {
      const sw = document.createElement('button');
      sw.className = 'bg-swatch' + (i === 0 ? ' active' : '');
      sw.title = opt.label; sw.type = 'button';
      sw.style.background = opt.value;
      sw.addEventListener('click', () => {
        applyChatBg(opt.value, 'color');
        markActiveSwatch(colorGrid, sw);
      });
      colorGrid.appendChild(sw);
    });
    colorPanel.appendChild(colorGrid);
    bgPicker.appendChild(colorPanel);

    // ── Images panel ──
    const imagePanel = document.createElement('div');
    imagePanel.className = 'bg-panel';
    imagePanel.style.display = 'none';
    const imageGrid = document.createElement('div');
    imageGrid.className = 'bg-image-grid';
    const imgHint = document.createElement('div');
    imgHint.className = 'bg-image-hint';
    imgHint.textContent = t('bg.loading');
    imagePanel.appendChild(imgHint);
    imagePanel.appendChild(imageGrid);
    bgPicker.appendChild(imagePanel);

    let imagesLoaded = false;
    function loadImages() {
      if (imagesLoaded) return;
      imagesLoaded = true;
      fetch('/api/backgrounds')
        .then(r => r.json())
        .then(({ images }) => {
          imageGrid.innerHTML = '';
          if (!images || !images.length) {
            imgHint.textContent = t('bg.noImages');
            return;
          }
          imgHint.style.display = 'none';
          images.forEach(img => {
            const thumb = document.createElement('button');
            thumb.className = 'bg-image-thumb';
            thumb.type = 'button';
            thumb.title = img.name;
            thumb.style.backgroundImage = `url("${img.url}")`;
            thumb.addEventListener('click', () => {
              applyChatBg(img.url, 'image');
              imageGrid.querySelectorAll('.bg-image-thumb').forEach(t => t.classList.remove('active'));
              thumb.classList.add('active');
            });
            imageGrid.appendChild(thumb);
          });
        })
        .catch(() => { imgHint.textContent = t('bg.noImages'); imagesLoaded = false; });
    }

    // Tab switching
    tabColors.addEventListener('click', () => {
      tabColors.classList.add('active'); tabImages.classList.remove('active');
      colorPanel.style.display = ''; imagePanel.style.display = 'none';
    });
    tabImages.addEventListener('click', () => {
      tabImages.classList.add('active'); tabColors.classList.remove('active');
      imagePanel.style.display = ''; colorPanel.style.display = 'none';
      loadImages();
    });
  }

  buildBgPicker();

  // Restore saved background on load
  (function restoreBg() {
    try {
      const saved = JSON.parse(localStorage.getItem('chatBg') || 'null');
      if (saved && saved.value) applyChatBg(saved.value, saved.type);
    } catch (_) {}
  })();

  bgBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    bgPickerOpen = !bgPickerOpen;
    bgPicker.style.display = bgPickerOpen ? 'block' : 'none';
    bgBtn.classList.toggle('active', bgPickerOpen);
  });

  document.addEventListener('click', (e) => {
    if (bgPickerOpen && !bgPicker.contains(e.target) && e.target !== bgBtn) {
      bgPickerOpen = false;
      bgPicker.style.display = 'none';
      bgBtn.classList.remove('active');
    }
  });

  // ── EMOJI PICKER ──────────────────────────────────────────
  const EMOJI_CATS = [
    { icon: '😀', label: 'Faces', emojis: ['😀','😁','😂','🤣','😃','😄','😅','😆','😊','🙂','😍','🥰','😘','😎','🤩','🥳','😴','😭','😤','😠','😡','🤬','😮','😲','🤔','😏','😒','😬','🤗','😈','👿','💀','☠️','🤐','🥴','😷','🤒','🤕'] },
    { icon: '👍', label: 'Hands', emojis: ['👍','👎','👏','🙌','🤝','🤜','🤛','✊','👊','✌️','🤞','🤟','🤘','👌','🤌','🤙','👋','🖐️','✋','🖖','🫶','💪','🦾'] },
    { icon: '❤️', label: 'Hearts', emojis: ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❤️‍🔥','💕','💞','💓','💗','💖','💘','💝','❣️','💯'] },
    { icon: '🔥', label: 'Symbols', emojis: ['🔥','⭐','🌟','✨','💫','🎉','🎊','🏆','🥇','🎯','💥','❗','❓','💡','🔊','📢','📣','🎵','🎶','🎤','📻','📡','🌈','⚡','🌊','💀'] },
  ];

  let emojiOpen = false;
  let activeEmojiCat = 0;

  function buildEmojiPicker() {
    const tabs = document.createElement('div');
    tabs.className = 'emoji-picker-tabs';

    const grid = document.createElement('div');
    grid.className = 'emoji-grid';

    function showCat(idx) {
      activeEmojiCat = idx;
      grid.innerHTML = '';
      EMOJI_CATS[idx].emojis.forEach((em) => {
        const btn = document.createElement('button');
        btn.className = 'emoji-btn-item';
        btn.textContent = em;
        btn.title = em;
        btn.type = 'button';
        btn.addEventListener('click', () => {
          insertAtCursor(chatInput, em);
          // keep picker open for multi-select
        });
        grid.appendChild(btn);
      });
      tabs.querySelectorAll('.emoji-tab').forEach((t, i) => t.classList.toggle('active', i === idx));
    }

    EMOJI_CATS.forEach((cat, i) => {
      const tab = document.createElement('button');
      tab.className = 'emoji-tab' + (i === 0 ? ' active' : '');
      tab.textContent = cat.icon;
      tab.title = cat.label;
      tab.type = 'button';
      tab.addEventListener('click', () => showCat(i));
      tabs.appendChild(tab);
    });

    emojiPicker.appendChild(tabs);
    emojiPicker.appendChild(grid);
    showCat(0);
  }

  buildEmojiPicker();

  emojiBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    emojiOpen = !emojiOpen;
    emojiPicker.style.display = emojiOpen ? 'block' : 'none';
    emojiBtn.classList.toggle('active', emojiOpen);
    if (emojiOpen) chatInput.focus();
  });

  // Close picker when clicking outside
  document.addEventListener('click', (e) => {
    if (emojiOpen && !emojiPicker.contains(e.target) && e.target !== emojiBtn) {
      emojiOpen = false;
      emojiPicker.style.display = 'none';
      emojiBtn.classList.remove('active');
    }
  });

  function insertAtCursor(input, text) {
    const start = input.selectionStart ?? input.value.length;
    const end   = input.selectionEnd   ?? input.value.length;
    input.value = input.value.slice(0, start) + text + input.value.slice(end);
    input.selectionStart = input.selectionEnd = start + text.length;
    input.focus();
  }

  // ── CAM TOGGLE ────────────────────────────────────────────
  function camConstraints() {
    // Use the explicitly chosen device (ManyCam, OBS Virtual Camera, etc.) if set.
    if (selectedCamId) return { video: { deviceId: { exact: selectedCamId } }, audio: false };
    return { video: { facingMode: 'user' }, audio: false };
  }

  function toggleCam() {
    if (!localStream) {
      // First time — request camera
      navigator.mediaDevices.getUserMedia(camConstraints())
        .catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false }))
        .then(async (stream) => {
          localStream = stream;
          camEnabled = true;
          localCamVideo.srcObject = stream;
          safePlay(localCamVideo);
          localCamSidebar.classList.remove('cam-off');

          // Remember which device actually opened (so the picker shows it selected)
          const liveTrack = stream.getVideoTracks()[0];
          const realId = liveTrack?.getSettings?.().deviceId;
          if (realId) { selectedCamId = realId; localStorage.setItem('camDeviceId', realId); }
          populateCamSelect(); // labels are now available (permission granted)

          // Push camera track to all existing peer connections via replaceTrack
          // No renegotiation needed — remote side detects via track.onmute/onunmute
          const videoTrack = stream.getVideoTracks()[0];
          for (const [, peer] of peers) {
            if (!peer.pc) continue;
            for (const t of peer.pc.getTransceivers()) {
              if (t.direction !== 'stopped' && !t.sender.track) {
                try { await t.sender.replaceTrack(videoTrack); } catch (e) { console.warn('replaceTrack', e); }
                break;
              }
            }
          }
          socket.emit('camera-state', { on: true });
          syncCamButtons();
        })
        .catch(() => showJoinError(t('system.camDenied')));
      return;
    }

    camEnabled = !camEnabled;
    const videoTrack = localStream.getVideoTracks()[0];

    if (camEnabled) {
      // Resume camera — restore track on all senders
      videoTrack.enabled = true;
      peers.forEach(peer => {
        if (!peer.pc) return;
        for (const t of peer.pc.getTransceivers()) {
          if (t.direction !== 'stopped' && !t.sender.track) {
            t.sender.replaceTrack(videoTrack).catch(console.warn); break;
          }
        }
      });
    } else {
      // Pause camera — null track hides tile on remote side
      peers.forEach(peer => {
        if (!peer.pc) return;
        const sender = peer.pc.getSenders().find(s => s.track?.kind === 'video');
        if (sender) sender.replaceTrack(null).catch(console.warn);
      });
      videoTrack.enabled = false;
      // Hide my own tile from others
      socket.emit('camera-state', { on: false });
    }

    if (camEnabled) socket.emit('camera-state', { on: true });
    localCamSidebar.classList.toggle('cam-off', !camEnabled);
    syncCamButtons();
  }

  // Called when language changes — refresh dynamic UI strings
  window.onLangChanged = function () {
    applyI18n();
    syncCamButtons();
    if (lastUserList) renderUserList(lastUserList);
  };

  function syncCamButtons() {
    const isOn = camEnabled && !!localStream;
    camToggle.classList.toggle('active', isOn);
    camToggle.querySelector('.ctrl-text').textContent = isOn ? t('sidebar.camOn') : t('sidebar.camOff');
  }

  // ── CAMERA VIEW: open a tile for an approved camera ───────
  function openCameraTile(socketId, displayName) {
    const tileId = `tile-${socketId}`;
    let tile = document.getElementById(tileId);
    if (tile) { tile.style.zIndex = ++zTop; return; } // already open
    const peer = peers.get(socketId);
    const stream = peer?.stream;
    const pos = getDefaultTilePos();
    tile = makeTile(tileId, displayName, false, pos);
    const video = tile.querySelector('video');
    if (stream) attachStream(video, stream);
    // If stream not yet received, ontrack handler attaches it on arrival.
    socket.emit('camera-view', { targetSocketId: socketId, viewing: true });
  }

  // ── CAMERA VIEW: approve/deny popup shown to the camera owner ──
  function showViewRequest(from, displayName, role) {
    const overlay = document.getElementById('view-request-modal');
    if (!overlay) return;
    const nameEl = document.getElementById('vr-name');
    const msgEl  = document.getElementById('vr-msg');
    if (nameEl) nameEl.textContent = displayName || '?';
    if (msgEl)  msgEl.textContent = t('view.wantsToWatch', { name: displayName || '?' });
    overlay.dataset.from = from;
    overlay.style.display = 'flex';
  }

  (function wireViewRequestModal() {
    const overlay  = document.getElementById('view-request-modal');
    if (!overlay) return;
    const allowBtn = document.getElementById('vr-allow');
    const denyBtn  = document.getElementById('vr-deny');
    function close() { overlay.style.display = 'none'; overlay.dataset.from = ''; }
    allowBtn?.addEventListener('click', () => {
      const to = overlay.dataset.from;
      if (to && socket) socket.emit('view-response', { to, approved: true });
      close();
    });
    denyBtn?.addEventListener('click', () => {
      const to = overlay.dataset.from;
      if (to && socket) socket.emit('view-response', { to, approved: false });
      close();
    });
  })();

  // ── Lightweight toast notifications ───────────────────────
  let toastTimer = null;
  function showToast(msg, type) {
    let el = document.getElementById('app-toast');
    if (!el) {
      el = document.createElement('div');
      el.id = 'app-toast';
      el.className = 'app-toast';
      document.body.appendChild(el);
    }
    el.textContent = msg;
    el.classList.toggle('toast-red', type === 'red');
    el.classList.add('show');
    clearTimeout(toastTimer);
    toastTimer = setTimeout(() => el.classList.remove('show'), 3200);
  }

  // ── CAMERA DEVICE PICKER (webcam / ManyCam / OBS Virtual Camera) ──
  async function populateCamSelect() {
    if (!camSelect || !navigator.mediaDevices?.enumerateDevices) return;
    try {
      const devices = await navigator.mediaDevices.enumerateDevices();
      const cams = devices.filter(d => d.kind === 'videoinput');
      if (cams.length <= 1) { camSelect.style.display = 'none'; return; }
      camSelect.innerHTML = '';
      cams.forEach((cam, i) => {
        const opt = document.createElement('option');
        opt.value = cam.deviceId;
        opt.textContent = cam.label || `${t('sidebar.camera')} ${i + 1}`;
        if (cam.deviceId === selectedCamId) opt.selected = true;
        camSelect.appendChild(opt);
      });
      camSelect.style.display = '';
    } catch (e) { console.warn('enumerateDevices', e); }
  }

  async function switchCamera(deviceId) {
    selectedCamId = deviceId;
    localStorage.setItem('camDeviceId', deviceId);
    if (!localStream) return; // will be used next time camera is enabled
    try {
      const newStream = await navigator.mediaDevices.getUserMedia({
        video: { deviceId: { exact: deviceId } }, audio: false
      });
      const newTrack = newStream.getVideoTracks()[0];
      // Stop old track(s)
      localStream.getVideoTracks().forEach(t => t.stop());
      localStream = newStream;
      localCamVideo.srcObject = newStream;
      safePlay(localCamVideo);
      newTrack.enabled = camEnabled;
      // Replace the outgoing track on every peer connection
      for (const [, peer] of peers) {
        if (!peer.pc) continue;
        const sender = peer.pc.getSenders().find(s => s.track?.kind === 'video')
          || peer.pc.getTransceivers().map(tr => tr.sender).find(s => !s.track);
        if (sender) { try { await sender.replaceTrack(newTrack); } catch (e) { console.warn('switch replaceTrack', e); } }
      }
    } catch (e) {
      console.warn('switchCamera failed', e);
      showToast(t('system.camDenied'), 'red');
    }
  }

  if (camSelect) {
    camSelect.addEventListener('change', () => switchCamera(camSelect.value));
  }
  // Refresh the list when devices are plugged in / virtual cams start (OBS/ManyCam)
  if (navigator.mediaDevices) {
    navigator.mediaDevices.addEventListener?.('devicechange', () => {
      if (localStream) populateCamSelect();
    });
  }

  function syncAdminControls() {
    const isAdminOrDj = myRole === 'admin' || myRole === 'dj';
    clearChatBar.style.display = isAdminOrDj ? '' : 'none';
  }

  clearChatBtn.addEventListener('click', () => {
    if (!confirm(t('chat.clearConfirm'))) return;
    socket.emit('clear-chat');
  });

  camToggle.addEventListener('click', toggleCam);

  // ── LEAVE ─────────────────────────────────────────────────
  leaveBtn.addEventListener('click', () => { cleanup(); window.location.href = '/logout'; });

  // ── MOBIL SIDEBAR TOGGLE ─────────────────────────────────
  const sidebarToggle  = document.getElementById('sidebar-toggle');
  const sidebarEl      = document.querySelector('.sidebar');
  const sidebarOverlay = document.getElementById('sidebar-overlay');
  function closeSidebar() {
    sidebarEl?.classList.remove('mobile-open');
    sidebarOverlay?.classList.remove('active');
  }
  if (sidebarToggle) {
    sidebarToggle.addEventListener('click', () => {
      const open = sidebarEl?.classList.toggle('mobile-open');
      sidebarOverlay?.classList.toggle('active', open);
    });
  }
  if (sidebarOverlay) sidebarOverlay.addEventListener('click', closeSidebar);

  function cleanup() {
    if (socket) { socket.disconnect(); socket = null; }
    peers.forEach(({ pc }) => pc?.close());
    peers.clear();
    localStream?.getTracks().forEach((t) => t.stop());
    localStream = null;
    camEnabled = false;
    // Reset sidebar cam
    localCamVideo.srcObject = null;
    localCamSidebar.classList.add('cam-off');
    videoContainer.innerHTML = '';
    userList.innerHTML = '';
    chatMessages.innerHTML = '';
    tileCount = 0;
    if (window._radioCleanup) window._radioCleanup();
  }
  window.addEventListener('beforeunload', cleanup);

  // ── PROFILE MODAL ─────────────────────────────────────────
  function updateProfileAvatar(url) {
    if (url) {
      profileAvatarImg.src = url;
      profileAvatarImg.style.display = 'block';
      profileAvatarPh.style.display  = 'none';
    } else {
      profileAvatarImg.style.display = 'none';
      profileAvatarPh.style.display  = 'flex';
      profileInitials.textContent = myDisplayName ? myDisplayName[0].toUpperCase() : '?';
    }
  }

  function openProfileModal() {
    updateProfileAvatar(myAvatarUrl);
    profileInitials.textContent = myDisplayName ? myDisplayName[0].toUpperCase() : '?';
    profileModal.style.display = 'flex';
    pwCurrent.value = '';
    pwNew.value = '';
    pwConfirm.value = '';
    pwStatus.textContent = '';
    pwStatus.className = 'profile-status';
    avatarUploadStatus.textContent = '';
    avatarUploadStatus.className = 'profile-status';
  }

  function closeProfileModal() {
    profileModal.style.display = 'none';
  }

  if (profileBtn)   profileBtn.addEventListener('click',   openProfileModal);
  if (profileClose) profileClose.addEventListener('click',  closeProfileModal);
  if (profileModal) profileModal.addEventListener('click', (e) => {
    if (e.target === profileModal) closeProfileModal();
  });

  // Avatar file upload
  if (avatarFileInput) {
    avatarFileInput.addEventListener('change', () => {
      const file = avatarFileInput.files[0];
      if (!file) return;
      if (file.size > 2 * 1024 * 1024) {
        setStatus(avatarUploadStatus, t('profile.fileTooLarge'), 'err');
        return;
      }
      const reader = new FileReader();
      reader.onload = () => {
        setStatus(avatarUploadStatus, t('profile.uploading'), '');
        fetch('/profile/avatar', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ base64: reader.result })
        })
        .then(r => r.json())
        .then(data => {
          if (data.error) { setStatus(avatarUploadStatus, data.error, 'err'); return; }
          myAvatarUrl = data.avatarUrl;
          updateProfileAvatar(myAvatarUrl);
          setStatus(avatarUploadStatus, t('profile.photoUpdated'), 'ok');
        })
        .catch(() => setStatus(avatarUploadStatus, t('profile.uploadFailed'), 'err'));
      };
      reader.readAsDataURL(file);
    });
  }

  // Change password
  if (pwSaveBtn) {
    pwSaveBtn.addEventListener('click', () => {
      const cur  = pwCurrent.value.trim();
      const nw   = pwNew.value.trim();
      const conf = pwConfirm.value.trim();
      if (!cur || !nw || !conf) { setStatus(pwStatus, t('profile.fillAllFields'), 'err'); return; }
      if (nw !== conf) { setStatus(pwStatus, t('profile.pwNoMatch'), 'err'); return; }
      if (nw.length < 4) { setStatus(pwStatus, t('profile.pwTooShort'), 'err'); return; }
      pwSaveBtn.disabled = true;
      fetch('/profile/password', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ current: cur, newPassword: nw })
      })
      .then(r => r.json())
      .then(data => {
        pwSaveBtn.disabled = false;
        if (data.error) { setStatus(pwStatus, data.error, 'err'); return; }
        setStatus(pwStatus, t('profile.pwChanged'), 'ok');
        pwCurrent.value = ''; pwNew.value = ''; pwConfirm.value = '';
      })
      .catch(() => { pwSaveBtn.disabled = false; setStatus(pwStatus, t('profile.requestFailed'), 'err'); });
    });
  }

  function setStatus(el, msg, type) {
    if (!el) return;
    el.textContent = msg;
    el.className = 'profile-status' + (type ? ` ${type}` : '');
  }

  // ── RADIO PLAYER ──────────────────────────────────────────
  window._initRadioPlayer = function initPlayer() {
    if (window._radioInited) return; // guard against double-init
    window._radioInited = true;
    const STREAM_URL   = BRANDING.streamUrl  || 'https://artemis.streamerr.co/listen/bare_radio/radio.mp3';
    const NOWPLAY_URL  = BRANDING.nowPlayingUrl || 'https://artemis.streamerr.co/api/nowplaying/bare_radio';
    const POLL_MS      = 15000; // refresh now-playing every 15s

    const playBtn   = document.getElementById('player-play');
    const stopBtn   = document.getElementById('player-stop');
    const volSlider = document.getElementById('player-vol');
    const titleEl   = document.getElementById('player-title');
    const artistEl  = document.getElementById('player-artist');
    const artImg    = document.getElementById('player-art');
    const artPh     = document.getElementById('player-art-ph');

    if (!playBtn) return; // player not in DOM (shouldn't happen)

    let audio    = null;
    let playing  = false;
    let pollTimer = null;

    function buildAudio() {
      if (audio) { audio.pause(); audio.src = ''; }
      audio = new Audio();
      audio.src    = STREAM_URL + '?_=' + Date.now(); // cache-bust for live stream
      audio.volume = parseFloat(volSlider.value);
      audio.preload = 'none';
    }

    function setPlaying(state) {
      playing = state;
      playBtn.textContent = state ? '⏸' : '▶';
      playBtn.classList.toggle('playing', state);
    }

    function play() {
      buildAudio();
      audio.play()
        .then(() => setPlaying(true))
        .catch(() => setPlaying(false));
      audio.addEventListener('error', () => setPlaying(false), { once: true });
    }

    function stop() {
      if (audio) { audio.pause(); audio.src = ''; audio = null; }
      setPlaying(false);
    }

    playBtn.addEventListener('click', () => {
      if (playing) {
        // Pause = stop for live streams (can't really pause)
        stop();
      } else {
        play();
      }
    });

    stopBtn.addEventListener('click', stop);

    volSlider.addEventListener('input', () => {
      if (audio) audio.volume = parseFloat(volSlider.value);
    });

    // Now-playing polling
    function fetchNowPlaying() {
      fetch(NOWPLAY_URL)
        .then(r => r.ok ? r.json() : null)
        .then(data => {
          if (!data) return;
          const np     = data.now_playing;
          const song   = np?.song;
          const title  = song?.title  || '—';
          const artist = song?.artist || BRANDING.tagline || 'Pulse Radio';
          const art    = song?.art    || '';

          titleEl.textContent  = title;
          artistEl.textContent = artist;

          if (art) {
            artImg.src = art;
            artImg.style.display  = 'block';
            artPh.style.display   = 'none';
          } else {
            artImg.style.display = 'none';
            artPh.style.display  = 'flex';
          }
        })
        .catch(() => {}); // silent — don't break anything
    }

    fetchNowPlaying();
    pollTimer = setInterval(fetchNowPlaying, POLL_MS);

    // Stop polling on cleanup (handled by cleanup fn)
    window._radioCleanup = () => {
      stop();
      clearInterval(pollTimer);
    };
  };

  // ── UTILS ─────────────────────────────────────────────────
  function showJoinError(msg) {
    // Join screen is gone — show as a room alert or console warning
    if (chatRoom && chatRoom.style.display !== 'none') {
      showRoomAlert(msg, 'red');
    } else {
      alert(msg);
    }
  }

  function showRoomAlert(msg, type = 'red') {
    const el = document.createElement('div');
    el.className = 'room-alert';
    el.textContent = msg;
    el.style.borderColor = type === 'red' ? 'var(--red)' : 'var(--blue-bright)';
    document.body.appendChild(el);
    setTimeout(() => {
      el.style.opacity = '0';
      setTimeout(() => el.remove(), 400);
    }, 2200);
  }

  function formatTime() {
    const d = new Date();
    return d.getHours().toString().padStart(2, '0') + ':' +
           d.getMinutes().toString().padStart(2, '0');
  }

  function escHtml(str) {
    return String(str)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;')
      .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  }

})();
