// Shared UI primitives

// ── Pill/Badge ──────────────────────────────────────────────
const Pill = ({ label, tone = 'neutral', dot, size = 'md', icon }) => {
  const t = TONE_PALETTE[tone] || TONE_PALETTE.neutral;
  const pads = {
    xs: { padding: '0 5px',    fontSize: 9 },
    sm: { padding: '2px 8px',  fontSize: 11 },
    md: { padding: '3px 10px', fontSize: 12 },
    lg: { padding: '5px 12px', fontSize: 13 },
  };
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 5,
      borderRadius: 999, fontWeight: 600,
      background: t.bg, color: t.fg, whiteSpace: 'nowrap',
      letterSpacing: '-0.005em',
      ...pads[size],
    }}>
      {icon}
      {dot && <span style={{ width: 6, height: 6, borderRadius: '50%', background: t.fg, opacity: 0.85 }} />}
      {label}
    </span>
  );
};

const RoleBadge = ({ role, size = 'md' }) => {
  const meta = ROLE_META[role] || ROLE_META.regular;
  return <Pill label={meta.label} tone={meta.tone} size={size} />;
};

// ── Status pill for members (정지 = crimson, 패널티 = danger) ─────
const MemberStatusPill = ({ m }) => {
  if (m.permanently_banned) return <Pill label="영구정지" tone="crimson" />;
  if (m.suspended_until)    return <Pill label="패널티"   tone="danger"  />;
  return <Pill label="정상" tone="ok" />;
};

// ── Avatar ──────────────────────────────────────────────────
const Avatar = ({ initials, size = 32 }) => (
  <div style={{
    width: size, height: size, borderRadius: '50%',
    background: `oklch(0.93 0.04 ${(initials.charCodeAt(0) * 7) % 360})`,
    color: `oklch(0.36 0.10 ${(initials.charCodeAt(0) * 7) % 360})`,
    display: 'grid', placeItems: 'center',
    fontSize: size * 0.4, fontWeight: 700,
    letterSpacing: '-0.02em', flexShrink: 0,
  }}>{initials}</div>
);

// ── Button ──────────────────────────────────────────────────
const Button = ({ variant = 'default', size = 'md', children, onClick, icon, style: xs, disabled, ...rest }) => {
  const sizes = {
    xs: { padding: '4px 8px',  fontSize: 11.5, height: 26, borderRadius: 7 },
    sm: { padding: '6px 10px', fontSize: 12,   height: 30, borderRadius: 8 },
    md: { padding: '8px 14px', fontSize: 13,   height: 36, borderRadius: 10 },
    lg: { padding: '10px 18px',fontSize: 14,   height: 42, borderRadius: 10 },
  };
  const variants = {
    default: { background: '#fff',            color: 'var(--ink-2)', border: '1px solid var(--line-strong)' },
    primary: { background: 'var(--accent)',   color: '#fff',         border: '1px solid transparent' },
    ghost:   { background: 'transparent',     color: 'var(--ink-3)', border: '1px solid transparent' },
    danger:  { background: '#fff',            color: 'var(--danger)',border: '1px solid oklch(0.88 0.06 25)' },
    solidDanger: { background: 'var(--danger)', color: '#fff',       border: '1px solid transparent' },
    excel:   { background: '#059669', color: '#fff', border: '1px solid transparent' },
  };
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 5,
        fontWeight: 500, letterSpacing: '-0.01em', cursor: disabled ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.5 : 1,
        transition: 'all 0.12s ease',
        fontFamily: 'inherit', whiteSpace: 'nowrap',
        ...sizes[size], ...variants[variant], ...xs,
      }}
      onMouseEnter={e => {
        if (disabled) return;
        // xs(inline style)에 background 가 있으면 그것을 우선 — brightness 로 hover 표현
        if (xs && xs.background) {
          e.currentTarget.style.filter = 'brightness(1.08)';
        } else if (variant === 'default') e.currentTarget.style.background = '#f6f8fa';
        else if (variant === 'primary') e.currentTarget.style.filter = 'brightness(1.08)';
        else if (variant === 'ghost')   e.currentTarget.style.background = '#f3f5f8';
        else if (variant === 'danger')  e.currentTarget.style.background = 'oklch(0.97 0.03 25)';
        else if (variant === 'solidDanger') e.currentTarget.style.filter = 'brightness(1.08)';
      }}
      onMouseLeave={e => {
        // xs.background 가 있으면 그 값으로 복귀, 아니면 variant 기본값
        e.currentTarget.style.background = (xs && xs.background) || variants[variant].background;
        e.currentTarget.style.filter = 'none';
      }}
      {...rest}
    >{icon}{children}</button>
  );
};

// ── Input / Select ──────────────────────────────────────────
const TextInput = ({ value, onChange, placeholder, icon, width, type = 'text', size = 'md', onKeyDown, maxLength }) => {
  const [focus, setFocus] = React.useState(false);
  const heights = { sm: 32, md: 38, lg: 42 };
  // 한글 IME: onChange는 매 단계 통과(원래 동작 유지) + compositionEnd 에서 최종값 한 번 더 push (안전망)
  return (
    <div style={{
      position: 'relative', display: 'inline-flex', alignItems: 'center',
      width: width || 'auto', flex: width ? 'none' : undefined,
      minWidth: width ? undefined : 0,
      height: heights[size], background: '#fff',
      border: `1px solid ${focus ? 'var(--accent)' : 'var(--line-strong)'}`,
      boxShadow: focus ? '0 0 0 3px var(--accent-soft)' : 'none',
      borderRadius: 10, padding: '0 12px', gap: 8,
      transition: 'all 0.12s',
    }}>
      {icon && <span style={{ color: 'var(--ink-4)', display: 'flex' }}>{icon}</span>}
      <input
        type={type}
        value={value}
        onChange={e => onChange(e.target.value)}
        onCompositionEnd={e => onChange(e.target.value)}
        onKeyDown={onKeyDown}
        onFocus={() => setFocus(true)}
        onBlur={() => setFocus(false)}
        placeholder={placeholder}
        maxLength={maxLength}
        style={{
          flex: 1, minWidth: 0, border: 'none', outline: 'none',
          background: 'transparent', fontFamily: 'inherit',
          fontSize: 13.5, color: 'var(--ink)',
        }}
      />
    </div>
  );
};

const Select = ({ value, onChange, options, icon, width, size = 'md' }) => {
  const heights = { sm: 32, md: 38, lg: 42 };
  return (
    <label style={{
      position: 'relative', display: 'inline-flex', alignItems: 'center',
      width: width || 'auto', height: heights[size], background: '#fff',
      border: '1px solid var(--line-strong)', borderRadius: 10,
      padding: '0 12px', gap: 8, cursor: 'pointer',
      transition: 'border-color 0.12s',
    }}
    onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--ink-4)'}
    onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--line-strong)'}
    >
      {icon && <span style={{ color: 'var(--ink-3)', display: 'flex' }}>{icon}</span>}
      <select
        value={value}
        onChange={e => onChange(e.target.value)}
        style={{
          flex: 1, border: 'none', outline: 'none', background: 'transparent',
          fontSize: 13.5, color: 'var(--ink)', appearance: 'none',
          cursor: 'pointer', paddingRight: 18, fontFamily: 'inherit',
        }}>
        {options.map(o => typeof o === 'string'
          ? <option key={o} value={o}>{o}</option>
          : <option key={o.value} value={o.value}>{o.label}</option>)}
      </select>
      <span style={{ position: 'absolute', right: 12, color: 'var(--ink-4)', pointerEvents: 'none' }}>
        <Icon name="chevron" size={14} />
      </span>
    </label>
  );
};

// ── Toggle ──────────────────────────────────────────────────
const Toggle = ({ value, onChange, size = 'md' }) => {
  const dims = size === 'sm' ? { w: 34, h: 20, k: 16 } : { w: 42, h: 24, k: 20 };
  return (
    <button onClick={onChange} aria-pressed={value} style={{
      width: dims.w, height: dims.h, borderRadius: 999, padding: 0,
      border: 'none', cursor: 'pointer', flexShrink: 0,
      background: value ? 'var(--accent)' : '#d4d8df',
      position: 'relative', transition: 'background 0.15s',
    }}>
      <span style={{
        position: 'absolute', top: 2, left: value ? dims.w - dims.k - 2 : 2,
        width: dims.k, height: dims.k, borderRadius: '50%',
        background: '#fff', transition: 'left 0.15s',
        boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
      }} />
    </button>
  );
};

// ── Chip (filter) ───────────────────────────────────────────
const Chip = ({ active, onClick, children, tone, style: xs }) => {
  const toneBg = tone && TONE_PALETTE[tone] ? TONE_PALETTE[tone].bg : 'var(--accent-soft)';
  const toneFg = tone && TONE_PALETTE[tone] ? TONE_PALETTE[tone].fg : 'var(--accent-ink)';
  return (
    <button onClick={onClick} style={{
      padding: '6px 12px', borderRadius: 999, fontSize: 12.5, fontWeight: 500,
      border: `1px solid ${active ? 'transparent' : 'var(--line-strong)'}`,
      background: active ? toneBg : '#fff',
      color: active ? toneFg : 'var(--ink-2)',
      cursor: 'pointer', transition: 'all 0.12s', fontFamily: 'inherit',
      ...xs,
    }}>{children}</button>
  );
};

// ── Card ────────────────────────────────────────────────────
const Card = ({ title, desc, actions, children, style: xs, noBody }) => (
  <div style={{
    background: '#fff', border: '1px solid var(--line)', borderRadius: 12,
    overflow: 'hidden', display: 'flex', flexDirection: 'column', ...xs,
  }}>
    {(title || actions) && (
      <div className="tsl-card-header" style={{
        display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
        padding: '14px 18px', borderBottom: noBody ? 'none' : '1px solid var(--line)', gap: 12,
        flexShrink: 0,
      }}>
        <div style={{ minWidth: 0 }}>
          {title && <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>{title}</div>}
          {desc && <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 3 }}>{desc}</div>}
        </div>
        {actions && <div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>{actions}</div>}
      </div>
    )}
    {children && <div style={{ padding: noBody ? 0 : 16, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>{children}</div>}
  </div>
);

// ── Section heading ─────────────────────────────────────────
const SectionHeader = ({ title, desc, actions }) => (
  <div className="tsl-section-header" style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginBottom: 16, gap: 12 }}>
    <div style={{ minWidth: 0 }}>
      <h1 style={{ fontSize: 20, fontWeight: 700, color: 'var(--ink)', margin: 0, letterSpacing: '-0.02em' }}>{title}</h1>
      {desc && <div style={{ fontSize: 13, color: 'var(--ink-3)', marginTop: 4 }}>{desc}</div>}
    </div>
    {actions && <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>{actions}</div>}
  </div>
);

// ── Toolbar (filter bar) ────────────────────────────────────
const Toolbar = ({ children, style: xs }) => (
  <div className="tsl-toolbar" style={{
    background: '#fff', border: '1px solid var(--line)', borderRadius: 12,
    padding: 12, display: 'flex', gap: 8, alignItems: 'center',
    flexWrap: 'wrap', ...xs,
  }}>{children}</div>
);

// ── Date range (시작~종료 + 프리셋) ──────────────────────────
const DateRange = ({ start, end, onStart, onEnd, preset, onPreset }) => {
  const isMobile = useIsMobile();
  const toast = useToast();
  const guardStart = (v) => {
    if (v && end && v > end) {
      if (toast) toast('error', '시작일이 종료일보다 뒤일 수 없습니다');
      return;
    }
    onStart(v);
  };
  const guardEnd = (v) => {
    if (v && start && v < start) {
      if (toast) toast('error', '종료일이 시작일보다 앞일 수 없습니다');
      return;
    }
    onEnd(v);
  };
  return (
    <div style={{
      display: 'flex',
      flexDirection: isMobile ? 'column' : 'row',
      alignItems: isMobile ? 'stretch' : 'center',
      gap: isMobile ? 6 : 6,
      width: isMobile ? '100%' : 'auto',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, width: isMobile ? '100%' : 'auto' }}>
        <DateInput value={start} onChange={guardStart} fullWidth={isMobile} />
        <span style={{ color: 'var(--ink-4)', flexShrink: 0 }}>~</span>
        <DateInput value={end} onChange={guardEnd} fullWidth={isMobile} />
      </div>
      <div style={{
        display: 'flex', gap: 4,
        marginLeft: isMobile ? 0 : 4,
        width: isMobile ? '100%' : 'auto',
      }}>
        {[['1m', '1달'], ['3m', '3달'], ['1y', '1년']].map(([v, l]) => (
          <Chip key={v} active={preset === v} onClick={() => onPreset(v)}
            style={isMobile ? { flex: 1, justifyContent: 'center' } : undefined}>{l}</Chip>
        ))}
      </div>
    </div>
  );
};

const DateInput = ({ value, onChange, fullWidth }) => (
  <label style={{
    display: fullWidth ? 'flex' : 'inline-flex', alignItems: 'center', height: 38,
    background: '#fff', border: '1px solid var(--line-strong)',
    borderRadius: 10, padding: '0 10px', gap: 6,
    flex: fullWidth ? 1 : 'none', minWidth: 0,
  }}>
    <span style={{ color: 'var(--ink-4)', flexShrink: 0 }}><Icon name="calendar" size={14} /></span>
    <input type="date" value={value} onChange={e => onChange(e.target.value)}
      style={{
        border: 'none', outline: 'none', background: 'transparent',
        fontFamily: 'var(--mono)', fontSize: 12.5, color: 'var(--ink)',
        width: fullWidth ? '100%' : 130, minWidth: 0,
      }}
    />
  </label>
);

// ── Empty state ─────────────────────────────────────────────
// Variants: search (검색 결과 없음) · check (패널티 없음) · mail (오류보고 없음) ·
//           chat (최근대화 없음) · coin (토큰내역 없음) · data (공통)
const EmptyIllustration = ({ variant }) => {
  const bgs = {
    search: { ring: 'oklch(0.94 0.03 255)', ink: 'oklch(0.58 0.13 255)' },
    check:  { ring: 'oklch(0.93 0.05 155)', ink: 'oklch(0.50 0.13 155)' },
    mail:   { ring: 'oklch(0.95 0.04 255)', ink: 'oklch(0.55 0.14 255)' },
    chat:   { ring: 'oklch(0.95 0.04 290)', ink: 'oklch(0.50 0.14 290)' },
    coin:   { ring: 'oklch(0.95 0.05 75)',  ink: 'oklch(0.50 0.12 75)' },
    data:   { ring: '#f1f3f6',              ink: '#94a3b8' },
  };
  const c = bgs[variant] || bgs.data;
  return (
    <svg width="88" height="88" viewBox="0 0 88 88" fill="none" style={{ display: 'block' }}>
      <circle cx="44" cy="44" r="40" fill={c.ring} />
      <circle cx="44" cy="44" r="28" fill="#fff" />
      <g stroke={c.ink} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none">
        {variant === 'search' && <>
          <circle cx="40" cy="40" r="10" />
          <path d="m48 48 6 6" />
        </>}
        {variant === 'check' && <path d="m34 44 7 7 14-16" strokeWidth="2.6" />}
        {variant === 'mail' && <>
          <rect x="30" y="34" width="28" height="20" rx="2" />
          <path d="m30 36 14 10 14-10" />
        </>}
        {variant === 'chat' && <>
          <path d="M30 36h28v16H42l-8 6v-6h0a4 4 0 0 1-4-4V36z" />
          <circle cx="40" cy="44" r="1" fill={c.ink} />
          <circle cx="44" cy="44" r="1" fill={c.ink} />
          <circle cx="48" cy="44" r="1" fill={c.ink} />
        </>}
        {variant === 'coin' && <>
          <circle cx="44" cy="44" r="11" />
          <path d="M40 44h8M44 40v8" />
        </>}
        {(variant === 'data' || !variant) && <>
          <rect x="32" y="36" width="24" height="16" rx="2" />
          <path d="M36 42h16M36 46h10" />
        </>}
      </g>
    </svg>
  );
};

const EmptyState = ({ title, desc, variant = 'data', action }) => (
  <div style={{
    padding: '72px 24px', textAlign: 'center',
    background: '#fff', border: '1px solid var(--line)', borderRadius: 12,
  }}>
    <div style={{ margin: '0 auto 16px', display: 'flex', justifyContent: 'center' }}>
      <EmptyIllustration variant={variant} />
    </div>
    <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--ink)', marginBottom: 6, letterSpacing: '-0.01em' }}>{title}</div>
    {desc && <div style={{ fontSize: 13, color: 'var(--ink-3)', maxWidth: 360, margin: '0 auto', lineHeight: 1.5 }}>{desc}</div>}
    {action && <div style={{ marginTop: 18 }}>{action}</div>}
  </div>
);

// ── Stat card ───────────────────────────────────────────────
const StatCard = ({ label, value, unit, tone, delta }) => {
  const toneColor = ({
    ok: 'oklch(0.52 0.13 155)', danger: 'oklch(0.52 0.17 25)',
    warn: 'oklch(0.52 0.14 75)', blue: 'oklch(0.48 0.15 255)',
    purple: 'oklch(0.48 0.15 300)',
  })[tone] || 'var(--ink)';
  return (
    <div style={{ background: '#fff', border: '1px solid var(--line)', borderRadius: 12, padding: '14px 16px' }}>
      <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 6, fontWeight: 500 }}>{label}</div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
        <span style={{ fontSize: 22, fontWeight: 700, color: toneColor, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>
          {typeof value === 'number' ? value.toLocaleString() : value}
        </span>
        <span style={{ fontSize: 12, color: 'var(--ink-3)' }}>{unit}</span>
        {delta && <span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--ink-4)' }}>{delta}</span>}
      </div>
    </div>
  );
};

// ── Modal ───────────────────────────────────────────────────
const Modal = ({ open, onClose, title, children, width = 640, actions, tone }) => {
  if (!open) return null;
  const headerBg = tone === 'danger' ? 'oklch(0.97 0.04 25)'
                 : tone === 'warn'   ? 'oklch(0.97 0.05 75)'
                 : '#fff';
  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 100,
      background: 'rgba(15,23,42,0.35)', backdropFilter: 'blur(2px)',
      display: 'grid', placeItems: 'center', padding: 20,
      animation: 'tslFadeIn 0.14s ease',
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} style={{
        width, maxWidth: '100%', maxHeight: '90vh',
        background: '#fff', borderRadius: 14, overflow: 'hidden',
        boxShadow: '0 20px 60px rgba(15,23,42,0.2)',
        display: 'flex', flexDirection: 'column',
        animation: 'tslPopIn 0.16s cubic-bezier(0.2, 0.9, 0.3, 1.3)',
      }}>
        <div style={{
          padding: '16px 20px', borderBottom: '1px solid var(--line)',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          background: headerBg,
        }}>
          <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--ink)' }}>{title}</div>
          <button onClick={onClose} style={{
            border: 'none', background: 'transparent', cursor: 'pointer',
            color: 'var(--ink-3)', padding: 4, borderRadius: 6,
          }}><Icon name="close" size={16} /></button>
        </div>
        <div style={{ padding: 20, overflowY: 'auto', flex: 1 }}>{children}</div>
        {actions && (
          <div style={{ padding: '14px 20px', borderTop: '1px solid var(--line)', display: 'flex', gap: 8, justifyContent: 'flex-end', background: '#fafbfc' }}>
            {actions}
          </div>
        )}
      </div>
    </div>
  );
};

// ── Confirm dialog (일반/위험 2종) ──────────────────────────
const ConfirmDialog = ({ open, onClose, onConfirm, title, message, tone = 'default', confirmLabel = '확인' }) => {
  if (!open) return null;
  const iconBg = tone === 'danger' ? 'oklch(0.94 0.06 25)'  : 'oklch(0.95 0.05 75)';
  const iconFg = tone === 'danger' ? 'oklch(0.50 0.17 25)'  : 'oklch(0.50 0.14 75)';
  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 110,
      background: 'rgba(15,23,42,0.4)', backdropFilter: 'blur(3px)',
      display: 'grid', placeItems: 'center', padding: 20,
      animation: 'tslFadeIn 0.14s ease',
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} style={{
        width: 440, maxWidth: '100%', background: '#fff',
        borderRadius: 14, overflow: 'hidden',
        boxShadow: '0 24px 70px rgba(15,23,42,0.25)',
        animation: 'tslPopIn 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.3)',
      }}>
        <div style={{ padding: '24px 24px 18px', display: 'flex', gap: 14 }}>
          <div style={{
            width: 44, height: 44, borderRadius: 12, flexShrink: 0,
            background: iconBg, color: iconFg,
            display: 'grid', placeItems: 'center',
          }}><Icon name={tone === 'danger' ? 'warning' : 'info'} size={22} stroke={2} /></div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--ink)', marginBottom: 6 }}>{title}</div>
            <div style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.55, whiteSpace: 'pre-line' }}>{message}</div>
          </div>
        </div>
        <div style={{ padding: '12px 20px', background: '#fafbfc', borderTop: '1px solid var(--line)',
          display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <Button onClick={onClose}>취소</Button>
          <Button variant={tone === 'danger' ? 'solidDanger' : 'primary'}
            onClick={() => { onConfirm && onConfirm(); onClose(); }}>{confirmLabel}</Button>
        </div>
      </div>
    </div>
  );
};

// ── Token adjust modal (검색·회원정보에서 호출) ─────────────
const TokenAdjustModal = ({ open, onClose, member, onApply }) => {
  const [free, setFree]   = React.useState(0);
  const [bonus, setBonus] = React.useState(0);
  const [reason, setReason] = React.useState('');

  React.useEffect(() => {
    if (open) { setFree(0); setBonus(0); setReason(''); }
  }, [open, member?.member_id]);

  if (!member) return null;
  const nextFree  = Math.max(0, member.free_coin + free);
  const nextBonus = Math.max(0, member.bonus_coin + bonus);

  return (
    <Modal open={open} onClose={onClose} title="토큰 수량 변경" width={520}
      actions={<>
        <Button onClick={onClose}>취소</Button>
        <Button variant="primary"
          disabled={free === 0 && bonus === 0}
          onClick={() => { onApply && onApply({ free, bonus, reason }); onClose(); }}
          icon={<Icon name="check" size={13} />}>적용</Button>
      </>}>
      <div style={{ display: 'grid', gap: 16 }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 10,
          padding: '10px 12px', borderRadius: 10, background: 'var(--surface-2)',
          border: '1px solid var(--line)',
        }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13.5, fontWeight: 700, color: 'var(--ink)' }}>
              {member.name || member.name_phone_key}
              <span style={{ fontWeight: 400, color: 'var(--ink-4)', marginLeft: 6, fontFamily: 'var(--mono)', fontSize: 11.5 }}>
                {member.name_phone_key || member.member_id}
              </span>
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--ink-3)', marginTop: 1 }}>{member.region}</div>
          </div>
          <RoleBadge role={member.role} />
        </div>

        <DeltaField
          label="일반 토큰"
          current={member.free_coin}
          delta={free}
          next={nextFree}
          onDelta={setFree}
          color="var(--ink)"
        />
        <DeltaField
          label="보너스 토큰"
          current={member.bonus_coin}
          delta={bonus}
          next={nextBonus}
          onDelta={setBonus}
          color="oklch(0.48 0.12 300)"
        />

        <div>
          <div style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600, marginBottom: 6 }}>
            사유 <span style={{ fontWeight: 400, color: 'var(--ink-4)' }}>· 최대 200자</span>
          </div>
          <textarea value={reason} onChange={e => setReason(e.target.value.slice(0, 200))}
            placeholder="지급/차감 사유를 입력하세요 (활동 로그에 기록됩니다)"
            style={{
              width: '100%', minHeight: 72, padding: '10px 12px',
              border: '1px solid var(--line-strong)', borderRadius: 10,
              fontFamily: 'inherit', fontSize: 13, resize: 'vertical',
              outline: 'none', color: 'var(--ink)',
            }} />
          <div style={{ textAlign: 'right', fontSize: 11, color: 'var(--ink-4)', marginTop: 4, fontFamily: 'var(--mono)' }}>
            {reason.length} / 200
          </div>
        </div>
      </div>
    </Modal>
  );
};

const DeltaField = ({ label, current, delta, next, onDelta, color }) => {
  const quickBtn = (v) => (
    <button onClick={() => onDelta(delta + v)} style={{
      height: 28, padding: '0 10px', borderRadius: 7,
      border: '1px solid var(--line-strong)', background: '#fff',
      fontSize: 11.5, color: v > 0 ? 'oklch(0.45 0.14 155)' : 'oklch(0.50 0.17 25)',
      fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--mono)',
    }}>{v > 0 ? `+${v}` : v}</button>
  );
  return (
    <div>
      <div style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600, marginBottom: 6 }}>
        {label}
      </div>
      <div style={{
        padding: '10px 12px', borderRadius: 10, border: '1px solid var(--line)',
        background: '#fafbfc',
        display: 'flex', alignItems: 'center', gap: 10,
      }}>
        <div style={{ flex: 1, display: 'flex', alignItems: 'baseline', gap: 6, fontVariantNumeric: 'tabular-nums', fontFamily: 'var(--mono)' }}>
          <span style={{ fontSize: 12, color: 'var(--ink-3)' }}>{current.toLocaleString()}</span>
          <span style={{ color: 'var(--ink-4)' }}>→</span>
          <span style={{ fontSize: 16, fontWeight: 700, color }}>{next.toLocaleString()}</span>
          {delta !== 0 && (
            <span style={{ fontSize: 11.5, fontWeight: 700, marginLeft: 2,
              color: delta > 0 ? 'oklch(0.45 0.14 155)' : 'oklch(0.50 0.17 25)' }}>
              ({delta > 0 ? '+' : ''}{delta.toLocaleString()})
            </span>
          )}
        </div>
        <input type="number" value={delta} onChange={e => onDelta(Number(e.target.value) || 0)}
          style={{
            width: 90, height: 32, padding: '0 10px', textAlign: 'right',
            border: '1px solid var(--line-strong)', borderRadius: 8,
            fontFamily: 'var(--mono)', fontSize: 13, fontWeight: 600,
            outline: 'none', color: 'var(--ink)', background: '#fff',
          }} />
      </div>
      <div style={{ display: 'flex', gap: 5, marginTop: 6, flexWrap: 'wrap' }}>
        {[-500, -100, -10, +10, +100, +500].map(v => <span key={v}>{quickBtn(v)}</span>)}
        <button onClick={() => onDelta(0)} style={{
          height: 28, padding: '0 10px', borderRadius: 7,
          border: '1px solid transparent', background: 'transparent',
          fontSize: 11.5, color: 'var(--ink-4)', cursor: 'pointer', fontFamily: 'inherit',
        }}>초기화</button>
      </div>
    </div>
  );
};

// ── Role dropdown popover ──────────────────────────────────
const RoleDropdown = ({ value, onChange, children }) => {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [open]);

  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
      <button onClick={(e) => { e.stopPropagation(); setOpen(o => !o); }} style={{
        border: 'none', background: 'transparent', padding: 0,
        cursor: 'pointer', fontFamily: 'inherit',
      }}>
        {children}
      </button>
      {open && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 4px)', left: 0, zIndex: 50,
          background: '#fff', borderRadius: 10, minWidth: 120,
          border: '1px solid var(--line-strong)',
          boxShadow: '0 8px 24px rgba(15,23,42,0.12)',
          padding: 4,
          animation: 'tslPopIn 0.12s ease',
        }}>
          {['regular', 'admin', 'master'].map(r => {
            const meta = ROLE_META[r];
            const isActive = value === r;
            return (
              <button key={r} onClick={() => { onChange(r); setOpen(false); }} style={{
                width: '100%', display: 'flex', alignItems: 'center', gap: 8,
                padding: '7px 10px', borderRadius: 7, border: 'none',
                background: isActive ? 'var(--accent-soft)' : 'transparent',
                color: isActive ? 'var(--accent-ink)' : 'var(--ink-2)',
                cursor: 'pointer', fontSize: 12.5, fontFamily: 'inherit',
                textAlign: 'left',
              }}
                onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = '#f6f8fa'; }}
                onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }}
              >
                <Pill label={meta.label} tone={meta.tone} size="sm" />
                <span style={{ flex: 1 }} />
                {isActive && <span style={{ color: 'var(--accent)' }}><Icon name="check" size={14} /></span>}
              </button>
            );
          })}
        </div>
      )}
    </span>
  );
};

// ── Toast ──────────────────────────────────────────────────
const ToastCtx = React.createContext(null);
const useToast = () => React.useContext(ToastCtx);

const ToastProvider = ({ children }) => {
  const [items, setItems] = React.useState([]);
  const push = React.useCallback((kind, msg) => {
    const id = Math.random().toString(36).slice(2);
    setItems(i => [...i, { id, kind, msg }]);
    setTimeout(() => setItems(i => i.filter(x => x.id !== id)), 2600);
  }, []);
  return (
    <ToastCtx.Provider value={push}>
      {children}
      {/* 원본 toast: top-right 20/20 */}
      <div style={{
        position: 'fixed', right: 20, top: 20, zIndex: 2000,
        display: 'flex', flexDirection: 'column', gap: 8, pointerEvents: 'none',
      }}>
        {items.map(t => <ToastItem key={t.id} kind={t.kind}>{t.msg}</ToastItem>)}
      </div>
    </ToastCtx.Provider>
  );
};

const ToastItem = ({ kind, children }) => {
  const meta = {
    success: { bg: 'oklch(0.40 0.13 155)', icon: 'check',   label: '성공' },
    error:   { bg: 'oklch(0.46 0.18 25)',  icon: 'close',   label: '실패' },
    info:    { bg: 'oklch(0.44 0.15 255)', icon: 'info',    label: '정보' },
    warn:    { bg: 'oklch(0.50 0.14 75)',  icon: 'warning', label: '경고' },
    neutral: { bg: '#1f2937',              icon: 'info',    label: '알림' },
  }[kind] || { bg: '#1f2937', icon: 'info', label: '알림' };
  return (
    <div style={{
      background: meta.bg, color: '#fff', borderRadius: 10,
      padding: '11px 14px', minWidth: 240, maxWidth: 360,
      boxShadow: '0 10px 30px rgba(15,23,42,0.24)',
      display: 'flex', alignItems: 'center', gap: 10,
      fontSize: 13, fontWeight: 500, lineHeight: 1.4,
      animation: 'tslSlideIn 0.22s cubic-bezier(0.2, 0.9, 0.3, 1.15)',
      pointerEvents: 'auto',
    }}>
      <Icon name={meta.icon} size={16} />
      <span style={{ flex: 1 }}>{children}</span>
    </div>
  );
};

Object.assign(window, {
  Pill, RoleBadge, MemberStatusPill, Avatar,
  Button, TextInput, Select, Toggle, Chip,
  Card, SectionHeader, Toolbar, DateRange, DateInput,
  EmptyState, StatCard, Modal, ConfirmDialog,
  TokenAdjustModal, RoleDropdown, ToastProvider, useToast,
});
