try {
/* ============================================================
   «Ваш прогресс» для любого тренинга — факультеты и другие программы

   Сотрудник ставит в колонку тренинга блок HTML:
     <div data-bd-widget="progress"></div>
   и на его месте появляется карточка: кольцо с процентом, кнопка
   «Перейти к заданию» — к первому несданному заданию, несданные
   задания текущего блока со ссылками. Список блоков с полосками из
   карточки убран (30.09) — полоски остались в карточках блоков.
   Считать прогресс другого тренинга: data-training="id".
   Заголовок карточки: data-title="…" (по умолчанию «Ваш прогресс»).

   Считает так же, как «Экосистема» (path.js): открывает страницы
   тренинга и всех вложенных в него, считает уроки с заданием
   по штатным классам состояний. Уроки без задания в процент не входят.
   На странице «Экосистемы» этот блок рисует path.js — здесь её пропускаем.
   Результат — в localStorage на полчаса.
   ============================================================ */

(function () {
  'use strict';

  if (window.bdProgressLoaded) return;
  window.bdProgressLoaded = true;

  var ECOSYSTEM_TID = 934216339;
  var CACHE_TTL = 30 * 60 * 1000;
  var MAX_DEPTH = 4;
  var CONCURRENCY = 8;

  /* Своих стилей немного — список блоков; остальное (карточка, кольцо,
     список заданий) уже есть в теме кабинета, account_3.css */
  var CSS =
    '.bd-progress-card__done{align-self:stretch;padding:10px 12px;font-size:var(--bd-fs-sm);font-weight:600;text-align:center;color:var(--bd-text);background:var(--bd-surface-2);border-radius:var(--bd-radius-sm)}' +
    /* Одна прокрутка на весь список, а не у каждого блока своя */
    '[data-bd-ready] .bd-debts.is-inline{max-height:380px;overflow-y:auto;padding-right:4px}' +
    '.gc-main-content [data-bd-ready] .bd-debts.is-inline ul{max-height:none;overflow:visible}' +
    '.bd-debts__more{padding:8px 6px 2px;font-size:var(--bd-fs-xs);color:var(--bd-text-faint);border-top:1px solid var(--bd-line)}' +
    '.bd-debt__t small{display:block;margin-bottom:1px;font-size:11px;font-weight:600;line-height:1.3;color:var(--bd-text-muted)}' +
    '.bd-progress-card__note{font-size:var(--bd-fs-xs);line-height:1.5;text-align:center;color:var(--bd-text-muted)}';

  /* ── Обход ─────────────────────────────────────────────── */

  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 directText(el) {
    return el ? Array.prototype.filter.call(el.childNodes, function (n) { return n.nodeType === 3; })
      .map(function (n) { return n.textContent; }).join(' ').replace(/\s+/g, ' ').trim() : '';
  }

  function fetchTraining(tid) {
    return limited(function () {
      return fetch('/teach/control/stream/view/id/' + tid, { credentials: 'same-origin' }).then(function (r) {
        /* Нет доступа (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 accessible = res.url.indexOf(String(tid)) !== -1 && !!doc.querySelector('.lesson-list, .stream-table');
      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) {
        return {
          id: li.getAttribute('data-lesson-id') || (li.className.match(/lesson-id-(\d+)/) || [])[1] || '',
          title: directText(li.querySelector('.link.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 t = tr.querySelector('.stream-title');
        return {
          tid: Number(tr.getAttribute('data-training-id')),
          title: t ? t.textContent.replace(/\s+/g, ' ').trim() : '',
          locked: /\bno-public\b/.test(tr.className)
        };
      });
      return { accessible: accessible, lessons: lessons, children: children };
    });
  }

  /* { accessible, total, done, pending: [{id,title,state}], parts: [{tid,title,total,done,pending,locked}] } */
  /* where — тема, в которой лежит урок (глубже блока): у многих уроков
     одинаковое название «ДОМАШНЕЕ ЗАДАНИЕ», без темы их не различить */
  function measure(tid, depth, visited, title) {
    if (visited[tid]) return Promise.resolve({ accessible: true, total: 0, done: 0, pending: [], parts: [] });
    visited[tid] = true;
    return fetchTraining(tid).then(function (page) {
      if (!page.accessible) return { accessible: false, total: 0, done: 0, pending: [], parts: [] };
      var own = page.lessons.map(function (l) {
        if (depth >= 2 && title) l.where = title.slice(0, 80);
        return l;
      });
      var result = {
        accessible: true,
        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;
      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, depth + 1, visited, child.title).then(function (m) {
          m.tid = child.tid;
          m.title = child.title;
          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.own = own.filter(function (l) { return l.state !== 'done'; });
          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 || p.accessible === false };
          });
        }
        return result;
      });
    });
  }

  /* ── Кэш ───────────────────────────────────────────────── */

  function cacheKey(tid) {
    return 'bd-progress-v3:' + (window.accountUserId || 'guest') + ':' + tid;
  }

  function readCache(tid) {
    try { return JSON.parse(localStorage.getItem(cacheKey(tid))); } catch (e) { return null; }
  }

  function writeCache(tid, value) {
    try { localStorage.setItem(cacheKey(tid), JSON.stringify(value)); } catch (e) { /* без кэша */ }
  }

  /* ── Разметка ──────────────────────────────────────────── */

  function esc(s) {
    return String(s).replace(/[&<>"]/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
    });
  }

  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 tasks(n) {
    return n + ' ' + plural(n, 'задания', 'заданий', 'заданий');
  }

  function pct(done, total) {
    return total ? Math.round((done / total) * 100) : 0;
  }

  function lessonUrl(id) {
    return '/teach/control/lesson/view/id/' + id;
  }

  function lessonLinks(list) {
    return '<ul>' + list.map(function (l) {
      var t = '<span class="bd-debt__t">' + (l.where ? '<small>' + esc(l.where) + '</small>' : '') + esc(l.title || 'Урок') + '</span>';
      return '<li>' + (l.id ? '<a href="' + lessonUrl(esc(l.id)) + '">' + t + '</a>' : '<span>' + t + '</span>') + '</li>';
    }).join('') + '</ul>';
  }

  function isStaff() {
    var u = window.userInfo || {};
    return !!(u.isAdmin || u.isManager || u.isTeacher || u.canCms);
  }

  /* Несданные — по блокам; на проверке — отдельной строкой */
  /* Несданные — только текущего блока (первого, где они есть), как по
     шагам в «Экосистеме»: список всех заданий факультета был слишком
     длинным, и нужное приходилось искать. Остальные — одной строкой */
  function debtsHtml(m) {
    var sources = (m.parts.length ? [{ title: '', pending: m.own || [] }].concat(m.parts) : [{ title: '', pending: m.pending }])
      .map(function (p) {
        return {
          title: p.title,
          todo: (p.pending || []).filter(function (l) { return l.state === 'todo'; }),
          review: (p.pending || []).filter(function (l) { return l.state === 'review'; })
        };
      });
    var cur = sources.filter(function (p) { return p.todo.length; })[0] ||
      sources.filter(function (p) { return p.review.length; })[0];
    if (!cur) return '';
    var allTodo = sources.reduce(function (a, p) { return a + p.todo.length; }, 0);
    var rest = allTodo - cur.todo.length;
    return '<details class="bd-progress-card__debts">' +
      '<summary>' + (cur.todo.length ? 'Не сдано в блоке<b>' + cur.todo.length + '</b>' : 'На проверке в блоке<b>' + cur.review.length + '</b>') + '</summary>' +
      '<div class="bd-debts is-inline"><div class="bd-debts__group">' +
        (cur.title ? '<div class="bd-debts__step">' + esc(cur.title) + '</div>' : '') +
        (cur.todo.length ? lessonLinks(cur.todo) : '') +
        (cur.review.length ? '<div class="bd-debts__title is-review">На проверке · ' + cur.review.length + '</div><div class="is-review">' + lessonLinks(cur.review) + '</div>' : '') +
      '</div>' +
      (rest > 0 ? '<div class="bd-debts__more">В следующих блоках не сдано ещё ' + tasks(rest) + '</div>' : '') +
      '</div>' +
    '</details>';
  }

  function cardHtml(m, title) {
    var p = pct(m.done, m.total);
    var next = m.pending.filter(function (l) { return l.state === 'todo' && l.id; })[0];
    return (
      '<div class="bd-progress-card">' +
        '<div class="bd-progress-card__title">' + esc(title) + '</div>' +
        '<span class="bd-ring" style="--bd-ring-pct:' + p + '" role="img" aria-label="Пройдено ' + p + '%">' +
          '<span class="bd-ring__value">' + p + '%</span>' +
        '</span>' +
        '<div class="bd-progress-card__caption">Выполнено<b>' + m.done + ' из ' + tasks(m.total) + '</b></div>' +
        (next ? '<a class="bd-progress-card__cta" href="' + lessonUrl(esc(next.id)) + '">Перейти к заданию</a>'
          : m.done >= m.total ? '<div class="bd-progress-card__done">Все задания приняты</div>' : '') +
        debtsHtml(m) +
        '<details class="bd-progress-card__how">' +
          '<summary>Как считается прогресс</summary>' +
          '<p>Считаются уроки с заданием: урок пройден, когда куратор принял ваш ответ. ' +
          'Отправленный ответ засчитается после проверки. Уроки без задания в процент не входят.</p>' +
          '<p>«Перейти к заданию» ведёт к первому несданному заданию. Данные обновляются раз в полчаса.</p>' +
        '</details>' +
      '</div>'
    );
  }

  function skeletonHtml(title) {
    return '<div class="bd-progress-card is-loading" aria-busy="true">' +
      '<div class="bd-progress-card__title">' + esc(title) + '</div>' +
      '<span class="bd-ring" style="--bd-ring-pct:0"></span>' +
      '<div class="bd-skel"><span></span><span></span></div>' +
    '</div>';
  }

  /* Заданий нет (или нет доступа): ученику карточку не показываем,
     сотруднику объясняем, почему пусто */
  function emptyHtml(title) {
    if (!isStaff()) return '';
    return '<div class="bd-progress-card">' +
      '<div class="bd-progress-card__title">' + esc(title) + '</div>' +
      '<div class="bd-progress-card__note">В этом тренинге не нашлось уроков с заданием — прогресс считать не из чего. ' +
      'Ученики эту карточку не видят (сообщение только для сотрудников).</div>' +
    '</div>';
  }

  /* Полоски в карточках блоков на странице тренинга — как в «Экосистеме» */
  function decorateCards(m) {
    m.parts.forEach(function (p) {
      var tr = document.querySelector('.gc-main-content .stream-table tr[data-training-id="' + p.tid + '"]');
      var a = tr && tr.querySelector('td > a');
      if (!a || !p.total) return;
      var old = a.querySelector('.bd-card-progress');
      if (old) old.remove();
      var v = pct(p.done, p.total);
      var el = document.createElement('span');
      el.className = 'bd-card-progress' + (v === 100 ? ' is-done' : '');
      el.title = 'Выполнено ' + p.done + ' из ' + tasks(p.total);
      el.innerHTML = '<span class="bd-card-progress__bar"><span style="width:' + v + '%"></span></span>' +
        '<span class="bd-card-progress__pct">' + v + '%</span>';
      a.appendChild(el);
    });
  }

  /* ── Запуск ────────────────────────────────────────────── */

  function render(host, tid, m, title) {
    if (!m.total) {
      host.innerHTML = emptyHtml(title);
    } else {
      host.innerHTML = cardHtml(m, title);
    }
    host.classList.toggle('is-empty', !host.innerHTML);
    if (String(tid) === pageTid()) decorateCards(m);
  }

  function pageTid() {
    var m = location.pathname.match(/\/stream\/view\/id\/(\d+)/);
    return m ? m[1] : '';
  }

  function mount(host) {
    if (host.getAttribute('data-bd-ready')) return;
    var tid = host.getAttribute('data-training') || pageTid();
    if (!tid || Number(tid) === ECOSYSTEM_TID) return;
    host.setAttribute('data-bd-ready', '1');
    host.classList.add('bd-progress-host');
    var title = host.getAttribute('data-title') || 'Ваш прогресс';

    var cached = readCache(tid);
    if (cached && cached.m) render(host, tid, cached.m, title);
    else host.innerHTML = skeletonHtml(title);
    if (cached && Date.now() - cached.at < CACHE_TTL) return;

    measure(Number(tid), 0, {}, '').then(function (m) {
      writeCache(tid, { at: Date.now(), m: m });
      render(host, tid, m, title);
    }).catch(function () {
      if (!cached) {
        host.innerHTML = '';
        host.classList.add('is-empty');
      }
    });
  }

  function init() {
    var hosts = document.querySelectorAll('.gc-main-content [data-bd-widget="progress"], .xdget-root [data-bd-widget="progress"]');
    if (!hosts.length) return;
    if (!document.getElementById('bd-progress-css')) {
      var st = document.createElement('style');
      st.id = 'bd-progress-css';
      st.textContent = CSS;
      document.head.appendChild(st);
    }
    Array.prototype.forEach.call(hosts, mount);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

} catch (e) { if (window.console) console.error('[bd] progress.js', e); }
try {
/* ============================================================
   Страница тренинга «Экосистема» — обвязка по референсам

   Данные о прогрессе рисует path.js, здесь — остальная обвязка:
     .bd-topbar       хлебные крошки над страницей
     строку поиска перед модулями ставит search.js
     .bd-section-head «Модули программы» + фильтр Все / В процессе / Завершённые
     слово основателя внизу страницы — блок HTML .bd-founder, его ставят
     сотрудники сами (11-ecosystem.css, docs/инструкция-для-команды.md)
   Правую колонку скрипт не переставляет: порядок блоков — порядок
   в конструкторе (docs/инструкция-для-команды.md).

   Фильтр опирается на data-bd-state у карточек (ставит path.js).
   Пока прогресс не посчитан, работает только «Все».
   ============================================================ */

(function () {
  'use strict';

  /* Скрипт уже запущен (например, тема стоит и вставили предпросмотр) */
  if (window.bdEcosystemLoaded) return;
  window.bdEcosystemLoaded = true;

  var ECOSYSTEM_TID = 934216339;
  var FILTERS = [
    ['all', 'Все'],
    ['current', 'В процессе'],
    ['done', 'Завершённые']
  ];

  function el(tag, cls, html) {
    var e = document.createElement(tag);
    e.className = cls;
    e.setAttribute('data-bd-injected', '');
    if (html) e.innerHTML = html;
    return e;
  }

  function topbar() {
    var bar = el('div', 'bd-topbar',
      '<nav class="bd-topbar__crumbs" aria-label="Навигация">' +
        '<a href="/teach/control">Тренинги</a><span aria-hidden="true">/</span><span>Экосистема</span>' +
      '</nav>'
    );
    return bar;
  }

  function sectionHead(table) {
    var head = el('div', 'bd-section-head',
      '<h2 class="bd-section-head__title">Модули программы</h2>' +
      '<div class="bd-filter" role="group" aria-label="Показать модули">' +
        FILTERS.map(function (f, i) {
          return '<button type="button" class="bd-filter__btn' + (i === 0 ? ' is-active' : '') + '" aria-pressed="' + (i === 0) + '" data-filter="' + f[0] + '">' + f[1] + '</button>';
        }).join('') +
      '</div>'
    );

    head.addEventListener('click', function (e) {
      var btn = e.target.closest('.bd-filter__btn');
      if (!btn) return;
      /* Пока прогресс не посчитан, у карточек нет состояния — фильтр
         спрятал бы всё */
      if (btn.getAttribute('data-filter') !== 'all' && !table.querySelector('tr[data-bd-state]')) return;
      Array.prototype.forEach.call(head.querySelectorAll('.bd-filter__btn'), function (b) {
        b.classList.toggle('is-active', b === btn);
        b.setAttribute('aria-pressed', b === btn ? 'true' : 'false');
      });
      var f = btn.getAttribute('data-filter');
      if (f === 'all') table.removeAttribute('data-bd-filter');
      else table.setAttribute('data-bd-filter', f);
    });
    return head;
  }

  function init() {
    var path = location.pathname.replace(/\/+$/, '');
    if (path !== '/teach/control/stream/view/id/' + ECOSYSTEM_TID) return;

    document.documentElement.classList.add('bd-page-ecosystem');

    var main = document.querySelector('.gc-main-content .xdget-row > .xdget-container:first-child:not(:only-child)');
    var row = main && main.parentElement;
    if (!main) return;

    var header = document.querySelector('.gc-main-content .page-header');
    var anchor = header || row;
    if (anchor && anchor.parentElement) anchor.parentElement.insertBefore(topbar(), anchor);

    var list = main.querySelector('.xdget-trainingList');
    var table = list && list.querySelector('.stream-table');
    if (list && table) {
      list.parentElement.insertBefore(sectionHead(table), list);
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

} catch (e) { if (window.console) console.error('[bd] ecosystem.js', e); }
try {
/* ============================================================
   Избранное — ученик отмечает уроки звёздочкой и находит их
   в пункте меню «Избранное»

   Где звёздочка:
     • в шапке урока — кнопка «В избранное» / «В избранном»;
     • в списке уроков — звёздочка справа в строке урока.
   Где список: пункт меню «Избранное» открывает панель справа
   (на телефоне — на весь экран). Панель открывает и любая ссылка
   на #bd-favorites — например, кнопка-плитка GetCourse (в приложении
   нашего меню нет), и готовая кнопка блоком HTML:
   <div data-bd-widget="favorites-button"></div>.
   Сам список можно поставить на страницу: <div data-bd-widget="favorites"></div>.

   Где хранится. В доп. поле пользователя GetCourse «Избранное» —
   номера уроков через запятую, новые первыми. Так список общий для
   телефона и компьютера.
     запись — событие модуля «Воронки»: запрос на ссылку события
              с параметрами user_id, email (по ним воронка находит
              ученика), act (add / remove), lesson (номер урока) и fav —
              весь новый список. Воронка кладёт fav в поле «Избранное».
              Удаление — тот же список без урока.
     чтение — GetCourse подставляет поля в блок страницы:
              <div data-bd-fav="{Избранное}" data-bd-email="{email}"></div>.
              Почты в кабинете больше нигде нет; без блока скрипт берёт её
              со страницы профиля («E-mail: …»). Блок стоит на
              списке тренингов (/teach/control); на остальных страницах
              скрипт раз за сеанс читает его оттуда фоном.
   Главное — поле. При каждом нажатии скрипт заново читает поле
   с сервера, применяет к нему нажатие и отправляет готовый список.
   Браузер хранит только копию для быстрого показа и свои нажатия за
   последние 3 минуты: воронке нужно несколько секунд, чтобы записать
   поле, и без них второе быстрое нажатие затёрло бы первое. Другой
   телефон, другой браузер или очищенный кэш список не ломают — там
   он просто читается из поля.
   Названия уроков для панели запоминаются при нажатии звёздочки;
   урок, отмеченный на другом устройстве, скрипт откроет фоном
   и возьмёт название со страницы урока.
   ============================================================ */

(function () {
  'use strict';

  if (window.bdFavLoaded) return;
  window.bdFavLoaded = true;

  var UID = window.accountUserId;
  if (!UID) return;  /* гость — хранить негде */

  var EVENT_URL = 'https://buhrost.ru/chtm/app/builder/event/~reg/rJe4aGSigkbaCoU4kmITZn5HS/I8y8EMDcI6?hash=IEQmir22b8';
  /* Страницы, где может стоять блок data-bd-fav: скрипт ищет по порядку
     и запоминает, где нашёл. Правильное место — «Список тренингов»: его
     видят все; «Экосистему» — только её участники */
  var SOURCES = ['/teach/control', '/teach/control/stream/view/id/934216339'];
  var KEY = 'bd-fav-v1:' + UID;
  var SYNC_KEY = 'bd-fav-synced:' + UID;
  var OPS_TTL = 3 * 60 * 1000;       /* столько свои нажатия ждут записи в поле */
  var MAX = 150;                      /* ~1,5 тыс. знаков в поле — с запасом */

  var STAR = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.8l2.5 5.2 5.7.7-4.2 3.9 1.1 5.6L12 16.4l-5.1 2.8 1.1-5.6-4.2-3.9 5.7-.7z"/></svg>';

  var CSS =
    /* кнопка в шапке урока (фиолетовая шапка) */
    '.gc-main-content .lesson-header-block:has(>.bd-fav-btn){position:relative}' +
    '.gc-main-content .lesson-header-block>.bd-fav-btn{position:absolute;top:14px;right:16px;z-index:2}' +
    '.bd-fav-btn{display:inline-flex;align-items:center;gap:6px;margin:0;padding:5px 12px 5px 10px;font:600 var(--bd-fs-xs)/1.2 var(--bd-font);color:#fff;background:transparent;border:1px solid rgba(255,255,255,.4);border-radius:var(--bd-radius-pill);cursor:pointer;transition:background-color var(--bd-ease)}' +
    '.bd-fav-btn:hover{background:rgba(255,255,255,.12)}' +
    '.bd-fav-btn svg,.bd-fav-star svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linejoin:round}' +
    /* включённая — белая с фиолетовой звездой: салатовый у шапки — «Следующий урок» */
    '.bd-fav-btn.is-on{color:var(--bd-primary-text);background:#fff;border-color:#fff}' +
    '.bd-fav-btn.is-on svg{fill:var(--bd-violet-500);stroke:var(--bd-violet-500)}' +
    /* звезда в списке уроков — в правом верхнем углу колонки с текстом,
       под неё оставлено место: на длинное название она не ложится */
    '.gc-main-content .lesson-list .vmiddle:has(>.bd-fav-star){position:relative;padding-right:32px!important}' +
    '.gc-main-content .lesson-list .vmiddle>.bd-fav-star{position:absolute;top:-4px;right:0;margin:0!important;flex-basis:auto}' +
    '.bd-fav-star{display:grid;place-items:center;width:28px;height:28px;padding:0;color:var(--bd-text-faint);background:transparent;border:0;border-radius:50%;cursor:pointer;opacity:0;transition:opacity var(--bd-ease),color var(--bd-ease)}' +
    '.bd-fav-star svg{width:18px;height:18px}' +
    '.lesson-list>li:hover .bd-fav-star,.bd-fav-star:focus-visible,.bd-fav-star.is-on{opacity:1}' +
    '.bd-fav-star:hover{color:var(--bd-violet-500)}' +
    '.bd-fav-star.is-on{color:var(--bd-violet-500)}.bd-fav-star.is-on svg{fill:currentColor}' +
    '@media (hover:none){.bd-fav-star{opacity:1}}' +
    /* панель */
    '.bd-fav-overlay{position:fixed;inset:0;z-index:10050;background:var(--bd-overlay);opacity:0;transition:opacity .2s}' +
    '.bd-fav-overlay.is-open{opacity:1}' +
    '.bd-fav-panel{position:fixed;top:0;right:0;bottom:0;z-index:10051;display:flex;flex-direction:column;width:min(440px,100vw);background:var(--bd-bg);box-shadow:-12px 0 40px rgba(20,8,35,.18);transform:translateX(100%);transition:transform .25s ease;font-family:var(--bd-font)}' +
    '.bd-fav-panel.is-open{transform:none}' +
    '.bd-fav-panel__head{display:flex;align-items:center;gap:10px;padding:20px 20px 14px;background:var(--bd-surface);border-bottom:1px solid var(--bd-line)}' +
    '.bd-fav-panel__title{margin:0;font:700 var(--bd-fs-lg)/1.2 var(--bd-font-display);color:var(--bd-text)}' +
    '.bd-fav-panel__count{padding:2px 9px;font-size:var(--bd-fs-xs);font-weight:600;color:var(--bd-primary-text);background:var(--bd-lavender);border-radius:var(--bd-radius-pill)}' +
    '.bd-fav-panel__close{margin-left:auto;width:36px;height:36px;padding:0;background:var(--bd-surface-2) var(--bd-i-close) center/18px no-repeat;border:0;border-radius:50%;cursor:pointer}' +
    '.bd-fav-panel__body{flex:1;overflow-y:auto;padding:14px 16px 24px}' +
    '.bd-fav-list{display:grid;gap:14px}' +
    '.bd-fav-group{padding:6px;background:var(--bd-surface);border:1px solid var(--bd-line);border-radius:var(--bd-radius)}' +
    '.bd-fav-group__head{padding:8px 10px 6px}' +
    '.bd-fav-group__prog{font-size:11px;font-weight:600;letter-spacing:.04em;color:var(--bd-text-faint)}' +
    '.bd-fav-group__title{font-size:var(--bd-fs-sm);font-weight:700;line-height:1.3;color:var(--bd-text)}' +
    '.bd-fav-group ul{margin:0;padding:0;list-style:none}' +
    '.bd-fav-item{display:flex;align-items:center;gap:6px;border-top:1px solid var(--bd-line)}' +
    '.bd-fav-item a{flex:1;display:flex;align-items:center;gap:10px;min-width:0;padding:10px;font-size:var(--bd-fs-sm);font-weight:500;line-height:1.35;color:var(--bd-text)!important;text-decoration:none!important;border-radius:10px}' +
    '.bd-fav-item a:hover{background:var(--bd-surface-2)}' +
    '.bd-fav-item a svg{flex:none;width:16px;height:16px;fill:var(--bd-violet-500);stroke:var(--bd-violet-500);stroke-width:1.6;stroke-linejoin:round}' +
    '.bd-fav-item a span{overflow-wrap:anywhere}' +
    '.bd-fav-item a.is-loading span{color:var(--bd-text-faint)}' +
    '.bd-fav-remove{flex:none;width:32px;height:32px;margin-right:4px;padding:0;background:transparent var(--bd-i-close) center/14px no-repeat;border:0;border-radius:50%;opacity:.55;cursor:pointer}' +
    '.bd-fav-remove:hover{background-color:var(--bd-surface-2);opacity:1}' +
    '.bd-fav-empty{padding:28px 18px;font-size:var(--bd-fs-sm);line-height:1.5;text-align:center;color:var(--bd-text-muted);background:var(--bd-surface);border:1px dashed var(--bd-line-strong);border-radius:var(--bd-radius)}' +
    '.bd-fav-empty b{display:block;margin-bottom:6px;font-size:var(--bd-fs-body);color:var(--bd-text)}' +
    '.bd-fav-note{margin-top:10px;font-size:var(--bd-fs-xs);color:var(--bd-text-faint);text-align:center}' +
    '.bd-fav-toast{position:fixed;left:50%;bottom:84px;z-index:10060;max-width:calc(100vw - 32px);padding:10px 16px;font:500 var(--bd-fs-sm)/1.35 var(--bd-font);color:#fff;background:var(--bd-violet-900);border-radius:var(--bd-radius-sm);box-shadow:0 10px 30px rgba(20,8,35,.3);transform:translateX(-50%)}' +
    'html.bd-fav-lock{overflow:hidden}' +
    '.bd-fav-open{display:flex;align-items:center;gap:10px;width:100%;padding:14px 16px;font:600 var(--bd-fs-body)/1.2 var(--bd-font);color:var(--bd-text)!important;text-decoration:none!important;background:var(--bd-surface);border:1px solid var(--bd-line);border-radius:var(--bd-radius);box-shadow:var(--bd-shadow-soft);cursor:pointer}' +
    '.bd-fav-open svg{flex:none;width:22px;height:22px;fill:var(--bd-violet-500);stroke:var(--bd-violet-500);stroke-width:1.6;stroke-linejoin:round}' +
    '.bd-fav-open b{margin-left:auto;padding:2px 9px;font-size:var(--bd-fs-xs);color:var(--bd-primary-text);background:var(--bd-lavender);border-radius:var(--bd-radius-pill)}' +
    '.bd-fav-open:hover{border-color:var(--bd-line-strong)}';

  /* ── Хранилище ─────────────────────────────────────────── */

  function readLocal() {
    try {
      var v = JSON.parse(localStorage.getItem(KEY));
      if (v && v.ids) return v;
    } catch (e) { /* приватный режим */ }
    return { ids: [], ops: [], meta: {}, email: '', server: false };
  }

  function writeLocal() {
    try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) { /* без кэша */ }
  }

  function parseIds(value) {
    var seen = {};
    return (String(value || '').match(/\d{5,}/g) || []).filter(function (id) {
      if (seen[id]) return false;
      seen[id] = true;
      return true;
    });
  }

  /* Свои нажатия за последние минуты — поверх значения поля */
  function applyOps(ids) {
    var now = Date.now();
    state.ops = (state.ops || []).filter(function (o) { return now - o.at < OPS_TTL; });
    ids = ids.slice();
    state.ops.forEach(function (o) {
      ids = ids.filter(function (x) { return x !== o.id; });
      if (o.act === 'add') ids.unshift(o.id);
    });
    return ids;
  }

  /* Номера уроков из блока data-bd-fav; null — блока нет или GetCourse
     не подставил поле ({Избранное}) */
  function serverIds(root) {
    var el = root.querySelector('[data-bd-fav]');
    var v = el ? el.getAttribute('data-bd-fav') : null;
    if (v == null) return null;
    if (!/\{/.test(v)) return parseIds(v);
    /* {Избранное} не подставился. Если соседние переменные на странице
       подставились — поле просто пустое (новый ученик), иначе страница
       переменных не подставляет вовсе */
    var probe = root.querySelectorAll('[data-bd-board], [data-bd-business], [data-bd-email]');
    for (var i = 0; i < probe.length; i++) {
      var a = probe[i].attributes;
      for (var j = 0; j < a.length; j++) {
        if (/^data-bd-(board|business|email)$/.test(a[j].name) && a[j].value && !/\{/.test(a[j].value)) return [];
      }
    }
    return null;
  }

  /* Прочитать поле: с этой страницы, если блок на ней и свежесть не
     нужна, иначе — заново со страницы-источника */
  function readServer(fresh) {
    if (!fresh && document.querySelector('[data-bd-fav]')) {
      takeEmail(document);
      return Promise.resolve(serverIds(document));
    }
    var list = SOURCES.slice();
    if (state.src && list.indexOf(state.src) > 0) list.unshift(list.splice(list.indexOf(state.src), 1)[0]);
    function next() {
      var url = list.shift();
      if (!url) return Promise.resolve(null);
      return fetch(url, { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { return r.text(); }).then(function (html) {
        var doc = new DOMParser().parseFromString(html, 'text/html');
        takeEmail(doc);
        var ids = serverIds(doc);
        if (ids == null) return next();
        if (state.src !== url) {
          state.src = url;
          writeLocal();
        }
        return ids;
      }).catch(next);
    }
    return next();
  }

  /* Список = поле + свои недавние нажатия */
  function adopt(ids) {
    if (ids == null) return false;
    state.server = true;
    var next = applyOps(ids);
    var changed = next.join(',') !== state.ids.join(',');
    state.ids = next;
    writeLocal();
    return changed;
  }

  /* Почта ученика — из блока data-bd-email="{email}" */
  function takeEmail(root) {
    var el = root.querySelector('[data-bd-email]');
    var v = el ? el.getAttribute('data-bd-email').trim() : '';
    if (/^[^\s@{}]+@[^\s@{}]+\.[^\s@{}]+$/.test(v) && v !== state.email) {
      state.email = v;
      writeLocal();
    }
  }

  /* При открытии страницы: блок на ней — читаем его; нет — страницу-
     источник раз за сеанс (звёздочки сразу верные). Перед каждым
     нажатием и при открытии панели поле читается заново */
  function sync() {
    var onPage = !!document.querySelector('[data-bd-fav]');
    try { if (!onPage && sessionStorage.getItem(SYNC_KEY)) return; } catch (e) { /* читаем без флага */ }
    readServer(false).then(function (ids) {
      if (ids != null) {
        try { sessionStorage.setItem(SYNC_KEY, '1'); } catch (e) { /* ничего */ }
      }
      if (adopt(ids)) refresh();
    });
  }

  /* Журнал для рамки диагностики (diag.js): в приложении отладчика нет */
  var LOG = window.bdFavLog = [];
  function log(text) {
    var d = new Date();
    LOG.push(d.toTimeString().slice(0, 8) + ' ' + text);
    if (LOG.length > 20) LOG.shift();
  }

  /* Ожидание не дольше ms: в приложении запрос может повиснуть, и тогда
     событие не уходило вовсе */
  function within(promise, ms, fallback) {
    return Promise.race([promise, new Promise(function (r) { setTimeout(function () { r(fallback); }, ms); })]);
  }

  /* Событие воронки: кто (user_id; email — если известен из блока), готовый
     новый список и что сделали. Воронка находит ученика по user_id. С того
     же домена читаем ответ сервера: в журнале видно, дошло ли событие */
  function send(act, id, fav) {
    var mail = state.email;
    var url = EVENT_URL +
      '&user_id=' + encodeURIComponent(UID) +
      (mail ? '&email=' + encodeURIComponent(mail) : '') +
      '&act=' + act +
      '&lesson=' + encodeURIComponent(id) +
      '&fav=' + encodeURIComponent(fav);
    var same = EVENT_URL.indexOf(location.origin + '/') === 0;
    log('отправка: уроков ' + (fav === '-' ? 0 : fav.split(',').length));
    fetch(url, { credentials: 'include', mode: same ? 'same-origin' : 'no-cors', cache: 'no-store' }).then(function (r) {
      return same ? r.text().then(function (t) { log('ответ ' + r.status + ': ' + t.replace(/\s+/g, ' ').slice(0, 160)); }) : log('ушло');
    }).catch(function (e) {
      log('не ушло: ' + e + ' — шлю картинкой');
      new Image().src = url;
    });
  }

  function has(id) {
    return state.ids.indexOf(String(id)) !== -1;
  }

  /* Нажатие: сразу показываем, затем читаем поле заново, применяем
     к нему нажатие и отправляем готовый список */
  function toggle(id, meta) {
    id = String(id);
    var act = has(id) ? 'remove' : 'add';
    if (act === 'add' && state.ids.length >= MAX) {
      toast('В избранном уже ' + MAX + ' уроков — уберите лишние, чтобы добавить новые');
      return;
    }
    if (act === 'add' && meta && meta.t) state.meta[id] = meta;
    state.ops = (state.ops || []).concat({ id: id, act: act, at: Date.now() });
    state.ids = state.ids.filter(function (x) { return x !== id; });
    if (act === 'add') state.ids.unshift(id);
    if (!batch) batch = { ids: state.ids.slice(), ops: [] };
    batch.ops.push(state.ops[state.ops.length - 1]);
    batch.last = [act, id];
    state.ids = state.ids.filter(function (x) { return x !== id; });
    if (act === 'add') state.ids.unshift(id);
    else delete state.meta[id];
    writeLocal();
    refresh();

    log('нажатие: ' + act + ' ' + id);
    clearTimeout(flushTimer);
    flushTimer = setTimeout(flush, 600);
  }

  /* Поле читается заново перед каждой отправкой. Не прочиталось —
     не отправляем: старый список затёр бы добавленное с другого
     устройства; нажатия откатываются, ученик видит подсказку */
  var batch = null, flushTimer = 0, flushing = false, again = false;
  function flush() {
    if (flushing) { again = true; return; }
    var b = batch;
    batch = null;
    if (!b) return;
    flushing = true;
    within(readServer(true), 8000, null).then(function (ids) {
      log('поле: ' + (ids == null ? 'не прочитано' : ids.length + ' ур., стр. ' + (state.src || 'эта')));
      if (ids == null) {
        state.ops = state.ops.filter(function (o) { return b.ops.indexOf(o) === -1; });
        state.ids = applyOps(b.ids);
        writeLocal();
        refresh();
        toast('Не удалось сохранить избранное — проверьте интернет и нажмите ещё раз');
      } else {
        if (adopt(ids)) refresh();
        send(b.last[0], b.last[1], state.ids.join(',') || '-');
      }
      flushing = false;
      if (again) { again = false; flush(); }
    });
  }

  var state = readLocal();
  if (!state.meta) state.meta = {};

  /* ── Названия уроков ───────────────────────────────────── */

  var NEW_RE = /(^|\s)(\uD83C\uDD95|\[new\]|#new)(?=\s|$)/gi;

  function clean(s) {
    return String(s || '').replace(NEW_RE, ' ').replace(/\s+/g, ' ').trim();
  }

  /* С открытой страницы урока (или скачанной): урок, тема, программа */
  function lessonMeta(doc) {
    var t = doc.querySelector('.lesson-title-value');
    var tr = doc.querySelector('.page-header h1 a, .page-header h1');
    var crumbs = doc.querySelectorAll('.breadcrumb a[href*="/stream/view/id/"]');
    var prog = crumbs.length ? crumbs[0].textContent : '';
    var trTitle = tr ? clean(tr.textContent) : '';
    return {
      t: t ? clean(t.textContent).slice(0, 160) : '',
      tr: trTitle.slice(0, 120),
      p: clean(prog) !== trTitle ? clean(prog).slice(0, 120) : ''
    };
  }

  var loading = {}, failed = {}, queue = [], running = 0;
  function loadMeta(ids) {
    ids.forEach(function (id) {
      if (state.meta[id] || loading[id] || failed[id]) return;
      loading[id] = true;
      queue.push(id);
    });
    pump();
  }

  function pump() {
    while (running < 4 && queue.length) fetchMeta(queue.shift());
  }

  function fetchMeta(id) {
    running++;
    fetch('/teach/control/lesson/view/id/' + id, { credentials: 'same-origin' }).then(function (r) {
      /* 401/403/404 — урока нет или он закрыт; сбой сервера — не приговор */
      if (!r.ok && [401, 403, 404].indexOf(r.status) === -1) throw new Error(r.status);
      return r.text();
    }).then(function (html) {
      var m = lessonMeta(new DOMParser().parseFromString(html, 'text/html'));
      state.meta[id] = m.t ? m : { t: 'Урок недоступен', tr: '', p: '' };
      writeLocal();
    }).catch(function () {
      failed[id] = true;  /* до перезагрузки страницы больше не просим */
    }).then(function () {
      running--;
      delete loading[id];
      renderLists();
      pump();
    });
  }

  /* ── Разметка ──────────────────────────────────────────── */

  function esc(s) {
    return String(s).replace(/[&<>"]/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
    });
  }

  function plural(n) {
    var m10 = n % 10, m100 = n % 100;
    if (m10 === 1 && m100 !== 11) return 'урок';
    if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'урока';
    return 'уроков';
  }

  /* Группы по теме урока — в порядке добавления (новые сверху) */
  function listHtml() {
    if (!state.ids.length) {
      return '<div class="bd-fav-empty"><b>Здесь пока пусто</b>Нажмите «В избранное» на странице урока или звёздочку в списке уроков — урок появится здесь.</div>';
    }
    var groups = [];
    var byKey = {};
    state.ids.forEach(function (id) {
      var m = state.meta[id] || null;
      var key = m ? (m.p + '|' + m.tr) : '';
      if (!byKey[key]) {
        byKey[key] = { m: m, ids: [] };
        groups.push(byKey[key]);
      }
      byKey[key].ids.push(id);
    });
    return '<div class="bd-fav-list">' + groups.map(function (g) {
      var head = g.m && (g.m.tr || g.m.p)
        ? '<div class="bd-fav-group__head">' + (g.m.p ? '<div class="bd-fav-group__prog">' + esc(g.m.p) + '</div>' : '') +
          (g.m.tr ? '<div class="bd-fav-group__title">' + esc(g.m.tr) + '</div>' : '') + '</div>'
        : '';
      return '<section class="bd-fav-group">' + head + '<ul>' + g.ids.map(function (id) {
        var m = state.meta[id];
        return '<li class="bd-fav-item"><a' + (m ? '' : ' class="is-loading"') + ' href="/teach/control/lesson/view/id/' + esc(id) + '">' +
          STAR + '<span>' + esc(m ? m.t : 'Загружаем название…') + '</span></a>' +
          '<button type="button" class="bd-fav-remove" data-bd-fav-remove="' + esc(id) + '" title="Убрать из избранного" aria-label="Убрать из избранного"></button></li>';
      }).join('') + '</ul></section>';
    }).join('') + '</div>' +
    '<div class="bd-fav-note">Список общий для телефона и компьютера</div>';
  }

  var panel = null;

  function renderLists() {
    var need = state.ids.filter(function (id) { return !state.meta[id]; });
    if (panel) {
      panel.querySelector('.bd-fav-panel__body').innerHTML = listHtml();
      var c = panel.querySelector('.bd-fav-panel__count');
      c.textContent = state.ids.length + ' ' + plural(state.ids.length);
      c.hidden = !state.ids.length;
    }
    Array.prototype.forEach.call(document.querySelectorAll('[data-bd-widget="favorites"]'), function (host) {
      host.innerHTML = listHtml();
    });
    if (need.length && (panel && panel.classList.contains('is-open') || document.querySelector('[data-bd-widget="favorites"]'))) loadMeta(need);
  }

  function openPanel() {
    if (!panel) {
      var overlay = document.createElement('div');
      overlay.className = 'bd-fav-overlay';
      panel = document.createElement('aside');
      panel.className = 'bd-fav-panel';
      panel.setAttribute('role', 'dialog');
      panel.setAttribute('aria-label', 'Избранное');
      panel.innerHTML =
        '<div class="bd-fav-panel__head"><h2 class="bd-fav-panel__title">Избранное</h2>' +
        '<span class="bd-fav-panel__count"></span>' +
        '<button type="button" class="bd-fav-panel__close" title="Закрыть" aria-label="Закрыть"></button></div>' +
        '<div class="bd-fav-panel__body"></div>';
      panel.overlay = overlay;
      document.body.appendChild(overlay);
      document.body.appendChild(panel);
      overlay.addEventListener('click', closePanel);
      panel.querySelector('.bd-fav-panel__close').addEventListener('click', closePanel);
    }
    panel.overlay.hidden = false;
    renderLists();
    loadMeta(state.ids);
    /* Список могли поменять на другом устройстве — читаем поле заново */
    readServer(true).then(function (ids) { if (adopt(ids)) refresh(); });
    document.documentElement.classList.add('bd-fav-lock');
    requestAnimationFrame(function () {
      panel.overlay.classList.add('is-open');
      panel.classList.add('is-open');
    });
    panel.querySelector('.bd-fav-panel__close').focus();
  }

  function closePanel() {
    if (!panel || !panel.classList.contains('is-open')) return;
    panel.classList.remove('is-open');
    panel.overlay.classList.remove('is-open');
    /* открыли по ссылке #bd-favorites — снимаем метку, иначе та же ссылка
       второй раз не сработает */
    if (location.hash === '#bd-favorites') {
      try { history.replaceState(null, '', location.pathname + location.search); } catch (e) { /* ничего */ }
    }
    document.documentElement.classList.remove('bd-fav-lock');
    setTimeout(function () { if (!panel.classList.contains('is-open')) panel.overlay.hidden = true; }, 250);
  }

  function toast(text) {
    var el = document.createElement('div');
    el.className = 'bd-fav-toast';
    el.setAttribute('role', 'status');
    el.textContent = text;
    document.body.appendChild(el);
    setTimeout(function () { el.remove(); }, 3500);
  }

  /* ── Звёздочки ─────────────────────────────────────────── */

  function lessonIdFromUrl() {
    var m = location.pathname.match(/\/lesson\/view\/id\/(\d+)/) || location.search.match(/[?&]id=(\d+)/);
    return /\/lesson\/view/.test(location.pathname) && m ? m[1] : '';
  }

  function paintButton(btn, on) {
    btn.classList.toggle('is-on', on);
    btn.setAttribute('aria-pressed', on ? 'true' : 'false');
    var label = btn.querySelector('span');
    if (label) label.textContent = on ? 'В избранном' : 'В избранное';
    if (!label) btn.title = on ? 'Убрать из избранного' : 'Добавить в избранное';
  }

  function lessonButton() {
    var id = lessonIdFromUrl();
    var head = document.querySelector('.gc-main-content .lesson-header-block');
    if (!id || !head) return false;
    var btn = head.querySelector(':scope > .bd-fav-btn');
    if (!btn) {
      btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'bd-fav-btn';
      btn.setAttribute('data-bd-fav-id', id);
      btn.innerHTML = STAR + '<span></span>';
      head.appendChild(btn);
    }
    paintButton(btn, has(id));
    /* По высоте — ровно по центру строки «← К списку уроков» */
    var back = head.querySelector('.bd-lesson-back');
    if (back && btn.offsetHeight) {
      var top = back.getBoundingClientRect().top - head.getBoundingClientRect().top + back.offsetHeight / 2 - btn.offsetHeight / 2;
      btn.style.top = Math.max(6, Math.round(top)) + 'px';
    }
    return true;
  }

  function listStars() {
    var tr = document.querySelector('.gc-main-content .page-header h1, .gc-main-content h1');
    Array.prototype.forEach.call(document.querySelectorAll('.gc-main-content .lesson-list > li[data-lesson-id]'), function (li) {
      var id = li.getAttribute('data-lesson-id');
      var star = li.querySelector('.bd-fav-star');
      if (!star) {
        star = document.createElement('button');
        star.type = 'button';
        star.className = 'bd-fav-star';
        star.setAttribute('data-bd-fav-id', id);
        star.innerHTML = STAR;
        (li.querySelector('.vmiddle') || li).appendChild(star);
      }
      paintButton(star, has(id));
      star.bdTraining = tr ? clean(tr.textContent) : '';
    });
  }

  function listMeta(star) {
    var li = star.closest('li');
    var t = li && li.querySelector('.link.title');
    var text = t ? Array.prototype.filter.call(t.childNodes, function (n) { return n.nodeType === 3; })
      .map(function (n) { return n.textContent; }).join(' ') : '';
    var crumbs = document.querySelectorAll('.breadcrumb a[href*="/stream/view/id/"]');
    var prog = crumbs.length ? clean(crumbs[0].textContent) : '';
    return { t: clean(text).slice(0, 160), tr: (star.bdTraining || '').slice(0, 120), p: prog !== star.bdTraining ? prog.slice(0, 120) : '' };
  }

  /* ── Меню ──────────────────────────────────────────────── */

  function menuItem() {
    var menu = document.querySelector('#gcAccountUserMenu .gc-account-leftbar .gc-account-user-menu');
    if (!menu || !menu.children.length) return false;
    if (menu.querySelector('.menu-item-bd-fav')) return true;
    var li = document.createElement('li');
    li.className = 'menu-item menu-item-bd-fav';
    li.innerHTML = '<a href="#bd-favorites" title="Избранное"></a>';
    var search = menu.querySelector(':scope > li.menu-item-search');
    var faq = menu.querySelector(':scope > li.menu-item-bd-faq');
    if (search) menu.insertBefore(li, search.nextSibling);
    else if (faq) menu.insertBefore(li, faq);
    else menu.appendChild(li);
    return true;
  }

  /* ── Запуск ────────────────────────────────────────────── */

  function openButtons() {
    Array.prototype.forEach.call(document.querySelectorAll('[data-bd-widget="favorites-button"]'), function (host) {
      host.innerHTML = '<a class="bd-fav-open" href="#bd-favorites">' + STAR + '<span>Избранное</span>' +
        (state.ids.length ? '<b>' + state.ids.length + '</b>' : '') + '</a>';
    });
  }

  function refresh() {
    lessonButton();
    listStars();
    renderLists();
    openButtons();
  }

  function onClick(e) {
    if (!e.target.closest) return;
    var item = e.target.closest('.menu-item-bd-fav > a, a[href$="#bd-favorites"]');
    if (item) {
      /* Меню GetCourse переходит по своему обработчику — перехватываем раньше */
      e.preventDefault();
      e.stopImmediatePropagation();
      var bar = document.querySelector('.gc-account-leftbar.expanded');
      if (bar) bar.classList.remove('expanded');
      openPanel();
      return;
    }
    var btn = e.target.closest('.bd-fav-btn, .bd-fav-star');
    if (btn) {
      e.preventDefault();
      e.stopPropagation();
      var id = btn.getAttribute('data-bd-fav-id');
      var meta = btn.classList.contains('bd-fav-btn') ? lessonMeta(document) : listMeta(btn);
      toggle(id, meta);
      if (btn.classList.contains('bd-fav-star') && has(id)) toast('Добавлено в избранное');
      return;
    }
    var rm = e.target.closest('[data-bd-fav-remove]');
    if (rm) {
      e.preventDefault();
      toggle(rm.getAttribute('data-bd-fav-remove'));
    }
  }

  function init() {
    var st = document.createElement('style');
    st.id = 'bd-fav-css';
    st.textContent = CSS;
    document.head.appendChild(st);

    /* На window и в фазе захвата — раньше обработчиков ссылок, в том
       числе тех, что вешает приложение GetCourse */
    window.addEventListener('click', onClick, true);
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closePanel();
    });
    if (location.hash === '#bd-favorites') openPanel();
    window.addEventListener('resize', lessonButton);
    window.addEventListener('hashchange', function () {
      if (location.hash === '#bd-favorites') openPanel();
    });

    refresh();
    sync();

    /* Меню, шапку урока и список GetCourse и наши скрипты дорисовывают
       после загрузки — ставим недостающее, пока всё не встанет */
    var tries = 0;
    var timer = setInterval(function () {
      var menuDone = menuItem();
      var lessonDone = !lessonIdFromUrl() || lessonButton();
      listStars();
      if ((menuDone && lessonDone) || ++tries > 40) clearInterval(timer);
    }, 250);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

} catch (e) { if (window.console) console.error('[bd] favorites.js', e); }
try {
/* ============================================================
   Всплывающий баннер

   Баннер собирают на странице сайта /banners (Сайт → Страницы) — блоком
   HTML с разметкой из docs/инструкция-для-команды.md:
     <div class="fixbanners"><div class="info-wrapper">…баннер…</div></div>
   Скрипт на каждой странице кабинета берёт оттуда содержимое
   .fixbanners .info-wrapper и показывает внизу экрана (вид — 10-banner.css).
   Раньше это делала отдельная тема 94239 — та же разметка работает и так.

   Страница читается не чаще раза в 10 минут (в пределах вкладки).
   Закрытый крестиком баннер не показывается, пока его не поменяют:
   помним «отпечаток» его текста. Пустая страница /banners — баннера нет.
   ============================================================ */

(function () {
  'use strict';

  if (window.bdBannerLoaded) return;
  window.bdBannerLoaded = true;

  var SOURCE = '/banners';
  var CACHE_KEY = 'bd-banner-v1';
  var CACHE_TTL = 10 * 60 * 1000;
  var CLOSED_KEY = 'bd-banner-closed';

  function safeGet(store, key) {
    try { return window[store].getItem(key); } catch (e) { return null; }
  }

  function safeSet(store, key, value) {
    try { window[store].setItem(key, value); } catch (e) { /* приватный режим */ }
  }

  /* Отпечаток текста баннера: поменяли баннер — снова покажем всем */
  function print(html) {
    var text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
    var h = 0;
    for (var i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) | 0;
    return String(h);
  }

  function load() {
    var cached = null;
    try { cached = JSON.parse(safeGet('sessionStorage', CACHE_KEY)); } catch (e) { /* нет кэша */ }
    if (cached && Date.now() - cached.at < CACHE_TTL) return Promise.resolve(cached.html);
    return fetch(SOURCE, { credentials: 'same-origin' }).then(function (r) {
      return r.ok ? r.text() : '';
    }).then(function (page) {
      var doc = new DOMParser().parseFromString(page, 'text/html');
      var box = doc.querySelector('.fixbanners .info-wrapper');
      var html = box ? box.innerHTML.trim() : '';
      safeSet('sessionStorage', CACHE_KEY, JSON.stringify({ at: Date.now(), html: html }));
      return html;
    }).catch(function () { return ''; });
  }

  function show(html) {
    if (!html || !/common-banner/.test(html)) return;
    var id = print(html);
    if (safeGet('localStorage', CLOSED_KEY) === id) return;

    var host = document.querySelector('.top-notification-wrapper');
    if (!host) {
      host = document.createElement('div');
      host.className = 'bd-banner-host';
      document.body.appendChild(host);
    }
    host.innerHTML = html;

    Array.prototype.forEach.call(host.querySelectorAll('.banner-close'), function (btn) {
      btn.setAttribute('role', 'button');
      btn.setAttribute('aria-label', 'Закрыть');
      btn.addEventListener('click', function (e) {
        e.preventDefault();
        var wrap = btn.closest('.common-banner-wrapper');
        if (wrap) wrap.classList.add('hide_banners');
        safeSet('localStorage', CLOSED_KEY, id);
      });
    });
  }

  function init() {
    /* Только в кабинете: гостю (страница входа) баннер не показываем */
    if (!window.accountUserId) return;
    /* Баннер не срочный — после загрузки страницы */
    setTimeout(function () { load().then(show); }, 800);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

} catch (e) { if (window.console) console.error('[bd] banner.js', e); }