/* global React, window, Icon */
const { useState, useEffect, useRef, useMemo } = React;

// ===== Sidebar =====
// Static nav definition. `badge` may be a string (e.g. "LIVE") or a function
// that returns the live count from window.PEOPLE/SESSIONS so the sidebar
// updates whenever the data refreshes (no more stale "24"/"6").
const NAV_ITEMS = [
  { id: 'dashboard', label: 'แดชบอร์ด',           icon: 'dashboard', adminOnly: true },
  { id: 'checkin',   label: 'ลงทะเบียน',           icon: 'log-in',    primary: true },
  { id: 'realtime',  label: 'การสแกนหน้า',         icon: 'scan',      badge: 'LIVE', adminOnly: true },
  { id: 'station',   label: 'มอนิเตอร์',            icon: 'grid' },
  { id: 'person',    label: 'รายชื่อ',             icon: 'users',     badge: () => (window.PEOPLE || []).length, hideBadgeIfZero: true, adminOnly: true },
  { id: 'session',   label: 'เซสชั่น',              icon: 'calendar',  badge: () => (window.SESSIONS || []).length, hideBadgeIfZero: true, adminOnly: true },
  { id: 'report',    label: 'รายงาน',                icon: 'report' },
];

const NAV_SETTINGS = [
  { id: 'device',    label: 'อุปกรณ์',               icon: 'device', adminOnly: true,
    badge: () => (window.DEVICES || []).filter(d => d.status === 'online').length,
    hideBadgeIfZero: true },
  { id: 'settings',  label: 'ตั้งค่า',                icon: 'settings' },
];

function Sidebar({ active, onNav, role, operatorDeviceId, onLogout }) {
  const myDevice = window.DEVICES.find(d => d.id === operatorDeviceId);
  const isAdmin = role === 'admin';
  const visibleItems = NAV_ITEMS.filter(item => isAdmin || !item.adminOnly);
  const visibleSettings = NAV_SETTINGS.filter(item => isAdmin || !item.adminOnly);
  return (
    <aside className="sidebar" data-screen-label="Sidebar">
      <div className="sb-brand">
        <div className="sb-brand-mark" />
        <div>
          <div className="sb-brand-name">FaceCheck</div>
          <div className="sb-brand-tag">EVENT REGISTRY</div>
        </div>
      </div>

      {/* Operator device binding banner */}
      {!isAdmin && myDevice && (
        <div className="sb-device-lock">
          <div className="row" style={{gap: 8, marginBottom: 6}}>
            <Icon name="lock" size={12}/>
            <span style={{fontSize: 10.5, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase'}}>ล็อกที่เครื่อง</span>
            <span className="status-dot online" style={{marginLeft: 'auto', width: 7, height: 7}}/>
          </div>
          <div className="mono" style={{fontSize: 17, fontWeight: 700, color: '#fff', lineHeight: 1.1}}>{myDevice.op}</div>
          <div style={{fontSize: 12, color: 'rgba(255,255,255,0.75)', marginTop: 4}}>{myDevice.name}</div>
          <div className="mono" style={{fontSize: 11, color: 'rgba(255,255,255,0.5)', marginTop: 2}}>{myDevice.ip}</div>
        </div>
      )}

      <div className="sb-section">
        <div className="sb-section-label">เมนูหลัก</div>
        {visibleItems.map(item => {
          const badgeValue = typeof item.badge === 'function' ? item.badge() : item.badge;
          const showBadge = badgeValue != null
            && !(item.hideBadgeIfZero && (badgeValue === 0 || badgeValue === '0'));
          return (
            <button
              key={item.id}
              className={`sb-item ${active === item.id ? 'is-active' : ''} ${item.primary && !isAdmin ? 'is-primary' : ''}`}
              onClick={() => onNav(item.id)}
            >
              <span className="sb-icon"><Icon name={item.icon} size={18}/></span>
              <span className="sb-label">{item.label}</span>
              {showBadge && <span className="sb-badge">{badgeValue}</span>}
            </button>
          );
        })}
      </div>

      <div className="sb-section">
        <div className="sb-section-label">{isAdmin ? 'การตั้งค่า' : 'อื่นๆ'}</div>
        {visibleSettings.map(item => {
          const badgeValue = typeof item.badge === 'function' ? item.badge() : item.badge;
          const showBadge = badgeValue != null
            && !(item.hideBadgeIfZero && (badgeValue === 0 || badgeValue === '0'));
          return (
            <button
              key={item.id}
              className={`sb-item ${active === item.id ? 'is-active' : ''}`}
              onClick={() => onNav(item.id)}
            >
              <span className="sb-icon"><Icon name={item.icon} size={18}/></span>
              <span className="sb-label">{item.label}</span>
              {showBadge && <span className="sb-badge">{badgeValue}</span>}
            </button>
          );
        })}
      </div>

      <div className="sb-foot">
        {(() => {
          const me = window.CURRENT_USER || {};
          const isAdmin = role === 'admin';
          const opLabel = me.username || myDevice?.op || 'op';
          const initials = (me.display_name || me.username || 'op').slice(0, 2).toUpperCase();
          return (
            <>
              <div className="sb-avatar">{isAdmin ? (initials || 'AD') : initials}</div>
              <div className="sb-user">
                <div className="sb-user-name">{me.display_name || me.username || (isAdmin ? 'Admin' : opLabel)}</div>
                <div className="sb-user-role">
                  {isAdmin ? 'Administrator' : `Operator · ${myDevice?.id || 'unbound'}`}
                </div>
              </div>
            </>
          );
        })()}
        <button className="sb-item" style={{width: 36, height: 36, padding: 0, justifyContent: 'center', flex: 'none'}} onClick={onLogout} title="ออกจากระบบ">
          <span className="sb-icon"><Icon name="log-out" size={16}/></span>
        </button>
      </div>
    </aside>
  );
}

// ===== Topbar =====
function Topbar({ title, sub, actions }) {
  // If both title and sub are empty, the page itself has its own hero — let
  // search box expand to fill the row instead of leaving an empty gap.
  const hasHeader = !!(title || sub);
  // Always-visible session indicator. Re-renders when WS broadcasts a
  // session_changed event (refreshData() flips window.ACTIVE_SESSION, we tick).
  const [, tick] = useState(0);
  useEffect(() => {
    if (!window.scanStream) return undefined;
    // refresh whenever a scan / session change arrives — cheap, just bumps state
    const id = setInterval(() => tick((v) => v + 1), 4000);
    return () => clearInterval(id);
  }, []);
  const active = window.ACTIVE_SESSION;

  // ── Search state ──────────────────────────────────────────────────────────
  const [query, setQuery] = useState('');
  const [searchOpen, setSearchOpen] = useState(false);
  const [highlight, setHighlight] = useState(0);
  const searchRef = useRef();

  // Global ⌘K / Ctrl+K shortcut → focus the search input from anywhere
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
        e.preventDefault();
        searchRef.current?.focus();
        searchRef.current?.select?.();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  // Compute fuzzy-ish results across people / sessions / devices.
  const results = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (q.length < 2) return null;
    const out = [];
    let n = 0;
    for (const p of (window.PEOPLE || [])) {
      if (n >= 8) break;
      const hay = `${p.name || ''} ${p.id || ''} ${p.org || ''} ${p.role || ''}`.toLowerCase();
      if (hay.includes(q)) { out.push({ kind: 'person', label: p.name || p.id, sub: `${p.id || '—'} · ${p.org || p.role || ''}`, item: p }); n++; }
    }
    n = 0;
    for (const s of (window.SESSIONS || [])) {
      if (n >= 5) break;
      const hay = `${s.title || ''} ${s.code || ''} ${s.id || ''}`.toLowerCase();
      if (hay.includes(q)) { out.push({ kind: 'session', label: s.title || s.code, sub: `${s.start || ''} – ${s.end || ''} · ${s.round || s.session_type || ''}`, item: s }); n++; }
    }
    n = 0;
    for (const d of (window.DEVICES || [])) {
      if (n >= 5) break;
      const hay = `${d.name || ''} ${d.id || ''} ${d.ip || ''}`.toLowerCase();
      if (hay.includes(q)) { out.push({ kind: 'device', label: d.name || d.id, sub: `${d.id} · ${d.ip || ''} · ${d.status || ''}`, item: d }); n++; }
    }
    return out;
  }, [query, hasHeader /* unused; just to placate exhaustive-deps */]);

  // Reset highlight whenever the result set changes
  useEffect(() => { setHighlight(0); }, [results && results.length]);

  const goTo = (r) => {
    setSearchOpen(false);
    setQuery('');
    if (!r) return;
    if (r.kind === 'person') {
      window.__personFocusId = r.item.id;            // for initial mount
      window.navigateTo?.('person');
      // For already-mounted person page, dispatch event so it re-selects
      setTimeout(() => window.dispatchEvent(new CustomEvent('sentinel:focus_person', { detail: { id: r.item.id } })), 0);
    } else if (r.kind === 'session') {
      window.__sessionFocusId = r.item.db_id;
      window.navigateTo?.('session');
    } else if (r.kind === 'device') {
      window.__deviceFocusId = r.item.db_id || r.item.id;
      window.navigateTo?.('device');
    }
  };

  const onSearchKey = (e) => {
    if (!results || !results.length) {
      if (e.key === 'Escape') { setQuery(''); searchRef.current?.blur(); }
      return;
    }
    if (e.key === 'ArrowDown') { e.preventDefault(); setHighlight((h) => Math.min(h + 1, results.length - 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight((h) => Math.max(h - 1, 0)); }
    else if (e.key === 'Enter') { e.preventDefault(); goTo(results[highlight]); }
    else if (e.key === 'Escape') { setQuery(''); searchRef.current?.blur(); }
  };

  // ── Bell: dropdown showing offline devices / recent failed syncs ──────────
  const [bellOpen, setBellOpen] = useState(false);
  const [unknownCount, setUnknownCount] = useState(0);
  const [showUnknownReview, setShowUnknownReview] = useState(false);
  const offlineDevices = (window.DEVICES || []).filter((d) => d.status === 'offline');
  // Poll the unknown-faces endpoint periodically so the operator sees pending
  // review items pop up without refreshing. Only admin role hits this; the
  // endpoint requires admin auth anyway and would 401 silently for operators.
  useEffect(() => {
    let cancelled = false;
    const fetchCount = () => {
      fetch('/api/admin/unknowns?limit=200', { credentials: 'include' })
        .then((r) => r.ok ? r.json() : null)
        .then((d) => { if (!cancelled && d) setUnknownCount(d.count || 0); })
        .catch(() => {});
    };
    fetchCount();
    const t = setInterval(fetchCount, 20000);
    const onResolved = () => fetchCount();
    window.addEventListener('sentinel:unknown_resolved', onResolved);
    return () => { cancelled = true; clearInterval(t); window.removeEventListener('sentinel:unknown_resolved', onResolved); };
  }, []);
  const notifyCount = offlineDevices.length + unknownCount;

  // Close dropdowns on outside click
  useEffect(() => {
    if (!searchOpen && !bellOpen) return undefined;
    const onDown = (e) => {
      if (!e.target.closest('.topbar-search') && !e.target.closest('.tb-bell-wrap')) {
        setSearchOpen(false);
        setBellOpen(false);
      }
    };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [searchOpen, bellOpen]);

  const kindLabel = { person: 'ผู้เข้างาน', session: 'เซสชั่น', device: 'อุปกรณ์' };
  const kindColor = { person: 'var(--primary)', session: 'var(--coral)', device: 'var(--ink-3)' };

  return (
    <div className="topbar">
      {hasHeader && (
        <div>
          {title && <div className="topbar-title">{title}</div>}
          {sub && <div className="topbar-sub">{sub}</div>}
        </div>
      )}
      {/* FaceInTime: no per-event sessions — the topbar session chip is hidden. */}
      <div className="topbar-search" style={{position: 'relative'}}>
        <Icon name="search" size={16} />
        <input
          ref={searchRef}
          value={query}
          placeholder="ค้นหารายชื่อ, รหัส, เซสชั่น..."
          onChange={(e) => { setQuery(e.target.value); setSearchOpen(true); }}
          onFocus={() => setSearchOpen(true)}
          onKeyDown={onSearchKey}
        />
        <kbd style={{fontSize: 11, color: 'var(--ink-4)', fontFamily: 'var(--font-mono)', background: 'var(--surface-2)', padding: '2px 6px', borderRadius: 6}}>⌘K</kbd>

        {searchOpen && query.trim().length >= 2 && (
          <div style={{
            position: 'absolute', top: 'calc(100% + 6px)', left: 0, right: 0,
            background: 'var(--surface)', border: '1px solid var(--line)',
            borderRadius: 12, boxShadow: '0 12px 32px rgba(0,0,0,0.12)',
            maxHeight: 380, overflowY: 'auto', zIndex: 50,
          }}>
            {(!results || results.length === 0) ? (
              <div className="muted" style={{padding: 18, textAlign: 'center', fontSize: 13}}>
                ไม่พบ "{query}"
              </div>
            ) : (
              <>
                {results.map((r, i) => (
                  <div
                    key={`${r.kind}-${r.item.id || r.item.db_id || i}`}
                    onMouseDown={(e) => { e.preventDefault(); goTo(r); }}
                    onMouseEnter={() => setHighlight(i)}
                    style={{
                      display: 'flex', alignItems: 'center', gap: 12,
                      padding: '10px 14px', cursor: 'pointer',
                      background: highlight === i ? 'var(--surface-2)' : 'transparent',
                      borderBottom: i < results.length - 1 ? '1px solid var(--line)' : 'none',
                    }}
                  >
                    <div style={{
                      width: 6, height: 28, borderRadius: 3, background: kindColor[r.kind] || 'var(--ink-3)',
                    }}/>
                    <div style={{flex: 1, minWidth: 0}}>
                      <div style={{fontSize: 13.5, fontWeight: 600, color: 'var(--ink-1)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>
                        {r.label}
                      </div>
                      <div className="muted" style={{fontSize: 11.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>
                        {r.sub}
                      </div>
                    </div>
                    <span style={{
                      fontSize: 10.5, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase',
                      color: kindColor[r.kind], padding: '3px 8px', borderRadius: 999,
                      background: 'var(--surface-2)',
                    }}>{kindLabel[r.kind]}</span>
                  </div>
                ))}
                <div style={{padding: '8px 14px', fontSize: 11, color: 'var(--ink-4)', borderTop: '1px solid var(--line)', textAlign: 'center'}}>
                  <span style={{marginRight: 12}}>↑↓ เลือก</span>
                  <span style={{marginRight: 12}}>↵ เปิด</span>
                  <span>esc ปิด</span>
                </div>
              </>
            )}
          </div>
        )}
      </div>
      <div className="topbar-actions">
        <div className="tb-bell-wrap" style={{position: 'relative'}}>
          <button className="tb-icon-btn" title="การแจ้งเตือน" onClick={() => setBellOpen((o) => !o)}>
            <Icon name="bell" size={17}/>
            {notifyCount > 0 && <span className="dot" />}
          </button>
          {bellOpen && (
            <div style={{
              position: 'absolute', top: 'calc(100% + 8px)', right: 0,
              width: 320, background: 'var(--surface)',
              border: '1px solid var(--line)', borderRadius: 12,
              boxShadow: '0 12px 32px rgba(0,0,0,0.12)', zIndex: 50,
              overflow: 'hidden',
            }}>
              <div style={{padding: '12px 14px', borderBottom: '1px solid var(--line)', fontSize: 12.5, fontWeight: 700}}>
                การแจ้งเตือน
              </div>
              {notifyCount === 0 ? (
                <div className="muted" style={{padding: 24, textAlign: 'center', fontSize: 13}}>
                  ทุกอย่างเรียบร้อย — ไม่มีการแจ้งเตือน
                </div>
              ) : (
                <>
                  {offlineDevices.length > 0 && (
                    <>
                      <div style={{padding: '10px 14px', background: 'rgba(220,38,38,0.06)', borderBottom: '1px solid var(--line)'}}>
                        <div style={{fontSize: 12, fontWeight: 700, color: '#DC2626', marginBottom: 4}}>
                          {offlineDevices.length} เครื่องออฟไลน์
                        </div>
                        <div className="muted" style={{fontSize: 11, marginBottom: 8}}>
                          ระบบใส่ใน retry queue ให้อัตโนมัติ — sync ต่อทันทีที่กลับมา online
                        </div>
                        <div className="mono" style={{fontSize: 11.5, color: 'var(--ink-2)', display: 'flex', flexWrap: 'wrap', gap: 6}}>
                          {offlineDevices.slice(0, 8).map((d) => (
                            <span key={d.id} style={{padding: '2px 8px', background: 'var(--surface-2)', borderRadius: 6}}>{d.id}</span>
                          ))}
                        </div>
                      </div>
                      <button
                        className="tb-bell-action"
                        onClick={() => { setBellOpen(false); window.navigateTo?.('device'); }}
                        style={{
                          width: '100%', padding: '10px 14px', background: 'transparent',
                          border: 'none', cursor: 'pointer', fontSize: 12.5, fontWeight: 600,
                          color: 'var(--primary)', textAlign: 'center',
                          borderBottom: unknownCount > 0 ? '1px solid var(--line)' : 'none',
                        }}
                      >
                        ดูหน้าอุปกรณ์ →
                      </button>
                    </>
                  )}
                  {unknownCount > 0 && (
                    <>
                      <div style={{padding: '10px 14px', background: 'rgba(245,158,11,0.06)', borderBottom: '1px solid var(--line)'}}>
                        <div style={{fontSize: 12, fontWeight: 700, color: '#B45309', marginBottom: 4}}>
                          {unknownCount} รูปนิรนามรอตรวจ
                        </div>
                        <div className="muted" style={{fontSize: 11}}>
                          สแกนแล้วระบบไม่รู้จัก — เปิด review เพื่อจับคู่กับคนในระบบ
                        </div>
                      </div>
                      <button
                        className="tb-bell-action"
                        onClick={() => { setBellOpen(false); setShowUnknownReview(true); }}
                        style={{
                          width: '100%', padding: '10px 14px', background: 'transparent',
                          border: 'none', cursor: 'pointer', fontSize: 12.5, fontWeight: 600,
                          color: 'var(--primary)', textAlign: 'center',
                        }}
                      >
                        เปิด review รูปนิรนาม →
                      </button>
                    </>
                  )}
                </>
              )}
            </div>
          )}
        </div>
        {actions}
      </div>
      <UnknownReviewModal open={showUnknownReview} onClose={() => setShowUnknownReview(false)}/>
    </div>
  );
}

// Inline chip in topbar — green when a session is active, red pulse when none.
function SessionStatusChip({ active }) {
  if (active) {
    return (
      <div
        title={`เซสชั่นที่กำลังบันทึก: ${(active.title || active.name || '(ไม่ระบุชื่อ)')}`}
        style={{
          display: 'flex', alignItems: 'center', gap: 8,
          padding: '6px 12px', marginLeft: 12,
          background: 'rgba(16,185,129,0.10)',
          border: '1px solid rgba(16,185,129,0.3)',
          borderRadius: 999, maxWidth: 340, minWidth: 0,
        }}
      >
        <span className="status-dot online" style={{width: 8, height: 8, flex: 'none'}}/>
        <div style={{minWidth: 0, lineHeight: 1.15}}>
          <div style={{fontSize: 10.5, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#065F46'}}>
            กำลังบันทึก
          </div>
          <div style={{
            fontSize: 12.5, fontWeight: 600, color: '#065F46',
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          }}>
            {[active.title || active.name, active.round].filter(Boolean).join(' · ') || '(ไม่ระบุชื่อ)'}
          </div>
        </div>
      </div>
    );
  }
  return (
    <div
      title="ยังไม่ได้เปิดเซสชั่น — การสแกนจะไม่ถูกบันทึก"
      style={{
        display: 'flex', alignItems: 'center', gap: 8,
        padding: '6px 12px', marginLeft: 12,
        background: 'rgba(220,38,38,0.10)',
        border: '1px solid rgba(220,38,38,0.35)',
        borderRadius: 999,
        animation: 'pulse-row 1.4s ease-in-out infinite',
        color: '#991919',
      }}
    >
      <Icon name="bell" size={14}/>
      <div style={{lineHeight: 1.15}}>
        <div style={{fontSize: 10.5, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase'}}>
          ยังไม่ได้เปิดเซสชั่น
        </div>
        <div style={{fontSize: 12, whiteSpace: 'nowrap'}}>
          การสแกนจะไม่ถูกบันทึก
        </div>
      </div>
    </div>
  );
}

// ===== Card =====
function Stat({ label, value, unit, color = 'indigo', icon, delta, deltaDir }) {
  const map = {
    coral:   { bg: 'var(--coral-soft)',   fg: '#99311E', accent: 'var(--coral)' },
    indigo:  { bg: 'var(--primary-soft)', fg: 'var(--primary-ink)', accent: 'var(--primary)' },
    mint:    { bg: 'var(--mint-soft)',    fg: '#065F46', accent: 'var(--mint)' },
    yellow:  { bg: 'var(--yellow-soft)',  fg: '#78580B', accent: 'var(--yellow)' },
    cyan:    { bg: 'var(--cyan-soft)',    fg: '#155E75', accent: 'var(--cyan)' },
    magenta: { bg: 'var(--magenta-soft)', fg: '#831843', accent: 'var(--magenta)' },
  }[color];
  return (
    <div className="stat">
      <div className="row" style={{justifyContent: 'space-between', alignItems: 'flex-start'}}>
        <div className="stat-icon" style={{background: map.bg, color: map.fg}}>
          <Icon name={icon} size={18}/>
        </div>
        {delta != null && (
          <span className={`stat-delta ${deltaDir}`}>
            <Icon name={deltaDir === 'up' ? 'arrow-up-right' : 'arrow-right'} size={12} />
            {delta}%
          </span>
        )}
      </div>
      <div className="col" style={{gap: 4}}>
        <div className="stat-label">{label}</div>
        <div className="stat-value">{value}{unit && <span className="unit">{unit}</span>}</div>
      </div>
    </div>
  );
}

// ===== Sparkline =====
function Sparkline({ data, color = 'var(--primary)', width = 200, height = 36 }) {
  if (!data || !data.length) return null;
  const max = Math.max(...data);
  const min = Math.min(...data);
  const range = max - min || 1;
  const dx = width / (data.length - 1);
  const pts = data.map((v, i) => `${i * dx},${height - ((v - min) / range) * (height - 6) - 3}`).join(' ');
  const area = `0,${height} ${pts} ${width},${height}`;
  return (
    <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{overflow: 'visible'}}>
      <polygon points={area} fill={color} opacity="0.12" />
      <polyline points={pts} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}

// ===== Bar chart (in/out hourly) =====
function HourlyChart({ data, mode = 'both', height = 260 }) {
  const showIn = mode === 'both' || mode === 'in';
  const showOut = mode === 'both' || mode === 'out';
  // Total across all hours — when this is 0, render an empty state overlay
  // instead of confusing 2px-stub bars that look like a broken chart.
  const totalActivity = data.reduce(
    (s, d) => s + (showIn ? (d.in || 0) : 0) + (showOut ? (d.out || 0) : 0),
    0
  );
  const isEmpty = totalActivity === 0;
  const rawMax = data.reduce(
    (m, d) => Math.max(m, showIn ? (d.in || 0) : 0, showOut ? (d.out || 0) : 0),
    0
  );
  // Floor the scale (min 4) AND keep headroom above the tallest bar, so a
  // single scan reads as a short bar — not a full-height one — and the value
  // label always fits above it. Ticks stay whole numbers.
  const scaleMax = Math.max(4, (Math.floor(rawMax / 2) + 1) * 2);
  const ticks = [scaleMax, scaleMax / 2, 0];
  const HOURS_H = 22; // reserved row height for the hour labels

  const bar = (val, color) => (
    <div style={{flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', alignItems: 'center', height: '100%'}}>
      {val > 0 && <div className="mono" style={{fontSize: 10, fontWeight: 700, color, marginBottom: 3, lineHeight: 1}}>{val}</div>}
      <div style={{
        width: mode === 'both' ? '78%' : '52%', maxWidth: 24,
        background: color, borderRadius: '5px 5px 0 0',
        height: `${(val / scaleMax) * 100}%`, minHeight: val > 0 ? 3 : 0,
        transition: 'height 600ms cubic-bezier(.5,1.5,.4,1)',
      }} title={`${val}`}/>
    </div>
  );

  return (
    <div style={{position: 'relative', height}}>
      <div style={{display: 'flex', height: '100%', opacity: isEmpty ? 0.4 : 1, transition: 'opacity 200ms ease'}}>
        {/* Y-axis (units) */}
        <div style={{width: 26, flex: 'none', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', alignItems: 'flex-end', paddingRight: 6, paddingBottom: HOURS_H}}>
          {ticks.map((t, i) => (
            <div key={i} className="mono" style={{fontSize: 9.5, color: 'var(--ink-4)', lineHeight: 1}}>{t}</div>
          ))}
        </div>
        {/* Plot area */}
        <div style={{flex: 1, position: 'relative', minWidth: 0}}>
          {/* Gridlines at each tick */}
          <div style={{position: 'absolute', left: 0, right: 0, top: 0, bottom: HOURS_H}}>
            {ticks.map((t, i) => (
              <div key={i} style={{
                position: 'absolute', left: 0, right: 0, top: `${(1 - t / scaleMax) * 100}%`,
                borderTop: t === 0 ? '1px solid var(--line)' : '1px dashed var(--line)',
                opacity: t === 0 ? 1 : 0.7,
              }}/>
            ))}
          </div>
          {/* Bars */}
          <div style={{position: 'absolute', inset: 0, display: 'flex', justifyContent: 'space-between', gap: 6}}>
            {data.map((d, i) => (
              <div key={i} style={{flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column'}}>
                <div style={{flex: 1, display: 'flex', gap: 2, alignItems: 'flex-end', justifyContent: 'center', minHeight: 0}}>
                  {showIn && bar(d.in || 0, 'var(--primary)')}
                  {showOut && bar(d.out || 0, 'var(--coral)')}
                </div>
                <div className="mono" style={{fontSize: 10.5, color: 'var(--ink-4)', textAlign: 'center', height: HOURS_H, lineHeight: `${HOURS_H}px`}}>{d.hr}</div>
              </div>
            ))}
          </div>
        </div>
      </div>
      {isEmpty && (
        <div style={{
          position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
          alignItems: 'center', justifyContent: 'center', pointerEvents: 'none',
          gap: 10, paddingBottom: 28,  // leave room for the hour labels below
        }}>
          <div style={{
            width: 48, height: 48, borderRadius: 14, background: 'var(--surface-2)',
            display: 'grid', placeItems: 'center', color: 'var(--ink-4)',
          }}>
            <Icon name="report" size={22}/>
          </div>
          <div style={{textAlign: 'center'}}>
            <div style={{fontSize: 13.5, fontWeight: 600, color: 'var(--ink-2)', marginBottom: 2}}>
              ยังไม่มีข้อมูลการสแกน
            </div>
            <div className="muted" style={{fontSize: 12}}>
              กราฟจะอัปเดตอัตโนมัติเมื่อมีคนเริ่มเช็คอิน
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ===== Avatar generator (initials in Thai) =====
function makeInitials(name) {
  if (!name) return '?';
  // For Thai names, take first 2 thai chars (skipping spaces)
  const clean = name.replace(/\s/g, '');
  return clean.slice(0, 2);
}

function Avatar({ person, size = 'md' }) {
  if (!person) return null;
  const sizeClass = size === 'lg' ? 'avatar lg' : size === 'xl' ? 'avatar xl' : 'avatar';
  // If the person has a real photo, show it (lazy-loaded so 1,500 people list
  // only downloads what's actually on screen). Fall back to coloured initials
  // bubble when the image is missing or fails to load.
  const [broken, setBroken] = useState(false);
  if (person.photo_url && !broken) {
    return (
      <img
        className={sizeClass}
        src={person.photo_url}
        alt={person.name || ''}
        loading="lazy"
        decoding="async"
        onError={() => setBroken(true)}
        style={{
          objectFit: 'cover',
          background: `var(--${person.color === 'indigo' ? 'primary' : person.color || 'coral'}-soft)`,
        }}
      />
    );
  }
  return (
    <div className={`${sizeClass} ${person.color}`}>
      {makeInitials(person.name)}
    </div>
  );
}

// ===== Toggle =====
function Toggle({ on, onChange }) {
  return <div className={`toggle ${on ? 'on' : ''}`} onClick={() => onChange(!on)} />;
}

// ===== Empty state =====
function Empty({ icon, title, desc, cta }) {
  return (
    <div style={{textAlign: 'center', padding: '48px 20px'}}>
      <div style={{width: 56, height: 56, margin: '0 auto 14px', borderRadius: 16, background: 'var(--surface-2)', display: 'grid', placeItems: 'center', color: 'var(--ink-4)'}}>
        <Icon name={icon || 'sparkles'} size={24}/>
      </div>
      <div className="h3" style={{marginBottom: 4}}>{title}</div>
      <div className="muted" style={{fontSize: 13, marginBottom: 18}}>{desc}</div>
      {cta}
    </div>
  );
}

// Format helpers
function fmtDateThai(d) {
  if (!(d instanceof Date)) d = new Date(d);
  const months = ['ม.ค.','ก.พ.','มี.ค.','เม.ย.','พ.ค.','มิ.ย.','ก.ค.','ส.ค.','ก.ย.','ต.ค.','พ.ย.','ธ.ค.'];
  return `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear() + 543}`;
}

function fmtClock(d) {
  if (!(d instanceof Date)) d = new Date(d);
  const pad = (n) => String(n).padStart(2, '0');
  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

function fmtClockShort(d) {
  if (!(d instanceof Date)) d = new Date(d);
  const pad = (n) => String(n).padStart(2, '0');
  return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

function timeAgo(d) {
  const diff = (Date.now() - new Date(d).getTime()) / 1000;
  if (diff < 60) return `${Math.floor(diff)} วินาทีที่แล้ว`;
  if (diff < 3600) return `${Math.floor(diff / 60)} นาทีที่แล้ว`;
  if (diff < 86400) return `${Math.floor(diff / 3600)} ชั่วโมงที่แล้ว`;
  return `${Math.floor(diff / 86400)} วันที่แล้ว`;
}

// ===== Unknown-face review modal =====
// Fullscreen review surface accessible from the bell when there are pending
// unknown scans. Lists snapshot thumbnails; clicking one shows AI-adjacent
// suggestions (top-K candidates based on time/space heuristics from the DB).
// Operator confirms a match or dismisses. Resolved rows disappear from the
// list. Snapshot can be promoted to be the person's profile photo on resolve.
function UnknownReviewModal({ open, onClose }) {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(false);
  const [selected, setSelected] = useState(null);
  const [suggestions, setSuggestions] = useState(null);
  const [suggLoading, setSuggLoading] = useState(false);

  const load = () => {
    setLoading(true);
    fetch('/api/admin/unknowns?limit=100', { credentials: 'include' })
      .then((r) => r.json())
      .then((d) => setItems(d.items || []))
      .catch(() => setItems([]))
      .finally(() => setLoading(false));
  };
  useEffect(() => { if (open) load(); }, [open]);
  useEffect(() => {
    if (!open || !selected) { setSuggestions(null); return; }
    setSuggLoading(true);
    fetch(`/api/admin/unknowns/${selected.id}/suggest`, { credentials: 'include' })
      .then((r) => r.json())
      .then((d) => setSuggestions(d.suggestions || []))
      .catch(() => setSuggestions([]))
      .finally(() => setSuggLoading(false));
  }, [selected, open]);

  if (!open) return null;

  const resolve = async (employee_id, promoteSnapshot) => {
    if (!selected) return;
    await fetch(`/api/admin/unknowns/${selected.id}/resolve`, {
      method: 'POST', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ employee_id, promote_snapshot: !!promoteSnapshot }),
    }).catch(() => null);
    setItems((prev) => prev.filter((x) => x.id !== selected.id));
    setSelected(null);
  };
  const dismiss = async () => {
    if (!selected) return;
    await fetch(`/api/admin/unknowns/${selected.id}/dismiss`, {
      method: 'POST', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ reason: 'not a person' }),
    }).catch(() => null);
    setItems((prev) => prev.filter((x) => x.id !== selected.id));
    setSelected(null);
  };

  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(15,15,22,0.55)',
      backdropFilter: 'blur(2px)', zIndex: 9999, display: 'grid',
      placeItems: 'center', padding: 20,
    }}>
      <div className="card" style={{
        width: 'min(1100px, 96vw)', height: 'min(720px, 92vh)',
        padding: 0, display: 'flex', flexDirection: 'column',
        boxShadow: '0 24px 60px rgba(0,0,0,0.35)',
      }}>
        {/* Header */}
        <div style={{padding: '14px 20px', borderBottom: '1px solid var(--line)', display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
          <div>
            <div className="h3">ตรวจรูปนิรนาม</div>
            <div className="muted" style={{fontSize: 12, marginTop: 2}}>
              รูปที่ระบบสแกนแล้วไม่รู้จัก — จับคู่กับคนในระบบเพื่อให้ข้อมูลครบ
            </div>
          </div>
          <div className="row" style={{gap: 8}}>
            <button className="btn btn-soft btn-sm" onClick={load}>
              <Icon name="refresh" size={13}/>โหลดใหม่
            </button>
            <button className="btn btn-soft btn-sm" onClick={onClose}>ปิด</button>
          </div>
        </div>

        {/* Body: grid (left) + detail (right) */}
        <div style={{flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: selected ? '1fr 360px' : '1fr', gap: 0}}>
          {/* Thumbnails grid */}
          <div style={{overflowY: 'auto', padding: 16}}>
            {loading ? (
              <div className="muted" style={{padding: 40, textAlign: 'center'}}>กำลังโหลด...</div>
            ) : items.length === 0 ? (
              <div style={{padding: 60, textAlign: 'center'}}>
                <div style={{width: 56, height: 56, margin: '0 auto 14px', borderRadius: 16, background: 'var(--surface-2)', display: 'grid', placeItems: 'center', color: 'var(--ink-4)'}}>
                  <Icon name="check" size={24}/>
                </div>
                <div className="h3" style={{marginBottom: 4}}>ไม่มีรูปนิรนามที่ต้องตรวจ</div>
                <div className="muted" style={{fontSize: 13}}>ทุกการสแกนถูกจับคู่กับคนในระบบเรียบร้อย</div>
              </div>
            ) : (
              <div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 12}}>
                {items.map((it) => (
                  <button
                    key={it.id}
                    onClick={() => setSelected(it)}
                    style={{
                      border: selected?.id === it.id ? '2px solid var(--primary)' : '1px solid var(--line)',
                      borderRadius: 12, padding: 0, cursor: 'pointer', background: 'var(--surface)',
                      overflow: 'hidden', textAlign: 'left',
                    }}
                  >
                    <div style={{aspectRatio: '1/1', background: 'var(--surface-2)', overflow: 'hidden'}}>
                      {it.snapshot_url && <img src={it.snapshot_url} alt="" style={{width: '100%', height: '100%', objectFit: 'cover'}}/>}
                    </div>
                    <div style={{padding: '8px 10px'}}>
                      <div className="mono" style={{fontSize: 10.5, color: 'var(--ink-4)'}}>
                        DEV-{String(it.device_id).padStart(2,'0')}
                      </div>
                      <div className="mono" style={{fontSize: 11, color: 'var(--ink-2)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>
                        {it.scan_time}
                      </div>
                    </div>
                  </button>
                ))}
              </div>
            )}
          </div>

          {/* Detail/suggestions panel */}
          {selected && (
            <div style={{borderLeft: '1px solid var(--line)', display: 'flex', flexDirection: 'column', minHeight: 0}}>
              <div style={{padding: 14, borderBottom: '1px solid var(--line)'}}>
                <div style={{aspectRatio: '1/1', background: 'var(--surface-2)', borderRadius: 12, overflow: 'hidden', marginBottom: 10}}>
                  {selected.snapshot_url && <img src={selected.snapshot_url} alt="" style={{width: '100%', height: '100%', objectFit: 'cover'}}/>}
                </div>
                <div className="mono" style={{fontSize: 11, color: 'var(--ink-3)'}}>
                  DEV-{String(selected.device_id).padStart(2,'0')} · {selected.scan_time}
                </div>
              </div>
              <div style={{flex: 1, overflowY: 'auto', padding: 14}}>
                <div style={{fontSize: 12, fontWeight: 700, marginBottom: 10, color: 'var(--ink-2)'}}>
                  น่าจะเป็นคนนี้?
                </div>
                {suggLoading ? (
                  <div className="muted" style={{padding: 20, textAlign: 'center', fontSize: 12}}>กำลังหาคำแนะนำ...</div>
                ) : !suggestions || suggestions.length === 0 ? (
                  <div className="muted" style={{padding: 20, textAlign: 'center', fontSize: 12}}>
                    ระบบไม่พบ candidate — กรุณาเลือกคนเองหรือ dismiss
                  </div>
                ) : suggestions.map((s) => (
                  <div key={s.employee_id} style={{
                    display: 'flex', alignItems: 'center', gap: 10,
                    padding: 10, marginBottom: 8, borderRadius: 10,
                    border: '1px solid var(--line)',
                  }}>
                    <div style={{
                      width: 40, height: 40, borderRadius: '50%', overflow: 'hidden',
                      background: 'var(--surface-2)', flex: 'none',
                    }}>
                      {s.photo_url && <img src={s.photo_url} alt="" style={{width: '100%', height: '100%', objectFit: 'cover'}}/>}
                    </div>
                    <div style={{flex: 1, minWidth: 0}}>
                      <div style={{fontSize: 12.5, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>
                        {s.name || s.employee_id}
                      </div>
                      <div className="muted" style={{fontSize: 10.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>
                        {(s.reasons || []).slice(0, 1).join(' · ')}
                      </div>
                    </div>
                    <button
                      className="btn btn-coral btn-sm"
                      style={{padding: '4px 10px', fontSize: 11}}
                      onClick={() => resolve(s.employee_id, !s.photo_path)}
                      title={s.photo_path ? 'ยืนยันการจับคู่' : 'ยืนยัน + ใช้รูปนี้เป็นรูปประจำตัว'}
                    >
                      ใช่
                    </button>
                  </div>
                ))}
              </div>
              <div style={{padding: 12, borderTop: '1px solid var(--line)', display: 'flex', gap: 8}}>
                <button
                  className="btn btn-soft btn-sm"
                  style={{flex: 1}}
                  onClick={dismiss}
                >
                  ไม่ใช่คน (ทิ้ง)
                </button>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ===== Sync progress overlay =====
// Modal that shows a big circular % ring while a long-running sync is in
// flight, then morphs into a result card (✓ success / ⚠ partial / ✗ error).
// On partial/error it surfaces a mini-popup list of which (employee, device)
// pairs failed so the user can retry or fix them.
//
// Props:
//   open         — boolean, render the overlay
//   total        — denominator (number of employees, OR emp×dev pairs)
//   processed    — numerator (incremented per WS sync_progress event)
//   failed       — array of { employee_id, device_code, reason } for failed rows
//   status       — 'running' | 'success' | 'partial' | 'error'
//   label        — small text above the % ("Sync รายชื่อ", "Sync ไปเครื่อง DEV-03", …)
//   errorMsg     — fatal error message (only used when status === 'error')
//   onClose      — fired when user clicks ปิด (only enabled when not running)
function SyncProgressOverlay({ open, total, processed, failed = [], skippedDevices = 0, status = 'running', label = 'Sync', errorMsg, onClose }) {
  const [showFailDetails, setShowFailDetails] = useState(false);
  // Auto-close on full success after 1.5s — keep the overlay around if there
  // are failures so the user actually reads them.
  useEffect(() => {
    if (status === 'success' && open) {
      const t = setTimeout(() => onClose?.(), 1500);
      return () => clearTimeout(t);
    }
    return undefined;
  }, [status, open, onClose]);

  if (!open) return null;

  const denom = Math.max(total || 0, 1);
  const pct = Math.min(100, Math.round((processed / denom) * 100));
  const size = 180;
  const stroke = 14;
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const offset = c * (1 - pct / 100);

  const isRunning = status === 'running';
  const ringColor = status === 'error'   ? '#DC2626'
                  : status === 'partial' ? '#F59E0B'
                  : status === 'success' ? '#10B981'
                  : 'var(--primary)';
  const headline  = status === 'success' ? 'Sync เสร็จสมบูรณ์'
                  : status === 'partial' ? 'Sync เสร็จ — บางส่วนล้มเหลว'
                  : status === 'error'   ? 'Sync ไม่สำเร็จ'
                  : 'กำลัง Sync...';
  const failedCount = (failed || []).length;

  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(15,15,22,0.55)',
      backdropFilter: 'blur(2px)', display: 'grid', placeItems: 'center',
      zIndex: 9999, padding: 20,
    }}>
      <div className="card" style={{
        minWidth: 360, maxWidth: 460, padding: 28, textAlign: 'center',
        boxShadow: '0 24px 60px rgba(0,0,0,0.35)',
      }}>
        {/* Big circular ring */}
        <div style={{position: 'relative', width: size, height: size, margin: '0 auto 18px'}}>
          <svg width={size} height={size} style={{transform: 'rotate(-90deg)'}}>
            <circle cx={size/2} cy={size/2} r={r}
              stroke="var(--surface-2)" strokeWidth={stroke} fill="none"/>
            <circle cx={size/2} cy={size/2} r={r}
              stroke={ringColor} strokeWidth={stroke} fill="none"
              strokeDasharray={c} strokeDashoffset={offset} strokeLinecap="round"
              style={{transition: 'stroke-dashoffset 400ms ease, stroke 300ms ease'}}
            />
          </svg>
          <div style={{position: 'absolute', inset: 0, display: 'grid', placeItems: 'center'}}>
            {status === 'success' ? (
              <div style={{
                width: 96, height: 96, borderRadius: '50%',
                background: 'rgba(16,185,129,0.12)',
                display: 'grid', placeItems: 'center', color: ringColor,
              }}>
                <Icon name="check" size={56} stroke={2.5}/>
              </div>
            ) : status === 'error' ? (
              <div style={{
                width: 96, height: 96, borderRadius: '50%',
                background: 'rgba(220,38,38,0.12)',
                display: 'grid', placeItems: 'center', color: ringColor,
              }}>
                <Icon name="x" size={56} stroke={2.5}/>
              </div>
            ) : status === 'partial' ? (
              <div className="mono" style={{fontSize: 32, fontWeight: 700, color: ringColor}}>
                {pct}<span style={{fontSize: 18, color: 'var(--ink-3)'}}>%</span>
              </div>
            ) : (
              <div className="mono" style={{fontSize: 36, fontWeight: 700, color: 'var(--ink-1)', lineHeight: 1}}>
                {pct}<span style={{fontSize: 18, color: 'var(--ink-3)', marginLeft: 2}}>%</span>
              </div>
            )}
          </div>
        </div>

        <div className="h3" style={{marginBottom: 4}}>{headline}</div>
        <div className="muted" style={{fontSize: 13, marginBottom: 18}}>
          {label} · {processed.toLocaleString('th-TH')} / {total.toLocaleString('th-TH')}
          {failedCount > 0 && <> · <span style={{color: '#DC2626', fontWeight: 600}}>{failedCount} ล้มเหลว</span></>}
          {skippedDevices > 0 && <> · <span style={{color: '#6B7280', fontWeight: 600}}>{skippedDevices} เครื่องข้าม (offline)</span></>}
        </div>

        {/* Fatal error message */}
        {status === 'error' && errorMsg && (
          <div style={{
            background: 'rgba(220,38,38,0.08)', border: '1px solid rgba(220,38,38,0.22)',
            borderRadius: 12, padding: 12, marginBottom: 14, textAlign: 'left',
            fontSize: 12.5, color: '#DC2626',
          }}>
            {errorMsg}
          </div>
        )}

        {/* Mini popup: failed list */}
        {failedCount > 0 && (
          <div style={{
            background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.28)',
            borderRadius: 12, padding: 12, marginBottom: 14, textAlign: 'left',
          }}>
            <div className="row" style={{justifyContent: 'space-between', marginBottom: showFailDetails ? 8 : 0}}>
              <div style={{fontSize: 12.5, fontWeight: 700, color: '#B45309'}}>
                {failedCount.toLocaleString('th-TH')} ราย sync ไม่สำเร็จ
              </div>
              <button
                className="btn btn-soft btn-sm"
                style={{padding: '2px 10px', fontSize: 11.5}}
                onClick={() => setShowFailDetails((v) => !v)}
              >
                {showFailDetails ? 'ซ่อน' : 'ดูรายละเอียด'}
              </button>
            </div>
            {showFailDetails && (
              <div style={{maxHeight: 160, overflowY: 'auto', fontSize: 11.5, color: 'var(--ink-3)'}}>
                {failed.slice(0, 50).map((f, i) => (
                  <div key={i} className="mono" style={{padding: '3px 0', borderBottom: i < Math.min(failed.length, 50) - 1 ? '1px dashed var(--line)' : 'none'}}>
                    <span style={{color: 'var(--ink-1)', fontWeight: 600}}>{f.employee_id || '—'}</span>
                    {f.device_code && <> → <span style={{color: 'var(--ink-2)'}}>{f.device_code}</span></>}
                    <span style={{color: '#B45309', marginLeft: 6}}>{f.reason || f.face_error || f.step || 'failed'}</span>
                  </div>
                ))}
                {failed.length > 50 && (
                  <div className="muted" style={{fontSize: 11, marginTop: 6}}>
                    ... และอีก {(failed.length - 50).toLocaleString('th-TH')} ราย
                  </div>
                )}
              </div>
            )}
          </div>
        )}

        {/* Footer */}
        {isRunning ? (
          <div className="muted" style={{fontSize: 11.5, lineHeight: 1.6}}>
            กรุณาอย่าปิดหน้าจอ — Sync กำลังดำเนินการ<br/>
            (เครื่อง offline จะถูกข้ามและรายงานเมื่อเสร็จ)
          </div>
        ) : (
          <button className="btn btn-coral btn-sm" style={{minWidth: 120}} onClick={onClose}>
            ปิด
          </button>
        )}
      </div>
    </div>
  );
}

// ===== ModalShell =====
// Shared modal container used by every CRUD page. Click-backdrop or X to close.
function ModalShell({ title, onClose, width = 520, children }) {
  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 80,
      background: 'rgba(15,14,20,0.5)', backdropFilter: 'blur(6px)',
      display: 'grid', placeItems: 'center', padding: 20,
      animation: 'result-in 200ms ease',
    }} onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: '100%', maxWidth: width,
        background: 'var(--surface)', borderRadius: 'var(--r-xl, 18px)',
        boxShadow: 'var(--shadow-lg, 0 20px 60px rgba(0,0,0,0.25))',
        overflow: 'hidden', maxHeight: '90vh', display: 'flex', flexDirection: 'column',
      }}>
        <div className="row" style={{padding: '18px 20px', borderBottom: '1px solid var(--line)', justifyContent: 'space-between'}}>
          <div className="h2" style={{fontSize: 18}}>{title}</div>
          <button className="tb-icon-btn" onClick={onClose} style={{width: 32, height: 32}}>
            <Icon name="x" size={16}/>
          </button>
        </div>
        <div style={{padding: 20, overflowY: 'auto'}}>{children}</div>
      </div>
    </div>
  );
}

Object.assign(window, {
  Sidebar, Topbar, Stat, Sparkline, HourlyChart, Avatar, Toggle, Empty, ModalShell,
  SyncProgressOverlay, UnknownReviewModal,
  makeInitials, fmtDateThai, fmtClock, fmtClockShort, timeAgo,
});
