/* global React */
// Schools.ArtemenkoCRM — primitives (Icon, Button, Pill, Avatar, Card, Money, Input)

// Custom brand icons (inline so currentColor inherits the button text color).
const CUSTOM_ICONS = {
  'add-student': '<rect x="3.5" y="3.5" width="17" height="17" rx="4.5"/><path d="M12 8.3v7.4M8.3 12h7.4"/>',
  'remove-record': '<rect x="3.5" y="3.5" width="17" height="17" rx="4.5"/><path d="M9.2 9.2l5.6 5.6M14.8 9.2l-5.6 5.6"/>',
  'close-tile': '<rect x="3.5" y="3.5" width="17" height="17" rx="4.5"/><path d="M9.2 9.2l5.6 5.6M14.8 9.2l-5.6 5.6"/>',
};

function Icon({ name, size = 18, strokeWidth = 1.75, style }) {
  const ref = React.useRef(null);
  if (CUSTOM_ICONS[name]) {
    return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
        strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
        style={{ display: 'inline-flex', flex: 'none', ...style }}
        dangerouslySetInnerHTML={{ __html: CUSTOM_ICONS[name] }} />
    );
  }
  React.useEffect(() => {
    const host = ref.current;
    if (!host || !window.lucide) return;
    host.innerHTML = '';
    const i = document.createElement('i');
    i.setAttribute('data-lucide', name);
    i.setAttribute('width', size);
    i.setAttribute('height', size);
    i.setAttribute('stroke-width', strokeWidth);
    host.appendChild(i);
    window.lucide.createIcons();
  }, [name, size, strokeWidth]);
  return <span ref={ref} className="lc-icon" style={{ display: 'inline-flex', width: size, height: size, ...style }} />;
}

// Minted-coin buttons: subtle darkened bottom edge that depresses on press.
function Button({ variant = 'primary', icon, children, onClick, style, full, size = 'md' }) {
  const [press, setPress] = React.useState(false);
  const [hover, setHover] = React.useState(false);
  const pad = size === 'sm' ? '7px 13px' : '10px 17px';
  const fs = size === 'sm' ? 13 : 14;
  const base = {
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
    fontFamily: 'var(--font-sans)', fontSize: fs, fontWeight: 600, lineHeight: 1,
    borderRadius: 5, padding: pad, border: '1px solid transparent', cursor: 'pointer',
    transition: 'background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease)',
    width: full ? '100%' : 'auto', whiteSpace: 'nowrap',
    transform: press ? 'translateY(1px)' : 'none',
  };
  const variants = {
    primary: {
      background: hover ? 'var(--brand-hover)' : 'var(--brand)', color: '#fff',
      boxShadow: press ? 'inset 0 1px 2px rgba(13,22,52,0.45)' : 'inset 0 -2px 0 rgba(13,22,52,0.30)',
    },
    secondary: {
      background: hover ? 'var(--bg-soft)' : 'var(--bg-card)', color: 'var(--brand-ink)',
      borderColor: 'var(--border-strong)',
      boxShadow: press ? 'none' : 'inset 0 -2px 0 rgba(28,34,51,0.05)',
    },
    ghost: { background: 'transparent', color: 'var(--brand-ink)', textDecoration: hover ? 'underline' : 'none', textUnderlineOffset: 3 },
    danger: { background: hover ? 'var(--coral-bg)' : 'var(--bg-card)', color: 'var(--coral)', borderColor: '#E6C9C4' },
  };
  return (
    <button className={'btn-' + variant}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}
      onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}
      style={{ ...base, ...variants[variant], ...style }} onClick={onClick}>
      {icon && <Icon name={icon} size={size === 'sm' ? 15 : 16} />}
      {children}
    </button>
  );
}

// Status = quiet colored dot + label (ledger style). Funnel: new→call→trial→payment→active; refusal exits.
// `ring: true` renders a hollow dot (used for «Пробное» to distinguish from filled «Активный»).
const PILL = {
  new:     { dot: '#1E3A8A', fg: '#234381', label: 'Новая' },
  call:    { dot: '#9A7B33', fg: '#6A5320', label: 'Созвон' },
  trial:   { dot: '#2F6B4F', fg: '#214E39', label: 'Пробное', ring: true },
  payment: { dot: '#93702F', fg: '#5E461E', label: 'Оплата' },
  active:  { dot: '#2F6B4F', fg: '#214E39', label: 'Активный' },
  refusal: { dot: '#A8443B', fg: '#79302A', label: 'Отказ' },
  draft:   { dot: '#A6A296', fg: '#7E8595', label: 'Черновик' },
  vip:     { dot: '#93702F', fg: '#5E461E', label: 'VIP' },
  // Счета и финансы
  paid:      { dot: '#2F6B4F', fg: '#214E39', label: 'Оплачен' },
  pending:   { dot: '#9A7B33', fg: '#6A5320', label: 'Ожидает' },
  overdue:   { dot: '#A8443B', fg: '#79302A', label: 'Просрочен' },
  cancelled: { dot: '#A6A296', fg: '#7E8595', label: 'Отменён' },
  refund:    { dot: '#234381', fg: '#234381', label: 'Возврат', ring: true },
};

function Pill({ kind, children }) {
  if (kind === 'vip') {
    return (
      <span style={{
        display: 'inline-flex', alignItems: 'center', gap: 5, padding: '2px 8px',
        borderRadius: 3, fontSize: 11, fontWeight: 700, letterSpacing: '0.05em',
        textTransform: 'uppercase', color: 'var(--gold-ink)',
        background: 'var(--gold-soft)', border: '1px solid #D8C291', whiteSpace: 'nowrap',
      }}>
        <Icon name="crown" size={11} strokeWidth={2.25} />{children || 'VIP'}
      </span>
    );
  }
  const p = PILL[kind] || PILL.draft;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, fontWeight: 600, color: p.fg, whiteSpace: 'nowrap' }}>
      <span style={{
        width: 8, height: 8, borderRadius: '50%', flex: 'none',
        background: p.ring ? 'transparent' : p.dot,
        border: p.ring ? ('2px solid ' + p.dot) : 'none', boxSizing: 'border-box',
      }} />
      {children || p.label}
    </span>
  );
}

// Quiet bordered chip for subjects/directions (Python, C++, Вышмат…).
function DirChip({ children }) {
  if (!children) return <span style={{ color: 'var(--text-dim)' }}>—</span>;
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', padding: '2px 9px', borderRadius: 'var(--radius-xs)',
      fontSize: 12.5, fontWeight: 600, background: 'var(--brand-soft)', color: 'var(--brand-ink)',
      border: '1px solid #CBD9F0', whiteSpace: 'nowrap',
    }}>{children}</span>
  );
}

// Avatars = white animal silhouettes (Phosphor fill SVG) on the segment-tone ground.
const AV_COLORS = { navy: 'var(--brand)', green: 'var(--green)', gold: 'var(--gold)' };
function Avatar({ animal = 'paw-print', tone = 'navy', size = 32 }) {
  return (
    <span style={{
      width: size, height: size, borderRadius: 'var(--radius-pill)', flex: 'none',
      background: AV_COLORS[tone], display: 'inline-flex',
      alignItems: 'center', justifyContent: 'center',
    }}>
      <img src={'../../assets/fill/' + animal + '-fill.svg'} alt="" style={{ width: Math.round(size * 0.6), height: Math.round(size * 0.6), filter: 'brightness(0) invert(1)' }} />
    </span>
  );
}

function Card({ children, style, className, pad = 22, onClick, hover }) {
  const [h, setH] = React.useState(false);
  return (
    <div
      className={className}
      onClick={onClick}
      onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
      style={{
        background: 'var(--bg-card)', border: '1px solid ' + (hover && h ? 'var(--border-strong)' : 'var(--border)'),
        borderRadius: 'var(--radius-lg)', padding: pad,
        boxShadow: hover && h ? 'var(--shadow-md)' : 'var(--shadow-sm)',
        cursor: onClick ? 'pointer' : 'default',
        transition: 'box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease)',
        ...style,
      }}>
      {children}
    </div>
  );
}

// Анимация числа 0→value (easeOutCubic). format(v) — как отрисовать целое (₽, %, шт.).
// Уважает prefers-reduced-motion (сразу конечное значение). RAF в фоновой вкладке throttl-ится — норм.
function CountUp({ to, duration = 900, format }) {
  const target = Number(to) || 0;
  const [n, setN] = React.useState(target);
  React.useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce || duration <= 0) { setN(target); return; }
    let raf, start = null;
    const step = (t) => {
      if (start == null) start = t;
      const p = Math.min(1, (t - start) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      setN(target * eased);
      if (p < 1) raf = requestAnimationFrame(step); else setN(target);
    };
    setN(0);
    raf = requestAnimationFrame(step);
    return () => { if (raf) cancelAnimationFrame(raf); };
  }, [target, duration]);
  const v = Math.round(n);
  return <React.Fragment>{format ? format(v) : v}</React.Fragment>;
}

function Money({ value, style }) {
  const formatted = new Intl.NumberFormat('ru-RU').format(value);
  return <span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600, fontVariantNumeric: 'tabular-nums', ...style }}>{formatted}&nbsp;₽</span>;
}

function Overline({ children, style }) {
  return <div style={{ fontSize: 13, fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--text-dim)', ...style }}>{children}</div>;
}

function Input({ label, value, onChange, placeholder, hint, error, prefix }) {
  const [focus, setFocus] = React.useState(false);
  return (
    <div>
      {label && <label style={{ display: 'block', fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 6 }}>{label}</label>}
      <div style={{
        display: 'flex', alignItems: 'center', background: 'var(--bg-card)',
        border: '1px solid ' + (error ? 'var(--coral)' : focus ? 'var(--brand)' : 'var(--border)'),
        borderRadius: 'var(--radius-sm)', boxShadow: focus && !error ? 'var(--shadow-focus)' : 'none',
        transition: 'border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease)',
      }}>
        {prefix && <span style={{ paddingLeft: 12, color: 'var(--text-dim)', display: 'inline-flex' }}><Icon name={prefix} size={16} /></span>}
        <input
          value={value} onChange={e => onChange && onChange(e.target.value)} placeholder={placeholder}
          onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
          style={{
            flex: 1, width: '100%', boxSizing: 'border-box', border: 'none', outline: 'none',
            background: 'transparent', fontFamily: 'var(--font-sans)', fontSize: 14,
            color: 'var(--text)', padding: '10px 12px',
          }} />
      </div>
      {hint && !error && <div style={{ fontSize: 12, color: 'var(--text-dim)', marginTop: 6 }}>{hint}</div>}
      {error && <div style={{ fontSize: 12, color: 'var(--coral)', marginTop: 6 }}>{error}</div>}
    </div>
  );
}

// Balance — mono, green when positive, dim at zero.
function Balance({ value }) {
  const formatted = new Intl.NumberFormat('ru-RU').format(value);
  const color = value > 0 ? 'var(--green)' : value < 0 ? 'var(--coral)' : 'var(--text-dim)';
  const weight = value !== 0 ? 600 : 500;
  return <span style={{ fontFamily: 'var(--font-mono)', fontWeight: weight, fontVariantNumeric: 'tabular-nums', color }}>{formatted}&nbsp;₽</span>;
}

// Dropdown filter — styled trigger + popover list. The menu is rendered with
// position:fixed (computed from the trigger rect) so it escapes table/overflow
// clipping and sits above everything. Search auto-enables for longer lists.
function Dropdown({ value, options, onChange, placeholder, width, searchable, multi }) {
  const [open, setOpen] = React.useState(false);
  const [q, setQ] = React.useState('');
  const [rect, setRect] = React.useState(null);
  const ref = React.useRef(null);
  const menuRef = React.useRef(null);
  const withSearch = searchable != null ? searchable : options.length > 5;

  const place = React.useCallback(() => {
    if (ref.current) setRect(ref.current.getBoundingClientRect());
  }, []);
  React.useEffect(() => {
    if (!open) return;
    place();
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && menuRef.current && !menuRef.current.contains(e.target)) setOpen(false); };
    const onScroll = () => setOpen(false);
    document.addEventListener('mousedown', onDoc);
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onScroll);
    return () => { document.removeEventListener('mousedown', onDoc); window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', onScroll); };
  }, [open, place]);
  React.useEffect(() => { if (!open) setQ(''); }, [open]);

  const sel = multi ? (Array.isArray(value) ? value : []) : null;
  const label = multi
    ? (sel.length === 0 ? placeholder : sel.length === 1 ? ((options.find(o => o.value === sel[0]) || {}).label || placeholder) : `${placeholder}: ${sel.length}`)
    : (value == null ? placeholder : (options.find(o => o.value === value) || {}).label || placeholder);
  const isDefault = multi ? sel.length === 0 : value == null;
  // В мультирежиме не показываем псевдо-опцию «Все …» (value:null) — пустой выбор = все.
  const baseOpts = multi ? options.filter(o => o.value != null) : options;
  const filtered = withSearch && q ? baseOpts.filter(o => o.label.toLowerCase().includes(q.trim().toLowerCase())) : baseOpts;
  const filteredVals = filtered.map(o => o.value);
  const allFilteredOn = multi && filteredVals.length > 0 && filteredVals.every(v => sel.includes(v));
  const toggleMulti = (v) => { const set = sel.includes(v) ? sel.filter(x => x !== v) : [...sel, v]; onChange(set); };
  const selectFiltered = () => onChange(Array.from(new Set([...sel, ...filteredVals])));
  const clearFiltered = () => onChange(sel.filter(v => !filteredVals.includes(v)));

  const menuW = rect ? Math.max(rect.width, 200) : 200;
  const belowRoom = rect ? window.innerHeight - rect.bottom : 999;
  const flipUp = belowRoom < 280 && rect && rect.top > belowRoom;
  const left = rect ? Math.min(rect.left, window.innerWidth - menuW - 8) : 0;

  return (
    <div ref={ref} style={{ position: 'relative', width }}>
      <button type="button" onClick={() => setOpen(o => !o)}
        onMouseEnter={e => { if (!open) e.currentTarget.style.borderColor = 'var(--border-strong)'; e.currentTarget.style.background = 'var(--bg-soft)'; }}
        onMouseLeave={e => { if (!open) e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'var(--bg-card)'; }}
        style={{
        display: 'inline-flex', alignItems: 'center', gap: 8, width: width ? '100%' : 'auto',
        justifyContent: 'space-between', padding: '8px 12px', cursor: 'pointer',
        background: 'var(--bg-card)', border: '1px solid ' + (open ? 'var(--brand)' : 'var(--border)'),
        borderRadius: 'var(--radius-sm)', boxShadow: open ? 'var(--shadow-focus)' : 'none',
        fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 500,
        color: isDefault ? 'var(--text-secondary)' : 'var(--text)', whiteSpace: 'nowrap',
        transition: 'border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease)',
      }}>
        <span style={{ flex: 1, minWidth: 0, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span>
        <Icon name="chevron-down" size={15} style={{ color: 'var(--text-dim)', flex: 'none' }} />
      </button>
      {open && rect && ReactDOM.createPortal(
        <div ref={menuRef} className="om-fade-in" style={{
          position: 'fixed', left, width: menuW, zIndex: 9999,
          top: flipUp ? undefined : rect.bottom + 4, bottom: flipUp ? (window.innerHeight - rect.top + 4) : undefined,
          background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
          boxShadow: 'var(--shadow-lg)', padding: 4, display: 'flex', flexDirection: 'column', maxHeight: 300,
        }}>
          {withSearch && (
            <div style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '7px 9px', margin: 4, borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)' }}>
              <Icon name="search" size={14} style={{ color: 'var(--text-dim)' }} />
              <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder="Поиск…" style={{ border: 'none', outline: 'none', background: 'transparent', flex: 1, minWidth: 0, fontFamily: 'var(--font-sans)', fontSize: 13, color: 'var(--text)' }} />
            </div>
          )}
          {multi && searchable && (
            <div style={{ display: 'flex', gap: 6, padding: '2px 6px 6px' }}>
              <button type="button" onClick={selectFiltered} disabled={allFilteredOn || filteredVals.length === 0} style={{ flex: 1, padding: '6px 8px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: (allFilteredOn || !filteredVals.length) ? 'default' : 'pointer', fontFamily: 'var(--font-sans)', fontSize: 11.5, fontWeight: 600, color: (allFilteredOn || !filteredVals.length) ? 'var(--text-dim)' : 'var(--text-secondary)', opacity: (allFilteredOn || !filteredVals.length) ? 0.55 : 1 }}>Выбрать найденных ({filteredVals.length})</button>
              <button type="button" onClick={clearFiltered} disabled={!filteredVals.some(v => sel.includes(v))} style={{ flex: 1, padding: '6px 8px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: filteredVals.some(v => sel.includes(v)) ? 'pointer' : 'default', fontFamily: 'var(--font-sans)', fontSize: 11.5, fontWeight: 600, color: filteredVals.some(v => sel.includes(v)) ? 'var(--text-secondary)' : 'var(--text-dim)', opacity: filteredVals.some(v => sel.includes(v)) ? 1 : 0.55 }}>Снять найденных</button>
            </div>
          )}
          <div style={{ overflowY: 'auto', flex: 1, minHeight: 0 }}>
            {filtered.map(o => {
              const on = multi ? sel.includes(o.value) : o.value === value;
              return (
                <button key={String(o.value)} type="button" onClick={() => { if (multi) { toggleMulti(o.value); } else { onChange(o.value); setOpen(false); } }} style={{
                  display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left',
                  padding: '8px 10px', border: 'none', cursor: 'pointer', borderRadius: 'var(--radius-sm)',
                  background: on ? 'var(--brand-soft)' : 'transparent',
                  color: on ? 'var(--brand-ink)' : 'var(--text)',
                  fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: on ? 600 : 500, whiteSpace: 'nowrap',
                }}
                onMouseEnter={e => { if (!on) e.currentTarget.style.background = 'var(--bg-soft)'; }}
                onMouseLeave={e => { if (!on) e.currentTarget.style.background = 'transparent'; }}>
                  {multi && <span style={{ width: 16, height: 16, borderRadius: 4, flex: 'none', border: '1.5px solid ' + (on ? 'var(--brand)' : 'var(--border-strong)'), background: on ? 'var(--brand)' : 'transparent', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{on && <Icon name="check" size={11} strokeWidth={3} style={{ color: '#fff' }} />}</span>}
                  <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>{o.label}</span>
                  {!multi && on && <Icon name="check" size={15} strokeWidth={2.5} />}
                </button>
              );
            })}
            {filtered.length === 0 && <div style={{ padding: '10px', fontSize: 13, color: 'var(--text-dim)' }}>Ничего не найдено</div>}
          </div>
        </div>,
        document.body
      )}
    </div>
  );
}

// Picker trigger — looks like a dropdown, but opens a left-sliding SlidePicker.
function PickerTrigger({ label, placeholder, onClick, width }) {
  const isDefault = !label;
  return (
    <button type="button" onClick={onClick} style={{
      display: 'inline-flex', alignItems: 'center', gap: 8, width: width || '100%',
      justifyContent: 'space-between', padding: '10px 13px', cursor: 'pointer',
      background: 'var(--bg-card)', border: '1px solid var(--border)',
      borderRadius: 'var(--radius-sm)', fontFamily: 'var(--font-sans)', fontSize: 14, fontWeight: 500,
      color: isDefault ? 'var(--text-secondary)' : 'var(--text)', whiteSpace: 'nowrap',
      transition: 'border-color var(--dur-fast) var(--ease)',
    }}>
      <span style={{ flex: 1, minWidth: 0, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label || placeholder}</span>
      <Icon name="chevron-right" size={15} style={{ color: 'var(--text-dim)', flex: 'none' }} />
    </button>
  );
}

// SlidePicker — fixed-size panel that slides out to the LEFT of a modal, with
// search + scroll, selected check, and optional free-text ("Другое — своё").
// Render as a sibling of the modal card inside a position:relative wrapper.
function SlidePicker({ cfg, onClose, closeReq }) {
  const [shown, setShown] = React.useState(false);
  const [qInput, setQInput] = React.useState('');
  const [q, setQ] = React.useState('');
  const [custom, setCustom] = React.useState('');
  const [customMode, setCustomMode] = React.useState(false);
  const panelRef = React.useRef(null);
  React.useEffect(() => {
    setShown(false); setQInput(''); setQ(''); setCustom(''); setCustomMode(false);
    // Force the off-screen start frame to paint before flipping, so the slide-in actually animates.
    let raf1, raf2;
    if (panelRef.current) void panelRef.current.offsetWidth; // force reflow
    raf1 = requestAnimationFrame(() => { raf2 = requestAnimationFrame(() => setShown(true)); });
    return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); };
  }, [cfg]);
  React.useEffect(() => { const t = setTimeout(() => setQ(qInput), 200); return () => clearTimeout(t); }, [qInput]);
  const close = () => { setShown(false); setTimeout(onClose, 280); };
  React.useEffect(() => { if (closeReq) close(); }, [closeReq]);
  React.useEffect(() => {
    if (!cfg) return;
    const onDown = (e) => { if (panelRef.current && !panelRef.current.contains(e.target)) close(); };
    document.addEventListener('mousedown', onDown, true);
    return () => document.removeEventListener('mousedown', onDown, true);
  }, [cfg]);
  if (!cfg) return null;
  const pick = (v) => { cfg.onPick(v); close(); };
  const options = cfg.options || [];
  const filtered = options.filter(o => o.label.toLowerCase().includes(q.trim().toLowerCase()));
  const isOther = (o) => cfg.allowCustom && (o.value === 'other' || o.label === 'Другое');

  return (
    <div ref={panelRef} style={{
      position: 'absolute', top: 0, bottom: 0, right: 'calc(100% + 12px)', width: 300, zIndex: 0,
      background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)',
      boxShadow: 'var(--shadow-lg)', display: 'flex', flexDirection: 'column', overflow: 'hidden', willChange: 'transform',
      transform: shown ? 'translateX(0)' : 'translateX(340px)', opacity: 1,
      transition: shown
        ? 'transform 380ms cubic-bezier(0.16, 1, 0.3, 1)'
        : 'transform 280ms cubic-bezier(0.4, 0, 0.6, 1)',
    }}>
      <div style={{ padding: '14px 16px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 10 }}>
        {customMode && (
          <button onClick={() => setCustomMode(false)} title="Назад" style={{ width: 30, height: 30, borderRadius: 6, border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="M14 6l-6 6 6 6"/></svg>
          </button>
        )}
        <h3 style={{ flex: 1, fontSize: 15, fontWeight: 700, margin: 0, color: 'var(--text)' }}>{customMode ? 'Своё значение' : cfg.title}</h3>
        <button onClick={close} title="Закрыть" style={{ width: 30, height: 30, borderRadius: 6, border: '1px solid var(--border)', background: 'var(--bg-card)', cursor: 'pointer', color: 'var(--text-secondary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round"><path d="M9.2 9.2l5.6 5.6M14.8 9.2l-5.6 5.6"/></svg>
        </button>
      </div>

      {customMode ? (
        <div className="om-fade-in" style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <div style={{ fontSize: 12.5, color: 'var(--text-dim)' }}>{cfg.customPlaceholder || 'Введите своё значение'}</div>
          <input autoFocus value={custom} onChange={e => setCustom(e.target.value)} placeholder={cfg.customPlaceholder || 'Введите вручную'}
            onKeyDown={e => { if (e.key === 'Enter' && custom.trim()) pick(custom.trim()); }}
            style={{ width: '100%', boxSizing: 'border-box', border: '1px solid var(--brand)', boxShadow: 'var(--shadow-focus)', borderRadius: 'var(--radius-sm)', padding: '10px 12px', fontFamily: 'var(--font-sans)', fontSize: 14, color: 'var(--text)', outline: 'none', background: 'var(--bg-card)' }} />
          <button onClick={() => custom.trim() && pick(custom.trim())} style={{ padding: '10px 12px', borderRadius: 'var(--radius-sm)', border: 'none', background: 'var(--brand)', color: '#fff', fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: 600, cursor: 'pointer' }}>Применить</button>
        </div>
      ) : (
        <React.Fragment>
          <div style={{ padding: '12px 14px 8px' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', padding: '8px 11px' }}>
              <Icon name="search" size={15} style={{ color: 'var(--text-dim)' }} />
              <input autoFocus value={qInput} onChange={e => setQInput(e.target.value)} placeholder="Поиск…" style={{ border: 'none', outline: 'none', background: 'transparent', flex: 1, fontFamily: 'var(--font-sans)', fontSize: 13.5, color: 'var(--text)' }} />
            </div>
          </div>
          <div key={q} className="om-fade-in" style={{ flex: 1, overflowY: 'auto', padding: '4px 8px 8px' }}>
            {cfg.note && <div style={{ display: 'flex', gap: 7, margin: '4px 6px 8px', padding: '8px 10px', borderRadius: 'var(--radius-sm)', background: 'var(--bg-soft)', border: '1px solid var(--border)' }}><Icon name="info" size={13} style={{ color: 'var(--text-dim)', flex: 'none', marginTop: 1 }} /><span style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.4 }}>{cfg.note}</span></div>}
            {filtered.map(o => {
              const on = o.value === cfg.value;
              const other = isOther(o);
              if (o.disabled) {
                return (
                  <div key={String(o.value)} title={o.disabledReason || 'Недоступно'} style={{
                    display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left',
                    padding: '9px 10px', borderRadius: 'var(--radius-sm)', background: 'transparent', opacity: 0.55, cursor: 'not-allowed',
                  }}>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 13.5, fontWeight: 500, color: 'var(--text-dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{o.label}</div>
                      {o.sub && <div style={{ fontSize: 10.5, color: 'var(--text-dim)', marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{o.sub}</div>}
                    </div>
                    <Icon name="lock" size={13} style={{ color: 'var(--text-dim)', flex: 'none' }} />
                  </div>
                );
              }
              return (
                <button key={String(o.value)} onClick={() => other ? setCustomMode(true) : pick(o.value)} style={{
                  display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left',
                  padding: '9px 10px', border: 'none', cursor: 'pointer', borderRadius: 'var(--radius-sm)',
                  background: on ? 'var(--brand-soft)' : 'transparent', color: on ? 'var(--brand-ink)' : 'var(--text)',
                  fontFamily: 'var(--font-sans)', fontSize: 13.5, fontWeight: on ? 600 : 500,
                }}
                onMouseEnter={e => { if (!on) e.currentTarget.style.background = 'var(--bg-soft)'; }}
                onMouseLeave={e => { if (!on) e.currentTarget.style.background = 'transparent'; }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{o.label}</div>
                    {o.sub && <div style={{ fontSize: 10.5, fontWeight: 500, color: 'var(--text-dim)', marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{o.sub}</div>}
                  </div>
                  {other ? <Icon name="chevron-right" size={15} style={{ color: 'var(--text-dim)' }} /> : (on && <Icon name="check" size={15} strokeWidth={2.5} />)}
                </button>
              );
            })}
            {filtered.length === 0 && <div style={{ padding: '12px 10px', fontSize: 13, color: 'var(--text-dim)' }}>Ничего не найдено</div>}
          </div>
        </React.Fragment>
      )}
    </div>
  );
}

// Слой для НЕполноэкранных модалок (подтверждения, предупреждения, маленькие диалоги).
// Рисуется ПОРТАЛОМ в <body> с position:fixed → всегда по центру ЭКРАНА, где бы пользователь ни
// прокрутил страницу. Без портала такие диалоги центрировались внутри своей панели (а у панели
// карточки ученика ещё и transform, из-за которого даже fixed липнет к панели, а не к экрану).
function ModalLayer({ z = 95, pad = 20, children }) {
  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', inset: 0, zIndex: z, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: pad }}>
      {children}
    </div>,
    document.body
  );
}

Object.assign(window, { Icon, Button, Pill, DirChip, Avatar, Card, Money, Balance, Dropdown, PickerTrigger, SlidePicker, Overline, Input, PILL, CountUp, ModalLayer });
