<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://www.pandoraburo.ru/" xml:lang="ru">
  <id>https://www.pandoraburo.ru/prover-nuzhen-li-rynku-vash-novyy-staryy-produkt-ili-uslugi</id>
  <link href="https://www.pandoraburo.ru/prover-nuzhen-li-rynku-vash-novyy-staryy-produkt-ili-uslugi.xml" rel="self"/>
  <title>Проверь! Нужен ли рынку ваш новый (старый) продукт или услуги</title>
  <subtitle type="html">
&lt;html lang="ru"&gt;
&lt;head&gt;
&lt;meta charset="UTF-8"&gt;
&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
&lt;title&gt;Гипотеза — проверьте идею до того, как в неё вложены деньги&lt;/title&gt;


&lt;script defer="defer" src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js"&gt;&lt;/script&gt;
&lt;script defer="defer" src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js"&gt;&lt;/script&gt;
&lt;script defer="defer" src="https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js"&gt;&lt;/script&gt;

&lt;style&gt;
/* ═══════════════ TAILWIND (скомпилированный, только используемые классы) ═══════════════ */
*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}
html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif}
body{margin:0;line-height:inherit}
h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit;margin:0}
a{color:inherit;text-decoration:inherit}
button,input,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}
button{background-color:transparent;background-image:none;cursor:pointer}
input,textarea,select{background-color:transparent}
ul,ol{list-style:none;margin:0;padding:0}
table{border-collapse:collapse;border-color:inherit;text-indent:0}
img,svg,video{display:block;max-width:100%;height:auto}
[hidden]{display:none}

.bg-slate-950{background-color:#020617}
.bg-slate-900{background-color:#0f172a}
.bg-slate-800{background-color:#1e293b}
.bg-orange-500{background-color:#f97316}
.bg-orange-950\/30{background-color:rgba(67,20,7,.3)}
.bg-orange-950\/40{background-color:rgba(67,20,7,.4)}
.bg-red-950\/30{background-color:rgba(69,10,10,.3)}
.bg-red-950\/40{background-color:rgba(69,10,10,.4)}
.bg-green-500{background-color:#22c55e}
.bg-white{background-color:#fff}

.text-slate-100{color:#f1f5f9}
.text-slate-300{color:#cbd5e1}
.text-slate-400{color:#94a3b8}
.text-slate-500{color:#64748b}
.text-slate-950{color:#020617}
.text-orange-400{color:#fb923c}
.text-orange-700{color:#c2410c}
.text-red-400{color:#f87171}
.text-green-400{color:#4ade80}
.text-white{color:#fff}

.border{border-width:1px}
.border-b{border-bottom-width:1px}
.border-2{border-width:2px}
.border-l-2{border-left-width:2px}
.border-dashed{border-style:dashed}
.border-slate-800{border-color:#1e293b}
.border-slate-700{border-color:#334155}
.border-slate-600{border-color:#475569}
.border-slate-900{border-color:#0f172a}
.border-orange-500{border-color:#f97316}
.border-orange-700{border-color:#c2410c}
.border-red-900{border-color:#7f1d1d}

.rounded{border-radius:.25rem}
.rounded-lg{border-radius:.5rem}
.rounded-full{border-radius:9999px}

.max-w-xl{max-width:36rem}
.max-w-2xl{max-width:42rem}
.max-w-3xl{max-width:48rem}
.max-w-4xl{max-width:56rem}
.max-w-5xl{max-width:64rem}
.max-w-md{max-width:28rem}

.mx-auto{margin-left:auto;margin-right:auto}
.my-6{margin-top:1.5rem;margin-bottom:1.5rem}
.mb-1{margin-bottom:.25rem}
.mb-2{margin-bottom:.5rem}
.mb-3{margin-bottom:.75rem}
.mb-4{margin-bottom:1rem}
.mb-5{margin-bottom:1.25rem}
.mb-6{margin-bottom:1.5rem}
.mb-8{margin-bottom:2rem}
.mb-10{margin-bottom:2.5rem}
.mb-12{margin-bottom:3rem}
.mt-1{margin-top:.25rem}
.mt-2{margin-top:.5rem}
.mt-3{margin-top:.75rem}
.mt-4{margin-top:1rem}
.mt-6{margin-top:1.5rem}
.mt-8{margin-top:2rem}
.mt-10{margin-top:2.5rem}
.ml-3{margin-left:.75rem}

.p-2{padding:.5rem}
.p-3{padding:.75rem}
.p-4{padding:1rem}
.p-5{padding:1.25rem}
.p-6{padding:1.5rem}
.px-3{padding-left:.75rem;padding-right:.75rem}
.px-6{padding-left:1.5rem;padding-right:1.5rem}
.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}
.py-2{padding-top:.5rem;padding-bottom:.5rem}
.py-3{padding-top:.75rem;padding-bottom:.75rem}
.py-4{padding-top:1rem;padding-bottom:1rem}
.py-10{padding-top:2.5rem;padding-bottom:2.5rem}
.py-12{padding-top:3rem;padding-bottom:3rem}
.py-16{padding-top:4rem;padding-bottom:4rem}
.pt-2{padding-top:.5rem}

.flex{display:flex}
.inline-flex{display:inline-flex}
.grid{display:grid}
.block{display:block}
.hidden{display:none}
.inline-block{display:inline-block}

.flex-1{flex:1 1 0%}
.flex-wrap{flex-wrap:wrap}
.flex-col{flex-direction:column}
.items-start{align-items:flex-start}
.items-center{align-items:center}
.justify-between{justify-content:space-between}
.gap-1{gap:.25rem}
.gap-2{gap:.5rem}
.gap-3{gap:.75rem}
.gap-4{gap:1rem}
.gap-5{gap:1.25rem}
.gap-6{gap:1.5rem}
.gap-8{gap:2rem}
.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}
.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}

.w-full{width:100%}
.w-24{width:6rem}
.h-1{height:.25rem}
.h-20{height:5rem}
.h-24{height:6rem}
.h-40{height:10rem}
.min-h-screen{min-height:100vh}
.min-w-\[140px\]{min-width:140px}
.max-h-\[400px\]{max-height:400px}
.overflow-x-auto{overflow-x:auto}
.overflow-hidden{overflow:hidden}
.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.whitespace-pre-wrap{white-space:pre-wrap}

.text-xs{font-size:.75rem;line-height:1rem}
.text-sm{font-size:.875rem;line-height:1.25rem}
.text-base{font-size:1rem;line-height:1.5rem}
.text-lg{font-size:1.125rem;line-height:1.75rem}
.text-xl{font-size:1.25rem;line-height:1.75rem}
.text-2xl{font-size:1.5rem;line-height:2rem}
.text-3xl{font-size:1.875rem;line-height:2.25rem}
.text-5xl{font-size:3rem;line-height:1}
.font-medium{font-weight:500}
.font-semibold{font-weight:600}
.font-bold{font-weight:700}
.text-left{text-align:left}
.text-center{text-align:center}
.text-right{text-align:right}
.italic{font-style:italic}
.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.tracking-widest{letter-spacing:.1em}
.leading-tight{line-height:1.25}
.uppercase{text-transform:uppercase}
.line-clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}

.relative{position:relative}
.absolute{position:absolute}
.fixed{position:fixed}
.sticky{position:sticky}
.-top-3{top:-.75rem}
.left-6{left:1.5rem}
.top-0{top:0}
.right-0{right:0}
.inset-0{top:0;right:0;bottom:0;left:0}

.z-10{z-index:10}
.z-50{z-index:50}

.cursor-pointer{cursor:pointer}
.cursor-not-allowed{cursor:not-allowed}
.select-none{user-select:none}
.transition{transition-property:color,background-color,border-color,opacity,transform;transition-duration:.15s}
.transition-colors{transition-property:color,background-color,border-color;transition-duration:.15s}
.hover\:bg-orange-400:hover{background-color:#fb923c}
.hover\:border-slate-500:hover{border-color:#64748b}
.hover\:border-slate-600:hover{border-color:#475569}
.hover\:text-slate-100:hover{color:#f1f5f9}
.hover\:text-red-400:hover{color:#f87171}
.hover\:underline:hover{text-decoration:underline}
.disabled\:opacity-50:disabled{opacity:.5}
.opacity-80{opacity:.8}

.space-y-1&gt;*+*{margin-top:.25rem}
.space-y-2&gt;*+*{margin-top:.5rem}
.space-y-3&gt;*+*{margin-top:.75rem}
.space-y-4&gt;*+*{margin-top:1rem}
.space-y-5&gt;*+*{margin-top:1.25rem}
.list-disc{list-style-type:disc}
.list-inside{list-style-position:inside}

/* Кастомные стили продукта */
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.fade{animation:fade .35s ease-out}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.bar{transition:width .6s cubic-bezier(.2,.8,.2,1)}
input[type=range]{accent-color:#f97316;width:100%}
input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:#0f172a}
::-webkit-scrollbar-thumb{background:#1e293b;border-radius:4px}
.preset-card:hover{border-color:#f97316 !important}
.drop-active{border-color:#f97316 !important;background:rgba(249,115,22,.06)}
.chip{display:inline-block;font-size:11px;padding:2px 8px;border-radius:999px}
.chip-gost{background:#7c2d12;color:#fed7aa}
.chip-cert{background:#14532d;color:#bbf7d0}
.chip-temp{background:#1e3a8a;color:#bfdbfe}
.chip-class{background:#581c87;color:#e9d5ff}
.chip-mech{background:#713f12;color:#fef08a}
.chip-tech{background:#164e63;color:#a5f3fc}

@media (min-width:768px){
  .md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
  .md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}
  .md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}
}

@media print{
  header,nav,button,.no-print,a[href^="#/"]{display:none !important}
  body{background:#fff !important;color:#000 !important}
  main{max-width:100% !important;padding:0 !important}
  .border,.border-b,.border-l-2{border-color:#ccc !important}
  .bg-slate-900,.bg-slate-950,.bg-slate-800{background:#fff !important}
  .text-slate-400,.text-slate-500,.text-slate-300{color:#444 !important}
  .text-orange-400{color:#c2410c !important}
  .bar{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  h1,h2{page-break-after:avoid}
  ul,table{page-break-inside:avoid}
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body class="bg-slate-950 text-slate-100 min-h-screen"&gt;
&lt;div id="app" class="fade"&gt;&lt;/div&gt;

&lt;script&gt;
/* ═══════════════════════════════════════════════════════════════════════════
   ГИПОТЕЗА v2.3 · fast-load · без Play CDN
   Single-file · localStorage · детерминированный движок
   ═══════════════════════════════════════════════════════════════════════════ */

// ─── КОНТАКТЫ И МЕТАДАННЫЕ ─────────────────────────────────────────────────
const PRODUCT = {
  email: 'arsenal-detal@list.ru',
  phone: '+7 902 873-36-90',
  phoneRaw: '+79028733690',
  contactName: 'Макс',
  price: 499,
  priceCurrency: '₽',
  trialDays: 30,
  paymentAvailableFrom: 2027,
  currentYear: new Date().getFullYear(),
  paymentNotYetAvailable: new Date().getFullYear() &lt; 2027,
};

// ─── PDF.JS WORKER ─────────────────────────────────────────────────────────
window.addEventListener('load', function() {
  if (window.pdfjsLib) {
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
  }
});

// ─── 1. ХРАНИЛИЩЕ ──────────────────────────────────────────────────────────
const LS = {
  get(k, fallback) { try { return JSON.parse(localStorage.getItem('gip_' + k)) ?? fallback; } catch { return fallback; } },
  set(k, v) { localStorage.setItem('gip_' + k, JSON.stringify(v)); },
};
const DB = {
  get users() { return LS.get('users', []); }, set users(v) { LS.set('users', v); },
  get session() { return LS.get('session', null); }, set session(v) { LS.set('session', v); },
  get panels() { return LS.get('panels', null); }, set panels(v) { LS.set('panels', v); },
  get runs() { return LS.get('runs', []); }, set runs(v) { LS.set('runs', v); },
  get calibrations() { return LS.get('calibrations', []); }, set calibrations(v) { LS.set('calibrations', v); },
};
const b64 = (s) =&gt; btoa(unescape(encodeURIComponent(s)));

// ─── 2. SEED ───────────────────────────────────────────────────────────────
const TASK_TYPES = [
  { code:'physical_product', title:'Физические товары и материалы', prompt:'Оценивай материал с точки зрения инженера и снабженца.' },
  { code:'equipment',        title:'Оборудование и промышленные системы', prompt:'Учитывай сервис, сроки поставки, обучение персонала.' },
  { code:'b2b_service',      title:'B2B-услуги', prompt:'Смотри на SLA, доверие к исполнителю, аутсорс.' },
  { code:'infrastructure',   title:'Инфраструктурные проекты', prompt:'Ключевое — регламенты, протоколы, согласования.' },
  { code:'strategy',         title:'Стратегические вопросы', prompt:'Оценивай риск, окупаемость, готовность менять процессы.' },
];

const VARIANTS = ['техническая','маркетинговая','эмоциональная','экономическая'];

const BUILTIN_PANELS = [
  { id:'p_armor', code:'armor_steel', is_builtin:true, is_visible:true,
    title:'Броневые стали и защитные конструкции',
    description:'Производители бронеавтомобилей, бронедверей, взрывозащитных панелей.',
    segments:{ 'инкассация':0.3, 'VIP':0.2, 'инфраструктура':0.3, 'нефтегаз':0.2 },
    roles:{
      'инкассация':['главный конструктор','технолог','снабженец'],
      'VIP':['главный конструктор','инженер-технолог'],
      'инфраструктура':['проектировщик','инженер по безопасности'],
      'нефтегаз':['главный механик','инженер по надёжности'],
    },
    sources:['ВЦИОМ','Росстат','НИИ Стали (открытые публикации)','Таможенная статистика'],
    objections:[
      'Нет сертификата по Бр3+.',
      'Не знаем производителя, нет референсов.',
      '540 МПа — мало для ударных нагрузок.',
      'Боимся, что при гибке потеряется класс защиты.',
      'Нужен ГОСТ Р 51112, иначе не пройдёт экспертиза.',
    ],
  },
  { id:'p_mining', code:'mining_equipment', is_builtin:true, is_visible:true,
    title:'Оборудование для нефтегаза и горнодобычи',
    description:'Футеровка, дробилки, бункеры, защитные ограждения.',
    segments:{ 'нефтегаз':0.5, 'горнодобыча':0.5 },
    roles:{
      'нефтегаз':['главный механик','инженер по надёжности'],
      'горнодобыча':['главный механик','технолог обогащения'],
    },
    sources:['Отраслевые обзоры','Росстат (добыча)','Тендеры на футеровку'],
    objections:[
      'RAEX проверен годами, зачем менять?',
      '540 МПа — это конструкционная сталь, а не износостойкая.',
      'Нужны протоколы на абразивный износ.',
      'Импортное дешевле при больших объёмах.',
    ],
  },
  { id:'p_infra', code:'infrastructure', is_builtin:true, is_visible:true,
    title:'Инфраструктура и взрывозащита',
    description:'Защита ТЭС, подстанций, АЗС, транспортных узлов.',
    segments:{ 'проектировщики':0.5, 'инженеры по безопасности':0.5 },
    roles:{
      'проектировщики':['проектировщик','главный инженер проекта'],
      'инженеры по безопасности':['инженер по безопасности','эксперт'],
    },
    sources:['Регламенты по антитеррористической защищённости','Тендеры ТЭК'],
    objections:[
      'Без сертификата по ГОСТ Р 51112 не пропустит экспертиза.',
      'С2 — это только осколки, а нам нужна защита от пуль.',
      'Нет протоколов на взрывостойкость.',
      'Проще купить готовую сертифицированную панель.',
    ],
  },
  { id:'p_saas', code:'it_saas', is_builtin:true, is_visible:false,
    title:'ИТ и SaaS',
    description:'B2B-софт, платформы, интеграции.',
    segments:{ 'cto':0.5, 'product_owner':0.5 },
    roles:{ 'cto':['технический директор'], 'product_owner':['продакт-менеджер'] },
    sources:['Открытые опросы','Публикации VC.ru'],
    objections:['У нас уже есть решение.','Нужна интеграция с нашим стеком.','Слишком дорого.'],
  },
];

const PRESETS = [
  { id:'preset_armor_s2', title:'Испытания С2 + мороз −70 °C', subtitle:'Броневые стали · для ТЗ и протоколов',
    panel_code:'armor_steel', task_type:'physical_product',
    text:'Производители бронеавтомобилей для инкассации готовы принять ТУ, где класс С2 подтверждается пробитием шариком при +20 °C, если дополнительно приложены протоколы на ударную вязкость KCU при −40 °C и −70 °C, даже без полноценных испытаний на взрывостойкость.',
    scenarios:[{label:'base',price_mult:1.00,variant:'техническая'},{label:'с полным ГОСТ',price_mult:1.00,variant:'техническая'},{label:'price +15%',price_mult:1.15,variant:'техническая'}] },
  { id:'preset_armor_energy', title:'Энергопоглощение vs твёрдость', subtitle:'Броневые стали · формулировка КП',
    panel_code:'armor_steel', task_type:'physical_product',
    text:'Главные конструкторы производителей бронеавтомобилей выберут БТВТ 450 вместо закалённой брони, если в КП сделан акцент на энергопоглощение и отсутствие споллинга, а не на твёрдость по Бринеллю — даже при том, что по твёрдости БТВТ проигрывает.',
    scenarios:[{label:'энергопоглощение',price_mult:1.00,variant:'техническая'},{label:'твёрдость',price_mult:1.00,variant:'техническая'},{label:'маркетинг',price_mult:1.00,variant:'маркетинговая'},{label:'экономика',price_mult:1.00,variant:'экономическая'}] },
  { id:'preset_armor_price', title:'Ценовая эластичность: ×0.85 / ×1.15', subtitle:'Броневые стали · подбор цены',
    panel_code:'armor_steel', task_type:'physical_product',
    text:'Производители бронеавтомобилей для инкассации готовы платить на 15% больше за БТВТ 450 с сертификатом Бр1–Бр2 и протоколами на −70 °C, если это снимает необходимость в кевларовом подбое и сокращает сборку.',
    scenarios:[{label:'price -15%',price_mult:0.85,variant:'техническая'},{label:'base',price_mult:1.00,variant:'техническая'},{label:'price +15%',price_mult:1.15,variant:'техническая'},{label:'price +30%',price_mult:1.30,variant:'техническая'}] },
  { id:'preset_armor_combo', title:'Комбо: БТВТ + С-500 для Бр3+', subtitle:'Броневые стали · защита от Бр3',
    panel_code:'armor_steel', task_type:'physical_product',
    text:'Производители бронеконструкций готовы использовать комбинацию БТВТ 450 как несущий слой и закалённой стали С-500 локально в зонах Бр3–Бр4, если это снижает стоимость изделия на 20–30% по сравнению с полностью закалённой бронёй.',
    scenarios:[{label:'только БТВТ',price_mult:1.00,variant:'техническая'},{label:'БТВТ + С-500',price_mult:1.00,variant:'техническая'},{label:'комбо -20%',price_mult:0.80,variant:'экономическая'}] },
  { id:'preset_mining_hardening', title:'Новые режимы закалки износостойких сталей', subtitle:'Оборудование · закалочные прессы',
    panel_code:'mining_equipment', task_type:'equipment',
    text:'Главные механики обогатительных фабрик готовы тестировать новые режимы закалки износостойких сталей (прерывистая закалка, отпуск 250–300 °C), если это даёт +20–30% к ресурсу футеровки, но требует замены поставщика стали и пересмотра ТУ.',
    scenarios:[{label:'base',price_mult:1.00,variant:'техническая'},{label:'resource +30%',price_mult:1.00,variant:'техническая'},{label:'TCO расчёт',price_mult:1.00,variant:'экономическая'}] },
  { id:'preset_mining_raex', title:'Замена RAEX на БТВТ 450', subtitle:'Оборудование · футеровка',
    panel_code:'mining_equipment', task_type:'physical_product',
    text:'Главные механики горнодобывающих предприятий готовы заменить RAEX 450 и Hardox 500 на БТВТ 450 для футеровки бункеров и дробилок, если материал в 2 раза дешевле, сваривается без подогрева, гнётся и сохраняет ударную вязкость при −70 °C.',
    scenarios:[{label:'base',price_mult:1.00,variant:'техническая'},{label:'price -50%',price_mult:0.50,variant:'техническая'},{label:'price -30%',price_mult:0.70,variant:'техническая'},{label:'экономика',price_mult:1.00,variant:'экономическая'}] },
];

function ensureSeed() {
  if (!DB.panels) DB.panels = BUILTIN_PANELS;
  const cur = DB.panels;
  for (const bp of BUILTIN_PANELS) {
    if (!cur.find(p =&gt; p.code === bp.code)) cur.push(bp);
  }
  DB.panels = cur;
}

// ─── 3. ДОКУМЕНТЫ ──────────────────────────────────────────────────────────
async function parseFile(file) {
  const name = file.name;
  const lower = name.toLowerCase();
  const sizeKB = Math.round(file.size / 1024);
  try {
    let text = '';
    if (lower.endsWith('.pdf')) {
      if (!window.pdfjsLib) return { name, sizeKB, error: 'PDF-парсер не загружен. Обновите страницу.' };
      text = await parsePDF(file);
    } else if (lower.endsWith('.docx')) {
      if (!window.mammoth) return { name, sizeKB, error: 'DOCX-парсер не загружен. Обновите страницу.' };
      text = await parseDOCX(file);
    } else if (lower.endsWith('.txt') || lower.endsWith('.md')) {
      text = await file.text();
    } else if (lower.endsWith('.doc')) {
      return { name, sizeKB, error: 'Формат .doc не поддерживается. Сохраните как .docx или PDF.' };
    } else {
      return { name, sizeKB, error: 'Неподдерживаемый формат. Принимаются PDF, DOCX, TXT, MD.' };
    }
    const clean = (text || '').replace(/\s+/g, ' ').trim();
    if (!clean) return { name, sizeKB, error: 'Не удалось извлечь текст (возможно, скан без OCR).' };
    const signals = extractSignals(clean);
    return { name, sizeKB, text: clean, preview: clean.slice(0, 400), signals };
  } catch (e) {
    return { name, sizeKB, error: 'Ошибка разбора: ' + (e.message || 'неизвестная') };
  }
}

async function parsePDF(file) {
  const buf = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument({ data: buf }).promise;
  let text = '';
  for (let i = 1; i &lt;= pdf.numPages; i++) {
    const page = await pdf.getPage(i);
    const content = await page.getTextContent();
    text += content.items.map(x =&gt; x.str).join(' ') + '\n';
  }
  return text;
}

async function parseDOCX(file) {
  const buf = await file.arrayBuffer();
  const res = await mammoth.extractRawText({ arrayBuffer: buf });
  return res.value;
}

function extractSignals(text) {
  const t = text || '';
  const gost = t.match(/ГОСТ\s*(?:Р\s*)?[\d\.\-–]+(?:\s*[-–]\s*\d{2,4})?/gi) || [];
  const tu = t.match(/ТУ\s*[\d\.\-–ВВ\s]+/gi) || [];
  const brClass = t.match(/(?:Бр|С)[1-6](?:\s*[-–]\s*[А-ЯA-Z])?/gi) || [];
  const mpa = t.match(/\d{3,4}\s*МПа/gi) || [];
  const temp = t.match(/[−\-–]?\d{1,3}\s*(?:°\s*)?C(?:ельсия)?/gi) || [];
  const hb = t.match(/HB\s*\d{2,4}/gi) || [];
  const kcu = t.match(/KCU\s*[\d\.\/]+/gi) || [];

  const signals = {
    hasGOST: gost.length &gt; 0, hasTU: tu.length &gt; 0,
    hasCert: /сертификат|декларац[ия]/i.test(t),
    hasProtokol: /протокол\s+испытан|акт\s+испытан|испытани[йя]/i.test(t),
    hasMoroz: /[−\-–]\s*(?:40|50|60|70)\s*°?\s*C/i.test(t) || /минус\s*(?:40|50|60|70)/i.test(t),
    hasBr: brClass.length &gt; 0, hasMPa: mpa.length &gt; 0, hasHB: hb.length &gt; 0, hasKCU: kcu.length &gt; 0,
    hasWeld: /сварк|сварива|электрод|Св08|сварн/i.test(t),
    hasBend: /гибк|гибка|штампов|вальцов|гнут/i.test(t),
    hasSpll: /споллинг|тыльн[ые]?\s+осколк|запреградн/i.test(t),
    hasExpl: /взрыво|СВУ|ударн[ая]\s+волн|детонац/i.test(t),
    hasAbrasion: /абразив|износ|футеровк|стирани/i.test(t),
    raw: { gost: gost.slice(0,5), tu: tu.slice(0,5), brClass: brClass.slice(0,6), mpa: mpa.slice(0,3), temp: temp.slice(0,6), hb: hb.slice(0,3), kcu: kcu.slice(0,3) },
  };

  const chips = [];
  signals.raw.gost.forEach(x =&gt; chips.push({ cls:'chip-gost', text:x }));
  signals.raw.tu.forEach(x =&gt; chips.push({ cls:'chip-gost', text:x }));
  signals.raw.brClass.forEach(x =&gt; chips.push({ cls:'chip-class', text:x }));
  signals.raw.mpa.forEach(x =&gt; chips.push({ cls:'chip-mech', text:x }));
  signals.raw.hb.forEach(x =&gt; chips.push({ cls:'chip-mech', text:x }));
  signals.raw.kcu.forEach(x =&gt; chips.push({ cls:'chip-mech', text:x }));
  signals.raw.temp.forEach(x =&gt; chips.push({ cls:'chip-temp', text:x }));
  if (signals.hasCert) chips.push({ cls:'chip-cert', text:'сертификат' });
  if (signals.hasProtokol) chips.push({ cls:'chip-cert', text:'протокол' });
  if (signals.hasWeld) chips.push({ cls:'chip-tech', text:'сварка' });
  if (signals.hasBend) chips.push({ cls:'chip-tech', text:'гибка' });
  if (signals.hasSpll) chips.push({ cls:'chip-tech', text:'споллинг' });
  if (signals.hasExpl) chips.push({ cls:'chip-tech', text:'взрыв' });
  if (signals.hasAbrasion) chips.push({ cls:'chip-tech', text:'абразив' });

  const seen = new Set();
  signals.chips = chips.filter(c =&gt; { const k = c.cls + '|' + c.text; if (seen.has(k)) return false; seen.add(k); return true; }).slice(0, 14);
  return signals;
}

function mergeDocSignals(docs) {
  const merged = {
    hasGOST:false, hasTU:false, hasCert:false, hasProtokol:false, hasMoroz:false,
    hasBr:false, hasMPa:false, hasHB:false, hasKCU:false,
    hasWeld:false, hasBend:false, hasSpll:false, hasExpl:false, hasAbrasion:false,
    chips: [], docCount: 0, docNames: [], docHash: '',
  };
  if (!docs || !docs.length) return merged;
  const allChips = [];
  let combinedText = '';
  docs.forEach(d =&gt; {
    if (!d.signals) return;
    merged.docCount++;
    merged.docNames.push(d.name);
    combinedText += (d.text || '') + '\n';
    for (const k of ['hasGOST','hasTU','hasCert','hasProtokol','hasMoroz','hasBr','hasMPa','hasHB','hasKCU','hasWeld','hasBend','hasSpll','hasExpl','hasAbrasion']) {
      if (d.signals[k]) merged[k] = true;
    }
    allChips.push(...(d.signals.chips || []));
  });
  const seen = new Set();
  merged.chips = allChips.filter(c =&gt; { const k = c.cls + '|' + c.text; if (seen.has(k)) return false; seen.add(k); return true; }).slice(0, 16);
  merged.docHash = String(hash32(combinedText.slice(0, 4000)));
  return merged;
}

function adaptObjections(baseObjections, signals) {
  let objs = [...baseObjections];
  const drop = (pattern) =&gt; { objs = objs.filter(o =&gt; !pattern.test(o)); };

  if (signals.hasCert) {
    drop(/нет сертификата|не пройдёт экспертиз|без сертификата/i);
    objs.push('Сертификат есть — но какая лаборатория выдала и на какой класс?');
  }
  if (signals.hasProtokol) {
    drop(/нет протокол/i);
    objs.push('Покажите образец протокола — как он применим к нашей конструкции?');
  }
  if (signals.hasGOST) {
    drop(/нужен гост|без гост/i);
    objs.push('ГОСТ — это базовый уровень. Нужен ли вам класс выше?');
  }
  if (signals.hasMoroz) objs.push('Как подтверждаете ударную вязкость при −70 °C в серийной партии?');
  if (signals.hasBend) {
    drop(/гибк.*класс.*защит/i);
    objs.push('Есть протокол на гибку минимального радиуса?');
  }
  if (signals.hasWeld) objs.push('Какая погонная энергия сварки без потери свойств?');
  if (signals.hasSpll) objs.push('Подтвердите запреградное воздействие по методике — кевлар всё равно нужен?');
  if (signals.hasAbrasion) objs.push('Нужны протоколы на абразивный износ при ваших режимах.');
  return objs;
}

// ─── 4. ДВИЖОК ─────────────────────────────────────────────────────────────
function hash32(str) {
  let h = 5381;
  for (let i = 0; i &lt; str.length; i++) h = ((h &lt;&lt; 5) + h + str.charCodeAt(i)) | 0;
  return h &gt;&gt;&gt; 0;
}
function mulberry32(seed) {
  return function() {
    seed = (seed + 0x6D2B79F5) | 0;
    let t = Math.imul(seed ^ (seed &gt;&gt;&gt; 15), 1 | seed);
    t = (t + Math.imul(t ^ (t &gt;&gt;&gt; 7), 61 | t)) ^ t;
    return ((t ^ (t &gt;&gt;&gt; 14)) &gt;&gt;&gt; 0) / 4294967296;
  };
}
function pick(arr, rnd) { return arr[Math.floor(rnd() * arr.length)]; }

function syntheticRespondent({ segment, role, industry, objections, hypothesis, scenario, taskPrompt, docHash, docBonus }) {
  const seed = hash32(`${segment}|${role}|${scenario}|${hypothesis.slice(0,120)}|${taskPrompt.slice(0,80)}|${docHash || ''}`);
  const rnd = mulberry32(seed);
  let appeal = 3.5 + rnd() * 6.0;
  let wtp = 0.2 + rnd() * 0.7;
  appeal = Math.min(10, appeal + (docBonus || 0));
  wtp = Math.min(0.95, wtp + (docBonus || 0) * 0.04);
  const objection = objections.length ? pick(objections, rnd) : 'Нет данных';
  const prefix = ['Честно — ','Скажу так: ','Если коротко — ','Смотрю на это так: '][Math.floor(rnd()*4)];
  const quote = `${prefix}«${objection.replace(/\.$/,'')}, но если покажете протоколы — готовы к пилоту».`;
  return { segment, role, appeal: +appeal.toFixed(1), wtp: +wtp.toFixed(2), objection, quote };
}

function allocate(total, weights) {
  const s = weights.reduce((a,b) =&gt; a+b, 0) || 1;
  const raw = weights.map(w =&gt; total * w / s);
  const out = raw.map(x =&gt; Math.floor(x));
  let rem = total - out.reduce((a,b) =&gt; a+b, 0);
  for (let i = 0; i &lt; rem; i++) out[i % out.length]++;
  return out;
}

function runSynthetic(panel, hypothesis, size, taskType, scenario, docSignals) {
  const signals = docSignals || { docHash: '', hasCert:false, hasProtokol:false, hasGOST:false, hasMoroz:false, hasBend:false };
  const adaptedObjections = adaptObjections(panel.objections || [], signals);
  let docBonus = 0;
  if (signals.hasCert) docBonus += 0.15;
  if (signals.hasProtokol) docBonus += 0.15;
  if (signals.hasGOST) docBonus += 0.05;
  if (signals.hasMoroz) docBonus += 0.05;

  const segments = Object.keys(panel.segments || {});
  const weights = Object.values(panel.segments || {});
  const counts = allocate(size, weights.length ? weights : [1]);
  const rows = [];
  segments.forEach((seg, i) =&gt; {
    const roles = (panel.roles &amp;&amp; panel.roles[seg]) || ['инженер'];
    for (let k = 0; k &lt; counts[i]; k++) {
      rows.push(syntheticRespondent({
        segment: seg, role: roles[k % roles.length], industry: panel.title,
        objections: adaptedObjections, hypothesis,
        scenario: scenario || 'base',
        taskPrompt: taskType?.prompt || '',
        docHash: signals.docHash, docBonus,
      }));
    }
  });
  return { rows, adaptedObjections };
}

// ─── 5. СКОРИНГ ────────────────────────────────────────────────────────────
function mean(arr) { return arr.reduce((a,b) =&gt; a+b, 0) / (arr.length || 1); }

function segmentConfidence(n, calibrations) {
  const cals = calibrations || [];
  if (cals.length) {
    const deltas = cals.map(c =&gt; Math.abs(c.delta));
    const mae = mean(deltas);
    if (mae &lt; 0.5 &amp;&amp; n &gt;= 30) return 'высокий';
    if (mae &lt; 1.0 &amp;&amp; n &gt;= 20) return 'средний';
    return 'низкий';
  }
  if (n &gt;= 50) return 'средний';
  return 'низкий';
}
function overallConfidence(segConf, n) {
  const order = { 'высокий':3, 'средний':2, 'низкий':1 };
  const vals = Object.values(segConf);
  if (!vals.length) return n &lt; 30 ? 'низкая' : 'средняя';
  const worst = vals.reduce((a,b) =&gt; order[a] &lt; order[b] ? a : b);
  return { 'высокий':'повышенная', 'средний':'средняя', 'низкий':'низкая' }[worst];
}

function buildReport(rows, hypothesis, taskType, calibrations, docSignals) {
  if (!rows.length) return { error: 'no responses' };
  const appeal = mean(rows.map(r =&gt; r.appeal));
  const wtp = mean(rows.map(r =&gt; r.wtp));
  const total = rows.length;

  const counts = {};
  rows.forEach(r =&gt; { counts[r.objection] = (counts[r.objection] || 0) + 1; });
  const topObjections = Object.entries(counts)
    .sort((a,b) =&gt; b[1] - a[1]).slice(0, 5)
    .map(([text, cnt]) =&gt; ({ text, count: cnt, share: +(cnt / total).toFixed(2) }));

  const bySeg = {};
  rows.forEach(r =&gt; { (bySeg[r.segment] ||= []).push(r.appeal); });
  const segmentScores = {};
  const segmentConf = {};
  Object.entries(bySeg).forEach(([seg, vals]) =&gt; {
    segmentScores[seg] = +mean(vals).toFixed(1);
    segmentConf[seg] = segmentConfidence(vals.length, calibrations);
  });

  const seen = new Set();
  const quotes = [];
  [...rows].sort((a,b) =&gt; b.quote.length - a.quote.length).forEach(r =&gt; {
    if (!seen.has(r.quote) &amp;&amp; quotes.length &lt; 5) { seen.add(r.quote); quotes.push(r.quote); }
  });

  return {
    hypothesis,
    task_type: taskType?.code || 'physical_product',
    task_type_title: taskType?.title || '',
    appeal_index: +appeal.toFixed(1),
    wtp: +wtp.toFixed(2),
    respondents: total,
    top_objections: topObjections,
    segment_scores: segmentScores,
    segment_confidence: segmentConf,
    confidence: overallConfidence(segmentConf, total),
    quotes,
    recommendations: recommend(topObjections, segmentScores, docSignals),
    documents_used: docSignals?.docCount || 0,
    documents: docSignals?.docNames || [],
    document_signals: docSignals?.chips || [],
    disclaimer: 'Предварительная оценка на синтетической панели. Уверенность по сегментам рассчитана с учётом калибровок. Результат требует валидации на реальных респондентах.',
  };
}

function recommend(objections, segments, docSignals) {
  const recs = [];
  for (const o of objections.slice(0, 3)) {
    const t = o.text.toLowerCase();
    if (t.includes('сертификат') || t.includes('гост')) recs.push('Подготовить копии сертификатов и протоколов испытаний.');
    else if (t.includes('производител') || t.includes('референс')) recs.push('Собрать 2–3 референса внедрений для снятия барьера доверия.');
    else if (t.includes('бр3') || t.includes('класс')) recs.push('Рассмотреть комбинированную схему с усилением до Бр3+.');
    else if (t.includes('мп') || t.includes('прочност') || t.includes('ударн')) recs.push('Добавить в ТУ протоколы на ударную вязкость и энергопоглощение.');
    else if (t.includes('цен')) recs.push('Протестировать сценарии с ценой 85% и 115%.');
    else if (t.includes('образец протокола')) recs.push('Подготовить образец протокола с методикой и лабораторией.');
    else if (t.includes('−70') || t.includes('мороз')) recs.push('Добавить в КП акцент на протокол −70 °C.');
    else if (t.includes('гибк')) recs.push('Приложить протокол на гибку минимального радиуса.');
    else if (t.includes('сварк')) recs.push('Указать рекомендуемую погонную энергию сварки.');
    else recs.push(`Отработать возражение: «${o.text}»`);
  }
  if (docSignals &amp;&amp; docSignals.docCount &gt; 0) recs.push(`Учтено документов: ${docSignals.docCount}. Возражения адаптированы под содержимое.`);
  const entries = Object.entries(segments);
  if (entries.length) {
    const [bestSeg, bestScore] = entries.reduce((a,b) =&gt; a[1] &gt;= b[1] ? a : b);
    recs.push(`Фокус на сегменте «${bestSeg}» — наивысший отклик (${bestScore}/10).`);
  }
  return recs;
}

function runMatrix(panel, hypothesis, size, taskType, scenarios, calibrations, docSignals) {
  const { rows: baseRows } = runSynthetic(panel, hypothesis, size, taskType, 'base', docSignals);
  const base = buildReport(baseRows, hypothesis, taskType, calibrations, docSignals);
  const matrix = (scenarios || []).map(sc =&gt; {
    const scenarioText = `${sc.variant} | цена ×${sc.price_mult}`;
    const { rows } = runSynthetic(panel, hypothesis, Math.max(30, Math.floor(size/2)), taskType, scenarioText, docSignals);
    const rep = buildReport(rows, hypothesis, taskType, calibrations, docSignals);
    return {
      label: sc.label, price_mult: sc.price_mult, variant: sc.variant,
      appeal_index: rep.appeal_index, wtp: rep.wtp,
      top_objection: rep.top_objections[0]?.text || '',
      segment_scores: rep.segment_scores,
    };
  });
  return { base, matrix };
}

// ─── 6. ПОДПИСКА ───────────────────────────────────────────────────────────
function getSubscription(user) {
  if (!user) return null;
  const now = Date.now();
  const startedAt = user.trial_started_at || now;
  const trialEndsAt = startedAt + PRODUCT.trialDays * 24 * 60 * 60 * 1000;
  const inTrial = now &lt; trialEndsAt;
  return {
    startedAt, trialEndsAt, inTrial,
    daysLeft: Math.max(0, Math.ceil((trialEndsAt - now) / (24 * 60 * 60 * 1000))),
    paymentAvailable: !PRODUCT.paymentNotYetAvailable,
    currentYear: PRODUCT.currentYear,
    availableFrom: PRODUCT.paymentAvailableFrom,
  };
}

function ensureTrialStarted(email) {
  const users = DB.users;
  const idx = users.findIndex(u =&gt; u.email === email);
  if (idx &gt;= 0 &amp;&amp; !users[idx].trial_started_at) {
    users[idx].trial_started_at = Date.now();
    DB.users = users;
  }
}

// ─── 7. РОУТИНГ ────────────────────────────────────────────────────────────
const VIEWS = {};
function nav(path) { location.hash = path; }
function currentPath() { return location.hash.replace(/^#/, '') || '/'; }

window.addEventListener('hashchange', render);
window.addEventListener('DOMContentLoaded', () =&gt; { ensureSeed(); render(); });

function render() {
  const app = document.getElementById('app');
  const path = currentPath();
  const [view, ...rest] = path.split('/').filter(Boolean);
  app.innerHTML = '';
  app.className = 'fade';
  app.appendChild(header());
  const main = document.createElement('main');
  main.className = 'max-w-5xl mx-auto px-6 py-10';
  app.appendChild(main);
  const route = (VIEWS[view] || VIEWS.home);
  route(main, rest);
}

function header() {
  const h = document.createElement('header');
  h.className = 'border-b border-slate-800';
  const user = DB.session;
  h.innerHTML = `
    &lt;div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between flex-wrap gap-3"&gt;
      &lt;a href="#/" class="font-bold tracking-widest text-lg"&gt;ГИПОТЕЗА&lt;/a&gt;
      &lt;nav class="text-sm text-slate-400 flex gap-5 items-center flex-wrap"&gt;
        &lt;a href="#/presets" class="hover:text-slate-100"&gt;Пресеты&lt;/a&gt;
        &lt;a href="#/dashboard" class="hover:text-slate-100"&gt;Кабинет&lt;/a&gt;
        &lt;a href="#/run" class="hover:text-slate-100"&gt;Прогон&lt;/a&gt;
        &lt;a href="#/panels/new" class="hover:text-slate-100"&gt;Новая панель&lt;/a&gt;
        &lt;a href="#/pricing" class="hover:text-slate-100"&gt;Подписка&lt;/a&gt;
        &lt;a href="#/finance" class="hover:text-slate-100"&gt;Финансы&lt;/a&gt;
        ${user
          ? `&lt;span class="text-slate-500"&gt;·&lt;/span&gt;&lt;span class="text-orange-400"&gt;${user.email}&lt;/span&gt;
             &lt;button id="logout" class="hover:text-slate-100"&gt;Выйти&lt;/button&gt;`
          : `&lt;a href="#/login" class="hover:text-slate-100"&gt;Войти&lt;/a&gt;`}
      &lt;/nav&gt;
    &lt;/div&gt;`;
  h.querySelector('#logout')?.addEventListener('click', () =&gt; {
    DB.session = null; nav('/'); render();
  });
  return h;
}

// ─── 8. HOME ───────────────────────────────────────────────────────────────
VIEWS.home = (root) =&gt; {
  const panels = DB.panels.filter(p =&gt; p.is_visible);
  root.innerHTML = `
    &lt;h1 class="text-5xl font-bold mb-3 leading-tight"&gt;Проверьте идею&lt;br&gt;до того, как в неё вложены деньги.&lt;/h1&gt;
    &lt;p class="text-slate-400 mb-10 max-w-2xl"&gt;
      Платформа гибридной валидации для B2B: синтетика для скорости, отраслевые панели для релевантности,
      калибровка на реальных людях для точности. Загрузка ТУ и протоколов, PPTX-экспорт, финансовая модель.
    &lt;/p&gt;
    &lt;div class="flex gap-3 mb-12 flex-wrap"&gt;
      &lt;a href="#/presets" class="px-6 py-3 bg-orange-500 rounded-lg font-semibold text-slate-950 hover:bg-orange-400"&gt;Готовые пресеты&lt;/a&gt;
      &lt;a href="#/run" class="px-6 py-3 border border-slate-700 rounded-lg font-semibold hover:border-slate-500"&gt;Свой прогон&lt;/a&gt;
      &lt;a href="#/pricing" class="px-6 py-3 border border-slate-700 rounded-lg font-semibold hover:border-slate-500"&gt;Подписка&lt;/a&gt;
    &lt;/div&gt;

    &lt;h2 class="text-xl font-semibold mb-4"&gt;Отраслевые панели&lt;/h2&gt;
    &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-12"&gt;
      ${panels.map(p =&gt; `
        &lt;a href="#/run" class="block border border-slate-800 rounded-lg p-5 hover:border-slate-600 transition"&gt;
          &lt;div class="font-semibold mb-1"&gt;${p.title}&lt;/div&gt;
          &lt;div class="text-sm text-slate-400 mb-3"&gt;${p.description}&lt;/div&gt;
          &lt;div class="text-xs text-slate-500"&gt;Сегментов: ${Object.keys(p.segments || {}).length} · Источников: ${(p.sources||[]).length}&lt;/div&gt;
        &lt;/a&gt;
      `).join('')}
    &lt;/div&gt;

    &lt;div class="grid grid-cols-1 md:grid-cols-4 gap-4"&gt;
      &lt;div class="border border-slate-800 rounded-lg p-5"&gt;
        &lt;div class="text-orange-400 text-sm mb-1"&gt;1 · Синтетика&lt;/div&gt;
        &lt;div class="font-semibold mb-1"&gt;Быстрый сигнал&lt;/div&gt;
        &lt;div class="text-sm text-slate-400"&gt;8–500 респондентов за секунды.&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="border border-slate-800 rounded-lg p-5"&gt;
        &lt;div class="text-orange-400 text-sm mb-1"&gt;2 · Документы&lt;/div&gt;
        &lt;div class="font-semibold mb-1"&gt;ТУ и протоколы&lt;/div&gt;
        &lt;div class="text-sm text-slate-400"&gt;PDF/DOCX → сигналы → адаптация возражений.&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="border border-slate-800 rounded-lg p-5"&gt;
        &lt;div class="text-orange-400 text-sm mb-1"&gt;3 · Калибровка&lt;/div&gt;
        &lt;div class="font-semibold mb-1"&gt;3–5 живых людей&lt;/div&gt;
        &lt;div class="text-sm text-slate-400"&gt;Сравниваем с синтетикой по отрасли.&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="border border-slate-800 rounded-lg p-5"&gt;
        &lt;div class="text-orange-400 text-sm mb-1"&gt;4 · Матрица&lt;/div&gt;
        &lt;div class="font-semibold mb-1"&gt;Сценарии&lt;/div&gt;
        &lt;div class="text-sm text-slate-400"&gt;Цена × подача. PPTX-экспорт.&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  `;
};

// ─── 9. PRICING ────────────────────────────────────────────────────────────
VIEWS.pricing = (root) =&gt; {
  const user = DB.session ? DB.users.find(u =&gt; u.email === DB.session.email) : null;
  const sub = user ? getSubscription(user) : null;

  root.innerHTML = `
    &lt;h1 class="text-3xl font-bold mb-3"&gt;Подписка&lt;/h1&gt;
    &lt;p class="text-slate-400 mb-8 max-w-2xl"&gt;
      «Гипотеза» — платформа для валидации B2B-гипотез. Первый месяц бесплатный,
      далее ${PRODUCT.price} ${PRODUCT.priceCurrency}/мес. Сейчас — полностью бесплатно.
    &lt;/p&gt;

    ${PRODUCT.paymentNotYetAvailable ? `
      &lt;div class="border border-orange-700 bg-orange-950/40 rounded-lg p-5 mb-10"&gt;
        &lt;div class="flex gap-3 items-start"&gt;
          &lt;div class="text-2xl"&gt;⏳&lt;/div&gt;
          &lt;div&gt;
            &lt;div class="font-semibold mb-1"&gt;Оплата появится в ${PRODUCT.paymentAvailableFrom} году&lt;/div&gt;
            &lt;div class="text-sm text-slate-300"&gt;
              Интеграция с ЮKassa (Яндекс) находится в разработке. До этого момента платформа работает
              &lt;b&gt;полностью бесплатно&lt;/b&gt;, без ограничений на прогоны, документы и экспорт.
              Когда оплата будет подключена — вы получите уведомление, и у вас будет ${PRODUCT.trialDays} дней бесплатного периода,
              чтобы решить, нужна ли подписка.
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    ` : ''}

    ${sub ? `
      &lt;div class="border border-slate-800 rounded-lg p-5 mb-10"&gt;
        &lt;div class="text-slate-400 text-sm mb-2"&gt;Ваш статус&lt;/div&gt;
        ${sub.inTrial
          ? `&lt;div class="text-lg font-semibold"&gt;Пробный период — осталось &lt;span class="text-orange-400"&gt;${sub.daysLeft} дн.&lt;/span&gt;&lt;/div&gt;
             &lt;div class="text-sm text-slate-400 mt-1"&gt;Начало: ${new Date(sub.startedAt).toLocaleDateString('ru-RU')} · окончание: ${new Date(sub.trialEndsAt).toLocaleDateString('ru-RU')}&lt;/div&gt;`
          : `&lt;div class="text-lg font-semibold text-orange-400"&gt;Пробный период завершён&lt;/div&gt;
             &lt;div class="text-sm text-slate-400 mt-1"&gt;${PRODUCT.paymentNotYetAvailable ? 'Платформа работает бесплатно до ' + PRODUCT.paymentAvailableFrom + ' года.' : 'Оформите подписку, чтобы продолжить.'}&lt;/div&gt;`}
      &lt;/div&gt;
    ` : ''}

    &lt;div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-10"&gt;
      &lt;div class="border-2 border-orange-500 rounded-lg p-6 relative"&gt;
        &lt;div class="absolute -top-3 left-6 bg-orange-500 text-slate-950 text-xs font-bold px-3 py-1 rounded-full"&gt;СЕЙЧАС ДОСТУПНО&lt;/div&gt;
        &lt;div class="text-2xl font-bold mb-1"&gt;Бесплатно&lt;/div&gt;
        &lt;div class="text-slate-400 text-sm mb-5"&gt;Полный доступ до ${PRODUCT.paymentAvailableFrom}&lt;/div&gt;
        &lt;ul class="space-y-2 text-sm mb-6"&gt;
          &lt;li&gt;✓ Неограниченные прогоны гипотез&lt;/li&gt;
          &lt;li&gt;✓ Все отраслевые панели&lt;/li&gt;
          &lt;li&gt;✓ Загрузка ТУ и протоколов (PDF/DOCX)&lt;/li&gt;
          &lt;li&gt;✓ Матрица сценариев&lt;/li&gt;
          &lt;li&gt;✓ Экспорт PDF, PPTX, JSON&lt;/li&gt;
          &lt;li&gt;✓ Калибровка на реальных респондентах&lt;/li&gt;
          &lt;li&gt;✓ Финансовая модель&lt;/li&gt;
        &lt;/ul&gt;
        &lt;a href="#/run" class="block w-full text-center py-3 bg-orange-500 rounded-lg font-semibold text-slate-950 hover:bg-orange-400"&gt;Начать прогон&lt;/a&gt;
      &lt;/div&gt;

      &lt;div class="border border-slate-800 rounded-lg p-6 opacity-80 relative"&gt;
        &lt;div class="absolute -top-3 left-6 bg-slate-800 text-slate-300 text-xs font-bold px-3 py-1 rounded-full"&gt;${PRODUCT.paymentAvailableFrom}&lt;/div&gt;
        &lt;div class="text-2xl font-bold mb-1"&gt;Pro&lt;/div&gt;
        &lt;div class="text-slate-400 text-sm mb-5"&gt;
          ${PRODUCT.price} ${PRODUCT.priceCurrency}/мес после ${PRODUCT.trialDays}-дневного пробного периода
        &lt;/div&gt;
        &lt;ul class="space-y-2 text-sm mb-6"&gt;
          &lt;li&gt;✓ Всё, что в бесплатном тарифе&lt;/li&gt;
          &lt;li&gt;✓ Приоритетная обработка документов&lt;/li&gt;
          &lt;li&gt;✓ Расширенные отраслевые панели&lt;/li&gt;
          &lt;li&gt;✓ Гибридная калибровка (реальные люди)&lt;/li&gt;
          &lt;li&gt;✓ Техподдержка&lt;/li&gt;
        &lt;/ul&gt;
        &lt;button disabled class="block w-full text-center py-3 border border-slate-700 rounded-lg font-semibold text-slate-500 cursor-not-allowed"&gt;
          Недоступно до ${PRODUCT.paymentAvailableFrom}
        &lt;/button&gt;
        &lt;div class="text-xs text-slate-500 mt-3 text-center"&gt;Интеграция с ЮKassa (Яндекс) — в разработке&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="border border-slate-800 rounded-lg p-6"&gt;
      &lt;h2 class="text-lg font-semibold mb-3"&gt;Как это будет работать&lt;/h2&gt;
      &lt;ol class="space-y-3 text-sm text-slate-300"&gt;
        &lt;li&gt;&lt;b class="text-orange-400"&gt;1.&lt;/b&gt; Вы регистрируетесь и пользуетесь платформой бесплатно — сейчас и до ${PRODUCT.paymentAvailableFrom} года.&lt;/li&gt;
        &lt;li&gt;&lt;b class="text-orange-400"&gt;2.&lt;/b&gt; Когда оплата станет доступна, вы получите уведомление в кабинете.&lt;/li&gt;
        &lt;li&gt;&lt;b class="text-orange-400"&gt;3.&lt;/b&gt; Первые ${PRODUCT.trialDays} дней после подключения оплаты — бесплатно, без списаний.&lt;/li&gt;
        &lt;li&gt;&lt;b class="text-orange-400"&gt;4.&lt;/b&gt; После пробного периода — ${PRODUCT.price} ${PRODUCT.priceCurrency}/мес. Отменить можно в любой момент.&lt;/li&gt;
      &lt;/ol&gt;
    &lt;/div&gt;

    &lt;div class="border border-slate-800 rounded-lg p-6 mt-10"&gt;
      &lt;h2 class="text-lg font-semibold mb-3"&gt;Связаться&lt;/h2&gt;
      &lt;div class="text-sm text-slate-300 space-y-2"&gt;
        &lt;div&gt;Почта: &lt;a href="mailto:${PRODUCT.email}" class="text-orange-400 hover:underline"&gt;${PRODUCT.email}&lt;/a&gt;&lt;/div&gt;
        &lt;div&gt;Телефон: &lt;a href="tel:${PRODUCT.phoneRaw}" class="text-orange-400 hover:underline"&gt;${PRODUCT.phone}&lt;/a&gt; (${PRODUCT.contactName})&lt;/div&gt;
        &lt;div class="text-slate-500 text-xs mt-3"&gt;По вопросам подписки, пилотов и корпоративных внедрений — пишите или звоните.&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  `;
};

// ─── 10. PRESETS ───────────────────────────────────────────────────────────
VIEWS.presets = (root) =&gt; {
  root.innerHTML = `
    &lt;h1 class="text-2xl font-bold mb-2"&gt;Готовые пресеты гипотез&lt;/h1&gt;
    &lt;p class="text-slate-400 mb-8"&gt;Нажмите на карточку — поля прогона заполнятся автоматически.&lt;/p&gt;
    &lt;div class="grid grid-cols-1 md:grid-cols-2 gap-4"&gt;
      ${PRESETS.map(p =&gt; `
        &lt;button data-preset="${p.id}" class="preset-card text-left border border-slate-800 rounded-lg p-5 transition"&gt;
          &lt;div class="text-xs text-orange-400 mb-2"&gt;${p.subtitle}&lt;/div&gt;
          &lt;div class="font-semibold mb-2"&gt;${p.title}&lt;/div&gt;
          &lt;div class="text-sm text-slate-400 mb-3"&gt;${p.text.slice(0, 180)}…&lt;/div&gt;
          &lt;div class="text-xs text-slate-500"&gt;${p.scenarios.length} сценариев · ${p.task_type}&lt;/div&gt;
        &lt;/button&gt;
      `).join('')}
    &lt;/div&gt;
  `;
  root.querySelectorAll('[data-preset]').forEach(btn =&gt; {
    btn.addEventListener('click', () =&gt; {
      const preset = PRESETS.find(p =&gt; p.id === btn.dataset.preset);
      sessionStorage.setItem('gip_preset', preset.id);
      nav('/run'); render();
    });
  });
};

// ─── 11. LOGIN / REGISTER ──────────────────────────────────────────────────
VIEWS.login = (root) =&gt; {
  root.innerHTML = `
    &lt;div class="max-w-md mx-auto"&gt;
      &lt;h1 class="text-2xl font-bold mb-6"&gt;Вход&lt;/h1&gt;
      &lt;form id="f" class="space-y-4"&gt;
        &lt;input name="email" class="w-full p-3 bg-slate-900 border border-slate-800 rounded" placeholder="Email"&gt;
        &lt;input name="password" type="password" class="w-full p-3 bg-slate-900 border border-slate-800 rounded" placeholder="Пароль"&gt;
        &lt;div id="err" class="text-red-400 text-sm hidden"&gt;&lt;/div&gt;
        &lt;button class="w-full py-3 bg-orange-500 rounded font-semibold text-slate-950"&gt;Войти&lt;/button&gt;
      &lt;/form&gt;
      &lt;p class="text-sm text-slate-400 mt-6"&gt;Нет аккаунта? &lt;a href="#/register" class="text-orange-400"&gt;Регистрация&lt;/a&gt;&lt;/p&gt;
      &lt;p class="text-xs text-slate-500 mt-3"&gt;Демо: demo@gipoteza.ru / demo1234&lt;/p&gt;
    &lt;/div&gt;`;
  root.querySelector('#f').addEventListener('submit', (e) =&gt; {
    e.preventDefault();
    const fd = new FormData(e.target);
    const pw = b64(fd.get('password'));
    const u = DB.users.find(x =&gt; x.email === fd.get('email') &amp;&amp; x.password === pw);
    if (!u) { const err = root.querySelector('#err'); err.textContent = 'Неверный email или пароль'; err.classList.remove('hidden'); return; }
    DB.session = { email: u.email, company: u.company };
    ensureTrialStarted(u.email);
    nav('/dashboard'); render();
  });
};

VIEWS.register = (root) =&gt; {
  root.innerHTML = `
    &lt;div class="max-w-md mx-auto"&gt;
      &lt;h1 class="text-2xl font-bold mb-6"&gt;Регистрация&lt;/h1&gt;
      &lt;form id="f" class="space-y-4"&gt;
        &lt;input name="email" class="w-full p-3 bg-slate-900 border border-slate-800 rounded" placeholder="Email"&gt;
        &lt;input name="password" type="password" class="w-full p-3 bg-slate-900 border border-slate-800 rounded" placeholder="Пароль (мин. 6)"&gt;
        &lt;input name="company" class="w-full p-3 bg-slate-900 border border-slate-800 rounded" placeholder="Компания"&gt;
        &lt;div id="err" class="text-red-400 text-sm hidden"&gt;&lt;/div&gt;
        &lt;button class="w-full py-3 bg-orange-500 rounded font-semibold text-slate-950"&gt;Создать аккаунт&lt;/button&gt;
      &lt;/form&gt;
      &lt;p class="text-sm text-slate-400 mt-6"&gt;Уже есть? &lt;a href="#/login" class="text-orange-400"&gt;Войти&lt;/a&gt;&lt;/p&gt;
      &lt;p class="text-xs text-slate-500 mt-4"&gt;
        Регистрируясь, вы соглашаетесь использовать платформу для оценки гипотез.
        Пробный период ${PRODUCT.trialDays} дней — с момента подключения оплаты (в ${PRODUCT.paymentAvailableFrom} году).
        Сейчас — бесплатно без ограничений.
      &lt;/p&gt;
    &lt;/div&gt;`;
  root.querySelector('#f').addEventListener('submit', (e) =&gt; {
    e.preventDefault();
    const fd = new FormData(e.target);
    const email = fd.get('email'), password = fd.get('password'), company = fd.get('company');
    const err = root.querySelector('#err');
    if (!email || !email.includes('@')) { err.textContent = 'Введите корректный email'; err.classList.remove('hidden'); return; }
    if (password.length &lt; 6) { err.textContent = 'Пароль минимум 6 символов'; err.classList.remove('hidden'); return; }
    if (DB.users.find(u =&gt; u.email === email)) { err.textContent = 'Email уже зарегистрирован'; err.classList.remove('hidden'); return; }
    DB.users = [...DB.users, { email, password: b64(password), company, credits: 100, trial_started_at: Date.now() }];
    DB.session = { email, company };
    nav('/dashboard'); render();
  });
};

// ─── 12. DASHBOARD ─────────────────────────────────────────────────────────
VIEWS.dashboard = (root) =&gt; {
  if (!DB.session) { nav('/login'); return; }
  const me = DB.users.find(u =&gt; u.email === DB.session.email) || { credits: 100 };
  const runs = DB.runs.filter(r =&gt; r.user_email === DB.session.email).sort((a,b) =&gt; b.created_at - a.created_at);
  const sub = getSubscription(me);

  root.innerHTML = `
    &lt;h1 class="text-2xl font-bold mb-1"&gt;${DB.session.email}&lt;/h1&gt;
    &lt;div class="text-slate-400 mb-8"&gt;${me.company || '—'}&lt;/div&gt;

    &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-10"&gt;
      &lt;div class="border border-slate-800 rounded-lg p-5"&gt;
        &lt;div class="text-slate-400 text-sm mb-1"&gt;Тариф&lt;/div&gt;
        ${PRODUCT.paymentNotYetAvailable ? `
          &lt;div class="text-xl font-bold text-orange-400"&gt;Бесплатный доступ&lt;/div&gt;
          &lt;div class="text-xs text-slate-500 mt-1"&gt;до ${PRODUCT.paymentAvailableFrom} года&lt;/div&gt;
        ` : sub?.inTrial ? `
          &lt;div class="text-xl font-bold text-orange-400"&gt;Пробный период&lt;/div&gt;
          &lt;div class="text-xs text-slate-500 mt-1"&gt;осталось ${sub.daysLeft} дн.&lt;/div&gt;
        ` : `
          &lt;div class="text-xl font-bold"&gt;Стандарт&lt;/div&gt;
          &lt;div class="text-xs text-slate-500 mt-1"&gt;${PRODUCT.price} ${PRODUCT.priceCurrency}/мес&lt;/div&gt;
        `}
      &lt;/div&gt;
      &lt;div class="border border-slate-800 rounded-lg p-5"&gt;
        &lt;div class="text-slate-400 text-sm mb-1"&gt;Прогонов&lt;/div&gt;
        &lt;div class="text-xl font-bold"&gt;${runs.length}&lt;/div&gt;
        &lt;div class="text-xs text-slate-500 mt-1"&gt;всего выполнено&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="border border-slate-800 rounded-lg p-5"&gt;
        &lt;div class="text-slate-400 text-sm mb-1"&gt;Документов&lt;/div&gt;
        &lt;div class="text-xl font-bold"&gt;${runs.reduce((s, r) =&gt; s + (r.documents_count || 0), 0)}&lt;/div&gt;
        &lt;div class="text-xs text-slate-500 mt-1"&gt;загружено в прогонах&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    ${PRODUCT.paymentNotYetAvailable ? `
      &lt;div class="border border-orange-700 bg-orange-950/30 rounded-lg p-4 mb-8 text-sm"&gt;
        &lt;span class="text-orange-400 font-semibold"&gt;Оплата появится в ${PRODUCT.paymentAvailableFrom} году.&lt;/span&gt;
        Сейчас платформа работает бесплатно. &lt;a href="#/pricing" class="text-orange-400 underline"&gt;Подробнее&lt;/a&gt;
      &lt;/div&gt;
    ` : ''}

    &lt;h2 class="text-xl font-semibold mb-4"&gt;Мои прогоны&lt;/h2&gt;
    ${runs.length === 0
      ? `&lt;p class="text-slate-400"&gt;Пока нет прогонов. &lt;a href="#/presets" class="text-orange-400"&gt;Выбрать пресет →&lt;/a&gt;&lt;/p&gt;`
      : `&lt;div class="overflow-x-auto"&gt;
        &lt;table class="w-full text-sm"&gt;
          &lt;thead class="text-slate-400 text-left border-b border-slate-800"&gt;
            &lt;tr&gt;&lt;th class="py-2"&gt;Дата&lt;/th&gt;&lt;th&gt;Тип задачи&lt;/th&gt;&lt;th&gt;Докум.&lt;/th&gt;&lt;th&gt;Размер&lt;/th&gt;&lt;th&gt;Индекс&lt;/th&gt;&lt;th&gt;&lt;/th&gt;&lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            ${runs.map(r =&gt; `
              &lt;tr class="border-b border-slate-900"&gt;
                &lt;td class="py-2 text-slate-400"&gt;${new Date(r.created_at).toLocaleString('ru-RU')}&lt;/td&gt;
                &lt;td&gt;${r.task_type_title || r.task_type_code || '—'}&lt;/td&gt;
                &lt;td&gt;${r.documents_count ? `? ${r.documents_count}` : '—'}&lt;/td&gt;
                &lt;td&gt;${r.size}&lt;/td&gt;
                &lt;td&gt;${r.report?.appeal_index ?? '—'} / 10&lt;/td&gt;
                &lt;td&gt;&lt;a href="#/report/${r.id}" class="text-orange-400"&gt;Открыть&lt;/a&gt;&lt;/td&gt;
              &lt;/tr&gt;`).join('')}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;`}
  `;
};

// ─── 13. RUN ───────────────────────────────────────────────────────────────
VIEWS.run = (root) =&gt; {
  if (!DB.session) { nav('/login'); return; }
  const panels = DB.panels.filter(p =&gt; p.is_visible);

  const presetId = sessionStorage.getItem('gip_preset');
  const preset = presetId ? PRESETS.find(p =&gt; p.id === presetId) : null;
  if (presetId) sessionStorage.removeItem('gip_preset');

  const state = {
    panelId: preset
      ? (DB.panels.find(p =&gt; p.code === preset.panel_code)?.id || panels[0]?.id)
      : (panels[0]?.id || ''),
    taskType: preset?.task_type || 'physical_product',
    text: preset?.text || '',
    size: 100,
    docs: [],
    parsing: false,
    scenarios: preset?.scenarios?.length
      ? JSON.parse(JSON.stringify(preset.scenarios))
      : [
          { label: 'base',       price_mult: 1.00, variant: 'техническая' },
          { label: 'price -15%', price_mult: 0.85, variant: 'техническая' },
          { label: 'price +15%', price_mult: 1.15, variant: 'техническая' },
        ],
  };

  function draw() {
    const mergedSignals = mergeDocSignals(state.docs);
    root.innerHTML = `
      &lt;h1 class="text-2xl font-bold mb-2"&gt;Новый прогон&lt;/h1&gt;
      ${preset ? `&lt;div class="text-sm text-orange-400 mb-6"&gt;Загружен пресет: ${preset.title}&lt;/div&gt;` : '&lt;div class="mb-6"&gt;&lt;/div&gt;'}
      &lt;div class="max-w-2xl space-y-5"&gt;
        &lt;div&gt;
          &lt;label class="block mb-2 text-sm text-slate-400"&gt;Отраслевая панель&lt;/label&gt;
          &lt;select id="panel" class="w-full p-3 bg-slate-900 border border-slate-800 rounded"&gt;
            ${panels.map(p =&gt; `&lt;option value="${p.id}" ${p.id === state.panelId ? 'selected' : ''}&gt;${p.title}&lt;/option&gt;`).join('')}
          &lt;/select&gt;
        &lt;/div&gt;

        &lt;div&gt;
          &lt;label class="block mb-2 text-sm text-slate-400"&gt;Тип задачи&lt;/label&gt;
          &lt;select id="tt" class="w-full p-3 bg-slate-900 border border-slate-800 rounded"&gt;
            ${TASK_TYPES.map(t =&gt; `&lt;option value="${t.code}" ${t.code === state.taskType ? 'selected' : ''}&gt;${t.title}&lt;/option&gt;`).join('')}
          &lt;/select&gt;
        &lt;/div&gt;

        &lt;div&gt;
          &lt;label class="block mb-2 text-sm text-slate-400"&gt;Гипотеза&lt;/label&gt;
          &lt;textarea id="hyp" rows="6" class="w-full p-3 bg-slate-900 border border-slate-800 rounded"
            placeholder="Опишите гипотезу: сегмент, проблема, предложение, цена…"&gt;${state.text}&lt;/textarea&gt;
        &lt;/div&gt;

        &lt;div&gt;
          &lt;label class="block mb-2 text-sm text-slate-400"&gt;Документы (ТУ, сертификаты, протоколы)&lt;/label&gt;
          &lt;div id="drop" class="border border-dashed border-slate-700 rounded-lg p-6 text-center transition-colors cursor-pointer"&gt;
            &lt;input id="file" type="file" multiple accept=".pdf,.docx,.doc,.txt,.md" class="hidden"&gt;
            &lt;div class="text-sm text-slate-300 mb-1"&gt;? Перетащите файлы сюда или &lt;span class="text-orange-400"&gt;выберите на диске&lt;/span&gt;&lt;/div&gt;
            &lt;div class="text-xs text-slate-500"&gt;PDF · DOCX · TXT · MD · до 20 МБ каждый&lt;/div&gt;
          &lt;/div&gt;
          ${state.parsing ? `&lt;div class="text-xs text-orange-400 mt-2"&gt;Разбираем файл…&lt;/div&gt;` : ''}
          ${state.docs.length ? `
            &lt;ul class="mt-3 space-y-2"&gt;
              ${state.docs.map((d, i) =&gt; `
                &lt;li class="border ${d.error ? 'border-red-900 bg-red-950/30' : 'border-slate-800'} rounded p-3 text-sm"&gt;
                  &lt;div class="flex items-start justify-between gap-3"&gt;
                    &lt;div class="flex-1 min-w-0"&gt;
                      &lt;div class="font-medium truncate"&gt;${d.name} &lt;span class="text-slate-500 text-xs"&gt;· ${d.sizeKB} КБ&lt;/span&gt;&lt;/div&gt;
                      ${d.error
                        ? `&lt;div class="text-red-400 text-xs mt-1"&gt;${d.error}&lt;/div&gt;`
                        : `&lt;div class="text-slate-400 text-xs mt-1 line-clamp-2"&gt;${d.preview || ''}…&lt;/div&gt;
                           &lt;div class="flex flex-wrap gap-1 mt-2"&gt;
                             ${(d.signals?.chips || []).map(c =&gt; `&lt;span class="chip ${c.cls}"&gt;${c.text}&lt;/span&gt;`).join('')}
                           &lt;/div&gt;`}
                    &lt;/div&gt;
                    &lt;button data-rmdoc="${i}" class="text-slate-500 hover:text-red-400 text-lg leading-none"&gt;✕&lt;/button&gt;
                  &lt;/div&gt;
                &lt;/li&gt;
              `).join('')}
            &lt;/ul&gt;
            ${mergedSignals.docCount ? `
              &lt;div class="mt-3 text-xs text-slate-400"&gt;
                Объединённые сигналы: &lt;span class="text-slate-500"&gt;документов ${mergedSignals.docCount}&lt;/span&gt;
                ${mergedSignals.hasCert ? ' · ✅ сертификат' : ''}
                ${mergedSignals.hasProtokol ? ' · ✅ протокол' : ''}
                ${mergedSignals.hasGOST ? ' · ✅ ГОСТ' : ''}
                ${mergedSignals.hasMoroz ? ' · ✅ мороз' : ''}
              &lt;/div&gt;` : ''}
          ` : ''}
        &lt;/div&gt;

        &lt;div&gt;
          &lt;label class="block mb-2 text-sm text-slate-400"&gt;Размер панели: &lt;span id="sizeLabel"&gt;${state.size}&lt;/span&gt;&lt;/label&gt;
          &lt;input id="size" type="range" min="8" max="500" value="${state.size}" class="w-full"&gt;
        &lt;/div&gt;

        &lt;div&gt;
          &lt;label class="block mb-2 text-sm text-slate-400"&gt;Матрица сценариев&lt;/label&gt;
          &lt;div class="space-y-2"&gt;
            ${state.scenarios.map((s, i) =&gt; `
              &lt;div class="flex flex-wrap gap-2 items-center text-sm"&gt;
                &lt;input data-i="${i}" data-f="label" value="${s.label}" class="flex-1 min-w-[140px] p-2 bg-slate-900 border border-slate-800 rounded"&gt;
                &lt;select data-i="${i}" data-f="variant" class="p-2 bg-slate-900 border border-slate-800 rounded"&gt;
                  ${VARIANTS.map(v =&gt; `&lt;option value="${v}" ${v === s.variant ? 'selected' : ''}&gt;${v}&lt;/option&gt;`).join('')}
                &lt;/select&gt;
                &lt;input data-i="${i}" data-f="price_mult" type="number" step="0.05" value="${s.price_mult}" class="w-24 p-2 bg-slate-900 border border-slate-800 rounded"&gt;
                &lt;button data-rm="${i}" class="text-slate-500 hover:text-red-400"&gt;✕&lt;/button&gt;
              &lt;/div&gt;
            `).join('')}
          &lt;/div&gt;
          &lt;button id="addScen" class="text-orange-400 text-sm mt-2"&gt;+ добавить сценарий&lt;/button&gt;
        &lt;/div&gt;

        &lt;button id="go" class="px-6 py-3 bg-orange-500 rounded-lg font-semibold text-slate-950 hover:bg-orange-400" ${state.parsing ? 'disabled' : ''}&gt;Запустить прогон&lt;/button&gt;
      &lt;/div&gt;
    `;

    root.querySelector('#panel').addEventListener('change', e =&gt; { state.panelId = e.target.value; });
    root.querySelector('#tt').addEventListener('change', e =&gt; { state.taskType = e.target.value; });
    root.querySelector('#hyp').addEventListener('input', e =&gt; { state.text = e.target.value; });
    root.querySelector('#size').addEventListener('input', e =&gt; {
      state.size = +e.target.value;
      root.querySelector('#sizeLabel').textContent = state.size;
    });
    root.querySelectorAll('[data-i]').forEach(inp =&gt; {
      const evt = inp.tagName === 'SELECT' ? 'change' : 'input';
      inp.addEventListener(evt, e =&gt; {
        const i = +e.target.dataset.i, f = e.target.dataset.f;
        state.scenarios[i][f] = f === 'price_mult' ? +e.target.value : e.target.value;
      });
    });
    root.querySelectorAll('button[data-rm]').forEach(b =&gt; {
      b.addEventListener('click', e =&gt; { state.scenarios.splice(+e.target.dataset.rm, 1); draw(); });
    });
    root.querySelectorAll('button[data-rmdoc]').forEach(b =&gt; {
      b.addEventListener('click', e =&gt; { state.docs.splice(+e.target.dataset.rmdoc, 1); draw(); });
    });
    root.querySelector('#addScen').addEventListener('click', () =&gt; {
      state.scenarios.push({ label: 'new', price_mult: 1.0, variant: 'техническая' });
      draw();
    });

    const drop = root.querySelector('#drop');
    const fileInput = root.querySelector('#file');
    drop.addEventListener('click', () =&gt; fileInput.click());
    fileInput.addEventListener('change', e =&gt; handleFiles([...e.target.files]));
    ['dragenter','dragover'].forEach(evt =&gt; drop.addEventListener(evt, e =&gt; {
      e.preventDefault(); e.stopPropagation(); drop.classList.add('drop-active');
    }));
    ['dragleave','drop'].forEach(evt =&gt; drop.addEventListener(evt, e =&gt; {
      e.preventDefault(); e.stopPropagation(); drop.classList.remove('drop-active');
    }));
    drop.addEventListener('drop', e =&gt; handleFiles([...e.dataTransfer.files]));

    root.querySelector('#go').addEventListener('click', go);
  }

  async function handleFiles(files) {
    if (!files.length) return;
    state.parsing = true; draw();
    for (const file of files) {
      if (file.size &gt; 20 * 1024 * 1024) {
        state.docs.push({ name: file.name, sizeKB: Math.round(file.size / 1024), error: 'Файл больше 20 МБ' });
        continue;
      }
      const parsed = await parseFile(file);
      state.docs.push(parsed);
    }
    state.parsing = false; draw();
  }

  function go() {
    if (!state.text.trim()) { alert('Опишите гипотезу'); return; }
    const panel = DB.panels.find(p =&gt; p.id === state.panelId);
    const taskType = TASK_TYPES.find(t =&gt; t.code === state.taskType);
    const cals = DB.calibrations.filter(c =&gt; c.panel_id === panel.id);
    const docSignals = mergeDocSignals(state.docs);

    const { base, matrix } = runMatrix(panel, state.text, state.size, taskType, state.scenarios, cals, docSignals);
    const run = {
      id: 'r_' + Date.now(),
      user_email: DB.session.email,
      panel_id: panel.id, panel_title: panel.title,
      task_type_code: taskType.code, task_type_title: taskType.title,
      size: state.size, scenarios: state.scenarios,
      documents_count: state.docs.filter(d =&gt; !d.error).length,
      documents_names: state.docs.map(d =&gt; d.name),
      report: base, matrix,
      created_at: Date.now(),
    };
    DB.runs = [...DB.runs, run];
    nav('/report/' + run.id);
  }

  draw();
};

// ─── 14. REPORT ────────────────────────────────────────────────────────────
VIEWS.report = (root, [id]) =&gt; {
  if (!DB.session) { nav('/login'); return; }
  const run = DB.runs.find(r =&gt; r.id === id);
  if (!run) { root.innerHTML = '&lt;div class="text-red-400"&gt;Отчёт не найден&lt;/div&gt;'; return; }
  const r = run.report;

  root.innerHTML = `
    &lt;div class="flex flex-wrap items-center justify-between gap-3 mb-2"&gt;
      &lt;h1 class="text-2xl font-bold"&gt;Отчёт&lt;/h1&gt;
      &lt;div class="flex gap-2 text-sm no-print flex-wrap"&gt;
        &lt;button id="pdf" class="px-3 py-1.5 border border-slate-700 rounded hover:border-slate-500"&gt;Печать / PDF&lt;/button&gt;
        &lt;button id="pptx" class="px-3 py-1.5 border border-slate-700 rounded hover:border-slate-500"&gt;Скачать PPTX&lt;/button&gt;
        &lt;button id="json" class="px-3 py-1.5 border border-slate-700 rounded hover:border-slate-500"&gt;JSON&lt;/button&gt;
        &lt;a href="#/calibrate/${run.id}" class="px-3 py-1.5 bg-orange-500 text-slate-950 rounded font-semibold hover:bg-orange-400"&gt;Калибровать&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="text-xs text-slate-500 mb-1"&gt;${run.panel_title} · ${run.task_type_title || ''}&lt;/div&gt;
    &lt;div class="text-sm text-orange-400 mb-8"&gt;${r.disclaimer}&lt;/div&gt;

    &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"&gt;
      &lt;div class="border border-slate-800 rounded p-4"&gt;
        &lt;div class="text-slate-400 text-sm"&gt;Индекс привлекательности&lt;/div&gt;
        &lt;div class="text-3xl font-bold"&gt;${r.appeal_index} &lt;span class="text-slate-500 text-lg"&gt;/ 10&lt;/span&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="border border-slate-800 rounded p-4"&gt;
        &lt;div class="text-slate-400 text-sm"&gt;Готовность платить&lt;/div&gt;
        &lt;div class="text-3xl font-bold"&gt;${Math.round(r.wtp * 100)}%&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="border border-slate-800 rounded p-4"&gt;
        &lt;div class="text-slate-400 text-sm"&gt;Уверенность&lt;/div&gt;
        &lt;div class="text-3xl font-bold"&gt;${r.confidence}&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    ${r.documents_used ? `
      &lt;div class="border border-slate-800 rounded p-4 mb-8"&gt;
        &lt;div class="text-slate-400 text-sm mb-2"&gt;? Учтено документов: ${r.documents_used}&lt;/div&gt;
        &lt;div class="text-xs text-slate-500 mb-2"&gt;${(r.documents || []).join(' · ')}&lt;/div&gt;
        &lt;div class="flex flex-wrap gap-1"&gt;
          ${(r.document_signals || []).map(c =&gt; `&lt;span class="chip ${c.cls}"&gt;${c.text}&lt;/span&gt;`).join('')}
        &lt;/div&gt;
      &lt;/div&gt;` : ''}

    &lt;h2 class="text-xl font-semibold mb-3"&gt;Топ-возражения&lt;/h2&gt;
    &lt;ul class="space-y-2 mb-8"&gt;
      ${r.top_objections.map(o =&gt; `
        &lt;li class="border border-slate-800 rounded p-3"&gt;
          &lt;div class="flex justify-between gap-4 mb-2"&gt;&lt;span&gt;${o.text}&lt;/span&gt;&lt;span class="text-slate-400 shrink-0"&gt;${Math.round(o.share * 100)}%&lt;/span&gt;&lt;/div&gt;
          &lt;div class="h-1 bg-slate-800 rounded"&gt;&lt;div class="bar h-1 bg-orange-500 rounded" style="width: ${o.share * 100}%"&gt;&lt;/div&gt;&lt;/div&gt;
        &lt;/li&gt;`).join('')}
    &lt;/ul&gt;

    &lt;h2 class="text-xl font-semibold mb-3"&gt;По сегментам&lt;/h2&gt;
    &lt;ul class="space-y-1 mb-8"&gt;
      ${Object.entries(r.segment_scores).map(([seg, score]) =&gt; `
        &lt;li class="flex justify-between border-b border-slate-900 py-2 gap-3 flex-wrap"&gt;
          &lt;span&gt;${seg}&lt;/span&gt;
          &lt;span class="flex gap-3 text-sm"&gt;
            &lt;span class="text-slate-500"&gt;уверенность: ${r.segment_confidence[seg]}&lt;/span&gt;
            &lt;span&gt;${score} / 10&lt;/span&gt;
          &lt;/span&gt;
        &lt;/li&gt;`).join('')}
    &lt;/ul&gt;

    ${run.matrix?.length ? `
      &lt;h2 class="text-xl font-semibold mb-3"&gt;Матрица сценариев&lt;/h2&gt;
      &lt;div class="overflow-x-auto mb-8"&gt;
        &lt;table class="w-full text-sm"&gt;
          &lt;thead class="text-slate-400 text-left border-b border-slate-800"&gt;
            &lt;tr&gt;&lt;th class="py-2"&gt;Сценарий&lt;/th&gt;&lt;th&gt;Формулировка&lt;/th&gt;&lt;th&gt;Цена ×&lt;/th&gt;&lt;th&gt;Индекс&lt;/th&gt;&lt;th&gt;WTP&lt;/th&gt;&lt;th&gt;Главное возражение&lt;/th&gt;&lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            ${run.matrix.map(m =&gt; `
              &lt;tr class="border-b border-slate-900"&gt;
                &lt;td class="py-2"&gt;${m.label}&lt;/td&gt;
                &lt;td class="text-slate-400"&gt;${m.variant}&lt;/td&gt;
                &lt;td&gt;${m.price_mult}&lt;/td&gt;
                &lt;td&gt;${m.appeal_index}&lt;/td&gt;
                &lt;td&gt;${Math.round(m.wtp * 100)}%&lt;/td&gt;
                &lt;td class="text-slate-400"&gt;${m.top_objection}&lt;/td&gt;
              &lt;/tr&gt;`).join('')}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;` : ''}

    &lt;h2 class="text-xl font-semibold mb-3"&gt;Цитаты&lt;/h2&gt;
    &lt;ul class="space-y-2 italic text-slate-300 mb-8"&gt;
      ${r.quotes.map(q =&gt; `&lt;li class="border-l-2 border-orange-500 pl-3"&gt;${q}&lt;/li&gt;`).join('')}
    &lt;/ul&gt;

    &lt;h2 class="text-xl font-semibold mb-3"&gt;Рекомендации&lt;/h2&gt;
    &lt;ul class="space-y-1 list-disc list-inside"&gt;
      ${r.recommendations.map(x =&gt; `&lt;li&gt;${x}&lt;/li&gt;`).join('')}
    &lt;/ul&gt;
  `;

  root.querySelector('#json').addEventListener('click', () =&gt; {
    const blob = new Blob([JSON.stringify(run, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = `report-${run.id}.json`; a.click();
    URL.revokeObjectURL(url);
  });

  root.querySelector('#pdf').addEventListener('click', () =&gt; window.print());
  root.querySelector('#pptx').addEventListener('click', async () =&gt; {
    const btn = root.querySelector('#pptx');
    btn.textContent = 'Формируем…';
    try { await exportPPTX(run); btn.textContent = 'Скачать PPTX'; }
    catch (e) { alert('Ошибка экспорта PPTX: ' + e.message); btn.textContent = 'Скачать PPTX'; }
  });
};

// ─── 15. PPTX ──────────────────────────────────────────────────────────────
async function exportPPTX(run) {
  if (typeof PptxGenJS === 'undefined') throw new Error('PptxGenJS не загружен. Обновите страницу.');
  const pptx = new PptxGenJS();
  pptx.defineLayout({ name: 'A16x9', width: 13.333, height: 7.5 });
  pptx.layout = 'A16x9';
  pptx.author = 'Гипотеза';
  pptx.title = 'Отчёт по гипотезе';

  const r = run.report;
  const BG = '0F172A', FG = 'F1F5F9', ACC = 'F97316', DIM = '94A3B8';

  let s = pptx.addSlide();
  s.background = { color: BG };
  s.addText('ГИПОТЕЗА', { x: 0.6, y: 2.4, w: 12, h: 1.2, fontSize: 54, bold: true, color: FG });
  s.addText(r.hypothesis.slice(0, 260) + (r.hypothesis.length &gt; 260 ? '…' : ''),
    { x: 0.6, y: 3.8, w: 12, h: 2, fontSize: 16, color: DIM });
  s.addText(new Date(run.created_at).toLocaleDateString('ru-RU'),
    { x: 0.6, y: 6.8, w: 12, h: 0.4, fontSize: 12, color: DIM });

  s = pptx.addSlide();
  s.background = { color: BG };
  s.addText('Ключевые метрики', { x: 0.6, y: 0.5, w: 12, h: 0.8, fontSize: 30, bold: true, color: FG });
  const metrics = [
    ['Индекс привлекательности', `${r.appeal_index} / 10`],
    ['Готовность платить', `${Math.round(r.wtp * 100)}%`],
    ['Уверенность', r.confidence],
    ['Респондентов', String(r.respondents)],
  ];
  metrics.forEach(([label, val], i) =&gt; {
    const x = 0.6 + (i % 2) * 6.2;
    const y = 1.8 + Math.floor(i / 2) * 1.6;
    s.addShape(pptx.ShapeType.rect, { x, y, w: 5.8, h: 1.3, fill: { color: '1E293B' }, line: { color: '334155' } });
    s.addText(label, { x: x + 0.2, y: y + 0.1, w: 5.4, h: 0.4, fontSize: 12, color: DIM });
    s.addText(val, { x: x + 0.2, y: y + 0.5, w: 5.4, h: 0.7, fontSize: 24, bold: true, color: FG });
  });

  if (r.documents_used) {
    s = pptx.addSlide();
    s.background = { color: BG };
    s.addText('Использованные документы', { x: 0.6, y: 0.5, w: 12, h: 0.8, fontSize: 30, bold: true, color: FG });
    s.addText((r.documents || []).join(' · '), { x: 0.6, y: 1.6, w: 12, h: 0.6, fontSize: 14, color: DIM });
    const chipRows = (r.document_signals || []).map(c =&gt; [{ text: c.text, options: { fontSize: 14, color: FG } }]);
    if (chipRows.length) {
      s.addTable(chipRows, { x: 0.6, y: 2.4, w: 12, colW: [12], border: { type: 'solid', color: '334155', pt: 1 }, fill: { color: '0F172A' }, rowH: 0.5 });
    }
  }

  s = pptx.addSlide();
  s.background = { color: BG };
  s.addText('Топ-возражения', { x: 0.6, y: 0.5, w: 12, h: 0.8, fontSize: 30, bold: true, color: FG });
  const objRows = r.top_objections.map(o =&gt; [
    { text: o.text, options: { fontSize: 14, color: FG } },
    { text: `${Math.round(o.share * 100)}%`, options: { fontSize: 14, color: ACC, align: 'right' } },
  ]);
  s.addTable(objRows, { x: 0.6, y: 1.6, w: 12, colW: [10, 2], border: { type: 'solid', color: '334155', pt: 1 }, fill: { color: '0F172A' }, rowH: 0.6 });

  s = pptx.addSlide();
  s.background = { color: BG };
  s.addText('По сегментам', { x: 0.6, y: 0.5, w: 12, h: 0.8, fontSize: 30, bold: true, color: FG });
  const segRows = Object.entries(r.segment_scores).map(([seg, score]) =&gt; [
    { text: seg, options: { fontSize: 14, color: FG } },
    { text: r.segment_confidence[seg], options: { fontSize: 12, color: DIM } },
    { text: `${score} / 10`, options: { fontSize: 14, color: ACC, align: 'right' } },
  ]);
  s.addTable(
    [[
      { text: 'Сегмент', options: { fontSize: 12, color: DIM, bold: true } },
      { text: 'Уверенность', options: { fontSize: 12, color: DIM, bold: true } },
      { text: 'Оценка', options: { fontSize: 12, color: DIM, bold: true, align: 'right' } },
    ], ...segRows],
    { x: 0.6, y: 1.6, w: 12, colW: [7, 3, 2], border: { type: 'solid', color: '334155', pt: 1 }, fill: { color: '0F172A' }, rowH: 0.55 }
  );

  if (run.matrix?.length) {
    s = pptx.addSlide();
    s.background = { color: BG };
    s.addText('Матрица сценариев', { x: 0.6, y: 0.5, w: 12, h: 0.8, fontSize: 30, bold: true, color: FG });
    const mRows = run.matrix.map(m =&gt; [
      { text: m.label, options: { fontSize: 12, color: FG } },
      { text: m.variant, options: { fontSize: 12, color: DIM } },
      { text: String(m.price_mult), options: { fontSize: 12, color: FG, align: 'center' } },
      { text: String(m.appeal_index), options: { fontSize: 12, color: ACC, align: 'right' } },
      { text: `${Math.round(m.wtp * 100)}%`, options: { fontSize: 12, color: FG, align: 'right' } },
    ]);
    s.addTable(
      [[
        { text: 'Сценарий', options: { fontSize: 11, color: DIM, bold: true } },
        { text: 'Формулировка', options: { fontSize: 11, color: DIM, bold: true } },
        { text: 'Цена ×', options: { fontSize: 11, color: DIM, bold: true, align: 'center' } },
        { text: 'Индекс', options: { fontSize: 11, color: DIM, bold: true, align: 'right' } },
        { text: 'WTP', options: { fontSize: 11, color: DIM, bold: true, align: 'right' } },
      ], ...mRows],
      { x: 0.6, y: 1.6, w: 12, colW: [3.5, 2.5, 1.5, 2, 2.5], border: { type: 'solid', color: '334155', pt: 1 }, fill: { color: '0F172A' }, rowH: 0.55 }
    );
  }

  s = pptx.addSlide();
  s.background = { color: BG };
  s.addText('Рекомендации', { x: 0.6, y: 0.5, w: 12, h: 0.8, fontSize: 30, bold: true, color: FG });
  r.recommendations.forEach((rec, i) =&gt; {
    s.addText(`• ${rec}`, { x: 0.8, y: 1.7 + i * 0.7, w: 12, h: 0.6, fontSize: 14, color: FG });
  });

  s = pptx.addSlide();
  s.background = { color: BG };
  s.addText('Границы применения', { x: 0.6, y: 0.5, w: 12, h: 0.8, fontSize: 30, bold: true, color: FG });
  s.addText(r.disclaimer, { x: 0.6, y: 1.7, w: 12, h: 3, fontSize: 16, color: DIM });
  s.addText(`${PRODUCT.email} · ${PRODUCT.phone} (${PRODUCT.contactName})`,
    { x: 0.6, y: 6.5, w: 12, h: 0.5, fontSize: 12, color: ACC });

  await pptx.writeFile({ fileName: `gipoteza-${run.id}.pptx` });
}

// ─── 16. CALIBRATE ─────────────────────────────────────────────────────────
VIEWS.calibrate = (root, [runId]) =&gt; {
  if (!DB.session) { nav('/login'); return; }
  const run = DB.runs.find(r =&gt; r.id === runId);
  if (!run) { root.innerHTML = '&lt;div class="text-red-400"&gt;Прогон не найден&lt;/div&gt;'; return; }

  const state = { scores: [0,0,0], segment: '' };

  function draw() {
    root.innerHTML = `
      &lt;div class="max-w-xl mx-auto"&gt;
        &lt;h1 class="text-2xl font-bold mb-3"&gt;Калибровка&lt;/h1&gt;
        &lt;p class="text-slate-400 mb-6 text-sm"&gt;
          Введите оценки 3–5 реальных респондентов (1–10). Мы сравним их с синтетическим прогнозом
          и учтём разницу для этой отрасли — это повысит уверенность следующих прогонов.
        &lt;/p&gt;

        &lt;label class="text-sm text-slate-400"&gt;Сегмент (опционально)&lt;/label&gt;
        &lt;select id="seg" class="w-full p-3 bg-slate-900 border border-slate-800 rounded mb-4"&gt;
          &lt;option value=""&gt;— общая —&lt;/option&gt;
          ${Object.keys(run.report.segment_scores).map(s =&gt; `&lt;option value="${s}" ${s === state.segment ? 'selected' : ''}&gt;${s}&lt;/option&gt;`).join('')}
        &lt;/select&gt;

        ${state.scores.map((s, i) =&gt; `
          &lt;div class="mb-3"&gt;
            &lt;label class="text-sm text-slate-400"&gt;Респондент ${i + 1}&lt;/label&gt;
            &lt;input data-i="${i}" type="number" min="1" max="10" value="${s || ''}"
              class="w-full p-3 bg-slate-900 border border-slate-800 rounded"&gt;
          &lt;/div&gt;
        `).join('')}

        &lt;button id="add" class="text-orange-400 text-sm mb-4"&gt;+ добавить респондента&lt;/button&gt;

        &lt;button id="go" class="w-full py-3 bg-orange-500 rounded font-semibold text-slate-950 hover:bg-orange-400"&gt;
          Записать калибровку
        &lt;/button&gt;
        &lt;div id="result" class="mt-6"&gt;&lt;/div&gt;
      &lt;/div&gt;
    `;

    root.querySelector('#seg').addEventListener('change', e =&gt; { state.segment = e.target.value; });
    root.querySelectorAll('input[data-i]').forEach(inp =&gt; {
      inp.addEventListener('input', e =&gt; { state.scores[+e.target.dataset.i] = +e.target.value; });
    });
    root.querySelector('#add').addEventListener('click', () =&gt; { state.scores.push(0); draw(); });
    root.querySelector('#go').addEventListener('click', save);
  }

  function save() {
    const scores = state.scores.filter(s =&gt; s &gt;= 1 &amp;&amp; s &lt;= 10);
    if (scores.length &lt; 1) { alert('Введите хотя бы одну оценку'); return; }
    const real = scores.reduce((a,b) =&gt; a+b, 0) / scores.length;
    const synth = run.report.appeal_index;
    const cal = {
      id: 'c_' + Date.now(),
      run_id: run.id, panel_id: run.panel_id,
      segment: state.segment || null,
      synthetic_score: synth, real_score: +real.toFixed(2),
      delta: +(synth - real).toFixed(2),
      respondents: scores.length, created_at: Date.now(),
    };
    DB.calibrations = [...DB.calibrations, cal];

    root.querySelector('#result').innerHTML = `
      &lt;div class="border border-slate-800 rounded p-5 fade"&gt;
        &lt;div class="text-slate-400 text-sm mb-3"&gt;Калибровка записана&lt;/div&gt;
        &lt;div class="space-y-1 mb-3"&gt;
          &lt;div&gt;Синтетика: &lt;b&gt;${cal.synthetic_score}&lt;/b&gt;&lt;/div&gt;
          &lt;div&gt;Реальные: &lt;b&gt;${cal.real_score}&lt;/b&gt;&lt;/div&gt;
          &lt;div&gt;Δ: &lt;b class="${cal.delta &gt; 0 ? 'text-orange-400' : 'text-green-400'}"&gt;${cal.delta}&lt;/b&gt;&lt;/div&gt;
          &lt;div class="text-slate-500 text-xs"&gt;Респондентов: ${cal.respondents}${cal.segment ? ' · сегмент: ' + cal.segment : ''}&lt;/div&gt;
        &lt;/div&gt;
        &lt;a href="#/report/${run.id}" class="inline-block mt-4 text-orange-400"&gt;← Вернуться к отчёту&lt;/a&gt;
      &lt;/div&gt;`;
  }

  draw();
};

// ─── 17. FINANCE ───────────────────────────────────────────────────────────
VIEWS.finance = (root) =&gt; {
  const state = LS.get('finance', {
    proPrice: PRODUCT.price, businessPrice: 19900, corpPrice: 99900,
    proCount: 30, businessCount: 8, corpCount: 2,
    infraBase: 12000, llmBase: 25000, devOneTime: 450000, marketing: 60000,
  });
  function save() { LS.set('finance', state); renderFinance(); }
  function renderFinance() {
    const { proPrice, businessPrice, corpPrice, proCount, businessCount, corpCount,
            infraBase, llmBase, devOneTime, marketing } = state;
    const mrr = proPrice * proCount + businessPrice * businessCount + corpPrice * corpCount;
    const arr = mrr * 12;
    const monthlyOpex = infraBase + llmBase + marketing;
    const monthlyGross = mrr - monthlyOpex;
    const paybackMonths = monthlyGross &gt; 0 ? Math.ceil(devOneTime / monthlyGross) : Infinity;
    const margin = mrr &gt; 0 ? (monthlyGross / mrr) * 100 : 0;
    const customers = proCount + businessCount + corpCount;
    const arpu = customers ? mrr / customers : 0;
    const scen = [
      { name: 'Пессимистичный', mult: 0.5, color: 'text-red-400' },
      { name: 'Базовый',        mult: 1.0, color: 'text-orange-400' },
      { name: 'Оптимистичный',  mult: 2.0, color: 'text-green-400' },
    ];

    root.innerHTML = `
      &lt;h1 class="text-2xl font-bold mb-2"&gt;Финансовая модель&lt;/h1&gt;
      &lt;p class="text-slate-400 mb-8"&gt;Крутите цифры — модель пересчитывается на лету.&lt;/p&gt;

      &lt;div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-10"&gt;
        &lt;div&gt;
          &lt;h2 class="text-lg font-semibold mb-4"&gt;Тарифы и подписчики&lt;/h2&gt;
          &lt;div class="space-y-3 text-sm"&gt;
            ${field('Pro, ₽/мес', 'proPrice')}
            ${field('Pro подписчиков', 'proCount')}
            ${field('Бизнес, ₽/мес', 'businessPrice')}
            ${field('Бизнес подписчиков', 'businessCount')}
            ${field('Корпоративный, ₽/мес', 'corpPrice')}
            ${field('Корпоративных', 'corpCount')}
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;h2 class="text-lg font-semibold mb-4"&gt;Затраты&lt;/h2&gt;
          &lt;div class="space-y-3 text-sm"&gt;
            ${field('Инфраструктура, ₽/мес', 'infraBase')}
            ${field('LLM API, ₽/мес', 'llmBase')}
            ${field('Маркетинг, ₽/мес', 'marketing')}
            ${field('Разработка (разово), ₽', 'devOneTime')}
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;h2 class="text-lg font-semibold mb-4"&gt;Итоги&lt;/h2&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-10"&gt;
        &lt;div class="border border-slate-800 rounded p-5"&gt;&lt;div class="text-slate-400 text-sm"&gt;MRR&lt;/div&gt;&lt;div class="text-2xl font-bold text-orange-400"&gt;${fmt(mrr)} ₽&lt;/div&gt;&lt;/div&gt;
        &lt;div class="border border-slate-800 rounded p-5"&gt;&lt;div class="text-slate-400 text-sm"&gt;ARR&lt;/div&gt;&lt;div class="text-2xl font-bold"&gt;${fmt(arr)} ₽&lt;/div&gt;&lt;/div&gt;
        &lt;div class="border border-slate-800 rounded p-5"&gt;&lt;div class="text-slate-400 text-sm"&gt;Маржа&lt;/div&gt;&lt;div class="text-2xl font-bold"&gt;${margin.toFixed(1)}%&lt;/div&gt;&lt;/div&gt;
        &lt;div class="border border-slate-800 rounded p-5"&gt;&lt;div class="text-slate-400 text-sm"&gt;Прибыль/мес&lt;/div&gt;&lt;div class="text-2xl font-bold ${monthlyGross &gt; 0 ? 'text-green-400' : 'text-red-400'}"&gt;${fmt(monthlyGross)} ₽&lt;/div&gt;&lt;/div&gt;
        &lt;div class="border border-slate-800 rounded p-5"&gt;&lt;div class="text-slate-400 text-sm"&gt;ARPU&lt;/div&gt;&lt;div class="text-2xl font-bold"&gt;${fmt(Math.round(arpu))} ₽&lt;/div&gt;&lt;/div&gt;
        &lt;div class="border border-slate-800 rounded p-5"&gt;&lt;div class="text-slate-400 text-sm"&gt;Окупаемость&lt;/div&gt;&lt;div class="text-2xl font-bold"&gt;${paybackMonths === Infinity ? '∞' : paybackMonths + ' мес'}&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;

      &lt;h2 class="text-lg font-semibold mb-4"&gt;Три сценария&lt;/h2&gt;
      &lt;div class="overflow-x-auto"&gt;
        &lt;table class="w-full text-sm"&gt;
          &lt;thead class="text-slate-400 text-left border-b border-slate-800"&gt;&lt;tr&gt;&lt;th class="py-2"&gt;Сценарий&lt;/th&gt;&lt;th&gt;Множитель&lt;/th&gt;&lt;th&gt;MRR&lt;/th&gt;&lt;th&gt;Прибыль/мес&lt;/th&gt;&lt;th&gt;Окупаемость&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
          &lt;tbody&gt;
            ${scen.map(sc =&gt; {
              const m = mrr * sc.mult;
              const g = m - monthlyOpex;
              const pb = g &gt; 0 ? Math.ceil(devOneTime / g) : Infinity;
              return `&lt;tr class="border-b border-slate-900"&gt;
                &lt;td class="py-2 ${sc.color}"&gt;${sc.name}&lt;/td&gt;&lt;td&gt;×${sc.mult}&lt;/td&gt;
                &lt;td&gt;${fmt(m)} ₽&lt;/td&gt;&lt;td&gt;${fmt(g)} ₽&lt;/td&gt;
                &lt;td&gt;${pb === Infinity ? '∞' : pb + ' мес'}&lt;/td&gt;&lt;/tr&gt;`;
            }).join('')}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    `;
    root.querySelectorAll('[data-fin]').forEach(inp =&gt; {
      inp.addEventListener('input', e =&gt; { state[e.target.dataset.fin] = +e.target.value || 0; save(); });
    });
  }
  function field(label, key) {
    return `&lt;label class="block"&gt;&lt;span class="text-slate-400"&gt;${label}&lt;/span&gt;
      &lt;input data-fin="${key}" type="number" value="${state[key]}" class="w-full p-2 mt-1 bg-slate-900 border border-slate-800 rounded"&gt;&lt;/label&gt;`;
  }
  function fmt(n) { return Math.round(n).toLocaleString('ru-RU'); }
  renderFinance();
};

// ─── 18. NEW PANEL ─────────────────────────────────────────────────────────
VIEWS.panels = (root, [sub]) =&gt; {
  if (sub !== 'new') { root.innerHTML = '&lt;div&gt;Раздел в разработке&lt;/div&gt;'; return; }
  if (!DB.session) { nav('/login'); return; }
  const state = {
    code: '', title: '', description: '',
    segmentsText: 'инкассация:0.3\nVIP:0.2\nинфраструктура:0.5',
    rolesText: 'инкассация:главный конструктор, технолог\nVIP:главный конструктор',
    objectionsText: 'Нет сертификата.\nНет референсов.',
    sourcesText: 'ВЦИОМ\nРосстат',
  };
  function draw() {
    root.innerHTML = `
      &lt;div class="max-w-2xl"&gt;
        &lt;h1 class="text-2xl font-bold mb-6"&gt;Новая отраслевая панель&lt;/h1&gt;
        &lt;input id="code" class="w-full p-3 bg-slate-900 border border-slate-800 rounded mb-3" placeholder="Код" value="${state.code}"&gt;
        &lt;input id="title" class="w-full p-3 bg-slate-900 border border-slate-800 rounded mb-3" placeholder="Название" value="${state.title}"&gt;
        &lt;textarea id="description" class="w-full p-3 bg-slate-900 border border-slate-800 rounded mb-3 h-20" placeholder="Описание"&gt;${state.description}&lt;/textarea&gt;
        &lt;label class="text-sm text-slate-400"&gt;Сегменты («название:вес»)&lt;/label&gt;
        &lt;textarea id="segments" class="w-full p-3 bg-slate-900 border border-slate-800 rounded mb-3 h-24 font-mono text-sm"&gt;${state.segmentsText}&lt;/textarea&gt;
        &lt;label class="text-sm text-slate-400"&gt;Роли по сегментам&lt;/label&gt;
        &lt;textarea id="roles" class="w-full p-3 bg-slate-900 border border-slate-800 rounded mb-3 h-24 font-mono text-sm"&gt;${state.rolesText}&lt;/textarea&gt;
        &lt;label class="text-sm text-slate-400"&gt;Типовые возражения&lt;/label&gt;
        &lt;textarea id="objections" class="w-full p-3 bg-slate-900 border border-slate-800 rounded mb-3 h-24"&gt;${state.objectionsText}&lt;/textarea&gt;
        &lt;label class="text-sm text-slate-400"&gt;Источники данных&lt;/label&gt;
        &lt;textarea id="sources" class="w-full p-3 bg-slate-900 border border-slate-800 rounded mb-5 h-20"&gt;${state.sourcesText}&lt;/textarea&gt;
        &lt;div id="msg" class="text-sm mb-3"&gt;&lt;/div&gt;
        &lt;button id="go" class="px-6 py-3 bg-orange-500 rounded font-semibold text-slate-950 hover:bg-orange-400"&gt;Создать панель&lt;/button&gt;
        &lt;a href="#/" class="ml-3 text-slate-400"&gt;Отмена&lt;/a&gt;
      &lt;/div&gt;`;
    const bind = (sel, key) =&gt; { const el = root.querySelector(sel); if (el) el.addEventListener('input', e =&gt; { state[key] = e.target.value; }); };
    bind('#code','code'); bind('#title','title'); bind('#description','description');
    bind('#segments','segmentsText'); bind('#roles','rolesText');
    bind('#objections','objectionsText'); bind('#sources','sourcesText');
    root.querySelector('#go').addEventListener('click', save);
  }
  function save() {
    const msg = root.querySelector('#msg');
    if (!state.code || !state.title) { msg.textContent = 'Заполните код и название'; msg.className = 'text-sm mb-3 text-red-400'; return; }
    if (DB.panels.find(p =&gt; p.code === state.code)) { msg.textContent = 'Код уже занят'; msg.className = 'text-sm mb-3 text-red-400'; return; }
    const segments = {};
    state.segmentsText.split('\n').filter(Boolean).forEach(l =&gt; {
      const [k, v] = l.split(':').map(x =&gt; x?.trim());
      if (k) segments[k] = parseFloat(v) || 1;
    });
    const roles = {};
    state.rolesText.split('\n').filter(Boolean).forEach(l =&gt; {
      const [k, v] = l.split(':').map(x =&gt; x?.trim());
      if (k) roles[k] = (v || '').split(',').map(x =&gt; x.trim()).filter(Boolean);
    });
    const panel = {
      id: 'p_' + Date.now(),
      code: state.code, title: state.title, description: state.description,
      segments, roles,
      sources: state.sourcesText.split('\n').map(x =&gt; x.trim()).filter(Boolean),
      objections: state.objectionsText.split('\n').map(x =&gt; x.trim()).filter(Boolean),
      is_builtin: false, is_visible: true, owner_email: DB.session.email,
    };
    DB.panels = [...DB.panels, panel];
    msg.textContent = 'Панель создана.';
    msg.className = 'text-sm mb-3 text-green-400';
  }
  draw();
};

// ─── 19. BOOTSTRAP ─────────────────────────────────────────────────────────
(function bootstrap() {
  if (!localStorage.getItem('gip_users')) {
    DB.users = [{
      email: 'demo@gipoteza.ru',
      password: b64('demo1234'),
      company: 'Арсенал-Деталь',
      credits: 100,
      trial_started_at: Date.now(),
    }];
  }
})();

// ─── 20. ДЕМО ──────────────────────────────────────────────────────────────
window.__gipotezaDemo = function() {
  const preset = PRESETS[0];
  const panel = DB.panels.find(p =&gt; p.code === preset.panel_code);
  const tt = TASK_TYPES.find(t =&gt; t.code === preset.task_type);
  const cals = DB.calibrations.filter(c =&gt; c.panel_id === panel.id);
  const { base, matrix } = runMatrix(panel, preset.text, 120, tt, preset.scenarios, cals, null);
  const run = {
    id: 'r_demo_' + Date.now(),
    user_email: 'demo@gipoteza.ru',
    panel_id: panel.id, panel_title: panel.title,
    task_type_code: tt.code, task_type_title: tt.title,
    size: 120, scenarios: preset.scenarios,
    documents_count: 0,
    report: base, matrix,
    created_at: Date.now(),
  };
  DB.runs = [...DB.runs, run];
  DB.session = { email: 'demo@gipoteza.ru', company: 'Арсенал-Деталь' };
  nav('/report/' + run.id); render();
};
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</subtitle>
  <updated>2026-09-29T04:36:35Z</updated>
  <logo>https://pandoraburo.ru/ssl/u/pic/ef/f66878aa6d11f1abb9f7ceed6face4/-/image%20%284%29.jpg</logo>
  <author>
    <name>1592962</name>
    <uri>https://www.pandoraburo.ru/</uri>
  </author>
</feed>
