(function(){
  'use strict';
  const $ = (s, r=document) => r.querySelector(s);
  const $$ = (s, r=document) => Array.from(r.querySelectorAll(s));
  const state = { files: [], results: [], zipBlob: null, delayTimer: null, modalShown: false };

  const WORD_RE = /[A-Za-zА-Яа-яЁё0-9\-]+/g;
  const evidenceWords = ['факт','факты','пример','данные','источник','источники','доказ','обосн','исслед','наблюд','цитат','эксперимент','расчет','расчёт','таблиц','потому','следовательно','поэтому','так как','вывод','аргумент','причин','evidence','data','because','therefore','argument','source'];
  const logicWords = ['потому','следовательно','поэтому','так как','однако','значит','следует','во-первых','во-вторых','итак','следовательно','because','therefore','however','thus'];

  function esc(s){ return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
  function xmlEsc(s){ return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&apos;'}[c])); }
  function clamp(x,a,b){ return Math.max(a, Math.min(b, x)); }
  function fmt(x, n=3){ return Number.isFinite(x) ? Number(x).toFixed(n) : '—'; }
  function words(text){ return (String(text).toLowerCase().match(WORD_RE) || []).filter(w => w.length > 1); }
  function sentenceCount(text){ return Math.max(1, (String(text).match(/[.!?。！？]+/g) || []).length); }
  function paragraphCount(text){ return Math.max(1, String(text).split(/\n\s*\n/g).filter(x => x.trim().length > 0).length); }
  function countHits(text, arr){ const low = String(text).toLowerCase(); return arr.reduce((n,w) => n + (low.includes(w) ? 1 : 0), 0); }
  function safeLambda(U,I,R,D){ const den = (U-D)*(I-R); if(Math.abs(den) < 1e-8) return NaN; return ((U-R)*(I-D))/den; }
  function dStar(U,I,R){ const den = U + I - 2*R; if(Math.abs(den) < 1e-8) return NaN; return (2*U*I - U*R - I*R)/den; }
  function auth(lambda){ return Number.isFinite(lambda) ? 1/(1+Math.abs(lambda+1)) : 0; }
  function zoneByScore(x){ if(x >= 78) return 'OK'; if(x >= 58) return 'доработать'; return 'риск'; }
  function statusByAuth(a){ if(a >= .65) return 'достаточно согласован'; if(a >= .45) return 'требуется доработка'; return 'риск структурного разрыва'; }
  function translit(s){
    const map = {'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'e','ж':'zh','з':'z','и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r','с':'s','т':'t','у':'u','ф':'f','х':'h','ц':'c','ч':'ch','ш':'sh','щ':'sch','ъ':'','ы':'y','ь':'','э':'e','ю':'yu','я':'ya'};
    return String(s).toLowerCase().split('').map(ch => map[ch] || ch).join('').replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'').slice(0,48) || 'klt414';
  }
  function downloadBlob(blob, name){ const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=name; document.body.appendChild(a); a.click(); setTimeout(()=>{URL.revokeObjectURL(a.href); a.remove();},1000); }
  function setStatus(msg, cls){ const box = $('#statusBox'); if(!box) return; box.className = 'notice k414-notice' + (cls ? ' '+cls : ''); box.innerHTML = msg; }
  function progress(pct){ let line = $('#progressLine'); if(!line){ line = document.createElement('div'); line.id='progressLine'; line.className='progress-line'; line.innerHTML='<div></div>'; $('#statusBox').appendChild(line); } $('div', line).style.width = pct + '%'; }

  function showDelay(){ if(state.modalShown) return; state.modalShown = true; $('#delayModal')?.classList.add('show'); }
  function hideDelay(){ $('#delayModal')?.classList.remove('show'); }
  function setDelayTimer(){ clearTimeout(state.delayTimer); state.delayTimer = setTimeout(showDelay, 2800); }
  function clearDelayTimer(){ clearTimeout(state.delayTimer); state.delayTimer = null; }

  async function extractDocxText(file){
    if(typeof JSZip === 'undefined') throw new Error('Не загружена библиотека JSZip. Проверьте подключение к интернету или разместите JSZip локально.');
    const zip = await JSZip.loadAsync(file);
    const names = Object.keys(zip.files).filter(n => /^word\/(document|header\d+|footer\d+)\.xml$/.test(n)).sort((a,b)=> a.includes('document') ? -1 : b.includes('document') ? 1 : a.localeCompare(b));
    if(!names.length) throw new Error('DOCX не содержит word/document.xml.');
    let text = '';
    for(const name of names){
      const xml = await zip.file(name).async('string');
      text += xmlToText(xml) + '\n';
    }
    return text.trim();
  }

  function xmlToText(xml){
    const parser = new DOMParser();
    const doc = parser.parseFromString(xml, 'application/xml');
    const body = [];
    const nodes = doc.getElementsByTagName('*');
    for(let i=0;i<nodes.length;i++){
      const n = nodes[i];
      const local = n.localName;
      if(local === 't') body.push(n.textContent || '');
      if(local === 'tab') body.push('\t');
      if(local === 'br' || local === 'cr') body.push('\n');
      if(local === 'p') body.push('\n');
    }
    return body.join('').replace(/\n{3,}/g,'\n\n').replace(/[ \t]{2,}/g,' ').trim();
  }

  async function extractDocText(file){
    const buf = await file.arrayBuffer();
    const utf16 = new TextDecoder('utf-16le', {fatal:false}).decode(buf);
    let cyr = '';
    try{ cyr = new TextDecoder('windows-1251', {fatal:false}).decode(buf); } catch(e){ cyr = new TextDecoder('utf-8', {fatal:false}).decode(buf); }
    const clean = s => String(s).replace(/[^\u0009\u000A\u000D\u0020-\u007E\u0400-\u04FF«»„“”–—№₽]+/g,' ').replace(/[ \t]{2,}/g,' ').replace(/\n{3,}/g,'\n\n').trim();
    const a = clean(utf16), b = clean(cyr);
    const best = words(a).length > words(b).length ? a : b;
    if(words(best).length < 25) throw new Error('Старый .doc не удалось прочитать в браузере. Пересохраните файл как .docx и загрузите снова.');
    return best;
  }

  async function extractText(file){
    const name = file.name.toLowerCase();
    if(name.endsWith('.docx')) return extractDocxText(file);
    if(name.endsWith('.doc')) return extractDocText(file);
    throw new Error('Разрешены только файлы .doc и .docx.');
  }

  function analyzeText(text, fileName, referenceText){
    const w = words(text); const unique = new Set(w); const sentences = sentenceCount(text); const paras = paragraphCount(text);
    const longWords = w.filter(x => x.length >= 8).length;
    const ev = countHits(text, evidenceWords); const lg = countHits(text, logicWords);
    const numbers = (String(text).match(/\d+[,.]?\d*/g) || []).length;
    const caps = (String(text).match(/[А-ЯЁA-Z][а-яёa-z]{3,}/g) || []).length;
    const lexical = w.length ? unique.size / w.length : 0;
    const avgSent = w.length / sentences;
    const U = clamp(0.48 + lexical*0.72 + Math.min(longWords, 50)*0.003 + Math.min(paras, 6)*0.018, .35, .95);
    const I = clamp(0.50 + Math.min(sentences, 12)*0.018 + Math.min(lg, 6)*0.045 + (avgSent >= 8 && avgSent <= 28 ? .08 : -.04), .32, .94);
    const R = clamp(0.40 + Math.min(ev, 8)*0.045 + Math.min(numbers, 8)*0.018 + Math.min(caps, 20)*0.006 + Math.min(w.length, 600)/600*0.12, .25, .92);
    const D = clamp(0.42 + Math.min(lg+ev, 10)*0.035 + (paras >= 2 ? .05 : 0) + (sentences >= 5 ? .05 : 0), .25, .91);
    const lambda = safeLambda(U,I,R,D);
    const delta = Number.isFinite(lambda) ? Math.abs(lambda + 1) : Infinity;
    const A = auth(lambda);
    const Dst = dStar(U,I,R);
    let grade100 = clamp(40 + A*44 + U*5 + I*5 + R*3 + D*3, 0, 100);
    let similarity = null;
    if(referenceText && referenceText.trim().length > 30){ similarity = compareTexts(text, referenceText); grade100 = clamp(grade100*0.78 + similarity.total*0.22, 0, 100); }
    const grade5 = grade100 >= 85 ? '5' : grade100 >= 70 ? '4' : grade100 >= 55 ? '3' : '2';
    const criteria = buildCriteria({U,I,R,D,grade100,w,sentences,paras,ev,lg,lexical,similarity});
    return { fileName, text, chars:text.length, words:w.length, unique:unique.size, sentences, paragraphs:paras, U,I,R,D,lambda,delta,auth:A,Dst,grade100,grade5,similarity,criteria,status:statusByAuth(A), created:new Date().toLocaleString('ru-RU') };
  }

  function compareTexts(a,b){
    const wa = new Set(words(a).filter(x=>x.length>3)); const wb = new Set(words(b).filter(x=>x.length>3));
    const inter = [...wa].filter(x => wb.has(x)).length; const union = new Set([...wa,...wb]).size || 1;
    const jaccard = inter/union;
    const lenRatio = Math.min(words(a).length, words(b).length) / Math.max(words(a).length, words(b).length, 1);
    const total = clamp((jaccard*0.72 + lenRatio*0.28)*100, 0, 100);
    return {jaccard, lenRatio, total};
  }

  function buildCriteria(o){
    const sc = (v) => clamp(v,0,100);
    const arr = [
      {name:'Раскрытие темы', weight:.14, score:sc(45 + o.U*52), comment:'Полнота раскрытия темы и соответствие поставленному вопросу.'},
      {name:'Главная идея / limI', weight:.14, score:sc(40 + o.I*58), comment:'Ясность ведущей идеи и устойчивость тезиса.'},
      {name:'Поле понятий / limU', weight:.12, score:sc(42 + o.U*56), comment:'Ширина, точность и связность понятийного поля.'},
      {name:'Фактическое содержание / limR', weight:.14, score:sc(38 + o.R*58), comment:'Факты, примеры, источники, данные и предметная опора.'},
      {name:'Логика доказательства', weight:.14, score:sc(38 + o.D*42 + Math.min(o.lg,5)*4), comment:'Связь тезисов, аргументов и вывода.'},
      {name:'Композиция', weight:.10, score:sc(48 + Math.min(o.paras,5)*7 + Math.min(o.sentences,10)*1.8), comment:'Структурная цельность, последовательность и завершённость.'},
      {name:'Самостоятельность', weight:.08, score:sc(56 + Math.min(o.unique/o.w.length || 0, .75)*48), comment:'Признаки самостоятельной сборки и обработки материала.'},
      {name:'Стиль и ясность', weight:.07, score:sc(55 + (o.w.length/o.sentences >= 8 && o.w.length/o.sentences <= 28 ? 28 : 8)), comment:'Понятность, речевая ясность и отсутствие лишней запутанности.'},
      {name:'Уместность материала', weight:.07, score:sc(50 + o.R*30 + o.D*18), comment:'Отсутствие нерелевантных, избыточных или отвлекающих фрагментов.'}
    ];
    if(o.similarity){ arr.push({name:'Сравнение с модельным ответом', weight:0, score:o.similarity.total, comment:'Jaccard и длиновое сходство с загруженным эталоном.'}); }
    return arr.map(x => ({...x, zone: zoneByScore(x.score)}));
  }

  function renderResults(){
    $('#resultCount').textContent = state.results.length + ' файлов';
    if(!state.results.length){ $('#resultsList').className='k414-results empty-state'; $('#resultsList').textContent='Здесь появятся результаты анализа загруженных файлов.'; $('#summaryMetrics').innerHTML=''; return; }
    $('#resultsList').className='k414-results';
    const avg = state.results.reduce((s,r)=>s+r.grade100,0)/state.results.length;
    const avgA = state.results.reduce((s,r)=>s+r.auth,0)/state.results.length;
    const risk = state.results.filter(r => r.auth < .45).length;
    $('#summaryMetrics').innerHTML = [
      ['Файлов', state.results.length], ['Средняя оценка', fmt(avg,1)+'/100'], ['Средний Auth', fmt(avgA)], ['Риск', risk], ['DOCX', state.results.length]
    ].map(([k,v])=>`<div class="k414-metric"><span>${esc(k)}</span><b>${esc(v)}</b></div>`).join('');
    $('#resultsList').innerHTML = state.results.map((r,idx) => resultCard(r,idx)).join('');
    $$('.download-one').forEach(btn => btn.addEventListener('click', () => downloadOne(Number(btn.dataset.idx))));
    $('#downloadAllBtn').disabled = !state.results.length;
    $('#sendEmailBtn').disabled = !state.results.length;
  }

  function resultCard(r, idx){
    const level = r.auth >= .65 ? 'status-good' : r.auth >= .45 ? 'status-warn' : 'status-bad';
    const sim = r.similarity ? `<p><b>Сравнение с модельным ответом:</b> ${fmt(r.similarity.total,1)}/100; Jaccard=${fmt(r.similarity.jaccard)}, длиновое сходство=${fmt(r.similarity.lenRatio)}</p>` : '';
    return `<article class="result-card">
      <div class="result-head"><h3>${esc(r.fileName)}</h3><span class="file-pill ${level}">${esc(r.status)}</span></div>
      <div class="result-body">
        <div class="mini-metrics">
          <div class="mini-metric"><span>U / limU</span><b>${fmt(r.U)}</b></div>
          <div class="mini-metric"><span>I / limI</span><b>${fmt(r.I)}</b></div>
          <div class="mini-metric"><span>R / limR</span><b>${fmt(r.R)}</b></div>
          <div class="mini-metric"><span>D</span><b>${fmt(r.D)}</b></div>
          <div class="mini-metric"><span>λ</span><b>${fmt(r.lambda)}</b></div>
          <div class="mini-metric"><span>Δ / Auth</span><b>${fmt(r.delta)} / ${fmt(r.auth)}</b></div>
        </div>
        <p><b>Итог:</b> ${fmt(r.grade100,1)}/100; пятибалльная шкала: <b>${esc(r.grade5)}</b>. Объём: ${r.words} слов, ${r.chars} знаков.</p>
        ${sim}
        <p><b>Рекомендация:</b> ${esc(recommendation(r))}</p>
        <table class="criteria-table"><thead><tr><th>Критерий</th><th>Вес</th><th>Оценка</th><th>Зона</th><th>Комментарий</th></tr></thead><tbody>${r.criteria.map(c => `<tr class="${c.zone==='OK'?'zone-ok':c.zone==='риск'?'zone-bad':'zone-warn'}"><td>${esc(c.name)}</td><td>${c.weight ? Math.round(c.weight*100)+'%' : '—'}</td><td>${fmt(c.score,1)}</td><td>${esc(c.zone)}</td><td>${esc(c.comment)}</td></tr>`).join('')}</tbody></table>
        <details><summary>Показать извлечённый текст</summary><div class="preview-text">${esc(r.text.slice(0,2500))}${r.text.length>2500?'…':''}</div></details>
        <div class="download-actions"><button class="small-btn primary download-one" data-idx="${idx}">Скачать DOCX чек-лист</button></div>
      </div>
    </article>`;
  }

  function recommendation(r){
    if(r.auth >= .65 && r.grade100 >= 78) return 'Текст достаточно согласован: сохранить сильную структуру, точечно усилить доказательную базу и примеры.';
    if(r.R < .55) return 'Усилить фактическое содержание: добавить примеры, источники, данные, цитаты или конкретные наблюдения.';
    if(r.D < .55) return 'Усилить достаточное основание: ясно связать тезисы, аргументы и вывод.';
    if(r.U < .55) return 'Уточнить понятийное поле: убрать расплывчатые термины и ввести ключевые определения.';
    return 'Текст можно доработать: усилить главную идею, логику доказательства и композиционную завершённость.';
  }

  async function runAnalysis(){
    if(!state.files.length){ setStatus('Сначала загрузите один или несколько файлов .doc/.docx.', 'status-warn'); return; }
    if(typeof JSZip === 'undefined'){ setStatus('Не загружена JSZip. DOCX-анализ и создание DOCX невозможны. Подключите интернет или разместите локальную копию JSZip.', 'status-bad'); return; }
    setDelayTimer(); state.modalShown = false; state.results = []; state.zipBlob = null; renderResults();
    $('#downloadAllBtn').disabled = true; $('#sendEmailBtn').disabled = true;
    const ref = $('#referenceText').value || '';
    setStatus('Идёт извлечение текста и λ-анализ. Пожалуйста, дождитесь результата...'); progress(2);
    for(let i=0;i<state.files.length;i++){
      const file = state.files[i];
      try{
        progress(Math.round((i/state.files.length)*80)+5);
        const text = await extractText(file);
        if(words(text).length < 20) throw new Error('Слишком мало извлечённого текста для анализа.');
        state.results.push(analyzeText(text, file.name, ref));
        setStatus(`Обработан файл ${i+1}/${state.files.length}: <b>${esc(file.name)}</b>`);
      }catch(err){
        state.results.push(errorResult(file.name, err.message));
      }
      renderResults();
      await new Promise(res => setTimeout(res, 40));
    }
    clearDelayTimer(); progress(100);
    await buildGroupZip();
    setStatus(`Готово: обработано <b>${state.results.length}</b> файлов. Можно скачать DOCX по каждому файлу, общий ZIP или отправить ZIP на e-mail.`, 'status-good');
    renderResults();
  }

  function errorResult(fileName, msg){
    return {fileName, text:'', chars:0, words:0, unique:0, sentences:0, paragraphs:0, U:NaN,I:NaN,R:NaN,D:NaN,lambda:NaN,delta:Infinity,auth:0,Dst:NaN,grade100:0,grade5:'—',similarity:null, status:'ошибка чтения файла', criteria:[{name:'Чтение файла',weight:0,score:0,zone:'риск',comment:msg}], created:new Date().toLocaleString('ru-RU'), error:msg};
  }

  function acceptFiles(fileList){
    const arr = Array.from(fileList || []);
    const accepted = [], rejected = [];
    arr.forEach(f => { const n=f.name.toLowerCase(); if(n.endsWith('.doc') || n.endsWith('.docx')) accepted.push(f); else rejected.push(f.name); });
    state.files = accepted;
    state.results = []; state.zipBlob = null; renderResults();
    if(rejected.length) setStatus(`Отклонены неподдерживаемые файлы: ${rejected.map(esc).join(', ')}. Разрешены только .doc и .docx.`, 'status-warn');
    else setStatus(accepted.length ? `Загружено файлов: <b>${accepted.length}</b>. Нажмите «Запустить λ-анализ».` : 'Файлы не выбраны.');
  }

  function sampleDocxText(){
    return 'Научное знание отличается от обычного мнения тем, что оно требует доказательства, проверки и связи с фактами. Если человек просто уверен в своей мысли, этого недостаточно для науки. Научный текст должен показать, какие понятия используются, на какие наблюдения или данные он опирается и почему из них следует вывод. Поэтому качество научного текста зависит не только от красивого языка, но и от того, насколько главная идея, доказательства и факты согласованы между собой. В заключение можно сказать, что научность текста определяется связностью тезиса, фактической основы и достаточного основания.';
  }

  async function loadDemo(){
    const xml = createDocxXmlFromPlainText(sampleDocxText());
    const blob = await makeDocxBlob(xml);
    const file = new File([blob], 'demo_klt414.docx', {type:'application/vnd.openxmlformats-officedocument.wordprocessingml.document'});
    acceptFiles([file]);
  }

  async function downloadOne(idx){
    const r = state.results[idx]; if(!r) return;
    const blob = await createChecklistDocx(r);
    downloadBlob(blob, `${translit(r.fileName)}_lambda_check.docx`);
  }

  async function buildGroupZip(){
    if(typeof JSZip === 'undefined' || !state.results.length) return null;
    const zip = new JSZip();
    for(const r of state.results){
      const blob = await createChecklistDocx(r);
      zip.file(`${translit(r.fileName)}_lambda_check.docx`, blob);
    }
    zip.file('summary.csv', makeSummaryCsv(state.results));
    state.zipBlob = await zip.generateAsync({type:'blob'});
    return state.zipBlob;
  }

  function makeSummaryCsv(results){
    const rows = [['file','grade100','grade5','lambda','delta','auth','U','I','R','D','status']];
    results.forEach(r => rows.push([r.fileName, fmt(r.grade100,1), r.grade5, fmt(r.lambda), fmt(r.delta), fmt(r.auth), fmt(r.U), fmt(r.I), fmt(r.R), fmt(r.D), r.status]));
    return rows.map(row => row.map(v => '"'+String(v).replace(/"/g,'""')+'"').join(';')).join('\n');
  }

  async function downloadAll(){
    if(!state.results.length) return;
    if(!state.zipBlob) await buildGroupZip();
    downloadBlob(state.zipBlob, 'klt414_lambda_checks.zip');
  }

  async function sendEmail(){
    const email = ($('#emailInput').value || '').trim();
    if(!/^\S+@\S+\.\S+$/.test(email)){ setStatus('Введите корректный e-mail для отправки ZIP.', 'status-warn'); $('#emailInput').focus(); return; }
    if(!state.results.length){ setStatus('Сначала выполните анализ.', 'status-warn'); return; }
    if(!state.zipBlob) await buildGroupZip();
    const fd = new FormData();
    fd.append('email', email);
    fd.append('expert', $('#expertName').value || 'KLT 4.14 web');
    fd.append('group', $('#groupName').value || '');
    fd.append('summary', makeHumanSummary(state.results));
    fd.append('zip', state.zipBlob, 'klt414_lambda_checks.zip');
    setStatus('Отправляем ZIP на e-mail через серверный обработчик...');
    try{
      const resp = await fetch('../../api/k414mail.php', {method:'POST', body:fd});
      const data = await resp.json().catch(()=>({ok:false,error:'Сервер вернул не JSON'}));
      if(data.ok) setStatus(`ZIP чек-листов отправлен на <b>${esc(email)}</b>.`, 'status-good');
      else throw new Error(data.error || 'Почтовый обработчик не отправил письмо.');
    }catch(err){
      setStatus(`Не удалось отправить e-mail: ${esc(err.message)}. DOCX/ZIP уже созданы — скачайте ZIP вручную.`, 'status-bad');
      await downloadAll();
    }
  }

  function makeHumanSummary(results){
    return results.map(r => `${r.fileName}: ${fmt(r.grade100,1)}/100, λ=${fmt(r.lambda)}, Δ=${fmt(r.delta)}, Auth=${fmt(r.auth)}, статус=${r.status}`).join('\n');
  }

  function wText(text, bold=false, size=22, color='111827'){
    return `<w:r><w:rPr>${bold?'<w:b/>':''}<w:color w:val="${color}"/><w:sz w:val="${size}"/><w:szCs w:val="${size}"/></w:rPr><w:t xml:space="preserve">${xmlEsc(text)}</w:t></w:r>`;
  }
  function wP(text, opts={}){
    const jc = opts.jc ? `<w:jc w:val="${opts.jc}"/>` : '';
    const before = opts.before || 80, after = opts.after || 80;
    return `<w:p><w:pPr><w:spacing w:before="${before}" w:after="${after}"/>${jc}</w:pPr>${wText(text, !!opts.bold, opts.size || 22, opts.color || '111827')}</w:p>`;
  }
  function wCell(content, shade='', width='2500'){
    const sh = shade ? `<w:shd w:fill="${shade}"/>` : '';
    return `<w:tc><w:tcPr><w:tcW w:w="${width}" w:type="dxa"/>${sh}<w:tcMar><w:top w:w="70" w:type="dxa"/><w:left w:w="90" w:type="dxa"/><w:bottom w:w="70" w:type="dxa"/><w:right w:w="90" w:type="dxa"/></w:tcMar></w:tcPr>${content}</w:tc>`;
  }
  function wRow(cells){ return `<w:tr>${cells.join('')}</w:tr>`; }
  function wTable(rows){
    return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="6" w:color="9CA3AF"/><w:left w:val="single" w:sz="6" w:color="9CA3AF"/><w:bottom w:val="single" w:sz="6" w:color="9CA3AF"/><w:right w:val="single" w:sz="6" w:color="9CA3AF"/><w:insideH w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideV w:val="single" w:sz="4" w:color="D1D5DB"/></w:tblBorders></w:tblPr>${rows.join('')}</w:tbl>`;
  }
  function cellText(t,b=false,color='111827'){ return wP(t,{bold:b,size:20,color, before:20, after:20}); }

  function checklistDocumentXml(r){
    const headerRows = [wRow([
      wCell(wP('KLT 4.14',{bold:true,size:28,color:'FFFFFF'}) + wP('ИТОГОВЫЙ ЛЯМБДА-ЧЕК-ЛИСТ',{bold:true,size:24,color:'FFFFFF'}),'1F4E79','4200'),
      wCell(wP('DOCX / A4',{bold:true,size:24,color:'FFFFFF'}) + wP('Kurpishev Lambda Truth',{size:18,color:'FFFFFF'}),'1F4E79','2600'),
      wCell(wP('Дата',{bold:true,size:18,color:'FFFFFF'}) + wP(r.created,{size:18,color:'FFFFFF'}),'1F4E79','2600')
    ])];
    const passRows = [
      wRow([wCell(cellText('Проверяемая работа',true),'DBEAFE'),wCell(cellText(r.fileName),'FFFFFF','5000'),wCell(cellText('Тип',true),'DBEAFE'),wCell(cellText('DOC/DOCX λ-анализ'),'FFFFFF')]),
      wRow([wCell(cellText('Проверяющий',true),'DBEAFE'),wCell(cellText($('#expertName')?.value || 'KLT 4.14 web'),'FFFFFF','5000'),wCell(cellText('Группа',true),'DBEAFE'),wCell(cellText($('#groupName')?.value || 'не указана'),'FFFFFF')]),
      wRow([wCell(cellText('Объём',true),'DBEAFE'),wCell(cellText(`${r.words} слов / ${r.chars} знаков`),'FFFFFF','5000'),wCell(cellText('Статус',true),'DBEAFE'),wCell(cellText(r.status),'FFFFFF')])
    ];
    const reperRows = [
      wRow(['Компонент','Интерпретация','Значение'].map(x=>wCell(cellText(x,true,'FFFFFF'),'1F4E79'))),
      wRow([wCell(cellText('U / limU')),wCell(cellText('поле значений / понятия')),wCell(cellText(fmt(r.U)))]),
      wRow([wCell(cellText('I / limI')),wCell(cellText('ведущая идея')),wCell(cellText(fmt(r.I)))]),
      wRow([wCell(cellText('R / limR')),wCell(cellText('реальное содержание / факты')),wCell(cellText(fmt(r.R)))]),
      wRow([wCell(cellText('D')),wCell(cellText('достаточное основание')),wCell(cellText(fmt(r.D)))])
    ];
    const calcRows = [
      wRow(['λ','Δ = |λ + 1|','Auth','D*','100 / итог','5-балльная'].map(x=>wCell(cellText(x,true,'FFFFFF'),'1F4E79'))),
      wRow([wCell(cellText(fmt(r.lambda))),wCell(cellText(fmt(r.delta))),wCell(cellText(fmt(r.auth))),wCell(cellText(fmt(r.Dst))),wCell(cellText(fmt(r.grade100,1) + '/100')),wCell(cellText(r.grade5))])
    ];
    const critRows = [wRow(['Критерий','Вес','Оценка /100','Зона','Комментарий'].map(x=>wCell(cellText(x,true,'FFFFFF'),'1F4E79')))].concat(r.criteria.map(c => {
      const sh = c.zone === 'OK' ? 'DCFCE7' : c.zone === 'риск' ? 'FEE2E2' : 'FEF3C7';
      return wRow([wCell(cellText(c.name)), wCell(cellText(c.weight ? Math.round(c.weight*100)+'%' : '—')), wCell(cellText(fmt(c.score,1))), wCell(cellText(c.zone), sh), wCell(cellText(c.comment))]);
    }));
    const simRows = r.similarity ? [
      wRow([wCell(cellText('Итог сходства',true),'DBEAFE'), wCell(cellText(fmt(r.similarity.total,1)+'/100')), wCell(cellText('Jaccard',true),'DBEAFE'), wCell(cellText(fmt(r.similarity.jaccard)))]),
      wRow([wCell(cellText('Длиновое сходство',true),'DBEAFE'), wCell(cellText(fmt(r.similarity.lenRatio))), wCell(cellText('Корректировка',true),'DBEAFE'), wCell(cellText('учтена в итоговом балле'))])
    ] : [wRow([wCell(cellText('Модельный ответ не загружен. Сравнение не применялось.'))])];
    const recRows = [
      wRow([wCell(cellText('Рекомендация',true),'DBEAFE','2000'), wCell(cellText(recommendation(r)),'FFFFFF','7600')]),
      wRow([wCell(cellText('Статус результата',true),'DBEAFE','2000'), wCell(cellText('Демонстрационный рекомендательный лист. Не является автоматическим наказанием, официальной оценкой ученика или заменой эксперта.'),'FFFFFF','7600')])
    ];
    const commentsRows = [
      wRow([wCell(wP('Замечания проверяющего / собственная оценка',{bold:true,size:20}), 'FFFFFF','9600')]),
      wRow([wCell(wP(' ',{size:20,before:220,after:220}), 'FFFFFF','9600')]),
      wRow([wCell(wP('Дата: ____________________        Подпись проверяющего: ____________________        E-mail: ____________________',{size:18}), 'FFFFFF','9600')])
    ];
    let body = '';
    body += wTable(headerRows);
    body += wP('1. Паспорт проверки',{bold:true,size:24,color:'1F4E79'}); body += wTable(passRows);
    body += wP('2. Reпер, λ-расчёт и соответствие шкалам',{bold:true,size:24,color:'1F4E79'}); body += wTable(reperRows); body += wTable(calcRows);
    body += wP('Формула: λ=((U-R)(I-D))/((U-D)(I-R)); Δ=|λ+1|; Auth=1/(1+Δ). Гармонический предел: λ→-1.',{size:18,color:'374151'});
    body += wP('3. Сравнение с модельным ответом',{bold:true,size:24,color:'1F4E79'}); body += wTable(simRows);
    body += wP('4. Критерии оценки текста',{bold:true,size:24,color:'1F4E79'}); body += wTable(critRows);
    body += wP('5. Итог и замечания проверяющего',{bold:true,size:24,color:'1F4E79'}); body += wTable(recRows); body += wTable(commentsRows);
    body += wP('© 2026 Иван Борисович Курпишев. KLT / Kurpishev Lambda Truth, λ-метод, Reпер, PIX/PEAKS и лямбда-чек-лист являются авторскими материалами И.Б. Курпишева.',{size:16,color:'6B7280'});
    return wrapDocXml(body);
  }

  function createDocxXmlFromPlainText(text){
    const pars = String(text).split(/\n+/).filter(x=>x.trim()).map(p => wP(p,{size:22})).join('');
    return wrapDocXml(wP('KLT 4.14 demo input',{bold:true,size:28,color:'1F4E79'}) + pars);
  }
  function wrapDocXml(body){
    return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 wp14"><w:body>${body}<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="720" w:right="650" w:bottom="720" w:left="650" w:header="360" w:footer="360" w:gutter="0"/><w:cols w:space="720"/><w:docGrid w:linePitch="360"/></w:sectPr></w:body></w:document>`;
  }

  async function createChecklistDocx(r){ return makeDocxBlob(checklistDocumentXml(r)); }
  async function makeDocxBlob(documentXml){
    const zip = new JSZip();
    zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`);
    zip.folder('_rels').file('.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>`);
    zip.folder('word').file('document.xml', documentXml);
    zip.folder('word').folder('_rels').file('document.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>`);
    zip.folder('word').file('styles.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:cs="Arial"/><w:sz w:val="22"/></w:rPr></w:style></w:styles>`);
    const now = new Date().toISOString();
    zip.folder('docProps').file('core.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>KLT 4.14 Lambda Checklist</dc:title><dc:creator>Ivan B. Kurpishev / KLT web</dc:creator><cp:lastModifiedBy>KLT 4.14 web</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`);
    zip.folder('docProps').file('app.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>KLT 4.14 Web</Application></Properties>`);
    return zip.generateAsync({type:'blob', mimeType:'application/vnd.openxmlformats-officedocument.wordprocessingml.document'});
  }

  document.addEventListener('DOMContentLoaded', () => {
    const dz = $('#dropzone'); const inp = $('#fileInput');
    dz?.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag'); });
    dz?.addEventListener('dragleave', () => dz.classList.remove('drag'));
    dz?.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('drag'); acceptFiles(e.dataTransfer.files); });
    inp?.addEventListener('change', e => acceptFiles(e.target.files));
    $('#runBtn')?.addEventListener('click', runAnalysis);
    $('#demoBtn')?.addEventListener('click', loadDemo);
    $('#downloadAllBtn')?.addEventListener('click', downloadAll);
    $('#sendEmailBtn')?.addEventListener('click', sendEmail);
    $('#delayClose')?.addEventListener('click', hideDelay);
    $('#delayOk')?.addEventListener('click', hideDelay);
    if(typeof JSZip === 'undefined') setStatus('Внимание: JSZip пока не загружен. Если сообщение не исчезает, проверьте подключение CDN или разместите библиотеку локально.', 'status-warn');
  });
})();
