<script>
  import { onMount, tick } from 'svelte'
  import {
    apiRequest, PROJECT_NAME,
    apiGetProject, apiSaveBlock, apiSaveSettings, apiSaveLetters, apiGetLetter, apiSaveLetter,
    apiDeleteLetter, apiSaveDraft, apiSavePresets, apiSaveNotes, apiGetNote, apiSaveNote,
    apiDeleteNote, apiRenderEmail, apiGetLetterHistory, apiAppendLetterHistory,
    apiGetConfig, apiSaveConfig, apiUploadImage,
    apiGetYonoteStatus, apiYonoteListDatabases, apiYonoteGetProperties, apiYonoteGetRows, apiYonoteUpdateRow,
    apiFtpTest, apiFtpUpload, apiFtpList, apiFtpDelete, apiCheckLinks, apiFeedRefresh, apiFeedSuggest,
    apiSaveStatEntry, apiGetAllStats,
    apiLogin, apiLogout, apiGetMe, apiSavePreferences,
    apiAdminGetUsers, apiAdminCreateUser, apiAdminUpdateUser, apiAdminDeleteUser,
    apiReadPartsFile, apiWritePartsFile,
  } from './lib/api.js'
  import {
    normalizeNewlines, createId, formatLetterDate, buildLetterName,
    escapeHtml, visualizeLineBreaks, escapeRegExp, unquoteValue,
    normalizeHtmlForCopy, parseOptionCode, parseOptionsObject, stringifyOptions,
    downloadFile,
  } from './lib/utils.js'
  import {
    isExtraTextBlock, isListBlock, isProductBlock, trimTrailingEmptyLines, parseBlocks,
    getMixinName, getDefaultMixinFieldLabel, normalizeMixinRules, getMixinRulesForLine,
    getMixinArgCount, isLikelyUiTextValue, guessAutoMixinField, guessObjectFieldByKey,
    shouldExposeObjectField, shouldExposeAutoMixinArg, parseMixinObjectArg, parseQuotedArgs,
    findMixinArgRange, extractMixinArgValue, replaceMixinArgQuoted, replaceMixinArgRaw,
    replaceObjectPropInArg, splitTextField, getParagraphMeta, getListItemRanges,
    getListItemTextLine, buildBaseSchema, parseSections, rebuildContentFromSections,
  } from './lib/parsing.js'
  import { extractTextFromHtml, injectSpellMarks } from './lib/spellcheck.js'
  import backIcon from './icons/back.svg'
  import clearIcon from './icons/clear.svg'
  import filesIcon from './icons/files.svg'
  import saveIcon from './icons/save.svg'
  import stackIcon from './icons/stack.svg'
  import pugIcon from './icons/pug.svg'
  import plusIconRaw from './icons/plus.svg?raw'
  import settingsIcon from './icons/settings.svg'
  import spacerIcon from './icons/spacer.svg'
  import newMailIcon from './icons/newmail.svg'

  const DEFAULT_SOURCE = '/Block.pug'
  const DEFAULT_MIXIN_RULE_TEMPLATES = [
    {
      test: (name) => /^buttonRounded$/i.test(name),
      fields: [
        { argIndex: 0, type: 'mixin-text', label: 'Текст кнопки' },
        { argIndex: 1, type: 'mixin-href', label: 'Ссылка кнопки' },
      ],
    },
    {
      test: (name) => /^products?/i.test(name),
      fields: [
        { argIndex: 0, type: 'mixin-ids', label: 'ID товаров' },
        { argIndex: 1, type: 'mixin-opts', label: 'Опции' },
      ],
    },
    {
      test: (name) => /^product/i.test(name),
      fields: [
        { argIndex: 0, type: 'mixin-ids', label: 'ID товаров' },
        { argIndex: 1, type: 'mixin-opts', label: 'Опции' },
      ],
    },
  ]

  // --- Auth state ---
  let currentUser = null // { id, login, name, role, projects }
  let authChecked = false
  let loginForm = { login: '', password: '' }
  let loginError = ''

  const LOGIN_BACKGROUNDS = [
    '/login-bg/1.jpg',
    '/login-bg/2.jpg',
    '/login-bg/3.jpg',
    '/login-bg/4.jpg',
    '/login-bg/5.jpg',
  ]
  const loginBg = LOGIN_BACKGROUNDS[Math.floor(Math.random() * LOGIN_BACKGROUNDS.length)]
  let loginLoading = false
  // Admin
  let adminUsers = []
  let adminLoading = false
  let adminNewUser = { login: '', password: '', name: '', role: 'user', projects: [] }
  let adminNewProjectDraft = ''
  let adminEditingId = null
  let adminError = ''

  let availableBlocks = []
  let assembledBlocks = []
  let certificateEnabled = false
  let certEditingMode = false
  let certEditorSavedState = null
  let outputPug = ''
  let loadError = ''
  let sourceName = 'Block.pug'
  let currentProject = PROJECT_NAME
  let presets = []
  let presetName = ''
  let typografStatus = {}
  let textFieldRefs = {}
  let linkDrafts = {}
  let selectedBlockIndex = ''
  let blockDropdownOpen = false
  let blockDropdownSearch = ''
  let blockDropdownRef = null
  let previewOpen = {}
  let theme = (() => { try { const t = localStorage.getItem('va-theme'); return t === 'dark' || t === 'light' ? t : 'light' } catch { return 'light' } })()
  let addMenuFor = null
  let pugOpen = {}
  let activePage = 'builder'
  let outputPanelMode = 'preview'
  let settingsTab = 'project'
  let settings = { globalSpacing: 40, blocks: {}, emailGenProject: '', mixinRules: [], mixinRulesInitialized: false }
  let settingsBlockIndex = 0
  let settingsBlockName = ''
  let settingsTemplate = ''
  let baseSchema = []
  let pugOutputRef
  let blockRanges = {}
  let activeBlockId = null
  let history = []
  let settingsReady = false
  let showPresetInput = false
  let presetSearch = ''
  let presetsCollapsed = true
  let presetSort = 'desc'
  let productOptionsList = []
  let notes = []
  let currentNoteId = ''
  let noteContent = ''
  let noteSearch = ''
  let showNoteModal = false
  let noteTitleDraft = ''
  let noteError = ''
  let noteTextareaRef
  let sidebarNoteTextareaRef
  let sidebarNoteCollapsed = true
  let assemblyInfo = null
  let assemblyCardHidden = false
  let isDirty = false
  let isSaving = false
  let saveErrorMsg = ''
  let saveTimer = null
  let pendingSave = false
  let blocksScrollRef
  let pendingFocusId = ''
  let showPresetModal = false
  let presetNameDraft = ''
  let presetSourceLetter = null
  let presetError = ''
  let letters = []
  let currentLetterId = ''
  let currentLetterName = ''
  let currentLetterTitle = ''
  let currentLetterDate = ''
  let isEditingLetterName = false
  let letterNameDraft = ''
  let letterNameInput
  let isEditingLetterDate = false
  let letterDateDraft = ''
  let letterDateInput
  let showLetterModal = false
  let letterModalMode = 'new'
  let newLetterTitle = ''
  let newLetterDate = ''
  let newLetterError = ''
  let letterSearch = ''
  let showHistoryFor = null
  let historySnapshots = []
  let historyLoading = false
  let showStartupModal = false
  let startupLastLetter = null
  let startupPlanRows = []
  let previewHtml = ''
  let rawPreviewHtml = ''
  let previewLoading = false
  let previewError = ''
  let previewGeneratedAt = ''
  let previewFlash = false
  let unavailableProducts = []
  let feedSyncedAt = null
  let suggestForProduct = null
  let unavailableCollapsed = true
  let suggestHoverItem = null
  let suggestHoverPos = { x: 0, y: 0 }
  let suggestLoading = false
  let suggestResults = []
  let suggestSearch = ''
  let previewSourcePug = ''
  let previewStale = true
  let spellCheckActive = false
  let spellLoading = false
  let spellErrors = []
  let spellerHtml = ''
  let linkCheckActive = false
  let linkCheckLoading = false
  let linkCheckResults = []
  let previewIframeRef
  let autoRenderTimer = null
  let quickEditBlock = null
  let quickEditField = null
  let quickEditValue = ''
  let quickEditPos = { top: 0, left: 0 }
  let quickEditVisible = false
  let customBlockName = ''
  let customBlockContent = ''
  let customBlockError = ''
  let editingCustomBlockId = null
  let templatesExpanded = new Set()
  let templateSearch = ''
  const PREVIEW_TARGET_WIDTH = 600
  let previewContentWidth = 600
  let previewContentHeight = 900
  let previewScale = 1
  let previewScaledWidth = 600
  let previewScaledHeight = 900
  let previewZoom = 100
  try { previewZoom = Number(localStorage.getItem('va-previewZoom')) || 100 } catch(e) {}

  // --- Yonote / Plan page ---
  let yonoteStatus = { configured: false, connected: false }
  let yonoteTokenDraft = '', yonoteBaseUrlDraft = '', yonoteSaving = false, yonoteConfigError = '', yonoteConfigStatus = ''
  let yonoteDatabases = [], yonoteDbLoading = false
  let yonoteCfgDatabaseId = '', yonoteCfgDateProp = '', yonoteCfgSubjectProp = '', yonoteCfgPreheaderProp = '', yonoteCfgStatusProp = ''
  let yonoteCfgStatusFilter = []
  let yonoteCfgSentStatus = ''  // статус который ставит кнопка "Отправлено"
  let yonoteDbProperties = [], yonotePropsLoading = false
  let yonoteTestResult = '', yonoteTestLoading = false
  // --- Image upload ---
  let uploadBaseUrl = '', uploadSaving = false, uploadConfigError = '', uploadConfigStatus = ''
  let uploadingFields = {}
  let dragOverField = null
  // --- FTP/SFTP upload ---
  let ftpProtocol = 'ftp', ftpHost = '', ftpPort = '', ftpUser = '', ftpPassword = ''
  let ftpRemotePath = '', ftpBaseUrl = ''
  let mailingServiceLabel = '', mailingServiceUrl = ''
  let feedUrlDraft = ''
  let ftpTestResult = '', ftpTestLoading = false
  let showFtpGalleryModal = false, ftpGalleryImages = [], ftpGalleryLoading = false, ftpGalleryError = ''
  let ftpDeletePending = null
  let ftpGalleryFolder = '', ftpUploadFileName = '', ftpUploadFile = null, ftpUploading = false, ftpUploadError = ''
  let ftpGalleryTargetField = null
  const ftpGalleryCache = {}  // { [folder]: files[] }
  // --- Plan page ---
  let planRows = [], planLoading = false, planError = '', planLoadedAt = null, planWarnings = []
  let planPollingTimer = null, planNotificationsEnabled = false
  let toasts = []
  let toastId = 0
  let planEditingRowId = null, planSavingRowId = null, planSaveError = ''
  // --- ID Pool ---
  let idPoolInput = '', idPoolQueue = [], idPoolOpen = false
  // --- Image auto-numbering ---
  let imageBaseUrl = '', imageExt = '.png'
  let yonoteStatusOptions = [] // заполняется из свойств Yonote при загрузке плана
  $: planStatusOptions = yonoteStatusOptions.length > 0
    ? yonoteStatusOptions
    : [...new Set(planRows.map(r => r.status).filter(Boolean))].sort()
  let planCalMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1)
  let planCalSelectedDate = ''
  let planShowDone = false
  const planTodayIso = planDateToIso(new Date())

  $: planDateMap = (() => {
    const map = new Map()
    for (const row of planRows) {
      const key = planDateToIso(row.date)
      if (!map.has(key)) map.set(key, [])
      map.get(key).push(row)
    }
    return map
  })()

  $: planCalDays = (() => {
    const year = planCalMonth.getFullYear()
    const month = planCalMonth.getMonth()
    const firstDay = new Date(year, month, 1)
    const lastDay = new Date(year, month + 1, 0)
    const startDow = (firstDay.getDay() + 6) % 7
    const days = []
    for (let i = startDow - 1; i >= 0; i--) {
      const d = new Date(year, month, -i)
      const iso = planDateToIso(d)
      days.push({ date: d, current: false, isoStr: iso, rows: planDateMap.get(iso) || [] })
    }
    for (let n = 1; n <= lastDay.getDate(); n++) {
      const d = new Date(year, month, n)
      const iso = planDateToIso(d)
      days.push({ date: d, current: true, isoStr: iso, rows: planDateMap.get(iso) || [] })
    }
    while (days.length < 42) {
      const prev = days[days.length - 1].date
      const d = new Date(prev.getFullYear(), prev.getMonth(), prev.getDate() + 1)
      const iso = planDateToIso(d)
      days.push({ date: d, current: false, isoStr: iso, rows: planDateMap.get(iso) || [] })
    }
    return days
  })()

  function prevPlanMonth() {
    planCalMonth = new Date(planCalMonth.getFullYear(), planCalMonth.getMonth() - 1, 1)
  }
  function nextPlanMonth() {
    planCalMonth = new Date(planCalMonth.getFullYear(), planCalMonth.getMonth() + 1, 1)
  }
  async function selectPlanDate(iso) {
    planCalSelectedDate = planCalSelectedDate === iso ? '' : iso
    if (planCalSelectedDate) {
      await tick()
      document.getElementById(`plan-day-${planCalSelectedDate}`)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
    }
  }

  $: planDayGroups = (() => {
    const rows = planShowDone ? planRows : planRows.filter(r => planStatusClass(r.status) !== 'done')
    const map = new Map()
    for (const row of rows) {
      const key = planDateToIso(row.date)
      if (!map.has(key)) map.set(key, { date: row.date, isoStr: key, rows: [] })
      map.get(key).rows.push(row)
    }
    return [...map.values()]
  })()

  $: planPeriodGroups = (() => {
    const now = new Date()
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
    const tomorrow = new Date(today.getTime() + 86400000)
    const dayOfWeek = today.getDay() === 0 ? 7 : today.getDay()
    const endOfWeek = new Date(today.getTime() + (7 - dayOfWeek) * 86400000)
    const endOfNextWeek = new Date(endOfWeek.getTime() + 7 * 86400000)
    const periods = [
      { key: 'overdue', label: 'Просроченные', accent: 'error', groups: [] },
      { key: 'today',    label: 'Сегодня',              accent: 'urgent', groups: [] },
      { key: 'tomorrow', label: 'Завтра',                accent: 'warn',   groups: [] },
      { key: 'week',     label: 'На этой неделе',        accent: '',       groups: [] },
      { key: 'next',     label: 'Следующая неделя',      accent: '',       groups: [] },
      { key: 'later',    label: 'Позже',                 accent: '',       groups: [] },
    ]
    for (const dg of planDayGroups) {
      const d = dg.date
      if (d < today)         periods[0].groups.push(dg)
      else if (d.getTime() === today.getTime())    periods[1].groups.push(dg)
      else if (d.getTime() === tomorrow.getTime()) periods[2].groups.push(dg)
      else if (d <= endOfWeek)     periods[3].groups.push(dg)
      else if (d <= endOfNextWeek) periods[4].groups.push(dg)
      else                         periods[5].groups.push(dg)
    }
    return periods.filter(p => p.groups.length > 0)
  })()

  $: acardPlanRows = (() => {
    if (planRows.length === 0) return []
    return planRows.filter(r => r.project === currentProject && r.date)
  })()

  let assemblyAutoMatched = false
  $: if (!assemblyInfo && !assemblyAutoMatched && currentLetterTitle && currentLetterDate && planRows.length > 0) {
    assemblyAutoMatched = true
    const match = planRows.find(r => r.project === currentProject && r.subject === currentLetterTitle && planDateToIso(r.date) === currentLetterDate)
    if (match) {
      assemblyInfo = { subject: match.subject, preheader: match.preheader, status: match.status, dateStr: match.dateStr, project: match.project, rowId: match.rowId, extra: extraWithoutIds(match.extra) }
      const idsFromMatch = extractIdsFromExtra(match.extra)
      if (idsFromMatch && !idPoolInput) { idPoolInput = idsFromMatch; applyIdPool(); idPoolOpen = true }
      assemblyCardHidden = false
    }
  }

  let acardCalCollapsed = false
  let acardListCollapsed = true
  let acardCalMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1)

  function acardCalDays(month, rows) {
    const year = month.getFullYear(), mo = month.getMonth()
    const firstDay = new Date(year, mo, 1)
    let startDow = firstDay.getDay(); if (startDow === 0) startDow = 7
    const daysInMonth = new Date(year, mo + 1, 0).getDate()
    const today = new Date(); today.setHours(0,0,0,0)
    const cells = []
    for (let i = 1; i < startDow; i++) cells.push(null)
    for (let d = 1; d <= daysInMonth; d++) {
      const date = new Date(year, mo, d)
      const iso = planDateToIso(date)
      const dayRows = rows.filter(r => planDateToIso(r.date) === iso)
      const isToday = date.getTime() === today.getTime()
      const isPast = date < today
      cells.push({ d, date, iso, rows: dayRows, isToday, isPast })
    }
    return cells
  }

  let acardCalInitialized = false
  $: if (acardPlanRows.length > 0 && !acardCalInitialized) {
    acardCalInitialized = true
    const nearest = acardPlanRows.reduce((best, r) => {
      const diff = Math.abs(r.date.getTime() - Date.now())
      return diff < best.diff ? { diff, date: r.date } : best
    }, { diff: Infinity, date: null })
    if (nearest.date) {
      acardCalMonth = new Date(nearest.date.getFullYear(), nearest.date.getMonth(), 1)
    }
  }
  $: acardCalCells = acardCalDays(acardCalMonth, acardPlanRows)

  function acardCalPrev() { acardCalMonth = new Date(acardCalMonth.getFullYear(), acardCalMonth.getMonth() - 1, 1) }
  function acardCalNext() { acardCalMonth = new Date(acardCalMonth.getFullYear(), acardCalMonth.getMonth() + 1, 1) }

  function planStatusClass(status) {
    const s = (status || '').toLowerCase()
    if (s.includes('отправлен') || s.includes('готов') || s.includes('выслан')) return 'done'
    if (s.includes('сборк') || s.includes('поставлен') || s.includes('очередь') || s.includes('отправк')) return 'queue'
    if (s.includes('рисов') || s.includes('визуал') || s.includes('разработ') || s.includes('передан')) return 'wip'
    return ''
  }

  const plusIconSvg = plusIconRaw.replace(/stroke="[^"]*"/g, 'stroke="currentColor"')

  // --- Stats / time tracking ---
  let statsTracking = null // { letterId, letterName, project, tag, startedAt, activeSeconds, htmlCopied, interval }
  let allStats = {} // { project: [entries] } for stats page
  let statsPageLoading = false

  function statsStart(letterId, letterName, project, tag) {
    statsStop()
    const t = {
      letterId, letterName, project, tag,
      startedAt: new Date().toISOString(),
      activeSeconds: 0,
      htmlCopied: false,
      interval: null,
      lastTick: Date.now(),
    }
    t.interval = setInterval(() => {
      if (t.htmlCopied || document.visibilityState === 'visible') {
        const now = Date.now()
        const elapsed = Math.min((now - t.lastTick) / 1000, 5)
        t.activeSeconds += elapsed
        t.lastTick = now
      } else {
        t.lastTick = Date.now()
      }
    }, 1000)
    statsTracking = t
  }

  function statsMarkHtmlCopied() {
    if (statsTracking) statsTracking.htmlCopied = true
  }

  async function statsStop(finalStatus) {
    const t = statsTracking
    if (!t) return
    clearInterval(t.interval)
    statsTracking = null
    const minutes = Math.round(t.activeSeconds / 60)
    if (minutes < 1 && !finalStatus) return
    try {
      await apiSaveStatEntry(t.project, {
        letterId: t.letterId,
        letterName: t.letterName,
        tag: t.tag || '',
        startedAt: t.startedAt,
        completedAt: finalStatus ? new Date().toISOString() : '',
        minutes: Math.max(1, minutes),
        status: finalStatus || '',
      })
    } catch {}
  }

  $: statsMonthGroups = (() => {
    const monthMap = new Map()
    for (const [proj, entries] of Object.entries(allStats)) {
      for (const e of entries) {
        const d = new Date(e.startedAt || e.completedAt)
        const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
        if (!monthMap.has(key)) monthMap.set(key, [])
        monthMap.get(key).push({ ...e, project: proj })
      }
    }
    return [...monthMap.entries()].sort((a, b) => b[0].localeCompare(a[0]))
  })()

  async function loadAllStats() {
    statsPageLoading = true
    try {
      const data = await apiGetAllStats()
      allStats = data.stats || {}
    } catch { allStats = {} }
    statsPageLoading = false
  }

  async function handleLogin() {
    loginError = ''
    loginLoading = true
    try {
      const data = await apiLogin(loginForm.login, loginForm.password)
      currentUser = data.user
      loginForm = { login: '', password: '' }
      await initProjects()
      await loadProject(currentProject)
      await loadYonoteStatus()
      settingsReady = true
    } catch (e) {
      loginError = e?.message === 'invalid_credentials' ? 'Неверный логин или пароль' : (e?.message || 'Ошибка входа')
    }
    loginLoading = false
  }

  async function handleLogout() {
    await apiLogout()
    currentUser = null
    authChecked = true
  }

  async function loadAdminUsers() {
    adminLoading = true
    try {
      const data = await apiAdminGetUsers()
      adminUsers = data.users || []
    } catch { adminUsers = [] }
    adminLoading = false
  }

  async function adminCreateUser() {
    adminError = ''
    if (!adminNewUser.login || !adminNewUser.password) { adminError = 'Логин и пароль обязательны'; return }
    try {
      await apiAdminCreateUser(adminNewUser)
      adminNewUser = { login: '', password: '', name: '', role: 'user', projects: [] }
      await loadAdminUsers()
    } catch (e) { adminError = e?.message || 'Ошибка' }
  }

  async function adminUpdateUser(id, data) {
    adminError = ''
    try {
      await apiAdminUpdateUser(id, data)
      await loadAdminUsers()
    } catch (e) { adminError = e?.message || 'Ошибка' }
  }

  async function adminDeleteUser(id) {
    if (!confirm('Удалить пользователя?')) return
    try {
      await apiAdminDeleteUser(id)
      await loadAdminUsers()
    } catch (e) { adminError = e?.message || 'Ошибка' }
  }

  $: setBodyTheme(theme)

  onMount(async () => {
    const meData = await apiGetMe()
    if (meData?.user) {
      currentUser = meData.user
    }
    authChecked = true
    if (!currentUser) return
    await initProjects()
    await loadProject(currentProject)
    await loadYonoteStatus()
    settingsReady = true
    const validPages = ['builder', 'settings', 'presets', 'notes', 'plan', 'stats', 'admin']
    const serverPage = currentUser?.activePage
    const savedPage = localStorage.getItem('va-active-page')
    const restoredPage = validPages.includes(serverPage) ? serverPage : validPages.includes(savedPage) ? savedPage : null
    if (restoredPage) setActivePage(restoredPage)

    const userTheme = currentUser?.theme
    const savedTheme = localStorage.getItem('va-theme')
    if (userTheme === 'dark' || userTheme === 'light') {
      theme = userTheme
    } else if (savedTheme === 'dark' || savedTheme === 'light') {
      theme = savedTheme
    }
    if (currentUser?.previewZoom && currentUser.previewZoom >= 40 && currentUser.previewZoom <= 100) {
      previewZoom = currentUser.previewZoom
    }
    enablePlanNotifications()
    const planCache = loadPlanCache()
    if (planCache && planCache.rows.length > 0) {
      planRows = planCache.rows
      planLoadedAt = planCache.at
    }
    loadPlan(true)
    startPlanPolling()
    applyAutoTitles()
    let autoTitlesTimer = null
    const mutationObserver = new MutationObserver(() => {
      if (autoTitlesTimer) return
      autoTitlesTimer = setTimeout(() => { autoTitlesTimer = null; applyAutoTitles() }, 200)
    })
    mutationObserver.observe(document.body, { childList: true, subtree: true })
    const onResize = () => fitPreviewFrame()
    window.addEventListener('resize', onResize)
    const onMessage = (e) => {
      if (e.data?.type === 'va-preview-click') handlePreviewElementClick(e.data)
    }
    window.addEventListener('message', onMessage)
    document.addEventListener('click', closeBlockDropdown)
    return () => {
      mutationObserver.disconnect()
      window.removeEventListener('resize', onResize)
      window.removeEventListener('message', onMessage)
      document.removeEventListener('click', closeBlockDropdown)
      stopPlanPolling()
      clearTimeout(autoRenderTimer)
      statsStop()
    }
  })

  $: settingsBlockName = allBlocks[settingsBlockIndex]?.name ?? ''
  $: settingsTemplate = settingsBlockName ? getEffectiveTemplate(settingsBlockName) : ''
  $: baseSchema = buildBaseSchema(settingsTemplate, settingsBlockName, settings?.mixinRules)
  $: settings?.accentColor && setAccentColor(settings.accentColor)
  $: customBlocksList = (settings?.customBlocks || []).map(cb => ({
    name: cb.name, content: cb.content, isCustom: true, customId: cb.id,
  }))
  $: allBlocks = (() => {
    const seen = new Set()
    const result = []
    for (const b of [...availableBlocks, ...customBlocksList]) {
      if (!seen.has(b.name)) { seen.add(b.name); result.push(b) }
    }
    return result
  })()
  $: filteredTemplateBlocks = templateSearch.trim()
    ? allBlocks.filter(b => b.name.toLowerCase().includes(templateSearch.toLowerCase()))
    : allBlocks
  $: allSpacingEnabled = allBlocks.every(b => (settings?.blocks?.[b.name]?.spacingEnabled ?? true))
  $: quickBlocksView = (() => {
    const raw = getQuickBlocks(settings)
    if (allBlocks.length === 0) return raw
    const allowed = new Set(allBlocks.map(b => b.name))
    return raw.filter(n => allowed.has(n))
  })()
  $: productOptionsList = (settings?.productOptions || [])
    .map((opt) => ({
      label: opt.label || opt.code,
      code: opt.code || '',
    }))
    .filter((opt) => opt.code)
  $: filteredLetters = letterSearch.trim()
    ? letters.filter((l) => l.name.toLowerCase().includes(letterSearch.toLowerCase()))
    : letters
  $: previewStale = previewSourcePug !== outputPug
  $: previewScaledHeight = Math.max(420, Math.round(previewContentHeight * previewScale))
  $: if (!previewHtml) {
    previewScale = 1
    previewContentWidth = PREVIEW_TARGET_WIDTH
    previewContentHeight = 900
    previewScaledWidth = PREVIEW_TARGET_WIDTH
  }

  async function loadFromUrl(url, label) {
    loadError = ''
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error(`Не удалось загрузить ${label}`)
      const text = await res.text()
      sourceName = label
      availableBlocks = parseBlocks(text)
      maybeAutoInitMixinRules()
      await apiSaveBlock(currentProject, { sourceName: label, blockText: text })
      assembledBlocks = []
      rebuildOutput()
    } catch (err) {
      loadError = err.message
    }
  }

  async function handleFile(event) {
    const file = event.target.files?.[0]
    if (!file) return
    const text = await file.text()
    sourceName = file.name
    availableBlocks = parseBlocks(text)
    maybeAutoInitMixinRules()
    await apiSaveBlock(currentProject, { sourceName: file.name, blockText: text })
    assembledBlocks = []
    rebuildOutput()
  }

  async function initProjects() {
    currentProject = PROJECT_NAME
  }

  async function loadProject(name, forcePickLetter = false) {
    statsStop()
    currentProject = name
    assemblyInfo = null
    assemblyCardHidden = false
    acardCalInitialized = false
    const data = await apiGetProject(name)
    sourceName = data.meta?.sourceName || sourceName
    availableBlocks = parseBlocks(data.blockText || '')
    settings = {
      globalSpacing: 40,
      blocks: {},
      accentColor: '#6b7280',
      projectTitle: '',
      emailGenProject: '',
      productOptions: [],
      mixinRules: [],
      mixinRulesInitialized: false,
      sidebarNote: '',
      ...(data.settings || {}),
    }
    const hadQuickBlocks = Array.isArray(settings.quickBlocks)
    const hasMixinRules =
      data.settings && Object.prototype.hasOwnProperty.call(data.settings, 'mixinRules')
    let normalizedSettings = ensureQuickBlocks(settings)
    if (hasMixinRules) {
      normalizedSettings = {
        ...normalizedSettings,
        mixinRules: normalizeMixinRules(normalizedSettings.mixinRules),
        mixinRulesInitialized:
          data.settings && Object.prototype.hasOwnProperty.call(data.settings, 'mixinRulesInitialized')
            ? Boolean(normalizedSettings.mixinRulesInitialized)
            : true,
      }
    } else {
      const defaultMixinRules = buildDefaultMixinRulesFromBlocks(allBlocks)
      normalizedSettings = {
        ...normalizedSettings,
        mixinRules: defaultMixinRules,
        mixinRulesInitialized: defaultMixinRules.length > 0,
      }
    }
    settings = normalizedSettings
    const _yc = normalizedSettings.yonoteConfig || {}
    yonoteCfgDatabaseId = _yc.databaseId || ''
    yonoteCfgDateProp = _yc.properties?.date || ''
    yonoteCfgSubjectProp = _yc.properties?.subject || ''
    yonoteCfgPreheaderProp = _yc.properties?.preheader || ''
    yonoteCfgStatusProp = _yc.properties?.status || ''
    yonoteCfgStatusFilter = _yc.statusFilter || []
    yonoteCfgSentStatus = _yc.sentStatus || ''
    if (yonoteCfgDatabaseId) loadYonoteDbProperties(yonoteCfgDatabaseId)
    const _ftp = normalizedSettings.ftpConfig || {}
    ftpProtocol = _ftp.protocol || 'ftp'; ftpHost = _ftp.host || ''; ftpPort = _ftp.port || ''
    ftpUser = _ftp.user || ''; ftpPassword = ''; ftpRemotePath = _ftp.remotePath || ''; ftpBaseUrl = _ftp.baseUrl || ''
    const _ms = normalizedSettings.mailingService || {}
    mailingServiceLabel = _ms.label || ''; mailingServiceUrl = _ms.url || ''
    feedUrlDraft = normalizedSettings.feedUrl || ''
    const _gender = normalizedSettings.genderPaths || {}
    genderHeaderFemale = _gender.headerFemale || ''; genderHeaderMale = _gender.headerMale || ''
    genderFooterFemale = _gender.footerFemale || ''; genderFooterMale = _gender.footerMale || ''
    imageBaseUrl = normalizedSettings.imageBaseUrl || ''; imageExt = normalizedSettings.imageExt || '.png'
    if (!hadQuickBlocks || (!hasMixinRules && normalizedSettings.mixinRulesInitialized)) {
      saveSettings(normalizedSettings)
    }
    quickBlocksView = hadQuickBlocks
      ? normalizedSettings.quickBlocks
      : normalizeQuickBlocks(defaultQuickBlocks)
    presets = data.presets || []
    letters = Array.isArray(data.letters?.list) ? data.letters.list : []
    currentLetterId = data.letters?.currentId || ''
    currentLetterName = ''
    currentLetterTitle = ''
    currentLetterDate = ''
    quickBlockCandidate = ''
    notes = Array.isArray(data.notes?.list) ? data.notes.list : []
    currentNoteId = data.notes?.currentId || ''
    noteContent = ''
    // Start both fetches in parallel
    const notePromise = currentNoteId ? apiGetNote(name, currentNoteId).catch(() => null) : null
    const normalizedDraft = normalizeDraftBlocks(data.draft || [])
    let normalizedBlocks = normalizedDraft
    if (forcePickLetter) {
      startupLastLetter = currentLetterId ? letters.find(l => l.id === currentLetterId) || null : null
      const filterProject = (rows) => rows.filter(r => r.project === name)
      startupPlanRows = filterProject(planRows)
      currentLetterId = ''
      showStartupModal = true
      if (startupPlanRows.length === 0) {
        fetchPlanRows().then(({ rows }) => {
          planRows = rows
          planLoadedAt = new Date()
          savePlanCache(rows, planLoadedAt)
          startupPlanRows = filterProject(rows)
        }).catch(() => {})
      }
    } else if (currentLetterId) {
      const letterData = await apiGetLetter(name, currentLetterId).catch(() => null)
      const letter = letterData?.letter
      if (letter && Array.isArray(letter.blocks)) {
        normalizedBlocks = normalizeDraftBlocks(letter.blocks || [])
        currentLetterName = letter.name || ''
        currentLetterTitle = letter.title || ''
        currentLetterDate = letter.date || ''
        assemblyInfo = letter.assemblyInfo || null
      }
      if (!currentLetterName) {
        const meta = letters.find((item) => item.id === currentLetterId)
        if (meta) {
          currentLetterName = meta.name || ''
          currentLetterTitle = meta.title || ''
          currentLetterDate = meta.date || ''
        }
      }
    } else {
      showStartupModal = true
    }
    assembledBlocks = mapDraftBlocks(normalizedBlocks)
    rebuildOutput()
    previewHtml = ''
    previewError = ''
    previewGeneratedAt = ''
    previewSourcePug = ''
    outputPanelMode = 'preview'
    isDirty = false
    selectedBlockIndex = ''
    settingsBlockIndex = 0
    if (currentNoteId) {
      const noteData = await notePromise
      const note = noteData?.note
      if (note) {
        noteContent = note.content || ''
        await tick()
        autoResizeTextarea(noteTextareaRef)
      }
    await tick()
    autoResizeTextarea(sidebarNoteTextareaRef)
  }
  }


  function mapDraftBlocks(list) {
    return list.map((block) => ({
      id: createId(),
      name: block.name,
      content: block.content,
      collapsed: block.collapsed ?? true,
      // Always rebuild schema from current content to avoid stale field sets from old saved drafts.
      schema: buildBaseSchema(block.content, block.name, settings?.mixinRules),
      addSpacing: block.addSpacing ?? true,
      spacing: typeof block.spacing === 'number' ? block.spacing : undefined,
      segment: block.segment || 'common',
    }))
  }

  async function openLetter(letter) {
    if (!letter?.id) return
    assemblyAutoMatched = false
    currentLetterId = letter.id
    currentLetterName = letter.name || ''
    currentLetterTitle = letter.title || ''
    currentLetterDate = letter.date || ''
    await apiSaveLetters(currentProject, { list: letters, currentId: currentLetterId })
    try {
      const data = await apiGetLetter(currentProject, letter.id)
      const l = data?.letter
      const normalized = normalizeDraftBlocks(l?.blocks || [])
      assembledBlocks = mapDraftBlocks(normalized)
      certificateEnabled = l?.certificateEnabled ?? false
      assemblyInfo = l?.assemblyInfo || null
      if (!assemblyInfo && letter.title && letter.date) {
        const row = planRows.find(r => r.project === currentProject && r.subject === letter.title && planDateToIso(r.date) === letter.date)
        if (row) {
          assemblyInfo = { subject: row.subject, preheader: row.preheader, status: row.status, dateStr: row.dateStr, project: row.project, rowId: row.rowId, extra: extraWithoutIds(row.extra) }
          const idsFromRow = extractIdsFromExtra(row.extra)
          if (idsFromRow && !idPoolInput) { idPoolInput = idsFromRow; applyIdPool(); idPoolOpen = true }
        }
      }
      assemblyCardHidden = false
    } catch {
      assembledBlocks = []
    }
    const tag = (assemblyInfo?.extra || []).find(e => !e.link && !isUrl(e.value))?.value || ''
    statsStart(letter.id, letter.name || letter.title || '', currentProject, tag)
    rebuildOutput()
    setActivePage('builder')
  }

  async function deleteLetter(letter) {
    if (!letter?.id) return
    const confirmed = typeof window === 'undefined' || window.confirm('Удалить письмо?')
    if (!confirmed) return
    await apiDeleteLetter(currentProject, letter.id)
    letters = letters.filter((item) => item.id !== letter.id)
    if (currentLetterId === letter.id) {
      currentLetterId = ''
      currentLetterName = ''
      currentLetterTitle = ''
      currentLetterDate = ''
      assembledBlocks = []
      rebuildOutput()
    }
    await apiSaveLetters(currentProject, { list: letters, currentId: currentLetterId })
  }

  async function duplicateLetter(letter) {
    if (!letter?.id) return
    let blocks = []
    try {
      const data = await apiGetLetter(currentProject, letter.id)
      blocks = normalizeDraftBlocks(data?.letter?.blocks || [])
    } catch {}
    const id = createId()
    const title = 'Копия — ' + (letter.title || letter.name)
    const date = letter.date || new Date().toISOString().slice(0, 10)
    const name = buildLetterName(title, date)
    const now = new Date().toISOString()
    const nextLetter = { id, name, title, date, createdAt: now, updatedAt: now }
    letters = [nextLetter, ...letters]
    currentLetterId = id
    currentLetterName = name
    currentLetterTitle = title
    currentLetterDate = date
    assembledBlocks = mapDraftBlocks(blocks)
    rebuildOutput()
    await apiSaveLetters(currentProject, { list: letters, currentId: id })
    await apiSaveLetter(currentProject, { ...nextLetter, blocks: buildDraftPayload() })
    setActivePage('builder')
  }

  async function openHistory(letter) {
    if (showHistoryFor?.id === letter.id) {
      showHistoryFor = null
      return
    }
    showHistoryFor = letter
    historyLoading = true
    historySnapshots = []
    try {
      const data = await apiGetLetterHistory(currentProject, letter.id)
      historySnapshots = data?.history || []
    } catch {}
    historyLoading = false
  }

  async function restoreFromHistory(snapshot) {
    if (!showHistoryFor) return
    const letter = showHistoryFor
    showHistoryFor = null
    currentLetterId = letter.id
    currentLetterName = letter.name || ''
    currentLetterTitle = letter.title || ''
    currentLetterDate = letter.date || ''
    assembledBlocks = mapDraftBlocks(normalizeDraftBlocks(snapshot.blocks || []))
    rebuildOutput()
    setActivePage('builder')
  }

  function openNoteModal() {
    noteTitleDraft = ''
    noteError = ''
    showNoteModal = true
  }

  async function createNote() {
    const title = noteTitleDraft.trim()
    if (!title) {
      noteError = 'Укажи название заметки.'
      return
    }
    const now = new Date().toISOString()
    const id = createId()
    const newNote = { id, title, createdAt: now, updatedAt: now }
    notes = [newNote, ...notes]
    currentNoteId = id
    noteContent = ''
    showNoteModal = false
    await apiSaveNotes(currentProject, { list: notes, currentId: currentNoteId })
    await apiSaveNote(currentProject, { ...newNote, content: '' })
    setActivePage('notes')
  }

  async function openNote(note) {
    if (!note?.id) return
    if (currentNoteId === note.id) {
      currentNoteId = ''
      noteContent = ''
      await apiSaveNotes(currentProject, { list: notes, currentId: currentNoteId })
      return
    }
    currentNoteId = note.id
    noteContent = ''
    await apiSaveNotes(currentProject, { list: notes, currentId: currentNoteId })
    try {
      const data = await apiGetNote(currentProject, note.id)
      const loaded = data?.note
      noteContent = loaded?.content || ''
      await tick()
      autoResizeTextarea(noteTextareaRef)
    } catch {
      noteContent = ''
    }
  }

  async function saveNote() {
    if (!currentNoteId) return
    const now = new Date().toISOString()
    const existingIndex = notes.findIndex((item) => item.id === currentNoteId)
    const existingTitle = existingIndex !== -1 ? notes[existingIndex].title : ''
    const nextNote = {
      id: currentNoteId,
      title: existingTitle,
      createdAt: existingIndex !== -1 ? notes[existingIndex].createdAt : now,
      updatedAt: now,
    }
    notes =
      existingIndex === -1
        ? [nextNote, ...notes]
        : notes.map((item) => (item.id === currentNoteId ? nextNote : item))
    await apiSaveNotes(currentProject, { list: notes, currentId: currentNoteId })
    await apiSaveNote(currentProject, { ...nextNote, content: noteContent })
  }

  function autoResizeTextarea(node) {
    if (!node) return
    node.style.height = 'auto'
    node.style.height = `${node.scrollHeight}px`
  }

  async function deleteNote(note) {
    if (!note?.id) return
    const confirmed = typeof window === 'undefined' || window.confirm('Удалить заметку?')
    if (!confirmed) return
    await apiDeleteNote(currentProject, note.id)
    notes = notes.filter((item) => item.id !== note.id)
    if (currentNoteId === note.id) {
      currentNoteId = ''
      noteContent = ''
    }
    await apiSaveNotes(currentProject, { list: notes, currentId: currentNoteId })
  }

  function getFilteredNotes() {
    const query = noteSearch.trim().toLowerCase()
    if (!query) return notes
    return notes.filter((item) => item.title.toLowerCase().includes(query))
  }

  async function startEditLetterName() {
    if (!currentLetterId) return
    letterNameDraft = currentLetterTitle || ''
    isEditingLetterName = true
    await tick()
    letterNameInput?.focus()
    letterNameInput?.select()
  }

  async function startEditLetterDate() {
    if (!currentLetterId) return
    letterDateDraft = currentLetterDate || ''
    isEditingLetterDate = true
    await tick()
    letterDateInput?.focus()
    if (letterDateInput?.showPicker) letterDateInput.showPicker()
  }

  function cancelEditLetterName() {
    isEditingLetterName = false
    letterNameDraft = ''
  }

  function cancelEditLetterDate() {
    isEditingLetterDate = false
    letterDateDraft = ''
  }

  async function persistCurrentLetter() {
    if (!currentLetterId) return
    const now = new Date().toISOString()
    const existingIndex = letters.findIndex((letter) => letter.id === currentLetterId)
    const nextLetter = {
      id: currentLetterId,
      name: buildLetterName(currentLetterTitle, currentLetterDate),
      title: currentLetterTitle,
      date: currentLetterDate,
      createdAt: existingIndex !== -1 ? letters[existingIndex].createdAt : now,
      updatedAt: now,
    }
    letters =
      existingIndex === -1
        ? [nextLetter, ...letters]
        : letters.map((letter) => (letter.id === currentLetterId ? nextLetter : letter))
    currentLetterName = nextLetter.name
    await apiSaveLetters(currentProject, { list: letters, currentId: currentLetterId })
    await apiSaveLetter(currentProject, { ...nextLetter, blocks: buildDraftPayload() })
    apiAppendLetterHistory(currentProject, currentLetterId, {
      savedAt: new Date().toISOString(),
      blocks: buildDraftPayload(),
    }).catch(() => {})
  }

  async function commitEditLetterName() {
    if (!currentLetterId) return
    const nextTitle = letterNameDraft.trim()
    if (!nextTitle) {
      cancelEditLetterName()
      return
    }
    currentLetterTitle = nextTitle
    isEditingLetterName = false
    await persistCurrentLetter()
  }

  async function commitEditLetterDate() {
    if (!currentLetterId) return
    const nextDate = letterDateDraft
    if (!nextDate) {
      cancelEditLetterDate()
      return
    }
    currentLetterDate = nextDate
    isEditingLetterDate = false
    await persistCurrentLetter()
  }

  function openLetterModal(mode = 'new') {
    letterModalMode = mode
    newLetterTitle = mode === 'update' ? currentLetterTitle || '' : ''
    newLetterDate = mode === 'update' ? currentLetterDate || '' : new Date().toISOString().slice(0, 10)
    newLetterError = ''
    showLetterModal = true
  }

  async function saveLetterFromModal() {
    const title = newLetterTitle.trim()
    const date = newLetterDate
    if (!title || !date) {
      newLetterError = 'Укажите название и дату отправки.'
      return
    }
    const now = new Date().toISOString()
    if (letterModalMode === 'update' && currentLetterId) {
      const name = buildLetterName(title, date)
      const existingIndex = letters.findIndex((letter) => letter.id === currentLetterId)
      const nextLetter = {
        id: currentLetterId,
        name,
        title,
        date,
        createdAt: existingIndex !== -1 ? letters[existingIndex].createdAt : now,
        updatedAt: now,
      }
      letters =
        existingIndex === -1
          ? [nextLetter, ...letters]
          : letters.map((letter) => (letter.id === currentLetterId ? nextLetter : letter))
      currentLetterName = name
      currentLetterTitle = title
      currentLetterDate = date
      showLetterModal = false
      setActivePage('builder')
      await apiSaveLetters(currentProject, { list: letters, currentId: currentLetterId })
      await apiSaveLetter(currentProject, { ...nextLetter, blocks: buildDraftPayload() })
      return
    }
    const id = createId()
    const name = buildLetterName(title, date)
    const nextLetter = {
      id,
      name,
      title,
      date,
      createdAt: now,
      updatedAt: now,
    }
    letters = [nextLetter, ...letters]
    currentLetterId = id
    currentLetterName = name
    currentLetterTitle = title
    currentLetterDate = date
    assembledBlocks = []
    rebuildOutput()
    showLetterModal = false
    setActivePage('builder')
    await apiSaveLetters(currentProject, { list: letters, currentId: currentLetterId })
    await apiSaveLetter(currentProject, { ...nextLetter, blocks: [] })
  }

  function requireLetter() {
    if (currentLetterId) return true
    openLetterModal('new')
    return false
  }

  function addBlock(block) {
    if (!requireLetter()) return
    pushHistory()
    let template = getBlockTemplate(block.name) || block.content
    if (isListBlock(block.name)) {
      template = trimListToFirstItem(template)
    }
    if (isProductBlock(template)) {
      template = applyDefaultProductOptions(template)
    }
    const newId = createId()
    assembledBlocks = [
      ...assembledBlocks,
      {
        id: newId,
        name: block.name,
        content: template,
        collapsed: false,
        schema: buildBaseSchema(template, block.name, settings?.mixinRules),
        addSpacing: true,
        spacing: undefined,
        segment: 'common',
      },
    ]
    rebuildOutput()
    saveDraft()
    pendingFocusId = newId
    focusNewBlock()
  }

  function addSelectedBlock() {
    if (selectedBlockIndex === '' || selectedBlockIndex == null) return
    const blockIndex = Number(selectedBlockIndex)
    if (Number.isNaN(blockIndex)) return
    const block = allBlocks[blockIndex]
    if (!block) return
    addBlock(block)
    selectedBlockIndex = ''
  }


  function selectBlockFromDropdown(index) {
    addBlock(allBlocks[index])
    blockDropdownOpen = false
    blockDropdownSearch = ''
  }

  function getBlockTypeTag(block) {
    const n = (block.name || '').toLowerCase()
    const c = (block.content || '').toLowerCase()
    if (n.includes('product') || n.includes('товар') || n.includes('tovar') || c.includes('mixin-ids')) return 'Товары'
    if (n.includes('banner') || n.includes('баннер')) return 'Баннер'
    if (n.includes('button') || n.includes('кнопк') || n.includes('btn') || n.includes('cta')) return 'Кнопка'
    if (n.includes('header') || n.includes('заголов') || n.includes('hero')) return 'Заголовок'
    if (n.includes('list') || n.includes('спис') || c.includes('each ')) return 'Список'
    if (n.includes('divid') || n.includes('раздел') || n.includes('spacer') || n.includes('line')) return 'Разделитель'
    if (n.includes('footer') || n.includes('подвал')) return 'Подвал'
    if (c.includes('img(') || c.includes('image')) return 'Картинка'
    if (c.includes('<a') || c.includes('a(href')) return 'Текст'
    return 'Блок'
  }

  function getBlockPreviewLines(block) {
    if (!block.content) return []
    return block.content.split('\n').filter(l => l.trim()).slice(0, 3).map(l => l.trim().length > 50 ? l.trim().slice(0, 50) + '…' : l.trim())
  }

  function closeBlockDropdown(event) {
    if (blockDropdownRef && !blockDropdownRef.contains(event.target)) blockDropdownOpen = false
  }

  function findBlockIndexByName(name) {
    return allBlocks.findIndex((block) => block.name === name)
  }

  const defaultQuickBlocks = [
    'Заголовок зеленый',
    'Текст',
    'Банер',
    '3 товара в ряд',
    'Кнопка',
    'Две кнопки',
    'Разделитель',
  ]

  let quickBlockCandidate = ''
  let quickBlocksSelectRef
  let quickBlocksStatus = ''
  let quickBlocksError = ''
  let quickBlocksView = []
  $: quickBlockColors = settings?.quickBlockColors || {}

  const QB_COLORS = [
    { value: null,      label: 'Белый',   text: null },
    { value: '#130F33', label: 'Синий',   text: '#ffffff' },
    { value: '#AAC8C8', label: 'Зелёный', text: '#130F33' },
  ]

  function qbButtonStyle(name) {
    const bg = quickBlockColors[name]
    if (!bg) return ''
    const c = QB_COLORS.find(c => c.value === bg)
    const text = c?.text || '#ffffff'
    return `background:${bg};border-color:${bg};color:${text}`
  }

  async function setQuickBlockColor(name, color) {
    const next = { ...quickBlockColors, [name]: color || undefined }
    if (!color) delete next[name]
    settings = { ...settings, quickBlockColors: next }
    await apiSaveSettings(currentProject, settings)
  }
  let qbDragIndex = null
  let qbDragOverIndex = null

  function qbDragStart(i) { qbDragIndex = i }
  function qbDragOver(i) { if (i !== qbDragIndex) qbDragOverIndex = i }
  function qbDragEnd() {
    if (qbDragIndex !== null && qbDragOverIndex !== null && qbDragIndex !== qbDragOverIndex) {
      const arr = quickBlocksView.slice()
      const [moved] = arr.splice(qbDragIndex, 1)
      arr.splice(qbDragOverIndex, 0, moved)
      updateQuickBlocks(arr)
    }
    qbDragIndex = null
    qbDragOverIndex = null
  }

  // ── Сертификат ──────────────────────────────────────────────
  $: certBlocks = settings?.certificate?.blocks || []
  let certDragIndex = null
  let certDragOverIndex = null
  let certExpandedId = null
  let certAddBlockName = ''

  async function saveCertificate(blocks) {
    const next = { ...settings, certificate: { ...(settings.certificate || {}), blocks } }
    settings = next
    await apiSaveSettings(currentProject, settings)
    rebuildOutput()
    if (outputPanelMode === 'preview' || outputPanelMode === 'html') renderEmailPreview()
  }

  function addCertBlock(name) {
    const tpl = allBlocks.find(b => b.name === name)
    if (!tpl) return
    const block = { id: createId(), name: tpl.name, content: tpl.content, schema: buildBaseSchema(tpl.content, tpl.name, settings?.mixinRules) }
    saveCertificate([...certBlocks, block])
  }

  function removeCertBlock(id) {
    saveCertificate(certBlocks.filter(b => b.id !== id))
  }

  function updateCertBlockContent(id, newContent) {
    saveCertificate(certBlocks.map(b => b.id === id
      ? { ...b, content: newContent, schema: buildBaseSchema(newContent, b.name, settings?.mixinRules) }
      : b
    ))
  }

  function updateCertField(block, field, newValue) {
    if (field.type === 'raw') { updateCertBlockContent(block.id, newValue); return }
    if (newValue.includes('\n')) newValue = (field.type === 'text' || field.type === 'list-item') ? newValue.replace(/\r?\n/g, '<br>') : newValue.replace(/\r?\n/g, ' ')
    const lines = normalizeNewlines(block.content).split('\n')
    const line = lines[field.lineIndex] ?? ''
    if (field.objectKey) {
      const range = findMixinArgRange(line, field.argIndex)
      if (!range) return
      lines[field.lineIndex] = line.slice(0, range.start) + replaceObjectPropInArg(line.slice(range.start, range.end), field.objectKey, newValue) + line.slice(range.end)
    } else if (field.argIndex !== undefined) {
      const range = findMixinArgRange(line, field.argIndex)
      if (!range) return
      lines[field.lineIndex] = line.slice(0, range.start) + JSON.stringify(newValue) + line.slice(range.end)
    } else {
      lines[field.lineIndex] = (line.match(/^(\s*\S+\s+)/) || ['', ''])[1] + newValue
    }
    updateCertBlockContent(block.id, lines.join('\n'))
  }

  function certWrapBold(block, field) {
    const current = getFieldValue(block, field)
    updateCertField(block, field, `${current}<span style="font-weight: 700;"></span>`)
  }
  function certInsertBr(block, field) {
    updateCertField(block, field, `${getFieldValue(block, field)}<br>`)
  }
  function certInsertBullet(block, field) {
    updateCertField(block, field, `${getFieldValue(block, field)}<br>&bull;`)
  }
  async function certApplyTypograf(block, field) {
    if (field.type !== 'text' && field.type !== 'list-item') return
    const key = fieldKey(block, field)
    typografStatus = { ...typografStatus, [key]: { loading: true, error: '' } }
    try {
      const soap = `<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ProcessText xmlns="http://typograf.artlebedev.ru/webservices/"><text>${wrapCdata(getFieldValue(block, field))}</text></ProcessText></soap:Body></soap:Envelope>`
      const res = await fetch('/typograf/webservices/typograf.asmx', { method: 'POST', headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: 'http://typograf.artlebedev.ru/webservices/ProcessText' }, body: soap })
      if (!res.ok) throw new Error('Сервис не ответил')
      const xml = await res.text()
      const resultNode = new DOMParser().parseFromString(xml, 'text/xml').getElementsByTagName('ProcessTextResult')[0]
      if (!resultNode) throw new Error('Некорректный ответ')
      updateCertField(block, field, sanitizeTypograf(resultNode.textContent ?? ''))
      typografStatus = { ...typografStatus, [key]: { loading: false, error: '' } }
    } catch (err) {
      typografStatus = { ...typografStatus, [key]: { loading: false, error: err.message || 'Ошибка' } }
    }
  }

  function certDragStart(i) { certDragIndex = i }
  function certDragOver(i) { if (i !== certDragIndex) certDragOverIndex = i }
  function certDragEnd() {
    if (certDragIndex !== null && certDragOverIndex !== null && certDragIndex !== certDragOverIndex) {
      const arr = certBlocks.slice()
      const [moved] = arr.splice(certDragIndex, 1)
      arr.splice(certDragOverIndex, 0, moved)
      saveCertificate(arr)
    }
    certDragIndex = null
    certDragOverIndex = null
  }

  function toggleCertificate(val) {
    certificateEnabled = val
    saveDraft()
    rebuildOutput()
    if (outputPanelMode === 'preview' || outputPanelMode === 'html') renderEmailPreview()
  }

  function openCertEditor() {
    certEditorSavedState = {
      assembledBlocks: JSON.parse(JSON.stringify(assembledBlocks)),
      outputPug,
      currentLetterId,
      currentLetterName,
      certEditingMode: false
    }
    assembledBlocks = JSON.parse(JSON.stringify(certBlocks.length > 0 ? certBlocks : []))
    certEditingMode = true
    rebuildOutput()
    setActivePage('builder')
  }

  function cancelCertEditor() {
    if (certEditorSavedState) {
      assembledBlocks = certEditorSavedState.assembledBlocks
      outputPug = certEditorSavedState.outputPug
      certEditingMode = false
      certEditorSavedState = null
      rebuildOutput()
    }
    setActivePage('settings')
  }

  async function saveCertEditor() {
    const blocks = assembledBlocks.map(b => ({
      id: b.id,
      name: b.name,
      content: b.content,
      schema: b.schema,
      addSpacing: b.addSpacing ?? true,
      spacing: b.spacing
    }))
    await saveCertificate(blocks)
    if (certEditorSavedState) {
      assembledBlocks = certEditorSavedState.assembledBlocks
      outputPug = certEditorSavedState.outputPug
      certEditingMode = false
      certEditorSavedState = null
      rebuildOutput()
    }
    setActivePage('settings')
    settingsTab = 'certificate'
  }
  // ────────────────────────────────────────────────────────────

  function normalizeQuickBlocks(list) {
    const allowed = new Set(allBlocks.map((block) => block.name))
    return (list || []).filter((name) => allowed.has(name))
  }

  function ensureQuickBlocks(nextSettings) {
    const hasCustom = Array.isArray(nextSettings.quickBlocks)
    if (!hasCustom) {
      return { ...nextSettings, quickBlocks: defaultQuickBlocks.slice() }
    }
    return nextSettings
  }

  function getQuickBlocks(source = settings) {
    if (!source) return []
    if (Array.isArray(source.quickBlocks)) return source.quickBlocks
    return normalizeQuickBlocks(defaultQuickBlocks)
  }

  function getQuickBlockLabel(name) {
    if (name === 'Заголовок зеленый') return 'h2'
    if (name === 'Две кнопки') return '2 кнопки'
    if (name === 'Разделитель') return 'DIV'
    return name
  }

  const QB_ICONS = {
    'Заголовок зеленый': `<span style="font-size:13px;font-weight:800;letter-spacing:-1px;line-height:1">АА</span>`,
    'Текст': `<span style="font-size:11px;font-weight:400;letter-spacing:-0.5px;line-height:1">аа</span>`,
    'Банер': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="16" height="14" rx="1.5"/><circle cx="6.5" cy="8" r="1.5"/><polyline points="2,15 6.5,10 10.5,14 13.5,11 18,15"/></svg>`,
    'Два баннера без текста': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1" stroke-linejoin="round"><rect x="1" y="3" width="8" height="14" rx="1"/><rect x="11" y="3" width="8" height="14" rx="1"/></svg>`,
    'Два баннера с текстом под каждым': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor" stroke="currentColor" stroke-width="1"><rect x="1" y="4" width="8" height="9" rx="1" fill="none"/><rect x="11" y="4" width="8" height="9" rx="1" fill="none"/><circle cx="5" cy="17" r="1.1"/><circle cx="15" cy="17" r="1.1"/></svg>`,
    '3 товара в ряд': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><circle cx="3.5" cy="10" r="2.5"/><circle cx="10" cy="10" r="2.5"/><circle cx="16.5" cy="10" r="2.5"/></svg>`,
    '3 товара с фото': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><rect x="1" y="3" width="7" height="14" rx="1" fill="none" stroke="currentColor" stroke-width="1"/><circle cx="14" cy="4" r="2"/><circle cx="14" cy="10" r="2"/><circle cx="14" cy="16" r="2"/></svg>`,
    '3 товара с фото справа': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><circle cx="6" cy="4" r="2"/><circle cx="6" cy="10" r="2"/><circle cx="6" cy="16" r="2"/><rect x="12" y="3" width="7" height="14" rx="1" fill="none" stroke="currentColor" stroke-width="1"/></svg>`,
    '4 товара в ряд': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><circle cx="2.5" cy="10" r="2"/><circle cx="7.5" cy="10" r="2"/><circle cx="12.5" cy="10" r="2"/><circle cx="17.5" cy="10" r="2"/></svg>`,
    'Кнопка': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1"><rect x="1" y="7" width="18" height="6" rx="1.5"/></svg>`,
    'Две кнопки': `<span style="display:flex;gap:2px;align-items:center"><span style="display:block;width:11px;height:7px;border:1px solid currentColor;border-radius:1px"></span><span style="display:block;width:11px;height:7px;border:1px solid currentColor;border-radius:1px"></span></span>`,
    'Разделитель': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><line x1="2" y1="10" x2="18" y2="10"/></svg>`,
    'Промокоды': `<span style="font-size:9px;font-weight:700;line-height:1;text-transform:uppercase;letter-spacing:0.5px">код</span>`,
  }

  function getQuickBlockIcon(name) {
    if (QB_ICONS[name]) return QB_ICONS[name]
    const n = name.toLowerCase()
    if (n.includes('заголов')) return QB_ICONS['Заголовок зеленый']
    if (n.includes('текст')) return QB_ICONS['Текст']
    if (n.includes('бан') && n.includes('текст')) return QB_ICONS['Два баннера с текстом под каждым']
    if ((n.includes('два') || n.includes('2')) && n.includes('бан')) return QB_ICONS['Два баннера без текста']
    if (n.includes('бан')) return QB_ICONS['Банер']
    if (n.includes('4 товар') || n.includes('четыр')) return QB_ICONS['4 товара в ряд']
    if ((n.includes('3 товар') || n.includes('три товар')) && (n.includes('фото') || n.includes('изображ')) && n.includes('справ')) return QB_ICONS['3 товара с фото справа']
    if ((n.includes('3 товар') || n.includes('три товар')) && (n.includes('фото') || n.includes('изображ') || n.includes('слева'))) return QB_ICONS['3 товара с фото']
    if (n.includes('3 товар') || n.includes('три товар')) return QB_ICONS['3 товара в ряд']
    if (n.includes('две кноп') || n.includes('2 кноп') || (n.includes('кноп') && /2|две|two/.test(n))) return QB_ICONS['Две кнопки']
    if (n.includes('кноп')) return QB_ICONS['Кнопка']
    if (n.includes('раздел')) return QB_ICONS['Разделитель']
    if (n.includes('промо') || n.includes('код')) return QB_ICONS['Промокоды']
    return `<span style="font-size:10px;font-weight:700;line-height:1">${name.slice(0, 3)}</span>`
  }

  function addQuickBlockByName(name) {
    const index = findBlockIndexByName(name)
    if (index === -1) return
    addBlock(allBlocks[index])
  }

  async function updateQuickBlocks(next) {
    settings = { ...settings, quickBlocks: next }
    quickBlocksView = next
    quickBlocksError = ''
    try {
      await apiSaveSettings(currentProject, settings)
      const fresh = await apiGetProject(currentProject)
      const normalizedSettings = ensureQuickBlocks(fresh.settings || { globalSpacing: 40, blocks: {} })
      settings = normalizedSettings
      quickBlocksView = normalizedSettings.quickBlocks || []
      quickBlocksStatus = 'Сохранено'
    } catch (err) {
      quickBlocksError = 'Ошибка сохранения'
    }
  }

  async function addQuickBlockFromSettings(nameInput) {
    const name = (nameInput || quickBlockCandidate || quickBlocksSelectRef?.value || '').trim()
    if (!name) {
      quickBlocksError = 'Выбери блок'
      return
    }
    const current = Array.isArray(settings.quickBlocks) ? settings.quickBlocks : normalizeQuickBlocks(defaultQuickBlocks)
    if (current.includes(name)) {
      quickBlockCandidate = ''
      quickBlocksError = 'Уже есть в списке'
      return
    }
    await updateQuickBlocks([...current, name])
    quickBlockCandidate = ''
  }

  async function removeQuickBlockFromSettings(name) {
    const current = Array.isArray(settings.quickBlocks) ? settings.quickBlocks : normalizeQuickBlocks(defaultQuickBlocks)
    await updateQuickBlocks(current.filter((item) => item !== name))
  }

  function addBlockAfter(index, blockIndex) {
    if (!requireLetter()) return
    const block = allBlocks[blockIndex]
    if (!block) return
    pushHistory()
    let template = getBlockTemplate(block.name) || block.content
    if (isListBlock(block.name)) {
      template = trimListToFirstItem(template)
    }
    if (isProductBlock(template)) {
      template = applyDefaultProductOptions(template)
    }
    const newId = createId()
    const next = {
      id: newId,
      name: block.name,
      content: template,
      collapsed: false,
      schema: buildBaseSchema(template, block.name, settings?.mixinRules),
      addSpacing: true,
      spacing: undefined,
    }
    assembledBlocks = [
      ...assembledBlocks.slice(0, index + 1),
      next,
      ...assembledBlocks.slice(index + 1),
    ]
    rebuildOutput()
    saveDraft()
    pendingFocusId = newId
    focusNewBlock()
  }

  async function focusNewBlock() {
    const id = pendingFocusId
    if (!id) return
    await tick()
    const node = document.querySelector(`[data-block-id="${id}"]`)
    if (node) {
      node.scrollIntoView({ behavior: 'smooth', block: 'center' })
      node.classList.add('flash')
      setTimeout(() => node.classList.remove('flash'), 500)
      setActiveBlock(id, true)
    }
    pendingFocusId = ''
  }

  function toggleAddMenu(id) {
    const next = addMenuFor === id ? null : id
    addMenuFor = next
    if (next) {
      tick().then(() => {
        const node = document.querySelector(`select[data-add-menu="${id}"]`)
        if (node?.showPicker) node.showPicker()
        else node?.focus()
      })
    }
  }

  function autosize(node) {
    const resize = () => {
      node.style.height = 'auto'
      node.style.height = `${node.scrollHeight}px`
    }
    resize()
    node.addEventListener('input', resize)
    return {
      destroy() {
        node.removeEventListener('input', resize)
      },
    }
  }


  function handleAddAfter(index, event) {
    const blockIndex = Number(event.target.value)
    if (Number.isNaN(blockIndex)) return
    addBlockAfter(index, blockIndex)
    addMenuFor = null
  }

  function togglePug(blockId) {
    pugOpen = { ...pugOpen, [blockId]: !pugOpen[blockId] }
  }

  function removeBlock(id) {
    pushHistory()
    assembledBlocks = assembledBlocks.filter((block) => block.id !== id)
    rebuildOutput()
    saveDraft()
  }

  // --- Gender segments ---
  let currentGenderVersion = 'female'
  let segmentFlipped = false
  let genderHeaderFemale = '', genderHeaderMale = '', genderFooterFemale = '', genderFooterMale = ''
  let partsFiles = []
  async function loadPartsFiles() {
    try { const d = await apiRequest('/parts-files'); partsFiles = d.files || [] } catch {}
  }

  // Parts file editor
  let partsEditorFile = ''
  let partsEditorRawContent = ''
  let partsEditorFields = [] // [{label, type:'link'|'button', text, url, _raw}]
  let partsEditorLoading = false
  let partsEditorSaving = false
  let partsEditorError = ''
  let partsEditorSuccess = ''

  function parsePugFields(content) {
    const fields = []
    const lines = content.split('\n')
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      // +buttonRounded("TEXT", "URL", ...)
      const btnMatch = line.match(/\+buttonRounded\("([^"]*)",\s*"([^"]*)"/)
      if (btnMatch) {
        fields.push({ type: 'button', label: 'Кнопка', text: btnMatch[1], url: btnMatch[2], _lineIndex: i })
        continue
      }
      // a(href="URL"...) TEXT — pug menu items
      const linkMatch = line.match(/a\(href="([^"]+)"[^)]*\)([^\s]*)\s+([^+\s].+)$/)
      if (linkMatch) {
        const text = linkMatch[3].trim()
        if (text && !text.includes('img(') && !text.includes('+')) {
          fields.push({ type: 'link', label: 'Ссылка', text, url: linkMatch[1], _classSuffix: linkMatch[2], _lineIndex: i })
          continue
        }
      }
      // HTML links: global search for all <a href="URL">TEXT</a> with plain text (no img)
      if (!line.includes('<img')) {
        const htmlLinkRe = /<a\s[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g
        let hlm
        while ((hlm = htmlLinkRe.exec(line)) !== null) {
          if (!hlm[2].trim() || hlm[1].startsWith('${')) continue
          const isTel = hlm[1].startsWith('tel:')
          fields.push({ type: 'html-link', label: isTel ? 'Телефон' : 'Ссылка', text: hlm[2].trim(), url: hlm[1], _lineIndex: i, _origMatch: hlm[0] })
        }
        if (fields.length > 0 && fields[fields.length - 1]._lineIndex === i) continue
      }
      // HTML span with text (may contain nested <span> for bold etc): <span style="...">TEXT</span></td>
      const spanMatch = line.match(/<span\s+style="[^"]*">([\s\S]{2,200})<\/span><\/td>/)
      if (spanMatch && spanMatch[1].replace(/<[^>]*>/g, '').trim() && !line.includes('</a>')) {
        fields.push({ type: 'html-text', label: 'Текст', text: spanMatch[1].trim(), _lineIndex: i })
      }
    }
    return fields
  }

  function applyFieldsToContent(content, fields) {
    const lines = content.split('\n')
    for (const field of fields) {
      const i = field._lineIndex
      if (field.type === 'button') {
        lines[i] = lines[i].replace(
          /\+buttonRounded\("[^"]*",\s*"[^"]*"/,
          `+buttonRounded("${field.text}", "${field.url}"`
        )
      } else if (field.type === 'link') {
        lines[i] = lines[i].replace(
          /(a\(href=)"[^"]*"([^)]*\))([^\s]*)\s+([^+\s].+)$/,
          `$1"${field.url}"$2${field._classSuffix} ${field.text}`
        )
      } else if (field.type === 'html-link') {
        const newMatch = field._origMatch
          .replace(/href="[^"]+"/, `href="${field.url}"`)
          .replace(/>([^<]+)<\/a>$/, `>${field.text}</a>`)
        lines[i] = lines[i].replace(field._origMatch, newMatch)
      } else if (field.type === 'html-text') {
        lines[i] = lines[i].replace(
          /(<span\s+style="[^"]*">)([\s\S]{2,200})(<\/span><\/td>)/,
          `$1${field.text}$3`
        )
      }
    }
    return lines.join('\n')
  }

  async function loadPartsEditorFile(filePath) {
    if (!filePath) { partsEditorFields = []; return }
    partsEditorLoading = true
    partsEditorError = ''
    try {
      const { content } = await apiReadPartsFile(filePath)
      partsEditorRawContent = content
      partsEditorFields = parsePugFields(content)
    } catch (e) {
      partsEditorError = 'Ошибка загрузки файла'
    } finally {
      partsEditorLoading = false
    }
  }

  async function savePartsEditorFile() {
    if (!partsEditorFile) return
    partsEditorSaving = true
    partsEditorError = ''
    partsEditorSuccess = ''
    try {
      const updated = applyFieldsToContent(partsEditorRawContent, partsEditorFields)
      await apiWritePartsFile(partsEditorFile, updated)
      partsEditorSuccess = 'Сохранено'
      setTimeout(() => partsEditorSuccess = '', 3000)
    } catch (e) {
      partsEditorError = 'Ошибка сохранения'
    } finally {
      partsEditorSaving = false
    }
  }

  function setBlockAxis(blockId) {
    const isAxis = assembledBlocks.find(b => b.id === blockId)?.swapCenter
    assembledBlocks = assembledBlocks.map(b => ({ ...b, swapCenter: b.id === blockId ? !isAxis : false }))
    rebuildOutput()
    saveDraft()
  }

  function setBlockSegment(blockId, segment) {
    pushHistory()
    assembledBlocks = assembledBlocks.map(b => b.id === blockId ? { ...b, segment } : b)
    rebuildOutput()
    saveDraft()
  }

  function getSegmentLabel(seg, femaleIdx, maleIdx) {
    if (seg === 'female') return `Ж${femaleIdx || ''}`
    if (seg === 'male') return `М${maleIdx || ''}`
    return 'О'
  }

  function buildVersion(list, target) {
    const dividerIndex = list.findIndex(b => b.swapCenter)
    if (dividerIndex !== -1) {
      const before = list.slice(0, dividerIndex)
      const divider = list[dividerIndex]
      const after = list.slice(dividerIndex + 1)
      const beforeCommons = [], beforeSeg = []
      before.forEach(b => { (b.segment === 'common' || !b.segment) ? beforeCommons.push(b) : beforeSeg.push(b) })
      const afterCommons = [], afterSeg = []
      after.forEach(b => { (b.segment === 'common' || !b.segment) ? afterCommons.push(b) : afterSeg.push(b) })
      if (target === 'male') {
        return [...beforeCommons, ...afterSeg, divider, ...afterCommons, ...beforeSeg]
      }
      return [...beforeCommons, ...beforeSeg, divider, ...afterCommons, ...afterSeg]
    }

    const commonsPrefix = []
    const commonsMid = []
    const commonsSuffix = []
    const segmentA = []
    const segmentB = []
    let firstSegType = null
    let secondSegType = null

    for (const b of list) {
      const seg = b.segment || 'common'
      if (seg === 'common') {
        if (!firstSegType) commonsPrefix.push(b)
        else if (!secondSegType) commonsMid.push(b)
        else commonsSuffix.push(b)
      } else {
        if (!firstSegType) {
          firstSegType = seg
          segmentA.push(b)
        } else if (seg === firstSegType && !secondSegType) {
          segmentA.push(b)
        } else {
          if (!secondSegType) secondSegType = seg
          segmentB.push(b)
        }
      }
    }

    if (!secondSegType) {
      return [...commonsPrefix, ...segmentA, ...commonsMid, ...segmentB, ...commonsSuffix]
    }

    const firstIsTarget = target === firstSegType
    return firstIsTarget
      ? [...commonsPrefix, ...segmentA, ...commonsMid, ...segmentB, ...commonsSuffix]
      : [...commonsPrefix, ...segmentB, ...commonsMid, ...segmentA, ...commonsSuffix]
  }

  function assembleGenderVersion(target) {
    currentGenderVersion = target
    renderEmailPreview()
  }

  function flipSegmentOrder() {
    pushHistory()
    const nextTarget = segmentFlipped ? 'female' : 'male'
    assembledBlocks = buildVersion(assembledBlocks, nextTarget)
    segmentFlipped = !segmentFlipped
    currentGenderVersion = nextTarget
    rebuildOutput()
    saveDraft()
    if (outputPanelMode === 'preview' || outputPanelMode === 'html') {
      renderEmailPreview()
    }
  }

  // --- ID Pool ---
  function parseIdPool(text) {
    return text.split(/[\n\r,\s]+/).map(s => s.trim()).filter(Boolean)
  }

  function applyIdPool() {
    const ids = parseIdPool(idPoolInput)
    idPoolQueue = ids.map(value => ({ value, usedBy: null }))
  }

  $: if (idPoolOpen) { applyIdPool(); void idPoolInput }

  let idPoolRefreshing = false
  async function refreshIdsFromYonote() {
    if (!assemblyInfo) return
    idPoolRefreshing = true
    try {
      const row = planRows.find(r => r.project === currentProject && r.subject === assemblyInfo.subject && r.dateStr === assemblyInfo.dateStr)
      if (!row) {
        // Нет в кеше плана — загрузим напрямую из Yonote
        const projData = await apiGetProject(currentProject)
        const yc = projData.settings?.yonoteConfig
        if (!yc?.databaseId) { idPoolRefreshing = false; return }
        const propsData = await apiYonoteGetProperties(yc.databaseId)
        const propsMap = new Map((propsData.properties || []).map(p => [p.id, p]))
        const data = await apiYonoteGetRows(yc.databaseId)
        for (const r of (data.rows || [])) {
          const subject = extractYonoteValue(r.values?.[yc.properties?.subject], propsMap.get(yc.properties?.subject))
          if (subject === assemblyInfo.subject) {
            const allExtra = Object.entries(r.values || {}).map(([propId, val]) => {
              const prop = propsMap.get(propId)
              return { label: prop?.title || propId, value: extractYonoteValue(val, prop) }
            })
            const ids = extractIdsFromExtra(allExtra)
            if (ids) { idPoolInput = ids; applyIdPool() }
            break
          }
        }
      } else {
        // Берём из данных плана — но extra уже без ID, нужно заново из Yonote
        const projData = await apiGetProject(currentProject)
        const yc = projData.settings?.yonoteConfig
        if (yc?.databaseId && row.rowId) {
          const propsData = await apiYonoteGetProperties(yc.databaseId)
          const propsMap = new Map((propsData.properties || []).map(p => [p.id, p]))
          const data = await apiYonoteGetRows(yc.databaseId)
          const yonoteRow = (data.rows || []).find(r => r.id === row.rowId)
          if (yonoteRow) {
            const allExtra = Object.entries(yonoteRow.values || {}).map(([propId, val]) => {
              const prop = propsMap.get(propId)
              return { label: prop?.title || propId, value: extractYonoteValue(val, prop) }
            })
            const ids = extractIdsFromExtra(allExtra)
            if (ids) { idPoolInput = ids; applyIdPool() }
          }
        }
      }
    } catch {}
    idPoolRefreshing = false
  }

  function distributeIds() {
    if (!idPoolQueue.length) return
    pushHistory()
    let queueIndex = 0
    assembledBlocks = assembledBlocks.map(block => {
      const schema = ensureSchema(block)
      const idsField = schema.find(f => f.type === 'mixin-ids')
      if (!idsField) return block
      // Count how many IDs this block needs based on mixin name in content
      const mixinLine = block.content.split('\n').find(l => /^\s*\+products/.test(l)) || ''
      const mixinMatch = mixinLine.match(/\+(\w+)/)
      const mn = mixinMatch ? mixinMatch[1].toLowerCase() : ''
      let count = 4
      // 3-product mixins: products3Row, productsColumn*
      if (mn.includes('3row') || mn.includes('column')) count = 3
      // 4-product mixins: products4Row, productsImageLeft, productsImageRight (default)
      const taken = []
      while (taken.length < count && queueIndex < idPoolQueue.length) {
        idPoolQueue[queueIndex].usedBy = block.id
        taken.push(idPoolQueue[queueIndex].value)
        queueIndex++
      }
      if (!taken.length) return block
      const newContent = updateFieldInContent(block.content, idsField, taken.join(','))
      return {
        ...block,
        content: newContent,
        schema: buildBaseSchema(newContent, block.name, settings?.mixinRules),
      }
    })
    idPoolQueue = [...idPoolQueue]
    rebuildOutput()
    saveDraft()
  }

  function updateFieldInContent(content, field, newValue) {
    if (field.type === 'mixin-ids' && field.argIndex !== undefined) {
      const lines = normalizeNewlines(content).split('\n')
      // Find the target line: by mixin name if available, or by lineIndex
      const targetLine = field.mixin
        ? lines.findIndex(l => new RegExp(`\\+${field.mixin}\\s*\\(`).test(l))
        : (field.lineIndex !== undefined ? field.lineIndex : lines.findIndex(l => /^\s*\+products/.test(l)))
      if (targetLine >= 0 && targetLine < lines.length) {
        lines[targetLine] = replaceMixinArgQuoted(lines[targetLine], field.argIndex, newValue)
      }
      return lines.join('\n')
    }
    return content
  }

  // --- Image auto-numbering ---
  function applyAutoImageNumbers() {
    pushHistory()
    let counter = 1
    assembledBlocks = assembledBlocks.map(block => {
      const schema = ensureSchema(block)
      const imageFields = schema.filter(f => f.type === 'image' || f.label?.toLowerCase().includes('картинк') || f.label?.toLowerCase().includes('image'))
      if (!imageFields.length) return block
      let content = block.content
      for (const field of imageFields) {
        const val = getFieldValue(block, field)
        if (!val || val.trim() === '') {
          const url = imageBaseUrl.replace(/\/?$/, '/') + counter + imageExt
          content = updateFieldInContent(content, field, url)
        }
        counter++
      }
      if (content !== block.content) {
        return {
          ...block,
          content,
          schema: buildBaseSchema(content, block.name, settings?.mixinRules),
        }
      }
      return block
    })
    rebuildOutput()
    saveDraft()
  }

  function moveBlock(index, delta) {
    const nextIndex = index + delta
    if (nextIndex < 0 || nextIndex >= assembledBlocks.length) return
    pushHistory()
    const updated = [...assembledBlocks]
    const [item] = updated.splice(index, 1)
    updated.splice(nextIndex, 0, item)
    assembledBlocks = updated
    rebuildOutput()
    saveDraft()
  }

  let dragBlockIndex = -1
  let dragOverBlockIndex = -1

  function handleBlockDragStart(e, index) {
    dragBlockIndex = index
    e.dataTransfer.effectAllowed = 'move'
    e.dataTransfer.setData('text/plain', String(index))
  }

  function handleBlockDragOver(e, index) {
    e.preventDefault()
    e.dataTransfer.dropEffect = 'move'
    dragOverBlockIndex = index
  }

  function handleBlockDrop(e, index) {
    e.preventDefault()
    if (dragBlockIndex >= 0 && dragBlockIndex !== index) {
      pushHistory()
      const updated = [...assembledBlocks]
      const [item] = updated.splice(dragBlockIndex, 1)
      updated.splice(index, 0, item)
      assembledBlocks = updated
      rebuildOutput()
      saveDraft()
    }
    dragBlockIndex = -1
    dragOverBlockIndex = -1
  }

  function handleBlockDragEnd() {
    dragBlockIndex = -1
    dragOverBlockIndex = -1
  }

  function toggleBlock(id) {
    assembledBlocks = assembledBlocks.map((block) =>
      block.id === id ? { ...block, collapsed: !block.collapsed } : block
    )
    saveDraft()
  }

  function setAllCollapsed(value) {
    assembledBlocks = assembledBlocks.map((block) => ({ ...block, collapsed: value }))
    saveDraft()
  }

  function updateBlockContent(id, newContent) {
    pushHistory()
    assembledBlocks = assembledBlocks.map((block) =>
      block.id === id
        ? {
            ...block,
            content: newContent,
            schema: buildBaseSchema(newContent, block.name, settings?.mixinRules),
          }
        : block
    )
    rebuildOutput()
    saveDraft()
  }

  function moveSectionInBlock(blockId, sectionIdx, direction) {
    const block = assembledBlocks.find(b => b.id === blockId)
    if (!block) return
    const parsed = parseSections(block.content)
    if (!parsed) return
    // Apply move to ALL columns
    for (const col of parsed.columns) {
      const idxs = []
      col.sections.forEach((s, i) => { if (s.type === 'section') idxs.push(i) })
      const swapIdx = sectionIdx + direction
      if (sectionIdx < 0 || sectionIdx >= idxs.length || swapIdx < 0 || swapIdx >= idxs.length) continue
      const a = idxs[sectionIdx], b = idxs[swapIdx]
      const temp = col.sections[a]
      col.sections[a] = col.sections[b]
      col.sections[b] = temp
    }
    updateBlockContent(blockId, rebuildContentFromSections(parsed))
  }

  function removeSectionFromBlock(blockId, sectionIdx) {
    const block = assembledBlocks.find(b => b.id === blockId)
    if (!block) return
    const parsed = parseSections(block.content)
    if (!parsed) return
    // Apply remove to ALL columns
    for (const col of parsed.columns) {
      const idxs = []
      col.sections.forEach((s, i) => { if (s.type === 'section') idxs.push(i) })
      if (idxs.length <= 1 || sectionIdx >= idxs.length) continue
      const removePos = idxs[sectionIdx]
      col.sections.splice(removePos, 1)
      if (removePos > 0 && col.sections[removePos - 1]?.type === 'spacer') {
        col.sections.splice(removePos - 1, 1)
      } else if (col.sections[removePos]?.type === 'spacer') {
        col.sections.splice(removePos, 1)
      }
    }
    updateBlockContent(blockId, rebuildContentFromSections(parsed))
  }

  function buildSectionHeaders(fields, parsed) {
    // Returns Map<fieldIndex, { section, si, total }> — only for the first field of each section
    const headers = new Map()
    if (!parsed || parsed.columns.length === 0) return headers
    const contentSecs = parsed.columns[0].sections.filter(s => s.type === 'section')
    if (contentSecs.length < 2) return headers
    const total = contentSecs.length
    const assigned = new Set()
    for (let fi = 0; fi < fields.length; fi++) {
      const fLine = fields[fi].lineIndex
      if (fLine == null) continue
      for (let si = 0; si < contentSecs.length; si++) {
        if (assigned.has(si)) continue
        const sec = contentSecs[si]
        const end = sec.startLine + sec.lines.length - 1
        if (fLine >= sec.startLine && fLine <= end) {
          headers.set(fi, { section: sec, si, total })
          assigned.add(si)
          break
        }
      }
    }
    return headers
  }

  function updateBlockSpacing(id, value) {
    pushHistory()
    assembledBlocks = assembledBlocks.map((block) =>
      block.id === id ? { ...block, addSpacing: value } : block
    )
    rebuildOutput()
    saveDraft()
  }

  function addTextParagraph(block, value = '') {
    pushHistory()
    const currentBlock = assembledBlocks.find((item) => item.id === block.id) ?? block
    const content = appendTextParagraph(currentBlock.content, value)
    assembledBlocks = assembledBlocks.map((item) =>
      item.id === block.id
        ? {
            ...item,
            content,
            schema: buildBaseSchema(content, item.name, settings?.mixinRules),
          }
        : item
    )
    rebuildOutput()
    saveDraft()
  }

  function trimListToFirstItem(content) {
    const lines = normalizeNewlines(content).trimEnd().split('\n')
    const ranges = getListItemRanges(lines)
    if (!ranges.length) return content
    const first = ranges[0]
    const tail = lines.slice(ranges[ranges.length - 1].end + 1)
    const nextLines = [
      ...lines.slice(0, first.start),
      ...lines.slice(first.start, first.end + 1),
      ...tail,
    ]
    return nextLines.join('\n')
  }

  function addListItem(block) {
    pushHistory()
    const currentBlock = assembledBlocks.find((item) => item.id === block.id) ?? block
    const lines = normalizeNewlines(currentBlock.content).trimEnd().split('\n')
    const ranges = getListItemRanges(lines)
    if (!ranges.length) return
    const last = ranges[ranges.length - 1]
    const itemLines = lines.slice(last.start, last.end + 1).map((line) => {
      if (!line.includes('span.text')) return line
      const meta = splitTextField(line)
      if (!meta) return line
      return line.slice(0, meta.start) + '' + line.slice(meta.end)
    })
    const tail = lines.slice(last.end + 1)
    const updated = [...lines.slice(0, last.end + 1), ...itemLines, ...tail].join('\n')
    assembledBlocks = assembledBlocks.map((item) =>
      item.id === block.id
        ? { ...item, content: updated, schema: buildBaseSchema(updated, item.name, settings?.mixinRules) }
        : item
    )
    rebuildOutput()
    saveDraft()
  }

  function removeListItem(block, field) {
    pushHistory()
    const currentBlock = assembledBlocks.find((item) => item.id === block.id) ?? block
    const lines = normalizeNewlines(currentBlock.content).split('\n')
    const ranges = getListItemRanges(lines)
    const target = ranges[field.itemIndex]
    if (!target) return
    const updated = [...lines.slice(0, target.start), ...lines.slice(target.end + 1)].join('\n').trimEnd()
    assembledBlocks = assembledBlocks.map((item) =>
      item.id === block.id
        ? { ...item, content: updated, schema: buildBaseSchema(updated, item.name, settings?.mixinRules) }
        : item
    )
    rebuildOutput()
    saveDraft()
  }

  function updateBlockSpacingValue(id, value) {
    pushHistory()
    if (value === '') {
      assembledBlocks = assembledBlocks.map((block) =>
        block.id === id ? { ...block, spacing: undefined } : block
      )
      rebuildOutput()
      saveDraft()
      return
    }
    const spacing = Number(value)
    if (Number.isNaN(spacing)) return
    assembledBlocks = assembledBlocks.map((block) =>
      block.id === id ? { ...block, spacing } : block
    )
    rebuildOutput()
    saveDraft()
  }

  const MAX_HISTORY = 50
  function pushHistory() {
    history = [
      ...history.slice(-(MAX_HISTORY - 1)),
      {
        blocks: JSON.parse(JSON.stringify(assembledBlocks)),
        settings: JSON.parse(JSON.stringify(settings)),
      },
    ]
  }

  function undoLast() {
    if (!history.length) return
    const prev = history[history.length - 1]
    history = history.slice(0, -1)
    assembledBlocks = prev.blocks
    settings = prev.settings
    apiSaveSettings(currentProject, settings)
    rebuildOutput()
    saveDraft()
  }

  function rebuildOutput() {
    let text = ''
    const ranges = {}
    assembledBlocks.forEach((block, index) => {
      const blockText = `//${block.name}\n${block.content.trimEnd()}`
      if (text) text += '\n\n'
      const start = text.length
      text += blockText
      const end = text.length
      ranges[block.id] = { start, end }
      if (shouldAddSpacing(block, assembledBlocks[index + 1])) {
        text += `\n\n+spacerLine(${getSpacingForBlock(block)})`
      }
    })
    // Append certificate blocks at the bottom when enabled
    if (certificateEnabled && !certEditingMode && certBlocks.length > 0) {
      certBlocks.forEach((block) => {
        if (text) text += '\n\n'
        text += `//${block.name}\n${block.content.trimEnd()}`
      })
    }
    outputPug = text
    blockRanges = ranges
    if (activeBlockId) {
      scrollToBlock(activeBlockId)
    }
    scheduleAutoRender()
  }

  function scheduleAutoRender() {
    if (outputPanelMode !== 'preview' && outputPanelMode !== 'html') return
    clearTimeout(autoRenderTimer)
    autoRenderTimer = setTimeout(() => {
      if (!previewLoading && previewStale) renderEmailPreview()
    }, 1000)
  }

  function setActiveBlock(id, focusPug = false) {
    activeBlockId = id
    scrollToBlock(id, focusPug)
  }

  async function scrollToBlock(id, focusPug = false) {
    await tick()
    const range = blockRanges[id]
    if (!pugOutputRef || !range) return
    const lineStart = outputPug.slice(0, range.start).split('\n').length - 1
    const lineHeight = parseFloat(getComputedStyle(pugOutputRef).lineHeight) || 16
    pugOutputRef.scrollTop = Math.max(0, (lineStart - 3) * lineHeight)
    pugOutputRef.setSelectionRange(range.start, range.end)
    if (focusPug) pugOutputRef.focus()
  }

  function shouldAddSpacing(block, nextBlock) {
    if (!block.addSpacing) return false
    if (!nextBlock) return true
    if (block.name === 'Отступ 20') return false
    if (block.name === 'Отступ 40') return false
    if (getBlockSetting(block.name).spacingEnabled === false) return false
    return true
  }

  function getSpacingForBlock(blockOrName) {
    if (blockOrName && typeof blockOrName === 'object') {
      if (typeof blockOrName.spacing === 'number') return blockOrName.spacing
    }
    const blockName = typeof blockOrName === 'string' ? blockOrName : blockOrName?.name
    const blockSetting = getBlockSetting(blockName)
    if (typeof blockSetting.spacing === 'number') return blockSetting.spacing
    return settings.globalSpacing ?? 40
  }

  function saveSettings(next) {
    settings = next
    apiSaveSettings(currentProject, next)
  }

  function updateSettingsBlock(name, patch) {
    const current = settings.blocks?.[name] ?? {}
    saveSettings({
      ...settings,
      blocks: {
        ...settings.blocks,
        [name]: { ...current, ...patch },
      },
    })
  }

  function addProductOption() {
    const next = [...(settings.productOptions || []), { label: '', code: '' }]
    saveSettings({ ...settings, productOptions: next })
  }

  function updateProductOption(index, patch) {
    const next = [...(settings.productOptions || [])]
    next[index] = { ...next[index], ...patch }
    saveSettings({ ...settings, productOptions: next })
  }

  function removeProductOption(index) {
    const next = [...(settings.productOptions || [])]
    next.splice(index, 1)
    saveSettings({ ...settings, productOptions: next })
  }

  function buildDefaultMixinRulesFromBlocks(blocks) {
    const map = new Map()
    const pushRule = (rule) => {
      const key = `${rule.mixin}:${rule.argIndex}:${rule.type}`
      if (map.has(key)) return
      map.set(key, rule)
    }

    ;(blocks || []).forEach((block) => {
      const lines = normalizeNewlines(block.content || '').split('\n')
      lines.forEach((line) => {
        const trimmed = line.trim()
        if (!trimmed.startsWith('+')) return
        const mixin = getMixinName(trimmed)
        if (!mixin) return
        const argCount = getMixinArgCount(trimmed)
        if (!argCount) return
        DEFAULT_MIXIN_RULE_TEMPLATES.forEach((template) => {
          if (!template.test(mixin)) return
          template.fields.forEach((field) => {
            if (field.argIndex >= argCount) return
            pushRule({
              mixin,
              argIndex: field.argIndex,
              type: field.type,
              label: field.label,
            })
          })
        })
      })
    })

    return normalizeMixinRules(Array.from(map.values()))
  }

  function addMixinRule() {
    const next = [...(settings.mixinRules || []), { mixin: '', argIndex: 0, type: 'mixin-text', label: '' }]
    saveSettings({ ...settings, mixinRules: next, mixinRulesInitialized: true })
  }

  function updateMixinRule(index, patch) {
    const next = [...(settings.mixinRules || [])]
    const current = next[index] || { mixin: '', argIndex: 0, type: 'mixin-text', label: '' }
    next[index] = { ...current, ...patch }
    if (patch.argIndex !== undefined) {
      const parsed = Number(patch.argIndex)
      next[index].argIndex = Number.isNaN(parsed) ? 0 : Math.max(0, parsed)
    }
    if (patch.mixin !== undefined) {
      next[index].mixin = String(patch.mixin || '').trim()
    }
    if (patch.label !== undefined) {
      next[index].label = String(patch.label || '').trim()
    }
    saveSettings({ ...settings, mixinRules: normalizeMixinRules(next), mixinRulesInitialized: true })
  }

  function removeMixinRule(index) {
    const next = [...(settings.mixinRules || [])]
    next.splice(index, 1)
    saveSettings({ ...settings, mixinRules: next, mixinRulesInitialized: true })
  }

  function addCustomBlock() {
    const name = customBlockName.trim()
    if (!name) { customBlockError = 'Введи название блока'; return }
    const allNames = new Set([
      ...availableBlocks.map(b => b.name),
      ...(settings.customBlocks || []).map(b => b.name),
    ])
    if (allNames.has(name)) { customBlockError = 'Блок с таким именем уже существует'; return }
    const content = customBlockContent.trim()
    if (!content) { customBlockError = 'Введи код блока'; return }
    const newBlock = { id: createId(), name, content }
    const next = [...(settings.customBlocks || []), newBlock]
    saveSettings({ ...settings, customBlocks: next })
    customBlockName = ''
    customBlockContent = ''
    customBlockError = ''
  }

  function updateCustomBlock(id, patch) {
    const current = (settings.customBlocks || [])
    const old = current.find(cb => cb.id === id)
    if (!old) return
    if (patch.name !== undefined) {
      const name = patch.name.trim()
      const otherNames = new Set([
        ...availableBlocks.map(b => b.name),
        ...current.filter(cb => cb.id !== id).map(cb => cb.name),
      ])
      if (otherNames.has(name)) { customBlockError = 'Блок с таким именем уже существует'; return }
    }
    const next = current.map(cb => cb.id === id ? { ...cb, ...patch } : cb)
    const updated = next.find(cb => cb.id === id)
    if (patch.name && patch.name !== old.name) {
      assembledBlocks = assembledBlocks.map(ab =>
        ab.name === old.name ? { ...ab, name: patch.name } : ab
      )
      const nextSettings = { ...settings, customBlocks: next }
      // Migrate settings.blocks from old name to new name
      const blockSettings = settings.blocks?.[old.name]
      if (blockSettings) {
        nextSettings.blocks = { ...nextSettings.blocks, [patch.name]: blockSettings }
        delete nextSettings.blocks[old.name]
      }
      if (Array.isArray(settings.quickBlocks)) {
        nextSettings.quickBlocks = settings.quickBlocks.map(n => n === old.name ? patch.name : n)
      }
      saveSettings(nextSettings)
    } else {
      saveSettings({ ...settings, customBlocks: next })
    }
    if (patch.content !== undefined && updated) {
      assembledBlocks = assembledBlocks.map(ab =>
        ab.name === updated.name ? {
          ...ab,
          content: patch.content,
          schema: buildBaseSchema(patch.content, updated.name, settings?.mixinRules),
        } : ab
      )
      rebuildOutput()
      saveDraft()
    }
    customBlockError = ''
  }

  function removeCustomBlock(id) {
    const block = (settings.customBlocks || []).find(cb => cb.id === id)
    const next = (settings.customBlocks || []).filter(cb => cb.id !== id)
    const nextSettings = { ...settings, customBlocks: next }
    if (block && Array.isArray(settings.quickBlocks) && settings.quickBlocks.includes(block.name)) {
      nextSettings.quickBlocks = settings.quickBlocks.filter(n => n !== block.name)
    }
    saveSettings(nextSettings)
    if (editingCustomBlockId === id) editingCustomBlockId = null
  }

  function maybeAutoInitMixinRules() {
    if (settings.mixinRulesInitialized) return
    const defaults = buildDefaultMixinRulesFromBlocks(allBlocks)
    if (!defaults.length) return
    saveSettings({
      ...settings,
      mixinRules: defaults,
      mixinRulesInitialized: true,
    })
  }

  function updateProjectTitle(value) {
    saveSettings({ ...settings, projectTitle: value })
  }

  function updateEmailGenProject(value) {
    saveSettings({ ...settings, emailGenProject: value })
  }

  function updateAccentColor(value) {
    const next = value || '#6b7280'
    settings = { ...settings, accentColor: next }
    saveSettings(settings)
    setAccentColor(next)
  }

  async function handleLogoUpload(event) {
    const file = event.target.files?.[0]
    if (!file) return
    const reader = new FileReader()
    reader.onload = () => {
      const result = String(reader.result || '')
      if (!result) return
      saveSettings({ ...settings, logoData: result })
    }
    reader.readAsDataURL(file)
    event.target.value = ''
  }

  function removeLogo() {
    const next = { ...settings }
    delete next.logoData
    saveSettings(next)
  }

  function getBlockSetting(name) {
    return settings.blocks?.[name] ?? {}
  }

  function getBlockTemplate(name) {
    const override = getBlockSetting(name).template
    if (typeof override === 'string' && override.trim() !== '') return override
    const original = allBlocks.find((block) => block.name === name)?.content
    return original ?? ''
  }

  function getEffectiveTemplate(name) {
    const override = getBlockSetting(name).template
    if (typeof override === 'string' && override.trim() !== '') return override
    return getBlockTemplate(name)
  }

  function fieldSignature(field) {
    const slot = `${field.argIndex ?? field.occurrence ?? 0}:${field.objectKey ?? ''}`
    return `${field.type}:${field.lineIndex ?? 'raw'}:${slot}`
  }

  function applySchemaSettings(schema, blockName) {
    const overrides = getBlockSetting(blockName).fields ?? {}
    return schema
      .filter((field) => overrides[fieldSignature(field)]?.enabled !== false)
      .map((field) => ({
        ...field,
        label: overrides[fieldSignature(field)]?.label || field.label,
      }))
  }

  function ensureSchema(block) {
    const schema = buildBaseSchema(block.content, block.name, settings?.mixinRules)
    block.schema = schema
    return schema
  }

  function extractFirstTextValue(content) {
    const lines = normalizeNewlines(content).split('\n')
    for (const line of lines) {
      const meta = getParagraphMeta(line)
      if (meta && meta.value.trim() !== '') return meta.value
    }
    return ''
  }

  function setFirstTextValue(content, value) {
    const lines = normalizeNewlines(content).split('\n')
    for (let index = 0; index < lines.length; index += 1) {
      const meta = getParagraphMeta(lines[index])
      if (meta) {
        lines[index] = lines[index].slice(0, meta.start) + value + lines[index].slice(meta.end)
        return lines.join('\n')
      }
    }
    return content
  }

  function appendTextParagraph(content, value) {
    const lines = normalizeNewlines(content).trimEnd().split('\n')
    const spanIndex = lines.findIndex((line) => Boolean(getParagraphMeta(line)))
    if (spanIndex === -1) return content
    const spanLine = lines[spanIndex]
    const spanIndent = spanLine.match(/^\s*/)?.[0] ?? ''
    const tdIndent = spanIndent.slice(0, Math.max(0, spanIndent.length - 4))
    const trIndent = spanIndent.slice(0, Math.max(0, spanIndent.length - 8))
    const spacerLine = lines.find((line) => line.trim().startsWith('+spacerLine'))
    const spacerIndent = spacerLine ? spacerLine.match(/^\s*/)?.[0] ?? '' : trIndent
    const tdLine = lines[spanIndex - 1] ?? `${tdIndent}td`
    const trLine = lines[spanIndex - 2] ?? `${trIndent}tr`
    const meta = getParagraphMeta(spanLine)
    const spanPrefix = meta ? spanLine.slice(0, meta.start) : `${spanLine.trimEnd()} `

    const appended = [
      `${spacerIndent}+spacerLine(20)`,
      trLine.trimEnd(),
      tdLine.trimEnd(),
      `${spanPrefix}${value}`,
    ]

    return `${lines.join('\n')}\n${appended.join('\n')}`
  }

  function removeTextParagraph(block, field) {
    pushHistory()
    const lines = normalizeNewlines(block.content).split('\n')
    const spanIndex = field.lineIndex
    if (spanIndex == null || spanIndex < 0 || spanIndex >= lines.length) return

    let removeStart = spanIndex - 2
    let removeEnd = spanIndex

    if (removeStart < 0) removeStart = spanIndex

    const spacerIndex = spanIndex - 3
    if (spacerIndex >= 0 && lines[spacerIndex].trim().startsWith('+spacerLine')) {
      removeStart = spacerIndex
    }

    const nextLines = [
      ...lines.slice(0, removeStart),
      ...lines.slice(removeEnd + 1),
    ]

    const content = nextLines.join('\n').trimEnd()
    assembledBlocks = assembledBlocks.map((item) =>
      item.id === block.id
        ? {
            ...item,
            content,
            schema: buildBaseSchema(content, block.name, settings?.mixinRules),
          }
        : item
    )
    rebuildOutput()
    saveDraft()
  }

  function normalizeDraftBlocks(draft) {
    const normalized = []
    draft.forEach((block) => {
      if (isExtraTextBlock(block.name)) {
        const extraText = extractFirstTextValue(block.content)
        const last = normalized[normalized.length - 1]
        if (last && last.name === 'Текст') {
          const mergedContent = appendTextParagraph(last.content, extraText)
          normalized[normalized.length - 1] = {
            ...last,
            content: mergedContent,
            schema: undefined,
          }
          return
        }
        const template = getBlockTemplate('Текст')
        if (template) {
          const withText = setFirstTextValue(template, extraText)
          normalized.push({
            ...block,
            name: 'Текст',
            content: withText,
            schema: undefined,
          })
          return
        }
      }
      normalized.push(block)
    })
    return normalized
  }

  function getProductLineInfo(content) {
    const lines = normalizeNewlines(content).split('\n')
    for (let i = 0; i < lines.length; i += 1) {
      const line = lines[i]
      if (line.trim().startsWith('+products')) {
        return { lineIndex: i, line }
      }
    }
    return { lineIndex: -1, line: '' }
  }

  function getProductOptionsFromLine(line) {
    const objStart = line.indexOf('{')
    const objEnd = line.lastIndexOf('}')
    if (objStart === -1 || objEnd === -1 || objEnd < objStart) {
      return { options: {}, hasObject: false, objStart: -1, objEnd: -1 }
    }
    const inside = line.slice(objStart + 1, objEnd)
    return { options: parseOptionsObject(inside), hasObject: true, objStart, objEnd }
  }

  function updateProductOptionsInLine(line, key, value, enabled) {
    const info = getProductOptionsFromLine(line)
    const options = { ...info.options }
    if (enabled) {
      options[key] = value
    } else {
      delete options[key]
    }
    const objString = stringifyOptions(options)
    if (!objString) {
      if (!info.hasObject) return line
      const prefix = line.slice(0, info.objStart).replace(/,\s*$/, '')
      const suffix = line.slice(info.objEnd + 1)
      return `${prefix}${suffix}`.replace(/\s+\)/, ')')
    }
    if (info.hasObject) {
      return line.slice(0, info.objStart + 1) + objString + line.slice(info.objEnd)
    }
    const endArgs = line.lastIndexOf(')')
    if (endArgs === -1) return line
    const before = line.slice(0, endArgs)
    const after = line.slice(endArgs)
    return `${before}, {${objString}}${after}`
  }

  function getProductOptionState(block) {
    const { line } = getProductLineInfo(block.content)
    if (!line) return {}
    return getProductOptionsFromLine(line).options
  }

  function updateProductOptionForBlock(block, option, enabled) {
    const parsed = parseOptionCode(option.code)
    if (!parsed) return
    const lines = normalizeNewlines(block.content).split('\n')
    const { lineIndex, line } = getProductLineInfo(block.content)
    if (lineIndex === -1) return
    const updatedLine = updateProductOptionsInLine(line, parsed.key, parsed.value, enabled)
    lines[lineIndex] = updatedLine
    updateBlockContent(block.id, lines.join('\n'))
  }

  function applyDefaultProductOptions(content) {
    let result = content
    const options = settings?.productOptions || []
    for (const option of options) {
      if (option.defaultEnabled === undefined) continue
      const parsed = parseOptionCode(option.code)
      if (!parsed) continue
      const lines = normalizeNewlines(result).split('\n')
      const lineIndex = lines.findIndex(l => /^\s*\+\s*\w/.test(l) && l.includes('{'))
      if (lineIndex === -1) continue
      lines[lineIndex] = updateProductOptionsInLine(lines[lineIndex], parsed.key, parsed.value, !!option.defaultEnabled)
      result = lines.join('\n')
    }
    return result
  }

  function toggleProductOptionForBlock(block, option) {
    const parsed = parseOptionCode(option.code)
    if (!parsed) return
    const state = getProductOptionState(block)
    const current = state[parsed.key]
    const enabled = current !== parsed.value
    updateProductOptionForBlock(block, option, enabled)
  }

  function isOptionEnabledInAllBlocks(option) {
    const parsed = parseOptionCode(option.code)
    if (!parsed) return false
    const productBlocks = assembledBlocks.filter((block) => isProductBlock(block.content))
    if (!productBlocks.length) return false
    return productBlocks.every((block) => getProductOptionState(block)[parsed.key] === parsed.value)
  }

  function isOptionEnabledInAnyBlock(option) {
    const parsed = parseOptionCode(option.code)
    if (!parsed) return false
    const productBlocks = assembledBlocks.filter((block) => isProductBlock(block.content))
    if (!productBlocks.length) return false
    return productBlocks.some((block) => getProductOptionState(block)[parsed.key] === parsed.value)
  }

  function toggleProductOptionForAll(option) {
    const enable = !isOptionEnabledInAllBlocks(option)
    assembledBlocks.forEach((block) => {
      if (!isProductBlock(block.content)) return
      updateProductOptionForBlock(block, option, enable)
    })
  }

  function getFieldValue(block, field) {
    if (field.type === 'raw') return block.content.trimEnd()
    const lines = normalizeNewlines(block.content).split('\n')
    const line = lines[field.lineIndex] ?? ''
    if (field.objectKey) {
      const range = findMixinArgRange(line, field.argIndex)
      if (!range) return ''
      const rawArg = line.slice(range.start, range.end)
      const props = parseMixinObjectArg(rawArg)
      const found = props.find((item) => item.key === field.objectKey)
      return found?.value ?? ''
    }
    if (field.type === 'mixin-text' || field.type === 'mixin-href' || field.type === 'mixin-ids' || field.type === 'mixin-opts') {
      return extractMixinArgValue(line, field.argIndex)
    }
    if (field.type === 'text' || field.type === 'list-item') {
      const meta = splitTextField(line)
      return meta ? meta.value : ''
    }
    const linkRegex = /(href|src)=("([^"]*)"|'([^']*)')/g
    let match
    let occurrence = 0
    while ((match = linkRegex.exec(line))) {
      if (occurrence === field.occurrence) {
        return match[3] ?? match[4] ?? ''
      }
      occurrence += 1
    }
    return ''
  }

  function updateField(block, field, newValue) {
    if (field.type === 'raw') {
      updateBlockContent(block.id, newValue)
      return
    }
    // Prevent newlines from breaking Pug structure — replace with <br> for text, space for others
    if (newValue.includes('\n')) {
      newValue = (field.type === 'text' || field.type === 'list-item')
        ? newValue.replace(/\r?\n/g, '<br>')
        : newValue.replace(/\r?\n/g, ' ')
    }
    const lines = normalizeNewlines(block.content).split('\n')
    const line = lines[field.lineIndex] ?? ''
    if (field.objectKey) {
      const range = findMixinArgRange(line, field.argIndex)
      if (!range) return
      const rawArg = line.slice(range.start, range.end)
      const updatedArg = replaceObjectPropInArg(rawArg, field.objectKey, newValue)
      lines[field.lineIndex] = line.slice(0, range.start) + updatedArg + line.slice(range.end)
      updateBlockContent(block.id, lines.join('\n'))
      return
    }
    if (field.type === 'mixin-text' || field.type === 'mixin-href' || field.type === 'mixin-ids' || field.type === 'mixin-opts') {
      lines[field.lineIndex] =
        field.type === 'mixin-opts'
          ? replaceMixinArgRaw(line, field.argIndex, newValue)
          : replaceMixinArgQuoted(line, field.argIndex, newValue)
      updateBlockContent(block.id, lines.join('\n'))
      return
    }
    if (field.type === 'text' || field.type === 'list-item') {
      const meta = splitTextField(line)
      if (meta) {
        lines[field.lineIndex] = line.slice(0, meta.start) + newValue + line.slice(meta.end)
      } else {
        lines[field.lineIndex] = line ? `${line} ${newValue}` : newValue
      }
    } else {
      const linkRegex = /(href|src)=("([^"]*)"|'([^']*)')/g
      let match
      let occurrence = 0
      let updated = line
      while ((match = linkRegex.exec(line))) {
        if (occurrence === field.occurrence) {
          const value = match[3] ?? match[4] ?? ''
          const valueIndex = match[1].length + 2
          const start = match.index + valueIndex
          const end = start + value.length
          updated = line.slice(0, start) + newValue + line.slice(end)
          break
        }
        occurrence += 1
      }
      lines[field.lineIndex] = updated
    }
    updateBlockContent(block.id, lines.join('\n'))
  }

  function handleImageDragOver(event, block, field) {
    event.preventDefault()
    event.dataTransfer.dropEffect = 'copy'
    dragOverField = fieldKey(block, field)
  }

  function handleImageDragLeave(event) {
    if (!event.currentTarget.contains(event.relatedTarget)) dragOverField = null
  }

  async function handleImageDrop(event, block, field) {
    event.preventDefault()
    dragOverField = null
    const file = event.dataTransfer?.files?.[0]
    if (!file || !file.type.startsWith('image/')) return
    const fk = fieldKey(block, field)
    if (file.size > 20 * 1024 * 1024) {
      uploadingFields = { ...uploadingFields, [fk]: { loading: false, error: 'Макс. 20 МБ' } }
      return
    }
    uploadingFields = { ...uploadingFields, [fk]: { loading: true, error: '' } }
    try {
      const dataUrl = await new Promise((resolve, reject) => {
        const reader = new FileReader()
        reader.onload = () => resolve(reader.result)
        reader.onerror = () => reject(new Error('Не удалось прочитать файл'))
        reader.readAsDataURL(file)
      })
      if (settings.ftpConfig?.host && currentLetterDate) {
        const folder = getLetterDateFolder()
        const baseName = file.name.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9_-]/g, '_')
        const fileName = `${baseName}-${Date.now().toString(36)}`
        const result = await apiFtpUpload(currentProject, { imageData: dataUrl, fileName, folder })
        updateField(block, field, result.url)
      } else {
        const result = await apiUploadImage(dataUrl, file.name, currentProject)
        updateField(block, field, result.url)
      }
      uploadingFields = { ...uploadingFields, [fk]: { loading: false, error: '' } }
    } catch (e) {
      uploadingFields = { ...uploadingFields, [fk]: { loading: false, error: e?.message || 'Ошибка загрузки' } }
    }
  }

  function fieldKey(block, field) {
    const slot = `${field.argIndex ?? field.occurrence ?? 0}:${field.objectKey ?? ''}`
    return `${block.id}:${field.type}:${field.lineIndex ?? 'raw'}:${slot}`
  }

  function isImageField(field) {
    if (field.type === 'src') return true
    if (field.label === 'Картинка') return true
    const label = (field.label || '').toLowerCase()
    return field.type === 'mixin-href' && (label.includes('картинк') || label.includes('image') || label.includes('banner'))
  }

  function looksLikeImageUrl(value) {
    return value ? /^https?:\/\/.+\.(png|jpe?g|gif|webp|svg)(\?|#|$)/i.test(value.trim()) : false
  }

  function registerTextField(key, node) {
    if (!node) return
    textFieldRefs = { ...textFieldRefs, [key]: node }
  }

  function unregisterTextField(key) {
    if (!textFieldRefs[key]) return
    const next = { ...textFieldRefs }
    delete next[key]
    textFieldRefs = next
  }

  function textRef(node, key) {
    registerTextField(key, node)
    return {
      update(nextKey) {
        if (nextKey !== key) {
          unregisterTextField(key)
          key = nextKey
          registerTextField(key, node)
        }
      },
      destroy() {
        unregisterTextField(key)
      },
    }
  }

  function wrapBold(block, field) {
    const key = fieldKey(block, field)
    const input = textFieldRefs[key]
    const current = getFieldValue(block, field)
    if (!input) {
      updateField(block, field, `${current}<span style="font-weight: 700;"></span>`)
      return
    }
    const start = input.selectionStart ?? current.length
    const end = input.selectionEnd ?? current.length
    const before = current.slice(0, start)
    const middle = current.slice(start, end)
    const after = current.slice(end)
    const wrapped = `<span style="font-weight: 700;">${middle}</span>`
    updateField(block, field, `${before}${wrapped}${after}`)
  }

  function insertBullet(block, field) {
    const key = fieldKey(block, field)
    const input = textFieldRefs[key]
    const current = getFieldValue(block, field)
    if (!input) {
      updateField(block, field, `${current}<br>&bull;`)
      return
    }
    const start = input.selectionStart ?? current.length
    const end = input.selectionEnd ?? current.length
    const before = current.slice(0, start)
    const after = current.slice(end)
    updateField(block, field, `${before}<br>&bull;${after}`)
  }

  function insertBr(block, field) {
    const key = fieldKey(block, field)
    const input = textFieldRefs[key]
    const current = getFieldValue(block, field)
    if (!input) {
      updateField(block, field, `${current}<br>`)
      return
    }
    const pos = input.selectionStart ?? current.length
    // Trim spaces around cursor
    let left = pos
    while (left > 0 && current[left - 1] === ' ') left--
    let right = pos
    while (right < current.length && current[right] === ' ') right++
    const before = current.slice(0, left)
    const after = current.slice(right)
    updateField(block, field, `${before}<br>${after}`)
  }

  function updateLinkDraft(key, value) {
    linkDrafts = { ...linkDrafts, [key]: value }
  }

  function togglePreview(key) {
    previewOpen = { ...previewOpen, [key]: !previewOpen[key] }
  }

  function setTheme(value) {
    theme = value
    localStorage.setItem('va-theme', theme)
    if (currentUser) apiSavePreferences({ theme }).catch(() => {})
  }

  function setActivePage(next) {
    activePage = next
    if (typeof localStorage !== 'undefined') localStorage.setItem('va-active-page', next)
    if (currentUser) apiSavePreferences({ activePage: next }).catch(() => {})
    if (next === 'plan') loadPlan()
    if (next === 'settings') loadPartsFiles()
  }

  // --- Yonote / Plan functions ---
  async function loadYonoteStatus() {
    try {
      const config = await apiGetConfig()
      uploadBaseUrl = config.upload_base_url || ''
      yonoteStatus = await apiGetYonoteStatus()
    } catch { yonoteStatus = { configured: false, connected: false } }
  }

  async function saveUploadConfig() {
    uploadSaving = true; uploadConfigError = ''; uploadConfigStatus = ''
    try {
      await apiSaveConfig({ upload_base_url: uploadBaseUrl.trim() })
      uploadConfigStatus = 'Сохранено'
      setTimeout(() => { uploadConfigStatus = '' }, 3000)
    } catch (e) { uploadConfigError = e.message }
    uploadSaving = false
  }

  async function saveYonoteToken() {
    yonoteSaving = true; yonoteConfigError = ''; yonoteConfigStatus = ''
    try {
      const payload = {}
      if (yonoteTokenDraft.trim()) payload.yonote_token = yonoteTokenDraft.trim()
      if (yonoteBaseUrlDraft.trim()) payload.yonote_base_url = yonoteBaseUrlDraft.trim()
      await apiSaveConfig(payload)
      yonoteTokenDraft = ''
      await loadYonoteStatus()
      yonoteConfigStatus = 'Сохранено'
      setTimeout(() => { yonoteConfigStatus = '' }, 3000)
    } catch (err) { yonoteConfigError = err.message }
    yonoteSaving = false
  }

  async function loadYonoteDatabases() {
    yonoteDbLoading = true
    try {
      const data = await apiYonoteListDatabases()
      yonoteDatabases = data.databases || []
    } catch { yonoteDatabases = [] }
    yonoteDbLoading = false
  }

  async function loadYonoteDbProperties(dbId) {
    if (!dbId) { yonoteDbProperties = []; return }
    yonotePropsLoading = true
    try {
      const data = await apiYonoteGetProperties(dbId)
      yonoteDbProperties = data.properties || []
    } catch { yonoteDbProperties = [] }
    yonotePropsLoading = false
  }

  function saveYonoteProjectConfig() {
    const yc = {
      databaseId: yonoteCfgDatabaseId.trim(),
      properties: {
        date: yonoteCfgDateProp,
        subject: yonoteCfgSubjectProp,
        preheader: yonoteCfgPreheaderProp,
        status: yonoteCfgStatusProp,
      },
      statusFilter: [],
      sentStatus: yonoteCfgSentStatus,
    }
    saveSettings({ ...settings, yonoteConfig: yc })
  }

  async function testYonoteConnection() {
    yonoteTestLoading = true; yonoteTestResult = ''
    try {
      saveYonoteProjectConfig()
      const yc = settings.yonoteConfig
      if (!yc?.databaseId) { yonoteTestResult = 'error:Выбери базу данных'; yonoteTestLoading = false; return }
      const data = await apiYonoteGetRows(yc.databaseId)
      yonoteTestResult = `ok:Успешно. Строк: ${(data.rows || []).length}`
    } catch (err) { yonoteTestResult = `error:${err.message}` }
    yonoteTestLoading = false
  }

  // --- FTP functions ---
  function saveFtpConfig() {
    const fc = {
      protocol: ftpProtocol, host: ftpHost.trim(), port: ftpPort.trim(),
      user: ftpUser.trim(), password: ftpPassword.trim() || undefined,
      remotePath: ftpRemotePath.trim().replace(/\/$/, ''),
      baseUrl: ftpBaseUrl.trim().replace(/\/$/, ''),
    }
    saveSettings({ ...settings, ftpConfig: fc })
  }
  function saveMailingService() {
    saveSettings({ ...settings, mailingService: {
      label: mailingServiceLabel.trim(), url: mailingServiceUrl.trim(),
    }})
  }

  let feedRefreshLoading = false, feedRefreshResult = '', feedRefreshDiff = null

  function saveGenderPaths() {
    saveSettings({
      ...settings,
      genderPaths: {
        headerFemale: genderHeaderFemale.trim(),
        headerMale: genderHeaderMale.trim(),
        footerFemale: genderFooterFemale.trim(),
        footerMale: genderFooterMale.trim(),
      }
    })
  }

  function saveImageConfig() {
    saveSettings({
      ...settings,
      imageBaseUrl: imageBaseUrl.trim(),
      imageExt: imageExt.trim() || '.png',
    })
  }

  function saveFeedUrl() {
    saveSettings({ ...settings, feedUrl: feedUrlDraft.trim() })
  }

  async function refreshFeed() {
    if (!currentProject || !feedUrlDraft.trim()) return
    feedRefreshLoading = true
    feedRefreshResult = ''
    feedRefreshDiff = null
    try {
      const data = await apiFeedRefresh(currentProject)
      if (data.added !== undefined) {
        const parts = [`${data.count} товаров`]
        if (data.added > 0) parts.push(`+${data.added} новых`)
        if (data.removed > 0) parts.push(`−${data.removed} удалено`)
        if (data.added === 0 && data.removed === 0) parts.push('без изменений')
        feedRefreshResult = `ok:${parts.join(', ')}`
        feedRefreshDiff = data
      } else {
        feedRefreshResult = `ok:Загружено ${data.count} товаров`
      }
    } catch (err) {
      feedRefreshResult = `error:${err.message}`
    }
    feedRefreshLoading = false
  }

  async function testFtpConnection() {
    ftpTestLoading = true; ftpTestResult = ''
    saveFtpConfig()
    try {
      const result = await apiFtpTest(currentProject)
      ftpTestResult = `ok:${result.message || 'Подключено'}`
    } catch (err) { ftpTestResult = `error:${err.message}` }
    ftpTestLoading = false
  }
  function getLetterDateFolder() {
    if (!currentLetterDate) return ''
    return formatLetterDate(currentLetterDate).slice(0, 5)
  }
  async function openFtpGallery(block, field) {
    const folder = getLetterDateFolder()
    if (!folder) { alert('Сначала создай или открой письмо'); return }
    ftpGalleryTargetField = { block, field }
    ftpGalleryFolder = folder; ftpGalleryError = ''
    ftpUploadFileName = ''; ftpUploadFile = null; ftpUploadError = ''; ftpUploading = false
    // показать кэш мгновенно, если есть
    ftpGalleryImages = ftpGalleryCache[folder] || []
    showFtpGalleryModal = true
    await loadFtpGallery()
  }
  async function loadFtpGallery() {
    ftpGalleryLoading = true; ftpGalleryError = ''
    try {
      const result = await apiFtpList(currentProject, ftpGalleryFolder)
      ftpGalleryImages = result.files || []
      ftpGalleryCache[ftpGalleryFolder] = ftpGalleryImages
    } catch (e) { ftpGalleryError = e.message }
    ftpGalleryLoading = false
  }
  async function deleteFtpImage(img) {
    try {
      await apiFtpDelete(currentProject, { folder: ftpGalleryFolder, fileName: img.name })
      ftpGalleryImages = ftpGalleryImages.filter(i => i.url !== img.url)
      ftpGalleryCache[ftpGalleryFolder] = ftpGalleryImages
      ftpDeletePending = null
    } catch (e) {
      alert('Ошибка удаления: ' + (e?.message || e))
    }
  }
  function selectFtpImage(url) {
    if (ftpGalleryTargetField) {
      updateField(ftpGalleryTargetField.block, ftpGalleryTargetField.field, url)
    }
    showFtpGalleryModal = false
  }
  let ftpUploadStatus = ''
  let ftpUploadProgress = ''  // "2 / 5"
  let ftpUploadPercent = 0    // 0–100
  async function uploadToFtp() {
    if (!ftpUploadFile || !ftpUploadFileName.trim()) return
    ftpUploading = true; ftpUploadError = ''; ftpUploadStatus = 'Читаю файл…'
    try {
      const dataUrl = await new Promise((resolve, reject) => {
        const reader = new FileReader()
        reader.onload = () => resolve(reader.result)
        reader.onerror = reject
        reader.readAsDataURL(ftpUploadFile)
      })
      ftpUploadStatus = 'Подключаюсь к серверу…'
      const result = await apiFtpUpload(currentProject, {
        imageData: dataUrl, fileName: ftpUploadFileName.trim(), folder: ftpGalleryFolder,
      })
      ftpUploadStatus = 'Загружено!'
      if (ftpGalleryTargetField) {
        updateField(ftpGalleryTargetField.block, ftpGalleryTargetField.field, result.url)
      }
      await loadFtpGallery()
      ftpUploadFileName = ''; ftpUploadFile = null
      setTimeout(() => { ftpUploadStatus = '' }, 5000)
    } catch (e) {
      // Попробуем достать steps из ответа
      let detail = e.message
      try {
        const body = JSON.parse(e.message.replace(/^[^{]*/, ''))
        if (body.steps) detail = body.details + '\nШаги: ' + body.steps.join(' → ')
      } catch {}
      ftpUploadError = detail
      ftpUploadStatus = ''
    }
    ftpUploading = false
  }
  async function handleFtpFileSelect(event) {
    const files = Array.from(event.target.files || [])
    if (!files.length) return
    ftpUploadError = ''; ftpUploading = true; ftpUploadProgress = ''; ftpUploadPercent = 0
    const errors = []
    let done = 0
    for (const file of files) {
      if (file.size > 20 * 1024 * 1024) { errors.push(`${file.name}: макс. 20 МБ`); continue }
      ftpUploadProgress = files.length > 1 ? `${done + 1} / ${files.length}` : ''
      ftpUploadPercent = files.length > 1 ? Math.round(done / files.length * 100) : 30
      ftpUploadStatus = `Загружаю…`
      try {
        const dataUrl = await new Promise((resolve, reject) => {
          const reader = new FileReader()
          reader.onload = () => resolve(reader.result)
          reader.onerror = reject
          reader.readAsDataURL(file)
        })
        const fileName = file.name.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9а-яА-ЯёЁ_-]/g, '_')
        const result = await apiFtpUpload(currentProject, { imageData: dataUrl, fileName, folder: ftpGalleryFolder })
        done++
        ftpUploadPercent = Math.round(done / files.length * 100)
        // добавляем в галерею и кэш сразу
        const newImg = { name: result.name, url: result.url }
        ftpGalleryImages = [...ftpGalleryImages, newImg]
        ftpGalleryCache[ftpGalleryFolder] = ftpGalleryImages
      } catch (e) { errors.push(`${file.name}: ${e?.message || 'Ошибка'}`) }
    }
    ftpUploading = false; ftpUploadProgress = ''; ftpUploadPercent = 0
    ftpUploadStatus = errors.length ? '' : `Загружено ${done} файл${done === 1 ? '' : done < 5 ? 'а' : 'ов'}!`
    ftpUploadError = errors.join('\n')
    event.target.value = ''
    if (!errors.length) setTimeout(() => { ftpUploadStatus = '' }, 4000)
  }

  function isUrl(str) { return /^https?:\/\//i.test(str) }

  function parsePlanDate(str) {
    const parts = String(str || '').trim().split(/[./]/)
    if (parts.length !== 3) return null
    const [d, m, y] = parts.map(Number)
    if (!d || !m || !y) return null
    return new Date(y, m - 1, d)
  }

  function formatPlanDate(date) {
    if (!date) return ''
    return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`
  }

  function planDateToIso(date) {
    if (!date) return ''
    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
  }

  function planRowFingerprint(r) { return `${r.project}|${r.dateStr}|${r.subject}|${r.status}` }

  function savePlanCache(rows, at) {
    try {
      const serializable = rows.map(r => ({ ...r, date: r.date?.toISOString() }))
      localStorage.setItem('va-plan-cache', JSON.stringify({ rows: serializable, at: at.toISOString() }))
    } catch {}
  }

  function loadPlanCache() {
    try {
      const raw = localStorage.getItem('va-plan-cache')
      if (!raw) return null
      const data = JSON.parse(raw)
      const rows = (data.rows || []).map(r => ({ ...r, date: r.date ? new Date(r.date) : null }))
      return { rows, at: new Date(data.at) }
    } catch { return null }
  }

  function detectPlanChanges(oldRows, newRows) {
    const oldSet = new Set(oldRows.map(planRowFingerprint))
    const newSet = new Set(newRows.map(planRowFingerprint))
    const added = newRows.filter(r => !oldSet.has(planRowFingerprint(r)))
    const removed = oldRows.filter(r => !newSet.has(planRowFingerprint(r)))
    return { added, removed }
  }

  function showToast(message, type = 'info', duration = 5000) {
    const id = ++toastId
    toasts = [...toasts, { id, message, type }]
    setTimeout(() => { toasts = toasts.filter(t => t.id !== id) }, duration)
  }

  function sendPlanNotification(changes) {
    const lines = []
    for (const r of changes.added) lines.push(`+ ${r.project}: ${r.subject}`)
    for (const r of changes.removed) lines.push(`− ${r.project}: ${r.subject}`)
    if (lines.length === 0) return
    const text = lines.slice(0, 5).join('\n')
    showToast('План обновлён:\n' + text, 'plan')
    if (planNotificationsEnabled && Notification.permission === 'granted') {
      new Notification('Aspekter VA — План обновлён', { body: text, icon: '/favicon.ico' })
    }
  }

  async function enablePlanNotifications() {
    if (!('Notification' in window)) return
    if (Notification.permission === 'granted') { planNotificationsEnabled = true; return }
    if (Notification.permission === 'denied') return
    const result = await Notification.requestPermission()
    planNotificationsEnabled = result === 'granted'
  }

  function extractYonoteValue(val, propDef) {
    if (val == null) return ''
    if (typeof val === 'string') return val
    if (typeof val === 'number') return String(val)
    if (typeof val === 'boolean') return val ? 'Да' : 'Нет'
    if (typeof val === 'object' && !Array.isArray(val) && val.from) return val.from // DatePropertyValue
    if (typeof val === 'object' && !Array.isArray(val) && val.url) return val.title || val.url // URLPropertyValue
    if (Array.isArray(val) && propDef?.options?.length) {
      // select/status/multi_select — массив ID опций, резолвим в лейблы
      const optMap = new Map(propDef.options.map(o => [o.id, o.label]))
      return val.map(id => optMap.get(id) || id).join(', ')
    }
    if (Array.isArray(val)) return val.map(v => v?.label || v?.title || v?.fileName || String(v)).filter(Boolean).join(', ')
    return String(val)
  }

  async function fetchPlanRows() {
    const now = new Date()
    const cutPast = new Date(now.getTime() - 7 * 86400000)
    const cutFuture = new Date(now.getTime() + 60 * 86400000)
    const results = []
    const warnings = []
    const projName = PROJECT_NAME
    let projSettings
    try { const d = await apiGetProject(projName); projSettings = d.settings || {} } catch { return { rows: [], warnings: ['Не удалось загрузить проект'] } }
    const yc = projSettings.yonoteConfig
    if (!yc?.databaseId || !yc?.properties) return { rows: [], warnings: ['Yonote не настроен: укажи базу данных и свойства в Настройки → Интеграции'] }
    try {
      // Загружаем свойства для резолва ID опций и статусов
      const propsData = await apiYonoteGetProperties(yc.databaseId)
      const propsMap = new Map((propsData.properties || []).map(p => [p.id, p]))
      const statusDef = propsMap.get(yc.properties.status)
      if (statusDef?.options?.length) {
        yonoteStatusOptions = statusDef.options.map(o => o.label)
      }
      const data = await apiYonoteGetRows(yc.databaseId)
      const rows = data.rows || []
      const corePropIds = new Set([yc.properties.date, yc.properties.subject, yc.properties.preheader, yc.properties.status].filter(Boolean))
      let found = 0
      for (const row of rows) {
        const vals = row.values || {}
        const dateRaw = vals[yc.properties.date]
        const subject = extractYonoteValue(vals[yc.properties.subject], propsMap.get(yc.properties.subject))
        const preheader = extractYonoteValue(vals[yc.properties.preheader], propsMap.get(yc.properties.preheader))
        const status = extractYonoteValue(vals[yc.properties.status], propsMap.get(yc.properties.status))
        if (!subject) continue
        let dateStr = ''
        let date = null
        if (dateRaw && typeof dateRaw === 'object' && dateRaw.from) {
          dateStr = dateRaw.from
          date = new Date(dateRaw.from)
        } else if (typeof dateRaw === 'string') {
          dateStr = dateRaw
          date = parsePlanDate(dateRaw)
        }
        if (!date || isNaN(date.getTime()) || date < cutPast || date > cutFuture) continue
        // Показываем все статусы (фильтрация убрана)
        const extra = []
        for (const [propId, val] of Object.entries(vals)) {
          if (corePropIds.has(propId)) continue
          const prop = propsMap.get(propId)
          const v = extractYonoteValue(val, prop)
          if (!v) continue
          extra.push({ label: prop?.title || propId, value: v, link: (prop?.type === 'url' && val?.url) ? val.url : '' })
        }
        results.push({ project: projName, date, dateStr, subject, preheader, status, rowId: row.id, extra })
        found++
      }
      if (found === 0) {
        warnings.push(`${projName}: 0 строк в диапазоне −7/+60 дней из ${rows.length} всего`)
      }
    } catch (e) {
      warnings.push(`${projName}: ошибка чтения Yonote — ${e?.message || e}`)
    }
    return { rows: results.sort((a, b) => a.date - b.date), warnings }
  }

  async function loadPlan(background = false) {
    if (!background) {
      const cache = loadPlanCache()
      if (cache && cache.rows.length > 0) {
        planRows = cache.rows
        planLoadedAt = cache.at
      }
      planLoading = true; planError = ''; planWarnings = []
    }
    try {
      const { rows, warnings } = await fetchPlanRows()
      if (planRows.length > 0) {
        const changes = detectPlanChanges(planRows, rows)
        if (changes.added.length || changes.removed.length) sendPlanNotification(changes)
      }
      planRows = rows
      planWarnings = warnings
      planLoadedAt = new Date()
      savePlanCache(rows, planLoadedAt)
    } catch (e) {
      if (!background) planError = e?.message || String(e)
    }
    if (!background) planLoading = false
  }

  function startPlanPolling() {
    stopPlanPolling()
    planPollingTimer = setInterval(() => loadPlan(true), 3 * 60 * 1000)
  }

  function stopPlanPolling() {
    if (planPollingTimer) { clearInterval(planPollingTimer); planPollingTimer = null }
  }

  function extractIdsFromExtra(extra) {
    const idFields = (extra || []).filter(e => /^ID\b/i.test(e.label))
    const female = idFields.filter(e => /жен|female|ж\b/i.test(e.label))
    const male = idFields.filter(e => /муж|male|м\b/i.test(e.label))
    const other = idFields.filter(e => !/жен|female|ж\b|муж|male|м\b/i.test(e.label))
    return [...female, ...other, ...male].map(e => e.value).filter(Boolean).join(',')
  }

  function extraWithoutIds(extra) {
    return (extra || []).filter(e => !/^ID\b/i.test(e.label))
  }

  async function startAssemble(row) {
    if (row.project !== currentProject) return
    // Check for existing letter linked to this exact plan row (by rowId), or by title+date if no rowId
    const date = planDateToIso(row.date)
    const existing = row.rowId
      ? letters.find(l => l.assemblyInfo?.rowId === row.rowId)
      : letters.find(l => l.title === row.subject && l.date === date && !l.assemblyInfo?.rowId)
    if (existing) {
      await openLetter(existing)
      // Update assemblyInfo with fresh data from plan (status/extras may have changed)
      assemblyInfo = {
        subject: row.subject,
        preheader: row.preheader,
        status: row.status,
        dateStr: row.dateStr,
        project: row.project,
        rowId: row.rowId,
        extra: extraWithoutIds(row.extra),
      }
      // Auto-fill ID pool from Yonote ID fields
      const idsFromYonote = extractIdsFromExtra(row.extra)
      if (idsFromYonote) {
        idPoolInput = idsFromYonote
        applyIdPool()
        idPoolOpen = true
      }
      assemblyCardHidden = false
      return
    }
    assemblyInfo = {
      subject: row.subject,
      preheader: row.preheader,
      status: row.status,
      dateStr: row.dateStr,
      project: row.project,
      rowId: row.rowId,
      extra: extraWithoutIds(row.extra),
    }
    // Auto-fill ID pool from Yonote ID fields
    const idsFromYonote = extractIdsFromExtra(row.extra)
    if (idsFromYonote) {
      idPoolInput = idsFromYonote
      applyIdPool()
      idPoolOpen = true
    }
    assemblyCardHidden = false
    const id = createId()
    const title = row.subject
    const name = buildLetterName(title, date)
    const now = new Date().toISOString()
    const nextLetter = { id, name, title, date, createdAt: now, updatedAt: now, assemblyInfo }
    letters = [nextLetter, ...letters]
    currentLetterId = id; currentLetterName = name; currentLetterTitle = title; currentLetterDate = date
    assembledBlocks = []; rebuildOutput()
    await apiSaveLetters(currentProject, { list: letters, currentId: id })
    await apiSaveLetter(currentProject, { ...nextLetter, blocks: [] })
    setActivePage('builder')
  }

  function planRowId(row) { return `${row.project}::${row.rowId}` }

  async function setPlanRowStatus(row, newStatus) {
    if (newStatus === row.status) { planEditingRowId = null; return }
    if (!confirm(`Изменить статус «${row.subject}» на «${newStatus}»?`)) {
      planEditingRowId = null; return
    }
    const id = planRowId(row)
    planSavingRowId = id; planSaveError = ''
    try {
      const projData = await apiGetProject(row.project)
      const yc = projData.settings?.yonoteConfig
      if (!yc?.properties?.status) throw new Error('Свойство статуса не настроено')
      await apiYonoteUpdateRow(row.rowId, { [yc.properties.status]: newStatus })
      planRows = planRows.map(r => planRowId(r) === id ? { ...r, status: newStatus } : r)
      planEditingRowId = null
    } catch (e) {
      planSaveError = e?.message || 'Ошибка записи'
    }
    planSavingRowId = null
  }

  async function updateAssemblyStatus(newStatus) {
    if (!assemblyInfo || newStatus === assemblyInfo.status) return
    const row = planRows.find(r => r.project === assemblyInfo.project && r.subject === assemblyInfo.subject && r.dateStr === assemblyInfo.dateStr)
    if (row) {
      try {
        const projData = await apiGetProject(row.project)
        const yc = projData.settings?.yonoteConfig
        if (yc?.properties?.status) {
          await apiYonoteUpdateRow(row.rowId, { [yc.properties.status]: newStatus })
          planRows = planRows.map(r => planRowId(r) === planRowId(row) ? { ...r, status: newStatus } : r)
        }
      } catch {}
    }
    assemblyInfo = { ...assemblyInfo, status: newStatus }
    const s = (newStatus || '').toLowerCase()
    if (s.includes('отправлен') || s.includes('отправк') || s.includes('поставлен')) {
      statsStop(newStatus)
    }
  }

  function applyAutoTitles() {
    if (typeof document === 'undefined') return
    const elements = document.querySelectorAll('button, input, select, textarea')
    elements.forEach((el) => {
      if (el.getAttribute('title')) return
      let title = el.getAttribute('aria-label') || ''
      if (!title && el instanceof HTMLInputElement) {
        title = el.placeholder || ''
      }
      if (!title && el instanceof HTMLTextAreaElement) {
        title = el.placeholder || ''
      }
      if (!title && el instanceof HTMLSelectElement) {
        const selected = el.selectedOptions?.[0]
        title = selected?.textContent?.trim() || ''
      }
      if (!title && el instanceof HTMLButtonElement) {
        title = el.textContent?.trim() || ''
      }
      if (!title) {
        const label = el.closest('label')
        if (label) {
          title = label.textContent?.trim() || ''
        }
      }
      if (!title) return
      el.setAttribute('title', title)
    })
  }

  function setBodyTheme(value) {
    if (typeof document === 'undefined') return
    document.body.classList.toggle('theme-dark', value === 'dark')
    document.body.classList.toggle('theme-light', value === 'light')
  }

  function setAccentColor(value) {
    if (typeof document === 'undefined') return
    if (!value) return
    const accent = value.trim()
    if (!/^#([0-9a-fA-F]{6})$/.test(accent)) return
    const root = document.documentElement
    const contrast = getContrastColor(accent)
    root.style.setProperty('--project-accent', accent)
    root.style.setProperty('--project-accent-contrast', contrast)
    document.body.style.setProperty('--project-accent', accent)
    document.body.style.setProperty('--project-accent-contrast', contrast)
  }

  function getContrastColor(hex) {
    const r = parseInt(hex.slice(1, 3), 16) / 255
    const g = parseInt(hex.slice(3, 5), 16) / 255
    const b = parseInt(hex.slice(5, 7), 16) / 255
    const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
    return luminance > 0.6 ? '#0b0f12' : '#ffffff'
  }

  function getSettingsBlockName() {
    return allBlocks[settingsBlockIndex]?.name ?? ''
  }

  function updateSettingsTemplate(name, value) {
    pushHistory()
    const trimmed = value.trim()
    if (trimmed === '') {
      const current = getBlockSetting(name)
      const nextFields = current.fields
      const next = { ...settings, blocks: { ...settings.blocks, [name]: { ...current } } }
      delete next.blocks[name].template
      if (!nextFields) delete next.blocks[name].fields
      if (Object.keys(next.blocks[name]).length === 0) delete next.blocks[name]
      saveSettings(next)
    } else {
      updateSettingsBlock(name, { template: value })
    }
    const effective = getEffectiveTemplate(name)
    assembledBlocks = assembledBlocks.map((block) =>
      block.name === name
        ? {
            ...block,
            content: effective,
            schema: buildBaseSchema(effective, name, settings?.mixinRules),
          }
        : block
    )
    rebuildOutput()
    saveDraft()
  }

  function updateSettingsGlobalSpacing(value) {
    const spacing = Number(value)
    if (Number.isNaN(spacing)) return
    pushHistory()
    saveSettings({ ...settings, globalSpacing: spacing })
    rebuildOutput()
  }

  function updateSettingsBlockSpacing(name, value) {
    const spacing = Number(value)
    if (Number.isNaN(spacing)) return
    pushHistory()
    updateSettingsBlock(name, { spacing })
    rebuildOutput()
  }

  function updateSettingsField(name, signature, patch) {
    pushHistory()
    const current = getBlockSetting(name).fields ?? {}
    updateSettingsBlock(name, {
      fields: {
        ...current,
        [signature]: { ...current[signature], ...patch },
      },
    })
  }

  function toggleTemplateCard(name) {
    if (templatesExpanded.has(name)) templatesExpanded.delete(name)
    else templatesExpanded.add(name)
    templatesExpanded = templatesExpanded
  }

  function templateRows(name) {
    const lines = getEffectiveTemplate(name).split('\n').length
    return Math.max(6, Math.min(30, lines + 1))
  }

  function duplicateBlockAsCustom(sourceName) {
    const allNames = new Set([
      ...availableBlocks.map(b => b.name),
      ...(settings.customBlocks || []).map(b => b.name),
    ])
    let defaultName = `${sourceName} копия`
    let n = 2
    while (allNames.has(defaultName)) {
      defaultName = `${sourceName} копия ${n}`
      n++
    }
    const name = prompt('Название нового блока:', defaultName)
    if (!name?.trim()) return
    const trimmedName = name.trim()
    if (allNames.has(trimmedName)) { alert('Блок с таким именем уже существует'); return }
    const newBlock = { id: createId(), name: trimmedName, content: getEffectiveTemplate(sourceName) }
    pushHistory()
    saveSettings({ ...settings, customBlocks: [...(settings.customBlocks || []), newBlock] })
  }

  function renameBlock(block) {
    const newName = prompt('Новое название:', block.name)
    if (!newName?.trim() || newName.trim() === block.name) return
    const trimmedName = newName.trim()
    const allNames = new Set(allBlocks.map(b => b.name))
    allNames.delete(block.name)
    if (allNames.has(trimmedName)) { alert('Блок с таким именем уже существует'); return }
    if (block.isCustom) {
      updateCustomBlock(block.customId, { name: trimmedName })
      return
    }
    // Built-in block rename
    pushHistory()
    const oldName = block.name
    availableBlocks = availableBlocks.map(b =>
      b.name === oldName ? { ...b, name: trimmedName } : b
    )
    assembledBlocks = assembledBlocks.map(ab =>
      ab.name === oldName ? { ...ab, name: trimmedName } : ab
    )
    const nextSettings = { ...settings }
    nextSettings.blocks = { ...(nextSettings.blocks || {}) }
    if (nextSettings.blocks[oldName]) {
      nextSettings.blocks[trimmedName] = nextSettings.blocks[oldName]
      delete nextSettings.blocks[oldName]
    }
    if (Array.isArray(nextSettings.quickBlocks)) {
      nextSettings.quickBlocks = nextSettings.quickBlocks.map(n => n === oldName ? trimmedName : n)
    }
    saveSettings(nextSettings)
    // Save updated block text to server
    const blockText = availableBlocks.map(b => `//${b.name}\n${b.content}`).join('\n\n') + '\n'
    apiSaveBlock(currentProject, { sourceName, blockText })
    rebuildOutput()
    saveDraft()
  }

  function toggleAllBlocksSpacing(enabled) {
    pushHistory()
    const nextBlocks = { ...(settings.blocks || {}) }
    for (const block of allBlocks) {
      if (enabled) {
        if (nextBlocks[block.name]) {
          delete nextBlocks[block.name].spacingEnabled
          if (Object.keys(nextBlocks[block.name]).length === 0) delete nextBlocks[block.name]
        }
      } else {
        nextBlocks[block.name] = { ...(nextBlocks[block.name] || {}), spacingEnabled: false }
      }
    }
    saveSettings({ ...settings, blocks: nextBlocks })
    rebuildOutput()
  }

  function buildBlocksPugText() {
    return allBlocks.map(b => `//${b.name}\n${getEffectiveTemplate(b.name)}`).join('\n\n') + '\n'
  }

  async function exportBlocksPug() {
    const text = buildBlocksPugText()
    try {
      await apiRequest(`/project/${encodeURIComponent(currentProject)}/block-custom`, {
        method: 'PUT',
        body: JSON.stringify({ content: text }),
      })
      alert('Сохранено в block-custom.pug')
    } catch (err) {
      alert('Ошибка: ' + err.message)
    }
  }

  function downloadBlocksPug() {
    downloadFile(buildBlocksPugText(), `${currentProject || 'blocks'}.pug`)
  }

  function importBlocksPug() {
    const input = document.createElement('input')
    input.type = 'file'
    input.accept = '.pug'
    input.onchange = async () => {
      const file = input.files[0]
      if (!file) return
      const text = await file.text()
      const importedBlocks = parseBlocks(text)
      if (!importedBlocks.length) { alert('Файл не содержит блоков'); return }
      pushHistory()
      const availableNames = new Set(availableBlocks.map(b => b.name))
      const customNames = new Set((settings.customBlocks || []).map(b => b.name))
      const nextSettings = { ...settings }
      nextSettings.blocks = { ...(nextSettings.blocks || {}) }
      const nextCustom = [...(nextSettings.customBlocks || [])]
      let updated = 0
      for (const block of importedBlocks) {
        if (availableNames.has(block.name)) {
          nextSettings.blocks[block.name] = {
            ...(nextSettings.blocks[block.name] || {}),
            template: block.content,
          }
          updated++
        } else if (customNames.has(block.name)) {
          const idx = nextCustom.findIndex(cb => cb.name === block.name)
          if (idx >= 0) { nextCustom[idx] = { ...nextCustom[idx], content: block.content }; updated++ }
        } else {
          nextCustom.push({ id: createId(), name: block.name, content: block.content })
          updated++
        }
      }
      nextSettings.customBlocks = nextCustom
      saveSettings(nextSettings)
      alert(`Импортировано ${updated} из ${importedBlocks.length} блоков`)
    }
    input.click()
  }

  function insertLink(block, field) {
    const key = fieldKey(block, field)
    const input = textFieldRefs[key]
    const current = getFieldValue(block, field)
    const url = (linkDrafts[key] ?? '').trim()
    if (!url) return
    const tpl = settings.linkTemplate?.trim()
    const template = (text) => tpl
      ? tpl.replace(/\{url\}/g, url).replace(/\{text\}/g, text)
      : `<a href="${url}" target="_blank" class="bold greenText" style="text-decoration: none;">${text}</a>`
    if (!input) {
      updateField(block, field, `${current}${template('')}`)
      return
    }
    const start = input.selectionStart ?? current.length
    const end = input.selectionEnd ?? current.length
    const before = current.slice(0, start)
    const middle = current.slice(start, end)
    const after = current.slice(end)
    updateField(block, field, `${before}${template(middle)}${after}`)
  }

  function wrapCdata(text) {
    const safe = text.replace(/]]>/g, ']]]]><![CDATA[>')
    return `<![CDATA[${safe}]]>`
  }

  async function applyTypograf(block, field) {
    if (field.type !== 'text' && field.type !== 'list-item') return
    const key = fieldKey(block, field)
    typografStatus = { ...typografStatus, [key]: { loading: true, error: '' } }
    try {
      const soap = `<?xml version="1.0" encoding="utf-8"?>\n` +
        `<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ` +
        `xmlns:xsd="http://www.w3.org/2001/XMLSchema" ` +
        `xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">` +
        `<soap:Body>` +
        `<ProcessText xmlns="http://typograf.artlebedev.ru/webservices/">` +
        `<text>${wrapCdata(getFieldValue(block, field))}</text>` +
        `</ProcessText>` +
        `</soap:Body>` +
        `</soap:Envelope>`
      const res = await fetch('/typograf/webservices/typograf.asmx', {
        method: 'POST',
        headers: {
          'Content-Type': 'text/xml; charset=utf-8',
          SOAPAction: 'http://typograf.artlebedev.ru/webservices/ProcessText',
        },
        body: soap,
      })
      if (!res.ok) throw new Error('Сервис не ответил')
      const xml = await res.text()
      const doc = new DOMParser().parseFromString(xml, 'text/xml')
      const resultNode = doc.getElementsByTagName('ProcessTextResult')[0]
      if (!resultNode) throw new Error('Некорректный ответ')
      const result = resultNode.textContent ?? ''
      updateField(block, field, sanitizeTypograf(result))
      // Дополнить типограф: &nbsp; после всех коротких слов (≤3 букв)
      applyNowrapSpaces(block, field)
      typografStatus = { ...typografStatus, [key]: { loading: false, error: '' } }
    } catch (err) {
      typografStatus = {
        ...typografStatus,
        [key]: { loading: false, error: err.message || 'Ошибка типографа' },
      }
    }
  }

  function sanitizeTypograf(text) {
    let cleaned = text.replace(/\r\n/g, '\n')
    cleaned = cleaned.replace(/<br\s*\/?>/gi, '')
    cleaned = cleaned.replace(/<\/p>\s*<p>/gi, '\n')
    cleaned = cleaned.replace(/^<p>/i, '')
    cleaned = cleaned.replace(/<\/p>$/i, '')
    cleaned = cleaned.replace(/  +/g, ' ')
    return cleaned.trim()
  }

  // --- Оттипографить всё ---

  let typografAllLoading = false

  async function typografAll() {
    typografAllLoading = true
    try {
      for (const block of assembledBlocks) {
        const fields = applySchemaSettings(ensureSchema(block), block.name)
        for (const field of fields) {
          if (field.type !== 'text' && field.type !== 'list-item') continue
          await applyTypograf(block, field)
        }
      }
    } finally {
      typografAllLoading = false
    }
  }

  // --- Неразрывные пробелы (nowrap) ---
  // Дополняет типограф: ставит &nbsp; после ВСЕХ коротких слов (≤3 букв),
  // которые типограф пропустил. Идемпотентная — повторный вызов ничего не меняет.
  // Бэкенд applyNowrap потом оборачивает &nbsp; в <span style="white-space:nowrap">

  function applyNowrapSpaces(block, field) {
    if (field.type !== 'text' && field.type !== 'list-item') return
    const text = getFieldValue(block, field)
    // Заменить обычный пробел после коротких слов на &nbsp;
    // Не трогает уже существующие &nbsp; (ищет только обычный пробел)
    // (?<![...]) — negative lookbehind, потому что \b не работает с кириллицей в JS
    const result = text.replace(/(?<![a-zA-Zа-яА-ЯёЁ])([a-zA-Zа-яА-ЯёЁ]{1,3}) /gu, '$1&nbsp;')
    if (result !== text) {
      updateField(block, field, result)
    }
  }

  function handleCopy() {
    if (!outputPug.trim()) return
    navigator.clipboard?.writeText(outputPug)
  }

  function handleCopyHtml() {
    const src = rawPreviewHtml || previewHtml
    if (!src.trim()) return
    navigator.clipboard?.writeText(normalizeHtmlForCopy(src))
    statsMarkHtmlCopied()
  }

  function handleCopyHtmlRaw() {
    const src = rawPreviewHtml || previewHtml
    if (!src.trim()) return
    navigator.clipboard?.writeText(src)
    statsMarkHtmlCopied()
  }

  function downloadPug() {
    downloadFile(outputPug, 'assembled.pug')
  }

  function downloadHtml() {
    const src = rawPreviewHtml || previewHtml
    if (!src.trim()) return
    downloadFile(src, 'index.html', 'text/html;charset=utf-8')
  }


  function getEmailProjectSlug() {
    const fromSettings = String(settings.emailGenProject || '').trim()
    if (fromSettings) return fromSettings
    return String(currentProject || '')
      .trim()
      .toLowerCase()
      .replace(/\s+/g, '-')
  }


  async function renderEmailPreview() {
    if (!outputPug.trim()) {
      previewError = 'Сначала добавь блоки в сборку'
      return
    }
    const projectSlug = getEmailProjectSlug()
    if (!projectSlug) {
      previewError = 'Укажи папку проекта в email-gen в настройках'
      return
    }
    previewLoading = true
    previewError = ''
    try {
      const preheaderText = assemblyInfo?.preheader || ''
      const data = await apiRenderEmail(currentProject, { projectSlug, pug: outputPug, preheader: preheaderText, gender: currentGenderVersion || 'female' })
      rawPreviewHtml = data.html || ''
      previewHtml = data.previewHtml || rawPreviewHtml
      unavailableProducts = data.unavailableProducts || []
      feedSyncedAt = data.feedSyncedAt || null
      previewGeneratedAt = data.generatedAt || ''
      previewSourcePug = outputPug
      spellerHtml = ''
      spellErrors = []
      if (spellCheckActive) runSpellCheck()
      previewFlash = true
      setTimeout(() => { previewFlash = false }, 1500)
    } catch (error) {
      previewError = error?.message || 'Не удалось сгенерировать HTML'
    } finally {
      previewLoading = false
    }
  }

  function collectAllProductIds() {
    const ids = new Set()
    for (const block of assembledBlocks) {
      const schema = ensureSchema(block)
      const fields = applySchemaSettings(schema, block.name)
      for (const field of fields) {
        if (field.type === 'mixin-ids') {
          const lines = normalizeNewlines(block.content).split('\n')
          const line = lines[field.lineIndex] ?? ''
          const args = parseQuotedArgs(line)
          const val = args[field.argIndex ?? 0]?.value || ''
          val.split(',').map(s => s.trim()).filter(Boolean).forEach(id => ids.add(id))
        }
      }
    }
    return [...ids]
  }

  async function loadSuggestions(productId, search = '') {
    suggestForProduct = productId
    suggestLoading = true
    suggestResults = []
    try {
      const excludeIds = collectAllProductIds()
      const data = await apiFeedSuggest(currentProject, productId, excludeIds, search)
      suggestResults = data.suggestions || []
    } catch {
      suggestResults = []
    } finally {
      suggestLoading = false
    }
  }

  function applySuggestion(oldId, newId) {
    for (const block of assembledBlocks) {
      const schema = ensureSchema(block)
      const fields = applySchemaSettings(schema, block.name)
      for (const field of fields) {
        if (field.type === 'mixin-ids') {
          const lines = normalizeNewlines(block.content).split('\n')
          const line = lines[field.lineIndex] ?? ''
          const args = parseQuotedArgs(line)
          const val = args[field.argIndex ?? 0]?.value || ''
          const ids = val.split(',').map(s => s.trim())
          const idx = ids.indexOf(String(oldId))
          if (idx !== -1) {
            ids[idx] = String(newId)
            updateField(block, field, ids.join(','))
          }
        }
      }
    }
    suggestForProduct = null
    suggestResults = []
  }

  function extractTextFromBlocks() {
    const parts = []
    for (const block of assembledBlocks) {
      if (!block.content) continue
      const schema = buildBaseSchema(block.content, block.name, settings?.mixinRules)
      const lines = normalizeNewlines(block.content).split('\n')
      for (const field of schema) {
        if (field.type === 'text' || field.type === 'list-item') {
          const line = lines[field.lineIndex] ?? ''
          const meta = splitTextField(line)
          if (meta && meta.value.trim()) parts.push(meta.value)
        } else if (field.type === 'mixin-text') {
          const line = lines[field.lineIndex] ?? ''
          const args = parseQuotedArgs(line)
          const val = args[field.argIndex ?? 0]
          if (val && val.value && val.value.trim()) parts.push(val.value)
        }
      }
    }
    const raw = parts.join('\n')
    const doc = new DOMParser().parseFromString(raw, 'text/html')
    return (doc.body?.textContent || '')
      .replace(/\u00a0/g, ' ')
      .replace(/[«»„\u201C\u201D\u2018\u2019]/g, ' ')
      .replace(/\s+/g, ' ')
      .trim()
  }

  async function runSpellCheck() {
    if (!previewHtml) return
    spellLoading = true
    spellErrors = []
    spellerHtml = ''
    try {
      const text = extractTextFromHtml(previewHtml).slice(0, 9500)
      if (!text.trim()) return
      const params = new URLSearchParams({ text, lang: 'ru,en', options: '10' })
      const res = await fetch('/speller/services/spellservice.json/checkText', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: params,
      })
      if (res.ok) {
        const errors = await res.json()
        spellErrors = errors || []
        spellerHtml = injectSpellMarks(previewHtml, spellErrors)
      }
    } catch (e) {
      console.error('[spell] error:', e)
    } finally {
      spellLoading = false
    }
  }

  async function toggleSpellCheck() {
    spellCheckActive = !spellCheckActive
    if (spellCheckActive && previewHtml && !spellerHtml) {
      await runSpellCheck()
    }
  }

  function extractLinksFromHtml(html) {
    const parser = new DOMParser()
    const doc = parser.parseFromString(html, 'text/html')
    const links = new Set()
    doc.querySelectorAll('a[href]').forEach(a => {
      const href = a.getAttribute('href')
      if (href && /^https?:\/\//i.test(href)) links.add(href)
    })
    doc.querySelectorAll('img[src]').forEach(img => {
      const src = img.getAttribute('src')
      if (src && /^https?:\/\//i.test(src)) links.add(src)
    })
    return [...links]
  }

  async function runLinkCheck() {
    if (!previewHtml) return
    linkCheckLoading = true
    linkCheckResults = []
    try {
      const urls = extractLinksFromHtml(previewHtml)
      if (!urls.length) { linkCheckResults = []; return }
      const data = await apiCheckLinks(urls)
      linkCheckResults = data.results || []
    } catch (e) {
      console.error('[linkcheck]', e)
    } finally {
      linkCheckLoading = false
    }
  }

  function toggleLinkCheck() {
    linkCheckActive = !linkCheckActive
    if (linkCheckActive && previewHtml) runLinkCheck()
  }

  async function setOutputPanelMode(mode) {
    outputPanelMode = mode
    if ((mode === 'preview' || mode === 'html') && (!previewHtml || previewStale)) {
      await renderEmailPreview()
    }
  }

  function fitPreviewFrame() {
    const frame = previewIframeRef
    const doc = frame?.contentDocument
    if (!doc) return
    const viewport = frame.closest('.email-preview-viewport')
    const viewportWidth = Math.max((viewport?.clientWidth || PREVIEW_TARGET_WIDTH) - 16, 320)
    const root = doc.documentElement
    const body = doc.body
    const rawWidth = Math.max(root?.scrollWidth || 0, body?.scrollWidth || 0, PREVIEW_TARGET_WIDTH)
    const widthCandidates = Array.from(doc.querySelectorAll('[width]'))
      .map((node) => Number.parseInt(node.getAttribute('width') || '', 10))
      .filter((value) => Number.isFinite(value) && value >= 320 && value <= 900)
    const has600Layout = widthCandidates.some((value) => Math.abs(value - PREVIEW_TARGET_WIDTH) <= 4)
    const contentWidth = has600Layout ? PREVIEW_TARGET_WIDTH : Math.min(rawWidth, PREVIEW_TARGET_WIDTH)
    const contentHeight = Math.max(root?.scrollHeight || 0, body?.scrollHeight || 0, 1)
    const zoomFactor = (previewZoom || 100) / 100
    const scale = Math.min(1, viewportWidth / contentWidth) * zoomFactor
    previewContentWidth = contentWidth
    previewContentHeight = contentHeight
    previewScale = scale
    previewScaledWidth = Math.max(320, Math.round(contentWidth * scale))
  }

  function handlePreviewFrameLoad() {
    fitPreviewFrame()
    setTimeout(fitPreviewFrame, 80)
    setTimeout(fitPreviewFrame, 250)
    setTimeout(fitPreviewFrame, 700)
    setTimeout(injectPreviewClickDetection, 100)
  }

  function injectPreviewClickDetection() {
    const doc = previewIframeRef?.contentDocument
    if (!doc?.body || doc.body.dataset.vaclick) return
    doc.body.dataset.vaclick = '1'
    doc.addEventListener('click', function(e) {
      let el = e.target
      while (el && el.tagName !== 'BODY') {
        const tag = (el.tagName || '').toLowerCase()
        const text = (el.textContent || '').trim()
        if (text.length > 1 && text.length < 600 &&
            ['td','p','a','span','h1','h2','h3','h4','div'].includes(tag)) {
          const r = el.getBoundingClientRect()
          window.postMessage({
            type:'va-preview-click', text, tag,
            rect:{top:r.top,left:r.left,width:r.width,height:r.height}
          },'*')
          e.preventDefault(); return
        }
        el = el.parentElement
      }
    }, true)
  }

  function normalizeMatchText(t) {
    return (t || '').replace(/&nbsp;/g, ' ').replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim()
  }

  function handlePreviewElementClick({ text, rect }) {
    const clickedText = normalizeMatchText(text)
    if (!clickedText) return
    const editableTypes = ['text', 'list-item', 'mixin-text']
    for (const block of assembledBlocks) {
      for (const field of (block.schema || [])) {
        if (!editableTypes.includes(field.type)) continue
        const fieldText = normalizeMatchText(getFieldValue(block, field))
        if (fieldText.length < 2) continue
        if (clickedText.includes(fieldText) || fieldText.includes(clickedText)) {
          openQuickEdit(block, field, rect)
          return
        }
      }
    }
    quickEditVisible = false
  }

  function openQuickEdit(block, field, iframeRect) {
    const frameEl = previewIframeRef
    if (!frameEl) return
    quickEditBlock = block
    quickEditField = field
    quickEditValue = getFieldValue(block, field) || ''
    const fb = frameEl.getBoundingClientRect()
    const sc = previewScale
    const PW = 340, PH = 160, M = 8
    let top = fb.top + (iframeRect.top + iframeRect.height) * sc + M
    let left = fb.left + iframeRect.left * sc
    if (left + PW > window.innerWidth - M) left = window.innerWidth - PW - M
    if (left < M) left = M
    if (top + PH > window.innerHeight - M) top = fb.top + iframeRect.top * sc - PH - M
    if (top < M) top = M
    quickEditPos = { top, left }
    quickEditVisible = true
  }

  function closeQuickEdit() {
    quickEditVisible = false
    quickEditBlock = null
    quickEditField = null
  }

  function handleQuickEditInput(e) {
    quickEditValue = e.target.value
    if (quickEditBlock && quickEditField) updateField(quickEditBlock, quickEditField, quickEditValue)
  }

  function handleQuickEditKeydown(e) {
    if (e.key === 'Escape') { closeQuickEdit(); return }
    if (e.key === 'Enter' && e.shiftKey) {
      e.preventDefault()
      const ta = e.target
      const start = ta.selectionStart
      const end = ta.selectionEnd
      quickEditValue = quickEditValue.slice(0, start) + '<br>' + quickEditValue.slice(end)
      if (quickEditBlock && quickEditField) updateField(quickEditBlock, quickEditField, quickEditValue)
      setTimeout(() => { ta.selectionStart = ta.selectionEnd = start + 4 }, 0)
    }
  }

  function insertBrAtCursor(e, block, field) {
    if (e.key !== 'Enter' || !e.shiftKey) return
    e.preventDefault()
    const el = e.target
    const start = el.selectionStart
    const end = el.selectionEnd
    const current = getFieldValue(block, field)
    updateField(block, field, current.slice(0, start) + '<br>' + current.slice(end))
    setTimeout(() => { el.selectionStart = el.selectionEnd = start + 4 }, 0)
  }

  function clearAll() {
    assembledBlocks = []
    rebuildOutput()
    saveDraft()
  }

  function savePresets() {
    apiSavePresets(currentProject, presets)
  }

  function getUniquePresetName(base) {
    const existing = new Set(presets.map((preset) => preset.name))
    if (!existing.has(base)) return base
    let counter = 2
    let next = `${base} (${counter})`
    while (existing.has(next)) {
      counter += 1
      next = `${base} (${counter})`
    }
    return next
  }

  function savePreset() {
    const name = presetName.trim()
    if (!name) return
    const existingIndex = presets.findIndex((preset) => preset.name === name)
    const shouldUpdate =
      existingIndex !== -1 &&
      typeof window !== 'undefined' &&
      window.confirm('Пресет с таким названием уже есть. Обновить его? Нажмите Отмена, чтобы создать новый.')
    const preset = {
      id: createId(),
      name,
      savedAt: new Date().toISOString(),
      blocks: assembledBlocks.map((block) => ({
        name: block.name,
        content: block.content,
        collapsed: true,
        schema: ensureSchema(block),
        addSpacing: block.addSpacing ?? true,
        spacing: typeof block.spacing === 'number' ? block.spacing : undefined,
      })),
    }
    if (existingIndex !== -1 && shouldUpdate) {
      const updated = { ...presets[existingIndex], ...preset, id: presets[existingIndex].id, name }
      presets = [updated, ...presets.filter((_, index) => index !== existingIndex)]
    } else {
      const newPreset = { ...preset, name: getUniquePresetName(name) }
      presets = [newPreset, ...presets]
    }
    presetName = ''
    showPresetInput = false
    savePresets()
  }

  function openPresetModal(letter) {
    presetSourceLetter = letter || null
    presetNameDraft = ''
    presetError = ''
    showPresetModal = true
  }

  async function createPresetFromLetter() {
    const name = presetNameDraft.trim()
    if (!name) {
      presetError = 'Укажи название пресета.'
      return
    }
    const baseLetter = presetSourceLetter
    if (!baseLetter?.id) {
      presetError = 'Не найдено письмо.'
      return
    }
    let blocks = []
    try {
      const data = await apiGetLetter(currentProject, baseLetter.id)
      blocks = normalizeDraftBlocks(data?.letter?.blocks || [])
    } catch {
      presetError = 'Не удалось загрузить письмо.'
      return
    }
    const preset = {
      id: createId(),
      name,
      savedAt: new Date().toISOString(),
      blocks,
    }
    presets = [preset, ...presets]
    showPresetModal = false
    presetNameDraft = ''
    presetSourceLetter = null
    savePresets()
  }

  function applyPresetBlocks(preset) {
    if (!requireLetter()) return
    const normalizedPreset = normalizeDraftBlocks(preset.blocks || [])
    assembledBlocks = mapDraftBlocks(normalizedPreset)
    rebuildOutput()
    saveDraft()
    setActivePage('builder')
  }

  function deletePresetById(id) {
    presets = presets.filter((preset) => preset.id !== id)
    savePresets()
  }

  function applyPreset(preset) {
    const normalizedPreset = normalizeDraftBlocks(preset.blocks || [])
    assembledBlocks = normalizedPreset.map((block) => ({
      id: createId(),
      name: block.name,
      content: block.content,
      collapsed: true,
      schema: buildBaseSchema(block.content, block.name, settings?.mixinRules),
      addSpacing: block.addSpacing ?? true,
      spacing: typeof block.spacing === 'number' ? block.spacing : undefined,
    }))
    presetName = preset.name || ''
    showPresetInput = true
    rebuildOutput()
    saveDraft()
    setActivePage('builder')
  }

  function formatPresetDate(value) {
    if (!value) return ''
    const date = new Date(value)
    if (Number.isNaN(date.getTime())) return value
    return date.toLocaleString('ru-RU', {
      day: '2-digit',
      month: '2-digit',
      year: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
    })
  }

  function getFilteredPresets() {
    const query = presetSearch.trim().toLowerCase()
    let list = [...presets]
    if (query) {
      list = list.filter((preset) => preset.name.toLowerCase().includes(query))
    }
    list.sort((a, b) => {
      const timeA = new Date(a.savedAt || 0).getTime()
      const timeB = new Date(b.savedAt || 0).getTime()
      return presetSort === 'asc' ? timeA - timeB : timeB - timeA
    })
    return list
  }

  function buildDraftPayload() {
    return assembledBlocks.map((block) => ({
      name: block.name,
      content: block.content,
      collapsed: block.collapsed,
      schema: ensureSchema(block),
      addSpacing: block.addSpacing ?? true,
      spacing: typeof block.spacing === 'number' ? block.spacing : undefined,
      segment: block.segment || 'common',
    }))
  }

  function saveDraft() {
    if (certEditingMode) return  // не сохранять письмо во время редактирования сертификата
    isDirty = true
    saveErrorMsg = ''
    if (saveTimer) clearTimeout(saveTimer)
    saveTimer = setTimeout(flushSave, 600)
  }

  async function flushSave() {
    if (isSaving) {
      pendingSave = true
      return
    }
    isSaving = true
    try {
      const payload = buildDraftPayload()
      if (currentLetterId) {
        const now = new Date().toISOString()
        const existingIndex = letters.findIndex((letter) => letter.id === currentLetterId)
        const nextLetter = {
          id: currentLetterId,
          name: currentLetterName || buildLetterName(currentLetterTitle, currentLetterDate),
          title: currentLetterTitle,
          date: currentLetterDate,
          createdAt: existingIndex !== -1 ? letters[existingIndex].createdAt : now,
          updatedAt: now,
          ...(assemblyInfo ? { assemblyInfo } : {}),
        }
        const nextList =
          existingIndex === -1
            ? [nextLetter, ...letters]
            : letters.map((letter) => (letter.id === currentLetterId ? nextLetter : letter))
        letters = nextList
        await apiSaveLetters(currentProject, { list: nextList, currentId: currentLetterId })
        await apiSaveLetter(currentProject, { ...nextLetter, blocks: payload, certificateEnabled })
      } else {
        await apiSaveDraft(currentProject, payload)
      }
      isDirty = false
      saveErrorMsg = ''
    } catch (e) {
      saveErrorMsg = e?.message || 'Ошибка сохранения'
    } finally {
      isSaving = false
      if (pendingSave) {
        pendingSave = false
        flushSave()
      }
    }
  }

  function handleGlobalKeydown(event) {
    const mod = event.ctrlKey || event.metaKey
    if (!mod) return
    if (event.key === 's') {
      event.preventDefault()
      flushSave()
    } else if (event.key === 'g') {
      event.preventDefault()
      renderEmailPreview()
    } else if (event.key === 'z') {
      const tag = event.target?.tagName
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
      event.preventDefault()
      undoLast()
    }
  }

</script>

<svelte:window on:keydown={handleGlobalKeydown} />

{#if !authChecked}
  <div class="login-page"><div class="login-loading">Загрузка...</div></div>
{:else if !currentUser}
  <div class="login-page" style="background-image: url('{loginBg}')">
    <form class="login-card" on:submit|preventDefault={handleLogin}>
      <div class="login-title">Aspekter VA</div>
      <input
        class="field-control"
        type="text"
        placeholder="Логин"
        bind:value={loginForm.login}
        autocomplete="username"
      />
      <input
        class="field-control"
        type="password"
        placeholder="Пароль"
        bind:value={loginForm.password}
        autocomplete="current-password"
      />
      {#if loginError}<div class="field-error">{loginError}</div>{/if}
      <button class="btn btn-primary login-btn" type="submit" disabled={loginLoading}>
        {loginLoading ? 'Вход...' : 'Войти'}
      </button>
    </form>
  </div>
{:else}

<div class="page">
  <aside class="control-panel panel-sidebar">
    <div class="brand">
      <button class="brand-button" on:click={() => activePage = 'builder'}>
        {#if settings.logoData}
          <img class="brand-logo" src={settings.logoData} alt={settings.projectTitle || currentProject} />
        {:else}
          <p class="brand-mark">{settings.projectTitle || currentProject}</p>
        {/if}
        <p class="brand-sub">ASPEKTER</p>
      </button>
    </div>

    <div class="sidebar-group">
      <div class="sidebar-title-row">
        <button class="sidebar-title sidebar-title-accent sidebar-collapse-btn" on:click={() => presetsCollapsed = !presetsCollapsed}>
          Пресеты {presets.length ? `(${presets.length})` : ''} <span class="collapse-arrow" class:collapsed={presetsCollapsed}>▾</span>
        </button>
        {#if currentLetterId}
          <button
            class="btn ghost sidebar-title-btn"
            on:click={() => openPresetModal(letters.find((l) => l.id === currentLetterId))}
            title="Сохранить текущее письмо как пресет"
          >+</button>
        {/if}
      </div>
      {#if !presetsCollapsed && presets.length > 0}
        <div class="preset-quick-list">
          {#each presets as preset}
            <button class="btn preset-pill-btn" on:click={() => applyPresetBlocks(preset)}>
              {preset.name}
            </button>
          {/each}
        </div>
        <div class="sidebar-separator"></div>
      {/if}
      <div class="sidebar-title sidebar-title-accent">Добавить блок</div>
      <div class="add-block">
        <div class="block-dropdown" bind:this={blockDropdownRef} on:click|stopPropagation>
          <button class="field-control block-dropdown-trigger"
            on:click={() => { blockDropdownOpen = !blockDropdownOpen; blockDropdownSearch = '' }}>
            Выбери блок ▾
          </button>
          {#if blockDropdownOpen}
            <div class="block-dropdown-menu">
              <input class="block-dropdown-search field-control" type="text" placeholder="Поиск..."
                bind:value={blockDropdownSearch} on:keydown|stopPropagation />
              <div class="block-dropdown-list">
                {#each allBlocks as block, index}
                  {@const matches = !blockDropdownSearch.trim() || block.name.toLowerCase().includes(blockDropdownSearch.toLowerCase())}
                  {#if matches}
                    <div class="block-dropdown-item" on:click={() => selectBlockFromDropdown(index)}>
                      <div class="block-dropdown-item-header">
                        <span class="block-dropdown-item-name">{block.name}</span>
                        <span class="block-type-tag">{getBlockTypeTag(block)}</span>
                      </div>
                    </div>
                  {/if}
                {/each}
              </div>
            </div>
          {/if}
        </div>
      </div>
      <div class="sidebar-separator"></div>
      {#if activePage === 'builder'}
        <div class="gender-controls sidebar-gender-controls">
          <button
            class="btn gender-btn"
            class:active={currentGenderVersion === 'female'}
            on:click={() => assembleGenderVersion('female')}
            title="Женский хедер/футер"
          >Жен</button>
          <button
            class="btn gender-btn"
            class:active={currentGenderVersion === 'male'}
            on:click={() => assembleGenderVersion('male')}
            title="Мужской хедер/футер"
          >Муж</button>
          <button
            class="btn gender-flip-btn"
            on:click={() => flipSegmentOrder()}
            title="Поменять порядок сегментов (Ж↔М)"
          >⇅</button>
          <button
            class="btn"
            class:toggled={idPoolOpen}
            on:click={() => { idPoolOpen = !idPoolOpen }}
            title="Пул ID"
          >ID</button>
          {#if certBlocks.length > 0}
            <button
              class="btn gender-btn"
              class:active={certificateEnabled}
              on:click={() => toggleCertificate(!certificateEnabled)}
              title="Сертификат"
            >Серт</button>
          {/if}
        </div>
        <div class="sidebar-separator"></div>
      {/if}
      <div class="sidebar-note">
        <!-- svelte-ignore a11y-click-events-have-key-events -->
        <div class="sidebar-title sidebar-title-accent clickable" on:click={() => sidebarNoteCollapsed = !sidebarNoteCollapsed}>
          Заметки {sidebarNoteCollapsed ? '▸' : '▾'}
        </div>
        {#if !sidebarNoteCollapsed}
          <textarea
            class="field-control sidebar-note-textarea"
            placeholder="Заметка проекта"
            value={settings.sidebarNote || ''}
            bind:this={sidebarNoteTextareaRef}
            on:input={(event) => {
              const value = event.target.value
              saveSettings({ ...settings, sidebarNote: value })
              autoResizeTextarea(event.target)
            }}
          ></textarea>
        {/if}
      </div>
      {#if assemblyInfo && !assemblyCardHidden}
        <div class="sidebar-separator"></div>
        <div class="acard">
          <div class="acard-header">
            <span class="acard-badge">{assemblyInfo.status || 'сборка'}</span>
            <button class="acard-close" on:click={() => assemblyCardHidden = true} title="Скрыть">&times;</button>
          </div>
          <div class="acard-subject">📮 {assemblyInfo.subject}</div>
          {#if assemblyInfo.preheader}
            <div class="acard-preheader">{assemblyInfo.preheader}</div>
          {/if}
          {#if (assemblyInfo.extra || []).some(e => !e.link && !isUrl(e.value))}
            <div class="acard-tags">
              {#each (assemblyInfo.extra || []).filter(e => !e.link && !isUrl(e.value)) as ext}
                <span class="acard-tag" title={ext.label}>{ext.value}</span>
              {/each}
            </div>
          {/if}
          {#if (assemblyInfo.extra || []).some(e => e.link || isUrl(e.value))}
            <div class="acard-links">
              {#each (assemblyInfo.extra || []).filter(e => e.link || isUrl(e.value)) as ext}
                <a class="acard-link" href={ext.link || ext.value} rel="noopener"
                  on:click|preventDefault={(e) => window.open(ext.link || ext.value, '_blank', `width=700,height=1000,left=${screen.width - 720},top=50`)}>
                  <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M6.5 3.5H3.5C2.948 3.5 2.5 3.948 2.5 4.5v8c0 .552.448 1 1 1h8c.552 0 1-.448 1-1V9.5M9.5 2.5h4v4M7.5 8.5l6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
                  {ext.label}
                </a>
              {/each}
            </div>
          {/if}
        </div>
        <div class="acard-actions">
          {#if settings.mailingService?.url}
            <button class="btn acard-mailing-btn"
              on:click={() => window.open(settings.mailingService?.url, '_blank', 'width=1200,height=800')}>
              {settings.mailingService?.label || 'Создать рассылку'}
            </button>
          {/if}
          {#if !yonoteCfgSentStatus || assemblyInfo?.status !== yonoteCfgSentStatus}
            <button class="btn acard-sent-btn" on:click={() => updateAssemblyStatus(yonoteCfgSentStatus || 'отправлено')}>{yonoteCfgSentStatus || 'Отправлено'}</button>
          {/if}
        </div>
      {/if}
      {#if acardPlanRows.length > 0}
        {#if !assemblyInfo || assemblyCardHidden}<div class="sidebar-separator"></div>{/if}
        <div class="acard-plan">
          <!-- svelte-ignore a11y-click-events-have-key-events -->
          <div class="acard-plan-title" on:click={() => acardCalCollapsed = !acardCalCollapsed}>
            Календарь {acardCalCollapsed ? '▸' : '▾'}
          </div>
          {#if !acardCalCollapsed}
            <div class="acard-cal">
              <div class="acard-cal-nav">
                <button class="acard-cal-arrow" on:click={acardCalPrev}>‹</button>
                <span class="acard-cal-month">{acardCalMonth.toLocaleString('ru-RU', { month: 'long', year: 'numeric' })}</span>
                <button class="acard-cal-arrow" on:click={acardCalNext}>›</button>
              </div>
              <div class="acard-cal-grid">
                {#each ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'] as dow}
                  <span class="acard-cal-dow">{dow}</span>
                {/each}
                {#each acardCalCells as cell}
                  {#if cell === null}
                    <span class="acard-cal-empty"></span>
                  {:else}
                    <!-- svelte-ignore a11y-click-events-have-key-events -->
                    <span
                      class="acard-cal-day"
                      class:acard-cal-today={cell.isToday}
                      class:acard-cal-has={cell.rows.length > 0}
                      class:acard-cal-clickable={cell.rows.length > 0}
                      title={cell.rows.map(r => r.subject + (r.status ? ` (${r.status})` : '')).join('\n')}
                      on:click={() => { if (cell.rows.length === 1) startAssemble(cell.rows[0]) }}
                    >
                      {cell.d}
                      {#if cell.rows.length > 0}
                        <span class="acard-cal-dots">
                          {#each cell.rows as r}
                            <span class="acard-cal-dot plan-dot-{planStatusClass(r.status)}" class:acard-cal-dot-past={cell.isPast && planStatusClass(r.status) === 'done'}></span>
                          {/each}
                        </span>
                      {/if}
                    </span>
                  {/if}
                {/each}
              </div>
            </div>
          {/if}
          <!-- svelte-ignore a11y-click-events-have-key-events -->
          <div class="acard-plan-title" style="margin-top: 8px" on:click={() => acardListCollapsed = !acardListCollapsed}>
            Список {acardListCollapsed ? '▸' : '▾'}
          </div>
          {#if !acardListCollapsed}
            {#each acardPlanRows.filter(r => { const today = new Date(); today.setHours(0,0,0,0); return !(r.date < today && planStatusClass(r.status) === 'done') }) as row}
              <!-- svelte-ignore a11y-click-events-have-key-events -->
              <div class="acard-plan-row" class:acard-plan-overdue={row.date < new Date(new Date().setHours(0,0,0,0)) && planStatusClass(row.status) !== 'done'} on:click={() => startAssemble(row)}>
                <span class="acard-plan-date">{row.dateStr}</span>
                <div class="acard-plan-dot plan-dot-{planStatusClass(row.status)}"></div>
                <span class="acard-plan-subject">{row.subject}</span>
              </div>
            {/each}
          {/if}
        </div>
      {/if}
    </div>
  </aside>

  <div class="workspace">
    <div class="workspace-header">
      <div class="workspace-header-left">
        <div class="workspace-meta-row">
          {#if activePage !== 'builder'}
            <button class="btn" on:click={() => setActivePage('builder')} title="Главная">
              Главная
            </button>
          {/if}
          <button
            class="btn icon-square"
            on:click={() => openLetterModal('new')}
            aria-label="Новое письмо"
            title="Новое письмо"
          >
            <img class="icon-img" src={newMailIcon} alt="" />
          </button>
          <button
            class="btn icon-square"
            on:click={() => (currentLetterId ? persistCurrentLetter() : openLetterModal('new'))}
            aria-label="Сохранить письмо"
            title="Сохранить письмо"
          >
            <img class="icon-img" src={saveIcon} alt="" />
          </button>
          <span
            class={`autosave-dot ${isDirty || isSaving ? 'active' : ''} ${saveErrorMsg ? 'error' : ''}`}
            title={saveErrorMsg || (isDirty || isSaving ? 'Сохранение...' : 'Все изменения сохранены')}
            aria-hidden="true"
          ></span>
          {#if saveErrorMsg}
            <span class="save-error-label" title={saveErrorMsg}>
              Ошибка сохранения
              <button type="button" class="save-error-dismiss" on:click={() => (saveErrorMsg = '')}>×</button>
            </span>
          {/if}
          <button
            class="btn icon-square"
            on:click={() => setActivePage('presets')}
            aria-label="Пресеты"
            title="Пресеты"
          >
            <img class="icon-img" src={filesIcon} alt="" />
          </button>
          {#if activePage === 'builder'}
            <button
              class="btn icon-square"
              on:click={undoLast}
              aria-label="Отменить последнее действие"
              title="Отменить последнее действие"
            >
              <img class="icon-img" src={backIcon} alt="" />
            </button>
            <button
              class="btn icon-square"
              on:click={() => setAllCollapsed(assembledBlocks.some((block) => !block.collapsed))}
              aria-label={assembledBlocks.some((block) => !block.collapsed) ? 'Свернуть все' : 'Развернуть все'}
              title={assembledBlocks.some((block) => !block.collapsed) ? 'Свернуть все' : 'Развернуть все'}
            >
              <img class="icon-img" src={stackIcon} alt="" />
            </button>
            <button class="btn icon-square" on:click={clearAll} aria-label="Сброс" title="Сброс">
              <img class="icon-img" src={clearIcon} alt="" />
            </button>
            {#if productOptionsList.length > 0}
              <div class="product-options-bar">
                {#each productOptionsList as option}
                  <button
                    class={`btn icon-square btn-textual ${isOptionEnabledInAnyBlock(option) ? 'active' : ''} ${isOptionEnabledInAnyBlock(option) && !isOptionEnabledInAllBlocks(option) ? 'partial' : ''}`}
                    aria-pressed={isOptionEnabledInAnyBlock(option)}
                    data-state={isOptionEnabledInAnyBlock(option)
                      ? isOptionEnabledInAllBlocks(option)
                        ? 'on'
                        : 'partial'
                      : 'off'}
                    on:click={() => toggleProductOptionForAll(option)}
                    title={`Опция для всех товаров: ${option.label}`}
                  >
                    {option.label}
                  </button>
                {/each}
              </div>
            {/if}
          {/if}
        </div>
        {#if currentLetterName}
          <div class="letter-title" title={currentLetterName}>
            {#if isEditingLetterDate}
              <input
                class="letter-date-input"
                type="date"
                bind:this={letterDateInput}
                bind:value={letterDateDraft}
                on:blur={commitEditLetterDate}
                on:keydown={(event) => {
                  if (event.key === 'Enter') commitEditLetterDate()
                  if (event.key === 'Escape') cancelEditLetterDate()
                }}
              />
            {:else}
              <span class="letter-date" on:click={startEditLetterDate}>
                {formatLetterDate(currentLetterDate)}
              </span>
            {/if}
            <span class="letter-gap"> </span>
            {#if isEditingLetterName}
              <input
                class="letter-title-input"
                bind:this={letterNameInput}
                bind:value={letterNameDraft}
                size={Math.max(12, letterNameDraft.length)}
                on:blur={commitEditLetterName}
                on:keydown={(event) => {
                  if (event.key === 'Enter') commitEditLetterName()
                  if (event.key === 'Escape') cancelEditLetterName()
                }}
              />
            {:else}
              <span class="letter-name" on:click={startEditLetterName}>
                {currentLetterTitle}
              </span>
            {/if}
          </div>
        {/if}
      </div>
      <div class="workspace-header-actions">
        <button
          class={`btn icon-square ${activePage === 'settings' ? 'toggled' : ''}`}
          on:click={() => { setActivePage('settings'); loadPartsFiles() }}
          title="Настройки"
          aria-label="Настройки"
        >
          <img class="icon-img" src={settingsIcon} alt="" />
        </button>
        <button
          class={`btn ${activePage === 'plan' ? 'toggled' : ''}`}
          on:click={() => setActivePage('plan')}
          title="План"
          aria-label="План"
        >
          План
        </button>
        <button
          class={`btn ${activePage === 'stats' ? 'toggled' : ''}`}
          on:click={() => { setActivePage('stats'); loadAllStats() }}
          title="Статистика"
          aria-label="Статистика"
        >
          Стат
        </button>
        <div class="theme-toggle">
          <label class="theme-switch">
            <input
              type="checkbox"
              checked={theme === 'dark'}
              on:change={(event) => setTheme(event.target.checked ? 'dark' : 'light')}
            />
            <span class="theme-slider"></span>
          </label>
        </div>
      </div>
    </div>

    {#if activePage === 'builder'}
      {#if loadError}
        <div class="error">{loadError}</div>
      {/if}

      <div class="workspace-grid builder-grid" class:preview-focus={outputPanelMode === 'preview'}>
        <div class="panel panel-left">
          {#if idPoolOpen}
            <div class="id-pool-panel">
              <div class="id-pool-header">
                <div>
                  <span class="id-pool-title">Пул ID</span>
                  {#if idPoolQueue.length}
                    <span class="id-pool-count">{idPoolQueue.filter(e => e.usedBy).length}/{idPoolQueue.length}</span>
                  {/if}
                </div>
                <div class="id-pool-actions">
                  <button class="btn btn-sm" on:click={distributeIds} disabled={!idPoolQueue.length}>Распределить</button>
                  {#if assemblyInfo}
                    <button class="btn btn-sm ghost" on:click={refreshIdsFromYonote} disabled={idPoolRefreshing} title="Загрузить ID из Yonote">{idPoolRefreshing ? '⟳' : '↻'}</button>
                  {/if}
                  <button class="btn btn-sm ghost" on:click={() => { idPoolOpen = false }}>×</button>
                </div>
              </div>
              <textarea class="id-pool-textarea" bind:value={idPoolInput} placeholder="Вставьте ID через запятую или с новой строки"></textarea>
              {#if idPoolQueue.length}
                <div class="id-pool-chips">
                  {#each idPoolQueue as entry}
                    <span class="id-chip" class:used={!!entry.usedBy}>{entry.value}</span>
                  {/each}
                </div>
              {/if}
            </div>
          {/if}
          {#if certEditingMode}
            <div class="cert-editor-banner">
              <span>Редактирование сертификата</span>
              <div class="cert-editor-actions">
                <button class="btn btn-primary" on:click={saveCertEditor}>Сохранить сертификат</button>
                <button class="btn ghost" on:click={cancelCertEditor}>Отмена</button>
              </div>
            </div>
          {/if}
          {#if assembledBlocks.length === 0}
            <div class="empty">Добавь блоки слева, чтобы начать сборку.</div>
            {#if !certEditingMode}
              <div class="qb-inline">
                {#each quickBlocksView as quickName, i}
                  <button
                    class="btn qb-inline-btn"
                    class:qb-drag-over={qbDragOverIndex === i}
                    class:qb-dragging={qbDragIndex === i}
                    draggable="true"
                    style={qbButtonStyle(quickName)}
                    on:click={() => addQuickBlockByName(quickName)}
                    on:dragstart|stopPropagation={() => qbDragStart(i)}
                    on:dragover|preventDefault|stopPropagation={() => qbDragOver(i)}
                    on:dragend={() => qbDragEnd()}
                    title={quickName}
                  >
                    {@html getQuickBlockIcon(quickName)}
                  </button>
                {/each}
                <button class="btn qb-add-btn" on:click={() => { blockDropdownOpen = !blockDropdownOpen; blockDropdownSearch = '' }}>+ Добавить блок</button>
              </div>
            {/if}
          {:else}
            <div class="blocks-toolbar">
              <button
                class="btn"
                on:click={typografAll}
                disabled={typografAllLoading || assembledBlocks.length === 0}
                title="Типограф + неразрывные пробелы для всех текстовых полей"
              >{typografAllLoading ? 'Типограф...' : 'Типограф'}</button>
            </div>
            <div class="blocks-container" bind:this={blocksScrollRef}>
              {#each assembledBlocks as block, index (block.id)}
                <div
                  class={`block-card ${block.collapsed ? 'collapsed' : ''} ${dragOverBlockIndex === index && dragBlockIndex !== index ? 'block-drag-over' : ''} ${dragBlockIndex === index ? 'block-dragging' : ''} segment-${block.segment || 'common'}`}
                  data-block-id={block.id}
                  draggable="true"
                  on:dragstart={(e) => handleBlockDragStart(e, index)}
                  on:dragover={(e) => handleBlockDragOver(e, index)}
                  on:drop={(e) => handleBlockDrop(e, index)}
                  on:dragend={handleBlockDragEnd}
                >
                  <div class="block-header">
                  <div
                    class="block-title clickable"
                    role="button"
                    tabindex="0"
                    on:click={() => { toggleBlock(block.id); setActiveBlock(block.id, true) }}
                    on:keydown={(event) => event.key === 'Enter' && toggleBlock(block.id)}
                  >
                    <span class="drag-handle" title="Перетащить">⠿</span>
                    <span class="block-num">{String(index + 1).padStart(2, '0')}</span>
                    {block.name}
                  </div>
                    <div class="block-actions">
                      <div class="segment-toggle">
                        <button class="seg-btn" class:active={block.segment === 'common'} on:click={() => setBlockSegment(block.id, 'common')} title="Общий">О</button>
                        <button class="seg-btn seg-f" class:active={block.segment === 'female'} on:click={() => setBlockSegment(block.id, 'female')} title="Женский">Ж</button>
                        <button class="seg-btn seg-m" class:active={block.segment === 'male'} on:click={() => setBlockSegment(block.id, 'male')} title="Мужской">М</button>
                        {#if block.name === 'Разделитель'}
                          <button class="seg-btn seg-axis" class:active={block.swapCenter} on:click={() => setBlockAxis(block.id)} title="Ось переворота сегментов">⇅</button>
                        {/if}
                      </div>
                      <button class="btn icon-square" on:click={() => moveBlock(index, -1)} aria-label="Поднять блок">▲</button>
                      <button class="btn icon-square" on:click={() => moveBlock(index, 1)} aria-label="Опустить блок">▼</button>
                      <button class="btn icon-square danger" on:click={() => removeBlock(block.id)} aria-label="Удалить блок">×</button>
                    </div>
                  </div>

                  {#if !block.collapsed}
                    {@const _parsed = parseSections(block.content)}
                    {@const _allFields = applySchemaSettings(ensureSchema(block), block.name)}
                    {@const _secHeaders = buildSectionHeaders(_allFields, _parsed)}
                    <div class="block-body">
                      {#each _allFields as field, fieldIndex (fieldKey(block, field))}
                        {#if _secHeaders.has(fieldIndex) && _secHeaders.get(fieldIndex).si > 0}
                          <div class="block-section-divider"></div>
                        {/if}
                        {@const key = fieldKey(block, field)}
                        {@const _sh = _secHeaders.get(fieldIndex)}
                        {@const _prevSectionLabel = _sh ? _sh.section.label : ''}
                        <div>
                          <div class="field-head">
                            {#if (field.type !== 'text' && field.type !== 'list-item') && (!_prevSectionLabel || field.label.toLowerCase() !== _prevSectionLabel.toLowerCase())}
                              <div class="field-label">
                                {field.label}
                              </div>
                            {/if}
                            {#if field.type === 'text' || field.type === 'list-item'}
                              <div class="editor-actions">
                                <button
                                  class="btn icon-square"
                                  on:click={() => applyTypograf(block, field)}
                                  disabled={typografStatus[key]?.loading}
                                  title="Типограф"
                                >
                                  {typografStatus[key]?.loading ? '...' : 'T'}
                                </button>
                                <button class="btn icon-square" on:click={() => applyNowrapSpaces(block, field)} title="Неразрывные пробелы (nowrap)">↵</button>
                                {#if block.name === 'Текст' && fieldIndex === 0}
                                  <button class="btn icon-square btn-textual" on:click={() => addTextParagraph(block)}>
                                    Абзац
                                  </button>
                                {/if}
                                {#if block.name === 'Текст' && field.removable}
                                  <button class="btn icon-square danger" on:click={() => removeTextParagraph(block, field)} aria-label="Удалить абзац">
                                    ×
                                  </button>
                                {/if}
                                {#if isListBlock(block.name) && fieldIndex === 0}
                                  <button class="btn icon-square btn-textual" on:click={() => addListItem(block)}>
                                    Пункт
                                  </button>
                                {/if}
                                {#if isListBlock(block.name) && field.removable}
                                  <button class="btn icon-square danger" on:click={() => removeListItem(block, field)} aria-label="Удалить пункт">
                                    ×
                                  </button>
                                {/if}
                                <button class="btn icon-square" on:click={() => wrapBold(block, field)}>Ж</button>
                                <button class="btn icon-square" on:click={() => insertBullet(block, field)}>•</button>
                                <button class="btn icon-square" on:click={() => insertBr(block, field)} title="Вставить перенос строки">br</button>
                                <span class="link-group">
                                  <button class="btn icon-square link-btn" on:click={() => insertLink(block, field)}>A</button>
                                  <input
                                    class="field-control link-input"
                                    type="text"
                                    placeholder="URL"
                                    value={linkDrafts[key] ?? ''}
                                    on:input={(event) => updateLinkDraft(key, event.target.value)}
                                  />
                                </span>
                                <button class={`btn icon-square ${previewOpen[key] ? 'active' : ''}`} on:click={() => togglePreview(key)} aria-label="Предпросмотр">
                                  <svg class="icon-eye" viewBox="0 0 24 24" aria-hidden="true">
                                    <path
                                      d="M2.5 12s3.8-6 9.5-6 9.5 6 9.5 6-3.8 6-9.5 6-9.5-6-9.5-6Z"
                                      fill="none"
                                      stroke="currentColor"
                                      stroke-width="1.5"
                                    />
                                    <circle
                                      cx="12"
                                      cy="12"
                                      r="3"
                                      fill="none"
                                      stroke="currentColor"
                                      stroke-width="1.5"
                                    />
                                  </svg>
                                </button>
                                {#if _sh}
                                  <span class="section-actions-inline">
                                    <button class="btn icon-square" disabled={_sh.si === 0} on:click={() => moveSectionInBlock(block.id, _sh.si, -1)} title="Переместить вверх">↑</button>
                                    <button class="btn icon-square" disabled={_sh.si === _sh.total - 1} on:click={() => moveSectionInBlock(block.id, _sh.si, 1)} title="Переместить вниз">↓</button>
                                    <button class="btn icon-square danger" on:click={() => removeSectionFromBlock(block.id, _sh.si)} title="Удалить секцию">×</button>
                                  </span>
                                {/if}
                              </div>
                            {/if}
                          </div>
                        {#if field.type === 'raw'}
                          <textarea
                            class="singleline-textarea"
                            rows="6"
                            value={getFieldValue(block, field)}
                            on:input={(event) => updateField(block, field, event.target.value)}
                            use:autosize
                            on:focus={() => setActiveBlock(block.id)}
                          ></textarea>
                        {:else if isImageField(field)}
                          <div class="image-upload-row"
                            class:image-drag-over={dragOverField === key}
                            on:dragover|preventDefault={(e) => handleImageDragOver(e, block, field)}
                            on:dragleave={handleImageDragLeave}
                            on:drop|preventDefault={(e) => handleImageDrop(e, block, field)}>
                            <input
                              type="text"
                              value={getFieldValue(block, field)}
                              on:input={(event) => updateField(block, field, event.target.value)}
                              on:focus={() => setActiveBlock(block.id)}
                              placeholder="https://..."
                            />
                            {#if settings.ftpConfig?.host}
                              <button class="btn icon-square image-upload-btn" title="FTP: загрузить / выбрать"
                                on:click={() => openFtpGallery(block, field)}>
                                <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/><line x1="12" y1="11" x2="12" y2="17"/><polyline points="9 14 12 11 15 14"/></svg>
                              </button>
                            {/if}
                            {#if uploadingFields[key]?.loading}<span class="image-upload-status">Загрузка…</span>{/if}
                          </div>
                          {#if uploadingFields[key]?.error}<p class="field-error">{uploadingFields[key].error}</p>{/if}
                          {#if looksLikeImageUrl(getFieldValue(block, field))}
                            <img class="image-preview-thumb" src={getFieldValue(block, field)} alt=""
                              on:error={(e) => { e.target.style.display = 'none' }}
                              on:load={(e) => { e.target.style.display = 'block' }} />
                          {/if}
                        {:else if field.type === 'mixin-ids'}
                          <input
                            class="ids-input"
                            type="text"
                            value={getFieldValue(block, field)}
                            on:input={(event) => updateField(block, field, event.target.value)}
                            on:focus={() => setActiveBlock(block.id)}
                            placeholder="ID через запятую"
                          />
                        {:else if getFieldValue(block, field).length > 80 || field.type === 'text'}
                          <textarea
                            rows="2"
                            value={getFieldValue(block, field)}
                            on:input={(event) => updateField(block, field, event.target.value)}
                            on:keydown={(e) => insertBrAtCursor(e, block, field)}
                            use:textRef={key}
                            use:autosize
                            on:focus={() => setActiveBlock(block.id)}
                          ></textarea>
                        {:else}
                          <input
                            type="text"
                            value={getFieldValue(block, field)}
                            on:input={(event) => updateField(block, field, event.target.value)}
                            on:keydown={(e) => { if (field.type === 'mixin-text' || field.type === 'text' || field.type === 'list-item') insertBrAtCursor(e, block, field) }}
                            on:focus={() => setActiveBlock(block.id)}
                          />
                        {/if}
                        {#if (field.type === 'text' || field.type === 'list-item') && previewOpen[key]}
                          <div class="text-preview">
                            <div class="text-preview-frame">
                              {@html getFieldValue(block, field)}
                            </div>
                          </div>
                        {/if}
                        {#if (field.type === 'text' || field.type === 'list-item') && getFieldValue(block, field).includes('\n')}
                          <div class="linebreak-preview">
                            <span class="linebreak-label">Переносы строк:</span>
                            <div class="linebreak-content">
                              {@html visualizeLineBreaks(getFieldValue(block, field))}
                            </div>
                          </div>
                        {/if}
                        {#if typografStatus[key]?.error}
                          <p class="field-error">{typografStatus[key]?.error}</p>
                        {/if}
                        </div>
                      {/each}
                    </div>

                    <div class="block-footer">
                      {#if isProductBlock(block.content)}
                        <div class="product-options-inline">
                          {#each productOptionsList as option}
                            <button
                              class={`btn icon-square btn-textual ${getProductOptionState(block)[parseOptionCode(option.code)?.key] === parseOptionCode(option.code)?.value ? 'active' : ''}`}
                              on:click={() => toggleProductOptionForBlock(block, option)}
                              title={`Опция товаров: ${option.label}`}
                            >
                              {option.label}
                            </button>
                          {/each}
                        </div>
                      {/if}
                      <div class={`spacing-control ${block.addSpacing ? 'active' : ''}`}>
                        <button
                          type="button"
                          class="spacing-toggle"
                          on:click={() => updateBlockSpacing(block.id, !block.addSpacing)}
                          aria-pressed={block.addSpacing}
                        >
                          <img class="icon-img" src={spacerIcon} alt="" />
                        </button>
                        <input
                          class="spacing-value"
                          type="text"
                          inputmode="numeric"
                          pattern="[0-9]*"
                          maxlength="3"
                          value={getSpacingForBlock(block)}
                          on:input={(event) => updateBlockSpacingValue(block.id, event.target.value)}
                          disabled={!block.addSpacing}
                        />
                      </div>
                      <button class="btn icon-square" on:click={() => togglePug(block.id)} aria-label="PUG" title="PUG">
                        <img class="icon-img" src={pugIcon} alt="" />
                      </button>
                      <button class="btn icon-square" on:click={() => toggleAddMenu(block.id)} aria-label="Добавить блок после" title="Добавить блок после">
                        <span class="icon-svg accent" aria-hidden="true">
                          {@html plusIconSvg}
                        </span>
                      </button>
                      {#if addMenuFor === block.id}
                        <select
                          class="field-control add-inline"
                          data-add-menu={block.id}
                          on:change={(event) => handleAddAfter(index, event)}
                        >
                          <option value="" disabled selected>Выбери блок</option>
                          {#each allBlocks as blockOption, blockOptionIdx}
                            {#if !blockOption.isCustom}
                              <option value={blockOptionIdx}>{blockOption.name}</option>
                            {/if}
                          {/each}
                          {#if allBlocks.some(b => b.isCustom)}
                            <optgroup label="Пользовательские">
                              {#each allBlocks as blockOption, blockOptionIdx}
                                {#if blockOption.isCustom}
                                  <option value={blockOptionIdx}>{blockOption.name}</option>
                                {/if}
                              {/each}
                            </optgroup>
                          {/if}
                        </select>
                      {/if}
                    </div>
                    {#if pugOpen[block.id]}
                    <textarea
                      class="code"
                      rows="16"
                      value={block.content}
                      on:input={(event) => updateBlockContent(block.id, event.target.value)}
                      on:focus={() => setActiveBlock(block.id)}
                    ></textarea>
                    {/if}
                  {/if}
                </div>
              {/each}
              {#if !certEditingMode}
                <div class="qb-inline">
                  {#each quickBlocksView as quickName, i}
                    <button
                      class="btn qb-inline-btn"
                      class:qb-drag-over={qbDragOverIndex === i}
                      class:qb-dragging={qbDragIndex === i}
                      draggable="true"
                      style={qbButtonStyle(quickName)}
                      on:click={() => addQuickBlockByName(quickName)}
                      on:dragstart|stopPropagation={() => qbDragStart(i)}
                      on:dragover|preventDefault|stopPropagation={() => qbDragOver(i)}
                      on:dragend={() => qbDragEnd()}
                      title={quickName}
                    >
                      {@html getQuickBlockIcon(quickName)}
                    </button>
                  {/each}
                  <button class="btn qb-add-btn" on:click={() => { blockDropdownOpen = !blockDropdownOpen; blockDropdownSearch = '' }}>+ Добавить блок</button>
                </div>
              {/if}
            </div>
          {/if}
        </div>

        <div class="panel panel-right">
          <div class="code-panel-header">
            <div>
              <div class="code-panel-title">
                {outputPanelMode === 'pug' ? 'PUG' : outputPanelMode === 'html' ? 'HTML' : 'Предпросмотр'}
              </div>
              <div class="code-panel-subtitle">
                {outputPanelMode === 'pug'
                  ? 'Итоговый файл'
                  : outputPanelMode === 'html'
                    ? 'Сгенерированный index.html'
                    : previewGeneratedAt
                      ? new Date(previewGeneratedAt).toLocaleString('ru-RU')
                      : 'Готовое письмо после генерации'}
              </div>
            </div>
            <div class="code-panel-actions">
              <button
                class={`btn ${outputPanelMode === 'pug' ? 'toggled' : ''}`}
                on:click={() => setOutputPanelMode('pug')}
              >
                PUG
              </button>
              <button
                class={`btn ${outputPanelMode === 'preview' ? 'toggled' : ''}`}
                on:click={() => setOutputPanelMode('preview')}
              >
                Превью
              </button>
              <button
                class={`btn ${outputPanelMode === 'html' ? 'toggled' : ''}`}
                on:click={() => setOutputPanelMode('html')}
              >
                HTML
              </button>
              <span class="actions-divider"></span>
              {#if outputPanelMode === 'pug'}
                <button id="copy-btn" class="btn btn-primary" on:click={handleCopy} title="Скопировать PUG">⧉</button>
                <button id="export-btn" class="btn btn-primary" on:click={downloadPug} title="Скачать PUG">↓</button>
              {:else if outputPanelMode === 'html'}
                <button class="btn btn-primary" on:click={renderEmailPreview} disabled={previewLoading} title="Сгенерировать HTML">
                  {previewLoading ? '⏳' : '🔄'}
                </button>
                <button class="btn btn-primary" on:click={handleCopyHtmlRaw} disabled={!previewHtml.trim()} title="Скопировать HTML как есть">⧉</button>
                <button class="btn btn-primary" on:click={handleCopyHtml} disabled={!previewHtml.trim()} title="Скопировать очищенный HTML">✦</button>
                <button class="btn btn-primary" on:click={downloadHtml} disabled={!previewHtml.trim()} title="Скачать HTML">↓</button>
              {:else}
                <button class="btn btn-primary" on:click={renderEmailPreview} disabled={previewLoading} title="Сгенерировать HTML">
                  {previewLoading ? '⏳' : '🔄'}
                </button>
                {#if previewHtml}
                  <button class="btn btn-primary" on:click={handleCopyHtml} title="Скопировать очищенный HTML">✦</button>
                  <button
                    class={`btn ${spellCheckActive ? 'toggled' : ''}`}
                    on:click={toggleSpellCheck}
                    disabled={spellLoading}
                    title="Подсветить орфографические ошибки"
                  >
                    {spellLoading ? '...' : spellCheckActive && spellErrors.length ? `АБВ (${spellErrors.length})` : 'АБВ'}
                  </button>
                  <button
                    class={`btn ${linkCheckActive ? 'toggled' : ''}`}
                    on:click={toggleLinkCheck}
                    disabled={linkCheckLoading}
                    title="Проверить ссылки и изображения"
                  >
                    {linkCheckLoading ? '...' : linkCheckActive && linkCheckResults.filter(r => !r.ok).length ? `🔗 (${linkCheckResults.filter(r => !r.ok).length})` : '🔗'}
                  </button>
                {/if}
              {/if}
            </div>
          </div>
          {#if linkCheckActive && linkCheckResults.length}
            <div class="link-check-panel">
              {#each linkCheckResults as result}
                <div class="link-check-item" class:link-ok={result.ok} class:link-error={!result.ok}>
                  <span class="link-status">{result.status || '???'}</span>
                  <a href={result.url} target="_blank" rel="noopener" class="link-url" title={result.url}>
                    {result.url.length > 60 ? result.url.slice(0, 60) + '...' : result.url}
                  </a>
                  {#if result.redirected}<span class="link-redirect" title={result.finalUrl}>→ редирект</span>{/if}
                  {#if result.error}<span class="link-error-msg">{result.error}</span>{/if}
                </div>
              {/each}
            </div>
          {/if}
          {#if outputPanelMode === 'pug'}
            <textarea id="pug-output" readonly value={outputPug} bind:this={pugOutputRef}></textarea>
          {:else if outputPanelMode === 'html'}
            <textarea id="html-output" readonly value={rawPreviewHtml || previewHtml}></textarea>
          {:else}
            <div class="preview-panel">
              {#if previewError}
                <div class="field-error">{previewError}</div>
              {/if}
              {#if unavailableProducts.length > 0}
                <div class="preview-warning">
                  <div class="unavailable-header" on:click={() => unavailableCollapsed = !unavailableCollapsed} role="button" tabindex="0" on:keydown={(e) => e.key === 'Enter' && (unavailableCollapsed = !unavailableCollapsed)}>
                    <span>⚠ Нет в наличии ({unavailableProducts.length}){#if feedSyncedAt} · фид {new Date(feedSyncedAt).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}{/if}</span>
                    <span class="unavailable-toggle">{unavailableCollapsed ? '▸' : '▾'}</span>
                  </div>
                  {#if !unavailableCollapsed}
                    {#each unavailableProducts as prod}
                      <div class="unavailable-item">
                        <span class="unavailable-name">{prod.name || ''}&nbsp;<span class="unavailable-meta">ID {prod.id}{#if prod.price}&nbsp;·&nbsp;{Number(prod.price).toLocaleString('ru-RU')} руб.{/if}</span></span>
                        <button class="btn btn-xs" on:click={() => loadSuggestions(prod.id)}>Замена</button>
                      </div>
                    {/each}
                  {/if}
                </div>
                {#if suggestForProduct}
                  <div class="suggest-panel">
                    <div class="suggest-header">
                      Замены для ID {suggestForProduct}
                      <button class="btn btn-xs" on:click={() => { suggestForProduct = null; suggestResults = []; suggestSearch = '' }}>×</button>
                    </div>
                    <div class="suggest-search">
                      <input
                        type="text"
                        placeholder="Поиск по названию или ID..."
                        bind:value={suggestSearch}
                        on:keydown={(e) => e.key === 'Enter' && loadSuggestions(suggestForProduct, suggestSearch)}
                      />
                      <button class="btn btn-xs" on:click={() => loadSuggestions(suggestForProduct, suggestSearch)}>Найти</button>
                    </div>
                    {#if suggestLoading}
                      <div class="suggest-loading">Загрузка...</div>
                    {:else if suggestResults.length === 0}
                      <div class="suggest-empty">Подходящих замен не найдено</div>
                    {:else}
                      {#each suggestResults as s}
                        <div class="suggest-item" on:click={() => applySuggestion(suggestForProduct, s.id)} role="button" tabindex="0" on:keydown={(e) => e.key === 'Enter' && applySuggestion(suggestForProduct, s.id)}
                          on:mouseleave={() => suggestHoverItem = null}>
                          {#if s.image}<img src={s.image} alt="" class="suggest-img"
                            on:mouseenter={(e) => { suggestHoverItem = s; suggestHoverPos = { x: e.clientX, y: e.clientY } }}
                            on:mousemove={(e) => { suggestHoverPos = { x: e.clientX, y: e.clientY } }}
                          />{/if}
                          <div class="suggest-info">
                            <div class="suggest-name">{s.name}</div>
                            <div class="suggest-price">{s.price ? `${Number(s.price).toLocaleString('ru-RU')} руб.` : ''}</div>
                          </div>
                          <div class="suggest-id">ID {s.id}</div>
                        </div>
                      {/each}
                    {/if}
                  </div>
                {/if}
              {/if}
              {#if previewStale && previewHtml}
                <div class="preview-meta">Изменения в PUG не сгенерированы, нажми 🔄.</div>
              {/if}
              {#if previewHtml}
                <div class="email-preview-viewport" class:preview-flash={previewFlash}>
                  <div class="preview-zoom-overlay">
                    <input type="range" min="40" max="100" step="5" bind:value={previewZoom}
                      on:input={() => { try { localStorage.setItem('va-previewZoom', previewZoom) } catch {} fitPreviewFrame(); if (currentUser) apiSavePreferences({ previewZoom }).catch(() => {}) }}
                      title={`Масштаб ${previewZoom}%`} />
                    <span>{previewZoom}%</span>
                  </div>
                  <div
                    class="email-preview-scale-shell"
                    style={`width:${previewScaledWidth}px;height:${previewScaledHeight}px;`}
                  >
                    <iframe
                      bind:this={previewIframeRef}
                      on:load={handlePreviewFrameLoad}
                      class="email-preview-frame"
                      title="Превью письма"
                      sandbox="allow-same-origin"
                      srcdoc={spellCheckActive && spellerHtml ? spellerHtml : previewHtml}
                      style={`width:${previewContentWidth}px;height:${previewContentHeight}px;transform:scale(${previewScale});`}
                    ></iframe>
                  </div>
                </div>
              {:else if !previewLoading && !previewError}
                <div class="preview-empty">Нажми 🔄, чтобы собрать и увидеть письмо.</div>
              {/if}
            </div>
          {/if}
        </div>
      </div>
    {:else if activePage === 'settings'}
      <div class="panel-heading">
        <div>
          <div class="panel-title">Настройки</div>
          <div class="panel-subtitle">Проект, блоки и интеграции</div>
        </div>
      </div>

      <div class="panel-heading">
        <div class="tabs-actions">
          <button class={`btn ${settingsTab === 'project' ? 'toggled' : ''}`} on:click={() => (settingsTab = 'project')}>
            Проект
          </button>
          <button class={`btn ${settingsTab === 'blocks' ? 'toggled' : ''}`} on:click={() => (settingsTab = 'blocks')}>
            Блоки
          </button>
          <button class={`btn ${settingsTab === 'templates' ? 'toggled' : ''}`} on:click={() => (settingsTab = 'templates')}>
            Шаблоны
          </button>
          <button class={`btn ${settingsTab === 'parts' ? 'toggled' : ''}`} on:click={() => { settingsTab = 'parts' }}>
            Хедер / Футер
          </button>
          <button class={`btn ${settingsTab === 'integrations' ? 'toggled' : ''}`} on:click={() => (settingsTab = 'integrations')}>
            Интеграции
          </button>
          <button class={`btn ${settingsTab === 'certificate' ? 'toggled' : ''}`} on:click={() => (settingsTab = 'certificate')}>
            Сертификат
          </button>
          {#if currentUser?.role === 'admin'}
            <button class={`btn ${settingsTab === 'users' ? 'toggled' : ''}`} on:click={() => { settingsTab = 'users'; loadAdminUsers() }}>
              Пользователи
            </button>
          {/if}
        </div>
      </div>

      <div class="workspace-grid settings-grid">
        <div class="panel panel-left">
          {#if settingsTab === 'project'}
            <div class="settings-two-col">
              <div class="settings-col">
                <div class="settings-section">
                  <div class="settings-section-title">Текущий проект</div>
                  <div class="settings-section-desc">Настрой отображение и цвета для активного проекта.</div>
                  <label>
                    Логотип проекта
                    <div class="inline">
                      <label class="btn folder-btn">
                        Загрузить логотип
                        <input type="file" accept="image/*" on:change={handleLogoUpload} />
                      </label>
                      {#if settings.logoData}
                        <button type="button" class="btn ghost" on:click={removeLogo}>Удалить</button>
                      {/if}
                    </div>
                    <div class="hint">PNG, JPG или WEBP. Логотип хранится в настройках проекта.</div>
                  </label>
                  <label>
                    Название проекта в интерфейсе
                    <input
                      type="text"
                      class="field-control"
                      placeholder={currentProject}
                      value={settings.projectTitle || ''}
                      on:input={(event) => updateProjectTitle(event.target.value)}
                    />
                  </label>
                  <label>
                    Папка проекта в email-gen
                    <input
                      type="text"
                      class="field-control"
                      placeholder="Например: numizmat"
                      value={settings.emailGenProject || ''}
                      on:input={(event) => updateEmailGenProject(event.target.value)}
                    />
                    <div class="hint">Используется для генерации HTML-превью из email-gen.</div>
                  </label>
                  <label>
                    Цвет акцентов
                    <div class="inline">
                      <input
                        type="color"
                        class="color-input"
                        value={settings.accentColor || '#6b7280'}
                        on:input={(event) => updateAccentColor(event.target.value)}
                      />
                      <input
                        type="text"
                        class="field-control"
                        value={settings.accentColor || '#6b7280'}
                        on:input={(event) => updateAccentColor(event.target.value)}
                      />
                    </div>
                  </label>
                  <div class="settings-section-title" style="margin-top: 16px">Стиль гиперссылок</div>
                  <div class="settings-section-desc">Шаблон для кнопки «A» в текстовых полях. Используй <code>{`{url}`}</code> и <code>{`{text}`}</code> как плейсхолдеры.</div>
                  <label>
                    <textarea
                      class="field-control"
                      rows="3"
                      style="font-family: monospace; font-size: 11px"
                      placeholder={`<a href="{url}" target="_blank" class="bold greenText" style="text-decoration: none;">{text}</a>`}
                      value={settings.linkTemplate || ''}
                      on:input={(e) => saveSettings({ ...settings, linkTemplate: e.target.value })}
                    ></textarea>
                  </label>
                  <div class="hint">Пусто = стиль по умолчанию. Пример: <code>{`<a href="{url}" style="color: #AAC8C8; font-weight: bold;">{text}</a>`}</code></div>
                </div>
                <div class="settings-section">
                  <div class="settings-section-title">Источник блоков</div>
                  <div class="settings-section-desc">Загрузи файл block.pug для текущего проекта.</div>
                  <div class="inline">
                    <label class="btn folder-btn">
                      Загрузить block.pug
                      <input type="file" accept=".pug" on:change={handleFile} />
                    </label>
                    <button class="btn ghost" on:click={() => loadFromUrl(DEFAULT_SOURCE, 'Block.pug')}>
                      Перезагрузить
                    </button>
                  </div>
                  <div class="hint">Текущий файл: {sourceName}</div>
                </div>
              </div>
              <div class="settings-col">
                <div class="settings-section">
                  <div class="settings-section-title">Отступы</div>
                  <div class="settings-section-desc">Глобальный отступ применяется ко всем блокам, если не задано иначе.</div>
                  <label>
                    Глобальный отступ после блока
                    <input
                      type="number"
                      class="field-control"
                      value={settings.globalSpacing}
                      on:input={(event) => updateSettingsGlobalSpacing(event.target.value)}
                    />
                  </label>
                  <div class="hint">
                    Отступы не добавляются автоматически после блоков &laquo;Отступ 20&raquo; и &laquo;Отступ 40&raquo;.
                  </div>
                </div>
                <div class="settings-section">
                  <div class="settings-section-title">Быстрые блоки</div>
                  <div class="settings-section-desc">Настрой список кнопок быстрого добавления в конструкторе.</div>
                  <div class="inline">
                    <select
                      class="field-control"
                      bind:value={quickBlockCandidate}
                      bind:this={quickBlocksSelectRef}
                      on:change={(event) => (quickBlockCandidate = event.target.value)}
                    >
                      <option value="">Выбери блок</option>
                      {#each allBlocks as block}
                        {#if !block.isCustom}
                          <option value={block.name}>{block.name}</option>
                        {/if}
                      {/each}
                      {#if allBlocks.some(b => b.isCustom)}
                        <optgroup label="Пользовательские">
                          {#each allBlocks as block}
                            {#if block.isCustom}
                              <option value={block.name}>{block.name}</option>
                            {/if}
                          {/each}
                        </optgroup>
                      {/if}
                    </select>
                    <button
                      type="button"
                      class="btn"
                      on:click|preventDefault={() => addQuickBlockFromSettings(quickBlocksSelectRef?.value)}
                    >
                      Добавить
                    </button>
                  </div>
                  {#if quickBlocksError}
                    <div class="field-error">{quickBlocksError}</div>
                  {:else if quickBlocksStatus}
                    <div class="field-success">{quickBlocksStatus}</div>
                  {/if}
                  <div class="quick-blocks-list">
                    {#each quickBlocksView as quickName, i}
                      <span
                        class="quick-block-pill"
                        class:qb-drag-over={qbDragOverIndex === i}
                        class:qb-dragging={qbDragIndex === i}
                        draggable="true"
                        on:dragstart|stopPropagation={() => qbDragStart(i)}
                        on:dragover|preventDefault|stopPropagation={() => qbDragOver(i)}
                        on:dragend={() => qbDragEnd()}
                      >
                        <span class="qb-drag-handle">⠿</span>
                        {quickName}
                        {#each QB_COLORS as c}
                          <button
                            class="qb-color-dot"
                            class:active={(quickBlockColors[quickName] || null) === c.value}
                            style={c.value ? `background:${c.value}` : ''}
                            title={c.label}
                            on:click|preventDefault={() => setQuickBlockColor(quickName, c.value)}
                          ></button>
                        {/each}
                        <button
                          class="btn icon-square danger"
                          on:click|preventDefault={() => removeQuickBlockFromSettings(quickName)}
                          aria-label="Удалить быстрый блок"
                        >
                          ×
                        </button>
                      </span>
                    {/each}
                  </div>
                </div>
              </div>
            </div>
            <div class="settings-section">
              <div class="settings-section-title">Аккаунт</div>
              <div class="settings-section-desc">{currentUser?.name || currentUser?.login} ({currentUser?.role})</div>
              <button class="btn ghost danger" on:click={handleLogout}>Выйти из аккаунта</button>
            </div>
          {:else if settingsTab === 'blocks'}
            <div class="settings-section">
              <div class="settings-section-title">Поля блока</div>
              <div class="settings-section-desc">Выбери блок и настрой, какие поля показывать и как их подписывать.</div>
              <label>
                Блок
                <select class="field-control" bind:value={settingsBlockIndex}>
                  {#each allBlocks as block, index}
                    {#if !block.isCustom}
                      <option value={index}>{block.name}</option>
                    {/if}
                  {/each}
                  {#if allBlocks.some(b => b.isCustom)}
                    <optgroup label="Пользовательские">
                      {#each allBlocks as block, index}
                        {#if block.isCustom}
                          <option value={index}>{block.name}</option>
                        {/if}
                      {/each}
                    </optgroup>
                  {/if}
                </select>
              </label>
              <div class="block-body">
                {#if baseSchema.length === 0}
                  <p class="muted">Поля не найдены для текущего шаблона.</p>
                {:else}
                  {#each baseSchema as field, fieldIndex (fieldIndex)}
                    {@const sig = fieldSignature(field)}
                    <div class="inline">
                      <label>
                        <input
                          type="checkbox"
                          checked={getBlockSetting(settingsBlockName).fields?.[sig]?.enabled ?? true}
                          on:change={(event) =>
                            updateSettingsField(settingsBlockName, sig, { enabled: event.target.checked })
                          }
                        />
                        Показывать поле
                      </label>
                      <input
                        type="text"
                        class="field-control"
                        value={getBlockSetting(settingsBlockName).fields?.[sig]?.label ?? field.label}
                        on:input={(event) =>
                          updateSettingsField(settingsBlockName, sig, { label: event.target.value })
                        }
                      />
                    </div>
                  {/each}
                {/if}
              </div>
            </div>

            <div class="settings-section">
              <div class="settings-section-title">Опции товаров</div>
              <div class="settings-section-desc">Добавь опции, которые будут доступны в товарных блоках и в шапке.</div>
              <div class="product-options-settings">
                {#each settings.productOptions || [] as option, index}
                  <div class="product-option-row">
                    <input
                      class="field-control"
                      type="text"
                      placeholder="Название (например, ЦЕНА)"
                      value={option.label || ''}
                      on:input={(event) => updateProductOption(index, { label: event.target.value })}
                    />
                    <input
                      class="field-control"
                      type="text"
                      placeholder="Код (например, showPrices: true)"
                      value={option.code || ''}
                      on:input={(event) => updateProductOption(index, { code: event.target.value })}
                    />
                    <label class="product-option-default" title="По умолчанию включена при добавлении блока">
                      <input
                        type="checkbox"
                        checked={!!option.defaultEnabled}
                        on:change={(e) => updateProductOption(index, { defaultEnabled: e.target.checked })}
                      />
                      <span>по умолч.</span>
                    </label>
                    <button class="btn icon-square danger" on:click={() => removeProductOption(index)} aria-label="Удалить опцию">
                      ×
                    </button>
                  </div>
                {/each}
                <button class="btn ghost" on:click={addProductOption}>Добавить опцию</button>
              </div>
            </div>

            <div class="settings-section">
              <div class="settings-section-title">Правила mixin-полей</div>
              <div class="settings-section-desc">Задай, какие аргументы mixin редактируются в конструкторе.</div>
              <div class="product-options-settings">
                {#each settings.mixinRules || [] as rule, index}
                  <div class="product-option-row">
                    <input
                      class="field-control"
                      type="text"
                      placeholder="Имя mixin (без +)"
                      value={rule.mixin || ''}
                      on:input={(event) => updateMixinRule(index, { mixin: event.target.value })}
                    />
                    <input
                      class="field-control tiny"
                      type="number"
                      min="1"
                      max="9"
                      value={(Number(rule.argIndex) || 0) + 1}
                      on:input={(event) => updateMixinRule(index, { argIndex: Number(event.target.value || 1) - 1 })}
                    />
                    <select
                      class="field-control"
                      value={rule.type || 'mixin-text'}
                      on:change={(event) => updateMixinRule(index, { type: event.target.value })}
                    >
                      <option value="mixin-text">Текст</option>
                      <option value="mixin-href">Ссылка</option>
                      <option value="mixin-ids">ID товаров</option>
                      <option value="mixin-opts">Опции (raw)</option>
                    </select>
                    <input
                      class="field-control"
                      type="text"
                      placeholder="Подпись поля (необязательно)"
                      value={rule.label || ''}
                      on:input={(event) => updateMixinRule(index, { label: event.target.value })}
                    />
                    <button class="btn icon-square danger" on:click={() => removeMixinRule(index)} aria-label="Удалить правило">
                      ×
                    </button>
                  </div>
                {/each}
                <button class="btn ghost" on:click={addMixinRule}>Добавить правило</button>
              </div>
            </div>

            <div class="settings-section">
              <div class="settings-section-title">Пользовательские блоки</div>
              <div class="settings-section-desc">Создай свои блоки из Pug-кода. Поля определяются автоматически.</div>
              {#each settings.customBlocks || [] as cb (cb.id)}
                <div class="block-card" style="margin-bottom:8px">
                  <div class="block-header">
                    {#if editingCustomBlockId === cb.id}
                      <input
                        type="text"
                        class="field-control"
                        value={cb.name}
                        on:change={(e) => updateCustomBlock(cb.id, { name: e.target.value })}
                      />
                    {:else}
                      <div class="block-title">{cb.name}</div>
                    {/if}
                    <div class="inline" style="gap:4px">
                      <button
                        class="btn icon-square"
                        title={editingCustomBlockId === cb.id ? 'Готово' : 'Редактировать'}
                        on:click={() => editingCustomBlockId = editingCustomBlockId === cb.id ? null : cb.id}
                      >
                        {editingCustomBlockId === cb.id ? '✓' : '✎'}
                      </button>
                      <button class="btn icon-square danger" title="Удалить блок" on:click={() => removeCustomBlock(cb.id)}>×</button>
                    </div>
                  </div>
                  {#if editingCustomBlockId === cb.id}
                    <textarea
                      class="code"
                      rows="6"
                      value={cb.content}
                      on:input={(e) => updateCustomBlock(cb.id, { content: e.target.value })}
                    ></textarea>
                    {@const previewSchema = buildBaseSchema(cb.content, cb.name, settings?.mixinRules)}
                    {#if previewSchema.length > 0}
                      <div style="margin-top:8px">
                        <div class="muted" style="margin-bottom:4px">Полей: {previewSchema.length}</div>
                        {#each previewSchema as field, fi (fi)}
                          {@const sig = fieldSignature(field)}
                          <div class="inline" style="margin-bottom:2px">
                            <label>
                              <input
                                type="checkbox"
                                checked={getBlockSetting(cb.name).fields?.[sig]?.enabled ?? true}
                                on:change={(e) => updateSettingsField(cb.name, sig, { enabled: e.target.checked })}
                              />
                            </label>
                            <input
                              type="text"
                              class="field-control"
                              value={getBlockSetting(cb.name).fields?.[sig]?.label ?? field.label}
                              on:input={(e) => updateSettingsField(cb.name, sig, { label: e.target.value })}
                            />
                          </div>
                        {/each}
                      </div>
                    {:else}
                      <div class="muted" style="margin-top:4px">Поля не найдены</div>
                    {/if}
                  {/if}
                </div>
              {/each}
              <div style="margin-top:8px">
                <input
                  type="text"
                  class="field-control"
                  placeholder="Название блока"
                  bind:value={customBlockName}
                  style="margin-bottom:4px"
                />
                <textarea
                  class="code"
                  rows="4"
                  placeholder="Pug-код блока"
                  bind:value={customBlockContent}
                ></textarea>
                {#if customBlockError}
                  <div class="field-error">{customBlockError}</div>
                {/if}
                <button class="btn ghost" style="margin-top:4px" on:click={addCustomBlock}>Добавить блок</button>
              </div>
            </div>
          {:else if settingsTab === 'templates'}
            <div class="inline" style="margin-bottom:8px; gap:6px;">
              <label style="white-space:nowrap">
                <input
                  type="checkbox"
                  checked={allSpacingEnabled}
                  on:change={(e) => toggleAllBlocksSpacing(e.target.checked)}
                />
                Отступ после всех блоков
              </label>
              <div style="flex:1"></div>
              <button class="btn ghost" on:click={importBlocksPug}>Импорт .pug</button>
              <button class="btn ghost" on:click={downloadBlocksPug}>Скачать .pug</button>
              <button class="btn ghost" on:click={exportBlocksPug}>Сохранить .pug</button>
            </div>
            {#if allBlocks.length > 8}
              <input class="search-input" type="text" placeholder="Поиск блоков…" bind:value={templateSearch} />
            {/if}
            <div class="blocks-container">
              {#each filteredTemplateBlocks as block, index}
                <div class="block-card" class:collapsed={!templatesExpanded.has(block.name)}>
                  <div class="block-header">
                    <!-- svelte-ignore a11y-click-events-have-key-events -->
                    <div class="block-title clickable" on:click={() => toggleTemplateCard(block.name)}>
                      {index + 1}. {block.name}
                      {#if getBlockSetting(block.name).template}
                        <span class="template-override-dot" title="Шаблон изменён"></span>
                      {/if}
                      {#if block.isCustom}
                        <span class="template-custom-badge">свой</span>
                      {/if}
                    </div>
                  </div>
                  {#if templatesExpanded.has(block.name)}
                    <textarea
                      class="code"
                      rows={templateRows(block.name)}
                      value={getEffectiveTemplate(block.name)}
                      on:input={(event) => updateSettingsTemplate(block.name, event.target.value)}
                    ></textarea>
                    <div class="inline" style="margin-top:4px">
                      <label>
                        <input
                          type="checkbox"
                          checked={getBlockSetting(block.name).spacingEnabled ?? true}
                          on:change={(event) =>
                            updateSettingsBlock(block.name, { spacingEnabled: event.target.checked })
                          }
                        />
                        Отступ
                      </label>
                      {#if getBlockSetting(block.name).spacingEnabled ?? true}
                        <input
                          type="number"
                          class="field-control"
                          style="width:60px"
                          value={getBlockSetting(block.name).spacing ?? settings.globalSpacing}
                          on:input={(event) => updateSettingsBlockSpacing(block.name, event.target.value)}
                        />
                      {/if}
                      {#if getBlockSetting(block.name).template}
                        <button class="btn ghost" on:click={() => updateSettingsTemplate(block.name, '')}>Сбросить</button>
                      {/if}
                      <button class="btn ghost" on:click={() => duplicateBlockAsCustom(block.name)}>Копировать</button>
                      <button class="btn ghost" on:click={() => renameBlock(block)}>Переименовать</button>
                    </div>
                  {/if}
                </div>
              {/each}
              {#if filteredTemplateBlocks.length === 0}
                <div class="empty">Ничего не найдено</div>
              {/if}
            </div>
          {:else if settingsTab === 'parts'}
            <div class="settings-section">
              <div class="settings-section-title">Редактор хедера / футера</div>
              <div class="settings-section-desc">Редактируй тексты кнопок и ссылки в pug-файлах хедера и футера.</div>
              <label>Файл
                <select class="field-control" bind:value={partsEditorFile} on:change={() => loadPartsEditorFile(partsEditorFile)}>
                  <option value="">— выбери файл —</option>
                  {#each [
                    { label: 'Хедер (Ж)', val: genderHeaderFemale },
                    { label: 'Хедер (М)', val: genderHeaderMale },
                    { label: 'Футер (Ж)', val: genderFooterFemale },
                    { label: 'Футер (М)', val: genderFooterMale },
                  ].filter(o => o.val) as opt}
                    <option value={opt.val}>{opt.label} — {opt.val}</option>
                  {/each}
                </select>
              </label>
              {#if partsEditorLoading}
                <div class="muted" style="margin-top:8px">Загрузка...</div>
              {:else if partsEditorFields.length > 0}
                <div class="parts-editor-fields">
                  {#each ['button', 'link', 'html-link', 'html-text'] as groupType}
                    {#if partsEditorFields.some(f => f.type === groupType)}
                      <div class="parts-editor-group-title">
                        {groupType === 'button' ? 'Кнопки' : groupType === 'html-text' ? 'Тексты' : 'Ссылки'}
                      </div>
                      {#each partsEditorFields as field, i}
                        {#if field.type === groupType}
                          <div class="parts-editor-row{field.type === 'html-text' ? ' single' : ''}">
                            {#if field.type === 'html-text'}
                              <input class="field-control" type="text" placeholder="Текст" bind:value={partsEditorFields[i].text} />
                            {:else}
                              <input class="field-control" type="text" placeholder="Текст" bind:value={partsEditorFields[i].text} />
                              <input class="field-control" type="text" placeholder="Ссылка" bind:value={partsEditorFields[i].url} />
                            {/if}
                          </div>
                        {/if}
                      {/each}
                    {/if}
                  {/each}
                </div>
                <div class="inline" style="gap:8px;margin-top:10px">
                  <button class="btn btn-primary" on:click={savePartsEditorFile} disabled={partsEditorSaving}>
                    {partsEditorSaving ? 'Сохранение...' : 'Сохранить'}
                  </button>
                  {#if partsEditorSuccess}<span class="field-success">{partsEditorSuccess}</span>{/if}
                  {#if partsEditorError}<span class="field-error">{partsEditorError}</span>{/if}
                </div>
              {:else if partsEditorFile}
                <div class="muted" style="margin-top:8px">Редактируемых полей не найдено.</div>
              {/if}
            </div>
          {:else if settingsTab === 'integrations'}
            <div class="settings-section">
              <div class="settings-section-title">Yonote API</div>
              <div class="settings-section-desc">
                API-токен из Yonote (Настройки → API). Используется для чтения и записи в базу данных Yonote.
              </div>
              <label>
                API токен
                <input class="field-control" type="password" bind:value={yonoteTokenDraft} placeholder={yonoteStatus.configured ? '(сохранён)' : 'Введи API токен Yonote'} />
              </label>
              <label>
                Base URL (необязательно)
                <input class="field-control" type="text" bind:value={yonoteBaseUrlDraft} placeholder="https://app.yonote.ru" />
              </label>
              {#if yonoteConfigError}<div class="plan-error">{yonoteConfigError}</div>{/if}
              <div class="inline">
                <button class="btn" on:click={saveYonoteToken} disabled={yonoteSaving}>{yonoteSaving ? 'Сохраняю…' : 'Сохранить'}</button>
                {#if yonoteStatus.configured}
                  <span class="plan-status-badge {yonoteStatus.connected ? 'ok' : 'warn'}">{yonoteStatus.connected ? 'Подключён' : 'Не подключён'}</span>
                {/if}
                {#if yonoteConfigStatus}<span class="plan-status-badge ok">{yonoteConfigStatus}</span>{/if}
              </div>
            </div>
            <div class="settings-section">
              <div class="settings-section-title">База данных Yonote для «{currentProject}»</div>
              <div class="settings-section-desc">Выбери базу данных и сопоставь свойства для страницы «План».</div>
              <label>
                База данных
                <div class="inline">
                  <select class="field-control" bind:value={yonoteCfgDatabaseId} on:change={() => loadYonoteDbProperties(yonoteCfgDatabaseId)}>
                    <option value="">— Выбери базу —</option>
                    {#each yonoteDatabases as db}
                      <option value={db.id}>{db.title}</option>
                    {/each}
                    {#if yonoteCfgDatabaseId && !yonoteDatabases.find(d => d.id === yonoteCfgDatabaseId)}
                      <option value={yonoteCfgDatabaseId}>{yonoteCfgDatabaseId}</option>
                    {/if}
                  </select>
                  <button class="btn ghost" on:click={loadYonoteDatabases} disabled={yonoteDbLoading}>{yonoteDbLoading ? 'Загружаю…' : 'Обновить список'}</button>
                </div>
              </label>
              {#if yonoteDbProperties.length > 0 || yonotePropsLoading}
                <div class="settings-section-desc" style="margin-top:8px;">Сопоставь ключевые поля. Все остальные свойства из Yonote будут показаны автоматически.</div>
                <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
                  <label>Дата
                    <select class="field-control" bind:value={yonoteCfgDateProp}>
                      <option value="">— не выбрано —</option>
                      {#each yonoteDbProperties as p}<option value={p.id}>{p.title} ({p.type})</option>{/each}
                    </select>
                  </label>
                  <label>Тема (заголовок)
                    <select class="field-control" bind:value={yonoteCfgSubjectProp}>
                      <option value="">— не выбрано —</option>
                      {#each yonoteDbProperties as p}<option value={p.id}>{p.title} ({p.type})</option>{/each}
                    </select>
                  </label>
                  <label>Прехедер
                    <select class="field-control" bind:value={yonoteCfgPreheaderProp}>
                      <option value="">— не выбрано —</option>
                      {#each yonoteDbProperties as p}<option value={p.id}>{p.title} ({p.type})</option>{/each}
                    </select>
                  </label>
                  <label>Статус
                    <select class="field-control" bind:value={yonoteCfgStatusProp}>
                      <option value="">— не выбрано —</option>
                      {#each yonoteDbProperties as p}<option value={p.id}>{p.title} ({p.type})</option>{/each}
                    </select>
                  </label>
                </div>
                {#if yonoteStatusOptions.length > 0}
                  <label>Статус кнопки «Отправлено»
                    <select class="field-control" bind:value={yonoteCfgSentStatus}>
                      <option value="">— не выбрано —</option>
                      {#each yonoteStatusOptions as s}<option value={s}>{s}</option>{/each}
                    </select>
                    <span class="settings-section-desc" style="margin-top:2px">Какой статус ставить при нажатии на кнопку в конструкторе</span>
                  </label>
                {/if}
                <div class="settings-section-desc" style="margin-top:4px;">
                  Свойства базы: {yonoteDbProperties.map(p => p.title).join(', ')}
                </div>
              {/if}
              <div class="inline">
                <button class="btn" on:click={saveYonoteProjectConfig}>Сохранить</button>
                <button class="btn ghost" on:click={testYonoteConnection} disabled={yonoteTestLoading}>{yonoteTestLoading ? 'Проверяю…' : 'Проверить подключение'}</button>
              </div>
              {#if yonoteTestResult}
                {@const isOk = yonoteTestResult.startsWith('ok:')}
                <div class="plan-status-badge {isOk ? 'ok' : 'error'}" style="margin-top:8px;display:inline-block;">{yonoteTestResult.replace(/^(ok|error):/, '')}</div>
              {/if}
            </div>
            <div class="settings-section">
              <div class="settings-section-title">FTP/SFTP загрузка для «{currentProject}»</div>
              <div class="settings-section-desc">Загружайте изображения на FTP/SFTP сервер. Папка по дате письма создаётся автоматически.</div>
              <label>
                Протокол
                <select class="field-control" bind:value={ftpProtocol}>
                  <option value="ftp">FTP</option>
                  <option value="sftp">SFTP</option>
                </select>
              </label>
              <div class="inline" style="gap:8px">
                <label style="flex:1">
                  Хост
                  <input class="field-control" type="text" bind:value={ftpHost} placeholder="ftp.example.com" />
                </label>
                <label style="width:80px">
                  Порт
                  <input class="field-control" type="text" bind:value={ftpPort} placeholder={ftpProtocol === 'sftp' ? '22' : '21'} />
                </label>
              </div>
              <div class="inline" style="gap:8px">
                <label style="flex:1">
                  Логин
                  <input class="field-control" type="text" bind:value={ftpUser} />
                </label>
                <label style="flex:1">
                  Пароль
                  <input class="field-control" type="password" bind:value={ftpPassword}
                    placeholder={settings.ftpConfig?.hasPassword ? '(сохранён)' : ''} />
                </label>
              </div>
              <label>
                Удалённый путь (базовая папка на сервере)
                <input class="field-control" type="text" bind:value={ftpRemotePath} placeholder="/upload/email/newsletter-2026" />
              </label>
              <label>
                Публичный URL (без / в конце)
                <input class="field-control" type="text" bind:value={ftpBaseUrl} placeholder="https://vipavenue.ru/upload/email/newsletter-2026" />
              </label>
              <div class="hint">Результат: {ftpBaseUrl || 'https://...'}/{getLetterDateFolder() || 'DD-MM'}/имя.png</div>
              <div class="inline" style="gap:8px;margin-top:4px">
                <button class="btn" on:click={saveFtpConfig}>Сохранить</button>
                <button class="btn ghost" on:click={testFtpConnection} disabled={ftpTestLoading}>
                  {ftpTestLoading ? 'Проверяю…' : 'Проверить'}
                </button>
              </div>
              {#if ftpTestResult}
                {@const isOk = ftpTestResult.startsWith('ok:')}
                <div class="plan-status-badge {isOk ? 'ok' : 'error'}" style="margin-top:6px;display:inline-block">
                  {ftpTestResult.replace(/^(ok|error):/, '')}
                </div>
              {/if}
            </div>
            <div class="settings-section">
              <div class="settings-section-title">Сервис рассылок</div>
              <div class="settings-section-desc">Кнопка под карточкой сборки для перехода в сервис</div>
              <label>Текст кнопки
                <input class="field-control" type="text" bind:value={mailingServiceLabel} placeholder="Создать рассылку" />
              </label>
              <label>Ссылка
                <input class="field-control" type="text" bind:value={mailingServiceUrl} placeholder="https://..." />
              </label>
              <div class="inline" style="gap:8px;margin-top:8px">
                <button class="btn" on:click={saveMailingService}>Сохранить</button>
              </div>
            </div>

            <div class="settings-section">
              <div class="settings-section-title">Гендерные пути</div>
              <div class="settings-section-desc">Include-пути для header/footer по гендеру. Используются при переключении Ж/М.</div>
              <div class="inline" style="gap:8px">
                <label style="flex:1">Header (Ж)
                  <select class="field-control" bind:value={genderHeaderFemale}>
                    <option value="">— не выбрано —</option>
                    {#each partsFiles.filter(f => f.includes('header')) as f}<option value={f}>{f}</option>{/each}
                  </select>
                </label>
                <label style="flex:1">Header (М)
                  <select class="field-control" bind:value={genderHeaderMale}>
                    <option value="">— не выбрано —</option>
                    {#each partsFiles.filter(f => f.includes('header')) as f}<option value={f}>{f}</option>{/each}
                  </select>
                </label>
              </div>
              <div class="inline" style="gap:8px;margin-top:4px">
                <label style="flex:1">Footer (Ж)
                  <select class="field-control" bind:value={genderFooterFemale}>
                    <option value="">— не выбрано —</option>
                    {#each partsFiles.filter(f => f.includes('footer')) as f}<option value={f}>{f}</option>{/each}
                  </select>
                </label>
                <label style="flex:1">Footer (М)
                  <select class="field-control" bind:value={genderFooterMale}>
                    <option value="">— не выбрано —</option>
                    {#each partsFiles.filter(f => f.includes('footer')) as f}<option value={f}>{f}</option>{/each}
                  </select>
                </label>
              </div>
              <div class="inline" style="gap:8px;margin-top:8px">
                <button class="btn" on:click={saveGenderPaths}>Сохранить</button>
              </div>
            </div>

            <div class="settings-section">
              <div class="settings-section-title">Автонумерация картинок</div>
              <div class="settings-section-desc">Базовый URL и расширение для автоматической нумерации картинок в блоках.</div>
              <div class="inline" style="gap:8px">
                <label style="flex:3">Базовый URL
                  <input class="field-control" type="text" bind:value={imageBaseUrl} placeholder="https://email-files.vipavenue.ru/" />
                </label>
                <label style="flex:1">Расширение
                  <input class="field-control" type="text" bind:value={imageExt} placeholder=".png" />
                </label>
              </div>
              <div class="inline" style="gap:8px;margin-top:8px">
                <button class="btn" on:click={saveImageConfig}>Сохранить</button>
              </div>
            </div>

            <div class="settings-section">
              <div class="settings-section-title">Фид товаров</div>
              <div class="settings-section-desc">URL YML-фида каталога. Товары подставятся в превью вместо Mindbox-тегов.</div>
              <label>URL фида
                <input class="field-control" type="text" bind:value={feedUrlDraft} placeholder="https://site.ru/bitrix/catalog_export/mindbox.php" />
              </label>
              <div class="inline" style="gap:8px;margin-top:8px">
                <button class="btn" on:click={saveFeedUrl}>Сохранить</button>
                {#if feedUrlDraft.trim()}
                  <button class="btn ghost" on:click={refreshFeed} disabled={feedRefreshLoading}>
                    {feedRefreshLoading ? '⏳ Загрузка фида…' : '🔄 Обновить фид'}
                  </button>
                {/if}
              </div>
              {#if feedRefreshResult}
                {@const isOk = feedRefreshResult.startsWith('ok:')}
                <div class="plan-status-badge {isOk ? 'ok' : 'error'}" style="margin-top:6px;display:inline-block">
                  {feedRefreshResult.replace(/^(ok|error):/, '')}
                </div>
              {/if}
              {#if feedRefreshDiff?.addedProducts?.length}
                <div class="feed-diff-list" style="margin-top:8px">
                  <div class="muted" style="margin-bottom:4px">Новые товары:</div>
                  {#each feedRefreshDiff.addedProducts as p}
                    <div class="feed-diff-item">
                      <span class="feed-diff-id">{p.id}</span>
                      <span class="feed-diff-name">{p.name}</span>
                    </div>
                  {/each}
                  {#if feedRefreshDiff.added > feedRefreshDiff.addedProducts.length}
                    <div class="muted">…и ещё {feedRefreshDiff.added - feedRefreshDiff.addedProducts.length}</div>
                  {/if}
                </div>
              {/if}
            </div>
          {:else if settingsTab === 'users' && currentUser?.role === 'admin'}
            <div class="admin-page">
              {#if adminLoading}
                <div class="preview-empty">Загрузка...</div>
              {:else}
                {#if adminError}<div class="field-error" style="margin-bottom:8px">{adminError}</div>{/if}
                <table class="stats-table">
                  <thead>
                    <tr>
                      <th>Логин</th>
                      <th>Имя</th>
                      <th>Роль</th>
                      <th>Проекты</th>
                      <th></th>
                    </tr>
                  </thead>
                  <tbody>
                    {#each adminUsers as user}
                      <tr>
                        <td><strong>{user.login}</strong></td>
                        <td>{user.name}</td>
                        <td>
                          <select value={user.role} on:change={(e) => adminUpdateUser(user.id, { role: e.target.value })}>
                            <option value="admin">admin</option>
                            <option value="user">user</option>
                          </select>
                        </td>
                        <td class="admin-projects-cell">
                          <span class="admin-all-projects">{PROJECT_NAME}</span>
                        </td>
                        <td>
                          {#if user.id !== currentUser.id}
                            <button class="btn ghost danger" on:click={() => adminDeleteUser(user.id)} title="Удалить">×</button>
                          {/if}
                        </td>
                      </tr>
                    {/each}
                  </tbody>
                </table>
                <div class="admin-new-user">
                  <div class="admin-new-title">Новый пользователь</div>
                  <div class="admin-new-fields">
                    <input class="field-control" placeholder="Логин" bind:value={adminNewUser.login} />
                    <input class="field-control" placeholder="Пароль" type="password" bind:value={adminNewUser.password} />
                    <input class="field-control" placeholder="Имя" bind:value={adminNewUser.name} />
                    <select class="field-control" bind:value={adminNewUser.role}>
                      <option value="user">user</option>
                      <option value="admin">admin</option>
                    </select>
                    <button class="btn btn-primary" on:click={adminCreateUser}>Создать</button>
                  </div>
                </div>
              {/if}
            </div>
          {:else if settingsTab === 'certificate'}
            <div class="settings-section">
              <div class="settings-section-title">Сертификат</div>
              <div class="settings-section-desc">Группа блоков, которая добавляется в конец письма. Включается/выключается в конструкторе для каждого письма отдельно.</div>

              <div style="margin-top: 12px; display: flex; align-items: center; gap: 12px;">
                <button class="btn btn-primary" on:click={openCertEditor}>
                  {certBlocks.length > 0 ? 'Редактировать' : 'Создать'}
                </button>
                {#if certBlocks.length > 0}
                  <span class="muted">{certBlocks.length} блок(а/ов)</span>
                  <button class="btn ghost danger" on:click={() => saveCertificate([])}>Очистить</button>
                {/if}
              </div>
              {#if certBlocks.length > 0}
                <div class="cert-block-summary">
                  {#each certBlocks as block}
                    <span class="cert-block-chip">{block.name}</span>
                  {/each}
                </div>
              {/if}
            </div>
          {/if}

        </div>
      </div>
    {:else if activePage === 'plan'}
      <div class="panel-heading">
        <div>
          <div class="panel-title">План</div>
          <div class="panel-subtitle">Ближайшие рассылки из всех проектов</div>
        </div>
        <div class="inline">
          {#if planLoadedAt}<span class="plan-loaded-at">Обновлено {planLoadedAt.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}</span>{/if}
          <button class="btn ghost" class:toggled={planShowDone} on:click={() => planShowDone = !planShowDone}>
            {planShowDone ? 'Скрыть выполненные' : 'Показать выполненные'}
          </button>
          <button class="btn ghost" on:click={() => loadPlan()} disabled={planLoading}>{planLoading ? 'Загрузка…' : 'Обновить'}</button>
        </div>
      </div>
      <div class="plan-page-grid">
        <div class="panel plan-list-panel">
          {#if planLoading && planRows.length === 0}
            <div class="plan-state">Загружаю данные из таблиц…</div>
          {:else if planError}
            <div class="plan-state plan-state-error">{planError}</div>
          {:else if planRows.length === 0}
            <div class="plan-state">Нет рассылок в диапазоне −7 / +60 дней, либо таблицы не настроены (Настройки → Интеграции).</div>
            {#if planWarnings.length > 0}
              <div class="plan-warnings">{#each planWarnings as w}<div class="plan-warning">⚠ {w}</div>{/each}</div>
            {/if}
          {:else}
            {#if planWarnings.length > 0}
              <div class="plan-warnings">{#each planWarnings as w}<div class="plan-warning">⚠ {w}</div>{/each}</div>
            {/if}
            <div class="plan-periods">
              {#each planPeriodGroups as period}
                <div class="plan-period plan-period-{period.accent}">
                  <div class="plan-period-label">{period.label}</div>
                  <div class="plan-groups">
                    {#each period.groups as dayGroup}
                      <div class="plan-group" id="plan-day-{dayGroup.isoStr}" class:plan-group-selected={planCalSelectedDate === dayGroup.isoStr}>
                        <div class="plan-day-header">
                          <span class="plan-day-num">{String(dayGroup.date.getDate()).padStart(2, '0')}</span>
                          <div class="plan-day-label">
                            <span class="plan-day-month">{dayGroup.date.toLocaleString('ru-RU', { month: 'long' })}</span>
                            <span class="plan-day-dow">{dayGroup.date.toLocaleString('ru-RU', { weekday: 'short' })}</span>
                          </div>
                          <span class="plan-group-count">{dayGroup.rows.length}</span>
                        </div>
                        {#each dayGroup.rows as row}
                          <div class="plan-item" class:plan-item-selected={planCalSelectedDate === dayGroup.isoStr}>
                            <span class="plan-proj-badge">{row.project}</span>
                            <div class="plan-item-dot plan-dot-{planStatusClass(row.status)}"></div>
                            <div class="plan-item-content">
                              <div class="plan-item-subject">{row.subject}</div>
                              <div class="plan-item-meta">
                                {#if planEditingRowId === planRowId(row)}
                                  <select
                                    class="plan-status-select"
                                    value={row.status}
                                    disabled={planSavingRowId === planRowId(row)}
                                    on:change={(e) => setPlanRowStatus(row, e.target.value)}
                                    on:blur={() => { if (planEditingRowId === planRowId(row)) planEditingRowId = null }}
                                  >
                                    {#each planStatusOptions as opt}
                                      <option value={opt}>{opt}</option>
                                    {/each}
                                    {#if !planStatusOptions.includes(row.status)}
                                      <option value={row.status}>{row.status}</option>
                                    {/if}
                                  </select>
                                  {#if planSavingRowId === planRowId(row)}<span class="plan-item-sep">сохранение…</span>{/if}
                                {:else}
                                  {#if row.status}
                                    <span
                                      class="plan-item-status plan-dot-{planStatusClass(row.status)} plan-status-editable"
                                      title="Нажми чтобы изменить статус"
                                      on:click={() => planEditingRowId = planRowId(row)}
                                    >{row.status}</span>
                                  {:else}
                                    <span class="plan-status-empty" on:click={() => planEditingRowId = planRowId(row)}>+ статус</span>
                                  {/if}
                                {/if}
                                {#if row.preheader && row.status}<span class="plan-item-sep">·</span>{/if}
                                {#if row.preheader}<span class="plan-item-preheader">{row.preheader.length > 70 ? row.preheader.slice(0, 70) + '…' : row.preheader}</span>{/if}
                                {#each (row.extra || []).filter(e => e.link || isUrl(e.value)) as ext}
                                  <a class="plan-extra-link" href={ext.link || ext.value} target="_blank" rel="noopener" title={ext.label}>{ext.label}</a>
                                {/each}
                              </div>
                              {#if (row.extra || []).some(e => !e.link && !isUrl(e.value))}
                                <div class="plan-item-extra">
                                  {#each (row.extra || []).filter(e => !e.link && !isUrl(e.value)) as ext}
                                    <span class="plan-extra-tag" class:plan-extra-highlight={/категори|тематик|тип/i.test(ext.label)} title={ext.label}>{ext.value}</span>
                                  {/each}
                                </div>
                              {/if}
                            </div>
                            <button class="plan-item-btn" on:click={() => startAssemble(row)}>Собирать →</button>
                          </div>
                        {/each}
                      </div>
                    {/each}
                  </div>
                </div>
              {/each}
            </div>
          {/if}
        </div>

        <div class="plan-cal-panel">
          <div class="plan-cal-nav">
            <button class="btn ghost icon-square" on:click={prevPlanMonth}>‹</button>
            <span class="plan-cal-title">{planCalMonth.toLocaleString('ru-RU', { month: 'long', year: 'numeric' })}</span>
            <button class="btn ghost icon-square" on:click={nextPlanMonth}>›</button>
          </div>
          <div class="plan-cal-grid">
            {#each ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'] as dow}
              <div class="plan-cal-dow">{dow}</div>
            {/each}
            {#each planCalDays as day}
              <div
                class="plan-cal-day"
                class:plan-cal-other={!day.current}
                class:plan-cal-today={day.isoStr === planTodayIso}
                class:plan-cal-selected={planCalSelectedDate === day.isoStr}
                class:plan-cal-has={day.rows.length > 0}
                on:click={() => selectPlanDate(day.isoStr)}
              >
                <span class="plan-cal-num">{day.date.getDate()}</span>
                {#if day.rows.length > 0}
                  <div class="plan-cal-dots">
                    {#each day.rows.slice(0, 4) as row}
                      <span class="plan-cal-dot plan-dot-{planStatusClass(row.status)}"></span>
                    {/each}
                  </div>
                {/if}
              </div>
            {/each}
          </div>
        </div>
      </div>
    {:else if activePage === 'stats'}
      <div class="panel-heading">
        <div>
          <div class="panel-title">Статистика</div>
          <div class="panel-subtitle">Время работы над письмами</div>
        </div>
      </div>
      <div class="stats-page">
        {#if statsPageLoading}
          <div class="preview-empty">Загрузка...</div>
        {:else if Object.keys(allStats).length === 0}
          <div class="preview-empty">Нет данных</div>
        {:else}
          {#each statsMonthGroups as [monthKey, entries]}
            {@const [y, m] = monthKey.split('-')}
            {@const monthLabel = new Date(Number(y), Number(m) - 1).toLocaleString('ru-RU', { month: 'long', year: 'numeric' })}
            {@const totalMin = entries.reduce((s, e) => s + (e.minutes || 0), 0)}
            <div class="stats-month">
              <div class="stats-month-header">
                <span class="stats-month-label">{monthLabel}</span>
                <span class="stats-month-total">{entries.length} писем, {Math.floor(totalMin / 60)}ч {totalMin % 60}м</span>
              </div>
              <table class="stats-table">
                <thead>
                  <tr>
                    <th>Проект</th>
                    <th>Письмо</th>
                    <th>Тег</th>
                    <th>Время</th>
                    <th>Статус</th>
                  </tr>
                </thead>
                <tbody>
                  {#each entries.sort((a, b) => (b.startedAt || '').localeCompare(a.startedAt || '')) as entry}
                    <tr>
                      <td class="stats-project">{entry.project}</td>
                      <td class="stats-letter">{entry.letterName}</td>
                      <td class="stats-tag">{entry.tag || ''}</td>
                      <td class="stats-time">{entry.minutes || 0} мин</td>
                      <td class="stats-status">{entry.status || ''}</td>
                    </tr>
                  {/each}
                </tbody>
              </table>
            </div>
          {/each}
        {/if}
      </div>
    {:else}
      <div class="panel-heading">
        <div>
          <div class="panel-title">Письма</div>
          <div class="panel-subtitle">Сохранённые письма</div>
        </div>
      </div>

      <div class="panel panel-left">
        {#if presets.length > 0}
          <div class="panel-subsection">
            <div class="panel-subtitle">Пресеты</div>
            <div class="preset-quick-list">
              {#each presets as preset}
                <div class="preset-chip">
                  <button class="btn ghost" on:click={() => applyPresetBlocks(preset)}>
                    {preset.name}
                  </button>
                  <button
                    class="btn icon-square danger"
                    on:click={() => deletePresetById(preset.id)}
                    title="Удалить пресет"
                    aria-label="Удалить пресет"
                  >
                    ×
                  </button>
                </div>
              {/each}
            </div>
          </div>
        {/if}
        {#if letters.length > 8}
          <input class="search-input" type="text" placeholder="Поиск писем…" bind:value={letterSearch} />
        {/if}
        {#if filteredLetters.length === 0}
          <div class="empty">{letterSearch ? 'Ничего не найдено.' : 'Писем пока нет.'}</div>
        {:else}
          <div class="letters-list">
            {#each filteredLetters as letter}
              <div class="letter-row" class:history-open={showHistoryFor?.id === letter.id}>
                <div class="letter-row-main">
                  <div class="letter-row-name">{letter.name}</div>
                  <div class="letter-row-date">{formatPresetDate(letter.updatedAt || letter.createdAt)}</div>
                </div>
                <div class="letter-row-actions">
                  <button class="btn ghost" on:click={() => duplicateLetter(letter)}>Копировать</button>
                  <button class="btn ghost" on:click={() => openLetter(letter)}>Открыть</button>
                  <button class="btn ghost" on:click={() => openHistory(letter)}>История</button>
                  <button class="btn icon-square danger" on:click={() => deleteLetter(letter)}>×</button>
                </div>
                {#if showHistoryFor?.id === letter.id}
                  <div class="history-dropdown">
                    {#if historyLoading}
                      <div class="history-item muted">Загрузка…</div>
                    {:else if historySnapshots.length === 0}
                      <div class="history-item muted">История пуста</div>
                    {:else}
                      {#each historySnapshots as snap}
                        <button class="history-item" on:click={() => restoreFromHistory(snap)}>
                          {new Date(snap.savedAt).toLocaleString('ru')}
                        </button>
                      {/each}
                    {/if}
                  </div>
                {/if}
              </div>
            {/each}
          </div>
        {/if}
      </div>
    {/if}
  </div>

  {#if showLetterModal}
    <div class="modal-backdrop" on:click|self={() => (showLetterModal = false)}>
      <div class="modal-card">
        <div class="modal-title">
          {letterModalMode === 'update' ? 'Сохранить письмо' : 'Новое письмо'}
        </div>
        <div class="modal-step">Шаг 1. Дата отправки</div>
        <input class="field-control" type="date" bind:value={newLetterDate} />
        <div class="modal-step">Шаг 2. Название письма</div>
        <input
          class="field-control"
          type="text"
          placeholder="Название письма"
          bind:value={newLetterTitle}
        />
        <div class="modal-preview">
          Итоговое имя: {buildLetterName(newLetterTitle, newLetterDate)}
        </div>
        {#if newLetterError}
          <div class="modal-error">{newLetterError}</div>
        {/if}
        <div class="modal-actions">
          <button class="btn ghost" on:click={() => (showLetterModal = false)}>Отмена</button>
          <button class="btn" on:click={saveLetterFromModal}>
            {letterModalMode === 'update' ? 'Сохранить' : 'Создать письмо'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  {#if showPresetModal}
    <div class="modal-backdrop" on:click|self={() => (showPresetModal = false)}>
      <div class="modal-card">
        <div class="modal-title">Новый пресет</div>
        <div class="modal-step">Короткое имя пресета</div>
        <input
          class="field-control"
          type="text"
          placeholder="Название пресета"
          bind:value={presetNameDraft}
          on:keydown={(event) => event.key === 'Enter' && createPresetFromLetter()}
        />
        {#if presetError}
          <div class="modal-error">{presetError}</div>
        {/if}
        <div class="modal-actions">
          <button class="btn ghost" on:click={() => (showPresetModal = false)}>Отмена</button>
          <button class="btn" on:click={createPresetFromLetter}>Создать</button>
        </div>
      </div>
    </div>
  {/if}

  {#if showStartupModal}
    <div class="modal-backdrop">
      <div class="modal-card">
        <div class="modal-title">{currentProject}</div>
        {#if startupLastLetter}
          <div class="modal-step">Последнее письмо</div>
          <button
            class="btn startup-last-letter-btn"
            on:click={() => { showStartupModal = false; openLetter(startupLastLetter) }}
          >
            {startupLastLetter.name}
          </button>
          <div class="modal-divider">или</div>
        {/if}
        <button class="btn" on:click={() => { showStartupModal = false; openLetterModal('new') }}>
          Создать новое письмо
        </button>
        {#if startupPlanRows.length > 0}
          <div class="modal-divider">рассылки из плана</div>
          <div class="startup-plan-list">
            {#each startupPlanRows as row}
              {@const isPast = row.date && row.date < new Date(new Date().setHours(0,0,0,0))}
              {@const isDone = planStatusClass(row.status) === 'done'}
              {#if !isPast || !isDone}
              {@const overdue = isPast && !isDone}
              <button
                class="btn ghost startup-plan-btn"
                class:startup-plan-overdue={overdue}
                on:click={() => { showStartupModal = false; startAssemble(row) }}
              >
                <span class="startup-plan-date">{row.dateStr}</span>
                <span class="startup-plan-subject">{row.subject}</span>
                {#if row.status}<span class="startup-plan-status plan-status-{planStatusClass(row.status)}">{row.status}</span>{/if}
              </button>
              {/if}
            {/each}
          </div>
        {/if}
        <div class="modal-actions">
          <button class="btn ghost" on:click={() => (showStartupModal = false)}>
            Продолжить без письма
          </button>
        </div>
      </div>
    </div>
  {/if}

  {#if showFtpGalleryModal}
    <div class="modal-backdrop" on:click|self={() => (showFtpGalleryModal = false)}>
      <div class="modal-card ftp-modal">
        <div class="modal-title">
          FTP: {settings.ftpConfig?.host}
          <span style="color:var(--muted);font-weight:400"> / {ftpGalleryFolder}</span>
        </div>
        <div class="ftp-modal-path">{settings.ftpConfig?.remotePath}/{ftpGalleryFolder}/</div>
        <div class="ftp-upload-form">
          <div class="ftp-upload-row">
            <label class="btn ghost ftp-file-btn" class:uploading={ftpUploading}>
              {#if ftpUploading}
                Загружаю…{#if ftpUploadProgress}&nbsp;<span class="ftp-progress">{ftpUploadProgress}</span>{/if}
              {:else}
                + Загрузить картинки
              {/if}
              <input type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple style="display:none"
                disabled={ftpUploading} on:change={handleFtpFileSelect} />
            </label>
          </div>
          {#if ftpUploading}
            <div class="ftp-progressbar"><div class="ftp-progressbar-fill" style="width:{ftpUploadPercent}%"></div></div>
          {/if}
          {#if ftpUploadStatus && !ftpUploading}<div class="ftp-upload-status">{ftpUploadStatus}</div>{/if}
          {#if ftpUploadError}<div class="plan-error" style="margin-top:4px;white-space:pre-wrap">{ftpUploadError}</div>{/if}
        </div>
        {#if ftpGalleryLoading && ftpGalleryImages.length === 0}
          <div class="ftp-gallery-state">Загружаю список…</div>
        {:else if ftpGalleryError}
          <div class="ftp-gallery-state ftp-gallery-error">{ftpGalleryError}</div>
        {:else if ftpGalleryImages.length === 0}
          <div class="ftp-gallery-state">Папка пуста</div>
        {:else}
          <div class="ftp-gallery-grid">
            {#each ftpGalleryImages as img}
              <div class="ftp-gallery-item" class:delete-pending={ftpDeletePending === img} title={img.url}>
                <div class="ftp-gallery-item-img" on:click={() => { if (ftpDeletePending === img) return; selectFtpImage(img.url) }}>
                  <img src={img.url} alt={img.name} on:error={(e) => { e.target.style.display = 'none' }} />
                </div>
                <div class="ftp-gallery-item-footer">
                  <div class="ftp-gallery-item-name">{img.name}</div>
                  {#if ftpDeletePending === img}
                    <button class="ftp-gallery-confirm-btn yes" title="Да, удалить" on:click|stopPropagation={() => deleteFtpImage(img)}>✓</button>
                    <button class="ftp-gallery-confirm-btn no" title="Отмена" on:click|stopPropagation={() => ftpDeletePending = null}>✕</button>
                  {:else}
                    <button class="ftp-gallery-delete-btn" title="Удалить" on:click|stopPropagation={() => ftpDeletePending = img}>×</button>
                  {/if}
                </div>
              </div>
            {/each}
          </div>
        {/if}
        <div class="modal-actions">
          <button class="btn ghost" on:click={loadFtpGallery} disabled={ftpGalleryLoading}>
            {ftpGalleryLoading && ftpGalleryImages.length > 0 ? 'Обновляю…' : 'Обновить'}
          </button>
          <button class="btn ghost" on:click={() => (showFtpGalleryModal = false)}>Закрыть</button>
        </div>
      </div>
    </div>
  {/if}

  {#if showNoteModal}
    <div class="modal-backdrop" on:click|self={() => (showNoteModal = false)}>
      <div class="modal-card">
        <div class="modal-title">Новая заметка</div>
        <div class="modal-step">Название заметки</div>
        <input
          class="field-control"
          type="text"
          placeholder="Название заметки"
          bind:value={noteTitleDraft}
          on:keydown={(event) => event.key === 'Enter' && createNote()}
        />
        {#if noteError}
          <div class="modal-error">{noteError}</div>
        {/if}
        <div class="modal-actions">
          <button class="btn ghost" on:click={() => (showNoteModal = false)}>Отмена</button>
          <button class="btn" on:click={createNote}>Создать</button>
        </div>
      </div>
    </div>
  {/if}
{#if quickEditVisible && quickEditBlock && quickEditField}
  <div
    class="quick-edit-popup"
    style={`top:${quickEditPos.top}px;left:${quickEditPos.left}px;`}
    on:click|stopPropagation
  >
    <div class="quick-edit-header">
      <span class="quick-edit-title">{quickEditBlock.name} — {quickEditField.label}</span>
      <button class="quick-edit-close" on:click={closeQuickEdit} title="Закрыть">✕</button>
    </div>
    <textarea
      class="quick-edit-textarea"
      value={quickEditValue}
      on:input={handleQuickEditInput}
      on:keydown={handleQuickEditKeydown}
      rows="3"
    ></textarea>
  </div>
{/if}
</div>

{#if toasts.length > 0}
  <div class="toast-container">
    {#each toasts as toast (toast.id)}
      <div class="toast toast-{toast.type}">
        <div class="toast-text">{toast.message}</div>
        <button class="toast-close" on:click={() => toasts = toasts.filter(t => t.id !== toast.id)}>×</button>
      </div>
    {/each}
  </div>
{/if}

{#if suggestHoverItem}
  <div class="suggest-hover-popup" style="left:{suggestHoverPos.x + 16}px;top:{suggestHoverPos.y - 20}px">
    {#if suggestHoverItem.image}<img src={suggestHoverItem.image} alt="" class="suggest-hover-img" />{/if}
    <div class="suggest-hover-info">
      <div class="suggest-hover-name">{suggestHoverItem.name}</div>
      {#if suggestHoverItem.vendor}<div class="suggest-hover-row">{suggestHoverItem.vendor}</div>{/if}
      {#if suggestHoverItem.color}<div class="suggest-hover-row">{suggestHoverItem.color}</div>{/if}
      {#if suggestHoverItem.gender}<div class="suggest-hover-row muted">{suggestHoverItem.gender}</div>{/if}
      <div class="suggest-hover-price">{suggestHoverItem.price ? `${Number(suggestHoverItem.price).toLocaleString('ru-RU')} руб.` : ''}</div>
      <div class="muted" style="font-size:11px">ID {suggestHoverItem.id}</div>
    </div>
  </div>
{/if}

{/if}
