'use strict';
/* ══════════════════════════════════════════════════════════════
   PeriodicTableSim — Периодическая таблица (118 элементов)
   Режимы: стандартный вид, подсветка по типам/блокам,
   графики свойств, боровские оболочки, поиск элементов
   ══════════════════════════════════════════════════════════════ */

/* ── Element data ───────────────────────────────────────────── */
const ELEMENTS = [
  { Z:1,  symbol:'H',  name:'Водород',     mass:1.008,   group:1,  period:1, block:'s', config:'1s¹',            oxStates:[-1,+1], En:2.20, density:0.0899, melt:14.01,  boil:20.28,   type:'nonmetal',      discovered:1766, by:'Кавендиш' },
  { Z:2,  symbol:'He', name:'Гелий',       mass:4.003,   group:18, period:1, block:'s', config:'1s²',            oxStates:[0],     En:null, density:0.1786, melt:0.95,   boil:4.22,    type:'noble',         discovered:1868, by:'Жансен' },
  { Z:3,  symbol:'Li', name:'Литий',       mass:6.941,   group:1,  period:2, block:'s', config:'[He]2s¹',        oxStates:[+1],    En:0.98, density:0.534,  melt:453.65, boil:1603,    type:'alkali',        discovered:1817, by:'Арфведсон' },
  { Z:4,  symbol:'Be', name:'Бериллий',    mass:9.012,   group:2,  period:2, block:'s', config:'[He]2s²',        oxStates:[+2],    En:1.57, density:1.85,   melt:1560,   boil:2742,    type:'alkaline',      discovered:1798, by:'Воклен' },
  { Z:5,  symbol:'B',  name:'Бор',         mass:10.811,  group:13, period:2, block:'p', config:'[He]2s²2p¹',     oxStates:[+3],    En:2.04, density:2.34,   melt:2349,   boil:4200,    type:'metalloid',     discovered:1808, by:'Гей-Люссак' },
  { Z:6,  symbol:'C',  name:'Углерод',     mass:12.011,  group:14, period:2, block:'p', config:'[He]2s²2p²',     oxStates:[-4,+4], En:2.55, density:2.267,  melt:3823,   boil:4098,    type:'nonmetal',      discovered:null, by:'Древний мир' },
  { Z:7,  symbol:'N',  name:'Азот',        mass:14.007,  group:15, period:2, block:'p', config:'[He]2s²2p³',     oxStates:[-3,+5], En:3.04, density:1.251,  melt:63.15,  boil:77.36,   type:'nonmetal',      discovered:1772, by:'Резерфорд' },
  { Z:8,  symbol:'O',  name:'Кислород',    mass:15.999,  group:16, period:2, block:'p', config:'[He]2s²2p⁴',     oxStates:[-2],    En:3.44, density:1.429,  melt:54.36,  boil:90.20,   type:'nonmetal',      discovered:1774, by:'Пристли' },
  { Z:9,  symbol:'F',  name:'Фтор',        mass:18.998,  group:17, period:2, block:'p', config:'[He]2s²2p⁵',     oxStates:[-1],    En:3.98, density:1.696,  melt:53.48,  boil:85.03,   type:'halogen',       discovered:1886, by:'Муассан' },
  { Z:10, symbol:'Ne', name:'Неон',        mass:20.180,  group:18, period:2, block:'p', config:'[He]2s²2p⁶',     oxStates:[0],     En:null, density:0.9002, melt:24.56,  boil:27.07,   type:'noble',         discovered:1898, by:'Рамзай' },
  { Z:11, symbol:'Na', name:'Натрий',      mass:22.990,  group:1,  period:3, block:'s', config:'[Ne]3s¹',        oxStates:[+1],    En:0.93, density:0.971,  melt:370.87, boil:1156,    type:'alkali',        discovered:1807, by:'Дэви' },
  { Z:12, symbol:'Mg', name:'Магний',      mass:24.305,  group:2,  period:3, block:'s', config:'[Ne]3s²',        oxStates:[+2],    En:1.31, density:1.738,  melt:923,    boil:1363,    type:'alkaline',      discovered:1755, by:'Блэк' },
  { Z:13, symbol:'Al', name:'Алюминий',    mass:26.982,  group:13, period:3, block:'p', config:'[Ne]3s²3p¹',     oxStates:[+3],    En:1.61, density:2.70,   melt:933.47, boil:2792,    type:'posttransition',discovered:1825, by:'Эрстед' },
  { Z:14, symbol:'Si', name:'Кремний',     mass:28.086,  group:14, period:3, block:'p', config:'[Ne]3s²3p²',     oxStates:[-4,+4], En:1.90, density:2.329,  melt:1687,   boil:3538,    type:'metalloid',     discovered:1824, by:'Берцелиус' },
  { Z:15, symbol:'P',  name:'Фосфор',      mass:30.974,  group:15, period:3, block:'p', config:'[Ne]3s²3p³',     oxStates:[-3,+5], En:2.19, density:1.823,  melt:317.30, boil:553.65,  type:'nonmetal',      discovered:1669, by:'Бранд' },
  { Z:16, symbol:'S',  name:'Сера',        mass:32.065,  group:16, period:3, block:'p', config:'[Ne]3s²3p⁴',     oxStates:[-2,+6], En:2.58, density:2.07,   melt:388.36, boil:717.87,  type:'nonmetal',      discovered:null, by:'Древний мир' },
  { Z:17, symbol:'Cl', name:'Хлор',        mass:35.453,  group:17, period:3, block:'p', config:'[Ne]3s²3p⁵',     oxStates:[-1,+7], En:3.16, density:3.214,  melt:171.65, boil:239.11,  type:'halogen',       discovered:1774, by:'Шееле' },
  { Z:18, symbol:'Ar', name:'Аргон',       mass:39.948,  group:18, period:3, block:'p', config:'[Ne]3s²3p⁶',     oxStates:[0],     En:null, density:1.784,  melt:83.80,  boil:87.30,   type:'noble',         discovered:1894, by:'Рэлей' },
  { Z:19, symbol:'K',  name:'Калий',       mass:39.098,  group:1,  period:4, block:'s', config:'[Ar]4s¹',        oxStates:[+1],    En:0.82, density:0.862,  melt:336.53, boil:1032,    type:'alkali',        discovered:1807, by:'Дэви' },
  { Z:20, symbol:'Ca', name:'Кальций',     mass:40.078,  group:2,  period:4, block:'s', config:'[Ar]4s²',        oxStates:[+2],    En:1.00, density:1.55,   melt:1115,   boil:1757,    type:'alkaline',      discovered:1808, by:'Дэви' },
  { Z:21, symbol:'Sc', name:'Скандий',     mass:44.956,  group:3,  period:4, block:'d', config:'[Ar]3d¹4s²',     oxStates:[+3],    En:1.36, density:2.985,  melt:1814,   boil:3109,    type:'transition',    discovered:1879, by:'Нильсон' },
  { Z:22, symbol:'Ti', name:'Титан',       mass:47.867,  group:4,  period:4, block:'d', config:'[Ar]3d²4s²',     oxStates:[+4],    En:1.54, density:4.507,  melt:1941,   boil:3560,    type:'transition',    discovered:1791, by:'Грегор' },
  { Z:23, symbol:'V',  name:'Ванадий',     mass:50.942,  group:5,  period:4, block:'d', config:'[Ar]3d³4s²',     oxStates:[+5],    En:1.63, density:6.11,   melt:2183,   boil:3680,    type:'transition',    discovered:1830, by:'Сефстрём' },
  { Z:24, symbol:'Cr', name:'Хром',        mass:51.996,  group:6,  period:4, block:'d', config:'[Ar]3d⁵4s¹',     oxStates:[+3,+6], En:1.66, density:7.19,   melt:2180,   boil:2944,    type:'transition',    discovered:1798, by:'Воклен' },
  { Z:25, symbol:'Mn', name:'Марганец',    mass:54.938,  group:7,  period:4, block:'d', config:'[Ar]3d⁵4s²',     oxStates:[+2,+7], En:1.55, density:7.21,   melt:1519,   boil:2334,    type:'transition',    discovered:1774, by:'Ган' },
  { Z:26, symbol:'Fe', name:'Железо',      mass:55.845,  group:8,  period:4, block:'d', config:'[Ar]3d⁶4s²',     oxStates:[+2,+3], En:1.83, density:7.874,  melt:1811,   boil:3134,    type:'transition',    discovered:null, by:'Древний мир' },
  { Z:27, symbol:'Co', name:'Кобальт',     mass:58.933,  group:9,  period:4, block:'d', config:'[Ar]3d⁷4s²',     oxStates:[+2,+3], En:1.88, density:8.90,   melt:1768,   boil:3200,    type:'transition',    discovered:1735, by:'Брандт' },
  { Z:28, symbol:'Ni', name:'Никель',      mass:58.693,  group:10, period:4, block:'d', config:'[Ar]3d⁸4s²',     oxStates:[+2],    En:1.91, density:8.908,  melt:1728,   boil:3186,    type:'transition',    discovered:1751, by:'Кронстедт' },
  { Z:29, symbol:'Cu', name:'Медь',        mass:63.546,  group:11, period:4, block:'d', config:'[Ar]3d¹⁰4s¹',    oxStates:[+1,+2], En:1.90, density:8.96,   melt:1357.77,boil:2835,    type:'transition',    discovered:null, by:'Древний мир' },
  { Z:30, symbol:'Zn', name:'Цинк',        mass:65.38,   group:12, period:4, block:'d', config:'[Ar]3d¹⁰4s²',    oxStates:[+2],    En:1.65, density:7.14,   melt:692.68, boil:1180,    type:'transition',    discovered:1746, by:'Марграф' },
  { Z:31, symbol:'Ga', name:'Галлий',      mass:69.723,  group:13, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p¹', oxStates:[+3],    En:1.81, density:5.91,   melt:302.91, boil:2477,    type:'posttransition',discovered:1875, by:'Де Буабодран' },
  { Z:32, symbol:'Ge', name:'Германий',    mass:72.630,  group:14, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p²', oxStates:[+4],    En:2.01, density:5.323,  melt:1211.40,boil:3106,    type:'metalloid',     discovered:1886, by:'Винклер' },
  { Z:33, symbol:'As', name:'Мышьяк',      mass:74.922,  group:15, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p³', oxStates:[-3,+5], En:2.18, density:5.776,  melt:1090,   boil:887,     type:'metalloid',     discovered:1250, by:'Альберт Великий' },
  { Z:34, symbol:'Se', name:'Селен',       mass:78.971,  group:16, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁴', oxStates:[-2,+6], En:2.55, density:4.809,  melt:493.65, boil:958,     type:'nonmetal',      discovered:1817, by:'Берцелиус' },
  { Z:35, symbol:'Br', name:'Бром',        mass:79.904,  group:17, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁵', oxStates:[-1,+5], En:2.96, density:3.122,  melt:265.95, boil:332.00,  type:'halogen',       discovered:1826, by:'Балар' },
  { Z:36, symbol:'Kr', name:'Криптон',     mass:83.798,  group:18, period:4, block:'p', config:'[Ar]3d¹⁰4s²4p⁶', oxStates:[0],     En:3.00, density:3.749,  melt:115.79, boil:119.93,  type:'noble',         discovered:1898, by:'Рамзай' },
  { Z:37, symbol:'Rb', name:'Рубидий',     mass:85.468,  group:1,  period:5, block:'s', config:'[Kr]5s¹',        oxStates:[+1],    En:0.82, density:1.532,  melt:312.46, boil:961,     type:'alkali',        discovered:1861, by:'Бунзен' },
  { Z:38, symbol:'Sr', name:'Стронций',    mass:87.62,   group:2,  period:5, block:'s', config:'[Kr]5s²',        oxStates:[+2],    En:0.95, density:2.64,   melt:1050,   boil:1655,    type:'alkaline',      discovered:1790, by:'Кроуфорд' },
  { Z:39, symbol:'Y',  name:'Иттрий',      mass:88.906,  group:3,  period:5, block:'d', config:'[Kr]4d¹5s²',     oxStates:[+3],    En:1.22, density:4.472,  melt:1799,   boil:3609,    type:'transition',    discovered:1794, by:'Гадолин' },
  { Z:40, symbol:'Zr', name:'Цирконий',    mass:91.224,  group:4,  period:5, block:'d', config:'[Kr]4d²5s²',     oxStates:[+4],    En:1.33, density:6.52,   melt:2128,   boil:4682,    type:'transition',    discovered:1789, by:'Клапрот' },
  { Z:41, symbol:'Nb', name:'Ниобий',      mass:92.906,  group:5,  period:5, block:'d', config:'[Kr]4d⁴5s¹',     oxStates:[+5],    En:1.6,  density:8.57,   melt:2750,   boil:5017,    type:'transition',    discovered:1801, by:'Хатчетт' },
  { Z:42, symbol:'Mo', name:'Молибден',    mass:95.95,   group:6,  period:5, block:'d', config:'[Kr]4d⁵5s¹',     oxStates:[+6],    En:2.16, density:10.28,  melt:2896,   boil:4912,    type:'transition',    discovered:1781, by:'Шееле' },
  { Z:43, symbol:'Tc', name:'Технеций',    mass:98,      group:7,  period:5, block:'d', config:'[Kr]4d⁵5s²',     oxStates:[+7],    En:1.9,  density:11.50,  melt:2430,   boil:4538,    type:'transition',    discovered:1937, by:'Перье' },
  { Z:44, symbol:'Ru', name:'Рутений',     mass:101.07,  group:8,  period:5, block:'d', config:'[Kr]4d⁷5s¹',     oxStates:[+4],    En:2.2,  density:12.45,  melt:2607,   boil:4423,    type:'transition',    discovered:1844, by:'Клаус' },
  { Z:45, symbol:'Rh', name:'Родий',       mass:102.906, group:9,  period:5, block:'d', config:'[Kr]4d⁸5s¹',     oxStates:[+3],    En:2.28, density:12.41,  melt:2237,   boil:3968,    type:'transition',    discovered:1803, by:'Воластон' },
  { Z:46, symbol:'Pd', name:'Палладий',    mass:106.42,  group:10, period:5, block:'d', config:'[Kr]4d¹⁰',       oxStates:[+2],    En:2.20, density:12.023, melt:1828.05,boil:3236,    type:'transition',    discovered:1803, by:'Воластон' },
  { Z:47, symbol:'Ag', name:'Серебро',     mass:107.868, group:11, period:5, block:'d', config:'[Kr]4d¹⁰5s¹',    oxStates:[+1],    En:1.93, density:10.49,  melt:1234.93,boil:2435,    type:'transition',    discovered:null, by:'Древний мир' },
  { Z:48, symbol:'Cd', name:'Кадмий',      mass:112.414, group:12, period:5, block:'d', config:'[Kr]4d¹⁰5s²',    oxStates:[+2],    En:1.69, density:8.65,   melt:594.22, boil:1040,    type:'transition',    discovered:1817, by:'Штромейер' },
  { Z:49, symbol:'In', name:'Индий',       mass:114.818, group:13, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p¹', oxStates:[+3],    En:1.78, density:7.31,   melt:429.75, boil:2345,    type:'posttransition',discovered:1863, by:'Рейх' },
  { Z:50, symbol:'Sn', name:'Олово',       mass:118.710, group:14, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p²', oxStates:[+2,+4], En:1.96, density:7.287,  melt:505.08, boil:2875,    type:'posttransition',discovered:null, by:'Древний мир' },
  { Z:51, symbol:'Sb', name:'Сурьма',      mass:121.760, group:15, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p³', oxStates:[-3,+5], En:2.05, density:6.697,  melt:903.78, boil:1860,    type:'metalloid',     discovered:null, by:'Древний мир' },
  { Z:52, symbol:'Te', name:'Теллур',      mass:127.60,  group:16, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁴', oxStates:[-2,+6], En:2.1,  density:6.24,   melt:722.66, boil:1261,    type:'metalloid',     discovered:1782, by:'фон Райхенштайн' },
  { Z:53, symbol:'I',  name:'Йод',         mass:126.904, group:17, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁵', oxStates:[-1,+7], En:2.66, density:4.933,  melt:386.85, boil:457.55,  type:'halogen',       discovered:1811, by:'Куртуа' },
  { Z:54, symbol:'Xe', name:'Ксенон',      mass:131.293, group:18, period:5, block:'p', config:'[Kr]4d¹⁰5s²5p⁶', oxStates:[0],     En:2.6,  density:5.894,  melt:161.40, boil:165.05,  type:'noble',         discovered:1898, by:'Рамзай' },
  { Z:55, symbol:'Cs', name:'Цезий',       mass:132.905, group:1,  period:6, block:'s', config:'[Xe]6s¹',        oxStates:[+1],    En:0.79, density:1.873,  melt:301.59, boil:944,     type:'alkali',        discovered:1860, by:'Бунзен' },
  { Z:56, symbol:'Ba', name:'Барий',       mass:137.327, group:2,  period:6, block:'s', config:'[Xe]6s²',        oxStates:[+2],    En:0.89, density:3.594,  melt:1000,   boil:2170,    type:'alkaline',      discovered:1808, by:'Дэви' },
  { Z:57, symbol:'La', name:'Лантан',      mass:138.905, group:3,  period:6, block:'f', config:'[Xe]5d¹6s²',     oxStates:[+3],    En:1.10, density:6.162,  melt:1193,   boil:3737,    type:'lanthanide',    discovered:1839, by:'Мосандер' },
  { Z:58, symbol:'Ce', name:'Церий',       mass:140.116, group:null,period:6, block:'f', config:'[Xe]4f¹5d¹6s²', oxStates:[+3,+4], En:1.12, density:6.770,  melt:1068,   boil:3716,    type:'lanthanide',    discovered:1803, by:'Берцелиус' },
  { Z:59, symbol:'Pr', name:'Празеодим',   mass:140.908, group:null,period:6, block:'f', config:'[Xe]4f³6s²',     oxStates:[+3],    En:1.13, density:6.77,   melt:1208,   boil:3793,    type:'lanthanide',    discovered:1885, by:'фон Вельсбах' },
  { Z:60, symbol:'Nd', name:'Неодим',      mass:144.242, group:null,period:6, block:'f', config:'[Xe]4f⁴6s²',     oxStates:[+3],    En:1.14, density:7.01,   melt:1297,   boil:3347,    type:'lanthanide',    discovered:1885, by:'фон Вельсбах' },
  { Z:61, symbol:'Pm', name:'Прометий',    mass:145,     group:null,period:6, block:'f', config:'[Xe]4f⁵6s²',     oxStates:[+3],    En:1.13, density:7.26,   melt:1315,   boil:3273,    type:'lanthanide',    discovered:1945, by:'Маринский' },
  { Z:62, symbol:'Sm', name:'Самарий',     mass:150.36,  group:null,period:6, block:'f', config:'[Xe]4f⁶6s²',     oxStates:[+2,+3], En:1.17, density:7.52,   melt:1345,   boil:2067,    type:'lanthanide',    discovered:1879, by:'Буабодран' },
  { Z:63, symbol:'Eu', name:'Европий',     mass:151.964, group:null,period:6, block:'f', config:'[Xe]4f⁷6s²',     oxStates:[+2,+3], En:1.20, density:5.244,  melt:1099,   boil:1802,    type:'lanthanide',    discovered:1901, by:'Демарсе' },
  { Z:64, symbol:'Gd', name:'Гадолиний',   mass:157.25,  group:null,period:6, block:'f', config:'[Xe]4f⁷5d¹6s²', oxStates:[+3],    En:1.20, density:7.90,   melt:1585,   boil:3546,    type:'lanthanide',    discovered:1880, by:'Мариньяк' },
  { Z:65, symbol:'Tb', name:'Тербий',      mass:158.925, group:null,period:6, block:'f', config:'[Xe]4f⁹6s²',     oxStates:[+3],    En:1.10, density:8.23,   melt:1629,   boil:3503,    type:'lanthanide',    discovered:1843, by:'Мосандер' },
  { Z:66, symbol:'Dy', name:'Диспрозий',   mass:162.500, group:null,period:6, block:'f', config:'[Xe]4f¹⁰6s²',    oxStates:[+3],    En:1.22, density:8.540,  melt:1680,   boil:2840,    type:'lanthanide',    discovered:1886, by:'Буабодран' },
  { Z:67, symbol:'Ho', name:'Гольмий',     mass:164.930, group:null,period:6, block:'f', config:'[Xe]4f¹¹6s²',    oxStates:[+3],    En:1.23, density:8.795,  melt:1734,   boil:2993,    type:'lanthanide',    discovered:1878, by:'Клеве' },
  { Z:68, symbol:'Er', name:'Эрбий',       mass:167.259, group:null,period:6, block:'f', config:'[Xe]4f¹²6s²',    oxStates:[+3],    En:1.24, density:9.066,  melt:1802,   boil:3141,    type:'lanthanide',    discovered:1843, by:'Мосандер' },
  { Z:69, symbol:'Tm', name:'Тулий',       mass:168.934, group:null,period:6, block:'f', config:'[Xe]4f¹³6s²',    oxStates:[+3],    En:1.25, density:9.32,   melt:1818,   boil:2223,    type:'lanthanide',    discovered:1879, by:'Клеве' },
  { Z:70, symbol:'Yb', name:'Иттербий',    mass:173.054, group:null,period:6, block:'f', config:'[Xe]4f¹⁴6s²',    oxStates:[+2,+3], En:1.10, density:6.90,   melt:1097,   boil:1469,    type:'lanthanide',    discovered:1878, by:'Мариньяк' },
  { Z:71, symbol:'Lu', name:'Лютеций',     mass:174.967, group:3,  period:6, block:'d', config:'[Xe]4f¹⁴5d¹6s²', oxStates:[+3],    En:1.27, density:9.841,  melt:1925,   boil:3675,    type:'lanthanide',    discovered:1907, by:'Урбен' },
  { Z:72, symbol:'Hf', name:'Гафний',      mass:178.49,  group:4,  period:6, block:'d', config:'[Xe]4f¹⁴5d²6s²', oxStates:[+4],    En:1.3,  density:13.31,  melt:2506,   boil:4876,    type:'transition',    discovered:1923, by:'Костер' },
  { Z:73, symbol:'Ta', name:'Тантал',      mass:180.948, group:5,  period:6, block:'d', config:'[Xe]4f¹⁴5d³6s²', oxStates:[+5],    En:1.5,  density:16.69,  melt:3290,   boil:5731,    type:'transition',    discovered:1802, by:'Экеберг' },
  { Z:74, symbol:'W',  name:'Вольфрам',    mass:183.84,  group:6,  period:6, block:'d', config:'[Xe]4f¹⁴5d⁴6s²', oxStates:[+6],    En:2.36, density:19.25,  melt:3695,   boil:5828,    type:'transition',    discovered:1783, by:'Братья дель Риo' },
  { Z:75, symbol:'Re', name:'Рений',       mass:186.207, group:7,  period:6, block:'d', config:'[Xe]4f¹⁴5d⁵6s²', oxStates:[+7],    En:1.9,  density:21.02,  melt:3459,   boil:5869,    type:'transition',    discovered:1925, by:'Ноддак' },
  { Z:76, symbol:'Os', name:'Осмий',       mass:190.23,  group:8,  period:6, block:'d', config:'[Xe]4f¹⁴5d⁶6s²', oxStates:[+4],    En:2.2,  density:22.59,  melt:3306,   boil:5285,    type:'transition',    discovered:1803, by:'Теннант' },
  { Z:77, symbol:'Ir', name:'Иридий',      mass:192.217, group:9,  period:6, block:'d', config:'[Xe]4f¹⁴5d⁷6s²', oxStates:[+4],    En:2.20, density:22.56,  melt:2719,   boil:4701,    type:'transition',    discovered:1803, by:'Теннант' },
  { Z:78, symbol:'Pt', name:'Платина',     mass:195.084, group:10, period:6, block:'d', config:'[Xe]4f¹⁴5d⁹6s¹', oxStates:[+2,+4], En:2.28, density:21.45,  melt:2041.4, boil:4098,    type:'transition',    discovered:1735, by:'де Улоа' },
  { Z:79, symbol:'Au', name:'Золото',      mass:196.967, group:11, period:6, block:'d', config:'[Xe]4f¹⁴5d¹⁰6s¹',oxStates:[+1,+3], En:2.54, density:19.30,  melt:1337.33,boil:3129,    type:'transition',    discovered:null, by:'Древний мир' },
  { Z:80, symbol:'Hg', name:'Ртуть',       mass:200.592, group:12, period:6, block:'d', config:'[Xe]4f¹⁴5d¹⁰6s²',oxStates:[+1,+2], En:2.00, density:13.534, melt:234.32, boil:629.88,  type:'transition',    discovered:null, by:'Древний мир' },
  { Z:81, symbol:'Tl', name:'Таллий',      mass:204.38,  group:13, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p¹',oxStates:[+1,+3],En:1.62,density:11.85, melt:577,    boil:1746,    type:'posttransition',discovered:1861, by:'Крукс' },
  { Z:82, symbol:'Pb', name:'Свинец',      mass:207.2,   group:14, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p²',oxStates:[+2,+4],En:2.33,density:11.34, melt:600.61, boil:2022,    type:'posttransition',discovered:null, by:'Древний мир' },
  { Z:83, symbol:'Bi', name:'Висмут',      mass:208.980, group:15, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p³',oxStates:[+3],   En:2.02,density:9.747, melt:544.55, boil:1837,    type:'posttransition',discovered:1753, by:'Жоффруа' },
  { Z:84, symbol:'Po', name:'Полоний',     mass:209,     group:16, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁴',oxStates:[+4],   En:2.0, density:9.32,  melt:527,    boil:1235,    type:'metalloid',     discovered:1898, by:'Кюри' },
  { Z:85, symbol:'At', name:'Астат',       mass:210,     group:17, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁵',oxStates:[-1,+1],En:2.2, density:null,  melt:575,    boil:null,    type:'halogen',       discovered:1940, by:'Корсон' },
  { Z:86, symbol:'Rn', name:'Радон',       mass:222,     group:18, period:6, block:'p', config:'[Xe]4f¹⁴5d¹⁰6s²6p⁶',oxStates:[0],    En:null,density:9.73,  melt:202,    boil:211.45,  type:'noble',         discovered:1900, by:'Дорн' },
  { Z:87, symbol:'Fr', name:'Франций',     mass:223,     group:1,  period:7, block:'s', config:'[Rn]7s¹',        oxStates:[+1],    En:0.7,  density:null,   melt:300,    boil:950,     type:'alkali',        discovered:1939, by:'Перей' },
  { Z:88, symbol:'Ra', name:'Радий',       mass:226,     group:2,  period:7, block:'s', config:'[Rn]7s²',        oxStates:[+2],    En:0.9,  density:5.0,    melt:973,    boil:2010,    type:'alkaline',      discovered:1898, by:'Кюри' },
  { Z:89, symbol:'Ac', name:'Актиний',     mass:227,     group:3,  period:7, block:'f', config:'[Rn]6d¹7s²',     oxStates:[+3],    En:1.1,  density:10.07,  melt:1323,   boil:3471,    type:'actinide',      discovered:1899, by:'Дебьерн' },
  { Z:90, symbol:'Th', name:'Торий',       mass:232.038, group:null,period:7, block:'f', config:'[Rn]6d²7s²',     oxStates:[+4],    En:1.3,  density:11.72,  melt:2115,   boil:5061,    type:'actinide',      discovered:1828, by:'Берцелиус' },
  { Z:91, symbol:'Pa', name:'Протактиний', mass:231.036, group:null,period:7, block:'f', config:'[Rn]5f²6d¹7s²',  oxStates:[+5],    En:1.5,  density:15.37,  melt:1841,   boil:4300,    type:'actinide',      discovered:1913, by:'Фаянс' },
  { Z:92, symbol:'U',  name:'Уран',        mass:238.029, group:null,period:7, block:'f', config:'[Rn]5f³6d¹7s²',  oxStates:[+6],    En:1.38, density:19.05,  melt:1405.3, boil:4404,    type:'actinide',      discovered:1789, by:'Клапрот' },
  { Z:93, symbol:'Np', name:'Нептуний',    mass:237,     group:null,period:7, block:'f', config:'[Rn]5f⁴6d¹7s²',  oxStates:[+5],    En:1.36, density:20.25,  melt:913,    boil:4273,    type:'actinide',      discovered:1940, by:'МакМиллан' },
  { Z:94, symbol:'Pu', name:'Плутоний',    mass:244,     group:null,period:7, block:'f', config:'[Rn]5f⁶7s²',     oxStates:[+4],    En:1.28, density:19.84,  melt:912.5,  boil:3501,    type:'actinide',      discovered:1940, by:'Сиборг' },
  { Z:95, symbol:'Am', name:'Америций',    mass:243,     group:null,period:7, block:'f', config:'[Rn]5f⁷7s²',     oxStates:[+3],    En:1.3,  density:13.67,  melt:1449,   boil:2880,    type:'actinide',      discovered:1944, by:'Сиборг' },
  { Z:96, symbol:'Cm', name:'Кюрий',       mass:247,     group:null,period:7, block:'f', config:'[Rn]5f⁷6d¹7s²',  oxStates:[+3],    En:1.3,  density:13.51,  melt:1613,   boil:3383,    type:'actinide',      discovered:1944, by:'Сиборг' },
  { Z:97, symbol:'Bk', name:'Берклий',     mass:247,     group:null,period:7, block:'f', config:'[Rn]5f⁹7s²',     oxStates:[+3],    En:1.3,  density:14.79,  melt:1259,   boil:null,    type:'actinide',      discovered:1949, by:'Сиборг' },
  { Z:98, symbol:'Cf', name:'Калифорний',  mass:251,     group:null,period:7, block:'f', config:'[Rn]5f¹⁰7s²',    oxStates:[+3],    En:1.3,  density:15.1,   melt:1173,   boil:null,    type:'actinide',      discovered:1950, by:'Сиборг' },
  { Z:99, symbol:'Es', name:'Эйнштейний',  mass:252,     group:null,period:7, block:'f', config:'[Rn]5f¹¹7s²',    oxStates:[+3],    En:1.3,  density:null,   melt:1133,   boil:null,    type:'actinide',      discovered:1952, by:'Гиорсо' },
  { Z:100,symbol:'Fm', name:'Фермий',      mass:257,     group:null,period:7, block:'f', config:'[Rn]5f¹²7s²',    oxStates:[+3],    En:1.3,  density:null,   melt:1800,   boil:null,    type:'actinide',      discovered:1952, by:'Гиорсо' },
  { Z:101,symbol:'Md', name:'Менделевий',  mass:258,     group:null,period:7, block:'f', config:'[Rn]5f¹³7s²',    oxStates:[+3],    En:1.3,  density:null,   melt:1100,   boil:null,    type:'actinide',      discovered:1955, by:'Гиорсо' },
  { Z:102,symbol:'No', name:'Нобелий',     mass:259,     group:null,period:7, block:'f', config:'[Rn]5f¹⁴7s²',    oxStates:[+2],    En:1.3,  density:null,   melt:1100,   boil:null,    type:'actinide',      discovered:1958, by:'Флёров' },
  { Z:103,symbol:'Lr', name:'Лоуренсий',   mass:266,     group:3,  period:7, block:'d', config:'[Rn]5f¹⁴7s²7p¹', oxStates:[+3],    En:1.3,  density:null,   melt:1900,   boil:null,    type:'actinide',      discovered:1961, by:'Гиорсо' },
  { Z:104,symbol:'Rf', name:'Резерфордий', mass:267,     group:4,  period:7, block:'d', config:'[Rn]5f¹⁴6d²7s²', oxStates:[+4],    En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1964, by:'Флёров' },
  { Z:105,symbol:'Db', name:'Дубний',      mass:268,     group:5,  period:7, block:'d', config:'[Rn]5f¹⁴6d³7s²', oxStates:[+5],    En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1968, by:'Флёров' },
  { Z:106,symbol:'Sg', name:'Сиборгий',    mass:269,     group:6,  period:7, block:'d', config:'[Rn]5f¹⁴6d⁴7s²', oxStates:[+6],    En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1974, by:'Флёров' },
  { Z:107,symbol:'Bh', name:'Борий',       mass:270,     group:7,  period:7, block:'d', config:'[Rn]5f¹⁴6d⁵7s²', oxStates:[+7],    En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1981, by:'ГСИ' },
  { Z:108,symbol:'Hs', name:'Хассий',      mass:277,     group:8,  period:7, block:'d', config:'[Rn]5f¹⁴6d⁶7s²', oxStates:[+8],    En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1984, by:'ГСИ' },
  { Z:109,symbol:'Mt', name:'Мейтнерий',   mass:278,     group:9,  period:7, block:'d', config:'[Rn]5f¹⁴6d⁷7s²', oxStates:[null],  En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1982, by:'ГСИ' },
  { Z:110,symbol:'Ds', name:'Дармштадтий', mass:281,     group:10, period:7, block:'d', config:'[Rn]5f¹⁴6d⁸7s²', oxStates:[null],  En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1994, by:'ГСИ' },
  { Z:111,symbol:'Rg', name:'Рентгений',   mass:282,     group:11, period:7, block:'d', config:'[Rn]5f¹⁴6d⁹7s²', oxStates:[null],  En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1994, by:'ГСИ' },
  { Z:112,symbol:'Cn', name:'Коперниций',  mass:285,     group:12, period:7, block:'d', config:'[Rn]5f¹⁴6d¹⁰7s²',oxStates:[null],  En:null, density:null,   melt:null,   boil:null,    type:'transition',    discovered:1996, by:'ГСИ' },
  { Z:113,symbol:'Nh', name:'Нихоний',     mass:286,     group:13, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p¹',oxStates:[null],En:null, density:null,  melt:null,   boil:null,    type:'posttransition',discovered:2004, by:'РИКЕН' },
  { Z:114,symbol:'Fl', name:'Флеровий',    mass:289,     group:14, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p²',oxStates:[null],En:null, density:null,  melt:null,   boil:null,    type:'posttransition',discovered:1998, by:'Флёров' },
  { Z:115,symbol:'Mc', name:'Московий',    mass:290,     group:15, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p³',oxStates:[null],En:null, density:null,  melt:null,   boil:null,    type:'posttransition',discovered:2003, by:'Флёров' },
  { Z:116,symbol:'Lv', name:'Ливерморий',  mass:293,     group:16, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁴',oxStates:[null],En:null, density:null,  melt:null,   boil:null,    type:'posttransition',discovered:2000, by:'Флёров' },
  { Z:117,symbol:'Ts', name:'Теннессин',   mass:294,     group:17, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁵',oxStates:[null],En:null, density:null,  melt:null,   boil:null,    type:'halogen',       discovered:2010, by:'Флёров' },
  { Z:118,symbol:'Og', name:'Оганессон',   mass:294,     group:18, period:7, block:'p', config:'[Rn]5f¹⁴6d¹⁰7s²7p⁶',oxStates:[null],En:null, density:null,  melt:null,   boil:null,    type:'noble',         discovered:2002, by:'Флёров' },
];

/* ── Colour palette per type ──────────────────────────────── */
const TYPE_COLORS = {
  alkali:        '#EF476F',
  alkaline:      '#FF6B35',
  transition:    '#7B8EF7',
  posttransition:'#06D6E0',
  metalloid:     '#7BF5A4',
  nonmetal:      '#FFD166',
  halogen:       '#C77DFF',
  noble:         '#A8DADC',
  lanthanide:    '#9B5DE5',
  actinide:      '#F15BB5',
  metal:         '#7B8EF7',
};
const TYPE_LABELS = {
  alkali:        'Щелочные металлы',
  alkaline:      'Щёлочноземельные',
  transition:    'Переходные металлы',
  posttransition:'Постпереходные',
  metalloid:     'Металлоиды',
  nonmetal:      'Неметаллы',
  halogen:       'Галогены',
  noble:         'Благородные газы',
  lanthanide:    'Лантаноиды',
  actinide:      'Актиноиды',
};
const BLOCK_COLORS = {
  s: '#EF476F',
  p: '#06D6E0',
  d: '#7B8EF7',
  f: '#9B5DE5',
};

/* ── Электронные оболочки (K,L,M,N,O,P,Q) ── */
const SHELL_CAPACITY = [2, 8, 18, 32, 32, 18, 8];

function getShellFill(Z) {
  const caps = SHELL_CAPACITY;
  const shells = [];
  let rem = Z;
  for (let i = 0; i < caps.length && rem > 0; i++) {
    const n = Math.min(rem, caps[i]);
    shells.push(n);
    rem -= n;
  }
  return shells;
}

/* ── Layout helpers ──────────────────────────────────────────── */
/* Standard 18-column layout: returns {col, row} for each element */
function getCell(el) {
  if (el.type === 'lanthanide' && el.Z !== 57 && el.Z !== 71) {
    return { col: el.Z - 57 + 3, row: 9 }; // lanthanide row
  }
  if (el.type === 'actinide' && el.Z !== 89 && el.Z !== 103) {
    return { col: el.Z - 89 + 3, row: 10 }; // actinide row
  }
  const g = el.group;
  const p = el.period;
  if (!g) return null;
  return { col: g, row: p };
}

/* ══════════════════════════════════════════════════════════════
   CLASS
   ══════════════════════════════════════════════════════════════ */
class PeriodicTableSim {
  constructor(wrap) {
    this._wrap = wrap;
    this._mode = 'type';      // type | block | none
    this._selected = null;    // element Z
    this._searchQ  = '';
    this._highlighted = new Set(); // Zs matching search
    this._propKey  = 'En';    // property for chart
    this._chartBy  = 'period';
    this._chartN   = 2;       // period number or group number
    this._bohrZ    = null;    // Z for Bohr shell panel
    this._bohrRaf  = null;
    this._bohrAngle = 0;

    // visual modes (wave B)
    this._heatProp   = 'En';      // heatmap property key
    this._heatLog    = false;     // log scale
    this._heatActive = false;     // heatmap mode on
    this._trendProp  = 'radius';  // trend arrows property
    this._trendOn    = false;     // trend arrows visible
    this._tableShape = 'std';     // std | long | short
    this._3dActive   = false;     // three.js 3D mode
    this._3dMode     = 'bar';     // bar | wave | stack
    this._3dScene    = null;
    this._3dRaf      = null;
    this._heatTweens = [];        // active tween handles

    // build
    this._buildUI();
    this._buildTable();
    this._updateCard(null);
    this._buildVisualModes();

    // chart defaults
    this._drawChart();
  }

  /* ─────────────────────────────────────────────────────
     UI BUILD
  ───────────────────────────────────────────────────── */
  _buildUI() {
    this._wrap.innerHTML = '';
    this._wrap.style.cssText = 'display:flex;flex-direction:column;height:100%;min-height:0;background:#0D0D1A;overflow:hidden;';

    /* top toolbar */
    const toolbar = document.createElement('div');
    toolbar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.08);flex-wrap:wrap;flex-shrink:0;';
    toolbar.innerHTML = `
      <span style="font-size:.72rem;font-weight:700;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.07em">Режим:</span>
      <button class="ptbl-mode-btn active" data-m="type">По типу</button>
      <button class="ptbl-mode-btn" data-m="block">По блоку</button>
      <button class="ptbl-mode-btn" data-m="none">Без подсветки</button>
      <span style="margin-left:8px;font-size:.72rem;font-weight:700;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.07em">Поиск:</span>
      <input id="ptbl-search" type="text" placeholder="Символ / название / Z / масса"
        style="padding:4px 8px;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.06);color:#fff;font-size:.78rem;width:200px;outline:none;">
      <button id="ptbl-search-clear" style="padding:3px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#aaa;font-size:.72rem;cursor:pointer">
        <svg class="ic" viewBox="0 0 24 24" style="width:12px;height:12px"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
      </button>`;
    this._wrap.appendChild(toolbar);

    /* mode buttons */
    toolbar.querySelectorAll('.ptbl-mode-btn').forEach(btn => {
      btn.style.cssText = 'padding:4px 10px;border-radius:6px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#aaa;font-size:.75rem;cursor:pointer;transition:all .15s';
      btn.addEventListener('click', () => {
        toolbar.querySelectorAll('.ptbl-mode-btn').forEach(b => { b.style.background='transparent'; b.style.color='#aaa'; b.classList.remove('active'); });
        btn.style.background='rgba(155,93,229,0.25)'; btn.style.color='#fff'; btn.classList.add('active');
        this._mode = btn.dataset.m;
        if (window.LabFX) LabFX.sound.play('whoosh', { pitch: 1.2, volume: 0.25 });
        this._colorTable();
      });
    });
    /* set initial active style */
    toolbar.querySelector('.ptbl-mode-btn.active').style.background = 'rgba(155,93,229,0.25)';
    toolbar.querySelector('.ptbl-mode-btn.active').style.color = '#fff';

    /* search */
    const si = toolbar.querySelector('#ptbl-search');
    si.addEventListener('input', () => { this._searchQ = si.value.trim().toLowerCase(); this._applySearch(); });
    toolbar.querySelector('#ptbl-search-clear').addEventListener('click', () => { si.value=''; this._searchQ=''; this._applySearch(); });

    /* main area: table + right panel */
    const main = document.createElement('div');
    main.style.cssText = 'display:flex;flex:1;min-height:0;overflow:hidden;';
    this._wrap.appendChild(main);

    /* left: table + legend */
    const leftCol = document.createElement('div');
    leftCol.style.cssText = 'flex:1;display:flex;flex-direction:column;min-width:0;overflow:auto;padding:8px 4px 8px 8px;';
    main.appendChild(leftCol);

    /* table grid container */
    this._tableEl = document.createElement('div');
    this._tableEl.id = 'ptbl-grid';
    this._tableEl.style.cssText = 'display:grid;grid-template-columns:repeat(18,1fr);gap:2px;min-width:540px;';
    leftCol.appendChild(this._tableEl);

    /* gap filler row */
    const gapDiv = document.createElement('div');
    gapDiv.style.cssText = 'height:8px;';
    leftCol.appendChild(gapDiv);

    /* f-block (lanthanide/actinide) */
    this._fblockEl = document.createElement('div');
    this._fblockEl.id = 'ptbl-fblock';
    this._fblockEl.style.cssText = 'display:grid;grid-template-columns:repeat(15,1fr);gap:2px;min-width:540px;margin-left:calc(2*100%/18 + 4px);';
    leftCol.appendChild(this._fblockEl);

    /* legend */
    this._legendEl = document.createElement('div');
    this._legendEl.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;min-width:540px;';
    leftCol.appendChild(this._legendEl);

    /* right panel */
    const rightCol = document.createElement('div');
    rightCol.style.cssText = 'width:280px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid rgba(255,255,255,0.07);overflow:hidden;';
    main.appendChild(rightCol);

    /* element card */
    this._cardEl = document.createElement('div');
    this._cardEl.id = 'ptbl-card';
    this._cardEl.style.cssText = 'flex:1;overflow:hidden;display:flex;flex-direction:column;font-size:.78rem;color:#ccc;';
    rightCol.appendChild(this._cardEl);

    /* Bohr shells canvas */
    const bohrWrap = document.createElement('div');
    bohrWrap.style.cssText = 'height:150px;flex-shrink:0;border-top:1px solid rgba(255,255,255,0.07);background:rgba(0,0,0,0.3);position:relative;';
    this._bohrCanvas = document.createElement('canvas');
    this._bohrCanvas.style.cssText = 'width:100%;height:100%;';
    bohrWrap.appendChild(this._bohrCanvas);
    rightCol.appendChild(bohrWrap);

    /* chart panel */
    const chartPan = document.createElement('div');
    chartPan.style.cssText = 'flex-shrink:0;border-top:1px solid rgba(255,255,255,0.07);padding:8px;background:rgba(0,0,0,0.2);';
    chartPan.innerHTML = `
      <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:6px;">
        <span style="font-size:.7rem;color:rgba(255,255,255,0.4)">Свойство:</span>
        <select id="ptbl-prop-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
          <option value="En">ЭО (Полинг)</option>
          <option value="mass">Масса</option>
          <option value="melt">T плавл. (K)</option>
          <option value="boil">T кип. (K)</option>
          <option value="density">Плотность</option>
        </select>
        <select id="ptbl-by-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
          <option value="period">По периоду</option>
          <option value="group">По группе</option>
        </select>
        <select id="ptbl-n-sel" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
          ${[1,2,3,4,5,6,7].map(n=>`<option value="${n}" ${n===2?'selected':''}>№ ${n}</option>`).join('')}
        </select>
      </div>
      <canvas id="ptbl-chart" style="width:100%;height:90px;display:block;"></canvas>`;
    rightCol.appendChild(chartPan);

    chartPan.querySelector('#ptbl-prop-sel').addEventListener('change', e => { this._propKey=e.target.value; this._drawChart(); });
    chartPan.querySelector('#ptbl-by-sel').addEventListener('change', e => {
      this._chartBy=e.target.value;
      const nSel = chartPan.querySelector('#ptbl-n-sel');
      if (this._chartBy === 'group') {
        nSel.innerHTML = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18].map(n=>`<option value="${n}" ${n===1?'selected':''}>Гр. ${n}</option>`).join('');
        this._chartN = 1;
      } else {
        nSel.innerHTML = [1,2,3,4,5,6,7].map(n=>`<option value="${n}" ${n===2?'selected':''}>№ ${n}</option>`).join('');
        this._chartN = 2;
      }
      this._drawChart();
    });
    chartPan.querySelector('#ptbl-n-sel').addEventListener('change', e => { this._chartN=+e.target.value; this._drawChart(); });

    this._chartCanvas = chartPan.querySelector('#ptbl-chart');
    new ResizeObserver(() => this._drawChart()).observe(this._chartCanvas);
    new ResizeObserver(() => this._drawBohr()).observe(this._bohrCanvas);
  }

  /* ─────────────────────────────────────────────────────
     TABLE BUILD
  ───────────────────────────────────────────────────── */
  _buildTable() {
    /* create placeholder grid for rows 1-7 × cols 1-18 */
    const cells = {}; // 'row,col' → div
    for (let r = 1; r <= 7; r++) {
      for (let c = 1; c <= 18; c++) {
        const d = document.createElement('div');
        d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
        cells[`${r},${c}`] = d;
        this._tableEl.appendChild(d);
      }
    }

    /* f-block rows */
    const fCells = { 9: {}, 10: {} }; // lanthanides / actinides
    for (let fc = 1; fc <= 15; fc++) {
      for (const fr of [9, 10]) {
        const d = document.createElement('div');
        d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
        fCells[fr][fc] = d;
        this._fblockEl.appendChild(d);
      }
    }

    /* place elements */
    this._cellMap = {}; // Z → div
    for (const el of ELEMENTS) {
      const pos = getCell(el);
      if (!pos) continue;
      let div;
      if (pos.row <= 7) {
        div = cells[`${pos.row},${pos.col}`];
      } else {
        const fCol = pos.col - 2; // 3..17 → 1..15
        div = fCells[pos.row][fCol];
      }
      if (!div) continue;
      this._cellMap[el.Z] = div;
      div.dataset.z = el.Z;
      div.title = `${el.name} (${el.symbol})`;
      div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;';
      div.innerHTML = `
        <span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
        <span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
        <span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
      div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
      div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
      div.addEventListener('click', () => this._selectElement(el.Z));
    }

    this._colorTable();
    this._buildLegend();
  }

  _colorTable() {
    for (const el of ELEMENTS) {
      const div = this._cellMap[el.Z];
      if (!div) continue;
      let bg, textC = '#fff';
      if (this._mode === 'type') {
        bg = TYPE_COLORS[el.type] || '#555';
      } else if (this._mode === 'block') {
        bg = BLOCK_COLORS[el.block] || '#555';
      } else {
        bg = '#2a2a3e';
      }
      div.style.background = bg + '33'; // 20% opacity base
      div.style.border = `1px solid ${bg}88`;
      div.style.color = '#fff';
      if (this._highlighted.size > 0) {
        if (this._highlighted.has(el.Z)) {
          div.style.background = bg + 'cc';
          div.style.border = `2px solid ${bg}`;
          div.style.boxShadow = `0 0 8px ${bg}99`;
        } else {
          div.style.opacity = '0.25';
        }
      } else {
        div.style.opacity = '1';
        div.style.boxShadow = '';
      }
    }
    this._buildLegend();
  }

  _buildLegend() {
    this._legendEl.innerHTML = '';
    const map = this._mode === 'block' ? BLOCK_COLORS : TYPE_COLORS;
    const labels = this._mode === 'block' ? { s:'s-блок', p:'p-блок', d:'d-блок', f:'f-блок' } : TYPE_LABELS;
    for (const [k, col] of Object.entries(map)) {
      if (!labels[k]) continue;
      const d = document.createElement('div');
      d.style.cssText = `display:flex;align-items:center;gap:4px;font-size:.68rem;color:#bbb;cursor:pointer;`;
      d.innerHTML = `<span style="width:12px;height:12px;border-radius:3px;background:${col};display:inline-block;flex-shrink:0;"></span>${labels[k]}`;
      /* highlight on hover */
      d.addEventListener('mouseenter', () => this._highlightType(k));
      d.addEventListener('mouseleave', () => this._unhighlightType());
      this._legendEl.appendChild(d);
    }
  }

  _highlightType(key) {
    for (const el of ELEMENTS) {
      const div = this._cellMap[el.Z];
      if (!div) continue;
      const match = this._mode === 'block' ? el.block === key : el.type === key;
      if (match) {
        div.style.filter = 'brightness(1.5)';
        div.style.transform = 'scale(1.05)';
        div.style.zIndex = '5';
      } else {
        div.style.opacity = '0.2';
      }
    }
  }
  _unhighlightType() {
    for (const el of ELEMENTS) {
      const div = this._cellMap[el.Z];
      if (!div) continue;
      div.style.filter = '';
      div.style.transform = '';
      div.style.zIndex = '';
      div.style.opacity = this._highlighted.has(el.Z) ? '1' : (this._highlighted.size > 0 ? '0.25' : '1');
    }
  }

  /* ─────────────────────────────────────────────────────
     SELECT / SEARCH
  ───────────────────────────────────────────────────── */
  _selectElement(Z) {
    this._selected = Z;
    if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.8 + Z * 0.008, volume: 0.25 });
    /* highlight selected cell */
    for (const el of ELEMENTS) {
      const div = this._cellMap[el.Z];
      if (!div) continue;
      if (el.Z === Z) {
        div.style.boxShadow = 'inset 0 0 0 2px #fff';
      } else {
        div.style.boxShadow = '';
      }
    }
    this._updateCard(ELEMENTS.find(e => e.Z === Z));
    this._bohrZ = Z;
    this._startBohr();
  }

  _applySearch() {
    this._highlighted.clear();
    if (this._searchQ) {
      for (const el of ELEMENTS) {
        const q = this._searchQ;
        if (
          el.symbol.toLowerCase() === q ||
          el.name.toLowerCase().includes(q) ||
          String(el.Z) === q ||
          String(el.mass).startsWith(q)
        ) {
          this._highlighted.add(el.Z);
        }
      }
    }
    this._colorTable();
    /* auto-select if single result */
    if (this._highlighted.size === 1) {
      this._selectElement([...this._highlighted][0]);
    }
  }

  /* ─────────────────────────────────────────────────────
     ELEMENT CARD (legacy — no-selection state only)
  ───────────────────────────────────────────────────── */
  _updateCard(el) {
    if (!el) {
      this._cardEl.innerHTML = `<div style="color:rgba(255,255,255,0.25);font-size:.8rem;text-align:center;padding:20px 0">Кликните на элемент</div>`;
      this._bohrZ = null;
      cancelAnimationFrame(this._bohrRaf);
      this._bohrRaf = null;
      this._drawBohr();
      return;
    }
    this._renderCardV2(el);
  }

  _row(label, val) {
    return `<tr>
      <td style="padding:3px 4px 3px 0;color:rgba(255,255,255,0.45);border-bottom:1px solid rgba(255,255,255,0.05);white-space:nowrap">${label}</td>
      <td style="padding:3px 0 3px 4px;color:#ddd;border-bottom:1px solid rgba(255,255,255,0.05)">${val}</td>
    </tr>`;
  }

  /* ─────────────────────────────────────────────────────
     TABBED CARD V2
  ───────────────────────────────────────────────────── */

  /* Tab definitions: id, label */
  _cardTabs() {
    return [
      { id: 'overview',      label: 'Обзор' },
      { id: 'properties',    label: 'Свойства' },
      { id: 'electronics',   label: 'Электроника' },
      { id: 'isotopes',      label: 'Изотопы' },
      { id: 'history',       label: 'История' },
      { id: 'applications',  label: 'Применения' },
      { id: 'biology',       label: 'Биология' },
      { id: 'minerals',      label: 'Минералы' },
      { id: 'spectrum',      label: 'Спектр' },
      { id: 'flame',         label: 'Пламя' },
      { id: 'reactions',     label: 'Реакции' },
    ];
  }

  _renderCardV2(el) {
    const col = TYPE_COLORS[el.type] || '#888';
    this._cardActiveTab = this._cardActiveTab || 'overview';

    /* build outer shell */
    this._cardEl.innerHTML = `
      <div class="ptbl-card-v2" style="--el-col:${col}">
        <button class="ptbl-card-close" aria-label="Закрыть">
          <svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
        </button>
        <div class="ptbl-hero">
          <div class="ptbl-hero-z">${el.Z}</div>
          <div class="ptbl-hero-sym">${el.symbol}</div>
          <div class="ptbl-hero-name">${el.name}</div>
          <div class="ptbl-hero-mass">${el.mass} а.е.м.</div>
          <div class="ptbl-hero-badge" style="background:${col}22;border-color:${col}55;color:${col}">${TYPE_LABELS[el.type] || el.type}</div>
        </div>
        <div class="ptbl-tabs" role="tablist">
          ${this._cardTabs().map(t => `<button class="ptbl-tab${t.id === this._cardActiveTab ? ' active' : ''}" data-tab="${t.id}" role="tab">${t.label}</button>`).join('')}
        </div>
        <div class="ptbl-tab-body" id="ptbl-tab-body"></div>
      </div>`;

    /* close button */
    this._cardEl.querySelector('.ptbl-card-close').addEventListener('click', () => {
      this._updateCard(null);
      for (const e2 of ELEMENTS) {
        const div = this._cellMap[e2.Z];
        if (div) { div.style.boxShadow = ''; div.style.outline = ''; div.style.outlineOffset = ''; }
      }
    });

    /* tab switching */
    this._cardEl.querySelectorAll('.ptbl-tab').forEach(btn => {
      btn.addEventListener('click', () => {
        this._cardActiveTab = btn.dataset.tab;
        this._cardEl.querySelectorAll('.ptbl-tab').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        this._switchTabContent(el, btn.dataset.tab);
      });
    });

    /* render initial tab */
    this._switchTabContent(el, this._cardActiveTab);
  }

  _switchTabContent(el, tabId) {
    const body = this._cardEl.querySelector('#ptbl-tab-body');
    if (!body) return;
    body.style.opacity = '0';
    setTimeout(() => {
      let html = '';
      switch (tabId) {
        case 'overview':     html = this._renderTab_overview(el);     break;
        case 'properties':   html = this._renderTab_properties(el);   break;
        case 'electronics':  html = this._renderTab_electronics(el);  break;
        case 'isotopes':     html = this._renderTab_isotopes(el);     break;
        case 'history':      html = this._renderTab_history(el);      break;
        case 'applications': html = this._renderTab_applications(el); break;
        case 'biology':      html = this._renderTab_biology(el);      break;
        case 'minerals':     html = this._renderTab_minerals(el);     break;
        case 'spectrum':     html = this._renderTab_spectrum(el);     break;
        case 'flame':        html = this._renderTab_flame(el);        break;
        case 'reactions':    html = this._renderTab_reactions(el);    break;
        default:             html = this._renderTab_overview(el);
      }
      body.innerHTML = html;
      body.style.opacity = '1';
      if (tabId === 'electronics') this._postRenderElectronics(el);
      if (tabId === 'isotopes')    this._postRenderIsotopesChart(el);
      if (tabId === 'spectrum')    this._postRenderSpectrum(el);
      if (tabId === 'reactions' && window.renderMathInElement) {
        renderMathInElement(body, { delimiters: [{ left: '$$', right: '$$', display: false }] });
      }
    }, 100);
  }

  /* ── Tab 1: Обзор ── */
  _renderTab_overview(el) {
    const col = TYPE_COLORS[el.type] || '#888';
    const fmt = v => (v !== null && v !== undefined) ? v : '—';
    const ox = el.oxStates && el.oxStates[0] !== null
      ? el.oxStates.map(s => (s > 0 ? '+' : '') + s).join(', ')
      : '—';
    const summary = el.summary || el.description || '';
    return `
      <div class="ptbl-overview">
        ${summary ? `<p class="ptbl-summary">${summary}</p>` : ''}
        <div class="ptbl-quick-grid">
          <div class="ptbl-quick-item">
            <span class="ptbl-quick-label">Тип</span>
            <span class="ptbl-quick-val" style="color:${col}">${TYPE_LABELS[el.type] || el.type}</span>
          </div>
          <div class="ptbl-quick-item">
            <span class="ptbl-quick-label">Конфигурация</span>
            <span class="ptbl-quick-val"><code>${el.config}</code></span>
          </div>
          <div class="ptbl-quick-item">
            <span class="ptbl-quick-label">Ст. окисления</span>
            <span class="ptbl-quick-val">${ox}</span>
          </div>
          <div class="ptbl-quick-item">
            <span class="ptbl-quick-label">ЭО (Полинг)</span>
            <span class="ptbl-quick-val">${fmt(el.En)}</span>
          </div>
          <div class="ptbl-quick-item">
            <span class="ptbl-quick-label">Период / Группа</span>
            <span class="ptbl-quick-val">${el.period} / ${el.group || '—'}</span>
          </div>
          <div class="ptbl-quick-item">
            <span class="ptbl-quick-label">Блок</span>
            <span class="ptbl-quick-val">${el.block}-блок</span>
          </div>
        </div>
      </div>`;
  }

  /* ── Tab 2: Свойства ── */
  _renderTab_properties(el) {
    const fmt = v => (v !== null && v !== undefined) ? v : '—';
    const rows = [
      ['Атомная масса, а.е.м.', fmt(el.mass)],
      ['Плотность, г/см³',      fmt(el.density)],
      ['T<sub>пл</sub>, K',     fmt(el.melt)],
      ['T<sub>кип</sub>, K',    fmt(el.boil)],
      ['ЭО (Полинг)',           fmt(el.En)],
      ['Атомный радиус, пм',    el.radius?.atomic         ?? '—'],
      ['Ковалентный радиус, пм',el.radius?.covalent       ?? '—'],
      ['Ионный радиус, пм',     el.radius?.ionic          ?? '—'],
      ['1-я эн. ионизации, эВ', el.ionization?.[0]        ?? '—'],
      ['2-я эн. ионизации, эВ', el.ionization?.[1]        ?? '—'],
      ['Электронное сродство',  el.electronAffinity       ?? '—'],
      ['Теплоёмкость, Дж/(г·K)',el.heatCapacity           ?? '—'],
      ['Теплопроводность',      el.thermalConductivity    ?? '—'],
      ['Электросопротивление',  el.electricalResistivity  ?? '—'],
      ['Кристаллич. структура', el.crystalStructure       ?? '—'],
      ['Параметр решётки, пм',  el.latticeParam           ?? '—'],
      ['Открыт',                el.discovered ? `${el.discovered}, ${el.by}` : (el.by || '—')],
    ];
    return `
      <table class="ptbl-prop-table">
        <tbody>
          ${rows.map(([l, v]) => `<tr><td>${l}</td><td>${v}</td></tr>`).join('')}
        </tbody>
      </table>`;
  }

  /* ── Tab 3: Электроника ── */
  _renderTab_electronics(el) {
    return `
      <div class="ptbl-electronics">
        <div class="ptbl-bohr-inline-wrap">
          <canvas id="ptbl-bohr-inline" style="width:100%;height:160px;display:block;"></canvas>
        </div>
        <div class="ptbl-el-config">
          <span class="ptbl-quick-label">Электронная конфигурация</span>
          <code class="ptbl-el-config-code">${el.config}</code>
        </div>
        <div class="ptbl-el-config" style="margin-top:6px;">
          <span class="ptbl-quick-label">Оболочки (K, L, M, …)</span>
          <code class="ptbl-el-config-code">${getShellFill(el.Z).join(' | ')}</code>
        </div>
      </div>`;
  }

  _postRenderElectronics(el) {
    const canvas = this._cardEl.querySelector('#ptbl-bohr-inline');
    if (!canvas) return;
    const dpr = window.devicePixelRatio || 1;
    const W = canvas.offsetWidth || 220;
    const H = 160;
    canvas.width  = W * dpr;
    canvas.height = H * dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, W, H);

    const shells = getShellFill(el.Z);
    const col = TYPE_COLORS[el.type] || '#7B8EF7';
    const cx = W / 2, cy = H / 2;
    const maxR = Math.min(W, H) * 0.44;
    const nShells = shells.length;

    ctx.beginPath();
    ctx.arc(cx, cy, nShells > 0 ? 5 + nShells * 1.5 : 6, 0, Math.PI * 2);
    ctx.fillStyle = col;
    ctx.fill();

    shells.forEach((count, i) => {
      const r = maxR * (i + 1) / nShells;
      ctx.beginPath();
      ctx.arc(cx, cy, r, 0, Math.PI * 2);
      ctx.strokeStyle = 'rgba(255,255,255,0.12)';
      ctx.lineWidth = 1;
      ctx.stroke();
      for (let e = 0; e < count; e++) {
        const a = (2 * Math.PI * e) / count - Math.PI / 2;
        const ex = cx + r * Math.cos(a);
        const ey = cy + r * Math.sin(a);
        ctx.beginPath();
        ctx.arc(ex, ey, 2.5, 0, Math.PI * 2);
        ctx.fillStyle = '#06D6E0';
        ctx.fill();
      }
    });

    ctx.font = `700 10px Manrope,sans-serif`;
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.textAlign = 'center';
    ctx.fillText(shells.join(','), cx, H - 4);
  }

  /* ── Tab 4: Изотопы ── */
  _renderTab_isotopes(el) {
    const isos = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || [];
    if (!isos.length) {
      return `<p class="ptbl-empty">Данные об изотопах не загружены.</p>`;
    }

    let avgMass = null;
    const stableIsos = isos.filter(iso => iso.abundance != null && iso.abundance > 0);
    if (stableIsos.length) {
      const total = stableIsos.reduce((s, iso) => s + iso.abundance, 0);
      avgMass = stableIsos.reduce((s, iso) => s + iso.mass * iso.abundance, 0) / total;
    }

    const rows = isos.map(iso => {
      const abStr  = iso.abundance != null ? (iso.abundance * 100).toFixed(2) + '%' : '—';
      const hlStr  = iso.halfLife || '—';
      const decStr = iso.decay    || '—';
      return `<tr>
        <td><sup>${iso.massNum ?? iso.mass}</sup>${el.symbol}</td>
        <td>${typeof iso.mass === 'number' ? iso.mass.toFixed(4) : (iso.mass ?? '—')}</td>
        <td>${abStr}</td>
        <td>${hlStr}</td>
        <td>${decStr}</td>
      </tr>`;
    }).join('');

    return `
      <div class="ptbl-isotopes">
        <table class="ptbl-prop-table ptbl-iso-table">
          <thead><tr><th>Изотоп</th><th>Масса</th><th>Распр.</th><th>T&#x00BD;</th><th>Распад</th></tr></thead>
          <tbody>${rows}</tbody>
        </table>
        ${avgMass != null
          ? `<div class="ptbl-iso-avg">Средняя атомная масса (взвеш.): <strong>${avgMass.toFixed(4)}</strong> а.е.м.</div>`
          : ''}
        <canvas id="ptbl-iso-chart" style="width:100%;height:70px;margin-top:8px;display:block;"></canvas>
      </div>`;
  }

  _postRenderIsotopesChart(el) {
    const canvas = this._cardEl.querySelector('#ptbl-iso-chart');
    if (!canvas) return;
    const isos   = (window.PERIODIC_ISOTOPES && window.PERIODIC_ISOTOPES[el.Z]) || [];
    const stable = isos.filter(iso => iso.abundance != null && iso.abundance > 0);
    if (!stable.length) return;

    const dpr = window.devicePixelRatio || 1;
    const W = canvas.offsetWidth || 220;
    const H = 70;
    canvas.width  = W * dpr;
    canvas.height = H * dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, W, H);

    const col  = TYPE_COLORS[el.type] || '#7B8EF7';
    const pad  = { t: 4, r: 4, b: 18, l: 4 };
    const gW   = W - pad.l - pad.r;
    const gH   = H - pad.t - pad.b;
    const n    = stable.length;
    const bw   = Math.max(2, (gW / n) - 2);
    const maxA = Math.max(...stable.map(iso => iso.abundance));

    stable.forEach((iso, i) => {
      const bh = (iso.abundance / maxA) * gH;
      const x  = pad.l + i * (gW / n) + (gW / n - bw) / 2;
      const y  = pad.t + gH - bh;
      ctx.fillStyle = col + 'bb';
      ctx.fillRect(x, y, bw, bh);
      ctx.font      = '8px Manrope,sans-serif';
      ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.textAlign = 'center';
      ctx.fillText(String(iso.massNum ?? ''), x + bw / 2, H - 4);
    });
  }

  /* ── Tab 5: История ── */
  _renderTab_history(el) {
    const year = el.discovered ? String(el.discovered) : null;
    const by   = el.by    || '—';
    const hist = el.historyText || '';
    const etym = el.etymology  || '';

    return `
      <div class="ptbl-history">
        <div class="ptbl-timeline-card">
          <div class="ptbl-timeline-year">${year || 'Древний мир'}</div>
          <div class="ptbl-timeline-who">${by}</div>
          ${el.discoveryCountry ? `<div class="ptbl-timeline-country">${el.discoveryCountry}</div>` : ''}
        </div>
        ${hist ? `<p class="ptbl-hist-text">${hist}</p>` : '<p class="ptbl-empty">История не указана.</p>'}
        ${etym ? `<div class="ptbl-etymology"><span class="ptbl-quick-label">Этимология: </span>${etym}</div>` : ''}
      </div>`;
  }

  /* ── Tab 6: Применения ── */
  _renderTab_applications(el) {
    const apps = el.applications    || [];
    const desc = el.applicationsDescription || '';
    const iconMap = {
      battery:       '<svg class="ic" viewBox="0 0 24 24"><rect x="2" y="7" width="18" height="10" rx="2"/><path d="M22 11v2"/><line x1="7" y1="12" x2="11" y2="12"/><line x1="9" y1="10" x2="9" y2="14"/></svg>',
      medicine:      '<svg class="ic" viewBox="0 0 24 24"><path d="M12 2a5 5 0 0 1 5 5v1h2a1 1 0 0 1 1 1v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9a1 1 0 0 1 1-1h2V7a5 5 0 0 1 5-5z"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/></svg>',
      electronics:   '<svg class="ic" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><circle cx="9" cy="9" r="1"/><circle cx="15" cy="9" r="1"/><circle cx="9" cy="15" r="1"/><circle cx="15" cy="15" r="1"/></svg>',
      metallurgy:    '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/><line x1="12" y1="10" x2="12" y2="16"/></svg>',
      construction:  '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="12" width="18" height="8" rx="1"/><path d="M3 12l4-8h10l4 8"/></svg>',
      nuclear:       '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="9" fill="none"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/></svg>',
      lighting:      '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 1 7 7c0 2.5-1.3 4.7-3.3 6L15 17H9l-.7-2C6.3 13.7 5 11.5 5 9a7 7 0 0 1 7-7z"/></svg>',
      catalyst:      '<svg class="ic" viewBox="0 0 24 24"><path d="M8 3L4 10h16L16 3z"/><path d="M4 14l4 7h8l4-7z"/></svg>',
      jewelry:       '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 16 9 22 9 17 14 19 21 12 17 5 21 7 14 2 9 8 9"/></svg>',
      fertilizer:    '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg>',
      semiconductor: '<svg class="ic" viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="10" rx="1"/><line x1="7" y1="12" x2="3" y2="12"/><line x1="17" y1="12" x2="21" y2="12"/><line x1="12" y1="7" x2="12" y2="3"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
      pigment:       '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 3c2.5 2 4 5 4 9s-1.5 7-4 9"/></svg>',
      aerospace:     '<svg class="ic" viewBox="0 0 24 24"><path d="M12 2l2 6h6l-5 4 2 6-5-4-5 4 2-6-5-4h6z"/></svg>',
      optical:       '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/></svg>',
      food:          '<svg class="ic" viewBox="0 0 24 24"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>',
    };
    const defaultIcon = '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';

    if (!apps.length && !desc) {
      return `<p class="ptbl-empty">Применения не указаны.</p>`;
    }

    const cards = apps.map(tag => {
      const ico   = iconMap[tag] || defaultIcon;
      const label = tag.charAt(0).toUpperCase() + tag.slice(1);
      return `<div class="ptbl-app-chip">${ico}<span>${label}</span></div>`;
    }).join('');

    return `
      <div class="ptbl-applications">
        ${cards ? `<div class="ptbl-app-grid">${cards}</div>` : ''}
        ${desc  ? `<p class="ptbl-app-desc">${desc}</p>` : ''}
      </div>`;
  }

  /* ── Tab 7: Биология ── */
  _renderTab_biology(el) {
    const bio  = el.biological   || null;
    const role = el.biologicalRole || '';
    const bioLabels = {
      macro:       'Макроэлемент (жизненно важен)',
      micro:       'Микроэлемент (жизненно важен)',
      trace:       'Следовой элемент',
      toxic:       'Токсичен для живых организмов',
      inert:       'Биологически инертен',
      radioactive: 'Радиоактивен / опасен',
    };
    const bioColors = {
      macro: '#06D6E0', micro: '#7BF5A4', trace: '#FFD166',
      toxic: '#EF476F', inert: '#888',    radioactive: '#F15BB5',
    };
    const bioColor = bio ? (bioColors[bio] || '#888') : '#888';
    const bioLabel = bio ? (bioLabels[bio] || bio)    : 'Нет данных';

    return `
      <div class="ptbl-biology">
        <div class="ptbl-bio-badge" style="border-color:${bioColor};color:${bioColor}">${bioLabel}</div>
        ${role ? `<p class="ptbl-bio-role">${role}</p>` : '<p class="ptbl-empty">Биологическая роль не указана.</p>'}
        ${el.toxicity ? `<div class="ptbl-bio-toxicity"><span class="ptbl-quick-label">Токсичность: </span>${el.toxicity}</div>` : ''}
      </div>`;
  }

  /* ── Tab 8: Минералы ── */
  _renderTab_minerals(el) {
    const mins    = el.mineralForms   || [];
    const sources = el.mineralSources || '';

    if (!mins.length && !sources) {
      return `<p class="ptbl-empty">Данные о минералах не указаны.</p>`;
    }

    const items = mins.map(m => {
      const name    = typeof m === 'object' ? (m.name    || '—') : m;
      const formula = typeof m === 'object' ? (m.formula || '')  : '';
      return `<div class="ptbl-mineral-item">
        <span class="ptbl-mineral-name">${name}</span>
        ${formula ? `<code class="ptbl-mineral-formula">${formula}</code>` : ''}
      </div>`;
    }).join('');

    return `
      <div class="ptbl-minerals">
        ${items ? `<div class="ptbl-mineral-list">${items}</div>` : ''}
        ${sources ? `<p class="ptbl-mineral-sources"><span class="ptbl-quick-label">Источники: </span>${sources}</p>` : ''}
      </div>`;
  }

  /* ── Tab 9: Спектр ── */
  _renderTab_spectrum(el) {
    if (!el.spectrum || !el.spectrum.length) {
      return `<p class="ptbl-empty">Спектральные данные не указаны.</p>`;
    }
    return `
      <div class="ptbl-spectrum">
        <canvas id="ptbl-spec-canvas" style="width:100%;height:80px;display:block;"></canvas>
        <div class="ptbl-spec-lines-list">
          ${el.spectrum.map(s => {
            const nm = typeof s === 'object' ? s.nm : s;
            return `<span class="ptbl-spec-tag">${nm} нм</span>`;
          }).join('')}
        </div>
      </div>`;
  }

  _postRenderSpectrum(el) {
    const canvas = this._cardEl.querySelector('#ptbl-spec-canvas');
    if (!canvas || !el.spectrum || !el.spectrum.length) return;

    const dpr = window.devicePixelRatio || 1;
    const W   = canvas.offsetWidth || 220;
    const H   = 80;
    canvas.width  = W * dpr;
    canvas.height = H * dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);

    /* rainbow background 380-780 nm */
    const grad = ctx.createLinearGradient(0, 0, W, 0);
    grad.addColorStop(0,    '#6600ff');
    grad.addColorStop(0.10, '#4400ff');
    grad.addColorStop(0.20, '#0000ff');
    grad.addColorStop(0.30, '#00aaff');
    grad.addColorStop(0.40, '#00ffcc');
    grad.addColorStop(0.50, '#00ff00');
    grad.addColorStop(0.60, '#aaff00');
    grad.addColorStop(0.70, '#ffff00');
    grad.addColorStop(0.80, '#ff8800');
    grad.addColorStop(0.90, '#ff2200');
    grad.addColorStop(1.0,  '#880000');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);

    ctx.fillStyle = 'rgba(0,0,0,0.55)';
    ctx.fillRect(0, 0, W, H);

    el.spectrum.forEach(s => {
      const nm  = typeof s === 'object' ? s.nm  : s;
      const int = typeof s === 'object' ? (s.intensity ?? 1) : 1;
      if (!nm || nm < 380 || nm > 780) return;
      const x   = ((nm - 380) / 400) * W;
      const col = this._nmToRGB(nm);
      ctx.strokeStyle = col;
      ctx.lineWidth   = Math.max(1, int * 2);
      ctx.globalAlpha = 0.7 + int * 0.3;
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, H - 18);
      ctx.stroke();
      ctx.globalAlpha = 1;
      ctx.font      = '7px Manrope,sans-serif';
      ctx.fillStyle = col;
      ctx.textAlign = 'center';
      ctx.fillText(String(nm), x, H - 4);
    });
  }

  _nmToRGB(nm) {
    if (nm < 380) return '#8800ff';
    if (nm < 440) return `hsl(${270 + (nm - 380) * 0.5},100%,60%)`;
    if (nm < 490) return `hsl(${240 - (nm - 440) * 1.2},100%,55%)`;
    if (nm < 510) return `hsl(${180 - (nm - 490)},100%,50%)`;
    if (nm < 580) return `hsl(${120 - (nm - 510) * 0.7},100%,45%)`;
    if (nm < 645) return `hsl(${60  - (nm - 580) * 0.92},100%,50%)`;
    return `hsl(0,100%,${Math.max(20, 50 - (nm - 645) * 0.4)}%)`;
  }

  /* ── Tab 10: Пламя ── */
  _renderTab_flame(el) {
    if (!el.flameColor) {
      return `<p class="ptbl-empty">Данные об окраске пламени не указаны.</p>`;
    }
    return `
      <div class="ptbl-flame">
        <div class="ptbl-flame-swatch" style="background:${el.flameColor};box-shadow:0 0 24px ${el.flameColor}88"></div>
        <div class="ptbl-flame-label">Окраска пламени: <strong>${el.flameColorName || el.flameColor}</strong></div>
        <p class="ptbl-flame-desc">
          При внесении соединений ${el.name} в пламя горелки оно окрашивается
          в характерный цвет — применяется в качественном анализе и в пиротехнике.
        </p>
      </div>`;
  }

  /* ── Tab 11: Реакции ── */
  _renderTab_reactions(el) {
    const col = TYPE_COLORS[el.type] || '#888';
    const reactionsByType = {
      alkali: [
        { label: 'С водой',      eq: `2${el.symbol} + 2H₂O → 2${el.symbol}OH + H₂↑` },
        { label: 'С кислородом', eq: `4${el.symbol} + O₂ → 2${el.symbol}₂O` },
        { label: 'С хлором',     eq: `2${el.symbol} + Cl₂ → 2${el.symbol}Cl` },
      ],
      alkaline: [
        { label: 'С водой',      eq: `${el.symbol} + 2H₂O → ${el.symbol}(OH)₂ + H₂↑` },
        { label: 'С кислородом', eq: `2${el.symbol} + O₂ → 2${el.symbol}O` },
        { label: 'С кислотой',   eq: `${el.symbol} + 2HCl → ${el.symbol}Cl₂ + H₂↑` },
      ],
      halogen: [
        { label: 'С натрием',    eq: `2Na + ${el.symbol}₂ → 2Na${el.symbol}` },
        { label: 'С водородом',  eq: `H₂ + ${el.symbol}₂ → 2H${el.symbol}` },
        { label: 'С водой',      eq: `${el.symbol}₂ + H₂O → H${el.symbol} + H${el.symbol}O` },
      ],
      transition: [
        { label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` },
        { label: 'С кислотой',   eq: `${el.symbol} + H₂SO₄ → ${el.symbol}SO₄ + H₂↑` },
      ],
      nonmetal: [
        { label: 'С металлом',   eq: `Me + ${el.symbol} → Me–${el.symbol}` },
        { label: 'С кислородом', eq: `${el.symbol} + O₂ → ${el.symbol}O₂` },
      ],
    };
    const reactions = reactionsByType[el.type] || [
      { label: 'Реакции', eq: `${el.symbol} + реагент → продукт` },
    ];

    const items = reactions.map(r => `
      <div class="ptbl-reaction-item">
        <div class="ptbl-reaction-label">${r.label}</div>
        <div class="ptbl-reaction-eq" style="color:${col}">${r.eq}</div>
      </div>`).join('');

    return `
      <div class="ptbl-reactions">
        ${items}
        <p class="ptbl-reaction-note">Уравнения приведены в общем виде для ознакомления.</p>
      </div>`;
  }

  /* ─────────────────────────────────────────────────────
     BOHR SHELLS ANIMATION
  ───────────────────────────────────────────────────── */
  _startBohr() {
    cancelAnimationFrame(this._bohrRaf);
    this._bohrAngle = 0;
    this._animBohr();
  }

  _animBohr() {
    this._bohrAngle += 0.018;
    this._drawBohr();
    this._bohrRaf = requestAnimationFrame(() => this._animBohr());
  }

  _drawBohr() {
    const canvas = this._bohrCanvas;
    const dpr = window.devicePixelRatio || 1;
    const W = canvas.offsetWidth || 240;
    const H = canvas.offsetHeight || 150;
    canvas.width  = W * dpr;
    canvas.height = H * dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, W, H);

    if (!this._bohrZ) return;

    const el = ELEMENTS.find(e => e.Z === this._bohrZ);
    if (!el) return;

    const shells = getShellFill(el.Z);
    const cx = W / 2, cy = H / 2;
    const maxR = Math.min(W, H) * 0.44;
    const nShells = shells.length;
    const col = TYPE_COLORS[el.type] || '#7B8EF7';

    /* nucleus */
    ctx.beginPath();
    ctx.arc(cx, cy, nShells > 0 ? 5 + nShells * 1.5 : 6, 0, Math.PI * 2);
    ctx.fillStyle = col;
    ctx.fill();

    shells.forEach((count, i) => {
      const r = maxR * (i + 1) / nShells;
      /* orbit ring */
      ctx.beginPath();
      ctx.arc(cx, cy, r, 0, Math.PI * 2);
      ctx.strokeStyle = 'rgba(255,255,255,0.12)';
      ctx.lineWidth = 1;
      ctx.stroke();

      /* electrons */
      const speed = 1 - i * 0.12; // inner shells faster
      for (let e = 0; e < count; e++) {
        const a = this._bohrAngle * speed + (2 * Math.PI * e) / count;
        const ex = cx + r * Math.cos(a);
        const ey = cy + r * Math.sin(a);
        ctx.beginPath();
        ctx.arc(ex, ey, 2.5, 0, Math.PI * 2);
        ctx.fillStyle = '#06D6E0';
        ctx.fill();
      }
    });

    /* label */
    ctx.font = `700 10px Manrope,sans-serif`;
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.textAlign = 'center';
    ctx.fillText(shells.join(','), cx, H - 4);
  }

  /* ─────────────────────────────────────────────────────
     PROPERTY CHART
  ───────────────────────────────────────────────────── */
  _drawChart() {
    const canvas = this._chartCanvas;
    if (!canvas) return;
    const dpr = window.devicePixelRatio || 1;
    const W = canvas.offsetWidth || 240;
    const H = canvas.offsetHeight || 90;
    canvas.width  = W * dpr;
    canvas.height = H * dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, W, H);

    /* filter elements */
    let els;
    if (this._chartBy === 'period') {
      els = ELEMENTS.filter(e => e.period === this._chartN && e.group !== null);
      els.sort((a, b) => a.group - b.group);
    } else {
      els = ELEMENTS.filter(e => e.group === this._chartN);
      els.sort((a, b) => a.period - b.period);
    }

    const vals = els.map(e => e[this._propKey]);
    const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v));
    if (validVals.length < 2) {
      ctx.font = '11px Manrope,sans-serif';
      ctx.fillStyle = 'rgba(255,255,255,0.3)';
      ctx.textAlign = 'center';
      ctx.fillText('Нет данных', W / 2, H / 2);
      return;
    }

    const minV = Math.min(...validVals);
    const maxV = Math.max(...validVals);
    const pad = { t: 10, r: 8, b: 20, l: 8 };
    const gW = W - pad.l - pad.r;
    const gH = H - pad.t - pad.b;

    /* axes */
    ctx.strokeStyle = 'rgba(255,255,255,0.1)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(pad.l, pad.t);
    ctx.lineTo(pad.l, pad.t + gH);
    ctx.lineTo(pad.l + gW, pad.t + gH);
    ctx.stroke();

    /* line */
    ctx.beginPath();
    let first = true;
    const points = [];
    els.forEach((el, i) => {
      const v = el[this._propKey];
      if (v === null || v === undefined || !isFinite(v)) return;
      const x = pad.l + (i / Math.max(els.length - 1, 1)) * gW;
      const y = pad.t + gH - ((v - minV) / (maxV - minV || 1)) * gH;
      points.push({ x, y, el });
      if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y);
    });
    ctx.strokeStyle = '#9B5DE5';
    ctx.lineWidth = 1.5;
    ctx.stroke();

    /* markers */
    points.forEach(({ x, y, el }) => {
      const col = TYPE_COLORS[el.type] || '#7B8EF7';
      ctx.beginPath();
      ctx.arc(x, y, 3, 0, Math.PI * 2);
      ctx.fillStyle = col;
      ctx.fill();
    });

    /* x labels (symbols) */
    ctx.font = '8px Manrope,sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.35)';
    ctx.textAlign = 'center';
    /* show at most 18 labels */
    const step = Math.ceil(els.length / 18);
    els.forEach((el, i) => {
      if (i % step === 0) {
        const x = pad.l + (i / Math.max(els.length - 1, 1)) * gW;
        ctx.fillText(el.symbol, x, H - 4);
      }
    });
  }

  /* ─────────────────────────────────────────────────────
     LIFECYCLE
  ───────────────────────────────────────────────────── */
  stop() {
    cancelAnimationFrame(this._bohrRaf);
    this._bohrRaf = null;
  }
}

/* ══════════════════════════════════════════════════════════════
   WAVE B — VISUAL MODES EXTENSION
   Methods: _buildVisualModes, _drawHeatmap, _init3DTable,
            _update3D, _morphToView, _drawTrendArrows
   ══════════════════════════════════════════════════════════════ */

/* ── Heatmap property config ─────────────────────────────────── */
const HEATMAP_PROPS = [
  { key: 'En',                  label: 'Электроотрицательность',  unit: 'П.',   get: el => el.En },
  { key: 'mass',                label: 'Атомная масса',            unit: 'а.е.м.', get: el => el.mass },
  { key: 'density',             label: 'Плотность',                unit: 'г/см³', get: el => el.density },
  { key: 'melt',                label: 'T плавления',              unit: 'K',    get: el => el.melt },
  { key: 'boil',                label: 'T кипения',                unit: 'K',    get: el => el.boil },
  { key: 'discovered',          label: 'Год открытия',             unit: 'г.',   get: el => el.discovered },
  { key: 'radius.atomic',       label: 'Атомный радиус',           unit: 'пм',   get: el => el.radius && el.radius.atomic ? el.radius.atomic : null },
  { key: 'ionization.e1',       label: '1-я энергия ионизации',    unit: 'эВ',   get: el => el.ionization && el.ionization.e1 ? el.ionization.e1 : null },
  { key: 'abundance.crust',     label: 'Распространённость в коре',unit: 'мг/кг',get: el => el.abundance && el.abundance.crust ? el.abundance.crust : null },
];

/* ── Trend arrows config ─────────────────────────────────────── */
const TREND_CONFIG = {
  radius: {
    label: 'Атомный радиус',
    hArrow: { dir: 'left',  text: 'Радиус растёт', color: '#06D6E0' },
    vArrow: { dir: 'down',  text: 'Радиус растёт', color: '#06D6E0' },
  },
  en: {
    label: 'Электроотрицательность',
    hArrow: { dir: 'right', text: 'ЭО растёт',     color: '#FF6B6B' },
    vArrow: { dir: 'up',    text: 'ЭО растёт',     color: '#FF6B6B' },
  },
  ie: {
    label: 'Энергия ионизации',
    hArrow: { dir: 'right', text: 'ИЕ растёт',     color: '#FFD93D' },
    vArrow: { dir: 'up',    text: 'ИЕ растёт',     color: '#FFD93D' },
  },
  metal: {
    label: 'Металличность',
    hArrow: { dir: 'left',  text: 'Металличность растёт', color: '#4CAF50' },
    vArrow: { dir: 'down',  text: 'Металличность растёт', color: '#4CAF50' },
  },
};

/* ── Secondary control bar ───────────────────────────────────── */
PeriodicTableSim.prototype._buildVisualModes = function() {
  const self = this;

  /* secondary bar container */
  const bar = document.createElement('div');
  bar.id = 'ptbl-vmodes-bar';
  bar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 12px;border-bottom:1px solid rgba(255,255,255,0.06);flex-wrap:wrap;flex-shrink:0;background:rgba(0,0,0,0.2);';

  /* ─ 1. HEATMAP section ─ */
  bar.innerHTML = `
    <span style="font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Тепловая карта:</span>

    <button id="ptbl-heat-toggle" class="ptbl-vm-btn" title="Тепловая карта">
      <svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px">
        <rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
        <rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
      </svg>
      Окрасить
    </button>

    <select id="ptbl-heat-prop" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
      ${HEATMAP_PROPS.map(p => `<option value="${p.key}"${p.key==='En'?' selected':''}>${p.label}</option>`).join('')}
    </select>

    <button id="ptbl-heat-scale" class="ptbl-vm-btn" style="display:none" title="Линейная/Лог шкала">Lin</button>

    <div id="ptbl-heat-legend" style="display:none;flex-shrink:0;"></div>

    <span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Вид:</span>

    <select id="ptbl-shape-sel" class="ptbl-vm-btn" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:3px 6px;font-size:.72rem;cursor:pointer;">
      <option value="std">Стандартная</option>
      <option value="long">Длинная (32 кол.)</option>
      <option value="short">Краткая (8 гр.)</option>
    </select>

    <span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">Тренды:</span>

    <button id="ptbl-trend-toggle" class="ptbl-vm-btn" title="Показать тренды">
      <svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width:13px;height:13px">
        <line x1="5" y1="19" x2="19" y2="5"/><polyline points="9 5 19 5 19 15"/>
      </svg>
      Тренды
    </button>

    <select id="ptbl-trend-prop" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
      ${Object.entries(TREND_CONFIG).map(([k,v]) => `<option value="${k}">${v.label}</option>`).join('')}
    </select>

    <span style="margin-left:4px;font-size:.68rem;font-weight:700;color:rgba(255,255,255,0.35);text-transform:uppercase;letter-spacing:.07em">3D:</span>

    <button id="ptbl-3d-toggle" class="ptbl-vm-btn" title="3D-вид">
      <svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px">
        <path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
      </svg>
      3D
    </button>

    <select id="ptbl-3d-mode" style="display:none;background:#1a1a2e;border:1px solid rgba(255,255,255,0.15);color:#ccc;border-radius:5px;padding:2px 5px;font-size:.72rem;">
      <option value="bar">Bar</option>
      <option value="wave">Wave</option>
      <option value="stack">Stack</option>
    </select>
  `;

  /* style vm buttons */
  const vmStyle = 'padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,0.12);background:transparent;color:#aaa;font-size:.72rem;cursor:pointer;display:inline-flex;align-items:center;gap:4px;transition:all .15s;';
  bar.querySelectorAll('.ptbl-vm-btn').forEach(b => { b.style.cssText = vmStyle; });

  /* insert bar after toolbar (first child of wrap) */
  const toolbar = this._wrap.querySelector('div');
  this._wrap.insertBefore(bar, toolbar.nextSibling);

  /* trend canvas overlay */
  this._trendCanvas = document.createElement('canvas');
  this._trendCanvas.id = 'ptbl-trend-canvas';
  this._trendCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0;transition:opacity .3s;';
  const tableGrid = this._tableEl;
  const tableParent = tableGrid.parentElement;
  tableParent.style.position = 'relative';
  tableParent.appendChild(this._trendCanvas);

  /* 3D canvas */
  this._canvas3d = document.createElement('canvas');
  this._canvas3d.id = 'ptbl-3d-canvas';
  this._canvas3d.style.cssText = 'display:none;flex:1;min-height:0;background:#0D0D1A;';
  /* insert into main area beside table */
  const mainArea = this._tableEl.closest('div[style*="flex:1"]') || tableParent;
  mainArea.parentElement.insertBefore(this._canvas3d, mainArea);

  /* ── wire events ── */

  /* heatmap toggle */
  bar.querySelector('#ptbl-heat-toggle').addEventListener('click', () => {
    self._heatActive = !self._heatActive;
    bar.querySelector('#ptbl-heat-toggle').style.background = self._heatActive ? 'rgba(155,93,229,0.3)' : 'transparent';
    bar.querySelector('#ptbl-heat-toggle').style.color = self._heatActive ? '#fff' : '#aaa';
    bar.querySelector('#ptbl-heat-prop').style.display = self._heatActive ? '' : 'none';
    bar.querySelector('#ptbl-heat-scale').style.display = self._heatActive ? '' : 'none';
    bar.querySelector('#ptbl-heat-legend').style.display = self._heatActive ? 'flex' : 'none';
    if (self._heatActive) {
      self._drawHeatmap(true);
    } else {
      self._colorTable();
      bar.querySelector('#ptbl-heat-legend').innerHTML = '';
    }
  });

  bar.querySelector('#ptbl-heat-prop').addEventListener('change', e => {
    self._heatProp = e.target.value;
    if (self._heatActive) self._drawHeatmap(true);
  });

  bar.querySelector('#ptbl-heat-scale').addEventListener('click', () => {
    self._heatLog = !self._heatLog;
    bar.querySelector('#ptbl-heat-scale').textContent = self._heatLog ? 'Log' : 'Lin';
    if (self._heatActive) self._drawHeatmap(true);
  });

  /* shape selector */
  bar.querySelector('#ptbl-shape-sel').addEventListener('change', e => {
    self._tableShape = e.target.value;
    self._morphToView(e.target.value);
  });

  /* trend toggle */
  bar.querySelector('#ptbl-trend-toggle').addEventListener('click', () => {
    self._trendOn = !self._trendOn;
    bar.querySelector('#ptbl-trend-toggle').style.background = self._trendOn ? 'rgba(155,93,229,0.3)' : 'transparent';
    bar.querySelector('#ptbl-trend-toggle').style.color = self._trendOn ? '#fff' : '#aaa';
    bar.querySelector('#ptbl-trend-prop').style.display = self._trendOn ? '' : 'none';
    self._trendCanvas.style.opacity = self._trendOn ? '1' : '0';
    if (self._trendOn) self._drawTrendArrows();
  });

  bar.querySelector('#ptbl-trend-prop').addEventListener('change', e => {
    self._trendProp = e.target.value;
    if (self._trendOn) self._drawTrendArrows();
  });

  /* 3D toggle */
  bar.querySelector('#ptbl-3d-toggle').addEventListener('click', () => {
    if (typeof THREE === 'undefined') {
      bar.querySelector('#ptbl-3d-toggle').textContent = '3D (нет Three.js)';
      return;
    }
    self._3dActive = !self._3dActive;
    bar.querySelector('#ptbl-3d-toggle').style.background = self._3dActive ? 'rgba(155,93,229,0.3)' : 'transparent';
    bar.querySelector('#ptbl-3d-toggle').style.color = self._3dActive ? '#fff' : '#aaa';
    bar.querySelector('#ptbl-3d-mode').style.display = self._3dActive ? '' : 'none';

    if (self._3dActive) {
      mainArea.style.display = 'none';
      self._canvas3d.style.display = 'block';
      self._init3DTable();
    } else {
      mainArea.style.display = '';
      self._canvas3d.style.display = 'none';
      if (self._3dRaf) { cancelAnimationFrame(self._3dRaf); self._3dRaf = null; }
      if (self._3dRenderer) { self._3dRenderer.dispose(); self._3dRenderer = null; }
    }
  });

  bar.querySelector('#ptbl-3d-mode').addEventListener('change', e => {
    self._3dMode = e.target.value;
    if (self._3dActive) self._init3DTable();
  });

  this._vmBar = bar;
};

/* ══════════════════════════════════════════════════════════════
   1. HEATMAP
   ══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._drawHeatmap = function(animate) {
  const self = this;
  const propDef = HEATMAP_PROPS.find(p => p.key === this._heatProp) || HEATMAP_PROPS[0];

  /* gather values */
  const vals = ELEMENTS.map(el => propDef.get(el));
  const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v) && v > 0);
  if (validVals.length === 0) return;

  const rawMin = Math.min(...validVals);
  const rawMax = Math.max(...validVals);

  const norm = v => {
    if (v === null || v === undefined || !isFinite(v)) return null;
    if (this._heatLog) {
      const logV   = Math.log(Math.max(v, 1e-9));
      const logMin = Math.log(Math.max(rawMin, 1e-9));
      const logMax = Math.log(Math.max(rawMax, 1e-9));
      return (logV - logMin) / (logMax - logMin || 1);
    }
    return (v - rawMin) / (rawMax - rawMin || 1);
  };

  const jet = t => {
    /* jet colormap: blue→cyan→green→yellow→red */
    const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 3)));
    const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 2)));
    const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 1)));
    return `rgba(${(r*255)|0},${(g*255)|0},${(b*255)|0},0.85)`;
  };

  /* cancel previous tweens */
  this._heatTweens.forEach(h => h && h.cancel && h.cancel());
  this._heatTweens = [];

  ELEMENTS.forEach((el, i) => {
    const div = this._cellMap[el.Z];
    if (!div) return;
    const t = norm(propDef.get(el));
    const targetBg = t !== null ? jet(t) : 'rgba(60,60,80,0.5)';
    const targetBorder = t !== null ? jet(Math.min(1, t + 0.1)) : 'rgba(80,80,100,0.4)';

    div.style.border = `1px solid ${targetBorder}`;

    if (animate && window.LabFX && LabFX.motion) {
      const tFrom = norm(propDef.get(el)) || 0;
      const delay = i * 4; // stagger
      setTimeout(() => {
        if (!self._heatActive) return;
        const handle = LabFX.motion.tween(0, 1, 400, 'easeInOutCubic', prog => {
          const jt = (tFrom !== null ? tFrom * prog : 0);
          div.style.background = t !== null ? jet(t * prog) : 'rgba(60,60,80,' + (0.5 * prog) + ')';
        });
        self._heatTweens.push(handle);
      }, delay);
    } else {
      div.style.background = targetBg;
    }
    div.style.color = '#fff';
    div.style.opacity = '1';
    div.style.boxShadow = '';
  });

  /* build gradient legend */
  this._buildHeatLegend(propDef, rawMin, rawMax, jet);
};

PeriodicTableSim.prototype._buildHeatLegend = function(propDef, minV, maxV, jet) {
  const legendEl = this._vmBar.querySelector('#ptbl-heat-legend');
  if (!legendEl) return;

  /* gradient canvas */
  const W = 120, H = 14;
  legendEl.innerHTML = `
    <span style="font-size:.65rem;color:rgba(255,255,255,0.45);white-space:nowrap">${propDef.unit ? minV.toFixed(1) + ' ' + propDef.unit : minV.toFixed(1)}</span>
    <canvas id="ptbl-heat-grad" width="${W}" height="${H}" style="border-radius:3px;margin:0 4px;"></canvas>
    <span style="font-size:.65rem;color:rgba(255,255,255,0.45);white-space:nowrap">${maxV.toFixed(1)}</span>
  `;
  legendEl.style.cssText = 'display:flex;align-items:center;gap:2px;flex-shrink:0;';

  const canvas = legendEl.querySelector('#ptbl-heat-grad');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  for (let x = 0; x < W; x++) {
    ctx.fillStyle = jet(x / (W - 1));
    ctx.fillRect(x, 0, 1, H);
  }
};

/* ══════════════════════════════════════════════════════════════
   2. 3D TABLE (Three.js)
   ══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._init3DTable = function() {
  const self = this;
  if (typeof THREE === 'undefined') return;

  /* cleanup previous */
  if (this._3dRaf) { cancelAnimationFrame(this._3dRaf); this._3dRaf = null; }
  if (this._3dRenderer) { this._3dRenderer.dispose(); this._3dRenderer = null; }
  if (this._3dScene) { this._3dScene = null; }

  const canvas = this._canvas3d;
  const W = canvas.offsetWidth || 600;
  const H = canvas.offsetHeight || 400;

  const scene    = new THREE.Scene();
  scene.background = new THREE.Color(0x0d0d1a);
  this._3dScene  = scene;

  const camera   = new THREE.PerspectiveCamera(45, W / H, 0.1, 2000);
  camera.position.set(0, 60, 120);
  camera.lookAt(0, 0, 0);
  this._3dCamera = camera;

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setPixelRatio(window.devicePixelRatio || 1);
  renderer.setSize(W, H);
  this._3dRenderer = renderer;

  /* ambient + directional light */
  scene.add(new THREE.AmbientLight(0xffffff, 0.45));
  const dir = new THREE.DirectionalLight(0xffffff, 0.9);
  dir.position.set(20, 40, 20);
  scene.add(dir);

  /* get height property */
  const propDef = HEATMAP_PROPS.find(p => p.key === this._heatProp) || HEATMAP_PROPS[0];
  const vals = ELEMENTS.map(el => propDef.get(el));
  const validVals = vals.filter(v => v !== null && v !== undefined && isFinite(v) && v > 0);
  const vMin = validVals.length ? Math.min(...validVals) : 0;
  const vMax = validVals.length ? Math.max(...validVals) : 1;

  const normV = v => (v !== null && v !== undefined && isFinite(v)) ? (v - vMin) / (vMax - vMin || 1) : 0;

  const jet3 = t => {
    const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 3)));
    const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 2)));
    const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * t - 1)));
    return new THREE.Color(r, g, b);
  };

  const CELL_W = 3.2, GAP = 0.4;
  const STEP = CELL_W + GAP;
  const mode = this._3dMode;
  this._3dMeshes = [];

  /* helper: text texture for cube face */
  const makeTexture = (el, col) => {
    const tc = document.createElement('canvas');
    tc.width = 64; tc.height = 64;
    const ctx2 = tc.getContext('2d');
    ctx2.fillStyle = '#' + col.getHexString();
    ctx2.fillRect(0, 0, 64, 64);
    ctx2.fillStyle = '#fff';
    ctx2.font = 'bold 22px sans-serif';
    ctx2.textAlign = 'center';
    ctx2.textBaseline = 'middle';
    ctx2.fillText(el.symbol, 32, 32);
    return new THREE.CanvasTexture(tc);
  };

  ELEMENTS.forEach(el => {
    const pos = getCell(el);
    if (!pos) return;

    const nt = normV(propDef.get(el));
    const height = Math.max(0.4, nt * 18);
    const col = jet3(nt);

    let gridRow = pos.row;
    let gridCol = pos.col;

    /* stack mode: f-block folded into main */
    if (mode === 'stack' && pos.row > 7) {
      if (pos.row === 9) { gridRow = 6; gridCol = pos.col; }
      if (pos.row === 10) { gridRow = 7; gridCol = pos.col; }
    }

    const x = (gridCol - 9.5) * STEP;
    const z = (gridRow - 4) * STEP;

    let boxH = height;
    if (mode === 'wave') {
      /* smooth surface: average with neighbors */
      const neighbors = ELEMENTS.filter(ne => {
        const np = getCell(ne);
        if (!np) return false;
        return Math.abs(np.col - pos.col) <= 1 && Math.abs(np.row - pos.row) <= 1 && ne.Z !== el.Z;
      });
      const avgNt = neighbors.length
        ? neighbors.reduce((s, ne) => s + normV(propDef.get(ne)), 0) / neighbors.length
        : nt;
      boxH = Math.max(0.4, ((nt + avgNt) / 2) * 18);
    }

    const geom = new THREE.BoxGeometry(CELL_W, boxH, CELL_W);
    const tex  = makeTexture(el, col);
    const mats = [
      new THREE.MeshLambertMaterial({ color: col }),   // right
      new THREE.MeshLambertMaterial({ color: col }),   // left
      new THREE.MeshLambertMaterial({ map: tex }),     // top (symbol)
      new THREE.MeshLambertMaterial({ color: col }),   // bottom
      new THREE.MeshLambertMaterial({ color: col }),   // front
      new THREE.MeshLambertMaterial({ color: col }),   // back
    ];
    const mesh = new THREE.Mesh(geom, mats);
    mesh.position.set(x, boxH / 2, z);
    mesh.userData = { z: el.Z, origY: boxH / 2, nt };
    scene.add(mesh);
    this._3dMeshes.push(mesh);
  });

  /* ── orbit camera (simple drag) ── */
  let isDragging = false, lastX = 0, lastY = 0;
  let camTheta = 0.4, camPhi = Math.PI / 4, camRadius = 140;

  const updateCam = () => {
    camera.position.set(
      camRadius * Math.sin(camPhi) * Math.sin(camTheta),
      camRadius * Math.cos(camPhi),
      camRadius * Math.sin(camPhi) * Math.cos(camTheta)
    );
    camera.lookAt(0, 0, 0);
  };
  updateCam();

  canvas.addEventListener('mousedown', e => { isDragging = true; lastX = e.clientX; lastY = e.clientY; });
  canvas.addEventListener('mousemove', e => {
    if (!isDragging) return;
    camTheta -= (e.clientX - lastX) * 0.008;
    camPhi    = Math.max(0.15, Math.min(Math.PI / 2, camPhi - (e.clientY - lastY) * 0.005));
    lastX = e.clientX; lastY = e.clientY;
    updateCam();
  });
  canvas.addEventListener('mouseup', () => { isDragging = false; });
  canvas.addEventListener('wheel', e => {
    camRadius = Math.max(30, Math.min(300, camRadius + e.deltaY * 0.3));
    updateCam();
    e.preventDefault();
  }, { passive: false });

  /* ── raycaster for hover/click ── */
  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();
  let hoveredMesh = null;

  /* 3D tooltip */
  let tip3d = document.getElementById('ptbl-3d-tip');
  if (!tip3d) {
    tip3d = document.createElement('div');
    tip3d.id = 'ptbl-3d-tip';
    tip3d.style.cssText = 'position:fixed;display:none;background:rgba(10,10,30,0.92);border:1px solid rgba(155,93,229,0.5);color:#fff;font-size:.76rem;padding:5px 9px;border-radius:7px;pointer-events:none;z-index:999;';
    document.body.appendChild(tip3d);
  }

  canvas.addEventListener('mousemove', e => {
    const rect = canvas.getBoundingClientRect();
    mouse.x = ((e.clientX - rect.left) / rect.width)  * 2 - 1;
    mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    const hits = raycaster.intersectObjects(self._3dMeshes);
    if (hoveredMesh) { hoveredMesh.scale.set(1, 1, 1); hoveredMesh = null; }
    if (hits.length) {
      hoveredMesh = hits[0].object;
      hoveredMesh.scale.set(1.08, 1.08, 1.08);
      const elZ = hoveredMesh.userData.z;
      const elObj = ELEMENTS.find(e2 => e2.Z === elZ);
      if (elObj) {
        const vl = propDef.get(elObj);
        tip3d.innerHTML = `<b>${elObj.symbol}</b> — ${elObj.name}<br>${propDef.label}: ${vl !== null ? vl : '—'} ${propDef.unit}`;
        tip3d.style.display = 'block';
        tip3d.style.left = (e.clientX + 12) + 'px';
        tip3d.style.top  = (e.clientY - 10) + 'px';
      }
    } else {
      tip3d.style.display = 'none';
    }
  });

  canvas.addEventListener('click', e => {
    const rect = canvas.getBoundingClientRect();
    mouse.x = ((e.clientX - rect.left) / rect.width)  * 2 - 1;
    mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    const hits = raycaster.intersectObjects(self._3dMeshes);
    if (hits.length) {
      const elZ = hits[0].object.userData.z;
      if (elZ) self._selectElement(elZ);
    }
  });

  /* ── render loop ── */
  const animate3d = () => {
    if (!self._3dActive) return;
    self._3dRaf = requestAnimationFrame(animate3d);
    renderer.render(scene, camera);
  };
  animate3d();

  /* handle resize */
  const ro3d = new ResizeObserver(() => {
    if (!self._3dActive || !self._3dRenderer) return;
    const nW = canvas.offsetWidth, nH = canvas.offsetHeight;
    if (nW > 0 && nH > 0) {
      renderer.setSize(nW, nH);
      camera.aspect = nW / nH;
      camera.updateProjectionMatrix();
    }
  });
  ro3d.observe(canvas);
  this._3dResizeObs = ro3d;
};

/* ══════════════════════════════════════════════════════════════
   3. TABLE SHAPE MORPH (std / long / short)
   ══════════════════════════════════════════════════════════════ */

/* positions for LONG form (32-column, f-block inline) */
function _longFormPos(el) {
  if (el.block === 'f') {
    /* lanthanides: period 6, cols 4-17; actinides: period 7, cols 4-17 */
    const fPos = { 57:4,58:5,59:6,60:7,61:8,62:9,63:10,64:11,65:12,66:13,67:14,68:15,69:16,70:17,71:18,
                   89:4,90:5,91:6,92:7,93:8,94:9,95:10,96:11,97:12,98:13,99:14,100:15,101:16,102:17,103:18 };
    return { row: el.period, col: fPos[el.Z] || (el.Z - 54 + 4) };
  }
  /* s and d blocks shift right by 14 (f-block inserted) */
  const stdPos = getCell(el);
  if (!stdPos) return null;
  if (el.block === 's') {
    if (el.group === 1) return { row: stdPos.row, col: 1 };
    if (el.group === 2) return { row: stdPos.row, col: 2 };
  }
  if (el.block === 'd' || (el.block === 'p')) {
    return { row: stdPos.row, col: stdPos.col + 14 };
  }
  return { row: stdPos.row, col: stdPos.col + 14 };
}

/* positions for SHORT (8-group) form — classic Russian table */
function _shortFormPos(el) {
  /* main group elements: groups 1-8 */
  const SHORT_MAP = {
    1:{1:1,3:1,11:1,19:1,37:1,55:1,87:1},
    2:{4:2,12:2,20:2,38:2,56:2,88:2},
  };
  const stdPos = getCell(el);
  if (!stdPos) return null;
  if (el.block === 'd') {
    /* transition metals: group 3-12 → columns 3-8 with subgroup */
    const subCol = ((el.group - 3) % 8) + 3;
    return { row: stdPos.row, col: subCol };
  }
  if (el.block === 'f') {
    /* lanthanides row 9, actinides row 10 — same as standard */
    return getCell(el);
  }
  /* p and s blocks */
  if (el.group) {
    const shortGroup = el.group <= 2 ? el.group : (el.group - 10 <= 0 ? el.group - 10 + 8 : el.group - 10);
    return { row: stdPos.row, col: Math.max(1, Math.min(8, el.group <= 2 ? el.group : el.group - 10)) };
  }
  return stdPos;
}

PeriodicTableSim.prototype._morphToView = function(shape) {
  const self = this;
  const DURATION = 800;

  /* snapshot current pixel positions of each cell */
  const gridRect = this._tableEl.getBoundingClientRect();

  /* we animate by moving each cell div to absolute position during morph
     then snap to new grid layout */

  /* for std, we just rebuild; for long/short we switch grid and re-place */

  if (shape === 'std') {
    /* restore normal 18-col grid */
    this._tableEl.style.gridTemplateColumns = 'repeat(18,1fr)';
    if (this._fblockEl) this._fblockEl.style.display = '';
    this._colorTable();
    return;
  }

  if (shape === 'long') {
    /* switch to 32-column grid, hide f-block row */
    if (this._fblockEl) this._fblockEl.style.display = 'none';
    this._tableEl.style.gridTemplateColumns = 'repeat(32,1fr)';

    /* re-map cells — rebuild grid items */
    this._tableEl.innerHTML = '';

    const cells = {};
    for (let r = 1; r <= 7; r++) {
      for (let c = 1; c <= 32; c++) {
        const d = document.createElement('div');
        d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
        cells[`${r},${c}`] = d;
        this._tableEl.appendChild(d);
      }
    }

    for (const el of ELEMENTS) {
      const pos = _longFormPos(el);
      if (!pos || pos.row > 7) continue;
      const div = cells[`${pos.row},${pos.col}`];
      if (!div) continue;
      this._cellMap[el.Z] = div;
      div.dataset.z = el.Z;
      div.title = `${el.name} (${el.symbol})`;
      div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;opacity:0;';
      div.innerHTML = `
        <span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
        <span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
        <span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
      div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
      div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
      div.addEventListener('click', () => self._selectElement(el.Z));

      /* fade-in staggered */
      const delay = (pos.col + pos.row * 2) * 8;
      setTimeout(() => {
        if (window.LabFX && LabFX.motion) {
          LabFX.motion.tween(0, 1, DURATION, 'easeInOutCubic', v => { div.style.opacity = v; });
        } else {
          div.style.opacity = '1';
        }
      }, delay);
    }

    this._colorTable();
    return;
  }

  if (shape === 'short') {
    /* 8-column grid */
    if (this._fblockEl) this._fblockEl.style.display = '';
    this._tableEl.style.gridTemplateColumns = 'repeat(8,1fr)';
    this._tableEl.innerHTML = '';

    const cells = {};
    for (let r = 1; r <= 7; r++) {
      for (let c = 1; c <= 8; c++) {
        const d = document.createElement('div');
        d.style.cssText = 'aspect-ratio:1;border-radius:4px;';
        cells[`${r},${c}`] = d;
        this._tableEl.appendChild(d);
      }
    }

    for (const el of ELEMENTS) {
      if (el.block === 'f') continue; /* f-block stays in fblockEl */
      const stdPos = getCell(el);
      if (!stdPos) continue;
      let col;
      if (el.group <= 2) {
        col = el.group;
      } else if (el.group >= 13) {
        col = el.group - 10;
      } else {
        /* d-block: groups 3-12 → compress to cols 3-8 with row offset tricks */
        col = Math.min(8, ((el.group - 3) % 8) + 3);
      }
      col = Math.max(1, Math.min(8, col));
      const div = cells[`${stdPos.row},${col}`] || cells[`${stdPos.row},1`];
      if (!div || div.dataset.z) continue;
      this._cellMap[el.Z] = div;
      div.dataset.z = el.Z;
      div.title = `${el.name} (${el.symbol})`;
      div.style.cssText += 'cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:filter .12s,transform .12s;position:relative;overflow:hidden;opacity:0;';
      div.innerHTML = `
        <span style="font-size:.55em;opacity:.7;line-height:1">${el.Z}</span>
        <span style="font-size:.85em;font-weight:800;line-height:1.1">${el.symbol}</span>
        <span style="font-size:.42em;opacity:.65;line-height:1.2;text-align:center;max-width:90%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${el.name}</span>`;
      div.addEventListener('mouseenter', () => { div.style.filter='brightness(1.4)'; div.style.transform='scale(1.12)'; div.style.zIndex='10'; });
      div.addEventListener('mouseleave', () => { div.style.filter=''; div.style.transform=''; div.style.zIndex=''; });
      div.addEventListener('click', () => self._selectElement(el.Z));

      const delay = (col + stdPos.row * 2) * 10;
      setTimeout(() => {
        if (window.LabFX && LabFX.motion) {
          LabFX.motion.tween(0, 1, DURATION, 'easeInOutCubic', v => { div.style.opacity = v; });
        } else {
          div.style.opacity = '1';
        }
      }, delay);
    }

    this._colorTable();
  }
};

/* ══════════════════════════════════════════════════════════════
   4. TREND ARROWS
   ══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._drawTrendArrows = function() {
  const self = this;
  const canvas = this._trendCanvas;
  if (!canvas) return;

  const parent = canvas.parentElement;
  const W = parent.offsetWidth  || 600;
  const H = parent.offsetHeight || 300;
  const dpr = window.devicePixelRatio || 1;
  canvas.width  = W * dpr;
  canvas.height = H * dpr;
  canvas.style.width  = W + 'px';
  canvas.style.height = H + 'px';
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  ctx.clearRect(0, 0, W, H);

  const cfg = TREND_CONFIG[this._trendProp] || TREND_CONFIG.radius;
  const ARROW_W = 12;
  const PADDING = 4;
  const PAD_BOTTOM = 14;
  const PAD_LEFT   = 14;

  /* draw arrow helper: from (x1,y1) to (x2,y2) with gradient and label */
  const drawArrow = (x1, y1, x2, y2, color, label, labelSide) => {
    const dx = x2 - x1, dy = y2 - y1;
    const len = Math.sqrt(dx * dx + dy * dy);
    if (len < 10) return;

    const grad = ctx.createLinearGradient(x1, y1, x2, y2);
    grad.addColorStop(0, color + '22');
    grad.addColorStop(0.5, color + 'bb');
    grad.addColorStop(1, color + 'ff');

    /* shaft */
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.strokeStyle = grad;
    ctx.lineWidth = ARROW_W;
    ctx.lineCap = 'round';
    ctx.stroke();

    /* arrowhead */
    const angle = Math.atan2(dy, dx);
    const AH = 16;
    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - AH * Math.cos(angle - 0.45), y2 - AH * Math.sin(angle - 0.45));
    ctx.lineTo(x2 - AH * Math.cos(angle + 0.45), y2 - AH * Math.sin(angle + 0.45));
    ctx.closePath();
    ctx.fillStyle = color;
    ctx.fill();

    /* label */
    ctx.save();
    ctx.font = 'bold 11px Manrope,sans-serif';
    ctx.fillStyle = color;
    ctx.shadowColor = 'rgba(0,0,0,0.8)';
    ctx.shadowBlur = 4;
    const mx = (x1 + x2) / 2;
    const my = (y1 + y2) / 2;
    if (labelSide === 'below') {
      ctx.textAlign = 'center';
      ctx.fillText(label, mx, my + 20);
    } else if (labelSide === 'left') {
      ctx.textAlign = 'right';
      ctx.fillText(label, mx - 14, my);
    }
    ctx.restore();
  };

  const hCfg = cfg.hArrow;
  const vCfg = cfg.vArrow;

  /* horizontal arrow: bottom of table */
  const yBottom = H - PAD_BOTTOM;
  if (hCfg.dir === 'left') {
    drawArrow(W - PADDING, yBottom, PADDING, yBottom, hCfg.color, hCfg.text, 'below');
  } else {
    drawArrow(PADDING, yBottom, W - PADDING, yBottom, hCfg.color, hCfg.text, 'below');
  }

  /* vertical arrow: left side of table */
  const xLeft = PAD_LEFT;
  if (vCfg.dir === 'down') {
    drawArrow(xLeft, PADDING, xLeft, H - PAD_BOTTOM - 20, vCfg.color, vCfg.text, 'left');
  } else {
    drawArrow(xLeft, H - PAD_BOTTOM - 20, xLeft, PADDING, vCfg.color, vCfg.text, 'left');
  }
};

/* ── patch stop() to clean up 3D ──────────────────────────── */
const _origStop = PeriodicTableSim.prototype.stop;
PeriodicTableSim.prototype.stop = function() {
  _origStop.call(this);
  if (this._3dRaf) { cancelAnimationFrame(this._3dRaf); this._3dRaf = null; }
  if (this._3dRenderer) { this._3dRenderer.dispose(); this._3dRenderer = null; }
  if (this._3dResizeObs) { this._3dResizeObs.disconnect(); }
  const tip3d = document.getElementById('ptbl-3d-tip');
  if (tip3d) tip3d.style.display = 'none';
};

/* ══════════════════════════════════════════════════════════════
   WAVE D — ELECTRON-CONFIG DEEP TOOLS
   _periodG_*: orbital filling, aufbau diagram,
               quantum-number hover, Bohr excitation overlay
   ══════════════════════════════════════════════════════════════ */

/* ── Arrow helper (module-level, used by orbital filling) ────── */
function _periodG_drawArrow(ctx, ax, ay1, ay2, col) {
  ctx.beginPath(); ctx.moveTo(ax, ay1); ctx.lineTo(ax, ay2);
  ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.stroke();
  const dir = ay2 < ay1 ? -1 : 1;
  ctx.beginPath();
  ctx.moveTo(ax, ay2); ctx.lineTo(ax - 2.5, ay2 - dir * 4); ctx.lineTo(ax + 2.5, ay2 - dir * 4);
  ctx.closePath(); ctx.fillStyle = col; ctx.fill();
}

/* ── Aufbau filling order ─────────────────────────────────────── */
PeriodicTableSim.prototype._periodG_aufbauOrder = function() {
  return [
    {n:1,l:0,label:'1s',cap:2},  {n:2,l:0,label:'2s',cap:2},
    {n:2,l:1,label:'2p',cap:6},  {n:3,l:0,label:'3s',cap:2},
    {n:3,l:1,label:'3p',cap:6},  {n:4,l:0,label:'4s',cap:2},
    {n:3,l:2,label:'3d',cap:10}, {n:4,l:1,label:'4p',cap:6},
    {n:5,l:0,label:'5s',cap:2},  {n:4,l:2,label:'4d',cap:10},
    {n:5,l:1,label:'5p',cap:6},  {n:6,l:0,label:'6s',cap:2},
    {n:4,l:3,label:'4f',cap:14}, {n:5,l:2,label:'5d',cap:10},
    {n:6,l:1,label:'6p',cap:6},  {n:7,l:0,label:'7s',cap:2},
    {n:5,l:3,label:'5f',cap:14}, {n:6,l:2,label:'6d',cap:10},
    {n:7,l:1,label:'7p',cap:6},
  ];
};

PeriodicTableSim.prototype._periodG_mlLabels = function(l) {
  if (l === 0) return [''];
  if (l === 1) return ['px','py','pz'];
  if (l === 2) return ['dxy','dxz','dyz','dx2y2','dz2'];
  const out = [];
  for (let m = -l; m <= l; m++) out.push('f' + (m >= 0 ? '+' : '') + m);
  return out;
};

PeriodicTableSim.prototype._periodG_buildElectronList = function(Z) {
  const order = this._periodG_aufbauOrder();
  const electrons = [];
  let rem = Z;
  for (const sub of order) {
    if (rem <= 0) break;
    const mlList = this._periodG_mlLabels(sub.l);
    const nOrb = mlList.length;
    for (let pass = 0; pass < 2 && rem > 0; pass++) {
      for (let oi = 0; oi < nOrb && rem > 0; oi++) {
        electrons.push({
          n: sub.n, l: sub.l, ml: oi - sub.l,
          ms: pass === 0 ? 0.5 : -0.5,
          subLabel: sub.label, orbIdx: oi, subRef: sub, mlLabel: mlList[oi],
        });
        rem--;
      }
    }
  }
  return electrons;
};

/* ══════════════════════════════════════════════════════════════
   TOOL 1 — Orbital Filling Diagram
   ══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._periodG_drawOrbitalFilling = function(canvas, el) {
  const electrons = this._periodG_buildElectronList(el.Z);
  if (!electrons.length) return;
  const subMap = new Map();
  for (const e of electrons) {
    if (!subMap.has(e.subLabel)) subMap.set(e.subLabel, []);
    subMap.get(e.subLabel).push(e);
  }
  const order = this._periodG_aufbauOrder();
  const subs = order.filter(s => subMap.has(s.label));
  const BOX = 18, GAP = 3, LPAD = 28, VPAD = 5, ROW_H = BOX + VPAD * 2 + 8;
  const W = canvas.offsetWidth || 240;
  const H_needed = subs.length * ROW_H + 10;
  const dpr = window.devicePixelRatio || 1;
  canvas.width = W * dpr; canvas.height = H_needed * dpr;
  canvas.style.height = H_needed + 'px';
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H_needed);
  const hitMap = [];

  subs.forEach((sub, si) => {
    const y0 = si * ROW_H + VPAD;
    const electronArr = subMap.get(sub.label);
    const nOrb = 2 * sub.l + 1;
    const mlLabels = this._periodG_mlLabels(sub.l);
    const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#aaa';
    ctx.font = 'bold 10px Manrope,sans-serif'; ctx.fillStyle = bColor;
    ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
    ctx.fillText(sub.label, 2, y0 + BOX / 2);
    const isLastSub = si === subs.length - 1;
    for (let oi = 0; oi < nOrb; oi++) {
      const bx = LPAD + oi * (BOX + GAP), by = y0;
      const inOrb = electronArr.filter(e => e.orbIdx === oi);
      const isLastBox = isLastSub && oi === nOrb - 1 && inOrb.length > 0;
      ctx.fillStyle = 'rgba(255,255,255,0.06)';
      ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
      ctx.fill(); ctx.stroke();
      if (nOrb > 1) {
        ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.25)';
        ctx.textAlign = 'center'; ctx.textBaseline = 'top';
        ctx.fillText(mlLabels[oi], bx + BOX / 2, by + BOX + 1);
      }
      inOrb.forEach((e, ei) => {
        const isUp = e.ms > 0;
        const ax = bx + (ei === 0 ? BOX * 0.35 : BOX * 0.65);
        const ay_t = by + 3, ay_b = by + BOX - 3;
        const col = isLastBox ? '#FFD166' : '#06D6E0';
        if (isLastBox && window.LabFX)
          LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 8 });
        else
          _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
        hitMap.push({ x: ax - 6, y: by, w: 12, h: BOX, electron: e });
      });
    }
  });
  canvas._hitMap = hitMap; canvas._dpr = dpr;
};

/* ── Highlight variant (single electron lit, rest dimmed) ─────── */
PeriodicTableSim.prototype._periodG_drawOrbFillingHL = function(canvas, el, hvEl) {
  const electrons = this._periodG_buildElectronList(el.Z);
  const subMap = new Map();
  for (const e of electrons) {
    if (!subMap.has(e.subLabel)) subMap.set(e.subLabel, []);
    subMap.get(e.subLabel).push(e);
  }
  const order = this._periodG_aufbauOrder();
  const subs = order.filter(s => subMap.has(s.label));
  const BOX = 18, GAP = 3, LPAD = 28, VPAD = 5, ROW_H = BOX + VPAD * 2 + 8;
  const W = canvas.offsetWidth || 240;
  const H_needed = subs.length * ROW_H + 10;
  const dpr = canvas._dpr || window.devicePixelRatio || 1;
  const ctx = canvas.getContext('2d');
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H_needed);
  subs.forEach((sub, si) => {
    const y0 = si * ROW_H + VPAD;
    const electronArr = subMap.get(sub.label);
    const nOrb = 2 * sub.l + 1;
    const mlLabels = this._periodG_mlLabels(sub.l);
    const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#aaa';
    ctx.font = 'bold 10px Manrope,sans-serif'; ctx.fillStyle = bColor;
    ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(sub.label, 2, y0 + BOX / 2);
    for (let oi = 0; oi < nOrb; oi++) {
      const bx = LPAD + oi * (BOX + GAP), by = y0;
      const inOrb = electronArr.filter(e => e.orbIdx === oi);
      ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
      ctx.fill(); ctx.stroke();
      if (nOrb > 1) {
        ctx.font = '7px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.25)';
        ctx.textAlign = 'center'; ctx.textBaseline = 'top';
        ctx.fillText(mlLabels[oi], bx + BOX / 2, by + BOX + 1);
      }
      inOrb.forEach((e, ei) => {
        const isHover = e.n === hvEl.n && e.l === hvEl.l && e.ml === hvEl.ml && e.ms === hvEl.ms;
        const isUp = e.ms > 0;
        const ax = bx + (ei === 0 ? BOX * 0.35 : BOX * 0.65);
        const ay_t = by + 3, ay_b = by + BOX - 3;
        const col = isHover ? '#FFD166' : 'rgba(6,214,224,0.3)';
        if (isHover && window.LabFX)
          LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 10 });
        else
          _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
      });
    }
  });
};

/* ══════════════════════════════════════════════════════════════
   TOOL 3 — Quantum-number hover on orbital filling
   ══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._periodG_attachQNHover = function(canvas, el) {
  if (canvas._periodG_mmH) {
    canvas.removeEventListener('mousemove', canvas._periodG_mmH);
    canvas.removeEventListener('mouseleave', canvas._periodG_mlH);
  }
  const tip = this._periodG_qTip;
  canvas._periodG_mmH = (ev) => {
    const rect = canvas.getBoundingClientRect();
    const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
    const hit = (canvas._hitMap || []).find(h => mx >= h.x && mx <= h.x + h.w && my >= h.y && my <= h.y + h.h);
    if (hit) {
      const e = hit.electron;
      const lName = ['s','p','d','f'][e.l] || String(e.l);
      if (tip) {
        tip.innerHTML = `<b style="color:#9B5DE5">${e.subLabel} ${e.mlLabel}</b><br>n = ${e.n}<br>l = ${e.l} (${lName})<br>m<sub>l</sub> = ${e.ml}<br>m<sub>s</sub> = ${e.ms > 0 ? '+1/2' : '-1/2'}`;
        tip.style.display = 'block';
        tip.style.left = (ev.clientX + 12) + 'px';
        tip.style.top  = (ev.clientY - 10) + 'px';
      }
      this._periodG_drawOrbFillingHL(canvas, el, hit.electron);
    } else {
      if (tip) tip.style.display = 'none';
      this._periodG_drawOrbitalFilling(canvas, el);
    }
  };
  canvas._periodG_mlH = () => { if (tip) tip.style.display = 'none'; this._periodG_drawOrbitalFilling(canvas, el); };
  canvas.addEventListener('mousemove', canvas._periodG_mmH);
  canvas.addEventListener('mouseleave', canvas._periodG_mlH);
};

/* ══════════════════════════════════════════════════════════════
   TOOL 2 — Aufbau Diagram with Z slider
   ══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._periodG_drawAufbau = function(canvas, Z) {
  const order = this._periodG_aufbauOrder();
  const W = canvas.offsetWidth || 240;
  const BOX = 14, GAP = 2, LPAD = 26, VPAD = 4, ROW_H = BOX + VPAD * 2;
  const H_needed = order.length * ROW_H + 14;
  const dpr = window.devicePixelRatio || 1;
  canvas.width = W * dpr; canvas.height = H_needed * dpr;
  canvas.style.height = H_needed + 'px';
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H_needed);
  const electrons = this._periodG_buildElectronList(Z);
  const subCount = new Map();
  for (const e of electrons) subCount.set(e.subLabel, (subCount.get(e.subLabel) || 0) + 1);
  let lastRow = -1;
  order.forEach((sub, si) => { if ((subCount.get(sub.label) || 0) > 0) lastRow = si; });
  order.forEach((sub, si) => {
    const y0 = si * ROW_H + VPAD;
    const nOrb = 2 * sub.l + 1;
    const filled = subCount.get(sub.label) || 0;
    const bColor = BLOCK_COLORS[sub.l===0?'s':sub.l===1?'p':sub.l===2?'d':'f'] || '#888';
    ctx.font = 'bold 9px Manrope,sans-serif';
    ctx.fillStyle = si <= lastRow ? bColor : 'rgba(255,255,255,0.2)';
    ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(sub.label, 2, y0 + BOX / 2);
    for (let oi = 0; oi < nOrb; oi++) {
      const bx = LPAD + oi * (BOX + GAP), by = y0;
      ctx.fillStyle = 'rgba(255,255,255,0.04)';
      ctx.strokeStyle = si <= lastRow ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.06)';
      ctx.lineWidth = 1; ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(bx, by, BOX, BOX, 2); else ctx.rect(bx, by, BOX, BOX);
      ctx.fill(); ctx.stroke();
    }
    let rem = filled;
    const spins = Array(nOrb).fill(0);
    for (let oi = 0; oi < nOrb && rem > 0; oi++) { spins[oi]++; rem--; }
    for (let oi = 0; oi < nOrb && rem > 0; oi++) { spins[oi]++; rem--; }
    for (let oi = 0; oi < nOrb; oi++) {
      const bx = LPAD + oi * (BOX + GAP), by = y0;
      const isLast = si === lastRow && oi === nOrb - 1 && spins[oi] > 0;
      for (let sp = 0; sp < spins[oi]; sp++) {
        const isUp = sp === 0;
        const ax = bx + (sp === 0 ? BOX * 0.35 : BOX * 0.65);
        const ay_t = by + 2, ay_b = by + BOX - 2;
        const col = isLast ? '#FFD166' : bColor;
        if (isLast && window.LabFX)
          LabFX.glow.drawGlow(ctx, () => _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col), { color: col, intensity: 6 });
        else
          _periodG_drawArrow(ctx, ax, isUp ? ay_b : ay_t, isUp ? ay_t : ay_b, col);
      }
    }
    if (si === lastRow && filled < sub.cap) {
      const nx = LPAD + nOrb * (BOX + GAP) + 2, ny = y0 + BOX / 2;
      ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(nx, ny); ctx.lineTo(nx + 7, ny); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(nx+7,ny); ctx.lineTo(nx+4,ny-3); ctx.lineTo(nx+4,ny+3);
      ctx.fillStyle = '#9B5DE5'; ctx.fill();
    }
  });
  const now = performance.now();
  if (this._periodG_lastAufbauZ !== Z && window.LabFX) {
    if (!this._periodG_aufbauSoundTs || now - this._periodG_aufbauSoundTs > 80) {
      LabFX.sound.play('tick', { pitch: 0.8 + Z * 0.01, volume: 0.08 });
      this._periodG_aufbauSoundTs = now;
    }
  }
  this._periodG_lastAufbauZ = Z;
};

/* ══════════════════════════════════════════════════════════════
   Tab injection — patches _updateCard to add «Орбитали»/«Aufbau»
   ══════════════════════════════════════════════════════════════ */
(function() {
  const _orig = PeriodicTableSim.prototype._updateCard;
  PeriodicTableSim.prototype._updateCard = function(el) {
    if (this._periodG_cleanupQTip) { this._periodG_cleanupQTip(); this._periodG_cleanupQTip = null; }
    _orig.call(this, el);
    if (!el) return;
    this._periodG_addElecTab(el);
  };
})();

PeriodicTableSim.prototype._periodG_addElecTab = function(el) {
  const card = this._cardEl;
  const tabBar = document.createElement('div');
  tabBar.style.cssText = 'display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,0.1);margin:8px -10px 0;';
  const btns = ['Орбитали','Aufbau'].map((lbl) => {
    const btn = document.createElement('button');
    btn.textContent = lbl;
    btn.style.cssText = 'flex:1;padding:5px 0;border:none;background:transparent;color:rgba(255,255,255,0.4);font-size:.7rem;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;';
    tabBar.appendChild(btn); return btn;
  });
  card.appendChild(tabBar);
  const wrap = document.createElement('div'); wrap.style.cssText = 'position:relative;';
  card.appendChild(wrap);

  // Panel 0: orbital filling canvas
  const orbPan = document.createElement('div'); orbPan.style.cssText = 'display:none;padding:4px 0 0;';
  const orbCv = document.createElement('canvas'); orbCv.style.cssText = 'width:100%;display:block;cursor:crosshair;';
  orbPan.appendChild(orbCv); wrap.appendChild(orbPan);

  // Panel 1: Aufbau canvas + Z slider
  const aufPan = document.createElement('div'); aufPan.style.cssText = 'display:none;padding:4px 0 0;';
  const aufCv = document.createElement('canvas'); aufCv.style.cssText = 'width:100%;display:block;';
  aufPan.appendChild(aufCv);
  const sw = document.createElement('div'); sw.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 0;';
  const sld = document.createElement('input'); sld.type='range'; sld.min=1; sld.max=118; sld.value=el.Z;
  sld.style.cssText = 'flex:1;accent-color:#9B5DE5;';
  const slLbl = document.createElement('span'); slLbl.style.cssText = 'font-size:.68rem;color:#9B5DE5;min-width:32px;text-align:right;';
  slLbl.textContent = 'Z=' + el.Z;
  sw.appendChild(sld); sw.appendChild(slLbl); aufPan.appendChild(sw); wrap.appendChild(aufPan);

  // quantum number tooltip (fixed positioned, cleaned up on new element)
  const qTip = document.createElement('div');
  qTip.style.cssText = 'position:fixed;display:none;padding:5px 9px;background:#1a1a2e;border:1px solid rgba(155,93,229,0.5);border-radius:6px;font-size:.68rem;color:#ccc;pointer-events:none;z-index:9999;line-height:1.65;';
  document.body.appendChild(qTip);
  this._periodG_qTip = qTip;
  this._periodG_cleanupQTip = () => {
    qTip.style.display = 'none';
    if (qTip.parentNode) qTip.parentNode.removeChild(qTip);
    this._periodG_qTip = null;
  };

  const panels = [orbPan, aufPan];
  const showTab = (idx) => {
    btns.forEach((b, i) => {
      b.style.color = i === idx ? '#fff' : 'rgba(255,255,255,0.4)';
      b.style.borderBottomColor = i === idx ? '#9B5DE5' : 'transparent';
    });
    panels.forEach((p, i) => p.style.display = i === idx ? 'block' : 'none');
    if (idx === 0) setTimeout(() => { this._periodG_drawOrbitalFilling(orbCv, el); this._periodG_attachQNHover(orbCv, el); }, 0);
    if (idx === 1) setTimeout(() => this._periodG_drawAufbau(aufCv, +sld.value), 0);
  };
  btns.forEach((b, i) => b.addEventListener('click', () => showTab(i)));
  sld.addEventListener('input', () => { const z = +sld.value; slLbl.textContent = 'Z='+z; this._periodG_drawAufbau(aufCv, z); });
  showTab(0);
};

/* ══════════════════════════════════════════════════════════════
   TOOL 4 — Bohr excitation (patches _drawBohr)
   ══════════════════════════════════════════════════════════════ */
PeriodicTableSim.prototype._periodG_wavelengthToRGB = function(nm) {
  let R=0,G=0,B=0,a=1;
  if(nm>=380&&nm<440){R=-(nm-440)/(440-380);G=0;B=1;}
  else if(nm<490){R=0;G=(nm-440)/(490-440);B=1;}
  else if(nm<510){R=0;G=1;B=-(nm-510)/(510-490);}
  else if(nm<580){R=(nm-510)/(580-510);G=1;B=0;}
  else if(nm<645){R=1;G=-(nm-645)/(645-580);B=0;}
  else if(nm<781){R=1;G=0;B=0;}
  if(nm>=700)a=0.3+0.7*(780-nm)/(780-700);
  else if(nm<420)a=0.3+0.7*(nm-380)/(420-380);
  return `rgba(${Math.round(R*255*a)},${Math.round(G*255*a)},${Math.round(B*255*a)},1)`;
};

PeriodicTableSim.prototype._periodG_initBohrExcite = function() {
  const canvas = this._bohrCanvas;
  if (canvas._periodG_excite_bound) return;
  canvas._periodG_excite_bound = true;
  this._periodG_excState = null;
  canvas.title = 'Клик на электрон — возбуждение';
  canvas.addEventListener('click', (ev) => {
    if (!this._bohrZ) return;
    const el = ELEMENTS.find(e => e.Z === this._bohrZ); if (!el) return;
    const shells = getShellFill(el.Z);
    const rect = canvas.getBoundingClientRect();
    const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
    const W = canvas.offsetWidth || 240, H = canvas.offsetHeight || 150;
    const cx = W/2, cy = H/2, maxR = Math.min(W,H)*0.44, nShells = shells.length;
    let clickedShell = -1;
    outer: for (let i = 0; i < nShells; i++) {
      const r = maxR*(i+1)/nShells, speed = 1-i*0.12;
      for (let e2 = 0; e2 < shells[i]; e2++) {
        const a = this._bohrAngle*speed + (2*Math.PI*e2)/shells[i];
        const dx = mx-(cx+r*Math.cos(a)), dy = my-(cy+r*Math.sin(a));
        if (Math.sqrt(dx*dx+dy*dy) < 9) { clickedShell = i; break outer; }
      }
    }
    if (clickedShell < 0) {
      for (let i = 0; i < nShells; i++) {
        const r = maxR*(i+1)/nShells;
        const dist = Math.sqrt((mx-cx)**2+(my-cy)**2);
        if (Math.abs(dist-r) < 8 && shells[i] > 0) { clickedShell = i; break; }
      }
    }
    if (clickedShell < 0) return;
    this._periodG_showExciteMenu(el, shells, clickedShell, ev.clientX, ev.clientY);
  });
};

PeriodicTableSim.prototype._periodG_showExciteMenu = function(el, shells, n1idx, px, py) {
  const old = document.getElementById('periodG-excite-menu'); if (old) old.remove();
  const menu = document.createElement('div');
  menu.id = 'periodG-excite-menu';
  menu.style.cssText = `position:fixed;left:${px+6}px;top:${py}px;background:#12122a;border:1px solid rgba(155,93,229,0.55);border-radius:8px;padding:8px;z-index:9999;font-size:.71rem;color:#ccc;min-width:175px;box-shadow:0 4px 18px rgba(0,0,0,0.5);`;
  menu.innerHTML = `<div style="color:#9B5DE5;font-weight:700;margin-bottom:5px;font-size:.7rem">Переход из n=${n1idx+1}:</div>`;
  for (let i = 0; i < shells.length; i++) {
    if (i === n1idx) continue;
    const n = i+1, n1 = n1idx+1;
    const dE = 13.6 * (1/(n1*n1) - 1/(n*n));
    const dE_abs = Math.abs(dE);
    const lam = dE_abs > 0.02 ? Math.round(1240/dE_abs) : 99999;
    const region = lam < 380 ? 'УФ' : lam > 780 ? 'ИК' : 'видим.';
    const abs = dE > 0;
    const btn = document.createElement('button');
    btn.style.cssText = 'display:block;width:100%;text-align:left;padding:3px 7px;border:none;background:transparent;color:#ccc;cursor:pointer;border-radius:4px;font-size:.7rem;';
    btn.innerHTML = `n=${n} &rarr; ${lam < 99999 ? lam+'нм ('+region+')' : '—'} ${abs ? '[+ф]' : '[-ф]'}`;
    btn.addEventListener('mouseenter', () => btn.style.background = 'rgba(155,93,229,0.2)');
    btn.addEventListener('mouseleave', () => btn.style.background = 'transparent');
    btn.addEventListener('click', () => { menu.remove(); this._periodG_startExcitation(el, n1idx, i, lam); });
    menu.appendChild(btn);
  }
  const cb = document.createElement('button');
  cb.style.cssText = 'display:block;width:100%;text-align:center;padding:2px 0;border:none;background:transparent;color:rgba(255,255,255,0.3);cursor:pointer;font-size:.68rem;margin-top:5px;';
  cb.textContent = 'Отмена'; cb.addEventListener('click', () => menu.remove());
  menu.appendChild(cb); document.body.appendChild(menu);
  const outside = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', outside); } };
  setTimeout(() => document.addEventListener('click', outside), 60);
};

PeriodicTableSim.prototype._periodG_startExcitation = function(el, n1idx, n2idx, lam) {
  this._periodG_excState = { n1: n1idx, n2: n2idx, lam, phase: 'up', t: performance.now() };
  if (window.LabFX) LabFX.sound.play('chime', { pitch: n2idx > n1idx ? 1.3 : 0.7, volume: 0.3 });
};

/* patch _drawBohr to incorporate excitation physics */
(function() {
  const _orig = PeriodicTableSim.prototype._drawBohr;
  PeriodicTableSim.prototype._drawBohr = function() {
    if (!this._bohrCanvas._periodG_excite_bound) this._periodG_initBohrExcite();
    const canvas = this._bohrCanvas;
    const dpr = window.devicePixelRatio || 1;
    const W = canvas.offsetWidth || 240, H = canvas.offsetHeight || 150;
    canvas.width = W*dpr; canvas.height = H*dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
    if (!this._bohrZ) return;
    const el = ELEMENTS.find(e => e.Z === this._bohrZ); if (!el) return;
    const shells = getShellFill(el.Z);
    const cx = W/2, cy = H/2, maxR = Math.min(W,H)*0.44, nShells = shells.length;
    const col = TYPE_COLORS[el.type] || '#7B8EF7';
    const exc = this._periodG_excState;
    const now = performance.now();
    if (exc) {
      const el2 = now - exc.t;
      if (exc.phase === 'up'   && el2 > 600) { exc.phase = 'stay'; exc.t = now; }
      else if (exc.phase === 'stay' && el2 > 800) { exc.phase = 'down'; exc.t = now; }
      else if (exc.phase === 'down' && el2 > 600) { exc.phase = 'done'; }
    }
    ctx.beginPath(); ctx.arc(cx, cy, nShells > 0 ? 5+nShells*1.5 : 6, 0, Math.PI*2);
    ctx.fillStyle = col; ctx.fill();
    shells.forEach((count, i) => {
      const r = maxR*(i+1)/nShells;
      ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
      ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 1; ctx.stroke();
      const speed = 1-i*0.12;
      for (let e2 = 0; e2 < count; e2++) {
        const a = this._bohrAngle*speed + (2*Math.PI*e2)/count;
        let drawR = r;
        const isExc = exc && exc.phase !== 'done' && i === exc.n1 && e2 === 0;
        if (isExc) {
          const r2 = maxR*(exc.n2+1)/nShells;
          const elapsed2 = now - exc.t;
          let prog = 0;
          if (exc.phase === 'up')   prog = Math.min(elapsed2/600, 1);
          else if (exc.phase === 'stay') prog = 1;
          else if (exc.phase === 'down') prog = 1 - Math.min(elapsed2/600, 1);
          drawR = r + (r2-r)*prog;
          if (exc.phase === 'up') {
            const pf = Math.min(elapsed2/600, 1);
            const pm = r + (r2-r)*pf*0.5, pa = a + Math.PI/2;
            const px2 = cx+pm*Math.cos(pa), py2 = cy+pm*Math.sin(pa);
            const lamNm = exc.lam;
            const pcol = (lamNm>=380&&lamNm<=780) ? this._periodG_wavelengthToRGB(lamNm) : (lamNm<380 ? '#cc88ff' : '#ffaaaa');
            if (window.LabFX) {
              LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.arc(px2,py2,4,0,Math.PI*2); ctx.fillStyle=pcol; ctx.fill(); }, { color: pcol, intensity: 12 });
            } else { ctx.beginPath(); ctx.arc(px2,py2,4,0,Math.PI*2); ctx.fillStyle=pcol; ctx.fill(); }
            ctx.font='8px Manrope,sans-serif'; ctx.fillStyle=pcol; ctx.textAlign='center';
            ctx.fillText((exc.lam<99999?exc.lam+'нм':'?')+(exc.lam<380?' УФ':exc.lam>780?' ИК':''), cx, H-4);
          }
        }
        const ex2 = cx+drawR*Math.cos(a), ey2 = cy+drawR*Math.sin(a);
        if (isExc && window.LabFX) {
          LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.arc(ex2,ey2,3.5,0,Math.PI*2); ctx.fillStyle='#FFD166'; ctx.fill(); }, { color:'#FFD166', intensity:10 });
        } else { ctx.beginPath(); ctx.arc(ex2,ey2,2.5,0,Math.PI*2); ctx.fillStyle='#06D6E0'; ctx.fill(); }
      }
    });
    if (!exc || exc.phase === 'done') {
      if (exc && exc.phase === 'done') {
        this._periodG_excState = null;
        if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.9, volume: 0.18 });
      }
      ctx.font = '700 10px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.textAlign = 'center'; ctx.fillText(shells.join(','), cx, H-4);
    }
    ctx.font = '7.5px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.2)';
    ctx.textAlign = 'center'; ctx.fillText('клик -> возбуждение', cx, 10);
  };
})();

/* patch stop() to clean up quantum tooltip (Wave D) */
(function() {
  const _origStopD = PeriodicTableSim.prototype.stop;
  PeriodicTableSim.prototype.stop = function() {
    _origStopD.call(this);
    if (this._periodG_cleanupQTip) { this._periodG_cleanupQTip(); this._periodG_cleanupQTip = null; }
  };
})();

/* ── global opener ─────────────────────────────────────────── */

/* ══════════════════════════════════════════════════════════════
   WAVE C — INTERACTIVE LEARNING MODES
   _buildInteractiveModeBar, _switchInteractiveMode,
   _modeBinary, _binaryClick, _binaryCalc,
   _modeCompare, _compareClick, _compareRefresh, _compareDraw,
   _modeActivity,
   _modeMendeleev1869, _m1869ShowPrediction,
   _modeTimeline, _timelineUpdate
   ══════════════════════════════════════════════════════════════ */

/* patch _buildVisualModes to also init interactive bar */
(function() {
  var _prevBVM = PeriodicTableSim.prototype._buildVisualModes;
  PeriodicTableSim.prototype._buildVisualModes = function() {
    if (_prevBVM) _prevBVM.call(this);
    this._buildInteractiveModeBar();
  };
})();

/* patch stop() to clean up timeline RAF */
(function() {
  var _prevSC = PeriodicTableSim.prototype.stop;
  PeriodicTableSim.prototype.stop = function() {
    if (_prevSC) _prevSC.call(this);
    if (this._iModeState && this._iModeState.raf) {
      cancelAnimationFrame(this._iModeState.raf);
      this._iModeState.raf = null;
    }
  };
})();

PeriodicTableSim.prototype._buildInteractiveModeBar = function() {
  this._interactiveMode = null;
  this._iModePanel = null;
  this._iModeState = {};

  var bar = document.createElement('div');
  bar.className = 'ptbl-imode-bar';
  bar.style.cssText = 'display:flex;align-items:center;gap:5px;padding:5px 12px;background:rgba(0,0,0,0.16);border-bottom:1px solid rgba(255,255,255,0.05);flex-wrap:wrap;flex-shrink:0;';

  var lbl = document.createElement('span');
  lbl.style.cssText = 'font-size:.67rem;font-weight:700;color:rgba(255,255,255,0.28);text-transform:uppercase;letter-spacing:.07em;';
  lbl.textContent = 'Интерактив:';
  bar.appendChild(lbl);

  var MODES = [
    { id: null,        text: 'Стандартный' },
    { id: 'binary',    text: 'Бинарные соединения' },
    { id: 'compare',   text: 'Сравнить' },
    { id: 'activity',  text: 'Ряд активности' },
    { id: 'mendeleev', text: 'Таблица 1869' },
    { id: 'timeline',  text: 'Таймлайн' },
  ];
  var BASE_S = 'padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:#777;font-size:.69rem;cursor:pointer;transition:all .15s;';
  var ACT_S  = 'background:rgba(6,214,224,0.16);color:#06D6E0;border-color:rgba(6,214,224,0.32);';

  var self = this;
  MODES.forEach(function(m) {
    var btn = document.createElement('button');
    btn.textContent = m.text;
    btn.style.cssText = BASE_S + (m.id === null ? ACT_S : '');
    btn.addEventListener('click', function() {
      bar.querySelectorAll('button').forEach(function(b) { b.style.cssText = BASE_S; });
      btn.style.cssText = BASE_S + ACT_S;
      self._switchInteractiveMode(m.id);
    });
    bar.appendChild(btn);
  });

  var vbar = this._wrap.querySelector('#ptbl-vmodes-bar');
  var anchor = vbar ? vbar.nextSibling : (this._wrap.children[1] ? this._wrap.children[1].nextSibling : null);
  this._wrap.insertBefore(bar, anchor);
  this._iModeBar = bar;
};

PeriodicTableSim.prototype._switchInteractiveMode = function(modeId) {
  if (this._iModePanel && this._iModePanel.parentNode) {
    this._iModePanel.parentNode.removeChild(this._iModePanel);
    this._iModePanel = null;
  }
  if (this._iModeState && this._iModeState.raf) {
    cancelAnimationFrame(this._iModeState.raf);
  }
  this._interactiveMode = modeId;
  this._iModeState = {};

  var self = this;
  for (var i = 0; i < ELEMENTS.length; i++) {
    var el = ELEMENTS[i];
    var div = this._cellMap[el.Z];
    if (!div) continue;
    var clone = div.cloneNode(true);
    (function(c, z) {
      c.addEventListener('mouseenter', function() { c.style.filter = 'brightness(1.4)'; c.style.transform = 'scale(1.12)'; c.style.zIndex = '10'; });
      c.addEventListener('mouseleave', function() { c.style.filter = ''; c.style.transform = ''; c.style.zIndex = ''; });
      c.addEventListener('click', function() { self._selectElement(z); });
    })(clone, el.Z);
    if (!div.parentNode) continue;
    div.parentNode.replaceChild(clone, div);
    this._cellMap[el.Z] = clone;
  }
  this._colorTable();

  if (!modeId) return;
  if (modeId === 'binary')    this._modeBinary();
  if (modeId === 'compare')   this._modeCompare();
  if (modeId === 'activity')  this._modeActivity();
  if (modeId === 'mendeleev') this._modeMendeleev1869();
  if (modeId === 'timeline')  this._modeTimeline();
};

/* ── MODE 1: BINARY COMPOUNDS ── */
PeriodicTableSim.prototype._modeBinary = function() {
  this._iModeState = { first: null };
  var panel = document.createElement('div');
  panel.className = 'ptbl-imode-panel';
  panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:9px 14px;flex-shrink:0;font-size:.75rem;color:#ccc;min-height:78px;';
  panel.innerHTML = '<div class="ptbl-bin-hint" style="color:rgba(255,255,255,0.36);font-size:.7rem;">Кликните первый элемент (реагент A)</div>' +
    '<div class="ptbl-bin-result" style="margin-top:6px;"></div>';
  this._wrap.appendChild(panel);
  this._iModePanel = panel;
  var self = this;
  for (var i = 0; i < ELEMENTS.length; i++) {
    var div = this._cellMap[ELEMENTS[i].Z];
    if (!div) continue;
    (function(z) { div.addEventListener('click', function() { self._binaryClick(z); }); })(ELEMENTS[i].Z);
  }
};

PeriodicTableSim.prototype._binaryClick = function(Z) {
  var st = this._iModeState;
  var panel = this._iModePanel;
  if (!panel) return;
  var hint = panel.querySelector('.ptbl-bin-hint');
  var res  = panel.querySelector('.ptbl-bin-result');
  var self = this;

  if (!st.first) {
    st.first = Z;
    if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.0, volume: 0.2 });
    ELEMENTS.forEach(function(el) {
      var d = self._cellMap[el.Z]; if (!d) return;
      d.style.outline = el.Z === Z ? '2px solid #FFD166' : '';
      d.style.outlineOffset = el.Z === Z ? '1px' : '';
    });
    var elA = ELEMENTS.find(function(e) { return e.Z === Z; });
    hint.textContent = 'Выбран: ' + elA.symbol + ' (' + elA.name + '). Кликните второй элемент.';
    res.innerHTML = '';
  } else if (st.first === Z) {
    st.first = null;
    ELEMENTS.forEach(function(el) { var d = self._cellMap[el.Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; } });
    hint.textContent = 'Кликните первый элемент (реагент A)';
    res.innerHTML = '';
  } else {
    if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.3, volume: 0.3 });
    var elA2 = ELEMENTS.find(function(e) { return e.Z === st.first; });
    var elB  = ELEMENTS.find(function(e) { return e.Z === Z; });
    ELEMENTS.forEach(function(el) {
      var d = self._cellMap[el.Z]; if (!d) return;
      if (el.Z === st.first)      { d.style.outline = '2px solid #FFD166'; d.style.outlineOffset = '1px'; }
      else if (el.Z === Z)        { d.style.outline = '2px solid #06D6E0'; d.style.outlineOffset = '1px'; }
      else                        { d.style.outline = ''; d.style.outlineOffset = ''; }
    });
    hint.textContent = elA2.symbol + ' + ' + elB.symbol;
    res.innerHTML = self._binaryCalc(elA2, elB);
    st.first = null;
  }
};

PeriodicTableSim.prototype._binaryCalc = function(elA, elB) {
  function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
  var oxA = (elA.oxStates || []).filter(function(s) { return s !== null && s !== 0; });
  var oxB = (elB.oxStates || []).filter(function(s) { return s !== null && s !== 0; });
  var enA = elA.En || 0, enB = elB.En || 0;
  var dEN = Math.abs(enA - enB);
  var bondType, bondColor;
  if (dEN > 1.7)       { bondType = 'ионная';               bondColor = '#FF6B35'; }
  else if (dEN >= 0.4) { bondType = 'ковалентная полярная'; bondColor = '#7B8EF7'; }
  else                 { bondType = 'ковалентная неполярная'; bondColor = '#7BF5A4'; }

  var seen = {};
  var formulas = [];
  var srcA = oxA.length ? oxA : [0];
  var srcB = oxB.length ? oxB : [0];
  for (var ai = 0; ai < srcA.length; ai++) {
    for (var bi = 0; bi < srcB.length; bi++) {
      var vA = srcA[ai], vB = srcB[bi];
      if (vA === 0 || vB === 0) continue;
      if ((vA > 0 && vB > 0) || (vA < 0 && vB < 0)) continue;
      var posEl = vA > 0 ? elA : elB;
      var negEl = vA > 0 ? elB : elA;
      var posV  = Math.abs(vA > 0 ? vA : vB);
      var negV  = Math.abs(vA > 0 ? vB : vA);
      var g = gcd(posV, negV);
      var nPos = negV / g, nNeg = posV / g;
      var key = posEl.symbol + nPos + negEl.symbol + nNeg;
      if (seen[key]) continue;
      seen[key] = true;
      var formula = posEl.symbol + (nPos > 1 ? '<sub>' + nPos + '</sub>' : '') +
                    negEl.symbol + (nNeg > 1 ? '<sub>' + nNeg + '</sub>' : '');
      var struct  = (nPos === 1 && nNeg === 1) ? posEl.symbol + '–' + negEl.symbol :
                    (nPos === 1 && nNeg === 2)  ? negEl.symbol + '–' + posEl.symbol + '–' + negEl.symbol :
                    formula;
      formulas.push({ formula: formula, struct: struct });
    }
  }
  if (!formulas.length) {
    return '<span style="color:rgba(255,255,255,0.28);font-style:italic;font-size:.7rem;">Соединение не образуется (одинаковые знаки ст. окисления)</span>';
  }
  var html = '<div style="margin-bottom:5px;">' +
    '<span style="color:rgba(255,255,255,0.42);font-size:.69rem;">Тип связи: </span>' +
    '<span style="color:' + bondColor + ';font-weight:700;font-size:.73rem;">' + bondType + '</span>';
  if (elA.En && elB.En) html += '<span style="color:rgba(255,255,255,0.28);font-size:.66rem;margin-left:6px;">ΔЭО = ' + dEN.toFixed(2) + '</span>';
  html += '</div><div style="display:flex;flex-wrap:wrap;gap:5px;">';
  formulas.forEach(function(f) {
    html += '<div style="background:rgba(155,93,229,0.13);border:1px solid rgba(155,93,229,0.36);border-radius:5px;padding:4px 8px;">' +
      '<div style="font-weight:700;color:#fff;font-size:.8rem;">' + f.formula + '</div>' +
      '<div style="font-size:.64rem;color:rgba(255,255,255,0.38);margin-top:1px;">' + f.struct + '</div></div>';
  });
  html += '</div>';
  return html;
};

/* ── MODE 2: COMPARE ELEMENTS ── */
PeriodicTableSim.prototype._modeCompare = function() {
  this._iModeState = { selected: [] };
  var panel = document.createElement('div');
  panel.className = 'ptbl-imode-panel';
  panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;overflow-x:auto;';
  panel.innerHTML = '<div style="display:flex;align-items:center;gap:7px;margin-bottom:6px;flex-wrap:wrap;">' +
    '<span style="color:rgba(255,255,255,0.35);font-size:.7rem;">Выберите до 4 элементов</span>' +
    '<button class="ptbl-cmp-clear" style="padding:2px 8px;border-radius:4px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#777;font-size:.67rem;cursor:pointer;">Очистить</button>' +
    '<span style="font-size:.67rem;color:rgba(255,255,255,0.28);">График:</span>' +
    '<select class="ptbl-cmp-prop" style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.13);color:#ccc;border-radius:4px;padding:2px 4px;font-size:.67rem;">' +
      '<option value="En">ЭО</option><option value="mass">Масса</option>' +
      '<option value="density">Плотность</option><option value="melt">T пл.</option><option value="boil">T кип.</option>' +
    '</select></div>' +
    '<div class="ptbl-cmp-table"></div>' +
    '<canvas class="ptbl-cmp-chart" style="width:100%;height:52px;display:block;margin-top:5px;"></canvas>';
  this._wrap.appendChild(panel);
  this._iModePanel = panel;
  var self = this;
  panel.querySelector('.ptbl-cmp-clear').addEventListener('click', function() {
    self._iModeState.selected = [];
    ELEMENTS.forEach(function(el) { var d = self._cellMap[el.Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; } });
    self._compareRefresh();
  });
  panel.querySelector('.ptbl-cmp-prop').addEventListener('change', function() { self._compareRefresh(); });
  ELEMENTS.forEach(function(el) {
    var div = self._cellMap[el.Z]; if (!div) return;
    (function(z) { div.addEventListener('click', function() { self._compareClick(z); }); })(el.Z);
  });
  this._compareRefresh();
};

PeriodicTableSim.prototype._compareClick = function(Z) {
  var st = this._iModeState;
  var idx = st.selected.indexOf(Z);
  if (idx >= 0) {
    st.selected.splice(idx, 1);
    var d = this._cellMap[Z]; if (d) { d.style.outline = ''; d.style.outlineOffset = ''; }
  } else {
    if (st.selected.length >= 4) return;
    st.selected.push(Z);
    if (window.LabFX) LabFX.sound.play('chime', { pitch: 0.85 + st.selected.length * 0.1, volume: 0.2 });
    var d2 = this._cellMap[Z];
    if (d2) { d2.style.outline = '2px solid #06D6E0'; d2.style.outlineOffset = '1px'; }
  }
  this._compareRefresh();
};

PeriodicTableSim.prototype._compareRefresh = function() {
  var panel = this._iModePanel; if (!panel) return;
  var st = this._iModeState;
  var propKey = panel.querySelector('.ptbl-cmp-prop').value;
  var els = st.selected.map(function(z) { return ELEMENTS.find(function(e) { return e.Z === z; }); }).filter(Boolean);
  var tbl = panel.querySelector('.ptbl-cmp-table');
  if (!els.length) {
    tbl.innerHTML = '<div style="color:rgba(255,255,255,0.2);font-size:.68rem;">Кликайте элементы на таблице</div>';
    this._compareDraw(panel.querySelector('.ptbl-cmp-chart'), els, propKey);
    return;
  }
  var PROPS = [
    { key:'Z', label:'Z' }, { key:'mass', label:'Масса' },
    { key:'config', label:'Конфиг.' }, { key:'En', label:'ЭО (Полинг)' },
    { key:'density', label:'Плотн.' }, { key:'melt', label:'T пл. (K)' }, { key:'boil', label:'T кип. (K)' },
  ];
  var fmt = function(v) { return (v !== null && v !== undefined) ? v : '—'; };
  var html = '<table style="border-collapse:collapse;font-size:.69rem;"><tr><th style="padding:2px 5px;color:rgba(255,255,255,0.32);border-bottom:1px solid rgba(255,255,255,0.07);text-align:left;">Свойство</th>';
  els.forEach(function(el) {
    var col = TYPE_COLORS[el.type] || '#888';
    html += '<th style="padding:2px 5px;color:' + col + ';border-bottom:1px solid rgba(255,255,255,0.07);text-align:center;">' + el.symbol + '<br><span style="font-size:.58rem;opacity:.5">' + el.name + '</span></th>';
  });
  html += '</tr>';
  PROPS.forEach(function(p) {
    var vals = els.map(function(el) { return el[p.key]; });
    var numVals = vals.filter(function(v) { return typeof v === 'number' && v !== null; });
    var maxV = numVals.length > 1 ? Math.max.apply(null, numVals) : null;
    var minV = numVals.length > 1 ? Math.min.apply(null, numVals) : null;
    html += '<tr><td style="padding:2px 5px;color:rgba(255,255,255,0.38);border-bottom:1px solid rgba(255,255,255,0.04);white-space:nowrap;">' + p.label + '</td>';
    vals.forEach(function(v) {
      var ex = '';
      if (maxV !== null && typeof v === 'number') {
        if (v === maxV) ex = 'background:rgba(123,245,164,0.1);color:#7BF5A4;';
        else if (v === minV) ex = 'background:rgba(239,71,111,0.1);color:#EF476F;';
      }
      html += '<td style="padding:2px 5px;text-align:center;border-bottom:1px solid rgba(255,255,255,0.04);' + ex + '">' + fmt(v) + '</td>';
    });
    html += '</tr>';
  });
  html += '</table>';
  tbl.innerHTML = html;
  this._compareDraw(panel.querySelector('.ptbl-cmp-chart'), els, propKey);
};

PeriodicTableSim.prototype._compareDraw = function(canvas, els, propKey) {
  if (!canvas) return;
  var dpr = window.devicePixelRatio || 1;
  var W = canvas.offsetWidth || 300, H = canvas.offsetHeight || 52;
  canvas.width = W * dpr; canvas.height = H * dpr;
  var ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
  if (!els.length) return;
  var vals = els.map(function(e) { return e[propKey]; }).filter(function(v) { return v !== null && v !== undefined && isFinite(v); });
  if (!vals.length) return;
  var minV = Math.min.apply(null, vals), maxV = Math.max.apply(null, vals);
  var pad = { t:4, r:14, b:15, l:5 };
  var gW = W - pad.l - pad.r, gH = H - pad.t - pad.b;
  ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(pad.l, pad.t); ctx.lineTo(pad.l, pad.t+gH); ctx.lineTo(pad.l+gW, pad.t+gH); ctx.stroke();
  var step = els.length > 1 ? gW / (els.length - 1) : gW * 0.5;
  els.forEach(function(el, i) {
    var v = el[propKey];
    if (v === null || v === undefined || !isFinite(v)) return;
    var x = pad.l + i * step;
    var y = pad.t + gH - ((v - minV) / (maxV - minV || 1)) * gH;
    var col = TYPE_COLORS[el.type] || '#7B8EF7';
    ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI*2);
    ctx.fillStyle = col; ctx.fill();
    ctx.strokeStyle = 'rgba(0,0,0,0.4)'; ctx.lineWidth = 1; ctx.stroke();
    ctx.font = '9px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.textAlign = 'center'; ctx.fillText(el.symbol, x, H - 2);
  });
};

/* ── MODE 3: ACTIVITY SERIES ── */
PeriodicTableSim.prototype._modeActivity = function() {
  var SERIES = [
    { s:'Li', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
    { s:'Cs', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
    { s:'Rb', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
    { s:'K',  t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
    { s:'Ba', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
    { s:'Sr', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
    { s:'Ca', t:'Реагирует с H₂O при н.у.; с разб. HCl', g:'active' },
    { s:'Na', t:'Бурно реагирует с H₂O; с разб. HCl — бурно', g:'active' },
    { s:'Mg', t:'Реагирует с горячей H₂O; с разб. HCl', g:'active' },
    { s:'Al', t:'Реагирует с разб. HCl; пассивируется конц. H₂SO₄', g:'active' },
    { s:'Mn', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
    { s:'Zn', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
    { s:'Cr', t:'Реагирует с разб. HCl; пассивируется конц. H₂SO₄', g:'medium' },
    { s:'Fe', t:'Реагирует с разб. HCl и H₂SO₄', g:'medium' },
    { s:'Cd', t:'Реагирует с разб. HCl', g:'medium' },
    { s:'Co', t:'Реагирует с разб. HCl медленно', g:'medium' },
    { s:'Ni', t:'Реагирует с разб. HCl медленно', g:'medium' },
    { s:'Sn', t:'Реагирует с разб. HCl медленно', g:'medium' },
    { s:'Pb', t:'Слабо реагирует с разб. HCl', g:'medium' },
    { s:'H',  t:'Разделитель: металлы левее вытесняют H₂ из кислот', g:'sep' },
    { s:'Sb', t:'Реагирует только с конц. кислотами', g:'low' },
    { s:'Bi', t:'Реагирует только с конц. кислотами', g:'low' },
    { s:'Cu', t:'Не реаг. с HCl; реаг. с конц. H₂SO₄, HNO₃', g:'low' },
    { s:'Hg', t:'Реагирует с конц. HNO₃ и H₂SO₄', g:'low' },
    { s:'Ag', t:'Реагирует с HNO₃', g:'low' },
    { s:'Pd', t:'Реагирует с царской водкой', g:'low' },
    { s:'Pt', t:'Реагирует только с царской водкой', g:'low' },
    { s:'Au', t:'Реагирует только с царской водкой', g:'low' },
  ];
  var GC = {
    active:{ bg:'rgba(239,71,111,0.1)', bd:'rgba(239,71,111,0.36)', c:'#EF476F', lbl:'Активные (H₂O)' },
    medium:{ bg:'rgba(255,209,102,0.1)', bd:'rgba(255,209,102,0.36)', c:'#FFD166', lbl:'Средние (разб. кислоты)' },
    low:   { bg:'rgba(123,142,247,0.1)', bd:'rgba(123,142,247,0.36)', c:'#7B8EF7', lbl:'Малоакт. (конц. / цар. водка)' },
  };
  var panel = document.createElement('div');
  panel.className = 'ptbl-imode-panel';
  panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;overflow-x:auto;';
  var legHtml = '<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:6px;">';
  ['active','medium','low'].forEach(function(g) {
    legHtml += '<div style="display:flex;align-items:center;gap:3px;font-size:.65rem;color:' + GC[g].c + ';">' +
      '<span style="width:8px;height:8px;border-radius:2px;background:' + GC[g].bd + ';display:inline-block;"></span>' + GC[g].lbl + '</div>';
  });
  legHtml += '</div>';
  var rowHtml = '<div style="display:flex;align-items:center;gap:2px;flex-wrap:wrap;">';
  SERIES.forEach(function(item, i) {
    if (item.g === 'sep') {
      rowHtml += '<div style="display:flex;flex-direction:column;align-items:center;margin:0 3px;">' +
        '<div style="width:1px;height:28px;background:rgba(255,255,255,0.18);"></div>' +
        '<span style="font-size:.58rem;color:rgba(255,255,255,0.35);margin-top:1px;">H</span></div>';
      return;
    }
    if (i > 0 && SERIES[i-1].g !== 'sep') rowHtml += '<div style="color:rgba(255,255,255,0.15);font-size:.72rem;align-self:center;">›</div>';
    var gc = GC[item.g];
    var el = ELEMENTS.find(function(e) { return e.symbol === item.s; });
    var name = el ? el.name : item.s;
    rowHtml += '<div class="ptbl-act-item" data-sym="' + item.s + '" title="' + item.t + '" ' +
      'style="min-width:24px;padding:3px 4px;border-radius:4px;background:' + gc.bg + ';border:1px solid ' + gc.bd + ';text-align:center;cursor:pointer;transition:all .12s;">' +
      '<div style="font-size:.74rem;font-weight:800;color:' + gc.c + ';line-height:1.1;">' + item.s + '</div>' +
      '<div style="font-size:.5rem;color:rgba(255,255,255,0.32);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:34px;">' + name + '</div></div>';
  });
  rowHtml += '</div>';
  panel.innerHTML = legHtml + rowHtml + '<div class="ptbl-act-tip" style="margin-top:5px;font-size:.68rem;color:rgba(255,255,255,0.42);min-height:14px;"></div>';
  this._wrap.appendChild(panel);
  this._iModePanel = panel;
  var self = this;
  panel.querySelectorAll('.ptbl-act-item').forEach(function(item) {
    item.addEventListener('mouseenter', function() {
      panel.querySelector('.ptbl-act-tip').textContent = item.title;
      item.style.filter = 'brightness(1.5)'; item.style.transform = 'scale(1.08)';
      var el = ELEMENTS.find(function(e) { return e.symbol === item.dataset.sym; });
      if (el && self._cellMap[el.Z]) { self._cellMap[el.Z].style.outline = '2px solid #FFD166'; self._cellMap[el.Z].style.outlineOffset = '1px'; }
    });
    item.addEventListener('mouseleave', function() {
      panel.querySelector('.ptbl-act-tip').textContent = '';
      item.style.filter = ''; item.style.transform = '';
      var el = ELEMENTS.find(function(e) { return e.symbol === item.dataset.sym; });
      if (el && self._cellMap[el.Z]) { self._cellMap[el.Z].style.outline = ''; self._cellMap[el.Z].style.outlineOffset = ''; }
    });
  });
};

/* ── MODE 4: MENDELEEV 1869 ── */
PeriodicTableSim.prototype._modeMendeleev1869 = function() {
  var KNOWN_1869 = new Set([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,
    22,23,24,25,26,27,28,29,30,33,34,35,36,37,38,40,41,42,
    44,45,47,48,50,51,52,53,55,56,57,60,62,64,65,66,68,70,72,74,76,78,79,80,82,83]);
  var PREDS = {
    31: { name:'Экаалюминий (Ga)', pred:{ mass:68, density:5.9, melt:302 }, act:{ mass:69.72, density:5.91, melt:303 }, year:1875, who:'Лекок де Буабодран (Франция)' },
    21: { name:'Экабор (Sc)',      pred:{ mass:44, density:3.5, melt:null }, act:{ mass:44.96, density:2.99, melt:1814 }, year:1879, who:'Ларс Нильсон (Швеция)' },
    32: { name:'Экасилиций (Ge)', pred:{ mass:72, density:5.5, melt:null }, act:{ mass:72.63, density:5.32, melt:1211 }, year:1886, who:'Клеменс Винклер (Германия)' },
    43: { name:'Экамарганец (Tc)', pred:{ mass:100, density:null, melt:null }, act:{ mass:98, density:11.5, melt:2430 }, year:1937, who:'Перье и Сегре (Италия)' },
  };
  var panel = document.createElement('div');
  panel.className = 'ptbl-imode-panel';
  panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;position:relative;min-height:38px;';
  panel.innerHTML = '<div style="color:rgba(255,255,255,0.32);font-size:.68rem;margin-bottom:5px;">' +
    'Таблица Менделеева 1869: 63 известных элемента. Фиол. «?» — предсказания Менделеева. Кликните «?».' +
    '</div>' +
    '<div class="ptbl-m1869-popup" style="display:none;position:absolute;top:8px;right:8px;width:230px;background:#1a1a2e;border:1px solid rgba(155,93,229,0.48);border-radius:8px;padding:10px;font-size:.7rem;z-index:20;box-shadow:0 4px 16px rgba(0,0,0,0.6);"></div>';
  this._wrap.appendChild(panel);
  this._iModePanel = panel;
  var self = this;
  ELEMENTS.forEach(function(el) {
    var div = self._cellMap[el.Z]; if (!div) return;
    if (KNOWN_1869.has(el.Z)) {
      div.style.opacity = '1';
    } else if (PREDS[el.Z]) {
      div.style.background = 'rgba(155,93,229,0.09)';
      div.style.border = '1px dashed rgba(155,93,229,0.48)';
      div.style.opacity = '1';
      div.innerHTML = '<span style="font-size:.76em;font-weight:900;color:rgba(155,93,229,0.88);">?</span>' +
        '<span style="font-size:.5em;color:rgba(155,93,229,0.5);margin-top:1px;">' + el.symbol + '</span>';
      div.title = el.name + ' — предсказан Менделеевым (кликните)';
      (function(z) { div.addEventListener('click', function() { self._m1869ShowPrediction(z, PREDS[z], panel); }); })(el.Z);
    } else {
      div.style.opacity = '0.13';
      div.style.background = 'rgba(255,255,255,0.02)';
      div.style.border = '1px solid rgba(255,255,255,0.05)';
    }
  });
};

PeriodicTableSim.prototype._m1869ShowPrediction = function(Z, pred, panel) {
  if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.15, volume: 0.3 });
  var popup = panel.querySelector('.ptbl-m1869-popup');
  var fmt = function(v) { return (v !== null && v !== undefined) ? v : '—'; };
  var diff = function(p, a) {
    if (p == null || a == null || !isFinite(+p) || !isFinite(+a) || +a === 0) return '';
    var pct = (Math.abs(+p - +a) / +a * 100).toFixed(1);
    var good = Math.abs(+p - +a) / +a < 0.05;
    return '<span style="color:' + (good ? '#7BF5A4' : '#FFD166') + ';font-size:.62rem;margin-left:3px;">(' + (good ? 'точно' : pct + '% откл.') + ')</span>';
  };
  popup.innerHTML = '<div style="font-weight:700;color:#9B5DE5;margin-bottom:5px;">' + pred.name + '</div>' +
    '<table style="width:100%;border-collapse:collapse;font-size:.68rem;">' +
    '<tr><th style="text-align:left;color:rgba(255,255,255,0.32);padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Свойство</th>' +
        '<th style="text-align:center;color:#FFD166;padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Предсказано</th>' +
        '<th style="text-align:center;color:#7BF5A4;padding:2px 3px;border-bottom:1px solid rgba(255,255,255,0.07);">Реально</th></tr>' +
    '<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">Масса</td>' +
        '<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.mass) + '</td>' +
        '<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.mass) + diff(pred.pred.mass, pred.act.mass) + '</td></tr>' +
    '<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">Плотность</td>' +
        '<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.density) + '</td>' +
        '<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.density) + diff(pred.pred.density, pred.act.density) + '</td></tr>' +
    '<tr><td style="padding:2px 3px;color:rgba(255,255,255,0.42);">T пл. (K)</td>' +
        '<td style="text-align:center;padding:2px 3px;">' + fmt(pred.pred.melt) + '</td>' +
        '<td style="text-align:center;padding:2px 3px;">' + fmt(pred.act.melt) + '</td></tr>' +
    '</table>' +
    '<div style="margin-top:5px;font-size:.64rem;color:rgba(255,255,255,0.32);">Открыт: ' + pred.year + ' г., ' + pred.who + '</div>' +
    '<button onclick="this.closest(&quot;.ptbl-m1869-popup&quot;).style.display=&quot;none&quot;" ' +
      'style="margin-top:5px;padding:2px 7px;border-radius:4px;border:1px solid rgba(255,255,255,0.15);background:transparent;color:#777;font-size:.65rem;cursor:pointer;">Закрыть</button>';
  popup.style.display = 'block';
};

/* ── MODE 5: TIMELINE ── */
PeriodicTableSim.prototype._modeTimeline = function() {
  var MIN_Y = 1660, MAX_Y = 2024;
  this._iModeState = { year: MAX_Y, playing: false, raf: null };
  var panel = document.createElement('div');
  panel.className = 'ptbl-imode-panel';
  panel.style.cssText = 'background:rgba(0,0,0,0.28);border-top:1px solid rgba(255,255,255,0.06);padding:8px 14px;flex-shrink:0;';
  panel.innerHTML = '<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;">' +
    '<span style="font-size:.68rem;color:rgba(255,255,255,0.32);">Год:</span>' +
    '<input type="range" class="ptbl-tl-slider" min="' + MIN_Y + '" max="' + MAX_Y + '" value="' + MAX_Y + '" style="flex:1;min-width:90px;accent-color:#9B5DE5;">' +
    '<span class="ptbl-tl-year" style="font-size:.8rem;font-weight:700;color:#9B5DE5;min-width:34px;">' + MAX_Y + '</span>' +
    '<button class="ptbl-tl-play" style="padding:3px 8px;border-radius:5px;border:1px solid rgba(155,93,229,0.36);background:rgba(155,93,229,0.12);color:#9B5DE5;font-size:.68rem;cursor:pointer;display:inline-flex;align-items:center;gap:3px;">' +
      '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто' +
    '</button>' +
    '<span class="ptbl-tl-count" style="font-size:.68rem;color:rgba(255,255,255,0.32);">Открыто 0 / 118</span>' +
    '</div>' +
    '<div class="ptbl-tl-info" style="font-size:.68rem;color:#FFD166;margin-top:3px;min-height:13px;"></div>';
  this._wrap.appendChild(panel);
  this._iModePanel = panel;
  var self = this;
  var slider   = panel.querySelector('.ptbl-tl-slider');
  var yearLbl  = panel.querySelector('.ptbl-tl-year');
  var playBtn  = panel.querySelector('.ptbl-tl-play');
  var info     = panel.querySelector('.ptbl-tl-info');
  var countLbl = panel.querySelector('.ptbl-tl-count');
  var update = function() {
    var y = +slider.value;
    self._iModeState.year = y;
    yearLbl.textContent = y;
    self._timelineUpdate(y, info, countLbl);
  };
  slider.addEventListener('input', update);
  update();
  playBtn.addEventListener('click', function() {
    var st = self._iModeState;
    if (st.playing) {
      st.playing = false;
      cancelAnimationFrame(st.raf);
      playBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто';
    } else {
      if (+slider.value >= MAX_Y) slider.value = MIN_Y;
      st.playing = true;
      playBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>Стоп';
      var last = null;
      var tick = function(ts) {
        if (!st.playing) return;
        if (!last) last = ts;
        if (ts - last > 38) {
          var cur = +slider.value;
          if (cur >= MAX_Y) {
            st.playing = false;
            playBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:9px;height:9px;fill:#9B5DE5;stroke:none;"><polygon points="5,3 19,12 5,21"/></svg>Авто';
            return;
          }
          slider.value = cur + 2;
          update();
          last = ts;
        }
        st.raf = requestAnimationFrame(tick);
      };
      st.raf = requestAnimationFrame(tick);
    }
  });
};

PeriodicTableSim.prototype._timelineUpdate = function(year, info, countLbl) {
  var count = 0, lastEl = null, lastYear = -Infinity;
  ELEMENTS.forEach(function(el) {
    var div = this._cellMap[el.Z]; if (!div) return;
    var known = (el.discovered === null) || (el.discovered <= year);
    if (known) {
      count++;
      div.style.opacity = '1';
      var col = TYPE_COLORS[el.type] || '#555';
      div.style.background = col + '44';
      div.style.border = '1px solid ' + col + '88';
      div.style.outline = ''; div.style.outlineOffset = '';
      if (el.discovered !== null && el.discovered <= year && el.discovered > lastYear) {
        lastYear = el.discovered; lastEl = el;
      }
    } else {
      div.style.opacity = '0.09';
      div.style.background = 'rgba(255,255,255,0.015)';
      div.style.border = '1px solid rgba(255,255,255,0.04)';
      div.style.outline = ''; div.style.outlineOffset = '';
    }
  }, this);
  if (countLbl) countLbl.textContent = 'Открыто ' + count + ' / 118';
  if (lastEl && lastYear > -Infinity) {
    var d = this._cellMap[lastEl.Z];
    if (d) { d.style.outline = '2px solid #FFD166'; d.style.outlineOffset = '1px'; }
    if (info) info.textContent = lastYear + ' г.: ' + lastEl.name + ' (' + lastEl.symbol + ') — ' + (lastEl.by || '?');
  } else if (info) {
    info.textContent = '';
  }
};

var periodicSim = null;

function _openPeriodic() {
  document.getElementById('sim-periodic').style.display = 'flex';
  if (!periodicSim) {
    periodicSim = new PeriodicTableSim(document.getElementById('periodic-wrap'));
  }
}
