--[[
  SpellSmasher UI - WoW panel structure + SpellSmasher brand colors.
  Structure: dialog metal / tooltip edges (game chrome).
  Color: navy / brand green #3dd68c / brand blue #6c8cff (the S - never generic gold shell).
  Product: ST chip = Method ST steps; AoE chip = aoeSteps.

  Part 11 (2026-08-09): keep as ONE file. Create + Refresh share closures.
  Do not split unless proven pain - docs/UI-FILE-SPLIT-RESEARCH-2026-08-09.md.
]]

local NS = SpellSmasherNS
local UI = {}
NS.UI = UI

local C = NS.C
local mainFrame
local ui = {}
local statusFS
local importBox
local tabButtons = {}

-- -- primitives ----------------------------------------------

-- Small UI icons (Blizzard textures - click-box affordances)
local ICONS = {
  up = "Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up",
  down = "Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up",
  plus = "Interface\\Buttons\\UI-PlusButton-Up",
  minus = "Interface\\Buttons\\UI-MinusButton-Up",
  delete = "Interface\\Buttons\\UI-GroupLoot-Pass-Up",
  check = "Interface\\Buttons\\UI-CheckBox-Check",
  edit = "Interface\\Buttons\\UI-GuildButton-PublicNote-Up",
  gear = "Interface\\Buttons\\UI-OptionsButton",
  refresh = "Interface\\Buttons\\UI-RefreshButton",
  book = "Interface\\Buttons\\UI-GuildButton-OfficerNote-Up",
  copy = "Interface\\Buttons\\UI-GuildButton-MOTD-Up",
  close = "Interface\\Buttons\\UI-Panel-MinimizeButton-Up",
  play = "Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up",
}

--- Legacy SoftFill used to paint solid color OVER backdrops (hid real WoW textures).
--- Keep helper only for rare non-backdrop strips; always hide when applying Hex.
local function SoftFill(frame, color, inset)
  -- No-op for fills: real UI-Tooltip / DialogBox textures show through Hex/HexBackdrop.
  if frame and frame._softFill then
  frame._softFill:Hide()
  end
end

local function ClearSoftFill(frame)
  if frame and frame._softFill then
  frame._softFill:Hide()
  end
end

--- Apply normal WoW backdrops. Default style = "panel" (tooltip metal).
--- style: "panel" | "dialog" | "toast" | "leather"
local function Hex(frame, bg, border, edge, style)
  ClearSoftFill(frame)
  style = style or "panel"
  if style == "flat" then
  style = "panel"
  end
  local es = edge
  if style == "panel" or style == "tooltip" then
  es = (edge and edge >= 12) and edge or 16
  elseif style == "leather" or style == "parchment" then
  es = edge or 16
  end
  NS.HexBackdrop(frame, bg, border, es, style)
end

-- Parchment bar art (same load style as working panel-bg / brand-mark)
local SEQ_PAGE = "Interface\\AddOns\\SpellSmasher\\Media\\seqbar"

--- Edit spell-row: solid parchment fill (must actually paint - black rows = texture miss).
local function HexLeather(frame, bg, border, selected)
  if not frame then
  return
  end
  -- Clear template backdrop so it cannot cover our texture
  if frame.SetBackdrop then
  pcall(function()
  frame:SetBackdrop(nil)
  end)
  end

  -- Base paper color ALWAYS visible (even if file texture fails)
  local base = frame._pageBase
  if not base then
  base = frame:CreateTexture(nil, "BACKGROUND")
  frame._pageBase = base
  end
  base:ClearAllPoints()
  base:SetAllPoints(frame)
  if selected then
  base:SetColorTexture(0.42, 0.48, 0.28, 1)
  else
  -- warm parchment brown
  base:SetColorTexture(0.55, 0.42, 0.26, 1)
  end
  base:Show()

  -- File texture on top of base (same pattern as ApplyPanelImage)
  local tex = frame._pageTex
  if not tex then
  tex = frame:CreateTexture(nil, "BORDER")
  frame._pageTex = tex
  end
  tex:ClearAllPoints()
  tex:SetAllPoints(frame)
  -- JPG is most reliable for custom addon Media (same folder as panel-bg)
  tex:SetTexture("Interface\\AddOns\\SpellSmasher\\Media\\seqbar")
  if tex.SetTexCoord then
  tex:SetTexCoord(0, 1, 0, 1)
  end
  tex:SetVertexColor(1, 1, 1, 1)
  tex:SetAlpha(1)
  tex:Show()

  -- Hide leftover edge-strip experiment (dark L/R/T/B bars - looked like lines on each spell)
  for _, key in ipairs({ "_pageEdgeL", "_pageEdgeR", "_pageEdgeT", "_pageEdgeB" }) do
  if frame[key] then
  frame[key]:Hide()
  end
  end

  local tint = frame._pageTint
  if not tint then
  tint = frame:CreateTexture(nil, "ARTWORK")
  frame._pageTint = tint
  end
  tint:ClearAllPoints()
  tint:SetPoint("TOPLEFT", 4, -3)
  tint:SetPoint("BOTTOMRIGHT", -4, 3)
  if selected then
  tint:SetColorTexture(0.15, 0.55, 0.28, 0.22)
  tint:Show()
  else
  tint:Hide()
  end
end

-- Real image files in Media/ (same load style as panel-bg / seqbar)
local BRAND_MARK = "Interface\\AddOns\\SpellSmasher\\Media\\logo"
local PANEL_BG = "Interface\\AddOns\\SpellSmasher\\Media\\panel-bg"

--- Use an actual image as the panel fill (not color blobs).
--- path: texture path without extension; alpha 0-1 (default ~0.92).
local function ApplyPanelImage(frame, path, opts)
  opts = opts or {}
  if not frame then
  return
  end
  path = path or PANEL_BG
  local inset = opts.inset or 12
  local alpha = opts.alpha
  if alpha == nil then
  alpha = 0.92
  end

  -- Kill any old multi-blob swirl layers from 4.0.82
  if frame._brandSwirl then
  frame._brandSwirl:Hide()
  frame._brandSwirl:SetParent(nil)
  frame._brandSwirl = nil
  end

  local tex = frame._panelBgTex
  if not tex then
  tex = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
  frame._panelBgTex = tex
  end
  tex:ClearAllPoints()
  tex:SetPoint("TOPLEFT", inset, -inset)
  tex:SetPoint("BOTTOMRIGHT", -inset, inset)
  tex:SetTexture(path)
  -- Stretch to fill the panel (real wallpaper image)
  if tex.SetHorizTile then
  tex:SetHorizTile(false)
  tex:SetVertTile(false)
  end
  tex:SetAlpha(alpha)
  tex:Show()
  return tex
end

local function SetFS(fs, color)
  if fs and color then
  fs:SetTextColor(color[1], color[2], color[3], color[4] or 1)
  end
end

--- Classic old-school WoW buttons (UIPanelButtonTemplate / UIPanelCloseButton).
--- kind only tints the gold label (green / blue / red) - chrome stays Blizzard stone.
--- opts.icon = key from ICONS or path; opts.iconOnly = square icon control
local function MakeThemeButton(parent, opts)
  opts = opts or {}
  local iconOnly = opts.iconOnly and true or false
  local w = opts.w or (iconOnly and 24 or 100)
  local h = opts.h or (iconOnly and 24 or 22)
  local kind = opts.kind or "secondary"
  local isClose = iconOnly and (opts.icon == "close" or opts.kind == "close")

  local b
  if isClose then
  -- Classic red X (same as Character / bag windows)
  b = CreateFrame("Button", nil, parent, "UIPanelCloseButton")
  b:SetSize(math.max(24, w), math.max(24, h))
  else
  -- Classic stone panel button (Options / OK / Cancel look)
  b = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
  b:SetSize(w, h)
  end
  b:EnableMouse(true)
  b._classic = true
  b._kind = kind
  b._opts = opts

  -- Template FontString (gold GameFontNormal by default)
  local label = b.Text or (b.GetFontString and b:GetFontString()) or nil
  if not label and not isClose then
  label = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  b:SetFontString(label)
  label:SetPoint("CENTER", 0, 0)
  end
  b.label = label

  if not isClose and opts.text and opts.text ~= "" and not iconOnly then
  if b.SetText then
  b:SetText(opts.text)
  elseif label then
  label:SetText(opts.text)
  end
  elseif iconOnly and not isClose and opts.text and opts.text ~= "" then
  -- tiny ^ v + etc. still use classic button with short text
  if b.SetText then
  b:SetText(opts.text)
  end
  end

  local iconTex
  local iconPath = (not isClose) and opts.icon and (ICONS[opts.icon] or opts.icon) or nil
  if iconPath then
  iconTex = b:CreateTexture(nil, "ARTWORK")
  local iz = opts.iconSize or (iconOnly and 14 or 12)
  iconTex:SetSize(iz, iz)
  if iconOnly or not opts.text or opts.text == "" then
  iconTex:SetPoint("CENTER", 0, 0)
  else
  iconTex:SetPoint("LEFT", 6, 0)
  end
  iconTex:SetTexture(iconPath)
  iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
  b.icon = iconTex
  end

  -- Classic gold text; brand tints only for status kinds (still look like WoW)
  local GOLD = { 1.0, 0.82, 0.0, 1 }
  -- Outer green ring when selected (panel button art covers BACKGROUND fills)
  local selBorder = CreateFrame("Frame", nil, b, "BackdropTemplate")
  selBorder:SetPoint("TOPLEFT", -2, 2)
  selBorder:SetPoint("BOTTOMRIGHT", 2, -2)
  selBorder:SetFrameLevel(math.max(1, (b:GetFrameLevel() or 1) - 1))
  if selBorder.SetBackdrop then
  selBorder:SetBackdrop({
  bgFile = "Interface\\Buttons\\WHITE8X8",
  edgeFile = "Interface\\Buttons\\WHITE8X8",
  edgeSize = 2,
  insets = { left = 0, right = 0, top = 0, bottom = 0 },
  })
  selBorder:SetBackdropColor(C.green[1], C.green[2], C.green[3], 0.35)
  selBorder:SetBackdropBorderColor(C.green[1], C.green[2], C.green[3], 1)
  end
  selBorder:Hide()
  b._selBorder = selBorder

  local function paintButtonFace(selected)
  -- Tint Blizzard stone face green when chosen
  local vr, vg, vb = 1, 1, 1
  if selected then
  vr, vg, vb = 0.35, 1.0, 0.55
  end
  for _, getter in ipairs({ "GetNormalTexture", "GetPushedTexture", "GetHighlightTexture", "GetDisabledTexture" }) do
  local fn = b[getter]
  if type(fn) == "function" then
  local tex = fn(b)
  if tex and tex.SetVertexColor then
  tex:SetVertexColor(vr, vg, vb)
  end
  end
  end
  end

  local function paint(_hover)
  if isClose then
  return
  end
  local k = b._kind
  local selected = opts.active and true or false
  if selBorder then
  if selected then
  selBorder:Show()
  else
  selBorder:Hide()
  end
  end
  paintButtonFace(selected)
  if not label then
  return
  end
  if selected then
  -- Chosen / On: bright green label
  label:SetTextColor(C.green[1], C.green[2], C.green[3], 1)
  elseif k == "green" then
  label:SetTextColor(C.green[1], C.green[2], C.green[3], 1)
  elseif k == "danger" then
  label:SetTextColor(C.danger[1], C.danger[2], C.danger[3], 1)
  elseif k == "primary" then
  label:SetTextColor(C.accent[1], C.accent[2], C.accent[3], 1)
  elseif k == "ghost" then
  label:SetTextColor(C.muted[1], C.muted[2], C.muted[3], 1)
  elseif k == "tab" then
  label:SetTextColor(GOLD[1], GOLD[2], GOLD[3], 0.85)
  else
  label:SetTextColor(GOLD[1], GOLD[2], GOLD[3], 1)
  end
  if iconTex then
  if k == "danger" then
  iconTex:SetVertexColor(C.danger[1], C.danger[2], C.danger[3], 1)
  else
  iconTex:SetVertexColor(1, 1, 1, 1)
  end
  end
  if selected and b.LockHighlight then
  b:LockHighlight()
  elseif b.UnlockHighlight then
  b:UnlockHighlight()
  end
  end
  b._paint = paint
  paint(false)

  b:SetScript("OnEnter", function(self)
  paint(true)
  if opts.tip then
  GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
  GameTooltip:AddLine(opts.tip, C.green[1], C.green[2], C.green[3])
  if opts.tipBody then
  GameTooltip:AddLine(opts.tipBody, 1, 1, 1, true)
  end
  GameTooltip:Show()
  end
  end)
  b:SetScript("OnLeave", function()
  paint(false)
  GameTooltip:Hide()
  end)
  if opts.onClick then
  b:SetScript("OnClick", function(self, ...)
  -- Classic UI click
  if PlaySound and SOUNDKIT and SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON then
  pcall(PlaySound, SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
  end
  opts.onClick(self, ...)
  end)
  end

  function b:SetThemeText(tx)
  tx = tx or ""
  if self.SetText then
  self:SetText(tx)
  end
  if label then
  label:SetText(tx)
  if not iconOnly then
  label:Show()
  end
  end
  paint(false)
  end
  function b:SetActive(on)
  opts.active = on and true or false
  paint(false)
  end
  function b:SetKind(k)
  b._kind = k
  paint(false)
  end
  return b
end

-- Forward declare: popups call this from OnAccept (defined later)
local SetStatus

-- Confirm delete pack (avoid fat-finger on the x)
StaticPopupDialogs["SPELLSMASHER_DELETE_PACK"] = {
  text = "Delete pack \"%s\"?\nThis cannot be undone (re-import / New pack to restore).",
  button1 = "Delete",
  button2 = "Cancel",
  OnAccept = function(_, data)
  if not data or not NS.db or not NS.db.sequences then
  return
  end
  local i = data.index
  if not i or not NS.db.sequences[i] then
  return
  end
  table.remove(NS.db.sequences, i)
  -- After remove, every index > i shifts down by 1. Only clamping the max is wrong:
  -- e.g. Active=4, delete pack 2 > must become 3 so Active stays the same pack.
  local n = #NS.db.sequences
  local function fixIndexAfterRemove(cur)
  cur = tonumber(cur) or 1
  if n < 1 then
  return 1
  end
  if cur > i then
  cur = cur - 1
  elseif cur == i then
  -- Deleted this slot: land on what slid into i (or last if we deleted the end).
  cur = math.min(i, n)
  end
  if cur < 1 then
  cur = 1
  elseif cur > n then
  cur = n
  end
  return cur
  end
  NS.db.activeIndex = fixIndexAfterRemove(NS.db.activeIndex)
  NS.db.editSeqIndex = fixIndexAfterRemove(NS.db.editSeqIndex)
  NS.db.listSelectIndex = fixIndexAfterRemove(NS.db.listSelectIndex)
  if NS.db.aoeIndex then
  local a = tonumber(NS.db.aoeIndex)
  if not a or a < 1 or a > n then
  NS.db.aoeIndex = nil
  elseif a > i then
  NS.db.aoeIndex = a - 1
  elseif a == i then
  NS.db.aoeIndex = nil
  end
  end
  NS.Secure.Recompile()
  UI.Refresh()
  SetStatus("|cffff6b7aPack deleted.|r")
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
  preferredIndex = 3,
}

-- Method-proxy Apply honesty: shared Class|Spec gold is not a hero-exact talent scrape
StaticPopupDialogs["SPELLSMASHER_APPLY_PROXY"] = {
  text = "Shared Method talent package\n\nThis pack's talent string is Class|Spec gold (not a hero-specific Method scrape). Spell list for \"%s\" stays hero-honest.\n\nMatch talents will spend that package on your tree and verify nodes. Check hero talent nodes in N afterward.\n\nContinue?",
  button1 = "Match",
  button2 = "Cancel",
  OnAccept = function(_, data)
    if not data or not UI then
      return
    end
    local opts = { confirmedProxy = true, editSeqIndex = data.editSeqIndex }
    if data.matchTalents and UI.MatchTalentsToList then
      UI.MatchTalentsToList(opts)
    elseif UI.ApplyTalentsToGame then
      UI.ApplyTalentsToGame(opts)
    end
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
  preferredIndex = 3,
}

-- Confirm remove one spell from the edit list
StaticPopupDialogs["SPELLSMASHER_DELETE_STEP"] = {
  text = "Remove \"%s\" from this pack?",
  button1 = "Remove",
  button2 = "Cancel",
  OnAccept = function(_, data)
  if not data or not NS.db then
  return
  end
  local s = NS.db.sequences and NS.db.sequences[NS.db.editSeqIndex or 1]
  local i = data.index
  if not s or not s.steps or not i or not s.steps[i] then
  return
  end
  table.remove(s.steps, i)
  NS.db.editStepIndex = math.max(1, math.min(NS.db.editStepIndex or 1, #s.steps))
  NS.Secure.Recompile()
  UI.Refresh()
  SetStatus("|cffff6b7aSpell removed.|r")
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
  preferredIndex = 3,
}

-- Rename a pack (sequence name on List / Edit)
StaticPopupDialogs["SPELLSMASHER_RENAME_PACK"] = {
  text = "Rename pack:",
  button1 = "Rename",
  button2 = "Cancel",
  hasEditBox = true,
  maxLetters = 64,
  OnShow = function(self, data)
  local eb = self.EditBox or self.editBox
  if not eb then
  return
  end
  local seq = NS.db and NS.db.sequences and data and NS.db.sequences[data.index]
  eb:SetText((seq and seq.name) or "")
  eb:HighlightText()
  eb:SetFocus()
  end,
  OnAccept = function(self, data)
  if not data or not NS.db or not NS.db.sequences then
  return
  end
  local seq = NS.db.sequences[data.index]
  if not seq then
  return
  end
  local eb = self.EditBox or self.editBox
  local name = NS.Trim((eb and eb:GetText()) or "")
  if name == "" then
  name = "Sequence"
  end
  seq.name = name
  SetStatus("|cff3dd68cRenamed:|r " .. name)
  UI.Refresh()
  end,
  EditBoxOnEnterPressed = function(self)
  local dialog = self:GetParent()
  if not dialog then
  return
  end
  local info = StaticPopupDialogs[dialog.which or "SPELLSMASHER_RENAME_PACK"]
  if info and info.OnAccept then
  info.OnAccept(dialog, dialog.data)
  end
  dialog:Hide()
  end,
  EditBoxOnEscapePressed = function(self)
  local dialog = self:GetParent()
  if dialog then
  dialog:Hide()
  end
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
  preferredIndex = 3,
}

local function MakeCard(parent)
  local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  -- Real tooltip backdrop, brand-tinted (texture shows through)
  Hex(f, { C.card[1], C.card[2], C.card[3], 0.88 }, C.border, 16, "panel")
  return f
end

local function MakeScrollEdit(parent)
  local card = MakeCard(parent)
  local scroll = CreateFrame("ScrollFrame", nil, card)
  scroll:SetPoint("TOPLEFT", 10, -10)
  scroll:SetPoint("BOTTOMRIGHT", -10, 10)
  -- Nested tooltip well for multi-line edit (real WoW bg, not solid fill)
  local sbgFrame = CreateFrame("Frame", nil, scroll, "BackdropTemplate")
  sbgFrame:SetAllPoints()
  sbgFrame:SetFrameLevel(math.max(1, (scroll:GetFrameLevel() or 1) - 1))
  Hex(sbgFrame, { C.elevated[1], C.elevated[2], C.elevated[3], 0.90 }, C.border, 16, "panel")
  scroll._sbgFrame = sbgFrame
  local eb = CreateFrame("EditBox", nil, scroll)
  eb:SetMultiLine(true)
  eb:SetFontObject(ChatFontNormal)
  eb:SetTextColor(C.text[1], C.text[2], C.text[3])
  eb:SetAutoFocus(false)
  eb:SetWidth(500)
  eb:SetHeight(240)
  eb:SetTextInsets(10, 10, 10, 10)
  eb:SetScript("OnEscapePressed", function(self)
  self:ClearFocus()
  end)
  eb:SetScript("OnMouseDown", function(self)
  self:SetFocus()
  end)
  scroll:SetScrollChild(eb)
  scroll:EnableMouse(true)
  scroll:SetScript("OnMouseDown", function()
  eb:SetFocus()
  end)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(self, delta)
  local cur = self:GetVerticalScroll() or 0
  self:SetVerticalScroll(math.max(0, cur - delta * 36))
  end)
  return card, eb
end

function SetStatus(text)
  if statusFS then
  statusFS:SetText(text or "")
  SetFS(statusFS, C.green)
  end
end

--- Help: how to get a missing pack back (does not change SavedVariables).
function UI.ShowLostSequenceHelp()
  local pop = ui.lostSeqPopup
  if not pop then
    pop = CreateFrame("Frame", "SpellSmasherLostSeqHelp", UIParent, "BackdropTemplate")
    ui.lostSeqPopup = pop
    pop:SetSize(560, 470)
    pop:SetPoint("CENTER")
    pop:SetFrameStrata("FULLSCREEN_DIALOG")
    pop:SetFrameLevel(260)
    pop:EnableMouse(true)
    pop:SetMovable(true)
    pop:RegisterForDrag("LeftButton")
    pop:SetScript("OnDragStart", pop.StartMoving)
    pop:SetScript("OnDragStop", pop.StopMovingOrSizing)
    Hex(pop, { 0.02, 0.025, 0.04, 0.95 }, C.green, 32, "dialog")
    ApplyPanelImage(pop, PANEL_BG, { inset = 11, alpha = 0.90 })

    local tFS = pop:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    tFS:SetPoint("TOPLEFT", 16, -14)
    tFS:SetText("Lost sequence?")
    SetFS(tFS, C.green)

    local close = MakeThemeButton(pop, {
      w = 90,
      h = 28,
      text = "Done",
      kind = "green",
      onClick = function()
        pop:Hide()
      end,
    })
    close:SetPoint("BOTTOMRIGHT", -14, 12)

    local goImport = MakeThemeButton(pop, {
      w = 110,
      h = 28,
      text = "Open Import",
      kind = "primary",
      tip = "Paste a !SpellSmasher! string",
      tipBody = "Import ADDS packs. It never wipes the ones you still have.",
      onClick = function()
        pop:Hide()
        if UI.Open then
          UI.Open()
        end
        if UI.ShowTab then
          UI.ShowTab("import")
        end
      end,
    })
    goImport:SetPoint("RIGHT", close, "LEFT", -8, 0)

    local scroll = CreateFrame("ScrollFrame", nil, pop)
    scroll:SetPoint("TOPLEFT", 16, -40)
    scroll:SetPoint("BOTTOMRIGHT", -18, 48)
    scroll:EnableMouseWheel(true)

    local child = CreateFrame("Frame", nil, scroll)
    child:SetSize(520, 720)
    scroll:SetScrollChild(child)
    scroll:SetScript("OnMouseWheel", function(self, delta)
      local cur = self:GetVerticalScroll() or 0
      local maxS = math.max(0, (child:GetHeight() or 0) - (self:GetHeight() or 1))
      self:SetVerticalScroll(math.max(0, math.min(maxS, cur - delta * 36)))
    end)

    local body = child:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    body:SetPoint("TOPLEFT", 0, 0)
    body:SetWidth(516)
    body:SetJustifyH("LEFT")
    body:SetJustifyV("TOP")
    body:SetSpacing(2)
    body:SetText(
      "An update does not delete packs. Lists live in your WoW account file, not in the zip.\n\n"
        .. "|cff3dd68c1) Check hidden, not deleted|r\n"
        .. "List tab  >  click |cffffff00All packs|r (not This class).\n"
        .. "Other-class lists stay on this account. Log into the same class you used before.\n\n"
        .. "|cff3dd68c2) Last session backup|r\n"
        .. "Fully exit WoW (not only /reload).\n"
        .. "Folder:  _retail_\\WTF\\Account\\<your account>\\SavedVariables\\\n"
        .. "Rename SpellSmasher.lua  >  SpellSmasher.lua.broken\n"
        .. "Copy SpellSmasher.lua.bak  >  SpellSmasher.lua\n"
        .. "Log in, then List  >  All packs.\n"
        .. ".bak is only the last logout. Later logouts replace it.\n\n"
        .. "|cff3dd68c3) You saved a pack string|r\n"
        .. "Import tab  >  paste a line that starts with |cff3dd68c!SpellSmasher!|r  >  Import.\n"
        .. "Import |cffffff00adds|r packs. It does not wipe what is still there.\n\n"
        .. "|cff3dd68c4) You built it on the website|r\n"
        .. "spellsmasher.com  >  Export SpellSmasher pack  >  Copy  >  Import here.\n\n"
        .. "|cff3dd68c5) No backup at all|r\n"
        .. "Set me up makes |cffffff00new|r Method ST + AoE for this character.\n"
        .. "That is not your old custom names or order.\n\n"
        .. "|cff6c8cffGoing forward|r\n"
        .. "List  >  Export all  >  Ctrl+C  >  paste into a text file. That string is the full backup."
    )
    SetFS(body, C.text)
    pop.bodyFS = body
    tinsert(UISpecialFrames, "SpellSmasherLostSeqHelp")
  end
  pop:SetFrameStrata("FULLSCREEN_DIALOG")
  pop:SetFrameLevel(260)
  pop:Show()
  SetStatus("|cff3dd68cLost sequence?|r  Follow the steps  -  Import adds, never wipes.")
end

local function PickActiveIndex(sequences)
  for i, s in ipairs(sequences) do
  if tostring(s.role or ""):lower() == "aoe" then
  return i
  end
  end
  return 1
end

-- -- actions -------------------------------------------------

local newState = { className = "Demon Hunter", spec = "Vengeance", source = "method", level = 90, hero = "", role = "aoe" }

local function SyncNewStateHero()
  if not SpellSmasherRebuild or not SpellSmasherRebuild.ListHeroes then
  newState.hero = newState.hero or ""
  return
  end
  local heroes = SpellSmasherRebuild.ListHeroes(newState.className, newState.spec) or {}
  if #heroes == 0 then
  newState.hero = ""
  return
  end
  local want = tostring(newState.hero or "")
  for _, h in ipairs(heroes) do
  if h == want then
  return
  end
  end
  newState.hero = heroes[1]
end

--- Prefer the logged-in character's class|spec for New pack.
local function SyncNewStateToPlayer()
  if not NS.GetPlayerClassSpec then
  SyncNewStateHero()
  return
  end
  local cn, sp = NS.GetPlayerClassSpec()
  if cn and cn ~= "" then
  newState.className = cn
  end
  if sp and sp ~= "" then
  newState.spec = sp
  elseif NS.SpecsForFlavor then
  local specs = NS.SpecsForFlavor(newState.className)
  newState.spec = specs[1] or ""
  elseif NS.SPECS[newState.className] and NS.SPECS[newState.className][1] then
  newState.spec = NS.SPECS[newState.className][1]
  end
  local lv = UnitLevel and UnitLevel("player") or newState.level
  -- Classic: use live level. Midnight: site/theme band is 70-90.
  if NS.IsClassicFlavor then
    if lv and lv >= 1 then
      newState.level = lv
    end
    if newState.className and NS.ClassAllowedOnFlavor and not NS.ClassAllowedOnFlavor(newState.className) then
      local _, classFile = UnitClass("player")
      newState.className = (NS.CLASS_FILE_TO_NAME and NS.CLASS_FILE_TO_NAME[classFile or ""]) or "Warrior"
      local specs = NS.SPECS[newState.className]
      newState.spec = (sp and sp ~= "") and sp or (specs and specs[1] or "")
      newState.hero = ""
    end
  elseif lv and lv >= 70 then
  newState.level = math.min(90, math.max(70, lv))
  end
  SyncNewStateHero()
end

--- Live character level (raw UnitLevel). Read-only honesty helpers - never mutate packs.
local function GetLivePlayerLevel()
  local lv = tonumber(UnitLevel and UnitLevel("player"))
  return (lv and lv >= 1) and math.floor(lv) or 1
end

--- Clamp to site pack band 70-90 (matches Create / theme characterLevel).
local function ClampPackLevelBand(lv)
  lv = math.floor(tonumber(lv) or 90)
  return math.min(90, math.max(70, lv))
end

--- Status suffix when exported pack(s) match you but stamped characterLevel differs.
--- Does not change the pack string - list and characterLevel stay as stored.
local function ExportLevelHonestySuffix(sequences)
  local live = GetLivePlayerLevel()
  if live < 70 then
  return ""
  end
  local liveBand = ClampPackLevelBand(live)
  local pClass, pSpec
  if NS.GetPlayerClassSpec then
  pClass, pSpec = NS.GetPlayerClassSpec()
  end
  if not pClass or pClass == "" then
  return ""
  end
  pClass = NS.Trim(pClass)
  pSpec = NS.Trim(pSpec or "")
  local mismatched = 0
  local examplePackLv = nil
  for _, s in ipairs(sequences or {}) do
  if type(s) == "table" then
  local cn = NS.Trim(s.className or "")
  local sp = NS.Trim(s.spec or "")
  if cn ~= "" and cn == pClass and (pSpec == "" or sp == pSpec) then
  local packLv = ClampPackLevelBand(s.characterLevel)
  if packLv ~= liveBand then
  mismatched = mismatched + 1
  if not examplePackLv then
  examplePackLv = packLv
  end
  end
  end
  end
  end
  if mismatched == 0 then
  return ""
  end
  if mismatched == 1 then
  return "  -  |cfff0b429Pack level "
  .. tostring(examplePackLv)
  .. " - you are "
  .. tostring(liveBand)
  .. " - list unchanged|r (level-legal lists on spellsmasher.com)"
  end
  return "  -  |cfff0b429"
  .. tostring(mismatched)
  .. " pack(s): level differs from you ("
  .. tostring(liveBand)
  .. ") - lists unchanged|r (level-legal on site)"
end

local function ShowTab(name)
  if name == "new" then
  SyncNewStateToPlayer()
  end
  if NS.db then
  NS.db.uiTab = name
  end
  UI.Refresh()
end
UI.ShowTab = ShowTab

local function DoImport()
  local raw = importBox and importBox:GetText() or ""
  if NS.Trim(raw) == "" then
  SetStatus("|cfff0b429Paste a |r|cff3dd68c!SpellSmasher!|r|cfff0b429 pack first.|r")
  return
  end
  local result, err = NS.Pack.Decode(raw)
  if not result then
  SetStatus("|cffff6b7aImport failed:|r " .. tostring(err))
  NS.Print("|cffff6b7aImport failed:|r " .. tostring(err))
  return
  end
  local db = NS.db
  db.sequences = db.sequences or {}
  local incoming = result.sequences or {}
  if #incoming == 0 then
  SetStatus("|cfff0b429Pack has no sequences.|r")
  return
  end

  -- Soft fill Method CUk when pack has spells but no loadout (smooth site  >  game)
  -- Classic: Method loadouts are not loaded. Midnight only.
  local filledLo = 0
  for _, seq in ipairs(incoming) do
  local lo = NS.Trim(seq.talentLoadout or seq.talents or "")
  if NS.IsClassicFlavor then
  -- keep spells; no CUk fill
  elseif #lo < 40 and seq.className and seq.spec and NS.AttachMethodLoadout then
  if NS.AttachMethodLoadout(seq, seq.heroTalent) then
  filledLo = filledLo + 1
  end
  elseif #lo < 40 and seq.className and seq.spec and SpellSmasherMethodLoadouts then
  local m = SpellSmasherMethodLoadouts.Get(seq.className, seq.spec, seq.heroTalent)
  if m and #m >= 40 then
  seq.talentLoadout = m
  seq.talents = m
  seq.talentLoadoutSource = "method"
  filledLo = filledLo + 1
  end
  end
  -- Normalize modes so Rev Pri / Priority survive import
  if type(seq.steps) == "table" then
  for _, st in ipairs(seq.steps) do
  if st then
  st.mode = NS.NormMode(st.mode)
  st.looped = st.mode ~= "once"
  end
  end
  end
  end

  -- APPEND - never wipe existing packs (old bug: db.sequences = result.sequences)
  local firstNew = #db.sequences + 1
  for _, seq in ipairs(incoming) do
  db.sequences[#db.sequences + 1] = seq
  end
  db.listSelectIndex = firstNew
  db.editSeqIndex = firstNew
  -- Import lands on the first new pack as Active (still appends; never wipes).
  db.activeIndex = firstNew
  local st = db.sequences[firstNew]
  if st then
  NS.Pack.ApplyHelpersFromPack(st)
  end
  db.aoeIndex = nil
  if not InCombatLockdown() then
  NS.Secure.Recompile()
  else
  NS.pendingRecompile = true
  end
  local nNew = #incoming
  local total = #db.sequences
  local first = db.sequences[firstNew]
  local stepN = first and first.steps and #first.steps or 0
  local loN = first and #NS.Trim(first.talentLoadout or first.talents or "") or 0
  local warnN = result.warnings and #result.warnings or 0
  local extra = ""
  if filledLo > 0 then
  extra = extra .. "  -  Method CUk filled for " .. filledLo
  end
  if loN >= 40 then
  extra = extra .. "  -  talents on pack"
  end
  if warnN > 0 then
  extra = extra .. "  -  " .. warnN .. " soft warning(s)"
  end
  SetStatus(
  "|cff3dd68cImported|r "
  .. nNew
  .. " pack(s)  -  "
  .. stepN
  .. " spells on first  -  "
  .. total
  .. " total (old packs kept)"
  .. extra
  )
  NS.Print(
  "Imported "
  .. nNew
  .. " pack(s) from site/export. First: |cff3dd68c"
  .. tostring(first and first.name or "?")
  .. "|r ("
  .. stepN
  .. " spells). List  >  Use  -  Edit  -  Copy talents if needed."
  )
  if result.warnings then
  for _, w in ipairs(result.warnings) do
  NS.Print("|cfff0b429Pack note:|r " .. tostring(w))
  end
  end
  ShowTab("list")
  if UI.Refresh then
  UI.Refresh()
  end
end

--- Export one pack or all.
--- opts.all = true  >  every sequence in one string (full backup)
--- default  >  list selection (listSelectIndex), else active, else first
local function DoExport(opts)
  opts = opts or {}
  local db = NS.db
  if not db or not db.sequences or #db.sequences == 0 then
  SetStatus("|cfff0b429Nothing to export.|r")
  return
  end

  local sequences
  local label
  if opts.all then
  sequences = db.sequences
  label = #sequences .. " packs (all)"
  else
  local idx = tonumber(db.listSelectIndex) or tonumber(db.activeIndex) or 1
  if idx < 1 or idx > #db.sequences then
  idx = 1
  end
  db.listSelectIndex = idx
  local seq = db.sequences[idx]
  if not seq then
  SetStatus("|cfff0b429Nothing to export.|r")
  return
  end
  sequences = { seq }
  label = seq.name or ("Pack " .. idx)
  end

  -- Pre-flight: same checks Decode will apply on the site / re-import
  local okV, warnings, hard = NS.Pack.ValidateSequences(sequences)
  if not okV then
  SetStatus("|cffff6b7aExport blocked:|r " .. tostring(hard))
  NS.Print("|cffff6b7aExport blocked:|r " .. tostring(hard))
  return
  end

  -- Encode with live CUk in the export string only (matching class|spec).
  -- Does not overwrite Method talent strings stored on packs in the DB.
  local pack, snapped = NS.Pack.Encode(sequences, NS.VERSION, { snapshotLive = true })
  snapped = tonumber(snapped) or 0
  if not pack or pack == "" or not pack:find("!SpellSmasher!", 1, true) then
  SetStatus("|cffff6b7aExport failed:|r empty pack string.")
  return
  end
  -- Round-trip self-check: encode  >  decode must yield same step counts
  local back, backErr = NS.Pack.Decode(pack)
  if not back then
  SetStatus("|cffff6b7aExport self-check failed:|r " .. tostring(backErr))
  NS.Print("|cffff6b7aExport self-check failed:|r " .. tostring(backErr))
  return
  end
  local stepOut = 0
  for _, s in ipairs(sequences) do
  stepOut = stepOut + #(s.steps or {})
  end
  local stepBack = 0
  for _, s in ipairs(back.sequences or {}) do
  stepBack = stepBack + #(s.steps or {})
  end
  if stepBack < stepOut then
  SetStatus(
  "|cffff6b7aExport self-check:|r steps "
  .. stepBack
  .. " < "
  .. stepOut
  .. " - pack may be truncated."
  )
  NS.Print("Export self-check warning: step count dropped on re-decode.")
  end

  ShowTab("import")
  if importBox then
  importBox:SetText(pack)
  importBox:SetFocus()
  importBox:HighlightText(0, -1)
  end
  local talentNote
  if snapped > 0 then
  talentNote = "  -  |cff3dd68clive talents attached|r (" .. snapped .. ")"
  else
  talentNote = "  -  pack/Method talents (match class|spec for live CUk)"
  end
  local warnNote = ""
  if warnings and #warnings > 0 then
  warnNote = "  -  " .. #warnings .. " note(s)"
  end
  -- Read-only honesty: pack characterLevel / spell list are not rewritten on Export.
  local levelNote = ExportLevelHonestySuffix(sequences)
  SetStatus(
  "|cff3dd68cExported:|r "
  .. label
  .. "  -  Ctrl+C ("
  .. #pack
  .. " chars)  -  "
  .. stepOut
  .. " spells OK"
  .. talentNote
  .. warnNote
  .. levelNote
  )
  NS.Print(
  "Exported |cff3dd68c"
  .. label
  .. "|r ("
  .. stepOut
  .. " spells, "
  .. #pack
  .. " chars) - Ctrl+C in the Import box  >  paste on spellsmasher.com."
  .. (snapped > 0 and (" Live talents on " .. snapped .. " pack(s).") or "")
  )
  if levelNote ~= "" then
  NS.Print(
  "|cfff0b429Export note:|r Pack characterLevel may differ from your live level - "
  .. "spell list is unchanged. Level-legal lists: spellsmasher.com."
  )
  end
  if warnings then
  for _, w in ipairs(warnings) do
  NS.Print("|cfff0b429Export note:|r " .. tostring(w))
  end
  end
  if snapped > 0 and UI.Refresh then
  UI.Refresh()
  end
end

local function DoCreatePack(opts)
  opts = type(opts) == "table" and opts or {}
  if not SpellSmasherRebuild then
  SetStatus("Rebuild module missing.")
  return
  end
  -- Honor the New-tab chips. Do not overwrite class/spec/role with the logged-in toon.
  SyncNewStateHero()
  local seq, packId, msg = SpellSmasherRebuild.CreatePack(
  newState.className,
  newState.spec,
  newState.level,
  newState.source,
  newState.hero,
  newState.role or "aoe"
  )
  if not seq then
  SetStatus("|cffff6b7a" .. tostring(msg or packId) .. "|r")
  return
  end
  local db = NS.db
  db.sequences = db.sequences or {}
  -- APPEND - do not wipe packs already on this character
  db.sequences[#db.sequences + 1] = seq
  local idx = #db.sequences
  db.activeIndex = idx
  db.listSelectIndex = idx
  db.editSeqIndex = idx
  db.editStepIndex = 1
  db.aoeIndex = nil
  NS.Secure.Recompile()
  SetStatus("|cff3dd68c" .. (msg or "Created pack") .. "|r  -  " .. idx .. " pack(s) on list")
  NS.Print((msg or "Created pack") .. " (added - existing packs kept).")
  local lo = seq.talentLoadout and #tostring(seq.talentLoadout) or 0
  if lo >= 40 then
  NS.Print(
  "|cff3dd68cMethod talent package|r on pack - Edit  >  |cffffff00Match talents|r (or Copy  >  N  >  Import)."
  )
  end
  if not opts.skipTab then
  if opts.goEdit then
  ShowTab("editor")
  else
  ShowTab("list")
  end
  end
  return idx
end

local function DoSetMeUp()
  -- Two Method packs (ST + AoE), two list rows. Not Shift. Not one pack with two lists.
  -- Does not spend talents and does not touch mash/compile internals.
  SyncNewStateToPlayer()
  newState.source = "method"
  local want = tostring(newState.role or "aoe"):lower()
  if want ~= "st" then
  want = "aoe"
  end

  newState.role = "st"
  local stIdx = DoCreatePack({ skipTab = true })
  newState.role = "aoe"
  local aoIdx = DoCreatePack({ skipTab = true })
  newState.role = want

  local db = NS.db
  local active = want == "st" and stIdx or aoIdx
  if not active then
  active = aoIdx or stIdx
  end
  if active and db then
  db.activeIndex = active
  db.listSelectIndex = active
  db.editSeqIndex = active
  db.editStepIndex = 1
  db.aoeIndex = nil
  if NS.Secure and NS.Secure.Recompile and not InCombatLockdown() then
  NS.Secure.Recompile()
  elseif NS.Secure then
  NS.pendingRecompile = true
  end
  end

  if not stIdx and not aoIdx then
  return
  end
  local bits = {}
  if stIdx then
  bits[#bits + 1] = "ST"
  end
  if aoIdx then
  bits[#bits + 1] = "AoE"
  end
  SetStatus(
  "|cff3dd68cSet me up|r - "
  .. table.concat(bits, " + ")
  .. " Method packs. Active = "
  .. (want == "st" and "ST" or "AoE")
  .. ". Matching talents..."
  )
  ShowTab("editor")
  -- One tree on this character. Match the Active pack only.
  -- Defer off this click stack (same rule as Match itself) - do not spend on the create click.
  if NS.IsClassicFlavor then
    SetStatus(
      "|cff3dd68cSet me up|r - "
        .. (NS.FlavorLabel and NS.FlavorLabel() or "Classic")
        .. " ST + AoE lists. No Match talents (no loadout strings)."
    )
    ShowTab("list")
    return
  end
  if InCombatLockdown and InCombatLockdown() then
  SetStatus(
  "|cff3dd68cSet me up|r - packs ready. Leave combat, then |cffffff00Match talents|r."
  )
  return
  end
  local function runMatch()
  if UI.MatchTalentsToList then
  UI.MatchTalentsToList()
  end
  end
  if C_Timer and C_Timer.After then
  C_Timer.After(0.2, runMatch)
  else
  runMatch()
  end
end

local function DoRebuild()
  local db = NS.db
  local seq = db.sequences[db.editSeqIndex or db.activeIndex or 1]
  if not seq or not SpellSmasherRebuild then
  SetStatus("No sequence to rebuild.")
  return
  end
  local ok, msg = SpellSmasherRebuild.RebuildInPlace(seq, db.sequences, {
  source = seq.packSource or "method",
  keepExtras = false,
  role = seq.role or "aoe",
  })
  if ok then
  NS.Secure.Recompile()
  SetStatus("|cff3dd68c" .. tostring(msg) .. "|r")
  UI.Refresh()
  else
  SetStatus("|cffff6b7a" .. tostring(msg) .. "|r")
  end
end

-- -- refresh -------------------------------------------------

function UI.Refresh()
  if not mainFrame or not NS.db then
  return
  end
  if UI.ApplyFlavorChrome then
  UI.ApplyFlavorChrome()
  end
  if UI.UpdateTalentApplyEnabled then
  UI.UpdateTalentApplyEnabled()
  end
  local db = NS.db
  local tab = db.uiTab or "list"

  for id, btn in pairs(tabButtons) do
  btn:SetActive(id == tab)
  end

  if ui.listPanel then
  ui.listPanel:SetShown(tab == "list")
  end
  if ui.newPanel then
  ui.newPanel:SetShown(tab == "new")
  end
  if ui.importPanel then
  ui.importPanel:SetShown(tab == "import")
  end
  if ui.editPanel then
  ui.editPanel:SetShown(tab == "editor")
  end

  -- list rows (optionally only packs for the logged-in character's class)
  -- Rows are keyed by sequence index, which is sparse when "This class" filter
  -- hides other classes - must use pairs() to hide, not ipairs() (stops at first hole).
  ui.listRows = ui.listRows or {}
  local totalCount = #(db.sequences or {})
  for k, row in pairs(ui.listRows) do
  if type(row) == "table" and row.Hide then
  row:Hide()
  end
  -- Drop frames for sequence indices that no longer exist after delete
  if type(k) == "number" and k > totalCount then
  ui.listRows[k] = nil
  end
  end
  local filterClass = db.listFilterThisClass ~= false
  local pClass = select(1, NS.GetPlayerClassSpec())
  local shownCount = 0
  local y = 0
  for i, s in ipairs(db.sequences or {}) do
  local matchClass = true
  if filterClass and pClass and pClass ~= "" then
  if NS.PackMatchesPlayerClass then
  matchClass = NS.PackMatchesPlayerClass(s) and true or false
  elseif NS.ClassNameEquals then
  matchClass = NS.ClassNameEquals(s.className, pClass) and true or false
  end
  end
  if matchClass then
  local row = ui.listRows[i]
  if not row then
  row = MakeCard(ui.listScrollChild)
  row:SetHeight(56)
  -- left accent bar
  local accent = row:CreateTexture(nil, "ARTWORK")
  accent:SetPoint("TOPLEFT", 0, 0)
  accent:SetPoint("BOTTOMLEFT", 0, 0)
  accent:SetWidth(4)
  accent:SetColorTexture(C.green[1], C.green[2], C.green[3], 1)
  row.accent = accent

  local nameFS = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  nameFS:SetPoint("TOPLEFT", 14, -10)
  nameFS:SetJustifyH("LEFT")
  row.nameFS = nameFS

  local metaFS = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  metaFS:SetPoint("BOTTOMLEFT", 14, 10)
  metaFS:SetJustifyH("LEFT")
  row.metaFS = metaFS

  -- Right side: Use | Edit | Rename | Dup | Delete
  local delB = MakeThemeButton(row, {
  w = 64,
  h = 26,
  text = "Delete",
  kind = "danger",
  tip = "Delete this pack",
  tipBody = "Asks for confirmation. You can re-import or New pack later.",
  onClick = function()
  local name = (NS.db.sequences[row.index] and NS.db.sequences[row.index].name) or "pack"
  StaticPopup_Show("SPELLSMASHER_DELETE_PACK", name, nil, { index = row.index })
  end,
  })
  delB:SetPoint("RIGHT", -8, 0)
  row.delB = delB

  local dupB = MakeThemeButton(row, {
  w = 48,
  h = 26,
  text = "Dup",
  kind = "secondary",
  tip = "Duplicate pack",
  tipBody = "Inserts a full copy of this sequence right below it (name + \" copy\").",
  onClick = function()
  local db = NS.db
  local idx = row.index
  local seq = db and db.sequences and db.sequences[idx]
  if not seq then
  return
  end
  -- MUST deep-copy: CopyTable(seq, true) is shallow in retail and
  -- shared the steps table - editing the Dup also changed the original.
  local copy = NS.DeepCopy(seq)
  copy.name = (seq.name or "Sequence") .. " copy"
  -- Break any accidental shared identity
  copy.packId = nil
  table.insert(db.sequences, idx + 1, copy)
  -- Insert shifts every index > idx up by 1. Keep Active on the same pack.
  if (db.activeIndex or 1) > idx then
  db.activeIndex = (db.activeIndex or 1) + 1
  end
  if db.aoeIndex and (tonumber(db.aoeIndex) or 0) > idx then
  db.aoeIndex = (tonumber(db.aoeIndex) or 0) + 1
  end
  -- Open the new pack so edits clearly apply to the copy
  db.editSeqIndex = idx + 1
  db.listSelectIndex = idx + 1
  SetStatus("|cff3dd68cDuplicated:|r " .. (copy.name or "?") .. " (independent copy)")
  UI.Refresh()
  end
  })
  dupB:SetPoint("RIGHT", delB, "LEFT", -4, 0)
  row.dupB = dupB

  local renB = MakeThemeButton(row, {
  w = 64,
  h = 26,
  text = "Rename",
  kind = "secondary",
  tip = "Rename pack",
  tipBody = "Change the display name (e.g. Vengeance Method copy  >  My M+ list).",
  onClick = function()
  NS.db.listSelectIndex = row.index
  StaticPopup_Show("SPELLSMASHER_RENAME_PACK", nil, nil, { index = row.index })
  end,
  })
  renB:SetPoint("RIGHT", dupB, "LEFT", -4, 0)
  row.renB = renB

  local editB = MakeThemeButton(row, {
  w = 52,
  h = 26,
  text = "Edit",
  kind = "secondary",
  tip = "Edit spells in this pack",
  onClick = function()
  NS.db.listSelectIndex = row.index
  NS.db.editSeqIndex = row.index
  NS.db.editStepIndex = 1
  ShowTab("editor")
  end,
  })
  editB:SetPoint("RIGHT", renB, "LEFT", -4, 0)
  row.editB = editB

  local useB = MakeThemeButton(row, {
  w = 64,
  h = 26,
  text = "Use",
  kind = "green",
  tip = "Mash this list",
  tipBody = "Sets this pack as Active (one-button sequence).",
  onClick = function()
  NS.db.listSelectIndex = row.index
  NS.db.activeIndex = row.index
  NS.db.aoeIndex = nil
  NS.Secure.Recompile()
  SetStatus("|cff3dd68cActive:|r " .. (NS.db.sequences[row.index].name or "?"))
  UI.Refresh()
  end,
  })
  useB:SetPoint("RIGHT", editB, "LEFT", -4, 0)
  row.useB = useB
  -- Spotlight tour targets (first visible / selected row preferred later)
  if not ui.tourUseBtn or row.index == (NS.db.listSelectIndex or NS.db.activeIndex or 1) then
  ui.tourUseBtn = useB
  ui.tourEditBtn = editB
  end

  -- Click row body to select for Export (does not change Active)
  row:EnableMouse(true)
  row:SetScript("OnMouseDown", function()
  NS.db.listSelectIndex = row.index
  SetStatus(
  "|cff6c8cffSelected for export:|r "
  .. ((NS.db.sequences[row.index] and NS.db.sequences[row.index].name) or "?")
  .. "  -  Export = this pack only"
  )
  UI.Refresh()
  end)

  -- keep pack name from running under the buttons
  nameFS:ClearAllPoints()
  nameFS:SetPoint("TOPLEFT", 14, -10)
  nameFS:SetPoint("RIGHT", useB, "LEFT", -8, 0)
  nameFS:SetWordWrap(false)
  metaFS:ClearAllPoints()
  metaFS:SetPoint("BOTTOMLEFT", 14, 10)
  metaFS:SetPoint("RIGHT", useB, "LEFT", -8, 0)
  metaFS:SetWordWrap(false)

  ui.listRows[i] = row
  end
  row.index = i
  row:ClearAllPoints()
  row:SetPoint("TOPLEFT", ui.listScrollChild, "TOPLEFT", 2, -y)
  row:SetPoint("TOPRIGHT", ui.listScrollChild, "TOPRIGHT", -2, -y)
  row:Show()

  local active = (i == (db.activeIndex or 1))
  local selected = (i == (tonumber(db.listSelectIndex) or tonumber(db.activeIndex) or 1))
  local nSteps = s.steps and #s.steps or 0
  local nameBits = s.name or "Sequence"
  -- ASCII only (Unicode bullets draw as empty boxes in WoW fonts)
  if active then
  nameBits = nameBits .. "  |cff3dd68c* ACTIVE|r"
  end
  if selected and not active then
  nameBits = nameBits .. "  |cff6c8cff* export|r"
  elseif selected and active then
  nameBits = nameBits .. "  |cff6c8cff* export|r"
  end
  row.nameFS:SetText(nameBits)
  SetFS(row.nameFS, C.text)
  row.metaFS:SetText(
  tostring(s.className or "")
  .. "  -  "
  .. tostring(s.spec or "")
  .. "  -  "
  .. (tostring(s.role or ""):lower() == "st" and "ST" or "AoE")
  .. "  -  "
  .. nSteps
  .. " spells  -  "
  .. tostring(s.packSource or "custom")
  )
  SetFS(row.metaFS, C.muted)
  if active then
  Hex(row, { C.green[1], C.green[2], C.green[3], 0.28 }, C.green, 16, "panel")
  row.accent:SetColorTexture(C.green[1], C.green[2], C.green[3], 1)
  row.useB:SetKind("green")
  row.useB:SetThemeText("Active")
  elseif selected then
  Hex(row, { C.accent[1], C.accent[2], C.accent[3], 0.28 }, C.accent, 16, "panel")
  row.accent:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 1)
  row.useB:SetKind("secondary")
  row.useB:SetThemeText("Use")
  else
  Hex(row, { C.card[1], C.card[2], C.card[3], 0.88 }, C.border, 16, "panel")
  row.accent:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.45)
  row.useB:SetKind("secondary")
  row.useB:SetThemeText("Use")
  end
  y = y + 64
  shownCount = shownCount + 1
  end -- if matchClass
  end
  if ui.listScrollChild then
  ui.listScrollChild:SetHeight(math.max(120, y + 8))
  end
  if ui.emptyList then
  if totalCount == 0 then
  if NS.IsClassicFlavor then
  ui.emptyList:SetText(
  "No packs yet.\n|cff8b96adSet me up|r  (ST + AoE for this class)  or  |cff8b96adImport|r"
  )
  else
  ui.emptyList:SetText(
  "No packs yet.\n|cff8b96adSet me up|r  or  |cff8b96adImport|r a site string"
  )
  end
  ui.emptyList:Show()
  elseif shownCount == 0 and filterClass then
  ui.emptyList:SetText(
  "No packs for |cff3dd68c"
  .. tostring(pClass or "this class")
  .. "|r.\n|cff8b96adNew pack|r for this class, or toggle |cffffff00All packs|r above."
  )
  ui.emptyList:Show()
  else
  ui.emptyList:Hide()
  end
  end
  if ui.listFilterBtn then
  if filterClass then
  ui.listFilterBtn:SetThemeText("This class")
  ui.listFilterBtn:SetKind("green")
  else
  ui.listFilterBtn:SetThemeText("All packs")
  ui.listFilterBtn:SetKind("secondary")
  end
  end
  if ui.listHead then
  if filterClass and pClass then
  ui.listHead:SetText("Your packs  -  " .. pClass)
  else
  ui.listHead:SetText("Your packs  -  all classes")
  end
  end
  if ui.listHint then
  if filterClass then
  ui.listHint:SetText(
  "|cff8b96adShowing "
  .. shownCount
  .. "/"
  .. totalCount
  .. "  -  Export = selected  -  Use = mash|r"
  )
  else
  ui.listHint:SetText("|cff8b96adAll classes  -  Export = selected  -  Use = mash|r")
  end
  end

  -- editor
  if tab == "editor" and ui.editPanel then
  local si = tonumber(db.editSeqIndex) or 1
  local seq = db.sequences[si]
  if not seq then
  if ui.packNameBox then
  if not (ui.packNameBox.HasFocus and ui.packNameBox:HasFocus()) then
  ui.packNameBox:SetText("No sequence selected")
  end
  elseif ui.editTitle and ui.editTitle.SetText then
  ui.editTitle:SetText("No sequence selected")
  end
  if ui.stepHost then
  for _, r in ipairs(ui.stepCards or {}) do
  r:Hide()
  end
  for _, h in ipairs(ui.modeHeaders or {}) do
  h:Hide()
  end
  end
  else
  if ui.packNameBox then
  if not (ui.packNameBox.HasFocus and ui.packNameBox:HasFocus()) then
  ui.packNameBox:SetText(seq.name or "Sequence")
  end
  elseif ui.editTitle and ui.editTitle.SetText then
  ui.editTitle:SetText(seq.name or "Sequence")
  end
  ui.editSub:SetText(
  tostring(seq.className or "")
  .. " / "
  .. tostring(seq.spec or "")
  .. "  -  "
  .. (#(seq.steps or {}))
  .. " spells  -  "
  .. (tostring(seq.role or ""):lower() == "st" and "ST list" or "AoE list")
  )
  SetFS(ui.editSub, C.muted)
  if ui.editRoleChips then
  local rr = tostring(seq.role or "aoe"):lower()
  if rr ~= "st" then
  rr = "aoe"
  end
  for rid, chip in pairs(ui.editRoleChips) do
  chip:SetActive(rid == rr)
  end
  end

  local lo = NS.Trim(seq.talentLoadout or seq.talents or "")
  if ui.talentBox then
  if not (ui.talentBox.HasFocus and ui.talentBox:HasFocus()) then
  ui.talentBox:SetText(lo)
  if ui.talentBox.SetCursorPosition then
  ui.talentBox:SetCursorPosition(0)
  end
  end
  end
  if ui.talentHint then
  if #lo >= 40 then
  local src = seq.talentLoadoutSource or ""
  local heroLab = NS.Trim(seq.heroTalent or "")
  local ready = "|cff3dd68cpackage ready (" .. tostring(#lo) .. " chars)|r - "
  if src == "live" then
  ui.talentHint:SetText(
    ready .. "|cff3dd68cYour live talents|r - |cffffff00Match talents|r re-spends THIS snapshot (not Method gold)."
  )
  elseif (src == "method-proxy" or seq.talentLoadoutProxy) and src ~= "live" and src ~= "custom" then
  ui.talentHint:SetText(
    ready
      .. "|cfff0b429Shared Method gold (proxy)|r"
      .. (heroLab ~= "" and (" - hero list " .. heroLab) or "")
      .. " - |cffffff00Match talents|r confirms first; check hero nodes in N after"
  )
  elseif src == "method" or seq.packSource == "method" or seq.packSource == "engine" then
  if seq.talentLoadoutHeroExact and heroLab ~= "" then
  ui.talentHint:SetText(
    ready .. "|cff3dd68cMethod loadout for " .. heroLab .. "|r - press |cffffff00Match talents|r"
  )
  else
  ui.talentHint:SetText(
    ready .. "|cff3dd68cMethod loadout for this spell list|r - press |cffffff00Match talents|r"
  )
  end
  elseif src == "custom" then
  ui.talentHint:SetText(ready .. "|cff6c8cffCustom package|r - |cffffff00Match talents|r spends it")
  else
  ui.talentHint:SetText(ready .. "|cffffff00Match talents|r or Copy > N > Import")
  end
  else
  ui.talentHint:SetText("|cfff0b429No talent package|r - |cffffff00Match talents|r attaches Method gold")
  end
  end

  -- step cards
  ui.stepCards = ui.stepCards or {}
  ui.modeHeaders = ui.modeHeaders or {}
  for _, r in ipairs(ui.stepCards) do
  r:Hide()
  end
  for _, h in ipairs(ui.modeHeaders) do
  h:Hide()
  end
  -- Row pitch + light padding (enough to fully show first/last, not a huge empty tail)
  -- Mode blocks get a theme-style group header + gap (Once / In order / Priority / Rev Pri).
  -- Taller rows so top/bottom parchment edges show (not a flat strip)
  local ROW_H = 62
  local HDR_H = 22
  local GROUP_GAP = 12
  local PAD_TOP = 4
  local PAD_BOT = 16
  local ROW_X_INSET = 4 -- side gap so left/right paper edges aren't clipped
  ui._stepRowH = ROW_H
  local sy = PAD_TOP
  local stepTops = {} -- card top Y offset per step index (for scroll-to-selection)
  local headerIdx = 0
  local prevMode = nil
  local selIdx = tonumber(db.editStepIndex) or 1
  local nSteps = #(seq.steps or {})
  if nSteps > 0 then
  selIdx = math.max(1, math.min(selIdx, nSteps))
  db.editStepIndex = selIdx
  end
  if ui.stepHead then
  local selSt = seq.steps and seq.steps[selIdx]
  local selName = selSt and (selSt.label or "Spell") or "-"
  local n = #(seq.steps or {})
  -- Fit inside bar width: truncate selected name so the line doesn't clip
  local maxName = 28
  if ui.stepHeadBar and ui.stepHeadBar.GetWidth then
  local bw = ui.stepHeadBar:GetWidth() or 400
  -- ~6.5px per char at small font; leave room for "SPELL LIST (nn) - "
  maxName = math.max(10, math.floor((bw - 150) / 7))
  end
  selName = tostring(selName)
  if #selName > maxName then
  selName = selName:sub(1, math.max(1, maxName - 1)) .. "..."
  end
  ui.stepHead:SetText(
  "SPELL LIST ("
  .. n
  .. ")  -  |cff3dd68c"
  .. selName
  .. "|r"
  )
  end
  if ui.stepHelp then
  ui.stepHelp:SetText("|cff8b96adOnce - In order - Priority - Rev Pri - xN loops|r")
  end
  -- Recompute triangle weights once per refresh (Nx + position  >  % of Priority clicks)
  if NS.Compile and NS.Compile.ComputeStepWeightPct then
  ui._stepWeightPct = NS.Compile.ComputeStepWeightPct(seq)
  else
  ui._stepWeightPct = {}
  end
  local function ModeGroupTitle(mode, count)
  if mode == "once" then
  if count == 1 then
  return "ONCE - per combat"
  end
  return "ONCE - per combat (" .. count .. ")"
  elseif mode == "sequential" then
  return "IN ORDER - " .. count .. " step" .. (count == 1 and "" or "s")
  elseif mode == "reverse_priority" then
  return "REV PRI - " .. count .. " step" .. (count == 1 and "" or "s")
  else
  return "PRIORITY - " .. count .. " step" .. (count == 1 and "" or "s")
  end
  end
  local function ModeGroupColor(mode)
  if mode == "once" then
  return C.gold
  elseif mode == "sequential" then
  return C.accent
  elseif mode == "reverse_priority" then
  return C.accent
  else
  return C.green
  end
  end
  -- Count contiguous same-mode lengths for header labels
  local modeRunLen = {}
  do
  local i = 1
  local steps = seq.steps or {}
  while i <= #steps do
  local m = NS.NormMode(steps[i].mode)
  local j = i
  while j <= #steps and NS.NormMode(steps[j].mode) == m do
  j = j + 1
  end
  local len = j - i
  for k = i, j - 1 do
  modeRunLen[k] = len
  end
  i = j
  end
  end
  for i, st in ipairs(seq.steps or {}) do
  local mode = NS.NormMode(st.mode)
  -- Theme parity: new mode block = header + gap (not one flat list)
  if mode ~= prevMode then
  if prevMode ~= nil then
  sy = sy + GROUP_GAP
  end
  headerIdx = headerIdx + 1
  local hdr = ui.modeHeaders[headerIdx]
  if not hdr then
  hdr = CreateFrame("Frame", nil, ui.stepHost)
  hdr:SetHeight(HDR_H)
  local bar = hdr:CreateTexture(nil, "ARTWORK")
  bar:SetPoint("TOPLEFT", 0, -3)
  bar:SetPoint("BOTTOMLEFT", 0, 3)
  bar:SetWidth(3)
  hdr.bar = bar
  local bg = hdr:CreateTexture(nil, "BACKGROUND")
  bg:SetAllPoints()
  bg:SetColorTexture(1, 1, 1, 0.03)
  hdr.bg = bg
  local fs = hdr:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  fs:SetPoint("LEFT", 10, 0)
  fs:SetPoint("RIGHT", -8, 0)
  fs:SetJustifyH("LEFT")
  hdr.label = fs
  ui.modeHeaders[headerIdx] = hdr
  end
  local col = ModeGroupColor(mode)
  local count = modeRunLen[i] or 1
  hdr.label:SetText(ModeGroupTitle(mode, count))
  SetFS(hdr.label, col)
  if hdr.bar then
  hdr.bar:SetColorTexture(col[1], col[2], col[3], 1)
  end
  hdr:ClearAllPoints()
  hdr:SetPoint("TOPLEFT", ui.stepHost, "TOPLEFT", 0, -sy)
  hdr:SetPoint("TOPRIGHT", ui.stepHost, "TOPRIGHT", 0, -sy)
  hdr:SetHeight(HDR_H)
  hdr:Show()
  sy = sy + HDR_H
  prevMode = mode
  end

  local card = ui.stepCards[i]
  if not card then
  -- Adventure Guide-style parchment PAGE (real image, not stretched gold bar)
  card = CreateFrame("Frame", nil, ui.stepHost, "BackdropTemplate")
  local L0 = NS.LEATHER or {}
  HexLeather(card, nil, L0.edge or { 0.50, 0.32, 0.12, 1 }, false)
  card:SetHeight(ROW_H - 6)
  card._leather = true

  -- Optional left selection stripe (hidden by default - was reading as a line on parchment)
  local accent = card:CreateTexture(nil, "ARTWORK")
  accent:SetPoint("TOPLEFT", 4, -4)
  accent:SetPoint("BOTTOMLEFT", 4, 4)
  accent:SetWidth(3)
  accent:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.5)
  accent:Hide()
  card.accent = accent

  -- White halo shadow so black text pops on gold parchment
  local function inkShadow(fs)
  if fs and fs.SetShadowColor then
  fs:SetShadowColor(1, 1, 1, 0.85)
  fs:SetShadowOffset(1, -1)
  end
  end

  local num = card:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
  num:SetPoint("LEFT", 8, 0)
  num:SetWidth(22)
  num:SetJustifyH("CENTER")
  inkShadow(num)
  card.num = num

  -- Spell icon (game art via C_Spell - same path as mash-button live icon)
  local iconFrame = CreateFrame("Frame", nil, card, "BackdropTemplate")
  iconFrame:SetSize(32, 32)
  iconFrame:SetPoint("LEFT", num, "RIGHT", 6, 0)
  iconFrame:SetFrameLevel((card:GetFrameLevel() or 1) + 8)
  iconFrame:EnableMouse(true)
  if iconFrame.SetBackdrop then
  iconFrame:SetBackdrop({
  edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
  edgeSize = 10,
  insets = { left = 2, right = 2, top = 2, bottom = 2 },
  })
  iconFrame:SetBackdropBorderColor(0.20, 0.55, 0.40, 1)
  iconFrame:SetBackdropColor(0.02, 0.03, 0.05, 0.85)
  end
  local iconTex = iconFrame:CreateTexture(nil, "OVERLAY")
  iconTex:SetPoint("TOPLEFT", 3, -3)
  iconTex:SetPoint("BOTTOMRIGHT", -3, 3)
  iconTex:SetTexture(134400) -- INV_Misc_QuestionMark fallback
  iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
  card.iconFrame = iconFrame
  card.iconTex = iconTex
  card._spellTip = nil -- { spellID, name, macro }

  local function ShowStepSpellTip(owner)
  local tip = card._spellTip
  if not tip then
  return
  end
  GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
  GameTooltip:ClearLines()
  local shown = false
  if tip.spellID and tip.spellID > 0 and GameTooltip.SetSpellByID then
  pcall(function()
  GameTooltip:SetSpellByID(tip.spellID)
  shown = true
  end)
  end
  if not shown then
  GameTooltip:AddLine(tip.name or "Spell", C.green[1], C.green[2], C.green[3])
  if tip.macro and tip.macro ~= "" then
  GameTooltip:AddLine(tip.macro, 0.75, 0.78, 0.85, true)
  end
  GameTooltip:AddLine(" ", 1, 1, 1)
  GameTooltip:AddLine("Spell data not in client cache (wrong class or unknown name).", 0.55, 0.58, 0.65, true)
  end
  -- Always add mode/pack context under Blizzard spell text
  if tip.mode then
  GameTooltip:AddLine(" ", 1, 1, 1)
  GameTooltip:AddLine("Mode: " .. tostring(tip.mode), C.accent[1], C.accent[2], C.accent[3])
  end
  if tip.weight then
  GameTooltip:AddLine("Priority share: " .. tostring(tip.weight) .. "%", C.muted[1], C.muted[2], C.muted[3])
  end
  GameTooltip:Show()
  end

  iconFrame:SetScript("OnEnter", function(self)
  ShowStepSpellTip(self)
  end)
  iconFrame:SetScript("OnLeave", function()
  GameTooltip:Hide()
  end)

  local nameFS = card:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  nameFS:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 8, -2)
  nameFS:SetJustifyH("LEFT")
  nameFS:SetWordWrap(false)
  inkShadow(nameFS)
  card.nameFS = nameFS

  local macFS = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  macFS:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 8, -18)
  macFS:SetJustifyH("LEFT")
  macFS:SetWordWrap(false)
  inkShadow(macFS)
  card.macFS = macFS

  -- Per-spell weight: - / Nx / +  (loopRepeat in Priority / Sequential)
  local function getStep()
  local s = NS.db.sequences[NS.db.editSeqIndex or 1]
  return s and s.steps and s.steps[card.index]
  end
  local function setRep(n)
  local step = getStep()
  if not step then
  return
  end
  n = math.max(1, math.min(20, math.floor(tonumber(n) or 1)))
  if n <= 1 then
  step.loopRepeat = nil
  else
  step.loopRepeat = n
  end
  NS.Secure.Recompile()
  UI.Refresh()
  SetStatus(
  "|cff3dd68c"
  .. (step.label or "Spell")
  .. "|r "
  .. n
  .. "x - that many times in the mash click list."
  )
  end

  -- Save footer fields, then swap two steps (prevents label box overwriting after move).
  local function MoveStep(delta)
  if UI.ApplyStepFields then
  UI.ApplyStepFields({
  stepIndex = NS.db.editStepIndex,
  refresh = false,
  status = false,
  fromLabel = true,
  force = true,
  })
  end
  local s = NS.db.sequences[NS.db.editSeqIndex or 1]
  if not s or not s.steps then
  return
  end
  local idx = card.index
  local j = idx + delta
  if j < 1 or j > #s.steps then
  return
  end
  s.steps[idx], s.steps[j] = s.steps[j], s.steps[idx]
  NS.db.editStepIndex = j
  if UI._setSuppressFieldApply then
  UI._setSuppressFieldApply(true)
  end
  NS.Secure.Recompile()
  UI.Refresh()
  if UI._setSuppressFieldApply then
  UI._setSuppressFieldApply(false)
  end
  SetStatus("|cff3dd68cMoved|r " .. tostring((s.steps[j] and s.steps[j].label) or "spell") .. " to #" .. tostring(j))
  end

  -- Right cluster: ^ v  - Nx +  Mode  X  (ASCII only - Unicode was empty boxes)
  -- Built BEFORE the name hit-frame so we can pin hit to the left of ^/v
  -- (old hit went to RIGHT-200 and sat ON TOP of move buttons > broken reorder).
  local del = MakeThemeButton(card, {
  w = 26,
  h = 26,
  text = "X",
  kind = "danger",
  tip = "Remove spell",
  tipBody = "Asks for confirmation, then deletes this step.",
  onClick = function()
  local s = NS.db.sequences[NS.db.editSeqIndex or 1]
  local step = s and s.steps and s.steps[card.index]
  if not step then
  return
  end
  StaticPopup_Show("SPELLSMASHER_DELETE_STEP", step.label or "Spell", nil, {
  index = card.index,
  })
  end,
  })
  del:SetPoint("RIGHT", -6, 0)
  card.del = del

  local modePill = MakeThemeButton(card, {
  w = 78,
  h = 26,
  text = "Priority",
  kind = "secondary",
  tip = "Priority (triangle)",
  tipBody = "Click to cycle: Once (per combat, marks used on mash press) > In order > Priority (top more often) > Rev Pri. Adjacent same-mode rows share one block. Mash always advances after each press.",
  onClick = function()
  local step = getStep()
  if not step then
  return
  end
  local m = NS.NormMode(step.mode)
  if m == "once" then
  step.mode = "sequential"
  elseif m == "sequential" then
  step.mode = "priority"
  elseif m == "priority" then
  step.mode = "reverse_priority"
  else
  step.mode = "once"
  end
  step.looped = step.mode ~= "once"
  if step.mode == "once" then
  step.loopRepeat = nil
  end
  NS.Secure.Recompile()
  UI.Refresh()
  end,
  })
  modePill:SetPoint("RIGHT", del, "LEFT", -4, 0)
  card.modePill = modePill
  if card.index == 1 or not ui.tourModePill then
  ui.tourModePill = modePill
  end

  local repPlus = MakeThemeButton(card, {
  w = 22,
  h = 24,
  text = "+",
  kind = "secondary",
  tip = "Weight up",
  tipBody = "How often this spell is tried in the mash (1x normal, 2x twice as often). Not a double-cast.",
  onClick = function()
  local step = getStep()
  if not step then
  return
  end
  local n = math.max(1, math.floor(tonumber(step.loopRepeat) or 1))
  setRep(n + 1)
  end,
  })
  repPlus:SetPoint("RIGHT", modePill, "LEFT", -6, 0)
  card.repPlus = repPlus

  local repLabel = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  repLabel:SetPoint("RIGHT", repPlus, "LEFT", -1, 0)
  repLabel:SetWidth(26)
  repLabel:SetJustifyH("CENTER")
  SetFS(repLabel, C.green)
  card.repLabel = repLabel

  local repMinus = MakeThemeButton(card, {
  w = 22,
  h = 24,
  text = "-",
  kind = "secondary",
  tip = "Weight down",
  tipBody = "Down to 1x = normal.",
  onClick = function()
  local step = getStep()
  if not step then
  return
  end
  local n = math.max(1, math.floor(tonumber(step.loopRepeat) or 1))
  setRep(n - 1)
  end,
  })
  repMinus:SetPoint("RIGHT", repLabel, "LEFT", -1, 0)
  card.repMinus = repMinus

  -- Triangle share of Priority clicks (e.g. 22%) - 1 - 12 - 123 weight honesty
  local weightFS = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  weightFS:SetPoint("RIGHT", repMinus, "LEFT", -4, 0)
  weightFS:SetWidth(34)
  weightFS:SetJustifyH("RIGHT")
  SetFS(weightFS, C.muted)
  card.weightFS = weightFS

  local moveDown = MakeThemeButton(card, {
  w = 22,
  h = 24,
  text = "v",
  kind = "secondary",
  tip = "Move down",
  tipBody = "Lower in the list.",
  onClick = function()
  MoveStep(1)
  end,
  })
  moveDown:SetPoint("RIGHT", weightFS, "LEFT", -4, 0)
  card.moveDown = moveDown

  local moveUp = MakeThemeButton(card, {
  w = 22,
  h = 24,
  text = "^",
  kind = "secondary",
  tip = "Move up",
  tipBody = "Higher in the list.",
  onClick = function()
  MoveStep(-1)
  end,
  })
  moveUp:SetPoint("RIGHT", moveDown, "LEFT", -2, 0)
  card.moveUp = moveUp

  -- Raise control cluster above name-hover hit (clicks must hit ^/v / Mode / X)
  local ctrlLevel = (card:GetFrameLevel() or 1) + 20
  for _, b in ipairs({ del, modePill, repPlus, repMinus, moveDown, moveUp }) do
  if b and b.SetFrameLevel then
  b:SetFrameLevel(ctrlLevel)
  end
  end

  -- Hover name/macro for spell tip - stops LEFT of ^ so it never steals reorder clicks
  local hit = CreateFrame("Frame", nil, card)
  hit:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 4, 4)
  hit:SetPoint("BOTTOMLEFT", iconFrame, "BOTTOMRIGHT", 4, -4)
  hit:SetPoint("RIGHT", moveUp, "LEFT", -6, 0)
  hit:SetFrameLevel((card:GetFrameLevel() or 1) + 6)
  hit:EnableMouse(true)
  hit:SetScript("OnEnter", function(self)
  ShowStepSpellTip(self)
  end)
  hit:SetScript("OnLeave", function()
  GameTooltip:Hide()
  end)
  hit:SetScript("OnMouseDown", function()
  if UI.ApplyStepFields then
  UI.ApplyStepFields({
  stepIndex = NS.db.editStepIndex,
  refresh = false,
  status = false,
  fromLabel = true,
  })
  end
  NS.db.editStepIndex = card.index
  UI.Refresh()
  end)
  card.spellHit = hit

  -- clip spell name / macro before the control cluster (icon sits after #)
  nameFS:ClearAllPoints()
  nameFS:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 8, -2)
  nameFS:SetPoint("RIGHT", moveUp, "LEFT", -8, 0)
  macFS:ClearAllPoints()
  macFS:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 8, -18)
  macFS:SetPoint("RIGHT", moveUp, "LEFT", -8, 0)

  card:EnableMouse(true)
  card:SetScript("OnMouseDown", function()
  if UI.ApplyStepFields then
  UI.ApplyStepFields({
  stepIndex = NS.db.editStepIndex,
  refresh = false,
  status = false,
  fromLabel = true,
  })
  end
  NS.db.editStepIndex = card.index
  UI.Refresh()
  end)

  ui.stepCards[i] = card
  end
  card.index = i
  stepTops[i] = sy
  card:ClearAllPoints()
  -- Inset left/right so worn paper edges aren't clipped by the scroll well
  card:SetPoint("TOPLEFT", ui.stepHost, "TOPLEFT", ROW_X_INSET, -sy)
  card:SetPoint("TOPRIGHT", ui.stepHost, "TOPRIGHT", -ROW_X_INSET, -sy)
  card:Show()

  -- mode already resolved above for group headers
  local sel = (i == selIdx)
  local stepRep = math.max(1, math.min(20, math.floor(tonumber(st.loopRepeat) or 1)))
  local modeCol = ModeGroupColor(mode)
  card.num:SetText(tostring(i))
  -- Number color = mode (Once gold / In order blue / Priority green / Rev Pri blue)
  SetFS(card.num, modeCol)
  card.nameFS:SetText(st.label or "Spell")
  SetFS(card.nameFS, sel and C.green or C.text)
  local mac = tostring(st.macro or ""):gsub("\n", " ")
  if #mac > 56 then
  mac = mac:sub(1, 56) .. "..."
  end
  card.macFS:SetText(mac ~= "" and mac or ("/cast [nochanneling] " .. (st.label or "")))
  SetFS(card.macFS, C.muted)

  -- Game spell icon + hover tip (full Blizzard spell tooltip when client knows the spell)
  do
  local pack = NS.db and NS.db.sequences and NS.db.sequences[NS.db.editSeqIndex or 1]
  local resolved = NS.Secure and NS.Secure.ResolveStepSpell and NS.Secure.ResolveStepSpell(st, {
  className = pack and pack.className,
  spec = pack and pack.spec,
  })
  local tex = resolved and resolved.iconID or 134400
  local spellName = (resolved and resolved.name) or NS.Trim(st.label or "Spell")
  local spellID = resolved and resolved.spellID or nil
  local modeLabel = "Priority"
  if mode == "once" then
  modeLabel = "Once (per combat)"
  elseif mode == "sequential" then
  modeLabel = "In order"
  elseif mode == "reverse_priority" then
  modeLabel = "Rev Pri"
  end
  local weightMap2 = ui._stepWeightPct
  local wp2 = weightMap2 and weightMap2[i]
  card._spellTip = {
  spellID = spellID,
  name = spellName,
  macro = tostring(st.macro or ""),
  mode = modeLabel,
  weight = (mode == "priority" or mode == "reverse_priority") and wp2 or nil,
  }
  if card.iconTex then
  card.iconTex:SetTexture(tex)
  card.iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
  card.iconTex:Show()
  end
  if card.iconFrame then
  card.iconFrame:Show()
  card.iconFrame:SetFrameLevel((card:GetFrameLevel() or 1) + 8)
  end
  -- Subline: cast time / range when known, else short macro
  if card.macFS and resolved and (resolved.castTime or resolved.maxRange) then
  local bits = {}
  if resolved.castTime and resolved.castTime > 0 then
  bits[#bits + 1] = string.format("%.1fs cast", resolved.castTime / 1000)
  elseif resolved.castTime == 0 then
  bits[#bits + 1] = "Instant"
  end
  if resolved.maxRange and resolved.maxRange > 0 then
  bits[#bits + 1] = tostring(math.floor(resolved.maxRange)) .. " yd"
  end
  if #bits > 0 then
  card.macFS:SetText(table.concat(bits, "  -  "))
  end
  end
  end

  -- Priority triangle share (Nx weight + list position  >  % of Priority clicks)
  local weightMap = ui._stepWeightPct
  local wp = weightMap and weightMap[i]
  if card.weightFS then
  if (mode == "priority" or mode == "reverse_priority") and wp and wp > 0 then
  card.weightFS:Show()
  card.weightFS:SetText(tostring(wp) .. "%")
  SetFS(card.weightFS, wp >= 20 and C.green or C.muted)
  else
  card.weightFS:SetText("")
  card.weightFS:Hide()
  end
  end

  if mode == "once" then
  card.repMinus:Hide()
  card.repPlus:Hide()
  card.repLabel:Hide()
  if card.weightFS then
  card.weightFS:Hide()
  end
  st.loopRepeat = nil
  else
  card.repMinus:Show()
  card.repPlus:Show()
  card.repLabel:Show()
  card.repLabel:SetText(stepRep .. "x")
  SetFS(card.repLabel, stepRep > 1 and C.green or C.muted)
  end
  if card.moveUp and card.moveDown then
  card.moveUp:Show()
  card.moveDown:Show()
  end

  if mode == "priority" then
  card.modePill:SetThemeText("Priority")
  Hex(card.modePill, { C.green[1], C.green[2], C.green[3], 0.30 }, C.green, 16, "panel")
  SetFS(card.modePill.label, C.green)
  if card.modePill._opts then
  card.modePill._opts.tip = "Priority (1  12  123  1234)"
  card.modePill._opts.tipBody =
  "Mash clicks: 1, then 1-2, then 1-2-3, then 1-2-3-4. The top row fires more. 4x = that row is in the click list four times. If this row is on cooldown, mash still fires a ready spell from the same pack."
  end
  elseif mode == "reverse_priority" then
  card.modePill:SetThemeText("Rev Pri")
  Hex(card.modePill, { C.accent[1], C.accent[2], C.accent[3], 0.30 }, C.accent, 16, "panel")
  SetFS(card.modePill.label, C.accent)
  if card.modePill._opts then
  card.modePill._opts.tip = "Reverse Priority (1  21  321)"
  card.modePill._opts.tipBody =
  "Mash clicks: 1, then 2-1, then 3-2-1. 4x = that row is in the click list four times."
  end
  elseif mode == "sequential" then
  card.modePill:SetThemeText("In order")
  Hex(card.modePill, { C.accent[1], C.accent[2], C.accent[3], 0.22 }, C.accent, 16, "panel")
  SetFS(card.modePill.label, C.accent)
  if card.modePill._opts then
  card.modePill._opts.tip = "In order (1  2  3  4)"
  card.modePill._opts.tipBody =
  "Each mash is the next row, top to bottom, then it loops. 4x = that row four presses in a row. Does not wait if the spell is down."
  end
  else
  card.modePill:SetThemeText("Once")
  Hex(card.modePill, { C.gold[1], C.gold[2], C.gold[3], 0.28 }, C.gold, 16, "panel")
  SetFS(card.modePill.label, C.gold)
  if card.modePill._opts then
  card.modePill._opts.tip = "Once (per combat - on press)"
  card.modePill._opts.tipBody =
  "On the mash press that arms this row, it is marked used for this fight (even if the cast fails range/CD). Then skipped until leave combat. OOC retarget also resets openers; mid-fight retarget does not. Weight (xN) ignored. Click to cycle."
  end
  end

  -- Parchment fill; step number keeps mode color (not black ink)
  local L = NS.LEATHER or {}
  local ink = L.ink or { 0.02, 0.02, 0.02, 1 }
  local inkDim = L.inkDim or { 0.10, 0.08, 0.05, 1 }
  if sel then
  HexLeather(card, nil, L.edgeSel or C.green, true)
  if card.accent then
  card.accent:Hide()
  end
  if card.nameFS then
  SetFS(card.nameFS, L.inkSel or ink)
  end
  if card.macFS then
  SetFS(card.macFS, L.inkMacSel or inkDim)
  end
  else
  local borderCol = L.edge or { 0.50, 0.32, 0.12, 1 }
  if mode == "priority" then
  borderCol = L.edgePri or C.green
  elseif mode == "reverse_priority" then
  borderCol = L.edgeRev or C.accent
  elseif mode == "sequential" then
  borderCol = L.edgeRev or C.accent
  end
  HexLeather(card, nil, borderCol, false)
  if card.accent then
  card.accent:Hide()
  end
  if card.nameFS then
  SetFS(card.nameFS, ink)
  end
  if card.macFS then
  SetFS(card.macFS, inkDim)
  end
  end
  -- Always: step # colored by mode (Once / In order / Priority / Rev Pri)
  if card.num then
  SetFS(card.num, modeCol)
  if card.num.SetShadowColor then
  card.num:SetShadowColor(0, 0, 0, 0.9)
  card.num:SetShadowOffset(1, -1)
  end
  end
  -- Weight / Nx: solid black on parchment
  if card.weightFS and card.weightFS:IsShown() then
  local wp = ui._stepWeightPct and ui._stepWeightPct[i]
  if wp and wp >= 20 then
  SetFS(card.weightFS, { 0.00, 0.35, 0.12, 1 })
  else
  SetFS(card.weightFS, inkDim)
  end
  if card.weightFS.SetShadowColor then
  card.weightFS:SetShadowColor(1, 1, 1, 0.8)
  card.weightFS:SetShadowOffset(1, -1)
  end
  end
  if card.repLabel and card.repLabel:IsShown() then
  if stepRep > 1 then
  SetFS(card.repLabel, { 0.00, 0.35, 0.12, 1 })
  else
  SetFS(card.repLabel, inkDim)
  end
  if card.repLabel.SetShadowColor then
  card.repLabel:SetShadowColor(1, 1, 1, 0.8)
  card.repLabel:SetShadowOffset(1, -1)
  end
  end
  sy = sy + ROW_H
  end
  -- Host height = exact content (+ small pad). Over-sizing left a big empty gap under the last spell.
  if ui.stepHost and ui.stepScroll then
  local viewW = ui.stepScroll:GetWidth() or 560
  local viewH = math.max(1, ui.stepScroll:GetHeight() or 200)
  local contentH = sy + PAD_BOT
  -- Only pad past the viewport when content is shorter than the view (so wheel still works a little)
  if nSteps > 0 and contentH < viewH then
  contentH = viewH
  end
  ui.stepHost:SetWidth(math.max(100, math.floor(viewW + 0.5)))
  ui.stepHost:SetHeight(math.max(80, math.floor(contentH + 0.5)))
  end

  -- Only snap scroll when the selected index changes (don't fight free scrolling)
  if ui.stepScroll and nSteps > 0 then
  local viewH = ui.stepScroll:GetHeight() or 200
  local maxS = math.max(0, (ui.stepHost:GetHeight() or 0) - viewH)
  if ui.stepScroll.GetVerticalScrollRange then
  local r = ui.stepScroll:GetVerticalScrollRange()
  if r and r > 0 then
  maxS = r
  end
  end
  local cur = ui.stepScroll:GetVerticalScroll() or 0
  -- clamp if content shrank
  if cur > maxS then
  ui.stepScroll:SetVerticalScroll(maxS)
  cur = maxS
  end
  if ui._lastStepScrollSel ~= selIdx then
  ui._lastStepScrollSel = selIdx
  local selTop = stepTops[selIdx] or (PAD_TOP + (selIdx - 1) * ROW_H)
  local selBot = selTop + ROW_H
  local margin = 8
  if selTop < cur + margin then
  ui.stepScroll:SetVerticalScroll(math.max(0, selTop - margin))
  elseif selBot > cur + viewH - margin then
  ui.stepScroll:SetVerticalScroll(math.min(maxS, selBot - viewH + margin))
  end
  end
  end
  ui._stepTops = stepTops

  -- load selected step into footer fields (suppress apply so SetText can't re-save wrong step)
  if ui.stepLabelBox and seq.steps and seq.steps[selIdx] then
  local st = seq.steps[selIdx]
  local typingLabel = ui.stepLabelBox.HasFocus and ui.stepLabelBox:HasFocus()
  local typingMacro = ui.stepMacroBox and ui.stepMacroBox.HasFocus and ui.stepMacroBox:HasFocus()
  if UI._setSuppressFieldApply then
  UI._setSuppressFieldApply(true)
  end
  if not typingLabel then
  ui.stepLabelBox:SetText(st.label or "")
  end
  if ui.stepMacroBox and not typingMacro then
  ui.stepMacroBox:SetText(st.macro or "")
  end
  if UI._setSuppressFieldApply then
  UI._setSuppressFieldApply(false)
  end
  elseif ui.stepLabelBox then
  if UI._setSuppressFieldApply then
  UI._setSuppressFieldApply(true)
  end
  ui.stepLabelBox:SetText("")
  if ui.stepMacroBox then
  ui.stepMacroBox:SetText("")
  end
  if UI._setSuppressFieldApply then
  UI._setSuppressFieldApply(false)
  end
  end
  end
  end

  -- new panel chips
  if tab == "new" then
  if ui.refreshSpecs then
  ui.refreshSpecs()
  end
  if ui.newInfo then
  local heroTxt = (newState.hero and newState.hero ~= "") and ("  -  " .. newState.hero) or ""
  if NS.IsClassicFlavor then
  local flavorName = (NS.FlavorLabel and NS.FlavorLabel()) or "Classic Era"
  ui.newInfo:SetText(
  "|cff3dd68c"
  .. newState.className
  .. "  /  "
  .. tostring(newState.spec or "")
  .. "|r\n"
  .. flavorName
  .. "  -  Level "
  .. tostring(newState.level)
  .. "  -  "
  .. ((newState.role == "st") and "ST" or "AoE")
  .. "\n|cff8b96adEmpty pack: add spells in Edit. Or Import a !SpellSmasher! string.|r"
  )
  else
  ui.newInfo:SetText(
  "|cff3dd68c"
  .. newState.className
  .. "  /  "
  .. newState.spec
  .. heroTxt
  .. "|r\n"
  .. "Source: "
  .. (newState.source == "engine" and "SpellSmasher Engine" or "Method.gg")
  .. "  -  Level "
  .. tostring(newState.level)
  .. " from this character"
  .. "  -  "
  .. ((newState.role == "st") and "ST" or "AoE")
  .. "\n|cff8b96adST = Method boss list. AoE = Method packs / M+ list.|r"
  )
  end
  end
  if ui.classChips then
  for cn, chip in pairs(ui.classChips) do
  local allow = (not NS.ClassAllowedOnFlavor) or NS.ClassAllowedOnFlavor(cn)
  chip:SetShown(allow)
  if allow then
  chip:SetActive(cn == newState.className)
  end
  end
  end
  if ui.roleChips then
  for rid, chip in pairs(ui.roleChips) do
  chip:SetActive(rid == (newState.role or "aoe"))
  end
  end
  end

  -- helpers (On = green wash + white label)
  if ui.aaBtn then
  local aaOn = db.helperAutoAttack and true or false
  local rtOn = db.helperRetarget and true or false
  local defOn = db.helperDefensive and true or false
  local sm = tostring(db.shiftMode or "ignore")
  local shiftOn = sm ~= "ignore"
  ui.aaBtn:SetThemeText(aaOn and "AA  -  On" or "AA  -  Off")
  ui.aaBtn:SetKind(aaOn and "green" or "secondary")
  ui.aaBtn:SetActive(aaOn)
  ui.rtBtn:SetThemeText(rtOn and "RT  -  On" or "RT  -  Off")
  ui.rtBtn:SetKind(rtOn and "green" or "secondary")
  ui.rtBtn:SetActive(rtOn)
  ui.defBtn:SetThemeText(defOn and "DEF  -  On" or "DEF  -  Off")
  ui.defBtn:SetKind(defOn and "green" or "secondary")
  ui.defBtn:SetActive(defOn)
  ui.shiftBtn:SetThemeText("Shift  -  " .. sm)
  ui.shiftBtn:SetKind(shiftOn and "green" or "secondary")
  ui.shiftBtn:SetActive(shiftOn)
  end

  local k = NS.Secure.GetBoundKey()
  if ui.bindFS then
  if k then
  ui.bindFS:SetText("|cff3dd68cKeybind|r  " .. k)
  else
  ui.bindFS:SetText("|cffff6b7aNo keybind|r  - tap 1-0 below")
  end
  end
  if ui.keyStatus then
  if k then
  ui.keyStatus:SetText("|cff3dd68cBound: " .. k .. "|r")
  else
  ui.keyStatus:SetText("|cfff0b429Not bound - click 1-0|r")
  end
  end
  -- Mash key 1-0: highlight chosen number (green wash)
  if ui.keyBtns then
  local bound = k and tostring(k) or ""
  local barKey = db.barKey and tostring(db.barKey) or ""
  for key, btn in pairs(ui.keyBtns) do
  local on = (bound == key)
  or (barKey == key)
  or (bound == ("NUMPAD" .. key))
  if btn.SetActive then
  btn:SetActive(on)
  end
  if btn.SetKind then
  btn:SetKind(on and "green" or "secondary")
  end
  end
  end
end

-- -- create --------------------------------------------------

function UI.Toggle()
  -- First /ssm must OPEN. (CreateFrame starts shown; if we Create then
  -- IsShown > Hide, users had to type the slash twice.)
  if not mainFrame then
  UI.Create()
  if mainFrame then
  mainFrame:Show()
  UI.Refresh()
  if UI.MaybeShowCoachOnOpen then
  UI.MaybeShowCoachOnOpen()
  end
  end
  return
  end
  if mainFrame:IsShown() then
  mainFrame:Hide()
  else
  mainFrame:Show()
  UI.Refresh()
  if UI.MaybeShowCoachOnOpen then
  UI.MaybeShowCoachOnOpen()
  end
  end
end

function UI.Open()
  if not mainFrame then
  UI.Create()
  end
  if mainFrame then
  mainFrame:Show()
  SyncNewStateToPlayer()
  UI.Refresh()
  if UI.MaybeShowCoachOnOpen then
  UI.MaybeShowCoachOnOpen()
  end
  end
end

function UI.Close()
  if mainFrame then
  mainFrame:Hide()
  end
end

function UI.Create()
  if mainFrame then
  return
  end

  local f = CreateFrame("Frame", "SpellSmasherMainFrame", UIParent, "BackdropTemplate")
  mainFrame = f
  local db0 = NS.db or {}
  local fw = tonumber(db0.frameW) or 620
  local fh = tonumber(db0.frameH) or 580
  fw = math.max(480, math.min(1400, fw))
  fh = math.max(420, math.min(1000, fh))
  f:SetSize(fw, fh)
  f:SetPoint("CENTER")
  f:SetFrameStrata("DIALOG")
  f:SetMovable(true)
  f:SetResizable(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", function(self)
  if not InCombatLockdown() then
  self:StartMoving()
  end
  end)
  f:SetScript("OnDragStop", function(self)
  self:StopMovingOrSizing()
  end)
  f:SetClampedToScreen(true)
  -- Resize limits (retail API; fall back for older clients)
  if f.SetResizeBounds then
  f:SetResizeBounds(480, 420, 1400, 1000)
  else
  if f.SetMinResize then
  f:SetMinResize(480, 420)
  end
  if f.SetMaxResize then
  f:SetMaxResize(1400, 1000)
  end
  end
  f:SetScript("OnSizeChanged", function(self, width, height)
  if not NS.db then
  return
  end
  width = math.floor(width + 0.5)
  height = math.floor(height + 0.5)
  if width >= 480 and height >= 420 then
  NS.db.frameW = width
  NS.db.frameH = height
  end
  if UI.LayoutChrome then
    UI.LayoutChrome()
  end
  end)
  -- Outer shell: metal dialog border; dark fill under the real panel image
  Hex(f, { 0.02, 0.025, 0.04, 0.95 }, C.green, 32, "dialog")
  -- ACTUAL image background (Media/panel-bg.png) - your S on navy
  ApplyPanelImage(f, PANEL_BG, { inset = 12, alpha = 0.95 })
  -- Start hidden; Toggle/Open decides visibility (avoids double-slash bug)
  f:Hide()
  -- Esc closes like other game panels
  if f:GetName() then
  local already
  if UISpecialFrames then
  for _, n in ipairs(UISpecialFrames) do
  if n == f:GetName() then
  already = true
  break
  end
  end
  if not already then
  tinsert(UISpecialFrames, f:GetName())
  end
  end
  end

  -- Bottom-right size grip (drag to resize the whole window)
  local grip = CreateFrame("Button", nil, f)
  grip:SetSize(18, 18)
  grip:SetPoint("BOTTOMRIGHT", -14, 14)
  grip:SetFrameLevel(f:GetFrameLevel() + 20)
  grip:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
  grip:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
  grip:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
  grip:SetScript("OnMouseDown", function()
  if not InCombatLockdown() then
  f:StartSizing("BOTTOMRIGHT")
  end
  end)
  grip:SetScript("OnMouseUp", function()
  f:StopMovingOrSizing()
  if NS.db then
  local w, h = f:GetWidth(), f:GetHeight()
  NS.db.frameW = math.floor(w + 0.5)
  NS.db.frameH = math.floor(h + 0.5)
  end
  end)
  grip:SetScript("OnEnter", function(self)
  GameTooltip:SetOwner(self, "ANCHOR_TOPLEFT")
  GameTooltip:AddLine("Resize window", C.green[1], C.green[2], C.green[3])
  GameTooltip:AddLine("Drag to make the SpellSmasher panel bigger or smaller.", 1, 1, 1, true)
  GameTooltip:Show()
  end)
  grip:SetScript("OnLeave", GameTooltip_Hide)
  f.resizeGrip = grip

  -- Header strip (brand navy + green bottom rail)
  local header = CreateFrame("Frame", nil, f, "BackdropTemplate")
  header:SetPoint("TOPLEFT", 14, -14)
  header:SetPoint("TOPRIGHT", -14, -14)
  header:SetHeight(52)
  Hex(header, { (C.headerBg or C.elevated)[1], (C.headerBg or C.elevated)[2], (C.headerBg or C.elevated)[3], 0.90 }, C.green, 16, "panel")
  f.header = header

  -- Ambient brand glow (blue wash under title - site identity)
  local glow = header:CreateTexture(nil, "BACKGROUND", nil, 2)
  glow:SetPoint("TOPLEFT", 4, -4)
  glow:SetPoint("BOTTOMRIGHT", -4, 4)
  glow:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.08)

  -- Green accent line under header
  local headerRail = header:CreateTexture(nil, "ARTWORK")
  headerRail:SetPoint("BOTTOMLEFT", 4, 2)
  headerRail:SetPoint("BOTTOMRIGHT", -4, 2)
  headerRail:SetHeight(2)
  headerRail:SetColorTexture(C.green[1], C.green[2], C.green[3], 0.85)

  -- Your SpellSmasher logo (top-left of header)
  local brandMark = header:CreateTexture(nil, "ARTWORK")
  brandMark:SetSize(42, 42)
  brandMark:SetPoint("LEFT", 8, 0)
  brandMark:SetTexture(BRAND_MARK)
  if brandMark.SetTexCoord then
  brandMark:SetTexCoord(0, 1, 0, 1)
  end
  brandMark:SetVertexColor(1, 1, 1, 1)
  brandMark:SetAlpha(1)
  brandMark:Show()
  f.brandLogo = brandMark

  local close = MakeThemeButton(header, {
  w = 28,
  h = 28,
  icon = "close",
  iconOnly = true,
  kind = "danger",
  tip = "Close",
  onClick = function()
  f:Hide()
  end,
  })
  close:SetPoint("RIGHT", -8, 0)

  -- Centered title block (SpellSmasher + version + tagline) in the header box
  local titleBlock = CreateFrame("Frame", nil, header)
  titleBlock:SetPoint("LEFT", brandMark, "RIGHT", 8, 0)
  titleBlock:SetPoint("RIGHT", close, "LEFT", -8, 0)
  titleBlock:SetPoint("TOP", 0, -6)
  titleBlock:SetPoint("BOTTOM", 0, 8)
  f.headerTitleBlock = titleBlock

  local title = titleBlock:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
  title:SetPoint("TOP", titleBlock, "TOP", 0, -2)
  title:SetJustifyH("CENTER")
  title:SetText("SpellSmasher")
  SetFS(title, C.green)
  f.headerTitle = title

  local ver = titleBlock:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ver:SetPoint("LEFT", title, "RIGHT", 8, 0)
  ver:SetText("v" .. NS.VERSION)
  SetFS(ver, C.muted)
  f.headerVer = ver

  -- Keep version visually paired with title while the pair stays centered:
  -- put title+ver in a nested row centered in the block
  local titleRow = CreateFrame("Frame", nil, titleBlock)
  titleRow:SetHeight(18)
  titleRow:SetPoint("TOP", titleBlock, "TOP", 0, -2)
  title:ClearAllPoints()
  ver:ClearAllPoints()
  title:SetParent(titleRow)
  ver:SetParent(titleRow)
  title:SetPoint("LEFT", titleRow, "LEFT", 0, 0)
  ver:SetPoint("LEFT", title, "RIGHT", 8, 0)
  -- size titleRow to content and center it
  local function layoutTitleRow()
  local tw = (title:GetStringWidth() or 0) + 8 + (ver:GetStringWidth() or 0)
  titleRow:SetWidth(math.max(40, tw + 4))
  titleRow:ClearAllPoints()
  titleRow:SetPoint("TOP", titleBlock, "TOP", 0, -2)
  end
  layoutTitleRow()
  titleRow:SetScript("OnShow", layoutTitleRow)

  local sub = titleBlock:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  sub:SetPoint("TOP", titleRow, "BOTTOM", 0, -4)
  sub:SetPoint("LEFT", titleBlock, "LEFT", 4, 0)
  sub:SetPoint("RIGHT", titleBlock, "RIGHT", -4, 0)
  sub:SetJustifyH("CENTER")
  sub:SetText("One mash list  -  Save  -  bind a key  -  cast only what's on the list")
  SetFS(sub, C.muted)
  f.headerSub = sub

  -- tab bar card
  local tabBar = MakeCard(f)
  tabBar:SetPoint("TOPLEFT", 16, -74)
  tabBar:SetPoint("TOPRIGHT", -16, -74)
  tabBar:SetHeight(46)
  Hex(tabBar, { C.elevated[1], C.elevated[2], C.elevated[3], 0.90 }, C.border, 16, "panel")

  local tabs = {
  { "list", "List" },
  { "new", "New pack" },
  { "import", "Import" },
  { "editor", "Edit" },
  }
  local prev
  for _, t in ipairs(tabs) do
  local b = MakeThemeButton(tabBar, {
  w = 88,
  h = 30,
  text = t[2],
  kind = "tab",
  onClick = function()
  ShowTab(t[1])
  end,
  })
  if not prev then
  b:SetPoint("LEFT", 8, 0)
  else
  b:SetPoint("LEFT", prev, "RIGHT", 6, 0)
  end
  tabButtons[t[1]] = b
  prev = b
  end

  local helpTab = MakeThemeButton(tabBar, {
  w = 88,
  h = 30,
  text = "Tutorial",
  kind = "primary",
  tip = "Full spotlight tour",
  tipBody = "Theme-style guided tour: dim hole + glow on real controls, full happy path (List > New > Import > Edit > helpers > talents > bind). Skip/Esc anytime. After finish: quiet Replay. /ssm help restarts.",
  onClick = function()
  if UI.ShowCoach then
  UI.ShowCoach(1)
  end
  end,
  })
  ui.helpTab = helpTab
  ui.tabBar = tabBar

  local lostSeqBtn = MakeThemeButton(tabBar, {
  w = 128,
  h = 30,
  text = "Lost sequence?",
  kind = "secondary",
  tip = "Pack missing from the list?",
  tipBody = "Help: how to find a hidden pack, restore the last logout, or Import a backup. Does not change your lists.",
  onClick = function()
  if UI.ShowLostSequenceHelp then
  UI.ShowLostSequenceHelp()
  end
  end,
  })
  ui.lostSeqBtn = lostSeqBtn

  local recompile = MakeThemeButton(tabBar, {
  w = 100,
  h = 30,
  text = "Recompile",
  icon = "refresh",
  kind = "green",
  tip = "Rebuild secure mash steps",
  tipBody = "Bakes the active list into the one-button runner after you edit spells.",
  onClick = function()
  NS.Secure.verboseCompile = true
  NS.Secure.Recompile()
  local warn = ""
  local c = NS.lastBakeInfo and NS.lastBakeInfo.compile
  if NS.Compile and NS.Compile.StatusLine then
    warn = NS.Compile.StatusLine(c) or ""
  end
  if warn ~= "" then
    SetStatus("|cff3dd68cRecompiled.|r  " .. warn)
  else
    local n = c and c.total or 0
    SetStatus("|cff3dd68cRecompiled.|r  " .. tostring(n) .. " click(s)")
  end
  end,
  })
  recompile:SetPoint("RIGHT", -8, 0)
  lostSeqBtn:SetPoint("RIGHT", recompile, "LEFT", -6, 0)
  -- Tutorial/Replay sits in the RIGHT cluster (left of Lost sequence?).
  -- Never LEFT-of-Edit: at 620px that lands on top of Lost sequence? ("Lost seqReplay?").
  helpTab:ClearAllPoints()
  helpTab:SetPoint("RIGHT", lostSeqBtn, "LEFT", -6, 0)
  ui.recompileBtn = recompile

  -- content area (room for mash key bar + helpers under it)
  local content = CreateFrame("Frame", nil, f)
  content:SetPoint("TOPLEFT", 18, -128)
  content:SetPoint("BOTTOMRIGHT", -18, 184)
  if content.SetClipsChildren then
    content:SetClipsChildren(true)
  end
  ui.content = content

  -- -- LIST --
  local listPanel = CreateFrame("Frame", nil, content)
  listPanel:SetAllPoints()
  listPanel:SetFrameLevel((content:GetFrameLevel() or 1) + 2)
  ui.listPanel = listPanel

  local listHead = listPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  listHead:SetPoint("TOPLEFT", 2, -2)
  listHead:SetText("Your packs")
  SetFS(listHead, C.text)
  ui.listHead = listHead

  local listFilterBtn = MakeThemeButton(listPanel, {
  w = 96,
  h = 22,
  text = "This class",
  kind = "green",
  tip = "Filter list by your class",
  tipBody = "On = only packs for the character you are playing (Rogue sees Rogue lists). Off = every pack on this account.",
  onClick = function()
  local db = NS.db
  if not db then
  return
  end
  db.listFilterThisClass = not (db.listFilterThisClass ~= false)
  -- When turning filter on, prefer Active pack for this class
  if db.listFilterThisClass ~= false and NS.EnsureActivePackForPlayer then
  if NS.EnsureActivePackForPlayer() and NS.Secure and NS.Secure.Recompile then
  NS.Secure.Recompile()
  end
  end
  UI.Refresh()
  end,
  })
  listFilterBtn:SetPoint("LEFT", listHead, "RIGHT", 10, 0)
  ui.listFilterBtn = listFilterBtn

  local listHint = listPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  listHint:SetPoint("LEFT", listFilterBtn, "RIGHT", 10, 0)
  listHint:SetText("|cff8b96adClick a pack to select  -  Export = selected only  -  Use = mash|r")
  SetFS(listHint, C.muted)
  ui.listHint = listHint

  ui.bindFS = listPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.bindFS:SetPoint("TOPRIGHT", -2, -4)
  SetFS(ui.bindFS, C.gold)

  local listScroll = CreateFrame("ScrollFrame", nil, listPanel)
  listScroll:SetPoint("TOPLEFT", 0, -28)
  listScroll:SetPoint("BOTTOMRIGHT", 0, 44)
  local listChild = CreateFrame("Frame", nil, listScroll)
  listChild:SetSize(560, 400)
  listScroll:SetScrollChild(listChild)
  listScroll:EnableMouseWheel(true)
  listScroll:SetScript("OnMouseWheel", function(self, delta)
  local cur = self:GetVerticalScroll() or 0
  local maxS = math.max(0, (listChild:GetHeight() or 0) - (self:GetHeight() or 1))
  self:SetVerticalScroll(math.max(0, math.min(maxS, cur - delta * 40)))
  end)
  ui.listScrollChild = listChild

  ui.emptyList = listPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  ui.emptyList:SetPoint("CENTER", listScroll, "CENTER", 0, 10)
  ui.emptyList:SetText(
  "|cff3dd68cNo packs yet - quick start|r\n\n"
  .. "|cffffff001.|r  |cff3dd68cSet me up|r  (Method ST + AoE packs for this character)\n"
  .. "|cffffff002.|r  Bind mash key 1-0 below\n"
  .. "|cffffff003.|r  Edit  >  |cff3dd68cMatch talents|r\n"
  .. "|cffffff004.|r  Or |cff6c8cffImport|r a !SpellSmasher! string from the website\n\n"
  .. "|cff8b96adLost a list?|r  |cffffff00Lost sequence?|r  next to Tutorial  (or /ssm lost)"
  )
  ui.emptyListClassic = (
  "|cff3dd68cNo packs yet - Classic|r\n\n"
  .. "|cffffff001.|r  |cff3dd68cSet me up|r  (ST + AoE for this class; known spells only)\n"
  .. "|cffffff002.|r  Bind mash key 1-0 below\n"
  .. "|cffffff003.|r  Mash. No Match talents on Era (no loadout strings).\n"
  .. "|cffffff004.|r  Or |cff6c8cffImport|r a !SpellSmasher! string (adds, never wipes)\n\n"
  .. "|cff8b96adLost a list?|r  |cffffff00Lost sequence?|r  next to Tutorial  (or /ssm lost)"
  )
  SetFS(ui.emptyList, C.muted)
  ui.emptyList:Hide()

  local exportBtn = MakeThemeButton(listPanel, {
  w = 150,
  h = 34,
  text = "Export selected",
  kind = "green",
  tip = "Export selected pack",
  tipBody = "Click a pack row first (blue  -  export). Exports that pack + live talents if class|spec matches you. Does not re-trim the spell list or rewrite pack level. Level-legal lists: spellsmasher.com.",
  onClick = function()
  DoExport({ all = false })
  end,
  })
  exportBtn:SetPoint("BOTTOMLEFT", 0, 2)
  ui.exportSelectedBtn = exportBtn

  local exportAllBtn = MakeThemeButton(listPanel, {
  w = 110,
  h = 34,
  text = "Export all",
  kind = "secondary",
  tip = "Export every pack",
  tipBody = "One string with all packs (full backup). Spell lists and pack levels stay as stored.",
  onClick = function()
  DoExport({ all = true })
  end,
  })
  exportAllBtn:SetPoint("LEFT", exportBtn, "RIGHT", 8, 0)
  ui.exportAllBtn = exportAllBtn

  local setMeUpBtn = MakeThemeButton(listPanel, {
  w = 110,
  h = 34,
  text = "Set me up",
  kind = "green",
  tip = "Method ST + AoE packs for this character",
  tipBody = "Creates two Method packs (ST and AoE) for your logged-in class / spec / hero. Two list rows, not Shift. Active is the pill you have selected (default AoE). Then runs Match talents (same as the Edit button) out of combat. Shared Method gold still asks confirm first.",
  onClick = function()
  DoSetMeUp()
  end,
  })
  setMeUpBtn:SetPoint("LEFT", exportAllBtn, "RIGHT", 8, 0)
  ui.setMeUpBtn = setMeUpBtn
  ui.heroLabel = heroLabel

  local newFromList = MakeThemeButton(listPanel, {
  w = 96,
  h = 34,
  text = "New pack",
  kind = "primary",
  tip = "Create Method/Engine pack",
  onClick = function()
  ShowTab("new")
  end,
  })
  newFromList:SetPoint("LEFT", setMeUpBtn, "RIGHT", 8, 0)
  ui.newFromListBtn = newFromList

  -- -- NEW --
  local newPanel = CreateFrame("Frame", nil, content)
  newPanel:SetAllPoints()
  newPanel:Hide()
  ui.newPanel = newPanel

  local newCard = MakeCard(newPanel)
  newCard:SetPoint("TOPLEFT", 0, 0)
  newCard:SetPoint("TOPRIGHT", 0, 0)
  newCard:SetHeight(72)
  ui.newInfo = newCard:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  ui.newInfo:SetPoint("TOPLEFT", 14, -12)
  ui.newInfo:SetPoint("BOTTOMRIGHT", -14, 12)
  ui.newInfo:SetJustifyH("LEFT")
  ui.newInfo:SetJustifyV("TOP")
  SetFS(ui.newInfo, C.text)

  local classLabel = newPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  classLabel:SetPoint("TOPLEFT", newCard, "BOTTOMLEFT", 2, -12)
  classLabel:SetText("CLASS")
  SetFS(classLabel, C.muted)

  local classHost = CreateFrame("Frame", nil, newPanel)
  classHost:SetPoint("TOPLEFT", classLabel, "BOTTOMLEFT", 0, -6)
  classHost:SetPoint("TOPRIGHT", newCard, "BOTTOMRIGHT", 0, -6)
  classHost:SetHeight(64)
  ui.classHost = classHost
  ui.classLabel = classLabel
  ui.classChips = {}
  for _, cn in ipairs(NS.CLASSES) do
  local chip = MakeThemeButton(classHost, {
  w = 82,
  h = 28,
  text = cn:gsub("Death Knight", "DK"):gsub("Demon Hunter", "DH"),
  kind = "chip",
  tip = cn,
  onClick = function()
  newState.className = cn
  local specs = (NS.SpecsForFlavor and NS.SpecsForFlavor(cn)) or NS.SPECS[cn]
  newState.spec = specs and specs[1] or ""
  newState.hero = ""
  SyncNewStateHero()
  UI.Refresh()
  end,
  })
  ui.classChips[cn] = chip
  end

  local specLabel = newPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  specLabel:SetPoint("TOPLEFT", classHost, "BOTTOMLEFT", 0, -10)
  specLabel:SetText("SPEC")
  SetFS(specLabel, C.muted)

  local specHost = CreateFrame("Frame", nil, newPanel)
  specHost:SetPoint("TOPLEFT", specLabel, "BOTTOMLEFT", 0, -6)
  specHost:SetPoint("RIGHT", classHost, "RIGHT", 0, 0)
  specHost:SetHeight(40)
  ui.specHost = specHost
  ui.specLabel = specLabel
  ui.specChips = {}

  local heroLabel = newPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  heroLabel:SetPoint("TOPLEFT", specHost, "BOTTOMLEFT", 0, -10)
  heroLabel:SetText("HERO TALENT")
  SetFS(heroLabel, C.muted)
  ui.heroLabel = heroLabel

  local heroHost = CreateFrame("Frame", nil, newPanel)
  heroHost:SetPoint("TOPLEFT", heroLabel, "BOTTOMLEFT", 0, -6)
  heroHost:SetSize(580, 40)
  ui.heroChips = {}

  local roleLabel = newPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  roleLabel:SetPoint("TOPLEFT", heroHost, "BOTTOMLEFT", 0, -10)
  roleLabel:SetText("LIST")
  SetFS(roleLabel, C.muted)
  ui.roleLabel = roleLabel

  local roleHost = CreateFrame("Frame", nil, newPanel)
  roleHost:SetPoint("TOPLEFT", roleLabel, "BOTTOMLEFT", 0, -6)
  roleHost:SetSize(580, 36)
  ui.roleHost = roleHost
  ui.roleChips = {}
  for i, pair in ipairs({ { "aoe", "AoE / packs" }, { "st", "Single target" } }) do
  local rid, rtxt = pair[1], pair[2]
  local chip = MakeThemeButton(roleHost, {
  w = 140,
  h = 30,
  text = rtxt,
  kind = "chip",
  tip = rtxt,
  tipBody = rid == "st"
  and "Method ST list (boss order). 25 specs differ from AoE; 15 share one list."
  or "Method AoE / M+ list (packs).",
  active = (newState.role == rid),
  onClick = function()
  newState.role = rid
  UI.Refresh()
  end,
  })
  chip:SetPoint("TOPLEFT", (i - 1) * 150, 0)
  ui.roleChips[rid] = chip
  end

  function ui.refreshSpecs()
  for _, c in pairs(ui.specChips) do
  c:Hide()
  end
  ui.specChips = {}
  local specs = (NS.SpecsForFlavor and NS.SpecsForFlavor(newState.className)) or NS.SPECS[newState.className] or {}
  for i, sp in ipairs(specs) do
  local chip = MakeThemeButton(specHost, {
  w = 110,
  h = 30,
  text = sp,
  kind = "chip",
  active = (sp == newState.spec),
  onClick = function()
  newState.spec = sp
  newState.hero = ""
  SyncNewStateHero()
  UI.Refresh()
  end,
  })
  chip:SetPoint("TOPLEFT", (i - 1) * 116, 0)
  chip:SetActive(sp == newState.spec)
  ui.specChips[sp] = chip
  end
  ui.refreshHeroes()
  end

  function ui.refreshHeroes()
  for _, c in pairs(ui.heroChips or {}) do
  c:Hide()
  end
  ui.heroChips = {}
  SyncNewStateHero()
  local heroes = {}
  if SpellSmasherRebuild and SpellSmasherRebuild.ListHeroes then
  heroes = SpellSmasherRebuild.ListHeroes(newState.className, newState.spec) or {}
  end
  for i, h in ipairs(heroes) do
  local short = h
  if #short > 18 then
  short = short:sub(1, 16) .. "..."
  end
  local chip = MakeThemeButton(heroHost, {
  w = 175,
  h = 30,
  text = short,
  kind = "chip",
  tip = h,
  tipBody = "Hero talent path for Method / Engine packs (research: Midnight 2 paths per spec).",
  active = (h == newState.hero),
  onClick = function()
  newState.hero = h
  UI.Refresh()
  end,
  })
  chip:SetPoint("TOPLEFT", (i - 1) * 185, 0)
  chip:SetActive(h == newState.hero)
  ui.heroChips[h] = chip
  end
  end

  local methodBtn = MakeThemeButton(newPanel, {
  w = 200,
  h = 40,
  text = "Create Method pack",
  kind = "green",
  tip = "Method spell list + Method talent package",
  tipBody = "Uses the LIST pills: AoE = Method aoeSteps, ST = Method ST steps. Then Edit  >  Match talents or Copy > N > Import.",
  onClick = function()
  newState.source = "method"
  DoCreatePack()
  end,
  })
  methodBtn:SetPoint("TOPLEFT", roleHost, "BOTTOMLEFT", 0, -14)
  ui.methodCreateBtn = methodBtn

  local engineBtn = MakeThemeButton(newPanel, {
  w = 200,
  h = 40,
  text = "Engine (advanced)",
  kind = "secondary",
  tip = "SpellSmasher Engine mash order (advanced)",
  tipBody = "CD-aware mash order for the selected hero. Same ST/AoE pills as Method. Full starter list (not level-trimmed). Level-legal lists: spellsmasher.com.",
  onClick = function()
  newState.source = "engine"
  DoCreatePack()
  end,
  })
  engineBtn:SetPoint("LEFT", methodBtn, "RIGHT", 10, 0)
  ui.engineCreateBtn = engineBtn
  ui.classHost = classHost
  ui.heroHost = heroHost

  -- -- IMPORT --
  local importPanel = CreateFrame("Frame", nil, content)
  importPanel:SetAllPoints()
  importPanel:Hide()
  ui.importPanel = importPanel

  local impHead = importPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  impHead:SetPoint("TOPLEFT", 2, -2)
  impHead:SetText("Import / Export")
  SetFS(impHead, C.text)
  local impSub = importPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  impSub:SetPoint("TOPLEFT", impHead, "BOTTOMLEFT", 0, -4)
  impSub:SetText(
  "Paste |cff3dd68c!SpellSmasher!|r from the website  >  Import (ADDS packs, never wipes). Export fills this box - press |cffffff00Ctrl+C|r."
  )
  SetFS(impSub, C.muted)

  local impCard, eb = MakeScrollEdit(importPanel)
  impCard:SetPoint("TOPLEFT", 0, -40)
  impCard:SetPoint("BOTTOMRIGHT", 0, 48)
  importBox = eb

  local impGo = MakeThemeButton(importPanel, {
  w = 120,
  h = 36,
  text = "Import",
  kind = "green",
  onClick = DoImport,
  })
  impGo:SetPoint("BOTTOMLEFT", 0, 4)
  ui.importBtn = impGo
  ui.importBox = importBox

  local expGo = MakeThemeButton(importPanel, {
  w = 150,
  h = 36,
  text = "Export selected",
  kind = "primary",
  tip = "Export selected pack only",
  tipBody = "List selection (not only Active). Attaches live talents when class|spec matches you. Does not re-trim spells or rewrite pack level.",
  onClick = function()
  DoExport({ all = false })
  end,
  })
  expGo:SetPoint("LEFT", impGo, "RIGHT", 8, 0)
  ui.exportImpBtn = expGo

  local expAll = MakeThemeButton(importPanel, {
  w = 110,
  h = 36,
  text = "Export all",
  kind = "secondary",
  tip = "Export every pack",
  tipBody = "Full backup string. Pack levels and spell lists stay as stored.",
  onClick = function()
  DoExport({ all = true })
  end,
  })
  expAll:SetPoint("LEFT", expGo, "RIGHT", 8, 0)

  local clearImp = MakeThemeButton(importPanel, {
  w = 80,
  h = 36,
  text = "Clear",
  kind = "ghost",
  onClick = function()
  if importBox then
  importBox:SetText("")
  end
  end,
  })
  clearImp:SetPoint("LEFT", expAll, "RIGHT", 8, 0)

  -- -- EDIT --
  local editPanel = CreateFrame("Frame", nil, content, "BackdropTemplate")
  editPanel:SetAllPoints()
  editPanel:SetFrameLevel((content:GetFrameLevel() or 1) + 10)
  editPanel:Hide()
  Hex(editPanel, { C.bg[1], C.bg[2], C.bg[3], 0.88 }, C.border, 16, "panel")
  ui.editPanel = editPanel

  -- Fixed layout zones (real frames) so list never paints over header/talents/footer
  -- 1) header  2) talent  3) spell list (scroll)  4) footer
  local editHeader = CreateFrame("Frame", nil, editPanel, "BackdropTemplate")
  editHeader:SetPoint("TOPLEFT", 0, 0)
  editHeader:SetPoint("TOPRIGHT", 0, 0)
  editHeader:SetHeight(48)
  Hex(editHeader, { C.elevated[1], C.elevated[2], C.elevated[3], 0.90 }, C.border, 16, "panel")
  ui.editHeader = editHeader

  -- Pack name (editable) - type a new name + Enter to rename
  local packNameCard = MakeCard(editHeader)
  packNameCard:SetPoint("TOPLEFT", 8, -6)
  packNameCard:SetPoint("TOPRIGHT", -8, -6)
  packNameCard:SetHeight(22)
  local packNameEB = CreateFrame("EditBox", nil, packNameCard)
  packNameEB:SetPoint("TOPLEFT", 8, -2)
  packNameEB:SetPoint("BOTTOMRIGHT", -8, 2)
  packNameEB:SetFontObject(GameFontNormalLarge)
  packNameEB:SetTextColor(C.green[1], C.green[2], C.green[3])
  packNameEB:SetAutoFocus(false)
  packNameEB:SetScript("OnEscapePressed", function(self)
  local seq = NS.db and NS.db.sequences and NS.db.sequences[NS.db.editSeqIndex or 1]
  self:SetText((seq and seq.name) or "")
  self:ClearFocus()
  end)
  packNameEB:SetScript("OnEnterPressed", function(self)
  local seq = NS.db and NS.db.sequences and NS.db.sequences[NS.db.editSeqIndex or 1]
  if seq then
  local name = NS.Trim(self:GetText() or "")
  if name == "" then
  name = "Sequence"
  end
  seq.name = name
  self:SetText(name)
  SetStatus("|cff3dd68cRenamed:|r " .. name)
  UI.Refresh()
  end
  self:ClearFocus()
  end)
  packNameEB:SetScript("OnEditFocusLost", function(self)
  local seq = NS.db and NS.db.sequences and NS.db.sequences[NS.db.editSeqIndex or 1]
  if not seq then
  return
  end
  local name = NS.Trim(self:GetText() or "")
  if name == "" then
  name = seq.name or "Sequence"
  self:SetText(name)
  return
  end
  if name ~= (seq.name or "") then
  seq.name = name
  SetStatus("|cff3dd68cRenamed:|r " .. name)
  UI.Refresh()
  end
  end)
  ui.packNameBox = packNameEB
  ui.editTitle = packNameEB -- Refresh still sets text via editTitle API-ish

  ui.editSub = editHeader:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.editSub:SetPoint("TOPLEFT", packNameCard, "BOTTOMLEFT", 2, -4)
  ui.editSub:SetPoint("RIGHT", editHeader, "RIGHT", -10, 0)
  ui.editSub:SetJustifyH("LEFT")
  ui.editSub:SetWordWrap(false)
  if ui.editSub.SetMaxLines then
  ui.editSub:SetMaxLines(1)
  end
  SetFS(ui.editSub, C.muted)

  -- Talent zone: honesty hint + Match/Copy. Raw CUk stays hidden (Copy popup).
  local talentZone = CreateFrame("Frame", nil, editPanel, "BackdropTemplate")
  talentZone:SetPoint("TOPLEFT", editHeader, "BOTTOMLEFT", 0, -6)
  talentZone:SetPoint("TOPRIGHT", editHeader, "BOTTOMRIGHT", 0, -6)
  talentZone:SetHeight(72)
  Hex(talentZone, { C.elevated[1], C.elevated[2], C.elevated[3], 0.90 }, C.border, 16, "panel")
  ui.talentZone = talentZone

  ui.talentHint = talentZone:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.talentHint:SetPoint("TOPLEFT", 10, -6)
  ui.talentHint:SetPoint("TOPRIGHT", -10, -6)
  ui.talentHint:SetJustifyH("LEFT")
  ui.talentHint:SetWordWrap(true)
  if ui.talentHint.SetMaxLines then
  ui.talentHint:SetMaxLines(2)
  end
  ui.talentHint:SetText("|cff6c8cffTalent package|r")
  SetFS(ui.talentHint, C.gold)

  local function after(sec, fn)
  if C_Timer and C_Timer.After then
  C_Timer.After(sec, fn)
  elseif type(fn) == "function" then
  fn()
  end
  end

  --- One reusable copy dialog (never re-CreateFrame same global name - that errors).
  --- Do NOT call CopyToClipboard: it is #protected and can hard-error addon code.
  local function ShowCopyPopup(title, text)
  text = tostring(text or "")
  title = tostring(title or "Copy")

  local pop = ui.copyPopup
  if not pop then
  pop = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  ui.copyPopup = pop
  pop:SetSize(540, 220)
  pop:SetPoint("CENTER")
  pop:SetFrameStrata("FULLSCREEN_DIALOG")
  pop:SetFrameLevel(250)
  pop:EnableMouse(true)
  pop:SetMovable(true)
  pop:RegisterForDrag("LeftButton")
  pop:SetScript("OnDragStart", pop.StartMoving)
  pop:SetScript("OnDragStop", pop.StopMovingOrSizing)
  Hex(pop, { 0.02, 0.025, 0.04, 0.95 }, C.green, 32, "dialog")
  ApplyPanelImage(pop, PANEL_BG, { inset = 11, alpha = 0.90 })

  local tFS = pop:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  tFS:SetPoint("TOPLEFT", 14, -12)
  SetFS(tFS, C.green)
  pop.titleFS = tFS

  local hint = pop:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  hint:SetPoint("TOPLEFT", 14, -32)
  hint:SetWidth(510)
  hint:SetJustifyH("LEFT")
  hint:SetText(
  "|cff3dd68cHighlighted|r - press |cffffff00Ctrl+C|r once, then Talents  >  Import.\n"
  .. "Blizzard blocks silent clipboard from addons; Ctrl+C is required."
  )
  SetFS(hint, C.muted)

  local card = MakeCard(pop)
  card:SetPoint("TOPLEFT", 12, -72)
  card:SetPoint("BOTTOMRIGHT", -12, 48)
  pop.card = card

  -- Plain single-line EditBox (no template, no scroll) - most reliable for Ctrl+C
  local eb = CreateFrame("EditBox", nil, card)
  eb:SetPoint("TOPLEFT", 12, -14)
  eb:SetPoint("BOTTOMRIGHT", -12, 14)
  eb:SetAutoFocus(false)
  eb:SetFontObject(ChatFontNormal)
  eb:SetTextColor(C.text[1], C.text[2], C.text[3], 1)
  eb:SetTextInsets(8, 8, 4, 4)
  eb:SetMultiLine(false)
  eb:SetScript("OnEscapePressed", function()
  pop:Hide()
  end)
  eb:SetScript("OnEditFocusGained", function(self)
  self:HighlightText(0, -1)
  end)
  pop.edit = eb

  local close = MakeThemeButton(pop, {
  w = 90,
  h = 28,
  text = "Done",
  kind = "green",
  onClick = function()
  pop:Hide()
  end,
  })
  close:SetPoint("BOTTOMRIGHT", -12, 12)

  local reselect = MakeThemeButton(pop, {
  w = 120,
  h = 28,
  text = "Select all",
  kind = "primary",
  onClick = function()
  if pop.edit then
  pop.edit:SetFocus()
  pop.edit:HighlightText(0, -1)
  end
  end,
  })
  reselect:SetPoint("RIGHT", close, "LEFT", -8, 0)
  end

  pop.titleFS:SetText(title)
  local eb = pop.edit
  eb:SetText(text)
  pop:SetFrameStrata("FULLSCREEN_DIALOG")
  pop:SetFrameLevel(250)
  pop:Show()
  after(0, function()
  if ui.copyPopup and ui.copyPopup:IsShown() and eb then
  eb:SetFocus()
  eb:HighlightText(0, -1)
  end
  end)
  after(0.1, function()
  if ui.copyPopup and ui.copyPopup:IsShown() and eb then
  eb:SetFocus()
  eb:HighlightText(0, -1)
  end
  end)
  return pop
  end
  UI.ShowCopyPopup = ShowCopyPopup

  local function CopyTalentString()
  local ok, err = pcall(function()
  if not NS.db or type(NS.db.sequences) ~= "table" then
  SetStatus("|cffff6b7aNo packs loaded.|r")
  return
  end
  local idx = tonumber(NS.db.editSeqIndex) or tonumber(NS.db.activeIndex) or 1
  local seq = NS.db.sequences[idx]
  if not seq then
  SetStatus("|cfff0b429No pack selected.|r Open a pack from List  >  Edit.")
  return
  end
  local lo = NS.Trim(seq.talentLoadout or "")
  if #lo < 40 then
  lo = NS.Trim(seq.talents or "")
  end
  -- Extract CUk-like run (avoid Lua pattern + magic outside class)
  if #lo >= 40 then
  local m = string.match(lo, "([%w%+%/][%w%+%/]+=*)")
  if m and #m >= 40 then
  lo = m
  end
  end
  if #lo < 40 and NS.AttachMethodLoadout then
  if NS.AttachMethodLoadout(seq, seq.heroTalent) then
  lo = NS.Trim(seq.talentLoadout or "")
  end
  elseif #lo < 40 and SpellSmasherMethodLoadouts and SpellSmasherMethodLoadouts.Get then
  local fromMethod = SpellSmasherMethodLoadouts.Get(seq.className, seq.spec, seq.heroTalent)
  if fromMethod and #fromMethod >= 40 then
  lo = fromMethod
  seq.talentLoadout = lo
  seq.talents = lo
  seq.talentLoadoutSource = "method"
  end
  end
  if #lo < 40 then
  SetStatus(
  "|cfff0b429No talent string on this pack.|r Press |cffffff00Method|r then Copy."
  )
  return
  end

  -- Under max level: full Method CUk leaves free points after Import.
  -- Stage Method + dump leftovers > GenerateImportString > rollback (tree unchanged).
  local level = (NS.TalentLevel and NS.TalentLevel.GetPlayerLevel and NS.TalentLevel.GetPlayerLevel())
  or (UnitLevel and UnitLevel("player"))
  or 1
  level = tonumber(level) or 1
  local levelMax = (NS.TalentLevel and NS.TalentLevel.LEVEL_MAX) or 90
  local src = string.lower(tostring(seq.talentLoadoutSource or "method"))
  local isLive = src == "live"
  local copyNote = ""
  if level < levelMax and not isLive and NS.TalentLevel and NS.TalentLevel.BuildPointsValidMethodString then
  -- Prefer Method/Best package for scaling (same as Apply).
  local methodLo = lo
  if SpellSmasherMethodLoadouts and SpellSmasherMethodLoadouts.Get then
  local m = SpellSmasherMethodLoadouts.Get(seq.className, seq.spec, seq.heroTalent)
  if m and #m >= 40 then
  methodLo = m
  end
  end
  -- methodLo may be method-proxy for this hero; Copy still uses the package string
  local validStr, buildErr, detail = NS.TalentLevel.BuildPointsValidMethodString(methodLo)
  if validStr and #validStr >= 40 then
  lo = validStr
  detail = detail or {}
  copyNote = " level "
  .. tostring(detail.level or level)
  .. " points-valid"
  if detail.bought then
  copyNote = copyNote .. " (" .. tostring(detail.bought) .. " Method"
  if (detail.dumped or 0) > 0 then
  copyNote = copyNote .. " +" .. tostring(detail.dumped) .. " fill"
  end
  copyNote = copyNote .. ")"
  end
  if detail.freeLeft or (detail.currencyFree or 0) > 0 then
  copyNote = copyNote .. " - some free may remain; prefer Apply"
  end
  else
  copyNote = " full Method (could not scale: "
  .. tostring(buildErr or "?")
  .. ") - free points may remain; prefer Apply"
  end
  end

  if ui.talentBox then
  pcall(function()
  ui.talentBox:SetText(lo)
  end)
  end

  -- Never call CopyToClipboard - protected API can error hard.
  -- Popup: select text so user presses Ctrl+C once.
  local popupTitle = "Press Ctrl+C - then Talents Import"
  if level < levelMax and not isLive then
  popupTitle = "Ctrl+C - level " .. tostring(level) .. " points-valid Import"
  end
  ShowCopyPopup(popupTitle, lo)

  SetStatus(
  "|cff3dd68cReady|r ("
  .. #lo
  .. " chars"
  .. copyNote
  .. ") - press |cffffff00Ctrl+C|r, then Talents  >  Import."
  )
  NS.Print(
  "|cff3dd68cTalent string ready|r ("
  .. #lo
  .. " chars"
  .. copyNote
  .. "). Press |cffffff00Ctrl+C|r in the popup, then Talents  >  Import."
  )
  end)
  if not ok then
  SetStatus("|cffff6b7aCopy error:|r " .. tostring(err))
  NS.Print("|cffff6b7aCopy talent error:|r " .. tostring(err))
  -- Last-ditch: put text in talent box and select it
  pcall(function()
  local seq = NS.db and NS.db.sequences and NS.db.sequences[NS.db.editSeqIndex or 1]
  local lo = seq and NS.Trim(seq.talentLoadout or seq.talents or "") or ""
  if ui.talentBox and #lo >= 20 then
  ui.talentBox:SetText(lo)
  ui.talentBox:SetFocus()
  ui.talentBox:HighlightText(0, -1)
  SetStatus("|cfff0b429Use talent box|r - press |cffffff00Ctrl+C|r now.")
  end
  end)
  end
  end
  UI.CopyTalentString = CopyTalentString

  --- Internal fallback when MatchTalentsToList API is missing (no UI button).
  --- method-proxy packs get a confirm popup first (UX honesty - not a purchase rewrite).
  local function ApplyTalentsToGame(opts)
  opts = type(opts) == "table" and opts or {}
  local seqIdx = opts.editSeqIndex or NS.db.editSeqIndex or NS.db.activeIndex or 1
  local seq = NS.db.sequences[seqIdx]
  if not seq then
  SetStatus("|cfff0b429No pack selected.|r")
  return
  end
  local srcLow = string.lower(tostring(seq.talentLoadoutSource or ""))
  -- Live/custom never use Method-proxy confirm (even if an old proxy flag lingered on the pack).
  local isProxy = (srcLow ~= "live" and srcLow ~= "custom")
    and (
      (seq.talentLoadoutProxy and true)
      or srcLow == "method-proxy"
    )
  if isProxy and not opts.confirmedProxy then
  local heroLab = NS.Trim(seq.heroTalent or "")
  if heroLab == "" then
  heroLab = tostring(seq.spec or "this pack")
  end
  StaticPopup_Show("SPELLSMASHER_APPLY_PROXY", heroLab, nil, { editSeqIndex = seqIdx, matchTalents = true })
  SetStatus("|cfff0b429Shared Method talent (proxy)|r - confirm Match in the popup (hero nodes may need a check in N).")
  return
  end
  local ok, msg, detail = NS.ApplyPackTalentsToGame(seq)
  detail = detail or {}
  if ui.talentBox and seq.talentLoadout then
  pcall(function()
  ui.talentBox:SetText(seq.talentLoadout)
  end)
  end
  -- Prefer points-valid live string after partial purchase
  if detail.liveString and #tostring(detail.liveString) >= 40 and ui.talentBox then
  pcall(function()
  ui.talentBox:SetText(detail.liveString)
  end)
  end
  -- Defer list Refresh off the Apply click stack (empty tree > starter package
  -- path used to re-enter Secure recompile/icon while cast bar secrets update).
  local function refreshLater()
    if C_Timer and C_Timer.After then
      C_Timer.After(0.35, function()
        if UI.Refresh then
          pcall(UI.Refresh)
        end
      end)
    elseif UI.Refresh then
      pcall(UI.Refresh)
    end
  end
  if ok then
  SetStatus("|cff3dd68c" .. tostring(msg) .. "|r")
  -- TalentLevel already Print'd full message; avoid double chat spam
  refreshLater()
  return
  end
  -- Partial: buys landed but free points / Apply Changes still needed (yellow, not green).
  if detail.mode == "partial" then
  SetStatus("|cfff0b429" .. tostring(msg) .. "|r")
  refreshLater()
  return
  end
  -- Soft fail with optional Copy popup
  if detail.mode == "copy" and detail.importString and #tostring(detail.importString) >= 40 then
  ShowCopyPopup("Ctrl+C - then Talents Import", detail.importString)
  SetStatus("|cfff0b429" .. tostring(msg) .. "|r  -  Copy popup ready.")
  NS.Print(tostring(msg):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""))
  return
  end
  SetStatus("|cfff0b429" .. tostring(msg) .. "|r")
  NS.Print(tostring(msg):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""))
  end
  UI.ApplyTalentsToGame = ApplyTalentsToGame

  --- One-button: Method package for list > spend > verify nodes (Core.MatchTalentsToList).
  local function MatchTalentsToListClick(opts)
    if NS.IsClassicFlavor then
      SetStatus("|cfff0b429Match talents|r is Midnight-only. Classic mash list is separate from the talent tree.")
      return
    end
    opts = type(opts) == "table" and opts or {}
    local seqIdx = opts.editSeqIndex or NS.db.editSeqIndex or NS.db.activeIndex or 1
    local seq = NS.db.sequences[seqIdx]
    if not seq then
      SetStatus("|cfff0b429No pack selected.|r")
      return
    end
    local srcLow = string.lower(tostring(seq.talentLoadoutSource or ""))
    local isProxy = (srcLow ~= "live" and srcLow ~= "custom")
      and (
        (seq.talentLoadoutProxy and true)
        or srcLow == "method-proxy"
        or srcLow == ""
        or srcLow == "method"
      )
    -- Proxy confirm only when we will spend Method-shared gold (not Mine/custom).
    if isProxy and srcLow ~= "live" and srcLow ~= "custom" and not opts.confirmedProxy then
      local heroLab = NS.Trim(seq.heroTalent or "")
      if heroLab == "" then
        heroLab = tostring(seq.spec or "this pack")
      end
      StaticPopup_Show("SPELLSMASHER_APPLY_PROXY", heroLab, nil, {
        editSeqIndex = seqIdx,
        matchTalents = true,
      })
      SetStatus("|cfff0b429Shared Method talent (proxy)|r - confirm Match in the popup (hero nodes may need a check in N).")
      return
    end
    if not NS.MatchTalentsToList then
      ApplyTalentsToGame({ confirmedProxy = true, editSeqIndex = seqIdx })
      return
    end
    local ok, msg, detail = NS.MatchTalentsToList(seq, opts)
    detail = detail or {}
    if ui.talentBox and seq.talentLoadout then
      pcall(function()
        ui.talentBox:SetText(seq.talentLoadout)
      end)
    end
    if detail.liveString and #tostring(detail.liveString) >= 40 and ui.talentBox then
      pcall(function()
        ui.talentBox:SetText(detail.liveString)
      end)
    end
    local function refreshLater()
      if C_Timer and C_Timer.After then
        C_Timer.After(0.35, function()
          if UI.Refresh then
            pcall(UI.Refresh)
          end
        end)
      elseif UI.Refresh then
        pcall(UI.Refresh)
      end
    end
    local vs = detail.verifyStatus or (detail.verify and detail.verify.status)
    if ok and vs == "verified" then
      SetStatus("|cff3dd68cTalents matched|r - " .. tostring(msg))
      refreshLater()
      return
    end
    if ok then
      SetStatus("|cff3dd68c" .. tostring(msg) .. "|r")
      refreshLater()
      return
    end
    if detail.mode == "partial" then
      SetStatus("|cfff0b429Partial match|r - " .. tostring(msg))
      refreshLater()
      return
    end
    if detail.mode == "copy" and detail.importString and #tostring(detail.importString) >= 40 then
      ShowCopyPopup("Ctrl+C - then Talents Import", detail.importString)
      SetStatus("|cfff0b429Could not finish Match|r - Copy popup ready. " .. tostring(msg))
      NS.Print(tostring(msg):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""))
      return
    end
    -- Fail: offer pack string for manual Import
    local lo = NS.Trim(seq.talentLoadout or seq.talents or "")
    if #lo >= 40 then
      ShowCopyPopup("Ctrl+C - then Talents Import", lo)
      SetStatus("|cfff0b429Match failed|r - string ready for N  >  Import. " .. tostring(msg))
    else
      SetStatus("|cfff0b429" .. tostring(msg) .. "|r")
    end
    NS.Print(tostring(msg):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""))
  end
  UI.MatchTalentsToList = MatchTalentsToListClick

  -- Hidden loadout box (kept for Match/Copy fallbacks). Player sees Copy popup, not CUk soup.
  local talentCard = MakeCard(talentZone)
  talentCard:SetPoint("TOPLEFT", 8, -24)
  talentCard:SetPoint("TOPRIGHT", -8, -24)
  talentCard:SetHeight(26)
  talentCard:Hide()
  ui.talentCard = talentCard
  local talentEB = CreateFrame("EditBox", nil, talentCard)
  talentEB:SetPoint("TOPLEFT", 8, -4)
  talentEB:SetPoint("BOTTOMRIGHT", -8, 4)
  talentEB:SetFontObject(ChatFontNormal)
  talentEB:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
  talentEB:SetAutoFocus(false)
  talentEB:SetScript("OnEscapePressed", function(self)
  self:ClearFocus()
  self:HighlightText(0, 0)
  end)
  talentEB:SetScript("OnEditFocusGained", function(self)
  self:HighlightText(0, -1)
  end)
  talentEB:SetScript("OnMouseDown", function(self)
  self:SetFocus()
  self:HighlightText(0, -1)
  end)
  talentEB:SetScript("OnEnterPressed", function(self)
  local seq = NS.db.sequences[NS.db.editSeqIndex or 1]
  if seq then
  local t = NS.Trim(self:GetText() or "")
  seq.talentLoadout = t
  seq.talents = t
  -- Manual edit: Match will not re-attach Method gold over this custom string.
  if t ~= "" then
  seq.talentLoadoutSource = "custom"
  seq.talentLoadoutProxy = false
  seq.talentLoadoutHeroExact = false
  else
  seq.talentLoadoutSource = nil
  end
  SetStatus("|cff3dd68cTalent string saved on pack.|r")
  end
  self:ClearFocus()
  end)
  talentEB:SetScript("OnEditFocusLost", function(self)
  local seq = NS.db.sequences[NS.db.editSeqIndex or 1]
  if seq then
  local t = NS.Trim(self:GetText() or "")
  if t ~= "" then
  seq.talentLoadout = t
  seq.talents = t
  seq.talentLoadoutSource = "custom"
  seq.talentLoadoutProxy = false
  seq.talentLoadoutHeroExact = false
  end
  end
  end)
  ui.talentBox = talentEB

  -- Talent actions: Match - Copy only
  local talentMatch = MakeThemeButton(talentZone, {
    w = 118,
    h = 26,
    text = "Match talents",
    kind = "green",
    tip = "Match talents to this list",
    tipBody = "One step: Method package for this pack > spend on your tree > verify package nodes landed. Out of combat. Class/spec must match. Shared Method (proxy) asks confirm first. On fail: Copy popup for N > Import.",
    onClick = function()
      MatchTalentsToListClick()
    end,
  })
  talentMatch:SetPoint("TOPLEFT", talentZone, "TOPLEFT", 8, -40)
  ui.talentMatchBtn = talentMatch

  local talentCopy = MakeThemeButton(talentZone, {
    w = 56,
    h = 26,
    text = "Copy",
    kind = "secondary",
    tip = "Copy talent string",
    tipBody = "Ctrl+C > N > Import (manual fallback). Under 90 builds a points-valid string when possible.",
    onClick = CopyTalentString,
  })
  talentCopy:SetPoint("LEFT", talentMatch, "RIGHT", 4, 0)
  ui.talentCopyBtn = talentCopy

  function UI.UpdateTalentApplyEnabled()
  local btn = ui.talentMatchBtn
  if not btn then
  return
  end
  btn:Enable()
  btn:SetAlpha(1)
  if btn._opts then
  local lv = GetLivePlayerLevel()
  local seq = NS.db and NS.db.sequences and NS.db.sequences[NS.db.editSeqIndex or NS.db.activeIndex or 1]
  local srcTip = seq and string.lower(tostring(seq.talentLoadoutSource or "")) or ""
  local proxy = seq
    and srcTip ~= "live"
    and srcTip ~= "custom"
    and (
      seq.talentLoadoutProxy
      or srcTip == "method-proxy"
    )
  btn._opts.tip = proxy and "Match talents (shared Method proxy)" or "Match talents to this list"
  if lv >= 90 then
  btn._opts.tipBody = "Level 90: Method package > spend on tree > verify package nodes. Unique loadout name if slots allow."
    .. (proxy and " Shared Class|Spec gold - confirm popup, then check hero nodes in N." or "")
  else
  btn._opts.tipBody = "Level "
  .. tostring(lv)
  .. ": spends free points along the package; may be Partial (finish choice/hero in N)."
    .. (proxy and " Shared Method proxy - confirm first." or "")
  end
  end
  end
  UI.UpdateTalentApplyEnabled()

  function UI.LayoutClassChips()
    local host = ui.classHost
    if not host or not ui.classChips then
      return
    end
    local w = host:GetWidth() or 580
    if w < 80 then
      w = 580
    end
    local chipW, chipH, gapX, gapY = 82, 28, 4, 4
    local cols = math.max(1, math.floor((w + gapX) / (chipW + gapX)))
    local n = 0
    for _, cn in ipairs(NS.CLASSES) do
      local chip = ui.classChips[cn]
      if chip then
        local allow = (not NS.ClassAllowedOnFlavor) or NS.ClassAllowedOnFlavor(cn)
        if allow then
          local col = n % cols
          local row = math.floor(n / cols)
          chip:ClearAllPoints()
          chip:SetPoint("TOPLEFT", col * (chipW + gapX), -row * (chipH + gapY))
          chip:SetShown(true)
          chip:SetActive(cn == newState.className)
          n = n + 1
        else
          chip:Hide()
        end
      end
    end
    local rows = math.max(1, math.ceil(math.max(n, 1) / cols))
    host:SetHeight(rows * (chipH + gapY))
  end

  function UI.LayoutTabs()
    local bar = ui.tabBar
    if not bar then
      return
    end
    local barW = bar:GetWidth() or 600
    local narrow = barW < 580
    local helpW = narrow and 64 or 80
    local lostW = narrow and 56 or 120
    local recW = narrow and 86 or 100
    if ui.recompileBtn then
      ui.recompileBtn:SetWidth(recW)
    end
    if ui.lostSeqBtn then
      ui.lostSeqBtn:SetWidth(lostW)
      if ui.lostSeqBtn.SetThemeText then
        ui.lostSeqBtn:SetThemeText(narrow and "Lost?" or "Lost sequence?")
      end
    end
    if ui.helpTab then
      ui.helpTab:SetWidth(helpW)
    end
    local rightW = 16 + recW
    if ui.lostSeqBtn and ui.lostSeqBtn:IsShown() then
      rightW = rightW + 6 + lostW
    end
    if ui.helpTab and ui.helpTab:IsShown() then
      rightW = rightW + 6 + helpW
    end
    local leftBudget = barW - rightW - 20
    local tabW = math.floor((leftBudget - 18) / 4)
    tabW = math.max(48, math.min(88, tabW))
    for _, id in ipairs({ "list", "new", "import", "editor" }) do
      local b = tabButtons[id]
      if b then
        b:SetWidth(tabW)
      end
    end
  end

  function UI.ApplyFlavorChrome()
    local classic = NS.IsClassicFlavor and true or false
    if ui.setMeUpBtn then
      ui.setMeUpBtn:SetShown(true)
    end
    if ui.heroLabel then
      ui.heroLabel:SetShown(not classic)
    end
    if ui.heroHost then
      ui.heroHost:SetShown(not classic)
    end
    if ui.talentMatchBtn then
      ui.talentMatchBtn:SetShown(not classic)
    end
    if ui.talentCopyBtn then
      ui.talentCopyBtn:SetShown(not classic)
    end
    if classic and ui.talentHint then
      ui.talentHint:SetText("|cff8b96adClassic: no talent apply. Mash list only. Import or add spells in Edit.|r")
    end
    if ui.engineCreateBtn then
      ui.engineCreateBtn:SetShown(not classic)
    end
    if ui.methodCreateBtn then
      ui.methodCreateBtn:ClearAllPoints()
      if ui.roleHost then
        ui.methodCreateBtn:SetPoint("TOPLEFT", ui.roleHost, "BOTTOMLEFT", 0, -14)
      else
        ui.methodCreateBtn:SetPoint("TOPLEFT", 0, -220)
      end
      if classic then
        ui.methodCreateBtn:SetThemeText("Create Method pack")
        if ui.methodCreateBtn._opts then
          ui.methodCreateBtn._opts.tip = (NS.FlavorLabel and NS.FlavorLabel() or "Classic")
            .. " list for this class / spec"
          ui.methodCreateBtn._opts.tipBody = "Icy Veins / Wowhead mash list (ST or AoE from the LIST pills). No talent Apply on Classic flavors."
        end
      else
        ui.methodCreateBtn:SetThemeText("Create Method pack")
      end
    end
    if ui.newFromListBtn and ui.setMeUpBtn then
      ui.setMeUpBtn:ClearAllPoints()
      if ui.exportAllBtn then
        ui.setMeUpBtn:SetPoint("LEFT", ui.exportAllBtn, "RIGHT", 8, 0)
      end
      ui.newFromListBtn:ClearAllPoints()
      ui.newFromListBtn:SetPoint("LEFT", ui.setMeUpBtn, "RIGHT", 8, 0)
    end
    if classic and ui.emptyList and ui.emptyListClassic and (not NS.db or not NS.db.sequences or #NS.db.sequences == 0) then
      ui.emptyList:SetText(ui.emptyListClassic)
    end
    if ui.roleLabel and ui.specHost then
      ui.roleLabel:ClearAllPoints()
      if classic or (ui.heroHost and not ui.heroHost:IsShown()) then
        ui.roleLabel:SetPoint("TOPLEFT", ui.specHost, "BOTTOMLEFT", 0, -10)
      elseif ui.heroHost then
        ui.roleLabel:SetPoint("TOPLEFT", ui.heroHost, "BOTTOMLEFT", 0, -10)
      else
        ui.roleLabel:SetPoint("TOPLEFT", ui.specHost, "BOTTOMLEFT", 0, -10)
      end
    end
    if UI.LayoutClassChips then
      UI.LayoutClassChips()
    end
    if UI.LayoutTabs then
      UI.LayoutTabs()
    end
  end

  function UI.LayoutChrome()
    if UI.ApplyFlavorChrome then
      UI.ApplyFlavorChrome()
    end
  end
  UI.ApplyFlavorChrome()

  -- Footer (highest layer - name/macro never covered by spell rows)
  local EDIT_FOOTER_H = 112
  local editFooter = CreateFrame("Frame", nil, editPanel, "BackdropTemplate")
  editFooter:SetPoint("BOTTOMLEFT", 0, 0)
  editFooter:SetPoint("BOTTOMRIGHT", 0, 0)
  editFooter:SetHeight(EDIT_FOOTER_H)
  editFooter:SetFrameLevel((editPanel:GetFrameLevel() or 1) + 30)
  Hex(editFooter, { C.elevated[1], C.elevated[2], C.elevated[3], 0.90 }, C.border, 16, "panel")
  ui.editFooter = editFooter

  -- List zone fills only between talent zone and footer (clips children)
  local listZone = CreateFrame("Frame", nil, editPanel, "BackdropTemplate")
  listZone:SetPoint("TOPLEFT", talentZone, "BOTTOMLEFT", 0, -6)
  listZone:SetPoint("BOTTOMRIGHT", editFooter, "TOPRIGHT", 0, -6)
  listZone:SetFrameLevel((editPanel:GetFrameLevel() or 1) + 5)
  if listZone.SetClipsChildren then
  listZone:SetClipsChildren(true)
  end
  Hex(listZone, { C.bg[1], C.bg[2], C.bg[3], 0.88 }, C.border, 16, "panel")
  ui.listZone = listZone

  local stepHeadBar = CreateFrame("Frame", nil, listZone)
  stepHeadBar:SetPoint("TOPLEFT", 4, -2)
  stepHeadBar:SetPoint("TOPRIGHT", -4, -2)
  stepHeadBar:SetHeight(36)
  ui.stepHeadBar = stepHeadBar

  -- Line 1: SPELL LIST (n) - selected name  (width-clamped, no wrap overflow)
  local stepHead = stepHeadBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  stepHead:SetPoint("TOPLEFT", 2, -2)
  stepHead:SetPoint("TOPRIGHT", -2, -2)
  stepHead:SetJustifyH("LEFT")
  stepHead:SetJustifyV("MIDDLE")
  stepHead:SetWordWrap(false)
  stepHead:SetMaxLines(1)
  stepHead:SetText("SPELL LIST")
  SetFS(stepHead, C.muted)
  ui.stepHead = stepHead

  -- Line 2: short mode legend (fits one line)
  local stepHelp = stepHeadBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  stepHelp:SetPoint("TOPLEFT", stepHead, "BOTTOMLEFT", 0, -2)
  stepHelp:SetPoint("RIGHT", stepHeadBar, "RIGHT", -2, 0)
  stepHelp:SetJustifyH("LEFT")
  stepHelp:SetWordWrap(false)
  stepHelp:SetMaxLines(1)
  stepHelp:SetText("|cff8b96adOnce - In order - Priority - Rev Pri - xN loops|r")
  SetFS(stepHelp, C.muted)
  ui.stepHelp = stepHelp

  local stepScroll = CreateFrame("ScrollFrame", nil, listZone)
  stepScroll:SetPoint("TOPLEFT", stepHeadBar, "BOTTOMLEFT", 0, -2)
  stepScroll:SetPoint("BOTTOMRIGHT", listZone, "BOTTOMRIGHT", 0, 2)
  if stepScroll.SetClipsChildren then
  stepScroll:SetClipsChildren(true)
  end
  ui.stepScroll = stepScroll
  ui.stepHost = CreateFrame("Frame", nil, stepScroll)
  ui.stepHost:SetSize(560, 200)
  stepScroll:SetScrollChild(ui.stepHost)
  stepScroll:EnableMouseWheel(true)
  local function StepScrollMax(self)
  local hostH = (ui.stepHost and ui.stepHost:GetHeight()) or 0
  local viewH = self:GetHeight() or 1
  local maxS = math.max(0, hostH - viewH)
  if self.GetVerticalScrollRange then
  local r = self:GetVerticalScrollRange()
  -- Prefer the larger of computed vs engine range (engine can lag one frame low)
  if r and r > maxS then
  maxS = r
  end
  end
  return maxS
  end
  stepScroll:SetScript("OnMouseWheel", function(self, delta)
  local cur = self:GetVerticalScroll() or 0
  local maxS = StepScrollMax(self)
  local step = (ui._stepRowH or 56)
  -- one row per notch so ends land cleanly
  self:SetVerticalScroll(math.max(0, math.min(maxS, cur - delta * step)))
  end)
  stepScroll:SetScript("OnSizeChanged", function(self, w)
  if ui.stepHost and w and w > 0 then
  ui.stepHost:SetWidth(math.floor(w + 0.5))
  end
  end)

  -- -- Save label + macro into the selected step
  local suppressFieldApply = false
  function UI._setSuppressFieldApply(on)
  suppressFieldApply = on and true or false
  end
  local function IsSimpleCastMacro(mac, spellName)
  mac = NS.Trim(tostring(mac or ""))
  spellName = NS.Trim(tostring(spellName or ""))
  if mac == "" then
  return true
  end
  if spellName == "" then
  return false
  end
  local low = mac:lower():gsub("%s+", " ")
  local sp = spellName:lower()
  -- /cast ... SpellName  or  /use ... SpellName  (optional [conds])
  if not (low:find("^/cast") or low:find("^/use")) then
  return false
  end
  -- ends with the spell name (ignore trailing spaces)
  local after = low:match("%]%s*(.+)$") or low:match("^/cast%s+(.+)$") or low:match("^/use%s+(.+)$")
  after = NS.Trim(after or "")
  return after == sp
  end

  local function ApplyStepFields(opts)
  opts = opts or {}
  if suppressFieldApply and not opts.force then
  return false
  end
  local db = NS.db
  if not db then
  return false
  end
  local seq = db.sequences[db.editSeqIndex or 1]
  local idx = tonumber(opts.stepIndex) or tonumber(db.editStepIndex) or 1
  local st = seq and seq.steps and seq.steps[idx]
  if not st then
  return false
  end
  if not ui.stepLabelBox or not ui.stepMacroBox then
  return false
  end

  local newLabel = NS.Trim(ui.stepLabelBox:GetText() or "")
  local newMacro = NS.Trim(ui.stepMacroBox:GetText() or "")
  local oldLabel = NS.Trim(st.label or "")
  local oldMacro = NS.Trim(st.macro or "")

  if newLabel == "" and newMacro == "" then
  return false
  end

  -- Block pure talent passives - mash lists are cast/use only
  local checkLabel = newLabel ~= "" and newLabel or oldLabel
  local checkMacro = newMacro ~= "" and newMacro or oldMacro
  if NS.CastGuard and NS.CastGuard.IsPassiveSpellName and NS.CastGuard.IsPassiveSpellName(checkLabel) then
  if not (checkLabel and string.find(string.lower(checkLabel), "custom")) then
  SetStatus("|cfff0b429Not a castable spell|r (talent passive) - use a real /cast ability.")
  return false
  end
  end
  local packClass = seq and seq.className
  local packSpec = seq and seq.spec
  if NS.CastGuard and NS.CastGuard.IsSequenceable and checkLabel ~= "" then
  local macTry = checkMacro
  if macTry == "" and NS.CastGuard.CombatMacroFor then
  macTry = NS.CastGuard.CombatMacroFor(checkLabel, "") or ""
  end
  local tryId = tonumber(st.spellId or st.spellID)
  if not NS.CastGuard.IsSequenceable(checkLabel, macTry, tryId, packClass, packSpec) then
  SetStatus("|cfff0b429Not sequenceable|r - need /cast or /use (not a passive talent name).")
  return false
  end
  end

  if newLabel ~= "" then
  st.label = newLabel
  end

  -- Macro rules:
  -- 1) Prefer user macro text, but always Normalize (fixes @player forms).
  -- 2) Label-only rename: rewrite cast BUT keep [@player]/[@cursor]/mod: conditions.
  -- 3) Never strip unit targeting when re-saving a simple cast.
  local rewriteFromLabel = false
  if newLabel ~= "" then
  if newMacro == "" then
  rewriteFromLabel = true
  elseif opts.fromLabel and IsSimpleCastMacro(oldMacro, oldLabel) then
  rewriteFromLabel = true
  elseif opts.fromLabel and IsSimpleCastMacro(newMacro, oldLabel) then
  rewriteFromLabel = true
  elseif IsSimpleCastMacro(newMacro, oldLabel) and newLabel ~= oldLabel then
  rewriteFromLabel = true
  end
  end

  local function preserveConds(...)
  for i = 1, select("#", ...) do
  local src = select(i, ...)
  local c = NS.Compile.ExtractCastConditions(src)
  if c and c ~= "" then
  return c
  end
  end
  return nil
  end

  if rewriteFromLabel then
  -- Keep @player / @cursor / mod: from old or typed macro
  local conds = preserveConds(newMacro, oldMacro)
  -- Label like "Infernal Strike @player" also works
  st.macro = NS.Compile.BuildCastLine(st.label, conds)
  if not (ui.stepMacroBox.HasFocus and ui.stepMacroBox:HasFocus()) then
  ui.stepMacroBox:SetText(st.macro)
  end
  elseif newMacro ~= "" then
  -- Normalize so "/cast Infernal Strike @player" becomes proper [@player]
  st.macro = NS.Compile.NormalizeCombatCastLine(newMacro)
  if not (ui.stepMacroBox.HasFocus and ui.stepMacroBox:HasFocus()) then
  ui.stepMacroBox:SetText(st.macro)
  end
  if newLabel == "" then
  local fromMac = st.macro:match("%]%s*(.+)$") or st.macro:match("^/cast%s+(.+)$")
  if fromMac then
  fromMac = NS.Trim(fromMac:gsub("%s+@%w+%s*$", ""))
  st.label = fromMac
  if not (ui.stepLabelBox.HasFocus and ui.stepLabelBox:HasFocus()) then
  ui.stepLabelBox:SetText(st.label)
  end
  end
  end
  elseif newLabel ~= "" then
  st.macro = NS.Compile.BuildCastLine(st.label, preserveConds(oldMacro))
  end

  -- Stamp castable spellId for icons/tooltips (class|spec-aware; never passive)
  do
  local lab = NS.Trim(st.label or "")
  local sid = nil
  if NS.SpellIDMap and NS.SpellIDMap.GetSpellID and lab ~= "" then
  sid = NS.SpellIDMap.GetSpellID(lab, packClass, packSpec)
  end
  if sid and NS.CastGuard and NS.CastGuard.IsPassiveSpellId and NS.CastGuard.IsPassiveSpellId(sid) then
  sid = nil
  end
  st.spellId = sid
  st.spellID = nil
  end

  if not InCombatLockdown() then
  NS.Secure.Recompile()
  else
  NS.pendingRecompile = true
  end
  if opts.refresh ~= false then
  UI.Refresh()
  end
  if opts.status ~= false then
  SetStatus(
  "|cff3dd68cSaved|r  "
  .. tostring(st.label or "?")
  .. "  -  "
  .. tostring(st.macro or ""):gsub("\n", " ")
  )
  end
  return true
  end
  UI.ApplyStepFields = ApplyStepFields

  -- Footer row 1: selected step fields (clear short labels - no overlap)
  local labL = editFooter:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  labL:SetPoint("TOPLEFT", 10, -6)
  labL:SetText("Name")
  SetFS(labL, C.muted)

  local labCard = MakeCard(editFooter)
  labCard:SetPoint("TOPLEFT", 10, -20)
  labCard:SetSize(160, 28)
  local labEB = CreateFrame("EditBox", nil, labCard)
  labEB:SetPoint("TOPLEFT", 8, -4)
  labEB:SetPoint("BOTTOMRIGHT", -8, 4)
  labEB:SetFontObject(ChatFontNormal)
  labEB:SetTextColor(C.text[1], C.text[2], C.text[3])
  labEB:SetAutoFocus(false)
  labEB:SetScript("OnEnterPressed", function(self)
  ApplyStepFields({ fromLabel = true, force = true })
  self:ClearFocus()
  end)
  labEB:SetScript("OnEditFocusLost", function()
  ApplyStepFields({ fromLabel = true, refresh = true })
  end)
  labEB:SetScript("OnEscapePressed", function(self)
  local seq = NS.db.sequences[NS.db.editSeqIndex or 1]
  local st = seq and seq.steps and seq.steps[NS.db.editStepIndex or 1]
  if st then
  suppressFieldApply = true
  self:SetText(st.label or "")
  suppressFieldApply = false
  end
  self:ClearFocus()
  end)
  ui.stepLabelBox = labEB

  local macL = editFooter:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  macL:SetPoint("TOPLEFT", 180, -6)
  macL:SetText("Macro")
  SetFS(macL, C.muted)

  local macCard = MakeCard(editFooter)
  macCard:SetPoint("TOPLEFT", 180, -20)
  macCard:SetPoint("TOPRIGHT", -96, -20)
  macCard:SetHeight(28)
  local macEB = CreateFrame("EditBox", nil, macCard)
  macEB:SetPoint("TOPLEFT", 8, -4)
  macEB:SetPoint("BOTTOMRIGHT", -8, 4)
  macEB:SetFontObject(ChatFontNormal)
  macEB:SetTextColor(C.text[1], C.text[2], C.text[3])
  macEB:SetAutoFocus(false)
  macEB:SetScript("OnEnterPressed", function(self)
  ApplyStepFields({ fromLabel = false, force = true })
  self:ClearFocus()
  end)
  macEB:SetScript("OnEditFocusLost", function()
  ApplyStepFields({ fromLabel = false, refresh = true })
  end)
  macEB:SetScript("OnEscapePressed", function(self)
  local seq = NS.db.sequences[NS.db.editSeqIndex or 1]
  local st = seq and seq.steps and seq.steps[NS.db.editStepIndex or 1]
  if st then
  suppressFieldApply = true
  self:SetText(st.macro or "")
  suppressFieldApply = false
  end
  self:ClearFocus()
  end)
  ui.stepMacroBox = macEB

  local applyBtn = MakeThemeButton(editFooter, {
  w = 80,
  h = 28,
  text = "Apply",
  kind = "green",
  tip = "Save name + macro for the selected spell",
  tipBody = "Custom macros OK. Pro tip: /castsequence [nochanneling] Sunfire,Moonfire on a step set to Once works wonders (e.g. Boomkin DoTs) - walk both dots on early presses, then that step is done for the fight. One spell per mash; sequence advances each press.",
  onClick = function()
  ApplyStepFields({ fromLabel = true, force = true })
  end,
  })
  applyBtn:SetPoint("TOPRIGHT", -10, -20)

  -- Macro field tip: castsequence is supported (player choice)
  macCard:EnableMouse(true)
  macCard:SetScript("OnEnter", function(self)
  GameTooltip:SetOwner(self, "ANCHOR_TOP")
  GameTooltip:AddLine("Macro", C.green[1], C.green[2], C.green[3])
  GameTooltip:AddLine(
  "Simple /cast or full macros. Pro tip: /castsequence [nochanneling] Sunfire,Moonfire + mode Once does wonders (Boomkin DoTs) - early presses walk the sequence, then Once is done for the fight. One spell per mash press.",
  1,
  1,
  1,
  true
  )
  GameTooltip:Show()
  end)
  macCard:SetScript("OnLeave", function()
  GameTooltip:Hide()
  end)

  -- ---------- Spell picker (choose from known castables - not type-only) ----------
  local function CollectKnownCastableSpells()
  local list, seen = {}, {}
  local function add(name, spellId)
  name = NS.Trim(tostring(name or ""))
  if name == "" then
  return
  end
  local key = string.lower(name)
  if seen[key] then
  return
  end
  if NS.CastGuard then
  if NS.CastGuard.IsPassiveSpellName and NS.CastGuard.IsPassiveSpellName(name) then
  return
  end
  if spellId and NS.CastGuard.IsPassiveSpellId and NS.CastGuard.IsPassiveSpellId(spellId) then
  return
  end
  end
  -- Prefer live known / player spell when we have an id
  if spellId then
  local known = true
  if IsPlayerSpell or IsSpellKnownOrOverridesKnown or IsSpellKnown then
  known = false
  if IsPlayerSpell and IsPlayerSpell(spellId) then
  known = true
  elseif IsSpellKnownOrOverridesKnown and IsSpellKnownOrOverridesKnown(spellId) then
  known = true
  elseif IsSpellKnown and IsSpellKnown(spellId) then
  known = true
  end
  end
  if not known then
  return
  end
  end
  seen[key] = true
  list[#list + 1] = { name = name, spellId = spellId }
  end

  -- Modern spellbook API
  pcall(function()
  if not (C_SpellBook and C_SpellBook.GetNumSpellBookSkillLines) then
  return
  end
  local bank = (Enum and Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Player) or 0
  local nLines = C_SpellBook.GetNumSpellBookSkillLines() or 0
  for line = 1, nLines do
  local info = C_SpellBook.GetSpellBookSkillLineInfo(line)
  if info and info.itemIndexOffset and info.numSpellBookItems then
  local off = info.itemIndexOffset or 0
  local n = info.numSpellBookItems or 0
  for i = off + 1, off + n do
  local item = C_SpellBook.GetSpellBookItemInfo(i, bank)
  if item then
  local sid = item.spellID or item.actionID
  local sname = item.name
  if (not sname or sname == "") and sid and C_Spell and C_Spell.GetSpellName then
  sname = C_Spell.GetSpellName(sid)
  end
  local itype = item.itemType or item.spellBookItemType
  local futureEnum = Enum and Enum.SpellBookItemType and Enum.SpellBookItemType.FutureSpell
  local isFuture = (futureEnum and itype == futureEnum) or itype == "FUTURESPELL"
  local isSpell = true
  if Enum and Enum.SpellBookItemType then
  isSpell = (itype == Enum.SpellBookItemType.Spell or itype == nil)
  end
  if isSpell and sname and not isFuture then
  add(sname, sid)
  end
  end
  end
  end
  end
  end)

  -- Legacy tabs fallback
  pcall(function()
  if not GetNumSpellTabs or not GetSpellTabInfo then
  return
  end
  local book = (BOOKTYPE_SPELL) or "spell"
  local tabs = GetNumSpellTabs() or 0
  for t = 1, tabs do
  local _, _, offset, numSpells = GetSpellTabInfo(t)
  offset = offset or 0
  numSpells = numSpells or 0
  for i = offset + 1, offset + numSpells do
  local sname, _rank = GetSpellBookItemName and GetSpellBookItemName(i, book)
  local stype, sid = nil, nil
  if GetSpellBookItemInfo then
  stype, sid = GetSpellBookItemInfo(i, book)
  end
  if (not stype or stype == "SPELL") and sname and stype ~= "FUTURESPELL" then
  add(sname, sid)
  end
  end
  end
  end)

  table.sort(list, function(a, b)
  return tostring(a.name):lower() < tostring(b.name):lower()
  end)
  return list
  end

  local function EnsureSpellPicker()
  if ui.spellPicker then
  return ui.spellPicker
  end
  local pop = CreateFrame("Frame", "SpellSmasherSpellPicker", UIParent, "BackdropTemplate")
  pop:SetSize(360, 420)
  pop:SetPoint("CENTER", 0, 40)
  pop:SetFrameStrata("DIALOG")
  pop:SetFrameLevel(200)
  pop:EnableMouse(true)
  pop:SetMovable(true)
  pop:RegisterForDrag("LeftButton")
  pop:SetScript("OnDragStart", pop.StartMoving)
  pop:SetScript("OnDragStop", pop.StopMovingOrSizing)
  Hex(pop, { C.bg[1], C.bg[2], C.bg[3], 0.98 }, C.border, 14, "panel")
  pop:Hide()

  local title = pop:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
  title:SetPoint("TOPLEFT", 14, -12)
  title:SetText("Pick a spell")
  SetFS(title, C.green)

  local sub = pop:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  sub:SetPoint("TOPLEFT", 14, -32)
  sub:SetPoint("RIGHT", -40, 0)
  sub:SetJustifyH("LEFT")
  sub:SetText("Your known castable spells. Click one to add it to the list.")
  SetFS(sub, C.muted)

  local closeB = MakeThemeButton(pop, {
  w = 28,
  h = 28,
  text = "X",
  kind = "danger",
  tip = "Close",
  onClick = function()
  pop:Hide()
  end,
  })
  closeB:SetPoint("TOPRIGHT", -8, -8)

  local searchCard = MakeCard(pop)
  searchCard:SetPoint("TOPLEFT", 12, -54)
  searchCard:SetPoint("TOPRIGHT", -12, -54)
  searchCard:SetHeight(28)
  local searchEB = CreateFrame("EditBox", nil, searchCard)
  searchEB:SetPoint("TOPLEFT", 8, -4)
  searchEB:SetPoint("BOTTOMRIGHT", -8, 4)
  searchEB:SetFontObject(ChatFontNormal)
  searchEB:SetTextColor(C.text[1], C.text[2], C.text[3])
  searchEB:SetAutoFocus(false)
  searchEB:SetText("")
  local searchPh = searchCard:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
  searchPh:SetPoint("LEFT", 10, 0)
  searchPh:SetText("Search...")
  SetFS(searchPh, C.muted)

  local scroll = CreateFrame("ScrollFrame", nil, pop)
  scroll:SetPoint("TOPLEFT", 12, -90)
  scroll:SetPoint("BOTTOMRIGHT", -12, 48)
  local child = CreateFrame("Frame", nil, scroll)
  child:SetSize(320, 10)
  scroll:SetScrollChild(child)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(self, delta)
  local cur = self:GetVerticalScroll() or 0
  local maxs = self:GetVerticalScrollRange() or 0
  self:SetVerticalScroll(math.max(0, math.min(maxs, cur - delta * 36)))
  end)

  local rows = {}
  local allSpells = {}
  local function PaintRows(filter)
  filter = string.lower(NS.Trim(filter or ""))
  local y = 0
  local shown = 0
  for _, sp in ipairs(allSpells) do
  if filter == "" or string.find(string.lower(sp.name), filter, 1, true) then
  shown = shown + 1
  local row = rows[shown]
  if not row then
  row = MakeThemeButton(child, {
  w = 320,
  h = 28,
  text = "Spell",
  kind = "secondary",
  onClick = function(self)
  if pop._onPick and self._spell then
  pop._onPick(self._spell.name, self._spell.spellId)
  end
  pop:Hide()
  end,
  })
  rows[shown] = row
  end
  row._spell = sp
  row:SetThemeText(sp.name)
  row:ClearAllPoints()
  row:SetPoint("TOPLEFT", 0, -y)
  row:SetPoint("TOPRIGHT", 0, -y)
  row:Show()
  y = y + 30
  end
  end
  for i = shown + 1, #rows do
  rows[i]:Hide()
  end
  child:SetHeight(math.max(10, y))
  scroll:SetVerticalScroll(0)
  if shown == 0 then
  SetStatus("|cfff0b429No matching known spells.|r Try another search or type a name in Name + Apply.")
  end
  end

  searchEB:SetScript("OnTextChanged", function(self)
  local t = self:GetText() or ""
  if searchPh then
  if t == "" then
  searchPh:Show()
  else
  searchPh:Hide()
  end
  end
  PaintRows(t)
  end)
  searchEB:SetScript("OnEscapePressed", function(self)
  self:ClearFocus()
  pop:Hide()
  end)

  local customB = MakeThemeButton(pop, {
  w = 160,
  h = 28,
  text = "Type custom name...",
  kind = "ghost",
  tip = "Close picker and type in the Name box",
  onClick = function()
  pop:Hide()
  if ui.stepLabelBox then
  ui.stepLabelBox:SetFocus()
  end
  SetStatus("Type the spell name in |cff3dd68cName|r, then Apply.")
  end,
  })
  customB:SetPoint("BOTTOMLEFT", 12, 12)

  local countFS = pop:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  countFS:SetPoint("BOTTOMRIGHT", -14, 18)
  SetFS(countFS, C.muted)

  function pop:Open(onPick)
  self._onPick = onPick
  allSpells = CollectKnownCastableSpells()
  countFS:SetText(#allSpells .. " known")
  searchEB:SetText("")
  if searchPh then
  searchPh:Show()
  end
  PaintRows("")
  self:Show()
  self:Raise()
  end

  ui.spellPicker = pop
  return pop
  end

  local function AddSpellByName(name, spellId)
  name = NS.Trim(tostring(name or ""))
  if name == "" then
  return
  end
  ApplyStepFields({ refresh = false, status = false, force = true })
  local seq = NS.db.sequences[NS.db.editSeqIndex or 1]
  if not seq then
  return
  end
  seq.steps = seq.steps or {}
  local mac = "/cast [nochanneling] " .. name
  if NS.Compile and NS.Compile.BuildCastLine then
  mac = NS.Compile.BuildCastLine(name, "") or mac
  end
  local row = {
  label = name,
  macro = mac,
  mode = "priority",
  interval = 3,
  looped = true,
  spellId = spellId,
  }
  -- Insert under the currently selected spell (after it), not always at the bottom
  local n = #seq.steps
  local sel = math.floor(tonumber(NS.db.editStepIndex) or n)
  if sel < 0 then
  sel = 0
  end
  if sel > n then
  sel = n
  end
  local insertAt = sel + 1 -- under selected; if none/empty list > 1
  if insertAt < 1 then
  insertAt = 1
  end
  if insertAt > n + 1 then
  insertAt = n + 1
  end
  table.insert(seq.steps, insertAt, row)
  NS.db.editStepIndex = insertAt
  NS.Secure.Recompile()
  UI.Refresh()
  if sel >= 1 and sel <= n then
  local under = seq.steps[sel] and seq.steps[sel].label or ("#" .. tostring(sel))
  SetStatus("|cff3dd68cAdded|r " .. name .. " |cff8b96adunder|r " .. tostring(under))
  else
  SetStatus("|cff3dd68cAdded|r " .. name)
  end
  end

  -- Footer row 2: pack actions
  local addStep = MakeThemeButton(editFooter, {
  w = 100,
  h = 28,
  text = "+ Spell",
  kind = "secondary",
  tip = "Add a spell under the selected row",
  tipBody = "Opens your known castable spells. Inserts under the selected row. Or custom macro + Apply. Tip: /castsequence (e.g. Sunfire,Moonfire) with mode Once does wonders for paired openers.",
  onClick = function()
  local pick = EnsureSpellPicker()
  pick:Open(function(name, spellId)
  AddSpellByName(name, spellId)
  end)
  end,
  })
  addStep:SetPoint("BOTTOMLEFT", 10, 10)

  -- Pick button next to Name field (replace selected step's spell from list)
  local pickNameB = MakeThemeButton(editFooter, {
  w = 48,
  h = 28,
  text = "Pick",
  kind = "secondary",
  tip = "Choose spell from list",
  tipBody = "Sets the Name (and simple cast macro) for the selected row from your known spells.",
  onClick = function()
  local pick = EnsureSpellPicker()
  pick:Open(function(name, spellId)
  if not ui.stepLabelBox then
  return
  end
  suppressFieldApply = true
  ui.stepLabelBox:SetText(name)
  if ui.stepMacroBox then
  local mac = "/cast [nochanneling] " .. name
  if NS.Compile and NS.Compile.BuildCastLine then
  mac = NS.Compile.BuildCastLine(name, "") or mac
  end
  ui.stepMacroBox:SetText(mac)
  end
  suppressFieldApply = false
  local seq = NS.db.sequences[NS.db.editSeqIndex or 1]
  local st = seq and seq.steps and seq.steps[NS.db.editStepIndex or 1]
  if st and spellId then
  st.spellId = spellId
  end
  ApplyStepFields({ fromLabel = true, force = true })
  end)
  end,
  })
  pickNameB:SetPoint("LEFT", labCard, "RIGHT", 6, 0)
  -- Keep Name label/macro layout: Name card was 160 wide; Pick sits after it.
  -- Macro field starts at 180 - nudge if needed
  macL:ClearAllPoints()
  macL:SetPoint("TOPLEFT", 236, -6)
  macCard:ClearAllPoints()
  macCard:SetPoint("TOPLEFT", 236, -20)
  macCard:SetPoint("TOPRIGHT", -96, -20)

  ui.editRoleChips = {}
  for i, pair in ipairs({ { "aoe", "AoE" }, { "st", "ST" } }) do
  local rid, rtxt = pair[1], pair[2]
  local chip = MakeThemeButton(editFooter, {
  w = 44,
  h = 28,
  text = rtxt,
  kind = "chip",
  tip = rtxt == "ST" and "Single target Method list" or "AoE / packs Method list",
  tipBody = "Switches this pack to that Method/Engine list and rebuilds the spell order.",
  onClick = function()
  local db = NS.db
  local seq = db and db.sequences[db.editSeqIndex or db.activeIndex or 1]
  if not seq then
  return
  end
  seq.role = rid
  DoRebuild()
  end,
  })
  if i == 1 then
  chip:SetPoint("LEFT", addStep, "RIGHT", 8, 0)
  else
  chip:SetPoint("LEFT", ui.editRoleChips.aoe, "RIGHT", 6, 0)
  end
  ui.editRoleChips[rid] = chip
  end

  local rebuildB = MakeThemeButton(editFooter, {
  w = 90,
  h = 28,
  text = "Rebuild",
  kind = "secondary",
  tip = "Reload Method/Engine list (overwrites edits)",
  onClick = function()
  ApplyStepFields({ refresh = false, status = false, force = true })
  DoRebuild()
  end,
  })
  rebuildB:SetPoint("LEFT", ui.editRoleChips.st, "RIGHT", 8, 0)

  local saveB = MakeThemeButton(editFooter, {
  w = 72,
  h = 28,
  text = "Save",
  kind = "green",
  tip = "Save pack on this character",
  onClick = function()
  ApplyStepFields({ fromLabel = true, refresh = true, force = true })
  if not InCombatLockdown() then
  NS.Secure.Recompile()
  else
  NS.pendingRecompile = true
  end
  local seq = NS.db and NS.db.sequences and NS.db.sequences[NS.db.editSeqIndex or NS.db.activeIndex or 1]
  local n = seq and seq.steps and #seq.steps or 0
  SetStatus("|cff3dd68cSaved.|r  " .. n .. " spells. List  >  Export for backup.")
  end,
  })
  saveB:SetPoint("LEFT", rebuildB, "RIGHT", 8, 0)
  ui.saveBtn = saveB
  ui.rebuildBtn = rebuildB
  ui.addSpellBtn = addStep
  ui.stepApplyBtn = applyBtn
  ui.editFooter = editFooter

  local footerHint = editFooter:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  footerHint:SetPoint("LEFT", saveB, "RIGHT", 10, 0)
  footerHint:SetPoint("RIGHT", editFooter, "RIGHT", -10, 0)
  footerHint:SetJustifyH("LEFT")
  footerHint:SetText("|cff8b96adClick a row  -  /castsequence + Once does wonders (e.g. Sunfire,Moonfire)|r")
  SetFS(footerHint, C.muted)

  -- -- MASH KEY BAR (1-0) --
  local keyBar = MakeCard(f)
  keyBar:SetPoint("BOTTOMLEFT", 16, 120)
  keyBar:SetPoint("BOTTOMRIGHT", -16, 120)
  keyBar:SetHeight(56)
  Hex(keyBar, { C.elevated[1], C.elevated[2], C.elevated[3], 0.90 }, C.border, 16, "panel")
  ui.keyBar = keyBar

  local keyTitle = keyBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  keyTitle:SetPoint("TOPLEFT", 12, -6)
  keyTitle:SetText("|cff3dd68cMash key|r  |cff8b96ad -  click 1-0 to bind (key only - no bar macro)|r")
  SetFS(keyTitle, C.green)

  ui.keyStatus = keyBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.keyStatus:SetPoint("TOPRIGHT", -12, -6)
  ui.keyStatus:SetJustifyH("RIGHT")
  ui.keyStatus:SetText("|cfff0b429Not bound|r")
  SetFS(ui.keyStatus, C.muted)

  ui.keyBtns = {}
  local keyPrev = nil
  for _, key in ipairs(NS.Secure.ACTION_BAR_KEYS) do
  local kb = MakeThemeButton(keyBar, {
  w = 34,
  h = 24,
  text = key,
  kind = "secondary",
  tip = "Bind mash to key " .. key,
  tipBody = "Binds key "
  .. key
  .. " to SpellSmasher (CLICK). Does not put a macro on the action bar (that taints Midnight bars). Next spell shows on the floating SSM button.",
  onClick = function()
  local ok, msg = NS.Secure.BindMashToKey(key)
  SetStatus(ok and ("|cff3dd68c" .. msg .. "|r") or ("|cffff6b7a" .. msg .. "|r"))
  if ok then
  NS.Print(msg)
  -- Keybind-only (v4.3.27+): no bar macro. Next spell is the floating SSM button.
  NS.Print("Next spell = floating SSM button (drag green |cff3dd68cdrag|r tab to move). Mash with key " .. key .. ".")
  else
  NS.Print(msg)
  end
  UI.Refresh()
  end
  })
  if keyPrev then
  kb:SetPoint("LEFT", keyPrev, "RIGHT", 4, 0)
  else
  kb:SetPoint("BOTTOMLEFT", 12, 6)
  end
  ui.keyBtns[key] = kb
  keyPrev = kb
  end

  local clearKey = MakeThemeButton(keyBar, {
  w = 56,
  h = 24,
  text = "Clear",
  kind = "ghost",
  tip = "Remove mash keybind",
  onClick = function()
  local ok, msg = NS.Secure.UnbindMashKey()
  SetStatus(msg)
  NS.Print(msg)
  UI.Refresh()
  end,
  })
  clearKey:SetPoint("BOTTOMRIGHT", -90, 6)

  local moreKey = MakeThemeButton(keyBar, {
  w = 80,
  h = 24,
  text = "Any key",
  kind = "secondary",
  tip = "Bind any key in WoW options",
  tipBody = "Esc  >  Options  >  Keybindings  >  AddOns  >  SpellSmasher (one-button)",
  onClick = function()
  SetStatus("Esc  >  Options  >  Key Bindings  >  AddOns  >  SpellSmasher")
  NS.Print("Or click |cff3dd68c1-0|r above to bind an action-bar number.")
  if Settings and Settings.OpenToCategory then
  -- retail may not open bindings directly; message is enough
  end
  end,
  })
  moreKey:SetPoint("BOTTOMRIGHT", -8, 6)

  -- -- bottom helper bar --
  local helpBar = MakeCard(f)
  helpBar:SetPoint("BOTTOMLEFT", 16, 12)
  helpBar:SetPoint("BOTTOMRIGHT", -16, 12)
  helpBar:SetHeight(100)
  Hex(helpBar, { C.elevated[1], C.elevated[2], C.elevated[3], 0.90 }, C.border, 16, "panel")

  ui.aaBtn = MakeThemeButton(helpBar, {
  w = 78,
  h = 30,
  text = "AA  -  Off",
  kind = "secondary",
  tip = "Auto Attack helper",
  tipBody = "Off by default. When On, adds start-attack comfort when the macro still fits in 255 characters. Never invents extra combat spells.",
  onClick = function()
  NS.db.helperAutoAttack = not NS.db.helperAutoAttack
  NS.Secure.Recompile()
  UI.Refresh()
  end,
  })
  ui.aaBtn:SetPoint("TOPLEFT", 10, -10)

  ui.rtBtn = MakeThemeButton(helpBar, {
  w = 78,
  h = 30,
  text = "RT  -  Off",
  kind = "secondary",
  tip = "Retarget helper",
  tipBody = "Off by default. When On, helps pick a new enemy if dead/noharm - only when it fits the macro cap.",
  onClick = function()
  NS.db.helperRetarget = not NS.db.helperRetarget
  NS.Secure.Recompile()
  UI.Refresh()
  end,
  })
  ui.rtBtn:SetPoint("LEFT", ui.aaBtn, "RIGHT", 6, 0)

  ui.defBtn = MakeThemeButton(helpBar, {
  w = 78,
  h = 30,
  text = "DEF  -  Off",
  kind = "secondary",
  tip = "Defensive helper",
  tipBody = "Only if that spell is NOT already on the list.",
  onClick = function()
  NS.db.helperDefensive = not NS.db.helperDefensive
  NS.Secure.Recompile()
  UI.Refresh()
  end,
  })
  ui.defBtn:SetPoint("LEFT", ui.rtBtn, "RIGHT", 6, 0)

  ui.shiftBtn = MakeThemeButton(helpBar, {
  w = 110,
  h = 30,
  text = "Shift  -  ignore",
  kind = "secondary",
  tip = "Shift while mashing",
  tipBody = "ignore (default)  -  aoe  -  pause",
  onClick = function()
  local m = NS.db.shiftMode or "ignore"
  if m == "ignore" then
  NS.db.shiftMode = "aoe"
  elseif m == "aoe" then
  NS.db.shiftMode = "pause"
  else
  NS.db.shiftMode = "ignore"
  end
  NS.Secure.Recompile()
  UI.Refresh()
  end,
  })
  ui.shiftBtn:SetPoint("LEFT", ui.defBtn, "RIGHT", 6, 0)

  statusFS = helpBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  statusFS:SetPoint("TOPLEFT", 12, -54)
  statusFS:SetPoint("BOTTOMRIGHT", -12, 8)
  statusFS:SetJustifyH("LEFT")
  statusFS:SetJustifyV("TOP")
  statusFS:SetWordWrap(true)
  statusFS:SetNonSpaceWrap(true)
  if statusFS.SetMaxLines then
  statusFS:SetMaxLines(2)
  end
  SetFS(statusFS, C.green)
  SetStatus("Ready  -  |cffffff00Tutorial|r = full spotlight tour (Skip / Esc anytime)")

  -- -- Spotlight tour (theme-parity; SSM_Tour.lua) ---------------------------------
  -- Registers real controls, sample pack ensure, Help CTA flash/Replay.
  do
  local function EnsureTourSamplePack()
  local db = NS.db
  if not db then
  return false
  end
  db.sequences = db.sequences or {}
  if #db.sequences > 0 then
  -- Prefer pack matching player class
  local pClass, pSpec = NS.GetPlayerClassSpec()
  local pick = db.activeIndex or db.listSelectIndex or 1
  if pClass then
  for i, s in ipairs(db.sequences) do
  if s and NS.ClassSpecEquals and NS.ClassSpecEquals(s.className, s.spec, pClass, pSpec) then
  pick = i
  break
  elseif s and tostring(s.className or "") == tostring(pClass) then
  pick = i
  end
  end
  end
  db.listSelectIndex = pick
  db.editSeqIndex = pick
  if not db.activeIndex or db.activeIndex < 1 then
  db.activeIndex = pick
  end
  return true
  end
  if InCombatLockdown and InCombatLockdown() then
  SetStatus("|cfff0b429Tour:|r leave combat to auto-create a Method sample pack.")
  return false
  end
  if not SpellSmasherRebuild or not SpellSmasherRebuild.CreatePack then
  return false
  end
  SyncNewStateToPlayer()
  SyncNewStateHero()
  local seq, packId, msg = SpellSmasherRebuild.CreatePack(
  newState.className,
  newState.spec,
  newState.level,
  "method",
  newState.hero
  )
  if not seq then
  SetStatus("|cffff6b7aTour sample failed:|r " .. tostring(msg or packId))
  return false
  end
  -- Mark as tour sample (user may delete)
  seq.name = (seq.name or "Method") .. " (tour sample)"
  db.sequences[#db.sequences + 1] = seq
  local idx = #db.sequences
  db.activeIndex = idx
  db.listSelectIndex = idx
  db.editSeqIndex = idx
  db.editStepIndex = 1
  db.aoeIndex = nil
  if NS.Secure and NS.Secure.Recompile then
  NS.Secure.Recompile()
  end
  SetStatus("|cff3dd68cTour sample pack|r created - " .. tostring(seq.name))
  return true
  end

  if NS.Tour then
  NS.Tour.Bind({
  showTab = ShowTab,
  getUI = function()
  return ui
  end,
  getMain = function()
  return mainFrame
  end,
  setStatus = SetStatus,
  openMain = function()
  if UI.Open then
  UI.Open()
  elseif mainFrame then
  mainFrame:Show()
  end
  end,
  refresh = function()
  if UI.Refresh then
  UI.Refresh()
  end
  end,
  ensureSample = EnsureTourSamplePack,
  })

  NS.Tour.RegisterMany({
  help = helpTab,
  tabList = tabButtons.list,
  tabNew = tabButtons.new,
  tabImport = tabButtons.import,
  tabEdit = tabButtons.editor,
  listHead = ui.listHead,
  listFilter = ui.listFilterBtn,
  exportSelected = ui.exportSelectedBtn,
  newFromList = ui.newFromListBtn,
  setMeUp = ui.setMeUpBtn,
  roleHost = ui.roleHost,
  editRoleAoe = ui.editRoleChips and ui.editRoleChips.aoe,
  editRoleSt = ui.editRoleChips and ui.editRoleChips.st,
  methodCreate = ui.methodCreateBtn,
  engineCreate = ui.engineCreateBtn,
  classHost = ui.classHost,
  heroHost = ui.heroHost,
  importBtn = ui.importBtn,
  importBox = ui.importBox,
  exportImp = ui.exportImpBtn,
  packName = ui.packNameBox,
  editHeader = ui.editHeader,
  talentZone = ui.talentZone,
  talentMatch = ui.talentMatchBtn,
  talentCopy = ui.talentCopyBtn,
  saveBtn = ui.saveBtn,
  rebuildBtn = ui.rebuildBtn,
  addSpell = ui.addSpellBtn,
  stepApply = ui.stepApplyBtn,
  stepList = ui.stepScroll or ui.listZone,
  helperAA = ui.aaBtn,
  helperRT = ui.rtBtn,
  helperDEF = ui.defBtn,
  shiftBtn = ui.shiftBtn,
  keyBar = ui.keyBar,
  })

  NS.Tour.SyncHelpCta(helpTab)
  end

  function UI.ShowCoach(page)
  if not mainFrame then
  UI.Create()
  end
  if NS.Tour then
  NS.Tour.Start(tonumber(page) or 1)
  else
  SetStatus("|cffff6b7aTour module missing.|r")
  end
  end

  function UI.MaybeShowCoachOnOpen()
  if not NS.db then
  return
  end
  if NS.Tour then
  if NS.Tour.SyncHelpCta then
  NS.Tour.SyncHelpCta(ui.helpTab or helpTab)
  end
  NS.Tour.MaybeAutoShow()
  end
  end
  end

  -- Esc closes (only once)
  local already = false
  for _, name in ipairs(UISpecialFrames) do
  if name == "SpellSmasherMainFrame" then
  already = true
  break
  end
  end
  if not already then
  tinsert(UISpecialFrames, "SpellSmasherMainFrame")
  end
end
