/* eslint-disable */
/* timing-data.jsx — типы активностей, библиотека блоков, шаблоны, хелперы. */

(function () {
  /* ---------- Время ---------- */
  function pad(n) { return n < 10 ? '0' + n : '' + n; }
  function fmt(min) {
    min = ((min % 1440) + 1440) % 1440;
    return pad(Math.floor(min / 60)) + ':' + pad(min % 60);
  }
  function parse(str) {
    if (str == null) return null;
    const m = String(str).match(/(\d{1,2})\D*(\d{0,2})/);
    if (!m) return null;
    let h = Math.min(47, parseInt(m[1] || '0', 10));
    let mi = Math.min(59, parseInt(m[2] || '0', 10));
    return h * 60 + mi;
  }
  function dur(min) {
    if (min <= 0) return '0м';
    const h = Math.floor(min / 60), m = min % 60;
    return (h ? h + 'ч' : '') + (m ? ' ' + m + 'м' : '') ;
  }
  let _id = 0;
  function uid() { return 'it' + (++_id) + '_' + Math.random().toString(36).slice(2, 6); }

  /* ---------- Типы активностей (цвет = метка, не единственный сигнал) ---------- */
  const TYPES = {
    logi:     { label: 'Логистика',  color: 'var(--color-ink-400)' },
    setup:    { label: 'Монтаж',     color: 'var(--color-info-500)' },
    tech:     { label: 'Техника',    color: 'var(--color-accent-500)' },
    reh:      { label: 'Репетиция',  color: 'var(--color-warning-500)' },
    program:  { label: 'Программа',  color: 'var(--color-success-500)' },
    catering: { label: 'Кейтеринг',  color: 'var(--color-danger-500)' },
  };
  const TYPE_ORDER = ['logi', 'setup', 'tech', 'reh', 'program', 'catering'];

  /* ---------- Библиотека блоков (drag-источник) ---------- */
  const LIBRARY = [
    { y: 'logi', t: 'Разгрузка оборудования',      d: 45, z: 'Парковка / вход' },
    { y: 'logi', t: 'Завоз декора и мебели',        d: 30, z: 'Служебный вход' },
    { y: 'logi', t: 'Финальная проверка площадки',  d: 30, z: 'Везде' },
    { y: 'setup', t: 'Сборка сцены и подиума',       d: 90, z: 'Главный зал' },
    { y: 'setup', t: 'Брендинг и навигация',         d: 60, z: 'Холл' },
    { y: 'setup', t: 'Флористика и фотозона',        d: 45, z: 'Холл' },
    { y: 'tech', t: 'Монтаж звука и света',          d: 60, z: 'Сцена' },
    { y: 'tech', t: 'Видеостена и контент',          d: 45, z: 'Сцена' },
    { y: 'tech', t: 'Саундчек и настройка',          d: 30, z: 'Сцена' },
    { y: 'reh', t: 'Прогон с ведущим',               d: 45, z: 'Сцена' },
    { y: 'reh', t: 'Репетиция спикеров',             d: 30, z: 'Сцена' },
    { y: 'program', t: 'Регистрация гостей',         d: 45, z: 'Холл' },
    { y: 'program', t: 'Приветственное слово',       d: 10, z: 'Сцена' },
    { y: 'program', t: 'Выступление спикера',        d: 15, z: 'Сцена' },
    { y: 'program', t: 'Нетворкинг',                 d: 60, z: 'Лаунж' },
    { y: 'catering', t: 'Расстановка фуршета',       d: 30, z: 'Лаунж' },
    { y: 'catering', t: 'Welcome-коктейль',          d: 15, z: 'Лаунж' },
    { y: 'catering', t: 'Фуршет',                    d: 90, z: 'Лаунж' },
  ];

  /* ---------- Шаблоны ---------- */
  function S(t) { return { k: 's', t }; }
  function R(y, t, d, w, z, opt) { return Object.assign({ k: 'r', y, t, d, w: w || '', z: z || '' }, opt || {}); }
  function Dn(date, start) { return { k: 'd', date, start: parse(start) }; }

  const TEMPLATES = {
    opening: {
      name: 'Открытие / презентация',
      hint: 'Открытие шоурума, запуск продукта, перерезание ленты',
      meta: { event: 'Открытие шоурума IkonTyres', date: '12 июня 2026', venue: 'Ташкент, ул. Амира Темура 15', guests: '200 гостей', code: '26UZ029', pm: 'Эрбол Рахимов' },
      prepStart: 480, eventStart: 960,
      prep: [
        S('Завоз и логистика'),
        R('logi',  'Разгрузка оборудования и декора',  60, 'Бригада монтажа',     'Парковка / вход'),
        R('setup', 'Сборка сцены и подиума',           90, 'Подрядчик «СтройСцена»', 'Главный зал'),
        S('Техника'),
        R('tech',  'Монтаж звука и света',             60, 'Тех-бригада',         'Сцена'),
        R('tech',  'Видеостена и брендированный контент', 45, 'Видео-инженер',     'Сцена'),
        R('tech',  'Саундчек и настройка микрофонов',  30, 'Звукорежиссёр',       'Сцена'),
        S('Оформление и репетиция'),
        R('setup', 'Брендинг, флористика, фотозона',   75, 'Декор-команда',       'Холл'),
        R('reh',   'Прогон с ведущим и спикерами',     45, 'Режиссёр',            'Сцена'),
        R('catering', 'Расстановка фуршета и бара',    30, 'Кейтеринг «Вкус»',    'Лаунж'),
        R('logi',  'Финальная проверка и клининг',     45, 'Менеджер площадки',   'Везде'),
      ],
      event: [
        S('Сбор гостей'),
        R('program',  'Регистрация и welcome-зона',     45, 'Хостес · 3 чел',     'Холл', { p: 960 }),
        R('catering', 'Приветственный коктейль',        15, 'Кейтеринг',          'Лаунж'),
        S('Официальная часть'),
        R('program',  'Открытие, слово ведущего',        5, 'Ведущий',            'Сцена', { p: 1020 }),
        R('program',  'Речь генерального директора',    10, 'IkonTyres',          'Сцена'),
        R('program',  'Презентация новой линейки шин',  15, 'Бренд-менеджер',     'Сцена'),
        R('program',  'Перерезание ленты, запуск',      10, 'VIP-гости',          'Вход в шоурум'),
        R('tech',     'Лазер-шоу и видеомэппинг',       20, 'Тех-бригада',        'Фасад'),
        S('Неформальная часть'),
        R('catering', 'Фуршет, живая музыка, нетворкинг', 90, 'Кейтеринг · бэнд', 'Лаунж'),
        R('program',  'Розыгрыш призов',                20, 'Ведущий',            'Сцена'),
        R('program',  'Финальное слово, общее фото',    10, 'Ведущий',            'Сцена'),
        S('Завершение'),
        R('logi',     'Проводы гостей, демонтаж',       60, 'Бригада монтажа',    'Везде'),
      ],
    },

    conference: {
      name: 'Конференция / форум',
      hint: 'Деловая программа, сессии, кофе-брейки, панель',
      meta: { event: 'Бизнес-форум «Рост 2026»', date: '20 июня 2026', venue: 'Hyatt Regency, конференц-зал', guests: '350 делегатов', code: '26UZ034', pm: 'Мунира Касым' },
      prepStart: 420, eventStart: 540,
      prep: [
        S('Подготовка зала'),
        R('logi',  'Завоз оборудования и раздаточных',   45, 'Логистика',        'Служебный вход'),
        R('setup', 'Рассадка, бейджи, стойки регистрации', 60, 'Координатор',     'Фойе'),
        R('setup', 'Брендинг сцены и баннеры',           45, 'Декор-команда',    'Зал'),
        S('Техника и прогон'),
        R('tech',  'Монтаж звука, света, проекции',      60, 'Тех-бригада',      'Зал'),
        R('tech',  'Тест презентаций спикеров',          30, 'Видео-инженер',    'Сцена'),
        R('reh',   'Репетиция модератора и тайминга',    30, 'Режиссёр',         'Сцена'),
      ],
      event: [
        S('Регистрация'),
        R('program',  'Регистрация, кофе, нетворкинг',   60, 'Хостес · 4 чел',   'Фойе', { p: 540 }),
        S('Пленарная часть'),
        R('program',  'Открытие, приветствие модератора', 10, 'Модератор',        'Сцена'),
        R('program',  'Кейноут: тренды рынка',           30, 'Спикер 1',         'Сцена'),
        R('program',  'Доклад партнёра',                 25, 'Спикер 2',         'Сцена'),
        R('catering', 'Кофе-брейк',                      20, 'Кейтеринг',        'Фойе'),
        S('Панель и сессии'),
        R('program',  'Панельная дискуссия',             45, 'Модератор · 4 эксперта', 'Сцена'),
        R('program',  'Сессия вопросов и ответов',       20, 'Модератор',        'Сцена'),
        R('catering', 'Обед-фуршет',                     60, 'Кейтеринг',        'Ресторан'),
        S('Завершение'),
        R('program',  'Итоги, награждение, фото',        20, 'Модератор',        'Сцена'),
      ],
    },

    corporate: {
      name: 'Корпоратив / вечеринка',
      hint: 'Праздник компании, шоу-программа, банкет, диджей',
      meta: { event: 'Новогодний корпоратив CCI', date: '26 декабря 2026', venue: 'Banquet Hall «Жемчуг»', guests: '180 гостей', code: '26UZ041', pm: 'Мадина Юсуп' },
      prepStart: 600, eventStart: 1140,
      prep: [
        S('Монтаж и декор'),
        R('logi',  'Завоз декора, мебели, реквизита',    45, 'Логистика',        'Служебный вход'),
        R('setup', 'Сборка сцены и танцпола',            75, 'Монтажники',       'Банкетный зал'),
        R('setup', 'Декор столов, оформление зала',      90, 'Декор-команда',    'Зал'),
        S('Техника и саундчек'),
        R('tech',  'Звук, свет, дым-машины',             60, 'Тех-бригада',      'Сцена'),
        R('tech',  'Саундчек диджея и кавер-бэнда',      45, 'Звукорежиссёр',    'Сцена'),
        R('reh',   'Прогон шоу-номеров с артистами',     45, 'Режиссёр',         'Сцена'),
        R('catering', 'Сервировка банкета',              60, 'Кейтеринг',        'Зал'),
      ],
      event: [
        S('Сбор'),
        R('program',  'Сбор гостей, welcome-зона',       30, 'Хостес',           'Холл', { p: 1140 }),
        S('Банкет и шоу'),
        R('program',  'Слово директора, тост',           15, 'Ведущий',          'Сцена'),
        R('catering', 'Подача горячего, банкет',         45, 'Кейтеринг',        'Зал'),
        R('program',  'Шоу-номер: артисты',              20, 'Артисты',          'Сцена'),
        R('program',  'Конкурсы и игры с гостями',       30, 'Ведущий',          'Зал'),
        R('program',  'Награждение сотрудников года',    25, 'Директор',         'Сцена'),
        S('Дискотека'),
        R('program',  'Выступление кавер-бэнда',         45, 'Кавер-бэнд',       'Сцена'),
        R('program',  'Диджей-сет, танцы',               90, 'DJ',               'Танцпол'),
        R('program',  'Финал, фейерверк, фото',          15, 'Ведущий',          'Терраса'),
      ],
    },

    concert: {
      name: 'Концерт / шоу',
      hint: 'Сцена, саундчеки, артисты, зрительный зал',
      meta: { event: 'Концерт ко Дню города', date: '1 сентября 2026', venue: 'Amphitheatre, открытая площадка', guests: '5 000 зрителей', code: '26UZ048', pm: 'Санжар Усман' },
      prepStart: 360, eventStart: 1140,
      prep: [
        Dn('31 авг, накануне', '08:00'),
        S('Сцена и инфраструктура'),
        R('logi',  'Завоз и разгрузка сценического комплекса', 90, 'Логистика', 'Площадка'),
        R('setup', 'Сборка сцены, рамп, барьеров',       180, 'Сцен-подрядчик', 'Площадка'),
        R('setup', 'Установка экранов и ферм',            90, 'Монтажники',      'Сцена'),
        Dn('1 сен, день концерта', '08:00'),
        S('Свет, звук, видео'),
        R('tech',  'Монтаж линейного массива и мониторов', 90, 'Звук-бригада',  'Сцена'),
        R('tech',  'Монтаж света и спецэффектов',         90, 'Свет-бригада',   'Сцена'),
        R('tech',  'Лайн-чек, саундчек хедлайнера',       60, 'FOH-инженер',    'Сцена'),
        S('Репетиция'),
        R('reh',   'Прогон выходов и переходов',          60, 'Сцен-менеджер',  'Сцена'),
        R('logi',  'Проверка периметра и безопасности',   45, 'Служба охраны',  'Периметр'),
      ],
      event: [
        S('Запуск'),
        R('program',  'Открытие ворот, вход зрителей',    60, 'Стюарды',         'Входы', { p: 1140 }),
        R('program',  'Разогрев: молодые артисты',        40, 'Артисты',         'Сцена'),
        S('Основная программа'),
        R('program',  'Выход хедлайнера',                 75, 'Хедлайнер',       'Сцена'),
        R('tech',     'Пиро- и лазер-шоу финала',         10, 'Тех-бригада',     'Сцена'),
        R('program',  'Бис',                              15, 'Хедлайнер',       'Сцена'),
        S('Завершение'),
        R('logi',     'Вывод зрителей, демонтаж',         90, 'Стюарды · бригада', 'Площадка'),
      ],
    },

    expo: {
      name: 'Выставочный стенд / экспо',
      hint: 'Монтаж стенда, работа на выставке, демонтаж',
      meta: { event: 'Стенд Cloud на ICT Expo', date: '8 октября 2026', venue: 'УзЭкспоцентр, павильон B', guests: 'Стенд 6×8 м', code: '26UZ031', pm: 'Эрбол Рахимов' },
      prepStart: 360, eventStart: 600,
      prep: [
        S('Монтаж стенда'),
        R('logi',  'Завоз конструкций и оборудования',    60, 'Логистика',       'Грузовой въезд'),
        R('setup', 'Сборка каркаса и стен стенда',        120, 'Монтажники',     'Павильон B'),
        R('setup', 'Брендинг, графика, мебель',           75, 'Декор-команда',   'Стенд'),
        S('Техника и контент'),
        R('tech',  'Подключение экранов и демо-зоны',      60, 'Тех-специалист', 'Стенд'),
        R('tech',  'Тест презентаций и интерактива',       30, 'Контент-менеджер', 'Стенд'),
        R('reh',   'Брифинг стендистов',                  30, 'Менеджер стенда', 'Стенд'),
      ],
      event: [
        S('Рабочий день выставки'),
        R('program',  'Открытие, приём первых посетителей', 30, 'Стендисты',      'Стенд', { p: 600 }),
        R('program',  'Демо продукта, лидогенерация',      180, 'Стендисты',      'Демо-зона'),
        R('catering', 'Кофе-пауза для гостей стенда',       30, 'Кейтеринг',      'Лаунж стенда'),
        R('program',  'Презентация на мини-сцене',          20, 'Спикер',         'Мини-сцена'),
        R('program',  'Работа с посетителями, нетворкинг', 150, 'Стендисты',      'Стенд'),
        S('Завершение дня'),
        R('logi',     'Подведение итогов, фиксация лидов',  30, 'Менеджер стенда', 'Стенд'),
        R('logi',     'Консервация стенда на ночь',         20, 'Дежурный',       'Стенд'),
      ],
    },
  };
  const TEMPLATE_ORDER = ['opening', 'conference', 'corporate', 'concert', 'expo'];

  /* ---------- Палитра треков (колонок) сетки мероприятия ---------- */
  const TRACK_PALETTE = [
    { key: 'accent',  color: 'var(--color-accent-500)' },
    { key: 'info',    color: 'var(--color-info-500)' },
    { key: 'success', color: 'var(--color-success-500)' },
    { key: 'warning', color: 'var(--color-warning-500)' },
    { key: 'danger',  color: 'var(--color-danger-500)' },
    { key: 'ink',     color: 'var(--color-ink-400)' },
  ];
  function trackColor(key) { const p = TRACK_PALETTE.find(x => x.key === key); return p ? p.color : 'var(--color-ink-400)'; }
  function nextTrackKey(used) {
    const free = TRACK_PALETTE.map(p => p.key).find(k => !used.includes(k));
    return free || TRACK_PALETTE[used.length % TRACK_PALETTE.length].key;
  }

  /* Ручной cue-sheet для демо-шаблона «Открытие» (start 16:00).
     card: [trackIdx, slotTime, title, dur|null, who|note] */
  const GRIDS = {
    opening: {
      tracks: [['Ведущий', 'success'], ['Звук', 'accent'], ['Экран', 'info'], ['Свет', 'warning'], ['Кейтеринг', 'danger'], ['Зал / гости', 'ink']],
      slots: ['16:00', '16:45', '17:00', '17:05', '17:15', '17:30', '17:40', '18:00', '19:30', '19:50', '20:00'],
      cards: [
        [5, '16:00', 'Регистрация, welcome-зона', 45, 'Хостес · 3'],
        [4, '16:00', 'Welcome-зона готова', null, ''],
        [4, '16:45', 'Приветственный коктейль', 15, ''],
        [1, '16:45', 'Фоновый плейлист', null, ''],
        [0, '17:00', 'Открытие, слово ведущего', 5, 'Ведущий'],
        [1, '17:00', 'Микрофон, стоп фон', null, 'Звукореж.'],
        [2, '17:00', 'Заставка мероприятия', null, ''],
        [0, '17:05', 'Речь ген. директора', 10, 'IkonTyres'],
        [2, '17:05', 'Слайды спикера', null, ''],
        [0, '17:15', 'Презентация линейки шин', 15, 'Бренд-менеджер'],
        [2, '17:15', 'Видеоролик продукта', null, ''],
        [0, '17:30', 'Перерезание ленты, запуск', 10, 'VIP-гости'],
        [5, '17:30', 'VIP к входу в шоурум', null, ''],
        [3, '17:40', 'Лазер-шоу', 20, 'Тех-бригада'],
        [2, '17:40', 'Видеомэппинг на фасаде', null, ''],
        [1, '17:40', 'Музыкальное сопровождение', null, ''],
        [4, '18:00', 'Фуршет', 90, 'Кейтеринг'],
        [1, '18:00', 'Живая музыка / бэнд', 90, ''],
        [5, '18:00', 'Нетворкинг', null, ''],
        [0, '19:30', 'Розыгрыш призов', 20, 'Ведущий'],
        [0, '19:50', 'Финальное слово, фото', 10, 'Ведущий'],
        [5, '20:00', 'Проводы гостей', null, ''],
      ],
    },
  };

  function expandGrid(g) {
    const tracks = g.tracks.map(([name, color]) => ({ id: uid(), name, color }));
    const slots = g.slots.map(t => ({ id: uid(), time: parse(t) }));
    const byTime = {}; slots.forEach(s => { byTime[s.time] = s.id; });
    const cards = g.cards.map(c => ({
      id: uid(), trackId: tracks[c[0]].id, slotId: byTime[parse(c[1])],
      title: c[2], dur: c[3] != null ? c[3] : null, who: c[4] || '', note: '',
    }));
    return { tracks, slots, cards };
  }

  /* Авто-конвертация списка мероприятия в сетку (колонки по типам активности). */
  const TYPE_TRACK = {
    program:  ['Программа / сцена', 'success'],
    tech:     ['Техника', 'accent'],
    catering: ['Кейтеринг', 'danger'],
    logi:     ['Логистика / зал', 'ink'],
    reh:      ['Репетиция', 'warning'],
    setup:    ['Монтаж', 'info'],
  };
  function deriveGrid(eventItems, eventStart) {
    const comp = computeTimes(eventItems, eventStart).filter(c => c.kind === 'row');
    const present = TYPE_ORDER.filter(ty => comp.some(c => c.type === ty));
    const tracks = present.map(ty => ({ id: uid(), name: TYPE_TRACK[ty][0], color: TYPE_TRACK[ty][1], _ty: ty }));
    const trackByTy = {}; tracks.forEach(t => { trackByTy[t._ty] = t.id; });
    const times = Array.from(new Set(comp.map(c => c._start))).sort((a, b) => a - b);
    const slots = times.map(t => ({ id: uid(), time: t }));
    const slotByTime = {}; slots.forEach(s => { slotByTime[s.time] = s.id; });
    const cards = comp.map(c => ({ id: uid(), trackId: trackByTy[c.type], slotId: slotByTime[c._start], title: c.title, dur: c.dur, who: c.who, note: c.note }));
    tracks.forEach(t => delete t._ty);
    return { tracks, slots, cards };
  }
  function blankGrid(start) {
    const tracks = [['Ведущий', 'success'], ['Звук', 'accent'], ['Экран', 'info'], ['Свет', 'warning'], ['Кейтеринг', 'danger'], ['Зал / гости', 'ink']]
      .map(([n, c]) => ({ id: uid(), name: n, color: c }));
    const slots = [start, start + 30, start + 60].map(t => ({ id: uid(), time: t }));
    return { tracks, slots, cards: [] };
  }
  function gridSpan(grid) {
    if (!grid.slots.length) return { start: 0, end: 0, total: 0 };
    const sorted = grid.slots.slice().sort((a, b) => a.time - b.time);
    const start = sorted[0].time;
    let end = sorted[sorted.length - 1].time;
    grid.cards.forEach(c => { const s = grid.slots.find(x => x.id === c.slotId); if (s && c.dur) end = Math.max(end, s.time + c.dur); });
    return { start, end, total: end - start };
  }

  function expand(list) {
    return list.map(it => {
      if (it.k === 's') return { id: uid(), kind: 'section', title: it.t };
      if (it.k === 'd') return { id: uid(), kind: 'day', date: it.date || '', start: it.start != null ? it.start : 540, collapsed: false };
      return { id: uid(), kind: 'row', type: it.y, title: it.t, dur: it.d, who: it.w || '', zone: it.z || '', note: it.n || '', pin: (it.p != null ? it.p : null) };
    });
  }
  function loadTemplate(key) {
    const tpl = TEMPLATES[key];
    return {
      key,
      meta: Object.assign({}, tpl.meta),
      prepStart: tpl.prepStart,
      eventStart: tpl.eventStart,
      prep: expand(tpl.prep),
      event: expand(tpl.event),
      eventGrid: GRIDS[key] ? expandGrid(GRIDS[key]) : deriveGrid(expand(tpl.event), tpl.eventStart),
    };
  }

  /* ---------- Каскад времени (с учётом разделителей дней) ---------- */
  function computeTimes(items, phaseStart) {
    let cursor = phaseStart;
    let dayIdx = 0;
    return items.map(it => {
      if (it.kind === 'day') {
        cursor = (it.start != null ? it.start : cursor);
        dayIdx++;
        return Object.assign({}, it, { _start: cursor, _dayIdx: dayIdx });
      }
      if (it.kind === 'section') return Object.assign({}, it, { _start: cursor, _dayIdx: dayIdx });
      let start = cursor, gap = 0, overlap = 0;
      if (it.pin != null) {
        if (it.pin >= cursor) { gap = it.pin - cursor; start = it.pin; }
        else { overlap = cursor - it.pin; start = it.pin; }
      }
      const end = start + it.dur;
      cursor = end;
      return Object.assign({}, it, { _start: start, _end: end, _gap: gap, _overlap: overlap, _dayIdx: dayIdx });
    });
  }
  function phaseSpan(computed, phaseStart) {
    const rows = computed.filter(c => c.kind === 'row');
    if (!rows.length) return { start: phaseStart, end: phaseStart, total: 0 };
    const start = Math.min(phaseStart, rows[0]._start);
    const end = rows[rows.length - 1]._end;
    return { start, end, total: end - start };
  }

  /* Разбивка подготовки на дни (неявный «День 1» + явные разделители). */
  function dayBlocks(items, phaseStart) {
    const comp = computeTimes(items, phaseStart);
    const days = [];
    let cur = { date: null, start: phaseStart, rows: [] };
    let started = false;
    comp.forEach(it => {
      if (it.kind === 'day') {
        if (started || cur.rows.length) days.push(cur);
        cur = { date: it.date, start: (it.start != null ? it.start : phaseStart), rows: [] };
        started = true;
      } else if (it.kind === 'row') {
        cur.rows.push(it);
      }
    });
    days.push(cur);
    return days.map((d, i) => {
      const rows = d.rows;
      const start = rows.length ? Math.min(d.start, rows[0]._start) : d.start;
      const end = rows.length ? rows[rows.length - 1]._end : d.start;
      const durs = rows.reduce((s, r) => s + (r.dur || 0), 0);
      return { date: d.date, label: d.date || ('День ' + (i + 1)), start, end, dur: durs, count: rows.length };
    });
  }

  window.TimingData = {
    fmt, parse, dur, uid, TYPES, TYPE_ORDER, LIBRARY,
    TEMPLATES, TEMPLATE_ORDER, loadTemplate, expand,
    computeTimes, phaseSpan, dayBlocks,
    TRACK_PALETTE, trackColor, nextTrackKey, blankGrid, gridSpan,
  };
})();
