try {
/* ============================================================
   Карточки тренингов и подписи кнопок

   1. Номер из названия → data-bd-num="01":
        «1 СТУПЕНЬ: ПОВЫШЕНИЕ ВЫРУЧКИ»   → 01, название «Повышение выручки»
        «ШАГ 2: ПОВЫШЕНИЕ СТОИМОСТИ»     → 02, название «Повышение стоимости»
        «3. ФОРМИРОВАНИЕ КАЛЬКУЛЯТОРА»   → 03
      Номер берётся из смысла, а не из порядка в списке — не сбивается
      при перестановке и не появляется там, где его нет.
   2. Иконка по смыслу названия → data-bd-icon.
      Картинка карточки → --bd-card-img (из тренинга в GetCourse или CARD_IMAGES).
   3. Метка «Новое» по пометке в названии.
   Регистр названий не трогаем: заглавные бывают намеренно (аббревиатуры),
   капс правят в самом GetCourse.
   ============================================================ */

(function () {
  'use strict';

  /* Скрипт уже запущен (например, тема стоит и вставили предпросмотр) */
  if (window.bdCardsLoaded) return;
  window.bdCardsLoaded = true;

  var PREFIXES = [
    /^\s*(\d{1,2})\s*ступень\s*[:.–—-]?\s*/i,
    /^\s*шаг\s*(\d{1,2})\s*[:.–—-]?\s*/i,
    /^\s*блок\s*(\d{1,2})\s*[:.–—-]?\s*/i,
    /^\s*(\d{1,2})\s*[.)]\s+/
  ];

  var ICONS = [
    [/^\s*0\s*ступень|онбординг|старт|вводн/i, 'rocket'],
    [/выручк|доход|стоимост/i, 'growth'],
    [/клиент|продаж|маркетинг/i, 'clients'],
    [/команд|найм|сотрудник/i, 'team'],
    [/масштаб|управлен/i, 'bars'],
    [/совет|директор/i, 'crown'],
    [/баз[аы] знаний|библиотек/i, 'book'],
    [/тариф/i, 'ticket'],
    [/экосистем/i, 'crown'],
    [/стандартизац|регламент|процесс|систем/i, 'bars'],
    [/прибыль|ценообразован/i, 'growth'],
    [/собственник|наставник/i, 'team'],
    [/факультет|курс|школ/i, 'book']
  ];

  /* Картинки карточек. Главный способ — сотрудники ставят картинку
     в самом тренинге («В приложении → Изображение»): GetCourse отдаёт
     её в карточку, и она важнее этого списка. Список — запасной:
     id тренинга → адрес картинки. Лучше PNG на прозрачном фоне. */
  var CARD_IMAGES = {
    934216339: 'https://fs.getcourse.ru/fileservice/file/download/a/730847/sc/368/h/5f69a8bb2e3df6f8381cb795866faffd.png',  /* Экосистема */
    935517147: 'https://fs.getcourse.ru/fileservice/file/download/a/730847/sc/114/h/e458356813086bfee87303894bbd6bb6.png',  /* Факультет: стандартизация */
    935736549: 'https://fs.getcourse.ru/fileservice/file/download/a/730847/sc/89/h/4df121c2dd83046848a5b89df22405a0.png',   /* Факультет «Система в бухгалтерии» */
    934412727: 'https://fs.getcourse.ru/fileservice/file/download/a/730847/sc/253/h/b9d06fee5d34fa118683a982c5c8fa35.png',  /* Факультет: системный собственник */
    935584792: 'https://fs.getcourse.ru/fileservice/file/download/a/730847/sc/197/h/71a0096e17bfdc7706e6681fdae0466c.png'   /* Факультет «Прибыльная бухгалтерия» */
  };

  /* Картинку из хранилища GetCourse берём уменьшенной копией: оригинал
     1254px весит больше мегабайта, а в карточке он ~150px. Копию
     300px хранилище делает само, по адресу thumbnail/…/s/300x/… */
  function thumb(url, size) {
    size = size || '300x';
    var m = url.match(/^(https?:\/\/[^/]+)\/fileservice\/file\/download\/a\/(\d+)\/sc\/(\d+)\/h\/([^/?#]+)/);
    if (m) return m[1] + '/fileservice/file/thumbnail/h/' + m[4] + '/s/' + size + '/a/' + m[2] + '/sc/' + m[3];
    return url.replace(/(\/fileservice\/file\/thumbnail\/h\/[^/]+\/s\/)[^/]+/, '$1' + size);
  }

  /* Особые карточки: id тренинга → класс. Класс получает строка карточки
     (tr), стили для него — в 04-cards.css. Так одну карточку можно
     оформить иначе, не трогая остальные. Доступные классы:
       bd-card-featured  главная программа: на всю строку, фиолетовая */
  var CARD_CLASSES = {
    934216339: 'bd-card-featured'   /* Экосистема «Стандарты бухгалтерского бизнеса» */
  };

  /* Тексты флагманской карточки (bd-card-featured): надпись над
     названием, строка под ним и плашки-факты. Число уроков GetCourse
     подставляет сам — оно встаёт первой плашкой. */
  var FEATURED = {
    934216339: {
      eyebrow: 'Флагманская программа',
      lead: 'Среда, где собственники бух. компаний и частные бухгалтеры получают не просто знания, а готовые бизнес-решения, инструменты и поддержку для роста.',
      facts: ['4 ступени', 'Совет директоров', 'Чаты и эфиры с экспертами', 'Бухразборы']
    }
  };

  function featured(tr, a) {
    var cfg = FEATURED[tr.getAttribute('data-training-id')];
    if (!cfg || a.querySelector('.bd-feat__eyebrow')) return;
    var title = a.querySelector('.stream-title');
    var count = a.querySelector(':scope > div b');

    var eyebrow = document.createElement('span');
    eyebrow.className = 'bd-feat__eyebrow';
    eyebrow.textContent = cfg.eyebrow;
    a.insertBefore(eyebrow, a.firstChild);

    var lead = document.createElement('span');
    lead.className = 'bd-feat__lead';
    lead.textContent = typo(cfg.lead);
    if (title) title.parentNode.insertBefore(lead, title.nextSibling);

    var facts = document.createElement('span');
    facts.className = 'bd-feat__facts';
    (count ? [count.textContent.trim().replace(/\.$/, '')] : []).concat(cfg.facts).forEach(function (text) {
      var chip = document.createElement('span');
      chip.textContent = text;
      facts.appendChild(chip);
    });
    lead.parentNode.insertBefore(facts, lead.nextSibling);
  }

  var ORDINALS = ['Нулевая', 'Первая', 'Вторая', 'Третья', 'Четвёртая', 'Пятая'];

  /* ── Метка «Новое» ─────────────────────────────────────────
     Сотрудник помечает новый тренинг или урок прямо в названии:
     ставит в начале или в конце &#127381;, [new] или #new — регистр не важен.
     Скрипт убирает пометку из текста и рисует плашку «Новое». Там, где
     скрипта нет (приложение, письма), видно &#127381; — тоже понятно.
     Когда урок перестал быть новым, пометку просто стирают.
     Эмодзи в коде — только кодом \uD83C\uDD95: GetCourse при сохранении
     темы заменяет его на &#127381;, и пометка переставала находиться. */
  var NEW_RE = /(^|\s)(\uD83C\uDD95|\[new\]|#new)(?=\s|$)/gi;

  function hasNew(text) {
    NEW_RE.lastIndex = 0;
    return NEW_RE.test(text);
  }

  function stripNew(text) {
    return String(text).replace(NEW_RE, ' ').replace(/\s{2,}/g, ' ').trim();
  }

  /* Типографика: короткое слово (в, от, и, на, для, без, бух.) не остаётся в конце
     строки — склеиваем его со следующим неразрывным пробелом. Перед тире —
     тоже неразрывный: тире не начинает строку */
  function typo(text) {
    var out = String(text);
    for (var i = 0; i < 2; i++) {   /* дважды — для цепочек «и в» */
      out = out.replace(/(^|[\s«("\u00a0])([А-Яа-яЁё]{1,2}|для|без|при|про|над|под|из-за|бух\.)\s+/g, '$1$2\u00a0');
    }
    return out.replace(/\s+—/g, '\u00a0—');
  }

  /* Для скриптов темы тренингов (next-section.js), поиска и path.js */
  window.bdText = { hasNew: hasNew, stripNew: stripNew, typo: typo };

  function decorate(tr) {
    var t = tr.querySelector('.stream-title');
    if (!t) return;
    if (hasNew(t.textContent)) {
      tr.classList.add('bd-new');
      t.textContent = stripNew(t.textContent);
    }
    var title = t.textContent.trim();

    /* «Вводный блок» идёт перед 01 — даём ему 00, чтобы ряд номеров не рвался */
    if (/^\s*вводн/i.test(title)) tr.setAttribute('data-bd-num', '00');

    for (var i = 0; i < PREFIXES.length; i++) {
      var m = title.match(PREFIXES[i]);
      if (m) {
        tr.setAttribute('data-bd-num', ('0' + m[1]).slice(-2));
        var rest = title.slice(m[0].length).replace(/[.\s]+$/, '');
        if (!rest && i === 0) rest = (ORDINALS[Number(m[1])] || m[1]) + ' ступень';
        if (rest) t.textContent = rest;
        break;
      }
    }

    for (var j = 0; j < ICONS.length; j++) {
      if (ICONS[j][0].test(title)) {
        tr.setAttribute('data-bd-icon', ICONS[j][1]);
        break;
      }
    }


    var extra = CARD_CLASSES[tr.getAttribute('data-training-id')];
    if (extra) tr.classList.add(extra);
    if (extra === 'bd-card-featured') featured(tr, t.parentNode);

    var img = tr.getAttribute('data-training-image') || CARD_IMAGES[tr.getAttribute('data-training-id')];
    if (img) {
      tr.classList.add('bd-has-img');
      tr.style.setProperty('--bd-card-img', 'url("' + thumb(img, extra === 'bd-card-featured' ? '600x' : '').replace(/"/g, '%22') + '")');
    }

    var b = tr.querySelector('td > a > div b');
    if (b) b.textContent = b.textContent.replace(/\.\s*$/, '');
  }

  /* Режим редактирования страницы: там заголовки правят прямо на месте,
     и убранная пометка «Новое» сохранилась бы в GetCourse */
  function isEditing() {
    return /[?&]editMode=1/.test(location.search) || !!document.querySelector('[contenteditable="true"]');
  }

  /* Пометку убираем из текстовых узлов, не трогая вложенные элементы
     (у названия урока внутри — метка «просмотрено») */
  function stripNewIn(el) {
    var found = false;
    Array.prototype.forEach.call(el.childNodes, function (node) {
      if (node.nodeType === 3 && hasNew(node.nodeValue)) {
        node.nodeValue = ' ' + stripNew(node.nodeValue) + ' ';
        found = true;
      }
    });
    return found;
  }

  function markNewLessons() {
    var titles = document.querySelectorAll('.gc-main-content .lesson-list > li .title');
    Array.prototype.forEach.call(titles, function (title) {
      if (stripNewIn(title)) {
        var li = title.closest('li');
        if (li) li.classList.add('bd-new');
      }
    });
    var heads = document.querySelectorAll(
      '.gc-main-content .lesson-title-value, .gc-main-content .page-header h1, ' +
      '.gc-main-content .breadcrumbs a, .gc-main-content .breadcrumb a, ' +
      '.gc-main-content .lesson-navigation td > div'
    );
    Array.prototype.forEach.call(heads, stripNewIn);
  }

  function init() {
    if (isEditing()) return;
    markNewLessons();
    var rows = document.querySelectorAll('.gc-main-content .stream-table tr[data-training-id]');
    Array.prototype.forEach.call(rows, decorate);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

} catch (e) { if (window.console) console.error('[bd] cards.js', e); }
try {
/* ============================================================
   Путь участника

   Рисует путь «онбординг → 1–3 ступени → Совет директоров →
   4 ступень → стратегические сессии» с прогрессом по ступеням.

   Откуда данные. Готового прогресса по вложенным тренингам в
   GetCourse нет. Скрипт открывает страницы ступени и всех вложенных
   тренингов (ступень → шаг → тема) тем же запросом, что и браузер
   ученика, и считает уроки по штатным классам состояний:
     считаем только уроки с заданием:
     li.user-state-accomplished  — задание принято, урок пройден
     has_mission / need_accomplish / answered — не пройден
     уроки без задания в процент не входят
   Результат кэшируется в localStorage, повторный обход — не чаще
   раза в CACHE_TTL. Пока идёт обход, показывается прошлый результат.

   Почему путь не заканчивается на 100%. Экосистема — подписка без финала.
   Шкала, упёршаяся в 100%, говорит «всё, закончил» прямо перед
   продлением. Поэтому после 3 ступени фокус уходит на следующую цель.

   Где выводится — только страница тренинга «Экосистема»:
     баннер с фактами о программе, под ним дорожная карта, статусы
     в карточках ступеней, справа «Ваш прогресс» с кнопкой
     «Продолжить».
   ============================================================ */

(function () {
  'use strict';

  /* Скрипт уже запущен (например, тема стоит и вставили предпросмотр) */
  if (window.bdPathLoaded) return;
  window.bdPathLoaded = true;

  /* ── Настройки ─────────────────────────────────────────── */

  var ECOSYSTEM_TID = 934216339;

  /* Этапы пути по порядку.
     tid      — id тренинга; null — этап ещё не заведён, в пути не показывается
     count    — считать прогресс по урокам
     skipRoot — не считать уроки, лежащие прямо на странице ступени
                (там «Чат с наставником» и «Кейсы» — это не учебные уроки)
     requires — этап открывается после прохождения перечисленных
     member   — пройден по полю пользователя GetCourse, а не по урокам.
                На странице блок HTML, в который GetCourse подставляет поля:
                <div data-bd-board="{В совете директоров?}" data-bd-business="{Бизнес тариф}"></div>
                или блок data-bd-board="true" с «Видимостью» по полю.
                member: 'board' — data-bd-board, 'business' — data-bd-business
     onlyMembers — без поля этап закрыт (4 ступень — только на Бизнес-тарифе)
     memberNote / memberWide — подписи в карточке ступени и в стрелке карты
     tariff   — подпись для этапов только на отдельном тарифе */
  var STAGES = [
    { key: 'onboarding', title: 'Онбординг', tid: null, count: true },
    { key: 's1', title: '1 ступень', subtitle: 'Повышение выручки', tid: 934224751, count: true, skipRoot: true },
    { key: 's2', title: '2 ступень', subtitle: 'Новые клиенты', tid: 934224817, count: true, skipRoot: true },
    { key: 's3', title: '3 ступень', subtitle: 'Эффективная команда', tid: 934224824, count: true, skipRoot: true },
    { key: 'board', title: 'Совет директоров', subtitle: 'Сообщество собственников', tid: 934640570, member: 'board', milestone: true, requires: ['s1', 's2', 's3'],
      memberNote: 'Вы участник', memberWide: 'Вы в Совете директоров',
      roadmap: 'Сообщество собственников бухгалтерского бизнеса и группы советов директоров' },
    { key: 's4', title: '4 ступень', subtitle: 'Управление и масштабирование', tid: 934640666, member: 'business', onlyMembers: true, tariff: 'Бизнес-тариф',
      memberNote: 'Бизнес-тариф подключён', memberWide: 'Бизнес-тариф подключён',
      roadmap: 'Закрытое сообщество собственников бухгалтерского бизнеса. Переход от базовых инструментов к стратегическому масштабированию' },
    { key: 'sessions', title: 'Стратегические сессии', subtitle: 'с Екатериной Сидиченко', tid: null, tariff: 'Бизнес-тариф' }
  ];

  /* Какие этапы входят в общий процент на кольце. Только обязательная
     часть пути до Совета директоров — 4 ступень есть не у всех. */
  var RING_KEYS = ['s1', 's2', 's3'];

  /* Какие этапы — строки дорожной карты. У ступеней 1–3 шаги берутся
     из вложенных тренингов («ШАГ 1. …»), у остальных — текст roadmap */
  var ROADMAP_KEYS = ['s1', 's2', 's3', 'board', 's4'];

  var CACHE_KEY = 'bd-path-v4:' + (window.accountUserId || 'guest');
  var CACHE_TTL = 30 * 60 * 1000;
  var LOCK_KEY = CACHE_KEY + ':crawling';
  var LOCK_TTL = 60 * 1000;  /* обход занимает ~10 с; зависшую метку игнорируем через минуту */
  var MAX_DEPTH = 4;
  /* Восемь страниц разом: время обхода — это ожидание ответа GetCourse
     (~0,4 с на страницу), сами страницы приходят быстро. Замер: 12 страниц
     по одной — 4,8 с, по шесть — 1,3 с, по двенадцать — 0,9 с */
  var CONCURRENCY = 8;

  /* ── Обход тренингов ───────────────────────────────────── */

  var queue = [];
  var running = 0;

  function limited(task) {
    return new Promise(function (resolve, reject) {
      queue.push(function () {
        running++;
        task().then(resolve, reject).finally(function () {
          running--;
          if (queue.length) queue.shift()();
        });
      });
      if (running < CONCURRENCY) queue.shift()();
    });
  }

  function fetchTraining(tid) {
    return limited(function () {
      return fetch('/teach/control/stream/view/id/' + tid, { credentials: 'same-origin' })
        .then(function (r) {
          /* 429/500 — сбой, а не «нет доступа»: обход прерываем, кэш не трогаем */
          /* Нет доступа (403/404 — например, 4 ступень без Бизнес-тарифа) — этап закрыт,
             а не сбой: раньше такой ответ обрывал весь подсчёт, и у ученика
             пропадали карта и прогресс */
          if (r.status === 401 || r.status === 403 || r.status === 404) return { url: '', html: '' };
          if (!r.ok) throw new Error('HTTP ' + r.status);
          return r.text().then(function (html) {
            return { url: r.url, html: html };
          });
        });
    }).then(function (res) {
      var doc = new DOMParser().parseFromString(res.html, 'text/html');
      var hasContent = doc.querySelector('.lesson-list, .stream-table');
      var accessible = res.url.indexOf(String(tid)) !== -1 && !!hasContent;

      /* Считаем только уроки с заданием: «пройден» для урока — принятый
         ответ. Отметку «просмотрено» с чужой страницы не увидеть — её
         ставит скрипт GetCourse уже после загрузки, а метка в разметке
         есть у каждого урока (раньше из-за неё все уроки считались
         просмотренными и проценты были завышены). */
      var lessons = Array.prototype.filter.call(doc.querySelectorAll('.lesson-list > li'), function (li) {
        return /\buser-state-(has_mission|need_accomplish|answered|accomplished)\b/.test(li.className);
      }).map(function (li) {
        /* state: done — принято, review — ответ на проверке, todo — не сдано.
           Название — прямой текст .link.title, без метки «просмотрено» */
        var t = li.querySelector('.link.title');
        var title = t ? Array.prototype.filter.call(t.childNodes, function (n) { return n.nodeType === 3; })
          .map(function (n) { return n.textContent; }).join(' ').replace(/\s+/g, ' ').trim() : '';
        return {
          id: li.getAttribute('data-lesson-id') || (li.className.match(/lesson-id-(\d+)/) || [])[1] || '',
          title: title.slice(0, 120),
          state: /\buser-state-accomplished\b/.test(li.className) ? 'done'
            : /\buser-state-answered\b/.test(li.className) ? 'review' : 'todo'
        };
      });

      var children = Array.prototype.map.call(doc.querySelectorAll('.stream-table tr[data-training-id]'), function (tr) {
        var title = tr.querySelector('.stream-title');
        return {
          tid: Number(tr.getAttribute('data-training-id')),
          title: title ? title.textContent.trim() : '',
          locked: /\bno-public\b/.test(tr.className)
        };
      });

      return { accessible: accessible, lessons: lessons, children: children };
    });
  }

  /* Возвращает { total, done, accessible, pending, parts: [{title, total, done, pending}] }
     pending — уроки с заданием, которое ещё не принято: [{id, title, state}] */
  function measure(tid, skipRoot, depth, visited) {
    depth = depth || 0;
    visited = visited || {};
    if (visited[tid]) return Promise.resolve({ accessible: true, total: 0, done: 0, pending: [], parts: [] });
    visited[tid] = true;

    return fetchTraining(tid).then(function (page) {
      /* Нет доступа — GetCourse перенаправил на другую страницу (часто на
         список тренингов). Её вложенные тренинги не наши — не обходим. */
      if (!page.accessible) return { accessible: false, total: 0, done: 0, pending: [], parts: [] };

      var own = skipRoot && depth === 0 ? [] : page.lessons;
      var result = {
        accessible: page.accessible,
        total: own.length,
        done: own.filter(function (l) { return l.state === 'done'; }).length,
        pending: own.filter(function (l) { return l.state !== 'done'; }),
        parts: []
      };
      if (depth >= MAX_DEPTH) return result;

      /* Закрытые ученику вложенные тренинги не открываем — их страница
         недоступна. Но в шаги дорожной карты они идут, с пометкой locked */
      return Promise.all(page.children.map(function (child) {
        if (child.locked) {
          return { tid: child.tid, title: child.title, total: 0, done: 0, pending: [], locked: true };
        }
        return measure(child.tid, false, depth + 1, visited).then(function (m) {
          m.title = child.title;
          m.tid = child.tid;
          return m;
        });
      })).then(function (parts) {
        parts.forEach(function (p) {
          result.total += p.total;
          result.done += p.done;
          result.pending = result.pending.concat(p.pending || []);
        });
        if (depth === 0) {
          result.parts = parts.map(function (p) {
            return { tid: p.tid, title: p.title, total: p.total, done: p.done, pending: p.pending || [], locked: !!p.locked };
          });
        }
        return result;
      });
    });
  }

  function collect() {
    var visited = {};
    var jobs = STAGES.filter(function (s) { return s.tid; }).map(function (s) {
      if (s.count) {
        return measure(s.tid, s.skipRoot, 0, visited).then(function (m) { return [s.key, m]; });
      }
      return fetchTraining(s.tid).then(function (p) {
        return [s.key, { accessible: p.accessible, total: 0, done: 0, pending: [], parts: [] }];
      });
    });

    return Promise.all([Promise.all(jobs), ecosystemLessons()]).then(function (res) {
      var data = {};
      res[0].forEach(function (p) { data[p[0]] = p[1]; });
      return { at: Date.now(), stages: data, lessons: res[1] };
    });
  }

  /* Сколько уроков в «Экосистеме» — ровно как пишет GetCourse в её
     карточке на списке тренингов («687 уроков»): баннер и карточка
     должны показывать одно число. Не вышло — баннер посчитает сам */
  function ecosystemLessons() {
    return limited(function () {
      return fetch('/teach/control', { credentials: 'same-origin' }).then(function (r) { return r.text(); });
    }).then(function (html) {
      var doc = new DOMParser().parseFromString(html, 'text/html');
      var b = doc.querySelector('tr[data-training-id="' + ECOSYSTEM_TID + '"] td > a > div b');
      var m = b && b.textContent.replace(/\s+/g, '').match(/^(\d+)/);
      return m ? Number(m[1]) : null;
    }).catch(function () { return null; });
  }

  /* ── Кэш ───────────────────────────────────────────────── */

  function safeGet(key) {
    try { return localStorage.getItem(key); } catch (e) { return null; }
  }

  function safeSet(key, value) {
    try { localStorage.setItem(key, value); } catch (e) { /* приватный режим */ }
  }

  function safeRemove(key) {
    try { localStorage.removeItem(key); } catch (e) { /* приватный режим */ }
  }

  function readCache() {
    try {
      return JSON.parse(localStorage.getItem(CACHE_KEY));
    } catch (e) {
      return null;
    }
  }

  function writeCache(value) {
    try {
      localStorage.setItem(CACHE_KEY, JSON.stringify(value));
    } catch (e) { /* приватный режим — просто без кэша */ }
  }

  /* ── Модель пути ───────────────────────────────────────── */

  /* Поле пользователя: значение, подставленное GetCourse в data-bd-board
     (data-bd-business). Блоков может быть несколько — хватит одного «да» */
  function hasField(name) {
    return Array.prototype.some.call(document.querySelectorAll('[data-bd-' + name + ']'), function (el) {
      return /^\s*(1|true|да|yes)\s*$/i.test(el.getAttribute('data-bd-' + name));
    });
  }

  function percent(m) {
    return m && m.total ? Math.round((m.done / m.total) * 100) : 0;
  }

  function isComplete(m) {
    return !!m && m.total > 0 && m.done >= m.total;
  }

  function buildModel(snapshot) {
    var data = snapshot.stages;
    var visible = STAGES.filter(function (s) { return s.tid; });
    var currentFound = false;

    var nodes = visible.map(function (s) {
      var m = data[s.key] || { accessible: false, total: 0, done: 0, parts: [] };
      var node = {
        key: s.key,
        tid: s.tid,
        title: s.title,
        subtitle: s.subtitle || '',
        tariff: s.tariff || '',
        roadmap: s.roadmap || '',
        milestone: !!s.milestone,
        total: m.total,
        done: m.done,
        pct: percent(m),
        parts: m.parts || [],
        state: 'todo',
        memberNote: s.memberNote || '',
        memberWide: s.memberWide || ''
      };

      var requirementsMet = (s.requires || []).every(function (k) { return isComplete(data[k]); });

      node.member = !!s.member && hasField(s.member);
      if (node.member) {
        node.state = 'done';
      } else if (s.onlyMembers || (!m.accessible && !s.member)) {
        node.state = 'locked';
      } else if (s.count && isComplete(m)) {
        node.state = 'done';
      } else if (s.milestone && !requirementsMet) {
        node.state = 'waiting';
      } else if (!currentFound) {
        node.state = 'current';
        currentFound = true;
      }
      return node;
    });

    var current = nodes.filter(function (n) { return n.state === 'current'; })[0] || null;
    var ring = nodes.filter(function (n) { return RING_KEYS.indexOf(n.key) !== -1; });
    var ringTotal = ring.reduce(function (a, n) { return a + n.total; }, 0);
    var ringDone = ring.reduce(function (a, n) { return a + n.done; }, 0);

    return {
      nodes: nodes,
      current: current,
      ringTotal: ringTotal,
      ringDone: ringDone,
      ringPct: ringTotal ? Math.round((ringDone / ringTotal) * 100) : 0,
      lessons: snapshot.lessons || null
    };
  }

  /* ── Разметка ──────────────────────────────────────────── */

  function esc(s) {
    return String(s).replace(/[&<>"]/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
    });
  }

  function link(tid) {
    return '/teach/control/stream/view/id/' + tid;
  }

  function plural(n, one, few, many) {
    var m10 = n % 10, m100 = n % 100;
    if (m10 === 1 && m100 !== 11) return one;
    if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return few;
    return many;
  }

  function lessons(n) {
    return n + ' ' + plural(n, 'урок', 'урока', 'уроков');
  }

  /* Прогресс считаем по заданиям, и подписываем его заданиями: «уроков»
     — это всегда число уроков, как в карточках GetCourse. Иначе на одной
     странице выходило «48 уроков» в карточке и «4 из 30 уроков» рядом */
  function tasks(n) {
    return n + ' ' + plural(n, 'задания', 'заданий', 'заданий');
  }

  /* Уроков в программе — как их считает сам GetCourse: сумма чисел
     в карточках модулей на странице и уроки прямо на ней */
  function pageLessonCount() {
    var sum = 0;
    Array.prototype.forEach.call(document.querySelectorAll('.gc-main-content .stream-table tr[data-training-id] td > a > div b'), function (b) {
      var m = b.textContent.replace(/\s+/g, '').match(/^(\d+)/);
      if (m) sum += Number(m[1]);
    });
    return sum + document.querySelectorAll('.gc-main-content .lesson-list > li').length;
  }

  function ringHtml(model) {
    return (
      '<span class="bd-ring" style="--bd-ring-pct:' + model.ringPct + '" role="img" aria-label="Пройдено ' + model.ringPct + '%">' +
        '<span class="bd-ring__value">' + model.ringPct + '%</span>' +
      '</span>'
    );
  }

  /* Подзаголовок баннера — как на сайте sistembuh.ru */
  var HERO_LEAD = 'Среда, где собственники бух. компаний и частные бухгалтеры получают не просто знания, а готовые бизнес-решения, инструменты и поддержку для роста.';

  /* Типографика — из cards.js (тот же файл темы), без неё — как есть */
  function heroLead() {
    return window.bdText && window.bdText.typo ? window.bdText.typo(HERO_LEAD) : HERO_LEAD;
  }

  /* Баннер: название, факты плашками; справа — строка о программе.
     Кнопка «Продолжить» и прогресс — в карточке справа */
  function heroHtml(model) {
    var total = model.lessons || pageLessonCount();
    var steps = model.nodes.filter(function (n) { return /ступень/.test(n.title); }).length;
    return (
      '<section class="bd-hero" aria-label="Флагманская программа">' +
        '<div class="bd-hero__body">' +
          '<div class="bd-hero__eyebrow">Флагманская программа</div>' +
          '<h1 class="bd-hero__title">Экосистема «Стандарты бухгалтерского бизнеса»</h1>' +
          '<p class="bd-hero__lead">' + heroLead() + '</p>' +
          '<ul class="bd-hero__facts">' +
            (total ? '<li class="is-lessons">' + lessons(total).replace(/^(\d+)/, '<b>$1</b>') + '</li>' : '') +
            (steps ? '<li class="is-steps"><b>' + steps + '</b> ' + plural(steps, 'ступень', 'ступени', 'ступеней') + '</li>' : '') +
            '<li class="is-board">Совет директоров</li>' +
            '<li class="is-chats">Чаты и эфиры с экспертами</li>' +
            '<li class="is-cases">Бухразборы</li>' +
          '</ul>' +
        '</div>' +
      '</section>'
    );
  }

  /* «Ваш прогресс» — верх правой колонки: кольцо, где я сейчас,
     «Продолжить» */
  function progressCardHtml(model) {
    if (!model.ringTotal) return '';
    var c = model.current;
    var now = '';
    if (c && c.milestone) {
      now = 'Следующая цель<b>' + esc(c.title) + '</b>';
    } else if (c) {
      now = 'Сейчас<b>' + esc(c.title) + (c.subtitle ? ' · ' + esc(c.subtitle) : '') + '</b>' +
        (c.total ? c.done + ' из ' + tasks(c.total) : '');
    }
    return (
      '<div class="bd-progress-card">' +
        '<div class="bd-progress-card__title">Ваш прогресс</div>' +
        ringHtml(model) +
        '<div class="bd-progress-card__caption">Выполнено на 1–3 ступенях<b>' + model.ringDone + ' из ' + tasks(model.ringTotal) + '</b></div>' +
        (now ? '<div class="bd-progress-card__now">' + now + '</div>' : '') +
        '<a class="bd-progress-card__cta" href="' + link(c ? c.tid : ECOSYSTEM_TID) + '">' +
          (c && c.milestone ? 'Узнать, как вступить' : 'Продолжить') +
        '</a>' +
        debtsCardHtml(model) +
        '<ul class="bd-progress-card__legend">' +
          '<li><span>До Совета директоров</span><b>' + tasks(model.ringTotal - model.ringDone) + '</b></li>' +
        '</ul>' +
        '<details class="bd-progress-card__how">' +
          '<summary>Как считается прогресс</summary>' +
          '<p>Считаются уроки с заданием: урок пройден, когда куратор принял ваш ответ. ' +
          'Отправленный ответ засчитается после проверки. Уроки без задания в процент не входят — ' +
          'смотрите их в своём темпе.</p>' +
          '<p>Фиолетовая точка с числом на шаге карты — сколько заданий в начатом шаге ещё не сдано. ' +
          'Нажмите на неё, чтобы увидеть уроки и перейти к ним.</p>' +
          '<p>Процент на кольце — по 1–3 ступеням: это путь до Совета директоров. ' +
          'Данные обновляются раз в полчаса.</p>' +
        '</details>' +
      '</div>'
    );
  }

  /* ── Дорожная карта ─────────────────────────────────────
     Ступени строками, шаги — стрелками, как на прежней картинке
     «Путь развития участника», но живая: пройденный шаг отмечен,
     текущий подсвечен, каждый шаг — ссылка на свой тренинг.
     Шаг пройден, когда приняты все задания в его уроках. */

  var STEP_PREFIX = /^\s*шаг\s*(\d{1,2})\s*[:.–—-]?\s*/i;

  /* ── Несданные задания ─────────────────────────────────
     Только по начатым шагам (есть принятое или отправленное задание):
     иначе точка горела бы на каждом шаге впереди. todo — не сдано,
     review — ответ отправлен и ждёт проверки куратора */
  function stepDebts(p) {
    var pending = p.pending || [];
    var todo = pending.filter(function (l) { return l.state === 'todo'; });
    var review = pending.filter(function (l) { return l.state === 'review'; });
    if (!p.done && !review.length) return null;
    if (!todo.length && !review.length) return null;
    return { todo: todo, review: review };
  }

  function lessonLinks(list) {
    return '<ul>' + list.map(function (l) {
      return '<li>' + (l.id ? '<a href="/teach/control/lesson/view/id/' + esc(l.id) + '">' + esc(l.title || 'Урок') + '</a>'
        : '<span>' + esc(l.title || 'Урок') + '</span>') + '</li>';
    }).join('') + '</ul>';
  }

  function debtsListHtml(d) {
    return (d.todo.length ? '<div class="bd-debts__title">Не сдано · ' + d.todo.length + '</div>' + lessonLinks(d.todo) : '') +
      (d.review.length ? '<div class="bd-debts__title is-review">На проверке · ' + d.review.length + '</div>' +
        '<div class="is-review">' + lessonLinks(d.review) + '</div>' : '');
  }

  /* Точка на шаге с числом несданных и окошко со списком уроков */
  function debtsHtml(p, d) {
    var id = 'bd-debts-' + p.tid;
    var n = d.todo.length || d.review.length;
    var label = d.todo.length ? 'Не сдано заданий: ' + n : 'На проверке: ' + n;
    return '<button type="button" class="bd-roadmap__dot' + (d.todo.length ? '' : ' is-review') + '" aria-expanded="false" aria-controls="' + id + '" title="' + label + '" aria-label="' + label + '">' + n + '</button>' +
      '<div class="bd-debts" id="' + id + '" hidden>' + debtsListHtml(d) + '</div>';
  }

  /* Сводка для «Ваш прогресс»: все начатые шаги с несданными заданиями */
  function debtsCardHtml(model) {
    var groups = [];
    var todo = 0;
    var review = 0;
    model.nodes.forEach(function (n) {
      if (ROADMAP_KEYS.indexOf(n.key) === -1 || n.state === 'locked') return;
      n.parts.forEach(function (p) {
        if (p.locked || !STEP_PREFIX.test(p.title)) return;
        var d = stepDebts(p);
        if (!d) return;
        todo += d.todo.length;
        review += d.review.length;
        var m = p.title.match(STEP_PREFIX);
        groups.push('<div class="bd-debts__group"><div class="bd-debts__step">Шаг ' + m[1] + ' · ' +
          esc(p.title.replace(STEP_PREFIX, '')) + '</div>' + debtsListHtml(d) + '</div>');
      });
    });
    if (!groups.length) return '';
    return '<details class="bd-progress-card__debts">' +
      '<summary>' + (todo ? 'Несданные задания<b>' + todo + '</b>' : 'Задания на проверке<b>' + review + '</b>') + '</summary>' +
      '<div class="bd-debts is-inline">' + groups.join('') + '</div>' +
    '</details>';
  }

  function roadmapHtml(model) {
    var rows = model.nodes.filter(function (n) { return ROADMAP_KEYS.indexOf(n.key) !== -1; });
    if (!rows.length) return '';

    var counter = 0;
    var currentMarked = false;

    var rowsHtml = rows.map(function (n) {
      var locked = n.state === 'locked';
      var steps = (n.roadmap ? [] : n.parts).filter(function (p) { return STEP_PREFIX.test(p.title); });
      /* У ступени без шагов (Совет директоров, 4 ступень) её описание —
         широкой стрелкой справа; на узком экране оно встаёт внутрь
         карточки ступени (bd-roadmap__stage-desc, 15-roadmap.css) */
      var desc = steps.length ? '' : (n.roadmap || n.subtitle);
      var stage =
        '<a class="bd-roadmap__stage" href="' + link(n.tid) + '">' +
          '<span class="bd-roadmap__stage-num">' + esc(n.title) + '</span>' +
          '<span class="bd-roadmap__stage-name">' + esc(n.subtitle) + '</span>' +
          (desc && desc !== n.subtitle ? '<span class="bd-roadmap__stage-desc">' + esc(desc) + '</span>' : '') +
          (locked && n.tariff ? '<span class="bd-roadmap__stage-meta">' + esc(n.tariff) + '</span>'
            : n.state === 'waiting' ? '<span class="bd-roadmap__stage-meta">После 1–3 ступеней</span>'
            : n.member ? '<span class="bd-roadmap__stage-meta">' + n.memberNote + '</span>'
            : n.total ? '<span class="bd-roadmap__stage-meta">' + n.done + ' из ' + tasks(n.total) + '</span>' : '') +
        '</a>';

      var body;
      if (steps.length) {
        body = '<ol class="bd-roadmap__steps">' + steps.map(function (p) {
          var m = p.title.match(STEP_PREFIX);
          var num = m ? Number(m[1]) : ++counter;
          var state = 'todo';
          if (locked || p.locked) state = 'locked';
          else if (p.total && p.done >= p.total) state = 'done';
          else if (!currentMarked && p.total && (n.state === 'current' || n.state === 'todo')) {
            state = 'current';
            currentMarked = true;
          }
          var debts = state === 'locked' || state === 'done' ? null : stepDebts(p);
          /* Начатый, но не законченный шаг — «в процессе», а не «впереди»:
             13 из 14 выглядело так же, как 0 из 14 */
          if (state === 'todo' && debts) state = 'progress';
          var pct = p.total ? Math.round((p.done / p.total) * 100) : 0;
          var inner =
            '<span class="bd-roadmap__step-num">' + num + '</span>' +
            '<span class="bd-roadmap__step-title">' + esc(p.title.replace(STEP_PREFIX, '')) + '</span>' +
            (state === 'locked' ? '<span class="bd-roadmap__step-meta">Нет доступа</span>'
              : p.total ? '<span class="bd-roadmap__step-meta">' + (state === 'current' ? 'Вы здесь · ' : '') + p.done + ' из ' + tasks(p.total) + '</span>' : '') +
            ((state === 'progress' || state === 'current') && pct
              ? '<span class="bd-roadmap__step-bar"><span style="width:' + pct + '%"></span></span>' : '');
          return '<li class="bd-roadmap__step is-' + state + '">' +
            (p.tid && state !== 'locked' ? '<a href="' + link(p.tid) + '">' + inner + '</a>' : '<span>' + inner + '</span>') +
            (debts ? debtsHtml(p, debts) : '') +
          '</li>';
        }).join('') + '</ol>';
      } else {
        /* Широкая стрелка красится, как шаг: пройдено — фиолетовая,
           «Вы здесь» — салатовая, начато — лавандовая с полоской.
           Ждёт 1–3 ступеней или нет доступа — серая, приглушённая */
        var ws = locked ? 'locked' : n.state === 'done' ? 'done' : n.state === 'current' ? 'current'
          : n.state === 'todo' && n.done > 0 ? 'progress' : '';
        var meta = n.member ? n.memberWide : ws === 'done' ? 'Пройдено'
          : ws === 'current' ? 'Вы здесь' + (n.total ? ' · ' + n.done + ' из ' + tasks(n.total) : ' · следующая цель')
          : ws === 'progress' ? n.done + ' из ' + tasks(n.total) : '';
        var wide = esc(desc) + (meta ? '<span class="bd-roadmap__step-meta">' + meta + '</span>' : '') +
          ((ws === 'current' || ws === 'progress') && n.pct ? '<span class="bd-roadmap__step-bar"><span style="width:' + n.pct + '%"></span></span>' : '');
        body = locked ? '<div class="bd-roadmap__wide is-locked">' + wide + '</div>'
          : '<a class="bd-roadmap__wide' + (ws ? ' is-' + ws : '') + '" href="' + link(n.tid) + '">' + wide + '</a>';
      }

      return '<li class="bd-roadmap__row is-' + n.state + (steps.length ? '' : ' is-single') + '">' + stage + body + '</li>';
    }).join('');

    return (
      '<section class="bd-roadmap" aria-label="Дорожная карта">' +
        '<div class="bd-roadmap__head">' +
          '<div>' +
            '<div class="bd-roadmap__eyebrow">Дорожная карта</div>' +
            '<h2 class="bd-roadmap__title">Путь развития участника экосистемы</h2>' +
          '</div>' +
          '<ul class="bd-roadmap__legend" aria-hidden="true">' +
            '<li class="is-done">Пройдено</li><li class="is-current">Вы здесь</li><li class="is-progress">Начато</li>' +
            '<li class="is-todo">Впереди</li><li class="is-debt">Не сдано</li>' +
          '</ul>' +
        '</div>' +
        '<ol class="bd-roadmap__rows">' + rowsHtml + '</ol>' +
      '</section>'
    );
  }

  function fallbackHtml() {
    return (
      '<section class="bd-hero" aria-label="Флагманская программа">' +
        '<div class="bd-hero__body">' +
          '<div class="bd-hero__eyebrow">Флагманская программа</div>' +
          '<h1 class="bd-hero__title">Экосистема «Стандарты бухгалтерского бизнеса»</h1>' +
          '<p class="bd-hero__lead">' + heroLead() + '</p>' +
        '</div>' +
      '</section>'
    );
  }

  /* Первый заход, данных ещё нет: заготовки на месте карты и «Ваш прогресс» */
  function roadmapSkeletonHtml() {
    return '<section class="bd-roadmap is-loading" aria-busy="true">' +
      '<div class="bd-roadmap__head"><div>' +
        '<div class="bd-roadmap__eyebrow">Дорожная карта</div>' +
        '<h2 class="bd-roadmap__title">Путь развития участника экосистемы</h2>' +
      '</div></div>' +
      '<p class="bd-roadmap__wait">Собираем ваш прогресс по всем ступеням — несколько секунд</p>' +
      '<div class="bd-skel"><span></span><span></span><span></span><span></span><span></span></div>' +
    '</section>';
  }

  function progressSkeletonHtml() {
    return '<div class="bd-progress-card is-loading" aria-busy="true">' +
      '<div class="bd-progress-card__title">Ваш прогресс</div>' +
      '<span class="bd-ring" style="--bd-ring-pct:0"></span>' +
      '<div class="bd-skel"><span></span><span></span></div>' +
    '</div>';
  }

  /* Чип статуса и прогресс в карточках ступеней на той же странице */
  var CARD_STATUS = {
    done: 'Завершено',
    current: 'В процессе',
    todo: 'Доступно',
    waiting: 'После 1–3 ступеней',
    locked: 'Закрыто'
  };

  function decorateCards(model) {
    model.nodes.forEach(function (n) {
      var tr = document.querySelector('.gc-main-content .stream-table tr[data-training-id="' + n.tid + '"]');
      if (!tr) return;
      var a = tr.querySelector('td > a');
      if (!a) return;

      tr.classList.toggle('is-current', n.state === 'current');
      tr.setAttribute('data-bd-state', n.state);

      /* Модуль начат, но не пройден — «Продолжить» и чип «В процессе»,
         даже если текущей считается другая ступень */
      var started = n.total > 0 && n.done > 0 && n.done < n.total;
      if (started) {
        tr.setAttribute('data-bd-started', '');
      } else {
        tr.removeAttribute('data-bd-started');
      }

      var old = tr.querySelectorAll('.bd-card-status, .bd-card-progress');
      Array.prototype.forEach.call(old, function (el) { el.remove(); });

      var chip = document.createElement('span');
      var chipState = started && n.state === 'todo' ? 'current' : n.state;
      chip.className = 'bd-card-status is-' + chipState;
      chip.textContent = n.member ? n.memberNote : n.state === 'locked' && n.tariff ? n.tariff : CARD_STATUS[chipState];
      tr.appendChild(chip);

      if (n.total) {
        var pct = Math.round((n.done / n.total) * 100);
        var p = document.createElement('span');
        p.className = 'bd-card-progress' + (pct === 100 ? ' is-done' : '');
        p.title = 'Выполнено ' + n.done + ' из ' + tasks(n.total);
        p.innerHTML =
          '<span class="bd-card-progress__bar"><span style="width:' + pct + '%"></span></span>' +
          '<span class="bd-card-progress__pct">' + pct + '%</span>';
        a.appendChild(p);
      }
    });
  }

  /* ── Поздравление после ступени ────────────────────────── */

  function congratulate(prev, next) {
    if (!prev) return;
    STAGES.forEach(function (s) {
      if (!s.count || !s.tid) return;
      if (isComplete(next.stages[s.key]) && !isComplete(prev.stages[s.key])) {
        showToast('Поздравляем! ' + s.title + (s.subtitle ? ' «' + s.subtitle + '»' : '') + ' пройдена');
      }
    });
  }

  function showToast(text) {
    var el = document.createElement('div');
    el.className = 'bd-toast';
    el.setAttribute('role', 'status');
    el.innerHTML = '<span class="bd-toast__icon" aria-hidden="true"></span><span>' + esc(text) + '</span>';
    document.body.appendChild(el);
    requestAnimationFrame(function () { el.classList.add('is-visible'); });
    setTimeout(function () {
      el.classList.remove('is-visible');
      setTimeout(function () { el.remove(); }, 400);
    }, 6000);
  }

  /* ── Монтирование ──────────────────────────────────────── */

  function findMount() {
    var path = location.pathname.replace(/\/+$/, '');
    if (path !== '/teach/control/stream/view/id/' + ECOSYSTEM_TID) return null;
    return document.querySelector('.xdget-training .xdget-row > .xdget-container:first-child:not(:only-child), .gc-main-content .xdget-row > .xdget-container:first-child:not(:only-child)');
  }

  /* Дорожная карта — сразу под баннером */
  function mapHost(host) {
    var map = document.querySelector('.bd-roadmap-host');
    if (!map) {
      map = document.createElement('div');
      map.className = 'bd-roadmap-host';
      map.setAttribute('data-bd-injected', '');
      host.parentNode.insertBefore(map, host.nextSibling);
    }
    return map;
  }

  /* «Ваш прогресс» — в блок HTML <div data-bd-widget="progress">,
     который сотрудники ставят в колонку сами; без него — наверх колонки */
  function cardHost() {
    var card = document.querySelector('.gc-main-content [data-bd-widget="progress"]');
    var aside = document.querySelector('.gc-main-content .xdget-row > .xdget-container:last-child:not(:only-child)');
    if (!card && !aside) return null;
    card = card || aside.querySelector('.bd-progress-host');
    if (!card) {
      card = document.createElement('div');
      card.className = 'bd-progress-host';
      card.setAttribute('data-bd-injected', '');
      aside.insertBefore(card, aside.firstChild);
    }
    return card;
  }

  function render(host, snapshot) {
    var model = buildModel(snapshot);
    host.innerHTML = heroHtml(model);
    decorateCards(model);
    mapHost(host).innerHTML = roadmapHtml(model);
    var card = cardHost();
    if (card) card.innerHTML = progressCardHtml(model);
  }

  /* Первый заход: баннер — сразу целиком (он от прогресса почти не
     зависит), на месте карты и «Ваш прогресс» — заготовки */
  function renderLoading(host) {
    host.innerHTML = heroHtml(buildModel({ stages: {}, lessons: null }));
    mapHost(host).innerHTML = roadmapSkeletonHtml();
    var card = cardHost();
    if (card) card.innerHTML = progressSkeletonHtml();
  }

  function clearLoading() {
    Array.prototype.forEach.call(document.querySelectorAll('.bd-roadmap.is-loading, .bd-progress-card.is-loading'), function (el) {
      el.remove();
    });
  }

  /* Обход — когда страница загрузилась или через секунду после её
     появления, что раньше: 60 запросов не должны отнимать у страницы сеть
     в первые мгновения. Ждать все картинки нельзя — картинка из хранилища
     GetCourse, бывает, не догружается никогда */
  function whenPageLoaded(fn) {
    var started = false;
    function start() {
      if (started) return;
      started = true;
      fn();
    }
    if (document.readyState === 'complete') {
      start();
      return;
    }
    window.addEventListener('load', start);
    setTimeout(start, 1000);
  }

  /* Точка на шаге открывает окошко со списком; клик мимо или Esc — закрывает */
  function bindDebts() {
    function closeAll(except) {
      Array.prototype.forEach.call(document.querySelectorAll('.bd-roadmap__dot[aria-expanded="true"]'), function (dot) {
        if (dot === except) return;
        dot.setAttribute('aria-expanded', 'false');
        var panel = document.getElementById(dot.getAttribute('aria-controls'));
        if (panel) panel.hidden = true;
        dot.parentNode.classList.remove('is-open');
      });
    }
    document.addEventListener('click', function (e) {
      var dot = e.target.closest ? e.target.closest('.bd-roadmap__dot') : null;
      if (dot) {
        e.preventDefault();
        closeAll(dot);
        var panel = document.getElementById(dot.getAttribute('aria-controls'));
        var open = dot.getAttribute('aria-expanded') !== 'true';
        dot.setAttribute('aria-expanded', open ? 'true' : 'false');
        if (panel) panel.hidden = !open;
        dot.parentNode.classList.toggle('is-open', open);
        return;
      }
      if (!e.target.closest || !e.target.closest('.bd-roadmap .bd-debts')) closeAll();
    });
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closeAll();
    });
  }

  function init() {
    var mount = findMount();
    if (!mount) return;
    bindDebts();

    /* Снимки прежних версий больше не читаются — не держим их в браузере */
    ['bd-path-v1:', 'bd-path-v2:', 'bd-path-v3:'].forEach(function (k) { safeRemove(k + (window.accountUserId || 'guest')); });

    var host = document.createElement('div');
    host.className = 'bd-path-host';
    mount.insertBefore(host, mount.firstChild);

    var cached = readCache();
    var fresh = cached && Date.now() - cached.at < CACHE_TTL;

    if (cached) {
      render(host, cached);
    } else {
      renderLoading(host);
    }
    if (fresh) return;

    /* Обход уже идёт в другой вкладке или до перезагрузки — не дублируем */
    var lock = Number(safeGet(LOCK_KEY)) || 0;
    if (Date.now() - lock < LOCK_TTL) {
      if (!cached) {
        clearLoading();
        host.innerHTML = fallbackHtml();
      }
      return;
    }
    safeSet(LOCK_KEY, String(Date.now()));

    whenPageLoaded(function () {
      collect().then(function (snapshot) {
        congratulate(cached, snapshot);
        writeCache(snapshot);
        render(host, snapshot);
      }).catch(function () {
        /* Прогресс не посчитался: оставляем прошлый результат, а без него —
           спокойный баннер без цифр, чтобы верх страницы не опустел */
        if (!cached) {
          clearLoading();
          host.innerHTML = fallbackHtml();
        }
      }).then(function () {
        safeRemove(LOCK_KEY);
      });
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

} catch (e) { if (window.console) console.error('[bd] path.js', e); }