// Chat — right-side internal comms overlay (Slack-lite)

const CHAT_CHANNELS = [
  { id: 'general',         name: 'general',          kind: 'public', members: 28, topic: 'Company-wide chatter.' },
  { id: 'random',          name: 'random',           kind: 'public', members: 28, topic: 'Off-topic.' },
  { id: 'announcements',   name: 'announcements',    kind: 'public', members: 28, topic: 'Read-only · admin posts.' },
  { id: 'eng',             name: 'eng',              kind: 'public', members: 8,  topic: 'Engineering team.' },
  { id: 'design',          name: 'design',           kind: 'public', members: 4,  topic: 'Design team.' },
  { id: 'product',         name: 'product',          kind: 'public', members: 6,  topic: 'Product team.' },
  { id: 'marketing',       name: 'marketing',        kind: 'public', members: 5,  topic: 'Marketing team.' },
  { id: 'sales',           name: 'sales',            kind: 'public', members: 5,  topic: 'Sales team.' },
  { id: 'support',         name: 'support',          kind: 'public', members: 4,  topic: 'IT Services support.' },
  { id: 'people',          name: 'people-ops',       kind: 'public', members: 3,  topic: 'HR & People.' },
  { id: 'finance',         name: 'finance',          kind: 'private', members: 4, topic: 'Finance, contracts, payouts. Restricted.' },
  { id: 'cmp-atlas-v4',    name: 'cmp-atlas-v4',     kind: 'public', members: 14, topic: 'Atlas v4 launch — pinned: launch checklist.' },
  { id: 'incidents',       name: 'incidents',        kind: 'public', members: 9,  topic: 'On-call & incidents.' },
];

const CHAT_DMS = [
  { id: 'dm-e2',  user: 'e2',  unread: 2 },
  { id: 'dm-e3',  user: 'e3',  unread: 0 },
  { id: 'dm-e20', user: 'e20', unread: 1 },
  { id: 'dm-e6',  user: 'e6',  unread: 0 },
  { id: 'dm-e8',  user: 'e8',  unread: 0 },
];

// Initial unread counts on channels
const CHAT_UNREAD = {
  'general': 3,
  'eng': 7,
  'cmp-atlas-v4': 12,
  'incidents': 0,
  'random': 4,
  'sales': 1,
};

// Messages per channel
const CHAT_MESSAGES = {
  'general': [
    { who: 'e1',  when: 'yesterday 9:01 AM', text: "Morning team! Reminder we've got the May all-hands at 10am tomorrow. Davis is presenting the Atlas v4 timeline." },
    { who: 'e5',  when: 'yesterday 10:14 AM', text: '☕ Coffee delivery has landed in the kitchen — the Bowling Green office is officially over-caffeinated.', reactions: [{ e: '☕', n: 6 }, { e: '❤️', n: 3 }] },
    { who: 'e6',  when: 'yesterday 2:20 PM', text: 'Open enrollment closes Friday. Two folks haven\'t submitted — DM if you need help.' },
    { who: 'e10', when: 'yesterday 4:55 PM', text: 'Atlas v4 webhook signature v2 is in canary. Looks healthy after 2 hours. Will roll wider tomorrow if metrics stay green.', reactions: [{ e: '🚀', n: 8 }, { e: '👀', n: 2 }] },
    { who: 'e8',  when: 'today 8:48 AM', text: 'Brand refresh — first round of explorations is in the design channel. Would love eyes from execs before we narrow.' },
    { who: 'e4',  when: 'today 9:12 AM', text: 'Closed Marigold Studios annual plan yesterday! Small but mighty. Renée is going to be a great reference.', reactions: [{ e: '🎉', n: 11 }, { e: '🔥', n: 4 }] },
    { who: 'e1',  when: 'today 10:30 AM', text: 'Q2 board pack draft is in Files → Finance. @maren and @ramon owe me edits by EOW.' },
  ],
  'eng': [
    { who: 'e2',  when: 'yesterday 8:30 AM', text: 'Standup notes: Imani on permission model finalization, Sahil on permissions UI shipping today, Jules pairing with Nadia on the job queue load test.' },
    { who: 'e14', when: 'yesterday 10:00 AM', text: 'Frankfurt region is fully wired up. DPA updates merged. Ready for the Klein & Auer pilot migration.', reactions: [{ e: '🇪🇺', n: 5 }, { e: '🙌', n: 4 }] },
    { who: 'e10', when: 'yesterday 3:42 PM', text: 'Heads up — race condition in workflow runner found and patched. T-2834 resolved. PR is ATL-4#1187 if anyone wants a look.' },
    { who: 'e11', when: 'today 9:01 AM', text: 'New permissions UI is in staging. Filing a PR after lunch. @brennan can you scope a regression pass?' },
    { who: 'e15', when: 'today 9:14 AM', text: 'Will do. I\'ll spin up the Cypress suite this afternoon.' },
    { who: 'e12', when: 'today 10:08 AM', text: 'Random one — anyone else seeing intermittent webhook signature mismatches in staging? Datadog has a small spike.', reactions: [{ e: '👀', n: 2 }] },
    { who: 'e10', when: 'today 10:14 AM', text: '@jules I just looked. There\'s a clock-skew issue with the canary instance. Filing as ATL-4#1192.', replyTo: 5 },
  ],
  'cmp-atlas-v4': [
    { who: 'e5',  when: 'yesterday 11:00 AM', text: '📌 Pinned: the launch readiness checklist is at the top of this channel. Update your row by EOD Wednesday.' },
    { who: 'e19', when: 'yesterday 1:24 PM', text: 'Teaser microsite v3 is up. Lucia has notes from Helena — feedback channel below.', reactions: [{ e: '🔥', n: 6 }] },
    { who: 'e23', when: 'yesterday 2:51 PM', text: 'Blog series outline finalized. 5 posts cadenced over 6 weeks. Will share the brief tonight.' },
    { who: 'e24', when: 'today 7:30 AM', text: 'LinkedIn ABM variant A is live. Spending paced at $1.4k/day. CTR is 3.1% in the first hour — significantly above benchmark.', reactions: [{ e: '🚀', n: 8 }, { e: '📈', n: 3 }] },
    { who: 'e3',  when: 'today 9:10 AM', text: 'Stratoscale customer story landing tomorrow. Reviewed final, copy is :chef\'s-kiss:. @yuki shipping it.' },
    { who: 'e5',  when: 'today 11:42 AM', text: 'Press tour reminder: embargo is the morning of Sep 8. Briefings start the week prior. Press kit is in Files → Marketing → Atlas v4 launch assets.' },
  ],
  'sales': [
    { who: 'e4',  when: 'today 8:14 AM', text: 'Weekly: $1.06M open pipeline, $384k weighted. Junction Bank pilot SOW going out Friday — pls keep me posted on legal review.' },
    { who: 'e20', when: 'today 9:30 AM', text: 'Klein & Auer redline #3 is back from their procurement team. Looks workable; sending to Ramon.' },
    { who: 'e22', when: 'today 10:11 AM', text: 'Foxtail Robotics POC scoping call this afternoon. Plenum Analytics is heating up — Aria leading.' },
  ],
  'design': [
    { who: 'e8',  when: 'today 8:48 AM', text: '@here brand refresh round 2 is up. Direction A is wine-and-orange with the slash-mark as a connective metaphor; Direction B keeps the orange but goes bolder on type. Feedback by Friday.' },
    { who: 'e19', when: 'today 9:10 AM', text: 'I think A reads more "operations" and B reads more "consumer". I\'d weight toward A for our ICP but B is more memorable. Hard one.' },
    { who: 'e18', when: 'today 9:48 AM', text: 'What if we do A as primary brand + B as a campaign-time accent system?', replyTo: 1 },
  ],
  'support': [
    { who: 'e7',  when: 'today 7:50 AM', text: 'Overnight queue: 4 new, 2 resolved. Northbrook SSO is open and tagged urgent — Esi has it.' },
    { who: 'e25', when: 'today 9:14 AM', text: 'Northbrook SSO issue isolated to JIT provisioning. Workaround issued, fix going to canary today.' },
    { who: 'e26', when: 'today 10:30 AM', text: 'Pinecrest onboarding going smoothly — they\'ve added 142 of 180 seats.' },
  ],
  'random': [
    { who: 'e21', when: 'yesterday 12:34 PM', text: 'Recipe channel? I just want to say this banana bread is dangerous.', reactions: [{ e: '🍌', n: 7 }, { e: '🥖', n: 4 }] },
    { who: 'e11', when: 'today 8:55 AM', text: 'Anyone in Toronto going to RustConf this fall? Looking for a coffee buddy.' },
    { who: 'e22', when: 'today 9:42 AM', text: 'Petition to add a "boop the sticker" button to the office key fobs.' },
  ],
  'announcements': [
    { who: 'e1',  when: '3 days ago', text: '🎉 We hit 99.94% SLA in April — best month on record. Owen and the support crew, you are unreal.', reactions: [{ e: '🎉', n: 18 }, { e: '🙌', n: 12 }] },
    { who: 'e6',  when: 'last week', text: 'Welcome to Caleb (Support) and Pratik (Sales) who joined this month! Stop by the kitchen and say hi.' },
  ],
  'people': [
    { who: 'e6',  when: 'today 9:00 AM', text: 'Open reqs: 4. We\'re tight on the senior backend role — please refer engineers you respect. Bounty doubled to $4k.' },
    { who: 'e27', when: 'today 10:14 AM', text: 'New hire welcome packs are stocked. Caleb\'s laptop ships tomorrow.' },
  ],
  'finance': [
    { who: 'e9',  when: 'today 8:14 AM', text: 'May affiliate payouts batch ready for review — $23,260 across 12 partners. @maren approval needed.' },
    { who: 'e28', when: 'today 9:00 AM', text: 'Q1 close docs finalized. Filed under Files → Finance.' },
  ],
  'incidents': [
    { who: 'e14', when: 'today 6:42 AM', text: '🟢 No active incidents. Last 24h: clean. SLO budget at 99.94%.', reactions: [{ e: '🟢', n: 5 }] },
    { who: 'e25', when: 'today 9:14 AM', text: 'Customer-side incident — Northbrook SSO. Acknowledged, mitigated. Postmortem to follow.' },
  ],
  'marketing': [
    { who: 'e5',  when: 'today 8:30 AM', text: 'May review numbers: $268k pipeline influenced, 4.2x ROI. Charts in /marketing > performance.' },
    { who: 'e23', when: 'today 9:20 AM', text: 'Stratoscale customer story going live tomorrow. Email blast queued for 10am ET.' },
    { who: 'e24', when: 'today 9:51 AM', text: 'LinkedIn ABM crushing it — early CTR on variant A is 3.1%.' },
  ],
  'product': [
    { who: 'e3',  when: 'today 8:00 AM', text: 'Reminder: customer health score kickoff is at 1pm today. Allegra owns, looping in @e7 for SLA signals.' },
    { who: 'e16', when: 'today 9:14 AM', text: 'Atlas v4 PRD is current as of yesterday. Migrations section is the only open question.' },
  ],
};

// DM threads
const CHAT_DM_THREADS = {
  'dm-e2': [
    { who: 'e2', when: 'today 9:08 AM', text: 'Hey — can we sync on the Atlas v4 readiness gate today? I want to make sure we\'re aligned on what blocks GA.' },
    { who: 'e2', when: 'today 9:09 AM', text: 'I have 2:30 open if that works.' },
  ],
  'dm-e3': [
    { who: 'e3', when: 'yesterday 4:12 PM', text: 'Did you see the customer health score draft? Want to chat through the signal weighting.' },
    { who: 'e1', when: 'yesterday 4:18 PM', text: 'Yes! Looks great. Tomorrow 11am?' },
    { who: 'e3', when: 'yesterday 4:19 PM', text: 'Done — calendar invite sent 🙏' },
  ],
  'dm-e20': [
    { who: 'e20', when: 'today 8:50 AM', text: 'Stratoscale expansion — they\'re asking for an exec sponsor on our side. Are you good to do a brief sync with their CTO next week?' },
  ],
  'dm-e6': [
    { who: 'e6', when: 'yesterday 5:00 PM', text: 'Wendy\'s offer letter is out. She countersigned within an hour. Start date Jun 2.' },
    { who: 'e1', when: 'yesterday 5:04 PM', text: 'Amazing news. Let\'s set up the welcome lunch.' },
  ],
  'dm-e8': [
    { who: 'e8', when: 'today 8:48 AM', text: 'Brand round 2 is up. Curious about your gut reaction before I share more broadly.' },
  ],
};

function ChatOverlay({ open, onClose, currentUser, initialDM }) {
  const D = window.HitcentsData;
  const [activeId, setActiveId] = useState('cmp-atlas-v4');
  const [unread, setUnread] = useState(CHAT_UNREAD);
  const [draft, setDraft] = useState('');
  const [replyTo, setReplyTo] = useState(null); // index of message being replied to
  const [searchQ, setSearchQ] = useState('');
  const scrollRef = useRef(null);
  const { popped, setPopped, pos, onHeaderPointerDown } = usePopout({ x: 220, y: 80 });

  // Mark active read on switch
  useEffect(() => {
    setUnread(u => ({ ...u, [activeId]: 0 }));
    setReplyTo(null);
    if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  }, [activeId, open]);

  // Open a direct message with a specific person — reuse the existing thread, or spin up a new one
  useEffect(() => {
    if (!open || !initialDM || initialDM === currentUser.id) return;
    const dmId = 'dm-' + initialDM;
    if (!CHAT_DMS.find(d => d.id === dmId)) {
      CHAT_DMS.push({ id: dmId, user: initialDM, unread: 0 });
    }
    if (!CHAT_DM_THREADS[dmId]) CHAT_DM_THREADS[dmId] = [];
    setActiveId(dmId);
  }, [open, initialDM, currentUser.id]);

  if (!open) return null;

  const isDM = activeId.startsWith('dm-');
  const channel = isDM ? null : CHAT_CHANNELS.find(c => c.id === activeId);
  const dm = isDM ? CHAT_DMS.find(d => d.id === activeId) : null;
  const dmUser = dm ? getEmployee(dm.user) : null;
  const messages = isDM ? (CHAT_DM_THREADS[activeId] || []) : (CHAT_MESSAGES[activeId] || []);

  const totalUnread = Object.values(unread).reduce((s,n) => s+n, 0) + CHAT_DMS.reduce((s,d) => s + d.unread, 0);

  function send() {
    if (!draft.trim()) return;
    // Visually append a synthetic message (state-only)
    const newMsg = { who: currentUser.id, when: 'just now', text: draft };
    if (replyTo != null) newMsg.replyTo = replyTo;
    if (isDM) {
      CHAT_DM_THREADS[activeId] = [...messages, newMsg];
    } else {
      CHAT_MESSAGES[activeId] = [...messages, newMsg];
    }
    setDraft('');
    setReplyTo(null);
    setTimeout(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, 30);
  }

  function onKey(e) {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
  }

  return (
    <div className={`chat-scrim ${popped ? 'popped' : ''}`} onClick={popped ? undefined : onClose}>
      <div className={`chat-shell ${popped ? 'popped' : ''}`} onClick={e => e.stopPropagation()}
           style={popped ? { left: pos.x, top: pos.y } : undefined}>
        {/* Sidebar */}
        <aside className="chat-sidebar">
          <div className="chat-side-head" onPointerDown={onHeaderPointerDown} style={popped ? { cursor: 'move' } : undefined}>
            <div>
              <div style={{ fontWeight: 600, fontSize: 15 }}>Hitcents</div>
              <div className="dim" style={{ fontSize: 11 }}>{totalUnread > 0 ? `${totalUnread} unread` : 'all caught up'}</div>
            </div>
            <div className="row gap-1">
              <button className="icon-btn" onClick={() => setPopped(p => !p)} title={popped ? 'Dock to side' : 'Pop out as window'}><Icon name={popped ? 'arrow_r' : 'copy'} size={14} /></button>
              <button className="icon-btn" onClick={onClose} title="Close"><Icon name="x" size={14} /></button>
            </div>
          </div>

          <div style={{ padding: '0 10px 10px' }}>
            <div className="search-inline" style={{ padding: '5px 8px' }}>
              <Icon name="search" size={12} />
              <input placeholder="Search messages…" value={searchQ} onChange={e => setSearchQ(e.target.value)} style={{ minWidth: 0, fontSize: 12 }} />
            </div>
          </div>

          <div className="chat-side-scroll">
            <div className="chat-side-group">
              <div className="chat-side-group-head">
                <span>Channels</span>
                <button className="icon-btn" style={{ width: 18, height: 18 }}><Icon name="plus" size={10} /></button>
              </div>
              {CHAT_CHANNELS.map(c => {
                const u = unread[c.id] || 0;
                return (
                  <div key={c.id} onClick={() => setActiveId(c.id)}
                       className={`chat-side-item ${activeId === c.id ? 'active' : ''}`}>
                    <span className="chat-prefix">{c.kind === 'private' ? <Icon name="lock" size={10} /> : '#'}</span>
                    <span className="chat-name">{c.name}</span>
                    {u > 0 && <span className="chat-unread">{u}</span>}
                  </div>
                );
              })}
            </div>

            <div className="chat-side-group">
              <div className="chat-side-group-head">
                <span>Direct messages</span>
                <button className="icon-btn" style={{ width: 18, height: 18 }}><Icon name="plus" size={10} /></button>
              </div>
              {CHAT_DMS.map(d => {
                const u = getEmployee(d.user);
                return (
                  <div key={d.id} onClick={() => setActiveId(d.id)}
                       className={`chat-side-item ${activeId === d.id ? 'active' : ''}`}>
                    <span className="chat-presence" style={{ background: 'var(--green)' }} />
                    <Avatar person={u} size={18} />
                    <span className="chat-name">{u?.name}</span>
                    {d.unread > 0 && <span className="chat-unread">{d.unread}</span>}
                  </div>
                );
              })}
            </div>
          </div>

          <div className="chat-side-foot">
            <Avatar person={currentUser} size={26} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 12, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{currentUser.name}</div>
              <div className="dim row gap-2" style={{ fontSize: 10.5 }}><span style={{ width: 6, height: 6, borderRadius: 3, background: 'var(--green)' }} />Active</div>
            </div>
          </div>
        </aside>

        {/* Main */}
        <main className="chat-main">
          <header className="chat-main-head">
            <div style={{ flex: 1 }}>
              <div className="row gap-2" style={{ fontWeight: 600, fontSize: 15 }}>
                {isDM ? <Avatar person={dmUser} size={20} /> : <span className="chat-prefix" style={{ fontSize: 16, color: 'var(--text-muted)' }}>{channel?.kind === 'private' ? <Icon name="lock" size={12} /> : '#'}</span>}
                <span>{isDM ? dmUser?.name : channel?.name}</span>
                {!isDM && <span className="dim mono" style={{ fontSize: 11, fontWeight: 400 }}>· {channel?.members} members</span>}
              </div>
              <div className="dim" style={{ fontSize: 11.5, marginTop: 2 }}>{isDM ? dmUser?.title : channel?.topic}</div>
            </div>
            <div className="row gap-1">
              <button className="icon-btn" title="Pinned items"><Icon name="pin" size={14} /></button>
              <button className="icon-btn" title="Members"><Icon name="users" size={14} /></button>
              <button className="icon-btn" title="Channel settings"><Icon name="settings" size={14} /></button>
            </div>
          </header>

          <div className="chat-messages" ref={scrollRef}>
            {/* Channel intro */}
            {!isDM && (
              <div className="chat-channel-intro">
                <div className="chat-channel-mark">{channel?.kind === 'private' ? <Icon name="lock" size={18} /> : '#'}</div>
                <h3 style={{ margin: '8px 0 4px', fontSize: 18, fontWeight: 600 }}>Welcome to #{channel?.name}</h3>
                <p className="muted" style={{ fontSize: 13, margin: 0 }}>{channel?.topic} Created by Maren · {channel?.members} members</p>
              </div>
            )}

            {messages.map((m, i) => {
              const author = getEmployee(m.who);
              const ref = (m.replyTo != null && messages[m.replyTo]) ? messages[m.replyTo] : null;
              const refAuthor = ref ? getEmployee(ref.who) : null;
              // A reply always starts a fresh group (Discord-style)
              const showAvatar = ref ? true : (i === 0 || messages[i-1].who !== m.who || messages[i-1].replyTo != null);
              return (
                <div key={i} className="chat-msg" style={{ marginTop: showAvatar ? 12 : 2 }}>
                  <div style={{ width: 32, flexShrink: 0 }}>
                    {showAvatar && <Avatar person={author} size={32} />}
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    {ref && (
                      <button className="chat-reply-ref" onClick={() => setReplyTo(m.replyTo)} title="Replying to this message">
                        <span className="chat-reply-spine" />
                        <Avatar person={refAuthor} size={16} />
                        <span className="chat-reply-name">{refAuthor?.name}</span>
                        <span className="chat-reply-text" dangerouslySetInnerHTML={{ __html: renderMessage(ref.text) }} />
                      </button>
                    )}
                    {showAvatar && (
                      <div className="row gap-2" style={{ marginBottom: 2 }}>
                        <strong style={{ fontSize: 13 }}>{author?.name}</strong>
                        <span className="dim" style={{ fontSize: 11 }}>{m.when}</span>
                      </div>
                    )}
                    <div className="chat-msg-body" dangerouslySetInnerHTML={{ __html: renderMessage(m.text) }} />
                    {m.reactions && (
                      <div className="row gap-2" style={{ marginTop: 6 }}>
                        {m.reactions.map(r => (
                          <button key={r.e} className="chat-reaction">{r.e} {r.n}</button>
                        ))}
                        <button className="chat-reaction add-reaction"><Icon name="plus" size={11} /></button>
                      </div>
                    )}
                  </div>
                  <div className="chat-msg-actions">
                    <button className="icon-btn" title="React"><Icon name="star" size={12} /></button>
                    <button className="icon-btn" title="Reply" onClick={() => { setReplyTo(i); }}><Icon name="chat" size={12} /></button>
                    <button className="icon-btn" title="Pin"><Icon name="pin" size={12} /></button>
                    <button className="icon-btn" title="More"><Icon name="more" size={12} /></button>
                  </div>
                </div>
              );
            })}
          </div>

          <div className="chat-composer">
            {replyTo != null && messages[replyTo] && (
              <div className="chat-reply-bar">
                <Icon name="chat" size={12} />
                <span>Replying to <strong>{getEmployee(messages[replyTo].who)?.name}</strong></span>
                <span className="chat-reply-bar-snip">{messages[replyTo].text.replace(/[*`#@]/g, '')}</span>
                <button className="icon-btn" style={{ width: 20, height: 20, marginLeft: 'auto' }} title="Cancel reply" onClick={() => setReplyTo(null)}><Icon name="x" size={11} /></button>
              </div>
            )}
            <textarea
              value={draft}
              onChange={e => setDraft(e.target.value)}
              onKeyDown={onKey}
              placeholder={`Message ${isDM ? dmUser?.name : '#' + channel?.name}…`}
              rows={1} />
            <div className="row spread" style={{ paddingTop: 8, borderTop: '1px solid var(--border)', marginTop: 8 }}>
              <div className="row gap-1">
                <button className="icon-btn" title="Bold"><strong style={{ fontSize: 11 }}>B</strong></button>
                <button className="icon-btn" title="Italic"><em style={{ fontSize: 11 }}>I</em></button>
                <button className="icon-btn" title="Code"><span className="mono" style={{ fontSize: 10 }}>{'<>'}</span></button>
                <span style={{ width: 1, background: 'var(--border)', margin: '0 4px' }} />
                <button className="icon-btn" title="Attach"><Icon name="link" size={13} /></button>
                <button className="icon-btn" title="Emoji"><Icon name="star" size={13} /></button>
                <button className="icon-btn" title="Mention"><span style={{ fontSize: 12, fontWeight: 600 }}>@</span></button>
              </div>
              <div className="row gap-2">
                <span className="dim" style={{ fontSize: 11 }}>Enter to send · Shift+Enter for newline</span>
                <Button size="sm" variant="primary" icon="send" disabled={!draft.trim()} onClick={send}>Send</Button>
              </div>
            </div>
          </div>
        </main>
      </div>
    </div>
  );
}

// Tiny message renderer — handles @mentions, #channels, code, bold, emoji shortcodes
function renderMessage(text) {
  if (!text) return '';
  let out = String(text)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  out = out.replace(/`([^`]+)`/g, '<code class="chat-code">$1</code>');
  out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  out = out.replace(/@([a-z0-9_-]+)/gi, '<span class="chat-mention">@$1</span>');
  out = out.replace(/(^|\s)#([a-z0-9_-]+)/gi, '$1<span class="chat-channel-ref">#$2</span>');
  out = out.replace(/:([a-z_-]+):/gi, (m, code) => {
    const map = { 'fire': '🔥', 'rocket': '🚀', 'tada': '🎉', 'heart': '❤️', 'eyes': '👀' };
    return map[code.toLowerCase()] || m;
  });
  return out;
}

window.ChatOverlay = ChatOverlay;
