/* ============================================================
   wordsquest/wordsquest-data.jsx — независимая подсистема
   «Английские слова»: категории слов, интервальное повторение
   (SM-2-подобный алгоритм), самооценка качества запоминания.
   Свой store (localStorage "wordsquest-state"). Полностью независим
   от остальной части планера и от других игровых подсистем.
   Exposes: WQ_CATEGORIES, WQ_INDEX, WQStore, useWQ, WQActions, WQLookup.
   ============================================================ */
const React = window.React;

/* ---- категории слов ---------------------------------------------------- */
const WQ_CATEGORIES = [
  { id: "war", title: "Война и оружие", icon: "swords", words: [
    { en: "siege", ts: "siːdʒ", ru: "осада", ex: "The city endured a siege that lasted three months." },
    { en: "ceasefire", ts: "ˈsiːsfaɪə", ru: "прекращение огня", ex: "Both sides agreed to a temporary ceasefire." },
    { en: "artillery", ts: "ɑːˈtɪləri", ru: "артиллерия", ex: "Heavy artillery pounded the ridge before dawn." },
    { en: "ambush", ts: "ˈæmbʊʃ", ru: "засада", ex: "The convoy was caught in an ambush at the mountain pass." },
    { en: "casualty", ts: "ˈkæʒuəlti", ru: "потери, жертва (человек)", ex: "Civilian casualties rose sharply during the offensive." },
    { en: "conscription", ts: "kənˈskrɪpʃn", ru: "призыв на военную службу", ex: "Conscription was introduced once the volunteers ran out." },
    { en: "insurgent", ts: "ɪnˈsɜːdʒənt", ru: "повстанец", ex: "Insurgents controlled the countryside but never the capital." },
    { en: "deterrence", ts: "dɪˈterəns", ru: "сдерживание (угрозой)", ex: "Nuclear deterrence rests on the threat of retaliation." },
    { en: "atrocity", ts: "əˈtrɒsəti", ru: "зверство", ex: "The tribunal documented atrocities committed on both sides." },
    { en: "arsenal", ts: "ˈɑːsənl", ru: "арсенал", ex: "The country quietly expanded its arsenal of long-range missiles." },
    { en: "truce", ts: "truːs", ru: "перемирие", ex: "A fragile truce held through the winter." },
    { en: "reinforcements", ts: "ˌriːɪnˈfɔːsmənts", ru: "подкрепление", ex: "They held the bridge until reinforcements arrived." },
    { en: "surrender", ts: "səˈrendə", ru: "капитуляция; сдаться", ex: "The garrison refused to surrender." },
    { en: "warfare", ts: "ˈwɔːfeə", ru: "ведение войны", ex: "Trench warfare turned the front into a stalemate." },
    { en: "skirmish", ts: "ˈskɜːmɪʃ", ru: "стычка, перестрелка", ex: "A brief skirmish broke out near the border post." },
    { en: "offensive", ts: "əˈfensɪv", ru: "наступление", ex: "The spring offensive gained only a few kilometres." },
    { en: "retreat", ts: "rɪˈtriːt", ru: "отступление; отступать", ex: "The order to retreat came far too late." },
    { en: "garrison", ts: "ˈɡærɪsn", ru: "гарнизон", ex: "A small garrison defended the fortress for weeks." },
    { en: "barracks", ts: "ˈbærəks", ru: "казарма", ex: "Recruits spent their first month confined to barracks." },
    { en: "trench", ts: "trentʃ", ru: "окоп, траншея", ex: "The trenches flooded after three days of rain." },
    { en: "shrapnel", ts: "ˈʃræpnəl", ru: "осколки", ex: "He still carries shrapnel in his shoulder." },
    { en: "grenade", ts: "ɡrəˈneɪd", ru: "граната", ex: "A grenade destroyed the machine-gun nest." },
    { en: "bayonet", ts: "ˈbeɪənət", ru: "штык", ex: "Bayonet charges became rare after the first year." },
    { en: "sniper", ts: "ˈsnaɪpə", ru: "снайпер", ex: "A sniper kept the whole square under fire." },
    { en: "mercenary", ts: "ˈmɜːsənəri", ru: "наёмник", ex: "Foreign mercenaries fought on both sides for pay." },
    { en: "veteran", ts: "ˈvetərən", ru: "ветеран", ex: "Veterans of that campaign rarely speak about it." },
    { en: "desertion", ts: "dɪˈzɜːʃn", ru: "дезертирство", ex: "Desertion was punished with extraordinary cruelty." },
    { en: "mutiny", ts: "ˈmjuːtəni", ru: "мятеж в войсках", ex: "Hunger and cold provoked a mutiny in the ranks." },
    { en: "blockade", ts: "blɒˈkeɪd", ru: "блокада", ex: "The naval blockade cut off all imports of fuel." },
    { en: "convoy", ts: "ˈkɒnvɔɪ", ru: "конвой, колонна", ex: "The supply convoy travelled only at night." },
    { en: "reconnaissance", ts: "rɪˈkɒnɪsns", ru: "разведка (местности)", ex: "Aerial reconnaissance revealed the hidden batteries." },
    { en: "espionage", ts: "ˈespiənɑːʒ", ru: "шпионаж", ex: "He was tried for espionage on flimsy evidence." },
    { en: "sabotage", ts: "ˈsæbətɑːʒ", ru: "диверсия, саботаж", ex: "Sabotage of the railway delayed the advance by a week." },
    { en: "propaganda", ts: "ˌprɒpəˈɡændə", ru: "пропаганда", ex: "Wartime propaganda painted the enemy as barely human." },
    { en: "armistice", ts: "ˈɑːmɪstɪs", ru: "перемирие (официальное)", ex: "The armistice was signed in a railway carriage." },
    { en: "treaty", ts: "ˈtriːti", ru: "договор", ex: "The peace treaty imposed crushing reparations." },
    { en: "annex", ts: "əˈneks", ru: "аннексировать", ex: "The empire annexed the province without a shot." },
    { en: "occupation", ts: "ˌɒkjuˈpeɪʃn", ru: "оккупация", ex: "Life under occupation is the subject of her memoir." },
    { en: "refugee", ts: "ˌrefjuˈdʒiː", ru: "беженец", ex: "Thousands of refugees crossed the mountains on foot." },
    { en: "evacuate", ts: "ɪˈvækjueɪt", ru: "эвакуировать", ex: "Children were evacuated to the countryside." },
    { en: "militia", ts: "məˈlɪʃə", ru: "ополчение", ex: "A volunteer militia defended the outskirts." },
    { en: "rifle", ts: "ˈraɪfl", ru: "винтовка", ex: "He was issued a rifle older than his father." },
    { en: "ammunition", ts: "ˌæmjuˈnɪʃn", ru: "боеприпасы", ex: "They ran out of ammunition before nightfall." },
    { en: "warhead", ts: "ˈwɔːhed", ru: "боеголовка", ex: "The treaty limited the number of deployed warheads." },
    { en: "disarmament", ts: "dɪsˈɑːməmənt", ru: "разоружение", ex: "Disarmament talks collapsed after a single session." },
    { en: "stalemate", ts: "ˈsteɪlmeɪt", ru: "патовая ситуация", ex: "The front settled into a bloody stalemate." },
    { en: "valour", ts: "ˈvælə", ru: "доблесть", ex: "He was decorated for valour under fire." },
    { en: "betrayal", ts: "bɪˈtreɪəl", ru: "предательство", ex: "The surrender was remembered as an act of betrayal." },
    { en: "onslaught", ts: "ˈɒnslɔːt", ru: "натиск, яростная атака", ex: "The defences buckled under the onslaught." },
    { en: "aftermath", ts: "ˈɑːftəmæθ", ru: "последствия", ex: "In the aftermath of the war, whole cities lay in ruins." },
  ]},
  { id: "home", title: "Быт дома", icon: "shelf", words: [
    { en: "clutter", ts: "ˈklʌtə", ru: "беспорядок, хлам", ex: "The desk disappeared under a clutter of papers." },
    { en: "appliance", ts: "əˈplaɪəns", ru: "бытовой прибор", ex: "Every appliance in the kitchen broke that month." },
    { en: "chore", ts: "tʃɔː", ru: "домашняя обязанность", ex: "We split the household chores evenly." },
    { en: "laundry", ts: "ˈlɔːndri", ru: "стирка; бельё", ex: "I still have a basket of laundry to fold." },
    { en: "plumbing", ts: "ˈplʌmɪŋ", ru: "водопровод, сантехника", ex: "The plumbing in this flat is ancient." },
    { en: "drawer", ts: "drɔː", ru: "выдвижной ящик", ex: "The keys were in the top drawer all along." },
    { en: "tidy up", ts: "ˈtaɪdi ʌp", ru: "прибираться", ex: "Could you tidy up before the guests arrive?" },
    { en: "leak", ts: "liːk", ru: "протечка; протекать", ex: "A leak under the sink ruined the floorboards." },
    { en: "cosy", ts: "ˈkəʊzi", ru: "уютный", ex: "The flat is small but remarkably cosy." },
    { en: "rubbish", ts: "ˈrʌbɪʃ", ru: "мусор", ex: "Someone has to take the rubbish out." },
    { en: "spare", ts: "speə", ru: "запасной", ex: "There is a spare key under the mat." },
    { en: "damp", ts: "dæmp", ru: "сырость; сырой", ex: "Damp had crept up the wall behind the wardrobe." },
    { en: "mortgage", ts: "ˈmɔːɡɪdʒ", ru: "ипотека", ex: "They will be paying off the mortgage for decades." },
    { en: "tenant", ts: "ˈtenənt", ru: "квартиросъёмщик", ex: "The previous tenant left the place spotless." },
    { en: "landlord", ts: "ˈlændlɔːd", ru: "арендодатель", ex: "The landlord raised the rent without warning." },
    { en: "lease", ts: "liːs", ru: "договор аренды", ex: "Our lease runs out at the end of the month." },
    { en: "utilities", ts: "juːˈtɪlətiz", ru: "коммунальные услуги", ex: "The rent is cheap, but utilities are not included." },
    { en: "furnish", ts: "ˈfɜːnɪʃ", ru: "обставлять мебелью", ex: "They furnished the whole flat from second-hand shops." },
    { en: "wardrobe", ts: "ˈwɔːdrəʊb", ru: "платяной шкаф", ex: "The wardrobe barely fits through the door." },
    { en: "cupboard", ts: "ˈkʌbəd", ru: "шкафчик, буфет", ex: "The cupboard above the sink is full of jars." },
    { en: "mattress", ts: "ˈmætrəs", ru: "матрас", ex: "The mattress sags in the middle." },
    { en: "blanket", ts: "ˈblæŋkɪt", ru: "одеяло", ex: "She pulled a woollen blanket over her knees." },
    { en: "curtain", ts: "ˈkɜːtn", ru: "штора", ex: "Draw the curtains before you turn on the lamp." },
    { en: "shelf", ts: "ʃelf", ru: "полка", ex: "The top shelf is out of reach without a stool." },
    { en: "socket", ts: "ˈsɒkɪt", ru: "розетка", ex: "There is only one socket in the entire room." },
    { en: "kettle", ts: "ˈketl", ru: "чайник", ex: "Put the kettle on and we will talk properly." },
    { en: "sink", ts: "sɪŋk", ru: "раковина", ex: "Dishes had been piling up in the sink for days." },
    { en: "tap", ts: "tæp", ru: "кран", ex: "The kitchen tap drips all night." },
    { en: "vacuum", ts: "ˈvækjuːm", ru: "пылесосить; пылесос", ex: "I vacuum the hallway twice a week." },
    { en: "sweep", ts: "swiːp", ru: "подметать", ex: "He swept the crumbs off the table." },
    { en: "scrub", ts: "skrʌb", ru: "оттирать, драить", ex: "She scrubbed the pan until it shone." },
    { en: "rinse", ts: "rɪns", ru: "ополаскивать", ex: "Rinse the glasses in cold water." },
    { en: "stain", ts: "steɪn", ru: "пятно", ex: "The wine left a stain on the tablecloth." },
    { en: "mould", ts: "məʊld", ru: "плесень", ex: "Mould had spread across the bathroom ceiling." },
    { en: "draught", ts: "drɑːft", ru: "сквозняк", ex: "There is a terrible draught from that window." },
    { en: "insulation", ts: "ˌɪnsjuˈleɪʃn", ru: "утепление, изоляция", ex: "Better insulation halved their heating bill." },
    { en: "renovation", ts: "ˌrenəˈveɪʃn", ru: "ремонт", ex: "The renovation dragged on for a year." },
    { en: "wiring", ts: "ˈwaɪərɪŋ", ru: "электропроводка", ex: "The wiring has not been touched since the sixties." },
    { en: "boiler", ts: "ˈbɔɪlə", ru: "котёл, бойлер", ex: "The boiler broke down in the middle of January." },
    { en: "rent", ts: "rent", ru: "арендная плата", ex: "Half his salary goes on rent." },
    { en: "attic", ts: "ˈætɪk", ru: "чердак", ex: "Old photographs were gathering dust in the attic." },
    { en: "basement", ts: "ˈbeɪsmənt", ru: "подвал", ex: "The basement floods every spring." },
    { en: "hallway", ts: "ˈhɔːlweɪ", ru: "прихожая, коридор", ex: "Leave your boots in the hallway." },
    { en: "doorstep", ts: "ˈdɔːstep", ru: "порог", ex: "The parcel was left on the doorstep." },
    { en: "hinge", ts: "hɪndʒ", ru: "дверная петля", ex: "The hinges creak every time you open it." },
    { en: "shed", ts: "ʃed", ru: "сарай", ex: "The tools are kept in the shed at the back." },
    { en: "broom", ts: "bruːm", ru: "метла", ex: "The broom stands behind the kitchen door." },
    { en: "bin", ts: "bɪn", ru: "мусорное ведро", ex: "The bin needs emptying again." },
    { en: "dishwasher", ts: "ˈdɪʃwɒʃə", ru: "посудомоечная машина", ex: "Load the dishwasher and I will start it." },
    { en: "household", ts: "ˈhaʊshəʊld", ru: "домашнее хозяйство", ex: "The whole household was asleep by ten." },
  ]},
  { id: "food", title: "Продукты и еда", icon: "flask", words: [
    { en: "perishable", ts: "ˈperɪʃəbl", ru: "скоропортящийся", ex: "Keep perishable goods at the back of the fridge." },
    { en: "seasoning", ts: "ˈsiːzənɪŋ", ru: "приправа", ex: "The soup needs a bit more seasoning." },
    { en: "simmer", ts: "ˈsɪmə", ru: "кипеть на медленном огне", ex: "Let the sauce simmer for twenty minutes." },
    { en: "pantry", ts: "ˈpæntri", ru: "кладовая", ex: "The pantry was stocked for the whole winter." },
    { en: "stale", ts: "steɪl", ru: "чёрствый, несвежий", ex: "The bread had gone stale overnight." },
    { en: "ripe", ts: "raɪp", ru: "спелый", ex: "Wait until the avocado is properly ripe." },
    { en: "savoury", ts: "ˈseɪvəri", ru: "несладкий, солоноватый", ex: "I prefer savoury snacks to sweet ones." },
    { en: "bland", ts: "blænd", ru: "пресный, безвкусный", ex: "Without salt the dish is completely bland." },
    { en: "hearty", ts: "ˈhɑːti", ru: "сытный, обильный", ex: "We had a hearty meal before the hike." },
    { en: "leftovers", ts: "ˈleftəʊvəz", ru: "остатки еды", ex: "I will take the leftovers for lunch tomorrow." },
    { en: "additive", ts: "ˈædətɪv", ru: "пищевая добавка", ex: "The label lists three artificial additives." },
    { en: "nourishing", ts: "ˈnʌrɪʃɪŋ", ru: "питательный", ex: "A simple but nourishing breakfast." },
    { en: "marinate", ts: "ˈmærɪneɪt", ru: "мариновать", ex: "Marinate the lamb overnight for the best result." },
    { en: "whisk", ts: "wɪsk", ru: "взбивать венчиком", ex: "Whisk the eggs until they are pale and frothy." },
    { en: "knead", ts: "niːd", ru: "месить тесто", ex: "Knead the dough for a good ten minutes." },
    { en: "grate", ts: "ɡreɪt", ru: "тереть на тёрке", ex: "Grate the cheese over the pasta while it is hot." },
    { en: "peel", ts: "piːl", ru: "чистить от кожуры", ex: "Peel the potatoes before boiling them." },
    { en: "mince", ts: "mɪns", ru: "рубить, прокручивать мясо", ex: "Mince the garlic as finely as you can." },
    { en: "roast", ts: "rəʊst", ru: "запекать в духовке", ex: "We roasted the vegetables with olive oil." },
    { en: "steam", ts: "stiːm", ru: "готовить на пару", ex: "Steamed fish keeps far more flavour." },
    { en: "stir", ts: "stɜː", ru: "помешивать", ex: "Stir constantly or the milk will burn." },
    { en: "dough", ts: "dəʊ", ru: "тесто", ex: "Leave the dough to rise in a warm place." },
    { en: "batter", ts: "ˈbætə", ru: "жидкое тесто, кляр", ex: "Dip the fish in batter before frying." },
    { en: "yeast", ts: "jiːst", ru: "дрожжи", ex: "Fresh yeast gives a much better crumb." },
    { en: "dairy", ts: "ˈdeəri", ru: "молочные продукты", ex: "He avoids dairy altogether." },
    { en: "poultry", ts: "ˈpəʊltri", ru: "птица (мясо)", ex: "Poultry is cheaper than beef here." },
    { en: "cured", ts: "kjʊəd", ru: "вяленый, солёный", ex: "Cured ham keeps for months in a cool room." },
    { en: "pickled", ts: "ˈpɪkld", ru: "маринованный", ex: "A jar of pickled cucumbers stood on the table." },
    { en: "tender", ts: "ˈtendə", ru: "мягкий, нежный (о мясе)", ex: "Slow cooking makes even cheap cuts tender." },
    { en: "tough", ts: "tʌf", ru: "жёсткий (о мясе)", ex: "The steak was tough and grey." },
    { en: "crisp", ts: "krɪsp", ru: "хрустящий", ex: "The crust came out beautifully crisp." },
    { en: "soggy", ts: "ˈsɒɡi", ru: "размокший", ex: "Nobody wants a soggy pastry." },
    { en: "rancid", ts: "ˈrænsɪd", ru: "прогорклый", ex: "The butter had turned rancid." },
    { en: "curdle", ts: "ˈkɜːdl", ru: "сворачиваться (о молоке)", ex: "Add the cream slowly or it will curdle." },
    { en: "ferment", ts: "fəˈment", ru: "бродить, ферментировать", ex: "Let the cabbage ferment for two weeks." },
    { en: "preserve", ts: "prɪˈzɜːv", ru: "консервировать, заготавливать", ex: "They preserve fruit every August." },
    { en: "staple", ts: "ˈsteɪpl", ru: "основной продукт питания", ex: "Rice is the staple of the region." },
    { en: "produce", ts: "ˈprɒdjuːs", ru: "овощи и фрукты, сельхозпродукция", ex: "The market sells local produce only." },
    { en: "groceries", ts: "ˈɡrəʊsəriz", ru: "продукты (покупки)", ex: "I carried the groceries up four flights of stairs." },
    { en: "portion", ts: "ˈpɔːʃn", ru: "порция", ex: "The portions are generous for the price." },
    { en: "nutrient", ts: "ˈnjuːtriənt", ru: "питательное вещество", ex: "Boiling destroys some of the nutrients." },
    { en: "calorie", ts: "ˈkæləri", ru: "калория", ex: "He counts every calorie obsessively." },
    { en: "appetite", ts: "ˈæpɪtaɪt", ru: "аппетит", ex: "The walk gave us all an appetite." },
    { en: "craving", ts: "ˈkreɪvɪŋ", ru: "сильное желание съесть", ex: "She had a sudden craving for something salty." },
    { en: "indulge", ts: "ɪnˈdʌldʒ", ru: "побаловать себя", ex: "We indulged in a second helping of cake." },
    { en: "digest", ts: "daɪˈdʒest", ru: "переваривать", ex: "Fatty food is harder to digest late at night." },
    { en: "seasonal", ts: "ˈsiːzənl", ru: "сезонный", ex: "The menu changes with seasonal ingredients." },
    { en: "edible", ts: "ˈedəbl", ru: "съедобный", ex: "Not every mushroom in this wood is edible." },
    { en: "utensil", ts: "juːˈtensl", ru: "кухонная утварь", ex: "The drawer is crammed with utensils he never uses." },
    { en: "recipe", ts: "ˈresəpi", ru: "рецепт", ex: "The recipe has been in the family for generations." },
  ]},
  { id: "philosophy", title: "Философия", icon: "quote", words: [
    { en: "epistemology", ts: "ɪˌpɪstɪˈmɒlədʒi", ru: "теория познания", ex: "Epistemology asks how we know what we claim to know." },
    { en: "ontology", ts: "ɒnˈtɒlədʒi", ru: "онтология (учение о бытии)", ex: "Their disagreement is ontological, not merely verbal." },
    { en: "empirical", ts: "ɪmˈpɪrɪkl", ru: "эмпирический, опытный", ex: "The claim has no empirical support whatsoever." },
    { en: "premise", ts: "ˈpremɪs", ru: "посылка (в рассуждении)", ex: "If the premise is false, the conclusion collapses." },
    { en: "fallacy", ts: "ˈfæləsi", ru: "логическая ошибка", ex: "That argument rests on a well-known fallacy." },
    { en: "inherent", ts: "ɪnˈhɪərənt", ru: "присущий, неотъемлемый", ex: "There is an inherent tension between the two goals." },
    { en: "determinism", ts: "dɪˈtɜːmɪnɪzəm", ru: "детерминизм", ex: "Determinism seems to leave no room for free will." },
    { en: "contingent", ts: "kənˈtɪndʒənt", ru: "случайный, обусловленный", ex: "Our existence is contingent, not necessary." },
    { en: "dialectic", ts: "ˌdaɪəˈlektɪk", ru: "диалектика", ex: "He read history as a dialectic of opposing forces." },
    { en: "transcendent", ts: "trænˈsendənt", ru: "трансцендентный, запредельный", ex: "She spoke of a transcendent order beyond experience." },
    { en: "nihilism", ts: "ˈnaɪɪlɪzəm", ru: "нигилизм", ex: "He mistook scepticism for nihilism." },
    { en: "virtue", ts: "ˈvɜːtʃuː", ru: "добродетель", ex: "For Aristotle, virtue is a habit, not a feeling." },
    { en: "presupposition", ts: "ˌpriːsʌpəˈzɪʃn", ru: "предпосылка, презумпция", ex: "Every question carries a presupposition." },
    { en: "metaphysics", ts: "ˌmetəˈfɪzɪks", ru: "метафизика", ex: "He dismissed the whole debate as idle metaphysics." },
    { en: "ethics", ts: "ˈeθɪks", ru: "этика", ex: "Applied ethics deals with concrete dilemmas." },
    { en: "aesthetics", ts: "iːsˈθetɪks", ru: "эстетика", ex: "Her aesthetics owe a great deal to Kant." },
    { en: "rationalism", ts: "ˈræʃnəlɪzəm", ru: "рационализм", ex: "Rationalism holds that reason alone can yield knowledge." },
    { en: "empiricism", ts: "ɪmˈpɪrɪsɪzəm", ru: "эмпиризм", ex: "British empiricism begins with Locke." },
    { en: "scepticism", ts: "ˈskeptɪsɪzəm", ru: "скептицизм", ex: "Radical scepticism doubts even the evidence of the senses." },
    { en: "dogma", ts: "ˈdɒɡmə", ru: "догма", ex: "What began as a hypothesis hardened into dogma." },
    { en: "axiom", ts: "ˈæksiəm", ru: "аксиома", ex: "The system rests on five axioms." },
    { en: "inference", ts: "ˈɪnfərəns", ru: "вывод, умозаключение", ex: "That inference does not follow from the premises." },
    { en: "deduction", ts: "dɪˈdʌkʃn", ru: "дедукция", ex: "Deduction guarantees truth only if the premises are true." },
    { en: "induction", ts: "ɪnˈdʌkʃn", ru: "индукция", ex: "Hume questioned whether induction can be justified at all." },
    { en: "coherent", ts: "kəʊˈhɪərənt", ru: "связный, непротиворечивый", ex: "The theory is elegant but not entirely coherent." },
    { en: "plausible", ts: "ˈplɔːzəbl", ru: "правдоподобный", ex: "It is a plausible reading, though not the only one." },
    { en: "ambiguous", ts: "æmˈbɪɡjuəs", ru: "двусмысленный", ex: "The key term remains stubbornly ambiguous." },
    { en: "arbitrary", ts: "ˈɑːbɪtrəri", ru: "произвольный", ex: "The distinction looks arbitrary on closer inspection." },
    { en: "subjective", ts: "səbˈdʒektɪv", ru: "субъективный", ex: "Taste is subjective, but not therefore trivial." },
    { en: "objective", ts: "əbˈdʒektɪv", ru: "объективный", ex: "He insists that moral facts are objective." },
    { en: "innate", ts: "ɪˈneɪt", ru: "врождённый", ex: "Are any of our ideas innate?" },
    { en: "finite", ts: "ˈfaɪnaɪt", ru: "конечный, ограниченный", ex: "A finite mind cannot grasp an infinite object." },
    { en: "infinite", ts: "ˈɪnfɪnət", ru: "бесконечный", ex: "The argument turns on the idea of an infinite regress." },
    { en: "essence", ts: "ˈesns", ru: "сущность", ex: "Existence precedes essence, Sartre insisted." },
    { en: "substance", ts: "ˈsʌbstəns", ru: "субстанция", ex: "For Spinoza there is only one substance." },
    { en: "phenomenon", ts: "fəˈnɒmɪnən", ru: "явление", ex: "We know phenomena, never things in themselves." },
    { en: "consciousness", ts: "ˈkɒnʃəsnəs", ru: "сознание", ex: "Consciousness remains stubbornly hard to explain." },
    { en: "perception", ts: "pəˈsepʃn", ru: "восприятие", ex: "Perception is already shaped by expectation." },
    { en: "intuition", ts: "ˌɪntjuˈɪʃn", ru: "интуиция", ex: "He appeals to intuition where argument runs out." },
    { en: "reasoning", ts: "ˈriːzənɪŋ", ru: "рассуждение", ex: "Her reasoning is careful but the conclusion is odd." },
    { en: "justification", ts: "ˌdʒʌstɪfɪˈkeɪʃn", ru: "обоснование", ex: "Belief without justification is mere opinion." },
    { en: "refute", ts: "rɪˈfjuːt", ru: "опровергать", ex: "A single counterexample refutes the whole rule." },
    { en: "assert", ts: "əˈsɜːt", ru: "утверждать", ex: "He asserts far more than he can demonstrate." },
    { en: "imply", ts: "ɪmˈplaɪ", ru: "подразумевать", ex: "Saying nothing can imply consent." },
    { en: "entail", ts: "ɪnˈteɪl", ru: "влечь за собой", ex: "Freedom entails responsibility." },
    { en: "paradox", ts: "ˈpærədɒks", ru: "парадокс", ex: "The paradox has resisted solution for centuries." },
    { en: "free will", ts: "friː wɪl", ru: "свобода воли", ex: "Whether free will survives physics is still disputed." },
    { en: "moral", ts: "ˈmɒrəl", ru: "нравственный, моральный", ex: "He faced a genuine moral dilemma." },
    { en: "absurd", ts: "əbˈsɜːd", ru: "абсурдный", ex: "Camus began from the absurd and refused despair." },
    { en: "wisdom", ts: "ˈwɪzdəm", ru: "мудрость", ex: "Philosophy once meant the love of wisdom." },
  ]},
  { id: "misc", title: "Разное", icon: "tag", words: [
    { en: "nightgown", ts: "ˈnaɪtɡaʊn", ru: "ночная рубашка", ex: "She put on a warm nightgown before bed." },
    { en: "nudge", ts: "nʌdʒ", ru: "подталкивать", ex: "He nudged me to get my attention." },
    { en: "rival", ts: "ˈraɪvl", ru: "соперник", ex: "The two teams have always been fierce rivals." },
    { en: "nascent", ts: "ˈnæsnt", ru: "зарождающийся", ex: "The nascent movement grew quickly." },
    { en: "lop off", ts: "lɒp ɒf", ru: "отрезать / отрубать", ex: "He lopped off the dead branches." },
    { en: "gouge", ts: "ɡaʊdʒ", ru: "выдалбливать", ex: "The flood gouged deep channels in the road." },
    { en: "hoof", ts: "huːf", ru: "копыто", ex: "The horse injured its hoof on the rocks." },
    { en: "rig", ts: "rɪɡ", ru: "подтасовывать", ex: "They accused him of rigging the election." },
    { en: "glance", ts: "ɡlɑːns", ru: "взглянуть мельком", ex: "She glanced at her watch and hurried off." },
    { en: "glimpse", ts: "ɡlɪmps", ru: "случайно заметить", ex: "I only glimpsed him through the crowd." },
    { en: "male gaze", ts: "ˌmeɪl ˈɡeɪz", ru: "мужской взгляд", ex: "The film critiques the male gaze in cinema." },
    { en: "glare", ts: "ɡleə", ru: "смотреть свирепо", ex: "She glared at him and said nothing." },
  ]},
  { id: "sex", title: "Секс и близость", icon: "flame", words: [
    { en: "desire", ts: "dɪˈzaɪə", ru: "желание, вожделение", ex: "She felt a sudden desire for him." },
    { en: "arousal", ts: "əˈraʊzl", ru: "возбуждение", ex: "Arousal is a natural physical response." },
    { en: "lust", ts: "lʌst", ru: "похоть", ex: "He mistook lust for love." },
    { en: "libido", ts: "lɪˈbiːdəʊ", ru: "либидо, половое влечение", ex: "Stress can lower your libido." },
    { en: "attraction", ts: "əˈtrækʃn", ru: "влечение", ex: "There was an instant attraction between them." },
    { en: "crush", ts: "krʌʃ", ru: "сильная влюблённость", ex: "She has a crush on a coworker." },
    { en: "seduce", ts: "sɪˈdjuːs", ru: "соблазнять", ex: "He tried to seduce her with wine and music." },
    { en: "flirt", ts: "flɜːt", ru: "флиртовать", ex: "They flirted all through dinner." },
    { en: "tease", ts: "tiːz", ru: "дразнить, заигрывать", ex: "She teased him with a playful smile." },
    { en: "foreplay", ts: "ˈfɔːpleɪ", ru: "прелюдия (ласки)", ex: "Foreplay is an important part of intimacy." },
    { en: "intimacy", ts: "ˈɪntɪməsi", ru: "близость, интимность", ex: "Physical intimacy grew between them." },
    { en: "intercourse", ts: "ˈɪntəkɔːs", ru: "половой акт", ex: "The class covered safe intercourse." },
    { en: "erotic", ts: "ɪˈrɒtɪk", ru: "эротический", ex: "The novel has several erotic scenes." },
    { en: "sensual", ts: "ˈsenʃuəl", ru: "чувственный", ex: "Her voice was low and sensual." },
    { en: "caress", ts: "kəˈres", ru: "ласкать", ex: "She caressed his cheek gently." },
    { en: "embrace", ts: "ɪmˈbreɪs", ru: "обнимать; объятие", ex: "They embraced for a long moment." },
    { en: "cuddle", ts: "ˈkʌdl", ru: "прижиматься, обниматься", ex: "They cuddled under the blanket." },
    { en: "make love", ts: "ˌmeɪk ˈlʌv", ru: "заниматься любовью", ex: "They made love for the first time that night." },
    { en: "climax", ts: "ˈklaɪmæks", ru: "оргазм, кульминация", ex: "She reached a climax." },
    { en: "orgasm", ts: "ˈɔːɡæzəm", ru: "оргазм", ex: "Orgasm releases certain hormones." },
    { en: "moan", ts: "məʊn", ru: "стонать; стон", ex: "She moaned softly." },
    { en: "arouse", ts: "əˈraʊz", ru: "возбуждать", ex: "The touch aroused him at once." },
    { en: "afterglow", ts: "ˈɑːftəɡləʊ", ru: "нега после близости", ex: "They lay together in the afterglow." },
    { en: "genitals", ts: "ˈdʒenɪtlz", ru: "половые органы", ex: "The doctor examined the genitals." },
    { en: "penis", ts: "ˈpiːnɪs", ru: "половой член", ex: "The textbook labelled the penis." },
    { en: "vagina", ts: "vəˈdʒaɪnə", ru: "влагалище", ex: "The article discussed vaginal health." },
    { en: "breast", ts: "brest", ru: "грудь", ex: "She felt a lump in her breast." },
    { en: "nipple", ts: "ˈnɪpl", ru: "сосок", ex: "The cold made her nipples hard." },
    { en: "buttocks", ts: "ˈbʌtəks", ru: "ягодицы", ex: "He pulled a muscle in his buttocks." },
    { en: "groin", ts: "ɡrɔɪn", ru: "пах", ex: "He strained a muscle in his groin." },
    { en: "erection", ts: "ɪˈrekʃn", ru: "эрекция", ex: "The medication can affect erection." },
    { en: "ejaculation", ts: "ɪˌdʒækjuˈleɪʃn", ru: "эякуляция", ex: "Premature ejaculation is fairly common." },
    { en: "contraception", ts: "ˌkɒntrəˈsepʃn", ru: "контрацепция", ex: "They discussed methods of contraception." },
    { en: "condom", ts: "ˈkɒndɒm", ru: "презерватив", ex: "Always use a condom to stay safe." },
    { en: "abstinence", ts: "ˈæbstɪnəns", ru: "воздержание", ex: "He chose abstinence before marriage." },
    { en: "celibate", ts: "ˈselɪbət", ru: "соблюдающий целомудрие", ex: "Monks are expected to be celibate." },
    { en: "fertility", ts: "fəˈtɪləti", ru: "фертильность, плодовитость", ex: "Fertility usually declines with age." },
    { en: "conceive", ts: "kənˈsiːv", ru: "зачать", ex: "They tried for years to conceive." },
    { en: "virginity", ts: "vəˈdʒɪnəti", ru: "девственность", ex: "She spoke openly about losing her virginity." },
    { en: "consent", ts: "kənˈsent", ru: "согласие", ex: "Consent must be clear and freely given." },
    { en: "promiscuous", ts: "prəˈmɪskjuəs", ru: "беспорядочный в связях", ex: "He led a promiscuous lifestyle in his twenties." },
    { en: "faithful", ts: "ˈfeɪθfl", ru: "верный", ex: "She stayed faithful to him for decades." },
    { en: "affair", ts: "əˈfeə", ru: "роман, интрижка (измена)", ex: "He had an affair with a colleague." },
    { en: "infidelity", ts: "ˌɪnfɪˈdeləti", ru: "неверность", ex: "Infidelity destroyed their marriage." },
    { en: "courtship", ts: "ˈkɔːtʃɪp", ru: "ухаживание", ex: "Their courtship lasted almost a year." },
    { en: "mate", ts: "meɪt", ru: "спариваться; партнёр", ex: "Many birds mate for life." },
    { en: "naked", ts: "ˈneɪkɪd", ru: "голый", ex: "He stood naked by the window." },
    { en: "nude", ts: "njuːd", ru: "обнажённый", ex: "The painting shows a nude figure." },
    { en: "aphrodisiac", ts: "ˌæfrəˈdɪziæk", ru: "афродизиак", ex: "Oysters are said to be an aphrodisiac." },
    { en: "turn-on", ts: "ˈtɜːnɒn", ru: "то, что возбуждает", ex: "Confidence is a real turn-on for her." },
  ]},
];

WQ_CATEGORIES.forEach((cat) => {
  cat.words.forEach((w, i) => { w.id = "w_" + cat.id + "_" + i; w.catId = cat.id; });
});
const WQ_INDEX = {};
WQ_CATEGORIES.forEach((cat) => cat.words.forEach((w) => { WQ_INDEX[w.id] = w; }));
const WQ_TOTAL_WORDS = WQ_CATEGORIES.reduce((s, c) => s + c.words.length, 0);

/* ---- время (независимо от основного приложения, как в др. квестах) ---- */
const wqDayKey = (t) => new Date(t || Date.now()).toISOString().slice(0, 10);
const wqYesterKey = () => wqDayKey(Date.now() - 86400000);
const wqAddDays = (n) => new Date(Date.now() + n * 86400000).toISOString().slice(0, 10);

/* ---- SM-2-подобный алгоритм интервального повторения -------------------
   quality: 0 (забыл), 3 (трудно), 4 (хорошо), 5 (легко) — тот же диапазон,
   что и в классическом SuperMemo-2, просто без промежуточных 1/2.       */
function wqNextSchedule(prog, quality) {
  let reps = (prog && prog.reps) || 0;
  let ef = (prog && prog.ef) || 2.5;
  let interval = (prog && prog.interval) || 0;
  if (quality < 3) {
    reps = 0;
    interval = 1;
  } else {
    if (reps === 0) interval = 1;
    else if (reps === 1) interval = 6;
    else interval = Math.round(interval * ef);
    reps += 1;
  }
  ef = Math.max(1.3, ef + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)));
  return { reps, ef: Math.round(ef * 100) / 100, interval, due: wqAddDays(interval), lastQuality: quality, lastAt: Date.now() };
}
const WQ_XP_BY_Q = { 0: 1, 1: 1, 2: 2, 3: 5, 4: 8, 5: 12 };
const WQ_MASTERED_INTERVAL = 21; /* дней — считаем слово выученным */

/* ---- store ---------------------------------------------------------------- */
/* v2 — полностью заменён набор слов (B2–C1: война, быт, еда, философия).
   Версию обязательно менять при смене словаря: идентификаторы слов вида
   w_<категория>_<номер> переиспользуются, и старый прогресс/картинки/правки
   молча прилипли бы к другим словам (категория food, например, осталась
   с тем же id, но слова в ней теперь другие). */
const WQ_VERSION = "wordsquest-v2";
function wqMakeSeed() {
  return { progress: {}, images: {}, edits: {}, custom: [], customCats: [], xp: 0, streak: { count: 0, last: null }, day: { key: wqDayKey(), reviews: 0 }, totalReviews: 0 };
}
/* при смене ВСТРОЕННОГО словаря чистим прогресс/правки/картинки встроенных
   слов, но сохраняем накопленную активность И полностью пользовательский
   контент — свои категории и свои карточки: они не привязаны к встроенным
   словам, и терять их при обновлении набора нельзя. */
function wqMigrate(state, savedVersion) {
  if (savedVersion === WQ_VERSION) return state;
  return {
    ...wqMakeSeed(),
    xp: state.xp || 0,
    streak: state.streak || { count: 0, last: null },
    totalReviews: state.totalReviews || 0,
    custom: state.custom || [],
    customCats: state.customCats || [],
  };
}
/* Хранится на сервере (см. makeQuestStore в data.jsx). Картинки вынесены
   отдельной записью через splitKey — иначе каждое повторение слова
   отправляло бы на сервер всю коллекцию картинок целиком. */
window.WQStore = window.makeQuestStore("wordsquest", WQ_VERSION, wqMakeSeed, {
  splitKey: "images",
  migrate: wqMigrate,
  onCacheFull: () => wqToast("Не хватает места в кэше браузера — прогресс сохранён на сервере"),
});
window.useWQ = function () {
  const store = window.WQStore;
  const [, force] = React.useState(0);
  React.useEffect(() => store.subscribe(() => force((n) => n + 1)), []);
  return store.get();
};

/* ---- слова с учётом пользовательских правок и своих карточек -------------
   Встроенный список — константа, поэтому правки живут отдельным слоем
   (state.edits[id] — патч поверх слова), а свои карточки — в state.custom.
   Всё, что показывает или спрашивает слова, должно ходить сюда, а не
   в WQ_CATEGORIES напрямую, иначе правки/свои слова будут не видны. */
function wqApplyEdit(w, edits) {
  const e = edits && edits[w.id];
  return e ? { ...w, ...e } : w;
}
function wqCategories(s) {
  const edits = (s && s.edits) || {};
  const custom = (s && s.custom) || [];
  const builtIn = WQ_CATEGORIES.map((cat) => ({
    ...cat,
    words: [
      ...cat.words.map((w) => wqApplyEdit(w, edits)),
      ...custom.filter((w) => w.catId === cat.id),
    ],
  }));
  /* свои категории идут после встроенных; их слова целиком из state.custom */
  const own = ((s && s.customCats) || []).map((cat) => ({
    ...cat,
    custom: true,
    words: custom.filter((w) => w.catId === cat.id),
  }));
  return [...builtIn, ...own];
}
function wqWord(s, id) {
  if (WQ_INDEX[id]) return wqApplyEdit(WQ_INDEX[id], (s && s.edits) || {});
  return ((s && s.custom) || []).find((w) => w.id === id) || null;
}
const wqIsCustom = (id) => typeof id === "string" && id.indexOf("wc_") === 0;
const wqIsCustomCat = (s, catId) => ((s && s.customCats) || []).some((c) => c.id === catId);
const wqCatExists = (s, catId) => WQ_CATEGORIES.some((c) => c.id === catId) || wqIsCustomCat(s, catId);

/* ---- derived --------------------------------------------------------------- */
function wqCategoryStats(s, cat) {
  const today = wqDayKey();
  let learned = 0, due = 0, fresh = 0;
  cat.words.forEach((w) => {
    const p = s.progress[w.id];
    if (!p) { fresh++; return; }
    if (p.interval >= WQ_MASTERED_INTERVAL) learned++;
    if (p.due <= today) due++;
  });
  return { total: cat.words.length, learned, due, fresh, started: cat.words.length - fresh };
}
function wqSessionQueue(s, cat, opts) {
  const today = wqDayKey();
  const newLimit = (opts && opts.newLimit) || 8;
  const pool = cat ? cat.words : wqCategories(s).flatMap((c) => c.words);
  const due = pool.filter((w) => { const p = s.progress[w.id]; return p && p.due <= today; });
  const fresh = pool.filter((w) => !s.progress[w.id]).slice(0, newLimit);
  /* сначала новые (чтобы познакомиться), потом due вперемешку */
  const shuffledDue = [...due].sort(() => Math.random() - 0.5);
  return [...fresh, ...shuffledDue];
}
function wqOverallStats(s) {
  const today = wqDayKey();
  let learned = 0, due = 0, fresh = 0, total = 0;
  wqCategories(s).forEach((cat) => cat.words.forEach((w) => {
    total++;
    const p = s.progress[w.id];
    if (!p) { fresh++; return; }
    if (p.interval >= WQ_MASTERED_INTERVAL) learned++;
    if (p.due <= today) due++;
  }));
  return { total, learned, due, fresh };
}

/* состояние одного слова: новое / изучается / выучено (+ ждёт ли повторения) */
function wqWordStatus(s, wordId) {
  const p = s.progress[wordId];
  if (!p) return { key: "new", label: "Новое", prog: null, due: false };
  const due = p.due <= wqDayKey();
  if (p.interval >= WQ_MASTERED_INTERVAL) return { key: "learned", label: "Выучено", prog: p, due };
  return { key: "learning", label: "Изучается", prog: p, due };
}

window.WQLookup = {
  categories: wqCategories,
  word: wqWord,
  isCustom: wqIsCustom,
  isCustomCat: wqIsCustomCat,
  wordStatus: wqWordStatus,
  categoryStats: wqCategoryStats,
  sessionQueue: wqSessionQueue,
  overallStats: wqOverallStats,
  dayKey: wqDayKey,
  masteredInterval: WQ_MASTERED_INTERVAL,
  totalWords: WQ_TOTAL_WORDS,
};

/* ---- toast ------------------------------------------------------------------ */
let wqToastSeq = 39000;
const wqToast = (text, pts) => { if (window.pushToast) window.pushToast({ id: ++wqToastSeq, text, pts: pts || 0 }); };

/* ---- actions ------------------------------------------------------------------ */
function wqEnsureDay(s) {
  const k = wqDayKey();
  if (s.day && s.day.key === k) return s;
  return { ...s, day: { key: k, reviews: 0 } };
}
function wqBumpStreak(s) {
  const k = wqDayKey();
  if (s.streak.last === k) return s;
  const count = s.streak.last === wqYesterKey() ? s.streak.count + 1 : 1;
  return { ...s, streak: { count, last: k } };
}

window.WQActions = {
  /* quality: 0 забыл / 3 трудно / 4 хорошо / 5 легко */
  reviewWord(wordId, quality) {
    if (!wqWord(window.WQStore.get(), wordId)) return;
    window.WQStore.set((s0) => {
      let s = wqEnsureDay(s0);
      const prog = wqNextSchedule(s.progress[wordId], quality);
      s = {
        ...s,
        progress: { ...s.progress, [wordId]: prog },
        xp: s.xp + (WQ_XP_BY_Q[quality] || 0),
        day: { ...s.day, reviews: s.day.reviews + 1 },
        totalReviews: s.totalReviews + 1,
      };
      s = wqBumpStreak(s);
      return s;
    });
  },
  /* правка встроенного слова — патчем поверх (оригинал не теряется,
     можно вернуть через resetWord). Для своей карточки правим её саму. */
  editWord(wordId, patch) {
    const clean = {};
    ["en", "ts", "ru", "ex"].forEach((k) => { if (patch[k] != null) clean[k] = String(patch[k]).trim(); });
    if (!clean.en || !clean.ru) { wqToast("Нужны и слово, и перевод"); return; }
    window.WQStore.set((s) => {
      if (wqIsCustom(wordId)) {
        return { ...s, custom: (s.custom || []).map((w) => (w.id === wordId ? { ...w, ...clean } : w)) };
      }
      return { ...s, edits: { ...(s.edits || {}), [wordId]: clean } };
    });
    wqToast("Карточка сохранена");
  },
  /* вернуть встроенному слову исходный текст */
  resetWord(wordId) {
    window.WQStore.set((s) => {
      const edits = { ...(s.edits || {}) };
      delete edits[wordId];
      return { ...s, edits };
    });
    wqToast("Возвращён исходный текст");
  },
  addWord(catId, fields) {
    const en = String(fields.en || "").trim();
    const ru = String(fields.ru || "").trim();
    if (!en || !ru) { wqToast("Нужны и слово, и перевод"); return null; }
    if (!wqCatExists(window.WQStore.get(), catId)) return null;
    const id = "wc_" + Date.now().toString(36) + Math.floor(Math.random() * 1000).toString(36);
    const word = { id, catId, en, ru, ts: String(fields.ts || "").trim(), ex: String(fields.ex || "").trim() };
    window.WQStore.set((s) => ({ ...s, custom: [...(s.custom || []), word] }));
    wqToast("Карточка добавлена");
    return id;
  },
  /* своя категория: только заголовок (иконка одинаковая — «тег»).
     Возвращает id, чтобы экран мог сразу открыть её и предложить добавить слова. */
  addCategory(title) {
    const t = String(title || "").trim();
    if (!t) { wqToast("Введите название категории"); return null; }
    const id = "wcat_" + Date.now().toString(36) + Math.floor(Math.random() * 1000).toString(36);
    window.WQStore.set((s) => ({ ...s, customCats: [...(s.customCats || []), { id, title: t, icon: "tag" }] }));
    wqToast("Категория создана");
    return id;
  },
  renameCategory(catId, title) {
    const t = String(title || "").trim();
    if (!t) return;
    window.WQStore.set((s) => ({
      ...s,
      customCats: (s.customCats || []).map((c) => (c.id === catId ? { ...c, title: t } : c)),
    }));
  },
  /* удаление своей категории уносит и её карточки со всем, что к ним привязано */
  deleteCategory(catId) {
    if (!wqIsCustomCat(window.WQStore.get(), catId)) return;
    window.WQStore.set((s) => {
      const doomed = new Set((s.custom || []).filter((w) => w.catId === catId).map((w) => w.id));
      const images = { ...(s.images || {}) };
      const progress = { ...(s.progress || {}) };
      doomed.forEach((id) => { delete images[id]; delete progress[id]; });
      return {
        ...s,
        customCats: (s.customCats || []).filter((c) => c.id !== catId),
        custom: (s.custom || []).filter((w) => w.catId !== catId),
        images, progress,
      };
    });
    wqToast("Категория удалена");
  },
  /* удаляем только свои карточки; встроенные — не наши, их можно лишь вернуть к оригиналу */
  deleteWord(wordId) {
    if (!wqIsCustom(wordId)) return;
    window.WQStore.set((s) => {
      const images = { ...(s.images || {}) }; delete images[wordId];
      const progress = { ...s.progress }; delete progress[wordId];
      return { ...s, custom: (s.custom || []).filter((w) => w.id !== wordId), images, progress };
    });
    wqToast("Карточка удалена");
  },
  /* картинка-ассоциация к слову (dataURL, уже уменьшенный на клиенте) */
  setWordImage(wordId, dataUrl) {
    if (!wqWord(window.WQStore.get(), wordId) || !dataUrl) return;
    window.WQStore.set((s) => ({ ...s, images: { ...s.images, [wordId]: dataUrl } }));
    if (window.WQStore.get().images[wordId]) wqToast("Картинка добавлена");
  },
  removeWordImage(wordId) {
    window.WQStore.set((s) => {
      const images = { ...s.images };
      delete images[wordId];
      return { ...s, images };
    });
  },
  resetAll() {
    /* set() сам перезапишет и кэш, и серверную запись — удалять
       localStorage напрямую больше нельзя, иначе сброс не доедет до сервера */
    window.WQStore.set(wqMakeSeed());
    wqToast("Прогресс «Английские слова» сброшен");
  },
  resetCategory(catId) {
    window.WQStore.set((s) => {
      const progress = { ...s.progress };
      (WQ_INDEX && WQ_CATEGORIES.find((c) => c.id === catId)?.words || []).forEach((w) => { delete progress[w.id]; });
      return { ...s, progress };
    });
  },
};

window.WQCategories = WQ_CATEGORIES;
window.WQIndex = WQ_INDEX;
