/* ============================================================
   B1 — shared session engine
   FlyingLayer (emoji bay) · DanmuLayer (comments) · MoteLayer
   StripedImg · SealStamp · MicBars · small UI atoms
   Exports to window for cross-file use.
   ============================================================ */
const { useState, useEffect, useRef, useImperativeHandle, forwardRef, useCallback } = React;

/* ---- helpers ---- */
function useInterval(cb, delay) {
  const saved = useRef(cb);
  useEffect(() => { saved.current = cb; });
  useEffect(() => {
    if (delay == null) return;
    const id = setInterval(() => saved.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}
let _uid = 0;
const uid = () => (++_uid);

/* ---- emoji + danmu libraries ---- */
function ic(d) {
  return <svg viewBox="0 0 24 24" style={{ width: '1em', height: '1em' }} fill="none" stroke="var(--gold)" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">{d}</svg>;
}
const EMOJI_LIB = {
  hop: [
    ic(<><path d="M12 3.5c1 1.1 1.5 2 1.5 2.8a1.5 1.5 0 1 1-3 0c0-.8.5-1.7 1.5-2.8Z"/><rect x="9.8" y="8.3" width="4.4" height="10.7" rx="1"/><path d="M6.5 19h11"/></>),
    ic(<><path d="M12 19V9"/><path d="M12 9c-1.3-1.5.6-2.6 0-4.3"/><path d="M8.5 19c0-2 1-3 3.5-3s3.5 1 3.5 3"/><path d="M5.5 19h13"/></>),
    ic(<><circle cx="12" cy="12" r="1.8"/><circle cx="12" cy="7.3" r="2.1"/><circle cx="16" cy="9.6" r="2.1"/><circle cx="16" cy="14.4" r="2.1"/><circle cx="12" cy="16.7" r="2.1"/><circle cx="8" cy="14.4" r="2.1"/><circle cx="8" cy="9.6" r="2.1"/></>),
    ic(<><path d="M4 15.5h16l-2 4H6l-2-4Z"/><circle cx="9.5" cy="12" r="2"/><circle cx="14.5" cy="12" r="2"/><circle cx="12" cy="9.3" r="2"/></>),
    ic(<><path d="M6 11h10v3a5 5 0 0 1-5 5 5 5 0 0 1-5-5v-3Z"/><path d="M16 12.5h1.5a2 2 0 0 1 0 4H16"/><path d="M4 20.5h14"/><path d="M9 8c-.6-1 .3-1.7 0-3M13 8c-.6-1 .3-1.7 0-3"/></>),
    ic(<><circle cx="9" cy="12" r="5.2"/><circle cx="15" cy="12" r="5.2"/></>),
    ic(<><path d="M7 3h7l4 4v14H7V3Z"/><path d="M14 3v4h4"/><path d="M9.5 12h5M9.5 15h5M9.5 18h3"/></>),
    ic(<><circle cx="12" cy="12" r="8.3"/><path d="M8.3 12.3l2.6 2.6 5-5.4"/></>),
    ic(<><path d="M12.8 3.2H19a1 1 0 0 1 1 1v6.2a1 1 0 0 1-.3.7l-8.8 8.8a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.4l8.8-8.8a1 1 0 0 1 .5-.5Z"/><circle cx="16.3" cy="7.7" r="1.3"/></>),
    ic(<><circle cx="7.2" cy="12" r="3.7"/><path d="M10.6 12h10.4M17.5 12v3M20 12v2.5"/></>),
  ],
  'gap-mat': [
    ic(<><rect x="5" y="13" width="14" height="7" rx="1.5"/><path d="M5 16.5h14"/><path d="M9 13V9M15 13V9"/><circle cx="9" cy="8" r="0.9"/><circle cx="15" cy="8" r="0.9"/></>),
    ic(<><path d="M8 3.5a4 4.4 0 1 1 0 8.8 4 4.4 0 1 1 0-8.8Z"/><path d="M8 12.3l-.8 1.3.8.8-.8 1"/><path d="M16 6a3.2 3.6 0 1 1 0 7.2 3.2 3.6 0 1 1 0-7.2Z"/><path d="M16 13.2l-.6 1 .6.7"/></>),
    ic(<><path d="M5 19l4.5-11.5L19 12 5 19Z"/><path d="M14 5l1 1.6M17.5 7.3l1.7-.4M15.7 9.8l1.8.6"/></>),
    ic(<><path d="M5 4h5L8.6 9a1 1 0 0 1-1.7 0L5 4Z"/><path d="M7.5 9v10"/><path d="M5 19h5"/><path d="M14 5h5l-1.4 5a1 1 0 0 1-1.7 0L14 5Z"/><path d="M16.5 10v9"/><path d="M14 19h5"/><path d="M9 6l6 1"/></>),
    ic(<><rect x="4" y="10" width="16" height="10" rx="1"/><path d="M4 14h16"/><path d="M12 10v10"/><path d="M12 10c-1.8 0-3.2-1-3.2-2.6S9.4 5 11 5.6C12 6 12 10 12 10Z"/><path d="M12 10c1.8 0 3.2-1 3.2-2.6S14.6 5 13 5.6C12 6 12 10 12 10Z"/></>),
    ic(<path d="M12 20S3.5 14.4 3.5 8.8A4.3 4.3 0 0 1 12 7a4.3 4.3 0 0 1 8.5 1.8C20.5 14.4 12 20 12 20Z"/>),
    ic(<><circle cx="7.5" cy="18" r="2.3"/><path d="M9.7 18V5l7-2v12.3"/><circle cx="14.7" cy="16.8" r="2.3"/></>),
    ic(<><path d="M12 3c.5 3.5 1.5 5 5 5.5-3.5.5-4.5 2-5 5.5-.5-3.5-1.5-5-5-5.5 3.5-.5 4.5-2 5-5.5Z"/><path d="M18.5 15c.3 1.6.9 2.2 2.5 2.5-1.6.3-2.2.9-2.5 2.5-.3-1.6-.9-2.2-2.5-2.5 1.6-.3 2.2-.9 2.5-2.5Z"/></>),
    ic(<><rect x="3" y="6.5" width="16" height="11" rx="2.4"/><circle cx="11" cy="12" r="3"/><path d="M8 6.5l1.3-2h3.4L14 6.5"/></>),
    ic(<><rect x="9.3" y="3" width="5.4" height="9.5" rx="2.7"/><path d="M6.5 11a5.5 5.5 0 0 0 11 0"/><path d="M12 16.5V21M9 21h6"/></>),
  ],
};
const TYPE_LABEL = { hop: 'Họp', 'gap-mat': 'Gặp mặt' };

const EMOJI_FLAT = ['🪷','🕯️','🎂','🎉','🤝','✨','🌸','❤️','🌿','🎈','📜','🙏'];

const DANMU_SEED = [
  { a: 'Cô Hoa',  t: 'Hướng này hợp tuổi quá 🙏' },
  { a: 'Anh Dũng', t: 'Đẹp lắm luôn ấy' },
  { a: 'Bác Tư',  t: 'Vạn sự như ý nhé cả nhà' },
  { a: 'Chị Lan', t: 'Lộc lá đầy nhà 🧧' },
  { a: 'Em Minh', t: 'Chúc mừng anh chị 🎉' },
  { a: 'Ông Bảy', t: 'Năm nay phát tài' },
  { a: 'Cô Hoa',  t: 'Bàn thờ ấm cúng ghê' },
  { a: 'Anh Dũng', t: 'Nhà view thoáng thật' },
];

/* ---- ambient particles · 5 biến thể host chọn ---- */
const PARTICLE_KINDS = { 'Bụi trầm': 'tram', 'Tuyết': 'snow', 'Mưa': 'rain', 'Khói hương': 'smoke', 'Cánh hoa': 'petal' };
const PETALS = ['🌸', '🍂', '🌺'];

function makeParticle(kind) {
  const id = uid();
  const x = (Math.random() * 102 - 1);
  const base = { position: 'absolute', left: x + '%' };
  if (kind === 'tram') return { id, glyph: null, style: { ...base, bottom: (Math.random() * 48) + '%', width: 3, height: 3, borderRadius: '50%', background: 'rgba(184,153,104,.6)', animation: `b1-rise ${7 + Math.random() * 6}s linear ${Math.random() * 6}s infinite`, '--rise': -(300 + Math.random() * 260) + 'px', '--drift': ((Math.random() * 2 - 1) * 26) + 'px', '--op': .5 } };
  if (kind === 'smoke') return { id, glyph: null, style: { ...base, bottom: (Math.random() * 26) + '%', width: 16 + Math.random() * 12, height: 16 + Math.random() * 12, borderRadius: '50%', background: 'radial-gradient(circle, rgba(184,153,104,.45), transparent 70%)', filter: 'blur(4px)', animation: `b1-rise ${11 + Math.random() * 6}s linear ${Math.random() * 8}s infinite`, '--rise': -(360 + Math.random() * 220) + 'px', '--drift': ((Math.random() * 2 - 1) * 46) + 'px', '--op': .3 } };
  if (kind === 'snow') return { id, glyph: null, style: { ...base, top: '-5%', width: 4 + Math.random() * 4, height: 4 + Math.random() * 4, borderRadius: '50%', background: 'rgba(237,229,211,.85)', filter: 'blur(.7px)', animation: `b1-fall ${9 + Math.random() * 7}s linear ${Math.random() * 8}s infinite`, '--fall': '770px', '--drift': ((Math.random() * 2 - 1) * 52) + 'px', '--op': .7 } };
  if (kind === 'rain') return { id, glyph: null, style: { ...base, top: '-6%', width: 1.4, height: 13 + Math.random() * 9, background: 'linear-gradient(rgba(206,212,224,.5), transparent)', animation: `b1-fall ${1.1 + Math.random() * 1}s linear ${Math.random() * 2}s infinite`, '--fall': '840px', '--drift': '-22px', '--op': .42 } };
  return { id, glyph: PETALS[Math.floor(Math.random() * PETALS.length)], style: { ...base, top: '-5%', fontSize: 13 + Math.random() * 9, filter: 'blur(.3px)', animation: `b1-fall ${9 + Math.random() * 6}s linear ${Math.random() * 8}s infinite`, '--fall': '780px', '--drift': ((Math.random() * 2 - 1) * 72) + 'px', '--rot': ((Math.random() * 2 - 1) * 200) + 'deg', '--op': .75 } };
}

function MoteLayer({ variant = 'Bụi trầm', count }) {
  const [parts, setParts] = useState([]);
  useEffect(() => {
    const kind = PARTICLE_KINDS[variant];
    if (!kind) { setParts([]); return; }
    const defaults = { tram: 14, smoke: 9, snow: 20, rain: 26, petal: 13 };
    const n = count || defaults[kind];
    setParts(Array.from({ length: n }, () => makeParticle(kind)));
  }, [variant, count]);
  return (
    <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 2, overflow: 'hidden' }}>
      {parts.map(p => <span key={p.id} className="b1-particle" style={p.style}>{p.glyph}</span>)}
    </div>
  );
}

/* ---- flying emoji layer (imperative .launch) ---- */
const FlyingLayer = forwardRef(function FlyingLayer({ autoEmojis = null, autoEvery = null, zIndex = 4 }, ref) {
  const [items, setItems] = useState([]);
  const spawn = useCallback((payload) => {
    const id = uid();
    const big = !!payload.img;            // ảnh user → ưu tiên: to hơn, lâu hơn
    const item = {
      id, emoji: payload.emoji, img: payload.img,
      left: 8 + Math.random() * 78,
      rise: -(480 + Math.random() * 240),
      sway: (Math.random() * 2 - 1) * 34,
      dur: (big ? 6.5 : 5.4) + Math.random() * 2.2,
      swaydur: 1.8 + Math.random() * 1.4,
      size: big ? 52 + Math.random() * 12 : 30 + Math.random() * 18,
    };
    setItems(s => [...s, item]);
    setTimeout(() => setItems(s => s.filter(i => i.id !== id)), item.dur * 1000 + 120);
  }, []);
  const launch = useCallback((emoji) => spawn({ emoji }), [spawn]);
  const launchImage = useCallback((url) => spawn({ img: url }), [spawn]);
  useImperativeHandle(ref, () => ({ launch, launchImage }), [launch, launchImage]);
  useInterval(() => {
    if (autoEmojis && autoEmojis.length) launch(autoEmojis[Math.floor(Math.random() * autoEmojis.length)]);
  }, autoEvery);
  return (
    <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex }}>
      {items.map(it => (
        <div key={it.id} className="b1-fly-item" style={{
          left: it.left + '%', fontSize: it.size,
          '--rise': it.rise + 'px', '--dur': it.dur + 's',
        }}>
          <span style={{ '--sway': it.sway + 'px', '--swaydur': it.swaydur + 's' }}>
            {it.img
              ? <img src={it.img} alt="" style={{ width: it.size, height: it.size, borderRadius: 11, objectFit: 'cover', display: 'block', boxShadow: '0 6px 18px rgba(0,0,0,.45)', border: '1.5px solid rgba(184,153,104,.55)' }} />
              : it.emoji}
          </span>
        </div>
      ))}
    </div>
  );
});

/* ---- in-session item tray (emoji + upload ảnh/sticker của bạn) ---- */
function SessionTray({ flyRef, emojis }) {
  const fileRef = useRef(null);
  const onUpload = (e) => {
    const f = e.target.files && e.target.files[0];
    if (f && flyRef.current) flyRef.current.launchImage(URL.createObjectURL(f));
    e.target.value = '';
  };
  const press = (ev) => { ev.currentTarget.style.transform = 'scale(.88)'; };
  const release = (ev) => { ev.currentTarget.style.transform = 'scale(1)'; };
  const btnStyle = {
    flexShrink: 0, width: 46, height: 46, borderRadius: 13, cursor: 'pointer', fontSize: 23,
    background: 'var(--ink-glass)', backdropFilter: 'blur(10px)', border: '1px solid var(--ink-line)',
    display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'transform .12s',
  };
  return (
    <div style={{ display: 'flex', gap: 9, overflowX: 'auto', paddingBottom: 12, marginBottom: 4 }} className="b1-scroll">
      <input ref={fileRef} type="file" accept="image/*" onChange={onUpload} style={{ display: 'none' }} />
      <button onClick={() => fileRef.current && fileRef.current.click()}
        title="Dán ảnh / sticker của bạn — bay ưu tiên"
        onMouseDown={press} onMouseUp={release} onMouseLeave={release}
        style={{ ...btnStyle, borderStyle: 'dashed', borderColor: 'var(--gold-dim)' }}>
        <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 4.5v11M4.5 10h11" stroke="var(--gold)" strokeWidth="1.7" strokeLinecap="round"/></svg>
      </button>
      {emojis.map((e, i) => (
        <button key={i} onClick={() => flyRef.current && flyRef.current.launch(e)}
          onMouseDown={press} onMouseUp={release} onMouseLeave={release} style={btnStyle}>{e}</button>
      ))}
    </div>
  );
}

/* ---- danmu layer (imperative .push, auto-feed by speed) ---- */
const SPEEDS = {
  'tắt':   { dur: 0,    every: null },
  'chậm':  { dur: 13,   every: 2600 },
  'vừa':   { dur: 9,    every: 1500 },
  'nhanh': { dur: 6,    every: 820  },
};
const DanmuLayer = forwardRef(function DanmuLayer({ speed = 'vừa', seed = DANMU_SEED, zIndex = 5 }, ref) {
  const wrapRef = useRef(null);
  const zoneRef = useRef(0);
  const speedRef = useRef(speed);
  useEffect(() => { speedRef.current = speed; }, [speed]);

  // chỉ chạy 1/3 trên hoặc 1/3 dưới — tránh che chính giữa, giữ trang nghiêm
  const TOP = [9, 15, 21, 27];
  const BOTTOM = [66, 72, 78, 84];

  const push = useCallback((c) => {
    const wrap = wrapRef.current;
    if (!wrap) return;
    const cfg = SPEEDS[speedRef.current] || SPEEDS['vừa'];
    if (!cfg.dur) return;
    const el = document.createElement('div');
    el.className = 'b1-danmu';
    el.innerHTML = `<span class="av">${c.a}</span>&nbsp;&nbsp;${c.t}`;
    zoneRef.current = (zoneRef.current + 1) % 2;
    const pool = zoneRef.current === 0 ? TOP : BOTTOM;
    el.style.top = pool[Math.floor(Math.random() * pool.length)] + '%';
    el.style.fontSize = (13 + Math.random() * 3) + 'px';
    wrap.appendChild(el);
    const cw = wrap.offsetWidth;
    const tw = el.offsetWidth;
    const anim = el.animate(
      [{ transform: 'translateX(0)' }, { transform: `translateX(${-(cw + tw + 24)}px)` }],
      { duration: cfg.dur * 1000, easing: 'linear' }
    );
    anim.onfinish = () => el.remove();
  }, []);

  useImperativeHandle(ref, () => ({ push }), [push]);

  // auto-feed
  const feedRef = useRef(0);
  useInterval(() => {
    const cfg = SPEEDS[speedRef.current] || SPEEDS['vừa'];
    if (!cfg.dur) return;
    push(seed[feedRef.current % seed.length]);
    feedRef.current++;
  }, (SPEEDS[speed] && SPEEDS[speed].every) || null);

  return <div ref={wrapRef} style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex, overflow: 'hidden' }} />;
});

/* ---- danmu compose: gõ → xác nhận (sửa được) → xuất bản ---- */
function DanmuCompose({ onPublish, placeholder = 'Viết bình luận…' }) {
  const [text, setText] = useState('');
  const [pending, setPending] = useState(null); // chuỗi đang chờ xác nhận
  const review = () => { const t = text.trim(); if (!t) return; setPending(t); };
  const publish = () => { onPublish(pending); setPending(null); setText(''); };
  const edit = () => { setText(pending); setPending(null); };

  if (pending) {
    return (
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8, marginBottom: 11, padding: '8px 8px 8px 13px',
        borderRadius: 12, background: 'var(--ink-glass)', backdropFilter: 'blur(10px)', border: '1px solid var(--gold-dim)',
      }}>
        <span className="b1-mono" style={{ fontSize: 8.5, color: 'var(--gold)', letterSpacing: '.1em', flexShrink: 0 }}>XEM TRƯỚC</span>
        <span style={{ flex: 1, minWidth: 0, fontSize: 14, color: 'var(--cream)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{pending}</span>
        <button onClick={edit} className="b1-mono" style={{ flexShrink: 0, padding: '8px 11px', borderRadius: 9, cursor: 'pointer', border: '1px solid var(--ink-line)', background: 'none', color: 'var(--cream-dim)', fontSize: 11 }}>Sửa</button>
        <button onClick={publish} style={{ flexShrink: 0, padding: '8px 14px', borderRadius: 9, cursor: 'pointer', border: 'none', fontFamily: 'var(--font-body)', fontWeight: 600, fontSize: 13, background: 'var(--gold)', color: '#15100A' }}>Xuất bản</button>
      </div>
    );
  }
  return (
    <div style={{ display: 'flex', gap: 8, marginBottom: 11 }}>
      <input value={text} onChange={e => setText(e.target.value)} placeholder={placeholder}
        onKeyDown={e => { if (e.key === 'Enter') review(); }}
        style={{
          flex: 1, fontFamily: 'var(--font-body)', fontSize: 14, color: 'var(--cream)',
          background: 'var(--ink-glass)', backdropFilter: 'blur(10px)', border: '1px solid var(--ink-line)',
          borderRadius: 12, padding: '11px 14px', outline: 'none',
        }} />
      <button onClick={review} disabled={!text.trim()} style={{
        flexShrink: 0, padding: '0 17px', borderRadius: 12, cursor: text.trim() ? 'pointer' : 'not-allowed',
        border: 'none', fontFamily: 'var(--font-body)', fontWeight: 600, fontSize: 14,
        background: text.trim() ? 'var(--gold)' : 'var(--ink-soft)',
        color: text.trim() ? '#15100A' : 'var(--cream-dim)', transition: 'all .15s ease',
      }}>Tiếp</button>
    </div>
  );
}

/* ---- notes list: chế độ Họp — bubble tĩnh kiểu chat, không bay ---- */
function NotesList({ notes, selfName, maxHeight = 220 }) {
  const scrollRef = useRef(null);
  useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [notes.length]);
  if (!notes.length) return null;
  return (
    <div ref={scrollRef} className="b1-scroll" style={{ maxHeight, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 7, marginBottom: 11, padding: '2px 1px' }}>
      {notes.map((n, i) => {
        const self = n.a === selfName;
        return (
          <div key={i} style={{ display: 'flex', gap: 7, alignItems: 'flex-end', flexDirection: self ? 'row-reverse' : 'row' }}>
            {!self && <Avatar name={n.a} color={n.color || '#B49B86'} size={22} />}
            <div style={{ maxWidth: '76%' }}>
              {!self && <div className="b1-mono" style={{ fontSize: 9, color: 'var(--gold)', marginBottom: 2, marginLeft: 3 }}>{n.a}</div>}
              <div style={{
                padding: '8px 12px', fontSize: 13, lineHeight: 1.45,
                borderRadius: self ? '13px 13px 3px 13px' : '13px 13px 13px 3px',
                background: self ? 'var(--gold)' : 'var(--ink-soft)',
                color: self ? '#15100A' : 'var(--cream)',
                border: self ? 'none' : '1px solid var(--ink-line)',
              }}>{n.t}</div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ---- in-app browser (Zalo/Messenger/...) detect + "mở bằng trình duyệt thật" banner ---- */
function detectInAppBrowser() {
  const ua = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
  if (/Zalo/i.test(ua)) return 'Zalo';
  if (/FBAN|FBAV|FB_IAB|MessengerForiOS/i.test(ua)) return 'Facebook/Messenger';
  if (/Instagram/i.test(ua)) return 'Instagram';
  if (/\bLine\//i.test(ua)) return 'Line';
  return null;
}
function InAppBrowserBanner() {
  const [app] = useState(detectInAppBrowser);
  const [dismissed, setDismissed] = useState(false);
  if (!app || dismissed) return null;
  return (
    <div style={{
      position: 'absolute', top: 0, left: 0, right: 0, zIndex: 95, padding: '10px 14px',
      background: 'var(--gold)', color: '#15100A', display: 'flex', alignItems: 'center', gap: 10,
      fontSize: 12, lineHeight: 1.4, fontFamily: 'var(--font-body)',
    }}>
      <span style={{ flex: 1 }}>
        <b>Đang mở trong {app}</b> · mic & cài app có thể bị chặn. Bấm <b>⋯</b> ở trên → <b>Mở bằng Safari/Chrome</b> để dùng đầy đủ.
      </span>
      <button onClick={() => setDismissed(true)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#15100A', fontSize: 18, lineHeight: 1, flexShrink: 0, padding: '0 2px' }}>×</button>
    </div>
  );
}

/* ---- striped image placeholder ---- */
function StripedImg({ label, style = {}, className = '', children }) {
  return (
    <div className={'b1-placeholder ' + className} style={style}>
      {label && <span className="b1-placeholder-label">{label}</span>}
      {children}
    </div>
  );
}

/* ---- B1 wordmark (in-app brand mark, replaces old 聚 seal) ---- */
function B1Wordmark({ size = 34, title, style = {} }) {
  return (
    <span title={title} style={{
      fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 600,
      fontSize: size, lineHeight: 1, color: 'var(--gold)', letterSpacing: '-.01em',
      ...style,
    }}>B1</span>
  );
}

/* ---- vermilion seal stamp (signature mark) ---- */
function SealStamp({ size = 46, char = '聚', title, style = {} }) {
  return (
    <div title={title} style={{
      width: size, height: size, flexShrink: 0,
      borderRadius: 7, background: 'var(--vermilion)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      color: '#FBEDE9', fontFamily: 'var(--font-display)', fontWeight: 600,
      fontSize: size * 0.5, lineHeight: 1,
      boxShadow: 'inset 0 0 0 1.5px rgba(251,237,233,.35), 0 4px 14px -6px rgba(168,65,58,.7)',
      animation: 'b1-seal-in .6s cubic-bezier(.2,.8,.2,1) both',
      ...style,
    }}>{char}</div>
  );
}

/* ---- mic level bars ---- */
function MicBars({ active = false, color = 'var(--gold)', size = 13 }) {
  if (!active) {
    return (
      <svg width={size} height={size} viewBox="0 0 14 14" style={{ opacity: .4 }}>
        <path d="M7 1.5a1.8 1.8 0 0 0-1.8 1.8v3.4a1.8 1.8 0 1 0 3.6 0V3.3A1.8 1.8 0 0 0 7 1.5Z" fill="none" stroke="var(--cream-dim)" strokeWidth="1.1"/>
        <path d="M3.4 6.4a3.6 3.6 0 0 0 7.2 0M7 10v2.5" fill="none" stroke="var(--cream-dim)" strokeWidth="1.1" strokeLinecap="round"/>
        <line x1="2.5" y1="11.5" x2="11.5" y2="2.5" stroke="var(--cream-dim)" strokeWidth="1.1" strokeLinecap="round"/>
      </svg>
    );
  }
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 2, height: size }}>
      {[0, 1, 2].map(i => (
        <span key={i} style={{
          width: 2.5, height: size, borderRadius: 2, background: color, transformOrigin: 'center',
          animation: `b1-pulse-mic ${0.6 + i * 0.18}s ease-in-out ${i * 0.12}s infinite`,
        }} />
      ))}
    </div>
  );
}

/* ---- push-to-talk button (thu âm thật bằng MediaRecorder) ---- */
function PushToTalk({ onStart, onEnd }) {
  const [held, setHeld] = useState(false);
  const [secs, setSecs] = useState(0);
  const [denied, setDenied] = useState(false);
  const recRef = useRef(null);
  const streamRef = useRef(null);
  const chunksRef = useRef([]);
  const startMsRef = useRef(0);
  useInterval(() => setSecs(s => s + 0.1), held ? 100 : null);

  const start = async (e) => {
    e.preventDefault();
    setHeld(true); setSecs(0); startMsRef.current = Date.now();
    onStart && onStart();
    try {
      if (!navigator.mediaDevices?.getUserMedia) throw new Error('no-media');
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      streamRef.current = stream;
      const rec = new MediaRecorder(stream);
      chunksRef.current = [];
      rec.ondataavailable = (ev) => { if (ev.data.size > 0) chunksRef.current.push(ev.data); };
      rec.onstop = () => {
        const dur = (Date.now() - startMsRef.current) / 1000;
        const blob = new Blob(chunksRef.current, { type: rec.mimeType || 'audio/webm' });
        const url = URL.createObjectURL(blob);
        stream.getTracks().forEach(t => t.stop());
        streamRef.current = null;
        onEnd && onEnd({ url, duration: dur });
      };
      recRef.current = rec;
      rec.start();
    } catch (err) {
      setDenied(true);
    }
  };

  const end = () => {
    if (!held) return;
    setHeld(false);
    if (recRef.current && recRef.current.state === 'recording') {
      recRef.current.stop(); // → onstop sẽ gọi onEnd với clip
      recRef.current = null;
    } else {
      // fallback: chưa kịp/không có quyền micro → vẫn báo thời lượng
      onEnd && onEnd({ url: null, duration: secs });
    }
  };

  return (
    <button
      onMouseDown={start} onMouseUp={end} onMouseLeave={end}
      onTouchStart={start} onTouchEnd={end}
      className="b1-btn"
      style={{
        position: 'relative', width: '100%', padding: '15px 22px',
        background: held ? 'var(--vermilion)' : 'var(--ink-soft)',
        color: held ? '#FBEDE9' : 'var(--cream)',
        border: '1px solid ' + (held ? 'var(--vermilion)' : 'var(--ink-line)'),
        animation: held ? 'b1-glow 1.2s ease-out infinite' : 'none',
      }}>
      <MicBars active={held} color={held ? '#FBEDE9' : 'var(--gold)'} />
      <span style={{ fontWeight: 500 }}>
        {held ? `Đang nói · ${secs.toFixed(1)}s` : denied ? 'Cần quyền mic · giữ để thử lại' : 'Giữ để nói'}
      </span>
    </button>
  );
}

/* ---- voice clip: bubble phát lại sau khi thả nút giữ-để-nói ---- */
function VoiceClip({ clip, onClose, mine = true }) {
  const [playing, setPlaying] = useState(false);
  const [pos, setPos] = useState(0);
  const audioRef = useRef(null);
  useEffect(() => {
    if (!clip || !clip.url) return;
    const a = new Audio(clip.url);
    audioRef.current = a;
    a.addEventListener('timeupdate', () => setPos(a.currentTime));
    a.addEventListener('ended', () => { setPlaying(false); setPos(0); });
    return () => { a.pause(); audioRef.current = null; };
  }, [clip && clip.url]);
  if (!clip) return null;
  const toggle = () => {
    const a = audioRef.current;
    if (!a) return;
    if (playing) { a.pause(); setPlaying(false); }
    else { a.currentTime = 0; a.play().catch(() => {}); setPlaying(true); }
  };
  const dur = Math.max(0.1, clip.duration || 0);
  const pct = playing ? Math.min(100, (pos / dur) * 100) : 0;
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px 8px 8px', marginBottom: 11,
      background: 'var(--ink-glass)', backdropFilter: 'blur(10px)',
      border: '1px solid ' + (mine ? 'var(--gold-dim)' : 'var(--ink-line)'), borderRadius: 999,
      animation: 'b1-rise-in .25s ease both',
    }}>
      <button onClick={toggle} disabled={!clip.url} title={!clip.url ? 'Không có quyền micro' : ''} style={{
        width: 30, height: 30, borderRadius: '50%', flexShrink: 0, cursor: clip.url ? 'pointer' : 'not-allowed',
        background: 'var(--gold)', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center',
        opacity: clip.url ? 1 : .4,
      }}>
        {playing
          ? <svg width="10" height="10" viewBox="0 0 12 12"><rect x="2.6" y="2" width="2.4" height="8" fill="#15100A"/><rect x="7" y="2" width="2.4" height="8" fill="#15100A"/></svg>
          : <svg width="10" height="10" viewBox="0 0 12 12"><path d="M3 2l7 4-7 4z" fill="#15100A"/></svg>}
      </button>
      <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 3, height: 18, minWidth: 0 }}>
        {Array.from({ length: 22 }).map((_, i) => {
          const h = 4 + ((i * 7919) % 12);
          const reached = (i / 22) * 100 <= pct;
          return <span key={i} style={{ width: 2, height: h, borderRadius: 2, background: reached ? 'var(--gold)' : 'rgba(184,153,104,.32)', flexShrink: 0 }} />;
        })}
      </div>
      <span className="b1-mono" style={{ fontSize: 10, color: 'var(--gold)', flexShrink: 0 }}>{dur.toFixed(1)}s</span>
      {onClose && <button onClick={onClose} style={{ flexShrink: 0, background: 'none', border: 'none', cursor: 'pointer', color: 'var(--cream-dim)', fontSize: 16, lineHeight: 1, padding: '0 4px' }}>×</button>}
    </div>
  );
}

Object.assign(window, {
  useInterval, uid, EMOJI_LIB, EMOJI_FLAT, TYPE_LABEL, DANMU_SEED, SPEEDS, PARTICLE_KINDS,
  MoteLayer, FlyingLayer, DanmuLayer, DanmuCompose, StripedImg, SealStamp, B1Wordmark, MicBars, PushToTalk, VoiceClip, SessionTray, NotesList, detectInAppBrowser, InAppBrowserBanner,
});
