// Chart Growth Hub — floating chat dock (v2, two-pane).
//
// One component for both portals. Staff get the full room list (team room,
// boardroom, broadcasts, signal view, and a support thread per student); a
// student gets their own coach thread plus whichever channels their tier lets
// them read.
//
// v2 changes: two-pane layout (list stays on the left, the room fills the
// right), mono message type at 16px for legibility, brighter bubbles, a
// "new messages" divider, reactions, staff-only read receipts, pinned
// messages, per-room mute (per device), a drag-resizable panel, and simple
// link cards.
//
// Exposes window.ChatDock({client, email, role, name, mode, tier, isVIP,
//                          openThread, onThreadOpened}).
//   mode 'staff'   — full room list, needs role
//   mode 'student' — own coach thread + eligible channels
(function(){
const { useState, useEffect, useRef, useMemo, useCallback } = React;

const C = window.ChatCommon || {};
const fmtTime = C.fmtTime || (d => new Date(d).toLocaleTimeString(undefined,{hour:'numeric',minute:'2-digit'}));
const dayKey  = C.dayKey  || (d => String(d).slice(0,10));
const dayLabel = C.dayLabel || (d => new Date(d).toLocaleDateString(undefined,{month:'short',day:'numeric'}));

const EMOJI = {
  'Smileys': ['😀','😄','😁','😊','🙂','😉','😍','🥰','😘','😎','🤩','🤗','🤔','😐','😑','🙄','😴','😮','😢','😭','😤','😡','🥳','😇','🤝','🙏'],
  'Gestures':['👍','👎','👌','✌️','🤞','👏','🙌','💪','👋','✋','🫡','🤙','☝️','👇','👉','👈'],
  'Trading': ['📈','📉','💹','💰','💵','💸','🏦','⚖️','🎯','🔔','⏰','⚡','🔥','💎','🚀','🧠','📊','📌','✅','❌','⚠️','🛑'],
  'Hearts':  ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💯','⭐','🌟','✨','🎉','🎊','🏆','👑'],
};
// Reactions people actually reach for — the full picker is one tap further in.
const QUICK_REACTIONS = ['👍','🔥','✅','🙏','😂','❤️'];

/* ── per-device room mute ────────────────────────────────────────────────
   Kept in localStorage rather than a table: muting is a personal, per-device
   preference, and this way it needs no migration to work today. */
const MUTE_KEY = 'cg_chat_muted';
function mutedSet(){ try{ return new Set(JSON.parse(localStorage.getItem(MUTE_KEY)||'[]')); }catch(e){ return new Set(); } }
function isMuted(key){ return mutedSet().has(key); }
function toggleMuted(key){ const s = mutedSet(); if(s.has(key)) s.delete(key); else s.add(key);
  try{ localStorage.setItem(MUTE_KEY, JSON.stringify([...s])); }catch(e){} return s.has(key); }
// chat-notify.js reads this so a muted room stays silent everywhere.
window.cgChatMutedThreads = () => [...mutedSet()];

const SIZE_KEY = 'cg_chat_dock_size';
function savedSize(){ try{ return JSON.parse(localStorage.getItem(SIZE_KEY)||'null'); }catch(e){ return null; } }

/* ── styles ──────────────────────────────────────────────────────────────── */
(function css(){
  if(document.getElementById('cg-chatdock-css')) document.getElementById('cg-chatdock-css').remove();
  const s = document.createElement('style');
  s.id = 'cg-chatdock-css';
  s.textContent = `
  .cd-launch{position:fixed;left:24px;bottom:24px;z-index:250;width:68px;height:68px;border-radius:50%;
    border:1px solid var(--border-gold);cursor:pointer;display:grid;place-items:center;
    background:radial-gradient(circle at 34% 24%,rgba(255,255,255,.55),rgba(255,255,255,0) 46%),
      linear-gradient(155deg,var(--gold-2),var(--gold) 62%,var(--gold-2));
    box-shadow:0 14px 30px rgba(0,0,0,.5),0 0 0 6px rgba(212,175,55,.10),
      inset 0 2px 1px rgba(255,255,255,.6),inset 0 -8px 14px rgba(90,60,0,.35);
    transition:transform .18s cubic-bezier(.2,.8,.2,1),box-shadow .18s}
  .cd-launch::after{content:'';position:absolute;inset:5px;border-radius:50%;pointer-events:none;
    background:linear-gradient(180deg,rgba(255,255,255,.42),rgba(255,255,255,0) 52%)}
  .cd-launch:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 20px 42px rgba(0,0,0,.55),0 0 0 9px rgba(212,175,55,.14),inset 0 2px 1px rgba(255,255,255,.65),inset 0 -8px 14px rgba(90,60,0,.35)}
  .cd-launch:active{transform:translateY(1px) scale(.99);box-shadow:0 6px 14px rgba(0,0,0,.5),inset 0 3px 8px rgba(90,60,0,.5)}
  .cd-launch svg{width:30px;height:30px;fill:none;stroke:#1a1303;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;
    filter:drop-shadow(0 1px 0 rgba(255,255,255,.45));position:relative;z-index:1}
  .cd-launch .cd-badge{position:absolute;top:-3px;right:-3px;font-size:.7rem;padding:4px 7px;min-width:21px}
  .cd-launch.has{animation:cdPulse 2.4s ease-out infinite}
  @keyframes cdPulse{
    0%{box-shadow:0 10px 30px rgba(0,0,0,.45),0 0 0 0 rgba(212,175,55,.42)}
    70%{box-shadow:0 10px 30px rgba(0,0,0,.45),0 0 0 16px rgba(212,175,55,0)}
    100%{box-shadow:0 10px 30px rgba(0,0,0,.45),0 0 0 0 rgba(212,175,55,0)}}
  @media(prefers-reduced-motion:reduce){ .cd-launch.has{animation:none} }
  .cd-badge{font-family:var(--font-mono);font-size:.68rem;font-weight:800;line-height:1;
    background:radial-gradient(circle at 34% 26%,rgba(255,255,255,.5),rgba(255,255,255,0) 55%),var(--red);
    color:#fff;padding:4px 7px;border-radius:999px;min-width:19px;text-align:center;
    box-shadow:0 3px 7px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.45),0 0 0 2px var(--navy-950)}

  /* shell — two panes: rooms on the left, the conversation on the right */
  .cd-panel{position:fixed;left:22px;bottom:22px;z-index:251;display:flex;border-radius:18px;overflow:hidden;
    border:1px solid var(--border-gold);background:var(--navy-950);
    box-shadow:0 26px 70px rgba(0,0,0,.62);animation:cdIn .22s cubic-bezier(.2,.8,.2,1) both}
  @keyframes cdIn{from{opacity:0;transform:translateY(14px) scale(.985)}to{opacity:1;transform:none}}
  .cd-side{width:266px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);
    background:var(--navy-900)}
  .cd-main{flex:1;min-width:0;display:flex;flex-direction:column}
  .cd-resize{position:absolute;top:0;right:0;width:22px;height:22px;cursor:nesw-resize;z-index:6}
  .cd-resize::after{content:'';position:absolute;top:7px;right:7px;width:9px;height:9px;
    border-top:2px solid var(--border-gold);border-right:2px solid var(--border-gold);border-radius:2px 3px 0 0}

  .cd-head{display:flex;align-items:center;gap:10px;padding:13px 14px;flex-shrink:0;min-height:60px;
    border-bottom:1px solid var(--border);
    background:linear-gradient(135deg,rgba(212,175,55,.16),transparent 70%),var(--navy-900)}
  /* Chat heads: lit glass orbs, not flat circles. A radial key light top-left,
     a rim light, an inner floor shadow, and a specular sheen layer. */
  .cd-avatar,.cd-av{position:relative;border-radius:50%;flex-shrink:0;display:grid;place-items:center;
    font-family:var(--font-mono);font-weight:800;color:var(--gold-2);
    border:1px solid var(--border-gold);isolation:isolate;
    background:radial-gradient(circle at 32% 24%,rgba(255,255,255,.34),rgba(255,255,255,0) 46%),
      linear-gradient(158deg,var(--navy-700),var(--navy-950) 78%);
    box-shadow:0 5px 12px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.30),
      inset 0 -7px 13px rgba(0,0,0,.45)}
  .cd-avatar::after,.cd-av::after{content:'';position:absolute;inset:2px;border-radius:50%;pointer-events:none;
    background:linear-gradient(180deg,rgba(255,255,255,.22),rgba(255,255,255,0) 48%)}
  .cd-avatar{width:38px;height:38px;font-size:.78rem;overflow:hidden}
  .cd-avatar img{width:100%;height:100%;object-fit:contain;padding:6px;border-radius:50%;
    filter:drop-shadow(0 2px 3px rgba(0,0,0,.5))}
  .cd-av{width:31px;height:31px;font-size:.66rem}
  .cd-av.hide{visibility:hidden}
  .cd-avatar svg,.cd-av svg{position:relative;z-index:1;fill:none;stroke:var(--gold-2);stroke-width:1.9;
    stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}
  .cd-avatar svg{width:20px;height:20px} .cd-av svg{width:17px;height:17px}
  /* one hue per room type, so a channel is recognisable at a glance */
  .cd-av.h-gold,.cd-avatar.h-gold{background:radial-gradient(circle at 32% 24%,rgba(255,255,255,.5),rgba(255,255,255,0) 46%),linear-gradient(158deg,var(--gold-2),var(--gold) 80%);color:#1a1303}
  .cd-av.h-gold svg,.cd-avatar.h-gold svg{stroke:#1a1303}
  .cd-av.h-em,.cd-avatar.h-em{background:radial-gradient(circle at 32% 24%,rgba(255,255,255,.42),rgba(255,255,255,0) 46%),linear-gradient(158deg,#3ddba0,#0b6647 82%);color:#04150f}
  .cd-av.h-em svg{stroke:#062018}
  .cd-av.h-sky,.cd-avatar.h-sky{background:radial-gradient(circle at 32% 24%,rgba(255,255,255,.42),rgba(255,255,255,0) 46%),linear-gradient(158deg,#57c7f7,#0a5687 82%);color:#04141f}
  .cd-av.h-sky svg{stroke:#04202f}
  .cd-av.h-ind,.cd-avatar.h-ind{background:radial-gradient(circle at 32% 24%,rgba(255,255,255,.42),rgba(255,255,255,0) 46%),linear-gradient(158deg,#a9b6ff,#3b34b8 82%);color:#0d0f2b}
  .cd-av.h-ind svg{stroke:#0d0f2b}
  .cd-av.h-slate,.cd-avatar.h-slate{background:radial-gradient(circle at 32% 24%,rgba(255,255,255,.36),rgba(255,255,255,0) 46%),linear-gradient(158deg,#cbd5e1,#55637a 82%);color:#10151f}
  .cd-av.h-slate svg{stroke:#141b26}
  .cd-head h4{margin:0;font-size:.98rem;font-weight:700;line-height:1.2;color:var(--ink);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .cd-sub{font-size:.75rem;color:var(--ink-dim);margin-top:2px;font-family:var(--font-mono);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* Glass 3D control buttons — a lit top edge, a soft floor shadow, and a real
     press. Flat squares read as unfinished at this size. */
  .cd-icobtn{border:1px solid var(--border);padding:7px;border-radius:11px;cursor:pointer;color:var(--ink-dim);
    display:grid;place-items:center;flex-shrink:0;position:relative;
    background:linear-gradient(165deg,rgba(255,255,255,.12),rgba(255,255,255,.02));
    -webkit-backdrop-filter:blur(9px);backdrop-filter:blur(9px);
    box-shadow:0 3px 8px rgba(0,0,0,.36),inset 0 1px 0 rgba(255,255,255,.20);
    transition:transform .13s,box-shadow .13s,color .13s,border-color .13s}
  .cd-icobtn:hover{color:var(--gold-2);border-color:var(--border-gold);transform:translateY(-1px);
    background:linear-gradient(165deg,rgba(255,255,255,.18),rgba(212,175,55,.07));
    box-shadow:0 6px 14px rgba(0,0,0,.42),inset 0 1px 0 rgba(255,255,255,.28)}
  .cd-icobtn:active{transform:translateY(1px);
    box-shadow:inset 0 3px 8px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.06)}
  .cd-icobtn.on{color:var(--gold-2)}
  .cd-icobtn.off{opacity:.55}
  .cd-icobtn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 1px 1px rgba(0,0,0,.45))}

  .cd-threads{flex:1;overflow-y:auto;padding:6px}
  .cd-thread{display:flex;gap:10px;align-items:center;width:100%;padding:11px 11px;border-radius:11px;
    border:0;background:none;cursor:pointer;text-align:left;color:var(--ink);font-family:inherit}
  .cd-thread:hover{background:var(--surface)}
  .cd-thread.on{background:rgba(212,175,55,.13);box-shadow:inset 3px 0 0 var(--gold-2)}
  .cd-thread .t{flex:1;min-width:0}
  .cd-thread .t b{display:block;font-size:.9rem;font-weight:700;color:var(--ink);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .cd-thread .t span{display:block;font-size:.74rem;color:var(--ink-dim);white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis;margin-top:1px}
  .cd-thread .cd-av{width:36px;height:36px}
  .cd-grouphead{font-size:.66rem;letter-spacing:.11em;text-transform:uppercase;color:var(--ink-dim);
    padding:13px 12px 6px;font-family:var(--font-mono);font-weight:700}

  .cd-body{flex:1;overflow-y:auto;padding:16px 16px 8px;display:flex;flex-direction:column;gap:2px;
    background:radial-gradient(1100px 420px at 20% -10%,rgba(212,175,55,.05),transparent 60%)}
  .cd-day{align-self:center;margin:14px 0 10px;font-size:.7rem;font-family:var(--font-mono);
    letter-spacing:.07em;text-transform:uppercase;color:var(--ink-dim);font-weight:700;
    background:var(--surface);border:1px solid var(--border);border-radius:999px;padding:4px 13px}
  .cd-newline{display:flex;align-items:center;gap:10px;margin:14px 2px 8px;
    font-family:var(--font-mono);font-size:.68rem;font-weight:800;letter-spacing:.1em;
    text-transform:uppercase;color:var(--gold-2)}
  .cd-newline::before,.cd-newline::after{content:'';flex:1;height:1px;background:var(--border-gold)}

  .cd-row{display:flex;gap:9px;align-items:flex-end;margin-top:3px;position:relative}
  .cd-row.me{flex-direction:row-reverse}
  .cd-row.first{margin-top:13px}
  .cd-row.first .cd-bub{border-bottom-left-radius:5px}
  .cd-row.me.first .cd-bub{border-bottom-left-radius:16px;border-bottom-right-radius:5px}
  /* Bubbles: brighter fills, real borders and a lit top edge so they sit above
     the ground rather than melting into it. */
  .cd-bub{max-width:78%;padding:10px 14px 8px;border-radius:16px;position:relative;
    background:linear-gradient(168deg,var(--navy-850),var(--navy-900));border:1px solid var(--border);
    min-width:80px;box-shadow:0 4px 12px rgba(0,0,0,.34),inset 0 1px 0 rgba(255,255,255,.07)}
  .cd-row.me .cd-bub{background:linear-gradient(155deg,rgba(212,175,55,.34),rgba(212,175,55,.16));
    border-color:var(--border-gold);box-shadow:0 5px 14px rgba(0,0,0,.38),inset 0 1px 0 rgba(255,255,255,.22)}
  .cd-name{font-size:.76rem;font-weight:800;color:var(--gold-2);margin-bottom:3px;
    font-family:var(--font-mono);letter-spacing:.02em}
  /* mono at 16px — the readability fix */
  .cd-text{font-family:var(--font-mono);font-size:16px;line-height:1.62;color:var(--ink);
    white-space:pre-wrap;word-break:break-word;font-weight:500}
  .cd-text.gone{color:var(--ink-dim);font-style:italic;font-weight:400}
  .cd-meta{display:flex;align-items:center;gap:6px;justify-content:flex-end;margin-top:4px;
    font-family:var(--font-mono);font-size:.7rem;color:var(--ink-dim);line-height:1}
  .cd-meta svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2.2;
    stroke-linecap:round;stroke-linejoin:round}
  .cd-meta .seen{color:var(--gold-2)}
  .cd-quote{border-left:3px solid var(--gold-2);padding:5px 9px;margin-bottom:7px;border-radius:5px;
    background:rgba(212,175,55,.09);cursor:pointer}
  .cd-quote b{display:block;font-size:.72rem;color:var(--gold-2);font-family:var(--font-mono)}
  .cd-quote span{display:block;font-size:.8rem;color:var(--ink-dim);white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis;max-width:230px}

  .cd-link{display:flex;align-items:center;gap:9px;margin-top:8px;padding:9px 11px;border-radius:11px;
    border:1px solid var(--border-gold);background:rgba(212,175,55,.07);text-decoration:none;
    color:var(--ink);max-width:100%}
  .cd-link:hover{background:rgba(212,175,55,.13)}
  .cd-link svg{width:17px;height:17px;flex-shrink:0;fill:none;stroke:var(--gold-2);stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round}
  .cd-link .l{min-width:0}
  .cd-link b{display:block;font-size:.8rem;font-family:var(--font-mono);color:var(--gold-2)}
  .cd-link span{display:block;font-size:.74rem;color:var(--ink-dim);white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis;max-width:240px}

  .cd-rx{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}
  .cd-rx button{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:999px;
    border:1px solid var(--border);cursor:pointer;font-size:.86rem;line-height:1.3;
    color:var(--ink-dim);font-family:var(--font-mono);font-weight:700;
    background:linear-gradient(165deg,rgba(255,255,255,.12),rgba(255,255,255,.02));
    -webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);
    box-shadow:0 2px 5px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.2)}
  .cd-rx button:active{transform:translateY(1px)}
  .cd-rx button.mine{border-color:var(--border-gold);background:var(--gold-soft);color:var(--gold-2)}
  .cd-rxpick{position:absolute;top:-42px;display:flex;gap:2px;padding:4px;border-radius:12px;
    background:var(--navy-900);border:1px solid var(--border-gold);box-shadow:0 8px 22px rgba(0,0,0,.5);z-index:6}
  .cd-row.me .cd-rxpick{right:8px} .cd-row:not(.me) .cd-rxpick{left:8px}
  .cd-rxpick button{background:none;border:0;font-size:1.1rem;padding:4px 6px;border-radius:8px;cursor:pointer}
  .cd-rxpick button:hover{background:var(--surface)}

  .cd-acts{position:absolute;top:-13px;display:flex;gap:1px;padding:3px;border-radius:10px;
    background:var(--navy-900);border:1px solid var(--border);box-shadow:0 6px 18px rgba(0,0,0,.45);
    opacity:0;pointer-events:none;transition:opacity .12s;z-index:3}
  .cd-row:hover .cd-acts,.cd-acts.open{opacity:1;pointer-events:auto}
  .cd-row.me .cd-acts{left:8px}
  .cd-row:not(.me) .cd-acts{right:8px}
  .cd-acts button{background:none;border:0;padding:6px;border-radius:7px;cursor:pointer;
    color:var(--ink-dim);display:grid;place-items:center}
  .cd-acts button:hover{background:var(--surface);color:var(--gold-2)}
  .cd-acts button.danger:hover{color:var(--red);background:rgba(248,113,113,.1)}
  .cd-acts svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round}

  .cd-pins{flex-shrink:0;border-bottom:1px solid var(--border);background:rgba(212,175,55,.08);
    padding:8px 13px;display:flex;flex-direction:column;gap:5px;max-height:104px;overflow-y:auto}
  .cd-pin{display:flex;align-items:center;gap:8px;font-size:.82rem;color:var(--ink);cursor:pointer}
  .cd-pin svg{width:14px;height:14px;flex-shrink:0;fill:none;stroke:var(--gold-2);stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round}
  .cd-pin span{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
    font-family:var(--font-mono)}
  .cd-pin button{background:none;border:0;color:var(--ink-dim);cursor:pointer;font-size:.72rem;
    font-family:var(--font-mono);flex-shrink:0}
  .cd-pin button:hover{color:var(--red)}

  .cd-foot{flex-shrink:0;border-top:1px solid var(--border);background:var(--navy-900)}
  .cd-reply{display:flex;align-items:center;gap:8px;padding:9px 13px;border-bottom:1px solid var(--border);
    background:rgba(212,175,55,.07)}
  .cd-reply .r{flex:1;min-width:0;border-left:3px solid var(--gold-2);padding-left:9px}
  .cd-reply b{display:block;font-size:.72rem;color:var(--gold-2);font-family:var(--font-mono)}
  .cd-reply span{display:block;font-size:.82rem;color:var(--ink-dim);white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis}
  .cd-compose{display:flex;align-items:flex-end;gap:8px;padding:10px 11px}
  .cd-compose textarea{flex:1;resize:none;max-height:132px;min-height:44px;padding:11px 13px;
    border-radius:13px;border:1px solid var(--border);background:var(--navy-950);color:var(--ink);
    font-family:var(--font-mono);font-size:15px;line-height:1.5}
  .cd-compose textarea::placeholder{color:var(--ink-faint)}
  .cd-compose textarea:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 3px var(--gold-soft)}
  .cd-send{width:42px;height:42px;flex-shrink:0;border-radius:50%;border:1px solid var(--border-gold);
    cursor:pointer;display:grid;place-items:center;position:relative;
    background:radial-gradient(circle at 34% 24%,rgba(255,255,255,.55),rgba(255,255,255,0) 48%),
      linear-gradient(155deg,var(--gold-2),var(--gold) 70%);
    box-shadow:0 6px 14px rgba(0,0,0,.45),inset 0 1px 0 rgba(255,255,255,.55),
      inset 0 -6px 11px rgba(90,60,0,.32);
    transition:filter .15s,opacity .15s,transform .12s,box-shadow .12s}
  .cd-send:disabled{opacity:.4;cursor:default}
  .cd-send:not(:disabled):hover{filter:brightness(1.07);transform:translateY(-1px)}
  .cd-send:not(:disabled):active{transform:translateY(1px);
    box-shadow:0 2px 6px rgba(0,0,0,.45),inset 0 3px 8px rgba(90,60,0,.5)}
  .cd-send svg{width:18px;height:18px;fill:none;stroke:#1a1303;stroke-width:2.2;
    stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 1px 0 rgba(255,255,255,.4))}
  .cd-readonly{padding:13px 15px;font-size:.82rem;color:var(--ink-dim);text-align:center;
    font-family:var(--font-mono)}

  .cd-emoji{position:absolute;bottom:60px;left:10px;right:10px;max-height:230px;overflow-y:auto;
    background:var(--navy-900);border:1px solid var(--border-gold);border-radius:13px;padding:8px;
    box-shadow:0 -8px 28px rgba(0,0,0,.45);z-index:5}
  .cd-emoji h6{margin:6px 4px 4px;font-size:.66rem;font-family:var(--font-mono);letter-spacing:.09em;
    text-transform:uppercase;color:var(--ink-dim);font-weight:700}
  .cd-emoji .g{display:grid;grid-template-columns:repeat(8,1fr);gap:2px}
  .cd-emoji button{background:none;border:0;font-size:1.2rem;line-height:1;padding:6px;border-radius:7px;cursor:pointer}
  .cd-emoji button:hover{background:var(--surface)}

  .cd-ask{position:absolute;inset:0;z-index:8;display:grid;place-items:center;padding:18px;
    background:rgba(4,7,14,.74);backdrop-filter:blur(3px)}
  .cd-askcard{background:var(--navy-900);border:1px solid var(--border-gold);border-radius:14px;
    padding:17px 19px;max-width:320px;box-shadow:0 18px 44px rgba(0,0,0,.55)}
  .cd-askcard b{display:block;font-size:.96rem;margin-bottom:7px;color:var(--ink)}
  .cd-askcard p{font-size:.83rem;line-height:1.55;color:var(--ink-dim);margin:0 0 15px}
  .cd-askbtns{display:flex;gap:8px;justify-content:flex-end}
  .cd-askbtns button{padding:8px 15px;border-radius:9px;font-size:.83rem;font-weight:700;
    cursor:pointer;font-family:inherit;border:1px solid var(--border);background:var(--surface);
    color:var(--ink-dim)}
  .cd-askno:hover{border-color:var(--border-gold);color:var(--gold-2)}
  .cd-askyes{border-color:var(--border-gold) !important;color:var(--gold-2) !important;
    background:var(--gold-soft) !important}
  .cd-askyes.danger{border-color:rgba(248,113,113,.45) !important;color:var(--red) !important;
    background:rgba(248,113,113,.12) !important}

  .cd-sig{border:1px solid var(--border);border-left-width:3px;border-radius:10px;padding:10px 12px;
    background:var(--surface);margin-bottom:9px}
  .cd-sig-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:.86rem}
  .cd-sig-top strong{font-family:var(--font-mono);letter-spacing:.03em;color:var(--ink)}
  .cd-sig-top span{font-family:var(--font-mono);font-size:.78rem;font-weight:800}
  .cd-sig-top em{font-style:normal;font-size:.74rem;color:var(--ink-dim);font-family:var(--font-mono)}
  .cd-sig-when{margin-left:auto;color:var(--ink-dim);font-weight:400 !important}
  .cd-sig-lv{font-family:var(--font-mono);font-size:.8rem;color:var(--ink-dim);margin-top:6px}
  .cd-sig-msg{font-size:.87rem;color:var(--ink);margin-top:7px;white-space:pre-wrap;line-height:1.5;
    font-family:var(--font-mono)}
  .cd-sig-by{font-family:var(--font-mono);font-size:.7rem;color:var(--ink-dim);margin-top:7px}

  .cd-typing{padding:3px 15px 7px;font-size:.76rem;color:var(--gold-2);font-family:var(--font-mono)}
  .cd-empty{flex:1;display:grid;place-items:center;padding:34px 22px;text-align:center;
    color:var(--ink-dim);font-size:.88rem}
  .cd-err{padding:8px 14px;font-size:.78rem;color:var(--red);font-family:var(--font-mono)}

  /* one pane at a time on narrow screens, full screen on phones */
  /* light theme: glass has to invert or it disappears on a white ground */
  html[data-cg-theme="light"] .cd-panel{background:var(--navy-900)}
  html[data-cg-theme="light"] .cd-side{background:var(--navy-850)}
  html[data-cg-theme="light"] .cd-icobtn{background:linear-gradient(165deg,rgba(255,255,255,.92),rgba(20,30,60,.05));
    box-shadow:0 3px 8px rgba(20,30,60,.14),inset 0 1px 0 rgba(255,255,255,.95)}
  html[data-cg-theme="light"] .cd-icobtn svg{filter:none}
  html[data-cg-theme="light"] .cd-bub{background:linear-gradient(168deg,var(--navy-900),var(--navy-850));
    box-shadow:0 3px 10px rgba(20,30,60,.12),inset 0 1px 0 rgba(255,255,255,.9)}
  html[data-cg-theme="light"] .cd-avatar,html[data-cg-theme="light"] .cd-av{
    box-shadow:0 4px 10px rgba(20,30,60,.22),inset 0 1px 0 rgba(255,255,255,.55),inset 0 -6px 12px rgba(20,30,60,.16)}
  html[data-cg-theme="light"] .cd-rx button{background:linear-gradient(165deg,rgba(255,255,255,.92),rgba(20,30,60,.05))}

  @media(max-width:900px){ .cd-side{width:100%;border-right:0} .cd-resize{display:none}
    .cd-panel{width:min(440px,calc(100vw - 32px));height:min(620px,calc(100vh - 44px))} }
  @media(max-width:640px){
    .cd-panel{left:0 !important;right:0;bottom:0;top:0;width:100% !important;height:100% !important;
      border-radius:0;border:0}
    .cd-launch{left:16px;bottom:16px}
    .cd-bub{max-width:86%}
  }`;
  document.head.appendChild(s);
})();

/* ── icons ───────────────────────────────────────────────────────────────── */
const I = {
  chat:  <svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>,
  close: <svg viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>,
  back:  <svg viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6"/></svg>,
  reply: <svg viewBox="0 0 24 24"><path d="M9 17l-6-5 6-5"/><path d="M3 12h10a6 6 0 0 1 6 6v2"/></svg>,
  edit:  <svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>,
  trash: <svg viewBox="0 0 24 24"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg>,
  copy:  <svg viewBox="0 0 24 24"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>,
  smile: <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8.5 14.5a4.5 4.5 0 0 0 7 0"/><path d="M9 9.5h.01M15 9.5h.01"/></svg>,
  react: <svg viewBox="0 0 24 24"><path d="M20.8 11.5A9 9 0 1 1 12.5 3.2"/><path d="M8.5 14.5a4.5 4.5 0 0 0 7 0"/><path d="M9 9.5h.01M15 9.5h.01"/><path d="M18 2v6M15 5h6"/></svg>,
  pin:   <svg viewBox="0 0 24 24"><path d="M12 17v5"/><path d="M9 3h6l-1 6 3.5 3H6.5L10 9 9 3z"/></svg>,
  send:  <svg viewBox="0 0 24 24"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg>,
  link:  <svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.5.5l2-2A5 5 0 0 0 12.5 4.5l-1 1"/><path d="M14 11a5 5 0 0 0-7.5-.5l-2 2A5 5 0 0 0 11.5 19.5l1-1"/></svg>,
  check: <svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg>,
  checks:<svg viewBox="0 0 24 24"><path d="M17 6L7.5 16 4 12.5"/><path d="M21 8l-8.5 9"/></svg>,
  bellOn:  <svg viewBox="0 0 24 24"><path d="M18 8a6 6 0 0 0-12 0c0 6-3 7-3 7h18s-3-1-3-7"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>,
  bellOff: <svg viewBox="0 0 24 24"><path d="M18 8a6 6 0 0 0-9.3-5"/><path d="M6 8c0 6-3 7-3 7h13"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/><path d="M3 3l18 18"/></svg>,
};

// One mark per room type — a channel is not the same kind of thing as a person.
const ROOM_ICON = {
  staff:     <svg viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.1"/><path d="M3.2 19a5.8 5.8 0 0 1 11.6 0"/><circle cx="17.5" cy="9.2" r="2.4"/><path d="M16 14.6a4.7 4.7 0 0 1 5 4.4"/></svg>,
  boardroom: <svg viewBox="0 0 24 24"><path d="M12 2l8 4v6c0 5-3.5 8-8 10-4.5-2-8-5-8-10V6l8-4z"/><path d="M9.2 12.2l2 2 3.6-3.8"/></svg>,
  all:       <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a15 15 0 0 1 0 18a15 15 0 0 1 0-18z"/></svg>,
  trial:     <svg viewBox="0 0 24 24"><path d="M12 20v-7"/><path d="M12 13c0-3.4 2.6-6 6-6 0 3.4-2.6 6-6 6z"/><path d="M12 15c0-2.8-2.2-5-5-5 0 2.8 2.2 5 5 5z"/></svg>,
  vip:       <svg viewBox="0 0 24 24"><path d="M3 8l4.2 3.2L12 5l4.8 6.2L21 8l-1.8 10H4.8L3 8z"/><path d="M4.8 20h14.4"/></svg>,
  signal:    <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.1"/><path d="M8.2 8.2a5.4 5.4 0 0 0 0 7.6"/><path d="M15.8 15.8a5.4 5.4 0 0 0 0-7.6"/><path d="M5.5 5.5a9.2 9.2 0 0 0 0 13"/><path d="M18.5 18.5a9.2 9.2 0 0 0 0-13"/></svg>,
};
function roomIcon(key){
  if(key==='staff')           return ROOM_ICON.staff;
  if(key==='boardroom')       return ROOM_ICON.boardroom;
  if(key==='broadcast:all')   return ROOM_ICON.all;
  if(key==='broadcast:trial') return ROOM_ICON.trial;
  if(key==='broadcast:vip')   return ROOM_ICON.vip;
  if(key==='signalview')      return ROOM_ICON.signal;
  return null;   // a person — initials read better
}

function hueClass(key){
  if(key==='staff')           return 'h-gold';
  if(key==='boardroom')       return 'h-em';
  if(key==='signalview')      return 'h-sky';
  if(key==='broadcast:all')   return 'h-ind';
  if(key==='broadcast:trial') return 'h-slate';
  if(key==='broadcast:vip')   return 'h-gold';
  return '';   // a person keeps the neutral navy orb
}

const initials = s => (String(s||'?').trim().split(/[\s@._-]+/).filter(Boolean).slice(0,2)
  .map(w=>w[0]).join('') || '?').toUpperCase();

let DOCK_SEQ = 0;

/* ── thread naming ───────────────────────────────────────────────────────── */
function threadTitle(key){
  if(key==='staff')     return 'Team room';
  if(key==='boardroom') return 'Boardroom';
  if(key==='broadcast:all')   return 'All members';
  if(key==='broadcast:trial') return 'Free trial tier';
  if(key==='broadcast:vip')   return 'VIP members';
  if(key==='signalview') return 'Signal View';
  if(key && key.indexOf('support:')===0) return key.slice(8);
  return key || '';
}
function threadSub(key){
  if(key==='staff')     return 'Coaches and admins';
  if(key==='boardroom') return 'Admin-only';
  if(key && key.indexOf('broadcast:')===0) return 'Broadcast channel';
  if(key==='signalview') return 'Live signal feed · read-only';
  if(key && key.indexOf('support:')===0) return 'Direct support';
  return '';
}
// Students read broadcast channels, they do not post in them.
function readOnlyFor(key, staffMode){
  if(key==='signalview') return true;
  if(!staffMode && String(key).indexOf('broadcast:')===0) return true;
  return false;
}
const URL_RE = /(https?:\/\/[^\s]+)/i;
function firstUrl(text){ const m = URL_RE.exec(String(text||'')); return m ? m[1].replace(/[.,)]+$/,'') : null; }
function hostOf(u){ try{ return new URL(u).hostname.replace(/^www\./,''); }catch(e){ return u; } }

/* The dock's own mark — the brand logo as a lit orb, with the chat glyph as a
   fallback if the image is missing. */
function BrandOrb(){
  const [bad,setBad] = useState(false);
  return <span className="cd-avatar h-gold">
    {bad ? I.chat : <img src="assets/brand/cgh-128.png" alt="Chart Growth Hub" onError={()=>setBad(true)}/>}
  </span>;
}

/* ── one message ─────────────────────────────────────────────────────────── */
function Bubble({m, mine, first, canPurge, canPin, quoted, reactions, meEmail, seen, showReceipts,
                 onReply, onEdit, onDelete, onPurge, onPin, onJumpTo, onReact}){
  const [picker,setPicker] = useState(false);
  const gone = !!m.deleted;
  const url = gone ? null : firstUrl(m.body);
  const counts = {};
  (reactions||[]).forEach(r=>{ counts[r.emoji] = (counts[r.emoji]||0)+1; });
  const mineRx = (reactions||[]).filter(r=>String(r.reactor_email).toLowerCase()===meEmail).map(r=>r.emoji);
  return <div className={'cd-row'+(mine?' me':'')+(first?' first':'')}>
    <span className={'cd-av'+(mine?' h-gold':'')+(first?'':' hide')} title={m.sender_name||m.sender_email}>
      {initials(m.sender_name||m.sender_email)}
    </span>
    <div className="cd-bub">
      {first && !mine && <div className="cd-name">{m.sender_name||m.sender_email}</div>}

      {quoted && <div className="cd-quote" onClick={()=>onJumpTo && onJumpTo(quoted.id)}
        title="Go to the message being replied to">
        <b>{quoted.sender_name||quoted.sender_email}</b>
        <span>{quoted.deleted ? 'Deleted message' : quoted.body}</span>
      </div>}

      <div className={'cd-text'+(gone?' gone':'')}>
        {gone ? 'This message was deleted' : m.body}
      </div>

      {url && <a className="cd-link" href={url} target="_blank" rel="noopener noreferrer">
        {I.link}<span className="l"><b>{hostOf(url)}</b><span>{url}</span></span>
      </a>}

      {Object.keys(counts).length>0 && <div className="cd-rx">
        {Object.keys(counts).map(e=>
          <button key={e} className={mineRx.indexOf(e)!==-1?'mine':''} onClick={()=>onReact(m,e)}>
            {e}<span>{counts[e]}</span></button>)}
      </div>}

      <div className="cd-meta">
        {m.pinned && !gone && <span title="Pinned">{I.pin}</span>}
        {m.edited_at && !gone && <span title={'Edited '+fmtTime(m.edited_at)}>edited</span>}
        <span title={new Date(m.created_at).toLocaleString()}>{fmtTime(m.created_at)}</span>
        {mine && !gone && showReceipts && <span className={seen?'seen':''} title={seen?'Seen':'Delivered'}>
          {seen ? I.checks : I.check}</span>}
      </div>

      {picker && !gone && <div className="cd-rxpick">
        {QUICK_REACTIONS.map(e=><button key={e} onClick={()=>{ onReact(m,e); setPicker(false); }}>{e}</button>)}
      </div>}

      {!gone && <div className={'cd-acts'+(picker?' open':'')}>
        <button onClick={()=>setPicker(v=>!v)} title="React">{I.react}</button>
        <button onClick={()=>onReply(m)} title="Reply">{I.reply}</button>
        {canPin && <button onClick={()=>onPin(m)} title={m.pinned?'Unpin':'Pin'}>{I.pin}</button>}
        {mine && <button onClick={()=>onEdit(m)} title="Edit">{I.edit}</button>}
        <button onClick={()=>{ try{ navigator.clipboard.writeText(m.body||''); }catch(e){} }}
          title="Copy text">{I.copy}</button>
        {(mine || canPurge) && <button className="danger" onClick={()=>onDelete(m)} title="Delete">{I.trash}</button>}
      </div>}
      {gone && canPurge && <div className="cd-acts">
        <button className="danger" onClick={()=>onPurge(m)} title="Remove permanently">{I.trash}</button>
      </div>}
    </div>
  </div>;
}

/* ── sound toggle (global) ───────────────────────────────────────────────── */
function SoundToggle(){
  const api = window.cgChatNotify;
  const [on,setOn] = useState(()=> api && api.soundOn ? !!api.soundOn() : true);
  if(!api || !api.setSound) return null;
  return <button className={'cd-icobtn'+(on?'':' off')} title={on?'Sound on':'Sound muted'}
    aria-label={on?'Mute notification sound':'Unmute notification sound'}
    onClick={()=>{ const v = !on; api.setSound(v); setOn(v); }}>
    {on ? I.bellOn : I.bellOff}
  </button>;
}

/* ── emoji picker ────────────────────────────────────────────────────────── */
function EmojiPicker({onPick, onClose}){
  const ref = useRef(null);
  useEffect(()=>{
    const h = e => { if(ref.current && !ref.current.contains(e.target)) onClose(); };
    const t = setTimeout(()=>document.addEventListener('mousedown',h),0);
    return ()=>{ clearTimeout(t); document.removeEventListener('mousedown',h); };
  },[onClose]);
  return <div className="cd-emoji" ref={ref}>
    {Object.keys(EMOJI).map(g=><div key={g}>
      <h6>{g}</h6>
      <div className="g">{EMOJI[g].map(e=>
        <button key={e} type="button" onClick={()=>onPick(e)}>{e}</button>)}</div>
    </div>)}
  </div>;
}

/* ── read-only signal feed ───────────────────────────────────────────────── */
function SignalFeed({client}){
  const [rows,setRows] = useState(null);
  useEffect(()=>{ let stop=false;
    async function load(){
      if(stop) return;
      const { data } = await client.from('signals').select('*')
        .order('created_at',{ascending:false}).limit(40);
      if(!stop) setRows(data||[]);
    }
    load(); const t=setInterval(load,15000);
    return ()=>{ stop=true; clearInterval(t); };
  },[client]);

  if(rows===null) return <div className="cd-empty"><span className="cg-spin"></span></div>;
  if(!rows.length) return <div className="cd-empty">No signals published yet.</div>;
  return <React.Fragment>{rows.map(r=>{
    const buy = r.direction==='buy';
    return <div key={r.id} className="cd-sig" style={{borderLeftColor: buy?'var(--green)':'var(--red)'}}>
      <div className="cd-sig-top">
        <strong>{r.pair}</strong>
        <span style={{color: buy?'var(--green)':'var(--red)'}}>{buy?'▲ BUY':'▼ SELL'}</span>
        {r.timeframe && <em>{r.timeframe}</em>}
        <span className="cd-sig-when">{fmtTime(r.created_at)}</span>
      </div>
      <div className="cd-sig-lv">
        {['Entry '+(r.entry||'—'),'SL '+(r.stop_loss||'—'),
          'TP '+(r.take_profit_1||r.take_profit||'—')].join('   ·   ')}
      </div>
      {r.message && <div className="cd-sig-msg">{r.message}</div>}
      <div className="cd-sig-by">{r.coach_name||'Coach'}{r.status?' · '+r.status.replace(/_/g,' '):''}</div>
    </div>;
  })}</React.Fragment>;
}

/* ── the dock ────────────────────────────────────────────────────────────── */
function ChatDock({client, email, role, name, mode, tier, isVIP, openThread, onThreadOpened}){
  const meEmail = (email||'').toLowerCase();
  const isMain  = role === 'main';
  const staffMode = mode !== 'student';
  const isStaff = staffMode;

  const [open,setOpen]     = useState(false);
  const [thread,setThread] = useState(null);
  const [msgs,setMsgs]     = useState(null);
  const [threads,setThreads] = useState([]);
  const [unread,setUnread] = useState({});
  const [reactions,setReactions] = useState([]);
  const [otherReadAt,setOtherReadAt] = useState(null);
  const [firstUnreadAt,setFirstUnreadAt] = useState(null);
  const [body,setBody]     = useState('');
  const [replyTo,setReplyTo] = useState(null);
  const [editing,setEditing] = useState(null);
  const [emoji,setEmoji]   = useState(false);
  const [sending,setSending] = useState(false);
  const [err,setErr]       = useState('');
  const [typing,setTyping] = useState([]);
  const [muted,setMuted]   = useState(false);
  const [wide,setWide]     = useState(typeof window!=='undefined' && window.innerWidth>900);
  const [size,setSize]     = useState(()=> savedSize() || {w:780,h:640});
  const [ask,setAsk] = useState(null);   // {title, note, danger, run}
  const bodyRef = useRef(null);
  const taRef   = useRef(null);
  const lastTyped = useRef(0);
  const uid = useRef(null);
  if(uid.current === null) uid.current = 'cd' + (++DOCK_SEQ);
  const anchorId = id => uid.current + '-m-' + id;
  const myName = name || (email||'').split('@')[0];

  useEffect(()=>{ const r=()=>setWide(window.innerWidth>900); window.addEventListener('resize',r);
    return ()=>window.removeEventListener('resize',r); },[]);
  useEffect(()=>{ setMuted(thread ? isMuted(thread) : false); },[thread]);

  // A student always starts in their own thread; staff start on the list.
  const didInit = useRef(false);
  useEffect(()=>{ if(!staffMode && !didInit.current){ didInit.current = true; setThread('support:'+meEmail); } },[staffMode, meEmail]);

  useEffect(()=>{
    if(!openThread) return;
    setThread(openThread); setOpen(true); setReplyTo(null); setEditing(null);
    if(onThreadOpened) onThreadOpened();
    /* eslint-disable-next-line */
  },[openThread]);

  useEffect(()=>{
    window.cgOpenChat = () => setOpen(true);
    return ()=>{ if(window.cgOpenChat) delete window.cgOpenChat; };
  },[]);

  const totalUnread = useMemo(
    ()=>Object.keys(unread).reduce((s,k)=>s+(unread[k]||0),0), [unread]);

  /* unread counts across every thread this account can see */
  const loadUnread = useCallback(async ()=>{
    const { data:rd } = await client.from('chat_reads').select('*').eq('reader_email',meEmail);
    const readMap = {}; (rd||[]).forEach(r=>{ readMap[r.thread_key] = r.last_read_at; });
    const { data:all } = await client.from('chat_messages')
      .select('thread_key,created_at,sender_email,deleted').neq('sender_email',meEmail);
    const counts = {};
    (all||[]).forEach(m=>{
      if(m.deleted) return;
      const last = readMap[m.thread_key];
      if(!last || new Date(m.created_at) > new Date(last)) counts[m.thread_key] = (counts[m.thread_key]||0)+1;
    });
    setUnread(counts);
    if(staffMode){
      const keys = {};
      (all||[]).forEach(m=>{ if(String(m.thread_key).indexOf('support:')===0) keys[m.thread_key]=true; });
      setThreads(Object.keys(keys).sort());
    }
  },[client, meEmail, staffMode]);

  useEffect(()=>{ loadUnread(); const t=setInterval(loadUnread,10000); return ()=>clearInterval(t); },[loadUnread]);

  /* messages + reactions + read state for the open thread */
  const loadMsgs = useCallback(async ()=>{
    if(!thread) return;
    const { data, error } = await client.from('chat_messages').select('*')
      .eq('thread_key',thread).order('created_at',{ascending:true});
    if(error){ setErr(error.message); return; }
    setErr(''); setMsgs(data||[]);
    const { data:rx } = await client.from('chat_reactions').select('*').eq('thread_key',thread);
    setReactions(rx||[]);
    if(isStaff){
      const { data:rd } = await client.from('chat_reads').select('*').eq('thread_key',thread);
      const others = (rd||[]).filter(r=>String(r.reader_email).toLowerCase()!==meEmail);
      setOtherReadAt(others.length ? Math.max(...others.map(r=>new Date(r.last_read_at).getTime())) : null);
    }
  },[client, thread, isStaff, meEmail]);

  // Where the "new messages" line goes: the read mark as it stood when the
  // room was opened, captured before we mark it read.
  useEffect(()=>{
    if(!open || !thread){ return; }
    let stop = false;
    (async ()=>{
      const { data:rd } = await client.from('chat_reads').select('last_read_at')
        .eq('thread_key',thread).eq('reader_email',meEmail).maybeSingle();
      if(!stop) setFirstUnreadAt(rd && rd.last_read_at ? new Date(rd.last_read_at).getTime() : null);
    })();
    setMsgs(null); loadMsgs();
    const t = setInterval(loadMsgs, 6000);
    return ()=>{ stop = true; clearInterval(t); };
  },[open, thread, loadMsgs, client, meEmail]);

  /* mark read whenever the thread is open and messages change */
  useEffect(()=>{
    if(!open || !thread || !msgs) return;
    client.from('chat_reads').upsert({thread_key:thread, reader_email:meEmail,
      last_read_at:new Date().toISOString()}).then(()=>{
        setUnread(u => { const n = {...u}; delete n[thread]; return n; });
      });
  },[open, thread, msgs, client, meEmail]);

  /* who else is typing */
  useEffect(()=>{
    if(!open || !thread) return;
    let stop = false;
    async function poll(){
      if(stop) return;
      const { data } = await client.from('chat_typing').select('*').eq('thread_key',thread);
      const fresh = (data||[]).filter(r =>
        String(r.sender_email).toLowerCase() !== meEmail &&
        Date.now() - new Date(r.updated_at).getTime() < 6000);
      setTyping(fresh.map(r=>r.sender_email));
    }
    poll(); const t = setInterval(poll, 3000);
    return ()=>{ stop = true; clearInterval(t); };
  },[open, thread, client, meEmail]);

  function pingTyping(){
    const now = Date.now();
    if(now - lastTyped.current < 2500) return;
    lastTyped.current = now;
    client.from('chat_typing').upsert({thread_key:thread, sender_email:meEmail,
      updated_at:new Date().toISOString()}).then(()=>{}, ()=>{});
  }

  useEffect(()=>{ const el = bodyRef.current; if(el) el.scrollTop = el.scrollHeight; },[msgs, thread]);

  const byId = useMemo(()=>{ const m = {}; (msgs||[]).forEach(x=>{ m[x.id]=x; }); return m; },[msgs]);
  const rxFor = useCallback(id => reactions.filter(r=>String(r.message_id)===String(id)), [reactions]);
  const pinned = useMemo(()=> (msgs||[]).filter(m=>m.pinned && !m.deleted), [msgs]);
  const readOnly = readOnlyFor(thread, staffMode);

  /* drag to resize — the panel is anchored bottom-left, so it grows up/right */
  function startResize(e){
    e.preventDefault();
    const sx=e.clientX, sy=e.clientY, sw=size.w, sh=size.h;
    function move(ev){
      const w = Math.max(560, Math.min(1180, sw + (ev.clientX-sx)));
      const h = Math.max(420, Math.min(window.innerHeight-60, sh - (ev.clientY-sy)));
      setSize({w,h});
    }
    function up(){ document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up);
      try{ localStorage.setItem(SIZE_KEY, JSON.stringify(size)); }catch(err){} }
    document.addEventListener('mousemove',move); document.addEventListener('mouseup',up);
  }
  useEffect(()=>{ try{ localStorage.setItem(SIZE_KEY, JSON.stringify(size)); }catch(e){} },[size]);

  async function send(){
    const text = body.trim();
    if(!text || sending || !thread || readOnly) return;
    setSending(true); setErr('');
    if(editing){
      const { error } = await client.from('chat_messages')
        .update({body:text, edited_at:new Date().toISOString()}).eq('id',editing.id);
      setSending(false);
      if(error){ setErr(error.message); return; }
      setEditing(null); setBody(''); loadMsgs(); return;
    }
    const { error } = await client.from('chat_messages').insert({
      thread_key: thread, sender_email: meEmail, sender_name: myName,
      sender_role: role || (staffMode?'staff':'student'), body: text,
      reply_to_id: replyTo ? replyTo.id : null,
    });
    setSending(false);
    if(error){ setErr(error.message); return; }
    setBody(''); setReplyTo(null); setFirstUnreadAt(null); loadMsgs(); loadUnread();
  }

  async function react(m, e){
    const mine = reactions.find(r=>String(r.message_id)===String(m.id)
      && String(r.reactor_email).toLowerCase()===meEmail && r.emoji===e);
    if(mine){ await client.from('chat_reactions').delete().eq('message_id',m.id)
      .eq('reactor_email',meEmail).eq('emoji',e); }
    else { await client.from('chat_reactions').insert({message_id:m.id, thread_key:thread,
      reactor_email:meEmail, emoji:e}); }
    loadMsgs();
  }
  async function togglePin(m){
    const { error } = await client.from('chat_messages').update({pinned:!m.pinned}).eq('id',m.id);
    if(error) setErr(error.message); else loadMsgs();
  }
  function softDelete(m){
    setAsk({ title:'Delete this message?',
      note:'It will read “This message was deleted” for everyone in this conversation.',
      run: async ()=>{
        const { error } = await client.from('chat_messages').update({deleted:true}).eq('id',m.id);
        if(error) setErr(error.message); else loadMsgs();
      }});
  }
  function purge(m){
    setAsk({ title:'Remove permanently?', danger:true,
      note:'The record is erased. Any reply pointing at it loses its quote. This cannot be undone.',
      run: async ()=>{
        const { error } = await client.from('chat_messages').delete().eq('id',m.id);
        if(error) setErr(error.message);
        else { if(window.cgLog) window.cgLog(client,'chat_purge','message '+m.id+' in '+thread); loadMsgs(); }
      }});
  }
  function startEdit(m){
    setEditing(m); setReplyTo(null); setBody(m.body||'');
    setTimeout(()=>{ if(taRef.current){ taRef.current.focus();
      taRef.current.setSelectionRange(taRef.current.value.length, taRef.current.value.length); } },0);
  }
  function startReply(m){
    setEditing(null); setReplyTo(m);
    setTimeout(()=>taRef.current && taRef.current.focus(), 0);
  }
  function insertEmoji(e){
    const ta = taRef.current;
    if(!ta){ setBody(b=>b+e); return; }
    const a = ta.selectionStart, b2 = ta.selectionEnd;
    setBody(v => v.slice(0,a) + e + v.slice(b2));
    setTimeout(()=>{ ta.focus(); ta.setSelectionRange(a+e.length, a+e.length); },0);
  }
  function jumpTo(id){
    const el = document.getElementById(anchorId(id));
    if(el){ el.scrollIntoView({block:'center', behavior:'smooth'});
      el.style.transition='background .4s'; el.style.background='rgba(212,175,55,.16)';
      setTimeout(()=>{ el.style.background=''; }, 900); }
  }
  function onKey(e){
    if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); send(); }
    if(e.key === 'Escape'){ setEditing(null); setReplyTo(null); setEmoji(false); }
  }

  /* ── launcher ─────────────────────────────────────────────────────────── */
  if(!open) return <button className={'cd-launch'+(totalUnread?' has':'')} onClick={()=>setOpen(true)}
      title={totalUnread ? totalUnread+' unread message'+(totalUnread===1?'':'s') : 'Chat'}
      aria-label={totalUnread ? totalUnread+' unread messages' : 'Open chat'}>
    {I.chat}
    {totalUnread>0 && <span className="cd-badge">{totalUnread>99?'99+':totalUnread}</span>}
  </button>;

  /* ── room list (left pane) ────────────────────────────────────────────── */
  const staffRooms = ['staff','boardroom'];
  const staffCasts = ['broadcast:all','broadcast:trial','broadcast:vip','signalview'];
  const studentRooms = ['support:'+meEmail];
  const studentCasts = ['broadcast:all']
    .concat(tier==='trial' ? ['broadcast:trial'] : [])
    .concat(isVIP ? ['broadcast:vip','signalview'] : []);
  const supp = threads.slice().sort((a,b)=>(unread[b]||0)-(unread[a]||0) || a.localeCompare(b));

  function ThreadBtn({k, label, sub}){
    const ico = roomIcon(k);
    return <button className={'cd-thread'+(thread===k?' on':'')} onClick={()=>{ setThread(k); setReplyTo(null); setEditing(null); }}>
      <span className={'cd-av '+hueClass(k)}>{ico || initials(label||threadTitle(k))}</span>
      <span className="t"><b>{label||threadTitle(k)}</b><span>{sub||threadSub(k)}</span></span>
      {unread[k]>0 && <span className="cd-badge">{unread[k]>99?'99+':unread[k]}</span>}
    </button>;
  }
  const Group = ({label, keys, labelFor}) => keys.length ? <React.Fragment>
    <div className="cd-grouphead">{label}</div>
    {keys.map(k=><ThreadBtn key={k} k={k} label={labelFor?labelFor(k):null}/>)}
  </React.Fragment> : null;

  const side = <div className="cd-side">
    <div className="cd-head">
      <BrandOrb/>
      <div style={{minWidth:0,flex:1}}>
        <h4>Messages</h4>
        <div className="cd-sub">{totalUnread ? totalUnread+' unread' : 'All caught up'}</div>
      </div>
      <SoundToggle/>
      {!wide && <button className="cd-icobtn" onClick={()=>setOpen(false)} title="Close">{I.close}</button>}
    </div>
    <div className="cd-threads">
      {staffMode ? <React.Fragment>
        <Group label="Rooms" keys={staffRooms}/>
        <Group label="Channels" keys={staffCasts}/>
        <Group label="Students" keys={supp}/>
        {supp.length===0 && <div style={{padding:'10px 13px',fontSize:'.8rem',color:'var(--ink-dim)'}}>
          No student conversations yet.</div>}
      </React.Fragment> : <React.Fragment>
        <Group label="Your coach" keys={studentRooms} labelFor={()=>'Coaching team'}/>
        <Group label="Channels" keys={studentCasts}/>
        {!isVIP && <div style={{padding:'10px 13px',fontSize:'.78rem',color:'var(--ink-dim)',
          fontFamily:'var(--font-mono)',lineHeight:1.5}}>VIP members also get the VIP channel and the live Signal View.</div>}
      </React.Fragment>}
    </div>
  </div>;

  /* ── conversation (right pane) ────────────────────────────────────────── */
  let lastSender = null, lastDay = null, dividerDone = false;
  const conversation = <div className="cd-main">
    <div className="cd-head">
      {!wide && <button className="cd-icobtn" onClick={()=>{setThread(null);setReplyTo(null);setEditing(null);}}
        title="All conversations">{I.back}</button>}
      <span className={'cd-avatar '+hueClass(thread)}>
        {roomIcon(thread) || initials(staffMode ? threadTitle(thread) : 'Coaching team')}</span>
      <div style={{minWidth:0,flex:1}}>
        <h4>{staffMode ? threadTitle(thread) : (String(thread).indexOf('support:')===0 ? 'Coaching team' : threadTitle(thread))}</h4>
        <div className="cd-sub">
          {typing.length ? 'typing…' : (staffMode ? threadSub(thread)
            : (String(thread).indexOf('support:')===0 ? 'Ask anything — we reply here' : threadSub(thread)))}
        </div>
      </div>
      <button className={'cd-icobtn'+(muted?' off':' on')} title={muted?'Room muted':'Mute this room'}
        onClick={()=>setMuted(toggleMuted(thread))}>{muted ? I.bellOff : I.bellOn}</button>
      <button className="cd-icobtn" onClick={()=>setOpen(false)} title="Close">{I.close}</button>
    </div>

    {pinned.length>0 && <div className="cd-pins">
      {pinned.map(p=><div key={p.id} className="cd-pin" onClick={()=>jumpTo(p.id)}>
        {I.pin}<span>{p.body}</span>
        {isStaff && <button onClick={e=>{ e.stopPropagation(); togglePin(p); }}>Unpin</button>}
      </div>)}
    </div>}

    <div className="cd-body" ref={bodyRef}>
      {thread==='signalview' && <SignalFeed client={client}/>}
      {thread!=='signalview' && msgs === null && <div className="cd-empty"><span className="cg-spin"></span></div>}
      {thread!=='signalview' && msgs && msgs.length===0 && <div className="cd-empty">
        No messages yet — start the conversation.</div>}
      {thread!=='signalview' && (msgs||[]).map(m=>{
        const mine = String(m.sender_email).toLowerCase() === meEmail;
        const dk = dayKey(m.created_at);
        const newDay = dk !== lastDay;
        const first = newDay || m.sender_email !== lastSender;
        lastDay = dk; lastSender = m.sender_email;
        const showDivider = !dividerDone && !mine && firstUnreadAt
          && new Date(m.created_at).getTime() > firstUnreadAt;
        if(showDivider) dividerDone = true;
        const seen = !!otherReadAt && otherReadAt >= new Date(m.created_at).getTime();
        return <React.Fragment key={m.id}>
          {newDay && <div className="cd-day">{dayLabel(m.created_at)}</div>}
          {showDivider && <div className="cd-newline">New messages</div>}
          <div id={anchorId(m.id)} data-mid={m.id} style={{borderRadius:12}}>
            <Bubble m={m} mine={mine} first={first} canPurge={isMain} canPin={isStaff}
              quoted={m.reply_to_id ? byId[m.reply_to_id] : null}
              reactions={rxFor(m.id)} meEmail={meEmail} seen={seen} showReceipts={isStaff}
              onReply={startReply} onEdit={startEdit} onReact={react} onPin={togglePin}
              onDelete={softDelete} onPurge={purge} onJumpTo={jumpTo}/>
          </div>
        </React.Fragment>;
      })}
    </div>

    {typing.length>0 && <div className="cd-typing">
      {typing.length===1 ? typing[0].split('@')[0]+' is typing…' : 'Several people are typing…'}
    </div>}
    {err && <div className="cd-err">{err}</div>}

    {readOnly
      ? <div className="cd-foot"><div className="cd-readonly">
          {thread==='signalview' ? 'Read-only feed · published from Signals/Strategy'
            : 'Read-only channel · your coaching team posts here'}</div></div>
      : <div className="cd-foot" style={{position:'relative'}}>
      {(replyTo || editing) && <div className="cd-reply">
        <div className="r">
          <b>{editing ? 'Editing your message' : 'Replying to '+(replyTo.sender_name||replyTo.sender_email)}</b>
          <span>{(editing||replyTo).body}</span>
        </div>
        <button className="cd-icobtn" onClick={()=>{ setReplyTo(null); setEditing(null); setBody(editing?'':body); }}
          title="Cancel">{I.close}</button>
      </div>}

      {emoji && <EmojiPicker onPick={insertEmoji} onClose={()=>setEmoji(false)}/>}

      {ask && <div className="cd-ask">
        <div className="cd-askcard">
          <b>{ask.title}</b>
          <p>{ask.note}</p>
          <div className="cd-askbtns">
            <button className="cd-askno" onClick={()=>setAsk(null)}>Cancel</button>
            <button className={'cd-askyes'+(ask.danger?' danger':'')}
              onClick={()=>{ const r=ask.run; setAsk(null); r(); }}>
              {ask.danger ? 'Remove' : 'Delete'}</button>
          </div>
        </div>
      </div>}

      <div className="cd-compose">
        <button className="cd-icobtn" onClick={()=>setEmoji(v=>!v)} title="Emoji"
          style={{height:42,width:36}}>{I.smile}</button>
        <textarea ref={taRef} value={body} rows={1}
          placeholder={editing ? 'Edit your message…' : 'Write a message…'}
          onChange={e=>{ setBody(e.target.value); pingTyping();
            e.target.style.height='auto'; e.target.style.height=Math.min(e.target.scrollHeight,132)+'px'; }}
          onKeyDown={onKey}/>
        <button className="cd-send" onClick={send} disabled={!body.trim() || sending}
          title={editing ? 'Save' : 'Send'} aria-label={editing ? 'Save' : 'Send'}>{I.send}</button>
      </div>
    </div>}
  </div>;

  const panelStyle = wide ? {width:size.w, height:Math.min(size.h, window.innerHeight-44)} : {};
  return <div className="cd-panel" style={panelStyle}>
    {wide && <div className="cd-resize" onMouseDown={startResize} title="Drag to resize"></div>}
    {(wide || !thread) && side}
    {thread && conversation}
    {wide && !thread && <div className="cd-main"><div className="cd-empty">
      Pick a conversation on the left.</div></div>}
  </div>;
}

// chat-notify.js paints no floating mute button — the dock header owns sound.
window.CG_CHAT_NO_MUTE_BTN = true;
window.ChatDock = ChatDock;
window.cgThreadTitle = threadTitle;   // exported for tests
})();
