/* ctor-notes.jsx — заметки проекта (project-node-threads): карточки, вложения, lightbox, секция инспектора */
const { useState: nUseState, useRef: nUseRef, useEffect: nUseEffect } = React;
const nCD = window.ConstructorData;

const NOTE_VIS = [
  { k: 'client', l: 'Клиент' }, { k: 'manager', l: 'Менеджер' }, { k: 'foreman', l: 'Прораб' },
  { k: 'executor', l: 'Исполнитель' }, { k: 'designer', l: 'Дизайнер' },
];
const ROLE_PRESETS = ['Дизайнер', 'Прораб', 'Менеджер', 'Исполнитель', 'Super-admin', 'Клиент'];
const ROLE_COLOR = { 'Дизайнер': '#d05858', 'Прораб': '#1a1714', 'Менеджер': '#5aad6e', 'Исполнитель': '#a855f7', 'Super-admin': '#e8793a', 'Клиент': '#e8793a' };

function initials(name) { return (name || '?').split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase(); }

/* ---- Badge 💬 N ---- */
function NoteBadge({ n, dark, onClick }) {
  if (!n) return null;
  return (
    <span onClick={onClick} title={n + ' сообщений — открыть'} style={{
      display: 'inline-flex', alignItems: 'center', gap: 4, padding: '1px 7px 1px 6px', borderRadius: 980,
      background: dark ? 'rgba(232,121,58,.18)' : '#fff0e6', color: dark ? '#7fb4ff' : '#e8793a',
      fontSize: 11, fontWeight: 700, fontVariantNumeric: 'tabular-nums', flexShrink: 0,
      cursor: onClick ? 'pointer' : 'default', transition: 'filter .12s',
    }}
    onMouseEnter={onClick ? (e)=>{ e.currentTarget.style.filter='brightness(.94)'; } : undefined}
    onMouseLeave={onClick ? (e)=>{ e.currentTarget.style.filter='none'; } : undefined}>
      <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
      {n}
    </span>
  );
}

/* ---- Avatar ---- */
function NAvatar({ name, color, size = 30 }) {
  return <span style={{ width: size, height: size, borderRadius: '50%', background: color || '#8a817a', color: '#fff', fontSize: size * 0.36, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>{initials(name)}</span>;
}

/* ---- Attachment thumb / chip ---- */
function Attachment({ att, onOpen, dark }) {
  if (att.kind === 'pdf') {
    return (
      <button onClick={() => onOpen && onOpen(att)} style={{
        display: 'inline-flex', alignItems: 'center', gap: 9, padding: '8px 12px', borderRadius: 10,
        border: '.5px solid ' + (dark ? 'rgba(255,255,255,.14)' : '#e4ddd2'), background: dark ? 'rgba(255,255,255,.04)' : '#fff',
        cursor: 'pointer', fontFamily: 'inherit', maxWidth: 240,
      }}>
        <span style={{ width: 26, height: 30, borderRadius: 5, background: (att.tint || '#d05858') + '1f', color: att.tint || '#d05858', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
        </span>
        <span style={{ minWidth: 0 }}>
          <span style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: dark ? '#e8e6e3' : '#1a1714', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{att.name}</span>
          <span style={{ fontSize: 11, color: dark ? '#8a8784' : '#8a817a' }}>PDF · открыть</span>
        </span>
      </button>
    );
  }
  return (
    <button onClick={() => onOpen && onOpen(att)} title={att.name} style={{
      width: 120, height: 88, borderRadius: 10, overflow: 'hidden', border: '.5px solid ' + (dark ? 'rgba(255,255,255,.12)' : '#e4ddd2'),
      cursor: 'pointer', padding: 0, position: 'relative', flexShrink: 0,
      background: att.url ? '#000' : `linear-gradient(135deg, ${att.tint || '#8b5cf6'}, ${att.tint || '#8b5cf6'}bb)`,
    }}>
      {att.url
        ? <img src={att.url} alt={att.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
        : <span style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'rgba(255,255,255,.85)' }}>
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="M21 15l-5-5L5 21"></path></svg>
          </span>}
      <span style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '10px 8px 5px', fontSize: 10.5, fontWeight: 600, color: '#fff', textAlign: 'left', background: 'linear-gradient(transparent, rgba(0,0,0,.6))', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{att.name}</span>
    </button>
  );
}
function AttachmentRow({ attachments, onOpen, dark }) {
  if (!attachments || !attachments.length) return null;
  return <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 10 }}>{attachments.map((a) => <Attachment key={a.id} att={a} onOpen={onOpen} dark={dark} />)}</div>;
}

/* ---- Lightbox (shared) ---- */
function Lightbox({ items, index, onClose, onNav }) {
  nUseEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); if (e.key === 'ArrowRight') onNav(1); if (e.key === 'ArrowLeft') onNav(-1); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose, onNav]);
  const it = items[index];
  if (!it) return null;
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.82)', backdropFilter: 'blur(4px)', zIndex: 400, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 16 }} className="ctor-fade">
      <button onClick={onClose} style={{ position: 'absolute', top: 22, right: 26, width: 40, height: 40, borderRadius: '50%', border: 'none', background: 'rgba(255,255,255,.12)', color: '#fff', cursor: 'pointer', fontSize: 18, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>✕</button>
      {items.length > 1 && <button onClick={(e) => { e.stopPropagation(); onNav(-1); }} style={lbNav('left')}>‹</button>}
      <div onClick={(e) => e.stopPropagation()} style={{ maxWidth: '82vw', maxHeight: '78vh', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14 }}>
        {it.url
          ? <img src={it.url} alt={it.name} style={{ maxWidth: '82vw', maxHeight: '70vh', borderRadius: 12, boxShadow: '0 24px 64px rgba(0,0,0,.5)' }} />
          : <div style={{ width: 'min(720px, 82vw)', height: 'min(460px, 60vh)', borderRadius: 14, background: `linear-gradient(135deg, ${it.tint || '#8b5cf6'}, ${it.tint || '#8b5cf6'}aa)`, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'rgba(255,255,255,.9)', boxShadow: '0 24px 64px rgba(0,0,0,.5)' }}>
              <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="M21 15l-5-5L5 21"></path></svg>
            </div>}
        <div style={{ color: '#fff', fontSize: 14, fontWeight: 500 }}>{it.name}{items.length > 1 ? `  ·  ${index + 1} / ${items.length}` : ''}</div>
      </div>
      {items.length > 1 && <button onClick={(e) => { e.stopPropagation(); onNav(1); }} style={lbNav('right')}>›</button>}
    </div>
  );
}
function lbNav(side) {
  return { position: 'absolute', [side]: 26, top: '50%', transform: 'translateY(-50%)', width: 48, height: 48, borderRadius: '50%', border: 'none', background: 'rgba(255,255,255,.12)', color: '#fff', cursor: 'pointer', fontSize: 26, lineHeight: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' };
}
/* хук для управления lightbox */
function useLightbox() {
  const [state, setState] = nUseState(null); // { items, index }
  const open = (items, index) => setState({ items: items.filter((a) => a.kind !== 'pdf'), index: Math.max(0, items.filter((a) => a.kind !== 'pdf').findIndex((x) => x === items[index])) });
  const openAtt = (attachments, att) => {
    if (att.kind === 'pdf') { window.alert('PDF «' + att.name + '» — в проде откроется в новой вкладке.'); return; }
    const imgs = attachments.filter((a) => a.kind === 'image');
    setState({ items: imgs, index: Math.max(0, imgs.findIndex((x) => x.id === att.id)) });
  };
  const node = state ? <Lightbox items={state.items} index={state.index} onClose={() => setState(null)} onNav={(d) => setState((s) => ({ ...s, index: (s.index + d + s.items.length) % s.items.length }))} /> : null;
  return { openAtt, node };
}

/* ---- Visibility chips (per-note) ---- */
function NoteVisibilityChips({ value, onChange, readOnly, dark }) {
  const v = value || {};
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
      <span style={{ fontSize: 11, fontWeight: 600, color: dark ? '#8a8784' : '#a89e92', textTransform: 'uppercase', letterSpacing: '.04em', marginRight: 2 }}>видят</span>
      {NOTE_VIS.map((r) => {
        const on = v[r.k] !== false;
        return (
          <button key={r.k} disabled={readOnly} onClick={() => !readOnly && onChange({ ...v, [r.k]: !on })} style={{
            padding: '3px 9px', borderRadius: 980, border: '.5px solid ' + (on ? '#e8793a40' : (dark ? 'rgba(255,255,255,.12)' : '#e4ddd2')),
            background: on ? (dark ? 'rgba(232,121,58,.18)' : '#fff0e6') : (dark ? 'rgba(255,255,255,.04)' : '#f0ece5'),
            color: on ? (dark ? '#7fb4ff' : '#e8793a') : (dark ? '#6b6864' : '#a89e92'),
            fontSize: 11.5, fontWeight: 600, cursor: readOnly ? 'default' : 'pointer', fontFamily: 'inherit',
            textDecoration: on ? 'none' : 'line-through', opacity: readOnly && !on ? 0.6 : 1,
          }}>{r.l}</button>
        );
      })}
    </div>
  );
}

/* ---- Reply ---- */
function ReplyItem({ reply, dark }) {
  return (
    <div style={{ display: 'flex', gap: 10, padding: '8px 0' }}>
      <NAvatar name={reply.author} color={reply.color} size={26} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 7, flexWrap: 'wrap' }}>
          <span style={{ fontSize: 13, fontWeight: 600, color: dark ? '#e8e6e3' : '#1a1714' }}>{reply.author}</span>
          <span style={{ fontSize: 11.5, color: dark ? '#8a8784' : '#8a817a' }}>{reply.role} · {reply.ts}</span>
        </div>
        <div style={{ fontSize: 13.5, color: dark ? '#c5c2bd' : '#6b6259', marginTop: 2, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{reply.text}</div>
        <AttachmentRow attachments={reply.attachments} dark={dark} />
      </div>
    </div>
  );
}
function ReplyForm({ onSend, dark }) {
  const [text, setText] = nUseState('');
  const send = () => { const v = text.trim(); if (!v) return; onSend(v); setText(''); };
  return (
    <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', marginTop: 8 }}>
      <textarea value={text} onChange={(e) => setText(e.target.value)} rows={1} placeholder="Ответить в треде…"
        onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
        style={{ flex: 1, resize: 'none', fontFamily: 'inherit', fontSize: 13.5, lineHeight: 1.45, padding: '9px 12px', borderRadius: 10, minHeight: 38,
          border: '.5px solid ' + (dark ? 'rgba(255,255,255,.14)' : '#e4ddd2'), background: dark ? 'rgba(255,255,255,.05)' : '#fff', color: dark ? '#e8e6e3' : '#1a1714', outline: 'none' }} />
      <button onClick={send} disabled={!text.trim()} title="Отправить (Enter)" style={{
        width: 38, height: 38, borderRadius: 10, border: 'none', flexShrink: 0, cursor: text.trim() ? 'pointer' : 'not-allowed',
        background: text.trim() ? '#e8793a' : (dark ? 'rgba(255,255,255,.08)' : '#e4ddd2'), color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
      </button>
    </div>
  );
}

/* ---- NoteCard (root note) ---- */
function NoteCard({ note, onPatch, onDelete, onReply, onOpenAtt, editable, dark }) {
  const [editing, setEditing] = nUseState(false);
  const [draftText, setDraftText] = nUseState(note.text);
  const [showAllReplies, setShowAllReplies] = nUseState(false);
  const replies = note.replies || [];
  const shown = showAllReplies ? replies : replies.slice(0, 5);

  const cardBg = dark ? 'rgba(255,255,255,.035)' : '#fff';
  const border = dark ? '.5px solid rgba(255,255,255,.09)' : '.5px solid #ece5da';
  return (
    <div style={{ background: cardBg, border, borderRadius: 14, padding: '16px 18px' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 11 }}>
        <NAvatar name={note.author} color={note.color} size={34} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
            <span style={{ fontSize: 14.5, fontWeight: 600, color: dark ? '#f0eeeb' : '#1a1714' }}>{note.author}</span>
            <span style={{ fontSize: 12, color: dark ? '#8a8784' : '#8a817a' }}>{note.role} · {note.ts}</span>
          </div>
        </div>
        {editable && (
          <div style={{ display: 'flex', gap: 2, flexShrink: 0 }}>
            <button onClick={() => { setEditing((e) => !e); setDraftText(note.text); }} title="Редактировать" style={{ ...ctorStyles.iconBtnSm, color: dark ? '#8a8784' : '#bcb3a7' }}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
            </button>
            <button onClick={() => { if (confirm('Удалить заметку и все ответы?')) onDelete(); }} title="Удалить" style={{ ...ctorStyles.iconBtnSm, color: dark ? '#8a8784' : '#bcb3a7' }}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"></path></svg>
            </button>
          </div>
        )}
      </div>

      {editing
        ? <div style={{ marginTop: 10 }}>
            <textarea value={draftText} onChange={(e) => setDraftText(e.target.value)} rows={4} autoFocus
              style={{ width: '100%', resize: 'vertical', fontFamily: 'inherit', fontSize: 13.5, lineHeight: 1.5, padding: '10px 12px', borderRadius: 10, border: '.5px solid #d4d8e0', background: '#f5f9ff', color: '#1a1714', outline: 'none' }} />
            <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
              <button onClick={() => { onPatch({ text: draftText }); setEditing(false); }} style={{ ...ctorStyles.btnPrimary, padding: '8px 16px', fontSize: 13 }}>Сохранить</button>
              <button onClick={() => setEditing(false)} style={{ ...ctorStyles.btnGhost, padding: '8px 14px' }}>Отмена</button>
            </div>
          </div>
        : <div style={{ fontSize: 14, color: dark ? '#d4d1cc' : '#1a1714', marginTop: 8, lineHeight: 1.55, whiteSpace: 'pre-wrap' }}>{note.text}</div>}

      <AttachmentRow attachments={note.attachments} onOpen={(a) => onOpenAtt(note.attachments, a)} dark={dark} />

      {editable && (
        <div style={{ marginTop: 12 }}>
          <NoteVisibilityChips value={note.visibility} onChange={(v) => onPatch({ visibility: v })} dark={dark} />
        </div>
      )}

      {(replies.length > 0 || onReply) && (
        <div style={{ marginTop: 14, paddingLeft: 14, borderLeft: '2px solid ' + (dark ? 'rgba(255,255,255,.1)' : '#f0ece5') }}>
          {shown.map((r) => <ReplyItem key={r.id} reply={r} dark={dark} />)}
          {replies.length > 5 && !showAllReplies && (
            <button onClick={() => setShowAllReplies(true)} style={{ border: 'none', background: 'transparent', color: '#e8793a', fontSize: 12.5, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', padding: '4px 0' }}>
              Показать ещё {replies.length - 5}
            </button>
          )}
          {onReply && <ReplyForm onSend={onReply} dark={dark} />}
        </div>
      )}
    </div>
  );
}

/* ---- NewNoteForm (конструктор) ---- */
function NewNoteForm({ onAdd }) {
  const [author, setAuthor] = nUseState('');
  const [role, setRole] = nUseState('Дизайнер');
  const [text, setText] = nUseState('');
  const [atts, setAtts] = nUseState([]);
  const fileRef = nUseRef(null);

  const onFiles = (files) => {
    [...files].slice(0, 4 - atts.length).forEach((f) => {
      const isImg = /image\//.test(f.type);
      if (isImg) {
        const reader = new FileReader();
        reader.onload = () => setAtts((a) => [...a, { id: 'u' + Math.random().toString(36).slice(2, 7), kind: 'image', name: f.name, url: reader.result }]);
        reader.readAsDataURL(f);
      } else {
        setAtts((a) => [...a, { id: 'u' + Math.random().toString(36).slice(2, 7), kind: 'pdf', name: f.name, tint: '#d05858' }]);
      }
    });
  };
  const submit = () => {
    if (!text.trim()) return;
    onAdd({ author: author.trim() || 'Super-admin', role, color: ROLE_COLOR[role] || '#e8793a', text: text.trim(), attachments: atts, ts: 'сейчас' });
    setAuthor(''); setText(''); setAtts([]); setRole('Дизайнер');
  };

  return (
    <div style={{ border: '1px dashed #f3c39b', borderRadius: 14, padding: 16, background: '#fbfcff' }}>
      <div style={{ display: 'flex', gap: 10, marginBottom: 10 }}>
        <input value={author} onChange={(e) => setAuthor(e.target.value)} placeholder="Автор"
          style={{ ...ctorStyles.input, flex: 1, background: '#fff' }} />
        <select value={role} onChange={(e) => setRole(e.target.value)} style={{ ...ctorStyles.input, width: 150, background: '#fff', cursor: 'pointer', appearance: 'auto' }}>
          {ROLE_PRESETS.map((r) => <option key={r} value={r}>{r}</option>)}
        </select>
      </div>
      <textarea value={text} onChange={(e) => setText(e.target.value)} rows={3} placeholder="Текст инструкции: технология, материалы, важные узлы…"
        style={{ ...ctorStyles.input, background: '#fff', resize: 'vertical', lineHeight: 1.5, marginBottom: 10 }} />
      {atts.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 10 }}>
          {atts.map((a) => (
            <span key={a.id} style={{ position: 'relative' }}>
              <Attachment att={a} />
              <button onClick={() => setAtts((x) => x.filter((y) => y.id !== a.id))} style={{ position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', border: '1.5px solid #fff', background: '#1a1714', color: '#fff', cursor: 'pointer', fontSize: 11, lineHeight: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2 }}>✕</button>
            </span>
          ))}
        </div>
      )}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, justifyContent: 'space-between', flexWrap: 'wrap' }}>
        <button onClick={() => fileRef.current && fileRef.current.click()} disabled={atts.length >= 4} style={{ ...ctorStyles.btnGhost, opacity: atts.length >= 4 ? 0.5 : 1 }}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>
          Вложить файл
        </button>
        <input ref={fileRef} type="file" accept="image/jpeg,image/png,image/webp,application/pdf" multiple style={{ display: 'none' }} onChange={(e) => { onFiles(e.target.files); e.target.value = ''; }} />
        <button onClick={submit} disabled={!text.trim()} style={{ ...ctorStyles.btnPrimary, opacity: text.trim() ? 1 : 0.4, cursor: text.trim() ? 'pointer' : 'not-allowed' }}>
          {Icon.plus} Добавить заметку
        </button>
      </div>
      <div style={{ fontSize: 11.5, color: '#a89e92', marginTop: 9 }}>До 10 заметок на узел · до 4 вложений · JPEG, PNG, WebP, PDF · ≤ 10 МБ</div>
    </div>
  );
}

/* ---- NotesSection (для инспектора конструктора) ---- */
function NotesSection({ nodeId, notes, setNotes }) {
  const lb = useLightbox();
  const list = (notes[nodeId] || []);
  const setList = (fn) => setNotes((all) => ({ ...all, [nodeId]: fn(all[nodeId] || []) }));

  const addNote = (data) => setList((l) => [...l, nCD.note({ ...data })]);
  const patchNote = (id, patch) => setList((l) => l.map((n) => (n.id === id ? { ...n, ...patch } : n)));
  const deleteNote = (id) => setList((l) => l.filter((n) => n.id !== id));
  const addReply = (id, text) => setList((l) => l.map((n) => (n.id === id ? { ...n, replies: [...(n.replies || []), { id: 'r' + Math.random().toString(36).slice(2, 7), author: 'Super-admin', role: 'Super-admin', color: '#e8793a', text, ts: 'сейчас' }] } : n)));

  return (
    <section style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div>
        <div style={insStyles.secLabel}>Заметки и инструкции</div>
        <div style={{ fontSize: 11.5, color: '#a89e92', marginTop: 5, lineHeight: 1.5 }}>Текст, схемы и фото для участников проекта. Появятся в ЛК после публикации каталога.</div>
      </div>
      {list.map((n) => (
        <NoteCard key={n.id} note={n} editable
          onPatch={(p) => patchNote(n.id, p)} onDelete={() => deleteNote(n.id)} onReply={(t) => addReply(n.id, t)}
          onOpenAtt={lb.openAtt} />
      ))}
      {list.length < 10 && <NewNoteForm onAdd={addNote} />}
      {lb.node}
    </section>
  );
}

Object.assign(window, { NoteBadge, NAvatar, Attachment, AttachmentRow, Lightbox, useLightbox, NoteVisibilityChips, ReplyItem, ReplyForm, NoteCard, NewNoteForm, NotesSection, NOTE_VIS, ROLE_COLOR });

/* ---- NotePopover — всплывающая карточка заметок (по клику на бейдж 💬 в дереве) ---- */
const { useState: popUseState, useEffect: popUseEffect, useRef: popUseRef } = React;

function NotePopover({ anchorRect, nodeId, nodeName, nodePath, nodeColor, notes, setNotes, onClose }) {
  const ref = popUseRef(null);
  const [pos, setPos] = popUseState(null);
  const W = 420;

  // позиционирование относительно бейджа, с учётом краёв экрана
  popUseEffect(() => {
    if (!anchorRect) return;
    const margin = 10;
    let left = anchorRect.left;
    if (left + W > window.innerWidth - margin) left = window.innerWidth - W - margin;
    if (left < margin) left = margin;
    const spaceBelow = window.innerHeight - anchorRect.bottom;
    const maxH = Math.min(520, Math.max(spaceBelow, anchorRect.top) - margin - 12);
    const placeAbove = spaceBelow < 280 && anchorRect.top > spaceBelow;
    setPos({ left, top: placeAbove ? null : anchorRect.bottom + 8, bottom: placeAbove ? (window.innerHeight - anchorRect.top + 8) : null, maxH });
  }, [anchorRect]);

  // закрытие по Esc и клику вне
  popUseEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    document.addEventListener('keydown', onKey);
    document.addEventListener('mousedown', onDown);
    return () => { document.removeEventListener('keydown', onKey); document.removeEventListener('mousedown', onDown); };
  }, [onClose]);

  if (!pos) return null;
  const cnt = (notes[nodeId] || []).length;

  return ReactDOM.createPortal(
    <div ref={ref} style={{
      position: 'fixed', left: pos.left, top: pos.top ?? undefined, bottom: pos.bottom ?? undefined, width: W, zIndex: 400,
      background: '#fff', borderRadius: 18, border: '.5px solid #e4ddd2',
      boxShadow: '0 18px 50px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.06)',
      display: 'flex', flexDirection: 'column', overflow: 'hidden',
      animation: 'popIn .14s cubic-bezier(.32,.72,0,1)',
    }}>
      {/* header */}
      <div style={{ padding: '15px 18px 13px', borderBottom: '.5px solid #f0ece5', display: 'flex', alignItems: 'flex-start', gap: 11 }}>
        <span style={{ width: 9, height: 9, borderRadius: '50%', background: nodeColor || '#8a817a', flexShrink: 0, marginTop: 5 }}></span>
        <div style={{ flex: 1, minWidth: 0 }}>
          {nodePath && <div style={{ fontSize: 11, color: '#a89e92', marginBottom: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{nodePath}</div>}
          <div style={{ fontSize: 15.5, fontWeight: 700, letterSpacing: '-.015em', color: '#1a1714' }}>{nodeName}</div>
          <div style={{ fontSize: 12, color: '#8a817a', marginTop: 1 }}>{cnt} {cnt===1?'заметка':(cnt>=2&&cnt<=4)?'заметки':'заметок'}</div>
        </div>
        <button onClick={onClose} title="Закрыть" style={{ width: 28, height: 28, borderRadius: 8, border: 'none', background: '#f0ece5', color: '#8a817a', cursor: 'pointer', display: 'grid', placeItems: 'center', flexShrink: 0 }}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
        </button>
      </div>
      {/* body — тот же интерфейс что в инспекторе / вкладке */}
      <div style={{ padding: '16px 18px', overflowY: 'auto', maxHeight: pos.maxH }}>
        <NotesSection nodeId={nodeId} notes={notes} setNotes={setNotes} />
      </div>
    </div>,
    document.body
  );
}

Object.assign(window, { NotePopover });

/* ---- NotesPanel — все заметки сайта + создание с выбором узла ---- */
const { useState: npUseState } = React;

function flattenCatalogNodes(catalog) {
  const out = [];
  (catalog || []).forEach(ph => {
    (ph.stages || []).forEach(st => {
      out.push({
        id: st.id, name: st.name, color: st.color || '#8a817a',
        path: ph.name + ' › ' + st.name, type: 'stage',
        selection: { type: 'stage', id: st.id, phaseId: ph.id, stageId: st.id },
      });
      (st.tasks || []).forEach(tk => {
        out.push({
          id: tk.id, name: tk.name, color: st.color || '#8a817a',
          path: ph.name + ' › ' + st.name + ' › ' + tk.name, type: 'task',
          selection: { type: 'task', id: tk.id, phaseId: ph.id, stageId: st.id, taskId: tk.id },
        });
        (tk.sub || []).forEach(su => {
          out.push({
            id: su.id, name: su.name, color: st.color || '#8a817a',
            path: ph.name + ' › ' + st.name + ' › ' + tk.name + ' › ' + su.name, type: 'work',
            selection: { type: 'work', id: su.id, phaseId: ph.id, stageId: st.id, taskId: tk.id, subId: su.id },
          });
        });
      });
    });
  });
  return out;
}
const NODE_TYPE_TAG = { stage: { l:'Этап', c:'#e8793a', bg:'#fff0e6' }, task: { l:'Задача', c:'#7c3aed', bg:'#f3eafe' }, work: { l:'Подзадача', c:'#8a817a', bg:'#f0ece5' } };

function catalogNodeAnchorId(nodeId) {
  return 'ctor-node-' + nodeId;
}

function scrollToCatalogNode(nodeId) {
  if (!nodeId) return;
  const run = () => {
    const el = document.getElementById(catalogNodeAnchorId(nodeId));
    if (el) el.scrollIntoView({ behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'center' });
  };
  requestAnimationFrame(() => setTimeout(run, 60));
}

function NoteNodeLink({ node, onNavigate }) {
  if (!node) return null;
  const canNavigate = !!(node.selection && onNavigate);
  const typeLabel = (NODE_TYPE_TAG[node.type] || {}).l || node.type;
  const targetLabel = node.type === 'stage' ? 'этап в смете' : node.type === 'task' ? 'задачу в смете' : 'подзадачу в смете';

  return (
    <div className="nt-node-ref">
      <span className="nt-node-type">{typeLabel} · </span>
      {canNavigate ? (
        <button
          type="button"
          className="note-node-link"
          onClick={(e) => { e.stopPropagation(); onNavigate(node.selection); }}
          title={'Открыть ' + targetLabel + ': ' + node.path}
          aria-label={'Открыть ' + targetLabel + ': ' + node.path}
        >
          {node.path}
        </button>
      ) : (
        <span>{node.path}</span>
      )}
    </div>
  );
}

/* Меню выбора узла (этап / задача / подзадача) */
function NodePicker({ nodes, value, onChange }) {
  const [open, setOpen] = npUseState(false);
  const [q, setQ] = npUseState('');
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);
  const sel = nodes.find(n => n.id === value);
  const filtered = q ? nodes.filter(n => n.name.toLowerCase().includes(q.toLowerCase()) || n.path.toLowerCase().includes(q.toLowerCase())) : nodes;
  return (
    <div ref={ref} className="nt-picker">
      <button type="button" className={'nt-picker-trigger' + (open ? ' is-open' : '')} onClick={() => setOpen(o => !o)}>
        {sel ? <span className="nt-picker-value">{sel.name}</span> : <span className="nt-picker-placeholder">Выбрать узел каталога</span>}
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg>
      </button>
      {open && (
        <div className="nt-picker-menu">
          <div className="nt-picker-search">
            <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder="Поиск узла" aria-label="Поиск узла" />
          </div>
          <div className="nt-picker-list">
            {filtered.map(n => (
              <button
                key={n.id}
                type="button"
                className={'nt-picker-item' + (n.id === value ? ' on' : '') + (n.type === 'task' ? ' type-task' : n.type === 'work' ? ' type-work' : '')}
                onClick={() => { onChange(n.id); setOpen(false); setQ(''); }}
              >
                <span style={{ flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{n.name}</span>
              </button>
            ))}
            {filtered.length === 0 && <div className="nt-picker-empty">Не найдено</div>}
          </div>
        </div>
      )}
    </div>
  );
}

function NotesPanel({ catalog, notes, setNotes, roles, onNavigateToNode }) {
  const allNodes = flattenCatalogNodes(catalog);
  const nodeById = id => allNodes.find(n => n.id === id);
  const [creating, setCreating] = npUseState(false);
  const [draftNode, setDraftNode] = npUseState(null);
  const [draftBlocks, setDraftBlocks] = npUseState([window.blankNoteBlock()]);
  const [draftLayout, setDraftLayout] = npUseState('cards');
  const [draftAtts, setDraftAtts] = npUseState([]);
  const draftHasContent = (draftBlocks || []).some(b => (b.title && b.title.trim()) || (b.text && b.text.trim()) || b.image);
  const resetDraft = () => { setDraftNode(null); setDraftBlocks([window.blankNoteBlock()]); setDraftLayout('cards'); setDraftAtts([]); };
  const draftFileRef = React.useRef(null);
  const onDraftFiles = (files) => {
    [...files].slice(0, 4 - draftAtts.length).forEach((f) => {
      const isImg = /image\//.test(f.type);
      if (isImg) {
        const reader = new FileReader();
        reader.onload = () => setDraftAtts(a => [...a, { id:'u'+Math.random().toString(36).slice(2,7), kind:'image', name:f.name, url:reader.result }]);
        reader.readAsDataURL(f);
      } else {
        const isPdf = /pdf/.test(f.type);
        setDraftAtts(a => [...a, { id:'u'+Math.random().toString(36).slice(2,7), kind: isPdf?'pdf':'file', name:f.name, tint: isPdf?'#d05858':'#e8793a' }]);
      }
    });
  };
  const [search, setSearch] = npUseState('');
  const [filterActive, setFilterActive] = npUseState('all'); // all | active | off

  // плоский список всех заметок сайта
  const allNotes = [];
  Object.keys(notes || {}).forEach(nodeId => {
    (notes[nodeId] || []).forEach(n => allNotes.push({ ...n, _nodeId: nodeId, _node: nodeById(nodeId) }));
  });
  const visible = allNotes.filter(n => {
    if (filterActive === 'active' && n.active === false) return false;
    if (filterActive === 'off' && n.active !== false) return false;
    if (search) { const q = search.toLowerCase(); return (n.text||'').toLowerCase().includes(q) || (n._node && (n._node.name.toLowerCase().includes(q) || n._node.path.toLowerCase().includes(q))); }
    return true;
  });
  const activeCnt = allNotes.filter(n => n.active !== false).length;

  const [editKey, setEditKey] = npUseState(null); // nodeId_noteId
  const [editBlocks, setEditBlocks] = npUseState([]);
  const [editLayout, setEditLayout] = npUseState('cards');
  const startEdit = (nodeId, note) => {
    setEditKey(nodeId+'_'+note.id);
    setEditBlocks(note.blocks && note.blocks.length ? note.blocks.map(b => ({ ...b })) : [{ ...window.blankNoteBlock(), text: note.text||'' }]);
    setEditLayout(note.layout || (note.blocks && note.blocks.length ? 'cards' : 'plain'));
  };
  const saveEdit = (nodeId, noteId) => {
    setNotes(all => ({ ...all, [nodeId]: (all[nodeId]||[]).map(n => n.id===noteId ? { ...n, blocks: editBlocks, layout: editLayout, text: window.blocksToText(editBlocks) } : n) }));
    setEditKey(null);
  };

  const addNote = () => {
    if (!draftNode || !draftHasContent) return;
    setNotes(all => ({ ...all, [draftNode]: [ ...(all[draftNode]||[]), nCD.note({ author:'Super-admin', role:'Super-admin', color:'#e8793a', text:window.blocksToText(draftBlocks), blocks:draftBlocks, layout:draftLayout, ts:'сейчас', active:true, attachments:draftAtts }) ] }));
    resetDraft(); setCreating(false);
  };
  const toggleActive = (nodeId, noteId) => {
    setNotes(all => ({ ...all, [nodeId]: (all[nodeId]||[]).map(n => n.id===noteId ? { ...n, active: n.active===false } : n) }));
  };
  const deleteNote = (nodeId, noteId) => {
    if (!confirm('Удалить заметку?')) return;
    setNotes(all => ({ ...all, [nodeId]: (all[nodeId]||[]).filter(n => n.id!==noteId) }));
  };

  return (
    <div className="nt-panel">
      <header className="nt-toolbar">
        <div>
          <h2 className="nt-heading">Заметки</h2>
          <p className="nt-lead">{allNotes.length} всего · {activeCnt} активных</p>
        </div>
        {!creating && (
          <button type="button" className="nt-add-btn" onClick={() => setCreating(true)}>+ Создать заметку</button>
        )}
      </header>

      {creating && (
        <div className="nt-compose ctor-fade">
          <div className="nt-compose-head">
            <span className="nt-compose-title">Новая заметка</span>
            <button type="button" className="nt-compose-close" onClick={() => { setCreating(false); resetDraft(); }} aria-label="Закрыть">{Icon.x}</button>
          </div>
          <div className="nt-field">
            <span className="nt-field-label">Узел каталога</span>
            <NodePicker nodes={allNodes} value={draftNode} onChange={setDraftNode} />
          </div>
          <div className="nt-field">
            <span className="nt-field-label">Текст</span>
            <NoteComposer blocks={draftBlocks} onChange={setDraftBlocks} layout={draftLayout} setLayout={setDraftLayout} />
          </div>
          <div className="nt-field">
            <span className="nt-field-label">Вложения</span>
            {draftAtts.length > 0 && (
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 8 }}>
                {draftAtts.map(a => (
                  <span key={a.id} style={{ position: 'relative' }}>
                    <Attachment att={a} />
                    <button type="button" onClick={() => setDraftAtts(x => x.filter(y => y.id !== a.id))} style={{ position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: '50%', border: '1px solid var(--card)', background: 'var(--foreground)', color: 'var(--card)', cursor: 'pointer', fontSize: 11, lineHeight: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2 }} aria-label="Убрать вложение">✕</button>
                  </span>
                ))}
              </div>
            )}
            <button type="button" onClick={() => draftFileRef.current && draftFileRef.current.click()} disabled={draftAtts.length >= 4} style={{ ...ctorStyles.btnGhost, opacity: draftAtts.length >= 4 ? 0.5 : 1 }}>
              Вложить файл
            </button>
            <input ref={draftFileRef} type="file" accept="image/jpeg,image/png,image/webp,application/pdf,.doc,.docx,.xls,.xlsx" multiple style={{ display: 'none' }} onChange={e => { onDraftFiles(e.target.files); e.target.value = ''; }} />
            <span style={{ fontSize: 11, color: 'var(--text-tertiary)', marginLeft: 8 }}>до 4 файлов</span>
          </div>
          <div className="nt-compose-foot">
            <button type="button" onClick={() => { setCreating(false); resetDraft(); }} style={ctorStyles.btnGhost}>Отмена</button>
            <button type="button" onClick={addNote} disabled={!draftNode || !draftHasContent} style={{ ...ctorStyles.btnPrimary, opacity: (!draftNode || !draftHasContent) ? 0.45 : 1 }}>Создать заметку</button>
          </div>
        </div>
      )}

      <div className="nt-filters">
        <label className="nt-search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
          <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Поиск по тексту или узлу" aria-label="Поиск заметок" />
        </label>
        <div className="nt-filter-mode" role="tablist" aria-label="Фильтр заметок">
          {[{ v: 'all', l: 'Все' }, { v: 'active', l: 'Активные' }, { v: 'off', l: 'Выкл.' }].map(o => (
            <button key={o.v} type="button" role="tab" aria-selected={filterActive === o.v} className={'nt-filter-btn' + (filterActive === o.v ? ' on' : '')} onClick={() => setFilterActive(o.v)}>{o.l}</button>
          ))}
        </div>
      </div>

      <div className="nt-list">
        {visible.map(n => {
          const active = n.active !== false;
          const nd = n._node;
          const editId = n._nodeId + '_' + n.id;
          return (
            <article key={editId} className={'nt-note' + (active ? '' : ' is-off')}>
              <div className="nt-note-row">
                <span className="nt-avatar" aria-hidden="true">{initials(n.author)}</span>
                <div className="nt-note-main">
                  <div className="nt-note-meta">
                    <span className="nt-note-author">{n.author}</span>
                    {n.role && <span className="nt-note-role">{n.role}</span>}
                    <span className="nt-note-ts">· {n.ts}</span>
                  </div>
                  {nd && <NoteNodeLink node={nd} onNavigate={onNavigateToNode} />}
                  {editKey === editId ? (
                    <div>
                      <NoteComposer blocks={editBlocks} onChange={setEditBlocks} layout={editLayout} setLayout={setEditLayout} />
                      <div className="nt-compose-foot">
                        <button type="button" onClick={() => setEditKey(null)} style={ctorStyles.btnGhost}>Отмена</button>
                        <button type="button" onClick={() => saveEdit(n._nodeId, n.id)} style={{ ...ctorStyles.btnPrimary, padding: '6px 14px', fontSize: 12.5 }}>Сохранить</button>
                      </div>
                    </div>
                  ) : (
                    <div className="nt-note-body" onClick={() => startEdit(n._nodeId, n)} title="Нажмите, чтобы редактировать">
                      <NoteContentView layout={n.layout} blocks={n.blocks} text={n.text} />
                    </div>
                  )}
                  {(n.attachments || []).length > 0 && <div style={{ marginTop: 8 }}><AttachmentRow atts={n.attachments} onOpen={() => {}} /></div>}
                </div>
                <div className="nt-note-actions">
                  <button type="button" className={'nt-toggle' + (active ? ' on' : '')} onClick={() => toggleActive(n._nodeId, n.id)} title={active ? 'Деактивировать' : 'Активировать'}>
                    {active ? 'Активна' : 'Выкл.'}
                  </button>
                  <button type="button" className="nt-del" onClick={() => deleteNote(n._nodeId, n.id)} title="Удалить" aria-label="Удалить заметку">{Icon.x}</button>
                </div>
              </div>
            </article>
          );
        })}
        {visible.length === 0 && (
          <div className="nt-empty">
            <p className="nt-empty-title">{search || filterActive !== 'all' ? 'Ничего не найдено' : 'Заметок пока нет'}</p>
            <p className="nt-empty-hint">Создайте заметку и привяжите её к этапу, задаче или подзадаче.</p>
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { NotesPanel, flattenCatalogNodes, NODE_TYPE_TAG, catalogNodeAnchorId, scrollToCatalogNode });