/* eslint-disable */
/* timing-main.jsx — standalone оболочка конструктора (без CRM-сайдбара). */

(function () {
  const R = window.React;
  const { useState, useRef, useEffect } = R;
  const D = window.TimingData;
  const P = window.TimingBuilderParts;
  const PR = window.TimingPrint;
  const TG = window.TimingGrid;
  const A = window.TimingAuth;
  const PJ = window.TimingProjects;

  const STORE_KEY = 'ezc.timing.doc.v3';

  function histBtn(enabled) {
    return {
      width: 34, height: 34, borderRadius: 'var(--radius-input)', border: '1px solid var(--color-line-subtle)',
      background: 'var(--color-surface)', color: enabled ? 'var(--color-ink-700)' : 'var(--color-ink-300)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      cursor: enabled ? 'pointer' : 'default',
    };
  }

  function loadInitial() {
    try {
      const raw = localStorage.getItem(STORE_KEY);
      if (raw) { const d = JSON.parse(raw); if (!d.eventGrid) d.eventGrid = D.blankGrid(d.eventStart || 960); return d; }
    } catch (e) {}
    return D.loadTemplate('opening');
  }

  function makeRow(block) {
    return { id: D.uid(), kind: 'row', type: block.y, title: block.t, dur: block.d, who: '', zone: block.z || '', note: '', pin: null };
  }

  function blankDoc() {
    return { key: 'blank', meta: { event: 'Новое мероприятие', date: '', venue: '', guests: '', code: '', pm: '' }, prepStart: 480, eventStart: 960, prep: [], event: [], eventGrid: D.blankGrid(960) };
  }

  /* ============ Меню шаблонов ============ */
  function TemplateMenu({ onPick, onClose }) {
    return (
      <React.Fragment>
        <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 40 }} />
        <div style={{
          position: 'absolute', top: '100%', right: 0, marginTop: 6, zIndex: 41, width: 320,
          background: 'var(--color-elevated)', border: '1px solid var(--color-line-subtle)',
          borderRadius: 'var(--radius-modal)', boxShadow: 'var(--shadow-modal)', padding: 6, overflow: 'hidden',
        }}>
          <div style={{ padding: '8px 10px 6px', fontSize: 11, fontWeight: 600, color: 'var(--color-ink-500)', letterSpacing: '.05em', textTransform: 'uppercase' }}>Загрузить шаблон</div>
          {D.TEMPLATE_ORDER.map(k => {
            const t = D.TEMPLATES[k];
            return (
              <button key={k} onClick={() => onPick(k)} style={{
                display: 'flex', flexDirection: 'column', gap: 2, width: '100%', textAlign: 'left',
                padding: '9px 10px', borderRadius: 'var(--radius-card)', border: 0, background: 'transparent',
              }}
                onMouseEnter={e => e.currentTarget.style.background = 'var(--color-muted-50)'}
                onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--color-ink-900)' }}>{t.name}</span>
                <span style={{ fontSize: 11.5, color: 'var(--color-ink-500)', lineHeight: 1.3 }}>{t.hint}</span>
              </button>
            );
          })}
          <div style={{ borderTop: '1px solid var(--color-line-subtle)', margin: '6px 4px 4px' }} />
          <button onClick={() => onPick('__blank')} style={{
            display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left',
            padding: '9px 10px', borderRadius: 'var(--radius-card)', border: 0, background: 'transparent', color: 'var(--color-ink-700)', fontSize: 13,
          }}
            onMouseEnter={e => e.currentTarget.style.background = 'var(--color-muted-50)'}
            onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
            <Icon name="plus" size={14} /> Пустой документ
          </button>
        </div>
      </React.Fragment>
    );
  }

  /* ============ Главный компонент ============ */
  function TimingConstructor({ user, onLogout }) {
    const [doc, setDocRaw] = useState(loadInitial);
    const [phase, setPhase] = useState('prep');
    const [grabbed, setGrabbed] = useState(null);
    const [overIdx, setOverIdx] = useState(null);
    const [showTpl, setShowTpl] = useState(false);
    const [showPrint, setShowPrint] = useState(false);
    const [printOrient, setPrintOrient] = useState('portrait');
    const [printFormat, setPrintFormat] = useState('print');
    const [pendingTpl, setPendingTpl] = useState(null);
    const [showProjects, setShowProjects] = useState(false);
    const [currentProject, setCurrentProject] = useState(null);
    const [saveState, setSaveState] = useState('idle');
    const [showClear, setShowClear] = useState(false);
    const [libCollapsed, setLibCollapsed] = useState(() => { try { return localStorage.getItem('ezc.timing.libCollapsed') === '1'; } catch (e) { return false; } });
    const [, setHistTick] = useState(0);
    const dragInfo = useRef(null);
    const focusId = useRef(null);

    useEffect(() => { try { localStorage.setItem('ezc.timing.libCollapsed', libCollapsed ? '1' : '0'); } catch (e) {} }, [libCollapsed]);

    /* ---- История (undo / redo) ---- */
    const hist = useRef({ past: [], future: [] });
    const prevDocRef = useRef(doc);
    const skipHist = useRef(false);
    const coalesceNext = useRef(false);
    const lastTs = useRef(0);

    useEffect(() => {
      if (prevDocRef.current === doc) return;
      if (skipHist.current) { skipHist.current = false; prevDocRef.current = doc; return; }
      const h = hist.current;
      const now = Date.now();
      const coalesce = coalesceNext.current && (now - lastTs.current) < 700 && h.past.length > 0;
      if (!coalesce) { h.past.push(prevDocRef.current); if (h.past.length > 80) h.past.shift(); }
      h.future = [];
      lastTs.current = now;
      coalesceNext.current = false;
      prevDocRef.current = doc;
      setHistTick(t => t + 1);
    }, [doc]);

    function setDoc(updater, coalesce) {
      if (coalesce) coalesceNext.current = true;
      setDocRaw(updater);
    }
    function undo() {
      const h = hist.current;
      if (!h.past.length) return;
      skipHist.current = true;
      h.future.push(prevDocRef.current);
      setDocRaw(h.past.pop());
      setHistTick(t => t + 1);
    }
    function redo() {
      const h = hist.current;
      if (!h.future.length) return;
      skipHist.current = true;
      h.past.push(prevDocRef.current);
      setDocRaw(h.future.pop());
      setHistTick(t => t + 1);
    }
    const canUndo = hist.current.past.length > 0;
    const canRedo = hist.current.future.length > 0;

    useEffect(() => {
      const onKey = e => {
        const mod = e.metaKey || e.ctrlKey;
        if (!mod) return;
        const k = e.key.toLowerCase();
        if (k === 'z') { e.preventDefault(); if (e.shiftKey) redo(); else undo(); }
        else if (k === 'y') { e.preventDefault(); redo(); }
      };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, []);

    useEffect(() => {
      try { localStorage.setItem(STORE_KEY, JSON.stringify(doc)); } catch (e) {}
    }, [doc]);

    /* ---- Проекты / сброс ---- */
    function loadDocFresh(nd) {
      skipHist.current = true;
      hist.current = { past: [], future: [] };
      setHistTick(t => t + 1);
      setDocRaw(nd);
    }
    function openProject(full) {
      const d = full.data || {};
      if (!d.eventGrid) d.eventGrid = D.blankGrid(d.eventStart || 960);
      loadDocFresh(d);
      setCurrentProject({ id: full.id, title: full.title });
      setPhase('prep'); setShowProjects(false);
    }
    function newProject() {
      loadDocFresh(blankDoc());
      setCurrentProject(null);
      setPhase('prep'); setShowProjects(false);
    }
    async function saveProject() {
      if (saveState === 'saving') return;
      setSaveState('saving');
      const title = (doc.meta.event || '').trim() || 'Без названия';
      let p = null;
      try {
        if (currentProject && currentProject.id) p = await A.api.update(currentProject.id, { title, data: doc });
        else p = await A.api.create(title, doc);
      } catch (e) { p = null; }
      if (p && p.id) setCurrentProject({ id: p.id, title: p.title || title });
      setSaveState('saved');
      setTimeout(() => setSaveState('idle'), 1600);
    }
    function clearDoc() {
      loadDocFresh(blankDoc());
      setCurrentProject(null);
      setPhase('prep');
      setShowClear(false);
    }

    const items = doc[phase];
    const phaseStart = phase === 'prep' ? doc.prepStart : doc.eventStart;
    const computed = D.computeTimes(items, phaseStart);
    const span = D.phaseSpan(computed, phaseStart);
    const sectionMeta = {}, dayMeta = {};
    { let curS = null, curD = null; computed.forEach((it, i) => {
        if (it.kind === 'day') { curD = i; dayMeta[i] = { count: 0, dur: 0 }; curS = null; }
        else if (it.kind === 'section') { curS = i; sectionMeta[i] = { count: 0, dur: 0 }; }
        else { if (curS != null) { sectionMeta[curS].count++; sectionMeta[curS].dur += it.dur || 0; } if (curD != null) { dayMeta[curD].count++; dayMeta[curD].dur += it.dur || 0; } }
      }); }
    const hasDays = phase === 'prep' && items.some(it => it.kind === 'day');
    const days = phase === 'prep' ? D.dayBlocks(items, phaseStart) : [];
    const prepTotalDur = days.reduce((s, d) => s + d.dur, 0);

    function setItems(updater, coalesce) {
      setDoc(prev => {
        const next = Object.assign({}, prev);
        next[phase] = typeof updater === 'function' ? updater(prev[phase]) : updater;
        return next;
      }, coalesce);
    }
    const patch = (id, partial) => setItems(arr => arr.map(it => it.id === id ? Object.assign({}, it, partial) : it), true);
    const del = id => setItems(arr => arr.filter(it => it.id !== id));
    const dup = id => setItems(arr => {
      const i = arr.findIndex(it => it.id === id);
      if (i < 0) return arr;
      const copy = Object.assign({}, arr[i], { id: D.uid(), pin: null });
      const next = arr.slice(); next.splice(i + 1, 0, copy); return next;
    });
    const addRow = () => { const nid = D.uid(); focusId.current = nid; setItems(arr => arr.concat([{ id: nid, kind: 'row', type: 'program', title: '', dur: 15, who: '', zone: '', note: '', pin: null }])); };
    const addRowAfter = id => {
      const nid = D.uid(); focusId.current = nid;
      setItems(arr => { const i = arr.findIndex(x => x.id === id); const row = { id: nid, kind: 'row', type: 'program', title: '', dur: 15, who: '', zone: '', note: '', pin: null }; const next = arr.slice(); next.splice(i < 0 ? arr.length : i + 1, 0, row); return next; });
    };
    const addSection = () => setItems(arr => arr.concat([{ id: D.uid(), kind: 'section', title: 'Новый раздел' }]));
    const addDay = () => setItems(arr => arr.concat([{ id: D.uid(), kind: 'day', date: '', start: 540, collapsed: false }]));
    function toggleCollapse(id) {
      skipHist.current = true;
      setDocRaw(prev => { const next = Object.assign({}, prev); next[phase] = prev[phase].map(it => it.id === id ? Object.assign({}, it, { collapsed: !it.collapsed }) : it); return next; });
    }
    function docHasContent() { return doc.prep.length > 0 || doc.event.length > 0 || (doc.eventGrid && doc.eventGrid.cards.length > 0); }
    const addFromLib = block => {
      if (phase === 'prep') { setItems(arr => arr.concat([makeRow(block)])); return; }
      setDoc(prev => {
        const g = prev.eventGrid;
        if (!g.tracks.length || !g.slots.length) return prev;
        const lastSlot = g.slots.slice().sort((a, b) => a.time - b.time).slice(-1)[0];
        return Object.assign({}, prev, { eventGrid: Object.assign({}, g, { cards: g.cards.concat([{ id: D.uid(), trackId: g.tracks[0].id, slotId: lastSlot.id, title: block.t, dur: block.d, who: '', note: '' }]) }) });
      });
    };
    const setPhaseStart = v => setDoc(prev => Object.assign({}, prev, phase === 'prep' ? { prepStart: v } : { eventStart: v }), true);
    const setMeta = (k, v) => setDoc(prev => Object.assign({}, prev, { meta: Object.assign({}, prev.meta, { [k]: v }) }), true);

    /* ---- Grid (мероприятие) ---- */
    const isEvent = phase === 'event';
    const eSpan = D.gridSpan(doc.eventGrid);
    function setGrid(updater, coalesce) { setDoc(prev => Object.assign({}, prev, { eventGrid: typeof updater === 'function' ? updater(prev.eventGrid) : updater }), coalesce); }
    const gridOps = {
      addCard: (trackId, slotId, partial) => setGrid(g => ({ ...g, cards: g.cards.concat([{ id: D.uid(), trackId, slotId, title: (partial && partial.title) || '', dur: (partial && partial.dur) || null, who: '', note: '' }]) })),
      addCardFromLib: (trackId, slotId, block) => setGrid(g => ({ ...g, cards: g.cards.concat([{ id: D.uid(), trackId, slotId, title: block.t, dur: block.d, who: '', note: '' }]) })),
      patchCard: (id, p) => setGrid(g => ({ ...g, cards: g.cards.map(c => c.id === id ? { ...c, ...p } : c) }), true),
      delCard: id => setGrid(g => ({ ...g, cards: g.cards.filter(c => c.id !== id) })),
      moveCard: (id, trackId, slotId) => setGrid(g => ({ ...g, cards: g.cards.map(c => c.id === id ? { ...c, trackId, slotId } : c) })),
      patchTrack: (id, p) => setGrid(g => ({ ...g, tracks: g.tracks.map(t => t.id === id ? { ...t, ...p } : t) }), true),
      delTrack: id => setGrid(g => ({ ...g, tracks: g.tracks.filter(t => t.id !== id), cards: g.cards.filter(c => c.trackId !== id) })),
      addTrack: () => setGrid(g => ({ ...g, tracks: g.tracks.concat([{ id: D.uid(), name: 'Новая колонка', color: D.nextTrackKey(g.tracks.map(t => t.color)) }]) })),
      patchSlot: (id, p) => setGrid(g => ({ ...g, slots: g.slots.map(s => s.id === id ? { ...s, ...p } : s) }), true),
      delSlot: id => setGrid(g => ({ ...g, slots: g.slots.filter(s => s.id !== id), cards: g.cards.filter(c => c.slotId !== id) })),
      addSlot: () => setGrid(g => { const max = g.slots.length ? Math.max.apply(null, g.slots.map(s => s.time)) : doc.eventStart; return { ...g, slots: g.slots.concat([{ id: D.uid(), time: max + 15 }]) }; }),
    };

    function applyTemplate(k) {
      if (k === '__blank') setDoc(blankDoc());
      else setDoc(D.loadTemplate(k));
      setCurrentProject(null);
      setPhase('prep');
      setPendingTpl(null);
    }
    function pickTemplate(k) {
      setShowTpl(false);
      if (docHasContent()) setPendingTpl(k);
      else applyTemplate(k);
    }

    /* ---- DnD ---- */
    function onListDragOver(e) {
      e.preventDefault();
      if (e.target.getAttribute && e.target.hasAttribute('data-list-pad')) setOverIdx(items.length);
    }
    function rowDragOver(e, i) {
      e.preventDefault();
      const rect = e.currentTarget.getBoundingClientRect();
      const after = e.clientY > rect.top + rect.height / 2;
      setOverIdx(after ? i + 1 : i);
    }
    function onDrop(e) {
      e.preventDefault();
      const di = dragInfo.current;
      const idx = overIdx == null ? items.length : overIdx;
      if (di) {
        setItems(arr => {
          const next = arr.slice();
          if (di.type === 'move') {
            const from = next.findIndex(x => x.id === di.id);
            if (from < 0) return arr;
              const kind = next[from].kind;
              if (kind === 'day' || kind === 'section') {
                let end = from + 1;
                while (end < next.length) {
                  const k = next[end].kind;
                  if (k === 'day') break;
                  if (kind === 'section' && k === 'section') break;
                  end++;
                }
                const block = next.splice(from, end - from);
                let target = idx; if (from < idx) target -= block.length;
                target = Math.max(0, Math.min(target, next.length));
                next.splice.apply(next, [target, 0].concat(block));
              } else {
                const [moved] = next.splice(from, 1);
                let target = idx; if (from < idx) target--;
                next.splice(target, 0, moved);
              }
          } else if (di.type === 'lib') {
            next.splice(idx, 0, makeRow(di.block));
          }
          return next;
        });
      }
      setOverIdx(null); setGrabbed(null); dragInfo.current = null;
    }
    function grab(id) {
      setGrabbed(id);
      dragInfo.current = id ? { type: 'move', id } : null;
    }
    function dragOverFor(i) {
      if (overIdx == null) return null;
      if (overIdx === i) return 'before';
      if (i === items.length - 1 && overIdx === items.length) return 'after';
      return null;
    }

    return (
      <div className="tm-app">
        {/* Top bar */}
        <header className="tm-top">
          <div className="tm-brand">
            <div className="tm-logo">EZ</div>
            <div style={{ minWidth: 0 }}>
              <input className="tm-event-name" value={doc.meta.event || ''} onChange={e => setMeta('event', e.target.value)} placeholder="Название мероприятия" />
              <div className="tm-event-sub">
                {[doc.meta.date, doc.meta.venue].filter(Boolean).join(' · ') || 'Конструктор тайминга'}
              </div>
            </div>
          </div>

          <div className="tm-phase-switch">
            {[['prep', 'Подготовка'], ['event', 'Мероприятие']].map(([k, label]) => (
              <button key={k} className={'tm-phase-btn' + (phase === k ? ' is-active' : '')} onClick={() => setPhase(k)}>
                {label}
              </button>
            ))}
          </div>

          <div style={{ display: 'flex', alignItems: 'center', gap: 8, position: 'relative' }}>
            <div style={{ display: 'flex', gap: 4 }}>
              <button onClick={undo} disabled={!canUndo} title="Отменить · ⌘Z" style={histBtn(canUndo)}><Icon name="undo" size={15} /></button>
              <button onClick={redo} disabled={!canRedo} title="Вернуть · ⌘⇧Z" style={histBtn(canRedo)}><Icon name="redo" size={15} /></button>
            </div>
            <Button variant="secondary" leftIcon="folder" onClick={() => setShowProjects(true)}>Проекты</Button>
            <div style={{ position: 'relative' }}>
              <Button variant="secondary" leftIcon="layoutGrid" rightIcon="chevronDown" onClick={() => setShowTpl(s => !s)}>Шаблоны</Button>
              {showTpl && <TemplateMenu onPick={pickTemplate} onClose={() => setShowTpl(false)} />}
            </div>
            <Button variant="ghost" onClick={() => setShowClear(true)}>Очистить</Button>
            <button onClick={saveProject} disabled={saveState === 'saving'} title="Сохранить проект" style={{
              display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 14px', border: 0, borderRadius: 'var(--radius-input)',
              background: saveState === 'saved' ? 'var(--color-success-500)' : 'var(--color-accent-500)', color: '#fff', fontSize: 13, fontWeight: 600,
              cursor: saveState === 'saving' ? 'default' : 'pointer', whiteSpace: 'nowrap',
            }}>
              {saveState === 'saving'
                ? <span className="tm-spin" style={{ width: 13, height: 13, border: '2px solid rgba(255,255,255,.4)', borderTopColor: '#fff', borderRadius: '50%', display: 'inline-block' }} />
                : <Icon name={saveState === 'saved' ? 'check' : 'save'} size={14} />}
              {saveState === 'saving' ? 'Сохранение…' : saveState === 'saved' ? 'Сохранено' : 'Сохранить'}
            </button>
            <Button variant="secondary" leftIcon="fileText" onClick={() => setShowPrint(true)}>PDF</Button>
            <div style={{ width: 1, height: 24, background: 'var(--color-line-subtle)', margin: '0 2px' }} />
            <button onClick={onLogout} title="Выйти" style={histBtn(true)}><Icon name="logout" size={15} /></button>
          </div>
        </header>

        {/* Body */}
        <div className="tm-body" style={{ gridTemplateColumns: libCollapsed ? '44px minmax(0,1fr) 320px' : '248px minmax(0,1fr) 320px' }}>
          {libCollapsed ? (
            <aside className="tm-col" style={{ alignItems: 'center', padding: '10px 0' }}>
              <button onClick={() => setLibCollapsed(false)} title="Показать библиотеку" style={{ width: 28, height: 28, borderRadius: 6, border: '1px solid var(--color-line-subtle)', background: 'var(--color-surface)', color: 'var(--color-ink-500)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
                <Icon name="chevronRight" size={15} />
              </button>
              <div style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)', fontSize: 10.5, letterSpacing: '.08em', textTransform: 'uppercase', color: 'var(--color-ink-400)', marginTop: 12 }}>Библиотека</div>
            </aside>
          ) : (
            <aside className="tm-col tm-library">
              <PR.Library dragInfo={dragInfo} onAdd={addFromLib} onCollapse={() => setLibCollapsed(true)} />
            </aside>
          )}

          <main className="tm-col tm-timeline">
            <div className="tm-tl-head">
              <div>
                <div className="tm-tl-title">{phase === 'prep' ? 'Тайминг подготовки и монтажа' : 'Тайминг мероприятия · сетка'}</div>
                <div className="tm-tl-sub">{isEvent
                  ? D.fmt(eSpan.start) + '–' + D.fmt(eSpan.end) + ' · ' + D.dur(eSpan.total) + ' · ' + doc.eventGrid.tracks.length + ' колонок · ' + doc.eventGrid.cards.length + ' карточек'
                  : hasDays
                    ? days.length + ' дн. · ' + D.dur(prepTotalDur) + ' работы · ' + computed.filter(c => c.kind === 'row').length + ' блоков'
                    : D.fmt(span.start) + '–' + D.fmt(span.end) + ' · ' + D.dur(span.total) + ' · ' + computed.filter(c => c.kind === 'row').length + ' блоков'}</div>
              </div>
              {!isEvent && (
                <div style={{ display: 'flex', gap: 8 }}>
                  <Button variant="secondary" size="sm" leftIcon="calendar" onClick={addDay}>День</Button>
                  <Button variant="secondary" size="sm" leftIcon="plus" onClick={addSection}>Раздел</Button>
                  <Button variant="secondary" size="sm" leftIcon="plus" onClick={addRow}>Строка</Button>
                </div>
              )}
              {isEvent && (
                <div style={{ display: 'flex', gap: 8 }}>
                  <Button variant="secondary" size="sm" leftIcon="plus" onClick={gridOps.addTrack}>Колонка</Button>
                  <Button variant="secondary" size="sm" leftIcon="plus" onClick={gridOps.addSlot}>Слот</Button>
                </div>
              )}
            </div>

            {isEvent ? (
              <div style={{ flex: '1 1 auto', minHeight: 0 }}>
                <TG.EventGrid grid={doc.eventGrid} ops={gridOps} dragInfo={dragInfo} />
              </div>
            ) : (
            <div className="tm-tl-scroll" onDragOver={onListDragOver} onDrop={onDrop}
              onDragLeave={e => { if (e.target === e.currentTarget) setOverIdx(null); }}>
              {(() => {
                let dayCollapsed = false, secCollapsed = false, dayNum = 0;
                return computed.map((it, i) => {
                  if (it.kind === 'day') {
                    dayNum++; dayCollapsed = !!it.collapsed; secCollapsed = false;
                    return (
                      <div key={it.id} onDragOver={e => rowDragOver(e, i)}>
                        <P.DayRow item={it} dayNum={dayNum} onPatch={p => patch(it.id, p)} onDelete={() => del(it.id)} onGrab={grab} grabbed={grabbed === it.id} dragOver={dragOverFor(i)} collapsed={!!it.collapsed} onToggle={() => toggleCollapse(it.id)} subtotal={dayMeta[i]} />
                      </div>
                    );
                  }
                  if (it.kind === 'section') {
                    if (dayCollapsed) return null;
                    secCollapsed = !!it.collapsed;
                    return (
                      <div key={it.id} onDragOver={e => rowDragOver(e, i)}>
                        <P.SectionRow item={it} onPatch={p => patch(it.id, p)} onDelete={() => del(it.id)} onGrab={grab} grabbed={grabbed === it.id} dragOver={dragOverFor(i)} collapsed={!!it.collapsed} onToggle={() => toggleCollapse(it.id)} subtotal={sectionMeta[i]} />
                      </div>
                    );
                  }
                  if (dayCollapsed || secCollapsed) return null;
                  return (
                    <div key={it.id} onDragOver={e => rowDragOver(e, i)}>
                      {it._gap > 0 && <P.GapBar minutes={it._gap} />}
                      <P.TimingRow item={it} idx={i} onPatch={p => patch(it.id, p)} onDelete={() => del(it.id)} onDup={() => dup(it.id)} onGrab={grab} grabbed={grabbed === it.id} dragOver={dragOverFor(i)} onAddAfter={addRowAfter} autoFocusId={focusId.current} />
                    </div>
                  );
                });
              })()}
              <div data-list-pad className="tm-tl-pad">
                {items.length === 0
                  ? <div className="tm-empty">Перетащите блоки из библиотеки слева<br />или нажмите «Строка»</div>
                  : <button className="tm-add-inline" onClick={addRow}><Icon name="plus" size={13} /> Добавить строку</button>}
              </div>
            </div>
            )}
          </main>

          <aside className="tm-col tm-summary">
            {isEvent
              ? <PR.GridSummary doc={doc} grid={doc.eventGrid} span={eSpan} onMeta={setMeta} />
              : <PR.Summary doc={doc} phase={phase} computed={computed} span={span} onMeta={setMeta} onPhaseStart={setPhaseStart} days={days} />}
          </aside>
        </div>

        {/* Print preview (portal → body, so print isolation works) */}
        {showPrint && (() => {
          const PAGE = { print: { portrait: [210, 297], landscape: [297, 210] }, screen: { portrait: [202.5, 360], landscape: [360, 202.5] } };
          const dims = PAGE[printFormat][printOrient];
          const pmargin = printFormat === 'screen' ? 10 : 14;
          const pageStyle = { width: dims[0] + 'mm', minHeight: dims[1] + 'mm', padding: pmargin + 'mm' };
          const pageCss = '@page{size:' + dims[0] + 'mm ' + dims[1] + 'mm;margin:' + pmargin + 'mm;}';
          const seg = active => ({ padding: '6px 12px', border: 0, borderRadius: 6, fontSize: 12.5, fontWeight: active ? 600 : 500, whiteSpace: 'nowrap', background: active ? 'var(--color-surface)' : 'transparent', color: active ? 'var(--color-ink-900)' : 'var(--color-ink-500)', boxShadow: active ? '0 1px 2px rgba(20,18,14,.06)' : 'none' });
          const segWrap = { display: 'inline-flex', gap: 3, background: 'var(--color-muted-50)', borderRadius: 9, padding: 3 };
          return ReactDOM.createPortal(
            <div className="print-overlay">
              <style>{pageCss}</style>
              <div className="print-toolbar">
                <div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <Icon name="fileText" size={15} color="var(--color-ink-500)" />
                    <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-ink-900)' }}>Экспорт в PDF</span>
                  </div>
                  <div style={segWrap}>
                    <button style={seg(printFormat === 'print')} onClick={() => setPrintFormat('print')}>Под печать · A4</button>
                    <button style={seg(printFormat === 'screen')} onClick={() => setPrintFormat('screen')}>Под экран · 16:9</button>
                  </div>
                  <div style={segWrap}>
                    <button style={seg(printOrient === 'portrait')} onClick={() => setPrintOrient('portrait')}>Вертикально</button>
                    <button style={seg(printOrient === 'landscape')} onClick={() => setPrintOrient('landscape')}>Горизонтально</button>
                  </div>
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                  <span style={{ fontSize: 11.5, color: 'var(--color-ink-400)', maxWidth: 230, lineHeight: 1.35 }}>2 листа: подготовка и мероприятие. В диалоге печати — «Сохранить как PDF».</span>
                  <Button variant="secondary" leftIcon="x" onClick={() => setShowPrint(false)}>Закрыть</Button>
                  <Button variant="primary" leftIcon="fileText" onClick={() => window.print()}>Печать / PDF</Button>
                </div>
              </div>
              <div className="print-scroll">
                <PR.PrintDoc doc={doc} pageStyle={pageStyle} />
              </div>
            </div>,
            document.body
          );
        })()}

        {pendingTpl && ReactDOM.createPortal(
          <div className="print-overlay" style={{ alignItems: 'center', justifyContent: 'center' }}>
            <div style={{ background: 'var(--color-elevated)', borderRadius: 'var(--radius-modal)', boxShadow: 'var(--shadow-modal)', padding: 22, width: 380, maxWidth: '90vw' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                <Icon name="alertCircle" size={16} color="var(--color-warning-500)" />
                <span style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-ink-900)' }}>Заменить текущий тайминг?</span>
              </div>
              <div style={{ fontSize: 13, color: 'var(--color-ink-500)', lineHeight: 1.5, marginBottom: 18 }}>
                Загрузка шаблона перезапишет все текущие блоки, сетку и шапку документа. Действие можно отменить через ⌘Z.
              </div>
              <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
                <Button variant="secondary" onClick={() => setPendingTpl(null)}>Отмена</Button>
                <Button variant="primary" onClick={() => applyTemplate(pendingTpl)}>Загрузить шаблон</Button>
              </div>
            </div>
          </div>,
          document.body
        )}

        {showProjects && <PJ.ProjectsPanel onClose={() => setShowProjects(false)} onOpen={openProject} onNew={newProject} currentId={currentProject ? currentProject.id : null} />}

        {showClear && ReactDOM.createPortal(
          <div className="print-overlay" style={{ alignItems: 'center', justifyContent: 'center' }} onMouseDown={e => { if (e.target === e.currentTarget) setShowClear(false); }}>
            <div style={{ background: 'var(--color-elevated)', borderRadius: 'var(--radius-modal)', boxShadow: 'var(--shadow-modal)', padding: 22, width: 380, maxWidth: '90vw' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                <Icon name="alertCircle" size={16} color="var(--color-danger-700)" />
                <span style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-ink-900)' }}>Очистить весь тайминг и начать с нуля?</span>
              </div>
              <div style={{ fontSize: 13, color: 'var(--color-ink-500)', lineHeight: 1.5, marginBottom: 18 }}>
                Текущий документ будет сброшен к пустому. Действие необратимо. Сохранённые проекты не затрагиваются.
              </div>
              <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
                <Button variant="secondary" onClick={() => setShowClear(false)}>Отмена</Button>
                <Button variant="danger" onClick={clearDoc}>Очистить</Button>
              </div>
            </div>
          </div>,
          document.body
        )}
      </div>
    );
  }

  /* ============ Гейт авторизации ============ */
  function AuthGate() {
    const [status, setStatus] = useState('checking');
    const [user, setUser] = useState(null);
    useEffect(() => {
      let on = true;
      (async () => {
        const r = await A.checkAuth();
        if (!on) return;
        if (r.authed) { setUser(r.user); setStatus('authed'); }
        else setStatus('login');
      })();
      return () => { on = false; };
    }, []);
    if (status === 'checking') {
      return (
        <div style={{ minHeight: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--color-canvas)' }}>
          <span className="tm-spin" style={{ width: 22, height: 22, border: '2.5px solid var(--color-line-strong)', borderTopColor: 'var(--color-accent-500)', borderRadius: '50%' }} />
        </div>
      );
    }
    if (status === 'login') return <A.LoginScreen onSuccess={u => { setUser(u); setStatus('authed'); }} />;
    return <TimingConstructor user={user} onLogout={() => { A.api.logout(); setStatus('login'); }} />;
  }

  window.TimingApp = AuthGate;
})();
