// ============================================================
// Main app entry — wires state, navigation, dice overlay, tweaks
// Multi-character with localStorage + Firebase persistence.
// ============================================================

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "auto",
  "accent": "#d4a857",
  "showActionBar": true,
  "ornaments": true,
  "density": "comfy"
}/*EDITMODE-END*/;

const LS_KEY    = 'grimoire.v4';
const LS_KEY_V3 = 'grimoire.v3';
// Унікальний ID цієї сесії/браузера — щоб не застосовувати власні зміни як "зовнішні"
const SESSION_ID = Math.random().toString(36).slice(2, 10);
// ⚠ Замінити після `npx partykit deploy` — скопіювати URL з виводу команди
const PARTY_HOST = 'grimoire.archenok1.partykit.dev';

// Застосовує безпечні дефолти до об'єкта персонажа
const processCharacter = (c) => {
  const { _by, _ts, ...rest } = (c || {});
  const base = {
    inventory:      [],
    currency:       { PP: 0, GP: 0, EP: 0, SP: 0, CP: 0 },
    featureCharges: {},
    spellSlots:     SPELLS_GROUPED.reduce((acc, g) => { if (g.slots) acc[g.level] = { ...g.slots }; return acc; }, {}),
    sessions:       [],
    customAttacks:  [],
    customFeatures: [],
    customSpells:   [],
    spellNotes:     {},
    domainSlots:    {},
    lore:           '',
    activeBuffs:    [],
    levelSystem:    'xp',
    spellPrepared:  {},
    abilityScores:  { STR: 10, DEX: 10, CON: 10, INT: 10, WIS: 10, CHA: 10 },
    conditions:     {},
    actionUsed:     { action: false, bonus: false, reaction: false },
    skillProfs:     {},
    saveProfs:      {},
    combatState:    { concSpell: null, round: 1, activeInitName: null, extraInit: [], playerRolledInit: null },
    ...rest,
  };
  // Firebase видаляє порожні масиви/об'єкти — санітизуємо після merge
  if (!Array.isArray(base.inventory)) base.inventory = [];
  if (!base.domainSlots || typeof base.domainSlots !== 'object' || Array.isArray(base.domainSlots)) base.domainSlots = {};
  if (!base.combatState || typeof base.combatState !== 'object') base.combatState = { concSpell: null, round: 1, activeInitName: null, extraInit: [], playerRolledInit: null };
  if (!Array.isArray(base.combatState.extraInit)) base.combatState.extraInit = [];
  return base;
};

const loadSaved = () => {
  try {
    // Спробуємо v4 спочатку
    const raw = localStorage.getItem(LS_KEY);
    if (raw) {
      const data = JSON.parse(raw);
      if (Array.isArray(data.characters) && data.activeId) {
        data.characters = data.characters.map(processCharacter);
        return { ...data, activeCampaignId: data.activeCampaignId || 'fadleburg' };
      }
    }
    // Fallback на v3
    const rawV3 = localStorage.getItem(LS_KEY_V3);
    if (rawV3) {
      const data = JSON.parse(rawV3);
      if (Array.isArray(data.characters) && data.activeId) {
        data.characters = data.characters.map(processCharacter);
        return { ...data, activeCampaignId: 'fadleburg' };
      }
      if (data.ch) return { characters: [processCharacter({ ...INITIAL_CHARACTER, ...data.ch })], activeId: data.ch.id || INITIAL_CHARACTER.id, diceHistory: data.diceHistory || [], activeCampaignId: 'fadleburg' };
    }
    return null;
  } catch (e) { return null; }
};

const INITIAL_ROSTER = {
  characters: [INITIAL_CHARACTER, IVI_CHARACTER, KORVIN_CHARACTER],
  activeId: INITIAL_CHARACTER.id,
  diceHistory: [],
};

// ============ AUTH ============
const _h = (pin) => {
  let h = 5381;
  for (const c of pin) h = (((h << 5) + h) ^ c.charCodeAt(0)) >>> 0;
  return h.toString(16).padStart(8, '0');
};

const CAMPAIGNS = [
  { id: 'fadleburg', name: 'Фадлебург',    icon: '⚔' },
  { id: 'zhora',     name: 'Кампанія Жори', icon: '🎲' },
];

// campaigns: null → ДМ або адмін (бачить усе); campaigns: [{campaignId, charIds}] → гравець
const USERS = [
  { id: 'gm',    name: 'Жора',   role: 'ДМ',            pinHash: '7c542d28', campaigns: null },
  { id: 'ruslan', name: 'Руслан', role: 'Корвін',        pinHash: '7c51de87', campaigns: [{ campaignId: 'fadleburg', charIds: ['korvin'] }] },
  { id: 'yehor',  name: 'Єгор',   role: 'Фінрад',        pinHash: '7c5493c6', campaigns: [{ campaignId: 'fadleburg', charIds: ['finrad'] }] },
  { id: 'masha',  name: 'Маша',   role: 'Іві',           pinHash: '7c57cd4a', campaigns: [{ campaignId: 'fadleburg', charIds: ['ivi'] }] },
  { id: 'admin',  name: 'Адмін',  role: 'Адміністратор', pinHash: '7c537b05', campaigns: null }, // PIN: 0000
];

const LoginScreen = ({ users, onLogin }) => {
  const [userId, setUserId] = useState(users[0]?.id || '');
  const [pin, setPin] = useState('');
  const [error, setError] = useState('');
  const [shake, setShake] = useState(false);

  const doLoginWith = (p) => {
    const user = users.find(u => u.id === userId);
    if (user && _h(p) === user.pinHash) {
      onLogin(user);
    } else {
      setError('Невірний PIN. Спробуй ще раз.');
      setPin('');
      setShake(true);
      setTimeout(() => setShake(false), 500);
    }
  };
  const doLogin = () => doLoginWith(pin);

  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', background: 'var(--bg-0)' }}>
      <div style={{
        width: 340, background: 'var(--bg-elv)', border: '1px solid var(--gold-2)',
        borderRadius: 'var(--radius-lg)', padding: '36px 32px',
        boxShadow: '0 24px 80px rgba(0,0,0,0.6), 0 0 60px var(--gold-glow)',
        animation: shake ? 'shake 0.45s ease' : 'login-in 0.35s cubic-bezier(.2,.8,.2,1)',
      }}>
        <div style={{ textAlign: 'center', marginBottom: 28 }}>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 30, letterSpacing: '0.28em', color: 'var(--gold-1)' }}>GRIMOIRE</div>
          <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', marginTop: 6, letterSpacing: '0.12em' }}>D&D Character Tracker</div>
        </div>

        <div style={{ marginBottom: 16 }}>
          <div className="field-label mb-4">Хто ти?</div>
          <select className="input" value={userId} onChange={e => { setUserId(e.target.value); setPin(''); setError(''); }}>
            {users.map(u => <option key={u.id} value={u.id}>{u.name} — {u.role}</option>)}
          </select>
        </div>

        <div style={{ marginBottom: 20 }}>
          <div className="field-label mb-4">PIN-код</div>
          <input className="input" type="password" inputMode="numeric" maxLength={4} placeholder="• • • •"
            value={pin}
            onChange={e => { const v = e.target.value.replace(/\D/g, '').slice(0, 4); setPin(v); setError(''); if (v.length === 4) doLoginWith(v); }}
            onKeyDown={e => e.key === 'Enter' && doLogin()}
            style={{ textAlign: 'center', fontSize: 22, letterSpacing: '0.5em' }}/>
        </div>

        {error && <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--crimson-1)', textAlign: 'center', marginBottom: 14 }}>{error}</div>}

        <Btn primary onClick={doLogin} style={{ width: '100%', justifyContent: 'center' }}>Увійти</Btn>
      </div>
    </div>
  );
};



const App = () => {
  const { push } = useToast();
  const [characters, setCharacters] = useState([]);
  const [activeId, setActiveId] = useState(null);
  const [activeCampaignId, setActiveCampaignId] = useState('fadleburg');
  const [active, setActive] = useState('character');
  // ── PartyKit ──
  const partyRef = useRef(null);
  const activeCampaignIdRef = useRef(activeCampaignId);
  useEffect(() => { activeCampaignIdRef.current = activeCampaignId; }, [activeCampaignId]);
  // ── Config ──
  const [configUsers,     setConfigUsers]     = useState(null);
  const [configCampaigns, setConfigCampaigns] = useState(null);
  const [configMembers,   setConfigMembers]   = useState({});
  const [campaignInfo, setCampaignInfoRaw] = useState({ description: '', lore: '', dmNotes: '' });
  const campaignInfoRef = useRef(campaignInfo);
  useEffect(() => { campaignInfoRef.current = campaignInfo; }, [campaignInfo]);
  const setCampaignInfo = (updater) => setCampaignInfoRaw(prev => {
    const next = typeof updater === 'function' ? updater(prev) : { ...prev, ...updater };
    return next;
  });

  const [diceOpen, setDiceOpen] = useState(false);
  const [diceHistory, setDiceHistory] = useState([]);
  const [timeline, setTimeline] = useState(TIMELINE);
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [savedAt, setSavedAt] = useState(null);
  const saveTimer   = useRef(null);
  // 'connecting' | 'loading' | 'ready' | 'error'
  const [syncState, setSyncState] = useState('connecting');
  const syncedRef = useRef(false);
  const justLoadedRef = useRef(false);
  const savedActiveIdRef = useRef(null);
  const currentUserRef = useRef(null);
  const timelineRef = useRef(timeline);
  const charactersRef = useRef(characters);
  useEffect(() => { timelineRef.current = timeline; }, [timeline]);
  useEffect(() => { charactersRef.current = characters; }, [characters]);

  // ── Helpers: config seed ──
  const buildSeedConfig = () => {
    const users = {}; USERS.forEach(u => { users[u.id] = { id: u.id, name: u.name, role: u.role, pinHash: u.pinHash }; });
    const campaigns = {}; CAMPAIGNS.forEach(c => { campaigns[c.id] = c; });
    const members = {};
    USERS.forEach(u => {
      if (u.id === 'admin') return;
      if (u.campaigns === null) {
        CAMPAIGNS.forEach(c => { if (!members[c.id]) members[c.id] = {}; members[c.id][u.id] = { isDM: true }; });
      } else {
        (u.campaigns || []).forEach(cp => {
          if (!members[cp.campaignId]) members[cp.campaignId] = {};
          members[cp.campaignId][u.id] = { isDM: false, charIds: (cp.charIds || []).join(',') };
        });
      }
    });
    return { users, campaigns, members };
  };
  const applyConfig = (cfg) => {
    if (cfg.users)     setConfigUsers(Array.isArray(cfg.users)     ? cfg.users     : Object.values(cfg.users));
    if (cfg.campaigns) setConfigCampaigns(Array.isArray(cfg.campaigns) ? cfg.campaigns : Object.values(cfg.campaigns));
    if (cfg.members)   setConfigMembers(cfg.members);
  };
  // Формує об'єкт конфігу для відправки в PartyKit
  const currentConfigObj = (overrideUsers, overrideCampaigns, overrideMembers) => ({
    users:     Object.fromEntries((overrideUsers     ?? configUsers     ?? []).map(u => [u.id, u])),
    campaigns: Object.fromEntries((overrideCampaigns ?? configCampaigns ?? []).map(c => [c.id, c])),
    members:   overrideMembers ?? configMembers ?? {},
  });
  const sendConfig = (cfg) => partyRef.current?.send(JSON.stringify({ type: 'config_update', config: cfg }));

  // ── PartyKit: підключення, обробка повідомлень ──
  useEffect(() => {
    let ws = null;
    let dead = false;
    const listeners = {};
    let campaignSyncTimer = null;

    const party = {
      send: (data) => {
        const type = (() => { try { return JSON.parse(data).type; } catch { return '?'; } })();
        console.log('[Grimoire] send:', type, '| ws.readyState:', ws?.readyState);
        if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
        else console.warn('[Grimoire] send BLOCKED — ws не відкритий');
      },
      addEventListener: (type, fn) => { listeners[type] = fn; },
      close: () => { dead = true; clearTimeout(campaignSyncTimer); ws && ws.close(); },
    };
    partyRef.current = party;

    const requestCampaign = () => {
      party.send(JSON.stringify({ type: 'request_campaign', campaignId: activeCampaignIdRef.current }));
      // Якщо campaign_sync не прийшов за 10 сек — помилка
      clearTimeout(campaignSyncTimer);
      campaignSyncTimer = setTimeout(() => {
        console.error('[Grimoire] campaign_sync timeout — server not responding');
        setSyncState('error');
      }, 10000);
    };

    const connect = () => {
      if (dead) return;
      setSyncState('connecting');
      ws = new WebSocket(`wss://${PARTY_HOST}/party/grimoire`);
      ws.onopen    = () => {
        console.log('[Grimoire] WS connected');
        const u = currentUserRef.current;
        if (u) party.send(JSON.stringify({ type: 'presence', userId: u.id, name: u.name, role: u.role, ts: Date.now() }));
      };
      ws.onmessage = (e) => listeners['message']?.(e);
      ws.onerror   = (e) => { console.warn('[Grimoire] WS error', e); setSyncState('error'); };
      ws.onclose   = () => {
        if (!dead) {
          console.log('[Grimoire] WS closed, reconnecting in 3s...');
          setTimeout(connect, 3000);
        }
      };
    };
    connect();

    party.addEventListener('message', (event) => {
      let msg; try { msg = JSON.parse(event.data); } catch { return; }
      console.log('[Grimoire] msg:', msg.type);

      switch (msg.type) {
        case 'config_sync': {
          setSyncState('loading');
          if (!msg.config) {
            const seed = buildSeedConfig();
            applyConfig(seed);
            party.send(JSON.stringify({ type: 'config_update', config: seed }));
          } else {
            applyConfig(msg.config);
          }
          requestCampaign();
          break;
        }
        case 'campaign_sync': {
          if (msg.campaignId !== activeCampaignIdRef.current) break;
          clearTimeout(campaignSyncTimer);
          const allChars = Object.entries(msg.characters || {})
            .filter(([, v]) => v && typeof v === 'object' && v.id && v.name)
            .map(([, v]) => processCharacter(v));
          const isFadleburg = msg.campaignId === 'fadleburg';
          if (isFadleburg) {
            INITIAL_ROSTER.characters.forEach(t => {
              if (!allChars.find(c => c.id === t.id)) allChars.push({ ...t });
            });
          }
          const rosterIds = INITIAL_ROSTER.characters.map(c => c.id);
          const sorted = isFadleburg
            ? [...rosterIds.map(id => allChars.find(c => c.id === id)).filter(Boolean), ...allChars.filter(c => !rosterIds.includes(c.id))]
            : allChars;
          syncedRef.current = true;
          justLoadedRef.current = true;
          setCharacters(sorted.length > 0 ? sorted : []);
          const firstId = sorted[0]?.id ?? null;
          if (savedActiveIdRef.current) {
            const found = sorted.find(c => c.id === savedActiveIdRef.current);
            setActiveId(found ? savedActiveIdRef.current : firstId);
          } else {
            setActiveId(firstId);
          }
          if (Array.isArray(msg.timeline) && msg.timeline.length > 0) setTimeline(msg.timeline);
          else setTimeline(TIMELINE);
          setCampaignInfo(msg.campaignInfo || { description: '', lore: '', dmNotes: '' });
          setSyncState('ready');
          break;
        }
        case 'config_update': {
          applyConfig(msg.config);
          break;
        }
        case 'char_update': {
          if (msg._by === SESSION_ID || msg.campaignId !== activeCampaignIdRef.current) break;
          const d = msg.data;
          if (!d?.id || !d?.name) break;
          setCharacters(cs => {
            const existing = cs.find(c => c.id === d.id);
            const portrait = existing?.portrait ?? null;
            if (existing) return cs.map(c => c.id === d.id ? { ...processCharacter(d), portrait } : c);
            return [...cs, { ...processCharacter(d), portrait: null }];
          });
          break;
        }
        case 'char_delete': {
          if (msg.campaignId !== activeCampaignIdRef.current) break;
          setCharacters(cs => cs.filter(c => c.id !== msg.charId));
          break;
        }
        case 'timeline_update': {
          if (msg._by === SESSION_ID || msg.campaignId !== activeCampaignIdRef.current) break;
          if (Array.isArray(msg.timeline)) setTimeline(msg.timeline);
          break;
        }
        case 'portrait_update': {
          if (msg._by === SESSION_ID) break;
          setCharacters(cs => cs.map(c => c.id === msg.charId ? { ...c, portrait: msg.portrait ?? null } : c));
          break;
        }
        case 'presence': {
          setOnlineUsers(prev => {
            if (!msg.userId) return prev;
            if (!msg.name) { const n = { ...prev }; delete n[msg.userId]; return n; }
            return { ...prev, [msg.userId]: { name: msg.name, role: msg.role, ts: msg.ts } };
          });
          // Відповідаємо своєю присутністю щоб новий учасник нас побачив
          const me = currentUserRef.current;
          if (me && msg.userId !== me.id) {
            party.send(JSON.stringify({ type: 'presence', userId: me.id, name: me.name, role: me.role, ts: Date.now() }));
          }
          break;
        }
        case 'campaign_info_update': {
          if (msg._by === SESSION_ID || msg.campaignId !== activeCampaignIdRef.current) break;
          setCampaignInfo(msg.info || {});
          break;
        }
        case 'bulk_import_done': {
          push({ label: '✅ Міграцію завершено', detail: 'Дані перенесено до PartyKit' });
          requestCampaign();
          break;
        }
        case 'storage_reset_done': {
          push({ label: '✅ Сховище скинуто' });
          requestCampaign();
          break;
        }
        case 'campaign_reset': {
          if (msg.campaignId !== activeCampaignIdRef.current) break;
          requestCampaign();
          break;
        }
      }
    });

    party.addEventListener('error', (err) => console.warn('[Grimoire] PartyKit error:', err));

    return () => { partyRef.current = null; party.close(); };
  }, []);  // eslint-disable-line react-hooks/exhaustive-deps

  // Ефективні списки (Firebase або хардкод-fallback)
  const effectiveUsers    = configUsers    ?? USERS.map(u => ({ id: u.id, name: u.name, role: u.role, pinHash: u.pinHash }));
  const effectiveCampaigns = configCampaigns ?? CAMPAIGNS;

  // ── Авторизація ──
  const [currentUser, setCurrentUser] = useState(() => {
    try { const s = sessionStorage.getItem('grimoire.session'); if (s) return JSON.parse(s); } catch(e) {}
    return null;
  });
  const handleLogin = (user) => {
    sessionStorage.setItem('grimoire.session', JSON.stringify(user));
    setCurrentUser(user);
    setActive('character'); // скидаємо вкладку при кожному логіні
    if (user.id === 'admin') { setActive('admin'); setSyncState('ready'); return; }
    // Знаходимо перші кампанії де юзер є учасником
    const accessibleCamps = effectiveCampaigns.filter(c => configMembers[c.id]?.[user.id] !== undefined);
    if (accessibleCamps.length > 0) {
      const campId = accessibleCamps[0].id;
      setActiveCampaignId(campId);
      const m = configMembers[campId]?.[user.id];
      if (!m?.isDM && m?.charIds) {
        const ids = m.charIds.split(',').map(s => s.trim()).filter(Boolean);
        if (ids.length >= 1) setActiveId(ids[0]);
      }
    }
  };
  const handleLogout = () => {
    if (currentUser) {
      partyRef.current?.send(JSON.stringify({ type: 'presence', userId: currentUser.id, name: null }));
    }
    sessionStorage.removeItem('grimoire.session');
    setCurrentUser(null);
  };

  useEffect(() => {
    if (!currentUser || currentUser.id === 'admin') return;
    const m = configMembers[activeCampaignId]?.[currentUser.id];
    if (!m || m.isDM) return;
    const charIds = m.charIds ? m.charIds.split(',').map(s => s.trim()).filter(Boolean) : [];
    if (charIds.length > 0 && !charIds.includes(activeId)) setActiveId(charIds[0]);
  }, [currentUser, activeCampaignId, configMembers]);

  // ── Онлайн-присутність ──
  const [onlineUsers, setOnlineUsers] = useState({});
  useEffect(() => {
    currentUserRef.current = currentUser;
    if (!currentUser || !partyRef.current) return;
    // WS може ще не бути відкритим — надсилаємо тільки якщо open
    partyRef.current.send(JSON.stringify({
      type: 'presence', userId: currentUser.id, name: currentUser.name, role: currentUser.role, ts: Date.now(),
    }));
  }, [currentUser?.id]);

  // ── Поточне членство і видимість персів ──
  const currentMember = currentUser ? (configMembers[activeCampaignId]?.[currentUser.id] ?? null) : null;
  const isDM = currentMember?.isDM === true;
  // isDM хоча б в одній кампанії — для показу списку всіх кампаній у сайдбарі
  const isGlobalDM = currentUser?.id === 'admin' || isDM ||
    Object.values(configMembers).some(camp => camp[currentUser?.id]?.isDM === true);
  const currentMemberCharIds = currentMember?.charIds
    ? currentMember.charIds.split(',').map(s => s.trim()).filter(Boolean)
    : [];
  const visibleCharacters = !currentUser ? [] : (currentUser.id === 'admin' || isDM)
    ? characters
    : characters.filter(c => currentMemberCharIds.includes(c.id) || c.ownerId === currentUser.id);

  // ── Admin ──
  // Персонажі що належать до кожної кампанії (для одноразової міграції з v3)
  // ── Міграція Firebase → PartyKit (одноразова, запускається з адмін-панелі) ──
  // Які charId належать якій кампанії
  const MIGRATION_MAP = { fadleburg: ['finrad', 'ivi', 'korvin'], zhora: ['ch-nb4uo8'] };
  const migrateFirebaseToV4 = async () => {
    const DB = window._db;
    if (!DB) { push({ label: '⚠ Firebase не підключений' }); return; }
    if (!partyRef.current) { push({ label: '⚠ PartyKit не підключений' }); return; }
    push({ label: 'Читаємо Firebase…' });
    try {
      const campaigns = {};
      for (const [cid] of Object.entries(MIGRATION_MAP)) {
        const charsSnap = await DB.ref(`grimoire/campaigns/${cid}/characters`).once('value');
        const tlSnap    = await DB.ref(`grimoire/campaigns/${cid}/timeline`).once('value');
        const rawChars  = charsSnap.val() || {};
        // Видаляємо портрети — вони занадто великі для WebSocket (>1MB)
        const characters = {};
        for (const [id, ch] of Object.entries(rawChars)) {
          const { portrait: _p, ...rest } = ch;
          characters[id] = rest;
        }
        campaigns[cid] = {
          characters,
          timeline: tlSnap.val() || [],
          portraits: {},
        };
      }

      partyRef.current.send(JSON.stringify({ type: 'bulk_import', config: null, campaigns }));
      push({ label: 'Дані відправлено…', detail: 'Очікуємо підтвердження від PartyKit' });
    } catch (e) {
      push({ label: '⚠ Помилка міграції', detail: e.message });
    }
  };

  // ── User CRUD ──
  const createUser = (id, name, role, pinHash) => {
    const u = { id, name, role, pinHash };
    const newUsers = [...(configUsers || []), u];
    setConfigUsers(newUsers);
    sendConfig(currentConfigObj(newUsers, null, null));
  };
  const updateUser = (id, patch) => {
    const newUsers = (configUsers || []).map(u => u.id === id ? { ...u, ...patch } : u);
    setConfigUsers(newUsers);
    sendConfig(currentConfigObj(newUsers, null, null));
  };
  const deleteUser = (id) => {
    const newUsers = (configUsers || []).filter(u => u.id !== id);
    const newMembers = {};
    Object.entries(configMembers).forEach(([campId, ms]) => { newMembers[campId] = { ...ms }; delete newMembers[campId][id]; });
    setConfigUsers(newUsers);
    setConfigMembers(newMembers);
    sendConfig(currentConfigObj(newUsers, null, newMembers));
  };

  // ── Campaign CRUD ──
  const createCampaign = (id, name, icon) => {
    const campaign = { id, name, icon, createdAt: Date.now() };
    const newCampaigns = [...(configCampaigns ?? CAMPAIGNS), campaign];
    setConfigCampaigns(newCampaigns);
    sendConfig(currentConfigObj(null, newCampaigns, null));
  };
  const deleteCampaign = (id) => {
    const newCampaigns = (configCampaigns ?? CAMPAIGNS).filter(c => c.id !== id);
    const newMembers = { ...configMembers }; delete newMembers[id];
    setConfigCampaigns(newCampaigns);
    setConfigMembers(newMembers);
    sendConfig(currentConfigObj(null, newCampaigns, newMembers));
    if (activeCampaignId === id) setActiveCampaignId(effectiveCampaigns.find(c => c.id !== id)?.id || 'fadleburg');
  };

  // ── Member CRUD ──
  const addMember = (campaignId, userId, isDMFlag = false) => {
    const entry = { isDM: isDMFlag };
    const newMembers = { ...configMembers, [campaignId]: { ...(configMembers[campaignId] || {}), [userId]: entry } };
    setConfigMembers(newMembers);
    sendConfig(currentConfigObj(null, null, newMembers));
  };
  const removeMember = (campaignId, userId) => {
    const camp = { ...(configMembers[campaignId] || {}) }; delete camp[userId];
    const newMembers = { ...configMembers, [campaignId]: camp };
    setConfigMembers(newMembers);
    sendConfig(currentConfigObj(null, null, newMembers));
  };
  const setMemberDM = (campaignId, userId, isDMFlag) => {
    const newMembers = { ...configMembers, [campaignId]: { ...configMembers[campaignId], [userId]: { ...(configMembers[campaignId]?.[userId] || {}), isDM: isDMFlag } } };
    setConfigMembers(newMembers);
    sendConfig(currentConfigObj(null, null, newMembers));
  };
  const setMemberCharIds = (campaignId, userId, charIds) => {
    const newMembers = { ...configMembers, [campaignId]: { ...configMembers[campaignId], [userId]: { ...(configMembers[campaignId]?.[userId] || {}), charIds } } };
    setConfigMembers(newMembers);
    sendConfig(currentConfigObj(null, null, newMembers));
  };

  const downloadBackup = () => {
    const raw = localStorage.getItem(LS_KEY) || localStorage.getItem(LS_KEY_V3);
    if (!raw) return;
    const blob = new Blob([raw], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a'); a.href = url; a.download = 'grimoire-backup.json'; a.click();
    URL.revokeObjectURL(url);
  };

  // ── New-character modal ──
  const [newCharOpen, setNewCharOpen] = useState(false);
  const [newCharName, setNewCharName] = useState('');
  const confirmNewChar = () => {
    const name = newCharName.trim() || 'Новий герой';
    const c = { ...newBlankCharacter(name), ownerId: currentUser?.id || null };
    setCharacters(cs => [...cs, c]);
    setActiveId(c.id);
    const { portrait: _p, ...cNoPortrait } = c;
    partyRef.current?.send(JSON.stringify({ type: 'char_update', campaignId: activeCampaignId, charId: c.id, data: cNoPortrait, _by: SESSION_ID }));
    // Автопризначення нового персонажа до поточного гравця (якщо не ДМ)
    if (currentUser && !isDM && currentUser.id !== 'admin') {
      const newCharIds = [...currentMemberCharIds, c.id].join(',');
      setMemberCharIds(activeCampaignId, currentUser.id, newCharIds);
    }
    setActive('character');
    setNewCharOpen(false);
    setNewCharName('');
  };

  // ── Reset / clear confirm modals ──
  const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
  const [clearConfirmOpen, setClearConfirmOpen] = useState(false);

  // ── Delete/confirm modal ──
  const [deleteTarget, setDeleteTarget] = useState(null);
  const confirmDelete = () => {
    const id = deleteTarget.id;
    partyRef.current?.send(JSON.stringify({ type: 'char_delete', campaignId: activeCampaignId, charId: id }));
    const remaining = characters.filter(c => c.id !== id);
    setCharacters(remaining);
    if (activeId === id && remaining.length > 0) setActiveId(remaining[0].id);
    setDeleteTarget(null);
  };

  const ch = characters.find(c => c.id === activeId) || characters[0] || null;
  const setCh = (updater) => {
    setCharacters(cs => cs.map(c => {
      if (c.id !== activeId) return c;
      return typeof updater === 'function' ? updater(c) : { ...c, ...updater };
    }));
  };

  // ── При зміні кампанії — запитуємо нові дані ──
  const isCampaignMountRef = useRef(true);
  useEffect(() => {
    if (isCampaignMountRef.current) { isCampaignMountRef.current = false; return; }
    syncedRef.current = false;
    setSyncState('loading');
    savedActiveIdRef.current = activeId;
    partyRef.current?.send(JSON.stringify({ type: 'request_campaign', campaignId: activeCampaignId }));
  }, [activeCampaignId]);

  // ============ Auto-save ============
  useEffect(() => {
    if (!syncedRef.current) return;
    if (justLoadedRef.current) { justLoadedRef.current = false; return; }
    if (saveTimer.current) clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => {
      // Надсилаємо кожного персонажа в PartyKit
      characters.forEach(c => {
        const { portrait: _p, ...noPortrait } = c;
        partyRef.current?.send(JSON.stringify({
          type: 'char_update', campaignId: activeCampaignId, charId: c.id,
          data: { ...noPortrait, _by: SESSION_ID, _ts: Date.now() }, _by: SESSION_ID,
        }));
      });
      try {
        localStorage.setItem(LS_KEY, JSON.stringify({ characters: charactersRef.current, activeId, activeCampaignId, diceHistory, timeline: timelineRef.current, ts: Date.now() }));
      } catch (e) { /* quota */ }
      setSavedAt(Date.now());
    }, 800);
    return () => clearTimeout(saveTimer.current);
  }, [characters, activeId, diceHistory]);

  // campaignInfo — PartyKit sync
  useEffect(() => {
    if (!syncedRef.current) return;
    const t = setTimeout(() => {
      partyRef.current?.send(JSON.stringify({ type: 'campaign_info_update', campaignId: activeCampaignId, info: campaignInfoRef.current, _by: SESSION_ID }));
    }, 800);
    return () => clearTimeout(t);
  }, [campaignInfo]);

  // Хронологія — PartyKit sync
  useEffect(() => {
    if (!syncedRef.current) return;
    const t = setTimeout(() => {
      partyRef.current?.send(JSON.stringify({ type: 'timeline_update', campaignId: activeCampaignId, timeline, _by: SESSION_ID }));
    }, 800);
    return () => clearTimeout(t);
  }, [timeline]);

  // ── Портрет — надсилається при зміні ──
  useEffect(() => {
    if (!syncedRef.current || !ch) return;
    partyRef.current?.send(JSON.stringify({ type: 'portrait_update', charId: activeId, portrait: ch.portrait ?? null, _by: SESSION_ID }));
  }, [ch?.portrait, activeId]); // eslint-disable-line react-hooks/exhaustive-deps

  // ============ Character roster mgmt ============
  const addCharacter = () => { setNewCharName(''); setNewCharOpen(true); };
  const duplicateCharacter = () => {
    if (!ch) return;
    const c = { ...ch, id: 'ch-' + Math.random().toString(36).slice(2, 8), name: ch.name + ' (копія)' };
    setCharacters(cs => [...cs, c]);
    setActiveId(c.id);
    push({ label: `Дубльовано: ${c.name}` });
  };
  const deleteCharacter = (id) => {
    if (characters.length <= 1) { push({ label: '⚠ Не можна видалити', detail: 'Має лишитись хоча б один персонаж' }); return; }
    const c = characters.find(x => x.id === id);
    setDeleteTarget({ id, name: c.name });
  };
  const resetActive = () => setResetConfirmOpen(true);
  const confirmReset = () => {
    setCharacters(cs => cs.map(c => c.id === activeId ? { ...newBlankCharacter(ch?.name || ''), id: activeId } : c));
    push({ label: 'Персонажа скинуто' });
    setResetConfirmOpen(false);
  };
  const clearAll = () => setClearConfirmOpen(true);
  const confirmClearAll = () => {
    const resetChars = {};
    INITIAL_ROSTER.characters.forEach(c => { const { portrait: _p, ...cp } = c; resetChars[c.id] = cp; });
    partyRef.current?.send(JSON.stringify({ type: 'campaign_reset', campaignId: activeCampaignId, characters: resetChars, timeline: TIMELINE }));
    localStorage.removeItem(LS_KEY);
    // Скидаємо і user-created chars зі стейту
    setCharacters(INITIAL_ROSTER.characters.map(c => ({ ...c })));
    setActiveId(INITIAL_ROSTER.activeId);
    setDiceHistory([]);
    setTimeline([...TIMELINE]);
    setClearConfirmOpen(false);
    push({ label: 'Всі дані скинуто до початкового стану' });
  };

  // ============ Themes & accent ============
  const CLASS_THEMES = {
    'Чарівник':  'necromancer',
    'Чародій':   'bard',
    'Бард':      'bard',
    'Друид':     'druid',
    'Друід':     'druid',
    'Жрець':     'paladin',
    'Клерик':    'paladin',
    'Паладин':   'paladin',
    'Боєць':     'fighter',
    'Воїн':      'fighter',
    'Файтер':    'fighter',
    'Варвар':    'barbarian',
    'Розбійник': 'rogue',
    'Шахрай':    'rogue',
    'Слідопит':  'druid',
    'Рейнджер':  'druid',
    'Монах':     'light',
    'Чорнокнижник': 'necromancer',
    'Некромант': 'necromancer',
  };

  const ACCENT_MAP = { '#d4a857': '', '#c97d4a': 'copper', '#c7c3b8': 'silver', '#6cb070': 'emerald' };
  useEffect(() => {
    const root = document.documentElement;
    const themes = ['vellum', 'paladin', 'necromancer', 'light', 'druid', 'bard', 'rogue', 'barbarian', 'fighter'];
    root.removeAttribute('data-theme');

    let theme = t.theme;
    if (theme === 'auto') {
      theme = CLASS_THEMES[ch?.klass] || 'dark';
    }
    if (themes.includes(theme)) root.setAttribute('data-theme', theme);

    const accentName = ACCENT_MAP[String(t.accent).toLowerCase()] ?? '';
    if (accentName) root.setAttribute('data-accent', accentName);
    else root.removeAttribute('data-accent');

    root.setAttribute('data-ornaments', t.ornaments ? 'on' : 'off');
  }, [t.theme, t.accent, t.ornaments, ch?.klass]);

  const pushHistory = useCallback((entry) => {
    setDiceHistory(h => [entry, ...h].slice(0, 50));
  }, []);

  // ============ Global keyboard ============
  useEffect(() => {
    const onKey = (e) => {
      const tag = (e.target.tagName || '').toLowerCase();
      if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      if (e.key === 'r' || e.key === 'R') { e.preventDefault(); setDiceOpen(v => !v); }
      else if (e.key >= '1' && e.key <= '8') {
        const ids = ['campaign','notes','character','combat','skills','spells','inventory','features'];
        setActive(ids[parseInt(e.key, 10) - 1]);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  const Screen = {
    character: ScreenCharacter,
    combat:    ScreenCombat,
    skills:    ScreenSkills,
    spells:    ScreenSpells,
    inventory: ScreenInventory,
    features:  ScreenFeatures,
  }[active] || ScreenCharacter;
  const showCampaignScreen = active === 'campaign';

  const showActions = t.showActionBar && (active === 'combat' || active === 'spells');

  // ── Екрани завантаження / помилки (до логіну не показуються) ──
  const GrimoireScreen = ({ text, sub, children }) => (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'center', height:'100vh', flexDirection:'column', gap:16, background:'var(--bg-0)' }}>
      <div style={{ fontFamily:'var(--font-display)', fontSize:26, letterSpacing:'0.25em', color:'var(--gold-1)' }}>GRIMOIRE</div>
      {text && <div style={{ fontFamily:'var(--font-mono)', fontSize:12, color:'var(--ink-3)' }}>{text}</div>}
      {sub  && <div style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'var(--ink-4)' }}>{sub}</div>}
      {children}
    </div>
  );

  if (syncState === 'connecting') {
    return <GrimoireScreen text="з'єднання з PartyKit…" />;
  }

  if (!currentUser) return <LoginScreen users={effectiveUsers} onLogin={handleLogin}/>;

  // Адмін завжди бачить панель — не чекає БД
  const isAdmin = currentUser.id === 'admin';

  if (!isAdmin && syncState === 'error') {
    return (
      <GrimoireScreen text="⚠ Не вдалось підключитись до бази даних" sub="Перевір з'єднання або зачекай">
        <button className="btn btn-ghost" style={{ marginTop:8 }} onClick={() => { setSyncState('connecting'); window.location.reload(); }}>↺ Спробувати знову</button>
        <button className="btn btn-ghost" style={{ fontSize:12 }} onClick={handleLogout}>← Вийти</button>
      </GrimoireScreen>
    );
  }

  if (!isAdmin && syncState === 'loading') {
    return <GrimoireScreen text="завантаження даних кампанії…" />;
  }

  if (!isAdmin && characters.length === 0) {
    const campaign = CAMPAIGNS.find(c => c.id === activeCampaignId);
    return (
      <div style={{ display:'flex', alignItems:'center', justifyContent:'center', height:'100vh', flexDirection:'column', gap:16, background:'var(--bg-0)' }}>
        <div style={{ fontFamily:'var(--font-display)', fontSize:26, letterSpacing:'0.25em', color:'var(--gold-1)' }}>GRIMOIRE</div>
        <div style={{ fontFamily:'var(--font-mono)', fontSize:13, color:'var(--ink-2)' }}>{campaign?.icon} {campaign?.name}</div>
        <div style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'var(--ink-3)' }}>У цій кампанії ще немає персонажів</div>
        {currentUser?.campaigns === null && (
          <button className="btn btn-ghost" style={{ marginTop:8, fontSize:12 }}
            onClick={() => setActiveCampaignId('fadleburg')}>
            ← Повернутись до Фадлебургу
          </button>
        )}
      </div>
    );
  }

  return (
    <div className="app" data-screen-label={`Grimoire · ${active}`}>
      <Sidebar
        active={active}
        onNavigate={setActive}
        onOpenDice={() => setDiceOpen(true)}
        character={ch}
        characters={visibleCharacters}
        activeId={activeId}
        onSwitchCharacter={(id) => { setActiveId(id); setActive('character'); }}
        onAddCharacter={addCharacter}
        onDuplicateCharacter={isDM ? duplicateCharacter : null}
        onDeleteCharacter={(id) => {
          const target = characters.find(c => c.id === id);
          if (isDM || target?.ownerId === currentUser.id) deleteCharacter(id);
        }}
        canDeleteChar={(id) => {
          const target = characters.find(c => c.id === id);
          return isDM || target?.ownerId === currentUser.id;
        }}
        currentUser={currentUser}
        onlineUsers={onlineUsers}
        campaigns={isGlobalDM
          ? effectiveCampaigns
          : effectiveCampaigns.filter(c => configMembers[c.id]?.[currentUser.id] !== undefined)
        }
        activeCampaignId={activeCampaignId}
        onSwitchCampaign={isGlobalDM ? setActiveCampaignId : null}
        isAdmin={currentUser?.id === 'admin'}
      />
      <div className="app-main">
        <Topbar
          ch={ch}
          setCh={setCh}
          onOpenDice={() => setDiceOpen(true)}
          openCombat={() => setActive('combat')}
          showActions={showActions}
          savedAt={savedAt}
        />
        <div className="app-body fade-in" key={`${activeId}-${active}`}>
          {showCampaignScreen
            ? <ScreenCampaign
                campaignInfo={campaignInfo}
                setCampaignInfo={setCampaignInfo}
                isDM={isDM}
                isAdmin={isAdmin}
                campaigns={effectiveCampaigns}
                activeCampaignId={activeCampaignId}
                members={configMembers}
                users={effectiveUsers}
                characters={visibleCharacters}
              />
          : active === 'admin'
            ? <ScreenAdmin
                campaigns={effectiveCampaigns}
                members={configMembers}
                users={effectiveUsers}
                onlineUsers={onlineUsers}
                characters={characters}
                activeCampaignId={activeCampaignId}
                onCreateUser={createUser}
                onUpdateUser={updateUser}
                onDeleteUser={deleteUser}
                onCreateCampaign={createCampaign}
                onDeleteCampaign={deleteCampaign}
                onAddMember={addMember}
                onRemoveMember={removeMember}
                onSetMemberDM={setMemberDM}
                onSetMemberCharIds={setMemberCharIds}
                onMigrate={migrateFirebaseToV4}
                onDownloadBackup={downloadBackup}
                onStorageReset={() => {
                  if (!partyRef.current) { push({ label: '⚠ PartyKit не підключений' }); return; }
                  partyRef.current.send(JSON.stringify({ type: 'storage_reset' }));
                  push({ label: 'Скидаємо сховище…' });
                }}
              />
            : active === 'notes'
            ? <ScreenNotes ch={ch} setCh={setCh} timeline={timeline} setTimeline={setTimeline}/>
            : <Screen ch={ch} setCh={setCh} isAdmin={currentUser?.id === 'admin'}
                onConvertAllWeights={active === 'inventory' && isAdmin ? () => {
                  setCharacters(cs => cs.map(c => ({
                    ...c,
                    inventory: (Array.isArray(c.inventory) ? c.inventory : []).map(i => ({
                      ...i, wt: Math.round((i.wt || 0) * 0.453592 * 10) / 10,
                    })),
                  })));
                  push({ label: `⚖ Ваги переконвертовано для ${characters.length} персонажів` });
                } : undefined}
              />
          }
        </div>
      </div>

      <DiceOverlay
        open={diceOpen}
        onClose={() => setDiceOpen(false)}
        history={diceHistory}
        pushHistory={pushHistory}
        ch={ch}
      />

      <TweaksPanel title="Tweaks">
        <TweakSection label="Тема" />
        <TweakSelect
          label="Палітра"
          value={t.theme} onChange={(v) => setTweak('theme', v)}
          options={[
            { value: 'auto',        label: `✨ Авто (за класом: ${ch?.klass ?? '—'})` },
            { value: 'dark',        label: '◐ Темна (walnut + gold)' },
            { value: 'light',       label: '○ Світла (clean modern)' },
            { value: 'vellum',      label: '◯ Пергамент (vellum)' },
            { value: 'paladin',     label: '✦ Паладин (steel + holy)' },
            { value: 'necromancer', label: '☠ Некромант (bone + venom)' },
            { value: 'druid',       label: '🌿 Друид (forest + amber)' },
            { value: 'bard',        label: '♪ Бард (magenta + plum)' },
            { value: 'rogue',       label: '🗡 Розбійник (shadow + blood)' },
            { value: 'barbarian',   label: '⚔ Варвар (rage + iron)' },
            { value: 'fighter',     label: '🛡 Воїн (steel + brass)' },
          ]}
        />
        <TweakColor
          label="Акцент"
          value={t.accent}
          onChange={(v) => setTweak('accent', v)}
          options={['#d4a857', '#c97d4a', '#c7c3b8', '#6cb070']}
        />

        <TweakSection label="Декор" />
        <TweakToggle
          label="Іллюміновані рамки"
          value={t.ornaments}
          onChange={(v) => setTweak('ornaments', v)}
        />
        <TweakToggle
          label="Чіпи дій у хедері (бій)"
          value={t.showActionBar}
          onChange={(v) => setTweak('showActionBar', v)}
        />

        <TweakSection label="Швидкі дії" />
        <TweakButton label="↳ Перейти до бою" onClick={() => setActive('combat')} />
        <TweakButton label="🎲 Відкрити куби" onClick={() => setDiceOpen(true)} />
        {isDM && <TweakButton label="+ Новий персонаж" onClick={addCharacter} />}
        {isDM && <TweakButton label="↻ Скинути цього" onClick={resetActive} />}
        {isDM && <TweakButton label="🗑 Очистити все" onClick={clearAll} />}

        <TweakSection label={`Сесія · ${currentUser?.name} (${currentUser?.role})`} />
        <TweakButton label="🚪 Вийти з акаунту" onClick={handleLogout} />
      </TweaksPanel>

      {/* ===== MODAL: Новий персонаж ===== */}
      {newCharOpen && (
        <div className="dice-overlay-backdrop" onClick={() => setNewCharOpen(false)}>
          <div className="dice-overlay" style={{ width: 380, minHeight: 280 }} onClick={e => e.stopPropagation()}>
            <div className="dice-overlay-head">
              <Icon name="character" size={18} style={{ color: 'var(--gold-1)' }}/>
              <h2>Новий персонаж</h2>
              <div className="igrow"/>
              <IconBtn name="x" onClick={() => setNewCharOpen(false)}/>
            </div>
            <div className="dice-overlay-body">
              <div className="icol ig-12">
                <div>
                  <div className="field-label mb-4">Ім'я персонажа</div>
                  <input className="input" autoFocus placeholder="Новий герой"
                    value={newCharName} onChange={e => setNewCharName(e.target.value)}
                    onKeyDown={e => e.key === 'Enter' && confirmNewChar()}/>
                </div>
                <div className="flex ig-8 mt-4">
                  <Btn primary onClick={confirmNewChar}><Icon name="plus" size={12}/> Створити</Btn>
                  <Btn ghost onClick={() => setNewCharOpen(false)}>Скасувати</Btn>
                </div>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* ===== MODAL: Підтвердження скидання персонажа ===== */}
      {resetConfirmOpen && (
        <div className="dice-overlay-backdrop" onClick={() => setResetConfirmOpen(false)}>
          <div className="dice-overlay" style={{ width: 360, minHeight: 260 }} onClick={e => e.stopPropagation()}>
            <div className="dice-overlay-head">
              <Icon name="refresh" size={18} style={{ color: '#e0a050' }}/>
              <h2>Скинути персонажа?</h2>
              <div className="igrow"/>
              <IconBtn name="x" onClick={() => setResetConfirmOpen(false)}/>
            </div>
            <div className="dice-overlay-body">
              <div className="script" style={{ fontSize: 15, marginBottom: 20, color: 'var(--ink-1)' }}>
                Всі дані «{ch?.name}» будуть очищені. Дію не можна відмінити.
              </div>
              <div className="flex ig-8">
                <Btn danger onClick={confirmReset}><Icon name="refresh" size={12}/> Скинути</Btn>
                <Btn ghost onClick={() => setResetConfirmOpen(false)}>Скасувати</Btn>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* ===== MODAL: Підтвердження очищення всіх даних ===== */}
      {clearConfirmOpen && (
        <div className="dice-overlay-backdrop" onClick={() => setClearConfirmOpen(false)}>
          <div className="dice-overlay" style={{ width: 380, minHeight: 280 }} onClick={e => e.stopPropagation()}>
            <div className="dice-overlay-head">
              <Icon name="trash" size={18} style={{ color: 'var(--crimson-1)' }}/>
              <h2>Очистити всі дані?</h2>
              <div className="igrow"/>
              <IconBtn name="x" onClick={() => setClearConfirmOpen(false)}/>
            </div>
            <div className="dice-overlay-body">
              <div className="script" style={{ fontSize: 15, marginBottom: 20, color: 'var(--ink-1)' }}>
                Всі персонажі, хронологія та історія кидків будуть скинуті до початкового стану. Дію не можна відмінити.
              </div>
              <div className="flex ig-8">
                <Btn danger onClick={confirmClearAll}><Icon name="trash" size={12}/> Скинути все</Btn>
                <Btn ghost onClick={() => setClearConfirmOpen(false)}>Скасувати</Btn>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* ===== MODAL: Підтвердження видалення ===== */}
      {deleteTarget && (
        <div className="dice-overlay-backdrop" onClick={() => setDeleteTarget(null)}>
          <div className="dice-overlay" style={{ width: 360, minHeight: 260 }} onClick={e => e.stopPropagation()}>
            <div className="dice-overlay-head">
              <Icon name="trash" size={18} style={{ color: 'var(--crimson-1)' }}/>
              <h2>Видалити персонажа?</h2>
              <div className="igrow"/>
              <IconBtn name="x" onClick={() => setDeleteTarget(null)}/>
            </div>
            <div className="dice-overlay-body">
              <div className="script" style={{ fontSize: 15, marginBottom: 20, color: 'var(--ink-1)' }}>
                «{deleteTarget.name}» буде видалено. Цю дію не можна відмінити.
              </div>
              <div className="flex ig-8">
                <Btn danger onClick={confirmDelete}><Icon name="trash" size={12}/> Видалити</Btn>
                <Btn ghost onClick={() => setDeleteTarget(null)}>Скасувати</Btn>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <ToastProvider>
    <App/>
  </ToastProvider>
);
