/* ctor-cross-rules.jsx — Tab «Перекрёстные работы»: условие (работа сметы / спецификация ДП / анкета и бриф /
   объект) само приносит в смету подготовительные работы (направление + этап) и пункты чек-листа сдачи
   (правило матчится и применяется в lk/calc2-cross-rules.js, docs/superpowers/specs/2026-09-28-cross-works-design.md).
   Данные: ConstructorData.loadCrossRules/saveCrossRules (ctor_cross_rules_v1). Каркас и стили — как
   constructor/ctor-intake-rules.jsx / ctor-packages.jsx (карточки-правила + drawer-редактор), переиспользует
   их pkPhaseLabel(), pkAnchorText(), PkPhaseSeg, PkAnchorPicker («Куда встроить» — спека
   2026-09-29-package-anchors-design.md, §5). Своё пространство имён у глобальных данных ConstructorData — crCD — чтобы не столкнуться
   с `const CD` из финального inline-скрипта «Конструктор проектов.html» (классические <script> делят один
   лексический скоуп верхнего уровня для let/const). */
const { useState: crUseState } = React;
const crCD = window.ConstructorData;

/* поля карточки объекта — те же списки, что и в форме калькулятора (lk/calc2-wizard-forms.jsx), но этот файл
   в Конструкторе не подключён, поэтому список продублирован здесь только для подписей пикера */
const CR_HOUSE_TYPES = [['monolith', 'Монолит'], ['panel', 'Панель'], ['brick', 'Кирпич'], ['block', 'Блочный'], ['wood', 'Дерево']];
const CR_OBJECT_TYPES = [['new_build', 'Новостройка'], ['secondary', 'Вторичка']];
const CR_FINISH_STATES = [['without', 'Без отделки'], ['partial', 'Черновая (white box)'], ['renovation', 'С ремонтом'], ['keep', 'С ремонтом, без демонтажа']];
const CR_OBJECT_FIELDS = [
  { id: 'houseType', label: 'Тип дома', op: '=', options: CR_HOUSE_TYPES },
  { id: 'wallMaterial', label: 'Материал стен и перегородок', op: 'in', options: null },
  { id: 'ceilingApprox', label: 'Высота потолка, м', op: '≥', options: null },
  { id: 'objectType', label: 'Тип объекта', op: '=', options: CR_OBJECT_TYPES },
  { id: 'finishState', label: 'Отделка сейчас', op: '=', options: CR_FINISH_STATES },
];
function crObjectFieldDef(key) { return CR_OBJECT_FIELDS.find((f) => f.id === key) || CR_OBJECT_FIELDS[0]; }
function crOpLabel(op) { return op === 'in' ? '∈' : op === '≥' ? '≥' : op === '~' ? 'содержит' : '='; }

const CR_SRC_OPTS = [
  { v: 'work', l: 'Работа сметы' },
  { v: 'spec', l: 'Спецификация ДП: слово' },
  { v: 'spec-cat', l: 'Спецификация ДП: категория' },
  { v: 'answer', l: 'Анкета / бриф' },
  { v: 'object', l: 'Объект' },
];
function crDefaultCond(src) {
  if (src === 'work') return { src: 'work', key: null };
  if (src === 'spec') return { src: 'spec', op: '~', value: '' };
  if (src === 'spec-cat') return { src: 'spec-cat', op: '=', value: '' };
  if (src === 'answer') return { src: 'answer', op: '=', value: '' };
  return { src: 'object', key: 'houseType', op: '=', value: '' };
}
function crNewWork() { return { type: 'work', name: '', role: null, mode: 'end', stageId: null, taskId: null, at: null, phase: 0, days: 0.5, price: 0 }; }
// фаза этапа в каталоге — для work без явного phase (легаси-правила до A6); зеркало Calc2CrossRules.stagePhaseIdx
// (lk/calc2-cross-rules.js — чужая зона A1–A5), здесь дублируется для страницы конструктора, куда движок не подключён.
function crPhaseIdxOfStage(catalog, stageId) {
  for (let i = 0; i < (catalog || []).length; i++) {
    if (((catalog[i].stages) || []).some((st) => st.id === stageId)) return i;
  }
  return 0;
}
// нормализует work к { mode, stageId, taskId, at, phase } — читает легаси before/after и достраивает phase
function crNormWork(w, catalog) {
  const a = crCD.normAnchor({ mode: w.mode, stageId: w.stageId, taskId: w.taskId, at: w.at });
  const phase = w.phase != null ? w.phase : (a.mode === 'end' ? 0 : crPhaseIdxOfStage(catalog, a.stageId));
  return { ...w, mode: a.mode, stageId: a.stageId || null, taskId: a.taskId || null, at: a.at || null, phase };
}
function crNewCheck() { return { type: 'check', nodeId: null, text: '' }; }
function crNewRule() { return { id: 'cr_' + Date.now().toString(36), name: '', why: '', active: true, when: [], then: [] }; }

/* ─────────── ошибки (та же логика, что валидирует движок lk/calc2-cross-rules.js) ─────────── */
function crCondErrors(cond, catalog) {
  if (!cond) return ['Пустое условие'];
  if (cond.src === 'work') {
    if (!cond.key) return ['Не выбрана работа/этап'];
    return crCD.nodeById(catalog, cond.key) ? [] : ['Работа/этап не найдены в каталоге'];
  }
  if (cond.src === 'spec') return cond.value && String(cond.value).trim() ? [] : ['Не указано слово'];
  if (cond.src === 'spec-cat') return cond.value ? [] : ['Не выбрана категория'];
  if (cond.src === 'answer') return cond.value && String(cond.value).trim() ? [] : ['Не указан ключ ответа'];
  if (cond.src === 'object') {
    if (!cond.key) return ['Не выбрано поле объекта'];
    if (cond.key === 'wallMaterial') return Array.isArray(cond.value) && cond.value.length ? [] : ['Не выбран материал стен'];
    if (cond.key === 'ceilingApprox') return cond.value !== '' && cond.value != null && isFinite(+cond.value) ? [] : ['Не указана высота'];
    return cond.value ? [] : ['Не указано значение'];
  }
  return ['Неизвестный источник условия'];
}
function crWorkErrors(w, catalog) {
  const e = [];
  if (!w.name || !w.name.trim()) e.push('Название работы обязательно');
  if (!w.role) e.push('Не выбрано направление');
  const ph = catalog[w.phase];
  if (!ph) { e.push('Фазы ' + w.phase + ' нет в каталоге'); return e; }
  const a = crCD.normAnchor({ mode: w.mode, stageId: w.stageId, taskId: w.taskId, at: w.at });
  if (a.mode !== 'end') {
    const st = (ph.stages || []).find((s) => s.id === a.stageId);
    if (!st) e.push('этап/работа удалены из каталога — выберите другую');
    else if (a.mode === 'task' && !(st.tasks || []).some((tk) => tk.id === a.taskId)) e.push('этап/работа удалены из каталога — выберите другую');
  }
  return e;
}
function crCheckErrors(c, catalog) {
  const e = [];
  if (!c.nodeId || !crCD.nodeById(catalog, c.nodeId)) e.push('Не выбран этап/работа');
  if (!c.text || !c.text.trim()) e.push('Текст пункта обязателен');
  return e;
}
function crRuleErrors(rule, catalog, packages) {
  const e = [];
  if (!rule.name || !rule.name.trim()) e.push('Название обязательно');
  const when = rule.when || [];
  const then = rule.then || [];
  if (!when.length) e.push('Нужно хотя бы одно условие');
  if (!then.length) e.push('Нужно хотя бы одно действие');
  when.forEach((c) => e.push(...crCondErrors(c, catalog)));
  then.forEach((a) => {
    if (a.type === 'work') e.push(...crWorkErrors(a, catalog));
    else if (a.type === 'check') e.push(...crCheckErrors(a, catalog));
    else if (a.type === 'package' && !(packages || []).find((p) => p.id === a.packageId)) e.push('Пакет не найден');
  });
  return e;
}

/* ─────────── справочник узлов каталога для пикеров (условие «работа», пункт чек-листа) ─────────── */
function crCatalogNodeGroups(catalog) {
  return (catalog || []).map((ph, i) => ({
    idx: i,
    label: i + ' · ' + pkPhaseLabel(catalog, i),
    nodes: (ph.stages || []).reduce((rows, st) => {
      rows.push({ id: st.id, label: st.name });
      (st.tasks || []).forEach((tk) => {
        rows.push({ id: tk.id, label: '— ' + tk.name });
        (tk.sub || []).forEach((su) => rows.push({ id: su.id, label: '—— ' + su.name }));
      });
      return rows;
    }, []),
  }));
}
function CrNodeSelect({ value, onChange, catalog, placeholder }) {
  const groups = crCatalogNodeGroups(catalog);
  return (
    <select value={value || ''} onChange={(e) => onChange(e.target.value || null)} style={{ minWidth: 210 }}>
      <option value="">{placeholder || '— этап/работа —'}</option>
      {groups.map((g) => g.nodes.length > 0 && (
        <optgroup key={g.idx} label={g.label}>
          {g.nodes.map((n) => <option key={n.id} value={n.id}>{n.label}</option>)}
        </optgroup>
      ))}
    </select>
  );
}
/* ─────────── ответы анкеты/брифа: известные ключи tech:<key> и чипы brief:<чип> из «Заготовок по анкете» ─────────── */
function crAnswerOptions(intakeRules) {
  const seen = new Map();
  (intakeRules || []).forEach((r) => {
    (r.source || []).forEach((s) => {
      if (seen.has(s)) return;
      const isTech = s.indexOf('tech:') === 0;
      const label = isTech ? (r.label || s.slice(5)) : s.slice('brief:'.length);
      seen.set(s, { value: s, label, group: isTech ? 'Анкета' : 'Бриф ДП' });
    });
  });
  return [...seen.values()];
}
function crAnswerLabel(value, intakeRules) {
  const found = crAnswerOptions(intakeRules).find((o) => o.value === value);
  if (found) return found.label;
  return (value || '').replace(/^tech:|^brief:/, '') || '—';
}
function CrAnswerPicker({ value, onChange, intakeRules }) {
  const opts = crAnswerOptions(intakeRules);
  return (
    <React.Fragment>
      <input list="cr-answers-dl" value={value || ''} onChange={(e) => onChange(e.target.value)}
        placeholder="tech:ac или brief:Кондиционер в спальне" style={{ ...insStyles.field, minWidth: 240 }} />
      <datalist id="cr-answers-dl">
        {opts.map((o) => <option key={o.value} value={o.value}>{o.group + ' · ' + o.label}</option>)}
      </datalist>
    </React.Fragment>
  );
}

/* ─────────── текст-резюме правила «Если: … → …» для владельца (карточка списка) ─────────── */
function crConditionText(cond, catalog, intakeRules) {
  if (!cond) return '';
  if (cond.src === 'work') {
    const row = crCD.nodeById(catalog, cond.key);
    return '«' + (row ? row.node.name : cond.key || '?') + '» в смете';
  }
  if (cond.src === 'spec') return '«' + (cond.value || '') + '» в спецификации ДП';
  if (cond.src === 'spec-cat') { const cat = crCD.specCategory(cond.value); return 'спецификация: ' + (cat ? cat.label : cond.value); }
  if (cond.src === 'answer') {
    const isTech = (cond.value || '').indexOf('tech:') === 0;
    return (isTech ? 'анкета — ' : 'бриф — ') + crAnswerLabel(cond.value, intakeRules);
  }
  if (cond.src === 'object') {
    const field = crObjectFieldDef(cond.key);
    if (cond.key === 'wallMaterial') {
      const labels = (cond.value || []).map((id) => { const m = crCD.WALL_MATERIALS.find((x) => x.id === id); return m ? m.label : id; });
      return field.label + ': ' + (labels.join(', ') || '—');
    }
    if (cond.key === 'ceilingApprox') return field.label + ' ≥ ' + (cond.value != null && cond.value !== '' ? cond.value : '?') + ' м';
    const opt = (field.options || []).find(([v]) => v === cond.value);
    return field.label + ': ' + (opt ? opt[1] : (cond.value || '?'));
  }
  return '';
}
function crWorkPlace(w, catalog) {
  const a = crCD.normAnchor({ mode: w.mode, stageId: w.stageId, taskId: w.taskId, at: w.at });
  if (a.mode === 'end') return 'в конце фазы «' + pkPhaseLabel(catalog, w.phase) + '»';
  return pkAnchorText(catalog, { phase: w.phase, anchor: a });
}
function crWorkSummary(w, catalog) {
  const dir = (crCD.CROSS_DIRECTIONS.find((d) => d.id === w.role) || {}).label || w.role || '—';
  return (w.name || 'Без названия') + ' (' + dir + ' · ' + crWorkPlace(w, catalog) + ')';
}
function crPlural3(n, one, few, many) {
  const m10 = n % 10, m100 = n % 100;
  if (m10 === 1 && m100 !== 11) return one;
  if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return few;
  return many;
}
function crRuleSummary(rule, catalog, intakeRules) {
  const conds = (rule.when || []).map((c) => crConditionText(c, catalog, intakeRules));
  const then = rule.then || [];
  const parts = then.filter((a) => a.type === 'work').map((w) => crWorkSummary(w, catalog));
  const checksN = then.filter((a) => a.type === 'check').length;
  const pkgsN = then.filter((a) => a.type === 'package').length;
  if (checksN) parts.push(checksN + ' ' + crPlural3(checksN, 'пункт чек-листа', 'пункта чек-листа', 'пунктов чек-листа'));
  if (pkgsN) parts.push(pkgsN + ' ' + crPlural3(pkgsN, 'пакет', 'пакета', 'пакетов'));
  return 'Если: ' + (conds.join(' и ') || '—') + ' → ' + (parts.join(', ') || '—');
}

/* toggle-чип (как IrChip в ctor-intake-rules.jsx): подсвечивается через ui-chips.css .chip--control[data-active] */
function CrChip({ active, onClick, children, title }) {
  return (
    <button type="button" className="chip chip--control" data-active={active ? 'true' : 'false'} onClick={onClick} title={title}>
      {children}
    </button>
  );
}

/* ─────────── условия ─────────── */
function CrCondRow({ cond, idx, catalog, intakeRules, onPatch, onRemove }) {
  const errs = crCondErrors(cond, catalog);
  const setSrc = (src) => onPatch(crDefaultCond(src));
  const setObjectField = (key) => { const f = crObjectFieldDef(key); onPatch({ key: f.id, op: f.op, value: f.id === 'wallMaterial' ? [] : '' }); };
  return (
    <div className={'pkg-step' + (errs.length ? ' has-err' : '')}>
      <div className="pkg-step-head">
        <span className="pkg-step-no">{idx + 1}</span>
        <select value={cond.src} onChange={(e) => setSrc(e.target.value)} style={{ minWidth: 200 }}>
          {CR_SRC_OPTS.map((o) => <option key={o.v} value={o.v}>{o.l}</option>)}
        </select>
        {cond.src === 'work' && <CrNodeSelect value={cond.key} onChange={(v) => onPatch({ key: v })} catalog={catalog} />}
        {cond.src === 'spec' &&
          <input className="pkg-step-name" value={cond.value || ''} onChange={(e) => onPatch({ value: e.target.value })} placeholder="слово в названии/артикуле, без учёта регистра" />}
        {cond.src === 'spec-cat' &&
          <select value={cond.value || ''} onChange={(e) => onPatch({ value: e.target.value })} style={{ minWidth: 220 }}>
            <option value="">— категория —</option>
            {crCD.SPEC_CATEGORIES.map((c) => <option key={c.id} value={c.id}>{c.label}</option>)}
          </select>}
        {cond.src === 'answer' && <CrAnswerPicker value={cond.value} onChange={(v) => onPatch({ value: v })} intakeRules={intakeRules} />}
        {cond.src === 'object' &&
          <select value={cond.key} onChange={(e) => setObjectField(e.target.value)} style={{ minWidth: 220 }}>
            {CR_OBJECT_FIELDS.map((f) => <option key={f.id} value={f.id}>{f.label}</option>)}
          </select>}
        <span className="pkg-step-tools">
          <button type="button" className="danger" onClick={onRemove} title="Удалить условие">✕</button>
        </span>
      </div>
      {cond.src === 'object' &&
        <div className="pkg-step-place">
          <span className="pkg-step-lbl">{crOpLabel(cond.op)}</span>
          {cond.key === 'wallMaterial' &&
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {crCD.WALL_MATERIALS.map((m) => (
                <CrChip key={m.id} active={(cond.value || []).indexOf(m.id) >= 0}
                  onClick={() => { const arr = cond.value || []; onPatch({ value: arr.indexOf(m.id) >= 0 ? arr.filter((x) => x !== m.id) : [...arr, m.id] }); }}>
                  {m.label}
                </CrChip>
              ))}
            </div>}
          {cond.key === 'ceilingApprox' &&
            <input type="number" step={0.1} min={0} value={cond.value === '' || cond.value == null ? '' : cond.value}
              onChange={(e) => onPatch({ value: e.target.value === '' ? '' : +e.target.value })} style={{ ...insStyles.field, width: 90 }} />}
          {['houseType', 'objectType', 'finishState'].indexOf(cond.key) >= 0 &&
            <select value={cond.value || ''} onChange={(e) => onPatch({ value: e.target.value })}>
              <option value="">— значение —</option>
              {crObjectFieldDef(cond.key).options.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
            </select>}
        </div>}
      {errs.length > 0 && <div className="pkg-step-err">{errs.join(' · ')}</div>}
    </div>
  );
}
function CrConditionsEditor({ value, onChange, catalog, intakeRules }) {
  const patch = (i, p) => onChange(value.map((c, j) => (j === i ? { ...c, ...p } : c)));
  const remove = (i) => onChange(value.filter((_, j) => j !== i));
  const add = () => onChange([...value, crDefaultCond('work')]);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      {value.map((c, i) => <CrCondRow key={i} cond={c} idx={i} catalog={catalog} intakeRules={intakeRules} onPatch={(p) => patch(i, p)} onRemove={() => remove(i)} />)}
      {!value.length && <div style={insStyles.hint}>Условий нет — добавьте хотя бы одно (все условия должны выполняться одновременно)</div>}
      <button type="button" className="opt-add-btn" onClick={add}>+ Добавить условие</button>
    </div>
  );
}

/* ─────────── работы ─────────── */
function CrWorkRow({ work, idx, total, catalog, onPatch, onMove, onRemove }) {
  const errs = crWorkErrors(work, catalog);
  const setPhase = (i) => onPatch({ phase: i, mode: 'end', stageId: null, taskId: null, at: null });
  const onAnchor = (a) => onPatch({ mode: a.mode, stageId: a.stageId || null, taskId: a.taskId || null, at: a.at || null });
  return (
    <div className={'pkg-step' + (errs.length ? ' has-err' : '')}>
      <div className="pkg-step-head">
        <span className="pkg-step-no">{idx + 1}</span>
        <input className="pkg-step-name" value={work.name} placeholder="Название работы" onChange={(e) => onPatch({ name: e.target.value })} />
        <span className="pkg-step-tools">
          <button type="button" disabled={idx === 0} onClick={() => onMove(-1)} title="Выше">↑</button>
          <button type="button" disabled={idx === total - 1} onClick={() => onMove(1)} title="Ниже">↓</button>
          <button type="button" className="danger" onClick={onRemove} title="Удалить работу">✕</button>
        </span>
      </div>
      <div className="pkg-step-grid">
        <label><span>Направление</span>
          <select value={work.role || ''} onChange={(e) => onPatch({ role: e.target.value || null })}>
            <option value="">—</option>
            {crCD.CROSS_DIRECTIONS.map((d) => <option key={d.id} value={d.id}>{d.label}</option>)}
          </select>
        </label>
        <label><span>Дни</span><input type="number" min={0.5} step={0.5} value={work.days} onChange={(e) => onPatch({ days: Math.max(0.5, +e.target.value || 0.5) })} /></label>
        <label><span>Цена, ₽</span><input type="number" min={0} step={100} value={work.price} onChange={(e) => onPatch({ price: Math.max(0, +e.target.value || 0) })} /></label>
        <div />
      </div>
      <div className="pkg-step-place">
        <div className="pkg-step-phase">
          <span className="pkg-step-lbl">Фаза</span>
          <PkPhaseSeg value={work.phase} onChange={setPhase} catalog={catalog} />
          <span className="pkg-step-phname">{pkPhaseLabel(catalog, work.phase)}</span>
        </div>
        <div className="pkg-step-anchor">
          <span className="pkg-step-lbl">Куда встроить</span>
          <PkAnchorPicker catalog={catalog} phase={work.phase} anchor={{ mode: work.mode, stageId: work.stageId, taskId: work.taskId, at: work.at }} role={work.role} onChange={onAnchor} />
        </div>
      </div>
      {errs.length > 0 && <div className="pkg-step-err">{errs.join(' · ')}</div>}
    </div>
  );
}
function CrWorksEditor({ value, onChange, catalog }) {
  const patch = (i, p) => onChange(value.map((w, j) => (j === i ? { ...w, ...p } : w)));
  const move = (i, dir) => onChange((() => { const n = [...value]; const j = i + dir; if (j < 0 || j >= n.length) return value; [n[i], n[j]] = [n[j], n[i]]; return n; })());
  const remove = (i) => onChange(value.filter((_, j) => j !== i));
  const add = () => onChange([...value, crNewWork()]);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      {value.map((w, i) => (
        <CrWorkRow key={i} work={w} idx={i} total={value.length} catalog={catalog}
          onPatch={(p) => patch(i, p)} onMove={(dir) => move(i, dir)} onRemove={() => remove(i)} />
      ))}
      {!value.length && <div style={insStyles.hint}>Работ нет</div>}
      <button type="button" className="opt-add-btn" onClick={add}>+ Добавить работу</button>
    </div>
  );
}

/* ─────────── пункты чек-листа ─────────── */
function CrCheckRow({ check, idx, catalog, onPatch, onRemove }) {
  const errs = crCheckErrors(check, catalog);
  return (
    <div className={'pkg-step' + (errs.length ? ' has-err' : '')}>
      <div className="pkg-step-head">
        <span className="pkg-step-no">{idx + 1}</span>
        <CrNodeSelect value={check.nodeId} onChange={(v) => onPatch({ nodeId: v })} catalog={catalog} />
        <input className="pkg-step-name" style={{ flex: 1 }} value={check.text} placeholder="Текст пункта чек-листа"
          onChange={(e) => onPatch({ text: e.target.value })} />
        <span className="pkg-step-tools">
          <button type="button" className="danger" onClick={onRemove} title="Удалить пункт">✕</button>
        </span>
      </div>
      {errs.length > 0 && <div className="pkg-step-err">{errs.join(' · ')}</div>}
    </div>
  );
}
function CrChecksEditor({ value, onChange, catalog }) {
  const patch = (i, p) => onChange(value.map((c, j) => (j === i ? { ...c, ...p } : c)));
  const remove = (i) => onChange(value.filter((_, j) => j !== i));
  const add = () => onChange([...value, crNewCheck()]);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      {value.map((c, i) => <CrCheckRow key={i} check={c} idx={i} catalog={catalog} onPatch={(p) => patch(i, p)} onRemove={() => remove(i)} />)}
      {!value.length && <div style={insStyles.hint}>Пунктов чек-листа нет</div>}
      <button type="button" className="opt-add-btn" onClick={add}>+ Добавить пункт</button>
    </div>
  );
}

/* ─────────── пакеты смежных работ (ctor-packages.jsx / loadPackages) ─────────── */
function CrPackagesEditor({ value, onChange, packages }) {
  const toggle = (id) => onChange(value.indexOf(id) >= 0 ? value.filter((x) => x !== id) : [...value, id]);
  if (!packages.length) return <div style={insStyles.hint}>Пакетов пока нет — создайте во вкладке «Пакеты».</div>;
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
      {packages.map((pkg) => (
        <CrChip key={pkg.id} active={value.indexOf(pkg.id) >= 0} onClick={() => toggle(pkg.id)} title={pkg.desc || ''}>
          <span aria-hidden="true">{pkg.icon}</span> {pkg.name || 'Без названия'}
        </CrChip>
      ))}
    </div>
  );
}

/* ─────────── drawer редактора правила ─────────── */
function CrossRuleDrawer({ rule, catalog, packages, intakeRules, onSave, onClose }) {
  const initDraft = () => {
    if (!rule) {
      const seed = crNewRule();
      return { id: seed.id, name: '', why: '', active: true, when: [], works: [], checks: [], packageIds: [] };
    }
    const then = rule.then || [];
    return {
      id: rule.id, name: rule.name || '', why: rule.why || '', active: rule.active !== false,
      when: JSON.parse(JSON.stringify(rule.when || [])),
      // легаси before/after и работы без явного phase — читаем через ConstructorData.normAnchor (спека §5)
      works: JSON.parse(JSON.stringify(then.filter((a) => a.type === 'work'))).map((w) => crNormWork(w, catalog)),
      checks: JSON.parse(JSON.stringify(then.filter((a) => a.type === 'check'))),
      packageIds: then.filter((a) => a.type === 'package').map((a) => a.packageId),
    };
  };
  const [draft, setDraft] = crUseState(initDraft);
  const p = (k, v) => setDraft((d) => ({ ...d, [k]: v }));
  const draftThen = [...draft.works, ...draft.checks, ...draft.packageIds.map((id) => ({ type: 'package', packageId: id }))];
  const errors = crRuleErrors({ name: draft.name, when: draft.when, then: draftThen }, catalog, packages);
  const save = () => onSave({ id: draft.id, name: draft.name, why: draft.why, active: draft.active, when: draft.when, then: draftThen });
  return (
    <InspectorDrawer title={rule ? 'Редактировать правило' : 'Новое правило'} onClose={onClose} width={700}
      footer={
        <React.Fragment>
          <button type="button" onClick={onClose} style={ctorStyles.btnGhost}>Отмена</button>
          <button type="button" onClick={() => { save(); onClose(); }} disabled={errors.length > 0}
            style={{ ...ctorStyles.btnPrimary, opacity: errors.length > 0 ? 0.45 : 1 }}>Сохранить</button>
        </React.Fragment>
      }>
      <InspectorSection title="Правило">
        <FText label="Название" value={draft.name} onChange={(v) => p('name', v)} placeholder="Кондиционер" />
        <FText label="Почему" value={draft.why} onChange={(v) => p('why', v)} multiline
          placeholder="Трассы и дренаж прокладывают до штукатурки" hint="Видит менеджер в смете и прораб/исполнитель в задаче" />
        <FToggle label="Активно" value={draft.active} onChange={(v) => p('active', v)} hint="Неактивное правило не применяется в калькуляторе" />
      </InspectorSection>
      <InspectorSection title={'Условия · ' + draft.when.length} lead="Все условия должны выполняться одновременно («и»); «или» — второе правило">
        <CrConditionsEditor value={draft.when} onChange={(v) => p('when', v)} catalog={catalog} intakeRules={intakeRules} />
      </InspectorSection>
      <InspectorSection title={'Работы · ' + draft.works.length} lead="Отдельная работа своего направления внутри выбранного этапа — в смете, Ганте и задачах проекта">
        <CrWorksEditor value={draft.works} onChange={(v) => p('works', v)} catalog={catalog} />
      </InspectorSection>
      <InspectorSection title={'Пункты чек-листа · ' + draft.checks.length} lead="Пункт чек-листа сдачи выбранной работы каталога, без цены">
        <CrChecksEditor value={draft.checks} onChange={(v) => p('checks', v)} catalog={catalog} />
      </InspectorSection>
      <InspectorSection title={'Пакеты · ' + draft.packageIds.length} lead="Включить пакет смежных работ целиком — чтобы не дублировать заготовку по анкете">
        <CrPackagesEditor value={draft.packageIds} onChange={(v) => p('packageIds', v)} packages={packages} />
      </InspectorSection>
      <InspectorSection title="Предпросмотр" lead="Так правило будет показано владельцу в списке">
        <div style={{ ...insStyles.hint, fontSize: 12.5, lineHeight: 1.5 }}>
          {crRuleSummary({ when: draft.when, then: draftThen }, catalog, intakeRules)}
        </div>
      </InspectorSection>
      {errors.length > 0 && <div style={{ color: 'var(--destructive)', fontSize: 12.5 }}>{errors.join(' · ')}</div>}
    </InspectorDrawer>
  );
}

/* ─────────── панель вкладки ─────────── */
function CrossRulesPanel({ rules, setRules, catalog, packages, intakeRules, focusId }) {
  const [editing, setEditing] = crUseState(null); // null | 'new' | rule
  const [query, setQuery] = crUseState('');
  const [flashId, setFlashId] = crUseState(null); // подсветка карточки, найденной глобальным поиском
  React.useEffect(() => {
    if (!focusId) return;
    const el = document.getElementById('cr-card-' + focusId);
    if (el) el.scrollIntoView({ behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'center' });
    setFlashId(focusId);
    const t = setTimeout(() => setFlashId(null), 1800);
    return () => clearTimeout(t);
  }, [focusId]);
  const save = (rule) => setRules((list) => (list.some((x) => x.id === rule.id) ? list.map((x) => (x.id === rule.id ? rule : x)) : [...list, rule]));
  const toggleActive = (id) => setRules((list) => list.map((x) => (x.id === id ? { ...x, active: !(x.active !== false) } : x)));
  const del = (id) => {
    if (!confirm('Удалить правило? Уже добавленные им работы и пункты в собранных сметах останутся.')) return;
    setRules((list) => list.filter((x) => x.id !== id));
  };
  const q = query.trim().toLowerCase();
  const filtered = q ? rules.filter((r) => (r.name || '').toLowerCase().indexOf(q) >= 0 || (r.why || '').toLowerCase().indexOf(q) >= 0) : rules;
  return (
    <div className="opt-panel">
      <header className="opt-toolbar">
        <div>
          <h2 className="opt-heading">Перекрёстные работы</h2>
          <p className="opt-lead">Условие в смете, спецификации ДП, анкете/брифе или карточке объекта само добавляет подготовительные работы и пункты чек-листа в нужный этап; калькулятор их только применяет.</p>
        </div>
        <button type="button" className="opt-add-btn" onClick={() => setEditing('new')}>+ Новое правило</button>
      </header>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Поиск по названию или «почему»"
        style={{ ...insStyles.field, maxWidth: 360, marginBottom: 16 }} />
      {!filtered.length &&
        <div className="pkg-empty">{rules.length ? 'Ничего не найдено' : 'Правил нет — создайте первое: условие → работы и пункты чек-листа.'}</div>}
      <div className="pkg-grid">
        {filtered.map((rule) => {
          const errs = crRuleErrors(rule, catalog, packages);
          const then = rule.then || [];
          const worksN = then.filter((a) => a.type === 'work').length;
          const checksN = then.filter((a) => a.type === 'check').length;
          const pkgsN = then.filter((a) => a.type === 'package').length;
          return (
            <div key={rule.id} id={'cr-card-' + rule.id} className={'pkg-card' + (rule.active === false ? ' is-off' : '')}
              style={flashId === rule.id ? { boxShadow: '0 0 0 3px var(--primary)', borderRadius: 14 } : undefined}>
              <div className="pkg-card-ic" style={{ background: '#0ea5e9' }}>⇄</div>
              <div className="pkg-card-body">
                <div className="pkg-card-name">{rule.name || 'Без названия'}</div>
                {rule.why && <div className="pkg-card-desc">{rule.why}</div>}
                <div className="pkg-card-desc" style={{ opacity: .8 }}>{crRuleSummary(rule, catalog, intakeRules)}</div>
                <div className="pkg-card-meta">
                  <span>{(rule.when || []).length} усл.</span>
                  <span>{worksN} работ.</span>
                  {checksN > 0 && <span>{checksN} чек-лист</span>}
                  {pkgsN > 0 && <span>{pkgsN} пак.</span>}
                  {errs.length > 0 && <span className="pkg-err-badge" title={errs.join(' · ')}>⚠ {errs.length}</span>}
                </div>
              </div>
              <div className="pkg-card-tools">
                <OToggle value={rule.active !== false} onChange={() => toggleActive(rule.id)} />
                <button type="button" onClick={() => setEditing(rule)} title="Редактировать">✎</button>
                <button type="button" className="danger" onClick={() => del(rule.id)} title="Удалить">✕</button>
              </div>
            </div>
          );
        })}
      </div>
      {editing && (
        <CrossRuleDrawer rule={editing === 'new' ? null : editing} catalog={catalog} packages={packages} intakeRules={intakeRules}
          onSave={save} onClose={() => setEditing(null)} />
      )}
    </div>
  );
}
window.CrossRulesPanel = CrossRulesPanel;
